Many fixes and additions

* NAMES and WALLOPS command
* -cloak option
* -v renamed to -verbose
* -passwords renamed to -passwd
* -debug option prints traffic messages
* without -verbose only startup/shutdown and error messages are printed
* -timestamped option provides timestamps in printed messages, as
  earlier. No timestamps is useful for running under daemontools
* passwords are stored in SHA256-hashed format
* state files replaced with state directory with files
* removed many unnecessary pointers and locks
* graceful shutdown with all clients notification
* fixed time structure printing in log files, instead of short human
  readable timestamp
* PART is sent to the user itself, to notify his client about leaving
* log messages are printed to stdout, instead of stderr, for
  friendliness with daemontools logger
* ability to configure newly created directories and files with
  -perm-state-dir, -perm-state-file, -perm-log-file
This commit is contained in:
Sergey Matveev 2020-11-06 20:10:06 +03:00
parent def58d0f49
commit b7fb219307
13 changed files with 998 additions and 872 deletions

View File

@ -1,8 +1,9 @@
goircd requires only standard Go's libraries and consists of single main goircd requires only standard Go's libraries and consists of single main
package. You can install it like either: package. You can install it like that:
* with: go get go.cypherpunks.ru/goircd $ go get go.cypherpunks.ru/goircd
* or manually:
or manually:
$ git clone git://git.cypherpunks.ru/goircd.git $ git clone git://git.cypherpunks.ru/goircd.git
$ cd goircd $ cd goircd

62
README
View File

