summaryrefslogtreecommitdiff
path: root/xS
diff options
context:
space:
mode:
Diffstat (limited to 'xS')
-rw-r--r--xS/main.go533
1 files changed, 427 insertions, 106 deletions
diff --git a/xS/main.go b/xS/main.go
index eec67d9..2c34f08 100644
--- a/xS/main.go
+++ b/xS/main.go
@@ -22,9 +22,11 @@ import (
"crypto/sha256"
"crypto/tls"
"encoding/hex"
+ "errors"
"flag"
"fmt"
"io"
+ "io/ioutil"
"log"
"net"
"os"
@@ -66,37 +68,42 @@ func splitString(s, delims string, ignoreEmpty bool) (result []string) {
return
}
-func findTildeHome(username string) string {
- if username != "" {
- if u, _ := user.Lookup(username); u != nil {
- return u.HomeDir
+//
+// 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)
+//
+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 = buf[0] == 22 && buf[1] == 3
+ case n == 1:
+ isTLS = buf[0] == 22
+ case err == syscall.EAGAIN:
+ return false
}
- } else if u, _ := user.Current(); u != nil {
- return u.HomeDir
- } else if v, ok := os.LookupEnv("HOME"); ok {
- return v
- }
- return "~" + username
+ return true
+ })
+ return isTLS
}
-// Tries to expand the tilde in paths, leaving it as-is on error.
-func expandTilde(path string) string {
- if path[0] != '~' {
- return path
- }
-
- var n int
- for n = 0; n < len(path); n++ {
- if path[n] == '/' {
- break
- }
- }
- return findTildeHome(path[1:n]) + path[n:]
-}
+// --- 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] == '/' {
+ if env != "" && env[0] == filepath.Separator {
return env
}
@@ -109,6 +116,21 @@ func getXDGHomeDir(name, def string) string {
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")
@@ -127,77 +149,179 @@ func getXDGConfigDirs() (result []string) {
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)
-//
-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 = buf[0] == 22 && buf[1] == 3
- case n == 1:
- isTLS = buf[0] == 22
- case err == syscall.EAGAIN:
- return false
+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
}
- return true
- })
- return isTLS
+ } else if u, _ := user.Current(); u != nil {
+ return u.HomeDir
+ } else if v, ok := os.LookupEnv("HOME"); ok {
+ return v
+ }
+ if debugMode {
+ log.Printf("failed to expand the home directory for %s", username)
+ }
+ return "~" + username
}
-// --- Configuration -----------------------------------------------------------
+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(filename string, data []byte) error {
+ if dir := filepath.Dir(filename); dir != "." {
+ if err := os.MkdirAll(dir, 0755); err != nil {
+ return err
+ }
+ }
+ return ioutil.WriteFile(filename, 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(filename string, data []byte) error {
+ temp := filename + ".new"
+ if err := writeFile(temp, data); err != nil {
+ return err
+ }
+ return os.Rename(temp, filename)
+}
-// XXX: Do we really want to support default nil values?
-var config = []struct {
+// --- Simple configuration ----------------------------------------------------
+
+type simpleConfigItem struct {
key string // INI key
- def []rune // default value, may be nil
+ def string // default value
description string // documentation
-}{
- // XXX: I'm not sure if Go will cooperate here.
- {"pid_file", nil, "Path or name of the PID file"},
- {"bind", []rune(":6667"), "Address of the IRC server"},
}
-/*
+type simpleConfig map[string]string
-// Read a configuration file with the given basename w/o extension.
-func readConfigFile(name string, output interface{}) error {
- var suffix = filepath.Join(projectName, name+".json")
- for _, path := range getXDGConfigDirs() {
- full := filepath.Join(path, suffix)
- file, err := os.Open(full)
- if err != nil {
- if !os.IsNotExist(err) {
- return err
- }
+func (sc simpleConfig) loadDefaults(table []simpleConfigItem) {
+ for _, item := range table {
+ sc[item.key] = item.def
+ }
+}
+
+func (sc simpleConfig) updateFromFile() error {
+ basename := projectName + ".conf"
+ filename := resolveFilename(basename, resolveRelativeConfigFilename)
+ if filename == "" {
+ return &os.PathError{
+ Op: "cannot find",
+ Path: basename,
+ Err: os.ErrNotExist,
+ }
+ }
+
+ f, err := os.Open(filename)
+ 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
}
- defer file.Close()
- // TODO: We don't want to use JSON.
- decoder := json.NewDecoder(file)
- err = decoder.Decode(output)
- if err != nil {
- return fmt.Errorf("%s: %s", full, err)
+ equals := strings.IndexByte(line, '=')
+ if equals <= 0 {
+ return fmt.Errorf("%s:%d: malformed line", filename, lineNo)
}
- return nil
+
+ 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,
+ ``,
+ ``,
+ }
+
+ filename, err := simpleConfigWriteDefault(
+ pathHint, strings.Join(prologLines, "\n"), table)
+ if err != nil {
+ log.Fatalln(err)
}
- return errors.New("configuration file not found")
+
+ log.Printf("configuration written to `%s'\n", filename)
}
-*/
+// --- Configuration -----------------------------------------------------------
+
+var configTable = []simpleConfigItem{
+ // TODO: Default to the result from os.Hostname (if successful).
+ {"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)"},
+
+ {"operators", "", "IRCop TLS certificate SHA-256 fingerprints"},
+
+ {"max_connections", "0", "Global connection limit"},
+ {"ping_interval", "180", "Interval between PINGs (sec)"},
+}
// --- Rate limiter ------------------------------------------------------------
@@ -372,9 +496,16 @@ const (
ircMaxMessageLength = 510
)
-const reClassSpecial = "\\[\\]\\\\`_^{|}"
+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-]*$`)
@@ -389,6 +520,19 @@ var (
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)
}
@@ -603,7 +747,8 @@ type writeEvent struct {
err error // write error
}
-// TODO: Port server_context. Maybe we want to keep it in a struct?
+// TODO: Port server_context. Maybe we want to keep it in a struct? A better
+// question might be: can we run multiple instances of it?
// XXX: Beware that maps with identifier keys need to be indexed correctly.
// We might want to enforce accessor functions for users and channels.
var (
@@ -614,6 +759,7 @@ var (
whowas map[string]*whowasInfo // WHOWAS registry
+ config simpleConfig // server configuration
serverName string // our server name
pingInterval uint // ping interval in seconds
maxConnections int // max connections allowed or 0
@@ -631,7 +777,7 @@ var (
tlsConf *tls.Config
clients = make(map[*client]bool)
- listener net.Listener
+ listeners []net.Listener
quitting bool
quitTimer <-chan time.Time
)
@@ -652,8 +798,10 @@ func forceQuit(reason string) {
// Initiate a clean shutdown of the whole daemon.
func initiateQuit() {
log.Println("shutting down")
- if err := listener.Close(); err != nil {
- log.Println(err)
+ for _, ln := range listeners {
+ if err := ln.Close(); err != nil {
+ log.Println(err)
+ }
}
for c := range clients {
c.closeLink("Shutting down")
@@ -2751,12 +2899,15 @@ func (c *client) onWrite(written int, writeErr error) {
func accept(ln net.Listener) {
for {
+ // Error handling here may be tricky, see go #6163, #24808.
if conn, err := ln.Accept(); err != nil {
- // TODO: Consider specific cases in error handling, some errors
- // are transitional while others are fatal.
- log.Println(err)
- break
+ if op, ok := err.(net.Error); !ok || !op.Temporary() {
+ log.Fatalln(err)
+ } else {
+ log.Println(err)
+ }
} else {
+ // TCP_NODELAY is set by default on TCPConns.
conns <- conn
}
}
@@ -2798,7 +2949,7 @@ func prepare(client *client) {
// This is just for the TLS detection and doesn't need to be fatal.
log.Println(err)
} else {
- isTLS = detectTLS(sysconn)
+ isTLS = tlsConf != nil && detectTLS(sysconn)
}
// FIXME: When the client sends no data, we still initialize its conn.
@@ -2827,7 +2978,7 @@ func write(client *client, data []byte) {
writes <- writeEvent{client, n, err}
}
-// --- Main --------------------------------------------------------------------
+// --- Event loop --------------------------------------------------------------
func processOneEvent() {
select {
@@ -2842,6 +2993,12 @@ func processOneEvent() {
forceQuit("timeout")
case conn := <-conns:
+ if len(clients) >= maxConnections {
+ log.Println("connection limit reached, refusing connection")
+ conn.Close()
+ break
+ }
+
log.Println("accepted client connection")
// In effect, we require TCP/UDP, as they have port numbers.
@@ -2889,24 +3046,34 @@ func processOneEvent() {
}
}
-func main() {
- flag.BoolVar(&debugMode, "debug", false, "debug mode")
- version := flag.Bool("version", false, "show version and exit")
- flag.Parse()
+// --- Application setup -------------------------------------------------------
- if *version {
- fmt.Printf("%s %s\n", projectName, projectVersion)
- return
+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)
}
- // TODO: Configuration--create an INI parser, probably.
- if len(flag.Args()) != 3 {
- log.Fatalf("usage: %s KEY CERT ADDRESS\n", os.Args[0])
+ pathKey := resolveFilename(configKey, resolveRelativeConfigFilename)
+ if pathKey == "" {
+ return fmt.Errorf("cannot find file: %s", configKey)
}
- cert, err := tls.LoadX509KeyPair(flag.Arg(1), flag.Arg(0))
+ cert, err := tls.LoadX509KeyPair(pathCert, pathKey)
if err != nil {
- log.Fatalln(err)
+ return err
}
tlsConf = &tls.Config{
@@ -2914,15 +3081,169 @@ func main() {
ClientAuth: tls.RequestClientCert,
SessionTicketsDisabled: true,
}
- listener, err = net.Listen("tcp", flag.Arg(2))
+ return nil
+}
+
+func ircInitializeCatalog() error {
+ // TODO: Not going to use catgets but a simple text file with basic
+ // checking whether the index is used by this daemon at all should do.
+ return nil
+}
+
+func ircInitializeMOTD() error {
+ configMOTD := config["motd"]
+ if configMOTD == "" {
+ return nil
+ }
+
+ pathMOTD := resolveFilename(configMOTD, resolveRelativeConfigFilename)
+ if pathMOTD == "" {
+ return fmt.Errorf("cannot find file: %s", configMOTD)
+ }
+
+ f, err := os.Open(pathMOTD)
if err != nil {
- log.Fatalln(err)
+ 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 configError struct {
+ name string // configuration key
+ err error // description of the issue
+}
+
+func (e *configError) Error() string {
+ return fmt.Sprintf("invalid configuration value for `%s': %s",
+ e.name, e.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 {
+ // TODO: I think we could further shorten this with lambdas, doing away
+ // with the custom error type completely and at the same time getting rid of
+ // the key stuttering.
+ if u, err := strconv.ParseUint(
+ config["ping_interval"], 10, 32); err != nil {
+ return &configError{"ping_interval", err}
+ } else if u < 1 {
+ return &configError{"ping_interval",
+ errors.New("the value is out of range")}
+ } else {
+ pingInterval = uint(u)
+ }
+
+ if i, err := strconv.ParseInt(
+ config["max_connections"], 10, 32); err != nil {
+ return &configError{"max_connections", err}
+ } else if i < 0 {
+ return &configError{"max_connections",
+ errors.New("the value is out of range")}
+ } else {
+ maxConnections = int(i)
+ }
+
+ operators = make(map[string]bool)
+ for _, fp := range splitString(config["operators"], ",", true) {
+ if !ircIsValidFingerprint(fp) {
+ return &configError{"operators",
+ errors.New("invalid fingerprint valeu")}
+ }
+ operators[strings.ToLower(fp)] = true
+ }
+ return nil
+}
+
+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)
+ }
+ 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 debug mode")
+ version := flag.Bool("version", false, "show version and exit")
+ writeDefaultCfg := flag.Bool("writedefaultcfg", false,
+ "write a default configuration file and exit")
+
+ flag.Parse()
+
+ if *version {
+ fmt.Printf("%s %s\n", projectName, projectVersion)
+ return
+ }
+ if *writeDefaultCfg {
+ callSimpleConfigWriteDefault("", configTable)
+ return
+ }
+ if flag.NArg() > 0 {
+ flag.Usage()
+ os.Exit(1)
+ }
+
+ config = make(simpleConfig)
+ if err := config.updateFromFile(); err != nil && !os.IsNotExist(err) {
+ log.Println("error loading configuration", err)
+ os.Exit(1)
}
started = time.Now()
- go accept(listener)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
+ for _, fn := range []func() error{
+ ircInitializeTLS,
+ ircInitializeServerName,
+ ircInitializeMOTD,
+ ircInitializeCatalog,
+ ircParseConfig,
+ ircSetupListenFDs,
+ } {
+ if err := fn(); err != nil {
+ log.Fatalln(err)
+ }
+ }
+
for !quitting || len(clients) > 0 {
processOneEvent()
}