From 813850c83ca64dae052e106defbce4a077a1cbee Mon Sep 17 00:00:00 2001 From: Fenny Date: Fri, 21 Feb 2020 16:56:32 +0100 Subject: [PATCH] v2.0.0 --- .github/README.md | 4 +- .github/README_de.md | 4 +- .github/README_es.md | 4 +- .github/README_fr.md | 4 +- .github/README_ja.md | 4 +- .github/README_ko.md | 4 +- .github/README_pt.md | 4 +- .github/README_ru.md | 8 +- .github/README_tr.md | 4 +- .github/README_zh-CN.md | 4 +- .github/workflows/security.yml | 18 + app.go | 367 ++++++++++++ application_test.go => app_test.go | 130 +++-- application.go | 549 ------------------ context.go | 869 +++++++++++++++++++++++++++++ request_test.go => context_test.go | 529 ++++++++++++++++-- go.mod | 8 +- go.sum | 21 +- group.go | 121 ++++ request.go | 442 --------------- response.go | 483 ---------------- response_test.go | 590 -------------------- router.go | 481 +++++++++++----- router_test.go | 10 - utils.go | 113 ++-- 25 files changed, 2370 insertions(+), 2405 deletions(-) create mode 100644 .github/workflows/security.yml create mode 100644 app.go rename application_test.go => app_test.go (59%) delete mode 100644 application.go create mode 100644 context.go rename request_test.go => context_test.go (57%) create mode 100644 group.go delete mode 100644 request.go delete mode 100644 response.go delete mode 100644 response_test.go delete mode 100644 router_test.go diff --git a/.github/README.md b/.github/README.md index 01b15116..dd049074 100644 --- a/.github/README.md +++ b/.github/README.md @@ -208,11 +208,11 @@ func main() { app := fiber.New() app.Static("/public") - + app.Get("/demo", func(c *fiber.Ctx) { c.Send("This is a demo!") }) - + app.Post("/register", func(c *fiber.Ctx) { c.Send("Welcome!") }) diff --git a/.github/README_de.md b/.github/README_de.md index 7f7be585..f7a8342c 100644 --- a/.github/README_de.md +++ b/.github/README_de.md @@ -1,8 +1,8 @@

- Fiber + Fiber -

+
diff --git a/.github/README_es.md b/.github/README_es.md index 74f46658..9cb1a91c 100644 --- a/.github/README_es.md +++ b/.github/README_es.md @@ -1,8 +1,8 @@

- Fiber + Fiber -

+
diff --git a/.github/README_fr.md b/.github/README_fr.md index f86c314d..5aa66db2 100644 --- a/.github/README_fr.md +++ b/.github/README_fr.md @@ -1,8 +1,8 @@

- Fiber + Fiber -

+
diff --git a/.github/README_ja.md b/.github/README_ja.md index 5d35a86b..faf4ce1c 100644 --- a/.github/README_ja.md +++ b/.github/README_ja.md @@ -1,8 +1,8 @@

- Fiber + Fiber -

+
diff --git a/.github/README_ko.md b/.github/README_ko.md index 80e76ff1..3a841a73 100644 --- a/.github/README_ko.md +++ b/.github/README_ko.md @@ -1,8 +1,8 @@

- Fiber + Fiber -

+
diff --git a/.github/README_pt.md b/.github/README_pt.md index 3b0b4c55..6de6ab74 100644 --- a/.github/README_pt.md +++ b/.github/README_pt.md @@ -1,8 +1,8 @@

- Fiber + Fiber -

+
diff --git a/.github/README_ru.md b/.github/README_ru.md index d5f378ff..a7577392 100644 --- a/.github/README_ru.md +++ b/.github/README_ru.md @@ -1,8 +1,8 @@

- Fiber + Fiber -

+
@@ -208,11 +208,11 @@ func main() { app := fiber.New() app.Static("/public") - + app.Get("/demo", func(c *fiber.Ctx) { c.Send("This is a demo!") }) - + app.Post("/register", func(c *fiber.Ctx) { c.Send("Welcome!") }) diff --git a/.github/README_tr.md b/.github/README_tr.md index bb40d6f8..d13372af 100644 --- a/.github/README_tr.md +++ b/.github/README_tr.md @@ -1,8 +1,8 @@

- Fiber + Fiber -

+
diff --git a/.github/README_zh-CN.md b/.github/README_zh-CN.md index a89a7099..99747374 100644 --- a/.github/README_zh-CN.md +++ b/.github/README_zh-CN.md @@ -1,8 +1,8 @@

- Fiber + Fiber -

+
diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml new file mode 100644 index 00000000..20d85808 --- /dev/null +++ b/.github/workflows/security.yml @@ -0,0 +1,18 @@ +on: [push] +name: Security +jobs: + test: + strategy: + matrix: + go-version: [1.13.x] + platform: [ubuntu-latest] + runs-on: ${{ matrix.platform }} + steps: + - name: Install Go + uses: actions/setup-go@v1 + with: + go-version: ${{ matrix.go-version }} + - name: Checkout code + uses: actions/checkout@v2 + - name: Security + run: go get github.com/securego/gosec/cmd/gosec; `go env GOPATH`/bin/gosec ./... diff --git a/app.go b/app.go new file mode 100644 index 00000000..a2bdc29b --- /dev/null +++ b/app.go @@ -0,0 +1,367 @@ +package fiber + +import ( + "bufio" + "flag" + "fmt" + "io/ioutil" + "net" + "net/http" + "net/http/httputil" + "os" + "os/exec" + "runtime" + "strconv" + "strings" + "time" + + fasthttp "github.com/valyala/fasthttp" +) + +// Version of Fiber +const Version = "2.0.0" + +type ( + // App denotes the Fiber application. + App struct { + server *fasthttp.Server + routes []*Route + child bool + recover func(*Ctx) + Settings *Settings + } + // Map defines a generic map of type `map[string]interface{}`. + Map map[string]interface{} + // Settings is a struct holding the server settings + Settings struct { + // fiber settings + Prefork bool `default:"false"` + // Enable strict routing. When enabled, the router treats "/foo" and "/foo/" as different. Otherwise, the router treats "/foo" and "/foo/" as the same. + StrictRouting bool `default:"false"` + // Enable case sensitivity. When enabled, "/Foo" and "/foo" are different routes. When disabled, "/Foo" and "/foo" are treated the same. + CaseSensitive bool `default:"false"` + // Enables the "Server: value" HTTP header. + ServerHeader string `default:""` + // fasthttp settings + GETOnly bool `default:"false"` + IdleTimeout time.Duration `default:"0"` + Concurrency int `default:"256 * 1024"` + ReadTimeout time.Duration `default:"0"` + WriteTimeout time.Duration `default:"0"` + TCPKeepalive bool `default:"false"` + MaxConnsPerIP int `default:"0"` + ReadBufferSize int `default:"4096"` + WriteBufferSize int `default:"4096"` + ConcurrencySleep time.Duration `default:"0"` + DisableKeepAlive bool `default:"false"` + ReduceMemoryUsage bool `default:"false"` + MaxRequestsPerConn int `default:"0"` + TCPKeepalivePeriod time.Duration `default:"0"` + MaxRequestBodySize int `default:"4 * 1024 * 1024"` + NoHeaderNormalizing bool `default:"false"` + NoDefaultContentType bool `default:"false"` + // template settings + ViewCache bool `default:"false"` + ViewFolder string `default:""` + ViewEngine string `default:""` + ViewExtension string `default:""` + } +) + +var prefork, child bool + +func regBoolVar(p *bool, name string, value bool, usage string) { + if flag.Lookup(name) == nil { + flag.BoolVar(p, name, value, usage) + } +} +func getBoolFlag(name string) bool { + return flag.Lookup(name).Value.(flag.Getter).Get().(bool) +} +func init() { + regBoolVar(&prefork, "prefork", false, "use prefork") + regBoolVar(&child, "child", false, "is child process") +} + +// New ... +func New(settings ...*Settings) (app *App) { + flag.Parse() + prefork = getBoolFlag("prefork") + child = getBoolFlag("child") + + app = &App{ + child: child, + } + if len(settings) > 0 { + opt := settings[0] + if !opt.Prefork { + opt.Prefork = prefork + } + if opt.Concurrency == 0 { + opt.Concurrency = 256 * 1024 + } + if opt.ReadBufferSize == 0 { + opt.ReadBufferSize = 4096 + } + if opt.WriteBufferSize == 0 { + opt.WriteBufferSize = 4096 + } + if opt.MaxRequestBodySize == 0 { + opt.MaxRequestBodySize = 4 * 1024 * 1024 + } + app.Settings = opt + return + } + app.Settings = &Settings{ + Prefork: prefork, + Concurrency: 256 * 1024, + ReadBufferSize: 4096, + WriteBufferSize: 4096, + MaxRequestBodySize: 4 * 1024 * 1024, + } + return +} + +// Static ... +func (app *App) Static(args ...string) *App { + app.registerStatic("/", args...) + return app +} + +// WebSocket ... +func (app *App) WebSocket(args ...interface{}) *App { + app.register("GET", "", args...) + return app +} + +// Connect ... +func (app *App) Connect(args ...interface{}) *App { + app.register("CONNECT", "", args...) + return app +} + +// Put ... +func (app *App) Put(args ...interface{}) *App { + app.register("PUT", "", args...) + return app +} + +// Post ... +func (app *App) Post(args ...interface{}) *App { + app.register("POST", "", args...) + return app +} + +// Delete ... +func (app *App) Delete(args ...interface{}) *App { + app.register("DELETE", "", args...) + return app +} + +// Head ... +func (app *App) Head(args ...interface{}) *App { + app.register("HEAD", "", args...) + return app +} + +// Patch ... +func (app *App) Patch(args ...interface{}) *App { + app.register("PATCH", "", args...) + return app +} + +// Options ... +func (app *App) Options(args ...interface{}) *App { + app.register("OPTIONS", "", args...) + return app +} + +// Trace ... +func (app *App) Trace(args ...interface{}) *App { + app.register("TRACE", "", args...) + return app +} + +// Get ... +func (app *App) Get(args ...interface{}) *App { + app.register("GET", "", args...) + return app +} + +// All ... +func (app *App) All(args ...interface{}) *App { + app.register("ALL", "", args...) + return app +} + +// Use ... +func (app *App) Use(args ...interface{}) *App { + app.register("USE", "", args...) + return app +} + +// Listen : https://fiber.wiki/application#listen +func (app *App) Listen(address interface{}, tls ...string) error { + addr, ok := address.(string) + if !ok { + port, ok := address.(int) + if !ok { + return fmt.Errorf("Listen: Host must be an INT port or STRING address") + } + addr = strconv.Itoa(port) + } + if !strings.Contains(addr, ":") { + addr = ":" + addr + } + // Create fasthttp server + app.server = app.newServer() + // Print banner + // if app.Settings.Banner && !app.child { + // fmt.Printf("Fiber-%s is listening on %s\n", Version, addr) + // } + var ln net.Listener + var err error + // Prefork enabled + if app.Settings.Prefork && runtime.NumCPU() > 1 { + if ln, err = app.prefork(addr); err != nil { + return err + } + } else { + if ln, err = net.Listen("tcp4", addr); err != nil { + return err + } + } + + // enable TLS/HTTPS + if len(tls) > 1 { + return app.server.ServeTLS(ln, tls[0], tls[1]) + } + return app.server.Serve(ln) +} + +// Shutdown server gracefully +func (app *App) Shutdown() error { + if app.server == nil { + return fmt.Errorf("Server is not running") + } + return app.server.Shutdown() +} + +// Test takes a http.Request and execute a fake connection to the application +// It returns a http.Response when the connection was successful +func (app *App) Test(req *http.Request) (*http.Response, error) { + // Get raw http request + reqRaw, err := httputil.DumpRequest(req, true) + if err != nil { + return nil, err + } + // Setup a fiber server struct + app.server = app.newServer() + // Create fake connection + conn := &testConn{} + // Pass HTTP request to conn + _, err = conn.r.Write(reqRaw) + if err != nil { + return nil, err + } + // Serve conn to server + channel := make(chan error) + go func() { + channel <- app.server.ServeConn(conn) + }() + // Wait for callback + select { + case err := <-channel: + if err != nil { + return nil, err + } + // Throw timeout error after 200ms + case <-time.After(1000 * time.Millisecond): + return nil, fmt.Errorf("timeout") + } + // Get raw HTTP response + respRaw, err := ioutil.ReadAll(&conn.w) + if err != nil { + return nil, err + } + // Create buffer + reader := strings.NewReader(getString(respRaw)) + buffer := bufio.NewReader(reader) + // Convert raw HTTP response to http.Response + resp, err := http.ReadResponse(buffer, req) + if err != nil { + return nil, err + } + // Return *http.Response + return resp, nil +} + +// https://www.nginx.com/blog/socket-sharding-nginx-release-1-9-1/ +func (app *App) prefork(address string) (ln net.Listener, err error) { + // Master proc + if !app.child { + addr, err := net.ResolveTCPAddr("tcp", address) + if err != nil { + return ln, err + } + tcplistener, err := net.ListenTCP("tcp", addr) + if err != nil { + return ln, err + } + fl, err := tcplistener.File() + if err != nil { + return ln, err + } + childs := make([]*exec.Cmd, runtime.NumCPU()/2) + + // #nosec G204 + for i := range childs { + childs[i] = exec.Command(os.Args[0], "-prefork", "-child") + childs[i].Stdout = os.Stdout + childs[i].Stderr = os.Stderr + childs[i].ExtraFiles = []*os.File{fl} + if err := childs[i].Start(); err != nil { + return ln, err + } + } + + for _, child := range childs { + if err := child.Wait(); err != nil { + return ln, err + } + } + os.Exit(0) + } else { + ln, err = net.FileListener(os.NewFile(3, "")) + } + return ln, err +} + +func (app *App) newServer() *fasthttp.Server { + return &fasthttp.Server{ + Handler: app.handler, + ErrorHandler: func(ctx *fasthttp.RequestCtx, err error) { + ctx.Response.SetStatusCode(400) + ctx.Response.SetBodyString("Bad Request") + }, + Name: app.Settings.ServerHeader, + Concurrency: app.Settings.Concurrency, + SleepWhenConcurrencyLimitsExceeded: app.Settings.ConcurrencySleep, + DisableKeepalive: app.Settings.DisableKeepAlive, + ReadBufferSize: app.Settings.ReadBufferSize, + WriteBufferSize: app.Settings.WriteBufferSize, + ReadTimeout: app.Settings.ReadTimeout, + WriteTimeout: app.Settings.WriteTimeout, + IdleTimeout: app.Settings.IdleTimeout, + MaxConnsPerIP: app.Settings.MaxConnsPerIP, + MaxRequestsPerConn: app.Settings.MaxRequestsPerConn, + TCPKeepalive: app.Settings.TCPKeepalive, + TCPKeepalivePeriod: app.Settings.TCPKeepalivePeriod, + MaxRequestBodySize: app.Settings.MaxRequestBodySize, + ReduceMemoryUsage: app.Settings.ReduceMemoryUsage, + GetOnly: app.Settings.GETOnly, + DisableHeaderNamesNormalizing: app.Settings.NoHeaderNormalizing, + NoDefaultServerHeader: app.Settings.ServerHeader == "", + NoDefaultContentType: app.Settings.NoDefaultContentType, + } +} diff --git a/application_test.go b/app_test.go similarity index 59% rename from application_test.go rename to app_test.go index 0c825549..8c0f5f18 100644 --- a/application_test.go +++ b/app_test.go @@ -7,51 +7,67 @@ import ( var handler = func(c *Ctx) {} +func is200(t *testing.T, app *App, url string, m ...string) { + method := "GET" + if len(m) > 0 { + method = m[0] + } + req, _ := http.NewRequest(method, url, nil) + resp, err := app.Test(req) + if err != nil { + t.Fatalf("%s - %s - %v", method, url, err) + } + if resp.StatusCode != 200 { + t.Fatalf("%s - %s - %v", method, url, resp.StatusCode) + } +} func Test_Methods(t *testing.T) { app := New() - methods := []string{"CONNECT", "PUT", "POST", "DELETE", "HEAD", "PATCH", "OPTIONS", "TRACE", "GET", "ALL", "USE"} - app.Connect("", handler) - app.Connect("/CONNECT", handler) - app.Put("/PUT", handler) - app.Post("/POST", handler) - app.Delete("/DELETE", handler) - app.Head("/HEAD", handler) - app.Patch("/PATCH", handler) - app.Options("/OPTIONS", handler) - app.Trace("/TRACE", handler) - app.Get("/GET", handler) - app.All("/ALL", handler) - app.Use("/USE", handler) + app.Connect("/:john?/:doe?", handler) + is200(t, app, "/", "CONNECT") + + app.Connect("/:john?/:doe?", handler) + is200(t, app, "/", "CONNECT") + + app.Put("/:john?/:doe?", handler) + is200(t, app, "/", "CONNECT") + + app.Post("/:john?/:doe?", handler) + is200(t, app, "/", "POST") + + app.Delete("/:john?/:doe?", handler) + is200(t, app, "/", "DELETE") + + app.Head("/:john?/:doe?", handler) + is200(t, app, "/", "HEAD") + + app.Patch("/:john?/:doe?", handler) + is200(t, app, "/", "PATCH") + + app.Options("/:john?/:doe?", handler) + is200(t, app, "/", "OPTIONS") + + app.Trace("/:john?/:doe?", handler) + is200(t, app, "/", "TRACE") + + app.Get("/:john?/:doe?", handler) + is200(t, app, "/", "GET") + + app.All("/:john?/:doe?", handler) + is200(t, app, "/", "POST") + + app.Use("/:john?/:doe?", handler) + is200(t, app, "/", "GET") - for _, method := range methods { - var req *http.Request - if method == "ALL" { - req, _ = http.NewRequest("CONNECT", "/"+method, nil) - } else if method == "USE" { - req, _ = http.NewRequest("OPTIONS", "/"+method+"/test", nil) - } else { - req, _ = http.NewRequest(method, "/"+method, nil) - } - resp, err := app.Test(req) - if err != nil { - t.Fatalf(`%s: %s %s`, t.Name(), method, err) - } - if resp.StatusCode != 200 { - t.Fatalf(`%s: %s expecting 200 but received %v`, t.Name(), method, resp.StatusCode) - } - } } func Test_Static(t *testing.T) { app := New() grp := app.Group("/v1") grp.Static("/v2", ".travis.yml") - grp.Static(".travis.yml") app.Static("/yesyes*", ".github/FUNDING.yml") app.Static("./.github") - app.Static("github", ".github/FUNDING.yml") - app.Static("/*", "./.github") app.Static("/john", "./.github") req, _ := http.NewRequest("GET", "/stale.yml", nil) resp, err := app.Test(req) @@ -98,37 +114,53 @@ func Test_Static(t *testing.T) { t.Fatalf(`%s: Missing Content-Length`, t.Name()) } } + func Test_Group(t *testing.T) { app := New() + grp := app.Group("/test") grp.Get("/", handler) + is200(t, app, "/test", "GET") + grp.Get("/:demo?", handler) + is200(t, app, "/test/john", "GET") + grp.Connect("/CONNECT", handler) + is200(t, app, "/test/CONNECT", "CONNECT") + grp.Put("/PUT", handler) + is200(t, app, "/test/PUT", "PUT") + grp.Post("/POST", handler) + is200(t, app, "/test/POST", "POST") + grp.Delete("/DELETE", handler) + is200(t, app, "/test/DELETE", "DELETE") + grp.Head("/HEAD", handler) + is200(t, app, "/test/HEAD", "HEAD") + grp.Patch("/PATCH", handler) + is200(t, app, "/test/PATCH", "PATCH") + grp.Options("/OPTIONS", handler) + is200(t, app, "/test/OPTIONS", "OPTIONS") + grp.Trace("/TRACE", handler) + is200(t, app, "/test/TRACE", "TRACE") + grp.All("/ALL", handler) + is200(t, app, "/test/ALL", "POST") + grp.Use("/USE", handler) - req, _ := http.NewRequest("GET", "/test", nil) - resp, err := app.Test(req) - if err != nil { - t.Fatalf(`%s: %s`, t.Name(), err) - } - if resp.StatusCode != 200 { - t.Fatalf(`%s: StatusCode %v`, t.Name(), resp.StatusCode) - } - req, _ = http.NewRequest("GET", "/test/test", nil) - resp, err = app.Test(req) - if err != nil { - t.Fatalf(`%s: %s`, t.Name(), err) - } - if resp.StatusCode != 200 { - t.Fatalf(`%s: StatusCode %v`, t.Name(), resp.StatusCode) - } + is200(t, app, "/test/USE/oke", "GET") + + api := grp.Group("/v1") + api.Post("/", handler) + is200(t, app, "/test/v1/", "POST") + + api.Get("/users", handler) + is200(t, app, "/test/v1/users", "GET") } // func Test_Listen(t *testing.T) { diff --git a/application.go b/application.go deleted file mode 100644 index cbdf4461..00000000 --- a/application.go +++ /dev/null @@ -1,549 +0,0 @@ -// 🚀 Fiber is an Express.js inspired web framework written in Go with 💖 -// 📌 Please open an issue if you got suggestions or found a bug! -// 🖥 Links: https://github.com/gofiber/fiber, https://fiber.wiki - -// 🦸 Not all heroes wear capes, thank you to some amazing people -// 💖 @valyala, @erikdubbelboer, @savsgio, @julienschmidt, @koddr - -package fiber - -import ( - "bufio" - "flag" - "fmt" - "io/ioutil" - "log" - "net" - "net/http" - "net/http/httputil" - "os" - "os/exec" - "path/filepath" - "runtime" - "strconv" - "strings" - "time" - - fasthttp "github.com/valyala/fasthttp" - reuseport "github.com/valyala/fasthttp/reuseport" -) - -const ( - // Version : Fiber release - Version = "1.7.0" - // Website : Fiber website - Website = "https://fiber.wiki" - banner = "\x1b[1;32m" + ` ______ __ ______ ______ ______ -/\ ___\ /\ \ /\ == \ /\ ___\ /\ == \ -\ \ __\ \ \ \ \ \ __< \ \ __\ \ \ __< - \ \_\ \ \_\ \ \_____\ \ \_____\ \ \_\ \_\ - \/_/ \/_/ \/_____/ \/_____/ \/_/ /_/ - -` + "\x1b[0mFiber \x1b[1;32mv%s\x1b[0m %s on \x1b[1;32m%s\x1b[0m, visit \x1b[1;32m%s\x1b[0m\n\n" -) - -var ( - prefork = flag.Bool("prefork", false, "use prefork") - child = flag.Bool("child", false, "is child process") -) - -// Application structure -type Application struct { - // Server name header - Server string - // HTTP server struct - httpServer *fasthttp.Server - // Show fiber banner - Banner bool - // https://github.com/valyala/fasthttp/blob/master/server.go#L150 - Engine *engine - // https://www.nginx.com/blog/socket-sharding-nginx-release-1-9-1/ - Prefork bool - // is child process - child bool - // Stores all routes - routes []*Route - // Recover holds a handler that is executed on a panic - recover func(*Ctx) -} - -// Fasthttp settings -// https://github.com/valyala/fasthttp/blob/master/server.go#L150 -type engine struct { - Concurrency int - DisableKeepAlive bool - ReadBufferSize int - WriteBufferSize int - ReadTimeout time.Duration - WriteTimeout time.Duration - IdleTimeout time.Duration - MaxConnsPerIP int - MaxRequestsPerConn int - TCPKeepalive bool - TCPKeepalivePeriod time.Duration - MaxRequestBodySize int - ReduceMemoryUsage bool - GetOnly bool - DisableHeaderNamesNormalizing bool - SleepWhenConcurrencyLimitsExceeded time.Duration - NoDefaultContentType bool - KeepHijackedConns bool -} - -// New https://fiber.wiki/application#new -func New() *Application { - flag.Parse() - schemaDecoder.SetAliasTag("form") - return &Application{ - Server: "", - httpServer: nil, - Banner: true, - Prefork: *prefork, - child: *child, - Engine: &engine{ - Concurrency: 256 * 1024, - DisableKeepAlive: false, - ReadBufferSize: 4096, - WriteBufferSize: 4096, - WriteTimeout: 0, - ReadTimeout: 0, - IdleTimeout: 0, - MaxConnsPerIP: 0, - MaxRequestsPerConn: 0, - TCPKeepalive: false, - TCPKeepalivePeriod: 0, - MaxRequestBodySize: 4 * 1024 * 1024, - ReduceMemoryUsage: false, - GetOnly: false, - DisableHeaderNamesNormalizing: false, - SleepWhenConcurrencyLimitsExceeded: 0, - NoDefaultContentType: false, - KeepHijackedConns: false, - }, - } -} - -// Recover catches panics and avoids crashes -func (app *Application) Recover(ctx func(*Ctx)) { - app.recover = ctx -} - -// Recover binding for groups -func (grp *Group) Recover(ctx func(*Ctx)) { - grp.app.recover = ctx -} - -// Group : -type Group struct { - path string - app *Application -} - -// Group : -func (app *Application) Group(path string) *Group { - return &Group{ - path: path, - app: app, - } -} - -// Connect establishes a tunnel to the server -// identified by the target resource. -func (app *Application) Connect(args ...interface{}) *Application { - app.register("CONNECT", args...) - return app -} - -// Connect for group -func (grp *Group) Connect(args ...interface{}) *Group { - grp.register("CONNECT", args...) - return grp -} - -// Put replaces all current representations -// of the target resource with the request payload. -func (app *Application) Put(args ...interface{}) *Application { - app.register("PUT", args...) - return app -} - -// Put for group -func (grp *Group) Put(args ...interface{}) *Group { - grp.register("PUT", args...) - return grp -} - -// Post is used to submit an entity to the specified resource, -// often causing a change in state or side effects on the server. -func (app *Application) Post(args ...interface{}) *Application { - app.register("POST", args...) - return app -} - -// Post for group -func (grp *Group) Post(args ...interface{}) *Group { - grp.register("POST", args...) - return grp -} - -// Delete deletes the specified resource. -func (app *Application) Delete(args ...interface{}) *Application { - app.register("DELETE", args...) - return app -} - -// Delete for group -func (grp *Group) Delete(args ...interface{}) *Group { - grp.register("DELETE", args...) - return grp -} - -// Head asks for a response identical to that of a GET request, -// but without the response body. -func (app *Application) Head(args ...interface{}) *Application { - app.register("HEAD", args...) - return app -} - -// Head for group -func (grp *Group) Head(args ...interface{}) *Group { - grp.register("HEAD", args...) - return grp -} - -// Patch is used to apply partial modifications to a resource. -func (app *Application) Patch(args ...interface{}) *Application { - app.register("PATCH", args...) - return app -} - -// Patch for group -func (grp *Group) Patch(args ...interface{}) *Group { - grp.register("PATCH", args...) - return grp -} - -// Options is used to describe the communication options -// for the target resource. -func (app *Application) Options(args ...interface{}) *Application { - app.register("OPTIONS", args...) - return app -} - -// Options for group -func (grp *Group) Options(args ...interface{}) *Group { - grp.register("OPTIONS", args...) - return grp -} - -// Trace performs a message loop-back test -// along the path to the target resource. -func (app *Application) Trace(args ...interface{}) *Application { - app.register("TRACE", args...) - return app -} - -// Trace for group -func (grp *Group) Trace(args ...interface{}) *Group { - grp.register("TRACE", args...) - return grp -} - -// Get requests a representation of the specified resource. -// Requests using GET should only retrieve data. -func (app *Application) Get(args ...interface{}) *Application { - app.register("GET", args...) - return app -} - -// Get for group -func (grp *Group) Get(args ...interface{}) *Group { - grp.register("GET", args...) - return grp -} - -// All matches any HTTP method -func (app *Application) All(args ...interface{}) *Application { - app.register("ALL", args...) - return app -} - -// All for group -func (grp *Group) All(args ...interface{}) *Group { - grp.register("ALL", args...) - return grp -} - -// Use only matches the starting path -func (app *Application) Use(args ...interface{}) *Application { - app.register("USE", args...) - return app -} - -// Use for group -func (grp *Group) Use(args ...interface{}) *Group { - grp.register("USE", args...) - return grp -} - -// Static for groups -func (grp *Group) Static(args ...string) { - prefix := grp.path - root := "./" - - if len(args) == 1 { - root = args[0] - } else if len(args) == 2 { - root = args[1] - prefix = prefix + args[0] - prefix = strings.Replace(prefix, "//", "/", -1) - prefix = filepath.Clean(prefix) - prefix = filepath.ToSlash(prefix) - } - grp.app.Static(prefix, root) -} - -// Static https://fiber.wiki/application#static -func (app *Application) Static(args ...string) { - prefix := "/" - root := "./" - wildcard := false - midware := false - // enable / disable gzipping somewhere? - // todo v2.0.0 - gzip := true - - if len(args) == 1 { - root = args[0] - } else if len(args) == 2 { - prefix = args[0] - root = args[1] - if prefix[0] != '/' { - prefix = "/" + prefix - } - } - - // Check if wildcard for single files - // app.Static("*", "./public/index.html") - // app.Static("/*", "./public/index.html") - if prefix == "*" || prefix == "/*" { - wildcard = true - } else if strings.Contains(prefix, "*") { - prefix = strings.Replace(prefix, "*", "", -1) - midware = true - } - - // Lets get all files from root - files, _, err := getFiles(root) - if err != nil { - log.Fatal("Static: ", err) - } - - // ./static/compiled => static/compiled - mount := filepath.Clean(root) - - // Loop over all files - for _, file := range files { - // Ignore the .gzipped files by fasthttp - if strings.Contains(file, ".fasthttp.gz") { - continue - } - - // Time to create a fake path for the route match - // static/index.html => /index.html - path := filepath.Join(prefix, strings.Replace(file, mount, "", 1)) - // for windows: static\index.html => /index.html - path = filepath.ToSlash(path) - // Store file path to use in ctx handler - filePath := file - - // If the file is an index.html, bind the prefix to index.html directly - if filepath.Base(filePath) == "index.html" || filepath.Base(filePath) == "index.htm" { - app.routes = append(app.routes, &Route{"GET", prefix, midware, wildcard, nil, nil, func(c *Ctx) { - c.SendFile(filePath, gzip) - }}) - } - - // Add the route + SendFile(filepath) to routes - app.routes = append(app.routes, &Route{"GET", path, midware, wildcard, nil, nil, func(c *Ctx) { - c.SendFile(filePath, gzip) - }}) - } -} - -// Listen : https://fiber.wiki/application#listen -func (app *Application) Listen(address interface{}, tls ...string) { - host := "" - switch val := address.(type) { - case int: - host = ":" + strconv.Itoa(val) // 8080 => ":8080" - case string: - if !strings.Contains(val, ":") { - val = ":" + val // "8080" => ":8080" - } - host = val - default: - log.Fatal("Listen: Host must be an INT port or STRING address") - } - // Create fasthttp server - app.httpServer = app.setupServer() - - // Prefork enabled - if app.Prefork && runtime.NumCPU() > 1 { - if app.Banner && !app.child { - fmt.Printf(banner, Version, "preforking", host, "fiber.wiki") - } - app.prefork(host, tls...) - } - - // Prefork disabled - if app.Banner { - fmt.Printf(banner, Version, "listening", host, "fiber.wiki") - } - - ln, err := net.Listen("tcp4", host) - if err != nil { - log.Fatal("Listen: ", err) - } - - // enable TLS/HTTPS - if len(tls) > 1 { - if err := app.httpServer.ServeTLS(ln, tls[0], tls[1]); err != nil { - log.Fatal("Listen: ", err) - } - } - - if err := app.httpServer.Serve(ln); err != nil { - log.Fatal("Listen: ", err) - } -} - -// Shutdown server gracefully -func (app *Application) Shutdown() error { - if app.httpServer == nil { - return fmt.Errorf("server is not running") - } - return app.httpServer.Shutdown() -} - -// Test takes a http.Request and execute a fake connection to the application -// It returns a http.Response when the connection was successful -func (app *Application) Test(req *http.Request) (*http.Response, error) { - // Get raw http request - reqRaw, err := httputil.DumpRequest(req, true) - if err != nil { - return nil, err - } - // Setup a fiber server struct - app.httpServer = app.setupServer() - // Create fake connection - conn := &conn{} - // Pass HTTP request to conn - _, err = conn.r.Write(reqRaw) - if err != nil { - return nil, err - } - // Serve conn to server - channel := make(chan error) - go func() { - channel <- app.httpServer.ServeConn(conn) - }() - // Wait for callback - select { - case err := <-channel: - if err != nil { - return nil, err - } - // Throw timeout error after 200ms - case <-time.After(1000 * time.Millisecond): - return nil, fmt.Errorf("timeout") - } - // Get raw HTTP response - respRaw, err := ioutil.ReadAll(&conn.w) - if err != nil { - return nil, err - } - // Create buffer - reader := strings.NewReader(getString(respRaw)) - buffer := bufio.NewReader(reader) - // Convert raw HTTP response to http.Response - resp, err := http.ReadResponse(buffer, req) - if err != nil { - return nil, err - } - // Return *http.Response - return resp, nil -} - -// https://www.nginx.com/blog/socket-sharding-nginx-release-1-9-1/ -func (app *Application) prefork(host string, tls ...string) { - // Master proc - if !app.child { - // Create babies - childs := make([]*exec.Cmd, runtime.NumCPU()) - - // #nosec G204 - for i := range childs { - childs[i] = exec.Command(os.Args[0], "-prefork", "-child") - childs[i].Stdout = os.Stdout - childs[i].Stderr = os.Stderr - if err := childs[i].Start(); err != nil { - log.Fatal("Listen-prefork: ", err) - } - } - - for _, child := range childs { - if err := child.Wait(); err != nil { - log.Fatal("Listen-prefork: ", err) - } - - } - - os.Exit(0) - } - - // Child proc - runtime.GOMAXPROCS(1) - - ln, err := reuseport.Listen("tcp4", host) - if err != nil { - log.Fatal("Listen-prefork: ", err) - } - - // enable TLS/HTTPS - if len(tls) > 1 { - if err := app.httpServer.ServeTLS(ln, tls[0], tls[1]); err != nil { - log.Fatal("Listen-prefork: ", err) - } - } - - if err := app.httpServer.Serve(ln); err != nil { - log.Fatal("Listen-prefork: ", err) - } -} - -func (app *Application) setupServer() *fasthttp.Server { - return &fasthttp.Server{ - Handler: app.handler, - Name: app.Server, - Concurrency: app.Engine.Concurrency, - DisableKeepalive: app.Engine.DisableKeepAlive, - ReadBufferSize: app.Engine.ReadBufferSize, - WriteBufferSize: app.Engine.WriteBufferSize, - ReadTimeout: app.Engine.ReadTimeout, - WriteTimeout: app.Engine.WriteTimeout, - IdleTimeout: app.Engine.IdleTimeout, - MaxConnsPerIP: app.Engine.MaxConnsPerIP, - MaxRequestsPerConn: app.Engine.MaxRequestsPerConn, - TCPKeepalive: app.Engine.TCPKeepalive, - TCPKeepalivePeriod: app.Engine.TCPKeepalivePeriod, - MaxRequestBodySize: app.Engine.MaxRequestBodySize, - ReduceMemoryUsage: app.Engine.ReduceMemoryUsage, - GetOnly: app.Engine.GetOnly, - DisableHeaderNamesNormalizing: app.Engine.DisableHeaderNamesNormalizing, - SleepWhenConcurrencyLimitsExceeded: app.Engine.SleepWhenConcurrencyLimitsExceeded, - NoDefaultServerHeader: app.Server == "", - NoDefaultContentType: app.Engine.NoDefaultContentType, - KeepHijackedConns: app.Engine.KeepHijackedConns, - } -} diff --git a/context.go b/context.go new file mode 100644 index 00000000..666badb5 --- /dev/null +++ b/context.go @@ -0,0 +1,869 @@ +package fiber + +import ( + "bytes" + "encoding/xml" + "fmt" + "html/template" + "io/ioutil" + "log" + "mime" + "mime/multipart" + "net/url" + "path/filepath" + "strings" + "sync" + "time" + + // templates + pug "github.com/Joker/jade" + handlebars "github.com/aymerick/raymond" + mustache "github.com/cbroglie/mustache" + amber "github.com/eknkc/amber" + // core + websocket "github.com/fasthttp/websocket" + jsoniter "github.com/json-iterator/go" + fasthttp "github.com/valyala/fasthttp" +) + +// Ctx represents the Context which hold the HTTP request and response. +// It has methods for the request query string, parameters, body, HTTP headers and so on. +// For more information please visit our documentation: https://fiber.wiki/context +type Ctx struct { + app *App + route *Route + next bool + error error + params *[]string + values []string + Fasthttp *fasthttp.RequestCtx + Socket *websocket.Conn +} + +// Ctx pool +var poolCtx = sync.Pool{ + New: func() interface{} { + return new(Ctx) + }, +} + +// Acquire Ctx from pool +func acquireCtx(fctx *fasthttp.RequestCtx) *Ctx { + ctx := poolCtx.Get().(*Ctx) + ctx.Fasthttp = fctx + return ctx +} + +// Return Ctx to pool +func releaseCtx(ctx *Ctx) { + ctx.route = nil + ctx.next = false + ctx.error = nil + ctx.params = nil + ctx.values = nil + ctx.Fasthttp = nil + ctx.Socket = nil + poolCtx.Put(ctx) +} + +// Conn https://godoc.org/github.com/gorilla/websocket#pkg-index +type Conn struct { + params *[]string + values []string + *websocket.Conn +} + +// Params : https://fiber.wiki/application#websocket +func (conn *Conn) Params(key string) string { + if conn.params == nil { + return "" + } + for i := 0; i < len(*conn.params); i++ { + if (*conn.params)[i] == key { + return conn.values[i] + } + } + return "" +} + +// Conn pool +var poolConn = sync.Pool{ + New: func() interface{} { + return new(Conn) + }, +} + +// Acquire Conn from pool +func acquireConn(fconn *websocket.Conn) *Conn { + conn := poolConn.Get().(*Conn) + conn.Conn = fconn + return conn +} + +// Return Conn to pool +func releaseConn(conn *Conn) { + conn.Close() + conn.params = nil + conn.values = nil + conn.Conn = nil + poolConn.Put(conn) +} + +// Cookie : struct +type Cookie struct { + Expire int // time.Unix(1578981376, 0) + MaxAge int + Domain string + Path string + + HTTPOnly bool + Secure bool + SameSite string +} + +// Accepts : https://fiber.wiki/context#accepts +func (ctx *Ctx) Accepts(offers ...string) string { + if len(offers) == 0 { + return "" + } + h := ctx.Get(fasthttp.HeaderAccept) + if h == "" { + return offers[0] + } + + specs := strings.Split(h, ",") + for _, offer := range offers { + mimetype := getType(offer) + // if mimetype != "" { + // mimetype = strings.Split(mimetype, ";")[0] + // } else { + // mimetype = offer + // } + for _, spec := range specs { + spec = strings.TrimSpace(spec) + if strings.HasPrefix(spec, "*/*") { + return offer + } + + if strings.HasPrefix(spec, mimetype) { + return offer + } + + if strings.Contains(spec, "/*") { + if strings.HasPrefix(spec, strings.Split(mimetype, "/")[0]) { + return offer + } + } + } + } + return "" +} + +// AcceptsCharsets : https://fiber.wiki/context#acceptscharsets +func (ctx *Ctx) AcceptsCharsets(offers ...string) string { + if len(offers) == 0 { + return "" + } + + h := ctx.Get(fasthttp.HeaderAcceptCharset) + if h == "" { + return offers[0] + } + + specs := strings.Split(h, ",") + for _, offer := range offers { + for _, spec := range specs { + spec = strings.TrimSpace(spec) + if strings.HasPrefix(spec, "*") { + return offer + } + if strings.HasPrefix(spec, offer) { + return offer + } + } + } + return "" +} + +// AcceptsEncodings : https://fiber.wiki/context#acceptsencodings +func (ctx *Ctx) AcceptsEncodings(offers ...string) string { + if len(offers) == 0 { + return "" + } + + h := ctx.Get(fasthttp.HeaderAcceptEncoding) + if h == "" { + return offers[0] + } + + specs := strings.Split(h, ",") + for _, offer := range offers { + for _, spec := range specs { + spec = strings.TrimSpace(spec) + if strings.HasPrefix(spec, "*") { + return offer + } + if strings.HasPrefix(spec, offer) { + return offer + } + } + } + return "" +} + +// AcceptsLanguages : https://fiber.wiki/context#acceptslanguages +func (ctx *Ctx) AcceptsLanguages(offers ...string) string { + if len(offers) == 0 { + return "" + } + h := ctx.Get(fasthttp.HeaderAcceptLanguage) + if h == "" { + return offers[0] + } + + specs := strings.Split(h, ",") + for _, offer := range offers { + for _, spec := range specs { + spec = strings.TrimSpace(spec) + if strings.HasPrefix(spec, "*") { + return offer + } + if strings.HasPrefix(spec, offer) { + return offer + } + } + } + return "" +} + +// Append : https://fiber.wiki/context#append +func (ctx *Ctx) Append(field string, values ...string) { + if len(values) == 0 { + return + } + h := getString(ctx.Fasthttp.Response.Header.Peek(field)) + for i := range values { + if h == "" { + h += values[i] + } else { + h += ", " + values[i] + } + } + ctx.Set(field, h) +} + +// Attachment : https://fiber.wiki/context#attachment +func (ctx *Ctx) Attachment(name ...string) { + if len(name) > 0 { + filename := filepath.Base(name[0]) + ctx.Type(filepath.Ext(filename)) + ctx.Set(fasthttp.HeaderContentDisposition, `attachment; filename="`+filename+`"`) + return + } + ctx.Set(fasthttp.HeaderContentDisposition, "attachment") +} + +// BaseURL : https://fiber.wiki/context#baseurl +func (ctx *Ctx) BaseURL() string { + return ctx.Protocol() + "://" + ctx.Hostname() +} + +// Body : https://fiber.wiki/context#body +func (ctx *Ctx) Body(args ...interface{}) string { + if len(args) == 0 { + return getString(ctx.Fasthttp.Request.Body()) + } + + if len(args) == 1 { + switch arg := args[0].(type) { + case string: + return getString(ctx.Fasthttp.Request.PostArgs().Peek(arg)) + case []byte: + return getString(ctx.Fasthttp.Request.PostArgs().PeekBytes(arg)) + case func(string, string): + ctx.Fasthttp.Request.PostArgs().VisitAll(func(k []byte, v []byte) { + arg(getString(k), getString(v)) + }) + default: + return getString(ctx.Fasthttp.Request.Body()) + } + } + return "" +} + +// BodyParser : https://fiber.wiki/context#bodyparser +func (ctx *Ctx) BodyParser(v interface{}) error { + ctype := getString(ctx.Fasthttp.Request.Header.ContentType()) + // application/json + if strings.HasPrefix(ctype, MIMEApplicationJSON) { + return jsoniter.Unmarshal(ctx.Fasthttp.Request.Body(), v) + } + // application/xml text/xml + if strings.HasPrefix(ctype, MIMEApplicationXML) || strings.HasPrefix(ctype, MIMETextXML) { + return xml.Unmarshal(ctx.Fasthttp.Request.Body(), v) + } + // application/x-www-form-urlencoded + if strings.HasPrefix(ctype, MIMEApplicationForm) { + data, err := url.ParseQuery(getString(ctx.Fasthttp.PostBody())) + if err != nil { + return err + } + return schemaDecoder.Decode(v, data) + } + // multipart/form-data + if strings.HasPrefix(ctype, MIMEMultipartForm) { + data, err := ctx.Fasthttp.MultipartForm() + if err != nil { + return err + } + return schemaDecoder.Decode(v, data.Value) + + } + return fmt.Errorf("cannot parse content-type: %v", ctype) +} + +// ClearCookie : https://fiber.wiki/context#clearcookie +func (ctx *Ctx) ClearCookie(name ...string) { + if len(name) > 0 { + for i := range name { + //ctx.Fasthttp.Request.Header.DelAllCookies() + ctx.Fasthttp.Response.Header.DelClientCookie(name[i]) + } + return + } + //ctx.Fasthttp.Response.Header.DelAllCookies() + ctx.Fasthttp.Request.Header.VisitAllCookie(func(k, v []byte) { + ctx.Fasthttp.Response.Header.DelClientCookie(getString(k)) + }) +} + +// Cookie : https://fiber.wiki/context#cookie +func (ctx *Ctx) Cookie(key, value string, options ...interface{}) { + cook := &fasthttp.Cookie{} + + cook.SetKey(key) + cook.SetValue(value) + + if len(options) > 0 { + switch opt := options[0].(type) { + case *Cookie: + if opt.Expire > 0 { + cook.SetExpire(time.Unix(int64(opt.Expire), 0)) + } + if opt.MaxAge > 0 { + cook.SetMaxAge(opt.MaxAge) + } + if opt.Domain != "" { + cook.SetDomain(opt.Domain) + } + if opt.Path != "" { + cook.SetPath(opt.Path) + } + if opt.HTTPOnly { + cook.SetHTTPOnly(opt.HTTPOnly) + } + if opt.Secure { + cook.SetSecure(opt.Secure) + } + if opt.SameSite != "" { + sameSite := fasthttp.CookieSameSiteDefaultMode + if strings.EqualFold(opt.SameSite, "lax") { + sameSite = fasthttp.CookieSameSiteLaxMode + } else if strings.EqualFold(opt.SameSite, "strict") { + sameSite = fasthttp.CookieSameSiteStrictMode + } else if strings.EqualFold(opt.SameSite, "none") { + sameSite = fasthttp.CookieSameSiteNoneMode + } + // } else { + // sameSite = fasthttp.CookieSameSiteDisabled + // } + cook.SetSameSite(sameSite) + } + default: + log.Println("Cookie: Invalid &Cookie{} struct") + } + } + + ctx.Fasthttp.Response.Header.SetCookie(cook) +} + +// Cookies : https://fiber.wiki/context#cookies +func (ctx *Ctx) Cookies(args ...interface{}) string { + if len(args) == 0 { + return ctx.Get(fasthttp.HeaderCookie) + } + + switch arg := args[0].(type) { + case string: + return getString(ctx.Fasthttp.Request.Header.Cookie(arg)) + case []byte: + return getString(ctx.Fasthttp.Request.Header.CookieBytes(arg)) + case func(string, string): + ctx.Fasthttp.Request.Header.VisitAllCookie(func(k, v []byte) { + arg(getString(k), getString(v)) + }) + default: + return ctx.Get(fasthttp.HeaderCookie) + } + + return "" +} + +// Download : https://fiber.wiki/context#download +func (ctx *Ctx) Download(file string, name ...string) { + filename := filepath.Base(file) + + if len(name) > 0 { + filename = name[0] + } + + ctx.Set(fasthttp.HeaderContentDisposition, "attachment; filename="+filename) + ctx.SendFile(file) +} + +// Error returns err that is passed via Next(err) +func (ctx *Ctx) Error() error { + return ctx.error +} + +// Format : https://fiber.wiki/context#format +func (ctx *Ctx) Format(args ...interface{}) { + var body string + + accept := ctx.Accepts("html", "json") + + for i := range args { + switch arg := args[i].(type) { + case string: + body = arg + case []byte: + body = getString(arg) + default: + body = fmt.Sprintf("%v", arg) + } + switch accept { + case "html": + ctx.SendString("

" + body + "

") + case "json": + if err := ctx.JSON(body); err != nil { + log.Println("Format: error serializing json ", err) + } + default: + ctx.SendString(body) + } + } +} + +// FormFile : https://fiber.wiki/context#formfile +func (ctx *Ctx) FormFile(key string) (*multipart.FileHeader, error) { + return ctx.Fasthttp.FormFile(key) +} + +// FormValue : https://fiber.wiki/context#formvalue +func (ctx *Ctx) FormValue(key string) string { + return getString(ctx.Fasthttp.FormValue(key)) +} + +// Fresh : https://fiber.wiki/context#fresh +func (ctx *Ctx) Fresh() bool { + return false +} + +// Get : https://fiber.wiki/context#get +func (ctx *Ctx) Get(key string) string { + if key == "referrer" { + key = "referer" + } + return getString(ctx.Fasthttp.Request.Header.Peek(key)) +} + +// Hostname : https://fiber.wiki/context#hostname +func (ctx *Ctx) Hostname() string { + return getString(ctx.Fasthttp.URI().Host()) +} + +// IP : https://fiber.wiki/context#Ip +func (ctx *Ctx) IP() string { + return ctx.Fasthttp.RemoteIP().String() +} + +// IPs : https://fiber.wiki/context#ips +func (ctx *Ctx) IPs() []string { + ips := strings.Split(ctx.Get(fasthttp.HeaderXForwardedFor), ",") + for i := range ips { + ips[i] = strings.TrimSpace(ips[i]) + } + return ips +} + +// Is : https://fiber.wiki/context#is +func (ctx *Ctx) Is(ext string) bool { + if ext[0] != '.' { + ext = "." + ext + } + + exts, _ := mime.ExtensionsByType(ctx.Get(fasthttp.HeaderContentType)) + if len(exts) > 0 { + for _, item := range exts { + if item == ext { + return true + } + } + } + return false +} + +// JSON : https://fiber.wiki/context#json +func (ctx *Ctx) JSON(v interface{}) error { + ctx.Fasthttp.Response.Header.SetContentType(MIMEApplicationJSON) + raw, err := jsoniter.Marshal(&v) + if err != nil { + ctx.Fasthttp.Response.SetBodyString("") + return err + } + ctx.Fasthttp.Response.SetBodyString(getString(raw)) + + return nil +} + +// JSONP : https://fiber.wiki/context#jsonp +func (ctx *Ctx) JSONP(v interface{}, cb ...string) error { + raw, err := jsoniter.Marshal(&v) + if err != nil { + return err + } + + str := "callback(" + if len(cb) > 0 { + str = cb[0] + "(" + } + str += getString(raw) + ");" + + ctx.Set(fasthttp.HeaderXContentTypeOptions, "nosniff") + ctx.Fasthttp.Response.Header.SetContentType(MIMEApplicationJavaScript) + ctx.Fasthttp.Response.SetBodyString(str) + + return nil +} + +// Links : https://fiber.wiki/context#links +func (ctx *Ctx) Links(link ...string) { + h := "" + for i, l := range link { + if i%2 == 0 { + h += "<" + l + ">" + } else { + h += `; rel="` + l + `",` + } + } + + if len(link) > 0 { + h = strings.TrimSuffix(h, ",") + ctx.Set(fasthttp.HeaderLink, h) + } +} + +// Locals : https://fiber.wiki/context#locals +func (ctx *Ctx) Locals(key string, val ...interface{}) interface{} { + if len(val) == 0 { + return ctx.Fasthttp.UserValue(key) + } + ctx.Fasthttp.SetUserValue(key, val[0]) + return nil +} + +// Location : https://fiber.wiki/context#location +func (ctx *Ctx) Location(path string) { + ctx.Set(fasthttp.HeaderLocation, path) +} + +// Method : https://fiber.wiki/context#method +func (ctx *Ctx) Method() string { + return getString(ctx.Fasthttp.Request.Header.Method()) +} + +// MultipartForm : https://fiber.wiki/context#multipartform +func (ctx *Ctx) MultipartForm() (*multipart.Form, error) { + return ctx.Fasthttp.MultipartForm() +} + +// Next : https://fiber.wiki/context#next +func (ctx *Ctx) Next(err ...error) { + ctx.route = nil + ctx.next = true + ctx.params = nil + ctx.values = nil + if len(err) > 0 { + ctx.error = err[0] + } +} + +// OriginalURL : https://fiber.wiki/context#originalurl +func (ctx *Ctx) OriginalURL() string { + return getString(ctx.Fasthttp.Request.Header.RequestURI()) +} + +// Params : https://fiber.wiki/context#params +func (ctx *Ctx) Params(key string) string { + if ctx.params == nil { + return "" + } + for i := 0; i < len(*ctx.params); i++ { + if (*ctx.params)[i] == key { + return ctx.values[i] + } + } + return "" +} + +// Path : https://fiber.wiki/context#path +func (ctx *Ctx) Path() string { + return getString(ctx.Fasthttp.URI().Path()) +} + +// Protocol : https://fiber.wiki/context#protocol +func (ctx *Ctx) Protocol() string { + if ctx.Fasthttp.IsTLS() { + return "https" + } + return "http" +} + +// Query : https://fiber.wiki/context#query +func (ctx *Ctx) Query(key string) string { + return getString(ctx.Fasthttp.QueryArgs().Peek(key)) +} + +// Range : https://fiber.wiki/context#range +func (ctx *Ctx) Range() { + // https://expressjs.com/en/api.html#req.range + // https://github.com/jshttp/range-parser/blob/master/index.js + // r := ctx.Fasthttp.Request.Header.Peek(fasthttp.HeaderRange) + // *magic* +} + +// Redirect : https://fiber.wiki/context#redirect +func (ctx *Ctx) Redirect(path string, status ...int) { + code := 302 + if len(status) > 0 { + code = status[0] + } + + ctx.Set(fasthttp.HeaderLocation, path) + ctx.Fasthttp.Response.SetStatusCode(code) +} + +// Render : https://fiber.wiki/context#render +func (ctx *Ctx) Render(file string, data interface{}, e ...string) error { + var err error + var raw []byte + var html string + var engine string + + if len(e) > 0 { + engine = e[0] + } else if ctx.app.Settings.ViewEngine != "" { + engine = ctx.app.Settings.ViewEngine + } else { + engine = filepath.Ext(file)[1:] + } + if ctx.app.Settings.ViewFolder != "" { + file = filepath.Join(ctx.app.Settings.ViewFolder, file) + } + if ctx.app.Settings.ViewExtension != "" { + file = file + ctx.app.Settings.ViewExtension + } + if raw, err = ioutil.ReadFile(filepath.Clean(file)); err != nil { + return err + } + + switch engine { + case "amber": // https://github.com/eknkc/amber + var buf bytes.Buffer + var tmpl *template.Template + + if tmpl, err = amber.Compile(getString(raw), amber.DefaultOptions); err != nil { + return err + } + if err = tmpl.Execute(&buf, data); err != nil { + return err + } + html = buf.String() + + case "handlebars": // https://github.com/aymerick/raymond + if html, err = handlebars.Render(getString(raw), data); err != nil { + return err + } + case "mustache": // https://github.com/cbroglie/mustache + if html, err = mustache.Render(getString(raw), data); err != nil { + return err + } + case "pug": // https://github.com/Joker/jade + var parsed string + var buf bytes.Buffer + var tmpl *template.Template + if parsed, err = pug.Parse("", raw); err != nil { + return err + } + if tmpl, err = template.New("").Parse(parsed); err != nil { + return err + } + if err = tmpl.Execute(&buf, data); err != nil { + return err + } + html = buf.String() + + default: // https://golang.org/pkg/text/template/ + var buf bytes.Buffer + var tmpl *template.Template + + if tmpl, err = template.New("").Parse(getString(raw)); err != nil { + return err + } + if err = tmpl.Execute(&buf, data); err != nil { + return err + } + html = buf.String() + } + ctx.Set("Content-Type", "text/html") + ctx.SendString(html) + return err +} + +// Route : https://fiber.wiki/context#route +func (ctx *Ctx) Route() *Route { + return ctx.route +} + +// SaveFile : https://fiber.wiki/context#secure +func (ctx *Ctx) SaveFile(fh *multipart.FileHeader, path string) error { + return fasthttp.SaveMultipartFile(fh, path) +} + +// Secure : https://fiber.wiki/context#secure +func (ctx *Ctx) Secure() bool { + return ctx.Fasthttp.IsTLS() +} + +// Send : https://fiber.wiki/context#send +func (ctx *Ctx) Send(args ...interface{}) { + if len(args) == 0 { + return + } + + switch body := args[0].(type) { + case string: + ctx.Fasthttp.Response.SetBodyString(body) + case []byte: + ctx.Fasthttp.Response.SetBodyString(getString(body)) + default: + ctx.Fasthttp.Response.SetBodyString(fmt.Sprintf("%v", body)) + } +} + +// SendBytes : https://fiber.wiki/context#sendbytes +func (ctx *Ctx) SendBytes(body []byte) { + ctx.Fasthttp.Response.SetBodyString(getString(body)) +} + +// SendFile : https://fiber.wiki/context#sendfile +func (ctx *Ctx) SendFile(file string, gzip ...bool) { + // Disable gzipping + if len(gzip) > 0 && !gzip[0] { + fasthttp.ServeFileUncompressed(ctx.Fasthttp, file) + return + } + fasthttp.ServeFile(ctx.Fasthttp, file) + // https://github.com/valyala/fasthttp/blob/master/fs.go#L81 + //ctx.Type(filepath.Ext(path)) + //ctx.Fasthttp.SendFile(path) +} + +// SendStatus : https://fiber.wiki/context#sendstatus +func (ctx *Ctx) SendStatus(status int) { + ctx.Fasthttp.Response.SetStatusCode(status) + // Only set status body when there is no response body + if len(ctx.Fasthttp.Response.Body()) == 0 { + ctx.Fasthttp.Response.SetBodyString(getStatus(status)) + } +} + +// SendString : https://fiber.wiki/context#sendstring +func (ctx *Ctx) SendString(body string) { + ctx.Fasthttp.Response.SetBodyString(body) +} + +// Set : https://fiber.wiki/context#set +func (ctx *Ctx) Set(key string, val string) { + ctx.Fasthttp.Response.Header.SetCanonical(getBytes(key), getBytes(val)) +} + +// Subdomains : https://fiber.wiki/context#subdomains +func (ctx *Ctx) Subdomains(offset ...int) (subs []string) { + o := 2 + if len(offset) > 0 { + o = offset[0] + } + subs = strings.Split(ctx.Hostname(), ".") + subs = subs[:len(subs)-o] + return subs +} + +// SignedCookies : https://fiber.wiki/context#signedcookies +func (ctx *Ctx) SignedCookies() { + +} + +// Stale : https://fiber.wiki/context#stale +func (ctx *Ctx) Stale() bool { + return !ctx.Fresh() +} + +// Status : https://fiber.wiki/context#status +func (ctx *Ctx) Status(status int) *Ctx { + ctx.Fasthttp.Response.SetStatusCode(status) + return ctx +} + +// Type : https://fiber.wiki/context#type +func (ctx *Ctx) Type(ext string) *Ctx { + ctx.Fasthttp.Response.Header.SetContentType(getType(ext)) + return ctx +} + +// Vary : https://fiber.wiki/context#vary +func (ctx *Ctx) Vary(fields ...string) { + if len(fields) == 0 { + return + } + + h := getString(ctx.Fasthttp.Response.Header.Peek(fasthttp.HeaderVary)) + for i := range fields { + if h == "" { + h += fields[i] + } else { + h += ", " + fields[i] + } + } + + ctx.Set(fasthttp.HeaderVary, h) +} + +// Write : https://fiber.wiki/context#write +func (ctx *Ctx) Write(args ...interface{}) { + for i := range args { + switch body := args[i].(type) { + case string: + ctx.Fasthttp.Response.AppendBodyString(body) + case []byte: + ctx.Fasthttp.Response.AppendBodyString(getString(body)) + default: + ctx.Fasthttp.Response.AppendBodyString(fmt.Sprintf("%v", body)) + } + } +} + +// XHR : https://fiber.wiki/context#xhr +func (ctx *Ctx) XHR() bool { + return ctx.Get(fasthttp.HeaderXRequestedWith) == "XMLHttpRequest" +} diff --git a/request_test.go b/context_test.go similarity index 57% rename from request_test.go rename to context_test.go index 00ac1e1b..f4a9aa22 100644 --- a/request_test.go +++ b/context_test.go @@ -3,6 +3,7 @@ package fiber import ( "bytes" "fmt" + "io/ioutil" "mime/multipart" "net/http" "net/http/httptest" @@ -92,11 +93,6 @@ func Test_AcceptsLanguages(t *testing.T) { if result != expect { t.Fatalf(`%s: Expecting %s, got %s`, t.Name(), expect, result) } - expect = "*" - result = c.AcceptsLanguages(expect) - if result != expect { - t.Fatalf(`%s: Expecting %s, got %s`, t.Name(), expect, result) - } }) req, _ := http.NewRequest("GET", "/test", nil) req.Header.Set("Accept-Language", "fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5") @@ -111,7 +107,6 @@ func Test_AcceptsLanguages(t *testing.T) { func Test_BaseURL(t *testing.T) { app := New() app.Get("/test", func(c *Ctx) { - c.BaseUrl() // deprecated expect := "http://google.com" result := c.BaseURL() if result != expect { @@ -127,29 +122,6 @@ func Test_BaseURL(t *testing.T) { t.Fatalf(`%s: StatusCode %v`, t.Name(), resp.StatusCode) } } -func Test_BasicAuth(t *testing.T) { - app := New() - app.Get("/test", func(c *Ctx) { - expect1 := "john" - expect2 := "doe" - result1, result2, _ := c.BasicAuth() - if result1 != expect1 { - t.Fatalf(`%s: Expecting %s, got %s`, t.Name(), expect1, expect1) - } - if result2 != expect2 { - t.Fatalf(`%s: Expecting %s, got %s`, t.Name(), result2, expect2) - } - }) - req, _ := http.NewRequest("GET", "/test", nil) - req.SetBasicAuth("john", "doe") - resp, err := app.Test(req) - if err != nil { - t.Fatalf(`%s: %s`, t.Name(), err) - } - if resp.StatusCode != 200 { - t.Fatalf(`%s: StatusCode %v`, t.Name(), resp.StatusCode) - } -} func Test_Body(t *testing.T) { app := New() app.Post("/test", func(c *Ctx) { @@ -345,7 +317,6 @@ func Test_Hostname(t *testing.T) { func Test_IP(t *testing.T) { app := New() app.Get("/test", func(c *Ctx) { - c.Ip() // deprecated expect := "0.0.0.0" result := c.IP() if result != expect { @@ -364,7 +335,6 @@ func Test_IP(t *testing.T) { func Test_IPs(t *testing.T) { app := New() app.Get("/test", func(c *Ctx) { - c.Ips() // deprecated expect := []string{"0.0.0.0", "1.1.1.1"} result := c.IPs() if result[0] != expect[0] && result[1] != expect[1] { @@ -402,7 +372,6 @@ func Test_IPs(t *testing.T) { // t.Fatalf(`%s: StatusCode %v`, t.Name(), resp.StatusCode) // } // } - func Test_Locals(t *testing.T) { app := New() app.Use(func(c *Ctx) { @@ -509,7 +478,6 @@ func Test_MultipartForm(t *testing.T) { func Test_OriginalURL(t *testing.T) { app := New() app.Get("/test", func(c *Ctx) { - c.OriginalUrl() // deprecated expect := "/test?search=demo" result := c.OriginalURL() if result != expect { @@ -682,10 +650,6 @@ func Test_Subdomains(t *testing.T) { if result[0] != expect[0] && result[1] != expect[1] { t.Fatalf(`%s: Expecting %s, got %s`, t.Name(), expect, result) } - result = c.Subdomains(1) - if result[0] != expect[0] && result[1] != expect[1] { - t.Fatalf(`%s: Expecting %s, got %s`, t.Name(), expect, result) - } }) req, _ := http.NewRequest("GET", "http://john.doe.google.com/test", nil) resp, err := app.Test(req) @@ -699,7 +663,6 @@ func Test_Subdomains(t *testing.T) { func Test_XHR(t *testing.T) { app := New() app.Get("/test", func(c *Ctx) { - c.Xhr() // deprecated expect := true result := c.XHR() if result != expect { @@ -716,3 +679,493 @@ func Test_XHR(t *testing.T) { t.Fatalf(`%s: StatusCode %v`, t.Name(), resp.StatusCode) } } + +func Test_Append(t *testing.T) { + app := New() + app.Get("/test", func(c *Ctx) { + c.Append("X-Test", "hel") + c.Append("X-Test", "lo", "world") + }) + req, _ := http.NewRequest("GET", "/test", nil) + resp, err := app.Test(req) + if err != nil { + t.Fatalf(`%s: %s`, t.Name(), err) + } + if resp.StatusCode != 200 { + t.Fatalf(`%s: StatusCode %v`, t.Name(), resp.StatusCode) + } + if resp.Header.Get("X-Test") != "hel, lo, world" { + t.Fatalf(`%s: Expecting %s`, t.Name(), "X-Test: hel, lo, world") + } +} +func Test_Attachment(t *testing.T) { + app := New() + app.Get("/test", func(c *Ctx) { + c.Attachment() + c.Attachment("./static/img/logo.png") + }) + req, _ := http.NewRequest("GET", "/test", nil) + resp, err := app.Test(req) + if err != nil { + t.Fatalf(`%s: %s`, t.Name(), err) + } + if resp.StatusCode != 200 { + t.Fatalf(`%s: StatusCode %v`, t.Name(), resp.StatusCode) + } + if resp.Header.Get("Content-Disposition") != `attachment; filename="logo.png"` { + t.Fatalf(`%s: Expecting %s`, t.Name(), `attachment; filename="logo.png"`) + } + if resp.Header.Get("Content-Type") != "image/png" { + t.Fatalf(`%s: Expecting %s`, t.Name(), "image/png") + } +} +func Test_ClearCookie(t *testing.T) { + app := New() + app.Get("/test", func(c *Ctx) { + c.ClearCookie() + }) + app.Get("/test2", func(c *Ctx) { + c.ClearCookie("john") + }) + req, _ := http.NewRequest("GET", "/test", nil) + req.AddCookie(&http.Cookie{Name: "john", Value: "doe"}) + resp, err := app.Test(req) + if err != nil { + t.Fatalf(`%s: %s`, t.Name(), err) + } + if resp.StatusCode != 200 { + t.Fatalf(`%s: StatusCode %v`, t.Name(), resp.StatusCode) + } + if !strings.Contains(resp.Header.Get("Set-Cookie"), "expires=") { + t.Fatalf(`%s: Expecting %s`, t.Name(), "expires=") + } + req, _ = http.NewRequest("GET", "/test2", nil) + req.AddCookie(&http.Cookie{Name: "john", Value: "doe"}) + resp, err = app.Test(req) + if err != nil { + t.Fatalf(`%s: %s`, t.Name(), err) + } + if resp.StatusCode != 200 { + t.Fatalf(`%s: StatusCode %v`, t.Name(), resp.StatusCode) + } + if !strings.Contains(resp.Header.Get("Set-Cookie"), "expires=") { + t.Fatalf(`%s: Expecting %s`, t.Name(), "expires=") + } +} +func Test_Cookie(t *testing.T) { + app := New() + app.Get("/test", func(c *Ctx) { + options := &Cookie{ + MaxAge: 60, + Domain: "example.com", + Path: "/", + HTTPOnly: true, + Secure: false, + SameSite: "lax", + } + c.Cookie("name", "john", options) + }) + req, _ := http.NewRequest("GET", "http://example.com/test", nil) + resp, err := app.Test(req) + if err != nil { + t.Fatalf(`%s: %s`, t.Name(), err) + } + if resp.StatusCode != 200 { + t.Fatalf(`%s: StatusCode %v`, t.Name(), resp.StatusCode) + } + if !strings.Contains(resp.Header.Get("Set-Cookie"), "name=john; max-age=60; domain=example.com; path=/; HttpOnly; SameSite=Lax") { + t.Fatalf(`%s: Expecting %s`, t.Name(), "name=john; max-age=60; domain=example.com; path=/; HttpOnly; SameSite=Lax") + } +} +func Test_Download(t *testing.T) { + // TODO +} +func Test_Format(t *testing.T) { + app := New() + app.Get("/test", func(c *Ctx) { + c.Format("Hello, World!") + }) + app.Get("/test2", func(c *Ctx) { + c.Format([]byte("Hello, World!")) + c.Format("Hello, World!") + }) + req, _ := http.NewRequest("GET", "http://example.com/test", nil) + req.Header.Set("Accept", "text/html") + resp, err := app.Test(req) + if err != nil { + t.Fatalf(`%s: %s`, t.Name(), err) + } + if resp.StatusCode != 200 { + t.Fatalf(`%s: StatusCode %v`, t.Name(), resp.StatusCode) + } + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + t.Fatalf(`%s: Error %s`, t.Name(), err) + } + if string(body) != "

Hello, World!

" { + t.Fatalf(`%s: Expecting %s`, t.Name(), "

Hello, World!

") + } + + req, _ = http.NewRequest("GET", "http://example.com/test2", nil) + req.Header.Set("Accept", "application/json") + resp, err = app.Test(req) + if err != nil { + t.Fatalf(`%s: %s`, t.Name(), err) + } + if resp.StatusCode != 200 { + t.Fatalf(`%s: StatusCode %v`, t.Name(), resp.StatusCode) + } + body, err = ioutil.ReadAll(resp.Body) + if err != nil { + t.Fatalf(`%s: Error %s`, t.Name(), err) + } + if string(body) != `"Hello, World!"` { + t.Fatalf(`%s: Expecting %s`, t.Name(), `"Hello, World!"`) + } +} +func Test_HeadersSent(t *testing.T) { + // TODO +} +func Test_JSON(t *testing.T) { + type SomeStruct struct { + Name string + Age uint8 + } + app := New() + app.Get("/test", func(c *Ctx) { + if err := c.JSON(""); err != nil { + t.Fatalf(`%s: %s`, t.Name(), err) + } + data := SomeStruct{ + Name: "Grame", + Age: 20, + } + if err := c.JSON(data); err != nil { + t.Fatalf(`%s: %s`, t.Name(), err) + } + }) + req, _ := http.NewRequest("GET", "http://example.com/test", nil) + resp, err := app.Test(req) + if err != nil { + t.Fatalf(`%s: %s`, t.Name(), err) + } + if resp.StatusCode != 200 { + t.Fatalf(`%s: StatusCode %v`, t.Name(), resp.StatusCode) + } + if resp.Header.Get("Content-Type") != "application/json" { + t.Fatalf(`%s: Expecting %s`, t.Name(), "application/json") + } + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + t.Fatalf(`%s: Error %s`, t.Name(), err) + } + if string(body) != `{"Name":"Grame","Age":20}` { + t.Fatalf(`%s: Expecting %s`, t.Name(), `{"Name":"Grame","Age":20}`) + } +} +func Test_JSONP(t *testing.T) { + type SomeStruct struct { + Name string + Age uint8 + } + app := New() + app.Get("/test", func(c *Ctx) { + if err := c.JSONP(""); err != nil { + t.Fatalf(`%s: %s`, t.Name(), err) + } + data := SomeStruct{ + Name: "Grame", + Age: 20, + } + if err := c.JSONP(data, "alwaysjohn"); err != nil { + t.Fatalf(`%s: %s`, t.Name(), err) + } + }) + req, _ := http.NewRequest("GET", "http://example.com/test", nil) + resp, err := app.Test(req) + if err != nil { + t.Fatalf(`%s: %s`, t.Name(), err) + } + if resp.StatusCode != 200 { + t.Fatalf(`%s: StatusCode %v`, t.Name(), resp.StatusCode) + } + if resp.Header.Get("Content-Type") != "application/javascript" { + t.Fatalf(`%s: Expecting %s`, t.Name(), "application/javascript") + } + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + t.Fatalf(`%s: Error %s`, t.Name(), err) + } + if string(body) != `alwaysjohn({"Name":"Grame","Age":20});` { + t.Fatalf(`%s: Expecting %s`, t.Name(), `alwaysjohn({"Name":"Grame","Age":20});`) + } +} +func Test_Links(t *testing.T) { + app := New() + app.Get("/test", func(c *Ctx) { + c.Links( + "http://api.example.com/users?page=2", "next", + "http://api.example.com/users?page=5", "last", + ) + }) + req, _ := http.NewRequest("GET", "http://example.com/test", nil) + resp, err := app.Test(req) + if err != nil { + t.Fatalf(`%s: %s`, t.Name(), err) + } + if resp.StatusCode != 200 { + t.Fatalf(`%s: StatusCode %v`, t.Name(), resp.StatusCode) + } + if resp.Header.Get("Link") != `; rel="next",; rel="last"` { + t.Fatalf(`%s: Expecting %s`, t.Name(), `Link: ; rel="next",; rel="last"`) + } +} +func Test_Location(t *testing.T) { + app := New() + app.Get("/test", func(c *Ctx) { + c.Location("http://example.com") + }) + req, _ := http.NewRequest("GET", "http://example.com/test", nil) + resp, err := app.Test(req) + if err != nil { + t.Fatalf(`%s: %s`, t.Name(), err) + } + if resp.StatusCode != 200 { + t.Fatalf(`%s: StatusCode %v`, t.Name(), resp.StatusCode) + } + if resp.Header.Get("Location") != "http://example.com" { + t.Fatalf(`%s: Expecting %s`, t.Name(), "http://example.com") + } +} +func Test_Next(t *testing.T) { + app := New() + app.Use("/", func(c *Ctx) { + c.Next() + }) + app.Get("/test", func(c *Ctx) { + c.Set("X-Next-Result", "Works") + }) + req, _ := http.NewRequest("GET", "http://example.com/test", nil) + resp, err := app.Test(req) + if err != nil { + t.Fatalf(`%s: %s`, t.Name(), err) + } + if resp.StatusCode != 200 { + t.Fatalf(`%s: StatusCode %v`, t.Name(), resp.StatusCode) + } + if resp.Header.Get("X-Next-Result") != "Works" { + t.Fatalf(`%s: Expecting %s`, t.Name(), "X-Next-Results: Works") + } +} +func Test_Redirect(t *testing.T) { + app := New() + app.Get("/test", func(c *Ctx) { + c.Redirect("http://example.com", 301) + }) + req, _ := http.NewRequest("GET", "http://example.com/test", nil) + resp, err := app.Test(req) + if err != nil { + t.Fatalf(`%s: %s`, t.Name(), err) + } + if resp.StatusCode != 301 { + t.Fatalf(`%s: StatusCode %v`, t.Name(), resp.StatusCode) + } + if resp.Header.Get("Location") != "http://example.com" { + t.Fatalf(`%s: Expecting %s`, t.Name(), "Location: http://example.com") + } +} +func Test_Render(t *testing.T) { + // TODO +} +func Test_Send(t *testing.T) { + app := New() + app.Get("/test", func(c *Ctx) { + c.Send([]byte("Hello, World")) + c.Send("Don't crash please") + c.Send(1337) + }) + req, _ := http.NewRequest("GET", "http://example.com/test", nil) + resp, err := app.Test(req) + if err != nil { + t.Fatalf(`%s: %s`, t.Name(), err) + } + if resp.StatusCode != 200 { + t.Fatalf(`%s: StatusCode %v`, t.Name(), resp.StatusCode) + } + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + t.Fatalf(`%s: Error %s`, t.Name(), err) + } + if string(body) != `1337` { + t.Fatalf(`%s: Expecting %s`, t.Name(), `1337`) + } +} +func Test_SendBytes(t *testing.T) { + app := New() + app.Get("/test", func(c *Ctx) { + c.SendBytes([]byte("Hello, World")) + }) + req, _ := http.NewRequest("GET", "http://example.com/test", nil) + resp, err := app.Test(req) + if err != nil { + t.Fatalf(`%s: %s`, t.Name(), err) + } + if resp.StatusCode != 200 { + t.Fatalf(`%s: StatusCode %v`, t.Name(), resp.StatusCode) + } + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + t.Fatalf(`%s: Error %s`, t.Name(), err) + } + if string(body) != `Hello, World` { + t.Fatalf(`%s: Expecting %s`, t.Name(), `Hello, World`) + } +} +func Test_SendStatus(t *testing.T) { + app := New() + app.Get("/test", func(c *Ctx) { + c.SendStatus(415) + }) + req, _ := http.NewRequest("GET", "http://example.com/test", nil) + resp, err := app.Test(req) + if err != nil { + t.Fatalf(`%s: %s`, t.Name(), err) + } + if resp.StatusCode != 415 { + t.Fatalf(`%s: StatusCode %v`, t.Name(), resp.StatusCode) + } + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + t.Fatalf(`%s: Error %s`, t.Name(), err) + } + if string(body) != `Unsupported Media Type` { + t.Fatalf(`%s: Expecting %s`, t.Name(), `Unsupported Media Type`) + } +} +func Test_SendString(t *testing.T) { + app := New() + app.Get("/test", func(c *Ctx) { + c.SendString("Don't crash please") + }) + req, _ := http.NewRequest("GET", "http://example.com/test", nil) + resp, err := app.Test(req) + if err != nil { + t.Fatalf(`%s: %s`, t.Name(), err) + } + if resp.StatusCode != 200 { + t.Fatalf(`%s: StatusCode %v`, t.Name(), resp.StatusCode) + } + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + t.Fatalf(`%s: Error %s`, t.Name(), err) + } + if string(body) != `Don't crash please` { + t.Fatalf(`%s: Expecting %s`, t.Name(), `Don't crash please`) + } +} +func Test_Set(t *testing.T) { + app := New() + app.Get("/test", func(c *Ctx) { + c.Set("X-1", "1") + c.Set("X-2", "2") + c.Set("X-3", "3") + }) + req, _ := http.NewRequest("GET", "http://example.com/test", nil) + resp, err := app.Test(req) + if err != nil { + t.Fatalf(`%s: %s`, t.Name(), err) + } + if resp.StatusCode != 200 { + t.Fatalf(`%s: StatusCode %v`, t.Name(), resp.StatusCode) + } + if resp.Header.Get("X-1") != "1" { + t.Fatalf(`%s: Expected %v`, t.Name(), "X-1: 1") + } + if resp.Header.Get("X-2") != "2" { + t.Fatalf(`%s: Expected %v`, t.Name(), "X-2: 2") + } + if resp.Header.Get("X-3") != "3" { + t.Fatalf(`%s: Expected %v`, t.Name(), "X-3: 3") + } +} +func Test_Status(t *testing.T) { + app := New() + app.Get("/test", func(c *Ctx) { + c.Status(400) + c.Status(415).Send("Hello, World") + }) + req, _ := http.NewRequest("GET", "http://example.com/test", nil) + resp, err := app.Test(req) + if err != nil { + t.Fatalf(`%s: %s`, t.Name(), err) + } + if resp.StatusCode != 415 { + t.Fatalf(`%s: StatusCode %v`, t.Name(), resp.StatusCode) + } + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + t.Fatalf(`%s: Error %s`, t.Name(), err) + } + if string(body) != `Hello, World` { + t.Fatalf(`%s: Expecting %s`, t.Name(), `Hello, World`) + } +} +func Test_Type(t *testing.T) { + app := New() + app.Get("/test", func(c *Ctx) { + c.Type(".json") + }) + req, _ := http.NewRequest("GET", "http://example.com/test", nil) + resp, err := app.Test(req) + if err != nil { + t.Fatalf(`%s: %s`, t.Name(), err) + } + if resp.StatusCode != 200 { + t.Fatalf(`%s: StatusCode %v`, t.Name(), resp.StatusCode) + } + if resp.Header.Get("Content-Type") != "application/json" { + t.Fatalf(`%s: Expected %v`, t.Name(), `Content-Type: application/json`) + } +} +func Test_Vary(t *testing.T) { + app := New() + app.Get("/test", func(c *Ctx) { + c.Vary("Origin") + c.Vary("User-Agent") + c.Vary("Accept-Encoding", "Accept") + }) + req, _ := http.NewRequest("GET", "http://example.com/test", nil) + resp, err := app.Test(req) + if err != nil { + t.Fatalf(`%s: %s`, t.Name(), err) + } + if resp.StatusCode != 200 { + t.Fatalf(`%s: StatusCode %v`, t.Name(), resp.StatusCode) + } + if resp.Header.Get("Vary") != "Origin, User-Agent, Accept-Encoding, Accept" { + t.Fatalf(`%s: Expected %v`, t.Name(), `Vary: Origin, User-Agent, Accept-Encoding, Accept`) + } +} +func Test_Write(t *testing.T) { + app := New() + app.Get("/test", func(c *Ctx) { + c.Write("Hello, ") + c.Write([]byte("World! ")) + c.Write(123) + }) + req, _ := http.NewRequest("GET", "http://example.com/test", nil) + resp, err := app.Test(req) + if err != nil { + t.Fatalf(`%s: %s`, t.Name(), err) + } + if resp.StatusCode != 200 { + t.Fatalf(`%s: StatusCode %v`, t.Name(), resp.StatusCode) + } + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + t.Fatalf(`%s: Error %s`, t.Name(), err) + } + if string(body) != `Hello, World! 123` { + t.Fatalf(`%s: Expecting %s`, t.Name(), `Hello, World! 123`) + } +} diff --git a/go.mod b/go.mod index 9dc1dc60..b2fa66fb 100644 --- a/go.mod +++ b/go.mod @@ -1,15 +1,15 @@ module github.com/gofiber/fiber -go 1.13 +go 1.11 require ( - github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 // indirect - github.com/CloudyKit/jet v2.1.2+incompatible + github.com/Joker/jade v1.0.0 github.com/aymerick/raymond v2.0.2+incompatible github.com/cbroglie/mustache v1.0.1 github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385 + github.com/fasthttp/websocket v1.4.1 github.com/gorilla/schema v1.1.0 github.com/json-iterator/go v1.1.9 github.com/valyala/fasthttp v1.9.0 - github.com/yosssi/ace v0.0.5 + gopkg.in/yaml.v2 v2.2.8 // indirect ) diff --git a/go.sum b/go.sum index 1db2ad22..49d340ff 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,6 @@ -github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 h1:sR+/8Yb4slttB4vD+b9btVEnWgL3Q00OBTzVT8B9C0c= -github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno= -github.com/CloudyKit/jet v2.1.2+incompatible h1:ybZoYzMBdoijK6I+Ke3vg9GZsmlKo/ZhKdNMWz0P26c= -github.com/CloudyKit/jet v2.1.2+incompatible/go.mod h1:HPYO+50pSWkPoj9Q/eq0aRGByCL6ScRlUmiEX5Zgm+w= +github.com/Joker/hpp v0.0.0-20180418125244-6893e659854a/go.mod h1:MzD2WMdSxvbHw5fM/OXOFily/lipJWRc9C1px0Mt0ZE= +github.com/Joker/jade v1.0.0 h1:lOCEPvTAtWfLpSZYMOv/g44MGQFAolbKh2khHHGu0Kc= +github.com/Joker/jade v1.0.0/go.mod h1:efZIdO0py/LtcJRSa/j2WEklMSAw84WV0zZVMxNToB8= github.com/aymerick/raymond v2.0.2+incompatible h1:VEp3GpgdAnv9B2GFyTvqgcKvY+mfKMjPOA3SbKLtnU0= github.com/aymerick/raymond v2.0.2+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g= github.com/cbroglie/mustache v1.0.1 h1:ivMg8MguXq/rrz2eu3tw6g3b16+PQhoTn6EZAhst2mw= @@ -10,13 +9,17 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385 h1:clC1lXBpe2kTj2VHdaIu9ajZQe4kcEY9j0NsnDDBZ3o= github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM= +github.com/fasthttp/websocket v1.4.1 h1:fisgNMCNCbIPM5GRRRTAckRrynbSzf76fevcJYJYnSM= +github.com/fasthttp/websocket v1.4.1/go.mod h1:toetUvZ3KISxtZERe0wzPPpnaN8GZCKHCowWctwA50o= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/gorilla/schema v1.1.0 h1:CamqUDOFUBqzrvxuz2vEwo8+SUdwsluFh7IlzJh30LY= github.com/gorilla/schema v1.1.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU= github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/klauspost/compress v1.4.0/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.8.2 h1:Bx0qjetmNjdFXASH02NSAREKpiaDwkO1DRZ3dV2KCcs= github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/klauspost/cpuid v0.0.0-20180405133222-e7e905edc00e/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/klauspost/cpuid v1.2.1 h1:vJi+O/nMdFt0vqm8NZBI6wzALWdA2X+egi0ogNyrC/w= github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= @@ -24,17 +27,21 @@ github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/savsgio/gotils v0.0.0-20190714152828-365999d0a274 h1:F52t1X2ziOrMcQMVHo8ZxwOrDTMAq6MrlKtL1Atu2wU= +github.com/savsgio/gotils v0.0.0-20190714152828-365999d0a274/go.mod h1:w803/Fg1m0hrp1ZT9KNfQe4E4+WOMMFLcgzPvOcye10= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasthttp v1.4.0/go.mod h1:4vX61m6KN+xDduDNwXrhIAVZaZaZiQ1luJk8LWSxF3s= github.com/valyala/fasthttp v1.9.0 h1:hNpmUdy/+ZXYpGy0OBfm7K0UQTzb73W0T0U4iJIVrMw= github.com/valyala/fasthttp v1.9.0/go.mod h1:FstJa9V+Pj9vQ7OJie2qMHdwemEDaDiSdBnvPM1Su9w= -github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a h1:0R4NLDRDZX6JcmhJgXi5E4b8Wg84ihbmUKp/GvSPEzc= github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio= -github.com/yosssi/ace v0.0.5 h1:tUkIP/BLdKqrlrPwcmH0shwEEhTRHoGnc1wFIWmaBUA= -github.com/yosssi/ace v0.0.5/go.mod h1:ALfIzm2vT7t5ZE7uoIZqF3TQ7SAOyupFZnkrF5id+K0= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/net v0.0.0-20180911220305-26e67e76b6c3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/group.go b/group.go new file mode 100644 index 00000000..78f6d2e0 --- /dev/null +++ b/group.go @@ -0,0 +1,121 @@ +package fiber + +import "strings" + +// Group ... +type Group struct { + prefix string + app *App +} + +// Group ... +func (app *App) Group(prefix string, args ...interface{}) *Group { + if len(args) > 0 { + app.register("USE", prefix, args...) + } + return &Group{ + prefix: prefix, + app: app, + } +} + +// Group ... +func (grp *Group) Group(newPrfx string, args ...interface{}) *Group { + var prefix = grp.prefix + if len(newPrfx) > 0 && newPrfx[0] != '/' && newPrfx[0] != '*' { + newPrfx = "/" + newPrfx + } + // When grouping, always remove single slash + if len(prefix) > 0 && newPrfx == "/" { + newPrfx = "" + } + // Prepent group prefix if exist + prefix = prefix + newPrfx + // Clean path by removing double "//" => "/" + prefix = strings.Replace(prefix, "//", "/", -1) + if len(args) > 0 { + grp.app.register("USE", prefix, args...) + } + return &Group{ + prefix: prefix, + app: grp.app, + } +} + +// Static ... +func (grp *Group) Static(args ...string) *Group { + grp.app.registerStatic(grp.prefix, args...) + return grp +} + +// WebSocket ... +func (grp *Group) WebSocket(args ...interface{}) *Group { + grp.app.register("GET", grp.prefix, args...) + return grp +} + +// Connect ... +func (grp *Group) Connect(args ...interface{}) *Group { + grp.app.register("CONNECT", grp.prefix, args...) + return grp +} + +// Put ... +func (grp *Group) Put(args ...interface{}) *Group { + grp.app.register("PUT", grp.prefix, args...) + return grp +} + +// Post ... +func (grp *Group) Post(args ...interface{}) *Group { + grp.app.register("POST", grp.prefix, args...) + return grp +} + +// Delete ... +func (grp *Group) Delete(args ...interface{}) *Group { + grp.app.register("DELETE", grp.prefix, args...) + return grp +} + +// Head ... +func (grp *Group) Head(args ...interface{}) *Group { + grp.app.register("HEAD", grp.prefix, args...) + return grp +} + +// Patch ... +func (grp *Group) Patch(args ...interface{}) *Group { + grp.app.register("PATCH", grp.prefix, args...) + return grp +} + +// Options ... +func (grp *Group) Options(args ...interface{}) *Group { + grp.app.register("OPTIONS", grp.prefix, args...) + return grp +} + +// Trace ... +func (grp *Group) Trace(args ...interface{}) *Group { + grp.app.register("TRACE", grp.prefix, args...) + return grp +} + +// Get ... +func (grp *Group) Get(args ...interface{}) *Group { + grp.app.register("GET", grp.prefix, args...) + return grp +} + +// All ... +func (grp *Group) All(args ...interface{}) *Group { + grp.app.register("ALL", grp.prefix, args...) + return grp +} + +// Use ... +func (grp *Group) Use(args ...interface{}) *Group { + grp.app.register("USE", grp.prefix, args...) + return grp +} diff --git a/request.go b/request.go deleted file mode 100644 index 207b32a3..00000000 --- a/request.go +++ /dev/null @@ -1,442 +0,0 @@ -// 🚀 Fiber is an Express.js inspired web framework written in Go with 💖 -// 📌 Please open an issue if you got suggestions or found a bug! -// 🖥 Links: https://github.com/gofiber/fiber, https://fiber.wiki - -// 🦸 Not all heroes wear capes, thank you to some amazing people -// 💖 @valyala, @erikdubbelboer, @savsgio, @julienschmidt, @koddr - -package fiber - -import ( - "encoding/base64" - "encoding/xml" - "fmt" - "mime" - "mime/multipart" - "net/url" - "strings" - - jsoniter "github.com/json-iterator/go" - fasthttp "github.com/valyala/fasthttp" -) - -// Accepts : https://fiber.wiki/context#accepts -func (ctx *Ctx) Accepts(offers ...string) string { - if len(offers) == 0 { - return "" - } - h := ctx.Get(fasthttp.HeaderAccept) - if h == "" { - return offers[0] - } - - specs := strings.Split(h, ",") - for _, offer := range offers { - mimetype := getType(offer) - // if mimetype != "" { - // mimetype = strings.Split(mimetype, ";")[0] - // } else { - // mimetype = offer - // } - for _, spec := range specs { - spec = strings.TrimSpace(spec) - if strings.HasPrefix(spec, "*/*") { - return offer - } - - if strings.HasPrefix(spec, mimetype) { - return offer - } - - if strings.Contains(spec, "/*") { - if strings.HasPrefix(spec, strings.Split(mimetype, "/")[0]) { - return offer - } - } - } - } - return "" -} - -// AcceptsCharsets : https://fiber.wiki/context#acceptscharsets -func (ctx *Ctx) AcceptsCharsets(offers ...string) string { - if len(offers) == 0 { - return "" - } - - h := ctx.Get(fasthttp.HeaderAcceptCharset) - if h == "" { - return offers[0] - } - - specs := strings.Split(h, ",") - for _, offer := range offers { - for _, spec := range specs { - spec = strings.TrimSpace(spec) - if strings.HasPrefix(spec, "*") { - return offer - } - if strings.HasPrefix(spec, offer) { - return offer - } - } - } - return "" -} - -// AcceptsEncodings : https://fiber.wiki/context#acceptsencodings -func (ctx *Ctx) AcceptsEncodings(offers ...string) string { - if len(offers) == 0 { - return "" - } - - h := ctx.Get(fasthttp.HeaderAcceptEncoding) - if h == "" { - return offers[0] - } - - specs := strings.Split(h, ",") - for _, offer := range offers { - for _, spec := range specs { - spec = strings.TrimSpace(spec) - if strings.HasPrefix(spec, "*") { - return offer - } - if strings.HasPrefix(spec, offer) { - return offer - } - } - } - return "" -} - -// AcceptsLanguages : https://fiber.wiki/context#acceptslanguages -func (ctx *Ctx) AcceptsLanguages(offers ...string) string { - if len(offers) == 0 { - return "" - } - h := ctx.Get(fasthttp.HeaderAcceptLanguage) - if h == "" { - return offers[0] - } - - specs := strings.Split(h, ",") - for _, offer := range offers { - for _, spec := range specs { - spec = strings.TrimSpace(spec) - if strings.HasPrefix(spec, "*") { - return offer - } - if strings.HasPrefix(spec, offer) { - return offer - } - } - } - return "" -} - -// BaseUrl will be removed in v2 -func (ctx *Ctx) BaseUrl() string { - fmt.Println("Fiber deprecated c.BaseUrl(), this will be removed in v2: Use c.BaseURL() instead") - return ctx.BaseURL() -} - -// BaseURL : https://fiber.wiki/context#baseurl -func (ctx *Ctx) BaseURL() string { - return ctx.Protocol() + "://" + ctx.Hostname() -} - -// BasicAuth : https://fiber.wiki/context#basicauth -func (ctx *Ctx) BasicAuth() (user, pass string, ok bool) { - fmt.Println("Fiber deprecated c.BasicAuth(), this will be removed in v2 and be available as a separate middleware") - auth := ctx.Get(fasthttp.HeaderAuthorization) - if auth == "" { - return - } - - const prefix = "Basic " - - // Case insensitive prefix match. - if len(auth) < len(prefix) || !strings.EqualFold(auth[:len(prefix)], prefix) { - return - } - - c, err := base64.StdEncoding.DecodeString(auth[len(prefix):]) - if err != nil { - return - } - - cs := getString(c) - s := strings.IndexByte(cs, ':') - if s < 0 { - return - } - - return cs[:s], cs[s+1:], true -} - -// Body : https://fiber.wiki/context#body -func (ctx *Ctx) Body(args ...interface{}) string { - if len(args) == 0 { - return getString(ctx.Fasthttp.Request.Body()) - } - - if len(args) == 1 { - switch arg := args[0].(type) { - case string: - return getString(ctx.Fasthttp.Request.PostArgs().Peek(arg)) - case []byte: - return getString(ctx.Fasthttp.Request.PostArgs().PeekBytes(arg)) - case func(string, string): - ctx.Fasthttp.Request.PostArgs().VisitAll(func(k []byte, v []byte) { - arg(getString(k), getString(v)) - }) - default: - return getString(ctx.Fasthttp.Request.Body()) - } - } - return "" -} - -// BodyParser : https://fiber.wiki/context#bodyparser -func (ctx *Ctx) BodyParser(v interface{}) error { - ctype := getString(ctx.Fasthttp.Request.Header.ContentType()) - // application/json - if strings.HasPrefix(ctype, mimeApplicationJSON) { - return jsoniter.Unmarshal(ctx.Fasthttp.Request.Body(), v) - } - // application/xml text/xml - if strings.HasPrefix(ctype, mimeApplicationXML) || strings.HasPrefix(ctype, mimeTextXML) { - return xml.Unmarshal(ctx.Fasthttp.Request.Body(), v) - } - // application/x-www-form-urlencoded - if strings.HasPrefix(ctype, mimeApplicationForm) { - data, err := url.ParseQuery(getString(ctx.Fasthttp.PostBody())) - if err != nil { - return err - } - return schemaDecoder.Decode(v, data) - } - // multipart/form-data - if strings.HasPrefix(ctype, mimeMultipartForm) { - data, err := ctx.Fasthttp.MultipartForm() - if err != nil { - return err - } - return schemaDecoder.Decode(v, data.Value) - - } - return fmt.Errorf("cannot parse content-type: %v", ctype) -} - -// Cookies : https://fiber.wiki/context#cookies -func (ctx *Ctx) Cookies(args ...interface{}) string { - if len(args) == 0 { - return ctx.Get(fasthttp.HeaderCookie) - } - - switch arg := args[0].(type) { - case string: - return getString(ctx.Fasthttp.Request.Header.Cookie(arg)) - case []byte: - return getString(ctx.Fasthttp.Request.Header.CookieBytes(arg)) - case func(string, string): - ctx.Fasthttp.Request.Header.VisitAllCookie(func(k, v []byte) { - arg(getString(k), getString(v)) - }) - default: - return ctx.Get(fasthttp.HeaderCookie) - } - - return "" -} - -// Error returns err that is passed via Next(err) -func (ctx *Ctx) Error() error { - return ctx.error -} - -// FormFile : https://fiber.wiki/context#formfile -func (ctx *Ctx) FormFile(key string) (*multipart.FileHeader, error) { - return ctx.Fasthttp.FormFile(key) -} - -// FormValue : https://fiber.wiki/context#formvalue -func (ctx *Ctx) FormValue(key string) string { - return getString(ctx.Fasthttp.FormValue(key)) -} - -// Fresh : https://fiber.wiki/context#fresh -func (ctx *Ctx) Fresh() bool { - return false -} - -// Get : https://fiber.wiki/context#get -func (ctx *Ctx) Get(key string) string { - if key == "referrer" { - key = "referer" - } - return getString(ctx.Fasthttp.Request.Header.Peek(key)) -} - -// Hostname : https://fiber.wiki/context#hostname -func (ctx *Ctx) Hostname() string { - return getString(ctx.Fasthttp.URI().Host()) -} - -// Ip will be removed in v2 -func (ctx *Ctx) Ip() string { - fmt.Println("Fiber deprecated c.Ip(), this will be removed in v2: Use c.IP() instead") - return ctx.IP() -} - -// IP : https://fiber.wiki/context#Ip -func (ctx *Ctx) IP() string { - return ctx.Fasthttp.RemoteIP().String() -} - -// Ips will be removed in v2 -func (ctx *Ctx) Ips() []string { // NOLINT - fmt.Println("Fiber deprecated c.Ips(), this will be removed in v2: Use c.IPs() instead") - return ctx.IPs() -} - -// IPs : https://fiber.wiki/context#ips -func (ctx *Ctx) IPs() []string { - ips := strings.Split(ctx.Get(fasthttp.HeaderXForwardedFor), ",") - for i := range ips { - ips[i] = strings.TrimSpace(ips[i]) - } - return ips -} - -// Is : https://fiber.wiki/context#is -func (ctx *Ctx) Is(ext string) bool { - if ext[0] != '.' { - ext = "." + ext - } - - exts, _ := mime.ExtensionsByType(ctx.Get(fasthttp.HeaderContentType)) - if len(exts) > 0 { - for _, item := range exts { - if item == ext { - return true - } - } - } - return false -} - -// Locals : https://fiber.wiki/context#locals -func (ctx *Ctx) Locals(key string, val ...interface{}) interface{} { - if len(val) == 0 { - return ctx.Fasthttp.UserValue(key) - } - - ctx.Fasthttp.SetUserValue(key, val[0]) - return nil -} - -// Method : https://fiber.wiki/context#method -func (ctx *Ctx) Method() string { - return getString(ctx.Fasthttp.Request.Header.Method()) -} - -// MultipartForm : https://fiber.wiki/context#multipartform -func (ctx *Ctx) MultipartForm() (*multipart.Form, error) { - return ctx.Fasthttp.MultipartForm() -} - -// OriginalUrl will be removed in v2 -func (ctx *Ctx) OriginalUrl() string { - fmt.Println("Fiber deprecated c.OriginalUrl(), this will be removed in v2: Use c.OriginalURL() instead") - return ctx.OriginalURL() -} - -// OriginalURL : https://fiber.wiki/context#originalurl -func (ctx *Ctx) OriginalURL() string { - return getString(ctx.Fasthttp.Request.Header.RequestURI()) -} - -// Params : https://fiber.wiki/context#params -func (ctx *Ctx) Params(key string) string { - for i := 0; i < len(*ctx.params); i++ { - if (*ctx.params)[i] == key { - return ctx.values[i] - } - } - return "" -} - -// Path : https://fiber.wiki/context#path -func (ctx *Ctx) Path() string { - return getString(ctx.Fasthttp.URI().Path()) -} - -// Protocol : https://fiber.wiki/context#protocol -func (ctx *Ctx) Protocol() string { - if ctx.Fasthttp.IsTLS() { - return "https" - } - return "http" -} - -// Query : https://fiber.wiki/context#query -func (ctx *Ctx) Query(key string) string { - return getString(ctx.Fasthttp.QueryArgs().Peek(key)) -} - -// Range : https://fiber.wiki/context#range -func (ctx *Ctx) Range() { - // https://expressjs.com/en/api.html#req.range - // https://github.com/jshttp/range-parser/blob/master/index.js - // r := ctx.Fasthttp.Request.Header.Peek(fasthttp.HeaderRange) - // *magic* -} - -// Route : https://fiber.wiki/context#route -func (ctx *Ctx) Route() *Route { - return ctx.route -} - -// SaveFile : https://fiber.wiki/context#secure -func (ctx *Ctx) SaveFile(fh *multipart.FileHeader, path string) error { - return fasthttp.SaveMultipartFile(fh, path) -} - -// Secure : https://fiber.wiki/context#secure -func (ctx *Ctx) Secure() bool { - return ctx.Fasthttp.IsTLS() -} - -// SignedCookies : https://fiber.wiki/context#signedcookies -func (ctx *Ctx) SignedCookies() { - -} - -// Stale : https://fiber.wiki/context#stale -func (ctx *Ctx) Stale() bool { - return !ctx.Fresh() -} - -// Subdomains : https://fiber.wiki/context#subdomains -func (ctx *Ctx) Subdomains(offset ...int) (subs []string) { - o := 2 - if len(offset) > 0 { - o = offset[0] - } - subs = strings.Split(ctx.Hostname(), ".") - subs = subs[:len(subs)-o] - return subs -} - -// Xhr will be removed in v2 -func (ctx *Ctx) Xhr() bool { - fmt.Println("Fiber deprecated c.Xhr(), this will be removed in v2: Use c.XHR() instead") - return ctx.XHR() -} - -// XHR : https://fiber.wiki/context#xhr -func (ctx *Ctx) XHR() bool { - return ctx.Get(fasthttp.HeaderXRequestedWith) == "XMLHttpRequest" -} diff --git a/response.go b/response.go deleted file mode 100644 index 30244065..00000000 --- a/response.go +++ /dev/null @@ -1,483 +0,0 @@ -// 🚀 Fiber is an Express.js inspired web framework written in Go with 💖 -// 📌 Please open an issue if you got suggestions or found a bug! -// 🖥 Links: https://github.com/gofiber/fiber, https://fiber.wiki - -// 🦸 Not all heroes wear capes, thank you to some amazing people -// 💖 @valyala, @erikdubbelboer, @savsgio, @julienschmidt, @koddr - -package fiber - -import ( - "bytes" - "encoding/xml" - "fmt" - "html/template" - "io/ioutil" - "log" - "path/filepath" - "strings" - "time" - - "github.com/CloudyKit/jet" - "github.com/aymerick/raymond" - "github.com/cbroglie/mustache" - "github.com/eknkc/amber" - jsoniter "github.com/json-iterator/go" - fasthttp "github.com/valyala/fasthttp" - "github.com/yosssi/ace" -) - -// Cookie : struct -type Cookie struct { - Expire int // time.Unix(1578981376, 0) - MaxAge int - Domain string - Path string - - HTTPOnly bool - Secure bool - SameSite string -} - -// Append : https://fiber.wiki/context#append -func (ctx *Ctx) Append(field string, values ...string) { - if len(values) == 0 { - return - } - h := getString(ctx.Fasthttp.Response.Header.Peek(field)) - for i := range values { - if h == "" { - h += values[i] - } else { - h += ", " + values[i] - } - } - ctx.Set(field, h) -} - -// Attachment : https://fiber.wiki/context#attachment -func (ctx *Ctx) Attachment(name ...string) { - if len(name) > 0 { - filename := filepath.Base(name[0]) - ctx.Type(filepath.Ext(filename)) - ctx.Set(fasthttp.HeaderContentDisposition, `attachment; filename="`+filename+`"`) - return - } - ctx.Set(fasthttp.HeaderContentDisposition, "attachment") -} - -// ClearCookie : https://fiber.wiki/context#clearcookie -func (ctx *Ctx) ClearCookie(name ...string) { - if len(name) > 0 { - for i := range name { - //ctx.Fasthttp.Request.Header.DelAllCookies() - ctx.Fasthttp.Response.Header.DelClientCookie(name[i]) - } - return - } - //ctx.Fasthttp.Response.Header.DelAllCookies() - ctx.Fasthttp.Request.Header.VisitAllCookie(func(k, v []byte) { - ctx.Fasthttp.Response.Header.DelClientCookie(getString(k)) - }) -} - -// Cookie : https://fiber.wiki/context#cookie -func (ctx *Ctx) Cookie(key, value string, options ...interface{}) { - cook := &fasthttp.Cookie{} - - cook.SetKey(key) - cook.SetValue(value) - - if len(options) > 0 { - switch opt := options[0].(type) { - case *Cookie: - if opt.Expire > 0 { - cook.SetExpire(time.Unix(int64(opt.Expire), 0)) - } - if opt.MaxAge > 0 { - cook.SetMaxAge(opt.MaxAge) - } - if opt.Domain != "" { - cook.SetDomain(opt.Domain) - } - if opt.Path != "" { - cook.SetPath(opt.Path) - } - if opt.HTTPOnly { - cook.SetHTTPOnly(opt.HTTPOnly) - } - if opt.Secure { - cook.SetSecure(opt.Secure) - } - if opt.SameSite != "" { - sameSite := fasthttp.CookieSameSiteDefaultMode - if strings.EqualFold(opt.SameSite, "lax") { - sameSite = fasthttp.CookieSameSiteLaxMode - } else if strings.EqualFold(opt.SameSite, "strict") { - sameSite = fasthttp.CookieSameSiteStrictMode - } else if strings.EqualFold(opt.SameSite, "none") { - sameSite = fasthttp.CookieSameSiteNoneMode - } - // } else { - // sameSite = fasthttp.CookieSameSiteDisabled - // } - cook.SetSameSite(sameSite) - } - default: - log.Println("Cookie: Invalid &Cookie{} struct") - } - } - - ctx.Fasthttp.Response.Header.SetCookie(cook) -} - -// Download : https://fiber.wiki/context#download -func (ctx *Ctx) Download(file string, name ...string) { - filename := filepath.Base(file) - - if len(name) > 0 { - filename = name[0] - } - - ctx.Set(fasthttp.HeaderContentDisposition, "attachment; filename="+filename) - ctx.SendFile(file) -} - -// End : https://fiber.wiki/context#end -func (ctx *Ctx) End() { - -} - -// Format : https://fiber.wiki/context#format -func (ctx *Ctx) Format(args ...interface{}) { - var body string - - accept := ctx.Accepts("html", "json") - - for i := range args { - switch arg := args[i].(type) { - case string: - body = arg - case []byte: - body = getString(arg) - default: - body = fmt.Sprintf("%v", arg) - } - switch accept { - case "html": - ctx.SendString("

" + body + "

") - case "json": - if err := ctx.JSON(body); err != nil { - log.Println("Format: error serializing json ", err) - } - default: - ctx.SendString(body) - } - } -} - -// HeadersSent indicates if the app sent HTTP headers for the response. -// func (ctx *Ctx) HeadersSent() {} - -// Json will be removed in v2 -func (ctx *Ctx) Json(v interface{}) error { - fmt.Println("Fiber deprecated c.Json(), this will be removed in v2: Use c.JSON() instead") - return ctx.JSON(v) -} - -// JSON : https://fiber.wiki/context#json -func (ctx *Ctx) JSON(v interface{}) error { - ctx.Fasthttp.Response.Header.SetContentType(mimeApplicationJSON) - raw, err := jsoniter.Marshal(&v) - if err != nil { - ctx.Fasthttp.Response.SetBodyString("") - return err - } - ctx.Fasthttp.Response.SetBodyString(getString(raw)) - - return nil -} - -// JsonBytes ... -func (ctx *Ctx) JsonBytes(raw []byte) { - ctx.JSONBytes(raw) -} - -// JSONBytes will be removed in v2 -func (ctx *Ctx) JSONBytes(raw []byte) { - fmt.Println("Fiber deprecated c.JSONBytes(), this will function be removed in v2") - ctx.Fasthttp.Response.Header.SetContentType(mimeApplicationJSON) - ctx.Fasthttp.Response.SetBodyString(getString(raw)) -} - -// Jsonp will be removed in v2 -func (ctx *Ctx) Jsonp(v interface{}, cb ...string) error { - fmt.Println("Fiber deprecated c.Jsonp(), this will be removed in v2: Use c.JSONP() instead") - return ctx.JSONP(v, cb...) -} - -// JSONP : https://fiber.wiki/context#jsonp -func (ctx *Ctx) JSONP(v interface{}, cb ...string) error { - raw, err := jsoniter.Marshal(&v) - if err != nil { - return err - } - - str := "callback(" - if len(cb) > 0 { - str = cb[0] + "(" - } - str += getString(raw) + ");" - - ctx.Set(fasthttp.HeaderXContentTypeOptions, "nosniff") - ctx.Fasthttp.Response.Header.SetContentType(mimeApplicationJavascript) - ctx.Fasthttp.Response.SetBodyString(str) - - return nil -} - -// JsonString ... -func (ctx *Ctx) JsonString(raw string) { - ctx.JSONString(raw) -} - -// JSONString will be removed in v2 -func (ctx *Ctx) JSONString(raw string) { - fmt.Println("Fiber deprecated c.JSONString(), this function will be removed in v2") - ctx.Fasthttp.Response.Header.SetContentType(mimeApplicationJSON) - ctx.Fasthttp.Response.SetBodyString(raw) -} - -// Links : https://fiber.wiki/context#links -func (ctx *Ctx) Links(link ...string) { - h := "" - for i, l := range link { - if i%2 == 0 { - h += "<" + l + ">" - } else { - h += `; rel="` + l + `",` - } - } - - if len(link) > 0 { - h = strings.TrimSuffix(h, ",") - ctx.Set(fasthttp.HeaderLink, h) - } -} - -// Location : https://fiber.wiki/context#location -func (ctx *Ctx) Location(path string) { - ctx.Set(fasthttp.HeaderLocation, path) -} - -// Next : https://fiber.wiki/context#next -func (ctx *Ctx) Next(err ...error) { - ctx.route = nil - ctx.next = true - ctx.params = nil - ctx.values = nil - if len(err) > 0 { - ctx.error = err[0] - } -} - -// Redirect : https://fiber.wiki/context#redirect -func (ctx *Ctx) Redirect(path string, status ...int) { - code := 302 - if len(status) > 0 { - code = status[0] - } - - ctx.Set(fasthttp.HeaderLocation, path) - ctx.Fasthttp.Response.SetStatusCode(code) -} - -// Render : https://fiber.wiki/context#render -func (ctx *Ctx) Render(file string, v ...interface{}) error { - var err error - var raw []byte - var html string - var data interface{} - var tmpl *template.Template - if len(v) > 0 { - data = v[0] - } - if raw, err = ioutil.ReadFile(file); err != nil { - return err - } - engine := filepath.Ext(file) - switch engine { - case ".template": // https://golang.org/pkg/text/template/ - if tmpl, err = template.New("test").Parse(getString(raw)); err != nil { - return err - } - var buf bytes.Buffer - if err = tmpl.Execute(&buf, data); err != nil { - return err - } - html = buf.String() - case ".ace": // https://github.com/yosssi/ace - if tmpl, err = ace.Load(strings.TrimSuffix(file, filepath.Ext(file)), "", nil); err != nil { - return err - } - var buf bytes.Buffer - if err = tmpl.Execute(&buf, data); err != nil { - return err - } - html = buf.String() - case ".amber": // https://github.com/eknkc/amber - if tmpl, err = amber.Compile(getString(raw), amber.DefaultOptions); err != nil { - return err - } - var buf bytes.Buffer - if err = tmpl.Execute(&buf, data); err != nil { - return err - } - html = buf.String() - case ".jet": // https://github.com/CloudyKit/jet - d, f := filepath.Split(file) - var jetview = jet.NewHTMLSet(d) - var t *jet.Template - if t, err = jetview.GetTemplate(f); err != nil { - return err - } - var buf bytes.Buffer - if err = t.Execute(&buf, make(jet.VarMap), data); err != nil { - return err - } - html = buf.String() - case ".mustache": // https://github.com/hoisie/mustache - if html, err = mustache.Render(getString(raw), data); err != nil { - return err - } - case ".raymond": // https://github.com/aymerick/raymond - if html, err = raymond.Render(getString(raw), data); err != nil { - return err - } - default: - err = fmt.Errorf("render: does not support the %s extension", engine) - } - ctx.Set("Content-Type", "text/html") - ctx.SendString(html) - return err -} - -// Send : https://fiber.wiki/context#send -func (ctx *Ctx) Send(args ...interface{}) { - if len(args) == 0 { - return - } - - switch body := args[0].(type) { - case string: - ctx.Fasthttp.Response.SetBodyString(body) - case []byte: - ctx.Fasthttp.Response.SetBodyString(getString(body)) - default: - ctx.Fasthttp.Response.SetBodyString(fmt.Sprintf("%v", body)) - } -} - -// SendBytes : https://fiber.wiki/context#sendbytes -func (ctx *Ctx) SendBytes(body []byte) { - ctx.Fasthttp.Response.SetBodyString(getString(body)) -} - -// SendFile : https://fiber.wiki/context#sendfile -func (ctx *Ctx) SendFile(file string, gzip ...bool) { - // Disable gzipping - if len(gzip) > 0 && !gzip[0] { - fasthttp.ServeFileUncompressed(ctx.Fasthttp, file) - return - } - fasthttp.ServeFile(ctx.Fasthttp, file) - // https://github.com/valyala/fasthttp/blob/master/fs.go#L81 - //ctx.Type(filepath.Ext(path)) - //ctx.Fasthttp.SendFile(path) -} - -// SendStatus : https://fiber.wiki/context#sendstatus -func (ctx *Ctx) SendStatus(status int) { - ctx.Fasthttp.Response.SetStatusCode(status) - - // Only set status body when there is no response body - if len(ctx.Fasthttp.Response.Body()) == 0 { - msg := getStatus(status) - if msg != "" { - ctx.Fasthttp.Response.SetBodyString(msg) - } - } -} - -// SendString : https://fiber.wiki/context#sendstring -func (ctx *Ctx) SendString(body string) { - ctx.Fasthttp.Response.SetBodyString(body) -} - -// Set : https://fiber.wiki/context#set -func (ctx *Ctx) Set(key string, val string) { - ctx.Fasthttp.Response.Header.SetCanonical(getBytes(key), getBytes(val)) -} - -// Status : https://fiber.wiki/context#status -func (ctx *Ctx) Status(status int) *Ctx { - ctx.Fasthttp.Response.SetStatusCode(status) - return ctx -} - -// Type : https://fiber.wiki/context#type -func (ctx *Ctx) Type(ext string) *Ctx { - ctx.Fasthttp.Response.Header.SetContentType(getType(ext)) - return ctx -} - -// Vary : https://fiber.wiki/context#vary -func (ctx *Ctx) Vary(fields ...string) { - if len(fields) == 0 { - return - } - - h := getString(ctx.Fasthttp.Response.Header.Peek(fasthttp.HeaderVary)) - for i := range fields { - if h == "" { - h += fields[i] - } else { - h += ", " + fields[i] - } - } - - ctx.Set(fasthttp.HeaderVary, h) -} - -// Write : https://fiber.wiki/context#write -func (ctx *Ctx) Write(args ...interface{}) { - for i := range args { - switch body := args[i].(type) { - case string: - ctx.Fasthttp.Response.AppendBodyString(body) - case []byte: - ctx.Fasthttp.Response.AppendBodyString(getString(body)) - default: - ctx.Fasthttp.Response.AppendBodyString(fmt.Sprintf("%v", body)) - } - } -} - -// Xml ... -func (ctx *Ctx) Xml(v interface{}) error { - return ctx.XML(v) -} - -// XML will be removed in v2 -func (ctx *Ctx) XML(v interface{}) error { - fmt.Println("Fiber deprecated c.XML(), this function will be removed in v2") - raw, err := xml.Marshal(v) - if err != nil { - return err - } - - ctx.Fasthttp.Response.Header.SetContentType(mimeApplicationXML) - ctx.Fasthttp.Response.SetBody(raw) - - return nil -} diff --git a/response_test.go b/response_test.go deleted file mode 100644 index 6cac43e7..00000000 --- a/response_test.go +++ /dev/null @@ -1,590 +0,0 @@ -package fiber - -import ( - "io/ioutil" - "net/http" - "strings" - "testing" -) - -func Test_Append(t *testing.T) { - app := New() - app.Get("/test", func(c *Ctx) { - c.Append("X-Test", "hel") - c.Append("X-Test", "lo", "world") - }) - req, _ := http.NewRequest("GET", "/test", nil) - resp, err := app.Test(req) - if err != nil { - t.Fatalf(`%s: %s`, t.Name(), err) - } - if resp.StatusCode != 200 { - t.Fatalf(`%s: StatusCode %v`, t.Name(), resp.StatusCode) - } - if resp.Header.Get("X-Test") != "hel, lo, world" { - t.Fatalf(`%s: Expecting %s`, t.Name(), "X-Test: hel, lo, world") - } -} -func Test_Attachment(t *testing.T) { - app := New() - app.Get("/test", func(c *Ctx) { - c.Attachment() - c.Attachment("./static/img/logo.png") - }) - req, _ := http.NewRequest("GET", "/test", nil) - resp, err := app.Test(req) - if err != nil { - t.Fatalf(`%s: %s`, t.Name(), err) - } - if resp.StatusCode != 200 { - t.Fatalf(`%s: StatusCode %v`, t.Name(), resp.StatusCode) - } - if resp.Header.Get("Content-Disposition") != `attachment; filename="logo.png"` { - t.Fatalf(`%s: Expecting %s`, t.Name(), `attachment; filename="logo.png"`) - } - if resp.Header.Get("Content-Type") != "image/png" { - t.Fatalf(`%s: Expecting %s`, t.Name(), "image/png") - } -} -func Test_ClearCookie(t *testing.T) { - app := New() - app.Get("/test", func(c *Ctx) { - c.ClearCookie() - }) - app.Get("/test2", func(c *Ctx) { - c.ClearCookie("john") - }) - req, _ := http.NewRequest("GET", "/test", nil) - req.AddCookie(&http.Cookie{Name: "john", Value: "doe"}) - resp, err := app.Test(req) - if err != nil { - t.Fatalf(`%s: %s`, t.Name(), err) - } - if resp.StatusCode != 200 { - t.Fatalf(`%s: StatusCode %v`, t.Name(), resp.StatusCode) - } - if !strings.Contains(resp.Header.Get("Set-Cookie"), "expires=") { - t.Fatalf(`%s: Expecting %s`, t.Name(), "expires=") - } - req, _ = http.NewRequest("GET", "/test2", nil) - req.AddCookie(&http.Cookie{Name: "john", Value: "doe"}) - resp, err = app.Test(req) - if err != nil { - t.Fatalf(`%s: %s`, t.Name(), err) - } - if resp.StatusCode != 200 { - t.Fatalf(`%s: StatusCode %v`, t.Name(), resp.StatusCode) - } - if !strings.Contains(resp.Header.Get("Set-Cookie"), "expires=") { - t.Fatalf(`%s: Expecting %s`, t.Name(), "expires=") - } -} -func Test_Cookie(t *testing.T) { - app := New() - app.Get("/test", func(c *Ctx) { - options := &Cookie{ - MaxAge: 60, - Domain: "example.com", - Path: "/", - HTTPOnly: true, - Secure: false, - SameSite: "lax", - } - c.Cookie("name", "john", options) - }) - req, _ := http.NewRequest("GET", "http://example.com/test", nil) - resp, err := app.Test(req) - if err != nil { - t.Fatalf(`%s: %s`, t.Name(), err) - } - if resp.StatusCode != 200 { - t.Fatalf(`%s: StatusCode %v`, t.Name(), resp.StatusCode) - } - if !strings.Contains(resp.Header.Get("Set-Cookie"), "name=john; max-age=60; domain=example.com; path=/; HttpOnly; SameSite=Lax") { - t.Fatalf(`%s: Expecting %s`, t.Name(), "name=john; max-age=60; domain=example.com; path=/; HttpOnly; SameSite=Lax") - } -} -func Test_Download(t *testing.T) { - // TODO -} -func Test_Format(t *testing.T) { - app := New() - app.Get("/test", func(c *Ctx) { - c.Format("Hello, World!") - }) - app.Get("/test2", func(c *Ctx) { - c.Format([]byte("Hello, World!")) - c.Format("Hello, World!") - }) - req, _ := http.NewRequest("GET", "http://example.com/test", nil) - req.Header.Set("Accept", "text/html") - resp, err := app.Test(req) - if err != nil { - t.Fatalf(`%s: %s`, t.Name(), err) - } - if resp.StatusCode != 200 { - t.Fatalf(`%s: StatusCode %v`, t.Name(), resp.StatusCode) - } - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - t.Fatalf(`%s: Error %s`, t.Name(), err) - } - if string(body) != "

Hello, World!

" { - t.Fatalf(`%s: Expecting %s`, t.Name(), "

Hello, World!

") - } - - req, _ = http.NewRequest("GET", "http://example.com/test2", nil) - req.Header.Set("Accept", "application/json") - resp, err = app.Test(req) - if err != nil { - t.Fatalf(`%s: %s`, t.Name(), err) - } - if resp.StatusCode != 200 { - t.Fatalf(`%s: StatusCode %v`, t.Name(), resp.StatusCode) - } - body, err = ioutil.ReadAll(resp.Body) - if err != nil { - t.Fatalf(`%s: Error %s`, t.Name(), err) - } - if string(body) != `"Hello, World!"` { - t.Fatalf(`%s: Expecting %s`, t.Name(), `"Hello, World!"`) - } -} -func Test_HeadersSent(t *testing.T) { - // TODO -} -func Test_JSON(t *testing.T) { - type SomeStruct struct { - Name string - Age uint8 - } - app := New() - app.Get("/test", func(c *Ctx) { - if err := c.Json(""); err != nil { - t.Fatalf(`%s: %s`, t.Name(), err) - } - if err := c.JSON(""); err != nil { - t.Fatalf(`%s: %s`, t.Name(), err) - } - data := SomeStruct{ - Name: "Grame", - Age: 20, - } - if err := c.JSON(data); err != nil { - t.Fatalf(`%s: %s`, t.Name(), err) - } - }) - req, _ := http.NewRequest("GET", "http://example.com/test", nil) - resp, err := app.Test(req) - if err != nil { - t.Fatalf(`%s: %s`, t.Name(), err) - } - if resp.StatusCode != 200 { - t.Fatalf(`%s: StatusCode %v`, t.Name(), resp.StatusCode) - } - if resp.Header.Get("Content-Type") != "application/json" { - t.Fatalf(`%s: Expecting %s`, t.Name(), "application/json") - } - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - t.Fatalf(`%s: Error %s`, t.Name(), err) - } - if string(body) != `{"Name":"Grame","Age":20}` { - t.Fatalf(`%s: Expecting %s`, t.Name(), `{"Name":"Grame","Age":20}`) - } -} -func Test_JSONBytes(t *testing.T) { - app := New() - app.Get("/test", func(c *Ctx) { - c.JsonBytes([]byte("")) - c.JSONBytes([]byte(`{"Name":"Grame","Age":20}`)) - }) - req, _ := http.NewRequest("GET", "http://example.com/test", nil) - resp, err := app.Test(req) - if err != nil { - t.Fatalf(`%s: %s`, t.Name(), err) - } - if resp.StatusCode != 200 { - t.Fatalf(`%s: StatusCode %v`, t.Name(), resp.StatusCode) - } - if resp.Header.Get("Content-Type") != "application/json" { - t.Fatalf(`%s: Expecting %s`, t.Name(), "application/json") - } - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - t.Fatalf(`%s: Error %s`, t.Name(), err) - } - if string(body) != `{"Name":"Grame","Age":20}` { - t.Fatalf(`%s: Expecting %s`, t.Name(), `{"Name":"Grame","Age":20}`) - } -} -func Test_JSONP(t *testing.T) { - type SomeStruct struct { - Name string - Age uint8 - } - app := New() - app.Get("/test", func(c *Ctx) { - if err := c.Jsonp(""); err != nil { - t.Fatalf(`%s: %s`, t.Name(), err) - } - if err := c.JSONP(""); err != nil { - t.Fatalf(`%s: %s`, t.Name(), err) - } - data := SomeStruct{ - Name: "Grame", - Age: 20, - } - if err := c.JSONP(data, "alwaysjohn"); err != nil { - t.Fatalf(`%s: %s`, t.Name(), err) - } - }) - req, _ := http.NewRequest("GET", "http://example.com/test", nil) - resp, err := app.Test(req) - if err != nil { - t.Fatalf(`%s: %s`, t.Name(), err) - } - if resp.StatusCode != 200 { - t.Fatalf(`%s: StatusCode %v`, t.Name(), resp.StatusCode) - } - if resp.Header.Get("Content-Type") != "application/javascript" { - t.Fatalf(`%s: Expecting %s`, t.Name(), "application/javascript") - } - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - t.Fatalf(`%s: Error %s`, t.Name(), err) - } - if string(body) != `alwaysjohn({"Name":"Grame","Age":20});` { - t.Fatalf(`%s: Expecting %s`, t.Name(), `alwaysjohn({"Name":"Grame","Age":20});`) - } -} -func Test_JSONString(t *testing.T) { - app := New() - app.Get("/test", func(c *Ctx) { - c.JsonString("") - c.JSONString(`{"Name":"Grame","Age":20}`) - }) - req, _ := http.NewRequest("GET", "http://example.com/test", nil) - resp, err := app.Test(req) - if err != nil { - t.Fatalf(`%s: %s`, t.Name(), err) - } - if resp.StatusCode != 200 { - t.Fatalf(`%s: StatusCode %v`, t.Name(), resp.StatusCode) - } - if resp.Header.Get("Content-Type") != "application/json" { - t.Fatalf(`%s: Expecting %s`, t.Name(), "application/json") - } - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - t.Fatalf(`%s: Error %s`, t.Name(), err) - } - if string(body) != `{"Name":"Grame","Age":20}` { - t.Fatalf(`%s: Expecting %s`, t.Name(), `{"Name":"Grame","Age":20}`) - } -} -func Test_Links(t *testing.T) { - app := New() - app.Get("/test", func(c *Ctx) { - c.Links( - "http://api.example.com/users?page=2", "next", - "http://api.example.com/users?page=5", "last", - ) - }) - req, _ := http.NewRequest("GET", "http://example.com/test", nil) - resp, err := app.Test(req) - if err != nil { - t.Fatalf(`%s: %s`, t.Name(), err) - } - if resp.StatusCode != 200 { - t.Fatalf(`%s: StatusCode %v`, t.Name(), resp.StatusCode) - } - if resp.Header.Get("Link") != `; rel="next",; rel="last"` { - t.Fatalf(`%s: Expecting %s`, t.Name(), `Link: ; rel="next",; rel="last"`) - } -} -func Test_Location(t *testing.T) { - app := New() - app.Get("/test", func(c *Ctx) { - c.Location("http://example.com") - }) - req, _ := http.NewRequest("GET", "http://example.com/test", nil) - resp, err := app.Test(req) - if err != nil { - t.Fatalf(`%s: %s`, t.Name(), err) - } - if resp.StatusCode != 200 { - t.Fatalf(`%s: StatusCode %v`, t.Name(), resp.StatusCode) - } - if resp.Header.Get("Location") != "http://example.com" { - t.Fatalf(`%s: Expecting %s`, t.Name(), "http://example.com") - } -} -func Test_Next(t *testing.T) { - app := New() - app.Use("/", func(c *Ctx) { - c.Next() - }) - app.Get("/test", func(c *Ctx) { - c.Set("X-Next-Result", "Works") - }) - req, _ := http.NewRequest("GET", "http://example.com/test", nil) - resp, err := app.Test(req) - if err != nil { - t.Fatalf(`%s: %s`, t.Name(), err) - } - if resp.StatusCode != 200 { - t.Fatalf(`%s: StatusCode %v`, t.Name(), resp.StatusCode) - } - if resp.Header.Get("X-Next-Result") != "Works" { - t.Fatalf(`%s: Expecting %s`, t.Name(), "X-Next-Results: Works") - } -} -func Test_Redirect(t *testing.T) { - app := New() - app.Get("/test", func(c *Ctx) { - c.Redirect("http://example.com", 301) - }) - req, _ := http.NewRequest("GET", "http://example.com/test", nil) - resp, err := app.Test(req) - if err != nil { - t.Fatalf(`%s: %s`, t.Name(), err) - } - if resp.StatusCode != 301 { - t.Fatalf(`%s: StatusCode %v`, t.Name(), resp.StatusCode) - } - if resp.Header.Get("Location") != "http://example.com" { - t.Fatalf(`%s: Expecting %s`, t.Name(), "Location: http://example.com") - } -} -func Test_Render(t *testing.T) { - // TODO -} -func Test_Send(t *testing.T) { - app := New() - app.Get("/test", func(c *Ctx) { - c.Send([]byte("Hello, World")) - c.Send("Don't crash please") - c.Send(1337) - }) - req, _ := http.NewRequest("GET", "http://example.com/test", nil) - resp, err := app.Test(req) - if err != nil { - t.Fatalf(`%s: %s`, t.Name(), err) - } - if resp.StatusCode != 200 { - t.Fatalf(`%s: StatusCode %v`, t.Name(), resp.StatusCode) - } - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - t.Fatalf(`%s: Error %s`, t.Name(), err) - } - if string(body) != `1337` { - t.Fatalf(`%s: Expecting %s`, t.Name(), `1337`) - } -} -func Test_SendBytes(t *testing.T) { - app := New() - app.Get("/test", func(c *Ctx) { - c.SendBytes([]byte("Hello, World")) - }) - req, _ := http.NewRequest("GET", "http://example.com/test", nil) - resp, err := app.Test(req) - if err != nil { - t.Fatalf(`%s: %s`, t.Name(), err) - } - if resp.StatusCode != 200 { - t.Fatalf(`%s: StatusCode %v`, t.Name(), resp.StatusCode) - } - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - t.Fatalf(`%s: Error %s`, t.Name(), err) - } - if string(body) != `Hello, World` { - t.Fatalf(`%s: Expecting %s`, t.Name(), `Hello, World`) - } -} -func Test_SendStatus(t *testing.T) { - app := New() - app.Get("/test", func(c *Ctx) { - c.SendStatus(415) - }) - req, _ := http.NewRequest("GET", "http://example.com/test", nil) - resp, err := app.Test(req) - if err != nil { - t.Fatalf(`%s: %s`, t.Name(), err) - } - if resp.StatusCode != 415 { - t.Fatalf(`%s: StatusCode %v`, t.Name(), resp.StatusCode) - } - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - t.Fatalf(`%s: Error %s`, t.Name(), err) - } - if string(body) != `Unsupported Media Type` { - t.Fatalf(`%s: Expecting %s`, t.Name(), `Unsupported Media Type`) - } -} -func Test_SendString(t *testing.T) { - app := New() - app.Get("/test", func(c *Ctx) { - c.SendString("Don't crash please") - }) - req, _ := http.NewRequest("GET", "http://example.com/test", nil) - resp, err := app.Test(req) - if err != nil { - t.Fatalf(`%s: %s`, t.Name(), err) - } - if resp.StatusCode != 200 { - t.Fatalf(`%s: StatusCode %v`, t.Name(), resp.StatusCode) - } - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - t.Fatalf(`%s: Error %s`, t.Name(), err) - } - if string(body) != `Don't crash please` { - t.Fatalf(`%s: Expecting %s`, t.Name(), `Don't crash please`) - } -} -func Test_Set(t *testing.T) { - app := New() - app.Get("/test", func(c *Ctx) { - c.Set("X-1", "1") - c.Set("X-2", "2") - c.Set("X-3", "3") - }) - req, _ := http.NewRequest("GET", "http://example.com/test", nil) - resp, err := app.Test(req) - if err != nil { - t.Fatalf(`%s: %s`, t.Name(), err) - } - if resp.StatusCode != 200 { - t.Fatalf(`%s: StatusCode %v`, t.Name(), resp.StatusCode) - } - if resp.Header.Get("X-1") != "1" { - t.Fatalf(`%s: Expected %v`, t.Name(), "X-1: 1") - } - if resp.Header.Get("X-2") != "2" { - t.Fatalf(`%s: Expected %v`, t.Name(), "X-2: 2") - } - if resp.Header.Get("X-3") != "3" { - t.Fatalf(`%s: Expected %v`, t.Name(), "X-3: 3") - } -} -func Test_Status(t *testing.T) { - app := New() - app.Get("/test", func(c *Ctx) { - c.Status(400) - c.Status(415).Send("Hello, World") - }) - req, _ := http.NewRequest("GET", "http://example.com/test", nil) - resp, err := app.Test(req) - if err != nil { - t.Fatalf(`%s: %s`, t.Name(), err) - } - if resp.StatusCode != 415 { - t.Fatalf(`%s: StatusCode %v`, t.Name(), resp.StatusCode) - } - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - t.Fatalf(`%s: Error %s`, t.Name(), err) - } - if string(body) != `Hello, World` { - t.Fatalf(`%s: Expecting %s`, t.Name(), `Hello, World`) - } -} -func Test_Type(t *testing.T) { - app := New() - app.Get("/test", func(c *Ctx) { - c.Type(".json") - }) - req, _ := http.NewRequest("GET", "http://example.com/test", nil) - resp, err := app.Test(req) - if err != nil { - t.Fatalf(`%s: %s`, t.Name(), err) - } - if resp.StatusCode != 200 { - t.Fatalf(`%s: StatusCode %v`, t.Name(), resp.StatusCode) - } - if resp.Header.Get("Content-Type") != "application/json" { - t.Fatalf(`%s: Expected %v`, t.Name(), `Content-Type: application/json`) - } -} -func Test_Vary(t *testing.T) { - app := New() - app.Get("/test", func(c *Ctx) { - c.Vary("Origin") - c.Vary("User-Agent") - c.Vary("Accept-Encoding", "Accept") - }) - req, _ := http.NewRequest("GET", "http://example.com/test", nil) - resp, err := app.Test(req) - if err != nil { - t.Fatalf(`%s: %s`, t.Name(), err) - } - if resp.StatusCode != 200 { - t.Fatalf(`%s: StatusCode %v`, t.Name(), resp.StatusCode) - } - if resp.Header.Get("Vary") != "Origin, User-Agent, Accept-Encoding, Accept" { - t.Fatalf(`%s: Expected %v`, t.Name(), `Vary: Origin, User-Agent, Accept-Encoding, Accept`) - } -} -func Test_Write(t *testing.T) { - app := New() - app.Get("/test", func(c *Ctx) { - c.Write("Hello, ") - c.Write([]byte("World! ")) - c.Write(123) - }) - req, _ := http.NewRequest("GET", "http://example.com/test", nil) - resp, err := app.Test(req) - if err != nil { - t.Fatalf(`%s: %s`, t.Name(), err) - } - if resp.StatusCode != 200 { - t.Fatalf(`%s: StatusCode %v`, t.Name(), resp.StatusCode) - } - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - t.Fatalf(`%s: Error %s`, t.Name(), err) - } - if string(body) != `Hello, World! 123` { - t.Fatalf(`%s: Expecting %s`, t.Name(), `Hello, World! 123`) - } -} -func Test_XML(t *testing.T) { - type person struct { - Name string `xml:"name"` - Stars int `xml:"stars"` - } - app := New() - app.Get("/test", func(c *Ctx) { - if err := c.Xml(""); err != nil { - t.Fatalf(`%s: %s`, t.Name(), err) - } - if err := c.XML(""); err != nil { - t.Fatalf(`%s: %s`, t.Name(), err) - } - if err := c.XML(person{"John", 50}); err != nil { - t.Fatalf(`%s: %s`, t.Name(), err) - } - }) - req, _ := http.NewRequest("GET", "http://example.com/test", nil) - resp, err := app.Test(req) - if err != nil { - t.Fatalf(`%s: %s`, t.Name(), err) - } - if resp.StatusCode != 200 { - t.Fatalf(`%s: StatusCode %v`, t.Name(), resp.StatusCode) - } - if resp.Header.Get("Content-Type") != "application/xml" { - t.Fatalf(`%s: Expected %v`, t.Name(), "application/xml") - } - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - t.Fatalf(`%s: Error %s`, t.Name(), err) - } - if string(body) != `John50` { - t.Fatalf(`%s: Expecting %s`, t.Name(), `John50`) - } -} diff --git a/router.go b/router.go index 025dd795..f0ccd46c 100644 --- a/router.go +++ b/router.go @@ -1,164 +1,276 @@ -// 🚀 Fiber is an Express.js inspired web framework written in Go with 💖 -// 📌 Please open an issue if you got suggestions or found a bug! -// 🖥 Links: https://github.com/gofiber/fiber, https://fiber.wiki - -// 🦸 Not all heroes wear capes, thank you to some amazing people -// 💖 @valyala, @erikdubbelboer, @savsgio, @julienschmidt, @koddr - package fiber import ( "fmt" "log" "path/filepath" + "reflect" "regexp" "strings" - "sync" + websocket "github.com/fasthttp/websocket" fasthttp "github.com/valyala/fasthttp" ) -// Ctx is the context that contains everything -type Ctx struct { - route *Route - next bool - error error - params *[]string - values []string - Fasthttp *fasthttp.RequestCtx -} - // Route struct type Route struct { - // HTTP method in uppercase, can be a * for Use() & All() + // HTTP method in uppercase, can be a * for "Use" & "All" routes Method string // Stores the original path Path string - // Bool that defines if the route is a Use() middleware - Midware bool - // wildcard bool is for routes without a path, * and /* - Wildcard bool - // Stores compiled regex special routes :params, *wildcards, optionals? + // Prefix is for ending wildcards or middlewares + Prefix string + // Stores regex for :params & :optionals? Regex *regexp.Regexp - // Store params if special routes :params, *wildcards, optionals? + // Stores params keys for :params & :optionals? Params []string - // Callback function for specific route - Handler func(*Ctx) + // Callback function for context + HandlerCtx func(*Ctx) + // Callback function for websockets + HandlerConn func(*Conn) } -// Ctx pool -var poolCtx = sync.Pool{ - New: func() interface{} { - return new(Ctx) - }, -} +func (app *App) registerStatic(grpPrefix string, args ...string) { + var prefix = "/" + var root = "./" + // enable / disable gzipping somewhere? + // todo v2.0.0 + gzip := true -// Get new Ctx from pool -func acquireCtx(fctx *fasthttp.RequestCtx) *Ctx { - ctx := poolCtx.Get().(*Ctx) - ctx.Fasthttp = fctx - return ctx -} - -// Return Context to pool -func releaseCtx(ctx *Ctx) { - ctx.route = nil - ctx.next = false - ctx.error = nil - ctx.params = nil - ctx.values = nil - ctx.Fasthttp = nil - poolCtx.Put(ctx) -} - -func (grp *Group) register(method string, args ...interface{}) { - path := grp.path - var handler func(*Ctx) if len(args) == 1 { - handler = args[0].(func(*Ctx)) - } else if len(args) > 1 { - path = path + args[0].(string) - handler = args[1].(func(*Ctx)) - if path[0] != '/' && path[0] != '*' { - path = "/" + path + root = args[0] + } + if len(args) == 2 { + prefix = args[0] + root = args[1] + } + + // A non wildcard path must start with a '/' + if prefix != "*" && len(prefix) > 0 && prefix[0] != '/' { + prefix = "/" + prefix + } + // Prepend group prefix + if len(grpPrefix) > 0 { + // `/v1`+`/` => `/v1`+`` + if prefix == "/" { + prefix = grpPrefix + } else { + prefix = grpPrefix + prefix } - path = strings.Replace(path, "//", "/", -1) - path = filepath.Clean(path) + // Remove duplicate slashes `//` + prefix = strings.Replace(prefix, "//", "/", -1) + } + // Empty or '/*' path equals "match anything" + // TODO fix * for paths with grpprefix + if prefix == "/*" { + prefix = "*" + } + // Lets get all files from root + files, _, err := getFiles(root) + if err != nil { + log.Fatal("Static: ", err) + } + // ./static/compiled => static/compiled + mount := filepath.Clean(root) + + if !app.Settings.CaseSensitive { + prefix = strings.ToLower(prefix) + } + if !app.Settings.StrictRouting && len(prefix) > 1 { + prefix = strings.TrimRight(prefix, "/") + } + + // Loop over all files + for _, file := range files { + // Ignore the .gzipped files by fasthttp + if strings.Contains(file, ".fasthttp.gz") { + continue + } + // Time to create a fake path for the route match + // static/index.html => /index.html + path := filepath.Join(prefix, strings.Replace(file, mount, "", 1)) + // for windows: static\index.html => /index.html path = filepath.ToSlash(path) + // Store file path to use in ctx handler + filePath := file + + if len(prefix) > 1 && strings.Contains(prefix, "*") { + app.routes = append(app.routes, &Route{ + Method: "GET", + Path: path, + Prefix: strings.Split(prefix, "*")[0], + HandlerCtx: func(c *Ctx) { + c.SendFile(filePath, gzip) + }, + }) + return + } + // If the file is an index.html, bind the prefix to index.html directly + if filepath.Base(filePath) == "index.html" || filepath.Base(filePath) == "index.htm" { + app.routes = append(app.routes, &Route{ + Method: "GET", + Path: prefix, + HandlerCtx: func(c *Ctx) { + c.SendFile(filePath, gzip) + }, + }) + } + if !app.Settings.CaseSensitive { + path = strings.ToLower(path) + } + if !app.Settings.StrictRouting && len(prefix) > 1 { + path = strings.TrimRight(path, "/") + } + // Add the route + SendFile(filepath) to routes + app.routes = append(app.routes, &Route{ + Method: "GET", + Path: path, + HandlerCtx: func(c *Ctx) { + c.SendFile(filePath, gzip) + }, + }) } - grp.app.register(method, path, handler) } - -// Function to add a route correctly -func (app *Application) register(method string, args ...interface{}) { - // Set if method is Use() midware - var midware = method == "USE" - - // Match any method - if method == "ALL" || midware { - method = "*" - } - - // Prepare possible variables - var path string // We could have a path/prefix - var handler func(*Ctx) // We could have a ctx handler - - // Only 1 argument, so no path/prefix - if len(args) == 1 { - handler = args[0].(func(*Ctx)) - } else if len(args) > 1 { - path = args[0].(string) - handler = args[1].(func(*Ctx)) - if path == "" || path[0] != '/' && path[0] != '*' { - path = "/" + path +func (app *App) register(method, grpPrefix string, args ...interface{}) { + // Set variables + var path = "*" + var prefix string + var middleware = method == "USE" + var handlersCtx []func(*Ctx) + var handlersConn []func(*Conn) + for i := 0; i < len(args); i++ { + switch arg := args[i].(type) { + case string: + path = arg + case func(*Ctx): + handlersCtx = append(handlersCtx, arg) + case func(*Conn): + handlersConn = append(handlersConn, arg) + default: + log.Fatalf("Invalid argument type: %v", reflect.TypeOf(arg)) } } - - if midware && strings.Contains(path, "/:") { - log.Fatal("Router: You cannot use :params in Use()") + // A non wildcard path must start with a '/' + if path != "*" && len(path) > 0 && path[0] != '/' { + path = "/" + path } - - // If Use() path == "/", match anything aka * - if midware && path == "/" { + // Prepend group prefix + if len(grpPrefix) > 0 { + // `/v1`+`/` => `/v1`+`` + if path == "/" { + path = grpPrefix + } else { + path = grpPrefix + path + } + // Remove duplicate slashes `//` + path = strings.Replace(path, "//", "/", -1) + } + // Empty or '/*' path equals "match anything" + // TODO fix * for paths with grpprefix + if path == "" || path == "/*" { path = "*" } - - // If the route needs to match any path - if path == "" || path == "*" || path == "/*" { - app.routes = append(app.routes, &Route{method, path, midware, true, nil, nil, handler}) + if method == "ALL" || middleware { + method = "*" + } + // Routes are case insensitive by default + if !app.Settings.CaseSensitive { + path = strings.ToLower(path) + } + if !app.Settings.StrictRouting && len(path) > 1 { + path = strings.TrimRight(path, "/") + } + // If the route can match anything + if path == "*" { + for i := range handlersCtx { + app.routes = append(app.routes, &Route{ + Method: method, Path: path, HandlerCtx: handlersCtx[i], + }) + } + for i := range handlersConn { + app.routes = append(app.routes, &Route{ + Method: method, Path: path, HandlerConn: handlersConn[i], + }) + } return } - - // Get params from path + // Get ':param' & ':optional?' & '*' from path params := getParams(path) + // Enable prefix for midware + if len(params) == 0 && middleware { + prefix = path + } - // If path has no params (simple path), we don't need regex (also for use()) - if midware || len(params) == 0 { - app.routes = append(app.routes, &Route{method, path, midware, false, nil, nil, handler}) + // If path has no params (simple path) + if len(params) == 0 { + for i := range handlersCtx { + app.routes = append(app.routes, &Route{ + Method: method, Path: path, Prefix: prefix, HandlerCtx: handlersCtx[i], + }) + } + for i := range handlersConn { + app.routes = append(app.routes, &Route{ + Method: method, Path: path, Prefix: prefix, HandlerConn: handlersConn[i], + }) + } return } - // We have parametes, so we need to compile regex from the path + // If path only contains 1 wildcard, we can create a prefix + // If its a middleware, we also create a prefix + if len(params) == 1 && params[0] == "*" { + prefix = strings.Split(path, "*")[0] + for i := range handlersCtx { + app.routes = append(app.routes, &Route{ + Method: method, Path: path, Prefix: prefix, + Params: params, HandlerCtx: handlersCtx[i], + }) + } + for i := range handlersConn { + app.routes = append(app.routes, &Route{ + Method: method, Path: path, Prefix: prefix, + Params: params, HandlerConn: handlersConn[i], + }) + } + return + } + // We have an :param or :optional? and need to compile a regex struct regex, err := getRegex(path) if err != nil { - log.Fatal("Router: Invalid url pattern: " + path) + log.Fatal("Router: Invalid path pattern: " + path) + } + // Add route with regex + for i := range handlersCtx { + app.routes = append(app.routes, &Route{ + Method: method, Path: path, Regex: regex, + Params: params, HandlerCtx: handlersCtx[i], + }) + } + for i := range handlersConn { + app.routes = append(app.routes, &Route{ + Method: method, Path: path, Regex: regex, + Params: params, HandlerConn: handlersConn[i], + }) } - - // Add regex + params to route - app.routes = append(app.routes, &Route{method, path, midware, false, regex, params, handler}) } - -// then try to match a route as efficient as possible. -func (app *Application) handler(fctx *fasthttp.RequestCtx) { - found := false - +func (app *App) handler(fctx *fasthttp.RequestCtx) { + // Use this boolean to perform 404 not found at the end + var match = false // get custom context from sync pool ctx := acquireCtx(fctx) - - // get path and method from main context + if ctx.app == nil { + ctx.app = app + } + // get path and method path := ctx.Path() + if !app.Settings.CaseSensitive { + path = strings.ToLower(path) + } + if !app.Settings.StrictRouting && len(path) > 1 { + path = strings.TrimRight(path, "/") + } method := ctx.Method() - + // enable recovery if app.recover != nil { defer func() { if r := recover(); r != nil { @@ -167,48 +279,101 @@ func (app *Application) handler(fctx *fasthttp.RequestCtx) { } }() } - // loop trough routes for _, route := range app.routes { - // Skip route if method is not allowed + // Skip route if method does not match if route.Method != "*" && route.Method != method { continue } - - // First check if we match a wildcard or static path - if route.Wildcard || route.Path == path { - // if route.wildcard || (route.path == path && route.params == nil) { - // If * always set the path to the wildcard parameter - if route.Wildcard { + // Set route pointer if user wants to call .Route() + ctx.route = route + // wilcard or exact same path + // TODO v2: enable or disable case insensitive match + if route.Path == "*" || route.Path == path { + // if * always set the path to the wildcard parameter + if route.Path == "*" { ctx.params = &[]string{"*"} - ctx.values = make([]string, 1) - ctx.values[0] = path + ctx.values = []string{path} } - found = true - // Set route pointer if user wants to call .Route() - ctx.route = route - // Execute handler with context - route.Handler(ctx) + // ctx.Fasthttp.Request.Header.ConnectionUpgrade() + // Websocket request + if route.HandlerConn != nil && websocket.FastHTTPIsWebSocketUpgrade(fctx) { + // Try to upgrade + err := socketUpgrade.Upgrade(ctx.Fasthttp, func(fconn *websocket.Conn) { + conn := acquireConn(fconn) + defer releaseConn(conn) + conn.params = ctx.params + conn.values = ctx.values + releaseCtx(ctx) + route.HandlerConn(conn) + }) + // Upgrading failed + if err != nil { + panic(err) + } + return + } + // No handler for HTTP nor websocket + if route.HandlerCtx == nil { + continue + } + // Match found, 404 not needed + match = true + route.HandlerCtx(ctx) // if next is not set, leave loop and release ctx if !ctx.next { break + } else { + // reset match to false + match = false } // set next to false for next iteration ctx.next = false // continue to go to the next route continue } - - // If route is Use() and path starts with route.path - // aka strings.HasPrefix(route.path, path) - if route.Midware && strings.HasPrefix(path, route.Path) { - found = true + if route.Prefix != "" && strings.HasPrefix(path, route.Prefix) { ctx.route = route - route.Handler(ctx) + if strings.Contains(route.Path, "*") { + ctx.params = &[]string{"*"} + // ctx.values = matches[0][1:len(matches[0])] + // parse query source + ctx.values = []string{strings.Replace(path, route.Prefix, "", 1)} + } + // Websocket request + if route.HandlerConn != nil { + // Try to upgrade + err := socketUpgrade.Upgrade(ctx.Fasthttp, func(fconn *websocket.Conn) { + conn := acquireConn(fconn) + defer releaseConn(conn) + conn.params = ctx.params + conn.values = ctx.values + releaseCtx(ctx) + route.HandlerConn(conn) + }) + // Upgrading failed + if err != nil { + panic(err) + } + return + } + // No handler for HTTP nor websocket + if route.HandlerCtx == nil { + continue + } + // Match found, 404 not needed + match = true + route.HandlerCtx(ctx) + // if next is not set, leave loop and release ctx if !ctx.next { break + } else { + // reset match to false + match = false } + // set next to false for next iteration ctx.next = false + // continue to go to the next route continue } @@ -228,34 +393,46 @@ func (app *Application) handler(fctx *fasthttp.RequestCtx) { // If we have matches, add params and values to context if len(matches) > 0 && len(matches[0]) > 1 { ctx.params = &route.Params - // ctx.values = make([]string, len(*ctx.params)) ctx.values = matches[0][1:len(matches[0])] } } - - found = true - - // Set route pointer if user wants to call .Route() - ctx.route = route - - // Execute handler with context - route.Handler(ctx) - + // Websocket route + if route.HandlerConn != nil { + // Try to upgrade + err := socketUpgrade.Upgrade(ctx.Fasthttp, func(fconn *websocket.Conn) { + conn := acquireConn(fconn) + conn.params = ctx.params + conn.values = ctx.values + releaseCtx(ctx) + defer releaseConn(conn) + route.HandlerConn(conn) + }) + // Upgrading failed + if err != nil { + panic(err) + } + return + } + // No handler for HTTP nor websocket + if route.HandlerCtx == nil { + continue + } + // Match found, 404 not needed + match = true + route.HandlerCtx(ctx) // if next is not set, leave loop and release ctx if !ctx.next { break + } else { + // reset match to false + match = false } - // set next to false for next iteration ctx.next = false } - - // No routes found - if !found { - // Custom 404 handler? + // No match, send default 404 + if !match { ctx.SendStatus(404) } - - // release context back into sync pool releaseCtx(ctx) } diff --git a/router_test.go b/router_test.go deleted file mode 100644 index 46ebab5e..00000000 --- a/router_test.go +++ /dev/null @@ -1,10 +0,0 @@ -package fiber - -import "testing" - -func BenchmarkFib10(b *testing.B) { - // run the Fib function b.N times - // for n := 0; n < b.N; n++ { - // Fib(10) - // } -} diff --git a/utils.go b/utils.go index a7165830..6ae8d0ab 100644 --- a/utils.go +++ b/utils.go @@ -1,10 +1,3 @@ -// 🚀 Fiber is an Express.js inspired web framework written in Go with 💖 -// 📌 Please open an issue if you got suggestions or found a bug! -// 🖥 Links: https://github.com/gofiber/fiber, https://fiber.wiki - -// 🦸 Not all heroes wear capes, thank you to some amazing people -// 💖 @valyala, @erikdubbelboer, @savsgio, @julienschmidt, @koddr - package fiber import ( @@ -13,18 +6,44 @@ import ( "net" "os" "path/filepath" - "reflect" "regexp" "strings" "time" "unsafe" + websocket "github.com/fasthttp/websocket" schema "github.com/gorilla/schema" + fasthttp "github.com/valyala/fasthttp" ) var schemaDecoder = schema.NewDecoder() +var socketUpgrade = websocket.FastHTTPUpgrader{ + ReadBufferSize: 1024, + WriteBufferSize: 1024, + CheckOrigin: func(fctx *fasthttp.RequestCtx) bool { + return true + }, +} + +// MIME types +const ( + MIMEApplicationJSON = "application/json" + MIMEApplicationJavaScript = "application/javascript" + MIMEApplicationXML = "application/xml" + MIMETextXML = "text/xml" + MIMEApplicationForm = "application/x-www-form-urlencoded" + MIMEApplicationProtobuf = "application/protobuf" + MIMEApplicationMsgpack = "application/msgpack" + MIMETextHTML = "text/html" + MIMETextPlain = "text/plain" + MIMEMultipartForm = "multipart/form-data" + MIMEOctetStream = "application/octet-stream" +) func getParams(path string) (params []string) { + if len(path) < 1 { + return + } segments := strings.Split(path, "/") replacer := strings.NewReplacer(":", "", "?", "") for _, s := range segments { @@ -32,11 +51,12 @@ func getParams(path string) (params []string) { continue } else if s[0] == ':' { params = append(params, replacer.Replace(s)) - } else if s[0] == '*' { + } + if strings.Contains(s, "*") { params = append(params, "*") } } - return params + return } func getRegex(path string) (*regexp.Regexp, error) { @@ -63,21 +83,20 @@ func getRegex(path string) (*regexp.Regexp, error) { return regex, err } -func getFiles(root string) (files []string, isDir bool, err error) { +func getFiles(root string) (files []string, dir bool, err error) { root = filepath.Clean(root) - // Check if dir/file exists if _, err := os.Lstat(root); err != nil { - return files, isDir, fmt.Errorf("%s", err) + return files, dir, fmt.Errorf("%s", err) } err = filepath.Walk(root, func(path string, info os.FileInfo, err error) error { if !info.IsDir() { files = append(files, path) } else { - isDir = true + dir = true } return err }) - return files, isDir, err + return } func getType(ext string) (mime string) { @@ -85,17 +104,18 @@ func getType(ext string) (mime string) { return mime } if ext[0] == '.' { - ext = ext[1:] + mime = extensionMIME[ext[1:]] + } else { + mime = extensionMIME[ext] } - mime = mimeTypes[ext] if mime == "" { - return mimeApplicationOctetStream + return MIMEOctetStream } return mime } func getStatus(status int) (msg string) { - return statusMessages[status] + return statusMessage[status] } // #nosec G103 @@ -109,47 +129,32 @@ func getString(b []byte) string { // getBytes converts string to a byte slice without memory allocation. // See https://groups.google.com/forum/#!msg/Golang-Nuts/ENgbUzYvCuU/90yGx7GUAgAJ . func getBytes(s string) (b []byte) { - // return *(*[]byte)(unsafe.Pointer(&s)) - sh := *(*reflect.StringHeader)(unsafe.Pointer(&s)) - bh := (*reflect.SliceHeader)(unsafe.Pointer(&b)) - bh.Data, bh.Len, bh.Cap = sh.Data, sh.Len, sh.Len - return b + return *(*[]byte)(unsafe.Pointer(&s)) } -// Check for error and format -// func checkErr(err error, title ...string) { -// if err != nil { -// t := "Error" -// if len(title) > 0 { -// t = title[0] -// } -// fmt.Printf("\n%s%s: %v%s\n\n", "\x1b[1;30m", t, err, "\x1b[0m") -// os.Exit(1) -// } -// } - // https://golang.org/src/net/net.go#L113 -// Helper methods for Testing -type conn struct { +// Helper methods for application#test +type testConn struct { net.Conn r bytes.Buffer w bytes.Buffer } -func (c *conn) RemoteAddr() net.Addr { +func (c *testConn) RemoteAddr() net.Addr { return &net.TCPAddr{ IP: net.IPv4(0, 0, 0, 0), } } -func (c *conn) LocalAddr() net.Addr { return c.LocalAddr() } -func (c *conn) Read(b []byte) (int, error) { return c.r.Read(b) } -func (c *conn) Write(b []byte) (int, error) { return c.w.Write(b) } -func (c *conn) Close() error { return nil } -func (c *conn) SetDeadline(t time.Time) error { return nil } -func (c *conn) SetReadDeadline(t time.Time) error { return nil } -func (c *conn) SetWriteDeadline(t time.Time) error { return nil } +func (c *testConn) LocalAddr() net.Addr { return c.RemoteAddr() } +func (c *testConn) Read(b []byte) (int, error) { return c.r.Read(b) } +func (c *testConn) Write(b []byte) (int, error) { return c.w.Write(b) } +func (c *testConn) Close() error { return nil } +func (c *testConn) SetDeadline(t time.Time) error { return nil } +func (c *testConn) SetReadDeadline(t time.Time) error { return nil } +func (c *testConn) SetWriteDeadline(t time.Time) error { return nil } -var statusMessages = map[int]string{ +// HTTP status codes +var statusMessage = map[int]string{ 100: "Continue", 101: "Switching Protocols", 102: "Processing", @@ -212,18 +217,8 @@ var statusMessages = map[int]string{ 511: "Network Authentication Required", } -const ( - mimeApplicationJSON = "application/json" - mimeApplicationJavascript = "application/javascript" - mimeApplicationXML = "application/xml" - mimeTextXML = "text/xml" - mimeApplicationOctetStream = "application/octet-stream" - mimeApplicationForm = "application/x-www-form-urlencoded" - mimeMultipartForm = "multipart/form-data" -) - -// https://github.com/nginx/nginx/blob/master/conf/mime.types -var mimeTypes = map[string]string{ +// MIME types for file extensions +var extensionMIME = map[string]string{ "html": "text/html", "htm": "text/html", "shtml": "text/html",