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/tls"
"encoding/hex"
"errors"
"flag"
"fmt"
"io"
"io/ioutil"
"log"
"net"
"os"
@ -66,67 +68,6 @@ 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
}
} 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
// 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
}
// --- Configuration -----------------------------------------------------------
// --- File system -------------------------------------------------------------
// XXX: Do we really want to support default nil values?
var config = []struct {
key string // INI key
def []rune // default value, may be nil
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"},
// 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] == filepath.Separator {
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)
}
/*
// 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 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
}
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)
file := filepath.Join(path, filename)
if _, err := os.Stat(file); err == nil {
return file
}
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 ------------------------------------------------------------
@ -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")
}
// TODO: Configuration--create an INI parser, probably.
if len(flag.Args()) != 3 {
log.Fatalf("usage: %s KEY CERT ADDRESS\n", os.Args[0])
pathCert := resolveFilename(configCert, resolveRelativeConfigFilename)
if pathCert == "" {
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 {
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()
}