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

Add support of LambdaFuctionURLRequest/Response #187

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
169 changes: 169 additions & 0 deletions core/requestFnURL.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
// Package core provides utility methods that help convert ALB events
// into an http.Request and http.ResponseWriter
package core

import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"log"
"net/http"
"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
// GetContext method of the RequestAccessorFnURL object.
FnURLContextHeader = "X-GoLambdaProxy-FnURL-Context"
)

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

// GetALBContext extracts the ALB context object from a request's custom header.
// Returns a populated events.ALBTargetGroupRequestContext object from the request.
func (r *RequestAccessorFnURL) GetContext(req *http.Request) (events.LambdaFunctionURLRequestContext, error) {
if req.Header.Get(FnURLContextHeader) == "" {
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("Error 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 API Gateway 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
}

// FunctionURLEventToHTTPRequest converts an a Function URL event into a http.Request object.
// Returns the populated http request with additional custom header for the Function URL context.
// To access these properties use the GetContext method of the RequestAccessorFnURL object.
func (r *RequestAccessorFnURL) FunctionURLEventToHTTPRequest(req events.LambdaFunctionURLRequest) (*http.Request, error) {
httpRequest, err := r.EventToRequest(req)
if err != nil {
log.Println(err)
return nil, err
}
return addToHeaderFnURL(httpRequest, req)
}

// FunctionURLEventToHTTPRequestWithContext converts a Function URL event and context into an http.Request object.
// Returns the populated http request with lambda context, Function URL RequestContext as part of its context.
func (r *RequestAccessorFnURL) FunctionURLEventToHTTPRequestWithContext(ctx context.Context, req events.LambdaFunctionURLRequest) (*http.Request, error) {
httpRequest, err := r.EventToRequest(req)
if err != nil {
log.Println(err)
return nil, err
}
return addToContextFnURL(ctx, httpRequest, req), nil
}

// EventToRequest converts a Function URL 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 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 + "?" + req.RawQueryString

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.RawPath)
log.Println(err)
return nil, err
}

for header, val := range req.Headers {
httpRequest.Header.Add(header, val)
}

httpRequest.RemoteAddr = req.RequestContext.HTTP.SourceIP
httpRequest.RequestURI = httpRequest.URL.RequestURI()

return httpRequest, nil
}

func addToHeaderFnURL(req *http.Request, fnUrlRequest events.LambdaFunctionURLRequest) (*http.Request, error) {
ctx, err := json.Marshal(fnUrlRequest.RequestContext)
if err != nil {
log.Println("Could not Marshal Function URL context for custom header")
return req, err
}
req.Header.Set(FnURLContextHeader, string(ctx))
return req, nil
}

// adds context data to http request so we can pass
func addToContextFnURL(ctx context.Context, req *http.Request, fnUrlRequest events.LambdaFunctionURLRequest) *http.Request {
lc, _ := lambdacontext.FromContext(ctx)
rc := requestContextFnURL{lambdaContext: lc, fnUrlContext: fnUrlRequest.RequestContext}
ctx = context.WithValue(ctx, ctxKey{}, rc)
return req.WithContext(ctx)
}

type requestContextFnURL struct {
lambdaContext *lambdacontext.LambdaContext
fnUrlContext events.LambdaFunctionURLRequestContext
}
117 changes: 117 additions & 0 deletions core/responseFnURL.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
// 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"
)

// ProxyResponseWriterFunctionURL implements http.ResponseWriter and adds the method
// necessary to return an events.LambdaFunctionURLResponse object
type ProxyResponseWriterFunctionURL struct {
status int
headers http.Header
body bytes.Buffer
observers []chan<- bool
}

// Ensure implementation satisfies http.ResponseWriter interface
var (
_ http.ResponseWriter = &ProxyResponseWriterFunctionURL{}
)

// NewProxyResponseWriterFnURL returns a new ProxyResponseWriterFunctionURL object.
// The object is initialized with an empty map of headers and a status code of -1
func NewProxyResponseWriterFnURL() *ProxyResponseWriterFunctionURL {
return &ProxyResponseWriterFunctionURL{
headers: make(http.Header),
status: defaultStatusCode,
observers: make([]chan<- bool, 0),
}
}

func (r *ProxyResponseWriterFunctionURL) CloseNotify() <-chan bool {
ch := make(chan bool, 1)

r.observers = append(r.observers, ch)

return ch
}

