mirror of
				https://github.com/caddyserver/caddy.git
				synced 2025-11-01 04:22:36 +08:00 
			
		
		
		
	 5a6b2f8d1d
			
		
	
	5a6b2f8d1d
	
	
	
		
			
			* events: Refactor; move Event into core, so core can emit events Requires some slight trickery to invert dependencies. We can't have the caddy package import the caddyevents package, because caddyevents imports caddy. Interface to the rescue! Also add two new events, experimentally: started, and stopping. At the request of a sponsor. Also rename "Filesystems" to "FileSystems" to match Go convention (unrelated to events, was just bugging me when I noticed it). * Coupla bug fixes * lol whoops
		
			
				
	
	
		
			419 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			419 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // Copyright 2015 Matthew Holt and The Caddy Authors
 | |
| //
 | |
| // Licensed under the Apache License, Version 2.0 (the "License");
 | |
| // you may not use this file except in compliance with the License.
 | |
| // You may obtain a copy of the License at
 | |
| //
 | |
| //     http://www.apache.org/licenses/LICENSE-2.0
 | |
| //
 | |
| // Unless required by applicable law or agreed to in writing, software
 | |
| // distributed under the License is distributed on an "AS IS" BASIS,
 | |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | |
| // See the License for the specific language governing permissions and
 | |
| // limitations under the License.
 | |
| 
 | |
| package fileserver
 | |
| 
 | |
| import (
 | |
| 	"context"
 | |
| 	"net/http"
 | |
| 	"net/http/httptest"
 | |
| 	"net/url"
 | |
| 	"os"
 | |
| 	"runtime"
 | |
| 	"testing"
 | |
| 
 | |
| 	"github.com/caddyserver/caddy/v2"
 | |
| 	"github.com/caddyserver/caddy/v2/internal/filesystems"
 | |
| 	"github.com/caddyserver/caddy/v2/modules/caddyhttp"
 | |
| )
 | |
| 
 | |
