Improve cache busting proxy URLs with file exts

Fixes #287.
This commit is contained in:
Andrew Godwin 2022-12-28 11:57:54 -07:00
parent 296780d5cc
commit 6137149f55
5 changed files with 57 additions and 24 deletions

View file

@ -17,7 +17,12 @@ from core.html import strip_html
from core.ld import format_ld_date from core.ld import format_ld_date
from core.models import Config from core.models import Config
from core.uploads import upload_emoji_namer from core.uploads import upload_emoji_namer
from core.uris import AutoAbsoluteUrl, RelativeAbsoluteUrl, StaticAbsoluteUrl from core.uris import (
AutoAbsoluteUrl,
ProxyAbsoluteUrl,
RelativeAbsoluteUrl,
StaticAbsoluteUrl,
)
from stator.models import State, StateField, StateGraph, StatorModel from stator.models import State, StateField, StateGraph, StatorModel
from users.models import Domain from users.models import Domain
@ -169,8 +174,9 @@ class Emoji(StatorModel):
if self.file: if self.file:
return AutoAbsoluteUrl(self.file.url) return AutoAbsoluteUrl(self.file.url)
elif self.remote_url: elif self.remote_url:
return AutoAbsoluteUrl( return ProxyAbsoluteUrl(
f"/proxy/emoji/{self.pk}/", hash_tail_input=self.remote_url f"/proxy/emoji/{self.pk}/",
remote_url=self.remote_url,
) )
return StaticAbsoluteUrl("img/blank-emoji-128.png") return StaticAbsoluteUrl("img/blank-emoji-128.png")

View file

@ -3,7 +3,7 @@ from functools import partial
from django.db import models from django.db import models
from core.uploads import upload_namer from core.uploads import upload_namer
from core.uris import AutoAbsoluteUrl, RelativeAbsoluteUrl from core.uris import ProxyAbsoluteUrl, RelativeAbsoluteUrl
from stator.models import State, StateField, StateGraph, StatorModel from stator.models import State, StateField, StateGraph, StatorModel
@ -81,16 +81,18 @@ class PostAttachment(StatorModel):
elif self.file: elif self.file:
return RelativeAbsoluteUrl(self.file.url) return RelativeAbsoluteUrl(self.file.url)
else: else:
return AutoAbsoluteUrl( return ProxyAbsoluteUrl(
f"/proxy/post_attachment/{self.pk}/", hash_tail_input=self.remote_url f"/proxy/post_attachment/{self.pk}/",
remote_url=self.remote_url,
) )
def full_url(self): def full_url(self):
if self.file: if self.file:
return RelativeAbsoluteUrl(self.file.url) return RelativeAbsoluteUrl(self.file.url)
else: else:
return AutoAbsoluteUrl( return ProxyAbsoluteUrl(
f"/proxy/post_attachment/{self.pk}/", hash_tail_input=self.remote_url f"/proxy/post_attachment/{self.pk}/",
remote_url=self.remote_url,
) )
### ActivityPub ### ### ActivityPub ###

View file

@ -32,15 +32,8 @@ class AutoAbsoluteUrl(RelativeAbsoluteUrl):
self, self,
relative: str, relative: str,
identity=None, identity=None,
hash_tail_input: str | None = None,
hash_tail_length: int = 10,
): ):
self.relative = relative self.relative = relative
if hash_tail_input:
# When provided, attach a hash of the input (typically the proxied URL)
# SHA1 chosen as it generally has the best performance in modern python, and security is not a concern
# Hash truncation is generally fine, as in the typical use case the hash is scoped to the identity PK
self.relative += f"{hashlib.sha1(hash_tail_input.encode('ascii')).hexdigest()[:hash_tail_length]}/"
if identity: if identity:
absolute_prefix = f"https://{identity.domain.uri_domain}/" absolute_prefix = f"https://{identity.domain.uri_domain}/"
else: else:
@ -48,6 +41,31 @@ class AutoAbsoluteUrl(RelativeAbsoluteUrl):
self.absolute = urljoin(absolute_prefix, self.relative) self.absolute = urljoin(absolute_prefix, self.relative)
class ProxyAbsoluteUrl(AutoAbsoluteUrl):
"""
AutoAbsoluteUrl variant for proxy paths, that also attaches a remote URI hash
plus extension to the end if it can.
"""
def __init__(
self,
relative: str,
identity=None,
remote_url: str | None = None,
):
if remote_url:
# See if there is a file extension we can grab
extension = "bin"
remote_filename = remote_url.split("/")[-1]
if "." in remote_filename:
extension = remote_filename.split(".")[-1]
# When provided, attach a hash of the remote URL
# SHA1 chosen as it generally has the best performance in modern python, and security is not a concern
# Hash truncation is generally fine, as in the typical use case the hash is scoped to the identity PK.
relative += f"{hashlib.sha1(remote_url.encode('ascii')).hexdigest()[:10]}.{extension}"
super().__init__(relative, identity)
class StaticAbsoluteUrl(RelativeAbsoluteUrl): class StaticAbsoluteUrl(RelativeAbsoluteUrl):
""" """
Creates static URLs given only the static-relative path Creates static URLs given only the static-relative path

