diff --git a/adapter/adapter.go b/adapter/adapter.go new file mode 100644 index 0000000..0100301 --- /dev/null +++ b/adapter/adapter.go @@ -0,0 +1,155 @@ +// Package fiberadapter adds Fiber support for the aws-severless-go-api library. +// Uses the core package behind the scenes and exposes the New method to +// get a new instance and Proxy method to send request to the Fiber app. +package fiberadapter + +import ( + "context" + "io/ioutil" + "net" + "net/http" + + "github.com/aws/aws-lambda-go/events" + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/utils" + "github.com/valyala/fasthttp" + + "github.com/fumeapp/fiber/core" +) + +// FiberLambda makes it easy to send API Gateway proxy events to a fiber.App. +// The library transforms the proxy event into an HTTP request and then +// creates a proxy response object from the *fiber.Ctx +type FiberLambda struct { + core.RequestAccessor + v2 core.RequestAccessorV2 + app *fiber.App +} + +// New creates a new instance of the FiberLambda object. +// Receives an initialized *fiber.App object - normally created with fiber.New(). +// It returns the initialized instance of the FiberLambda object. +func New(app *fiber.App) *FiberLambda { + return &FiberLambda{ + app: app, + } +} + +// Proxy receives an API Gateway proxy event, transforms it into an http.Request +// object, and sends it to the fiber.App for routing. +// It returns a proxy response object generated from the http.ResponseWriter. +func (f *FiberLambda) Proxy(req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { + fiberRequest, err := f.ProxyEventToHTTPRequest(req) + return f.proxyInternal(fiberRequest, err) +} + +// ProxyV2 is just same as Proxy() but for APIGateway HTTP payload v2 +func (f *FiberLambda) ProxyV2(req events.APIGatewayV2HTTPRequest) (events.APIGatewayV2HTTPResponse, error) { + fiberRequest, err := f.v2.ProxyEventToHTTPRequest(req) + return f.proxyInternalV2(fiberRequest, 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 (f *FiberLambda) ProxyWithContext(ctx context.Context, req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { + fiberRequest, err := f.EventToRequestWithContext(ctx, req) + return f.proxyInternal(fiberRequest, err) +} + +// ProxyWithContextV2 is just same as ProxyWithContext() but for APIGateway HTTP payload v2 +func (f *FiberLambda) ProxyWithContextV2(ctx context.Context, req events.APIGatewayV2HTTPRequest) (events.APIGatewayV2HTTPResponse, error) { + fiberRequest, err := f.v2.EventToRequestWithContext(ctx, req) + return f.proxyInternalV2(fiberRequest, err) +} + +func (f *FiberLambda) proxyInternal(req *http.Request, err error) (events.APIGatewayProxyResponse, error) { + + if err != nil { + return core.GatewayTimeout(), core.NewLoggedError("Could not convert proxy event to request: %v", err) + } + + resp := core.NewProxyResponseWriter() + f.adaptor(resp, req) + + proxyResponse, err := resp.GetProxyResponse() + if err != nil { + return core.GatewayTimeout(), core.NewLoggedError("Error while generating proxy response: %v", err) + } + + return proxyResponse, nil +} + +func (f *FiberLambda) proxyInternalV2(req *http.Request, err error) (events.APIGatewayV2HTTPResponse, error) { + + if err != nil { + return core.GatewayTimeoutV2(), core.NewLoggedError("Could not convert proxy event to request: %v", err) + } + + resp := core.NewProxyResponseWriterV2() + f.adaptor(resp, req) + + proxyResponse, err := resp.GetProxyResponse() + if err != nil { + return core.GatewayTimeoutV2(), core.NewLoggedError("Error while generating proxy response: %v", err) + } + + return proxyResponse, nil +} + +func (f *FiberLambda) adaptor(w http.ResponseWriter, r *http.Request) { + // New fasthttp request + req := fasthttp.AcquireRequest() + defer fasthttp.ReleaseRequest(req) + + // Convert net/http -> fasthttp request + body, err := ioutil.ReadAll(r.Body) + if err != nil { + http.Error(w, utils.StatusMessage(fiber.StatusInternalServerError), fiber.StatusInternalServerError) + return + } + req.Header.SetContentLength(len(body)) + _, _ = req.BodyWriter().Write(body) + + req.Header.SetMethod(r.Method) + req.SetRequestURI(r.RequestURI) + req.SetHost(r.Host) + for key, val := range r.Header { + for _, v := range val { + switch key { + case fiber.HeaderHost, + fiber.HeaderContentType, + fiber.HeaderUserAgent, + fiber.HeaderContentLength, + fiber.HeaderConnection: + req.Header.Set(key, v) + default: + req.Header.Add(key, v) + } + } + } + + remoteAddr, err := net.ResolveTCPAddr("tcp", r.RemoteAddr) + if err != nil { + http.Error(w, utils.StatusMessage(fiber.StatusInternalServerError), fiber.StatusInternalServerError) + return + } + + // New fasthttp Ctx + var fctx fasthttp.RequestCtx + fctx.Init(req, remoteAddr, nil) + + // Pass RequestCtx to Fiber router + f.app.Handler()(&fctx) + + // Set response headers + fctx.Response.Header.VisitAll(func(k, v []byte) { + w.Header().Add(utils.UnsafeString(k), utils.UnsafeString(v)) + }) + + // Set response statuscode + w.WriteHeader(fctx.Response.StatusCode()) + + // Set response body + _, _ = w.Write(fctx.Response.Body()) +} diff --git a/adapter/fiber_suite_test.go b/adapter/fiber_suite_test.go new file mode 100644 index 0000000..89fd77b --- /dev/null +++ b/adapter/fiber_suite_test.go @@ -0,0 +1,13 @@ +package fiberadapter_test + +import ( + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func TestFiber(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Fiber Suite") +} diff --git a/adapter/fiberlambda_test.go b/adapter/fiberlambda_test.go new file mode 100644 index 0000000..01eebf9 --- /dev/null +++ b/adapter/fiberlambda_test.go @@ -0,0 +1,284 @@ +package fiberadapter_test + +import ( + "context" + fiberadapter "github.com/fumeapp/fiber/adapter" + + "github.com/aws/aws-lambda-go/events" + "github.com/gofiber/fiber/v2" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("FiberLambda tests", func() { + Context("Simple ping request", func() { + It("Proxies the event correctly", func() { + app := fiber.New() + app.Get("/ping", func(c *fiber.Ctx) error { + return c.SendString("pong") + }) + + adapter := fiberadapter.New(app) + + req := events.APIGatewayProxyRequest{ + Path: "/ping", + HTTPMethod: "GET", + } + + 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)) + }) + }) + + Context("Request header", func() { + It("Check pass canonical header to fiber", func() { + app := fiber.New() + app.Post("/canonical_header", func(c *fiber.Ctx) error { + Expect(c.Get(fiber.HeaderHost)).To(Equal("localhost")) + Expect(c.Get(fiber.HeaderContentType)).To(Equal(fiber.MIMEApplicationJSONCharsetUTF8)) + Expect(c.Get(fiber.HeaderUserAgent)).To(Equal("fiber")) + + Expect(c.Cookies("a")).To(Equal("b")) + Expect(c.Cookies("b")).To(Equal("c")) + Expect(c.Cookies("c")).To(Equal("d")) + + Expect(c.Get(fiber.HeaderContentLength)).To(Equal("77")) + Expect(c.Get(fiber.HeaderConnection)).To(Equal("Keep-Alive")) + Expect(c.Get(fiber.HeaderKeepAlive)).To(Equal("timeout=5, max=1000")) + + return c.Status(fiber.StatusNoContent).Send(nil) + }) + + adapter := fiberadapter.New(app) + + req := events.APIGatewayProxyRequest{ + Path: "/canonical_header", + HTTPMethod: "POST", + MultiValueHeaders: map[string][]string{ + fiber.HeaderHost: {"localhost"}, + fiber.HeaderContentType: {fiber.MIMEApplicationJSONCharsetUTF8}, + fiber.HeaderUserAgent: {"fiber"}, + + "cookie": {"a=b", "b=c;c=d"}, + + fiber.HeaderContentLength: {"77"}, + fiber.HeaderConnection: {"Keep-Alive"}, + fiber.HeaderKeepAlive: {"timeout=5, max=1000"}, + }, + } + + resp, err := adapter.ProxyWithContext(context.Background(), req) + + Expect(err).To(BeNil()) + Expect(resp.StatusCode).To(Equal(fiber.StatusNoContent)) + Expect(resp.Body).To(Equal("")) + }) + + It("Check pass non canonical header to fiber", func() { + app := fiber.New() + app.Post("/header", func(c *fiber.Ctx) error { + Expect(c.Get(fiber.HeaderReferer)).To(Equal("https://github.com/gofiber/fiber")) + Expect(c.Get(fiber.HeaderAuthorization)).To(Equal("Bearer drink_beer_not_coffee")) + + c.Context().Request.Header.VisitAll(func(key, value []byte) { + if string(key) == "K1" { + Expect(Expect(c.Get("K1")).To(Or(Equal("v1"), Equal("v2")))) + } + }) + + return c.Status(fiber.StatusNoContent).Send(nil) + }) + + adapter := fiberadapter.New(app) + + req := events.APIGatewayProxyRequest{ + Path: "/header", + HTTPMethod: "POST", + MultiValueHeaders: map[string][]string{ + fiber.HeaderReferer: {"https://github.com/gofiber/fiber"}, + fiber.HeaderAuthorization: {"Bearer drink_beer_not_coffee"}, + + "k1": {"v1", "v2"}, + }, + } + + resp, err := adapter.ProxyWithContext(context.Background(), req) + + Expect(err).To(BeNil()) + Expect(resp.StatusCode).To(Equal(fiber.StatusNoContent)) + Expect(resp.Body).To(Equal("")) + }) + }) + + Context("Response header", func() { + It("Check pass canonical header to fiber", func() { + app := fiber.New() + app.Post("/canonical_header", func(c *fiber.Ctx) error { + c.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSONCharsetUTF8) + c.Set(fiber.HeaderServer, "localhost") + + c.Cookie(&fiber.Cookie{ + Name: "a", + Value: "b", + HTTPOnly: true, + }) + c.Cookie(&fiber.Cookie{ + Name: "b", + Value: "c", + HTTPOnly: true, + }) + c.Cookie(&fiber.Cookie{ + Name: "c", + Value: "d", + HTTPOnly: true, + }) + + c.Set(fiber.HeaderContentLength, "77") + c.Set(fiber.HeaderConnection, "keep-alive") + + return c.Status(fiber.StatusNoContent).Send(nil) + }) + + adapter := fiberadapter.New(app) + + req := events.APIGatewayProxyRequest{ + Path: "/canonical_header", + HTTPMethod: "POST", + } + + resp, err := adapter.ProxyWithContext(context.Background(), req) + + Expect(err).To(BeNil()) + Expect(resp.StatusCode).To(Equal(fiber.StatusNoContent)) + // NOTI: core.NewProxyResponseWriter().GetProxyResponse() => Doesn't use `resp.Header` + Expect(resp.MultiValueHeaders[fiber.HeaderContentType]).To(Equal([]string{fiber.MIMEApplicationJSONCharsetUTF8})) + Expect(resp.MultiValueHeaders[fiber.HeaderServer]).To(Equal([]string{"localhost"})) + Expect(resp.MultiValueHeaders[fiber.HeaderSetCookie]).To(Equal([]string{"a=b; path=/; HttpOnly; SameSite=Lax", "b=c; path=/; HttpOnly; SameSite=Lax", "c=d; path=/; HttpOnly; SameSite=Lax"})) + Expect(resp.MultiValueHeaders[fiber.HeaderContentLength]).To(Equal([]string{"77"})) + Expect(resp.MultiValueHeaders[fiber.HeaderConnection]).To(Equal([]string{"keep-alive"})) + Expect(resp.Body).To(Equal("")) + }) + It("Check pass non canonical header to fiber", func() { + app := fiber.New() + app.Post("/header", func(c *fiber.Ctx) error { + c.Links("http://api.example.com/users?page=2", "next", "http://api.example.com/users?page=5", "last") + return c.Redirect("https://github.com/gofiber/fiber") + }) + + adapter := fiberadapter.New(app) + + req := events.APIGatewayProxyRequest{ + Path: "/header", + HTTPMethod: "POST", + } + + resp, err := adapter.ProxyWithContext(context.Background(), req) + + Expect(err).To(BeNil()) + Expect(resp.StatusCode).To(Equal(fiber.StatusFound)) + Expect(resp.MultiValueHeaders[fiber.HeaderLocation]).To(Equal([]string{"https://github.com/gofiber/fiber"})) + Expect(resp.MultiValueHeaders[fiber.HeaderLink]).To(Equal([]string{"; rel=\"next\",; rel=\"last\""})) + Expect(resp.Body).To(Equal("")) + }) + }) + + Context("Next method", func() { + It("Check missing values in request header", func() { + app := fiber.New() + app.Post("/next", func(c *fiber.Ctx) error { + c.Next() + Expect(c.Get(fiber.HeaderHost)).To(Equal("localhost")) + Expect(c.Get(fiber.HeaderContentType)).To(Equal(fiber.MIMEApplicationJSONCharsetUTF8)) + Expect(c.Get(fiber.HeaderUserAgent)).To(Equal("fiber")) + + Expect(c.Cookies("a")).To(Equal("b")) + Expect(c.Cookies("b")).To(Equal("c")) + Expect(c.Cookies("c")).To(Equal("d")) + + Expect(c.Get(fiber.HeaderContentLength)).To(Equal("77")) + Expect(c.Get(fiber.HeaderConnection)).To(Equal("Keep-Alive")) + Expect(c.Get(fiber.HeaderKeepAlive)).To(Equal("timeout=5, max=1000")) + + return c.Status(fiber.StatusNoContent).Send(nil) + }) + adapter := fiberadapter.New(app) + + req := events.APIGatewayProxyRequest{ + Path: "/next", + HTTPMethod: "POST", + MultiValueHeaders: map[string][]string{ + fiber.HeaderHost: {"localhost"}, + fiber.HeaderContentType: {fiber.MIMEApplicationJSONCharsetUTF8}, + fiber.HeaderUserAgent: {"fiber"}, + + "cookie": {"a=b", "b=c;c=d"}, + + fiber.HeaderContentLength: {"77"}, + fiber.HeaderConnection: {"Keep-Alive"}, + fiber.HeaderKeepAlive: {"timeout=5, max=1000"}, + }, + } + + resp, err := adapter.ProxyWithContext(context.Background(), req) + + Expect(err).To(BeNil()) + Expect(resp.StatusCode).To(Equal(fiber.StatusNoContent)) + Expect(resp.Body).To(Equal("")) + }) + + It("Check missing values in response header", func() { + app := fiber.New() + app.Post("/next", func(c *fiber.Ctx) error { + c.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSONCharsetUTF8) + c.Set(fiber.HeaderServer, "localhost") + + c.Cookie(&fiber.Cookie{ + Name: "a", + Value: "b", + HTTPOnly: true, + }) + c.Cookie(&fiber.Cookie{ + Name: "b", + Value: "c", + HTTPOnly: true, + }) + c.Cookie(&fiber.Cookie{ + Name: "c", + Value: "d", + HTTPOnly: true, + }) + + c.Set(fiber.HeaderContentLength, "77") + c.Set(fiber.HeaderConnection, "keep-alive") + + c.Next() + return c.Status(fiber.StatusNoContent).Send(nil) + }) + adapter := fiberadapter.New(app) + + req := events.APIGatewayProxyRequest{ + Path: "/next", + HTTPMethod: "POST", + } + + resp, err := adapter.ProxyWithContext(context.Background(), req) + + Expect(err).To(BeNil()) + Expect(resp.StatusCode).To(Equal(fiber.StatusNoContent)) + Expect(resp.MultiValueHeaders[fiber.HeaderContentType]).To(Equal([]string{fiber.MIMEApplicationJSONCharsetUTF8})) + Expect(resp.MultiValueHeaders[fiber.HeaderServer]).To(Equal([]string{"localhost"})) + Expect(resp.MultiValueHeaders[fiber.HeaderSetCookie]).To(Equal([]string{"a=b; path=/; HttpOnly; SameSite=Lax", "b=c; path=/; HttpOnly; SameSite=Lax", "c=d; path=/; HttpOnly; SameSite=Lax"})) + Expect(resp.MultiValueHeaders[fiber.HeaderContentLength]).To(Equal([]string{"77"})) + Expect(resp.MultiValueHeaders[fiber.HeaderConnection]).To(Equal([]string{"keep-alive"})) + Expect(resp.Body).To(Equal("")) + }) + }) +}) diff --git a/core/core_suite_test.go b/core/core_suite_test.go new file mode 100644 index 0000000..ae6f001 --- /dev/null +++ b/core/core_suite_test.go @@ -0,0 +1,13 @@ +package core_test + +import ( + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func TestCore(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Core Suite") +} diff --git a/core/request.go b/core/request.go new file mode 100644 index 0000000..fd27bd0 --- /dev/null +++ b/core/request.go @@ -0,0 +1,255 @@ +// 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 ( + // CustomHostVariable is the name of the environment variable that contains + // the custom hostname for the request. If this variable is not set the framework + // reverts to `RequestContext.DomainName`. The value for a custom host should + // include a protocol: http://my-custom.host.com + CustomHostVariable = "GO_API_HOST" + + // APIGwContextHeader is the custom header key used to store the + // API Gateway context. To access the Context properties use the + // GetAPIGatewayContext method of the RequestAccessor object. + APIGwContextHeader = "X-GoLambdaProxy-ApiGw-Context" + + // APIGwStageVarsHeader is the custom header key used to store the + // API Gateway stage variables. To access the stage variable values + // use the GetAPIGatewayStageVars method of the RequestAccessor object. + APIGwStageVarsHeader = "X-GoLambdaProxy-ApiGw-StageVars" +) + +// RequestAccessor objects give access to custom API Gateway properties +// in the request. +type RequestAccessor struct { + stripBasePath string +} + +// GetAPIGatewayContext extracts the API Gateway context object from a +// request's custom header. +// Returns a populated events.APIGatewayProxyRequestContext object from +// the request. +func (r *RequestAccessor) GetAPIGatewayContext(req *http.Request) (events.APIGatewayProxyRequestContext, error) { + if req.Header.Get(APIGwContextHeader) == "" { + return events.APIGatewayProxyRequestContext{}, errors.New("No context header in request") + } + context := events.APIGatewayProxyRequestContext{} + err := json.Unmarshal([]byte(req.Header.Get(APIGwContextHeader)), &context) + if err != nil { + log.Println("Error while unmarshalling context") + log.Println(err) + return events.APIGatewayProxyRequestContext{}, err + } + return context, nil +} + +// GetAPIGatewayStageVars extracts the API Gateway stage variables from a +// request's custom header. +// Returns a map[string]string of the stage variables and their values from +// the request. +func (r *RequestAccessor) GetAPIGatewayStageVars(req *http.Request) (map[string]string, error) { + stageVars := make(map[string]string) + if req.Header.Get(APIGwStageVarsHeader) == "" { + return stageVars, errors.New("No stage vars header in request") + } + err := json.Unmarshal([]byte(req.Header.Get(APIGwStageVarsHeader)), &stageVars) + if err != nil { + log.Println("Error while unmarshalling stage variables") + log.Println(err) + return stageVars, err + } + return stageVars, 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 API Gateway is configured with +// base path mappings in custom domain names. +func (r *RequestAccessor) 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 API Gateway proxy event into a http.Request object. +// Returns the populated http request with additional two custom headers for the stage variables and API Gateway context. +// To access these properties use the GetAPIGatewayStageVars and GetAPIGatewayContext method of the RequestAccessor object. +func (r *RequestAccessor) ProxyEventToHTTPRequest(req events.APIGatewayProxyRequest) (*http.Request, error) { + httpRequest, err := r.EventToRequest(req) + if err != nil { + log.Println(err) + return nil, err + } + return addToHeader(httpRequest, req) +} + +// EventToRequestWithContext converts an API Gateway 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 GetAPIGatewayContextFromContext, GetStageVarsFromContext and GetRuntimeContextFromContext functions in this package. +func (r *RequestAccessor) EventToRequestWithContext(ctx context.Context, req events.APIGatewayProxyRequest) (*http.Request, error) { + httpRequest, err := r.EventToRequest(req) + if err != nil { + log.Println(err) + return nil, err + } + return addToContext(ctx, httpRequest, req), nil +} + +// EventToRequest converts an API Gateway proxy event into an http.Request object. +// Returns the populated request maintaining headers +func (r *RequestAccessor) EventToRequest(req events.APIGatewayProxyRequest) (*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.Path + if r.stripBasePath != "" && len(r.stripBasePath) > 1 { + if strings.HasPrefix(path, r.stripBasePath) { + path = strings.Replace(path, r.stripBasePath, "", 1) + } + } + 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.MultiValueQueryStringParameters) > 0 { + queryString := "" + for q, l := range req.MultiValueQueryStringParameters { + for _, v := range l { + if queryString != "" { + queryString += "&" + } + queryString += url.QueryEscape(q) + "=" + url.QueryEscape(v) + } + } + path += "?" + queryString + } else if len(req.QueryStringParameters) > 0 { + queryString := "" + for q := range req.QueryStringParameters { + if queryString != "" { + queryString += "&" + } + queryString += url.QueryEscape(q) + "=" + url.QueryEscape(req.QueryStringParameters[q]) + } + path += "?" + queryString + } + + httpRequest, err := http.NewRequest( + strings.ToUpper(req.HTTPMethod), + path, + bytes.NewReader(decodedBody), + ) + + if err != nil { + fmt.Printf("Could not convert request %s:%s to http.Request\n", req.HTTPMethod, req.Path) + log.Println(err) + return nil, err + } + + if req.MultiValueHeaders != nil { + for k, values := range req.MultiValueHeaders { + for _, value := range values { + httpRequest.Header.Add(k, value) + } + } + } else { + for h := range req.Headers { + httpRequest.Header.Add(h, req.Headers[h]) + } + } + + httpRequest.RequestURI = httpRequest.URL.RequestURI() + + return httpRequest, nil +} + +func addToHeader(req *http.Request, apiGwRequest events.APIGatewayProxyRequest) (*http.Request, error) { + stageVars, err := json.Marshal(apiGwRequest.StageVariables) + if err != nil { + log.Println("Could not marshal stage variables for custom header") + return nil, err + } + req.Header.Set(APIGwStageVarsHeader, string(stageVars)) + apiGwContext, err := json.Marshal(apiGwRequest.RequestContext) + if err != nil { + log.Println("Could not Marshal API GW context for custom header") + return req, err + } + req.Header.Set(APIGwContextHeader, string(apiGwContext)) + return req, nil +} + +func addToContext(ctx context.Context, req *http.Request, apiGwRequest events.APIGatewayProxyRequest) *http.Request { + lc, _ := lambdacontext.FromContext(ctx) + rc := requestContext{lambdaContext: lc, gatewayProxyContext: apiGwRequest.RequestContext, stageVars: apiGwRequest.StageVariables} + ctx = context.WithValue(ctx, ctxKey{}, rc) + return req.WithContext(ctx) +} + +// GetAPIGatewayContextFromContext retrieve APIGatewayProxyRequestContext from context.Context +func GetAPIGatewayContextFromContext(ctx context.Context) (events.APIGatewayProxyRequestContext, bool) { + v, ok := ctx.Value(ctxKey{}).(requestContext) + return v.gatewayProxyContext, ok +} + +// GetRuntimeContextFromContext retrieve Lambda Runtime Context from context.Context +func GetRuntimeContextFromContext(ctx context.Context) (*lambdacontext.LambdaContext, bool) { + v, ok := ctx.Value(ctxKey{}).(requestContext) + return v.lambdaContext, ok +} + +// GetStageVarsFromContext retrieve stage variables from context +func GetStageVarsFromContext(ctx context.Context) (map[string]string, bool) { + v, ok := ctx.Value(ctxKey{}).(requestContext) + return v.stageVars, ok +} + +type ctxKey struct{} + +type requestContext struct { + lambdaContext *lambdacontext.LambdaContext + gatewayProxyContext events.APIGatewayProxyRequestContext + stageVars map[string]string +} diff --git a/core/request_test.go b/core/request_test.go new file mode 100644 index 0000000..1fd879c --- /dev/null +++ b/core/request_test.go @@ -0,0 +1,367 @@ +package core_test + +import ( + "context" + "encoding/base64" + "io/ioutil" + "math/rand" + "os" + "strings" + + "github.com/aws/aws-lambda-go/events" + "github.com/aws/aws-lambda-go/lambdacontext" + "github.com/fumeapp/fiber/core" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("RequestAccessor tests", func() { + Context("event conversion", func() { + accessor := core.RequestAccessor{} + basicRequest := getProxyRequest("/hello", "GET") + It("Correctly converts a basic event", func() { + httpReq, err := accessor.EventToRequestWithContext(context.Background(), basicRequest) + Expect(err).To(BeNil()) + Expect("/hello").To(Equal(httpReq.URL.Path)) + Expect("/hello").To(Equal(httpReq.RequestURI)) + Expect("GET").To(Equal(httpReq.Method)) + }) + + basicRequest = getProxyRequest("/hello", "get") + It("Converts method to uppercase", func() { + // calling old method to verify reverse compatibility + httpReq, err := accessor.ProxyEventToHTTPRequest(basicRequest) + Expect(err).To(BeNil()) + Expect("/hello").To(Equal(httpReq.URL.Path)) + Expect("/hello").To(Equal(httpReq.RequestURI)) + Expect("GET").To(Equal(httpReq.Method)) + }) + + binaryBody := make([]byte, 256) + _, err := rand.Read(binaryBody) + if err != nil { + Fail("Could not generate random binary body") + } + + encodedBody := base64.StdEncoding.EncodeToString(binaryBody) + + binaryRequest := getProxyRequest("/hello", "POST") + 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").To(Equal(httpReq.RequestURI)) + Expect("POST").To(Equal(httpReq.Method)) + + bodyBytes, err := ioutil.ReadAll(httpReq.Body) + + Expect(err).To(BeNil()) + Expect(len(binaryBody)).To(Equal(len(bodyBytes))) + Expect(binaryBody).To(Equal(bodyBytes)) + }) + + mqsRequest := getProxyRequest("/hello", "GET") + mqsRequest.MultiValueQueryStringParameters = map[string][]string{ + "hello": {"1"}, + "world": {"2", "3"}, + } + mqsRequest.QueryStringParameters = map[string]string{ + "hello": "1", + "world": "2", + } + It("Populates multiple value query string correctly", func() { + httpReq, err := accessor.EventToRequestWithContext(context.Background(), mqsRequest) + Expect(err).To(BeNil()) + Expect("/hello").To(Equal(httpReq.URL.Path)) + Expect(httpReq.RequestURI).To(ContainSubstring("hello=1")) + Expect(httpReq.RequestURI).To(ContainSubstring("world=2")) + Expect(httpReq.RequestURI).To(ContainSubstring("world=3")) + 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()) + Expect(1).To(Equal(len(query["hello"]))) + Expect(2).To(Equal(len(query["world"]))) + Expect("1").To(Equal(query["hello"][0])) + Expect("2").To(Equal(query["world"][0])) + Expect("3").To(Equal(query["world"][1])) + }) + + qsRequest := getProxyRequest("/hello", "GET") + qsRequest.QueryStringParameters = map[string]string{ + "hello": "1", + "world": "2", + } + It("Populates query string correctly", func() { + httpReq, err := accessor.EventToRequestWithContext(context.Background(), qsRequest) + Expect(err).To(BeNil()) + Expect("/hello").To(Equal(httpReq.URL.Path)) + 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()) + Expect(1).To(Equal(len(query["hello"]))) + Expect(1).To(Equal(len(query["world"]))) + Expect("1").To(Equal(query["hello"][0])) + Expect("2").To(Equal(query["world"][0])) + }) + + mvhRequest := getProxyRequest("/hello", "GET") + mvhRequest.MultiValueHeaders = map[string][]string{ + "hello": {"1"}, + "world": {"2", "3"}, + } + It("Populates multiple value headers correctly", func() { + httpReq, err := accessor.EventToRequestWithContext(context.Background(), mvhRequest) + Expect(err).To(BeNil()) + Expect("/hello").To(Equal(httpReq.URL.Path)) + Expect("GET").To(Equal(httpReq.Method)) + + headers := httpReq.Header + Expect(2).To(Equal(len(headers))) + + for k, value := range headers { + Expect(value).To(Equal(mvhRequest.MultiValueHeaders[strings.ToLower(k)])) + } + }) + + svhRequest := getProxyRequest("/hello", "GET") + svhRequest.Headers = map[string]string{ + "hello": "1", + "world": "2", + } + It("Populates single value headers correctly", func() { + httpReq, err := accessor.EventToRequestWithContext(context.Background(), svhRequest) + Expect(err).To(BeNil()) + Expect("/hello").To(Equal(httpReq.URL.Path)) + Expect("GET").To(Equal(httpReq.Method)) + + headers := httpReq.Header + Expect(2).To(Equal(len(headers))) + + for k, value := range headers { + Expect(value[0]).To(Equal(svhRequest.Headers[strings.ToLower(k)])) + } + }) + + basePathRequest := getProxyRequest("/app1/orders", "GET") + + It("Stips the base path correct", func() { + accessor.StripBasePath("app1") + httpReq, err := accessor.EventToRequestWithContext(context.Background(), basePathRequest) + + Expect(err).To(BeNil()) + Expect("/orders").To(Equal(httpReq.URL.Path)) + Expect("/orders").To(Equal(httpReq.RequestURI)) + }) + + contextRequest := getProxyRequest("orders", "GET") + contextRequest.RequestContext = getRequestContext() + + It("Populates context header correctly", func() { + // calling old method to verify reverse compatibility + httpReq, err := accessor.ProxyEventToHTTPRequest(contextRequest) + Expect(err).To(BeNil()) + Expect(2).To(Equal(len(httpReq.Header))) + Expect(httpReq.Header.Get(core.APIGwContextHeader)).ToNot(BeNil()) + }) + }) + + Context("StripBasePath tests", func() { + accessor := core.RequestAccessor{} + 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)) + }) + }) + + Context("Retrieves API Gateway context", func() { + It("Returns a correctly unmarshalled object", func() { + contextRequest := getProxyRequest("orders", "GET") + contextRequest.RequestContext = getRequestContext() + + accessor := core.RequestAccessor{} + // calling old method to verify reverse compatibility + httpReq, err := accessor.ProxyEventToHTTPRequest(contextRequest) + Expect(err).To(BeNil()) + + headerContext, err := accessor.GetAPIGatewayContext(httpReq) + Expect(err).To(BeNil()) + Expect(headerContext).ToNot(BeNil()) + Expect("x").To(Equal(headerContext.AccountID)) + Expect("x").To(Equal(headerContext.RequestID)) + Expect("x").To(Equal(headerContext.APIID)) + proxyContext, ok := core.GetAPIGatewayContextFromContext(httpReq.Context()) + // should fail because using header proxy method + Expect(ok).To(BeFalse()) + + // overwrite existing context header + contextRequestWithHeaders := getProxyRequest("orders", "GET") + contextRequestWithHeaders.RequestContext = getRequestContext() + contextRequestWithHeaders.Headers = map[string]string{core.APIGwContextHeader: `{"AccountID":"abc123"}`} + httpReq, err = accessor.ProxyEventToHTTPRequest(contextRequestWithHeaders) + Expect(err).To(BeNil()) + headerContext, err = accessor.GetAPIGatewayContext(httpReq) + Expect(err).To(BeNil()) + Expect(headerContext.AccountID).To(Equal("x")) + + httpReq, err = accessor.EventToRequestWithContext(context.Background(), contextRequest) + Expect(err).To(BeNil()) + proxyContext, ok = core.GetAPIGatewayContextFromContext(httpReq.Context()) + Expect(ok).To(BeTrue()) + Expect("x").To(Equal(proxyContext.APIID)) + Expect("x").To(Equal(proxyContext.RequestID)) + Expect("x").To(Equal(proxyContext.APIID)) + Expect("prod").To(Equal(proxyContext.Stage)) + runtimeContext, ok := core.GetRuntimeContextFromContext(httpReq.Context()) + Expect(ok).To(BeTrue()) + Expect(runtimeContext).To(BeNil()) + + lambdaContext := lambdacontext.NewContext(context.Background(), &lambdacontext.LambdaContext{AwsRequestID: "abc123"}) + httpReq, err = accessor.EventToRequestWithContext(lambdaContext, contextRequest) + Expect(err).To(BeNil()) + + headerContext, err = accessor.GetAPIGatewayContext(httpReq) + // should fail as new context method doesn't populate headers + Expect(err).ToNot(BeNil()) + proxyContext, ok = core.GetAPIGatewayContextFromContext(httpReq.Context()) + Expect(ok).To(BeTrue()) + Expect("x").To(Equal(proxyContext.APIID)) + Expect("x").To(Equal(proxyContext.RequestID)) + Expect("x").To(Equal(proxyContext.APIID)) + Expect("prod").To(Equal(proxyContext.Stage)) + runtimeContext, ok = core.GetRuntimeContextFromContext(httpReq.Context()) + Expect(ok).To(BeTrue()) + Expect(runtimeContext).ToNot(BeNil()) + Expect("abc123").To(Equal(runtimeContext.AwsRequestID)) + }) + + It("Populates stage variables correctly", func() { + varsRequest := getProxyRequest("orders", "GET") + varsRequest.StageVariables = getStageVariables() + + accessor := core.RequestAccessor{} + httpReq, err := accessor.ProxyEventToHTTPRequest(varsRequest) + Expect(err).To(BeNil()) + + stageVars, err := accessor.GetAPIGatewayStageVars(httpReq) + Expect(err).To(BeNil()) + Expect(2).To(Equal(len(stageVars))) + Expect(stageVars["var1"]).ToNot(BeNil()) + Expect(stageVars["var2"]).ToNot(BeNil()) + Expect("value1").To(Equal(stageVars["var1"])) + Expect("value2").To(Equal(stageVars["var2"])) + + // overwrite existing stagevars header + varsRequestWithHeaders := getProxyRequest("orders", "GET") + varsRequestWithHeaders.StageVariables = getStageVariables() + varsRequestWithHeaders.Headers = map[string]string{core.APIGwStageVarsHeader: `{"var1":"abc123"}`} + httpReq, err = accessor.ProxyEventToHTTPRequest(varsRequestWithHeaders) + Expect(err).To(BeNil()) + stageVars, err = accessor.GetAPIGatewayStageVars(httpReq) + Expect(err).To(BeNil()) + Expect(stageVars["var1"]).To(Equal("value1")) + + stageVars, ok := core.GetStageVarsFromContext(httpReq.Context()) + // not present in context + Expect(ok).To(BeFalse()) + + httpReq, err = accessor.EventToRequestWithContext(context.Background(), varsRequest) + Expect(err).To(BeNil()) + + stageVars, err = accessor.GetAPIGatewayStageVars(httpReq) + // should not be in headers + Expect(err).ToNot(BeNil()) + + stageVars, ok = core.GetStageVarsFromContext(httpReq.Context()) + Expect(ok).To(BeTrue()) + Expect(2).To(Equal(len(stageVars))) + Expect(stageVars["var1"]).ToNot(BeNil()) + Expect(stageVars["var2"]).ToNot(BeNil()) + Expect("value1").To(Equal(stageVars["var1"])) + Expect("value2").To(Equal(stageVars["var2"])) + }) + + It("Populates the default hostname correctly", func() { + + basicRequest := getProxyRequest("orders", "GET") + basicRequest.RequestContext = getRequestContext() + accessor := core.RequestAccessor{} + httpReq, err := accessor.ProxyEventToHTTPRequest(basicRequest) + Expect(err).To(BeNil()) + + Expect(basicRequest.RequestContext.DomainName).To(Equal(httpReq.Host)) + Expect(basicRequest.RequestContext.DomainName).To(Equal(httpReq.URL.Host)) + }) + + It("Uses a custom hostname", func() { + myCustomHost := "http://my-custom-host.com" + os.Setenv(core.CustomHostVariable, myCustomHost) + basicRequest := getProxyRequest("orders", "GET") + accessor := core.RequestAccessor{} + httpReq, err := accessor.EventToRequestWithContext(context.Background(), basicRequest) + Expect(err).To(BeNil()) + + Expect(myCustomHost).To(Equal("http://" + httpReq.Host)) + Expect(myCustomHost).To(Equal("http://" + httpReq.URL.Host)) + os.Unsetenv(core.CustomHostVariable) + }) + + It("Strips terminating / from hostname", func() { + myCustomHost := "http://my-custom-host.com" + os.Setenv(core.CustomHostVariable, myCustomHost+"/") + basicRequest := getProxyRequest("orders", "GET") + accessor := core.RequestAccessor{} + httpReq, err := accessor.EventToRequestWithContext(context.Background(), basicRequest) + Expect(err).To(BeNil()) + + Expect(myCustomHost).To(Equal("http://" + httpReq.Host)) + Expect(myCustomHost).To(Equal("http://" + httpReq.URL.Host)) + os.Unsetenv(core.CustomHostVariable) + }) + }) +}) + +func getProxyRequest(path string, method string) events.APIGatewayProxyRequest { + return events.APIGatewayProxyRequest{ + Path: path, + HTTPMethod: method, + } +} + +func getRequestContext() events.APIGatewayProxyRequestContext { + return events.APIGatewayProxyRequestContext{ + AccountID: "x", + RequestID: "x", + APIID: "x", + Stage: "prod", + DomainName: "12abcdefgh.execute-api.us-east-2.amazonaws.com", + } +} + +func getStageVariables() map[string]string { + return map[string]string{ + "var1": "value1", + "var2": "value2", + } +} diff --git a/core/requestv2.go b/core/requestv2.go new file mode 100644 index 0000000..e3c8d56 --- /dev/null +++ b/core/requestv2.go @@ -0,0 +1,227 @@ +// 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" +) + +// RequestAccessorV2 objects give access to custom API Gateway properties +// in the request. +type RequestAccessorV2 struct { + stripBasePath string +} + +// GetAPIGatewayContextV2 extracts the API Gateway context object from a +// request's custom header. +// Returns a populated events.APIGatewayProxyRequestContext object from +// the request. +func (r *RequestAccessorV2) GetAPIGatewayContextV2(req *http.Request) (events.APIGatewayV2HTTPRequestContext, error) { + if req.Header.Get(APIGwContextHeader) == "" { + return events.APIGatewayV2HTTPRequestContext{}, errors.New("No context header in request") + } + context := events.APIGatewayV2HTTPRequestContext{} + err := json.Unmarshal([]byte(req.Header.Get(APIGwContextHeader)), &context) + if err != nil { + log.Println("Erorr while unmarshalling context") + log.Println(err) + return events.APIGatewayV2HTTPRequestContext{}, err + } + return context, nil +} + +// GetAPIGatewayStageVars extracts the API Gateway stage variables from a +// request's custom header. +// Returns a map[string]string of the stage variables and their values from +// the request. +func (r *RequestAccessorV2) GetAPIGatewayStageVars(req *http.Request) (map[string]string, error) { + stageVars := make(map[string]string) + if req.Header.Get(APIGwStageVarsHeader) == "" { + return stageVars, errors.New("No stage vars header in request") + } + err := json.Unmarshal([]byte(req.Header.Get(APIGwStageVarsHeader)), &stageVars) + if err != nil { + log.Println("Erorr while unmarshalling stage variables") + log.Println(err) + return stageVars, err + } + return stageVars, 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 API Gateway is configured with +// base path mappings in custom domain names. +func (r *RequestAccessorV2) 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 API Gateway proxy event into a http.Request object. +// Returns the populated http request with additional two custom headers for the stage variables and API Gateway context. +// To access these properties use the GetAPIGatewayStageVars and GetAPIGatewayContext method of the RequestAccessor object. +func (r *RequestAccessorV2) ProxyEventToHTTPRequest(req events.APIGatewayV2HTTPRequest) (*http.Request, error) { + httpRequest, err := r.EventToRequest(req) + if err != nil { + log.Println(err) + return nil, err + } + return addToHeaderV2(httpRequest, req) +} + +// EventToRequestWithContext converts an API Gateway 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 GetAPIGatewayContextFromContext, GetStageVarsFromContext and GetRuntimeContextFromContext functions in this package. +func (r *RequestAccessorV2) EventToRequestWithContext(ctx context.Context, req events.APIGatewayV2HTTPRequest) (*http.Request, error) { + httpRequest, err := r.EventToRequest(req) + if err != nil { + log.Println(err) + return nil, err + } + return addToContextV2(ctx, httpRequest, req), nil +} + +// EventToRequest converts an API Gateway proxy event into an http.Request object. +// Returns the populated request maintaining headers +func (r *RequestAccessorV2) EventToRequest(req events.APIGatewayV2HTTPRequest) (*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) + } + } + 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 + } + + 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 addToHeaderV2(req *http.Request, apiGwRequest events.APIGatewayV2HTTPRequest) (*http.Request, error) { + stageVars, err := json.Marshal(apiGwRequest.StageVariables) + if err != nil { + log.Println("Could not marshal stage variables for custom header") + return nil, err + } + req.Header.Add(APIGwStageVarsHeader, string(stageVars)) + apiGwContext, err := json.Marshal(apiGwRequest.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 addToContextV2(ctx context.Context, req *http.Request, apiGwRequest events.APIGatewayV2HTTPRequest) *http.Request { + lc, _ := lambdacontext.FromContext(ctx) + rc := requestContextV2{lambdaContext: lc, gatewayProxyContext: apiGwRequest.RequestContext, stageVars: apiGwRequest.StageVariables} + ctx = context.WithValue(ctx, ctxKey{}, rc) + return req.WithContext(ctx) +} + +// GetAPIGatewayV2ContextFromContext retrieve APIGatewayProxyRequestContext from context.Context +func GetAPIGatewayV2ContextFromContext(ctx context.Context) (events.APIGatewayV2HTTPRequestContext, bool) { + v, ok := ctx.Value(ctxKey{}).(requestContextV2) + return v.gatewayProxyContext, ok +} + +// GetRuntimeContextFromContextV2 retrieve Lambda Runtime Context from context.Context +func GetRuntimeContextFromContextV2(ctx context.Context) (*lambdacontext.LambdaContext, bool) { + v, ok := ctx.Value(ctxKey{}).(requestContextV2) + return v.lambdaContext, ok +} + +// GetStageVarsFromContextV2 retrieve stage variables from context +func GetStageVarsFromContextV2(ctx context.Context) (map[string]string, bool) { + v, ok := ctx.Value(ctxKey{}).(requestContextV2) + return v.stageVars, ok +} + +type requestContextV2 struct { + lambdaContext *lambdacontext.LambdaContext + gatewayProxyContext events.APIGatewayV2HTTPRequestContext + stageVars map[string]string +} diff --git a/core/requestv2_test.go b/core/requestv2_test.go new file mode 100644 index 0000000..8cc94a0 --- /dev/null +++ b/core/requestv2_test.go @@ -0,0 +1,357 @@ +package core_test + +import ( + "context" + "encoding/base64" + "github.com/onsi/gomega/gstruct" + "io/ioutil" + "math/rand" + "os" + "strings" + + "github.com/aws/aws-lambda-go/events" + "github.com/aws/aws-lambda-go/lambdacontext" + "github.com/fumeapp/fiber/core" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("RequestAccessorV2 tests", func() { + Context("event conversion", func() { + accessor := core.RequestAccessorV2{} + basicRequest := getProxyRequestV2("/hello", "GET") + It("Correctly converts a basic event", func() { + httpReq, err := accessor.EventToRequestWithContext(context.Background(), basicRequest) + Expect(err).To(BeNil()) + Expect("/hello").To(Equal(httpReq.URL.Path)) + Expect("/hello").To(Equal(httpReq.RequestURI)) + Expect("GET").To(Equal(httpReq.Method)) + }) + + basicRequest = getProxyRequestV2("/hello", "get") + It("Converts method to uppercase", func() { + // calling old method to verify reverse compatibility + httpReq, err := accessor.ProxyEventToHTTPRequest(basicRequest) + Expect(err).To(BeNil()) + Expect("/hello").To(Equal(httpReq.URL.Path)) + Expect("/hello").To(Equal(httpReq.RequestURI)) + Expect("GET").To(Equal(httpReq.Method)) + }) + + binaryBody := make([]byte, 256) + _, err := rand.Read(binaryBody) + if err != nil { + Fail("Could not generate random binary body") + } + + encodedBody := base64.StdEncoding.EncodeToString(binaryBody) + + binaryRequest := getProxyRequestV2("/hello", "POST") + 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").To(Equal(httpReq.RequestURI)) + Expect("POST").To(Equal(httpReq.Method)) + + bodyBytes, err := ioutil.ReadAll(httpReq.Body) + + Expect(err).To(BeNil()) + Expect(len(binaryBody)).To(Equal(len(bodyBytes))) + Expect(binaryBody).To(Equal(bodyBytes)) + }) + + mqsRequest := getProxyRequestV2("/hello", "GET") + mqsRequest.RawQueryString = "hello=1&world=2&world=3" + mqsRequest.QueryStringParameters = map[string]string{ + "hello": "1", + "world": "2", + } + It("Populates multiple value query string correctly", func() { + httpReq, err := accessor.EventToRequestWithContext(context.Background(), mqsRequest) + Expect(err).To(BeNil()) + Expect("/hello").To(Equal(httpReq.URL.Path)) + Expect(httpReq.RequestURI).To(ContainSubstring("hello=1")) + Expect(httpReq.RequestURI).To(ContainSubstring("world=2")) + Expect(httpReq.RequestURI).To(ContainSubstring("world=3")) + 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()) + Expect(1).To(Equal(len(query["hello"]))) + Expect(2).To(Equal(len(query["world"]))) + Expect("1").To(Equal(query["hello"][0])) + Expect("2").To(Equal(query["world"][0])) + Expect("3").To(Equal(query["world"][1])) + }) + + qsRequest := getProxyRequestV2("/hello", "GET") + qsRequest.QueryStringParameters = map[string]string{ + "hello": "1", + "world": "2", + } + It("Populates query string correctly", func() { + httpReq, err := accessor.EventToRequestWithContext(context.Background(), qsRequest) + Expect(err).To(BeNil()) + Expect("/hello").To(Equal(httpReq.URL.Path)) + 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()) + Expect(1).To(Equal(len(query["hello"]))) + Expect(1).To(Equal(len(query["world"]))) + Expect("1").To(Equal(query["hello"][0])) + Expect("2").To(Equal(query["world"][0])) + }) + + mvhRequest := getProxyRequestV2("/hello", "GET") + mvhRequest.Headers = map[string]string{ + "hello": "1", + "world": "2,3", + } + + It("Populates multiple value headers correctly", func() { + httpReq, err := accessor.EventToRequestWithContext(context.Background(), mvhRequest) + Expect(err).To(BeNil()) + Expect("/hello").To(Equal(httpReq.URL.Path)) + Expect("GET").To(Equal(httpReq.Method)) + + headers := httpReq.Header + Expect(2).To(Equal(len(headers))) + + for k, value := range headers { + Expect(strings.Join(value, ",")).To(Equal(mvhRequest.Headers[strings.ToLower(k)])) + } + }) + + svhRequest := getProxyRequestV2("/hello", "GET") + svhRequest.Headers = map[string]string{ + "hello": "1", + "world": "2", + } + It("Populates single value headers correctly", func() { + httpReq, err := accessor.EventToRequestWithContext(context.Background(), svhRequest) + Expect(err).To(BeNil()) + Expect("/hello").To(Equal(httpReq.URL.Path)) + Expect("GET").To(Equal(httpReq.Method)) + + headers := httpReq.Header + Expect(2).To(Equal(len(headers))) + + for k, value := range headers { + Expect(value[0]).To(Equal(svhRequest.Headers[strings.ToLower(k)])) + } + }) + + basePathRequest := getProxyRequestV2("/app1/orders", "GET") + + It("Stips the base path correct", func() { + accessor.StripBasePath("app1") + httpReq, err := accessor.EventToRequestWithContext(context.Background(), basePathRequest) + + Expect(err).To(BeNil()) + Expect("/orders").To(Equal(httpReq.URL.Path)) + Expect("/orders").To(Equal(httpReq.RequestURI)) + }) + + contextRequest := getProxyRequestV2("orders", "GET") + contextRequest.RequestContext = getRequestContextV2() + + It("Populates context header correctly", func() { + // calling old method to verify reverse compatibility + httpReq, err := accessor.ProxyEventToHTTPRequest(contextRequest) + Expect(err).To(BeNil()) + Expect(2).To(Equal(len(httpReq.Header))) + Expect(httpReq.Header.Get(core.APIGwContextHeader)).ToNot(BeNil()) + }) + }) + + Context("StripBasePath tests", func() { + accessor := core.RequestAccessorV2{} + 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)) + }) + }) + + Context("Retrieves API Gateway context", func() { + It("Returns a correctly unmarshalled object", func() { + contextRequest := getProxyRequestV2("orders", "GET") + contextRequest.RequestContext = getRequestContextV2() + + accessor := core.RequestAccessorV2{} + // calling old method to verify reverse compatibility + httpReq, err := accessor.ProxyEventToHTTPRequest(contextRequest) + Expect(err).To(BeNil()) + + headerContext, err := accessor.GetAPIGatewayContextV2(httpReq) + Expect(err).To(BeNil()) + Expect(headerContext).ToNot(BeNil()) + Expect("x").To(Equal(headerContext.AccountID)) + Expect("x").To(Equal(headerContext.RequestID)) + Expect("x").To(Equal(headerContext.APIID)) + proxyContext, ok := core.GetAPIGatewayV2ContextFromContext(httpReq.Context()) + // should fail because using header proxy method + Expect(ok).To(BeFalse()) + + httpReq, err = accessor.EventToRequestWithContext(context.Background(), contextRequest) + Expect(err).To(BeNil()) + proxyContext, ok = core.GetAPIGatewayV2ContextFromContext(httpReq.Context()) + Expect(ok).To(BeTrue()) + Expect("x").To(Equal(proxyContext.APIID)) + Expect("x").To(Equal(proxyContext.RequestID)) + Expect("x").To(Equal(proxyContext.APIID)) + Expect("prod").To(Equal(proxyContext.Stage)) + runtimeContext, ok := core.GetRuntimeContextFromContextV2(httpReq.Context()) + Expect(ok).To(BeTrue()) + Expect(runtimeContext).To(BeNil()) + + lambdaContext := lambdacontext.NewContext(context.Background(), &lambdacontext.LambdaContext{AwsRequestID: "abc123"}) + httpReq, err = accessor.EventToRequestWithContext(lambdaContext, contextRequest) + Expect(err).To(BeNil()) + + headerContext, err = accessor.GetAPIGatewayContextV2(httpReq) + // should fail as new context method doesn't populate headers + Expect(err).ToNot(BeNil()) + proxyContext, ok = core.GetAPIGatewayV2ContextFromContext(httpReq.Context()) + Expect(ok).To(BeTrue()) + Expect("x").To(Equal(proxyContext.APIID)) + Expect("x").To(Equal(proxyContext.RequestID)) + Expect("x").To(Equal(proxyContext.APIID)) + Expect("prod").To(Equal(proxyContext.Stage)) + runtimeContext, ok = core.GetRuntimeContextFromContextV2(httpReq.Context()) + Expect(ok).To(BeTrue()) + Expect(runtimeContext).ToNot(BeNil()) + Expect("abc123").To(Equal(runtimeContext.AwsRequestID)) + }) + + It("Populates stage variables correctly", func() { + varsRequest := getProxyRequestV2("orders", "GET") + varsRequest.StageVariables = getStageVariables() + + accessor := core.RequestAccessorV2{} + httpReq, err := accessor.ProxyEventToHTTPRequest(varsRequest) + Expect(err).To(BeNil()) + + stageVars, err := accessor.GetAPIGatewayStageVars(httpReq) + Expect(err).To(BeNil()) + Expect(2).To(Equal(len(stageVars))) + Expect(stageVars["var1"]).ToNot(BeNil()) + Expect(stageVars["var2"]).ToNot(BeNil()) + Expect("value1").To(Equal(stageVars["var1"])) + Expect("value2").To(Equal(stageVars["var2"])) + + stageVars, ok := core.GetStageVarsFromContextV2(httpReq.Context()) + // not present in context + Expect(ok).To(BeFalse()) + + httpReq, err = accessor.EventToRequestWithContext(context.Background(), varsRequest) + Expect(err).To(BeNil()) + + stageVars, err = accessor.GetAPIGatewayStageVars(httpReq) + // should not be in headers + Expect(err).ToNot(BeNil()) + + stageVars, ok = core.GetStageVarsFromContextV2(httpReq.Context()) + Expect(ok).To(BeTrue()) + Expect(2).To(Equal(len(stageVars))) + Expect(stageVars["var1"]).ToNot(BeNil()) + Expect(stageVars["var2"]).ToNot(BeNil()) + Expect("value1").To(Equal(stageVars["var1"])) + Expect("value2").To(Equal(stageVars["var2"])) + }) + + It("Populates the default hostname correctly", func() { + + basicRequest := getProxyRequestV2("orders", "GET") + basicRequest.RequestContext = getRequestContextV2() + accessor := core.RequestAccessorV2{} + httpReq, err := accessor.ProxyEventToHTTPRequest(basicRequest) + Expect(err).To(BeNil()) + + Expect(basicRequest.RequestContext.DomainName).To(Equal(httpReq.Host)) + Expect(basicRequest.RequestContext.DomainName).To(Equal(httpReq.URL.Host)) + }) + + It("Uses a custom hostname", func() { + myCustomHost := "http://my-custom-host.com" + os.Setenv(core.CustomHostVariable, myCustomHost) + basicRequest := getProxyRequestV2("orders", "GET") + accessor := core.RequestAccessorV2{} + httpReq, err := accessor.EventToRequestWithContext(context.Background(), basicRequest) + Expect(err).To(BeNil()) + + Expect(myCustomHost).To(Equal("http://" + httpReq.Host)) + Expect(myCustomHost).To(Equal("http://" + httpReq.URL.Host)) + os.Unsetenv(core.CustomHostVariable) + }) + + It("Strips terminating / from hostname", func() { + myCustomHost := "http://my-custom-host.com" + os.Setenv(core.CustomHostVariable, myCustomHost+"/") + basicRequest := getProxyRequestV2("orders", "GET") + accessor := core.RequestAccessorV2{} + httpReq, err := accessor.EventToRequestWithContext(context.Background(), basicRequest) + Expect(err).To(BeNil()) + + Expect(myCustomHost).To(Equal("http://" + httpReq.Host)) + Expect(myCustomHost).To(Equal("http://" + httpReq.URL.Host)) + os.Unsetenv(core.CustomHostVariable) + }) + + It("handles cookies okay", func() { + basicRequest := getProxyRequestV2("orders", "GET") + basicRequest.Cookies = []string{ + "TestCookie=123", + } + accessor := core.RequestAccessorV2{} + httpReq, err := accessor.EventToRequestWithContext(context.Background(), basicRequest) + Expect(err).To(BeNil()) + Expect(httpReq.Cookie("TestCookie")).To(gstruct.PointTo(gstruct.MatchFields(gstruct.IgnoreExtras, gstruct.Fields{ + "Value": Equal("123"), + }))) + }) + }) +}) + +func getProxyRequestV2(path string, method string) events.APIGatewayV2HTTPRequest { + return events.APIGatewayV2HTTPRequest{ + RequestContext: events.APIGatewayV2HTTPRequestContext{ + HTTP: events.APIGatewayV2HTTPRequestContextHTTPDescription{ + Path: path, + Method: method, + }, + }, + RawPath: path, + } +} + +func getRequestContextV2() events.APIGatewayV2HTTPRequestContext { + return events.APIGatewayV2HTTPRequestContext{ + AccountID: "x", + RequestID: "x", + APIID: "x", + Stage: "prod", + DomainName: "12abcdefgh.execute-api.us-east-2.amazonaws.com", + } +} diff --git a/core/response.go b/core/response.go new file mode 100644 index 0000000..c8f3acf --- /dev/null +++ b/core/response.go @@ -0,0 +1,114 @@ +// 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" + "unicode/utf8" + + "github.com/aws/aws-lambda-go/events" +) + +const ( + defaultStatusCode = -1 + contentTypeHeaderKey = "Content-Type" +) + +// ProxyResponseWriter implements http.ResponseWriter and adds the method +// necessary to return an events.APIGatewayProxyResponse object +type ProxyResponseWriter struct { + headers http.Header + body bytes.Buffer + status int + observers []chan<- bool +} + +// NewProxyResponseWriter returns a new ProxyResponseWriter object. +// The object is initialized with an empty map of headers and a +// status code of -1 +func NewProxyResponseWriter() *ProxyResponseWriter { + return &ProxyResponseWriter{ + headers: make(http.Header), + status: defaultStatusCode, + observers: make([]chan<- bool, 0), + } + +} + +func (r *ProxyResponseWriter) CloseNotify() <-chan bool { + ch := make(chan bool, 1) + + r.observers = append(r.observers, ch) + + return ch +} + +func (r *ProxyResponseWriter) notifyClosed() { + for _, v := range r.observers { + v <- true + } +} + +// Header implementation from the http.ResponseWriter interface. +func (r *ProxyResponseWriter) 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 *ProxyResponseWriter) 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 *ProxyResponseWriter) 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 *ProxyResponseWriter) GetProxyResponse() (events.APIGatewayProxyResponse, error) { + r.notifyClosed() + + if r.status == defaultStatusCode { + return events.APIGatewayProxyResponse{}, 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 + } + + return events.APIGatewayProxyResponse{ + StatusCode: r.status, + MultiValueHeaders: http.Header(r.headers), + Body: output, + IsBase64Encoded: isBase64, + }, nil +} diff --git a/core/response_test.go b/core/response_test.go new file mode 100644 index 0000000..b6f50a2 --- /dev/null +++ b/core/response_test.go @@ -0,0 +1,180 @@ +package core + +import ( + "encoding/base64" + "math/rand" + "net/http" + "strings" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("ResponseWriter tests", func() { + Context("writing to response object", func() { + response := NewProxyResponseWriter() + + 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 := NewProxyResponseWriter() + 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.MultiValueHeaders))) + Expect("application/json").To(Equal(proxyResp.MultiValueHeaders["Content-Type"][0])) + Expect(xmlBodyContent).To(Equal(proxyResp.Body)) + }) + + It("Sets the content type to text/xml given the body", func() { + resp := NewProxyResponseWriter() + 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.MultiValueHeaders))) + Expect(true).To(Equal(strings.HasPrefix(proxyResp.MultiValueHeaders["Content-Type"][0], "text/xml;"))) + Expect(xmlBodyContent).To(Equal(proxyResp.Body)) + }) + + It("Sets the content type to text/html given the body", func() { + resp := NewProxyResponseWriter() + 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.MultiValueHeaders))) + Expect(true).To(Equal(strings.HasPrefix(proxyResp.MultiValueHeaders["Content-Type"][0], "text/html;"))) + Expect(htmlBodyContent).To(Equal(proxyResp.Body)) + }) + }) + + Context("Export API Gateway proxy response", func() { + emtpyResponse := NewProxyResponseWriter() + emtpyResponse.Header().Add("Content-Type", "application/json") + + It("Refuses empty responses with default status code", func() { + _, err := emtpyResponse.GetProxyResponse() + Expect(err).ToNot(BeNil()) + Expect("Status code not set on response").To(Equal(err.Error())) + }) + + simpleResponse := NewProxyResponseWriter() + simpleResponse.Write([]byte("hello")) + simpleResponse.Header().Add("Content-Type", "text/plain") + It("Writes text body correctly", func() { + proxyResponse, err := simpleResponse.GetProxyResponse() + Expect(err).To(BeNil()) + Expect(proxyResponse).ToNot(BeNil()) + + Expect("hello").To(Equal(proxyResponse.Body)) + Expect(http.StatusOK).To(Equal(proxyResponse.StatusCode)) + Expect(1).To(Equal(len(proxyResponse.MultiValueHeaders))) + Expect(true).To(Equal(strings.HasPrefix(proxyResponse.MultiValueHeaders["Content-Type"][0], "text/plain"))) + Expect(proxyResponse.IsBase64Encoded).To(BeFalse()) + }) + + binaryResponse := NewProxyResponseWriter() + binaryResponse.Header().Add("Content-Type", "application/octet-stream") + binaryBody := make([]byte, 256) + _, err := rand.Read(binaryBody) + if err != nil { + Fail("Could not generate random binary body") + } + binaryResponse.Write(binaryBody) + binaryResponse.WriteHeader(http.StatusAccepted) + + It("Encodes binary responses correctly", func() { + proxyResponse, err := binaryResponse.GetProxyResponse() + Expect(err).To(BeNil()) + Expect(proxyResponse).ToNot(BeNil()) + + Expect(proxyResponse.IsBase64Encoded).To(BeTrue()) + Expect(base64.StdEncoding.EncodedLen(len(binaryBody))).To(Equal(len(proxyResponse.Body))) + + Expect(base64.StdEncoding.EncodeToString(binaryBody)).To(Equal(proxyResponse.Body)) + Expect(1).To(Equal(len(proxyResponse.MultiValueHeaders))) + Expect("application/octet-stream").To(Equal(proxyResponse.MultiValueHeaders["Content-Type"][0])) + Expect(http.StatusAccepted).To(Equal(proxyResponse.StatusCode)) + }) + }) + + Context("Handle multi-value headers", func() { + + It("Writes single-value headers correctly", func() { + response := NewProxyResponseWriter() + response.Header().Add("Content-Type", "application/json") + response.Write([]byte("hello")) + proxyResponse, err := response.GetProxyResponse() + Expect(err).To(BeNil()) + + // Headers are not also written to `Headers` field + Expect(0).To(Equal(len(proxyResponse.Headers))) + Expect(1).To(Equal(len(proxyResponse.MultiValueHeaders["Content-Type"]))) + Expect("application/json").To(Equal(proxyResponse.MultiValueHeaders["Content-Type"][0])) + }) + + It("Writes multi-value headers correctly", func() { + response := NewProxyResponseWriter() + response.Header().Add("Set-Cookie", "csrftoken=foobar") + response.Header().Add("Set-Cookie", "session_id=barfoo") + response.Write([]byte("hello")) + proxyResponse, err := response.GetProxyResponse() + Expect(err).To(BeNil()) + + // Headers are not also written to `Headers` field + Expect(0).To(Equal(len(proxyResponse.Headers))) + + // There are two headers here because Content-Type is always written implicitly + Expect(2).To(Equal(len(proxyResponse.MultiValueHeaders["Set-Cookie"]))) + Expect("csrftoken=foobar").To(Equal(proxyResponse.MultiValueHeaders["Set-Cookie"][0])) + Expect("session_id=barfoo").To(Equal(proxyResponse.MultiValueHeaders["Set-Cookie"][1])) + }) + }) + +}) diff --git a/core/responsev2.go b/core/responsev2.go new file mode 100644 index 0000000..8771845 --- /dev/null +++ b/core/responsev2.go @@ -0,0 +1,122 @@ +// 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" +) + +// ProxyResponseWriterV2 implements http.ResponseWriter and adds the method +// necessary to return an events.APIGatewayProxyResponse object +type ProxyResponseWriterV2 struct { + headers http.Header + body bytes.Buffer + status int + observers []chan<- bool +} + +// NewProxyResponseWriter returns a new ProxyResponseWriter object. +// The object is initialized with an empty map of headers and a +// status code of -1 +func NewProxyResponseWriterV2() *ProxyResponseWriterV2 { + return &ProxyResponseWriterV2{ + headers: make(http.Header), + status: defaultStatusCode, + observers: make([]chan<- bool, 0), + } + +} + +func (r *ProxyResponseWriterV2) CloseNotify() <-chan bool { + ch := make(chan bool, 1) + + r.observers = append(r.observers, ch) + + return ch +} + +func (r *ProxyResponseWriterV2) notifyClosed() { + for _, v := range r.observers { + v <- true + } +} + +// Header implementation from the http.ResponseWriter interface. +func (r *ProxyResponseWriterV2) 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 *ProxyResponseWriterV2) 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 *ProxyResponseWriterV2) 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 *ProxyResponseWriterV2) GetProxyResponse() (events.APIGatewayV2HTTPResponse, error) { + r.notifyClosed() + + if r.status == defaultStatusCode { + return events.APIGatewayV2HTTPResponse{}, 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.APIGatewayV2HTTPResponse{ + StatusCode: r.status, + Headers: headers, + Body: output, + IsBase64Encoded: isBase64, + Cookies: cookies, + }, nil +} diff --git a/core/responsev2_test.go b/core/responsev2_test.go new file mode 100644 index 0000000..df8fee1 --- /dev/null +++ b/core/responsev2_test.go @@ -0,0 +1,185 @@ +package core + +import ( + "encoding/base64" + "math/rand" + "net/http" + "strings" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("ResponseWriterV2 tests", func() { + Context("writing to response object", func() { + response := NewProxyResponseWriterV2() + + 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 := NewProxyResponseWriterV2() + 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 := NewProxyResponseWriterV2() + 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 := NewProxyResponseWriterV2() + 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 API Gateway proxy response", func() { + emtpyResponse := NewProxyResponseWriterV2() + emtpyResponse.Header().Add("Content-Type", "application/json") + + It("Refuses empty responses with default status code", func() { + _, err := emtpyResponse.GetProxyResponse() + Expect(err).ToNot(BeNil()) + Expect("Status code not set on response").To(Equal(err.Error())) + }) + + simpleResponse := NewProxyResponseWriterV2() + simpleResponse.Write([]byte("hello")) + simpleResponse.Header().Add("Content-Type", "text/plain") + It("Writes text body correctly", func() { + proxyResponse, err := simpleResponse.GetProxyResponse() + Expect(err).To(BeNil()) + Expect(proxyResponse).ToNot(BeNil()) + + Expect("hello").To(Equal(proxyResponse.Body)) + Expect(http.StatusOK).To(Equal(proxyResponse.StatusCode)) + Expect(1).To(Equal(len(proxyResponse.Headers))) + Expect(true).To(Equal(strings.HasPrefix(proxyResponse.Headers["Content-Type"], "text/plain"))) + Expect(proxyResponse.IsBase64Encoded).To(BeFalse()) + }) + + binaryResponse := NewProxyResponseWriterV2() + binaryResponse.Header().Add("Content-Type", "application/octet-stream") + binaryBody := make([]byte, 256) + _, err := rand.Read(binaryBody) + if err != nil { + Fail("Could not generate random binary body") + } + binaryResponse.Write(binaryBody) + binaryResponse.WriteHeader(http.StatusAccepted) + + It("Encodes binary responses correctly", func() { + proxyResponse, err := binaryResponse.GetProxyResponse() + Expect(err).To(BeNil()) + Expect(proxyResponse).ToNot(BeNil()) + + Expect(proxyResponse.IsBase64Encoded).To(BeTrue()) + Expect(base64.StdEncoding.EncodedLen(len(binaryBody))).To(Equal(len(proxyResponse.Body))) + + Expect(base64.StdEncoding.EncodeToString(binaryBody)).To(Equal(proxyResponse.Body)) + Expect(1).To(Equal(len(proxyResponse.Headers))) + Expect("application/octet-stream").To(Equal(proxyResponse.Headers["Content-Type"])) + Expect(http.StatusAccepted).To(Equal(proxyResponse.StatusCode)) + }) + }) + + Context("Handle multi-value headers", func() { + + It("Writes single-value headers correctly", func() { + response := NewProxyResponseWriterV2() + response.Header().Add("Content-Type", "application/json") + response.Write([]byte("hello")) + proxyResponse, err := response.GetProxyResponse() + Expect(err).To(BeNil()) + + Expect(1).To(Equal(len(proxyResponse.Headers))) + Expect("application/json").To(Equal(proxyResponse.Headers["Content-Type"])) + }) + + It("Writes multi-value headers correctly", func() { + response := NewProxyResponseWriterV2() + response.Header().Add("Accepts", "foobar") + response.Header().Add("Accepts", "barfoo") + response.Write([]byte("hello")) + proxyResponse, err := response.GetProxyResponse() + Expect(err).To(BeNil()) + + Expect(2).To(Equal(len(proxyResponse.Headers))) + Expect("foobar,barfoo").To(Equal(proxyResponse.Headers["Accepts"])) + }) + + It("Writes cookies correctly", func() { + response := NewProxyResponseWriterV2() + response.Header().Add("Set-Cookie", "csrftoken=foobar") + response.Header().Add("Set-Cookie", "session_id=barfoo") + response.Write([]byte("hello")) + proxyResponse, err := response.GetProxyResponse() + Expect(err).To(BeNil()) + + Expect(2).To(Equal(len(proxyResponse.Cookies))) + Expect(strings.Split("csrftoken=foobar,session_id=barfoo", ",")).To(Equal(proxyResponse.Cookies)) + }) + }) + +}) diff --git a/core/switchablerequest.go b/core/switchablerequest.go new file mode 100644 index 0000000..0cc0aec --- /dev/null +++ b/core/switchablerequest.go @@ -0,0 +1,72 @@ +package core + +import ( + "encoding/json" + "errors" + "github.com/aws/aws-lambda-go/events" +) + +type SwitchableAPIGatewayRequest struct { + v interface{} // v is Always nil, or a pointer of APIGatewayProxyRequest or APIGatewayV2HTTPRequest +} + +// NewSwitchableAPIGatewayRequestV1 creates a new SwitchableAPIGatewayRequest from APIGatewayProxyRequest +func NewSwitchableAPIGatewayRequestV1(v *events.APIGatewayProxyRequest) *SwitchableAPIGatewayRequest { + return &SwitchableAPIGatewayRequest{ + v: v, + } +} +// NewSwitchableAPIGatewayRequestV2 creates a new SwitchableAPIGatewayRequest from APIGatewayV2HTTPRequest +func NewSwitchableAPIGatewayRequestV2(v *events.APIGatewayV2HTTPRequest) *SwitchableAPIGatewayRequest { + return &SwitchableAPIGatewayRequest{ + v: v, + } +} + +// MarshalJSON is a pass through serialization +func (s *SwitchableAPIGatewayRequest) MarshalJSON() ([]byte, error) { + return json.Marshal(s.v) +} + +// UnmarshalJSON is a switching serialization based on the presence of fields in the +// source JSON, multiValueQueryStringParameters for APIGatewayProxyRequest and rawQueryString for +// APIGatewayV2HTTPRequest. +func (s *SwitchableAPIGatewayRequest) UnmarshalJSON(b []byte) error { + delta := map[string]json.RawMessage{} + if err := json.Unmarshal(b, &delta); err != nil { + return err + } + _, v1test := delta["multiValueQueryStringParameters"] + _, v2test := delta["rawQueryString"] + s.v = nil + if v1test && !v2test { + s.v = &events.APIGatewayProxyRequest{} + } else if !v1test && v2test { + s.v = &events.APIGatewayV2HTTPRequest{} + } else { + return errors.New("unable to determine request version") + } + return json.Unmarshal(b, s.v) +} + +// Version1 returns the contained events.APIGatewayProxyRequest or nil +func (s *SwitchableAPIGatewayRequest) Version1() *events.APIGatewayProxyRequest { + switch v := s.v.(type) { + case *events.APIGatewayProxyRequest: + return v + case events.APIGatewayProxyRequest: + return &v + } + return nil +} + +// Version2 returns the contained events.APIGatewayV2HTTPRequest or nil +func (s *SwitchableAPIGatewayRequest) Version2() *events.APIGatewayV2HTTPRequest { + switch v := s.v.(type) { + case *events.APIGatewayV2HTTPRequest: + return v + case events.APIGatewayV2HTTPRequest: + return &v + } + return nil +} diff --git a/core/switchablerequest_test.go b/core/switchablerequest_test.go new file mode 100644 index 0000000..2c2a54d --- /dev/null +++ b/core/switchablerequest_test.go @@ -0,0 +1,74 @@ +package core + +import ( + "encoding/json" + "github.com/aws/aws-lambda-go/events" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("SwitchableAPIGatewayRequest", func() { + Context("Serialization", func() { + It("v1 serialized okay", func() { + e := NewSwitchableAPIGatewayRequestV1(&events.APIGatewayProxyRequest{ + MultiValueQueryStringParameters: map[string][]string{}, + }) + b, err := json.Marshal(e) + Expect(err).To(BeNil()) + m := map[string]interface{}{} + err = json.Unmarshal(b, &m) + Expect(err).To(BeNil()) + Expect(m["multiValueQueryStringParameters"]).To(Equal(map[string]interface {}{})) + Expect(m["body"]).To(Equal("")) + }) + It("v2 serialized okay", func() { + e := NewSwitchableAPIGatewayRequestV2(&events.APIGatewayV2HTTPRequest{}) + b, err := json.Marshal(e) + Expect(err).To(BeNil()) + m := map[string]interface{}{} + err = json.Unmarshal(b, &m) + Expect(err).To(BeNil()) + Expect(m["rawQueryString"]).To(Equal("")) + Expect(m["isBase64Encoded"]).To(Equal(false)) + }) + }) + Context("Deserialization", func() { + It("v1 deserialized okay", func() { + input := &events.APIGatewayProxyRequest{ + Body: "234", + MultiValueQueryStringParameters: map[string][]string{ + "Test": []string{ "Value1", "Value2", }, + }, + } + b, _ := json.Marshal(input) + s := SwitchableAPIGatewayRequest{} + err := s.UnmarshalJSON(b) + Expect(err).To(BeNil()) + Expect(s.Version2()).To(BeNil()) + Expect(s.Version1()).To(BeEquivalentTo(input)) + }) + It("v2 deserialized okay", func() { + input := &events.APIGatewayV2HTTPRequest{ + IsBase64Encoded: true, + RawQueryString: "a=b&c=d", + } + b, _ := json.Marshal(input) + s := SwitchableAPIGatewayRequest{} + err := s.UnmarshalJSON(b) + Expect(err).To(BeNil()) + Expect(s.Version1()).To(BeNil()) + Expect(s.Version2()).To(BeEquivalentTo(input)) + }) + })}) + +func getProxyRequestV2(path string, method string) events.APIGatewayV2HTTPRequest { + return events.APIGatewayV2HTTPRequest{ + RequestContext: events.APIGatewayV2HTTPRequestContext{ + HTTP: events.APIGatewayV2HTTPRequestContextHTTPDescription{ + Path: path, + Method: method, + }, + }, + RawPath: path, + } +} diff --git a/core/switchableresponse.go b/core/switchableresponse.go new file mode 100644 index 0000000..fb3d3e6 --- /dev/null +++ b/core/switchableresponse.go @@ -0,0 +1,77 @@ +package core + +import ( + "encoding/json" + "errors" + "github.com/aws/aws-lambda-go/events" +) + +// SwitchableAPIGatewayResponse is a container for an APIGatewayProxyResponse or an APIGatewayV2HTTPResponse object which +// handles serialization and deserialization and switching between the entities based on the presence of fields in the +// source JSON, multiValueQueryStringParameters for APIGatewayProxyResponse and rawQueryString for +// APIGatewayV2HTTPResponse. It also provides some simple switching functions (wrapped type switching.) +type SwitchableAPIGatewayResponse struct { + v interface{} +} + +// NewSwitchableAPIGatewayResponseV1 creates a new SwitchableAPIGatewayResponse from APIGatewayProxyResponse +func NewSwitchableAPIGatewayResponseV1(v *events.APIGatewayProxyResponse) *SwitchableAPIGatewayResponse { + return &SwitchableAPIGatewayResponse{ + v: v, + } +} + +// NewSwitchableAPIGatewayResponseV2 creates a new SwitchableAPIGatewayResponse from APIGatewayV2HTTPResponse +func NewSwitchableAPIGatewayResponseV2(v *events.APIGatewayV2HTTPResponse) *SwitchableAPIGatewayResponse { + return &SwitchableAPIGatewayResponse{ + v: v, + } +} + +// MarshalJSON is a pass through serialization +func (s *SwitchableAPIGatewayResponse) MarshalJSON() ([]byte, error) { + return json.Marshal(s.v) +} + +// UnmarshalJSON is a switching serialization based on the presence of fields in the +// source JSON, statusCode to verify that it's either APIGatewayProxyResponse or APIGatewayV2HTTPResponse and then +// rawQueryString for to determine if it is APIGatewayV2HTTPResponse or not. +func (s *SwitchableAPIGatewayResponse) UnmarshalJSON(b []byte) error { + delta := map[string]json.RawMessage{} + if err := json.Unmarshal(b, &delta); err != nil { + return err + } + _, test := delta["statusCode"] + _, v2test := delta["cookies"] + s.v = nil + if test && !v2test { + s.v = &events.APIGatewayProxyResponse{} + } else if test && v2test { + s.v = &events.APIGatewayV2HTTPResponse{} + } else { + return errors.New("unable to determine response version") + } + return json.Unmarshal(b, s.v) +} + +// Version1 returns the contained events.APIGatewayProxyResponse or nil +func (s *SwitchableAPIGatewayResponse) Version1() *events.APIGatewayProxyResponse { + switch v := s.v.(type) { + case *events.APIGatewayProxyResponse: + return v + case events.APIGatewayProxyResponse: + return &v + } + return nil +} + +// Version2 returns the contained events.APIGatewayV2HTTPResponse or nil +func (s *SwitchableAPIGatewayResponse) Version2() *events.APIGatewayV2HTTPResponse { + switch v := s.v.(type) { + case *events.APIGatewayV2HTTPResponse: + return v + case events.APIGatewayV2HTTPResponse: + return &v + } + return nil +} diff --git a/core/switchableresponse_test.go b/core/switchableresponse_test.go new file mode 100644 index 0000000..11fa138 --- /dev/null +++ b/core/switchableresponse_test.go @@ -0,0 +1,61 @@ +package core + +import ( + "encoding/json" + "github.com/aws/aws-lambda-go/events" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("SwitchableAPIGatewayResponse", func() { + Context("Serialization", func() { + It("v1 serialized okay", func() { + e := NewSwitchableAPIGatewayResponseV1(&events.APIGatewayProxyResponse{}) + b, err := json.Marshal(e) + Expect(err).To(BeNil()) + m := map[string]interface{}{} + err = json.Unmarshal(b, &m) + Expect(err).To(BeNil()) + Expect(m["statusCode"]).To(Equal(0.0)) + Expect(m["body"]).To(Equal("")) + }) + It("v2 serialized okay", func() { + e := NewSwitchableAPIGatewayResponseV2(&events.APIGatewayV2HTTPResponse{}) + b, err := json.Marshal(e) + Expect(err).To(BeNil()) + m := map[string]interface{}{} + err = json.Unmarshal(b, &m) + Expect(err).To(BeNil()) + Expect(m["statusCode"]).To(Equal(0.0)) + Expect(m["body"]).To(Equal("")) + }) + }) + Context("Deserialization", func() { + It("v1 deserialized okay", func() { + input := &events.APIGatewayProxyResponse{ + StatusCode: 123, + Body: "234", + } + b, _ := json.Marshal(input) + s := SwitchableAPIGatewayResponse{} + err := s.UnmarshalJSON(b) + Expect(err).To(BeNil()) + Expect(s.Version2()).To(BeNil()) + Expect(s.Version1()).To(BeEquivalentTo(input)) + }) + It("v2 deserialized okay", func() { + input := &events.APIGatewayV2HTTPResponse{ + StatusCode: 123, + Body: "234", + Cookies: []string{"4", "5"}, + } + b, _ := json.Marshal(input) + s := SwitchableAPIGatewayResponse{} + err := s.UnmarshalJSON(b) + Expect(err).To(BeNil()) + Expect(s.Version1()).To(BeNil()) + Expect(s.Version2()).To(BeEquivalentTo(input)) + }) + }) +}) + diff --git a/core/types.go b/core/types.go new file mode 100644 index 0000000..d3f5a9e --- /dev/null +++ b/core/types.go @@ -0,0 +1,20 @@ +package core + +import ( + "fmt" + "net/http" + + "github.com/aws/aws-lambda-go/events" +) + +// GatewayTimeout returns a dafault Gateway Timeout (504) response +func GatewayTimeout() events.APIGatewayProxyResponse { + return events.APIGatewayProxyResponse{StatusCode: http.StatusGatewayTimeout} +} + +// NewLoggedError generates a new error and logs it to stdout +func NewLoggedError(format string, a ...interface{}) error { + err := fmt.Errorf(format, a...) + fmt.Println(err.Error()) + return err +} diff --git a/core/typesv2.go b/core/typesv2.go new file mode 100644 index 0000000..50e03d2 --- /dev/null +++ b/core/typesv2.go @@ -0,0 +1,11 @@ +package core + +import ( + "net/http" + + "github.com/aws/aws-lambda-go/events" +) + +func GatewayTimeoutV2() events.APIGatewayV2HTTPResponse { + return events.APIGatewayV2HTTPResponse{StatusCode: http.StatusGatewayTimeout} +} diff --git a/go.mod b/go.mod index c27382e..330f278 100644 --- a/go.mod +++ b/go.mod @@ -3,25 +3,33 @@ module github.com/fumeapp/fiber go 1.19 require ( - github.com/acidjazz/aws-lambda-go-api-proxy v0.0.4 github.com/aws/aws-lambda-go v1.40.0 github.com/gofiber/fiber/v2 v2.44.0 + github.com/onsi/ginkgo v1.16.5 + github.com/onsi/gomega v1.27.10 + github.com/valyala/fasthttp v1.46.0 ) require ( github.com/andybalholm/brotli v1.0.5 // indirect + github.com/fsnotify/fsnotify v1.4.9 // indirect + github.com/google/go-cmp v0.5.9 // indirect github.com/google/uuid v1.3.0 // indirect github.com/klauspost/compress v1.16.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.19 // indirect github.com/mattn/go-runewidth v0.0.14 // indirect + github.com/nxadm/tail v1.4.8 // indirect github.com/philhofer/fwd v1.1.2 // indirect github.com/rivo/uniseg v0.4.4 // indirect github.com/savsgio/dictpool v0.0.0-20221023140959-7bf2e61cea94 // indirect github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee // indirect github.com/tinylib/msgp v1.1.8 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect - github.com/valyala/fasthttp v1.46.0 // indirect github.com/valyala/tcplisten v1.0.0 // indirect + golang.org/x/net v0.12.0 // indirect golang.org/x/sys v0.11.0 // indirect + golang.org/x/text v0.11.0 // indirect + gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index c846ef3..8ca6ead 100644 --- a/go.sum +++ b/go.sum @@ -1,15 +1,35 @@ -github.com/acidjazz/aws-lambda-go-api-proxy v0.0.4 h1:gvQ18da+hIopfIBeOleofk0UVLdXP1+8aqEZeOdV+Lk= -github.com/acidjazz/aws-lambda-go-api-proxy v0.0.4/go.mod h1:U7XhEvHV12ygb0Cuh/rPJJsqy0wuBFgyJrI3GHM/DBc= github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/aws/aws-lambda-go v1.40.0 h1:6dKcDpXsTpapfCFF6Debng6CiV/Z3sNHekM6bwhI2J0= github.com/aws/aws-lambda-go v1.40.0/go.mod h1:jwFe2KmMsHmffA1X2R09hH6lFzJQxzI8qK17ewzbQMM= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/gofiber/fiber/v2 v2.44.0 h1:Z90bEvPcJM5GFJnu1py0E1ojoerkyew3iiNJ78MQCM8= github.com/gofiber/fiber/v2 v2.44.0/go.mod h1:VTMtb/au8g01iqvHyaCzftuM/xmZgKOZCtFzz6CdV9w= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I= github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= @@ -19,13 +39,23 @@ github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APP github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= -github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= +github.com/onsi/ginkgo/v2 v2.11.0 h1:WgqUCUt/lT6yXoQ8Wef0fsNn5cAuMK7+KT9UFRz2tcU= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= +github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= github.com/philhofer/fwd v1.1.1/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU= github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw= github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= @@ -34,6 +64,8 @@ github.com/savsgio/dictpool v0.0.0-20221023140959-7bf2e61cea94/go.mod h1:90zrgN3 github.com/savsgio/gotils v0.0.0-20220530130905-52f3993e8d6d/go.mod h1:Gy+0tqhJvgGlqnTF8CVGP0AaGRjwBtXs/a5PA0Y3+A4= github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee h1:8Iv5m6xEo1NR1AvpV+7XmhI4r39LGNzwUL4YpMuL5vk= github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee/go.mod h1:qwtSXrKuJh/zsFQ12yEE89xfCrGKK63Rr7ctU/uCo4g= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s= github.com/tinylib/msgp v1.1.6/go.mod h1:75BAfg2hauQhs3qedfdDZmWAPcFMAvJE5b9rGOMufyw= github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0= @@ -53,21 +85,31 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= -golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14= +golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50= +golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -83,15 +125,33 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc= +golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= +golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20201022035929-9cf592e881e9/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= +golang.org/x/tools v0.9.3 h1:Gn1I8+64MsuTb/HpH+LmQtNas23LhUVr3rYZ0eKuaMM= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go index ed1a77c..f833be1 100644 --- a/main.go +++ b/main.go @@ -5,9 +5,9 @@ import ( "log" "os" - fiberadapter "github.com/acidjazz/aws-lambda-go-api-proxy/fiber" "github.com/aws/aws-lambda-go/events" "github.com/aws/aws-lambda-go/lambda" + fiberadapter "github.com/fumeapp/fiber/adapter" "github.com/gofiber/fiber/v2" )