]> Cypherpunks.ru repositories - goircd.git/commitdiff
Many fixes and additions v1.9.0
authorSergey Matveev <stargrave@stargrave.org>
Fri, 6 Nov 2020 17:10:06 +0000 (20:10 +0300)
committerSergey Matveev <stargrave@stargrave.org>
Sat, 7 Nov 2020 13:52:53 +0000 (16:52 +0300)
* 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

13 files changed:
INSTALL
README
clean.do [new file with mode: 0644]
client.go
client_test.go
common_test.go
daemon.go
daemon_test.go
events.go [deleted file]
goircd.go
log.go [new file with mode: 0644]
room.go
room_test.go

diff --git a/INSTALL b/INSTALL
index 3c5c63420328e3b6fc35715651366212b449ba99..f3d979ca5bb003071bba41a8e7a8ba16912e0cc9 100644 (file)
--- a/INSTALL
+++ b/INSTALL
@@ -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
-* or manually:
+    $ go get go.cypherpunks.ru/goircd
+
+or manually:
 
     $ git clone git://git.cypherpunks.ru/goircd.git
     $ cd goircd
 
     $ git clone git://git.cypherpunks.ru/goircd.git
     $ cd goircd
diff --git a/README b/README
index 663aaf30096e91a020217eded12b328025acdc3f..e9c5eb110a785cce6b2886f68a83cf0ec7dfae63 100644 (file)
--- 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
 
 * 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:
 * 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
 * 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)
 * 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:
 
 
 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
 
 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:
 
 * 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
 
 
 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:
 
 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
 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
 
 
 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
 
 
 LICENCE
 
diff --git a/clean.do b/clean.do
new file mode 100644 (file)
index 0000000..34df69f
--- /dev/null
+++ b/clean.do
@@ -0,0 +1 @@
+rm -f goircd
index 2c8c0ad6721fb04a828029329a0ab607d74beef3..6a21ab0ce7d415edb05bf3f062f4546e010c23e4 100644 (file)
--- a/client.go
+++ b/client.go
@@ -19,8 +19,15 @@ package main
 
 import (
        "bytes"
 
 import (
        "bytes"
+       "crypto/sha256"
+       "crypto/subtle"
+       "encoding/hex"
+       "fmt"
+       "io/ioutil"
        "log"
        "net"
        "log"
        "net"
+       "regexp"
+       "sort"
        "strings"
        "sync"
        "time"
        "strings"
        "sync"
        "time"
@@ -28,29 +35,37 @@ import (
 
 const (
        BufSize   = 1500
 
 const (
        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 {
        conn          net.Conn
        registered    bool
 )
 
 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
        recvTimestamp time.Time
        sendTimestamp time.Time
-       outBuf        chan *string
+       outBuf        chan string
        alive         bool
        sync.Mutex
 }
 
 func (c *Client) Host() 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
        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 {
 }
 
 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,
        c := Client{
                conn:          conn,
-               nickname:      &nickname,
-               username:      &username,
+               nickname:      "*",
+               username:      "",
                recvTimestamp: time.Now(),
                sendTimestamp: time.Now(),
                alive:         true,
                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.MsgSender()
+       go c.Processor(events)
        return &c
 }
 
        return &c
 }
 
-func (c *Client) SetDead() {
-       c.outBuf <- nil
-       c.alive = false
-}
-
 func (c *Client) Close() {
        c.Lock()
        if c.alive {
 func (c *Client) Close() {
        c.Lock()
        if c.alive {
-               c.SetDead()
+               close(c.outBuf)
+               c.alive = false
        }
        c.Unlock()
 }
 
        }
        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)
        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 {
        var err error
        for {
                if prev == BufSize {
@@ -120,26 +129,43 @@ func (c *Client) Processor(sink chan ClientEvent) {
                if i == -1 {
                        continue
                }
                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()
                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() {
 }
 
 func (c *Client) MsgSender() {
+       var err error
        for msg := range c.outBuf {
        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
                }
                        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()
 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 {
        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
        }
                }
                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)
 }
 
 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 {
 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, " "))
 }
 
        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) {
        c.ReplyNicknamed("461", command, "Not enough parameters")
 }
 
 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")
 }
 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) 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")
