@marcel91200 / caddy Mirror · read-only / commits / 22927e27
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 13, 01:45 UTC

core: Add optional unix socket file permissions (#4741)

* core: Add optional unix socket file permissions This commit also changes the default unix socket file permissions to `u=w,g=,o=` (octal: `0200`). It used to default to the shell's umask (usually `u=rwx,g=rx,o=rx`, octal: `0755`). `/run/caddy.sock` -> `/run/caddy.sock` with `0200` default perms `/run/caddy.sock|0222` -> `/run/caddy.sock` with `0222` perms `|` instead of `:` is used as a separator, to account for the `:` in Windows drive letters (e.g. `C:\absolute\path.sock`) Fun fact: The old unix(7) man page (pre Jun 2016) stated a socket needs both read and write perms. Turns out, only write perms are needed. Corrected in https://github.com/mkerrisk/man-pages/commit/7578ea2f85b272363d22680d69e7d32f0b59c83b Despite this, most implementations still default to read+write to this date. * Add cases with Windows paths to test * Require write perms for the owning user

Emily committed Jun 23, 2023 at 22:49 UTC 22927e278dc29c9d1804c20f483510ec569f23ed
2 files changed +156 -3
listeners.go
+61 -3
@@ -20,6 +20,7 @@ import (
20 "errors"
21 "fmt"
22 "io"
23 + "io/fs"
24 "net"
25 "net/netip"
26 "os"
@@ -148,11 +149,27 @@ func (na NetworkAddress) Listen(ctx context.Context, portOffset uint, config net
149 func (na NetworkAddress) listen(ctx context.Context, portOffset uint, config net.ListenConfig) (any, error) {
150 var ln any
151 var err error
152 + var address string
153 + var unixFileMode fs.FileMode
154
152 - address := na.JoinHostPort(portOffset)
155 + // split unix socket addr early so lnKey
156 + // is independent of permissions bits
157 + if na.IsUnixNetwork() {
158 + var err error
159 + address, unixFileMode, err = splitUnixSocketPermissionsBits(na.Host)
160 + if err != nil {
161 + return nil, err
162 + }
163 + } else {
164 + address = na.JoinHostPort(portOffset)
165 + }
166
154 - // if this is a unix socket, see if we already have it open
167 + // if this is a unix socket, see if we already have it open,
168 + // force socket permissions on it and return early
169 if socket, err := reuseUnixSocket(na.Network, address); socket != nil || err != nil {
170 + if err := os.Chmod(address, unixFileMode); err != nil {
171 + return nil, fmt.Errorf("unable to set permissions (%s) on %s: %v", unixFileMode, address, err)
172 + }
173 return socket, err
174 }
175
@@ -193,6 +210,12 @@ func (na NetworkAddress) listen(ctx context.Context, portOffset uint, config net
210 unixSockets[lnKey] = unix
211 }
212
213 + if IsUnixNetwork(na.Network) {
214 + if err := os.Chmod(address, unixFileMode); err != nil {
215 + return nil, fmt.Errorf("unable to set permissions (%s) on %s: %v", unixFileMode, address, err)
216 + }
217 + }
218 +
219 return ln, nil
220 }
221
@@ -288,6 +311,40 @@ func IsUnixNetwork(netw string) bool {
311 return strings.HasPrefix(netw, "unix")
312 }
313
314 +// Takes a unix socket address in the unusual "path|bits" format
315 +// (e.g. /run/caddy.sock|0222) and tries to split it into
316 +// socket path (host) and permissions bits (port). Colons (":")
317 +// can't be used as separator, as socket paths on Windows may
318 +// include a drive letter (e.g. `unix/c:\absolute\path.sock`).
319 +// Permission bits will default to 0200 if none are specified.
320 +// Throws an error, if the first carrying bit does not
321 +// include write perms (e.g. `0422` or `022`).
322 +// Symbolic permission representation (e.g. `u=w,g=w,o=w`)
323 +// is not supported and will throw an error for now!
324 +func splitUnixSocketPermissionsBits(addr string) (path string, fileMode fs.FileMode, err error) {
325 + addrSplit := strings.SplitN(addr, "|", 2)
326 +
327 + if len(addrSplit) == 2 {
328 + // parse octal permission bit string as uint32
329 + fileModeUInt64, err := strconv.ParseUint(addrSplit[1], 8, 32)
330 + if err != nil {
331 + return "", 0, fmt.Errorf("could not parse octal permission bits in %s: %v", addr, err)
332 + }
333 + fileMode = fs.FileMode(fileModeUInt64)
334 +
335 + // FileMode.String() returns a string like `-rwxr-xr--` for `u=rwx,g=rx,o=r` (`0754`)
336 + if string(fileMode.String()[2]) != "w" {
337 + return "", 0, fmt.Errorf("owner of the socket requires '-w-' (write, octal: '2') permissions at least; got '%s' in %s", fileMode.String()[1:4], addr)
338 + }
339 +
340 + return addrSplit[0], fileMode, nil
341 + }
342 +
343 + // default to 0200 (symbolic: `u=w,g=,o=`)
344 + // if no permission bits are specified
345 + return addr, 0200, nil
346 +}
347 +
348 // ParseNetworkAddress parses addr into its individual
349 // components. The input string is expected to be of
350 // the form "network/host:port-range" where any part is
@@ -312,10 +369,11 @@ func ParseNetworkAddressWithDefaults(addr, defaultNetwork string, defaultPort ui
369 network = defaultNetwork
370 }
371 if IsUnixNetwork(network) {
372 + _, _, err := splitUnixSocketPermissionsBits(host)
373 return NetworkAddress{
374 Network: network,
375 Host: host,
318 - }, nil
376 + }, err
377 }
378 var start, end uint64
379 if port == "" {
listeners_test.go
+95
@@ -555,3 +555,98 @@ func TestExpand(t *testing.T) {
555 }
556 }
557 }
558 +
559 +func TestSplitUnixSocketPermissionsBits(t *testing.T) {
560 + for i, tc := range []struct {
561 + input string
562 + expectNetwork string
563 + expectPath string
564 + expectFileMode string
565 + expectErr bool
566 + }{
567 + {
568 + input: "./foo.socket",
569 + expectPath: "./foo.socket",
570 + expectFileMode: "--w-------",
571 + },
572 + {
573 + input: `.\relative\path.socket`,
574 + expectPath: `.\relative\path.socket`,
575 + expectFileMode: "--w-------",
576 + },
577 + {
578 + // literal colon in resulting address
579 + // and defaulting to 0200 bits
580 + input: "./foo.socket:0666",
581 + expectPath: "./foo.socket:0666",
582 + expectFileMode: "--w-------",
583 + },
584 + {
585 + input: "./foo.socket|0220",
586 + expectPath: "./foo.socket",
587 + expectFileMode: "--w--w----",
588 + },
589 + {
590 + input: "/var/run/foo|222",
591 + expectPath: "/var/run/foo",
592 + expectFileMode: "--w--w--w-",
593 + },
594 + {
595 + input: "./foo.socket|0660",
596 + expectPath: "./foo.socket",
597 + expectFileMode: "-rw-rw----",
598 + },
599 + {
600 + input: "./foo.socket|0666",
601 + expectPath: "./foo.socket",
602 + expectFileMode: "-rw-rw-rw-",
603 + },
604 + {
605 + input: "/var/run/foo|666",
606 + expectPath: "/var/run/foo",
607 + expectFileMode: "-rw-rw-rw-",
608 + },
609 + {
610 + input: `c:\absolute\path.socket|220`,
611 + expectPath: `c:\absolute\path.socket`,
612 + expectFileMode: "--w--w----",
613 + },
614 + {
615 + // symbolic permission representation is not supported for now
616 + input: "./foo.socket|u=rw,g=rw,o=rw",
617 + expectErr: true,
618 + },
619 + {
620 + // octal (base-8) permission representation has to be between
621 + // `0` for no read, no write, no exec (`---`) and
622 + // `7` for read (4), write (2), exec (1) (`rwx` => `4+2+1 = 7`)
623 + input: "./foo.socket|888",
624 + expectErr: true,
625 + },
626 + {
627 + // too many colons in address
628 + input: "./foo.socket|123456|0660",
629 + expectErr: true,
630 + },
631 + {
632 + // owner is missing write perms
633 + input: "./foo.socket|0522",
634 + expectErr: true,
635 + },
636 + } {
637 + actualPath, actualFileMode, err := splitUnixSocketPermissionsBits(tc.input)
638 + if tc.expectErr && err == nil {
639 + t.Errorf("Test %d: Expected error but got: %v", i, err)
640 + }
641 + if !tc.expectErr && err != nil {
642 + t.Errorf("Test %d: Expected no error but got: %v", i, err)
643 + }
644 + if actualPath != tc.expectPath {
645 + t.Errorf("Test %d: Expected path '%s' but got '%s'", i, tc.expectPath, actualPath)
646 + }
647 + // fileMode.Perm().String() parses 0 to "----------"
648 + if !tc.expectErr && actualFileMode.Perm().String() != tc.expectFileMode {
649 + t.Errorf("Test %d: Expected perms '%s' but got '%s'", i, tc.expectFileMode, actualFileMode.Perm().String())
650 + }
651 + }
652 +}