@ -10,7 +10,7 @@ It does not aim to replace full featured mass scalable IRC networks:
* It can not connect to other servers. Just standalone installation * It can not connect to other servers. Just standalone installation
* It has few basic IRC commands * It has few basic IRC commands
* There is no support for channel operators, modes, votes, invites * There is no support for channel operators, many modes, votes, invites
* No ident lookups * No ident lookups
But it has some convincing features: But it has some convincing features:
@ -19,8 +19,8 @@ But it has some convincing features:
* Single executable binary * Single executable binary
* No configuration file, just few command line arguments * No configuration file, just few command line arguments
* IPv6 out-of-box support * IPv6 out-of-box support
* Ability to listen on TLS-capable ports * Ability to additionally listen on TLS-capable ports
* Optional channel logging to plain text files * Optional channels logging to plain text files
* Optional permanent channel's state saving in plain text files * Optional permanent channel's state saving in plain text files
(so you can reload daemon and all channels topics and keys won't (so you can reload daemon and all channels topics and keys won't
disappear) disappear)
@ -28,35 +28,42 @@ But it has some convincing features:
Some remarks and recommendations related to it's simplicity: Some remarks and recommendations related to it's simplicity:
* Use either nohup or similar tools to daemonize it * Use daemontools to daemonize, setuid/gid it
* Just plain logging on stderr, without syslog support * Just plaintext logging to stdout, without syslog support -- use
daemontool's multilog
SUPPORTED IRC COMMANDS SUPPORTED IRC COMMANDS
* PASS/NICK/USER during registration workflow * PASS/NICK/USER during registration workflow
* PING/PONGs * PING/PONGs
* NOTICE/PRIVMSG, ISON * NOTICE/PRIVMSG, ISON
* AWAY, MOTD, LUSERS, WHO, WHOIS, VERSION, QUIT * AWAY, MOTD, LUSERS, NAMES, WHO, WHOIS, VERSION, WALLOPS, QUIT
* LIST, JOIN, TOPIC, +k/-k channel MODE * LIST, JOIN, TOPIC, +k/-k channel MODE
USAGE USAGE
Just execute goircd daemon. It has following optional arguments: Just execute goircd daemon. It has following optional arguments:
-hostname: hostname to show for client's connections -hostname: hostname to show for client's connections
-bind: address to bind to (:6667 by default) -bind: address to bind to (:6667 by default)
-motd: absolute path to MOTD file. It is reread every time -cloak: cloak user's host with the given hostname
MOTD is requested -motd: absolute path to MOTD file. It is reread every time
-logdir: directory where all channels messages will be saved. If MOTD is requested
omitted, then no logs will be kept -logdir: directory where all channels messages will be saved. If
-statedir: directory where all channels states will be saved and omitted, then no logs will be kept
loaded during startup. If omitted, then states will be -statedir: directory where all channels states will be saved and
lost after daemon termination loaded during startup. If omitted, then states will be
-tlsbind: enable TLS, specify address to listen on and path lost after daemon termination
-tlspem to PEM file with certificate and private key -tlsbind: enable TLS, specify address to listen on and path
-passwords: enable client authentication and specify path to -tlspem: to PEM file with certificate and private key
passwords file -passwd: enable client authentication and specify path to
-v: increase verbosity passwords file
-timestamped: enabled timestamps for stderr messages
-verbose: increase verbosity
-debug: also show traffic messages
-perm-state-dir: permission (before umask) for newly created state directory
-perm-state-file: permission (before umask) for newly created state file
-perm-log-file: permission (before umask) for newly created log file
TLS TLS
@ -69,10 +76,12 @@ AUTHENTICATION
You can turn on optional client authentication by preparing passwords You can turn on optional client authentication by preparing passwords
file and using the -passwords argument. Format of passwords file is: file and using the -passwords argument. Format of passwords file is:
login1:password1\n login1:hex(sha256(password1))\n
login2:password2\n login2:hex(sha256(password2))\n
... ...
You can get hashed password value using: echo -n password | sha256
LOG FILES LOG FILES
Log files are not opened all the time, but only during each message Log files are not opened all the time, but only during each message
@ -80,13 +89,8 @@ saving. That is why you can safely rename them for rotation purposes.
STATE FILES STATE FILES
Each state file has the name equals to room's one. It contains two plain Room's state is created/saved when either topic or key is set. State is
text lines: room's topic and room's authentication key (empty if none a directory (room's name) with "topic" and "key" plaintext files.
specified). For example:
$ cat states/meinroom
This is meinroom's topic
secretkey
LICENCE LICENCE

1
clean.do Normal file
View File

@ -0,0 +1 @@
rm -f goircd

361
client.go
View File

@ -19,8 +19,15 @@ package main
import ( import (
"bytes" "bytes"
"crypto/sha256"
"crypto/subtle"
"encoding/hex"
"fmt"
"io/ioutil"
"log" "log"
"net" "net"
"regexp"
"sort"
"strings" "strings"
"sync" "sync"
"time" "time"
@ -28,29 +35,37 @@ import (
const ( const (
BufSize = 1500 BufSize = 1500
MaxOutBuf = 1 << 12 MaxOutBuf = 128
) )
var ( var (
CRLF []byte = []byte{'\x0d', '\x0a'} CRLF []byte = []byte{'\x0d', '\x0a'}
RENickname = regexp.MustCompile("^[a-zA-Z0-9-]{1,24}$")
clients map[*Client]struct{} = make(map[*Client]struct{})
clientsLock sync.RWMutex
clientsWG sync.WaitGroup
) )
type Client struct { type Client struct {
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 recvTimestamp time.Time
sendTimestamp time.Time sendTimestamp time.Time
outBuf chan *string outBuf chan string
alive bool alive bool
sync.Mutex sync.Mutex
} }
func (c *Client) Host() string { func (c *Client) Host() string {
if *cloak != "" {
return *cloak
}
addr := c.conn.RemoteAddr().String() addr := c.conn.RemoteAddr().String()
if host, _, err := net.SplitHostPort(addr); err == nil { if host, _, err := net.SplitHostPort(addr); err == nil {
addr = host addr = host
@ -62,48 +77,42 @@ func (c *Client) Host() string {
} }
func (c *Client) String() string { func (c *Client) String() string {
return *c.nickname + "!" + *c.username + "@" + c.Host() return strings.Join([]string{c.nickname, "!", c.username, "@", c.Host()}, "")
} }
func NewClient(conn net.Conn) *Client { func NewClient(conn net.Conn, events chan ClientEvent) *Client {
nickname := "*"
username := ""
c := Client{ c := Client{
conn: conn, conn: conn,
nickname: &nickname, nickname: "*",
username: &username, username: "",
recvTimestamp: time.Now(), recvTimestamp: time.Now(),
sendTimestamp: time.Now(), sendTimestamp: time.Now(),
alive: true, alive: true,
outBuf: make(chan *string, MaxOutBuf), outBuf: make(chan string, MaxOutBuf),
} }
clientsWG.Add(2)
go c.MsgSender() go c.MsgSender()
go c.Processor(events)
return &c return &c
} }
func (c *Client) SetDead() {
c.outBuf <- nil
c.alive = false
}
func (c *Client) Close() { func (c *Client) Close() {
c.Lock() c.Lock()
if c.alive { if c.alive {
c.SetDead() close(c.outBuf)
c.alive = false
} }
c.Unlock() c.Unlock()
} }
// Client processor blockingly reads everything remote client sends, func (c *Client) Processor(events chan ClientEvent) {
// splits messages by CRLF and send them to Daemon gorouting for processing events <- ClientEvent{c, EventNew, ""}
// it futher. Also it can signalize that client is unavailable (disconnected). if *verbose {
func (c *Client) Processor(sink chan ClientEvent) { log.Println(c, "connected")
sink <- ClientEvent{c, EventNew, ""} }
log.Println(c, "New client")
buf := make([]byte, BufSize*2) buf := make([]byte, BufSize*2)
var n int var n, prev, i int
var prev int var msg string
var i int
var err error var err error
for { for {
if prev == BufSize { if prev == BufSize {
@ -120,26 +129,43 @@ func (c *Client) Processor(sink chan ClientEvent) {
if i == -1 { if i == -1 {
continue continue
} }
sink <- ClientEvent{c, EventMsg, string(buf[:i])} if *debug {
log.Println(c, "<-", msg)
}
msg = string(buf[:i])
if *debug {
log.Println(c, "->", msg)
}
events <- ClientEvent{c, EventMsg, msg}
copy(buf, buf[i+2:prev]) copy(buf, buf[i+2:prev])
prev -= (i + 2) prev -= (i + 2)
goto CheckMore goto CheckMore
} }
c.Close() c.Close()
sink <- ClientEvent{c, EventDel, ""} if *verbose {
log.Println(c, "disconnected")
}
events <- ClientEvent{c, EventDel, ""}
clientsWG.Done()
} }
func (c *Client) MsgSender() { func (c *Client) MsgSender() {
var err error
for msg := range c.outBuf { for msg := range c.outBuf {
if msg == nil { if *debug {
c.conn.Close() log.Println(c, "<-", msg)
}
if _, err = c.conn.Write(append([]byte(msg), CRLF...)); err != nil {
if *verbose {
log.Println(c, "error writing", err)
}
break break
} }
c.conn.Write(append([]byte(*msg), CRLF...))
} }
c.conn.Close()
clientsWG.Done()
} }
// Send message as is with CRLF appended.
func (c *Client) Msg(text string) { func (c *Client) Msg(text string) {
c.Lock() c.Lock()
defer c.Unlock() defer c.Unlock()
@ -149,20 +175,18 @@ func (c *Client) Msg(text string) {
if len(c.outBuf) == MaxOutBuf { if len(c.outBuf) == MaxOutBuf {
log.Println(c, "output buffer size exceeded, kicking him") log.Println(c, "output buffer size exceeded, kicking him")
if c.alive { if c.alive {
c.SetDead() close(c.outBuf)
c.alive = false
} }
return return
} }
c.outBuf <- &text c.outBuf <- text
} }
// Send message from server. It has ": servername" prefix.
func (c *Client) Reply(text string) { func (c *Client) Reply(text string) {
c.Msg(":" + *hostname + " " + text) c.Msg(":" + *hostname + " " + text)
} }
// Send server message, concatenating all provided text parts and
// prefix the last one with ":".
func (c *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 {
@ -172,18 +196,14 @@ func (c *Client) ReplyParts(code string, text ...string) {
c.Reply(strings.Join(parts, " ")) c.Reply(strings.Join(parts, " "))
} }
// Send nicknamed server message. After servername it always has target
// client's nickname. The last part is prefixed with ":".
func (c *Client) ReplyNicknamed(code string, text ...string) { func (c *Client) ReplyNicknamed(code string, text ...string) {
c.ReplyParts(code, append([]string{*c.nickname}, text...)...) c.ReplyParts(code, append([]string{c.nickname}, text...)...)
} }
// Reply "461 not enough parameters" error for given command.
func (c *Client) ReplyNotEnoughParameters(command string) { func (c *Client) ReplyNotEnoughParameters(command string) {
c.ReplyNicknamed("461", command, "Not enough parameters") c.ReplyNicknamed("461", command, "Not enough parameters")
} }
// Reply "403 no such channel" error for specified channel.
func (c *Client) ReplyNoChannel(channel string) { func (c *Client) ReplyNoChannel(channel string) {
c.ReplyNicknamed("403", channel, "No such channel") c.ReplyNicknamed("403", channel, "No such channel")
} }
@ -191,3 +211,252 @@ func (c *Client) ReplyNoChannel(channel string) {
func (c *Client) ReplyNoNickChan(channel string) { func (c *Client) ReplyNoNickChan(channel string) {
c.ReplyNicknamed("401", channel, "No such nick/channel") c.ReplyNicknamed("401", channel, "No such nick/channel")
} }
func (c *Client) SendLusers() {
lusers := 0
clientsLock.RLock()
for client := range clients {
if client.registered {
lusers++
}
}
clientsLock.RUnlock()
c.ReplyNicknamed(
"251",
fmt.Sprintf("There are %d users and 0 invisible on 1 servers",
lusers,
))
}
func (c *Client) SendMotd() {
if motd == nil {
c.ReplyNicknamed("422", "MOTD File is missing")
return
}
motdText, err := ioutil.ReadFile(*motd)
if err != nil {
log.Printf("can not read motd file %s: %v", *motd, err)
c.ReplyNicknamed("422", "Error reading MOTD File")
return
}
c.ReplyNicknamed("375", "- "+*hostname+" Message of the day -")
for _, s := range strings.Split(strings.TrimSuffix(string(motdText), "\n"), "\n") {
c.ReplyNicknamed("372", "- "+s)
}
c.ReplyNicknamed("376", "End of /MOTD command")
}
func (c *Client) Join(cmd string) {
args := strings.Split(cmd, " ")
rs := strings.Split(args[0], ",")
keys := []string{}
if len(args) > 1 {
keys = strings.Split(args[1], ",")
}
RoomCycle:
for n, roomName := range rs {
if !RERoom.MatchString(roomName) {
c.ReplyNoChannel(roomName)
continue
}
var key string
if (n < len(keys)) && (keys[n] != "") {
key = keys[n]
}
roomsLock.RLock()
for roomNameExisting, room := range rooms {
if roomName != roomNameExisting {
continue
}
if (room.key != "") && (room.key != key) {
c.ReplyNicknamed("475", roomName, "Cannot join channel (+k)")
roomsLock.RUnlock()
return
}
room.events <- ClientEvent{c, EventNew, ""}
roomsLock.RUnlock()
continue RoomCycle
}
roomsLock.RUnlock()
roomNew := RoomRegister(roomName)
if *verbose {
log.Println("room", roomName, "created")
}
if key != "" {
roomNew.key = key
roomNew.StateSave()
}
roomNew.events <- ClientEvent{c, EventNew, ""}
}
}
func (client *Client) SendWhois(nicknames []string) {
var c *Client
for _, nickname := range nicknames {
nickname = strings.ToLower(nickname)
clientsLock.RLock()
for c = range clients {
if strings.ToLower(c.nickname) == nickname {
clientsLock.RUnlock()
goto Found
}
}
clientsLock.RUnlock()
client.ReplyNoNickChan(nickname)
continue
Found:
var host string
if *cloak != "" {
host = *cloak
} else {
host, _, err := net.SplitHostPort(c.conn.RemoteAddr().String())
if err != nil {
log.Printf("can't parse RemoteAddr %q: %v", host, err)
host = "Unknown"
}
}
client.ReplyNicknamed("311", c.nickname, c.username, host, "*", c.realname)
client.ReplyNicknamed("312", c.nickname, *hostname, *hostname)
if c.away != "" {
client.ReplyNicknamed("301", c.nickname, c.away)
}
subscriptions := make([]string, 0)
roomsLock.RLock()
for _, room := range rooms {
for subscriber := range room.members {
if subscriber.nickname == nickname {
subscriptions = append(subscriptions, room.name)
}
}
}
roomsLock.RUnlock()
sort.Strings(subscriptions)
client.ReplyNicknamed("319", c.nickname, strings.Join(subscriptions, " "))
client.ReplyNicknamed("318", c.nickname, "End of /WHOIS list")
}
}
func (c *Client) SendList(cols []string) {
var rs []string
if (len(cols) > 1) && (cols[1] != "") {
rs = strings.Split(strings.Split(cols[1], " ")[0], ",")
} else {
rs = make([]string, 0)
roomsLock.RLock()
for r := range rooms {
rs = append(rs, r)
}
roomsLock.RUnlock()
}
sort.Strings(rs)
roomsLock.RLock()
for _, r := range rs {
if room, found := rooms[r]; found {
c.ReplyNicknamed(
"322",
r,
fmt.Sprintf("%d", len(room.members)),
room.topic,
)
}
}
roomsLock.RUnlock()
c.ReplyNicknamed("323", "End of /LIST")
}
func (c *Client) Register(cmd string, cols []string) {
switch cmd {
case "PASS":
if len(cols) == 1 || len(cols[1]) < 1 {
c.ReplyNotEnoughParameters("PASS")
return
}
password := strings.TrimPrefix(cols[1], ":")
c.password = password
case "NICK":
if len(cols) == 1 || len(cols[1]) < 1 {
c.ReplyParts("431", "No nickname given")
return
}
nickname := cols[1]
// Compatibility with some clients prepending colons to nickname
nickname = strings.TrimPrefix(nickname, ":")
nickname = strings.ToLower(nickname)
if !RENickname.MatchString(nickname) {
c.ReplyParts("432", "*", cols[1], "Erroneous nickname")
return
}
clientsLock.RLock()
for existingClient := range clients {
if existingClient.nickname == nickname {
clientsLock.RUnlock()
c.ReplyParts("433", "*", nickname, "Nickname is already in use")
return
}
}
clientsLock.RUnlock()
c.nickname = nickname
case "USER":
if len(cols) == 1 {
c.ReplyNotEnoughParameters("USER")
return
}
args := strings.SplitN(cols[1], " ", 4)
if len(args) < 4 {
c.ReplyNotEnoughParameters("USER")
return
}
c.username = args[0]
realname := strings.TrimLeft(args[3], ":")
c.realname = realname
}
if c.nickname != "*" && c.username != "" {
if *passwords != "" {
authenticated := false
if c.password == "" {
c.ReplyParts("462", "You may not register")
c.Close()
return
}
contents, err := ioutil.ReadFile(*passwords)
if err != nil {
log.Fatalf("can not read passwords file %s: %s", *passwords, err)
return
}
for n, entry := range strings.Split(string(contents), "\n") {
if entry == "" || strings.HasPrefix(entry, "#") {
continue
}
cols := strings.Split(entry, ":")
if len(cols) != 2 {
log.Fatalf("bad passwords format: %s:%d", *passwords, n)
continue
}
if cols[0] != c.nickname {
continue
}
h := sha256.Sum256([]byte(c.password))
authenticated = subtle.ConstantTimeCompare(
[]byte(hex.EncodeToString(h[:])),
[]byte(cols[1]),
) == 1
break
}
if !authenticated {
c.ReplyParts("462", "You may not register")
c.Close()
return
}
}
c.registered = true
c.ReplyNicknamed("001", "Hi, welcome to IRC")
c.ReplyNicknamed("002", "Your host is "+*hostname+", running goircd "+Version)
c.ReplyNicknamed("003", "This server was created sometime")
c.ReplyNicknamed("004", *hostname+" goircd o o")
c.SendLusers()
c.SendMotd()
if *verbose {
log.Println(c, "logged in")
}
}
}

View File

@ -21,45 +21,46 @@ import (
"testing" "testing"
) )
// New client creation test. It must send an event about new client,
// 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) events := make(chan ClientEvent)
host := "foohost" host := "foohost"
hostname = &host hostname = &host
client := NewClient(conn) client := NewClient(conn, events)
go client.Processor(sink) defer func() {
client.Close()
}()
event := <-sink event := <-events
if event.eventType != EventNew { if event.eventType != EventNew {
t.Fatal("no NEW event", event) t.Fatal("no NEW event", event)
} }
conn.inbound <- "foo" conn.inbound <- "foo"
event = <-sink event = <-events
if (event.eventType != EventMsg) || (event.text != "foo") { if (event.eventType != EventMsg) || (event.text != "foo") {
t.Fatal("no first MSG", event) t.Fatal("no first MSG", event)
} }
conn.inbound <- "bar" conn.inbound <- "bar"
event = <-sink event = <-events
if (event.eventType != EventMsg) || (event.text != "bar") { if (event.eventType != EventMsg) || (event.text != "bar") {
t.Fatal("no second MSG", event) t.Fatal("no second MSG", event)
} }
conn.inbound <- "" conn.inbound <- ""
event = <-sink event = <-events
if event.eventType != EventDel { if event.eventType != EventDel {
t.Fatal("no client termination", event) t.Fatal("no client termination", event)
} }
} }
// Test replies formatting
func TestClientReplies(t *testing.T) { func TestClientReplies(t *testing.T) {
conn := NewTestingConn() conn := NewTestingConn()
host := "foohost" host := "foohost"
hostname = &host hostname = &host
client := NewClient(conn) client := NewClient(conn, make(chan ClientEvent, 2))
nickname := "мойник" defer func() {
client.nickname = &nickname client.Close()
}()
client.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" {

View File

@ -18,22 +18,23 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
package main package main
import ( import (
"io"
"net" "net"
"time" "time"
) )
// Testing network connection that satisfies net.Conn interface
// Can send predefined messages and store all written ones
type TestingConn struct { type TestingConn struct {
inbound chan string inbound chan string
outbound chan string outbound chan string
closed bool closed chan struct{}
} }
func NewTestingConn() *TestingConn { func NewTestingConn() *TestingConn {
inbound := make(chan string, 8) return &TestingConn{
outbound := make(chan string, 8) inbound: make(chan string, 8),
return &TestingConn{inbound: inbound, outbound: outbound} outbound: make(chan string, 8),
closed: make(chan struct{}),
}
} }
func (conn TestingConn) Error() string { func (conn TestingConn) Error() string {
@ -41,14 +42,18 @@ func (conn TestingConn) Error() string {
} }
func (conn *TestingConn) Read(b []byte) (n int, err error) { func (conn *TestingConn) Read(b []byte) (n int, err error) {
msg := <-conn.inbound select {
if msg == "" { case msg := <-conn.inbound:
return 0, conn if msg == "" {
return 0, conn
}
for n, bt := range append([]byte(msg), CRLF...) {
b[n] = bt
}
return len(msg) + 2, nil
case <-conn.closed:
return 0, io.EOF
} }
for n, bt := range append([]byte(msg), CRLF...) {
b[n] = bt
}
return len(msg) + 2, nil
} }
type MyAddr struct{} type MyAddr struct{}
@ -66,7 +71,7 @@ func (conn *TestingConn) Write(b []byte) (n int, err error) {
} }
func (conn *TestingConn) Close() error { func (conn *TestingConn) Close() error {
conn.closed = true close(conn.closed)
close(conn.outbound) close(conn.outbound)
return nil return nil
} }

543
daemon.go
View File

@ -19,13 +19,8 @@ package main
import ( import (
"fmt" "fmt"
"io/ioutil"
"log" "log"
"net"
"regexp"
"sort"
"strings" "strings"
"sync"
"time" "time"
) )
@ -36,289 +31,26 @@ const (
PingThreshold = time.Second * 90 PingThreshold = time.Second * 90
) )
var (
RENickname = regexp.MustCompile("^[a-zA-Z0-9-]{1,24}$")
clients map[*Client]struct{} = make(map[*Client]struct{})
clientsM sync.RWMutex
rooms map[string]*Room = make(map[string]*Room)
roomsM sync.RWMutex
roomsGroup sync.WaitGroup
roomSinks map[*Room]chan ClientEvent = make(map[*Room]chan ClientEvent)
)
func SendLusers(client *Client) {
lusers := 0
clientsM.RLock()
for client := range clients {
if client.registered {
lusers++
}
}
clientsM.RUnlock()
client.ReplyNicknamed("251", fmt.Sprintf("There are %d users and 0 invisible on 1 servers", lusers))
}
func SendMotd(client *Client) {
if motd == nil {
client.ReplyNicknamed("422", "MOTD File is missing")
return
}
motdText, err := ioutil.ReadFile(*motd)
if err != nil {
log.Printf("Can not read motd file %s: %v", *motd, err)
client.ReplyNicknamed("422", "Error reading MOTD File")
return
}
client.ReplyNicknamed("375", "- "+*hostname+" Message of the day -")
for _, s := range strings.Split(strings.TrimSuffix(string(motdText), "\n"), "\n") {
client.ReplyNicknamed("372", "- "+s)
}
client.ReplyNicknamed("376", "End of /MOTD command")
}
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 {
nickname = strings.ToLower(nickname)
clientsM.RLock()
for c = range clients {
if strings.ToLower(*c.nickname) == nickname {
clientsM.RUnlock()
goto Found
}
}
clientsM.RUnlock()
client.ReplyNoNickChan(nickname)
continue
Found:
hostPort, _, err = net.SplitHostPort(c.conn.RemoteAddr().String())
if err != nil {
log.Printf("Can't parse RemoteAddr %q: %v", hostPort, err)
hostPort = "Unknown"
}
client.ReplyNicknamed("311", *c.nickname, *c.username, hostPort, "*", *c.realname)
client.ReplyNicknamed("312", *c.nickname, *hostname, *hostname)
if c.away != nil {
client.ReplyNicknamed("301", *c.nickname, *c.away)
}
subscriptions = make([]string, 0)
roomsM.RLock()
for _, room = range rooms {
for subscriber = range room.members {
if *subscriber.nickname == nickname {
subscriptions = append(subscriptions, *room.name)
}
}
}
roomsM.RUnlock()
sort.Strings(subscriptions)
client.ReplyNicknamed("319", *c.nickname, strings.Join(subscriptions, " "))
client.ReplyNicknamed("318", *c.nickname, "End of /WHOIS list")
}
}
func SendList(client *Client, cols []string) {
var rs []string
var r string
if (len(cols) > 1) && (cols[1] != "") {
rs = strings.Split(strings.Split(cols[1], " ")[0], ",")
} else {
rs = make([]string, 0)
roomsM.RLock()
for r = range rooms {
rs = append(rs, r)
}
roomsM.RUnlock()
}
sort.Strings(rs)
var room *Room
var found bool
for _, r = range rs {
roomsM.RLock()
if room, found = rooms[r]; found {
client.ReplyNicknamed(
"322",
r,
fmt.Sprintf("%d", len(room.members)),
*room.topic,
)
}
roomsM.RUnlock()
}
client.ReplyNicknamed("323", "End of /LIST")
}
// Unregistered client workflow processor. Unregistered client:
// * is not PINGed
// * only QUIT, NICK and USER commands are processed
// * other commands are quietly ignored
// When client finishes NICK/USER workflow, then MOTD and LUSERS are send to him.
func ClientRegister(client *Client, cmd string, cols []string) {
switch cmd {
case "PASS":
if len(cols) == 1 || len(cols[1]) < 1 {
client.ReplyNotEnoughParameters("PASS")
return
}
password := strings.TrimPrefix(cols[1], ":")
client.password = &password
case "NICK":
if len(cols) == 1 || len(cols[1]) < 1 {
client.ReplyParts("431", "No nickname given")
return
}
nickname := cols[1]
// Compatibility with some clients prepending colons to nickname
nickname = strings.TrimPrefix(nickname, ":")
nickname = strings.ToLower(nickname)
clientsM.RLock()
for existingClient := range clients {
if *existingClient.nickname == nickname {
clientsM.RUnlock()
client.ReplyParts("433", "*", nickname, "Nickname is already in use")
return
}
}
clientsM.RUnlock()
if !RENickname.MatchString(nickname) {
client.ReplyParts("432", "*", cols[1], "Erroneous nickname")
return
}
client.nickname = &nickname
case "USER":
if len(cols) == 1 {
client.ReplyNotEnoughParameters("USER")
return
}
args := strings.SplitN(cols[1], " ", 4)
if len(args) < 4 {
client.ReplyNotEnoughParameters("USER")
return
}
client.username = &args[0]
realname := strings.TrimLeft(args[3], ":")
client.realname = &realname
}
if *client.nickname != "*" && *client.username != "" {
if passwords != nil && *passwords != "" {
if client.password == nil {
client.ReplyParts("462", "You may not register")
client.Close()
return
}
contents, err := ioutil.ReadFile(*passwords)
if err != nil {
log.Fatalf("Can no read passwords file %s: %s", *passwords, err)
return
}
for _, entry := range strings.Split(string(contents), "\n") {
if entry == "" {
continue
}
if lp := strings.Split(entry, ":"); lp[0] == *client.nickname && lp[1] != *client.password {
client.ReplyParts("462", "You may not register")
client.Close()
return
}
}
}
client.registered = true
client.ReplyNicknamed("001", "Hi, welcome to IRC")
client.ReplyNicknamed("002", "Your host is "+*hostname+", running goircd "+Version)
client.ReplyNicknamed("003", "This server was created sometime")
client.ReplyNicknamed("004", *hostname+" goircd o o")
SendLusers(client)
SendMotd(client)
log.Println(client, "logged in")
}
}
// Register new room in Daemon. Create an object, events sink, save pointers
// to corresponding daemon's places and start room's processor goroutine.
func RoomRegister(name string) (*Room, chan ClientEvent) {
roomNew := NewRoom(name)
roomSink := make(chan ClientEvent)
roomsM.Lock()
rooms[name] = roomNew
roomSinks[roomNew] = roomSink
roomsM.Unlock()
go roomNew.Processor(roomSink)
roomsGroup.Add(1)
return roomNew, roomSink
}
func HandlerJoin(client *Client, cmd string) {
args := strings.Split(cmd, " ")
rs := strings.Split(args[0], ",")
var keys []string
if len(args) > 1 {
keys = strings.Split(args[1], ",")
} else {
keys = make([]string, 0)
}
var roomExisting *Room
var roomSink chan ClientEvent
var roomNew *Room
for n, room := range rs {
if !RoomNameValid(room) {
client.ReplyNoChannel(room)
continue
}
var key string
if (n < len(keys)) && (keys[n] != "") {
key = keys[n]
} else {
key = ""
}
roomsM.RLock()
for roomExisting, roomSink = range roomSinks {
if room == *roomExisting.name {
roomsM.RUnlock()
if (*roomExisting.key != "") && (*roomExisting.key != key) {
goto Denied
}
roomSink <- ClientEvent{client, EventNew, ""}
goto Joined
}
}
roomsM.RUnlock()
roomNew, roomSink = RoomRegister(room)
log.Println("Room", roomNew, "created")
if key != "" {
roomNew.key = &key
roomNew.StateSave()
}
roomSink <- ClientEvent{client, EventNew, ""}
continue
Denied:
client.ReplyNicknamed("475", room, "Cannot join channel (+k) - bad key")
Joined:
}
}
func Processor(events chan ClientEvent, finished chan struct{}) { func Processor(events chan ClientEvent, finished chan struct{}) {
var now time.Time var now time.Time
ticker := time.NewTicker(10 * time.Second)
go func() { go func() {
for { for range ticker.C {
time.Sleep(10 * time.Second)
events <- ClientEvent{eventType: EventTick} events <- ClientEvent{eventType: EventTick}
} }
}() }()
for event := range events { EventsCycle:
for e := range events {
now = time.Now() now = time.Now()
client := event.client client := e.client
switch event.eventType { switch e.eventType {
case EventTick: case EventTick:
clientsM.RLock() clientsLock.RLock()
for c := range clients { for c := range clients {
if c.recvTimestamp.Add(PingTimeout).Before(now) { if c.recvTimestamp.Add(PingTimeout).Before(now) {
log.Println(c, "ping timeout") if *verbose {
log.Println(c, "ping timeout")
}
c.Close() c.Close()
continue continue
} }
@ -327,57 +59,55 @@ func Processor(events chan ClientEvent, finished chan struct{}) {
c.Msg("PING :" + *hostname) c.Msg("PING :" + *hostname)
c.sendTimestamp = time.Now() c.sendTimestamp = time.Now()
} else { } else {
log.Println(c, "ping timeout") if *verbose {
log.Println(c, "ping timeout")
}
c.Close() c.Close()
} }
} }
} }
clientsM.RUnlock() clientsLock.RUnlock()
roomsM.Lock() roomsLock.Lock()
for rn, r := range rooms { for rn, r := range rooms {
if *statedir == "" && len(r.members) == 0 { if *statedir == "" && len(r.members) == 0 {
log.Println(rn, "emptied room") if *verbose {
log.Println(rn, "emptied room")
}
delete(rooms, rn) delete(rooms, rn)
close(roomSinks[r]) close(r.events)
delete(roomSinks, r)
} }
} }
roomsM.Unlock() roomsLock.Unlock()
case EventTerm: case EventTerm:
roomsM.RLock() break EventsCycle
for _, sink := range roomSinks {
sink <- ClientEvent{eventType: EventTerm}
}
roomsM.RUnlock()
roomsGroup.Wait()
close(finished)
return
case EventNew: case EventNew:
clientsM.Lock() clientsLock.Lock()
clients[client] = struct{}{} clients[client] = struct{}{}
clientsM.Unlock() clientsLock.Unlock()
case EventDel: case EventDel:
clientsM.Lock() clientsLock.Lock()
delete(clients, client) delete(clients, client)
clientsM.Unlock() clientsLock.Unlock()
roomsM.RLock() roomsLock.RLock()
for _, roomSink := range roomSinks { for _, r := range rooms {
roomSink <- event r.events <- e
} }
roomsM.RUnlock() roomsLock.RUnlock()
case EventMsg: case EventMsg:
cols := strings.SplitN(event.text, " ", 2) cols := strings.SplitN(e.text, " ", 2)
cmd := strings.ToUpper(cols[0]) cmd := strings.ToUpper(cols[0])
if *verbose { if *verbose {
log.Println(client, "command", cmd) log.Println(client, "command", cmd)
} }
if cmd == "QUIT" { if cmd == "QUIT" {
log.Println(client, "quit")
client.Close() client.Close()
if *verbose {
log.Println(client, "quit")
}
continue continue
} }
if !client.registered { if !client.registered {
ClientRegister(client, cmd, cols) client.Register(cmd, cols)
continue continue
} }
if client != nil { if client != nil {
@ -386,68 +116,83 @@ func Processor(events chan ClientEvent, finished chan struct{}) {
switch cmd { switch cmd {
case "AWAY": case "AWAY":
if len(cols) == 1 { if len(cols) == 1 {
client.away = nil client.away = ""
client.ReplyNicknamed("305", "You are no longer marked as being away") client.ReplyNicknamed("305", "You are no longer marked as being away")
continue continue
} }
msg := strings.TrimLeft(cols[1], ":") client.away = strings.TrimLeft(cols[1], ":")
client.away = &msg
client.ReplyNicknamed("306", "You have been marked as being away") client.ReplyNicknamed("306", "You have been marked as being away")
case "JOIN": case "JOIN":
if len(cols) == 1 || len(cols[1]) < 1 { if len(cols) == 1 || len(cols[1]) < 1 {
client.ReplyNotEnoughParameters("JOIN") client.ReplyNotEnoughParameters("JOIN")
continue continue
} }
HandlerJoin(client, cols[1]) client.Join(cols[1])
case "LIST": case "LIST":
SendList(client, cols) client.SendList(cols)
case "LUSERS": case "LUSERS":
SendLusers(client) client.SendLusers()
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 { client.Msg("221 " + client.nickname + " +w")
client.Msg("221 " + *client.nickname + " +") // client.ReplyNicknamed("501", "Unknown MODE flag")
} else {
client.ReplyNicknamed("501", "Unknown MODE flag")
}
continue continue
} }
room := cols[0] room := cols[0]
roomsM.RLock()
r, found := rooms[room] r, found := rooms[room]
if !found { if !found {
client.ReplyNoChannel(room) client.ReplyNoChannel(room)
roomsM.RUnlock()
continue continue
} }
if len(cols) == 1 { if len(cols) == 1 {
roomSinks[r] <- ClientEvent{client, EventMode, ""} r.events <- ClientEvent{client, EventMode, ""}
} else { } else {
roomSinks[r] <- ClientEvent{client, EventMode, cols[1]} r.events <- ClientEvent{client, EventMode, cols[1]}
} }
roomsM.RUnlock()
case "MOTD": case "MOTD":
SendMotd(client) 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": 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
} }
rs := strings.Split(cols[1], " ")[0] rs := strings.Split(cols[1], " ")[0]
roomsM.RLock() roomsLock.RLock()
for _, room := range strings.Split(rs, ",") { for _, room := range strings.Split(rs, ",") {
if r, found := rooms[room]; found { if r, found := rooms[room]; found {
roomSinks[r] <- ClientEvent{client, EventDel, ""} r.events <- ClientEvent{client, EventDel, ""}
} else { } else {
client.ReplyNoChannel(room) client.ReplyNoChannel(room)
} }
} }
roomsM.RUnlock() roomsLock.RUnlock()
case "PING": case "PING":
if len(cols) == 1 { if len(cols) == 1 {
client.ReplyNicknamed("409", "No origin specified") client.ReplyNicknamed("409", "No origin specified")
@ -466,43 +211,43 @@ func Processor(events chan ClientEvent, finished chan struct{}) {
client.ReplyNicknamed("412", "No text to send") client.ReplyNicknamed("412", "No text to send")
continue continue
} }
msg := ""
target := strings.ToLower(cols[0]) target := strings.ToLower(cols[0])
clientsM.RLock() roomsLock.RLock()
for c := range clients {
if *c.nickname == target {
msg = fmt.Sprintf(":%s %s %s %s", client, cmd, *c.nickname, cols[1])
c.Msg(msg)
if c.away != nil {
client.ReplyNicknamed("301", *c.nickname, *c.away)
}
break
}
}
clientsM.RUnlock()
if msg != "" {
continue
}
roomsM.RLock()
if r, found := rooms[target]; found { if r, found := rooms[target]; found {
roomSinks[r] <- ClientEvent{ r.events <- ClientEvent{
client, client,
EventMsg, EventMsg,
cmd + " " + strings.TrimLeft(cols[1], ":"), cmd + " " + strings.TrimLeft(cols[1], ":"),
} }
} else { roomsLock.RUnlock()
client.ReplyNoNickChan(target) continue
} }
roomsM.RUnlock() 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": case "TOPIC":
if len(cols) == 1 { if len(cols) == 1 {
client.ReplyNotEnoughParameters("TOPIC") client.ReplyNotEnoughParameters("TOPIC")
continue continue
} }
cols = strings.SplitN(cols[1], " ", 2) cols = strings.SplitN(cols[1], " ", 2)
roomsM.RLock()
r, found := rooms[cols[0]] r, found := rooms[cols[0]]
roomsM.RUnlock()
if !found { if !found {
client.ReplyNoChannel(cols[0]) client.ReplyNoChannel(cols[0])
continue continue
@ -510,25 +255,20 @@ func Processor(events chan ClientEvent, finished chan struct{}) {
var change string var change string
if len(cols) > 1 { if len(cols) > 1 {
change = cols[1] change = cols[1]
} else {
change = ""
} }
roomsM.RLock() r.events <- ClientEvent{client, EventTopic, change}
roomSinks[r] <- ClientEvent{client, EventTopic, change}
roomsM.RUnlock()
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]
roomsM.RLock() r, found := rooms[room]
if r, found := rooms[room]; found { if found {
roomSinks[r] <- ClientEvent{client, EventWho, ""} r.events <- ClientEvent{client, EventWho, ""}
} else { } else {
client.ReplyNoChannel(room) client.ReplyNoChannel(room)
} }
roomsM.RUnlock()
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")
@ -536,31 +276,51 @@ func Processor(events chan ClientEvent, finished chan struct{}) {
} }
cols := strings.Split(cols[1], " ") cols := strings.Split(cols[1], " ")
nicknames := strings.Split(cols[len(cols)-1], ",") nicknames := strings.Split(cols[len(cols)-1], ",")
SendWhois(client, nicknames) client.SendWhois(nicknames)
case "ISON": case "ISON":
if len(cols) == 1 || len(cols[1]) < 1 { if len(cols) == 1 || len(cols[1]) < 1 {
client.ReplyNotEnoughParameters("ISON") client.ReplyNotEnoughParameters("ISON")
continue continue
} }
nicksKnown := make(map[string]struct{}) nicknamesList := strings.Split(cols[1], " ")
clientsM.RLock() nicknames := make(map[string]bool, len(nicknamesList))
for c := range clients { for _, nickname := range nicknamesList {
nicksKnown[*c.nickname] = struct{}{} nicknames[nickname] = false
} }
clientsM.RUnlock() clientsLock.RLock()
var nicksExists []string for c := range clients {
for _, nickname := range strings.Split(cols[1], " ") { if _, exists := nicknames[c.nickname]; exists {
if _, exists := nicksKnown[nickname]; exists { nicknames[c.nickname] = true
nicksExists = append(nicksExists, nickname)
} }
} }
client.ReplyNicknamed("303", strings.Join(nicksExists, " ")) 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": case "VERSION":
var debug string var debug string
if *verbose { if *verbose {
debug = "debug" debug = "debug"
} else {
debug = ""
} }
client.ReplyNicknamed("351", fmt.Sprintf("%s.%s %s :", Version, debug, *hostname)) client.ReplyNicknamed("351", fmt.Sprintf("%s.%s %s :", Version, debug, *hostname))
default: default:
@ -568,4 +328,47 @@ func Processor(events chan ClientEvent, finished chan struct{}) {
} }
} }
} }
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)
} }

View File

@ -28,13 +28,14 @@ func TestRegistrationWorkflow(t *testing.T) {
host := "foohost" host := "foohost"
hostname = &host hostname = &host
events := make(chan ClientEvent) events := make(chan ClientEvent)
finished := make(chan struct{})
defer func() { defer func() {
events <- ClientEvent{eventType: EventTerm} events <- ClientEvent{eventType: EventTerm}
<-finished
}() }()
go Processor(events, make(chan struct{})) go Processor(events, finished)
conn := NewTestingConn() conn := NewTestingConn()
client := NewClient(conn) client := NewClient(conn, events)
go client.Processor(events)
conn.inbound <- "UNEXISTENT CMD" // should receive nothing on this conn.inbound <- "UNEXISTENT CMD" // should receive nothing on this
conn.inbound <- "NICK" conn.inbound <- "NICK"
@ -54,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")
} }
@ -63,7 +64,7 @@ func TestRegistrationWorkflow(t *testing.T) {
t.Fatal("461 again for USER", r) t.Fatal("461 again for USER", r)
} }
SendLusers(client) client.SendLusers()
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)
} }
@ -87,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")
} }
@ -98,7 +99,7 @@ func TestRegistrationWorkflow(t *testing.T) {
t.Fatal("reply for unexistent command", r) t.Fatal("reply for unexistent command", r)
} }
SendLusers(client) client.SendLusers()
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)
} }
@ -122,11 +123,14 @@ func TestMotd(t *testing.T) {
conn := NewTestingConn() conn := NewTestingConn()
host := "foohost" host := "foohost"
hostname = &host hostname = &host
client := NewClient(conn) client := NewClient(conn, make(chan ClientEvent, 2))
defer func() {
client.Close()
}()
motdName := fd.Name() motdName := fd.Name()
motd = &motdName motd = &motdName
SendMotd(client) client.SendMotd()
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)
} }

