//===----------------------------------------------------------------------===// // // This source file is part of the SwiftNIO open source project // // Copyright (c) 2017-2021 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 // //===----------------------------------------------------------------------===// import NIOCore private func writeChunk( wrapOutboundOut: (IOData) -> NIOAny, context: ChannelHandlerContext, isChunked: Bool, chunk: IOData, promise: EventLoopPromise? ) { let readableBytes = chunk.readableBytes // we don't want to copy the chunk unnecessarily and therefore call write an annoyingly large number of times // we also don't frame empty chunks as they would otherwise end the response stream // we still need to write the empty IODate to complete the promise in the right order but is otherwise a no-op. if isChunked && readableBytes > 0 { let (mW1, mW2, mW3): (EventLoopPromise?, EventLoopPromise?, EventLoopPromise?) if let p = promise { // chunked encoding and the user's interested: we need three promises and need to cascade into the users promise let (w1, w2, w3) = ( context.eventLoop.makePromise() as EventLoopPromise, context.eventLoop.makePromise() as EventLoopPromise, context.eventLoop.makePromise() as EventLoopPromise ) w1.futureResult.and(w2.futureResult).and(w3.futureResult).map { (_: ((((), ()), ()))) in }.cascade(to: p) (mW1, mW2, mW3) = (w1, w2, w3) } else { // user isn't interested, let's not bother even allocating promises (mW1, mW2, mW3) = (nil, nil, nil) } var buffer = context.channel.allocator.buffer(capacity: 32) let len = String(readableBytes, radix: 16) buffer.writeString(len) buffer.writeStaticString("\r\n") context.write(wrapOutboundOut(.byteBuffer(buffer)), promise: mW1) context.write(wrapOutboundOut(chunk), promise: mW2) // Just move the buffers readerIndex to only make the \r\n readable and depend on COW semantics. buffer.moveReaderIndex(forwardBy: buffer.readableBytes - 2) context.write(wrapOutboundOut(.byteBuffer(buffer)), promise: mW3) } else { context.write(wrapOutboundOut(chunk), promise: promise) } } private func writeTrailers( wrapOutboundOut: (IOData) -> NIOAny, context: ChannelHandlerContext, isChunked: Bool, trailers: HTTPHeaders?, promise: EventLoopPromise? ) { switch (isChunked, promise) { case (true, let p): var buffer: ByteBuffer if let trailers = trailers { buffer = context.channel.allocator.buffer(capacity: 256) buffer.writeStaticString("0\r\n") buffer.write(headers: trailers) // Includes trailing CRLF. } else { buffer = context.channel.allocator.buffer(capacity: 8) buffer.writeStaticString("0\r\n\r\n") } context.write(wrapOutboundOut(.byteBuffer(buffer)), promise: p) case (false, .some(let p)): // Not chunked so we have nothing to write. However, we don't want to satisfy this promise out-of-order // so we issue a zero-length write down the chain. let buf = context.channel.allocator.buffer(capacity: 0) context.write(wrapOutboundOut(.byteBuffer(buf)), promise: p) case (false, .none): break } } // starting about swift-5.0-DEVELOPMENT-SNAPSHOT-2019-01-20-a, this doesn't get automatically inlined, which costs // 2 extra allocations so we need to help the optimiser out. @inline(__always) private func writeHead( wrapOutboundOut: (IOData) -> NIOAny, writeStartLine: (inout ByteBuffer) -> Void, context: ChannelHandlerContext, headers: HTTPHeaders, promise: EventLoopPromise? ) { var buffer = context.channel.allocator.buffer(capacity: 256) writeStartLine(&buffer) buffer.write(headers: headers) context.write(wrapOutboundOut(.byteBuffer(buffer)), promise: promise) } /// The type of framing that is used to mark the end of the body. private enum BodyFraming { case chunked case contentLength case neither } /// Adjusts the response/request headers to ensure that the response/request will be well-framed. /// /// This method strips Content-Length and Transfer-Encoding headers from responses/requests that must /// not have a body. It also adds Transfer-Encoding headers to responses/requests that do have bodies /// but do not have any other transport headers when using HTTP/1.1. This ensures that we can /// always safely reuse a connection. /// /// Note that for HTTP/1.0 if there is no Content-Length then the response should be followed /// by connection close. We require that the user send that connection close: we don't do it. private func correctlyFrameTransportHeaders( hasBody: HTTPMethod.HasBody, headers: inout HTTPHeaders, version: HTTPVersion ) -> BodyFraming { switch hasBody { case .no: headers.remove(name: "content-length") headers.remove(name: "transfer-encoding") return .neither case .yes: if headers.contains(name: "content-length") { return .contentLength } if version.major == 1 && version.minor >= 1 { headers.replaceOrAdd(name: "transfer-encoding", value: "chunked") return .chunked } else { return .neither } case .unlikely: if headers.contains(name: "content-length") { return .contentLength } if version.major == 1 && version.minor >= 1 { return headers["transfer-encoding"].first == "chunked" ? .chunked : .neither } return .neither } } /// Validates the response/request headers to ensure that we correctly send the body with chunked transfer /// encoding, when needed. private func messageIsChunked(headers: HTTPHeaders, version: HTTPVersion) -> Bool { if version.major == 1 && version.minor >= 1 { return headers.first(name: "transfer-encoding") == "chunked" ? true : false } else { return false } } /// A `ChannelOutboundHandler` that can serialize HTTP requests. /// /// This channel handler is used to translate messages from a series of /// `HTTPClientRequestPart` into the HTTP/1.1 wire format. public final class HTTPRequestEncoder: ChannelOutboundHandler, RemovableChannelHandler { public typealias OutboundIn = HTTPClientRequestPart public typealias OutboundOut = IOData /// Configuration for the ``HTTPRequestEncoder``. /// /// This object controls the behaviour of the ``HTTPRequestEncoder``. It enables users to /// change the default behaviour of the type to better handle a wide range of use-cases. public struct Configuration: Sendable, Hashable { /// Whether the ``HTTPRequestEncoder`` should automatically add `Content-Length` or /// `Transfer-Encoding` headers when appropriate. /// /// Defaults to `true`. /// /// Set to `false` if you are confident you are appropriately setting these framing /// headers yourself, to skip NIO's transformation. public var automaticallySetFramingHeaders: Bool public init() { self.automaticallySetFramingHeaders = true } } private var isChunked = false private var configuration: Configuration public convenience init() { self.init(configuration: Configuration()) } public init(configuration: Configuration) { self.configuration = configuration } public func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise?) { switch HTTPRequestEncoder.unwrapOutboundIn(data) { case .head(var request): assert( !(request.headers.contains(name: "content-length") && request.headers[canonicalForm: "transfer-encoding"].contains("chunked"[...])), "illegal HTTP sent: \(request) contains both a content-length and transfer-encoding:chunked" ) if self.configuration.automaticallySetFramingHeaders { self.isChunked = correctlyFrameTransportHeaders( hasBody: request.method.hasRequestBody, headers: &request.headers, version: request.version ) == .chunked } else { self.isChunked = messageIsChunked(headers: request.headers, version: request.version) } writeHead( wrapOutboundOut: HTTPRequestEncoder.wrapOutboundOut, writeStartLine: { buffer in buffer.write(request: request) }, context: context, headers: request.headers, promise: promise ) case .body(let bodyPart): writeChunk( wrapOutboundOut: HTTPRequestEncoder.wrapOutboundOut, context: context, isChunked: self.isChunked, chunk: bodyPart, promise: promise ) case .end(let trailers): writeTrailers( wrapOutboundOut: HTTPRequestEncoder.wrapOutboundOut, context: context, isChunked: self.isChunked, trailers: trailers, promise: promise ) } } } @available(*, unavailable) extension HTTPRequestEncoder: Sendable {} /// A `ChannelOutboundHandler` that can serialize HTTP responses. /// /// This channel handler is used to translate messages from a series of /// `HTTPServerResponsePart` into the HTTP/1.1 wire format. public final class HTTPResponseEncoder: ChannelOutboundHandler, RemovableChannelHandler { public typealias OutboundIn = HTTPServerResponsePart public typealias OutboundOut = IOData /// Configuration for the ``HTTPResponseEncoder``. /// /// This object controls the behaviour of the ``HTTPResponseEncoder``. It enables users to /// change the default behaviour of the type to better handle a wide range of use-cases. public struct Configuration: Sendable, Hashable { /// Whether the ``HTTPResponseEncoder`` should automatically add `Content-Length` or /// `Transfer-Encoding` headers when appropriate. /// /// Defaults to `true`. /// /// Set to `false` if you are confident you are appropriately setting these framing /// headers yourself, to skip NIO's transformation. public var automaticallySetFramingHeaders: Bool public init() { self.automaticallySetFramingHeaders = true } } private var isChunked = false private var configuration: Configuration public convenience init() { self.init(configuration: Configuration()) } public init(configuration: Configuration) { self.configuration = configuration } public func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise?) { switch HTTPResponseEncoder.unwrapOutboundIn(data) { case .head(var response): assert( !(response.headers.contains(name: "content-length") && response.headers[canonicalForm: "transfer-encoding"].contains("chunked"[...])), "illegal HTTP sent: \(response) contains both a content-length and transfer-encoding:chunked" ) if self.configuration.automaticallySetFramingHeaders { self.isChunked = correctlyFrameTransportHeaders( hasBody: response.status.mayHaveResponseBody ? .yes : .no, headers: &response.headers, version: response.version ) == .chunked } else { self.isChunked = messageIsChunked(headers: response.headers, version: response.version) } writeHead( wrapOutboundOut: HTTPResponseEncoder.wrapOutboundOut, writeStartLine: { buffer in buffer.write(response: response) }, context: context, headers: response.headers, promise: promise ) case .body(let bodyPart): writeChunk( wrapOutboundOut: HTTPResponseEncoder.wrapOutboundOut, context: context, isChunked: self.isChunked, chunk: bodyPart, promise: promise ) case .end(let trailers): writeTrailers( wrapOutboundOut: HTTPResponseEncoder.wrapOutboundOut, context: context, isChunked: self.isChunked, trailers: trailers, promise: promise ) } } } @available(*, unavailable) extension HTTPResponseEncoder: Sendable {} extension ByteBuffer { private mutating func write(status: HTTPResponseStatus) { self.writeString(String(status.code)) self.writeWhitespace() self.writeString(status.reasonPhrase) } fileprivate mutating func write(response: HTTPResponseHead) { switch (response.version.major, response.version.minor, response.status) { // Optimization for HTTP/1.0 case (1, 0, .custom(_, _)): self.writeStaticString("HTTP/1.0 ") self.write(status: response.status) self.writeStaticString("\r\n") case (1, 0, .continue): self.writeStaticString("HTTP/1.0 100 Continue\r\n") case (1, 0, .switchingProtocols): self.writeStaticString("HTTP/1.0 101 Switching Protocols\r\n") case (1, 0, .processing): self.writeStaticString("HTTP/1.0 102 Processing\r\n") case (1, 0, .ok): self.writeStaticString("HTTP/1.0 200 OK\r\n") case (1, 0, .created): self.writeStaticString("HTTP/1.0 201 Created\r\n") case (1, 0, .accepted): self.writeStaticString("HTTP/1.0 202 Accepted\r\n") case (1, 0, .nonAuthoritativeInformation): self.writeStaticString("HTTP/1.0 203 Non-Authoritative Information\r\n") case (1, 0, .noContent): self.writeStaticString("HTTP/1.0 204 No Content\r\n") case (1, 0, .resetContent): self.writeStaticString("HTTP/1.0 205 Reset Content\r\n") case (1, 0, .partialContent): self.writeStaticString("HTTP/1.0 206 Partial Content\r\n") case (1, 0, .multiStatus): self.writeStaticString("HTTP/1.0 207 Multi-Status\r\n") case (1, 0, .alreadyReported): self.writeStaticString("HTTP/1.0 208 Already Reported\r\n") case (1, 0, .imUsed): self.writeStaticString("HTTP/1.0 226 IM Used\r\n") case (1, 0, .multipleChoices): self.writeStaticString("HTTP/1.0 300 Multiple Choices\r\n") case (1, 0, .movedPermanently): self.writeStaticString("HTTP/1.0 301 Moved Permanently\r\n") case (1, 0, .found): self.writeStaticString("HTTP/1.0 302 Found\r\n") case (1, 0, .seeOther): self.writeStaticString("HTTP/1.0 303 See Other\r\n") case (1, 0, .notModified): self.writeStaticString("HTTP/1.0 304 Not Modified\r\n") case (1, 0, .useProxy): self.writeStaticString("HTTP/1.0 305 Use Proxy\r\n") case (1, 0, .temporaryRedirect): self.writeStaticString("HTTP/1.0 307 Tempory Redirect\r\n") case (1, 0, .permanentRedirect): self.writeStaticString("HTTP/1.0 308 Permanent Redirect\r\n") case (1, 0, .badRequest): self.writeStaticString("HTTP/1.0 400 Bad Request\r\n") case (1, 0, .unauthorized): self.writeStaticString("HTTP/1.0 401 Unauthorized\r\n") case (1, 0, .paymentRequired): self.writeStaticString("HTTP/1.0 402 Payment Required\r\n") case (1, 0, .forbidden): self.writeStaticString("HTTP/1.0 403 Forbidden\r\n") case (1, 0, .notFound): self.writeStaticString("HTTP/1.0 404 Not Found\r\n") case (1, 0, .methodNotAllowed): self.writeStaticString("HTTP/1.0 405 Method Not Allowed\r\n") case (1, 0, .notAcceptable): self.writeStaticString("HTTP/1.0 406 Not Acceptable\r\n") case (1, 0, .proxyAuthenticationRequired): self.writeStaticString("HTTP/1.0 407 Proxy Authentication Required\r\n") case (1, 0, .requestTimeout): self.writeStaticString("HTTP/1.0 408 Request Timeout\r\n") case (1, 0, .conflict): self.writeStaticString("HTTP/1.0 409 Conflict\r\n") case (1, 0, .gone): self.writeStaticString("HTTP/1.0 410 Gone\r\n") case (1, 0, .lengthRequired): self.writeStaticString("HTTP/1.0 411 Length Required\r\n") case (1, 0, .preconditionFailed): self.writeStaticString("HTTP/1.0 412 Precondition Failed\r\n") case (1, 0, .payloadTooLarge): self.writeStaticString("HTTP/1.0 413 Payload Too Large\r\n") case (1, 0, .uriTooLong): self.writeStaticString("HTTP/1.0 414 URI Too Long\r\n") case (1, 0, .unsupportedMediaType): self.writeStaticString("HTTP/1.0 415 Unsupported Media Type\r\n") case (1, 0, .rangeNotSatisfiable): self.writeStaticString("HTTP/1.0 416 Range Not Satisfiable\r\n") case (1, 0, .expectationFailed): self.writeStaticString("HTTP/1.0 417 Expectation Failed\r\n") case (1, 0, .misdirectedRequest): self.writeStaticString("HTTP/1.0 421 Misdirected Request\r\n") case (1, 0, .unprocessableEntity): self.writeStaticString("HTTP/1.0 422 Unprocessable Entity\r\n") case (1, 0, .locked): self.writeStaticString("HTTP/1.0 423 Locked\r\n") case (1, 0, .failedDependency): self.writeStaticString("HTTP/1.0 424 Failed Dependency\r\n") case (1, 0, .upgradeRequired): self.writeStaticString("HTTP/1.0 426 Upgrade Required\r\n") case (1, 0, .preconditionRequired): self.writeStaticString("HTTP/1.0 428 Precondition Required\r\n") case (1, 0, .tooManyRequests): self.writeStaticString("HTTP/1.0 429 Too Many Requests\r\n") case (1, 0, .requestHeaderFieldsTooLarge): self.writeStaticString("HTTP/1.0 431 Request Header Fields Too Large\r\n") case (1, 0, .unavailableForLegalReasons): self.writeStaticString("HTTP/1.0 451 Unavailable For Legal Reasons\r\n") case (1, 0, .internalServerError): self.writeStaticString("HTTP/1.0 500 Internal Server Error\r\n") case (1, 0, .notImplemented): self.writeStaticString("HTTP/1.0 501 Not Implemented\r\n") case (1, 0, .badGateway): self.writeStaticString("HTTP/1.0 502 Bad Gateway\r\n") case (1, 0, .serviceUnavailable): self.writeStaticString("HTTP/1.0 503 Service Unavailable\r\n") case (1, 0, .gatewayTimeout): self.writeStaticString("HTTP/1.0 504 Gateway Timeout\r\n") case (1, 0, .httpVersionNotSupported): self.writeStaticString("HTTP/1.0 505 HTTP Version Not Supported\r\n") case (1, 0, .variantAlsoNegotiates): self.writeStaticString("HTTP/1.0 506 Variant Also Negotiates\r\n") case (1, 0, .insufficientStorage): self.writeStaticString("HTTP/1.0 507 Insufficient Storage\r\n") case (1, 0, .loopDetected): self.writeStaticString("HTTP/1.0 508 Loop Detected\r\n") case (1, 0, .notExtended): self.writeStaticString("HTTP/1.0 510 Not Extended\r\n") case (1, 0, .networkAuthenticationRequired): self.writeStaticString("HTTP/1.1 511 Network Authentication Required\r\n") // Optimization for HTTP/1.1 case (1, 1, .custom(_, _)): self.writeStaticString("HTTP/1.1 ") self.write(status: response.status) self.writeStaticString("\r\n") case (1, 1, .continue): self.writeStaticString("HTTP/1.1 100 Continue\r\n") case (1, 1, .switchingProtocols): self.writeStaticString("HTTP/1.1 101 Switching Protocols\r\n") case (1, 1, .processing): self.writeStaticString("HTTP/1.1 102 Processing\r\n") case (1, 1, .ok): self.writeStaticString("HTTP/1.1 200 OK\r\n") case (1, 1, .created): self.writeStaticString("HTTP/1.1 201 Created\r\n") case (1, 1, .accepted): self.writeStaticString("HTTP/1.1 202 Accepted\r\n") case (1, 1, .nonAuthoritativeInformation): self.writeStaticString("HTTP/1.1 203 Non-Authoritative Information\r\n") case (1, 1, .noContent): self.writeStaticString("HTTP/1.1 204 No Content\r\n") case (1, 1, .resetContent): self.writeStaticString("HTTP/1.1 205 Reset Content\r\n") case (1, 1, .partialContent): self.writeStaticString("HTTP/1.1 206 Partial Content\r\n") case (1, 1, .multiStatus): self.writeStaticString("HTTP/1.1 207 Multi-Status\r\n") case (1, 1, .alreadyReported): self.writeStaticString("HTTP/1.1 208 Already Reported\r\n") case (1, 1, .imUsed): self.writeStaticString("HTTP/1.1 226 IM Used\r\n") case (1, 1, .multipleChoices): self.writeStaticString("HTTP/1.1 300 Multiple Choices\r\n") case (1, 1, .movedPermanently): self.writeStaticString("HTTP/1.1 301 Moved Permanently\r\n") case (1, 1, .found): self.writeStaticString("HTTP/1.1 302 Found\r\n") case (1, 1, .seeOther): self.writeStaticString("HTTP/1.1 303 See Other\r\n") case (1, 1, .notModified): self.writeStaticString("HTTP/1.1 304 Not Modified\r\n") case (1, 1, .useProxy): self.writeStaticString("HTTP/1.1 305 Use Proxy\r\n") case (1, 1, .temporaryRedirect): self.writeStaticString("HTTP/1.1 307 Tempory Redirect\r\n") case (1, 1, .permanentRedirect): self.writeStaticString("HTTP/1.1 308 Permanent Redirect\r\n") case (1, 1, .badRequest): self.writeStaticString("HTTP/1.1 400 Bad Request\r\n") case (1, 1, .unauthorized): self.writeStaticString("HTTP/1.1 401 Unauthorized\r\n") case (1, 1, .paymentRequired): self.writeStaticString("HTTP/1.1 402 Payment Required\r\n") case (1, 1, .forbidden): self.writeStaticString("HTTP/1.1 403 Forbidden\r\n") case (1, 1, .notFound): self.writeStaticString("HTTP/1.1 404 Not Found\r\n") case (1, 1, .methodNotAllowed): self.writeStaticString("HTTP/1.1 405 Method Not Allowed\r\n") case (1, 1, .notAcceptable): self.writeStaticString("HTTP/1.1 406 Not Acceptable\r\n") case (1, 1, .proxyAuthenticationRequired): self.writeStaticString("HTTP/1.1 407 Proxy Authentication Required\r\n") case (1, 1, .requestTimeout): self.writeStaticString("HTTP/1.1 408 Request Timeout\r\n") case (1, 1, .conflict): self.writeStaticString("HTTP/1.1 409 Conflict\r\n") case (1, 1, .gone): self.writeStaticString("HTTP/1.1 410 Gone\r\n") case (1, 1, .lengthRequired): self.writeStaticString("HTTP/1.1 411 Length Required\r\n") case (1, 1, .preconditionFailed): self.writeStaticString("HTTP/1.1 412 Precondition Failed\r\n") case (1, 1, .payloadTooLarge): self.writeStaticString("HTTP/1.1 413 Payload Too Large\r\n") case (1, 1, .uriTooLong): self.writeStaticString("HTTP/1.1 414 URI Too Long\r\n") case (1, 1, .unsupportedMediaType): self.writeStaticString("HTTP/1.1 415 Unsupported Media Type\r\n") case (1, 1, .rangeNotSatisfiable): self.writeStaticString("HTTP/1.1 416 Request Range Not Satisified\r\n") case (1, 1, .expectationFailed): self.writeStaticString("HTTP/1.1 417 Expectation Failed\r\n") case (1, 1, .misdirectedRequest): self.writeStaticString("HTTP/1.1 421 Misdirected Request\r\n") case (1, 1, .unprocessableEntity): self.writeStaticString("HTTP/1.1 422 Unprocessable Entity\r\n") case (1, 1, .locked): self.writeStaticString("HTTP/1.1 423 Locked\r\n") case (1, 1, .failedDependency): self.writeStaticString("HTTP/1.1 424 Failed Dependency\r\n") case (1, 1, .upgradeRequired): self.writeStaticString("HTTP/1.1 426 Upgrade Required\r\n") case (1, 1, .preconditionRequired): self.writeStaticString("HTTP/1.1 428 Precondition Required\r\n") case (1, 1, .tooManyRequests): self.writeStaticString("HTTP/1.1 429 Too Many Requests\r\n") case (1, 1, .requestHeaderFieldsTooLarge): self.writeStaticString("HTTP/1.1 431 Range Not Satisfiable\r\n") case (1, 1, .unavailableForLegalReasons): self.writeStaticString("HTTP/1.1 451 Unavailable For Legal Reasons\r\n") case (1, 1, .internalServerError): self.writeStaticString("HTTP/1.1 500 Internal Server Error\r\n") case (1, 1, .notImplemented): self.writeStaticString("HTTP/1.1 501 Not Implemented\r\n") case (1, 1, .badGateway): self.writeStaticString("HTTP/1.1 502 Bad Gateway\r\n") case (1, 1, .serviceUnavailable): self.writeStaticString("HTTP/1.1 503 Service Unavailable\r\n") case (1, 1, .gatewayTimeout): self.writeStaticString("HTTP/1.1 504 Gateway Timeout\r\n") case (1, 1, .httpVersionNotSupported): self.writeStaticString("HTTP/1.1 505 HTTP Version Not Supported\r\n") case (1, 1, .variantAlsoNegotiates): self.writeStaticString("HTTP/1.1 506 Variant Also Negotiates\r\n") case (1, 1, .insufficientStorage): self.writeStaticString("HTTP/1.1 507 Insufficient Storage\r\n") case (1, 1, .loopDetected): self.writeStaticString("HTTP/1.1 508 Loop Detected\r\n") case (1, 1, .notExtended): self.writeStaticString("HTTP/1.1 510 Not Extended\r\n") case (1, 1, .networkAuthenticationRequired): self.writeStaticString("HTTP/1.1 511 Network Authentication Required\r\n") // Fallback for non-known HTTP version default: self.write(version: response.version) self.writeWhitespace() self.write(status: response.status) self.writeStaticString("\r\n") } } private mutating func write(version: HTTPVersion) { switch (version.minor, version.major) { case (1, 0): self.writeStaticString("HTTP/1.0") case (1, 1): self.writeStaticString("HTTP/1.1") default: self.writeStaticString("HTTP/") self.writeString(String(version.major)) self.writeStaticString(".") self.writeString(String(version.minor)) } } fileprivate mutating func write(request: HTTPRequestHead) { self.write(method: request.method) self.writeWhitespace() self.writeString(request.uri) self.writeWhitespace() self.write(version: request.version) self.writeStaticString("\r\n") } fileprivate mutating func writeWhitespace() { self.writeInteger(32, as: UInt8.self) } private mutating func write(method: HTTPMethod) { switch method { case .GET: self.writeStaticString("GET") case .PUT: self.writeStaticString("PUT") case .ACL: self.writeStaticString("ACL") case .HEAD: self.writeStaticString("HEAD") case .POST: self.writeStaticString("POST") case .COPY: self.writeStaticString("COPY") case .LOCK: self.writeStaticString("LOCK") case .MOVE: self.writeStaticString("MOVE") case .BIND: self.writeStaticString("BIND") case .LINK: self.writeStaticString("LINK") case .PATCH: self.writeStaticString("PATCH") case .TRACE: self.writeStaticString("TRACE") case .MKCOL: self.writeStaticString("MKCOL") case .MERGE: self.writeStaticString("MERGE") case .PURGE: self.writeStaticString("PURGE") case .NOTIFY: self.writeStaticString("NOTIFY") case .SEARCH: self.writeStaticString("SEARCH") case .UNLOCK: self.writeStaticString("UNLOCK") case .REBIND: self.writeStaticString("REBIND") case .UNBIND: self.writeStaticString("UNBIND") case .REPORT: self.writeStaticString("REPORT") case .DELETE: self.writeStaticString("DELETE") case .UNLINK: self.writeStaticString("UNLINK") case .CONNECT: self.writeStaticString("CONNECT") case .MSEARCH: self.writeStaticString("MSEARCH") case .OPTIONS: self.writeStaticString("OPTIONS") case .PROPFIND: self.writeStaticString("PROPFIND") case .CHECKOUT: self.writeStaticString("CHECKOUT") case .PROPPATCH: self.writeStaticString("PROPPATCH") case .SUBSCRIBE: self.writeStaticString("SUBSCRIBE") case .MKCALENDAR: self.writeStaticString("MKCALENDAR") case .MKACTIVITY: self.writeStaticString("MKACTIVITY") case .UNSUBSCRIBE: self.writeStaticString("UNSUBSCRIBE") case .SOURCE: self.writeStaticString("SOURCE") case .RAW(let value): self.writeString(value) } } }