Skip to content

Commit 30a7d51

Browse files
committed
Add hashes to downloadExternalMedia URLs returned by API
1 parent 6c22c0a commit 30a7d51

File tree

11 files changed

+96
-39
lines changed

11 files changed

+96
-39
lines changed

src/main/java/smithereen/activitypub/objects/LocalImage.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,8 +127,8 @@ public JsonObject asActivityPubObject(JsonObject obj, SerializerContext serializ
127127

128128
@Override
129129
@Nullable
130-
@Contract("_, _, _, true -> !null")
131-
public URI getUriForSizeAndFormat(@NotNull Type size, @NotNull Format format, boolean is2x, boolean useFallback){
130+
@Contract("_, _, _, true, _ -> !null")
131+
public URI getUriForSizeAndFormat(@NotNull Type size, @NotNull Format format, boolean is2x, boolean useFallback, boolean addApiHash){
132132
if(fileRecord==null){
133133
LOG.warn("Tried to get a URL for a LocalImage with fileRecord not set (file ID {})", fileID);
134134
if(useFallback)

src/main/java/smithereen/api/methods/ApiUtils.java

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
import org.jetbrains.annotations.Nullable;
1010
import org.jetbrains.annotations.Unmodifiable;
1111

12+
import java.io.ByteArrayOutputStream;
13+
import java.io.IOException;
1214
import java.nio.charset.StandardCharsets;
1315
import java.util.ArrayList;
1416
import java.util.Base64;
@@ -72,6 +74,7 @@
7274

7375
public class ApiUtils{
7476
public static final byte[] UPLOAD_KEY=CryptoUtils.randomBytes(16);
77+
public static final byte[] EXTERNAL_MEDIA_KEY=CryptoUtils.randomBytes(16);
7578
public static final HashMap<Integer, Long> uploadUrlIDs=new HashMap<>();
7679
public static final AtomicInteger lastUploadUrlID=new AtomicInteger();
7780

@@ -464,9 +467,9 @@ public static ApiGroup.Link getGroupLink(ApplicationContext ctx, ApiCallContext
464467
};
465468
}
466469
return new ApiGroup.Link(l.id, l.url.toString(), title, l.getDescription(),
467-
img==null ? null : img.getUriForSizeAndFormat(SizedImage.Type.AVA_SQUARE_SMALL, actx.imageFormat).toString(),
468-
img==null ? null : img.getUriForSizeAndFormat(SizedImage.Type.AVA_SQUARE_MEDIUM, actx.imageFormat).toString(),
469-
img==null ? null : img.getUriForSizeAndFormat(SizedImage.Type.AVA_SQUARE_LARGE, actx.imageFormat).toString(),
470+
img==null ? null : img.getApiUriForSizeAndFormat(SizedImage.Type.AVA_SQUARE_SMALL, actx.imageFormat).toString(),
471+
img==null ? null : img.getApiUriForSizeAndFormat(SizedImage.Type.AVA_SQUARE_MEDIUM, actx.imageFormat).toString(),
472+
img==null ? null : img.getApiUriForSizeAndFormat(SizedImage.Type.AVA_SQUARE_LARGE, actx.imageFormat).toString(),
470473
objType, objID);
471474
}
472475

@@ -749,6 +752,21 @@ public static List<ApiPhoto> getPhotos(ApplicationContext ctx, ApiCallContext ac
749752
return photos.stream().map(p->new ApiPhoto(p, actx, interactions, tagCounts)).toList();
750753
}
751754

755+
public static String getExternalMediaHash(Map<String, String> params){
756+
List<String> hashParts=new ArrayList<>();
757+
params.forEach((k, v)->{
758+
if(!"api".equals(k))
759+
hashParts.add(k+'='+v);
760+
});
761+
hashParts.sort(String::compareTo);
762+
ByteArrayOutputStream buf=new ByteArrayOutputStream();
763+
try{
764+
buf.write(EXTERNAL_MEDIA_KEY);
765+
buf.write(String.join("&", hashParts).getBytes(StandardCharsets.UTF_8));
766+
}catch(IOException ignore){}
767+
return Base64.getUrlEncoder().withoutPadding().encodeToString(CryptoUtils.sha256(buf.toByteArray()));
768+
}
769+
752770
public record InputAttachments(@NotNull List<String> ids, @NotNull Map<String, String> altTexts, @Nullable Poll poll){
753771
}
754772
}

src/main/java/smithereen/api/model/ApiAttachment.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ public ApiAttachment(Attachment att, ApiCallContext actx, AttachmentHostContentO
2929
switch(att){
3030
case GraffitiAttachment ga -> {
3131
type="graffiti";
32-
graffiti=new Graffiti(ga.image.getUriForSizeAndFormat(SizedImage.Type.PHOTO_ORIGINAL, SizedImage.Format.PNG).toString(),
33-
ga.image.getUriForSizeAndFormat(SizedImage.Type.PHOTO_THUMB_MEDIUM, actx.imageFormat).toString(),
32+
graffiti=new Graffiti(ga.image.getApiUriForSizeAndFormat(SizedImage.Type.PHOTO_ORIGINAL, SizedImage.Format.PNG).toString(),
33+
ga.image.getApiUriForSizeAndFormat(SizedImage.Type.PHOTO_THUMB_MEDIUM, actx.imageFormat).toString(),
3434
ga.getWidth(), ga.getHeight());
3535
}
3636
case PhotoAttachment pa -> {

src/main/java/smithereen/api/model/ApiGroup.java

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -156,21 +156,21 @@ public ApiGroup(ApiCallContext actx, Group group, EnumSet<Field> fields, Map<Int
156156
SizedImage img=group.getAvatar();
157157
if(img!=null){
158158
if(fields.contains(Field.PHOTO_50))
159-
photo50=img.getUriForSizeAndFormat(SizedImage.Type.AVA_SQUARE_SMALL, actx.imageFormat).toString();
159+
photo50=img.getApiUriForSizeAndFormat(SizedImage.Type.AVA_SQUARE_SMALL, actx.imageFormat).toString();
160160
if(fields.contains(Field.PHOTO_100))
161-
photo100=img.getUriForSizeAndFormat(SizedImage.Type.AVA_SQUARE_MEDIUM, actx.imageFormat).toString();
161+
photo100=img.getApiUriForSizeAndFormat(SizedImage.Type.AVA_SQUARE_MEDIUM, actx.imageFormat).toString();
162162
if(fields.contains(Field.PHOTO_200))
163-
photo200=img.getUriForSizeAndFormat(SizedImage.Type.AVA_SQUARE_LARGE, actx.imageFormat).toString();
163+
photo200=img.getApiUriForSizeAndFormat(SizedImage.Type.AVA_SQUARE_LARGE, actx.imageFormat).toString();
164164
if(fields.contains(Field.PHOTO_400))
165-
photo400=img.getUriForSizeAndFormat(SizedImage.Type.AVA_SQUARE_XLARGE, actx.imageFormat).toString();
165+
photo400=img.getApiUriForSizeAndFormat(SizedImage.Type.AVA_SQUARE_XLARGE, actx.imageFormat).toString();
166166
if(fields.contains(Field.PHOTO_200_ORIG))
167-
photo200Orig=img.getUriForSizeAndFormat(SizedImage.Type.AVA_RECT, actx.imageFormat).toString();
167+
photo200Orig=img.getApiUriForSizeAndFormat(SizedImage.Type.AVA_RECT, actx.imageFormat).toString();
168168
if(fields.contains(Field.PHOTO_400_ORIG))
169-
photo400Orig=img.getUriForSizeAndFormat(SizedImage.Type.AVA_RECT_LARGE, actx.imageFormat).toString();
169+
photo400Orig=img.getApiUriForSizeAndFormat(SizedImage.Type.AVA_RECT_LARGE, actx.imageFormat).toString();
170170
if(fields.contains(Field.PHOTO_MAX_ORIG))
171-
photoMaxOrig=img.getUriForSizeAndFormat(SizedImage.Type.AVA_RECT_LARGE, actx.imageFormat).toString();
171+
photoMaxOrig=img.getApiUriForSizeAndFormat(SizedImage.Type.AVA_RECT_LARGE, actx.imageFormat).toString();
172172
if(fields.contains(Field.PHOTO_MAX))
173-
photoMax=img.getUriForSizeAndFormat(SizedImage.Type.AVA_SQUARE_XLARGE, actx.imageFormat).toString();
173+
photoMax=img.getApiUriForSizeAndFormat(SizedImage.Type.AVA_SQUARE_XLARGE, actx.imageFormat).toString();
174174

175175
if(fields.contains(Field.PHOTO_ID)){
176176
Photo photo=profilePhotos.get(id);

src/main/java/smithereen/api/model/ApiPhoto.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ private void populateSizes(@NotNull SizedImage image, @NotNull ApiCallContext ac
124124
sizes=new ArrayList<>();
125125
for(SizedImage.Type sz:SIZES){
126126
SizedImage.Dimensions dimensions=image.getDimensionsForSize(sz);
127-
String url=image.getUriForSizeAndFormat(sz, actx.imageFormat).toString();
127+
String url=image.getApiUriForSizeAndFormat(sz, actx.imageFormat).toString();
128128
sizes.add(new Size(sz.suffix(), url, dimensions.width, dimensions.height));
129129
if(dimensions.width<sz.getMaxWidth() && dimensions.height<sz.getMaxHeight())
130130
break;

src/main/java/smithereen/api/model/ApiUser.java

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -333,21 +333,21 @@ public ApiUser(ApiCallContext actx, User user, EnumSet<Field> fields, Map<Intege
333333
SizedImage img=user.getAvatar();
334334
if(img!=null){
335335
if(fields.contains(Field.PHOTO_50))
336-
photo50=img.getUriForSizeAndFormat(SizedImage.Type.AVA_SQUARE_SMALL, actx.imageFormat).toString();
336+
photo50=img.getApiUriForSizeAndFormat(SizedImage.Type.AVA_SQUARE_SMALL, actx.imageFormat).toString();
337337
if(fields.contains(Field.PHOTO_100))
338-
photo100=img.getUriForSizeAndFormat(SizedImage.Type.AVA_SQUARE_MEDIUM, actx.imageFormat).toString();
338+
photo100=img.getApiUriForSizeAndFormat(SizedImage.Type.AVA_SQUARE_MEDIUM, actx.imageFormat).toString();
339339
if(fields.contains(Field.PHOTO_200))
340-
photo200=img.getUriForSizeAndFormat(SizedImage.Type.AVA_SQUARE_LARGE, actx.imageFormat).toString();
340+
photo200=img.getApiUriForSizeAndFormat(SizedImage.Type.AVA_SQUARE_LARGE, actx.imageFormat).toString();
341341
if(fields.contains(Field.PHOTO_400))
342-
photo400=img.getUriForSizeAndFormat(SizedImage.Type.AVA_SQUARE_XLARGE, actx.imageFormat).toString();
342+
photo400=img.getApiUriForSizeAndFormat(SizedImage.Type.AVA_SQUARE_XLARGE, actx.imageFormat).toString();
343343
if(fields.contains(Field.PHOTO_200_ORIG))
344-
photo200Orig=img.getUriForSizeAndFormat(SizedImage.Type.AVA_RECT, actx.imageFormat).toString();
344+
photo200Orig=img.getApiUriForSizeAndFormat(SizedImage.Type.AVA_RECT, actx.imageFormat).toString();
345345
if(fields.contains(Field.PHOTO_400_ORIG))
346-
photo400Orig=img.getUriForSizeAndFormat(SizedImage.Type.AVA_RECT_LARGE, actx.imageFormat).toString();
346+
photo400Orig=img.getApiUriForSizeAndFormat(SizedImage.Type.AVA_RECT_LARGE, actx.imageFormat).toString();
347347
if(fields.contains(Field.PHOTO_MAX_ORIG))
348-
photoMaxOrig=img.getUriForSizeAndFormat(SizedImage.Type.AVA_RECT_LARGE, actx.imageFormat).toString();
348+
photoMaxOrig=img.getApiUriForSizeAndFormat(SizedImage.Type.AVA_RECT_LARGE, actx.imageFormat).toString();
349349
if(fields.contains(Field.PHOTO_MAX))
350-
photoMax=img.getUriForSizeAndFormat(SizedImage.Type.AVA_SQUARE_XLARGE, actx.imageFormat).toString();
350+
photoMax=img.getApiUriForSizeAndFormat(SizedImage.Type.AVA_SQUARE_XLARGE, actx.imageFormat).toString();
351351

352352
if(fields.contains(Field.PHOTO_ID)){
353353
Photo photo=profilePhotos.get(id);

src/main/java/smithereen/model/CachedRemoteImage.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ public File getPathInMediaCache(){
4343

4444
@Override
4545
@NotNull
46-
public URI getUriForSizeAndFormat(@NotNull Type size, @NotNull Format format, boolean is2x, boolean useFallback){
46+
public URI getUriForSizeAndFormat(@NotNull Type size, @NotNull Format format, boolean is2x, boolean useFallback, boolean addApiHash){
4747
ImgProxy.UrlBuilder builder=new ImgProxy.UrlBuilder("local://"+Config.imgproxyLocalMediaCache+"/"+cacheKey+".webp")
4848
.resize(size.getMaxWidth(), size.getMaxHeight())
4949
.format(format)

src/main/java/smithereen/model/NonCachedRemoteImage.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import java.net.URI;
66

77
import smithereen.Utils;
8+
import smithereen.api.methods.ApiUtils;
89
import smithereen.model.comments.Comment;
910
import smithereen.model.groups.GroupLink;
1011
import smithereen.model.photos.Photo;
@@ -26,17 +27,25 @@ public NonCachedRemoteImage(Args args, @NotNull Dimensions origDimensions, URI o
2627

2728
@Override
2829
@NotNull
29-
public URI getUriForSizeAndFormat(@NotNull Type size, @NotNull Format format, boolean is2x, boolean useFallback){
30+
public URI getUriForSizeAndFormat(@NotNull Type size, @NotNull Format format, boolean is2x, boolean useFallback, boolean addApiHash){
3031
UriBuilder builder=UriBuilder.local().path("system", "downloadExternalMedia");
3132
args.addToUriBuilder(builder);
3233
builder.queryParam("size", size.suffix()).queryParam("format", format.fileExtension());
3334
if(is2x)
3435
builder.queryParam("2x", "");
3536
if(useFallback)
3637
builder.queryParam("fb", "");
38+
if(addApiHash)
39+
builder.queryParam("api", ApiUtils.getExternalMediaHash(builder.getQueryParamsMap()));
3740
return builder.build();
3841
}
3942

43+
@NotNull
44+
@Override
45+
public URI getApiUriForSizeAndFormat(Type size, Format format){
46+
return getUriForSizeAndFormat(size, format, false, false, true);
47+
}
48+
4049
@Override
4150
@NotNull
4251
public Dimensions getOriginalDimensions(){

src/main/java/smithereen/model/SizedImage.java

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@
2121
public sealed interface SizedImage extends BlurHashable permits LocalImage, RemoteImage{
2222

2323
@Nullable
24-
@Contract("_, _, _, true -> !null")
25-
URI getUriForSizeAndFormat(@NotNull Type size, @NotNull Format format, boolean is2x, boolean useFallback);
24+
@Contract("_, _, _, true, _ -> !null")
25+
URI getUriForSizeAndFormat(@NotNull Type size, @NotNull Format format, boolean is2x, boolean useFallback, boolean addApiHash);
2626

2727
@NotNull
2828
Dimensions getOriginalDimensions();
@@ -43,7 +43,7 @@ default List<SizedImageURLs> getURLsForPhotoViewer(){
4343
boolean isLocal=this instanceof LocalImage;
4444
for(Type t:List.of(Type.PHOTO_SMALL, Type.PHOTO_MEDIUM, Type.PHOTO_LARGE, Type.PHOTO_ORIGINAL)){
4545
Dimensions size=getDimensionsForSize(t);
46-
urls.add(new SizedImageURLs(t.suffix, size.width, size.height, Objects.toString(getUriForSizeAndFormat(t, Format.WEBP, false, isLocal)), Objects.toString(getUriForSizeAndFormat(t, Format.JPEG, false, isLocal))));
46+
urls.add(new SizedImageURLs(t.suffix, size.width, size.height, Objects.toString(getUriForSizeAndFormat(t, Format.WEBP, false, isLocal, false)), Objects.toString(getUriForSizeAndFormat(t, Format.JPEG, false, isLocal, false))));
4747
if(size.width>=origSize.width && size.height>=origSize.height)
4848
break;
4949
}
@@ -56,7 +56,12 @@ default String getUrlForSizeAndFormat(String size, String format){
5656

5757
@NotNull
5858
default URI getUriForSizeAndFormat(Type size, Format format){
59-
return getUriForSizeAndFormat(size, format, false, true);
59+
return getUriForSizeAndFormat(size, format, false, true, false);
60+
}
61+
62+
@NotNull
63+
default URI getApiUriForSizeAndFormat(Type size, Format format){
64+
return getUriForSizeAndFormat(size, format, false, true, true);
6065
}
6166

6267
default String generateHTML(Type size, List<String> additionalClasses, List<String> extraAttrs, int width, int height, boolean add2x, String altText){
@@ -109,7 +114,7 @@ private void appendHtmlForFormat(Type size, Format format, StringBuilder sb, boo
109114
sb.append(getUriForSizeAndFormat(size, format));
110115
if(add2x){
111116
sb.append(", ");
112-
sb.append(getUriForSizeAndFormat(size.get2xType(), format, true, true));
117+
sb.append(getUriForSizeAndFormat(size.get2xType(), format, true, true, false));
113118
sb.append(" 2x");
114119
}
115120
sb.append("\" type=\"");

src/main/java/smithereen/routes/SystemRoutes.java

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,14 @@
2020
import java.time.Instant;
2121
import java.util.ArrayList;
2222
import java.util.Arrays;
23+
import java.util.Base64;
2324
import java.util.Collections;
2425
import java.util.HashMap;
2526
import java.util.List;
2627
import java.util.Locale;
2728
import java.util.Map;
2829
import java.util.Set;
30+
import java.util.function.Function;
2931
import java.util.function.Predicate;
3032
import java.util.regex.Matcher;
3133
import java.util.regex.Pattern;
@@ -46,6 +48,7 @@
4648
import smithereen.activitypub.objects.Document;
4749
import smithereen.activitypub.objects.Image;
4850
import smithereen.activitypub.objects.LocalImage;
51+
import smithereen.api.methods.ApiUtils;
4952
import smithereen.controllers.ObjectLinkResolver;
5053
import smithereen.exceptions.BadRequestException;
5154
import smithereen.exceptions.ObjectNotFoundException;
@@ -100,6 +103,7 @@
100103
import smithereen.text.TextProcessor;
101104
import smithereen.util.CaptchaGenerator;
102105
import smithereen.util.CharacterRange;
106+
import smithereen.util.CryptoUtils;
103107
import smithereen.util.JsonArrayBuilder;
104108
import smithereen.util.JsonObjectBuilder;
105109
import smithereen.util.NamedMutexCollection;
@@ -434,7 +438,17 @@ void updateContainerInDatabase(@NotNull DatabaseConnection conn, @NotNull Photo
434438
}
435439
try{
436440
SessionInfo sessionInfo=sessionInfo(req);
437-
if(sessionInfo==null || sessionInfo.account==null){ // Only download attachments for logged-in users. Prevents crawlers from causing unnecessary churn in the media cache
441+
boolean canProceed=sessionInfo!=null && sessionInfo.account!=null;
442+
boolean hadCorrectApiHash=false;
443+
if(!canProceed){
444+
String apiHash=req.queryParams("api");
445+
if(StringUtils.isNotEmpty(apiHash)){
446+
Map<String, String> params=req.queryParams().stream().collect(Collectors.toMap(Function.identity(), req::queryParams));
447+
canProceed=apiHash.equals(ApiUtils.getExternalMediaHash(params));
448+
hadCorrectApiHash=canProceed;
449+
}
450+
}
451+
if(!canProceed){ // Only download attachments for logged-in users. Prevents crawlers from causing unnecessary churn in the media cache
438452
if(req.queryParams("fb")!=null){
439453
boolean is2x=req.queryParams("2x")!=null;
440454
resp.redirect(Config.localURI(sizeType==SizedImage.Type.AVA_SQUARE_SMALL || (is2x && sizeType==SizedImage.Type.AVA_SQUARE_MEDIUM) ? "/res/broken_photo_small.svg" : "/res/broken_photo.svg").toString());
@@ -452,20 +466,27 @@ void updateContainerInDatabase(@NotNull DatabaseConnection conn, @NotNull Photo
452466
if(item==null){
453467
if(itemType==MediaCache.ItemType.AVATAR && req.queryParams("retrying")==null){
454468
try{
455-
String extraParams="";
469+
HashMap<String, String> params=new HashMap<>();
456470
if(req.queryParams("fb")!=null)
457-
extraParams+="&fb";
471+
params.put("fb", "");
458472
if(req.queryParams("2x")!=null)
459-
extraParams+="&2x";
473+
params.put("fb", "");
474+
params.put("size", sizeType.suffix());
475+
params.put("format", format.fileExtension());
476+
params.put("retrying", "");
460477
if(user!=null){
461478
ForeignUser updatedUser=context(req).getObjectLinkResolver().resolve(user.activityPubID, ForeignUser.class, true, true, true);
462-
resp.redirect(Config.localURI("/system/downloadExternalMedia?type=user_ava&user_id="+updatedUser.id+"&size="+sizeType.suffix()+"&format="+format.fileExtension()+"&retrying"+extraParams).toString());
463-
return "";
479+
params.put("type", "user_ava");
480+
params.put("user_id", updatedUser.id+"");
464481
}else{
465482
ForeignGroup updatedGroup=context(req).getObjectLinkResolver().resolve(group.activityPubID, ForeignGroup.class, true, true, true);
466-
resp.redirect(Config.localURI("/system/downloadExternalMedia?type=group_ava&user_id="+updatedGroup.id+"&size="+sizeType.suffix()+"&format="+format.fileExtension()+"&retrying"+extraParams).toString());
467-
return "";
483+
params.put("type", "group_ava");
484+
params.put("group_id", updatedGroup.id+"");
468485
}
486+
if(hadCorrectApiHash)
487+
params.put("api", ApiUtils.getExternalMediaHash(params));
488+
resp.redirect("/system/downloadExternalMedia?"+params.entrySet().stream().map(e->e.getKey()+"="+URLEncoder.encode(e.getValue(), StandardCharsets.UTF_8)).collect(Collectors.joining("&")));
489+
return "";
469490
}catch(ObjectNotFoundException ignore){}
470491
}
471492
LOG.debug("downloadExternalMedia: all attempts failed for {}", uri);

0 commit comments

Comments
 (0)