fetchcert/server.go
Emmanuel BENOîT 3b7757b1d9
All checks were successful
Run tests and linters / test (push) Successful in 42s
Run tests and linters / build (push) Successful in 41s
Run tests and linters / lint (push) Successful in 1m18s
chore: add support for build automation (#5)
This PR seeks to solve #2.

It adds a Makefile that can build, test, lint and package the code and the Forgejo workflows needed to automate the build. In addition, it resolves various issues highlighted by the linter.

Reviewed-on: #5
Co-authored-by: Emmanuel BENOÎT <tseeker@nocternity.net>
Co-committed-by: Emmanuel BENOÎT <tseeker@nocternity.net>
2024-07-26 14:07:46 +02:00

229 lines
5.2 KiB
Go

package main
import (
"fmt"
"net"
"os"
"os/user"
"strconv"
"time"
"unicode/utf8"
"github.com/sirupsen/logrus"
)
type tCommandType int
const (
CMD_IGNORE tCommandType = iota
CMD_QUIT
CMD_RELOAD
CMD_UPDATE
)
type (
tCommand struct {
CommandType tCommandType
Force bool
Selector string
}
// The state of the main server
TServerState struct {
// The path to the configuration file
cfgFile string
// The configuration
config tConfiguration
// The UNIX socket listener
listener net.Listener
}
)
func configureSocket(cfg *tSocketConfig) error {
if cfg.Group != "" {
group, err := user.LookupGroup(cfg.Group)
if err != nil {
return fmt.Errorf("Group %s not found: %w", cfg.Group, err)
}
gid, err := strconv.Atoi(group.Gid)
if err != nil {
return fmt.Errorf("Group %s has non-numeric GID %s", cfg.Group, group.Gid)
}
err = os.Chown(cfg.Path, -1, gid)
if err != nil {
return fmt.Errorf("Cannot change group on UNIX socket: %w", err)
}
}
if cfg.Mode != 0 {
err := os.Chmod(cfg.Path, cfg.Mode)
if err != nil {
return fmt.Errorf("Cannot set UNIX socket access mode: %w", err)
}
}
return nil
}
func initSocket(cfg *tSocketConfig) (net.Listener, error) {
listener, err := net.Listen("unix", cfg.Path)
if err != nil {
return nil, fmt.Errorf("Cannot listen on UNIX socket at %s: %w", cfg.Path, err)
}
err = configureSocket(cfg)
if err != nil {
listener.Close()
return nil, err
}
log.WithField("path", cfg.Path).Info("UNIX socket created")
return listener, nil
}
func socketServer(cfg *tConfiguration, listener net.Listener) tCommandType {
for {
fd, err := listener.Accept()
if err != nil {
log.WithField("error", err).Fatal("Error while waiting for connections.")
}
cmd := executeFromSocket(cfg, fd)
if cmd != CMD_IGNORE {
return cmd
}
}
}
func executeUpdateFromSocket(cfg *tConfiguration, conn net.Conn, command *tCommand) {
log.WithFields(logrus.Fields{
"force": command.Force,
"selector": command.Selector,
}).Info("Update request received")
success := executeUpdate(cfg, command.Selector, command.Force)
if err := conn.SetWriteDeadline(time.Now().Add(1 * time.Second)); err != nil {
log.WithField("error", err).Error("Could not set the socket's write deadline")
return
}
var bval byte
if success {
bval = '1'
} else {
bval = '0'
}
if _, err := conn.Write([]byte{bval}); err != nil {
log.WithField("error", err).Error("Could not write result to socket")
return
}
}
func executeFromSocket(cfg *tConfiguration, conn net.Conn) tCommandType {
defer conn.Close()
log.Debug("Received connection")
buf := make([]byte, 512)
if err := conn.SetReadDeadline(time.Now().Add(1 * time.Second)); err != nil {
log.WithField("error", err).Error("Could not set the socket's read deadline")
return CMD_IGNORE
}
n, err := conn.Read(buf)
if err != nil {
log.WithField("error", err).Error("Could not read from socket")
return CMD_IGNORE
}
command := parseCommand(n, buf)
if command == nil {
return CMD_IGNORE
}
if command.CommandType == CMD_UPDATE {
executeUpdateFromSocket(cfg, conn, command)
return CMD_IGNORE
}
return command.CommandType
}
func parseCommand(n int, buf []byte) *tCommand {
if n == 512 {
log.Warn("Too much data received")
return nil
}
if n == 0 {
log.Warn("Not enough data received")
return nil
}
if n == 1 {
if buf[0] == 'Q' {
return &tCommand{CommandType: CMD_QUIT}
} else if buf[0] == 'R' {
return &tCommand{CommandType: CMD_RELOAD}
}
} else if n > 2 && buf[0] == 'U' {
res := &tCommand{CommandType: CMD_UPDATE}
if buf[1] == '!' {
res.Force = true
}
if utf8.Valid(buf[2:]) {
res.Selector = string(buf[2:n])
return res
}
}
log.Warn("Invalid command received")
return nil
}
// Initialize server state
func InitServer(cfgFile string, config tConfiguration) TServerState {
if config.Socket == nil {
log.Fatal("Cannot run in server mode without a socket configuration")
}
ss := TServerState{
cfgFile: cfgFile,
config: config,
}
listener, err := initSocket(ss.config.Socket)
if err != nil {
log.WithField("error", err).Fatal("Failed to initialize socket.")
}
ss.listener = listener
return ss
}
// Destroy the server
func (state *TServerState) Destroy() {
state.listener.Close()
}
// Server main loop. Processes commands received from connections. Certificate
// update requests are processed directly, but Quit/Reload commands are
// propagated back to this loop and handled here.
func (state *TServerState) MainLoop() {
for {
cmd := socketServer(&state.config, state.listener)
if cmd == CMD_QUIT {
break
} else if cmd != CMD_RELOAD {
continue
}
new_cfg, err := LoadConfiguration(state.cfgFile)
if err != nil {
log.WithField("error", err).Error("Failed to load updated configuration.")
continue
}
replace_ok := true
if new_cfg.Socket.Path != state.config.Socket.Path {
new_listener, err := initSocket(new_cfg.Socket)
if err != nil {
log.WithField("error", err).Error("Failed to initialize new server socket.")
replace_ok = false
} else {
state.listener.Close()
state.listener = new_listener
}
}
if replace_ok {
state.config = new_cfg
log.Info("Configuration reloaded")
}
}
}