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
import (
"net"
"os"
"github.com/karrick/golf"
@ -16,6 +15,11 @@ type (
// then quits), client (connects to the server and requests an
// update) or server (runs the server in the foreground).
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
// if running in client or standalone mode.
selector string
@ -33,16 +37,6 @@ type (
// Send logs to syslog.
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.
@ -52,6 +46,10 @@ func parseCommandLine() tCliFlags {
golf.StringVarP(&flags.cfgFile, 'c', "config", "/etc/fetch-certificates.yml",
"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,
"Force update of selected certificates. Only meaningful in "+
"client or standalone mode.")
@ -81,65 +79,6 @@ func parseCommandLine() tCliFlags {
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() {
flags := parseCommandLine()
err := configureLogging(flags)
@ -147,26 +86,40 @@ func main() {
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)
if err != nil {
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)
if result {
log.Debug("Update successful")
} else {
log.Fatal("Update failed")
}
} 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 {
log.WithField("mode", flags.runMode).Fatal("Unknown execution mode.")
}

View file

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