mirror of
https://github.com/renorris/openfsd
synced 2026-03-22 06:25:35 +08:00
1. Database Enhancements (db/repositories.go): - Added ConfigRepository interface and implementations for PostgreSQL and SQLite - Updated Repositories struct to include ConfigRepository - Modified NewRepositories to initialize both UserRepo and ConfigRepo 2. FSD Server Improvements: - Removed hardcoded jwtSecret, now retrieved from ConfigRepository (fsd/conn.go, web/auth.go) - Added dynamic welcome message retrieval from ConfigRepository (fsd/conn.go) - Optimized METAR buffer size from 4096 to 512 bytes (fsd/metar.go) - Reduced minimum fields for DeleteATC and DeletePilot packets (fsd/packet.go) - Improved Haversine distance calculation with constants (fsd/postoffice.go) - Added thread-safety documentation for sendError (fsd/client.go) 3. Server Configuration (fsd/server.go): - Added NewDefaultServer to initialize server with environment-based config - Implemented automatic database migration and default admin user creation - Added configurable METAR worker count - Improved logging with slog and environment-based debug level 4. Web Interface Enhancements: - Added user and config editor frontend routes (web/frontend.go, web/routes.go) - Improved JWT handling by retrieving secret from ConfigRepository (web/auth.go) - Enhanced user management API endpoints (web/user.go) - Updated dashboard to display CID and conditional admin links (web/templates/dashboard.html) - Embedded templates using go:embed (web/templates.go) 5. Frontend JavaScript Improvements: - Added networkRatingFromInt helper for readable ratings (web/static/js/openfsd/dashboard.js) - Improved API request handling with auth/no-auth variants (web/static/js/openfsd/api.js) 6. Miscellaneous: - Added sethvargo/go-envconfig dependency for environment variable parsing - Fixed parseVisRange to use 64-bit float parsing (fsd/util.go) - Added strPtr utility function (fsd/util.go, web/main.go) - Improved SVG logo rendering in layout (web/templates/layout.html)
194 lines
4.2 KiB
Go
194 lines
4.2 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 {
|
|
listenAddrs []string
|
|
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(listenAddrs []string, dbRepo *db.Repositories, numMetarWorkers int) (server *Server, err error) {
|
|
server = &Server{
|
|
listenAddrs: listenAddrs,
|
|
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 = dbRepo.ConfigRepo.InitDefault(); err != nil {
|
|
return
|
|
}
|
|
slog.Debug("config OK")
|
|
|
|
if server, err = NewServer(config.FsdListenAddrs, 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)
|
|
|
|
errCh := make(chan error, len(s.listenAddrs))
|
|
var listenerWg sync.WaitGroup
|
|
|
|
for _, addr := range s.listenAddrs {
|
|
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)
|
|
}
|
|
}
|