Skip to content

Commit 6761c27

Browse files
committed
feat: Add media proxy for Luma and Mastodon so that nothing reaches out to external services
Signed-off-by: Felicitas Pojtinger <felicitas@pojtinger.com>
1 parent 932af5e commit 6761c27

7 files changed

Lines changed: 94 additions & 16 deletions

File tree

assets/js/event-detail-modal.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ customElements.define(
124124

125125
const cover = $("cover");
126126
if (data.cover_url) {
127-
cover.src = proxyImageUrl(data.cover_url, api, "luma");
127+
cover.src = proxyImageUrl(data.cover_url, api);
128128
cover.alt = data.name;
129129
} else {
130130
cover.remove();

assets/js/events.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
export const proxyImageUrl = (url, api) => {
2+
if (!url) return "";
3+
return `${api}/image?url=${encodeURIComponent(url)}`;
4+
};
5+
16
export const formatEvent = (evt, api) => {
27
const start = new Date(evt.start_at);
38
const days = Math.ceil((start - new Date()) / 86400000);
@@ -27,7 +32,7 @@ export const formatEvent = (evt, api) => {
2732
? `https://luma.com/${encodeURI(evt.url)}`
2833
: "https://luma.com/vanlug",
2934
location: evt.location || "",
30-
coverUrl: evt.cover_url,
35+
coverUrl: proxyImageUrl(evt.cover_url, api),
3136
};
3237
};
3338

assets/js/mastodon-feed.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import DOMPurify from "dompurify";
2+
import { proxyImageUrl } from "./events.js";
23

34
const tootTemplate = document.createElement("template");
45
tootTemplate.innerHTML = `
@@ -12,7 +13,7 @@ tootTemplate.innerHTML = `
1213
</div>
1314
</a>`;
1415

15-
const renderToot = (toot, data, { isFirst, isLast, inDrawer, profile }) => {
16+
const renderToot = (toot, data, { isFirst, isLast, inDrawer, profile, api }) => {
1617
const clone = tootTemplate.content.cloneNode(true);
1718
const $ = (s) => clone.querySelector(`[data-slot="${s}"]`);
1819
const item = clone.querySelector("a");
@@ -37,7 +38,7 @@ const renderToot = (toot, data, { isFirst, isLast, inDrawer, profile }) => {
3738
const mediaSlot = $("media");
3839
for (const m of toot.media?.slice(0, 1) ?? []) {
3940
const el = document.createElement(m.isVideo ? "video" : "img");
40-
el.src = m.url;
41+
el.src = proxyImageUrl(m.url, api);
4142
if (m.isVideo) el.controls = true;
4243
else el.alt = m.altText || "";
4344
el.className = "pf-v6-u-w-100";
@@ -49,7 +50,7 @@ const renderToot = (toot, data, { isFirst, isLast, inDrawer, profile }) => {
4950
if (!mediaSlot.children.length) mediaSlot.remove();
5051

5152
const avatar = $("avatar");
52-
avatar.src = data.userProfilePictureURL;
53+
avatar.src = proxyImageUrl(data.userProfilePictureURL, api);
5354
avatar.alt = data.userDisplayName;
5455

5556
$("timestamp").textContent = new Date(toot.timestamp).toLocaleDateString(
@@ -105,6 +106,7 @@ customElements.define(
105106
isLast: i === data.toots.length - 1,
106107
inDrawer,
107108
profile,
109+
api,
108110
}),
109111
);
110112
}

content/privacy.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ When accessing our website, the following information is processed for the reaso
3232

3333
#### 3.2 Luma Event Data
3434

35-
Our home page and events page fetch event data from Luma's API via our API proxy on Railway. Your browser connects to our proxy, not directly to Luma. No data is sent to Luma from your browser. We also provide a RSS/Atom feed of upcoming events via the same proxy; when you subscribe to it, your feed reader connects to our proxy, which fetches event listings and per-event descriptions from Luma's API on your behalf.
35+
Our home page and events page fetch event data from Luma's API and images from Luma's CDN via our proxy on Railway. Your browser connects to our proxy, not directly to Luma. No data is sent to Luma from your browser. We also provide the RSS/Atom feed of upcoming events and their images via the same proxy.
3636

3737
Luma, Inc.\
3838
548 Market St PMB 36143\
@@ -42,7 +42,7 @@ Privacy policy: [https://luma.com/privacy-policy](https://luma.com/privacy-polic
4242

4343
#### 3.3 Mastodon Feed Data
4444

45-
Our home page fetches recent posts from our Mastodon account via our API proxy on Railway. Your browser connects to our proxy, not directly to [thecanadian.social](https://thecanadian.social). No data is sent to [thecanadian.social](https://thecanadian.social) from your browser. We also link to the public Mastodon RSS feed at `thecanadian.social/@vanlug.rss`; if you subscribe to it, your feed reader connects directly to [thecanadian.social](https://thecanadian.social).
45+
Our home page fetches recent posts and media from our Mastodon account via our proxy on Railway. Your browser connects to our proxy, not directly to [thecanadian.social](https://thecanadian.social). No data is sent to [thecanadian.social](https://thecanadian.social) from your browser. We also link to the public Mastodon RSS feed at `thecanadian.social/@vanlug.rss`; if you subscribe to it, your feed reader connects directly to [thecanadian.social](https://thecanadian.social).
4646

4747
The Canadian ([thecanadian.social](https://thecanadian.social))\
4848
Kelowna and Burnaby, British Columbia\

main.go

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,6 @@ func Handler(w http.ResponseWriter, r *http.Request) {
2121
handlers.NextEventHandler(w, r, defaultLumaAPIBase)
2222
}
2323

24-
func FeedHandler(w http.ResponseWriter, r *http.Request) {
25-
handlers.EventsFeedHandler(w, r, defaultLumaAPIBase, defaultLumaEventDetailBase, defaultLumaBase, defaultMapBase, "https://vanlug.ca/")
26-
}
27-
2824
func main() {
2925
port := os.Getenv("PORT")
3026
if port == "" {
@@ -61,6 +57,11 @@ func main() {
6157
siteURL = "https://vanlug.ca/"
6258
}
6359

60+
apiURL := os.Getenv("API_URL")
61+
if apiURL == "" {
62+
apiURL = "http://localhost:" + port
63+
}
64+
6465
mastodonServer := os.Getenv("MASTODON_SERVER")
6566
if mastodonServer == "" {
6667
mastodonServer = "https://thecanadian.social"
@@ -127,7 +128,7 @@ func main() {
127128
}
128129
}()
129130

130-
handlers.EventsFeedHandler(w, r, apiBase, eventDetailBase, lumaBase, mapBase, siteURL)
131+
handlers.EventsFeedHandler(w, r, apiBase, eventDetailBase, lumaBase, mapBase, siteURL, apiURL)
131132
}))
132133

133134
mux.HandleFunc("/events/detail", cors(func(w http.ResponseWriter, r *http.Request) {
@@ -144,6 +145,20 @@ func main() {
144145
handlers.EventDetailHandler(w, r, eventDetailBase, lumaBase, mapBase)
145146
}))
146147

148+
mux.HandleFunc("/image", cors(func(w http.ResponseWriter, r *http.Request) {
149+
defer func() {
150+
if err := recover(); err != nil {
151+
log.Println("Error occured in image proxy:", err)
152+
153+
http.Error(w, "Error occured in image proxy", http.StatusInternalServerError)
154+
155+
return
156+
}
157+
}()
158+
159+
handlers.ImageProxyHandler(w, r)
160+
}))
161+
147162
mux.HandleFunc("/mastodon", cors(func(w http.ResponseWriter, r *http.Request) {
148163
defer func() {
149164
if err := recover(); err != nil {

pkg/handlers/images.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package handlers
2+
3+
import (
4+
"io"
5+
"net/http"
6+
"net/url"
7+
)
8+
9+
var allowedImageHosts = map[string]bool{
10+
"images.lumacdn.com": true,
11+
"s3.us-west-000.backblazeb2.com": true,
12+
}
13+
14+
func ImageProxyHandler(w http.ResponseWriter, r *http.Request) {
15+
upstream := r.URL.Query().Get("url")
16+
if upstream == "" {
17+
http.Error(w, "missing url query parameter", http.StatusBadRequest)
18+
return
19+
}
20+
21+
parsed, err := url.Parse(upstream)
22+
if err != nil || parsed.Scheme != "https" {
23+
http.Error(w, "invalid url", http.StatusBadRequest)
24+
return
25+
}
26+
27+
if !allowedImageHosts[parsed.Host] {
28+
http.Error(w, "host not allowed", http.StatusForbidden)
29+
return
30+
}
31+
32+
resp, err := http.Get(upstream)
33+
if err != nil {
34+
http.Error(w, "failed to fetch media", http.StatusBadGateway)
35+
return
36+
}
37+
defer resp.Body.Close()
38+
39+
if resp.StatusCode != http.StatusOK {
40+
http.Error(w, "upstream error", resp.StatusCode)
41+
return
42+
}
43+
44+
if ct := resp.Header.Get("Content-Type"); ct != "" {
45+
w.Header().Set("Content-Type", ct)
46+
}
47+
w.Header().Set("Cache-Control", "public, max-age=86400")
48+
if cl := resp.Header.Get("Content-Length"); cl != "" {
49+
w.Header().Set("Content-Length", cl)
50+
}
51+
52+
io.Copy(w, resp.Body)
53+
}

pkg/handlers/luma.go

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -452,7 +452,7 @@ func EventDetailHandler(w http.ResponseWriter, r *http.Request, eventDetailBase
452452
fmt.Fprintf(w, "%v", string(j))
453453
}
454454

455-
func EventsFeedHandler(w http.ResponseWriter, r *http.Request, apiBase string, eventDetailBase string, lumaBase string, mapBase string, siteURL string) {
455+
func EventsFeedHandler(w http.ResponseWriter, r *http.Request, apiBase string, eventDetailBase string, lumaBase string, mapBase string, siteURL string, apiURL string) {
456456
items, err := lumaFetchItems(r, apiBase, "20")
457457
if err != nil {
458458
w.Write([]byte(err.Error()))
@@ -493,6 +493,10 @@ func EventsFeedHandler(w http.ResponseWriter, r *http.Request, apiBase string, e
493493
for i, item := range items {
494494
data := buildEntryData(item, descriptions[i], lumaBase, mapBase)
495495

496+
if data.CoverURL != "" {
497+
data.CoverURL = apiURL + "/image?" + url.Values{"url": {data.CoverURL}}.Encode()
498+
}
499+
496500
published := now
497501
if t, err := time.Parse(time.RFC3339, item.StartAt); err == nil {
498502
published = t
@@ -518,10 +522,9 @@ func EventsFeedHandler(w http.ResponseWriter, r *http.Request, apiBase string, e
518522
Content: buf.String(),
519523
}
520524

521-
if item.CoverURL != "" {
525+
if data.CoverURL != "" {
522526
feedItem.Enclosure = &feeds.Enclosure{
523-
Url: item.CoverURL,
524-
Type: "image/jpeg",
527+
Url: data.CoverURL,
525528
}
526529
}
527530

0 commit comments

Comments
 (0)