diff --git a/README.md b/README.md index 207351b..f5b521a 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ import ( "github.com/aws/aws-lambda-go/events" "github.com/aws/aws-lambda-go/lambda" - "github.com/awslabs/aws-lambda-go-api-proxy/gin" + ginadapter "github.com/awslabs/aws-lambda-go-api-proxy/gin" "github.com/gin-gonic/gin" ) @@ -84,6 +84,8 @@ func main() { } ``` +If you're using a Function URL, you can use the `ProxyFunctionURLWithContext` instead. + ### Fiber To use with the Fiber framework, following the instructions from the [Lambda documentation](https://docs.aws.amazon.com/lambda/latest/dg/go-programming-model-handler-types.html), declare a `Handler` method for the main package. @@ -132,6 +134,7 @@ func main() { lambda.Start(Handler) } ``` +If you're using a Function URL, you can use the `ProxyFunctionURLWithContext` instead. ## Other frameworks This package also supports [Negroni](https://github.com/urfave/negroni), [GorillaMux](https://github.com/gorilla/mux), and plain old `HandlerFunc` - take a look at the code in their respective sub-directories. All packages implement the `Proxy` method exactly like our Gin sample above. diff --git a/core/requestFnURL.go b/core/requestFnURL.go new file mode 100644 index 0000000..00f3dbe --- /dev/null +++ b/core/requestFnURL.go @@ -0,0 +1,205 @@ +// Package core provides utility methods that help convert proxy events +// into an http.Request and http.ResponseWriter +package core + +import ( + "bytes" + "context" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "log" + "net/http" + "net/url" + "os" + "strings" + + "github.com/aws/aws-lambda-go/events" + "github.com/aws/aws-lambda-go/lambdacontext" +) + +const ( + // FnURLContextHeader is the custom header key used to store the + // Function Url context. To access the Context properties use the + // GetFunctionURLContext method of the RequestAccessorFnURL object. + FnURLContextHeader = "X-GoLambdaProxy-Fu-Context" +) + +// RequestAccessorFnURL objects give access to custom API Gateway properties +// in the request. +type RequestAccessorFnURL struct { + stripBasePath string +} + +// GetFunctionURLContext extracts the API Gateway context object from a +// request's custom header. +// Returns a populated events.LambdaFunctionURLRequestContext object from +// the request. +func (r *RequestAccessorFnURL) GetFunctionURLContext(req *http.Request) (events.LambdaFunctionURLRequestContext, error) { + if req.Header.Get(APIGwContextHeader) == "" { + return events.LambdaFunctionURLRequestContext{}, errors.New("No context header in request") + } + context := events.LambdaFunctionURLRequestContext{} + err := json.Unmarshal([]byte(req.Header.Get(FnURLContextHeader)), &context) + if err != nil { + log.Println("Erorr while unmarshalling context") + log.Println(err) + return events.LambdaFunctionURLRequestContext{}, err + } + return context, nil +} + +// StripBasePath instructs the RequestAccessor object that the given base +// path should be removed from the request path before sending it to the +// framework for routing. This is used when the Lambda is configured with +// base path mappings in custom domain names. +func (r *RequestAccessorFnURL) StripBasePath(basePath string) string { + if strings.Trim(basePath, " ") == "" { + r.stripBasePath = "" + return "" + } + + newBasePath := basePath + if !strings.HasPrefix(newBasePath, "/") { + newBasePath = "/" + newBasePath + } + + if strings.HasSuffix(newBasePath, "/") { + newBasePath = newBasePath[:len(newBasePath)-1] + } + + r.stripBasePath = newBasePath + + return newBasePath +} + +// ProxyEventToHTTPRequest converts an Function URL proxy event into a http.Request object. +// Returns the populated http request with additional two custom headers for the stage variables and Function Url context. +// To access these properties use GetFunctionURLContext method of the RequestAccessor object. +func (r *RequestAccessorFnURL) ProxyEventToHTTPRequest(req events.LambdaFunctionURLRequest) (*http.Request, error) { + httpRequest, err := r.EventToRequest(req) + if err != nil { + log.Println(err) + return nil, err + } + return addToHeaderFunctionURL(httpRequest, req) +} + +// EventToRequestWithContext converts an Function URL proxy event and context into an http.Request object. +// Returns the populated http request with lambda context, stage variables and APIGatewayProxyRequestContext as part of its context. +// Access those using GetFunctionURLContextFromContext and GetRuntimeContextFromContext functions in this package. +func (r *RequestAccessorFnURL) EventToRequestWithContext(ctx context.Context, req events.LambdaFunctionURLRequest) (*http.Request, error) { + httpRequest, err := r.EventToRequest(req) + if err != nil { + log.Println(err) + return nil, err + } + return addToContextFunctionURL(ctx, httpRequest, req), nil +} + +// EventToRequest converts an Function URL proxy event into an http.Request object. +// Returns the populated request maintaining headers +func (r *RequestAccessorFnURL) EventToRequest(req events.LambdaFunctionURLRequest) (*http.Request, error) { + decodedBody := []byte(req.Body) + if req.IsBase64Encoded { + base64Body, err := base64.StdEncoding.DecodeString(req.Body) + if err != nil { + return nil, err + } + decodedBody = base64Body + } + + path := req.RawPath + // if RawPath empty is, populate from request context + if len(path) == 0 { + path = req.RequestContext.HTTP.Path + } + + if r.stripBasePath != "" && len(r.stripBasePath) > 1 { + if strings.HasPrefix(path, r.stripBasePath) { + path = strings.Replace(path, r.stripBasePath, "", 1) + } + fmt.Printf("%v", path) + } + if !strings.HasPrefix(path, "/") { + path = "/" + path + } + serverAddress := "https://" + req.RequestContext.DomainName + if customAddress, ok := os.LookupEnv(CustomHostVariable); ok { + serverAddress = customAddress + } + path = serverAddress + path + + if len(req.RawQueryString) > 0 { + path += "?" + req.RawQueryString + } else if len(req.QueryStringParameters) > 0 { + values := url.Values{} + for key, value := range req.QueryStringParameters { + values.Add(key, value) + } + path += "?" + values.Encode() + } + + httpRequest, err := http.NewRequest( + strings.ToUpper(req.RequestContext.HTTP.Method), + path, + bytes.NewReader(decodedBody), + ) + + if err != nil { + fmt.Printf("Could not convert request %s:%s to http.Request\n", req.RequestContext.HTTP.Method, req.RequestContext.HTTP.Path) + log.Println(err) + return nil, err + } + + httpRequest.RemoteAddr = req.RequestContext.HTTP.SourceIP + + for _, cookie := range req.Cookies { + httpRequest.Header.Add("Cookie", cookie) + } + + for headerKey, headerValue := range req.Headers { + for _, val := range strings.Split(headerValue, ",") { + httpRequest.Header.Add(headerKey, strings.Trim(val, " ")) + } + } + + httpRequest.RequestURI = httpRequest.URL.RequestURI() + + return httpRequest, nil +} + +func addToHeaderFunctionURL(req *http.Request, FunctionURLRequest events.LambdaFunctionURLRequest) (*http.Request, error) { + apiGwContext, err := json.Marshal(FunctionURLRequest.RequestContext) + if err != nil { + log.Println("Could not Marshal API GW context for custom header") + return req, err + } + req.Header.Add(APIGwContextHeader, string(apiGwContext)) + return req, nil +} + +func addToContextFunctionURL(ctx context.Context, req *http.Request, FunctionURLRequest events.LambdaFunctionURLRequest) *http.Request { + lc, _ := lambdacontext.FromContext(ctx) + rc := requestContextFnURL{lambdaContext: lc, FunctionURLProxyContext: FunctionURLRequest.RequestContext} + ctx = context.WithValue(ctx, ctxKey{}, rc) + return req.WithContext(ctx) +} + +// GetFunctionURLContextFromContext retrieve APIGatewayProxyRequestContext from context.Context +func GetFunctionURLContextFromContext(ctx context.Context) (events.LambdaFunctionURLRequestContext, bool) { + v, ok := ctx.Value(ctxKey{}).(requestContextFnURL) + return v.FunctionURLProxyContext, ok +} + +// GetRuntimeContextFromContextFnURL retrieve Lambda Runtime Context from context.Context +func GetRuntimeContextFromContextFnURL(ctx context.Context) (*lambdacontext.LambdaContext, bool) { + v, ok := ctx.Value(ctxKey{}).(requestContextFnURL) + return v.lambdaContext, ok +} + +type requestContextFnURL struct { + lambdaContext *lambdacontext.LambdaContext + FunctionURLProxyContext events.LambdaFunctionURLRequestContext +} diff --git a/core/requestFnURL_test.go b/core/requestFnURL_test.go new file mode 100644 index 0000000..6bd5eb2 --- /dev/null +++ b/core/requestFnURL_test.go @@ -0,0 +1,133 @@ +package core_test + +import ( + "context" + "crypto/rand" + "encoding/base64" + "fmt" + + "github.com/aws/aws-lambda-go/events" + "github.com/awslabs/aws-lambda-go-api-proxy/core" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("RequestAccessorFnURL tests", func() { + Context("Function URL event conversion", func() { + accessor := core.RequestAccessorFnURL{} + qs := make(map[string]string) + mvqs := make(map[string][]string) + hdr := make(map[string]string) + qs["UniqueId"] = "12345" + hdr["header1"] = "Testhdr1" + hdr["header2"] = "Testhdr2" + // Multivalue query strings + mvqs["k1"] = []string{"t1"} + mvqs["k2"] = []string{"t2"} + bdy := "Test BODY" + basePathRequest := getFunctionURLProxyRequest("/hello", getFunctionURLRequestContext("/hello", "GET"), false, hdr, bdy, qs, mvqs) + + It("Correctly converts a basic event", func() { + httpReq, err := accessor.EventToRequestWithContext(context.Background(), basePathRequest) + Expect(err).To(BeNil()) + Expect("/hello").To(Equal(httpReq.URL.Path)) + Expect("/hello?UniqueId=12345").To(Equal(httpReq.RequestURI)) + Expect("GET").To(Equal(httpReq.Method)) + headers := basePathRequest.Headers + Expect(2).To(Equal(len(headers))) + }) + + binaryBody := make([]byte, 256) + _, err := rand.Read(binaryBody) + if err != nil { + Fail("Could not generate random binary body") + } + + encodedBody := base64.StdEncoding.EncodeToString(binaryBody) + + binaryRequest := getFunctionURLProxyRequest("/hello", getFunctionURLRequestContext("/hello", "POST"), true, hdr, bdy, qs, mvqs) + binaryRequest.Body = encodedBody + binaryRequest.IsBase64Encoded = true + + It("Decodes a base64 encoded body", func() { + httpReq, err := accessor.EventToRequestWithContext(context.Background(), binaryRequest) + Expect(err).To(BeNil()) + Expect("/hello").To(Equal(httpReq.URL.Path)) + Expect("/hello?UniqueId=12345").To(Equal(httpReq.RequestURI)) + Expect("POST").To(Equal(httpReq.Method)) + }) + + mqsRequest := getFunctionURLProxyRequest("/hello", getFunctionURLRequestContext("/hello", "GET"), false, hdr, bdy, qs, mvqs) + mqsRequest.RawQueryString = "hello=1&world=2&world=3" + mqsRequest.QueryStringParameters = map[string]string{ + "hello": "1", + "world": "2", + } + + It("Populates query string correctly", func() { + httpReq, err := accessor.EventToRequestWithContext(context.Background(), mqsRequest) + Expect(err).To(BeNil()) + Expect("/hello").To(Equal(httpReq.URL.Path)) + fmt.Println("SDYFSDKFJDL") + fmt.Printf("%v", httpReq.RequestURI) + Expect(httpReq.RequestURI).To(ContainSubstring("hello=1")) + Expect(httpReq.RequestURI).To(ContainSubstring("world=2")) + Expect("GET").To(Equal(httpReq.Method)) + query := httpReq.URL.Query() + Expect(2).To(Equal(len(query))) + Expect(query["hello"]).ToNot(BeNil()) + Expect(query["world"]).ToNot(BeNil()) + }) + }) + + Context("StripBasePath tests", func() { + accessor := core.RequestAccessorFnURL{} + It("Adds prefix slash", func() { + basePath := accessor.StripBasePath("app1") + Expect("/app1").To(Equal(basePath)) + }) + + It("Removes trailing slash", func() { + basePath := accessor.StripBasePath("/app1/") + Expect("/app1").To(Equal(basePath)) + }) + + It("Ignores blank strings", func() { + basePath := accessor.StripBasePath(" ") + Expect("").To(Equal(basePath)) + }) + }) +}) + +func getFunctionURLProxyRequest(path string, requestCtx events.LambdaFunctionURLRequestContext, + is64 bool, header map[string]string, body string, qs map[string]string, mvqs map[string][]string) events.LambdaFunctionURLRequest { + return events.LambdaFunctionURLRequest{ + RequestContext: requestCtx, + RawPath: path, + RawQueryString: generateQueryString(qs), + Headers: header, + Body: body, + IsBase64Encoded: is64, + } +} + +func getFunctionURLRequestContext(path, method string) events.LambdaFunctionURLRequestContext { + return events.LambdaFunctionURLRequestContext{ + DomainName: "example.com", + HTTP: events.LambdaFunctionURLRequestContextHTTPDescription{ + Method: method, + Path: path, + }, + } +} + +func generateQueryString(queryParameters map[string]string) string { + var queryString string + for key, value := range queryParameters { + if queryString != "" { + queryString += "&" + } + queryString += fmt.Sprintf("%s=%s", key, value) + } + return queryString +} diff --git a/core/requestv2_test.go b/core/requestv2_test.go index 180e22b..8a45e78 100644 --- a/core/requestv2_test.go +++ b/core/requestv2_test.go @@ -3,12 +3,14 @@ package core_test import ( "context" "encoding/base64" - "github.com/onsi/gomega/gstruct" + "fmt" "io/ioutil" "math/rand" "os" "strings" + "github.com/onsi/gomega/gstruct" + "github.com/aws/aws-lambda-go/events" "github.com/aws/aws-lambda-go/lambdacontext" "github.com/awslabs/aws-lambda-go-api-proxy/core" @@ -74,6 +76,8 @@ var _ = Describe("RequestAccessorV2 tests", func() { It("Populates multiple value query string correctly", func() { httpReq, err := accessor.EventToRequestWithContext(context.Background(), mqsRequest) Expect(err).To(BeNil()) + fmt.Println("SDY!@$#!@FSDKFJDL") + fmt.Printf("%v", httpReq.RequestURI) Expect("/hello").To(Equal(httpReq.URL.Path)) Expect(httpReq.RequestURI).To(ContainSubstring("hello=1")) Expect(httpReq.RequestURI).To(ContainSubstring("world=2")) diff --git a/core/responseFnURL.go b/core/responseFnURL.go new file mode 100644 index 0000000..31cd892 --- /dev/null +++ b/core/responseFnURL.go @@ -0,0 +1,121 @@ +// Package core provides utility methods that help convert proxy events +// into an http.Request and http.ResponseWriter +package core + +import ( + "bytes" + "encoding/base64" + "errors" + "net/http" + "strings" + "unicode/utf8" + + "github.com/aws/aws-lambda-go/events" +) + +// FunctionURLResponseWriter implements http.ResponseWriter and adds the method +// necessary to return an events.LambdaFunctionURLResponse object +type FunctionURLResponseWriter struct { + headers http.Header + body bytes.Buffer + status int + observers []chan<- bool +} + +// NewFunctionURLResponseWriter returns a new FunctionURLResponseWriter object. +// The object is initialized with an empty map of headers and a +// status code of -1 +func NewFunctionURLResponseWriter() *FunctionURLResponseWriter { + return &FunctionURLResponseWriter{ + headers: make(http.Header), + status: defaultStatusCode, + observers: make([]chan<- bool, 0), + } +} + +func (r *FunctionURLResponseWriter) CloseNotify() <-chan bool { + ch := make(chan bool, 1) + + r.observers = append(r.observers, ch) + + return ch +} + +func (r *FunctionURLResponseWriter) notifyClosed() { + for _, v := range r.observers { + v <- true + } +} + +// Header implementation from the http.ResponseWriter interface. +func (r *FunctionURLResponseWriter) Header() http.Header { + return r.headers +} + +// Write sets the response body in the object. If no status code +// was set before with the WriteHeader method it sets the status +// for the response to 200 OK. +func (r *FunctionURLResponseWriter) Write(body []byte) (int, error) { + if r.status == defaultStatusCode { + r.status = http.StatusOK + } + + // if the content type header is not set when we write the body we try to + // detect one and set it by default. If the content type cannot be detected + // it is automatically set to "application/octet-stream" by the + // DetectContentType method + if r.Header().Get(contentTypeHeaderKey) == "" { + r.Header().Add(contentTypeHeaderKey, http.DetectContentType(body)) + } + + return (&r.body).Write(body) +} + +// WriteHeader sets a status code for the response. This method is used +// for error responses. +func (r *FunctionURLResponseWriter) WriteHeader(status int) { + r.status = status +} + +// GetProxyResponse converts the data passed to the response writer into +// an events.APIGatewayProxyResponse object. +// Returns a populated proxy response object. If the response is invalid, for example +// has no headers or an invalid status code returns an error. +func (r *FunctionURLResponseWriter) GetProxyResponse() (events.LambdaFunctionURLResponse, error) { + r.notifyClosed() + + if r.status == defaultStatusCode { + return events.LambdaFunctionURLResponse{}, errors.New("Status code not set on response") + } + + var output string + isBase64 := false + + bb := (&r.body).Bytes() + + if utf8.Valid(bb) { + output = string(bb) + } else { + output = base64.StdEncoding.EncodeToString(bb) + isBase64 = true + } + + headers := make(map[string]string) + cookies := make([]string, 0) + + for headerKey, headerValue := range http.Header(r.headers) { + if strings.EqualFold("set-cookie", headerKey) { + cookies = append(cookies, headerValue...) + continue + } + headers[headerKey] = strings.Join(headerValue, ",") + } + + return events.LambdaFunctionURLResponse{ + StatusCode: r.status, + Headers: headers, + Body: output, + IsBase64Encoded: isBase64, + Cookies: cookies, + }, nil +} diff --git a/core/responseFnURL_test.go b/core/responseFnURL_test.go new file mode 100644 index 0000000..3607297 --- /dev/null +++ b/core/responseFnURL_test.go @@ -0,0 +1,118 @@ +package core + +import ( + "math/rand" + "net/http" + "strings" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("FunctionURLResponseWriter tests", func() { + Context("writing to response object", func() { + response := NewFunctionURLResponseWriter() + + It("Sets the correct default status", func() { + Expect(defaultStatusCode).To(Equal(response.status)) + }) + + It("Initializes the headers map", func() { + Expect(response.headers).ToNot(BeNil()) + Expect(0).To(Equal(len(response.headers))) + }) + + It("Writes headers correctly", func() { + response.Header().Add("Content-Type", "application/json") + + Expect(1).To(Equal(len(response.headers))) + Expect("application/json").To(Equal(response.headers["Content-Type"][0])) + }) + + It("Writes body content correctly", func() { + binaryBody := make([]byte, 256) + _, err := rand.Read(binaryBody) + Expect(err).To(BeNil()) + + written, err := response.Write(binaryBody) + Expect(err).To(BeNil()) + Expect(len(binaryBody)).To(Equal(written)) + }) + + It("Automatically set the status code to 200", func() { + Expect(http.StatusOK).To(Equal(response.status)) + }) + + It("Forces the status to a new code", func() { + response.WriteHeader(http.StatusAccepted) + Expect(http.StatusAccepted).To(Equal(response.status)) + }) + }) + + Context("Automatically set response content type", func() { + xmlBodyContent := "ToveJaniReminderDon't forget me this weekend!" + htmlBodyContent := " Title of the documentContent of the document......" + + It("Does not set the content type if it's already set", func() { + resp := NewFunctionURLResponseWriter() + resp.Header().Add("Content-Type", "application/json") + + resp.Write([]byte(xmlBodyContent)) + + Expect("application/json").To(Equal(resp.Header().Get("Content-Type"))) + proxyResp, err := resp.GetProxyResponse() + Expect(err).To(BeNil()) + Expect(1).To(Equal(len(proxyResp.Headers))) + Expect("application/json").To(Equal(proxyResp.Headers["Content-Type"])) + Expect(xmlBodyContent).To(Equal(proxyResp.Body)) + }) + + It("Sets the content type to text/xml given the body", func() { + resp := NewFunctionURLResponseWriter() + resp.Write([]byte(xmlBodyContent)) + + Expect("").ToNot(Equal(resp.Header().Get("Content-Type"))) + Expect(true).To(Equal(strings.HasPrefix(resp.Header().Get("Content-Type"), "text/xml;"))) + proxyResp, err := resp.GetProxyResponse() + Expect(err).To(BeNil()) + Expect(1).To(Equal(len(proxyResp.Headers))) + Expect(true).To(Equal(strings.HasPrefix(proxyResp.Headers["Content-Type"], "text/xml;"))) + Expect(xmlBodyContent).To(Equal(proxyResp.Body)) + }) + + It("Sets the content type to text/html given the body", func() { + resp := NewFunctionURLResponseWriter() + resp.Write([]byte(htmlBodyContent)) + + Expect("").ToNot(Equal(resp.Header().Get("Content-Type"))) + Expect(true).To(Equal(strings.HasPrefix(resp.Header().Get("Content-Type"), "text/html;"))) + proxyResp, err := resp.GetProxyResponse() + Expect(err).To(BeNil()) + Expect(1).To(Equal(len(proxyResp.Headers))) + Expect(true).To(Equal(strings.HasPrefix(proxyResp.Headers["Content-Type"], "text/html;"))) + Expect(htmlBodyContent).To(Equal(proxyResp.Body)) + }) + }) + + Context("Export Lambda Function URL response", func() { + emptyResponse := NewFunctionURLResponseWriter() + emptyResponse.Header().Add("Content-Type", "application/json") + + It("Refuses empty responses with default status code", func() { + _, err := emptyResponse.GetProxyResponse() + Expect(err).ToNot(BeNil()) + Expect("Status code not set on response").To(Equal(err.Error())) + }) + + simpleResponse := NewFunctionURLResponseWriter() + simpleResponse.Write([]byte("https://example.com")) + simpleResponse.WriteHeader(http.StatusAccepted) + + It("Writes function URL response correctly", func() { + FunctionURLResponse, err := simpleResponse.GetProxyResponse() + Expect(err).To(BeNil()) + Expect(FunctionURLResponse).ToNot(BeNil()) + Expect(http.StatusAccepted).To(Equal(FunctionURLResponse.StatusCode)) + }) + }) +}) diff --git a/core/typesFnURL.go b/core/typesFnURL.go new file mode 100644 index 0000000..7370e57 --- /dev/null +++ b/core/typesFnURL.go @@ -0,0 +1,11 @@ +package core + +import ( + "net/http" + + "github.com/aws/aws-lambda-go/events" +) + +func FunctionURLTimeout() events.LambdaFunctionURLResponse { + return events.LambdaFunctionURLResponse{StatusCode: http.StatusGatewayTimeout} +} diff --git a/fiber/adapter.go b/fiber/adapter.go index 592674e..a461581 100644 --- a/fiber/adapter.go +++ b/fiber/adapter.go @@ -13,11 +13,10 @@ import ( "strings" "github.com/aws/aws-lambda-go/events" + "github.com/awslabs/aws-lambda-go-api-proxy/core" "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/utils" "github.com/valyala/fasthttp" - - "github.com/awslabs/aws-lambda-go-api-proxy/core" ) // FiberLambda makes it easy to send API Gateway proxy events to a fiber.App. @@ -26,6 +25,7 @@ import ( type FiberLambda struct { core.RequestAccessor v2 core.RequestAccessorV2 + fn core.RequestAccessorFnURL app *fiber.App } @@ -66,6 +66,16 @@ func (f *FiberLambda) ProxyWithContextV2(ctx context.Context, req events.APIGate return f.proxyInternalV2(fiberRequest, err) } +func (f *FiberLambda) ProxyFunctionURL(req events.LambdaFunctionURLRequest) (events.LambdaFunctionURLResponse, error) { + fiberRequest, err := f.fn.EventToRequest(req) + return f.proxyFunctionURL(fiberRequest, err) +} + +func (f *FiberLambda) ProxyFunctionURLWithContext(ctx context.Context, req events.LambdaFunctionURLRequest) (events.LambdaFunctionURLResponse, error) { + fiberRequest, err := f.fn.EventToRequestWithContext(ctx, req) + return f.proxyFunctionURL(fiberRequest, err) +} + func (f *FiberLambda) proxyInternal(req *http.Request, err error) (events.APIGatewayProxyResponse, error) { if err != nil { @@ -100,6 +110,23 @@ func (f *FiberLambda) proxyInternalV2(req *http.Request, err error) (events.APIG return proxyResponse, nil } +func (f *FiberLambda) proxyFunctionURL(req *http.Request, err error) (events.LambdaFunctionURLResponse, error) { + + if err != nil { + return core.FunctionURLTimeout(), core.NewLoggedError("Could not convert proxy event to request: %v", err) + } + + resp := core.NewFunctionURLResponseWriter() + f.adaptor(resp, req) + + FunctionURLResponse, err := resp.GetProxyResponse() + if err != nil { + return core.FunctionURLTimeout(), core.NewLoggedError("Error while generating proxy response: %v", err) + } + + return FunctionURLResponse, nil +} + func (f *FiberLambda) adaptor(w http.ResponseWriter, r *http.Request) { // New fasthttp request req := fasthttp.AcquireRequest() diff --git a/fiber/fiberlambda_test.go b/fiber/fiberlambda_test.go index 7813911..dbbc5a5 100644 --- a/fiber/fiberlambda_test.go +++ b/fiber/fiberlambda_test.go @@ -315,4 +315,45 @@ var _ = Describe("FiberLambda tests", func() { Expect(resp.Body).To(Equal("")) }) }) + + Context("Function URL", func() { + It("Proxies the event correctly", func() { + app := fiber.New() + app.Get("/ping", func(c *fiber.Ctx) error { + return c.SendString("pong") + }) + + adapter := fiberadaptor.New(app) + + req := events.LambdaFunctionURLRequest{ + RawPath: "/ping", + } + + resp, err := adapter.ProxyFunctionURL(req) + + Expect(err).To(BeNil()) + Expect(resp.StatusCode).To(Equal(200)) + Expect(resp.Body).To(Equal("pong")) + }) + + It("Proxies the event correctly with context", func() { + app := fiber.New() + app.Get("/ping", func(c *fiber.Ctx) error { + return c.SendString("pong") + }) + + adapter := fiberadaptor.New(app) + + req := events.LambdaFunctionURLRequest{ + RawPath: "/ping", + } + + ctx := context.Background() + resp, err := adapter.ProxyFunctionURLWithContext(ctx, req) + + Expect(err).To(BeNil()) + Expect(resp.StatusCode).To(Equal(200)) + Expect(resp.Body).To(Equal("pong")) + }) + }) }) diff --git a/gin/adapter.go b/gin/adapter.go index 2c3b36f..ecf706c 100644 --- a/gin/adapter.go +++ b/gin/adapter.go @@ -17,6 +17,7 @@ import ( // creates a proxy response object from the http.ResponseWriter type GinLambda struct { core.RequestAccessor + fn core.RequestAccessorFnURL ginEngine *gin.Engine } @@ -44,6 +45,16 @@ func (g *GinLambda) ProxyWithContext(ctx context.Context, req events.APIGatewayP return g.proxyInternal(ginRequest, err) } +func (g *GinLambda) ProxyFunctionURL(req events.LambdaFunctionURLRequest) (events.LambdaFunctionURLResponse, error) { + ginRequest, err := g.fn.ProxyEventToHTTPRequest(req) + return g.proxyFunctionURL(ginRequest, err) +} + +func (g *GinLambda) ProxyFunctionURLWithContext(ctx context.Context, req events.LambdaFunctionURLRequest) (events.LambdaFunctionURLResponse, error) { + ginRequest, err := g.fn.EventToRequestWithContext(ctx, req) + return g.proxyFunctionURL(ginRequest, err) +} + func (g *GinLambda) proxyInternal(req *http.Request, err error) (events.APIGatewayProxyResponse, error) { if err != nil { @@ -60,3 +71,20 @@ func (g *GinLambda) proxyInternal(req *http.Request, err error) (events.APIGatew return proxyResponse, nil } + +func (g *GinLambda) proxyFunctionURL(req *http.Request, err error) (events.LambdaFunctionURLResponse, error) { + + if err != nil { + return core.FunctionURLTimeout(), core.NewLoggedError("Could not convert proxy event to request: %v", err) + } + + resp := core.NewFunctionURLResponseWriter() + g.ginEngine.ServeHTTP(resp, req) + + FunctionURLResponse, err := resp.GetProxyResponse() + if err != nil { + return core.FunctionURLTimeout(), core.NewLoggedError("Error while generating proxy response: %v", err) + } + + return FunctionURLResponse, nil +} diff --git a/gin/ginlambda_test.go b/gin/ginlambda_test.go index e17610b..da664e9 100644 --- a/gin/ginlambda_test.go +++ b/gin/ginlambda_test.go @@ -42,6 +42,53 @@ var _ = Describe("GinLambda tests", func() { Expect(resp.StatusCode).To(Equal(200)) }) }) + Context("Function URL", func() { + It("Proxies the event correctly", func() { + log.Println("Starting test") + r := gin.Default() + r.GET("/ping", func(c *gin.Context) { + log.Println("Handler!!") + c.JSON(200, gin.H{ + "message": "pong", + }) + }) + + adapter := ginadapter.New(r) + + req := events.LambdaFunctionURLRequest{ + RawPath: "/ping", + } + + resp, err := adapter.ProxyFunctionURL(req) + + Expect(err).To(BeNil()) + Expect(resp.StatusCode).To(Equal(200)) + Expect(resp.Body).To(Equal("{\"message\":\"pong\"}")) + }) + + It("Proxies the event correctly with context", func() { + r := gin.Default() + r.GET("/ping", func(c *gin.Context) { + log.Println("Handler!!") + c.JSON(200, gin.H{ + "message": "pong", + }) + }) + + adapter := ginadapter.New(r) + + req := events.LambdaFunctionURLRequest{ + RawPath: "/ping", + } + + ctx := context.Background() + resp, err := adapter.ProxyFunctionURLWithContext(ctx, req) + + Expect(err).To(BeNil()) + Expect(resp.StatusCode).To(Equal(200)) + Expect(resp.Body).To(Equal("{\"message\":\"pong\"}")) + }) + }) }) var _ = Describe("GinLambdaV2 tests", func() { diff --git a/handlerfunc/adapterFnURL.go b/handlerfunc/adapterFnURL.go new file mode 100644 index 0000000..a4dcc58 --- /dev/null +++ b/handlerfunc/adapterFnURL.go @@ -0,0 +1,13 @@ +package handlerfunc + +import ( + "net/http" + + "github.com/awslabs/aws-lambda-go-api-proxy/httpadapter" +) + +type HandlerFuncAdapterFnURL = httpadapter.HandlerAdapterFnURL + +func NewFunctionURL(handlerFunc http.HandlerFunc) *HandlerFuncAdapterFnURL { + return httpadapter.NewFunctionURL(handlerFunc) +} diff --git a/handlerfunc/adapterFnURL_test.go b/handlerfunc/adapterFnURL_test.go new file mode 100644 index 0000000..5e99c11 --- /dev/null +++ b/handlerfunc/adapterFnURL_test.go @@ -0,0 +1,48 @@ +package handlerfunc_test + +import ( + "context" + "fmt" + "log" + "net/http" + + "github.com/aws/aws-lambda-go/events" + "github.com/awslabs/aws-lambda-go-api-proxy/httpadapter" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("HandlerFuncAdapter tests", func() { + Context("Simple ping request", func() { + It("Proxies the event correctly", func() { + log.Println("Starting test") + + var handler http.Handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + w.Header().Add("unfortunately-required-header", "") + fmt.Fprintf(w, "Go Lambda!!") + }) + + adapter := httpadapter.NewFunctionURL(handler) + + req := events.LambdaFunctionURLRequest{ + RequestContext: events.LambdaFunctionURLRequestContext{ + HTTP: events.LambdaFunctionURLRequestContextHTTPDescription{ + Method: http.MethodGet, + Path: "/ping", + }, + }, + } + + resp, err := adapter.ProxyWithContext(context.Background(), req) + + Expect(err).To(BeNil()) + Expect(resp.StatusCode).To(Equal(200)) + + resp, err = adapter.Proxy(req) + + Expect(err).To(BeNil()) + Expect(resp.StatusCode).To(Equal(200)) + }) + }) +}) diff --git a/httpadapter/adapterFnURL.go b/httpadapter/adapterFnURL.go new file mode 100644 index 0000000..d894095 --- /dev/null +++ b/httpadapter/adapterFnURL.go @@ -0,0 +1,52 @@ +package httpadapter + +import ( + "context" + "net/http" + + "github.com/aws/aws-lambda-go/events" + "github.com/awslabs/aws-lambda-go-api-proxy/core" +) + +type HandlerAdapterFnURL struct { + core.RequestAccessorFnURL + handler http.Handler +} + +func NewFunctionURL(handler http.Handler) *HandlerAdapterFnURL { + return &HandlerAdapterFnURL{ + handler: handler, + } +} + +// Proxy receives an ALB Target Group proxy event, transforms it into an http.Request +// object, and sends it to the http.HandlerFunc for routing. +// It returns a proxy response object generated from the http.ResponseWriter. +func (h *HandlerAdapterFnURL) Proxy(event events.LambdaFunctionURLRequest) (events.LambdaFunctionURLResponse, error) { + req, err := h.ProxyEventToHTTPRequest(event) + return h.proxyInternal(req, err) +} + +// ProxyWithContext receives context and an API Gateway proxy event, +// transforms them into an http.Request object, and sends it to the echo.Echo for routing. +// It returns a proxy response object generated from the http.ResponseWriter. +func (h *HandlerAdapterFnURL) ProxyWithContext(ctx context.Context, event events.LambdaFunctionURLRequest) (events.LambdaFunctionURLResponse, error) { + req, err := h.EventToRequestWithContext(ctx, event) + return h.proxyInternal(req, err) +} + +func (h *HandlerAdapterFnURL) proxyInternal(req *http.Request, err error) (events.LambdaFunctionURLResponse, error) { + if err != nil { + return core.FunctionURLTimeout(), core.NewLoggedError("Could not convert proxy event to request: %v", err) + } + + w := core.NewFunctionURLResponseWriter() + h.handler.ServeHTTP(http.ResponseWriter(w), req) + + resp, err := w.GetProxyResponse() + if err != nil { + return core.FunctionURLTimeout(), core.NewLoggedError("Error while generating proxy response: %v", err) + } + + return resp, nil +} diff --git a/httpadapter/adapterFnURL_test.go b/httpadapter/adapterFnURL_test.go new file mode 100644 index 0000000..ff13961 --- /dev/null +++ b/httpadapter/adapterFnURL_test.go @@ -0,0 +1,48 @@ +package httpadapter_test + +import ( + "context" + "fmt" + "log" + "net/http" + + "github.com/aws/aws-lambda-go/events" + "github.com/awslabs/aws-lambda-go-api-proxy/httpadapter" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("HandlerFuncAdapter tests", func() { + Context("Simple ping request", func() { + It("Proxies the event correctly", func() { + log.Println("Starting test") + + var handler http.Handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + w.Header().Add("unfortunately-required-header", "") + fmt.Fprintf(w, "Go Lambda!!") + }) + + adapter := httpadapter.NewFunctionURL(handler) + + req := events.LambdaFunctionURLRequest{ + RequestContext: events.LambdaFunctionURLRequestContext{ + HTTP: events.LambdaFunctionURLRequestContextHTTPDescription{ + Method: http.MethodGet, + Path: "/ping", + }, + }, + } + + resp, err := adapter.ProxyWithContext(context.Background(), req) + + Expect(err).To(BeNil()) + Expect(resp.StatusCode).To(Equal(200)) + + resp, err = adapter.Proxy(req) + + Expect(err).To(BeNil()) + Expect(resp.StatusCode).To(Equal(200)) + }) + }) +})