Skip to content
This repository was archived by the owner on May 21, 2025. It is now read-only.

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