Skip to content

Commit

Permalink
Add HTTP2Frame.FramePayload codecs
Browse files Browse the repository at this point in the history
Motivation:

As part of the work for apple#214 we need new codecs which transform
`HTTP2Frame.FramePayload` to and from the appropriate request and
response parts.

Modifications:

- Add `HTTP2FramePayloadToHTTP1ClientCodec`
- Add `HTTP2FramePayloadToHTTP1ServerCodec`
- Duplicate the HTTP2ToHTTP1CodecTests and update the relevant parts to
  use payloads instead of frames.
- Add relevant test helpers.
- Note: the HTTP2 to HTTP1 (frame based) codecs haven't been deprecated:
  doing so without warnings depends on apple#221.

Result:

- We can transform `HTTP2Frame.FramePayload` types to the relevant HTTP
  client and server request and response types.
  • Loading branch information
glbrntt committed Jul 30, 2020
1 parent cde820e commit 64c5631
Show file tree
Hide file tree
Showing 7 changed files with 1,005 additions and 54 deletions.
130 changes: 125 additions & 5 deletions Sources/NIOHTTP2/HTTP2ToHTTP1Codec.swift
Expand Up @@ -16,6 +16,7 @@ import NIO
import NIOHTTP1
import NIOHPACK

// MARK: - Client

