apiary/cmd/apiary.go

359 lines
9.9 KiB
Go
Raw Normal View History

2021-04-10 05:58:01 +00:00
package main
import (
"context"
"fmt"
2021-04-10 06:16:59 +00:00
"net/http"
2021-04-10 05:58:01 +00:00
"os"
"os/signal"
2021-11-06 00:51:19 +00:00
"strings"
2021-04-10 05:58:01 +00:00
"time"
2022-01-13 08:10:36 +00:00
"git.t-juice.club/torjus/apiary"
"git.t-juice.club/torjus/apiary/config"
"git.t-juice.club/torjus/apiary/honeypot/ports"
"git.t-juice.club/torjus/apiary/honeypot/smtp"
"git.t-juice.club/torjus/apiary/honeypot/ssh"
"git.t-juice.club/torjus/apiary/honeypot/ssh/store"
"git.t-juice.club/torjus/apiary/web"
2021-11-06 00:29:29 +00:00
"github.com/coreos/go-systemd/daemon"
2021-10-21 10:36:01 +00:00
sshlib "github.com/gliderlabs/ssh"
2021-04-10 05:58:01 +00:00
"github.com/urfave/cli/v2"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
2021-04-10 09:24:10 +00:00
"golang.org/x/crypto/acme/autocert"
2021-04-10 05:58:01 +00:00
)
func main() {
app := &cli.App{
Name: "apiary",
2021-04-12 06:58:39 +00:00
Version: apiary.FullVersion(),
2021-04-10 05:58:01 +00:00
Authors: []*cli.Author{
{
Name: "Torjus Håkestad",
Email: "torjus@usit.uio.no",
},
},
Commands: []*cli.Command{
{
Name: "serve",
Action: ActionServe,
Usage: "Start Apiary server",
},
},
}
if err := app.Run(os.Args); err != nil {
fmt.Printf("Error: %s\n", err)
os.Exit(1)
}
}
func ActionServe(c *cli.Context) error {
cfg, err := getConfig()
if err != nil {
return err
}
2021-04-10 09:24:10 +00:00
// Setup logging
2021-04-10 05:58:01 +00:00
loggers := setupLoggers(cfg)
2021-09-17 00:05:07 +00:00
loggers.rootLogger.Infow("Starting apiary", "version", apiary.FullVersion())
2021-04-10 05:58:01 +00:00
2021-04-10 09:24:10 +00:00
// Setup store
2021-04-10 05:58:01 +00:00
var s store.LoginAttemptStore
switch cfg.Store.Type {
case "MEMORY", "memory":
2021-11-03 20:24:23 +00:00
loggers.rootLogger.Infow("Initialized store", "store_type", "memory")
2021-04-10 05:58:01 +00:00
s = &store.MemoryStore{}
case "POSTGRES", "postgres":
2021-11-03 20:24:23 +00:00
pgStartTime := time.Now()
loggers.rootLogger.Debugw("Initializing store", "store_type", "postgres")
2021-04-10 05:58:01 +00:00
pgStore, err := store.NewPostgresStore(cfg.Store.Postgres.DSN)
if err != nil {
return err
}
if err := pgStore.InitDB(); err != nil {
return err
}
2021-11-03 20:24:23 +00:00
loggers.rootLogger.Infow("Initialized store", "store_type", "postgres", "init_time", time.Since(pgStartTime))
2021-09-17 00:01:43 +00:00
if cfg.Store.EnableCache {
loggers.rootLogger.Debugw("Initializing store", "store_type", "cache-postgres")
2021-11-03 20:24:23 +00:00
startTime := time.Now()
2021-09-17 00:01:43 +00:00
cachingStore := store.NewCachingStore(pgStore)
s = cachingStore
2021-11-03 20:24:23 +00:00
loggers.rootLogger.Infow("Initialized store", "store_type", "cache-postgres", "init_time", time.Since(startTime))
2021-09-17 00:01:43 +00:00
} else {
s = pgStore
}
2021-04-10 05:58:01 +00:00
default:
return fmt.Errorf("Invalid store configured")
}
2021-10-28 23:45:47 +00:00
// Setup interrupt handling
interruptChan := make(chan os.Signal, 1)
signal.Notify(interruptChan, os.Interrupt)
rootCtx, rootCancel := context.WithCancel(c.Context)
defer rootCancel()
serversCtx, serversCancel := context.WithCancel(rootCtx)
defer serversCancel()
// Setup metrics collection
s = store.NewMetricsCollectingStore(rootCtx, s)
2021-10-28 14:38:33 +00:00
2021-04-10 09:24:10 +00:00
// Setup honeypot
2021-10-21 10:36:01 +00:00
hs, err := ssh.NewHoneypotServer(cfg.Honeypot, s)
2021-04-10 05:58:01 +00:00
if err != nil {
return err
}
hs.Logger = loggers.honeypotLogger
2021-04-10 09:24:10 +00:00
// Setup webserver
2021-04-10 05:58:01 +00:00
web := web.NewServer(cfg.Frontend, hs, s)
web.AccessLogger = loggers.webAccessLogger
web.ServerLogger = loggers.webServerLogger
2021-04-10 09:24:10 +00:00
if cfg.Frontend.Autocert.Enable {
certManager := autocert.Manager{
Prompt: autocert.AcceptTOS,
HostPolicy: autocert.HostWhitelist(cfg.Frontend.Autocert.Domains...),
Email: cfg.Frontend.Autocert.Email,
}
if cfg.Frontend.Autocert.CacheDir != "" {
certManager.Cache = autocert.DirCache(cfg.Frontend.Autocert.CacheDir)
}
tlsConfig := certManager.TLSConfig()
web.TLSConfig = tlsConfig
}
2021-04-10 05:58:01 +00:00
2021-10-21 08:33:28 +00:00
// Setup portlistener, if configured
if cfg.Ports.Enable {
portsCtx, cancel := context.WithCancel(rootCtx)
defer cancel()
// TODO: Add more stores
store := &ports.MemoryStore{}
portsServer := ports.New(store)
portsServer.Logger = loggers.portsLogger
for _, port := range cfg.Ports.TCPPorts {
portsServer.AddTCPPort(port)
}
go func() {
loggers.rootLogger.Info("Starting ports server")
if err := portsServer.Start(portsCtx); err != nil {
loggers.rootLogger.Warnw("Ports server returned error", "err", err)
}
2021-10-21 08:33:28 +00:00
}()
}
2021-11-06 13:04:11 +00:00
// Setup smtp honeypot if enabled
if cfg.SMTP.Enable {
honeypot, err := smtp.NewSMTPHoneypot()
if err != nil {
loggers.rootLogger.Warnw("Error seting up SMTP honeypot", "error", err)
}
honeypot.Addr = cfg.SMTP.Addr
honeypot.Logger = loggers.smtpLogger
if cfg.SMTP.EnableMetrics {
honeypot.Store = smtp.NewMetricsStore(&smtp.DiscardStore{})
} else {
honeypot.Store = &smtp.DiscardStore{}
}
// Start smtp honeypot
go func() {
loggers.rootLogger.Info("Starting SMTP server")
if err := honeypot.ListenAndServe(); err != nil {
loggers.rootLogger.Warnw("SMTP server returned error", "error", err)
}
}()
// Wait for smtp shutdown
go func() {
<-serversCtx.Done()
loggers.rootLogger.Info("SMTP server shutdown started")
if err := honeypot.Shutdown(); err != nil {
loggers.rootLogger.Errorw("Error shutting down SMTP server", "error", err)
}
loggers.rootLogger.Info("SMTP server shutdown complete")
}()
}
2021-04-10 05:58:01 +00:00
// Handle interrupt
go func() {
<-interruptChan
2021-04-10 06:16:59 +00:00
loggers.rootLogger.Info("Interrupt received, shutting down")
2021-04-10 05:58:01 +00:00
serversCancel()
}()
// Start ssh server
go func() {
2021-04-10 06:16:59 +00:00
loggers.rootLogger.Info("Starting SSH server")
2021-10-21 10:36:01 +00:00
if err := hs.ListenAndServe(); err != nil && err != sshlib.ErrServerClosed {
2021-04-10 06:16:59 +00:00
loggers.rootLogger.Warnw("SSH server returned error", "error", err)
}
2021-04-10 05:58:01 +00:00
}()
// Start web server
go func() {
2021-04-10 06:16:59 +00:00
loggers.rootLogger.Info("Starting web server")
2021-04-10 09:24:10 +00:00
if err := web.StartServe(); err != nil && err != http.ErrServerClosed {
2021-04-10 06:16:59 +00:00
loggers.rootLogger.Warnw("Web server returned error", "error", err)
}
2021-04-10 05:58:01 +00:00
}()
2021-11-06 00:29:29 +00:00
// If run by systemd, enable watchdog and notify ready
go func() {
notifyCtx, cancel := context.WithCancel(rootCtx)
defer cancel()
_, ok := os.LookupEnv("NOTIFY_SOCKET")
if !ok {
return
}
loggers.rootLogger.Info("Systemd notify socket detected. Sending ready and enabling watchdog.")
ok, err := daemon.SdNotify(false, daemon.SdNotifyReady)
if !ok {
loggers.rootLogger.Info("Systemd notify not enabled.")
return
}
if err != nil {
loggers.rootLogger.Warnw("Unable to connect to NOTIFY_SOCKET.", "error", err)
return
}
loggers.rootLogger.Debug("Sent READY=1 to NOTIFY_SOCKET.")
2021-11-06 00:36:01 +00:00
// Setup timer
2021-11-06 00:29:29 +00:00
timeout, err := daemon.SdWatchdogEnabled(false)
if err != nil {
2021-11-06 00:59:21 +00:00
loggers.rootLogger.Warnw("Unable to get watchdog timeout.", "error", err)
return
}
if timeout == 0 {
loggers.rootLogger.Infow("Systemd watchdog not enabled.")
2021-11-06 00:29:29 +00:00
return
}
2021-11-06 00:54:56 +00:00
2021-11-06 00:29:29 +00:00
ticker := time.NewTicker(timeout / 2)
for {
2022-01-13 08:06:14 +00:00
// TODO: Consider using ctx with deadline to avoid healthcheck taking more time than ticker,
// which will result in being killed by watchdog.
2021-11-06 00:29:29 +00:00
healthy := s.IsHealthy()
select {
case <-ticker.C:
if healthy == nil {
if _, err := daemon.SdNotify(false, daemon.SdNotifyWatchdog); err != nil {
loggers.rootLogger.Warnw("Error notifying watchdog.", "err", err)
}
2022-01-13 08:06:14 +00:00
continue
2021-11-06 00:29:29 +00:00
}
2022-01-13 08:06:14 +00:00
// TODO: If unhealthy, should we retry healthcheck immediately, otherwise service will most likely get killed by watchdog.
loggers.rootLogger.Errorw("Store reported not healthy, might get killed by watchdog.", "err", healthy)
2021-11-06 00:29:29 +00:00
case <-notifyCtx.Done():
loggers.rootLogger.Debugw("Notify context cancelled.")
return
}
}
}()
2021-04-10 05:58:01 +00:00
go func() {
<-serversCtx.Done()
// Stop SSH server
sshShutdownCtx, sshShutdownCancel := context.WithTimeout(context.Background(), 10*time.Second)
defer sshShutdownCancel()
loggers.rootLogger.Info("SSH server shutdown started")
if err := hs.Shutdown(sshShutdownCtx); err != nil {
loggers.rootLogger.Infow("Error shutting down SSH server", "error", err)
}
loggers.rootLogger.Info("SSH server shutdown complete")
// Stop Web server
webShutdownCtx, webShutdownCancel := context.WithTimeout(context.Background(), 10*time.Second)
defer webShutdownCancel()
loggers.rootLogger.Info("Web server shutdown started")
if err := web.Shutdown(webShutdownCtx); err != nil {
loggers.rootLogger.Infow("Error shutting down web server", "error", err)
}
loggers.rootLogger.Info("Web server shutdown complete")
rootCancel()
}()
<-rootCtx.Done()
return nil
}
type loggerCollection struct {
rootLogger *zap.SugaredLogger
honeypotLogger *zap.SugaredLogger
webAccessLogger *zap.SugaredLogger
webServerLogger *zap.SugaredLogger
2021-10-21 08:33:28 +00:00
portsLogger *zap.SugaredLogger
2021-11-06 13:04:11 +00:00
smtpLogger *zap.SugaredLogger
2021-04-10 05:58:01 +00:00
}
func setupLoggers(cfg config.Config) *loggerCollection {
logEncoderCfg := zap.NewProductionEncoderConfig()
logEncoderCfg.EncodeCaller = func(caller zapcore.EntryCaller, enc zapcore.PrimitiveArrayEncoder) {}
2021-11-06 00:51:19 +00:00
var level zap.AtomicLevel
switch strings.ToUpper(cfg.Honeypot.LogLevel) {
case "INFO":
level = zap.NewAtomicLevelAt(zap.InfoLevel)
case "DEBUG":
level = zap.NewAtomicLevelAt(zap.DebugLevel)
case "WARN", "WARNING":
level = zap.NewAtomicLevelAt(zap.WarnLevel)
case "ERR", "ERROR":
level = zap.NewAtomicLevelAt(zap.WarnLevel)
default:
level = zap.NewAtomicLevelAt(zap.InfoLevel)
}
2021-04-10 05:58:01 +00:00
logEncoderCfg.EncodeLevel = zapcore.CapitalColorLevelEncoder
logEncoderCfg.EncodeTime = zapcore.ISO8601TimeEncoder
2021-11-03 20:28:02 +00:00
logEncoderCfg.EncodeDuration = zapcore.StringDurationEncoder
2021-04-10 05:58:01 +00:00
rootLoggerCfg := &zap.Config{
Level: level,
OutputPaths: []string{"stdout"},
ErrorOutputPaths: []string{"stderr"},
Encoding: "console",
EncoderConfig: logEncoderCfg,
}
rootLogger, err := rootLoggerCfg.Build()
if err != nil {
panic(err)
}
return &loggerCollection{
rootLogger: rootLogger.Named("APP").Sugar(),
honeypotLogger: rootLogger.Named("HON").Sugar(),
webAccessLogger: rootLogger.Named("ACC").Sugar(),
webServerLogger: rootLogger.Named("WEB").Sugar(),
2021-10-21 08:33:28 +00:00
portsLogger: rootLogger.Named("PRT").Sugar(),
2021-11-06 13:04:11 +00:00
smtpLogger: rootLogger.Named("SMT").Sugar(),
2021-04-10 05:58:01 +00:00
}
}
func getConfig() (config.Config, error) {
defaultLocations := []string{
"apiary.toml",
"/etc/apiary.toml",
2021-04-10 07:05:30 +00:00
"/etc/apiary/apiary.toml",
2021-04-10 05:58:01 +00:00
}
for _, fname := range defaultLocations {
if _, err := os.Stat(fname); os.IsNotExist(err) {
continue
}
cfg, err := config.FromFile(fname)
if err != nil {
return config.Config{}, err
}
return cfg, nil
}
return config.Config{}, fmt.Errorf("Could not find config file")
}