Code refactoring

* Less memory allocations
* Daemon instance replaced with global variables
* Code simplification
* Asynchronously send messages to clients
This commit is contained in:
Sergey Matveev 2015-10-11 11:12:55 +03:00
parent 186ec4b4bd
commit e657ffd2ab
9 changed files with 518 additions and 452 deletions

121
client.go
View File

@ -23,11 +23,13 @@ import (
"log" "log"
"net" "net"
"strings" "strings"
"sync"
"time" "time"
) )
const ( const (
BufSize = 1500 BufSize = 1500
MaxOutBuf = 1 << 12
) )
var ( var (
@ -35,35 +37,60 @@ var (
) )
type Client struct { type Client struct {
hostname *string conn net.Conn
conn net.Conn registered bool
registered bool nickname *string
nickname string username *string
username string realname *string
realname string password *string
password string away *string
away *string recvTimestamp time.Time
sendTimestamp time.Time
outBuf chan string
alive bool
sync.Mutex
} }
type ClientAlivenessState struct { func (c Client) String() string {
pingSent bool return *c.nickname + "!" + *c.username + "@" + c.conn.RemoteAddr().String()
timestamp time.Time
} }
func (client Client) String() string { func NewClient(conn net.Conn) *Client {
return client.nickname + "!" + client.username + "@" + client.conn.RemoteAddr().String() nickname := "*"
username := ""
c := Client{
conn: conn,
nickname: &nickname,
username: &username,
recvTimestamp: time.Now(),
sendTimestamp: time.Now(),
alive: true,
outBuf: make(chan string, MaxOutBuf),
}
go c.MsgSender()
return &c
} }
func NewClient(hostname *string, conn net.Conn) *Client { func (c *Client) SetDead() {
return &Client{hostname: hostname, conn: conn, nickname: "*", password: ""} close(c.outBuf)
c.alive = false
}
func (c *Client) Close() {
c.Lock()
c.conn.Close()
if c.alive {
c.SetDead()
}
c.Unlock()
} }
// Client processor blockingly reads everything remote client sends, // Client processor blockingly reads everything remote client sends,
// splits messages by CRLF and send them to Daemon gorouting for processing // splits messages by CRLF and send them to Daemon gorouting for processing
// it futher. Also it can signalize that client is unavailable (disconnected). // it futher. Also it can signalize that client is unavailable (disconnected).
func (client *Client) Processor(sink chan<- ClientEvent) { func (c *Client) Processor(sink chan ClientEvent) {
sink <- ClientEvent{client, EventNew, ""} sink <- ClientEvent{c, EventNew, ""}
log.Println(client, "New client") log.Println(c, "New client")
buf := make([]byte, BufSize*2) buf := make([]byte, BufSize*2)
var n int var n int
var prev int var prev int
@ -71,14 +98,11 @@ func (client *Client) Processor(sink chan<- ClientEvent) {
var err error var err error
for { for {
if prev == BufSize { if prev == BufSize {
log.Println(client, "buffer size exceeded, kicking him") log.Println(c, "input buffer size exceeded, kicking him")
sink <- ClientEvent{client, EventDel, ""}
client.conn.Close()
break break
} }
n, err = client.conn.Read(buf[prev:]) n, err = c.conn.Read(buf[prev:])
if err != nil { if err != nil {
sink <- ClientEvent{client, EventDel, ""}
break break
} }
prev += n prev += n
@ -87,50 +111,69 @@ func (client *Client) Processor(sink chan<- ClientEvent) {
if i == -1 { if i == -1 {
continue continue
} }
sink <- ClientEvent{client, EventMsg, string(buf[:i])} sink <- ClientEvent{c, EventMsg, string(buf[:i])}
copy(buf, buf[i+2:prev]) copy(buf, buf[i+2:prev])
prev -= (i + 2) prev -= (i + 2)
goto CheckMore goto CheckMore
} }
c.Close()
sink <- ClientEvent{c, EventDel, ""}
}
func (c *Client) MsgSender() {
for msg := range c.outBuf {
c.conn.Write(append([]byte(msg), CRLF...))
}
} }
// Send message as is with CRLF appended. // Send message as is with CRLF appended.
func (client *Client) Msg(text string) { func (c *Client) Msg(text string) {
client.conn.Write(append([]byte(text), CRLF...)) c.Lock()
defer c.Unlock()
if !c.alive {
return
}
if len(c.outBuf) == MaxOutBuf {
log.Println(c, "output buffer size exceeded, kicking him")
go c.Close()
c.SetDead()
return
}
c.outBuf <- text
} }
// Send message from server. It has ": servername" prefix. // Send message from server. It has ": servername" prefix.
func (client *Client) Reply(text string) { func (c *Client) Reply(text string) {
client.Msg(":" + *client.hostname + " " + text) c.Msg(":" + *hostname + " " + text)
} }
// Send server message, concatenating all provided text parts and // Send server message, concatenating all provided text parts and
// prefix the last one with ":". // prefix the last one with ":".
func (client *Client) ReplyParts(code string, text ...string) { func (c *Client) ReplyParts(code string, text ...string) {
parts := []string{code} parts := []string{code}
for _, t := range text { for _, t := range text {
parts = append(parts, t) parts = append(parts, t)
} }
parts[len(parts)-1] = ":" + parts[len(parts)-1] parts[len(parts)-1] = ":" + parts[len(parts)-1]
client.Reply(strings.Join(parts, " ")) c.Reply(strings.Join(parts, " "))
} }
// Send nicknamed server message. After servername it always has target // Send nicknamed server message. After servername it always has target
// client's nickname. The last part is prefixed with ":". // client's nickname. The last part is prefixed with ":".
func (client *Client) ReplyNicknamed(code string, text ...string) { func (c *Client) ReplyNicknamed(code string, text ...string) {
client.ReplyParts(code, append([]string{client.nickname}, text...)...) c.ReplyParts(code, append([]string{*c.nickname}, text...)...)
} }
// Reply "461 not enough parameters" error for given command. // Reply "461 not enough parameters" error for given command.
func (client *Client) ReplyNotEnoughParameters(command string) { func (c *Client) ReplyNotEnoughParameters(command string) {
client.ReplyNicknamed("461", command, "Not enough parameters") c.ReplyNicknamed("461", command, "Not enough parameters")
} }
// Reply "403 no such channel" error for specified channel. // Reply "403 no such channel" error for specified channel.
func (client *Client) ReplyNoChannel(channel string) { func (c *Client) ReplyNoChannel(channel string) {
client.ReplyNicknamed("403", channel, "No such channel") c.ReplyNicknamed("403", channel, "No such channel")
} }
func (client *Client) ReplyNoNickChan(channel string) { func (c *Client) ReplyNoNickChan(channel string) {
client.ReplyNicknamed("401", channel, "No such nick/channel") c.ReplyNicknamed("401", channel, "No such nick/channel")
} }

View File

@ -19,86 +19,17 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
package main package main
import ( import (
"net"
"testing" "testing"
"time"
) )
// Testing network connection that satisfies net.Conn interface
// Can send predefined messages and store all written ones
type TestingConn struct {
inbound chan string
outbound chan string
closed bool
}
func NewTestingConn() *TestingConn {
inbound := make(chan string, 8)
outbound := make(chan string, 8)
return &TestingConn{inbound: inbound, outbound: outbound}
}
func (conn TestingConn) Error() string {
return "i am finished"
}
func (conn *TestingConn) Read(b []byte) (n int, err error) {
msg := <-conn.inbound
if msg == "" {
return 0, conn
}
for n, bt := range append([]byte(msg), CRLF...) {
b[n] = bt
}
return len(msg)+2, nil
}
type MyAddr struct{}
func (a MyAddr) String() string {
return "someclient"
}
func (a MyAddr) Network() string {
return "somenet"
}
func (conn *TestingConn) Write(b []byte) (n int, err error) {
conn.outbound <- string(b)
return len(b), nil
}
func (conn *TestingConn) Close() error {
conn.closed = true
return nil
}
func (conn TestingConn) LocalAddr() net.Addr {
return nil
}
func (conn TestingConn) RemoteAddr() net.Addr {
return MyAddr{}
}
func (conn TestingConn) SetDeadline(t time.Time) error {
return nil
}
func (conn TestingConn) SetReadDeadline(t time.Time) error {
return nil
}
func (conn TestingConn) SetWriteDeadline(t time.Time) error {
return nil
}
// New client creation test. It must send an event about new client, // New client creation test. It must send an event about new client,
// two predefined messages from it and deletion one // two predefined messages from it and deletion one
func TestNewClient(t *testing.T) { func TestNewClient(t *testing.T) {
conn := NewTestingConn() conn := NewTestingConn()
sink := make(chan ClientEvent) sink := make(chan ClientEvent)
host := "foohost" host := "foohost"
client := NewClient(&host, conn) hostname = &host
client := NewClient(conn)
go client.Processor(sink) go client.Processor(sink)
event := <-sink event := <-sink
@ -126,8 +57,10 @@ func TestNewClient(t *testing.T) {
func TestClientReplies(t *testing.T) { func TestClientReplies(t *testing.T) {
conn := NewTestingConn() conn := NewTestingConn()
host := "foohost" host := "foohost"
client := NewClient(&host, conn) hostname = &host
client.nickname = "мойник" client := NewClient(conn)
nickname := "мойник"
client.nickname = &nickname
client.Reply("hello") client.Reply("hello")
if r := <-conn.outbound; r != ":foohost hello\r\n" { if r := <-conn.outbound; r != ":foohost hello\r\n" {

93
common_test.go Normal file
View File

@ -0,0 +1,93 @@
/*
goircd -- minimalistic simple Internet Relay Chat (IRC) server
Copyright (C) 2014-2015 Sergey Matveev <stargrave@stargrave.org>
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, either version 3 of the License, or
(at your option) any later version.
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 <http://www.gnu.org/licenses/>.
*/
package main
import (
"net"
"time"
)
// Testing network connection that satisfies net.Conn interface
// Can send predefined messages and store all written ones
type TestingConn struct {
inbound chan string
outbound chan string
closed bool
}
func NewTestingConn() *TestingConn {
inbound := make(chan string, 8)
outbound := make(chan string, 8)
return &TestingConn{inbound: inbound, outbound: outbound}
}
func (conn TestingConn) Error() string {
return "i am finished"
}
func (conn *TestingConn) Read(b []byte) (n int, err error) {
msg := <-conn.inbound
if msg == "" {
return 0, conn
}
for n, bt := range append([]byte(msg), CRLF...) {
b[n] = bt
}
return len(msg) + 2, nil
}
type MyAddr struct{}
func (a MyAddr) String() string {
return "someclient"
}
func (a MyAddr) Network() string {
return "somenet"
}
func (conn *TestingConn) Write(b []byte) (n int, err error) {
conn.outbound <- string(b)
return len(b), nil
}
func (conn *TestingConn) Close() error {
conn.closed = true
close(conn.outbound)
return nil
}
func (conn TestingConn) LocalAddr() net.Addr {
return nil
}
func (conn TestingConn) RemoteAddr() net.Addr {
return MyAddr{}
}
func (conn TestingConn) SetDeadline(t time.Time) error {
return nil
}
func (conn TestingConn) SetReadDeadline(t time.Time) error {
return nil
}
func (conn TestingConn) SetWriteDeadline(t time.Time) error {
return nil
}

391
daemon.go
View File

@ -26,56 +26,28 @@ import (
"regexp" "regexp"
"sort" "sort"
"strings" "strings"
"sync"
"time" "time"
) )
const ( const (
// Max time deadline for client's unresponsiveness // Max deadline time of client's unresponsiveness
PingTimeout = time.Second * 180 PingTimeout = time.Second * 180
// Max idle client's time before PING are sent // Max idle client's time before PING are sent
PingThreshold = time.Second * 90 PingThreshold = time.Second * 90
// Client's aliveness check period
AlivenessCheck = time.Second * 10
) )
var ( var (
RENickname = regexp.MustCompile("^[a-zA-Z0-9-]{1,24}$") RENickname = regexp.MustCompile("^[a-zA-Z0-9-]{1,24}$")
roomsGroup sync.WaitGroup
clients map[*Client]struct{} = make(map[*Client]struct{})
) )
type Daemon struct { func SendLusers(client *Client) {
Verbose bool
version string
hostname *string
motd *string
passwords *string
clients map[*Client]struct{}
clientAliveness map[*Client]*ClientAlivenessState
rooms map[string]*Room
roomSinks map[*Room]chan ClientEvent
lastAlivenessCheck time.Time
logSink chan<- LogEvent
stateSink chan<- StateEvent
}
func NewDaemon(version string, hostname, motd, passwords *string, logSink chan<- LogEvent, stateSink chan<- StateEvent) *Daemon {
daemon := Daemon{
version: version,
hostname: hostname,
motd: motd,
passwords: passwords,
}
daemon.clients = make(map[*Client]struct{})
daemon.clientAliveness = make(map[*Client]*ClientAlivenessState)
daemon.rooms = make(map[string]*Room)
daemon.roomSinks = make(map[*Room]chan ClientEvent)
daemon.logSink = logSink
daemon.stateSink = stateSink
return &daemon
}
func (daemon *Daemon) SendLusers(client *Client) {
lusers := 0 lusers := 0
for client := range daemon.clients { for client := range clients {
if client.registered { if client.registered {
lusers++ lusers++
} }
@ -83,79 +55,87 @@ func (daemon *Daemon) SendLusers(client *Client) {
client.ReplyNicknamed("251", fmt.Sprintf("There are %d users and 0 invisible on 1 servers", lusers)) client.ReplyNicknamed("251", fmt.Sprintf("There are %d users and 0 invisible on 1 servers", lusers))
} }
func (daemon *Daemon) SendMotd(client *Client) { func SendMotd(client *Client) {
if daemon.motd == nil || *daemon.motd == "" { if motd == nil {
client.ReplyNicknamed("422", "MOTD File is missing") client.ReplyNicknamed("422", "MOTD File is missing")
return return
} }
motdText, err := ioutil.ReadFile(*motd)
motd, err := ioutil.ReadFile(*daemon.motd)
if err != nil { if err != nil {
log.Printf("Can not read motd file %s: %v", *daemon.motd, err) log.Printf("Can not read motd file %s: %v", *motd, err)
client.ReplyNicknamed("422", "Error reading MOTD File") client.ReplyNicknamed("422", "Error reading MOTD File")
return return
} }
client.ReplyNicknamed("375", "- "+*hostname+" Message of the day -")
client.ReplyNicknamed("375", "- "+*daemon.hostname+" Message of the day -") for _, s := range strings.Split(strings.Trim(string(motdText), "\n"), "\n") {
for _, s := range strings.Split(strings.Trim(string(motd), "\n"), "\n") { client.ReplyNicknamed("372", "- "+s)
client.ReplyNicknamed("372", "- "+string(s))
} }
client.ReplyNicknamed("376", "End of /MOTD command") client.ReplyNicknamed("376", "End of /MOTD command")
} }
func (daemon *Daemon) SendWhois(client *Client, nicknames []string) { func SendWhois(client *Client, nicknames []string) {
var c *Client
var hostPort string
var err error
var subscriptions []string
var room *Room
var subscriber *Client
for _, nickname := range nicknames { for _, nickname := range nicknames {
nickname = strings.ToLower(nickname) nickname = strings.ToLower(nickname)
found := false for c = range clients {
for c := range daemon.clients { if strings.ToLower(*c.nickname) == nickname {
if strings.ToLower(c.nickname) != nickname { goto Found
continue
} }
found = true }
h := c.conn.RemoteAddr().String() client.ReplyNoNickChan(nickname)
h, _, err := net.SplitHostPort(h) continue
if err != nil { Found:
log.Printf("Can't parse RemoteAddr %q: %v", h, err) hostPort, _, err = net.SplitHostPort(c.conn.RemoteAddr().String())
h = "Unknown" if err != nil {
} log.Printf("Can't parse RemoteAddr %q: %v", hostPort, err)
client.ReplyNicknamed("311", c.nickname, c.username, h, "*", c.realname) hostPort = "Unknown"
client.ReplyNicknamed("312", c.nickname, *daemon.hostname, *daemon.hostname) }
if c.away != nil { client.ReplyNicknamed("311", *c.nickname, *c.username, hostPort, "*", *c.realname)
client.ReplyNicknamed("301", c.nickname, *c.away) client.ReplyNicknamed("312", *c.nickname, *hostname, *hostname)
} if c.away != nil {
subscriptions := []string{} client.ReplyNicknamed("301", *c.nickname, *c.away)
for _, room := range daemon.rooms { }
for subscriber := range room.members { subscriptions = make([]string, 0)
if subscriber.nickname == nickname { for _, room = range rooms {
subscriptions = append(subscriptions, room.name) for subscriber = range room.members {
} if *subscriber.nickname == nickname {
subscriptions = append(subscriptions, *room.name)
} }
} }
sort.Strings(subscriptions)
client.ReplyNicknamed("319", c.nickname, strings.Join(subscriptions, " "))
client.ReplyNicknamed("318", c.nickname, "End of /WHOIS list")
}
if !found {
client.ReplyNoNickChan(nickname)
} }
sort.Strings(subscriptions)
client.ReplyNicknamed("319", *c.nickname, strings.Join(subscriptions, " "))
client.ReplyNicknamed("318", *c.nickname, "End of /WHOIS list")
} }
} }
func (daemon *Daemon) SendList(client *Client, cols []string) { func SendList(client *Client, cols []string) {
var rooms []string var rs []string
var r string
if (len(cols) > 1) && (cols[1] != "") { if (len(cols) > 1) && (cols[1] != "") {
rooms = strings.Split(strings.Split(cols[1], " ")[0], ",") rs = strings.Split(strings.Split(cols[1], " ")[0], ",")
} else { } else {
rooms = []string{} rs = make([]string, 0)
for room := range daemon.rooms { for r = range rooms {
rooms = append(rooms, room) rs = append(rs, r)
} }
} }
sort.Strings(rooms) sort.Strings(rs)
for _, room := range rooms { var room *Room
r, found := daemon.rooms[room] var found bool
if found { for _, r = range rs {
client.ReplyNicknamed("322", room, fmt.Sprintf("%d", len(r.members)), r.topic) if room, found = rooms[r]; found {
client.ReplyNicknamed(
"322",
r,
fmt.Sprintf("%d", len(room.members)),
*room.topic,
)
} }
} }
client.ReplyNicknamed("323", "End of /LIST") client.ReplyNicknamed("323", "End of /LIST")
@ -166,14 +146,14 @@ func (daemon *Daemon) SendList(client *Client, cols []string) {
// * only QUIT, NICK and USER commands are processed // * only QUIT, NICK and USER commands are processed
// * other commands are quietly ignored // * other commands are quietly ignored
// When client finishes NICK/USER workflow, then MOTD and LUSERS are send to him. // When client finishes NICK/USER workflow, then MOTD and LUSERS are send to him.
func (daemon *Daemon) ClientRegister(client *Client, command string, cols []string) { func ClientRegister(client *Client, cmd string, cols []string) {
switch command { switch cmd {
case "PASS": case "PASS":
if len(cols) == 1 || len(cols[1]) < 1 { if len(cols) == 1 || len(cols[1]) < 1 {
client.ReplyNotEnoughParameters("PASS") client.ReplyNotEnoughParameters("PASS")
return return
} }
client.password = cols[1] client.password = &cols[1]
case "NICK": case "NICK":
if len(cols) == 1 || len(cols[1]) < 1 { if len(cols) == 1 || len(cols[1]) < 1 {
client.ReplyParts("431", "No nickname given") client.ReplyParts("431", "No nickname given")
@ -182,8 +162,8 @@ func (daemon *Daemon) ClientRegister(client *Client, command string, cols []stri
nickname := cols[1] nickname := cols[1]
// Compatibility with some clients prepending colons to nickname // Compatibility with some clients prepending colons to nickname
nickname = strings.TrimPrefix(nickname, ":") nickname = strings.TrimPrefix(nickname, ":")
for existingClient := range daemon.clients { for existingClient := range clients {
if existingClient.nickname == nickname { if *existingClient.nickname == nickname {
client.ReplyParts("433", "*", nickname, "Nickname is already in use") client.ReplyParts("433", "*", nickname, "Nickname is already in use")
return return
} }
@ -192,7 +172,7 @@ func (daemon *Daemon) ClientRegister(client *Client, command string, cols []stri
client.ReplyParts("432", "*", cols[1], "Erroneous nickname") client.ReplyParts("432", "*", cols[1], "Erroneous nickname")
return return
} }
client.nickname = nickname client.nickname = &nickname
case "USER": case "USER":
if len(cols) == 1 { if len(cols) == 1 {
client.ReplyNotEnoughParameters("USER") client.ReplyNotEnoughParameters("USER")
@ -203,66 +183,69 @@ func (daemon *Daemon) ClientRegister(client *Client, command string, cols []stri
client.ReplyNotEnoughParameters("USER") client.ReplyNotEnoughParameters("USER")
return return
} }
client.username = args[0] client.username = &args[0]
client.realname = strings.TrimLeft(args[3], ":") realname := strings.TrimLeft(args[3], ":")
client.realname = &realname
} }
if client.nickname != "*" && client.username != "" { if *client.nickname != "*" && *client.username != "" {
if daemon.passwords != nil && *daemon.passwords != "" { if passwords != nil && *passwords != "" {
if client.password == "" { if client.password == nil {
client.ReplyParts("462", "You may not register") client.ReplyParts("462", "You may not register")
client.conn.Close() client.Close()
return return
} }
contents, err := ioutil.ReadFile(*daemon.passwords) contents, err := ioutil.ReadFile(*passwords)
if err != nil { if err != nil {
log.Fatalf("Can no read passwords file %s: %s", *daemon.passwords, err) log.Fatalf("Can no read passwords file %s: %s", *passwords, err)
return return
} }
for _, entry := range strings.Split(string(contents), "\n") { for _, entry := range strings.Split(string(contents), "\n") {
if entry == "" { if entry == "" {
continue continue
} }
if lp := strings.Split(entry, ":"); lp[0] == client.nickname && lp[1] != client.password { if lp := strings.Split(entry, ":"); lp[0] == *client.nickname && lp[1] != *client.password {
client.ReplyParts("462", "You may not register") client.ReplyParts("462", "You may not register")
client.conn.Close() client.Close()
return return
} }
} }
} }
client.registered = true client.registered = true
client.ReplyNicknamed("001", "Hi, welcome to IRC") client.ReplyNicknamed("001", "Hi, welcome to IRC")
client.ReplyNicknamed("002", "Your host is "+*daemon.hostname+", running goircd "+daemon.version) client.ReplyNicknamed("002", "Your host is "+*hostname+", running goircd "+version)
client.ReplyNicknamed("003", "This server was created sometime") client.ReplyNicknamed("003", "This server was created sometime")
client.ReplyNicknamed("004", *daemon.hostname+" goircd o o") client.ReplyNicknamed("004", *hostname+" goircd o o")
daemon.SendLusers(client) SendLusers(client)
daemon.SendMotd(client) SendMotd(client)
log.Println(client, "logged in") log.Println(client, "logged in")
} }
} }
// Register new room in Daemon. Create an object, events sink, save pointers // Register new room in Daemon. Create an object, events sink, save pointers
// to corresponding daemon's places and start room's processor goroutine. // to corresponding daemon's places and start room's processor goroutine.
func (daemon *Daemon) RoomRegister(name string) (*Room, chan<- ClientEvent) { func RoomRegister(name string) (*Room, chan ClientEvent) {
roomNew := NewRoom(daemon.hostname, name, daemon.logSink, daemon.stateSink) roomNew := NewRoom(name)
roomNew.Verbose = daemon.Verbose
roomSink := make(chan ClientEvent) roomSink := make(chan ClientEvent)
daemon.rooms[name] = roomNew rooms[name] = roomNew
daemon.roomSinks[roomNew] = roomSink roomSinks[roomNew] = roomSink
go roomNew.Processor(roomSink) go roomNew.Processor(roomSink)
roomsGroup.Add(1)
return roomNew, roomSink return roomNew, roomSink
} }
func (daemon *Daemon) HandlerJoin(client *Client, cmd string) { func HandlerJoin(client *Client, cmd string) {
args := strings.Split(cmd, " ") args := strings.Split(cmd, " ")
rooms := strings.Split(args[0], ",") rs := strings.Split(args[0], ",")
var keys []string var keys []string
if len(args) > 1 { if len(args) > 1 {
keys = strings.Split(args[1], ",") keys = strings.Split(args[1], ",")
} else { } else {
keys = []string{} keys = make([]string, 0)
} }
for n, room := range rooms { var roomExisting *Room
var roomSink chan ClientEvent
var roomNew *Room
for n, room := range rs {
if !RoomNameValid(room) { if !RoomNameValid(room) {
client.ReplyNoChannel(room) client.ReplyNoChannel(room)
continue continue
@ -273,97 +256,88 @@ func (daemon *Daemon) HandlerJoin(client *Client, cmd string) {
} else { } else {
key = "" key = ""
} }
denied := false for roomExisting, roomSink = range roomSinks {
joined := false if room == *roomExisting.name {
for roomExisting, roomSink := range daemon.roomSinks { if (roomExisting.key != nil) && (*roomExisting.key != key) {
if room == roomExisting.name { goto Denied
if (roomExisting.key != "") && (roomExisting.key != key) {
denied = true
} else {
roomSink <- ClientEvent{client, EventNew, ""}
joined = true
} }
break roomSink <- ClientEvent{client, EventNew, ""}
goto Joined
} }
} }
if denied { roomNew, roomSink = RoomRegister(room)
client.ReplyNicknamed("475", room, "Cannot join channel (+k) - bad key")
}
if denied || joined {
continue
}
roomNew, roomSink := daemon.RoomRegister(room)
log.Println("Room", roomNew, "created") log.Println("Room", roomNew, "created")
if key != "" { if key != "" {
roomNew.key = key roomNew.key = &key
roomNew.StateSave() roomNew.StateSave()
} }
roomSink <- ClientEvent{client, EventNew, ""} roomSink <- ClientEvent{client, EventNew, ""}
continue
Denied:
client.ReplyNicknamed("475", room, "Cannot join channel (+k) - bad key")
Joined:
} }
} }
func (daemon *Daemon) Processor(events <-chan ClientEvent) { func Processor(events chan ClientEvent, finished chan struct{}) {
var now time.Time var now time.Time
go func() {
for {
time.Sleep(10 * time.Second)
events <- ClientEvent{eventType: EventTick}
}
}()
for event := range events { for event := range events {
now = time.Now() now = time.Now()
client := event.client client := event.client
switch event.eventType {
// Check for clients aliveness case EventTick:
if daemon.lastAlivenessCheck.Add(AlivenessCheck).Before(now) { for c := range clients {
for c := range daemon.clients { if c.recvTimestamp.Add(PingTimeout).Before(now) {
aliveness, alive := daemon.clientAliveness[c]
if !alive {
continue
}
if aliveness.timestamp.Add(PingTimeout).Before(now) {
log.Println(c, "ping timeout") log.Println(c, "ping timeout")
c.conn.Close() c.Close()
continue continue
} }
if !aliveness.pingSent && aliveness.timestamp.Add(PingThreshold).Before(now) { if c.sendTimestamp.Add(PingThreshold).Before(now) {
if c.registered { if c.registered {
c.Msg("PING :" + *daemon.hostname) c.Msg("PING :" + *hostname)
aliveness.pingSent = true c.sendTimestamp = time.Now()
} else { } else {
log.Println(c, "ping timeout") log.Println(c, "ping timeout")
c.conn.Close() c.Close()
} }
} }
} }
daemon.lastAlivenessCheck = now case EventTerm:
} for _, sink := range roomSinks {
sink <- ClientEvent{eventType: EventTerm}
switch event.eventType {
case EventNew:
daemon.clients[client] = struct{}{}
daemon.clientAliveness[client] = &ClientAlivenessState{
pingSent: false,
timestamp: now,
} }
roomsGroup.Wait()
close(finished)
return
case EventNew:
clients[client] = struct{}{}
case EventDel: case EventDel:
delete(daemon.clients, client) delete(clients, client)
delete(daemon.clientAliveness, client) for _, roomSink := range roomSinks {
for _, roomSink := range daemon.roomSinks {
roomSink <- event roomSink <- event
} }
case EventMsg: case EventMsg:
cols := strings.SplitN(event.text, " ", 2) cols := strings.SplitN(event.text, " ", 2)
command := strings.ToUpper(cols[0]) cmd := strings.ToUpper(cols[0])
if daemon.Verbose { if *verbose {
log.Println(client, "command", command) log.Println(client, "command", cmd)
} }
if command == "QUIT" { if cmd == "QUIT" {
log.Println(client, "quit") log.Println(client, "quit")
delete(daemon.clients, client) client.Close()
delete(daemon.clientAliveness, client)
client.conn.Close()
continue continue
} }
if !client.registered { if !client.registered {
daemon.ClientRegister(client, command, cols) ClientRegister(client, cmd, cols)
continue continue
} }
switch command { switch cmd {
case "AWAY": case "AWAY":
if len(cols) == 1 { if len(cols) == 1 {
client.away = nil client.away = nil
@ -378,63 +352,63 @@ func (daemon *Daemon) Processor(events <-chan ClientEvent) {
client.ReplyNotEnoughParameters("JOIN") client.ReplyNotEnoughParameters("JOIN")
continue continue
} }
daemon.HandlerJoin(client, cols[1]) HandlerJoin(client, cols[1])
case "LIST": case "LIST":
daemon.SendList(client, cols) SendList(client, cols)
case "LUSERS": case "LUSERS":
daemon.SendLusers(client) SendLusers(client)
case "MODE": case "MODE":
if len(cols) == 1 || len(cols[1]) < 1 { if len(cols) == 1 || len(cols[1]) < 1 {
client.ReplyNotEnoughParameters("MODE") client.ReplyNotEnoughParameters("MODE")
continue continue
} }
cols = strings.SplitN(cols[1], " ", 2) cols = strings.SplitN(cols[1], " ", 2)
if cols[0] == client.username { if cols[0] == *client.username {
if len(cols) == 1 { if len(cols) == 1 {
client.Msg("221 " + client.nickname + " +") client.Msg("221 " + *client.nickname + " +")
} else { } else {
client.ReplyNicknamed("501", "Unknown MODE flag") client.ReplyNicknamed("501", "Unknown MODE flag")
} }
continue continue
} }
room := cols[0] room := cols[0]
r, found := daemon.rooms[room] r, found := rooms[room]
if !found { if !found {
client.ReplyNoChannel(room) client.ReplyNoChannel(room)
continue continue
} }
if len(cols) == 1 { if len(cols) == 1 {
daemon.roomSinks[r] <- ClientEvent{client, EventMode, ""} roomSinks[r] <- ClientEvent{client, EventMode, ""}
} else { } else {
daemon.roomSinks[r] <- ClientEvent{client, EventMode, cols[1]} roomSinks[r] <- ClientEvent{client, EventMode, cols[1]}
} }
case "MOTD": case "MOTD":
go daemon.SendMotd(client) SendMotd(client)
case "PART": case "PART":
if len(cols) == 1 || len(cols[1]) < 1 { if len(cols) == 1 || len(cols[1]) < 1 {
client.ReplyNotEnoughParameters("PART") client.ReplyNotEnoughParameters("PART")
continue continue
} }
rooms := strings.Split(cols[1], " ")[0] rs := strings.Split(cols[1], " ")[0]
for _, room := range strings.Split(rooms, ",") { for _, room := range strings.Split(rs, ",") {
r, found := daemon.rooms[room] if r, found := rooms[room]; found {
if !found { roomSinks[r] <- ClientEvent{client, EventDel, ""}
} else {
client.ReplyNoChannel(room) client.ReplyNoChannel(room)
continue continue
} }
daemon.roomSinks[r] <- ClientEvent{client, EventDel, ""}
} }
case "PING": case "PING":
if len(cols) == 1 { if len(cols) == 1 {
client.ReplyNicknamed("409", "No origin specified") client.ReplyNicknamed("409", "No origin specified")
continue continue
} }
client.Reply(fmt.Sprintf("PONG %s :%s", *daemon.hostname, cols[1])) client.Reply(fmt.Sprintf("PONG %s :%s", *hostname, cols[1]))
case "PONG": case "PONG":
continue continue
case "NOTICE", "PRIVMSG": case "NOTICE", "PRIVMSG":
if len(cols) == 1 { if len(cols) == 1 {
client.ReplyNicknamed("411", "No recipient given ("+command+")") client.ReplyNicknamed("411", "No recipient given ("+cmd+")")
continue continue
} }
cols = strings.SplitN(cols[1], " ", 2) cols = strings.SplitN(cols[1], " ", 2)
@ -444,12 +418,12 @@ func (daemon *Daemon) Processor(events <-chan ClientEvent) {
} }
msg := "" msg := ""
target := strings.ToLower(cols[0]) target := strings.ToLower(cols[0])
for c := range daemon.clients { for c := range clients {
if c.nickname == target { if *c.nickname == target {
msg = fmt.Sprintf(":%s %s %s %s", client, command, c.nickname, cols[1]) msg = fmt.Sprintf(":%s %s %s %s", client, cmd, *c.nickname, cols[1])
c.Msg(msg) c.Msg(msg)
if c.away != nil { if c.away != nil {
client.ReplyNicknamed("301", c.nickname, *c.away) client.ReplyNicknamed("301", *c.nickname, *c.away)
} }
break break
} }
@ -457,15 +431,14 @@ func (daemon *Daemon) Processor(events <-chan ClientEvent) {
if msg != "" { if msg != "" {
continue continue
} }
r, found := daemon.rooms[target] if r, found := rooms[target]; found {
if !found { roomSinks[r] <- ClientEvent{
client,
EventMsg,
cmd + " " + strings.TrimLeft(cols[1], ":"),
}
} else {
client.ReplyNoNickChan(target) client.ReplyNoNickChan(target)
continue
}
daemon.roomSinks[r] <- ClientEvent{
client,
EventMsg,
command + " " + strings.TrimLeft(cols[1], ":"),
} }
case "TOPIC": case "TOPIC":
if len(cols) == 1 { if len(cols) == 1 {
@ -473,7 +446,7 @@ func (daemon *Daemon) Processor(events <-chan ClientEvent) {
continue continue
} }
cols = strings.SplitN(cols[1], " ", 2) cols = strings.SplitN(cols[1], " ", 2)
r, found := daemon.rooms[cols[0]] r, found := rooms[cols[0]]
if !found { if !found {
client.ReplyNoChannel(cols[0]) client.ReplyNoChannel(cols[0])
continue continue
@ -484,19 +457,18 @@ func (daemon *Daemon) Processor(events <-chan ClientEvent) {
} else { } else {
change = "" change = ""
} }
daemon.roomSinks[r] <- ClientEvent{client, EventTopic, change} roomSinks[r] <- ClientEvent{client, EventTopic, change}
case "WHO": case "WHO":
if len(cols) == 1 || len(cols[1]) < 1 { if len(cols) == 1 || len(cols[1]) < 1 {
client.ReplyNotEnoughParameters("WHO") client.ReplyNotEnoughParameters("WHO")
continue continue
} }
room := strings.Split(cols[1], " ")[0] room := strings.Split(cols[1], " ")[0]
r, found := daemon.rooms[room] if r, found := rooms[room]; found {
if !found { roomSinks[r] <- ClientEvent{client, EventWho, ""}
} else {
client.ReplyNoChannel(room) client.ReplyNoChannel(room)
continue
} }
daemon.roomSinks[r] <- ClientEvent{client, EventWho, ""}
case "WHOIS": case "WHOIS":
if len(cols) == 1 || len(cols[1]) < 1 { if len(cols) == 1 || len(cols[1]) < 1 {
client.ReplyNotEnoughParameters("WHOIS") client.ReplyNotEnoughParameters("WHOIS")
@ -504,22 +476,21 @@ func (daemon *Daemon) Processor(events <-chan ClientEvent) {
} }
cols := strings.Split(cols[1], " ") cols := strings.Split(cols[1], " ")
nicknames := strings.Split(cols[len(cols)-1], ",") nicknames := strings.Split(cols[len(cols)-1], ",")
daemon.SendWhois(client, nicknames) SendWhois(client, nicknames)
case "VERSION": case "VERSION":
var debug string var debug string
if daemon.Verbose { if *verbose {
debug = "debug" debug = "debug"
} else { } else {
debug = "" debug = ""
} }
client.ReplyNicknamed("351", fmt.Sprintf("%s.%s %s :", daemon.version, debug, *daemon.hostname)) client.ReplyNicknamed("351", fmt.Sprintf("%s.%s %s :", version, debug, *hostname))
default: default:
client.ReplyNicknamed("421", command, "Unknown command") client.ReplyNicknamed("421", cmd, "Unknown command")
} }
} }
if aliveness, alive := daemon.clientAliveness[client]; alive { if client != nil {
aliveness.timestamp = now client.recvTimestamp = now
aliveness.pingSent = false
} }
} }
} }

View File

@ -27,21 +27,24 @@ import (
func TestRegistrationWorkflow(t *testing.T) { func TestRegistrationWorkflow(t *testing.T) {
host := "foohost" host := "foohost"
daemon := NewDaemon("ver1", &host, nil, nil, nil, nil) hostname = &host
events := make(chan ClientEvent) events := make(chan ClientEvent)
go daemon.Processor(events) defer func() {
events <- ClientEvent{eventType: EventTerm}
}()
go Processor(events, make(chan struct{}))
conn := NewTestingConn() conn := NewTestingConn()
client := NewClient(&host, conn) client := NewClient(conn)
go client.Processor(events) go client.Processor(events)
conn.inbound <- "UNEXISTENT CMD" // should recieve nothing on this conn.inbound <- "UNEXISTENT CMD" // should receive nothing on this
conn.inbound <- "NICK" conn.inbound <- "NICK"
if r := <-conn.outbound; r != ":foohost 431 :No nickname given\r\n" { if r := <-conn.outbound; r != ":foohost 431 :No nickname given\r\n" {
t.Fatal("431 for NICK", r) t.Fatal("431 for NICK", r)
} }
for _, n := range []string{"привет", " foo", "longlonglong", "#foo", "mein nick", "foo_bar"} { for _, n := range []string{"привет", " foo", "#foo", "mein nick", "foo_bar"} {
conn.inbound <- "NICK " + n conn.inbound <- "NICK " + n
if r := <-conn.outbound; r != ":foohost 432 * "+n+" :Erroneous nickname\r\n" { if r := <-conn.outbound; r != ":foohost 432 * "+n+" :Erroneous nickname\r\n" {
t.Fatal("nickname validation", r) t.Fatal("nickname validation", r)
@ -52,7 +55,7 @@ func TestRegistrationWorkflow(t *testing.T) {
if r := <-conn.outbound; r != ":foohost 461 meinick USER :Not enough parameters\r\n" { if r := <-conn.outbound; r != ":foohost 461 meinick USER :Not enough parameters\r\n" {
t.Fatal("461 for USER", r) t.Fatal("461 for USER", r)
} }
if (client.nickname != "meinick") || client.registered { if (*client.nickname != "meinick") || client.registered {
t.Fatal("NICK saved") t.Fatal("NICK saved")
} }
@ -61,7 +64,7 @@ func TestRegistrationWorkflow(t *testing.T) {
t.Fatal("461 again for USER", r) t.Fatal("461 again for USER", r)
} }
daemon.SendLusers(client) SendLusers(client)
if r := <-conn.outbound; !strings.Contains(r, "There are 0 users") { if r := <-conn.outbound; !strings.Contains(r, "There are 0 users") {
t.Fatal("LUSERS", r) t.Fatal("LUSERS", r)
} }
@ -85,7 +88,7 @@ func TestRegistrationWorkflow(t *testing.T) {
if r := <-conn.outbound; !strings.Contains(r, ":foohost 422") { if r := <-conn.outbound; !strings.Contains(r, ":foohost 422") {
t.Fatal("422 after registration", r) t.Fatal("422 after registration", r)
} }
if (client.username != "1") || (client.realname != "4 5") || !client.registered { if (*client.username != "1") || (*client.realname != "4 5") || !client.registered {
t.Fatal("client register") t.Fatal("client register")
} }
@ -96,7 +99,7 @@ func TestRegistrationWorkflow(t *testing.T) {
t.Fatal("reply for unexistent command", r) t.Fatal("reply for unexistent command", r)
} }
daemon.SendLusers(client) SendLusers(client)
if r := <-conn.outbound; !strings.Contains(r, "There are 1 users") { if r := <-conn.outbound; !strings.Contains(r, "There are 1 users") {
t.Fatal("1 users logged in", r) t.Fatal("1 users logged in", r)
} }
@ -107,10 +110,6 @@ func TestRegistrationWorkflow(t *testing.T) {
} }
conn.inbound <- "QUIT\r\nUNEXISTENT CMD" conn.inbound <- "QUIT\r\nUNEXISTENT CMD"
<-conn.outbound
if !conn.closed {
t.Fatal("closed connection on QUIT")
}
} }
func TestMotd(t *testing.T) { func TestMotd(t *testing.T) {
@ -123,11 +122,12 @@ func TestMotd(t *testing.T) {
conn := NewTestingConn() conn := NewTestingConn()
host := "foohost" host := "foohost"
client := NewClient(&host, conn) hostname = &host
client := NewClient(conn)
motdName := fd.Name() motdName := fd.Name()
daemon := NewDaemon("ver1", &host, &motdName, nil, nil, nil) motd = &motdName
daemon.SendMotd(client) SendMotd(client)
if r := <-conn.outbound; !strings.HasPrefix(r, ":foohost 375") { if r := <-conn.outbound; !strings.HasPrefix(r, ":foohost 375") {
t.Fatal("MOTD start", r) t.Fatal("MOTD start", r)
} }

View File

@ -34,10 +34,17 @@ const (
EventTopic = iota EventTopic = iota
EventWho = iota EventWho = iota
EventMode = iota EventMode = iota
EventTerm = iota
EventTick = iota
FormatMsg = "[%s] <%s> %s\n" FormatMsg = "[%s] <%s> %s\n"
FormatMeta = "[%s] * %s %s\n" FormatMeta = "[%s] * %s %s\n"
) )
var (
logSink chan LogEvent = make(chan LogEvent)
stateSink chan StateEvent = make(chan StateEvent)
)
// Client events going from each of client // Client events going from each of client
// They can be either NEW, DEL or unparsed MSG // They can be either NEW, DEL or unparsed MSG
type ClientEvent struct { type ClientEvent struct {
@ -70,7 +77,7 @@ func Logger(logdir string, events <-chan LogEvent) {
var fd *os.File var fd *os.File
var err error var err error
for event := range events { for event := range events {
logfile = path.Join(logdir, event.where + ".log") logfile = path.Join(logdir, event.where+".log")
fd, err = os.OpenFile(logfile, mode, perm) fd, err = os.OpenFile(logfile, mode, perm)
if err != nil { if err != nil {
log.Println("Can not open logfile", logfile, err) log.Println("Can not open logfile", logfile, err)

View File

@ -37,21 +37,19 @@ var (
logdir = flag.String("logdir", "", "Absolute path to directory for logs") logdir = flag.String("logdir", "", "Absolute path to directory for logs")
statedir = flag.String("statedir", "", "Absolute path to directory for states") statedir = flag.String("statedir", "", "Absolute path to directory for states")
passwords = flag.String("passwords", "", "Optional path to passwords file") passwords = flag.String("passwords", "", "Optional path to passwords file")
tlsBind = flag.String("tlsbind", "", "TLS address to bind to")
tlsBind = flag.String("tlsbind", "", "TLS address to bind to") tlsPEM = flag.String("tlspem", "", "Path to TLS certificat+key PEM file")
tlsPEM = flag.String("tlspem", "", "Path to TLS certificat+key PEM file") verbose = flag.Bool("v", false, "Enable verbose logging.")
verbose = flag.Bool("v", false, "Enable verbose logging.")
) )
func listenerLoop(sock net.Listener, events chan<- ClientEvent) { func listenerLoop(sock net.Listener, events chan ClientEvent) {
for { for {
conn, err := sock.Accept() conn, err := sock.Accept()
if err != nil { if err != nil {
log.Println("Error during accepting connection", err) log.Println("Error during accepting connection", err)
continue continue
} }
client := NewClient(hostname, conn) client := NewClient(conn)
go client.Processor(events) go client.Processor(events)
} }
} }
@ -60,7 +58,6 @@ func Run() {
events := make(chan ClientEvent) events := make(chan ClientEvent)
log.SetFlags(log.Ldate | log.Lmicroseconds | log.Lshortfile) log.SetFlags(log.Ldate | log.Lmicroseconds | log.Lshortfile)
logSink := make(chan LogEvent)
if *logdir == "" { if *logdir == "" {
// Dummy logger // Dummy logger
go func() { go func() {
@ -75,10 +72,7 @@ func Run() {
log.Println(*logdir, "logger initialized") log.Println(*logdir, "logger initialized")
} }
stateSink := make(chan StateEvent) log.Println("goircd " + version + " is starting")
daemon := NewDaemon(version, hostname, motd, passwords, logSink, stateSink)
daemon.Verbose = *verbose
log.Println("goircd " + daemon.version + " is starting")
if *statedir == "" { if *statedir == "" {
// Dummy statekeeper // Dummy statekeeper
go func() { go func() {
@ -98,14 +92,14 @@ func Run() {
if err != nil { if err != nil {
log.Fatalf("Can not read state %s: %v", state, err) log.Fatalf("Can not read state %s: %v", state, err)
} }
room, _ := daemon.RoomRegister(path.Base(state)) room, _ := RoomRegister(path.Base(state))
contents := strings.Split(string(buf), "\n") contents := strings.Split(string(buf), "\n")
if len(contents) < 2 { if len(contents) < 2 {
log.Printf("State corrupted for %s: %q", room.name, contents) log.Printf("State corrupted for %s: %q", *room.name, contents)
} else { } else {
room.topic = contents[0] room.topic = &contents[0]
room.key = contents[1] room.key = &contents[1]
log.Println("Loaded state for room", room.name) log.Println("Loaded state for room", *room.name)
} }
} }
go StateKeeper(*statedir, stateSink) go StateKeeper(*statedir, stateSink)
@ -133,8 +127,7 @@ func Run() {
log.Println("TLS listening on", *tlsBind) log.Println("TLS listening on", *tlsBind)
go listenerLoop(listenerTLS, events) go listenerLoop(listenerTLS, events)
} }
Processor(events, make(chan struct{}))
daemon.Processor(events)
} }
func main() { func main() {

144
room.go
View File

@ -28,6 +28,10 @@ import (
var ( var (
RERoom = regexp.MustCompile("^#[^\x00\x07\x0a\x0d ,:/]{1,200}$") RERoom = regexp.MustCompile("^#[^\x00\x07\x0a\x0d ,:/]{1,200}$")
rooms map[string]*Room = make(map[string]*Room)
roomSinks map[*Room]chan ClientEvent = make(map[*Room]chan ClientEvent)
) )
// Sanitize room's name. It can consist of 1 to 50 ASCII symbols // Sanitize room's name. It can consist of 1 to 50 ASCII symbols
@ -37,40 +41,34 @@ func RoomNameValid(name string) bool {
} }
type Room struct { type Room struct {
Verbose bool name *string
name string topic *string
topic string key *string
key string members map[*Client]struct{}
members map[*Client]bool
hostname *string
logSink chan<- LogEvent
stateSink chan<- StateEvent
} }
func (room Room) String() string { func (room Room) String() string {
return room.name return *room.name
} }
func NewRoom(hostname *string, name string, logSink chan<- LogEvent, stateSink chan<- StateEvent) *Room { func NewRoom(name string) *Room {
room := Room{name: name} topic := ""
room.members = make(map[*Client]bool) return &Room{
room.topic = "" name: &name,
room.key = "" topic: &topic,
room.hostname = hostname members: make(map[*Client]struct{}),
room.logSink = logSink
room.stateSink = stateSink
return &room
}
func (room *Room) SendTopic(client *Client) {
if room.topic == "" {
client.ReplyNicknamed("331", room.name, "No topic is set")
} else {
client.ReplyNicknamed("332", room.name, room.topic)
} }
} }
// Send message to all room's subscribers, possibly excluding someone func (room *Room) SendTopic(client *Client) {
if *room.topic == "" {
client.ReplyNicknamed("331", *room.name, "No topic is set")
} else {
client.ReplyNicknamed("332", *room.name, *room.topic)
}
}
// Send message to all room's subscribers, possibly excluding someone.
func (room *Room) Broadcast(msg string, clientToIgnore ...*Client) { func (room *Room) Broadcast(msg string, clientToIgnore ...*Client) {
for member := range room.members { for member := range room.members {
if (len(clientToIgnore) > 0) && member == clientToIgnore[0] { if (len(clientToIgnore) > 0) && member == clientToIgnore[0] {
@ -81,7 +79,11 @@ func (room *Room) Broadcast(msg string, clientToIgnore ...*Client) {
} }
func (room *Room) StateSave() { func (room *Room) StateSave() {
room.stateSink <- StateEvent{room.name, room.topic, room.key} var key string
if room.key != nil {
key = *room.key
}
stateSink <- StateEvent{*room.name, *room.topic, key}
} }
func (room *Room) Processor(events <-chan ClientEvent) { func (room *Room) Processor(events <-chan ClientEvent) {
@ -89,46 +91,50 @@ func (room *Room) Processor(events <-chan ClientEvent) {
for event := range events { for event := range events {
client = event.client client = event.client
switch event.eventType { switch event.eventType {
case EventTerm:
roomsGroup.Done()
return
case EventNew: case EventNew:
room.members[client] = true room.members[client] = struct{}{}
if room.Verbose { if *verbose {
log.Println(client, "joined", room.name) log.Println(client, "joined", room.name)
} }
room.SendTopic(client) room.SendTopic(client)
room.Broadcast(fmt.Sprintf(":%s JOIN %s", client, room.name)) room.Broadcast(fmt.Sprintf(":%s JOIN %s", client, *room.name))
room.logSink <- LogEvent{room.name, client.nickname, "joined", true} logSink <- LogEvent{*room.name, *client.nickname, "joined", true}
nicknames := []string{} nicknames := make([]string, 0)
for member := range room.members { for member := range room.members {
nicknames = append(nicknames, member.nickname) nicknames = append(nicknames, *member.nickname)
} }
sort.Strings(nicknames) sort.Strings(nicknames)
client.ReplyNicknamed("353", "=", room.name, strings.Join(nicknames, " ")) client.ReplyNicknamed("353", "=", *room.name, strings.Join(nicknames, " "))
client.ReplyNicknamed("366", room.name, "End of NAMES list") client.ReplyNicknamed("366", *room.name, "End of NAMES list")
case EventDel: case EventDel:
if _, subscribed := room.members[client]; !subscribed { if _, subscribed := room.members[client]; !subscribed {
client.ReplyNicknamed("442", room.name, "You are not on that channel") client.ReplyNicknamed("442", *room.name, "You are not on that channel")
continue continue
} }
delete(room.members, client) delete(room.members, client)
msg := fmt.Sprintf(":%s PART %s :%s", client, room.name, client.nickname) msg := fmt.Sprintf(":%s PART %s :%s", client, *room.name, *client.nickname)
room.Broadcast(msg) room.Broadcast(msg)
room.logSink <- LogEvent{room.name, client.nickname, "left", true} logSink <- LogEvent{*room.name, *client.nickname, "left", true}
case EventTopic: case EventTopic:
if _, subscribed := room.members[client]; !subscribed { if _, subscribed := room.members[client]; !subscribed {
client.ReplyParts("442", room.name, "You are not on that channel") client.ReplyParts("442", *room.name, "You are not on that channel")
continue continue
} }
if event.text == "" { if event.text == "" {
go room.SendTopic(client) room.SendTopic(client)
continue continue
} }
room.topic = strings.TrimLeft(event.text, ":") topic := strings.TrimLeft(event.text, ":")
msg := fmt.Sprintf(":%s TOPIC %s :%s", client, room.name, room.topic) room.topic = &topic
go room.Broadcast(msg) msg := fmt.Sprintf(":%s TOPIC %s :%s", client, *room.name, *room.topic)
room.logSink <- LogEvent{ room.Broadcast(msg)
room.name, logSink <- LogEvent{
client.nickname, *room.name,
"set topic to " + room.topic, *client.nickname,
"set topic to " + *room.topic,
true, true,
} }
room.StateSave() room.StateSave()
@ -136,32 +142,32 @@ func (room *Room) Processor(events <-chan ClientEvent) {
for m := range room.members { for m := range room.members {
client.ReplyNicknamed( client.ReplyNicknamed(
"352", "352",
room.name, *room.name,
m.username, *m.username,
m.conn.RemoteAddr().String(), m.conn.RemoteAddr().String(),
*room.hostname, *hostname,
m.nickname, *m.nickname,
"H", "H",
"0 "+m.realname, "0 "+*m.realname,
) )
} }
client.ReplyNicknamed("315", room.name, "End of /WHO list") client.ReplyNicknamed("315", *room.name, "End of /WHO list")
case EventMode: case EventMode:
if event.text == "" { if event.text == "" {
mode := "+" mode := "+"
if room.key != "" { if room.key != nil {
mode = mode + "k" mode = mode + "k"
} }
client.Msg(fmt.Sprintf("324 %s %s %s", client.nickname, room.name, mode)) client.Msg(fmt.Sprintf("324 %s %s %s", *client.nickname, *room.name, mode))
continue continue
} }
if strings.HasPrefix(event.text, "b") { if strings.HasPrefix(event.text, "b") {
client.ReplyNicknamed("368", room.name, "End of channel ban list") client.ReplyNicknamed("368", *room.name, "End of channel ban list")
continue continue
} }
if strings.HasPrefix(event.text, "-k") || strings.HasPrefix(event.text, "+k") { if strings.HasPrefix(event.text, "-k") || strings.HasPrefix(event.text, "+k") {
if _, subscribed := room.members[client]; !subscribed { if _, subscribed := room.members[client]; !subscribed {
client.ReplyParts("442", room.name, "You are not on that channel") client.ReplyParts("442", *room.name, "You are not on that channel")
continue continue
} }
} else { } else {
@ -176,16 +182,16 @@ func (room *Room) Processor(events <-chan ClientEvent) {
client.ReplyNotEnoughParameters("MODE") client.ReplyNotEnoughParameters("MODE")
continue continue
} }
room.key = cols[1] room.key = &cols[1]
msg = fmt.Sprintf(":%s MODE %s +k %s", client, room.name, room.key) msg = fmt.Sprintf(":%s MODE %s +k %s", client, *room.name, *room.key)
msgLog = "set channel key to " + room.key msgLog = "set channel key to " + *room.key
} else if strings.HasPrefix(event.text, "-k") { } else {
room.key = "" room.key = nil
msg = fmt.Sprintf(":%s MODE %s -k", client, room.name) msg = fmt.Sprintf(":%s MODE %s -k", client, *room.name)
msgLog = "removed channel key" msgLog = "removed channel key"
} }
go room.Broadcast(msg) room.Broadcast(msg)
room.logSink <- LogEvent{room.name, client.nickname, msgLog, true} logSink <- LogEvent{*room.name, *client.nickname, msgLog, true}
room.StateSave() room.StateSave()
case EventMsg: case EventMsg:
sep := strings.Index(event.text, " ") sep := strings.Index(event.text, " ")
@ -193,13 +199,13 @@ func (room *Room) Processor(events <-chan ClientEvent) {
":%s %s %s :%s", ":%s %s %s :%s",
client, client,
event.text[:sep], event.text[:sep],
room.name, *room.name,
event.text[sep+1:]), event.text[sep+1:]),
client, client,
) )
room.logSink <- LogEvent{ logSink <- LogEvent{
room.name, *room.name,
client.nickname, *client.nickname,
event.text[sep+1:], event.text[sep+1:],
false, false,
} }

View File

@ -42,17 +42,25 @@ func notEnoughParams(t *testing.T, c *TestingConn) {
} }
func TestTwoUsers(t *testing.T) { func TestTwoUsers(t *testing.T) {
logSink := make(chan LogEvent, 8) logSink = make(chan LogEvent, 8)
stateSink := make(chan StateEvent, 8) stateSink = make(chan StateEvent, 8)
host := "foohost" host := "foohost"
daemon := NewDaemon("ver1", &host, nil, nil, logSink, stateSink) hostname = &host
events := make(chan ClientEvent) events := make(chan ClientEvent)
go daemon.Processor(events) rooms = make(map[string]*Room)
clients = make(map[*Client]struct{})
roomSinks = make(map[*Room]chan ClientEvent)
finished := make(chan struct{})
go Processor(events, finished)
defer func() {
events <- ClientEvent{eventType: EventTerm}
<-finished
}()
conn1 := NewTestingConn() conn1 := NewTestingConn()
conn2 := NewTestingConn() conn2 := NewTestingConn()
client1 := NewClient(&host, conn1) client1 := NewClient(conn1)
client2 := NewClient(&host, conn2) client2 := NewClient(conn2)
go client1.Processor(events) go client1.Processor(events)
go client2.Processor(events) go client2.Processor(events)
@ -63,7 +71,7 @@ func TestTwoUsers(t *testing.T) {
<-conn2.outbound <-conn2.outbound
} }
daemon.SendLusers(client1) SendLusers(client1)
if r := <-conn1.outbound; !strings.Contains(r, "There are 2 users") { if r := <-conn1.outbound; !strings.Contains(r, "There are 2 users") {
t.Fatal("LUSERS", r) t.Fatal("LUSERS", r)
} }
@ -105,27 +113,40 @@ func TestTwoUsers(t *testing.T) {
conn1.inbound <- "PRIVMSG nick2 :Hello" conn1.inbound <- "PRIVMSG nick2 :Hello"
conn1.inbound <- "PRIVMSG #foo :world" conn1.inbound <- "PRIVMSG #foo :world"
conn1.inbound <- "NOTICE #foo :world" conn1.inbound <- "NOTICE #foo :world"
<-conn2.outbound m1 := <-conn2.outbound
if r := <-conn2.outbound; r != ":nick1!foo1@someclient PRIVMSG nick2 :Hello\r\n" { m2 := <-conn2.outbound
t.Fatal("first message", r) mNeeded := ":nick1!foo1@someclient PRIVMSG nick2 :Hello\r\n"
if !(m1 == mNeeded || m2 == mNeeded) {
t.Fatal("first message", m1, m2)
} }
if r := <-conn2.outbound; r != ":nick1!foo1@someclient PRIVMSG #foo :world\r\n" { if m2 == mNeeded {
t.Fatal("second message", r) m2 = <-conn2.outbound
} }
if r := <-conn2.outbound; r != ":nick1!foo1@someclient NOTICE #foo :world\r\n" { if m2 != ":nick1!foo1@someclient PRIVMSG #foo :world\r\n" {
t.Fatal("third message", r) t.Fatal("second message", m2)
}
if m2 = <-conn2.outbound; m2 != ":nick1!foo1@someclient NOTICE #foo :world\r\n" {
t.Fatal("third message", m2)
} }
} }
func TestJoin(t *testing.T) { func TestJoin(t *testing.T) {
logSink := make(chan LogEvent, 8) logSink = make(chan LogEvent, 8)
stateSink := make(chan StateEvent, 8) stateSink = make(chan StateEvent, 8)
host := "foohost" host := "foohost"
daemon := NewDaemon("ver1", &host, nil, nil, logSink, stateSink) hostname = &host
events := make(chan ClientEvent) events := make(chan ClientEvent)
go daemon.Processor(events) rooms = make(map[string]*Room)
clients = make(map[*Client]struct{})
roomSinks = make(map[*Room]chan ClientEvent)
finished := make(chan struct{})
go Processor(events, finished)
defer func() {
events <- ClientEvent{eventType: EventTerm}
<-finished
}()
conn := NewTestingConn() conn := NewTestingConn()
client := NewClient(&host, conn) client := NewClient(conn)
go client.Processor(events) go client.Processor(events)
conn.inbound <- "NICK nick2\r\nUSER foo2 bar2 baz2 :Long name2" conn.inbound <- "NICK nick2\r\nUSER foo2 bar2 baz2 :Long name2"
@ -161,10 +182,10 @@ func TestJoin(t *testing.T) {
for i := 0; i < 4*2; i++ { for i := 0; i < 4*2; i++ {
<-conn.outbound <-conn.outbound
} }
if _, ok := daemon.rooms["#bar"]; !ok { if _, ok := rooms["#bar"]; !ok {
t.Fatal("#bar does not exist") t.Fatal("#bar does not exist")
} }
if _, ok := daemon.rooms["#baz"]; !ok { if _, ok := rooms["#baz"]; !ok {
t.Fatal("#baz does not exist") t.Fatal("#baz does not exist")
} }
if r := <-logSink; (r.what != "joined") || (r.where != "#bar") || (r.who != "nick2") || (r.meta != true) { if r := <-logSink; (r.what != "joined") || (r.where != "#bar") || (r.who != "nick2") || (r.meta != true) {
@ -178,10 +199,10 @@ func TestJoin(t *testing.T) {
for i := 0; i < 4*2; i++ { for i := 0; i < 4*2; i++ {
<-conn.outbound <-conn.outbound
} }
if daemon.rooms["#barenc"].key != "key1" { if *rooms["#barenc"].key != "key1" {
t.Fatal("no room with key1") t.Fatal("no room with key1")
} }
if daemon.rooms["#bazenc"].key != "key2" { if *rooms["#bazenc"].key != "key2" {
t.Fatal("no room with key2") t.Fatal("no room with key2")
} }
if r := <-logSink; (r.what != "joined") || (r.where != "#barenc") || (r.who != "nick2") || (r.meta != true) { if r := <-logSink; (r.what != "joined") || (r.where != "#barenc") || (r.who != "nick2") || (r.meta != true) {
@ -201,7 +222,7 @@ func TestJoin(t *testing.T) {
if r := <-conn.outbound; r != ":nick2!foo2@someclient MODE #barenc -k\r\n" { if r := <-conn.outbound; r != ":nick2!foo2@someclient MODE #barenc -k\r\n" {
t.Fatal("remove #barenc key", r) t.Fatal("remove #barenc key", r)
} }
if daemon.rooms["#barenc"].key != "" { if rooms["#barenc"].key != nil {
t.Fatal("removing key from #barenc") t.Fatal("removing key from #barenc")
} }
if r := <-logSink; (r.what != "removed channel key") || (r.where != "#barenc") || (r.who != "nick2") || (r.meta != true) { if r := <-logSink; (r.what != "removed channel key") || (r.where != "#barenc") || (r.who != "nick2") || (r.meta != true) {
@ -253,5 +274,4 @@ func TestJoin(t *testing.T) {
if r := <-conn.outbound; r != ":foohost 315 nick2 #barenc :End of /WHO list\r\n" { if r := <-conn.outbound; r != ":foohost 315 nick2 #barenc :End of /WHO list\r\n" {
t.Fatal("end of WHO", r) t.Fatal("end of WHO", r)
} }
} }