119
events.go
View File

@ -1,119 +0,0 @@
/*
goircd -- minimalistic simple Internet Relay Chat (IRC) server
Copyright (C) 2014-2020 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, 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 <http://www.gnu.org/licenses/>.
*/
package main
import (
"fmt"
"io/ioutil"
"log"
"os"
"path"
"time"
)
const (
EventNew = iota
EventDel = iota
EventMsg = iota
EventTopic = iota
EventWho = iota
EventMode = iota
EventTerm = iota
EventTick = iota
FormatMsg = "[%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
// They can be either NEW, DEL or unparsed MSG
type ClientEvent struct {
client *Client
eventType int
text string
}
func (m ClientEvent) String() string {
return string(m.eventType) + ": " + m.client.String() + ": " + m.text
}
// Logging in-room events
// Intended to tell when, where and who send a message or meta command
type LogEvent struct {
where string
who string
what string
meta bool
}
// Logging events logger itself
// Each room's events are written to separate file in logdir
// Events include messages, topic and keys changes, joining and leaving
func Logger(logdir string, events <-chan LogEvent) {
mode := os.O_CREATE | os.O_WRONLY | os.O_APPEND
perm := os.FileMode(0660)
var format string
var logfile string
var fd *os.File
var err error
for event := range events {
logfile = path.Join(logdir, event.where+".log")
fd, err = os.OpenFile(logfile, mode, perm)
if err != nil {
log.Println("Can not open logfile", logfile, err)
continue
}
if event.meta {
format = FormatMeta
} else {
format = FormatMsg
}
_, err = fd.WriteString(fmt.Sprintf(format, time.Now(), event.who, event.what))
fd.Close()
if err != nil {
log.Println("Error writing to logfile", logfile, err)
}
}
}
type StateEvent struct {
where string
topic string
key string
}
// Room state events saver
// Room states shows that either topic or key has been changed
// Each room's state is written to separate file in statedir
func StateKeeper(statedir string, events <-chan StateEvent) {
var fn string
var data string
var err error
for event := range events {
fn = path.Join(statedir, event.where)
data = event.topic + "\n" + event.key + "\n"
err = ioutil.WriteFile(fn, []byte(data), os.FileMode(0660))
if err != nil {
log.Printf("Can not write statefile %s: %v", fn, err)
}
}
}

277
goircd.go
View File

@ -23,114 +23,213 @@ import (
"io/ioutil" "io/ioutil"
"log" "log"
"net" "net"
"os"
"os/signal"
"path" "path"
"path/filepath" "path/filepath"
"strconv"
"strings" "strings"
"syscall"
) )
const Version = "1.8.2" const (
Version = "1.9.0"
var ( StateTopicFilename = "topic"
hostname = flag.String("hostname", "localhost", "Hostname") StateKeyFilename = "key"
bind = flag.String("bind", ":6667", "Address to bind to")
motd = flag.String("motd", "", "Path to MOTD file") EventNew = iota
logdir = flag.String("logdir", "", "Absolute path to directory for logs") EventDel = iota
statedir = flag.String("statedir", "", "Absolute path to directory for states") EventMsg = iota
passwords = flag.String("passwords", "", "Optional path to passwords file") EventTopic = iota
tlsBind = flag.String("tlsbind", "", "TLS address to bind to") EventWho = iota
tlsPEM = flag.String("tlspem", "", "Path to TLS certificat+key PEM file") EventMode = iota
verbose = flag.Bool("v", false, "Enable verbose logging.") EventTerm = iota
EventTick = iota
) )
func listenerLoop(sock net.Listener, events chan ClientEvent) { type ClientEvent struct {
for { client *Client
conn, err := sock.Accept() eventType int
if err != nil { text string
log.Println("Error during accepting connection", err)
continue
}
client := NewClient(conn)
go client.Processor(events)
}
} }
func Run() { type StateEvent struct {
events := make(chan ClientEvent) where string
log.SetFlags(log.Ldate | log.Lmicroseconds | log.Lshortfile) topic string
key string
}
if *logdir == "" { var (
// Dummy logger hostname = flag.String("hostname", "localhost", "hostname")
go func() { bind = flag.String("bind", "[::1]:6667", "address to bind to")
for _ = range logSink { cloak = flag.String("cloak", "", "cloak user's host with the given hostname")
} motd = flag.String("motd", "", "path to MOTD file")
}() logdir = flag.String("logdir", "", "absolute path to directory for logs")
} else { statedir = flag.String("statedir", "", "absolute path to directory for states")
if !path.IsAbs(*logdir) { passwords = flag.String("passwd", "", "optional path to passwords file")
log.Fatalln("Need absolute path for logdir") tlsBind = flag.String("tlsbind", "", "TLS address to bind to")
} tlsPEM = flag.String("tlspem", "", "path to TLS certificat+key PEM file")
go Logger(*logdir, logSink) permStateDirS = flag.String("perm-state-dir", "755", "state directory permissions")
log.Println(*logdir, "logger initialized") permStateFileS = flag.String("perm-state-file", "600", "state files permissions")
} permLogFileS = flag.String("perm-log-file", "644", "log files permissions")
timestamped = flag.Bool("timestamped", false, "enable timestamps on stderr messages")
verbose = flag.Bool("verbose", false, "enable verbose logging")
debug = flag.Bool("debug", false, "enable debug (traffic) logging")
log.Println("goircd " + Version + " is starting") permStateDir os.FileMode
if *statedir == "" { permStateFile os.FileMode
// Dummy statekeeper permLogFile os.FileMode
go func() {
for _ = range stateSink {
}
}()
} else {
if !path.IsAbs(*statedir) {
log.Fatalln("Need absolute path for statedir")
}
states, err := filepath.Glob(path.Join(*statedir, "#*"))
if err != nil {
log.Fatalln("Can not read statedir", err)
}
for _, state := range states {
buf, err := ioutil.ReadFile(state)
if err != nil {
log.Fatalf("Can not read state %s: %v", state, err)
}
room, _ := RoomRegister(path.Base(state))
contents := strings.Split(string(buf), "\n")
if len(contents) < 2 {
log.Printf("State corrupted for %s: %q", *room.name, contents)
} else {
room.topic = &contents[0]
room.key = &contents[1]
log.Println("Loaded state for room", *room.name)
}
}
go StateKeeper(*statedir, stateSink)
log.Println(*statedir, "statekeeper initialized")
}
if *bind != "" { stateSink chan StateEvent = make(chan StateEvent)
listener, err := net.Listen("tcp", *bind) )
if err != nil {
log.Fatalf("Can not listen on %s: %v", *bind, err) func permParse(s string) os.FileMode {
} r, err := strconv.ParseUint(s, 8, 16)
log.Println("Raw listening on", *bind) if err != nil {
go listenerLoop(listener, events) log.Fatalln(err)
} }
if *tlsBind != "" { return os.FileMode(r)
cert, err := tls.LoadX509KeyPair(*tlsPEM, *tlsPEM) }
func listenerLoop(ln net.Listener, events chan ClientEvent) {
for {
conn, err := ln.Accept()
if err != nil { if err != nil {
log.Fatalf("Could not load TLS keys from %s: %s", *tlsPEM, err) log.Println("error during accept", err)
continue
} }
config := tls.Config{Certificates: []tls.Certificate{cert}} NewClient(conn, events)
listenerTLS, err := tls.Listen("tcp", *tlsBind, &config)
if err != nil {
log.Fatalf("Can not listen on %s: %v", *tlsBind, err)
}
log.Println("TLS listening on", *tlsBind)
go listenerLoop(listenerTLS, events)
} }
Processor(events, make(chan struct{}))
} }
func main() { func main() {
flag.Parse() flag.Parse()
Run() permStateDir = permParse(*permStateDirS)
permStateFile = permParse(*permStateFileS)
permLogFile = permParse(*permLogFileS)
if *timestamped {
log.SetFlags(log.Ldate | log.Lmicroseconds | log.Lshortfile)
} else {
log.SetFlags(log.Lshortfile)
}
log.SetOutput(os.Stdout)
if *logdir == "" {
// Dummy logger
go func() {
for range logSink {
}
}()
} else {
if !path.IsAbs(*logdir) {
log.Fatalln("need absolute path for logdir")
}
go Logger(*logdir, logSink)
}
if *statedir == "" {
// Dummy statekeeper
go func() {
for range stateSink {
}
}()
} else {
if !path.IsAbs(*statedir) {
log.Fatalln("need absolute path for statedir")
}
states, err := filepath.Glob(path.Join(*statedir, "#*"))
if err != nil {
log.Fatalln("can not read statedir", err)
}
for _, state := range states {
buf, err := ioutil.ReadFile(path.Join(state, StateTopicFilename))
if err != nil {
log.Fatalf(
"can not read state %s/%s: %v",
state, StateTopicFilename, err,
)
}
room := RoomRegister(path.Base(state))
room.topic = strings.TrimRight(string(buf), "\n")
buf, err = ioutil.ReadFile(path.Join(state, StateKeyFilename))
if err == nil {
room.key = strings.TrimRight(string(buf), "\n")
} else {
if !os.IsNotExist(err) {
log.Fatalf(
"can not read state %s/%s: %v",
state, StateKeyFilename, err,
)
}
}
log.Println("loaded state for room:", room.name)
}
go func() {
for event := range stateSink {
statePath := path.Join(*statedir, event.where)
if _, err := os.Stat(statePath); os.IsNotExist(err) {
if err := os.Mkdir(statePath, permStateDir); err != nil {
log.Printf("can not create state %s: %v", statePath, err)
continue
}
}
topicPath := path.Join(statePath, StateTopicFilename)
if err := ioutil.WriteFile(
topicPath,
[]byte(event.topic+"\n"),
permStateFile,
); err != nil {
log.Printf("can not write statefile %s: %v", topicPath, err)
continue
}
keyPath := path.Join(statePath, StateKeyFilename)
if err := ioutil.WriteFile(
keyPath,
[]byte(event.key+"\n"),
permStateFile,
); err != nil {
log.Printf("can not write statefile %s: %v", keyPath, err)
}
}
}()
}
events := make(chan ClientEvent)
if *bind != "" {
ln, err := net.Listen("tcp", *bind)
if err != nil {
log.Fatalf("can not listen on %s: %v", *bind, err)
}
go listenerLoop(ln, events)
}
if *tlsBind != "" {
cert, err := tls.LoadX509KeyPair(*tlsPEM, *tlsPEM)
if err != nil {
log.Fatalf("can not load TLS keys from %s: %s", *tlsPEM, err)
}
config := tls.Config{Certificates: []tls.Certificate{cert}}
ln, err := tls.Listen("tcp", *tlsBind, &config)
if err != nil {
log.Fatalf("can not listen on %s: %v", *tlsBind, err)
}
go listenerLoop(ln, events)
}
log.Println("goircd", Version, "started")
needsShutdown := make(chan os.Signal, 0)
signal.Notify(needsShutdown, syscall.SIGTERM, syscall.SIGINT)
go func() {
<-needsShutdown
events <- ClientEvent{eventType: EventTerm}
log.Println("goircd shutting down")
}()
finished := make(chan struct{})
go Processor(events, finished)
<-finished
} }

