Line data Source code
1 : /*
2 : * Copyright (C) 2004-2026 Savoir-faire Linux Inc.
3 : *
4 : * This program is free software: you can redistribute it and/or modify
5 : * it under the terms of the GNU General Public License as published by
6 : * the Free Software Foundation, either version 3 of the License, or
7 : * (at your option) any later version.
8 : *
9 : * This program is distributed in the hope that it will be useful,
10 : * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 : * GNU General Public License for more details.
13 : *
14 : * You should have received a copy of the GNU General Public License
15 : * along with this program. If not, see <https://www.gnu.org/licenses/>.
16 : */
17 :
18 : #include "jamidht/transfer_channel_handler.h"
19 : #include "jamidht/conversationrepository.h"
20 : #include "data_transfer.h"
21 :
22 : #include <opendht/thread_pool.h>
23 : #include <charconv>
24 :
25 : #include "fileutils.h"
26 :
27 : namespace jami {
28 :
29 : namespace {
30 :
31 : /** "<uri>.vcf" for a member's profile: the uri must be an account hash */
32 : bool
33 26 : isMemberProfileName(std::string_view fileId)
34 : {
35 26 : constexpr std::string_view SUFFIX = ".vcf";
36 26 : if (fileId.size() != 40 + SUFFIX.size() || !fileId.ends_with(SUFFIX))
37 0 : return false;
38 26 : return static_cast<bool>(dht::InfoHash(fileId.substr(0, 40)));
39 : }
40 :
41 : } // namespace
42 :
43 796 : TransferChannelHandler::TransferChannelHandler(const std::shared_ptr<JamiAccount>& account,
44 796 : dhtnet::ConnectionManager& cm)
45 : : ChannelHandlerInterface()
46 796 : , account_(account)
47 796 : , connectionManager_(cm)
48 : {
49 796 : if (auto acc = account_.lock())
50 796 : idPath_ = fileutils::get_data_dir() / acc->getAccountID();
51 796 : }
52 :
53 1592 : TransferChannelHandler::~TransferChannelHandler() {}
54 :
55 : void
56 0 : TransferChannelHandler::connect(const DeviceId& /*deviceId*/,
57 : const std::string& /*channelName*/,
58 : ConnectCb&& /*cb*/,
59 : const std::string& /*connectionType*/,
60 : bool /*forceNewConnection*/)
61 : {
62 0 : throw std::runtime_error("connect is not supported in TransferChannelHandler");
63 : }
64 :
65 : bool
66 84 : TransferChannelHandler::onRequest(const std::shared_ptr<dht::crypto::Certificate>& cert, const std::string& name)
67 : {
68 84 : auto acc = account_.lock();
69 84 : if (!acc || !cert || !cert->issuer)
70 0 : return false;
71 84 : auto cm = acc->convModule(true);
72 84 : if (!cm)
73 0 : return false;
74 84 : auto uri = cert->issuer->getId().toString();
75 : // Else, check if it's a profile or file in a conversation.
76 84 : auto idstr = std::string_view(name).substr(DATA_TRANSFER_SCHEME.size());
77 : // Remove arguments for now
78 84 : auto sep = idstr.find_last_of('?');
79 84 : idstr = idstr.substr(0, sep);
80 84 : if (idstr == "profile.vcf") {
81 : // If it's our profile from another device
82 2 : return uri == acc->getUsername();
83 : }
84 82 : sep = idstr.find('/');
85 82 : auto lastSep = idstr.find_last_of('/');
86 82 : if (sep == std::string_view::npos || lastSep == sep)
87 0 : return false;
88 82 : auto conversationId = std::string(idstr.substr(0, sep));
89 82 : auto fileHost = idstr.substr(sep + 1, lastSep - sep - 1);
90 82 : auto fileId = idstr.substr(lastSep + 1);
91 82 : if (fileHost == acc->currentDeviceId())
92 0 : return false;
93 : // Both ids end up in filesystem paths
94 82 : if (!ConversationRepository::isValidConversationId(conversationId))
95 0 : return false;
96 :
97 : // Check if peer is member of the conversation
98 164 : if (fileId == fmt::format("{}.vcf", acc->getUsername()) || fileId == "profile.vcf") {
99 : // Or a member from the conversation
100 55 : auto members = cm->getConversationMembers(conversationId);
101 376 : return std::find_if(members.begin(), members.end(), [&](auto m) { return m["uri"] == uri; }) != members.end();
102 82 : } else if (fileHost == "profile") {
103 : // If a profile is sent, check if it's from another device
104 13 : return uri == acc->getUsername() && isMemberProfileName(fileId);
105 : }
106 :
107 14 : if (!isValidFileId(fileId))
108 0 : return false;
109 42 : return cm->onFileChannelRequest(conversationId, uri, std::string(fileId), acc->sha3SumVerify());
110 84 : }
111 :
112 : void
113 159 : TransferChannelHandler::onReady(const std::shared_ptr<dht::crypto::Certificate>&,
114 : const std::string& name,
115 : std::shared_ptr<dhtnet::ChannelSocket> channel)
116 : {
117 159 : auto acc = account_.lock();
118 160 : if (!acc)
119 0 : return;
120 :
121 : // Remove scheme
122 160 : auto idstr = name.substr(DATA_TRANSFER_SCHEME.size());
123 : // Parse arguments
124 160 : auto sep = idstr.find_last_of('?');
125 160 : std::string arguments;
126 159 : if (sep != std::string::npos) {
127 21 : arguments = idstr.substr(sep + 1);
128 22 : idstr = idstr.substr(0, sep);
129 : }
130 :
131 160 : auto start = 0u, end = 0u;
132 160 : uint64_t lastModified = 0;
133 160 : std::string sha3Sum;
134 203 : for (const auto arg : split_string(arguments, '&')) {
135 43 : auto keyVal = split_string(arg, '=');
136 43 : if (keyVal.size() == 2) {
137 43 : if (keyVal[0] == "start") {
138 4 : start = to_int<unsigned>(keyVal[1]);
139 40 : } else if (keyVal[0] == "end") {
140 4 : end = to_int<unsigned>(keyVal[1]);
141 36 : } else if (keyVal[0] == "sha3") {
142 18 : sha3Sum = keyVal[1];
143 18 : } else if (keyVal[0] == "modified") {
144 : try {
145 18 : lastModified = to_int<uint64_t>(keyVal[1]);
146 0 : } catch (const std::exception& e) {
147 0 : JAMI_WARNING("TransferChannel: Unable to parse modified date: {}: {}", keyVal[1], e.what());
148 0 : }
149 : }
150 : }
151 202 : }
152 :
153 : // Check if profile
154 159 : if (idstr == "profile.vcf") {
155 8 : dht::ThreadPool::io().run(
156 8 : [wacc = acc->weak(), path = idPath_ / "profile.vcf", channel, idstr, lastModified, sha3Sum] {
157 4 : if (auto acc = wacc.lock()) {
158 4 : if (!channel->isInitiator()) {
159 : // Only accept newest profiles
160 2 : if (lastModified == 0 || lastModified > fileutils::lastWriteTimeInSeconds(acc->profilePath()))
161 6 : acc->dataTransfer()->onIncomingProfile(channel, sha3Sum);
162 : else
163 0 : channel->shutdown();
164 : } else {
165 : // If it's a profile from sync
166 10 : acc->dataTransfer()->transferFile(channel, idstr, "", path.string());
167 : }
168 4 : }
169 4 : });
170 4 : return;
171 : }
172 :
173 155 : auto splitted_id = split_string(idstr, '/');
174 156 : if (splitted_id.size() < 3) {
175 0 : JAMI_ERROR("Unsupported ID detected {}", name);
176 0 : channel->shutdown();
177 0 : return;
178 : }
179 :
180 : // convId/fileHost/fileId or convId/profile/fileId
181 312 : auto conversationId = std::string(splitted_id[0]);
182 156 : auto fileHost = std::string(splitted_id[1]);
183 156 : auto isContactProfile = splitted_id[1] == "profile";
184 156 : auto fileId = std::string(splitted_id[splitted_id.size() - 1]);
185 156 : if (channel->isInitiator())
186 78 : return;
187 :
188 : // Profile for a member in the conversation
189 156 : dht::ThreadPool::io().run([wacc = acc->weak(),
190 78 : profilePath = idPath_ / "profile.vcf",
191 : channel,
192 : conversationId,
193 : fileId,
194 : isContactProfile,
195 : idstr,
196 : start,
197 : end,
198 : sha3Sum] {
199 78 : if (auto acc = wacc.lock()) {
200 156 : if (fileId == fmt::format("{}.vcf", acc->getUsername())) {
201 180 : acc->dataTransfer()->transferFile(channel, fileId, "", profilePath.string());
202 65 : return;
203 33 : } else if (isContactProfile && isMemberProfileName(fileId)) {
204 26 : auto path = acc->dataTransfer()->profilePath(fileId.substr(0, fileId.size() - 4));
205 52 : acc->dataTransfer()->transferFile(channel, fileId, "", path.string());
206 13 : return;
207 33 : } else if (fileId == "profile.vcf") {
208 14 : acc->dataTransfer()->onIncomingProfile(channel, sha3Sum);
209 7 : return;
210 : }
211 : // Check if it's a file in a conversation
212 13 : if (!ConversationRepository::isValidConversationId(conversationId) || !isValidFileId(fileId)) {
213 0 : channel->shutdown();
214 0 : return;
215 : }
216 13 : auto dt = acc->dataTransfer(conversationId);
217 13 : if (!dt) {
218 0 : channel->shutdown();
219 0 : return;
220 : }
221 13 : auto interactionId = fileId.substr(0, fileId.find('_'));
222 13 : auto path = dt->path(fileId);
223 13 : dt->transferFile(channel, fileId, interactionId, path.string(), start, end);
224 91 : }
225 : });
226 718 : }
227 :
228 : } // namespace jami
|