Files
techircd/client.go

1224 lines
30 KiB
Go

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, &registrationActive, 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())
}
}
}