Skip to content

Commit 55f5f3c

Browse files
committed
middleware: add earlydata middleware
1 parent fd1a29e commit 55f5f3c

File tree

4 files changed

+390
-0
lines changed

4 files changed

+390
-0
lines changed

middleware/earlydata/README.md

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# Early Data Middleware
2+
3+
The Early Data middleware for [Fiber](https://github.com/gofiber/fiber) adds support for TLS 1.3's early data ("0-RTT") feature.
4+
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.
5+
6+
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.
7+
8+
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:
9+
10+
- https://datatracker.ietf.org/doc/html/rfc8446#section-8
11+
- https://blog.trailofbits.com/2019/03/25/what-application-developers-need-to-know-about-tls-early-data-0rtt/
12+
13+
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.
14+
Safe HTTP methods — `GET`, `HEAD`, `OPTIONS` and `TRACE` — should not modify a state on the server.
15+
16+
## Table of Contents
17+
18+
- [Early Data Middleware](#early-data-middleware)
19+
- [Table of Contents](#table-of-contents)
20+
- [Signatures](#signatures)
21+
- [Examples](#examples)
22+
- [Default Config](#default-config)
23+
- [Custom Config](#custom-config)
24+
- [Config](#config)
25+
- [Default Config](#default-config-1)
26+
27+
## Signatures
28+
29+
```go
30+
func New(config ...Config) fiber.Handler
31+
```
32+
33+
## Examples
34+
35+
First import the middleware from Fiber,
36+
37+
```go
38+
import (
39+
"github.com/gofiber/fiber/v3"
40+
"github.com/gofiber/fiber/v3/middleware/earlydata"
41+
)
42+
```
43+
44+
Then create a Fiber app with `app := fiber.New()`.
45+
46+
### Default Config
47+
48+
```go
49+
app.Use(earlydata.New())
50+
```
51+
52+
### Custom Config
53+
54+
```go
55+
app.Use(earlydata.New(earlydata.Config{
56+
Error: fiber.ErrTooEarly,
57+
// ...
58+
}))
59+
```
60+
61+
### Config
62+
63+
```go
64+
type Config struct {
65+
// IsEarlyData returns whether the request is an early-data request.
66+
//
67+
// Optional. Default: a function which checks if the "Early-Data" request header equals "1".
68+
IsEarlyData func(c fiber.Ctx) bool
69+
70+
// AllowEarlyData returns whether the early-data request should be allowed or rejected.
71+
//
72+
// Optional. Default: a function which rejects the request on unsafe and allows the request on safe HTTP request methods.
73+
AllowEarlyData func(c fiber.Ctx) bool
74+
75+
// Error is returned in case an early-data request is rejected.
76+
//
77+
// Optional. Default: fiber.ErrTooEarly.
78+
Error error
79+
}
80+
```
81+
82+
### Default Config
83+
84+
```go
85+
var ConfigDefault = Config{
86+
IsEarlyData: func(c fiber.Ctx) bool {
87+
return c.Get("Early-Data") == "1"
88+
},
89+
90+
AllowEarlyData: func(c fiber.Ctx) bool {
91+
return fiber.IsMethodSafe(c.Method())
92+
},
93+
94+
Error: fiber.ErrTooEarly,
95+
}
96+
```

middleware/earlydata/config.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package earlydata
2+
3+
import (
4+
"github.com/gofiber/fiber/v3"
5+
)
6+
7+
// Config defines the config for middleware.
8+
type Config struct {
9+
// IsEarlyData returns whether the request is an early-data request.
10+
//
11+
// Optional. Default: a function which checks if the "Early-Data" request header equals "1".
12+
IsEarlyData func(c fiber.Ctx) bool
13+
14+
// AllowEarlyData returns whether the early-data request should be allowed or rejected.
15+
//
16+
// Optional. Default: a function which rejects the request on unsafe and allows the request on safe HTTP request methods.
17+
AllowEarlyData func(c fiber.Ctx) bool
18+
19+
// Error is returned in case an early-data request is rejected.
20+
//
21+
// Optional. Default: fiber.ErrTooEarly.
22+
Error error
23+
}
24+
25+
// ConfigDefault is the default config
26+
var ConfigDefault = Config{
27+
IsEarlyData: func(c fiber.Ctx) bool {
28+
return c.Get("Early-Data") == "1"
29+
},
30+
31+
AllowEarlyData: func(c fiber.Ctx) bool {
32+
return fiber.IsMethodSafe(c.Method())
33+
},
34+
35+
Error: fiber.ErrTooEarly,
36+
}
37+
38+
// Helper function to set default values
39+
func configDefault(config ...Config) Config {
40+
// Return default config if nothing provided
41+
if len(config) < 1 {
42+
return ConfigDefault
43+
}
44+
45+
// Override default config
46+
cfg := config[0]
47+
48+
// Set default values
49+
50+
if cfg.IsEarlyData == nil {
51+
cfg.IsEarlyData = ConfigDefault.IsEarlyData
52+
}
53+
54+
if cfg.AllowEarlyData == nil {
55+
cfg.AllowEarlyData = ConfigDefault.AllowEarlyData
56+
}
57+
58+
if cfg.Error == nil {
59+
cfg.Error = ConfigDefault.Error
60+
}
61+
62+
return cfg
63+
}

middleware/earlydata/earlydata.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package earlydata
2+
3+
import (
4+
"github.com/gofiber/fiber/v3"
5+
)
6+
7+
const (
8+
localsKeyAllowed = "earlydata_allowed"
9+
)
10+
11+
func IsEarly(c fiber.Ctx) bool {
12+
return c.Locals(localsKeyAllowed) != nil
13+
}
14+
15+
// New creates a new middleware handler
16+
// https://datatracker.ietf.org/doc/html/rfc8470#section-5.1
17+
func New(config ...Config) fiber.Handler {
18+
// Set default config
19+
cfg := configDefault(config...)
20+
21+
// Return new handler
22+
return func(c fiber.Ctx) error {
23+
// Abort if we can't trust the early-data header
24+
if !c.IsProxyTrusted() {
25+
return cfg.Error
26+
}
27+
28+
// Continue stack if request is not an early-data request
29+
if !cfg.IsEarlyData(c) {
30+
return c.Next()
31+
}
32+
33+
// Continue stack if we allow early-data for this request
34+
if cfg.AllowEarlyData(c) {
35+
_ = c.Locals(localsKeyAllowed, true)
36+
return c.Next()
37+
}
38+
39+
// Else return our error
40+
return cfg.Error
41+
}
42+
}
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
package earlydata_test
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"net/http"
7+
"net/http/httptest"
8+
"testing"
9+
10+
"github.com/gofiber/fiber/v3"
11+
"github.com/gofiber/fiber/v3/middleware/earlydata"
12+
"github.com/stretchr/testify/require"
13+
)
14+
15+
const (
16+
headerName = "Early-Data"
17+
headerValOn = "1"
18+
headerValOff = "0"
19+
)
20+
21+
func appWithConfig(t *testing.T, c *fiber.Config) *fiber.App {
22+
t.Helper()
23+
t.Parallel()
24+
25+
var app *fiber.App
26+
if c == nil {
27+
app = fiber.New()
28+
} else {
29+
app = fiber.New(*c)
30+
}
31+
32+
app.Use(earlydata.New())
33+
34+
// Middleware to test IsEarly func
35+
const localsKeyTestValid = "earlydata_testvalid"
36+
app.Use(func(c fiber.Ctx) error {
37+
isEarly := earlydata.IsEarly(c)
38+
39+
switch h := c.Get(headerName); h {
40+
case "",
41+
headerValOff:
42+
if isEarly {
43+
return errors.New("is early-data even though it's not")
44+
}
45+
46+
case headerValOn:
47+
switch {
48+
case fiber.IsMethodSafe(c.Method()):
49+
if !isEarly {
50+
return errors.New("should be early-data on safe HTTP methods")
51+
}
52+
default:
53+
if isEarly {
54+
return errors.New("early-data unsuported on unsafe HTTP methods")
55+
}
56+
}
57+
58+
default:
59+
return fmt.Errorf("header has unsupported value: %s", h)
60+
}
61+
62+
_ = c.Locals(localsKeyTestValid, true)
63+
64+
return c.Next()
65+
})
66+
67+
app.Add([]string{
68+
fiber.MethodGet,
69+
fiber.MethodPost,
70+
}, "/", func(c fiber.Ctx) error {
71+
if !c.Locals(localsKeyTestValid).(bool) {
72+
return errors.New("handler called even though validation failed")
73+
}
74+
75+
return nil
76+
})
77+
78+
return app
79+
}
80+
81+
// go test -run Test_EarlyData
82+
func Test_EarlyData(t *testing.T) {
83+
t.Parallel()
84+
85+
trustedRun := func(t *testing.T, app *fiber.App) {
86+
t.Helper()
87+
88+
{
89+
req := httptest.NewRequest(fiber.MethodGet, "/", http.NoBody)
90+
91+
resp, err := app.Test(req)
92+
require.NoError(t, err)
93+
require.Equal(t, fiber.StatusOK, resp.StatusCode)
94+
95+
req.Header.Set(headerName, headerValOff)
96+
resp, err = app.Test(req)
97+
require.NoError(t, err)
98+
require.Equal(t, fiber.StatusOK, resp.StatusCode)
99+
100+
req.Header.Set(headerName, headerValOn)
101+
resp, err = app.Test(req)
102+
require.NoError(t, err)
103+
require.Equal(t, fiber.StatusOK, resp.StatusCode)
104+
}
105+
106+
{
107+
req := httptest.NewRequest(fiber.MethodPost, "/", http.NoBody)
108+
109+
resp, err := app.Test(req)
110+
require.NoError(t, err)
111+
require.Equal(t, fiber.StatusOK, resp.StatusCode)
112+
113+
req.Header.Set(headerName, headerValOff)
114+
resp, err = app.Test(req)
115+
require.NoError(t, err)
116+
require.Equal(t, fiber.StatusOK, resp.StatusCode)
117+
118+
req.Header.Set(headerName, headerValOn)
119+
resp, err = app.Test(req)
120+
require.NoError(t, err)
121+
require.Equal(t, fiber.StatusTooEarly, resp.StatusCode)
122+
}
123+
}
124+
125+
untrustedRun := func(t *testing.T, app *fiber.App) {
126+
t.Helper()
127+
128+
{
129+
req := httptest.NewRequest(fiber.MethodGet, "/", http.NoBody)
130+
131+
resp, err := app.Test(req)
132+
require.NoError(t, err)
133+
require.Equal(t, fiber.StatusTooEarly, resp.StatusCode)
134+
135+
req.Header.Set(headerName, headerValOff)
136+
resp, err = app.Test(req)
137+
require.NoError(t, err)
138+
require.Equal(t, fiber.StatusTooEarly, resp.StatusCode)
139+
140+
req.Header.Set(headerName, headerValOn)
141+
resp, err = app.Test(req)
142+
require.NoError(t, err)
143+
require.Equal(t, fiber.StatusTooEarly, resp.StatusCode)
144+
}
145+
146+
{
147+
req := httptest.NewRequest(fiber.MethodPost, "/", http.NoBody)
148+
149+
resp, err := app.Test(req)
150+
require.NoError(t, err)
151+
require.Equal(t, fiber.StatusTooEarly, resp.StatusCode)
152+
153+
req.Header.Set(headerName, headerValOff)
154+
resp, err = app.Test(req)
155+
require.NoError(t, err)
156+
require.Equal(t, fiber.StatusTooEarly, resp.StatusCode)
157+
158+
req.Header.Set(headerName, headerValOn)
159+
resp, err = app.Test(req)
160+
require.NoError(t, err)
161+
require.Equal(t, fiber.StatusTooEarly, resp.StatusCode)
162+
}
163+
}
164+
165+
t.Run("empty config", func(t *testing.T) {
166+
app := appWithConfig(t, nil)
167+
trustedRun(t, app)
168+
})
169+
t.Run("default config", func(t *testing.T) {
170+
app := appWithConfig(t, &fiber.Config{})
171+
trustedRun(t, app)
172+
})
173+
174+
t.Run("config with EnableTrustedProxyCheck", func(t *testing.T) {
175+
app := appWithConfig(t, &fiber.Config{
176+
EnableTrustedProxyCheck: true,
177+
})
178+
untrustedRun(t, app)
179+
})
180+
t.Run("config with EnableTrustedProxyCheck and trusted TrustedProxies", func(t *testing.T) {
181+
app := appWithConfig(t, &fiber.Config{
182+
EnableTrustedProxyCheck: true,
183+
TrustedProxies: []string{
184+
"0.0.0.0",
185+
},
186+
})
187+
trustedRun(t, app)
188+
})
189+
}

0 commit comments

Comments
 (0)