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:
parent
8d1a04cb27
commit
212c9f79fb
6 changed files with 222 additions and 182 deletions
39
config.go
39
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,31 +92,29 @@ 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
|
||||
}
|
||||
_, 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)
|
||||
config.CertPath, err = filepath.Abs(config.CertPath)
|
||||
if err != nil {
|
||||
return config, err
|
||||
}
|
||||
if config.ChrootDir != "" {
|
||||
config.DocBase, err = filepath.Rel(config.ChrootDir, abs)
|
||||
config.KeyPath, err = filepath.Abs(config.KeyPath)
|
||||
if err != nil {
|
||||
return config, err
|
||||
}
|
||||
} else {
|
||||
config.DocBase = abs
|
||||
if config.AccessLog != "" && config.AccessLog != "-" {
|
||||
config.AccessLog, err = filepath.Abs(config.AccessLog)
|
||||
if err != nil {
|
||||
return config, err
|
||||
}
|
||||
}
|
||||
if config.ErrorLog != "" {
|
||||
config.ErrorLog, err = filepath.Abs(config.ErrorLog)
|
||||
if err != nil {
|
||||
return config, err
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
134
launch.go
Normal 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
158
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))
|
||||
}
|
||||
|
|
55
main_unix.go
Normal file
55
main_unix.go
Normal 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))
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue