Skip to content

Commit 9755d5c

Browse files
committed
addfunctionurl
1 parent 06305ac commit 9755d5c

File tree

6 files changed

+489
-59
lines changed

6 files changed

+489
-59
lines changed

core/requestFunctionUrl.go

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
// Package core provides utility methods that help convert proxy events
2+
// into an http.Request and http.ResponseWriter
3+
package core
4+
5+
import (
6+
"bytes"
7+
"context"
8+
"encoding/base64"
9+
"encoding/json"
10+
"errors"
11+
"fmt"
12+
"log"
13+
"net/http"
14+
"net/url"
15+
"os"
16+
"strings"
17+
18+
"github.com/aws/aws-lambda-go/events"
19+
"github.com/aws/aws-lambda-go/lambdacontext"
20+
)
21+
22+
const (
23+
// FuContextHeader is the custom header key used to store the
24+
// Function Url context. To access the Context properties use the
25+
// GetFunctionUrlContext method of the RequestAccessorFu object.
26+
FuContextHeader = "X-GoLambdaProxy-Fu-Context"
27+
)
28+
29+
// RequestAccessorV2 objects give access to custom API Gateway properties
30+
// in the request.
31+
type RequestAccessorFu struct {
32+
stripBasePath string
33+
}
34+
35+
// GetAPIGatewayContextV2 extracts the API Gateway context object from a
36+
// request's custom header.
37+
// Returns a populated events.APIGatewayProxyRequestContext object from
38+
// the request.
39+
func (r *RequestAccessorFu) GetFunctionUrlContext(req *http.Request) (events.LambdaFunctionURLRequestContext, error) {
40+
if req.Header.Get(APIGwContextHeader) == "" {
41+
return events.LambdaFunctionURLRequestContext{}, errors.New("No context header in request")
42+
}
43+
context := events.LambdaFunctionURLRequestContext{}
44+
err := json.Unmarshal([]byte(req.Header.Get(FuContextHeader)), &context)
45+
if err != nil {
46+
log.Println("Erorr while unmarshalling context")
47+
log.Println(err)
48+
return events.LambdaFunctionURLRequestContext{}, err
49+
}
50+
return context, nil
51+
}
52+
53+
// StripBasePath instructs the RequestAccessor object that the given base
54+
// path should be removed from the request path before sending it to the
55+
// framework for routing. This is used when the Lambda is configured with
56+
// base path mappings in custom domain names.
57+
func (r *RequestAccessorFu) StripBasePath(basePath string) string {
58+
if strings.Trim(basePath, " ") == "" {
59+
r.stripBasePath = ""
60+
return ""
61+
}
62+
63+
newBasePath := basePath
64+
if !strings.HasPrefix(newBasePath, "/") {
65+
newBasePath = "/" + newBasePath
66+
}
67+
68+
if strings.HasSuffix(newBasePath, "/") {
69+
newBasePath = newBasePath[:len(newBasePath)-1]
70+
}
71+
72+
r.stripBasePath = newBasePath
73+
74+
return newBasePath
75+
}
76+
77+
// ProxyEventToHTTPRequest converts an API Gateway proxy event into a http.Request object.
78+
// Returns the populated http request with additional two custom headers for the stage variables and API Gateway context.
79+
// To access these properties use the GetAPIGatewayStageVars and GetAPIGatewayContext method of the RequestAccessor object.
80+
func (r *RequestAccessorFu) ProxyEventToHTTPRequest(req events.LambdaFunctionURLRequest) (*http.Request, error) {
81+
httpRequest, err := r.EventToRequest(req)
82+
if err != nil {
83+
log.Println(err)
84+
return nil, err
85+
}
86+
return addToHeaderFu(httpRequest, req)
87+
}
88+
89+
// EventToRequestWithContext converts an API Gateway proxy event and context into an http.Request object.
90+
// Returns the populated http request with lambda context, stage variables and APIGatewayProxyRequestContext as part of its context.
91+
// Access those using GetAPIGatewayContextFromContext, GetStageVarsFromContext and GetRuntimeContextFromContext functions in this package.
92+
func (r *RequestAccessorFu) EventToRequestWithContext(ctx context.Context, req events.LambdaFunctionURLRequest) (*http.Request, error) {
93+
httpRequest, err := r.EventToRequest(req)
94+
if err != nil {
95+
log.Println(err)
96+
return nil, err
97+
}
98+
return addToContextFu(ctx, httpRequest, req), nil
99+
}
100+
101+
// EventToRequest converts an API Gateway proxy event into an http.Request object.
102+
// Returns the populated request maintaining headers
103+
func (r *RequestAccessorFu) EventToRequest(req events.LambdaFunctionURLRequest) (*http.Request, error) {
104+
decodedBody := []byte(req.Body)
105+
if req.IsBase64Encoded {
106+
base64Body, err := base64.StdEncoding.DecodeString(req.Body)
107+
if err != nil {
108+
return nil, err
109+
}
110+
decodedBody = base64Body
111+
}
112+
113+
path := req.RawPath
114+
115+
// if RawPath empty is, populate from request context
116+
if len(path) == 0 {
117+
path = req.RequestContext.HTTP.Path
118+
}
119+
120+
if r.stripBasePath != "" && len(r.stripBasePath) > 1 {
121+
if strings.HasPrefix(path, r.stripBasePath) {
122+
path = strings.Replace(path, r.stripBasePath, "", 1)
123+
}
124+
}
125+
if !strings.HasPrefix(path, "/") {
126+
path = "/" + path
127+
}
128+
serverAddress := "https://" + req.RequestContext.DomainName
129+
if customAddress, ok := os.LookupEnv(CustomHostVariable); ok {
130+
serverAddress = customAddress
131+
}
132+
path = serverAddress + path
133+
134+
if len(req.RawQueryString) > 0 {
135+
path += "?" + req.RawQueryString
136+
} else if len(req.QueryStringParameters) > 0 {
137+
values := url.Values{}
138+
for key, value := range req.QueryStringParameters {
139+
values.Add(key, value)
140+
}
141+
path += "?" + values.Encode()
142+
}
143+
144+
httpRequest, err := http.NewRequest(
145+
strings.ToUpper(req.RequestContext.HTTP.Method),
146+
path,
147+
bytes.NewReader(decodedBody),
148+
)
149+
150+
if err != nil {
151+
fmt.Printf("Could not convert request %s:%s to http.Request\n", req.RequestContext.HTTP.Method, req.RequestContext.HTTP.Path)
152+
log.Println(err)
153+
return nil, err
154+
}
155+
156+
httpRequest.RemoteAddr = req.RequestContext.HTTP.SourceIP
157+
158+
for _, cookie := range req.Cookies {
159+
httpRequest.Header.Add("Cookie", cookie)
160+
}
161+
162+
for headerKey, headerValue := range req.Headers {
163+
for _, val := range strings.Split(headerValue, ",") {
164+
httpRequest.Header.Add(headerKey, strings.Trim(val, " "))
165+
}
166+
}
167+
168+
httpRequest.RequestURI = httpRequest.URL.RequestURI()
169+
170+
return httpRequest, nil
171+
}
172+
173+
func addToHeaderFu(req *http.Request, functionUrlRequest events.LambdaFunctionURLRequest) (*http.Request, error) {
174+
apiGwContext, err := json.Marshal(functionUrlRequest.RequestContext)
175+
if err != nil {
176+
log.Println("Could not Marshal API GW context for custom header")
177+
return req, err
178+
}
179+
req.Header.Add(APIGwContextHeader, string(apiGwContext))
180+
return req, nil
181+
}
182+
183+
func addToContextFu(ctx context.Context, req *http.Request, functionUrlRequest events.LambdaFunctionURLRequest) *http.Request {
184+
lc, _ := lambdacontext.FromContext(ctx)
185+
rc := requestContextFu{lambdaContext: lc, functionUrlProxyContext: functionUrlRequest.RequestContext}
186+
ctx = context.WithValue(ctx, ctxKey{}, rc)
187+
return req.WithContext(ctx)
188+
}
189+
190+
// GetAPIGatewayV2ContextFromContext retrieve APIGatewayProxyRequestContext from context.Context
191+
func GetFunctionUrlContextFromContext(ctx context.Context) (events.LambdaFunctionURLRequestContext, bool) {
192+
v, ok := ctx.Value(ctxKey{}).(requestContextFu)
193+
return v.functionUrlProxyContext, ok
194+
}
195+
196+
// GetRuntimeContextFromContextV2 retrieve Lambda Runtime Context from context.Context
197+
func GetRuntimeContextFromContextFu(ctx context.Context) (*lambdacontext.LambdaContext, bool) {
198+
v, ok := ctx.Value(ctxKey{}).(requestContextFu)
199+
return v.lambdaContext, ok
200+
}
201+
202+
type requestContextFu struct {
203+
lambdaContext *lambdacontext.LambdaContext
204+
functionUrlProxyContext events.LambdaFunctionURLRequestContext
205+
}

