Also support ssh.exe which needs the DETACHED_PROCESS flag
[TortoiseGit.git] / src / libgit2 / ssh-wintunnel.c
blob94738e5392524b0716c6a9b200e75a3874c235ee
1 // TortoiseGit - a Windows shell extension for easy version control
3 // Copyright (C) 2014 TortoiseGit
4 // Copyright (C) the libgit2 contributors. All rights reserved.
5 // - based on libgit2/src/transports/ssh.c
7 // This program is free software; you can redistribute it and/or
8 // modify it under the terms of the GNU General Public License
9 // as published by the Free Software Foundation; either version 2
10 // of the License, or (at your option) any later version.
12 // This program is distributed in the hope that it will be useful,
13 // but WITHOUT ANY WARRANTY; without even the implied warranty of
14 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 // GNU General Public License for more details.
17 // You should have received a copy of the GNU General Public License
18 // along with this program; if not, write to the Free Software Foundation,
19 // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
22 #include "buffer.h"
23 #include "netops.h"
24 #include "http_parser.h"
25 #include "../../ext/libgit2/src/transports/smart.h"
26 #include "system-call.h"
27 #include "ssh-wintunnel.h"
29 #define OWNING_SUBTRANSPORT(s) ((ssh_subtransport *)(s)->parent.subtransport)
31 static const char prefix_ssh[] = "ssh://";
32 static const char cmd_uploadpack[] = "git-upload-pack";
33 static const char cmd_receivepack[] = "git-receive-pack";
35 typedef struct {
36 git_smart_subtransport_stream parent;
37 COMMAND_HANDLE commandHandle;
38 const char *cmd;
39 char *url;
40 } ssh_stream;
42 typedef struct {
43 git_smart_subtransport parent;
44 transport_smart *owner;
45 ssh_stream *current_stream;
46 char *cmd_uploadpack;
47 char *cmd_receivepack;
48 LPWSTR pEnv;
49 LPWSTR sshtoolpath;
50 } ssh_subtransport;
53 * Create a git protocol request.
55 * For example: git-upload-pack '/libgit2/libgit2'
57 static int gen_proto(git_buf *request, const char *cmd, const char *url)
59 const char *repo;
61 if (!git__prefixcmp(url, prefix_ssh)) {
62 url = url + strlen(prefix_ssh);
63 repo = strchr(url, '/');
64 } else {
65 repo = strchr(url, ':');
66 if (repo) repo++;
69 if (!repo) {
70 giterr_set(GITERR_NET, "Malformed git protocol URL");
71 return -1;
74 git_buf_printf(request, "\"%s '%s'\"", cmd, repo);
76 if (git_buf_oom(request)) {
77 giterr_set_oom();
78 return -1;
81 return 0;
84 static int ssh_stream_read(
85 git_smart_subtransport_stream *stream,
86 char *buffer,
87 size_t buf_size,
88 size_t *bytes_read)
90 ssh_stream *s = (ssh_stream *)stream;
92 if (command_read_stdout(&s->commandHandle, buffer, buf_size, bytes_read))
93 return -1;
95 return 0;
98 static int ssh_stream_write(
99 git_smart_subtransport_stream *stream,
100 const char *buffer,
101 size_t len)
103 ssh_stream *s = (ssh_stream *)stream;
105 if (command_write(&s->commandHandle, buffer, len))
106 return -1;
108 return 0;
111 static void ssh_stream_free(git_smart_subtransport_stream *stream)
113 ssh_stream *s = (ssh_stream *)stream;
114 ssh_subtransport *t = OWNING_SUBTRANSPORT(s);
116 DWORD exitcode = command_close(&s->commandHandle);
117 if (s->commandHandle.errBuf) {
118 if (exitcode && exitcode != MAXDWORD) {
119 if (!git_buf_oom(s->commandHandle.errBuf) && git_buf_len(s->commandHandle.errBuf))
120 giterr_set(GITERR_SSH, "Command exited non-zero (%ld) and returned:\n%s", exitcode, s->commandHandle.errBuf->ptr);
121 else
122 giterr_set(GITERR_SSH, "Command exited non-zero: %ld", exitcode);
124 git_buf_free(s->commandHandle.errBuf);
125 git__free(s->commandHandle.errBuf);
128 t->current_stream = NULL;
130 git__free(s->url);
131 git__free(s);
134 static int ssh_stream_alloc(
135 ssh_subtransport *t,
136 const char *url,
137 const char *cmd,
138 git_smart_subtransport_stream **stream)
140 ssh_stream *s;
141 git_buf *errBuf;
143 assert(stream);
145 s = git__calloc(sizeof(ssh_stream), 1);
146 if (!s) {
147 giterr_set_oom();
148 return -1;
151 errBuf = git__calloc(sizeof(git_buf), 1);
152 if (!errBuf) {
153 git__free(s);
154 giterr_set_oom();
155 return -1;
158 s->parent.subtransport = &t->parent;
159 s->parent.read = ssh_stream_read;
160 s->parent.write = ssh_stream_write;
161 s->parent.free = ssh_stream_free;
163 s->cmd = cmd;
165 s->url = git__strdup(url);
166 if (!s->url) {
167 git__free(s);
168 giterr_set_oom();
169 return -1;
172 command_init(&s->commandHandle);
173 s->commandHandle.errBuf = errBuf;
175 *stream = &s->parent;
176 return 0;
179 static int git_ssh_extract_url_parts(
180 char **host,
181 char **username,
182 const char *url)
184 const char *colon, *at;
185 const char *start;
187 colon = strchr(url, ':');
189 at = strchr(url, '@');
190 if (at) {
191 start = at + 1;
192 *username = git__substrdup(url, at - url);
193 if (!*username) {
194 giterr_set_oom();
195 return -1;
197 } else {
198 start = url;
199 *username = NULL;
202 if (colon == NULL || (colon < start)) {
203 giterr_set(GITERR_NET, "Malformed URL");
204 return -1;
207 *host = git__substrdup(start, colon - start);
208 if (!*host) {
209 giterr_set_oom();
210 return -1;
213 return 0;
216 static int wcstristr(const wchar_t *heystack, const wchar_t *needle)
218 const wchar_t *end;
219 size_t lenNeedle = wcslen(needle);
220 size_t lenHeystack = wcslen(heystack);
221 if (lenNeedle > lenHeystack)
222 return 0;
224 end = heystack + lenHeystack - lenNeedle;
225 while (heystack <= end) {
226 if (!wcsnicmp(heystack, needle, lenNeedle))
227 return 1;
228 ++heystack;
231 return 0;
234 /* based on netops.c code of libgit2, needed by extract_url_parts, see comment there */
235 #define hex2c(c) ((c | 32) % 39 - 9)
236 static char* unescape(char *str)
238 int x, y;
239 int len = (int)strlen(str);
241 for (x = y = 0; str[y]; ++x, ++y) {
242 if ((str[x] = str[y]) == '%') {
243 if (y < len - 2 && isxdigit(str[y + 1]) && isxdigit(str[y + 2])) {
244 str[x] = (hex2c(str[y + 1]) << 4) + hex2c(str[y + 2]);
245 y += 2;
249 str[x] = '\0';
250 return str;
253 /* based on gitno_extract_url_parts, keep c&p copy here until https://github.com/libgit2/libgit2/pull/2492 gets merged or forever ;)*/
254 static int extract_url_parts(
255 char **host,
256 char **port,
257 char **path,
258 char **username,
259 char **password,
260 const char *url,
261 const char *default_port)
263 struct http_parser_url u = { 0 };
264 const char *_host, *_port, *_path, *_userinfo;
266 if (http_parser_parse_url(url, strlen(url), false, &u)) {
267 giterr_set(GITERR_NET, "Malformed URL '%s'", url);
268 return GIT_EINVALIDSPEC;
271 _host = url + u.field_data[UF_HOST].off;
272 _port = url + u.field_data[UF_PORT].off;
273 _path = url + u.field_data[UF_PATH].off;
274 _userinfo = url + u.field_data[UF_USERINFO].off;
276 if (u.field_set & (1 << UF_HOST)) {
277 *host = git__substrdup(_host, u.field_data[UF_HOST].len);
278 GITERR_CHECK_ALLOC(*host);
281 if (u.field_set & (1 << UF_PORT)) {
282 *port = git__substrdup(_port, u.field_data[UF_PORT].len);
283 } else if (default_port) {
284 *port = git__strdup(default_port);
285 GITERR_CHECK_ALLOC(*port);
286 } else {
287 *port = NULL;
290 if (u.field_set & (1 << UF_PATH)) {
291 *path = git__substrdup(_path, u.field_data[UF_PATH].len);
292 GITERR_CHECK_ALLOC(*path);
293 } else {
294 giterr_set(GITERR_NET, "invalid url, missing path");
295 return GIT_EINVALIDSPEC;
298 if (u.field_set & (1 << UF_USERINFO)) {
299 const char *colon = memchr(_userinfo, ':', u.field_data[UF_USERINFO].len);
300 if (colon) {
301 *username = unescape(git__substrdup(_userinfo, colon - _userinfo));
302 *password = unescape(git__substrdup(colon + 1, u.field_data[UF_USERINFO].len - (colon + 1 - _userinfo)));
303 GITERR_CHECK_ALLOC(*password);
304 } else {
305 *username = git__substrdup(_userinfo, u.field_data[UF_USERINFO].len);
307 GITERR_CHECK_ALLOC(*username);
311 return 0;
314 static int _git_ssh_setup_tunnel(
315 ssh_subtransport *t,
316 const char *url,
317 const char *gitCmd,
318 git_smart_subtransport_stream **stream)
320 char *host = NULL, *port = NULL, *path = NULL, *user = NULL, *pass = NULL;
321 ssh_stream *s;
322 wchar_t *ssh = t->sshtoolpath;
323 wchar_t *wideParams = NULL;
324 wchar_t *cmd = NULL;
325 git_buf params = GIT_BUF_INIT;
326 int isPutty;
327 size_t length;
329 *stream = NULL;
330 if (ssh_stream_alloc(t, url, gitCmd, stream) < 0) {
331 giterr_set_oom();
332 return -1;
335 s = (ssh_stream *)*stream;
337 if (!git__prefixcmp(url, prefix_ssh)) {
338 if (extract_url_parts(&host, &port, &path, &user, &pass, url, NULL) < 0)
339 goto on_error;
340 } else {
341 if (git_ssh_extract_url_parts(&host, &user, url) < 0)
342 goto on_error;
345 if (!ssh)
347 giterr_set(GITERR_SSH, "No GIT_SSH tool configured");
348 goto on_error;
351 isPutty = wcstristr(ssh, L"plink");
352 if (port) {
353 if (isPutty)
354 git_buf_printf(&params, " -P %s", port);
355 else
356 git_buf_printf(&params, " -p %s", port);
358 if (isPutty && !wcstristr(ssh, L"tortoiseplink")) {
359 git_buf_puts(&params, " -batch");
361 if (user)
362 git_buf_printf(&params, " %s@%s ", user, host);
363 else
364 git_buf_printf(&params, " %s ", host);
365 if (gen_proto(&params, s->cmd, s->url))
366 goto on_error;
367 if (git_buf_oom(&params)) {
368 giterr_set_oom();
369 goto on_error;
372 if (git__utf8_to_16_alloc(&wideParams, params.ptr) < 0) {
373 giterr_set_oom();
374 goto on_error;
376 git_buf_free(&params);
378 length = wcslen(ssh) + wcslen(wideParams) + 3;
379 cmd = git__calloc(length, sizeof(wchar_t));
380 if (!cmd) {
381 giterr_set_oom();
382 goto on_error;
385 wcscat_s(cmd, length, L"\"");
386 wcscat_s(cmd, length, ssh);
387 wcscat_s(cmd, length, L"\"");
388 wcscat_s(cmd, length, wideParams);
390 if (command_start(cmd, &s->commandHandle, t->pEnv, isPutty ? CREATE_NEW_CONSOLE : DETACHED_PROCESS))
391 goto on_error;
393 git__free(wideParams);
394 git__free(cmd);
395 t->current_stream = s;
396 git__free(host);
397 git__free(port);
398 git__free(path);
399 git__free(user);
400 git__free(pass);
402 return 0;
404 on_error:
405 t->current_stream = NULL;
407 if (*stream)
408 ssh_stream_free(*stream);
410 git_buf_free(&params);
412 if (wideParams)
413 git__free(wideParams);
415 if (cmd)
416 git__free(cmd);
418 git__free(host);
419 git__free(port);
420 git__free(user);
421 git__free(pass);
423 return -1;
426 static int ssh_uploadpack_ls(
427 ssh_subtransport *t,
428 const char *url,
429 git_smart_subtransport_stream **stream)
431 const char *cmd = t->cmd_uploadpack ? t->cmd_uploadpack : cmd_uploadpack;
433 if (_git_ssh_setup_tunnel(t, url, cmd, stream) < 0)
434 return -1;
436 return 0;
439 static int ssh_uploadpack(
440 ssh_subtransport *t,
441 const char *url,
442 git_smart_subtransport_stream **stream)
444 GIT_UNUSED(url);
446 if (t->current_stream) {
447 *stream = &t->current_stream->parent;
448 return 0;
451 giterr_set(GITERR_NET, "Must call UPLOADPACK_LS before UPLOADPACK");
452 return -1;
455 static int ssh_receivepack_ls(
456 ssh_subtransport *t,
457 const char *url,
458 git_smart_subtransport_stream **stream)
460 const char *cmd = t->cmd_receivepack ? t->cmd_receivepack : cmd_receivepack;
462 if (_git_ssh_setup_tunnel(t, url, cmd, stream) < 0)
463 return -1;
465 return 0;
468 static int ssh_receivepack(
469 ssh_subtransport *t,
470 const char *url,
471 git_smart_subtransport_stream **stream)
473 GIT_UNUSED(url);
475 if (t->current_stream) {
476 *stream = &t->current_stream->parent;
477 return 0;
480 giterr_set(GITERR_NET, "Must call RECEIVEPACK_LS before RECEIVEPACK");
481 return -1;
484 static int _ssh_action(
485 git_smart_subtransport_stream **stream,
486 git_smart_subtransport *subtransport,
487 const char *url,
488 git_smart_service_t action)
490 ssh_subtransport *t = (ssh_subtransport *) subtransport;
492 switch (action) {
493 case GIT_SERVICE_UPLOADPACK_LS:
494 return ssh_uploadpack_ls(t, url, stream);
496 case GIT_SERVICE_UPLOADPACK:
497 return ssh_uploadpack(t, url, stream);
499 case GIT_SERVICE_RECEIVEPACK_LS:
500 return ssh_receivepack_ls(t, url, stream);
502 case GIT_SERVICE_RECEIVEPACK:
503 return ssh_receivepack(t, url, stream);
506 *stream = NULL;
507 return -1;
510 static int _ssh_close(git_smart_subtransport *subtransport)
512 ssh_subtransport *t = (ssh_subtransport *) subtransport;
514 assert(!t->current_stream);
516 return 0;
519 static void _ssh_free(git_smart_subtransport *subtransport)
521 ssh_subtransport *t = (ssh_subtransport *) subtransport;
523 assert(!t->current_stream);
525 git__free(t->cmd_uploadpack);
526 git__free(t->cmd_receivepack);
527 git__free(t->sshtoolpath);
528 git__free(t);
531 int git_smart_subtransport_ssh_wintunnel(
532 git_smart_subtransport **out, git_transport *owner, LPCWSTR sshtoolpath, LPWSTR pEnv)
534 ssh_subtransport *t;
536 assert(out);
538 t = git__calloc(sizeof(ssh_subtransport), 1);
539 if (!t) {
540 giterr_set_oom();
541 return -1;
544 t->sshtoolpath = wcsdup(sshtoolpath);
545 t->pEnv = pEnv;
547 t->owner = (transport_smart *)owner;
548 t->parent.action = _ssh_action;
549 t->parent.close = _ssh_close;
550 t->parent.free = _ssh_free;
552 *out = (git_smart_subtransport *) t;
553 return 0;