From 1a2991214992e5148078231641d853893e3d2370 Mon Sep 17 00:00:00 2001 From: Mike Lewis Date: Thu, 14 Jul 2022 04:20:01 -0600 Subject: [PATCH 1/5] Add request and response events for Lambda Function URL invocations [Lambdas can be called with an auto-generated URL](https://docs.aws.amazon.com/lambda/latest/dg/urls-invocation.html), which has a specific format similar but not identical to API Gateway. --- Sources/AWSLambdaEvents/FunctionURL.swift | 112 ++++++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 Sources/AWSLambdaEvents/FunctionURL.swift diff --git a/Sources/AWSLambdaEvents/FunctionURL.swift b/Sources/AWSLambdaEvents/FunctionURL.swift new file mode 100644 index 0000000..18319f3 --- /dev/null +++ b/Sources/AWSLambdaEvents/FunctionURL.swift @@ -0,0 +1,112 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2017-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 class Foundation.JSONEncoder + +// https://docs.aws.amazon.com/lambda/latest/dg/urls-invocation.html + +/// FunctionURLRequest contains data coming from a bare Lambda Function URL +public struct FunctionURLRequest: Codable { + public struct Context: Codable { + public struct Authorizer: Codable { + public struct IAMAuthorizer: Codable { + public let accessKey: String + + public let accountId: String + public let callerId: String + public let cognitoIdentity: String? + + public let principalOrgId: String? + + public let userArn: String + public let userId: String + } + + public let iam: IAMAuthorizer? + } + + public struct HTTP: Codable { + public let method: String + public let path: String + public let `protocol`: String + public let sourceIp: String + public let userAgent: String + } + + public let accountId: String + public let apiId: String + public let authentication: String? + public let authorizer: Authorizer + public let domainName: String + public let domainPrefix: String + public let http: HTTP + + public let requestId: String + public let routeKey: String + public let stage: String + + public let time: String + public let timeEpoch: Int + } + + 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 requestContext: Context + + public let body: String? + public let pathParameters: [String: String]? + public let isBase64Encoded: Bool + + public let stageVariables: [String: String]? +} + +// MARK: - Response - + +public struct FunctionURLResponse: Codable { + public var statusCode: HTTPResponseStatus + public var headers: HTTPHeaders? + public var body: String? + public let cookies: [String]? + public var isBase64Encoded: Bool? + + public init( + statusCode: HTTPResponseStatus, + headers: HTTPHeaders? = nil, + body: String? = nil, + cookies: [String]? = nil, + isBase64Encoded: Bool? = nil + ) { + self.statusCode = statusCode + self.headers = headers + self.body = body + self.cookies = cookies + self.isBase64Encoded = isBase64Encoded + } +} + +#if swift(>=5.6) +extension FunctionURLRequest: Sendable {} +extension FunctionURLRequest.Context: Sendable {} +extension FunctionURLRequest.Context.Authorizer: Sendable {} +extension FunctionURLRequest.Context.Authorizer.IAMAuthorizer: Sendable {} +extension FunctionURLRequest.Context.HTTP: Sendable {} +extension FunctionURLResponse: Sendable {} +#endif From d0781407d1c40d07b24c943f88dfd6b8da8ef42d Mon Sep 17 00:00:00 2001 From: Mike Lewis Date: Thu, 14 Jul 2022 04:31:41 -0600 Subject: [PATCH 2/5] Make `cookies` optional Based on real-world testing. --- Sources/AWSLambdaEvents/FunctionURL.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/AWSLambdaEvents/FunctionURL.swift b/Sources/AWSLambdaEvents/FunctionURL.swift index 18319f3..d9be984 100644 --- a/Sources/AWSLambdaEvents/FunctionURL.swift +++ b/Sources/AWSLambdaEvents/FunctionURL.swift @@ -65,7 +65,7 @@ public struct FunctionURLRequest: Codable { public let routeKey: String public let rawPath: String public let rawQueryString: String - public let cookies: [String] + public let cookies: [String]? public let headers: HTTPHeaders public let queryStringParameters: [String: String]? From a5c541a12343c06bf87af00b436e95e209780471 Mon Sep 17 00:00:00 2001 From: Mike Lewis Date: Thu, 14 Jul 2022 04:31:41 -0600 Subject: [PATCH 3/5] Make `authorizer` optional Based on real-world testing. --- Sources/AWSLambdaEvents/FunctionURL.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/AWSLambdaEvents/FunctionURL.swift b/Sources/AWSLambdaEvents/FunctionURL.swift index d9be984..785d61d 100644 --- a/Sources/AWSLambdaEvents/FunctionURL.swift +++ b/Sources/AWSLambdaEvents/FunctionURL.swift @@ -47,7 +47,7 @@ public struct FunctionURLRequest: Codable { public let accountId: String public let apiId: String public let authentication: String? - public let authorizer: Authorizer + public let authorizer: Authorizer? public let domainName: String public let domainPrefix: String public let http: HTTP From 3bd01e245604916e49093ceaf549ae2cf4c57160 Mon Sep 17 00:00:00 2001 From: Mike Lewis Date: Sun, 30 Oct 2022 23:00:06 -0600 Subject: [PATCH 4/5] Use higher-level HTTP method type --- Sources/AWSLambdaEvents/FunctionURL.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/AWSLambdaEvents/FunctionURL.swift b/Sources/AWSLambdaEvents/FunctionURL.swift index 785d61d..c575a37 100644 --- a/Sources/AWSLambdaEvents/FunctionURL.swift +++ b/Sources/AWSLambdaEvents/FunctionURL.swift @@ -37,7 +37,7 @@ public struct FunctionURLRequest: Codable { } public struct HTTP: Codable { - public let method: String + public let method: HTTPMethod public let path: String public let `protocol`: String public let sourceIp: String From 77a6ef3c2ce396ddd2040799c9f02fed3e6f9783 Mon Sep 17 00:00:00 2001 From: Mike Lewis Date: Sun, 30 Oct 2022 23:00:18 -0600 Subject: [PATCH 5/5] Add request decoding test cases --- .../FunctionURLTests.swift | 152 ++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100644 Tests/AWSLambdaEventsTests/FunctionURLTests.swift diff --git a/Tests/AWSLambdaEventsTests/FunctionURLTests.swift b/Tests/AWSLambdaEventsTests/FunctionURLTests.swift new file mode 100644 index 0000000..7663da3 --- /dev/null +++ b/Tests/AWSLambdaEventsTests/FunctionURLTests.swift @@ -0,0 +1,152 @@ +//===----------------------------------------------------------------------===// +// +// 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 FunctionURLTests: XCTestCase { + /// Example event body pulled from [AWS documentation](https://docs.aws.amazon.com/lambda/latest/dg/urls-invocation.html#urls-request-payload). + static let documentationExample = """ + { + "version": "2.0", + "routeKey": "$default", + "rawPath": "/my/path", + "rawQueryString": "parameter1=value1¶meter1=value2¶meter2=value", + "cookies": [ + "cookie1", + "cookie2" + ], + "headers": { + "header1": "value1", + "header2": "value1,value2" + }, + "queryStringParameters": { + "parameter1": "value1,value2", + "parameter2": "value" + }, + "requestContext": { + "accountId": "123456789012", + "apiId": "", + "authentication": null, + "authorizer": { + "iam": { + "accessKey": "AKIA...", + "accountId": "111122223333", + "callerId": "AIDA...", + "cognitoIdentity": null, + "principalOrgId": null, + "userArn": "arn:aws:iam::111122223333:user/example-user", + "userId": "AIDA..." + } + }, + "domainName": ".lambda-url.us-west-2.on.aws", + "domainPrefix": "", + "http": { + "method": "POST", + "path": "/my/path", + "protocol": "HTTP/1.1", + "sourceIp": "123.123.123.123", + "userAgent": "agent" + }, + "requestId": "id", + "routeKey": "$default", + "stage": "$default", + "time": "12/Mar/2020:19:03:58 +0000", + "timeEpoch": 1583348638390 + }, + "body": "Hello from client!", + "pathParameters": null, + "isBase64Encoded": false, + "stageVariables": null + } + """ + + /// Example event body pulled from an an actual Lambda invocation. + static let realWorldExample = """ + { + "headers": { + "x-amzn-tls-cipher-suite": "ECDHE-RSA-AES128-GCM-SHA256", + "x-amzn-tls-version": "TLSv1.2", + "x-amzn-trace-id": "Root=0-12345678-9abcdef0123456789abcdef0", + "cookie": "test", + "x-forwarded-proto": "https", + "host": "0123456789abcdefghijklmnopqrstuv.lambda-url.us-west-2.on.aws", + "x-forwarded-port": "443", + "x-forwarded-for": "1.2.3.4", + "accept": "*/*", + "user-agent": "curl" + }, + "isBase64Encoded": false, + "rawPath": "/", + "routeKey": "$default", + "requestContext": { + "accountId": "anonymous", + "timeEpoch": 1667192002044, + "routeKey": "$default", + "stage": "$default", + "domainPrefix": "0123456789abcdefghijklmnopqrstuv", + "requestId": "01234567-89ab-cdef-0123-456789abcdef", + "domainName": "0123456789abcdefghijklmnopqrstuv.lambda-url.us-west-2.on.aws", + "http": { + "path": "/", + "protocol": "HTTP/1.1", + "method": "GET", + "sourceIp": "1.2.3.4", + "userAgent": "curl" + }, + "time": "31/Oct/2022:04:53:22 +0000", + "apiId": "0123456789abcdefghijklmnopqrstuv" + }, + "queryStringParameters": { + "test": "2" + }, + "version": "2.0", + "rawQueryString": "test=2", + "cookies": [ + "test" + ] + } + """ + + // MARK: - Request - + + // MARK: Decoding + + func testRequestDecodingDocumentationExampleRequest() { + let data = Self.documentationExample.data(using: .utf8)! + var req: FunctionURLRequest? + XCTAssertNoThrow(req = try JSONDecoder().decode(FunctionURLRequest.self, from: data)) + + XCTAssertEqual(req?.rawPath, "/my/path") + XCTAssertEqual(req?.requestContext.http.method, .POST) + XCTAssertEqual(req?.queryStringParameters?.count, 2) + XCTAssertEqual(req?.rawQueryString, "parameter1=value1¶meter1=value2¶meter2=value") + XCTAssertEqual(req?.headers.count, 2) + XCTAssertEqual(req?.body, "Hello from client!") + } + + func testRequestDecodingRealWorldExampleRequest() { + let data = Self.realWorldExample.data(using: .utf8)! + var req: FunctionURLRequest? + XCTAssertNoThrow(req = try JSONDecoder().decode(FunctionURLRequest.self, from: data)) + + XCTAssertEqual(req?.rawPath, "/") + XCTAssertEqual(req?.requestContext.http.method, .GET) + XCTAssertEqual(req?.queryStringParameters?.count, 1) + XCTAssertEqual(req?.rawQueryString, "test=2") + XCTAssertEqual(req?.headers.count, 10) + XCTAssertEqual(req?.cookies, ["test"]) + XCTAssertNil(req?.body) + } +}