Skip to content

Commit 5804758

Browse files
thomasgouveiadza89
authored andcommitted
feat: add support for LambdaFuctionURLRequest/Response (awslabs#172)
Signed-off-by: thomasgouveia <[email protected]>
1 parent 8c74d92 commit 5804758

File tree

7 files changed

+459
-0
lines changed

7 files changed

+459
-0
lines changed

Diff for: core/requestFnURL.go

+169
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
// Package core provides utility methods that help convert ALB events
2+
// into an http.Request and http.ResponseWriter
3+
package core
4+
5+
import (
6+
"bytes"
7+
"context"
8+
"encoding/base64"
9+
"encoding/json"
10+
"errors"
11+
"fmt"
12+
"log"
13+
"net/http"
14+
"os"
15+
"strings"
16+
17+
"github.com/aws/aws-lambda-go/events"
18+
"github.com/aws/aws-lambda-go/lambdacontext"
19+
)
20+
21+
const (
22+
// FnURLContextHeader is the custom header key used to store the
23+
// Function URL context. To access the Context properties use the
24+
// GetContext method of the RequestAccessorFnURL object.
25+
FnURLContextHeader = "X-GoLambdaProxy-FnURL-Context"
26+
)
27+
28+
// RequestAccessorFnURL objects give access to custom Function URL properties
29+
// in the request.
30+
type RequestAccessorFnURL struct {
31+
stripBasePath string
32+
}
33+
34+
// GetALBContext extracts the ALB context object from a request's custom header.
35+
// Returns a populated events.ALBTargetGroupRequestContext object from the request.
36+
func (r *RequestAccessorFnURL) GetContext(req *http.Request) (events.LambdaFunctionURLRequestContext, error) {
37+
if req.Header.Get(FnURLContextHeader) == "" {
38+
return events.LambdaFunctionURLRequestContext{}, errors.New("no context header in request")
39+
}
40+
context := events.LambdaFunctionURLRequestContext{}
41+
err := json.Unmarshal([]byte(req.Header.Get(FnURLContextHeader)), &context)
42+
if err != nil {
43+
log.Println("Error while unmarshalling context")
44+
log.Println(err)
45+
return events.LambdaFunctionURLRequestContext{}, err
46+
}
47+
return context, nil
48+
}
49+
50+
// StripBasePath instructs the RequestAccessor object that the given base
51+
// path should be removed from the request path before sending it to the
52+
// framework for routing. This is used when API Gateway is configured with
53+
// base path mappings in custom domain names.
54+
func (r *RequestAccessorFnURL) StripBasePath(basePath string) string {
55+
if strings.Trim(basePath, " ") == "" {
56+
r.stripBasePath = ""
57+
return ""
58+
}
59+
60+
newBasePath := basePath
61+
if !strings.HasPrefix(newBasePath, "/") {
62+
newBasePath = "/" + newBasePath
63+
}
64+
65+
if strings.HasSuffix(newBasePath, "/") {
66+
newBasePath = newBasePath[:len(newBasePath)-1]
67+
}
68+
69+
r.stripBasePath = newBasePath
70+
71+
return newBasePath
72+
}
73+
74+
// FunctionURLEventToHTTPRequest converts an a Function URL event into a http.Request object.
75+
// Returns the populated http request with additional custom header for the Function URL context.
76+
// To access these properties use the GetContext method of the RequestAccessorFnURL object.
77+
func (r *RequestAccessorFnURL) FunctionURLEventToHTTPRequest(req events.LambdaFunctionURLRequest) (*http.Request, error) {
78+
httpRequest, err := r.EventToRequest(req)
79+
if err != nil {
80+
log.Println(err)
81+
return nil, err
82+
}
83+
return addToHeaderFnURL(httpRequest, req)
84+
}
85+
86+
// FunctionURLEventToHTTPRequestWithContext converts a Function URL event and context into an http.Request object.
87+
// Returns the populated http request with lambda context, Function URL RequestContext as part of its context.
88+
func (r *RequestAccessorFnURL) FunctionURLEventToHTTPRequestWithContext(ctx context.Context, req events.LambdaFunctionURLRequest) (*http.Request, error) {
89+
httpRequest, err := r.EventToRequest(req)
90+
if err != nil {
91+
log.Println(err)
92+
return nil, err
93+
}
94+
return addToContextFnURL(ctx, httpRequest, req), nil
95+
}
96+
97+
// EventToRequest converts a Function URL event into an http.Request object.
98+
// Returns the populated request maintaining headers
99+
func (r *RequestAccessorFnURL) EventToRequest(req events.LambdaFunctionURLRequest) (*http.Request, error) {
100+
decodedBody := []byte(req.Body)
101+
if req.IsBase64Encoded {
102+
base64Body, err := base64.StdEncoding.DecodeString(req.Body)
103+
if err != nil {
104+
return nil, err
105+
}
106+
decodedBody = base64Body
107+
}
108+
109+
path := req.RawPath
110+
if r.stripBasePath != "" && len(r.stripBasePath) > 1 {
111+
if strings.HasPrefix(path, r.stripBasePath) {
112+
path = strings.Replace(path, r.stripBasePath, "", 1)
113+
}
114+
}
115+
if !strings.HasPrefix(path, "/") {
116+
path = "/" + path
117+
}
118+
119+
serverAddress := "https://" + req.RequestContext.DomainName
120+
if customAddress, ok := os.LookupEnv(CustomHostVariable); ok {
121+
serverAddress = customAddress
122+
}
123+
124+
path = serverAddress + path + "?" + req.RawQueryString
125+
126+
httpRequest, err := http.NewRequest(
127+
strings.ToUpper(req.RequestContext.HTTP.Method),
128+
path,
129+
bytes.NewReader(decodedBody),
130+
)
131+
132+
if err != nil {
133+
fmt.Printf("Could not convert request %s:%s to http.Request\n", req.RequestContext.HTTP.Method, req.RawPath)
134+
log.Println(err)
135+
return nil, err
136+
}
137+
138+
for header, val := range req.Headers {
139+
httpRequest.Header.Add(header, val)
140+
}
141+
142+
httpRequest.RemoteAddr = req.RequestContext.HTTP.SourceIP
143+
httpRequest.RequestURI = httpRequest.URL.RequestURI()
144+
145+
return httpRequest, nil
146+
}
147+
148+
func addToHeaderFnURL(req *http.Request, fnUrlRequest events.LambdaFunctionURLRequest) (*http.Request, error) {
149+
ctx, err := json.Marshal(fnUrlRequest.RequestContext)
150+
if err != nil {
151+
log.Println("Could not Marshal Function URL context for custom header")
152+
return req, err
153+
}
154+
req.Header.Set(FnURLContextHeader, string(ctx))
155+
return req, nil
156+
}
157+
158+
// adds context data to http request so we can pass
159+
func addToContextFnURL(ctx context.Context, req *http.Request, fnUrlRequest events.LambdaFunctionURLRequest) *http.Request {
160+
lc, _ := lambdacontext.FromContext(ctx)
161+
rc := requestContextFnURL{lambdaContext: lc, fnUrlContext: fnUrlRequest.RequestContext}
162+
ctx = context.WithValue(ctx, ctxKey{}, rc)
163+
return req.WithContext(ctx)
164+
}
165+
166+
type requestContextFnURL struct {
167+
lambdaContext *lambdacontext.LambdaContext
168+
fnUrlContext events.LambdaFunctionURLRequestContext
169+
}

Diff for: core/responseFnURL.go

+117
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
// Package core provides utility methods that help convert proxy events
2+
// into an http.Request and http.ResponseWriter
3+
package core
4+
5+
import (
6+
"bytes"
7+
"encoding/base64"
8+
"errors"
9+
"net/http"
10+
"unicode/utf8"
11+
12+
"github.com/aws/aws-lambda-go/events"
13+
)
14+
15+
// ProxyResponseWriterFunctionURL implements http.ResponseWriter and adds the method
16+
// necessary to return an events.LambdaFunctionURLResponse object
17+
type ProxyResponseWriterFunctionURL struct {
18+
status int
19+
headers http.Header
20+
body bytes.Buffer
21+
observers []chan<- bool
22+
}
23+
24+
// Ensure implementation satisfies http.ResponseWriter interface
25+
var (
26+
_ http.ResponseWriter = &ProxyResponseWriterFunctionURL{}
27+
)
28+
29+
// NewProxyResponseWriterFnURL returns a new ProxyResponseWriterFunctionURL object.
30+
// The object is initialized with an empty map of headers and a status code of -1
31+
func NewProxyResponseWriterFnURL() *ProxyResponseWriterFunctionURL {
32+
return &ProxyResponseWriterFunctionURL{
33+
headers: make(http.Header),
34+
status: defaultStatusCode,
35+
observers: make([]chan<- bool, 0),
36+
}
37+
}
38+
39+
func (r *ProxyResponseWriterFunctionURL) CloseNotify() <-chan bool {
40+
ch := make(chan bool, 1)
41+
42+
r.observers = append(r.observers, ch)
43+
44+
return ch
45+
}
46+
47+
func (r *ProxyResponseWriterFunctionURL) notifyClosed() {
48+
for _, v := range r.observers {
49+
v <- true
50+
}
51+
}
52+
53+
// Header implementation from the http.ResponseWriter interface.
54+
func (r *ProxyResponseWriterFunctionURL) Header() http.Header {
55+
return r.headers
56+
}
57+
58+
// Write sets the response body in the object. If no status code
59+
// was set before with the WriteHeader method it sets the status
60+
// for the response to 200 OK.
61+
func (r *ProxyResponseWriterFunctionURL) Write(body []byte) (int, error) {
62+
if r.status == defaultStatusCode {
63+
r.status = http.StatusOK
64+
}
65+
66+
// if the content type header is not set when we write the body we try to
67+
// detect one and set it by default. If the content type cannot be detected
68+
// it is automatically set to "application/octet-stream" by the
69+
// DetectContentType method
70+
if r.Header().Get(contentTypeHeaderKey) == "" {
71+
r.Header().Add(contentTypeHeaderKey, http.DetectContentType(body))
72+
}
73+
74+
return (&r.body).Write(body)
75+
}
76+
77+
// WriteHeader sets a status code for the response. This method is used
78+
// for error responses.
79+
func (r *ProxyResponseWriterFunctionURL) WriteHeader(status int) {
80+
r.status = status
81+
}
82+
83+
// GetProxyResponse converts the data passed to the response writer into
84+
// an events.LambdaFunctionURLResponse object.
85+
// Returns a populated proxy response object. If the response is invalid, for example
86+
// has no headers or an invalid status code returns an error.
87+
func (r *ProxyResponseWriterFunctionURL) GetProxyResponse() (events.LambdaFunctionURLResponse, error) {
88+
r.notifyClosed()
89+
90+
if r.status == defaultStatusCode {
91+
return events.LambdaFunctionURLResponse{}, errors.New("status code not set on response")
92+
}
93+
94+
var output string
95+
isBase64 := false
96+
97+
bb := (&r.body).Bytes()
98+
99+
if utf8.Valid(bb) {
100+
output = string(bb)
101+
} else {
102+
output = base64.StdEncoding.EncodeToString(bb)
103+
isBase64 = true
104+
}
105+
106+
headers := make(map[string]string)
107+
for h, v := range r.Header() {
108+
headers[h] = v[0]
109+
}
110+
111+
return events.LambdaFunctionURLResponse{
112+
StatusCode: r.status,
113+
Headers: headers,
114+
Body: output,
115+
IsBase64Encoded: isBase64,
116+
}, nil
117+
}

Diff for: core/typesFnURL.go

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package core
2+
3+
import (
4+
"net/http"
5+
6+
"github.com/aws/aws-lambda-go/events"
7+
)
8+
9+
// GatewayTimeoutFnURL returns a dafault Gateway Timeout (504) response
10+
func GatewayTimeoutFnURL() events.LambdaFunctionURLResponse {
11+
return events.LambdaFunctionURLResponse{StatusCode: http.StatusGatewayTimeout}
12+
}

Diff for: handlerfunc/adapterFnURL.go

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package handlerfunc
2+
3+
import (
4+
"net/http"
5+
6+
"github.com/awslabs/aws-lambda-go-api-proxy/httpadapter"
7+
)
8+
9+
type HandlerFuncAdapterFnURL = httpadapter.HandlerAdapterFnURL
10+
11+
func NewFunctionURL(handlerFunc http.HandlerFunc) *HandlerFuncAdapterFnURL {
12+
return httpadapter.NewFunctionURL(handlerFunc)
13+
}

Diff for: handlerfunc/adapterFnURL_test.go

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package handlerfunc_test
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"log"
7+
"net/http"
8+
9+
"github.com/aws/aws-lambda-go/events"
10+
"github.com/awslabs/aws-lambda-go-api-proxy/httpadapter"
11+
12+
. "github.com/onsi/ginkgo"
13+
. "github.com/onsi/gomega"
14+
)
15+
16+
var _ = Describe("HandlerFuncAdapter tests", func() {
17+
Context("Simple ping request", func() {
18+
It("Proxies the event correctly", func() {
19+
log.Println("Starting test")
20+
21+
var handler http.Handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
22+
w.Header().Add("unfortunately-required-header", "")
23+
fmt.Fprintf(w, "Go Lambda!!")
24+
})
25+
26+
adapter := httpadapter.NewFunctionURL(handler)
27+
28+
req := events.LambdaFunctionURLRequest{
29+
RequestContext: events.LambdaFunctionURLRequestContext{
30+
HTTP: events.LambdaFunctionURLRequestContextHTTPDescription{
31+
Method: http.MethodGet,
32+
Path: "/ping",
33+
},
34+
},
35+
}
36+
37+
resp, err := adapter.ProxyWithContext(context.Background(), req)
38+
39+
Expect(err).To(BeNil())
40+
Expect(resp.StatusCode).To(Equal(200))
41+
42+
resp, err = adapter.Proxy(req)
43+
44+
Expect(err).To(BeNil())
45+
Expect(resp.StatusCode).To(Equal(200))
46+
})
47+
})
48+
})

0 commit comments

Comments
 (0)