core/responseFunctionUrl.go

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
// Package core provides utility methods that help convert proxy events
2+
// into an http.Request and http.ResponseWriter
3+
package core
4+
5+
import (
6+
"bytes"
7+
"encoding/base64"
8+
"errors"
9+
"net/http"
10+
"strings"
11+
"unicode/utf8"
12+
13+
"github.com/aws/aws-lambda-go/events"
14+
)
15+
16+
// FunctionUrlResponseWriter implements http.ResponseWriter and adds the method
17+
// necessary to return an events.LambdaFunctionURLResponse object
18+
type FunctionUrlResponseWriter struct {
19+
headers http.Header
20+
body bytes.Buffer
21+
status int
22+
observers []chan<- bool
23+
}
24+
25+
// NewFunctionUrlResponseWriter returns a new FunctionUrlResponseWriter object.
26+
// The object is initialized with an empty map of headers and a
27+
// status code of -1
28+
func NewFunctionUrlResponseWriter() *FunctionUrlResponseWriter {
29+
return &FunctionUrlResponseWriter{
30+
headers: make(http.Header),
31+
status: defaultStatusCode,
32+
observers: make([]chan<- bool, 0),
33+
}
34+
}
35+
36+
func (r *FunctionUrlResponseWriter) CloseNotify() <-chan bool {
37+
ch := make(chan bool, 1)
38+
39+
r.observers = append(r.observers, ch)
40+
41+
return ch
42+
}
43+
44+
func (r *FunctionUrlResponseWriter) notifyClosed() {
45+
for _, v := range r.observers {
46+
v <- true
47+
}
48+
}
49+
50+
// Header implementation from the http.ResponseWriter interface.
51+
func (r *FunctionUrlResponseWriter) Header() http.Header {
52+
return r.headers
53+
}
54+
55+
// Write sets the response body in the object. If no status code
56+
// was set before with the WriteHeader method it sets the status
57+
// for the response to 200 OK.
58+
func (r *FunctionUrlResponseWriter) Write(body []byte) (int, error) {
59+
if r.status == defaultStatusCode {
60+
r.status = http.StatusOK
61+
}
62+
63+
// if the content type header is not set when we write the body we try to
64+
// detect one and set it by default. If the content type cannot be detected
65+
// it is automatically set to "application/octet-stream" by the
66+
// DetectContentType method
67+
if r.Header().Get(contentTypeHeaderKey) == "" {
68+
r.Header().Add(contentTypeHeaderKey, http.DetectContentType(body))
69+
}
70+
71+
return (&r.body).Write(body)
72+
}
73+
74+
// WriteHeader sets a status code for the response. This method is used
75+
// for error responses.
76+
func (r *FunctionUrlResponseWriter) WriteHeader(status int) {
77+
r.status = status
78+
}
79+
80+
// GetProxyResponse converts the data passed to the response writer into
81+
// an events.APIGatewayProxyResponse object.
82+
// Returns a populated proxy response object. If the response is invalid, for example
83+
// has no headers or an invalid status code returns an error.
84+
func (r *FunctionUrlResponseWriter) GetFunctionUrlResponse() (events.LambdaFunctionURLResponse, error) {
85+
r.notifyClosed()
86+
87+
if r.status == defaultStatusCode {
88+
return events.LambdaFunctionURLResponse{}, errors.New("Status code not set on response")
89+
}
90+
91+
var output string
92+
isBase64 := false
93+
94+
bb := (&r.body).Bytes()
95+
96+
if utf8.Valid(bb) {
97+
output = string(bb)
98+
} else {
99+
output = base64.StdEncoding.EncodeToString(bb)
100+
isBase64 = true
101+
}
102+
103+
headers := make(map[string]string)
104+
cookies := make([]string, 0)
105+
106+
for headerKey, headerValue := range http.Header(r.headers) {
107+
if strings.EqualFold("set-cookie", headerKey) {
108+
cookies = append(cookies, headerValue...)
109+
continue
110+
}
111+
headers[headerKey] = strings.Join(headerValue, ",")
112+
}
113+
114+
return events.LambdaFunctionURLResponse{
115+
StatusCode: r.status,
116+
Headers: headers,
117+
Body: output,
118+
IsBase64Encoded: isBase64,
119+
Cookies: cookies,
120+
}, nil
121+
}

core/typesFunctionUrl.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package core
2+
3+
import (
4+
"net/http"
5+
6+
"github.com/aws/aws-lambda-go/events"
7+
)
8+
9+
func FunctionUrlTimeout() events.LambdaFunctionURLResponse {
10+
return events.LambdaFunctionURLResponse{StatusCode: http.StatusGatewayTimeout}
11+
}

0 commit comments

Comments
 (0)