Client mode

* Refactored so that all server code is in a single file
  * Added missing CLI option to send reload/quit commands to the server
  * Implemented client mode
This commit is contained in:
Emmanuel BENOîT 2021-12-04 18:31:19 +01:00
parent cd295e51ba
commit be6198dbed
3 changed files with 196 additions and 92 deletions

84
client.go Normal file
View file

@ -0,0 +1,84 @@
package main
import (
"net"
"github.com/sirupsen/logrus"
)
// Client runtime data
type TClient struct {
config tConfiguration
}
// Initialize the client's state.
func InitClient(config tConfiguration) TClient {
return TClient{
config: config,
}
}
// Connect to the UNIX socket. Terminate the program with an error if connection
// fails.
func (c *TClient) getConnection() net.Conn {
conn, err := net.Dial("unix", c.config.Socket.Path)
if err != nil {
log.WithFields(logrus.Fields{
"error": err,
"path": c.config.Socket.Path,
}).Fatal("Could not connect to the UNIX socket")
}
return conn
}
// Send a string to the UNIX socket. Terminate the program with an error if
// some form of IO error occurs.
func (c *TClient) send(conn net.Conn, data string) {
_, err := conn.Write([]byte(data))
if err != nil {
log.WithFields(logrus.Fields{
"error": err,
"path": c.config.Socket.Path,
"data": data,
}).Fatal("Could not write to the UNIX socket")
}
}
// Send a command to the server then disconnect.
func (c *TClient) SendCommand(command string) {
conn := c.getConnection()
defer conn.Close()
c.send(conn, command)
}
// Request an update by sending the selector and force flag to the server, then
// wait for the server to respond. Returns true if the server responded that the
// updates were executed without problem.
func (c *TClient) RequestUpdate(selector string, force bool) bool {
command := "U"
if force {
command += "!"
} else {
command += " "
}
command += selector
conn := c.getConnection()
defer conn.Close()
c.send(conn, command)
buf := make([]byte, 2)
nr, err := conn.Read(buf)
if err != nil {
log.WithFields(logrus.Fields{
"error": err,
"path": c.config.Socket.Path,
}).Fatal("Could not read server response from the UNIX socket")
}
if nr != 1 {
log.WithFields(logrus.Fields{
"path": c.config.Socket.Path,
}).Fatal("Invalid response from server")
}
return buf[0] == 49
}

111
main.go
View file

