Skip to content

Commit 75bfa2a

Browse files
committed
msglist: Use HTTP header instead of URL query param to auth image requests
Keeping the user's API key out of these URLs makes it easier to avoid leaking the API key, e.g. to the clipboard with a future "copy image link" feature.
1 parent 9cbcac1 commit 75bfa2a

File tree

2 files changed

+25
-28
lines changed

2 files changed

+25
-28
lines changed

lib/widgets/content.dart

Lines changed: 20 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import 'package:flutter/material.dart';
22
import 'package:html/dom.dart' as dom;
33

4+
import '../api/core.dart';
45
import '../api/model/model.dart';
56
import '../model/content.dart';
67
import '../model/store.dart';
@@ -184,7 +185,7 @@ class MessageImage extends StatelessWidget {
184185
final src = node.srcUrl;
185186

186187
final store = PerAccountStoreWidget.of(context);
187-
final adjustedSrc = rewriteImageUrl(src, store.account);
188+
final resolvedSrc = resolveUrl(src, store.account);
188189

189190
return Align(
190191
alignment: Alignment.centerLeft,
@@ -200,8 +201,11 @@ class MessageImage extends StatelessWidget {
200201
alignment: Alignment.center,
201202
color: const Color.fromRGBO(0, 0, 0, 0.03),
202203
child: Image.network(
203-
adjustedSrc,
204+
resolvedSrc,
204205
filterQuality: FilterQuality.medium,
206+
headers: isUrlOnRealm(resolvedSrc, store.account)
207+
? Map.fromEntries([authHeader(store.account)])
208+
: null,
205209
))));
206210
}
207211
}
@@ -442,7 +446,7 @@ class MessageImageEmoji extends StatelessWidget {
442446
@override
443447
Widget build(BuildContext context) {
444448
final store = PerAccountStoreWidget.of(context);
445-
final adjustedSrc = rewriteImageUrl(node.src, store.account);
449+
final resolvedSrc = resolveUrl(node.src, store.account);
446450

447451
const size = 20.0;
448452

@@ -456,7 +460,10 @@ class MessageImageEmoji extends StatelessWidget {
456460
// too low.
457461
top: -1.5,
458462
child: Image.network(
459-
adjustedSrc.toString(),
463+
resolvedSrc.toString(),
464+
headers: isUrlOnRealm(resolvedSrc, store.account)
465+
? Map.fromEntries([authHeader(store.account)])
466+
: null,
460467
filterQuality: FilterQuality.medium,
461468
width: size,
462469
height: size,
@@ -469,32 +476,18 @@ class MessageImageEmoji extends StatelessWidget {
469476
// Small helpers.
470477
//
471478

472-
/// Resolve URL if relative; add the user's API key if appropriate.
473-
///
474-
/// The API key is added if the URL is on the realm, and is an endpoint
475-
/// known to require authentication (and to accept it in this form.)
476-
String rewriteImageUrl(String src, Account account) {
479+
/// Resolve `src` to `account`'s realm, if relative
480+
String resolveUrl(String url, Account account) {
477481
final realmUrl = Uri.parse(account.realmUrl); // TODO clean this up
478-
final resolved = realmUrl.resolve(src); // TODO handle if fails to parse
479-
480-
Uri adjustedSrc = resolved;
481-
if (resolved.origin == realmUrl.origin) {
482-
if (_kInlineApiRoutes.any((regexp) => regexp.hasMatch(resolved.path))) {
483-
final delimiter = resolved.query.isNotEmpty ? '&' : '';
484-
adjustedSrc = resolved
485-
.resolve('?${resolved.query}${delimiter}api_key=${account.apiKey}');
486-
}
487-
}
488-
489-
return adjustedSrc.toString();
482+
final resolved = realmUrl.resolve(url); // TODO handle if fails to parse
483+
return resolved.toString();
490484
}
491485

492-
/// List of routes which accept the API key appended as a GET parameter.
493-
final List<RegExp> _kInlineApiRoutes = [
494-
RegExp(r'^/user_uploads/'),
495-
RegExp(r'^/thumbnail$'),
496-
RegExp(r'^/avatar/')
497-
];
486+
/// Whether the given absolute URL has the same origin as the account's realm.
487+
// TODO: Take a `Uri` instead of String for `url`; remove "absolute" from doc
488+
bool isUrlOnRealm(String url, Account account) {
489+
return Uri.parse(url).origin == Uri.parse(account.realmUrl).origin;
490+
}
498491

499492
InlineSpan _errorUnimplemented(UnimplementedNode node) {
500493
// For now this shows error-styled HTML code even in release mode,

lib/widgets/message_list.dart

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import 'dart:io' show Platform;
22
import 'package:flutter/material.dart';
33
import 'package:intl/intl.dart';
44

5+
import '../api/core.dart';
56
import '../api/model/model.dart';
67
import '../model/content.dart';
78
import '../model/message_list.dart';
@@ -290,12 +291,15 @@ class MessageWithSender extends StatelessWidget {
290291

291292
final avatarUrl = message.avatar_url == null // TODO get from user data
292293
? null // TODO handle computing gravatars
293-
: rewriteImageUrl(message.avatar_url!, store.account);
294+
: resolveUrl(message.avatar_url!, store.account);
294295
final avatar = (avatarUrl == null)
295296
? const SizedBox.shrink()
296297
: Image.network(
297298
avatarUrl,
298299
filterQuality: FilterQuality.medium,
300+
headers: isUrlOnRealm(avatarUrl, store.account)
301+
? Map.fromEntries([authHeader(store.account)])
302+
: null,
299303
);
300304

301305
final time = _kMessageTimestampFormat

0 commit comments

Comments
 (0)