From e9d87838590a02c7ca028fad8cd16e42a582dd32 Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Mon, 29 Jun 2026 01:00:17 -0400 Subject: feat: self-signed certificates --- internal/config/config.go | 32 ++++++------ internal/game/cmd_consume.go | 4 +- internal/game/cmd_verbs.go | 22 ++++---- internal/game/combat_mob.go | 2 +- internal/game/core_login_char.go | 4 +- internal/game/game.go | 8 +-- internal/net/server.go | 106 ++++++++++++++++++++++++++++++++------- internal/player/player.go | 2 +- 8 files changed, 124 insertions(+), 56 deletions(-) (limited to 'internal') diff --git a/internal/config/config.go b/internal/config/config.go index 91d5986..6393816 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -7,12 +7,14 @@ import ( ) type Config struct { - Game GameConfig `yaml:"game"` - Colors ColorsConfig `yaml:"colors"` - Telnet TelnetConfig `yaml:"telnet"` - TelnetTLS TelnetTLSConfig `yaml:"telnet_tls"` - HTTP HTTPConfig `yaml:"http"` - HTTPS HTTPSConfig `yaml:"https"` + TickLength int `yaml:"tick_length"` + StartingRoom int `yaml:"starting_room"` + StartupValidation ValidationConfig `yaml:"startup_validation"` + DefaultColors ColorsConfig `yaml:"default_colors"` + Telnet TelnetConfig `yaml:"telnet"` + TelnetTLS TelnetTLSConfig `yaml:"telnet_tls"` + HTTP HTTPConfig `yaml:"http"` + HTTPS HTTPSConfig `yaml:"https"` } type ColorsConfig map[string]string @@ -63,11 +65,6 @@ func DefaultColors() ColorsConfig { } } -type GameConfig struct { - TickLength int `yaml:"tick_length"` - StartupValidation ValidationConfig `yaml:"startup_validation"` -} - type ValidationConfig struct { RootRooms []int `yaml:"root_rooms"` IgnoreUnreachable []int `yaml:"ignore_unreachable"` @@ -99,14 +96,13 @@ type HTTPSConfig struct { func Default() *Config { return &Config{ - Game: GameConfig{ - TickLength: 600, - StartupValidation: ValidationConfig{ - RootRooms: []int{1}, - IgnoreUnreachable: []int{}, - }, + TickLength: 600, + StartingRoom: 1001, + StartupValidation: ValidationConfig{ + RootRooms: []int{1}, + IgnoreUnreachable: []int{}, }, - Colors: DefaultColors(), + DefaultColors: DefaultColors(), Telnet: TelnetConfig{ Enabled: true, Port: 4000, diff --git a/internal/game/cmd_consume.go b/internal/game/cmd_consume.go index 423cddb..aec33c5 100644 --- a/internal/game/cmd_consume.go +++ b/internal/game/cmd_consume.go @@ -71,7 +71,7 @@ func (g *Game) doEat(sess *net.Session, input string) { Timestamp: time.Now(), }) - if !p.OptionBool("queue_silently") { + if p.OptionBool("show_queued_cmds") { sess.WriteLine(fmt.Sprintf("You prepare to eat %s.", g.itemColorize(sess, def, def.Name))) } } @@ -206,7 +206,7 @@ func (g *Game) doDrink(sess *net.Session, args []string) { Timestamp: time.Now(), }) - if !p.OptionBool("queue_silently") { + if p.OptionBool("show_queued_cmds") { sess.WriteLine(fmt.Sprintf("You prepare to drink the %s.", g.itemColorize(sess, def, def.Name))) } } diff --git a/internal/game/cmd_verbs.go b/internal/game/cmd_verbs.go index d07ef2f..70b2fb2 100644 --- a/internal/game/cmd_verbs.go +++ b/internal/game/cmd_verbs.go @@ -9,7 +9,7 @@ import ( ) var alwaysAvailable = []string{ - "look", "l", + "look", "say", "global", "who", @@ -17,30 +17,30 @@ var alwaysAvailable = []string{ "map", "walk", "score", "stats", - "inventory", "inv", "i", - "equipment", "eq", "equip", "wear", "wield", - "remove", "unwear", "unwield", + "inventory", + "equip", + "remove", "style", - "tech", "t", - "option", "options", - "color", "colors", "colortable", + "tech", + "option", + "color", "colortable", "prompt", "alias", "unalias", "help", "queued", "stop", "aps", - "autotrigger", "auto", + "autotrigger", "trigger", "sneak", - "mods", "modules", "modlist", "mod", "module", - "id", "identify", + "mods", + "identify", "eat", "drink", "fletch", "clean", "burn", "stoke", "search", - "get", "take", "grab", "pick", + "get", "drop", } diff --git a/internal/game/combat_mob.go b/internal/game/combat_mob.go index a4bf435..4cdd27b 100644 --- a/internal/game/combat_mob.go +++ b/internal/game/combat_mob.go @@ -270,7 +270,7 @@ func (g *Game) killPlayer(sess *net.Session, p *player.Player, mob *world.MobIns } g.dropItemsOnDeath(p) p.HP = p.MaxHP() - p.RoomID = 1001 + p.RoomID = g.StartingRoom g.AccountStore.SaveCharacter(p) if g.Hub != nil { g.Hub.EnterRoom(sess, p.RoomID) diff --git a/internal/game/core_login_char.go b/internal/game/core_login_char.go index d705684..b1d3d26 100644 --- a/internal/game/core_login_char.go +++ b/internal/game/core_login_char.go @@ -41,8 +41,8 @@ func (g *Game) handleNewCharName(sess *net.Session, input string) { } p := player.New(name) - p.RoomID = 1001 - p.Stats.RecordRoomVisit(1001) + p.RoomID = g.StartingRoom + p.Stats.RecordRoomVisit(g.StartingRoom) if err := g.AccountStore.SaveCharacter(p); err != nil { sess.WriteLine(fmt.Sprintf("Error creating character: %v", err)) diff --git a/internal/game/game.go b/internal/game/game.go index bed75b0..c30f9aa 100644 --- a/internal/game/game.go +++ b/internal/game/game.go @@ -56,6 +56,7 @@ type Game struct { queue *CommandQueue safespot *SafespotManager ValidationConfig config.ValidationConfig + StartingRoom int // Per-tick / per-session runtime bookkeeping. charsMu sync.Mutex @@ -73,7 +74,7 @@ type Game struct { shutdownMu sync.Mutex } -func New(dataDir string, colorConfig *config.ColorsConfig, valConfig config.ValidationConfig) *Game { +func New(dataDir string, colorConfig *config.ColorsConfig, valConfig config.ValidationConfig, startingRoom int) *Game { g := &Game{ Deps: Deps{ World: world.New(dataDir), @@ -93,6 +94,7 @@ func New(dataDir string, colorConfig *config.ColorsConfig, valConfig config.Vali queue: NewCommandQueue(), safespot: NewSafespotManager(), ValidationConfig: valConfig, + StartingRoom: startingRoom, loggedInChars: make(map[string]*net.Session), restTimers: make(map[string]uint64), guardWatchTimers: make(map[string]int), @@ -251,7 +253,7 @@ func (g *Game) handleGameCommand(sess *net.Session, input string) { Args: strings.Join(parts[1:], " "), Timestamp: time.Now(), }) - if !p.OptionBool("queue_silently") { + if p.OptionBool("show_queued_cmds") { sess.WriteLine(fmt.Sprintf("You prepare to %s.", cmd)) } return @@ -270,7 +272,7 @@ func (g *Game) handleGameCommand(sess *net.Session, input string) { Timestamp: time.Now(), }) g.cancelRest(p.Name) - if !p.OptionBool("queue_silently") { + if p.OptionBool("show_queued_cmds") { sess.WriteLine(fmt.Sprintf("You prepare to %s.", cmd)) } } diff --git a/internal/net/server.go b/internal/net/server.go index 851a4b5..345122e 100644 --- a/internal/net/server.go +++ b/internal/net/server.go @@ -1,12 +1,18 @@ package net import ( + "crypto/rand" + "crypto/rsa" "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" "fmt" "log" + "math/big" "net" "net/http" "sync" + "time" "thehouseoficarus/internal/behavior" "thehouseoficarus/internal/color" @@ -70,13 +76,14 @@ type Session struct { } type Server struct { - config *config.Config - telnetLn net.Listener - telnetTLSLn net.Listener - httpServer *http.Server - httpsServer *http.Server - hub *Hub - handler func(*Session, string) + config *config.Config + telnetLn net.Listener + telnetTLSLn net.Listener + httpServer *http.Server + httpsServer *http.Server + httpsTLSConfig *tls.Config + hub *Hub + handler func(*Session, string) } type Hub struct { @@ -192,12 +199,20 @@ func NewServer(cfg *config.Config) (*Server, error) { } if cfg.TelnetTLS.Enabled { + var cert tls.Certificate if cfg.TelnetTLS.CertFile == "" || cfg.TelnetTLS.KeyFile == "" { - return nil, fmt.Errorf("telnet_tls enabled but cert_file and key_file are required") - } - cert, err := tls.LoadX509KeyPair(cfg.TelnetTLS.CertFile, cfg.TelnetTLS.KeyFile) - if err != nil { - return nil, fmt.Errorf("telnet_tls cert: %w", err) + var err error + cert, err = generateSelfSignedCert() + if err != nil { + return nil, fmt.Errorf("telnet_tls self-signed cert: %w", err) + } + log.Printf("telnet_tls: using self-signed certificate (no cert_file/key_file configured)") + } else { + var err error + cert, err = tls.LoadX509KeyPair(cfg.TelnetTLS.CertFile, cfg.TelnetTLS.KeyFile) + if err != nil { + return nil, fmt.Errorf("telnet_tls cert: %w", err) + } } tlsCfg := &tls.Config{ Certificates: []tls.Certificate{cert}, @@ -219,19 +234,68 @@ func NewServer(cfg *config.Config) (*Server, error) { } if cfg.HTTPS.Enabled { + var cert tls.Certificate if cfg.HTTPS.CertFile == "" || cfg.HTTPS.KeyFile == "" { - return nil, fmt.Errorf("https enabled but cert_file and key_file are required") + var err error + cert, err = generateSelfSignedCert() + if err != nil { + return nil, fmt.Errorf("https self-signed cert: %w", err) + } + log.Printf("https: using self-signed certificate (no cert_file/key_file configured)") + } else { + var err error + cert, err = tls.LoadX509KeyPair(cfg.HTTPS.CertFile, cfg.HTTPS.KeyFile) + if err != nil { + return nil, fmt.Errorf("https cert: %w", err) + } + } + s.httpsTLSConfig = &tls.Config{ + Certificates: []tls.Certificate{cert}, + MinVersion: tls.VersionTLS12, } s.httpsServer = &http.Server{ - Addr: fmt.Sprintf(":%d", cfg.HTTPS.Port), - Handler: newHTTPMux(s), - TLSConfig: &tls.Config{MinVersion: tls.VersionTLS12}, + Addr: fmt.Sprintf(":%d", cfg.HTTPS.Port), + Handler: newHTTPMux(s), } } return s, nil } +func generateSelfSignedCert() (tls.Certificate, error) { + key, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return tls.Certificate{}, fmt.Errorf("rsa key generation: %w", err) + } + serial, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128)) + if err != nil { + return tls.Certificate{}, fmt.Errorf("serial number: %w", err) + } + now := time.Now() + template := &x509.Certificate{ + SerialNumber: serial, + Subject: pkix.Name{ + CommonName: "THOI Self-Signed Certificate", + }, + NotBefore: now.Add(-1 * time.Hour), + NotAfter: now.Add(365 * 24 * time.Hour), + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + IsCA: true, + DNSNames: []string{"localhost"}, + IPAddresses: []net.IP{net.ParseIP("127.0.0.1"), net.ParseIP("::1")}, + } + certDER, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key) + if err != nil { + return tls.Certificate{}, fmt.Errorf("x509 creation: %w", err) + } + return tls.Certificate{ + Certificate: [][]byte{certDER}, + PrivateKey: key, + }, nil +} + func (s *Server) Hub() *Hub { return s.hub } @@ -300,8 +364,14 @@ func (s *Server) serveHTTP() { } func (s *Server) serveHTTPS() { - err := s.httpsServer.ListenAndServeTLS(s.config.HTTPS.CertFile, s.config.HTTPS.KeyFile) - if err != nil && err != http.ErrServerClosed { + addr := fmt.Sprintf(":%d", s.config.HTTPS.Port) + ln, err := net.Listen("tcp", addr) + if err != nil { + log.Printf("https server error: %v", err) + return + } + tlsLn := tls.NewListener(ln, s.httpsTLSConfig) + if err := s.httpsServer.Serve(tlsLn); err != nil && err != http.ErrServerClosed { log.Printf("https server error: %v", err) } } diff --git a/internal/player/player.go b/internal/player/player.go index 7d9b397..845dbb0 100644 --- a/internal/player/player.go +++ b/internal/player/player.go @@ -124,7 +124,7 @@ var OptionDefs = []OptionDef{ {"map_height", OptInt, 20, nil, "Map height for the map command"}, {"map_padding", OptString, "none", []string{"none", "x", "y", "xy"}, "Map output padding mode"}, {"automap", OptBool, false, nil, "Show map automatically after moving"}, - {"queue_silently", OptBool, true, nil, "Suppress messages for queued tick actions"}, + {"show_queued_cmds", OptBool, false, nil, "Show confirmation messages for queued tick actions"}, {"room_desc_width", OptInt, 70, nil, "Maximum width for room descriptions"}, {"wrap_width", OptInt, 120, nil, "Wrap all output to this many columns (minimum 80)"}, {"unicode", OptBool, true, nil, "Unicode box-drawing characters"}, -- cgit v1.2.3