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.
This commit is contained in:
Winni Neessen 2023-12-14 12:09:01 +01:00
parent 4689c24617
commit cf17cc3419
Signed by: wneessen
GPG key ID: 385AC9889632126E
9 changed files with 346 additions and 0 deletions

11
.golangci.toml Normal file
View file

@ -0,0 +1,11 @@
## SPDX-FileCopyrightText: 2022 Winni Neessen <winni@neessen.dev>
##
## SPDX-License-Identifier: MIT
[run]
go = "1.20"
tests = true
[linters]
enable = ["stylecheck", "whitespace", "containedctx", "contextcheck", "decorder",
"errname", "errorlint", "gofmt", "gofumpt"]

71
cmd/server/main.go Normal file
View file

@ -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)
*/
}
}
}
}

50
config.go Normal file
View file

@ -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
}

6
error.go Normal file
View file

@ -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")

11
go.mod Normal file
View file

@ -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
)

23
go.sum Normal file
View file

@ -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=

79
listener.go Normal file
View file

@ -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"
}
}

5
logranger.toml Normal file
View file

@ -0,0 +1,5 @@
[server]
pid_file = "/var/tmp/logranger.pid"
[listener]
type = "tcp"

90
server.go Normal file
View file

@ -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"))
}