72
log.go Normal file
View File

@ -0,0 +1,72 @@
/*
goircd -- minimalistic simple Internet Relay Chat (IRC) server
Copyright (C) 2014-2020 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, 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 <http://www.gnu.org/licenses/>.
*/
package main
import (
"fmt"
"log"
"os"
"path"
"time"
)
const (
FormatMsg = "[%s] <%s> %s\n"
FormatMeta = "[%s] * %s %s\n"
)
var logSink chan LogEvent = make(chan LogEvent)
type LogEvent struct {
where string
who string
what string
meta bool
}
func Logger(logdir string, events <-chan LogEvent) {
mode := os.O_CREATE | os.O_WRONLY | os.O_APPEND
perm := os.FileMode(permLogFile)
var format string
var logfile string
var fd *os.File
var err error
for event := range events {
logfile = path.Join(logdir, event.where+".log")
fd, err = os.OpenFile(logfile, mode, perm)
if err != nil {
log.Println("can not open logfile", logfile, err)
continue
}
if event.meta {
format = FormatMeta
} else {
format = FormatMsg
}
_, err = fd.WriteString(fmt.Sprintf(
format,
time.Now().Format(time.RFC3339),
event.who,
event.what,
))
fd.Close()
if err != nil {
log.Println("error writing to logfile", logfile, err)
}
}
}

