diff --git a/INSTALL b/INSTALL
index 3c5c634..f3d979c 100644
--- a/INSTALL
+++ b/INSTALL
@@ -1,8 +1,9 @@
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
-* or manually:
+ $ go get go.cypherpunks.ru/goircd
+
+or manually:
$ git clone git://git.cypherpunks.ru/goircd.git
$ cd goircd
diff --git a/README b/README
index 663aaf3..e9c5eb1 100644
--- a/README
+++ b/README
@@ -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 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
But it has some convincing features:
@@ -19,8 +19,8 @@ But it has some convincing features:
* Single executable binary
* No configuration file, just few command line arguments
* IPv6 out-of-box support
-* Ability to listen on TLS-capable ports
-* Optional channel logging to plain text files
+* Ability to additionally listen on TLS-capable ports
+* Optional channels logging to 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
disappear)
@@ -28,35 +28,42 @@ But it has some convincing features:
Some remarks and recommendations related to it's simplicity:
-* Use either nohup or similar tools to daemonize it
-* Just plain logging on stderr, without syslog support
+* Use daemontools to daemonize, setuid/gid it
+* Just plaintext logging to stdout, without syslog support -- use
+ daemontool's multilog
SUPPORTED IRC COMMANDS
* PASS/NICK/USER during registration workflow
* PING/PONGs
* 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
USAGE
Just execute goircd daemon. It has following optional arguments:
- -hostname: hostname to show for client's connections
- -bind: address to bind to (:6667 by default)
- -motd: absolute path to MOTD file. It is reread every time
- MOTD is requested
- -logdir: directory where all channels messages will be saved. If
- omitted, then no logs will be kept
- -statedir: directory where all channels states will be saved and
- loaded during startup. If omitted, then states will be
- lost after daemon termination
- -tlsbind: enable TLS, specify address to listen on and path
- -tlspem to PEM file with certificate and private key
- -passwords: enable client authentication and specify path to
- passwords file
- -v: increase verbosity
+ -hostname: hostname to show for client's connections
+ -bind: address to bind to (:6667 by default)
+ -cloak: cloak user's host with the given hostname
+ -motd: absolute path to MOTD file. It is reread every time
+ MOTD is requested
+ -logdir: directory where all channels messages will be saved. If
+ omitted, then no logs will be kept
+ -statedir: directory where all channels states will be saved and
+ loaded during startup. If omitted, then states will be
+ lost after daemon termination
+ -tlsbind: enable TLS, specify address to listen on and path
+ -tlspem: to PEM file with certificate and private key
+ -passwd: enable client authentication and specify path to
+ 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
@@ -69,10 +76,12 @@ AUTHENTICATION
You can turn on optional client authentication by preparing passwords
file and using the -passwords argument. Format of passwords file is:
- login1:password1\n
- login2:password2\n
+ login1:hex(sha256(password1))\n
+ login2:hex(sha256(password2))\n
...
+You can get hashed password value using: echo -n password | sha256
+
LOG FILES
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
-Each state file has the name equals to room's one. It contains two plain
-text lines: room's topic and room's authentication key (empty if none
-specified). For example:
-
- $ cat states/meinroom
- This is meinroom's topic
- secretkey
+Room's state is created/saved when either topic or key is set. State is
+a directory (room's name) with "topic" and "key" plaintext files.
LICENCE
diff --git a/clean.do b/clean.do
new file mode 100644
index 0000000..34df69f
--- /dev/null
+++ b/clean.do
@@ -0,0 +1 @@
+rm -f goircd
diff --git a/client.go b/client.go
index 2c8c0ad..6a21ab0 100644
--- a/client.go
+++ b/client.go
@@ -19,8 +19,15 @@ package main
import (
"bytes"
+ "crypto/sha256"
+ "crypto/subtle"
+ "encoding/hex"
+ "fmt"
+ "io/ioutil"
"log"
"net"
+ "regexp"
+ "sort"
"strings"
"sync"
"time"
@@ -28,29 +35,37 @@ import (
const (
BufSize = 1500
- MaxOutBuf = 1 << 12
+ MaxOutBuf = 128
)
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 {
conn net.Conn
registered bool
- nickname *string
- username *string
- realname *string
- password *string
- away *string
+ nickname string
+ username string
+ realname string
+ password string
+ away string
recvTimestamp time.Time
sendTimestamp time.Time
- outBuf chan *string
+ outBuf chan string
alive bool
sync.Mutex
}
func (c *Client) Host() string {
+ if *cloak != "" {
+ return *cloak
+ }
addr := c.conn.RemoteAddr().String()
if host, _, err := net.SplitHostPort(addr); err == nil {
addr = host
@@ -62,48 +77,42 @@ func (c *Client) Host() 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 {
- nickname := "*"
- username := ""
+func NewClient(conn net.Conn, events chan ClientEvent) *Client {
c := Client{
conn: conn,
- nickname: &nickname,
- username: &username,
+ nickname: "*",
+ username: "",
recvTimestamp: time.Now(),
sendTimestamp: time.Now(),
alive: true,
- outBuf: make(chan *string, MaxOutBuf),
+ outBuf: make(chan string, MaxOutBuf),
}
+ clientsWG.Add(2)
go c.MsgSender()
+ go c.Processor(events)
return &c
}
-func (c *Client) SetDead() {
- c.outBuf <- nil
- c.alive = false
-}
-
func (c *Client) Close() {
c.Lock()
if c.alive {
- c.SetDead()
+ close(c.outBuf)
+ c.alive = false
}
c.Unlock()
}
-// Client processor blockingly reads everything remote client sends,
-// splits messages by CRLF and send them to Daemon gorouting for processing
-// it futher. Also it can signalize that client is unavailable (disconnected).
-func (c *Client) Processor(sink chan ClientEvent) {
- sink <- ClientEvent{c, EventNew, ""}
- log.Println(c, "New client")
+func (c *Client) Processor(events chan ClientEvent) {
+ events <- ClientEvent{c, EventNew, ""}
+ if *verbose {
+ log.Println(c, "connected")
+ }
buf := make([]byte, BufSize*2)
- var n int
- var prev int
- var i int
+ var n, prev, i int
+ var msg string
var err error
for {
if prev == BufSize {
@@ -120,26 +129,43 @@ func (c *Client) Processor(sink chan ClientEvent) {
if i == -1 {
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])
prev -= (i + 2)
goto CheckMore
}
c.Close()
- sink <- ClientEvent{c, EventDel, ""}
+ if *verbose {
+ log.Println(c, "disconnected")
+ }
+ events <- ClientEvent{c, EventDel, ""}
+ clientsWG.Done()
}
func (c *Client) MsgSender() {
+ var err error
for msg := range c.outBuf {
- if msg == nil {
- c.conn.Close()
+ if *debug {
+ log.Println(c, "<-", msg)
+ }
+ if _, err = c.conn.Write(append([]byte(msg), CRLF...)); err != nil {
+ if *verbose {
+ log.Println(c, "error writing", err)
+ }
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) {
c.Lock()
defer c.Unlock()
@@ -149,20 +175,18 @@ func (c *Client) Msg(text string) {
if len(c.outBuf) == MaxOutBuf {
log.Println(c, "output buffer size exceeded, kicking him")
if c.alive {
- c.SetDead()
+ close(c.outBuf)
+ c.alive = false
}
return
}
- c.outBuf <- &text
+ c.outBuf <- text
}
-// Send message from server. It has ": servername" prefix.
func (c *Client) Reply(text string) {
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) {
parts := []string{code}
for _, t := range text {
@@ -172,18 +196,14 @@ func (c *Client) ReplyParts(code string, text ...string) {
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) {
- 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) {
c.ReplyNicknamed("461", command, "Not enough parameters")
}
-// Reply "403 no such channel" error for specified channel.
func (c *Client) ReplyNoChannel(channel string) {
c.ReplyNicknamed("403", channel, "No such channel")
}
@@ -191,3 +211,252 @@ func (c *Client) ReplyNoChannel(channel string) {
func (c *Client) ReplyNoNickChan(channel string) {
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")
+ }
+ }
+}
diff --git a/client_test.go b/client_test.go
index 4af1dd3..ed71da8 100644
--- a/client_test.go
+++ b/client_test.go
@@ -21,45 +21,46 @@ import (
"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) {
conn := NewTestingConn()
- sink := make(chan ClientEvent)
+ events := make(chan ClientEvent)
host := "foohost"
hostname = &host
- client := NewClient(conn)
- go client.Processor(sink)
+ client := NewClient(conn, events)
+ defer func() {
+ client.Close()
+ }()
- event := <-sink
+ event := <-events
if event.eventType != EventNew {
t.Fatal("no NEW event", event)
}
conn.inbound <- "foo"
- event = <-sink
+ event = <-events
if (event.eventType != EventMsg) || (event.text != "foo") {
t.Fatal("no first MSG", event)
}
conn.inbound <- "bar"
- event = <-sink
+ event = <-events
if (event.eventType != EventMsg) || (event.text != "bar") {
t.Fatal("no second MSG", event)
}
conn.inbound <- ""
- event = <-sink
+ event = <-events
if event.eventType != EventDel {
t.Fatal("no client termination", event)
}
}
-// Test replies formatting
func TestClientReplies(t *testing.T) {
conn := NewTestingConn()
host := "foohost"
hostname = &host
- client := NewClient(conn)
- nickname := "мойник"
- client.nickname = &nickname
+ client := NewClient(conn, make(chan ClientEvent, 2))
+ defer func() {
+ client.Close()
+ }()
+ client.nickname = "мойник"
client.Reply("hello")
if r := <-conn.outbound; r != ":foohost hello\r\n" {
diff --git a/common_test.go b/common_test.go
index 870852f..9750b86 100644
--- a/common_test.go
+++ b/common_test.go
@@ -18,22 +18,23 @@ along with this program. If not, see .
package main
import (
+ "io"
"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
+ closed chan struct{}
}
func NewTestingConn() *TestingConn {
- inbound := make(chan string, 8)
- outbound := make(chan string, 8)
- return &TestingConn{inbound: inbound, outbound: outbound}
+ return &TestingConn{
+ inbound: make(chan string, 8),
+ outbound: make(chan string, 8),
+ closed: make(chan struct{}),
+ }
}
func (conn TestingConn) Error() string {
@@ -41,14 +42,18 @@ func (conn TestingConn) Error() string {
}
func (conn *TestingConn) Read(b []byte) (n int, err error) {
- msg := <-conn.inbound
- if msg == "" {
- return 0, conn
+ select {
+ case msg := <-conn.inbound:
+ 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{}
@@ -66,7 +71,7 @@ func (conn *TestingConn) Write(b []byte) (n int, err error) {
}
func (conn *TestingConn) Close() error {
- conn.closed = true
+ close(conn.closed)
close(conn.outbound)
return nil
}
diff --git a/daemon.go b/daemon.go
index 4b07e59..ce2dd00 100644
--- a/daemon.go
+++ b/daemon.go
@@ -19,13 +19,8 @@ package main
import (
"fmt"
- "io/ioutil"
"log"
- "net"
- "regexp"
- "sort"
"strings"
- "sync"
"time"
)
@@ -36,289 +31,26 @@ const (
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{}) {
var now time.Time
+ ticker := time.NewTicker(10 * time.Second)
go func() {
- for {
- time.Sleep(10 * time.Second)
+ for range ticker.C {
events <- ClientEvent{eventType: EventTick}
}
}()
- for event := range events {
+EventsCycle:
+ for e := range events {
now = time.Now()
- client := event.client
- switch event.eventType {
+ client := e.client
+ switch e.eventType {
case EventTick:
- clientsM.RLock()
+ clientsLock.RLock()
for c := range clients {
if c.recvTimestamp.Add(PingTimeout).Before(now) {
- log.Println(c, "ping timeout")
+ if *verbose {
+ log.Println(c, "ping timeout")
+ }
c.Close()
continue
}
@@ -327,57 +59,55 @@ func Processor(events chan ClientEvent, finished chan struct{}) {
c.Msg("PING :" + *hostname)
c.sendTimestamp = time.Now()
} else {
- log.Println(c, "ping timeout")
+ if *verbose {
+ log.Println(c, "ping timeout")
+ }
c.Close()
}
}
}
- clientsM.RUnlock()
- roomsM.Lock()
+ clientsLock.RUnlock()
+ roomsLock.Lock()
for rn, r := range rooms {
if *statedir == "" && len(r.members) == 0 {
- log.Println(rn, "emptied room")
+ if *verbose {
+ log.Println(rn, "emptied room")
+ }
delete(rooms, rn)
- close(roomSinks[r])
- delete(roomSinks, r)
+ close(r.events)
}
}
- roomsM.Unlock()
+ roomsLock.Unlock()
case EventTerm:
- roomsM.RLock()
- for _, sink := range roomSinks {
- sink <- ClientEvent{eventType: EventTerm}
- }
- roomsM.RUnlock()
- roomsGroup.Wait()
- close(finished)
- return
+ break EventsCycle
case EventNew:
- clientsM.Lock()
+ clientsLock.Lock()
clients[client] = struct{}{}
- clientsM.Unlock()
+ clientsLock.Unlock()
case EventDel:
- clientsM.Lock()
+ clientsLock.Lock()
delete(clients, client)
- clientsM.Unlock()
- roomsM.RLock()
- for _, roomSink := range roomSinks {
- roomSink <- event
+ clientsLock.Unlock()
+ roomsLock.RLock()
+ for _, r := range rooms {
+ r.events <- e
}
- roomsM.RUnlock()
+ roomsLock.RUnlock()
case EventMsg:
- cols := strings.SplitN(event.text, " ", 2)
+ cols := strings.SplitN(e.text, " ", 2)
cmd := strings.ToUpper(cols[0])
if *verbose {
log.Println(client, "command", cmd)
}
if cmd == "QUIT" {
- log.Println(client, "quit")
client.Close()
+ if *verbose {
+ log.Println(client, "quit")
+ }
continue
}
if !client.registered {
- ClientRegister(client, cmd, cols)
+ client.Register(cmd, cols)
continue
}
if client != nil {
@@ -386,68 +116,83 @@ func Processor(events chan ClientEvent, finished chan struct{}) {
switch cmd {
case "AWAY":
if len(cols) == 1 {
- client.away = nil
+ client.away = ""
client.ReplyNicknamed("305", "You are no longer marked as being away")
continue
}
- msg := strings.TrimLeft(cols[1], ":")
- client.away = &msg
+ client.away = strings.TrimLeft(cols[1], ":")
client.ReplyNicknamed("306", "You have been marked as being away")
case "JOIN":
if len(cols) == 1 || len(cols[1]) < 1 {
client.ReplyNotEnoughParameters("JOIN")
continue
}
- HandlerJoin(client, cols[1])
+ client.Join(cols[1])
case "LIST":
- SendList(client, cols)
+ client.SendList(cols)
case "LUSERS":
- SendLusers(client)
+ client.SendLusers()
case "MODE":
if len(cols) == 1 || len(cols[1]) < 1 {
client.ReplyNotEnoughParameters("MODE")
continue
}
cols = strings.SplitN(cols[1], " ", 2)
- if cols[0] == *client.username {
- if len(cols) == 1 {
- client.Msg("221 " + *client.nickname + " +")
- } else {
- client.ReplyNicknamed("501", "Unknown MODE flag")
- }
+ if cols[0] == client.username {
+ client.Msg("221 " + client.nickname + " +w")
+ // client.ReplyNicknamed("501", "Unknown MODE flag")
continue
}
room := cols[0]
- roomsM.RLock()
r, found := rooms[room]
if !found {
client.ReplyNoChannel(room)
- roomsM.RUnlock()
continue
}
if len(cols) == 1 {
- roomSinks[r] <- ClientEvent{client, EventMode, ""}
+ r.events <- ClientEvent{client, EventMode, ""}
} else {
- roomSinks[r] <- ClientEvent{client, EventMode, cols[1]}
+ r.events <- ClientEvent{client, EventMode, cols[1]}
}
- roomsM.RUnlock()
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":
if len(cols) == 1 || len(cols[1]) < 1 {
client.ReplyNotEnoughParameters("PART")
continue
}
rs := strings.Split(cols[1], " ")[0]
- roomsM.RLock()
+ roomsLock.RLock()
for _, room := range strings.Split(rs, ",") {
if r, found := rooms[room]; found {
- roomSinks[r] <- ClientEvent{client, EventDel, ""}
+ r.events <- ClientEvent{client, EventDel, ""}
} else {
client.ReplyNoChannel(room)
}
}
- roomsM.RUnlock()
+ roomsLock.RUnlock()
case "PING":
if len(cols) == 1 {
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")
continue
}
- msg := ""
target := strings.ToLower(cols[0])
- clientsM.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()
+ roomsLock.RLock()
if r, found := rooms[target]; found {
- roomSinks[r] <- ClientEvent{
+ r.events <- ClientEvent{
client,
EventMsg,
cmd + " " + strings.TrimLeft(cols[1], ":"),
}
- } else {
- client.ReplyNoNickChan(target)
+ roomsLock.RUnlock()
+ 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":
if len(cols) == 1 {
client.ReplyNotEnoughParameters("TOPIC")
continue
}
cols = strings.SplitN(cols[1], " ", 2)
- roomsM.RLock()
r, found := rooms[cols[0]]
- roomsM.RUnlock()
if !found {
client.ReplyNoChannel(cols[0])
continue
@@ -510,25 +255,20 @@ func Processor(events chan ClientEvent, finished chan struct{}) {
var change string
if len(cols) > 1 {
change = cols[1]
- } else {
- change = ""
}
- roomsM.RLock()
- roomSinks[r] <- ClientEvent{client, EventTopic, change}
- roomsM.RUnlock()
+ r.events <- ClientEvent{client, EventTopic, change}
case "WHO":
if len(cols) == 1 || len(cols[1]) < 1 {
client.ReplyNotEnoughParameters("WHO")
continue
}
room := strings.Split(cols[1], " ")[0]
- roomsM.RLock()
- if r, found := rooms[room]; found {
- roomSinks[r] <- ClientEvent{client, EventWho, ""}
+ r, found := rooms[room]
+ if found {
+ r.events <- ClientEvent{client, EventWho, ""}
} else {
client.ReplyNoChannel(room)
}
- roomsM.RUnlock()
case "WHOIS":
if len(cols) == 1 || len(cols[1]) < 1 {
client.ReplyNotEnoughParameters("WHOIS")
@@ -536,31 +276,51 @@ func Processor(events chan ClientEvent, finished chan struct{}) {
}
cols := strings.Split(cols[1], " ")
nicknames := strings.Split(cols[len(cols)-1], ",")
- SendWhois(client, nicknames)
+ client.SendWhois(nicknames)
case "ISON":
if len(cols) == 1 || len(cols[1]) < 1 {
client.ReplyNotEnoughParameters("ISON")
continue
}
- nicksKnown := make(map[string]struct{})
- clientsM.RLock()
- for c := range clients {
- nicksKnown[*c.nickname] = struct{}{}
+ nicknamesList := strings.Split(cols[1], " ")
+ nicknames := make(map[string]bool, len(nicknamesList))
+ for _, nickname := range nicknamesList {
+ nicknames[nickname] = false
}
- clientsM.RUnlock()
- var nicksExists []string
- for _, nickname := range strings.Split(cols[1], " ") {
- if _, exists := nicksKnown[nickname]; exists {
- nicksExists = append(nicksExists, nickname)
+ clientsLock.RLock()
+ for c := range clients {
+ if _, exists := nicknames[c.nickname]; exists {
+ nicknames[c.nickname] = true
}
}
- 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":
var debug string
if *verbose {
debug = "debug"
- } else {
- debug = ""
}
client.ReplyNicknamed("351", fmt.Sprintf("%s.%s %s :", Version, debug, *hostname))
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)
}
diff --git a/daemon_test.go b/daemon_test.go
index 599cbf6..76d6203 100644
--- a/daemon_test.go
+++ b/daemon_test.go
@@ -28,13 +28,14 @@ func TestRegistrationWorkflow(t *testing.T) {
host := "foohost"
hostname = &host
events := make(chan ClientEvent)
+ finished := make(chan struct{})
defer func() {
events <- ClientEvent{eventType: EventTerm}
+ <-finished
}()
- go Processor(events, make(chan struct{}))
+ go Processor(events, finished)
conn := NewTestingConn()
- client := NewClient(conn)
- go client.Processor(events)
+ client := NewClient(conn, events)
conn.inbound <- "UNEXISTENT CMD" // should receive nothing on this
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" {
t.Fatal("461 for USER", r)
}
- if (*client.nickname != "meinick") || client.registered {
+ if (client.nickname != "meinick") || client.registered {
t.Fatal("NICK saved")
}
@@ -63,7 +64,7 @@ func TestRegistrationWorkflow(t *testing.T) {
t.Fatal("461 again for USER", r)
}
- SendLusers(client)
+ client.SendLusers()
if r := <-conn.outbound; !strings.Contains(r, "There are 0 users") {
t.Fatal("LUSERS", r)
}
@@ -87,7 +88,7 @@ func TestRegistrationWorkflow(t *testing.T) {
if r := <-conn.outbound; !strings.Contains(r, ":foohost 422") {
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")
}
@@ -98,7 +99,7 @@ func TestRegistrationWorkflow(t *testing.T) {
t.Fatal("reply for unexistent command", r)
}
- SendLusers(client)
+ client.SendLusers()
if r := <-conn.outbound; !strings.Contains(r, "There are 1 users") {
t.Fatal("1 users logged in", r)
}
@@ -122,11 +123,14 @@ func TestMotd(t *testing.T) {
conn := NewTestingConn()
host := "foohost"
hostname = &host
- client := NewClient(conn)
+ client := NewClient(conn, make(chan ClientEvent, 2))
+ defer func() {
+ client.Close()
+ }()
motdName := fd.Name()
motd = &motdName
- SendMotd(client)
+ client.SendMotd()
if r := <-conn.outbound; !strings.HasPrefix(r, ":foohost 375") {
t.Fatal("MOTD start", r)
}
diff --git a/events.go b/events.go
deleted file mode 100644
index 283b5f7..0000000
--- a/events.go
+++ /dev/null
@@ -1,119 +0,0 @@
-/*
-goircd -- minimalistic simple Internet Relay Chat (IRC) server
-Copyright (C) 2014-2020 Sergey Matveev
-
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, version 3 of the License.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program. If not, see .
-*/
-
-package main
-
-import (
- "fmt"
- "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)
- }
- }
-}
diff --git a/goircd.go b/goircd.go
index deb6e35..f8aad49 100644
--- a/goircd.go
+++ b/goircd.go
@@ -23,114 +23,213 @@ import (
"io/ioutil"
"log"
"net"
+ "os"
+ "os/signal"
"path"
"path/filepath"
+ "strconv"
"strings"
+ "syscall"
)
-const Version = "1.8.2"
+const (
+ Version = "1.9.0"
-var (
- hostname = flag.String("hostname", "localhost", "Hostname")
- bind = flag.String("bind", ":6667", "Address to bind to")
- motd = flag.String("motd", "", "Path to MOTD file")
- logdir = flag.String("logdir", "", "Absolute path to directory for logs")
- statedir = flag.String("statedir", "", "Absolute path to directory for states")
- passwords = flag.String("passwords", "", "Optional path to passwords file")
- tlsBind = flag.String("tlsbind", "", "TLS address to bind to")
- tlsPEM = flag.String("tlspem", "", "Path to TLS certificat+key PEM file")
- verbose = flag.Bool("v", false, "Enable verbose logging.")
+ StateTopicFilename = "topic"
+ StateKeyFilename = "key"
+
+ EventNew = iota
+ EventDel = iota
+ EventMsg = iota
+ EventTopic = iota
+ EventWho = iota
+ EventMode = iota
+ EventTerm = iota
+ EventTick = iota
)
-func listenerLoop(sock net.Listener, events chan ClientEvent) {
- for {
- conn, err := sock.Accept()
- if err != nil {
- log.Println("Error during accepting connection", err)
- continue
- }
- client := NewClient(conn)
- go client.Processor(events)
- }
+type ClientEvent struct {
+ client *Client
+ eventType int
+ text string
}
-func Run() {
- events := make(chan ClientEvent)
- log.SetFlags(log.Ldate | log.Lmicroseconds | log.Lshortfile)
+type StateEvent struct {
+ where string
+ topic string
+ key string
+}
- 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)
- log.Println(*logdir, "logger initialized")
- }
+var (
+ hostname = flag.String("hostname", "localhost", "hostname")
+ bind = flag.String("bind", "[::1]:6667", "address to bind to")
+ 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")
+ statedir = flag.String("statedir", "", "absolute path to directory for states")
+ passwords = flag.String("passwd", "", "optional path to passwords file")
+ tlsBind = flag.String("tlsbind", "", "TLS address to bind to")
+ tlsPEM = flag.String("tlspem", "", "path to TLS certificat+key PEM file")
+ permStateDirS = flag.String("perm-state-dir", "755", "state directory permissions")
+ 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")
- 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(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")
- }
+ permStateDir os.FileMode
+ permStateFile os.FileMode
+ permLogFile os.FileMode
- if *bind != "" {
- listener, err := net.Listen("tcp", *bind)
- if err != nil {
- log.Fatalf("Can not listen on %s: %v", *bind, err)
- }
- log.Println("Raw listening on", *bind)
- go listenerLoop(listener, events)
+ stateSink chan StateEvent = make(chan StateEvent)
+)
+
+func permParse(s string) os.FileMode {
+ r, err := strconv.ParseUint(s, 8, 16)
+ if err != nil {
+ log.Fatalln(err)
}
- if *tlsBind != "" {
- cert, err := tls.LoadX509KeyPair(*tlsPEM, *tlsPEM)
+ return os.FileMode(r)
+}
+
+func listenerLoop(ln net.Listener, events chan ClientEvent) {
+ for {
+ conn, err := ln.Accept()
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}}
- 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)
+ NewClient(conn, events)
}
- Processor(events, make(chan struct{}))
}
func main() {
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
}
diff --git a/log.go b/log.go
new file mode 100644
index 0000000..08367cb
--- /dev/null
+++ b/log.go
@@ -0,0 +1,72 @@
+/*
+goircd -- minimalistic simple Internet Relay Chat (IRC) server
+Copyright (C) 2014-2020 Sergey Matveev
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, version 3 of the License.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+*/
+
+package main
+
+import (
+ "fmt"
+ "log"
+ "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)
+ }
+ }
+}
diff --git a/room.go b/room.go
index 76b4c42..cc2b0d7 100644
--- a/room.go
+++ b/room.go
@@ -26,225 +26,213 @@ import (
"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 {
- name *string
- topic *string
- key *string
+ name string
+ topic string
+ key string
members map[*Client]struct{}
+ events chan ClientEvent
sync.RWMutex
}
-func (room *Room) String() (name string) {
- room.RLock()
- name = *room.name
- room.RUnlock()
- return
-}
+var (
+ RERoom = regexp.MustCompile("^#[^\x00\x07\x0a\x0d ,:/]{1,200}$")
-func NewRoom(name string) *Room {
- topic := ""
- key := ""
- return &Room{
- name: &name,
- topic: &topic,
- key: &key,
- members: make(map[*Client]struct{}),
- }
-}
+ rooms map[string]*Room = make(map[string]*Room)
+ roomsLock sync.RWMutex
+ roomsWG sync.WaitGroup
+)
-func (room *Room) SendTopic(client *Client) {
- room.RLock()
- if *room.topic == "" {
- client.ReplyNicknamed("331", room.String(), "No topic is set")
+func (r *Room) SendTopic(c *Client) {
+ t := r.topic
+ if t == "" {
+ c.ReplyNicknamed("331", r.name, "No topic is set")
} 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 (room *Room) Broadcast(msg string, clientToIgnore ...*Client) {
- room.RLock()
- for member := range room.members {
- if (len(clientToIgnore) > 0) && member == clientToIgnore[0] {
+func (r *Room) SendNames(c *Client) {
+ allowed := false
+ if r.key == "" {
+ allowed = true
+ } 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
}
member.Msg(msg)
}
- room.RUnlock()
+ r.RUnlock()
}
-func (room *Room) StateSave() {
- room.RLock()
- stateSink <- StateEvent{room.String(), *room.topic, *room.key}
- room.RUnlock()
+func (r *Room) StateSave() {
+ stateSink <- StateEvent{r.name, r.topic, r.key}
}
-func (room *Room) Processor(events <-chan ClientEvent) {
- var client *Client
- for event := range events {
- client = event.client
- switch event.eventType {
+func (r *Room) Processor(events <-chan ClientEvent) {
+ for e := range events {
+ c := e.client
+ switch e.eventType {
case EventTerm:
- roomsGroup.Done()
+ roomsWG.Done()
return
case EventNew:
- room.Lock()
- room.members[client] = struct{}{}
+ r.Lock()
+ r.members[c] = struct{}{}
+ r.Unlock()
if *verbose {
- log.Println(client, "joined", room.name)
+ log.Println(c, "joined", r.name)
}
- room.Unlock()
- room.SendTopic(client)
- room.Broadcast(fmt.Sprintf(":%s JOIN %s", client, room.String()))
- logSink <- LogEvent{room.String(), *client.nickname, "joined", true}
- 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")
+ r.SendTopic(c)
+ r.Broadcast(fmt.Sprintf(":%s JOIN %s", c, r.name))
+ logSink <- LogEvent{r.name, c.nickname, "joined", true}
+ r.SendNames(c)
case EventDel:
- room.RLock()
- if _, subscribed := room.members[client]; !subscribed {
- client.ReplyNicknamed("442", room.String(), "You are not on that channel")
- room.RUnlock()
+ if _, subscribed := r.members[c]; !subscribed {
+ c.ReplyNicknamed("442", r.name, "You are not on that channel")
continue
}
- room.RUnlock()
- room.Lock()
- delete(room.members, client)
- room.Unlock()
- room.RLock()
- msg := fmt.Sprintf(":%s PART %s :%s", client, room.String(), *client.nickname)
- room.Broadcast(msg)
- logSink <- LogEvent{room.String(), *client.nickname, "left", true}
- room.RUnlock()
+ msg := fmt.Sprintf(":%s PART %s :%s", c, r.name, c.nickname)
+ r.Broadcast(msg)
+ r.Lock()
+ delete(r.members, c)
+ r.Unlock()
+ logSink <- LogEvent{r.name, c.nickname, "left", true}
+ if *verbose {
+ log.Println(c, "left", r.name)
+ }
case EventTopic:
- room.RLock()
- if _, subscribed := room.members[client]; !subscribed {
- client.ReplyParts("442", room.String(), "You are not on that channel")
- room.RUnlock()
+ if _, subscribed := r.members[c]; !subscribed {
+ c.ReplyParts("442", r.name, "You are not on that channel")
continue
}
- if event.text == "" {
- room.SendTopic(client)
- room.RUnlock()
+ if e.text == "" {
+ r.SendTopic(c)
continue
}
- room.RUnlock()
- topic := strings.TrimLeft(event.text, ":")
- room.Lock()
- room.topic = &topic
- room.Unlock()
- room.RLock()
- 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()
+ topic := strings.TrimLeft(e.text, ":")
+ r.topic = topic
+ msg := fmt.Sprintf(":%s TOPIC %s :%s", c, r.name, r.topic)
+ r.Broadcast(msg)
+ logSink <- LogEvent{r.name, c.nickname, "set topic to " + r.topic, true}
+ r.StateSave()
case EventWho:
- room.RLock()
- for m := range room.members {
- client.ReplyNicknamed(
+ r.RLock()
+ for m := range r.members {
+ c.ReplyNicknamed(
"352",
- room.String(),
- *m.username,
+ r.name,
+ m.username,
m.Host(),
*hostname,
- *m.nickname,
+ m.nickname,
"H",
- "0 "+*m.realname,
+ "0 "+m.realname,
)
}
- client.ReplyNicknamed("315", room.String(), "End of /WHO list")
- room.RUnlock()
+ c.ReplyNicknamed("315", r.name, "End of /WHO list")
+ r.RUnlock()
case EventMode:
- room.RLock()
- if event.text == "" {
- mode := "+"
- if *room.key != "" {
+ if e.text == "" {
+ mode := "+n"
+ if r.key != "" {
mode = mode + "k"
}
- client.Msg(fmt.Sprintf("324 %s %s %s", *client.nickname, room.String(), mode))
- room.RUnlock()
+ c.Msg(fmt.Sprintf("324 %s %s %s", c.nickname, r.name, mode))
continue
}
- if strings.HasPrefix(event.text, "b") {
- client.ReplyNicknamed("368", room.String(), "End of channel ban list")
- room.RUnlock()
+ if strings.HasPrefix(e.text, "b") {
+ c.ReplyNicknamed("368", r.name, "End of channel ban list")
continue
}
- if strings.HasPrefix(event.text, "-k") || strings.HasPrefix(event.text, "+k") {
- if _, subscribed := room.members[client]; !subscribed {
- client.ReplyParts("442", room.String(), "You are not on that channel")
- room.RUnlock()
+ if strings.HasPrefix(e.text, "-k") || strings.HasPrefix(e.text, "+k") {
+ if _, subscribed := r.members[c]; !subscribed {
+ c.ReplyParts("442", r.name, "You are not on that channel")
continue
}
} else {
- client.ReplyNicknamed("472", event.text, "Unknown MODE flag")
- room.RUnlock()
+ c.ReplyNicknamed("472", e.text, "Unknown MODE flag")
continue
}
- room.RUnlock()
var msg string
var msgLog string
- if strings.HasPrefix(event.text, "+k") {
- cols := strings.Split(event.text, " ")
+ if strings.HasPrefix(e.text, "+k") {
+ cols := strings.Split(e.text, " ")
if len(cols) == 1 {
- client.ReplyNotEnoughParameters("MODE")
+ c.ReplyNotEnoughParameters("MODE")
continue
}
- room.Lock()
- room.key = &cols[1]
- msg = fmt.Sprintf(":%s MODE %s +k %s", client, *room.name, *room.key)
- msgLog = "set channel key to " + *room.key
- room.Unlock()
+ r.key = cols[1]
+ msg = fmt.Sprintf(":%s MODE %s +k %s", c, r.name, r.key)
+ msgLog = "set channel key"
} else {
- key := ""
- room.Lock()
- room.key = &key
- msg = fmt.Sprintf(":%s MODE %s -k", client, *room.name)
- room.Unlock()
+ r.key = ""
+ msg = fmt.Sprintf(":%s MODE %s -k", c, r.name)
msgLog = "removed channel key"
}
- room.Broadcast(msg)
- logSink <- LogEvent{room.String(), *client.nickname, msgLog, true}
- room.StateSave()
+ r.Broadcast(msg)
+ logSink <- LogEvent{r.name, c.nickname, msgLog, true}
+ r.StateSave()
case EventMsg:
- sep := strings.Index(event.text, " ")
- room.Broadcast(fmt.Sprintf(
- ":%s %s %s :%s",
- client,
- event.text[:sep],
- room.String(),
- event.text[sep+1:]),
- client,
- )
- logSink <- LogEvent{
- room.String(),
- *client.nickname,
- event.text[sep+1:],
- false,
- }
+ sep := strings.Index(e.text, " ")
+ r.Broadcast(fmt.Sprintf(
+ ":%s %s %s :%s", c, e.text[:sep], r.name, e.text[sep+1:],
+ ), c)
+ logSink <- LogEvent{r.name, c.nickname, e.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
+}
diff --git a/room_test.go b/room_test.go
index 9694818..97c202b 100644
--- a/room_test.go
+++ b/room_test.go
@@ -46,10 +46,9 @@ func TestTwoUsers(t *testing.T) {
host := "foohost"
hostname = &host
events := make(chan ClientEvent)
- roomsM.Lock()
+ roomsLock.Lock()
rooms = make(map[string]*Room)
- roomSinks = make(map[*Room]chan ClientEvent)
- roomsM.Unlock()
+ roomsLock.Unlock()
clients = make(map[*Client]struct{})
finished := make(chan struct{})
go Processor(events, finished)
@@ -60,10 +59,8 @@ func TestTwoUsers(t *testing.T) {
conn1 := NewTestingConn()
conn2 := NewTestingConn()
- client1 := NewClient(conn1)
- client2 := NewClient(conn2)
- go client1.Processor(events)
- go client2.Processor(events)
+ client1 := NewClient(conn1, events)
+ NewClient(conn2, events)
conn1.inbound <- "NICK nick1\r\nUSER foo1 bar1 baz1 :Long name1"
conn2.inbound <- "NICK nick2\r\nUSER foo2 bar2 baz2 :Long name2"
@@ -72,7 +69,7 @@ func TestTwoUsers(t *testing.T) {
<-conn2.outbound
}
- SendLusers(client1)
+ client1.SendLusers()
if r := <-conn1.outbound; !strings.Contains(r, "There are 2 users") {
t.Fatal("LUSERS", r)
}
@@ -139,7 +136,6 @@ func TestJoin(t *testing.T) {
events := make(chan ClientEvent)
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() {
@@ -147,8 +143,7 @@ func TestJoin(t *testing.T) {
<-finished
}()
conn := NewTestingConn()
- client := NewClient(conn)
- go client.Processor(events)
+ NewClient(conn, events)
conn.inbound <- "NICK nick2\r\nUSER foo2 bar2 baz2 :Long name2"
for i := 0; i < 6; i++ {
@@ -183,14 +178,14 @@ func TestJoin(t *testing.T) {
for i := 0; i < 4*2; i++ {
<-conn.outbound
}
- roomsM.RLock()
+ roomsLock.RLock()
if _, ok := rooms["#bar"]; !ok {
t.Fatal("#bar does not exist")
}
if _, ok := rooms["#baz"]; !ok {
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) {
t.Fatal("invalid join log event #bar", r)
}
@@ -202,14 +197,14 @@ func TestJoin(t *testing.T) {
for i := 0; i < 4*2; i++ {
<-conn.outbound
}
- roomsM.RLock()
- if *rooms["#barenc"].key != "key1" {
+ roomsLock.RLock()
+ if rooms["#barenc"].key != "key1" {
t.Fatal("no room with key1")
}
- if *rooms["#bazenc"].key != "key2" {
+ if rooms["#bazenc"].key != "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) {
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" {
t.Fatal("remove #barenc key", r)
}
- roomsM.RLock()
- if *rooms["#barenc"].key != "" {
+ roomsLock.RLock()
+ if rooms["#barenc"].key != "" {
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) {
t.Fatal("removed channel key log", r)
}
@@ -240,6 +235,9 @@ func TestJoin(t *testing.T) {
}
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" {
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" {
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)
}
if r := <-stateSink; (r.topic != "") || (r.where != "#barenc") || (r.key != "newkey") {