Ring Daemon 16.0.0
Loading...
Searching...
No Matches
gitserver.cpp
Go to the documentation of this file.
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
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
40 = " HEAD\0side-band side-band-64k shallow no-progress include-tag"sv;
41
42namespace jami {
43
45{
46public:
47 Impl(const std::string& accountId,
48 const std::string& repositoryId,
49 const std::string& repository,
50 const std::shared_ptr<dhtnet::ChannelSocket>& socket)
51 : accountId_(accountId)
53 , repository_(repository)
54 , socket_(socket)
55 {
56 JAMI_DEBUG("[Account {}] [Conversation {}] [GitServer {}] created",
59 fmt::ptr(this));
60 // Check at least if repository is correct
62 if (git_repository_open(&repo, repository_.c_str()) != 0) {
63 socket_->shutdown();
64 return;
65 }
67
68 socket_->setOnRecv([this](const uint8_t* buf, std::size_t len) {
69 std::lock_guard lk(destroyMtx_);
70 if (isDestroying_)
71 return len;
72 if (parseOrder(std::string_view((const char*)buf, len)))
73 while(parseOrder());
74 return len;
75 });
76 }
78 stop();
79 JAMI_DEBUG("[Account {}] [Conversation {}] [GitServer {}] destroyed",
82 fmt::ptr(this));
83 }
84 void stop()
85 {
86 std::lock_guard lk(destroyMtx_);
87 if (isDestroying_.exchange(true)) {
88 socket_->setOnRecv({});
89 socket_->shutdown();
90 }
91 }
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};
112};
113
114bool
116{
117 std::string pkt = std::move(cachedPkt_);
118 if (!buf.empty())
119 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 unsigned int pkt_len = 0;
126 auto [p, ec] = std::from_chars(pkt.data(), pkt.data() + 4, pkt_len, 16);
127 if (ec != std::errc()) {
128 JAMI_ERROR("[Account {}] [Conversation {}] [GitServer {}] Unable to parse packet size", accountId_, repositoryId_, fmt::ptr(this));
129 }
130 if (pkt_len != pkt.size()) {
131 // Store next packet part
132 if (pkt_len == 0) {
133 // FLUSH_PKT
134 pkt_len = 4;
135 }
136 cachedPkt_ = pkt.substr(pkt_len);
137 }
138
139 auto pack = std::string_view(pkt).substr(4, pkt_len - 4);
140 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 JAMI_LOG("[Account {}] [Conversation {}] [GitServer {}] Peer negotiation is done. Answering to want order", accountId_, repositoryId_, fmt::ptr(this));
146 bool sendData;
147 if (common_.empty())
148 sendData = NAK();
149 else
150 sendData = ACKFirst();
151 if (sendData)
152 sendPackData();
153 return !cachedPkt_.empty();
154 } else if (pack.empty()) {
155 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 ACKCommon();
160 NAK();
161 }
162 return !cachedPkt_.empty();
163 }
164
165 auto lim = pack.find(' ');
166 auto cmd = pack.substr(0, lim);
167 auto dat = (lim < pack.size()) ? pack.substr(lim+1) : std::string_view{};
168 if (cmd == UPLOAD_PACK_CMD) {
169 // Cf: https://github.com/git/git/blob/master/Documentation/technical/pack-protocol.txt#L166
170 // References discovery
171 JAMI_LOG("[Account {}] [Conversation {}] [GitServer {}] Upload pack command detected.", accountId_, repositoryId_, fmt::ptr(this));
172 auto version = 1;
173 auto parameters = getParameters(dat);
174 auto versionIt = parameters.find("version");
175 bool sendVersion = false;
176 if (versionIt != parameters.end()) {
177 auto [p, ec] = std::from_chars(versionIt->second.data(),
178 versionIt->second.data() + versionIt->second.size(),
179 version);
180 if (ec == std::errc()) {
181 sendVersion = true;
182 } else {
183 JAMI_WARNING("[Account {}] [Conversation {}] [GitServer {}] Invalid version detected: {}", accountId_, repositoryId_, fmt::ptr(this), versionIt->second);
184 }
185 }
186 if (version == 1) {
188 } else {
189 JAMI_ERROR("[Account {}] [Conversation {}] [GitServer {}] That protocol version is not yet supported (version: {:d})", accountId_, repositoryId_, fmt::ptr(this), version);
190 }
191 } 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 wantedReference_ = dat.substr(0, 40);
196 JAMI_LOG("[Account {}] [Conversation {}] [GitServer {}] Peer want ref: {}", accountId_, repositoryId_, fmt::ptr(this), wantedReference_);
197 } else if (cmd == HAVE_CMD) {
198 const auto& commit = haveRefs_.emplace_back(dat.substr(0, 40));
199 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
205 if (git_repository_open(&repo, repository_.c_str()) != 0) {
206 JAMI_WARNING("[Account {}] [Conversation {}] [GitServer {}] Unable to open {}", accountId_, repositoryId_, fmt::ptr(this), repository_);
207 return !cachedPkt_.empty();
208 }
211 if (git_oid_fromstr(&commit_id, commit.c_str()) == 0) {
212 // Reference found
213 common_ = commit;
214 }
215 }
216 } else {
217 JAMI_WARNING("[Account {}] [Conversation {}] [GitServer {}] Unwanted packet received: {}", accountId_, repositoryId_, fmt::ptr(this), pkt);
218 }
219 return !cachedPkt_.empty();
220}
221
222std::string
224 return fmt::format(FMT_COMPILE("{:04x}"), value & 0x0FFFF);
225}
226
227void
229{
230 // Get references
231 // First, get the HEAD reference
232 // https://github.com/git/git/blob/master/Documentation/technical/pack-protocol.txt#L166
234 if (git_repository_open(&repo, repository_.c_str()) != 0) {
235 JAMI_WARNING("[Account {}] [Conversation {}] [GitServer {}] Unable to open {}", accountId_, repositoryId_, fmt::ptr(this), repository_);
236 socket_->shutdown();
237 return;
238 }
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 std::error_code ec;
245 if (sendVersion) {
246 constexpr auto toSend = "000eversion 1\0"sv;
247 socket_->write(reinterpret_cast<const unsigned char*>(toSend.data()),
248 toSend.size(),
249 ec);
250 if (ec) {
251 JAMI_WARNING("[Account {}] [Conversation {}] [GitServer {}] Unable to send data for {}: {}", accountId_, repositoryId_, fmt::ptr(this), repository_, ec.message());
252 socket_->shutdown();
253 return;
254 }
255 }
256
258 if (git_reference_name_to_id(&commit_id, rep.get(), "HEAD") < 0) {
259 JAMI_ERROR("[Account {}] [Conversation {}] [GitServer {}] Unable to get reference for HEAD", accountId_, repositoryId_, fmt::ptr(this));
260 socket_->shutdown();
261 return;
262 }
263 std::string_view currentHead = git_oid_tostr_s(&commit_id);
264
265 // Send references
266 std::ostringstream packet;
267 packet << toGitHex(5 + currentHead.size() + SERVER_CAPABILITIES.size());
268 packet << currentHead << SERVER_CAPABILITIES << "\n";
269
270 // Now, add other references
272 if (git_reference_list(&refs, rep.get()) == 0) {
273 for (std::size_t i = 0; i < refs.count; ++i) {
274 std::string_view ref = refs.strings[i];
275 if (git_reference_name_to_id(&commit_id, rep.get(), ref.data()) < 0) {
276 JAMI_WARNING("[Account {}] [Conversation {}] [GitServer {}] Unable to get reference for {}", accountId_, repositoryId_, fmt::ptr(this), ref);
277 continue;
278 }
280
281 packet << toGitHex(6 /* size + space + \n */ + currentHead.size() + ref.size());
282 packet << currentHead << " " << ref << "\n";
283 }
284 }
286
287 // And add FLUSH
288 packet << FLUSH_PKT;
289 auto toSend = packet.str();
290 socket_->write(reinterpret_cast<const unsigned char*>(toSend.data()), toSend.size(), ec);
291 if (ec) {
292 JAMI_WARNING("[Account {}] [Conversation {}] [GitServer {}] Unable to send data for {}: {}", accountId_, repositoryId_, fmt::ptr(this), repository_, ec.message());
293 socket_->shutdown();
294 }
295}
296
297void
299{
300 std::error_code ec;
301 // Ack common base
302 if (!common_.empty()) {
303 auto toSend = fmt::format(FMT_COMPILE("{:04x}ACK {} continue\n"),
304 18 + common_.size() /* size + ACK + space * 2 + continue + \n */, common_);
305 socket_->write(reinterpret_cast<const unsigned char*>(toSend.c_str()), toSend.size(), ec);
306 if (ec) {
307 JAMI_WARNING("[Account {}] [Conversation {}] [GitServer {}] Unable to send data for {}: {}", accountId_, repositoryId_, fmt::ptr(this), repository_, ec.message());
308 socket_->shutdown();
309 }
310 }
311}
312
313bool
315{
316 std::error_code ec;
317 // Ack common base
318 if (!common_.empty()) {
319 auto toSend = fmt::format(FMT_COMPILE("{:04x}ACK {}\n"),
320 9 + common_.size() /* size + ACK + space + \n */, common_);
321 socket_->write(reinterpret_cast<const unsigned char*>(toSend.c_str()), toSend.size(), ec);
322 if (ec) {
323 JAMI_WARNING("[Account {}] [Conversation {}] [GitServer {}] Unable to send data for {}: {}", accountId_, repositoryId_, fmt::ptr(this), repository_, ec.message());
324 socket_->shutdown();
325 return false;
326 }
327 }
328 return true;
329}
330
331bool
333{
334 std::error_code ec;
335 // NAK
336 socket_->write(reinterpret_cast<const unsigned char*>(NAK_PKT.data()), NAK_PKT.size(), ec);
337 if (ec) {
338 JAMI_WARNING("[Account {}] [Conversation {}] [GitServer {}] Unable to send data for {}: {}", accountId_, repositoryId_, fmt::ptr(this), repository_, ec.message());
339 socket_->shutdown();
340 return false;
341 }
342 return true;
343}
344
345void
347{
349 if (git_repository_open(&repo_ptr, repository_.c_str()) != 0) {
350 JAMI_WARNING("[Account {}] [Conversation {}] [GitServer {}] Unable to open {}", accountId_, repositoryId_, fmt::ptr(this), repository_);
351 return;
352 }
354
356 if (git_packbuilder_new(&pb_ptr, repo.get()) != 0) {
357 JAMI_WARNING("[Account {}] [Conversation {}] [GitServer {}] Unable to open packbuilder for {}", accountId_, repositoryId_, fmt::ptr(this), repository_);
358 return;
359 }
361
362 std::string fetched = wantedReference_;
363 git_oid oid;
364 if (git_oid_fromstr(&oid, fetched.c_str()) < 0) {
365 JAMI_ERROR("[Account {}] [Conversation {}] [GitServer {}] Unable to get reference for commit {}", accountId_, repositoryId_, fmt::ptr(this), fetched);
366 return;
367 }
368
369 git_revwalk* walker_ptr = nullptr;
370 if (git_revwalk_new(&walker_ptr, repo.get()) < 0 || git_revwalk_push(walker_ptr, &oid) < 0) {
371 if (walker_ptr)
373 return;
374 }
377 // Add first commit
378 std::set<std::string> parents;
379 auto haveCommit = false;
380
381 while (!git_revwalk_next(&oid, walker.get())) {
382 // log until have refs
383 std::string id = git_oid_tostr_s(&oid);
384 haveCommit |= std::find(haveRefs_.begin(), haveRefs_.end(), id) != haveRefs_.end();
385 auto itParents = std::find(parents.begin(), parents.end(), id);
386 if (itParents != parents.end())
387 parents.erase(itParents);
388 if (haveCommit && parents.size() == 0 /* We are sure that all commits are there */)
389 break;
390 if (git_packbuilder_insert_commit(pb.get(), &oid) != 0) {
391 JAMI_WARNING("[Account {}] [Conversation {}] [GitServer {}] Unable to open insert commit {} for {}", accountId_, repositoryId_, fmt::ptr(this), git_oid_tostr_s(&oid), repository_);
392 return;
393 }
394
395 // Get next commit to pack
397 if (git_commit_lookup(&commit_ptr, repo.get(), &oid) < 0) {
398 JAMI_ERROR("[Account {}] [Conversation {}] [GitServer {}] Unable to look up current commit", accountId_, repositoryId_, fmt::ptr(this));
399 return;
400 }
402 auto parentsCount = git_commit_parentcount(commit.get());
403 for (unsigned int p = 0; p < parentsCount; ++p) {
404 // make sure to explore all branches
405 const git_oid* pid = git_commit_parent_id(commit.get(), p);
406 if (pid)
407 parents.emplace(git_oid_tostr_s(pid));
408 }
409 }
410
411 git_buf data = {};
412 if (git_packbuilder_write_buf(&data, pb.get()) != 0) {
413 JAMI_WARNING("[Account {}] [Conversation {}] [GitServer {}] Unable to write pack data for {}", accountId_, repositoryId_, fmt::ptr(this), repository_);
414 return;
415 }
416
417 std::size_t sent = 0;
418 std::size_t len = data.size;
419 std::error_code ec;
420 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 std::size_t pkt_size = std::min(static_cast<std::size_t>(65515), len - sent);
426 std::string toSendHeader = toGitHex(pkt_size + 5);
427 toSendData.clear();
428 toSendData.reserve(pkt_size + 5);
429 toSendData.insert(toSendData.end(), toSendHeader.begin(), toSendHeader.end());
430 toSendData.push_back(0x1);
431 toSendData.insert(toSendData.end(), data.ptr + sent, data.ptr + sent + pkt_size);
432
433 socket_->write(reinterpret_cast<const unsigned char*>(toSendData.data()),
434 toSendData.size(),
435 ec);
436 if (ec) {
437 JAMI_WARNING("[Account {}] [Conversation {}] [GitServer {}] Unable to send data for {}: {}", accountId_, repositoryId_, fmt::ptr(this), repository_, ec.message());
438 git_buf_dispose(&data);
439 return;
440 }
441 sent += pkt_size;
442 } while (sent < len);
443 git_buf_dispose(&data);
444 toSendData = {};
445
446 // And finish by a little FLUSH
447 socket_->write(reinterpret_cast<const uint8_t*>(FLUSH_PKT.data()), FLUSH_PKT.size(), ec);
448 if (ec) {
449 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 haveRefs_.clear();
454 wantedReference_.clear();
455 common_.clear();
456 if (onFetchedCb_)
457 onFetchedCb_(fetched);
458}
459
460std::map<std::string, std::string>
462{
463 std::map<std::string, std::string> parameters;
464 std::string key, value;
465 auto isKey = true;
466 auto nullChar = 0;
467 for (auto letter: pkt_line) {
468 if (letter == '\0') {
469 // parameters such as host or version are after the first \0
470 if (nullChar != 0 && !key.empty()) {
471 parameters.try_emplace(std::move(key), std::move(value));
472 }
473 nullChar += 1;
474 isKey = true;
475 key.clear();
476 value.clear();
477 } else if (letter == '=') {
478 isKey = false;
479 } else if (nullChar != 0) {
480 if (isKey) {
481 key += letter;
482 } else {
483 value += letter;
484 }
485 }
486 }
487 return parameters;
488}
489
490GitServer::GitServer(const std::string& accountId,
491 const std::string& conversationId,
492 const std::shared_ptr<dhtnet::ChannelSocket>& client)
493{
494 auto path = (fileutils::get_data_dir() / accountId / "conversations" / conversationId).string();
495 pimpl_ = std::make_unique<GitServer::Impl>(accountId, conversationId, path, client);
496}
497
499{
500 stop();
501 pimpl_.reset();
502}
503
504void
506{
507 if (!pimpl_)
508 return;
509 pimpl_->onFetchedCb_ = cb;
510}
511
512void
514{
515 pimpl_->stop();
516}
517
518} // 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:47
void sendReferenceCapabilities(bool sendVersion=false)
std::map< std::string, std::string > getParameters(std::string_view pkt_line)
std::string repositoryId_
std::atomic_bool isDestroying_
bool parseOrder(std::string_view buf={})
std::shared_ptr< dhtnet::ChannelSocket > socket_
onFetchedCb onFetchedCb_
std::vector< std::string > haveRefs_
std::string wantedReference_
std::string repository_
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.
std::unique_ptr< git_commit, decltype(&git_commit_free)> GitCommit
std::unique_ptr< git_repository, decltype(&git_repository_free)> GitRepository
std::unique_ptr< git_packbuilder, decltype(&git_packbuilder_free)> GitPackBuilder
std::unique_ptr< git_revwalk, decltype(&git_revwalk_free)> GitRevWalker
constexpr auto HAVE_CMD
Definition gitserver.cpp:38
constexpr auto WANT_CMD
Definition gitserver.cpp:37
constexpr auto SERVER_CAPABILITIES
Definition gitserver.cpp:40
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:228
#define JAMI_DEBUG(formatstr,...)
Definition logger.h:226
#define JAMI_WARNING(formatstr,...)
Definition logger.h:227
#define JAMI_LOG(formatstr,...)
Definition logger.h:225
const std::filesystem::path & get_data_dir()
static constexpr int version
void emitSignal(Args... args)
Definition ring_signal.h:64
std::string toGitHex(size_t value)
std::function< void(const std::string &)> onFetchedCb
Definition gitserver.h:32