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 : #include "gittransport.h"
18 :
19 : #include "manager.h"
20 :
21 : #include <dhtnet/multiplexed_socket.h>
22 : #include <dhtnet/connectionmanager.h>
23 :
24 : using namespace std::string_view_literals;
25 :
26 : // Inactivity timeout for a git fetch. The read no longer runs with
27 : // ConversationRepository::opMtx_ held, so a peer that goes quiet costs this
28 : // fetch and nothing else on the conversation.
29 : constexpr auto P2P_READ_TIMEOUT = std::chrono::days(1);
30 :
31 : // NOTE: THIS MUST BE IN THE ROOT NAMESPACE FOR LIBGIT2
32 :
33 : #ifdef LIBJAMI_TEST
34 : std::atomic<std::shared_ptr<P2PStallHook>> P2P_STALL_HOOK {};
35 : #endif
36 :
37 : /*
38 : * Create a git protocol request.
39 : *
40 : * For example: 0029git-upload-pack conversation\0host=device\0
41 : * @param buf The buffer to fill
42 : * @param cmd The wanted command
43 : * @param url The repository's URL
44 : * @return 0 on success, - 1 on error
45 : */
46 : static int
47 2019 : generateRequest(git_buf* request, const std::string& cmd, std::string_view url)
48 : {
49 2019 : if (cmd.empty()) {
50 0 : giterr_set_str(GITERR_NET, "empty command");
51 0 : return -1;
52 : }
53 : // url format = deviceId/conversationId
54 2019 : auto delim = url.find('/');
55 2019 : if (delim == std::string::npos) {
56 0 : giterr_set_str(GITERR_NET, "malformed URL");
57 0 : return -1;
58 : }
59 :
60 2019 : auto deviceId = url.substr(0, delim);
61 2018 : auto conversationId = url.substr(delim, url.size());
62 :
63 2018 : constexpr auto nullSeparator = "\0"sv;
64 : auto total = 4 /* 4 bytes for the len len */
65 2018 : + cmd.size() /* followed by the command */
66 : + 1 /* space */
67 2018 : + conversationId.size() /* conversation */
68 : + 1 /* \0 */
69 2018 : + HOST_TAG.size() + deviceId.size() /* device */
70 2018 : + nullSeparator.size() /* \0 */;
71 :
72 2018 : std::string str;
73 2019 : str.reserve(total);
74 2018 : str.append("0000"sv);
75 2018 : fmt::format_to_n(str.begin(), 4, "{:04x}", total);
76 2019 : str.append(cmd)
77 2019 : .append(" "sv)
78 2019 : .append(conversationId)
79 2018 : .append(nullSeparator)
80 2018 : .append(HOST_TAG)
81 2018 : .append(deviceId)
82 2019 : .append(nullSeparator);
83 :
84 2019 : return git_buf_set(request, str.data(), str.size());
85 2019 : }
86 :
87 : int
88 2019 : sendCmd(P2PStream* s)
89 : {
90 2019 : git_buf request = {};
91 2019 : if (generateRequest(&request, s->cmd, s->url) < 0) {
92 0 : git_buf_dispose(&request);
93 0 : return -1;
94 : }
95 :
96 2018 : auto sock = s->socket.lock();
97 2018 : if (!sock) {
98 0 : git_buf_dispose(&request);
99 0 : giterr_set_str(GITERR_NET, "unavailable socket");
100 0 : return -1;
101 : }
102 :
103 : // write() reports how many bytes it managed to push, so only ec tells a
104 : // short write apart from a complete one.
105 2019 : std::error_code ec;
106 2019 : sock->write(reinterpret_cast<const unsigned char*>(request.ptr), request.size, ec);
107 2019 : git_buf_dispose(&request);
108 2019 : if (ec) {
109 18 : giterr_set_str(GITERR_NET, ec.message().c_str());
110 18 : return -1;
111 : }
112 :
113 2001 : s->sent_command = 1;
114 2001 : return 0;
115 2019 : }
116 :
117 : int
118 6565 : P2PStreamRead(git_smart_subtransport_stream* stream, char* buffer, size_t buflen, size_t* read)
119 : {
120 6565 : *read = 0;
121 6565 : auto* fs = reinterpret_cast<P2PStream*>(stream);
122 : #ifdef LIBJAMI_TEST
123 : // The strong reference outlives the call, so the hook cannot be destroyed
124 : // under the thread that is stalled inside it.
125 6565 : if (auto hook = P2P_STALL_HOOK.load())
126 6565 : (*hook)(fs->url);
127 : #endif
128 6565 : auto sock = fs->socket.lock();
129 6564 : if (!sock) {
130 0 : giterr_set_str(GITERR_NET, "unavailable socket");
131 0 : return -1;
132 : }
133 :
134 : // If it's the first read, we need to send
135 : // the upload-pack command
136 6563 : if (!fs->sent_command && sendCmd(fs) < 0)
137 17 : return -1;
138 :
139 6545 : std::error_code ec;
140 6546 : auto datalen = sock->waitForData(P2P_READ_TIMEOUT, ec);
141 6548 : if (ec && ec != asio::error::eof) {
142 2 : auto reason = fmt::format("channel closed ({})", ec.message());
143 1 : JAMI_WARNING("[git] {}: {}, ending stream", fs->url, reason);
144 1 : giterr_set_str(GITERR_NET, reason.c_str());
145 1 : return -1;
146 1 : }
147 :
148 6546 : *read = sock->read(reinterpret_cast<unsigned char*>(buffer), std::min<size_t>(datalen, buflen), ec);
149 6547 : if (ec && ec != asio::error::eof) {
150 0 : JAMI_WARNING("[git] read error: {}", ec.message());
151 0 : giterr_set_str(GITERR_NET, ec.message().c_str());
152 0 : return -1;
153 : }
154 :
155 6546 : return 0;
156 6564 : }
157 :
158 : int
159 4182 : P2PStreamWrite(git_smart_subtransport_stream* stream, const char* buffer, size_t len)
160 : {
161 4182 : auto* fs = reinterpret_cast<P2PStream*>(stream);
162 4182 : auto sock = fs->socket.lock();
163 4182 : if (!sock) {
164 0 : giterr_set_str(GITERR_NET, "unavailable socket");
165 0 : return -1;
166 : }
167 4183 : std::error_code ec;
168 4181 : sock->write(reinterpret_cast<const unsigned char*>(buffer), len, ec);
169 4184 : if (ec) {
170 2 : giterr_set_str(GITERR_NET, ec.message().c_str());
171 2 : return -1;
172 : }
173 4181 : return 0;
174 4183 : }
175 :
176 : void
177 2013 : P2PStreamFree(git_smart_subtransport_stream*)
178 2013 : {}
179 :
180 : int
181 6223 : P2PSubTransportAction(git_smart_subtransport_stream** out,
182 : git_smart_subtransport* transport,
183 : const char* url,
184 : git_smart_service_t action)
185 : {
186 6223 : auto* sub = reinterpret_cast<P2PSubTransport*>(transport);
187 6223 : if (!sub || !sub->remote) {
188 0 : JAMI_ERROR("Invalid subtransport");
189 0 : return -1;
190 : }
191 :
192 6223 : auto* repo = git_remote_owner(sub->remote);
193 6223 : if (!repo) {
194 0 : JAMI_ERROR("No repository linked to the transport");
195 0 : return -1;
196 : }
197 :
198 6223 : const auto* workdir = git_repository_workdir(repo);
199 6223 : if (!workdir) {
200 0 : JAMI_ERROR("No working linked to the repository");
201 0 : return -1;
202 : }
203 6223 : std::string_view path = workdir;
204 6219 : auto delimConv = path.rfind("/conversations");
205 6222 : if (delimConv == std::string::npos) {
206 0 : JAMI_ERROR("No conversation id found");
207 0 : return -1;
208 : }
209 6222 : auto delimAccount = path.rfind('/', delimConv - 1);
210 6221 : if (delimAccount == std::string::npos && delimConv - 1 - delimAccount == 16) {
211 0 : JAMI_ERROR("No account id found");
212 0 : return -1;
213 : }
214 6221 : auto accountId = path.substr(delimAccount + 1, delimConv - 1 - delimAccount);
215 6215 : std::string_view gitUrl = url + ("git://"sv).size();
216 6220 : auto delim = gitUrl.find('/');
217 6217 : if (delim == std::string::npos) {
218 0 : JAMI_ERROR("Incorrect url {:s}", gitUrl);
219 0 : return -1;
220 : }
221 6217 : auto deviceId = gitUrl.substr(0, delim);
222 6221 : auto conversationId = gitUrl.substr(delim + 1, gitUrl.size());
223 :
224 6215 : if (action == GIT_SERVICE_UPLOADPACK_LS) {
225 2039 : auto gitSocket = jami::Manager::instance().gitSocket(accountId, deviceId, conversationId);
226 2039 : if (!gitSocket) {
227 26 : JAMI_ERROR("Unable to find related socket for {:s}, {:s}, {:s}", accountId, deviceId, conversationId);
228 26 : return -1;
229 : }
230 2013 : auto stream = std::make_unique<P2PStream>();
231 2013 : stream->socket = gitSocket;
232 2013 : stream->base.read = P2PStreamRead;
233 2013 : stream->base.write = P2PStreamWrite;
234 2013 : stream->base.free = P2PStreamFree;
235 2013 : stream->cmd = UPLOAD_PACK_CMD;
236 2013 : stream->url = gitUrl;
237 2012 : sub->stream = std::move(stream);
238 2013 : *out = &sub->stream->base;
239 2012 : return 0;
240 6214 : } else if (action == GIT_SERVICE_UPLOADPACK) {
241 4181 : if (sub->stream) {
242 4180 : *out = &sub->stream->base;
243 4181 : return 0;
244 : }
245 0 : return -1;
246 : }
247 0 : return 0;
248 : }
249 :
250 : int
251 6073 : P2PSubTransportClose(git_smart_subtransport*)
252 : {
253 6073 : return 0;
254 : }
255 :
256 : void
257 2037 : P2PSubTransportFree(git_smart_subtransport* transport)
258 : {
259 2037 : jami::Manager::instance().eraseGitTransport(transport);
260 2039 : }
261 :
262 : int
263 2039 : P2PSubTransportNew(P2PSubTransport** out, git_transport*, void* payload)
264 : {
265 2039 : auto sub = std::make_unique<P2PSubTransport>();
266 2039 : sub->remote = reinterpret_cast<git_remote*>(payload);
267 2039 : auto* base = &sub->base;
268 2039 : base->action = P2PSubTransportAction;
269 2039 : base->close = P2PSubTransportClose;
270 2039 : base->free = P2PSubTransportFree;
271 2039 : *out = sub.get();
272 2039 : jami::Manager::instance().insertGitTransport(base, std::move(sub));
273 2039 : return 0;
274 2039 : }
275 :
276 : int
277 2039 : p2p_subtransport_cb(git_smart_subtransport** out, git_transport* owner, void* payload)
278 : {
279 : P2PSubTransport* sub;
280 :
281 2039 : if (P2PSubTransportNew(&sub, owner, payload) < 0)
282 0 : return -1;
283 :
284 2039 : *out = &sub->base;
285 2039 : return 0;
286 : }
287 :
288 : int
289 2039 : p2p_transport_cb(git_transport** out, git_remote* owner, void*)
290 : {
291 2039 : git_smart_subtransport_definition def
292 : = {p2p_subtransport_cb,
293 : 0, /* Because we use an already existing channel socket, we use a permanent transport */
294 2039 : reinterpret_cast<void*>(owner)};
295 4078 : return git_transport_smart(out, owner, &def);
296 : }
|