diff --git a/README.md b/README.md index bea47b3..769daa9 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,7 @@ This will display help for the tool. Here are all the switches it supports. | `-realm` | Basic auth message | `simplehttpserver -realm "insert the credentials"` | | `-version` | Show version | `simplehttpserver -version` | | `-silent` | Show only results | `simplehttpserver -silent` | +| `-py` | Emulate Python Style | `simplehttpserver -py` | | `-header` | HTTP response header (can be used multiple times) | `simplehttpserver -header 'X-Powered-By: Go'` | ### Running simplehttpserver in the current folder diff --git a/internal/runner/options.go b/internal/runner/options.go index 2e99cf1..2890a04 100644 --- a/internal/runner/options.go +++ b/internal/runner/options.go @@ -35,6 +35,7 @@ type Options struct { MaxFileSize int HTTP1Only bool MaxDumpBodySize int + Python bool CORS bool HTTPHeaders HTTPHeaders } @@ -65,6 +66,7 @@ func ParseOptions() *Options { flag.BoolVar(&options.HTTP1Only, "http1", false, "Enable only HTTP1") flag.IntVar(&options.MaxFileSize, "max-file-size", 50, "Max Upload File Size") flag.IntVar(&options.MaxDumpBodySize, "max-dump-body-size", -1, "Max Dump Body Size") + flag.BoolVar(&options.Python, "py", false, "Emulate Python Style") flag.BoolVar(&options.CORS, "cors", false, "Enable Cross-Origin Resource Sharing (CORS)") flag.Var(&options.HTTPHeaders, "header", "Add HTTP Response Header (name: value), can be used multiple times") flag.Parse() diff --git a/internal/runner/runner.go b/internal/runner/runner.go index e7ea127..dc63940 100644 --- a/internal/runner/runner.go +++ b/internal/runner/runner.go @@ -68,6 +68,7 @@ func New(options *Options) (*Runner, error) { MaxFileSize: r.options.MaxFileSize, HTTP1Only: r.options.HTTP1Only, MaxDumpBodySize: unit.ToMb(r.options.MaxDumpBodySize), + Python: r.options.Python, CORS: r.options.CORS, HTTPHeaders: r.options.HTTPHeaders, }) diff --git a/pkg/httpserver/httpserver.go b/pkg/httpserver/httpserver.go index 4dd6682..5dd65a6 100644 --- a/pkg/httpserver/httpserver.go +++ b/pkg/httpserver/httpserver.go @@ -27,6 +27,7 @@ type Options struct { HTTP1Only bool MaxFileSize int // 50Mb MaxDumpBodySize int64 + Python bool CORS bool HTTPHeaders []HTTPHeader } @@ -59,7 +60,13 @@ func New(options *Options) (*HTTPServer, error) { dir = SandboxFileSystem{fs: http.Dir(options.Folder), RootFolder: options.Folder} } - httpHandler := http.FileServer(dir) + var httpHandler http.Handler + if options.Python { + httpHandler = PythonStyle(dir.(http.Dir)) + } else { + httpHandler = http.FileServer(dir) + } + addHandler := func(newHandler Middleware) { httpHandler = newHandler(httpHandler) } diff --git a/pkg/httpserver/pythonliststyle.go b/pkg/httpserver/pythonliststyle.go new file mode 100644 index 0000000..65c6b0a --- /dev/null +++ b/pkg/httpserver/pythonliststyle.go @@ -0,0 +1,96 @@ +package httpserver + +import ( + "bytes" + "fmt" + "io" + "net/http" + "os" + "path/filepath" +) + +const ( + preTag = "
"
+	preTagClose = "
" + aTag = " + + + +Directory listing for %s + + +` + htmlFooter = `
+ + +` +) + +type pythonStyleHandler struct { + origWriter http.ResponseWriter + root http.Dir +} + +func (h *pythonStyleHandler) Header() http.Header { + return h.origWriter.Header() +} + +func (h *pythonStyleHandler) writeListItem(b []byte, written *int) { + var i int + i, _ = fmt.Fprint(h.origWriter, "
  • ") + *written += i + i, _ = h.origWriter.Write(bytes.Trim(b, "\r\n")) + *written += i + i, _ = fmt.Fprint(h.origWriter, "
  • \n") + *written += i +} + +func (h *pythonStyleHandler) Write(b []byte) (int, error) { + var i int + written := 0 + + if bytes.HasPrefix(b, []byte(preTag)) { + _, _ = io.Discard.Write(b) + i, _ = fmt.Fprintln(h.origWriter, "") + written += i + return written, nil + } + + if bytes.HasPrefix(b, []byte(aTag)) { + h.writeListItem(b, &written) + } + return i, nil +} + +func (h *pythonStyleHandler) WriteHeader(statusCode int) { + h.origWriter.WriteHeader(statusCode) +} + +func (h *pythonStyleHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) { + target := filepath.Join(string(h.root), filepath.Clean(request.URL.Path)) + file, err := os.Stat(target) + + if err != nil || !file.IsDir() { + http.ServeFile(writer, request, target) + return + } else { + _, _ = fmt.Fprintf(writer, htmlHeader, request.URL.Path) + _, _ = fmt.Fprintf(writer, "

    Directory listing for %s

    \n
    \n", request.URL.Path) + h.origWriter = writer + http.ServeFile(h, request, target) + _, _ = fmt.Fprint(writer, htmlFooter) + } +} + +func PythonStyle(root http.Dir) http.Handler { + return &pythonStyleHandler{ + root: root, + } +} diff --git a/pkg/httpserver/uploadlayer.go b/pkg/httpserver/uploadlayer.go index 670d75a..4064714 100644 --- a/pkg/httpserver/uploadlayer.go +++ b/pkg/httpserver/uploadlayer.go @@ -91,7 +91,7 @@ func handleUpload(base, file string, data []byte) error { } trustedPath := untrustedPath - if _, err := os.Stat(path.Dir(trustedPath)); os.IsNotExist(err) { + if _, err := os.Stat(filepath.Dir(trustedPath)); os.IsNotExist(err) { return errors.New("invalid path") } diff --git a/test/fixture/pythonliststyle/test file.txt b/test/fixture/pythonliststyle/test file.txt new file mode 100644 index 0000000..5cd8fbc --- /dev/null +++ b/test/fixture/pythonliststyle/test file.txt @@ -0,0 +1,2 @@ +This is the content of "test file.txt". +这是“test file.txt”文件的内容。 diff --git a/test/pythonliststyle_test.go b/test/pythonliststyle_test.go new file mode 100644 index 0000000..1826c8e --- /dev/null +++ b/test/pythonliststyle_test.go @@ -0,0 +1,75 @@ +package test + +import ( + "bytes" + "github.com/projectdiscovery/simplehttpserver/pkg/httpserver" + "io" + "net/http/httptest" + "os" + "strings" + "testing" +) + +func TestServePythonStyleHtmlPageForDirectories(t *testing.T) { + const want = ` + + + +Directory listing for / + + +

    Directory listing for /

    +
    + +
    + + +` + py := httpserver.PythonStyle("./fixture/pythonliststyle") + + w := httptest.NewRecorder() + py.ServeHTTP(w, httptest.NewRequest("GET", "http://example.com/", nil)) + b, _ := io.ReadAll(w.Result().Body) + + body := string(b) + if strings.Compare(want, body) != 0 { + t.Errorf("want:\n%s\ngot:\n%s", want, body) + } +} + +func TestServeFileContentForFiles(t *testing.T) { + want, _ := os.ReadFile("./fixture/pythonliststyle/test file.txt") + + py := httpserver.PythonStyle("./fixture/pythonliststyle") + + w := httptest.NewRecorder() + py.ServeHTTP(w, httptest.NewRequest( + "GET", + "http://example.com/test%20file.txt", + nil, + )) + got, _ := io.ReadAll(w.Result().Body) + if !bytes.Equal(want, got) { + t.Errorf("want:\n%x\ngot:\n%x", want, got) + } +} + +func TestResponseNotFound(t *testing.T) { + const want = `404 page not found +` + + py := httpserver.PythonStyle("./fixture/pythonliststyle") + + w := httptest.NewRecorder() + py.ServeHTTP(w, httptest.NewRequest( + "GET", + "http://example.com/does-not-exist.txt", + nil, + )) + got, _ := io.ReadAll(w.Result().Body) + if strings.Compare(want, string(got)) != 0 { + t.Errorf("want:\n%s\ngot:\n%s", want, got) + } +}