main.go

package main

import (
	"context"
	"crypto/tls"
	"encoding/json"
	"flag"
	"fmt"
	"log"
	"log/slog"
	"os"
	"os/signal"
	"sync"
	"syscall"
	"time"

	"honeypot/internal/dashboard"
	"honeypot/internal/database"
	"honeypot/internal/geodb"
	"honeypot/internal/honeypot"
	dnshoneypot "honeypot/internal/honeypot/dns"
	"honeypot/internal/honeypot/ftp"
	httphoneypot "honeypot/internal/honeypot/http"
	"honeypot/internal/honeypot/packetlogger"
	"honeypot/internal/honeypot/rdp"
	"honeypot/internal/honeypot/sip"
	"honeypot/internal/honeypot/smtp"
	"honeypot/internal/honeypot/ssh"
	"honeypot/internal/honeypot/telnet"
	"honeypot/internal/logger"
	"honeypot/internal/metrics"
	tlscert "honeypot/internal/tls"
)

// version is set at build time or by git hook
var version = "0.59.6"

// Config holds the application configuration.
type Config struct {
	ListenAddr       string             `json:"listen_addr"`
	LogFile          string             `json:"log_file"`
	DatabaseFile     string             `json:"database_file"`
	UIPort           uint16             `json:"ui_port"`
	UIPassword       string             `json:"ui_password"`
	APIToken         string             `json:"api_token"`
	DisableMetrics   bool               `json:"disable_metrics"`
	DisableHWMetrics bool               `json:"disable_hw_metrics"`
	DisableDashboard bool               `json:"disable_dashboard"`
	Interface        string             `json:"interface"`
	BpfExpression    string             `json:"bpf_expression"`
	SSHPorts         []uint16           `json:"ssh_ports"`
	TelnetPorts      []uint16           `json:"telnet_ports"`
	RDPPorts         []uint16           `json:"rdp_ports"`
	SMTPPorts        []uint16           `json:"smtp_ports"`
	SMTPSPorts       []uint16           `json:"smtps_ports"`
	FTPPorts         []uint16           `json:"ftp_ports"`
	FTPSPorts        []uint16           `json:"ftps_ports"`
	SIPPorts         []uint16           `json:"sip_ports"`
	HTTPPorts        []uint16           `json:"http_ports"`
	HTTPSPorts       []uint16           `json:"https_ports"`
	DNSPorts         []uint16           `json:"dns_ports"`
	CertConfig       tlscert.CertConfig `json:"cert_config"`
	ASNDBFile        string             `json:"asn_db_file"`
	CityDBFile       string             `json:"city_db_file"`
	ASNDBURL         string             `json:"asn_db_url"`
	CityDBURL        string             `json:"city_db_url"`
	TrustedProxies   []string           `json:"trusted_proxies"`
}

