|
|
|
@ -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)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Fall back to HTTP/1.1
|
|
|
|
|
|
|
|
s.logger.Info("Falling back to HTTP/1.1 (no TLS)")
|
|
|
|
|
|
|
|
s.server.TLSConfig = nil
|
|
|
|
|
|
|
|
return s.server.ListenAndServe()
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Write certificate files
|
|
|
|
|
|
|
|
if err := os.WriteFile(certFile, cert, 0600); err != nil {
|
|
|
|
|
|
|
|
s.logger.Error("Failed to write certificate file", "error", err)
|
|
|
|
|
|
|
|
return err
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Start server in a goroutine
|
|
|
|
if err := os.WriteFile(keyFile, key, 0600); err != nil {
|
|
|
|
go func() {
|
|
|
|
s.logger.Error("Failed to write key file", "error", err)
|
|
|
|
s.logger.Info("Web server starting", "port", s.port)
|
|
|
|
return err
|
|
|
|
if err := s.server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
|
|
|
|
|
|
|
s.logger.Error("HTTP server error", "error", err)
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
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
|
|
|
|
|