2018-07-28 16:21:34 +02:00
|
|
|
//
|
2022-03-15 18:58:27 +01:00
|
|
|
// Copyright (c) 2014 - 2022, Přemysl Eric Janouch <p@janouch.name>
|
2018-07-28 16:21:34 +02:00
|
|
|
//
|
|
|
|
// 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.
|
|
|
|
//
|
|
|
|
|
2022-09-26 12:23:58 +02:00
|
|
|
// xS is a straight-forward port of xD IRCd from C.
|
2018-07-28 16:21:34 +02:00
|
|
|
package main
|
|
|
|
|
2018-07-29 07:50:27 +02:00
|
|
|
import (
|
|
|
|
"bufio"
|
2018-07-30 09:42:01 +02:00
|
|
|
"bytes"
|
2018-07-29 07:50:27 +02:00
|
|
|
"crypto/sha256"
|
|
|
|
"crypto/tls"
|
|
|
|
"encoding/hex"
|
2018-07-31 20:53:23 +02:00
|
|
|
"errors"
|
2018-07-29 07:50:27 +02:00
|
|
|
"flag"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
2018-07-31 20:53:23 +02:00
|
|
|
"io/ioutil"
|
2018-08-06 14:12:45 +02:00
|
|
|
"log/syslog"
|
2018-07-29 07:50:27 +02:00
|
|
|
"net"
|
|
|
|
"os"
|
|
|
|
"os/signal"
|
|
|
|
"os/user"
|
|
|
|
"path/filepath"
|
|
|
|
"regexp"
|
|
|
|
"strconv"
|
|
|
|
"strings"
|
|
|
|
"syscall"
|
|
|
|
"time"
|
|
|
|
)
|
2018-07-28 16:21:34 +02:00
|
|
|
|
2022-09-27 23:39:53 +02:00
|
|
|
const projectName = "xS"
|
2018-07-28 16:21:34 +02:00
|
|
|
|
2023-07-03 08:53:20 +02:00
|
|
|
var projectVersion = "?"
|
|
|
|
|
2022-09-27 23:39:53 +02:00
|
|
|
var debugMode = false
|
2018-07-28 16:21:34 +02:00
|
|
|
|
2018-08-06 14:12:45 +02:00
|
|
|
// --- 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)
|
|
|
|
}
|
|
|
|
|
2018-07-29 07:50:27 +02:00
|
|
|
// --- Utilities ---------------------------------------------------------------
|
2018-07-28 16:21:34 +02:00
|
|
|
|
2018-07-29 07:50:27 +02:00
|
|
|
// 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
|
2018-07-28 16:21:34 +02:00
|
|
|
}
|
|
|
|
|
2018-07-31 20:53:23 +02:00
|
|
|
// 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.
|
|
|
|
//
|
2022-09-26 12:23:58 +02:00
|
|
|
// SSL2: 1xxx xxxx | xxxx xxxx | <1>
|
|
|
|
// (message length) (client hello)
|
|
|
|
// SSL3/TLS: <22> | <3> | xxxx xxxx
|
|
|
|
// (handshake)| (protocol version)
|
2018-07-31 20:53:23 +02:00
|
|
|
//
|
2019-02-27 02:36:04 +01:00
|
|
|
// 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."
|
2018-07-31 20:53:23 +02:00
|
|
|
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:
|
2018-08-04 21:13:28 +02:00
|
|
|
isTLS = isTLS || buf[0] == 22 && buf[1] == 3
|
2018-07-31 20:53:23 +02:00
|
|
|
case n == 1:
|
|
|
|
isTLS = buf[0] == 22
|
|
|
|
case err == syscall.EAGAIN:
|
|
|
|
return false
|
2018-07-29 07:50:27 +02:00
|
|
|
}
|
2018-07-31 20:53:23 +02:00
|
|
|
return true
|
|
|
|
})
|
|
|
|
return isTLS
|
2018-07-28 16:21:34 +02:00
|
|
|
}
|
|
|
|
|
2018-07-31 20:53:23 +02:00
|
|
|
// --- File system -------------------------------------------------------------
|
2018-07-28 16:21:34 +02:00
|
|
|
|
2018-07-31 20:53:23 +02:00
|
|
|
// Look up the value of an XDG path from environment, or fall back to a default.
|
2018-07-29 07:50:27 +02:00
|
|
|
func getXDGHomeDir(name, def string) string {
|
2018-07-28 16:21:34 +02:00
|
|
|
env := os.Getenv(name)
|
2018-07-31 20:53:23 +02:00
|
|
|
if env != "" && env[0] == filepath.Separator {
|
2018-07-28 16:21:34 +02:00
|
|
|
return env
|
|
|
|
}
|
2018-07-29 07:50:27 +02:00
|
|
|
|
|
|
|
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)
|
2018-07-28 16:21:34 +02:00
|
|
|
}
|
|
|
|
|
2018-07-31 20:53:23 +02:00
|
|
|
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 ""
|
|
|
|
}
|
|
|
|
|
2018-07-29 07:50:27 +02:00
|
|
|
// Retrieve all XDG base directories for configuration files.
|
|
|
|
func getXDGConfigDirs() (result []string) {
|
|
|
|
home := getXDGHomeDir("XDG_CONFIG_HOME", ".config")
|
2018-07-28 16:21:34 +02:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2018-07-31 20:53:23 +02:00
|
|
|
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
|
2018-07-28 16:21:34 +02:00
|
|
|
}
|
2018-07-31 20:53:23 +02:00
|
|
|
} else if u, _ := user.Current(); u != nil {
|
|
|
|
return u.HomeDir
|
|
|
|
} else if v, ok := os.LookupEnv("HOME"); ok {
|
|
|
|
return v
|
|
|
|
}
|
2018-08-06 19:50:53 +02:00
|
|
|
printDebug("failed to expand the home directory for %s", username)
|
2018-07-31 20:53:23 +02:00
|
|
|
return "~" + username
|
2018-07-28 16:21:34 +02:00
|
|
|
}
|
|
|
|
|
2018-07-31 20:53:23 +02:00
|
|
|
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.
|
2018-08-06 12:31:31 +02:00
|
|
|
func writeFile(path string, data []byte) error {
|
|
|
|
if dir := filepath.Dir(path); dir != "." {
|
2018-07-31 20:53:23 +02:00
|
|
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
2018-08-06 12:31:31 +02:00
|
|
|
return ioutil.WriteFile(path, data, 0644)
|
2018-07-31 20:53:23 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// Wrapper for writeFile that makes sure that the new data has been written
|
|
|
|
// to disk in its entirety before overriding the old file.
|
2018-08-06 12:31:31 +02:00
|
|
|
func writeFileSafe(path string, data []byte) error {
|
|
|
|
temp := path + ".new"
|
2018-07-31 20:53:23 +02:00
|
|
|
if err := writeFile(temp, data); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2018-08-06 12:31:31 +02:00
|
|
|
return os.Rename(temp, path)
|
2018-07-31 20:53:23 +02:00
|
|
|
}
|
2018-07-28 16:21:34 +02:00
|
|
|
|
2018-07-31 20:53:23 +02:00
|
|
|
// --- Simple configuration ----------------------------------------------------
|
|
|
|
|
2018-08-06 12:31:31 +02:00
|
|
|
// This is the bare minimum to make an application configurable.
|
|
|
|
// Keys are stripped of surrounding whitespace, values are not.
|
|
|
|
|
2018-07-31 20:53:23 +02:00
|
|
|
type simpleConfigItem struct {
|
2018-07-28 16:21:34 +02:00
|
|
|
key string // INI key
|
2018-07-31 20:53:23 +02:00
|
|
|
def string // default value
|
2018-07-28 16:21:34 +02:00
|
|
|
description string // documentation
|
|
|
|
}
|
|
|
|
|
2018-07-31 20:53:23 +02:00
|
|
|
type simpleConfig map[string]string
|
2018-07-29 07:50:27 +02:00
|
|
|
|
2018-07-31 20:53:23 +02:00
|
|
|
func (sc simpleConfig) loadDefaults(table []simpleConfigItem) {
|
|
|
|
for _, item := range table {
|
|
|
|
sc[item.key] = item.def
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (sc simpleConfig) updateFromFile() error {
|
|
|
|
basename := projectName + ".conf"
|
2018-08-06 12:31:31 +02:00
|
|
|
path := resolveFilename(basename, resolveRelativeConfigFilename)
|
|
|
|
if path == "" {
|
2018-07-31 20:53:23 +02:00
|
|
|
return &os.PathError{
|
|
|
|
Op: "cannot find",
|
|
|
|
Path: basename,
|
|
|
|
Err: os.ErrNotExist,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-08-06 12:31:31 +02:00
|
|
|
f, err := os.Open(path)
|
2018-07-31 20:53:23 +02:00
|
|
|
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, "#") {
|
2018-07-29 07:50:27 +02:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2018-07-31 20:53:23 +02:00
|
|
|
equals := strings.IndexByte(line, '=')
|
|
|
|
if equals <= 0 {
|
2018-08-06 12:31:31 +02:00
|
|
|
return fmt.Errorf("%s:%d: malformed line", path, lineNo)
|
2018-07-29 07:50:27 +02:00
|
|
|
}
|
2018-07-31 20:53:23 +02:00
|
|
|
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2022-09-26 12:23:58 +02:00
|
|
|
// Convenience wrapper suitable for most simple applications.
|
2018-07-31 20:53:23 +02:00
|
|
|
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,
|
|
|
|
``,
|
|
|
|
``,
|
|
|
|
}
|
|
|
|
|
2018-08-06 12:31:31 +02:00
|
|
|
path, err := simpleConfigWriteDefault(
|
2018-07-31 20:53:23 +02:00
|
|
|
pathHint, strings.Join(prologLines, "\n"), table)
|
|
|
|
if err != nil {
|
2018-08-06 19:50:53 +02:00
|
|
|
exitFatal("%s", err)
|
2018-07-29 07:50:27 +02:00
|
|
|
}
|
2018-07-31 20:53:23 +02:00
|
|
|
|
2018-08-06 19:50:53 +02:00
|
|
|
printStatus("configuration written to `%s'", path)
|
2018-07-29 07:50:27 +02:00
|
|
|
}
|
|
|
|
|
2018-07-31 20:53:23 +02:00
|
|
|
// --- 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)"},
|
2022-03-15 18:58:27 +01:00
|
|
|
{"webirc_password", "", "Password for WebIRC"},
|
2018-07-31 20:53:23 +02:00
|
|
|
|
|
|
|
{"operators", "", "IRCop TLS certificate SHA-256 fingerprints"},
|
|
|
|
|
|
|
|
{"max_connections", "0", "Global connection limit"},
|
|
|
|
{"ping_interval", "180", "Interval between PINGs (sec)"},
|
|
|
|
}
|
2018-07-29 07:50:27 +02:00
|
|
|
|
2018-07-28 16:21:34 +02:00
|
|
|
// --- Rate limiter ------------------------------------------------------------
|
|
|
|
|
|
|
|
type floodDetector struct {
|
2018-07-30 10:04:05 +02:00
|
|
|
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
|
2018-07-28 16:21:34 +02:00
|
|
|
}
|
|
|
|
|
2018-07-30 10:04:05 +02:00
|
|
|
func newFloodDetector(interval time.Duration, limit uint) *floodDetector {
|
2018-07-28 16:21:34 +02:00
|
|
|
return &floodDetector{
|
|
|
|
interval: interval,
|
|
|
|
limit: limit,
|
2018-07-30 10:04:05 +02:00
|
|
|
timestamps: make([]time.Time, limit+1),
|
2018-07-28 16:21:34 +02:00
|
|
|
pos: 0,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (fd *floodDetector) check() bool {
|
2018-07-30 10:04:05 +02:00
|
|
|
now := time.Now()
|
2018-07-28 16:21:34 +02:00
|
|
|
fd.timestamps[fd.pos] = now
|
|
|
|
|
|
|
|
fd.pos++
|
|
|
|
if fd.pos > fd.limit {
|
|
|
|
fd.pos = 0
|
|
|
|
}
|
|
|
|
|
|
|
|
var count uint
|
2018-07-30 10:04:05 +02:00
|
|
|
begin := now.Add(-fd.interval)
|
2018-07-28 16:21:34 +02:00
|
|
|
for _, ts := range fd.timestamps {
|
2018-07-30 10:04:05 +02:00
|
|
|
if ts.After(begin) {
|
2018-07-28 16:21:34 +02:00
|
|
|
count++
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return count <= fd.limit
|
|
|
|
}
|
|
|
|
|
2018-07-31 23:37:54 +02:00
|
|
|
// --- IRC token validation ----------------------------------------------------
|
|
|
|
|
2018-07-28 16:21:34 +02:00
|
|
|
// Everything as per RFC 2812
|
|
|
|
const (
|
|
|
|
ircMaxNickname = 9
|
|
|
|
ircMaxHostname = 63
|
|
|
|
ircMaxChannelName = 50
|
|
|
|
ircMaxMessageLength = 510
|
|
|
|
)
|
|
|
|
|
2018-07-31 20:53:23 +02:00
|
|
|
const (
|
|
|
|
reClassSpecial = "\\[\\]\\\\`_^{|}"
|
|
|
|
// "shortname" from RFC 2812 doesn't work how its author thought it would.
|
|
|
|
reShortname = "[0-9A-Za-z](-*[0-9A-Za-z])*"
|
|
|
|
)
|
2018-07-28 16:21:34 +02:00
|
|
|
|
|
|
|
var (
|
2018-07-31 20:53:23 +02:00
|
|
|
reHostname = regexp.MustCompile(
|
|
|
|
`^` + reShortname + `(\.` + reShortname + `)*$`)
|
|
|
|
|
2018-07-28 16:21:34 +02:00
|
|
|
// 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 @]+$`)
|
|
|
|
|
2018-07-31 21:13:30 +02:00
|
|
|
reChannelName = regexp.MustCompile(`^[^\0\007\r\n ,:]+$`)
|
2018-07-30 09:42:01 +02:00
|
|
|
reKey = regexp.MustCompile(`^[^\r\n\f\t\v ]{1,23}$`)
|
|
|
|
reUserMask = regexp.MustCompile(`^[^!@]+![^!@]+@[^@!]+$`)
|
|
|
|
reFingerprint = regexp.MustCompile(`^[a-fA-F0-9]{64}$`)
|
2018-07-28 16:21:34 +02:00
|
|
|
)
|
|
|
|
|
2018-07-31 20:53:23 +02:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2018-07-28 16:21:34 +02:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2018-07-30 09:42:01 +02:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2018-07-28 16:21:34 +02:00
|
|
|
// --- Clients (equals users) --------------------------------------------------
|
|
|
|
|
2018-08-06 12:06:42 +02:00
|
|
|
type connCloseWriter interface {
|
2018-07-28 16:21:34 +02:00
|
|
|
net.Conn
|
|
|
|
CloseWrite() error
|
|
|
|
}
|
|
|
|
|
|
|
|
const ircSupportedUserModes = "aiwros"
|
|
|
|
|
|
|
|
const (
|
2018-07-30 09:42:01 +02:00
|
|
|
ircUserModeInvisible uint = 1 << iota
|
2018-07-28 16:21:34 +02:00
|
|
|
ircUserModeRxWallops
|
|
|
|
ircUserModeRestricted
|
|
|
|
ircUserModeOperator
|
|
|
|
ircUserModeRxServerNotices
|
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
2018-07-30 09:42:01 +02:00
|
|
|
ircCapMultiPrefix uint = 1 << iota
|
2018-07-28 16:21:34 +02:00
|
|
|
ircCapInviteNotify
|
|
|
|
ircCapEchoMessage
|
|
|
|
ircCapUserhostInNames
|
|
|
|
ircCapServerTime
|
|
|
|
)
|
|
|
|
|
|
|
|
type client struct {
|
2018-08-06 12:06:42 +02:00
|
|
|
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
|
2018-07-28 16:21:34 +02:00
|
|
|
|
2018-07-30 10:04:05 +02:00
|
|
|
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
|
2018-07-28 16:21:34 +02:00
|
|
|
|
|
|
|
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
|
2018-07-30 10:04:05 +02:00
|
|
|
lastActive time.Time // last PRIVMSG, to get idle time
|
2018-07-28 16:21:34 +02:00
|
|
|
invites map[string]bool // channel invitations by operators
|
2018-07-30 10:04:05 +02:00
|
|
|
antiflood *floodDetector // flood detector
|
2018-07-28 16:21:34 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// --- Channels ----------------------------------------------------------------
|
|
|
|
|
|
|
|
const ircSupportedChanModes = "ov" + "beI" + "imnqpst" + "kl"
|
|
|
|
|
|
|
|
const (
|
2018-07-30 09:42:01 +02:00
|
|
|
ircChanModeInviteOnly uint = 1 << iota
|
2018-07-28 16:21:34 +02:00
|
|
|
ircChanModeModerated
|
|
|
|
ircChanModeNoOutsideMsgs
|
|
|
|
ircChanModeQuiet
|
|
|
|
ircChanModePrivate
|
|
|
|
ircChanModeSecret
|
|
|
|
ircChanModeProtectedTopic
|
|
|
|
|
|
|
|
ircChanModeOperator
|
|
|
|
ircChanModeVoice
|
|
|
|
)
|
|
|
|
|
|
|
|
type channel struct {
|
2018-07-30 10:04:05 +02:00
|
|
|
name string // channel name
|
|
|
|
modes uint // channel modes
|
|
|
|
key string // channel key
|
|
|
|
userLimit int // user limit or -1
|
|
|
|
created time.Time // creation time
|
2018-07-28 16:21:34 +02:00
|
|
|
|
2018-07-30 10:04:05 +02:00
|
|
|
topic string // channel topic
|
|
|
|
topicWho string // who set the topic
|
|
|
|
topicTime time.Time // when the topic was set
|
2018-07-28 16:21:34 +02:00
|
|
|
|
|
|
|
userModes map[*client]uint // modes for all channel users
|
|
|
|
|
|
|
|
banList []string // ban list
|
|
|
|
exceptionList []string // exceptions from bans
|
|
|
|
inviteList []string // exceptions from +I
|
|
|
|
}
|
|
|
|
|
2018-07-30 09:42:01 +02:00
|
|
|
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')
|
|
|
|
}
|
2018-07-28 16:21:34 +02:00
|
|
|
|
2018-07-30 09:42:01 +02:00
|
|
|
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)
|
|
|
|
}
|
2018-07-28 16:21:34 +02:00
|
|
|
|
|
|
|
// --- 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
|
2018-07-29 15:57:39 +02:00
|
|
|
bytesReceived int // number of bytes received total
|
2018-07-28 16:21:34 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2018-08-03 21:12:45 +02:00
|
|
|
// TODO: Maybe we want to keep it in a struct?
|
|
|
|
// A better question might be: can we run multiple instances of it?
|
2018-07-28 16:21:34 +02:00
|
|
|
var (
|
2018-08-03 21:12:45 +02:00
|
|
|
// network
|
2018-07-28 16:21:34 +02:00
|
|
|
|
2018-08-03 21:12:45 +02:00
|
|
|
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
|
2018-07-31 21:13:30 +02:00
|
|
|
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
|
2018-07-28 16:21:34 +02:00
|
|
|
|
2018-08-03 21:12:45 +02:00
|
|
|
// event loop
|
|
|
|
|
|
|
|
quitting bool
|
|
|
|
quitTimer <-chan time.Time
|
2018-07-28 16:21:34 +02:00
|
|
|
|
|
|
|
sigs = make(chan os.Signal, 1)
|
|
|
|
conns = make(chan net.Conn)
|
|
|
|
prepared = make(chan preparedEvent)
|
|
|
|
reads = make(chan readEvent)
|
|
|
|
writes = make(chan writeEvent)
|
2018-08-01 17:49:27 +02:00
|
|
|
timers = make(chan func())
|
2018-07-28 16:21:34 +02:00
|
|
|
|
2018-08-03 21:12:45 +02:00
|
|
|
// 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
|
2018-07-28 16:21:34 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
// Forcefully tear down all connections.
|
2018-07-29 07:50:27 +02:00
|
|
|
func forceQuit(reason string) {
|
|
|
|
if !quitting {
|
2018-08-06 19:50:53 +02:00
|
|
|
exitFatal("forceQuit called without initiateQuit")
|
2018-07-28 16:21:34 +02:00
|
|
|
}
|
|
|
|
|
2018-08-06 19:50:53 +02:00
|
|
|
printStatus("forced shutdown (%s)", reason)
|
2018-07-28 16:21:34 +02:00
|
|
|
for c := range clients {
|
2018-07-29 07:50:27 +02:00
|
|
|
// initiateQuit has already unregistered the client.
|
|
|
|
c.kill("Shutting down")
|
2018-07-28 16:21:34 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Initiate a clean shutdown of the whole daemon.
|
2018-07-29 07:50:27 +02:00
|
|
|
func initiateQuit() {
|
2018-08-06 19:50:53 +02:00
|
|
|
printStatus("shutting down")
|
2018-07-31 20:53:23 +02:00
|
|
|
for _, ln := range listeners {
|
|
|
|
if err := ln.Close(); err != nil {
|
2018-08-06 19:50:53 +02:00
|
|
|
printError("%s", err)
|
2018-07-31 20:53:23 +02:00
|
|
|
}
|
2018-07-28 16:21:34 +02:00
|
|
|
}
|
|
|
|
for c := range clients {
|
2018-07-29 07:50:27 +02:00
|
|
|
c.closeLink("Shutting down")
|
2018-07-28 16:21:34 +02:00
|
|
|
}
|
|
|
|
|
2018-07-29 07:50:27 +02:00
|
|
|
quitTimer = time.After(5 * time.Second)
|
|
|
|
quitting = true
|
2018-07-28 16:21:34 +02:00
|
|
|
}
|
|
|
|
|
2018-07-29 17:49:57 +02:00
|
|
|
func ircChannelCreate(name string) *channel {
|
|
|
|
ch := &channel{
|
|
|
|
name: name,
|
|
|
|
userLimit: -1,
|
2018-07-31 21:13:30 +02:00
|
|
|
created: time.Now(),
|
|
|
|
userModes: make(map[*client]uint),
|
2018-07-29 17:49:57 +02:00
|
|
|
}
|
|
|
|
channels[ircToCanon(name)] = ch
|
|
|
|
return ch
|
|
|
|
}
|
2018-07-28 16:21:34 +02:00
|
|
|
|
|
|
|
func ircChannelDestroyIfEmpty(ch *channel) {
|
2018-07-29 17:49:57 +02:00
|
|
|
if len(ch.userModes) == 0 {
|
|
|
|
delete(channels, ircToCanon(ch.name))
|
|
|
|
}
|
2018-07-28 16:21:34 +02:00
|
|
|
}
|
|
|
|
|
2018-08-01 19:35:47 +02:00
|
|
|
func ircNotifyRoommates(c *client, message string) {
|
2018-07-29 17:49:57 +02:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|
2018-07-28 16:21:34 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// --- Clients (continued) -----------------------------------------------------
|
|
|
|
|
2018-08-06 20:31:22 +02:00
|
|
|
func (c *client) printDebug(format string, args ...interface{}) {
|
|
|
|
if debugMode {
|
|
|
|
printDebug("(%s) %s", c.address, fmt.Sprintf(format, args...))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-07-29 15:57:39 +02:00
|
|
|
func ircAppendClientModes(m uint, mode []byte) []byte {
|
2018-07-28 16:21:34 +02:00
|
|
|
if 0 != m&ircUserModeInvisible {
|
2018-07-29 15:57:39 +02:00
|
|
|
mode = append(mode, 'i')
|
2018-07-28 16:21:34 +02:00
|
|
|
}
|
|
|
|
if 0 != m&ircUserModeRxWallops {
|
2018-07-29 15:57:39 +02:00
|
|
|
mode = append(mode, 'w')
|
2018-07-28 16:21:34 +02:00
|
|
|
}
|
|
|
|
if 0 != m&ircUserModeRestricted {
|
2018-07-29 15:57:39 +02:00
|
|
|
mode = append(mode, 'r')
|
2018-07-28 16:21:34 +02:00
|
|
|
}
|
|
|
|
if 0 != m&ircUserModeOperator {
|
2018-07-29 15:57:39 +02:00
|
|
|
mode = append(mode, 'o')
|
2018-07-28 16:21:34 +02:00
|
|
|
}
|
|
|
|
if 0 != m&ircUserModeRxServerNotices {
|
2018-07-29 15:57:39 +02:00
|
|
|
mode = append(mode, 's')
|
2018-07-28 16:21:34 +02:00
|
|
|
}
|
2018-07-29 15:57:39 +02:00
|
|
|
return mode
|
2018-07-28 16:21:34 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func (c *client) getMode() string {
|
2018-07-29 07:50:27 +02:00
|
|
|
var mode []byte
|
2018-07-28 16:21:34 +02:00
|
|
|
if c.awayMessage != "" {
|
2018-07-29 07:50:27 +02:00
|
|
|
mode = append(mode, 'a')
|
2018-07-28 16:21:34 +02:00
|
|
|
}
|
2018-07-29 15:57:39 +02:00
|
|
|
return string(ircAppendClientModes(c.mode, mode))
|
2018-07-28 16:21:34 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func (c *client) send(line string) {
|
|
|
|