xK/xS/xS.go
Přemysl Eric Janouch 6f596f1dcb
Move project version to file, add xS manual page
So far Go applications remain independent to handle Nix's inability
to easily combine them with the CMake part.

There is also no "install" target, because any packagers will want to
adjust installation paths manually, and there is no configure step.
2023-07-04 23:26:05 +02:00

3526 lines
86 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 protocol ------------------------------------------------------------
func ircToLower(c byte) byte {
switch c {
case '[':
return '{'
case ']':
return '}'
case '\\':
return '|'
case '~':
return '^'
}
if c >= 'A' && c <= 'Z' {
return c + ('a' - 'A')
}
return c
}
func ircToUpper(c byte) byte {
switch c {
case '{':
return '['
case '}':
return ']'
case '|':
return '\\'
case '^':
return '~'
}
if c >= 'a' && c <= 'z' {
return c - ('a' - 'A')
}
return c
}
// Convert identifier to a canonical form for case-insensitive comparisons.
// ircToUpper is used so that statically initialized maps can be in uppercase.
func ircToCanon(ident string) string {
var canon []byte
for _, c := range []byte(ident) {
canon = append(canon, ircToUpper(c))
}
return string(canon)
}
func ircEqual(s1, s2 string) bool {
return ircToCanon(s1) == ircToCanon(s2)
}
func ircFnmatch(pattern string, s string) bool {
pattern, s = ircToCanon(pattern), ircToCanon(s)
// FIXME: This should not support [] ranges and handle '/' specially.
// We could translate the pattern to a regular expression.
matched, _ := filepath.Match(pattern, s)
return matched
}
var reMsg = regexp.MustCompile(
`^(?:@([^ ]*) +)?(?::([^! ]*)(?:!([^@]*)@([^ ]*))? +)?([^ ]+)(.*)?$`)
var reArgs = regexp.MustCompile(`:.*| [^: ][^ ]*`)
type message struct {
tags map[string]string // IRC 3.2 message tags
nick string // optional nickname
user string // optional username
host string // optional hostname or IP address
command string // command name
params []string // arguments
}
func ircUnescapeMessageTag(value string) string {
var buf []byte
escape := false
for i := 0; i < len(value); i++ {
if escape {
switch value[i] {
case ':':
buf = append(buf, ';')
case 's':
buf = append(buf, ' ')
case 'r':
buf = append(buf, '\r')
case 'n':
buf = append(buf, '\n')
default:
buf = append(buf, value[i])
}
escape = false
} else if value[i] == '\\' {
escape = true
} else {
buf = append(buf, value[i])
}
}
return string(buf)
}
func ircParseMessageTags(tags string, out map[string]string) {
for _, tag := range splitString(tags, ";", true /* ignoreEmpty */) {
if equal := strings.IndexByte(tag, '='); equal < 0 {
out[tag] = ""
} else {
out[tag[:equal]] = ircUnescapeMessageTag(tag[equal+1:])
}
}
}
func ircParseMessage(line string) *message {
m := reMsg.FindStringSubmatch(line)
if m == nil {
return nil
}
msg := message{nil, m[2], m[3], m[4], m[5], nil}
if m[1] != "" {
msg.tags = make(map[string]string)
ircParseMessageTags(m[1], msg.tags)
}
for _, x := range reArgs.FindAllString(m[6], -1) {
msg.params = append(msg.params, x[1:])
}
return &msg
}
// --- 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 {
mask = msg.params[1]
} else {
mask = msg.params[0]
}
}
if ircFnmatch(mask, serverName) {
c.sendReply(RPL_LINKS, mask, serverName,
0 /* hop count */, config["server_info"])
}
c.sendReply(RPL_ENDOFLINKS, mask)
}
func ircHandleWALLOPS(msg *message, c *client) {
if len(msg.params) < 1 {
c.sendReply(ERR_NEEDMOREPARAMS, msg.command)
return
}
if 0 == c.mode&ircUserModeOperator {
c.sendReply(ERR_NOPRIVILEGES)
return
}
// Our interpretation: anonymize the sender,
// and target all users who want to receive these messages.
for _, client := range users {
if client == c || 0 != client.mode&ircUserModeRxWallops {
client.sendf(":%s WALLOPS :%s", serverName, msg.params[0])
}
}
}
func ircHandleKILL(msg *message, c *client) {
if len(msg.params) < 2 {
c.sendReply(ERR_NEEDMOREPARAMS, msg.command)
return
}
if 0 == c.mode&ircUserModeOperator {
c.sendReply(ERR_NOPRIVILEGES)
return
}
target := users[ircToCanon(msg.params[0])]
if target == nil {
c.sendReply(ERR_NOSUCHNICK, msg.params[0])
return
}
c.sendf(":%s!%s@%s KILL %s :%s",
c.nickname, c.username, c.hostname, target.nickname, msg.params[1])
target.closeLink(fmt.Sprintf("Killed by %s: %s", c.nickname, msg.params[1]))
}
func ircHandleDIE(msg *message, c *client) {
if 0 == c.mode&ircUserModeOperator {
c.sendReply(ERR_NOPRIVILEGES)
} else if !quitting {
initiateQuit()
}
}
// -----------------------------------------------------------------------------
// TODO: Add an index for ERR_NOSUCHSERVER validation?
// TODO: Add a minimal parameter count?
// TODO: Add a field for oper-only commands? Use flags?
var ircHandlers = map[string]*ircCommand{
"WEBIRC": {false, ircHandleWEBIRC, 0, 0},
"CAP": {false, ircHandleCAP, 0, 0},
"PASS": {false, ircHandlePASS, 0, 0},
"NICK": {false, ircHandleNICK, 0, 0},
"USER": {false, ircHandleUSER, 0, 0},
"USERHOST": {true, ircHandleUSERHOST, 0, 0},
"LUSERS": {true, ircHandleLUSERS, 0, 0},
"MOTD": {true, ircHandleMOTD, 0, 0},
"PING": {true, ircHandlePING, 0, 0},
"PONG": {false, ircHandlePONG, 0, 0},
"QUIT": {false, ircHandleQUIT, 0, 0},
"TIME": {true, ircHandleTIME, 0, 0},
"VERSION": {true, ircHandleVERSION, 0, 0},
"USERS": {true, ircHandleUSERS, 0, 0},
"SUMMON": {true, ircHandleSUMMON, 0, 0},
"AWAY": {true, ircHandleAWAY, 0, 0},
"ADMIN": {true, ircHandleADMIN, 0, 0},
"STATS": {true, ircHandleSTATS, 0, 0},
"LINKS": {true, ircHandleLINKS, 0, 0},
"WALLOPS": {true, ircHandleWALLOPS, 0, 0},
"MODE": {true, ircHandleMODE, 0, 0},
"PRIVMSG": {true, ircHandlePRIVMSG, 0, 0},
"NOTICE": {true, ircHandleNOTICE, 0, 0},
"JOIN": {true, ircHandleJOIN, 0, 0},
"PART": {true, ircHandlePART, 0, 0},
"KICK": {true, ircHandleKICK, 0, 0},
"INVITE": {true, ircHandleINVITE, 0, 0},
"TOPIC": {true, ircHandleTOPIC, 0, 0},
"LIST": {true, ircHandleLIST, 0, 0},
"NAMES": {true, ircHandleNAMES, 0, 0},
"WHO": {true, ircHandleWHO, 0, 0},
"WHOIS": {true, ircHandleWHOIS, 0, 0},
"WHOWAS": {true, ircHandleWHOWAS, 0, 0},
"ISON": {true, ircHandleISON, 0, 0},
"KILL": {true, ircHandleKILL, 0, 0},
"DIE": {true, ircHandleDIE, 0, 0},
}
func ircProcessMessage(c *client, msg *message, raw string) {
if c.closing {
return
}
c.nReceivedMessages++
c.receivedBytes += len(raw) + 2
if !c.antiflood.check() {
c.closeLink("Excess flood")
return
}
if cmd, ok := ircHandlers[ircToCanon(msg.command)]; !ok {
c.sendReply(ERR_UNKNOWNCOMMAND, msg.command)
} else {
cmd.nReceived++
cmd.bytesReceived += len(raw) + 2
if cmd.requiresRegistration && !c.registered {
c.sendReply(ERR_NOTREGISTERED)
} else {
cmd.handler(msg, c)
}
}
}
// --- Network I/O -------------------------------------------------------------
// Handle the results from initializing the client's connection.
func (c *client) onPrepared(host string, isTLS bool) {
c.printDebug("client resolved to %s, TLS %t", host, isTLS)
if !isTLS {
c.conn = c.transport.(connCloseWriter)
} else if tlsConf != nil {
c.tls = tls.Server(c.transport, tlsConf)
c.conn = c.tls
} else {
c.printDebug("could not initialize TLS: disabled")
c.kill("TLS support disabled")
return
}
c.hostname = host
c.address = net.JoinHostPort(host, c.port)
// If we tried to send any data before now, we would need to flushSendQ.
go read(c)
c.reading = true
c.setPingTimer()
}
// Handle the results from trying to read from the client connection.
func (c *client) onRead(data []byte, readErr error) {
if !c.reading {
// Abusing the flag to emulate CloseRead and skip over data, see below.
return
}
c.recvQ = append(c.recvQ, data...)
for {
// XXX: This accepts even simple LF newlines, even though they're not
// really allowed by the protocol.
advance, token, _ := bufio.ScanLines(c.recvQ, false /* atEOF */)
if advance == 0 {
break
}
// XXX: And since it accepts LF, we miscalculate receivedBytes within.
c.recvQ = c.recvQ[advance:]
line := string(token)
c.printDebug("-> %s", line)
if msg := ircParseMessage(line); msg == nil {
c.printDebug("error: invalid line")
} else {
ircProcessMessage(c, msg, line)
}
}
if readErr != nil {
c.reading = false
if readErr != io.EOF {
c.printDebug("%s", readErr)
c.kill(readErr.Error())
} else if c.closing {
// Disregarding whether a clean shutdown has happened or not.
c.printDebug("client finished shutdown")
c.kill("")
} else {
c.printDebug("client EOF")
c.closeLink("")
}
} else if len(c.recvQ) > 8192 {
c.closeLink("recvQ overrun")
// tls.Conn doesn't have the CloseRead method (and it needs to be able
// to read from the TCP connection even for writes, so there isn't much
// sense in expecting the implementation to do anything useful),
// otherwise we'd use it to block incoming packet data.
c.reading = false
}
}
// Spawn a goroutine to flush the sendQ if possible and necessary.
func (c *client) flushSendQ() {
if !c.writing && c.conn != nil {
go write(c, c.sendQ)
c.writing = true
}
}
// Handle the results from trying to write to the client connection.
func (c *client) onWrite(written int, writeErr error) {
c.sendQ = c.sendQ[written:]
c.writing = false
if writeErr != nil {
c.printDebug("%s", writeErr)
c.kill(writeErr.Error())
} else if len(c.sendQ) > 0 {
c.flushSendQ()
} else if c.closing {
if c.reading {
c.conn.CloseWrite()
} else {
c.kill("")
}
}
}
// --- Worker goroutines -------------------------------------------------------
func accept(ln net.Listener) {
for {
// Error handling here may be tricky, see go #6163, #24808.
if conn, err := ln.Accept(); err != nil {
// See go #4373, they're being dicks. Another solution would be to
// pass a done channel to this function and close it before closing
// all the listeners, returning from here if it's readable.
if strings.Contains(err.Error(),
"use of closed network connection") {
return
}
// XXX: net.Error.Temporary() has been deprecated in 1.18.
if op, ok := err.(net.Error); !ok || !op.Temporary() {
exitFatal("%s", err)
} else {
printError("%s", err)
}
} else {
// TCP_NODELAY is set by default on TCPConns.
conns <- conn
}
}
}
func prepare(client *client) {
conn, host := client.transport, client.hostname
// The Cgo resolver doesn't pthread_cancel getnameinfo threads, so not
// bothering with pointless contexts.
ch := make(chan string, 1)
go func() {
defer close(ch)
if names, err := net.LookupAddr(host); err != nil {
printError("%s", err)
} else {
ch <- names[0]
}
}()
// While we can't cancel it, we still want to set a timeout on it.
select {
case <-time.After(5 * time.Second):
case resolved, ok := <-ch:
if ok {
host = resolved
}
}
// Note that in this demo application the autodetection prevents non-TLS
// clients from receiving any messages until they send something.
isTLS := false
if sysconn, err := conn.(syscall.Conn).SyscallConn(); err != nil {
// This is just for the TLS detection and doesn't need to be fatal.
printError("%s", err)
} else {
isTLS = detectTLS(sysconn)
}
prepared <- preparedEvent{client, host, isTLS}
}
func read(client *client) {
// A new buffer is allocated each time we receive some bytes, because of
// thread-safety. Therefore the buffer shouldn't be too large, or we'd
// need to copy it each time into a precisely sized new buffer.
var err error
for err == nil {
var (
buf [512]byte
n int
)
n, err = client.conn.Read(buf[:])
reads <- readEvent{client, buf[:n], err}
}
}
// Flush sendQ, which is passed by parameter so that there are no data races.
func write(client *client, data []byte) {
// We just write as much as we can, the main goroutine does the looping.
n, err := client.conn.Write(data)
writes <- writeEvent{client, n, err}
}
// --- Event loop --------------------------------------------------------------
func processOneEvent() {
select {
case <-sigs:
if quitting {
forceQuit("requested by user")
} else {
initiateQuit()
}
case <-quitTimer:
forceQuit("timeout")
case callback := <-timers:
callback()
case conn := <-conns:
if maxConnections > 0 && len(clients) >= maxConnections {
printDebug("connection limit reached, refusing connection")
conn.Close()
break
}
address := conn.RemoteAddr().String()
host, port, err := net.SplitHostPort(address)
if err != nil {
// In effect, we require TCP/UDP, as they have port numbers.
exitFatal("%s", err)
}
c := &client{
transport: conn,
address: address,
hostname: host,
port: port,
capVersion: 301,
opened: time.Now(),
lastActive: time.Now(),
// TODO: Make this configurable and more fine-grained.
antiflood: newFloodDetector(10*time.Second, 20),
}
clients[c] = true
c.printDebug("new client")
go prepare(c)
// The TLS autodetection in prepare needs to have a timeout.
c.setKillTimer()
case ev := <-prepared:
if _, ok := clients[ev.client]; ok {
ev.client.onPrepared(ev.host, ev.isTLS)
}
case ev := <-reads:
if _, ok := clients[ev.client]; ok {
ev.client.onRead(ev.data, ev.err)
}
case ev := <-writes:
if _, ok := clients[ev.client]; ok {
ev.client.onWrite(ev.written, ev.err)
}
}
}
// --- Application setup -------------------------------------------------------
func ircInitializeTLS() error {
configCert, configKey := config["tls_cert"], config["tls_key"]
// Only try to enable SSL support if the user configures it; it is not
// a failure if no one has requested it.
if configCert == "" && configKey == "" {
return nil
} else if configCert == "" {
return errors.New("no TLS certificate set")
} else if configKey == "" {
return errors.New("no TLS private key set")
}
pathCert := resolveFilename(configCert, resolveRelativeConfigFilename)
if pathCert == "" {
return fmt.Errorf("cannot find file: %s", configCert)
}
pathKey := resolveFilename(configKey, resolveRelativeConfigFilename)
if pathKey == "" {
return fmt.Errorf("cannot find file: %s", configKey)
}
cert, err := tls.LoadX509KeyPair(pathCert, pathKey)
if err != nil {
return err
}
tlsConf = &tls.Config{
Certificates: []tls.Certificate{cert},
ClientAuth: tls.RequestClientCert,
SessionTicketsDisabled: true,
}
return nil
}
func ircInitializeCatalog() error {
configCatalog := config["catalog"]
if configCatalog == "" {
return nil
}
path := resolveFilename(configCatalog, resolveRelativeConfigFilename)
if path == "" {
return fmt.Errorf("cannot find file: %s", configCatalog)
}
f, err := os.Open(path)
if err != nil {
return fmt.Errorf("failed reading the MOTD file: %s", err)
}
defer f.Close()
scanner := bufio.NewScanner(f)
catalog = make(map[int]string)
for lineNo := 1; scanner.Scan(); lineNo++ {
line := strings.TrimLeft(scanner.Text(), " \t")
if line == "" || strings.HasPrefix(line, "#") {
continue
}
delim := strings.IndexAny(line, " \t")
if delim < 0 {
return fmt.Errorf("%s:%d: malformed line", path, lineNo)
}
id, err := strconv.ParseUint(line[:delim], 10, 16)
if err != nil {
return fmt.Errorf("%s:%d: %s", path, lineNo, err)
}
catalog[int(id)] = line[delim+1:]
}
return scanner.Err()
}
func ircInitializeMOTD() error {
configMOTD := config["motd"]
if configMOTD == "" {
return nil
}
path := resolveFilename(configMOTD, resolveRelativeConfigFilename)
if path == "" {
return fmt.Errorf("cannot find file: %s", configMOTD)
}
f, err := os.Open(path)
if err != nil {
return fmt.Errorf("failed reading the MOTD file: %s", err)
}
defer f.Close()
scanner := bufio.NewScanner(f)
motd = nil
for scanner.Scan() {
motd = append(motd, scanner.Text())
}
return scanner.Err()
}
type configProcessor struct {
err error // any error that has occurred so far
}
func (cp *configProcessor) read(name string, process func(string) string) {
if cp.err != nil {
return
}
if err := process(config[name]); err != "" {
cp.err = fmt.Errorf("invalid configuration value for `%s': %s",
name, err)
}
}
// This function handles values that require validation before their first use,
// or some kind of a transformation (such as conversion to an integer) needs
// to be done before they can be used directly.
func ircParseConfig() error {
cp := &configProcessor{}
cp.read("ping_interval", func(value string) string {
if u, err := strconv.ParseUint(
config["ping_interval"], 10, 32); err != nil {
return err.Error()
} else if u < 1 {
return "the value is out of range"
} else {
pingInterval = time.Second * time.Duration(u)
}
return ""
})
cp.read("max_connections", func(value string) string {
if i, err := strconv.ParseInt(
value, 10, 32); err != nil {
return err.Error()
} else if i < 0 {
return "the value is out of range"
} else {
maxConnections = int(i)
}
return ""
})
cp.read("operators", func(value string) string {
operators = make(map[string]bool)
for _, fp := range splitString(value, ",", true) {
if !ircIsValidFingerprint(fp) {
return "invalid fingerprint value"
}
operators[strings.ToLower(fp)] = true
}
return ""
})
return cp.err
}
func ircInitializeServerName() error {
if value := config["server_name"]; value != "" {
if err := ircValidateHostname(value); err != nil {
return err
}
serverName = value
return nil
}
if hostname, err := os.Hostname(); err != nil {
return err
} else if err := ircValidateHostname(hostname); err != nil {
return err
} else {
serverName = hostname
}
return nil
}
func ircSetupListenFDs() error {
for _, address := range splitString(config["bind"], ",", true) {
ln, err := net.Listen("tcp", address)
if err != nil {
return err
}
listeners = append(listeners, ln)
printStatus("listening on %s", address)
}
if len(listeners) == 0 {
return errors.New("network setup failed: no ports to listen on")
}
for _, ln := range listeners {
go accept(ln)
}
return nil
}
// --- Main --------------------------------------------------------------------
func main() {
flag.BoolVar(&debugMode, "debug", false, "run in verbose debug mode")
version := flag.Bool("version", false, "show version and exit")
writeDefaultCfg := flag.Bool("writedefaultcfg", false,
"write a default configuration file and exit")
systemd := flag.Bool("systemd", false, "log in systemd format")
flag.Parse()
if *version {
fmt.Printf("%s %s\n", projectName, projectVersion)
return
}
if *writeDefaultCfg {
callSimpleConfigWriteDefault("", configTable)
return
}
if *systemd {
logMessage = logMessageSystemd
}
if flag.NArg() > 0 {
flag.Usage()
os.Exit(2)
}
// Note that this has become unnecessary since Go 1.19.
var limit syscall.Rlimit
if err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &limit); err == nil &&
limit.Cur != limit.Max {
limit.Cur = limit.Max
syscall.Setrlimit(syscall.RLIMIT_NOFILE, &limit)
}
config = make(simpleConfig)
config.loadDefaults(configTable)
if err := config.updateFromFile(); err != nil && !os.IsNotExist(err) {
printError("error loading configuration: %s", err)
os.Exit(1)
}
started = time.Now()
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
for _, fn := range []func() error{
ircInitializeTLS,
ircInitializeServerName,
ircInitializeMOTD,
ircInitializeCatalog,
ircParseConfig,
ircSetupListenFDs,
} {
if err := fn(); err != nil {
exitFatal("%s", err)
}
}
for !quitting || len(clients) > 0 {
processOneEvent()
}
}