@marcel91200 / caddy Mirror · read-only / commits / 7910dd94
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 11, 03:30 UTC

caddyhttp: ignore case when matching hosts in logger_names (#8176)

* caddyhttp: ignore case when matching hosts in logger_names * caddyhttp: test case-insensitive logger_names through App.Provision * caddyhttp: test case-insensitive logger_names in integration harness

tbshfr committed Oct 9, 2026 at 23:15 UTC 7910dd94eea2750896f187301f4d99a21a522d24
4 files changed +192
caddytest/integration/logging_test.go new
+109
@@ -0,0 +1,109 @@
1 +// Copyright 2015 Matthew Holt and The Caddy Authors
2 +//
3 +// Licensed under the Apache License, Version 2.0 (the "License");
4 +// you may not use this file except in compliance with the License.
5 +// You may obtain a copy of the License at
6 +//
7 +// http://www.apache.org/licenses/LICENSE-2.0
8 +//
9 +// Unless required by applicable law or agreed to in writing, software
10 +// distributed under the License is distributed on an "AS IS" BASIS,
11 +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 +// See the License for the specific language governing permissions and
13 +// limitations under the License.
14 +
15 +package integration
16 +
17 +import (
18 + "bufio"
19 + "bytes"
20 + "encoding/json"
21 + "net/http"
22 + "os"
23 + "path/filepath"
24 + "testing"
25 + "time"
26 +
27 + "github.com/caddyserver/caddy/v2/caddytest"
28 +)
29 +
30 +// TestLoggerNamesCaseInsensitive loads a config whose logger_names key is
31 +// mixed-case and asserts that a request with a differently-cased Host is
32 +// written by the configured named logger.
33 +func TestLoggerNamesCaseInsensitive(t *testing.T) {
34 + logFile := filepath.Join(t.TempDir(), "access.log")
35 + logFileJSON, err := json.Marshal(logFile)
36 + if err != nil {
37 + t.Fatal(err)
38 + }
39 +
40 + tester := caddytest.NewTester(t)
41 + // Load a config without the file logger on cleanup so the log file is
42 + // closed before t.TempDir removes it; Windows cant delete open files.
43 + t.Cleanup(func() {
44 + tester.InitServer(`{"admin": {"listen": "localhost:2999"}}`, "json")
45 + })
46 + // The config is JSON because the Caddyfile adapter already lowercases
47 + // site addresses, so it would never produce a mixed-case key.
48 + tester.InitServer(`{
49 + "admin": {"listen": "localhost:2999"},
50 + "logging": {
51 + "logs": {
52 + "mixed": {
53 + "writer": {"output": "file", "filename": `+string(logFileJSON)+`},
54 + "encoder": {"format": "json"},
55 + "include": ["http.log.access.mixed"]
56 + }
57 + }
58 + },
59 + "apps": {
60 + "http": {
61 + "http_port": 9080,
62 + "grace_period": 1,
63 + "servers": {
64 + "srv0": {
65 + "listen": [":9080"],
66 + "routes": [
67 + {"handle": [{"handler": "static_response", "body": "ok"}]}
68 + ],
69 + "logs": {
70 + "logger_names": {"Mixed.Example.org": ["mixed"]}
71 + }
72 + }
73 + }
74 + }
75 + }
76 + }`, "json")
77 +
78 + req, err := http.NewRequest(http.MethodGet, "http://localhost:9080/", nil)
79 + if err != nil {
80 + t.Fatal(err)
81 + }
82 + req.Host = "MIXED.example.org"
83 + tester.AssertResponse(req, http.StatusOK, "ok")
84 +
85 + // the access log entry is written after the response, so poll for it
86 + var logs []byte
87 + for deadline := time.Now().Add(5 * time.Second); time.Now().Before(deadline); time.Sleep(50 * time.Millisecond) {
88 + logs, err = os.ReadFile(logFile)
89 + if err != nil {
90 + continue
91 + }
92 + scanner := bufio.NewScanner(bytes.NewReader(logs))
93 + for scanner.Scan() {
94 + var entry struct {
95 + Logger string `json:"logger"`
96 + Request struct {
97 + Host string `json:"host"`
98 + } `json:"request"`
99 + }
100 + if err := json.Unmarshal(scanner.Bytes(), &entry); err != nil {
101 + continue
102 + }
103 + if entry.Logger == "http.log.access.mixed" && entry.Request.Host == "MIXED.example.org" {
104 + return
105 + }
106 + }
107 + }
108 + t.Errorf("no access log entry from logger %q for host %q, got %q", "http.log.access.mixed", "MIXED.example.org", logs)
109 +}
modules/caddyhttp/app.go
+1
@@ -242,6 +242,7 @@ func (app *App) Provision(ctx caddy.Context) error {
242
243 // only enable access logs if configured
244 if srv.Logs != nil {
245 + srv.Logs.normalizeLoggerNames()
246 srv.accessLogger = app.logger.Named("log.access")
247 if srv.Logs.Trace {
248 srv.traceLogger = app.logger.Named("log.trace")
modules/caddyhttp/logging.go
+28
@@ -19,8 +19,10 @@ import (
19 "encoding/json"
20 "errors"
21 "log/slog"
22 + "maps"
23 "net"
24 "net/http"
25 + "slices"
26 "strings"
27 "sync"
28
@@ -133,6 +135,9 @@ func (slc ServerLogConfig) wrapLogger(logger *zap.Logger, req *http.Request) []*
135 }
136
137 func (slc ServerLogConfig) getLoggerHosts(host string) []string {
138 + // hostnames are case insensitive, keys were lowercased during provisioning
139 + host = strings.ToLower(host)
140 +
141 // try the exact hostname first
142 if hosts, ok := slc.LoggerNames[host]; ok {
143 return hosts
@@ -154,6 +159,29 @@ func (slc ServerLogConfig) getLoggerHosts(host string) []string {
159 return []string{slc.DefaultLoggerName}
160 }
161
162 +// normalizeLoggerNames lowercases the hostname keys of LoggerNames
163 +// because hostnames are case-insensitive. When two keys differ only
164 +// by case, their logger names are merged
165 +func (slc *ServerLogConfig) normalizeLoggerNames() {
166 + if len(slc.LoggerNames) == 0 {
167 + return
168 + }
169 + normalized := make(map[string]StringArray, len(slc.LoggerNames))
170 + // iterate in sorted order so merged logger names are deterministic
171 + for _, host := range slices.Sorted(maps.Keys(slc.LoggerNames)) {
172 + lowerHost := strings.ToLower(host)
173 + merged := normalized[lowerHost]
174 + for _, name := range slc.LoggerNames[host] {
175 + if !slices.Contains(merged, name) {
176 + merged = append(merged, name)
177 + }
178 + }
179 + // keep empty mappings, they disable logging for the host
180 + normalized[lowerHost] = merged
181 + }
182 + slc.LoggerNames = normalized
183 +}
184 +
185 func (slc *ServerLogConfig) clone() *ServerLogConfig {
186 clone := &ServerLogConfig{
187 DefaultLoggerName: slc.DefaultLoggerName,
modules/caddyhttp/logging_test.go new
+54
@@ -0,0 +1,54 @@
1 +// Copyright 2015 Matthew Holt and The Caddy Authors
2 +//
3 +// Licensed under the Apache License, Version 2.0 (the "License");
4 +// you may not use this file except in compliance with the License.
5 +// You may obtain a copy of the License at
6 +//
7 +// http://www.apache.org/licenses/LICENSE-2.0
8 +//
9 +// Unless required by applicable law or agreed to in writing, software
10 +// distributed under the License is distributed on an "AS IS" BASIS,
11 +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 +// See the License for the specific language governing permissions and
13 +// limitations under the License.
14 +
15 +package caddyhttp
16 +
17 +import (
18 + "slices"
19 + "testing"
20 +)
21 +
22 +func TestServerLogConfigLoggerNamesCaseInsensitive(t *testing.T) {
23 + slc := ServerLogConfig{
24 + DefaultLoggerName: "default",
25 + LoggerNames: map[string]StringArray{
26 + "example.com": {"exact"},
27 + "Mixed.Example.org": {"mixed"},
28 + "*.EXAMPLE.net": {"wildcard"},
29 + "foo.example.dev": {"lower"},
30 + "FOO.example.dev": {"upper", "lower"},
31 + "muted.example.com": {},
32 + },
33 + }
34 + slc.normalizeLoggerNames()
35 +
36 + for i, tc := range []struct {
37 + host string
38 + expected []string
39 + }{
40 + {host: "EXAMPLE.COM", expected: []string{"exact"}},
41 + {host: "mixed.example.org", expected: []string{"mixed"}},
42 + {host: "Sub.example.NET", expected: []string{"wildcard"}},
43 + // merged in sorted key order; "FOO..." sorts before "foo..."
44 + {host: "foo.example.dev", expected: []string{"upper", "lower"}},
45 + // mapped to no loggers, so no fallback to the default logger,
46 + // an empty mapping can only come from a JSON config
47 + {host: "MUTED.example.com", expected: []string{}},
48 + } {
49 + actual := slc.getLoggerHosts(tc.host)
50 + if !slices.Equal(actual, tc.expected) {
51 + t.Errorf("Test %d (%s): expected %v, got %v", i, tc.host, tc.expected, actual)
52 + }
53 + }
54 +}