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