@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 279 lines 8.75 KB
Raw
1 // Copyright 2015 Matthew Holt and The Caddy Authors
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 // http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14
15 package integration
16
17 import (
18 "fmt"
19 "io"
20 "net"
21 "net/http"
22 "net/http/fcgi"
23 "os"
24 "path/filepath"
25 "runtime"
26 "strings"
27 "testing"
28
29 "github.com/caddyserver/caddy/v2/caddytest"
30 )
31
32 // startFakePHPFPM starts a FastCGI responder that reports the SCRIPT_FILENAME it
33 // was asked to execute, so a test can assert which script Caddy selected.
34 func startFakePHPFPM(t *testing.T) string {
35 t.Helper()
36
37 ln, err := net.Listen("tcp", "127.0.0.1:0")
38 if err != nil {
39 t.Fatalf("listening: %v", err)
40 }
41 t.Cleanup(func() { ln.Close() })
42
43 go func() {
44 _ = fcgi.Serve(ln, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
45 env := fcgi.ProcessEnv(r)
46 fmt.Fprintf(w, "SCRIPT_FILENAME=%s", env["SCRIPT_FILENAME"])
47 }))
48 }()
49
50 return ln.Addr().String()
51 }
52
53 // A php_fastcgi route protected by a path matcher must not execute a script the
54 // matcher never evaluated. Path matchers judge the cleaned request path, so
55 // /admin.php/../../other.php is matched as /other.php and must not cause
56 // <root>/admin.php to be executed.
57 //
58 // `index off` is used so that php_fastcgi generates no rewrite route at all
59 // (fastcgi/caddyfile.go skips the redirect and try_files routes entirely). That
60 // rewrite is otherwise the only step that normalizes r.URL.Path before the
61 // transport reads it, so without this the test would be satisfied by the rewrite
62 // rather than by the transport, and would not cover the transport's own
63 // normalization.
64 func TestPHPFastCGIProtectedRouteIgnoresDotSegments(t *testing.T) {
65 root := t.TempDir()
66 if err := os.WriteFile(filepath.Join(root, "admin.php"), []byte("<?php // protected"), 0o644); err != nil {
67 t.Fatalf("writing admin.php: %v", err)
68 }
69
70 backend := startFakePHPFPM(t)
71
72 tester := caddytest.NewTester(t)
73 tester.InitServer(fmt.Sprintf(`{
74 skip_install_trust
75 admin localhost:2999
76 http_port 9080
77 https_port 9443
78 grace_period 1ns
79 }
80
81 localhost:9080 {
82 root * %s
83
84 handle /admin* {
85 basic_auth {
86 alice $2a$04$wqmCpxiUKZl83bZKH.Xp0uFvNQoGz3HoBH3IxBwRMrKoCRfTmc3KK
87 }
88 php_fastcgi %s {
89 index off
90 }
91 }
92
93 handle {
94 php_fastcgi %s {
95 index off
96 }
97 }
98 }
99 `, root, backend, backend), "caddyfile")
100
101 get := func(t *testing.T, uri string, basicAuth bool) (int, string) {
102 t.Helper()
103 req, err := http.NewRequest(http.MethodGet, "http://localhost:9080"+uri, nil)
104 if err != nil {
105 t.Fatalf("building request: %v", err)
106 }
107 if basicAuth {
108 req.SetBasicAuth("alice", "test-password")
109 }
110 resp, err := tester.Client.Do(req)
111 if err != nil {
112 t.Fatalf("requesting %q: %v", uri, err)
113 }
114 defer resp.Body.Close()
115 body, err := io.ReadAll(resp.Body)
116 if err != nil {
117 t.Fatalf("reading body: %v", err)
118 }
119 return resp.StatusCode, string(body)
120 }
121
122 // The gate works for a direct request...
123 if code, _ := get(t, "/admin.php", false); code != http.StatusUnauthorized {
124 t.Fatalf("GET /admin.php without credentials: expected 401, got %d", code)
125 }
126
127 // ...and valid credentials reach the protected script, which proves the route
128 // is wired up and the 401 above is the gate rather than a misconfiguration.
129 code, body := get(t, "/admin.php", true)
130 if code != http.StatusOK {
131 t.Fatalf("GET /admin.php with credentials: expected 200, got %d", code)
132 }
133 if !strings.HasSuffix(body, filepath.Join(root, "admin.php")) {
134 t.Fatalf("authenticated request should execute admin.php, got %q", body)
135 }
136
137 // The bypass: every path matcher sees /other.php, so the /admin* gate does not
138 // fire. The transport must not then select admin.php.
139 for _, payload := range []string{
140 "/admin.php/../../other.php",
141 "/admin.php/../other.php",
142 "/admin.php/..%2f..%2fother.php",
143 "/admin.php/%2e%2e/other.php",
144 } {
145 t.Run(payload, func(t *testing.T) {
146 _, body := get(t, payload, false)
147 if strings.Contains(body, "admin.php") {
148 t.Fatalf("unauthenticated request %q selected the protected script: %s", payload, body)
149 }
150 })
151 }
152 }
153
154 // The php_fastcgi directory redirect must stay on the requesting origin. Its
155 // matcher judges the cleaned request path, so /%2Fevil.com%2F..%2Fsub is matched
156 // as /sub and resolves to <root>/sub/index.php, but the Location header used to
157 // be built from the raw original path and came out as //evil.com/../sub/, which
158 // browsers treat as a scheme-relative URL to another host. The relative
159 // reference is built from the escaped original segment, so names with reserved
160 // or non-ASCII characters must still round-trip to the same directory.
161 func TestPHPFastCGIDirectoryRedirectStaysOnOrigin(t *testing.T) {
162 dirs := []string{"sub", "a b", "a#b", "50%", "a;b", "café", "x/sub"}
163 if runtime.GOOS != "windows" {
164 dirs = append(dirs, "a?b", "a:b")
165 }
166 root := t.TempDir()
167 for _, dir := range dirs {
168 if err := os.MkdirAll(filepath.Join(root, filepath.FromSlash(dir)), 0o755); err != nil {
169 t.Fatalf("creating %s: %v", dir, err)
170 }
171 if err := os.WriteFile(filepath.Join(root, filepath.FromSlash(dir), "index.php"), []byte("<?php // index"), 0o644); err != nil {
172 t.Fatalf("writing %s/index.php: %v", dir, err)
173 }
174 }
175
176 tester := caddytest.NewTester(t)
177 tester.InitServer(fmt.Sprintf(`{
178 skip_install_trust
179 admin localhost:2999
180 http_port 9080
181 https_port 9443
182 grace_period 1ns
183 }
184
185 localhost:9080 {
186 root * %s
187 php_fastcgi 127.0.0.1:9
188 }
189 `, root), "caddyfile")
190
191 tests := []struct {
192 uri string
193 want string
194 }{
195 {uri: "/sub", want: "/sub/"},
196 {uri: "/sub?a=1", want: "/sub/?a=1"},
197 {uri: "/%2Fevil.com%2F..%2Fsub", want: "/%2Fevil.com%2F..%2Fsub/"},
198 {uri: "/%5Cevil.com%2F..%2Fsub", want: "/%5Cevil.com%2F..%2Fsub/"},
199 {uri: "/a%20b", want: "/a%20b/"},
200 {uri: "/a%23b", want: "/a%23b/"},
201 {uri: "/50%25", want: "/50%25/"},
202 {uri: "/a;b", want: "/a;b/"},
203 {uri: "/caf%C3%A9", want: "/caf%C3%A9/"},
204 {uri: "/x%2Fsub", want: "/x%2Fsub/"},
205 }
206 if runtime.GOOS != "windows" {
207 tests = append(tests,
208 struct{ uri, want string }{uri: "/a%3Fb", want: "/a%3Fb/"},
209 struct{ uri, want string }{uri: "/a:b", want: "/a:b/"},
210 )
211 }
212 for _, tc := range tests {
213 resp := tester.AssertRedirect("http://localhost:9080"+tc.uri, "http://localhost:9080"+tc.want, http.StatusPermanentRedirect)
214 if resp == nil {
215 continue
216 }
217 loc := resp.Header.Get("Location")
218 // a Location beginning with // or /\ is scheme-relative to browsers
219 if strings.HasPrefix(loc, "//") || strings.HasPrefix(loc, `/\`) {
220 t.Errorf("requesting %q: Location %q is not origin-relative", tc.uri, loc)
221 }
222 for i := 0; i < len(loc); i++ {
223 if loc[i] < 0x21 || loc[i] > 0x7e {
224 t.Errorf("requesting %q: Location %q contains an unescaped byte %#x", tc.uri, loc, loc[i])
225 break
226 }
227 }
228 }
229 }
230
231 // Without a PHP front controller, php_fastcgi must leave static files and
232 // directory indexes to file_server instead of sending them to a missing
233 // index.php (https://github.com/caddyserver/caddy/issues/8367).
234 func TestPHPFastCGIWithoutIndexPHP(t *testing.T) {
235 root := t.TempDir()
236 for name, content := range map[string]string{
237 "index.html": "home",
238 "style.css": "css",
239 "about/index.html": "about",
240 } {
241 file := filepath.Join(root, filepath.FromSlash(name))
242 if err := os.MkdirAll(filepath.Dir(file), 0o755); err != nil {
243 t.Fatal(err)
244 }
245 if err := os.WriteFile(file, []byte(content), 0o644); err != nil {
246 t.Fatal(err)
247 }
248 }
249
250 backend := startFakePHPFPM(t)
251
252 tester := caddytest.NewTester(t)
253 tester.InitServer(fmt.Sprintf(`{
254 skip_install_trust
255 admin localhost:2999
256 http_port 9080
257 https_port 9443
258 grace_period 1ns
259 }
260
261 localhost:9080 {
262 root * %s
263 php_fastcgi %s
264 file_server
265 }
266 `, root, backend), "caddyfile")
267
268 tester.AssertGetResponse("http://localhost:9080/", http.StatusOK, "home")
269 tester.AssertGetResponse("http://localhost:9080/about/", http.StatusOK, "about")
270 tester.AssertGetResponse("http://localhost:9080/style.css", http.StatusOK, "css")
271 tester.AssertGetResponse("http://localhost:9080/missing", http.StatusNotFound, "")
272
273 // a front controller added later handles the paths that match no file
274 if err := os.WriteFile(filepath.Join(root, "index.php"), []byte("<?php // front controller"), 0o644); err != nil {
275 t.Fatal(err)
276 }
277 tester.AssertGetResponse("http://localhost:9080/missing", http.StatusOK, "SCRIPT_FILENAME="+filepath.Join(root, "index.php"))
278 tester.AssertGetResponse("http://localhost:9080/style.css", http.StatusOK, "css")
279 }