knotserver: also serve text/plain in BlobRaw #390

closed
opened by oppi.li targeting master from push-svtkmrzmrwky
Changed files
+88 -24
appview
pages
markup
templates
repo
repo
knotserver
+11 -3
knotserver/routes.go
···
mimeType = "image/svg+xml"
}
-
if !strings.HasPrefix(mimeType, "image/") && !strings.HasPrefix(mimeType, "video/") {
-
l.Error("attempted to serve non-image/video file", "mimetype", mimeType)
-
writeError(w, "only image and video files can be accessed directly", http.StatusForbidden)
return
}
···
mimeType = "image/svg+xml"
}
+
// allow image, video, and text/plain files to be served directly
+
switch {
+
case strings.HasPrefix(mimeType, "image/"):
+
// allowed
+
case strings.HasPrefix(mimeType, "video/"):
+
// allowed
+
case strings.HasPrefix(mimeType, "text/plain"):
+
// allowed
+
default:
+
l.Error("attempted to serve disallowed file type", "mimetype", mimeType)
+
writeError(w, "only image, video, and text files can be accessed directly", http.StatusForbidden)
return
}
+2 -2
appview/pages/markup/camo.go
···
"github.com/yuin/goldmark/ast"
)
-
func generateCamoURL(baseURL, secret, imageURL string) string {
h := hmac.New(sha256.New, []byte(secret))
h.Write([]byte(imageURL))
signature := hex.EncodeToString(h.Sum(nil))
···
}
if rctx.CamoUrl != "" && rctx.CamoSecret != "" {
-
return generateCamoURL(rctx.CamoUrl, rctx.CamoSecret, dst)
}
return dst
···
"github.com/yuin/goldmark/ast"
)
+
func GenerateCamoURL(baseURL, secret, imageURL string) string {
h := hmac.New(sha256.New, []byte(secret))
h.Write([]byte(imageURL))
signature := hex.EncodeToString(h.Sum(nil))
···
}
if rctx.CamoUrl != "" && rctx.CamoSecret != "" {
+
return GenerateCamoURL(rctx.CamoUrl, rctx.CamoSecret, dst)
}
return dst
+4
appview/pages/pages.go
···
LoggedInUser *oauth.User
RepoInfo repoinfo.RepoInfo
Active string
BreadCrumbs [][]string
ShowRendered bool
RenderToggle bool
···
LoggedInUser *oauth.User
RepoInfo repoinfo.RepoInfo
Active string
+
Unsupported bool
+
IsImage bool
+
IsVideo bool
+
ContentSrc string
BreadCrumbs [][]string
ShowRendered bool
RenderToggle bool
+19 -6
appview/pages/templates/repo/blob.html
···
{{ $title := printf "%s at %s · %s" .Path .Ref .RepoInfo.FullName }}
{{ $url := printf "https://tangled.sh/%s/blob/%s/%s" .RepoInfo.FullName .Ref .Path }}
-
{{ template "repo/fragments/og" (dict "RepoInfo" .RepoInfo "Title" $title "Url" $url) }}
-
{{ end }}
{{ define "repoContent" }}
···
<a href="/{{ .RepoInfo.FullName }}/raw/{{ .Ref }}/{{ .Path }}">view raw</a>
{{ if .RenderToggle }}
<span class="select-none px-1 md:px-2 [&:before]:content-['·']"></span>
-
<a
-
href="/{{ .RepoInfo.FullName }}/blob/{{ .Ref }}/{{ .Path }}?code={{ .ShowRendered }}"
hx-boost="true"
>view {{ if .ShowRendered }}code{{ else }}rendered{{ end }}</a>
{{ end }}
</div>
</div>
</div>
-
{{ if .IsBinary }}
<p class="text-center text-gray-400 dark:text-gray-500">
-
This is a binary file and will not be displayed.
</p>
{{ else }}
<div class="overflow-auto relative">
{{ if .ShowRendered }}
···
{{ $title := printf "%s at %s &middot; %s" .Path .Ref .RepoInfo.FullName }}
{{ $url := printf "https://tangled.sh/%s/blob/%s/%s" .RepoInfo.FullName .Ref .Path }}
+
{{ template "repo/fragments/og" (dict "RepoInfo" .RepoInfo "Title" $title "Url" $url) }}
+
{{ end }}
{{ define "repoContent" }}
···
<a href="/{{ .RepoInfo.FullName }}/raw/{{ .Ref }}/{{ .Path }}">view raw</a>
{{ if .RenderToggle }}
<span class="select-none px-1 md:px-2 [&:before]:content-['·']"></span>
+
<a
+
href="/{{ .RepoInfo.FullName }}/blob/{{ .Ref }}/{{ .Path }}?code={{ .ShowRendered }}"
hx-boost="true"
>view {{ if .ShowRendered }}code{{ else }}rendered{{ end }}</a>
{{ end }}
</div>
</div>
</div>
+
{{ if and .IsBinary .Unsupported }}
<p class="text-center text-gray-400 dark:text-gray-500">
+
Previews are not supported for this file type.
</p>
+
{{ else if .IsBinary }}
+
<div class="text-center">
+
{{ if .IsImage }}
+
<img src="{{ .ContentSrc }}"
+
alt="{{ .Path }}"
+
class="max-w-full h-auto mx-auto border border-gray-200 dark:border-gray-700 rounded" />
+
{{ else if .IsVideo }}
+
<video controls class="max-w-full h-auto mx-auto border border-gray-200 dark:border-gray-700 rounded">
+
<source src="{{ .ContentSrc }}">
+
Your browser does not support the video tag.
+
</video>
+
{{ end }}
+
</div>
{{ else }}
<div class="overflow-auto relative">
{{ if .ShowRendered }}
+52 -13
appview/repo/repo.go
···
"log"
"net/http"
"net/url"
"slices"
"strconv"
"strings"
···
showRendered = r.URL.Query().Get("code") != "true"
}
user := rp.oauth.GetUser(r)
rp.pages.RepoBlob(w, pages.RepoBlobParams{
LoggedInUser: user,
···
BreadCrumbs: breadcrumbs,
ShowRendered: showRendered,
RenderToggle: renderToggle,
})
}
···
f, err := rp.repoResolver.Resolve(r)
if err != nil {
log.Println("failed to get repo and knot", err)
return
}
···
if !rp.config.Core.Dev {
protocol = "https"
}
-
resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/blob/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, filePath))
if err != nil {
-
log.Println("failed to reach knotserver", err)
return
}
-
body, err := io.ReadAll(resp.Body)
-
if err != nil {
-
log.Printf("Error reading response body: %v", err)
return
}
-
var result types.RepoBlobResponse
-
err = json.Unmarshal(body, &result)
if err != nil {
-
log.Println("failed to parse response:", err)
return
}
-
if result.IsBinary {
-
w.Header().Set("Content-Type", "application/octet-stream")
w.Write(body)
return
}
-
-
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
-
w.Write([]byte(result.Contents))
}
// modify the spindle configured for this repo
···
"log"
"net/http"
"net/url"
+
"path/filepath"
"slices"
"strconv"
"strings"
···
showRendered = r.URL.Query().Get("code") != "true"
}
+
var unsupported bool
+
var isImage bool
+
var isVideo bool
+
var contentSrc string
+
+
if result.IsBinary {
+
ext := strings.ToLower(filepath.Ext(result.Path))
+
switch ext {
+
case ".jpg", ".jpeg", ".png", ".gif", ".svg", ".webp":
+
isImage = true
+
case ".mp4", ".webm", ".ogg", ".mov", ".avi":
+
isVideo = true
+
default:
+
unsupported = true
+
}
+
+
// fetch the actual binary content like in RepoBlobRaw
+
+
blobURL := fmt.Sprintf("%s://%s/%s/%s/raw/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, filePath)
+
contentSrc = blobURL
+
if !rp.config.Core.Dev {
+
contentSrc = markup.GenerateCamoURL(rp.config.Camo.Host, rp.config.Camo.SharedSecret, blobURL)
+
}
+
}
+
user := rp.oauth.GetUser(r)
rp.pages.RepoBlob(w, pages.RepoBlobParams{
LoggedInUser: user,
···
BreadCrumbs: breadcrumbs,
ShowRendered: showRendered,
RenderToggle: renderToggle,
+
Unsupported: unsupported,
+
IsImage: isImage,
+
IsVideo: isVideo,
+
ContentSrc: contentSrc,
})
}
···
f, err := rp.repoResolver.Resolve(r)
if err != nil {
log.Println("failed to get repo and knot", err)
+
w.WriteHeader(http.StatusBadRequest)
return
}
···
if !rp.config.Core.Dev {
protocol = "https"
}
+
blobURL := fmt.Sprintf("%s://%s/%s/%s/raw/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, filePath)
+
resp, err := http.Get(blobURL)
if err != nil {
+
log.Println("failed to reach knotserver:", err)
+
rp.pages.Error503(w)
return
}
+
defer resp.Body.Close()
+
if resp.StatusCode != http.StatusOK {
+
log.Printf("knotserver returned non-OK status for raw blob %s: %d", blobURL, resp.StatusCode)
+
w.WriteHeader(resp.StatusCode)
+
_, _ = io.Copy(w, resp.Body)
return
}
+
contentType := resp.Header.Get("Content-Type")
+
body, err := io.ReadAll(resp.Body)
if err != nil {
+
log.Printf("error reading response body from knotserver: %v", err)
+
w.WriteHeader(http.StatusInternalServerError)
return
}
+
if strings.Contains(contentType, "text/plain") {
+
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.Write(body)
+
} else if strings.HasPrefix(contentType, "image/") || strings.HasPrefix(contentType, "video/") {
+
w.Header().Set("Content-Type", contentType)
+
w.Write(body)
+
} else {
+
w.WriteHeader(http.StatusUnsupportedMediaType)
+
w.Write([]byte("unsupported content type"))
return
}
}
// modify the spindle configured for this repo