diff options
| -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()  	} | 
