package main import ( "bufio" "crypto/tls" "fmt" "log" "math/rand" "net" "strings" "sync" "time" ) // Handle CAP negotiation and capability enabling func (c *Client) handleCap(parts []string) { if len(parts) < 2 { return } subcmd := strings.ToUpper(parts[1]) serverName := "techircd.local" if c.server != nil && c.server.config != nil { serverName = c.server.config.Server.Name } switch subcmd { case "LS": // Start CAP negotiation c.capNegotiation = true // Advertise capabilities - using proper IRC format capabilities := "away-notify chghost invite-notify multi-prefix userhost-in-names server-time message-tags account-tag account-notify" c.SendMessage(fmt.Sprintf(":%s CAP * LS :%s", serverName, capabilities)) case "REQ": if len(parts) < 3 { return } // Join all parts from [2] onwards to handle multi-word capability lists requestedCaps := strings.Join(parts[2:], " ") if strings.HasPrefix(requestedCaps, ":") { requestedCaps = requestedCaps[1:] } caps := strings.Fields(requestedCaps) ack := []string{} for _, cap := range caps { // Accept common capabilities switch cap { case "away-notify", "chghost", "invite-notify", "multi-prefix", "userhost-in-names", "server-time", "message-tags", "account-tag", "account-notify": c.SetCapability(cap, true) ack = append(ack, cap) } } if len(ack) > 0 { c.SendMessage(fmt.Sprintf(":%s CAP %s ACK :%s", serverName, c.Nick(), strings.Join(ack, " "))) } case "END": c.capNegotiation = false // Try to complete registration now that CAP negotiation is done c.checkRegistration() case "LIST": // List active capabilities for this client c.mu.RLock() var activeCaps []string for cap := range c.capabilities { activeCaps = append(activeCaps, cap) } c.mu.RUnlock() capsList := strings.Join(activeCaps, " ") c.SendMessage(fmt.Sprintf(":%s CAP %s LIST :%s", serverName, c.Nick(), capsList)) } } type Client struct { conn net.Conn nick string user string realname string host string server *Server channels map[string]*Channel modes map[rune]bool away string oper bool operClass string // Operator class name ssl bool registered bool account string // Services account name connectTime time.Time // When the client connected lastActivity time.Time // Last time client sent a message // Unique client ID for server tracking clientID string // Connection stability tracking disconnected bool // Flag to track if client is disconnected writeErrors int // Count of consecutive write errors maxWriteErrors int // Maximum write errors before disconnection lastWriteError time.Time // Time of last write error // Flood protection lastMessage time.Time messageCount int // SASL authentication saslMech string saslData string // IRCv3 capabilities capabilities map[string]bool capNegotiation bool // True if client is in CAP negotiation // Server Notice Masks (snomasks) for operators snomasks map[rune]bool // Ping timeout tracking lastPong time.Time waitingForPong bool // SILENCE and MONITOR lists silenceList []string monitorList []string mu sync.RWMutex } func NewClient(conn net.Conn, server *Server) *Client { host, _, _ := net.SplitHostPort(conn.RemoteAddr().String()) // Check if connection is SSL isSSL := false if _, ok := conn.(*tls.Conn); ok { isSSL = true } // Generate unique client ID clientID := fmt.Sprintf("%s_%d_%d", host, time.Now().Unix(), rand.Intn(10000)) client := &Client{ clientID: clientID, conn: conn, host: host, server: server, channels: make(map[string]*Channel), modes: make(map[rune]bool), capabilities: make(map[string]bool), capNegotiation: false, snomasks: make(map[rune]bool), silenceList: make([]string, 0), monitorList: make([]string, 0), ssl: isSSL, connectTime: time.Now(), lastActivity: time.Now(), lastMessage: time.Now(), lastPong: time.Now(), waitingForPong: false, // Connection stability disconnected: false, writeErrors: 0, maxWriteErrors: 3, // Allow 3 consecutive write errors before marking disconnected } // Set SSL user mode if connected via SSL if isSSL { client.SetMode('z', true) } return client } func (c *Client) SendMessage(message string) { c.mu.Lock() defer c.mu.Unlock() // Enhanced connection health check if c.conn == nil { log.Printf("SendMessage: connection is nil for client %s", c.Nick()) return } // Validate message before sending if message == "" { return } // Prevent sending to disconnected clients if c.isDisconnected() { return } // Debug logging for outgoing messages if DebugMode { log.Printf(">>> SEND to %s: %s", c.getClientInfoUnsafe(), message) } // Set write deadline to prevent hanging with retry mechanism writeTimeout := 15 * time.Second c.conn.SetWriteDeadline(time.Now().Add(writeTimeout)) defer func() { // Always clear deadline, ignore errors on disconnected connections if c.conn != nil { c.conn.SetWriteDeadline(time.Time{}) } }() // Attempt to write with error recovery _, err := fmt.Fprintf(c.conn, "%s\r\n", message) if err != nil { // Enhanced error logging with client identification clientInfo := c.getClientInfoUnsafe() log.Printf("Error sending message to %s: %v (msg: %.50s...)", clientInfo, err, message) // Mark client for disconnection on write errors c.markDisconnected() // Don't attempt to send error messages that could cause recursion return } // Reset write error counter on successful write c.resetWriteErrors() // Only log PONG messages in debug mode to reduce log spam if strings.HasPrefix(message, "PONG") && c.server != nil && c.server.config != nil { if c.server.config.Logging.Level == "debug" { log.Printf("Successfully sent to client %s: %s", c.getClientInfoUnsafe(), message) } } } func (c *Client) SendFrom(source, message string) { c.SendMessage(fmt.Sprintf(":%s %s", source, message)) } func (c *Client) SendNumeric(code int, message string) { if c.server == nil || c.server.config == nil { return } c.SendFrom(c.server.config.Server.Name, fmt.Sprintf("%03d %s %s", code, c.Nick(), message)) } func (c *Client) Nick() string { c.mu.RLock() defer c.mu.RUnlock() return c.nick } func (c *Client) SetNick(nick string) { c.mu.Lock() defer c.mu.Unlock() c.nick = nick } func (c *Client) User() string { c.mu.RLock() defer c.mu.RUnlock() return c.user } func (c *Client) SetUser(user string) { c.mu.Lock() defer c.mu.Unlock() c.user = user } func (c *Client) Realname() string { c.mu.RLock() defer c.mu.RUnlock() return c.realname } func (c *Client) SetRealname(realname string) { c.mu.Lock() defer c.mu.Unlock() c.realname = realname } func (c *Client) Host() string { c.mu.RLock() defer c.mu.RUnlock() return c.host } // HostForUser returns the appropriate hostname to show to a requesting user // based on privacy settings and the requester's privileges func (c *Client) HostForUser(requester *Client) string { c.mu.RLock() defer c.mu.RUnlock() // If host hiding is disabled, always show real host if !c.server.config.Privacy.HideHostsFromUsers { return c.host } // If requester is an operator and bypass is enabled, show real host if requester != nil && requester.IsOper() && c.server.config.Privacy.OperBypassHostHide { return c.host } // If requester is viewing themselves, show real host if requester != nil && requester.Nick() == c.Nick() { return c.host } // Check if user has +x mode set (host masking) if c.HasMode('x') { // Return masked hostname return c.nick + "." + c.server.config.Privacy.MaskedHostSuffix } // Default behavior: show masked host when privacy is enabled return c.nick + "." + c.server.config.Privacy.MaskedHostSuffix } // canSeeWhoisInfo checks if the requester can see specific WHOIS information about the target func (c *Client) canSeeWhoisInfo(target *Client, infoType string) bool { config := c.server.config.WhoisFeatures var toEveryone, toOpers, toSelf bool switch infoType { case "user_modes": toEveryone = config.ShowUserModes.ToEveryone toOpers = config.ShowUserModes.ToOpers toSelf = config.ShowUserModes.ToSelf case "ssl_status": toEveryone = config.ShowSSLStatus.ToEveryone toOpers = config.ShowSSLStatus.ToOpers toSelf = config.ShowSSLStatus.ToSelf case "idle_time": toEveryone = config.ShowIdleTime.ToEveryone toOpers = config.ShowIdleTime.ToOpers toSelf = config.ShowIdleTime.ToSelf case "signon_time": toEveryone = config.ShowSignonTime.ToEveryone toOpers = config.ShowSignonTime.ToOpers toSelf = config.ShowSignonTime.ToSelf case "real_host": toEveryone = config.ShowRealHost.ToEveryone toOpers = config.ShowRealHost.ToOpers toSelf = config.ShowRealHost.ToSelf case "oper_class": toEveryone = config.ShowOperClass.ToEveryone toOpers = config.ShowOperClass.ToOpers toSelf = config.ShowOperClass.ToSelf case "client_info": toEveryone = config.ShowClientInfo.ToEveryone toOpers = config.ShowClientInfo.ToOpers toSelf = config.ShowClientInfo.ToSelf case "account_name": toEveryone = config.ShowAccountName.ToEveryone toOpers = config.ShowAccountName.ToOpers toSelf = config.ShowAccountName.ToSelf default: return false } // Check if target is viewing themselves if c.Nick() == target.Nick() && toSelf { return true } // Check if requester is an operator if c.IsOper() && toOpers { return true } // Check if everyone can see this info if toEveryone { return true } return false } // canSeeChannels checks if the requester can see channel information func (c *Client) canSeeChannels(target *Client) bool { config := c.server.config.WhoisFeatures.ShowChannels // Check if target is viewing themselves if c.Nick() == target.Nick() && config.ToSelf { return true } // Check if requester is an operator if c.IsOper() && config.ToOpers { return true } // Check if everyone can see this info if config.ToEveryone { return true } return false } // UpdateActivity updates the last activity time for the client func (c *Client) UpdateActivity() { c.mu.Lock() defer c.mu.Unlock() c.lastActivity = time.Now() } // SetAccount sets the account name for services integration func (c *Client) SetAccount(account string) { c.mu.Lock() defer c.mu.Unlock() oldAccount := c.account c.account = account // IRCv3 account-notify: send ACCOUNT message if capability enabled and account changes if c.HasCapability("account-notify") && oldAccount != account { msg := "ACCOUNT " if account == "" { msg += "*" } else { msg += account } c.SendMessage(msg) } } // Account returns the account name func (c *Client) Account() string { c.mu.RLock() defer c.mu.RUnlock() return c.account } // ConnectTime returns when the client connected func (c *Client) ConnectTime() time.Time { c.mu.RLock() defer c.mu.RUnlock() return c.connectTime } // LastActivity returns the last activity time func (c *Client) LastActivity() time.Time { c.mu.RLock() defer c.mu.RUnlock() return c.lastActivity } // SetOperClass sets the operator class for this client func (c *Client) SetOperClass(class string) { c.mu.Lock() defer c.mu.Unlock() c.operClass = class } // OperClass returns the operator class func (c *Client) OperClass() string { c.mu.RLock() defer c.mu.RUnlock() return c.operClass } // HasOperPermission checks if the client has a specific operator permission func (c *Client) HasOperPermission(permission string) bool { if !c.IsOper() { return false } // Load oper config and check permissions operConfig, err := LoadOperConfig(c.server.config.OperConfig.ConfigFile) if err != nil || !c.server.config.OperConfig.Enable { // Fallback to legacy system return true // Basic oper permissions } return operConfig.HasPermission(c.Nick(), permission) } // GetOperRank returns the operator rank (higher number = higher authority) func (c *Client) GetOperRank() int { if !c.IsOper() { return 0 } operConfig, err := LoadOperConfig(c.server.config.OperConfig.ConfigFile) if err != nil || !c.server.config.OperConfig.Enable { return 1 // Basic rank for legacy } return operConfig.GetOperRank(c.Nick()) } // CanOperateOn checks if this operator can perform actions on another operator func (c *Client) CanOperateOn(target *Client) bool { if !c.IsOper() { return false } if !target.IsOper() { return true // Opers can operate on regular users } operConfig, err := LoadOperConfig(c.server.config.OperConfig.ConfigFile) if err != nil || !c.server.config.OperConfig.Enable { return true // Legacy behavior } return operConfig.CanOperateOn(c.Nick(), target.Nick()) } // GetOperSymbol returns the symbol for this operator class func (c *Client) GetOperSymbol() string { if !c.IsOper() { return "" } operConfig, err := LoadOperConfig(c.server.config.OperConfig.ConfigFile) if err != nil || !c.server.config.OperConfig.Enable { return "*" // Default symbol } class := operConfig.GetOperClass(c.OperClass()) if class == nil { return "*" } return class.Symbol } func (c *Client) IsRegistered() bool { c.mu.RLock() defer c.mu.RUnlock() return c.registered } func (c *Client) SetRegistered(registered bool) { c.mu.Lock() defer c.mu.Unlock() c.registered = registered } func (c *Client) IsOper() bool { c.mu.RLock() defer c.mu.RUnlock() return c.oper } func (c *Client) SetOper(oper bool) { c.mu.Lock() defer c.mu.Unlock() c.oper = oper } func (c *Client) IsSSL() bool { c.mu.RLock() defer c.mu.RUnlock() return c.ssl } func (c *Client) Away() string { c.mu.RLock() defer c.mu.RUnlock() return c.away } func (c *Client) SetAway(away string) { c.mu.Lock() defer c.mu.Unlock() c.away = away } func (c *Client) HasMode(mode rune) bool { c.mu.RLock() defer c.mu.RUnlock() return c.modes[mode] } func (c *Client) SetMode(mode rune, set bool) { c.mu.Lock() defer c.mu.Unlock() if set { c.modes[mode] = true } else { delete(c.modes, mode) } } func (c *Client) GetModes() string { c.mu.RLock() defer c.mu.RUnlock() var modes []rune for mode := range c.modes { modes = append(modes, mode) } if len(modes) == 0 { return "" } return "+" + string(modes) } func (c *Client) HasSnomask(snomask rune) bool { c.mu.RLock() defer c.mu.RUnlock() return c.snomasks[snomask] } func (c *Client) SetSnomask(snomask rune, set bool) { c.mu.Lock() defer c.mu.Unlock() if set { c.snomasks[snomask] = true } else { delete(c.snomasks, snomask) } } func (c *Client) GetSnomasks() string { c.mu.RLock() defer c.mu.RUnlock() var snomasks []rune for snomask := range c.snomasks { snomasks = append(snomasks, snomask) } if len(snomasks) == 0 { return "" } return "+" + string(snomasks) } // HasGodMode returns true if the client has god mode enabled (user mode +G) func (c *Client) HasGodMode() bool { return c.HasMode('G') && c.HasOperPermission("god_mode") } // HasStealthMode returns true if the client has stealth mode enabled (user mode +S) func (c *Client) HasStealthMode() bool { return c.HasMode('S') && c.HasOperPermission("stealth_mode") } // IsVisibleTo returns true if this client should be visible to the target client func (c *Client) IsVisibleTo(target *Client) bool { // If client doesn't have stealth mode, always visible if !c.HasStealthMode() { return true } // If target is an operator, stealth users are visible if target.IsOper() { return true } // If target is the stealth user themselves, always visible if target == c { return true } // Otherwise, stealth users are invisible to normal users return false } // CanBypassChannelRestrictions returns true if the client can bypass channel restrictions func (c *Client) CanBypassChannelRestrictions() bool { return c.HasGodMode() } func (c *Client) AddChannel(channel *Channel) { c.mu.Lock() defer c.mu.Unlock() c.channels[strings.ToLower(channel.name)] = channel } func (c *Client) RemoveChannel(channelName string) { c.mu.Lock() defer c.mu.Unlock() delete(c.channels, strings.ToLower(channelName)) } func (c *Client) IsInChannel(channelName string) bool { c.mu.RLock() defer c.mu.RUnlock() _, exists := c.channels[strings.ToLower(channelName)] return exists } func (c *Client) GetChannels() map[string]*Channel { c.mu.RLock() defer c.mu.RUnlock() channels := make(map[string]*Channel) for name, channel := range c.channels { channels[name] = channel } return channels } func (c *Client) Prefix() string { return fmt.Sprintf("%s!%s@%s", c.Nick(), c.User(), c.Host()) } // sendSnomask sends a server notice to all operators with the specified snomask func (c *Client) sendSnomask(snomask rune, message string) { if c.server == nil { return } serverName := "localhost" if c.server.config != nil { serverName = c.server.config.Server.Name } // Send to all operators with this snomask c.server.mu.RLock() clients := make([]*Client, 0, len(c.server.clients)) for _, client := range c.server.clients { clients = append(clients, client) } c.server.mu.RUnlock() for _, client := range clients { if client.IsOper() && client.HasSnomask(snomask) { client.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** %s", serverName, client.Nick(), message)) } } } // getServerConfig safely returns the server config, or nil if not available func (c *Client) getServerConfig() *Config { if c.server == nil { return nil } return c.server.config } // getRegistrationTimeout safely gets the registration timeout duration func (c *Client) getRegistrationTimeout() time.Duration { config := c.getServerConfig() if config == nil { return 60 * time.Second // default 60 seconds } return config.RegistrationTimeoutDuration() } func (c *Client) CheckFlood() bool { c.mu.Lock() defer c.mu.Unlock() // Enhanced nil checks and stability if c.server == nil || c.server.config == nil { return false } // IRC operators are exempt from flood protection if c.oper { return false } // Be very lenient with flood protection for unregistered clients // during the initial connection phase (first 60 seconds) if !c.registered { // Allow up to 100 commands per minute for unregistered clients now := time.Now() if now.Sub(c.lastMessage) > 60*time.Second { c.messageCount = 0 } c.messageCount++ c.lastMessage = now return c.messageCount > 100 } // For registered clients, use enhanced flood protection now := time.Now() floodWindow := time.Duration(c.server.config.Limits.FloodSeconds) * time.Second // Ensure minimum flood window to prevent issues if floodWindow < 10*time.Second { floodWindow = 10 * time.Second } if now.Sub(c.lastMessage) > floodWindow { c.messageCount = 0 } c.messageCount++ c.lastMessage = now // Use higher limits than configured for better user experience // and prevent false positives configuredLimit := c.server.config.Limits.FloodLines if configuredLimit < 5 { configuredLimit = 5 // Minimum reasonable limit } maxLines := configuredLimit * 3 // Triple the configured limit if maxLines > 100 { maxLines = 100 // Cap at reasonable maximum } exceeded := c.messageCount > maxLines // Log flood attempts for debugging if exceeded { log.Printf("Flood limit exceeded for %s: %d messages in %v (limit: %d)", c.getClientInfoUnsafe(), c.messageCount, floodWindow, maxLines) } return exceeded } func (c *Client) HasCapability(cap string) bool { c.mu.RLock() defer c.mu.RUnlock() return c.capabilities[cap] } func (c *Client) SetCapability(cap string, enabled bool) { c.mu.Lock() defer c.mu.Unlock() if enabled { c.capabilities[cap] = true } else { delete(c.capabilities, cap) } } // Connection stability helper methods // isDisconnected checks if the client is marked as disconnected func (c *Client) isDisconnected() bool { // Don't need to lock here since we're already locked in SendMessage return c.disconnected } // markDisconnected marks the client as disconnected func (c *Client) markDisconnected() { // Don't need to lock here since we're already locked in SendMessage if !c.disconnected { c.disconnected = true c.writeErrors++ c.lastWriteError = time.Now() log.Printf("Client %s marked as disconnected after %d write errors", c.getClientInfoUnsafe(), c.writeErrors) } } // getClientInfo returns identifying information about the client (thread-safe) func (c *Client) getClientInfo() string { c.mu.RLock() defer c.mu.RUnlock() return c.getClientInfoUnsafe() } // getClientInfoUnsafe returns identifying information about the client (not thread-safe) func (c *Client) getClientInfoUnsafe() string { nick := c.nick if nick == "" { nick = "unknown" } host := c.host if host == "" { host = "unknown" } return fmt.Sprintf("%s@%s[%s]", nick, host, c.clientID) } // IsConnected checks if the client connection is still valid func (c *Client) IsConnected() bool { c.mu.RLock() defer c.mu.RUnlock() return c.conn != nil && !c.disconnected } // resetWriteErrors resets the write error counter (called on successful writes) func (c *Client) resetWriteErrors() { // Don't need to lock here since we're already locked in SendMessage if c.writeErrors > 0 { c.writeErrors = 0 c.lastWriteError = time.Time{} } } // HealthCheck performs a comprehensive health check on the client func (c *Client) HealthCheck() (bool, string) { c.mu.RLock() defer c.mu.RUnlock() // Check if disconnected if c.disconnected { return false, "client marked as disconnected" } // Check connection validity if c.conn == nil { return false, "connection is nil" } // Check for excessive write errors if c.writeErrors >= c.maxWriteErrors { return false, fmt.Sprintf("too many write errors (%d/%d)", c.writeErrors, c.maxWriteErrors) } // Check for stale connections (inactive for too long) maxInactiveTime := 30 * time.Minute if !c.registered { maxInactiveTime = 5 * time.Minute // Shorter timeout for unregistered clients } if time.Since(c.lastActivity) > maxInactiveTime { return false, fmt.Sprintf("inactive for %v (max: %v)", time.Since(c.lastActivity), maxInactiveTime) } // Check for registration timeout if !c.registered && time.Since(c.connectTime) > 5*time.Minute { return false, fmt.Sprintf("registration timeout: connected %v ago but not registered", time.Since(c.connectTime)) } return true, "healthy" } // ForceDisconnect forcefully disconnects the client with a reason func (c *Client) ForceDisconnect(reason string) { log.Printf("Force disconnecting client %s: %s", c.getClientInfo(), reason) c.mu.Lock() c.disconnected = true c.mu.Unlock() // Send error message if possible if c.conn != nil { c.SendMessage(fmt.Sprintf("ERROR :%s", reason)) } } func (c *Client) Handle() { defer func() { // Enhanced panic recovery with detailed logging if r := recover(); r != nil { log.Printf("PANIC in client handler for %s: %v", c.getClientInfo(), r) // Log stack trace for debugging log.Printf("Stack trace: %v", r) } // Enhanced cleanup with error handling c.cleanup() }() // Validate initial state if c.server == nil || c.server.config == nil { log.Printf("Client handler: server or config is nil for %s", c.getClientInfo()) return } log.Printf("Starting client handler for %s", c.getClientInfo()) scanner := bufio.NewScanner(c.conn) // Set maximum line length to prevent memory exhaustion const maxLineLength = 4096 scanner.Buffer(make([]byte, maxLineLength), maxLineLength) // Set initial read deadline - be more generous during connection setup c.setReadDeadline(10 * time.Minute) // Set registration timeout registrationTimer := time.NewTimer(c.getRegistrationTimeout()) defer registrationTimer.Stop() registrationActive := true // Initialize ping state c.mu.Lock() c.lastPong = time.Now() c.lastActivity = time.Now() // Set initial activity time c.waitingForPong = false c.mu.Unlock() connectionStartTime := time.Now() // Create a context for graceful shutdown done := make(chan bool, 1) defer close(done) // Main message processing loop with timeout for { // Check if client is marked as disconnected if !c.IsConnected() { log.Printf("Client %s marked as disconnected, ending handler", c.getClientInfo()) return } select { case <-registrationTimer.C: if registrationActive && !c.IsRegistered() { log.Printf("Registration timeout for client %s after %v", c.getClientInfo(), time.Since(connectionStartTime)) c.SendMessage("ERROR :Registration timeout") return } case <-done: return default: // Handle message reading with timeout and enhanced error recovery if !c.handleMessageRead(scanner, ®istrationActive, registrationTimer, connectionStartTime) { return // Connection closed or error } } } } // Enhanced connection handling methods // cleanup performs thorough cleanup of client resources func (c *Client) cleanup() { log.Printf("Starting cleanup for client %s", c.getClientInfo()) // Mark as disconnected to prevent further operations c.mu.Lock() c.disconnected = true c.mu.Unlock() // Close connection safely with timeout to prevent hanging if c.conn != nil { // Set a short deadline to force close if needed c.conn.SetDeadline(time.Now().Add(5 * time.Second)) if err := c.conn.Close(); err != nil { log.Printf("Error closing connection for %s: %v", c.getClientInfo(), err) } // Clear the connection reference c.mu.Lock() c.conn = nil c.mu.Unlock() } // Part all channels with error handling in a separate goroutine to prevent blocking go func() { defer func() { if r := recover(); r != nil { log.Printf("Panic during channel cleanup for %s: %v", c.getClientInfo(), r) } }() channels := c.GetChannels() for channelName, channel := range channels { if channel != nil { channel.RemoveClient(c) // Clean up empty channels if len(channel.GetClients()) == 0 && c.server != nil { c.server.RemoveChannel(channelName) } } } }() // Remove from server in a separate goroutine to prevent deadlock go func() { defer func() { if r := recover(); r != nil { log.Printf("Panic during server cleanup for %s: %v", c.getClientInfo(), r) } }() if c.server != nil { c.server.RemoveClient(c) } }() log.Printf("Cleanup completed for client %s", c.getClientInfo()) } // setReadDeadline safely sets read deadline on connection func (c *Client) setReadDeadline(duration time.Duration) { if c.conn != nil && !c.isDisconnected() { if err := c.conn.SetReadDeadline(time.Now().Add(duration)); err != nil { log.Printf("Error setting read deadline for %s: %v", c.getClientInfo(), err) } } } // handleMessageRead handles reading and processing a single message func (c *Client) handleMessageRead(scanner *bufio.Scanner, registrationActive *bool, registrationTimer *time.Timer, connectionStartTime time.Time) bool { // Set read deadline with timeout to prevent hanging readTimeout := 30 * time.Second if c.IsRegistered() { readTimeout = 5 * time.Minute } c.setReadDeadline(readTimeout) // Use a channel to make scanning non-blocking with timeout scanChan := make(chan bool, 1) var line string var scanErr error go func() { defer func() { if r := recover(); r != nil { log.Printf("Panic in scanner goroutine for %s: %v", c.getClientInfo(), r) scanChan <- false } }() if scanner.Scan() { line = strings.TrimSpace(scanner.Text()) scanErr = scanner.Err() scanChan <- true } else { scanErr = scanner.Err() scanChan <- false } }() // Wait for scan result with timeout select { case scanResult := <-scanChan: if !scanResult { // Check for scanner error if scanErr != nil { log.Printf("Scanner error for client %s: %v", c.getClientInfo(), scanErr) } else { log.Printf("Client %s disconnected cleanly", c.getClientInfo()) } return false } case <-time.After(readTimeout + 5*time.Second): // Additional buffer log.Printf("Read timeout for client %s after %v", c.getClientInfo(), readTimeout) c.SendMessage("ERROR :Read timeout") return false } if line == "" { return true // Continue processing } // Enhanced flood checking - be more lenient during initial connection if c.IsRegistered() && c.CheckFlood() { log.Printf("Flood protection triggered for %s", c.getClientInfo()) c.SendMessage("ERROR :Excess Flood") return false } // Validate message content to prevent issues if len(line) > 4096 { log.Printf("Oversized message from %s, truncating", c.getClientInfo()) line = line[:4096] } // Check for binary data that might cause issues if !isPrintableASCII(line) { log.Printf("Non-printable data from %s, skipping", c.getClientInfo()) return true } // Update activity time for any valid message c.mu.Lock() c.lastActivity = time.Now() c.mu.Unlock() // Stop registration timer once registered if *registrationActive && c.IsRegistered() { registrationTimer.Stop() *registrationActive = false log.Printf("Client %s registration completed successfully", c.getClientInfo()) } // Handle the message with error recovery func() { defer func() { if r := recover(); r != nil { log.Printf("Panic handling message from %s: %v (message: %.100s)", c.getClientInfo(), r, line) } }() if c.server != nil { c.server.HandleMessage(c, line) } }() return true } // isPrintableASCII checks if a string contains only printable ASCII characters func isPrintableASCII(s string) bool { for _, r := range s { if r < 32 || r > 126 { // Allow common IRC control characters if r != '\r' && r != '\n' && r != '\t' { return false } } } return true } // broadcastToChannel sends an IRCv3 message to all users in a channel func (c *Client) broadcastToChannel(channel *Channel, msg *IRCMessage) { for _, client := range channel.GetClients() { if client != c { // Don't send to sender // Create a copy of the message for each recipient clientMsg := &IRCMessage{ Tags: make(map[string]string), Prefix: msg.Prefix, Command: msg.Command, Params: msg.Params, } // Copy base tags for k, v := range msg.Tags { clientMsg.Tags[k] = v } // Add recipient-specific server-time if they support it if client.HasCapability("server-time") && clientMsg.Tags["time"] == "" { clientMsg.Tags["time"] = time.Now().UTC().Format(time.RFC3339Nano) } client.SendMessage(clientMsg.FormatMessage()) } } }