View file

@ -212,22 +212,22 @@ urlpatterns = [
path("debug/oauth_authorize/", debug.OauthAuthorize.as_view()), path("debug/oauth_authorize/", debug.OauthAuthorize.as_view()),
# Media/image proxy # Media/image proxy
re_path( re_path(
"^proxy/identity_icon/(?P<identity_id>[^/]+)/((?P<image_hash>[^/]+)/)?$", "^proxy/identity_icon/(?P<identity_id>[^/]+)/((?P<image_hash>[^/]+))?$",
mediaproxy.IdentityIconCacheView.as_view(), mediaproxy.IdentityIconCacheView.as_view(),
name="proxy_identity_icon", name="proxy_identity_icon",
), ),
re_path( re_path(
"^proxy/identity_image/(?P<identity_id>[^/]+)/((?P<image_hash>[^/]+)/)?$", "^proxy/identity_image/(?P<identity_id>[^/]+)/((?P<image_hash>[^/]+))?$",
mediaproxy.IdentityImageCacheView.as_view(), mediaproxy.IdentityImageCacheView.as_view(),
name="proxy_identity_image", name="proxy_identity_image",
), ),
re_path( re_path(
"^proxy/post_attachment/(?P<attachment_id>[^/]+)/((?P<image_hash>[^/]+)/)?$", "^proxy/post_attachment/(?P<attachment_id>[^/]+)/((?P<image_hash>[^/]+))?$",
mediaproxy.PostAttachmentCacheView.as_view(), mediaproxy.PostAttachmentCacheView.as_view(),
name="proxy_post_attachment", name="proxy_post_attachment",
), ),
re_path( re_path(
"^proxy/emoji/(?P<emoji_id>[^/]+)/((?P<image_hash>[^/]+)/)?$", "^proxy/emoji/(?P<emoji_id>[^/]+)/((?P<image_hash>[^/]+))?$",
mediaproxy.EmojiCacheView.as_view(), mediaproxy.EmojiCacheView.as_view(),
name="proxy_emoji", name="proxy_emoji",
), ),

View file

@ -23,7 +23,12 @@ from core.ld import (
from core.models import Config from core.models import Config
from core.signatures import HttpSignature, RsaKeys from core.signatures import HttpSignature, RsaKeys
from core.uploads import upload_namer from core.uploads import upload_namer
from core.uris import AutoAbsoluteUrl, RelativeAbsoluteUrl, StaticAbsoluteUrl from core.uris import (
AutoAbsoluteUrl,
ProxyAbsoluteUrl,
RelativeAbsoluteUrl,
StaticAbsoluteUrl,
)
from stator.models import State, StateField, StateGraph, StatorModel from stator.models import State, StateField, StateGraph, StatorModel
from users.models.domain import Domain from users.models.domain import Domain
from users.models.system_actor import SystemActor from users.models.system_actor import SystemActor
@ -268,8 +273,9 @@ class Identity(StatorModel):
if self.icon: if self.icon:
return RelativeAbsoluteUrl(self.icon.url) return RelativeAbsoluteUrl(self.icon.url)
elif self.icon_uri: elif self.icon_uri:
return AutoAbsoluteUrl( return ProxyAbsoluteUrl(
f"/proxy/identity_icon/{self.pk}/", hash_tail_input=self.icon_uri f"/proxy/identity_icon/{self.pk}/",
remote_url=self.icon_uri,
) )
else: else:
return StaticAbsoluteUrl("img/unknown-icon-128.png") return StaticAbsoluteUrl("img/unknown-icon-128.png")
@ -281,8 +287,9 @@ class Identity(StatorModel):
if self.image: if self.image:
return AutoAbsoluteUrl(self.image.url) return AutoAbsoluteUrl(self.image.url)
elif self.image_uri: elif self.image_uri:
return AutoAbsoluteUrl( return ProxyAbsoluteUrl(
f"/proxy/identity_image/{self.pk}/", hash_tail_input=self.image_uri f"/proxy/identity_image/{self.pk}/",
remote_url=self.image_uri,
) )
return None return None