@marcel91200 / caddy Mirror · read-only
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 11, 01:00 UTC

master
go 261 lines 8.38 KB
Raw
1 package reverseproxy
2
3 import (
4 "context"
5 "net/http"
6 "net/http/httptest"
7 "net/http/httptrace"
8 "net/textproto"
9 "testing"
10
11 "github.com/caddyserver/caddy/v2/modules/caddyhttp"
12 )
13
14 func TestAddForwardedHeadersNonIP(t *testing.T) {
15 h := Handler{}
16
17 // Simulate a request with a non-IP remote address (e.g. SCION, abstract socket, or hostname)
18 req := httptest.NewRequest("GET", "/", nil)
19 req.RemoteAddr = "my-weird-network:12345"
20
21 // Mock the context variables required by Caddy.
22 // We need to inject the variable map manually since we aren't running the full server.
23 vars := map[string]any{
24 caddyhttp.TrustedProxyVarKey: false,
25 }
26 ctx := context.WithValue(req.Context(), caddyhttp.VarsCtxKey, vars)
27 req = req.WithContext(ctx)
28
29 // Execute the unexported function
30 err := h.addForwardedHeaders(req)
31
32 // Expectation: No error should be returned for non-IP addresses.
33 // The function should simply skip the trusted proxy check.
34 if err != nil {
35 t.Errorf("expected no error for non-IP address, got: %v", err)
36 }
37 }
38
39 func TestAddForwardedHeaders_UnixSocketTrusted(t *testing.T) {
40 h := Handler{}
41
42 req := httptest.NewRequest("GET", "http://example.com/", nil)
43 req.RemoteAddr = "@"
44 req.Header.Set("X-Forwarded-For", "1.2.3.4, 10.0.0.1")
45 req.Header.Set("X-Forwarded-Proto", "https")
46 req.Header.Set("X-Forwarded-Host", "original.example.com")
47
48 vars := map[string]any{
49 caddyhttp.TrustedProxyVarKey: true,
50 caddyhttp.ClientIPVarKey: "1.2.3.4",
51 }
52 ctx := context.WithValue(req.Context(), caddyhttp.VarsCtxKey, vars)
53 req = req.WithContext(ctx)
54
55 err := h.addForwardedHeaders(req)
56 if err != nil {
57 t.Fatalf("expected no error, got: %v", err)
58 }
59
60 if got := req.Header.Get("X-Forwarded-For"); got != "1.2.3.4, 10.0.0.1" {
61 t.Errorf("X-Forwarded-For = %q, want %q", got, "1.2.3.4, 10.0.0.1")
62 }
63 if got := req.Header.Get("X-Forwarded-Proto"); got != "https" {
64 t.Errorf("X-Forwarded-Proto = %q, want %q", got, "https")
65 }
66 if got := req.Header.Get("X-Forwarded-Host"); got != "original.example.com" {
67 t.Errorf("X-Forwarded-Host = %q, want %q", got, "original.example.com")
68 }
69 }
70
71 func TestAddForwardedHeaders_UnixSocketUntrusted(t *testing.T) {
72 h := Handler{}
73
74 req := httptest.NewRequest("GET", "http://example.com/", nil)
75 req.RemoteAddr = "@"
76 req.Header.Set("X-Forwarded-For", "1.2.3.4")
77 req.Header.Set("X-Forwarded-Proto", "https")
78 req.Header.Set("X-Forwarded-Host", "spoofed.example.com")
79
80 vars := map[string]any{
81 caddyhttp.TrustedProxyVarKey: false,
82 caddyhttp.ClientIPVarKey: "",
83 }
84 ctx := context.WithValue(req.Context(), caddyhttp.VarsCtxKey, vars)
85 req = req.WithContext(ctx)
86
87 err := h.addForwardedHeaders(req)
88 if err != nil {
89 t.Fatalf("expected no error, got: %v", err)
90 }
91
92 if got := req.Header.Get("X-Forwarded-For"); got != "" {
93 t.Errorf("X-Forwarded-For should be deleted, got %q", got)
94 }
95 if got := req.Header.Get("X-Forwarded-Proto"); got != "" {
96 t.Errorf("X-Forwarded-Proto should be deleted, got %q", got)
97 }
98 if got := req.Header.Get("X-Forwarded-Host"); got != "" {
99 t.Errorf("X-Forwarded-Host should be deleted, got %q", got)
100 }
101 }
102
103 func TestAddForwardedHeaders_UnixSocketTrustedNoExistingHeaders(t *testing.T) {
104 h := Handler{}
105
106 req := httptest.NewRequest("GET", "http://example.com/", nil)
107 req.RemoteAddr = "@"
108
109 vars := map[string]any{
110 caddyhttp.TrustedProxyVarKey: true,
111 caddyhttp.ClientIPVarKey: "5.6.7.8",
112 }
113 ctx := context.WithValue(req.Context(), caddyhttp.VarsCtxKey, vars)
114 req = req.WithContext(ctx)
115
116 err := h.addForwardedHeaders(req)
117 if err != nil {
118 t.Fatalf("expected no error, got: %v", err)
119 }
120
121 if got := req.Header.Get("X-Forwarded-For"); got != "" {
122 t.Errorf("X-Forwarded-For should be empty when no prior XFF exists, got %q", got)
123 }
124 if got := req.Header.Get("X-Forwarded-Proto"); got != "http" {
125 t.Errorf("X-Forwarded-Proto = %q, want %q", got, "http")
126 }
127 if got := req.Header.Get("X-Forwarded-Host"); got != "example.com" {
128 t.Errorf("X-Forwarded-Host = %q, want %q", got, "example.com")
129 }
130 }
131
132 // TestInformationalResponseKeepsHandlerHeaders verifies that a 1xx response
133 // from the upstream does not remove response headers set before proxying.
134 func TestInformationalResponseKeepsHandlerHeaders(t *testing.T) {
135 backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
136 w.Header().Set("Link", "</style.css>; rel=preload")
137 w.WriteHeader(http.StatusEarlyHints)
138 w.Header().Del("Link")
139 w.WriteHeader(http.StatusOK)
140 }))
141 t.Cleanup(backend.Close)
142
143 h := minimalHandler(0, &Upstream{Host: new(Host), Dial: backend.Listener.Addr().String()})
144
145 front := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
146 w.Header().Set("Strict-Transport-Security", "max-age=31536000")
147 _ = h.ServeHTTP(w, prepareTestRequest(r), caddyhttp.HandlerFunc(func(http.ResponseWriter, *http.Request) error {
148 return nil
149 }))
150 }))
151 t.Cleanup(front.Close)
152
153 var earlyHints []textproto.MIMEHeader
154 trace := &httptrace.ClientTrace{
155 Got1xxResponse: func(code int, header textproto.MIMEHeader) error {
156 if code == http.StatusEarlyHints {
157 earlyHints = append(earlyHints, header)
158 }
159 return nil
160 },
161 }
162 req, err := http.NewRequestWithContext(httptrace.WithClientTrace(context.Background(), trace), http.MethodGet, front.URL, nil)
163 if err != nil {
164 t.Fatal(err)
165 }
166 resp, err := http.DefaultClient.Do(req)
167 if err != nil {
168 t.Fatalf("request failed: %v", err)
169 }
170 resp.Body.Close()
171
172 if len(earlyHints) != 1 {
173 t.Fatalf("103 responses: got %d, want 1", len(earlyHints))
174 }
175 if got := earlyHints[0].Get("Link"); got != "</style.css>; rel=preload" {
176 t.Errorf("Link on the 103: got %q, want %q", got, "</style.css>; rel=preload")
177 }
178 if resp.StatusCode != http.StatusOK {
179 t.Fatalf("status: got %d, want %d", resp.StatusCode, http.StatusOK)
180 }
181 if got := resp.Header.Get("Strict-Transport-Security"); got != "max-age=31536000" {
182 t.Errorf("Strict-Transport-Security: got %q, want %q", got, "max-age=31536000")
183 }
184 if got := resp.Header.Get("Link"); got != "" {
185 t.Errorf("Link from the 103 leaked into the final response: %q", got)
186 }
187 }
188
189 // TestInformationalResponseStripsHopByHopHeaders verifies that hop-by-hop
190 // headers from a 1xx response are not forwarded to the client, including
191 // headers nominated by the Connection header.
192 func TestInformationalResponseStripsHopByHopHeaders(t *testing.T) {
193 hopByHop := map[string]string{
194 "Alt-Svc": `h2="attacker.example:443"; ma=86400`,
195 "Keep-Alive": "timeout=999",
196 "Proxy-Authenticate": `Basic realm="phish"`,
197 "X-Internal": "secret",
198 }
199
200 backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
201 w.Header().Set("Link", "</style.css>; rel=preload")
202 for k, v := range hopByHop {
203 w.Header().Set(k, v)
204 }
205 w.Header().Set("Connection", "X-Internal")
206 w.WriteHeader(http.StatusEarlyHints)
207 for k := range hopByHop {
208 w.Header().Del(k)
209 }
210 w.Header().Del("Link")
211 w.Header().Del("Connection")
212 w.WriteHeader(http.StatusOK)
213 }))
214 t.Cleanup(backend.Close)
215
216 h := minimalHandler(0, &Upstream{Host: new(Host), Dial: backend.Listener.Addr().String()})
217
218 front := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
219 _ = h.ServeHTTP(w, prepareTestRequest(r), caddyhttp.HandlerFunc(func(http.ResponseWriter, *http.Request) error {
220 return nil
221 }))
222 }))
223 t.Cleanup(front.Close)
224
225 var earlyHints []textproto.MIMEHeader
226 trace := &httptrace.ClientTrace{
227 Got1xxResponse: func(code int, header textproto.MIMEHeader) error {
228 if code == http.StatusEarlyHints {
229 earlyHints = append(earlyHints, header)
230 }
231 return nil
232 },
233 }
234 req, err := http.NewRequestWithContext(httptrace.WithClientTrace(context.Background(), trace), http.MethodGet, front.URL, nil)
235 if err != nil {
236 t.Fatal(err)
237 }
238 resp, err := http.DefaultClient.Do(req)
239 if err != nil {
240 t.Fatalf("request failed: %v", err)
241 }
242 resp.Body.Close()
243
244 if len(earlyHints) != 1 {
245 t.Fatalf("103 responses: got %d, want 1", len(earlyHints))
246 }
247 if got := earlyHints[0].Get("Link"); got != "</style.css>; rel=preload" {
248 t.Errorf("Link on the 103: got %q, want %q", got, "</style.css>; rel=preload")
249 }
250 for k := range hopByHop {
251 if got := earlyHints[0].Get(k); got != "" {
252 t.Errorf("hop-by-hop header %s on the 103: got %q, want it stripped", k, got)
253 }
254 }
255 if got := earlyHints[0].Get("Connection"); got != "" {
256 t.Errorf("Connection on the 103: got %q, want it stripped", got)
257 }
258 if resp.StatusCode != http.StatusOK {
259 t.Fatalf("status: got %d, want %d", resp.StatusCode, http.StatusOK)
260 }
261 }