// ⚡️ Fiber is an Express inspired web framework written in Go with ☕️ // 📝 GitHub Repository: https://github.com/gofiber/fiber // 📌 API Documentation: https://docs.gofiber.io package fiber import ( "bytes" "encoding/hex" "sync" "github.com/gofiber/utils/v2" utilsbytes "github.com/gofiber/utils/v2/bytes" "github.com/valyala/bytebufferpool" "github.com/valyala/fasthttp" "github.com/gofiber/fiber/v3/binder" ) // Pool for redirection var ( redirectPool = sync.Pool{ New: func() any { return &Redirect{ status: StatusSeeOther, messages: make(redirectionMsgs, 0), } }, } oldInputPool = sync.Pool{ New: func() any { return make(map[string]string) }, } ) const maxPoolableMapSize = 64 // FlashCookieName Cookie name to send flash messages when to use redirection. const ( FlashCookieName = "fiber_flash" ) var flashCookieNeedle = []byte(FlashCookieName + "=") // hasFlashCookie is on the request hot path and runs on every request/response cycle. // Keep this cheap for users who don't use flash messages: // 1) a fast raw-header prefilter to avoid unnecessary cookie parsing, // 2) an exact cookie lookup to avoid prefix false positives (e.g. fiber_flashX). func hasFlashCookie(header *fasthttp.RequestHeader) bool { rawHeaders := header.RawHeaders() if len(rawHeaders) == 0 { return false } if !bytes.Contains(rawHeaders, flashCookieNeedle) { return false } return header.Cookie(FlashCookieName) != nil } // redirectionMsgs is a struct that used to store flash messages and old input data in cookie using MSGP. // msgp -file="redirect.go" -o="redirect_msgp.go" -unexported // Cookie payloads are limited to ~4KB, so keep flash message counts bounded but usable. // //msgp:limit arrays:256 maps:32 marshal:true //msgp:ignore Redirect RedirectConfig OldInputData FlashMessage type redirectionMsg struct { key string value string level uint8 isOldInput bool } type redirectionMsgs []redirectionMsg // OldInputData is a struct that holds the old input data. type OldInputData struct { Key string Value string } // FlashMessage is a struct that holds the flash message data. type FlashMessage struct { Key string Value string Level uint8 } // Redirect is a struct that holds the redirect data. type Redirect struct { c *DefaultCtx // Embed ctx messages redirectionMsgs // Flash messages and old input data status int // Status code of redirection. Default: 303 StatusSeeOther } // RedirectConfig A config to use with Redirect().Route() // You can specify queries or route parameters. // NOTE: We don't use net/url to parse parameters because of it has poor performance. You have to pass map. type RedirectConfig struct { Params Map // Route parameters Queries map[string]string // Query map } // AcquireRedirect return default Redirect reference from the redirect pool func AcquireRedirect() *Redirect { redirect, ok := redirectPool.Get().(*Redirect) if !ok { panic(errRedirectTypeAssertion) } return redirect } // ReleaseRedirect returns c acquired via Redirect to redirect pool. // // It is forbidden accessing req and/or its members after returning // it to redirect pool. func ReleaseRedirect(r *Redirect) { r.release() redirectPool.Put(r) } func (r *Redirect) release() { r.status = StatusSeeOther r.messages = r.messages[:0] r.c = nil } func acquireOldInput() map[string]string { oldInput, ok := oldInputPool.Get().(map[string]string) if !ok { return make(map[string]string) } return oldInput } func releaseOldInput(oldInput map[string]string) { if len(oldInput) > maxPoolableMapSize { return } clear(oldInput) oldInputPool.Put(oldInput) } // Status sets the status code of redirection. // If status is not specified, status defaults to 303 See Other. func (r *Redirect) Status(code int) *Redirect { r.status = code return r } // With You can send flash messages by using With(). // They will be sent as a cookie. // You can get them by using: Redirect().Messages(), Redirect().Message() // Note: You must use escape char before using ',' and ':' chars to avoid wrong parsing. func (r *Redirect) With(key, value string, level ...uint8) *Redirect { // Get level var msgLevel uint8 if len(level) > 0 { msgLevel = level[0] } // Override old message if exists for i, msg := range r.messages { if msg.key == key && !msg.isOldInput { r.messages[i].value = value r.messages[i].level = msgLevel return r } } r.messages = append(r.messages, redirectionMsg{ key: key, value: value, level: msgLevel, }) return r } // WithInput You can send input data by using WithInput(). // They will be sent as a cookie. // This method can send form, multipart form, query data to redirected route. // You can get them by using: Redirect().OldInputs(), Redirect().OldInput() func (r *Redirect) WithInput() *Redirect { // Get content-type ctype := utils.UnsafeString(utilsbytes.UnsafeToLower(r.c.RequestCtx().Request.Header.ContentType())) ctype = binder.FilterFlags(utils.ParseVendorSpecificContentType(ctype)) oldInput := acquireOldInput() defer releaseOldInput(oldInput) switch ctype { case MIMEApplicationForm, MIMEMultipartForm: _ = r.c.Bind().Form(oldInput) //nolint:errcheck // not needed default: _ = r.c.Bind().Query(oldInput) //nolint:errcheck // not needed } // Add old input data for k, v := range oldInput { r.messages = append(r.messages, redirectionMsg{ key: k, value: v, isOldInput: true, }) } return r } // Messages Get flash messages. func (r *Redirect) Messages() []FlashMessage { if len(r.c.flashMessages) == 0 { return nil } flashMessages := make([]FlashMessage, 0, len(r.c.flashMessages)) writeIdx := 0 for _, msg := range r.c.flashMessages { if msg.isOldInput { r.c.flashMessages[writeIdx] = msg writeIdx++ continue } flashMessages = append(flashMessages, FlashMessage{ Key: msg.key, Value: msg.value, Level: msg.level, }) } for i := writeIdx; i < len(r.c.flashMessages); i++ { r.c.flashMessages[i] = redirectionMsg{} } r.c.flashMessages = r.c.flashMessages[:writeIdx] return flashMessages } // Message Get flash message by key. func (r *Redirect) Message(key string) FlashMessage { if len(r.c.flashMessages) == 0 { return FlashMessage{} } var flashMessage FlashMessage found := false writeIdx := 0 for _, msg := range r.c.flashMessages { if msg.isOldInput || found || msg.key != key { r.c.flashMessages[writeIdx] = msg writeIdx++ continue } flashMessage = FlashMessage{ Key: msg.key, Value: msg.value, Level: msg.level, } found = true } for i := writeIdx; i < len(r.c.flashMessages); i++ { r.c.flashMessages[i] = redirectionMsg{} } r.c.flashMessages = r.c.flashMessages[:writeIdx] return flashMessage } // OldInputs Get old input data. func (r *Redirect) OldInputs() []OldInputData { // Count old inputs first to avoid allocation if none exist count := 0 for _, msg := range r.c.flashMessages { if msg.isOldInput { count++ } } if count == 0 { return nil } inputs := make([]OldInputData, 0, count) for _, msg := range r.c.flashMessages { if msg.isOldInput { inputs = append(inputs, OldInputData{ Key: msg.key, Value: msg.value, }) } } return inputs } // OldInput Get old input data by key. func (r *Redirect) OldInput(key string) OldInputData { msgs := r.c.flashMessages for _, msg := range msgs { if msg.key == key && msg.isOldInput { return OldInputData{ Key: msg.key, Value: msg.value, } } } return OldInputData{} } // To redirect to the URL derived from the specified path, with specified status. func (r *Redirect) To(location string) error { r.c.setCanonical(HeaderLocation, location) r.c.Status(r.status) r.processFlashMessages() return nil } // Route redirects to the Route registered in the app with appropriate parameters. // If you want to send queries or params to route, you should use config parameter. func (r *Redirect) Route(name string, config ...RedirectConfig) error { // Check config cfg := RedirectConfig{} if len(config) > 0 { cfg = config[0] } // Get location from route name route := r.c.App().GetRoute(name) location, err := r.c.getLocationFromRoute(&route, cfg.Params) if err != nil { return err } // Check queries if len(cfg.Queries) > 0 { queryText := bytebufferpool.Get() defer bytebufferpool.Put(queryText) first := true for k, v := range cfg.Queries { if !first { queryText.WriteByte('&') } first = false queryText.WriteString(k) queryText.WriteByte('=') queryText.WriteString(v) } return r.To(location + "?" + r.c.app.toString(queryText.Bytes())) } return r.To(location) } // Back redirect to the URL to referer. func (r *Redirect) Back(fallback ...string) error { location := r.c.Get(HeaderReferer) if location == "" { // Check fallback URL if len(fallback) == 0 { err := ErrRedirectBackNoFallback r.c.Status(err.Code) return err } location = fallback[0] } return r.To(location) } // parseAndClearFlashMessages is a method to get flash messages before they are getting removed func (r *Redirect) parseAndClearFlashMessages() { // parse flash messages cookieValue, err := hex.DecodeString(r.c.Cookies(FlashCookieName)) if err != nil { return } _, err = r.c.flashMessages.UnmarshalMsg(cookieValue) if err != nil { return } r.c.Cookie(&Cookie{ Name: FlashCookieName, Value: "", Path: "/", MaxAge: -1, }) } // processFlashMessages is a helper function to process flash messages and old input data // and set them as cookies func (r *Redirect) processFlashMessages() { if len(r.messages) == 0 { return } val, err := r.messages.MarshalMsg(nil) if err != nil { return } dst := make([]byte, hex.EncodedLen(len(val))) hex.Encode(dst, val) r.c.Cookie(&Cookie{ Name: FlashCookieName, Value: r.c.app.toString(dst), SessionOnly: true, }) }