From 1b9e2da3b3c438d8dc53d3489725dd5ba0022777 Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Thu, 11 Jun 2026 04:46:27 -0400 Subject: feat: web client, get/drop updates and fixes --- internal/net/conn.go | 101 ++++++++++++++++++++++++++++ internal/net/server.go | 153 +++++++++++++++++++++++++++++++----------- internal/net/terminal.html | 164 +++++++++++++++++++++++++++++++++++++++++++++ internal/net/web.go | 72 ++++++++++++++++++++ internal/net/wsconn.go | 34 ++++++++++ 5 files changed, 484 insertions(+), 40 deletions(-) create mode 100644 internal/net/conn.go create mode 100644 internal/net/terminal.html create mode 100644 internal/net/web.go create mode 100644 internal/net/wsconn.go (limited to 'internal/net') diff --git a/internal/net/conn.go b/internal/net/conn.go new file mode 100644 index 0000000..e46031b --- /dev/null +++ b/internal/net/conn.go @@ -0,0 +1,101 @@ +package net + +import ( + "bufio" + "bytes" + "io" + "net" + "strings" +) + +type Conn interface { + ReadMessage() (string, error) + io.Writer + io.Closer + SetEcho(bool) error +} + +type tcpConn struct { + conn net.Conn + reader *bufio.Reader + echoOff bool +} + +func newTCPConn(conn net.Conn) *tcpConn { + return &tcpConn{ + conn: conn, + reader: bufio.NewReader(conn), + } +} + +func (t *tcpConn) ReadMessage() (string, error) { + line, err := t.reader.ReadString('\n') + if err != nil { + return "", err + } + line = strings.TrimRight(line, "\r\n") + line = stripIAC(line) + if t.echoOff { + t.conn.Write([]byte("\r\n")) + } + return line, nil +} + +func (t *tcpConn) Write(b []byte) (int, error) { + return t.conn.Write(b) +} + +func (t *tcpConn) Close() error { + return t.conn.Close() +} + +func (t *tcpConn) SetEcho(on bool) error { + t.echoOff = !on + if on { + return t.writeTelnetCmd(wont, echo) + } + return t.writeTelnetCmd(will, echo) +} + +const ( + iac = 255 + will = 251 + wont = 252 + do = 253 + dont = 254 + sb = 250 + se = 240 + echo = 1 +) + +func (t *tcpConn) writeTelnetCmd(cmd, opt byte) error { + _, err := t.conn.Write([]byte{iac, cmd, opt}) + return err +} + +func stripIAC(s string) string { + b := []byte(s) + var out []byte + i := 0 + iacSE := []byte{iac, se} + for i < len(b) { + if b[i] == iac && i+2 < len(b) { + if b[i+1] == sb { + end := bytes.Index(b[i+2:], iacSE) + if end >= 0 { + i += end + 4 + continue + } + } + i += 3 + continue + } + if b[i] == iac { + i++ + continue + } + out = append(out, b[i]) + i++ + } + return string(out) +} diff --git a/internal/net/server.go b/internal/net/server.go index 08d427f..7e73bc9 100644 --- a/internal/net/server.go +++ b/internal/net/server.go @@ -1,11 +1,13 @@ package net import ( - "bufio" + "crypto/tls" "fmt" "log" "net" - "strings" + "net/http" + + "thirdcollapse/internal/config" ) type SessionState int @@ -30,14 +32,13 @@ const ( ) type Session struct { - Conn net.Conn - Reader *bufio.Reader - State SessionState - Account *AccountEntry - Player interface{} // *player.Player once character is selected - PendingChar string // char being renamed/deleted - PendingPass string // first password during signup - Disconnecting bool + Conn Conn + State SessionState + Account *AccountEntry + Player interface{} + PendingChar string + PendingPass string + Disconnecting bool DisconnectTicks int } @@ -49,14 +50,18 @@ type AccountEntry struct { } type Server struct { - listener net.Listener - hub *Hub + config *config.Config + telnetLn 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) + sessions map[*Session]bool + rooms map[int]map[*Session]bool + onRemove func(*Session) } func NewHub() *Hub { @@ -125,12 +130,40 @@ func (h *Hub) PlayersInRoom(roomID int) []*Session { return out } -func NewServer(addr string) (*Server, error) { - l, err := net.Listen("tcp", addr) - if err != nil { - return nil, err +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.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 &Server{listener: l, hub: NewHub()}, nil + + return s, nil } func (s *Server) Hub() *Hub { @@ -138,31 +171,66 @@ func (s *Server) Hub() *Hub { } func (s *Server) ListenAndServe(handler func(*Session, string)) error { + s.handler = handler + + if s.telnetLn != nil { + go s.serveTelnet() + } + + if s.httpServer != nil { + go s.serveHTTP() + } + + if s.httpsServer != nil { + go s.serveHTTPS() + } + + if s.telnetLn == nil && s.httpServer == nil && s.httpsServer == nil { + return fmt.Errorf("no listeners configured") + } + + select {} +} + +func (s *Server) serveTelnet() { for { - conn, err := s.listener.Accept() + conn, err := s.telnetLn.Accept() if err != nil { - return err + return } - session := &Session{ - Conn: conn, - Reader: bufio.NewReader(conn), - State: StateAccountName, + sess := &Session{ + Conn: newTCPConn(conn), + State: StateAccountName, } - s.hub.Add(session) - go s.handleSession(session, handler) + s.hub.Add(sess) + go s.handleSession(sess, s.handler) } } -func (s *Server) handleSession(session *Session, handler func(*Session, string)) { +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 (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(session) - session.Conn.Close() + s.hub.Remove(sess) + sess.Close() }() - _, _ = session.Conn.Write([]byte("\033[2J\033[H")) // clear screen + sess.Conn.Write([]byte("\033[2J\033[H")) welcomeart := ` , 3333333 333 333 333 3333333 3333333 @@ -179,29 +247,30 @@ func (s *Server) handleSession(session *Session, handler func(*Session, string)) :!! !!: !!! !!: !!: !!: !!! !!: !:! !!: :: :: : : :. : : ::.: : : ::.: : : : : : ::.: : : :: ::: - ` - session.Write(welcomeart) - session.Write("ACCOUNT NAME> ") + sess.Write(welcomeart) + sess.Write("What's your account name? ") for { - line, err := session.Reader.ReadString('\n') + line, err := sess.Conn.ReadMessage() if err != nil { log.Printf("session read error: %v", err) return } - line = strings.TrimSpace(line) - handler(session, line) + if len(line) > 1024 { + line = line[:1024] + } + handler(sess, line) } } func (sess *Session) Write(msg string) { - _, _ = sess.Conn.Write([]byte(msg)) + sess.Conn.Write([]byte(msg)) } func (sess *Session) WriteLine(msg string) { - _, _ = sess.Conn.Write([]byte(msg + "\r\n")) + sess.Conn.Write([]byte(msg + "\r\n")) } func (sess *Session) WriteLines(lines ...string) { @@ -213,3 +282,7 @@ func (sess *Session) WriteLines(lines ...string) { func (sess *Session) Writef(format string, args ...interface{}) { sess.Write(fmt.Sprintf(format, args...)) } + +func (sess *Session) Close() error { + return sess.Conn.Close() +} diff --git a/internal/net/terminal.html b/internal/net/terminal.html new file mode 100644 index 0000000..2d98178 --- /dev/null +++ b/internal/net/terminal.html @@ -0,0 +1,164 @@ + + + + + +Third Collapse + + + +

+
+ > + +
+ + + diff --git a/internal/net/web.go b/internal/net/web.go new file mode 100644 index 0000000..e573aba --- /dev/null +++ b/internal/net/web.go @@ -0,0 +1,72 @@ +package net + +import ( + "embed" + "log" + "net" + "net/http" + "strings" + + "github.com/gorilla/websocket" +) + +//go:embed terminal.html +var terminalHTML embed.FS + +var upgrader = websocket.Upgrader{ + ReadBufferSize: 1024, + WriteBufferSize: 1024, + CheckOrigin: func(r *http.Request) bool { + origin := r.Header.Get("Origin") + if origin == "" { + return true + } + host := r.Host + hostname, _, err := net.SplitHostPort(host) + if err != nil { + hostname = host + } + if strings.EqualFold(origin, "http://"+host) || + strings.EqualFold(origin, "https://"+host) || + strings.EqualFold(origin, "http://"+hostname) || + strings.EqualFold(origin, "https://"+hostname) { + return true + } + if hn, _, err := net.SplitHostPort(r.RemoteAddr); err == nil && + (hn == "127.0.0.1" || hn == "::1") { + return true + } + log.Printf("ws rejected origin: %s (host: %s)", origin, r.Host) + return false + }, +} + +func newHTTPMux(s *Server) *http.ServeMux { + mux := http.NewServeMux() + + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + data, err := terminalHTML.ReadFile("terminal.html") + if err != nil { + http.Error(w, "not found", http.StatusNotFound) + return + } + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.Write(data) + }) + + mux.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) { + ws, err := upgrader.Upgrade(w, r, nil) + if err != nil { + log.Printf("ws upgrade error: %v", err) + return + } + sess := &Session{ + Conn: newWSConn(ws), + State: StateAccountName, + } + s.hub.Add(sess) + go s.handleSession(sess, s.handler) + }) + + return mux +} diff --git a/internal/net/wsconn.go b/internal/net/wsconn.go new file mode 100644 index 0000000..d6f45dc --- /dev/null +++ b/internal/net/wsconn.go @@ -0,0 +1,34 @@ +package net + +import ( + "github.com/gorilla/websocket" +) + +type wsConn struct { + conn *websocket.Conn +} + +func newWSConn(conn *websocket.Conn) *wsConn { + return &wsConn{conn: conn} +} + +func (w *wsConn) ReadMessage() (string, error) { + _, msg, err := w.conn.ReadMessage() + return string(msg), err +} + +func (w *wsConn) Write(b []byte) (int, error) { + err := w.conn.WriteMessage(websocket.TextMessage, b) + return len(b), err +} + +func (w *wsConn) Close() error { + return w.conn.Close() +} + +func (w *wsConn) SetEcho(on bool) error { + if on { + return w.conn.WriteMessage(websocket.TextMessage, []byte("\x1b]10;0\x07")) + } + return w.conn.WriteMessage(websocket.TextMessage, []byte("\x1b]10;1\x07")) +} -- cgit v1.2.3