func (r *ProxyResponseWriterFunctionURL) notifyClosed() {
for _, v := range r.observers {
v <- true
}
}

// Header implementation from the http.ResponseWriter interface.
func (r *ProxyResponseWriterFunctionURL) 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 *ProxyResponseWriterFunctionURL) 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 *ProxyResponseWriterFunctionURL) WriteHeader(status int) {
r.status = status
}

// GetProxyResponse converts the data passed to the response writer into
// an events.LambdaFunctionURLResponse 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 *ProxyResponseWriterFunctionURL) GetProxyResponse() (events.LambdaFunctionURLResponse, error) {
r.notifyClosed()

if r.status == defaultStatusCode {
return events.LambdaFunctionURLResponse{}, errors.New("status code not set on response")
}

var output string
isBase64 := false

bb := (&r.body).Bytes()

if utf8.Valid(bb) {
output = string(bb)
} else {
output = base64.StdEncoding.EncodeToString(bb)
isBase64 = true
}

headers := make(map[string]string)
for h, v := range r.Header() {
headers[h] = v[0]
}

return events.LambdaFunctionURLResponse{
StatusCode: r.status,
Headers: headers,
Body: output,
IsBase64Encoded: isBase64,
}, nil
}
12 changes: 12 additions & 0 deletions core/typesFnURL.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package core

import (
"net/http"

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

// GatewayTimeoutFnURL returns a dafault Gateway Timeout (504) response
func GatewayTimeoutFnURL() events.LambdaFunctionURLResponse {
return events.LambdaFunctionURLResponse{StatusCode: http.StatusGatewayTimeout}
}
3 changes: 1 addition & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ go 1.14

require (
github.com/BurntSushi/toml v1.1.0 // indirect
github.com/aws/aws-lambda-go v1.19.1
github.com/aws/aws-lambda-go v1.41.0
github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible // indirect
github.com/gin-gonic/gin v1.7.7
github.com/go-chi/chi/v5 v5.0.2
Expand All @@ -27,7 +27,6 @@ require (
golang.org/x/time v0.0.0-20220609170525-579cf78fd858 // indirect
google.golang.org/protobuf v1.28.0 // indirect
gopkg.in/ini.v1 v1.66.6 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

replace (
Expand Down
10 changes: 4 additions & 6 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,8 @@ github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hC
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/aws/aws-lambda-go v1.19.1 h1:5iUHbIZ2sG6Yq/J1IN3sWm3+vAB1CWwhI21NffLNuNI=
github.com/aws/aws-lambda-go v1.19.1/go.mod h1:jJmlefzPfGnckuHdXX7/80O3BvUUi12XOkbv4w9SGLU=
github.com/aws/aws-lambda-go v1.41.0 h1:l/5fyVb6Ud9uYd411xdHZzSf2n86TakxzpvIoz7l+3Y=
github.com/aws/aws-lambda-go v1.41.0/go.mod h1:jwFe2KmMsHmffA1X2R09hH6lFzJQxzI8qK17ewzbQMM=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/aymerick/raymond v2.0.2+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g=
Expand All @@ -88,7 +88,6 @@ github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3Ee
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
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=
Expand Down Expand Up @@ -430,8 +429,9 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s=
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/tdewolff/minify/v2 v2.10.0/go.mod h1:6XAjcHM46pFcRE0eztigFPm0Q+Cxsw8YhEWT+rDkcZM=
github.com/tdewolff/minify/v2 v2.11.10 h1:2tk9nuKfc8YOTD8glZ7JF/VtE8W5HOgmepWdjcPtRro=
Expand All @@ -448,7 +448,6 @@ github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVM
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
github.com/urfave/cli/v2 v2.2.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ=
github.com/urfave/negroni v1.0.0 h1:kIimOitoypq34K7TG7DUaJ9kq/N4Ofuwi1sjz0KipXc=
github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
Expand Down Expand Up @@ -902,7 +901,6 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
Expand Down
13 changes: 13 additions & 0 deletions handlerfunc/adapterFnURL.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package handlerfunc

import (
"net/http"

"github.com/awslabs/aws-lambda-go-api-proxy/httpadapter"
)

type HandlerFuncAdapterFnURL = httpadapter.HandlerAdapterFnURL

func NewFunctionURL(handlerFunc http.HandlerFunc) *HandlerFuncAdapterFnURL {
return httpadapter.NewFunctionURL(handlerFunc)
}
Loading