Signed-off-by: Anirudh Oppiliappan anirudh@tangled.sh
+11
-3
knotserver/routes.go
+11
-3
knotserver/routes.go
···
286
286
mimeType = "image/svg+xml"
287
287
}
288
288
289
-
if !strings.HasPrefix(mimeType, "image/") && !strings.HasPrefix(mimeType, "video/") {
290
-
l.Error("attempted to serve non-image/video file", "mimetype", mimeType)
291
-
writeError(w, "only image and video files can be accessed directly", http.StatusForbidden)
289
+
// allow image, video, and text/plain files to be served directly
290
+
switch {
291
+
case strings.HasPrefix(mimeType, "image/"):
292
+
// allowed
293
+
case strings.HasPrefix(mimeType, "video/"):
294
+
// allowed
295
+
case strings.HasPrefix(mimeType, "text/plain"):
296
+
// allowed
297
+
default:
298
+
l.Error("attempted to serve disallowed file type", "mimetype", mimeType)
299
+
writeError(w, "only image, video, and text files can be accessed directly", http.StatusForbidden)
292
300
return
293
301
}
294
302
+2
-2
appview/pages/markup/camo.go
+2
-2
appview/pages/markup/camo.go
···
9
9
"github.com/yuin/goldmark/ast"
10
10
)
11
11
12
-
func generateCamoURL(baseURL, secret, imageURL string) string {
12
+
func GenerateCamoURL(baseURL, secret, imageURL string) string {
13
13
h := hmac.New(sha256.New, []byte(secret))
14
14
h.Write([]byte(imageURL))
15
15
signature := hex.EncodeToString(h.Sum(nil))
···
24
24
}
25
25
26
26
if rctx.CamoUrl != "" && rctx.CamoSecret != "" {
27
-
return generateCamoURL(rctx.CamoUrl, rctx.CamoSecret, dst)
27
+
return GenerateCamoURL(rctx.CamoUrl, rctx.CamoSecret, dst)
28
28
}
29
29
30
30
return dst
+4
appview/pages/pages.go
+4
appview/pages/pages.go
+19
-6
appview/pages/templates/repo/blob.html
+19
-6
appview/pages/templates/repo/blob.html
···
5
5
6
6
{{ $title := printf "%s at %s · %s" .Path .Ref .RepoInfo.FullName }}
7
7
{{ $url := printf "https://tangled.sh/%s/blob/%s/%s" .RepoInfo.FullName .Ref .Path }}
8
-
8
+
9
9
{{ template "repo/fragments/og" (dict "RepoInfo" .RepoInfo "Title" $title "Url" $url) }}
10
-
10
+
11
11
{{ end }}
12
12
13
13
{{ define "repoContent" }}
···
44
44
<a href="/{{ .RepoInfo.FullName }}/raw/{{ .Ref }}/{{ .Path }}">view raw</a>
45
45
{{ if .RenderToggle }}
46
46
<span class="select-none px-1 md:px-2 [&:before]:content-['·']"></span>
47
-
<a
48
-
href="/{{ .RepoInfo.FullName }}/blob/{{ .Ref }}/{{ .Path }}?code={{ .ShowRendered }}"
47
+
<a
48
+
href="/{{ .RepoInfo.FullName }}/blob/{{ .Ref }}/{{ .Path }}?code={{ .ShowRendered }}"
49
49
hx-boost="true"
50
50
>view {{ if .ShowRendered }}code{{ else }}rendered{{ end }}</a>
51
51
{{ end }}
52
52
</div>
53
53
</div>
54
54
</div>
55
-
{{ if .IsBinary }}
55
+
{{ if and .IsBinary .Unsupported }}
56
56
<p class="text-center text-gray-400 dark:text-gray-500">
57
-
This is a binary file and will not be displayed.
57
+
Previews are not supported for this file type.
58
58
</p>
59
+
{{ else if .IsBinary }}
60
+
<div class="text-center">
61
+
{{ if .IsImage }}
62
+
<img src="{{ .ContentSrc }}"
63
+
alt="{{ .Path }}"
64
+
class="max-w-full h-auto mx-auto border border-gray-200 dark:border-gray-700 rounded" />
65
+
{{ else if .IsVideo }}
66
+
<video controls class="max-w-full h-auto mx-auto border border-gray-200 dark:border-gray-700 rounded">
67
+
<source src="{{ .ContentSrc }}">
68
+
Your browser does not support the video tag.
69
+
</video>
70
+
{{ end }}
71
+
</div>
59
72
{{ else }}
60
73
<div class="overflow-auto relative">
61
74
{{ if .ShowRendered }}
+52
-13
appview/repo/repo.go
+52
-13
appview/repo/repo.go
···
10
10
"log"
11
11
"net/http"
12
12
"net/url"
13
+
"path/filepath"
13
14
"slices"
14
15
"strconv"
15
16
"strings"
···
532
533
showRendered = r.URL.Query().Get("code") != "true"
533
534
}
534
535
536
+
var unsupported bool
537
+
var isImage bool
538
+
var isVideo bool
539
+
var contentSrc string
540
+
541
+
if result.IsBinary {
542
+
ext := strings.ToLower(filepath.Ext(result.Path))
543
+
switch ext {
544
+
case ".jpg", ".jpeg", ".png", ".gif", ".svg", ".webp":
545
+
isImage = true
546
+
case ".mp4", ".webm", ".ogg", ".mov", ".avi":
547
+
isVideo = true
548
+
default:
549
+
unsupported = true
550
+
}
551
+
552
+
// fetch the actual binary content like in RepoBlobRaw
553
+
554
+
blobURL := fmt.Sprintf("%s://%s/%s/%s/raw/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, filePath)
555
+
contentSrc = blobURL
556
+
if !rp.config.Core.Dev {
557
+
contentSrc = markup.GenerateCamoURL(rp.config.Camo.Host, rp.config.Camo.SharedSecret, blobURL)
558
+
}
559
+
}
560
+
535
561
user := rp.oauth.GetUser(r)
536
562
rp.pages.RepoBlob(w, pages.RepoBlobParams{
537
563
LoggedInUser: user,
···
540
566
BreadCrumbs: breadcrumbs,
541
567
ShowRendered: showRendered,
542
568
RenderToggle: renderToggle,
569
+
Unsupported: unsupported,
570
+
IsImage: isImage,
571
+
IsVideo: isVideo,
572
+
ContentSrc: contentSrc,
543
573
})
544
574
}
545
575
···
547
577
f, err := rp.repoResolver.Resolve(r)
548
578
if err != nil {
549
579
log.Println("failed to get repo and knot", err)
580
+
w.WriteHeader(http.StatusBadRequest)
550
581
return
551
582
}
552
583
···
557
588
if !rp.config.Core.Dev {
558
589
protocol = "https"
559
590
}
560
-
resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/blob/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, filePath))
591
+
blobURL := fmt.Sprintf("%s://%s/%s/%s/raw/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, filePath)
592
+
resp, err := http.Get(blobURL)
561
593
if err != nil {
562
-
log.Println("failed to reach knotserver", err)
594
+
log.Println("failed to reach knotserver:", err)
595
+
rp.pages.Error503(w)
563
596
return
564
597
}
598
+
defer resp.Body.Close()
565
599
566
-
body, err := io.ReadAll(resp.Body)
567
-
if err != nil {
568
-
log.Printf("Error reading response body: %v", err)
600
+
if resp.StatusCode != http.StatusOK {
601
+
log.Printf("knotserver returned non-OK status for raw blob %s: %d", blobURL, resp.StatusCode)
602
+
w.WriteHeader(resp.StatusCode)
603
+
_, _ = io.Copy(w, resp.Body)
569
604
return
570
605
}
571
606
572
-
var result types.RepoBlobResponse
573
-
err = json.Unmarshal(body, &result)
607
+
contentType := resp.Header.Get("Content-Type")
608
+
body, err := io.ReadAll(resp.Body)
574
609
if err != nil {
575
-
log.Println("failed to parse response:", err)
610
+
log.Printf("error reading response body from knotserver: %v", err)
611
+
w.WriteHeader(http.StatusInternalServerError)
576
612
return
577
613
}
578
614
579
-
if result.IsBinary {
580
-
w.Header().Set("Content-Type", "application/octet-stream")
615
+
if strings.Contains(contentType, "text/plain") {
616
+
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
581
617
w.Write(body)
618
+
} else if strings.HasPrefix(contentType, "image/") || strings.HasPrefix(contentType, "video/") {
619
+
w.Header().Set("Content-Type", contentType)
620
+
w.Write(body)
621
+
} else {
622
+
w.WriteHeader(http.StatusUnsupportedMediaType)
623
+
w.Write([]byte("unsupported content type"))
582
624
return
583
625
}
584
-
585
-
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
586
-
w.Write([]byte(result.Contents))
587
626
}
588
627
589
628
// modify the spindle configured for this repo