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(preTagClose)) {
+ _, _ = 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)
+ }
+}