mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-02 11:07:10 +00:00
feat(inspect): serveHTML route under /api/runs/<id>/html/<name>
Mirrors serveScreenshot path validation; rejects traversal segments and unknown extensions. text/html content-type so the iframe renders cleanly.
This commit is contained in:
1 parent
d2626eeccc
commit
ee3737c831
2 files changed
+64
No files matched your search
@@ -84,6 +84,7 @@ func (s *Server) handleRunsList(responseWriter http.ResponseWriter, request *htt
|
|||||||
|
|
||||||
var stepPathPattern = regexp.MustCompile(`^([a-zA-Z0-9._-]+)/steps/([^/]+)$`)
|
var stepPathPattern = regexp.MustCompile(`^([a-zA-Z0-9._-]+)/steps/([^/]+)$`)
|
||||||
var screenshotPathPattern = regexp.MustCompile(`^([a-zA-Z0-9._-]+)/screenshots/([a-zA-Z0-9._-]+\.png)$`)
|
var screenshotPathPattern = regexp.MustCompile(`^([a-zA-Z0-9._-]+)/screenshots/([a-zA-Z0-9._-]+\.png)$`)
|
||||||
|
var htmlPathPattern = regexp.MustCompile(`^([a-zA-Z0-9._-]+)/html/([a-zA-Z0-9._-]+\.html)$`)
|
||||||
var runDetailPathPattern = regexp.MustCompile(`^([a-zA-Z0-9._-]+)/?$`)
|
var runDetailPathPattern = regexp.MustCompile(`^([a-zA-Z0-9._-]+)/?$`)
|
||||||
|
|
||||||
func (s *Server) handleRunsTree(responseWriter http.ResponseWriter, request *http.Request) {
|
func (s *Server) handleRunsTree(responseWriter http.ResponseWriter, request *http.Request) {
|
||||||
@@ -104,6 +105,10 @@ func (s *Server) handleRunsTree(responseWriter http.ResponseWriter, request *htt
|
|||||||
s.serveScreenshot(responseWriter, request, match[1], match[2])
|
s.serveScreenshot(responseWriter, request, match[1], match[2])
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if match := htmlPathPattern.FindStringSubmatch(rest); match != nil {
|
||||||
|
s.serveHTML(responseWriter, request, match[1], match[2])
|
||||||
|
return
|
||||||
|
}
|
||||||
if match := runDetailPathPattern.FindStringSubmatch(rest); match != nil {
|
if match := runDetailPathPattern.FindStringSubmatch(rest); match != nil {
|
||||||
s.serveDetail(responseWriter, match[1])
|
s.serveDetail(responseWriter, match[1])
|
||||||
return
|
return
|
||||||
@@ -160,6 +165,20 @@ func (s *Server) serveScreenshot(responseWriter http.ResponseWriter, request *ht
|
|||||||
http.ServeFile(responseWriter, request, full)
|
http.ServeFile(responseWriter, request, full)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Server) serveHTML(responseWriter http.ResponseWriter, request *http.Request, id, name string) {
|
||||||
|
if !validRunID(id) || strings.Contains(name, "..") {
|
||||||
|
http.Error(responseWriter, "run not found", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
full := filepath.Join(s.options.RunsDirectory, id, "html", name)
|
||||||
|
if _, err := http.Dir(filepath.Join(s.options.RunsDirectory, id, "html")).Open(name); err != nil {
|
||||||
|
http.NotFound(responseWriter, request)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
responseWriter.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
http.ServeFile(responseWriter, request, full)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Server) handleEvents(responseWriter http.ResponseWriter, request *http.Request) {
|
func (s *Server) handleEvents(responseWriter http.ResponseWriter, request *http.Request) {
|
||||||
flusher, ok := responseWriter.(http.Flusher)
|
flusher, ok := responseWriter.(http.Flusher)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|||||||
@@ -171,6 +171,51 @@ func TestScreenshot_ServesWhitelistedPNG(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestServeHTML_Returns200(t *testing.T) {
|
||||||
|
server, root := newFixtureServer(t)
|
||||||
|
htmlDirectory := filepath.Join(root, "run-a", "html")
|
||||||
|
if err := os.MkdirAll(htmlDirectory, 0o755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
body := []byte("<!doctype html><html><body>hi</body></html>")
|
||||||
|
if err := os.WriteFile(filepath.Join(htmlDirectory, "step-00001.html"), body, 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
request := httptest.NewRequest(http.MethodGet, "/api/runs/run-a/html/step-00001.html", nil)
|
||||||
|
server.Handler().ServeHTTP(recorder, request)
|
||||||
|
if recorder.Code != http.StatusOK {
|
||||||
|
t.Fatalf("status = %d body=%s", recorder.Code, recorder.Body.String())
|
||||||
|
}
|
||||||
|
if recorder.Body.String() != string(body) {
|
||||||
|
t.Errorf("body mismatch")
|
||||||
|
}
|
||||||
|
if !strings.Contains(recorder.Header().Get("Content-Type"), "text/html") {
|
||||||
|
t.Errorf("content-type = %q", recorder.Header().Get("Content-Type"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServeHTML_RejectsTraversalAndMissing(t *testing.T) {
|
||||||
|
server, _ := newFixtureServer(t)
|
||||||
|
cases := []string{
|
||||||
|
"/api/runs/run-a/html/../meta.json",
|
||||||
|
"/api/runs/run-a/html/..%2Fmeta.json",
|
||||||
|
"/api/runs/run-a/html/missing.html",
|
||||||
|
"/api/runs/run-a/html/step-00001.txt",
|
||||||
|
}
|
||||||
|
for _, path := range cases {
|
||||||
|
t.Run(path, func(t *testing.T) {
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
request := httptest.NewRequest(http.MethodGet, path, nil)
|
||||||
|
server.Handler().ServeHTTP(recorder, request)
|
||||||
|
if recorder.Code == http.StatusOK {
|
||||||
|
t.Errorf("status = %d (should not be 200) body=%s", recorder.Code, recorder.Body.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestScreenshot_RejectsTraversalAndBadNames(t *testing.T) {
|
func TestScreenshot_RejectsTraversalAndBadNames(t *testing.T) {
|
||||||
server, _ := newFixtureServer(t)
|
server, _ := newFixtureServer(t)
|
||||||
cases := []string{
|
cases := []string{
|
||||||
|
|||||||
Reference in new issue
Block a user