// Mgmt
// Copyright (C) James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <https://www.gnu.org/licenses/>.
//
// Additional permission under GNU GPL version 3 section 7
//
// If you modify this program, or any covered work, by linking or combining it
// with embedded mcl code and modules (and that the embedded mcl code and
// modules which link with this program, contain a copy of their source code in
// the authoritative form) containing parts covered by the terms of any other
// license, the licensors of this program grant you additional permission to
// convey the resulting work. Furthermore, the licensors of this program grant
// the original author, James Shubin, additional permission to update this
// additional permission if he deems it necessary to achieve the goals of this
// additional permission.

//go:build !darwin

// Package socketset is in API for creating a select style netlink socket to
// receive events from the kernel.
package socketset

import (
	"bytes"
	"fmt"
	"os"
	"path"
	"strings"
	"syscall"

	"github.com/purpleidea/mgmt/util/errwrap"

	"golang.org/x/sys/unix"
)

// socketRcvbufSize is the size in bytes we request for the netlink socket
// receive buffer. A larger buffer helps avoid dropping events (and the
// resulting ENOBUFS errors) during bursts of netlink messages. The kernel may
// double this value for bookkeeping, and will cap it at net.core.rmem_max
// unless the SO_RCVBUFFORCE option (which needs CAP_NET_ADMIN) is used.
const socketRcvbufSize = 8 * 1024 * 1024 // 8 MiB

// SocketSet is used to receive events from a socket and shut it down cleanly
// when asked. It contains a socket for events and a pipe socket to unblock
// receive on shutdown.
type SocketSet struct {
	fdEvents int
	fdPipe   int
	pipeFile string
}

// NewSocketSet returns a socketSet, initialized with the given parameters.
func NewSocketSet(groups uint32, name string, proto int) (*SocketSet, error) {
	fdEvents, err := unix.Socket(unix.AF_NETLINK, unix.SOCK_RAW, proto)
	if err != nil {
		return nil, errwrap.Wrapf(err, "error creating netlink socket")
	}

	// bind to the socket and add add the netlink groups we need to get events
	if err := unix.Bind(fdEvents, &unix.SockaddrNetlink{
		Family: unix.AF_NETLINK,
		Groups: groups,
		//nolint:gosec // G115: a process ID is always non-negative and fits
		Pid: uint32(os.Getpid()), // set PID to our process
	}); err != nil {
		return nil, errwrap.Wrapf(err, "error binding netlink socket")
	}

	// Increase the size of the socket receive buffer to help avoid dropping
	// events (and getting ENOBUFS) when a large burst of messages arrives
	// faster than we can read them. We try SO_RCVBUFFORCE first since it
	// ignores the rmem_max limit (but needs CAP_NET_ADMIN) and fall back to
	// the regular SO_RCVBUF which is capped by rmem_max. This is
	// best-effort, so we ignore any errors and carry on with the default
	// buffer size.
	if err := unix.SetsockoptInt(fdEvents, unix.SOL_SOCKET, unix.SO_RCVBUFFORCE, socketRcvbufSize); err != nil {
		// ignore the error and fall back to the non-privileged option
		_ = unix.SetsockoptInt(fdEvents, unix.SOL_SOCKET, unix.SO_RCVBUF, socketRcvbufSize)
	}

	// this pipe unblocks unix.Select upon closing
	fdPipe, err := unix.Socket(unix.AF_UNIX, unix.SOCK_RAW, unix.PROT_NONE)
	if err != nil {
		return nil, errwrap.Wrapf(err, "error creating pipe socket")
	}

	// bind the pipe to a file
	if err = unix.Bind(fdPipe, &unix.SockaddrUnix{
		Name: name,
	}); err != nil {
		return nil, errwrap.Wrapf(err, "error binding pipe socket")
	}

	return &SocketSet{
		fdEvents: fdEvents,
		fdPipe:   fdPipe,
		pipeFile: name,
	}, nil
}

// ReceiveBytes waits for bytes from fdEvents and return a byte array truncated
// to the message length. It will block until an event is produced, or shutdown
// is called.
func (obj *SocketSet) ReceiveBytes() ([]byte, error) {
	if obj == nil {
		return nil, fmt.Errorf("nil object") // caller used this incorrectly
	}

	// Select will return when any fd in fdSet (fdEvents and fdPipe) is ready
	// to read.
	_, err := unix.Select(obj.nfd(), obj.fdSet(), nil, nil, nil)
	if err != nil {
		// if a system interrupt is caught
		if err == unix.EINTR { // signal interrupt
			return nil, nil
		}
		return nil, errwrap.Wrapf(err, "error selecting on fd")
	}
	// receive the message from the netlink socket into b
	b := make([]byte, os.Getpagesize())
	n, _, err := unix.Recvfrom(obj.fdEvents, b, unix.MSG_DONTWAIT) // non-blocking receive
	if err != nil {
		// if fdEvents is closed
		if err == unix.EBADF { // bad file descriptor
			return nil, nil
		}
		// The kernel ran out of buffer space and dropped one or more
		// events. This is recoverable: the socket is still usable, we
		// have just lost some messages. Return empty (like the EINTR
		// case) so the caller resyncs its state on the next pass rather
		// than treating this as a fatal error.
		if err == unix.ENOBUFS { // no buffer space available
			return nil, nil
		}
		return nil, errwrap.Wrapf(err, "error receiving messages")
	}
	// if we didn't get enough bytes for a header, something went wrong
	if n < unix.NLMSG_HDRLEN {
		return nil, fmt.Errorf("received short header")
	}
	b = b[:n] // truncate b to message length
	return b, nil
}