+               }
+       }
+}
index 4af1dd34f521348769c3cc9976dd62a87ed52f1d..ed71da819be899f950006039f0bdb68d38729400 100644 (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) {
        conn := NewTestingConn()
 func TestNewClient(t *testing.T) {
        conn := NewTestingConn()
-       sink := make(chan ClientEvent)
+       events := make(chan ClientEvent)
        host := "foohost"
        hostname = &host
        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"
        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"
        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 <- ""
        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)
        }
 }
 
        if event.eventType != EventDel {
                t.Fatal("no client termination", event)
        }
 }
 
-// Test replies formatting
 func TestClientReplies(t *testing.T) {
        conn := NewTestingConn()
        host := "foohost"
        hostname = &host
 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" {
 
        client.Reply("hello")
        if r := <-conn.outbound; r != ":foohost hello\r\n" {
index 870852f6181d0a93d72f8bcfd732e11c99ea508d..9750b86c36cf926af681e0fbe0b83017e00e6302 100644 (file)
@@ -18,22 +18,23 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 package main
 
 import (
 package main
 
 import (
+       "io"
        "net"
        "time"
 )
 
        "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
 type TestingConn struct {
        inbound  chan string
        outbound chan string
-       closed   bool
+       closed   chan struct{}
 }
 
 func NewTestingConn() *TestingConn {
 }
 
 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 {
 }
 
 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
-       if msg == "" {
-               return 0, conn
-       }
-       for n, bt := range append([]byte(msg), CRLF...) {
-               b[n] = bt
+       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
        }
        }
-       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)
        return nil
 }
        close(conn.outbound)
        return nil
 }
index 4b07e59fcdcfd4357515cd62cc42cfde889dc8de..ce2dd000d2ae1bcd59a92cae8eeb0ca18a70ea79 100644 (file)
--- a/daemon.go
+++ b/daemon.go
@@ -19,13 +19,8 @@ package main
 
 import (
        "fmt"
 
 import (
        "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{}) {
        var now time.Time
 func Processor(events chan ClientEvent, finished chan struct{}) {
        var now time.Time
+       ticker := time.NewTicker(10 * time.Second)
        go func() {
        go func() {
-               for {
-                       time.Sleep(10 * time.Second)
+               for range ticker.C {
                        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
-               switch event.eventType {
+               client := e.client
+               switch e.eventType {
                case EventTick:
                case EventTick:
-                       clientsM.RLock()
+                       clientsLock.RLock()
                        for c := range clients {
                                if c.recvTimestamp.Add(PingTimeout).Before(now) {
                        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
                                }
                                        c.Close()
                                        continue
                                }
@@ -327,57 +59,55 @@ func Processor(events chan ClientEvent, finished chan struct{}) {
                                                c.Msg("PING :" + *hostname)
                                                c.sendTimestamp = time.Now()
                                        } else {
                                                c.Msg("PING :" + *hostname)
                                                c.sendTimestamp = time.Now()
                                        } else {
-                                               log.Println(c, "ping timeout")
+                                               if *verbose {
+                                                       log.Println(c, "ping timeout")
+                                               }
                                                c.Close()
                                        }
                                }
                        }
                                                c.Close()
                                        }
                                }
                        }
-                       clientsM.RUnlock()
-                       roomsM.Lock()
+                       clientsLock.RUnlock()
+                       roomsLock.Lock()
                        for rn, r := range rooms {
                                if *statedir == "" && len(r.members) == 0 {
                        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)
                                        delete(rooms, rn)
-                                       close(roomSinks[r])
-                                       delete(roomSinks, r)
+                                       close(r.events)
                                }
                        }
                                }
                        }
-                       roomsM.Unlock()
+                       roomsLock.Unlock()
                case EventTerm:
                case EventTerm:
-                       roomsM.RLock()
-                       for _, sink := range roomSinks {
-                               sink <- ClientEvent{eventType: EventTerm}
-                       }
-                       roomsM.RUnlock()
-                       roomsGroup.Wait()
-                       close(finished)
-                       return
+                       break EventsCycle
                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()
-                       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:
                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" {
                        cmd := strings.ToUpper(cols[0])
                        if *verbose {
                                log.Println(client, "command", cmd)
                        }
                        if cmd == "QUIT" {
-                               log.Println(client, "quit")
                                client.Close()
                                client.Close()
+                               if *verbose {
+                                       log.Println(client, "quit")
+                               }
                                continue
                        }
                        if !client.registered {
                                continue
                        }
                        if !client.registered {
-                               ClientRegister(client, cmd, cols)
+                               client.Register(cmd, cols)
                                continue
                        }
                        if client != nil {
                                continue
                        }
                        if client != nil {
@@ -386,68 +116,83 @@ func Processor(events chan ClientEvent, finished chan struct{}) {
                        switch cmd {
                        case "AWAY":
                                if len(cols) == 1 {
                        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
                                }
                                        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
                                }
                                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":
                        case "LIST":
-                               SendList(client, cols)
+                               client.SendList(cols)
                        case "LUSERS":
                        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)
                        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]
                                        continue
                                }
                                room := cols[0]
