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...
This commit is contained in:
Solderpunk 2023-02-23 18:49:15 +01:00
parent 8d1a04cb27
commit 212c9f79fb
6 changed files with 222 additions and 182 deletions

View File

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

134
launch.go Normal file
View File

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

158
main.go
View File

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

55
main_unix.go Normal file
View File

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

View File

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

View File

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