// ReceiveNetlinkMessages is a wrapper around ReceiveBytes that parses the bytes
// from the event and returns an array of NetlinkMessages.
func (obj *SocketSet) ReceiveNetlinkMessages() ([]syscall.NetlinkMessage, error) {
	msgBytes, err := obj.ReceiveBytes()
	if err != nil {
		return nil, err
	}
	// use syscall to parse, as func does not exist in x/sys/unix
	return syscall.ParseNetlinkMessage(msgBytes)
}

// UEvent struct has attributes for KOBJECT_NETWORK_UEVENT, passed from the
// kernel.
type UEvent struct {
	// default keys, as per https://github.com/torvalds/linux/blob/master/lib/kobject_uevent.c
	Action    string
	Devpath   string
	Subsystem string

	// every other KV pair
	Data map[string]string
}

// ReceiveUEvent is a wrapper around ReceiveBytes. Parses the UEvent data
// received from the socket and puts it into a UEvent struct.
func (obj *SocketSet) ReceiveUEvent() (*UEvent, error) {
	// TODO: can multiple events come in the same socket?
	event := &UEvent{Data: map[string]string{}}

	msgBytes, err := obj.ReceiveBytes()
	if err != nil {
		return nil, err
	}

	submsg := msgBytes[:]
	i := 0
Loop:
	for {
		submsg = submsg[i:]
		n := bytes.IndexByte(submsg, 0x0)
		if n == -1 {
			break Loop
		}
		i = n + 1

		attrLine := string(submsg[:n])
		split := strings.SplitN(attrLine, "=", 2)
		if len(split) < 2 {
			continue
		}
		switch split[0] {
		case "ACTION":
			event.Action = split[1]
		case "DEVPATH":
			event.Devpath = split[1]
		case "SUBSYSTEM":
			event.Subsystem = split[1]
		default:
			event.Data[split[0]] = split[1]
		}
	}

	return event, nil
}

// Shutdown closes the event file descriptor and unblocks receive by sending a
// message to the pipe file descriptor. It must be called before close, and
// should only be called once.
func (obj *SocketSet) Shutdown() error {
	if obj == nil {
		return fmt.Errorf("nil object") // caller used this incorrectly
	}

	// close the event socket so no more events are produced
	if err := unix.Close(obj.fdEvents); err != nil {
		return err
	}
	// send a message to the pipe to unblock select
	return unix.Sendto(obj.fdPipe, nil, 0, &unix.SockaddrUnix{
		Name: path.Join(obj.pipeFile),
	})
}

// Close closes the pipe file descriptor. It must only be called after Shutdown
// has closed fdEvents, and unblocked receive. It should only be called once.
func (obj *SocketSet) Close() error {
	if obj == nil {
		return fmt.Errorf("nil object") // caller used this incorrectly
	}

	if err := unix.Unlink(obj.pipeFile); err != nil {
		return errwrap.Wrapf(err, "could not unbind %s", obj.pipeFile)
	}
	return unix.Close(obj.fdPipe)
}

// nfd returns one more than the highest fd value in the struct, for use as as
// the nfds parameter in select. It represents the file descriptor set maximum
// size. See man select for more info.
func (obj *SocketSet) nfd() int {
	if obj.fdEvents > obj.fdPipe {
		return obj.fdEvents + 1
	}
	return obj.fdPipe + 1
}

// fdSet returns a bitmask representation of the integer values of fdEvents and
// fdPipe. See man select for more info.
func (obj *SocketSet) fdSet() *unix.FdSet {
	fdSet := &unix.FdSet{}
	// Generate the bitmask representing the file descriptors in the SocketSet.
	// The rightmost bit corresponds to file descriptor zero, and each bit to
	// the left represents the next file descriptor number in the sequence of
	// all real numbers. E.g. the FdSet containing containing 0 and 4 is 10001.
	fdSet.Bits[obj.fdEvents/64] |= 1 << uint(obj.fdEvents)
	fdSet.Bits[obj.fdPipe/64] |= 1 << uint(obj.fdPipe)
	return fdSet
}
