From 212c9f79fb83e4be7d44ba357d9fab0b6a778c4d Mon Sep 17 00:00:00 2001 From: Solderpunk Date: Thu, 23 Feb 2023 18:49:15 +0100 Subject: [PATCH] A rather extensive refactor. Basically the function formerly known as do_main() in main.go has been renamed launch() and moved into launch.go. Now there are main.go and main_unix.go files implementing minmial main() functions which load a config and pass it to launch. This allows separating unix-specific security stuff (both the actual system calls which won't compile on other platforms and the definition of command line switches) out from the platform agnostic implementation of the main server logic. It also simplifies the interaction of relative paths in config files with chrooting. Docs still need updating... --- config.go | 45 ++++++------ launch.go | 134 +++++++++++++++++++++++++++++++++++ main.go | 158 ++---------------------------------------- main_unix.go | 55 +++++++++++++++ security.go | 4 ++ security_dropprivs.go | 8 +-- 6 files changed, 222 insertions(+), 182 deletions(-) create mode 100644 launch.go create mode 100644 main_unix.go diff --git a/config.go b/config.go index 96f8907..0d1ce4c 100644 --- a/config.go +++ b/config.go @@ -16,8 +16,6 @@ type Config struct { KeyPath string DocBase string HomeDocBase string - ChrootDir string - UnprivUsername string GeminiExt string DefaultLang string DefaultEncoding string @@ -61,8 +59,6 @@ func getConfig(filename string) (Config, error) { config.KeyPath = "key.pem" config.DocBase = "/var/gemini/" config.HomeDocBase = "users" - config.ChrootDir = "" - config.UnprivUsername = "nobody" config.GeminiExt = "gmi" config.DefaultLang = "" config.DefaultEncoding = "" @@ -96,32 +92,30 @@ func getConfig(filename string) (Config, error) { return config, errors.New("Invalid DirectorySort value.") } - // Validate chroot() dir - if config.ChrootDir != "" { - config.ChrootDir, err = filepath.Abs(config.ChrootDir) + // Absolutise paths + config.DocBase, err = filepath.Abs(config.DocBase) + if err != nil { + return config, err + } + config.CertPath, err = filepath.Abs(config.CertPath) + if err != nil { + return config, err + } + config.KeyPath, err = filepath.Abs(config.KeyPath) + if err != nil { + return config, err + } + if config.AccessLog != "" && config.AccessLog != "-" { + config.AccessLog, err = filepath.Abs(config.AccessLog) if err != nil { return config, err } - _, err := os.Stat(config.ChrootDir) - if os.IsNotExist(err) { - return config, err - } } - - // Absolutise DocBase, relative to the chroot dir - if !filepath.IsAbs(config.DocBase) { - abs, err := filepath.Abs(config.DocBase) + if config.ErrorLog != "" { + config.ErrorLog, err = filepath.Abs(config.ErrorLog) if err != nil { return config, err } - if config.ChrootDir != "" { - config.DocBase, err = filepath.Rel(config.ChrootDir, abs) - if err != nil { - return config, err - } - } else { - config.DocBase = abs - } } // Absolutise CGI paths @@ -144,8 +138,9 @@ func getConfig(filename string) (Config, error) { // Absolutise SCGI paths for index, scgiPath := range config.SCGIPaths { - if !filepath.IsAbs(scgiPath) { - config.SCGIPaths[index] = filepath.Join(config.DocBase, scgiPath) + config.SCGIPaths[index], err = filepath.Abs( scgiPath) + if err != nil { + return config, err } } diff --git a/launch.go b/launch.go new file mode 100644 index 0000000..1fd8c25 --- /dev/null +++ b/launch.go @@ -0,0 +1,134 @@ +package main + +import ( + "crypto/tls" + "log" + "os" + "os/signal" + "strconv" + "sync" + "syscall" +) + +var VERSION = "0.0.0" + +func launch(config Config, privInfo userInfo) int { + + // Open log files + if config.ErrorLog != "" { + errorLogFile, err := os.OpenFile(config.ErrorLog, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + log.Println("Error opening error log file: " + err.Error()) + return 1 + } + defer errorLogFile.Close() + log.SetOutput(errorLogFile) + } + log.SetFlags(log.Ldate|log.Ltime) + + var accessLogFile *os.File + if config.AccessLog == "-" { + accessLogFile = os.Stdout + } else if config.AccessLog != "" { + accessLogFile, err := os.OpenFile(config.AccessLog, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + log.Println("Error opening access log file: " + err.Error()) + return 1 + } + defer accessLogFile.Close() + } + + // Read TLS files, create TLS config + // Check key file permissions first + info, err := os.Stat(config.KeyPath) + if err != nil { + log.Println("Error opening TLS key file: " + err.Error()) + return 1 + } + if uint64(info.Mode().Perm())&0444 == 0444 { + log.Println("Refusing to use world-readable TLS key file " + config.KeyPath) + return 1 + } + cert, err := tls.LoadX509KeyPair(config.CertPath, config.KeyPath) + if err != nil { + log.Println("Error loading TLS keypair: " + err.Error()) + return 1 + } + var tlscfg tls.Config + tlscfg.Certificates = []tls.Certificate{cert} + tlscfg.MinVersion = tls.VersionTLS12 + if len(config.CertificateZones) > 0 { + tlscfg.ClientAuth = tls.RequestClientCert + } + + // Try to chdir to /, so we don't block any mountpoints + // But if we can't for some reason it's no big deal + err = os.Chdir("/") + if err != nil { + log.Println("Could not change working directory to /: " + err.Error()) + } + + // Apply security restrictions + err = enableSecurityRestrictions(config, privInfo) + if err != nil { + log.Println("Exiting due to failure to apply security restrictions.") + return 1 + } + + // Create TLS listener + listener, err := tls.Listen("tcp", ":"+strconv.Itoa(config.Port), &tlscfg) + if err != nil { + log.Println("Error creating TLS listener: " + err.Error()) + return 1 + } + defer listener.Close() + + // Start log handling routines + var accessLogEntries chan LogEntry + if config.AccessLog == "" { + accessLogEntries = nil + } else { + accessLogEntries := make(chan LogEntry, 10) + go func() { + for { + entry := <-accessLogEntries + writeLogEntry(accessLogFile, entry) + } + }() + } + + // Start listening for signals + shutdown := make(chan struct{}) + sigterm := make(chan os.Signal, 1) + signal.Notify(sigterm, syscall.SIGTERM) + go func() { + <-sigterm + log.Println("Caught SIGTERM. Waiting for handlers to finish...") + close(shutdown) + listener.Close() + }() + + // Infinite serve loop (SIGTERM breaks out) + running := true + var wg sync.WaitGroup + for running { + conn, err := listener.Accept() + if err == nil { + wg.Add(1) + go handleGeminiRequest(conn, config, accessLogEntries, &wg) + } else { + select { + case <-shutdown: + running = false + default: + log.Println("Error accepting connection: " + err.Error()) + } + } + } + // Wait for still-running handler Go routines to finish + wg.Wait() + log.Println("Exiting.") + + // Exit successfully + return 0 +} diff --git a/main.go b/main.go index f511a3c..1991a33 100644 --- a/main.go +++ b/main.go @@ -1,25 +1,20 @@ +// +build js nacl plan9 windows + package main import ( - "crypto/tls" "flag" "fmt" "log" "os" - "os/signal" - "strconv" - "sync" - "syscall" ) -var VERSION = "0.0.0" - func main() { var conf_file string var version bool // Parse args - flag.StringVar(&conf_file, "c", "", "Path to config file") + flag.StringVar(&conf_file, "c", "/etc/molly.conf", "Path to config file") flag.BoolVar(&version, "v", false, "Print version and exit") flag.Parse() @@ -30,155 +25,12 @@ func main() { } // Read config - if conf_file == "" { - _, err := os.Stat("/etc/molly.conf") - if err == nil { - conf_file = "/etc/molly.conf" - } - } config, err := getConfig(conf_file) if err != nil { log.Fatal(err) } // Run server and exit - os.Exit(do_main(config)) -} - -func do_main(config Config) int { - - // If we are running as root, find the UID of the "nobody" user, before a - // chroot() possibly stops seeing /etc/passwd - privInfo, err := getUserInfo(config) - if err != nil { - log.Println("Exiting due to failure to apply security restrictions.") - return 1 - } - - // Chroot, if asked - if config.ChrootDir != "" { - err := syscall.Chroot(config.ChrootDir) - if err != nil { - log.Println("Could not chroot to " + config.ChrootDir + ": " + err.Error()) - return 1 - } - } - - // Open log files - if config.ErrorLog != "" { - errorLogFile, err := os.OpenFile(config.ErrorLog, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) - if err != nil { - log.Println("Error opening error log file: " + err.Error()) - return 1 - } - defer errorLogFile.Close() - log.SetOutput(errorLogFile) - } - log.SetFlags(log.Ldate|log.Ltime) - - var accessLogFile *os.File - if config.AccessLog == "-" { - accessLogFile = os.Stdout - } else if config.AccessLog != "" { - accessLogFile, err = os.OpenFile(config.AccessLog, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) - if err != nil { - log.Println("Error opening access log file: " + err.Error()) - return 1 - } - defer accessLogFile.Close() - } - - // Read TLS files, create TLS config - // Check key file permissions first - info, err := os.Stat(config.KeyPath) - if err != nil { - log.Println("Error opening TLS key file: " + err.Error()) - return 1 - } - if uint64(info.Mode().Perm())&0444 == 0444 { - log.Println("Refusing to use world-readable TLS key file " + config.KeyPath) - return 1 - } - cert, err := tls.LoadX509KeyPair(config.CertPath, config.KeyPath) - if err != nil { - log.Println("Error loading TLS keypair: " + err.Error()) - return 1 - } - var tlscfg tls.Config - tlscfg.Certificates = []tls.Certificate{cert} - tlscfg.MinVersion = tls.VersionTLS12 - if len(config.CertificateZones) > 0 { - tlscfg.ClientAuth = tls.RequestClientCert - } - - // Try to chdir to /, so we don't block any mountpoints - // But if we can't for some reason it's no big deal - err = os.Chdir("/") - if err != nil { - log.Println("Could not change working directory to /: " + err.Error()) - } - - // Apply security restrictions - err = enableSecurityRestrictions(config, privInfo) - if err != nil { - log.Println("Exiting due to failure to apply security restrictions.") - return 1 - } - - // Create TLS listener - listener, err := tls.Listen("tcp", ":"+strconv.Itoa(config.Port), &tlscfg) - if err != nil { - log.Println("Error creating TLS listener: " + err.Error()) - return 1 - } - defer listener.Close() - - // Start log handling routines - var accessLogEntries chan LogEntry - if config.AccessLog == "" { - accessLogEntries = nil - } else { - accessLogEntries := make(chan LogEntry, 10) - go func() { - for { - entry := <-accessLogEntries - writeLogEntry(accessLogFile, entry) - } - }() - } - - // Start listening for signals - shutdown := make(chan struct{}) - sigterm := make(chan os.Signal, 1) - signal.Notify(sigterm, syscall.SIGTERM) - go func() { - <-sigterm - log.Println("Caught SIGTERM. Waiting for handlers to finish...") - close(shutdown) - listener.Close() - }() - - // Infinite serve loop (SIGTERM breaks out) - running := true - var wg sync.WaitGroup - for running { - conn, err := listener.Accept() - if err == nil { - wg.Add(1) - go handleGeminiRequest(conn, config, accessLogEntries, &wg) - } else { - select { - case <-shutdown: - running = false - default: - log.Println("Error accepting connection: " + err.Error()) - } - } - } - // Wait for still-running handler Go routines to finish - wg.Wait() - log.Println("Exiting.") - - // Exit successfully - return 0 + var dummy userInfo + os.Exit(launch(config, dummy)) } diff --git a/main_unix.go b/main_unix.go new file mode 100644 index 0000000..0a25e23 --- /dev/null +++ b/main_unix.go @@ -0,0 +1,55 @@ +// +build aix darwin dragonfly freebsd illumos linux netbsd openbsd solaris + +package main + +import ( + "flag" + "fmt" + "log" + "os" + "syscall" +) + +func main() { + var conf_file string + var chroot string + var user string + var version bool + + // Parse args + flag.StringVar(&conf_file, "c", "/etc/molly.conf", "Path to config file") + flag.StringVar(&chroot, "C", "", "Path to chroot into") + flag.StringVar(&user, "u", "nobody", "Unprivileged user") + flag.BoolVar(&version, "v", false, "Print version and exit") + flag.Parse() + + // If requested, print version and exit + if version { + fmt.Println("Molly Brown version", VERSION) + os.Exit(0) + } + + // Read config + config, err := getConfig(conf_file) + if err != nil { + log.Fatal(err) + } + + // Read user info + privInfo, err := getUserInfo(user) + + // Chroot, if asked + if chroot != "" { + err := syscall.Chroot(chroot) + if err == nil { + err = os.Chdir("/") + } + if err != nil { + log.Println("Could not chroot to " + chroot + ": " + err.Error()) + os.Exit(1) + } + } + + // Run server and exit + os.Exit(launch(config, privInfo)) +} diff --git a/security.go b/security.go index 7b1a2c0..2bd11f1 100644 --- a/security.go +++ b/security.go @@ -2,9 +2,13 @@ package main +type userInfo struct { +} + // Restrict access to the files specified in config in an OS-dependent way. // This is intended to be called immediately prior to accepting client // connections and may be used to establish a security "jail" for the molly // brown executable. func enableSecurityRestrictions(config Config, ui userInfo) error { + return nil } diff --git a/security_dropprivs.go b/security_dropprivs.go index 8aa4204..8e6aee5 100644 --- a/security_dropprivs.go +++ b/security_dropprivs.go @@ -26,7 +26,7 @@ type userInfo struct { unpriv_gid int } -func getUserInfo(config Config) (userInfo, error) { +func getUserInfo(unprivUser string) (userInfo, error) { var ui userInfo ui.uid = os.Getuid() ui.euid = os.Geteuid() @@ -54,15 +54,15 @@ func getUserInfo(config Config) (userInfo, error) { ui.need_drop = ui.is_setuid || ui.is_setgid || ui.root_user || ui.root_prim_group || ui.root_supp_group if ui.root_user || ui.root_prim_group { - nobody_user, err := user.Lookup(config.UnprivUsername) + nobody_user, err := user.Lookup(unprivUser) if err != nil { - log.Println("Running as root but could not lookup UID for user " + config.UnprivUsername + ": " + err.Error()) + log.Println("Running as root but could not lookup UID for user " + unprivUser + ": " + err.Error()) return ui, err } ui.unpriv_uid, err = strconv.Atoi(nobody_user.Uid) ui.unpriv_gid, err = strconv.Atoi(nobody_user.Gid) if err != nil { - log.Println("Running as root but could not lookup UID for user " + config.UnprivUsername + ": " + err.Error()) + log.Println("Running as root but could not lookup UID for user " + unprivUser + ": " + err.Error()) return ui, err } }