a self-hosted git server in one binary — everyone reads, only the owner writes
e2e_test.go | 11 +++++++++++
web.go | 24 +++++++++++++++++++++---
2 files changed, 32 insertions(+), 3 deletions(-)
diff --git a/e2e_test.go b/e2e_test.go
index ab841c4..fd9b89d 100644
--- a/e2e_test.go
+++ b/e2e_test.go
@@ -149,6 +149,7 @@ func TestWebPages(t *testing.T) {
os.MkdirAll(filepath.Join(dir, "docs"), 0o755)
os.WriteFile(filepath.Join(dir, "README.md"), []byte("# site\nreadme body here\n"), 0o644)
os.WriteFile(filepath.Join(dir, "docs", "guide.txt"), []byte("guide line one\n"), 0o644)
+ os.WriteFile(filepath.Join(dir, "logo.svg"), []byte("<svg xmlns=\"http://www.w3.org/2000/svg\"/>\n"), 0o644)
mustGit(t, dir, "add", ".")
mustGit(t, dir, "commit", "-m", "add docs")
mustGit(t, dir, "push", withToken(t, ts.URL+"/site.git"), "master")
@@ -182,6 +183,16 @@ func TestWebPages(t *testing.T) {
t.Errorf("commit page: code %d, diff shown: %v", code, strings.Contains(body, "guide line one"))
}
+ // Media files get their real content type so README embeds render.
+ resp, err := http.Get(ts.URL + "/site/raw/master/logo.svg")
+ if err != nil {
+ t.Fatal(err)
+ }
+ resp.Body.Close()
+ if ct := resp.Header.Get("Content-Type"); ct != "image/svg+xml" {
+ t.Errorf("svg raw content-type = %q", ct)
+ }
+
// Traversal and junk stay 404.
for _, path := range []string{"/nope", "/site/blob/master/../../etc/passwd", "/site/raw/master/%2e%2e/x"} {
if code, _ := get(t, ts.URL+path); code != http.StatusNotFound {
diff --git a/web.go b/web.go
index 0e22055..c2a72d2 100644
--- a/web.go
+++ b/web.go
@@ -244,6 +244,20 @@ func (srv *Server) blobPage(w http.ResponseWriter, r *http.Request) {
srv.render(w, "blob", data)
}
+var rawTypes = map[string]string{
+ ".svg": "image/svg+xml", ".png": "image/png", ".jpg": "image/jpeg",
+ ".jpeg": "image/jpeg", ".gif": "image/gif", ".webp": "image/webp",
+ ".ico": "image/x-icon", ".avif": "image/avif", ".pdf": "application/pdf",
+ ".mp4": "video/mp4", ".webm": "video/webm", ".mp3": "audio/mpeg",
+}
+
+func extOf(path string) string {
+ if i := strings.LastIndexByte(path, '.'); i >= 0 && !strings.ContainsRune(path[i:], '/') {
+ return strings.ToLower(path[i:])
+ }
+ return ""
+}
+
func (srv *Server) rawFile(w http.ResponseWriter, r *http.Request) {
repo := srv.openRepo(w, r)
if repo == nil {
@@ -259,11 +273,15 @@ func (srv *Server) rawFile(w http.ResponseWriter, r *http.Request) {
http.NotFound(w, r)
return
}
- // Never let the browser interpret repository content as HTML.
+ // Never let the browser interpret repository content as HTML. Known
+ // media types get their real content type so <img>/<video> embeds work.
w.Header().Set("X-Content-Type-Options", "nosniff")
- if isBinary(blob) {
+ switch {
+ case rawTypes[extOf(path)] != "":
+ w.Header().Set("Content-Type", rawTypes[extOf(path)])
+ case isBinary(blob):
w.Header().Set("Content-Type", "application/octet-stream")
- } else {
+ default:
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
}
w.Write(blob)