From 7006d7e6a414e03bee0f8f54e68dd6b6637f4676 Mon Sep 17 00:00:00 2001 From: Hedy Li Date: Mon, 19 Jul 2021 12:58:41 +0800 Subject: [PATCH] config, userdirs, refactor --- config.go | 62 +++++++++++++++++++++++++++ dirlist.go | 13 +++--- go.mod | 5 +++ go.sum | 4 ++ spsrv.go | 124 ++++++++++++++++++++++++++++++++++++----------------- 5 files changed, 162 insertions(+), 46 deletions(-) create mode 100644 config.go create mode 100644 go.sum diff --git a/config.go b/config.go new file mode 100644 index 0000000..a1fb371 --- /dev/null +++ b/config.go @@ -0,0 +1,62 @@ +package main + +import ( + "github.com/BurntSushi/toml" + "io/ioutil" + "os" + "fmt" +) + +type Config struct { + Port int + Hostname string + RootDir string + UserDirEnable bool + UserDir string + // UserSlug string + DirlistReverse bool + DirlistSort string + DirlistTitles bool +} + +var defaultConf = &Config{ + Port: 300, + Hostname: "localhost", + RootDir: "/var/spartan/", + DirlistReverse: false, + DirlistSort: "name", + DirlistTitles: true, + UserDirEnable: false, + UserDir: "public_spartan", +} + +func LoadConfig(path string) (*Config, error) { + var err error + var conf Config + // Defaults + conf = *defaultConf + + _, err = os.Stat(path) + if os.IsNotExist(err) { + fmt.Println(path, "does not exist, using default configuration values") + return &conf, nil + } + f, err := os.Open(path) + if err == nil { + defer f.Close() + contents, err := ioutil.ReadAll(f) + if err != nil { + return nil, err + } + if _, err = toml.Decode(string(contents), &conf); err != nil { + return nil, err + } + } + + if conf.DirlistSort != "name" && conf.DirlistSort != "time" && conf.DirlistSort != "size" { + fmt.Println("Warning: DirlistSort config option is not one of name/time/size, defaulting to name.") + conf.DirlistSort = "name" + } + + return &conf, nil +} diff --git a/dirlist.go b/dirlist.go index e45bf42..03af349 100644 --- a/dirlist.go +++ b/dirlist.go @@ -11,9 +11,9 @@ import ( "strings" ) -func generateDirectoryListing(reqPath, path string) ([]byte, error) { - dirSort := "time" - dirReverse := false +func generateDirectoryListing(reqPath, path string, conf *Config) ([]byte, error) { + dirSort := conf.DirlistSort + dirReverse := conf.DirlistReverse var listing string files, err := ioutil.ReadDir(path) if err != nil { @@ -22,6 +22,7 @@ func generateDirectoryListing(reqPath, path string) ([]byte, error) { listing = "# Directory listing\n\n" // TODO: custom dirlist header in config // Do "up" link first + reqPath = strings.ReplaceAll(reqPath, "/.", "") if reqPath != "/" { if strings.HasSuffix(reqPath, "/") { reqPath = reqPath[:len(reqPath)-1] @@ -59,13 +60,13 @@ func generateDirectoryListing(reqPath, path string) ([]byte, error) { if file.IsDir() { relativeUrl += "/" } - listing += fmt.Sprintf("=> %s %s\n", relativeUrl, generatePrettyFileLabel(file, path)) + listing += fmt.Sprintf("=> %s %s\n", relativeUrl, generatePrettyFileLabel(file, path, conf)) } return []byte(listing), nil } -func generatePrettyFileLabel(info os.FileInfo, path string) string { - dirTitles := true // TODO: config +func generatePrettyFileLabel(info os.FileInfo, path string, conf *Config) string { + dirTitles := conf.DirlistTitles var size string if info.IsDir() { size = " " diff --git a/go.mod b/go.mod index 140736d..b924d5d 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,8 @@ module spsrv go 1.15 + +require ( + github.com/BurntSushi/toml v0.3.1 + github.com/spf13/pflag v1.0.5 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..33499b6 --- /dev/null +++ b/go.sum @@ -0,0 +1,4 @@ +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= diff --git a/spsrv.go b/spsrv.go index c43aec1..18d5fc4 100644 --- a/spsrv.go +++ b/spsrv.go @@ -3,7 +3,6 @@ package main import ( "bufio" "errors" - "flag" "fmt" "io" "io/ioutil" @@ -14,6 +13,8 @@ import ( "path/filepath" "strconv" "strings" + + flag "github.com/spf13/pflag" ) const ( @@ -24,38 +25,58 @@ const ( ) var ( - hostname = flag.String("h", "localhost", "hostname") - contentDir = flag.String("d", "/var/spartan", "content directory") - port = flag.Int("p", 300, "port number") + hostname = flag.StringP("hostname", "h", defaultConf.Hostname, "Hostname") + port = flag.IntP("port", "p", defaultConf.Port, "Port to listen to") + rootDir = flag.StringP("dir", "d", defaultConf.RootDir, "Root content directory") + confPath = flag.StringP("config", "c", "/etc/spsrv.conf", "Path to config file") ) func main() { flag.Parse() + conf, err := LoadConfig(*confPath) + if err != nil { + fmt.Println("Error loading config") + fmt.Println(err.Error()) + return + } - listener, err := net.Listen("tcp", fmt.Sprintf(":%d", *port)) + // This allows users overriding values in config via the CLI + if *hostname != defaultConf.Hostname { + conf.Hostname = *hostname + } + if *port != defaultConf.Port { + conf.Port = *port + } + if *rootDir != defaultConf.RootDir { + conf.RootDir = *rootDir + } + + // TODO: do something with conf.Hostname (b(like restricting to ipv4/6 etc) + listener, err := net.Listen("tcp", fmt.Sprintf(":%d", conf.Port)) if err != nil { log.Fatalf("Unable to listen: %s", err) } log.Println("✨ You are now running on spsrv ✨") - log.Printf("Listening for connections on port: %d", *port) + log.Printf("Listening for connections on port: %d", conf.Port) - serveSpartan(listener) + serveSpartan(listener, conf) } // serveSpartan accepts connections and returns content -func serveSpartan(listener net.Listener) { +func serveSpartan(listener net.Listener, conf *Config) { for { + // Blocking until request received conn, err := listener.Accept() if err != nil { continue } - log.Println("Accepted connection") - go handleConnection(conn) + log.Println("Accepted connection from", conn.RemoteAddr()) + go handleConnection(conn, conf) } } -// handleConnection handles a request and does the reponse -func handleConnection(conn io.ReadWriteCloser) { +// handleConnection handles a request and does the response +func handleConnection(conn io.ReadWriteCloser, conf *Config) { defer conn.Close() // Check the size of the request buffer. @@ -73,59 +94,82 @@ func handleConnection(conn io.ReadWriteCloser) { // Parse incoming request URL. request := s.Text() - path, _, err := parseRequest(request) + reqPath, _, err := parseRequest(request) if err != nil { sendResponseHeader(conn, statusClientError, "Bad request") return } log.Println("Handling request:", request) + if strings.Contains(reqPath, ".."){ + sendResponseHeader(conn, statusClientError, "Stop it with your directory traversal technique!") + return + } // Time to fetch the files! - serveFile(conn, path) + path := resolvePath(reqPath, conf) + serveFile(conn, reqPath, path, conf) log.Println("Closed connection") } -// serveFile serves opens the requested path and returns the file content -func serveFile(conn io.ReadWriteCloser, reqPath string) { +func resolvePath(reqPath string, conf *Config) (path string) { + // Handle tildes + if strings.HasPrefix(reqPath, "/~") { + bits := strings.Split(reqPath, "/") + username := bits[1][1:] + new_prefix := filepath.Join("/home/", username, conf.UserDir) + path = filepath.Clean(strings.Replace(reqPath, bits[1], new_prefix, 1)) + if strings.HasSuffix(reqPath, "/") { + path = filepath.Join(path, "index.gmi") + } + return + } + path = reqPath // TODO: [config] default index file for a directory is index.gmi - path := reqPath if strings.HasSuffix(reqPath, "/") || reqPath == "" { path = filepath.Join(reqPath, "index.gmi") } - cleanPath := filepath.Clean(path) + path = filepath.Clean(filepath.Join(conf.RootDir, path)) + return +} +// serveFile serves opens the requested path and returns the file content +func serveFile(conn io.ReadWriteCloser, reqPath, path string, conf *Config) { // If the content directory is not specified as an absolute path, make it absolute. - prefixDir := "" - var rootDir http.Dir - if !strings.HasPrefix(*contentDir, "/") { - prefixDir, _ = os.Getwd() - } + // prefixDir := "" + // var rootDir http.Dir + // if !strings.HasPrefix(conf.RootDir, "/") { + // prefixDir, _ = os.Getwd() + // } // Avoid directory traversal type attacks. - rootDir = http.Dir(prefixDir + strings.Replace(*contentDir, ".", "", -1)) + // rootDir = http.Dir(prefixDir + strings.Replace(conf.RootDir, ".", "", -1)) // Open the requested resource. var content []byte - log.Printf("Fetching: %s", cleanPath) - f, err := rootDir.Open(cleanPath) + log.Printf("Fetching: %s", path) + f, err := os.Open(path) if err != nil { // not putting the /folder to /folder/ redirect here because folder can still // be opened without errors // Directory listing - if strings.HasSuffix(cleanPath, "index.gmi") { - fullPath := filepath.Join(fmt.Sprint(rootDir), cleanPath) + if strings.HasSuffix(path, "index.gmi") { + // fullPath := filepath.Join(fmt.Sprint(rootDir), path) + fullPath := path if _, err := os.Stat(fullPath); os.IsNotExist(err) { // If and only if the path is index.gmi AND index.gmi does not exist fullPath = strings.TrimSuffix(fullPath, "index.gmi") - log.Println("Generating directory listing:", fullPath) - content, err = generateDirectoryListing(reqPath, fullPath) - if err != nil { - log.Println(err) - sendResponseHeader(conn, statusServerError, "Error generating directory listing") + if _, err := os.Stat(fullPath); err == nil { + // If the directly exists + log.Println("Generating directory listing:", fullPath) + content, err = generateDirectoryListing(reqPath, fullPath, conf) + if err != nil { + log.Println(err) + sendResponseHeader(conn, statusServerError, "Error generating directory listing") + return + } + path += ".gmi" // OOF, this is just to have the text/gemini meta later lol + serveContent(conn, content, path) return } - cleanPath += ".gmi" // OOF, this is just to have the text/gemini meta later lol - serveContent(conn, content, cleanPath) - return } } log.Println(err) @@ -141,16 +185,16 @@ func serveFile(conn io.ReadWriteCloser, reqPath string) { // I wish I could check if err is a "path/to/dir" is a directory error // but I couldn't figure out how, so this check below is the best I // can come up with I guess - if _, err := os.Stat(filepath.Join(fmt.Sprint(rootDir), cleanPath+"/")); !os.IsNotExist(err) { - log.Println("Redirecting", cleanPath, "to", cleanPath+"/") - sendResponseHeader(conn, statusRedirect, cleanPath+"/") + if _, err := os.Stat(path + "/"); !os.IsNotExist(err) { + log.Println("Redirecting", path, "to", reqPath+"/") + sendResponseHeader(conn, statusRedirect, reqPath+"/") return } log.Println(err) sendResponseHeader(conn, statusServerError, "Resource could not be read") return } - serveContent(conn, content, cleanPath) + serveContent(conn, content, path) } func serveContent(conn io.ReadWriteCloser, content []byte, path string) {