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 } }