Skip to content

Commit 15d4cc1

Browse files
committed
lambdabackend: support "HTTP" event model (as opposed to "REST" model)
1 parent 02d741d commit 15d4cc1

4 files changed

Lines changed: 140 additions & 14 deletions

File tree

go.mod

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ module github.com/function61/edgerouter
33
go 1.23.1
44

55
require (
6-
github.com/aws/aws-lambda-go v1.14.0
7-
github.com/aws/aws-sdk-go v1.30.20
6+
github.com/aws/aws-lambda-go v1.49.0
7+
github.com/aws/aws-sdk-go v1.55.7
88
github.com/cozy/httpcache v0.0.0-20180914105234-d3dc4988de66
99
github.com/felixge/httpsnoop v1.0.1
1010
github.com/function61/certbus v0.0.0-20210703130951-f282ab2b0381
@@ -28,7 +28,7 @@ require (
2828
github.com/google/btree v1.0.0 // indirect
2929
github.com/hashicorp/hcl v1.0.0 // indirect
3030
github.com/inconshreveable/mousetrap v1.0.0 // indirect
31-
github.com/jmespath/go-jmespath v0.3.0 // indirect
31+
github.com/jmespath/go-jmespath v0.4.0 // indirect
3232
github.com/kataras/jwt v0.1.2 // indirect
3333
github.com/mattn/go-runewidth v0.0.8 // indirect
3434
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect

go.sum

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,14 @@ github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5
5555
github.com/aws/aws-lambda-go v1.13.2/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU=
5656
github.com/aws/aws-lambda-go v1.14.0 h1:kTr1VPabIgJsMVzHuZpNhs/5RR46LU6wyWUiHxtb3ag=
5757
github.com/aws/aws-lambda-go v1.14.0/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU=
58+
github.com/aws/aws-lambda-go v1.49.0 h1:z4VhTqkFZPM3xpEtTqWqRqsRH4TZBMJqTkRiBPYLqIQ=
59+
github.com/aws/aws-lambda-go v1.49.0/go.mod h1:dpMpZgvWx5vuQJfBt0zqBha60q7Dd7RfgJv23DymV8A=
5860
github.com/aws/aws-sdk-go v1.16.15/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
5961
github.com/aws/aws-sdk-go v1.29.0/go.mod h1:1KvfttTE3SPKMpo8g2c6jL3ZKfXtFvKscTgahTma5Xg=
6062
github.com/aws/aws-sdk-go v1.30.20 h1:ktsy2vodSZxz/arYqo7DlpkIeNohHL+4Rmjdo7YGtrE=
6163
github.com/aws/aws-sdk-go v1.30.20/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0=
64+
github.com/aws/aws-sdk-go v1.55.7 h1:UJrkFq7es5CShfBwlWAC8DA077vp8PyVbQd3lqLiztE=
65+
github.com/aws/aws-sdk-go v1.55.7/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
6266
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
6367
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
6468
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
@@ -208,6 +212,9 @@ github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANyt
208212
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
209213
github.com/jmespath/go-jmespath v0.3.0 h1:OS12ieG61fsCg5+qLJ+SsW9NicxNkg3b25OyT2yCeUc=
210214
github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik=
215+
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
216+
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
217+
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
211218
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
212219
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
213220
github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
@@ -346,6 +353,7 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P
346353
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
347354
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
348355
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
356+
github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s=
349357
github.com/tj/assert v0.0.0-20190920132354-ee03d75cd160/go.mod h1:mZ9/Rh9oLWpLLDRpvE+3b7gP/C2YyLFYxNmcLnPTMe0=
350358
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
351359
github.com/transip/gotransip/v6 v6.2.0/go.mod h1:pQZ36hWWRahCUXkFWlx9Hs711gLd8J4qdgLdRzmtY+g=

pkg/erbackend/lambdabackend/lambdabackend.go

Lines changed: 126 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@ import (
88
"fmt"
99
"io"
1010
"log"
11+
"net"
1112
"net/http"
1213
"net/url"
14+
"time"
1315

1416
"github.com/aws/aws-lambda-go/events"
1517
"github.com/aws/aws-sdk-go/aws"
@@ -23,6 +25,7 @@ import (
2325
type lambdaBackend struct {
2426
functionName string
2527
lambda *lambda.Lambda
28+
isPayloadV2 bool // https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html
2629
}
2730

2831
func New(opts erconfig.BackendOptsAwsLambda, logger *log.Logger) (http.Handler, error) {
@@ -36,31 +39,115 @@ func New(opts erconfig.BackendOptsAwsLambda, logger *log.Logger) (http.Handler,
3639
return nil, err
3740
}
3841

42+
isPayloadV2, err := validatePayloadFormatVersion(opts.PayloadFormatVersion)
43+
if err != nil {
44+
return nil, err
45+
}
46+
3947
handler := &lambdaBackend{
4048
functionName: opts.FunctionName,
4149
lambda: lambda.New(
4250
awsSession,
4351
aws.NewConfig().WithCredentials(creds).WithRegion(opts.RegionId)),
52+
isPayloadV2: isPayloadV2,
4453
}
4554

4655
return turbocharger.WrapWithMiddlewareIfConfigAvailable(handler, logger)
4756
}
4857

4958
func (b *lambdaBackend) ServeHTTP(w http.ResponseWriter, r *http.Request) {
59+
if b.isPayloadV2 {
60+
// https://pkg.go.dev/github.com/aws/aws-lambda-go/events#APIGatewayV2HTTPRequest
61+
b.serveHTTPModel(w, r)
62+
} else {
63+
// https://pkg.go.dev/github.com/aws/aws-lambda-go/events#APIGatewayProxyRequest
64+
// https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-input-format
65+
b.serveRESTModel(w, r)
66+
}
67+
}
68+
69+
func (b *lambdaBackend) serveHTTPModel(w http.ResponseWriter, r *http.Request) {
5070
defer r.Body.Close()
5171

52-
requestBodyBase64 := &bytes.Buffer{}
53-
requestBodyBase64Encoder := base64.NewEncoder(base64.StdEncoding, requestBodyBase64)
54-
if _, err := io.Copy(requestBodyBase64Encoder, r.Body); err != nil {
72+
bodyBase64, err := encodeToBase64RawStd(r.Body)
73+
if err != nil {
74+
http.Error(w, err.Error(), http.StatusBadRequest)
75+
return
76+
}
77+
78+
headers, err := copyHeaders(r)
79+
if err != nil {
5580
http.Error(w, err.Error(), http.StatusBadRequest)
5681
return
5782
}
5883

59-
if err := requestBodyBase64Encoder.Close(); err != nil {
84+
sourceIP, _, _ := net.SplitHostPort(r.RemoteAddr)
85+
86+
now := time.Now().UTC()
87+
88+
const routeKey = "$default"
89+
90+
proxyRequestJson, err := json.Marshal(events.APIGatewayV2HTTPRequest{
91+
Version: "2.0",
92+
RouteKey: routeKey,
93+
RawPath: r.URL.Path,
94+
RawQueryString: r.URL.RawQuery,
95+
// Cookies: []string{},
96+
Headers: headers,
97+
QueryStringParameters: queryParametersToSimpleMap(r.URL.Query()),
98+
RequestContext: events.APIGatewayV2HTTPRequestContext{
99+
RouteKey: routeKey,
100+
Stage: "$default",
101+
// RequestID: "",
102+
DomainName: r.URL.Host,
103+
Time: now.Format("02/Jan/2006:15:04:05 -0700"), // what a dumbass format
104+
TimeEpoch: now.UnixMilli(),
105+
HTTP: events.APIGatewayV2HTTPRequestContextHTTPDescription{
106+
Method: r.Method,
107+
Path: r.URL.Path,
108+
Protocol: r.Proto,
109+
SourceIP: sourceIP,
110+
UserAgent: r.UserAgent(),
111+
},
112+
},
113+
Body: bodyBase64,
114+
IsBase64Encoded: true,
115+
})
116+
if err != nil {
117+
http.Error(w, err.Error(), http.StatusInternalServerError)
118+
return
119+
}
120+
121+
lambdaResponse, err := b.lambda.InvokeWithContext(r.Context(), &lambda.InvokeInput{
122+
FunctionName: aws.String(b.functionName),
123+
Payload: proxyRequestJson,
124+
})
125+
if err != nil {
60126
http.Error(w, err.Error(), http.StatusInternalServerError)
61127
return
62128
}
63129

130+
payloadResponse := &events.APIGatewayV2HTTPResponse{}
131+
if err := json.Unmarshal(lambdaResponse.Payload, payloadResponse); err != nil {
132+
http.Error(w, err.Error(), http.StatusBadGateway)
133+
return
134+
}
135+
136+
if err := proxyApiGatewayResponse(payloadResponse, w); err != nil {
137+
// TODO: if we already wrote headers, this will not succeed
138+
http.Error(w, err.Error(), http.StatusBadGateway)
139+
}
140+
}
141+
142+
func (b *lambdaBackend) serveRESTModel(w http.ResponseWriter, r *http.Request) {
143+
defer r.Body.Close()
144+
145+
requestBodyBase64, err := encodeToBase64RawStd(r.Body)
146+
if err != nil {
147+
http.Error(w, err.Error(), http.StatusBadRequest)
148+
return
149+
}
150+
64151
headers, err := copyHeaders(r)
65152
if err != nil {
66153
http.Error(w, err.Error(), http.StatusBadRequest)
@@ -78,8 +165,8 @@ func (b *lambdaBackend) ServeHTTP(w http.ResponseWriter, r *http.Request) {
78165
},
79166
}
80167

81-
if requestBodyBase64.Len() > 0 {
82-
proxyRequest.Body = requestBodyBase64.String()
168+
if len(requestBodyBase64) > 0 {
169+
proxyRequest.Body = requestBodyBase64
83170
proxyRequest.IsBase64Encoded = true
84171
}
85172

@@ -89,7 +176,7 @@ func (b *lambdaBackend) ServeHTTP(w http.ResponseWriter, r *http.Request) {
89176
return
90177
}
91178

92-
lambdaResponse, err := b.lambda.Invoke(&lambda.InvokeInput{
179+
lambdaResponse, err := b.lambda.InvokeWithContext(r.Context(), &lambda.InvokeInput{
93180
FunctionName: aws.String(b.functionName),
94181
Payload: proxyRequestJson,
95182
})
@@ -110,13 +197,20 @@ func (b *lambdaBackend) ServeHTTP(w http.ResponseWriter, r *http.Request) {
110197
return
111198
}
112199

113-
if err := proxyApiGatewayResponse(payloadResponse, w); err != nil {
200+
if err := proxyApiGatewayResponse(&events.APIGatewayV2HTTPResponse{
201+
StatusCode: payloadResponse.StatusCode,
202+
Headers: payloadResponse.Headers,
203+
MultiValueHeaders: payloadResponse.MultiValueHeaders,
204+
Body: payloadResponse.Body,
205+
IsBase64Encoded: payloadResponse.IsBase64Encoded,
206+
// only field missing from old struct: `Cookies`
207+
}, w); err != nil {
114208
// TODO: if we already wrote headers, this will not succeed
115209
http.Error(w, err.Error(), http.StatusBadGateway)
116210
}
117211
}
118212

119-
func proxyApiGatewayResponse(payloadResponse *events.APIGatewayProxyResponse, w http.ResponseWriter) error {
213+
func proxyApiGatewayResponse(payloadResponse *events.APIGatewayV2HTTPResponse, w http.ResponseWriter) error {
120214
responseHeaders := w.Header()
121215

122216
for key, val := range payloadResponse.Headers {
@@ -165,3 +259,26 @@ func copyHeaders(r *http.Request) (map[string]string, error) {
165259

166260
return headers, nil
167261
}
262+
263+
func encodeToBase64RawStd(content io.Reader) (string, error) {
264+
bodyBuffered := &bytes.Buffer{}
265+
encodeBase64 := base64.NewEncoder(base64.RawStdEncoding, bodyBuffered)
266+
if _, err := io.Copy(encodeBase64, content); err != nil {
267+
return "", err
268+
}
269+
if err := encodeBase64.Close(); err != nil {
270+
return "", err
271+
}
272+
return bodyBuffered.String(), nil
273+
}
274+
275+
func validatePayloadFormatVersion(version string) (bool, error) {
276+
switch version {
277+
case "1.0", "":
278+
return false, nil
279+
case "2.0":
280+
return true, nil
281+
default:
282+
return false, fmt.Errorf("unrecognized PayloadFormatVersion: %s", version)
283+
}
284+
}

pkg/erconfig/appconfig.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -159,8 +159,9 @@ func (b *BackendOptsReverseProxy) Validate() error {
159159
}
160160

161161
type BackendOptsAwsLambda struct {
162-
FunctionName string `json:"function_name"`
163-
RegionId string `json:"region_id"`
162+
FunctionName string `json:"function_name"`
163+
RegionId string `json:"region_id"`
164+
PayloadFormatVersion string `json:"payload_format_version,omitempty"` // "1.0" (the default) or "2.0". https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html https://www.serverless.com/framework/docs/providers/aws/events/apigateway
164165
}
165166

166167
func (b *BackendOptsAwsLambda) Validate() error {

0 commit comments

Comments
 (0)