github.com/gramework/gramework@v1.8.1-0.20231027140105-82555c9057f5/router.go (about) 1 // Copyright 2017-present Kirill Danshin and Gramework contributors 2 // Copyright 2019-present Highload LTD (UK CN: 11893420) 3 // 4 // Licensed under the Apache License, Version 2.0 (the "License"); 5 // you may not use this file except in compliance with the License. 6 // You may obtain a copy of the License at 7 // 8 // http://www.apache.org/licenses/LICENSE-2.0 9 // 10 11 package gramework 12 13 import ( 14 "fmt" 15 "path" 16 "reflect" 17 "runtime" 18 "strings" 19 20 "github.com/apex/log" 21 "github.com/valyala/fasthttp" 22 ) 23 24 // JSON register internal handler that sets json content type 25 // and serves given handler with GET method 26 func (r *Router) JSON(route string, handler interface{}) *Router { 27 h := r.determineHandler(handler) 28 r.GET(route, jsonHandler(h)) 29 30 return r 31 } 32 33 // GET registers a handler for a GET request to the given route 34 func (r *Router) GET(route string, handler interface{}) *Router { 35 r.Handle(MethodGET, route, handler) 36 return r 37 } 38 39 // Forbidden serves 403 on route it registered on 40 func (r *Router) Forbidden(ctx *Context) { 41 ctx.Forbidden() 42 } 43 44 // DELETE registers a handler for a DELETE request to the given route 45 func (r *Router) DELETE(route string, handler interface{}) *Router { 46 r.Handle(MethodDELETE, route, handler) 47 return r 48 } 49 50 // HEAD registers a handler for a HEAD request to the given route 51 func (r *Router) HEAD(route string, handler interface{}) *Router { 52 r.Handle(MethodHEAD, route, handler) 53 return r 54 } 55 56 // OPTIONS registers a handler for a OPTIONS request to the given route 57 func (r *Router) OPTIONS(route string, handler interface{}) *Router { 58 r.Handle(MethodOPTIONS, route, handler) 59 return r 60 } 61 62 // PUT registers a handler for a PUT request to the given route 63 func (r *Router) PUT(route string, handler interface{}) *Router { 64 r.Handle(MethodPUT, route, handler) 65 return r 66 } 67 68 // POST registers a handler for a POST request to the given route 69 func (r *Router) POST(route string, handler interface{}) *Router { 70 r.Handle(MethodPOST, route, handler) 71 return r 72 } 73 74 // PATCH registers a handler for a PATCH request to the given route 75 func (r *Router) PATCH(route string, handler interface{}) *Router { 76 r.Handle(MethodPATCH, route, handler) 77 return r 78 } 79 80 // ServeFile serves a file on a given route 81 func (r *Router) ServeFile(route, file string) *Router { 82 r.Handle(MethodGET, route, func(ctx *Context) { 83 ctx.SendFile(file) 84 }) 85 return r 86 } 87 88 // SPAIndex serves an index file or handler on any unregistered route 89 func (r *Router) SPAIndex(pathOrHandler interface{}) *Router { 90 switch v := pathOrHandler.(type) { 91 case string: 92 r.NotFound(func(ctx *Context) { 93 ctx.HTML() 94 ctx.SendFile(v) 95 }) 96 default: 97 r.NotFound(r.determineHandler(v)) 98 } 99 return r 100 } 101 102 // Sub let you quickly register subroutes with given prefix 103 // like app.Sub("v1").GET("route", "hi"), that give you /v1/route 104 // registered 105 func (r *Router) Sub(path string) *SubRouter { 106 return &SubRouter{ 107 prefix: path, 108 parent: r, 109 prefixes: []string{path}, 110 } 111 } 112 113 func (r *Router) handleReg(method, route string, handler interface{}, prefixes []string) { 114 r.initRouter() 115 r.app.internalLog.Debugf("registering %s %s", method, route) 116 typedHandler := r.determineHandler(handler) 117 for prefix := range r.app.protectedPrefixes { 118 if strings.HasPrefix(strings.TrimLeft(route, "/"), strings.TrimLeft(prefix, "/")) { 119 r.app.internalLog. 120 WithField("route", route). 121 WithField("method", method). 122 Info("[Gramework Protection] Protection enabled for a new route") 123 r.app.protectedEndpoints[route] = struct{}{} 124 typedHandler = r.app.protectionMiddleware(typedHandler) 125 break 126 } 127 } 128 if path.Clean(route) == "/" { 129 ok := r.setRootFastpath(method, staticHandler{ 130 handle: typedHandler, 131 prefixes: prefixes, 132 }) 133 if ok { 134 return 135 } 136 } 137 r.router.Handle(method, route, typedHandler, prefixes) 138 } 139 140 func (r *Router) getEFuncStrHandler(h func() string) func(*Context) { 141 return func(ctx *Context) { 142 ctx.Response.SetBodyRaw([]byte(h())) 143 } 144 } 145 146 // substring -> full name 147 var internalShortcuts = map[string]string{ 148 "#g.(*App).ServeDirCustom": "#g.(*App).ServeDirCustom", 149 } 150 151 func handlerName(h interface{}) string { 152 v := reflect.ValueOf(h) 153 if v.Kind() != reflect.Func { 154 return fmt.Sprintf("<raw %T>", h) 155 } 156 funcDesc := runtime.FuncForPC(v.Pointer()) 157 file, line := funcDesc.FileLine(v.Pointer()) 158 pathidx := strings.Index(file, "/go/src/") 159 if strings.Contains(file, "/go/src") && len(file) > pathidx+len("/go/src/") { 160 file = file[strings.Index(file, "/go/src/")+8:] 161 } 162 file = strings.Replace(file, "github.com/gramework/gramework/", "#g/", -1) 163 file = strings.Replace(file, "github.com/gramework/gramework.", "#g.", -1) 164 fName := funcDesc.Name() 165 fName = strings.Replace(fName, "github.com/gramework/gramework/", "#g/", -1) 166 fName = strings.Replace(fName, "github.com/gramework/gramework.", "#g.", -1) 167 for substr, fullName := range internalShortcuts { 168 if strings.Contains(fName, substr) { 169 fName = fullName 170 break 171 } 172 } 173 name := fmt.Sprintf("%s@%s:%v", fName, file, line) 174 return name 175 } 176 177 // Handle registers a new request handle with the given path and method. 178 // For GET, POST, PUT, PATCH and DELETE requests the respective shortcut functions can be used. 179 // This function is intended for bulk loading and to allow the usage of less frequently used, 180 // non-standardized or custom methods (e.g. for internal communication with a proxy). 181 func (r *Router) Handle(method, route string, handler interface{}) *Router { 182 r.app.internalLog.WithFields(log.Fields{ 183 "handler": handlerName(handler), 184 "method": method, 185 "route": route, 186 }).Debug("registering route") 187 r.handleReg(method, route, handler, nil) 188 return r 189 } 190 191 func (r *Router) getFmtVHandler(v interface{}) func(*Context) { 192 cache := []byte(fmt.Sprintf("%v", v)) 193 return func(ctx *Context) { 194 ctx.Response.SetBodyRaw(cache) 195 } 196 } 197 198 func (r *Router) getStringServer(str string) func(*Context) { 199 b := []byte(str) 200 return func(ctx *Context) { 201 ctx.Response.SetBodyRaw(b) 202 } 203 } 204 205 func (r *Router) getHTMLServer(str HTML) func(*Context) { 206 b := []byte(str) 207 return func(ctx *Context) { 208 if _, err := ctx.HTML().Write(b); err != nil { 209 // connection broken 210 ctx.Error("", 500) 211 } 212 } 213 } 214 215 func (r *Router) getJSONServer(str JSON) func(*Context) { 216 b := []byte(str) 217 return func(ctx *Context) { 218 ctx.SetContentType(jsonCTshort) 219 ctx.Response.SetBodyRaw(b) 220 } 221 } 222 223 func (r *Router) getBytesServer(b []byte) func(*Context) { 224 return func(ctx *Context) { 225 ctx.Response.SetBodyRaw(b) 226 } 227 } 228 229 func (r *Router) getFmtDHandler(v interface{}) func(*Context) { 230 const fmtD = "%d" 231 res := []byte(fmt.Sprintf(fmtD, v)) 232 return func(ctx *Context) { 233 ctx.Response.SetBodyRaw(res) 234 } 235 } 236 237 func (r *Router) getFmtFHandler(v interface{}) func(*Context) { 238 const fmtF = "%f" 239 res := []byte(fmt.Sprintf(fmtF, v)) 240 return func(ctx *Context) { 241 ctx.Response.SetBodyRaw(res) 242 } 243 } 244 245 // PanicHandler set a handler for unhandled panics 246 func (r *Router) PanicHandler(panicHandler func(*Context, interface{})) { 247 r.initRouter() 248 r.router.PanicHandler = panicHandler 249 } 250 251 // NotFound set a handler which is called when no matching route is found 252 func (r *Router) NotFound(notFoundHandler func(*Context)) { 253 r.initRouter() 254 r.router.NotFound = notFoundHandler 255 } 256 257 // HandleMethodNotAllowed changes HandleMethodNotAllowed mode in the router 258 func (r *Router) HandleMethodNotAllowed(newValue bool) (oldValue bool) { 259 r.initRouter() 260 oldValue = r.router.HandleMethodNotAllowed 261 r.router.HandleMethodNotAllowed = newValue 262 return 263 } 264 265 // HandleOPTIONS changes HandleOPTIONS mode in the router 266 func (r *Router) HandleOPTIONS(newValue bool) (oldValue bool) { 267 r.initRouter() 268 oldValue = r.router.HandleOPTIONS 269 r.router.HandleOPTIONS = newValue 270 return 271 } 272 273 // HTTP router returns a router instance that work only on HTTP requests 274 func (r *Router) HTTP() *Router { 275 if r.root != nil { 276 return r.root.HTTP() 277 } 278 r.mu.Lock() 279 if r.httprouter == nil { 280 r.httprouter = &Router{ 281 router: newRouter(), 282 app: r.app, 283 root: r, 284 } 285 } 286 r.mu.Unlock() 287 288 return r.httprouter 289 } 290 291 // HTTPS router returns a router instance that work only on HTTPS requests 292 func (r *Router) HTTPS() *Router { 293 if r.root != nil { 294 return r.root.HTTPS() 295 } 296 r.mu.Lock() 297 if r.httpsrouter == nil { 298 r.httpsrouter = &Router{ 299 router: newRouter(), 300 app: r.app, 301 root: r, 302 } 303 } 304 r.mu.Unlock() 305 306 return r.httpsrouter 307 } 308 309 // ServeFiles serves files from the given file system root. 310 // The path must end with "/*filepath", files are then served from the local 311 // path /defined/root/dir/*filepath. 312 // For example if root is "/etc" and *filepath is "passwd", the local file 313 // "/etc/passwd" would be served. 314 // Internally a http.FileServer is used, therefore http.NotFound is used instead 315 // of the Router's NotFound handler. 316 // router.ServeFiles("/src/*filepath", "/var/www") 317 func (r *Router) ServeFiles(path string, rootPath string) { 318 r.router.ServeFiles(path, rootPath, nil) 319 } 320 321 // Lookup allows the manual lookup of a method + path combo. 322 // This is e.g. useful to build a framework around this router. 323 // If the path was found, it returns the handle function and the path parameter 324 // values. Otherwise the third return value indicates whether a redirection to 325 // the same path with an extra / without the trailing slash should be performed. 326 func (r *Router) Lookup(method, path string, ctx *Context) (RequestHandler, bool) { 327 if path == "/" { 328 h, found := r.getRootFastpath(method) 329 if found { 330 return h.handle, true 331 } 332 } 333 return r.router.Lookup(method, path, ctx) 334 } 335 336 // MethodNotAllowed sets MethodNotAllowed handler 337 func (r *Router) MethodNotAllowed(c func(ctx *Context)) { 338 r.router.MethodNotAllowed = c 339 } 340 341 // Allowed returns Allow header's value used in OPTIONS responses 342 func (r *Router) Allowed(path, reqMethod string) (allow string) { 343 return r.router.Allowed(path, reqMethod) 344 } 345 346 // Handler makes the router implement the fasthttp.ListenAndServe interface. 347 func (r *Router) Handler() func(*Context) { 348 return r.handler 349 } 350 351 func (r *Router) setRootFastpath(method string, h staticHandler) (ok bool) { 352 methodIdx := r.methodToIdx(method) 353 if methodIdx < 0 { 354 return false 355 } 356 r.mu.Lock() 357 if r.rootHandler == nil { 358 r.rootHandler = make([]staticHandler, 32) 359 } 360 r.rootHandler[methodIdx] = h 361 r.mu.Unlock() 362 return true 363 } 364 365 func (r *Router) getRootFastpath(method string) (h staticHandler, found bool) { 366 if r.rootHandler == nil { 367 return zeroStaticHandler, false 368 } 369 methodIdx := r.methodToIdx(method) 370 if methodIdx < 0 { 371 return zeroStaticHandler, false 372 } 373 374 return r.rootHandler[methodIdx], r.rootHandler[methodIdx].handle != nil 375 } 376 377 var zeroStaticHandler = staticHandler{} 378 379 func (r *Router) methodToIdx(method string) int { 380 return methodToIdx(method) 381 } 382 383 func methodToIdx(method string) int { 384 switch method { 385 case GET: 386 return 0 387 case HEAD: 388 return 1 389 case OPTIONS: 390 return 2 391 case POST: 392 return 3 393 case PUT: 394 return 4 395 case PATCH: 396 return 5 397 case DELETE: 398 return 6 399 case CONNECT: 400 return 7 401 default: 402 return -1 403 } 404 } 405 406 func methodByIdx(method int) string { 407 switch method { 408 case 0: 409 return GET 410 case 1: 411 return HEAD 412 case 2: 413 return OPTIONS 414 case 3: 415 return POST 416 case 4: 417 return PUT 418 case 5: 419 return PATCH 420 case 6: 421 return DELETE 422 case 7: 423 return CONNECT 424 default: 425 return "<unknown>" 426 } 427 } 428 429 func (r *Router) handleReq(router *Router, method, path string, ctx *Context) (stop bool) { 430 if supported, shouldStop := router.fastpath(method, path, ctx); supported { 431 return shouldStop 432 } 433 434 return router.defaultHandlingPath(router, method, path, ctx) 435 } 436 437 func (r *Router) fastpath(method, path string, ctx *Context) (supported, shouldStop bool) { 438 if path == "/" { 439 h, found := r.getRootFastpath(method) 440 if !found { 441 return false, false 442 } 443 444 if h.handle != nil { 445 h.handle(ctx) 446 } else { 447 r.default404(ctx) 448 } 449 return true, true 450 } 451 452 return false, false 453 } 454 455 func (r *Router) defaultHandlingPath(router *Router, method, path string, ctx *Context) (stop bool) { 456 handler, tsr := router.router.Lookup(method, path, ctx) 457 if handler != nil && router.handle(path, method, ctx, handler, tsr, false) { 458 return true 459 } 460 isStatic := r.router.routeIsStatic(method, path) 461 462 if router.router.RedirectFixedPath { 463 if isStatic { 464 lowerPath := strings.ToLower(path) 465 sh, _, found := router.router.lookupStatic(method, lowerPath) 466 if found { 467 code := redirectCode 468 if method != GET { 469 code = temporaryRedirectCode 470 } 471 472 uri := r.pathAppendQueryFromCtx([]byte(sh.originalRoute), ctx) 473 474 ctx.SetStatusCode(code) 475 ctx.Response.Header.SetBytesV("Location", uri) 476 return true 477 } 478 } 479 480 if root, ok := router.router.Trees[method]; ok && root != nil { 481 fixedPath, found := root.FindCaseInsensitivePath( 482 CleanPath(path), 483 router.router.RedirectTrailingSlash, 484 ) 485 486 if found { 487 code := redirectCode 488 if method != GET { 489 code = temporaryRedirectCode 490 } 491 492 uri := r.pathAppendQueryFromCtx([]byte(fixedPath), ctx) 493 494 ctx.SetStatusCode(code) 495 ctx.Response.Header.SetBytesV("Location", uri) 496 return true 497 } 498 } 499 } 500 501 if isStatic { 502 if method == OPTIONS { 503 // Handle OPTIONS requests 504 if r.router.HandleOPTIONS { 505 if allow := r.getStaticAllowed(method, path, ctx); len(allow) > 0 { 506 ctx.Response.Header.Set(HeaderAllow, allow) 507 } 508 } 509 } else { 510 if isStatic { 511 if allow := r.getStaticAllowed(method, path, ctx); len(allow) > 0 { 512 ctx.Response.Header.Set(HeaderAllow, allow) 513 if r.router.MethodNotAllowed != nil { 514 r.router.MethodNotAllowed(ctx) 515 } else { 516 ctx.SetStatusCode(fasthttp.StatusMethodNotAllowed) 517 ctx.SetContentTypeBytes(DefaultContentType) 518 ctx.SetBodyString(fasthttp.StatusMessage(fasthttp.StatusMethodNotAllowed)) 519 } 520 return true 521 } 522 } 523 } 524 } 525 526 if router.router.NotFound != nil { 527 router.router.NotFound(ctx) 528 return true 529 } 530 return false 531 } 532 533 func (r *Router) handler(ctx *Context) { 534 path := string(ctx.Path()) 535 method := string(ctx.Method()) 536 537 switch ctx.IsTLS() { 538 case true: 539 if r.httpsrouter != nil { 540 if r.handleReq(r.httpsrouter, method, path, ctx) { 541 return 542 } 543 } 544 case false: 545 if r.httprouter != nil { 546 if r.handleReq(r.httprouter, method, path, ctx) { 547 return 548 } 549 } 550 } 551 if r.handleReq(r, method, path, ctx) { 552 return 553 } 554 r.default404(ctx) 555 } 556 557 func (r *Router) default404(ctx *Context) { 558 ctx.Error(fasthttp.StatusMessage(fasthttp.StatusNotFound), fasthttp.StatusNotFound) 559 } 560 561 // pathAppendQueryFromCtx append query string to path in bytes 562 func (r *Router) pathAppendQueryFromCtx(path []byte, ctx *Context) []byte { 563 queryBuf := ctx.URI().QueryString() 564 if len(queryBuf) > zero { 565 path = append(path, QuestionMark...) 566 path = append(path, queryBuf...) 567 } 568 569 return path 570 } 571 572 // pathAppendQueryFromCtx append query string to path in bytes 573 func (r *Router) trimTrailingSlash(path string) (string, bool) { 574 if len(path) > one && path[len(path)-one] == SlashByte { 575 return path[:len(path)-one], true 576 } 577 578 return path, false 579 } 580 581 func (r *Router) getStaticAllowed(method, path string, ctx *Context) string { 582 allowed := "" 583 for m, p := range r.router.StaticHandlers { 584 if m == methodToIdx(OPTIONS) { 585 continue 586 } 587 588 found := false 589 if _, ok := p[path]; ok { 590 found = true 591 } else if _, ok := p[strings.ToLower(path)]; ok { 592 found = true 593 } 594 if found { 595 if len(allowed) > 0 { 596 allowed += ", " 597 } 598 allowed += methodByIdx(m) 599 } 600 } 601 if allowed != "" { 602 allowed += ", OPTIONS" 603 } 604 return allowed 605 } 606 607 func (r *Router) handle(path, method string, ctx *Context, handler func(ctx *Context), redirectTrailingSlashs bool, isRootRouter bool) (handlerFound bool) { 608 if r.router.PanicHandler != nil { 609 defer r.router.Recv(ctx, nil) 610 } 611 612 isStatic := r.router.routeIsStatic(method, path) 613 614 if isStatic { 615 if f, ok := r.router.StaticHandlers[methodToIdx(method)][path]; ok { 616 ctx.subPrefixes = f.prefixes 617 f.handle(ctx) 618 return true 619 } 620 } 621 622 if method != CONNECT && path != PathSlash { 623 code := redirectCode // Permanent redirect, request with GET method 624 if method != GET { 625 // Temporary redirect, request with same method 626 // As of Go 1.3, Go does not support status code 308. 627 code = temporaryRedirectCode 628 } 629 630 if r.router.RedirectTrailingSlash { 631 if fixedPath, ok := r.trimTrailingSlash(path); ok { 632 if _, ok := r.router.StaticHandlers[methodToIdx(method)][fixedPath]; ok { 633 uri := r.pathAppendQueryFromCtx([]byte(fixedPath), ctx) 634 635 ctx.SetStatusCode(code) 636 ctx.Response.Header.SetBytesV("Location", uri) 637 return ok 638 } 639 } 640 } 641 642 if r.router.RedirectFixedPath { 643 fixedPath := CleanPath(strings.ToLower(path)) 644 645 if r.router.RedirectTrailingSlash { 646 fixedPath, _ = r.trimTrailingSlash(fixedPath) 647 } 648 649 if isStatic { 650 if _, ok := r.router.StaticHandlers[methodToIdx(method)][fixedPath]; ok { 651 uri := r.pathAppendQueryFromCtx([]byte(fixedPath), ctx) 652 653 ctx.SetStatusCode(code) 654 ctx.Response.Header.SetBytesV("Location", uri) 655 return true 656 } 657 } 658 } 659 } 660 661 if root := r.router.Trees[method]; root != nil { 662 if f, prefixes, tsr := root.GetValue(path, ctx, string(ctx.Method())); f != nil { 663 ctx.subPrefixes = prefixes 664 f(ctx) 665 return true 666 } else if method != CONNECT && path != PathSlash { 667 code := redirectCode // Permanent redirect, request with GET method 668 if method != GET { 669 // Temporary redirect, request with same method 670 // As of Go 1.3, Go does not support status code 308. 671 code = temporaryRedirectCode 672 } 673 674 if tsr && r.router.RedirectTrailingSlash { 675 if trimmedPath, ok := r.trimTrailingSlash(path); ok { 676 fixedPath := r.pathAppendQueryFromCtx([]byte(trimmedPath), ctx) 677 678 ctx.SetStatusCode(code) 679 ctx.Response.Header.SetBytesV("Location", fixedPath) 680 return false 681 } 682 } 683 684 // Try to fix the request path 685 if r.router.RedirectFixedPath { 686 fixedPath, found := root.FindCaseInsensitivePath( 687 CleanPath(path), 688 r.router.RedirectTrailingSlash, 689 ) 690 691 if found && len(fixedPath) > 0 { 692 fixedPath = r.pathAppendQueryFromCtx(fixedPath, ctx) 693 ctx.SetStatusCode(code) 694 ctx.Response.Header.SetBytesV("Location", fixedPath) 695 return true 696 } 697 } 698 } 699 } 700 701 if !isRootRouter { 702 return false 703 } 704 705 if method == OPTIONS { 706 // Handle OPTIONS requests 707 if r.router.HandleOPTIONS { 708 if isStatic { 709 if allow := r.getStaticAllowed(method, path, ctx); len(allow) > 0 { 710 ctx.Response.Header.Set(HeaderAllow, allow) 711 } 712 } 713 if allow := r.router.Allowed(path, method); len(allow) > zero { 714 ctx.Response.Header.Set(HeaderAllow, allow) 715 return true 716 } 717 } 718 } else { 719 if isStatic { 720 if allow := r.getStaticAllowed(method, path, ctx); len(allow) > 0 { 721 ctx.Response.Header.Set(HeaderAllow, allow) 722 if r.router.MethodNotAllowed != nil { 723 r.router.MethodNotAllowed(ctx) 724 } else { 725 ctx.SetStatusCode(fasthttp.StatusMethodNotAllowed) 726 ctx.SetContentTypeBytes(DefaultContentType) 727 ctx.SetBodyString(fasthttp.StatusMessage(fasthttp.StatusMethodNotAllowed)) 728 } 729 return true 730 } 731 } 732 // Handle 405 733 if r.router.HandleMethodNotAllowed { 734 if allow := r.router.Allowed(path, method); len(allow) > zero { 735 ctx.Response.Header.Set(HeaderAllow, allow) 736 if r.router.MethodNotAllowed != nil { 737 r.router.MethodNotAllowed(ctx) 738 } else { 739 ctx.SetStatusCode(fasthttp.StatusMethodNotAllowed) 740 ctx.SetContentTypeBytes(DefaultContentType) 741 ctx.SetBodyString(fasthttp.StatusMessage(fasthttp.StatusMethodNotAllowed)) 742 } 743 return true 744 } 745 } 746 } 747 748 return false 749 } 750 751 // Redir sends 301 redirect to the given url 752 // 753 // it's equivalent to 754 // 755 // ctx.Redirect(url, 301) 756 func (r *Router) Redir(route, url string) { 757 r.GET(route, func(ctx *Context) { 758 ctx.Redirect(route, redirectCode) 759 }) 760 }