| func TestFileMatcher(t *testing.T) {
 | |
| 	// Windows doesn't like colons in files names
 | |
| 	isWindows := runtime.GOOS == "windows"
 | |
| 	if !isWindows {
 | |
| 		filename := "with:in-name.txt"
 | |
| 		f, err := os.Create("./testdata/" + filename)
 | |
| 		if err != nil {
 | |
| 			t.Fail()
 | |
| 			return
 | |
| 		}
 | |
| 		t.Cleanup(func() {
 | |
| 			os.Remove("./testdata/" + filename)
 | |
| 		})
 | |
| 		f.WriteString(filename)
 | |
| 		f.Close()
 | |
| 	}
 | |
| 
 | |
| 	for i, tc := range []struct {
 | |
| 		path         string
 | |
| 		expectedPath string
 | |
| 		expectedType string
 | |
| 		matched      bool
 | |
| 	}{
 | |
| 		{
 | |
| 			path:         "/foo.txt",
 | |
| 			expectedPath: "/foo.txt",
 | |
| 			expectedType: "file",
 | |
| 			matched:      true,
 | |
| 		},
 | |
| 		{
 | |
| 			path:         "/foo.txt/",
 | |
| 			expectedPath: "/foo.txt",
 | |
| 			expectedType: "file",
 | |
| 			matched:      true,
 | |
| 		},
 | |
| 		{
 | |
| 			path:         "/foo.txt?a=b",
 | |
| 			expectedPath: "/foo.txt",
 | |
| 			expectedType: "file",
 | |
| 			matched:      true,
 | |
| 		},
 | |
| 		{
 | |
| 			path:         "/foodir",
 | |
| 			expectedPath: "/foodir/",
 | |
| 			expectedType: "directory",
 | |
| 			matched:      true,
 | |
| 		},
 | |
| 		{
 | |
| 			path:         "/foodir/",
 | |
| 			expectedPath: "/foodir/",
 | |
| 			expectedType: "directory",
 | |
| 			matched:      true,
 | |
| 		},
 | |
| 		{
 | |
| 			path:         "/foodir/foo.txt",
 | |
| 			expectedPath: "/foodir/foo.txt",
 | |
| 			expectedType: "file",
 | |
| 			matched:      true,
 | |
| 		},
 | |
| 		{
 | |
| 			path:    "/missingfile.php",
 | |
| 			matched: false,
 | |
| 		},
 | |
| 		{
 | |
| 			path:         "ملف.txt", // the path file name is not escaped
 | |
| 			expectedPath: "/ملف.txt",
 | |
| 			expectedType: "file",
 | |
| 			matched:      true,
 | |
| 		},
 | |
| 		{
 | |
| 			path:         url.PathEscape("ملف.txt"), // singly-escaped path
 | |
| 			expectedPath: "/ملف.txt",
 | |
| 			expectedType: "file",
 | |
| 			matched:      true,
 | |
| 		},
 | |
| 		{
 | |
| 			path:         url.PathEscape(url.PathEscape("ملف.txt")), // doubly-escaped path
 | |
| 			expectedPath: "/%D9%85%D9%84%D9%81.txt",
 | |
| 			expectedType: "file",
 | |
| 			matched:      true,
 | |
| 		},
 | |
| 		{
 | |
| 			path:         "./with:in-name.txt", // browsers send the request with the path as such
 | |
| 			expectedPath: "/with:in-name.txt",
 | |
| 			expectedType: "file",
 | |
| 			matched:      !isWindows,
 | |
| 		},
 | |
| 	} {
 | |
| 		m := &MatchFile{
 | |
| 			fsmap:    &filesystems.FileSystemMap{},
 | |
| 			Root:     "./testdata",
 | |
| 			TryFiles: []string{"{http.request.uri.path}", "{http.request.uri.path}/"},
 | |
| 		}
 | |
| 
 | |
| 		u, err := url.Parse(tc.path)
 | |
| 		if err != nil {
 | |
| 			t.Errorf("Test %d: parsing path: %v", i, err)
 | |
| 		}
 | |
| 
 | |
| 		req := &http.Request{URL: u}
 | |
| 		repl := caddyhttp.NewTestReplacer(req)
 | |
| 
 | |
| 		result, err := m.MatchWithError(req)
 | |
| 		if err != nil {
 | |
| 			t.Errorf("Test %d: unexpected error: %v", i, err)
 | |
| 		}
 | |
| 		if result != tc.matched {
 | |
| 			t.Errorf("Test %d: expected match=%t, got %t", i, tc.matched, result)
 | |
| 		}
 | |
| 
 | |
| 		rel, ok := repl.Get("http.matchers.file.relative")
 | |
| 		if !ok && result {
 | |
| 			t.Errorf("Test %d: expected replacer value", i)
 | |
| 		}
 | |
| 		if !result {
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		if rel != tc.expectedPath {
 | |
| 			t.Errorf("Test %d: actual path: %v, expected: %v", i, rel, tc.expectedPath)
 | |
| 		}
 | |
| 
 | |
| 		fileType, _ := repl.Get("http.matchers.file.type")
 | |
| 		if fileType != tc.expectedType {
 | |
| 			t.Errorf("Test %d: actual file type: %v, expected: %v", i, fileType, tc.expectedType)
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestPHPFileMatcher(t *testing.T) {
 | |
| 	for i, tc := range []struct {
 | |
| 		path         string
 | |
| 		expectedPath string
 | |
| 		expectedType string
 | |
| 		matched      bool
 | |
| 	}{
 | |
| 		{
 | |
| 			path:         "/index.php",
 | |
| 			expectedPath: "/index.php",
 | |
| 			expectedType: "file",
 | |
| 			matched:      true,
 | |
| 		},
 | |
| 		{
 | |
| 			path:         "/index.php/somewhere",
 | |
| 			expectedPath: "/index.php",
 | |
| 			expectedType: "file",
 | |
| 			matched:      true,
 | |
| 		},
 | |
| 		{
 | |
| 			path:         "/remote.php",
 | |
| 			expectedPath: "/remote.php",
 | |
| 			expectedType: "file",
 | |
| 			matched:      true,
 | |
| 		},
 | |
| 		{
 | |
| 			path:         "/remote.php/somewhere",
 | |
| 			expectedPath: "/remote.php",
 | |
| 			expectedType: "file",
 | |
| 			matched:      true,
 | |
| 		},
 | |
| 		{
 | |
| 			path:    "/missingfile.php",
 | |
| 			matched: false,
 | |
| 		},
 | |
| 		{
 | |
| 			path:         "/notphp.php.txt",
 | |
| 			expectedPath: "/notphp.php.txt",
 | |
| 			expectedType: "file",
 | |
| 			matched:      true,
 | |
| 		},
 | |
| 		{
 | |
| 			path:         "/notphp.php.txt/",
 | |
| 			expectedPath: "/notphp.php.txt",
 | |
| 			expectedType: "file",
 | |
| 			matched:      true,
 | |
| 		},
 | |
| 		{
 | |
| 			path:    "/notphp.php.txt.suffixed",
 | |
| 			matched: false,
 | |
| 		},
 | |
| 		{
 | |
| 			path:         "/foo.php.php/index.php",
 | |
| 			expectedPath: "/foo.php.php/index.php",
 | |
| 			expectedType: "file",
 | |
| 			matched:      true,
 | |
| 		},
 | |
| 		{
 | |
| 			// See https://github.com/caddyserver/caddy/issues/3623
 | |
| 			path:         "/%E2%C3",
 | |
| 			expectedPath: "/%E2%C3",
 | |
| 			expectedType: "file",
 | |
| 			matched:      false,
 | |
| 		},
 | |
| 		{
 | |
| 			path:         "/index.php?path={path}&{query}",
 | |
| 			expectedPath: "/index.php",
 | |
| 			expectedType: "file",
 | |
| 			matched:      true,
 | |
| 		},
 | |
| 	} {
 | |
| 		m := &MatchFile{
 | |
| 			fsmap:     &filesystems.FileSystemMap{},
 | |
| 			Root:      "./testdata",
 | |
| 			TryFiles:  []string{"{http.request.uri.path}", "{http.request.uri.path}/index.php"},
 | |
| 			SplitPath: []string{".php"},
 | |
| 		}
 | |
| 
 | |
| 		u, err := url.Parse(tc.path)
 | |
| 		if err != nil {
 | |
| 			t.Errorf("Test %d: parsing path: %v", i, err)
 | |
| 		}
 | |
| 
 | |
| 		req := &http.Request{URL: u}
 | |
| 		repl := caddyhttp.NewTestReplacer(req)
 | |
| 
 | |
| 		result, err := m.MatchWithError(req)
 | |
| 		if err != nil {
 | |
| 			t.Errorf("Test %d: unexpected error: %v", i, err)
 | |
| 		}
 | |
| 		if result != tc.matched {
 | |
| 			t.Errorf("Test %d: expected match=%t, got %t", i, tc.matched, result)
 | |
| 		}
 | |
| 
 | |
| 		rel, ok := repl.Get("http.matchers.file.relative")
 | |
| 		if !ok && result {
 | |
| 			t.Errorf("Test %d: expected replacer value", i)
 | |
| 		}
 | |
| 		if !result {
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		if rel != tc.expectedPath {
 | |
| 			t.Errorf("Test %d: actual path: %v, expected: %v", i, rel, tc.expectedPath)
 | |
| 		}
 | |
| 
 | |
| 		fileType, _ := repl.Get("http.matchers.file.type")
 | |
| 		if fileType != tc.expectedType {
 | |
| 			t.Errorf("Test %d: actual file type: %v, expected: %v", i, fileType, tc.expectedType)
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestFirstSplit(t *testing.T) {
 | |
| 	m := MatchFile{
 | |
| 		SplitPath: []string{".php"},
 | |
| 		fsmap:     &filesystems.FileSystemMap{},
 | |
| 	}
 | |
| 	actual, remainder := m.firstSplit("index.PHP/somewhere")
 | |
| 	expected := "index.PHP"
 | |
| 	expectedRemainder := "/somewhere"
 | |
| 	if actual != expected {
 | |
| 		t.Errorf("Expected split %s but got %s", expected, actual)
 | |
| 	}
 | |
| 	if remainder != expectedRemainder {
 | |
| 		t.Errorf("Expected remainder %s but got %s", expectedRemainder, remainder)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| var expressionTests = []struct {
 | |
| 	name              string
 | |
| 	expression        *caddyhttp.MatchExpression
 | |
| 	urlTarget         string
 | |
| 	httpMethod        string
 | |
| 	httpHeader        *http.Header
 | |
| 	wantErr           bool
 | |
| 	wantResult        bool
 | |
| 	clientCertificate []byte
 | |
| 	expectedPath      string
 | |
| }{
 | |
| 	{
 | |
| 		name: "file error no args (MatchFile)",
 | |
| 		expression: &caddyhttp.MatchExpression{
 | |
| 			Expr: `file()`,
 | |
| 		},
 | |
| 		urlTarget:  "https://example.com/foo.txt",
 | |
| 		wantResult: true,
 | |
| 	},
 | |
| 	{
 | |
| 		name: "file error bad try files (MatchFile)",
 | |
| 		expression: &caddyhttp.MatchExpression{
 | |
| 			Expr: `file({"try_file": ["bad_arg"]})`,
 | |
| 		},
 | |
| 		urlTarget: "https://example.com/foo",
 | |
| 		wantErr:   true,
 | |
| 	},
 | |
| 	{
 | |
| 		name: "file match short pattern index.php (MatchFile)",
 | |
| 		expression: &caddyhttp.MatchExpression{
 | |
| 			Expr: `file("index.php")`,
 | |
| 		},
 | |
| 		urlTarget:  "https://example.com/foo",
 | |
| 		wantResult: true,
 | |
| 	},
 | |
| 	{
 | |
| 		name: "file match short pattern foo.txt (MatchFile)",
 | |
| 		expression: &caddyhttp.MatchExpression{
 | |
| 			Expr: `file({http.request.uri.path})`,
 | |
| 		},
 | |
| 		urlTarget:  "https://example.com/foo.txt",
 | |
| 		wantResult: true,
 | |
| 	},
 | |
| 	{
 | |
| 		name: "file match index.php (MatchFile)",
 | |
| 		expression: &caddyhttp.MatchExpression{
 | |
| 			Expr: `file({"root": "./testdata", "try_files": [{http.request.uri.path}, "/index.php"]})`,
 | |
| 		},
 | |
| 		urlTarget:  "https://example.com/foo",
 | |
| 		wantResult: true,
 | |
| 	},
 | |
| 	{
 | |
| 		name: "file match long pattern foo.txt (MatchFile)",
 | |
| 		expression: &caddyhttp.MatchExpression{
 | |
| 			Expr: `file({"root": "./testdata", "try_files": [{http.request.uri.path}]})`,
 | |
| 		},
 | |
| 		urlTarget:  "https://example.com/foo.txt",
 | |
| 		wantResult: true,
 | |
| 	},
 | |
| 	{
 | |
| 		name: "file match long pattern foo.txt with concatenation (MatchFile)",
 | |
| 		expression: &caddyhttp.MatchExpression{
 | |
| 			Expr: `file({"root": ".", "try_files": ["./testdata" + {http.request.uri.path}]})`,
 | |
| 		},
 | |
| 		urlTarget:  "https://example.com/foo.txt",
 | |
| 		wantResult: true,
 | |
| 	},
 | |
| 	{
 | |
| 		name: "file not match long pattern (MatchFile)",
 | |
| 		expression: &caddyhttp.MatchExpression{
 | |
| 			Expr: `file({"root": "./testdata", "try_files": [{http.request.uri.path}]})`,
 | |
| 		},
 | |
| 		urlTarget:  "https://example.com/nopenope.txt",
 | |
| 		wantResult: false,
 | |
| 	},
 | |
| 	{
 | |
| 		name: "file match long pattern foo.txt with try_policy (MatchFile)",
 | |
| 		expression: &caddyhttp.MatchExpression{
 | |
| 			Expr: `file({"root": "./testdata", "try_policy": "largest_size", "try_files": ["foo.txt", "large.txt"]})`,
 | |
| 		},
 | |
| 		urlTarget:    "https://example.com/",
 | |
| 		wantResult:   true,
 | |
| 		expectedPath: "/large.txt",
 | |
| 	},
 | |
| }
 | |
| 
 | |
| func TestMatchExpressionMatch(t *testing.T) {
 | |
| 	for _, tst := range expressionTests {
 | |
| 		tc := tst
 | |
| 		t.Run(tc.name, func(t *testing.T) {
 | |
| 			caddyCtx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
 | |
| 			defer cancel()
 | |
| 			err := tc.expression.Provision(caddyCtx)
 | |
| 			if err != nil {
 | |
| 				if !tc.wantErr {
 | |
| 					t.Errorf("MatchExpression.Provision() error = %v, wantErr %v", err, tc.wantErr)
 | |
| 				}
 | |
| 				return
 | |
| 			}
 | |
| 
 | |
| 			req := httptest.NewRequest(tc.httpMethod, tc.urlTarget, nil)
 | |
| 			if tc.httpHeader != nil {
 | |
| 				req.Header = *tc.httpHeader
 | |
| 			}
 | |
| 			repl := caddyhttp.NewTestReplacer(req)
 | |
| 			repl.Set("http.vars.root", "./testdata")
 | |
| 			ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
 | |
| 			req = req.WithContext(ctx)
 | |
| 
 | |
| 			matches, err := tc.expression.MatchWithError(req)
 | |
| 			if err != nil {
 | |
| 				t.Errorf("MatchExpression.Match() error = %v", err)
 | |
| 				return
 | |
| 			}
 | |
| 			if matches != tc.wantResult {
 | |
| 				t.Errorf("MatchExpression.Match() expected to return '%t', for expression : '%s'", tc.wantResult, tc.expression.Expr)
 | |
| 			}
 | |
| 
 | |
| 			if tc.expectedPath != "" {
 | |
| 				path, ok := repl.Get("http.matchers.file.relative")
 | |
| 				if !ok {
 | |
| 					t.Errorf("MatchExpression.Match() expected to return path '%s', but got none", tc.expectedPath)
 | |
| 				}
 | |
| 				if path != tc.expectedPath {
 | |
| 					t.Errorf("MatchExpression.Match() expected to return path '%s', but got '%s'", tc.expectedPath, path)
 | |
| 				}
 | |
| 			}
 | |
| 		})
 | |
| 	}
 | |
| }
 |