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 :
20 : #include <opendht/thread_pool.h>
21 : #include <charconv>
22 :
23 : #include "fileutils.h"
24 :
25 : namespace jami {
26 :
27 668 : TransferChannelHandler::TransferChannelHandler(const std::shared_ptr<JamiAccount>& account,
28 668 : dhtnet::ConnectionManager& cm)
29 : : ChannelHandlerInterface()
30 668 : , account_(account)
31 668 : , connectionManager_(cm)
32 : {
33 668 : if (auto acc = account_.lock())
34 668 : idPath_ = fileutils::get_data_dir() / acc->getAccountID();
35 668 : }
36 :
37 1336 : TransferChannelHandler::~TransferChannelHandler() {}
38 :
39 : void
40 0 : TransferChannelHandler::connect(const DeviceId& /*deviceId*/,
41 : const std::string& /*channelName*/,
42 : ConnectCb&& /*cb*/,
43 : const std::string& /*connectionType*/,
44 : bool /*forceNewConnection*/)
45 : {
46 0 : throw std::runtime_error("connect is not supported in TransferChannelHandler");
47 : }
48 :
49 : bool
50 69 : TransferChannelHandler::onRequest(const std::shared_ptr<dht::crypto::Certificate>& cert, const std::string& name)
51 : {
52 69 : auto acc = account_.lock();
53 69 : if (!acc || !cert || !cert->issuer)
54 0 : return false;
55 69 : auto cm = acc->convModule(true);
56 69 : if (!cm)
57 0 : return false;
58 69 : auto uri = cert->issuer->getId().toString();
59 : // Else, check if it's a profile or file in a conversation.
60 69 : auto idstr = std::string_view(name).substr(DATA_TRANSFER_SCHEME.size());
61 : // Remove arguments for now
62 69 : auto sep = idstr.find_last_of('?');
63 69 : idstr = idstr.substr(0, sep);
64 69 : if (idstr == "profile.vcf") {
65 : // If it's our profile from another device
66 2 : return uri == acc->getUsername();
67 : }
68 67 : sep = idstr.find('/');
69 67 : auto lastSep = idstr.find_last_of('/');
70 67 : auto conversationId = std::string(idstr.substr(0, sep));
71 67 : auto fileHost = idstr.substr(sep + 1, lastSep - sep - 1);
72 67 : auto fileId = idstr.substr(lastSep + 1);
73 67 : if (fileHost == acc->currentDeviceId())
74 0 : return false;
75 :
76 : // Check if peer is member of the conversation
77 134 : if (fileId == fmt::format("{}.vcf", acc->getUsername()) || fileId == "profile.vcf") {
78 : // Or a member from the conversation
79 42 : auto members = cm->getConversationMembers(conversationId);
80 123 : return std::find_if(members.begin(), members.end(), [&](auto m) { return m["uri"] == uri; }) != members.end();
81 67 : } else if (fileHost == "profile") {
82 : // If a profile is sent, check if it's from another device
83 12 : return uri == acc->getUsername();
84 : }
85 :
86 13 : return cm->onFileChannelRequest(conversationId, uri, std::string(fileId), acc->sha3SumVerify());
87 69 : }
88 :
89 : void
90 130 : TransferChannelHandler::onReady(const std::shared_ptr<dht::crypto::Certificate>&,
91 : const std::string& name,
92 : std::shared_ptr<dhtnet::ChannelSocket> channel)
93 : {
94 130 : auto acc = account_.lock();
95 131 : if (!acc)
96 0 : return;
97 :
98 : // Remove scheme
99 131 : auto idstr = name.substr(DATA_TRANSFER_SCHEME.size());
100 : // Parse arguments
101 131 : auto sep = idstr.find_last_of('?');
102 131 : std::string arguments;
103 131 : if (sep != std::string::npos) {
104 16 : arguments = idstr.substr(sep + 1);
105 16 : idstr = idstr.substr(0, sep);
106 : }
107 :
108 131 : auto start = 0u, end = 0u;
109 131 : uint64_t lastModified = 0;
110 131 : std::string sha3Sum;
111 163 : for (const auto arg : split_string(arguments, '&')) {
112 32 : auto keyVal = split_string(arg, '=');
113 32 : if (keyVal.size() == 2) {
114 32 : if (keyVal[0] == "start") {
115 4 : start = to_int<unsigned>(keyVal[1]);
116 28 : } else if (keyVal[0] == "end") {
117 4 : end = to_int<unsigned>(keyVal[1]);
118 24 : } else if (keyVal[0] == "sha3") {
119 12 : sha3Sum = keyVal[1];
120 12 : } else if (keyVal[0] == "modified") {
121 : try {
122 12 : lastModified = to_int<uint64_t>(keyVal[1]);
123 0 : } catch (const std::exception& e) {
124 0 : JAMI_WARNING("TransferChannel: Unable to parse modified date: {}: {}", keyVal[1], e.what());
125 0 : }
126 : }
127 : }
128 161 : }
129 :
130 : // Check if profile
131 129 : if (idstr == "profile.vcf") {
132 8 : dht::ThreadPool::io().run(
133 4 : [wacc = acc->weak(), path = idPath_ / "profile.vcf", channel, idstr, lastModified, sha3Sum] {
134 4 : if (auto acc = wacc.lock()) {
135 4 : if (!channel->isInitiator()) {
136 : // Only accept newest profiles
137 2 : if (lastModified == 0 || lastModified > fileutils::lastWriteTimeInSeconds(acc->profilePath()))
138 2 : acc->dataTransfer()->onIncomingProfile(channel, sha3Sum);
139 : else
140 0 : channel->shutdown();
141 : } else {
142 : // If it's a profile from sync
143 2 : acc->dataTransfer()->transferFile(channel, idstr, "", path.string());
144 : }
145 4 : }
146 4 : });
147 4 : return;
148 : }
149 :
150 127 : auto splitted_id = split_string(idstr, '/');
151 127 : if (splitted_id.size() < 3) {
152 0 : JAMI_ERROR("Unsupported ID detected {}", name);
153 0 : channel->shutdown();
154 0 : return;
155 : }
156 :
157 : // convId/fileHost/fileId or convId/profile/fileId
158 127 : auto conversationId = std::string(splitted_id[0]);
159 127 : auto fileHost = std::string(splitted_id[1]);
160 126 : auto isContactProfile = splitted_id[1] == "profile";
161 125 : auto fileId = std::string(splitted_id[splitted_id.size() - 1]);
162 126 : if (channel->isInitiator())
163 62 : return;
164 :
165 : // Profile for a member in the conversation
166 130 : dht::ThreadPool::io().run([wacc = acc->weak(),
167 65 : profilePath = idPath_ / "profile.vcf",
168 : channel,
169 : conversationId,
170 : fileId,
171 : isContactProfile,
172 : idstr,
173 : start,
174 : end,
175 40 : sha3Sum] {
176 65 : if (auto acc = wacc.lock()) {
177 130 : if (fileId == fmt::format("{}.vcf", acc->getUsername())) {
178 37 : acc->dataTransfer()->transferFile(channel, fileId, "", profilePath.string());
179 53 : return;
180 28 : } else if (isContactProfile && fileId.find(".vcf") != std::string::npos) {
181 24 : auto path = acc->dataTransfer()->profilePath(fileId.substr(0, fileId.size() - 4));
182 12 : acc->dataTransfer()->transferFile(channel, fileId, "", path.string());
183 12 : return;
184 28 : } else if (fileId == "profile.vcf") {
185 4 : acc->dataTransfer()->onIncomingProfile(channel, sha3Sum);
186 4 : return;
187 : }
188 : // Check if it's a file in a conversation
189 12 : auto dt = acc->dataTransfer(conversationId);
190 12 : auto sep = fileId.find('_');
191 12 : if (!dt or sep == std::string::npos) {
192 0 : channel->shutdown();
193 0 : return;
194 : }
195 12 : auto interactionId = fileId.substr(0, sep);
196 12 : auto path = dt->path(fileId);
197 12 : dt->transferFile(channel, fileId, interactionId, path.string(), start, end);
198 77 : }
199 : });
200 575 : }
201 :
202 : } // namespace jami
|