Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS
Mirrored from https://github.com/Slazee/caddy.git
Synced Nov 12, 10:45 UTC
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.
Ririsoft committed
Jun 6, 2024 at 16:33 UTC
101d3e740783581110340a68f0b0cbe5f1ab6dbb
3 files changed
+426
-2
modules/logging/filewriter.go
+63
-2
@@ -15,6 +15,7 @@
15
package logging
16
17
import (
18
+ "encoding/json"
19
"fmt"
20
"io"
21
"math"
@@ -33,6 +34,43 @@ func init() {
34
caddy.RegisterModule(FileWriter{})
35
}
36
37
+// fileMode is a string made of 1 to 4 octal digits representing
38
+// a numeric mode as specified with the `chmod` unix command.
39
+// `"0777"` and `"777"` are thus equivalent values.
40
+type fileMode os.FileMode
41
+
42
+// UnmarshalJSON satisfies json.Unmarshaler.
43
+func (m *fileMode) UnmarshalJSON(b []byte) error {
44
+ if len(b) == 0 {
45
+ return io.EOF
46
+ }
47
+
48
+ var s string
49
+ if err := json.Unmarshal(b, &s); err != nil {
50
+ return err
51
+ }
52
+
53
+ mode, err := parseFileMode(s)
54
+ if err != nil {
55
+ return err
56
+ }
57
+
58
+ *m = fileMode(mode)
59
+ return err
60
+}
61
+
62
+// parseFileMode parses a file mode string,
63
+// adding support for `chmod` unix command like
64
+// 1 to 4 digital octal values.
65
+func parseFileMode(s string) (os.FileMode, error) {
66
+ modeStr := fmt.Sprintf("%04s", s)
67
+ mode, err := strconv.ParseUint(modeStr, 8, 32)
68
+ if err != nil {
69
+ return 0, err
70
+ }
71
+ return os.FileMode(mode), nil
72
+}
73
+
74
// FileWriter can write logs to files. By default, log files
75
// are rotated ("rolled") when they get large, and old log
76
// files get deleted, to ensure that the process does not
@@ -41,6 +79,10 @@ type FileWriter struct {
79
// Filename is the name of the file to write.
80
Filename string `json:"filename,omitempty"`
81
82
+ // The file permissions mode.
83
+ // 0600 by default.
84
+ Mode fileMode `json:"mode,omitempty"`
85
+
86
// Roll toggles log rolling or rotation, which is
87
// enabled by default.
88
Roll *bool `json:"roll,omitempty"`
@@ -100,6 +142,10 @@ func (fw FileWriter) WriterKey() string {
142
143
// OpenWriter opens a new file writer.
144
func (fw FileWriter) OpenWriter() (io.WriteCloser, error) {
145
+ if fw.Mode == 0 {
146
+ fw.Mode = 0o600
147
+ }
148
+
149
// roll log files by default
150
if fw.Roll == nil || *fw.Roll {
151
if fw.RollSizeMB == 0 {
@@ -116,6 +162,9 @@ func (fw FileWriter) OpenWriter() (io.WriteCloser, error) {
162
fw.RollKeepDays = 90
163
}
164
165
+ f_tmp, _ := os.OpenFile(fw.Filename, os.O_WRONLY|os.O_APPEND|os.O_CREATE, os.FileMode(fw.Mode))
166
+ f_tmp.Close()
167
+
168
return &lumberjack.Logger{
169
Filename: fw.Filename,
170
MaxSize: fw.RollSizeMB,
@@ -127,12 +176,13 @@ func (fw FileWriter) OpenWriter() (io.WriteCloser, error) {
176
}
177
178
// otherwise just open a regular file
130
- return os.OpenFile(fw.Filename, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0o666)
179
+ return os.OpenFile(fw.Filename, os.O_WRONLY|os.O_APPEND|os.O_CREATE, os.FileMode(fw.Mode))
180
}
181
182
// UnmarshalCaddyfile sets up the module from Caddyfile tokens. Syntax:
183
//
184
// file <filename> {
185
+// mode <mode>
186
// roll_disabled
187
// roll_size <size>
188
// roll_uncompressed
@@ -150,7 +200,7 @@ func (fw FileWriter) OpenWriter() (io.WriteCloser, error) {
200
// The roll_keep_for duration has day resolution.
201
// Fractional values are rounded up to the next whole number of days.
202
//
153
-// If any of the roll_size, roll_keep, or roll_keep_for subdirectives are
203
+// If any of the mode, roll_size, roll_keep, or roll_keep_for subdirectives are
204
// omitted or set to a zero value, then Caddy's default value for that
205
// subdirective is used.
206
func (fw *FileWriter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
@@ -165,6 +215,17 @@ func (fw *FileWriter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
215
216
for d.NextBlock(0) {
217
switch d.Val() {
218
+ case "mode":
219
+ var modeStr string
220
+ if !d.AllArgs(&modeStr) {
221
+ return d.ArgErr()
222
+ }
223
+ mode, err := parseFileMode(modeStr)
224
+ if err != nil {
225
+ return d.Errf("parsing mode: %v", err)
226
+ }
227
+ fw.Mode = fileMode(mode)
228
+
229
case "roll_disabled":
230
var f bool
231
fw.Roll = &f
modules/logging/filewriter_test.go
new
+308
@@ -0,0 +1,308 @@
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
+//go:build !windows
16
+
17
+package logging
18
+
19
+import (
20
+ "encoding/json"
21
+ "os"
22
+ "path"
23
+ "syscall"
24
+ "testing"
25
+
26
+ "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
27
+)
28
+
29
+func TestFileCreationMode(t *testing.T) {
30
+ on := true
31
+ off := false
32
+
33
+ tests := []struct {
34
+ name string
35
+ fw FileWriter
36
+ wantMode os.FileMode
37
+ }{
38
+ {
39
+ name: "default mode no roll",
40
+ fw: FileWriter{
41
+ Roll: &off,
42
+ },
43
+ wantMode: 0o600,
44
+ },
45
+ {
46
+ name: "default mode roll",
47
+ fw: FileWriter{
48
+ Roll: &on,
49
+ },
50
+ wantMode: 0o600,
51
+ },
52
+ {
53
+ name: "custom mode no roll",
54
+ fw: FileWriter{
55
+ Roll: &off,
56
+ Mode: 0o666,
57
+ },
58
+ wantMode: 0o666,
59
+ },
60
+ {
61
+ name: "custom mode roll",
62
+ fw: FileWriter{
63
+ Roll: &on,
64
+ Mode: 0o666,
65
+ },
66
+ wantMode: 0o666,
67
+ },
68
+ }
69
+
70
+ m := syscall.Umask(0o000)
71
+ defer syscall.Umask(m)
72
+
73
+ for _, tt := range tests {
74
+ t.Run(tt.name, func(t *testing.T) {
75
+ dir, err := os.MkdirTemp("", "caddytest")
76
+ if err != nil {
77
+ t.Fatalf("failed to create tempdir: %v", err)
78
+ }
79
+ defer os.RemoveAll(dir)
80
+ fpath := path.Join(dir, "test.log")
81
+ tt.fw.Filename = fpath
82
+
83
+ logger, err := tt.fw.OpenWriter()
84
+ if err != nil {
85
+ t.Fatalf("failed to create file: %v", err)
86
+ }
87
+ defer logger.Close()
88
+
89
+ st, err := os.Stat(fpath)
90
+ if err != nil {
91
+ t.Fatalf("failed to check file permissions: %v", err)
92
+ }
93
+
94
+ if st.Mode() != tt.wantMode {
95
+ t.Errorf("file mode is %v, want %v", st.Mode(), tt.wantMode)
96
+ }
97
+ })
98
+ }
99
+}
100
+
101
+func TestFileRotationPreserveMode(t *testing.T) {
102
+ m := syscall.Umask(0o000)
103
+ defer syscall.Umask(m)
104
+
105
+ dir, err := os.MkdirTemp("", "caddytest")
106
+ if err != nil {
107
+ t.Fatalf("failed to create tempdir: %v", err)
108
+ }
109
+ defer os.RemoveAll(dir)
110
+
111
+ fpath := path.Join(dir, "test.log")
112
+
113
+ roll := true
114
+ mode := fileMode(0o640)
115
+ fw := FileWriter{
116
+ Filename: fpath,
117
+ Mode: mode,
118
+ Roll: &roll,
119
+ RollSizeMB: 1,
120
+ }
121
+
122
+ logger, err := fw.OpenWriter()
123
+ if err != nil {
124
+ t.Fatalf("failed to create file: %v", err)
125
+ }
126
+ defer logger.Close()
127
+
128
+ b := make([]byte, 1024*1024-1000)
129
+ logger.Write(b)
130
+ logger.Write(b[0:2000])
131
+
132
+ files, err := os.ReadDir(dir)
133
+ if err != nil {
134
+ t.Fatalf("failed to read temporary log dir: %v", err)
135
+ }
136
+
137
+ // We might get 2 or 3 files depending
138
+ // on the race between compressed log file generation,
139
+ // removal of the non compressed file and reading the directory.
140
+ // Ordering of the files are [ test-*.log test-*.log.gz test.log ]
141
+ if len(files) < 2 || len(files) > 3 {
142
+ t.Log("got files: ", files)
143
+ t.Fatalf("got %v files want 2", len(files))
144
+ }
145
+
146
+ wantPattern := "test-*-*-*-*-*.*.log"
147
+ test_date_log := files[0]
148
+ if m, _ := path.Match(wantPattern, test_date_log.Name()); m != true {
149
+ t.Fatalf("got %v filename want %v", test_date_log.Name(), wantPattern)
150
+ }
151
+
152
+ st, err := os.Stat(path.Join(dir, test_date_log.Name()))
153
+ if err != nil {
154
+ t.Fatalf("failed to check file permissions: %v", err)
155
+ }
156
+
157
+ if st.Mode() != os.FileMode(mode) {
158
+ t.Errorf("file mode is %v, want %v", st.Mode(), mode)
159
+ }
160
+
161
+ test_dot_log := files[len(files)-1]
162
+ if test_dot_log.Name() != "test.log" {
163
+ t.Fatalf("got %v filename want test.log", test_dot_log.Name())
164
+ }
165
+
166
+ st, err = os.Stat(path.Join(dir, test_dot_log.Name()))
167
+ if err != nil {
168
+ t.Fatalf("failed to check file permissions: %v", err)
169
+ }
170
+
171
+ if st.Mode() != os.FileMode(mode) {
172
+ t.Errorf("file mode is %v, want %v", st.Mode(), mode)
173
+ }
174
+}
175
+
176
+func TestFileModeConfig(t *testing.T) {
177
+ tests := []struct {
178
+ name string
179
+ d *caddyfile.Dispenser
180
+ fw FileWriter
181
+ wantErr bool
182
+ }{
183
+ {
184
+ name: "set mode",
185
+ d: caddyfile.NewTestDispenser(`
186
+file test.log {
187
+ mode 0666
188
+}
189
+`),
190
+ fw: FileWriter{
191
+ Mode: 0o666,
192
+ },
193
+ wantErr: false,
194
+ },
195
+ {
196
+ name: "set mode 3 digits",
197
+ d: caddyfile.NewTestDispenser(`
198
+file test.log {
199
+ mode 666
200
+}
201
+`),
202
+ fw: FileWriter{
203
+ Mode: 0o666,
204
+ },
205
+ wantErr: false,
206
+ },
207
+ {
208
+ name: "set mode 2 digits",
209
+ d: caddyfile.NewTestDispenser(`
210
+file test.log {
211
+ mode 66
212
+}
213
+`),
214
+ fw: FileWriter{
215
+ Mode: 0o066,
216
+ },
217
+ wantErr: false,
218
+ },
219
+ {
220
+ name: "set mode 1 digits",
221
+ d: caddyfile.NewTestDispenser(`
222
+file test.log {
223
+ mode 6
224
+}
225
+`),
226
+ fw: FileWriter{
227
+ Mode: 0o006,
228
+ },
229
+ wantErr: false,
230
+ },
231
+ {
232
+ name: "invalid mode",
233
+ d: caddyfile.NewTestDispenser(`
234
+file test.log {
235
+ mode foobar
236
+}
237
+`),
238
+ fw: FileWriter{},
239
+ wantErr: true,
240
+ },
241
+ }
242
+
243
+ for _, tt := range tests {
244
+ t.Run(tt.name, func(t *testing.T) {
245
+ fw := &FileWriter{}
246
+ if err := fw.UnmarshalCaddyfile(tt.d); (err != nil) != tt.wantErr {
247
+ t.Fatalf("UnmarshalCaddyfile() error = %v, want %v", err, tt.wantErr)
248
+ }
249
+ if fw.Mode != tt.fw.Mode {
250
+ t.Errorf("got mode %v, want %v", fw.Mode, tt.fw.Mode)
251
+ }
252
+ })
253
+ }
254
+}
255
+
256
+func TestFileModeJSON(t *testing.T) {
257
+ tests := []struct {
258
+ name string
259
+ config string
260
+ fw FileWriter
261
+ wantErr bool
262
+ }{
263
+ {
264
+ name: "set mode",
265
+ config: `
266
+{
267
+ "mode": "0666"
268
+}
269
+`,
270
+ fw: FileWriter{
271
+ Mode: 0o666,
272
+ },
273
+ wantErr: false,
274
+ },
275
+ {
276
+ name: "set mode invalid value",
277
+ config: `
278
+{
279
+ "mode": "0x666"
280
+}
281
+`,
282
+ fw: FileWriter{},
283
+ wantErr: true,
284
+ },
285
+ {
286
+ name: "set mode invalid string",
287
+ config: `
288
+{
289
+ "mode": 777
290
+}
291
+`,
292
+ fw: FileWriter{},
293
+ wantErr: true,
294
+ },
295
+ }
296
+
297
+ for _, tt := range tests {
298
+ t.Run(tt.name, func(t *testing.T) {
299
+ fw := &FileWriter{}
300
+ if err := json.Unmarshal([]byte(tt.config), fw); (err != nil) != tt.wantErr {
301
+ t.Fatalf("UnmarshalJSON() error = %v, want %v", err, tt.wantErr)
302
+ }
303
+ if fw.Mode != tt.fw.Mode {
304
+ t.Errorf("got mode %v, want %v", fw.Mode, tt.fw.Mode)
305
+ }
306
+ })
307
+ }
308
+}
modules/logging/filewriter_test_windows.go
new
+55
@@ -0,0 +1,55 @@
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
+//go:build windows
16
+
17
+package logging
18
+
19
+import (
20
+ "os"
21
+ "path"
22
+ "testing"
23
+)
24
+
25
+// Windows relies on ACLs instead of unix permissions model.
26
+// Go allows to open files with a particular mode put it is limited to read or write.
27
+// See https://cs.opensource.google/go/go/+/refs/tags/go1.22.3:src/syscall/syscall_windows.go;l=708.
28
+// This is pretty restrictive and has few interest for log files and thus we just test that log files are
29
+// opened with R/W permissions by default on Windows too.
30
+func TestFileCreationMode(t *testing.T) {
31
+ dir, err := os.MkdirTemp("", "caddytest")
32
+ if err != nil {
33
+ t.Fatalf("failed to create tempdir: %v", err)
34
+ }
35
+ defer os.RemoveAll(dir)
36
+
37
+ fw := &FileWriter{
38
+ Filename: path.Join(dir, "test.log"),
39
+ }
40
+
41
+ logger, err := fw.OpenWriter()
42
+ if err != nil {
43
+ t.Fatalf("failed to create file: %v", err)
44
+ }
45
+ defer logger.Close()
46
+
47
+ st, err := os.Stat(fw.Filename)
48
+ if err != nil {
49
+ t.Fatalf("failed to check file permissions: %v", err)
50
+ }
51
+
52
+ if st.Mode().Perm()&0o600 != 0o600 {
53
+ t.Fatalf("file mode is %v, want rw for user", st.Mode().Perm())
54
+ }
55
+}