Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Function URL's v3 #192

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ import (

"github.com/aws/aws-lambda-go/events"
"github.com/aws/aws-lambda-go/lambda"
"github.com/awslabs/aws-lambda-go-api-proxy/gin"
ginadapter "github.com/awslabs/aws-lambda-go-api-proxy/gin"
"github.com/gin-gonic/gin"
)

Expand Down Expand Up @@ -84,6 +84,8 @@ func main() {
}
```

If you're using a Function URL, you can use the `ProxyFunctionURLWithContext` instead.

### Fiber

To use with the Fiber framework, following the instructions from the [Lambda documentation](https://docs.aws.amazon.com/lambda/latest/dg/go-programming-model-handler-types.html), declare a `Handler` method for the main package.
Expand Down Expand Up @@ -132,6 +134,7 @@ func main() {
lambda.Start(Handler)
}
```
If you're using a Function URL, you can use the `ProxyFunctionURLWithContext` instead.

## Other frameworks
This package also supports [Negroni](https://github.com/urfave/negroni), [GorillaMux](https://github.com/gorilla/mux), and plain old `HandlerFunc` - take a look at the code in their respective sub-directories. All packages implement the `Proxy` method exactly like our Gin sample above.
Expand Down
205 changes: 205 additions & 0 deletions core/requestFnURL.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
// Package core provides utility methods that help convert proxy events
// into an http.Request and http.ResponseWriter
package core

import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"log"
"net/http"
"net/url"
"os"
"strings"

"github.com/aws/aws-lambda-go/events"
"github.com/aws/aws-lambda-go/lambdacontext"
)

const (
// FnURLContextHeader is the custom header key used to store the
// Function Url context. To access the Context properties use the
// GetFunctionURLContext method of the RequestAccessorFnURL object.
FnURLContextHeader = "X-GoLambdaProxy-Fu-Context"
)

// RequestAccessorFnURL objects give access to custom API Gateway properties
// in the request.
type RequestAccessorFnURL struct {
stripBasePath string
}

// GetFunctionURLContext extracts the API Gateway context object from a
// request's custom header.
// Returns a populated events.LambdaFunctionURLRequestContext object from
// the request.
func (r *RequestAccessorFnURL) GetFunctionURLContext(req *http.Request) (events.LambdaFunctionURLRequestContext, error) {
if req.Header.Get(APIGwContextHeader) == "" {
return events.LambdaFunctionURLRequestContext{}, errors.New("No context header in request")
}
context := events.LambdaFunctionURLRequestContext{}
err := json.Unmarshal([]byte(req.Header.Get(FnURLContextHeader)), &context)
if err != nil {
log.Println("Erorr while unmarshalling context")
log.Println(err)
return events.LambdaFunctionURLRequestContext{}, err
}
return context, nil
}

// StripBasePath instructs the RequestAccessor object that the given base
// path should be removed from the request path before sending it to the
// framework for routing. This is used when the Lambda is configured with
// base path mappings in custom domain names.
func (r *RequestAccessorFnURL) StripBasePath(basePath string) string {
if strings.Trim(basePath, " ") == "" {
r.stripBasePath = ""
return ""
}

newBasePath := basePath
if !strings.HasPrefix(newBasePath, "/") {
newBasePath = "/" + newBasePath
}

if strings.HasSuffix(newBasePath, "/") {
newBasePath = newBasePath[:len(newBasePath)-1]
}

r.stripBasePath = newBasePath

return newBasePath
}

// ProxyEventToHTTPRequest converts an Function URL proxy event into a http.Request object.
// Returns the populated http request with additional two custom headers for the stage variables and Function Url context.
// To access these properties use GetFunctionURLContext method of the RequestAccessor object.
func (r *RequestAccessorFnURL) ProxyEventToHTTPRequest(req events.LambdaFunctionURLRequest) (*http.Request, error) {
httpRequest, err := r.EventToRequest(req)
if err != nil {
log.Println(err)
return nil, err
}
return addToHeaderFunctionURL(httpRequest, req)
}

// EventToRequestWithContext converts an Function URL proxy event and context into an http.Request object.
// Returns the populated http request with lambda context, stage variables and APIGatewayProxyRequestContext as part of its context.
// Access those using GetFunctionURLContextFromContext and GetRuntimeContextFromContext functions in this package.
func (r *RequestAccessorFnURL) EventToRequestWithContext(ctx context.Context, req events.LambdaFunctionURLRequest) (*http.Request, error) {
httpRequest, err := r.EventToRequest(req)
if err != nil {
log.Println(err)
return nil, err
}
return addToContextFunctionURL(ctx, httpRequest, req), nil
}

