Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions internal/runner/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ type Options struct {
MaxFileSize int
HTTP1Only bool
MaxDumpBodySize int
Python bool
CORS bool
HTTPHeaders HTTPHeaders
}
Expand Down Expand Up @@ -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()
Expand Down
1 change: 1 addition & 0 deletions internal/runner/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
})
Expand Down
9 changes: 8 additions & 1 deletion pkg/httpserver/httpserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ type Options struct {
HTTP1Only bool
MaxFileSize int // 50Mb
MaxDumpBodySize int64
Python bool
CORS bool
HTTPHeaders []HTTPHeader
}
Expand Down Expand Up @@ -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)
}
Expand Down
96 changes: 96 additions & 0 deletions pkg/httpserver/pythonliststyle.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package httpserver

import (
"bytes"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
)

const (
preTag = "<pre>"
preTagClose = "</pre>"
aTag = "<a"
htmlHeader = `<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Directory listing for %s</title>
</head>
<body>
`
htmlFooter = `<hr>
</body>
</html>
`
)

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, "<li>")
*written += i
i, _ = h.origWriter.Write(bytes.Trim(b, "\r\n"))
*written += i
i, _ = fmt.Fprint(h.origWriter, "</li>\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, "<ul>")
written += i
return written, nil
}
if bytes.HasPrefix(b, []byte(preTagClose)) {
_, _ = io.Discard.Write(b)
i, _ = fmt.Fprintln(h.origWriter, "</ul>")
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)

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression

This path depends on a [user-provided value](1).

if err != nil || !file.IsDir() {
http.ServeFile(writer, request, target)
return
} else {
_, _ = fmt.Fprintf(writer, htmlHeader, request.URL.Path)

Check warning

Code scanning / CodeQL

Reflected cross-site scripting

Cross-site scripting vulnerability due to [user-provided value](1).
_, _ = fmt.Fprintf(writer, "<h1>Directory listing for %s</h1>\n<hr>\n", request.URL.Path)

Check warning

Code scanning / CodeQL

Reflected cross-site scripting

Cross-site scripting vulnerability due to [user-provided value](1).
h.origWriter = writer
http.ServeFile(h, request, target)
_, _ = fmt.Fprint(writer, htmlFooter)
}
}

func PythonStyle(root http.Dir) http.Handler {
return &pythonStyleHandler{
root: root,
}
}
2 changes: 1 addition & 1 deletion pkg/httpserver/uploadlayer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}

Expand Down
2 changes: 2 additions & 0 deletions test/fixture/pythonliststyle/test file.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
This is the content of "test file.txt".
这是“test file.txt”文件的内容。
75 changes: 75 additions & 0 deletions test/pythonliststyle_test.go
Original file line number Diff line number Diff line change
@@ -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 = `<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Directory listing for /</title>
</head>
<body>
<h1>Directory listing for /</h1>
<hr>
<ul>
<li><a href="test%20file.txt">test file.txt</a></li>
</ul>
<hr>
</body>
</html>
`
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)
}
}