-                               roomsM.RLock()
                                r, found := rooms[room]
                                if !found {
                                        client.ReplyNoChannel(room)
                                r, found := rooms[room]
                                if !found {
                                        client.ReplyNoChannel(room)
-                                       roomsM.RUnlock()
                                        continue
                                }
                                if len(cols) == 1 {
                                        continue
                                }
                                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":
                                if len(cols) == 1 || len(cols[1]) < 1 {
                                        client.ReplyNotEnoughParameters("PART")
                                        continue
                                }
                                rs := strings.Split(cols[1], " ")[0]
                        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 {
                                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)
                                        }
                                }
                                        } else {
                                                client.ReplyNoChannel(room)
                                        }
                                }
-                               roomsM.RUnlock()
+                               roomsLock.RUnlock()
                        case "PING":
                                if len(cols) == 1 {
                                        client.ReplyNicknamed("409", "No origin specified")
                        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
                                }
                                        client.ReplyNicknamed("412", "No text to send")
                                        continue
                                }
-                               msg := ""
                                target := strings.ToLower(cols[0])
                                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 {
                                if r, found := rooms[target]; found {
-                                       roomSinks[r] <- ClientEvent{
+                                       r.events <- ClientEvent{
                                                client,
                                                EventMsg,
                                                cmd + " " + strings.TrimLeft(cols[1], ":"),
                                        }
                                                client,
                                                EventMsg,
                                                cmd + " " + strings.TrimLeft(cols[1], ":"),
                                        }
-                               } else {
-                                       client.ReplyNoNickChan(target)
+                                       roomsLock.RUnlock()
+                                       continue
+                               }
+                               roomsLock.RUnlock()
+                               var msg string
+                               clientsLock.RLock()
+                               for c := range clients {
+                                       if c.nickname != target {
+                                               continue
+                                       }
+                                       msg = fmt.Sprintf(":%s %s %s %s", client, cmd, c.nickname, cols[1])
+                                       c.Msg(msg)
+                                       if c.away != "" {
+                                               client.ReplyNicknamed("301", c.nickname, c.away)
+                                       }
+                                       break
+                               }
+                               clientsLock.RUnlock()
+                               if msg != "" {
+                                       continue
                                }
                                }
-                               roomsM.RUnlock()
+                               client.ReplyNoNickChan(target)
                        case "TOPIC":
                                if len(cols) == 1 {
                                        client.ReplyNotEnoughParameters("TOPIC")
                                        continue
                                }
                                cols = strings.SplitN(cols[1], " ", 2)
                        case "TOPIC":
                                if len(cols) == 1 {
                                        client.ReplyNotEnoughParameters("TOPIC")
                                        continue
                                }
                                cols = strings.SplitN(cols[1], " ", 2)
-                               roomsM.RLock()
                                r, found := rooms[cols[0]]
                                r, found := rooms[cols[0]]
-                               roomsM.RUnlock()
                                if !found {
                                        client.ReplyNoChannel(cols[0])
                                        continue
                                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]
                                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]
                        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)
                                }
                                } else {
                                        client.ReplyNoChannel(room)
                                }
-                               roomsM.RUnlock()
                        case "WHOIS":
                                if len(cols) == 1 || len(cols[1]) < 1 {
                                        client.ReplyNotEnoughParameters("WHOIS")
                        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], ",")
                                }
                                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
                                }
                        case "ISON":
                                if len(cols) == 1 || len(cols[1]) < 1 {
                                        client.ReplyNotEnoughParameters("ISON")
                                        continue
                                }
-                               nicksKnown := make(map[string]struct{})
-                               clientsM.RLock()
+                               nicknamesList := strings.Split(cols[1], " ")
+                               nicknames := make(map[string]bool, len(nicknamesList))
+                               for _, nickname := range nicknamesList {
+                                       nicknames[nickname] = false
+                               }
+                               clientsLock.RLock()
                                for c := range clients {
                                for c := range clients {
-                                       nicksKnown[*c.nickname] = struct{}{}
+                                       if _, exists := nicknames[c.nickname]; exists {
+                                               nicknames[c.nickname] = true
+                                       }
                                }
                                }
