Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 25 additions & 8 deletions src/network/backends/session_open_group_server.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,24 +19,41 @@ namespace session::network::open_group_server {
constexpr std::string_view ENDPOINT_ROOM = "room/{}";
constexpr std::string_view ENDPOINT_FILE = "room/{}/file/{}";

// Some clients (e.g. older Android versions) generate community file download urls without the
// room segment (`{base_url}/file/{file_id}`). We accept these as a fallback so their attachments
// remain downloadable.
constexpr std::string_view LEGACY_ENDPOINT_FILE = "file/{}";

std::optional<DownloadInfo> parse_download_url(std::string_view url) {
// Expected format: {base_url}/room/{room}/file/{file_id}(?:#d)
// Examples:
// https://example.com/room/file/123
// https://example.com/room/file/123#d
// https://example.com/room/test/file/123
// https://example.com/room/test/file/123#d
// Legacy fallback (no room, e.g. from older Android clients): {base_url}/file/{file_id}
// https://example.com/file/123
DownloadInfo info{};

int64_t file_id;

auto match = backends::match_endpoint(ENDPOINT_FILE, url);

if (!match || match->base.empty() || match->captures.size() != 2)
return std::nullopt;
if (match && !match->base.empty() && match->captures.size() == 2) {
info.room = std::string{match->captures[0]};

info.room = std::string{match->captures[0]};
if (!quic::parse_int(match->captures[1], file_id))
return std::nullopt;
} else {
// Fall back to the legacy room-less endpoint. The room can't be recovered from such a url
// so it's left empty for the caller to populate from context (the conversation the
// attachment belongs to).
match = backends::match_endpoint(LEGACY_ENDPOINT_FILE, url);

int64_t file_id;
if (!match || match->base.empty() || match->captures.size() != 1)
return std::nullopt;

if (!quic::parse_int(match->captures[1], file_id))
return std::nullopt;
if (!quic::parse_int(match->captures[0], file_id))
return std::nullopt;
}

info.file_id = file_id;
info.base_url = match->base;
Expand Down
18 changes: 18 additions & 0 deletions tests/test_backed_session_open_group_server.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,24 @@ TEST_CASE("Download url parsing", "[backend][session_open_group_server]") {
CHECK(parsed_download_url->file_id == 123);
CHECK_FALSE(parsed_download_url->wants_stream_decryption);

// Falls back to the legacy room-less format (used by some older clients, e.g. Android),
// leaving the room empty for the caller to populate from context
parsed_download_url = open_group_server::parse_download_url("https://example.com/file/123"sv);
REQUIRE(parsed_download_url.has_value());
CHECK(parsed_download_url->base_url == "https://example.com"sv);
CHECK(parsed_download_url->room.empty());
CHECK(parsed_download_url->file_id == 123);
CHECK_FALSE(parsed_download_url->wants_stream_decryption);

// Handles the legacy room-less format with a trailing slash and stream-decryption fragment
parsed_download_url =
open_group_server::parse_download_url("https://example.com/file/123/#d"sv);
REQUIRE(parsed_download_url.has_value());
CHECK(parsed_download_url->base_url == "https://example.com"sv);
CHECK(parsed_download_url->room.empty());
CHECK(parsed_download_url->file_id == 123);
CHECK(parsed_download_url->wants_stream_decryption);

// Doesn't have an issue with a url that isn't in the right format
parsed_download_url =
open_group_server::parse_download_url("https://example.com/test/test2/test3/5432"sv);
Expand Down