-
Notifications
You must be signed in to change notification settings - Fork 466
Expand file tree
/
Copy pathrequestbodytimeout_test.go
More file actions
205 lines (168 loc) · 6.65 KB
/
Copy pathrequestbodytimeout_test.go
File metadata and controls
205 lines (168 loc) · 6.65 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
package frankenphp_test
import (
"context"
"crypto/tls"
"fmt"
"io"
"net"
"net/http"
"os"
"strings"
"testing"
"time"
"github.com/dunglas/frankenphp"
"github.com/stretchr/testify/require"
"golang.org/x/net/http2"
)
// newH2CServer starts a cleartext HTTP/2 (h2c) server for handler and returns
// its address and a matching client. h2c drives the SetReadDeadline path that
// only HTTP/2 exercises. The listener and server are closed via t.Cleanup.
func newH2CServer(t *testing.T, handler http.HandlerFunc) (addr string, client *http.Client) {
t.Helper()
ln, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
protocols := new(http.Protocols)
protocols.SetUnencryptedHTTP2(true)
srv := &http.Server{Handler: handler, Protocols: protocols}
go func() { _ = srv.Serve(ln) }()
t.Cleanup(func() {
_ = srv.Close()
_ = ln.Close()
})
client = &http.Client{Transport: &http2.Transport{
AllowHTTP: true,
DialTLSContext: func(_ context.Context, network, addr string, _ *tls.Config) (net.Conn, error) {
return net.Dial(network, addr)
},
}}
return ln.Addr().String(), client
}
// TestRequestBodyTimeout proves that WithRequestBodyTimeout bounds a slow-POST
// client: it announces a large Content-Length, then stalls without sending the
// body. Without the option the PHP thread would block in Body.Read until the
// connection closed; with it, the read is cut off and the request completes.
func TestRequestBodyTimeout(t *testing.T) {
require.NoError(t, frankenphp.Init())
defer frankenphp.Shutdown()
cwd, _ := os.Getwd()
handler := func(w http.ResponseWriter, r *http.Request) {
req, err := frankenphp.NewRequestWithContext(r,
frankenphp.WithRequestDocumentRoot(cwd+"/testdata/", false),
frankenphp.WithRequestBodyTimeout(300*time.Millisecond),
)
require.NoError(t, err)
require.NoError(t, frankenphp.ServeHTTP(w, req))
}
ts := newRawServer(t, handler)
defer ts.Close()
conn, err := net.Dial("tcp", ts.Addr())
require.NoError(t, err)
defer func() { _ = conn.Close() }()
// Announce a 1 MiB body, then send nothing: a classic slow POST.
_, err = fmt.Fprintf(conn,
"POST /read-input.php HTTP/1.1\r\nHost: %s\r\nContent-Type: application/octet-stream\r\nContent-Length: 1048576\r\nConnection: close\r\n\r\n",
ts.Addr(),
)
require.NoError(t, err)
require.NoError(t, conn.SetReadDeadline(time.Now().Add(5*time.Second)))
start := time.Now()
resp, err := io.ReadAll(conn)
elapsed := time.Since(start)
require.NoError(t, err)
// The 300ms idle timeout (applied at most twice by PHP's read loop) must
// release the thread well before the client's 5s read deadline.
require.Less(t, elapsed, 4*time.Second, "slow body must be bounded by the timeout")
require.Contains(t, string(resp), "200 OK")
// PHP saw an empty body: php://input read zero bytes.
require.Contains(t, string(resp), "read=0")
}
// TestRequestBodyTimeoutHTTP2 is the HTTP/2 counterpart of the test above: over
// HTTP/2 the deadline lands on the stream (via x/net) rather than the net.Conn,
// so this exercises the SetReadDeadline path that HTTP/1 never touches. A slow
// POST that trips the idle timeout must bound the read and return cleanly.
// The nil-dereference crash of php/frankenphp#2535 needs the writer to be used
// after its stream is finalized; see TestSetReadDeadlineRecoversFromPanic.
func TestRequestBodyTimeoutHTTP2(t *testing.T) {
require.NoError(t, frankenphp.Init())
defer frankenphp.Shutdown()
cwd, _ := os.Getwd()
handler := func(w http.ResponseWriter, r *http.Request) {
req, err := frankenphp.NewRequestWithContext(r,
frankenphp.WithRequestDocumentRoot(cwd+"/testdata/", false),
frankenphp.WithRequestBodyTimeout(300*time.Millisecond),
)
require.NoError(t, err)
require.NoError(t, frankenphp.ServeHTTP(w, req))
}
addr, client := newH2CServer(t, handler)
// A body that never sends data: the server blocks in Body.Read until the
// idle timeout fires. Close the writer once the request returns.
pr, pw := io.Pipe()
defer func() { _ = pw.Close() }()
req, err := http.NewRequest(http.MethodPost, "http://"+addr+"/read-input.php", pr)
require.NoError(t, err)
req.Header.Set("Content-Type", "application/octet-stream")
start := time.Now()
resp, err := client.Do(req)
require.NoError(t, err)
defer func() { _ = resp.Body.Close() }()
body, err := io.ReadAll(resp.Body)
require.NoError(t, err)
elapsed := time.Since(start)
require.Less(t, elapsed, 4*time.Second, "slow body must be bounded by the timeout")
require.Equal(t, http.StatusOK, resp.StatusCode)
require.Contains(t, string(body), "read=0")
}
// TestFinishRequestThenReadBodyHTTP2 reproduces php/frankenphp#2535: a script
// that calls frankenphp_finish_request() and then reads php://input triggers
// go_read_post after the HTTP/2 responseWriter has been finalized. Setting a
// read deadline on that dead writer would dereference a nil pointer and crash
// the whole process. enable_post_data_reading=Off defers the body read until
// the explicit php://input access, i.e. after the request is finished.
func TestFinishRequestThenReadBodyHTTP2(t *testing.T) {
iniDir := t.TempDir()
require.NoError(t, os.WriteFile(iniDir+"/php.ini", []byte("enable_post_data_reading=Off\n"), 0o600))
t.Setenv("PHPRC", iniDir+"/php.ini")
require.NoError(t, frankenphp.Init())
defer frankenphp.Shutdown()
cwd, _ := os.Getwd()
handler := func(w http.ResponseWriter, r *http.Request) {
req, err := frankenphp.NewRequestWithContext(r,
frankenphp.WithRequestDocumentRoot(cwd+"/testdata/", false),
frankenphp.WithRequestBodyTimeout(300*time.Millisecond),
)
require.NoError(t, err)
require.NoError(t, frankenphp.ServeHTTP(w, req))
}
addr, client := newH2CServer(t, handler)
req, err := http.NewRequest(http.MethodPost, "http://"+addr+"/finish-then-read-input.php", strings.NewReader("hello world"))
require.NoError(t, err)
req.Header.Set("Content-Type", "application/octet-stream")
resp, err := client.Do(req)
require.NoError(t, err)
defer func() { _ = resp.Body.Close() }()
_, err = io.ReadAll(resp.Body)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
}
// rawServer is a minimal HTTP server exposing its listener address so a test
// can drive it with a raw TCP connection (needed to simulate a stalled body).
type rawServer struct {
ln net.Listener
srv *http.Server
}
func newRawServer(t *testing.T, handler http.HandlerFunc) *rawServer {
t.Helper()
ln, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
srv := &http.Server{Handler: handler}
go func() {
_ = srv.Serve(ln)
}()
return &rawServer{ln: ln, srv: srv}
}
func (s *rawServer) Addr() string { return s.ln.Addr().String() }
func (s *rawServer) Close() {
_ = s.srv.Close()
_ = s.ln.Close()
}