-                               clientsM.RUnlock()
-                               var nicksExists []string
-                               for _, nickname := range strings.Split(cols[1], " ") {
-                                       if _, exists := nicksKnown[nickname]; exists {
-                                               nicksExists = append(nicksExists, nickname)
+                               clientsLock.RUnlock()
+                               nicknamesList = nicknamesList[:0]
+                               for n, exists := range nicknames {
+                                       if exists {
+                                               nicknamesList = append(nicknamesList, n)
                                        }
                                }
                                        }
                                }
-                               client.ReplyNicknamed("303", strings.Join(nicksExists, " "))
+                               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"
                        case "VERSION":
                                var debug string
                                if *verbose {
                                        debug = "debug"
-                               } else {
-                                       debug = ""
                                }
                                client.ReplyNicknamed("351", fmt.Sprintf("%s.%s %s :", Version, debug, *hostname))
                        default:
                                }
                                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)
 }
 }
index 599cbf615376f52813a2721154f7f31b7f7e0c09..76d620305dcf83725ef3e43f7d68e21028f4b6d3 100644 (file)
@@ -28,13 +28,14 @@ func TestRegistrationWorkflow(t *testing.T) {
        host := "foohost"
        hostname = &host
        events := make(chan ClientEvent)
        host := "foohost"
        hostname = &host
        events := make(chan ClientEvent)
+       finished := make(chan struct{})
        defer func() {
                events <- ClientEvent{eventType: EventTerm}
        defer func() {
                events <- ClientEvent{eventType: EventTerm}
+               <-finished
        }()
        }()
-       go Processor(events, make(chan struct{}))
+       go Processor(events, finished)
        conn := NewTestingConn()
        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"
 
        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 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")
        }
 
                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") {
                t.Fatal("LUSERS", r)
        }
        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 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")
        }
 
                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") {
                t.Fatal("1 users logged in", r)
        }
        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
        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
 
        motdName := fd.Name()
        motd = &motdName
 
-       SendMotd(client)
+       client.SendMotd()
        if r := <-conn.outbound; !strings.HasPrefix(r, ":foohost 375") {
                t.Fatal("MOTD start", r)
        }
        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 (file)
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 <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)
-               }
-       }
-}
index deb6e3542feca9d32a1f494f3ffefdb5f0415403..f8aad490d8a47b44f900c022ea9ee7adb7e37bdd 100644 (file)
--- a/goircd.go
+++ b/goircd.go
@@ -23,114 +23,213 @@ import (
        "io/ioutil"
        "log"
        "net"
        "io/ioutil"
        "log"
        "net"
+       "os"
+       "os/signal"
        "path"
        "path/filepath"
        "path"
        "path/filepath"
+       "strconv"
        "strings"
        "strings"
+       "syscall"
 )
 
 )
 
-const Version = "1.8.2"
+const (
+       Version = "1.9.0"
+
+       StateTopicFilename = "topic"
+       StateKeyFilename   = "key"
+
+       EventNew   = iota
+       EventDel   = iota
+       EventMsg   = iota
+       EventTopic = iota
+       EventWho   = iota
+       EventMode  = iota
+       EventTerm  = iota
+       EventTick  = iota
+)
+
+type ClientEvent struct {
+       client    *Client
+       eventType int
+       text      string
+}
+
+type StateEvent struct {
+       where string
+       topic string
+       key   string
+}
 
 var (
 
 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.")
+       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")
+
+       permStateDir  os.FileMode
+       permStateFile os.FileMode
+       permLogFile   os.FileMode
+
+       stateSink chan StateEvent = make(chan StateEvent)
 )
 
 )
 
-func listenerLoop(sock net.Listener, events chan ClientEvent) {
+func permParse(s string) os.FileMode {
+       r, err := strconv.ParseUint(s, 8, 16)
+       if err != nil {
+               log.Fatalln(err)
+       }
+       return os.FileMode(r)
+}
+
+func listenerLoop(ln net.Listener, events chan ClientEvent) {
        for {
        for {
-               conn, err := sock.Accept()
+               conn, err := ln.Accept()
                if err != nil {
                if err != nil {
-                       log.Println("Error during accepting connection", err)
+                       log.Println("error during accept", err)
                        continue
                }
                        continue
                }
-               client := NewClient(conn)
-               go client.Processor(events)
+               NewClient(conn, events)
        }
 }
 
        }
 }
 