func main() {
	var versionFlag bool
	var configFile string
	flag.BoolVar(&versionFlag, "version", false, "print version and exit")
	flag.StringVar(&configFile, "config", "config.json", "path to configuration file")
	flag.Parse()

	if versionFlag {
		fmt.Printf("honeypot version %s\n", version)
		os.Exit(0)
	}

	cfg := parseConfig(configFile)

	l, err := logger.Setup(cfg.LogFile)
	if err != nil {
		log.Fatalf("failed to setup logger: %v", err)
	}

	log.Println("Config OK")

	ctx, cancel := context.WithCancel(context.Background())
	sigCh := make(chan os.Signal, 1)
	signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
	go func() {
		sig := <-sigCh
		fmt.Println("Received signal", sig)
		cancel()
	}()

	// Generate shared certificate if TLS is needed (HTTPS, SMTPS, FTPS, or RDP)
	var sharedCert *tls.Certificate
	if len(cfg.HTTPSPorts) > 0 || len(cfg.SMTPSPorts) > 0 || len(cfg.FTPSPorts) > 0 || len(cfg.RDPPorts) > 0 {
		cert, err := tlscert.GenerateSelfSignedCert(cfg.ListenAddr, cfg.CertConfig, l)
		if err != nil {
			l.Error("failed to generate shared TLS certificate", "error", err)
			// Continue without certificate - honeypots will handle this
		} else {
			sharedCert = cert
			l.Info("generated shared TLS certificate for HTTPS, SMTPS, FTPS and RDP")
		}
	}

	honeypots := []honeypot.Honeypot{}

	// Create honeypots early so they can register their fields before restoration
	// Add SSH honeypot if ports are configured
	if len(cfg.SSHPorts) > 0 {
		sshHp := ssh.New(ssh.Config{
			ListenAddr: cfg.ListenAddr,
			Ports:      cfg.SSHPorts,
		})
		honeypots = append(honeypots, sshHp)
		log.Println("SSH honeypot created", cfg.SSHPorts)
	}

	// Add Telnet honeypot if ports are configured
	if len(cfg.TelnetPorts) > 0 {
		telnetHp := telnet.New(telnet.Config{
			ListenAddr: cfg.ListenAddr,
			Ports:      cfg.TelnetPorts,
		})
		honeypots = append(honeypots, telnetHp)
		log.Println("Telnet honeypot created", cfg.TelnetPorts)
	}

	// Add RDP honeypot if ports are configured
	if len(cfg.RDPPorts) > 0 {
		rdpHp := rdp.New(rdp.Config{
			ListenAddr:  cfg.ListenAddr,
			Ports:       cfg.RDPPorts,
			Certificate: sharedCert,
		})
		honeypots = append(honeypots, rdpHp)
		log.Println("RDP honeypot created", cfg.RDPPorts)
	}

	// Add SMTP honeypot if ports are configured
	if len(cfg.SMTPPorts) > 0 || len(cfg.SMTPSPorts) > 0 {
		smtpHp := smtp.New(smtp.Config{
			ListenAddr:  cfg.ListenAddr,
			Ports:       cfg.SMTPPorts,
			SMTPSPorts:  cfg.SMTPSPorts,
			Certificate: sharedCert,
			CertConfig:  cfg.CertConfig,
		})
		honeypots = append(honeypots, smtpHp)
		log.Println("SMTP honeypot created", cfg.SMTPPorts, cfg.SMTPSPorts)
	}

	// Add FTP honeypot if ports are configured
	if len(cfg.FTPPorts) > 0 || len(cfg.FTPSPorts) > 0 {
		ftpHp := ftp.New(ftp.Config{
			ListenAddr:  cfg.ListenAddr,
			Ports:       cfg.FTPPorts,
			FTPSPorts:   cfg.FTPSPorts,
			Certificate: sharedCert,
			CertConfig:  cfg.CertConfig,
		})
		honeypots = append(honeypots, ftpHp)
		log.Println("FTP honeypot created", cfg.FTPPorts, cfg.FTPSPorts)
	}

	// Add HTTP honeypot if ports are configured
	if len(cfg.HTTPPorts) > 0 || len(cfg.HTTPSPorts) > 0 {
		httpHp := httphoneypot.New(httphoneypot.Config{
			ListenAddr:     cfg.ListenAddr,
			HTTPPorts:      cfg.HTTPPorts,
			HTTPSPorts:     cfg.HTTPSPorts,
			CertConfig:     cfg.CertConfig,
			TrustedProxies: cfg.TrustedProxies,
		})
		honeypots = append(honeypots, httpHp)
		log.Println("HTTP honeypot created", cfg.HTTPPorts, cfg.HTTPSPorts)
	}

	// Add DNS honeypot if ports are configured
	if len(cfg.DNSPorts) > 0 {
		dnsHp := dnshoneypot.New(dnshoneypot.Config{
			ListenAddr: cfg.ListenAddr,
			Ports:      cfg.DNSPorts,
		})
		honeypots = append(honeypots, dnsHp)
		log.Println("DNS honeypot created", cfg.DNSPorts)
	}

	// Add SIP honeypot if ports are configured
	if len(cfg.SIPPorts) > 0 {
		sipHp := sip.New(sip.Config{
			ListenAddr: cfg.ListenAddr,
			Ports:      cfg.SIPPorts,
		})
		honeypots = append(honeypots, sipHp)
		log.Println("SIP honeypot created", cfg.SIPPorts)
	}

	// Add packet logger if interface is specified
	if cfg.Interface != "" {
		plHp := packetlogger.New(packetlogger.Config{
			Interface:     cfg.Interface,
			BpfExpression: cfg.BpfExpression,
		})
		honeypots = append(honeypots, plHp)
		log.Println("Packet logger created", cfg.Interface)
	}

	// Start dashboard server if configured
	var dashboardShutdownDone chan struct{}
	var geoDb *geodb.GeoDB
	var db *database.Database
	if cfg.UIPort > 0 {
		dashboardShutdownDone = make(chan struct{})
		var err error
		// Download GeoDB if missing
		if _, err := os.Stat(cfg.ASNDBFile); os.IsNotExist(err) {
			log.Printf("ASN database missing, downloading from %s...", cfg.ASNDBURL)
			if err := geodb.DownloadFile(cfg.ASNDBURL, cfg.ASNDBFile); err != nil {
				log.Printf("Warning: failed to download ASN database: %v", err)
			}
		}
		if _, err := os.Stat(cfg.CityDBFile); os.IsNotExist(err) {
			log.Printf("City database missing, downloading from %s...", cfg.CityDBURL)
			if err := geodb.DownloadFile(cfg.CityDBURL, cfg.CityDBFile); err != nil {
				log.Printf("Warning: failed to download City database: %v", err)
			}
		}

		geoDb, err = geodb.NewGeoDB(cfg.ASNDBFile, cfg.CityDBFile)
		if err != nil {
			log.Printf("Warning: GeoDB could not be initialized: %v", err)
			fmt.Println("GeoDB could not be initialized. GeoLite2 databases not found.")
		}

		var metricsCollector logger.MetricsCollector
		if !cfg.DisableMetrics {
			metricsCollector = metrics.NewMetricsCollector(cfg.DisableHWMetrics)
			logger.RegisterMetricsCollector(metricsCollector)
		}

		db = database.NewDatabase(cfg.DatabaseFile)
		if db != nil {
			if err := db.CreateTables(); err != nil {
				log.Fatalf("failed to create database tables: %v", err)
			}
			logger.RegisterDatabase(db)
			if metricsCollector != nil {
				metricsCollector.SetDatabase(db)
			}
			log.Println("Database created", cfg.DatabaseFile)
		}

		dashboard.StartServer(ctx, dashboard.ServerConfig{
			ListenAddr:       cfg.ListenAddr,
			UIPort:           cfg.UIPort,
			UIPassword:       cfg.UIPassword,
			APIToken:         cfg.APIToken,
			LogFile:          cfg.LogFile,
			DisableMetrics:   cfg.DisableMetrics,
			DisableDashboard: cfg.DisableDashboard,
			ASNDBFile:        cfg.ASNDBFile,
			CityDBFile:       cfg.CityDBFile,
			ASNDBURL:         cfg.ASNDBURL,
			CityDBURL:        cfg.CityDBURL,
		},
			l,
			metricsCollector,
			honeypots,
			db, geoDb,
			dashboardShutdownDone,
		)

		// Start background IP info updater
		go startIPInfoUpdater(ctx, db, geoDb, l)
	}

	if len(honeypots) == 0 && cfg.UIPort == 0 {
		l.Info("no honeypots or dashboard configured, exiting")
		return
	}

	var wg sync.WaitGroup

	// Start all honeypots
	for _, hp := range honeypots {
		wg.Add(1)
		go func(h honeypot.Honeypot) {
			defer wg.Done()
			if err := h.Start(ctx, l); err != nil {
				l.Error("honeypot failed", "name", h.Name(), "error", err)
			}
		}(hp)
	}

	log.Println("Honeypots started", len(honeypots))

	<-ctx.Done()
	log.Println("Shutdown signal received")

	// Wait for dashboard to finish shutting down
	if dashboardShutdownDone != nil {
		<-dashboardShutdownDone
		l.Info("dashboard server stopped")
	}

	// Close database connection
	if db != nil {
		if err := db.Close(); err != nil {
			l.Error("failed to close database", "error", err)
		}
	}

	if geoDb != nil {
		if err := geoDb.Close(); err != nil {
			l.Error("failed to close geodb", "error", err)
		}
	}

	l.Info("all services stopped, shutdown complete")
}

