diff --git a/app.go b/app.go new file mode 100644 index 00000000..0f83049e --- /dev/null +++ b/app.go @@ -0,0 +1,349 @@ +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 + TemplateCache bool `default:"false"` + TemplateFolder string `default:""` + TemplateEngine string `default:""` + TemplateExtension string `default:""` + } +) + +// New ... +func New(settings ...*Settings) (app *App) { + flag.Parse() + 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/app_test.go b/app_test.go new file mode 100644 index 00000000..4aac866e --- /dev/null +++ b/app_test.go @@ -0,0 +1,180 @@ +package fiber + +import ( + "net/http" + "testing" +) + +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() + + 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") + +} + +// func Test_Static(t *testing.T) { +// app := New() +// grp := app.Group("/v1") +// grp.Static("/v2", ".travis.yml") +// app.Static("/yesyes*", ".github/FUNDING.yml") +// app.Static("./.github") +// app.Static("/john", "./.github") +// req, _ := http.NewRequest("GET", "/stale.yml", 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-Length") == "" { +// t.Fatalf(`%s: Missing Content-Length`, t.Name()) +// } +// req, _ = http.NewRequest("GET", "/yesyes/john/doe", 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-Length") == "" { +// t.Fatalf(`%s: Missing Content-Length`, t.Name()) +// } +// req, _ = http.NewRequest("GET", "/john/stale.yml", 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-Length") == "" { +// t.Fatalf(`%s: Missing Content-Length`, t.Name()) +// } +// req, _ = http.NewRequest("GET", "/v1/v2", 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-Length") == "" { +// 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) + 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) { +// t.Parallel() +// app := New() +// app.Banner = false +// go func() { +// time.Sleep(1 * time.Second) +// _ = app.Shutdown() +// }() +// app.Listen(3002) +// go func() { +// time.Sleep(1 * time.Second) +// _ = app.Shutdown() +// }() +// app.Listen("3002") +// } diff --git a/application.go b/application.go deleted file mode 100644 index b2bc9298..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.6.1" - // 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/application_test.go b/application_test.go deleted file mode 100644 index 0c825549..00000000 --- a/application_test.go +++ /dev/null @@ -1,148 +0,0 @@ -package fiber - -import ( - "net/http" - "testing" -) - -var handler = func(c *Ctx) {} - -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) - - 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) - 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-Length") == "" { - t.Fatalf(`%s: Missing Content-Length`, t.Name()) - } - req, _ = http.NewRequest("GET", "/yesyes/john/doe", 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-Length") == "" { - t.Fatalf(`%s: Missing Content-Length`, t.Name()) - } - req, _ = http.NewRequest("GET", "/john/stale.yml", 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-Length") == "" { - t.Fatalf(`%s: Missing Content-Length`, t.Name()) - } - req, _ = http.NewRequest("GET", "/v1/v2", 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-Length") == "" { - t.Fatalf(`%s: Missing Content-Length`, t.Name()) - } -} -func Test_Group(t *testing.T) { - app := New() - grp := app.Group("/test") - grp.Get("/", handler) - grp.Get("/:demo?", handler) - grp.Connect("/CONNECT", handler) - grp.Put("/PUT", handler) - grp.Post("/POST", handler) - grp.Delete("/DELETE", handler) - grp.Head("/HEAD", handler) - grp.Patch("/PATCH", handler) - grp.Options("/OPTIONS", handler) - grp.Trace("/TRACE", handler) - grp.All("/ALL", handler) - 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) - } -} - -// func Test_Listen(t *testing.T) { -// t.Parallel() -// app := New() -// app.Banner = false -// go func() { -// time.Sleep(1 * time.Second) -// _ = app.Shutdown() -// }() -// app.Listen(3002) -// go func() { -// time.Sleep(1 * time.Second) -// _ = app.Shutdown() -// }() -// app.Listen("3002") -// } diff --git a/context.go b/context.go new file mode 100644 index 00000000..6e0010bf --- /dev/null +++ b/context.go @@ -0,0 +1,858 @@ +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 { + 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 raw, err = ioutil.ReadFile(file); err != nil { + return err + } + if len(e) > 0 { + engine = e[0] + } else { + engine = filepath.Ext(file)[1:] + } + 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") != `" + 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") != `