@marcel91200 / caddy Mirror · read-only / commits / 101d3e74
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 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 +}