From 0ae93820cbde6b2cf3ad6bbe6b579e264cf14cf2 Mon Sep 17 00:00:00 2001 From: Fabian Fett Date: Tue, 7 Dec 2021 09:29:37 +0100 Subject: [PATCH 1/7] Add ControlPlaneResponseDecoder --- .../ControlPlaneRequest.swift | 43 +- .../ControlPlaneResponseDecoder.swift | 502 ++++++++++++++++++ .../LambdaRuntimeError.swift | 66 +++ .../ControlPlaneResponseDecoderTests.swift | 76 +++ scripts/soundness.sh | 2 +- 5 files changed, 675 insertions(+), 14 deletions(-) create mode 100644 Sources/AWSLambdaRuntimeCore/ControlPlaneResponseDecoder.swift create mode 100644 Sources/AWSLambdaRuntimeCore/LambdaRuntimeError.swift create mode 100644 Tests/AWSLambdaRuntimeCoreTests/ControlPlaneResponseDecoderTests.swift diff --git a/Sources/AWSLambdaRuntimeCore/ControlPlaneRequest.swift b/Sources/AWSLambdaRuntimeCore/ControlPlaneRequest.swift index 48da5237..0aeb035b 100644 --- a/Sources/AWSLambdaRuntimeCore/ControlPlaneRequest.swift +++ b/Sources/AWSLambdaRuntimeCore/ControlPlaneRequest.swift @@ -2,7 +2,7 @@ // // This source file is part of the SwiftAWSLambdaRuntime open source project // -// Copyright (c) 2017-2021 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Copyright (c) 2021-2022 Apple Inc. and the SwiftAWSLambdaRuntime project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -29,12 +29,27 @@ enum ControlPlaneResponse: Hashable { } struct Invocation: Hashable { - let requestID: String - let deadlineInMillisSinceEpoch: Int64 - let invokedFunctionARN: String - let traceID: String - let clientContext: String? - let cognitoIdentity: String? + var requestID: String + var deadlineInMillisSinceEpoch: Int64 + var invokedFunctionARN: String + var traceID: String + var clientContext: String? + var cognitoIdentity: String? + + init(requestID: String, + deadlineInMillisSinceEpoch: Int64, + invokedFunctionARN: String, + traceID: String, + clientContext: String?, + cognitoIdentity: String? + ) { + self.requestID = requestID + self.deadlineInMillisSinceEpoch = deadlineInMillisSinceEpoch + self.invokedFunctionARN = invokedFunctionARN + self.traceID = traceID + self.clientContext = clientContext + self.cognitoIdentity = cognitoIdentity + } init(headers: HTTPHeaders) throws { guard let requestID = headers.first(name: AmazonHeaders.requestID), !requestID.isEmpty else { @@ -55,12 +70,14 @@ struct Invocation: Hashable { throw Lambda.RuntimeError.invocationMissingHeader(AmazonHeaders.traceID) } - self.requestID = requestID - self.deadlineInMillisSinceEpoch = unixTimeInMilliseconds - self.invokedFunctionARN = invokedFunctionARN - self.traceID = traceID - self.clientContext = headers["Lambda-Runtime-Client-Context"].first - self.cognitoIdentity = headers["Lambda-Runtime-Cognito-Identity"].first + self.init( + requestID: requestID, + deadlineInMillisSinceEpoch: unixTimeInMilliseconds, + invokedFunctionARN: invokedFunctionARN, + traceID: traceID, + clientContext: headers["Lambda-Runtime-Client-Context"].first, + cognitoIdentity: headers["Lambda-Runtime-Cognito-Identity"].first + ) } } diff --git a/Sources/AWSLambdaRuntimeCore/ControlPlaneResponseDecoder.swift b/Sources/AWSLambdaRuntimeCore/ControlPlaneResponseDecoder.swift new file mode 100644 index 00000000..b2f6fcc8 --- /dev/null +++ b/Sources/AWSLambdaRuntimeCore/ControlPlaneResponseDecoder.swift @@ -0,0 +1,502 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2022 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import NIOCore +#if canImport(Darwin) +import Darwin +#else +import Glibc +#endif + +struct ControlPlaneResponseDecoder: NIOSingleStepByteToMessageDecoder { + typealias InboundOut = ControlPlaneResponse + + private enum State { + case waitingForNewResponse + case parsingHead(PartialHead) + case waitingForBody(PartialHead) + case receivingBody(PartialHead, ByteBuffer) + } + + private var state: State + + init() { + self.state = .waitingForNewResponse + } + + mutating func decode(buffer: inout ByteBuffer) throws -> ControlPlaneResponse? { + switch self.state { + case .waitingForNewResponse: + guard case .decoded(let head) = try self.decodeResponseHead(from: &buffer) else { + return nil + } + + guard case .decoded(let body) = try self.decodeBody(from: &buffer) else { + return nil + } + + return try self.decodeResponse(head: head, body: body) + + case .parsingHead: + guard case .decoded(let head) = try self.decodeHeaderLines(from: &buffer) else { + return nil + } + + guard case .decoded(let body) = try self.decodeBody(from: &buffer) else { + return nil + } + + return try self.decodeResponse(head: head, body: body) + + case .waitingForBody(let head), .receivingBody(let head, _): + guard case .decoded(let body) = try self.decodeBody(from: &buffer) else { + return nil + } + + return try self.decodeResponse(head: head, body: body) + } + } + + mutating func decodeLast(buffer: inout ByteBuffer, seenEOF: Bool) throws -> ControlPlaneResponse? { + try self.decode(buffer: &buffer) + } + + // MARK: - Private Methods - + + private enum DecodeResult { + case needMoreData + case decoded(T) + } + + private mutating func decodeResponseHead(from buffer: inout ByteBuffer) throws -> DecodeResult { + guard case .decoded = try self.decodeResponseStatusLine(from: &buffer) else { + return .needMoreData + } + + return try self.decodeHeaderLines(from: &buffer) + } + + private mutating func decodeResponseStatusLine(from buffer: inout ByteBuffer) throws -> DecodeResult { + guard case .waitingForNewResponse = self.state else { + preconditionFailure("Invalid state: \(self.state)") + } + + guard case .decoded(var lineBuffer) = try self.decodeCRLFTerminatedLine(from: &buffer) else { + return .needMoreData + } + + let statusCode = try self.decodeStatusLine(from: &lineBuffer) + self.state = .parsingHead(.init(statusCode: statusCode)) + return .decoded(statusCode) + } + + private mutating func decodeHeaderLines(from buffer: inout ByteBuffer) throws -> DecodeResult { + guard case .parsingHead(var head) = self.state else { + preconditionFailure("Invalid state: \(self.state)") + } + + while true { + guard case .decoded(var nextLine) = try self.decodeCRLFTerminatedLine(from: &buffer) else { + self.state = .parsingHead(head) + return .needMoreData + } + + switch try self.decodeHeaderLine(from: &nextLine) { + case .headerEnd: + self.state = .waitingForBody(head) + return .decoded(head) + + case .contentLength(let length): + head.contentLength = length // TODO: This can crash + + case .contentType: + break // switch + + case .requestID(let requestID): + head.requestID = requestID + + case .traceID(let traceID): + head.traceID = traceID + + case .functionARN(let arn): + head.invokedFunctionARN = arn + + case .cognitoIdentity(let cognitoIdentity): + head.cognitoIdentity = cognitoIdentity + + case .deadlineMS(let deadline): + head.deadlineInMillisSinceEpoch = deadline + + case .weDontCare: + break // switch + } + } + } + + enum BodyEncoding { + case chunked + case plain(length: Int) + case none + } + + private mutating func decodeBody(from buffer: inout ByteBuffer) throws -> DecodeResult { + switch self.state { + case .waitingForBody(let partialHead): + switch partialHead.contentLength { + case .none: + return .decoded(nil) + case .some(let length): + if let slice = buffer.readSlice(length: length) { + self.state = .waitingForNewResponse + return .decoded(slice) + } + return .needMoreData + } + + case .waitingForNewResponse, .parsingHead, .receivingBody: + preconditionFailure("Invalid state: \(self.state)") + } + } + + private mutating func decodeResponse(head: PartialHead, body: ByteBuffer?) throws -> ControlPlaneResponse { + switch head.statusCode { + case 200: + guard let body = body else { + preconditionFailure("TODO: implement") + } + return .next(try Invocation(head: head), body) + case 202: + return .accepted + case 400..<600: + preconditionFailure("TODO: implement") + + default: + throw LambdaRuntimeError.unexpectedStatusCode + } + } + + mutating func decodeStatusLine(from buffer: inout ByteBuffer) throws -> Int { + guard buffer.readableBytes >= 11 else { + throw LambdaRuntimeError.responseHeadInvalidStatusLine + } + + let cmp = buffer.readableBytesView.withUnsafeBytes { ptr in + memcmp("HTTP/1.1 ", ptr.baseAddress, 8) == 0 ? true : false + } + buffer.moveReaderIndex(forwardBy: 9) + + guard cmp else { + throw LambdaRuntimeError.responseHeadInvalidStatusLine + } + + let statusAsString = buffer.readString(length: 3)! + guard let status = Int(statusAsString) else { + throw LambdaRuntimeError.responseHeadInvalidStatusLine + } + + return status + } + + private mutating func decodeCRLFTerminatedLine(from buffer: inout ByteBuffer) throws -> DecodeResult { + guard let crIndex = buffer.readableBytesView.firstIndex(of: UInt8(ascii: "\r")) else { + if buffer.readableBytes > 256 { + throw LambdaRuntimeError.responseHeadMoreThan256BytesBeforeCRLF + } + return .needMoreData + } + let lfIndex = buffer.readableBytesView.index(after: crIndex) + guard lfIndex < buffer.readableBytesView.endIndex else { + // the buffer is split exactly after the \r and \n. Let's wait for more data + return .needMoreData + } + + guard buffer.readableBytesView[lfIndex] == UInt8(ascii: "\n") else { + throw LambdaRuntimeError.responseHeadInvalidHeader + } + + let slice = buffer.readSlice(length: crIndex - buffer.readerIndex)! + buffer.moveReaderIndex(forwardBy: 2) // move over \r\n + return .decoded(slice) + } + + private enum HeaderLineContent: Equatable { + case traceID(String) + case contentType + case contentLength(Int) + case cognitoIdentity(String) + case deadlineMS(Int) + case functionARN(String) + case requestID(LambdaRequestID) + + case weDontCare + case headerEnd + } + + private mutating func decodeHeaderLine(from buffer: inout ByteBuffer) throws -> HeaderLineContent { + guard let colonIndex = buffer.readableBytesView.firstIndex(of: UInt8(ascii: ":")) else { + if buffer.readableBytes == 0 { + return .headerEnd + } + throw LambdaRuntimeError.responseHeadHeaderMissingColon + } + + // based on colonIndex we can already make some good guesses... + // 4: Date + // 12: Content-Type + // 14: Content-Length + // 17: Transfer-Encoding + // 23: Lambda-Runtime-Trace-Id + // 26: Lambda-Runtime-Deadline-Ms + // 29: Lambda-Runtime-Aws-Request-Id + // Lambda-Runtime-Client-Context + // 31: Lambda-Runtime-Cognito-Identity + // 35: Lambda-Runtime-Invoked-Function-Arn + + switch colonIndex { + case 4: + if buffer.readHeaderName("date") { + return .weDontCare + } + + case 12: + if buffer.readHeaderName("content-type") { + return .weDontCare + } + + case 14: + if buffer.readHeaderName("content-length") { + buffer.moveReaderIndex(forwardBy: 1) // move forward for colon + try self.decodeOptionalWhiteSpaceBeforeFieldValue(from: &buffer) + guard let length = buffer.readIntegerFromHeader() else { + throw LambdaRuntimeError.responseHeadInvalidDeadlineValue + } + return .contentLength(length) + } + + case 17: + if buffer.readHeaderName("transfer-encoding") { + buffer.moveReaderIndex(forwardBy: 1) // move forward for colon + try self.decodeOptionalWhiteSpaceBeforeFieldValue(from: &buffer) + guard let length = buffer.readIntegerFromHeader() else { + throw LambdaRuntimeError.responseHeadInvalidDeadlineValue + } + return .contentLength(length) + } + + case 23: + if buffer.readHeaderName("lambda-runtime-trace-id") { + buffer.moveReaderIndex(forwardBy: 1) + guard let string = try self.decodeHeaderValue(from: &buffer) else { + throw LambdaRuntimeError.responseHeadInvalidTraceIDValue + } + return .traceID(string) + } + + case 26: + if buffer.readHeaderName("lambda-runtime-deadline-ms") { + buffer.moveReaderIndex(forwardBy: 1) // move forward for colon + try self.decodeOptionalWhiteSpaceBeforeFieldValue(from: &buffer) + guard let deadline = buffer.readIntegerFromHeader() else { + throw LambdaRuntimeError.responseHeadInvalidContentLengthValue + } + return .deadlineMS(deadline) + } + + case 29: + if buffer.readHeaderName("lambda-runtime-aws-request-id") { + buffer.moveReaderIndex(forwardBy: 1) // move forward for colon + try self.decodeOptionalWhiteSpaceBeforeFieldValue(from: &buffer) + guard let requestID = buffer.readRequestID() else { + throw LambdaRuntimeError.responseHeadInvalidRequestIDValue + } + return .requestID(requestID) + } + if buffer.readHeaderName("lambda-runtime-client-context") { + return .weDontCare + } + + case 31: + if buffer.readHeaderName("lambda-runtime-cognito-identity") { + return .weDontCare + } + + case 35: + if buffer.readHeaderName("lambda-runtime-invoked-function-arn") { + buffer.moveReaderIndex(forwardBy: 1) + guard let string = try self.decodeHeaderValue(from: &buffer) else { + throw LambdaRuntimeError.responseHeadInvalidTraceIDValue + } + return .functionARN(string) + } + + default: + return .weDontCare + } + + return .weDontCare + } + + @discardableResult + mutating func decodeOptionalWhiteSpaceBeforeFieldValue(from buffer: inout ByteBuffer) throws -> Int { + let startIndex = buffer.readerIndex + guard let index = buffer.readableBytesView.firstIndex(where: { ($0 != UInt8(ascii: " ") && $0 != UInt8(ascii: "\t")) }) else { + throw LambdaRuntimeError.responseHeadHeaderMissingFieldValue + } + buffer.moveReaderIndex(to: index) + return index - startIndex + } + + private func decodeHeaderValue(from buffer: inout ByteBuffer) throws -> String? { + func isNotOptionalWhiteSpace(_ val: UInt8) -> Bool { + val != UInt8(ascii: " ") && val != UInt8(ascii: "\t") + } + + guard let firstCharacterIndex = buffer.readableBytesView.firstIndex(where: isNotOptionalWhiteSpace), + let lastCharacterIndex = buffer.readableBytesView.lastIndex(where: isNotOptionalWhiteSpace) + else { + throw LambdaRuntimeError.responseHeadHeaderMissingFieldValue + } + + let string = buffer.getString(at: firstCharacterIndex, length: lastCharacterIndex + 1 - firstCharacterIndex) + buffer.moveReaderIndex(to: buffer.writerIndex) + return string + } + +} + +extension ControlPlaneResponseDecoder { + + fileprivate struct PartialHead { + var statusCode: Int + var contentLength: Int? + + var requestID: LambdaRequestID? + var deadlineInMillisSinceEpoch: Int? + var invokedFunctionARN: String? + var traceID: String? + var clientContext: String? + var cognitoIdentity: String? + + init(statusCode: Int) { + self.statusCode = statusCode + self.contentLength = nil + + self.requestID = nil + self.deadlineInMillisSinceEpoch = nil + self.invokedFunctionARN = nil + self.traceID = nil + self.clientContext = nil + self.cognitoIdentity = nil + } + } +} + +extension ByteBuffer { + fileprivate mutating func readHeaderName(_ name: String) -> Bool { + let result = self.withUnsafeReadableBytes { inputBuffer in + name.utf8.withContiguousStorageIfAvailable { nameBuffer -> Bool in + assert(inputBuffer.count >= nameBuffer.count) + + for idx in 0 ..< nameBuffer.count { + // let's hope this gets vectorised ;) + if inputBuffer[idx] & 0xdf != nameBuffer[idx] & 0xdf { + return false + } + } + return true + } + }! + + if result { + self.moveReaderIndex(forwardBy: name.utf8.count) + return true + } + + return false + } + + mutating func readIntegerFromHeader() -> Int? { + guard let ascii = self.readInteger(as: UInt8.self), UInt8(ascii: "0") <= ascii && ascii <= UInt8(ascii: "9") else { + return nil + } + var value = Int(ascii - UInt8(ascii: "0")) + loop: while let ascii = self.readInteger(as: UInt8.self) { + switch ascii { + case UInt8(ascii: "0")...UInt8(ascii: "9"): + value = value * 10 + value += Int(ascii - UInt8(ascii: "0")) + + case UInt8(ascii: " "), UInt8(ascii: "\t"): + // verify that all following characters are also whitespace + guard self.readableBytesView.allSatisfy({ $0 == UInt8(ascii: " ") || $0 == UInt8(ascii: "\t") }) else { + return nil + } + return value + + default: + return nil + } + } + + return value + } + +// mutating func validateHeaderValue(_ value: String) -> Bool { +// func isNotOptionalWhiteSpace(_ val: UInt8) -> Bool { +// val != UInt8(ascii: " ") && val != UInt8(ascii: "\t") +// } +// +// guard let firstCharacterIndex = self.readableBytesView.firstIndex(where: isNotOptionalWhiteSpace), +// let lastCharacterIndex = self.readableBytesView.lastIndex(where: isNotOptionalWhiteSpace) +// else { +// return false +// } +// +// self.com +// } + + mutating func readOptionalWhiteSpace() { + + } +} + +extension Invocation { + + fileprivate init(head: ControlPlaneResponseDecoder.PartialHead) throws { + guard let requestID = head.requestID else { + throw LambdaRuntimeError.invocationHeadMissingRequestID + } + + guard let deadlineInMillisSinceEpoch = head.deadlineInMillisSinceEpoch else { + throw LambdaRuntimeError.invocationHeadMissingDeadlineInMillisSinceEpoch + } + + guard let invokedFunctionARN = head.invokedFunctionARN else { + throw LambdaRuntimeError.invocationHeadMissingFunctionARN + } + + guard let traceID = head.traceID else { + throw LambdaRuntimeError.invocationHeadMissingTraceID + } + + self = Invocation( + requestID: requestID.lowercased, + deadlineInMillisSinceEpoch: Int64(deadlineInMillisSinceEpoch), + invokedFunctionARN: invokedFunctionARN, + traceID: traceID, + clientContext: head.clientContext, + cognitoIdentity: head.cognitoIdentity + ) + } +} diff --git a/Sources/AWSLambdaRuntimeCore/LambdaRuntimeError.swift b/Sources/AWSLambdaRuntimeCore/LambdaRuntimeError.swift new file mode 100644 index 00000000..10ee1931 --- /dev/null +++ b/Sources/AWSLambdaRuntimeCore/LambdaRuntimeError.swift @@ -0,0 +1,66 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2022 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +struct LambdaRuntimeError: Error, Hashable { + enum Base: Hashable { + case unsolicitedResponse + case unexpectedStatusCode + + case responseHeadInvalidStatusLine + case responseHeadMissingContentLengthOrTransferEncodingChunked + case responseHeadMoreThan256BytesBeforeCRLF + case responseHeadHeaderMissingColon + case responseHeadHeaderMissingFieldValue + case responseHeadInvalidHeader + case responseHeadInvalidContentLengthValue + case responseHeadInvalidRequestIDValue + case responseHeadInvalidTraceIDValue + case responseHeadInvalidDeadlineValue + + case invocationHeadMissingRequestID + case invocationHeadMissingDeadlineInMillisSinceEpoch + case invocationHeadMissingFunctionARN + case invocationHeadMissingTraceID + + case controlPlaneErrorResponse(ErrorResponse) + } + + private let base: Base + + private init(_ base: Base) { + self.base = base + } + + static var unsolicitedResponse = LambdaRuntimeError(.unsolicitedResponse) + static var unexpectedStatusCode = LambdaRuntimeError(.unexpectedStatusCode) + static var responseHeadInvalidStatusLine = LambdaRuntimeError(.responseHeadInvalidStatusLine) + static var responseHeadMissingContentLengthOrTransferEncodingChunked = + LambdaRuntimeError(.responseHeadMissingContentLengthOrTransferEncodingChunked) + static var responseHeadMoreThan256BytesBeforeCRLF = LambdaRuntimeError(.responseHeadMoreThan256BytesBeforeCRLF) + static var responseHeadHeaderMissingColon = LambdaRuntimeError(.responseHeadHeaderMissingColon) + static var responseHeadHeaderMissingFieldValue = LambdaRuntimeError(.responseHeadHeaderMissingFieldValue) + static var responseHeadInvalidHeader = LambdaRuntimeError(.responseHeadInvalidHeader) + static var responseHeadInvalidContentLengthValue = LambdaRuntimeError(.responseHeadInvalidContentLengthValue) + static var responseHeadInvalidRequestIDValue = LambdaRuntimeError(.responseHeadInvalidRequestIDValue) + static var responseHeadInvalidTraceIDValue = LambdaRuntimeError(.responseHeadInvalidTraceIDValue) + static var responseHeadInvalidDeadlineValue = LambdaRuntimeError(.responseHeadInvalidDeadlineValue) + static var invocationHeadMissingRequestID = LambdaRuntimeError(.invocationHeadMissingRequestID) + static var invocationHeadMissingDeadlineInMillisSinceEpoch = LambdaRuntimeError(.invocationHeadMissingDeadlineInMillisSinceEpoch) + static var invocationHeadMissingFunctionARN = LambdaRuntimeError(.invocationHeadMissingFunctionARN) + static var invocationHeadMissingTraceID = LambdaRuntimeError(.invocationHeadMissingTraceID) + + static func controlPlaneErrorResponse(_ response: ErrorResponse) -> Self { + LambdaRuntimeError(.controlPlaneErrorResponse(response)) + } +} diff --git a/Tests/AWSLambdaRuntimeCoreTests/ControlPlaneResponseDecoderTests.swift b/Tests/AWSLambdaRuntimeCoreTests/ControlPlaneResponseDecoderTests.swift new file mode 100644 index 00000000..b3d9be0e --- /dev/null +++ b/Tests/AWSLambdaRuntimeCoreTests/ControlPlaneResponseDecoderTests.swift @@ -0,0 +1,76 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2022 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +@testable import AWSLambdaRuntimeCore +import NIOCore +import XCTest +import NIOTestUtils + +final class ControlPlaneResponseDecoderTests: XCTestCase { + + func testNextAndAcceptedResponse() { + let nextResponse = ByteBuffer(string: """ + HTTP/1.1 200 OK\r\n\ + Content-Type: application/json\r\n\ + Lambda-Runtime-Aws-Request-Id: 9028dc49-a01b-4b44-8ffe-4912e9dabbbd\r\n\ + Lambda-Runtime-Deadline-Ms: 1638392696671\r\n\ + Lambda-Runtime-Invoked-Function-Arn: arn:aws:lambda:eu-central-1:079477498937:function:lambda-log-http-HelloWorldLambda-NiDlzMFXtF3x\r\n\ + Lambda-Runtime-Trace-Id: Root=1-61a7e375-40b3edf95b388fe75d1fa416;Parent=348bb48e251c1254;Sampled=0\r\n\ + Date: Wed, 01 Dec 2021 21:04:53 GMT\r\n\ + Content-Length: 49\r\n\ + \r\n\ + {"name":"Fabian","key2":"value2","key3":"value3"} + """ + ) + let invocation = Invocation( + requestID: "9028dc49-a01b-4b44-8ffe-4912e9dabbbd", + deadlineInMillisSinceEpoch: 1638392696671, + invokedFunctionARN: "arn:aws:lambda:eu-central-1:079477498937:function:lambda-log-http-HelloWorldLambda-NiDlzMFXtF3x", + traceID: "Root=1-61a7e375-40b3edf95b388fe75d1fa416;Parent=348bb48e251c1254;Sampled=0", + clientContext: nil, + cognitoIdentity: nil + ) + let next: ControlPlaneResponse = .next(invocation, ByteBuffer(string: #"{"name":"Fabian","key2":"value2","key3":"value3"}"#)) + + let acceptedResponse = ByteBuffer(string: """ + HTTP/1.1 202 Accepted\r\n\ + Content-Type: application/json\r\n\ + Date: Sun, 05 Dec 2021 11:53:40 GMT\r\n\ + Content-Length: 16\r\n\ + \r\n\ + {"status":"OK"}\n + """ + ) + + let pairs: [(ByteBuffer, [ControlPlaneResponse])] = [ + (nextResponse, [next]), + (acceptedResponse, [.accepted]), + (nextResponse + acceptedResponse, [next, .accepted]) + ] + + XCTAssertNoThrow(try ByteToMessageDecoderVerifier.verifyDecoder( + inputOutputPairs: pairs, + decoderFactory: { ControlPlaneResponseDecoder() } + )) + } +} + +extension ByteBuffer { + static func + (lhs: Self, rhs: Self) -> ByteBuffer { + var new = lhs + var rhs = rhs + new.writeBuffer(&rhs) + return new + } +} diff --git a/scripts/soundness.sh b/scripts/soundness.sh index eb9e173b..603ab19a 100755 --- a/scripts/soundness.sh +++ b/scripts/soundness.sh @@ -19,7 +19,7 @@ here="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" function replace_acceptable_years() { # this needs to replace all acceptable forms with 'YEARS' - sed -e 's/2017-2018/YEARS/' -e 's/2017-2020/YEARS/' -e 's/2017-2021/YEARS/' -e 's/2020-2021/YEARS/' -e 's/2019/YEARS/' -e 's/2020/YEARS/' -e 's/2021/YEARS/' + sed -e 's/2017-2018/YEARS/' -e 's/2017-2020/YEARS/' -e 's/2017-2021/YEARS/' -e 's/2020-2021/YEARS/' -e 's/2021-2022/YEARS/' -e 's/2019/YEARS/' -e 's/2020/YEARS/' -e 's/2021/YEARS/' -e 's/2022/YEARS/' } printf "=> Checking for unacceptable language... " From c7baa4b7081ff6e2dc136f0223c8ff3b9a145fe8 Mon Sep 17 00:00:00 2001 From: Fabian Fett Date: Mon, 3 Jan 2022 10:06:31 +0100 Subject: [PATCH 2/7] code format --- .../ControlPlaneRequest.swift | 5 +- .../ControlPlaneResponseDecoder.swift | 173 +++++++++--------- .../LambdaRuntimeError.swift | 14 +- .../ControlPlaneResponseDecoderTests.swift | 13 +- 4 files changed, 99 insertions(+), 106 deletions(-) diff --git a/Sources/AWSLambdaRuntimeCore/ControlPlaneRequest.swift b/Sources/AWSLambdaRuntimeCore/ControlPlaneRequest.swift index 0aeb035b..a9808ba4 100644 --- a/Sources/AWSLambdaRuntimeCore/ControlPlaneRequest.swift +++ b/Sources/AWSLambdaRuntimeCore/ControlPlaneRequest.swift @@ -35,14 +35,13 @@ struct Invocation: Hashable { var traceID: String var clientContext: String? var cognitoIdentity: String? - + init(requestID: String, deadlineInMillisSinceEpoch: Int64, invokedFunctionARN: String, traceID: String, clientContext: String?, - cognitoIdentity: String? - ) { + cognitoIdentity: String?) { self.requestID = requestID self.deadlineInMillisSinceEpoch = deadlineInMillisSinceEpoch self.invokedFunctionARN = invokedFunctionARN diff --git a/Sources/AWSLambdaRuntimeCore/ControlPlaneResponseDecoder.swift b/Sources/AWSLambdaRuntimeCore/ControlPlaneResponseDecoder.swift index b2f6fcc8..65d27d09 100644 --- a/Sources/AWSLambdaRuntimeCore/ControlPlaneResponseDecoder.swift +++ b/Sources/AWSLambdaRuntimeCore/ControlPlaneResponseDecoder.swift @@ -21,135 +21,135 @@ import Glibc struct ControlPlaneResponseDecoder: NIOSingleStepByteToMessageDecoder { typealias InboundOut = ControlPlaneResponse - + private enum State { case waitingForNewResponse case parsingHead(PartialHead) case waitingForBody(PartialHead) case receivingBody(PartialHead, ByteBuffer) } - + private var state: State - + init() { self.state = .waitingForNewResponse } - + mutating func decode(buffer: inout ByteBuffer) throws -> ControlPlaneResponse? { switch self.state { case .waitingForNewResponse: guard case .decoded(let head) = try self.decodeResponseHead(from: &buffer) else { return nil } - + guard case .decoded(let body) = try self.decodeBody(from: &buffer) else { return nil } - + return try self.decodeResponse(head: head, body: body) - + case .parsingHead: guard case .decoded(let head) = try self.decodeHeaderLines(from: &buffer) else { return nil } - + guard case .decoded(let body) = try self.decodeBody(from: &buffer) else { return nil } - + return try self.decodeResponse(head: head, body: body) - + case .waitingForBody(let head), .receivingBody(let head, _): guard case .decoded(let body) = try self.decodeBody(from: &buffer) else { return nil } - + return try self.decodeResponse(head: head, body: body) } } - + mutating func decodeLast(buffer: inout ByteBuffer, seenEOF: Bool) throws -> ControlPlaneResponse? { try self.decode(buffer: &buffer) } - + // MARK: - Private Methods - - + private enum DecodeResult { case needMoreData case decoded(T) } - + private mutating func decodeResponseHead(from buffer: inout ByteBuffer) throws -> DecodeResult { guard case .decoded = try self.decodeResponseStatusLine(from: &buffer) else { return .needMoreData } - + return try self.decodeHeaderLines(from: &buffer) } - + private mutating func decodeResponseStatusLine(from buffer: inout ByteBuffer) throws -> DecodeResult { guard case .waitingForNewResponse = self.state else { preconditionFailure("Invalid state: \(self.state)") } - + guard case .decoded(var lineBuffer) = try self.decodeCRLFTerminatedLine(from: &buffer) else { return .needMoreData } - + let statusCode = try self.decodeStatusLine(from: &lineBuffer) self.state = .parsingHead(.init(statusCode: statusCode)) return .decoded(statusCode) } - + private mutating func decodeHeaderLines(from buffer: inout ByteBuffer) throws -> DecodeResult { guard case .parsingHead(var head) = self.state else { preconditionFailure("Invalid state: \(self.state)") } - + while true { guard case .decoded(var nextLine) = try self.decodeCRLFTerminatedLine(from: &buffer) else { self.state = .parsingHead(head) return .needMoreData } - + switch try self.decodeHeaderLine(from: &nextLine) { case .headerEnd: self.state = .waitingForBody(head) return .decoded(head) - + case .contentLength(let length): head.contentLength = length // TODO: This can crash - + case .contentType: break // switch - + case .requestID(let requestID): head.requestID = requestID - + case .traceID(let traceID): head.traceID = traceID - + case .functionARN(let arn): head.invokedFunctionARN = arn - + case .cognitoIdentity(let cognitoIdentity): head.cognitoIdentity = cognitoIdentity - + case .deadlineMS(let deadline): head.deadlineInMillisSinceEpoch = deadline - + case .weDontCare: break // switch } } } - + enum BodyEncoding { case chunked case plain(length: Int) case none } - + private mutating func decodeBody(from buffer: inout ByteBuffer) throws -> DecodeResult { switch self.state { case .waitingForBody(let partialHead): @@ -163,12 +163,12 @@ struct ControlPlaneResponseDecoder: NIOSingleStepByteToMessageDecoder { } return .needMoreData } - + case .waitingForNewResponse, .parsingHead, .receivingBody: preconditionFailure("Invalid state: \(self.state)") } } - + private mutating func decodeResponse(head: PartialHead, body: ByteBuffer?) throws -> ControlPlaneResponse { switch head.statusCode { case 200: @@ -178,36 +178,36 @@ struct ControlPlaneResponseDecoder: NIOSingleStepByteToMessageDecoder { return .next(try Invocation(head: head), body) case 202: return .accepted - case 400..<600: + case 400 ..< 600: preconditionFailure("TODO: implement") - + default: throw LambdaRuntimeError.unexpectedStatusCode } } - + mutating func decodeStatusLine(from buffer: inout ByteBuffer) throws -> Int { guard buffer.readableBytes >= 11 else { throw LambdaRuntimeError.responseHeadInvalidStatusLine } - + let cmp = buffer.readableBytesView.withUnsafeBytes { ptr in memcmp("HTTP/1.1 ", ptr.baseAddress, 8) == 0 ? true : false } buffer.moveReaderIndex(forwardBy: 9) - + guard cmp else { throw LambdaRuntimeError.responseHeadInvalidStatusLine } - + let statusAsString = buffer.readString(length: 3)! guard let status = Int(statusAsString) else { throw LambdaRuntimeError.responseHeadInvalidStatusLine } - + return status } - + private mutating func decodeCRLFTerminatedLine(from buffer: inout ByteBuffer) throws -> DecodeResult { guard let crIndex = buffer.readableBytesView.firstIndex(of: UInt8(ascii: "\r")) else { if buffer.readableBytes > 256 { @@ -220,16 +220,16 @@ struct ControlPlaneResponseDecoder: NIOSingleStepByteToMessageDecoder { // the buffer is split exactly after the \r and \n. Let's wait for more data return .needMoreData } - + guard buffer.readableBytesView[lfIndex] == UInt8(ascii: "\n") else { throw LambdaRuntimeError.responseHeadInvalidHeader } - + let slice = buffer.readSlice(length: crIndex - buffer.readerIndex)! buffer.moveReaderIndex(forwardBy: 2) // move over \r\n return .decoded(slice) } - + private enum HeaderLineContent: Equatable { case traceID(String) case contentType @@ -238,7 +238,7 @@ struct ControlPlaneResponseDecoder: NIOSingleStepByteToMessageDecoder { case deadlineMS(Int) case functionARN(String) case requestID(LambdaRequestID) - + case weDontCare case headerEnd } @@ -250,7 +250,7 @@ struct ControlPlaneResponseDecoder: NIOSingleStepByteToMessageDecoder { } throw LambdaRuntimeError.responseHeadHeaderMissingColon } - + // based on colonIndex we can already make some good guesses... // 4: Date // 12: Content-Type @@ -262,18 +262,18 @@ struct ControlPlaneResponseDecoder: NIOSingleStepByteToMessageDecoder { // Lambda-Runtime-Client-Context // 31: Lambda-Runtime-Cognito-Identity // 35: Lambda-Runtime-Invoked-Function-Arn - + switch colonIndex { case 4: if buffer.readHeaderName("date") { return .weDontCare } - + case 12: if buffer.readHeaderName("content-type") { return .weDontCare } - + case 14: if buffer.readHeaderName("content-length") { buffer.moveReaderIndex(forwardBy: 1) // move forward for colon @@ -283,7 +283,7 @@ struct ControlPlaneResponseDecoder: NIOSingleStepByteToMessageDecoder { } return .contentLength(length) } - + case 17: if buffer.readHeaderName("transfer-encoding") { buffer.moveReaderIndex(forwardBy: 1) // move forward for colon @@ -293,7 +293,7 @@ struct ControlPlaneResponseDecoder: NIOSingleStepByteToMessageDecoder { } return .contentLength(length) } - + case 23: if buffer.readHeaderName("lambda-runtime-trace-id") { buffer.moveReaderIndex(forwardBy: 1) @@ -302,7 +302,7 @@ struct ControlPlaneResponseDecoder: NIOSingleStepByteToMessageDecoder { } return .traceID(string) } - + case 26: if buffer.readHeaderName("lambda-runtime-deadline-ms") { buffer.moveReaderIndex(forwardBy: 1) // move forward for colon @@ -312,7 +312,7 @@ struct ControlPlaneResponseDecoder: NIOSingleStepByteToMessageDecoder { } return .deadlineMS(deadline) } - + case 29: if buffer.readHeaderName("lambda-runtime-aws-request-id") { buffer.moveReaderIndex(forwardBy: 1) // move forward for colon @@ -325,12 +325,12 @@ struct ControlPlaneResponseDecoder: NIOSingleStepByteToMessageDecoder { if buffer.readHeaderName("lambda-runtime-client-context") { return .weDontCare } - + case 31: if buffer.readHeaderName("lambda-runtime-cognito-identity") { return .weDontCare } - + case 35: if buffer.readHeaderName("lambda-runtime-invoked-function-arn") { buffer.moveReaderIndex(forwardBy: 1) @@ -339,59 +339,57 @@ struct ControlPlaneResponseDecoder: NIOSingleStepByteToMessageDecoder { } return .functionARN(string) } - + default: return .weDontCare } - + return .weDontCare } - + @discardableResult mutating func decodeOptionalWhiteSpaceBeforeFieldValue(from buffer: inout ByteBuffer) throws -> Int { let startIndex = buffer.readerIndex - guard let index = buffer.readableBytesView.firstIndex(where: { ($0 != UInt8(ascii: " ") && $0 != UInt8(ascii: "\t")) }) else { + guard let index = buffer.readableBytesView.firstIndex(where: { $0 != UInt8(ascii: " ") && $0 != UInt8(ascii: "\t") }) else { throw LambdaRuntimeError.responseHeadHeaderMissingFieldValue } buffer.moveReaderIndex(to: index) return index - startIndex } - + private func decodeHeaderValue(from buffer: inout ByteBuffer) throws -> String? { func isNotOptionalWhiteSpace(_ val: UInt8) -> Bool { val != UInt8(ascii: " ") && val != UInt8(ascii: "\t") } - + guard let firstCharacterIndex = buffer.readableBytesView.firstIndex(where: isNotOptionalWhiteSpace), let lastCharacterIndex = buffer.readableBytesView.lastIndex(where: isNotOptionalWhiteSpace) else { throw LambdaRuntimeError.responseHeadHeaderMissingFieldValue } - + let string = buffer.getString(at: firstCharacterIndex, length: lastCharacterIndex + 1 - firstCharacterIndex) buffer.moveReaderIndex(to: buffer.writerIndex) return string } - } extension ControlPlaneResponseDecoder { - fileprivate struct PartialHead { var statusCode: Int var contentLength: Int? - + var requestID: LambdaRequestID? var deadlineInMillisSinceEpoch: Int? var invokedFunctionARN: String? var traceID: String? var clientContext: String? var cognitoIdentity: String? - + init(statusCode: Int) { self.statusCode = statusCode self.contentLength = nil - + self.requestID = nil self.deadlineInMillisSinceEpoch = nil self.invokedFunctionARN = nil @@ -407,25 +405,25 @@ extension ByteBuffer { let result = self.withUnsafeReadableBytes { inputBuffer in name.utf8.withContiguousStorageIfAvailable { nameBuffer -> Bool in assert(inputBuffer.count >= nameBuffer.count) - + for idx in 0 ..< nameBuffer.count { // let's hope this gets vectorised ;) - if inputBuffer[idx] & 0xdf != nameBuffer[idx] & 0xdf { + if inputBuffer[idx] & 0xDF != nameBuffer[idx] & 0xDF { return false } } return true } }! - + if result { self.moveReaderIndex(forwardBy: name.utf8.count) return true } - + return false } - + mutating func readIntegerFromHeader() -> Int? { guard let ascii = self.readInteger(as: UInt8.self), UInt8(ascii: "0") <= ascii && ascii <= UInt8(ascii: "9") else { return nil @@ -433,63 +431,60 @@ extension ByteBuffer { var value = Int(ascii - UInt8(ascii: "0")) loop: while let ascii = self.readInteger(as: UInt8.self) { switch ascii { - case UInt8(ascii: "0")...UInt8(ascii: "9"): + case UInt8(ascii: "0") ... UInt8(ascii: "9"): value = value * 10 value += Int(ascii - UInt8(ascii: "0")) - + case UInt8(ascii: " "), UInt8(ascii: "\t"): // verify that all following characters are also whitespace guard self.readableBytesView.allSatisfy({ $0 == UInt8(ascii: " ") || $0 == UInt8(ascii: "\t") }) else { return nil } return value - + default: return nil } } - + return value } - + // mutating func validateHeaderValue(_ value: String) -> Bool { // func isNotOptionalWhiteSpace(_ val: UInt8) -> Bool { // val != UInt8(ascii: " ") && val != UInt8(ascii: "\t") // } -// +// // guard let firstCharacterIndex = self.readableBytesView.firstIndex(where: isNotOptionalWhiteSpace), // let lastCharacterIndex = self.readableBytesView.lastIndex(where: isNotOptionalWhiteSpace) // else { // return false // } -// +// // self.com // } - - mutating func readOptionalWhiteSpace() { - - } + + mutating func readOptionalWhiteSpace() {} } extension Invocation { - fileprivate init(head: ControlPlaneResponseDecoder.PartialHead) throws { guard let requestID = head.requestID else { throw LambdaRuntimeError.invocationHeadMissingRequestID } - + guard let deadlineInMillisSinceEpoch = head.deadlineInMillisSinceEpoch else { throw LambdaRuntimeError.invocationHeadMissingDeadlineInMillisSinceEpoch } - + guard let invokedFunctionARN = head.invokedFunctionARN else { throw LambdaRuntimeError.invocationHeadMissingFunctionARN } - + guard let traceID = head.traceID else { throw LambdaRuntimeError.invocationHeadMissingTraceID } - + self = Invocation( requestID: requestID.lowercased, deadlineInMillisSinceEpoch: Int64(deadlineInMillisSinceEpoch), diff --git a/Sources/AWSLambdaRuntimeCore/LambdaRuntimeError.swift b/Sources/AWSLambdaRuntimeCore/LambdaRuntimeError.swift index 10ee1931..6cc2866a 100644 --- a/Sources/AWSLambdaRuntimeCore/LambdaRuntimeError.swift +++ b/Sources/AWSLambdaRuntimeCore/LambdaRuntimeError.swift @@ -16,7 +16,7 @@ struct LambdaRuntimeError: Error, Hashable { enum Base: Hashable { case unsolicitedResponse case unexpectedStatusCode - + case responseHeadInvalidStatusLine case responseHeadMissingContentLengthOrTransferEncodingChunked case responseHeadMoreThan256BytesBeforeCRLF @@ -27,21 +27,21 @@ struct LambdaRuntimeError: Error, Hashable { case responseHeadInvalidRequestIDValue case responseHeadInvalidTraceIDValue case responseHeadInvalidDeadlineValue - + case invocationHeadMissingRequestID case invocationHeadMissingDeadlineInMillisSinceEpoch case invocationHeadMissingFunctionARN case invocationHeadMissingTraceID - + case controlPlaneErrorResponse(ErrorResponse) } - + private let base: Base - + private init(_ base: Base) { self.base = base } - + static var unsolicitedResponse = LambdaRuntimeError(.unsolicitedResponse) static var unexpectedStatusCode = LambdaRuntimeError(.unexpectedStatusCode) static var responseHeadInvalidStatusLine = LambdaRuntimeError(.responseHeadInvalidStatusLine) @@ -59,7 +59,7 @@ struct LambdaRuntimeError: Error, Hashable { static var invocationHeadMissingDeadlineInMillisSinceEpoch = LambdaRuntimeError(.invocationHeadMissingDeadlineInMillisSinceEpoch) static var invocationHeadMissingFunctionARN = LambdaRuntimeError(.invocationHeadMissingFunctionARN) static var invocationHeadMissingTraceID = LambdaRuntimeError(.invocationHeadMissingTraceID) - + static func controlPlaneErrorResponse(_ response: ErrorResponse) -> Self { LambdaRuntimeError(.controlPlaneErrorResponse(response)) } diff --git a/Tests/AWSLambdaRuntimeCoreTests/ControlPlaneResponseDecoderTests.swift b/Tests/AWSLambdaRuntimeCoreTests/ControlPlaneResponseDecoderTests.swift index b3d9be0e..5a7d7c9f 100644 --- a/Tests/AWSLambdaRuntimeCoreTests/ControlPlaneResponseDecoderTests.swift +++ b/Tests/AWSLambdaRuntimeCoreTests/ControlPlaneResponseDecoderTests.swift @@ -14,11 +14,10 @@ @testable import AWSLambdaRuntimeCore import NIOCore -import XCTest import NIOTestUtils +import XCTest final class ControlPlaneResponseDecoderTests: XCTestCase { - func testNextAndAcceptedResponse() { let nextResponse = ByteBuffer(string: """ HTTP/1.1 200 OK\r\n\ @@ -35,14 +34,14 @@ final class ControlPlaneResponseDecoderTests: XCTestCase { ) let invocation = Invocation( requestID: "9028dc49-a01b-4b44-8ffe-4912e9dabbbd", - deadlineInMillisSinceEpoch: 1638392696671, + deadlineInMillisSinceEpoch: 1_638_392_696_671, invokedFunctionARN: "arn:aws:lambda:eu-central-1:079477498937:function:lambda-log-http-HelloWorldLambda-NiDlzMFXtF3x", traceID: "Root=1-61a7e375-40b3edf95b388fe75d1fa416;Parent=348bb48e251c1254;Sampled=0", clientContext: nil, cognitoIdentity: nil ) let next: ControlPlaneResponse = .next(invocation, ByteBuffer(string: #"{"name":"Fabian","key2":"value2","key3":"value3"}"#)) - + let acceptedResponse = ByteBuffer(string: """ HTTP/1.1 202 Accepted\r\n\ Content-Type: application/json\r\n\ @@ -52,13 +51,13 @@ final class ControlPlaneResponseDecoderTests: XCTestCase { {"status":"OK"}\n """ ) - + let pairs: [(ByteBuffer, [ControlPlaneResponse])] = [ (nextResponse, [next]), (acceptedResponse, [.accepted]), - (nextResponse + acceptedResponse, [next, .accepted]) + (nextResponse + acceptedResponse, [next, .accepted]), ] - + XCTAssertNoThrow(try ByteToMessageDecoderVerifier.verifyDecoder( inputOutputPairs: pairs, decoderFactory: { ControlPlaneResponseDecoder() } From 776ea106b3a6322e618224093d3b29508dadb086 Mon Sep 17 00:00:00 2001 From: Fabian Fett Date: Mon, 3 Jan 2022 16:49:10 +0100 Subject: [PATCH 3/7] Fix failing tests --- .../ControlPlaneResponseDecoder.swift | 34 +++++++++++++------ 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/Sources/AWSLambdaRuntimeCore/ControlPlaneResponseDecoder.swift b/Sources/AWSLambdaRuntimeCore/ControlPlaneResponseDecoder.swift index 65d27d09..1bd2385d 100644 --- a/Sources/AWSLambdaRuntimeCore/ControlPlaneResponseDecoder.swift +++ b/Sources/AWSLambdaRuntimeCore/ControlPlaneResponseDecoder.swift @@ -13,11 +13,6 @@ //===----------------------------------------------------------------------===// import NIOCore -#if canImport(Darwin) -import Darwin -#else -import Glibc -#endif struct ControlPlaneResponseDecoder: NIOSingleStepByteToMessageDecoder { typealias InboundOut = ControlPlaneResponse @@ -191,12 +186,7 @@ struct ControlPlaneResponseDecoder: NIOSingleStepByteToMessageDecoder { throw LambdaRuntimeError.responseHeadInvalidStatusLine } - let cmp = buffer.readableBytesView.withUnsafeBytes { ptr in - memcmp("HTTP/1.1 ", ptr.baseAddress, 8) == 0 ? true : false - } - buffer.moveReaderIndex(forwardBy: 9) - - guard cmp else { + guard buffer.readString("HTTP/1.1 ") else { throw LambdaRuntimeError.responseHeadInvalidStatusLine } @@ -401,6 +391,28 @@ extension ControlPlaneResponseDecoder { } extension ByteBuffer { + fileprivate mutating func readString(_ string: String) -> Bool { + let result = self.withUnsafeReadableBytes { inputBuffer in + string.utf8.withContiguousStorageIfAvailable { validateBuffer -> Bool in + assert(inputBuffer.count >= validateBuffer.count) + + for idx in 0 ..< validateBuffer.count { + if inputBuffer[idx] != validateBuffer[idx] { + return false + } + } + return true + } + }! + + if result { + self.moveReaderIndex(forwardBy: string.utf8.count) + return true + } + + return false + } + fileprivate mutating func readHeaderName(_ name: String) -> Bool { let result = self.withUnsafeReadableBytes { inputBuffer in name.utf8.withContiguousStorageIfAvailable { nameBuffer -> Bool in From 690b688d1b53810f39c0db6f5fba4b070bb4fc82 Mon Sep 17 00:00:00 2001 From: Fabian Fett Date: Mon, 3 Jan 2022 17:08:11 +0100 Subject: [PATCH 4/7] Better naming --- .../ControlPlaneResponseDecoder.swift | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Sources/AWSLambdaRuntimeCore/ControlPlaneResponseDecoder.swift b/Sources/AWSLambdaRuntimeCore/ControlPlaneResponseDecoder.swift index 1bd2385d..dc32750e 100644 --- a/Sources/AWSLambdaRuntimeCore/ControlPlaneResponseDecoder.swift +++ b/Sources/AWSLambdaRuntimeCore/ControlPlaneResponseDecoder.swift @@ -133,7 +133,7 @@ struct ControlPlaneResponseDecoder: NIOSingleStepByteToMessageDecoder { case .deadlineMS(let deadline): head.deadlineInMillisSinceEpoch = deadline - case .weDontCare: + case .ignore: break // switch } } @@ -229,7 +229,7 @@ struct ControlPlaneResponseDecoder: NIOSingleStepByteToMessageDecoder { case functionARN(String) case requestID(LambdaRequestID) - case weDontCare + case ignore case headerEnd } @@ -256,12 +256,12 @@ struct ControlPlaneResponseDecoder: NIOSingleStepByteToMessageDecoder { switch colonIndex { case 4: if buffer.readHeaderName("date") { - return .weDontCare + return .ignore } case 12: if buffer.readHeaderName("content-type") { - return .weDontCare + return .ignore } case 14: @@ -313,12 +313,12 @@ struct ControlPlaneResponseDecoder: NIOSingleStepByteToMessageDecoder { return .requestID(requestID) } if buffer.readHeaderName("lambda-runtime-client-context") { - return .weDontCare + return .ignore } case 31: if buffer.readHeaderName("lambda-runtime-cognito-identity") { - return .weDontCare + return .ignore } case 35: @@ -331,10 +331,10 @@ struct ControlPlaneResponseDecoder: NIOSingleStepByteToMessageDecoder { } default: - return .weDontCare + return .ignore } - return .weDontCare + return .ignore } @discardableResult From 4d6243ee443212cabb349bd571c0ccd561cd991b Mon Sep 17 00:00:00 2001 From: Fabian Fett Date: Mon, 3 Jan 2022 18:04:56 +0100 Subject: [PATCH 5/7] Validate header fields. --- .../ControlPlaneResponseDecoder.swift | 34 ++++++++++++++++++- .../LambdaRuntimeError.swift | 2 ++ .../ControlPlaneResponseDecoderTests.swift | 27 +++++++++++++++ 3 files changed, 62 insertions(+), 1 deletion(-) diff --git a/Sources/AWSLambdaRuntimeCore/ControlPlaneResponseDecoder.swift b/Sources/AWSLambdaRuntimeCore/ControlPlaneResponseDecoder.swift index dc32750e..4fb48ffb 100644 --- a/Sources/AWSLambdaRuntimeCore/ControlPlaneResponseDecoder.swift +++ b/Sources/AWSLambdaRuntimeCore/ControlPlaneResponseDecoder.swift @@ -331,7 +331,39 @@ struct ControlPlaneResponseDecoder: NIOSingleStepByteToMessageDecoder { } default: - return .ignore + // Ensure we received a valid http header: + break // fallthrough + } + + // We received a header we didn't expect, let's ensure it is valid. + let satisfy = buffer.readableBytesView[0.. Bool in + switch char { + case UInt8(ascii: "a")...UInt8(ascii: "z"), + UInt8(ascii: "A")...UInt8(ascii: "Z"), + UInt8(ascii: "0")...UInt8(ascii: "9"), + UInt8(ascii: "!"), + UInt8(ascii: "#"), + UInt8(ascii: "$"), + UInt8(ascii: "%"), + UInt8(ascii: "&"), + UInt8(ascii: "'"), + UInt8(ascii: "*"), + UInt8(ascii: "+"), + UInt8(ascii: "-"), + UInt8(ascii: "."), + UInt8(ascii: "^"), + UInt8(ascii: "_"), + UInt8(ascii: "`"), + UInt8(ascii: "|"), + UInt8(ascii: "~"): + return true + default: + return false + } + } + + guard satisfy else { + throw LambdaRuntimeError.responseHeadHeaderInvalidCharacter } return .ignore diff --git a/Sources/AWSLambdaRuntimeCore/LambdaRuntimeError.swift b/Sources/AWSLambdaRuntimeCore/LambdaRuntimeError.swift index 6cc2866a..acf79c34 100644 --- a/Sources/AWSLambdaRuntimeCore/LambdaRuntimeError.swift +++ b/Sources/AWSLambdaRuntimeCore/LambdaRuntimeError.swift @@ -20,6 +20,7 @@ struct LambdaRuntimeError: Error, Hashable { case responseHeadInvalidStatusLine case responseHeadMissingContentLengthOrTransferEncodingChunked case responseHeadMoreThan256BytesBeforeCRLF + case responseHeadHeaderInvalidCharacter case responseHeadHeaderMissingColon case responseHeadHeaderMissingFieldValue case responseHeadInvalidHeader @@ -48,6 +49,7 @@ struct LambdaRuntimeError: Error, Hashable { static var responseHeadMissingContentLengthOrTransferEncodingChunked = LambdaRuntimeError(.responseHeadMissingContentLengthOrTransferEncodingChunked) static var responseHeadMoreThan256BytesBeforeCRLF = LambdaRuntimeError(.responseHeadMoreThan256BytesBeforeCRLF) + static var responseHeadHeaderInvalidCharacter = LambdaRuntimeError(.responseHeadHeaderInvalidCharacter) static var responseHeadHeaderMissingColon = LambdaRuntimeError(.responseHeadHeaderMissingColon) static var responseHeadHeaderMissingFieldValue = LambdaRuntimeError(.responseHeadHeaderMissingFieldValue) static var responseHeadInvalidHeader = LambdaRuntimeError(.responseHeadInvalidHeader) diff --git a/Tests/AWSLambdaRuntimeCoreTests/ControlPlaneResponseDecoderTests.swift b/Tests/AWSLambdaRuntimeCoreTests/ControlPlaneResponseDecoderTests.swift index 5a7d7c9f..42d21588 100644 --- a/Tests/AWSLambdaRuntimeCoreTests/ControlPlaneResponseDecoderTests.swift +++ b/Tests/AWSLambdaRuntimeCoreTests/ControlPlaneResponseDecoderTests.swift @@ -63,6 +63,33 @@ final class ControlPlaneResponseDecoderTests: XCTestCase { decoderFactory: { ControlPlaneResponseDecoder() } )) } + + func testWhitespaceInHeaderIsRejected() { + let nextResponse = ByteBuffer(string: """ + HTTP/1.1 200 OK\r\n\ + Content-Type: application/json\r\n\ + Lambda-Runtime Aws-Request-Id: 9028dc49-a01b-4b44-8ffe-4912e9dabbbd\r\n\ + Lambda-Runtime-Deadline-Ms: 1638392696671\r\n\ + Lambda-Runtime-Invoked-Function-Arn: arn:aws:lambda:eu-central-1:079477498937:function:lambda-log-http-HelloWorldLambda-NiDlzMFXtF3x\r\n\ + Lambda-Runtime-Trace-Id: Root=1-61a7e375-40b3edf95b388fe75d1fa416;Parent=348bb48e251c1254;Sampled=0\r\n\ + Date: Wed, 01 Dec 2021 21:04:53 GMT\r\n\ + Content-Length: 49\r\n\ + \r\n\ + {"name":"Fabian","key2":"value2","key3":"value3"} + """ + ) + + let pairs: [(ByteBuffer, [ControlPlaneResponse])] = [ + (nextResponse, []) + ] + + XCTAssertThrowsError(try ByteToMessageDecoderVerifier.verifyDecoder( + inputOutputPairs: pairs, + decoderFactory: { ControlPlaneResponseDecoder() } + )) { + XCTAssertEqual($0 as? LambdaRuntimeError, .responseHeadHeaderInvalidCharacter) + } + } } extension ByteBuffer { From 239e1718ddbca1a66fa33045765f5ad726031a30 Mon Sep 17 00:00:00 2001 From: Fabian Fett Date: Mon, 3 Jan 2022 23:08:21 +0100 Subject: [PATCH 6/7] More tests --- .../ControlPlaneResponseDecoder.swift | 30 +- .../LambdaRuntimeError.swift | 1 + .../ControlPlaneResponseDecoderTests.swift | 259 +++++++++++++++++- 3 files changed, 258 insertions(+), 32 deletions(-) diff --git a/Sources/AWSLambdaRuntimeCore/ControlPlaneResponseDecoder.swift b/Sources/AWSLambdaRuntimeCore/ControlPlaneResponseDecoder.swift index 4fb48ffb..4f3a44af 100644 --- a/Sources/AWSLambdaRuntimeCore/ControlPlaneResponseDecoder.swift +++ b/Sources/AWSLambdaRuntimeCore/ControlPlaneResponseDecoder.swift @@ -269,7 +269,7 @@ struct ControlPlaneResponseDecoder: NIOSingleStepByteToMessageDecoder { buffer.moveReaderIndex(forwardBy: 1) // move forward for colon try self.decodeOptionalWhiteSpaceBeforeFieldValue(from: &buffer) guard let length = buffer.readIntegerFromHeader() else { - throw LambdaRuntimeError.responseHeadInvalidDeadlineValue + throw LambdaRuntimeError.responseHeadInvalidContentLengthValue } return .contentLength(length) } @@ -334,13 +334,13 @@ struct ControlPlaneResponseDecoder: NIOSingleStepByteToMessageDecoder { // Ensure we received a valid http header: break // fallthrough } - + // We received a header we didn't expect, let's ensure it is valid. - let satisfy = buffer.readableBytesView[0.. Bool in + let satisfy = buffer.readableBytesView[0 ..< colonIndex].allSatisfy { char -> Bool in switch char { - case UInt8(ascii: "a")...UInt8(ascii: "z"), - UInt8(ascii: "A")...UInt8(ascii: "Z"), - UInt8(ascii: "0")...UInt8(ascii: "9"), + case UInt8(ascii: "a") ... UInt8(ascii: "z"), + UInt8(ascii: "A") ... UInt8(ascii: "Z"), + UInt8(ascii: "0") ... UInt8(ascii: "9"), UInt8(ascii: "!"), UInt8(ascii: "#"), UInt8(ascii: "$"), @@ -361,7 +361,7 @@ struct ControlPlaneResponseDecoder: NIOSingleStepByteToMessageDecoder { return false } } - + guard satisfy else { throw LambdaRuntimeError.responseHeadHeaderInvalidCharacter } @@ -493,22 +493,6 @@ extension ByteBuffer { return value } - -// mutating func validateHeaderValue(_ value: String) -> Bool { -// func isNotOptionalWhiteSpace(_ val: UInt8) -> Bool { -// val != UInt8(ascii: " ") && val != UInt8(ascii: "\t") -// } -// -// guard let firstCharacterIndex = self.readableBytesView.firstIndex(where: isNotOptionalWhiteSpace), -// let lastCharacterIndex = self.readableBytesView.lastIndex(where: isNotOptionalWhiteSpace) -// else { -// return false -// } -// -// self.com -// } - - mutating func readOptionalWhiteSpace() {} } extension Invocation { diff --git a/Sources/AWSLambdaRuntimeCore/LambdaRuntimeError.swift b/Sources/AWSLambdaRuntimeCore/LambdaRuntimeError.swift index acf79c34..4c91c90a 100644 --- a/Sources/AWSLambdaRuntimeCore/LambdaRuntimeError.swift +++ b/Sources/AWSLambdaRuntimeCore/LambdaRuntimeError.swift @@ -57,6 +57,7 @@ struct LambdaRuntimeError: Error, Hashable { static var responseHeadInvalidRequestIDValue = LambdaRuntimeError(.responseHeadInvalidRequestIDValue) static var responseHeadInvalidTraceIDValue = LambdaRuntimeError(.responseHeadInvalidTraceIDValue) static var responseHeadInvalidDeadlineValue = LambdaRuntimeError(.responseHeadInvalidDeadlineValue) + static var invocationHeadMissingRequestID = LambdaRuntimeError(.invocationHeadMissingRequestID) static var invocationHeadMissingDeadlineInMillisSinceEpoch = LambdaRuntimeError(.invocationHeadMissingDeadlineInMillisSinceEpoch) static var invocationHeadMissingFunctionARN = LambdaRuntimeError(.invocationHeadMissingFunctionARN) diff --git a/Tests/AWSLambdaRuntimeCoreTests/ControlPlaneResponseDecoderTests.swift b/Tests/AWSLambdaRuntimeCoreTests/ControlPlaneResponseDecoderTests.swift index 42d21588..45c5f61a 100644 --- a/Tests/AWSLambdaRuntimeCoreTests/ControlPlaneResponseDecoderTests.swift +++ b/Tests/AWSLambdaRuntimeCoreTests/ControlPlaneResponseDecoderTests.swift @@ -24,7 +24,7 @@ final class ControlPlaneResponseDecoderTests: XCTestCase { Content-Type: application/json\r\n\ Lambda-Runtime-Aws-Request-Id: 9028dc49-a01b-4b44-8ffe-4912e9dabbbd\r\n\ Lambda-Runtime-Deadline-Ms: 1638392696671\r\n\ - Lambda-Runtime-Invoked-Function-Arn: arn:aws:lambda:eu-central-1:079477498937:function:lambda-log-http-HelloWorldLambda-NiDlzMFXtF3x\r\n\ + Lambda-Runtime-Invoked-Function-Arn: arn:aws:lambda:eu-central-1:000000000000:function:lambda-log-http-HelloWorldLambda-NiDlzMFXtF3x\r\n\ Lambda-Runtime-Trace-Id: Root=1-61a7e375-40b3edf95b388fe75d1fa416;Parent=348bb48e251c1254;Sampled=0\r\n\ Date: Wed, 01 Dec 2021 21:04:53 GMT\r\n\ Content-Length: 49\r\n\ @@ -35,7 +35,7 @@ final class ControlPlaneResponseDecoderTests: XCTestCase { let invocation = Invocation( requestID: "9028dc49-a01b-4b44-8ffe-4912e9dabbbd", deadlineInMillisSinceEpoch: 1_638_392_696_671, - invokedFunctionARN: "arn:aws:lambda:eu-central-1:079477498937:function:lambda-log-http-HelloWorldLambda-NiDlzMFXtF3x", + invokedFunctionARN: "arn:aws:lambda:eu-central-1:000000000000:function:lambda-log-http-HelloWorldLambda-NiDlzMFXtF3x", traceID: "Root=1-61a7e375-40b3edf95b388fe75d1fa416;Parent=348bb48e251c1254;Sampled=0", clientContext: nil, cognitoIdentity: nil @@ -63,14 +63,14 @@ final class ControlPlaneResponseDecoderTests: XCTestCase { decoderFactory: { ControlPlaneResponseDecoder() } )) } - + func testWhitespaceInHeaderIsRejected() { let nextResponse = ByteBuffer(string: """ HTTP/1.1 200 OK\r\n\ Content-Type: application/json\r\n\ Lambda-Runtime Aws-Request-Id: 9028dc49-a01b-4b44-8ffe-4912e9dabbbd\r\n\ Lambda-Runtime-Deadline-Ms: 1638392696671\r\n\ - Lambda-Runtime-Invoked-Function-Arn: arn:aws:lambda:eu-central-1:079477498937:function:lambda-log-http-HelloWorldLambda-NiDlzMFXtF3x\r\n\ + Lambda-Runtime-Invoked-Function-Arn: arn:aws:lambda:eu-central-1:000000000000:function:lambda-log-http-HelloWorldLambda-NiDlzMFXtF3x\r\n\ Lambda-Runtime-Trace-Id: Root=1-61a7e375-40b3edf95b388fe75d1fa416;Parent=348bb48e251c1254;Sampled=0\r\n\ Date: Wed, 01 Dec 2021 21:04:53 GMT\r\n\ Content-Length: 49\r\n\ @@ -79,17 +79,258 @@ final class ControlPlaneResponseDecoderTests: XCTestCase { """ ) - let pairs: [(ByteBuffer, [ControlPlaneResponse])] = [ - (nextResponse, []) - ] - XCTAssertThrowsError(try ByteToMessageDecoderVerifier.verifyDecoder( - inputOutputPairs: pairs, + inputOutputPairs: [(nextResponse, [])], decoderFactory: { ControlPlaneResponseDecoder() } )) { XCTAssertEqual($0 as? LambdaRuntimeError, .responseHeadHeaderInvalidCharacter) } } + + func testVeryLongHTTPStatusLine() { + let nextResponse = ByteBuffer(repeating: UInt8(ascii: "H"), count: 1024) + + XCTAssertThrowsError(try ByteToMessageDecoderVerifier.verifyDecoder( + inputOutputPairs: [(nextResponse, [])], + decoderFactory: { ControlPlaneResponseDecoder() } + )) { + XCTAssertEqual($0 as? LambdaRuntimeError, .responseHeadMoreThan256BytesBeforeCRLF) + } + } + + func testVeryLongHTTPHeader() { + let acceptedResponse = ByteBuffer(string: """ + HTTP/1.1 202 Accepted\r\n\ + Content-Type: application/json\r\n\ + Date: Sun, 05 Dec 2021 11:53:40 GMT\r\n\ + Content-Length: 16\r\n + """ + ) + ByteBuffer(repeating: UInt8(ascii: "H"), count: 1024) + + XCTAssertThrowsError(try ByteToMessageDecoderVerifier.verifyDecoder( + inputOutputPairs: [(acceptedResponse, [])], + decoderFactory: { ControlPlaneResponseDecoder() } + )) { + XCTAssertEqual($0 as? LambdaRuntimeError, .responseHeadMoreThan256BytesBeforeCRLF) + } + } + + func testNextResponseWithoutTraceID() { + let nextResponse = ByteBuffer(string: """ + HTTP/1.1 200 OK\r\n\ + Content-Type: application/json\r\n\ + Lambda-Runtime-Aws-Request-Id: 9028dc49-a01b-4b44-8ffe-4912e9dabbbd\r\n\ + Lambda-Runtime-Deadline-Ms: 1638392696671\r\n\ + Lambda-Runtime-Invoked-Function-Arn: arn:aws:lambda:eu-central-1:000000000000:function:lambda-log-http-HelloWorldLambda-NiDlzMFXtF3x\r\n\ + Date: Wed, 01 Dec 2021 21:04:53 GMT\r\n\ + Content-Length: 49\r\n\ + \r\n\ + {"name":"Fabian","key2":"value2","key3":"value3"} + """ + ) + + XCTAssertThrowsError(try ByteToMessageDecoderVerifier.verifyDecoder( + inputOutputPairs: [(nextResponse, [])], + decoderFactory: { ControlPlaneResponseDecoder() } + )) { + XCTAssertEqual($0 as? LambdaRuntimeError, .invocationHeadMissingTraceID) + } + } + + func testNextResponseWithoutRequestID() { + let nextResponse = ByteBuffer(string: """ + HTTP/1.1 200 OK\r\n\ + Content-Type: application/json\r\n\ + Lambda-Runtime-Deadline-Ms: 1638392696671\r\n\ + Lambda-Runtime-Invoked-Function-Arn: arn:aws:lambda:eu-central-1:000000000000:function:lambda-log-http-HelloWorldLambda-NiDlzMFXtF3x\r\n\ + Lambda-Runtime-Trace-Id: Root=1-61a7e375-40b3edf95b388fe75d1fa416;Parent=348bb48e251c1254;Sampled=0\r\n\ + Date: Wed, 01 Dec 2021 21:04:53 GMT\r\n\ + Content-Length: 49\r\n\ + \r\n\ + {"name":"Fabian","key2":"value2","key3":"value3"} + """ + ) + + XCTAssertThrowsError(try ByteToMessageDecoderVerifier.verifyDecoder( + inputOutputPairs: [(nextResponse, [])], + decoderFactory: { ControlPlaneResponseDecoder() } + )) { + XCTAssertEqual($0 as? LambdaRuntimeError, .invocationHeadMissingRequestID) + } + } + + func testNextResponseWithInvalidStatusCode() { + let nextResponse = ByteBuffer(string: """ + HTTP/1.1 20 OK\r\n\ + Content-Type: application/json\r\n\ + Lambda-Runtime-Deadline-Ms: 1638392696671\r\n\ + Lambda-Runtime-Invoked-Function-Arn: arn:aws:lambda:eu-central-1:000000000000:function:lambda-log-http-HelloWorldLambda-NiDlzMFXtF3x\r\n\ + Lambda-Runtime-Trace-Id: Root=1-61a7e375-40b3edf95b388fe75d1fa416;Parent=348bb48e251c1254;Sampled=0\r\n\ + Date: Wed, 01 Dec 2021 21:04:53 GMT\r\n\ + Content-Length: 49\r\n\ + \r\n\ + {"name":"Fabian","key2":"value2","key3":"value3"} + """ + ) + + XCTAssertThrowsError(try ByteToMessageDecoderVerifier.verifyDecoder( + inputOutputPairs: [(nextResponse, [])], + decoderFactory: { ControlPlaneResponseDecoder() } + )) { + XCTAssertEqual($0 as? LambdaRuntimeError, .responseHeadInvalidStatusLine) + } + } + + func testNextResponseWithVersionHTTP2() { + let nextResponse = ByteBuffer(string: """ + HTTP/2.0 200 OK\r\n\ + Content-Type: application/json\r\n\ + Lambda-Runtime-Deadline-Ms: 1638392696671\r\n\ + Lambda-Runtime-Invoked-Function-Arn: arn:aws:lambda:eu-central-1:000000000000:function:lambda-log-http-HelloWorldLambda-NiDlzMFXtF3x\r\n\ + Lambda-Runtime-Trace-Id: Root=1-61a7e375-40b3edf95b388fe75d1fa416;Parent=348bb48e251c1254;Sampled=0\r\n\ + Date: Wed, 01 Dec 2021 21:04:53 GMT\r\n\ + Content-Length: 49\r\n\ + \r\n\ + {"name":"Fabian","key2":"value2","key3":"value3"} + """ + ) + + XCTAssertThrowsError(try ByteToMessageDecoderVerifier.verifyDecoder( + inputOutputPairs: [(nextResponse, [])], + decoderFactory: { ControlPlaneResponseDecoder() } + )) { + XCTAssertEqual($0 as? LambdaRuntimeError, .responseHeadInvalidStatusLine) + } + } + + func testNextResponseLeadingAndTrailingWhitespaceHeaders() { + let nextResponse = ByteBuffer(string: """ + HTTP/1.1 200 OK\r\n\ + Content-Type: \t \t application/json\t \t \r\n\ + Lambda-Runtime-Aws-Request-Id: \t \t 9028dc49-a01b-4b44-8ffe-4912e9dabbbd\t \t \r\n\ + Lambda-Runtime-Deadline-Ms: \t \t 1638392696671\t \t \r\n\ + Lambda-Runtime-Invoked-Function-Arn: \t \t arn:aws:lambda:eu-central-1:000000000000:function:lambda-log-http-HelloWorldLambda-NiDlzMFXtF3x\t \t \r\n\ + Lambda-Runtime-Trace-Id: \t \t Root=1-61a7e375-40b3edf95b388fe75d1fa416;Parent=348bb48e251c1254;Sampled=0\t \t \r\n\ + Date: \t \t Wed, 01 Dec 2021 21:04:53 GMT\t \t \r\n\ + Content-Length: \t \t 49\t \t \r\n\ + \r\n\ + {"name":"Fabian","key2":"value2","key3":"value3"} + """ + ) + let invocation = Invocation( + requestID: "9028dc49-a01b-4b44-8ffe-4912e9dabbbd", + deadlineInMillisSinceEpoch: 1_638_392_696_671, + invokedFunctionARN: "arn:aws:lambda:eu-central-1:000000000000:function:lambda-log-http-HelloWorldLambda-NiDlzMFXtF3x", + traceID: "Root=1-61a7e375-40b3edf95b388fe75d1fa416;Parent=348bb48e251c1254;Sampled=0", + clientContext: nil, + cognitoIdentity: nil + ) + let next: ControlPlaneResponse = .next(invocation, ByteBuffer(string: #"{"name":"Fabian","key2":"value2","key3":"value3"}"#)) + + XCTAssertNoThrow(try ByteToMessageDecoderVerifier.verifyDecoder( + inputOutputPairs: [(nextResponse, [next])], + decoderFactory: { ControlPlaneResponseDecoder() } + )) + } + + func testContentLengthHasTrailingCharacterSurroundedByWhitespace() { + let nextResponse = ByteBuffer(string: """ + HTTP/1.1 200 OK\r\n\ + Content-Type: \t \t application/json\t \t \r\n\ + Content-Length: 49 r \r\n\ + Date: \t \t Wed, 01 Dec 2021 21:04:53 GMT\t \t \r\n\ + + \r\n\ + {"name":"Fabian","key2":"value2","key3":"value3"} + """ + ) + + XCTAssertThrowsError(try ByteToMessageDecoderVerifier.verifyDecoder( + inputOutputPairs: [(nextResponse, [])], + decoderFactory: { ControlPlaneResponseDecoder() } + )) { + XCTAssertEqual($0 as? LambdaRuntimeError, .responseHeadInvalidContentLengthValue) + } + } + + func testInvalidContentLength() { + let nextResponse = ByteBuffer(string: """ + HTTP/1.1 200 OK\r\n\ + Content-Type: \t \t application/json\t \t \r\n\ + Content-Length: 4u9 \r\n\ + Date: \t \t Wed, 01 Dec 2021 21:04:53 GMT\t \t \r\n\ + + \r\n\ + {"name":"Fabian","key2":"value2","key3":"value3"} + """ + ) + + XCTAssertThrowsError(try ByteToMessageDecoderVerifier.verifyDecoder( + inputOutputPairs: [(nextResponse, [])], + decoderFactory: { ControlPlaneResponseDecoder() } + )) { + XCTAssertEqual($0 as? LambdaRuntimeError, .responseHeadInvalidContentLengthValue) + } + } + + func testResponseHeaderWithoutColon() { + let nextResponse = ByteBuffer(string: """ + HTTP/1.1 200 OK\r\n\ + Content-Type application/json\r\n\ + Content-Length: 49\r\n\ + Date: Wed, 01 Dec 2021 21:04:53 GMT\r\n\ + + \r\n\ + {"name":"Fabian","key2":"value2","key3":"value3"} + """ + ) + + XCTAssertThrowsError(try ByteToMessageDecoderVerifier.verifyDecoder( + inputOutputPairs: [(nextResponse, [])], + decoderFactory: { ControlPlaneResponseDecoder() } + )) { + XCTAssertEqual($0 as? LambdaRuntimeError, .responseHeadHeaderMissingColon) + } + } + + func testResponseHeaderWithDoubleCR() { + let nextResponse = ByteBuffer(string: """ + HTTP/1.1 200 OK\r\n\ + Content-Type: application/json\r\r\n\ + Content-Length: 49\r\n\ + Date: Wed, 01 Dec 2021 21:04:53 GMT\r\n\ + + \r\n\ + {"name":"Fabian","key2":"value2","key3":"value3"} + """ + ) + + XCTAssertThrowsError(try ByteToMessageDecoderVerifier.verifyDecoder( + inputOutputPairs: [(nextResponse, [])], + decoderFactory: { ControlPlaneResponseDecoder() } + )) { + XCTAssertEqual($0 as? LambdaRuntimeError, .responseHeadInvalidHeader) + } + } + + func testResponseHeaderWithoutValue() { + let nextResponse = ByteBuffer(string: """ + HTTP/1.1 200 OK\r\n\ + Content-Type: \r\n\ + Content-Length: 49\r\n\ + Date: Wed, 01 Dec 2021 21:04:53 GMT\r\n\ + \r\n\ + {"name":"Fabian","key2":"value2","key3":"value3"} + """ + ) + + XCTAssertThrowsError(try ByteToMessageDecoderVerifier.verifyDecoder( + inputOutputPairs: [(nextResponse, [])], + decoderFactory: { ControlPlaneResponseDecoder() } + )) { + XCTAssertEqual($0 as? LambdaRuntimeError, .responseHeadInvalidHeader) + } + } } extension ByteBuffer { From c84850959a9b5ed389e690fca8f4283180e17692 Mon Sep 17 00:00:00 2001 From: Fabian Fett Date: Wed, 16 Feb 2022 23:10:42 +0100 Subject: [PATCH 7/7] Code review --- .../ControlPlaneRequest.swift | 6 +- .../ControlPlaneResponseDecoder.swift | 56 +++++++++++------- .../LambdaRuntimeError.swift | 47 +++++++++------ Sources/AWSLambdaRuntimeCore/Utils.swift | 4 +- .../ControlPlaneResponseDecoderTests.swift | 57 +++++++++++++++++++ Tests/AWSLambdaRuntimeCoreTests/Utils.swift | 4 +- 6 files changed, 128 insertions(+), 46 deletions(-) diff --git a/Sources/AWSLambdaRuntimeCore/ControlPlaneRequest.swift b/Sources/AWSLambdaRuntimeCore/ControlPlaneRequest.swift index a9808ba4..a640ff71 100644 --- a/Sources/AWSLambdaRuntimeCore/ControlPlaneRequest.swift +++ b/Sources/AWSLambdaRuntimeCore/ControlPlaneRequest.swift @@ -30,14 +30,14 @@ enum ControlPlaneResponse: Hashable { struct Invocation: Hashable { var requestID: String - var deadlineInMillisSinceEpoch: Int64 + var deadlineInMillisSinceEpoch: UInt64 var invokedFunctionARN: String var traceID: String var clientContext: String? var cognitoIdentity: String? init(requestID: String, - deadlineInMillisSinceEpoch: Int64, + deadlineInMillisSinceEpoch: UInt64, invokedFunctionARN: String, traceID: String, clientContext: String?, @@ -56,7 +56,7 @@ struct Invocation: Hashable { } guard let deadline = headers.first(name: AmazonHeaders.deadline), - let unixTimeInMilliseconds = Int64(deadline) + let unixTimeInMilliseconds = UInt64(deadline) else { throw Lambda.RuntimeError.invocationMissingHeader(AmazonHeaders.deadline) } diff --git a/Sources/AWSLambdaRuntimeCore/ControlPlaneResponseDecoder.swift b/Sources/AWSLambdaRuntimeCore/ControlPlaneResponseDecoder.swift index 4f3a44af..f47cf24d 100644 --- a/Sources/AWSLambdaRuntimeCore/ControlPlaneResponseDecoder.swift +++ b/Sources/AWSLambdaRuntimeCore/ControlPlaneResponseDecoder.swift @@ -113,7 +113,7 @@ struct ControlPlaneResponseDecoder: NIOSingleStepByteToMessageDecoder { return .decoded(head) case .contentLength(let length): - head.contentLength = length // TODO: This can crash + head.contentLength = length case .contentType: break // switch @@ -145,6 +145,8 @@ struct ControlPlaneResponseDecoder: NIOSingleStepByteToMessageDecoder { case none } + static let sixMegaBytes = 6 * 1024 * 1024 + private mutating func decodeBody(from buffer: inout ByteBuffer) throws -> DecodeResult { switch self.state { case .waitingForBody(let partialHead): @@ -152,7 +154,8 @@ struct ControlPlaneResponseDecoder: NIOSingleStepByteToMessageDecoder { case .none: return .decoded(nil) case .some(let length): - if let slice = buffer.readSlice(length: length) { + precondition(length <= Self.sixMegaBytes) + if let slice = buffer.readSlice(length: Int(length)) { self.state = .waitingForNewResponse return .decoded(slice) } @@ -168,13 +171,15 @@ struct ControlPlaneResponseDecoder: NIOSingleStepByteToMessageDecoder { switch head.statusCode { case 200: guard let body = body else { - preconditionFailure("TODO: implement") + throw LambdaRuntimeError.invocationMissingPayload } return .next(try Invocation(head: head), body) + case 202: return .accepted - case 400 ..< 600: - preconditionFailure("TODO: implement") + + case 400, 403: + throw LambdaRuntimeError.controlPlaneErrorResponse(<#T##response: ErrorResponse##ErrorResponse#>) default: throw LambdaRuntimeError.unexpectedStatusCode @@ -223,9 +228,9 @@ struct ControlPlaneResponseDecoder: NIOSingleStepByteToMessageDecoder { private enum HeaderLineContent: Equatable { case traceID(String) case contentType - case contentLength(Int) + case contentLength(UInt64) case cognitoIdentity(String) - case deadlineMS(Int) + case deadlineMS(UInt64) case functionARN(String) case requestID(LambdaRequestID) @@ -268,20 +273,21 @@ struct ControlPlaneResponseDecoder: NIOSingleStepByteToMessageDecoder { if buffer.readHeaderName("content-length") { buffer.moveReaderIndex(forwardBy: 1) // move forward for colon try self.decodeOptionalWhiteSpaceBeforeFieldValue(from: &buffer) - guard let length = buffer.readIntegerFromHeader() else { + guard let length = buffer.readUInt64FromHeader() else { + throw LambdaRuntimeError.responseHeadInvalidContentLengthValue + } + guard length < 6 * 1024 * 1024 else { + // A lambda must not have a body larger than 6MB: + // https://docs.aws.amazon.com/lambda/latest/dg/gettingstarted-limits.html throw LambdaRuntimeError.responseHeadInvalidContentLengthValue } + return .contentLength(length) } case 17: if buffer.readHeaderName("transfer-encoding") { - buffer.moveReaderIndex(forwardBy: 1) // move forward for colon - try self.decodeOptionalWhiteSpaceBeforeFieldValue(from: &buffer) - guard let length = buffer.readIntegerFromHeader() else { - throw LambdaRuntimeError.responseHeadInvalidDeadlineValue - } - return .contentLength(length) + throw LambdaRuntimeError.responseHeadTransferEncodingChunkedNotSupported } case 23: @@ -297,8 +303,8 @@ struct ControlPlaneResponseDecoder: NIOSingleStepByteToMessageDecoder { if buffer.readHeaderName("lambda-runtime-deadline-ms") { buffer.moveReaderIndex(forwardBy: 1) // move forward for colon try self.decodeOptionalWhiteSpaceBeforeFieldValue(from: &buffer) - guard let deadline = buffer.readIntegerFromHeader() else { - throw LambdaRuntimeError.responseHeadInvalidContentLengthValue + guard let deadline = buffer.readUInt64FromHeader() else { + throw LambdaRuntimeError.responseHeadInvalidDeadlineValue } return .deadlineMS(deadline) } @@ -399,10 +405,10 @@ struct ControlPlaneResponseDecoder: NIOSingleStepByteToMessageDecoder { extension ControlPlaneResponseDecoder { fileprivate struct PartialHead { var statusCode: Int - var contentLength: Int? + var contentLength: UInt64? var requestID: LambdaRequestID? - var deadlineInMillisSinceEpoch: Int? + var deadlineInMillisSinceEpoch: UInt64? var invokedFunctionARN: String? var traceID: String? var clientContext: String? @@ -468,16 +474,22 @@ extension ByteBuffer { return false } - mutating func readIntegerFromHeader() -> Int? { + // We must ensure that we can multiply our current value by ten and add at least 9. + private static let maxIncreasableUInt64 = UInt64.max / 100 + + mutating func readUInt64FromHeader() -> UInt64? { guard let ascii = self.readInteger(as: UInt8.self), UInt8(ascii: "0") <= ascii && ascii <= UInt8(ascii: "9") else { return nil } - var value = Int(ascii - UInt8(ascii: "0")) + var value = UInt64(ascii - UInt8(ascii: "0")) loop: while let ascii = self.readInteger(as: UInt8.self) { switch ascii { case UInt8(ascii: "0") ... UInt8(ascii: "9"): + if value > Self.maxIncreasableUInt64 { + return nil + } value = value * 10 - value += Int(ascii - UInt8(ascii: "0")) + value += UInt64(ascii - UInt8(ascii: "0")) case UInt8(ascii: " "), UInt8(ascii: "\t"): // verify that all following characters are also whitespace @@ -515,7 +527,7 @@ extension Invocation { self = Invocation( requestID: requestID.lowercased, - deadlineInMillisSinceEpoch: Int64(deadlineInMillisSinceEpoch), + deadlineInMillisSinceEpoch: deadlineInMillisSinceEpoch, invokedFunctionARN: invokedFunctionARN, traceID: traceID, clientContext: head.clientContext, diff --git a/Sources/AWSLambdaRuntimeCore/LambdaRuntimeError.swift b/Sources/AWSLambdaRuntimeCore/LambdaRuntimeError.swift index 4c91c90a..e197171d 100644 --- a/Sources/AWSLambdaRuntimeCore/LambdaRuntimeError.swift +++ b/Sources/AWSLambdaRuntimeCore/LambdaRuntimeError.swift @@ -16,8 +16,12 @@ struct LambdaRuntimeError: Error, Hashable { enum Base: Hashable { case unsolicitedResponse case unexpectedStatusCode + case statusCodeBadRequest + case statusCodeForbidden + case containerError case responseHeadInvalidStatusLine + case responseHeadTransferEncodingChunkedNotSupported case responseHeadMissingContentLengthOrTransferEncodingChunked case responseHeadMoreThan256BytesBeforeCRLF case responseHeadHeaderInvalidCharacter @@ -34,6 +38,7 @@ struct LambdaRuntimeError: Error, Hashable { case invocationHeadMissingFunctionARN case invocationHeadMissingTraceID + case invocationMissingPayload case controlPlaneErrorResponse(ErrorResponse) } @@ -43,25 +48,33 @@ struct LambdaRuntimeError: Error, Hashable { self.base = base } - static var unsolicitedResponse = LambdaRuntimeError(.unsolicitedResponse) - static var unexpectedStatusCode = LambdaRuntimeError(.unexpectedStatusCode) - static var responseHeadInvalidStatusLine = LambdaRuntimeError(.responseHeadInvalidStatusLine) - static var responseHeadMissingContentLengthOrTransferEncodingChunked = + static let unsolicitedResponse = LambdaRuntimeError(.unsolicitedResponse) + static let unexpectedStatusCode = LambdaRuntimeError(.unexpectedStatusCode) + static let statusCodeBadRequest = LambdaRuntimeError(.statusCodeBadRequest) + static let statusCodeForbidden = LambdaRuntimeError(.statusCodeForbidden) + static let containerError = LambdaRuntimeError(.containerError) + + static let responseHeadInvalidStatusLine = LambdaRuntimeError(.responseHeadInvalidStatusLine) + static let responseHeadTransferEncodingChunkedNotSupported = + LambdaRuntimeError(.responseHeadTransferEncodingChunkedNotSupported) + static let responseHeadMissingContentLengthOrTransferEncodingChunked = LambdaRuntimeError(.responseHeadMissingContentLengthOrTransferEncodingChunked) - static var responseHeadMoreThan256BytesBeforeCRLF = LambdaRuntimeError(.responseHeadMoreThan256BytesBeforeCRLF) - static var responseHeadHeaderInvalidCharacter = LambdaRuntimeError(.responseHeadHeaderInvalidCharacter) - static var responseHeadHeaderMissingColon = LambdaRuntimeError(.responseHeadHeaderMissingColon) - static var responseHeadHeaderMissingFieldValue = LambdaRuntimeError(.responseHeadHeaderMissingFieldValue) - static var responseHeadInvalidHeader = LambdaRuntimeError(.responseHeadInvalidHeader) - static var responseHeadInvalidContentLengthValue = LambdaRuntimeError(.responseHeadInvalidContentLengthValue) - static var responseHeadInvalidRequestIDValue = LambdaRuntimeError(.responseHeadInvalidRequestIDValue) - static var responseHeadInvalidTraceIDValue = LambdaRuntimeError(.responseHeadInvalidTraceIDValue) - static var responseHeadInvalidDeadlineValue = LambdaRuntimeError(.responseHeadInvalidDeadlineValue) + static let responseHeadMoreThan256BytesBeforeCRLF = LambdaRuntimeError(.responseHeadMoreThan256BytesBeforeCRLF) + static let responseHeadHeaderInvalidCharacter = LambdaRuntimeError(.responseHeadHeaderInvalidCharacter) + static let responseHeadHeaderMissingColon = LambdaRuntimeError(.responseHeadHeaderMissingColon) + static let responseHeadHeaderMissingFieldValue = LambdaRuntimeError(.responseHeadHeaderMissingFieldValue) + static let responseHeadInvalidHeader = LambdaRuntimeError(.responseHeadInvalidHeader) + static let responseHeadInvalidContentLengthValue = LambdaRuntimeError(.responseHeadInvalidContentLengthValue) + static let responseHeadInvalidRequestIDValue = LambdaRuntimeError(.responseHeadInvalidRequestIDValue) + static let responseHeadInvalidTraceIDValue = LambdaRuntimeError(.responseHeadInvalidTraceIDValue) + static let responseHeadInvalidDeadlineValue = LambdaRuntimeError(.responseHeadInvalidDeadlineValue) + + static let invocationHeadMissingRequestID = LambdaRuntimeError(.invocationHeadMissingRequestID) + static let invocationHeadMissingDeadlineInMillisSinceEpoch = LambdaRuntimeError(.invocationHeadMissingDeadlineInMillisSinceEpoch) + static let invocationHeadMissingFunctionARN = LambdaRuntimeError(.invocationHeadMissingFunctionARN) + static let invocationHeadMissingTraceID = LambdaRuntimeError(.invocationHeadMissingTraceID) - static var invocationHeadMissingRequestID = LambdaRuntimeError(.invocationHeadMissingRequestID) - static var invocationHeadMissingDeadlineInMillisSinceEpoch = LambdaRuntimeError(.invocationHeadMissingDeadlineInMillisSinceEpoch) - static var invocationHeadMissingFunctionARN = LambdaRuntimeError(.invocationHeadMissingFunctionARN) - static var invocationHeadMissingTraceID = LambdaRuntimeError(.invocationHeadMissingTraceID) + static let invocationMissingPayload = LambdaRuntimeError(.invocationMissingPayload) static func controlPlaneErrorResponse(_ response: ErrorResponse) -> Self { LambdaRuntimeError(.controlPlaneErrorResponse(response)) diff --git a/Sources/AWSLambdaRuntimeCore/Utils.swift b/Sources/AWSLambdaRuntimeCore/Utils.swift index 9924a05b..99f167d2 100644 --- a/Sources/AWSLambdaRuntimeCore/Utils.swift +++ b/Sources/AWSLambdaRuntimeCore/Utils.swift @@ -59,8 +59,8 @@ internal enum Signal: Int32 { } extension DispatchWallTime { - internal init(millisSinceEpoch: Int64) { - let nanoSinceEpoch = UInt64(millisSinceEpoch) * 1_000_000 + internal init(millisSinceEpoch: UInt64) { + let nanoSinceEpoch = millisSinceEpoch * 1_000_000 let seconds = UInt64(nanoSinceEpoch / 1_000_000_000) let nanoseconds = nanoSinceEpoch - (seconds * 1_000_000_000) self.init(timespec: timespec(tv_sec: Int(seconds), tv_nsec: Int(nanoseconds))) diff --git a/Tests/AWSLambdaRuntimeCoreTests/ControlPlaneResponseDecoderTests.swift b/Tests/AWSLambdaRuntimeCoreTests/ControlPlaneResponseDecoderTests.swift index 45c5f61a..2d0d984f 100644 --- a/Tests/AWSLambdaRuntimeCoreTests/ControlPlaneResponseDecoderTests.swift +++ b/Tests/AWSLambdaRuntimeCoreTests/ControlPlaneResponseDecoderTests.swift @@ -331,6 +331,63 @@ final class ControlPlaneResponseDecoderTests: XCTestCase { XCTAssertEqual($0 as? LambdaRuntimeError, .responseHeadInvalidHeader) } } + + func testResponseHeadWithToLargeContentLengthHeaderValue() { + let nextResponse = ByteBuffer(string: """ + HTTP/1.1 200 OK\r\n\ + Content-Type: \r\n\ + Content-Length: 18446744073709551615\r\n\ + Date: Wed, 01 Dec 2021 21:04:53 GMT\r\n\ + \r\n\ + {"name":"Fabian","key2":"value2","key3":"value3"} + """ + ) + + XCTAssertThrowsError(try ByteToMessageDecoderVerifier.verifyDecoder( + inputOutputPairs: [(nextResponse, [])], + decoderFactory: { ControlPlaneResponseDecoder() } + )) { + XCTAssertEqual($0 as? LambdaRuntimeError, .responseHeadInvalidContentLengthValue) + } + } + + func testResponseHeadWithDeadlineThatIsToLArgeForUInt64() { + let nextResponse = ByteBuffer(string: """ + HTTP/1.1 200 OK\r\n\ + Content-Type: \r\n\ + Lambda-Runtime-Deadline-Ms: 18446744073709551615\r\n\ + Date: Wed, 01 Dec 2021 21:04:53 GMT\r\n\ + \r\n\ + {"name":"Fabian","key2":"value2","key3":"value3"} + """ + ) + + XCTAssertThrowsError(try ByteToMessageDecoderVerifier.verifyDecoder( + inputOutputPairs: [(nextResponse, [])], + decoderFactory: { ControlPlaneResponseDecoder() } + )) { + XCTAssertEqual($0 as? LambdaRuntimeError, .responseHeadInvalidDeadlineValue) + } + } + + func testResponseHeadWithContentLengthLargerThan6MB() { + let nextResponse = ByteBuffer(string: """ + HTTP/1.1 200 OK\r\n\ + Content-Type: \r\n\ + Content-Length: \(6 * 1024 * 1024 + 1)\r\n\ + Date: Wed, 01 Dec 2021 21:04:53 GMT\r\n\ + \r\n\ + {"name":"Fabian","key2":"value2","key3":"value3"} + """ + ) + + XCTAssertThrowsError(try ByteToMessageDecoderVerifier.verifyDecoder( + inputOutputPairs: [(nextResponse, [])], + decoderFactory: { ControlPlaneResponseDecoder() } + )) { + XCTAssertEqual($0 as? LambdaRuntimeError, .responseHeadInvalidContentLengthValue) + } + } } extension ByteBuffer { diff --git a/Tests/AWSLambdaRuntimeCoreTests/Utils.swift b/Tests/AWSLambdaRuntimeCoreTests/Utils.swift index 96b1a1c8..46c59807 100644 --- a/Tests/AWSLambdaRuntimeCoreTests/Utils.swift +++ b/Tests/AWSLambdaRuntimeCoreTests/Utils.swift @@ -55,8 +55,8 @@ struct TestError: Error, Equatable, CustomStringConvertible { } extension Date { - internal var millisSinceEpoch: Int64 { - Int64(self.timeIntervalSince1970 * 1000) + internal var millisSinceEpoch: UInt64 { + UInt64(self.timeIntervalSince1970 * 1000) } }