You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

213 lines
5.5 KiB
Go

package main
import (
"errors"
"fmt"
"io"
"log"
"net"
"os"
"path/filepath"
"strings"
"syscall"
"unsafe"
)
type inotifyRequest struct {
filename string
key string
conn *net.UnixConn
}
type connection struct {
fd int
key string
connection *net.UnixConn
}
func readEvents(inotifyFd int, events chan string) {
defer syscall.Close(inotifyFd)
var buf [syscall.SizeofInotifyEvent * 4096]byte // Buffer for a maximum of 4096 raw events
for {
n, err := syscall.Read(inotifyFd, buf[:])
// If a signal interrupted execution, see if we've been asked to close, and try again.
// http://man7.org/linux/man-pages/man7/signal.7.html :
// "Before Linux 3.8, reads from an inotify(7) file descriptor were not restartable"
if errors.Is(err, syscall.EINTR) {
continue
}
if n < syscall.SizeofInotifyEvent {
if n == 0 {
log.Fatalf("notify: read EOF from inotify (cause: %v)", err)
} else if n < 0 {
log.Fatalf("notify: Received error while reading from inotify: %v", err)
} else {
log.Fatal("notify: short read in readEvents()")
}
continue
}
var offset uint32
for offset+syscall.SizeofInotifyEvent <= uint32(n) {
// Point "raw" to the event in the buffer
raw := (*syscall.InotifyEvent)(unsafe.Pointer(&buf[offset]))
mask := uint32(raw.Mask)
nameLen := uint32(raw.Len)
if mask&syscall.IN_Q_OVERFLOW != 0 {
// TODO Re-scan all files in this case
log.Fatal("Overflow in inotify")
}
if nameLen > 0 {
// Point "bytes" at the first byte of the filename
bytes := (*[syscall.PathMax]byte)(unsafe.Pointer(&buf[uintptr(offset)+unsafe.Offsetof(raw.Name)]))
// The filename is padded with NULL bytes. TrimRight() gets rid of those.
fname := strings.TrimRight(string(bytes[0:nameLen]), "\000")
log.Printf("Detected added file: %s", fname)
events <- fname
} else {
log.Printf("file added without length!?")
}
// Move to the next event in the buffer
offset += syscall.SizeofInotifyEvent + nameLen
}
}
}
func connFd(conn *net.UnixConn) (int, error) {
file, err := conn.File()
if err != nil {
return -1, err
}
return int(file.Fd()), nil
}
func (s *server) watch(inotifyFd int) {
connsForPath := make(map[string][]connection)
fdToPath := make(map[int]string)
fsEvents := make(chan string)
go readEvents(inotifyFd, fsEvents)
for {
select {
case req, ok := <-s.inotifyRequests:
if !ok {
return
}
fd, err := connFd(req.conn)
if err != nil {
log.Println("Received inotify request for closed connection")
continue
}
fdToPath[fd] = req.filename
conns, ok := connsForPath[req.filename]
if ok {
connsForPath[req.filename] = append(conns, connection{fd, req.key, req.conn})
continue
}
connsForPath[req.filename] = []connection{{fd, req.key, req.conn}}
case fname, ok := <-fsEvents:
if !ok {
return
}
conns := connsForPath[fname]
if conns == nil {
log.Printf("Ignore unknown file: %s", fname)
continue
}
delete(connsForPath, fname)
var secretMap map[string]interface{}
var err error
if isEnvironmentFile(fname) {
content, err := os.ReadFile(filepath.Join(s.SecretDir, fname))
if err != nil {
log.Printf("Failed to process service file: %v", err)
continue
}
secretMap = map[string]interface{}{fname: string(content)}
} else {
secretMap, err = parseServiceSecrets(filepath.Join(s.SecretDir, fname))
if err != nil {
log.Printf("Failed to process service file: %v", err)
continue
}
}
for _, conn := range conns {
defer delete(fdToPath, conn.fd)
if err == nil {
val, ok := secretMap[conn.key]
if !ok {
log.Printf("Secret map %s has no value for key %s", fname, conn.key)
continue
}
_, err = io.WriteString(conn.connection, fmt.Sprint(val))
if err == nil {
log.Printf("Served %s to %s", fname, conn.connection.RemoteAddr().String())
} else {
log.Printf("Failed to send secret: %v", err)
}
if err := s.epollDelete(conn.fd); err != nil && !errors.Is(err, syscall.ENOENT) {
log.Printf("failed to remove socket from epoll: %s", err)
}
if err := syscall.Shutdown(conn.fd, syscall.SHUT_RDWR); err != nil {
log.Printf("Failed to shutdown socket: %v", err)
}
} else {
log.Printf("Failed to open secret: %v", err)
}
}
case fd, ok := <-s.connectionClosed:
if !ok {
return
}
path := fdToPath[fd]
delete(fdToPath, fd)
conns := connsForPath[path]
if conns == nil {
// watcher has been already deregistered
continue
}
for idx, c := range conns {
if c.fd == fd {
last := len(conns) - 1
conns[idx] = conns[last]
conns = conns[:last]
c.connection.Close()
break
}
}
if len(conns) == 0 {
delete(connsForPath, path)
}
}
}
}
func (s *server) setupWatcher(dir string) error {
fd, err := syscall.InotifyInit1(syscall.IN_CLOEXEC)
if err != nil {
return fmt.Errorf("Failed to initialize inotify: %v", err)
}
flags := uint32(syscall.IN_CREATE | syscall.IN_MOVED_TO | syscall.IN_ONLYDIR)
// Allow processes to read files from this directory if they have the
// permissions on the files, but don't allow them to list files in it.
res := os.MkdirAll(dir, 0o711)
if err != nil && !os.IsNotExist(res) {
return fmt.Errorf("Failed to create secret directory: %v", err)
}
if _, err = syscall.InotifyAddWatch(fd, dir, flags); err != nil {
return fmt.Errorf("Failed to initialize inotify on secret directory %s: %v", dir, err)
}
go s.watch(fd)
return nil
}