306
room.go
View File

@ -26,225 +26,213 @@ import (
"sync" "sync"
) )
var (
RERoom = regexp.MustCompile("^#[^\x00\x07\x0a\x0d ,:/]{1,200}$")
)
// Sanitize room's name. It can consist of 1 to 50 ASCII symbols
// with some exclusions. All room names will have "#" prefix.
func RoomNameValid(name string) bool {
return RERoom.MatchString(name)
}
type Room struct { type Room struct {
name *string name string
topic *string topic string
key *string key string
members map[*Client]struct{} members map[*Client]struct{}
events chan ClientEvent
sync.RWMutex sync.RWMutex
} }
func (room *Room) String() (name string) { var (
room.RLock() RERoom = regexp.MustCompile("^#[^\x00\x07\x0a\x0d ,:/]{1,200}$")
name = *room.name
room.RUnlock()
return
}
func NewRoom(name string) *Room { rooms map[string]*Room = make(map[string]*Room)
topic := "" roomsLock sync.RWMutex
key := "" roomsWG sync.WaitGroup
return &Room{ )
name: &name,
topic: &topic,
key: &key,
members: make(map[*Client]struct{}),
}
}
func (room *Room) SendTopic(client *Client) { func (r *Room) SendTopic(c *Client) {
room.RLock() t := r.topic
if *room.topic == "" { if t == "" {
client.ReplyNicknamed("331", room.String(), "No topic is set") c.ReplyNicknamed("331", r.name, "No topic is set")
} else { } else {
client.ReplyNicknamed("332", room.String(), *room.topic) c.ReplyNicknamed("332", r.name, t)
} }
room.RUnlock()
} }
// Send message to all room's subscribers, possibly excluding someone. func (r *Room) SendNames(c *Client) {
func (room *Room) Broadcast(msg string, clientToIgnore ...*Client) { allowed := false
room.RLock() if r.key == "" {
for member := range room.members { allowed = true
if (len(clientToIgnore) > 0) && member == clientToIgnore[0] { } else if _, isMember := r.members[c]; isMember {
allowed = true
}
if !allowed {
c.ReplyNicknamed("475", r.name, "Cannot join channel (+k)")
return
}
r.RLock()
nicknames := make([]string, 0, len(r.members))
for member := range r.members {
nicknames = append(nicknames, member.nickname)
}
r.RUnlock()
sort.Strings(nicknames)
maxLen := 512 - len(*hostname) - 2 - 2
MoreNicknames:
lenAll := 0
lenName := 0
for i, n := range nicknames {
lenName = len(n) + 1
if lenAll+lenName >= maxLen {
c.ReplyNicknamed("353", "=", r.name, strings.Join(nicknames[:i-1], " "))
nicknames = nicknames[i:]
goto MoreNicknames
}
lenAll += lenName
}
if len(nicknames) > 0 {
c.ReplyNicknamed("353", "=", r.name, strings.Join(nicknames, " "))
}
c.ReplyNicknamed("366", r.name, "End of NAMES list")
}
func (r *Room) Broadcast(msg string, excludes ...*Client) {
var exclude *Client
if len(excludes) > 0 {
exclude = excludes[0]
}
r.RLock()
for member := range r.members {
if member == exclude {
continue continue
} }
member.Msg(msg) member.Msg(msg)
} }
room.RUnlock() r.RUnlock()
} }
func (room *Room) StateSave() { func (r *Room) StateSave() {
room.RLock() stateSink <- StateEvent{r.name, r.topic, r.key}
stateSink <- StateEvent{room.String(), *room.topic, *room.key}
room.RUnlock()
} }
func (room *Room) Processor(events <-chan ClientEvent) { func (r *Room) Processor(events <-chan ClientEvent) {
var client *Client for e := range events {
for event := range events { c := e.client
client = event.client switch e.eventType {
switch event.eventType {
case EventTerm: case EventTerm:
roomsGroup.Done() roomsWG.Done()
return return
case EventNew: case EventNew:
room.Lock() r.Lock()
room.members[client] = struct{}{} r.members[c] = struct{}{}
r.Unlock()
if *verbose { if *verbose {
log.Println(client, "joined", room.name) log.Println(c, "joined", r.name)
} }
room.Unlock() r.SendTopic(c)
room.SendTopic(client) r.Broadcast(fmt.Sprintf(":%s JOIN %s", c, r.name))
room.Broadcast(fmt.Sprintf(":%s JOIN %s", client, room.String())) logSink <- LogEvent{r.name, c.nickname, "joined", true}
logSink <- LogEvent{room.String(), *client.nickname, "joined", true} r.SendNames(c)
nicknames := make([]string, 0)
room.RLock()
for member := range room.members {
nicknames = append(nicknames, *member.nickname)
}
room.RUnlock()
sort.Strings(nicknames)
client.ReplyNicknamed("353", "=", room.String(), strings.Join(nicknames, " "))
client.ReplyNicknamed("366", room.String(), "End of NAMES list")
case EventDel: case EventDel:
room.RLock() if _, subscribed := r.members[c]; !subscribed {
if _, subscribed := room.members[client]; !subscribed { c.ReplyNicknamed("442", r.name, "You are not on that channel")
client.ReplyNicknamed("442", room.String(), "You are not on that channel")
room.RUnlock()
continue continue
} }
room.RUnlock() msg := fmt.Sprintf(":%s PART %s :%s", c, r.name, c.nickname)
room.Lock() r.Broadcast(msg)
delete(room.members, client) r.Lock()
room.Unlock() delete(r.members, c)
room.RLock() r.Unlock()
msg := fmt.Sprintf(":%s PART %s :%s", client, room.String(), *client.nickname) logSink <- LogEvent{r.name, c.nickname, "left", true}
room.Broadcast(msg) if *verbose {
logSink <- LogEvent{room.String(), *client.nickname, "left", true} log.Println(c, "left", r.name)
room.RUnlock() }
case EventTopic: case EventTopic:
room.RLock() if _, subscribed := r.members[c]; !subscribed {
if _, subscribed := room.members[client]; !subscribed { c.ReplyParts("442", r.name, "You are not on that channel")
client.ReplyParts("442", room.String(), "You are not on that channel")
room.RUnlock()
continue continue
} }
if event.text == "" { if e.text == "" {
room.SendTopic(client) r.SendTopic(c)
room.RUnlock()
continue continue
} }
room.RUnlock() topic := strings.TrimLeft(e.text, ":")
topic := strings.TrimLeft(event.text, ":") r.topic = topic
room.Lock() msg := fmt.Sprintf(":%s TOPIC %s :%s", c, r.name, r.topic)
room.topic = &topic r.Broadcast(msg)
room.Unlock() logSink <- LogEvent{r.name, c.nickname, "set topic to " + r.topic, true}
room.RLock() r.StateSave()
msg := fmt.Sprintf(":%s TOPIC %s :%s", client, room.String(), *room.topic)
room.Broadcast(msg)
logSink <- LogEvent{
room.String(),
*client.nickname,
"set topic to " + *room.topic,
true,
}
room.RUnlock()
room.StateSave()
case EventWho: case EventWho:
room.RLock() r.RLock()
for m := range room.members { for m := range r.members {
client.ReplyNicknamed( c.ReplyNicknamed(
"352", "352",
room.String(), r.name,
*m.username, m.username,
m.Host(), m.Host(),
*hostname, *hostname,
*m.nickname, m.nickname,
"H", "H",
"0 "+*m.realname, "0 "+m.realname,
) )
} }
client.ReplyNicknamed("315", room.String(), "End of /WHO list") c.ReplyNicknamed("315", r.name, "End of /WHO list")
room.RUnlock() r.RUnlock()
case EventMode: case EventMode:
room.RLock() if e.text == "" {
if event.text == "" { mode := "+n"
mode := "+" if r.key != "" {
if *room.key != "" {
mode = mode + "k" mode = mode + "k"
} }
client.Msg(fmt.Sprintf("324 %s %s %s", *client.nickname, room.String(), mode)) c.Msg(fmt.Sprintf("324 %s %s %s", c.nickname, r.name, mode))
room.RUnlock()
continue continue
} }
if strings.HasPrefix(event.text, "b") { if strings.HasPrefix(e.text, "b") {
client.ReplyNicknamed("368", room.String(), "End of channel ban list") c.ReplyNicknamed("368", r.name, "End of channel ban list")
room.RUnlock()
continue continue
} }
if strings.HasPrefix(event.text, "-k") || strings.HasPrefix(event.text, "+k") { if strings.HasPrefix(e.text, "-k") || strings.HasPrefix(e.text, "+k") {
if _, subscribed := room.members[client]; !subscribed { if _, subscribed := r.members[c]; !subscribed {
client.ReplyParts("442", room.String(), "You are not on that channel") c.ReplyParts("442", r.name, "You are not on that channel")
room.RUnlock()
continue continue
} }
} else { } else {
client.ReplyNicknamed("472", event.text, "Unknown MODE flag") c.ReplyNicknamed("472", e.text, "Unknown MODE flag")
room.RUnlock()
continue continue
} }
room.RUnlock()
var msg string var msg string
var msgLog string var msgLog string
if strings.HasPrefix(event.text, "+k") { if strings.HasPrefix(e.text, "+k") {
cols := strings.Split(event.text, " ") cols := strings.Split(e.text, " ")
if len(cols) == 1 { if len(cols) == 1 {
client.ReplyNotEnoughParameters("MODE") c.ReplyNotEnoughParameters("MODE")
continue continue
} }
room.Lock() r.key = cols[1]
room.key = &cols[1] msg = fmt.Sprintf(":%s MODE %s +k %s", c, r.name, r.key)
msg = fmt.Sprintf(":%s MODE %s +k %s", client, *room.name, *room.key) msgLog = "set channel key"
msgLog = "set channel key to " + *room.key
room.Unlock()
} else { } else {
key := "" r.key = ""
room.Lock() msg = fmt.Sprintf(":%s MODE %s -k", c, r.name)
room.key = &key
msg = fmt.Sprintf(":%s MODE %s -k", client, *room.name)
room.Unlock()
msgLog = "removed channel key" msgLog = "removed channel key"
} }
room.Broadcast(msg) r.Broadcast(msg)
logSink <- LogEvent{room.String(), *client.nickname, msgLog, true} logSink <- LogEvent{r.name, c.nickname, msgLog, true}
room.StateSave() r.StateSave()
case EventMsg: case EventMsg:
sep := strings.Index(event.text, " ") sep := strings.Index(e.text, " ")
room.Broadcast(fmt.Sprintf( r.Broadcast(fmt.Sprintf(
":%s %s %s :%s", ":%s %s %s :%s", c, e.text[:sep], r.name, e.text[sep+1:],
client, ), c)
event.text[:sep], logSink <- LogEvent{r.name, c.nickname, e.text[sep+1:], false}
room.String(),
event.text[sep+1:]),
client,
)
logSink <- LogEvent{
room.String(),
*client.nickname,
event.text[sep+1:],
false,
}
} }
} }
} }
func RoomRegister(name string) *Room {
r := &Room{
name: name,
members: make(map[*Client]struct{}),
events: make(chan ClientEvent),
}
roomsLock.Lock()
roomsWG.Add(1)
rooms[name] = r
roomsLock.Unlock()
go r.Processor(r.events)
return r
}

