@marcel91200 / caddy Mirror · read-only / commits / 96f638ea
Fork 0

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