-
Notifications
You must be signed in to change notification settings - Fork 15
/
lmdrouter.go
322 lines (294 loc) · 9.53 KB
/
lmdrouter.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
// lmdrouter is a simple-to-use library for writing AWS Lambda functions in Go
// that listen to events of type API Gateway Proxy Request (represented by the
// `events.APIGatewayProxyRequest` type of the github.com/aws-lambda-go/events
// package).
//
// The library allows creating functions that can match requests based on their
// URI, just like an HTTP server that uses the standard
// https://golang.org/pkg/net/http/#ServeMux (or any other community-built routing
// library such as https://github.com/julienschmidt/httprouter or
// https://github.com/go-chi/chi) would. The interface provided by the library
// is very similar to these libraries and should be familiar to anyone who has
// written HTTP applications in Go.
//
//
//
// Use Case
//
//
//
// When building large cloud-native applications, there's a certain balance to
// strike when it comes to deployment of APIs. On one side of the scale, each API
// endpoint has its own lambda function. This provides the greatest flexibility,
// but is extremely difficult to maintain. On the other side of the scale, there
// can be one lambda function for the entire API. This provides the least flexibility,
// but is the easiest to maintain. Both are probably not a good idea.
//
// With `lmdrouter`, one can create small lambda functions for different aspects of
// the API. For example, if your application model contains multiple domains (e.g.
// articles, authors, topics, etc...), you can create one lambda function for each
// domain, and deploy these independently (e.g. everything below "/api/articles" is
// one lambda function, everything below "/api/authors" is another function). This
// is also useful for applications where different teams are in charge of different
// parts of the API.
//
//
//
// Features
//
//
//
// * Supports all HTTP methods.
//
// * Supports middleware functions at a global and per-resource level.
//
// * Supports path parameters with a simple ":<name>" format (e.g. "/posts/:id").
//
// * Provides ability to automatically "unmarshal" an API Gateway request to an
// arbitrary Go struct, with data coming either from path and query string
// parameters, or from the request body (only JSON requests are currently
// supported). See the documentation for the `UnmarshalRequest` function for
// more information.
//
// * Provides the ability to automatically "marshal" responses of any type to an
// API Gateway response (only JSON responses are currently generated). See the
// MarshalResponse function for more information.
//
// * Implements net/http.Handler for local development and general usage outside
// of an AWS Lambda environment.
//
package lmdrouter
import (
"context"
"fmt"
"net/http"
"net/url"
"regexp"
"strings"
"github.com/aws/aws-lambda-go/events"
)
type hasMiddleware struct {
middleware []Middleware
}
// Router is the main type of the library. Lambda routes are registered to it,
// and it's Handler method is used by the lambda to match requests and execute
// the appropriate handler.
type Router struct {
basePath string
routes map[string]route
hasMiddleware
}
type route struct {
re *regexp.Regexp
paramNames []string
methods map[string]resource
}
type resource struct {
handler Handler
hasMiddleware
}
// Middleware is a function that receives a handler function (the next function
// in the chain, possibly another middleware or the actual handler matched for
// a request), and returns a handler function. These functions are quite similar
// to HTTP middlewares in other libraries.
//
// Example middleware that logs all requests:
//
// func loggerMiddleware(next lmdrouter.Handler) lmdrouter.Handler {
// return func(ctx context.Context, req events.APIGatewayProxyRequest) (
// res events.APIGatewayProxyResponse,
// err error,
// ) {
// format := "[%s] [%s %s] [%d]%s"
// level := "INF"
// var code int
// var extra string
//
// res, err = next(ctx, req)
// if err != nil {
// level = "ERR"
// code = http.StatusInternalServerError
// extra = " " + err.Error()
// } else {
// code = res.StatusCode
// if code >= 400 {
// level = "ERR"
// }
// }
//
// log.Printf(format, level, req.HTTPMethod, req.Path, code, extra)
//
// return res, err
// }
// }
//
type Middleware func(Handler) Handler
// Handler is a request handler function. It receives a context, and the API
// Gateway's proxy request object, and returns a proxy response object and an
// error.
//
// Example:
//
// func listSomethings(ctx context.Context, req events.APIGatewayProxyRequest) (
// res events.APIGatewayProxyResponse,
// err error,
// ) {
// // parse input
// var input listSomethingsInput
// err = lmdrouter.UnmarshalRequest(req, false, &input)
// if err != nil {
// return lmdrouter.HandleError(err)
// }
//
// // call some business logic that generates an output struct
// // ...
//
// return lmdrouter.MarshalResponse(http.StatusOK, nil, output)
// }
//
type Handler func(context.Context, events.APIGatewayProxyRequest) (
events.APIGatewayProxyResponse,
error,
)
// NewRouter creates a new Router object with a base path and a list of zero or
// more global middleware functions. The base path is necessary if the lambda
// function is not going to be mounted to a domain's root (for example, if the
// function is mounted to "https://my.app/api", then the base path must be
// "/api"). Use an empty string if the function is mounted to the root of the
// domain.
func NewRouter(basePath string, middleware ...Middleware) (l *Router) {
return &Router{
basePath: basePath,
routes: make(map[string]route),
hasMiddleware: hasMiddleware{
middleware: middleware,
},
}
}
// Route registers a new route, with the provided HTTP method name and path,
// and zero or more local middleware functions.
func (l *Router) Route(method, path string, handler Handler, middleware ...Middleware) {
// check if this route already exists
r, ok := l.routes[path]
if !ok {
r = route{
methods: make(map[string]resource),
}
// create a regular expression from the path
var err error
re := fmt.Sprintf("^%s", l.basePath)
for _, part := range strings.Split(path, "/") {
if part == "" {
continue
}
// is this a parameter?
if strings.HasPrefix(part, ":") {
r.paramNames = append(r.paramNames, strings.TrimPrefix(part, ":"))
re = fmt.Sprintf("%s/([^/]+)", re)
} else {
re = fmt.Sprintf("%s/%s", re, part)
}
}
re = fmt.Sprintf("%s$", re)
r.re, err = regexp.Compile(re)
if err != nil {
panic(fmt.Sprintf("Generated invalid regex for route %s", path))
}
}
r.methods[method] = resource{
handler: handler,
hasMiddleware: hasMiddleware{
middleware: middleware,
},
}
l.routes[path] = r
}
// Handler receives a context and an API Gateway Proxy request, and handles the
// request, matching the appropriate handler and executing it. This is the
// method that must be provided to the lambda's `main` function:
//
// package main
//
// import (
// "github.com/aws/aws-lambda-go/lambda"
// "github.com/aquasecurity/lmdrouter"
// )
//
// var router *lmdrouter.Router
//
// func init() {
// router = lmdrouter.NewRouter("/api", loggerMiddleware, authMiddleware)
// router.Route("GET", "/", listSomethings)
// router.Route("POST", "/", postSomething, someOtherMiddleware)
// router.Route("GET", "/:id", getSomething)
// router.Route("PUT", "/:id", updateSomething)
// router.Route("DELETE", "/:id", deleteSomething)
// }
//
// func main() {
// lambda.Start(router.Handler)
// }
//
func (l *Router) Handler(
ctx context.Context,
req events.APIGatewayProxyRequest,
) (events.APIGatewayProxyResponse, error) {
rsrc, err := l.matchRequest(&req)
if err != nil {
return HandleError(err)
}
handler := rsrc.handler
for i := len(rsrc.middleware) - 1; i >= 0; i-- {
handler = rsrc.middleware[i](handler)
}
for i := len(l.middleware) - 1; i >= 0; i-- {
handler = l.middleware[i](handler)
}
return handler(ctx, req)
}
func (l *Router) matchRequest(req *events.APIGatewayProxyRequest) (
rsrc resource,
err error,
) {
// remove trailing slash from request path
req.Path = strings.TrimSuffix(req.Path, "/")
negErr := HTTPError{
Code: http.StatusNotFound,
Message: "No such resource",
}
// find a route that matches the request
for _, r := range l.routes {
// does the path match?
matches := r.re.FindStringSubmatch(req.Path)
if matches == nil {
continue
}
// do we have this method?
var ok bool
rsrc, ok = r.methods[req.HTTPMethod]
if !ok {
// we matched a route, but it didn't support this method. Mark negErr
// with a 405 error, but continue, we might match another route
negErr = HTTPError{
Code: http.StatusMethodNotAllowed,
Message: fmt.Sprintf("%s requests not supported by this resource", req.HTTPMethod),
}
continue
}
// process path parameters
for i, param := range r.paramNames {
if len(matches)-1 < len(r.paramNames) {
return rsrc, HTTPError{
Code: http.StatusInternalServerError,
Message: "Failed matching path parameters",
}
}
if req.PathParameters == nil {
req.PathParameters = make(map[string]string)
}
req.PathParameters[param], _ = url.QueryUnescape(matches[i+1])
}
return rsrc, nil
}
return rsrc, negErr
}