Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS
Mirrored from https://github.com/Slazee/caddy.git
Synced Nov 12, 20:45 UTC
httpredirectlistener: Only set read limit for when request is HTTP (#5917)
WeidiDeng committed
Nov 20, 2023 at 20:31 UTC
96f638eaada62134c292c766aeec0b71ade20257
2 files changed
+151
-50
caddytest/integration/listener_test.go
new
+94
@@ -0,0 +1,94 @@
1
+package integration
2
+
3
+import (
4
+ "bytes"
5
+ "fmt"
6
+ "math/rand"
7
+ "net"
8
+ "net/http"
9
+ "strings"
10
+ "testing"
11
+
12
+ "github.com/caddyserver/caddy/v2/caddytest"
13
+)
14
+
15
+func setupListenerWrapperTest(t *testing.T, handlerFunc http.HandlerFunc) *caddytest.Tester {
16
+ l, err := net.Listen("tcp", "127.0.0.1:0")
17
+ if err != nil {
18
+ t.Fatalf("failed to listen: %s", err)
19
+ }
20
+
21
+ mux := http.NewServeMux()
22
+ mux.Handle("/", handlerFunc)
23
+ srv := &http.Server{
24
+ Handler: mux,
25
+ }
26
+ go srv.Serve(l)
27
+ t.Cleanup(func() {
28
+ _ = srv.Close()
29
+ _ = l.Close()
30
+ })
31
+ tester := caddytest.NewTester(t)
32
+ tester.InitServer(fmt.Sprintf(`
33
+ {
34
+ skip_install_trust
35
+ admin localhost:2999
36
+ http_port 9080
37
+ https_port 9443
38
+ local_certs
39
+ servers :9443 {
40
+ listener_wrappers {
41
+ http_redirect
42
+ tls
43
+ }
44
+ }
45
+ }
46
+ localhost {
47
+ reverse_proxy %s
48
+ }
49
+ `, l.Addr().String()), "caddyfile")
50
+ return tester
51
+}
52
+
53
+func TestHTTPRedirectWrapperWithLargeUpload(t *testing.T) {
54
+ const uploadSize = (1024 * 1024) + 1 // 1 MB + 1 byte
55
+ // 1 more than an MB
56
+ body := make([]byte, uploadSize)
57
+ rand.New(rand.NewSource(0)).Read(body)
58
+
59
+ tester := setupListenerWrapperTest(t, func(writer http.ResponseWriter, request *http.Request) {
60
+ buf := new(bytes.Buffer)
61
+ _, err := buf.ReadFrom(request.Body)
62
+ if err != nil {
63
+ t.Fatalf("failed to read body: %s", err)
64
+ }
65
+
66
+ if !bytes.Equal(buf.Bytes(), body) {
67
+ t.Fatalf("body not the same")
68
+ }
69
+
70
+ writer.WriteHeader(http.StatusNoContent)
71
+ })
72
+ resp, err := tester.Client.Post("https://localhost:9443", "application/octet-stream", bytes.NewReader(body))
73
+ if err != nil {
74
+ t.Fatalf("failed to post: %s", err)
75
+ }
76
+
77
+ if resp.StatusCode != http.StatusNoContent {
78
+ t.Fatalf("unexpected status: %d != %d", resp.StatusCode, http.StatusNoContent)
79
+ }
80
+}
81
+
82
+func TestLargeHttpRequest(t *testing.T) {
83
+ tester := setupListenerWrapperTest(t, func(writer http.ResponseWriter, request *http.Request) {
84
+ t.Fatal("not supposed to handle a request")
85
+ })
86
+
87
+ // We never read the body in any way, set an extra long header instead.
88
+ req, _ := http.NewRequest("POST", "http://localhost:9443", nil)
89
+ req.Header.Set("Long-Header", strings.Repeat("X", 1024*1024))
90
+ _, err := tester.Client.Do(req)
91
+ if err == nil {
92
+ t.Fatal("not supposed to succeed")
93
+ }
94
+}
modules/caddyhttp/httpredirectlistener.go
+57
-50
@@ -16,11 +16,11 @@ package caddyhttp
16
17
import (
18
"bufio"
19
+ "bytes"
20
"fmt"
21
"io"
22
"net"
23
"net/http"
23
- "sync"
24
25
"github.com/caddyserver/caddy/v2"
26
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
@@ -86,15 +86,17 @@ func (l *httpRedirectListener) Accept() (net.Conn, error) {
86
}
87
88
return &httpRedirectConn{
89
- Conn: c,
90
- r: bufio.NewReader(io.LimitReader(c, maxHeaderBytes)),
89
+ Conn: c,
90
+ limit: maxHeaderBytes,
91
+ r: bufio.NewReader(c),
92
}, nil
93
}
94
95
type httpRedirectConn struct {
96
net.Conn
96
- once sync.Once
97
- r *bufio.Reader
97
+ once bool
98
+ limit int64
99
+ r *bufio.Reader
100
}
101
102
// Read tries to peek at the first few bytes of the request, and if we get
@@ -102,53 +104,58 @@ type httpRedirectConn struct {
104
// like an HTTP request, then we perform a HTTP->HTTPS redirect on the same
105
// port as the original connection.
106
func (c *httpRedirectConn) Read(p []byte) (int, error) {
105
- var errReturn error
106
- c.once.Do(func() {
107
- firstBytes, err := c.r.Peek(5)
108
- if err != nil {
109
- return
110
- }
111
-
112
- // If the request doesn't look like HTTP, then it's probably
113
- // TLS bytes and we don't need to do anything.
114
- if !firstBytesLookLikeHTTP(firstBytes) {
115
- return
116
- }
117
-
118
- // Parse the HTTP request, so we can get the Host and URL to redirect to.
119
- req, err := http.ReadRequest(c.r)
120
- if err != nil {
121
- return
122
- }
123
-
124
- // Build the redirect response, using the same Host and URL,
125
- // but replacing the scheme with https.
126
- headers := make(http.Header)
127
- headers.Add("Location", "https://"+req.Host+req.URL.String())
128
- resp := &http.Response{
129
- Proto: "HTTP/1.0",
130
- Status: "308 Permanent Redirect",
131
- StatusCode: 308,
132
- ProtoMajor: 1,
133
- ProtoMinor: 0,
134
- Header: headers,
135
- }
136
-
137
- err = resp.Write(c.Conn)
138
- if err != nil {
139
- errReturn = fmt.Errorf("couldn't write HTTP->HTTPS redirect")
140
- return
141
- }
142
-
143
- errReturn = fmt.Errorf("redirected HTTP request on HTTPS port")
144
- c.Conn.Close()
145
- })
146
-
147
- if errReturn != nil {
148
- return 0, errReturn
107
+ if c.once {
108
+ return c.r.Read(p)
109
}
110
+ // no need to use sync.Once - net.Conn is not read from concurrently.
111
+ c.once = true
112
151
- return c.r.Read(p)
113
+ firstBytes, err := c.r.Peek(5)
114
+ if err != nil {
115
+ return 0, err
116
+ }
117
+
118
+ // If the request doesn't look like HTTP, then it's probably
119
+ // TLS bytes, and we don't need to do anything.
120
+ if !firstBytesLookLikeHTTP(firstBytes) {
121
+ return c.r.Read(p)
122
+ }
123
+
124
+ // From now on, we can be almost certain the request is HTTP.
125
+ // The returned error will be non nil and caller are expected to
126
+ // close the connection.
127
+
128
+ // Set the read limit, io.MultiReader is needed because
129
+ // when resetting, *bufio.Reader discards buffered data.
130
+ buffered, _ := c.r.Peek(c.r.Buffered())
131
+ mr := io.MultiReader(bytes.NewReader(buffered), c.Conn)
132
+ c.r.Reset(io.LimitReader(mr, c.limit))
133
+
134
+ // Parse the HTTP request, so we can get the Host and URL to redirect to.
135
+ req, err := http.ReadRequest(c.r)
136
+ if err != nil {
137
+ return 0, fmt.Errorf("couldn't read HTTP request")
138
+ }
139
+
140
+ // Build the redirect response, using the same Host and URL,
141
+ // but replacing the scheme with https.
142
+ headers := make(http.Header)
143
+ headers.Add("Location", "https://"+req.Host+req.URL.String())
144
+ resp := &http.Response{
145
+ Proto: "HTTP/1.0",
146
+ Status: "308 Permanent Redirect",
147
+ StatusCode: 308,
148
+ ProtoMajor: 1,
149
+ ProtoMinor: 0,
150
+ Header: headers,
151
+ }
152
+
153
+ err = resp.Write(c.Conn)
154
+ if err != nil {
155
+ return 0, fmt.Errorf("couldn't write HTTP->HTTPS redirect")
156
+ }
157
+
158
+ return 0, fmt.Errorf("redirected HTTP request on HTTPS port")
159
}
160
161
// firstBytesLookLikeHTTP reports whether a TLS record header