Skip to content
Snippets Groups Projects
Select Git revision
  • 072fef2e67cf551c062ec738f1715e2aa7e1e07e
  • master default protected
  • release/202005
  • release/202001
  • release/201912
  • release/201911
  • release/releaseWindowsTestOne
  • release/windowsReleaseTest
  • release/releaseTest
  • release/releaseWindowsTest
  • release/201910
  • release/qt/201910
  • release/windows-test/201910
  • release/201908
  • release/201906
  • release/201905
  • release/201904
  • release/201903
  • release/201902
  • release/201901
  • release/201812
  • 4.0.0
  • 2.2.0
  • 2.1.0
  • 2.0.1
  • 2.0.0
  • 1.4.1
  • 1.4.0
  • 1.3.0
  • 1.2.0
  • 1.1.0
31 results

gitserver.cpp

Blame
  • Code owners
    Assign users and groups as approvers for specific file changes. Learn more.
    gitserver.cpp 16.57 KiB
    /*
     *  Copyright (C) 2019-2024 Savoir-faire Linux Inc.
     *  Author: Sébastien Blin <sebastien.blin@savoirfairelinux.com>
     *
     *  This program is free software; you can redistribute it and/or modify
     *  it under the terms of the GNU General Public License as published by
     *  the Free Software Foundation; either version 3 of the License, or
     *  (at your option) any later version.
     *
     *  This program is distributed in the hope that it will be useful,
     *  but WITHOUT ANY WARRANTY; without even the implied warranty of
     *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     *  GNU General Public License for more details.
     *
     *  You should have received a copy of the GNU General Public License
     *  along with this program. If not, see <https://www.gnu.org/licenses/>.
     */
    #include "gitserver.h"
    
    #include "fileutils.h"
    #include "logger.h"
    #include "gittransport.h"
    #include "manager.h"
    #include <opendht/thread_pool.h>
    #include <dhtnet/multiplexed_socket.h>
    #include <fmt/compile.h>
    
    #include <charconv>
    #include <ctime>
    #include <fstream>
    #include <git2.h>
    #include <iomanip>
    
    using namespace std::string_view_literals;
    constexpr auto FLUSH_PKT = "0000"sv;
    constexpr auto NAK_PKT = "0008NAK\n"sv;
    constexpr auto DONE_CMD = "done\n"sv;
    constexpr auto WANT_CMD = "want"sv;
    constexpr auto HAVE_CMD = "have"sv;
    constexpr auto SERVER_CAPABILITIES
        = " HEAD\0side-band side-band-64k shallow no-progress include-tag"sv;
    
    namespace jami {
    
    class GitServer::Impl
    {
    public:
        Impl(const std::string& repositoryId,
             const std::string& repository,
             const std::shared_ptr<dhtnet::ChannelSocket>& socket)
            : repositoryId_(repositoryId)
            , repository_(repository)
            , socket_(socket)
        {
            // Check at least if repository is correct
            git_repository* repo;
            if (git_repository_open(&repo, repository_.c_str()) != 0) {
                socket_->shutdown();
                return;
            }
            git_repository_free(repo);
    
            socket_->setOnRecv([this](const uint8_t* buf, std::size_t len) {
                std::lock_guard lk(destroyMtx_);
                if (isDestroying_)
                    return len;
                if (parseOrder(std::string_view((const char*)buf, len)))
                    while(parseOrder());
                return len;
            });
        }
        ~Impl() { stop(); }
        void stop()
        {
            std::lock_guard lk(destroyMtx_);
            if (isDestroying_.exchange(true)) {
                socket_->setOnRecv({});
                socket_->shutdown();
            }
        }
        bool parseOrder(std::string_view buf = {});
    
        void sendReferenceCapabilities(bool sendVersion = false);
        bool NAK();
        void ACKCommon();
        bool ACKFirst();
        void sendPackData();
        std::map<std::string, std::string> getParameters(std::string_view pkt_line);
    
        std::string repositoryId_ {};
        std::string repository_ {};
        std::shared_ptr<dhtnet::ChannelSocket> socket_ {};
        std::string wantedReference_ {};
        std::string common_ {};
        std::vector<std::string> haveRefs_ {};
        std::string cachedPkt_ {};
        std::mutex destroyMtx_ {};
        std::atomic_bool isDestroying_ {false};
        onFetchedCb onFetchedCb_ {};
    };
    
    bool
    GitServer::Impl::parseOrder(std::string_view buf)
    {
        std::string pkt = std::move(cachedPkt_);
        if (!buf.empty())
            pkt += buf;
    
        // Parse pkt len
        // Reference: https://github.com/git/git/blob/master/Documentation/technical/protocol-common.txt#L51
        // The first four bytes define the length of the packet and 0000 is a FLUSH pkt
    
        unsigned int pkt_len = 0;
        auto [p, ec] = std::from_chars(pkt.data(), pkt.data() + 4, pkt_len, 16);
        if (ec != std::errc()) {
            JAMI_ERROR("Can't parse packet size");
        }
        if (pkt_len != pkt.size()) {
            // Store next packet part
            if (pkt_len == 0) {
                // FLUSH_PKT
                pkt_len = 4;
            }
            cachedPkt_ = pkt.substr(pkt_len);
        }
    
        auto pack = std::string_view(pkt).substr(4, pkt_len - 4);
        if (pack == DONE_CMD) {
            // Reference:
            // https://github.com/git/git/blob/master/Documentation/technical/pack-protocol.txt#L390 Do
            // not do multi-ack, just send ACK + pack file
            // In case of no common base, send NAK
            JAMI_INFO("Peer negotiation is done. Answering to want order");
            bool sendData;
            if (common_.empty())
                sendData = NAK();
            else
                sendData = ACKFirst();
            if (sendData)
                sendPackData();
            return !cachedPkt_.empty();
        } else if (pack.empty()) {
            if (!haveRefs_.empty()) {
                // Reference:
                // https://github.com/git/git/blob/master/Documentation/technical/pack-protocol.txt#L390
                // Do not do multi-ack, just send ACK + pack file In case of no common base ACK
                ACKCommon();
                NAK();
            }
            return !cachedPkt_.empty();
        }
    
        auto lim = pack.find(' ');
        auto cmd = pack.substr(0, lim);
        auto dat = (lim < pack.size()) ? pack.substr(lim+1) : std::string_view{};
        if (cmd == UPLOAD_PACK_CMD) {
            // Cf: https://github.com/git/git/blob/master/Documentation/technical/pack-protocol.txt#L166
            // References discovery
            JAMI_INFO("Upload pack command detected.");
            auto version = 1;
            auto parameters = getParameters(dat);
            auto versionIt = parameters.find("version");
            bool sendVersion = false;
            if (versionIt != parameters.end()) {
                try {
                    version = std::stoi(versionIt->second);
                    sendVersion = true;
                } catch (...) {
                    JAMI_WARNING("Invalid version detected: {}", versionIt->second);
                }
            }
            if (version == 1) {
                sendReferenceCapabilities(sendVersion);
            } else {
                JAMI_ERR("That protocol version is not yet supported (version: %u)", version);
            }
        } else if (cmd == WANT_CMD) {
            // Reference:
            // https://github.com/git/git/blob/master/Documentation/technical/pack-protocol.txt#L229
            // TODO can have more want
            wantedReference_ = dat.substr(0, 40);
            JAMI_INFO("Peer want ref: %s", wantedReference_.c_str());
        } else if (cmd == HAVE_CMD) {
            const auto& commit = haveRefs_.emplace_back(dat.substr(0, 40));
            if (common_.empty()) {
                // Detect first common commit
                // Reference:
                // https://github.com/git/git/blob/master/Documentation/technical/pack-protocol.txt#L390
                // TODO do not open repository every time
                git_repository* repo;
                if (git_repository_open(&repo, repository_.c_str()) != 0) {
                    JAMI_WARN("Couldn't open %s", repository_.c_str());
                    return !cachedPkt_.empty();
                }
                GitRepository rep {repo, git_repository_free};
                git_oid commit_id;
                if (git_oid_fromstr(&commit_id, commit.c_str()) == 0) {
                    // Reference found
                    common_ = commit;
                }
            }
        } else {
            JAMI_WARNING("Unwanted packet received: {}", pkt);
        }
        return !cachedPkt_.empty();
    }
    
    std::string
    toGitHex(size_t value) {
        return fmt::format(FMT_COMPILE("{:04x}"), value & 0x0FFFF);
    }
    
    void
    GitServer::Impl::sendReferenceCapabilities(bool sendVersion)
    {
        // Get references
        // First, get the HEAD reference
        // https://github.com/git/git/blob/master/Documentation/technical/pack-protocol.txt#L166
        git_repository* repo;
        if (git_repository_open(&repo, repository_.c_str()) != 0) {
            JAMI_WARNING("Couldn't open {}", repository_);
            socket_->shutdown();
            return;
        }
        GitRepository rep {repo, git_repository_free};
    
        // Answer with the version number
        // **** When the client initially connects the server will immediately respond
        // **** with a version number (if "version=1" is sent as an Extra Parameter),
        std::error_code ec;
        if (sendVersion) {
            auto toSend = "000eversion 1\0"sv;
            socket_->write(reinterpret_cast<const unsigned char*>(toSend.data()),
                           toSend.size(),
                           ec);
            if (ec) {
                JAMI_WARNING("Couldn't send data for {}: {}", repository_, ec.message());
                socket_->shutdown();
                return;
            }
        }
    
        git_oid commit_id;
        if (git_reference_name_to_id(&commit_id, rep.get(), "HEAD") < 0) {
            JAMI_ERROR("Cannot get reference for HEAD");
            socket_->shutdown();
            return;
        }
        std::string currentHead = git_oid_tostr_s(&commit_id);
    
        // Send references
        std::ostringstream packet;
        packet << toGitHex(5 + currentHead.size() + SERVER_CAPABILITIES.size());
        packet << currentHead << SERVER_CAPABILITIES << "\n";
    
        // Now, add other references
        git_strarray refs;
        if (git_reference_list(&refs, rep.get()) == 0) {
            for (std::size_t i = 0; i < refs.count; ++i) {
                std::string_view ref = refs.strings[i];
                if (git_reference_name_to_id(&commit_id, rep.get(), ref.data()) < 0) {
                    JAMI_WARNING("Cannot get reference for {}", ref);
                    continue;
                }
                currentHead = git_oid_tostr_s(&commit_id);
    
                packet << toGitHex(6 /* size + space + \n */ + currentHead.size() + ref.size());
                packet << currentHead << " " << ref << "\n";
            }
        }
        git_strarray_dispose(&refs);
    
        // And add FLUSH
        packet << FLUSH_PKT;
        auto toSend = packet.str();
        socket_->write(reinterpret_cast<const unsigned char*>(toSend.data()), toSend.size(), ec);
        if (ec) {
            JAMI_WARNING("Couldn't send data for {}: {}", repository_, ec.message());
            socket_->shutdown();
        }
    }
    
    void
    GitServer::Impl::ACKCommon()
    {
        std::error_code ec;
        // Ack common base
        if (!common_.empty()) {
            auto toSend = fmt::format(FMT_COMPILE("{:04x}ACK {} continue\n"),
                18 + common_.size() /* size + ACK + space * 2 + continue + \n */, common_);
            socket_->write(reinterpret_cast<const unsigned char*>(toSend.c_str()), toSend.size(), ec);
            if (ec) {
                JAMI_WARNING("Couldn't send data for {}: {}", repository_, ec.message());
                socket_->shutdown();
            }
        }
    }
    
    bool
    GitServer::Impl::ACKFirst()
    {
        std::error_code ec;
        // Ack common base
        if (!common_.empty()) {
            auto toSend = fmt::format(FMT_COMPILE("{:04x}ACK {}\n"),
                9 + common_.size() /* size + ACK + space + \n */, common_);
            socket_->write(reinterpret_cast<const unsigned char*>(toSend.c_str()), toSend.size(), ec);
            if (ec) {
                JAMI_WARNING("Couldn't send data for {}: {}", repository_, ec.message());
                socket_->shutdown();
                return false;
            }
        }
        return true;
    }
    
    bool
    GitServer::Impl::NAK()
    {
        std::error_code ec;
        // NAK
        socket_->write(reinterpret_cast<const unsigned char*>(NAK_PKT.data()), NAK_PKT.size(), ec);
        if (ec) {
            JAMI_WARNING("Couldn't send data for {}: {}", repository_, ec.message());
            socket_->shutdown();
            return false;
        }
        return true;
    }
    
    void
    GitServer::Impl::sendPackData()
    {
        git_repository* repo_ptr;
        if (git_repository_open(&repo_ptr, repository_.c_str()) != 0) {
            JAMI_WARN("Couldn't open %s", repository_.c_str());
            return;
        }
        GitRepository repo {repo_ptr, git_repository_free};
    
        git_packbuilder* pb_ptr;
        if (git_packbuilder_new(&pb_ptr, repo.get()) != 0) {
            JAMI_WARNING("Couldn't open packbuilder for {}", repository_);
            return;
        }
        GitPackBuilder pb {pb_ptr, git_packbuilder_free};
    
        std::string fetched = wantedReference_;
        git_oid oid;
        if (git_oid_fromstr(&oid, fetched.c_str()) < 0) {
            JAMI_ERROR("Cannot get reference for commit {}", fetched);
            return;
        }
    
        git_revwalk* walker_ptr = nullptr;
        if (git_revwalk_new(&walker_ptr, repo.get()) < 0 || git_revwalk_push(walker_ptr, &oid) < 0) {
            if (walker_ptr)
                git_revwalk_free(walker_ptr);
            return;
        }
        GitRevWalker walker {walker_ptr, git_revwalk_free};
        git_revwalk_sorting(walker.get(), GIT_SORT_TOPOLOGICAL);
        // Add first commit
        std::set<std::string> parents;
        auto haveCommit = false;
    
        while (!git_revwalk_next(&oid, walker.get())) {
            // log until have refs
            std::string id = git_oid_tostr_s(&oid);
            haveCommit |= std::find(haveRefs_.begin(), haveRefs_.end(), id) != haveRefs_.end();
            auto itParents = std::find(parents.begin(), parents.end(), id);
            if (itParents != parents.end())
                parents.erase(itParents);
            if (haveCommit && parents.size() == 0 /* We are sure that all commits are there */)
                break;
            if (git_packbuilder_insert_commit(pb.get(), &oid) != 0) {
                JAMI_WARN("Couldn't open insert commit %s for %s",
                          git_oid_tostr_s(&oid),
                          repository_.c_str());
                return;
            }
    
            // Get next commit to pack
            git_commit* commit_ptr;
            if (git_commit_lookup(&commit_ptr, repo.get(), &oid) < 0) {
                JAMI_ERR("Could not look up current commit");
                return;
            }
            GitCommit commit {commit_ptr, git_commit_free};
            auto parentsCount = git_commit_parentcount(commit.get());
            for (unsigned int p = 0; p < parentsCount; ++p) {
                // make sure to explore all branches
                const git_oid* pid = git_commit_parent_id(commit.get(), p);
                if (pid)
                    parents.emplace(git_oid_tostr_s(pid));
            }
        }
    
        git_buf data = {};
        if (git_packbuilder_write_buf(&data, pb.get()) != 0) {
            JAMI_WARN("Couldn't write pack data for %s", repository_.c_str());
            return;
        }
    
        std::size_t sent = 0;
        std::size_t len = data.size;
        std::error_code ec;
        std::vector<uint8_t> toSendData;
        do {
            // cf https://github.com/git/git/blob/master/Documentation/technical/pack-protocol.txt#L166
            // In 'side-band-64k' mode it will send up to 65519 data bytes plus 1 control code, for a
            // total of up to 65520 bytes in a pkt-line.
            std::size_t pkt_size = std::min(static_cast<std::size_t>(65515), len - sent);
            std::string toSendHeader = toGitHex(pkt_size + 5);
            toSendData.clear();
            toSendData.reserve(pkt_size + 5);
            toSendData.insert(toSendData.end(), toSendHeader.begin(), toSendHeader.end());
            toSendData.push_back(0x1);
            toSendData.insert(toSendData.end(), data.ptr + sent, data.ptr + sent + pkt_size);
    
            socket_->write(reinterpret_cast<const unsigned char*>(toSendData.data()),
                           toSendData.size(),
                           ec);
            if (ec) {
                JAMI_WARNING("Couldn't send data for {}: {}", repository_, ec.message());
                git_buf_dispose(&data);
                return;
            }
            sent += pkt_size;
        } while (sent < len);
        git_buf_dispose(&data);
        toSendData = {};
    
        // And finish by a little FLUSH
        socket_->write(reinterpret_cast<const uint8_t*>(FLUSH_PKT.data()), FLUSH_PKT.size(), ec);
        if (ec) {
            JAMI_WARNING("Couldn't send data for {}: {}", repository_, ec.message());
        }
    
        // Clear sent data
        haveRefs_.clear();
        wantedReference_.clear();
        common_.clear();
        if (onFetchedCb_)
            onFetchedCb_(fetched);
    }
    
    std::map<std::string, std::string>
    GitServer::Impl::getParameters(std::string_view pkt_line)
    {
        std::map<std::string, std::string> parameters;
        std::string key, value;
        auto isKey = true;
        auto nullChar = 0;
        for (auto letter: pkt_line) {
            if (letter == '\0') {
                // parameters such as host or version are after the first \0
                if (nullChar != 0 && !key.empty()) {
                    parameters[std::move(key)] = std::move(value);
                }
                nullChar += 1;
                isKey = true;
                key.clear();
                value.clear();
            } else if (letter == '=') {
                isKey = false;
            } else if (nullChar != 0) {
                if (isKey) {
                    key += letter;
                } else {
                    value += letter;
                }
            }
        }
        return parameters;
    }
    
    GitServer::GitServer(const std::string& accountId,
                         const std::string& conversationId,
                         const std::shared_ptr<dhtnet::ChannelSocket>& client)
    {
        auto path = (fileutils::get_data_dir() / accountId / "conversations" / conversationId).string();
        pimpl_ = std::make_unique<GitServer::Impl>(conversationId, path, client);
    }
    
    GitServer::~GitServer()
    {
        stop();
        pimpl_.reset();
        JAMI_INFO("GitServer destroyed");
    }
    
    void
    GitServer::setOnFetched(const onFetchedCb& cb)
    {
        if (!pimpl_)
            return;
        pimpl_->onFetchedCb_ = cb;
    }
    
    void
    GitServer::stop()
    {
        pimpl_->stop();
    }
    
    } // namespace jami