From 38ff3807b3b97da22006b5bdcf03fdfaaa4b0582 Mon Sep 17 00:00:00 2001 From: tjpcc Date: Thu, 7 Sep 2023 12:36:17 -0600 Subject: all the gopher CGI handlers to support gophernicus behaviors --- contrib/cgi/cgi.go | 16 +- contrib/cgi/gopher.go | 139 +++++++++++++++- contrib/fs/gopher.go | 183 +++++++++++++-------- examples/gopher_fileserver/main.go | 11 +- gopher/gophermap/extended.go | 47 +++++- gopher/gophermap/extended_test.go | 6 +- gopher/gophermap/listdir.go | 91 +++++----- .../gophermap/testdata/customlist_output.gophermap | 4 +- gopher/gophermap/testdata/file4 | 0 gopher/gophermap/testdata/file4.txt | 0 gopher/gophermap/testdata/subdir/gophertag | 1 + gopher/gophermap/testdata/subdir2/gophermap | 1 + gopher/response.go | 2 +- internal/users.go | 53 ++++++ 14 files changed, 396 insertions(+), 158 deletions(-) delete mode 100644 gopher/gophermap/testdata/file4 create mode 100644 gopher/gophermap/testdata/file4.txt create mode 100644 gopher/gophermap/testdata/subdir/gophertag create mode 100644 gopher/gophermap/testdata/subdir2/gophermap create mode 100644 internal/users.go diff --git a/contrib/cgi/cgi.go b/contrib/cgi/cgi.go index bcdd5e1..749a284 100644 --- a/contrib/cgi/cgi.go +++ b/contrib/cgi/cgi.go @@ -25,11 +25,11 @@ import ( // a request for /foo/bar/baz can run an executable found at /foo or /foo/bar. In such // a case the PATH_INFO would include the remaining portion of the URI path. func ResolveCGI(requestPath, fsRoot string) (string, string, error) { + fsRoot = strings.TrimRight(fsRoot, "/") segments := strings.Split(strings.TrimLeft(requestPath, "/"), "/") for i := range append(segments, "") { filepath := strings.Join(append([]string{fsRoot}, segments[:i]...), "/") - filepath = strings.TrimRight(filepath, "/") isDir, isExecutable, err := executableFile(filepath) if err != nil { return "", "", err @@ -52,26 +52,20 @@ func ResolveCGI(requestPath, fsRoot string) (string, string, error) { } func executableFile(filepath string) (bool, bool, error) { - file, err := os.Open(filepath) + info, err := os.Stat(filepath) if isNotExistError(err) { return false, false, nil } if err != nil { return false, false, err } - defer file.Close() - - info, err := file.Stat() - if err != nil { - return false, false, err - } if info.IsDir() { return true, false, nil } // readable + executable by anyone - return false, info.Mode()&0005 == 0005, nil + return false, info.Mode()&5 == 5, nil } func isNotExistError(err error) bool { @@ -94,7 +88,7 @@ func RunCGI( request *sr.Request, executable string, pathInfo string, -) (io.Reader, int, error) { +) (*bytes.Buffer, int, error) { pathSegments := strings.Split(executable, "/") dirPath := "." @@ -105,7 +99,7 @@ func RunCGI( infoLen := len(pathInfo) if pathInfo == "/" { - infoLen -= 1 + infoLen = 0 } scriptName := request.Path[:len(request.Path)-infoLen] diff --git a/contrib/cgi/gopher.go b/contrib/cgi/gopher.go index 290adfa..98a3c75 100644 --- a/contrib/cgi/gopher.go +++ b/contrib/cgi/gopher.go @@ -3,10 +3,14 @@ package cgi import ( "context" "fmt" + "os" + "path" + "path/filepath" "strings" sr "tildegit.org/tjp/sliderule" "tildegit.org/tjp/sliderule/gopher" + "tildegit.org/tjp/sliderule/gopher/gophermap" ) // GopherCGIDirectory runs any executable files relative to a root directory on the file system. @@ -15,31 +19,148 @@ import ( // a request for /foo/bar/baz can also run an executable found at /foo or /foo/bar. In // such a case the PATH_INFO environment variable will include the remaining portion of // the URI path. -func GopherCGIDirectory(pathRoot, fsRoot string) sr.Handler { +func GopherCGIDirectory(pathRoot, fsRoot string, settings *gophermap.FileSystemSettings) sr.Handler { + if settings == nil { + settings = &gophermap.FileSystemSettings{} + } + + if !settings.Exec { + return sr.HandlerFunc(func(ctx context.Context, request *sr.Request) *sr.Response { return nil }) + } + fsRoot = strings.TrimRight(fsRoot, "/") return sr.HandlerFunc(func(ctx context.Context, request *sr.Request) *sr.Response { if !strings.HasPrefix(request.Path, pathRoot) { return nil } - filepath, pathinfo, err := ResolveCGI(request.Path[len(pathRoot):], fsRoot) + fullpath, pathinfo, err := resolveGopherCGI(fsRoot, request) if err != nil { return gopher.Error(err).Response() } - if filepath == "" { + if fullpath == "" { + return nil + } + + return runGopherCGI(ctx, request, fullpath, pathinfo, *settings) + }) +} + +// ExecGopherMaps runs any gophermaps +func ExecGopherMaps(pathRoot, fsRoot string, settings *gophermap.FileSystemSettings) sr.Handler { + if settings == nil { + settings = &gophermap.FileSystemSettings{} + } + + if !settings.Exec { + return sr.HandlerFunc(func(ctx context.Context, request *sr.Request) *sr.Response { return nil }) + } + + fsRoot = strings.TrimRight(fsRoot, "/") + return sr.HandlerFunc(func(ctx context.Context, request *sr.Request) *sr.Response { + if !strings.HasPrefix(request.Path, pathRoot) { return nil } - stdout, exitCode, err := RunCGI(ctx, request, filepath, pathinfo) + fullpath := filepath.Join(fsRoot, strings.Trim(request.Path, "/")) + info, err := os.Stat(fullpath) + if isNotExistError(err) { + return nil + } if err != nil { return gopher.Error(err).Response() } - if exitCode != 0 { - return gopher.Error( - fmt.Errorf("CGI process exited with status %d", exitCode), - ).Response() + + if info.IsDir() { + for _, fname := range settings.DirMaps { + fpath := filepath.Join(fullpath, fname) + finfo, err := os.Stat(fpath) + if isNotExistError(err) { + continue + } + if err != nil { + return gopher.Error(err).Response() + } + + m := finfo.Mode() + if m.IsDir() { + continue + } + if !m.IsRegular() || m&5 != 5 { + continue + } + return runGopherCGI(ctx, request, fpath, "/", *settings) + } + + return nil + } + + m := info.Mode() + if !m.IsRegular() || m&5 != 5 { + return nil } - return gopher.File(0, stdout) + return runGopherCGI(ctx, request, fullpath, "/", *settings) }) } + +func runGopherCGI( + ctx context.Context, + request *sr.Request, + fullpath string, + pathinfo string, + settings gophermap.FileSystemSettings, +) *sr.Response { + stdout, exitCode, err := RunCGI(ctx, request, fullpath, pathinfo) + if err != nil { + return gopher.Error(err).Response() + } + if exitCode != 0 { + return gopher.Error( + fmt.Errorf("CGI process exited with status %d", exitCode), + ).Response() + } + + if settings.ParseExtended { + edoc, err := gophermap.ParseExtended(stdout, request.URL) + if err != nil { + return gopher.Error(err).Response() + } + + doc, _, err := edoc.Compatible(filepath.Dir(fullpath), settings) + return doc.Response() + } + + return gopher.File(gopher.MenuType, stdout) +} + +func resolveGopherCGI(fsRoot string, request *sr.Request) (string, string, error) { + reqPath := strings.TrimLeft(request.Path, "/") + + segments := append([]string{""}, strings.Split(reqPath, "/")...) + fullpath := fsRoot + for i, segment := range segments { + fullpath = filepath.Join(fullpath, segment) + + info, err := os.Stat(fullpath) + if isNotExistError(err) { + return "", "", nil + } + if err != nil { + return "", "", err + } + + if !info.IsDir() { + if info.Mode()&5 == 5 { + pathinfo := "/" + if len(segments) > i+1 { + pathinfo = path.Join(segments[i:]...) + } + return fullpath, pathinfo, nil + } + break + } + } + + return "", "", nil +} diff --git a/contrib/fs/gopher.go b/contrib/fs/gopher.go index 0594730..4d86ba6 100644 --- a/contrib/fs/gopher.go +++ b/contrib/fs/gopher.go @@ -2,29 +2,56 @@ package fs import ( "context" - "io/fs" + "os" + "path/filepath" + "slices" "strings" - "text/template" sr "tildegit.org/tjp/sliderule" "tildegit.org/tjp/sliderule/gopher" + "tildegit.org/tjp/sliderule/gopher/gophermap" ) // GopherFileHandler builds a handler which serves up files from a file system. // // It only serves responses for paths which correspond to files, not directories. -func GopherFileHandler(fileSystem fs.FS) sr.Handler { +func GopherFileHandler(rootpath string, settings *gophermap.FileSystemSettings) sr.Handler { return sr.HandlerFunc(func(ctx context.Context, request *sr.Request) *sr.Response { - filepath, file, err := ResolveFile(request, fileSystem) + path := filepath.Join(rootpath, strings.Trim(request.Path, "/")) + if isPrivate(path) { + return nil + } + if isf, err := isFile(path); err != nil { + return gopher.Error(err).Response() + } else if !isf { + return nil + } + + if settings == nil { + settings = &gophermap.FileSystemSettings{} + } + + file, err := os.Open(path) if err != nil { return gopher.Error(err).Response() } - if file == nil { - return nil + if !(settings.ParseExtended && isMap(path, *settings)) { + return gopher.File(gopher.GuessItemType(path), file) + } + + defer func() { _ = file.Close() }() + + edoc, err := gophermap.ParseExtended(file, request.URL) + if err != nil { + return gopher.Error(err).Response() } - return gopher.File(gopher.GuessItemType(filepath), file) + doc, _, err := edoc.Compatible(filepath.Dir(path), *settings) + if err != nil { + return gopher.Error(err).Response() + } + return doc.Response() }) } @@ -34,95 +61,111 @@ func GopherFileHandler(fileSystem fs.FS) sr.Handler { // contents of that file is returned as the gopher response. // // It returns nil for any paths which don't correspond to a directory. -// -// It requires that files from the provided fs.FS implement fs.ReadDirFile. If -// they don't, it will produce nil responses for all directory paths. -func GopherDirectoryDefault(fileSystem fs.FS, filenames ...string) sr.Handler { +func GopherDirectoryDefault(rootpath string, settings *gophermap.FileSystemSettings) sr.Handler { return sr.HandlerFunc(func(ctx context.Context, request *sr.Request) *sr.Response { - dirpath, dir, err := ResolveDirectory(request, fileSystem) - if err != nil { - return gopher.Error(err).Response() + path := filepath.Join(rootpath, strings.Trim(request.Path, "/")) + if isPrivate(path) { + return nil } - if dir == nil { + if isd, err := isDir(path); err != nil { + return gopher.Error(err).Response() + } else if !isd { return nil } - defer func() { _ = dir.Close() }() - _, file, err := ResolveDirectoryDefault(fileSystem, dirpath, dir, filenames) - if err != nil { - return gopher.Error(err).Response() + if settings == nil { + settings = &gophermap.FileSystemSettings{} } - if file == nil { - return nil + + for _, fname := range settings.DirMaps { + fpath := filepath.Join(path, fname) + if isf, err := isFile(fpath); err != nil { + return gopher.Error(err).Response() + } else if !isf { + continue + } + + file, err := os.Open(fpath) + if err != nil { + return gopher.Error(err).Response() + } + + if settings.ParseExtended { + defer func() { _ = file.Close() }() + + edoc, err := gophermap.ParseExtended(file, request.URL) + if err != nil { + return gopher.Error(err).Response() + } + + doc, _, err := edoc.Compatible(path, *settings) + if err != nil { + return gopher.Error(err).Response() + } + return doc.Response() + } else { + return gopher.File(gopher.MenuType, file) + } } - return gopher.File(gopher.MenuType, file) + return nil }) } // GopherDirectoryListing produces a listing of the contents of any requested directories. // // It returns nil for any paths which don't correspond to a filesystem directory. -// -// It requires that files from the provided fs.FS implement fs.ReadDirFile. If they -// don't, it will produce nil responses for any directory paths. -// -// A template may be nil, in which case DefaultGopherDirectoryList is used instead. The -// template is then processed with RenderDirectoryListing. -func GopherDirectoryListing(fileSystem fs.FS, tpl *template.Template) sr.Handler { +func GopherDirectoryListing(rootpath string, settings *gophermap.FileSystemSettings) sr.Handler { return sr.HandlerFunc(func(ctx context.Context, request *sr.Request) *sr.Response { - dirpath, dir, err := ResolveDirectory(request, fileSystem) - if err != nil { - return gopher.Error(err).Response() + path := filepath.Join(rootpath, strings.Trim(request.Path, "/")) + if isPrivate(path) { + return nil } - if dir == nil { + if isd, err := isDir(path); err != nil { + return gopher.Error(err).Response() + } else if !isd { return nil } - defer func() { _ = dir.Close() }() - if tpl == nil { - tpl = DefaultGopherDirectoryList + if settings == nil { + settings = &gophermap.FileSystemSettings{} } - body, err := RenderDirectoryListing(dirpath, dir, tpl, request.Server) + doc, err := gophermap.ListDir(path, request.URL, *settings) if err != nil { return gopher.Error(err).Response() } - return gopher.File(gopher.MenuType, body) + return doc.Response() }) } -// GopherTemplateFunctions is a map for templates providing useful functions for gophermaps. -// -// - GuessItemType: return a gopher item type for a file based on its path/name. -var GopherTemplateFunctions = template.FuncMap{ - "GuessItemType": func(filepath string) string { - return string([]byte{byte(gopher.GuessItemType(filepath))}) - }, +func isDir(path string) (bool, error) { + info, err := os.Stat(path) + if err != nil { + if isNotFound(err) { + err = nil + } + return false, err + } + return info.IsDir() && info.Mode()&4 == 4, nil } -// DefaultGopherDirectoryList is a template which renders a directory listing as gophermap. -var DefaultGopherDirectoryList = template.Must( - template.New("gopher_dirlist").Funcs(GopherTemplateFunctions).Parse( - strings.ReplaceAll( - ` -{{ $root := .FullPath -}} -{{ if eq .FullPath "." }}{{ $root = "" }}{{ end -}} -{{ $hostname := .Hostname -}} -{{ $port := .Port -}} -i{{ .DirName }} {{ $hostname }} {{ $port }} -i {{ $hostname }} {{ $port }} -{{ range .Entries -}} -{{ if .IsDir -}} -1{{ .Name }} {{ $root }}/{{ .Name }} {{ $hostname }} {{ $port }} -{{- else -}} -{{ GuessItemType .Name }}{{ .Name }} {{ $root }}/{{ .Name }} {{ $hostname }} {{ $port }} -{{- end }} -{{ end -}} -. -`[1:], - "\n", - "\r\n", - ), - ), -) +func isFile(path string) (bool, error) { + info, err := os.Stat(path) + if err != nil { + if isNotFound(err) { + err = nil + } + return false, err + } + m := info.Mode() + + return m.IsRegular() && m&4 == 4, nil +} + +func isMap(path string, settings gophermap.FileSystemSettings) bool { + if strings.HasSuffix(path, ".gophermap") { + return true + } + return slices.Contains(settings.DirMaps, filepath.Base(path)) +} diff --git a/examples/gopher_fileserver/main.go b/examples/gopher_fileserver/main.go index 244f61b..057aa21 100644 --- a/examples/gopher_fileserver/main.go +++ b/examples/gopher_fileserver/main.go @@ -3,7 +3,6 @@ package main import ( "context" "log" - "os" sr "tildegit.org/tjp/sliderule" "tildegit.org/tjp/sliderule/contrib/cgi" @@ -13,13 +12,11 @@ import ( ) func main() { - fileSystem := os.DirFS(".") - handler := sr.FallthroughHandler( - fs.GopherDirectoryDefault(fileSystem, "index.gophermap"), - fs.GopherDirectoryListing(fileSystem, nil), - cgi.GopherCGIDirectory("/cgi-bin", "./cgi-bin"), - fs.GopherFileHandler(fileSystem), + fs.GopherDirectoryDefault(".", nil), + fs.GopherDirectoryListing(".", nil), + cgi.GopherCGIDirectory("/cgi-bin", "./cgi-bin", nil), + fs.GopherFileHandler(".", nil), ) _, infoLog, _, errLog := logging.DefaultLoggers() diff --git a/gopher/gophermap/extended.go b/gopher/gophermap/extended.go index d9fedd0..a0360fc 100644 --- a/gopher/gophermap/extended.go +++ b/gopher/gophermap/extended.go @@ -7,10 +7,12 @@ import ( "net/url" "os" "path/filepath" + "sort" "strconv" "strings" "tildegit.org/tjp/sliderule/gopher" + "tildegit.org/tjp/sliderule/internal" "tildegit.org/tjp/sliderule/internal/types" ) @@ -139,6 +141,8 @@ const ( UserListType types.Status = '~' // VHostListType generates a listing of virtual hosts. + // + // It is not supported in sliderule. VHostListType types.Status = '%' // InclusionType causes another gophermap to be included at this location. @@ -151,8 +155,16 @@ const ( EndDocType types.Status = '.' ) +type FileSystemSettings struct { + ParseExtended bool + Exec bool + ListUsers bool + DirMaps []string + DirTag string +} + // Compatible builds a standards-compliant gophermap from the current extended menu. -func (edoc ExtendedMapDocument) Compatible(cwd string) (gopher.MapDocument, string, error) { +func (edoc ExtendedMapDocument) Compatible(cwd string, settings FileSystemSettings) (gopher.MapDocument, string, error) { doc := gopher.MapDocument{} title := "" @@ -172,9 +184,32 @@ func (edoc ExtendedMapDocument) Compatible(cwd string) (gopher.MapDocument, stri return nil, "", InvalidLine(num) } extensions[from] = types.Status(to[0]) - case UserListType: //TODO - return nil, "", errors.New("User listings '~' are not supported") - case VHostListType: //TODO + case UserListType: + if !settings.ListUsers { + doc = append(doc, gopher.MapItem{ + Type: gopher.InfoMessageType, + Display: "~", + Selector: edoc.Location.Path, + Hostname: edoc.Location.Hostname(), + Port: edoc.Location.Port(), + }) + } + + users, err := internal.ListUsersWithHomeSubdir("public_gopher", 4) + if err != nil { + return nil, "", err + } + sort.Strings(users) + for _, user := range users { + doc = append(doc, gopher.MapItem{ + Type: gopher.MenuType, + Display: "~" + user, + Selector: "/~" + user, + Hostname: edoc.Location.Hostname(), + Port: edoc.Location.Port(), + }) + } + case VHostListType: return nil, "", errors.New("Virtual host listings '%' are not supported") case InclusionType: location := filepath.Join(cwd, item.Selector) @@ -183,13 +218,13 @@ func (edoc ExtendedMapDocument) Compatible(cwd string) (gopher.MapDocument, stri return nil, "", err } - lines, _, err := subEdoc.Compatible(filepath.Dir(location)) + lines, _, err := subEdoc.Compatible(filepath.Dir(location), settings) if err != nil { return nil, "", err } doc = append(doc, lines...) case DirListType: - dirlist, err := listDir(cwd, edoc.Location, hidden, extensions) + dirlist, err := listDir(cwd, edoc.Location, settings, hidden, extensions) if err != nil { return nil, "", err } diff --git a/gopher/gophermap/extended_test.go b/gopher/gophermap/extended_test.go index e956df1..2d9e9b1 100644 --- a/gopher/gophermap/extended_test.go +++ b/gopher/gophermap/extended_test.go @@ -31,7 +31,11 @@ func TestExtendedDoc(t *testing.T) { t.Fatal(err) } - doc, _, err := edoc.Compatible("testdata") + doc, _, err := edoc.Compatible("testdata", FileSystemSettings{ + ParseExtended: true, + DirMaps: []string{"gophermap"}, + DirTag: "gophertag", + }) if err != nil { t.Fatal(err) } diff --git a/gopher/gophermap/listdir.go b/gopher/gophermap/listdir.go index 8d66277..a2c5214 100644 --- a/gopher/gophermap/listdir.go +++ b/gopher/gophermap/listdir.go @@ -6,6 +6,7 @@ import ( "os" "path" "path/filepath" + "slices" "strings" "tildegit.org/tjp/sliderule/gopher" @@ -13,11 +14,11 @@ import ( ) // ListDir builds a gopher menu representing the contents of a directory. -func ListDir(dir string, location *url.URL) (gopher.MapDocument, error) { - return listDir(dir, location, nil, nil) +func ListDir(dir string, location *url.URL, settings FileSystemSettings) (gopher.MapDocument, error) { + return listDir(dir, location, settings, nil, nil) } -func listDir(dir string, location *url.URL, hidden map[string]struct{}, extensions map[string]types.Status) (gopher.MapDocument, error) { +func listDir(dir string, location *url.URL, settings FileSystemSettings, hidden map[string]struct{}, extensions map[string]types.Status) (gopher.MapDocument, error) { contents, err := os.ReadDir(dir) if err != nil { return nil, err @@ -28,55 +29,36 @@ func listDir(dir string, location *url.URL, hidden map[string]struct{}, extensio for _, entry := range contents { name := entry.Name() - if _, ok := hidden[name]; ok || name == "gophermap" { + inf, err := entry.Info() + if err != nil { + return nil, err + } + if inf.Mode()&4 == 0 { + continue + } + + if _, ok := hidden[name]; ok || slices.Contains(settings.DirMaps, name) { continue } var code types.Status - ext := strings.TrimPrefix(filepath.Ext(name), ".") if entry.IsDir() { code = gopher.MenuType - } else if c, ok := extensions[ext]; ok && ext != "" { - code = c } else { - switch ext { - case "gophermap": - code = gopher.MenuType - case "exe", "bin", "out": - code = gopher.BinaryFileType - case "gif": - code = gopher.GifFileType - case "jpg", "jpeg", "tif", "tiff": - code = gopher.ImageFileType - case "bmp": - code = gopher.BitmapType - case "mp4", "mov", "avi", "wmv", "webm": - code = gopher.MovieFileType - case "pcm", "aiff", "mp3", "aac", "ogg", "wma", "flac", "alac": - code = gopher.SoundFileType - case "doc", "docx", "odt", "fodt": - code = gopher.DocumentType - case "html": - code = gopher.HTMLType - case "png": - code = gopher.PngImageFileType - case "rtf": - code = gopher.RtfDocumentType - case "wav": - code = gopher.WavSoundFileType - case "pdf": - code = gopher.PdfDocumentType - case "xml", "atom": - code = gopher.XmlDocumentType - default: - code = gopher.TextFileType + ext := strings.TrimPrefix(filepath.Ext(name), ".") + if c, ok := extensions[ext]; ok { + code = c + } else if c, ok := extensions[name]; ok { + code = c + } else { + code = gopher.GuessItemType(name) } } doc = append(doc, gopher.MapItem{ Type: code, - Display: displayName(dir, entry), + Display: displayName(dir, entry, settings), Selector: path.Join(path.Dir(location.Path), name), Hostname: location.Hostname(), Port: location.Port(), @@ -86,32 +68,37 @@ func listDir(dir string, location *url.URL, hidden map[string]struct{}, extensio return doc, nil } -func displayName(dir string, entry os.DirEntry) string { +func displayName(dir string, entry os.DirEntry, settings FileSystemSettings) string { fname := entry.Name() + fullpath := filepath.Join(dir, fname) - // if is a gophermap, use !title or filename - if strings.HasSuffix(fname, ".gophermap") { - if title := gophermapTitle(dir, fname); title != "" { + if entry.Type().IsRegular() && settings.ParseExtended && (strings.HasSuffix(fname, ".gophermap") || slices.Contains(settings.DirMaps, fname)) { + if title := gophermapTitle(fullpath); title != "" { return title } - return fname } if entry.IsDir() { - if tag := tagTitle(dir, fname); tag != "" { - return tag + if settings.DirTag != "" { + if tag := tagTitle(filepath.Join(fullpath, settings.DirTag)); tag != "" { + return tag + } } - if title := gophermapTitle(dir, filepath.Join(fname, "gophermap")); title != "" { - return title + if settings.ParseExtended { + for _, mapname := range settings.DirMaps { + if title := gophermapTitle(filepath.Join(fullpath, mapname)); title != "" { + return title + } + } } } return fname } -func gophermapTitle(dir, name string) string { - file, err := os.Open(filepath.Join(dir, name)) +func gophermapTitle(path string) string { + file, err := os.Open(path) if err != nil { return "" } @@ -131,8 +118,8 @@ func gophermapTitle(dir, name string) string { return strings.TrimRight(line[1:], "\r\n") } -func tagTitle(parent, name string) string { - file, err := os.Open(filepath.Join(parent, name, "gophertag")) +func tagTitle(path string) string { + file, err := os.Open(path) if err != nil { return "" } diff --git a/gopher/gophermap/testdata/customlist_output.gophermap b/gopher/gophermap/testdata/customlist_output.gophermap index 1a96644..e5cc99d 100644 --- a/gopher/gophermap/testdata/customlist_output.gophermap +++ b/gopher/gophermap/testdata/customlist_output.gophermap @@ -8,5 +8,7 @@ i /customlist.gophermap localhost.localdomain 70 1This is the document title /customlist.gophermap localhost.localdomain 70 1customlist_output.gophermap /customlist_output.gophermap localhost.localdomain 70 9file3.executablething /file3.executablething localhost.localdomain 70 -0file4 /file4 localhost.localdomain 70 +0file4.txt /file4.txt localhost.localdomain 70 +1subdir title /subdir localhost.localdomain 70 +1subdir2 title /subdir2 localhost.localdomain 70 . diff --git a/gopher/gophermap/testdata/file4 b/gopher/gophermap/testdata/file4 deleted file mode 100644 index e69de29..0000000 diff --git a/gopher/gophermap/testdata/file4.txt b/gopher/gophermap/testdata/file4.txt new file mode 100644 index 0000000..e69de29 diff --git a/gopher/gophermap/testdata/subdir/gophertag b/gopher/gophermap/testdata/subdir/gophertag new file mode 100644 index 0000000..73a248e --- /dev/null +++ b/gopher/gophermap/testdata/subdir/gophertag @@ -0,0 +1 @@ +subdir title diff --git a/gopher/gophermap/testdata/subdir2/gophermap b/gopher/gophermap/testdata/subdir2/gophermap new file mode 100644 index 0000000..bba9538 --- /dev/null +++ b/gopher/gophermap/testdata/subdir2/gophermap @@ -0,0 +1 @@ +!subdir2 title diff --git a/gopher/response.go b/gopher/response.go index 1ad7f1d..46f9db0 100644 --- a/gopher/response.go +++ b/gopher/response.go @@ -121,7 +121,7 @@ func Error(err error) *MapItem { // File builds a minimal response delivering a file's contents. // -// Meta is nil and Status is 0 in this response. +// Meta is nil in this response. func File(status types.Status, contents io.Reader) *types.Response { return &types.Response{Status: status, Body: contents} } diff --git a/internal/users.go b/internal/users.go new file mode 100644 index 0000000..e7a25a3 --- /dev/null +++ b/internal/users.go @@ -0,0 +1,53 @@ +package internal + +import ( + "bufio" + "errors" + "io" + "os" + "path/filepath" + "strings" +) + +// ListUsersWithHomeSubdir provides a list of user names on the current host. +// +// The users returned will only be those with a specifically named subdirectory +// of their home dir, which has the given permission bits. +func ListUsersWithHomeSubdir(subdir string, required_perms os.FileMode) ([]string, error) { + file, err := os.Open("/etc/passwd") + if err != nil { + return nil, err + } + defer func() { + _ = file.Close() + }() + + users := []string{} + rdr := bufio.NewReader(file) + for { + line, err := rdr.ReadString('\n') + isEOF := errors.Is(err, io.EOF) + if err != nil && !isEOF { + return nil, err + } + + if len(line) != 0 && line[0] == '#' { + continue + } + + spl := strings.Split(line, ":") + home := spl[len(spl)-2] + st, err := os.Stat(filepath.Join(home, subdir)) + if err != nil || !st.IsDir() || st.Mode()&required_perms != required_perms { + continue + } + + users = append(users, spl[0]) + + if isEOF { + break + } + } + + return users, nil +} -- cgit v1.2.3