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.
This commit is contained in:
Přemysl Eric Janouch 2018-07-31 20:53:23 +02:00
parent 051bbedc2f
commit 2f841d214f
1 changed files with 439 additions and 118 deletions

View File

@ -22,9 +22,11 @@ import (
"crypto/sha256" "crypto/sha256"
"crypto/tls" "crypto/tls"
"encoding/hex" "encoding/hex"
"errors"
"flag" "flag"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"log" "log"
"net" "net"
"os" "os"
@ -66,67 +68,6 @@ func splitString(s, delims string, ignoreEmpty bool) (result []string) {
return return
} }
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
}
return "~" + username
}
// 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:]
}
func getXDGHomeDir(name, def string) string {
env := os.Getenv(name)
if env != "" && env[0] == '/' {
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)
}
// 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
}
// //
// Trivial SSL/TLS autodetection. The first block of data returned by Recvfrom // 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 // must be at least three octets long for this to work reliably, but that should
@ -157,47 +98,230 @@ func detectTLS(sysconn syscall.RawConn) (isTLS bool) {
return isTLS return isTLS
} }
// --- Configuration ----------------------------------------------------------- // --- File system -------------------------------------------------------------
// XXX: Do we really want to support default nil values? // Look up the value of an XDG path from environment, or fall back to a default.
var config = []struct { func getXDGHomeDir(name, def string) string {
key string // INI key env := os.Getenv(name)
def []rune // default value, may be nil if env != "" && env[0] == filepath.Separator {
description string // documentation return env
}{ }
// XXX: I'm not sure if Go will cooperate here.
{"pid_file", nil, "Path or name of the PID file"}, home := ""
{"bind", []rune(":6667"), "Address of the IRC server"}, 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 {
// Read a configuration file with the given basename w/o extension. // As per XDG spec, relative paths are ignored.
func readConfigFile(name string, output interface{}) error { if path == "" || path[0] != filepath.Separator {
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
}
continue continue
} }
defer file.Close()
// TODO: We don't want to use JSON. file := filepath.Join(path, filename)
decoder := json.NewDecoder(file) if _, err := os.Stat(file); err == nil {
err = decoder.Decode(output) return file
if err != nil {
return fmt.Errorf("%s: %s", full, err)
} }
return nil
} }
return errors.New("configuration file not found") 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
}
if debugMode {
log.Printf("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(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)
}
// --- Simple configuration ----------------------------------------------------
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"
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
}
equals := strings.IndexByte(line, '=')
if equals <= 0 {
return fmt.Errorf("%s:%d: malformed line", filename, 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,
``,
``,
}
filename, err := simpleConfigWriteDefault(
pathHint, strings.Join(prologLines, "\n"), table)
if err != nil {
log.Fatalln(err)
}
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 ------------------------------------------------------------ // --- Rate limiter ------------------------------------------------------------
@ -372,9 +496,16 @@ const (
ircMaxMessageLength = 510 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 ( var (
reHostname = regexp.MustCompile(
`^` + reShortname + `(\.` + reShortname + `)*$`)
// Extending ASCII to the whole range of Unicode letters. // Extending ASCII to the whole range of Unicode letters.
reNickname = regexp.MustCompile( reNickname = regexp.MustCompile(
`^[\pL` + reClassSpecial + `][\pL` + reClassSpecial + `0-9-]*$`) `^[\pL` + reClassSpecial + `][\pL` + reClassSpecial + `0-9-]*$`)
@ -389,6 +520,19 @@ var (
reFingerprint = regexp.MustCompile(`^[a-fA-F0-9]{64}$`) 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 { func ircIsValidNickname(nickname string) bool {
return len(nickname) <= ircMaxNickname && reNickname.MatchString(nickname) return len(nickname) <= ircMaxNickname && reNickname.MatchString(nickname)
} }
@ -603,7 +747,8 @@ type writeEvent struct {
err error // write error 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. // XXX: Beware that maps with identifier keys need to be indexed correctly.
// We might want to enforce accessor functions for users and channels. // We might want to enforce accessor functions for users and channels.
var ( var (
@ -614,6 +759,7 @@ var (
whowas map[string]*whowasInfo // WHOWAS registry whowas map[string]*whowasInfo // WHOWAS registry
config simpleConfig // server configuration
serverName string // our server name serverName string // our server name
pingInterval uint // ping interval in seconds pingInterval uint // ping interval in seconds
maxConnections int // max connections allowed or 0 maxConnections int // max connections allowed or 0
@ -631,7 +777,7 @@ var (
tlsConf *tls.Config tlsConf *tls.Config
clients = make(map[*client]bool) clients = make(map[*client]bool)
listener net.Listener listeners []net.Listener
quitting bool quitting bool
quitTimer <-chan time.Time quitTimer <-chan time.Time
) )
@ -652,8 +798,10 @@ func forceQuit(reason string) {
// Initiate a clean shutdown of the whole daemon. // Initiate a clean shutdown of the whole daemon.
func initiateQuit() { func initiateQuit() {
log.Println("shutting down") log.Println("shutting down")
if err := listener.Close(); err != nil { for _, ln := range listeners {
log.Println(err) if err := ln.Close(); err != nil {
log.Println(err)
}
} }
for c := range clients { for c := range clients {
c.closeLink("Shutting down") c.closeLink("Shutting down")
@ -2751,12 +2899,15 @@ func (c *client) onWrite(written int, writeErr error) {
func accept(ln net.Listener) { func accept(ln net.Listener) {
for { for {
// Error handling here may be tricky, see go #6163, #24808.
if conn, err := ln.Accept(); err != nil { if conn, err := ln.Accept(); err != nil {
// TODO: Consider specific cases in error handling, some errors if op, ok := err.(net.Error); !ok || !op.Temporary() {
// are transitional while others are fatal. log.Fatalln(err)
log.Println(err) } else {
break log.Println(err)
}
} else { } else {
// TCP_NODELAY is set by default on TCPConns.
conns <- conn conns <- conn
} }
} }
@ -2798,7 +2949,7 @@ func prepare(client *client) {
// This is just for the TLS detection and doesn't need to be fatal. // This is just for the TLS detection and doesn't need to be fatal.
log.Println(err) log.Println(err)
} else { } else {
isTLS = detectTLS(sysconn) isTLS = tlsConf != nil && detectTLS(sysconn)
} }
// FIXME: When the client sends no data, we still initialize its conn. // 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} writes <- writeEvent{client, n, err}
} }
// --- Main -------------------------------------------------------------------- // --- Event loop --------------------------------------------------------------
func processOneEvent() { func processOneEvent() {
select { select {
@ -2842,6 +2993,12 @@ func processOneEvent() {
forceQuit("timeout") forceQuit("timeout")
case conn := <-conns: case conn := <-conns:
if len(clients) >= maxConnections {
log.Println("connection limit reached, refusing connection")
conn.Close()
break
}
log.Println("accepted client connection") log.Println("accepted client connection")
// In effect, we require TCP/UDP, as they have port numbers. // In effect, we require TCP/UDP, as they have port numbers.
@ -2889,24 +3046,34 @@ func processOneEvent() {
} }
} }
func main() { // --- Application setup -------------------------------------------------------
flag.BoolVar(&debugMode, "debug", false, "debug mode")
version := flag.Bool("version", false, "show version and exit")
flag.Parse()
if *version { func ircInitializeTLS() error {
fmt.Printf("%s %s\n", projectName, projectVersion) configCert, configKey := config["tls_cert"], config["tls_key"]
return
// 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")
} }
// TODO: Configuration--create an INI parser, probably. pathCert := resolveFilename(configCert, resolveRelativeConfigFilename)
if len(flag.Args()) != 3 { if pathCert == "" {
log.Fatalf("usage: %s KEY CERT ADDRESS\n", os.Args[0]) return fmt.Errorf("cannot find file: %s", configCert)
} }
cert, err := tls.LoadX509KeyPair(flag.Arg(1), flag.Arg(0)) pathKey := resolveFilename(configKey, resolveRelativeConfigFilename)
if pathKey == "" {
return fmt.Errorf("cannot find file: %s", configKey)
}
cert, err := tls.LoadX509KeyPair(pathCert, pathKey)
if err != nil { if err != nil {
log.Fatalln(err) return err
} }
tlsConf = &tls.Config{ tlsConf = &tls.Config{
@ -2914,15 +3081,169 @@ func main() {
ClientAuth: tls.RequestClientCert, ClientAuth: tls.RequestClientCert,
SessionTicketsDisabled: true, 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 { 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() started = time.Now()
go accept(listener)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) 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 { for !quitting || len(clients) > 0 {
processOneEvent() processOneEvent()
} }