git » gofer » commit fc83ed6

Support dir options

author Alberto Bertogli
2020-06-06 01:31:17 UTC
committer Alberto Bertogli
2020-06-06 01:31:17 UTC
parent a50e209dc92e0aa0f96d631f62a1209c28ef6921

Support dir options

This patch adds support for configuring directory options, to
selectively enable/disable listing, and exclude files.

config/config.go +28 -0
server/dir.go +104 -0
server/http.go +8 -8
test/01-be.yaml +10 -1
test/test.sh +12 -0
test/testdata/dir/ignored.file +1 -0
test/testdata/dir/withindex/index.html +5 -0
test/testdata/dir/withoutindex/chau +1 -0
test/util/exp.go +12 -0

diff --git a/config/config.go b/config/config.go
index 7b9f432..6f715df 100644
--- a/config/config.go
+++ b/config/config.go
@@ -4,6 +4,7 @@ package config
 import (
 	"fmt"
 	"io/ioutil"
+	"regexp"
 
 	"gopkg.in/yaml.v3"
 )
@@ -23,6 +24,8 @@ type HTTP struct {
 	Static   map[string]string
 	Redirect map[string]string
 	CGI      map[string]string
+
+	DirOpts map[string]DirOpts
 }
 
 type HTTPS struct {
@@ -30,6 +33,11 @@ type HTTPS struct {
 	Certs string
 }
 
+type DirOpts struct {
+	Listing map[string]bool
+	Exclude []Regexp
+}
+
 type Raw struct {
 	Addr  string
 	Certs string
@@ -63,3 +71,23 @@ func LoadString(contents string) (*Config, error) {
 	err := yaml.Unmarshal([]byte(contents), conf)
 	return conf, err
 }
+
+// Wrapper to simplify regexp in configuration.
+type Regexp struct {
+	*regexp.Regexp
+}
+
+func (re *Regexp) UnmarshalYAML(unmarshal func(interface{}) error) error {
+	var s string
+	if err := unmarshal(&s); err != nil {
+		return err
+	}
+
+	rx, err := regexp.Compile("^(?:" + s + ")$")
+	if err != nil {
+		return err
+	}
+
+	re.Regexp = rx
+	return nil
+}
diff --git a/server/dir.go b/server/dir.go
new file mode 100644
index 0000000..bb809b7
--- /dev/null
+++ b/server/dir.go
@@ -0,0 +1,104 @@
+package server
+
+import (
+	"net/http"
+	"os"
+	"path/filepath"
+	"strings"
+
+	"blitiri.com.ar/go/gofer/config"
+)
+
+type FileSystem struct {
+	fs http.FileSystem
+
+	opts config.DirOpts
+}
+
+func NewFS(fs http.FileSystem, opts config.DirOpts) *FileSystem {
+	return &FileSystem{
+		fs:   fs,
+		opts: opts,
+	}
+}
+
+func ListingEnabled(opts *config.DirOpts, name string) bool {
+	if name == "" {
+		name = "/"
+	}
+	name = filepath.Clean(name)
+
+	longestP := ""
+	value := false
+	for p, val := range opts.Listing {
+		p = filepath.Clean(p)
+		if strings.HasPrefix(name, p) && len(p) > len(longestP) {
+			longestP = p
+			value = val
+		}
+	}
+
+	return value
+}
+
+func (fs *FileSystem) Open(name string) (http.File, error) {
+	for _, re := range fs.opts.Exclude {
+		if re.MatchString(name) {
+			return nil, os.ErrNotExist
+		}
+	}
+
+	f, err := fs.fs.Open(name)
+	if err != nil {
+		return nil, err
+	}
+
+	f = wrappedFile{File: f, name: name, opts: &fs.opts}
+
+	if ListingEnabled(&fs.opts, name) {
+		return f, nil
+	}
+
+	// If it's not a directory, let it be.
+	if s, _ := f.Stat(); s == nil || !s.IsDir() {
+		return f, nil
+	}
+
+	// It's a directory, and listing not allowed.
+	// However, if there is an index.html, we let it be served.
+	index := filepath.Join(name, "index.html")
+	if idxf, err := fs.fs.Open(index); err == nil {
+		idxf.Close()
+		return f, err
+	}
+
+	f.Close()
+	return nil, os.ErrNotExist
+}
+
+type wrappedFile struct {
+	http.File
+	name string
+	opts *config.DirOpts
+}
+
+func (f wrappedFile) Readdir(count int) ([]os.FileInfo, error) {
+	if !ListingEnabled(f.opts, f.name) {
+		return nil, os.ErrNotExist
+	}
+
+	// Exclude files from listings too.
+	all, err := f.File.Readdir(count)
+	var fis []os.FileInfo
+outer:
+	for _, fi := range all {
+		for _, re := range f.opts.Exclude {
+			name := filepath.Join(f.name, fi.Name())
+			if re.MatchString(name) {
+				continue outer
+			}
+		}
+		fis = append(fis, fi)
+	}
+	return fis, err
+}
diff --git a/server/http.go b/server/http.go
index b120122..61c8693 100644
--- a/server/http.go
+++ b/server/http.go
@@ -39,7 +39,7 @@ func httpServer(addr string, conf config.HTTP) *http.Server {
 	routes := []struct {
 		name        string
 		table       map[string]string
-		makeHandler func(string, url.URL) http.Handler
+		makeHandler func(string, url.URL, *config.HTTP) http.Handler
 	}{
 		{"proxy", conf.Proxy, makeProxy},
 		{"dir", conf.Dir, makeDir},
@@ -56,7 +56,7 @@ func httpServer(addr string, conf config.HTTP) *http.Server {
 					r.name, from, to, err)
 			}
 			log.Infof("%s route %q -> %s %q", srv.Addr, from, r.name, toURL)
-			mux.Handle(from, r.makeHandler(from, *toURL))
+			mux.Handle(from, r.makeHandler(from, *toURL, &conf))
 		}
 	}
 
@@ -99,7 +99,7 @@ func HTTPS(addr string, conf config.HTTPS) {
 	log.Fatalf("%s https proxy exited: %v", addr, err)
 }
 
-func makeProxy(from string, to url.URL) http.Handler {
+func makeProxy(from string, to url.URL, conf *config.HTTP) http.Handler {
 	proxy := &httputil.ReverseProxy{}
 	proxy.Transport = transport
 
@@ -203,11 +203,11 @@ func pathOrOpaque(u url.URL) string {
 	return u.Opaque
 }
 
-func makeDir(from string, to url.URL) http.Handler {
+func makeDir(from string, to url.URL, conf *config.HTTP) http.Handler {
 	from = stripDomain(from)
 	path := pathOrOpaque(to)
 
-	fs := http.FileServer(http.Dir(path))
+	fs := http.FileServer(NewFS(http.Dir(path), conf.DirOpts[from]))
 	return WithLogging("http:dir",
 		http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 			tr, _ := trace.FromContext(r.Context())
@@ -223,7 +223,7 @@ func makeDir(from string, to url.URL) http.Handler {
 	)
 }
 
-func makeStatic(from string, to url.URL) http.Handler {
+func makeStatic(from string, to url.URL, conf *config.HTTP) http.Handler {
 	path := pathOrOpaque(to)
 
 	return WithLogging("http:static",
@@ -235,7 +235,7 @@ func makeStatic(from string, to url.URL) http.Handler {
 	)
 }
 
-func makeCGI(from string, to url.URL) http.Handler {
+func makeCGI(from string, to url.URL, conf *config.HTTP) http.Handler {
 	from = stripDomain(from)
 	path := pathOrOpaque(to)
 	args := queryToArgs(to.RawQuery)
@@ -274,7 +274,7 @@ func queryToArgs(query string) []string {
 	return args
 }
 
-func makeRedirect(from string, to url.URL) http.Handler {
+func makeRedirect(from string, to url.URL, conf *config.HTTP) http.Handler {
 	from = stripDomain(from)
 
 	return WithLogging("http:redirect",
diff --git a/test/01-be.yaml b/test/01-be.yaml
index 1a25b0d..5b12a06 100644
--- a/test/01-be.yaml
+++ b/test/01-be.yaml
@@ -4,9 +4,18 @@ control_addr: "127.0.0.1:8459"
 http:
   ":8450":
     dir:
-      "/dir": "testdata/dir"
+      "/dir/": "testdata/dir"
     static:
       "/file": "testdata/file"
       "/file/second": "testdata/dir/ñaca"
     cgi:
       "/cgi": "testdata/cgi.sh?param 1;param 2"
+
+    diropts:
+      "/dir/":
+        listing:
+          "/": true
+          "/withindex/": false
+          "/withoutindex/": false
+        exclude: ["/ignored\\..*"]
+
diff --git a/test/test.sh b/test/test.sh
index eb20ca3..1ae0321 100755
--- a/test/test.sh
+++ b/test/test.sh
@@ -111,9 +111,21 @@ do
 	exp $base/file -body "ñaca\n"
 
 	exp $base/dir -status 301 -redir /dir/
+
 	exp $base/dir/ -bodyre '<a href="%C3%B1aca">ñaca</a>'
+	exp $base/dir/ -bodyre '>withindex/<'
+	exp $base/dir/ -bodyre '>withoutindex/<'
+	exp $base/dir/ -bodynotre 'ignored'
+
 	exp $base/dir/hola -body 'hola marola\n'
 	exp $base/dir/ñaca -body "tracañaca\n"
+	exp $base/dir/ignored.file -status 404
+
+	exp $base/dir/withindex -status 301 -redir withindex/
+	exp $base/dir/withindex/ -bodyre 'This is the index.'
+	exp $base/dir/withoutindex -status 404
+	exp $base/dir/withoutindex/ -status 404
+	exp $base/dir/withoutindex/chau -body 'chau\n'
 
 	exp $base/cgi/ -bodyre '"param 1" "param 2"'
 	exp "$base/cgi/?cucu=melo&a;b" -bodyre 'QUERY_STRING=cucu=melo&a;b\n'
diff --git a/test/testdata/dir/ignored.file b/test/testdata/dir/ignored.file
new file mode 100644
index 0000000..ea75411
--- /dev/null
+++ b/test/testdata/dir/ignored.file
@@ -0,0 +1 @@
+this file should be ignored
diff --git a/test/testdata/dir/withindex/index.html b/test/testdata/dir/withindex/index.html
new file mode 100644
index 0000000..dc8d25a
--- /dev/null
+++ b/test/testdata/dir/withindex/index.html
@@ -0,0 +1,5 @@
+<html>
+<body>
+This is the index.
+</body>
+</html>
diff --git a/test/testdata/dir/withoutindex/chau b/test/testdata/dir/withoutindex/chau
new file mode 100644
index 0000000..e87bc76
--- /dev/null
+++ b/test/testdata/dir/withoutindex/chau
@@ -0,0 +1 @@
+chau
diff --git a/test/util/exp.go b/test/util/exp.go
index 530a0ce..37deedc 100644
--- a/test/util/exp.go
+++ b/test/util/exp.go
@@ -26,6 +26,8 @@ func main() {
 			"expect body with these exact contents")
 		bodyRE = flag.String("bodyre", "",
 			"expect body matching these contents (regexp match)")
+		bodyNotRE = flag.String("bodynotre", "",
+			"expect body NOT matching these contents (regexp match)")
 		redir = flag.String("redir", "",
 			"expect a redirect to this URL")
 		status = flag.Int("status", 200,
@@ -90,6 +92,16 @@ func main() {
 		}
 	}
 
+	if *bodyNotRE != "" {
+		matched, err := regexp.Match(*bodyNotRE, rbody)
+		if err != nil {
+			errorf("regexp error: %q", err)
+		}
+		if matched {
+			errorf("body matched regexp: %q\n", rbody)
+		}
+	}
+
 	if *redir != "" {
 		if loc := resp.Header.Get("Location"); loc != *redir {
 			errorf("unexpected redir location: %q\n", loc)