mirror of
https://github.com/gofiber/fiber.git
synced 2025-05-31 11:52:41 +00:00
🚀 Feature: Add earlydata middleware (#2270)
* middleware: add earlydata middleware * middleware/earlydata: address comments * Update README.md * Update README.md Co-authored-by: RW <rene@gofiber.io>
This commit is contained in:
parent
0b5a7d0425
commit
7170b2a40d
101
middleware/earlydata/README.md
Normal file
101
middleware/earlydata/README.md
Normal file
@ -0,0 +1,101 @@
|
||||
# Early Data Middleware
|
||||
|
||||
The Early Data middleware for [Fiber](https://github.com/gofiber/fiber) adds support for TLS 1.3's early data ("0-RTT") feature.
|
||||
Citing [RFC 8446](https://datatracker.ietf.org/doc/html/rfc8446#section-2-3), when a client and server share a PSK, TLS 1.3 allows clients to send data on the first flight ("early data") to speed up the request, effectively reducing the regular 1-RTT request to a 0-RTT request.
|
||||
|
||||
Make sure to enable fiber's `EnableTrustedProxyCheck` config option before using this middleware in order to not trust bogus HTTP request headers of the client.
|
||||
|
||||
Also be aware that enabling support for early data in your reverse proxy (e.g. nginx, as done with a simple `ssl_early_data on;`) makes requests replayable. Refer to the following documents before continuing:
|
||||
|
||||
- https://datatracker.ietf.org/doc/html/rfc8446#section-8
|
||||
- https://blog.trailofbits.com/2019/03/25/what-application-developers-need-to-know-about-tls-early-data-0rtt/
|
||||
|
||||
By default, this middleware allows early data requests on safe HTTP request methods only and rejects the request otherwise, i.e. aborts the request before executing your handler. This behavior can be controlled by the `AllowEarlyData` config option.
|
||||
Safe HTTP methods — `GET`, `HEAD`, `OPTIONS` and `TRACE` — should not modify a state on the server.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Early Data Middleware](#early-data-middleware)
|
||||
- [Table of Contents](#table-of-contents)
|
||||
- [Signatures](#signatures)
|
||||
- [Examples](#examples)
|
||||
- [Default Config](#default-config)
|
||||
- [Custom Config](#custom-config)
|
||||
- [Config](#config)
|
||||
- [Default Config](#default-config-1)
|
||||
|
||||
## Signatures
|
||||
|
||||
```go
|
||||
func New(config ...Config) fiber.Handler
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
First import the middleware from Fiber,
|
||||
|
||||
```go
|
||||
import (
|
||||
"github.com/gofiber/fiber/v3"
|
||||
"github.com/gofiber/fiber/v3/middleware/earlydata"
|
||||
)
|
||||
```
|
||||
|
||||
Then create a Fiber app with `app := fiber.New()`.
|
||||
|
||||
### Default Config
|
||||
|
||||
```go
|
||||
app.Use(earlydata.New())
|
||||
```
|
||||
|
||||
### Custom Config
|
||||
|
||||
```go
|
||||
app.Use(earlydata.New(earlydata.Config{
|
||||
Error: fiber.ErrTooEarly,
|
||||
// ...
|
||||
}))
|
||||
```
|
||||
|
||||
### Config
|
||||
|
||||
```go
|
||||
type Config struct {
|
||||
// Next defines a function to skip this middleware when returned true.
|
||||
//
|
||||
// Optional. Default: nil
|
||||
Next func(c fiber.Ctx) bool
|
||||
|
||||
// IsEarlyData returns whether the request is an early-data request.
|
||||
//
|
||||
// Optional. Default: a function which checks if the "Early-Data" request header equals "1".
|
||||
IsEarlyData func(c fiber.Ctx) bool
|
||||
|
||||
// AllowEarlyData returns whether the early-data request should be allowed or rejected.
|
||||
//
|
||||
// Optional. Default: a function which rejects the request on unsafe and allows the request on safe HTTP request methods.
|
||||
AllowEarlyData func(c fiber.Ctx) bool
|
||||
|
||||
// Error is returned in case an early-data request is rejected.
|
||||
//
|
||||
// Optional. Default: fiber.ErrTooEarly.
|
||||
Error error
|
||||
}
|
||||
```
|
||||
|
||||
### Default Config
|
||||
|
||||
```go
|
||||
var ConfigDefault = Config{
|
||||
IsEarlyData: func(c fiber.Ctx) bool {
|
||||
return c.Get("Early-Data") == "1"
|
||||
},
|
||||
|
||||
AllowEarlyData: func(c fiber.Ctx) bool {
|
||||
return fiber.IsMethodSafe(c.Method())
|
||||
},
|
||||
|
||||
Error: fiber.ErrTooEarly,
|
||||
}
|
||||
```
|
73
middleware/earlydata/config.go
Normal file
73
middleware/earlydata/config.go
Normal file
@ -0,0 +1,73 @@
|
||||
package earlydata
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultHeaderName = "Early-Data"
|
||||
DefaultHeaderTrueValue = "1"
|
||||
)
|
||||
|
||||
// Config defines the config for middleware.
|
||||
type Config struct {
|
||||
// Next defines a function to skip this middleware when returned true.
|
||||
//
|
||||
// Optional. Default: nil
|
||||
Next func(c fiber.Ctx) bool
|
||||
|
||||
// IsEarlyData returns whether the request is an early-data request.
|
||||
//
|
||||
// Optional. Default: a function which checks if the "Early-Data" request header equals "1".
|
||||
IsEarlyData func(c fiber.Ctx) bool
|
||||
|
||||
// AllowEarlyData returns whether the early-data request should be allowed or rejected.
|
||||
//
|
||||
// Optional. Default: a function which rejects the request on unsafe and allows the request on safe HTTP request methods.
|
||||
AllowEarlyData func(c fiber.Ctx) bool
|
||||
|
||||
// Error is returned in case an early-data request is rejected.
|
||||
//
|
||||
// Optional. Default: fiber.ErrTooEarly.
|
||||
Error error
|
||||
}
|
||||
|
||||
// ConfigDefault is the default config
|
||||
var ConfigDefault = Config{
|
||||
IsEarlyData: func(c fiber.Ctx) bool {
|
||||
return c.Get(DefaultHeaderName) == DefaultHeaderTrueValue
|
||||
},
|
||||
|
||||
AllowEarlyData: func(c fiber.Ctx) bool {
|
||||
return fiber.IsMethodSafe(c.Method())
|
||||
},
|
||||
|
||||
Error: fiber.ErrTooEarly,
|
||||
}
|
||||
|
||||
// Helper function to set default values
|
||||
func configDefault(config ...Config) Config {
|
||||
// Return default config if nothing provided
|
||||
if len(config) < 1 {
|
||||
return ConfigDefault
|
||||
}
|
||||
|
||||
// Override default config
|
||||
cfg := config[0]
|
||||
|
||||
// Set default values
|
||||
|
||||
if cfg.IsEarlyData == nil {
|
||||
cfg.IsEarlyData = ConfigDefault.IsEarlyData
|
||||
}
|
||||
|
||||
if cfg.AllowEarlyData == nil {
|
||||
cfg.AllowEarlyData = ConfigDefault.AllowEarlyData
|
||||
}
|
||||
|
||||
if cfg.Error == nil {
|
||||
cfg.Error = ConfigDefault.Error
|
||||
}
|
||||
|
||||
return cfg
|
||||
}
|
47
middleware/earlydata/earlydata.go
Normal file
47
middleware/earlydata/earlydata.go
Normal file
@ -0,0 +1,47 @@
|
||||
package earlydata
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
const (
|
||||
localsKeyAllowed = "earlydata_allowed"
|
||||
)
|
||||
|
||||
func IsEarly(c fiber.Ctx) bool {
|
||||
return c.Locals(localsKeyAllowed) != nil
|
||||
}
|
||||
|
||||
// New creates a new middleware handler
|
||||
// https://datatracker.ietf.org/doc/html/rfc8470#section-5.1
|
||||
func New(config ...Config) fiber.Handler {
|
||||
// Set default config
|
||||
cfg := configDefault(config...)
|
||||
|
||||
// Return new handler
|
||||
return func(c fiber.Ctx) error {
|
||||
// Don't execute middleware if Next returns true
|
||||
if cfg.Next != nil && cfg.Next(c) {
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
// Abort if we can't trust the early-data header
|
||||
if !c.IsProxyTrusted() {
|
||||
return cfg.Error
|
||||
}
|
||||
|
||||
// Continue stack if request is not an early-data request
|
||||
if !cfg.IsEarlyData(c) {
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
// Continue stack if we allow early-data for this request
|
||||
if cfg.AllowEarlyData(c) {
|
||||
_ = c.Locals(localsKeyAllowed, true)
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
// Else return our error
|
||||
return cfg.Error
|
||||
}
|
||||
}
|
189
middleware/earlydata/earlydata_test.go
Normal file
189
middleware/earlydata/earlydata_test.go
Normal file
@ -0,0 +1,189 @@
|
||||
package earlydata_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
"github.com/gofiber/fiber/v3/middleware/earlydata"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const (
|
||||
headerName = "Early-Data"
|
||||
headerValOn = "1"
|
||||
headerValOff = "0"
|
||||
)
|
||||
|
||||
func appWithConfig(t *testing.T, c *fiber.Config) *fiber.App {
|
||||
t.Helper()
|
||||
t.Parallel()
|
||||
|
||||
var app *fiber.App
|
||||
if c == nil {
|
||||
app = fiber.New()
|
||||
} else {
|
||||
app = fiber.New(*c)
|
||||
}
|
||||
|
||||
app.Use(earlydata.New())
|
||||
|
||||
// Middleware to test IsEarly func
|
||||
const localsKeyTestValid = "earlydata_testvalid"
|
||||
app.Use(func(c fiber.Ctx) error {
|
||||
isEarly := earlydata.IsEarly(c)
|
||||
|
||||
switch h := c.Get(headerName); h {
|
||||
case "",
|
||||
headerValOff:
|
||||
if isEarly {
|
||||
return errors.New("is early-data even though it's not")
|
||||
}
|
||||
|
||||
case headerValOn:
|
||||
switch {
|
||||
case fiber.IsMethodSafe(c.Method()):
|
||||
if !isEarly {
|
||||
return errors.New("should be early-data on safe HTTP methods")
|
||||
}
|
||||
default:
|
||||
if isEarly {
|
||||
return errors.New("early-data unsuported on unsafe HTTP methods")
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
return fmt.Errorf("header has unsupported value: %s", h)
|
||||
}
|
||||
|
||||
_ = c.Locals(localsKeyTestValid, true)
|
||||
|
||||
return c.Next()
|
||||
})
|
||||
|
||||
app.Add([]string{
|
||||
fiber.MethodGet,
|
||||
fiber.MethodPost,
|
||||
}, "/", func(c fiber.Ctx) error {
|
||||
if !c.Locals(localsKeyTestValid).(bool) {
|
||||
return errors.New("handler called even though validation failed")
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return app
|
||||
}
|
||||
|
||||
// go test -run Test_EarlyData
|
||||
func Test_EarlyData(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
trustedRun := func(t *testing.T, app *fiber.App) {
|
||||
t.Helper()
|
||||
|
||||
{
|
||||
req := httptest.NewRequest(fiber.MethodGet, "/", http.NoBody)
|
||||
|
||||
resp, err := app.Test(req)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, fiber.StatusOK, resp.StatusCode)
|
||||
|
||||
req.Header.Set(headerName, headerValOff)
|
||||
resp, err = app.Test(req)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, fiber.StatusOK, resp.StatusCode)
|
||||
|
||||
req.Header.Set(headerName, headerValOn)
|
||||
resp, err = app.Test(req)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, fiber.StatusOK, resp.StatusCode)
|
||||
}
|
||||
|
||||
{
|
||||
req := httptest.NewRequest(fiber.MethodPost, "/", http.NoBody)
|
||||
|
||||
resp, err := app.Test(req)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, fiber.StatusOK, resp.StatusCode)
|
||||
|
||||
req.Header.Set(headerName, headerValOff)
|
||||
resp, err = app.Test(req)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, fiber.StatusOK, resp.StatusCode)
|
||||
|
||||
req.Header.Set(headerName, headerValOn)
|
||||
resp, err = app.Test(req)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, fiber.StatusTooEarly, resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
untrustedRun := func(t *testing.T, app *fiber.App) {
|
||||
t.Helper()
|
||||
|
||||
{
|
||||
req := httptest.NewRequest(fiber.MethodGet, "/", http.NoBody)
|
||||
|
||||
resp, err := app.Test(req)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, fiber.StatusTooEarly, resp.StatusCode)
|
||||
|
||||
req.Header.Set(headerName, headerValOff)
|
||||
resp, err = app.Test(req)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, fiber.StatusTooEarly, resp.StatusCode)
|
||||
|
||||
req.Header.Set(headerName, headerValOn)
|
||||
resp, err = app.Test(req)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, fiber.StatusTooEarly, resp.StatusCode)
|
||||
}
|
||||
|
||||
{
|
||||
req := httptest.NewRequest(fiber.MethodPost, "/", http.NoBody)
|
||||
|
||||
resp, err := app.Test(req)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, fiber.StatusTooEarly, resp.StatusCode)
|
||||
|
||||
req.Header.Set(headerName, headerValOff)
|
||||
resp, err = app.Test(req)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, fiber.StatusTooEarly, resp.StatusCode)
|
||||
|
||||
req.Header.Set(headerName, headerValOn)
|
||||
resp, err = app.Test(req)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, fiber.StatusTooEarly, resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("empty config", func(t *testing.T) {
|
||||
app := appWithConfig(t, nil)
|
||||
trustedRun(t, app)
|
||||
})
|
||||
t.Run("default config", func(t *testing.T) {
|
||||
app := appWithConfig(t, &fiber.Config{})
|
||||
trustedRun(t, app)
|
||||
})
|
||||
|
||||
t.Run("config with EnableTrustedProxyCheck", func(t *testing.T) {
|
||||
app := appWithConfig(t, &fiber.Config{
|
||||
EnableTrustedProxyCheck: true,
|
||||
})
|
||||
untrustedRun(t, app)
|
||||
})
|
||||
t.Run("config with EnableTrustedProxyCheck and trusted TrustedProxies", func(t *testing.T) {
|
||||
app := appWithConfig(t, &fiber.Config{
|
||||
EnableTrustedProxyCheck: true,
|
||||
TrustedProxies: []string{
|
||||
"0.0.0.0",
|
||||
},
|
||||
})
|
||||
trustedRun(t, app)
|
||||
})
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user