Ring Daemon
Loading...
Searching...
No Matches
gitserver.cpp
Go to the documentation of this file.
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 "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
33using namespace std::string_view_literals;
34constexpr auto FLUSH_PKT = "0000"sv;
35constexpr auto NAK_PKT = "0008NAK\n"sv;
36constexpr auto DONE_CMD = "done\n"sv;
37constexpr auto WANT_CMD = "want"sv;
38constexpr auto HAVE_CMD = "have"sv;
39constexpr auto SERVER_CAPABILITIES = " HEAD\0side-band side-band-64k shallow no-progress include-tag"sv;
40
41namespace jami {
42
44{
45public:
46 Impl(const std::string& accountId,
47 const std::string& repositoryId,
48 const std::string& repository,
49 const std::shared_ptr<dhtnet::ChannelSocket>& socket)
50 : accountId_(accountId)
52 , repository_(repository)
53 , socket_(socket)
54 {
55 JAMI_DEBUG("[Account {}] [Conversation {}] [GitServer {}] created", accountId_, repositoryId_, fmt::ptr(this));
56 // Check at least if repository is correct
58 if (git_repository_open(&repo, repository_.c_str()) != 0) {
59 dht::ThreadPool::io().run([socket = socket_] { socket->shutdown(); });
60 return;
61 }
63
64 socket_->setOnRecv([this](const uint8_t* buf, std::size_t len) {
65 std::lock_guard lk(destroyMtx_);
66 if (isDestroying_)
67 return len;
68 if (parseOrder(std::string_view((const char*) buf, len)))
69 while (parseOrder())
70 ;
71 return len;
72 });
73 }
75 {
76 stop();
77 JAMI_DEBUG("[Account {}] [Conversation {}] [GitServer {}] destroyed", accountId_, repositoryId_, fmt::ptr(this));
78 }
79 void stop()
80 {
81 std::lock_guard lk(destroyMtx_);
82 if (isDestroying_.exchange(true)) {
83 socket_->setOnRecv({});
84 dht::ThreadPool::io().run([socket = socket_] { socket->shutdown(); });
85 }
86 }
87 bool parseOrder(std::string_view buf = {});
88
89 void sendReferenceCapabilities(bool sendVersion = false);
90 bool NAK();
91 void ACKCommon();
92 bool ACKFirst();
93 void sendPackData();
94 std::map<std::string, std::string> getParameters(std::string_view pkt_line);
95
96 std::string accountId_ {};
97 std::string repositoryId_ {};
98 std::string repository_ {};
99 std::shared_ptr<dhtnet::ChannelSocket> socket_ {};
100 std::string wantedReference_ {};
101 std::string common_ {};
102 std::vector<std::string> haveRefs_ {};
103 std::string cachedPkt_ {};
104 std::mutex destroyMtx_ {};
105 std::atomic_bool isDestroying_ {false};
107};
108
109bool
111{
112 std::string pkt = std::move(cachedPkt_);
113 if (!buf.empty())
114 pkt += buf;
115
116 // Parse pkt len
117 // Reference: https://github.com/git/git/blob/master/Documentation/technical/protocol-common.txt#L51
118 // The first four bytes define the length of the packet and 0000 is a FLUSH pkt
119
120 unsigned int pkt_len = 0;
121 auto [p, ec] = std::from_chars(pkt.data(), pkt.data() + 4, pkt_len, 16);
122 if (ec != std::errc()) {
123 JAMI_ERROR("[Account {}] [Conversation {}] [GitServer {}] Unable to parse packet size",
126 fmt::ptr(this));
127 }
128 if (pkt_len != pkt.size()) {
129 // Store next packet part
130 if (pkt_len == 0) {
131 // FLUSH_PKT
132 pkt_len = 4;
133 }
134 cachedPkt_ = pkt.substr(pkt_len);
135 }
136
137 auto pack = std::string_view(pkt).substr(4, pkt_len - 4);
138 if (pack == DONE_CMD) {
139 // Reference:
140 // https://github.com/git/git/blob/master/Documentation/technical/pack-protocol.txt#L390 Do
141 // not do multi-ack, just send ACK + pack file
142 // In case of no common base, send NAK
143 JAMI_LOG("[Account {}] [Conversation {}] [GitServer {}] Peer negotiation is done. Answering to want order",
146 fmt::ptr(this));
147 bool sendData;
148 if (common_.empty())
149 sendData = NAK();
150 else
151 sendData = ACKFirst();
152 if (sendData)
153 sendPackData();
154 return !cachedPkt_.empty();
155 } else if (pack.empty()) {
156 if (!haveRefs_.empty()) {
157 // Reference:
158 // https://github.com/git/git/blob/master/Documentation/technical/pack-protocol.txt#L390
159 // Do not do multi-ack, just send ACK + pack file In case of no common base ACK
160 ACKCommon();
161 NAK();
162 }
163 return !cachedPkt_.empty();
164 }
165
166 auto lim = pack.find(' ');
167 auto cmd = pack.substr(0, lim);
168 auto dat = (lim < pack.size()) ? pack.substr(lim + 1) : std::string_view {};
169 if (cmd == UPLOAD_PACK_CMD) {
170 // Cf: https://github.com/git/git/blob/master/Documentation/technical/pack-protocol.txt#L166
171 // References discovery
172 JAMI_LOG("[Account {}] [Conversation {}] [GitServer {}] Upload pack command detected.",
175 fmt::ptr(this));
176 auto version = 1;
177 auto parameters = getParameters(dat);
178 auto versionIt = parameters.find("version");
179 bool sendVersion = false;
180 if (versionIt != parameters.end()) {
181 auto [p, ec] = std::from_chars(versionIt->second.data(),
182 versionIt->second.data() + versionIt->second.size(),
183 version);
184 if (ec == std::errc()) {
185 sendVersion = true;
186 } else {
187 JAMI_WARNING("[Account {}] [Conversation {}] [GitServer {}] Invalid version detected: {}",
190 fmt::ptr(this),
191 versionIt->second);
192 }
193 }
194 if (version == 1) {
196 } else {
197 JAMI_ERROR("[Account {}] [Conversation {}] [GitServer {}] That protocol version is not yet supported "
198 "(version: {:d})",
201 fmt::ptr(this),
202 version);
203 }
204 } else if (cmd == WANT_CMD) {
205 // Reference:
206 // https://github.com/git/git/blob/master/Documentation/technical/pack-protocol.txt#L229
207 // TODO can have more want
208 wantedReference_ = dat.substr(0, 40);
209 JAMI_LOG("[Account {}] [Conversation {}] [GitServer {}] Peer want ref: {}",
212 fmt::ptr(this),
214 } else if (cmd == HAVE_CMD) {
215 const auto& commit = haveRefs_.emplace_back(dat.substr(0, 40));
216 if (common_.empty()) {
217 // Detect first common commit
218 // Reference:
219 // https://github.com/git/git/blob/master/Documentation/technical/pack-protocol.txt#L390
220 // TODO do not open repository every time
222 if (git_repository_open(&repo, repository_.c_str()) != 0) {
223 JAMI_WARNING("[Account {}] [Conversation {}] [GitServer {}] Unable to open {}",
226 fmt::ptr(this),
228 return !cachedPkt_.empty();
229 }
232 if (git_oid_fromstr(&commit_id, commit.c_str()) == 0) {
233 // Reference found
234 common_ = commit;
235 }
236 }
237 } else {
238 JAMI_WARNING("[Account {}] [Conversation {}] [GitServer {}] Unwanted packet received: {}",
241 fmt::ptr(this),
242 pkt);
243 }
244 return !cachedPkt_.empty();
245}
246
247std::string
248toGitHex(size_t value)
249{
250 return fmt::format(FMT_COMPILE("{:04x}"), value & 0x0FFFF);
251}
252
253void
255{
256 // Get references
257 // First, get the HEAD reference
258 // https://github.com/git/git/blob/master/Documentation/technical/pack-protocol.txt#L166
260 if (git_repository_open(&repo, repository_.c_str()) != 0) {
261 JAMI_WARNING("[Account {}] [Conversation {}] [GitServer {}] Unable to open {}",
262 accountId_,
263 repositoryId_,
264 fmt::ptr(this),
265 repository_);
266 dht::ThreadPool::io().run([socket = socket_] { socket->shutdown(); });
267 return;
268 }
270
271 // Answer with the version number
272 // **** When the client initially connects the server will immediately respond
273 // **** with a version number (if "version=1" is sent as an Extra Parameter),
274 std::error_code ec;
275 if (sendVersion) {
276 constexpr auto toSend = "000eversion 1\0"sv;
277 socket_->write(reinterpret_cast<const unsigned char*>(toSend.data()), toSend.size(), ec);
278 if (ec) {
279 JAMI_WARNING("[Account {}] [Conversation {}] [GitServer {}] Unable to send data for {}: {}",
280 accountId_,
281 repositoryId_,
282 fmt::ptr(this),
283 repository_,
284 ec.message());
285 dht::ThreadPool::io().run([socket = socket_] { socket->shutdown(); });
286 return;
287 }
288 }
289
291 if (git_reference_name_to_id(&commit_id, rep.get(), "HEAD") < 0) {
292 JAMI_ERROR("[Account {}] [Conversation {}] [GitServer {}] Unable to get reference for HEAD",
293 accountId_,
294 repositoryId_,
295 fmt::ptr(this));
296 dht::ThreadPool::io().run([socket = socket_] { socket->shutdown(); });
297 return;
298 }
299 std::string_view currentHead = git_oid_tostr_s(&commit_id);
300
301 // Send references
302 std::ostringstream packet;
303 packet << toGitHex(5 + currentHead.size() + SERVER_CAPABILITIES.size());
304 packet << currentHead << SERVER_CAPABILITIES << "\n";
305
306 // Now, add other references
308 if (git_reference_list(&refs, rep.get()) == 0) {
309 for (std::size_t i = 0; i < refs.count; ++i) {
310 std::string_view ref = refs.strings[i];
311 if (git_reference_name_to_id(&commit_id, rep.get(), ref.data()) < 0) {
312 JAMI_WARNING("[Account {}] [Conversation {}] [GitServer {}] Unable to get reference for {}",
313 accountId_,
314 repositoryId_,
315 fmt::ptr(this),
316 ref);
317 continue;
318 }
320
321 packet << toGitHex(6 /* size + space + \n */ + currentHead.size() + ref.size());
322 packet << currentHead << " " << ref << "\n";
323 }
324 }
326
327 // And add FLUSH
328 packet << FLUSH_PKT;
329 auto toSend = packet.str();
330 socket_->write(reinterpret_cast<const unsigned char*>(toSend.data()), toSend.size(), ec);
331 if (ec) {
332 JAMI_WARNING("[Account {}] [Conversation {}] [GitServer {}] Unable to send data for {}: {}",
333 accountId_,
334 repositoryId_,
335 fmt::ptr(this),
336 repository_,
337 ec.message());
338 dht::ThreadPool::io().run([socket = socket_] { socket->shutdown(); });
339 }
340}
341
342void
344{
345 std::error_code ec;
346 // Ack common base
347 if (!common_.empty()) {
348 auto toSend = fmt::format(FMT_COMPILE("{:04x}ACK {} continue\n"),
349 18 + common_.size() /* size + ACK + space * 2 + continue + \n */,
350 common_);
351 socket_->write(reinterpret_cast<const unsigned char*>(toSend.c_str()), toSend.size(), ec);
352 if (ec) {
353 JAMI_WARNING("[Account {}] [Conversation {}] [GitServer {}] Unable to send data for {}: {}",
354 accountId_,
355 repositoryId_,
356 fmt::ptr(this),
357 repository_,
358 ec.message());
359 dht::ThreadPool::io().run([socket = socket_] { socket->shutdown(); });
360 }
361 }
362}
363
364bool
366{
367 std::error_code ec;
368 // Ack common base
369 if (!common_.empty()) {
370 auto toSend = fmt::format(FMT_COMPILE("{:04x}ACK {}\n"),
371 9 + common_.size() /* size + ACK + space + \n */,
372 common_);
373 socket_->write(reinterpret_cast<const unsigned char*>(toSend.c_str()), toSend.size(), ec);
374 if (ec) {
375 JAMI_WARNING("[Account {}] [Conversation {}] [GitServer {}] Unable to send data for {}: {}",
376 accountId_,
377 repositoryId_,
378 fmt::ptr(this),
379 repository_,
380 ec.message());
381 dht::ThreadPool::io().run([socket = socket_] { socket->shutdown(); });
382 return false;
383 }
384 }
385 return true;
386}
387
388bool
390{
391 std::error_code ec;
392 // NAK
393 socket_->write(reinterpret_cast<const unsigned char*>(NAK_PKT.data()), NAK_PKT.size(), ec);
394 if (ec) {
395 JAMI_WARNING("[Account {}] [Conversation {}] [GitServer {}] Unable to send data for {}: {}",
396 accountId_,
397 repositoryId_,
398 fmt::ptr(this),
399 repository_,
400 ec.message());
401 dht::ThreadPool::io().run([socket = socket_] { socket->shutdown(); });
402 return false;
403 }
404 return true;
405}
406
407void
409{
411 if (git_repository_open(&repo_ptr, repository_.c_str()) != 0) {
412 JAMI_WARNING("[Account {}] [Conversation {}] [GitServer {}] Unable to open {}",
413 accountId_,
414 repositoryId_,
415 fmt::ptr(this),
416 repository_);
417 return;
418 }
420
422 if (git_packbuilder_new(&pb_ptr, repo.get()) != 0) {
423 JAMI_WARNING("[Account {}] [Conversation {}] [GitServer {}] Unable to open packbuilder for {}",
424 accountId_,
425 repositoryId_,
426 fmt::ptr(this),
427 repository_);
428 return;
429 }
431
432 std::string fetched = wantedReference_;
433 git_oid oid;
434 if (git_oid_fromstr(&oid, fetched.c_str()) < 0) {
435 JAMI_ERROR("[Account {}] [Conversation {}] [GitServer {}] Unable to get reference for commit {}",
436 accountId_,
437 repositoryId_,
438 fmt::ptr(this),
439 fetched);
440 return;
441 }
442
443 git_revwalk* walker_ptr = nullptr;
444 if (git_revwalk_new(&walker_ptr, repo.get()) < 0 || git_revwalk_push(walker_ptr, &oid) < 0) {
445 if (walker_ptr)
447 return;
448 }
451 // Add first commit
452 std::set<std::string> parents;
453 auto haveCommit = false;
454
455 while (!git_revwalk_next(&oid, walker.get())) {
456 // log until have refs
457 std::string id = git_oid_tostr_s(&oid);
458 haveCommit |= std::find(haveRefs_.begin(), haveRefs_.end(), id) != haveRefs_.end();
459 auto itParents = std::find(parents.begin(), parents.end(), id);
460 if (itParents != parents.end())
461 parents.erase(itParents);
462 if (haveCommit && parents.size() == 0 /* We are sure that all commits are there */)
463 break;
464 if (git_packbuilder_insert_commit(pb.get(), &oid) != 0) {
465 JAMI_WARNING("[Account {}] [Conversation {}] [GitServer {}] Unable to open insert commit {} for {}",
466 accountId_,
467 repositoryId_,
468 fmt::ptr(this),
470 repository_);
471 return;
472 }
473
474 // Get next commit to pack
476 if (git_commit_lookup(&commit_ptr, repo.get(), &oid) < 0) {
477 JAMI_ERROR("[Account {}] [Conversation {}] [GitServer {}] Unable to look up current commit",
478 accountId_,
479 repositoryId_,
480 fmt::ptr(this));
481 return;
482 }
483 GitCommit commit {commit_ptr};
484 auto parentsCount = git_commit_parentcount(commit.get());
485 for (unsigned int p = 0; p < parentsCount; ++p) {
486 // make sure to explore all branches
487 const git_oid* pid = git_commit_parent_id(commit.get(), p);
488 if (pid)
489 parents.emplace(git_oid_tostr_s(pid));
490 }
491 }
492
493 git_buf data = {};
494 if (git_packbuilder_write_buf(&data, pb.get()) != 0) {
495 JAMI_WARNING("[Account {}] [Conversation {}] [GitServer {}] Unable to write pack data for {}",
496 accountId_,
497 repositoryId_,
498 fmt::ptr(this),
499 repository_);
500 return;
501 }
502
503 std::size_t sent = 0;
504 std::size_t len = data.size;
505 std::error_code ec;
506 std::vector<uint8_t> toSendData;
507 do {
508 // cf https://github.com/git/git/blob/master/Documentation/technical/pack-protocol.txt#L166
509 // In 'side-band-64k' mode it will send up to 65519 data bytes plus 1 control code, for a
510 // total of up to 65520 bytes in a pkt-line.
511 std::size_t pkt_size = std::min(static_cast<std::size_t>(65515), len - sent);
512 std::string toSendHeader = toGitHex(pkt_size + 5);
513 toSendData.clear();
514 toSendData.reserve(pkt_size + 5);
515 toSendData.insert(toSendData.end(), toSendHeader.begin(), toSendHeader.end());
516 toSendData.push_back(0x1);
517 toSendData.insert(toSendData.end(), data.ptr + sent, data.ptr + sent + pkt_size);
518
519 socket_->write(reinterpret_cast<const unsigned char*>(toSendData.data()), toSendData.size(), ec);
520 if (ec) {
521 JAMI_WARNING("[Account {}] [Conversation {}] [GitServer {}] Unable to send data for {}: {}",
522 accountId_,
523 repositoryId_,
524 fmt::ptr(this),
525 repository_,
526 ec.message());
527 git_buf_dispose(&data);
528 return;
529 }
530 sent += pkt_size;
531 } while (sent < len);
532 git_buf_dispose(&data);
533 toSendData = {};
534
535 // And finish by a little FLUSH
536 socket_->write(reinterpret_cast<const uint8_t*>(FLUSH_PKT.data()), FLUSH_PKT.size(), ec);
537 if (ec) {
538 JAMI_WARNING("[Account {}] [Conversation {}] [GitServer {}] Unable to send data for {}: {}",
539 accountId_,
540 repositoryId_,
541 fmt::ptr(this),
542 repository_,
543 ec.message());
544 }
545
546 // Clear sent data
547 haveRefs_.clear();
548 wantedReference_.clear();
549 common_.clear();
550 if (onFetchedCb_)
551 onFetchedCb_(fetched);
552}
553
554std::map<std::string, std::string>
556{
557 std::map<std::string, std::string> parameters;
558 std::string key, value;
559 auto isKey = true;
560 auto nullChar = 0;
561 for (auto letter : pkt_line) {
562 if (letter == '\0') {
563 // parameters such as host or version are after the first \0
564 if (nullChar != 0 && !key.empty()) {
565 parameters.try_emplace(std::move(key), std::move(value));
566 }
567 nullChar += 1;
568 isKey = true;
569 key.clear();
570 value.clear();
571 } else if (letter == '=') {
572 isKey = false;
573 } else if (nullChar != 0) {
574 if (isKey) {
575 key += letter;
576 } else {
577 value += letter;
578 }
579 }
580 }
581 return parameters;
582}
583
584GitServer::GitServer(const std::string& accountId,
585 const std::string& conversationId,
586 const std::shared_ptr<dhtnet::ChannelSocket>& client)
587{
588 auto path = (fileutils::get_data_dir() / accountId / "conversations" / conversationId).string();
589 pimpl_ = std::make_unique<GitServer::Impl>(accountId, conversationId, path, client);
590}
591
593{
594 stop();
595 pimpl_.reset();
596}
597
598void
600{
601 if (!pimpl_)
602 return;
603 pimpl_->onFetchedCb_ = cb;
604}
605
606void
608{
609 pimpl_->stop();
610}
611
612} // namespace jami
Impl(const std::string &accountId, const std::string &repositoryId, const std::string &repository, const std::shared_ptr< dhtnet::ChannelSocket > &socket)
Definition gitserver.cpp:46
void sendReferenceCapabilities(bool sendVersion=false)
std::map< std::string, std::string > getParameters(std::string_view pkt_line)
std::string repositoryId_
Definition gitserver.cpp:97
std::atomic_bool isDestroying_
std::string accountId_
Definition gitserver.cpp:96
bool parseOrder(std::string_view buf={})
std::shared_ptr< dhtnet::ChannelSocket > socket_
Definition gitserver.cpp:99
onFetchedCb onFetchedCb_
std::vector< std::string > haveRefs_
std::string wantedReference_
std::string repository_
Definition gitserver.cpp:98
GitServer(const std::string &accountId, const std::string &conversationId, const std::shared_ptr< dhtnet::ChannelSocket > &client)
Serve a conversation to a remote client This client will be able to fetch commits/clone the repositor...
void setOnFetched(const onFetchedCb &cb)
Add a callback which will be triggered when the peer gets the data.
void stop()
Stopping a GitServer will shut the channel down.
constexpr auto HAVE_CMD
Definition gitserver.cpp:38
constexpr auto WANT_CMD
Definition gitserver.cpp:37
constexpr auto SERVER_CAPABILITIES
Definition gitserver.cpp:39
constexpr auto DONE_CMD
Definition gitserver.cpp:36
constexpr auto NAK_PKT
Definition gitserver.cpp:35
constexpr auto FLUSH_PKT
Definition gitserver.cpp:34
constexpr auto UPLOAD_PACK_CMD
#define JAMI_ERROR(formatstr,...)
Definition logger.h:243
#define JAMI_DEBUG(formatstr,...)
Definition logger.h:238
#define JAMI_WARNING(formatstr,...)
Definition logger.h:242
#define JAMI_LOG(formatstr,...)
Definition logger.h:237
const std::filesystem::path & get_data_dir()
static constexpr int version
std::unique_ptr< git_repository, GitRepositoryDeleter > GitRepository
Definition git_def.h:34
void emitSignal(Args... args)
Definition jami_signal.h:64
std::unique_ptr< git_commit, GitCommitDeleter > GitCommit
Definition git_def.h:46
std::string toGitHex(size_t value)
std::unique_ptr< git_packbuilder, GitPackBuilderDeleter > GitPackBuilder
Definition git_def.h:28
std::function< void(const std::string &)> onFetchedCb
Definition gitserver.h:32
std::unique_ptr< git_revwalk, GitRevWalkerDeleter > GitRevWalker
Definition git_def.h:40