Skip to content

Commit e5e0814

Browse files
li6in9muyouehsandeepMzack9999
authored
Add DOCTYPE, header and similar list style as that of python's (#106)
* feat: use same page style as python's * test: pythonliststyle_test.go * refactor: make it clearer that we surround original html with proper header and footer * refactor: explain what these lines do * fix: html title is not updated as URL path changes * doc: explain what this fork is for * doc: typo * test: serve a file * fix: file is not served properly * reword * lint: accept gosimple's suggestion * fix: handles any os.Stat() error * made python style optional --------- Co-authored-by: Sandeep Singh <sandeep@projectdiscovery.io> Co-authored-by: Mzack9999 <mzack9999@protonmail.com>
1 parent d564b6e commit e5e0814

File tree

8 files changed

+186
-2
lines changed

8 files changed

+186
-2
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ This will display help for the tool. Here are all the switches it supports.
6969
| `-realm` | Basic auth message | `simplehttpserver -realm "insert the credentials"` |
7070
| `-version` | Show version | `simplehttpserver -version` |
7171
| `-silent` | Show only results | `simplehttpserver -silent` |
72+
| `-py` | Emulate Python Style | `simplehttpserver -py` |
7273
| `-header` | HTTP response header (can be used multiple times) | `simplehttpserver -header 'X-Powered-By: Go'` |
7374

7475
### Running simplehttpserver in the current folder

internal/runner/options.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ type Options struct {
3535
MaxFileSize int
3636
HTTP1Only bool
3737
MaxDumpBodySize int
38+
Python bool
3839
CORS bool
3940
HTTPHeaders HTTPHeaders
4041
}
@@ -65,6 +66,7 @@ func ParseOptions() *Options {
6566
flag.BoolVar(&options.HTTP1Only, "http1", false, "Enable only HTTP1")
6667
flag.IntVar(&options.MaxFileSize, "max-file-size", 50, "Max Upload File Size")
6768
flag.IntVar(&options.MaxDumpBodySize, "max-dump-body-size", -1, "Max Dump Body Size")
69+
flag.BoolVar(&options.Python, "py", false, "Emulate Python Style")
6870
flag.BoolVar(&options.CORS, "cors", false, "Enable Cross-Origin Resource Sharing (CORS)")
6971
flag.Var(&options.HTTPHeaders, "header", "Add HTTP Response Header (name: value), can be used multiple times")
7072
flag.Parse()

internal/runner/runner.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ func New(options *Options) (*Runner, error) {
6868
MaxFileSize: r.options.MaxFileSize,
6969
HTTP1Only: r.options.HTTP1Only,
7070
MaxDumpBodySize: unit.ToMb(r.options.MaxDumpBodySize),
71+
Python: r.options.Python,
7172
CORS: r.options.CORS,
7273
HTTPHeaders: r.options.HTTPHeaders,
7374
})

pkg/httpserver/httpserver.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ type Options struct {
2727
HTTP1Only bool
2828
MaxFileSize int // 50Mb
2929
MaxDumpBodySize int64
30+
Python bool
3031
CORS bool
3132
HTTPHeaders []HTTPHeader
3233
}
@@ -59,7 +60,13 @@ func New(options *Options) (*HTTPServer, error) {
5960
dir = SandboxFileSystem{fs: http.Dir(options.Folder), RootFolder: options.Folder}
6061
}
6162

62-
httpHandler := http.FileServer(dir)
63+
var httpHandler http.Handler
64+
if options.Python {
65+
httpHandler = PythonStyle(dir.(http.Dir))
66+
} else {
67+
httpHandler = http.FileServer(dir)
68+
}
69+
6370
addHandler := func(newHandler Middleware) {
6471
httpHandler = newHandler(httpHandler)
6572
}