// EventToRequest converts an Function URL proxy event into an http.Request object.
// Returns the populated request maintaining headers
func (r *RequestAccessorFnURL) EventToRequest(req events.LambdaFunctionURLRequest) (*http.Request, error) {
decodedBody := []byte(req.Body)
if req.IsBase64Encoded {
base64Body, err := base64.StdEncoding.DecodeString(req.Body)
if err != nil {
return nil, err
}
decodedBody = base64Body
}

path := req.RawPath
// if RawPath empty is, populate from request context
if len(path) == 0 {
path = req.RequestContext.HTTP.Path
}

if r.stripBasePath != "" && len(r.stripBasePath) > 1 {
if strings.HasPrefix(path, r.stripBasePath) {
path = strings.Replace(path, r.stripBasePath, "", 1)
}
fmt.Printf("%v", path)
}
if !strings.HasPrefix(path, "/") {
path = "/" + path
}
serverAddress := "https://" + req.RequestContext.DomainName
if customAddress, ok := os.LookupEnv(CustomHostVariable); ok {
serverAddress = customAddress
}
path = serverAddress + path

if len(req.RawQueryString) > 0 {
path += "?" + req.RawQueryString
} else if len(req.QueryStringParameters) > 0 {
values := url.Values{}
for key, value := range req.QueryStringParameters {
values.Add(key, value)
}
path += "?" + values.Encode()
}

httpRequest, err := http.NewRequest(
strings.ToUpper(req.RequestContext.HTTP.Method),
path,
bytes.NewReader(decodedBody),
)

if err != nil {
fmt.Printf("Could not convert request %s:%s to http.Request\n", req.RequestContext.HTTP.Method, req.RequestContext.HTTP.Path)
log.Println(err)
return nil, err
}

httpRequest.RemoteAddr = req.RequestContext.HTTP.SourceIP

for _, cookie := range req.Cookies {
httpRequest.Header.Add("Cookie", cookie)
}

for headerKey, headerValue := range req.Headers {
for _, val := range strings.Split(headerValue, ",") {
httpRequest.Header.Add(headerKey, strings.Trim(val, " "))
}
}

httpRequest.RequestURI = httpRequest.URL.RequestURI()

return httpRequest, nil
}

func addToHeaderFunctionURL(req *http.Request, FunctionURLRequest events.LambdaFunctionURLRequest) (*http.Request, error) {
apiGwContext, err := json.Marshal(FunctionURLRequest.RequestContext)
if err != nil {
log.Println("Could not Marshal API GW context for custom header")
return req, err
}
req.Header.Add(APIGwContextHeader, string(apiGwContext))
return req, nil
}

func addToContextFunctionURL(ctx context.Context, req *http.Request, FunctionURLRequest events.LambdaFunctionURLRequest) *http.Request {
lc, _ := lambdacontext.FromContext(ctx)
rc := requestContextFnURL{lambdaContext: lc, FunctionURLProxyContext: FunctionURLRequest.RequestContext}
ctx = context.WithValue(ctx, ctxKey{}, rc)
return req.WithContext(ctx)
}

// GetFunctionURLContextFromContext retrieve APIGatewayProxyRequestContext from context.Context
func GetFunctionURLContextFromContext(ctx context.Context) (events.LambdaFunctionURLRequestContext, bool) {
v, ok := ctx.Value(ctxKey{}).(requestContextFnURL)
return v.FunctionURLProxyContext, ok
}

// GetRuntimeContextFromContextFnURL retrieve Lambda Runtime Context from context.Context
func GetRuntimeContextFromContextFnURL(ctx context.Context) (*lambdacontext.LambdaContext, bool) {
v, ok := ctx.Value(ctxKey{}).(requestContextFnURL)
return v.lambdaContext, ok
}

type requestContextFnURL struct {
lambdaContext *lambdacontext.LambdaContext
FunctionURLProxyContext events.LambdaFunctionURLRequestContext
}
133 changes: 133 additions & 0 deletions core/requestFnURL_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package core_test

import (
"context"
"crypto/rand"
"encoding/base64"
"fmt"

"github.com/aws/aws-lambda-go/events"
"github.com/awslabs/aws-lambda-go-api-proxy/core"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)

