Line data Source code
1 : /*
2 : * Copyright (C) 2004-2025 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 "gitserver.h"
18 :
19 : #include "fileutils.h"
20 : #include "logger.h"
21 : #include "gittransport.h"
22 : #include "manager.h"
23 : #include <opendht/thread_pool.h>
24 : #include <dhtnet/multiplexed_socket.h>
25 : #include <fmt/compile.h>
26 :
27 : #include <charconv>
28 : #include <ctime>
29 : #include <fstream>
30 : #include <git2.h>
31 : #include <iomanip>
32 :
33 : using namespace std::string_view_literals;
34 : constexpr auto FLUSH_PKT = "0000"sv;
35 : constexpr auto NAK_PKT = "0008NAK\n"sv;
36 : constexpr auto DONE_CMD = "done\n"sv;
37 : constexpr auto WANT_CMD = "want"sv;
38 : constexpr auto HAVE_CMD = "have"sv;
39 : constexpr auto SERVER_CAPABILITIES
40 : = " HEAD\0side-band side-band-64k shallow no-progress include-tag"sv;
41 :
42 : namespace jami {
43 :
44 : class GitServer::Impl
45 : {
46 : public:
47 762 : Impl(const std::string& accountId,
48 : const std::string& repositoryId,
49 : const std::string& repository,
50 : const std::shared_ptr<dhtnet::ChannelSocket>& socket)
51 762 : : accountId_(accountId)
52 762 : , repositoryId_(repositoryId)
53 762 : , repository_(repository)
54 762 : , socket_(socket)
55 : {
56 2286 : JAMI_DEBUG("[Account {}] [Conversation {}] [GitServer {}] created",
57 : accountId_,
58 : repositoryId_,
59 : fmt::ptr(this));
60 : // Check at least if repository is correct
61 : git_repository* repo;
62 762 : if (git_repository_open(&repo, repository_.c_str()) != 0) {
63 0 : socket_->shutdown();
64 0 : return;
65 : }
66 762 : git_repository_free(repo);
67 :
68 762 : socket_->setOnRecv([this](const uint8_t* buf, std::size_t len) {
69 5539 : std::lock_guard lk(destroyMtx_);
70 5539 : if (isDestroying_)
71 0 : return len;
72 5539 : if (parseOrder(std::string_view((const char*)buf, len)))
73 15095 : while(parseOrder());
74 5539 : return len;
75 5539 : });
76 0 : }
77 762 : ~Impl() {
78 762 : stop();
79 2286 : JAMI_DEBUG("[Account {}] [Conversation {}] [GitServer {}] destroyed",
80 : accountId_,
81 : repositoryId_,
82 : fmt::ptr(this));
83 762 : }
84 1886 : void stop()
85 : {
86 1886 : std::lock_guard lk(destroyMtx_);
87 1886 : if (isDestroying_.exchange(true)) {
88 1124 : socket_->setOnRecv({});
89 1124 : socket_->shutdown();
90 : }
91 1886 : }
92 : bool parseOrder(std::string_view buf = {});
93 :
94 : void sendReferenceCapabilities(bool sendVersion = false);
95 : bool NAK();
96 : void ACKCommon();
97 : bool ACKFirst();
98 : void sendPackData();
99 : std::map<std::string, std::string> getParameters(std::string_view pkt_line);
100 :
101 : std::string accountId_ {};
102 : std::string repositoryId_ {};
103 : std::string repository_ {};
104 : std::shared_ptr<dhtnet::ChannelSocket> socket_ {};
105 : std::string wantedReference_ {};
106 : std::string common_ {};
107 : std::vector<std::string> haveRefs_ {};
108 : std::string cachedPkt_ {};
109 : std::mutex destroyMtx_ {};
110 : std::atomic_bool isDestroying_ {false};
111 : onFetchedCb onFetchedCb_ {};
112 : };
113 :
114 : bool
115 20634 : GitServer::Impl::parseOrder(std::string_view buf)
116 : {
117 20634 : std::string pkt = std::move(cachedPkt_);
118 20634 : if (!buf.empty())
119 5539 : pkt += buf;
120 :
121 : // Parse pkt len
122 : // Reference: https://github.com/git/git/blob/master/Documentation/technical/protocol-common.txt#L51
123 : // The first four bytes define the length of the packet and 0000 is a FLUSH pkt
124 :
125 20634 : unsigned int pkt_len = 0;
126 20634 : auto [p, ec] = std::from_chars(pkt.data(), pkt.data() + 4, pkt_len, 16);
127 20634 : if (ec != std::errc()) {
128 0 : JAMI_ERROR("[Account {}] [Conversation {}] [GitServer {}] Unable to parse packet size", accountId_, repositoryId_, fmt::ptr(this));
129 : }
130 20634 : if (pkt_len != pkt.size()) {
131 : // Store next packet part
132 17642 : if (pkt_len == 0) {
133 : // FLUSH_PKT
134 3551 : pkt_len = 4;
135 : }
136 17642 : cachedPkt_ = pkt.substr(pkt_len);
137 : }
138 :
139 20634 : auto pack = std::string_view(pkt).substr(4, pkt_len - 4);
140 20634 : if (pack == DONE_CMD) {
141 : // Reference:
142 : // https://github.com/git/git/blob/master/Documentation/technical/pack-protocol.txt#L390 Do
143 : // not do multi-ack, just send ACK + pack file
144 : // In case of no common base, send NAK
145 3012 : JAMI_LOG("[Account {}] [Conversation {}] [GitServer {}] Peer negotiation is done. Answering to want order", accountId_, repositoryId_, fmt::ptr(this));
146 : bool sendData;
147 1004 : if (common_.empty())
148 144 : sendData = NAK();
149 : else
150 860 : sendData = ACKFirst();
151 1004 : if (sendData)
152 1004 : sendPackData();
153 1004 : return !cachedPkt_.empty();
154 19630 : } else if (pack.empty()) {
155 3551 : if (!haveRefs_.empty()) {
156 : // Reference:
157 : // https://github.com/git/git/blob/master/Documentation/technical/pack-protocol.txt#L390
158 : // Do not do multi-ack, just send ACK + pack file In case of no common base ACK
159 560 : ACKCommon();
160 560 : NAK();
161 : }
162 3551 : return !cachedPkt_.empty();
163 : }
164 :
165 16079 : auto lim = pack.find(' ');
166 16079 : auto cmd = pack.substr(0, lim);
167 16079 : auto dat = (lim < pack.size()) ? pack.substr(lim+1) : std::string_view{};
168 16079 : if (cmd == UPLOAD_PACK_CMD) {
169 : // Cf: https://github.com/git/git/blob/master/Documentation/technical/pack-protocol.txt#L166
170 : // References discovery
171 5964 : JAMI_LOG("[Account {}] [Conversation {}] [GitServer {}] Upload pack command detected.", accountId_, repositoryId_, fmt::ptr(this));
172 1988 : auto version = 1;
173 1988 : auto parameters = getParameters(dat);
174 1988 : auto versionIt = parameters.find("version");
175 1988 : bool sendVersion = false;
176 1988 : if (versionIt != parameters.end()) {
177 0 : auto [p, ec] = std::from_chars(versionIt->second.data(),
178 0 : versionIt->second.data() + versionIt->second.size(),
179 0 : version);
180 0 : if (ec == std::errc()) {
181 0 : sendVersion = true;
182 : } else {
183 0 : JAMI_WARNING("[Account {}] [Conversation {}] [GitServer {}] Invalid version detected: {}", accountId_, repositoryId_, fmt::ptr(this), versionIt->second);
184 : }
185 : }
186 1988 : if (version == 1) {
187 1988 : sendReferenceCapabilities(sendVersion);
188 : } else {
189 0 : JAMI_ERROR("[Account {}] [Conversation {}] [GitServer {}] That protocol version is not yet supported (version: {:d})", accountId_, repositoryId_, fmt::ptr(this), version);
190 : }
191 16079 : } else if (cmd == WANT_CMD) {
192 : // Reference:
193 : // https://github.com/git/git/blob/master/Documentation/technical/pack-protocol.txt#L229
194 : // TODO can have more want
195 1004 : wantedReference_ = dat.substr(0, 40);
196 3012 : JAMI_LOG("[Account {}] [Conversation {}] [GitServer {}] Peer want ref: {}", accountId_, repositoryId_, fmt::ptr(this), wantedReference_);
197 13087 : } else if (cmd == HAVE_CMD) {
198 13087 : const auto& commit = haveRefs_.emplace_back(dat.substr(0, 40));
199 13087 : if (common_.empty()) {
200 : // Detect first common commit
201 : // Reference:
202 : // https://github.com/git/git/blob/master/Documentation/technical/pack-protocol.txt#L390
203 : // TODO do not open repository every time
204 : git_repository* repo;
205 860 : if (git_repository_open(&repo, repository_.c_str()) != 0) {
206 0 : JAMI_WARNING("[Account {}] [Conversation {}] [GitServer {}] Unable to open {}", accountId_, repositoryId_, fmt::ptr(this), repository_);
207 0 : return !cachedPkt_.empty();
208 : }
209 860 : GitRepository rep {repo, git_repository_free};
210 : git_oid commit_id;
211 860 : if (git_oid_fromstr(&commit_id, commit.c_str()) == 0) {
212 : // Reference found
213 860 : common_ = commit;
214 : }
215 860 : }
216 : } else {
217 0 : JAMI_WARNING("[Account {}] [Conversation {}] [GitServer {}] Unwanted packet received: {}", accountId_, repositoryId_, fmt::ptr(this), pkt);
218 : }
219 16079 : return !cachedPkt_.empty();
220 20634 : }
221 :
222 : std::string
223 28224 : toGitHex(size_t value) {
224 84672 : return fmt::format(FMT_COMPILE("{:04x}"), value & 0x0FFFF);
225 : }
226 :
227 : void
228 1988 : GitServer::Impl::sendReferenceCapabilities(bool sendVersion)
229 : {
230 : // Get references
231 : // First, get the HEAD reference
232 : // https://github.com/git/git/blob/master/Documentation/technical/pack-protocol.txt#L166
233 : git_repository* repo;
234 1988 : if (git_repository_open(&repo, repository_.c_str()) != 0) {
235 0 : JAMI_WARNING("[Account {}] [Conversation {}] [GitServer {}] Unable to open {}", accountId_, repositoryId_, fmt::ptr(this), repository_);
236 0 : socket_->shutdown();
237 0 : return;
238 : }
239 1988 : GitRepository rep {repo, git_repository_free};
240 :
241 : // Answer with the version number
242 : // **** When the client initially connects the server will immediately respond
243 : // **** with a version number (if "version=1" is sent as an Extra Parameter),
244 1988 : std::error_code ec;
245 1988 : if (sendVersion) {
246 0 : constexpr auto toSend = "000eversion 1\0"sv;
247 0 : socket_->write(reinterpret_cast<const unsigned char*>(toSend.data()),
248 : toSend.size(),
249 : ec);
250 0 : if (ec) {
251 0 : JAMI_WARNING("[Account {}] [Conversation {}] [GitServer {}] Unable to send data for {}: {}", accountId_, repositoryId_, fmt::ptr(this), repository_, ec.message());
252 0 : socket_->shutdown();
253 0 : return;
254 : }
255 : }
256 :
257 : git_oid commit_id;
258 1988 : if (git_reference_name_to_id(&commit_id, rep.get(), "HEAD") < 0) {
259 0 : JAMI_ERROR("[Account {}] [Conversation {}] [GitServer {}] Unable to get reference for HEAD", accountId_, repositoryId_, fmt::ptr(this));
260 0 : socket_->shutdown();
261 0 : return;
262 : }
263 1988 : std::string_view currentHead = git_oid_tostr_s(&commit_id);
264 :
265 : // Send references
266 1988 : std::ostringstream packet;
267 1988 : packet << toGitHex(5 + currentHead.size() + SERVER_CAPABILITIES.size());
268 1988 : packet << currentHead << SERVER_CAPABILITIES << "\n";
269 :
270 : // Now, add other references
271 : git_strarray refs;
272 1988 : if (git_reference_list(&refs, rep.get()) == 0) {
273 26761 : for (std::size_t i = 0; i < refs.count; ++i) {
274 24773 : std::string_view ref = refs.strings[i];
275 24773 : if (git_reference_name_to_id(&commit_id, rep.get(), ref.data()) < 0) {
276 0 : JAMI_WARNING("[Account {}] [Conversation {}] [GitServer {}] Unable to get reference for {}", accountId_, repositoryId_, fmt::ptr(this), ref);
277 0 : continue;
278 0 : }
279 24773 : currentHead = git_oid_tostr_s(&commit_id);
280 :
281 24773 : packet << toGitHex(6 /* size + space + \n */ + currentHead.size() + ref.size());
282 24773 : packet << currentHead << " " << ref << "\n";
283 : }
284 : }
285 1988 : git_strarray_dispose(&refs);
286 :
287 : // And add FLUSH
288 1988 : packet << FLUSH_PKT;
289 1988 : auto toSend = packet.str();
290 1988 : socket_->write(reinterpret_cast<const unsigned char*>(toSend.data()), toSend.size(), ec);
291 1988 : if (ec) {
292 0 : JAMI_WARNING("[Account {}] [Conversation {}] [GitServer {}] Unable to send data for {}: {}", accountId_, repositoryId_, fmt::ptr(this), repository_, ec.message());
293 0 : socket_->shutdown();
294 : }
295 1988 : }
296 :
297 : void
298 560 : GitServer::Impl::ACKCommon()
299 : {
300 560 : std::error_code ec;
301 : // Ack common base
302 560 : if (!common_.empty()) {
303 560 : auto toSend = fmt::format(FMT_COMPILE("{:04x}ACK {} continue\n"),
304 560 : 18 + common_.size() /* size + ACK + space * 2 + continue + \n */, common_);
305 560 : socket_->write(reinterpret_cast<const unsigned char*>(toSend.c_str()), toSend.size(), ec);
306 560 : if (ec) {
307 0 : JAMI_WARNING("[Account {}] [Conversation {}] [GitServer {}] Unable to send data for {}: {}", accountId_, repositoryId_, fmt::ptr(this), repository_, ec.message());
308 0 : socket_->shutdown();
309 : }
310 560 : }
311 560 : }
312 :
313 : bool
314 860 : GitServer::Impl::ACKFirst()
315 : {
316 860 : std::error_code ec;
317 : // Ack common base
318 860 : if (!common_.empty()) {
319 860 : auto toSend = fmt::format(FMT_COMPILE("{:04x}ACK {}\n"),
320 860 : 9 + common_.size() /* size + ACK + space + \n */, common_);
321 860 : socket_->write(reinterpret_cast<const unsigned char*>(toSend.c_str()), toSend.size(), ec);
322 860 : if (ec) {
323 0 : JAMI_WARNING("[Account {}] [Conversation {}] [GitServer {}] Unable to send data for {}: {}", accountId_, repositoryId_, fmt::ptr(this), repository_, ec.message());
324 0 : socket_->shutdown();
325 0 : return false;
326 : }
327 860 : }
328 860 : return true;
329 : }
330 :
331 : bool
332 704 : GitServer::Impl::NAK()
333 : {
334 704 : std::error_code ec;
335 : // NAK
336 704 : socket_->write(reinterpret_cast<const unsigned char*>(NAK_PKT.data()), NAK_PKT.size(), ec);
337 704 : if (ec) {
338 0 : JAMI_WARNING("[Account {}] [Conversation {}] [GitServer {}] Unable to send data for {}: {}", accountId_, repositoryId_, fmt::ptr(this), repository_, ec.message());
339 0 : socket_->shutdown();
340 0 : return false;
341 : }
342 704 : return true;
343 : }
344 :
345 : void
346 1004 : GitServer::Impl::sendPackData()
347 : {
348 : git_repository* repo_ptr;
349 1004 : if (git_repository_open(&repo_ptr, repository_.c_str()) != 0) {
350 0 : JAMI_WARNING("[Account {}] [Conversation {}] [GitServer {}] Unable to open {}", accountId_, repositoryId_, fmt::ptr(this), repository_);
351 0 : return;
352 : }
353 1004 : GitRepository repo {repo_ptr, git_repository_free};
354 :
355 : git_packbuilder* pb_ptr;
356 1004 : if (git_packbuilder_new(&pb_ptr, repo.get()) != 0) {
357 0 : JAMI_WARNING("[Account {}] [Conversation {}] [GitServer {}] Unable to open packbuilder for {}", accountId_, repositoryId_, fmt::ptr(this), repository_);
358 0 : return;
359 : }
360 1004 : GitPackBuilder pb {pb_ptr, git_packbuilder_free};
361 :
362 1004 : std::string fetched = wantedReference_;
363 : git_oid oid;
364 1004 : if (git_oid_fromstr(&oid, fetched.c_str()) < 0) {
365 0 : JAMI_ERROR("[Account {}] [Conversation {}] [GitServer {}] Unable to get reference for commit {}", accountId_, repositoryId_, fmt::ptr(this), fetched);
366 0 : return;
367 : }
368 :
369 1004 : git_revwalk* walker_ptr = nullptr;
370 1004 : if (git_revwalk_new(&walker_ptr, repo.get()) < 0 || git_revwalk_push(walker_ptr, &oid) < 0) {
371 0 : if (walker_ptr)
372 0 : git_revwalk_free(walker_ptr);
373 0 : return;
374 : }
375 1004 : GitRevWalker walker {walker_ptr, git_revwalk_free};
376 1004 : git_revwalk_sorting(walker.get(), GIT_SORT_TOPOLOGICAL);
377 : // Add first commit
378 1004 : std::set<std::string> parents;
379 1004 : auto haveCommit = false;
380 :
381 2784 : while (!git_revwalk_next(&oid, walker.get())) {
382 : // log until have refs
383 2640 : std::string id = git_oid_tostr_s(&oid);
384 2640 : haveCommit |= std::find(haveRefs_.begin(), haveRefs_.end(), id) != haveRefs_.end();
385 2640 : auto itParents = std::find(parents.begin(), parents.end(), id);
386 2640 : if (itParents != parents.end())
387 1636 : parents.erase(itParents);
388 2640 : if (haveCommit && parents.size() == 0 /* We are sure that all commits are there */)
389 860 : break;
390 1780 : if (git_packbuilder_insert_commit(pb.get(), &oid) != 0) {
391 0 : JAMI_WARNING("[Account {}] [Conversation {}] [GitServer {}] Unable to open insert commit {} for {}", accountId_, repositoryId_, fmt::ptr(this), git_oid_tostr_s(&oid), repository_);
392 0 : return;
393 : }
394 :
395 : // Get next commit to pack
396 : git_commit* commit_ptr;
397 1780 : if (git_commit_lookup(&commit_ptr, repo.get(), &oid) < 0) {
398 0 : JAMI_ERROR("[Account {}] [Conversation {}] [GitServer {}] Unable to look up current commit", accountId_, repositoryId_, fmt::ptr(this));
399 0 : return;
400 : }
401 1780 : GitCommit commit {commit_ptr, git_commit_free};
402 1780 : auto parentsCount = git_commit_parentcount(commit.get());
403 3419 : for (unsigned int p = 0; p < parentsCount; ++p) {
404 : // make sure to explore all branches
405 1639 : const git_oid* pid = git_commit_parent_id(commit.get(), p);
406 1639 : if (pid)
407 1639 : parents.emplace(git_oid_tostr_s(pid));
408 : }
409 2640 : }
410 :
411 1004 : git_buf data = {};
412 1004 : if (git_packbuilder_write_buf(&data, pb.get()) != 0) {
413 0 : JAMI_WARNING("[Account {}] [Conversation {}] [GitServer {}] Unable to write pack data for {}", accountId_, repositoryId_, fmt::ptr(this), repository_);
414 0 : return;
415 : }
416 :
417 1004 : std::size_t sent = 0;
418 1004 : std::size_t len = data.size;
419 1004 : std::error_code ec;
420 1004 : std::vector<uint8_t> toSendData;
421 : do {
422 : // cf https://github.com/git/git/blob/master/Documentation/technical/pack-protocol.txt#L166
423 : // In 'side-band-64k' mode it will send up to 65519 data bytes plus 1 control code, for a
424 : // total of up to 65520 bytes in a pkt-line.
425 1463 : std::size_t pkt_size = std::min(static_cast<std::size_t>(65515), len - sent);
426 1463 : std::string toSendHeader = toGitHex(pkt_size + 5);
427 1463 : toSendData.clear();
428 1463 : toSendData.reserve(pkt_size + 5);
429 1463 : toSendData.insert(toSendData.end(), toSendHeader.begin(), toSendHeader.end());
430 1463 : toSendData.push_back(0x1);
431 1463 : toSendData.insert(toSendData.end(), data.ptr + sent, data.ptr + sent + pkt_size);
432 :
433 1463 : socket_->write(reinterpret_cast<const unsigned char*>(toSendData.data()),
434 : toSendData.size(),
435 : ec);
436 1463 : if (ec) {
437 0 : JAMI_WARNING("[Account {}] [Conversation {}] [GitServer {}] Unable to send data for {}: {}", accountId_, repositoryId_, fmt::ptr(this), repository_, ec.message());
438 0 : git_buf_dispose(&data);
439 0 : return;
440 : }
441 1463 : sent += pkt_size;
442 2926 : } while (sent < len);
443 1004 : git_buf_dispose(&data);
444 1004 : toSendData = {};
445 :
446 : // And finish by a little FLUSH
447 1004 : socket_->write(reinterpret_cast<const uint8_t*>(FLUSH_PKT.data()), FLUSH_PKT.size(), ec);
448 1004 : if (ec) {
449 0 : JAMI_WARNING("[Account {}] [Conversation {}] [GitServer {}] Unable to send data for {}: {}", accountId_, repositoryId_, fmt::ptr(this), repository_, ec.message());
450 : }
451 :
452 : // Clear sent data
453 1004 : haveRefs_.clear();
454 1004 : wantedReference_.clear();
455 1004 : common_.clear();
456 1004 : if (onFetchedCb_)
457 1004 : onFetchedCb_(fetched);
458 1004 : }
459 :
460 : std::map<std::string, std::string>
461 1988 : GitServer::Impl::getParameters(std::string_view pkt_line)
462 : {
463 1988 : std::map<std::string, std::string> parameters;
464 1988 : std::string key, value;
465 1988 : auto isKey = true;
466 1988 : auto nullChar = 0;
467 224644 : for (auto letter: pkt_line) {
468 222656 : if (letter == '\0') {
469 : // parameters such as host or version are after the first \0
470 3976 : if (nullChar != 0 && !key.empty()) {
471 1988 : parameters.try_emplace(std::move(key), std::move(value));
472 : }
473 3976 : nullChar += 1;
474 3976 : isKey = true;
475 3976 : key.clear();
476 3976 : value.clear();
477 218680 : } else if (letter == '=') {
478 1988 : isKey = false;
479 216692 : } else if (nullChar != 0) {
480 135184 : if (isKey) {
481 7952 : key += letter;
482 : } else {
483 127232 : value += letter;
484 : }
485 : }
486 : }
487 3976 : return parameters;
488 1988 : }
489 :
490 762 : GitServer::GitServer(const std::string& accountId,
491 : const std::string& conversationId,
492 762 : const std::shared_ptr<dhtnet::ChannelSocket>& client)
493 : {
494 1524 : auto path = (fileutils::get_data_dir() / accountId / "conversations" / conversationId).string();
495 762 : pimpl_ = std::make_unique<GitServer::Impl>(accountId, conversationId, path, client);
496 762 : }
497 :
498 762 : GitServer::~GitServer()
499 : {
500 762 : stop();
501 762 : pimpl_.reset();
502 762 : }
503 :
504 : void
505 762 : GitServer::setOnFetched(const onFetchedCb& cb)
506 : {
507 762 : if (!pimpl_)
508 0 : return;
509 762 : pimpl_->onFetchedCb_ = cb;
510 : }
511 :
512 : void
513 1124 : GitServer::stop()
514 : {
515 1124 : pimpl_->stop();
516 1124 : }
517 :
518 : } // namespace jami
|