diff --git a/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata b/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000..919434a
--- /dev/null
+++ b/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/Package.swift b/Package.swift
index 3befa1e..e3d8253 100644
--- a/Package.swift
+++ b/Package.swift
@@ -16,7 +16,7 @@ let package = Package(
],
dependencies: [
.package(url: "https://github.com/apple/swift-nio.git", .upToNextMajor(from: "2.13.0")),
- .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", .upToNextMajor(from: "0.3.0")),
+ .package(url: "https://github.com/skelpo/swift-aws-lambda-runtime.git", .upToNextMajor(from: "0.4.0")),
.package(url: "https://github.com/vapor/vapor.git", .upToNextMajor(from: "4.0.0")),
.package(url: "https://github.com/swift-extras/swift-extras-base64", .upToNextMajor(from: "0.4.0")),
],
diff --git a/Sources/VaporAWSLambdaRuntime/ALB.swift b/Sources/VaporAWSLambdaRuntime/ALB.swift
new file mode 100644
index 0000000..5035b18
--- /dev/null
+++ b/Sources/VaporAWSLambdaRuntime/ALB.swift
@@ -0,0 +1,158 @@
+//
+// File.swift
+//
+//
+// Created by Ralph Küpper on 1/5/21.
+//
+
+import AWSLambdaEvents
+import AWSLambdaRuntimeCore
+import ExtrasBase64
+import NIO
+import NIOHTTP1
+import Vapor
+
+// MARK: - Handler -
+
+struct ALBHandler: EventLoopLambdaHandler {
+
+ typealias In = ALB.TargetGroupRequest
+ typealias Out = ALB.TargetGroupResponse
+
+ private let application: Application
+ private let responder: Responder
+
+ init(application: Application, responder: Responder) {
+ self.application = application
+ self.responder = responder
+ }
+
+ public func handle(context: Lambda.Context, event: ALB.TargetGroupRequest)
+ -> EventLoopFuture
+ {
+ let vaporRequest: Vapor.Request
+ do {
+ vaporRequest = try Vapor.Request(req: event, in: context, for: self.application)
+ } catch {
+ return context.eventLoop.makeFailedFuture(error)
+ }
+
+ return self.responder.respond(to: vaporRequest).flatMap { ALB.TargetGroupResponse.from(response: $0, in: context) }
+ }
+}
+
+// MARK: - Request -
+
+extension Vapor.Request {
+ private static let bufferAllocator = ByteBufferAllocator()
+
+ convenience init(req: ALB.TargetGroupRequest, in ctx: Lambda.Context, for application: Application) throws {
+ var buffer: NIO.ByteBuffer?
+ switch (req.body, req.isBase64Encoded) {
+ case (let .some(string), true):
+ let bytes = try string.base64decoded()
+ buffer = Vapor.Request.bufferAllocator.buffer(capacity: bytes.count)
+ buffer!.writeBytes(bytes)
+
+ case (let .some(string), false):
+ buffer = Vapor.Request.bufferAllocator.buffer(capacity: string.utf8.count)
+ buffer!.writeString(string)
+
+ case (.none, _):
+ break
+ }
+
+ var nioHeaders = NIOHTTP1.HTTPHeaders()
+ req.headers?.forEach { key, value in
+ nioHeaders.add(name: key, value: value)
+ }
+
+ /*if let cookies = req., cookies.count > 0 {
+ nioHeaders.add(name: "Cookie", value: cookies.joined(separator: "; "))
+ }*/
+
+ var url: String = req.path
+ if req.queryStringParameters.count > 0 {
+ url += "?"
+ for key in req.queryStringParameters.keys {
+ // It leaves an ampersand (&) at the end, but who cares?
+ url += key + "=" + (req.queryStringParameters[key] ?? "") + "&"
+ }
+ }
+
+ ctx.logger.debug("The constructed URL is: \(url)")
+
+ self.init(
+ application: application,
+ method: NIOHTTP1.HTTPMethod(rawValue: req.httpMethod.rawValue),
+ url: Vapor.URI(path: url),
+ version: HTTPVersion(major: 1, minor: 1),
+ headers: nioHeaders,
+ collectedBody: buffer,
+ remoteAddress: nil,
+ logger: ctx.logger,
+ on: ctx.eventLoop
+ )
+
+ storage[ALB.TargetGroupRequest] = req
+ }
+}
+
+extension ALB.TargetGroupRequest: Vapor.StorageKey {
+ public typealias Value = ALB.TargetGroupRequest
+}
+
+// MARK: - Response -
+
+extension ALB.TargetGroupResponse {
+ static func from(response: Vapor.Response, in context: Lambda.Context) -> EventLoopFuture {
+ // Create the headers
+ var headers = [String: String]()
+ response.headers.forEach { name, value in
+ if let current = headers[name] {
+ headers[name] = "\(current),\(value)"
+ } else {
+ headers[name] = value
+ }
+ }
+
+ // Can we access the body right away?
+ if let string = response.body.string {
+ return context.eventLoop.makeSucceededFuture(.init(
+ statusCode: AWSLambdaEvents.HTTPResponseStatus(code: response.status.code),
+ headers: headers,
+ body: string,
+ isBase64Encoded: false
+ ))
+ } else if let bytes = response.body.data {
+ return context.eventLoop.makeSucceededFuture(.init(
+ statusCode: AWSLambdaEvents.HTTPResponseStatus(code: response.status.code),
+ headers: headers,
+ body: String(base64Encoding: bytes),
+ isBase64Encoded: true
+ ))
+ } else {
+ // See if it is a stream and try to gather the data
+ return response.body.collect(on: context.eventLoop).map { (buffer) -> ALB.TargetGroupResponse in
+ // Was there any content
+ guard
+ var buffer = buffer,
+ let bytes = buffer.readBytes(length: buffer.readableBytes)
+ else {
+ return ALB.TargetGroupResponse(
+ statusCode: AWSLambdaEvents.HTTPResponseStatus(code: response.status.code),
+ headers: headers
+ )
+ }
+
+ // Done
+ return ALB.TargetGroupResponse(
+ statusCode: AWSLambdaEvents.HTTPResponseStatus(code: response.status.code),
+ headers: headers,
+ body: String(base64Encoding: bytes),
+ isBase64Encoded: true
+ )
+ }
+ }
+ }
+}
diff --git a/Sources/VaporAWSLambdaRuntime/LambdaServer.swift b/Sources/VaporAWSLambdaRuntime/LambdaServer.swift
index de1fbf1..71c5bb8 100644
--- a/Sources/VaporAWSLambdaRuntime/LambdaServer.swift
+++ b/Sources/VaporAWSLambdaRuntime/LambdaServer.swift
@@ -66,8 +66,8 @@ public extension Application.Lambda {
}
}
- struct ConfigurationKey: StorageKey {
- typealias Value = LambdaServer.Configuration
+ public struct ConfigurationKey: StorageKey {
+ public typealias Value = LambdaServer.Configuration
}
}
}
@@ -79,13 +79,14 @@ public class LambdaServer: Server {
public enum RequestSource {
case apiGateway
case apiGatewayV2
-// case applicationLoadBalancer // not in this release
+ case applicationLoadBalancer
+ case sqs
}
var requestSource: RequestSource
var logger: Logger
- init(apiService: RequestSource = .apiGatewayV2, logger: Logger) {
+ public init(apiService: RequestSource = .apiGatewayV2, logger: Logger) {
self.requestSource = apiService
self.logger = logger
}
@@ -115,6 +116,10 @@ public class LambdaServer: Server {
handler = APIGatewayHandler(application: application, responder: responder)
case .apiGatewayV2:
handler = APIGatewayV2Handler(application: application, responder: responder)
+ case .applicationLoadBalancer:
+ handler = ALBHandler(application: application, responder: responder)
+ case .sqs:
+ handler = SQSHandler(application: application, responder: responder)
}
self.lambdaLifecycle = Lambda.Lifecycle(
diff --git a/Sources/VaporAWSLambdaRuntime/SQS.swift b/Sources/VaporAWSLambdaRuntime/SQS.swift
new file mode 100644
index 0000000..167cf88
--- /dev/null
+++ b/Sources/VaporAWSLambdaRuntime/SQS.swift
@@ -0,0 +1,152 @@
+//
+// File.swift
+//
+//
+// Created by Ralph Küpper on 10/23/21.
+//
+
+
+import AWSLambdaEvents
+import AWSLambdaRuntimeCore
+import ExtrasBase64
+import NIO
+import NIOHTTP1
+import Vapor
+
+// MARK: - Handler -
+
+struct SQSHandler: EventLoopLambdaHandler {
+
+ typealias In = SQS.Event
+ typealias Out = SQSResponse
+
+ private let application: Application
+ private let responder: Responder
+
+ init(application: Application, responder: Responder) {
+ self.application = application
+ self.responder = responder
+ }
+
+ public func handle(context: Lambda.Context, event: SQS.Event)
+ -> EventLoopFuture
+ {
+ let vaporRequest: Vapor.Request
+ do {
+ vaporRequest = try Vapor.Request(req: event, in: context, for: self.application)
+ } catch {
+ return context.eventLoop.makeFailedFuture(error)
+ }
+
+ return self.responder.respond(to: vaporRequest).flatMap { SQSResponse.from(response: $0, in: context) }
+ }
+}
+
+// MARK: - Request -
+
+extension Vapor.Request {
+ private static let bufferAllocator = ByteBufferAllocator()
+
+ convenience init(req: SQS.Event, in ctx: Lambda.Context, for application: Application) throws {
+ let event = req.records.first!
+ print("incoming events: ", req.records.count)
+ /*var buffer: NIO.ByteBuffer?
+ switch (req.body, req.isBase64Encoded) {
+ case (let .some(string), true):
+ let bytes = try string.base64decoded()
+ buffer = Vapor.Request.bufferAllocator.buffer(capacity: bytes.count)
+ buffer!.writeBytes(bytes)
+
+ case (let .some(string), false):
+ buffer = Vapor.Request.bufferAllocator.buffer(capacity: string.utf8.count)
+ buffer!.writeString(string)
+
+ case (.none, _):
+ break
+ }
+
+ var nioHeaders = NIOHTTP1.HTTPHeaders()
+ req.headers?.forEach { key, value in
+ nioHeaders.add(name: key, value: value)
+ }
+
+ /*if let cookies = req., cookies.count > 0 {
+ nioHeaders.add(name: "Cookie", value: cookies.joined(separator: "; "))
+ }*/
+
+ var url: String = req.path
+ if req.queryStringParameters.count > 0 {
+ url += "?"
+ for key in req.queryStringParameters.keys {
+ // It leaves an ampersand (&) at the end, but who cares?
+ url += key + "=" + (req.queryStringParameters[key] ?? "") + "&"
+ }
+ }*/
+ var buffer: NIO.ByteBuffer?
+ buffer = Vapor.Request.bufferAllocator.buffer(capacity: event.body.utf8.count)
+ buffer!.writeString(event.body)
+
+ let url = "/sqs"
+
+ ctx.logger.debug("The constructed URL is: \(url)")
+
+ self.init(
+ application: application,
+ method: NIOHTTP1.HTTPMethod.POST,
+ url: Vapor.URI(path: url),
+ version: HTTPVersion(major: 1, minor: 1),
+ headers: [:],
+ collectedBody: buffer,
+ remoteAddress: nil,
+ logger: ctx.logger,
+ on: ctx.eventLoop
+ )
+
+ storage[SQS.Event] = req
+ }
+}
+
+extension SQS.Event: Vapor.StorageKey {
+ public typealias Value = SQS.Event
+}
+
+// MARK: - Response -
+
+struct SQSResponse: Codable {
+ public var statusCode: HTTPResponseStatus
+ public var statusDescription: String?
+ public var headers: HTTPHeaders?
+ public var multiValueHeaders: HTTPMultiValueHeaders?
+ public var body: String
+ public var 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
+ }
+
+ static func from(response: Vapor.Response, in context: Lambda.Context) -> EventLoopFuture {
+ // Create the headers
+ var headers: HTTPHeaders = [:]
+
+ // Can we access the body right away?
+ let string = response.body.string ?? ""
+ return context.eventLoop.makeSucceededFuture(.init(
+ statusCode: HTTPResponseStatus.ok,
+ headers: headers,
+ body: string,
+ isBase64Encoded: false
+ ))
+ }
+}
diff --git a/Tests/VaporAWSLambdaRuntimeTests/ALB.swift b/Tests/VaporAWSLambdaRuntimeTests/ALB.swift
new file mode 100644
index 0000000..403984d
--- /dev/null
+++ b/Tests/VaporAWSLambdaRuntimeTests/ALB.swift
@@ -0,0 +1,80 @@
+import AWSLambdaEvents
+@testable import AWSLambdaRuntimeCore
+import Logging
+import NIO
+import Vapor
+@testable import VaporAWSLambdaRuntime
+import XCTest
+
+final class ALBTests: XCTestCase {
+ func testALBRequest() throws {
+ let requestdata = """
+ {
+ "requestContext": {
+ "elb": {
+ "targetGroupArn": "arn:aws:elasticloadbalancing:us-east-2:123456789012:targetgroup/lambda-279XGJDqGZ5rsrHC2Fjr/49e9d65c45c6791a"
+ }
+ },
+ "httpMethod": "GET",
+ "path": "/lambda",
+ "queryStringParameters": {
+ "query": "1234ABCD"
+ },
+ "headers": {
+ "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8",
+ "accept-encoding": "gzip",
+ "accept-language": "en-US,en;q=0.9",
+ "connection": "keep-alive",
+ "host": "lambda-alb-123578498.us-east-2.elb.amazonaws.com",
+ "upgrade-insecure-requests": "1",
+ "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36",
+ "x-amzn-trace-id": "Root=1-5c536348-3d683b8b04734faae651f476",
+ "x-forwarded-for": "72.12.164.125",
+ "x-forwarded-port": "80",
+ "x-forwarded-proto": "http",
+ "x-imforwards": "20"
+ },
+ "body": "",
+ "isBase64Encoded": false
+ }
+ """
+ let decoder = JSONDecoder()
+ let request = try decoder.decode(ALB.TargetGroupRequest.self, from: requestdata.data(using: .utf8)!)
+ print("F: ", request)
+ }
+
+ func testCreateALBResponse() {
+ let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
+ defer { XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) }
+ let eventLoop = eventLoopGroup.next()
+ let allocator = ByteBufferAllocator()
+ let logger = Logger(label: "test")
+
+ let body = #"{"hello": "world"}"#
+ let vaporResponse = Vapor.Response(
+ status: .ok,
+ headers: HTTPHeaders([
+ ("Content-Type", "application/json"),
+ ]),
+ body: .init(string: body)
+ )
+
+ let context = Lambda.Context(
+ requestID: "abc123",
+ traceID: AmazonHeaders.generateXRayTraceID(),
+ invokedFunctionARN: "function-arn",
+ deadline: .now() + .seconds(3),
+ logger: logger,
+ eventLoop: eventLoop,
+ allocator: allocator
+ )
+
+ var response: ALB.TargetGroupResponse?
+ XCTAssertNoThrow(response = try ALB.TargetGroupResponse.from(response: vaporResponse, in: context).wait())
+
+ XCTAssertEqual(response?.body, body)
+ XCTAssertEqual(response?.headers?.count, 2)
+ XCTAssertEqual(response?.headers?["Content-Type"], "application/json")
+ XCTAssertEqual(response?.headers?["content-length"], String(body.count))
+ }
+}
diff --git a/Tests/VaporAWSLambdaRuntimeTests/SQSTests.swift b/Tests/VaporAWSLambdaRuntimeTests/SQSTests.swift
new file mode 100644
index 0000000..6951d25
--- /dev/null
+++ b/Tests/VaporAWSLambdaRuntimeTests/SQSTests.swift
@@ -0,0 +1,79 @@
+//
+// File.swift
+//
+//
+// Created by Ralph Küpper on 10/23/21.
+//
+
+import AWSLambdaEvents
+@testable import AWSLambdaRuntimeCore
+import Logging
+import NIO
+import Vapor
+@testable import VaporAWSLambdaRuntime
+import XCTest
+
+final class SQSTests: XCTestCase {
+ func testSQSRequest() throws {
+ let requestdata = """
+ {
+ "Records": [
+ {
+ "messageId": "19dd0b57-b21e-4ac1-bd88-01bbb068cb78",
+ "receiptHandle": "MessageReceiptHandle",
+ "body": "Hello from SQS!",
+ "attributes": {
+ "ApproximateReceiveCount": "1",
+ "SentTimestamp": "1523232000000",
+ "SenderId": "123456789012",
+ "ApproximateFirstReceiveTimestamp": "1523232000001"
+ },
+ "messageAttributes": {},
+ "md5OfBody": "{{{md5_of_body}}}",
+ "eventSource": "aws:sqs",
+ "eventSourceARN": "arn:aws:sqs:us-east-1:123456789012:MyQueue",
+ "awsRegion": "us-east-1"
+ }
+ ]
+ }
+ """
+ let decoder = JSONDecoder()
+ let request = try decoder.decode(SQS.Event.self, from: requestdata.data(using: .utf8)!)
+ print("F: ", request)
+ }
+
+ func testCreateALBResponse() {
+ let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
+ defer { XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) }
+ let eventLoop = eventLoopGroup.next()
+ let allocator = ByteBufferAllocator()
+ let logger = Logger(label: "test")
+
+ let body = #"{"hello": "world"}"#
+ let vaporResponse = Vapor.Response(
+ status: .ok,
+ headers: HTTPHeaders([
+ ("Content-Type", "application/json"),
+ ]),
+ body: .init(string: body)
+ )
+
+ let context = Lambda.Context(
+ requestID: "abc123",
+ traceID: AmazonHeaders.generateXRayTraceID(),
+ invokedFunctionARN: "function-arn",
+ deadline: .now() + .seconds(3),
+ logger: logger,
+ eventLoop: eventLoop,
+ allocator: allocator
+ )
+
+ var response: ALB.TargetGroupResponse?
+ XCTAssertNoThrow(response = try ALB.TargetGroupResponse.from(response: vaporResponse, in: context).wait())
+
+ XCTAssertEqual(response?.body, body)
+ XCTAssertEqual(response?.headers?.count, 2)
+ XCTAssertEqual(response?.headers?["Content-Type"], "application/json")
+ XCTAssertEqual(response?.headers?["content-length"], String(body.count))
+ }
+}
diff --git a/examples/Hello/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata b/examples/Hello/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000..919434a
--- /dev/null
+++ b/examples/Hello/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/examples/Hello/.swiftpm/xcode/xcshareddata/xcschemes/Hello.xcscheme b/examples/Hello/.swiftpm/xcode/xcshareddata/xcschemes/Hello.xcscheme
new file mode 100644
index 0000000..1e67dfd
--- /dev/null
+++ b/examples/Hello/.swiftpm/xcode/xcshareddata/xcschemes/Hello.xcscheme
@@ -0,0 +1,90 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/Hello/Package.resolved b/examples/Hello/Package.resolved
index f69666b..cfc54ae 100644
--- a/examples/Hello/Package.resolved
+++ b/examples/Hello/Package.resolved
@@ -39,11 +39,11 @@
},
{
"package": "swift-aws-lambda-runtime",
- "repositoryURL": "https://github.com/swift-server/swift-aws-lambda-runtime.git",
+ "repositoryURL": "https://github.com/skelpo/swift-aws-lambda-runtime.git",
"state": {
"branch": null,
- "revision": "2bac89639fffd7b1197ab597473a4d10c459a230",
- "version": "0.2.0"
+ "revision": "eb8924dde415c8d9daca242556c23b84b2ef3a5d",
+ "version": "0.4.0"
}
},
{
@@ -56,21 +56,21 @@
}
},
{
- "package": "swift-base64-kit",
- "repositoryURL": "https://github.com/fabianfett/swift-base64-kit",
+ "package": "swift-crypto",
+ "repositoryURL": "https://github.com/apple/swift-crypto.git",
"state": {
"branch": null,
- "revision": "3ffa48a7047fc9ac6581cd53ab1df29466d8f13b",
- "version": "0.2.0"
+ "revision": "9b9d1868601a199334da5d14f4ab2d37d4f8d0c5",
+ "version": "1.0.2"
}
},
{
- "package": "swift-crypto",
- "repositoryURL": "https://github.com/apple/swift-crypto.git",
+ "package": "swift-extras-base64",
+ "repositoryURL": "https://github.com/swift-extras/swift-extras-base64",
"state": {
"branch": null,
- "revision": "9b9d1868601a199334da5d14f4ab2d37d4f8d0c5",
- "version": "1.0.2"
+ "revision": "bf6706e1811e746cb204deaa921d8c7b4d0509e2",
+ "version": "0.4.0"
}
},
{
diff --git a/examples/Hello/Sources/Hello/main.swift b/examples/Hello/Sources/Hello/main.swift
index e2e6f79..d6c6316 100644
--- a/examples/Hello/Sources/Hello/main.swift
+++ b/examples/Hello/Sources/Hello/main.swift
@@ -19,6 +19,9 @@ app.post("hello") { req -> Hello in
let name = try req.content.decode(Name.self)
return Hello(hello: name.name)
}
-
+app.storage[Application.Lambda.Server.ConfigurationKey.self] = .init(apiService: .applicationLoadBalancer,
+ logger: app.logger)
app.servers.use(.lambda)
try app.run()
+
+
diff --git a/examples/Hello/makefile b/examples/Hello/makefile
index f28bec8..8335e2a 100644
--- a/examples/Hello/makefile
+++ b/examples/Hello/makefile
@@ -11,7 +11,7 @@ build_lambda:
$(SWIFT_DOCKER_IMAGE) \
swift build --product Hello -c release
-package_lambda: build_lambda
+package_lambda:
docker run \
--rm \
--volume "$(shell pwd)/../..:/src" \