From cf17cc3419c32bd8d42518109104911c90674bc0 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Thu, 14 Dec 2023 12:09:01 +0100 Subject: [PATCH] Add initial server implementation with config loading and listener setup This commit introduces the creation of the core server and its components: config loading mechanism, listener setup, and error handling. The provided configuration allows the server to run different types of listeners. Additionally, it includes robust log-level settings to facilitate debugging and operational transparency. --- .golangci.toml | 11 ++++++ cmd/server/main.go | 71 ++++++++++++++++++++++++++++++++++++ config.go | 50 ++++++++++++++++++++++++++ error.go | 6 ++++ go.mod | 11 ++++++ go.sum | 23 ++++++++++++ listener.go | 79 ++++++++++++++++++++++++++++++++++++++++ logranger.toml | 5 +++ server.go | 90 ++++++++++++++++++++++++++++++++++++++++++++++ 9 files changed, 346 insertions(+) create mode 100644 .golangci.toml create mode 100644 cmd/server/main.go create mode 100644 config.go create mode 100644 error.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 listener.go create mode 100644 logranger.toml create mode 100644 server.go diff --git a/.golangci.toml b/.golangci.toml new file mode 100644 index 0000000..ef4d48a --- /dev/null +++ b/.golangci.toml @@ -0,0 +1,11 @@ +## SPDX-FileCopyrightText: 2022 Winni Neessen +## +## SPDX-License-Identifier: MIT + +[run] +go = "1.20" +tests = true + +[linters] +enable = ["stylecheck", "whitespace", "containedctx", "contextcheck", "decorder", + "errname", "errorlint", "gofmt", "gofumpt"] \ No newline at end of file diff --git a/cmd/server/main.go b/cmd/server/main.go new file mode 100644 index 0000000..b36e0d6 --- /dev/null +++ b/cmd/server/main.go @@ -0,0 +1,71 @@ +package main + +import ( + "log/slog" + "os" + "os/signal" + "path/filepath" + "syscall" + + "github.com/wneessen/logranger" +) + +const ( + // LogErrKey is the keyword used in slog for error messages + LogErrKey = "error" +) + +func main() { + l := slog.New(slog.NewJSONHandler(os.Stdout, nil)).With(slog.String("context", "logranger")) + cp := "logranger.toml" + cpe := os.Getenv("LOGRANGER_CONFIG") + if cpe != "" { + cp = cpe + } + + p := filepath.Dir(cp) + f := filepath.Base(cp) + c, err := logranger.NewConfig(p, f) + if err != nil { + l.Error("failed to read/parse config", LogErrKey, err) + os.Exit(1) + } + + s := logranger.New(c) + go func() { + if err = s.Run(); err != nil { + l.Error("failed to start logranger: %s", LogErrKey, err) + os.Exit(1) + } + }() + + sc := make(chan os.Signal, 1) + signal.Notify(sc) + for { + select { + case rc := <-sc: + if rc == syscall.SIGKILL || rc == syscall.SIGABRT || rc == syscall.SIGINT || rc == syscall.SIGTERM { + l.Warn("received signal. shutting down server", slog.String("signal", rc.String())) + //s.Stop() + l.Info("server gracefully shut down") + os.Exit(0) + } + if rc == syscall.SIGHUP { + l.Info(`received "SIGHUP" signal - reloading rules...`) + /* + _, nr, err := config.New(config.WithConfFile(*cf), config.WithRulesFile(*rf)) + if err != nil { + s.Log.Errorf("%s - skipping reload", err) + continue + } + if err := nr.CheckRegEx(); err != nil { + s.Log.Errorf("ruleset validation failed for new ruleset - skipping reload: %s", err) + continue + } + s.SetRules(nr) + + */ + } + } + } +} diff --git a/config.go b/config.go new file mode 100644 index 0000000..00cbe4f --- /dev/null +++ b/config.go @@ -0,0 +1,50 @@ +package logranger + +import ( + "fmt" + "os" + + "github.com/kkyr/fig" +) + +// Config holds all the global configuration settings that are parsed by fig +type Config struct { + // Server holds server specific configuration values + Server struct { + PIDFile string `fig:"pid_file" default:"/var/run/logranger.pid"` + } + Listener struct { + ListenerUnix struct { + Path string `fig:"path" default:"/var/tmp/logranger.sock"` + } `fig:"unix"` + ListenerTCP struct { + Addr string `fig:"addr" default:"0.0.0.0"` + Port uint `fig:"port" default:"9099"` + } `fig:"tcp"` + ListenerTLS struct { + Addr string `fig:"addr" default:"0.0.0.0"` + Port uint `fig:"port" default:"9099"` + CertPath string `fig:"cert_path"` + KeyPath string `fig:"key_path"` + } `fig:"tls"` + Type ListenerType `fig:"type" default:"unix"` + } `fig:"listener"` + Log struct { + Level string `fig:"level" default:"info"` + } `fig:"log"` +} + +// NewConfig returns a new Config object +func NewConfig(p, f string) (*Config, error) { + co := Config{} + _, err := os.Stat(fmt.Sprintf("%s/%s", p, f)) + if err != nil { + return &co, fmt.Errorf("failed to read config: %w", err) + } + + if err := fig.Load(&co, fig.Dirs(p), fig.File(f), fig.UseEnv("logranger")); err != nil { + return &co, fmt.Errorf("failed to load config: %w", err) + } + + return &co, nil +} diff --git a/error.go b/error.go new file mode 100644 index 0000000..04a6311 --- /dev/null +++ b/error.go @@ -0,0 +1,6 @@ +package logranger + +import "errors" + +// ErrCertConfigEmpty is returned if a TLS listener is configured but not certificate or key paths are set +var ErrCertConfigEmpty = errors.New("certificate and key paths are required for listener type: TLS") diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..f9596fb --- /dev/null +++ b/go.mod @@ -0,0 +1,11 @@ +module github.com/wneessen/logranger + +go 1.21 + +require github.com/kkyr/fig v0.4.0 + +require ( + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/pelletier/go-toml/v2 v2.1.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..0157e46 --- /dev/null +++ b/go.sum @@ -0,0 +1,23 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/kkyr/fig v0.4.0 h1:4D/g72a8ij1fgRypuIbEoqIT7ukf2URVBtE777/gkbc= +github.com/kkyr/fig v0.4.0/go.mod h1:U4Rq/5eUNJ8o5UvOEc9DiXtNf41srOLn2r/BfCyuc58= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI= +github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/listener.go b/listener.go new file mode 100644 index 0000000..8b0026e --- /dev/null +++ b/listener.go @@ -0,0 +1,79 @@ +package logranger + +import ( + "crypto/tls" + "fmt" + "net" + "strings" +) + +// ListenerType is an enumeration wrapper for the different listener types +type ListenerType uint + +const ( + ListenerUnix ListenerType = iota + ListenerTCP + ListenerTLS +) + +func NewListener(c *Config) (net.Listener, error) { + var l net.Listener + var lerr error + switch c.Listener.Type { + case ListenerUnix: + rua, err := net.ResolveUnixAddr("unix", c.Listener.ListenerUnix.Path) + if err != nil { + return nil, fmt.Errorf("failed to resolve UNIX listener socket: %w", err) + } + l, lerr = net.Listen("unix", rua.String()) + case ListenerTCP: + la := net.JoinHostPort(c.Listener.ListenerTCP.Addr, fmt.Sprintf("%d", c.Listener.ListenerTCP.Port)) + l, lerr = net.Listen("tcp", la) + case ListenerTLS: + if c.Listener.ListenerTLS.CertPath == "" || c.Listener.ListenerTLS.KeyPath == "" { + return nil, ErrCertConfigEmpty + } + ce, err := tls.LoadX509KeyPair(c.Listener.ListenerTLS.CertPath, c.Listener.ListenerTLS.KeyPath) + if err != nil { + return nil, fmt.Errorf("failed to load X509 certificate: %w", err) + } + la := net.JoinHostPort(c.Listener.ListenerTCP.Addr, fmt.Sprintf("%d", c.Listener.ListenerTCP.Port)) + lc := &tls.Config{Certificates: []tls.Certificate{ce}} + l, lerr = tls.Listen("tcp", la, lc) + default: + return nil, fmt.Errorf("failed to initialize listener: unknown listener type in config") + } + if lerr != nil { + return nil, fmt.Errorf("failed to initalize listener: %w", lerr) + } + return l, nil +} + +// UnmarshalString satisfies the fig.StringUnmarshaler interface for the ListenerType type +func (l *ListenerType) UnmarshalString(v string) error { + switch strings.ToLower(v) { + case "unix": + *l = ListenerUnix + case "tcp": + *l = ListenerTCP + case "tls": + *l = ListenerTLS + default: + return fmt.Errorf("unknown listener type: %s", v) + } + return nil +} + +// String satisfies the fmt.Stringer interface for the ListenerType type +func (l ListenerType) String() string { + switch l { + case ListenerUnix: + return "UNIX listener" + case ListenerTCP: + return "TCP listener" + case ListenerTLS: + return "TLS listener" + default: + return "Unknown listener type" + } +} diff --git a/logranger.toml b/logranger.toml new file mode 100644 index 0000000..9c37ead --- /dev/null +++ b/logranger.toml @@ -0,0 +1,5 @@ +[server] +pid_file = "/var/tmp/logranger.pid" + +[listener] +type = "tcp" diff --git a/server.go b/server.go new file mode 100644 index 0000000..5482227 --- /dev/null +++ b/server.go @@ -0,0 +1,90 @@ +package logranger + +import ( + "fmt" + "log/slog" + "net" + "os" + "strings" + "sync" +) + +const ( + // LogErrKey is the keyword used in slog for error messages + LogErrKey = "error" +) + +// Server is the main server struct +type Server struct { + // conf is a pointer to the config.Config + conf *Config + // listener is a listener that satisfies the net.Listener interface + listener net.Listener + // log is a pointer to the slog.Logger + log *slog.Logger + + // wg is a sync.WaitGroup + wg sync.WaitGroup +} + +// New returns a Server struct +func New(c *Config) *Server { + s := &Server{ + conf: c, + } + s.setLogLevel() + return s +} + +// Run starts the logranger Server with a new Listener based on the config settings +func (s *Server) Run() error { + l, err := NewListener(s.conf) + if err != nil { + return err + } + return s.RunWithListener(l) +} + +// RunWithListener starts the logranger Server using a provided net.Listener +func (s *Server) RunWithListener(l net.Listener) error { + s.listener = l + + // Create PID file + pf, err := os.Create(s.conf.Server.PIDFile) + if err != nil { + s.log.Error("failed to create PID file", LogErrKey, err) + os.Exit(1) + } + _, err = pf.WriteString(fmt.Sprintf("%d", os.Getpid())) + if err != nil { + s.log.Error("failed to write PID to PID file", LogErrKey, err) + _ = pf.Close() + } + if err = pf.Close(); err != nil { + s.log.Error("failed to close PID file", LogErrKey, err) + } + + // Listen for connections + s.wg.Add(1) + + return nil +} + +// setLogLevel assigns a new slog.Logger instance to the Server based on the configured log level +func (s *Server) setLogLevel() { + lo := slog.HandlerOptions{} + switch strings.ToLower(s.conf.Log.Level) { + case "debug": + lo.Level = slog.LevelDebug + case "info": + lo.Level = slog.LevelInfo + case "warn": + lo.Level = slog.LevelWarn + case "error": + lo.Level = slog.LevelError + default: + lo.Level = slog.LevelInfo + } + lh := slog.NewJSONHandler(os.Stdout, &lo) + s.log = slog.New(lh).With(slog.String("context", "logranger")) +}