3400 lines
83 KiB
Go
3400 lines
83 KiB
Go
//
|
|
// Copyright (c) 2014 - 2022, Přemysl Eric Janouch <p@janouch.name>
|
|
//
|
|
// Permission to use, copy, modify, and/or distribute this software for any
|
|
// purpose with or without fee is hereby granted.
|
|
//
|
|
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
|
|
// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
|
|
// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
|
|
// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
//
|
|
|
|
// xS is a straight-forward port of xD IRCd from C.
|
|
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"crypto/sha256"
|
|
"crypto/tls"
|
|
"encoding/hex"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"log/syslog"
|
|
"net"
|
|
"os"
|
|
"os/signal"
|
|
"os/user"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
)
|
|
|
|
const projectName = "xS"
|
|
|
|
var projectVersion = "?"
|
|
|
|
var debugMode = false
|
|
|
|
// --- Logging -----------------------------------------------------------------
|
|
|
|
type logPrio int
|
|
|
|
const (
|
|
prioFatal logPrio = iota
|
|
prioError
|
|
prioWarning
|
|
prioStatus
|
|
prioDebug
|
|
)
|
|
|
|
func (lp logPrio) prefix() string {
|
|
switch lp {
|
|
case prioFatal:
|
|
return "fatal: "
|
|
case prioError:
|
|
return "error: "
|
|
case prioWarning:
|
|
return "warning: "
|
|
case prioStatus:
|
|
return ""
|
|
case prioDebug:
|
|
return "debug: "
|
|
default:
|
|
panic("unhandled log priority")
|
|
}
|
|
}
|
|
|
|
func (lp logPrio) syslogPrio() syslog.Priority {
|
|
switch lp {
|
|
case prioFatal:
|
|
return syslog.LOG_ERR
|
|
case prioError:
|
|
return syslog.LOG_ERR
|
|
case prioWarning:
|
|
return syslog.LOG_WARNING
|
|
case prioStatus:
|
|
return syslog.LOG_INFO
|
|
case prioDebug:
|
|
return syslog.LOG_DEBUG
|
|
default:
|
|
panic("unhandled log priority")
|
|
}
|
|
}
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
func logMessageStdio(prio logPrio, format string, args ...interface{}) {
|
|
// TODO: isatty-enabled colors based on prio.
|
|
os.Stderr.WriteString(time.Now().Format("2006-01-02 15:04:05 ") +
|
|
prio.prefix() + fmt.Sprintf(format, args...) + "\n")
|
|
}
|
|
|
|
func logMessageSystemd(prio logPrio, format string, args ...interface{}) {
|
|
if prio == prioFatal {
|
|
// There is no corresponding syslog severity.
|
|
format = "fatal: " + format
|
|
}
|
|
fmt.Fprintf(os.Stderr, "<%d>%s\n",
|
|
prio.syslogPrio(), fmt.Sprintf(format, args...))
|
|
}
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
var logMessage = logMessageStdio
|
|
|
|
func printDebug(format string, args ...interface{}) {
|
|
if debugMode {
|
|
logMessage(prioDebug, format, args...)
|
|
}
|
|
}
|
|
|
|
func printStatus(format string, args ...interface{}) {
|
|
logMessage(prioStatus, format, args...)
|
|
}
|
|
func printWarning(format string, args ...interface{}) {
|
|
logMessage(prioWarning, format, args...)
|
|
}
|
|
func printError(format string, args ...interface{}) {
|
|
logMessage(prioError, format, args...)
|
|
}
|
|
|
|
// "fatal" is reserved for failures that would harm further operation.
|
|
|
|
func printFatal(format string, args ...interface{}) {
|
|
logMessage(prioFatal, format, args...)
|
|
}
|
|
|
|
func exitFatal(format string, args ...interface{}) {
|
|
printFatal(format, args...)
|
|
os.Exit(1)
|
|
}
|
|
|
|
// --- Utilities ---------------------------------------------------------------
|
|
|
|
// Split a string by a set of UTF-8 delimiters, optionally ignoring empty items.
|
|
func splitString(s, delims string, ignoreEmpty bool) (result []string) {
|
|
for {
|
|
end := strings.IndexAny(s, delims)
|
|
if end < 0 {
|
|
break
|
|
}
|
|
if !ignoreEmpty || end != 0 {
|
|
result = append(result, s[:end])
|
|
}
|
|
s = s[end+1:]
|
|
}
|
|
if !ignoreEmpty || s != "" {
|
|
result = append(result, s)
|
|
}
|
|
return
|
|
}
|
|
|
|
// Trivial SSL/TLS autodetection. The first block of data returned by Recvfrom
|
|
// must be at least three octets long for this to work reliably, but that should
|
|
// not pose a problem in practice. We might try waiting for them.
|
|
//
|
|
// SSL2: 1xxx xxxx | xxxx xxxx | <1>
|
|
// (message length) (client hello)
|
|
// SSL3/TLS: <22> | <3> | xxxx xxxx
|
|
// (handshake)| (protocol version)
|
|
//
|
|
// Note that Go 1.12's crypto/tls offers a slightly more straight-forward
|
|
// solution: "If a client sends an initial message that does not look like TLS,
|
|
// the server will no longer reply with an alert, and it will expose the
|
|
// underlying net.Conn in the new field Conn of RecordHeaderError."
|
|
func detectTLS(sysconn syscall.RawConn) (isTLS bool) {
|
|
sysconn.Read(func(fd uintptr) (done bool) {
|
|
var buf [3]byte
|
|
n, _, err := syscall.Recvfrom(int(fd), buf[:], syscall.MSG_PEEK)
|
|
switch {
|
|
case n == 3:
|
|
isTLS = buf[0]&0x80 != 0 && buf[2] == 1
|
|
fallthrough
|
|
case n == 2:
|
|
isTLS = isTLS || buf[0] == 22 && buf[1] == 3
|
|
case n == 1:
|
|
isTLS = buf[0] == 22
|
|
case err == syscall.EAGAIN:
|
|
return false
|
|
}
|
|
return true
|
|
})
|
|
return isTLS
|
|
}
|
|
|
|
// --- File system -------------------------------------------------------------
|
|
|
|
// Look up the value of an XDG path from environment, or fall back to a default.
|
|
func getXDGHomeDir(name, def string) string {
|
|
env := os.Getenv(name)
|
|
if env != "" && env[0] == filepath.Separator {
|
|
return env
|
|
}
|
|
|
|
home := ""
|
|
if v, ok := os.LookupEnv("HOME"); ok {
|
|
home = v
|
|
} else if u, _ := user.Current(); u != nil {
|
|
home = u.HomeDir
|
|
}
|
|
return filepath.Join(home, def)
|
|
}
|
|
|
|
func resolveRelativeFilenameGeneric(paths []string, filename string) string {
|
|
for _, path := range paths {
|
|
// As per XDG spec, relative paths are ignored.
|
|
if path == "" || path[0] != filepath.Separator {
|
|
continue
|
|
}
|
|
|
|
file := filepath.Join(path, filename)
|
|
if _, err := os.Stat(file); err == nil {
|
|
return file
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// Retrieve all XDG base directories for configuration files.
|
|
func getXDGConfigDirs() (result []string) {
|
|
home := getXDGHomeDir("XDG_CONFIG_HOME", ".config")
|
|
if home != "" {
|
|
result = append(result, home)
|
|
}
|
|
dirs := os.Getenv("XDG_CONFIG_DIRS")
|
|
if dirs == "" {
|
|
dirs = "/etc/xdg"
|
|
}
|
|
for _, path := range strings.Split(dirs, ":") {
|
|
if path != "" {
|
|
result = append(result, path)
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func resolveRelativeConfigFilename(filename string) string {
|
|
return resolveRelativeFilenameGeneric(getXDGConfigDirs(),
|
|
filepath.Join(projectName, filename))
|
|
}
|
|
|
|
func findTildeHome(username string) string {
|
|
if username != "" {
|
|
if u, _ := user.Lookup(username); u != nil {
|
|
return u.HomeDir
|
|
}
|
|
} else if u, _ := user.Current(); u != nil {
|
|
return u.HomeDir
|
|
} else if v, ok := os.LookupEnv("HOME"); ok {
|
|
return v
|
|
}
|
|
printDebug("failed to expand the home directory for %s", username)
|
|
return "~" + username
|
|
}
|
|
|
|
func resolveFilename(filename string, relativeCB func(string) string) string {
|
|
// Absolute path is absolute.
|
|
if filename == "" || filename[0] == filepath.Separator {
|
|
return filename
|
|
}
|
|
if filename[0] != '~' {
|
|
return relativeCB(filename)
|
|
}
|
|
|
|
// Paths to home directories ought to be absolute.
|
|
var n int
|
|
for n = 0; n < len(filename); n++ {
|
|
if filename[n] == filepath.Separator {
|
|
break
|
|
}
|
|
}
|
|
return findTildeHome(filename[1:n]) + filename[n:]
|
|
}
|
|
|
|
// --- Simple file I/O ---------------------------------------------------------
|
|
|
|
// Overwrites filename contents with data; creates directories as needed.
|
|
func writeFile(path string, data []byte) error {
|
|
if dir := filepath.Dir(path); dir != "." {
|
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return ioutil.WriteFile(path, data, 0644)
|
|
}
|
|
|
|
// Wrapper for writeFile that makes sure that the new data has been written
|
|
// to disk in its entirety before overriding the old file.
|
|
func writeFileSafe(path string, data []byte) error {
|
|
temp := path + ".new"
|
|
if err := writeFile(temp, data); err != nil {
|
|
return err
|
|
}
|
|
return os.Rename(temp, path)
|
|
}
|
|
|
|
// --- Simple configuration ----------------------------------------------------
|
|
|
|
// This is the bare minimum to make an application configurable.
|
|
// Keys are stripped of surrounding whitespace, values are not.
|
|
|
|
type simpleConfigItem struct {
|
|
key string // INI key
|
|
def string // default value
|
|
description string // documentation
|
|
}
|
|
|
|
type simpleConfig map[string]string
|
|
|
|
func (sc simpleConfig) loadDefaults(table []simpleConfigItem) {
|
|
for _, item := range table {
|
|
sc[item.key] = item.def
|
|
}
|
|
}
|
|
|
|
func (sc simpleConfig) updateFromFile() error {
|
|
basename := projectName + ".conf"
|
|
path := resolveFilename(basename, resolveRelativeConfigFilename)
|
|
if path == "" {
|
|
return &os.PathError{
|
|
Op: "cannot find",
|
|
Path: basename,
|
|
Err: os.ErrNotExist,
|
|
}
|
|
}
|
|
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer f.Close()
|
|
|
|
scanner := bufio.NewScanner(f)
|
|
for lineNo := 1; scanner.Scan(); lineNo++ {
|
|
line := strings.TrimLeft(scanner.Text(), " \t")
|
|
if line == "" || strings.HasPrefix(line, "#") {
|
|
continue
|
|
}
|
|
|
|
equals := strings.IndexByte(line, '=')
|
|
if equals <= 0 {
|
|
return fmt.Errorf("%s:%d: malformed line", path, lineNo)
|
|
}
|
|
|
|
sc[strings.TrimRight(line[:equals], " \t")] = line[equals+1:]
|
|
}
|
|
return scanner.Err()
|
|
}
|
|
|
|
func writeConfigurationFile(pathHint string, data []byte) (string, error) {
|
|
path := pathHint
|
|
if path == "" {
|
|
path = filepath.Join(getXDGHomeDir("XDG_CONFIG_HOME", ".config"),
|
|
projectName, projectName+".conf")
|
|
}
|
|
|
|
if err := writeFileSafe(path, data); err != nil {
|
|
return "", err
|
|
}
|
|
return path, nil
|
|
}
|
|
|
|
func simpleConfigWriteDefault(pathHint string, prolog string,
|
|
table []simpleConfigItem) (string, error) {
|
|
data := []byte(prolog)
|
|
for _, item := range table {
|
|
data = append(data, fmt.Sprintf("# %s\n%s=%s\n",
|
|
item.description, item.key, item.def)...)
|
|
}
|
|
return writeConfigurationFile(pathHint, data)
|
|
}
|
|
|
|
// Convenience wrapper suitable for most simple applications.
|
|
func callSimpleConfigWriteDefault(pathHint string, table []simpleConfigItem) {
|
|
prologLines := []string{
|
|
`# ` + projectName + ` ` + projectVersion + ` configuration file`,
|
|
"#",
|
|
`# Relative paths are searched for in ${XDG_CONFIG_HOME:-~/.config}`,
|
|
`# /` + projectName + ` as well as in $XDG_CONFIG_DIRS/` + projectName,
|
|
``,
|
|
``,
|
|
}
|
|
|
|
path, err := simpleConfigWriteDefault(
|
|
pathHint, strings.Join(prologLines, "\n"), table)
|
|
if err != nil {
|
|
exitFatal("%s", err)
|
|
}
|
|
|
|
printStatus("configuration written to `%s'", path)
|
|
}
|
|
|
|
// --- Configuration -----------------------------------------------------------
|
|
|
|
var configTable = []simpleConfigItem{
|
|
{"server_name", "", "Server name"},
|
|
{"server_info", "My server", "Brief server description"},
|
|
{"motd", "", "MOTD filename"},
|
|
{"catalog", "", "Localisation catalog"},
|
|
|
|
{"bind", ":6667", "Bind addresses of the IRC server"},
|
|
{"tls_cert", "", "Server TLS certificate (PEM)"},
|
|
{"tls_key", "", "Server TLS private key (PEM)"},
|
|
{"webirc_password", "", "Password for WebIRC"},
|
|
|
|
{"operators", "", "IRCop TLS certificate SHA-256 fingerprints"},
|
|
|
|
{"max_connections", "0", "Global connection limit"},
|
|
{"ping_interval", "180", "Interval between PINGs (sec)"},
|
|
}
|
|
|
|
// --- Rate limiter ------------------------------------------------------------
|
|
|
|
type floodDetector struct {
|
|
interval time.Duration // interval for the limit in seconds
|
|
limit uint // maximum number of events allowed
|
|
timestamps []time.Time // timestamps of last events
|
|
pos uint // index of the oldest event
|
|
}
|
|
|
|
func newFloodDetector(interval time.Duration, limit uint) *floodDetector {
|
|
return &floodDetector{
|
|
interval: interval,
|
|
limit: limit,
|
|
timestamps: make([]time.Time, limit+1),
|
|
pos: 0,
|
|
}
|
|
}
|
|
|
|
func (fd *floodDetector) check() bool {
|
|
now := time.Now()
|
|
fd.timestamps[fd.pos] = now
|
|
|
|
fd.pos++
|
|
if fd.pos > fd.limit {
|
|
fd.pos = 0
|
|
}
|
|
|
|
var count uint
|
|
begin := now.Add(-fd.interval)
|
|
for _, ts := range fd.timestamps {
|
|
if ts.After(begin) {
|
|
count++
|
|
}
|
|
}
|
|
return count <= fd.limit
|
|
}
|
|
|
|
// --- IRC token validation ----------------------------------------------------
|
|
|
|
// Everything as per RFC 2812
|
|
const (
|
|
ircMaxNickname = 9
|
|
ircMaxHostname = 63
|
|
ircMaxChannelName = 50
|
|
ircMaxMessageLength = 510
|
|
)
|
|
|
|
const (
|
|
reClassSpecial = "\\[\\]\\\\`_^{|}"
|
|
// "shortname" from RFC 2812 doesn't work how its author thought it would.
|
|
reShortname = "[0-9A-Za-z](-*[0-9A-Za-z])*"
|
|
)
|
|
|
|
var (
|
|
reHostname = regexp.MustCompile(
|
|
`^` + reShortname + `(\.` + reShortname + `)*$`)
|
|
|
|
// Extending ASCII to the whole range of Unicode letters.
|
|
reNickname = regexp.MustCompile(
|
|
`^[\pL` + reClassSpecial + `][\pL` + reClassSpecial + `0-9-]*$`)
|
|
|
|
// Notably, this won't match invalid UTF-8 characters, although the
|
|
// behaviour seems to be unstated in the documentation.
|
|
reUsername = regexp.MustCompile(`^[^\0\r\n @]+$`)
|
|
|
|
reChannelName = regexp.MustCompile(`^[^\0\007\r\n ,:]+$`)
|
|
reKey = regexp.MustCompile(`^[^\r\n\f\t\v ]{1,23}$`)
|
|
reUserMask = regexp.MustCompile(`^[^!@]+![^!@]+@[^@!]+$`)
|
|
reFingerprint = regexp.MustCompile(`^[a-fA-F0-9]{64}$`)
|
|
)
|
|
|
|
func ircValidateHostname(hostname string) error {
|
|
if hostname == "" {
|
|
return errors.New("the value is empty")
|
|
}
|
|
if !reHostname.MatchString(hostname) {
|
|
return errors.New("invalid format")
|
|
}
|
|
if len(hostname) > ircMaxHostname {
|
|
return errors.New("the value is too long")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func ircIsValidNickname(nickname string) bool {
|
|
return len(nickname) <= ircMaxNickname && reNickname.MatchString(nickname)
|
|
}
|
|
|
|
func ircIsValidUsername(username string) bool {
|
|
// XXX: We really should put a limit on this
|
|
// despite the RFC not mentioning one.
|
|
return reUsername.MatchString(username)
|
|
}
|
|
|
|
func ircIsValidChannelName(name string) bool {
|
|
return len(name) <= ircMaxChannelName && reChannelName.MatchString(name)
|
|
}
|
|
|
|
func ircIsValidKey(key string) bool {
|
|
// XXX: Should be 7-bit as well but whatever.
|
|
return reKey.MatchString(key)
|
|
}
|
|
|
|
func ircIsValidUserMask(mask string) bool {
|
|
return reUserMask.MatchString(mask)
|
|
}
|
|
|
|
func ircIsValidFingerprint(fp string) bool {
|
|
return reFingerprint.MatchString(fp)
|
|
}
|
|
|
|
// --- Clients (equals users) --------------------------------------------------
|
|
|
|
type connCloseWriter interface {
|
|
net.Conn
|
|
CloseWrite() error
|
|
}
|
|
|
|
const ircSupportedUserModes = "aiwros"
|
|
|
|
const (
|
|
ircUserModeInvisible uint = 1 << iota
|
|
ircUserModeRxWallops
|
|
ircUserModeRestricted
|
|
ircUserModeOperator
|
|
ircUserModeRxServerNotices
|
|
)
|
|
|
|
const (
|
|
ircCapMultiPrefix uint = 1 << iota
|
|
ircCapInviteNotify
|
|
ircCapEchoMessage
|
|
ircCapUserhostInNames
|
|
ircCapServerTime
|
|
)
|
|
|
|
type client struct {
|
|
transport net.Conn // underlying connection
|
|
tls *tls.Conn // TLS, if detected
|
|
conn connCloseWriter // high-level connection
|
|
recvQ []byte // unprocessed input
|
|
sendQ []byte // unprocessed output
|
|
reading bool // whether a reading goroutine is running
|
|
writing bool // whether a writing goroutine is running
|
|
closing bool // whether we're closing the connection
|
|
killTimer *time.Timer // hard kill timeout
|
|
|
|
opened time.Time // when the connection was opened
|
|
nSentMessages uint // number of sent messages total
|
|
sentBytes int // number of sent bytes total
|
|
nReceivedMessages uint // number of received messages total
|
|
receivedBytes int // number of received bytes total
|
|
|
|
hostname string // hostname or IP shown to the network
|
|
port string // port of the peer as a string
|
|
address string // full network address
|
|
|
|
pingTimer *time.Timer // we should send a PING
|
|
timeoutTimer *time.Timer // connection seems to be dead
|
|
registered bool // the user has registered
|
|
capNegotiating bool // negotiating capabilities
|
|
capsEnabled uint // enabled capabilities
|
|
capVersion uint // CAP protocol version
|
|
|
|
tlsCertFingerprint string // client certificate fingerprint
|
|
|
|
nickname string // IRC nickname (main identifier)
|
|
username string // IRC username
|
|
realname string // IRC realname (or e-mail)
|
|
|
|
mode uint // user's mode
|
|
awayMessage string // away message
|
|
lastActive time.Time // last PRIVMSG, to get idle time
|
|
invites map[string]bool // channel invitations by operators
|
|
antiflood *floodDetector // flood detector
|
|
}
|
|
|
|
// --- Channels ----------------------------------------------------------------
|
|
|
|
const ircSupportedChanModes = "ov" + "beI" + "imnqpst" + "kl"
|
|
|
|
const (
|
|
ircChanModeInviteOnly uint = 1 << iota
|
|
ircChanModeModerated
|
|
ircChanModeNoOutsideMsgs
|
|
ircChanModeQuiet
|
|
ircChanModePrivate
|
|
ircChanModeSecret
|
|
ircChanModeProtectedTopic
|
|
|
|
ircChanModeOperator
|
|
ircChanModeVoice
|
|
)
|
|
|
|
type channel struct {
|
|
name string // channel name
|
|
modes uint // channel modes
|
|
key string // channel key
|
|
userLimit int // user limit or -1
|
|
created time.Time // creation time
|
|
|
|
topic string // channel topic
|
|
topicWho string // who set the topic
|
|
topicTime time.Time // when the topic was set
|
|
|
|
userModes map[*client]uint // modes for all channel users
|
|
|
|
banList []string // ban list
|
|
exceptionList []string // exceptions from bans
|
|
inviteList []string // exceptions from +I
|
|
}
|
|
|
|
func (ch *channel) getMode(discloseSecrets bool) string {
|
|
var buf []byte
|
|
if 0 != ch.modes&ircChanModeInviteOnly {
|
|
buf = append(buf, 'i')
|
|
}
|
|
if 0 != ch.modes&ircChanModeModerated {
|
|
buf = append(buf, 'm')
|
|
}
|
|
if 0 != ch.modes&ircChanModeNoOutsideMsgs {
|
|
buf = append(buf, 'n')
|
|
}
|
|
if 0 != ch.modes&ircChanModeQuiet {
|
|
buf = append(buf, 'q')
|
|
}
|
|
if 0 != ch.modes&ircChanModePrivate {
|
|
buf = append(buf, 'p')
|
|
}
|
|
if 0 != ch.modes&ircChanModeSecret {
|
|
buf = append(buf, 's')
|
|
}
|
|
if 0 != ch.modes&ircChanModeProtectedTopic {
|
|
buf = append(buf, 'r')
|
|
}
|
|
|
|
if ch.userLimit != -1 {
|
|
buf = append(buf, 'l')
|
|
}
|
|
if ch.key != "" {
|
|
buf = append(buf, 'k')
|
|
}
|
|
|
|
// XXX: Is it correct to split it? Try it on an existing implementation.
|
|
if discloseSecrets {
|
|
if ch.userLimit != -1 {
|
|
buf = append(buf, fmt.Sprintf(" %d", ch.userLimit)...)
|
|
}
|
|
if ch.key != "" {
|
|
buf = append(buf, fmt.Sprintf(" %s", ch.key)...)
|
|
}
|
|
}
|
|
return string(buf)
|
|
}
|
|
|
|
// --- IRC server context ------------------------------------------------------
|
|
|
|
type whowasInfo struct {
|
|
nickname, username, realname, hostname string
|
|
}
|
|
|
|
func newWhowasInfo(c *client) *whowasInfo {
|
|
return &whowasInfo{
|
|
nickname: c.nickname,
|
|
username: c.username,
|
|
realname: c.realname,
|
|
hostname: c.hostname,
|
|
}
|
|
}
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
type ircCommand struct {
|
|
requiresRegistration bool
|
|
handler func(*message, *client)
|
|
|
|
nReceived uint // number of commands received
|
|
bytesReceived int // number of bytes received total
|
|
}
|
|
|
|
type preparedEvent struct {
|
|
client *client
|
|
host string // client's hostname or literal IP address
|
|
isTLS bool // the client seems to use TLS
|
|
}
|
|
|
|
type readEvent struct {
|
|
client *client
|
|
data []byte // new data from the client
|
|
err error // read error
|
|
}
|
|
|
|
type writeEvent struct {
|
|
client *client
|
|
written int // amount of bytes written
|
|
err error // write error
|
|
}
|
|
|
|
// TODO: Maybe we want to keep it in a struct?
|
|
// A better question might be: can we run multiple instances of it?
|
|
var (
|
|
// network
|
|
|
|
listeners []net.Listener
|
|
clients = make(map[*client]bool)
|
|
|
|
// IRC state
|
|
|
|
// XXX: Beware that maps with identifier keys need to be indexed correctly.
|
|
// We might want to enforce accessor functions for users and channels.
|
|
|
|
started time.Time // when the server has been started
|
|
users = make(map[string]*client) // maps nicknames to clients
|
|
channels = make(map[string]*channel) // maps channel names to data
|
|
whowas = make(map[string]*whowasInfo) // WHOWAS registry
|
|
|
|
// event loop
|
|
|
|
quitting bool
|
|
quitTimer <-chan time.Time
|
|
|
|
sigs = make(chan os.Signal, 1)
|
|
conns = make(chan net.Conn)
|
|
prepared = make(chan preparedEvent)
|
|
reads = make(chan readEvent)
|
|
writes = make(chan writeEvent)
|
|
timers = make(chan func())
|
|
|
|
// configuration
|
|
|
|
config simpleConfig // server configuration
|
|
tlsConf *tls.Config // TLS connection configuration
|
|
serverName string // our server name
|
|
pingInterval time.Duration // ping interval
|
|
maxConnections int // max connections allowed or 0
|
|
motd []string // MOTD (none if empty)
|
|
catalog map[int]string // message catalog for server msgs
|
|
operators map[string]bool // TLS certificate fingerprints for IRCops
|
|
)
|
|
|
|
// Forcefully tear down all connections.
|
|
func forceQuit(reason string) {
|
|
if !quitting {
|
|
exitFatal("forceQuit called without initiateQuit")
|
|
}
|
|
|
|
printStatus("forced shutdown (%s)", reason)
|
|
for c := range clients {
|
|
// initiateQuit has already unregistered the client.
|
|
c.kill("Shutting down")
|
|
}
|
|
}
|
|
|
|
// Initiate a clean shutdown of the whole daemon.
|
|
func initiateQuit() {
|
|
printStatus("shutting down")
|
|
for _, ln := range listeners {
|
|
if err := ln.Close(); err != nil {
|
|
printError("%s", err)
|
|
}
|
|
}
|
|
for c := range clients {
|
|
c.closeLink("Shutting down")
|
|
}
|
|
|
|
quitTimer = time.After(5 * time.Second)
|
|
quitting = true
|
|
}
|
|
|
|
func ircChannelCreate(name string) *channel {
|
|
ch := &channel{
|
|
name: name,
|
|
userLimit: -1,
|
|
created: time.Now(),
|
|
userModes: make(map[*client]uint),
|
|
}
|
|
channels[ircToCanon(name)] = ch
|
|
return ch
|
|
}
|
|
|
|
func ircChannelDestroyIfEmpty(ch *channel) {
|
|
if len(ch.userModes) == 0 {
|
|
delete(channels, ircToCanon(ch.name))
|
|
}
|
|
}
|
|
|
|
func ircNotifyRoommates(c *client, message string) {
|
|
targets := make(map[*client]bool)
|
|
for _, ch := range channels {
|
|
_, present := ch.userModes[c]
|
|
if !present || 0 != ch.modes&ircChanModeQuiet {
|
|
continue
|
|
}
|
|
for client := range ch.userModes {
|
|
targets[client] = true
|
|
}
|
|
}
|
|
|
|
for roommate := range targets {
|
|
if roommate != c {
|
|
roommate.send(message)
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- Clients (continued) -----------------------------------------------------
|
|
|
|
func (c *client) printDebug(format string, args ...interface{}) {
|
|
if debugMode {
|
|
printDebug("(%s) %s", c.address, fmt.Sprintf(format, args...))
|
|
}
|
|
}
|
|
|
|
func ircAppendClientModes(m uint, mode []byte) []byte {
|
|
if 0 != m&ircUserModeInvisible {
|
|
mode = append(mode, 'i')
|
|
}
|
|
if 0 != m&ircUserModeRxWallops {
|
|
mode = append(mode, 'w')
|
|
}
|
|
if 0 != m&ircUserModeRestricted {
|
|
mode = append(mode, 'r')
|
|
}
|
|
if 0 != m&ircUserModeOperator {
|
|
mode = append(mode, 'o')
|
|
}
|
|
if 0 != m&ircUserModeRxServerNotices {
|
|
mode = append(mode, 's')
|
|
}
|
|
return mode
|
|
}
|
|
|
|
func (c *client) getMode() string {
|
|
var mode []byte
|
|
if c.awayMessage != "" {
|
|
mode = append(mode, 'a')
|
|
}
|
|
return string(ircAppendClientModes(c.mode, mode))
|
|
}
|
|
|
|
func (c *client) send(line string) {
|
|
if c.conn == nil || c.closing {
|
|
return
|
|
}
|
|
|
|
oldSendQLen := len(c.sendQ)
|
|
|
|
// So far there's only one message tag we use, so we can do it simple;
|
|
// note that a 1024-character limit applies to messages with tags on.
|
|
if 0 != c.capsEnabled&ircCapServerTime {
|
|
c.sendQ = time.Now().UTC().
|
|
AppendFormat(c.sendQ, "@time=2006-01-02T15:04:05.000Z ")
|
|
}
|
|
|
|
bytes := []byte(line)
|
|
if len(bytes) > ircMaxMessageLength {
|
|
bytes = bytes[:ircMaxMessageLength]
|
|
}
|
|
|
|
c.printDebug("<- %s", bytes)
|
|
|
|
// TODO: Kill the connection above some "SendQ" threshold (careful!)
|
|
c.sendQ = append(c.sendQ, bytes...)
|
|
c.sendQ = append(c.sendQ, "\r\n"...)
|
|
c.flushSendQ()
|
|
|
|
// Technically we haven't sent it yet but that's a minor detail
|
|
c.nSentMessages++
|
|
c.sentBytes += len(c.sendQ) - oldSendQLen
|
|
}
|
|
|
|
func (c *client) sendf(format string, a ...interface{}) {
|
|
c.send(fmt.Sprintf(format, a...))
|
|
}
|
|
|
|
func (c *client) addToWhowas() {
|
|
// Only keeping one entry for each nickname.
|
|
// TODO: Make sure this list doesn't get too long, for example by
|
|
// putting them in a linked list ordered by time.
|
|
whowas[ircToCanon(c.nickname)] = newWhowasInfo(c)
|
|
}
|
|
|
|
func (c *client) nicknameOrStar() string {
|
|
if c.nickname == "" {
|
|
return "*"
|
|
}
|
|
return c.nickname
|
|
}
|
|
|
|
func (c *client) unregister(reason string) {
|
|
if !c.registered {
|
|
return
|
|
}
|
|
|
|
ircNotifyRoommates(c, fmt.Sprintf(":%s!%s@%s QUIT :%s",
|
|
c.nickname, c.username, c.hostname, reason))
|
|
|
|
// The QUIT message will take care of state on clients.
|
|
for _, ch := range channels {
|
|
delete(ch.userModes, c)
|
|
ircChannelDestroyIfEmpty(ch)
|
|
}
|
|
|
|
c.addToWhowas()
|
|
delete(users, ircToCanon(c.nickname))
|
|
c.nickname = ""
|
|
c.registered = false
|
|
}
|
|
|
|
// Close the connection and forget about the client.
|
|
func (c *client) kill(reason string) {
|
|
if reason == "" {
|
|
c.unregister("Client exited")
|
|
} else {
|
|
c.unregister(reason)
|
|
}
|
|
|
|
c.printDebug("client destroyed (%s)", reason)
|
|
|
|
// Try to send a "close notify" alert if the TLS object is ready,
|
|
// otherwise just tear down the transport.
|
|
if c.conn != nil {
|
|
_ = c.conn.Close()
|
|
} else {
|
|
_ = c.transport.Close()
|
|
}
|
|
|
|
c.cancelTimers()
|
|
delete(clients, c)
|
|
}
|
|
|
|
// Tear down the client connection, trying to do so in a graceful manner.
|
|
func (c *client) closeLink(reason string) {
|
|
// Let's just cut the connection, the client can try again later.
|
|
// We also want to avoid accidentally writing to the socket before
|
|
// address resolution has finished.
|
|
if c.conn == nil {
|
|
c.kill(reason)
|
|
return
|
|
}
|
|
if c.closing {
|
|
return
|
|
}
|
|
|
|
// We push an "ERROR" message to the write buffer and let the writer send
|
|
// it, with some arbitrary timeout. The "closing" state makes sure
|
|
// that a/ we ignore any successive messages, and b/ that the connection
|
|
// is killed after the write buffer is transferred and emptied.
|
|
// (Since we send this message, we don't need to call CloseWrite here.)
|
|
c.sendf("ERROR :Closing link: %s[%s] (%s)",
|
|
c.nicknameOrStar(), c.hostname /* TODO host IP? */, reason)
|
|
c.closing = true
|
|
|
|
c.unregister(reason)
|
|
c.setKillTimer()
|
|
}
|
|
|
|
func (c *client) inMaskList(masks []string) bool {
|
|
client := fmt.Sprintf("%s!%s@%s", c.nickname, c.username, c.hostname)
|
|
for _, mask := range masks {
|
|
if ircFnmatch(mask, client) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (c *client) getTLSCertFingerprint() string {
|
|
if c.tls == nil {
|
|
return ""
|
|
}
|
|
|
|
peerCerts := c.tls.ConnectionState().PeerCertificates
|
|
if len(peerCerts) == 0 {
|
|
return ""
|
|
}
|
|
|
|
hash := sha256.Sum256(peerCerts[0].Raw)
|
|
return hex.EncodeToString(hash[:])
|
|
}
|
|
|
|
// --- Timers ------------------------------------------------------------------
|
|
|
|
// Free the resources of timers that haven't fired yet and for timers that are
|
|
// in between firing and being collected by the event loop, mark that the event
|
|
// should not be acted upon.
|
|
func (c *client) cancelTimers() {
|
|
for _, timer := range []**time.Timer{
|
|
&c.killTimer, &c.timeoutTimer, &c.pingTimer,
|
|
} {
|
|
if *timer != nil {
|
|
(*timer).Stop()
|
|
*timer = nil
|
|
}
|
|
}
|
|
}
|
|
|
|
// Arrange for a function to be called later from the main goroutine.
|
|
func (c *client) setTimer(timer **time.Timer, delay time.Duration, cb func()) {
|
|
c.cancelTimers()
|
|
|
|
var identityCapture *time.Timer
|
|
identityCapture = time.AfterFunc(delay, func() {
|
|
timers <- func() {
|
|
// The timer might have been cancelled or even replaced.
|
|
// When the client is killed, this will be nil.
|
|
if *timer == identityCapture {
|
|
cb()
|
|
}
|
|
}
|
|
})
|
|
|
|
*timer = identityCapture
|
|
}
|
|
|
|
func (c *client) setKillTimer() {
|
|
c.setTimer(&c.killTimer, pingInterval, func() {
|
|
c.kill("Timeout")
|
|
})
|
|
}
|
|
|
|
func (c *client) setTimeoutTimer() {
|
|
c.setTimer(&c.timeoutTimer, pingInterval, func() {
|
|
c.closeLink(fmt.Sprintf("Ping timeout: >%d seconds",
|
|
pingInterval/time.Second))
|
|
})
|
|
}
|
|
|
|
func (c *client) setPingTimer() {
|
|
c.setTimer(&c.pingTimer, pingInterval, func() {
|
|
c.sendf("PING :%s", serverName)
|
|
c.setTimeoutTimer()
|
|
})
|
|
}
|
|
|
|
// --- IRC command handling ----------------------------------------------------
|
|
|
|
func (c *client) makeReply(id int, ap ...interface{}) string {
|
|
s := fmt.Sprintf(":%s %03d %s ", serverName, id, c.nicknameOrStar())
|
|
if reply, ok := catalog[id]; ok {
|
|
return s + fmt.Sprintf(reply, ap...)
|
|
}
|
|
return s + fmt.Sprintf(defaultReplies[id], ap...)
|
|
}
|
|
|
|
// XXX: This way simple static analysis cannot typecheck the arguments, so we
|
|
// need to be careful.
|
|
func (c *client) sendReply(id int, args ...interface{}) {
|
|
c.send(c.makeReply(id, args...))
|
|
}
|
|
|
|
// Send a space-separated list of words across as many replies as needed.
|
|
func (c *client) sendReplyVector(id int, items []string, args ...interface{}) {
|
|
common := c.makeReply(id, args...)
|
|
|
|
// We always send at least one message (there might be a client that
|
|
// expects us to send this message at least once).
|
|
if len(items) == 0 {
|
|
items = append(items, "")
|
|
}
|
|
|
|
for len(items) > 0 {
|
|
// If not even a single item fits in the limit (which may happen,
|
|
// in theory) it just gets cropped. We could also skip it.
|
|
reply := append([]byte(common), items[0]...)
|
|
items = items[1:]
|
|
|
|
// Append as many items as fits in a single message.
|
|
for len(items) > 0 &&
|
|
len(reply)+1+len(items[0]) <= ircMaxMessageLength {
|
|
reply = append(reply, ' ')
|
|
reply = append(reply, items[0]...)
|
|
items = items[1:]
|
|
}
|
|
|
|
c.send(string(reply))
|
|
}
|
|
}
|
|
|
|
func (c *client) sendMOTD() {
|
|
if len(motd) == 0 {
|
|
c.sendReply(ERR_NOMOTD)
|
|
return
|
|
}
|
|
|
|
c.sendReply(RPL_MOTDSTART, serverName)
|
|
for _, line := range motd {
|
|
c.sendReply(RPL_MOTD, line)
|
|
}
|
|
c.sendReply(RPL_ENDOFMOTD)
|
|
}
|
|
|
|
func (c *client) sendLUSERS() {
|
|
nUsers, nServices, nOpers, nUnknown := 0, 0, 0, 0
|
|
for c := range clients {
|
|
if c.registered {
|
|
nUsers++
|
|
} else {
|
|
nUnknown++
|
|
}
|
|
if 0 != c.mode&ircUserModeOperator {
|
|
nOpers++
|
|
}
|
|
}
|
|
|
|
nChannels := 0
|
|
for _, ch := range channels {
|
|
if 0 != ch.modes&ircChanModeSecret {
|
|
nChannels++
|
|
}
|
|
}
|
|
|
|
c.sendReply(RPL_LUSERCLIENT, nUsers, nServices, 1 /* servers total */)
|
|
if nOpers != 0 {
|
|
c.sendReply(RPL_LUSEROP, nOpers)
|
|
}
|
|
if nUnknown != 0 {
|
|
c.sendReply(RPL_LUSERUNKNOWN, nUnknown)
|
|
}
|
|
if nChannels != 0 {
|
|
c.sendReply(RPL_LUSERCHANNELS, nChannels)
|
|
}
|
|
c.sendReply(RPL_LUSERME, nUsers+nServices+nUnknown, 0 /* peer servers */)
|
|
}
|
|
|
|
func ircIsThisMe(target string) bool {
|
|
// Target servers can also be matched by their users
|
|
if ircFnmatch(target, serverName) {
|
|
return true
|
|
}
|
|
_, ok := users[ircToCanon(target)]
|
|
return ok
|
|
}
|
|
|
|
func (c *client) sendISUPPORT() {
|
|
// Only # channels, +e supported, +I supported, unlimited arguments to MODE.
|
|
c.sendReply(RPL_ISUPPORT, fmt.Sprintf("CHANTYPES=# EXCEPTS INVEX MODES"+
|
|
" TARGMAX=WHOIS:,LIST:,NAMES:,PRIVMSG:1,NOTICE:1,KICK:"+
|
|
" NICKLEN=%d CHANNELLEN=%d", ircMaxNickname, ircMaxChannelName))
|
|
}
|
|
|
|
func (c *client) tryFinishRegistration() {
|
|
if c.registered || c.capNegotiating {
|
|
return
|
|
}
|
|
if c.nickname == "" || c.username == "" {
|
|
return
|
|
}
|
|
|
|
c.registered = true
|
|
c.sendReply(RPL_WELCOME, c.nickname, c.username, c.hostname)
|
|
|
|
c.sendReply(RPL_YOURHOST, serverName, projectVersion)
|
|
// The purpose of this message eludes me.
|
|
c.sendReply(RPL_CREATED, started.Format("Mon, 02 Jan 2006"))
|
|
c.sendReply(RPL_MYINFO, serverName, projectVersion,
|
|
ircSupportedUserModes, ircSupportedChanModes)
|
|
|
|
c.sendISUPPORT()
|
|
c.sendLUSERS()
|
|
c.sendMOTD()
|
|
|
|
if mode := c.getMode(); mode != "" {
|
|
c.sendf(":%s MODE %s :+%s", c.nickname, c.nickname, mode)
|
|
}
|
|
|
|
c.tlsCertFingerprint = c.getTLSCertFingerprint()
|
|
if c.tlsCertFingerprint != "" {
|
|
c.sendf(":%s NOTICE %s :Your TLS client certificate fingerprint is %s",
|
|
serverName, c.nickname, c.tlsCertFingerprint)
|
|
}
|
|
|
|
delete(whowas, ircToCanon(c.nickname))
|
|
}
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
// IRCv3 capability negotiation. See http://ircv3.org for details.
|
|
|
|
type ircCapArgs struct {
|
|
subcommand string // the subcommand being processed
|
|
fullParams string // whole parameter string
|
|
params []string // split parameters
|
|
target string // target parameter for replies
|
|
}
|
|
|
|
var ircCapTable = []struct {
|
|
flag uint // flag
|
|
name string // name of the capability
|
|
}{
|
|
{ircCapMultiPrefix, "multi-prefix"},
|
|
{ircCapInviteNotify, "invite-notify"},
|
|
{ircCapEchoMessage, "echo-message"},
|
|
{ircCapUserhostInNames, "userhost-in-names"},
|
|
{ircCapServerTime, "server-time"},
|
|
}
|
|
|
|
func (c *client) handleCAPLS(a *ircCapArgs) {
|
|
if len(a.params) == 1 {
|
|
if ver, err := strconv.ParseUint(a.params[0], 10, 32); err != nil {
|
|
c.sendReply(ERR_INVALIDCAPCMD, a.subcommand,
|
|
"Ignoring invalid protocol version number")
|
|
} else {
|
|
c.capVersion = uint(ver)
|
|
}
|
|
}
|
|
|
|
c.capNegotiating = true
|
|
c.sendf(":%s CAP %s LS :multi-prefix invite-notify echo-message"+
|
|
" userhost-in-names server-time", serverName, a.target)
|
|
}
|
|
|
|
func (c *client) handleCAPLIST(a *ircCapArgs) {
|
|
caps := []string{}
|
|
for _, cap := range ircCapTable {
|
|
if 0 != c.capsEnabled&cap.flag {
|
|
caps = append(caps, cap.name)
|
|
}
|
|
}
|
|
|
|
c.sendf(":%s CAP %s LIST :%s", serverName, a.target,
|
|
strings.Join(caps, " "))
|
|
}
|
|
|
|
func ircDecodeCapability(name string) uint {
|
|
for _, cap := range ircCapTable {
|
|
if cap.name == name {
|
|
return cap.flag
|
|
}
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func (c *client) handleCAPREQ(a *ircCapArgs) {
|
|
c.capNegotiating = true
|
|
|
|
newCaps := c.capsEnabled
|
|
ok := true
|
|
for _, param := range a.params {
|
|
removing := false
|
|
name := param
|
|
if name[:1] == "-" {
|
|
removing = true
|
|
name = name[1:]
|
|
}
|
|
|
|
if cap := ircDecodeCapability(name); cap == 0 {
|
|
ok = false
|
|
} else if removing {
|
|
newCaps &= ^cap
|
|
} else {
|
|
newCaps |= cap
|
|
}
|
|
}
|
|
|
|
if ok {
|
|
c.capsEnabled = newCaps
|
|
c.sendf(":%s CAP %s ACK :%s", serverName, a.target, a.fullParams)
|
|
} else {
|
|
c.sendf(":%s CAP %s NAK :%s", serverName, a.target, a.fullParams)
|
|
}
|
|
}
|
|
|
|
func (c *client) handleCAPACK(a *ircCapArgs) {
|
|
if len(a.params) > 0 {
|
|
c.sendReply(ERR_INVALIDCAPCMD, a.subcommand,
|
|
"No acknowledgable capabilities supported")
|
|
}
|
|
}
|
|
|
|
func (c *client) handleCAPEND(a *ircCapArgs) {
|
|
c.capNegotiating = false
|
|
c.tryFinishRegistration()
|
|
}
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
var ircCapHandlers = map[string]func(*client, *ircCapArgs){
|
|
"LS": (*client).handleCAPLS,
|
|
"LIST": (*client).handleCAPLIST,
|
|
"REQ": (*client).handleCAPREQ,
|
|
"ACK": (*client).handleCAPACK,
|
|
"END": (*client).handleCAPEND,
|
|
}
|
|
|
|
// XXX: Maybe these also deserve to be methods for client? They operate on
|
|
// global state, though.
|
|
|
|
func ircParseWEBIRCOptions(options string, out map[string]string) {
|
|
for _, option := range strings.Split(options, " ") {
|
|
if equal := strings.IndexByte(option, '='); equal < 0 {
|
|
out[option] = ""
|
|
} else {
|
|
out[option[:equal]] = ircUnescapeMessageTag(option[equal+1:])
|
|
}
|
|
}
|
|
}
|
|
|
|
func ircHandleWEBIRC(msg *message, c *client) {
|
|
if len(msg.params) < 4 {
|
|
c.sendReply(ERR_NEEDMOREPARAMS, msg.command)
|
|
return
|
|
}
|
|
|
|
password, gateway, hostname := msg.params[0], msg.params[1], msg.params[2]
|
|
if config["webirc_password"] != password {
|
|
c.closeLink("Invalid WebIRC password")
|
|
return
|
|
}
|
|
|
|
options := make(map[string]string)
|
|
if len(msg.params) >= 5 {
|
|
ircParseWEBIRCOptions(msg.params[4], options)
|
|
}
|
|
|
|
c.hostname = hostname
|
|
c.port = "WebIRC-" + gateway
|
|
c.address = net.JoinHostPort(hostname, c.port)
|
|
|
|
// Note that this overrides the gateway's certificate, conditionally.
|
|
fp, _ := options["certfp-sha-256"]
|
|
if _, secure := options["secure"]; secure && ircIsValidFingerprint(fp) {
|
|
c.tlsCertFingerprint = strings.ToLower(fp)
|
|
}
|
|
}
|
|
|
|
func ircHandleCAP(msg *message, c *client) {
|
|
if len(msg.params) < 1 {
|
|
c.sendReply(ERR_NEEDMOREPARAMS, msg.command)
|
|
return
|
|
}
|
|
|
|
args := &ircCapArgs{
|
|
target: c.nicknameOrStar(),
|
|
subcommand: msg.params[0],
|
|
fullParams: "",
|
|
params: []string{},
|
|
}
|
|
|
|
if len(msg.params) > 1 {
|
|
args.fullParams = msg.params[1]
|
|
args.params = splitString(args.fullParams, " ", true)
|
|
}
|
|
|
|
if fn, ok := ircCapHandlers[ircToCanon(args.subcommand)]; !ok {
|
|
c.sendReply(ERR_INVALIDCAPCMD, args.subcommand,
|
|
"Invalid CAP subcommand")
|
|
} else {
|
|
fn(c, args)
|
|
}
|
|
}
|
|
|
|
func ircHandlePASS(msg *message, c *client) {
|
|
if c.registered {
|
|
c.sendReply(ERR_ALREADYREGISTERED)
|
|
} else if len(msg.params) < 1 {
|
|
c.sendReply(ERR_NEEDMOREPARAMS, msg.command)
|
|
}
|
|
|
|
// We have TLS client certificates for this purpose; ignoring.
|
|
}
|
|
|
|
func ircHandleNICK(msg *message, c *client) {
|
|
if len(msg.params) < 1 {
|
|
c.sendReply(ERR_NONICKNAMEGIVEN)
|
|
return
|
|
}
|
|
|
|
nickname := msg.params[0]
|
|
if !ircIsValidNickname(nickname) {
|
|
c.sendReply(ERR_ERRONEOUSNICKNAME, nickname)
|
|
return
|
|
}
|
|
|
|
nicknameCanon := ircToCanon(nickname)
|
|
if client, ok := users[nicknameCanon]; ok && client != c {
|
|
c.sendReply(ERR_NICKNAMEINUSE, nickname)
|
|
return
|
|
}
|
|
|
|
if c.registered {
|
|
c.addToWhowas()
|
|
|
|
message := fmt.Sprintf(":%s!%s@%s NICK :%s",
|
|
c.nickname, c.username, c.hostname, nickname)
|
|
ircNotifyRoommates(c, message)
|
|
c.send(message)
|
|
}
|
|
|
|
// Release the old nickname and allocate a new one.
|
|
if c.nickname != "" {
|
|
delete(users, ircToCanon(c.nickname))
|
|
}
|
|
|
|
c.nickname = nickname
|
|
users[nicknameCanon] = c
|
|
|
|
c.tryFinishRegistration()
|
|
}
|
|
|
|
func ircHandleUSER(msg *message, c *client) {
|
|
if c.registered {
|
|
c.sendReply(ERR_ALREADYREGISTERED)
|
|
return
|
|
}
|
|
if len(msg.params) < 4 {
|
|
c.sendReply(ERR_NEEDMOREPARAMS, msg.command)
|
|
return
|
|
}
|
|
|
|
username, mode, realname := msg.params[0], msg.params[1], msg.params[3]
|
|
|
|
// Unfortunately, the protocol doesn't give us any means of rejecting it.
|
|
if !ircIsValidUsername(username) {
|
|
username = "*"
|
|
}
|
|
|
|
c.username = username
|
|
c.realname = realname
|
|
|
|
c.mode = 0
|
|
if m, err := strconv.ParseUint(mode, 10, 32); err != nil {
|
|
if 0 != m&4 {
|
|
c.mode |= ircUserModeRxWallops
|
|
}
|
|
if 0 != m&8 {
|
|
c.mode |= ircUserModeInvisible
|
|
}
|
|
}
|
|
|
|
c.tryFinishRegistration()
|
|
}
|
|
|
|
func ircHandleUSERHOST(msg *message, c *client) {
|
|
if len(msg.params) < 1 {
|
|
c.sendReply(ERR_NEEDMOREPARAMS, msg.command)
|
|
return
|
|
}
|
|
|
|
var reply []byte
|
|
for i := 0; i < 5 && i < len(msg.params); i++ {
|
|
nick := msg.params[i]
|
|
target := users[ircToCanon(nick)]
|
|
if target == nil {
|
|
continue
|
|
}
|
|
if i != 0 {
|
|
reply = append(reply, ' ')
|
|
}
|
|
|
|
reply = append(reply, nick...)
|
|
if 0 != target.mode&ircUserModeOperator {
|
|
reply = append(reply, '*')
|
|
}
|
|
|
|
if target.awayMessage != "" {
|
|
reply = append(reply, "=-"...)
|
|
} else {
|
|
reply = append(reply, "=+"...)
|
|
}
|
|
reply = append(reply, (target.username + "@" + target.hostname)...)
|
|
}
|
|
c.sendReply(RPL_USERHOST, string(reply))
|
|
}
|
|
|
|
func ircHandleLUSERS(msg *message, c *client) {
|
|
if len(msg.params) > 1 && !ircIsThisMe(msg.params[1]) {
|
|
c.sendReply(ERR_NOSUCHSERVER, msg.params[1])
|
|
return
|
|
}
|
|
c.sendLUSERS()
|
|
}
|
|
|
|
func ircHandleMOTD(msg *message, c *client) {
|
|
if len(msg.params) > 0 && !ircIsThisMe(msg.params[0]) {
|
|
c.sendReply(ERR_NOSUCHSERVER, msg.params[1])
|
|
return
|
|
}
|
|
c.sendMOTD()
|
|
}
|
|
|
|
func ircHandlePING(msg *message, c *client) {
|
|
// XXX: The RFC is pretty incomprehensible about the exact usage.
|
|
if len(msg.params) > 1 && !ircIsThisMe(msg.params[1]) {
|
|
c.sendReply(ERR_NOSUCHSERVER, msg.params[1])
|
|
} else if len(msg.params) < 1 {
|
|
c.sendReply(ERR_NOORIGIN)
|
|
} else {
|
|
c.sendf(":%s PONG :%s", serverName, msg.params[0])
|
|
}
|
|
}
|
|
|
|
func ircHandlePONG(msg *message, c *client) {
|
|
// We are the only server, so we don't have to care too much.
|
|
if len(msg.params) < 1 {
|
|
c.sendReply(ERR_NOORIGIN)
|
|
return
|
|
}
|
|
|
|
// Set a new timer to send another PING
|
|
c.setPingTimer()
|
|
}
|
|
|
|
func ircHandleQUIT(msg *message, c *client) {
|
|
reason := c.nickname
|
|
if len(msg.params) > 0 {
|
|
reason = msg.params[0]
|
|
}
|
|
|
|
c.closeLink("Quit: " + reason)
|
|
}
|
|
|
|
func ircHandleTIME(msg *message, c *client) {
|
|
if len(msg.params) > 0 && !ircIsThisMe(msg.params[0]) {
|
|
c.sendReply(ERR_NOSUCHSERVER, msg.params[0])
|
|
return
|
|
}
|
|
|
|
c.sendReply(RPL_TIME, serverName,
|
|
time.Now().Format("Mon Jan _2 2006 15:04:05"))
|
|
}
|
|
|
|
func ircHandleVERSION(msg *message, c *client) {
|
|
if len(msg.params) > 0 && !ircIsThisMe(msg.params[0]) {
|
|
c.sendReply(ERR_NOSUCHSERVER, msg.params[0])
|
|
return
|
|
}
|
|
|
|
postVersion := 0
|
|
if debugMode {
|
|
postVersion = 1
|
|
}
|
|
|
|
c.sendReply(RPL_VERSION, projectVersion, postVersion, serverName,
|
|
projectName+" "+projectVersion)
|
|
c.sendISUPPORT()
|
|
}
|
|
|
|
func ircChannelMulticast(ch *channel, msg string, except *client) {
|
|
for c := range ch.userModes {
|
|
if c != except {
|
|
c.send(msg)
|
|
}
|
|
}
|
|
}
|
|
|
|
func ircModifyMode(mask *uint, mode uint, add bool) bool {
|
|
orig := *mask
|
|
if add {
|
|
*mask |= mode
|
|
} else {
|
|
*mask &= ^mode
|
|
}
|
|
return *mask != orig
|
|
}
|
|
|
|
func ircUpdateUserMode(c *client, newMode uint) {
|
|
oldMode := c.mode
|
|
c.mode = newMode
|
|
|
|
added, removed := newMode & ^oldMode, oldMode & ^newMode
|
|
|
|
var diff []byte
|
|
if added != 0 {
|
|
diff = append(diff, '+')
|
|
diff = ircAppendClientModes(added, diff)
|
|
}
|
|
if removed != 0 {
|
|
diff = append(diff, '-')
|
|
diff = ircAppendClientModes(removed, diff)
|
|
}
|
|
|
|
if len(diff) > 0 {
|
|
c.sendf(":%s MODE %s :%s", c.nickname, c.nickname, string(diff))
|
|
}
|
|
}
|
|
|
|
func ircHandleUserModeChange(c *client, modeString string) {
|
|
newMode := c.mode
|
|
adding := true
|
|
|
|
for _, flag := range modeString {
|
|
switch flag {
|
|
case '+':
|
|
adding = true
|
|
case '-':
|
|
adding = false
|
|
|
|
case 'a':
|
|
// Ignore, the client should use AWAY.
|
|
case 'i':
|
|
ircModifyMode(&newMode, ircUserModeInvisible, adding)
|
|
case 'w':
|
|
ircModifyMode(&newMode, ircUserModeRxWallops, adding)
|
|
case 'r':
|
|
// It's not possible ot un-restrict yourself.
|
|
if adding {
|
|
newMode |= ircUserModeRestricted
|
|
}
|
|
case 'o':
|
|
if !adding {
|
|
newMode &= ^ircUserModeOperator
|
|
} else if operators[c.tlsCertFingerprint] {
|
|
newMode |= ircUserModeOperator
|
|
} else {
|
|
c.sendf(":%s NOTICE %s :Either you're not using an TLS"+
|
|
" client certificate, or the fingerprint doesn't match",
|
|
serverName, c.nickname)
|
|
}
|
|
case 's':
|
|
ircModifyMode(&newMode, ircUserModeRxServerNotices, adding)
|
|
default:
|
|
c.sendReply(ERR_UMODEUNKNOWNFLAG)
|
|
return
|
|
}
|
|
}
|
|
ircUpdateUserMode(c, newMode)
|
|
}
|
|
|
|
func ircSendChannelList(c *client, channelName string, list []string,
|
|
reply, endReply int) {
|
|
for _, line := range list {
|
|
c.sendReply(reply, channelName, line)
|
|
}
|
|
c.sendReply(endReply, channelName)
|
|
}
|
|
|
|
func ircCheckExpandUserMask(mask string) string {
|
|
var result []byte
|
|
result = append(result, mask...)
|
|
|
|
// Make sure it is a complete mask.
|
|
if bytes.IndexByte(result, '!') < 0 {
|
|
result = append(result, "!*"...)
|
|
}
|
|
if bytes.IndexByte(result, '@') < 0 {
|
|
result = append(result, "@*"...)
|
|
}
|
|
|
|
// And validate whatever the result is.
|
|
s := string(result)
|
|
if !ircIsValidUserMask(s) {
|
|
return ""
|
|
}
|
|
|
|
return s
|
|
}
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
// Channel MODE command handling. This is by far the worst command to implement
|
|
// from the whole RFC; don't blame me if it doesn't work exactly as expected.
|
|
|
|
type modeProcessor struct {
|
|
params []string // mode string parameters
|
|
|
|
c *client // who does the changes
|
|
ch *channel // the channel we're modifying
|
|
present bool // c present on ch
|
|
modes uint // channel user modes
|
|
|
|
adding bool // currently adding modes
|
|
modeChar byte // currently processed mode char
|
|
|
|
added []byte // added modes
|
|
removed []byte // removed modes
|
|
output *[]byte // "added" or "removed"
|
|
|
|
addedParams []string // params for added modes
|
|
removedParams []string // params for removed modes
|
|
outputParams *[]string // "addedParams" or "removedParams"
|
|
}
|
|
|
|
func (mp *modeProcessor) nextParam() string {
|
|
if len(mp.params) == 0 {
|
|
return ""
|
|
}
|
|
|
|
param := mp.params[0]
|
|
mp.params = mp.params[1:]
|
|
return param
|
|
}
|
|
|
|
func (mp *modeProcessor) checkOperator() bool {
|
|
if (mp.present && 0 != mp.modes&ircChanModeOperator) ||
|
|
0 != mp.c.mode&ircUserModeOperator {
|
|
return true
|
|
}
|
|
|
|
mp.c.sendReply(ERR_CHANOPRIVSNEEDED, mp.ch.name)
|
|
return false
|
|
}
|
|
|
|
func (mp *modeProcessor) doUser(mode uint) {
|
|
target := mp.nextParam()
|
|
if !mp.checkOperator() || target == "" {
|
|
return
|
|
}
|
|
|
|
if client := users[ircToCanon(target)]; client == nil {
|
|
mp.c.sendReply(ERR_NOSUCHNICK, target)
|
|
} else if modes, present := mp.ch.userModes[client]; !present {
|
|
mp.c.sendReply(ERR_USERNOTINCHANNEL, target, mp.ch.name)
|
|
} else if ircModifyMode(&modes, mode, mp.adding) {
|
|
mp.ch.userModes[client] = modes
|
|
*mp.output = append(*mp.output, mp.modeChar)
|
|
*mp.outputParams = append(*mp.outputParams, client.nickname)
|
|
}
|
|
}
|
|
|
|
func (mp *modeProcessor) doChan(mode uint) bool {
|
|
if !mp.checkOperator() || !ircModifyMode(&mp.ch.modes, mode, mp.adding) {
|
|
return false
|
|
}
|
|
*mp.output = append(*mp.output, mp.modeChar)
|
|
return true
|
|
}
|
|
|
|
func (mp *modeProcessor) doChanRemove(modeChar byte, mode uint) {
|
|
if mp.adding && ircModifyMode(&mp.ch.modes, mode, false) {
|
|
mp.removed = append(mp.removed, modeChar)
|
|
}
|
|
}
|
|
|
|
func (mp *modeProcessor) doList(list *[]string, listMsg, endMsg int) {
|
|
target := mp.nextParam()
|
|
if target == "" {
|
|
if mp.adding {
|
|
ircSendChannelList(mp.c, mp.ch.name, *list, listMsg, endMsg)
|
|
}
|
|
return
|
|
}
|
|
|
|
if !mp.checkOperator() {
|
|
return
|
|
}
|
|
|
|
mask := ircCheckExpandUserMask(target)
|
|
if mask == "" {
|
|
return
|
|
}
|
|
|
|
var i int
|
|
for i = 0; i < len(*list); i++ {
|
|
if ircEqual((*list)[i], mask) {
|
|
break
|
|
}
|
|
}
|
|
|
|
found := i < len(*list)
|
|
if found != mp.adding {
|
|
if mp.adding {
|
|
*list = append(*list, mask)
|
|
} else {
|
|
*list = append((*list)[:i], (*list)[i+1:]...)
|
|
}
|
|
|
|
*mp.output = append(*mp.output, mp.modeChar)
|
|
*mp.outputParams = append(*mp.outputParams, mask)
|
|
}
|
|
}
|
|
|
|
func (mp *modeProcessor) doKey() {
|
|
target := mp.nextParam()
|
|
if !mp.checkOperator() || target == "" {
|
|
return
|
|
}
|
|
|
|
if !mp.adding {
|
|
if mp.ch.key == "" || !ircEqual(target, mp.ch.key) {
|
|
return
|
|
}
|
|
|
|
mp.removed = append(mp.removed, mp.modeChar)
|
|
mp.removedParams = append(mp.removedParams, mp.ch.key)
|
|
mp.ch.key = ""
|
|
} else if !ircIsValidKey(target) {
|
|
// TODO: We should notify the user somehow.
|
|
return
|
|
} else if mp.ch.key != "" {
|
|
mp.c.sendReply(ERR_KEYSET, mp.ch.name)
|
|
} else {
|
|
mp.ch.key = target
|
|
mp.added = append(mp.added, mp.modeChar)
|
|
mp.addedParams = append(mp.addedParams, mp.ch.key)
|
|
}
|
|
}
|
|
|
|
func (mp *modeProcessor) doLimit() {
|
|
if !mp.checkOperator() {
|
|
return
|
|
}
|
|
|
|
if !mp.adding {
|
|
if mp.ch.userLimit == -1 {
|
|
return
|
|
}
|
|
|
|
mp.ch.userLimit = -1
|
|
mp.removed = append(mp.removed, mp.modeChar)
|
|
} else if target := mp.nextParam(); target != "" {
|
|
if x, err := strconv.ParseInt(target, 10, 32); err == nil && x > 0 {
|
|
mp.ch.userLimit = int(x)
|
|
mp.added = append(mp.added, mp.modeChar)
|
|
mp.addedParams = append(mp.addedParams, target)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (mp *modeProcessor) step(modeChar byte) bool {
|
|
mp.modeChar = modeChar
|
|
switch mp.modeChar {
|
|
case '+':
|
|
mp.adding = true
|
|
mp.output = &mp.added
|
|
mp.outputParams = &mp.addedParams
|
|
case '-':
|
|
mp.adding = false
|
|
mp.output = &mp.removed
|
|
mp.outputParams = &mp.removedParams
|
|
|
|
case 'o':
|
|
mp.doUser(ircChanModeOperator)
|
|
case 'v':
|
|
mp.doUser(ircChanModeVoice)
|
|
|
|
case 'i':
|
|
mp.doChan(ircChanModeInviteOnly)
|
|
case 'm':
|
|
mp.doChan(ircChanModeModerated)
|
|
case 'n':
|
|
mp.doChan(ircChanModeNoOutsideMsgs)
|
|
case 'q':
|
|
mp.doChan(ircChanModeQuiet)
|
|
case 't':
|
|
mp.doChan(ircChanModeProtectedTopic)
|
|
|
|
case 'p':
|
|
if mp.doChan(ircChanModePrivate) {
|
|
mp.doChanRemove('s', ircChanModeSecret)
|
|
}
|
|
case 's':
|
|
if mp.doChan(ircChanModeSecret) {
|
|
mp.doChanRemove('p', ircChanModePrivate)
|
|
}
|
|
|
|
case 'b':
|
|
mp.doList(&mp.ch.banList, RPL_BANLIST, RPL_ENDOFBANLIST)
|
|
case 'e':
|
|
mp.doList(&mp.ch.banList, RPL_EXCEPTLIST, RPL_ENDOFEXCEPTLIST)
|
|
case 'I':
|
|
mp.doList(&mp.ch.banList, RPL_INVITELIST, RPL_ENDOFINVITELIST)
|
|
|
|
case 'k':
|
|
mp.doKey()
|
|
case 'l':
|
|
mp.doLimit()
|
|
|
|
default:
|
|
// It's not safe to continue, results could be undesired.
|
|
mp.c.sendReply(ERR_UNKNOWNMODE, modeChar, mp.ch.name)
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func ircHandleChanModeChange(c *client, ch *channel, params []string) {
|
|
modes, present := ch.userModes[c]
|
|
mp := &modeProcessor{
|
|
c: c,
|
|
ch: ch,
|
|
present: present,
|
|
modes: modes,
|
|
params: params,
|
|
}
|
|
|
|
Outer:
|
|
for {
|
|
modeString := mp.nextParam()
|
|
if modeString == "" {
|
|
break
|
|
}
|
|
|
|
mp.step('+')
|
|
for _, modeChar := range []byte(modeString) {
|
|
if !mp.step(modeChar) {
|
|
break Outer
|
|
}
|
|
}
|
|
}
|
|
|
|
// TODO: Limit to three changes with parameter per command.
|
|
if len(mp.added) > 0 || len(mp.removed) > 0 {
|
|
buf := []byte(fmt.Sprintf(":%s!%s@%s MODE %s ",
|
|
mp.c.nickname, mp.c.username, mp.c.hostname, mp.ch.name))
|
|
if len(mp.added) > 0 {
|
|
buf = append(buf, '+')
|
|
buf = append(buf, mp.added...)
|
|
}
|
|
if len(mp.removed) > 0 {
|
|
buf = append(buf, '-')
|
|
buf = append(buf, mp.removed...)
|
|
}
|
|
for _, param := range mp.addedParams {
|
|
buf = append(buf, ' ')
|
|
buf = append(buf, param...)
|
|
}
|
|
for _, param := range mp.removedParams {
|
|
buf = append(buf, ' ')
|
|
buf = append(buf, param...)
|
|
}
|
|
ircChannelMulticast(mp.ch, string(buf), nil)
|
|
}
|
|
}
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
func ircHandleMODE(msg *message, c *client) {
|
|
if len(msg.params) < 1 {
|
|
c.sendReply(ERR_NEEDMOREPARAMS, msg.command)
|
|
return
|
|
}
|
|
|
|
target := msg.params[0]
|
|
client := users[ircToCanon(target)]
|
|
ch := channels[ircToCanon(target)]
|
|
|
|
if client != nil {
|
|
if !ircEqual(target, c.nickname) {
|
|
c.sendReply(ERR_USERSDONTMATCH)
|
|
return
|
|
}
|
|
|
|
if len(msg.params) < 2 {
|
|
c.sendReply(RPL_UMODEIS, c.getMode())
|
|
} else {
|
|
ircHandleUserModeChange(c, msg.params[1])
|
|
}
|
|
} else if ch != nil {
|
|
if len(msg.params) < 2 {
|
|
_, present := ch.userModes[c]
|
|
c.sendReply(RPL_CHANNELMODEIS, target, ch.getMode(present))
|
|
c.sendReply(RPL_CREATIONTIME, target, ch.created.Unix())
|
|
} else {
|
|
ircHandleChanModeChange(c, ch, msg.params[1:])
|
|
}
|
|
} else {
|
|
c.sendReply(ERR_NOSUCHNICK, target)
|
|
}
|
|
}
|
|
|
|
func ircHandleUserMessage(msg *message, c *client,
|
|
command string, allowAwayReply bool) {
|
|
if len(msg.params) < 1 {
|
|
c.sendReply(ERR_NORECIPIENT, msg.command)
|
|
return
|
|
}
|
|
if len(msg.params) < 2 || msg.params[1] == "" {
|
|
c.sendReply(ERR_NOTEXTTOSEND)
|
|
return
|
|
}
|
|
|
|
target, text := msg.params[0], msg.params[1]
|
|
message := fmt.Sprintf(":%s!%s@%s %s %s :%s",
|
|
c.nickname, c.username, c.hostname, command, target, text)
|
|
|
|
if client := users[ircToCanon(target)]; client != nil {
|
|
client.send(message)
|
|
if allowAwayReply && client.awayMessage != "" {
|
|
c.sendReply(RPL_AWAY, target, client.awayMessage)
|
|
}
|
|
|
|
// Acknowledging a message from the client to itself would be silly.
|
|
if client != c && (0 != c.capsEnabled&ircCapEchoMessage) {
|
|
c.send(message)
|
|
}
|
|
} else if ch := channels[ircToCanon(target)]; ch != nil {
|
|
modes, present := ch.userModes[c]
|
|
|
|
outsider := !present && 0 != ch.modes&ircChanModeNoOutsideMsgs
|
|
moderated := 0 != ch.modes&ircChanModeModerated &&
|
|
0 == modes&(ircChanModeVoice|ircChanModeOperator)
|
|
banned := c.inMaskList(ch.banList) && !c.inMaskList(ch.exceptionList)
|
|
|
|
if outsider || moderated || banned {
|
|
c.sendReply(ERR_CANNOTSENDTOCHAN, target)
|
|
return
|
|
}
|
|
|
|
except := c
|
|
if 0 != c.capsEnabled&ircCapEchoMessage {
|
|
except = nil
|
|
}
|
|
|
|
ircChannelMulticast(ch, message, except)
|
|
} else {
|
|
c.sendReply(ERR_NOSUCHNICK, target)
|
|
}
|
|
}
|
|
|
|
func ircHandlePRIVMSG(msg *message, c *client) {
|
|
ircHandleUserMessage(msg, c, "PRIVMSG", true /* allowAwayReply */)
|
|
c.lastActive = time.Now()
|
|
}
|
|
|
|
func ircHandleNOTICE(msg *message, c *client) {
|
|
ircHandleUserMessage(msg, c, "NOTICE", false /* allowAwayReply */)
|
|
}
|
|
|
|
func ircHandleLIST(msg *message, c *client) {
|
|
if len(msg.params) > 1 && !ircIsThisMe(msg.params[1]) {
|
|
c.sendReply(ERR_NOSUCHSERVER, msg.params[1])
|
|
return
|
|
}
|
|
|
|
// XXX: Maybe we should skip ircUserModeInvisible from user counts.
|
|
if len(msg.params) == 0 {
|
|
for _, ch := range channels {
|
|
if _, present := ch.userModes[c]; present ||
|
|
0 == ch.modes&(ircChanModePrivate|ircChanModeSecret) {
|
|
c.sendReply(RPL_LIST, ch.name, len(ch.userModes), ch.topic)
|
|
}
|
|
}
|
|
} else {
|
|
for _, target := range splitString(msg.params[0], ",", true) {
|
|
if ch := channels[ircToCanon(target)]; ch != nil &&
|
|
0 == ch.modes&ircChanModeSecret {
|
|
c.sendReply(RPL_LIST, ch.name, len(ch.userModes), ch.topic)
|
|
}
|
|
}
|
|
}
|
|
c.sendReply(RPL_LISTEND)
|
|
}
|
|
|
|
func ircAppendPrefixes(c *client, modes uint, buf []byte) []byte {
|
|
var all []byte
|
|
if 0 != modes&ircChanModeOperator {
|
|
all = append(all, '@')
|
|
}
|
|
if 0 != modes&ircChanModeVoice {
|
|
all = append(all, '+')
|
|
}
|
|
|
|
if len(all) > 0 {
|
|
if 0 != c.capsEnabled&ircCapMultiPrefix {
|
|
buf = append(buf, all...)
|
|
} else {
|
|
buf = append(buf, all[0])
|
|
}
|
|
}
|
|
return buf
|
|
}
|
|
|
|
func ircMakeRPLNAMREPLYItem(c, target *client, modes uint) string {
|
|
result := string(ircAppendPrefixes(c, modes, nil)) + target.nickname
|
|
if 0 != c.capsEnabled&ircCapUserhostInNames {
|
|
result += fmt.Sprintf("!%s@%s", target.username, target.hostname)
|
|
}
|
|
return result
|
|
}
|
|
|
|
func ircSendRPLNAMREPLY(c *client, ch *channel, usedNicks map[*client]bool) {
|
|
kind := '='
|
|
if 0 != ch.modes&ircChanModeSecret {
|
|
kind = '@'
|
|
} else if 0 != ch.modes&ircChanModePrivate {
|
|
kind = '*'
|
|
}
|
|
|
|
_, present := ch.userModes[c]
|
|
|
|
var nicks []string
|
|
for client, modes := range ch.userModes {
|
|
if !present && 0 != client.mode&ircUserModeInvisible {
|
|
continue
|
|
}
|
|
if usedNicks != nil {
|
|
usedNicks[client] = true
|
|
}
|
|
nicks = append(nicks, ircMakeRPLNAMREPLYItem(c, client, modes))
|
|
}
|
|
c.sendReplyVector(RPL_NAMREPLY, nicks, kind, ch.name, "")
|
|
}
|
|
|
|
func ircSendDisassociatedNames(c *client, usedNicks map[*client]bool) {
|
|
var nicks []string
|
|
for _, client := range users {
|
|
if 0 == client.mode&ircUserModeInvisible && !usedNicks[client] {
|
|
nicks = append(nicks, ircMakeRPLNAMREPLYItem(c, client, 0))
|
|
}
|
|
}
|
|
if len(nicks) > 0 {
|
|
c.sendReplyVector(RPL_NAMREPLY, nicks, '*', "*", "")
|
|
}
|
|
}
|
|
|
|
func ircHandleNAMES(msg *message, c *client) {
|
|
if len(msg.params) > 1 && !ircIsThisMe(msg.params[1]) {
|
|
c.sendReply(ERR_NOSUCHSERVER, msg.params[1])
|
|
return
|
|
}
|
|
|
|
if len(msg.params) == 0 {
|
|
usedNicks := make(map[*client]bool)
|
|
for _, ch := range channels {
|
|
if _, present := ch.userModes[c]; present ||
|
|
0 == ch.modes&(ircChanModePrivate|ircChanModeSecret) {
|
|
ircSendRPLNAMREPLY(c, ch, usedNicks)
|
|
}
|
|
}
|
|
|
|
// Also send all visible users we haven't listed yet.
|
|
ircSendDisassociatedNames(c, usedNicks)
|
|
c.sendReply(RPL_ENDOFNAMES, "*")
|
|
} else {
|
|
for _, target := range splitString(msg.params[0], ",", true) {
|
|
if ch := channels[ircToCanon(target)]; ch == nil {
|
|
} else if _, present := ch.userModes[c]; present ||
|
|
0 == ch.modes&ircChanModeSecret {
|
|
ircSendRPLNAMREPLY(c, ch, nil)
|
|
c.sendReply(RPL_ENDOFNAMES, target)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func ircSendRPLWHOREPLY(c *client, ch *channel, target *client) {
|
|
var chars []byte
|
|
if target.awayMessage != "" {
|
|
chars = append(chars, 'G')
|
|
} else {
|
|
chars = append(chars, 'H')
|
|
}
|
|
|
|
if 0 != target.mode&ircUserModeOperator {
|
|
chars = append(chars, '*')
|
|
}
|
|
|
|
channelName := "*"
|
|
if ch != nil {
|
|
channelName = ch.name
|
|
if modes, present := ch.userModes[target]; present {
|
|
chars = ircAppendPrefixes(c, modes, chars)
|
|
}
|
|
}
|
|
|
|
c.sendReply(RPL_WHOREPLY, channelName,
|
|
target.username, target.hostname, serverName,
|
|
target.nickname, string(chars), 0 /* hop count */, target.realname)
|
|
}
|
|
|
|
func ircMatchSendRPLWHOREPLY(c, target *client, mask string) {
|
|
isRoommate := false
|
|
for _, ch := range channels {
|
|
_, presentClient := ch.userModes[c]
|
|
_, presentTarget := ch.userModes[target]
|
|
if presentClient && presentTarget {
|
|
isRoommate = true
|
|
break
|
|
}
|
|
}
|
|
if !isRoommate && 0 != target.mode&ircUserModeInvisible {
|
|
return
|
|
}
|
|
|
|
if !ircFnmatch(mask, target.hostname) &&
|
|
!ircFnmatch(mask, target.nickname) &&
|
|
!ircFnmatch(mask, target.realname) &&
|
|
!ircFnmatch(mask, serverName) {
|
|
return
|
|
}
|
|
|
|
// Try to find a channel they're on that's visible to us.
|
|
var userCh *channel
|
|
for _, ch := range channels {
|
|
_, presentClient := ch.userModes[c]
|
|
_, presentTarget := ch.userModes[target]
|
|
if presentTarget && (presentClient ||
|
|
0 == ch.modes&(ircChanModePrivate|ircChanModeSecret)) {
|
|
userCh = ch
|
|
break
|
|
}
|
|
}
|
|
ircSendRPLWHOREPLY(c, userCh, target)
|
|
}
|
|
|
|
func ircHandleWHO(msg *message, c *client) {
|
|
onlyOps := len(msg.params) > 1 && msg.params[1] == "o"
|
|
|
|
shownMask, usedMask := "*", "*"
|
|
if len(msg.params) > 0 {
|
|
shownMask = msg.params[0]
|
|
if shownMask != "0" {
|
|
usedMask = shownMask
|
|
}
|
|
}
|
|
|
|
if ch := channels[ircToCanon(usedMask)]; ch != nil {
|
|
_, present := ch.userModes[c]
|
|
if present || 0 == ch.modes&ircChanModeSecret {
|
|
for client := range ch.userModes {
|
|
if (present || 0 == client.mode&ircUserModeInvisible) &&
|
|
(!onlyOps || 0 != client.mode&ircUserModeOperator) {
|
|
ircSendRPLWHOREPLY(c, ch, client)
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
for _, client := range users {
|
|
if !onlyOps || 0 != client.mode&ircUserModeOperator {
|
|
ircMatchSendRPLWHOREPLY(c, client, usedMask)
|
|
}
|
|
}
|
|
}
|
|
c.sendReply(RPL_ENDOFWHO, shownMask)
|
|
}
|
|
|
|
func ircSendWHOISReply(c, target *client) {
|
|
nick := target.nickname
|
|
c.sendReply(RPL_WHOISUSER, nick,
|
|
target.username, target.hostname, target.realname)
|
|
c.sendReply(RPL_WHOISSERVER, nick, serverName, config["server_info"])
|
|
if 0 != target.mode&ircUserModeOperator {
|
|
c.sendReply(RPL_WHOISOPERATOR, nick)
|
|
}
|
|
c.sendReply(RPL_WHOISIDLE, nick,
|
|
time.Now().Sub(target.lastActive)/time.Second)
|
|
if target.awayMessage != "" {
|
|
c.sendReply(RPL_AWAY, nick, target.awayMessage)
|
|
}
|
|
|
|
var chans []string
|
|
for _, ch := range channels {
|
|
_, presentClient := ch.userModes[c]
|
|
modes, presentTarget := ch.userModes[target]
|
|
if presentTarget && (presentClient ||
|
|
0 == ch.modes&(ircChanModePrivate|ircChanModeSecret)) {
|
|
// TODO: Deduplicate, ircAppendPrefixes just also cuts prefixes.
|
|
var all []byte
|
|
if 0 != modes&ircChanModeOperator {
|
|
all = append(all, '@')
|
|
}
|
|
if 0 != modes&ircChanModeVoice {
|
|
all = append(all, '+')
|
|
}
|
|
chans = append(chans, string(all)+ch.name)
|
|
}
|
|
}
|
|
c.sendReplyVector(RPL_WHOISCHANNELS, chans, nick, "")
|
|
c.sendReply(RPL_ENDOFWHOIS, nick)
|
|
}
|
|
|
|
func ircHandleWHOIS(msg *message, c *client) {
|
|
if len(msg.params) < 1 {
|
|
c.sendReply(ERR_NEEDMOREPARAMS, msg.command)
|
|
return
|
|
}
|
|
if len(msg.params) > 1 && !ircIsThisMe(msg.params[0]) {
|
|
c.sendReply(ERR_NOSUCHSERVER, msg.params[0])
|
|
return
|
|
}
|
|
|
|
masksStr := msg.params[0]
|
|
if len(msg.params) > 1 {
|
|
masksStr = msg.params[1]
|
|
}
|
|
|
|
for _, mask := range splitString(masksStr, ",", true /* ignoreEmpty */) {
|
|
if strings.IndexAny(mask, "*?") < 0 {
|
|
if target := users[ircToCanon(mask)]; target == nil {
|
|
c.sendReply(ERR_NOSUCHNICK, mask)
|
|
} else {
|
|
ircSendWHOISReply(c, target)
|
|
}
|
|
} else {
|
|
found := false
|
|
for _, target := range users {
|
|
if ircFnmatch(mask, target.nickname) {
|
|
ircSendWHOISReply(c, target)
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
c.sendReply(ERR_NOSUCHNICK, mask)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func ircHandleWHOWAS(msg *message, c *client) {
|
|
if len(msg.params) < 1 {
|
|
c.sendReply(ERR_NEEDMOREPARAMS, msg.command)
|
|
return
|
|
}
|
|
if len(msg.params) > 2 && !ircIsThisMe(msg.params[2]) {
|
|
c.sendReply(ERR_NOSUCHSERVER, msg.params[2])
|
|
return
|
|
}
|
|
// The "count" parameter is ignored, we only store one entry for a nick.
|
|
|
|
for _, nick := range splitString(msg.params[0], ",", true) {
|
|
if info := whowas[ircToCanon(nick)]; info == nil {
|
|
c.sendReply(ERR_WASNOSUCHNICK, nick)
|
|
} else {
|
|
c.sendReply(RPL_WHOWASUSER, nick,
|
|
info.username, info.hostname, info.realname)
|
|
c.sendReply(RPL_WHOISSERVER, nick,
|
|
serverName, config["server_info"])
|
|
}
|
|
c.sendReply(RPL_ENDOFWHOWAS, nick)
|
|
}
|
|
}
|
|
|
|
func ircSendRPLTOPIC(c *client, ch *channel) {
|
|
if ch.topic == "" {
|
|
c.sendReply(RPL_NOTOPIC, ch.name)
|
|
} else {
|
|
c.sendReply(RPL_TOPIC, ch.name, ch.topic)
|
|
c.sendReply(RPL_TOPICWHOTIME,
|
|
ch.name, ch.topicWho, ch.topicTime.Unix())
|
|
}
|
|
}
|
|
|
|
func ircHandleTOPIC(msg *message, c *client) {
|
|
if len(msg.params) < 1 {
|
|
c.sendReply(ERR_NEEDMOREPARAMS, msg.command)
|
|
return
|
|
}
|
|
|
|
channelName := msg.params[0]
|
|
ch := channels[ircToCanon(channelName)]
|
|
if ch == nil {
|
|
c.sendReply(ERR_NOSUCHCHANNEL, channelName)
|
|
return
|
|
}
|
|
|
|
if len(msg.params) < 2 {
|
|
ircSendRPLTOPIC(c, ch)
|
|
return
|
|
}
|
|
|
|
modes, present := ch.userModes[c]
|
|
if !present {
|
|
c.sendReply(ERR_NOTONCHANNEL, channelName)
|
|
return
|
|
}
|
|
|
|
if 0 != ch.modes&ircChanModeProtectedTopic &&
|
|
0 == modes&ircChanModeOperator {
|
|
c.sendReply(ERR_CHANOPRIVSNEEDED, channelName)
|
|
return
|
|
}
|
|
|
|
ch.topic = msg.params[1]
|
|
ch.topicWho = fmt.Sprintf("%s@%s@%s", c.nickname, c.username, c.hostname)
|
|
ch.topicTime = time.Now()
|
|
|
|
message := fmt.Sprintf(":%s!%s@%s TOPIC %s :%s",
|
|
c.nickname, c.username, c.hostname, channelName, ch.topic)
|
|
ircChannelMulticast(ch, message, nil)
|
|
}
|
|
|
|
func ircTryPart(c *client, channelName string, reason string) {
|
|
if reason == "" {
|
|
reason = c.nickname
|
|
}
|
|
|
|
ch := channels[ircToCanon(channelName)]
|
|
if ch == nil {
|
|
c.sendReply(ERR_NOSUCHCHANNEL, channelName)
|
|
return
|
|
}
|
|
|
|
if _, present := ch.userModes[c]; !present {
|
|
c.sendReply(ERR_NOTONCHANNEL, channelName)
|
|
return
|
|
}
|
|
|
|
message := fmt.Sprintf(":%s@%s@%s PART %s :%s",
|
|
c.nickname, c.username, c.hostname, channelName, reason)
|
|
if 0 == ch.modes&ircChanModeQuiet {
|
|
ircChannelMulticast(ch, message, nil)
|
|
} else {
|
|
c.send(message)
|
|
}
|
|
|
|
delete(ch.userModes, c)
|
|
ircChannelDestroyIfEmpty(ch)
|
|
}
|
|
|
|
func ircPartAllChannels(c *client) {
|
|
for _, ch := range channels {
|
|
if _, present := ch.userModes[c]; present {
|
|
ircTryPart(c, ch.name, "")
|
|
}
|
|
}
|
|
}
|
|
|
|
func ircHandlePART(msg *message, c *client) {
|
|
if len(msg.params) < 1 {
|
|
c.sendReply(ERR_NEEDMOREPARAMS, msg.command)
|
|
return
|
|
}
|
|
|
|
reason := ""
|
|
if len(msg.params) > 1 {
|
|
reason = msg.params[1]
|
|
}
|
|
|
|
for _, channelName := range splitString(msg.params[0], ",", true) {
|
|
ircTryPart(c, channelName, reason)
|
|
}
|
|
}
|
|
|
|
func ircTryKick(c *client, channelName, nick, reason string) {
|
|
ch := channels[ircToCanon(channelName)]
|
|
if ch == nil {
|
|
c.sendReply(ERR_NOSUCHCHANNEL, channelName)
|
|
return
|
|
}
|
|
|
|
if modes, present := ch.userModes[c]; !present {
|
|
c.sendReply(ERR_NOTONCHANNEL, channelName)
|
|
return
|
|
} else if 0 == modes&ircChanModeOperator {
|
|
c.sendReply(ERR_CHANOPRIVSNEEDED, channelName)
|
|
return
|
|
}
|
|
|
|
client := users[ircToCanon(nick)]
|
|
if _, present := ch.userModes[client]; client == nil || !present {
|
|
c.sendReply(ERR_USERNOTINCHANNEL, nick, channelName)
|
|
return
|
|
}
|
|
|
|
message := fmt.Sprintf(":%s@%s@%s KICK %s %s :%s",
|
|
c.nickname, c.username, c.hostname, channelName, nick, reason)
|
|
if 0 == ch.modes&ircChanModeQuiet {
|
|
ircChannelMulticast(ch, message, nil)
|
|
} else {
|
|
c.send(message)
|
|
}
|
|
|
|
delete(ch.userModes, client)
|
|
ircChannelDestroyIfEmpty(ch)
|
|
}
|
|
|
|
func ircHandleKICK(msg *message, c *client) {
|
|
if len(msg.params) < 2 {
|
|
c.sendReply(ERR_NEEDMOREPARAMS, msg.command)
|
|
return
|
|
}
|
|
|
|
reason := c.nickname
|
|
if len(msg.params) > 2 {
|
|
reason = msg.params[2]
|
|
}
|
|
|
|
targetChannels := splitString(msg.params[0], ",", true)
|
|
targetUsers := splitString(msg.params[1], ",", true)
|
|
|
|
if len(channels) == 1 {
|
|
for i := 0; i < len(targetUsers); i++ {
|
|
ircTryKick(c, targetChannels[0], targetUsers[i], reason)
|
|
}
|
|
} else {
|
|
for i := 0; i < len(channels) && i < len(targetUsers); i++ {
|
|
ircTryKick(c, targetChannels[i], targetUsers[i], reason)
|
|
}
|
|
}
|
|
}
|
|
|
|
func ircSendInviteNotifications(ch *channel, c, target *client) {
|
|
for client := range ch.userModes {
|
|
if client != target && 0 != client.capsEnabled&ircCapInviteNotify {
|
|
client.sendf(":%s!%s@%s INVITE %s %s",
|
|
c.nickname, c.username, c.hostname, target.nickname, ch.name)
|
|
}
|
|
}
|
|
}
|
|
|
|
func ircHandleINVITE(msg *message, c *client) {
|
|
if len(msg.params) < 2 {
|
|
c.sendReply(ERR_NEEDMOREPARAMS, msg.command)
|
|
return
|
|
}
|
|
|
|
target, channelName := msg.params[0], msg.params[1]
|
|
client := users[ircToCanon(target)]
|
|
if client == nil {
|
|
c.sendReply(ERR_NOSUCHNICK, target)
|
|
return
|
|
}
|
|
|
|
if ch := channels[ircToCanon(channelName)]; ch != nil {
|
|
invitingModes, invitingPresent := ch.userModes[c]
|
|
if !invitingPresent {
|
|
c.sendReply(ERR_NOTONCHANNEL, channelName)
|
|
return
|
|
}
|
|
if _, present := ch.userModes[client]; present {
|
|
c.sendReply(ERR_USERONCHANNEL, target, channelName)
|
|
return
|
|
}
|
|
|
|
if 0 != invitingModes&ircChanModeOperator {
|
|
client.invites[ircToCanon(channelName)] = true
|
|
} else if 0 != ch.modes&ircChanModeInviteOnly {
|
|
c.sendReply(ERR_CHANOPRIVSNEEDED, channelName)
|
|
return
|
|
}
|
|
|
|
// It's not specified when and how we should send out invite-notify.
|
|
if 0 != ch.modes&ircChanModeInviteOnly {
|
|
ircSendInviteNotifications(ch, c, client)
|
|
}
|
|
}
|
|
|
|
client.sendf(":%s!%s@%s INVITE %s %s",
|
|
c.nickname, c.username, c.hostname, client.nickname, channelName)
|
|
if client.awayMessage != "" {
|
|
c.sendReply(RPL_AWAY, client.nickname, client.awayMessage)
|
|
}
|
|
c.sendReply(RPL_INVITING, client.nickname, channelName)
|
|
}
|
|
|
|
func ircTryJoin(c *client, channelName, key string) {
|
|
ch := channels[ircToCanon(channelName)]
|
|
var userMode uint
|
|
if ch == nil {
|
|
if !ircIsValidChannelName(channelName) {
|
|
c.sendReply(ERR_BADCHANMASK, channelName)
|
|
return
|
|
}
|
|
ch = ircChannelCreate(channelName)
|
|
userMode = ircChanModeOperator
|
|
} else if _, present := ch.userModes[c]; present {
|
|
return
|
|
}
|
|
|
|
_, invitedByChanop := c.invites[ircToCanon(channelName)]
|
|
if 0 != ch.modes&ircChanModeInviteOnly && c.inMaskList(ch.inviteList) &&
|
|
!invitedByChanop {
|
|
c.sendReply(ERR_INVITEONLYCHAN, channelName)
|
|
return
|
|
}
|
|
if ch.key != "" && (key == "" || key != ch.key) {
|
|
c.sendReply(ERR_BADCHANNELKEY, channelName)
|
|
return
|
|
}
|
|
if ch.userLimit != -1 && len(ch.userModes) >= ch.userLimit {
|
|
c.sendReply(ERR_CHANNELISFULL, channelName)
|
|
return
|
|
}
|
|
if c.inMaskList(ch.banList) && !c.inMaskList(ch.exceptionList) &&
|
|
!invitedByChanop {
|
|
c.sendReply(ERR_BANNEDFROMCHAN, channelName)
|
|
return
|
|
}
|
|
|
|
// Destroy any invitation as there's no other way to get rid of it.
|
|
delete(c.invites, ircToCanon(channelName))
|
|
|
|
ch.userModes[c] = userMode
|
|
|
|
message := fmt.Sprintf(":%s!%s@%s JOIN %s",
|
|
c.nickname, c.username, c.hostname, channelName)
|
|
if 0 == ch.modes&ircChanModeQuiet {
|
|
ircChannelMulticast(ch, message, nil)
|
|
} else {
|
|
c.send(message)
|
|
}
|
|
|
|
ircSendRPLTOPIC(c, ch)
|
|
ircSendRPLNAMREPLY(c, ch, nil)
|
|
c.sendReply(RPL_ENDOFNAMES, ch.name)
|
|
}
|
|
|
|
func ircHandleJOIN(msg *message, c *client) {
|
|
if len(msg.params) < 1 {
|
|
c.sendReply(ERR_NEEDMOREPARAMS, msg.command)
|
|
return
|
|
}
|
|
|
|
if msg.params[0] == "0" {
|
|
ircPartAllChannels(c)
|
|
return
|
|
}
|
|
|
|
targetChannels := splitString(msg.params[0], ",", true)
|
|
|
|
var keys []string
|
|
if len(msg.params) > 1 {
|
|
keys = splitString(msg.params[1], ",", true)
|
|
}
|
|
|
|
for i, name := range targetChannels {
|
|
key := ""
|
|
if i < len(keys) {
|
|
key = keys[i]
|
|
}
|
|
ircTryJoin(c, name, key)
|
|
}
|
|
}
|
|
|
|
func ircHandleSUMMON(msg *message, c *client) {
|
|
c.sendReply(ERR_SUMMONDISABLED)
|
|
}
|
|
|
|
func ircHandleUSERS(msg *message, c *client) {
|
|
c.sendReply(ERR_USERSDISABLED)
|
|
}
|
|
|
|
func ircHandleAWAY(msg *message, c *client) {
|
|
if len(msg.params) < 1 {
|
|
c.awayMessage = ""
|
|
c.sendReply(RPL_UNAWAY)
|
|
} else {
|
|
c.awayMessage = msg.params[0]
|
|
c.sendReply(RPL_NOWAWAY)
|
|
}
|
|
}
|
|
|
|
func ircHandleISON(msg *message, c *client) {
|
|
if len(msg.params) < 1 {
|
|
c.sendReply(ERR_NEEDMOREPARAMS, msg.command)
|
|
return
|
|
}
|
|
|
|
var on []string
|
|
for _, nick := range msg.params {
|
|
if client := users[ircToCanon(nick)]; client != nil {
|
|
on = append(on, nick)
|
|
}
|
|
}
|
|
c.sendReply(RPL_ISON, strings.Join(on, " "))
|
|
}
|
|
|
|
func ircHandleADMIN(msg *message, c *client) {
|
|
if len(msg.params) > 0 && !ircIsThisMe(msg.params[0]) {
|
|
c.sendReply(ERR_NOSUCHSERVER, msg.params[0])
|
|
return
|
|
}
|
|
c.sendReply(ERR_NOADMININFO, serverName)
|
|
}
|
|
|
|
func ircHandleStatsLinks(c *client, msg *message) {
|
|
// There is only an "l" query in RFC 2812 but we cannot link,
|
|
// so instead we provide the "L" query giving information for all users.
|
|
filter := ""
|
|
if len(msg.params) > 1 {
|
|
filter = msg.params[1]
|
|
}
|
|
|
|
for _, client := range users {
|
|
if filter != "" && !ircEqual(client.nickname, filter) {
|
|
continue
|
|
}
|
|
c.sendReply(RPL_STATSLINKINFO,
|
|
client.address, // linkname
|
|
len(client.sendQ), // sendq
|
|
client.nSentMessages, client.sentBytes/1024,
|
|
client.nReceivedMessages, client.receivedBytes/1024,
|
|
time.Now().Sub(client.opened)/time.Second)
|
|
}
|
|
}
|
|
|
|
func ircHandleStatsCommands(c *client) {
|
|
for name, handler := range ircHandlers {
|
|
if handler.nReceived > 0 {
|
|
c.sendReply(RPL_STATSCOMMANDS, name,
|
|
handler.nReceived, handler.bytesReceived, 0)
|
|
}
|
|
}
|
|
}
|
|
|
|
// We need to do it this way because of an initialization loop concerning
|
|
// ircHandlers. Workaround proposed by rsc in go #1817.
|
|
var ircHandleStatsCommandsIndirect func(c *client)
|
|
|
|
func init() {
|
|
ircHandleStatsCommandsIndirect = ircHandleStatsCommands
|
|
}
|
|
|
|
func ircHandleStatsUptime(c *client) {
|
|
uptime := time.Now().Sub(started) / time.Second
|
|
|
|
days := uptime / 60 / 60 / 24
|
|
hours := (uptime % (60 * 60 * 24)) / 60 / 60
|
|
mins := (uptime % (60 * 60)) / 60
|
|
secs := uptime % 60
|
|
|
|
c.sendReply(RPL_STATSUPTIME, days, hours, mins, secs)
|
|
}
|
|
|
|
func ircHandleSTATS(msg *message, c *client) {
|
|
var query byte
|
|
if len(msg.params) > 0 && len(msg.params[0]) > 0 {
|
|
query = msg.params[0][0]
|
|
}
|
|
|
|
if len(msg.params) > 1 && !ircIsThisMe(msg.params[1]) {
|
|
c.sendReply(ERR_NOSUCHSERVER, msg.params[0])
|
|
return
|
|
}
|
|
if 0 == c.mode&ircUserModeOperator {
|
|
c.sendReply(ERR_NOPRIVILEGES)
|
|
return
|
|
}
|
|
|
|
switch query {
|
|
case 'L':
|
|
ircHandleStatsLinks(c, msg)
|
|
case 'm':
|
|
ircHandleStatsCommandsIndirect(c)
|
|
case 'u':
|
|
ircHandleStatsUptime(c)
|
|
}
|
|
c.sendReply(RPL_ENDOFSTATS, query)
|
|
}
|
|
|
|
func ircHandleLINKS(msg *message, c *client) {
|
|
if len(msg.params) > 1 && !ircIsThisMe(msg.params[0]) {
|
|
c.sendReply(ERR_NEEDMOREPARAMS, msg.command)
|
|
return
|
|
}
|
|
|
|
mask := "*"
|
|
if len(msg.params) > 0 {
|
|
if len(msg.params) > 1 {
|
|