mirror of
https://github.com/caddyserver/caddy.git
synced 2025-04-19 10:49:17 +08:00

Some checks failed
Tests / test (./cmd/caddy/caddy, ~1.24.1, macos-14, 0, 1.24, mac) (push) Has been cancelled
Tests / test (./cmd/caddy/caddy, ~1.24.1, ubuntu-latest, 0, 1.24, linux) (push) Has been cancelled
Tests / test (./cmd/caddy/caddy.exe, ~1.24.1, windows-latest, True, 1.24, windows) (push) Has been cancelled
Lint / lint (macos-14, mac) (push) Has been cancelled
Lint / lint (ubuntu-latest, linux) (push) Has been cancelled
Lint / lint (windows-latest, windows) (push) Has been cancelled
Lint / govulncheck (push) Has been cancelled
Tests / test (s390x on IBM Z) (push) Has been cancelled
Tests / goreleaser-check (push) Has been cancelled
Cross-Build / build (~1.24.1, 1.24, aix) (push) Has been cancelled
Cross-Build / build (~1.24.1, 1.24, darwin) (push) Has been cancelled
Cross-Build / build (~1.24.1, 1.24, dragonfly) (push) Has been cancelled
Cross-Build / build (~1.24.1, 1.24, freebsd) (push) Has been cancelled
Cross-Build / build (~1.24.1, 1.24, illumos) (push) Has been cancelled
Cross-Build / build (~1.24.1, 1.24, linux) (push) Has been cancelled
Cross-Build / build (~1.24.1, 1.24, netbsd) (push) Has been cancelled
Cross-Build / build (~1.24.1, 1.24, openbsd) (push) Has been cancelled
Cross-Build / build (~1.24.1, 1.24, solaris) (push) Has been cancelled
Cross-Build / build (~1.24.1, 1.24, windows) (push) Has been cancelled
* 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)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|