From 8d5d1f1e6ed87d149834afbff8a24e88c6cd3c3c Mon Sep 17 00:00:00 2001 From: ikegam1 Date: Sun, 18 Sep 2022 15:50:24 +0900 Subject: [PATCH 1/3] issue-144 --- core/requestalb.go | 158 ++++++++++++++++++++++++++ core/requestalb_test.go | 239 +++++++++++++++++++++++++++++++++++++++ core/responsealb.go | 126 +++++++++++++++++++++ core/responsealb_test.go | 182 +++++++++++++++++++++++++++++ core/typesalb.go | 11 ++ echo/adapteralb.go | 59 ++++++++++ echo/echolambda_test.go | 26 +++++ 7 files changed, 801 insertions(+) create mode 100644 core/requestalb.go create mode 100644 core/requestalb_test.go create mode 100644 core/responsealb.go create mode 100644 core/responsealb_test.go create mode 100644 core/typesalb.go create mode 100644 echo/adapteralb.go diff --git a/core/requestalb.go b/core/requestalb.go new file mode 100644 index 0000000..a2ca65b --- /dev/null +++ b/core/requestalb.go @@ -0,0 +1,158 @@ +// 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" + "strings" + + "github.com/aws/aws-lambda-go/events" + "github.com/aws/aws-lambda-go/lambdacontext" +) + +const ( + // ALBTgContextHeader is the custom header key used to store the + // ALB Target Group Request context. To access the Context properties use the + // RequestAccessorALB method of the RequestAccessorALB object. + ALBTgContextHeader = "X-Golambdaproxy-Albtargetgroup-Context" +) + +// RequestAccessorALB objects give access to custom API Gateway properties +// in the request. +type RequestAccessorALB struct{} + +// GetALBTargetGroupRequestContext extracts the ALB Target Group Responce context object from a +// request's custom header. +// Returns a populated events.ALBTargetGroupRequestContext object from +// the request. +func (r *RequestAccessorALB) GetALBTargetGroupRequestContext(req *http.Request) (events.ALBTargetGroupRequestContext, error) { + if req.Header.Get(ALBTgContextHeader) == "" { + return events.ALBTargetGroupRequestContext{}, errors.New("No context header in request") + } + context := events.ALBTargetGroupRequestContext{} + err := json.Unmarshal([]byte(req.Header.Get(ALBTgContextHeader)), &context) + if err != nil { + log.Println("Erorr while unmarshalling context") + log.Println(err) + return events.ALBTargetGroupRequestContext{}, err + } + return context, nil +} + +// ProxyEventToHTTPRequest converts an ALB Target Group proxy event into a http.Request object. +// Returns the populated http request with additional two custom headers for ALB Tg Req context. +// To access these properties use the GetALBTargetGroupRequestContext method of the RequestAccessor object. +func (r *RequestAccessorALB) ProxyEventToHTTPRequest(req events.ALBTargetGroupRequest) (*http.Request, error) { + httpRequest, err := r.EventToRequest(req) + if err != nil { + log.Println(err) + return nil, err + } + return addToHeaderALB(httpRequest, req) +} + +// EventToRequestWithContext converts an ALB Target Group proxy event and context into an http.Request object. +// Returns the populated http request with lambda context, ALBTargetGroupRequestContext as part of its context. +// Access those using GetRuntimeContextFromContextALB and GetRuntimeContextFromContext functions in this package. +func (r *RequestAccessorALB) EventToRequestWithContext(ctx context.Context, req events.ALBTargetGroupRequest) (*http.Request, error) { + httpRequest, err := r.EventToRequest(req) + if err != nil { + log.Println(err) + return nil, err + } + return addToContextALB(ctx, httpRequest, req), nil +} + +// EventToRequest converts an ALB Target group proxy event into an http.Request object. +// Returns the populated request maintaining headers +func (r *RequestAccessorALB) EventToRequest(req events.ALBTargetGroupRequest) (*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 !strings.HasPrefix(path, "/") { + path = "/" + path + } + + 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.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 + } + + for headerKey, headerValue := range req.Headers { + for _, val := range strings.Split(headerValue, ",") { + httpRequest.Header.Add(headerKey, strings.Trim(val, " ")) + } + } + + for headerKey, headerValue := range req.MultiValueHeaders { + for _, arrVal := range headerValue { + for _, val := range strings.Split(arrVal, ",") { + httpRequest.Header.Add(headerKey, strings.Trim(val, " ")) + } + } + } + + httpRequest.RequestURI = httpRequest.URL.RequestURI() + + return httpRequest, nil +} + +func addToHeaderALB(req *http.Request, albTgRequest events.ALBTargetGroupRequest) (*http.Request, error) { + //req.Header.Add(ALBTgContextHeader, albTgRequest.RequestContext.ELB.TargetGroupArn) + albTgContext, err := json.Marshal(albTgRequest.RequestContext) + if err != nil { + log.Println("Could not Marshal ALB Tg context for custom header") + return req, err + } + req.Header.Add(ALBTgContextHeader, string(albTgContext)) + return req, nil +} + +func addToContextALB(ctx context.Context, req *http.Request, albTgRequest events.ALBTargetGroupRequest) *http.Request { + lc, _ := lambdacontext.FromContext(ctx) + rc := requestContextALB{lambdaContext: lc, gatewayProxyContext: albTgRequest.RequestContext} + ctx = context.WithValue(ctx, ctxKey{}, rc) + return req.WithContext(ctx) +} + +// GetRuntimeContextFromContextALB retrieve Lambda Runtime Context from context.Context +func GetRuntimeContextFromContextALB(ctx context.Context) (*lambdacontext.LambdaContext, bool) { + v, ok := ctx.Value(ctxKey{}).(requestContextALB) + return v.lambdaContext, ok +} + +type requestContextALB struct { + lambdaContext *lambdacontext.LambdaContext + gatewayProxyContext events.ALBTargetGroupRequestContext +} diff --git a/core/requestalb_test.go b/core/requestalb_test.go new file mode 100644 index 0000000..fe84f89 --- /dev/null +++ b/core/requestalb_test.go @@ -0,0 +1,239 @@ +package core_test + +import ( + "context" + "encoding/base64" + "encoding/json" + "io/ioutil" + "math/rand" + "strings" + + "github.com/aws/aws-lambda-go/events" + "github.com/aws/aws-lambda-go/lambdacontext" + "github.com/awslabs/aws-lambda-go-api-proxy/core" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("RequestAccessorALB tests", func() { + Context("event conversion", func() { + accessor := core.RequestAccessorALB{} + basicRequest := getProxyRequestALB("/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 = getProxyRequestALB("/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 := getProxyRequestALB("/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 := getProxyRequestALB("/hello", "GET") + 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("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])) + }) + + // Support `QueryStringParameters` for backward compatibility. + // https://github.com/awslabs/aws-lambda-go-api-proxy/issues/37 + qsRequest := getProxyRequestALB("/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 := getProxyRequestALB("/hello", "GET") + mvhRequest.Headers = map[string]string{ + "hello": "1", + "world": "2,3", + } + mvhRequest.MultiValueHeaders = map[string][]string{ + "hello world": []string{"4", "5", "6"}, + } + + 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(3).To(Equal(len(headers))) + + for k, value := range headers { + if mvhRequest.Headers[strings.ToLower(k)] != "" { + Expect(strings.Join(value, ",")).To(Equal(mvhRequest.Headers[strings.ToLower(k)])) + } else { + Expect(strings.Join(value, ",")).To(Equal(strings.Join(mvhRequest.MultiValueHeaders[strings.ToLower(k)], ","))) + } + } + }) + + svhRequest := getProxyRequestALB("/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 := getProxyRequestALB("/orders", "GET") + + It("Stips the base path correct", func() { + 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 := getProxyRequestALB("/orders", "GET") + contextRequest.RequestContext = getRequestContextALB() + + It("Populates context header correctly", func() { + // calling old method to verify reverse compatibility + httpReq, err := accessor.ProxyEventToHTTPRequest(contextRequest) + Expect(err).To(BeNil()) + Expect(1).To(Equal(len(httpReq.Header))) + Expect(httpReq.Header.Get(core.ALBTgContextHeader)).ToNot(BeNil()) + }) + }) + + Context("Retrieves ALB Target Group context", func() { + It("Returns a correctly unmarshalled object", func() { + contextRequest := getProxyRequestALB("/orders", "GET") + contextRequest.RequestContext = getRequestContextALB() + + accessor := core.RequestAccessorALB{} + httpReq, err := accessor.ProxyEventToHTTPRequest(contextRequest) + Expect(err).To(BeNil()) + ctx := httpReq.Header[core.ALBTgContextHeader][0] + var parsedCtx events.ALBTargetGroupRequestContext + json.Unmarshal([]byte(ctx), &parsedCtx) + Expect("foo").To(Equal(parsedCtx.ELB.TargetGroupArn)) + + headerContext, err := accessor.GetALBTargetGroupRequestContext(httpReq) + Expect(err).To(BeNil()) + Expect("foo").To(Equal(headerContext.ELB.TargetGroupArn)) + + httpReq, err = accessor.EventToRequestWithContext(context.Background(), contextRequest) + Expect(err).To(BeNil()) + Expect("/orders").To(Equal(httpReq.RequestURI)) + runtimeContext, ok := core.GetRuntimeContextFromContextALB(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()) + Expect("/orders").To(Equal(httpReq.RequestURI)) + + headerContext, err = accessor.GetALBTargetGroupRequestContext(httpReq) + // should fail as new context method doesn't populate headers + Expect(err).ToNot(BeNil()) + Expect("").To(Equal(headerContext.ELB.TargetGroupArn)) + runtimeContext, ok = core.GetRuntimeContextFromContextALB(httpReq.Context()) + Expect(ok).To(BeTrue()) + Expect(runtimeContext).ToNot(BeNil()) + Expect("abc123").To(Equal(runtimeContext.AwsRequestID)) + }) + }) +}) + +func getProxyRequestALB(path string, method string) events.ALBTargetGroupRequest { + return events.ALBTargetGroupRequest{ + RequestContext: events.ALBTargetGroupRequestContext{}, + Path: path, + HTTPMethod: method, + Headers: map[string]string{}, + } +} + +func getRequestContextALB() events.ALBTargetGroupRequestContext { + return events.ALBTargetGroupRequestContext{ + ELB: events.ELBContext{ + TargetGroupArn: "foo", + }, + } +} diff --git a/core/responsealb.go b/core/responsealb.go new file mode 100644 index 0000000..cca6e17 --- /dev/null +++ b/core/responsealb.go @@ -0,0 +1,126 @@ +// Package core provides utility methods that help convert proxy events +// into an http.Request and http.ResponseWriter +package core + +import ( + "bytes" + "encoding/base64" + "errors" + "fmt" + "net/http" + "strings" + "unicode/utf8" + + "github.com/aws/aws-lambda-go/events" +) + +// ProxyResponseWriterALB implements http.ResponseWriter and adds the method +// necessary to return an events.ALBTargetGroupResponse object +type ProxyResponseWriterALB 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 NewProxyResponseWriterALB() *ProxyResponseWriterALB { + return &ProxyResponseWriterALB{ + headers: make(http.Header), + status: defaultStatusCode, + observers: make([]chan<- bool, 0), + } + +} + +func (r *ProxyResponseWriterALB) CloseNotify() <-chan bool { + ch := make(chan bool, 1) + + r.observers = append(r.observers, ch) + + return ch +} + +func (r *ProxyResponseWriterALB) notifyClosed() { + for _, v := range r.observers { + v <- true + } +} + +// Header implementation from the http.ResponseWriter interface. +func (r *ProxyResponseWriterALB) 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 *ProxyResponseWriterALB) 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 *ProxyResponseWriterALB) WriteHeader(status int) { + r.status = status +} + +// GetProxyResponse converts the data passed to the response writer into +// an events.ALBTargetGroupResponse 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 *ProxyResponseWriterALB) GetProxyResponse() (events.ALBTargetGroupResponse, error) { + r.notifyClosed() + + if r.status == defaultStatusCode { + return events.ALBTargetGroupResponse{}, 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) + multiHeaders := make(map[string][]string) + + // set both Headers and MultiValueHeaders + for headerKey, headerValue := range http.Header(r.headers) { + headers[headerKey] = strings.Join(headerValue, ",") + if multiHeaders[headerKey] != nil { + multiHeaders[headerKey] = append(multiHeaders[headerKey], strings.Join(headerValue, ",")) + } else { + multiHeaders[headerKey] = []string{strings.Join(headerValue, ",")} + } + } + + return events.ALBTargetGroupResponse{ + StatusCode: r.status, + StatusDescription: fmt.Sprintf("%d %s", r.status, http.StatusText(r.status)), + Headers: headers, + MultiValueHeaders: multiHeaders, + Body: output, + IsBase64Encoded: isBase64, + }, nil +} diff --git a/core/responsealb_test.go b/core/responsealb_test.go new file mode 100644 index 0000000..704f2df --- /dev/null +++ b/core/responsealb_test.go @@ -0,0 +1,182 @@ +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 := NewProxyResponseWriterALB() + + 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") + response.Header().Add("Content-Type", "charset=utf-8") + + Expect(1).To(Equal(len(response.headers))) + Expect("application/json").To(Equal(response.headers["Content-Type"][0])) + Expect("charset=utf-8").To(Equal(response.headers["Content-Type"][1])) + }) + + 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/typesalb.go b/core/typesalb.go new file mode 100644 index 0000000..bc5cf94 --- /dev/null +++ b/core/typesalb.go @@ -0,0 +1,11 @@ +package core + +import ( + "net/http" + + "github.com/aws/aws-lambda-go/events" +) + +func GatewayTimeoutALB() events.ALBTargetGroupResponse { + return events.ALBTargetGroupResponse{StatusCode: http.StatusGatewayTimeout} +} diff --git a/echo/adapteralb.go b/echo/adapteralb.go new file mode 100644 index 0000000..0d92fc5 --- /dev/null +++ b/echo/adapteralb.go @@ -0,0 +1,59 @@ +package echoadapter + +import ( + "context" + "net/http" + + "github.com/aws/aws-lambda-go/events" + "github.com/awslabs/aws-lambda-go-api-proxy/core" + "github.com/labstack/echo/v4" +) + +// EchoLambdaV2 makes it easy to send API Gateway proxy V2 events to a echo.Echo. +// The library transforms the proxy event into an HTTP request and then +// creates a proxy response object from the http.ResponseWriter +type EchoLambdaALB struct { + core.RequestAccessorALB + + Echo *echo.Echo +} + +// NewV2 creates a new instance of the EchoLambda object. +// Receives an initialized *echo.Echo object - normally created with echo.New(). +// It returns the initialized instance of the EchoLambdaV2 object. +func NewALB(e *echo.Echo) *EchoLambdaALB { + return &EchoLambdaALB{Echo: e} +} + +// Proxy receives an API Gateway proxy V2 event, transforms it 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 (e *EchoLambdaALB) Proxy(req events.ALBTargetGroupRequest) (events.ALBTargetGroupResponse, error) { + echoRequest, err := e.ProxyEventToHTTPRequest(req) + return e.proxyInternal(echoRequest, err) +} + +// ProxyWithContext receives context and an API Gateway proxy V2 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 (e *EchoLambdaALB) ProxyWithContext(ctx context.Context, req events.ALBTargetGroupRequest) (events.ALBTargetGroupResponse, error) { + echoRequest, err := e.EventToRequestWithContext(ctx, req) + return e.proxyInternal(echoRequest, err) +} + +func (e *EchoLambdaALB) proxyInternal(req *http.Request, err error) (events.ALBTargetGroupResponse, error) { + + if err != nil { + return core.GatewayTimeoutALB(), core.NewLoggedError("Could not convert proxy event to request: %v", err) + } + + respWriter := core.NewProxyResponseWriterALB() + e.Echo.ServeHTTP(http.ResponseWriter(respWriter), req) + + proxyResponse, err := respWriter.GetProxyResponse() + if err != nil { + return core.GatewayTimeoutALB(), core.NewLoggedError("Error while generating proxy response: %v", err) + } + + return proxyResponse, nil +} diff --git a/echo/echolambda_test.go b/echo/echolambda_test.go index 877760b..fbbb508 100644 --- a/echo/echolambda_test.go +++ b/echo/echolambda_test.go @@ -64,3 +64,29 @@ var _ = Describe("EchoLambdaV2 tests", func() { }) }) }) + +var _ = Describe("EchoLambdaALB tests", func() { + Context("Simple ping request", func() { + It("Proxies the event correctly", func() { + log.Println("Starting test") + e := echo.New() + e.GET("/ping", func(c echo.Context) error { + log.Println("Handler!!") + return c.String(200, "pong") + }) + + adapter := echoadapter.NewALB(e) + + req := events.ALBTargetGroupRequest{ + HTTPMethod: "GET", + Path: "/ping", + RequestContext: events.ALBTargetGroupRequestContext{}, + } + + resp, err := adapter.Proxy(req) + + Expect(err).To(BeNil()) + Expect(resp.StatusCode).To(Equal(200)) + }) + }) +}) From 2f9b5fd072823567004665289d5c5d8eb6745b4a Mon Sep 17 00:00:00 2001 From: ikegam1 Date: Sun, 18 Sep 2022 16:50:07 +0900 Subject: [PATCH 2/3] text fix --- core/requestalb.go | 2 +- core/responsealb_test.go | 2 +- echo/adapteralb.go | 10 +++++----- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/core/requestalb.go b/core/requestalb.go index a2ca65b..8a24a03 100644 --- a/core/requestalb.go +++ b/core/requestalb.go @@ -25,7 +25,7 @@ const ( ALBTgContextHeader = "X-Golambdaproxy-Albtargetgroup-Context" ) -// RequestAccessorALB objects give access to custom API Gateway properties +// RequestAccessorALB objects give access to custom ALB Target Group properties // in the request. type RequestAccessorALB struct{} diff --git a/core/responsealb_test.go b/core/responsealb_test.go index 704f2df..99badb4 100644 --- a/core/responsealb_test.go +++ b/core/responsealb_test.go @@ -96,7 +96,7 @@ var _ = Describe("ResponseWriter tests", func() { }) }) - Context("Export API Gateway proxy response", func() { + Context("Export ALB Target Group proxy response", func() { emtpyResponse := NewProxyResponseWriter() emtpyResponse.Header().Add("Content-Type", "application/json") diff --git a/echo/adapteralb.go b/echo/adapteralb.go index 0d92fc5..a9c657e 100644 --- a/echo/adapteralb.go +++ b/echo/adapteralb.go @@ -9,7 +9,7 @@ import ( "github.com/labstack/echo/v4" ) -// EchoLambdaV2 makes it easy to send API Gateway proxy V2 events to a echo.Echo. +// EchoLambdaALB makes it easy to send ALB Target Group events to a echo.Echo. // The library transforms the proxy event into an HTTP request and then // creates a proxy response object from the http.ResponseWriter type EchoLambdaALB struct { @@ -18,14 +18,14 @@ type EchoLambdaALB struct { Echo *echo.Echo } -// NewV2 creates a new instance of the EchoLambda object. +// NewALB creates a new instance of the EchoLambda object. // Receives an initialized *echo.Echo object - normally created with echo.New(). -// It returns the initialized instance of the EchoLambdaV2 object. +// It returns the initialized instance of the EchoLambdaALB object. func NewALB(e *echo.Echo) *EchoLambdaALB { return &EchoLambdaALB{Echo: e} } -// Proxy receives an API Gateway proxy V2 event, transforms it into an http.Request +// Proxy receives an ALB Target Group event, transforms it 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 (e *EchoLambdaALB) Proxy(req events.ALBTargetGroupRequest) (events.ALBTargetGroupResponse, error) { @@ -33,7 +33,7 @@ func (e *EchoLambdaALB) Proxy(req events.ALBTargetGroupRequest) (events.ALBTarge return e.proxyInternal(echoRequest, err) } -// ProxyWithContext receives context and an API Gateway proxy V2 event, +// ProxyWithContext receives context and an ALB Target Group 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 (e *EchoLambdaALB) ProxyWithContext(ctx context.Context, req events.ALBTargetGroupRequest) (events.ALBTargetGroupResponse, error) { From e466b1eb8371adb6abb8d017654429c03494b396 Mon Sep 17 00:00:00 2001 From: ikegam1 Date: Sun, 18 Sep 2022 16:53:22 +0900 Subject: [PATCH 3/3] delte comment --- core/requestalb.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/core/requestalb.go b/core/requestalb.go index 8a24a03..648dcb0 100644 --- a/core/requestalb.go +++ b/core/requestalb.go @@ -129,7 +129,6 @@ func (r *RequestAccessorALB) EventToRequest(req events.ALBTargetGroupRequest) (* } func addToHeaderALB(req *http.Request, albTgRequest events.ALBTargetGroupRequest) (*http.Request, error) { - //req.Header.Add(ALBTgContextHeader, albTgRequest.RequestContext.ELB.TargetGroupArn) albTgContext, err := json.Marshal(albTgRequest.RequestContext) if err != nil { log.Println("Could not Marshal ALB Tg context for custom header") @@ -146,7 +145,6 @@ func addToContextALB(ctx context.Context, req *http.Request, albTgRequest events return req.WithContext(ctx) } -// GetRuntimeContextFromContextALB retrieve Lambda Runtime Context from context.Context func GetRuntimeContextFromContextALB(ctx context.Context) (*lambdacontext.LambdaContext, bool) { v, ok := ctx.Value(ctxKey{}).(requestContextALB) return v.lambdaContext, ok