author | Alberto Bertogli
<albertito@blitiri.com.ar> 2020-06-06 01:31:17 UTC |
committer | Alberto Bertogli
<albertito@blitiri.com.ar> 2020-06-06 01:31:17 UTC |
parent | a50e209dc92e0aa0f96d631f62a1209c28ef6921 |
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)