diff --git a/middleware/etag/etag.go b/middleware/etag/etag.go new file mode 100644 index 00000000..9907ef31 --- /dev/null +++ b/middleware/etag/etag.go @@ -0,0 +1,121 @@ +package etag + +import ( + "bytes" + "hash/crc32" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/internal/bytebufferpool" +) + +// Config defines the config for middleware. +type Config struct { + // Weak + Weak bool + + // Next defines a function to skip this middleware when returned true. + // + // Optional. Default: nil + Next func(c *fiber.Ctx) bool +} + +// ConfigDefault is the default config +var ConfigDefault = Config{} + +var normalizedHeaderETag = []byte("Etag") +var weakPrefix = []byte("W/") + +// New creates a new middleware handler +func New(config ...Config) fiber.Handler { + // Set default config + cfg := ConfigDefault + + // Override config if provided + if len(config) > 0 { + cfg = config[0] + } + + // Return new handler + return func(c *fiber.Ctx) (err error) { + // Don't execute middleware if Next returns true + if cfg.Next != nil && cfg.Next(c) { + return c.Next() + } + + // Return err if next handler returns one + if err = c.Next(); err != nil { + return + } + + // Don't generate ETags for invalid responses + if c.Response().StatusCode() != fiber.StatusOK { + return + } + body := c.Response().Body() + // Skips ETag if no response body is present + if len(body) <= 0 { + return + } + // Get ETag header from request + clientEtag := c.Request().Header.Peek(fiber.HeaderIfNoneMatch) + + // Generate ETag for response + crc32q := crc32.MakeTable(0xD5828281) + + bb := bytebufferpool.Get() + defer bytebufferpool.Put(bb) + + // Enable weak tag + if cfg.Weak { + _, _ = bb.Write(weakPrefix) + } + + _ = bb.WriteByte('"') + appendUint(bb.Bytes(), uint32(len(body))) + _ = bb.WriteByte('-') + appendUint(bb.Bytes(), crc32.Checksum(body, crc32q)) + _ = bb.WriteByte('"') + etag := bb.Bytes() + + // Check if client's ETag is weak + if bytes.HasPrefix(clientEtag, weakPrefix) { + // Check if server's ETag is weak + if bytes.Equal(clientEtag[2:], etag) || bytes.Equal(clientEtag[2:], etag[2:]) { + // W/1 == 1 || W/1 == W/1 + c.Context().ResetBody() + return c.SendStatus(fiber.StatusNotModified) + } + // W/1 != W/2 || W/1 != 2 + c.Response().Header.SetCanonical(normalizedHeaderETag, etag) + return + } + if bytes.Contains(clientEtag, etag) { + // 1 == 1 + c.Context().ResetBody() + return c.SendStatus(fiber.StatusNotModified) + } + // 1 != 2 + c.Response().Header.SetCanonical(normalizedHeaderETag, etag) + + return + } +} + +// appendUint appends n to dst and returns the extended dst. +func appendUint(dst []byte, n uint32) []byte { + var b [20]byte + buf := b[:] + i := len(buf) + var q uint32 + for n >= 10 { + i-- + q = n / 10 + buf[i] = '0' + byte(n-q*10) + n = q + } + i-- + buf[i] = '0' + byte(n) + + dst = append(dst, buf[i:]...) + return dst +} diff --git a/middleware/etag/etag_test.go b/middleware/etag/etag_test.go new file mode 100644 index 00000000..8406d7b1 --- /dev/null +++ b/middleware/etag/etag_test.go @@ -0,0 +1,32 @@ +package etag + +import ( + "net/http/httptest" + "testing" + + "github.com/valyala/fasthttp" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/utils" +) + +// go test -run Test_ETag +func Test_ETag(t *testing.T) { + i := 1<<32 - 1 + + t.Logf("%d, %d, %s", int32(i), uint32(i), fasthttp.AppendUint(nil, i)) +} + +// go test -run Test_ETag_Next +func Test_ETag_Next(t *testing.T) { + app := fiber.New() + app.Use(New(Config{ + Next: func(_ *fiber.Ctx) bool { + return true + }, + })) + + resp, err := app.Test(httptest.NewRequest("GET", "/", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, fiber.StatusNotFound, resp.StatusCode) +}