diff --git a/Sources/AWSLambdaEvents/ALB.swift b/Sources/AWSLambdaEvents/ALB.swift new file mode 100644 index 00000000..121531a6 --- /dev/null +++ b/Sources/AWSLambdaEvents/ALB.swift @@ -0,0 +1,77 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2017-2020 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 class Foundation.JSONEncoder + +// https://github.com/aws/aws-lambda-go/blob/master/events/alb.go +public enum ALB { + /// ALBTargetGroupRequest contains data originating from the ALB Lambda target group integration + public struct TargetGroupRequest: Codable { + /// ALBTargetGroupRequestContext contains the information to identify the load balancer invoking the lambda + public struct Context: Codable { + public let elb: ELBContext + } + + public let httpMethod: HTTPMethod + public let path: String + public let queryStringParameters: [String: [String]] + + /// Depending on your configuration of your target group either `headers` or `multiValueHeaders` + /// are set. + /// + /// For more information visit: + /// https://docs.aws.amazon.com/elasticloadbalancing/latest/application/lambda-functions.html#multi-value-headers + public let headers: HTTPHeaders? + + /// Depending on your configuration of your target group either `headers` or `multiValueHeaders` + /// are set. + /// + /// For more information visit: + /// https://docs.aws.amazon.com/elasticloadbalancing/latest/application/lambda-functions.html#multi-value-headers + public let multiValueHeaders: HTTPMultiValueHeaders? + public let requestContext: Context + public let isBase64Encoded: Bool + public let body: String? + } + + /// ELBContext contains the information to identify the ARN invoking the lambda + public struct ELBContext: Codable { + public let targetGroupArn: String + } + + public struct TargetGroupResponse: Codable { + public let statusCode: HTTPResponseStatus + public let statusDescription: String? + public let headers: HTTPHeaders? + public let multiValueHeaders: HTTPMultiValueHeaders? + public let body: String + public let isBase64Encoded: Bool + + public init( + statusCode: HTTPResponseStatus, + statusDescription: String? = nil, + headers: HTTPHeaders? = nil, + multiValueHeaders: HTTPMultiValueHeaders? = nil, + body: String = "", + isBase64Encoded: Bool = false + ) { + self.statusCode = statusCode + self.statusDescription = statusDescription + self.headers = headers + self.multiValueHeaders = multiValueHeaders + self.body = body + self.isBase64Encoded = isBase64Encoded + } + } +} diff --git a/Sources/AWSLambdaEvents/APIGateway+V2.swift b/Sources/AWSLambdaEvents/APIGateway+V2.swift new file mode 100644 index 00000000..12c4c2ce --- /dev/null +++ b/Sources/AWSLambdaEvents/APIGateway+V2.swift @@ -0,0 +1,119 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2017-2020 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 +// +//===----------------------------------------------------------------------===// + +extension APIGateway { + public struct V2 {} +} + +extension APIGateway.V2 { + /// APIGateway.V2.Request contains data coming from the new HTTP API Gateway + public struct Request: Codable { + /// Context contains the information to identify the AWS account and resources invoking the Lambda function. + public struct Context: Codable { + public struct HTTP: Codable { + public let method: HTTPMethod + public let path: String + public let `protocol`: String + public let sourceIp: String + public let userAgent: String + } + + /// Authorizer contains authorizer information for the request context. + public struct Authorizer: Codable { + /// JWT contains JWT authorizer information for the request context. + public struct JWT: Codable { + public let claims: [String: String] + public let scopes: [String]? + } + + let jwt: JWT + } + + public let accountId: String + public let apiId: String + public let domainName: String + public let domainPrefix: String + public let stage: String + public let requestId: String + + public let http: HTTP + public let authorizer: Authorizer? + + /// The request time in format: 23/Apr/2020:11:08:18 +0000 + public let time: String + public let timeEpoch: UInt64 + } + + public let version: String + public let routeKey: String + public let rawPath: String + public let rawQueryString: String + + public let cookies: [String]? + public let headers: HTTPHeaders + public let queryStringParameters: [String: String]? + public let pathParameters: [String: String]? + + public let context: Context + public let stageVariables: [String: String]? + + public let body: String? + public let isBase64Encoded: Bool + + enum CodingKeys: String, CodingKey { + case version + case routeKey + case rawPath + case rawQueryString + + case cookies + case headers + case queryStringParameters + case pathParameters + + case context = "requestContext" + case stageVariables + + case body + case isBase64Encoded + } + } +} + +extension APIGateway.V2 { + public struct Response: Codable { + public let statusCode: HTTPResponseStatus + public let headers: HTTPHeaders? + public let multiValueHeaders: HTTPMultiValueHeaders? + public let body: String? + public let isBase64Encoded: Bool? + public let cookies: [String]? + + public init( + statusCode: HTTPResponseStatus, + headers: HTTPHeaders? = nil, + multiValueHeaders: HTTPMultiValueHeaders? = nil, + body: String? = nil, + isBase64Encoded: Bool? = nil, + cookies: [String]? = nil + ) { + self.statusCode = statusCode + self.headers = headers + self.multiValueHeaders = multiValueHeaders + self.body = body + self.isBase64Encoded = isBase64Encoded + self.cookies = cookies + } + } +} diff --git a/Sources/AWSLambdaEvents/APIGateway.swift b/Sources/AWSLambdaEvents/APIGateway.swift new file mode 100644 index 00000000..948aca25 --- /dev/null +++ b/Sources/AWSLambdaEvents/APIGateway.swift @@ -0,0 +1,92 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2017-2020 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 class Foundation.JSONEncoder + +// https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html + +public enum APIGateway { + /// APIGatewayRequest contains data coming from the API Gateway + public struct Request: Codable { + public struct Context: Codable { + public struct Identity: Codable { + public let cognitoIdentityPoolId: String? + + public let apiKey: String? + public let userArn: String? + public let cognitoAuthenticationType: String? + public let caller: String? + public let userAgent: String? + public let user: String? + + public let cognitoAuthenticationProvider: String? + public let sourceIp: String? + public let accountId: String? + } + + public let resourceId: String + public let apiId: String + public let resourcePath: String + public let httpMethod: String + public let requestId: String + public let accountId: String + public let stage: String + + public let identity: Identity + public let extendedRequestId: String? + public let path: String + } + + public let resource: String + public let path: String + public let httpMethod: HTTPMethod + + public let queryStringParameters: [String: String]? + public let multiValueQueryStringParameters: [String: [String]]? + public let headers: HTTPHeaders + public let multiValueHeaders: HTTPMultiValueHeaders + public let pathParameters: [String: String]? + public let stageVariables: [String: String]? + + public let requestContext: Context + public let body: String? + public let isBase64Encoded: Bool + } +} + +// MARK: - Response - + +extension APIGateway { + public struct Response: Codable { + public let statusCode: HTTPResponseStatus + public let headers: HTTPHeaders? + public let multiValueHeaders: HTTPMultiValueHeaders? + public let body: String? + public let isBase64Encoded: Bool? + + public init( + statusCode: HTTPResponseStatus, + headers: HTTPHeaders? = nil, + multiValueHeaders: HTTPMultiValueHeaders? = nil, + body: String? = nil, + isBase64Encoded: Bool? = nil + ) { + self.statusCode = statusCode + self.headers = headers + self.multiValueHeaders = multiValueHeaders + self.body = body + self.isBase64Encoded = isBase64Encoded + } + } +} diff --git a/Sources/AWSLambdaEvents/Utils/Base64.swift b/Sources/AWSLambdaEvents/Utils/Base64.swift index 66f16080..310a6aa5 100644 --- a/Sources/AWSLambdaEvents/Utils/Base64.swift +++ b/Sources/AWSLambdaEvents/Utils/Base64.swift @@ -18,136 +18,6 @@ struct Base64 {} -// MARK: Encoding - -extension Base64 { - struct EncodingOptions: OptionSet { - let rawValue: UInt - init(rawValue: UInt) { self.rawValue = rawValue } - - static let base64UrlAlphabet = EncodingOptions(rawValue: UInt(1 << 0)) - } - - /// Base64 encode a collection of UInt8 to a string, without the use of Foundation. - /// - /// This function performs the world's most naive Base64 encoding: no attempts to use a larger - /// lookup table or anything intelligent like that, just shifts and masks. This works fine, for - /// now: the purpose of this encoding is to avoid round-tripping through Data, and the perf gain - /// from avoiding that is more than enough to outweigh the silliness of this code. - @inline(__always) - static func encode(bytes: Buffer, options: EncodingOptions = []) - -> String where Buffer.Element == UInt8 { - // In Base64, 3 bytes become 4 output characters, and we pad to the - // nearest multiple of four. - let newCapacity = ((bytes.count + 2) / 3) * 4 - let alphabet = options.contains(.base64UrlAlphabet) - ? Base64.encodeBase64Url - : Base64.encodeBase64 - - var outputBytes = [UInt8]() - outputBytes.reserveCapacity(newCapacity) - - var input = bytes.makeIterator() - - while let firstByte = input.next() { - let secondByte = input.next() - let thirdByte = input.next() - - let firstChar = Base64.encode(alphabet: alphabet, firstByte: firstByte) - let secondChar = Base64.encode(alphabet: alphabet, firstByte: firstByte, secondByte: secondByte) - let thirdChar = Base64.encode(alphabet: alphabet, secondByte: secondByte, thirdByte: thirdByte) - let forthChar = Base64.encode(alphabet: alphabet, thirdByte: thirdByte) - - outputBytes.append(firstChar) - outputBytes.append(secondChar) - outputBytes.append(thirdChar) - outputBytes.append(forthChar) - } - - return String(decoding: outputBytes, as: Unicode.UTF8.self) - } - - // MARK: Internal - - // The base64 unicode table. - @usableFromInline - static let encodeBase64: [UInt8] = [ - UInt8(ascii: "A"), UInt8(ascii: "B"), UInt8(ascii: "C"), UInt8(ascii: "D"), - UInt8(ascii: "E"), UInt8(ascii: "F"), UInt8(ascii: "G"), UInt8(ascii: "H"), - UInt8(ascii: "I"), UInt8(ascii: "J"), UInt8(ascii: "K"), UInt8(ascii: "L"), - UInt8(ascii: "M"), UInt8(ascii: "N"), UInt8(ascii: "O"), UInt8(ascii: "P"), - UInt8(ascii: "Q"), UInt8(ascii: "R"), UInt8(ascii: "S"), UInt8(ascii: "T"), - UInt8(ascii: "U"), UInt8(ascii: "V"), UInt8(ascii: "W"), UInt8(ascii: "X"), - UInt8(ascii: "Y"), UInt8(ascii: "Z"), UInt8(ascii: "a"), UInt8(ascii: "b"), - UInt8(ascii: "c"), UInt8(ascii: "d"), UInt8(ascii: "e"), UInt8(ascii: "f"), - UInt8(ascii: "g"), UInt8(ascii: "h"), UInt8(ascii: "i"), UInt8(ascii: "j"), - UInt8(ascii: "k"), UInt8(ascii: "l"), UInt8(ascii: "m"), UInt8(ascii: "n"), - UInt8(ascii: "o"), UInt8(ascii: "p"), UInt8(ascii: "q"), UInt8(ascii: "r"), - UInt8(ascii: "s"), UInt8(ascii: "t"), UInt8(ascii: "u"), UInt8(ascii: "v"), - UInt8(ascii: "w"), UInt8(ascii: "x"), UInt8(ascii: "y"), UInt8(ascii: "z"), - UInt8(ascii: "0"), UInt8(ascii: "1"), UInt8(ascii: "2"), UInt8(ascii: "3"), - UInt8(ascii: "4"), UInt8(ascii: "5"), UInt8(ascii: "6"), UInt8(ascii: "7"), - UInt8(ascii: "8"), UInt8(ascii: "9"), UInt8(ascii: "+"), UInt8(ascii: "/"), - ] - - @usableFromInline - static let encodeBase64Url: [UInt8] = [ - UInt8(ascii: "A"), UInt8(ascii: "B"), UInt8(ascii: "C"), UInt8(ascii: "D"), - UInt8(ascii: "E"), UInt8(ascii: "F"), UInt8(ascii: "G"), UInt8(ascii: "H"), - UInt8(ascii: "I"), UInt8(ascii: "J"), UInt8(ascii: "K"), UInt8(ascii: "L"), - UInt8(ascii: "M"), UInt8(ascii: "N"), UInt8(ascii: "O"), UInt8(ascii: "P"), - UInt8(ascii: "Q"), UInt8(ascii: "R"), UInt8(ascii: "S"), UInt8(ascii: "T"), - UInt8(ascii: "U"), UInt8(ascii: "V"), UInt8(ascii: "W"), UInt8(ascii: "X"), - UInt8(ascii: "Y"), UInt8(ascii: "Z"), UInt8(ascii: "a"), UInt8(ascii: "b"), - UInt8(ascii: "c"), UInt8(ascii: "d"), UInt8(ascii: "e"), UInt8(ascii: "f"), - UInt8(ascii: "g"), UInt8(ascii: "h"), UInt8(ascii: "i"), UInt8(ascii: "j"), - UInt8(ascii: "k"), UInt8(ascii: "l"), UInt8(ascii: "m"), UInt8(ascii: "n"), - UInt8(ascii: "o"), UInt8(ascii: "p"), UInt8(ascii: "q"), UInt8(ascii: "r"), - UInt8(ascii: "s"), UInt8(ascii: "t"), UInt8(ascii: "u"), UInt8(ascii: "v"), - UInt8(ascii: "w"), UInt8(ascii: "x"), UInt8(ascii: "y"), UInt8(ascii: "z"), - UInt8(ascii: "0"), UInt8(ascii: "1"), UInt8(ascii: "2"), UInt8(ascii: "3"), - UInt8(ascii: "4"), UInt8(ascii: "5"), UInt8(ascii: "6"), UInt8(ascii: "7"), - UInt8(ascii: "8"), UInt8(ascii: "9"), UInt8(ascii: "-"), UInt8(ascii: "_"), - ] - - static let encodePaddingCharacter: UInt8 = 61 - - static func encode(alphabet: [UInt8], firstByte: UInt8) -> UInt8 { - let index = firstByte >> 2 - return alphabet[Int(index)] - } - - static func encode(alphabet: [UInt8], firstByte: UInt8, secondByte: UInt8?) -> UInt8 { - var index = (firstByte & 0b0000_0011) << 4 - if let secondByte = secondByte { - index += (secondByte & 0b1111_0000) >> 4 - } - return alphabet[Int(index)] - } - - static func encode(alphabet: [UInt8], secondByte: UInt8?, thirdByte: UInt8?) -> UInt8 { - guard let secondByte = secondByte else { - // No second byte means we are just emitting padding. - return Base64.encodePaddingCharacter - } - var index = (secondByte & 0b0000_1111) << 2 - if let thirdByte = thirdByte { - index += (thirdByte & 0b1100_0000) >> 6 - } - return alphabet[Int(index)] - } - - @inlinable - static func encode(alphabet: [UInt8], thirdByte: UInt8?) -> UInt8 { - guard let thirdByte = thirdByte else { - // No third byte means just padding. - return Base64.encodePaddingCharacter - } - let index = thirdByte & 0b0011_1111 - return alphabet[Int(index)] - } -} - // MARK: - Decode - extension Base64 { @@ -341,11 +211,6 @@ extension IteratorProtocol where Self.Element == UInt8 { // MARK: - Extensions - extension String { - init(base64Encoding bytes: Buffer, options: Base64.EncodingOptions = []) - where Buffer.Element == UInt8 { - self = Base64.encode(bytes: bytes, options: options) - } - func base64decoded(options: Base64.DecodingOptions = []) throws -> [UInt8] { // In Base64, 3 bytes become 4 output characters, and we pad to the nearest multiple // of four. diff --git a/Sources/AWSLambdaEvents/Utils/HTTP.swift b/Sources/AWSLambdaEvents/Utils/HTTP.swift new file mode 100644 index 00000000..9e0d8f2d --- /dev/null +++ b/Sources/AWSLambdaEvents/Utils/HTTP.swift @@ -0,0 +1,187 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2017-2020 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 +// +//===----------------------------------------------------------------------===// + +// MARK: HTTPMethod + +public typealias HTTPHeaders = [String: String] +public typealias HTTPMultiValueHeaders = [String: [String]] + +public struct HTTPMethod: RawRepresentable, Equatable { + public var rawValue: String + + public init?(rawValue: String) { + guard rawValue.isValidHTTPToken else { + return nil + } + self.rawValue = rawValue + } + + public static var GET: HTTPMethod { HTTPMethod(rawValue: "GET")! } + public static var POST: HTTPMethod { HTTPMethod(rawValue: "POST")! } + public static var PUT: HTTPMethod { HTTPMethod(rawValue: "PUT")! } + public static var PATCH: HTTPMethod { HTTPMethod(rawValue: "PATCH")! } + public static var DELETE: HTTPMethod { HTTPMethod(rawValue: "DELETE")! } + public static var OPTIONS: HTTPMethod { HTTPMethod(rawValue: "OPTIONS")! } + public static var HEAD: HTTPMethod { HTTPMethod(rawValue: "HEAD")! } + + public static func RAW(value: String) -> HTTPMethod? { HTTPMethod(rawValue: value) } +} + +extension HTTPMethod: Codable { + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + let rawMethod = try container.decode(String.self) + + guard let method = HTTPMethod(rawValue: rawMethod) else { + throw DecodingError.dataCorruptedError( + in: container, + debugDescription: #"Method "\#(rawMethod)" does not conform to allowed http method syntax defined in RFC 7230 Section 3.2.6"# + ) + } + + self = method + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(self.rawValue) + } +} + +// MARK: HTTPResponseStatus + +public struct HTTPResponseStatus { + public let code: UInt + public let reasonPhrase: String? + + public init(code: UInt, reasonPhrase: String? = nil) { + self.code = code + self.reasonPhrase = reasonPhrase + } + + public static var `continue`: HTTPResponseStatus { HTTPResponseStatus(code: 100) } + public static var switchingProtocols: HTTPResponseStatus { HTTPResponseStatus(code: 101) } + public static var processing: HTTPResponseStatus { HTTPResponseStatus(code: 102) } + public static var earlyHints: HTTPResponseStatus { HTTPResponseStatus(code: 103) } + + public static var ok: HTTPResponseStatus { HTTPResponseStatus(code: 200) } + public static var created: HTTPResponseStatus { HTTPResponseStatus(code: 201) } + public static var accepted: HTTPResponseStatus { HTTPResponseStatus(code: 202) } + public static var nonAuthoritativeInformation: HTTPResponseStatus { HTTPResponseStatus(code: 203) } + public static var noContent: HTTPResponseStatus { HTTPResponseStatus(code: 204) } + public static var resetContent: HTTPResponseStatus { HTTPResponseStatus(code: 205) } + public static var partialContent: HTTPResponseStatus { HTTPResponseStatus(code: 206) } + public static var multiStatus: HTTPResponseStatus { HTTPResponseStatus(code: 207) } + public static var alreadyReported: HTTPResponseStatus { HTTPResponseStatus(code: 208) } + public static var imUsed: HTTPResponseStatus { HTTPResponseStatus(code: 226) } + + public static var multipleChoices: HTTPResponseStatus { HTTPResponseStatus(code: 300) } + public static var movedPermanently: HTTPResponseStatus { HTTPResponseStatus(code: 301) } + public static var found: HTTPResponseStatus { HTTPResponseStatus(code: 302) } + public static var seeOther: HTTPResponseStatus { HTTPResponseStatus(code: 303) } + public static var notModified: HTTPResponseStatus { HTTPResponseStatus(code: 304) } + public static var useProxy: HTTPResponseStatus { HTTPResponseStatus(code: 305) } + public static var temporaryRedirect: HTTPResponseStatus { HTTPResponseStatus(code: 307) } + public static var permanentRedirect: HTTPResponseStatus { HTTPResponseStatus(code: 308) } + + public static var badRequest: HTTPResponseStatus { HTTPResponseStatus(code: 400) } + public static var unauthorized: HTTPResponseStatus { HTTPResponseStatus(code: 401) } + public static var paymentRequired: HTTPResponseStatus { HTTPResponseStatus(code: 402) } + public static var forbidden: HTTPResponseStatus { HTTPResponseStatus(code: 403) } + public static var notFound: HTTPResponseStatus { HTTPResponseStatus(code: 404) } + public static var methodNotAllowed: HTTPResponseStatus { HTTPResponseStatus(code: 405) } + public static var notAcceptable: HTTPResponseStatus { HTTPResponseStatus(code: 406) } + public static var proxyAuthenticationRequired: HTTPResponseStatus { HTTPResponseStatus(code: 407) } + public static var requestTimeout: HTTPResponseStatus { HTTPResponseStatus(code: 408) } + public static var conflict: HTTPResponseStatus { HTTPResponseStatus(code: 409) } + public static var gone: HTTPResponseStatus { HTTPResponseStatus(code: 410) } + public static var lengthRequired: HTTPResponseStatus { HTTPResponseStatus(code: 411) } + public static var preconditionFailed: HTTPResponseStatus { HTTPResponseStatus(code: 412) } + public static var payloadTooLarge: HTTPResponseStatus { HTTPResponseStatus(code: 413) } + public static var uriTooLong: HTTPResponseStatus { HTTPResponseStatus(code: 414) } + public static var unsupportedMediaType: HTTPResponseStatus { HTTPResponseStatus(code: 415) } + public static var rangeNotSatisfiable: HTTPResponseStatus { HTTPResponseStatus(code: 416) } + public static var expectationFailed: HTTPResponseStatus { HTTPResponseStatus(code: 417) } + public static var imATeapot: HTTPResponseStatus { HTTPResponseStatus(code: 418) } + public static var misdirectedRequest: HTTPResponseStatus { HTTPResponseStatus(code: 421) } + public static var unprocessableEntity: HTTPResponseStatus { HTTPResponseStatus(code: 422) } + public static var locked: HTTPResponseStatus { HTTPResponseStatus(code: 423) } + public static var failedDependency: HTTPResponseStatus { HTTPResponseStatus(code: 424) } + public static var upgradeRequired: HTTPResponseStatus { HTTPResponseStatus(code: 426) } + public static var preconditionRequired: HTTPResponseStatus { HTTPResponseStatus(code: 428) } + public static var tooManyRequests: HTTPResponseStatus { HTTPResponseStatus(code: 429) } + public static var requestHeaderFieldsTooLarge: HTTPResponseStatus { HTTPResponseStatus(code: 431) } + public static var unavailableForLegalReasons: HTTPResponseStatus { HTTPResponseStatus(code: 451) } + + public static var internalServerError: HTTPResponseStatus { HTTPResponseStatus(code: 500) } + public static var notImplemented: HTTPResponseStatus { HTTPResponseStatus(code: 501) } + public static var badGateway: HTTPResponseStatus { HTTPResponseStatus(code: 502) } + public static var serviceUnavailable: HTTPResponseStatus { HTTPResponseStatus(code: 503) } + public static var gatewayTimeout: HTTPResponseStatus { HTTPResponseStatus(code: 504) } + public static var httpVersionNotSupported: HTTPResponseStatus { HTTPResponseStatus(code: 505) } + public static var variantAlsoNegotiates: HTTPResponseStatus { HTTPResponseStatus(code: 506) } + public static var insufficientStorage: HTTPResponseStatus { HTTPResponseStatus(code: 507) } + public static var loopDetected: HTTPResponseStatus { HTTPResponseStatus(code: 508) } + public static var notExtended: HTTPResponseStatus { HTTPResponseStatus(code: 510) } + public static var networkAuthenticationRequired: HTTPResponseStatus { HTTPResponseStatus(code: 511) } +} + +extension HTTPResponseStatus: Equatable { + public static func == (lhs: Self, rhs: Self) -> Bool { + lhs.code == rhs.code + } +} + +extension HTTPResponseStatus: Codable { + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + self.code = try container.decode(UInt.self) + self.reasonPhrase = nil + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(self.code) + } +} + +extension String { + internal var isValidHTTPToken: Bool { + self.utf8.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 + } + } + } +} diff --git a/Tests/AWSLambdaEventsTests/ALBTests.swift b/Tests/AWSLambdaEventsTests/ALBTests.swift new file mode 100644 index 00000000..398e32ed --- /dev/null +++ b/Tests/AWSLambdaEventsTests/ALBTests.swift @@ -0,0 +1,64 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2017-2020 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 AWSLambdaEvents +import XCTest + +class ALBTests: XCTestCase { + static let exampleSingleValueHeadersPayload = """ + { + "requestContext":{ + "elb":{ + "targetGroupArn": "arn:aws:elasticloadbalancing:eu-central-1:079477498937:targetgroup/EinSternDerDeinenNamenTraegt/621febf5a44b2ce5" + } + }, + "httpMethod": "GET", + "path": "/", + "queryStringParameters": {}, + "headers":{ + "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + "accept-encoding": "gzip, deflate", + "accept-language": "en-us", + "connection": "keep-alive", + "host": "event-testl-1wa3wrvmroilb-358275751.eu-central-1.elb.amazonaws.com", + "upgrade-insecure-requests": "1", + "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.2 Safari/605.1.15", + "x-amzn-trace-id": "Root=1-5e189143-ad18a2b0a7728cd0dac45e10", + "x-forwarded-for": "90.187.8.137", + "x-forwarded-port": "80", + "x-forwarded-proto": "http" + }, + "body":"", + "isBase64Encoded":false + } + """ + + func testRequestWithSingleValueHeadersPayload() { + let data = ALBTests.exampleSingleValueHeadersPayload.data(using: .utf8)! + do { + let decoder = JSONDecoder() + + let event = try decoder.decode(ALB.TargetGroupRequest.self, from: data) + + XCTAssertEqual(event.httpMethod, .GET) + XCTAssertEqual(event.body, "") + XCTAssertEqual(event.isBase64Encoded, false) + XCTAssertEqual(event.headers?.count, 11) + XCTAssertEqual(event.path, "/") + XCTAssertEqual(event.queryStringParameters, [:]) + } catch { + XCTFail("Unexpected error: \(error)") + } + } +} diff --git a/Tests/AWSLambdaEventsTests/APIGateway+V2Tests.swift b/Tests/AWSLambdaEventsTests/APIGateway+V2Tests.swift new file mode 100644 index 00000000..c48d9dc0 --- /dev/null +++ b/Tests/AWSLambdaEventsTests/APIGateway+V2Tests.swift @@ -0,0 +1,91 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2017-2020 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 AWSLambdaEvents +import XCTest + +class APIGatewayV2Tests: XCTestCase { + static let exampleGetPayload = """ + { + "routeKey":"GET /hello", + "version":"2.0", + "rawPath":"/hello", + "stageVariables":{ + "foo":"bar" + }, + "requestContext":{ + "timeEpoch":1587750461466, + "domainPrefix":"hello", + "authorizer":{ + "jwt":{ + "scopes":[ + "hello" + ], + "claims":{ + "aud":"customers", + "iss":"https://hello.test.com/", + "iat":"1587749276", + "exp":"1587756476" + } + } + }, + "accountId":"0123456789", + "stage":"$default", + "domainName":"hello.test.com", + "apiId":"pb5dg6g3rg", + "requestId":"LgLpnibOFiAEPCA=", + "http":{ + "path":"/hello", + "userAgent":"Paw/3.1.10 (Macintosh; OS X/10.15.4) GCDHTTPRequest", + "method":"GET", + "protocol":"HTTP/1.1", + "sourceIp":"91.64.117.86" + }, + "time":"24/Apr/2020:17:47:41 +0000" + }, + "isBase64Encoded":false, + "rawQueryString":"foo=bar", + "queryStringParameters":{ + "foo":"bar" + }, + "headers":{ + "x-forwarded-proto":"https", + "x-forwarded-for":"91.64.117.86", + "x-forwarded-port":"443", + "authorization":"Bearer abc123", + "host":"hello.test.com", + "x-amzn-trace-id":"Root=1-5ea3263d-07c5d5ddfd0788bed7dad831", + "user-agent":"Paw/3.1.10 (Macintosh; OS X/10.15.4) GCDHTTPRequest", + "content-length":"0" + } + } + """ + + // MARK: - Request - + + // MARK: Decoding + + func testRequestDecodingExampleGetRequest() { + let data = APIGatewayV2Tests.exampleGetPayload.data(using: .utf8)! + var req: APIGateway.V2.Request? + XCTAssertNoThrow(req = try JSONDecoder().decode(APIGateway.V2.Request.self, from: data)) + + XCTAssertEqual(req?.rawPath, "/hello") + XCTAssertEqual(req?.context.http.method, .GET) + XCTAssertEqual(req?.queryStringParameters?.count, 1) + XCTAssertEqual(req?.rawQueryString, "foo=bar") + XCTAssertEqual(req?.headers.count, 8) + XCTAssertNil(req?.body) + } +} diff --git a/Tests/AWSLambdaEventsTests/APIGatewayTests.swift b/Tests/AWSLambdaEventsTests/APIGatewayTests.swift new file mode 100644 index 00000000..7a1a7d9e --- /dev/null +++ b/Tests/AWSLambdaEventsTests/APIGatewayTests.swift @@ -0,0 +1,77 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2017-2020 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 AWSLambdaEvents +import XCTest + +class APIGatewayTests: XCTestCase { + static let exampleGetPayload = """ + {"httpMethod": "GET", "body": null, "resource": "/test", "requestContext": {"resourceId": "123456", "apiId": "1234567890", "resourcePath": "/test", "httpMethod": "GET", "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", "accountId": "123456789012", "stage": "Prod", "identity": {"apiKey": null, "userArn": null, "cognitoAuthenticationType": null, "caller": null, "userAgent": "Custom User Agent String", "user": null, "cognitoIdentityPoolId": null, "cognitoAuthenticationProvider": null, "sourceIp": "127.0.0.1", "accountId": null}, "extendedRequestId": null, "path": "/test"}, "queryStringParameters": null, "multiValueQueryStringParameters": null, "headers": {"Host": "127.0.0.1:3000", "Connection": "keep-alive", "Cache-Control": "max-age=0", "Dnt": "1", "Upgrade-Insecure-Requests": "1", "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.87 Safari/537.36 Edg/78.0.276.24", "Sec-Fetch-User": "?1", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3", "Sec-Fetch-Site": "none", "Sec-Fetch-Mode": "navigate", "Accept-Encoding": "gzip, deflate, br", "Accept-Language": "en-US,en;q=0.9", "X-Forwarded-Proto": "http", "X-Forwarded-Port": "3000"}, "multiValueHeaders": {"Host": ["127.0.0.1:3000"], "Connection": ["keep-alive"], "Cache-Control": ["max-age=0"], "Dnt": ["1"], "Upgrade-Insecure-Requests": ["1"], "User-Agent": ["Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.87 Safari/537.36 Edg/78.0.276.24"], "Sec-Fetch-User": ["?1"], "Accept": ["text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3"], "Sec-Fetch-Site": ["none"], "Sec-Fetch-Mode": ["navigate"], "Accept-Encoding": ["gzip, deflate, br"], "Accept-Language": ["en-US,en;q=0.9"], "X-Forwarded-Proto": ["http"], "X-Forwarded-Port": ["3000"]}, "pathParameters": null, "stageVariables": null, "path": "/test", "isBase64Encoded": false} + """ + + static let todoPostPayload = """ + {"httpMethod": "POST", "body": "{\\"title\\":\\"a todo\\"}", "resource": "/todos", "requestContext": {"resourceId": "123456", "apiId": "1234567890", "resourcePath": "/todos", "httpMethod": "POST", "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", "accountId": "123456789012", "stage": "test", "identity": {"apiKey": null, "userArn": null, "cognitoAuthenticationType": null, "caller": null, "userAgent": "Custom User Agent String", "user": null, "cognitoIdentityPoolId": null, "cognitoAuthenticationProvider": null, "sourceIp": "127.0.0.1", "accountId": null}, "extendedRequestId": null, "path": "/todos"}, "queryStringParameters": null, "multiValueQueryStringParameters": null, "headers": {"Host": "127.0.0.1:3000", "Connection": "keep-alive", "Content-Length": "18", "Pragma": "no-cache", "Cache-Control": "no-cache", "Accept": "text/plain, */*; q=0.01", "Origin": "http://todobackend.com", "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.36 Safari/537.36 Edg/79.0.309.25", "Dnt": "1", "Content-Type": "application/json", "Sec-Fetch-Site": "cross-site", "Sec-Fetch-Mode": "cors", "Referer": "http://todobackend.com/specs/index.html?http://127.0.0.1:3000/todos", "Accept-Encoding": "gzip, deflate, br", "Accept-Language": "en-US,en;q=0.9", "X-Forwarded-Proto": "http", "X-Forwarded-Port": "3000"}, "multiValueHeaders": {"Host": ["127.0.0.1:3000"], "Connection": ["keep-alive"], "Content-Length": ["18"], "Pragma": ["no-cache"], "Cache-Control": ["no-cache"], "Accept": ["text/plain, */*; q=0.01"], "Origin": ["http://todobackend.com"], "User-Agent": ["Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.36 Safari/537.36 Edg/79.0.309.25"], "Dnt": ["1"], "Content-Type": ["application/json"], "Sec-Fetch-Site": ["cross-site"], "Sec-Fetch-Mode": ["cors"], "Referer": ["http://todobackend.com/specs/index.html?http://127.0.0.1:3000/todos"], "Accept-Encoding": ["gzip, deflate, br"], "Accept-Language": ["en-US,en;q=0.9"], "X-Forwarded-Proto": ["http"], "X-Forwarded-Port": ["3000"]}, "pathParameters": null, "stageVariables": null, "path": "/todos", "isBase64Encoded": false} + """ + + // MARK: - Request - + + // MARK: Decoding + + func testRequestDecodingExampleGetRequest() { + let data = APIGatewayTests.exampleGetPayload.data(using: .utf8)! + var req: APIGateway.Request? + XCTAssertNoThrow(req = try JSONDecoder().decode(APIGateway.Request.self, from: data)) + + XCTAssertEqual(req?.path, "/test") + XCTAssertEqual(req?.httpMethod, .GET) + } + + func testRequestDecodingTodoPostRequest() { + let data = APIGatewayTests.todoPostPayload.data(using: .utf8)! + var req: APIGateway.Request? + XCTAssertNoThrow(req = try JSONDecoder().decode(APIGateway.Request.self, from: data)) + + XCTAssertEqual(req?.path, "/todos") + XCTAssertEqual(req?.httpMethod, .POST) + } + + // MARK: - Response - + + // MARK: Encoding + + struct JSONResponse: Codable { + let statusCode: UInt + let headers: [String: String]? + let body: String? + let isBase64Encoded: Bool? + } + + func testResponseEncoding() { + let resp = APIGateway.Response( + statusCode: .ok, + headers: ["Server": "Test"], + body: "abc123" + ) + + var data: Data? + XCTAssertNoThrow(data = try JSONEncoder().encode(resp)) + var json: JSONResponse? + XCTAssertNoThrow(json = try JSONDecoder().decode(JSONResponse.self, from: XCTUnwrap(data))) + + XCTAssertEqual(json?.statusCode, resp.statusCode.code) + XCTAssertEqual(json?.body, resp.body) + XCTAssertEqual(json?.isBase64Encoded, resp.isBase64Encoded) + XCTAssertEqual(json?.headers?["Server"], "Test") + } +} diff --git a/Tests/AWSLambdaEventsTests/Utils/Base64Tests.swift b/Tests/AWSLambdaEventsTests/Utils/Base64Tests.swift index 8e3c8695..59e300c1 100644 --- a/Tests/AWSLambdaEventsTests/Utils/Base64Tests.swift +++ b/Tests/AWSLambdaEventsTests/Utils/Base64Tests.swift @@ -16,32 +16,6 @@ import XCTest class Base64Tests: XCTestCase { - // MARK: - Encoding - - - func testEncodeEmptyData() throws { - let data = [UInt8]() - let encodedData = String(base64Encoding: data) - XCTAssertEqual(encodedData.count, 0) - } - - func testBase64EncodingArrayOfNulls() throws { - let data = Array(repeating: UInt8(0), count: 10) - let encodedData = String(base64Encoding: data) - XCTAssertEqual(encodedData, "AAAAAAAAAAAAAA==") - } - - func testBase64EncodingAllTheBytesSequentially() throws { - let data = Array(UInt8(0) ... UInt8(255)) - let encodedData = String(base64Encoding: data) - XCTAssertEqual(encodedData, "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6e3x9fn+AgYKDhIWGh4iJiouMjY6PkJGSk5SVlpeYmZqbnJ2en6ChoqOkpaanqKmqq6ytrq+wsbKztLW2t7i5uru8vb6/wMHCw8TFxsfIycrLzM3Oz9DR0tPU1dbX2Nna29zd3t/g4eLj5OXm5+jp6uvs7e7v8PHy8/T19vf4+fr7/P3+/w==") - } - - func testBase64UrlEncodingAllTheBytesSequentially() throws { - let data = Array(UInt8(0) ... UInt8(255)) - let encodedData = String(base64Encoding: data, options: .base64UrlAlphabet) - XCTAssertEqual(encodedData, "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0-P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6e3x9fn-AgYKDhIWGh4iJiouMjY6PkJGSk5SVlpeYmZqbnJ2en6ChoqOkpaanqKmqq6ytrq-wsbKztLW2t7i5uru8vb6_wMHCw8TFxsfIycrLzM3Oz9DR0tPU1dbX2Nna29zd3t_g4eLj5OXm5-jp6uvs7e7v8PHy8_T19vf4-fr7_P3-_w==") - } - // MARK: - Decoding - func testDecodeEmptyString() throws {