LCOV - code coverage report
Current view: top level - src - gittransport.cpp (source / functions) Coverage Total Hit
Test: jami-coverage-filtered.info Lines: 81.1 % 164 133
Test Date: 2026-08-24 08:14:20 Functions: 100.0 % 11 11

            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         2010 : generateRequest(git_buf* request, const std::string& cmd, std::string_view url)
      48              : {
      49         2010 :     if (cmd.empty()) {
      50            0 :         giterr_set_str(GITERR_NET, "empty command");
      51            0 :         return -1;
      52              :     }
      53              :     // url format = deviceId/conversationId
      54         2010 :     auto delim = url.find('/');
      55         2010 :     if (delim == std::string::npos) {
      56            0 :         giterr_set_str(GITERR_NET, "malformed URL");
      57            0 :         return -1;
      58              :     }
      59              : 
      60         2010 :     auto deviceId = url.substr(0, delim);
      61         2010 :     auto conversationId = url.substr(delim, url.size());
      62              : 
      63         2010 :     constexpr auto nullSeparator = "\0"sv;
      64              :     auto total = 4                                   /* 4 bytes for the len len */
      65         2010 :                  + cmd.size()                        /* followed by the command */
      66              :                  + 1                                 /* space */
      67         2010 :                  + conversationId.size()             /* conversation */
      68              :                  + 1                                 /* \0 */
      69         2010 :                  + HOST_TAG.size() + deviceId.size() /* device */
      70         2010 :                  + nullSeparator.size() /* \0 */;
      71              : 
      72         2010 :     std::string str;
      73         2010 :     str.reserve(total);
      74         2010 :     str.append("0000"sv);
      75         2009 :     fmt::format_to_n(str.begin(), 4, "{:04x}", total);
      76         2010 :     str.append(cmd)
      77         2010 :         .append(" "sv)
      78         2010 :         .append(conversationId)
      79         2009 :         .append(nullSeparator)
      80         2010 :         .append(HOST_TAG)
      81         2010 :         .append(deviceId)
      82         2009 :         .append(nullSeparator);
      83              : 
      84         2010 :     return git_buf_set(request, str.data(), str.size());
      85         2010 : }
      86              : 
      87              : int
      88         2010 : sendCmd(P2PStream* s)
      89              : {
      90         2010 :     git_buf request = {};
      91         2010 :     if (generateRequest(&request, s->cmd, s->url) < 0) {
      92            0 :         git_buf_dispose(&request);
      93            0 :         return -1;
      94              :     }
      95              : 
      96         2010 :     auto sock = s->socket.lock();
      97         2010 :     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         2010 :     std::error_code ec;
     106         2010 :     sock->write(reinterpret_cast<const unsigned char*>(request.ptr), request.size, ec);
     107         2010 :     git_buf_dispose(&request);
     108         2010 :     if (ec) {
     109           17 :         giterr_set_str(GITERR_NET, ec.message().c_str());
     110           17 :         return -1;
     111              :     }
     112              : 
     113         1992 :     s->sent_command = 1;
     114         1992 :     return 0;
     115         2009 : }
     116              : 
     117              : int
     118         6694 : P2PStreamRead(git_smart_subtransport_stream* stream, char* buffer, size_t buflen, size_t* read)
     119              : {
     120         6694 :     *read = 0;
     121         6694 :     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         6694 :     if (auto hook = P2P_STALL_HOOK.load())
     126         6694 :         (*hook)(fs->url);
     127              : #endif
     128         6694 :     auto sock = fs->socket.lock();
     129         6693 :     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         6694 :     if (!fs->sent_command && sendCmd(fs) < 0)
     137           16 :         return -1;
     138              : 
     139         6677 :     std::error_code ec;
     140         6676 :     auto datalen = sock->waitForData(P2P_READ_TIMEOUT, ec);
     141         6678 :     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         6677 :     *read = sock->read(reinterpret_cast<unsigned char*>(buffer), std::min<size_t>(datalen, buflen), ec);
     149         6677 :     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         6677 :     return 0;
     156         6694 : }
     157              : 
     158              : int
     159         4255 : P2PStreamWrite(git_smart_subtransport_stream* stream, const char* buffer, size_t len)
     160              : {
     161         4255 :     auto* fs = reinterpret_cast<P2PStream*>(stream);
     162         4255 :     auto sock = fs->socket.lock();
     163         4253 :     if (!sock) {
     164            0 :         giterr_set_str(GITERR_NET, "unavailable socket");
     165            0 :         return -1;
     166              :     }
     167         4254 :     std::error_code ec;
     168         4253 :     sock->write(reinterpret_cast<const unsigned char*>(buffer), len, ec);
     169         4256 :     if (ec) {
     170            2 :         giterr_set_str(GITERR_NET, ec.message().c_str());
     171            1 :         return -1;
     172              :     }
     173         4254 :     return 0;
     174         4255 : }
     175              : 
     176              : void
     177         2004 : P2PStreamFree(git_smart_subtransport_stream*)
     178         2004 : {}
     179              : 
     180              : int
     181         6278 : P2PSubTransportAction(git_smart_subtransport_stream** out,
     182              :                       git_smart_subtransport* transport,
     183              :                       const char* url,
     184              :                       git_smart_service_t action)
     185              : {
     186         6278 :     auto* sub = reinterpret_cast<P2PSubTransport*>(transport);
     187         6278 :     if (!sub || !sub->remote) {
     188            0 :         JAMI_ERROR("Invalid subtransport");
     189            0 :         return -1;
     190              :     }
     191              : 
     192         6278 :     auto* repo = git_remote_owner(sub->remote);
     193         6279 :     if (!repo) {
     194            0 :         JAMI_ERROR("No repository linked to the transport");
     195            0 :         return -1;
     196              :     }
     197              : 
     198         6279 :     const auto* workdir = git_repository_workdir(repo);
     199         6279 :     if (!workdir) {
     200            0 :         JAMI_ERROR("No working linked to the repository");
     201            0 :         return -1;
     202              :     }
     203         6279 :     std::string_view path = workdir;
     204         6270 :     auto delimConv = path.rfind("/conversations");
     205         6275 :     if (delimConv == std::string::npos) {
     206            0 :         JAMI_ERROR("No conversation id found");
     207            0 :         return -1;
     208              :     }
     209         6275 :     auto delimAccount = path.rfind('/', delimConv - 1);
     210         6280 :     if (delimAccount == std::string::npos && delimConv - 1 - delimAccount == 16) {
     211            0 :         JAMI_ERROR("No account id found");
     212            0 :         return -1;
     213              :     }
     214         6280 :     auto accountId = path.substr(delimAccount + 1, delimConv - 1 - delimAccount);
     215         6276 :     std::string_view gitUrl = url + ("git://"sv).size();
     216         6273 :     auto delim = gitUrl.find('/');
     217         6274 :     if (delim == std::string::npos) {
     218            0 :         JAMI_ERROR("Incorrect url {:s}", gitUrl);
     219            0 :         return -1;
     220              :     }
     221         6274 :     auto deviceId = gitUrl.substr(0, delim);
     222         6271 :     auto conversationId = gitUrl.substr(delim + 1, gitUrl.size());
     223              : 
     224         6271 :     if (action == GIT_SERVICE_UPLOADPACK_LS) {
     225         2022 :         auto gitSocket = jami::Manager::instance().gitSocket(accountId, deviceId, conversationId);
     226         2022 :         if (!gitSocket) {
     227           18 :             JAMI_ERROR("Unable to find related socket for {:s}, {:s}, {:s}", accountId, deviceId, conversationId);
     228           18 :             return -1;
     229              :         }
     230         2004 :         auto stream = std::make_unique<P2PStream>();
     231         2004 :         stream->socket = gitSocket;
     232         2004 :         stream->base.read = P2PStreamRead;
     233         2004 :         stream->base.write = P2PStreamWrite;
     234         2004 :         stream->base.free = P2PStreamFree;
     235         2004 :         stream->cmd = UPLOAD_PACK_CMD;
     236         2004 :         stream->url = gitUrl;
     237         2004 :         sub->stream = std::move(stream);
     238         2004 :         *out = &sub->stream->base;
     239         2004 :         return 0;
     240         6271 :     } else if (action == GIT_SERVICE_UPLOADPACK) {
     241         4251 :         if (sub->stream) {
     242         4254 :             *out = &sub->stream->base;
     243         4255 :             return 0;
     244              :         }
     245            0 :         return -1;
     246              :     }
     247            0 :     return 0;
     248              : }
     249              : 
     250              : int
     251         6033 : P2PSubTransportClose(git_smart_subtransport*)
     252              : {
     253         6033 :     return 0;
     254              : }
     255              : 
     256              : void
     257         2020 : P2PSubTransportFree(git_smart_subtransport* transport)
     258              : {
     259         2020 :     jami::Manager::instance().eraseGitTransport(transport);
     260         2022 : }
     261              : 
     262              : int
     263         2022 : P2PSubTransportNew(P2PSubTransport** out, git_transport*, void* payload)
     264              : {
     265         2022 :     auto sub = std::make_unique<P2PSubTransport>();
     266         2020 :     sub->remote = reinterpret_cast<git_remote*>(payload);
     267         2020 :     auto* base = &sub->base;
     268         2021 :     base->action = P2PSubTransportAction;
     269         2021 :     base->close = P2PSubTransportClose;
     270         2021 :     base->free = P2PSubTransportFree;
     271         2021 :     *out = sub.get();
     272         2021 :     jami::Manager::instance().insertGitTransport(base, std::move(sub));
     273         2022 :     return 0;
     274         2022 : }
     275              : 
     276              : int
     277         2022 : p2p_subtransport_cb(git_smart_subtransport** out, git_transport* owner, void* payload)
     278              : {
     279              :     P2PSubTransport* sub;
     280              : 
     281         2022 :     if (P2PSubTransportNew(&sub, owner, payload) < 0)
     282            0 :         return -1;
     283              : 
     284         2022 :     *out = &sub->base;
     285         2022 :     return 0;
     286              : }
     287              : 
     288              : int
     289         2022 : p2p_transport_cb(git_transport** out, git_remote* owner, void*)
     290              : {
     291         2022 :     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         2022 :            reinterpret_cast<void*>(owner)};
     295         4044 :     return git_transport_smart(out, owner, &def);
     296              : }
        

Generated by: LCOV version 2.0-1