Skip to content
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,9 @@ dependencies: [
### Binary HTTP Encoding

To serialise binary HTTP messages use `BHTTPSerializer.serialize(message, buffer)`.
As defined in [RFC9292](https://www.rfc-editor.org/rfc/rfc9292), you can choose to use either the known-length format or an indeterminate-length format. This choice can be configured during the initialization of the serializer by passing the desired type: `BHTTPSerializer(type: .knownLength)`.

To deserialise binary HTTP messages use `BHTTPParser`, adding received data with `append()`, then calling `completeBodyRecieved()`. The read the message parts received call `nextMessage()`.
To deserialise binary HTTP messages use `BHTTPParser`, adding received data with `append()`, then calling `completeBodyReceived()`. The read the message parts received call `nextMessage()`.

### Oblivious Encapsulation

Expand Down
271 changes: 238 additions & 33 deletions Sources/ObliviousHTTP/BHTTPSerializer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,45 +14,75 @@
import NIOCore
import NIOHTTP1

// For now this type is entirely stateless, which is achieved by using the indefinite-length encoding.
// It also means it does not enforce correctness, and so can produce invalid encodings if a user holds
// it wrong.
//
// Later optimizations can be made by adding more state into this type.
/// Binary HTTP serialiser as described in [RFC9292](https://www.rfc-editor.org/rfc/rfc9292).
/// Currently only indeterminate-length encoding is supported.
public struct BHTTPSerializer {

private var fsm: BHTTPSerializerFSM
public var type: SerializerType
private var chunkBuffer: ByteBuffer
private var fieldSectionBuffer: ByteBuffer

/// Initialise a Binary HTTP Serialiser.
public init() {}
/// - Parameters:
/// - type: The type of BHTTPSerializer you want: either known or indeterminate length.
/// - allocator: Byte buffer allocator used.
public init(
type: SerializerType = .indeterminateLength,
allocator: ByteBufferAllocator = ByteBufferAllocator()
) {
self.type = type
self.chunkBuffer = allocator.buffer(capacity: 0)
self.fieldSectionBuffer = allocator.buffer(capacity: 0)
self.fsm = BHTTPSerializerFSM(initialState: BHTTPSerializerState.start)
}

private var requestFramingIndicator: Int {
switch self.type {
case .knownLength:
return FramingIndicator.requestKnownLength
default:
return FramingIndicator.requestIndeterminateLength
}
}

private var responseFramingIndicator: Int {
switch self.type {
case .knownLength:
return FramingIndicator.responseKnownLength
default:
return FramingIndicator.responseIndeterminateLength
}
}

/// Serialise a message into a buffer using binary HTTP encoding.
/// - Parameters:
/// - message: The message to serialise. File regions are currently not supported.
/// - message: The message to serialise. File regions are currently not supported.
/// - buffer: Destination buffer to serialise into.
public func serialize(_ message: Message, into buffer: inout ByteBuffer) {
public mutating func serialize(_ message: Message, into buffer: inout ByteBuffer) throws {
switch message {
case .request(.head(let requestHead)):
Self.serializeRequestHead(requestHead, into: &buffer)
try self.fsm.writeRequestHead(requestHead, into: &buffer, using: &self)

case .response(.head(let responseHead)):
Self.serializeResponseHead(responseHead, into: &buffer)
try self.fsm.writeResponseHead(responseHead, into: &buffer, using: &self)

case .request(.body(.byteBuffer(let body))), .response(.body(.byteBuffer(let body))):
Self.serializeContentChunk(body, into: &buffer)
try self.fsm.writeBodyChunk(body, into: &buffer, using: &self)

case .request(.body(.fileRegion)), .response(.body(.fileRegion)):
fatalError("fileregion unsupported")
throw ObliviousHTTPError.unsupportedOption(reason: "fileregion unsupported")

case .request(.end(.some(let trailers))), .response(.end(.some(let trailers))):
// Send a 0 to terminate the body, then a field section.
buffer.writeInteger(UInt8(0))
Self.serializeIndeterminateLengthFieldSection(trailers, into: &buffer)
try self.fsm.writeTrailers(trailers, into: &buffer, using: &self)

case .request(.end(.none)), .response(.end(.none)):
// We can omit the trailers in this context, but we will always send a zero
// byte, either to communicate no trailers or no body.
buffer.writeInteger(UInt8(0))
try self.fsm.writeRequestEnd(into: &buffer, using: &self)
}
}

private static func serializeRequestHead(_ head: HTTPRequestHead, into buffer: inout ByteBuffer) {
// First, the framing indicator. 2 for indeterminate length request.
buffer.writeVarint(2)
private mutating func serializeRequestHead(_ head: HTTPRequestHead, into buffer: inout ByteBuffer) {
// First, the framing indicator
buffer.writeVarint(requestFramingIndicator)

let method = head.method
let scheme = "https" // Hardcoded for now, but not really the right option.
Expand All @@ -64,22 +94,57 @@ public struct BHTTPSerializer {
buffer.writeVarintPrefixedString(authority)
buffer.writeVarintPrefixedString(path)

Self.serializeIndeterminateLengthFieldSection(head.headers, into: &buffer)
switch self.type {
case .knownLength:
self.stackKnownLengthFieldSection(head.headers)
self.serializeKnownLengthFieldSection(into: &buffer)
break
default:
Self.serializeIndeterminateLengthFieldSection(head.headers, into: &buffer)
}
}

private static func serializeResponseHead(_ head: HTTPResponseHead, into buffer: inout ByteBuffer) {
// First, the framing indicator. 3 for indeterminate length response.
buffer.writeVarint(3)
private mutating func serializeResponseHead(_ head: HTTPResponseHead, into buffer: inout ByteBuffer) {
// First, the framing indicator
buffer.writeVarint(responseFramingIndicator)

buffer.writeVarint(Int(head.status.code))
Self.serializeIndeterminateLengthFieldSection(head.headers, into: &buffer)

switch self.type {
case .knownLength:
self.stackKnownLengthFieldSection(head.headers)
self.serializeKnownLengthFieldSection(into: &buffer)
break
default:
Self.serializeIndeterminateLengthFieldSection(head.headers, into: &buffer)
}
}

private mutating func serializeChunk(_ chunk: ByteBuffer, into buffer: inout ByteBuffer) {
switch self.type {
case .knownLength:
self.stackContentChunk(chunk)
break
default:
Self.serializeContentChunk(chunk, into: &buffer)
}
}

private static func serializeContentChunk(_ chunk: ByteBuffer, into buffer: inout ByteBuffer) {
// Omit zero-length chunks.
if chunk.readableBytes == 0 { return }
buffer.writeVarintPrefixedImmutableBuffer(chunk)
}

private mutating func serializeContent(into buffer: inout ByteBuffer) {
if self.chunkBuffer.readableBytes == 0 { return }
buffer.writeVarintPrefixedImmutableBuffer(self.chunkBuffer)
self.chunkBuffer.clear()
}

private mutating func stackContentChunk(_ chunk: ByteBuffer) {
self.chunkBuffer.writeImmutableBuffer(chunk)
}

private static func serializeIndeterminateLengthFieldSection(
_ fields: HTTPHeaders,
into buffer: inout ByteBuffer
Expand All @@ -88,18 +153,158 @@ public struct BHTTPSerializer {
buffer.writeVarintPrefixedString(name)
buffer.writeVarintPrefixedString(value)
}
// This is technically a varint but we can skip the check there because we know it can always encode in one byte.
buffer.writeInteger(UInt8(0))
buffer.writeInteger(UInt8(0)) // End of field section
}

private mutating func serializeTrailers(_ trailers: HTTPHeaders, into buffer: inout ByteBuffer) {
switch self.type {
case .knownLength:
self.serializeContent(into: &buffer)
self.stackKnownLengthFieldSection(trailers)
break
default:
// Send a 0 to terminate the body, then a field section.
buffer.writeInteger(UInt8(0))
Self.serializeIndeterminateLengthFieldSection(trailers, into: &buffer)
}
}

private mutating func serializeKnownLengthFieldSection(into buffer: inout ByteBuffer) {
buffer.writeVarintPrefixedImmutableBuffer(self.fieldSectionBuffer)
self.fieldSectionBuffer.clear()
}

private mutating func stackKnownLengthFieldSection(_ fields: HTTPHeaders) {
for (name, value) in fields {
self.fieldSectionBuffer.writeVarintPrefixedString(name)
self.fieldSectionBuffer.writeVarintPrefixedString(value)
}
}

private mutating func endRequest(into buffer: inout ByteBuffer) {
switch self.type {
case .knownLength:
self.serializeContent(into: &buffer)
self.serializeKnownLengthFieldSection(into: &buffer)
break
default:
buffer.writeInteger(UInt8(0))
}
}
}

// Enum definitions for message, states, and types.
extension BHTTPSerializer {
/// Types of message for binary http serilaisation
// Finite State Machine for managing transitions in BHTTPSerializer.
public class BHTTPSerializerFSM {
private(set) var currentState: BHTTPSerializerState

init(initialState: BHTTPSerializerState) {
self.currentState = initialState
}

func writeRequestHead(
_ requestHead: HTTPRequestHead,
into buffer: inout ByteBuffer,
using serializer: inout BHTTPSerializer
) throws {
try self.transition(to: .header)
serializer.serializeRequestHead(requestHead, into: &buffer)
}

func writeResponseHead(
_ responseHead: HTTPResponseHead,
into buffer: inout ByteBuffer,
using serializer: inout BHTTPSerializer
) throws {
try self.transition(to: .header)
serializer.serializeResponseHead(responseHead, into: &buffer)
}


func writeRequestEnd(
into buffer: inout ByteBuffer,
using serializer: inout BHTTPSerializer
) throws {
serializer.endRequest(into: &buffer)
try self.transition(to: .end)
}
func writeBodyChunk(
_ body: ByteBuffer,
into buffer: inout ByteBuffer,
using serializer: inout BHTTPSerializer
) throws {
serializer.serializeChunk(body, into: &buffer)
try self.transition(to: .chunk)
}

func writeTrailers(
_ trailers: HTTPHeaders,
into buffer: inout ByteBuffer,
using serializer: inout BHTTPSerializer
) throws {
serializer.serializeTrailers(trailers, into: &buffer)
try self.transition(to: .trailers)
}

func transition(to state: BHTTPSerializerState) throws {
let allowedNextStates: Set<BHTTPSerializerState>
switch currentState {
case .start:
allowedNextStates = [.header]
case .header:
allowedNextStates = [.chunk, .trailers, .end]
case .chunk:
allowedNextStates = [.trailers, .end, .chunk]
case .trailers:
allowedNextStates = [.trailers, .end]
case .end:
allowedNextStates = []
}
guard allowedNextStates.contains(state) else {
throw ObliviousHTTPError.unexpectedHTTPMessageSection()
}
currentState = state
}
}

public enum Message {
/// Part of an HTTP request.
case request(HTTPClientRequestPart)
/// Part of an HTTP response.
case response(HTTPServerResponsePart)
}

public struct SerializerType: Equatable {
private enum InternalType: Equatable {
case knownLength
case indeterminateLength
}

private let type: InternalType

public static let knownLength = SerializerType(type: .knownLength)
public static let indeterminateLength = SerializerType(type: .indeterminateLength)

private init(type: InternalType) {
self.type = type
}

public static func == (lop: SerializerType, rop: SerializerType) -> Bool {
lop.type == rop.type
}
}

internal struct FramingIndicator {
static var requestKnownLength: Int { 0 }
static var responseKnownLength: Int { 1 }
static var requestIndeterminateLength: Int { 2 }
static var responseIndeterminateLength: Int { 3 }
}

public enum BHTTPSerializerState {
case start
case header
case chunk
case trailers
case end
}
}
21 changes: 20 additions & 1 deletion Sources/ObliviousHTTP/Errors.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,30 @@ public struct ObliviousHTTPError: Error, Hashable {
Self.init(backing: .truncatedEncoding(reason: reason))
}

/// Create an error indicating that parsing faileud due to an unexpected HTTP status code.
/// Create an error indicating that parsing failed due to an unexpected HTTP status code.
/// - Parameter status: The status code encountered.
/// - Returns: An Error representing this failure.
@inline(never)
public static func invalidStatus(status: Int) -> ObliviousHTTPError {
Self.init(backing: .invalidStatus(status: status))
}

/// Create an error indicating that serializing failed due to an unexpected HTTP section.
/// - Parameter state: The state encountered.
/// - Returns: An Error representing this failure.
@inline(never)
public static func unexpectedHTTPMessageSection() -> ObliviousHTTPError {
Self.init(backing: .unexpectedHTTPMessageSection(state: "An unexpected HTTP message section was encountered."))
}

/// Create an error indicating that serializing failed due to an unsupported option.
/// - Parameter reason: The unsupported option details.
/// - Returns: An Error representing this failure.
@inline(never)
public static func unsupportedOption(reason: String) -> ObliviousHTTPError {
Self.init(backing: .unsupportedOption(reason: reason))
}

}

extension ObliviousHTTPError {
Expand All @@ -59,5 +76,7 @@ extension ObliviousHTTPError {
case invalidFieldSection(reason: String)
case truncatedEncoding(reason: String)
case invalidStatus(status: Int)
case unexpectedHTTPMessageSection(state: String)
case unsupportedOption(reason: String)
}
}
Loading