Files
openfsd/fsd/server.go

197 lines
4.3 KiB
Go

package fsd
import (
"context"
"crypto/rand"
"database/sql"
"encoding/hex"
"errors"
"fmt"
"github.com/renorris/openfsd/db"
"io"
"log/slog"
"net"
"sync"
)
type Server struct {
cfg *ServerConfig
postOffice *postOffice
metarService *metarService
dbRepo *db.Repositories
}
// NewServer creates a new Server instance.
//
// See NewDefaultServer to create a server using default settings obtained via environment variables.
func NewServer(cfg *ServerConfig, dbRepo *db.Repositories, numMetarWorkers int) (server *Server, err error) {
server = &Server{
cfg: cfg,
postOffice: newPostOffice(),
metarService: newMetarService(numMetarWorkers),
dbRepo: dbRepo,
}
return
}
// NewDefaultServer creates a new Server instance using the default configuration obtained via environment variables
func NewDefaultServer(ctx context.Context) (server *Server, err error) {
config, err := loadServerConfig(ctx)
if err != nil {
return
}
slog.Info(fmt.Sprintf("using %s", config.DatabaseDriver))
slog.Debug("connecting to SQL")
sqlDb, err := sql.Open(config.DatabaseDriver, config.DatabaseSourceName)
if err != nil {
return
}
slog.Debug("SQL OK")
sqlDb.SetMaxOpenConns(config.DatabaseMaxConns)
if config.DatabaseAutoMigrate {
slog.Debug("automatically migrating database")
if err = db.Migrate(sqlDb); err != nil {
return
}
slog.Debug("migrate OK")
}
dbRepo, err := db.NewRepositories(sqlDb)
if err != nil {
return
}
// Generate a default admin user if CID 1 isn't taken
if _, err = dbRepo.UserRepo.GetUserByCID(1); err != nil {
if !errors.Is(err, sql.ErrNoRows) {
return
}
err = nil
slog.Debug("no user with CID = 1 found, creating default admin user")
var user *db.User
if user, err = generateDefaultAdminUser(dbRepo); err != nil {
return
}
slog.Info(fmt.Sprintf(
`
DEFAULT ADMINISTRATOR CREDENTIALS:
CID: %d
Password: %s
`,
user.CID,
user.Password,
))
}
// Ensure default configuration is written to persistent storage
slog.Debug("initializing default config")
if err = db.InitDefaultConfig(&dbRepo.ConfigRepo); err != nil {
return
}
slog.Debug("config OK")
if server, err = NewServer(config, dbRepo, config.NumMetarWorkers); err != nil {
return
}
return
}
func generateDefaultAdminUser(dbRepo *db.Repositories) (user *db.User, err error) {
passwordBuf := make([]byte, 8)
if _, err = io.ReadFull(rand.Reader, passwordBuf); err != nil {
return
}
password := hex.EncodeToString(passwordBuf)
user = &db.User{
Password: password,
FirstName: strPtr("Default Administrator"),
NetworkRating: int(NetworkRatingAdministator),
}
if err = dbRepo.UserRepo.CreateUser(user); err != nil {
return
}
return
}
func (s *Server) Run(ctx context.Context) (err error) {
// Start metar service
go s.metarService.run(ctx)
// Start HTTP service
go s.runServiceHTTP(ctx)
errCh := make(chan error, len(s.cfg.FsdListenAddrs))
var listenerWg sync.WaitGroup
for _, addr := range s.cfg.FsdListenAddrs {
slog.Info(fmt.Sprintf("Listening on %s\n", addr))
listenerWg.Add(1)
go func(ctx context.Context, addr string) {
defer listenerWg.Done()
s.listen(ctx, addr, errCh)
}(ctx, addr)
}
// Collect startup errors
go func() {
listenerWg.Wait()
close(errCh)
}()
var startupErrors []error
for err := range errCh {
startupErrors = append(startupErrors, err)
}
if len(startupErrors) > 0 {
return fmt.Errorf("some listeners failed: %v", startupErrors)
}
// All listeners started successfully; wait for context to be cancelled
<-ctx.Done()
return
}
func (s *Server) listen(ctx context.Context, addr string, errCh chan<- error) {
config := net.ListenConfig{}
listener, err := config.Listen(ctx, "tcp4", addr)
if err != nil {
errCh <- fmt.Errorf("failed to listen on %s: %w", addr, err)
return
}
defer listener.Close()
// Start a goroutine to close the listener when the context is cancelled
go func() {
<-ctx.Done()
listener.Close()
}()
// Accept connections in a loop
for {
conn, err := listener.Accept()
if err != nil {
if errors.Is(err, net.ErrClosed) {
// Listener was closed due to context cancellation; exit the loop
return
}
// Log or handle non-fatal accept errors
continue
}
// Handle the connection in another goroutine
go s.handleConn(ctx, conn)
}
}