var _ = Describe("RequestAccessorFnURL tests", func() {
Context("Function URL event conversion", func() {
accessor := core.RequestAccessorFnURL{}
qs := make(map[string]string)
mvqs := make(map[string][]string)
hdr := make(map[string]string)
qs["UniqueId"] = "12345"
hdr["header1"] = "Testhdr1"
hdr["header2"] = "Testhdr2"
// Multivalue query strings
mvqs["k1"] = []string{"t1"}
mvqs["k2"] = []string{"t2"}
bdy := "Test BODY"
basePathRequest := getFunctionURLProxyRequest("/hello", getFunctionURLRequestContext("/hello", "GET"), false, hdr, bdy, qs, mvqs)

It("Correctly converts a basic event", func() {
httpReq, err := accessor.EventToRequestWithContext(context.Background(), basePathRequest)
Expect(err).To(BeNil())
Expect("/hello").To(Equal(httpReq.URL.Path))
Expect("/hello?UniqueId=12345").To(Equal(httpReq.RequestURI))
Expect("GET").To(Equal(httpReq.Method))
headers := basePathRequest.Headers
Expect(2).To(Equal(len(headers)))
})

binaryBody := make([]byte, 256)
_, err := rand.Read(binaryBody)
if err != nil {
Fail("Could not generate random binary body")
}

encodedBody := base64.StdEncoding.EncodeToString(binaryBody)

binaryRequest := getFunctionURLProxyRequest("/hello", getFunctionURLRequestContext("/hello", "POST"), true, hdr, bdy, qs, mvqs)
binaryRequest.Body = encodedBody
binaryRequest.IsBase64Encoded = true

It("Decodes a base64 encoded body", func() {
httpReq, err := accessor.EventToRequestWithContext(context.Background(), binaryRequest)
Expect(err).To(BeNil())
Expect("/hello").To(Equal(httpReq.URL.Path))
Expect("/hello?UniqueId=12345").To(Equal(httpReq.RequestURI))
Expect("POST").To(Equal(httpReq.Method))
})

mqsRequest := getFunctionURLProxyRequest("/hello", getFunctionURLRequestContext("/hello", "GET"), false, hdr, bdy, qs, mvqs)
mqsRequest.RawQueryString = "hello=1&world=2&world=3"
mqsRequest.QueryStringParameters = map[string]string{
"hello": "1",
"world": "2",
}

It("Populates query string correctly", func() {
httpReq, err := accessor.EventToRequestWithContext(context.Background(), mqsRequest)
Expect(err).To(BeNil())
Expect("/hello").To(Equal(httpReq.URL.Path))
fmt.Println("SDYFSDKFJDL")
fmt.Printf("%v", httpReq.RequestURI)
Expect(httpReq.RequestURI).To(ContainSubstring("hello=1"))
Expect(httpReq.RequestURI).To(ContainSubstring("world=2"))
Expect("GET").To(Equal(httpReq.Method))
query := httpReq.URL.Query()
Expect(2).To(Equal(len(query)))
Expect(query["hello"]).ToNot(BeNil())
Expect(query["world"]).ToNot(BeNil())
})
})

Context("StripBasePath tests", func() {
accessor := core.RequestAccessorFnURL{}
It("Adds prefix slash", func() {
basePath := accessor.StripBasePath("app1")
Expect("/app1").To(Equal(basePath))
})

It("Removes trailing slash", func() {
basePath := accessor.StripBasePath("/app1/")
Expect("/app1").To(Equal(basePath))
})

It("Ignores blank strings", func() {
basePath := accessor.StripBasePath(" ")
Expect("").To(Equal(basePath))
})
})
})

func getFunctionURLProxyRequest(path string, requestCtx events.LambdaFunctionURLRequestContext,
is64 bool, header map[string]string, body string, qs map[string]string, mvqs map[string][]string) events.LambdaFunctionURLRequest {
return events.LambdaFunctionURLRequest{
RequestContext: requestCtx,
RawPath: path,
RawQueryString: generateQueryString(qs),
Headers: header,
Body: body,
IsBase64Encoded: is64,
}
}

func getFunctionURLRequestContext(path, method string) events.LambdaFunctionURLRequestContext {
return events.LambdaFunctionURLRequestContext{
DomainName: "example.com",
HTTP: events.LambdaFunctionURLRequestContextHTTPDescription{
Method: method,
Path: path,
},
}
}

func generateQueryString(queryParameters map[string]string) string {
var queryString string
for key, value := range queryParameters {
if queryString != "" {
queryString += "&"
}
queryString += fmt.Sprintf("%s=%s", key, value)
}
return queryString
}
6 changes: 5 additions & 1 deletion core/requestv2_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ package core_test
import (
"context"
"encoding/base64"
"github.com/onsi/gomega/gstruct"
"fmt"
"io/ioutil"
"math/rand"
"os"
"strings"

"github.com/onsi/gomega/gstruct"

"github.com/aws/aws-lambda-go/events"
"github.com/aws/aws-lambda-go/lambdacontext"
"github.com/awslabs/aws-lambda-go-api-proxy/core"
Expand Down Expand Up @@ -74,6 +76,8 @@ var _ = Describe("RequestAccessorV2 tests", func() {
It("Populates multiple value query string correctly", func() {
httpReq, err := accessor.EventToRequestWithContext(context.Background(), mqsRequest)
Expect(err).To(BeNil())
fmt.Println("SDY!@$#!@FSDKFJDL")
fmt.Printf("%v", httpReq.RequestURI)
Expect("/hello").To(Equal(httpReq.URL.Path))
Expect(httpReq.RequestURI).To(ContainSubstring("hello=1"))
Expect(httpReq.RequestURI).To(ContainSubstring("world=2"))
Expand Down
Loading