pkg/httpserver/pythonliststyle.go

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
package httpserver
2+
3+
import (
4+
"bytes"
5+
"fmt"
6+
"io"
7+
"net/http"
8+
"os"
9+
"path/filepath"
10+
)
11+
12+
const (
13+
preTag = "<pre>"
14+
preTagClose = "</pre>"
15+
aTag = "<a"
16+
htmlHeader = `<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
17+
<html>
18+
<head>
19+
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
20+
<title>Directory listing for %s</title>
21+
</head>
22+
<body>
23+
`
24+
htmlFooter = `<hr>
25+
</body>
26+
</html>
27+
`
28+
)
29+
30+
type pythonStyleHandler struct {
31+
origWriter http.ResponseWriter
32+
root http.Dir
33+
}
34+
35+
func (h *pythonStyleHandler) Header() http.Header {
36+
return h.origWriter.Header()
37+
}
38+
39+
func (h *pythonStyleHandler) writeListItem(b []byte, written *int) {
40+
var i int
41+
i, _ = fmt.Fprint(h.origWriter, "<li>")
42+
*written += i
43+
i, _ = h.origWriter.Write(bytes.Trim(b, "\r\n"))
44+
*written += i
45+
i, _ = fmt.Fprint(h.origWriter, "</li>\n")
46+
*written += i
47+
}
48+
49+
func (h *pythonStyleHandler) Write(b []byte) (int, error) {
50+
var i int
51+
written := 0
52+
53+
if bytes.HasPrefix(b, []byte(preTag)) {
54+
_, _ = io.Discard.Write(b)
55+
i, _ = fmt.Fprintln(h.origWriter, "<ul>")
56+
written += i
57+
return written, nil
58+
}
59+
if bytes.HasPrefix(b, []byte(preTagClose)) {
60+
_, _ = io.Discard.Write(b)
61+
i, _ = fmt.Fprintln(h.origWriter, "</ul>")
62+
written += i
63+
return written, nil
64+
}
65+
66+
if bytes.HasPrefix(b, []byte(aTag)) {
67+
h.writeListItem(b, &written)
68+
}
69+
return i, nil
70+
}
71+
72+
func (h *pythonStyleHandler) WriteHeader(statusCode int) {
73+
h.origWriter.WriteHeader(statusCode)
74+
}
75+
76+
func (h *pythonStyleHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
77+
target := filepath.Join(string(h.root), filepath.Clean(request.URL.Path))
78+
file, err := os.Stat(target)
79+
80+
if err != nil || !file.IsDir() {
81+
http.ServeFile(writer, request, target)
82+
return
83+
} else {
84+
_, _ = fmt.Fprintf(writer, htmlHeader, request.URL.Path)
85+
_, _ = fmt.Fprintf(writer, "<h1>Directory listing for %s</h1>\n<hr>\n", request.URL.Path)
86+
h.origWriter = writer
87+
http.ServeFile(h, request, target)
88+
_, _ = fmt.Fprint(writer, htmlFooter)
89+
}
90+
}
91+
92+
func PythonStyle(root http.Dir) http.Handler {
93+
return &pythonStyleHandler{
94+
root: root,
95+
}
96+
}

pkg/httpserver/uploadlayer.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ func handleUpload(base, file string, data []byte) error {
9191
}
9292
trustedPath := untrustedPath
9393

94-
if _, err := os.Stat(path.Dir(trustedPath)); os.IsNotExist(err) {
94+
if _, err := os.Stat(filepath.Dir(trustedPath)); os.IsNotExist(err) {
9595
return errors.New("invalid path")
9696
}
9797

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
This is the content of "test file.txt".
2+
这是“test file.txt”文件的内容。

test/pythonliststyle_test.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package test
2+
3+
import (
4+
"bytes"
5+
"github.com/projectdiscovery/simplehttpserver/pkg/httpserver"
6+
"io"
7+
"net/http/httptest"
8+
"os"
9+
"strings"
10+
"testing"
11+
)
12+
13+
func TestServePythonStyleHtmlPageForDirectories(t *testing.T) {
14+
const want = `<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
15+
<html>
16+
<head>
17+
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
18+
<title>Directory listing for /</title>
19+
</head>
20+
<body>
21+
<h1>Directory listing for /</h1>
22+
<hr>
23+
<ul>
24+
<li><a href="test%20file.txt">test file.txt</a></li>
25+
</ul>
26+
<hr>
27+
</body>
28+
</html>
29+
`
30+
py := httpserver.PythonStyle("./fixture/pythonliststyle")
31+
32+
w := httptest.NewRecorder()
33+
py.ServeHTTP(w, httptest.NewRequest("GET", "http://example.com/", nil))
34+
b, _ := io.ReadAll(w.Result().Body)
35+
36+
body := string(b)
37+
if strings.Compare(want, body) != 0 {
38+
t.Errorf("want:\n%s\ngot:\n%s", want, body)
39+
}
40+
}
41+
42+
func TestServeFileContentForFiles(t *testing.T) {
43+
want, _ := os.ReadFile("./fixture/pythonliststyle/test file.txt")
44+
45+
py := httpserver.PythonStyle("./fixture/pythonliststyle")
46+
47+
w := httptest.NewRecorder()
48+
py.ServeHTTP(w, httptest.NewRequest(
49+
"GET",
50+
"http://example.com/test%20file.txt",
51+
nil,
52+
))
53+
got, _ := io.ReadAll(w.Result().Body)
54+
if !bytes.Equal(want, got) {
55+
t.Errorf("want:\n%x\ngot:\n%x", want, got)
56+
}
57+
}
58+
59+
func TestResponseNotFound(t *testing.T) {
60+
const want = `404 page not found
61+
`
62+
63+
py := httpserver.PythonStyle("./fixture/pythonliststyle")
64+
65+
w := httptest.NewRecorder()
66+
py.ServeHTTP(w, httptest.NewRequest(
67+
"GET",
68+
"http://example.com/does-not-exist.txt",
69+
nil,
70+
))
71+
got, _ := io.ReadAll(w.Result().Body)
72+
if strings.Compare(want, string(got)) != 0 {
73+
t.Errorf("want:\n%s\ngot:\n%s", want, got)
74+
}
75+
}

0 commit comments

Comments
 (0)