diff --git a/client.go b/client.go new file mode 100644 index 0000000..705ad56 --- /dev/null +++ b/client.go @@ -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 +} diff --git a/main.go b/main.go index 0b34242..6d7867a 100644 --- a/main.go +++ b/main.go @@ -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.") } diff --git a/socket.go b/server.go similarity index 56% rename from socket.go rename to server.go index 5a497b9..bec4d69 100644 --- a/socket.go +++ b/server.go @@ -12,20 +12,32 @@ 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 - Force bool - Selector string -} +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 != "" { @@ -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") + } + } +}