DustyP 9 months ago
parent 5b13458e10
commit a9303bda3a

@ -33,6 +33,7 @@ func main() {
dbPath := flag.String("db", "./data/derby.db", "Path to SQLite database file") dbPath := flag.String("db", "./data/derby.db", "Path to SQLite database file")
noWeb := flag.Bool("no-web", false, "Disable web interface") noWeb := flag.Bool("no-web", false, "Disable web interface")
noTerminal := flag.Bool("no-terminal", false, "Disable terminal interface") noTerminal := flag.Bool("no-terminal", false, "Disable terminal interface")
useHTTP2 := flag.Bool("http2", true, "Enable HTTP/2 (requires TLS)")
flag.Parse() flag.Parse()
// Create a new derby clock connection // Create a new derby clock connection
@ -73,7 +74,7 @@ func main() {
wg.Add(1) wg.Add(1)
go func() { go func() {
defer wg.Done() defer wg.Done()
startWebInterface(clock, webEvents, *webPort, *dbPath, ctx) startWebInterface(clock, webEvents, *webPort, *dbPath, *useHTTP2, ctx)
}() }()
} }
@ -101,13 +102,13 @@ func main() {
} }
// startWebInterface initializes and runs the web interface // startWebInterface initializes and runs the web interface
func startWebInterface(clock *derby.DerbyClock, events <-chan derby.Event, webPort int, dbPath string, ctx context.Context) { func startWebInterface(clock *derby.DerbyClock, events <-chan derby.Event, webPort int, dbPath string, useHTTP2 bool, ctx context.Context) {
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelInfo, Level: slog.LevelInfo,
})) }))
// Create and start the web server // Create and start the web server
server, err := web.NewServer(clock, events, dbPath, webPort, logger) server, err := web.NewServer(clock, events, dbPath, webPort, useHTTP2, logger)
if err != nil { if err != nil {
logger.Error("Error creating web server", "error", err) logger.Error("Error creating web server", "error", err)
return return

@ -1,15 +1,25 @@
package web package web
import ( import (
"bytes"
"context" "context"
"crypto/rand"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"embed" "embed"
"encoding/json" "encoding/json"
"encoding/pem"
"fmt" "fmt"
"io/fs" "io/fs"
"log" "log"
"log/slog" "log/slog"
"math/big"
"net"
"net/http" "net/http"
"net/url" "net/url"
"os"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
@ -42,10 +52,11 @@ type Server struct {
logger *slog.Logger logger *slog.Logger
db *db.DB db *db.DB
adminEvents chan string adminEvents chan string
useHTTP2 bool
} }
// NewServer creates a new web server // NewServer creates a new web server
func NewServer(clock *derby.DerbyClock, events <-chan derby.Event, dbPath string, port int, logger *slog.Logger) (*Server, error) { func NewServer(clock *derby.DerbyClock, events <-chan derby.Event, dbPath string, port int, useHTTP2 bool, logger *slog.Logger) (*Server, error) {
// Initialize database // Initialize database
database, err := db.New(dbPath) database, err := db.New(dbPath)
if err != nil { if err != nil {
@ -66,6 +77,7 @@ func NewServer(clock *derby.DerbyClock, events <-chan derby.Event, dbPath string
logger: logger, logger: logger,
db: database, db: database,
adminEvents: make(chan string, 10), adminEvents: make(chan string, 10),
useHTTP2: useHTTP2,
} }
// Set up routes // Set up routes
@ -218,34 +230,114 @@ func (s *Server) routes() {
} }
// Start starts the web server // Start starts the web server with HTTP/2 support
func (s *Server) Start() error { func (s *Server) Start() error {
addr := fmt.Sprintf(":%d", s.port) addr := fmt.Sprintf(":%d", s.port)
// Configure TLS
tlsConfig := &tls.Config{
MinVersion: tls.VersionTLS12,
PreferServerCipherSuites: true,
CipherSuites: []uint16{
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
},
NextProtos: []string{"h2", "http/1.1"},
}
s.server = &http.Server{ s.server = &http.Server{
Addr: addr, Addr: addr,
Handler: s.router, Handler: s.router,
// Add these settings to handle multiple concurrent connections TLSConfig: tlsConfig,
ReadTimeout: 120 * time.Second, // Longer timeout for SSE ReadTimeout: 120 * time.Second,
WriteTimeout: 120 * time.Second, // Longer timeout for SSE WriteTimeout: 120 * time.Second,
IdleTimeout: 120 * time.Second, IdleTimeout: 120 * time.Second,
MaxHeaderBytes: 1 << 20, // 1 MB
} }
// Log middleware configuration // Check if certificate files exist
s.logger.Info("Chi router middleware configuration", certFile := "server.crt"
"timeout", "120s", keyFile := "server.key"
"recoverer", true,
"logger", true) certExists := fileExists(certFile)
keyExists := fileExists(keyFile)
s.logger.Info("Starting web server", "port", s.port, "http2", true)
if certExists && keyExists {
// Start HTTPS server with HTTP/2 support
s.logger.Info("Using TLS with HTTP/2", "certFile", certFile, "keyFile", keyFile)
return s.server.ListenAndServeTLS(certFile, keyFile)
} else {
// Generate self-signed certificate for development
s.logger.Info("Generating self-signed certificate for HTTP/2")
cert, key, err := generateSelfSignedCert()
if err != nil {
s.logger.Error("Failed to generate self-signed certificate", "error", err)
// Start server in a goroutine // Fall back to HTTP/1.1
go func() { s.logger.Info("Falling back to HTTP/1.1 (no TLS)")
s.logger.Info("Web server starting", "port", s.port) s.server.TLSConfig = nil
if err := s.server.ListenAndServe(); err != nil && err != http.ErrServerClosed { return s.server.ListenAndServe()
s.logger.Error("HTTP server error", "error", err)
} }
}()
return nil // Write certificate files
if err := os.WriteFile(certFile, cert, 0600); err != nil {
s.logger.Error("Failed to write certificate file", "error", err)
return err
}
if err := os.WriteFile(keyFile, key, 0600); err != nil {
s.logger.Error("Failed to write key file", "error", err)
return err
}
s.logger.Info("Self-signed certificate generated", "certFile", certFile, "keyFile", keyFile)
return s.server.ListenAndServeTLS(certFile, keyFile)
}
}
// Helper function to check if a file exists
func fileExists(filename string) bool {
_, err := os.Stat(filename)
return err == nil
}
// generateSelfSignedCert creates a self-signed certificate for development
func generateSelfSignedCert() ([]byte, []byte, error) {
priv, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return nil, nil, err
}
template := x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{
Organization: []string{"Derby Race Manager"},
CommonName: "localhost",
},
NotBefore: time.Now(),
NotAfter: time.Now().Add(365 * 24 * time.Hour),
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
IPAddresses: []net.IP{net.ParseIP("127.0.0.1")},
DNSNames: []string{"localhost"},
}
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
if err != nil {
return nil, nil, err
}
certPEM := &bytes.Buffer{}
pem.Encode(certPEM, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
keyPEM := &bytes.Buffer{}
pem.Encode(keyPEM, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)})
return certPEM.Bytes(), keyPEM.Bytes(), nil
} }
// Shutdown gracefully shuts down the server // Shutdown gracefully shuts down the server

@ -186,6 +186,19 @@
}); });
} }
}); });
// Update URLs to use https when connecting to SSE
document.addEventListener('DOMContentLoaded', function() {
// Check if we're already on HTTPS
const protocol = window.location.protocol;
if (protocol === 'https:') {
console.log('Already using HTTPS');
} else {
console.log('Using HTTP, SSE connections may be limited');
// Optionally redirect to HTTPS
// window.location.href = 'https://' + window.location.host + window.location.pathname;
}
});
</script> </script>
</body> </body>
</html> </html>
Loading…
Cancel
Save