-func Run() {
-       events := make(chan ClientEvent)
-       log.SetFlags(log.Ldate | log.Lmicroseconds | log.Lshortfile)
+func main() {
+       flag.Parse()
+       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() {
 
        if *logdir == "" {
                // Dummy logger
                go func() {
-                       for _ = range logSink {
+                       for range logSink {
                        }
                }()
        } else {
                if !path.IsAbs(*logdir) {
                        }
                }()
        } else {
                if !path.IsAbs(*logdir) {
-                       log.Fatalln("Need absolute path for logdir")
+                       log.Fatalln("need absolute path for logdir")
                }
                go Logger(*logdir, logSink)
                }
                go Logger(*logdir, logSink)
-               log.Println(*logdir, "logger initialized")
        }
 
        }
 
-       log.Println("goircd " + Version + " is starting")
        if *statedir == "" {
                // Dummy statekeeper
                go func() {
        if *statedir == "" {
                // Dummy statekeeper
                go func() {
-                       for _ = range stateSink {
+                       for range stateSink {
                        }
                }()
        } else {
                if !path.IsAbs(*statedir) {
                        }
                }()
        } else {
                if !path.IsAbs(*statedir) {
-                       log.Fatalln("Need absolute path for statedir")
+                       log.Fatalln("need absolute path for statedir")
                }
                states, err := filepath.Glob(path.Join(*statedir, "#*"))
                if err != nil {
                }
                states, err := filepath.Glob(path.Join(*statedir, "#*"))
                if err != nil {
-                       log.Fatalln("Can not read statedir", err)
+                       log.Fatalln("can not read statedir", err)
                }
                for _, state := range states {
                }
                for _, state := range states {
-                       buf, err := ioutil.ReadFile(state)
+                       buf, err := ioutil.ReadFile(path.Join(state, StateTopicFilename))
                        if err != nil {
                        if err != nil {
-                               log.Fatalf("Can not read state %s: %v", state, err)
+                               log.Fatalf(
+                                       "can not read state %s/%s: %v",
+                                       state, StateTopicFilename, 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)
+                       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 {
                        } else {
-                               room.topic = &contents[0]
-                               room.key = &contents[1]
-                               log.Println("Loaded state for room", *room.name)
+                               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 StateKeeper(*statedir, stateSink)
-               log.Println(*statedir, "statekeeper initialized")
+
+               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 != "" {
        if *bind != "" {
-               listener, err := net.Listen("tcp", *bind)
+               ln, err := net.Listen("tcp", *bind)
                if err != nil {
                if err != nil {
-                       log.Fatalf("Can not listen on %s: %v", *bind, err)
+                       log.Fatalf("can not listen on %s: %v", *bind, err)
                }
                }
-               log.Println("Raw listening on", *bind)
-               go listenerLoop(listener, events)
+               go listenerLoop(ln, events)
        }
        if *tlsBind != "" {
                cert, err := tls.LoadX509KeyPair(*tlsPEM, *tlsPEM)
                if err != nil {
        }
        if *tlsBind != "" {
                cert, err := tls.LoadX509KeyPair(*tlsPEM, *tlsPEM)
                if err != nil {
-                       log.Fatalf("Could not load TLS keys from %s: %s", *tlsPEM, err)
+                       log.Fatalf("can not load TLS keys from %s: %s", *tlsPEM, err)
                }
                config := tls.Config{Certificates: []tls.Certificate{cert}}
                }
                config := tls.Config{Certificates: []tls.Certificate{cert}}
-               listenerTLS, err := tls.Listen("tcp", *tlsBind, &config)
+               ln, err := tls.Listen("tcp", *tlsBind, &config)
                if err != nil {
                if err != nil {
-                       log.Fatalf("Can not listen on %s: %v", *tlsBind, err)
+                       log.Fatalf("can not listen on %s: %v", *tlsBind, err)
                }
                }
-               log.Println("TLS listening on", *tlsBind)
-               go listenerLoop(listenerTLS, events)
+               go listenerLoop(ln, events)
        }
        }
-       Processor(events, make(chan struct{}))
-}
+       log.Println("goircd", Version, "started")
 
 
-func main() {
-       flag.Parse()
-       Run()
+       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 (file)
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 <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)
+               }
+       }
+}
diff --git a/room.go b/room.go
index 76b4c4275a48dcb16c562ebb88cb1213ee2eb6b1..cc2b0d739938e3998a3cc08089c882c4fdf263a3 100644 (file)
--- a/room.go
+++ b/room.go
@@ -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
-       topic   *string
-       key     *string
+       name    string
+       topic   string
+       key     string
        members map[*Client]struct{}
        members map[*Client]struct{}
+       events  chan ClientEvent
        sync.RWMutex
 }
 
        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 (r *Room) SendTopic(c *Client) {
+       t := r.topic
+       if t == "" {
+               c.ReplyNicknamed("331", r.name, "No topic is set")
+       } else {
+               c.ReplyNicknamed("332", r.name, t)
        }
 }
 
        }
 }
 
-func (room *Room) SendTopic(client *Client) {
-       room.RLock()
-       if *room.topic == "" {
-               client.ReplyNicknamed("331", room.String(), "No topic is set")
-       } else {
-               client.ReplyNicknamed("332", room.String(), *room.topic)
+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
        }
        }
-       room.RUnlock()
+       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")
 }
 
 }
 
