diff --git a/Sources/AWSLambdaRuntimeCore/ControlPlaneRequest.swift b/Sources/AWSLambdaRuntimeCore/ControlPlaneRequest.swift index 48da5237..a640ff71 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,26 @@ 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: UInt64 + var invokedFunctionARN: String + var traceID: String + var clientContext: String? + var cognitoIdentity: String? + + init(requestID: String, + deadlineInMillisSinceEpoch: UInt64, + 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 { @@ -42,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) } @@ -55,12 +69,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..f47cf24d --- /dev/null +++ b/Sources/AWSLambdaRuntimeCore/ControlPlaneResponseDecoder.swift @@ -0,0 +1,537 @@ +//===----------------------------------------------------------------------===// +// +// 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 + +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 + + 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 .ignore: + break // switch + } + } + } + + enum BodyEncoding { + case chunked + case plain(length: Int) + 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): + switch partialHead.contentLength { + case .none: + return .decoded(nil) + case .some(let length): + precondition(length <= Self.sixMegaBytes) + if let slice = buffer.readSlice(length: Int(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 { + throw LambdaRuntimeError.invocationMissingPayload + } + return .next(try Invocation(head: head), body) + + case 202: + return .accepted + + case 400, 403: + throw LambdaRuntimeError.controlPlaneErrorResponse(<#T##response: ErrorResponse##ErrorResponse#>) + + default: + throw LambdaRuntimeError.unexpectedStatusCode + } + } + + mutating func decodeStatusLine(from buffer: inout ByteBuffer) throws -> Int { + guard buffer.readableBytes >= 11 else { + throw LambdaRuntimeError.responseHeadInvalidStatusLine + } + + guard buffer.readString("HTTP/1.1 ") 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(UInt64) + case cognitoIdentity(String) + case deadlineMS(UInt64) + case functionARN(String) + case requestID(LambdaRequestID) + + case ignore + 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 .ignore + } + + case 12: + if buffer.readHeaderName("content-type") { + return .ignore + } + + case 14: + if buffer.readHeaderName("content-length") { + buffer.moveReaderIndex(forwardBy: 1) // move forward for colon + try self.decodeOptionalWhiteSpaceBeforeFieldValue(from: &buffer) + 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") { + throw LambdaRuntimeError.responseHeadTransferEncodingChunkedNotSupported + } + + 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.readUInt64FromHeader() else { + throw LambdaRuntimeError.responseHeadInvalidDeadlineValue + } + 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 .ignore + } + + case 31: + if buffer.readHeaderName("lambda-runtime-cognito-identity") { + return .ignore + } + + 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: + // 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 ..< 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"), + 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 + } + + @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: UInt64? + + var requestID: LambdaRequestID? + var deadlineInMillisSinceEpoch: UInt64? + 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 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 + 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 + } + + // 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 = 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 += UInt64(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 + } +} + +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: 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..e197171d --- /dev/null +++ b/Sources/AWSLambdaRuntimeCore/LambdaRuntimeError.swift @@ -0,0 +1,82 @@ +//===----------------------------------------------------------------------===// +// +// 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 statusCodeBadRequest + case statusCodeForbidden + case containerError + + case responseHeadInvalidStatusLine + case responseHeadTransferEncodingChunkedNotSupported + case responseHeadMissingContentLengthOrTransferEncodingChunked + case responseHeadMoreThan256BytesBeforeCRLF + case responseHeadHeaderInvalidCharacter + case responseHeadHeaderMissingColon + case responseHeadHeaderMissingFieldValue + case responseHeadInvalidHeader + case responseHeadInvalidContentLengthValue + case responseHeadInvalidRequestIDValue + case responseHeadInvalidTraceIDValue + case responseHeadInvalidDeadlineValue + + case invocationHeadMissingRequestID + case invocationHeadMissingDeadlineInMillisSinceEpoch + case invocationHeadMissingFunctionARN + case invocationHeadMissingTraceID + + case invocationMissingPayload + case controlPlaneErrorResponse(ErrorResponse) + } + + private let base: Base + + private init(_ base: Base) { + self.base = base + } + + 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 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 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 new file mode 100644 index 00000000..2d0d984f --- /dev/null +++ b/Tests/AWSLambdaRuntimeCoreTests/ControlPlaneResponseDecoderTests.swift @@ -0,0 +1,400 @@ +//===----------------------------------------------------------------------===// +// +// 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 NIOTestUtils +import XCTest + +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: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"} + """ + ) + 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"}"#)) + + 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() } + )) + } + + 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: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, .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) + } + } + + 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 { + static func + (lhs: Self, rhs: Self) -> ByteBuffer { + var new = lhs + var rhs = rhs + new.writeBuffer(&rhs) + return new + } +} 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) } } 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... "