View File

@ -46,10 +46,9 @@ func TestTwoUsers(t *testing.T) {
host := "foohost" host := "foohost"
hostname = &host hostname = &host
events := make(chan ClientEvent) events := make(chan ClientEvent)
roomsM.Lock() roomsLock.Lock()
rooms = make(map[string]*Room) rooms = make(map[string]*Room)
roomSinks = make(map[*Room]chan ClientEvent) roomsLock.Unlock()
roomsM.Unlock()
clients = make(map[*Client]struct{}) clients = make(map[*Client]struct{})
finished := make(chan struct{}) finished := make(chan struct{})
go Processor(events, finished) go Processor(events, finished)
@ -60,10 +59,8 @@ func TestTwoUsers(t *testing.T) {
conn1 := NewTestingConn() conn1 := NewTestingConn()
conn2 := NewTestingConn() conn2 := NewTestingConn()
client1 := NewClient(conn1) client1 := NewClient(conn1, events)
client2 := NewClient(conn2) NewClient(conn2, events)
go client1.Processor(events)
go client2.Processor(events)
conn1.inbound <- "NICK nick1\r\nUSER foo1 bar1 baz1 :Long name1" conn1.inbound <- "NICK nick1\r\nUSER foo1 bar1 baz1 :Long name1"
conn2.inbound <- "NICK nick2\r\nUSER foo2 bar2 baz2 :Long name2" conn2.inbound <- "NICK nick2\r\nUSER foo2 bar2 baz2 :Long name2"
@ -72,7 +69,7 @@ func TestTwoUsers(t *testing.T) {
<-conn2.outbound <-conn2.outbound
} }
SendLusers(client1) client1.SendLusers()
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)
} }
@ -139,7 +136,6 @@ func TestJoin(t *testing.T) {
events := make(chan ClientEvent) events := make(chan ClientEvent)
rooms = make(map[string]*Room) rooms = make(map[string]*Room)
clients = make(map[*Client]struct{}) clients = make(map[*Client]struct{})
roomSinks = make(map[*Room]chan ClientEvent)
finished := make(chan struct{}) finished := make(chan struct{})
go Processor(events, finished) go Processor(events, finished)
defer func() { defer func() {
@ -147,8 +143,7 @@ func TestJoin(t *testing.T) {
<-finished <-finished
}() }()
conn := NewTestingConn() conn := NewTestingConn()
client := NewClient(conn) NewClient(conn, 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"
for i := 0; i < 6; i++ { for i := 0; i < 6; i++ {
@ -183,14 +178,14 @@ func TestJoin(t *testing.T) {
for i := 0; i < 4*2; i++ { for i := 0; i < 4*2; i++ {
<-conn.outbound <-conn.outbound
} }
roomsM.RLock() roomsLock.RLock()
if _, ok := rooms["#bar"]; !ok { if _, ok := rooms["#bar"]; !ok {
t.Fatal("#bar does not exist") t.Fatal("#bar does not exist")
} }
if _, ok := rooms["#baz"]; !ok { if _, ok := rooms["#baz"]; !ok {
t.Fatal("#baz does not exist") t.Fatal("#baz does not exist")
} }
roomsM.RUnlock() roomsLock.RUnlock()
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) {
t.Fatal("invalid join log event #bar", r) t.Fatal("invalid join log event #bar", r)
} }
@ -202,14 +197,14 @@ func TestJoin(t *testing.T) {
for i := 0; i < 4*2; i++ { for i := 0; i < 4*2; i++ {
<-conn.outbound <-conn.outbound
} }
roomsM.RLock() roomsLock.RLock()
if *rooms["#barenc"].key != "key1" { if rooms["#barenc"].key != "key1" {
t.Fatal("no room with key1") t.Fatal("no room with key1")
} }
if *rooms["#bazenc"].key != "key2" { if rooms["#bazenc"].key != "key2" {
t.Fatal("no room with key2") t.Fatal("no room with key2")
} }
roomsM.RUnlock() roomsLock.RUnlock()
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) {
t.Fatal("invalid join log event #barenc", r) t.Fatal("invalid join log event #barenc", r)
} }
@ -227,11 +222,11 @@ 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)
} }
roomsM.RLock() roomsLock.RLock()
if *rooms["#barenc"].key != "" { if rooms["#barenc"].key != "" {
t.Fatal("removing key from #barenc") t.Fatal("removing key from #barenc")
} }
roomsM.RUnlock() roomsLock.RUnlock()
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) {
t.Fatal("removed channel key log", r) t.Fatal("removed channel key log", r)
} }
@ -240,6 +235,9 @@ func TestJoin(t *testing.T) {
} }
conn.inbound <- "PART #bazenc\r\nMODE #bazenc -k" conn.inbound <- "PART #bazenc\r\nMODE #bazenc -k"
if r := <-conn.outbound; r != ":nick2!foo2@someclient PART #bazenc :nick2\r\n" {
t.Fatal("part", r)
}
if r := <-conn.outbound; r != ":foohost 442 #bazenc :You are not on that channel\r\n" { if r := <-conn.outbound; r != ":foohost 442 #bazenc :You are not on that channel\r\n" {
t.Fatal("not on that channel", r) t.Fatal("not on that channel", r)
} }
@ -256,7 +254,7 @@ func TestJoin(t *testing.T) {
if r := <-conn.outbound; r != ":nick2!foo2@someclient MODE #barenc +k newkey\r\n" { if r := <-conn.outbound; r != ":nick2!foo2@someclient MODE #barenc +k newkey\r\n" {
t.Fatal("+k MODE setting", r) t.Fatal("+k MODE setting", r)
} }
if r := <-logSink; (r.what != "set channel key to newkey") || (r.where != "#barenc") || (r.who != "nick2") || (r.meta != true) { if r := <-logSink; (r.what != "set channel key") || (r.where != "#barenc") || (r.who != "nick2") || (r.meta != true) {
t.Fatal("set channel key", r) t.Fatal("set channel key", r)
} }
if r := <-stateSink; (r.topic != "") || (r.where != "#barenc") || (r.key != "newkey") { if r := <-stateSink; (r.topic != "") || (r.where != "#barenc") || (r.key != "newkey") {