-// 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) 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)
        }
                        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:
                case EventTerm:
-                       roomsGroup.Done()
+                       roomsWG.Done()
                        return
                case EventNew:
                        return
                case EventNew:
-                       room.Lock()
-                       room.members[client] = struct{}{}
+                       r.Lock()
+                       r.members[c] = struct{}{}
+                       r.Unlock()
                        if *verbose {
                        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:
                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
                        }
                                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:
                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
                        }
                                continue
                        }
-                       if event.text == "" {
-                               room.SendTopic(client)
-                               room.RUnlock()
+                       if e.text == "" {
+                               r.SendTopic(c)
                                continue
                        }
                                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:
                case EventWho:
-                       room.RLock()
-                       for m := range room.members {
-                               client.ReplyNicknamed(
+                       r.RLock()
+                       for m := range r.members {
+                               c.ReplyNicknamed(
                                        "352",
                                        "352",
-                                       room.String(),
-                                       *m.username,
+                                       r.name,
+                                       m.username,
                                        m.Host(),
                                        *hostname,
                                        m.Host(),
                                        *hostname,
-                                       *m.nickname,
+                                       m.nickname,
                                        "H",
                                        "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:
                case EventMode:
-                       room.RLock()
-                       if event.text == "" {
-                               mode := "+"
-                               if *room.key != "" {
+                       if e.text == "" {
+                               mode := "+n"
+                               if r.key != "" {
                                        mode = mode + "k"
                                }
                                        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
                        }
                                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
                        }
                                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 {
                                        continue
                                }
                        } else {
-                               client.ReplyNicknamed("472", event.text, "Unknown MODE flag")
-                               room.RUnlock()
+                               c.ReplyNicknamed("472", e.text, "Unknown MODE flag")
                                continue
                        }
                                continue
                        }
-                       room.RUnlock()
                        var msg string
                        var msgLog string
                        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 {
                                if len(cols) == 1 {
-                                       client.ReplyNotEnoughParameters("MODE")
+                                       c.ReplyNotEnoughParameters("MODE")
                                        continue
                                }
                                        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 {
                        } 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"
                        }
                                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:
                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
+}
index 969481843f290891ef5307879d5b165ba822a112..97c202b82f19d3321776752a5049063b1914ec4b 100644 (file)
@@ -46,10 +46,9 @@ func TestTwoUsers(t *testing.T) {
        host := "foohost"
        hostname = &host
        events := make(chan ClientEvent)
        host := "foohost"
        hostname = &host
        events := make(chan ClientEvent)
-       roomsM.Lock()
+       roomsLock.Lock()
        rooms = make(map[string]*Room)
        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)
        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()
 
        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"
 
        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
        }
 
                <-conn2.outbound
        }
 
-       SendLusers(client1)
+       client1.SendLusers()
        if r := <-conn1.outbound; !strings.Contains(r, "There are 2 users") {
                t.Fatal("LUSERS", r)
        }
        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{})
        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() {
        finished := make(chan struct{})
        go Processor(events, finished)
        defer func() {
@@ -147,8 +143,7 @@ func TestJoin(t *testing.T) {
                <-finished
        }()
        conn := NewTestingConn()
                <-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++ {
 
        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
        }
        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")
        }
        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)
        }
        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
        }
        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")
        }
                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) {
                t.Fatal("invalid join log event #barenc", r)
        }
        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)
        }
        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")
        }
                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)
        }
        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"
        }
 
        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)
        }
        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 := <-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") {
                t.Fatal("set channel key", r)
        }
        if r := <-stateSink; (r.topic != "") || (r.where != "#barenc") || (r.key != "newkey") {