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
+}