package net import ( "crypto/tls" "fmt" "log" "net" "net/http" "strings" "thehouseoficarus/internal/color" "thehouseoficarus/internal/config" ) type SessionState int const ( StateAccountName SessionState = iota StatePassword StateNewAccountPass StateNewAccountConfirm StateMenu StateNewCharName StateNewCharConfirm StateRenameAccount StateRenameChar StateRenameCharName StateDeleteChar StatePurgeAccount StateGame StateChangeDescription StateTalk StateDropAllConfirm StateRecipeChoice StateHowMany StateSmithProduct StateFletchProduct StateCraftProduct StateProductChoice StateColorChoice ) type Session struct { Conn Conn State SessionState Account *AccountEntry Player interface{} PendingChar string PendingPass string PendingMenu []map[string]string PendingRecipeID string PendingSkill string PendingLastFlag string PendingBackground bool Disconnecting bool DisconnectTicks int } type AccountEntry struct { Name string PasswordHash string Characters []string Aliases map[string]string Colors map[string]string Options map[string]any } type Server struct { config *config.Config telnetLn net.Listener telnetTLSLn net.Listener httpServer *http.Server httpsServer *http.Server hub *Hub handler func(*Session, string) } type Hub struct { sessions map[*Session]bool rooms map[int]map[*Session]bool onRemove func(*Session) } func NewHub() *Hub { return &Hub{ sessions: make(map[*Session]bool), rooms: make(map[int]map[*Session]bool), } } func (h *Hub) OnRemove(cb func(*Session)) { h.onRemove = cb } func (h *Hub) Add(s *Session) { h.sessions[s] = true } func (h *Hub) Remove(s *Session) { if s.Player != nil && s.State == StateGame && !s.Disconnecting { s.Disconnecting = true s.DisconnectTicks = 10 return } h.HardRemove(s) } func (h *Hub) HardRemove(s *Session) { delete(h.sessions, s) for _, room := range h.rooms { delete(room, s) } if h.onRemove != nil { h.onRemove(s) } } func (h *Hub) EnterRoom(s *Session, roomID int) { h.LeaveRoom(s) if h.rooms[roomID] == nil { h.rooms[roomID] = make(map[*Session]bool) } h.rooms[roomID][s] = true } func (h *Hub) LeaveRoom(s *Session) { for _, room := range h.rooms { delete(room, s) } } func (h *Hub) AllSessions() []*Session { var out []*Session for s := range h.sessions { out = append(out, s) } return out } func (h *Hub) PlayersInRoom(roomID int) []*Session { var out []*Session if room, ok := h.rooms[roomID]; ok { for s := range room { out = append(out, s) } } return out } func NewServer(cfg *config.Config) (*Server, error) { s := &Server{ config: cfg, hub: NewHub(), } if cfg.Telnet.Enabled { addr := fmt.Sprintf(":%d", cfg.Telnet.Port) ln, err := net.Listen("tcp", addr) if err != nil { return nil, fmt.Errorf("telnet listen: %w", err) } s.telnetLn = ln } if cfg.TelnetTLS.Enabled { 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) } tlsCfg := &tls.Config{ Certificates: []tls.Certificate{cert}, MinVersion: tls.VersionTLS12, } addr := fmt.Sprintf(":%d", cfg.TelnetTLS.Port) ln, err := net.Listen("tcp", addr) if err != nil { return nil, fmt.Errorf("telnet_tls listen: %w", err) } s.telnetTLSLn = tls.NewListener(ln, tlsCfg) } if cfg.HTTP.Enabled { s.httpServer = &http.Server{ Addr: fmt.Sprintf(":%d", cfg.HTTP.Port), Handler: newHTTPMux(s), } } if cfg.HTTPS.Enabled { if cfg.HTTPS.CertFile == "" || cfg.HTTPS.KeyFile == "" { return nil, fmt.Errorf("https enabled but cert_file and key_file are required") } s.httpsServer = &http.Server{ Addr: fmt.Sprintf(":%d", cfg.HTTPS.Port), Handler: newHTTPMux(s), TLSConfig: &tls.Config{MinVersion: tls.VersionTLS12}, } } return s, nil } func (s *Server) Hub() *Hub { return s.hub } func (s *Server) ListenAndServe(handler func(*Session, string)) error { s.handler = handler if s.telnetLn != nil { go s.serveTelnet() } if s.telnetTLSLn != nil { go s.serveTelnetTLS() } if s.httpServer != nil { go s.serveHTTP() } if s.httpsServer != nil { go s.serveHTTPS() } if s.telnetLn == nil && s.telnetTLSLn == nil && s.httpServer == nil && s.httpsServer == nil { return fmt.Errorf("no listeners configured") } select {} } func (s *Server) serveTelnet() { for { conn, err := s.telnetLn.Accept() if err != nil { return } sess := &Session{ Conn: newTCPConn(conn), State: StateAccountName, } s.hub.Add(sess) go s.handleSession(sess, s.handler) } } func (s *Server) serveTelnetTLS() { for { conn, err := s.telnetTLSLn.Accept() if err != nil { return } sess := &Session{ Conn: newTCPConn(conn), State: StateAccountName, } s.hub.Add(sess) go s.handleSession(sess, s.handler) } } func (s *Server) serveHTTP() { err := s.httpServer.ListenAndServe() if err != nil && err != http.ErrServerClosed { log.Printf("http server error: %v", err) } } func (s *Server) serveHTTPS() { err := s.httpsServer.ListenAndServeTLS(s.config.HTTPS.CertFile, s.config.HTTPS.KeyFile) if err != nil && err != http.ErrServerClosed { log.Printf("https server error: %v", err) } } func artColor(r rune) string { switch r { case '@': return color.FgCode("ansi", 4) case '!', ':', '.': return color.StyleCode("dim") + color.FgCode("ansi", 4) case ',': return color.FgCode("ansi", 5) case '*': return color.FgCode("ansi", 5) case '+', '|', '-': return color.FgCode("ansi", 3) } return "" } func colorizeRunes(s string) string { var sb strings.Builder current := "" for _, r := range s { want := artColor(r) if want != current { if current != "" { sb.WriteString(color.Reset) } if want != "" { sb.WriteString(want) } current = want } sb.WriteRune(r) } if current != "" { sb.WriteString(color.Reset) } return sb.String() } func colorizeArt(art string) string { welcomeText := "welcome to the house of icarus" before, after, found := strings.Cut(art, welcomeText) if found { return colorizeRunes(before) + color.FgCode("ansi", 7) + welcomeText + color.Reset + colorizeRunes(after) } return colorizeRunes(art) } func (s *Server) handleSession(sess *Session, handler func(*Session, string)) { defer func() { if r := recover(); r != nil { log.Printf("session panic: %v", r) } s.hub.Remove(sess) sess.Close() }() sess.Conn.Write([]byte("\033[2J\033[H")) welcomeart := colorizeArt(` | * * + -+- . , | . , . , * @@@@@@@ @@@ @@@ @@@@@@@@ @@@ @@@ @@@@@@ @@@ @@@ @@@@@@ @@@@@@@@ + @@! @@! @@@ @@! + @@! @@@ @@! @@@ @@! @@@ !@@ @@! @!! @!@!@!@! @!!!:! @!@!@!@! @!@ !@! @!@ !@! !@@!! @!!!:! * !!: !!: !!! !!: . !!: !!! !!: !!! !!: !!! !:! !!: : : : : : :: ::: : : : : :. : :.:: : ::.: : : :: ::: . . + * welcome to the house of icarus , , , . @@@@@@ @@@@@@@@ @@@ @@@@@@@ @@@@@@ @@@@@@@ @@@ @@@ @@@@@@ @@! @@@ @@! , @@! !@@ @@! @@@ @@! @@@ @@! @@@ !@@ | @!@ !@! @!!!:! !!@ !@! @!@!@!@! @!@!!@! @!@ !@! !@@!! -+- !!: !!! !!: !!: :!! !!: !!! !!: :!! !!: !!! !:! | : :. : : . : :: :: : : : : : : : :.:: : ::.: : , . + , . . , * . , * + `) sess.Write(welcomeart) sess.Write("What's your account name? ") for { line, err := sess.Conn.ReadMessage() if err != nil { log.Printf("session read error: %v", err) return } if len(line) > 1024 { line = line[:1024] } handler(sess, line) } } func (sess *Session) Write(msg string) { sess.Conn.Write([]byte(msg)) } func (sess *Session) WriteLine(msg string) { sess.Conn.Write([]byte(msg + "\r\n")) } func (sess *Session) WriteLines(lines ...string) { for _, l := range lines { sess.WriteLine(l) } } func (sess *Session) Writef(format string, args ...interface{}) { sess.Write(fmt.Sprintf(format, args...)) } func (sess *Session) Close() error { return sess.Conn.Close() }