/* goircd -- minimalistic simple Internet Relay Chat (IRC) server Copyright (C) 2014-2020 Sergey Matveev This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 3 of the License. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package main import ( "fmt" "log" "strings" "time" ) const ( // Max deadline time of client's unresponsiveness PingTimeout = time.Second * 180 // Max idle client's time before PING are sent PingThreshold = time.Second * 90 ) func Processor(events chan ClientEvent, finished chan struct{}) { var now time.Time ticker := time.NewTicker(10 * time.Second) go func() { for range ticker.C { events <- ClientEvent{eventType: EventTick} } }() EventsCycle: for e := range events { now = time.Now() client := e.client switch e.eventType { case EventTick: clientsLock.RLock() for c := range clients { if c.recvTimestamp.Add(PingTimeout).Before(now) { if *verbose { log.Println(c, "ping timeout") } c.Close() continue } if c.sendTimestamp.Add(PingThreshold).Before(now) { if c.registered { c.Msg("PING :" + *hostname) c.sendTimestamp = time.Now() } else { if *verbose { log.Println(c, "ping timeout") } c.Close() } } } clientsLock.RUnlock() roomsLock.Lock() for rn, r := range rooms { if *statedir == "" && len(r.members) == 0 { if *verbose { log.Println(rn, "emptied room") } delete(rooms, rn) r.events <- ClientEvent{eventType: EventTerm} close(r.events) } } roomsLock.Unlock() case EventTerm: break EventsCycle case EventNew: clientsLock.Lock() clients[client] = struct{}{} clientsLock.Unlock() case EventDel: clientsLock.Lock() delete(clients, client) clientsLock.Unlock() roomsLock.RLock() for _, r := range rooms { r.events <- e } roomsLock.RUnlock() case EventMsg: cols := strings.SplitN(e.text, " ", 2) cmd := strings.ToUpper(cols[0]) if *verbose { log.Println(client, "command", cmd) } if cmd == "QUIT" { client.Close() if *verbose { log.Println(client, "quit") } continue } if !client.registered { client.Register(cmd, cols) continue } if client != nil { client.recvTimestamp = now } switch cmd { case "AWAY": if len(cols) == 1 { client.away = "" client.ReplyNicknamed("305", "You are no longer marked as being away") continue } client.away = strings.TrimLeft(cols[1], ":") client.ReplyNicknamed("306", "You have been marked as being away") case "JOIN": if len(cols) == 1 || len(cols[1]) < 1 { client.ReplyNotEnoughParameters("JOIN") continue } client.Join(cols[1]) case "LIST": client.SendList(cols) case "LUSERS": client.SendLusers() case "MODE": if len(cols) == 1 || len(cols[1]) < 1 { client.ReplyNotEnoughParameters("MODE") continue } cols = strings.SplitN(cols[1], " ", 2) if cols[0] == client.username { client.Msg("221 " + client.nickname + " +w") // client.ReplyNicknamed("501", "Unknown MODE flag") continue } room := cols[0] r, found := rooms[room] if !found { client.ReplyNoChannel(room) continue } if len(cols) == 1 { r.events <- ClientEvent{client, EventMode, ""} } else { r.events <- ClientEvent{client, EventMode, cols[1]} } case "MOTD": client.SendMotd() case "NAMES": rs := make([]*Room, len(cols)) roomsLock.RLock() if len(cols) == 0 { for _, r := range rooms { rs = append(rs, r) } } else { needed := make(map[string]struct{}, len(rs)) for _, r := range cols { needed[r] = struct{}{} } for rn, r := range rooms { if _, found := needed[rn]; found { rs = append(rs, r) } } } roomsLock.RUnlock() for _, r := range rs { r.SendNames(client) } case "PART": if len(cols) == 1 || len(cols[1]) < 1 { client.ReplyNotEnoughParameters("PART") continue } rs := strings.Split(cols[1], " ")[0] roomsLock.RLock() for _, room := range strings.Split(rs, ",") { if r, found := rooms[room]; found { r.events <- ClientEvent{client, EventDel, ""} } else { client.ReplyNoChannel(room) } } roomsLock.RUnlock() case "PING": if len(cols) == 1 { client.ReplyNicknamed("409", "No origin specified") continue } client.Reply(fmt.Sprintf("PONG %s :%s", *hostname, cols[1])) case "PONG": continue case "NOTICE", "PRIVMSG": if len(cols) == 1 { client.ReplyNicknamed("411", "No recipient given ("+cmd+")") continue } cols = strings.SplitN(cols[1], " ", 2) if len(cols) == 1 { client.ReplyNicknamed("412", "No text to send") continue } target := strings.ToLower(cols[0]) roomsLock.RLock() if r, found := rooms[target]; found { r.events <- ClientEvent{ client, EventMsg, cmd + " " + strings.TrimLeft(cols[1], ":"), } roomsLock.RUnlock() continue } roomsLock.RUnlock() var msg string clientsLock.RLock() for c := range clients { if c.nickname != target { continue } msg = fmt.Sprintf(":%s %s %s %s", client, cmd, c.nickname, cols[1]) c.Msg(msg) if c.away != "" { client.ReplyNicknamed("301", c.nickname, c.away) } break } clientsLock.RUnlock() if msg != "" { continue } client.ReplyNoNickChan(target) case "TOPIC": if len(cols) == 1 { client.ReplyNotEnoughParameters("TOPIC") continue } cols = strings.SplitN(cols[1], " ", 2) r, found := rooms[cols[0]] if !found { client.ReplyNoChannel(cols[0]) continue } var change string if len(cols) > 1 { change = cols[1] } r.events <- ClientEvent{client, EventTopic, change} case "WHO": if len(cols) == 1 || len(cols[1]) < 1 { client.ReplyNotEnoughParameters("WHO") continue } room := strings.Split(cols[1], " ")[0] r, found := rooms[room] if found { r.events <- ClientEvent{client, EventWho, ""} } else { client.ReplyNoChannel(room) } case "WHOIS": if len(cols) == 1 || len(cols[1]) < 1 { client.ReplyNotEnoughParameters("WHOIS") continue } cols := strings.Split(cols[1], " ") nicknames := strings.Split(cols[len(cols)-1], ",") client.SendWhois(nicknames) case "ISON": if len(cols) == 1 || len(cols[1]) < 1 { client.ReplyNotEnoughParameters("ISON") continue } nicknamesList := strings.Split(cols[1], " ") nicknames := make(map[string]bool, len(nicknamesList)) for _, nickname := range nicknamesList { nicknames[nickname] = false } clientsLock.RLock() for c := range clients { if _, exists := nicknames[c.nickname]; exists { nicknames[c.nickname] = true } } clientsLock.RUnlock() nicknamesList = nicknamesList[:0] for n, exists := range nicknames { if exists { nicknamesList = append(nicknamesList, n) } } client.ReplyNicknamed("303", strings.Join(nicknamesList, " ")) case "WALLOPS": if len(cols) == 1 { client.ReplyNotEnoughParameters("WALLOPS") continue } cs := make([]*Client, 0, len(clients)) clientsLock.RLock() for c := range clients { if c != client { cs = append(cs, c) } } clientsLock.RUnlock() for _, c := range cs { c.Msg(fmt.Sprintf(":%s NOTICE %s %s", client, c.nickname, cols[1])) } case "VERSION": var debug string if *verbose { debug = "debug" } client.ReplyNicknamed("351", fmt.Sprintf("%s.%s %s :", Version, debug, *hostname)) default: client.ReplyNicknamed("421", cmd, "Unknown command") } } } ticker.Stop() // Notify all clients about shutdown clientsLock.RLock() for c := range clients { c.Msg(fmt.Sprintf( ":%s NOTICE %s %s", *hostname, c.nickname, ":Server is shutting down", )) c.Close() } clientsLock.RUnlock() // Read their EventDel go func() { for range events { } }() // Stop room processors roomsLock.RLock() for _, r := range rooms { r.events <- ClientEvent{eventType: EventTerm} } roomsLock.RUnlock() roomsWG.Wait() // Wait for either 5sec or all clients quitting t := time.NewTimer(5 * time.Second) clientsDone := make(chan struct{}) go func() { clientsWG.Wait() close(clientsDone) }() select { case <-t.C: case <-clientsDone: } if !t.Stop() { <-t.C } close(events) close(finished) }