Compare commits

...

37 Commits

Author SHA1 Message Date
a 3f1ff118f8 noot 2024-06-18 23:48:21 -05:00
a 4d40619aa4 Merge branch 'caddytest-2' of github.com:elee1766/caddy into caddytest-2 2024-06-18 23:45:57 -05:00
a 3c591ecac9 noot 2024-06-18 23:45:54 -05:00
a 73854014d9 Merge branch 'master' into caddytest-2 2024-06-18 22:48:21 -05:00
a c0d9a2383e noot 2024-06-18 22:36:02 -05:00
a 7bc7e1680e noot 2024-06-18 21:57:46 -05:00
a edf4168c8e noot 2024-06-18 21:18:38 -05:00
a 926fb82f6b noot 2024-06-18 21:18:07 -05:00
a 841fe2544d noot 2024-06-18 21:17:51 -05:00
a b19feec6dc noot 2024-06-18 21:01:15 -05:00
a 41a4320fd3 noot 2024-06-18 20:14:51 -05:00
a b491fc5d6c noot 2024-06-18 20:11:56 -05:00
a 01cb878087 noot 2024-06-18 20:08:38 -05:00
a b98c89fbb6 noot 2024-06-18 19:46:43 -05:00
a 2619271a5c noot 2024-06-18 19:44:05 -05:00
a 93a1853022 noot 2024-06-18 18:16:33 -05:00
Matthew Holt 99dcdf7e42 caddyhttp: Convert IDNs to ASCII when provisioning Host matcher 2024-06-18 14:44:05 -06:00
Jason Yuan fab6375a8b reverseproxy: add Max-Age option to sticky cookie (#6398)
* reverseproxy: add Max-Age option to sticky cookie

* Update selectionpolicies.go

Co-authored-by: Francis Lavoie <lavofr@gmail.com>

* Update selectionpolicies.go

Co-authored-by: Francis Lavoie <lavofr@gmail.com>

---------

Co-authored-by: Francis Lavoie <lavofr@gmail.com>
2024-06-15 07:50:31 -06:00
a aca4002fd8 caddyfile: Pass blocks to import for snippets (#6130)
* a

* a

* a

* a

* a

* a
2024-06-14 11:27:51 -06:00
Ririsoft 8e0d3e1ec5 logging: set file mode when the file already exist (#6391)
101d3e7 introduced a configuration option to set the log file mode.
This option was not taken into account if the file already exists,
making users having to delete their logs to have new logs created
with the right mode.
2024-06-12 15:17:46 -06:00
Omar Ramadan d85cc2ec10 logging: Customizable zap cores (#6381) 2024-06-10 09:03:24 -06:00
Will Norris 04fb9fe87f go.mod: update tscert package (#6384)
The latest tscert allows callers to provide a custom http.Transport for
calling Tailscale's local API.

Updates tailscale/caddy-tailscale#66
2024-06-10 07:28:30 -06:00
Ririsoft 0bc27e5fb1 logging: fix file mode configuration parsing (#6383)
Commit 101d3e7 introduced file mode setting,
but was missing a JSON Marshaller so that
CaddyFile can be converted to JSON safely.
2024-06-08 11:34:18 -06:00
Andreas Kohn 9be4f194e0 caddyhttp: Write header if needed in responseRecorder.WriteResponse (#6380) 2024-06-07 07:25:36 -06:00
Andreas Kohn a10117f8bd core: Split run into a public ProvisionContext and a private method (#6378)
* Split `run` into a public `BuildContext` and a private part

`BuildContext` can be used to set up a caddy context from a config, but not start any listeners
or active components: The returned context has the configured apps provisioned, but otherwise is
inert.

This is EXPERIMENTAL: Minimally it's missing documentation and the example for how this can be
used to run unit tests.

* Use the config from the context

The config passed into `BuildContext` can be nil, in which case `BuildContext` will just make one
up that works. In either case that will end up in the finished context.

* Rename `BuildContext` to `ProvisionContext` to better match the function

* Hide the `replaceAdminServer` parts

The admin server is a global thing, and in the envisioned use case for `ProvisionContext`
shouldn't actually exist. Hide this detail in a private `provisionContext` instead, and
only expose it publicly with `replaceAdminServer` set to `false`.

This should reduce foot-shooting potential further; in addition the documentation comment
now clearly spells out that the exact interface and implementation details of `ProvisionContext`
are experimental and subject to change.
2024-06-06 14:36:06 -06:00
Ririsoft 101d3e7407 logging: Customize log file permissions (#6314)
Adding a "mode" option to overwrite the default logfile permissions.
Default remains "0600" which is the one currently used by lumberjack.
2024-06-06 08:33:34 -06:00
Matthew Holt 3f1add6c9f events: Getters for event info (close #6377) 2024-06-06 07:11:28 -06:00
Mohammed Al Sahaf 5db2f81695 ci: add version key for .goreleaser.yml (#6376)
Signed-off-by: Mohammed Al Sahaf <msaa1990@gmail.com>
2024-06-06 11:33:19 +03:00
Mohammed Al Sahaf 243351b2b1 cmd: remove zealous check of Caddyfile auto-detection (#6370)
* cmd: remove zealous check of Caddyfile auto-detection

* add test case

* remove redundant check, add comment

* one more case
2024-06-05 08:57:15 -06:00
Matt Holt 198f4385d2 caddyhttp: Add test cases to corpus (#6374)
* caddyhttp: Add test case to corpus

* One more test case

* Clean up stray comment

* More tests
2024-06-04 14:23:55 -06:00
Andreas Kohn e7ecc7ede2 Make it possible to configure the DisableStorageCheck setting for certmagic (#6368)
See discussion about this setting in https://github.com/caddyserver/certmagic/issues/201
2024-06-04 07:00:15 -06:00
Mohammed Al Sahaf 7088605cc1 cmd: fix regression in auto-detect of Caddyfile (#6362)
* cmd: fix regression in auto-detect of Caddyfile

Signed-off-by: Mohammed Al Sahaf <msaa1990@gmail.com>

* fix typo

Co-authored-by: Git'Fellow <12234510+solracsf@users.noreply.github.com>

* add tests

* address review comments

---------

Signed-off-by: Mohammed Al Sahaf <msaa1990@gmail.com>
Co-authored-by: Git'Fellow <12234510+solracsf@users.noreply.github.com>
2024-06-02 11:40:56 +00:00
Mohammed Al Sahaf 15faeacb60 cmd: fix auto-detetction of .caddyfile extension (#6356)
* cmd: fix auto-detetction of .caddyfile extension

Signed-off-by: Mohammed Al Sahaf <msaa1990@gmail.com>

* move conditions around and add clarifying comment

Signed-off-by: Mohammed Al Sahaf <msaa1990@gmail.com>

* reject ambiguous config file name

Signed-off-by: Mohammed Al Sahaf <msaa1990@gmail.com>

---------

Signed-off-by: Mohammed Al Sahaf <msaa1990@gmail.com>
2024-06-02 03:49:38 +00:00
Will Norris f8a2c60297 caddyhttp: properly sanitize requests for root path (#6360)
SanitizePathJoin protects against directory traversal attacks by
checking for requests whose URL path look like they are trying to
request something other than a local file, and returns the root
directory in those cases.

The method is also careful to ensure that requests which contain a
trailing slash include a trailing slash in the returned value.  However,
for requests that contain only a slash (requests for the root path), the
IsLocal check returns early before the matching trailing slash is
re-added.

This change updates SanitizePathJoin to only perform the
filepath.IsLocal check if the cleaned request URL path is non-empty.

---

This change also updates the existing SanitizePathJoin tests to use
filepath.FromSlash rather than filepath.Join. This makes the expected
value a little easier to read, but also has the advantage of not being
processed by filepath.Clean like filepath.Join is. This means that the
exact expect value will be compared, not the result of first cleaning
it.

Fixes #6352
2024-06-02 03:40:59 +00:00
Matthew Holt 01308b4bae I'm so tired of typos 2024-06-01 20:43:35 -06:00
Matthew Holt b7280e6949 caddytls: Implement certmagic.RenewalInfoGetter
Fixes ARI errors reported here:
https://caddy.community/t/error-in-logs-with-updating-ari-after-upgrading-to-caddy-v2-8-1/24320
2024-06-01 18:02:49 -06:00
dependabot[bot] a63767d3f8 build(deps): bump golangci/golangci-lint-action from 5 to 6 (#6361)
Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 5 to 6.
- [Release notes](https://github.com/golangci/golangci-lint-action/releases)
- [Commits](https://github.com/golangci/golangci-lint-action/compare/v5...v6)

---
updated-dependencies:
- dependency-name: golangci/golangci-lint-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-02 02:26:31 +03:00
50 changed files with 2341 additions and 973 deletions
+1 -1
View File
@@ -175,7 +175,7 @@ jobs:
- name: Checkout code
uses: actions/checkout@v4
- uses: goreleaser/goreleaser-action@v5
- uses: goreleaser/goreleaser-action@v6
with:
version: latest
args: check
+1 -1
View File
@@ -47,7 +47,7 @@ jobs:
check-latest: true
- name: golangci-lint
uses: golangci/golangci-lint-action@v5
uses: golangci/golangci-lint-action@v6
with:
version: v1.55
+1 -1
View File
@@ -106,7 +106,7 @@ jobs:
run: syft version
# GoReleaser will take care of publishing those artifacts into the release
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v5
uses: goreleaser/goreleaser-action@v6
with:
version: latest
args: release --clean --timeout 60m
+2
View File
@@ -1,3 +1,5 @@
version: 2
before:
hooks:
# The build is done in this particular way to build Caddy in a designated directory named in .gitignore.
+67 -44
View File
@@ -20,6 +20,7 @@ import (
"encoding/hex"
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"io/fs"
@@ -397,6 +398,58 @@ func unsyncedDecodeAndRun(cfgJSON []byte, allowPersist bool) error {
// will want to use Run instead, which also
// updates the config's raw state.
func run(newCfg *Config, start bool) (Context, error) {
ctx, err := provisionContext(newCfg, start)
if err != nil {
return ctx, err
}
if !start {
return ctx, nil
}
// Provision any admin routers which may need to access
// some of the other apps at runtime
err = ctx.cfg.Admin.provisionAdminRouters(ctx)
if err != nil {
return ctx, err
}
// Start
err = func() error {
started := make([]string, 0, len(ctx.cfg.apps))
for name, a := range ctx.cfg.apps {
err := a.Start()
if err != nil {
// an app failed to start, so we need to stop
// all other apps that were already started
for _, otherAppName := range started {
err2 := ctx.cfg.apps[otherAppName].Stop()
if err2 != nil {
err = fmt.Errorf("%v; additionally, aborting app %s: %v",
err, otherAppName, err2)
}
}
return fmt.Errorf("%s app module: start: %v", name, err)
}
started = append(started, name)
}
return nil
}()
if err != nil {
return ctx, err
}
// now that the user's config is running, finish setting up anything else,
// such as remote admin endpoint, config loader, etc.
return ctx, finishSettingUp(ctx, ctx.cfg)
}
// provisionContext creates a new context from the given configuration and provisions
// storage and apps.
// If `newCfg` is nil a new empty configuration will be created.
// If `replaceAdminServer` is true any currently active admin server will be replaced
// with a new admin server based on the provided configuration.
func provisionContext(newCfg *Config, replaceAdminServer bool) (Context, error) {
// because we will need to roll back any state
// modifications if this function errors, we
// keep a single error value and scope all
@@ -444,7 +497,7 @@ func run(newCfg *Config, start bool) (Context, error) {
}
// start the admin endpoint (and stop any prior one)
if start {
if replaceAdminServer {
err = replaceLocalAdminServer(newCfg)
if err != nil {
return ctx, fmt.Errorf("starting caddy administration endpoint: %v", err)
@@ -491,49 +544,16 @@ func run(newCfg *Config, start bool) (Context, error) {
}
return nil
}()
if err != nil {
return ctx, err
}
return ctx, err
}
if !start {
return ctx, nil
}
// Provision any admin routers which may need to access
// some of the other apps at runtime
err = newCfg.Admin.provisionAdminRouters(ctx)
if err != nil {
return ctx, err
}
// Start
err = func() error {
started := make([]string, 0, len(newCfg.apps))
for name, a := range newCfg.apps {
err := a.Start()
if err != nil {
// an app failed to start, so we need to stop
// all other apps that were already started
for _, otherAppName := range started {
err2 := newCfg.apps[otherAppName].Stop()
if err2 != nil {
err = fmt.Errorf("%v; additionally, aborting app %s: %v",
err, otherAppName, err2)
}
}
return fmt.Errorf("%s app module: start: %v", name, err)
}
started = append(started, name)
}
return nil
}()
if err != nil {
return ctx, err
}
// now that the user's config is running, finish setting up anything else,
// such as remote admin endpoint, config loader, etc.
return ctx, finishSettingUp(ctx, newCfg)
// ProvisionContext creates a new context from the configuration and provisions storage
// and app modules.
// The function is intended for testing and advanced use cases only, typically `Run` should be
// use to ensure a fully functional caddy instance.
// EXPERIMENTAL: While this is public the interface and implementation details of this function may change.
func ProvisionContext(newCfg *Config) (Context, error) {
return provisionContext(newCfg, false)
}
// finishSettingUp should be run after all apps have successfully started.
@@ -759,7 +779,10 @@ func exitProcess(ctx context.Context, logger *zap.Logger) {
} else {
logger.Error("unclean shutdown")
}
os.Exit(exitCode)
// check if we are in test environment, and dont call exit if we are
if flag.Lookup("test.v") == nil && !strings.Contains(os.Args[0], ".test") {
os.Exit(exitCode)
}
}()
if remoteAdminServer != nil {
+65 -2
View File
@@ -364,9 +364,45 @@ func (p *parser) doImport(nesting int) error {
// set up a replacer for non-variadic args replacement
repl := makeArgsReplacer(args)
// grab all the tokens (if it exists) from within a block that follows the import
var blockTokens []Token
for currentNesting := p.Nesting(); p.NextBlock(currentNesting); {
blockTokens = append(blockTokens, p.Token())
}
// initialize with size 1
blockMapping := make(map[string][]Token, 1)
if len(blockTokens) > 0 {
// use such tokens to create a new dispenser, and then use it to parse each block
bd := NewDispenser(blockTokens)
for bd.Next() {
// see if we can grab a key
var currentMappingKey string
if bd.Val() == "{" {
return p.Err("anonymous blocks are not supported")
}
currentMappingKey = bd.Val()
currentMappingTokens := []Token{}
// read all args until end of line / {
if bd.NextArg() {
currentMappingTokens = append(currentMappingTokens, bd.Token())
for bd.NextArg() {
currentMappingTokens = append(currentMappingTokens, bd.Token())
}
// TODO(elee1766): we don't enter another mapping here because it's annoying to extract the { and } properly.
// maybe someone can do that in the future
} else {
// attempt to enter a block and add tokens to the currentMappingTokens
for mappingNesting := bd.Nesting(); bd.NextBlock(mappingNesting); {
currentMappingTokens = append(currentMappingTokens, bd.Token())
}
}
blockMapping[currentMappingKey] = currentMappingTokens
}
}
// splice out the import directive and its arguments
// (2 tokens, plus the length of args)
tokensBefore := p.tokens[:p.cursor-1-len(args)]
tokensBefore := p.tokens[:p.cursor-1-len(args)-len(blockTokens)]
tokensAfter := p.tokens[p.cursor+1:]
var importedTokens []Token
var nodes []string
@@ -495,6 +531,33 @@ func (p *parser) doImport(nesting int) error {
maybeSnippet = false
}
}
// if it is {block}, we substitute with all tokens in the block
// if it is {blocks.*}, we substitute with the tokens in the mapping for the *
var skip bool
var tokensToAdd []Token
switch {
case token.Text == "{block}":
tokensToAdd = blockTokens
case strings.HasPrefix(token.Text, "{blocks.") && strings.HasSuffix(token.Text, "}"):
// {blocks.foo.bar} will be extracted to key `foo.bar`
blockKey := strings.TrimPrefix(strings.TrimSuffix(token.Text, "}"), "{blocks.")
val, ok := blockMapping[blockKey]
if ok {
tokensToAdd = val
}
default:
skip = true
}
if !skip {
if len(tokensToAdd) == 0 {
// if there is no content in the snippet block, don't do any replacement
// this allows snippets which contained {block}/{block.*} before this change to continue functioning as normal
tokensCopy = append(tokensCopy, token)
} else {
tokensCopy = append(tokensCopy, tokensToAdd...)
}
continue
}
if maybeSnippet {
tokensCopy = append(tokensCopy, token)
@@ -516,7 +579,7 @@ func (p *parser) doImport(nesting int) error {
// splice the imported tokens in the place of the import statement
// and rewind cursor so Next() will land on first imported token
p.tokens = append(tokensBefore, append(tokensCopy, tokensAfter...)...)
p.cursor -= len(args) + 1
p.cursor -= len(args) + len(blockTokens) + 1
return nil
}
+17
View File
@@ -849,6 +849,7 @@ func parseInvoke(h Helper) (caddyhttp.MiddlewareHandler, error) {
// log <logger_name> {
// hostnames <hostnames...>
// output <writer_module> ...
// core <core_module> ...
// format <encoder_module> ...
// level <level>
// }
@@ -960,6 +961,22 @@ func parseLogHelper(h Helper, globalLogNames map[string]struct{}) ([]ConfigValue
}
cl.WriterRaw = caddyconfig.JSONModuleObject(wo, "output", moduleName, h.warnings)
case "core":
if !h.NextArg() {
return nil, h.ArgErr()
}
moduleName := h.Val()
moduleID := "caddy.logging.cores." + moduleName
unm, err := caddyfile.UnmarshalModule(h.Dispenser, moduleID)
if err != nil {
return nil, err
}
core, ok := unm.(zapcore.Core)
if !ok {
return nil, h.Errf("module %s (%T) is not a zapcore.Core", moduleID, unm)
}
cl.CoreRaw = caddyconfig.JSONModuleObject(core, "module", moduleName, h.warnings)
case "format":
if !h.NextArg() {
return nil, h.ArgErr()
+4 -2
View File
@@ -25,11 +25,12 @@ func TestLogDirectiveSyntax(t *testing.T) {
{
input: `:8080 {
log {
core mock
output file foo.log
}
}
`,
output: `{"logging":{"logs":{"default":{"exclude":["http.log.access.log0"]},"log0":{"writer":{"filename":"foo.log","output":"file"},"include":["http.log.access.log0"]}}},"apps":{"http":{"servers":{"srv0":{"listen":[":8080"],"logs":{"default_logger_name":"log0"}}}}}}`,
output: `{"logging":{"logs":{"default":{"exclude":["http.log.access.log0"]},"log0":{"writer":{"filename":"foo.log","output":"file"},"core":{"module":"mock"},"include":["http.log.access.log0"]}}},"apps":{"http":{"servers":{"srv0":{"listen":[":8080"],"logs":{"default_logger_name":"log0"}}}}}}`,
expectError: false,
},
{
@@ -53,11 +54,12 @@ func TestLogDirectiveSyntax(t *testing.T) {
{
input: `:8080 {
log name-override {
core mock
output file foo.log
}
}
`,
output: `{"logging":{"logs":{"default":{"exclude":["http.log.access.name-override"]},"name-override":{"writer":{"filename":"foo.log","output":"file"},"include":["http.log.access.name-override"]}}},"apps":{"http":{"servers":{"srv0":{"listen":[":8080"],"logs":{"default_logger_name":"name-override"}}}}}}`,
output: `{"logging":{"logs":{"default":{"exclude":["http.log.access.name-override"]},"name-override":{"writer":{"filename":"foo.log","output":"file"},"core":{"module":"mock"},"include":["http.log.access.name-override"]}}},"apps":{"http":{"servers":{"srv0":{"listen":[":8080"],"logs":{"default_logger_name":"name-override"}}}}}}`,
expectError: false,
},
} {
+151 -391
View File
@@ -1,40 +1,29 @@
package caddytest
import (
"bytes"
"context"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"io"
"io/fs"
"log"
"net"
"net/http"
"net/http/cookiejar"
"os"
"path"
"reflect"
"regexp"
"runtime"
"strconv"
"strings"
"testing"
"sync/atomic"
"time"
"github.com/aryann/difflib"
caddycmd "github.com/caddyserver/caddy/v2/cmd"
"github.com/caddyserver/caddy/v2/caddyconfig"
// plug in Caddy modules here
_ "github.com/caddyserver/caddy/v2/modules/standard"
)
// Defaults store any configuration required to make the tests run
type Defaults struct {
// Port we expect caddy to listening on
AdminPort int
// Certificates we expect to be loaded before attempting to run the tests
Certificates []string
// TestRequestTimeout is the time to wait for a http request to
@@ -45,29 +34,31 @@ type Defaults struct {
// Default testing values
var Default = Defaults{
AdminPort: 2999, // different from what a real server also running on a developer's machine might be
Certificates: []string{"/caddy.localhost.crt", "/caddy.localhost.key"},
TestRequestTimeout: 5 * time.Second,
LoadRequestTimeout: 5 * time.Second,
}
var (
matchKey = regexp.MustCompile(`(/[\w\d\.]+\.key)`)
matchCert = regexp.MustCompile(`(/[\w\d\.]+\.crt)`)
)
// Tester represents an instance of a test client.
type Tester struct {
Client *http.Client
configLoaded bool
t testing.TB
Client *http.Client
adminPort int
portOne int
portTwo int
started atomic.Bool
configLoaded bool
configFileName string
envFileName string
}
// NewTester will create a new testing client with an attached cookie jar
func NewTester(t testing.TB) *Tester {
func NewTester() (*Tester, error) {
jar, err := cookiejar.New(nil)
if err != nil {
t.Fatalf("failed to create cookiejar: %s", err)
return nil, fmt.Errorf("failed to create cookiejar: %w", err)
}
return &Tester{
@@ -77,8 +68,7 @@ func NewTester(t testing.TB) *Tester {
Timeout: Default.TestRequestTimeout,
},
configLoaded: false,
t: t,
}
}, nil
}
type configLoadError struct {
@@ -92,53 +82,73 @@ func timeElapsed(start time.Time, name string) {
log.Printf("%s took %s", name, elapsed)
}
// InitServer this will configure the server with a configurion of a specific
// type. The configType must be either "json" or the adapter type.
func (tc *Tester) InitServer(rawConfig string, configType string) {
if err := tc.initServer(rawConfig, configType); err != nil {
tc.t.Logf("failed to load config: %s", err)
tc.t.Fail()
// launch caddy will start the server
func (tc *Tester) LaunchCaddy() error {
if !tc.started.CompareAndSwap(false, true) {
return fmt.Errorf("already launched caddy with this tester")
}
if err := tc.ensureConfigRunning(rawConfig, configType); err != nil {
tc.t.Logf("failed ensuring config is running: %s", err)
tc.t.Fail()
if err := tc.startServer(); err != nil {
return fmt.Errorf("failed to start server: %w", err)
}
return nil
}
// InitServer this will configure the server with a configurion of a specific
// type. The configType must be either "json" or the adapter type.
func (tc *Tester) initServer(rawConfig string, configType string) error {
if testing.Short() {
tc.t.SkipNow()
return nil
}
err := validateTestPrerequisites(tc.t)
if err != nil {
tc.t.Skipf("skipping tests as failed integration prerequisites. %s", err)
return nil
}
tc.t.Cleanup(func() {
if tc.t.Failed() && tc.configLoaded {
res, err := http.Get(fmt.Sprintf("http://localhost:%d/config/", Default.AdminPort))
if err != nil {
tc.t.Log("unable to read the current config")
return
}
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
var out bytes.Buffer
_ = json.Indent(&out, body, "", " ")
tc.t.Logf("----------- failed with config -----------\n%s", out.String())
func (tc *Tester) CleanupCaddy() error {
// now shutdown the server, since the test is done.
defer func() {
// try to remove pthe tmp config file we created
if tc.configFileName != "" {
os.Remove(tc.configFileName)
}
})
if tc.envFileName != "" {
os.Remove(tc.envFileName)
}
}()
resp, err := http.Post(fmt.Sprintf("http://localhost:%d/stop", tc.adminPort), "", nil)
if err != nil {
return fmt.Errorf("couldn't stop caddytest server: %w", err)
}
resp.Body.Close()
for retries := 0; retries < 10; retries++ {
if tc.isCaddyAdminRunning() != nil {
return nil
}
time.Sleep(100 * time.Millisecond)
}
rawConfig = prependCaddyFilePath(rawConfig)
return fmt.Errorf("timed out waiting for caddytest server to stop")
}
func (tc *Tester) AdminPort() int {
return tc.adminPort
}
func (tc *Tester) PortOne() int {
return tc.portOne
}
func (tc *Tester) PortTwo() int {
return tc.portTwo
}
func (tc *Tester) ReplaceTestingPlaceholders(x string) string {
x = strings.ReplaceAll(x, "{$TESTING_CADDY_ADMIN_BIND}", fmt.Sprintf("localhost:%d", tc.adminPort))
x = strings.ReplaceAll(x, "{$TESTING_CADDY_ADMIN_PORT}", fmt.Sprintf("%d", tc.adminPort))
x = strings.ReplaceAll(x, "{$TESTING_CADDY_PORT_ONE}", fmt.Sprintf("%d", tc.portOne))
x = strings.ReplaceAll(x, "{$TESTING_CADDY_PORT_TWO}", fmt.Sprintf("%d", tc.portTwo))
return x
}
// LoadConfig loads the config to the tester server and also ensures that the config was loaded
// it should not be run
func (tc *Tester) LoadConfig(rawConfig string, configType string) error {
if tc.adminPort == 0 {
return fmt.Errorf("load config called where startServer didnt succeed")
}
rawConfig = tc.ReplaceTestingPlaceholders(rawConfig)
// replace special testing placeholders so we can have our admin api be on a random port
// normalize JSON config
if configType == "json" {
tc.t.Logf("Before: %s", rawConfig)
var conf any
if err := json.Unmarshal([]byte(rawConfig), &conf); err != nil {
return err
@@ -148,16 +158,14 @@ func (tc *Tester) initServer(rawConfig string, configType string) error {
return err
}
rawConfig = string(c)
tc.t.Logf("After: %s", rawConfig)
}
client := &http.Client{
Timeout: Default.LoadRequestTimeout,
}
start := time.Now()
req, err := http.NewRequest("POST", fmt.Sprintf("http://localhost:%d/load", Default.AdminPort), strings.NewReader(rawConfig))
req, err := http.NewRequest("POST", fmt.Sprintf("http://localhost:%d/load", tc.adminPort), strings.NewReader(rawConfig))
if err != nil {
tc.t.Errorf("failed to create request. %s", err)
return err
return fmt.Errorf("failed to create request. %w", err)
}
if configType == "json" {
@@ -168,16 +176,14 @@ func (tc *Tester) initServer(rawConfig string, configType string) error {
res, err := client.Do(req)
if err != nil {
tc.t.Errorf("unable to contact caddy server. %s", err)
return err
return fmt.Errorf("unable to contact caddy server. %w", err)
}
timeElapsed(start, "caddytest: config load time")
defer res.Body.Close()
body, err := io.ReadAll(res.Body)
if err != nil {
tc.t.Errorf("unable to read response. %s", err)
return err
return fmt.Errorf("unable to read response. %w", err)
}
if res.StatusCode != 200 {
@@ -185,133 +191,115 @@ func (tc *Tester) initServer(rawConfig string, configType string) error {
}
tc.configLoaded = true
// if the config is not loaded at this point, it is a bug in caddy's config.Load
// the contract for config.Load states that the config must be loaded before it returns, and that it will
// error if the config fails to apply
return nil
}
func (tc *Tester) ensureConfigRunning(rawConfig string, configType string) error {
expectedBytes := []byte(prependCaddyFilePath(rawConfig))
if configType != "json" {
adapter := caddyconfig.GetAdapter(configType)
if adapter == nil {
return fmt.Errorf("adapter of config type is missing: %s", configType)
}
expectedBytes, _, _ = adapter.Adapt([]byte(rawConfig), nil)
}
var expected any
err := json.Unmarshal(expectedBytes, &expected)
if err != nil {
return err
}
func (tc *Tester) GetCurrentConfig(receiver any) error {
client := &http.Client{
Timeout: Default.LoadRequestTimeout,
}
fetchConfig := func(client *http.Client) any {
resp, err := client.Get(fmt.Sprintf("http://localhost:%d/config/", Default.AdminPort))
if err != nil {
return nil
}
defer resp.Body.Close()
actualBytes, err := io.ReadAll(resp.Body)
if err != nil {
return nil
}
var actual any
err = json.Unmarshal(actualBytes, &actual)
if err != nil {
return nil
}
return actual
resp, err := client.Get(fmt.Sprintf("http://localhost:%d/config/", tc.adminPort))
if err != nil {
return err
}
for retries := 10; retries > 0; retries-- {
if reflect.DeepEqual(expected, fetchConfig(client)) {
return nil
}
time.Sleep(1 * time.Second)
defer resp.Body.Close()
actualBytes, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
tc.t.Errorf("POSTed configuration isn't active")
return errors.New("EnsureConfigRunning: POSTed configuration isn't active")
err = json.Unmarshal(actualBytes, receiver)
if err != nil {
return err
}
return nil
}
const initConfig = `{
admin localhost:2999
}
`
// validateTestPrerequisites ensures the certificates are available in the
// designated path and Caddy sub-process is running.
func validateTestPrerequisites(t testing.TB) error {
// check certificates are found
for _, certName := range Default.Certificates {
if _, err := os.Stat(getIntegrationDir() + certName); errors.Is(err, fs.ErrNotExist) {
return fmt.Errorf("caddy integration test certificates (%s) not found", certName)
}
func getFreePort() (int, error) {
lr, err := net.Listen("tcp", "localhost:0")
if err != nil {
return 0, err
}
port := strings.Split(lr.Addr().String(), ":")
if len(port) < 2 {
return 0, fmt.Errorf("no port available")
}
i, err := strconv.Atoi(port[1])
if err != nil {
return 0, err
}
err = lr.Close()
if err != nil {
return 0, fmt.Errorf("failed to close listener: %w", err)
}
return i, nil
}
if isCaddyAdminRunning() != nil {
// setup the init config file, and set the cleanup afterwards
// launches caddy, and then ensures the Caddy sub-process is running.
func (tc *Tester) startServer() error {
if tc.isCaddyAdminRunning() == nil {
return fmt.Errorf("caddy test admin port still in use")
}
a, err := getFreePort()
if err != nil {
return fmt.Errorf("could not find a open port to listen on: %w", err)
}
tc.adminPort = a
tc.portOne, err = getFreePort()
if err != nil {
return fmt.Errorf("could not find a open portOne: %w", err)
}
tc.portTwo, err = getFreePort()
if err != nil {
return fmt.Errorf("could not find a open portOne: %w", err)
}
// setup the init config file, and set the cleanup afterwards
{
f, err := os.CreateTemp("", "")
if err != nil {
return err
}
t.Cleanup(func() {
os.Remove(f.Name())
})
tc.configFileName = f.Name()
initConfig := fmt.Sprintf(`{
admin localhost:%d
}`, a)
if _, err := f.WriteString(initConfig); err != nil {
return err
}
}
// start inprocess caddy server
os.Args = []string{"caddy", "run", "--config", f.Name(), "--adapter", "caddyfile"}
go func() {
caddycmd.Main()
}()
// wait for caddy to start serving the initial config
for retries := 10; retries > 0 && isCaddyAdminRunning() != nil; retries-- {
time.Sleep(1 * time.Second)
}
// start inprocess caddy server
go func() {
_ = caddycmd.MainForTesting("run", "--config", tc.configFileName, "--adapter", "caddyfile")
}()
// wait for caddy admin api to start. it should happen quickly.
for retries := 10; retries > 0 && tc.isCaddyAdminRunning() != nil; retries-- {
time.Sleep(100 * time.Millisecond)
}
// one more time to return the error
return isCaddyAdminRunning()
return tc.isCaddyAdminRunning()
}
func isCaddyAdminRunning() error {
func (tc *Tester) isCaddyAdminRunning() error {
// assert that caddy is running
client := &http.Client{
Timeout: Default.LoadRequestTimeout,
}
resp, err := client.Get(fmt.Sprintf("http://localhost:%d/config/", Default.AdminPort))
resp, err := client.Get(fmt.Sprintf("http://localhost:%d/config/", tc.adminPort))
if err != nil {
return fmt.Errorf("caddy integration test caddy server not running. Expected to be listening on localhost:%d", Default.AdminPort)
return fmt.Errorf("caddy integration test caddy server not running. Expected to be listening on localhost:%d", tc.adminPort)
}
resp.Body.Close()
return nil
}
func getIntegrationDir() string {
_, filename, _, ok := runtime.Caller(1)
if !ok {
panic("unable to determine the current file path")
}
return path.Dir(filename)
}
// use the convention to replace /[certificatename].[crt|key] with the full path
// this helps reduce the noise in test configurations and also allow this
// to run in any path
func prependCaddyFilePath(rawConfig string) string {
r := matchKey.ReplaceAllString(rawConfig, getIntegrationDir()+"$1")
r = matchCert.ReplaceAllString(r, getIntegrationDir()+"$1")
return r
}
// CreateTestingTransport creates a testing transport that forces call dialing connections to happen locally
func CreateTestingTransport() *http.Transport {
dialer := net.Dialer{
@@ -338,231 +326,3 @@ func CreateTestingTransport() *http.Transport {
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, //nolint:gosec
}
}
// AssertLoadError will load a config and expect an error
func AssertLoadError(t *testing.T, rawConfig string, configType string, expectedError string) {
tc := NewTester(t)
err := tc.initServer(rawConfig, configType)
if !strings.Contains(err.Error(), expectedError) {
t.Errorf("expected error \"%s\" but got \"%s\"", expectedError, err.Error())
}
}
// AssertRedirect makes a request and asserts the redirection happens
func (tc *Tester) AssertRedirect(requestURI string, expectedToLocation string, expectedStatusCode int) *http.Response {
redirectPolicyFunc := func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
}
// using the existing client, we override the check redirect policy for this test
old := tc.Client.CheckRedirect
tc.Client.CheckRedirect = redirectPolicyFunc
defer func() { tc.Client.CheckRedirect = old }()
resp, err := tc.Client.Get(requestURI)
if err != nil {
tc.t.Errorf("failed to call server %s", err)
return nil
}
if expectedStatusCode != resp.StatusCode {
tc.t.Errorf("requesting \"%s\" expected status code: %d but got %d", requestURI, expectedStatusCode, resp.StatusCode)
}
loc, err := resp.Location()
if err != nil {
tc.t.Errorf("requesting \"%s\" expected location: \"%s\" but got error: %s", requestURI, expectedToLocation, err)
}
if loc == nil && expectedToLocation != "" {
tc.t.Errorf("requesting \"%s\" expected a Location header, but didn't get one", requestURI)
}
if loc != nil {
if expectedToLocation != loc.String() {
tc.t.Errorf("requesting \"%s\" expected location: \"%s\" but got \"%s\"", requestURI, expectedToLocation, loc.String())
}
}
return resp
}
// CompareAdapt adapts a config and then compares it against an expected result
func CompareAdapt(t testing.TB, filename, rawConfig string, adapterName string, expectedResponse string) bool {
cfgAdapter := caddyconfig.GetAdapter(adapterName)
if cfgAdapter == nil {
t.Logf("unrecognized config adapter '%s'", adapterName)
return false
}
options := make(map[string]any)
result, warnings, err := cfgAdapter.Adapt([]byte(rawConfig), options)
if err != nil {
t.Logf("adapting config using %s adapter: %v", adapterName, err)
return false
}
// prettify results to keep tests human-manageable
var prettyBuf bytes.Buffer
err = json.Indent(&prettyBuf, result, "", "\t")
if err != nil {
return false
}
result = prettyBuf.Bytes()
if len(warnings) > 0 {
for _, w := range warnings {
t.Logf("warning: %s:%d: %s: %s", filename, w.Line, w.Directive, w.Message)
}
}
diff := difflib.Diff(
strings.Split(expectedResponse, "\n"),
strings.Split(string(result), "\n"))
// scan for failure
failed := false
for _, d := range diff {
if d.Delta != difflib.Common {
failed = true
break
}
}
if failed {
for _, d := range diff {
switch d.Delta {
case difflib.Common:
fmt.Printf(" %s\n", d.Payload)
case difflib.LeftOnly:
fmt.Printf(" - %s\n", d.Payload)
case difflib.RightOnly:
fmt.Printf(" + %s\n", d.Payload)
}
}
return false
}
return true
}
// AssertAdapt adapts a config and then tests it against an expected result
func AssertAdapt(t testing.TB, rawConfig string, adapterName string, expectedResponse string) {
ok := CompareAdapt(t, "Caddyfile", rawConfig, adapterName, expectedResponse)
if !ok {
t.Fail()
}
}
// Generic request functions
func applyHeaders(t testing.TB, req *http.Request, requestHeaders []string) {
requestContentType := ""
for _, requestHeader := range requestHeaders {
arr := strings.SplitAfterN(requestHeader, ":", 2)
k := strings.TrimRight(arr[0], ":")
v := strings.TrimSpace(arr[1])
if k == "Content-Type" {
requestContentType = v
}
t.Logf("Request header: %s => %s", k, v)
req.Header.Set(k, v)
}
if requestContentType == "" {
t.Logf("Content-Type header not provided")
}
}
// AssertResponseCode will execute the request and verify the status code, returns a response for additional assertions
func (tc *Tester) AssertResponseCode(req *http.Request, expectedStatusCode int) *http.Response {
resp, err := tc.Client.Do(req)
if err != nil {
tc.t.Fatalf("failed to call server %s", err)
}
if expectedStatusCode != resp.StatusCode {
tc.t.Errorf("requesting \"%s\" expected status code: %d but got %d", req.URL.RequestURI(), expectedStatusCode, resp.StatusCode)
}
return resp
}
// AssertResponse request a URI and assert the status code and the body contains a string
func (tc *Tester) AssertResponse(req *http.Request, expectedStatusCode int, expectedBody string) (*http.Response, string) {
resp := tc.AssertResponseCode(req, expectedStatusCode)
defer resp.Body.Close()
bytes, err := io.ReadAll(resp.Body)
if err != nil {
tc.t.Fatalf("unable to read the response body %s", err)
}
body := string(bytes)
if body != expectedBody {
tc.t.Errorf("requesting \"%s\" expected response body \"%s\" but got \"%s\"", req.RequestURI, expectedBody, body)
}
return resp, body
}
// Verb specific test functions
// AssertGetResponse GET a URI and expect a statusCode and body text
func (tc *Tester) AssertGetResponse(requestURI string, expectedStatusCode int, expectedBody string) (*http.Response, string) {
req, err := http.NewRequest("GET", requestURI, nil)
if err != nil {
tc.t.Fatalf("unable to create request %s", err)
}
return tc.AssertResponse(req, expectedStatusCode, expectedBody)
}
// AssertDeleteResponse request a URI and expect a statusCode and body text
func (tc *Tester) AssertDeleteResponse(requestURI string, expectedStatusCode int, expectedBody string) (*http.Response, string) {
req, err := http.NewRequest("DELETE", requestURI, nil)
if err != nil {
tc.t.Fatalf("unable to create request %s", err)
}
return tc.AssertResponse(req, expectedStatusCode, expectedBody)
}
// AssertPostResponseBody POST to a URI and assert the response code and body
func (tc *Tester) AssertPostResponseBody(requestURI string, requestHeaders []string, requestBody *bytes.Buffer, expectedStatusCode int, expectedBody string) (*http.Response, string) {
req, err := http.NewRequest("POST", requestURI, requestBody)
if err != nil {
tc.t.Errorf("failed to create request %s", err)
return nil, ""
}
applyHeaders(tc.t, req, requestHeaders)
return tc.AssertResponse(req, expectedStatusCode, expectedBody)
}
// AssertPutResponseBody PUT to a URI and assert the response code and body
func (tc *Tester) AssertPutResponseBody(requestURI string, requestHeaders []string, requestBody *bytes.Buffer, expectedStatusCode int, expectedBody string) (*http.Response, string) {
req, err := http.NewRequest("PUT", requestURI, requestBody)
if err != nil {
tc.t.Errorf("failed to create request %s", err)
return nil, ""
}
applyHeaders(tc.t, req, requestHeaders)
return tc.AssertResponse(req, expectedStatusCode, expectedBody)
}
// AssertPatchResponseBody PATCH to a URI and assert the response code and body
func (tc *Tester) AssertPatchResponseBody(requestURI string, requestHeaders []string, requestBody *bytes.Buffer, expectedStatusCode int, expectedBody string) (*http.Response, string) {
req, err := http.NewRequest("PATCH", requestURI, requestBody)
if err != nil {
tc.t.Errorf("failed to create request %s", err)
return nil, ""
}
applyHeaders(tc.t, req, requestHeaders)
return tc.AssertResponse(req, expectedStatusCode, expectedBody)
}
+116
View File
@@ -0,0 +1,116 @@
package caddytest
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"strings"
"testing"
"github.com/aryann/difflib"
"github.com/stretchr/testify/require"
"github.com/caddyserver/caddy/v2/caddyconfig"
)
// AssertLoadError will load a config and expect an error
func AssertLoadError(t *testing.T, rawConfig string, configType string, expectedError string) {
tc, err := NewTester()
require.NoError(t, err)
err = tc.LaunchCaddy()
require.NoError(t, err)
err = tc.LoadConfig(rawConfig, configType)
if !strings.Contains(err.Error(), expectedError) {
t.Errorf("expected error \"%s\" but got \"%s\"", expectedError, err.Error())
}
_ = tc.CleanupCaddy()
}
// CompareAdapt adapts a config and then compares it against an expected result
func CompareAdapt(t testing.TB, filename, rawConfig string, adapterName string, expectedResponse string) bool {
cfgAdapter := caddyconfig.GetAdapter(adapterName)
if cfgAdapter == nil {
t.Logf("unrecognized config adapter '%s'", adapterName)
return false
}
options := make(map[string]any)
result, warnings, err := cfgAdapter.Adapt([]byte(rawConfig), options)
if err != nil {
t.Logf("adapting config using %s adapter: %v", adapterName, err)
return false
}
// prettify results to keep tests human-manageable
var prettyBuf bytes.Buffer
err = json.Indent(&prettyBuf, result, "", "\t")
if err != nil {
return false
}
result = prettyBuf.Bytes()
if len(warnings) > 0 {
for _, w := range warnings {
t.Logf("warning: %s:%d: %s: %s", filename, w.Line, w.Directive, w.Message)
}
}
diff := difflib.Diff(
strings.Split(expectedResponse, "\n"),
strings.Split(string(result), "\n"))
// scan for failure
failed := false
for _, d := range diff {
if d.Delta != difflib.Common {
failed = true
break
}
}
if failed {
for _, d := range diff {
switch d.Delta {
case difflib.Common:
fmt.Printf(" %s\n", d.Payload)
case difflib.LeftOnly:
fmt.Printf(" - %s\n", d.Payload)
case difflib.RightOnly:
fmt.Printf(" + %s\n", d.Payload)
}
}
return false
}
return true
}
// AssertAdapt adapts a config and then tests it against an expected result
func AssertAdapt(t testing.TB, rawConfig string, adapterName string, expectedResponse string) {
ok := CompareAdapt(t, "Caddyfile", rawConfig, adapterName, expectedResponse)
if !ok {
t.Fail()
}
}
// Generic request functions
func applyHeaders(t testing.TB, req *http.Request, requestHeaders []string) {
requestContentType := ""
for _, requestHeader := range requestHeaders {
arr := strings.SplitAfterN(requestHeader, ":", 2)
k := strings.TrimRight(arr[0], ":")
v := strings.TrimSpace(arr[1])
if k == "Content-Type" {
requestContentType = v
}
t.Logf("Request header: %s => %s", k, v)
req.Header.Set(k, v)
}
if requestContentType == "" {
t.Logf("Content-Type header not provided")
}
}
+13 -12
View File
@@ -1,21 +1,22 @@
package caddytest
import (
"fmt"
"net/http"
"strings"
"testing"
)
func TestReplaceCertificatePaths(t *testing.T) {
rawConfig := `a.caddy.localhost:9443 {
rawConfig := `a.caddy.localhost:9443{
tls /caddy.localhost.crt /caddy.localhost.key {
}
redir / https://b.caddy.localhost:9443/version 301
respond /version 200 {
body "hello from a.caddy.localhost"
}
}
}`
r := prependCaddyFilePath(rawConfig)
@@ -34,8 +35,8 @@ func TestReplaceCertificatePaths(t *testing.T) {
}
func TestLoadUnorderedJSON(t *testing.T) {
tester := NewTester(t)
tester.InitServer(`
harness := StartHarness(t)
harness.LoadConfig(`
{
"logging": {
"logs": {
@@ -68,7 +69,7 @@ func TestLoadUnorderedJSON(t *testing.T) {
}
},
"admin": {
"listen": "localhost:2999"
"listen": "{$TESTING_CADDY_ADMIN_BIND}"
},
"apps": {
"pki": {
@@ -79,13 +80,13 @@ func TestLoadUnorderedJSON(t *testing.T) {
}
},
"http": {
"http_port": 9080,
"https_port": 9443,
"http_port": {$TESTING_CADDY_PORT_ONE},
"https_port": {$TESTING_CADDY_PORT_TWO},
"servers": {
"s_server": {
"listen": [
":9443",
":9080"
":{$TESTING_CADDY_PORT_ONE}",
":{$TESTING_CADDY_PORT_TWO}"
],
"routes": [
{
@@ -120,10 +121,10 @@ func TestLoadUnorderedJSON(t *testing.T) {
}
}
`, "json")
req, err := http.NewRequest(http.MethodGet, "http://localhost:9080/", nil)
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("http://localhost:%d/", harness.Tester().PortOne()), nil)
if err != nil {
t.Fail()
return
}
tester.AssertResponseCode(req, 200)
harness.AssertResponseCode(req, 200)
}
+15 -20
View File
@@ -24,19 +24,13 @@ const acmeChallengePort = 9081
// Test the basic functionality of Caddy's ACME server
func TestACMEServerWithDefaults(t *testing.T) {
ctx := context.Background()
logger, err := zap.NewDevelopment()
if err != nil {
t.Error(err)
return
}
tester := caddytest.NewTester(t)
tester.InitServer(`
harness := caddytest.StartHarness(t)
harness.LoadConfig(`
{
skip_install_trust
admin localhost:2999
http_port 9080
https_port 9443
admin {$TESTING_CADDY_ADMIN_BIND}
http_port {$TESTING_CADDY_PORT_ONE}
https_port {$TESTING_CADDY_PORT_TWO}
local_certs
}
acme.localhost {
@@ -44,10 +38,11 @@ func TestACMEServerWithDefaults(t *testing.T) {
}
`, "caddyfile")
logger := caddy.Log().Named("acmeserver")
client := acmez.Client{
Client: &acme.Client{
Directory: "https://acme.localhost:9443/acme/local/directory",
HTTPClient: tester.Client,
Directory: fmt.Sprintf("https://acme.localhost:%d/acme/local/directory", harness.Tester().PortTwo()),
HTTPClient: harness.Client(),
Logger: logger,
},
ChallengeSolvers: map[string]acmez.Solver{
@@ -97,13 +92,13 @@ func TestACMEServerWithMismatchedChallenges(t *testing.T) {
ctx := context.Background()
logger := caddy.Log().Named("acmez")
tester := caddytest.NewTester(t)
tester.InitServer(`
harness := caddytest.StartHarness(t)
harness.LoadConfig(`
{
skip_install_trust
admin localhost:2999
http_port 9080
https_port 9443
admin {$TESTING_CADDY_ADMIN_BIND}
http_port {$TESTING_CADDY_PORT_ONE}
https_port {$TESTING_CADDY_PORT_TWO}
local_certs
}
acme.localhost {
@@ -115,8 +110,8 @@ func TestACMEServerWithMismatchedChallenges(t *testing.T) {
client := acmez.Client{
Client: &acme.Client{
Directory: "https://acme.localhost:9443/acme/local/directory",
HTTPClient: tester.Client,
Directory: fmt.Sprintf("https://acme.localhost:%d/acme/local/directory", harness.Tester().PortTwo()),
HTTPClient: harness.Client(),
Logger: logger,
},
ChallengeSolvers: map[string]acmez.Solver{
+29 -36
View File
@@ -5,50 +5,51 @@ import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"fmt"
"strings"
"testing"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddytest"
"github.com/mholt/acmez/v2"
"github.com/mholt/acmez/v2/acme"
"go.uber.org/zap"
)
func TestACMEServerDirectory(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
harness := caddytest.StartHarness(t)
harness.LoadConfig(`
{
skip_install_trust
local_certs
admin localhost:2999
http_port 9080
https_port 9443
admin {$TESTING_CADDY_ADMIN_BIND}
http_port {$TESTING_CADDY_PORT_ONE}
https_port {$TESTING_CADDY_PORT_TWO}
pki {
ca local {
name "Caddy Local Authority"
}
}
}
acme.localhost:9443 {
acme.localhost:{$TESTING_CADDY_PORT_TWO} {
acme_server
}
`, "caddyfile")
tester.AssertGetResponse(
"https://acme.localhost:9443/acme/local/directory",
harness.AssertGetResponse(
fmt.Sprintf("https://acme.localhost:%d/acme/local/directory", harness.Tester().PortTwo()),
200,
`{"newNonce":"https://acme.localhost:9443/acme/local/new-nonce","newAccount":"https://acme.localhost:9443/acme/local/new-account","newOrder":"https://acme.localhost:9443/acme/local/new-order","revokeCert":"https://acme.localhost:9443/acme/local/revoke-cert","keyChange":"https://acme.localhost:9443/acme/local/key-change"}
`)
fmt.Sprintf(`{"newNonce":"https://acme.localhost:%[1]d/acme/local/new-nonce","newAccount":"https://acme.localhost:%[1]d/acme/local/new-account","newOrder":"https://acme.localhost:%[1]d/acme/local/new-order","revokeCert":"https://acme.localhost:%[1]d/acme/local/revoke-cert","keyChange":"https://acme.localhost:%[1]d/acme/local/key-change"}
`, harness.Tester().PortTwo()))
}
func TestACMEServerAllowPolicy(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
harness := caddytest.StartHarness(t)
harness.LoadConfig(`
{
skip_install_trust
local_certs
admin localhost:2999
http_port 9080
https_port 9443
admin {$TESTING_CADDY_ADMIN_BIND}
http_port {$TESTING_CADDY_PORT_ONE}
https_port {$TESTING_CADDY_PORT_TWO}
pki {
ca local {
name "Caddy Local Authority"
@@ -66,16 +67,12 @@ func TestACMEServerAllowPolicy(t *testing.T) {
`, "caddyfile")
ctx := context.Background()
logger, err := zap.NewDevelopment()
if err != nil {
t.Error(err)
return
}
logger := caddy.Log().Named("acmez")
client := acmez.Client{
Client: &acme.Client{
Directory: "https://acme.localhost:9443/acme/local/directory",
HTTPClient: tester.Client,
Directory: fmt.Sprintf("https://acme.localhost:%d/acme/local/directory", harness.Tester().PortTwo()),
HTTPClient: harness.Client(),
Logger: logger,
},
ChallengeSolvers: map[string]acmez.Solver{
@@ -131,14 +128,14 @@ func TestACMEServerAllowPolicy(t *testing.T) {
}
func TestACMEServerDenyPolicy(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
harness := caddytest.StartHarness(t)
harness.LoadConfig(`
{
skip_install_trust
local_certs
admin localhost:2999
http_port 9080
https_port 9443
admin {$TESTING_CADDY_ADMIN_BIND}
http_port {$TESTING_CADDY_PORT_ONE}
https_port {$TESTING_CADDY_PORT_TWO}
pki {
ca local {
name "Caddy Local Authority"
@@ -155,16 +152,12 @@ func TestACMEServerDenyPolicy(t *testing.T) {
`, "caddyfile")
ctx := context.Background()
logger, err := zap.NewDevelopment()
if err != nil {
t.Error(err)
return
}
logger := caddy.Log().Named("acmez")
client := acmez.Client{
Client: &acme.Client{
Directory: "https://acme.localhost:9443/acme/local/directory",
HTTPClient: tester.Client,
Directory: fmt.Sprintf("https://acme.localhost:%d/acme/local/directory", harness.Tester().PortTwo()),
HTTPClient: harness.Client(),
Logger: logger,
},
ChallengeSolvers: map[string]acmez.Solver{
@@ -197,7 +190,7 @@ func TestACMEServerDenyPolicy(t *testing.T) {
_, err := client.ObtainCertificateForSANs(ctx, account, certPrivateKey, []string{"deny.localhost"})
if err == nil {
t.Errorf("obtaining certificate for 'deny.localhost' domain")
} else if err != nil && !strings.Contains(err.Error(), "urn:ietf:params:acme:error:rejectedIdentifier") {
} else if !strings.Contains(err.Error(), "urn:ietf:params:acme:error:rejectedIdentifier") {
t.Logf("unexpected error: %v", err)
}
}
+47 -46
View File
@@ -1,6 +1,7 @@
package integration
import (
"fmt"
"net/http"
"testing"
@@ -8,69 +9,69 @@ import (
)
func TestAutoHTTPtoHTTPSRedirectsImplicitPort(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
harness := caddytest.StartHarness(t)
harness.LoadConfig(`
{
admin localhost:2999
admin {$TESTING_CADDY_ADMIN_BIND}
skip_install_trust
http_port 9080
https_port 9443
http_port {$TESTING_CADDY_PORT_ONE}
https_port {$TESTING_CADDY_PORT_TWO}
}
localhost
respond "Yahaha! You found me!"
`, "caddyfile")
tester.AssertRedirect("http://localhost:9080/", "https://localhost/", http.StatusPermanentRedirect)
harness.AssertRedirect(fmt.Sprintf("http://localhost:%d/", harness.Tester().PortOne()), "https://localhost/", http.StatusPermanentRedirect)
}
func TestAutoHTTPtoHTTPSRedirectsExplicitPortSameAsHTTPSPort(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
harness := caddytest.StartHarness(t)
harness.LoadConfig(`
{
skip_install_trust
admin localhost:2999
http_port 9080
https_port 9443
admin {$TESTING_CADDY_ADMIN_BIND}
http_port {$TESTING_CADDY_PORT_ONE}
https_port {$TESTING_CADDY_PORT_TWO}
}
localhost:9443
localhost:{$TESTING_CADDY_PORT_TWO}
respond "Yahaha! You found me!"
`, "caddyfile")
tester.AssertRedirect("http://localhost:9080/", "https://localhost/", http.StatusPermanentRedirect)
harness.AssertRedirect(fmt.Sprintf("http://localhost:%d/", harness.Tester().PortOne()), "https://localhost/", http.StatusPermanentRedirect)
}
func TestAutoHTTPtoHTTPSRedirectsExplicitPortDifferentFromHTTPSPort(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
harness := caddytest.StartHarness(t)
harness.LoadConfig(`
{
skip_install_trust
admin localhost:2999
http_port 9080
https_port 9443
admin {$TESTING_CADDY_ADMIN_BIND}
http_port {$TESTING_CADDY_PORT_ONE}
https_port {$TESTING_CADDY_PORT_TWO}
}
localhost:1234
respond "Yahaha! You found me!"
`, "caddyfile")
tester.AssertRedirect("http://localhost:9080/", "https://localhost:1234/", http.StatusPermanentRedirect)
harness.AssertRedirect(fmt.Sprintf("http://localhost:%d/", harness.Tester().PortOne()), "https://localhost:1234/", http.StatusPermanentRedirect)
}
func TestAutoHTTPRedirectsWithHTTPListenerFirstInAddresses(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
harness := caddytest.StartHarness(t)
harness.LoadConfig(`
{
"admin": {
"listen": "localhost:2999"
"listen": "{$TESTING_CADDY_ADMIN_BIND}"
},
"apps": {
"http": {
"http_port": 9080,
"https_port": 9443,
"http_port": {$TESTING_CADDY_PORT_ONE},
"https_port": {$TESTING_CADDY_PORT_TWO},
"servers": {
"ingress_server": {
"listen": [
":9080",
":9443"
":{$TESTING_CADDY_PORT_ONE}",
":{$TESTING_CADDY_PORT_TWO}"
],
"routes": [
{
@@ -94,52 +95,52 @@ func TestAutoHTTPRedirectsWithHTTPListenerFirstInAddresses(t *testing.T) {
}
}
`, "json")
tester.AssertRedirect("http://localhost:9080/", "https://localhost/", http.StatusPermanentRedirect)
harness.AssertRedirect(fmt.Sprintf("http://localhost:%d/", harness.Tester().PortOne()), "https://localhost/", http.StatusPermanentRedirect)
}
func TestAutoHTTPRedirectsInsertedBeforeUserDefinedCatchAll(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
harness := caddytest.StartHarness(t)
harness.LoadConfig(`
{
skip_install_trust
admin localhost:2999
http_port 9080
https_port 9443
admin {$TESTING_CADDY_ADMIN_BIND}
http_port {$TESTING_CADDY_PORT_ONE}
https_port {$TESTING_CADDY_PORT_TWO}
local_certs
}
http://:9080 {
http://:{$TESTING_CADDY_PORT_ONE} {
respond "Foo"
}
http://baz.localhost:9080 {
http://baz.localhost:{$TESTING_CADDY_PORT_ONE} {
respond "Baz"
}
bar.localhost {
respond "Bar"
}
`, "caddyfile")
tester.AssertRedirect("http://bar.localhost:9080/", "https://bar.localhost/", http.StatusPermanentRedirect)
tester.AssertGetResponse("http://foo.localhost:9080/", 200, "Foo")
tester.AssertGetResponse("http://baz.localhost:9080/", 200, "Baz")
harness.AssertRedirect(fmt.Sprintf("http://bar.localhost:%d/", harness.Tester().PortOne()), "https://bar.localhost/", http.StatusPermanentRedirect)
harness.AssertGetResponse(fmt.Sprintf("http://foo.localhost:%d/", harness.Tester().PortOne()), 200, "Foo")
harness.AssertGetResponse(fmt.Sprintf("http://baz.localhost:%d/", harness.Tester().PortOne()), 200, "Baz")
}
func TestAutoHTTPRedirectsInsertedBeforeUserDefinedCatchAllWithNoExplicitHTTPSite(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
harness := caddytest.StartHarness(t)
harness.LoadConfig(`
{
skip_install_trust
admin localhost:2999
http_port 9080
https_port 9443
admin {$TESTING_CADDY_ADMIN_BIND}
http_port {$TESTING_CADDY_PORT_ONE}
https_port {$TESTING_CADDY_PORT_TWO}
local_certs
}
http://:9080 {
http://:{$TESTING_CADDY_PORT_ONE} {
respond "Foo"
}
bar.localhost {
respond "Bar"
}
`, "caddyfile")
tester.AssertRedirect("http://bar.localhost:9080/", "https://bar.localhost/", http.StatusPermanentRedirect)
tester.AssertGetResponse("http://foo.localhost:9080/", 200, "Foo")
tester.AssertGetResponse("http://baz.localhost:9080/", 200, "Foo")
harness.AssertRedirect(fmt.Sprintf("http://bar.localhost:%d/", harness.Tester().PortOne()), "https://bar.localhost/", http.StatusPermanentRedirect)
harness.AssertGetResponse(fmt.Sprintf("http://foo.localhost:%d/", harness.Tester().PortOne()), 200, "Foo")
harness.AssertGetResponse(fmt.Sprintf("http://baz.localhost:%d/", harness.Tester().PortOne()), 200, "Foo")
}
@@ -0,0 +1,58 @@
(snippet) {
header {
{block}
}
}
example.com {
import snippet {
foo bar
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"example.com"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "headers",
"response": {
"set": {
"Foo": [
"bar"
]
}
}
}
]
}
]
}
],
"terminal": true
}
]
}
}
}
}
}
@@ -0,0 +1,56 @@
(snippet) {
{block}
}
example.com {
import snippet {
header foo bar
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"example.com"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "headers",
"response": {
"set": {
"Foo": [
"bar"
]
}
}
}
]
}
]
}
],
"terminal": true
}
]
}
}
}
}
}
@@ -0,0 +1,76 @@
(snippet) {
header {
{blocks.foo}
}
header {
{blocks.bar}
}
}
example.com {
import snippet {
foo {
foo a
}
bar {
bar b
}
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"example.com"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "headers",
"response": {
"set": {
"Foo": [
"a"
]
}
}
},
{
"handler": "headers",
"response": {
"set": {
"Bar": [
"b"
]
}
}
}
]
}
]
}
],
"terminal": true
}
]
}
}
}
}
}
@@ -0,0 +1,82 @@
(snippet) {
header {
{blocks.bar}
}
import sub_snippet {
bar {
{blocks.foo}
}
}
}
(sub_snippet) {
header {
{blocks.bar}
}
}
example.com {
import snippet {
foo {
foo a
}
bar {
bar b
}
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"example.com"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "headers",
"response": {
"set": {
"Bar": [
"b"
]
}
}
},
{
"handler": "headers",
"response": {
"set": {
"Foo": [
"a"
]
}
}
}
]
}
]
}
],
"terminal": true
}
]
}
}
}
}
}
+182 -162
View File
@@ -1,6 +1,7 @@
package integration
import (
"fmt"
"net/http"
"net/url"
"testing"
@@ -10,62 +11,63 @@ import (
func TestRespond(t *testing.T) {
// arrange
tester := caddytest.NewTester(t)
tester.InitServer(`
harness := caddytest.StartHarness(t)
harness.LoadConfig(`
{
admin localhost:2999
http_port 9080
https_port 9443
admin {$TESTING_CADDY_ADMIN_BIND}
http_port {$TESTING_CADDY_PORT_ONE}
https_port {$TESTING_CADDY_PORT_TWO}
grace_period 1ns
}
localhost:9080 {
localhost:{$TESTING_CADDY_PORT_ONE} {
respond /version 200 {
body "hello from localhost"
}
}
}
`, "caddyfile")
// act and assert
tester.AssertGetResponse("http://localhost:9080/version", 200, "hello from localhost")
harness.AssertGetResponse(fmt.Sprintf("http://localhost:%d/version", harness.Tester().PortOne()), 200, "hello from localhost")
}
func TestRedirect(t *testing.T) {
// arrange
tester := caddytest.NewTester(t)
tester.InitServer(`
harness := caddytest.StartHarness(t)
harness.LoadConfig(`
{
admin localhost:2999
http_port 9080
https_port 9443
admin {$TESTING_CADDY_ADMIN_BIND}
http_port {$TESTING_CADDY_PORT_ONE}
https_port {$TESTING_CADDY_PORT_TWO}
grace_period 1ns
}
localhost:9080 {
redir / http://localhost:9080/hello 301
localhost:{$TESTING_CADDY_PORT_ONE} {
redir / http://localhost:{$TESTING_CADDY_PORT_ONE}/hello 301
respond /hello 200 {
body "hello from localhost"
}
}
}
`, "caddyfile")
target := fmt.Sprintf("http://localhost:%d/", harness.Tester().PortOne())
// act and assert
tester.AssertRedirect("http://localhost:9080/", "http://localhost:9080/hello", 301)
harness.AssertRedirect(target, target+"hello", 301)
// follow redirect
tester.AssertGetResponse("http://localhost:9080/", 200, "hello from localhost")
harness.AssertGetResponse(target, 200, "hello from localhost")
}
func TestDuplicateHosts(t *testing.T) {
// act and assert
caddytest.AssertLoadError(t,
`
localhost:9080 {
localhost:{$TESTING_CADDY_PORT_ONE} {
}
localhost:9080 {
localhost:{$TESTING_CADDY_PORT_ONE} {
}
`,
"caddyfile",
@@ -80,18 +82,18 @@ func TestReadCookie(t *testing.T) {
}
// arrange
tester := caddytest.NewTester(t)
tester.Client.Jar.SetCookies(localhost, []*http.Cookie{&cookie})
tester.InitServer(`
harness := caddytest.StartHarness(t)
harness.Client().Jar.SetCookies(localhost, []*http.Cookie{&cookie})
harness.LoadConfig(`
{
skip_install_trust
admin localhost:2999
http_port 9080
https_port 9443
admin {$TESTING_CADDY_ADMIN_BIND}
http_port {$TESTING_CADDY_PORT_ONE}
https_port {$TESTING_CADDY_PORT_TWO}
grace_period 1ns
}
localhost:9080 {
localhost:{$TESTING_CADDY_PORT_ONE} {
templates {
root testdata
}
@@ -102,21 +104,22 @@ func TestReadCookie(t *testing.T) {
`, "caddyfile")
// act and assert
tester.AssertGetResponse("http://localhost:9080/cookie.html", 200, "<h2>Cookie.ClientName caddytest</h2>")
target := fmt.Sprintf("http://localhost:%d/", harness.Tester().PortOne())
harness.AssertGetResponse(target+"cookie.html", 200, "<h2>Cookie.ClientName caddytest</h2>")
}
func TestReplIndex(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
harness := caddytest.StartHarness(t)
harness.LoadConfig(`
{
skip_install_trust
admin localhost:2999
http_port 9080
https_port 9443
admin {$TESTING_CADDY_ADMIN_BIND}
http_port {$TESTING_CADDY_PORT_ONE}
https_port {$TESTING_CADDY_PORT_TWO}
grace_period 1ns
}
localhost:9080 {
localhost:{$TESTING_CADDY_PORT_ONE} {
templates {
root testdata
}
@@ -128,7 +131,8 @@ func TestReplIndex(t *testing.T) {
`, "caddyfile")
// act and assert
tester.AssertGetResponse("http://localhost:9080/", 200, "")
target := fmt.Sprintf("http://localhost:%d/", harness.Tester().PortOne())
harness.AssertGetResponse(target, 200, "")
}
func TestInvalidPrefix(t *testing.T) {
@@ -481,40 +485,42 @@ func TestValidPrefix(t *testing.T) {
}
func TestUriReplace(t *testing.T) {
tester := caddytest.NewTester(t)
harness := caddytest.StartHarness(t)
tester.InitServer(`
harness.LoadConfig(`
{
admin localhost:2999
http_port 9080
admin {$TESTING_CADDY_ADMIN_BIND}
http_port {$TESTING_CADDY_PORT_ONE}
}
:9080
:{$TESTING_CADDY_PORT_ONE}
uri replace "\}" %7D
uri replace "\{" %7B
respond "{query}"`, "caddyfile")
tester.AssertGetResponse("http://localhost:9080/endpoint?test={%20content%20}", 200, "test=%7B%20content%20%7D")
target := fmt.Sprintf("http://localhost:%d/", harness.Tester().PortOne())
harness.AssertGetResponse(target+"endpoint?test={%20content%20}", 200, "test=%7B%20content%20%7D")
}
func TestUriOps(t *testing.T) {
tester := caddytest.NewTester(t)
harness := caddytest.StartHarness(t)
tester.InitServer(`
harness.LoadConfig(`
{
admin localhost:2999
http_port 9080
admin {$TESTING_CADDY_ADMIN_BIND}
http_port {$TESTING_CADDY_PORT_ONE}
}
:9080
:{$TESTING_CADDY_PORT_ONE}
uri query +foo bar
uri query -baz
uri query taz test
uri query key=value example
uri query changethis>changed
respond "{query}"`, "caddyfile")
tester.AssertGetResponse("http://localhost:9080/endpoint?foo=bar0&baz=buz&taz=nottest&changethis=val", 200, "changed=val&foo=bar0&foo=bar&key%3Dvalue=example&taz=test")
target := fmt.Sprintf("http://localhost:%d/", harness.Tester().PortOne())
harness.AssertGetResponse(target+"endpoint?foo=bar0&baz=buz&taz=nottest&changethis=val", 200, "changed=val&foo=bar0&foo=bar&key%3Dvalue=example&taz=test")
}
// Tests the `http.request.local.port` placeholder.
@@ -523,204 +529,215 @@ func TestUriOps(t *testing.T) {
// refer to 127.0.0.1 or ::1.
// TODO: Test each http version separately (especially http/3)
func TestHttpRequestLocalPortPlaceholder(t *testing.T) {
tester := caddytest.NewTester(t)
harness := caddytest.StartHarness(t)
tester.InitServer(`
harness.LoadConfig(`
{
admin localhost:2999
http_port 9080
admin {$TESTING_CADDY_ADMIN_BIND}
http_port {$TESTING_CADDY_PORT_ONE}
}
:9080
:{$TESTING_CADDY_PORT_ONE}
respond "{http.request.local.port}"`, "caddyfile")
tester.AssertGetResponse("http://localhost:9080/", 200, "9080")
target := fmt.Sprintf("http://localhost:%d/", harness.Tester().PortOne())
harness.AssertGetResponse(target, 200, fmt.Sprintf("%d", harness.Tester().PortOne()))
}
func TestSetThenAddQueryParams(t *testing.T) {
tester := caddytest.NewTester(t)
harness := caddytest.StartHarness(t)
tester.InitServer(`
harness.LoadConfig(`
{
admin localhost:2999
http_port 9080
admin {$TESTING_CADDY_ADMIN_BIND}
http_port {$TESTING_CADDY_PORT_ONE}
}
:9080
:{$TESTING_CADDY_PORT_ONE}
uri query foo bar
uri query +foo baz
respond "{query}"`, "caddyfile")
tester.AssertGetResponse("http://localhost:9080/endpoint", 200, "foo=bar&foo=baz")
target := fmt.Sprintf("http://localhost:%d/", harness.Tester().PortOne())
harness.AssertGetResponse(target+"endpoint", 200, "foo=bar&foo=baz")
}
func TestSetThenDeleteParams(t *testing.T) {
tester := caddytest.NewTester(t)
harness := caddytest.StartHarness(t)
tester.InitServer(`
harness.LoadConfig(`
{
admin localhost:2999
http_port 9080
admin {$TESTING_CADDY_ADMIN_BIND}
http_port {$TESTING_CADDY_PORT_ONE}
}
:9080
:{$TESTING_CADDY_PORT_ONE}
uri query bar foo{query.foo}
uri query -foo
respond "{query}"`, "caddyfile")
tester.AssertGetResponse("http://localhost:9080/endpoint?foo=bar", 200, "bar=foobar")
target := fmt.Sprintf("http://localhost:%d/", harness.Tester().PortOne())
harness.AssertGetResponse(target+"endpoint?foo=bar", 200, "bar=foobar")
}
func TestRenameAndOtherOps(t *testing.T) {
tester := caddytest.NewTester(t)
harness := caddytest.StartHarness(t)
tester.InitServer(`
harness.LoadConfig(`
{
admin localhost:2999
http_port 9080
admin {$TESTING_CADDY_ADMIN_BIND}
http_port {$TESTING_CADDY_PORT_ONE}
}
:9080
:{$TESTING_CADDY_PORT_ONE}
uri query foo>bar
uri query bar taz
uri query +bar baz
respond "{query}"`, "caddyfile")
tester.AssertGetResponse("http://localhost:9080/endpoint?foo=bar", 200, "bar=taz&bar=baz")
target := fmt.Sprintf("http://localhost:%d/", harness.Tester().PortOne())
harness.AssertGetResponse(target+"endpoint?foo=bar", 200, "bar=taz&bar=baz")
}
func TestReplaceOps(t *testing.T) {
tester := caddytest.NewTester(t)
harness := caddytest.StartHarness(t)
tester.InitServer(`
harness.LoadConfig(`
{
admin localhost:2999
http_port 9080
admin {$TESTING_CADDY_ADMIN_BIND}
http_port {$TESTING_CADDY_PORT_ONE}
}
:9080
uri query foo bar baz
:{$TESTING_CADDY_PORT_ONE}
uri query foo bar baz
respond "{query}"`, "caddyfile")
tester.AssertGetResponse("http://localhost:9080/endpoint?foo=bar", 200, "foo=baz")
target := fmt.Sprintf("http://localhost:%d/", harness.Tester().PortOne())
harness.AssertGetResponse(target+"endpoint?foo=bar", 200, "foo=baz")
}
func TestReplaceWithReplacementPlaceholder(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
harness := caddytest.StartHarness(t)
harness.LoadConfig(`
{
admin localhost:2999
http_port 9080
admin {$TESTING_CADDY_ADMIN_BIND}
http_port {$TESTING_CADDY_PORT_ONE}
}
:9080
uri query foo bar {query.placeholder}
:{$TESTING_CADDY_PORT_ONE}
uri query foo bar {query.placeholder}
respond "{query}"`, "caddyfile")
tester.AssertGetResponse("http://localhost:9080/endpoint?placeholder=baz&foo=bar", 200, "foo=baz&placeholder=baz")
target := fmt.Sprintf("http://localhost:%d/", harness.Tester().PortOne())
harness.AssertGetResponse(target+"endpoint?placeholder=baz&foo=bar", 200, "foo=baz&placeholder=baz")
}
func TestReplaceWithKeyPlaceholder(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
harness := caddytest.StartHarness(t)
harness.LoadConfig(`
{
admin localhost:2999
http_port 9080
admin {$TESTING_CADDY_ADMIN_BIND}
http_port {$TESTING_CADDY_PORT_ONE}
}
:9080
uri query {query.placeholder} bar baz
:{$TESTING_CADDY_PORT_ONE}
uri query {query.placeholder} bar baz
respond "{query}"`, "caddyfile")
tester.AssertGetResponse("http://localhost:9080/endpoint?placeholder=foo&foo=bar", 200, "foo=baz&placeholder=foo")
target := fmt.Sprintf("http://localhost:%d/", harness.Tester().PortOne())
harness.AssertGetResponse(target+"endpoint?placeholder=foo&foo=bar", 200, "foo=baz&placeholder=foo")
}
func TestPartialReplacement(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
harness := caddytest.StartHarness(t)
harness.LoadConfig(`
{
admin localhost:2999
http_port 9080
admin {$TESTING_CADDY_ADMIN_BIND}
http_port {$TESTING_CADDY_PORT_ONE}
}
:9080
uri query foo ar az
:{$TESTING_CADDY_PORT_ONE}
uri query foo ar az
respond "{query}"`, "caddyfile")
tester.AssertGetResponse("http://localhost:9080/endpoint?foo=bar", 200, "foo=baz")
target := fmt.Sprintf("http://localhost:%d/", harness.Tester().PortOne())
harness.AssertGetResponse(target+"endpoint?foo=bar", 200, "foo=baz")
}
func TestNonExistingSearch(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
harness := caddytest.StartHarness(t)
harness.LoadConfig(`
{
admin localhost:2999
http_port 9080
admin {$TESTING_CADDY_ADMIN_BIND}
http_port {$TESTING_CADDY_PORT_ONE}
}
:9080
uri query foo var baz
:{$TESTING_CADDY_PORT_ONE}
uri query foo var baz
respond "{query}"`, "caddyfile")
tester.AssertGetResponse("http://localhost:9080/endpoint?foo=bar", 200, "foo=bar")
target := fmt.Sprintf("http://localhost:%d/", harness.Tester().PortOne())
harness.AssertGetResponse(target+"endpoint?foo=bar", 200, "foo=bar")
}
func TestReplaceAllOps(t *testing.T) {
tester := caddytest.NewTester(t)
harness := caddytest.StartHarness(t)
tester.InitServer(`
harness.LoadConfig(`
{
admin localhost:2999
http_port 9080
admin {$TESTING_CADDY_ADMIN_BIND}
http_port {$TESTING_CADDY_PORT_ONE}
}
:9080
uri query * bar baz
:{$TESTING_CADDY_PORT_ONE}
uri query * bar baz
respond "{query}"`, "caddyfile")
tester.AssertGetResponse("http://localhost:9080/endpoint?foo=bar&baz=bar", 200, "baz=baz&foo=baz")
target := fmt.Sprintf("http://localhost:%d/", harness.Tester().PortOne())
harness.AssertGetResponse(target+"endpoint?foo=bar&baz=bar", 200, "baz=baz&foo=baz")
}
func TestUriOpsBlock(t *testing.T) {
tester := caddytest.NewTester(t)
harness := caddytest.StartHarness(t)
tester.InitServer(`
harness.LoadConfig(`
{
admin localhost:2999
http_port 9080
admin {$TESTING_CADDY_ADMIN_BIND}
http_port {$TESTING_CADDY_PORT_ONE}
}
:9080
:{$TESTING_CADDY_PORT_ONE}
uri query {
+foo bar
-baz
taz test
}
}
respond "{query}"`, "caddyfile")
tester.AssertGetResponse("http://localhost:9080/endpoint?foo=bar0&baz=buz&taz=nottest", 200, "foo=bar0&foo=bar&taz=test")
target := fmt.Sprintf("http://localhost:%d/", harness.Tester().PortOne())
harness.AssertGetResponse(target+"endpoint?foo=bar0&baz=buz&taz=nottest", 200, "foo=bar0&foo=bar&taz=test")
}
func TestHandleErrorSimpleCodes(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`{
admin localhost:2999
http_port 9080
harness := caddytest.StartHarness(t)
harness.LoadConfig(`{
admin {$TESTING_CADDY_ADMIN_BIND}
http_port {$TESTING_CADDY_PORT_ONE}
}
localhost:9080 {
localhost:{$TESTING_CADDY_PORT_ONE} {
root * /srv
error /private* "Unauthorized" 410
error /hidden* "Not found" 404
handle_errors 404 410 {
respond "404 or 410 error"
}
}`, "caddyfile")
// act and assert
tester.AssertGetResponse("http://localhost:9080/private", 410, "404 or 410 error")
tester.AssertGetResponse("http://localhost:9080/hidden", 404, "404 or 410 error")
target := fmt.Sprintf("http://localhost:%d/", harness.Tester().PortOne())
harness.AssertGetResponse(target+"private", 410, "404 or 410 error")
harness.AssertGetResponse(target+"hidden", 404, "404 or 410 error")
}
func TestHandleErrorRange(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`{
admin localhost:2999
http_port 9080
harness := caddytest.StartHarness(t)
harness.LoadConfig(`{
admin {$TESTING_CADDY_ADMIN_BIND}
http_port {$TESTING_CADDY_PORT_ONE}
}
localhost:9080 {
localhost:{$TESTING_CADDY_PORT_ONE} {
root * /srv
error /private* "Unauthorized" 410
error /hidden* "Not found" 404
@@ -730,17 +747,18 @@ func TestHandleErrorRange(t *testing.T) {
}
}`, "caddyfile")
// act and assert
tester.AssertGetResponse("http://localhost:9080/private", 410, "Error in the [400 .. 499] range")
tester.AssertGetResponse("http://localhost:9080/hidden", 404, "Error in the [400 .. 499] range")
target := fmt.Sprintf("http://localhost:%d/", harness.Tester().PortOne())
harness.AssertGetResponse(target+"private", 410, "Error in the [400 .. 499] range")
harness.AssertGetResponse(target+"hidden", 404, "Error in the [400 .. 499] range")
}
func TestHandleErrorSort(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`{
admin localhost:2999
http_port 9080
harness := caddytest.StartHarness(t)
harness.LoadConfig(`{
admin {$TESTING_CADDY_ADMIN_BIND}
http_port {$TESTING_CADDY_PORT_ONE}
}
localhost:9080 {
localhost:{$TESTING_CADDY_PORT_ONE} {
root * /srv
error /private* "Unauthorized" 410
error /hidden* "Not found" 404
@@ -754,17 +772,18 @@ func TestHandleErrorSort(t *testing.T) {
}
}`, "caddyfile")
// act and assert
tester.AssertGetResponse("http://localhost:9080/internalerr", 500, "Fallback route: code outside the [400..499] range")
tester.AssertGetResponse("http://localhost:9080/hidden", 404, "Error in the [400 .. 499] range")
target := fmt.Sprintf("http://localhost:%d/", harness.Tester().PortOne())
harness.AssertGetResponse(target+"internalerr", 500, "Fallback route: code outside the [400..499] range")
harness.AssertGetResponse(target+"hidden", 404, "Error in the [400 .. 499] range")
}
func TestHandleErrorRangeAndCodes(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`{
admin localhost:2999
http_port 9080
harness := caddytest.StartHarness(t)
harness.LoadConfig(`{
admin {$TESTING_CADDY_ADMIN_BIND}
http_port {$TESTING_CADDY_PORT_ONE}
}
localhost:9080 {
localhost:{$TESTING_CADDY_PORT_ONE} {
root * /srv
error /private* "Unauthorized" 410
error /threehundred* "Moved Permanently" 301
@@ -778,9 +797,10 @@ func TestHandleErrorRangeAndCodes(t *testing.T) {
}
}`, "caddyfile")
// act and assert
tester.AssertGetResponse("http://localhost:9080/internalerr", 500, "Error code is equal to 500 or in the [300..399] range")
tester.AssertGetResponse("http://localhost:9080/threehundred", 301, "Error code is equal to 500 or in the [300..399] range")
tester.AssertGetResponse("http://localhost:9080/private", 410, "Error in the [400 .. 499] range")
target := fmt.Sprintf("http://localhost:%d/", harness.Tester().PortOne())
harness.AssertGetResponse(target+"internalerr", 500, "Error code is equal to 500 or in the [300..399] range")
harness.AssertGetResponse(target+"threehundred", 301, "Error code is equal to 500 or in the [300..399] range")
harness.AssertGetResponse(target+"private", 410, "Error in the [400 .. 499] range")
}
func TestInvalidSiteAddressesAsDirectives(t *testing.T) {
+15 -14
View File
@@ -2,6 +2,7 @@ package integration
import (
"bytes"
"fmt"
"net/http"
"testing"
@@ -9,36 +10,36 @@ import (
)
func TestBrowse(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
harness := caddytest.StartHarness(t)
harness.LoadConfig(`
{
skip_install_trust
admin localhost:2999
http_port 9080
https_port 9443
admin {$TESTING_CADDY_ADMIN_BIND}
http_port {$TESTING_CADDY_PORT_ONE}
https_port {$TESTING_CADDY_PORT_TWO}
grace_period 1ns
}
http://localhost:9080 {
http://localhost:{$TESTING_CADDY_PORT_ONE} {
file_server browse
}
`, "caddyfile")
req, err := http.NewRequest(http.MethodGet, "http://localhost:9080/", nil)
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("http://localhost:%d/", harness.Tester().PortOne()), nil)
if err != nil {
t.Fail()
return
}
tester.AssertResponseCode(req, 200)
harness.AssertResponseCode(req, 200)
}
func TestRespondWithJSON(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
harness := caddytest.StartHarness(t)
harness.LoadConfig(`
{
skip_install_trust
admin localhost:2999
http_port 9080
https_port 9443
admin {$TESTING_CADDY_ADMIN_BIND}
http_port {$TESTING_CADDY_PORT_ONE}
https_port {$TESTING_CADDY_PORT_TWO}
grace_period 1ns
}
localhost {
@@ -46,7 +47,7 @@ func TestRespondWithJSON(t *testing.T) {
}
`, "caddyfile")
res, _ := tester.AssertPostResponseBody("https://localhost:9443/",
res, _ := harness.AssertPostResponseBody(fmt.Sprintf("https://localhost:%d/", harness.Tester().PortTwo()),
nil,
bytes.NewBufferString(`{
"greeting": "Hello, world!"
+11 -10
View File
@@ -1,22 +1,23 @@
package integration
import (
"fmt"
"testing"
"github.com/caddyserver/caddy/v2/caddytest"
)
func TestIntercept(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`{
harness := caddytest.StartHarness(t)
harness.LoadConfig(`{
skip_install_trust
admin localhost:2999
http_port 9080
https_port 9443
admin {$TESTING_CADDY_ADMIN_BIND}
http_port {$TESTING_CADDY_PORT_ONE}
https_port {$TESTING_CADDY_PORT_TWO}
grace_period 1ns
}
localhost:9080 {
localhost:{$TESTING_CADDY_PORT_ONE} {
respond /intercept "I'm a teapot" 408
respond /no-intercept "I'm not a teapot"
@@ -25,10 +26,10 @@ func TestIntercept(t *testing.T) {
handle_response @teapot {
respond /intercept "I'm a combined coffee/tea pot that is temporarily out of coffee" 503
}
}
}
}
`, "caddyfile")
tester.AssertGetResponse("http://localhost:9080/intercept", 503, "I'm a combined coffee/tea pot that is temporarily out of coffee")
tester.AssertGetResponse("http://localhost:9080/no-intercept", 200, "I'm not a teapot")
harness.AssertGetResponse(fmt.Sprintf("http://localhost:%d/intercept", harness.Tester().PortOne()), 503, "I'm a combined coffee/tea pot that is temporarily out of coffee")
harness.AssertGetResponse(fmt.Sprintf("http://localhost:%d/no-intercept", harness.Tester().PortOne()), 200, "I'm not a teapot")
}
@@ -7,21 +7,21 @@ import (
)
func TestLeafCertLoaders(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
harness := caddytest.StartHarness(t)
harness.LoadConfig(`
{
"admin": {
"listen": "localhost:2999"
"listen": "{$TESTING_CADDY_ADMIN_BIND}"
},
"apps": {
"http": {
"http_port": 9080,
"https_port": 9443,
"http_port": {$TESTING_CADDY_PORT_ONE},
"https_port": {$TESTING_CADDY_PORT_TWO},
"grace_period": 1,
"servers": {
"srv0": {
"listen": [
":9443"
":{$TESTING_CADDY_PORT_TWO}"
],
"routes": [
{
+13 -13
View File
@@ -12,7 +12,7 @@ import (
"github.com/caddyserver/caddy/v2/caddytest"
)
func setupListenerWrapperTest(t *testing.T, handlerFunc http.HandlerFunc) *caddytest.Tester {
func setupListenerWrapperTest(t *testing.T, handlerFunc http.HandlerFunc) *caddytest.TestHarness {
l, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("failed to listen: %s", err)
@@ -28,15 +28,15 @@ func setupListenerWrapperTest(t *testing.T, handlerFunc http.HandlerFunc) *caddy
_ = srv.Close()
_ = l.Close()
})
tester := caddytest.NewTester(t)
tester.InitServer(fmt.Sprintf(`
harness := caddytest.StartHarness(t)
harness.LoadConfig(fmt.Sprintf(`
{
skip_install_trust
admin localhost:2999
http_port 9080
https_port 9443
admin {$TESTING_CADDY_ADMIN_BIND}
http_port {$TESTING_CADDY_PORT_ONE}
https_port {$TESTING_CADDY_PORT_TWO}
local_certs
servers :9443 {
servers :{$TESTING_CADDY_PORT_TWO} {
listener_wrappers {
http_redirect
tls
@@ -47,7 +47,7 @@ func setupListenerWrapperTest(t *testing.T, handlerFunc http.HandlerFunc) *caddy
reverse_proxy %s
}
`, l.Addr().String()), "caddyfile")
return tester
return harness
}
func TestHTTPRedirectWrapperWithLargeUpload(t *testing.T) {
@@ -56,7 +56,7 @@ func TestHTTPRedirectWrapperWithLargeUpload(t *testing.T) {
body := make([]byte, uploadSize)
rand.New(rand.NewSource(0)).Read(body)
tester := setupListenerWrapperTest(t, func(writer http.ResponseWriter, request *http.Request) {
harness := setupListenerWrapperTest(t, func(writer http.ResponseWriter, request *http.Request) {
buf := new(bytes.Buffer)
_, err := buf.ReadFrom(request.Body)
if err != nil {
@@ -69,7 +69,7 @@ func TestHTTPRedirectWrapperWithLargeUpload(t *testing.T) {
writer.WriteHeader(http.StatusNoContent)
})
resp, err := tester.Client.Post("https://localhost:9443", "application/octet-stream", bytes.NewReader(body))
resp, err := harness.Client().Post(fmt.Sprintf("https://localhost:%d", harness.Tester().PortTwo()), "application/octet-stream", bytes.NewReader(body))
if err != nil {
t.Fatalf("failed to post: %s", err)
}
@@ -80,14 +80,14 @@ func TestHTTPRedirectWrapperWithLargeUpload(t *testing.T) {
}
func TestLargeHttpRequest(t *testing.T) {
tester := setupListenerWrapperTest(t, func(writer http.ResponseWriter, request *http.Request) {
harness := setupListenerWrapperTest(t, func(writer http.ResponseWriter, request *http.Request) {
t.Fatal("not supposed to handle a request")
})
// We never read the body in any way, set an extra long header instead.
req, _ := http.NewRequest("POST", "http://localhost:9443", nil)
req, _ := http.NewRequest("POST", fmt.Sprintf("http://localhost:%d", harness.Tester().PortTwo()), nil)
req.Header.Set("Long-Header", strings.Repeat("X", 1024*1024))
_, err := tester.Client.Do(req)
_, err := harness.Client().Do(req)
if err == nil {
t.Fatal("not supposed to succeed")
}
+31 -30
View File
@@ -2,6 +2,7 @@ package integration
import (
"bytes"
"fmt"
"testing"
"github.com/caddyserver/caddy/v2/caddytest"
@@ -9,16 +10,16 @@ import (
func TestMap(t *testing.T) {
// arrange
tester := caddytest.NewTester(t)
tester.InitServer(`{
harness := caddytest.StartHarness(t)
harness.LoadConfig(`{
skip_install_trust
admin localhost:2999
http_port 9080
https_port 9443
admin {$TESTING_CADDY_ADMIN_BIND}
http_port {$TESTING_CADDY_PORT_ONE}
https_port {$TESTING_CADDY_PORT_TWO}
grace_period 1ns
}
localhost:9080 {
localhost:{$TESTING_CADDY_PORT_ONE} {
map {http.request.method} {dest-1} {dest-2} {
default unknown1 unknown2
@@ -28,50 +29,50 @@ func TestMap(t *testing.T) {
respond /version 200 {
body "hello from localhost {dest-1} {dest-2}"
}
}
}
`, "caddyfile")
// act and assert
tester.AssertGetResponse("http://localhost:9080/version", 200, "hello from localhost GET-called unknown2")
tester.AssertPostResponseBody("http://localhost:9080/version", []string{}, bytes.NewBuffer([]byte{}), 200, "hello from localhost post-called foobar")
harness.AssertGetResponse(fmt.Sprintf("http://localhost:%d/version", harness.Tester().PortOne()), 200, "hello from localhost GET-called unknown2")
harness.AssertPostResponseBody(fmt.Sprintf("http://localhost:%d/version", harness.Tester().PortOne()), []string{}, bytes.NewBuffer([]byte{}), 200, "hello from localhost post-called foobar")
}
func TestMapRespondWithDefault(t *testing.T) {
// arrange
tester := caddytest.NewTester(t)
tester.InitServer(`{
harness := caddytest.StartHarness(t)
harness.LoadConfig(`{
skip_install_trust
admin localhost:2999
http_port 9080
https_port 9443
admin {$TESTING_CADDY_ADMIN_BIND}
http_port {$TESTING_CADDY_PORT_ONE}
https_port {$TESTING_CADDY_PORT_TWO}
}
localhost:9080 {
localhost:{$TESTING_CADDY_PORT_ONE} {
map {http.request.method} {dest-name} {
default unknown
GET get-called
}
respond /version 200 {
body "hello from localhost {dest-name}"
}
}
}
`, "caddyfile")
// act and assert
tester.AssertGetResponse("http://localhost:9080/version", 200, "hello from localhost get-called")
tester.AssertPostResponseBody("http://localhost:9080/version", []string{}, bytes.NewBuffer([]byte{}), 200, "hello from localhost unknown")
harness.AssertGetResponse(fmt.Sprintf("http://localhost:%d/version", harness.Tester().PortOne()), 200, "hello from localhost get-called")
harness.AssertPostResponseBody(fmt.Sprintf("http://localhost:%d/version", harness.Tester().PortOne()), []string{}, bytes.NewBuffer([]byte{}), 200, "hello from localhost unknown")
}
func TestMapAsJSON(t *testing.T) {
// arrange
tester := caddytest.NewTester(t)
tester.InitServer(`
harness := caddytest.StartHarness(t)
harness.LoadConfig(`
{
"admin": {
"listen": "localhost:2999"
"listen": "{$TESTING_CADDY_ADMIN_BIND}"
},
"apps": {
"pki": {
@@ -82,12 +83,12 @@ func TestMapAsJSON(t *testing.T) {
}
},
"http": {
"http_port": 9080,
"https_port": 9443,
"http_port": {$TESTING_CADDY_PORT_ONE},
"https_port": {$TESTING_CADDY_PORT_TWO},
"servers": {
"srv0": {
"listen": [
":9080"
":{$TESTING_CADDY_PORT_ONE}"
],
"routes": [
{
@@ -145,7 +146,7 @@ func TestMapAsJSON(t *testing.T) {
}
}
}`, "json")
tester.AssertGetResponse("http://localhost:9080/version", 200, "hello from localhost get-called")
tester.AssertPostResponseBody("http://localhost:9080/version", []string{}, bytes.NewBuffer([]byte{}), 200, "hello from localhost post-called")
target := fmt.Sprintf("http://localhost:%d/version", harness.Tester().PortOne())
harness.AssertGetResponse(target, 200, "hello from localhost get-called")
harness.AssertPostResponseBody(target, []string{}, bytes.NewBuffer([]byte{}), 200, "hello from localhost post-called")
}
+47 -45
View File
@@ -14,11 +14,11 @@ import (
)
func TestSRVReverseProxy(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
harness := caddytest.StartHarness(t)
harness.LoadConfig(`
{
"admin": {
"listen": "localhost:2999"
"listen": "{$TESTING_CADDY_ADMIN_BIND}"
},
"apps": {
"pki": {
@@ -87,11 +87,11 @@ func TestDialWithPlaceholderUnix(t *testing.T) {
})
runtime.Gosched() // Allow other goroutines to run
tester := caddytest.NewTester(t)
tester.InitServer(`
harness := caddytest.StartHarness(t)
harness.LoadConfig(`
{
"admin": {
"listen": "localhost:2999"
"listen": "{$TESTING_CADDY_ADMIN_BIND}"
},
"apps": {
"pki": {
@@ -135,15 +135,15 @@ func TestDialWithPlaceholderUnix(t *testing.T) {
return
}
req.Header.Set("X-Caddy-Upstream-Dial", socketName)
tester.AssertResponse(req, 200, "Hello, World!")
harness.AssertResponse(req, 200, "Hello, World!")
}
func TestReverseProxyWithPlaceholderDialAddress(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
harness := caddytest.StartHarness(t)
harness.LoadConfig(`
{
"admin": {
"listen": "localhost:2999"
"listen": "{$TESTING_CADDY_ADMIN_BIND}"
},
"apps": {
"pki": {
@@ -186,7 +186,7 @@ func TestReverseProxyWithPlaceholderDialAddress(t *testing.T) {
},
"srv1": {
"listen": [
":9080"
":{$TESTING_CADDY_PORT_ONE}"
],
"routes": [
{
@@ -199,7 +199,7 @@ func TestReverseProxyWithPlaceholderDialAddress(t *testing.T) {
],
"handle": [
{
"handler": "reverse_proxy",
"upstreams": [
{
@@ -223,21 +223,21 @@ func TestReverseProxyWithPlaceholderDialAddress(t *testing.T) {
}
`, "json")
req, err := http.NewRequest(http.MethodGet, "http://localhost:9080", nil)
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("http://localhost:%d", harness.Tester().PortOne()), nil)
if err != nil {
t.Fail()
return
}
req.Header.Set("X-Caddy-Upstream-Dial", "localhost:18080")
tester.AssertResponse(req, 200, "Hello, World!")
harness.AssertResponse(req, 200, "Hello, World!")
}
func TestReverseProxyWithPlaceholderTCPDialAddress(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
harness := caddytest.StartHarness(t)
harness.LoadConfig(`
{
"admin": {
"listen": "localhost:2999"
"listen": "{$TESTING_CADDY_ADMIN_BIND}"
},
"apps": {
"pki": {
@@ -280,7 +280,7 @@ func TestReverseProxyWithPlaceholderTCPDialAddress(t *testing.T) {
},
"srv1": {
"listen": [
":9080"
":{$TESTING_CADDY_PORT_ONE}"
],
"routes": [
{
@@ -293,7 +293,7 @@ func TestReverseProxyWithPlaceholderTCPDialAddress(t *testing.T) {
],
"handle": [
{
"handler": "reverse_proxy",
"upstreams": [
{
@@ -317,23 +317,23 @@ func TestReverseProxyWithPlaceholderTCPDialAddress(t *testing.T) {
}
`, "json")
req, err := http.NewRequest(http.MethodGet, "http://localhost:9080", nil)
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("http://localhost:%d", harness.Tester().PortOne()), nil)
if err != nil {
t.Fail()
return
}
req.Header.Set("X-Caddy-Upstream-Dial", "localhost")
tester.AssertResponse(req, 200, "Hello, World!")
harness.AssertResponse(req, 200, "Hello, World!")
}
func TestReverseProxyHealthCheck(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
harness := caddytest.StartHarness(t)
harness.LoadConfig(`
{
skip_install_trust
admin localhost:2999
http_port 9080
https_port 9443
admin {$TESTING_CADDY_ADMIN_BIND}
http_port {$TESTING_CADDY_PORT_ONE}
https_port {$TESTING_CADDY_PORT_TWO}
grace_period 1ns
}
http://localhost:2020 {
@@ -342,10 +342,10 @@ func TestReverseProxyHealthCheck(t *testing.T) {
http://localhost:2021 {
respond "ok"
}
http://localhost:9080 {
http://localhost:{$TESTING_CADDY_PORT_ONE} {
reverse_proxy {
to localhost:2020
health_uri /health
health_port 2021
health_interval 10ms
@@ -357,14 +357,15 @@ func TestReverseProxyHealthCheck(t *testing.T) {
`, "caddyfile")
time.Sleep(100 * time.Millisecond) // TODO: for some reason this test seems particularly flaky, getting 503 when it should be 200, unless we wait
tester.AssertGetResponse("http://localhost:9080/", 200, "Hello, World!")
target := fmt.Sprintf("http://localhost:%d/", harness.Tester().PortOne())
harness.AssertGetResponse(target, 200, "Hello, World!")
}
func TestReverseProxyHealthCheckUnixSocket(t *testing.T) {
if runtime.GOOS == "windows" {
t.SkipNow()
}
tester := caddytest.NewTester(t)
harness := caddytest.StartHarness(t)
f, err := os.CreateTemp("", "*.sock")
if err != nil {
t.Errorf("failed to create TempFile: %s", err)
@@ -395,18 +396,18 @@ func TestReverseProxyHealthCheckUnixSocket(t *testing.T) {
})
runtime.Gosched() // Allow other goroutines to run
tester.InitServer(fmt.Sprintf(`
harness.LoadConfig(fmt.Sprintf(`
{
skip_install_trust
admin localhost:2999
http_port 9080
https_port 9443
admin {$TESTING_CADDY_ADMIN_BIND}
http_port {$TESTING_CADDY_PORT_ONE}
https_port {$TESTING_CADDY_PORT_TWO}
grace_period 1ns
}
http://localhost:9080 {
http://localhost:{$TESTING_CADDY_PORT_ONE} {
reverse_proxy {
to unix/%s
health_uri /health
health_port 2021
health_interval 2s
@@ -415,14 +416,15 @@ func TestReverseProxyHealthCheckUnixSocket(t *testing.T) {
}
`, socketName), "caddyfile")
tester.AssertGetResponse("http://localhost:9080/", 200, "Hello, World!")
target := fmt.Sprintf("http://localhost:%d/", harness.Tester().PortOne())
harness.AssertGetResponse(target, 200, "Hello, World!")
}
func TestReverseProxyHealthCheckUnixSocketWithoutPort(t *testing.T) {
if runtime.GOOS == "windows" {
t.SkipNow()
}
tester := caddytest.NewTester(t)
harness := caddytest.StartHarness(t)
f, err := os.CreateTemp("", "*.sock")
if err != nil {
t.Errorf("failed to create TempFile: %s", err)
@@ -453,18 +455,18 @@ func TestReverseProxyHealthCheckUnixSocketWithoutPort(t *testing.T) {
})
runtime.Gosched() // Allow other goroutines to run
tester.InitServer(fmt.Sprintf(`
harness.LoadConfig(fmt.Sprintf(`
{
skip_install_trust
admin localhost:2999
http_port 9080
https_port 9443
admin {$TESTING_CADDY_ADMIN_BIND}
http_port {$TESTING_CADDY_PORT_ONE}
https_port {$TESTING_CADDY_PORT_TWO}
grace_period 1ns
}
http://localhost:9080 {
http://localhost:{$TESTING_CADDY_PORT_ONE} {
reverse_proxy {
to unix/%s
health_uri /health
health_interval 2s
health_timeout 5s
@@ -472,5 +474,5 @@ func TestReverseProxyHealthCheckUnixSocketWithoutPort(t *testing.T) {
}
`, socketName), "caddyfile")
tester.AssertGetResponse("http://localhost:9080/", 200, "Hello, World!")
harness.AssertGetResponse(fmt.Sprintf("http://localhost:%d/", harness.Tester().PortOne()), 200, "Hello, World!")
}
+25 -21
View File
@@ -1,6 +1,7 @@
package integration
import (
"fmt"
"testing"
"github.com/caddyserver/caddy/v2/caddytest"
@@ -8,20 +9,20 @@ import (
func TestDefaultSNI(t *testing.T) {
// arrange
tester := caddytest.NewTester(t)
tester.InitServer(`{
harness := caddytest.StartHarness(t)
harness.LoadConfig(`{
"admin": {
"listen": "localhost:2999"
"listen": "{$TESTING_CADDY_ADMIN_BIND}"
},
"apps": {
"http": {
"http_port": 9080,
"https_port": 9443,
"http_port": {$TESTING_CADDY_PORT_ONE},
"https_port": {$TESTING_CADDY_PORT_TWO},
"grace_period": 1,
"servers": {
"srv0": {
"listen": [
":9443"
":{$TESTING_CADDY_PORT_TWO}"
],
"routes": [
{
@@ -102,26 +103,27 @@ func TestDefaultSNI(t *testing.T) {
// act and assert
// makes a request with no sni
tester.AssertGetResponse("https://127.0.0.1:9443/version", 200, "hello from a.caddy.localhost")
target := fmt.Sprintf("https://127.0.0.1:%d/", harness.Tester().PortTwo())
harness.AssertGetResponse(target+"version", 200, "hello from a.caddy.localhost")
}
func TestDefaultSNIWithNamedHostAndExplicitIP(t *testing.T) {
// arrange
tester := caddytest.NewTester(t)
tester.InitServer(`
harness := caddytest.StartHarness(t)
harness.LoadConfig(`
{
"admin": {
"listen": "localhost:2999"
"listen": "{$TESTING_CADDY_ADMIN_BIND}"
},
"apps": {
"http": {
"http_port": 9080,
"https_port": 9443,
"http_port": {$TESTING_CADDY_PORT_ONE},
"https_port": {$TESTING_CADDY_PORT_TWO},
"grace_period": 1,
"servers": {
"srv0": {
"listen": [
":9443"
":{$TESTING_CADDY_PORT_TWO}"
],
"routes": [
{
@@ -206,26 +208,27 @@ func TestDefaultSNIWithNamedHostAndExplicitIP(t *testing.T) {
// act and assert
// makes a request with no sni
tester.AssertGetResponse("https://127.0.0.1:9443/version", 200, "hello from a")
target := fmt.Sprintf("https://127.0.0.1:%d/", harness.Tester().PortTwo())
harness.AssertGetResponse(target+"version", 200, "hello from a")
}
func TestDefaultSNIWithPortMappingOnly(t *testing.T) {
// arrange
tester := caddytest.NewTester(t)
tester.InitServer(`
harness := caddytest.StartHarness(t)
harness.LoadConfig(`
{
"admin": {
"listen": "localhost:2999"
"listen": "{$TESTING_CADDY_ADMIN_BIND}"
},
"apps": {
"http": {
"http_port": 9080,
"https_port": 9443,
"http_port": {$TESTING_CADDY_PORT_ONE},
"https_port": {$TESTING_CADDY_PORT_TWO},
"grace_period": 1,
"servers": {
"srv0": {
"listen": [
":9443"
":{$TESTING_CADDY_PORT_TWO}"
],
"routes": [
{
@@ -282,7 +285,8 @@ func TestDefaultSNIWithPortMappingOnly(t *testing.T) {
// act and assert
// makes a request with no sni
tester.AssertGetResponse("https://127.0.0.1:9443/version", 200, "hello from a.caddy.localhost")
target := fmt.Sprintf("https://127.0.0.1:%d/", harness.Tester().PortTwo())
harness.AssertGetResponse(target+"version", 200, "hello from a.caddy.localhost")
}
func TestHttpOnlyOnDomainWithSNI(t *testing.T) {
+24 -31
View File
@@ -20,21 +20,21 @@ import (
// (see https://github.com/caddyserver/caddy/issues/3556 for use case)
func TestH2ToH2CStream(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
harness := caddytest.StartHarness(t)
harness.LoadConfig(`
{
"admin": {
"listen": "localhost:2999"
"listen": "{$TESTING_CADDY_ADMIN_BIND}"
},
"apps": {
"http": {
"http_port": 9080,
"https_port": 9443,
"grace_period": 1,
"http_port": {$TESTING_CADDY_PORT_ONE},
"https_port": {$TESTING_CADDY_PORT_TWO},
"grace_period": 1,
"servers": {
"srv0": {
"listen": [
":9443"
":{$TESTING_CADDY_PORT_TWO}"
],
"routes": [
{
@@ -102,7 +102,7 @@ func TestH2ToH2CStream(t *testing.T) {
expectedBody := "some data to be echoed"
// start the server
server := testH2ToH2CStreamServeH2C(t)
server := testH2ToH2CStreamServeH2C(harness, t)
go server.ListenAndServe()
defer func() {
ctx, cancel := context.WithTimeout(context.Background(), time.Nanosecond)
@@ -116,7 +116,7 @@ func TestH2ToH2CStream(t *testing.T) {
Body: io.NopCloser(r),
URL: &url.URL{
Scheme: "https",
Host: "127.0.0.1:9443",
Host: fmt.Sprintf("127.0.0.1:%d", harness.Tester().PortTwo()),
Path: "/tov2ray",
},
Proto: "HTTP/2",
@@ -127,7 +127,7 @@ func TestH2ToH2CStream(t *testing.T) {
// Disable any compression method from server.
req.Header.Set("Accept-Encoding", "identity")
resp := tester.AssertResponseCode(req, http.StatusOK)
resp := harness.AssertResponseCode(req, http.StatusOK)
if resp.StatusCode != http.StatusOK {
return
}
@@ -149,7 +149,7 @@ func TestH2ToH2CStream(t *testing.T) {
}
}
func testH2ToH2CStreamServeH2C(t *testing.T) *http.Server {
func testH2ToH2CStreamServeH2C(harness *caddytest.TestHarness, t *testing.T) *http.Server {
h2s := &http2.Server{}
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
rstring, err := httputil.DumpRequest(r, false)
@@ -163,7 +163,7 @@ func testH2ToH2CStreamServeH2C(t *testing.T) *http.Server {
return
}
if r.Host != "127.0.0.1:9443" {
if r.Host != fmt.Sprintf("127.0.0.1:%d", harness.Tester().PortTwo()) {
t.Errorf("r.Host doesn't match, %v!", r.Host)
w.WriteHeader(http.StatusNotFound)
return
@@ -204,28 +204,21 @@ func testH2ToH2CStreamServeH2C(t *testing.T) *http.Server {
// (see https://github.com/caddyserver/caddy/issues/3606 for use case)
func TestH2ToH1ChunkedResponse(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
harness := caddytest.StartHarness(t)
harness.LoadConfig(`
{
"admin": {
"listen": "localhost:2999"
"listen": "{$TESTING_CADDY_ADMIN_BIND}"
},
"logging": {
"logs": {
"default": {
"level": "DEBUG"
}
}
},
"apps": {
"http": {
"http_port": 9080,
"https_port": 9443,
"grace_period": 1,
"http_port": {$TESTING_CADDY_PORT_ONE},
"https_port": {$TESTING_CADDY_PORT_TWO},
"grace_period": 1,
"servers": {
"srv0": {
"listen": [
":9443"
":{$TESTING_CADDY_PORT_TWO}"
],
"routes": [
{
@@ -312,7 +305,7 @@ func TestH2ToH1ChunkedResponse(t *testing.T) {
}
// start the server
server := testH2ToH1ChunkedResponseServeH1(t)
server := testH2ToH1ChunkedResponseServeH1(harness, t)
go server.ListenAndServe()
defer func() {
ctx, cancel := context.WithTimeout(context.Background(), time.Nanosecond)
@@ -326,7 +319,7 @@ func TestH2ToH1ChunkedResponse(t *testing.T) {
Body: io.NopCloser(r),
URL: &url.URL{
Scheme: "https",
Host: "127.0.0.1:9443",
Host: fmt.Sprintf("127.0.0.1:%d", harness.Tester().PortTwo()),
Path: "/tov2ray",
},
Proto: "HTTP/2",
@@ -340,7 +333,7 @@ func TestH2ToH1ChunkedResponse(t *testing.T) {
fmt.Fprint(w, expectedBody)
w.Close()
}()
resp := tester.AssertResponseCode(req, http.StatusOK)
resp := harness.AssertResponseCode(req, http.StatusOK)
if resp.StatusCode != http.StatusOK {
return
}
@@ -358,9 +351,9 @@ func TestH2ToH1ChunkedResponse(t *testing.T) {
}
}
func testH2ToH1ChunkedResponseServeH1(t *testing.T) *http.Server {
func testH2ToH1ChunkedResponseServeH1(harness *caddytest.TestHarness, t *testing.T) *http.Server {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Host != "127.0.0.1:9443" {
if r.Host != fmt.Sprintf("127.0.0.1:%d", harness.Tester().PortTwo()) {
t.Errorf("r.Host doesn't match, %v!", r.Host)
w.WriteHeader(http.StatusNotFound)
return
+241
View File
@@ -0,0 +1,241 @@
package caddytest
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"path"
"regexp"
"runtime"
"testing"
"github.com/stretchr/testify/require"
)
// use the convention to replace /[certificatename].[crt|key] with the full path
// this helps reduce the noise in test configurations and also allow this
// to run in any path
func prependCaddyFilePath(rawConfig string) string {
r := matchKey.ReplaceAllString(rawConfig, getIntegrationDir()+"$1")
r = matchCert.ReplaceAllString(r, getIntegrationDir()+"$1")
return r
}
func getIntegrationDir() string {
_, filename, _, ok := runtime.Caller(1)
if !ok {
panic("unable to determine the current file path")
}
return path.Dir(filename)
}
var (
matchKey = regexp.MustCompile(`(/[\w\d\.]+\.key)`)
matchCert = regexp.MustCompile(`(/[\w\d\.]+\.crt)`)
)
type TestHarness struct {
t testing.TB
tester *Tester
}
// StartHarness creates and starts a test harness environment which spans the lifetime a single caddy instance
// This is used for the integration tests
func StartHarness(t *testing.T) *TestHarness {
if testing.Short() {
t.SkipNow()
return nil
}
o := &TestHarness{t: t}
o.init()
return o
}
func (tc *TestHarness) Tester() *Tester {
return tc.tester
}
func (tc *TestHarness) Client() *http.Client {
return tc.tester.Client
}
func (tc *TestHarness) LoadConfig(rawConfig, configType string) {
rawConfig = prependCaddyFilePath(rawConfig)
err := tc.tester.LoadConfig(rawConfig, configType)
require.NoError(tc.t, err)
}
func (tc *TestHarness) init() {
// start the server
tester, err := NewTester()
if err != nil {
tc.t.Errorf("Failed to create caddy tester: %s", err)
return
}
tc.tester = tester
err = tc.tester.LaunchCaddy()
if err != nil {
tc.t.Errorf("Failed to launch caddy server: %s", err)
tc.t.FailNow()
return
}
// cleanup
tc.t.Cleanup(func() {
func() {
if tc.t.Failed() {
res, err := http.Get(fmt.Sprintf("http://localhost:%d/config/", tc.tester.adminPort))
if err != nil {
tc.t.Log("unable to read the current config")
return
}
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
var out bytes.Buffer
_ = json.Indent(&out, body, "", " ")
tc.t.Logf("----------- failed with config -----------\n%s", out.String())
}
}()
// shutdown server after extracing the config
err = tc.tester.CleanupCaddy()
if err != nil {
tc.t.Errorf("failed to clean up caddy instance: %s", err)
tc.t.FailNow()
}
})
}
// AssertRedirect makes a request and asserts the redirection happens
func (tc *TestHarness) AssertRedirect(requestURI string, expectedToLocation string, expectedStatusCode int) *http.Response {
redirectPolicyFunc := func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
}
// using the existing client, we override the check redirect policy for this test
old := tc.tester.Client.CheckRedirect
tc.tester.Client.CheckRedirect = redirectPolicyFunc
defer func() { tc.tester.Client.CheckRedirect = old }()
resp, err := tc.tester.Client.Get(requestURI)
if err != nil {
tc.t.Errorf("failed to call server %s", err)
return nil
}
if expectedStatusCode != resp.StatusCode {
tc.t.Errorf("requesting \"%s\" expected status code: %d but got %d", requestURI, expectedStatusCode, resp.StatusCode)
}
loc, err := resp.Location()
if err != nil {
tc.t.Errorf("requesting \"%s\" expected location: \"%s\" but got error: %s", requestURI, expectedToLocation, err)
}
if loc == nil && expectedToLocation != "" {
tc.t.Errorf("requesting \"%s\" expected a Location header, but didn't get one", requestURI)
}
if loc != nil {
if expectedToLocation != loc.String() {
tc.t.Errorf("requesting \"%s\" expected location: \"%s\" but got \"%s\"", requestURI, expectedToLocation, loc.String())
}
}
return resp
}
// AssertResponseCode will execute the request and verify the status code, returns a response for additional assertions
func (tc *TestHarness) AssertResponseCode(req *http.Request, expectedStatusCode int) *http.Response {
resp, err := tc.tester.Client.Do(req)
if err != nil {
tc.t.Fatalf("failed to call server %s", err)
}
if expectedStatusCode != resp.StatusCode {
tc.t.Errorf("requesting \"%s\" expected status code: %d but got %d", req.URL.RequestURI(), expectedStatusCode, resp.StatusCode)
}
return resp
}
// AssertResponse request a URI and assert the status code and the body contains a string
func (tc *TestHarness) AssertResponse(req *http.Request, expectedStatusCode int, expectedBody string) (*http.Response, string) {
resp := tc.AssertResponseCode(req, expectedStatusCode)
defer resp.Body.Close()
bytes, err := io.ReadAll(resp.Body)
if err != nil {
tc.t.Fatalf("unable to read the response body %s", err)
}
body := string(bytes)
if body != expectedBody {
tc.t.Errorf("requesting \"%s\" expected response body \"%s\" but got \"%s\"", req.RequestURI, expectedBody, body)
}
return resp, body
}
// Verb specific test functions
// AssertGetResponse GET a URI and expect a statusCode and body text
func (tc *TestHarness) AssertGetResponse(requestURI string, expectedStatusCode int, expectedBody string) (*http.Response, string) {
req, err := http.NewRequest("GET", requestURI, nil)
if err != nil {
tc.t.Fatalf("unable to create request %s", err)
}
return tc.AssertResponse(req, expectedStatusCode, expectedBody)
}
// AssertDeleteResponse request a URI and expect a statusCode and body text
func (tc *TestHarness) AssertDeleteResponse(requestURI string, expectedStatusCode int, expectedBody string) (*http.Response, string) {
req, err := http.NewRequest("DELETE", requestURI, nil)
if err != nil {
tc.t.Fatalf("unable to create request %s", err)
}
return tc.AssertResponse(req, expectedStatusCode, expectedBody)
}
// AssertPostResponseBody POST to a URI and assert the response code and body
func (tc *TestHarness) AssertPostResponseBody(requestURI string, requestHeaders []string, requestBody *bytes.Buffer, expectedStatusCode int, expectedBody string) (*http.Response, string) {
req, err := http.NewRequest("POST", requestURI, requestBody)
if err != nil {
tc.t.Errorf("failed to create request %s", err)
return nil, ""
}
applyHeaders(tc.t, req, requestHeaders)
return tc.AssertResponse(req, expectedStatusCode, expectedBody)
}
// AssertPutResponseBody PUT to a URI and assert the response code and body
func (tc *TestHarness) AssertPutResponseBody(requestURI string, requestHeaders []string, requestBody *bytes.Buffer, expectedStatusCode int, expectedBody string) (*http.Response, string) {
req, err := http.NewRequest("PUT", requestURI, requestBody)
if err != nil {
tc.t.Errorf("failed to create request %s", err)
return nil, ""
}
applyHeaders(tc.t, req, requestHeaders)
return tc.AssertResponse(req, expectedStatusCode, expectedBody)
}
// AssertPatchResponseBody PATCH to a URI and assert the response code and body
func (tc *TestHarness) AssertPatchResponseBody(requestURI string, requestHeaders []string, requestBody *bytes.Buffer, expectedStatusCode int, expectedBody string) (*http.Response, string) {
req, err := http.NewRequest("PATCH", requestURI, requestBody)
if err != nil {
tc.t.Errorf("failed to create request %s", err)
return nil, ""
}
applyHeaders(tc.t, req, requestHeaders)
return tc.AssertResponse(req, expectedStatusCode, expectedBody)
}
+15 -11
View File
@@ -8,9 +8,10 @@ import (
"github.com/caddyserver/caddy/v2"
)
var rootCmd = &cobra.Command{
Use: "caddy",
Long: `Caddy is an extensible server platform written in Go.
var defaultFactory = NewRootCommandFactory(func() *cobra.Command {
return &cobra.Command{
Use: "caddy",
Long: `Caddy is an extensible server platform written in Go.
At its core, Caddy merely manages configuration. Modules are plugged
in statically at compile-time to provide useful functionality. Caddy's
@@ -91,23 +92,26 @@ package installers: https://caddyserver.com/docs/install
Instructions for running Caddy in production are also available:
https://caddyserver.com/docs/running
`,
Example: ` $ caddy run
Example: ` $ caddy run
$ caddy run --config caddy.json
$ caddy reload --config caddy.json
$ caddy stop`,
// kind of annoying to have all the help text printed out if
// caddy has an error provisioning its modules, for instance...
SilenceUsage: true,
Version: onlyVersionText(),
}
// kind of annoying to have all the help text printed out if
// caddy has an error provisioning its modules, for instance...
SilenceUsage: true,
Version: onlyVersionText(),
}
})
const fullDocsFooter = `Full documentation is available at:
https://caddyserver.com/docs/command-line`
func init() {
rootCmd.SetVersionTemplate("{{.Version}}\n")
rootCmd.SetHelpTemplate(rootCmd.HelpTemplate() + "\n" + fullDocsFooter + "\n")
defaultFactory.Use(func(cmd *cobra.Command) {
cmd.SetVersionTemplate("{{.Version}}\n")
cmd.SetHelpTemplate(cmd.HelpTemplate() + "\n" + fullDocsFooter + "\n")
})
}
func onlyVersionText() string {
+28
View File
@@ -0,0 +1,28 @@
package caddycmd
import (
"github.com/spf13/cobra"
)
type RootCommandFactory struct {
constructor func() *cobra.Command
options []func(*cobra.Command)
}
func NewRootCommandFactory(fn func() *cobra.Command) *RootCommandFactory {
return &RootCommandFactory{
constructor: fn,
}
}
func (f *RootCommandFactory) Use(fn func(cmd *cobra.Command)) {
f.options = append(f.options, fn)
}
func (f *RootCommandFactory) Build() *cobra.Command {
o := f.constructor()
for _, v := range f.options {
v(o)
}
return o
}
+7 -1
View File
@@ -20,6 +20,7 @@ import (
"crypto/rand"
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"io/fs"
@@ -257,6 +258,7 @@ func cmdRun(fl Flags) (int, error) {
// if enabled, reload config file automatically on changes
// (this better only be used in dev!)
// do not enable this during tests, it will cause leaks
if watchFlag {
go watchConfigFile(configFile, configAdapterFlag)
}
@@ -280,7 +282,11 @@ func cmdRun(fl Flags) (int, error) {
}
}
select {}
if flag.Lookup("test.v") == nil || !strings.Contains(os.Args[0], ".test") {
select {}
} else {
return caddy.ExitCodeSuccess, nil
}
}
func cmdStop(fl Flags) (int, error) {
+29 -24
View File
@@ -459,7 +459,8 @@ argument of --directory. If the directory does not exist, it will be created.
if err := os.MkdirAll(dir, 0o755); err != nil {
return caddy.ExitCodeFailedQuit, err
}
if err := doc.GenManTree(rootCmd, &doc.GenManHeader{
ccmd := defaultFactory.Build()
if err := doc.GenManTree(ccmd, &doc.GenManHeader{
Title: "Caddy",
Section: "8", // https://en.wikipedia.org/wiki/Man_page#Manual_sections
}, dir); err != nil {
@@ -471,10 +472,11 @@ argument of --directory. If the directory does not exist, it will be created.
})
// source: https://github.com/spf13/cobra/blob/main/shell_completions.md
rootCmd.AddCommand(&cobra.Command{
Use: "completion [bash|zsh|fish|powershell]",
Short: "Generate completion script",
Long: fmt.Sprintf(`To load completions:
defaultFactory.Use(func(ccmd *cobra.Command) {
ccmd.AddCommand(&cobra.Command{
Use: "completion [bash|zsh|fish|powershell]",
Short: "Generate completion script",
Long: fmt.Sprintf(`To load completions:
Bash:
@@ -512,24 +514,25 @@ argument of --directory. If the directory does not exist, it will be created.
# To load completions for every new session, run:
PS> %[1]s completion powershell > %[1]s.ps1
# and source this file from your PowerShell profile.
`, rootCmd.Root().Name()),
DisableFlagsInUseLine: true,
ValidArgs: []string{"bash", "zsh", "fish", "powershell"},
Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs),
RunE: func(cmd *cobra.Command, args []string) error {
switch args[0] {
case "bash":
return cmd.Root().GenBashCompletion(os.Stdout)
case "zsh":
return cmd.Root().GenZshCompletion(os.Stdout)
case "fish":
return cmd.Root().GenFishCompletion(os.Stdout, true)
case "powershell":
return cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout)
default:
return fmt.Errorf("unrecognized shell: %s", args[0])
}
},
`, defaultFactory.constructor().Name()),
DisableFlagsInUseLine: true,
ValidArgs: []string{"bash", "zsh", "fish", "powershell"},
Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs),
RunE: func(cmd *cobra.Command, args []string) error {
switch args[0] {
case "bash":
return cmd.Root().GenBashCompletion(os.Stdout)
case "zsh":
return cmd.Root().GenZshCompletion(os.Stdout)
case "fish":
return cmd.Root().GenFishCompletion(os.Stdout, true)
case "powershell":
return cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout)
default:
return fmt.Errorf("unrecognized shell: %s", args[0])
}
},
})
})
}
@@ -563,7 +566,9 @@ func RegisterCommand(cmd Command) {
if !commandNameRegex.MatchString(cmd.Name) {
panic("invalid command name")
}
rootCmd.AddCommand(caddyCmdToCobra(cmd))
defaultFactory.Use(func(ccmd *cobra.Command) {
ccmd.AddCommand(caddyCmdToCobra(cmd))
})
}
var commandNameRegex = regexp.MustCompile(`^[a-z0-9]$|^([a-z0-9]+-?[a-z0-9]*)+[a-z0-9]$`)
+50 -12
View File
@@ -71,7 +71,7 @@ func Main() {
if err != nil {
caddy.Log().Warn("failed to set GOMAXPROCS", zap.Error(err))
}
rootCmd := defaultFactory.Build()
if err := rootCmd.Execute(); err != nil {
var exitError *exitError
if errors.As(err, &exitError) {
@@ -81,6 +81,18 @@ func Main() {
}
}
// MainForTesting implements the main function of the caddy command, used internally for testing
func MainForTesting(args ...string) error {
// create a root command for testing which will not pollute the global namespace, and does not
// call os.Exit().
rootCmd := defaultFactory.Build()
rootCmd.SetArgs(args)
if err := rootCmd.Execute(); err != nil {
return err
}
return nil
}
// handlePingbackConn reads from conn and ensures it matches
// the bytes in expect, or returns an error if it doesn't.
func handlePingbackConn(conn net.Conn, expect []byte) error {
@@ -107,6 +119,40 @@ func LoadConfig(configFile, adapterName string) ([]byte, string, error) {
return loadConfigWithLogger(caddy.Log(), configFile, adapterName)
}
func isCaddyfile(configFile, adapterName string) (bool, error) {
if adapterName == "caddyfile" {
return true, nil
}
// as a special case, if a config file starts with "caddyfile" or
// has a ".caddyfile" extension, and no adapter is specified, and
// no adapter module name matches the extension, assume
// caddyfile adapter for convenience
baseConfig := strings.ToLower(filepath.Base(configFile))
baseConfigExt := filepath.Ext(baseConfig)
startsOrEndsInCaddyfile := strings.HasPrefix(baseConfig, "caddyfile") || strings.HasSuffix(baseConfig, ".caddyfile")
if baseConfigExt == ".json" {
return false, nil
}
// If the adapter is not specified,
// the config file starts with "caddyfile",
// the config file has an extension,
// and isn't a JSON file (e.g. Caddyfile.yaml),
// then we don't know what the config format is.
if adapterName == "" && startsOrEndsInCaddyfile {
return true, nil
}
// adapter is not empty,
// adapter is not "caddyfile",
// extension is not ".json",
// extension is not ".caddyfile"
// file does not start with "Caddyfile"
return false, nil
}
func loadConfigWithLogger(logger *zap.Logger, configFile, adapterName string) ([]byte, string, error) {
// if no logger is provided, use a nop logger
// just so we don't have to check for nil
@@ -157,18 +203,10 @@ func loadConfigWithLogger(logger *zap.Logger, configFile, adapterName string) ([
}
}
// as a special case, if a config file starts with "caddyfile" or
// has a ".caddyfile" extension, and no adapter is specified, and
// no adapter module name matches the extension, assume
// caddyfile adapter for convenience
baseConfig := strings.ToLower(filepath.Base(configFile))
baseConfigExt := filepath.Ext(baseConfig)
if (strings.HasPrefix(baseConfig, "caddyfile") ||
strings.HasSuffix(baseConfig, ".caddyfile")) &&
(len(baseConfigExt) == 0 || caddyconfig.GetAdapter(baseConfigExt[1:]) == nil) &&
baseConfigExt != ".json" &&
adapterName == "" {
if yes, err := isCaddyfile(configFile, adapterName); yes {
adapterName = "caddyfile"
} else if err != nil {
return nil, "", err
}
// load config adapter
+110
View File
@@ -168,3 +168,113 @@ here"
}
}
}
func Test_isCaddyfile(t *testing.T) {
type args struct {
configFile string
adapterName string
}
tests := []struct {
name string
args args
want bool
wantErr bool
}{
{
name: "bare Caddyfile without adapter",
args: args{
configFile: "Caddyfile",
adapterName: "",
},
want: true,
wantErr: false,
},
{
name: "local Caddyfile without adapter",
args: args{
configFile: "./Caddyfile",
adapterName: "",
},
want: true,
wantErr: false,
},
{
name: "local caddyfile with adapter",
args: args{
configFile: "./Caddyfile",
adapterName: "caddyfile",
},
want: true,
wantErr: false,
},
{
name: "ends with .caddyfile with adapter",
args: args{
configFile: "./conf.caddyfile",
adapterName: "caddyfile",
},
want: true,
wantErr: false,
},
{
name: "ends with .caddyfile without adapter",
args: args{
configFile: "./conf.caddyfile",
adapterName: "",
},
want: true,
wantErr: false,
},
{
name: "config is Caddyfile.yaml with adapter",
args: args{
configFile: "./Caddyfile.yaml",
adapterName: "yaml",
},
want: false,
wantErr: false,
},
{
name: "json is not caddyfile but not error",
args: args{
configFile: "./Caddyfile.json",
adapterName: "",
},
want: false,
wantErr: false,
},
{
name: "prefix of Caddyfile and ./ with any extension is Caddyfile",
args: args{
configFile: "./Caddyfile.prd",
adapterName: "",
},
want: true,
wantErr: false,
},
{
name: "prefix of Caddyfile without ./ with any extension is Caddyfile",
args: args{
configFile: "Caddyfile.prd",
adapterName: "",
},
want: true,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := isCaddyfile(tt.args.configFile, tt.args.adapterName)
if (err != nil) != tt.wantErr {
t.Errorf("isCaddyfile() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("isCaddyfile() = %v, want %v", got, tt.want)
}
})
}
}
+2 -2
View File
@@ -9,7 +9,7 @@ require (
github.com/Masterminds/sprig/v3 v3.2.3
github.com/alecthomas/chroma/v2 v2.13.0
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b
github.com/caddyserver/certmagic v0.21.2
github.com/caddyserver/certmagic v0.21.3
github.com/caddyserver/zerossl v0.1.3
github.com/dustin/go-humanize v1.0.1
github.com/go-chi/chi/v5 v5.0.12
@@ -26,7 +26,7 @@ require (
github.com/spf13/cobra v1.8.0
github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.9.0
github.com/tailscale/tscert v0.0.0-20240517230440-bbccfbf48933
github.com/tailscale/tscert v0.0.0-20240608151842-d3f834017e53
github.com/yuin/goldmark v1.7.1
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0
+4 -4
View File
@@ -73,8 +73,8 @@ github.com/aws/smithy-go v1.20.2 h1:tbp628ireGtzcHDDmLT/6ADHidqnwgF57XOXZe6tp4Q=
github.com/aws/smithy-go v1.20.2/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/caddyserver/certmagic v0.21.2 h1:O18LtaYBGDooyy257cYePnhp4lPfz6TaJELil6Q1fDg=
github.com/caddyserver/certmagic v0.21.2/go.mod h1:Zq6pklO9nVRl3DIFUw9gVUfXKdpc/0qwTUAQMBlfgtI=
github.com/caddyserver/certmagic v0.21.3 h1:pqRRry3yuB4CWBVq9+cUqu+Y6E2z8TswbhNx1AZeYm0=
github.com/caddyserver/certmagic v0.21.3/go.mod h1:Zq6pklO9nVRl3DIFUw9gVUfXKdpc/0qwTUAQMBlfgtI=
github.com/caddyserver/zerossl v0.1.3 h1:onS+pxp3M8HnHpN5MMbOMyNjmTheJyWRaZYwn+YTAyA=
github.com/caddyserver/zerossl v0.1.3/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4=
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
@@ -415,8 +415,8 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tailscale/tscert v0.0.0-20240517230440-bbccfbf48933 h1:pV0H+XIvFoP7pl1MRtyPXh5hqoxB5I7snOtTHgrn6HU=
github.com/tailscale/tscert v0.0.0-20240517230440-bbccfbf48933/go.mod h1:kNGUQ3VESx3VZwRwA9MSCUegIl6+saPL8Noq82ozCaU=
github.com/tailscale/tscert v0.0.0-20240608151842-d3f834017e53 h1:uxMgm0C+EjytfAqyfBG55ZONKQ7mvd7x4YYCWsf8QHQ=
github.com/tailscale/tscert v0.0.0-20240608151842-d3f834017e53/go.mod h1:kNGUQ3VESx3VZwRwA9MSCUegIl6+saPL8Noq82ozCaU=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/urfave/cli v1.22.14 h1:ebbhrRiGK2i4naQJr+1Xj92HXZCrK7MsyTS/ob3HnAk=
github.com/urfave/cli v1.22.14/go.mod h1:X0eDS6pD6Exaclxm99NJ3FiCDRED7vIHpx2mDOHLvkA=
+20 -1
View File
@@ -16,6 +16,7 @@ package caddy
import (
"encoding/json"
"flag"
"fmt"
"io"
"log"
@@ -292,6 +293,10 @@ type BaseLog struct {
// The encoder is how the log entries are formatted or encoded.
EncoderRaw json.RawMessage `json:"encoder,omitempty" caddy:"namespace=caddy.logging.encoders inline_key=format"`
// Tees entries through a zap.Core module which can extract
// log entry metadata and fields for further processing.
CoreRaw json.RawMessage `json:"core,omitempty" caddy:"namespace=caddy.logging.cores inline_key=module"`
// Level is the minimum level to emit, and is inclusive.
// Possible levels: DEBUG, INFO, WARN, ERROR, PANIC, and FATAL
Level string `json:"level,omitempty"`
@@ -366,6 +371,14 @@ func (cl *BaseLog) provisionCommon(ctx Context, logging *Logging) error {
cl.encoder = newDefaultProductionLogEncoder(cl.writerOpener)
}
cl.buildCore()
if cl.CoreRaw != nil {
mod, err := ctx.LoadModule(cl, "CoreRaw")
if err != nil {
return fmt.Errorf("loading log core module: %v", err)
}
core := mod.(zapcore.Core)
cl.core = zapcore.NewTee(cl.core, core)
}
return nil
}
@@ -687,7 +700,13 @@ type defaultCustomLog struct {
// and enables INFO-level logs and higher.
func newDefaultProductionLog() (*defaultCustomLog, error) {
cl := new(CustomLog)
cl.writerOpener = StderrWriter{}
f := flag.Lookup("test.v")
if (f != nil && f.Value.String() != "true") || strings.Contains(os.Args[0], ".test") {
cl.writerOpener = &DiscardWriter{}
} else {
cl.writerOpener = StderrWriter{}
}
var err error
cl.writer, err = cl.writerOpener.OpenWriter()
if err != nil {
+5
View File
@@ -355,6 +355,11 @@ type Event struct {
origin caddy.Module
}
func (e Event) ID() uuid.UUID { return e.id }
func (e Event) Timestamp() time.Time { return e.ts }
func (e Event) Name() string { return e.name }
func (e Event) Origin() caddy.Module { return e.origin }
// CloudEvent exports event e as a structure that, when
// serialized as JSON, is compatible with the
// CloudEvents spec.
+1 -1
View File
@@ -239,7 +239,7 @@ func SanitizedPathJoin(root, reqPath string) string {
}
relPath := path.Clean("/" + reqPath)[1:] // clean path and trim the leading /
if !filepath.IsLocal(relPath) {
if relPath != "" && !filepath.IsLocal(relPath) {
// path is unsafe (see https://github.com/golang/go/issues/56336#issuecomment-1416214885)
return root
}
+43 -11
View File
@@ -26,22 +26,28 @@ func TestSanitizedPathJoin(t *testing.T) {
inputPath: "/",
expect: ".",
},
{
// fileserver.MatchFile passes an inputPath of "//" for some try_files values.
// See https://github.com/caddyserver/caddy/issues/6352
inputPath: "//",
expect: filepath.FromSlash("./"),
},
{
inputPath: "/foo",
expect: "foo",
},
{
inputPath: "/foo/",
expect: "foo" + separator,
expect: filepath.FromSlash("foo/"),
},
{
inputPath: "/foo/bar",
expect: filepath.Join("foo", "bar"),
expect: filepath.FromSlash("foo/bar"),
},
{
inputRoot: "/a",
inputPath: "/foo/bar",
expect: filepath.Join("/", "a", "foo", "bar"),
expect: filepath.FromSlash("/a/foo/bar"),
},
{
inputPath: "/foo/../bar",
@@ -50,32 +56,34 @@ func TestSanitizedPathJoin(t *testing.T) {
{
inputRoot: "/a/b",
inputPath: "/foo/../bar",
expect: filepath.Join("/", "a", "b", "bar"),
expect: filepath.FromSlash("/a/b/bar"),
},
{
inputRoot: "/a/b",
inputPath: "/..%2fbar",
expect: filepath.Join("/", "a", "b", "bar"),
expect: filepath.FromSlash("/a/b/bar"),
},
{
inputRoot: "/a/b",
inputPath: "/%2e%2e%2fbar",
expect: filepath.Join("/", "a", "b", "bar"),
expect: filepath.FromSlash("/a/b/bar"),
},
{
// inputPath fails the IsLocal test so only the root is returned,
// but with a trailing slash since one was included in inputPath
inputRoot: "/a/b",
inputPath: "/%2e%2e%2f%2e%2e%2f",
expect: "/a/b", // inputPath fails the IsLocal test so only the root is returned
expect: filepath.FromSlash("/a/b/"),
},
{
inputRoot: "/a/b",
inputPath: "/foo%2fbar",
expect: filepath.Join("/", "a", "b", "foo", "bar"),
expect: filepath.FromSlash("/a/b/foo/bar"),
},
{
inputRoot: "/a/b",
inputPath: "/foo%252fbar",
expect: filepath.Join("/", "a", "b", "foo%2fbar"),
expect: filepath.FromSlash("/a/b/foo%2fbar"),
},
{
inputRoot: "C:\\www",
@@ -86,13 +94,37 @@ func TestSanitizedPathJoin(t *testing.T) {
inputRoot: "C:\\www",
inputPath: "/D:\\foo\\bar",
expect: filepath.Join("C:\\www", "D:\\foo\\bar"),
expectWindows: filepath.Join("C:\\www"), // inputPath fails IsLocal on Windows
expectWindows: "C:\\www", // inputPath fails IsLocal on Windows
},
{
inputRoot: `C:\www`,
inputPath: `/..\windows\win.ini`,
expect: `C:\www/..\windows\win.ini`,
expectWindows: `C:\www`,
},
{
inputRoot: `C:\www`,
inputPath: `/..\..\..\..\..\..\..\..\..\..\windows\win.ini`,
expect: `C:\www/..\..\..\..\..\..\..\..\..\..\windows\win.ini`,
expectWindows: `C:\www`,
},
{
inputRoot: `C:\www`,
inputPath: `/..%5cwindows%5cwin.ini`,
expect: `C:\www/..\windows\win.ini`,
expectWindows: `C:\www`,
},
{
inputRoot: `C:\www`,
inputPath: `/..%5c..%5c..%5c..%5c..%5c..%5c..%5c..%5c..%5c..%5cwindows%5cwin.ini`,
expect: `C:\www/..\..\..\..\..\..\..\..\..\..\windows\win.ini`,
expectWindows: `C:\www`,
},
{
// https://github.com/golang/go/issues/56336#issuecomment-1416214885
inputRoot: "root",
inputPath: "/a/b/../../c",
expect: filepath.Join("root", "c"),
expect: filepath.FromSlash("root/c"),
},
} {
// we don't *need* to use an actual parsed URL, but it
+14 -6
View File
@@ -34,6 +34,7 @@ import (
"github.com/google/cel-go/cel"
"github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/ref"
"golang.org/x/net/idna"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
@@ -239,13 +240,20 @@ func (m *MatchHost) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
func (m MatchHost) Provision(_ caddy.Context) error {
// check for duplicates; they are nonsensical and reduce efficiency
// (we could just remove them, but the user should know their config is erroneous)
seen := make(map[string]int)
for i, h := range m {
h = strings.ToLower(h)
if firstI, ok := seen[h]; ok {
return fmt.Errorf("host at index %d is repeated at index %d: %s", firstI, i, h)
seen := make(map[string]int, len(m))
for i, host := range m {
asciiHost, err := idna.ToASCII(host)
if err != nil {
return fmt.Errorf("converting hostname '%s' to ASCII: %v", host, err)
}
seen[h] = i
if asciiHost != host {
m[i] = asciiHost
}
normalizedHost := strings.ToLower(asciiHost)
if firstI, ok := seen[normalizedHost]; ok {
return fmt.Errorf("host at index %d is repeated at index %d: %s", firstI, i, host)
}
seen[normalizedHost] = i
}
if m.large() {
+9
View File
@@ -78,6 +78,11 @@ func TestHostMatcher(t *testing.T) {
input: "bar.example.com",
expect: false,
},
{
match: MatchHost{"éxàmplê.com"},
input: "xn--xmpl-0na6cm.com",
expect: true,
},
{
match: MatchHost{"*.example.com"},
input: "example.com",
@@ -149,6 +154,10 @@ func TestHostMatcher(t *testing.T) {
ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
req = req.WithContext(ctx)
if err := tc.match.Provision(caddy.Context{}); err != nil {
t.Errorf("Test %d %v: provisioning failed: %v", i, tc.match, err)
}
actual := tc.match.Match(req)
if actual != tc.expect {
t.Errorf("Test %d %v: Expected %t, got %t for '%s'", i, tc.match, tc.expect, actual, tc.input)
+4 -4
View File
@@ -219,13 +219,13 @@ func (rr *responseRecorder) Buffered() bool {
}
func (rr *responseRecorder) WriteResponse() error {
if rr.stream {
return nil
}
if rr.statusCode == 0 {
// could happen if no handlers actually wrote anything,
// and this prevents a panic; status must be > 0
rr.statusCode = http.StatusOK
rr.WriteHeader(http.StatusOK)
}
if rr.stream {
return nil
}
rr.ResponseWriterWrapper.WriteHeader(rr.statusCode)
_, err := io.Copy(rr.ResponseWriterWrapper, rr.buf)
@@ -26,6 +26,7 @@ import (
"strconv"
"strings"
"sync/atomic"
"time"
"github.com/cespare/xxhash/v2"
@@ -613,6 +614,8 @@ type CookieHashSelection struct {
Name string `json:"name,omitempty"`
// Secret to hash (Hmac256) chosen upstream in cookie
Secret string `json:"secret,omitempty"`
// The cookie's Max-Age before it expires. Default is no expiry.
MaxAge caddy.Duration `json:"max_age,omitempty"`
// The fallback policy to use if the cookie is not present. Defaults to `random`.
FallbackRaw json.RawMessage `json:"fallback,omitempty" caddy:"namespace=http.reverse_proxy.selection_policies inline_key=policy"`
@@ -671,6 +674,9 @@ func (s CookieHashSelection) Select(pool UpstreamPool, req *http.Request, w http
cookie.Secure = true
cookie.SameSite = http.SameSiteNoneMode
}
if s.MaxAge > 0 {
cookie.MaxAge = int(time.Duration(s.MaxAge).Seconds())
}
http.SetCookie(w, cookie)
return upstream
}
@@ -699,6 +705,7 @@ func (s CookieHashSelection) Select(pool UpstreamPool, req *http.Request, w http
//
// lb_policy cookie [<name> [<secret>]] {
// fallback <policy>
// max_age <duration>
// }
//
// By default name is `lb`
@@ -728,6 +735,24 @@ func (s *CookieHashSelection) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
return err
}
s.FallbackRaw = mod
case "max_age":
if !d.NextArg() {
return d.ArgErr()
}
if s.MaxAge != 0 {
return d.Err("cookie max_age already specified")
}
maxAge, err := caddy.ParseDuration(d.Val())
if err != nil {
return d.Errf("invalid duration: %s", d.Val())
}
if maxAge <= 0 {
return d.Errf("invalid duration: %s, max_age should be non-zero and positive", d.Val())
}
if d.NextArg() {
return d.ArgErr()
}
s.MaxAge = caddy.Duration(maxAge)
default:
return d.Errf("unrecognized option '%s'", d.Val())
}
+13 -6
View File
@@ -264,6 +264,12 @@ func (iss *ACMEIssuer) Revoke(ctx context.Context, cert certmagic.CertificateRes
// to be accessed and manipulated.
func (iss *ACMEIssuer) GetACMEIssuer() *ACMEIssuer { return iss }
// GetRenewalInfo wraps the underlying GetRenewalInfo method and satisfies
// the CertMagic interface for ARI support.
func (iss *ACMEIssuer) GetRenewalInfo(ctx context.Context, cert certmagic.Certificate) (acme.RenewalInfo, error) {
return iss.issuer.GetRenewalInfo(ctx, cert)
}
// generateZeroSSLEABCredentials generates ZeroSSL EAB credentials for the primary contact email
// on the issuer. It should only be usedif the CA endpoint is ZeroSSL. An email address is required.
func (iss *ACMEIssuer) generateZeroSSLEABCredentials(ctx context.Context, acct acme.Account) (*acme.EAB, acme.Account, error) {
@@ -649,10 +655,11 @@ type ChainPreference struct {
// Interface guards
var (
_ certmagic.PreChecker = (*ACMEIssuer)(nil)
_ certmagic.Issuer = (*ACMEIssuer)(nil)
_ certmagic.Revoker = (*ACMEIssuer)(nil)
_ caddy.Provisioner = (*ACMEIssuer)(nil)
_ ConfigSetter = (*ACMEIssuer)(nil)
_ caddyfile.Unmarshaler = (*ACMEIssuer)(nil)
_ certmagic.PreChecker = (*ACMEIssuer)(nil)
_ certmagic.Issuer = (*ACMEIssuer)(nil)
_ certmagic.Revoker = (*ACMEIssuer)(nil)
_ certmagic.RenewalInfoGetter = (*ACMEIssuer)(nil)
_ caddy.Provisioner = (*ACMEIssuer)(nil)
_ ConfigSetter = (*ACMEIssuer)(nil)
_ caddyfile.Unmarshaler = (*ACMEIssuer)(nil)
)
+11
View File
@@ -81,6 +81,16 @@ type TLS struct {
// EXPERIMENTAL. Subject to change.
DisableOCSPStapling bool `json:"disable_ocsp_stapling,omitempty"`
// Disables checks in certmagic that the configured storage is ready
// and able to handle writing new content to it. These checks are
// intended to prevent information loss (newly issued certificates), but
// can be expensive on the storage.
//
// Disabling these checks should only be done when the storage
// can be trusted to have enough capacity and no other problems.
// EXPERIMENTAL. Subject to change.
DisableStorageCheck bool `json:"disable_storage_check,omitempty"`
certificateLoaders []CertificateLoader
automateNames []string
ctx caddy.Context
@@ -255,6 +265,7 @@ func (t *TLS) Provision(ctx caddy.Context) error {
OCSP: certmagic.OCSPConfig{
DisableStapling: t.DisableOCSPStapling,
},
DisableStorageCheck: t.DisableStorageCheck,
})
certCacheMu.RUnlock()
for _, loader := range t.certificateLoaders {
+36
View File
@@ -0,0 +1,36 @@
package logging
import (
"go.uber.org/zap/zapcore"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
)
func init() {
caddy.RegisterModule(MockCore{})
}
// MockCore is a no-op module, purely for testing
type MockCore struct {
zapcore.Core `json:"-"`
}
// CaddyModule returns the Caddy module information.
func (MockCore) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
ID: "caddy.logging.cores.mock",
New: func() caddy.Module { return new(MockCore) },
}
}
func (lec *MockCore) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
return nil
}
// Interface guards
var (
_ zapcore.Core = (*MockCore)(nil)
_ caddy.Module = (*MockCore)(nil)
_ caddyfile.Unmarshaler = (*MockCore)(nil)
)
+78 -2
View File
@@ -15,6 +15,7 @@
package logging
import (
"encoding/json"
"fmt"
"io"
"math"
@@ -33,6 +34,48 @@ func init() {
caddy.RegisterModule(FileWriter{})
}
// fileMode is a string made of 1 to 4 octal digits representing
// a numeric mode as specified with the `chmod` unix command.
// `"0777"` and `"777"` are thus equivalent values.
type fileMode os.FileMode
// UnmarshalJSON satisfies json.Unmarshaler.
func (m *fileMode) UnmarshalJSON(b []byte) error {
if len(b) == 0 {
return io.EOF
}
var s string
if err := json.Unmarshal(b, &s); err != nil {
return err
}
mode, err := parseFileMode(s)
if err != nil {
return err
}
*m = fileMode(mode)
return err
}
// MarshalJSON satisfies json.Marshaler.
func (m *fileMode) MarshalJSON() ([]byte, error) {
return []byte(fmt.Sprintf("\"%04o\"", *m)), nil
}
// parseFileMode parses a file mode string,
// adding support for `chmod` unix command like
// 1 to 4 digital octal values.
func parseFileMode(s string) (os.FileMode, error) {
modeStr := fmt.Sprintf("%04s", s)
mode, err := strconv.ParseUint(modeStr, 8, 32)
if err != nil {
return 0, err
}
return os.FileMode(mode), nil
}
// FileWriter can write logs to files. By default, log files
// are rotated ("rolled") when they get large, and old log
// files get deleted, to ensure that the process does not
@@ -41,6 +84,10 @@ type FileWriter struct {
// Filename is the name of the file to write.
Filename string `json:"filename,omitempty"`
// The file permissions mode.
// 0600 by default.
Mode fileMode `json:"mode,omitempty"`
// Roll toggles log rolling or rotation, which is
// enabled by default.
Roll *bool `json:"roll,omitempty"`
@@ -100,6 +147,10 @@ func (fw FileWriter) WriterKey() string {
// OpenWriter opens a new file writer.
func (fw FileWriter) OpenWriter() (io.WriteCloser, error) {
if fw.Mode == 0 {
fw.Mode = 0o600
}
// roll log files by default
if fw.Roll == nil || *fw.Roll {
if fw.RollSizeMB == 0 {
@@ -116,6 +167,19 @@ func (fw FileWriter) OpenWriter() (io.WriteCloser, error) {
fw.RollKeepDays = 90
}
// create the file if it does not exist with the right mode.
// lumberjack will reuse the file mode across log rotation.
f_tmp, err := os.OpenFile(fw.Filename, os.O_WRONLY|os.O_APPEND|os.O_CREATE, os.FileMode(fw.Mode))
if err != nil {
return nil, err
}
f_tmp.Close()
// ensure already existing files have the right mode,
// since OpenFile will not set the mode in such case.
if err = os.Chmod(fw.Filename, os.FileMode(fw.Mode)); err != nil {
return nil, err
}
return &lumberjack.Logger{
Filename: fw.Filename,
MaxSize: fw.RollSizeMB,
@@ -127,12 +191,13 @@ func (fw FileWriter) OpenWriter() (io.WriteCloser, error) {
}
// otherwise just open a regular file
return os.OpenFile(fw.Filename, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0o666)
return os.OpenFile(fw.Filename, os.O_WRONLY|os.O_APPEND|os.O_CREATE, os.FileMode(fw.Mode))
}
// UnmarshalCaddyfile sets up the module from Caddyfile tokens. Syntax:
//
// file <filename> {
// mode <mode>
// roll_disabled
// roll_size <size>
// roll_uncompressed
@@ -150,7 +215,7 @@ func (fw FileWriter) OpenWriter() (io.WriteCloser, error) {
// The roll_keep_for duration has day resolution.
// Fractional values are rounded up to the next whole number of days.
//
// If any of the roll_size, roll_keep, or roll_keep_for subdirectives are
// If any of the mode, roll_size, roll_keep, or roll_keep_for subdirectives are
// omitted or set to a zero value, then Caddy's default value for that
// subdirective is used.
func (fw *FileWriter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
@@ -165,6 +230,17 @@ func (fw *FileWriter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
for d.NextBlock(0) {
switch d.Val() {
case "mode":
var modeStr string
if !d.AllArgs(&modeStr) {
return d.ArgErr()
}
mode, err := parseFileMode(modeStr)
if err != nil {
return d.Errf("parsing mode: %v", err)
}
fw.Mode = fileMode(mode)
case "roll_disabled":
var f bool
fw.Roll = &f
+386
View File
@@ -0,0 +1,386 @@
// 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.
//go:build !windows
package logging
import (
"encoding/json"
"os"
"path"
"syscall"
"testing"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
)
func TestFileCreationMode(t *testing.T) {
on := true
off := false
tests := []struct {
name string
fw FileWriter
wantMode os.FileMode
}{
{
name: "default mode no roll",
fw: FileWriter{
Roll: &off,
},
wantMode: 0o600,
},
{
name: "default mode roll",
fw: FileWriter{
Roll: &on,
},
wantMode: 0o600,
},
{
name: "custom mode no roll",
fw: FileWriter{
Roll: &off,
Mode: 0o666,
},
wantMode: 0o666,
},
{
name: "custom mode roll",
fw: FileWriter{
Roll: &on,
Mode: 0o666,
},
wantMode: 0o666,
},
}
m := syscall.Umask(0o000)
defer syscall.Umask(m)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
dir, err := os.MkdirTemp("", "caddytest")
if err != nil {
t.Fatalf("failed to create tempdir: %v", err)
}
defer os.RemoveAll(dir)
fpath := path.Join(dir, "test.log")
tt.fw.Filename = fpath
logger, err := tt.fw.OpenWriter()
if err != nil {
t.Fatalf("failed to create file: %v", err)
}
defer logger.Close()
st, err := os.Stat(fpath)
if err != nil {
t.Fatalf("failed to check file permissions: %v", err)
}
if st.Mode() != tt.wantMode {
t.Errorf("file mode is %v, want %v", st.Mode(), tt.wantMode)
}
})
}
}
func TestFileRotationPreserveMode(t *testing.T) {
m := syscall.Umask(0o000)
defer syscall.Umask(m)
dir, err := os.MkdirTemp("", "caddytest")
if err != nil {
t.Fatalf("failed to create tempdir: %v", err)
}
defer os.RemoveAll(dir)
fpath := path.Join(dir, "test.log")
roll := true
mode := fileMode(0o640)
fw := FileWriter{
Filename: fpath,
Mode: mode,
Roll: &roll,
RollSizeMB: 1,
}
logger, err := fw.OpenWriter()
if err != nil {
t.Fatalf("failed to create file: %v", err)
}
defer logger.Close()
b := make([]byte, 1024*1024-1000)
logger.Write(b)
logger.Write(b[0:2000])
files, err := os.ReadDir(dir)
if err != nil {
t.Fatalf("failed to read temporary log dir: %v", err)
}
// We might get 2 or 3 files depending
// on the race between compressed log file generation,
// removal of the non compressed file and reading the directory.
// Ordering of the files are [ test-*.log test-*.log.gz test.log ]
if len(files) < 2 || len(files) > 3 {
t.Log("got files: ", files)
t.Fatalf("got %v files want 2", len(files))
}
wantPattern := "test-*-*-*-*-*.*.log"
test_date_log := files[0]
if m, _ := path.Match(wantPattern, test_date_log.Name()); m != true {
t.Fatalf("got %v filename want %v", test_date_log.Name(), wantPattern)
}
st, err := os.Stat(path.Join(dir, test_date_log.Name()))
if err != nil {
t.Fatalf("failed to check file permissions: %v", err)
}
if st.Mode() != os.FileMode(mode) {
t.Errorf("file mode is %v, want %v", st.Mode(), mode)
}
test_dot_log := files[len(files)-1]
if test_dot_log.Name() != "test.log" {
t.Fatalf("got %v filename want test.log", test_dot_log.Name())
}
st, err = os.Stat(path.Join(dir, test_dot_log.Name()))
if err != nil {
t.Fatalf("failed to check file permissions: %v", err)
}
if st.Mode() != os.FileMode(mode) {
t.Errorf("file mode is %v, want %v", st.Mode(), mode)
}
}
func TestFileModeConfig(t *testing.T) {
tests := []struct {
name string
d *caddyfile.Dispenser
fw FileWriter
wantErr bool
}{
{
name: "set mode",
d: caddyfile.NewTestDispenser(`
file test.log {
mode 0666
}
`),
fw: FileWriter{
Mode: 0o666,
},
wantErr: false,
},
{
name: "set mode 3 digits",
d: caddyfile.NewTestDispenser(`
file test.log {
mode 666
}
`),
fw: FileWriter{
Mode: 0o666,
},
wantErr: false,
},
{
name: "set mode 2 digits",
d: caddyfile.NewTestDispenser(`
file test.log {
mode 66
}
`),
fw: FileWriter{
Mode: 0o066,
},
wantErr: false,
},
{
name: "set mode 1 digits",
d: caddyfile.NewTestDispenser(`
file test.log {
mode 6
}
`),
fw: FileWriter{
Mode: 0o006,
},
wantErr: false,
},
{
name: "invalid mode",
d: caddyfile.NewTestDispenser(`
file test.log {
mode foobar
}
`),
fw: FileWriter{},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
fw := &FileWriter{}
if err := fw.UnmarshalCaddyfile(tt.d); (err != nil) != tt.wantErr {
t.Fatalf("UnmarshalCaddyfile() error = %v, want %v", err, tt.wantErr)
}
if fw.Mode != tt.fw.Mode {
t.Errorf("got mode %v, want %v", fw.Mode, tt.fw.Mode)
}
})
}
}
func TestFileModeJSON(t *testing.T) {
tests := []struct {
name string
config string
fw FileWriter
wantErr bool
}{
{
name: "set mode",
config: `
{
"mode": "0666"
}
`,
fw: FileWriter{
Mode: 0o666,
},
wantErr: false,
},
{
name: "set mode invalid value",
config: `
{
"mode": "0x666"
}
`,
fw: FileWriter{},
wantErr: true,
},
{
name: "set mode invalid string",
config: `
{
"mode": 777
}
`,
fw: FileWriter{},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
fw := &FileWriter{}
if err := json.Unmarshal([]byte(tt.config), fw); (err != nil) != tt.wantErr {
t.Fatalf("UnmarshalJSON() error = %v, want %v", err, tt.wantErr)
}
if fw.Mode != tt.fw.Mode {
t.Errorf("got mode %v, want %v", fw.Mode, tt.fw.Mode)
}
})
}
}
func TestFileModeToJSON(t *testing.T) {
tests := []struct {
name string
mode fileMode
want string
wantErr bool
}{
{
name: "none zero",
mode: 0644,
want: `"0644"`,
wantErr: false,
},
{
name: "zero mode",
mode: 0,
want: `"0000"`,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var b []byte
var err error
if b, err = json.Marshal(&tt.mode); (err != nil) != tt.wantErr {
t.Fatalf("MarshalJSON() error = %v, want %v", err, tt.wantErr)
}
got := string(b[:])
if got != tt.want {
t.Errorf("got mode %v, want %v", got, tt.want)
}
})
}
}
func TestFileModeModification(t *testing.T) {
m := syscall.Umask(0o000)
defer syscall.Umask(m)
dir, err := os.MkdirTemp("", "caddytest")
if err != nil {
t.Fatalf("failed to create tempdir: %v", err)
}
defer os.RemoveAll(dir)
fpath := path.Join(dir, "test.log")
f_tmp, err := os.OpenFile(fpath, os.O_WRONLY|os.O_APPEND|os.O_CREATE, os.FileMode(0600))
if err != nil {
t.Fatalf("failed to create test file: %v", err)
}
f_tmp.Close()
fw := FileWriter{
Mode: 0o666,
Filename: fpath,
}
logger, err := fw.OpenWriter()
if err != nil {
t.Fatalf("failed to create file: %v", err)
}
defer logger.Close()
st, err := os.Stat(fpath)
if err != nil {
t.Fatalf("failed to check file permissions: %v", err)
}
want := os.FileMode(fw.Mode)
if st.Mode() != want {
t.Errorf("file mode is %v, want %v", st.Mode(), want)
}
}
@@ -0,0 +1,55 @@
// 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.
//go:build windows
package logging
import (
"os"
"path"
"testing"
)
// Windows relies on ACLs instead of unix permissions model.
// Go allows to open files with a particular mode put it is limited to read or write.
// See https://cs.opensource.google/go/go/+/refs/tags/go1.22.3:src/syscall/syscall_windows.go;l=708.
// This is pretty restrictive and has few interest for log files and thus we just test that log files are
// opened with R/W permissions by default on Windows too.
func TestFileCreationMode(t *testing.T) {
dir, err := os.MkdirTemp("", "caddytest")
if err != nil {
t.Fatalf("failed to create tempdir: %v", err)
}
defer os.RemoveAll(dir)
fw := &FileWriter{
Filename: path.Join(dir, "test.log"),
}
logger, err := fw.OpenWriter()
if err != nil {
t.Fatalf("failed to create file: %v", err)
}
defer logger.Close()
st, err := os.Stat(fw.Filename)
if err != nil {
t.Fatalf("failed to check file permissions: %v", err)
}
if st.Mode().Perm()&0o600 != 0o600 {
t.Fatalf("file mode is %v, want rw for user", st.Mode().Perm())
}
}