fileprivate struct BaseClientCodec {
private let protocolString: String
Expand All @@ -32,7 +33,7 @@ fileprivate struct BaseClientCodec {
/// HTTP/2 spec) and remove headers that are unsuitable for HTTP/2 such as
/// headers related to HTTP/1's keep-alive behaviour. Unless you are sure that all your
/// headers conform to the HTTP/2 spec, you should leave this parameter set to `true`.
fileprivate init(httpProtocol: HTTP2ToHTTP1ClientCodec.HTTPProtocol, normalizeHTTPHeaders: Bool) {
fileprivate init(httpProtocol: HTTP2FramePayloadToHTTP1ClientCodec.HTTPProtocol, normalizeHTTPHeaders: Bool) {
self.normalizeHTTPHeaders = normalizeHTTPHeaders

switch httpProtocol {
Expand Down Expand Up @@ -109,10 +110,7 @@ public final class HTTP2ToHTTP1ClientCodec: ChannelInboundHandler, ChannelOutbou
public typealias OutboundOut = HTTP2Frame

/// The HTTP protocol scheme being used on this connection.
public enum HTTPProtocol {
case https
case http
}
public typealias HTTPProtocol = HTTP2FramePayloadToHTTP1ClientCodec.HTTPProtocol

private let streamID: HTTP2StreamID
private var baseCodec: BaseClientCodec
Expand Down Expand Up @@ -170,6 +168,71 @@ public final class HTTP2ToHTTP1ClientCodec: ChannelInboundHandler, ChannelOutbou
}
}

/// A simple channel handler that translates HTTP/2 concepts into HTTP/1 data types,
/// and vice versa, for use on the client side.
///
/// This channel handler should be used alongside the `HTTP2StreamMultiplexer` to
/// help provide a HTTP/1.1-like abstraction on top of a HTTP/2 multiplexed
/// connection.
///
/// This handler uses `HTTP2Frame.FramePayload` as it's HTTP/2 currency type.
public final class HTTP2FramePayloadToHTTP1ClientCodec: ChannelInboundHandler, ChannelOutboundHandler {
public typealias InboundIn = HTTP2Frame.FramePayload
public typealias InboundOut = HTTPClientResponsePart

public typealias OutboundIn = HTTPClientRequestPart
public typealias OutboundOut = HTTP2Frame.FramePayload

private var baseCodec: BaseClientCodec

/// The HTTP protocol scheme being used on this connection.
public enum HTTPProtocol {
case https
case http
}

/// Initializes a `HTTP2PayloadToHTTP1ClientCodec`.
///
/// - parameters:
/// - httpProtocol: The protocol (usually `"http"` or `"https"` that is used).
/// - normalizeHTTPHeaders: Whether to automatically normalize the HTTP headers to be suitable for HTTP/2.
/// The normalization will for example lower-case all heder names (as required by the
/// HTTP/2 spec) and remove headers that are unsuitable for HTTP/2 such as
/// headers related to HTTP/1's keep-alive behaviour. Unless you are sure that all your
/// headers conform to the HTTP/2 spec, you should leave this parameter set to `true`.
public init(httpProtocol: HTTPProtocol, normalizeHTTPHeaders: Bool = true) {
self.baseCodec = BaseClientCodec(httpProtocol: httpProtocol, normalizeHTTPHeaders: normalizeHTTPHeaders)
}

public func channelRead(context: ChannelHandlerContext, data: NIOAny) {
let payload = self.unwrapInboundIn(data)
do {
let (first, second) = try self.baseCodec.processInboundData(payload)
if let first = first {
context.fireChannelRead(self.wrapInboundOut(first))
}
if let second = second {
context.fireChannelRead(self.wrapInboundOut(second))
}
} catch {
context.fireErrorCaught(error)
}
}

public func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise<Void>?) {
let responsePart = self.unwrapOutboundIn(data)

do {
let transformedPayload = try self.baseCodec.processOutboundData(responsePart, allocator: context.channel.allocator)
context.write(self.wrapOutboundOut(transformedPayload), promise: promise)
} catch {
promise?.fail(error)
context.fireErrorCaught(error)
}
}
}

// MARK: - Server

fileprivate struct BaseServerCodec {
private let normalizeHTTPHeaders: Bool
Expand Down Expand Up @@ -297,6 +360,63 @@ public final class HTTP2ToHTTP1ServerCodec: ChannelInboundHandler, ChannelOutbou
}
}

/// A simple channel handler that translates HTTP/2 concepts into HTTP/1 data types,
/// and vice versa, for use on the server side.
///
/// This channel handler should be used alongside the `HTTP2StreamMultiplexer` to
/// help provide a HTTP/1.1-like abstraction on top of a HTTP/2 multiplexed
/// connection.
///
/// This handler uses `HTTP2Frame.FramePayload` as it's HTTP/2 currency type.
public final class HTTP2FramePayloadToHTTP1ServerCodec: ChannelInboundHandler, ChannelOutboundHandler {
public typealias InboundIn = HTTP2Frame.FramePayload
public typealias InboundOut = HTTPServerRequestPart

public typealias OutboundIn = HTTPServerResponsePart
public typealias OutboundOut = HTTP2Frame.FramePayload

private var baseCodec: BaseServerCodec

/// Initializes a `HTTP2PayloadToHTTP1ServerCodec`.
///
/// - parameters:
/// - normalizeHTTPHeaders: Whether to automatically normalize the HTTP headers to be suitable for HTTP/2.
/// The normalization will for example lower-case all heder names (as required by the
/// HTTP/2 spec) and remove headers that are unsuitable for HTTP/2 such as
/// headers related to HTTP/1's keep-alive behaviour. Unless you are sure that all your
/// headers conform to the HTTP/2 spec, you should leave this parameter set to `true`.
public init(normalizeHTTPHeaders: Bool = true) {
self.baseCodec = BaseServerCodec(normalizeHTTPHeaders: normalizeHTTPHeaders)
}

public func channelRead(context: ChannelHandlerContext, data: NIOAny) {
let payload = self.unwrapInboundIn(data)

do {
let (first, second) = try self.baseCodec.processInboundData(payload)
if let first = first {
context.fireChannelRead(self.wrapInboundOut(first))
}
if let second = second {
context.fireChannelRead(self.wrapInboundOut(second))
}
} catch {
context.fireErrorCaught(error)
}
}

public func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise<Void>?) {
let responsePart = self.unwrapOutboundIn(data)

do {
let transformedPayload = try self.baseCodec.processOutboundData(responsePart, allocator: context.channel.allocator)
context.write(self.wrapOutboundOut(transformedPayload), promise: promise)
} catch {
promise?.fail(error)
context.fireErrorCaught(error)
}
}
}

