Přemysl Eric Janouch
6f596f1dcb
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.
3526 lines
86 KiB
Go
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()
|
|
}
|
|
}
|