diff options
author | Přemysl Janouch <p@janouch.name> | 2018-07-31 20:53:23 +0200 |
---|---|---|
committer | Přemysl Janouch <p@janouch.name> | 2018-07-31 20:53:23 +0200 |
commit | b103d5e2eb39682e6285a82fd6bf4b536c420cc6 (patch) | |
tree | 9c3f1500d5172497e67ba868f26ffcd279b712a1 /hid | |
parent | c75299e1c376026ada795d7edf704e0bf5f0eb6b (diff) | |
download | haven-b103d5e2eb39682e6285a82fd6bf4b536c420cc6.tar.gz haven-b103d5e2eb39682e6285a82fd6bf4b536c420cc6.tar.xz haven-b103d5e2eb39682e6285a82fd6bf4b536c420cc6.zip |
hid: port configuration and initialization
All the basic elements should be there now, we just need to port PING
timers and fix some remaining issues and we're basically done.
Diffstat (limited to 'hid')
-rw-r--r-- | hid/main.go | 533 |
1 files changed, 427 insertions, 106 deletions
diff --git a/hid/main.go b/hid/main.go index eec67d9..2c34f08 100644 --- a/hid/main.go +++ b/hid/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() } |