@ -1,7 +1,6 @@
package main package main
import ( import (
"net"
"os" "os"
"github.com/karrick/golf" "github.com/karrick/golf"
@ -16,6 +15,11 @@ type (
// then quits), client (connects to the server and requests an // then quits), client (connects to the server and requests an
// update) or server (runs the server in the foreground). // update) or server (runs the server in the foreground).
runMode string runMode string
// When running in client mode, if this is set to a string, it
// will be interpreted as a command to send to the server.
// Supported commands are 'Q' (quit) and 'R' (reload
// configuration)
command string
// The selector to use when running the updates. Only meaningful // The selector to use when running the updates. Only meaningful
// if running in client or standalone mode. // if running in client or standalone mode.
selector string selector string
@ -33,16 +37,6 @@ type (
// Send logs to syslog. // Send logs to syslog.
logSyslog bool logSyslog bool
} }
// 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
}
) )
// Parse command line options. // Parse command line options.
@ -52,6 +46,10 @@ func parseCommandLine() tCliFlags {
golf.StringVarP(&flags.cfgFile, 'c', "config", "/etc/fetch-certificates.yml", golf.StringVarP(&flags.cfgFile, 'c', "config", "/etc/fetch-certificates.yml",
"Path to the configuration file.") "Path to the configuration file.")
golf.StringVarP(&flags.command, 'C', "command", "",
"Send a command to the server instead of requesting an "+
"update. Only meaningful in client mode. Command may be "+
"Q (quit) or R (reload configuration).")
golf.BoolVarP(&flags.force, 'f', "force", false, golf.BoolVarP(&flags.force, 'f', "force", false,
"Force update of selected certificates. Only meaningful in "+ "Force update of selected certificates. Only meaningful in "+
"client or standalone mode.") "client or standalone mode.")
@ -81,65 +79,6 @@ func parseCommandLine() tCliFlags {
return flags return flags
} }
// Initialize server state
func initServer(cfgFile string) tServerState {
ss := tServerState{
cfgFile: cfgFile,
}
cfg, err := LoadConfiguration(ss.cfgFile)
if err != nil {
log.WithField("error", err).Fatal("Failed to load initial configuration.")
}
ss.config = cfg
listener, err := initSocket(cfg.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")
}
}
}
func main() { func main() {
flags := parseCommandLine() flags := parseCommandLine()
err := configureLogging(flags) err := configureLogging(flags)
@ -147,26 +86,40 @@ func main() {
log.WithField("error", err).Fatal("Failed to configure logging.") log.WithField("error", err).Fatal("Failed to configure logging.")
} }
if flags.runMode == "server" {
server := initServer(flags.cfgFile)
defer server.destroy()
server.mainLoop()
return
}
cfg, err := LoadConfiguration(flags.cfgFile) cfg, err := LoadConfiguration(flags.cfgFile)
if err != nil { if err != nil {
log.WithField("error", err).Fatal("Failed to load initial configuration.") log.WithField("error", err).Fatal("Failed to load initial configuration.")
} }
if flags.runMode == "standalone" {
if flags.runMode == "server" {
server := InitServer(flags.cfgFile, cfg)
defer server.Destroy()
server.MainLoop()
} else if flags.runMode == "standalone" {
result := executeUpdate(&cfg, flags.selector, flags.force) result := executeUpdate(&cfg, flags.selector, flags.force)
if result { if result {
log.Debug("Update successful") log.Debug("Update successful")
} else { } else {
log.Fatal("Update failed") log.Fatal("Update failed")
} }
} else if flags.runMode == "client" { } else if flags.runMode == "client" {
panic("CLIENT MODE NOT IMPLEMENTED") // FIXME client := InitClient(cfg)
if flags.command == "Q" || flags.command == "R" {
client.SendCommand(flags.command)
} else if flags.command != "" {
log.WithField("command", flags.command).Fatal(
"Unknown server command.")
} else {
result := client.RequestUpdate(flags.selector, flags.force)
if result {
log.Debug("Update successful")
} else {
log.Fatal("Update failed")
}
}
} else { } else {
log.WithField("mode", flags.runMode).Fatal("Unknown execution mode.") log.WithField("mode", flags.runMode).Fatal("Unknown execution mode.")
} }

View file

@ -12,20 +12,32 @@ import (
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
type TCommandType int type tCommandType int
const ( const (
CMD_IGNORE TCommandType = iota CMD_IGNORE tCommandType = iota
CMD_QUIT CMD_QUIT
CMD_RELOAD CMD_RELOAD
CMD_UPDATE CMD_UPDATE
) )
type TCommand struct { type (
CommandType TCommandType tCommand struct {
Force bool CommandType tCommandType
Selector string 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 { func configureSocket(cfg tSocketConfig) error {
if cfg.Group != "" { if cfg.Group != "" {
@ -67,7 +79,7 @@ func initSocket(cfg tSocketConfig) (net.Listener, error) {
return listener, nil return listener, nil
} }
func socketServer(cfg *tConfiguration, listener net.Listener) TCommandType { func socketServer(cfg *tConfiguration, listener net.Listener) tCommandType {
for { for {
fd, err := listener.Accept() fd, err := listener.Accept()
if err != nil { if err != nil {
@ -80,7 +92,7 @@ func socketServer(cfg *tConfiguration, listener net.Listener) TCommandType {
} }
} }
func executeFromSocket(cfg *tConfiguration, conn net.Conn) TCommandType { func executeFromSocket(cfg *tConfiguration, conn net.Conn) tCommandType {
defer conn.Close() defer conn.Close()
log.Debug("Received connection") log.Debug("Received connection")
@ -114,7 +126,7 @@ func executeFromSocket(cfg *tConfiguration, conn net.Conn) TCommandType {
return command.CommandType return command.CommandType
} }
func parseCommand(n int, buf []byte) *TCommand { func parseCommand(n int, buf []byte) *tCommand {
if n == 512 { if n == 512 {
log.Warn("Too much data received") log.Warn("Too much data received")
return nil return nil
@ -125,12 +137,12 @@ func parseCommand(n int, buf []byte) *TCommand {
} }
if n == 1 { if n == 1 {
if buf[0] == 'Q' { if buf[0] == 'Q' {
return &TCommand{CommandType: CMD_QUIT} return &tCommand{CommandType: CMD_QUIT}
} else if buf[0] == 'R' { } else if buf[0] == 'R' {
return &TCommand{CommandType: CMD_RELOAD} return &tCommand{CommandType: CMD_RELOAD}
} }
} else if n > 2 && buf[0] == 'U' { } else if n > 2 && buf[0] == 'U' {
res := &TCommand{CommandType: CMD_UPDATE} res := &tCommand{CommandType: CMD_UPDATE}
if buf[1] == '!' { if buf[1] == '!' {
res.Force = true res.Force = true
} }
@ -142,3 +154,58 @@ func parseCommand(n int, buf []byte) *TCommand {
log.Warn("Invalid command received") log.Warn("Invalid command received")
return nil return nil
} }
// Initialize server state
func InitServer(cfgFile string, config tConfiguration) TServerState {
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")
}
}
}