package main import ( "bytes" "crypto/sha256" "crypto/subtle" "crypto/tls" "encoding/hex" "errors" "flag" "fmt" "log" "net" "os" "os/signal" "strings" "sync" "syscall" "time" "github.com/BurntSushi/toml" "github.com/racingmars/go3270" "golang.org/x/crypto/bcrypt" ) func ensureSlotCapacity(s *UserMasterSession, slotIdx int) { for len(s.Slots) <= slotIdx { s.Slots = append(s.Slots, nil) } } var ( GlobalConfig *Config ConfiguredSystem []TargetSystem configFile string listen string certFile string keyFile string genPassword string publicFlag bool serverListener net.Listener shutdownFunc = func() { os.Exit(0) } adminNoticeSleep = 1500 * time.Millisecond registryMu sync.Mutex userSessions = make(map[string]*UserMasterSession) telnetEOR = []byte{IAC, EOR} telnetDoTimingMark = []byte{IAC, DO, OPT_TIME_MARK} telnetWillTimingMark = []byte{IAC, WILL, OPT_TIME_MARK} ) func init() { flag.StringVar(&configFile, "c", "config.toml", "Path to toml config file") flag.StringVar(&listen, "l", "0.0.0.0:2323", "Listen address") flag.StringVar(&certFile, "t", "", "Path to TLS certificate file (PEM)") flag.StringVar(&keyFile, "k", "", "Path to TLS private key file (PEM)") flag.StringVar(&genPassword, "p", "", "Generate bcrypt hash for password and exit") flag.BoolVar(&publicFlag, "public", false, "Allow public access") flag.BoolVar(&publicFlag, "P", false, "Allow public access") } func main() { flag.Parse() var hasGenPassword, hasListen bool flag.CommandLine.Visit(func(f *flag.Flag) { switch f.Name { case "p": hasGenPassword = true case "l": hasListen = true } }) if hasGenPassword { if len(strings.TrimSpace(genPassword)) < 3 { fmt.Fprintln(os.Stderr, "Error: Password cannot be less than 3 characters") os.Exit(1) } hash, err := bcrypt.GenerateFromPassword([]byte(genPassword), bcrypt.DefaultCost) if err != nil { fmt.Fprintf(os.Stderr, "Error generating bcrypt hash: %v\n", err) os.Exit(1) } fmt.Println(string(hash)) return } cfg, err := loadHosts(configFile) if err != nil { log.Fatalf("Configuration error: %v", err) } if hasListen { cfg.Server.BindAddress = listen } enabledTLS, certPath, keyPath, err := resolveTLSConfig(certFile, keyFile, &cfg.Server) if err != nil { log.Fatalf("TLS configuration error: %v", err) } if publicFlag { cfg.Public = true cfg.Server.Public = true } GlobalConfig = cfg ConfiguredSystem = cfg.Systems applyCodepage(cfg.Server.Codepage) log.Printf("Loaded configuration from %s (%d target systems, Codepage: %s)", configFile, len(ConfiguredSystem), cfg.Server.Codepage) if len(cfg.Users) > 0 { if isPublic() { log.Printf("Public access enabled (%d users configured)", len(cfg.Users)) } else { log.Printf("User whitelist enabled (%d users configured)", len(cfg.Users)) } } else { log.Printf("User whitelist not configured (public access mode)") } for _, sys := range ConfiguredSystem { tlsMode := "Plaintext" if sys.TLS { tlsMode = "TLS" } log.Printf(" + %s -> %s (%s)", sys.Name, sys.HostPort, tlsMode) } bindAddr := cfg.Server.BindAddress listener, err := startServerListener(bindAddr, enabledTLS, certPath, keyPath) if err != nil { log.Fatalf("Failed to bind on %s: %v", bindAddr, err) } serverListener = listener defer listener.Close() sigCh := make(chan os.Signal, 1) signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM) go func() { sig, ok := <-sigCh if !ok { return } log.Printf("Received signal %v: draining sessions and shutting down...", sig) for _, sess := range drainAllSessions() { terminateSession(sess, "Server shutting down.") } _ = listener.Close() }() defer signal.Stop(sigCh) if enabledTLS { log.Printf("Session Broker online and listening on %s (TLS)", bindAddr) } else { log.Printf("Session Broker online and listening on %s", bindAddr) } var tempDelay time.Duration for { conn, err := listener.Accept() if err != nil { if errors.Is(err, net.ErrClosed) { log.Printf("Listener closed, stopping main loop: %v", err) return } if tempDelay == 0 { tempDelay = 5 * time.Millisecond } else { tempDelay *= 2 } if maxDelay := 1 * time.Second; tempDelay > maxDelay { tempDelay = maxDelay } log.Printf("Accept error: %v; retrying in %v", err, tempDelay) time.Sleep(tempDelay) continue } tempDelay = 0 log.Printf("[%s] Accepted new inbound connection", conn.RemoteAddr()) go handleNewConnection(conn) } } func resolveTLSConfig(certFlag, keyFlag string, cfgServer *ServerConfig) (bool, string, string, error) { cert := strings.TrimSpace(certFlag) key := strings.TrimSpace(keyFlag) tlsEnabled := key != "" || cert != "" if tlsEnabled && (key == "" || cert == "") { return false, "", "", fmt.Errorf("-t and -k require both of each other") } if !tlsEnabled && cfgServer != nil && cfgServer.TLS { tlsEnabled = true cert = strings.TrimSpace(cfgServer.CertFile) key = strings.TrimSpace(cfgServer.KeyFile) if cert == "" || key == "" { return false, "", "", fmt.Errorf("both certificate and key are required when TLS is enabled") } } return tlsEnabled, cert, key, nil } func startServerListener(bindAddr string, enabledTLS bool, certPath, keyPath string) (net.Listener, error) { if enabledTLS { cert, err := tls.LoadX509KeyPair(certPath, keyPath) if err != nil { return nil, fmt.Errorf("failed to load TLS certificate/key (%s, %s): %w", certPath, keyPath, err) } tlsConfig := &tls.Config{ Certificates: []tls.Certificate{cert}, } return tls.Listen("tcp4", bindAddr, tlsConfig) } return net.Listen("tcp4", bindAddr) } func computeSessionKey(username, password string) string { h := sha256.New() h.Write([]byte(strings.TrimSpace(username))) h.Write([]byte(":")) h.Write([]byte(password)) return hex.EncodeToString(h.Sum(nil)) } func isPublic() bool { return publicFlag || (GlobalConfig != nil && (GlobalConfig.Public || GlobalConfig.Server.Public)) } func authenticateAndAuthorize(username, password string) AuthResult { if GlobalConfig == nil || len(GlobalConfig.Users) == 0 { return AuthResult{OK: true, Reason: "", IsAdmin: false} } trimmedUser := strings.TrimSpace(username) var matchedUser *UserConfig for i := range GlobalConfig.Users { if strings.TrimSpace(GlobalConfig.Users[i].Username) == trimmedUser { matchedUser = &GlobalConfig.Users[i] break } } if matchedUser != nil { if hash := matchedUser.GetHash(); hash != "" { if strings.HasPrefix(hash, "$2") { if err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)); err == nil { return AuthResult{OK: true, Reason: "", IsAdmin: matchedUser.Admin} } } else if subtle.ConstantTimeCompare([]byte(hash), []byte(password)) == 1 { log.Printf("[SECURITY WARNING] User '%s' authenticated with plaintext password in config. Use -p to hash it.", username) return AuthResult{OK: true, Reason: "", IsAdmin: matchedUser.Admin} } } } if isPublic() { return AuthResult{OK: true, Reason: "", IsAdmin: false} } return AuthResult{OK: false, Reason: fmt.Sprintf("user '%s' not authorized", username)} } func applyCodepage(cp string) { switch strings.ToLower(strings.TrimSpace(cp)) { case "1047", "cp1047", "ibm1047": go3270.SetCodepage(go3270.Codepage1047()) case "bracket", "brackets", "cpbracket": go3270.SetCodepage(go3270.CodepageBracket()) case "500", "cp500": go3270.SetCodepage(go3270.Codepage500()) case "273", "cp273": go3270.SetCodepage(go3270.Codepage273()) case "1140", "cp1140": go3270.SetCodepage(go3270.Codepage1140()) default: go3270.SetCodepage(go3270.Codepage037()) } } func loadHosts(configPath string) (*Config, error) { data, err := os.ReadFile(configPath) if err != nil { return nil, fmt.Errorf("could not read %s: %w", configPath, err) } cfg := Config{ Server: ServerConfig{ BindAddress: listen, Codepage: "037", }, Login: LoginConfig{ Title: "SESSMAN3270 SESSION BROKER", Subtitle: "Enter your Username and Password to begin or resume a session.", PromptUsername: "Username ===>", PromptPassword: "Password ===>", Instructions: "[Enter] Submit [Clear] Reset ", }, Menu: MenuConfig{ Title: "SESSMAN3270 CENTRAL SESSION MANAGER", PromptSelect: "Selection ===>", Footer: "[PA3] Return to Menu [PF3] Disconnect", }, } if _, err := toml.Decode(string(data), &cfg); err != nil { return nil, fmt.Errorf("failed to parse TOML from %s: %w", configPath, err) } if len(cfg.Systems) == 0 { return nil, fmt.Errorf("no systems defined in %s", configPath) } return &cfg, nil } func cleanupSessionIfEmpty(s *UserMasterSession) { if s == nil || s.SessionKey == "" { return } registryMu.Lock() defer registryMu.Unlock() s.Mu.Lock() shouldDelete := s.IsDisconnected && s.countActiveSlotsLocked() == 0 key := s.SessionKey s.Mu.Unlock() if shouldDelete && userSessions[key] == s { delete(userSessions, key) } } func handleNewConnection(conn net.Conn) { if tlsConn, ok := conn.(*tls.Conn); ok { _ = tlsConn.SetDeadline(time.Now().Add(5 * time.Second)) if err := tlsConn.Handshake(); err != nil { log.Printf("[%s] TLS handshake failed: %v", conn.RemoteAddr(), err) _ = conn.Close() return } _ = tlsConn.SetDeadline(time.Time{}) } devInfo, err := go3270.NegotiateTelnet(conn) if err != nil { log.Printf("[%s] Telnet negotiation failed: %v", conn.RemoteAddr(), err) _ = conn.Close() return } termType := ResolveTerminalType(devInfo.TerminalType(), devInfo) log.Printf("[%s] Telnet negotiation completed (Terminal: %s)", conn.RemoteAddr(), termType) creds, err := promptLogin(conn, devInfo) if err != nil { log.Printf("[%s] Client disconnected during login: %v", conn.RemoteAddr(), err) _ = conn.Close() return } sessionKey := creds.SessionKey shortTag := sessionKey[:8] authRes := authenticateAndAuthorize(creds.Username, creds.Password) if !authRes.OK { log.Printf("[%s] Authentication failed for user '%s': %s", conn.RemoteAddr(), creds.Username, authRes.Reason) send3270Message(conn, fmt.Sprintf("Authentication failed: %s.", authRes.Reason), devInfo.Codepage()) time.Sleep(adminNoticeSleep) _ = conn.Close() return } isAdmin := authRes.IsAdmin registryMu.Lock() userSession, exists := userSessions[sessionKey] if !exists { userSession = &UserMasterSession{ Username: creds.Username, SessionKey: sessionKey, DisplayTag: shortTag, Slots: make([]*SessionSlot, 0), } userSessions[sessionKey] = userSession } registryMu.Unlock() userSession.Mu.Lock() oldConn := userSession.ClientConn userSession.ClientConn = conn userSession.Username = creds.Username userSession.TerminalType = termType userSession.DevInfo = devInfo userSession.IsDisconnected = false userSession.ActiveSlotIdx = -1 userSession.IsAdmin = isAdmin activeSlots := userSession.countActiveSlotsLocked() userSession.Mu.Unlock() if !exists { log.Printf("[%s] '%s' authenticated -> allocated new session %s (Admin: %v)", conn.RemoteAddr(), creds.Username, shortTag, isAdmin) } else { log.Printf("[%s] '%s' authenticated -> resumed existing session %s (%d active slots, Admin: %v)", conn.RemoteAddr(), creds.Username, shortTag, activeSlots, isAdmin) } if oldConn != nil { log.Printf("[%s] '%s' Closing previous client connection socket", shortTag, creds.Username) _ = oldConn.Close() } if activeSlots > 0 { handleResumeSession(userSession, conn) return } runMenuLoop(userSession, conn) } func dialUpstream(target TargetSystem) (net.Conn, error) { dialer := &net.Dialer{Timeout: 10 * time.Second} if target.TLS { host, _, err := net.SplitHostPort(target.HostPort) if err != nil { host = target.HostPort } tlsConfig := &tls.Config{ InsecureSkipVerify: target.InsecureSkipVerify, ServerName: host, } return tls.DialWithDialer(dialer, "tcp", target.HostPort, tlsConfig) } return dialer.Dial("tcp", target.HostPort) } func sendQueryReply(s *UserMasterSession, upConn net.Conn, isTN3270E, isBound bool) { s.Mu.Lock() termType := s.TerminalType if termType == "" { termType = ResolveTerminalType(termType, s.DevInfo) } rows, cols, _ := GetTerminalMatrix(termType, s.DevInfo) s.Mu.Unlock() isColor := strings.Contains(termType, "3279") reply := buildQueryReply(rows, cols, isColor, isTN3270E, isBound) _, _ = upConn.Write(reply) } func ensureSlotConnected(s *UserMasterSession, slotIdx int, background bool, clientConn net.Conn) *SessionSlot { s.Mu.Lock() if s.ClientConn != clientConn { s.Mu.Unlock() return nil } username, displayTag := s.Username, s.DisplayTag if slotIdx < len(s.Slots) && s.Slots[slotIdx] != nil { slot := s.Slots[slotIdx] if background { log.Printf("[%s] '%s' Slot [%d] %s already active in background", displayTag, username, slotIdx+1, slot.System.Name) } else { log.Printf("[%s] '%s' Resuming existing slot [%d] %s", displayTag, username, slotIdx+1, slot.System.Name) } s.Mu.Unlock() return slot } if slotIdx < 0 || slotIdx >= len(ConfiguredSystem) { s.Mu.Unlock() return nil } termType := ResolveTerminalType(s.TerminalType, s.DevInfo) _, _, maxBuf := GetTerminalMatrix(termType, s.DevInfo) cp := s.codepageLocked() s.Mu.Unlock() target := ConfiguredSystem[slotIdx] mode := "TCP" if target.TLS { mode = "TLS" } if background { log.Printf("[%s] '%s' Background dialing %s (%s, %s, Terminal: %s)...", displayTag, username, target.Name, target.HostPort, mode, termType) } else { log.Printf("[%s] '%s' Dialing %s (%s, %s, Terminal: %s)...", displayTag, username, target.Name, target.HostPort, mode, termType) } upConn, err := dialUpstream(target) if err != nil { log.Printf("[%s] '%s' Failed to connect to %s (%s): %v", displayTag, username, target.Name, target.HostPort, err) send3270Message(clientConn, fmt.Sprintf("Error dialing %s over %s: %v", target.Name, mode, err), cp) time.Sleep(adminNoticeSleep) return nil } initData, isTN3270E, isBound, err := negotiateUpstreamTelnet(upConn, termType) if err != nil { _ = upConn.Close() log.Printf("[%s] '%s' Upstream telnet negotiation failed for %s: %v", displayTag, username, target.Name, err) send3270Message(clientConn, fmt.Sprintf("Error negotiating with %s: %v", target.Name, err), cp) time.Sleep(adminNoticeSleep) return nil } if background { log.Printf("[%s] '%s' Backgrounded connection established to %s (%s, TN3270E: %v, Terminal: %s)", displayTag, username, target.Name, target.HostPort, isTN3270E, termType) if is3270Query(initData) { sendQueryReply(s, upConn, isTN3270E, isBound) } } else { log.Printf("[%s] '%s' Upstream connection to %s (%s)", displayTag, username, target.Name, target.HostPort) } s.Mu.Lock() defer s.Mu.Unlock() if s.ClientConn != clientConn { _ = upConn.Close() return nil } ensureSlotCapacity(s, slotIdx) if s.Slots[slotIdx] != nil { _ = upConn.Close() return s.Slots[slotIdx] } slot := &SessionSlot{ System: target, Upstream: upConn, LastScreen: initData, IsTN3270E: isTN3270E, IsBound: isBound, MaxBufferSize: maxBuf, } s.Slots[slotIdx] = slot go upstreamReader(s, slot, slotIdx) return slot } func connectOrResumeSlot(s *UserMasterSession, slotIdx int, clientConn net.Conn) { slot := ensureSlotConnected(s, slotIdx, false, clientConn) if slot == nil { return } s.Mu.Lock() if s.ClientConn != clientConn { s.Mu.Unlock() return } s.ActiveSlotIdx = slotIdx s.IsDisconnected = false s.Mu.Unlock() defer func() { s.Mu.Lock() if s.ClientConn == clientConn { s.ActiveSlotIdx = -1 } s.Mu.Unlock() }() slot.Mu.Lock() slot.ScreenChanged = false slot.IsHeld = false if len(slot.LastScreen) > 0 { _, _ = clientConn.Write(slot.LastScreen) } slot.Mu.Unlock() bridgeClientToUpstream(s, slot, clientConn) } func cleanCloseUpstream(up net.Conn, isTN3270E bool) { if up == nil { return } defer up.Close() _ = up.SetWriteDeadline(time.Now().Add(250 * time.Millisecond)) // 1. If TN3270E, send RFC 2355 UNBIND data message (reason 0x01: normal end of session) if isTN3270E { unbindMsg := []byte{TN3270E_DATA_UNBIND, 0x00, 0x00, 0x00, 0x00, TN3270E_UNBIND_NORMAL, IAC, EOR} _, _ = up.Write(unbindMsg) } // 2. Send RFC 854 Telnet option unwinding unwind := make([]byte, 0, 18) unwind = append(unwind, IAC, WONT, OPT_BINARY, IAC, DONT, OPT_BINARY, IAC, WONT, OPT_EOR, IAC, DONT, OPT_EOR, ) if isTN3270E { unwind = append(unwind, IAC, WONT, OPT_TN3270E, IAC, DONT, OPT_TN3270E) } _, _ = up.Write(unwind) } func killSlot(s *UserMasterSession, slotIdx int) { s.Mu.Lock() if slotIdx < 0 || slotIdx >= len(s.Slots) || s.Slots[slotIdx] == nil { s.Mu.Unlock() return } slot := s.Slots[slotIdx] s.Slots[slotIdx] = nil if s.ActiveSlotIdx == slotIdx { s.ActiveSlotIdx = -1 } sysName := slot.System.Name username, displayTag := s.Username, s.DisplayTag s.Mu.Unlock() slot.Mu.Lock() up := slot.Upstream isTN3270E := slot.IsTN3270E slot.Upstream = nil slot.Mu.Unlock() cleanCloseUpstream(up, isTN3270E) cleanupSessionIfEmpty(s) log.Printf("[%s] '%s' Killed system connection [%d] %s (TN3270E: %v)", displayTag, username, slotIdx+1, sysName, isTN3270E) } func bridgeClientToUpstream(s *UserMasterSession, slot *SessionSlot, clientConn net.Conn) { if clientConn == nil { return } buf := make([]byte, 4096) for { n, err := clientConn.Read(buf) if err != nil { s.Mu.Lock() username, displayTag := s.Username, s.DisplayTag if s.ClientConn == clientConn { log.Printf("[%s] '%s' disconnected during active session [%s].", displayTag, username, slot.System.Name) s.IsDisconnected = true s.ActiveSlotIdx = -1 s.Mu.Unlock() cleanupSessionIfEmpty(s) } else { log.Printf("[%s] '%s' Transferring previous session [%s] to new terminal.", displayTag, username, slot.System.Name) s.Mu.Unlock() } return } packet := buf[:n] aid := extractAID(packet) if aid == go3270.AIDPA3 { username, displayTag := s.Identity() log.Printf("[%s] '%s' sent PA3. Backgrounding [%s] and returning to menu.", displayTag, username, slot.System.Name) return } slot.Mu.Lock() upConn := slot.Upstream isTN3270E := slot.IsTN3270E isBound := slot.IsBound seq := slot.SeqNumber if isTN3270E && isBound { slot.SeqNumber = (seq + 1) & TN3270E_MAX_SEQ } slot.Mu.Unlock() if upConn == nil { username, displayTag := s.Identity() log.Printf("[%s] '%s' Upstream host [%s] has disconnected.", displayTag, username, slot.System.Name) send3270Message(clientConn, fmt.Sprintf("Upstream host %s sent disconnect.", slot.System.Name), s.Codepage()) time.Sleep(adminNoticeSleep) return } toSend := packet if isTN3270E { if isBound { seqHi := byte(seq >> 8) seqLo := byte(seq & 0xFF) if seqLo == IAC { toSend = make([]byte, 0, 6+len(packet)) toSend = append(toSend, TN3270E_DATA_3270, 0x00, 0x00, seqHi, IAC, IAC) } else { toSend = make([]byte, 0, 5+len(packet)) toSend = append(toSend, TN3270E_DATA_3270, 0x00, 0x00, seqHi, seqLo) } } else { toSend = make([]byte, 0, 5+len(packet)) toSend = append(toSend, TN3270E_DATA_SSCP_LU, 0x00, 0x00, 0x00, 0x00) } toSend = append(toSend, packet...) } _, err = upConn.Write(toSend) if err != nil { username, displayTag := s.Identity() log.Printf("[%s] '%s' Upstream write error on [%s]: %v", displayTag, username, slot.System.Name, err) return } } } func upstreamReader(s *UserMasterSession, slot *SessionSlot, slotIdx int) { buf := make([]byte, 8192) var recBuf []byte for { slot.Mu.Lock() upConn := slot.Upstream slot.Mu.Unlock() if upConn == nil { return } n, err := upConn.Read(buf) if err != nil { username, displayTag := s.Identity() log.Printf("[%s] '%s' Upstream host %s severed connection. Killing [%d].", displayTag, username, slot.System.Name, slotIdx+1) slot.Mu.Lock() if slot.Upstream != nil { _ = slot.Upstream.Close() slot.Upstream = nil } slot.Mu.Unlock() s.Mu.Lock() if slotIdx < len(s.Slots) && s.Slots[slotIdx] == slot { s.Slots[slotIdx] = nil if s.ActiveSlotIdx == slotIdx { s.ActiveSlotIdx = -1 } } s.Mu.Unlock() cleanupSessionIfEmpty(s) return } data := buf[:n] // Handle upstream IAC DO TIMING-MARK (FF FD 06) keep-alive locally if bytes.Contains(data, telnetDoTimingMark) { _, _ = upConn.Write(telnetWillTimingMark) data = bytes.ReplaceAll(data, telnetDoTimingMark, nil) if len(data) == 0 { continue } } s.Mu.Lock() isActive := !s.IsDisconnected && (s.ActiveSlotIdx >= 0 && s.ActiveSlotIdx < len(s.Slots) && s.Slots[s.ActiveSlotIdx] == slot) client := s.ClientConn username, displayTag := s.Username, s.DisplayTag _, _, defaultMaxBuf := GetTerminalMatrix(s.TerminalType, s.DevInfo) slot.Mu.Lock() isTN3270E := slot.IsTN3270E slot.Mu.Unlock() s.Mu.Unlock() if !isTN3270E && isActive && client != nil { _, _ = client.Write(data) } // Cap buffer at 64KB to avoid unbounded memory use from constant ticks if len(recBuf) > 65536 { recBuf = recBuf[len(recBuf)-16384:] } recBuf = append(recBuf, data...) for { idx := indexTelnetEOR(recBuf) if idx == -1 { break } record := recBuf[:idx+2] recBuf = recBuf[idx+2:] screenData := record if isTN3270E && len(record) >= 7 { // 5-byte header + data + IAC EOR (2 bytes) dataType := record[0] respFlag := record[2] seqHi := record[3] seqLo := record[4] if dataType == TN3270E_DATA_UNBIND { reason := byte(0) if len(record) > 7 { reason = record[5] } log.Printf("[%s] '%s' Upstream host %s sent UNBIND (reason: 0x%02x). Session returned to unbound/SSCP-LU state.", displayTag, username, slot.System.Name, reason) slot.Mu.Lock() slot.IsBound = false slot.Mu.Unlock() continue } if respFlag == TN3270E_RESP_ALWAYS { ack := []byte{TN3270E_DATA_RESP, 0x00, 0x00, seqHi, seqLo, 0x00, IAC, EOR} _, _ = upConn.Write(ack) } switch dataType { case TN3270E_DATA_BIND: slot.Mu.Lock() slot.IsBound = true slot.Mu.Unlock() continue case TN3270E_DATA_3270: slot.Mu.Lock() slot.IsBound = true slot.Mu.Unlock() screenData = record[5:] case TN3270E_DATA_SSCP_LU: slot.Mu.Lock() slot.IsBound = false slot.Mu.Unlock() screenData = record[5:] default: continue } } if isTN3270E && isActive && client != nil { _, _ = client.Write(screenData) } // If background session's host sends a 3270 Query command, respond // with the attached terminal's characteristics locally if !isActive && is3270Query(screenData) { slot.Mu.Lock() isBound := slot.IsBound slot.Mu.Unlock() sendQueryReply(s, upConn, isTN3270E, isBound) } if len(screenData) > 2 { firstByte := screenData[0] isEraseWrite := firstByte == CMD_ERASE_WRITE_SNA || firstByte == CMD_ERASE_WRITE || firstByte == CMD_ERASE_WRITE_ALT_SNA || firstByte == CMD_ERASE_WRITE_ALT || firstByte == CMD_ERASE_ALL_UNPROTECTED slot.Mu.Lock() if slot.MaxBufferSize <= 0 { slot.MaxBufferSize = defaultMaxBuf } maxBuf := slot.MaxBufferSize if slot.IsHeld { // Allow screen erase/resets so base state is accurate, but // drop/suppress incremental write ticks and do not set ScreenChanged if isEraseWrite { slot.LastScreen = bytes.Clone(screenData) } slot.Mu.Unlock() continue } if isEraseWrite || len(slot.LastScreen) == 0 { slot.LastScreen = bytes.Clone(screenData) } else if len(slot.LastScreen)+len(screenData) <= maxBuf { slot.LastScreen = append(slot.LastScreen, screenData...) } else { // Exceeds maxBuf: retain initial screen baseline and append newest write update, // trimming older intermediate writes so total buffer remains strictly <= maxBuf baseLen := min(maxBuf/2, len(slot.LastScreen)) eorIdx := lastIndexTelnetEOR(slot.LastScreen[:baseLen]) if eorIdx != -1 { baseLen = eorIdx + 2 } newBuf := make([]byte, 0, baseLen+len(screenData)) newBuf = append(newBuf, slot.LastScreen[:baseLen]...) newBuf = append(newBuf, screenData...) slot.LastScreen = newBuf } if !isActive { slot.ScreenChanged = true } slot.Mu.Unlock() } } } } func findSession(targetID string) *UserMasterSession { cleanID := strings.TrimSpace(targetID) cleanTag := strings.TrimPrefix(cleanID, "#") registryMu.Lock() defer registryMu.Unlock() // 1. Exact match on DisplayTag (hex, case-insensitive) for _, sess := range userSessions { _, displayTag := sess.Identity() if strings.EqualFold(displayTag, cleanTag) { return sess } } // 2. Direct map lookup on SessionKey (hex, case-insensitive) if sess, ok := userSessions[strings.ToLower(cleanID)]; ok { return sess } // 3. Exact match on Username (case-sensitive) for _, sess := range userSessions { username, _ := sess.Identity() if username == cleanID { return sess } } // 4. Prefix match on SessionKey (at least 4 chars) if len(cleanTag) >= 4 { cleanTagLower := strings.ToLower(cleanTag) for _, sess := range userSessions { if strings.HasPrefix(strings.ToLower(sess.SessionKey), cleanTagLower) { return sess } } } return nil } func terminateSession(sess *UserMasterSession, msg string) { sess.Mu.Lock() type upInfo struct { conn net.Conn isTN3270E bool } var upstreams []upInfo for _, slot := range sess.Slots { if slot != nil { slot.Mu.Lock() if slot.Upstream != nil { upstreams = append(upstreams, upInfo{conn: slot.Upstream, isTN3270E: slot.IsTN3270E}) slot.Upstream = nil } slot.Mu.Unlock() } } sess.Slots = nil sess.ActiveSlotIdx = -1 sess.IsDisconnected = true c := sess.ClientConn sess.ClientConn = nil cp := sess.codepageLocked() sess.Mu.Unlock() if c != nil { if msg != "" { send3270Message(c, msg, cp) time.Sleep(adminNoticeSleep / 5) } _ = c.Close() } for _, up := range upstreams { cleanCloseUpstream(up.conn, up.isTN3270E) } } func handleAdminKill(adminSession *UserMasterSession, targetID string) bool { adminSession.Mu.Lock() adminUser := adminSession.Username adminConn := adminSession.ClientConn adminCP := adminSession.codepageLocked() adminSession.Mu.Unlock() target := findSession(targetID) if target == nil { log.Printf("[ADMIN] %s attempted to kill session '%s' (not found)", adminUser, targetID) send3270Message(adminConn, fmt.Sprintf("Session '%s' not found.", targetID), adminCP) time.Sleep(adminNoticeSleep) return false } targetUser, targetTag := target.Identity() log.Printf("[ADMIN] %s killing session '%s' (User: %s)", adminUser, targetTag, targetUser) registryMu.Lock() delete(userSessions, target.SessionKey) registryMu.Unlock() if target == adminSession { terminateSession(target, "Session terminated.") return true } terminateSession(target, "Session terminated by administrator.") send3270Message(adminConn, fmt.Sprintf("Session '%s' (%s) terminated.", targetTag, targetUser), adminCP) time.Sleep(adminNoticeSleep) return false } func drainAllSessions() []*UserMasterSession { registryMu.Lock() defer registryMu.Unlock() allSessions := make([]*UserMasterSession, 0, len(userSessions)) for _, sess := range userSessions { allSessions = append(allSessions, sess) } userSessions = make(map[string]*UserMasterSession) return allSessions } func handleAdminKillAll(adminSession *UserMasterSession) { adminUser, _ := adminSession.Identity() log.Printf("[ADMIN] %s executed 'killall'", adminUser) allSessions := drainAllSessions() for _, sess := range allSessions { terminateSession(sess, "Session terminated by administrator.") } } func handleAdminBye(adminSession *UserMasterSession) { adminUser, _ := adminSession.Identity() log.Printf("[ADMIN] %s executed 'bye': shutting down server", adminUser) allSessions := drainAllSessions() for _, sess := range allSessions { terminateSession(sess, "Server shutting down.") } if serverListener != nil { _ = serverListener.Close() } if shutdownFunc != nil { shutdownFunc() } }