package cgi import ( "bytes" "context" "fmt" "os" "path" "path/filepath" "strings" sr "tildegit.org/tjp/sliderule" "tildegit.org/tjp/sliderule/gopher" "tildegit.org/tjp/sliderule/gopher/gophermap" "tildegit.org/tjp/sliderule/logging" ) // GopherCGIDirectory runs any executable files relative to a root directory on the file system. // // It will also find and run any executables part way through the path, so for example // 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, 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 } requestPath := strings.Trim(strings.TrimPrefix(request.Path, pathRoot), "/") fullpath, pathinfo, err := resolveGopherCGI(fsRoot, requestPath) if err != nil { return gopher.Error(err).Response() } 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 } 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 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 runGopherCGI(ctx, request, fullpath, "/", *settings) }) } func runGopherCGI( ctx context.Context, request *sr.Request, fullpath string, pathinfo string, settings gophermap.FileSystemSettings, ) *sr.Response { stderr := &bytes.Buffer{} stdout, exitCode, err := RunCGI(ctx, request, fullpath, pathinfo, stderr) if err != nil { return gopher.Error(err).Response() } if exitCode != 0 { ctx.Value("warnlog").(logging.Logger).Log( "msg", "cgi exited with non-zero exit code", "code", exitCode, "stderr", stderr.String(), ) 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) if err != nil { return gopher.Error(err).Response() } return doc.Response() } return gopher.File(gopher.MenuType, stdout) } func resolveGopherCGI(fsRoot string, reqPath string) (string, string, error) { 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 }