func parseConfig(configFile string) Config {
	var cfg Config

	// Read config file
	if configFile == "" {
		log.Fatalf("config file path is required (use -config flag)")
	}

	data, err := os.ReadFile(configFile)
	if err != nil {
		log.Fatalf("failed to read config file %s: %v", configFile, err)
	}

	if err := json.Unmarshal(data, &cfg); err != nil {
		log.Fatalf("failed to parse config file %s: %v", configFile, err)
	}

	// Set defaults if not specified
	if cfg.ListenAddr == "" {
		cfg.ListenAddr = "0.0.0.0"
	}

	if cfg.UIPort > 0 {
		if cfg.ASNDBURL == "" {
			if _, err := os.Stat(cfg.ASNDBFile); os.IsNotExist(err) {
				log.Fatalf("asn_db_url is required in config when asn_db_file is missing")
			}
		}
		if cfg.CityDBURL == "" {
			if _, err := os.Stat(cfg.CityDBFile); os.IsNotExist(err) {
				log.Fatalf("city_db_url is required in config when city_db_file is missing")
			}
		}
	}

	return cfg
}

func startIPInfoUpdater(ctx context.Context, db *database.Database, geoDb *geodb.GeoDB, l *slog.Logger) {
	if db == nil || geoDb == nil {
		return
	}

	ticker := time.NewTicker(time.Minute)
	defer ticker.Stop()

	// Initial update
	db.UpdateIPInfo(ctx, geoDb, l)

	for {
		select {
		case <-ctx.Done():
			return
		case <-ticker.C:
			db.UpdateIPInfo(ctx, geoDb, l)
		}
	}
}