-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathengine.go
368 lines (332 loc) · 10.9 KB
/
engine.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
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
package tokay
import (
"crypto/tls"
"errors"
"fmt"
"html/template"
"net"
"net/http"
"os"
"reflect"
"runtime"
"sort"
"strings"
"sync"
"time"
render "github.com/night-codes/tokay-render"
"github.com/valyala/fasthttp"
)
type (
// Render is interface for engine.Render
Render interface {
JSON(*fasthttp.RequestCtx, int, interface{}) error
JSONP(*fasthttp.RequestCtx, int, string, interface{}) error
HTML(*fasthttp.RequestCtx, int, string, interface{}, ...string) error
XML(*fasthttp.RequestCtx, int, interface{}) error
JS(*fasthttp.RequestCtx, int, string, interface{}, ...string) error
}
// Handler is the function for handling HTTP requests.
Handler func(*Context)
// Engine manages routes and dispatches HTTP requests to the handlers of the matching routes.
Engine struct {
RouterGroup
// Default render engine
Render Render
// AppEngine usage marker
AppEngine bool
// Print debug messages to log
Debug bool
// DebugFunc is a middleware function
DebugFunc func(*Context, time.Duration)
// Close server
Close func() error
// fasthhtp server
Server *fasthttp.Server
// Enables automatic redirection if the current route can't be matched but a
// handler for the path with the trailing slash exists.
// For example if /foo is requested but a route only exists for /foo/, the
// client is redirected to /foo/ with http status code 301 for GET requests
// and 307 for all other request methods.
RedirectTrailingSlash bool
pool sync.Pool
routes map[string]*Route
stores storesMap
maxParams int
notFound []Handler
notFoundHandlers []Handler
// maxGracefulWaitTime is 'graceful shutdown' waiting duration
maxGracefulWaitTime time.Duration
}
// Config is a struct for specifying configuration options for the tokay.Engine object.
Config struct {
// Print debug messages to log
Debug bool
// DebugFunc is callback function that calls after context
DebugFunc func(*Context, time.Duration)
// Extensions to parse template files from. Defaults to [".html"].
TemplatesExtensions []string
// Directories to load templates. Default is ["templates"].
TemplatesDirs []string
// Left templates delimiter, defaults to {{.
LeftTemplateDelimiter string
// Right templates delimiter, defaults to }}.
RightTemplateDelimiter string
// Funcs is a slice of FuncMaps to apply to the template upon compilation. This is useful for helper functions. Defaults to [].
TemplatesFuncs template.FuncMap
// MaxGracefulWaitTime is 'graceful shutdown' waiting duration
MaxGracefulWaitTime time.Duration
}
)
var (
// AppEngine usage marker
AppEngine bool
// Methods lists all supported HTTP methods by Engine.
Methods = []string{
"HEAD",
"GET",
"POST",
"CONNECT",
"DELETE",
"OPTIONS",
"PATCH",
"PUT",
"TRACE",
}
)
// New creates a new Engine object.
func New(config ...*Config) *Engine {
var r *render.Render
var cfgDebug bool
var maxGracefulWaitTime = 10 * time.Second
var cfgDebugFunc func(*Context, time.Duration)
rCfg := &render.Config{}
if len(config) != 0 && config[0] != nil {
if config[0].MaxGracefulWaitTime != 0 {
maxGracefulWaitTime = config[0].MaxGracefulWaitTime
}
if len(config[0].TemplatesDirs) != 0 {
rCfg = &render.Config{
Directories: config[0].TemplatesDirs,
Extensions: config[0].TemplatesExtensions,
Delims: render.Delims{
Left: config[0].LeftTemplateDelimiter,
},
Funcs: config[0].TemplatesFuncs,
}
}
cfgDebug = config[0].Debug
cfgDebugFunc = config[0].DebugFunc
}
r = render.New(rCfg)
engine := &Engine{
AppEngine: AppEngine,
routes: make(map[string]*Route),
stores: *newStoresMap(),
Render: r,
RedirectTrailingSlash: true,
Debug: cfgDebug,
DebugFunc: cfgDebugFunc,
Server: &fasthttp.Server{},
maxGracefulWaitTime: maxGracefulWaitTime,
Close: func() error {
return errors.New("server is not runned")
},
}
engine.RouterGroup = *newRouteGroup("", engine, make([]Handler, 0))
engine.NotFound(MethodNotAllowedHandler, NotFoundHandler)
engine.pool.New = func() interface{} {
return &Context{
pvalues: make([]string, engine.maxParams),
engine: engine,
}
}
return engine
}
func runmsg(addr string, ec chan error, message string) (err error) {
if message != "" {
select {
case err = <-ec:
return
case <-time.After(time.Second / 4):
if strings.Contains(message, "%s") {
fmt.Printf(message+"\n", addr)
} else {
fmt.Println(message)
}
}
}
err = <-ec
return
}
// Run attaches the engine to a fasthttp server and starts listening and serving HTTP requests.
// It is a shortcut for engine.Server.ListenAndServe(addr, engine.HandleRequest) Note: this method will block the
// calling goroutine indefinitely unless an error happens.
func (engine *Engine) Run(addr string, message ...string) error {
ec := make(chan error)
go func() {
engine.Server.Handler = engine.HandleRequest
ec <- listenAndServe(engine, addr)
}()
return runmsg(addr, ec, append(message, "HTTP server started at %s")[0])
}
// RunTLS attaches the engine to a fasthttp server and starts listening and
// serving HTTPS (secure) requests. It is a shortcut for
// engine.Server.ListenAndServeTLS(addr, certFile, keyFile)
// Note: this method will block the calling goroutine indefinitely unless an error happens.
func (engine *Engine) RunTLS(addr string, certFile, keyFile string, message ...string) error {
ec := make(chan error)
go func() {
engine.Server.Handler = engine.HandleRequest
ec <- listenAndServeTLS(engine, addr, certFile, keyFile)
}()
return runmsg(addr, ec, append(message, "HTTPS server started at %s")[0])
}
// RunUnix attaches the engine to a fasthttp server and starts listening and
// serving HTTP requests through the specified unix socket (ie. a file).
// Note: this method will block the calling goroutine indefinitely unless an error happens.
func (engine *Engine) RunUnix(addr string, mode os.FileMode, message ...string) error {
ec := make(chan error)
go func() {
engine.Server.Handler = engine.HandleRequest
ec <- engine.Server.ListenAndServeUNIX(addr, mode)
}()
return runmsg(addr, ec, append(message, "Unix server started at %s")[0])
}
// Serve serves incoming connections from the given listener using the given handler.
// Serve blocks until the given listener returns permanent error.
func (engine *Engine) Serve(addr string, cfg *tls.Config, message ...string) error {
ec := make(chan error)
go func() {
ln, err := net.Listen("tcp4", addr)
if err != nil {
panic(err)
}
lnTls := tls.NewListener(ln, cfg)
ec <- fasthttp.Serve(lnTls, engine.HandleRequest)
}()
return runmsg(addr, ec, append(message, "Server started at %s")[0])
}
// HandleRequest handles the HTTP request.
func (engine *Engine) HandleRequest(ctx *fasthttp.RequestCtx) {
start := time.Now()
c := engine.pool.Get().(*Context)
c.init(ctx)
c.handlers, c.pnames = engine.find(string(ctx.Method()), string(ctx.Path()), c.pvalues)
fin := func() {
c.Next()
engine.pool.Put(c)
engine.debug(fmt.Sprintf("%-21s | %d | %9v | %-7s %-25s ", time.Now().Format("2006/01/02 - 15:04:05"), c.Response.StatusCode(), time.Since(start), string(ctx.Method()), string(ctx.Path())))
if engine.DebugFunc != nil {
engine.DebugFunc(c, time.Since(start))
}
}
fin()
}
// Route returns the named route.
// Nil is returned if the named route cannot be found.
func (engine *Engine) Route(name string) *Route {
return engine.routes[name]
}
// Use appends the specified handlers to the engine and shares them with all routes.
func (engine *Engine) Use(handlers ...Handler) {
engine.RouterGroup.Use(handlers...)
engine.notFoundHandlers = combineHandlers(engine.handlers, engine.notFound)
}
// NotFound specifies the handlers that should be invoked when the engine cannot find any route matching a request.
// Note that the handlers registered via Use will be invoked first in this case.
func (engine *Engine) NotFound(handlers ...Handler) {
engine.notFound = handlers
engine.notFoundHandlers = combineHandlers(engine.handlers, engine.notFound)
}
// handleError is the error handler for handling any unhandled errors.
func (engine *Engine) handleError(c *Context, err error) {
c.Error(err.Error(), http.StatusInternalServerError)
}
func (engine *Engine) add(method, path string, handlers []Handler) {
for _, h := range handlers {
engine.debug(fmt.Sprintf("%-7s %-25s -->", method, path), runtime.FuncForPC(reflect.ValueOf(h).Pointer()).Name())
}
store := engine.stores.Get(method)
if store == nil {
store = newStore()
engine.stores.Set(method, store)
}
if n := store.Add(path, handlers); n > engine.maxParams {
engine.maxParams = n
}
}
func (engine *Engine) find(method, path string, pvalues []string) (handlers []Handler, pnames []string) {
var hh interface{}
if store := engine.stores.Get(method); store != nil {
if hh, pnames = store.Get(path, pvalues); hh != nil {
return hh.([]Handler), pnames
}
}
return engine.notFoundHandlers, pnames
}
func (engine *Engine) findAllowedMethods(path string) map[string]bool {
methods := make(map[string]bool)
pvalues := make([]string, engine.maxParams)
engine.stores.Range(func(m string, store routeStore) {
if handlers, _ := store.Get(path, pvalues); handlers != nil {
methods[m] = true
}
})
return methods
}
func (engine *Engine) debug(text ...interface{}) {
if engine.Debug {
debug.Println(text...)
}
}
// NotFoundHandler returns a 404 HTTP error indicating a request has no matching route.
func NotFoundHandler(c *Context) {
if c.engine.RedirectTrailingSlash && redirectTrailingSlash(c) {
return
}
c.String(http.StatusNotFound, http.StatusText(http.StatusNotFound))
}
// MethodNotAllowedHandler handles the situation when a request has matching route without matching HTTP method.
// In this case, the handler will respond with an Allow HTTP header listing the allowed HTTP methods.
// Otherwise, the handler will do nothing and let the next handler (usually a NotFoundHandler) to handle the problem.
func MethodNotAllowedHandler(c *Context) {
methods := c.Engine().findAllowedMethods(string(c.Path()))
if len(methods) == 0 {
return
}
methods["OPTIONS"] = true
ms := make([]string, len(methods))
i := 0
for method := range methods {
ms[i] = method
i++
}
sort.Strings(ms)
c.Response.Header.Set("Allow", strings.Join(ms, ", "))
if string(c.Method()) != "OPTIONS" {
c.Response.SetStatusCode(http.StatusMethodNotAllowed)
}
c.Abort()
return
}
func redirectTrailingSlash(c *Context) bool {
if c.GetHeader("Redirect-Trailing-Slash") != "" {
return false
}
path := c.Path()
statusCode := 301 // Permanent redirect, request with GET method
if c.Method() != "GET" {
statusCode = 307
}
if length := len(path); length > 1 && path[length-1] == '/' {
path = path[:length-1]
} else {
path = path + "/"
}
methods := c.Engine().findAllowedMethods(path)
if len(methods) == 0 {
return false
}
c.Redirect(statusCode, path)
return true
}