Many fixes and additions
* NAMES and WALLOPS command * -cloak option * -v renamed to -verbose * -passwords renamed to -passwd * -debug option prints traffic messages * without -verbose only startup/shutdown and error messages are printed * -timestamped option provides timestamps in printed messages, as earlier. No timestamps is useful for running under daemontools * passwords are stored in SHA256-hashed format * state files replaced with state directory with files * removed many unnecessary pointers and locks * graceful shutdown with all clients notification * fixed time structure printing in log files, instead of short human readable timestamp * PART is sent to the user itself, to notify his client about leaving * log messages are printed to stdout, instead of stderr, for friendliness with daemontools logger * ability to configure newly created directories and files with -perm-state-dir, -perm-state-file, -perm-log-file
This commit is contained in:
parent
def58d0f49
commit
b7fb219307
7
INSTALL
7
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
|
$ go get go.cypherpunks.ru/goircd
|
||||||
* or manually:
|
|
||||||
|
or manually:
|
||||||
|
|
||||||
$ git clone git://git.cypherpunks.ru/goircd.git
|
$ git clone git://git.cypherpunks.ru/goircd.git
|
||||||
$ cd goircd
|
$ cd goircd
|
||||||
|
40
README
40
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 can not connect to other servers. Just standalone installation
|
||||||
* It has few basic IRC commands
|
* It has few basic IRC commands
|
||||||
* There is no support for channel operators, modes, votes, invites
|
* There is no support for channel operators, many modes, votes, invites
|
||||||
* No ident lookups
|
* No ident lookups
|
||||||
|
|
||||||
But it has some convincing features:
|
But it has some convincing features:
|
||||||
@ -19,8 +19,8 @@ But it has some convincing features:
|
|||||||
* Single executable binary
|
* Single executable binary
|
||||||
* No configuration file, just few command line arguments
|
* No configuration file, just few command line arguments
|
||||||
* IPv6 out-of-box support
|
* IPv6 out-of-box support
|
||||||
* Ability to listen on TLS-capable ports
|
* Ability to additionally listen on TLS-capable ports
|
||||||
* Optional channel logging to plain text files
|
* Optional channels logging to plain text files
|
||||||
* Optional permanent channel's state saving in plain text files
|
* Optional permanent channel's state saving in plain text files
|
||||||
(so you can reload daemon and all channels topics and keys won't
|
(so you can reload daemon and all channels topics and keys won't
|
||||||
disappear)
|
disappear)
|
||||||
@ -28,15 +28,16 @@ But it has some convincing features:
|
|||||||
|
|
||||||
Some remarks and recommendations related to it's simplicity:
|
Some remarks and recommendations related to it's simplicity:
|
||||||
|
|
||||||
* Use either nohup or similar tools to daemonize it
|
* Use daemontools to daemonize, setuid/gid it
|
||||||
* Just plain logging on stderr, without syslog support
|
* Just plaintext logging to stdout, without syslog support -- use
|
||||||
|
daemontool's multilog
|
||||||
|
|
||||||
SUPPORTED IRC COMMANDS
|
SUPPORTED IRC COMMANDS
|
||||||
|
|
||||||
* PASS/NICK/USER during registration workflow
|
* PASS/NICK/USER during registration workflow
|
||||||
* PING/PONGs
|
* PING/PONGs
|
||||||
* NOTICE/PRIVMSG, ISON
|
* NOTICE/PRIVMSG, ISON
|
||||||
* AWAY, MOTD, LUSERS, WHO, WHOIS, VERSION, QUIT
|
* AWAY, MOTD, LUSERS, NAMES, WHO, WHOIS, VERSION, WALLOPS, QUIT
|
||||||
* LIST, JOIN, TOPIC, +k/-k channel MODE
|
* LIST, JOIN, TOPIC, +k/-k channel MODE
|
||||||
|
|
||||||
USAGE
|
USAGE
|
||||||
@ -45,6 +46,7 @@ Just execute goircd daemon. It has following optional arguments:
|
|||||||
|
|
||||||
-hostname: hostname to show for client's connections
|
-hostname: hostname to show for client's connections
|
||||||
-bind: address to bind to (:6667 by default)
|
-bind: address to bind to (:6667 by default)
|
||||||
|
-cloak: cloak user's host with the given hostname
|
||||||
-motd: absolute path to MOTD file. It is reread every time
|
-motd: absolute path to MOTD file. It is reread every time
|
||||||
MOTD is requested
|
MOTD is requested
|
||||||
-logdir: directory where all channels messages will be saved. If
|
-logdir: directory where all channels messages will be saved. If
|
||||||
@ -53,10 +55,15 @@ Just execute goircd daemon. It has following optional arguments:
|
|||||||
loaded during startup. If omitted, then states will be
|
loaded during startup. If omitted, then states will be
|
||||||
lost after daemon termination
|
lost after daemon termination
|
||||||
-tlsbind: enable TLS, specify address to listen on and path
|
-tlsbind: enable TLS, specify address to listen on and path
|
||||||
-tlspem to PEM file with certificate and private key
|
-tlspem: to PEM file with certificate and private key
|
||||||
-passwords: enable client authentication and specify path to
|
-passwd: enable client authentication and specify path to
|
||||||
passwords file
|
passwords file
|
||||||
-v: increase verbosity
|
-timestamped: enabled timestamps for stderr messages
|
||||||
|
-verbose: increase verbosity
|
||||||
|
-debug: also show traffic messages
|
||||||
|
-perm-state-dir: permission (before umask) for newly created state directory
|
||||||
|
-perm-state-file: permission (before umask) for newly created state file
|
||||||
|
-perm-log-file: permission (before umask) for newly created log file
|
||||||
|
|
||||||
TLS
|
TLS
|
||||||
|
|
||||||
@ -69,10 +76,12 @@ AUTHENTICATION
|
|||||||
You can turn on optional client authentication by preparing passwords
|
You can turn on optional client authentication by preparing passwords
|
||||||
file and using the -passwords argument. Format of passwords file is:
|
file and using the -passwords argument. Format of passwords file is:
|
||||||
|
|
||||||
login1:password1\n
|
login1:hex(sha256(password1))\n
|
||||||
login2:password2\n
|
login2:hex(sha256(password2))\n
|
||||||
...
|
...
|
||||||
|
|
||||||
|
You can get hashed password value using: echo -n password | sha256
|
||||||
|
|
||||||
LOG FILES
|
LOG FILES
|
||||||
|
|
||||||
Log files are not opened all the time, but only during each message
|
Log files are not opened all the time, but only during each message
|
||||||
@ -80,13 +89,8 @@ saving. That is why you can safely rename them for rotation purposes.
|
|||||||
|
|
||||||
STATE FILES
|
STATE FILES
|
||||||
|
|
||||||
Each state file has the name equals to room's one. It contains two plain
|
Room's state is created/saved when either topic or key is set. State is
|
||||||
text lines: room's topic and room's authentication key (empty if none
|
a directory (room's name) with "topic" and "key" plaintext files.
|
||||||
specified). For example:
|
|
||||||
|
|
||||||
$ cat states/meinroom
|
|
||||||
This is meinroom's topic
|
|
||||||
secretkey
|
|
||||||
|
|
||||||
LICENCE
|
LICENCE
|
||||||
|
|
||||||
|
359
client.go
359
client.go
@ -19,8 +19,15 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"crypto/sha256"
|
||||||
|
"crypto/subtle"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
|
"regexp"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@ -28,29 +35,37 @@ import (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
BufSize = 1500
|
BufSize = 1500
|
||||||
MaxOutBuf = 1 << 12
|
MaxOutBuf = 128
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
CRLF []byte = []byte{'\x0d', '\x0a'}
|
CRLF []byte = []byte{'\x0d', '\x0a'}
|
||||||
|
RENickname = regexp.MustCompile("^[a-zA-Z0-9-]{1,24}$")
|
||||||
|
|
||||||
|
clients map[*Client]struct{} = make(map[*Client]struct{})
|
||||||
|
clientsLock sync.RWMutex
|
||||||
|
clientsWG sync.WaitGroup
|
||||||
)
|
)
|
||||||
|
|
||||||
type Client struct {
|
type Client struct {
|
||||||
conn net.Conn
|
conn net.Conn
|
||||||
registered bool
|
registered bool
|
||||||
nickname *string
|
nickname string
|
||||||
username *string
|
username string
|
||||||
realname *string
|
realname string
|
||||||
password *string
|
password string
|
||||||
away *string
|
away string
|
||||||
recvTimestamp time.Time
|
recvTimestamp time.Time
|
||||||
sendTimestamp time.Time
|
sendTimestamp time.Time
|
||||||
outBuf chan *string
|
outBuf chan string
|
||||||
alive bool
|
alive bool
|
||||||
sync.Mutex
|
sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) Host() string {
|
func (c *Client) Host() string {
|
||||||
|
if *cloak != "" {
|
||||||
|
return *cloak
|
||||||
|
}
|
||||||
addr := c.conn.RemoteAddr().String()
|
addr := c.conn.RemoteAddr().String()
|
||||||
if host, _, err := net.SplitHostPort(addr); err == nil {
|
if host, _, err := net.SplitHostPort(addr); err == nil {
|
||||||
addr = host
|
addr = host
|
||||||
@ -62,48 +77,42 @@ func (c *Client) Host() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) String() string {
|
func (c *Client) String() string {
|
||||||
return *c.nickname + "!" + *c.username + "@" + c.Host()
|
return strings.Join([]string{c.nickname, "!", c.username, "@", c.Host()}, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewClient(conn net.Conn) *Client {
|
func NewClient(conn net.Conn, events chan ClientEvent) *Client {
|
||||||
nickname := "*"
|
|
||||||
username := ""
|
|
||||||
c := Client{
|
c := Client{
|
||||||
conn: conn,
|
conn: conn,
|
||||||
nickname: &nickname,
|
nickname: "*",
|
||||||
username: &username,
|
username: "",
|
||||||
recvTimestamp: time.Now(),
|
recvTimestamp: time.Now(),
|
||||||
sendTimestamp: time.Now(),
|
sendTimestamp: time.Now(),
|
||||||
alive: true,
|
alive: true,
|
||||||
outBuf: make(chan *string, MaxOutBuf),
|
outBuf: make(chan string, MaxOutBuf),
|
||||||
}
|
}
|
||||||
|
clientsWG.Add(2)
|
||||||
go c.MsgSender()
|
go c.MsgSender()
|
||||||
|
go c.Processor(events)
|
||||||
return &c
|
return &c
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) SetDead() {
|
|
||||||
c.outBuf <- nil
|
|
||||||
c.alive = false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) Close() {
|
func (c *Client) Close() {
|
||||||
c.Lock()
|
c.Lock()
|
||||||
if c.alive {
|
if c.alive {
|
||||||
c.SetDead()
|
close(c.outBuf)
|
||||||
|
c.alive = false
|
||||||
}
|
}
|
||||||
c.Unlock()
|
c.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Client processor blockingly reads everything remote client sends,
|
func (c *Client) Processor(events chan ClientEvent) {
|
||||||
// splits messages by CRLF and send them to Daemon gorouting for processing
|
events <- ClientEvent{c, EventNew, ""}
|
||||||
// it futher. Also it can signalize that client is unavailable (disconnected).
|
if *verbose {
|
||||||
func (c *Client) Processor(sink chan ClientEvent) {
|
log.Println(c, "connected")
|
||||||
sink <- ClientEvent{c, EventNew, ""}
|
}
|
||||||
log.Println(c, "New client")
|
|
||||||
buf := make([]byte, BufSize*2)
|
buf := make([]byte, BufSize*2)
|
||||||
var n int
|
var n, prev, i int
|
||||||
var prev int
|
var msg string
|
||||||
var i int
|
|
||||||
var err error
|
var err error
|
||||||
for {
|
for {
|
||||||
if prev == BufSize {
|
if prev == BufSize {
|
||||||
@ -120,26 +129,43 @@ func (c *Client) Processor(sink chan ClientEvent) {
|
|||||||
if i == -1 {
|
if i == -1 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
sink <- ClientEvent{c, EventMsg, string(buf[:i])}
|
if *debug {
|
||||||
|
log.Println(c, "<-", msg)
|
||||||
|
}
|
||||||
|
msg = string(buf[:i])
|
||||||
|
if *debug {
|
||||||
|
log.Println(c, "->", msg)
|
||||||
|
}
|
||||||
|
events <- ClientEvent{c, EventMsg, msg}
|
||||||
copy(buf, buf[i+2:prev])
|
copy(buf, buf[i+2:prev])
|
||||||
prev -= (i + 2)
|
prev -= (i + 2)
|
||||||
goto CheckMore
|
goto CheckMore
|
||||||
}
|
}
|
||||||
c.Close()
|
c.Close()
|
||||||
sink <- ClientEvent{c, EventDel, ""}
|
if *verbose {
|
||||||
|
log.Println(c, "disconnected")
|
||||||
|
}
|
||||||
|
events <- ClientEvent{c, EventDel, ""}
|
||||||
|
clientsWG.Done()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) MsgSender() {
|
func (c *Client) MsgSender() {
|
||||||
|
var err error
|
||||||
for msg := range c.outBuf {
|
for msg := range c.outBuf {
|
||||||
if msg == nil {
|
if *debug {
|
||||||
c.conn.Close()
|
log.Println(c, "<-", msg)
|
||||||
|
}
|
||||||
|
if _, err = c.conn.Write(append([]byte(msg), CRLF...)); err != nil {
|
||||||
|
if *verbose {
|
||||||
|
log.Println(c, "error writing", err)
|
||||||
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
c.conn.Write(append([]byte(*msg), CRLF...))
|
|
||||||
}
|
}
|
||||||
|
c.conn.Close()
|
||||||
|
clientsWG.Done()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send message as is with CRLF appended.
|
|
||||||
func (c *Client) Msg(text string) {
|
func (c *Client) Msg(text string) {
|
||||||
c.Lock()
|
c.Lock()
|
||||||
defer c.Unlock()
|
defer c.Unlock()
|
||||||
@ -149,20 +175,18 @@ func (c *Client) Msg(text string) {
|
|||||||
if len(c.outBuf) == MaxOutBuf {
|
if len(c.outBuf) == MaxOutBuf {
|
||||||
log.Println(c, "output buffer size exceeded, kicking him")
|
log.Println(c, "output buffer size exceeded, kicking him")
|
||||||
if c.alive {
|
if c.alive {
|
||||||
c.SetDead()
|
close(c.outBuf)
|
||||||
|
c.alive = false
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.outBuf <- &text
|
c.outBuf <- text
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send message from server. It has ": servername" prefix.
|
|
||||||
func (c *Client) Reply(text string) {
|
func (c *Client) Reply(text string) {
|
||||||
c.Msg(":" + *hostname + " " + text)
|
c.Msg(":" + *hostname + " " + text)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send server message, concatenating all provided text parts and
|
|
||||||
// prefix the last one with ":".
|
|
||||||
func (c *Client) ReplyParts(code string, text ...string) {
|
func (c *Client) ReplyParts(code string, text ...string) {
|
||||||
parts := []string{code}
|
parts := []string{code}
|
||||||
for _, t := range text {
|
for _, t := range text {
|
||||||
@ -172,18 +196,14 @@ func (c *Client) ReplyParts(code string, text ...string) {
|
|||||||
c.Reply(strings.Join(parts, " "))
|
c.Reply(strings.Join(parts, " "))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send nicknamed server message. After servername it always has target
|
|
||||||
// client's nickname. The last part is prefixed with ":".
|
|
||||||
func (c *Client) ReplyNicknamed(code string, text ...string) {
|
func (c *Client) ReplyNicknamed(code string, text ...string) {
|
||||||
c.ReplyParts(code, append([]string{*c.nickname}, text...)...)
|
c.ReplyParts(code, append([]string{c.nickname}, text...)...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reply "461 not enough parameters" error for given command.
|
|
||||||
func (c *Client) ReplyNotEnoughParameters(command string) {
|
func (c *Client) ReplyNotEnoughParameters(command string) {
|
||||||
c.ReplyNicknamed("461", command, "Not enough parameters")
|
c.ReplyNicknamed("461", command, "Not enough parameters")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reply "403 no such channel" error for specified channel.
|
|
||||||
func (c *Client) ReplyNoChannel(channel string) {
|
func (c *Client) ReplyNoChannel(channel string) {
|
||||||
c.ReplyNicknamed("403", channel, "No such channel")
|
c.ReplyNicknamed("403", channel, "No such channel")
|
||||||
}
|
}
|
||||||
@ -191,3 +211,252 @@ func (c *Client) ReplyNoChannel(channel string) {
|
|||||||
func (c *Client) ReplyNoNickChan(channel string) {
|
func (c *Client) ReplyNoNickChan(channel string) {
|
||||||
c.ReplyNicknamed("401", channel, "No such nick/channel")
|
c.ReplyNicknamed("401", channel, "No such nick/channel")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Client) SendLusers() {
|
||||||
|
lusers := 0
|
||||||
|
clientsLock.RLock()
|
||||||
|
for client := range clients {
|
||||||
|
if client.registered {
|
||||||
|
lusers++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
clientsLock.RUnlock()
|
||||||
|
c.ReplyNicknamed(
|
||||||
|
"251",
|
||||||
|
fmt.Sprintf("There are %d users and 0 invisible on 1 servers",
|
||||||
|
lusers,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) SendMotd() {
|
||||||
|
if motd == nil {
|
||||||
|
c.ReplyNicknamed("422", "MOTD File is missing")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
motdText, err := ioutil.ReadFile(*motd)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("can not read motd file %s: %v", *motd, err)
|
||||||
|
c.ReplyNicknamed("422", "Error reading MOTD File")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.ReplyNicknamed("375", "- "+*hostname+" Message of the day -")
|
||||||
|
for _, s := range strings.Split(strings.TrimSuffix(string(motdText), "\n"), "\n") {
|
||||||
|
c.ReplyNicknamed("372", "- "+s)
|
||||||
|
}
|
||||||
|
c.ReplyNicknamed("376", "End of /MOTD command")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Join(cmd string) {
|
||||||
|
args := strings.Split(cmd, " ")
|
||||||
|
rs := strings.Split(args[0], ",")
|
||||||
|
keys := []string{}
|
||||||
|
if len(args) > 1 {
|
||||||
|
keys = strings.Split(args[1], ",")
|
||||||
|
}
|
||||||
|
RoomCycle:
|
||||||
|
for n, roomName := range rs {
|
||||||
|
if !RERoom.MatchString(roomName) {
|
||||||
|
c.ReplyNoChannel(roomName)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var key string
|
||||||
|
if (n < len(keys)) && (keys[n] != "") {
|
||||||
|
key = keys[n]
|
||||||
|
}
|
||||||
|
roomsLock.RLock()
|
||||||
|
for roomNameExisting, room := range rooms {
|
||||||
|
if roomName != roomNameExisting {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (room.key != "") && (room.key != key) {
|
||||||
|
c.ReplyNicknamed("475", roomName, "Cannot join channel (+k)")
|
||||||
|
roomsLock.RUnlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
room.events <- ClientEvent{c, EventNew, ""}
|
||||||
|
roomsLock.RUnlock()
|
||||||
|
continue RoomCycle
|
||||||
|
}
|
||||||
|
roomsLock.RUnlock()
|
||||||
|
roomNew := RoomRegister(roomName)
|
||||||
|
if *verbose {
|
||||||
|
log.Println("room", roomName, "created")
|
||||||
|
}
|
||||||
|
if key != "" {
|
||||||
|
roomNew.key = key
|
||||||
|
roomNew.StateSave()
|
||||||
|
}
|
||||||
|
roomNew.events <- ClientEvent{c, EventNew, ""}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *Client) SendWhois(nicknames []string) {
|
||||||
|
var c *Client
|
||||||
|
for _, nickname := range nicknames {
|
||||||
|
nickname = strings.ToLower(nickname)
|
||||||
|
clientsLock.RLock()
|
||||||
|
for c = range clients {
|
||||||
|
if strings.ToLower(c.nickname) == nickname {
|
||||||
|
clientsLock.RUnlock()
|
||||||
|
goto Found
|
||||||
|
}
|
||||||
|
}
|
||||||
|
clientsLock.RUnlock()
|
||||||
|
client.ReplyNoNickChan(nickname)
|
||||||
|
continue
|
||||||
|
Found:
|
||||||
|
var host string
|
||||||
|
if *cloak != "" {
|
||||||
|
host = *cloak
|
||||||
|
} else {
|
||||||
|
host, _, err := net.SplitHostPort(c.conn.RemoteAddr().String())
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("can't parse RemoteAddr %q: %v", host, err)
|
||||||
|
host = "Unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
client.ReplyNicknamed("311", c.nickname, c.username, host, "*", c.realname)
|
||||||
|
client.ReplyNicknamed("312", c.nickname, *hostname, *hostname)
|
||||||
|
if c.away != "" {
|
||||||
|
client.ReplyNicknamed("301", c.nickname, c.away)
|
||||||
|
}
|
||||||
|
subscriptions := make([]string, 0)
|
||||||
|
roomsLock.RLock()
|
||||||
|
for _, room := range rooms {
|
||||||
|
for subscriber := range room.members {
|
||||||
|
if subscriber.nickname == nickname {
|
||||||
|
subscriptions = append(subscriptions, room.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
roomsLock.RUnlock()
|
||||||
|
sort.Strings(subscriptions)
|
||||||
|
client.ReplyNicknamed("319", c.nickname, strings.Join(subscriptions, " "))
|
||||||
|
client.ReplyNicknamed("318", c.nickname, "End of /WHOIS list")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) SendList(cols []string) {
|
||||||
|
var rs []string
|
||||||
|
if (len(cols) > 1) && (cols[1] != "") {
|
||||||
|
rs = strings.Split(strings.Split(cols[1], " ")[0], ",")
|
||||||
|
} else {
|
||||||
|
rs = make([]string, 0)
|
||||||
|
roomsLock.RLock()
|
||||||
|
for r := range rooms {
|
||||||
|
rs = append(rs, r)
|
||||||
|
}
|
||||||
|
roomsLock.RUnlock()
|
||||||
|
}
|
||||||
|
sort.Strings(rs)
|
||||||
|
roomsLock.RLock()
|
||||||
|
for _, r := range rs {
|
||||||
|
if room, found := rooms[r]; found {
|
||||||
|
c.ReplyNicknamed(
|
||||||
|
"322",
|
||||||
|
r,
|
||||||
|
fmt.Sprintf("%d", len(room.members)),
|
||||||
|
room.topic,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
roomsLock.RUnlock()
|
||||||
|
c.ReplyNicknamed("323", "End of /LIST")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Register(cmd string, cols []string) {
|
||||||
|
switch cmd {
|
||||||
|
case "PASS":
|
||||||
|
if len(cols) == 1 || len(cols[1]) < 1 {
|
||||||
|
c.ReplyNotEnoughParameters("PASS")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
password := strings.TrimPrefix(cols[1], ":")
|
||||||
|
c.password = password
|
||||||
|
case "NICK":
|
||||||
|
if len(cols) == 1 || len(cols[1]) < 1 {
|
||||||
|
c.ReplyParts("431", "No nickname given")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
nickname := cols[1]
|
||||||
|
// Compatibility with some clients prepending colons to nickname
|
||||||
|
nickname = strings.TrimPrefix(nickname, ":")
|
||||||
|
nickname = strings.ToLower(nickname)
|
||||||
|
if !RENickname.MatchString(nickname) {
|
||||||
|
c.ReplyParts("432", "*", cols[1], "Erroneous nickname")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
clientsLock.RLock()
|
||||||
|
for existingClient := range clients {
|
||||||
|
if existingClient.nickname == nickname {
|
||||||
|
clientsLock.RUnlock()
|
||||||
|
c.ReplyParts("433", "*", nickname, "Nickname is already in use")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
clientsLock.RUnlock()
|
||||||
|
c.nickname = nickname
|
||||||
|
case "USER":
|
||||||
|
if len(cols) == 1 {
|
||||||
|
c.ReplyNotEnoughParameters("USER")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
args := strings.SplitN(cols[1], " ", 4)
|
||||||
|
if len(args) < 4 {
|
||||||
|
c.ReplyNotEnoughParameters("USER")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.username = args[0]
|
||||||
|
realname := strings.TrimLeft(args[3], ":")
|
||||||
|
c.realname = realname
|
||||||
|
}
|
||||||
|
if c.nickname != "*" && c.username != "" {
|
||||||
|
if *passwords != "" {
|
||||||
|
authenticated := false
|
||||||
|
if c.password == "" {
|
||||||
|
c.ReplyParts("462", "You may not register")
|
||||||
|
c.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
contents, err := ioutil.ReadFile(*passwords)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("can not read passwords file %s: %s", *passwords, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for n, entry := range strings.Split(string(contents), "\n") {
|
||||||
|
if entry == "" || strings.HasPrefix(entry, "#") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cols := strings.Split(entry, ":")
|
||||||
|
if len(cols) != 2 {
|
||||||
|
log.Fatalf("bad passwords format: %s:%d", *passwords, n)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if cols[0] != c.nickname {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
h := sha256.Sum256([]byte(c.password))
|
||||||
|
authenticated = subtle.ConstantTimeCompare(
|
||||||
|
[]byte(hex.EncodeToString(h[:])),
|
||||||
|
[]byte(cols[1]),
|
||||||
|
) == 1
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if !authenticated {
|
||||||
|
c.ReplyParts("462", "You may not register")
|
||||||
|
c.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.registered = true
|
||||||
|
c.ReplyNicknamed("001", "Hi, welcome to IRC")
|
||||||
|
c.ReplyNicknamed("002", "Your host is "+*hostname+", running goircd "+Version)
|
||||||
|
c.ReplyNicknamed("003", "This server was created sometime")
|
||||||
|
c.ReplyNicknamed("004", *hostname+" goircd o o")
|
||||||
|
c.SendLusers()
|
||||||
|
c.SendMotd()
|
||||||
|
if *verbose {
|
||||||
|
log.Println(c, "logged in")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -21,45 +21,46 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
// New client creation test. It must send an event about new client,
|
|
||||||
// two predefined messages from it and deletion one
|
|
||||||
func TestNewClient(t *testing.T) {
|
func TestNewClient(t *testing.T) {
|
||||||
conn := NewTestingConn()
|
conn := NewTestingConn()
|
||||||
sink := make(chan ClientEvent)
|
events := make(chan ClientEvent)
|
||||||
host := "foohost"
|
host := "foohost"
|
||||||
hostname = &host
|
hostname = &host
|
||||||
client := NewClient(conn)
|
client := NewClient(conn, events)
|
||||||
go client.Processor(sink)
|
defer func() {
|
||||||
|
client.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
event := <-sink
|
event := <-events
|
||||||
if event.eventType != EventNew {
|
if event.eventType != EventNew {
|
||||||
t.Fatal("no NEW event", event)
|
t.Fatal("no NEW event", event)
|
||||||
}
|
}
|
||||||
conn.inbound <- "foo"
|
conn.inbound <- "foo"
|
||||||
event = <-sink
|
event = <-events
|
||||||
if (event.eventType != EventMsg) || (event.text != "foo") {
|
if (event.eventType != EventMsg) || (event.text != "foo") {
|
||||||
t.Fatal("no first MSG", event)
|
t.Fatal("no first MSG", event)
|
||||||
}
|
}
|
||||||
conn.inbound <- "bar"
|
conn.inbound <- "bar"
|
||||||
event = <-sink
|
event = <-events
|
||||||
if (event.eventType != EventMsg) || (event.text != "bar") {
|
if (event.eventType != EventMsg) || (event.text != "bar") {
|
||||||
t.Fatal("no second MSG", event)
|
t.Fatal("no second MSG", event)
|
||||||
}
|
}
|
||||||
conn.inbound <- ""
|
conn.inbound <- ""
|
||||||
event = <-sink
|
event = <-events
|
||||||
if event.eventType != EventDel {
|
if event.eventType != EventDel {
|
||||||
t.Fatal("no client termination", event)
|
t.Fatal("no client termination", event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test replies formatting
|
|
||||||
func TestClientReplies(t *testing.T) {
|
func TestClientReplies(t *testing.T) {
|
||||||
conn := NewTestingConn()
|
conn := NewTestingConn()
|
||||||
host := "foohost"
|
host := "foohost"
|
||||||
hostname = &host
|
hostname = &host
|
||||||
client := NewClient(conn)
|
client := NewClient(conn, make(chan ClientEvent, 2))
|
||||||
nickname := "мойник"
|
defer func() {
|
||||||
client.nickname = &nickname
|
client.Close()
|
||||||
|
}()
|
||||||
|
client.nickname = "мойник"
|
||||||
|
|
||||||
client.Reply("hello")
|
client.Reply("hello")
|
||||||
if r := <-conn.outbound; r != ":foohost hello\r\n" {
|
if r := <-conn.outbound; r != ":foohost hello\r\n" {
|
||||||
|
@ -18,22 +18,23 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Testing network connection that satisfies net.Conn interface
|
|
||||||
// Can send predefined messages and store all written ones
|
|
||||||
type TestingConn struct {
|
type TestingConn struct {
|
||||||
inbound chan string
|
inbound chan string
|
||||||
outbound chan string
|
outbound chan string
|
||||||
closed bool
|
closed chan struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTestingConn() *TestingConn {
|
func NewTestingConn() *TestingConn {
|
||||||
inbound := make(chan string, 8)
|
return &TestingConn{
|
||||||
outbound := make(chan string, 8)
|
inbound: make(chan string, 8),
|
||||||
return &TestingConn{inbound: inbound, outbound: outbound}
|
outbound: make(chan string, 8),
|
||||||
|
closed: make(chan struct{}),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (conn TestingConn) Error() string {
|
func (conn TestingConn) Error() string {
|
||||||
@ -41,7 +42,8 @@ func (conn TestingConn) Error() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (conn *TestingConn) Read(b []byte) (n int, err error) {
|
func (conn *TestingConn) Read(b []byte) (n int, err error) {
|
||||||
msg := <-conn.inbound
|
select {
|
||||||
|
case msg := <-conn.inbound:
|
||||||
if msg == "" {
|
if msg == "" {
|
||||||
return 0, conn
|
return 0, conn
|
||||||
}
|
}
|
||||||
@ -49,6 +51,9 @@ func (conn *TestingConn) Read(b []byte) (n int, err error) {
|
|||||||
b[n] = bt
|
b[n] = bt
|
||||||
}
|
}
|
||||||
return len(msg) + 2, nil
|
return len(msg) + 2, nil
|
||||||
|
case <-conn.closed:
|
||||||
|
return 0, io.EOF
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type MyAddr struct{}
|
type MyAddr struct{}
|
||||||
@ -66,7 +71,7 @@ func (conn *TestingConn) Write(b []byte) (n int, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (conn *TestingConn) Close() error {
|
func (conn *TestingConn) Close() error {
|
||||||
conn.closed = true
|
close(conn.closed)
|
||||||
close(conn.outbound)
|
close(conn.outbound)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
537
daemon.go
537
daemon.go
@ -19,13 +19,8 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
|
||||||
"log"
|
"log"
|
||||||
"net"
|
|
||||||
"regexp"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -36,289 +31,26 @@ const (
|
|||||||
PingThreshold = time.Second * 90
|
PingThreshold = time.Second * 90
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
|
||||||
RENickname = regexp.MustCompile("^[a-zA-Z0-9-]{1,24}$")
|
|
||||||
|
|
||||||
clients map[*Client]struct{} = make(map[*Client]struct{})
|
|
||||||
clientsM sync.RWMutex
|
|
||||||
rooms map[string]*Room = make(map[string]*Room)
|
|
||||||
roomsM sync.RWMutex
|
|
||||||
roomsGroup sync.WaitGroup
|
|
||||||
roomSinks map[*Room]chan ClientEvent = make(map[*Room]chan ClientEvent)
|
|
||||||
)
|
|
||||||
|
|
||||||
func SendLusers(client *Client) {
|
|
||||||
lusers := 0
|
|
||||||
clientsM.RLock()
|
|
||||||
for client := range clients {
|
|
||||||
if client.registered {
|
|
||||||
lusers++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
clientsM.RUnlock()
|
|
||||||
client.ReplyNicknamed("251", fmt.Sprintf("There are %d users and 0 invisible on 1 servers", lusers))
|
|
||||||
}
|
|
||||||
|
|
||||||
func SendMotd(client *Client) {
|
|
||||||
if motd == nil {
|
|
||||||
client.ReplyNicknamed("422", "MOTD File is missing")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
motdText, err := ioutil.ReadFile(*motd)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Can not read motd file %s: %v", *motd, err)
|
|
||||||
client.ReplyNicknamed("422", "Error reading MOTD File")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
client.ReplyNicknamed("375", "- "+*hostname+" Message of the day -")
|
|
||||||
for _, s := range strings.Split(strings.TrimSuffix(string(motdText), "\n"), "\n") {
|
|
||||||
client.ReplyNicknamed("372", "- "+s)
|
|
||||||
}
|
|
||||||
client.ReplyNicknamed("376", "End of /MOTD command")
|
|
||||||
}
|
|
||||||
|
|
||||||
func SendWhois(client *Client, nicknames []string) {
|
|
||||||
var c *Client
|
|
||||||
var hostPort string
|
|
||||||
var err error
|
|
||||||
var subscriptions []string
|
|
||||||
var room *Room
|
|
||||||
var subscriber *Client
|
|
||||||
for _, nickname := range nicknames {
|
|
||||||
nickname = strings.ToLower(nickname)
|
|
||||||
clientsM.RLock()
|
|
||||||
for c = range clients {
|
|
||||||
if strings.ToLower(*c.nickname) == nickname {
|
|
||||||
clientsM.RUnlock()
|
|
||||||
goto Found
|
|
||||||
}
|
|
||||||
}
|
|
||||||
clientsM.RUnlock()
|
|
||||||
client.ReplyNoNickChan(nickname)
|
|
||||||
continue
|
|
||||||
Found:
|
|
||||||
hostPort, _, err = net.SplitHostPort(c.conn.RemoteAddr().String())
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Can't parse RemoteAddr %q: %v", hostPort, err)
|
|
||||||
hostPort = "Unknown"
|
|
||||||
}
|
|
||||||
client.ReplyNicknamed("311", *c.nickname, *c.username, hostPort, "*", *c.realname)
|
|
||||||
client.ReplyNicknamed("312", *c.nickname, *hostname, *hostname)
|
|
||||||
if c.away != nil {
|
|
||||||
client.ReplyNicknamed("301", *c.nickname, *c.away)
|
|
||||||
}
|
|
||||||
subscriptions = make([]string, 0)
|
|
||||||
roomsM.RLock()
|
|
||||||
for _, room = range rooms {
|
|
||||||
for subscriber = range room.members {
|
|
||||||
if *subscriber.nickname == nickname {
|
|
||||||
subscriptions = append(subscriptions, *room.name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
roomsM.RUnlock()
|
|
||||||
sort.Strings(subscriptions)
|
|
||||||
client.ReplyNicknamed("319", *c.nickname, strings.Join(subscriptions, " "))
|
|
||||||
client.ReplyNicknamed("318", *c.nickname, "End of /WHOIS list")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func SendList(client *Client, cols []string) {
|
|
||||||
var rs []string
|
|
||||||
var r string
|
|
||||||
if (len(cols) > 1) && (cols[1] != "") {
|
|
||||||
rs = strings.Split(strings.Split(cols[1], " ")[0], ",")
|
|
||||||
} else {
|
|
||||||
rs = make([]string, 0)
|
|
||||||
roomsM.RLock()
|
|
||||||
for r = range rooms {
|
|
||||||
rs = append(rs, r)
|
|
||||||
}
|
|
||||||
roomsM.RUnlock()
|
|
||||||
}
|
|
||||||
sort.Strings(rs)
|
|
||||||
var room *Room
|
|
||||||
var found bool
|
|
||||||
for _, r = range rs {
|
|
||||||
roomsM.RLock()
|
|
||||||
if room, found = rooms[r]; found {
|
|
||||||
client.ReplyNicknamed(
|
|
||||||
"322",
|
|
||||||
r,
|
|
||||||
fmt.Sprintf("%d", len(room.members)),
|
|
||||||
*room.topic,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
roomsM.RUnlock()
|
|
||||||
}
|
|
||||||
client.ReplyNicknamed("323", "End of /LIST")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unregistered client workflow processor. Unregistered client:
|
|
||||||
// * is not PINGed
|
|
||||||
// * only QUIT, NICK and USER commands are processed
|
|
||||||
// * other commands are quietly ignored
|
|
||||||
// When client finishes NICK/USER workflow, then MOTD and LUSERS are send to him.
|
|
||||||
func ClientRegister(client *Client, cmd string, cols []string) {
|
|
||||||
switch cmd {
|
|
||||||
case "PASS":
|
|
||||||
if len(cols) == 1 || len(cols[1]) < 1 {
|
|
||||||
client.ReplyNotEnoughParameters("PASS")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
password := strings.TrimPrefix(cols[1], ":")
|
|
||||||
client.password = &password
|
|
||||||
case "NICK":
|
|
||||||
if len(cols) == 1 || len(cols[1]) < 1 {
|
|
||||||
client.ReplyParts("431", "No nickname given")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
nickname := cols[1]
|
|
||||||
// Compatibility with some clients prepending colons to nickname
|
|
||||||
nickname = strings.TrimPrefix(nickname, ":")
|
|
||||||
nickname = strings.ToLower(nickname)
|
|
||||||
clientsM.RLock()
|
|
||||||
for existingClient := range clients {
|
|
||||||
if *existingClient.nickname == nickname {
|
|
||||||
clientsM.RUnlock()
|
|
||||||
client.ReplyParts("433", "*", nickname, "Nickname is already in use")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
clientsM.RUnlock()
|
|
||||||
if !RENickname.MatchString(nickname) {
|
|
||||||
client.ReplyParts("432", "*", cols[1], "Erroneous nickname")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
client.nickname = &nickname
|
|
||||||
case "USER":
|
|
||||||
if len(cols) == 1 {
|
|
||||||
client.ReplyNotEnoughParameters("USER")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
args := strings.SplitN(cols[1], " ", 4)
|
|
||||||
if len(args) < 4 {
|
|
||||||
client.ReplyNotEnoughParameters("USER")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
client.username = &args[0]
|
|
||||||
realname := strings.TrimLeft(args[3], ":")
|
|
||||||
client.realname = &realname
|
|
||||||
}
|
|
||||||
if *client.nickname != "*" && *client.username != "" {
|
|
||||||
if passwords != nil && *passwords != "" {
|
|
||||||
if client.password == nil {
|
|
||||||
client.ReplyParts("462", "You may not register")
|
|
||||||
client.Close()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
contents, err := ioutil.ReadFile(*passwords)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Can no read passwords file %s: %s", *passwords, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
for _, entry := range strings.Split(string(contents), "\n") {
|
|
||||||
if entry == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if lp := strings.Split(entry, ":"); lp[0] == *client.nickname && lp[1] != *client.password {
|
|
||||||
client.ReplyParts("462", "You may not register")
|
|
||||||
client.Close()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
client.registered = true
|
|
||||||
client.ReplyNicknamed("001", "Hi, welcome to IRC")
|
|
||||||
client.ReplyNicknamed("002", "Your host is "+*hostname+", running goircd "+Version)
|
|
||||||
client.ReplyNicknamed("003", "This server was created sometime")
|
|
||||||
client.ReplyNicknamed("004", *hostname+" goircd o o")
|
|
||||||
SendLusers(client)
|
|
||||||
SendMotd(client)
|
|
||||||
log.Println(client, "logged in")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Register new room in Daemon. Create an object, events sink, save pointers
|
|
||||||
// to corresponding daemon's places and start room's processor goroutine.
|
|
||||||
func RoomRegister(name string) (*Room, chan ClientEvent) {
|
|
||||||
roomNew := NewRoom(name)
|
|
||||||
roomSink := make(chan ClientEvent)
|
|
||||||
roomsM.Lock()
|
|
||||||
rooms[name] = roomNew
|
|
||||||
roomSinks[roomNew] = roomSink
|
|
||||||
roomsM.Unlock()
|
|
||||||
go roomNew.Processor(roomSink)
|
|
||||||
roomsGroup.Add(1)
|
|
||||||
return roomNew, roomSink
|
|
||||||
}
|
|
||||||
|
|
||||||
func HandlerJoin(client *Client, cmd string) {
|
|
||||||
args := strings.Split(cmd, " ")
|
|
||||||
rs := strings.Split(args[0], ",")
|
|
||||||
var keys []string
|
|
||||||
if len(args) > 1 {
|
|
||||||
keys = strings.Split(args[1], ",")
|
|
||||||
} else {
|
|
||||||
keys = make([]string, 0)
|
|
||||||
}
|
|
||||||
var roomExisting *Room
|
|
||||||
var roomSink chan ClientEvent
|
|
||||||
var roomNew *Room
|
|
||||||
for n, room := range rs {
|
|
||||||
if !RoomNameValid(room) {
|
|
||||||
client.ReplyNoChannel(room)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
var key string
|
|
||||||
if (n < len(keys)) && (keys[n] != "") {
|
|
||||||
key = keys[n]
|
|
||||||
} else {
|
|
||||||
key = ""
|
|
||||||
}
|
|
||||||
roomsM.RLock()
|
|
||||||
for roomExisting, roomSink = range roomSinks {
|
|
||||||
if room == *roomExisting.name {
|
|
||||||
roomsM.RUnlock()
|
|
||||||
if (*roomExisting.key != "") && (*roomExisting.key != key) {
|
|
||||||
goto Denied
|
|
||||||
}
|
|
||||||
roomSink <- ClientEvent{client, EventNew, ""}
|
|
||||||
goto Joined
|
|
||||||
}
|
|
||||||
}
|
|
||||||
roomsM.RUnlock()
|
|
||||||
roomNew, roomSink = RoomRegister(room)
|
|
||||||
log.Println("Room", roomNew, "created")
|
|
||||||
if key != "" {
|
|
||||||
roomNew.key = &key
|
|
||||||
roomNew.StateSave()
|
|
||||||
}
|
|
||||||
roomSink <- ClientEvent{client, EventNew, ""}
|
|
||||||
continue
|
|
||||||
Denied:
|
|
||||||
client.ReplyNicknamed("475", room, "Cannot join channel (+k) - bad key")
|
|
||||||
Joined:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Processor(events chan ClientEvent, finished chan struct{}) {
|
func Processor(events chan ClientEvent, finished chan struct{}) {
|
||||||
var now time.Time
|
var now time.Time
|
||||||
|
ticker := time.NewTicker(10 * time.Second)
|
||||||
go func() {
|
go func() {
|
||||||
for {
|
for range ticker.C {
|
||||||
time.Sleep(10 * time.Second)
|
|
||||||
events <- ClientEvent{eventType: EventTick}
|
events <- ClientEvent{eventType: EventTick}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
for event := range events {
|
EventsCycle:
|
||||||
|
for e := range events {
|
||||||
now = time.Now()
|
now = time.Now()
|
||||||
client := event.client
|
client := e.client
|
||||||
switch event.eventType {
|
switch e.eventType {
|
||||||
case EventTick:
|
case EventTick:
|
||||||
clientsM.RLock()
|
clientsLock.RLock()
|
||||||
for c := range clients {
|
for c := range clients {
|
||||||
if c.recvTimestamp.Add(PingTimeout).Before(now) {
|
if c.recvTimestamp.Add(PingTimeout).Before(now) {
|
||||||
|
if *verbose {
|
||||||
log.Println(c, "ping timeout")
|
log.Println(c, "ping timeout")
|
||||||
|
}
|
||||||
c.Close()
|
c.Close()
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@ -327,57 +59,55 @@ func Processor(events chan ClientEvent, finished chan struct{}) {
|
|||||||
c.Msg("PING :" + *hostname)
|
c.Msg("PING :" + *hostname)
|
||||||
c.sendTimestamp = time.Now()
|
c.sendTimestamp = time.Now()
|
||||||
} else {
|
} else {
|
||||||
|
if *verbose {
|
||||||
log.Println(c, "ping timeout")
|
log.Println(c, "ping timeout")
|
||||||
|
}
|
||||||
c.Close()
|
c.Close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
clientsM.RUnlock()
|
clientsLock.RUnlock()
|
||||||
roomsM.Lock()
|
roomsLock.Lock()
|
||||||
for rn, r := range rooms {
|
for rn, r := range rooms {
|
||||||
if *statedir == "" && len(r.members) == 0 {
|
if *statedir == "" && len(r.members) == 0 {
|
||||||
|
if *verbose {
|
||||||
log.Println(rn, "emptied room")
|
log.Println(rn, "emptied room")
|
||||||
|
}
|
||||||
delete(rooms, rn)
|
delete(rooms, rn)
|
||||||
close(roomSinks[r])
|
close(r.events)
|
||||||
delete(roomSinks, r)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
roomsM.Unlock()
|
roomsLock.Unlock()
|
||||||
case EventTerm:
|
case EventTerm:
|
||||||
roomsM.RLock()
|
break EventsCycle
|
||||||
for _, sink := range roomSinks {
|
|
||||||
sink <- ClientEvent{eventType: EventTerm}
|
|
||||||
}
|
|
||||||
roomsM.RUnlock()
|
|
||||||
roomsGroup.Wait()
|
|
||||||
close(finished)
|
|
||||||
return
|
|
||||||
case EventNew:
|
case EventNew:
|
||||||
clientsM.Lock()
|
clientsLock.Lock()
|
||||||
clients[client] = struct{}{}
|
clients[client] = struct{}{}
|
||||||
clientsM.Unlock()
|
clientsLock.Unlock()
|
||||||
case EventDel:
|
case EventDel:
|
||||||
clientsM.Lock()
|
clientsLock.Lock()
|
||||||
delete(clients, client)
|
delete(clients, client)
|
||||||
clientsM.Unlock()
|
clientsLock.Unlock()
|
||||||
roomsM.RLock()
|
roomsLock.RLock()
|
||||||
for _, roomSink := range roomSinks {
|
for _, r := range rooms {
|
||||||
roomSink <- event
|
r.events <- e
|
||||||
}
|
}
|
||||||
roomsM.RUnlock()
|
roomsLock.RUnlock()
|
||||||
case EventMsg:
|
case EventMsg:
|
||||||
cols := strings.SplitN(event.text, " ", 2)
|
cols := strings.SplitN(e.text, " ", 2)
|
||||||
cmd := strings.ToUpper(cols[0])
|
cmd := strings.ToUpper(cols[0])
|
||||||
if *verbose {
|
if *verbose {
|
||||||
log.Println(client, "command", cmd)
|
log.Println(client, "command", cmd)
|
||||||
}
|
}
|
||||||
if cmd == "QUIT" {
|
if cmd == "QUIT" {
|
||||||
log.Println(client, "quit")
|
|
||||||
client.Close()
|
client.Close()
|
||||||
|
if *verbose {
|
||||||
|
log.Println(client, "quit")
|
||||||
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if !client.registered {
|
if !client.registered {
|
||||||
ClientRegister(client, cmd, cols)
|
client.Register(cmd, cols)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if client != nil {
|
if client != nil {
|
||||||
@ -386,68 +116,83 @@ func Processor(events chan ClientEvent, finished chan struct{}) {
|
|||||||
switch cmd {
|
switch cmd {
|
||||||
case "AWAY":
|
case "AWAY":
|
||||||
if len(cols) == 1 {
|
if len(cols) == 1 {
|
||||||
client.away = nil
|
client.away = ""
|
||||||
client.ReplyNicknamed("305", "You are no longer marked as being away")
|
client.ReplyNicknamed("305", "You are no longer marked as being away")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
msg := strings.TrimLeft(cols[1], ":")
|
client.away = strings.TrimLeft(cols[1], ":")
|
||||||
client.away = &msg
|
|
||||||
client.ReplyNicknamed("306", "You have been marked as being away")
|
client.ReplyNicknamed("306", "You have been marked as being away")
|
||||||
case "JOIN":
|
case "JOIN":
|
||||||
if len(cols) == 1 || len(cols[1]) < 1 {
|
if len(cols) == 1 || len(cols[1]) < 1 {
|
||||||
client.ReplyNotEnoughParameters("JOIN")
|
client.ReplyNotEnoughParameters("JOIN")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
HandlerJoin(client, cols[1])
|
client.Join(cols[1])
|
||||||
case "LIST":
|
case "LIST":
|
||||||
SendList(client, cols)
|
client.SendList(cols)
|
||||||
case "LUSERS":
|
case "LUSERS":
|
||||||
SendLusers(client)
|
client.SendLusers()
|
||||||
case "MODE":
|
case "MODE":
|
||||||
if len(cols) == 1 || len(cols[1]) < 1 {
|
if len(cols) == 1 || len(cols[1]) < 1 {
|
||||||
client.ReplyNotEnoughParameters("MODE")
|
client.ReplyNotEnoughParameters("MODE")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
cols = strings.SplitN(cols[1], " ", 2)
|
cols = strings.SplitN(cols[1], " ", 2)
|
||||||
if cols[0] == *client.username {
|
if cols[0] == client.username {
|
||||||
if len(cols) == 1 {
|
client.Msg("221 " + client.nickname + " +w")
|
||||||
client.Msg("221 " + *client.nickname + " +")
|
// client.ReplyNicknamed("501", "Unknown MODE flag")
|
||||||
} else {
|
|
||||||
client.ReplyNicknamed("501", "Unknown MODE flag")
|
|
||||||
}
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
room := cols[0]
|
room := cols[0]
|
||||||
roomsM.RLock()
|
|
||||||
r, found := rooms[room]
|
r, found := rooms[room]
|
||||||
if !found {
|
if !found {
|
||||||
client.ReplyNoChannel(room)
|
client.ReplyNoChannel(room)
|
||||||
roomsM.RUnlock()
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if len(cols) == 1 {
|
if len(cols) == 1 {
|
||||||
roomSinks[r] <- ClientEvent{client, EventMode, ""}
|
r.events <- ClientEvent{client, EventMode, ""}
|
||||||
} else {
|
} else {
|
||||||
roomSinks[r] <- ClientEvent{client, EventMode, cols[1]}
|
r.events <- ClientEvent{client, EventMode, cols[1]}
|
||||||
}
|
}
|
||||||
roomsM.RUnlock()
|
|
||||||
case "MOTD":
|
case "MOTD":
|
||||||
SendMotd(client)
|
client.SendMotd()
|
||||||
|
case "NAMES":
|
||||||
|
rs := make([]*Room, len(cols))
|
||||||
|
roomsLock.RLock()
|
||||||
|
if len(cols) == 0 {
|
||||||
|
for _, r := range rooms {
|
||||||
|
rs = append(rs, r)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
needed := make(map[string]struct{}, len(rs))
|
||||||
|
for _, r := range cols {
|
||||||
|
needed[r] = struct{}{}
|
||||||
|
}
|
||||||
|
for rn, r := range rooms {
|
||||||
|
if _, found := needed[rn]; found {
|
||||||
|
rs = append(rs, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
roomsLock.RUnlock()
|
||||||
|
for _, r := range rs {
|
||||||
|
r.SendNames(client)
|
||||||
|
}
|
||||||
case "PART":
|
case "PART":
|
||||||
if len(cols) == 1 || len(cols[1]) < 1 {
|
if len(cols) == 1 || len(cols[1]) < 1 {
|
||||||
client.ReplyNotEnoughParameters("PART")
|
client.ReplyNotEnoughParameters("PART")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
rs := strings.Split(cols[1], " ")[0]
|
rs := strings.Split(cols[1], " ")[0]
|
||||||
roomsM.RLock()
|
roomsLock.RLock()
|
||||||
for _, room := range strings.Split(rs, ",") {
|
for _, room := range strings.Split(rs, ",") {
|
||||||
if r, found := rooms[room]; found {
|
if r, found := rooms[room]; found {
|
||||||
roomSinks[r] <- ClientEvent{client, EventDel, ""}
|
r.events <- ClientEvent{client, EventDel, ""}
|
||||||
} else {
|
} else {
|
||||||
client.ReplyNoChannel(room)
|
client.ReplyNoChannel(room)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
roomsM.RUnlock()
|
roomsLock.RUnlock()
|
||||||
case "PING":
|
case "PING":
|
||||||
if len(cols) == 1 {
|
if len(cols) == 1 {
|
||||||
client.ReplyNicknamed("409", "No origin specified")
|
client.ReplyNicknamed("409", "No origin specified")
|
||||||
@ -466,43 +211,43 @@ func Processor(events chan ClientEvent, finished chan struct{}) {
|
|||||||
client.ReplyNicknamed("412", "No text to send")
|
client.ReplyNicknamed("412", "No text to send")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
msg := ""
|
|
||||||
target := strings.ToLower(cols[0])
|
target := strings.ToLower(cols[0])
|
||||||
clientsM.RLock()
|
roomsLock.RLock()
|
||||||
for c := range clients {
|
|
||||||
if *c.nickname == target {
|
|
||||||
msg = fmt.Sprintf(":%s %s %s %s", client, cmd, *c.nickname, cols[1])
|
|
||||||
c.Msg(msg)
|
|
||||||
if c.away != nil {
|
|
||||||
client.ReplyNicknamed("301", *c.nickname, *c.away)
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
clientsM.RUnlock()
|
|
||||||
if msg != "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
roomsM.RLock()
|
|
||||||
if r, found := rooms[target]; found {
|
if r, found := rooms[target]; found {
|
||||||
roomSinks[r] <- ClientEvent{
|
r.events <- ClientEvent{
|
||||||
client,
|
client,
|
||||||
EventMsg,
|
EventMsg,
|
||||||
cmd + " " + strings.TrimLeft(cols[1], ":"),
|
cmd + " " + strings.TrimLeft(cols[1], ":"),
|
||||||
}
|
}
|
||||||
} else {
|
roomsLock.RUnlock()
|
||||||
client.ReplyNoNickChan(target)
|
continue
|
||||||
}
|
}
|
||||||
roomsM.RUnlock()
|
roomsLock.RUnlock()
|
||||||
|
var msg string
|
||||||
|
clientsLock.RLock()
|
||||||
|
for c := range clients {
|
||||||
|
if c.nickname != target {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
msg = fmt.Sprintf(":%s %s %s %s", client, cmd, c.nickname, cols[1])
|
||||||
|
c.Msg(msg)
|
||||||
|
if c.away != "" {
|
||||||
|
client.ReplyNicknamed("301", c.nickname, c.away)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
clientsLock.RUnlock()
|
||||||
|
if msg != "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
client.ReplyNoNickChan(target)
|
||||||
case "TOPIC":
|
case "TOPIC":
|
||||||
if len(cols) == 1 {
|
if len(cols) == 1 {
|
||||||
client.ReplyNotEnoughParameters("TOPIC")
|
client.ReplyNotEnoughParameters("TOPIC")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
cols = strings.SplitN(cols[1], " ", 2)
|
cols = strings.SplitN(cols[1], " ", 2)
|
||||||
roomsM.RLock()
|
|
||||||
r, found := rooms[cols[0]]
|
r, found := rooms[cols[0]]
|
||||||
roomsM.RUnlock()
|
|
||||||
if !found {
|
if !found {
|
||||||
client.ReplyNoChannel(cols[0])
|
client.ReplyNoChannel(cols[0])
|
||||||
continue
|
continue
|
||||||
@ -510,25 +255,20 @@ func Processor(events chan ClientEvent, finished chan struct{}) {
|
|||||||
var change string
|
var change string
|
||||||
if len(cols) > 1 {
|
if len(cols) > 1 {
|
||||||
change = cols[1]
|
change = cols[1]
|
||||||
} else {
|
|
||||||
change = ""
|
|
||||||
}
|
}
|
||||||
roomsM.RLock()
|
r.events <- ClientEvent{client, EventTopic, change}
|
||||||
roomSinks[r] <- ClientEvent{client, EventTopic, change}
|
|
||||||
roomsM.RUnlock()
|
|
||||||
case "WHO":
|
case "WHO":
|
||||||
if len(cols) == 1 || len(cols[1]) < 1 {
|
if len(cols) == 1 || len(cols[1]) < 1 {
|
||||||
client.ReplyNotEnoughParameters("WHO")
|
client.ReplyNotEnoughParameters("WHO")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
room := strings.Split(cols[1], " ")[0]
|
room := strings.Split(cols[1], " ")[0]
|
||||||
roomsM.RLock()
|
r, found := rooms[room]
|
||||||
if r, found := rooms[room]; found {
|
if found {
|
||||||
roomSinks[r] <- ClientEvent{client, EventWho, ""}
|
r.events <- ClientEvent{client, EventWho, ""}
|
||||||
} else {
|
} else {
|
||||||
client.ReplyNoChannel(room)
|
client.ReplyNoChannel(room)
|
||||||
}
|
}
|
||||||
roomsM.RUnlock()
|
|
||||||
case "WHOIS":
|
case "WHOIS":
|
||||||
if len(cols) == 1 || len(cols[1]) < 1 {
|
if len(cols) == 1 || len(cols[1]) < 1 {
|
||||||
client.ReplyNotEnoughParameters("WHOIS")
|
client.ReplyNotEnoughParameters("WHOIS")
|
||||||
@ -536,31 +276,51 @@ func Processor(events chan ClientEvent, finished chan struct{}) {
|
|||||||
}
|
}
|
||||||
cols := strings.Split(cols[1], " ")
|
cols := strings.Split(cols[1], " ")
|
||||||
nicknames := strings.Split(cols[len(cols)-1], ",")
|
nicknames := strings.Split(cols[len(cols)-1], ",")
|
||||||
SendWhois(client, nicknames)
|
client.SendWhois(nicknames)
|
||||||
case "ISON":
|
case "ISON":
|
||||||
if len(cols) == 1 || len(cols[1]) < 1 {
|
if len(cols) == 1 || len(cols[1]) < 1 {
|
||||||
client.ReplyNotEnoughParameters("ISON")
|
client.ReplyNotEnoughParameters("ISON")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
nicksKnown := make(map[string]struct{})
|
nicknamesList := strings.Split(cols[1], " ")
|
||||||
clientsM.RLock()
|
nicknames := make(map[string]bool, len(nicknamesList))
|
||||||
|
for _, 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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
client.ReplyNicknamed("303", strings.Join(nicksExists, " "))
|
clientsLock.RUnlock()
|
||||||
|
nicknamesList = nicknamesList[:0]
|
||||||
|
for n, exists := range nicknames {
|
||||||
|
if exists {
|
||||||
|
nicknamesList = append(nicknamesList, n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
client.ReplyNicknamed("303", strings.Join(nicknamesList, " "))
|
||||||
|
case "WALLOPS":
|
||||||
|
if len(cols) == 1 {
|
||||||
|
client.ReplyNotEnoughParameters("WALLOPS")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cs := make([]*Client, 0, len(clients))
|
||||||
|
clientsLock.RLock()
|
||||||
|
for c := range clients {
|
||||||
|
if c != client {
|
||||||
|
cs = append(cs, c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
clientsLock.RUnlock()
|
||||||
|
for _, c := range cs {
|
||||||
|
c.Msg(fmt.Sprintf(":%s NOTICE %s %s", client, c.nickname, cols[1]))
|
||||||
|
}
|
||||||
case "VERSION":
|
case "VERSION":
|
||||||
var debug string
|
var debug string
|
||||||
if *verbose {
|
if *verbose {
|
||||||
debug = "debug"
|
debug = "debug"
|
||||||
} else {
|
|
||||||
debug = ""
|
|
||||||
}
|
}
|
||||||
client.ReplyNicknamed("351", fmt.Sprintf("%s.%s %s :", Version, debug, *hostname))
|
client.ReplyNicknamed("351", fmt.Sprintf("%s.%s %s :", Version, debug, *hostname))
|
||||||
default:
|
default:
|
||||||
@ -568,4 +328,47 @@ func Processor(events chan ClientEvent, finished chan struct{}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ticker.Stop()
|
||||||
|
|
||||||
|
// Notify all clients about shutdown
|
||||||
|
clientsLock.RLock()
|
||||||
|
for c := range clients {
|
||||||
|
c.Msg(fmt.Sprintf(
|
||||||
|
":%s NOTICE %s %s", *hostname, c.nickname,
|
||||||
|
":Server is shutting down",
|
||||||
|
))
|
||||||
|
c.Close()
|
||||||
|
}
|
||||||
|
clientsLock.RUnlock()
|
||||||
|
|
||||||
|
// Read their EventDel
|
||||||
|
go func() {
|
||||||
|
for range events {
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Stop room processors
|
||||||
|
roomsLock.RLock()
|
||||||
|
for _, r := range rooms {
|
||||||
|
r.events <- ClientEvent{eventType: EventTerm}
|
||||||
|
}
|
||||||
|
roomsLock.RUnlock()
|
||||||
|
roomsWG.Wait()
|
||||||
|
|
||||||
|
// Wait for either 5sec or all clients quitting
|
||||||
|
t := time.NewTimer(5 * time.Second)
|
||||||
|
clientsDone := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
clientsWG.Wait()
|
||||||
|
close(clientsDone)
|
||||||
|
}()
|
||||||
|
select {
|
||||||
|
case <-t.C:
|
||||||
|
case <-clientsDone:
|
||||||
|
}
|
||||||
|
if !t.Stop() {
|
||||||
|
<-t.C
|
||||||
|
}
|
||||||
|
close(events)
|
||||||
|
close(finished)
|
||||||
}
|
}
|
||||||
|
@ -28,13 +28,14 @@ func TestRegistrationWorkflow(t *testing.T) {
|
|||||||
host := "foohost"
|
host := "foohost"
|
||||||
hostname = &host
|
hostname = &host
|
||||||
events := make(chan ClientEvent)
|
events := make(chan ClientEvent)
|
||||||
|
finished := make(chan struct{})
|
||||||
defer func() {
|
defer func() {
|
||||||
events <- ClientEvent{eventType: EventTerm}
|
events <- ClientEvent{eventType: EventTerm}
|
||||||
|
<-finished
|
||||||
}()
|
}()
|
||||||
go Processor(events, make(chan struct{}))
|
go Processor(events, finished)
|
||||||
conn := NewTestingConn()
|
conn := NewTestingConn()
|
||||||
client := NewClient(conn)
|
client := NewClient(conn, events)
|
||||||
go client.Processor(events)
|
|
||||||
|
|
||||||
conn.inbound <- "UNEXISTENT CMD" // should receive nothing on this
|
conn.inbound <- "UNEXISTENT CMD" // should receive nothing on this
|
||||||
conn.inbound <- "NICK"
|
conn.inbound <- "NICK"
|
||||||
@ -54,7 +55,7 @@ func TestRegistrationWorkflow(t *testing.T) {
|
|||||||
if r := <-conn.outbound; r != ":foohost 461 meinick USER :Not enough parameters\r\n" {
|
if r := <-conn.outbound; r != ":foohost 461 meinick USER :Not enough parameters\r\n" {
|
||||||
t.Fatal("461 for USER", r)
|
t.Fatal("461 for USER", r)
|
||||||
}
|
}
|
||||||
if (*client.nickname != "meinick") || client.registered {
|
if (client.nickname != "meinick") || client.registered {
|
||||||
t.Fatal("NICK saved")
|
t.Fatal("NICK saved")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -63,7 +64,7 @@ func TestRegistrationWorkflow(t *testing.T) {
|
|||||||
t.Fatal("461 again for USER", r)
|
t.Fatal("461 again for USER", r)
|
||||||
}
|
}
|
||||||
|
|
||||||
SendLusers(client)
|
client.SendLusers()
|
||||||
if r := <-conn.outbound; !strings.Contains(r, "There are 0 users") {
|
if r := <-conn.outbound; !strings.Contains(r, "There are 0 users") {
|
||||||
t.Fatal("LUSERS", r)
|
t.Fatal("LUSERS", r)
|
||||||
}
|
}
|
||||||
@ -87,7 +88,7 @@ func TestRegistrationWorkflow(t *testing.T) {
|
|||||||
if r := <-conn.outbound; !strings.Contains(r, ":foohost 422") {
|
if r := <-conn.outbound; !strings.Contains(r, ":foohost 422") {
|
||||||
t.Fatal("422 after registration", r)
|
t.Fatal("422 after registration", r)
|
||||||
}
|
}
|
||||||
if (*client.username != "1") || (*client.realname != "4 5") || !client.registered {
|
if (client.username != "1") || (client.realname != "4 5") || !client.registered {
|
||||||
t.Fatal("client register")
|
t.Fatal("client register")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -98,7 +99,7 @@ func TestRegistrationWorkflow(t *testing.T) {
|
|||||||
t.Fatal("reply for unexistent command", r)
|
t.Fatal("reply for unexistent command", r)
|
||||||
}
|
}
|
||||||
|
|
||||||
SendLusers(client)
|
client.SendLusers()
|
||||||
if r := <-conn.outbound; !strings.Contains(r, "There are 1 users") {
|
if r := <-conn.outbound; !strings.Contains(r, "There are 1 users") {
|
||||||
t.Fatal("1 users logged in", r)
|
t.Fatal("1 users logged in", r)
|
||||||
}
|
}
|
||||||
@ -122,11 +123,14 @@ func TestMotd(t *testing.T) {
|
|||||||
conn := NewTestingConn()
|
conn := NewTestingConn()
|
||||||
host := "foohost"
|
host := "foohost"
|
||||||
hostname = &host
|
hostname = &host
|
||||||
client := NewClient(conn)
|
client := NewClient(conn, make(chan ClientEvent, 2))
|
||||||
|
defer func() {
|
||||||
|
client.Close()
|
||||||
|
}()
|
||||||
motdName := fd.Name()
|
motdName := fd.Name()
|
||||||
motd = &motdName
|
motd = &motdName
|
||||||
|
|
||||||
SendMotd(client)
|
client.SendMotd()
|
||||||
if r := <-conn.outbound; !strings.HasPrefix(r, ":foohost 375") {
|
if r := <-conn.outbound; !strings.HasPrefix(r, ":foohost 375") {
|
||||||
t.Fatal("MOTD start", r)
|
t.Fatal("MOTD start", r)
|
||||||
}
|
}
|
||||||
|
119
events.go
119
events.go
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
275
goircd.go
275
goircd.go
@ -23,114 +23,213 @@ import (
|
|||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"syscall"
|
||||||
)
|
)
|
||||||
|
|
||||||
const Version = "1.8.2"
|
const (
|
||||||
|
Version = "1.9.0"
|
||||||
|
|
||||||
var (
|
StateTopicFilename = "topic"
|
||||||
hostname = flag.String("hostname", "localhost", "Hostname")
|
StateKeyFilename = "key"
|
||||||
bind = flag.String("bind", ":6667", "Address to bind to")
|
|
||||||
motd = flag.String("motd", "", "Path to MOTD file")
|
EventNew = iota
|
||||||
logdir = flag.String("logdir", "", "Absolute path to directory for logs")
|
EventDel = iota
|
||||||
statedir = flag.String("statedir", "", "Absolute path to directory for states")
|
EventMsg = iota
|
||||||
passwords = flag.String("passwords", "", "Optional path to passwords file")
|
EventTopic = iota
|
||||||
tlsBind = flag.String("tlsbind", "", "TLS address to bind to")
|
EventWho = iota
|
||||||
tlsPEM = flag.String("tlspem", "", "Path to TLS certificat+key PEM file")
|
EventMode = iota
|
||||||
verbose = flag.Bool("v", false, "Enable verbose logging.")
|
EventTerm = iota
|
||||||
|
EventTick = iota
|
||||||
)
|
)
|
||||||
|
|
||||||
func listenerLoop(sock net.Listener, events chan ClientEvent) {
|
type ClientEvent struct {
|
||||||
for {
|
client *Client
|
||||||
conn, err := sock.Accept()
|
eventType int
|
||||||
if err != nil {
|
text string
|
||||||
log.Println("Error during accepting connection", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
client := NewClient(conn)
|
|
||||||
go client.Processor(events)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func Run() {
|
type StateEvent struct {
|
||||||
events := make(chan ClientEvent)
|
where string
|
||||||
log.SetFlags(log.Ldate | log.Lmicroseconds | log.Lshortfile)
|
topic string
|
||||||
|
key string
|
||||||
|
}
|
||||||
|
|
||||||
if *logdir == "" {
|
var (
|
||||||
// Dummy logger
|
hostname = flag.String("hostname", "localhost", "hostname")
|
||||||
go func() {
|
bind = flag.String("bind", "[::1]:6667", "address to bind to")
|
||||||
for _ = range logSink {
|
cloak = flag.String("cloak", "", "cloak user's host with the given hostname")
|
||||||
}
|
motd = flag.String("motd", "", "path to MOTD file")
|
||||||
}()
|
logdir = flag.String("logdir", "", "absolute path to directory for logs")
|
||||||
} else {
|
statedir = flag.String("statedir", "", "absolute path to directory for states")
|
||||||
if !path.IsAbs(*logdir) {
|
passwords = flag.String("passwd", "", "optional path to passwords file")
|
||||||
log.Fatalln("Need absolute path for logdir")
|
tlsBind = flag.String("tlsbind", "", "TLS address to bind to")
|
||||||
}
|
tlsPEM = flag.String("tlspem", "", "path to TLS certificat+key PEM file")
|
||||||
go Logger(*logdir, logSink)
|
permStateDirS = flag.String("perm-state-dir", "755", "state directory permissions")
|
||||||
log.Println(*logdir, "logger initialized")
|
permStateFileS = flag.String("perm-state-file", "600", "state files permissions")
|
||||||
}
|
permLogFileS = flag.String("perm-log-file", "644", "log files permissions")
|
||||||
|
timestamped = flag.Bool("timestamped", false, "enable timestamps on stderr messages")
|
||||||
|
verbose = flag.Bool("verbose", false, "enable verbose logging")
|
||||||
|
debug = flag.Bool("debug", false, "enable debug (traffic) logging")
|
||||||
|
|
||||||
log.Println("goircd " + Version + " is starting")
|
permStateDir os.FileMode
|
||||||
if *statedir == "" {
|
permStateFile os.FileMode
|
||||||
// Dummy statekeeper
|
permLogFile os.FileMode
|
||||||
go func() {
|
|
||||||
for _ = range stateSink {
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
} else {
|
|
||||||
if !path.IsAbs(*statedir) {
|
|
||||||
log.Fatalln("Need absolute path for statedir")
|
|
||||||
}
|
|
||||||
states, err := filepath.Glob(path.Join(*statedir, "#*"))
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalln("Can not read statedir", err)
|
|
||||||
}
|
|
||||||
for _, state := range states {
|
|
||||||
buf, err := ioutil.ReadFile(state)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Can not read state %s: %v", state, err)
|
|
||||||
}
|
|
||||||
room, _ := RoomRegister(path.Base(state))
|
|
||||||
contents := strings.Split(string(buf), "\n")
|
|
||||||
if len(contents) < 2 {
|
|
||||||
log.Printf("State corrupted for %s: %q", *room.name, contents)
|
|
||||||
} else {
|
|
||||||
room.topic = &contents[0]
|
|
||||||
room.key = &contents[1]
|
|
||||||
log.Println("Loaded state for room", *room.name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
go StateKeeper(*statedir, stateSink)
|
|
||||||
log.Println(*statedir, "statekeeper initialized")
|
|
||||||
}
|
|
||||||
|
|
||||||
if *bind != "" {
|
stateSink chan StateEvent = make(chan StateEvent)
|
||||||
listener, err := net.Listen("tcp", *bind)
|
)
|
||||||
|
|
||||||
|
func permParse(s string) os.FileMode {
|
||||||
|
r, err := strconv.ParseUint(s, 8, 16)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Can not listen on %s: %v", *bind, err)
|
log.Fatalln(err)
|
||||||
}
|
}
|
||||||
log.Println("Raw listening on", *bind)
|
return os.FileMode(r)
|
||||||
go listenerLoop(listener, events)
|
}
|
||||||
}
|
|
||||||
if *tlsBind != "" {
|
func listenerLoop(ln net.Listener, events chan ClientEvent) {
|
||||||
cert, err := tls.LoadX509KeyPair(*tlsPEM, *tlsPEM)
|
for {
|
||||||
|
conn, err := ln.Accept()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Could not load TLS keys from %s: %s", *tlsPEM, err)
|
log.Println("error during accept", err)
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
config := tls.Config{Certificates: []tls.Certificate{cert}}
|
NewClient(conn, events)
|
||||||
listenerTLS, err := tls.Listen("tcp", *tlsBind, &config)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Can not listen on %s: %v", *tlsBind, err)
|
|
||||||
}
|
}
|
||||||
log.Println("TLS listening on", *tlsBind)
|
|
||||||
go listenerLoop(listenerTLS, events)
|
|
||||||
}
|
|
||||||
Processor(events, make(chan struct{}))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
Run()
|
permStateDir = permParse(*permStateDirS)
|
||||||
|
permStateFile = permParse(*permStateFileS)
|
||||||
|
permLogFile = permParse(*permLogFileS)
|
||||||
|
|
||||||
|
if *timestamped {
|
||||||
|
log.SetFlags(log.Ldate | log.Lmicroseconds | log.Lshortfile)
|
||||||
|
} else {
|
||||||
|
log.SetFlags(log.Lshortfile)
|
||||||
|
}
|
||||||
|
log.SetOutput(os.Stdout)
|
||||||
|
|
||||||
|
if *logdir == "" {
|
||||||
|
// Dummy logger
|
||||||
|
go func() {
|
||||||
|
for range logSink {
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
} else {
|
||||||
|
if !path.IsAbs(*logdir) {
|
||||||
|
log.Fatalln("need absolute path for logdir")
|
||||||
|
}
|
||||||
|
go Logger(*logdir, logSink)
|
||||||
|
}
|
||||||
|
|
||||||
|
if *statedir == "" {
|
||||||
|
// Dummy statekeeper
|
||||||
|
go func() {
|
||||||
|
for range stateSink {
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
} else {
|
||||||
|
if !path.IsAbs(*statedir) {
|
||||||
|
log.Fatalln("need absolute path for statedir")
|
||||||
|
}
|
||||||
|
states, err := filepath.Glob(path.Join(*statedir, "#*"))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln("can not read statedir", err)
|
||||||
|
}
|
||||||
|
for _, state := range states {
|
||||||
|
buf, err := ioutil.ReadFile(path.Join(state, StateTopicFilename))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf(
|
||||||
|
"can not read state %s/%s: %v",
|
||||||
|
state, StateTopicFilename, err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
room := RoomRegister(path.Base(state))
|
||||||
|
room.topic = strings.TrimRight(string(buf), "\n")
|
||||||
|
buf, err = ioutil.ReadFile(path.Join(state, StateKeyFilename))
|
||||||
|
if err == nil {
|
||||||
|
room.key = strings.TrimRight(string(buf), "\n")
|
||||||
|
} else {
|
||||||
|
if !os.IsNotExist(err) {
|
||||||
|
log.Fatalf(
|
||||||
|
"can not read state %s/%s: %v",
|
||||||
|
state, StateKeyFilename, err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Println("loaded state for room:", room.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for event := range stateSink {
|
||||||
|
statePath := path.Join(*statedir, event.where)
|
||||||
|
if _, err := os.Stat(statePath); os.IsNotExist(err) {
|
||||||
|
if err := os.Mkdir(statePath, permStateDir); err != nil {
|
||||||
|
log.Printf("can not create state %s: %v", statePath, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
topicPath := path.Join(statePath, StateTopicFilename)
|
||||||
|
if err := ioutil.WriteFile(
|
||||||
|
topicPath,
|
||||||
|
[]byte(event.topic+"\n"),
|
||||||
|
permStateFile,
|
||||||
|
); err != nil {
|
||||||
|
log.Printf("can not write statefile %s: %v", topicPath, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
keyPath := path.Join(statePath, StateKeyFilename)
|
||||||
|
if err := ioutil.WriteFile(
|
||||||
|
keyPath,
|
||||||
|
[]byte(event.key+"\n"),
|
||||||
|
permStateFile,
|
||||||
|
); err != nil {
|
||||||
|
log.Printf("can not write statefile %s: %v", keyPath, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
events := make(chan ClientEvent)
|
||||||
|
if *bind != "" {
|
||||||
|
ln, err := net.Listen("tcp", *bind)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("can not listen on %s: %v", *bind, err)
|
||||||
|
}
|
||||||
|
go listenerLoop(ln, events)
|
||||||
|
}
|
||||||
|
if *tlsBind != "" {
|
||||||
|
cert, err := tls.LoadX509KeyPair(*tlsPEM, *tlsPEM)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("can not load TLS keys from %s: %s", *tlsPEM, err)
|
||||||
|
}
|
||||||
|
config := tls.Config{Certificates: []tls.Certificate{cert}}
|
||||||
|
ln, err := tls.Listen("tcp", *tlsBind, &config)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("can not listen on %s: %v", *tlsBind, err)
|
||||||
|
}
|
||||||
|
go listenerLoop(ln, events)
|
||||||
|
}
|
||||||
|
log.Println("goircd", Version, "started")
|
||||||
|
|
||||||
|
needsShutdown := make(chan os.Signal, 0)
|
||||||
|
signal.Notify(needsShutdown, syscall.SIGTERM, syscall.SIGINT)
|
||||||
|
go func() {
|
||||||
|
<-needsShutdown
|
||||||
|
events <- ClientEvent{eventType: EventTerm}
|
||||||
|
log.Println("goircd shutting down")
|
||||||
|
}()
|
||||||
|
|
||||||
|
finished := make(chan struct{})
|
||||||
|
go Processor(events, finished)
|
||||||
|
<-finished
|
||||||
}
|
}
|
||||||
|
72
log.go
Normal file
72
log.go
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
/*
|
||||||
|
goircd -- minimalistic simple Internet Relay Chat (IRC) server
|
||||||
|
Copyright (C) 2014-2020 Sergey Matveev <stargrave@stargrave.org>
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, version 3 of the License.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
FormatMsg = "[%s] <%s> %s\n"
|
||||||
|
FormatMeta = "[%s] * %s %s\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
var logSink chan LogEvent = make(chan LogEvent)
|
||||||
|
|
||||||
|
type LogEvent struct {
|
||||||
|
where string
|
||||||
|
who string
|
||||||
|
what string
|
||||||
|
meta bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func Logger(logdir string, events <-chan LogEvent) {
|
||||||
|
mode := os.O_CREATE | os.O_WRONLY | os.O_APPEND
|
||||||
|
perm := os.FileMode(permLogFile)
|
||||||
|
var format string
|
||||||
|
var logfile string
|
||||||
|
var fd *os.File
|
||||||
|
var err error
|
||||||
|
for event := range events {
|
||||||
|
logfile = path.Join(logdir, event.where+".log")
|
||||||
|
fd, err = os.OpenFile(logfile, mode, perm)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("can not open logfile", logfile, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if event.meta {
|
||||||
|
format = FormatMeta
|
||||||
|
} else {
|
||||||
|
format = FormatMsg
|
||||||
|
}
|
||||||
|
_, err = fd.WriteString(fmt.Sprintf(
|
||||||
|
format,
|
||||||
|
time.Now().Format(time.RFC3339),
|
||||||
|
event.who,
|
||||||
|
event.what,
|
||||||
|
))
|
||||||
|
fd.Close()
|
||||||
|
if err != nil {
|
||||||
|
log.Println("error writing to logfile", logfile, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
306
room.go
306
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
|
name string
|
||||||
topic *string
|
topic string
|
||||||
key *string
|
key string
|
||||||
members map[*Client]struct{}
|
members map[*Client]struct{}
|
||||||
|
events chan ClientEvent
|
||||||
sync.RWMutex
|
sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
func (room *Room) String() (name string) {
|
var (
|
||||||
room.RLock()
|
RERoom = regexp.MustCompile("^#[^\x00\x07\x0a\x0d ,:/]{1,200}$")
|
||||||
name = *room.name
|
|
||||||
room.RUnlock()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewRoom(name string) *Room {
|
rooms map[string]*Room = make(map[string]*Room)
|
||||||
topic := ""
|
roomsLock sync.RWMutex
|
||||||
key := ""
|
roomsWG sync.WaitGroup
|
||||||
return &Room{
|
)
|
||||||
name: &name,
|
|
||||||
topic: &topic,
|
|
||||||
key: &key,
|
|
||||||
members: make(map[*Client]struct{}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (room *Room) SendTopic(client *Client) {
|
func (r *Room) SendTopic(c *Client) {
|
||||||
room.RLock()
|
t := r.topic
|
||||||
if *room.topic == "" {
|
if t == "" {
|
||||||
client.ReplyNicknamed("331", room.String(), "No topic is set")
|
c.ReplyNicknamed("331", r.name, "No topic is set")
|
||||||
} else {
|
} else {
|
||||||
client.ReplyNicknamed("332", room.String(), *room.topic)
|
c.ReplyNicknamed("332", r.name, t)
|
||||||
}
|
}
|
||||||
room.RUnlock()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send message to all room's subscribers, possibly excluding someone.
|
func (r *Room) SendNames(c *Client) {
|
||||||
func (room *Room) Broadcast(msg string, clientToIgnore ...*Client) {
|
allowed := false
|
||||||
room.RLock()
|
if r.key == "" {
|
||||||
for member := range room.members {
|
allowed = true
|
||||||
if (len(clientToIgnore) > 0) && member == clientToIgnore[0] {
|
} else if _, isMember := r.members[c]; isMember {
|
||||||
|
allowed = true
|
||||||
|
}
|
||||||
|
if !allowed {
|
||||||
|
c.ReplyNicknamed("475", r.name, "Cannot join channel (+k)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r.RLock()
|
||||||
|
nicknames := make([]string, 0, len(r.members))
|
||||||
|
for member := range r.members {
|
||||||
|
nicknames = append(nicknames, member.nickname)
|
||||||
|
}
|
||||||
|
r.RUnlock()
|
||||||
|
sort.Strings(nicknames)
|
||||||
|
maxLen := 512 - len(*hostname) - 2 - 2
|
||||||
|
|
||||||
|
MoreNicknames:
|
||||||
|
lenAll := 0
|
||||||
|
lenName := 0
|
||||||
|
for i, n := range nicknames {
|
||||||
|
lenName = len(n) + 1
|
||||||
|
if lenAll+lenName >= maxLen {
|
||||||
|
c.ReplyNicknamed("353", "=", r.name, strings.Join(nicknames[:i-1], " "))
|
||||||
|
nicknames = nicknames[i:]
|
||||||
|
goto MoreNicknames
|
||||||
|
}
|
||||||
|
lenAll += lenName
|
||||||
|
}
|
||||||
|
if len(nicknames) > 0 {
|
||||||
|
c.ReplyNicknamed("353", "=", r.name, strings.Join(nicknames, " "))
|
||||||
|
}
|
||||||
|
c.ReplyNicknamed("366", r.name, "End of NAMES list")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Room) Broadcast(msg string, excludes ...*Client) {
|
||||||
|
var exclude *Client
|
||||||
|
if len(excludes) > 0 {
|
||||||
|
exclude = excludes[0]
|
||||||
|
}
|
||||||
|
r.RLock()
|
||||||
|
for member := range r.members {
|
||||||
|
if member == exclude {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
member.Msg(msg)
|
member.Msg(msg)
|
||||||
}
|
}
|
||||||
room.RUnlock()
|
r.RUnlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (room *Room) StateSave() {
|
func (r *Room) StateSave() {
|
||||||
room.RLock()
|
stateSink <- StateEvent{r.name, r.topic, r.key}
|
||||||
stateSink <- StateEvent{room.String(), *room.topic, *room.key}
|
|
||||||
room.RUnlock()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (room *Room) Processor(events <-chan ClientEvent) {
|
func (r *Room) Processor(events <-chan ClientEvent) {
|
||||||
var client *Client
|
for e := range events {
|
||||||
for event := range events {
|
c := e.client
|
||||||
client = event.client
|
switch e.eventType {
|
||||||
switch event.eventType {
|
|
||||||
case EventTerm:
|
case EventTerm:
|
||||||
roomsGroup.Done()
|
roomsWG.Done()
|
||||||
return
|
return
|
||||||
case EventNew:
|
case EventNew:
|
||||||
room.Lock()
|
r.Lock()
|
||||||
room.members[client] = struct{}{}
|
r.members[c] = struct{}{}
|
||||||
|
r.Unlock()
|
||||||
if *verbose {
|
if *verbose {
|
||||||
log.Println(client, "joined", room.name)
|
log.Println(c, "joined", r.name)
|
||||||
}
|
}
|
||||||
room.Unlock()
|
r.SendTopic(c)
|
||||||
room.SendTopic(client)
|
r.Broadcast(fmt.Sprintf(":%s JOIN %s", c, r.name))
|
||||||
room.Broadcast(fmt.Sprintf(":%s JOIN %s", client, room.String()))
|
logSink <- LogEvent{r.name, c.nickname, "joined", true}
|
||||||
logSink <- LogEvent{room.String(), *client.nickname, "joined", true}
|
r.SendNames(c)
|
||||||
nicknames := make([]string, 0)
|
|
||||||
room.RLock()
|
|
||||||
for member := range room.members {
|
|
||||||
nicknames = append(nicknames, *member.nickname)
|
|
||||||
}
|
|
||||||
room.RUnlock()
|
|
||||||
sort.Strings(nicknames)
|
|
||||||
client.ReplyNicknamed("353", "=", room.String(), strings.Join(nicknames, " "))
|
|
||||||
client.ReplyNicknamed("366", room.String(), "End of NAMES list")
|
|
||||||
case EventDel:
|
case EventDel:
|
||||||
room.RLock()
|
if _, subscribed := r.members[c]; !subscribed {
|
||||||
if _, subscribed := room.members[client]; !subscribed {
|
c.ReplyNicknamed("442", r.name, "You are not on that channel")
|
||||||
client.ReplyNicknamed("442", room.String(), "You are not on that channel")
|
|
||||||
room.RUnlock()
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
room.RUnlock()
|
msg := fmt.Sprintf(":%s PART %s :%s", c, r.name, c.nickname)
|
||||||
room.Lock()
|
r.Broadcast(msg)
|
||||||
delete(room.members, client)
|
r.Lock()
|
||||||
room.Unlock()
|
delete(r.members, c)
|
||||||
room.RLock()
|
r.Unlock()
|
||||||
msg := fmt.Sprintf(":%s PART %s :%s", client, room.String(), *client.nickname)
|
logSink <- LogEvent{r.name, c.nickname, "left", true}
|
||||||
room.Broadcast(msg)
|
if *verbose {
|
||||||
logSink <- LogEvent{room.String(), *client.nickname, "left", true}
|
log.Println(c, "left", r.name)
|
||||||
room.RUnlock()
|
}
|
||||||
case EventTopic:
|
case EventTopic:
|
||||||
room.RLock()
|
if _, subscribed := r.members[c]; !subscribed {
|
||||||
if _, subscribed := room.members[client]; !subscribed {
|
c.ReplyParts("442", r.name, "You are not on that channel")
|
||||||
client.ReplyParts("442", room.String(), "You are not on that channel")
|
|
||||||
room.RUnlock()
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if event.text == "" {
|
if e.text == "" {
|
||||||
room.SendTopic(client)
|
r.SendTopic(c)
|
||||||
room.RUnlock()
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
room.RUnlock()
|
topic := strings.TrimLeft(e.text, ":")
|
||||||
topic := strings.TrimLeft(event.text, ":")
|
r.topic = topic
|
||||||
room.Lock()
|
msg := fmt.Sprintf(":%s TOPIC %s :%s", c, r.name, r.topic)
|
||||||
room.topic = &topic
|
r.Broadcast(msg)
|
||||||
room.Unlock()
|
logSink <- LogEvent{r.name, c.nickname, "set topic to " + r.topic, true}
|
||||||
room.RLock()
|
r.StateSave()
|
||||||
msg := fmt.Sprintf(":%s TOPIC %s :%s", client, room.String(), *room.topic)
|
|
||||||
room.Broadcast(msg)
|
|
||||||
logSink <- LogEvent{
|
|
||||||
room.String(),
|
|
||||||
*client.nickname,
|
|
||||||
"set topic to " + *room.topic,
|
|
||||||
true,
|
|
||||||
}
|
|
||||||
room.RUnlock()
|
|
||||||
room.StateSave()
|
|
||||||
case EventWho:
|
case EventWho:
|
||||||
room.RLock()
|
r.RLock()
|
||||||
for m := range room.members {
|
for m := range r.members {
|
||||||
client.ReplyNicknamed(
|
c.ReplyNicknamed(
|
||||||
"352",
|
"352",
|
||||||
room.String(),
|
r.name,
|
||||||
*m.username,
|
m.username,
|
||||||
m.Host(),
|
m.Host(),
|
||||||
*hostname,
|
*hostname,
|
||||||
*m.nickname,
|
m.nickname,
|
||||||
"H",
|
"H",
|
||||||
"0 "+*m.realname,
|
"0 "+m.realname,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
client.ReplyNicknamed("315", room.String(), "End of /WHO list")
|
c.ReplyNicknamed("315", r.name, "End of /WHO list")
|
||||||
room.RUnlock()
|
r.RUnlock()
|
||||||
case EventMode:
|
case EventMode:
|
||||||
room.RLock()
|
if e.text == "" {
|
||||||
if event.text == "" {
|
mode := "+n"
|
||||||
mode := "+"
|
if r.key != "" {
|
||||||
if *room.key != "" {
|
|
||||||
mode = mode + "k"
|
mode = mode + "k"
|
||||||
}
|
}
|
||||||
client.Msg(fmt.Sprintf("324 %s %s %s", *client.nickname, room.String(), mode))
|
c.Msg(fmt.Sprintf("324 %s %s %s", c.nickname, r.name, mode))
|
||||||
room.RUnlock()
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if strings.HasPrefix(event.text, "b") {
|
if strings.HasPrefix(e.text, "b") {
|
||||||
client.ReplyNicknamed("368", room.String(), "End of channel ban list")
|
c.ReplyNicknamed("368", r.name, "End of channel ban list")
|
||||||
room.RUnlock()
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if strings.HasPrefix(event.text, "-k") || strings.HasPrefix(event.text, "+k") {
|
if strings.HasPrefix(e.text, "-k") || strings.HasPrefix(e.text, "+k") {
|
||||||
if _, subscribed := room.members[client]; !subscribed {
|
if _, subscribed := r.members[c]; !subscribed {
|
||||||
client.ReplyParts("442", room.String(), "You are not on that channel")
|
c.ReplyParts("442", r.name, "You are not on that channel")
|
||||||
room.RUnlock()
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
client.ReplyNicknamed("472", event.text, "Unknown MODE flag")
|
c.ReplyNicknamed("472", e.text, "Unknown MODE flag")
|
||||||
room.RUnlock()
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
room.RUnlock()
|
|
||||||
var msg string
|
var msg string
|
||||||
var msgLog string
|
var msgLog string
|
||||||
if strings.HasPrefix(event.text, "+k") {
|
if strings.HasPrefix(e.text, "+k") {
|
||||||
cols := strings.Split(event.text, " ")
|
cols := strings.Split(e.text, " ")
|
||||||
if len(cols) == 1 {
|
if len(cols) == 1 {
|
||||||
client.ReplyNotEnoughParameters("MODE")
|
c.ReplyNotEnoughParameters("MODE")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
room.Lock()
|
r.key = cols[1]
|
||||||
room.key = &cols[1]
|
msg = fmt.Sprintf(":%s MODE %s +k %s", c, r.name, r.key)
|
||||||
msg = fmt.Sprintf(":%s MODE %s +k %s", client, *room.name, *room.key)
|
msgLog = "set channel key"
|
||||||
msgLog = "set channel key to " + *room.key
|
|
||||||
room.Unlock()
|
|
||||||
} else {
|
} else {
|
||||||
key := ""
|
r.key = ""
|
||||||
room.Lock()
|
msg = fmt.Sprintf(":%s MODE %s -k", c, r.name)
|
||||||
room.key = &key
|
|
||||||
msg = fmt.Sprintf(":%s MODE %s -k", client, *room.name)
|
|
||||||
room.Unlock()
|
|
||||||
msgLog = "removed channel key"
|
msgLog = "removed channel key"
|
||||||
}
|
}
|
||||||
room.Broadcast(msg)
|
r.Broadcast(msg)
|
||||||
logSink <- LogEvent{room.String(), *client.nickname, msgLog, true}
|
logSink <- LogEvent{r.name, c.nickname, msgLog, true}
|
||||||
room.StateSave()
|
r.StateSave()
|
||||||
case EventMsg:
|
case EventMsg:
|
||||||
sep := strings.Index(event.text, " ")
|
sep := strings.Index(e.text, " ")
|
||||||
room.Broadcast(fmt.Sprintf(
|
r.Broadcast(fmt.Sprintf(
|
||||||
":%s %s %s :%s",
|
":%s %s %s :%s", c, e.text[:sep], r.name, e.text[sep+1:],
|
||||||
client,
|
), c)
|
||||||
event.text[:sep],
|
logSink <- LogEvent{r.name, c.nickname, e.text[sep+1:], false}
|
||||||
room.String(),
|
|
||||||
event.text[sep+1:]),
|
|
||||||
client,
|
|
||||||
)
|
|
||||||
logSink <- LogEvent{
|
|
||||||
room.String(),
|
|
||||||
*client.nickname,
|
|
||||||
event.text[sep+1:],
|
|
||||||
false,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func RoomRegister(name string) *Room {
|
||||||
|
r := &Room{
|
||||||
|
name: name,
|
||||||
|
members: make(map[*Client]struct{}),
|
||||||
|
events: make(chan ClientEvent),
|
||||||
|
}
|
||||||
|
roomsLock.Lock()
|
||||||
|
roomsWG.Add(1)
|
||||||
|
rooms[name] = r
|
||||||
|
roomsLock.Unlock()
|
||||||
|
go r.Processor(r.events)
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
40
room_test.go
40
room_test.go
@ -46,10 +46,9 @@ func TestTwoUsers(t *testing.T) {
|
|||||||
host := "foohost"
|
host := "foohost"
|
||||||
hostname = &host
|
hostname = &host
|
||||||
events := make(chan ClientEvent)
|
events := make(chan ClientEvent)
|
||||||
roomsM.Lock()
|
roomsLock.Lock()
|
||||||
rooms = make(map[string]*Room)
|
rooms = make(map[string]*Room)
|
||||||
roomSinks = make(map[*Room]chan ClientEvent)
|
roomsLock.Unlock()
|
||||||
roomsM.Unlock()
|
|
||||||
clients = make(map[*Client]struct{})
|
clients = make(map[*Client]struct{})
|
||||||
finished := make(chan struct{})
|
finished := make(chan struct{})
|
||||||
go Processor(events, finished)
|
go Processor(events, finished)
|
||||||
@ -60,10 +59,8 @@ func TestTwoUsers(t *testing.T) {
|
|||||||
|
|
||||||
conn1 := NewTestingConn()
|
conn1 := NewTestingConn()
|
||||||
conn2 := NewTestingConn()
|
conn2 := NewTestingConn()
|
||||||
client1 := NewClient(conn1)
|
client1 := NewClient(conn1, events)
|
||||||
client2 := NewClient(conn2)
|
NewClient(conn2, events)
|
||||||
go client1.Processor(events)
|
|
||||||
go client2.Processor(events)
|
|
||||||
|
|
||||||
conn1.inbound <- "NICK nick1\r\nUSER foo1 bar1 baz1 :Long name1"
|
conn1.inbound <- "NICK nick1\r\nUSER foo1 bar1 baz1 :Long name1"
|
||||||
conn2.inbound <- "NICK nick2\r\nUSER foo2 bar2 baz2 :Long name2"
|
conn2.inbound <- "NICK nick2\r\nUSER foo2 bar2 baz2 :Long name2"
|
||||||
@ -72,7 +69,7 @@ func TestTwoUsers(t *testing.T) {
|
|||||||
<-conn2.outbound
|
<-conn2.outbound
|
||||||
}
|
}
|
||||||
|
|
||||||
SendLusers(client1)
|
client1.SendLusers()
|
||||||
if r := <-conn1.outbound; !strings.Contains(r, "There are 2 users") {
|
if r := <-conn1.outbound; !strings.Contains(r, "There are 2 users") {
|
||||||
t.Fatal("LUSERS", r)
|
t.Fatal("LUSERS", r)
|
||||||
}
|
}
|
||||||
@ -139,7 +136,6 @@ func TestJoin(t *testing.T) {
|
|||||||
events := make(chan ClientEvent)
|
events := make(chan ClientEvent)
|
||||||
rooms = make(map[string]*Room)
|
rooms = make(map[string]*Room)
|
||||||
clients = make(map[*Client]struct{})
|
clients = make(map[*Client]struct{})
|
||||||
roomSinks = make(map[*Room]chan ClientEvent)
|
|
||||||
finished := make(chan struct{})
|
finished := make(chan struct{})
|
||||||
go Processor(events, finished)
|
go Processor(events, finished)
|
||||||
defer func() {
|
defer func() {
|
||||||
@ -147,8 +143,7 @@ func TestJoin(t *testing.T) {
|
|||||||
<-finished
|
<-finished
|
||||||
}()
|
}()
|
||||||
conn := NewTestingConn()
|
conn := NewTestingConn()
|
||||||
client := NewClient(conn)
|
NewClient(conn, events)
|
||||||
go client.Processor(events)
|
|
||||||
|
|
||||||
conn.inbound <- "NICK nick2\r\nUSER foo2 bar2 baz2 :Long name2"
|
conn.inbound <- "NICK nick2\r\nUSER foo2 bar2 baz2 :Long name2"
|
||||||
for i := 0; i < 6; i++ {
|
for i := 0; i < 6; i++ {
|
||||||
@ -183,14 +178,14 @@ func TestJoin(t *testing.T) {
|
|||||||
for i := 0; i < 4*2; i++ {
|
for i := 0; i < 4*2; i++ {
|
||||||
<-conn.outbound
|
<-conn.outbound
|
||||||
}
|
}
|
||||||
roomsM.RLock()
|
roomsLock.RLock()
|
||||||
if _, ok := rooms["#bar"]; !ok {
|
if _, ok := rooms["#bar"]; !ok {
|
||||||
t.Fatal("#bar does not exist")
|
t.Fatal("#bar does not exist")
|
||||||
}
|
}
|
||||||
if _, ok := rooms["#baz"]; !ok {
|
if _, ok := rooms["#baz"]; !ok {
|
||||||
t.Fatal("#baz does not exist")
|
t.Fatal("#baz does not exist")
|
||||||
}
|
}
|
||||||
roomsM.RUnlock()
|
roomsLock.RUnlock()
|
||||||
if r := <-logSink; (r.what != "joined") || (r.where != "#bar") || (r.who != "nick2") || (r.meta != true) {
|
if r := <-logSink; (r.what != "joined") || (r.where != "#bar") || (r.who != "nick2") || (r.meta != true) {
|
||||||
t.Fatal("invalid join log event #bar", r)
|
t.Fatal("invalid join log event #bar", r)
|
||||||
}
|
}
|
||||||
@ -202,14 +197,14 @@ func TestJoin(t *testing.T) {
|
|||||||
for i := 0; i < 4*2; i++ {
|
for i := 0; i < 4*2; i++ {
|
||||||
<-conn.outbound
|
<-conn.outbound
|
||||||
}
|
}
|
||||||
roomsM.RLock()
|
roomsLock.RLock()
|
||||||
if *rooms["#barenc"].key != "key1" {
|
if rooms["#barenc"].key != "key1" {
|
||||||
t.Fatal("no room with key1")
|
t.Fatal("no room with key1")
|
||||||
}
|
}
|
||||||
if *rooms["#bazenc"].key != "key2" {
|
if rooms["#bazenc"].key != "key2" {
|
||||||
t.Fatal("no room with key2")
|
t.Fatal("no room with key2")
|
||||||
}
|
}
|
||||||
roomsM.RUnlock()
|
roomsLock.RUnlock()
|
||||||
if r := <-logSink; (r.what != "joined") || (r.where != "#barenc") || (r.who != "nick2") || (r.meta != true) {
|
if r := <-logSink; (r.what != "joined") || (r.where != "#barenc") || (r.who != "nick2") || (r.meta != true) {
|
||||||
t.Fatal("invalid join log event #barenc", r)
|
t.Fatal("invalid join log event #barenc", r)
|
||||||
}
|
}
|
||||||
@ -227,11 +222,11 @@ func TestJoin(t *testing.T) {
|
|||||||
if r := <-conn.outbound; r != ":nick2!foo2@someclient MODE #barenc -k\r\n" {
|
if r := <-conn.outbound; r != ":nick2!foo2@someclient MODE #barenc -k\r\n" {
|
||||||
t.Fatal("remove #barenc key", r)
|
t.Fatal("remove #barenc key", r)
|
||||||
}
|
}
|
||||||
roomsM.RLock()
|
roomsLock.RLock()
|
||||||
if *rooms["#barenc"].key != "" {
|
if rooms["#barenc"].key != "" {
|
||||||
t.Fatal("removing key from #barenc")
|
t.Fatal("removing key from #barenc")
|
||||||
}
|
}
|
||||||
roomsM.RUnlock()
|
roomsLock.RUnlock()
|
||||||
if r := <-logSink; (r.what != "removed channel key") || (r.where != "#barenc") || (r.who != "nick2") || (r.meta != true) {
|
if r := <-logSink; (r.what != "removed channel key") || (r.where != "#barenc") || (r.who != "nick2") || (r.meta != true) {
|
||||||
t.Fatal("removed channel key log", r)
|
t.Fatal("removed channel key log", r)
|
||||||
}
|
}
|
||||||
@ -240,6 +235,9 @@ func TestJoin(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
conn.inbound <- "PART #bazenc\r\nMODE #bazenc -k"
|
conn.inbound <- "PART #bazenc\r\nMODE #bazenc -k"
|
||||||
|
if r := <-conn.outbound; r != ":nick2!foo2@someclient PART #bazenc :nick2\r\n" {
|
||||||
|
t.Fatal("part", r)
|
||||||
|
}
|
||||||
if r := <-conn.outbound; r != ":foohost 442 #bazenc :You are not on that channel\r\n" {
|
if r := <-conn.outbound; r != ":foohost 442 #bazenc :You are not on that channel\r\n" {
|
||||||
t.Fatal("not on that channel", r)
|
t.Fatal("not on that channel", r)
|
||||||
}
|
}
|
||||||
@ -256,7 +254,7 @@ func TestJoin(t *testing.T) {
|
|||||||
if r := <-conn.outbound; r != ":nick2!foo2@someclient MODE #barenc +k newkey\r\n" {
|
if r := <-conn.outbound; r != ":nick2!foo2@someclient MODE #barenc +k newkey\r\n" {
|
||||||
t.Fatal("+k MODE setting", r)
|
t.Fatal("+k MODE setting", r)
|
||||||
}
|
}
|
||||||
if r := <-logSink; (r.what != "set channel key to newkey") || (r.where != "#barenc") || (r.who != "nick2") || (r.meta != true) {
|
if r := <-logSink; (r.what != "set channel key") || (r.where != "#barenc") || (r.who != "nick2") || (r.meta != true) {
|
||||||
t.Fatal("set channel key", r)
|
t.Fatal("set channel key", r)
|
||||||
}
|
}
|
||||||
if r := <-stateSink; (r.topic != "") || (r.where != "#barenc") || (r.key != "newkey") {
|
if r := <-stateSink; (r.topic != "") || (r.where != "#barenc") || (r.key != "newkey") {
|
||||||
|
Loading…
Reference in New Issue
Block a user