diff --git a/.github/.gitpod.yml b/.github/.gitpod.yml deleted file mode 100644 index f1e44ef146..0000000000 --- a/.github/.gitpod.yml +++ /dev/null @@ -1,2 +0,0 @@ -tasks: - - init: go get && go build ./... && go test ./... diff --git a/.github/CNAME b/.github/CNAME deleted file mode 100644 index 862e8d01f3..0000000000 --- a/.github/CNAME +++ /dev/null @@ -1 +0,0 @@ -fiber.wiki \ No newline at end of file diff --git a/app.go b/app.go index 21227faaf1..35d9b6bbf0 100644 --- a/app.go +++ b/app.go @@ -24,7 +24,7 @@ import ( ) // Version of current package -const Version = "1.9.2" +const Version = "1.9.3" // Map is a shortcut for map[string]interface{} type Map map[string]interface{} @@ -48,6 +48,10 @@ type Settings struct { ServerHeader string // default: "" // Enables handler values to be immutable even if you return from handler Immutable bool // default: false + // Enable or disable ETag header generation, since both weak and strong etags are generated + // using the same hashing method (CRC-32). Weak ETags are the default when enabled. + // Optional. Default value false + ETag bool // Max body size that the server accepts BodyLimit int // default: 4 * 1024 * 1024 // Maximum number of concurrent connections. @@ -80,6 +84,25 @@ type Group struct { app *App } +// Static struct +type Static struct { + // Transparently compresses responses if set to true + // This works differently than the github.com/gofiber/compression middleware + // The server tries minimizing CPU usage by caching compressed files. + // It adds ".fiber.gz" suffix to the original file name. + // Optional. Default value false + Compress bool + // Enables byte range requests if set to true. + // Optional. Default value false + ByteRange bool + // Enable directory browsing. + // Optional. Default value false. + Browse bool + // Index file for serving a directory. + // Optional. Default value "index.html". + Index string +} + // New creates a new Fiber named instance. // You can pass optional settings when creating a new instance. func New(settings ...*Settings) *App { @@ -125,25 +148,6 @@ func (app *App) Group(prefix string, handlers ...func(*Ctx)) *Group { } } -// Static struct -type Static struct { - // Transparently compresses responses if set to true - // This works differently than the github.com/gofiber/compression middleware - // The server tries minimizing CPU usage by caching compressed files. - // It adds ".fiber.gz" suffix to the original file name. - // Optional. Default value false - Compress bool - // Enables byte range requests if set to true. - // Optional. Default value false - ByteRange bool - // Enable directory browsing. - // Optional. Default value false. - Browse bool - // Index file for serving a directory. - // Optional. Default value "index.html". - Index string -} - // Static registers a new route with path prefix to serve static files from the provided root directory. func (app *App) Static(prefix, root string, config ...Static) *App { app.registerStatic(prefix, root, config...) diff --git a/app_test.go b/app_test.go index ee6220e915..d88c908983 100644 --- a/app_test.go +++ b/app_test.go @@ -33,7 +33,9 @@ func is200(t *testing.T, app *App, url string, m ...string) { } } func Test_Methods(t *testing.T) { - app := New() + app := New(&Settings{ + DisableStartupMessage: true, + }) app.Connect("/:john?/:doe?", handler) is200(t, app, "/", "CONNECT") @@ -83,14 +85,18 @@ func Test_New(t *testing.T) { } func Test_Shutdown(t *testing.T) { - app := New() + app := New(&Settings{ + DisableStartupMessage: true, + }) _ = app.Shutdown() } func Test_Static(t *testing.T) { - app := New() + app := New(&Settings{ + DisableStartupMessage: true, + }) grp := app.Group("/v1") - grp.Static("/v2", ".travis.yml") + grp.Static("/v2", ".github/auth_assign.yml") app.Static("/*", ".github/FUNDING.yml") app.Static("/john", "./.github") req, _ := http.NewRequest("GET", "/john/stale.yml", nil) @@ -140,7 +146,9 @@ func Test_Static(t *testing.T) { } func Test_Group(t *testing.T) { - app := New() + app := New(&Settings{ + DisableStartupMessage: true, + }) grp := app.Group("/test") grp.Get("/", handler) @@ -188,7 +196,9 @@ func Test_Group(t *testing.T) { } func Test_Listen(t *testing.T) { - app := New() + app := New(&Settings{ + DisableStartupMessage: true, + }) go func() { time.Sleep(500 * time.Millisecond) _ = app.Shutdown() @@ -203,7 +213,8 @@ func Test_Listen(t *testing.T) { func Test_Serve(t *testing.T) { app := New(&Settings{ - Prefork: true, + DisableStartupMessage: true, + Prefork: true, }) ln, err := net.Listen("tcp4", ":3004") if err != nil { diff --git a/ctx.go b/ctx.go index 9bdf19fbdb..b812910d0b 100644 --- a/ctx.go +++ b/ctx.go @@ -409,7 +409,11 @@ func (ctx *Ctx) FormValue(key string) (value string) { return getString(ctx.Fasthttp.FormValue(key)) } -// Fresh is not implemented yet, pull requests are welcome! +// Fresh When the response is still “fresh” in the client’s cache true is returned, +// otherwise false is returned to indicate that the client cache is now stale +// and the full response should be sent. +// When a client sends the Cache-Control: no-cache request header to indicate an end-to-end +// reload request, this module will return false to make handling these requests transparent. // https://github.com/jshttp/fresh/blob/10e0471669dbbfbfd8de65bc6efac2ddd0bfa057/index.js#L33 func (ctx *Ctx) Fresh() bool { // fields @@ -545,7 +549,6 @@ func (ctx *Ctx) JSONP(data interface{}, callback ...string) error { ctx.Set(HeaderXContentTypeOptions, "nosniff") ctx.Fasthttp.Response.Header.SetContentType(MIMEApplicationJavaScript) ctx.Fasthttp.Response.SetBodyString(str) - return nil } diff --git a/ctx_test.go b/ctx_test.go index 4488944a4c..93c575b491 100644 --- a/ctx_test.go +++ b/ctx_test.go @@ -21,7 +21,9 @@ import ( ) func Test_Accepts(t *testing.T) { - app := New() + app := New(&Settings{ + DisableStartupMessage: true, + }) app.Get("/test", func(c *Ctx) { expect := "" result := c.Accepts(expect) @@ -155,6 +157,11 @@ func Test_BodyParser(t *testing.T) { type Demo struct { Name string `json:"name" xml:"name" form:"name" query:"name"` } + type Query struct { + ID int + Name string + Hobby []string + } app.Post("/test", func(c *Ctx) { d := new(Demo) err := c.BodyParser(d) @@ -162,9 +169,21 @@ func Test_BodyParser(t *testing.T) { t.Fatalf(`%s: BodyParser %v`, t.Name(), err) } if d.Name != "john" { - t.Fatalf(`%s: Expect %s got %s`, t.Name(), "john", d) + t.Fatalf(`%s: Expect %s got %v`, t.Name(), "john", d) + } + }) + + app.Get("/query", func(c *Ctx) { + d := new(Query) + err := c.BodyParser(d) + if err != nil { + t.Fatalf(`%s: BodyParser %v`, t.Name(), err) + } + if len(d.Hobby) != 2 { + t.Fatalf(`%s: Expect length %d got %v`, t.Name(), 2, d) } }) + req := httptest.NewRequest("POST", "/test", bytes.NewBuffer([]byte(`{"name":"john"}`))) req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Length", strconv.Itoa(len([]byte(`{"name":"john"}`)))) @@ -174,25 +193,12 @@ func Test_BodyParser(t *testing.T) { t.Fatalf(`%s: %s`, t.Name(), err) } - // data := url.Values{} - // data.Set("name", "john") - // req = httptest.NewRequest("POST", "/test", strings.NewReader(data.Encode())) - // req.Header.Add("Content-Type", "application/x-www-form-urlencoded") - // req.Header.Add("Content-Length", strconv.Itoa(len(data.Encode()))) - - // _, err = app.Test(req) - // if err != nil { - // t.Fatalf(`%s: %s`, t.Name(), err) - // } + req = httptest.NewRequest("GET", "/query?id=1&name=tom&hobby=basketball&hobby=football", nil) - // req = httptest.NewRequest("POST", "/test", bytes.NewBuffer([]byte(`john`))) - // req.Header.Set("Content-Type", "application/xml") - // req.Header.Set("Content-Length", strconv.Itoa(len([]byte(`john`)))) - - // _, err = app.Test(req) - // if err != nil { - // t.Fatalf(`%s: %s`, t.Name(), err) - // } + _, err = app.Test(req) + if err != nil { + t.Fatalf(`%s: %s`, t.Name(), err) + } } func Test_Cookies(t *testing.T) { app := New() diff --git a/router.go b/router.go index f3fc46c4af..cee7e0e8b8 100644 --- a/router.go +++ b/router.go @@ -42,9 +42,14 @@ func (app *App) nextRoute(ctx *Ctx) { ctx.route = route ctx.values = values route.Handler(ctx) + // Generate ETag if enabled / found + if app.Settings.ETag { + setETag(ctx, ctx.Fasthttp.Response.Body(), false) + } return } } + // Send a 404 if len(ctx.Fasthttp.Response.Body()) == 0 { ctx.SendStatus(404) } diff --git a/utils.go b/utils.go index 9495f231ee..b18cd18f85 100644 --- a/utils.go +++ b/utils.go @@ -7,6 +7,7 @@ package fiber import ( "bytes" "fmt" + "hash/crc32" "net" "os" "path/filepath" @@ -16,6 +17,47 @@ import ( "unsafe" ) +// Document elke line gelijk even +func setETag(ctx *Ctx, body []byte, weak bool) { + // Skips ETag if no response body is present + if len(body) <= 0 { + return + } + // Get ETag header from request + clientEtag := ctx.Get("If-None-Match") + + // Generate ETag for response + crc32q := crc32.MakeTable(0xD5828281) + etag := fmt.Sprintf("\"%d-%v\"", len(body), crc32.Checksum(body, crc32q)) + + // Enable weak tag + if weak { + etag = "W/" + "\"" + etag + "\"" + } + + // Check if client's ETag is weak + if strings.HasPrefix(clientEtag, "W/") { + // Check if server's ETag is weak + if clientEtag[2:] == etag || clientEtag[2:] == etag[2:] { + // W/1 == 1 || W/1 == W/1 + ctx.SendStatus(304) + ctx.Fasthttp.ResetBody() + return + } + // W/1 != W/2 || W/1 != 2 + ctx.Set("ETag", etag) + return + } + if strings.Contains(clientEtag, etag) { + // 1 == 1 + ctx.SendStatus(304) + ctx.Fasthttp.ResetBody() + return + } + // 1 != 2 + ctx.Set("ETag", etag) +} + func groupPaths(prefix, path string) string { if path == "/" { path = ""