Line data Source code
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 "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 : // NOTE: THIS MUST BE IN THE ROOT NAMESPACE FOR LIBGIT2
27 :
28 : /*
29 : * Create a git protocol request.
30 : *
31 : * For example: 0029git-upload-pack conversation\0host=device\0
32 : * @param buf The buffer to fill
33 : * @param cmd The wanted command
34 : * @param url The repository's URL
35 : * @return 0 on success, - 1 on error
36 : */
37 : int
38 1988 : generateRequest(git_buf* request, const std::string& cmd, std::string_view url)
39 : {
40 1988 : if (cmd.empty()) {
41 0 : giterr_set_str(GITERR_NET, "empty command");
42 0 : return -1;
43 : }
44 : // url format = deviceId/conversationId
45 1988 : auto delim = url.find('/');
46 1988 : if (delim == std::string::npos) {
47 0 : giterr_set_str(GITERR_NET, "malformed URL");
48 0 : return -1;
49 : }
50 :
51 1988 : auto deviceId = url.substr(0, delim);
52 1988 : auto conversationId = url.substr(delim, url.size());
53 :
54 1988 : constexpr auto nullSeparator = "\0"sv;
55 : auto total = 4 /* 4 bytes for the len len */
56 1988 : + cmd.size() /* followed by the command */
57 : + 1 /* space */
58 1988 : + conversationId.size() /* conversation */
59 : + 1 /* \0 */
60 1988 : + HOST_TAG.size() + deviceId.size() /* device */
61 1988 : + nullSeparator.size() /* \0 */;
62 :
63 1988 : std::string str;
64 1988 : str.reserve(total);
65 1988 : str.append("0000"sv);
66 1988 : fmt::format_to_n(str.begin(), 4, "{:04x}", total);
67 1988 : str.append(cmd)
68 1988 : .append(" "sv)
69 1988 : .append(conversationId)
70 1988 : .append(nullSeparator)
71 1988 : .append(HOST_TAG)
72 1988 : .append(deviceId)
73 1988 : .append(nullSeparator);
74 :
75 1988 : git_buf_set(request, str.data(), str.size());
76 1988 : return 0;
77 1988 : }
78 :
79 : int
80 1988 : sendCmd(P2PStream* s)
81 : {
82 1988 : auto res = 0;
83 1988 : git_buf request = {};
84 1988 : if ((res = generateRequest(&request, s->cmd, s->url)) < 0) {
85 0 : git_buf_dispose(&request);
86 0 : return res;
87 : }
88 :
89 1988 : std::error_code ec;
90 1988 : auto sock = s->socket.lock();
91 1988 : if (!sock) {
92 0 : git_buf_dispose(&request);
93 0 : return -1;
94 : }
95 1988 : if ((res = sock->write(reinterpret_cast<const unsigned char*>(request.ptr), request.size, ec))) {
96 1988 : s->sent_command = 1;
97 1988 : git_buf_dispose(&request);
98 1988 : return res;
99 : }
100 :
101 0 : s->sent_command = 1;
102 0 : git_buf_dispose(&request);
103 0 : return res;
104 1988 : }
105 :
106 : int
107 5455 : P2PStreamRead(git_smart_subtransport_stream* stream, char* buffer, size_t buflen, size_t* read)
108 : {
109 5455 : *read = 0;
110 5455 : auto* fs = reinterpret_cast<P2PStream*>(stream);
111 5455 : auto sock = fs->socket.lock();
112 5455 : if (!sock) {
113 0 : giterr_set_str(GITERR_NET, "unavailable socket");
114 0 : return -1;
115 : }
116 :
117 5455 : int res = 0;
118 : // If it's the first read, we need to send
119 : // the upload-pack command
120 5455 : if (!fs->sent_command && (res = sendCmd(fs)) < 0)
121 0 : return res;
122 :
123 5455 : std::error_code ec;
124 : // TODO ChannelSocket needs a blocking read operation
125 5455 : size_t datalen = sock->waitForData(std::chrono::milliseconds(3600 * 1000 * 24), ec);
126 5455 : if (datalen > 0)
127 5455 : *read = sock->read(reinterpret_cast<unsigned char*>(buffer), std::min(datalen, buflen), ec);
128 :
129 5455 : return res;
130 5455 : }
131 :
132 : int
133 3552 : P2PStreamWrite(git_smart_subtransport_stream* stream, const char* buffer, size_t len)
134 : {
135 3552 : auto* fs = reinterpret_cast<P2PStream*>(stream);
136 3552 : auto sock = fs->socket.lock();
137 3552 : if (!sock) {
138 1 : giterr_set_str(GITERR_NET, "unavailable socket");
139 1 : return -1;
140 : }
141 3551 : std::error_code ec;
142 3551 : sock->write(reinterpret_cast<const unsigned char*>(buffer), len, ec);
143 3551 : if (ec) {
144 0 : giterr_set_str(GITERR_NET, ec.message().c_str());
145 0 : return -1;
146 : }
147 3551 : return 0;
148 3552 : }
149 :
150 : void
151 1988 : P2PStreamFree(git_smart_subtransport_stream*)
152 1988 : {}
153 :
154 : int
155 5567 : P2PSubTransportAction(git_smart_subtransport_stream** out,
156 : git_smart_subtransport* transport,
157 : const char* url,
158 : git_smart_service_t action)
159 : {
160 5567 : auto* sub = reinterpret_cast<P2PSubTransport*>(transport);
161 5567 : if (!sub || !sub->remote) {
162 0 : JAMI_ERROR("Invalid subtransport");
163 0 : return -1;
164 : }
165 :
166 5567 : auto repo = git_remote_owner(sub->remote);
167 5567 : if (!repo) {
168 0 : JAMI_ERROR("No repository linked to the transport");
169 0 : return -1;
170 : }
171 :
172 5567 : const auto* workdir = git_repository_workdir(repo);
173 5567 : if (!workdir) {
174 0 : JAMI_ERROR("No working linked to the repository");
175 0 : return -1;
176 : }
177 5567 : std::string_view path = workdir;
178 5567 : auto delimConv = path.rfind("/conversations");
179 5567 : if (delimConv == std::string::npos) {
180 0 : JAMI_ERROR("No conversation id found");
181 0 : return -1;
182 : }
183 5567 : auto delimAccount = path.rfind('/', delimConv - 1);
184 5567 : if (delimAccount == std::string::npos && delimConv - 1 - delimAccount == 16) {
185 0 : JAMI_ERROR("No account id found");
186 0 : return -1;
187 : }
188 5567 : auto accountId = path.substr(delimAccount + 1, delimConv - 1 - delimAccount);
189 5567 : std::string_view gitUrl = url + ("git://"sv).size();
190 5567 : auto delim = gitUrl.find('/');
191 5567 : if (delim == std::string::npos) {
192 0 : JAMI_ERROR("Incorrect url {:s}", gitUrl);
193 0 : return -1;
194 : }
195 5567 : auto deviceId = gitUrl.substr(0, delim);
196 5567 : auto conversationId = gitUrl.substr(delim + 1, gitUrl.size());
197 :
198 5567 : if (action == GIT_SERVICE_UPLOADPACK_LS) {
199 2015 : auto gitSocket = jami::Manager::instance().gitSocket(accountId, deviceId, conversationId);
200 2015 : if (!gitSocket) {
201 81 : JAMI_ERROR("Unable to find related socket for {:s}, {:s}, {:s}",
202 : accountId,
203 : deviceId,
204 : conversationId);
205 27 : return -1;
206 : }
207 1988 : auto stream = std::make_unique<P2PStream>();
208 1988 : stream->socket = gitSocket;
209 1988 : stream->base.read = P2PStreamRead;
210 1988 : stream->base.write = P2PStreamWrite;
211 1988 : stream->base.free = P2PStreamFree;
212 1988 : stream->cmd = UPLOAD_PACK_CMD;
213 1988 : stream->url = gitUrl;
214 1988 : sub->stream = std::move(stream);
215 1988 : *out = &sub->stream->base;
216 1988 : return 0;
217 5567 : } else if (action == GIT_SERVICE_UPLOADPACK) {
218 3552 : if (sub->stream) {
219 3552 : *out = &sub->stream->base;
220 3552 : return 0;
221 : }
222 0 : return -1;
223 : }
224 0 : return 0;
225 : }
226 :
227 : int
228 6018 : P2PSubTransportClose(git_smart_subtransport*)
229 : {
230 6018 : return 0;
231 : }
232 :
233 : void
234 2015 : P2PSubTransportFree(git_smart_subtransport* transport)
235 : {
236 2015 : jami::Manager::instance().eraseGitTransport(transport);
237 2015 : }
238 :
239 : int
240 2015 : P2PSubTransportNew(P2PSubTransport** out, git_transport*, void* payload)
241 : {
242 2015 : auto sub = std::make_unique<P2PSubTransport>();
243 2015 : sub->remote = reinterpret_cast<git_remote*>(payload);
244 2015 : auto* base = &sub->base;
245 2015 : base->action = P2PSubTransportAction;
246 2015 : base->close = P2PSubTransportClose;
247 2015 : base->free = P2PSubTransportFree;
248 2015 : *out = sub.get();
249 2015 : jami::Manager::instance().insertGitTransport(base, std::move(sub));
250 2015 : return 0;
251 2015 : }
252 :
253 : int
254 2015 : p2p_subtransport_cb(git_smart_subtransport** out, git_transport* owner, void* payload)
255 : {
256 : P2PSubTransport* sub;
257 :
258 2015 : if (P2PSubTransportNew(&sub, owner, payload) < 0)
259 0 : return -1;
260 :
261 2015 : *out = &sub->base;
262 2015 : return 0;
263 : }
264 :
265 : int
266 2015 : p2p_transport_cb(git_transport** out, git_remote* owner, void*)
267 : {
268 2015 : git_smart_subtransport_definition def
269 : = {p2p_subtransport_cb,
270 : 0, /* Because we use an already existing channel socket, we use a permanent transport */
271 2015 : reinterpret_cast<void*>(owner)};
272 4030 : return git_transport_smart(out, owner, &def);
273 : }
|