private extension HTTPMethod {
/// Create a `HTTPMethod` from the string representation of that method.
Expand Down
1 change: 1 addition & 0 deletions Tests/LinuxMain.swift
Expand Up @@ -37,6 +37,7 @@ import XCTest
testCase(HPACKRegressionTests.allTests),
testCase(HTTP2FrameConvertibleTests.allTests),
testCase(HTTP2FrameParserTests.allTests),
testCase(HTTP2FramePayloadToHTTP1CodecTests.allTests),
testCase(HTTP2StreamMultiplexerTests.allTests),
testCase(HTTP2ToHTTP1CodecTests.allTests),
testCase(HeaderTableTests.allTests),
Expand Down
@@ -0,0 +1,66 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftNIO open source project
//
// Copyright (c) 2017-2018 Apple Inc. and the SwiftNIO project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//
//
// HTTP2FramePayloadToHTTP1CodecTests+XCTest.swift
//
import XCTest

///
/// NOTE: This file was generated by generate_linux_tests.rb
///
/// Do NOT edit this file directly as it will be regenerated automatically when needed.
///

extension HTTP2FramePayloadToHTTP1CodecTests {

static var allTests : [(String, (HTTP2FramePayloadToHTTP1CodecTests) -> () throws -> Void)] {
return [
("testBasicRequestServerSide", testBasicRequestServerSide),
("testRequestWithOnlyHeadServerSide", testRequestWithOnlyHeadServerSide),
("testRequestWithTrailers", testRequestWithTrailers),
("testSendingSimpleResponse", testSendingSimpleResponse),
("testResponseWithoutTrailers", testResponseWithoutTrailers),
("testResponseWith100Blocks", testResponseWith100Blocks),
("testPassingPromisesThroughWritesOnServer", testPassingPromisesThroughWritesOnServer),
("testBasicResponseClientSide", testBasicResponseClientSide),
("testResponseWithOnlyHeadClientSide", testResponseWithOnlyHeadClientSide),
("testResponseWithTrailers", testResponseWithTrailers),
("testSendingSimpleRequest", testSendingSimpleRequest),
("testRequestWithoutTrailers", testRequestWithoutTrailers),
("testResponseWith100BlocksClientSide", testResponseWith100BlocksClientSide),
("testPassingPromisesThroughWritesOnClient", testPassingPromisesThroughWritesOnClient),
("testReceiveRequestWithoutMethod", testReceiveRequestWithoutMethod),
("testReceiveRequestWithDuplicateMethod", testReceiveRequestWithDuplicateMethod),
("testReceiveRequestWithoutPath", testReceiveRequestWithoutPath),
("testReceiveRequestWithDuplicatePath", testReceiveRequestWithDuplicatePath),
("testReceiveRequestWithoutAuthority", testReceiveRequestWithoutAuthority),
("testReceiveRequestWithDuplicateAuthority", testReceiveRequestWithDuplicateAuthority),
("testReceiveRequestWithoutScheme", testReceiveRequestWithoutScheme),
("testReceiveRequestWithDuplicateScheme", testReceiveRequestWithDuplicateScheme),
("testReceiveResponseWithoutStatus", testReceiveResponseWithoutStatus),
("testReceiveResponseWithDuplicateStatus", testReceiveResponseWithDuplicateStatus),
("testReceiveResponseWithNonNumericalStatus", testReceiveResponseWithNonNumericalStatus),
("testSendRequestWithoutHost", testSendRequestWithoutHost),
("testSendRequestWithDuplicateHost", testSendRequestWithDuplicateHost),
("testFramesWithoutHTTP1EquivalentAreIgnored", testFramesWithoutHTTP1EquivalentAreIgnored),
("testWeTolerateUpperCasedHTTP1HeadersForRequests", testWeTolerateUpperCasedHTTP1HeadersForRequests),
("testWeTolerateUpperCasedHTTP1HeadersForResponses", testWeTolerateUpperCasedHTTP1HeadersForResponses),
("testWeDoNotNormalizeHeadersIfUserAskedUsNotToForRequests", testWeDoNotNormalizeHeadersIfUserAskedUsNotToForRequests),
("testWeDoNotNormalizeHeadersIfUserAskedUsNotToForResponses", testWeDoNotNormalizeHeadersIfUserAskedUsNotToForResponses),
("testWeStripIllegalHeadersAsWellAsTheHeadersNominatedByTheConnectionHeaderForRequests", testWeStripIllegalHeadersAsWellAsTheHeadersNominatedByTheConnectionHeaderForRequests),
("testWeStripIllegalHeadersAsWellAsTheHeadersNominatedByTheConnectionHeaderForResponses", testWeStripIllegalHeadersAsWellAsTheHeadersNominatedByTheConnectionHeaderForResponses),
]
}
}

0 comments on commit 64c5631

Please sign in to comment.