]>
Commit | Line | Data |
---|---|---|
297758dc BM |
1 | /* |
2 | * Copyright (C) the libgit2 contributors. All rights reserved. | |
3 | * | |
4 | * This file is part of libgit2, distributed under the GNU GPL v2 with | |
5 | * a Linking Exception. For full terms see the included COPYING file. | |
6 | */ | |
7 | ||
eae0bfdc PP |
8 | #include "ssh.h" |
9 | ||
ae241ae1 JG |
10 | #ifdef GIT_SSH |
11 | #include <libssh2.h> | |
12 | #endif | |
13 | ||
2ce2a48f | 14 | #include "global.h" |
297758dc BM |
15 | #include "git2.h" |
16 | #include "buffer.h" | |
22a2d3d5 | 17 | #include "net.h" |
297758dc | 18 | #include "netops.h" |
f7158cd7 | 19 | #include "smart.h" |
eae0bfdc | 20 | #include "streams/socket.h" |
297758dc | 21 | |
22a2d3d5 UG |
22 | #include "git2/credential.h" |
23 | #include "git2/sys/credential.h" | |
24 | ||
a3c062db RB |
25 | #ifdef GIT_SSH |
26 | ||
297758dc BM |
27 | #define OWNING_SUBTRANSPORT(s) ((ssh_subtransport *)(s)->parent.subtransport) |
28 | ||
4df17045 | 29 | static const char *ssh_prefixes[] = { "ssh://", "ssh+git://", "git+ssh://" }; |
ed21fd74 | 30 | |
297758dc BM |
31 | static const char cmd_uploadpack[] = "git-upload-pack"; |
32 | static const char cmd_receivepack[] = "git-receive-pack"; | |
33 | ||
34 | typedef struct { | |
35 | git_smart_subtransport_stream parent; | |
4fd4341f | 36 | git_stream *io; |
f7158cd7 BM |
37 | LIBSSH2_SESSION *session; |
38 | LIBSSH2_CHANNEL *channel; | |
297758dc BM |
39 | const char *cmd; |
40 | char *url; | |
41 | unsigned sent_command : 1; | |
42 | } ssh_stream; | |
43 | ||
44 | typedef struct { | |
45 | git_smart_subtransport parent; | |
f7158cd7 | 46 | transport_smart *owner; |
8ae55d94 | 47 | ssh_stream *current_stream; |
22a2d3d5 | 48 | git_credential *cred; |
d4256ed5 CMN |
49 | char *cmd_uploadpack; |
50 | char *cmd_receivepack; | |
297758dc BM |
51 | } ssh_subtransport; |
52 | ||
ccb85c8f | 53 | static int list_auth_methods(int *out, LIBSSH2_SESSION *session, const char *username); |
22618906 | 54 | |
b622aabe ES |
55 | static void ssh_error(LIBSSH2_SESSION *session, const char *errmsg) |
56 | { | |
57 | char *ssherr; | |
58 | libssh2_session_last_error(session, &ssherr, NULL, 0); | |
59 | ||
ac3d33df | 60 | git_error_set(GIT_ERROR_SSH, "%s: %s", errmsg, ssherr); |
b622aabe ES |
61 | } |
62 | ||
297758dc BM |
63 | /* |
64 | * Create a git protocol request. | |
65 | * | |
58ba0a4e | 66 | * For example: git-upload-pack '/libgit2/libgit2' |
297758dc | 67 | */ |
7261d983 | 68 | static int gen_proto(git_buf *request, const char *cmd, const char *url) |
297758dc | 69 | { |
eae0bfdc | 70 | const char *repo; |
300f4412 | 71 | int len; |
4df17045 | 72 | size_t i; |
a3c062db | 73 | |
4df17045 | 74 | for (i = 0; i < ARRAY_SIZE(ssh_prefixes); ++i) { |
ed21fd74 CB |
75 | const char *p = ssh_prefixes[i]; |
76 | ||
77 | if (!git__prefixcmp(url, p)) { | |
78 | url = url + strlen(p); | |
79 | repo = strchr(url, '/'); | |
80 | if (repo && repo[1] == '~') | |
81 | ++repo; | |
82 | ||
83 | goto done; | |
84 | } | |
7261d983 | 85 | } |
ed21fd74 CB |
86 | repo = strchr(url, ':'); |
87 | if (repo) repo++; | |
a3c062db | 88 | |
ed21fd74 | 89 | done: |
7261d983 | 90 | if (!repo) { |
ac3d33df | 91 | git_error_set(GIT_ERROR_NET, "malformed git protocol URL"); |
7261d983 BM |
92 | return -1; |
93 | } | |
a3c062db | 94 | |
300f4412 | 95 | len = strlen(cmd) + 1 /* Space */ + 1 /* Quote */ + strlen(repo) + 1 /* Quote */ + 1; |
a3c062db | 96 | |
297758dc | 97 | git_buf_grow(request, len); |
eae0bfdc PP |
98 | git_buf_puts(request, cmd); |
99 | git_buf_puts(request, " '"); | |
100 | git_buf_decode_percent(request, repo, strlen(repo)); | |
101 | git_buf_puts(request, "'"); | |
a3c062db | 102 | |
297758dc BM |
103 | if (git_buf_oom(request)) |
104 | return -1; | |
a3c062db | 105 | |
297758dc BM |
106 | return 0; |
107 | } | |
108 | ||
8ae55d94 | 109 | static int send_command(ssh_stream *s) |
297758dc BM |
110 | { |
111 | int error; | |
112 | git_buf request = GIT_BUF_INIT; | |
a3c062db | 113 | |
7261d983 | 114 | error = gen_proto(&request, s->cmd, s->url); |
297758dc BM |
115 | if (error < 0) |
116 | goto cleanup; | |
a3c062db | 117 | |
c2de6b1a | 118 | error = libssh2_channel_exec(s->channel, request.ptr); |
b622aabe ES |
119 | if (error < LIBSSH2_ERROR_NONE) { |
120 | ssh_error(s->session, "SSH could not execute request"); | |
f7158cd7 | 121 | goto cleanup; |
c2de6b1a | 122 | } |
a3c062db | 123 | |
f7158cd7 | 124 | s->sent_command = 1; |
a3c062db | 125 | |
297758dc | 126 | cleanup: |
ac3d33df | 127 | git_buf_dispose(&request); |
297758dc BM |
128 | return error; |
129 | } | |
130 | ||
8ae55d94 BM |
131 | static int ssh_stream_read( |
132 | git_smart_subtransport_stream *stream, | |
133 | char *buffer, | |
134 | size_t buf_size, | |
135 | size_t *bytes_read) | |
297758dc | 136 | { |
c2de6b1a | 137 | int rc; |
22a2d3d5 | 138 | ssh_stream *s = GIT_CONTAINER_OF(stream, ssh_stream, parent); |
a3c062db | 139 | |
297758dc | 140 | *bytes_read = 0; |
a3c062db | 141 | |
297758dc BM |
142 | if (!s->sent_command && send_command(s) < 0) |
143 | return -1; | |
a3c062db | 144 | |
b622aabe | 145 | if ((rc = libssh2_channel_read(s->channel, buffer, buf_size)) < LIBSSH2_ERROR_NONE) { |
e3435673 | 146 | ssh_error(s->session, "SSH could not read data"); |
297758dc | 147 | return -1; |
c2de6b1a | 148 | } |
a3c062db | 149 | |
1396c381 CMN |
150 | /* |
151 | * If we can't get anything out of stdout, it's typically a | |
152 | * not-found error, so read from stderr and signal EOF on | |
153 | * stderr. | |
154 | */ | |
02fdc2db MG |
155 | if (rc == 0) { |
156 | if ((rc = libssh2_channel_read_stderr(s->channel, buffer, buf_size)) > 0) { | |
ac3d33df | 157 | git_error_set(GIT_ERROR_SSH, "%*s", rc, buffer); |
02fdc2db MG |
158 | return GIT_EEOF; |
159 | } else if (rc < LIBSSH2_ERROR_NONE) { | |
160 | ssh_error(s->session, "SSH could not read stderr"); | |
161 | return -1; | |
162 | } | |
e3435673 CMN |
163 | } |
164 | ||
165 | ||
f7158cd7 | 166 | *bytes_read = rc; |
a3c062db | 167 | |
297758dc BM |
168 | return 0; |
169 | } | |
170 | ||
8ae55d94 BM |
171 | static int ssh_stream_write( |
172 | git_smart_subtransport_stream *stream, | |
173 | const char *buffer, | |
174 | size_t len) | |
297758dc | 175 | { |
22a2d3d5 | 176 | ssh_stream *s = GIT_CONTAINER_OF(stream, ssh_stream, parent); |
0963716b CMN |
177 | size_t off = 0; |
178 | ssize_t ret = 0; | |
a3c062db | 179 | |
297758dc BM |
180 | if (!s->sent_command && send_command(s) < 0) |
181 | return -1; | |
a3c062db | 182 | |
0963716b CMN |
183 | do { |
184 | ret = libssh2_channel_write(s->channel, buffer + off, len - off); | |
185 | if (ret < 0) | |
186 | break; | |
187 | ||
188 | off += ret; | |
189 | ||
190 | } while (off < len); | |
191 | ||
192 | if (ret < 0) { | |
b622aabe | 193 | ssh_error(s->session, "SSH could not write data"); |
f7158cd7 BM |
194 | return -1; |
195 | } | |
a3c062db | 196 | |
c2de6b1a | 197 | return 0; |
297758dc BM |
198 | } |
199 | ||
8ae55d94 | 200 | static void ssh_stream_free(git_smart_subtransport_stream *stream) |
297758dc | 201 | { |
22a2d3d5 | 202 | ssh_stream *s = GIT_CONTAINER_OF(stream, ssh_stream, parent); |
81be2f46 | 203 | ssh_subtransport *t; |
a3c062db | 204 | |
81be2f46 CMN |
205 | if (!stream) |
206 | return; | |
a3c062db | 207 | |
81be2f46 | 208 | t = OWNING_SUBTRANSPORT(s); |
297758dc | 209 | t->current_stream = NULL; |
a3c062db | 210 | |
22595b84 BM |
211 | if (s->channel) { |
212 | libssh2_channel_close(s->channel); | |
68bc49a1 RB |
213 | libssh2_channel_free(s->channel); |
214 | s->channel = NULL; | |
22595b84 | 215 | } |
a3c062db | 216 | |
22595b84 | 217 | if (s->session) { |
4b3ec53c | 218 | libssh2_session_disconnect(s->session, "closing transport"); |
c2de6b1a RB |
219 | libssh2_session_free(s->session); |
220 | s->session = NULL; | |
22595b84 | 221 | } |
a3c062db | 222 | |
4fd4341f CMN |
223 | if (s->io) { |
224 | git_stream_close(s->io); | |
225 | git_stream_free(s->io); | |
226 | s->io = NULL; | |
297758dc | 227 | } |
a3c062db | 228 | |
297758dc BM |
229 | git__free(s->url); |
230 | git__free(s); | |
231 | } | |
232 | ||
8ae55d94 BM |
233 | static int ssh_stream_alloc( |
234 | ssh_subtransport *t, | |
235 | const char *url, | |
236 | const char *cmd, | |
237 | git_smart_subtransport_stream **stream) | |
297758dc | 238 | { |
8ae55d94 | 239 | ssh_stream *s; |
a3c062db | 240 | |
c2de6b1a | 241 | assert(stream); |
a3c062db | 242 | |
8ae55d94 | 243 | s = git__calloc(sizeof(ssh_stream), 1); |
ac3d33df | 244 | GIT_ERROR_CHECK_ALLOC(s); |
a3c062db | 245 | |
297758dc | 246 | s->parent.subtransport = &t->parent; |
8ae55d94 BM |
247 | s->parent.read = ssh_stream_read; |
248 | s->parent.write = ssh_stream_write; | |
249 | s->parent.free = ssh_stream_free; | |
a3c062db | 250 | |
297758dc | 251 | s->cmd = cmd; |
a3c062db | 252 | |
c2de6b1a | 253 | s->url = git__strdup(url); |
297758dc BM |
254 | if (!s->url) { |
255 | git__free(s); | |
256 | return -1; | |
257 | } | |
a3c062db | 258 | |
297758dc BM |
259 | *stream = &s->parent; |
260 | return 0; | |
261 | } | |
262 | ||
7261d983 | 263 | static int git_ssh_extract_url_parts( |
22a2d3d5 | 264 | git_net_url *urldata, |
d04c3840 BM |
265 | const char *url) |
266 | { | |
267 | char *colon, *at; | |
268 | const char *start; | |
a3c062db | 269 | |
68bc49a1 | 270 | colon = strchr(url, ':'); |
a3c062db | 271 | |
a3c062db | 272 | |
7261d983 | 273 | at = strchr(url, '@'); |
d04c3840 | 274 | if (at) { |
eec4dcc3 | 275 | start = at + 1; |
22a2d3d5 UG |
276 | urldata->username = git__substrdup(url, at - url); |
277 | GIT_ERROR_CHECK_ALLOC(urldata->username); | |
d04c3840 | 278 | } else { |
c87bf86c | 279 | start = url; |
22a2d3d5 | 280 | urldata->username = NULL; |
d04c3840 | 281 | } |
a3c062db | 282 | |
048f837b | 283 | if (colon == NULL || (colon < start)) { |
ac3d33df | 284 | git_error_set(GIT_ERROR_NET, "malformed URL"); |
048f837b BS |
285 | return -1; |
286 | } | |
287 | ||
22a2d3d5 UG |
288 | urldata->host = git__substrdup(start, colon - start); |
289 | GIT_ERROR_CHECK_ALLOC(urldata->host); | |
a3c062db | 290 | |
d04c3840 BM |
291 | return 0; |
292 | } | |
293 | ||
22a2d3d5 | 294 | static int ssh_agent_auth(LIBSSH2_SESSION *session, git_credential_ssh_key *c) { |
ee7040fd AG |
295 | int rc = LIBSSH2_ERROR_NONE; |
296 | ||
297 | struct libssh2_agent_publickey *curr, *prev = NULL; | |
298 | ||
299 | LIBSSH2_AGENT *agent = libssh2_agent_init(session); | |
300 | ||
301 | if (agent == NULL) | |
302 | return -1; | |
303 | ||
304 | rc = libssh2_agent_connect(agent); | |
305 | ||
306 | if (rc != LIBSSH2_ERROR_NONE) | |
307 | goto shutdown; | |
308 | ||
309 | rc = libssh2_agent_list_identities(agent); | |
310 | ||
311 | if (rc != LIBSSH2_ERROR_NONE) | |
312 | goto shutdown; | |
313 | ||
314 | while (1) { | |
315 | rc = libssh2_agent_get_identity(agent, &curr, prev); | |
316 | ||
317 | if (rc < 0) | |
318 | goto shutdown; | |
319 | ||
d71e3b25 MU |
320 | /* rc is set to 1 whenever the ssh agent ran out of keys to check. |
321 | * Set the error code to authentication failure rather than erroring | |
322 | * out with an untranslatable error code. | |
323 | */ | |
324 | if (rc == 1) { | |
325 | rc = LIBSSH2_ERROR_AUTHENTICATION_FAILED; | |
ee7040fd | 326 | goto shutdown; |
d71e3b25 | 327 | } |
ee7040fd AG |
328 | |
329 | rc = libssh2_agent_userauth(agent, c->username, curr); | |
330 | ||
331 | if (rc == 0) | |
332 | break; | |
333 | ||
334 | prev = curr; | |
335 | } | |
336 | ||
337 | shutdown: | |
2dc399a8 CMN |
338 | |
339 | if (rc != LIBSSH2_ERROR_NONE) | |
340 | ssh_error(session, "error authenticating"); | |
341 | ||
ee7040fd AG |
342 | libssh2_agent_disconnect(agent); |
343 | libssh2_agent_free(agent); | |
344 | ||
345 | return rc; | |
346 | } | |
347 | ||
c0cef9e0 | 348 | static int _git_ssh_authenticate_session( |
22a2d3d5 UG |
349 | LIBSSH2_SESSION *session, |
350 | git_credential *cred) | |
c0cef9e0 BM |
351 | { |
352 | int rc; | |
c2de6b1a | 353 | |
c0cef9e0 | 354 | do { |
ac3d33df | 355 | git_error_clear(); |
c0cef9e0 | 356 | switch (cred->credtype) { |
22a2d3d5 UG |
357 | case GIT_CREDENTIAL_USERPASS_PLAINTEXT: { |
358 | git_credential_userpass_plaintext *c = (git_credential_userpass_plaintext *)cred; | |
bd270b70 | 359 | rc = libssh2_userauth_password(session, c->username, c->password); |
c2de6b1a RB |
360 | break; |
361 | } | |
22a2d3d5 UG |
362 | case GIT_CREDENTIAL_SSH_KEY: { |
363 | git_credential_ssh_key *c = (git_credential_ssh_key *)cred; | |
ee7040fd AG |
364 | |
365 | if (c->privatekey) | |
366 | rc = libssh2_userauth_publickey_fromfile( | |
367 | session, c->username, c->publickey, | |
368 | c->privatekey, c->passphrase); | |
369 | else | |
370 | rc = ssh_agent_auth(session, c); | |
371 | ||
c2de6b1a RB |
372 | break; |
373 | } | |
22a2d3d5 UG |
374 | case GIT_CREDENTIAL_SSH_CUSTOM: { |
375 | git_credential_ssh_custom *c = (git_credential_ssh_custom *)cred; | |
7affc2f7 | 376 | |
c2de6b1a | 377 | rc = libssh2_userauth_publickey( |
7affc2f7 | 378 | session, c->username, (const unsigned char *)c->publickey, |
8ec0a552 | 379 | c->publickey_len, c->sign_callback, &c->payload); |
c2de6b1a RB |
380 | break; |
381 | } | |
22a2d3d5 | 382 | case GIT_CREDENTIAL_SSH_INTERACTIVE: { |
478408c0 | 383 | void **abstract = libssh2_session_abstract(session); |
22a2d3d5 | 384 | git_credential_ssh_interactive *c = (git_credential_ssh_interactive *)cred; |
478408c0 JG |
385 | |
386 | /* ideally, we should be able to set this by calling | |
387 | * libssh2_session_init_ex() instead of libssh2_session_init(). | |
388 | * libssh2's API is inconsistent here i.e. libssh2_userauth_publickey() | |
389 | * allows you to pass the `abstract` as part of the call, whereas | |
390 | * libssh2_userauth_keyboard_interactive() does not! | |
391 | * | |
392 | * The only way to set the `abstract` pointer is by calling | |
393 | * libssh2_session_abstract(), which will replace the existing | |
394 | * pointer as is done below. This is safe for now (at time of writing), | |
395 | * but may not be valid in future. | |
396 | */ | |
397 | *abstract = c->payload; | |
398 | ||
399 | rc = libssh2_userauth_keyboard_interactive( | |
400 | session, c->username, c->prompt_callback); | |
401 | break; | |
402 | } | |
7a8b8503 | 403 | #ifdef GIT_SSH_MEMORY_CREDENTIALS |
22a2d3d5 UG |
404 | case GIT_CREDENTIAL_SSH_MEMORY: { |
405 | git_credential_ssh_key *c = (git_credential_ssh_key *)cred; | |
7a8b8503 | 406 | |
2629fc87 MG |
407 | assert(c->username); |
408 | assert(c->privatekey); | |
409 | ||
7a8b8503 DC |
410 | rc = libssh2_userauth_publickey_frommemory( |
411 | session, | |
412 | c->username, | |
413 | strlen(c->username), | |
414 | c->publickey, | |
2629fc87 | 415 | c->publickey ? strlen(c->publickey) : 0, |
7a8b8503 DC |
416 | c->privatekey, |
417 | strlen(c->privatekey), | |
418 | c->passphrase); | |
419 | break; | |
420 | } | |
421 | #endif | |
c2de6b1a RB |
422 | default: |
423 | rc = LIBSSH2_ERROR_AUTHENTICATION_FAILED; | |
c0cef9e0 | 424 | } |
08bf80fa | 425 | } while (LIBSSH2_ERROR_EAGAIN == rc || LIBSSH2_ERROR_TIMEOUT == rc); |
a3c062db | 426 | |
eae0bfdc PP |
427 | if (rc == LIBSSH2_ERROR_PASSWORD_EXPIRED || |
428 | rc == LIBSSH2_ERROR_AUTHENTICATION_FAILED || | |
429 | rc == LIBSSH2_ERROR_PUBLICKEY_UNVERIFIED) | |
430 | return GIT_EAUTH; | |
d7f962f4 | 431 | |
b622aabe | 432 | if (rc != LIBSSH2_ERROR_NONE) { |
ac3d33df | 433 | if (!git_error_last()) |
2dc399a8 | 434 | ssh_error(session, "Failed to authenticate SSH session"); |
c2de6b1a RB |
435 | return -1; |
436 | } | |
437 | ||
438 | return 0; | |
c0cef9e0 BM |
439 | } |
440 | ||
22a2d3d5 | 441 | static int request_creds(git_credential **out, ssh_subtransport *t, const char *user, int auth_methods) |
d7f962f4 CMN |
442 | { |
443 | int error, no_callback = 0; | |
22a2d3d5 | 444 | git_credential *cred = NULL; |
d7f962f4 CMN |
445 | |
446 | if (!t->owner->cred_acquire_cb) { | |
447 | no_callback = 1; | |
448 | } else { | |
449 | error = t->owner->cred_acquire_cb(&cred, t->owner->url, user, auth_methods, | |
450 | t->owner->cred_acquire_payload); | |
451 | ||
ac3d33df | 452 | if (error == GIT_PASSTHROUGH) { |
d7f962f4 | 453 | no_callback = 1; |
ac3d33df | 454 | } else if (error < 0) { |
d7f962f4 | 455 | return error; |
ac3d33df JK |
456 | } else if (!cred) { |
457 | git_error_set(GIT_ERROR_SSH, "callback failed to initialize SSH credentials"); | |
d7f962f4 CMN |
458 | return -1; |
459 | } | |
460 | } | |
461 | ||
462 | if (no_callback) { | |
ac3d33df | 463 | git_error_set(GIT_ERROR_SSH, "authentication required but no callback set"); |
d7f962f4 CMN |
464 | return -1; |
465 | } | |
466 | ||
ccb85c8f CMN |
467 | if (!(cred->credtype & auth_methods)) { |
468 | cred->free(cred); | |
ac3d33df | 469 | git_error_set(GIT_ERROR_SSH, "callback returned unsupported credentials type"); |
ccb85c8f CMN |
470 | return -1; |
471 | } | |
472 | ||
d7f962f4 CMN |
473 | *out = cred; |
474 | ||
475 | return 0; | |
476 | } | |
477 | ||
08bf80fa | 478 | static int _git_ssh_session_create( |
3eed595e | 479 | LIBSSH2_SESSION** session, |
4fd4341f | 480 | git_stream *io) |
3eed595e | 481 | { |
c2de6b1a RB |
482 | int rc = 0; |
483 | LIBSSH2_SESSION* s; | |
22a2d3d5 | 484 | git_socket_stream *socket = GIT_CONTAINER_OF(io, git_socket_stream, parent); |
a3c062db | 485 | |
c2de6b1a RB |
486 | assert(session); |
487 | ||
488 | s = libssh2_session_init(); | |
489 | if (!s) { | |
ac3d33df | 490 | git_error_set(GIT_ERROR_NET, "failed to initialize SSH session"); |
08bf80fa | 491 | return -1; |
c2de6b1a | 492 | } |
a3c062db | 493 | |
08bf80fa | 494 | do { |
4b3ec53c | 495 | rc = libssh2_session_handshake(s, socket->s); |
08bf80fa | 496 | } while (LIBSSH2_ERROR_EAGAIN == rc || LIBSSH2_ERROR_TIMEOUT == rc); |
a3c062db | 497 | |
b622aabe | 498 | if (rc != LIBSSH2_ERROR_NONE) { |
909d5494 | 499 | ssh_error(s, "failed to start SSH session"); |
c2de6b1a | 500 | libssh2_session_free(s); |
c2de6b1a | 501 | return -1; |
68bc49a1 | 502 | } |
a3c062db | 503 | |
3eed595e | 504 | libssh2_session_set_blocking(s, 1); |
a3c062db | 505 | |
3eed595e | 506 | *session = s; |
a3c062db | 507 | |
3eed595e | 508 | return 0; |
3eed595e BM |
509 | } |
510 | ||
22a2d3d5 UG |
511 | #define SSH_DEFAULT_PORT "22" |
512 | ||
120b0122 | 513 | static int _git_ssh_setup_conn( |
8ae55d94 BM |
514 | ssh_subtransport *t, |
515 | const char *url, | |
120b0122 | 516 | const char *cmd, |
c2de6b1a | 517 | git_smart_subtransport_stream **stream) |
297758dc | 518 | { |
22a2d3d5 | 519 | git_net_url urldata = GIT_NET_URL_INIT; |
d7f962f4 | 520 | int auth_methods, error = 0; |
4df17045 | 521 | size_t i; |
8ae55d94 | 522 | ssh_stream *s; |
22a2d3d5 | 523 | git_credential *cred = NULL; |
3eed595e BM |
524 | LIBSSH2_SESSION* session=NULL; |
525 | LIBSSH2_CHANNEL* channel=NULL; | |
a3c062db | 526 | |
bc42479a ET |
527 | t->current_stream = NULL; |
528 | ||
297758dc | 529 | *stream = NULL; |
120b0122 | 530 | if (ssh_stream_alloc(t, url, cmd, stream) < 0) |
297758dc | 531 | return -1; |
a3c062db | 532 | |
8ae55d94 | 533 | s = (ssh_stream *)*stream; |
bc42479a ET |
534 | s->session = NULL; |
535 | s->channel = NULL; | |
a3c062db | 536 | |
4df17045 | 537 | for (i = 0; i < ARRAY_SIZE(ssh_prefixes); ++i) { |
ed21fd74 CB |
538 | const char *p = ssh_prefixes[i]; |
539 | ||
540 | if (!git__prefixcmp(url, p)) { | |
22a2d3d5 | 541 | if ((error = git_net_url_parse(&urldata, url)) < 0) |
ed21fd74 CB |
542 | goto done; |
543 | ||
544 | goto post_extract; | |
545 | } | |
7261d983 | 546 | } |
22a2d3d5 | 547 | if ((error = git_ssh_extract_url_parts(&urldata, url)) < 0) |
ed21fd74 | 548 | goto done; |
22a2d3d5 UG |
549 | |
550 | if (urldata.port == NULL) | |
551 | urldata.port = git__strdup(SSH_DEFAULT_PORT); | |
552 | ||
553 | GIT_ERROR_CHECK_ALLOC(urldata.port); | |
a3c062db | 554 | |
ed21fd74 | 555 | post_extract: |
22a2d3d5 | 556 | if ((error = git_socket_stream_new(&s->io, urldata.host, urldata.port)) < 0 || |
4fd4341f | 557 | (error = git_stream_connect(s->io)) < 0) |
bc42479a | 558 | goto done; |
2f5864c5 | 559 | |
4fd4341f | 560 | if ((error = _git_ssh_session_create(&session, s->io)) < 0) |
bc42479a | 561 | goto done; |
2f5864c5 CMN |
562 | |
563 | if (t->owner->certificate_check_cb != NULL) { | |
79698030 | 564 | git_cert_hostkey cert = {{ 0 }}, *cert_ptr; |
2f5864c5 | 565 | const char *key; |
2f5864c5 | 566 | |
79698030 | 567 | cert.parent.cert_type = GIT_CERT_HOSTKEY_LIBSSH2; |
0782fc43 | 568 | |
22a2d3d5 UG |
569 | #ifdef LIBSSH2_HOSTKEY_HASH_SHA256 |
570 | key = libssh2_hostkey_hash(session, LIBSSH2_HOSTKEY_HASH_SHA256); | |
571 | if (key != NULL) { | |
572 | cert.type |= GIT_CERT_SSH_SHA256; | |
573 | memcpy(&cert.hash_sha256, key, 32); | |
574 | } | |
575 | #endif | |
576 | ||
2f5864c5 CMN |
577 | key = libssh2_hostkey_hash(session, LIBSSH2_HOSTKEY_HASH_SHA1); |
578 | if (key != NULL) { | |
1e0aa105 CMN |
579 | cert.type |= GIT_CERT_SSH_SHA1; |
580 | memcpy(&cert.hash_sha1, key, 20); | |
2f5864c5 CMN |
581 | } |
582 | ||
1e0aa105 CMN |
583 | key = libssh2_hostkey_hash(session, LIBSSH2_HOSTKEY_HASH_MD5); |
584 | if (key != NULL) { | |
585 | cert.type |= GIT_CERT_SSH_MD5; | |
586 | memcpy(&cert.hash_md5, key, 16); | |
587 | } | |
588 | ||
589 | if (cert.type == 0) { | |
ac3d33df | 590 | git_error_set(GIT_ERROR_SSH, "unable to get the host key"); |
bc42479a ET |
591 | error = -1; |
592 | goto done; | |
2f5864c5 CMN |
593 | } |
594 | ||
595 | /* We don't currently trust any hostkeys */ | |
ac3d33df | 596 | git_error_clear(); |
369b0217 ET |
597 | |
598 | cert_ptr = &cert; | |
599 | ||
22a2d3d5 | 600 | error = t->owner->certificate_check_cb((git_cert *) cert_ptr, 0, urldata.host, t->owner->message_cb_payload); |
ac3d33df JK |
601 | |
602 | if (error < 0 && error != GIT_PASSTHROUGH) { | |
603 | if (!git_error_last()) | |
604 | git_error_set(GIT_ERROR_NET, "user cancelled hostkey check"); | |
23ca0ad5 | 605 | |
bc42479a | 606 | goto done; |
23ca0ad5 | 607 | } |
bc42479a | 608 | } |
2f5864c5 | 609 | |
ccb85c8f | 610 | /* we need the username to ask for auth methods */ |
22a2d3d5 UG |
611 | if (!urldata.username) { |
612 | if ((error = request_creds(&cred, t, NULL, GIT_CREDENTIAL_USERNAME)) < 0) | |
bc42479a | 613 | goto done; |
a3c062db | 614 | |
22a2d3d5 | 615 | urldata.username = git__strdup(((git_credential_username *) cred)->username); |
ccb85c8f CMN |
616 | cred->free(cred); |
617 | cred = NULL; | |
22a2d3d5 | 618 | if (!urldata.username) |
bc42479a | 619 | goto done; |
22a2d3d5 UG |
620 | } else if (urldata.username && urldata.password) { |
621 | if ((error = git_credential_userpass_plaintext_new(&cred, urldata.username, urldata.password)) < 0) | |
bc42479a | 622 | goto done; |
bc0a6198 CMN |
623 | } |
624 | ||
22a2d3d5 | 625 | if ((error = list_auth_methods(&auth_methods, session, urldata.username)) < 0) |
bc42479a | 626 | goto done; |
ccb85c8f | 627 | |
d7f962f4 CMN |
628 | error = GIT_EAUTH; |
629 | /* if we already have something to try */ | |
ccb85c8f | 630 | if (cred && auth_methods & cred->credtype) |
d7f962f4 | 631 | error = _git_ssh_authenticate_session(session, cred); |
a3c062db | 632 | |
d7f962f4 CMN |
633 | while (error == GIT_EAUTH) { |
634 | if (cred) { | |
635 | cred->free(cred); | |
636 | cred = NULL; | |
637 | } | |
638 | ||
22a2d3d5 | 639 | if ((error = request_creds(&cred, t, urldata.username, auth_methods)) < 0) |
bc42479a | 640 | goto done; |
d7f962f4 | 641 | |
22a2d3d5 | 642 | if (strcmp(urldata.username, git_credential_get_username(cred))) { |
ac3d33df | 643 | git_error_set(GIT_ERROR_SSH, "username does not match previous request"); |
ccb85c8f | 644 | error = -1; |
bc42479a | 645 | goto done; |
ccb85c8f CMN |
646 | } |
647 | ||
d7f962f4 | 648 | error = _git_ssh_authenticate_session(session, cred); |
22a2d3d5 UG |
649 | |
650 | if (error == GIT_EAUTH) { | |
651 | /* refresh auth methods */ | |
652 | if ((error = list_auth_methods(&auth_methods, session, urldata.username)) < 0) | |
653 | goto done; | |
654 | else | |
655 | error = GIT_EAUTH; | |
656 | } | |
d7f962f4 | 657 | } |
a3c062db | 658 | |
d7f962f4 | 659 | if (error < 0) |
bc42479a | 660 | goto done; |
a3c062db | 661 | |
3eed595e | 662 | channel = libssh2_channel_open_session(session); |
367c1903 | 663 | if (!channel) { |
b529c5f9 | 664 | error = -1; |
b622aabe | 665 | ssh_error(session, "Failed to open SSH channel"); |
bc42479a | 666 | goto done; |
367c1903 | 667 | } |
a3c062db | 668 | |
d04c3840 | 669 | libssh2_channel_set_blocking(channel, 1); |
a3c062db | 670 | |
f7158cd7 BM |
671 | s->session = session; |
672 | s->channel = channel; | |
a3c062db | 673 | |
297758dc | 674 | t->current_stream = s; |
d7f962f4 | 675 | |
bc42479a ET |
676 | done: |
677 | if (error < 0) { | |
81be2f46 | 678 | ssh_stream_free(*stream); |
a3c062db | 679 | |
bc42479a ET |
680 | if (session) |
681 | libssh2_session_free(session); | |
682 | } | |
a3c062db | 683 | |
d7f962f4 CMN |
684 | if (cred) |
685 | cred->free(cred); | |
686 | ||
22a2d3d5 | 687 | git_net_url_dispose(&urldata); |
22011b33 | 688 | |
b529c5f9 | 689 | return error; |
297758dc BM |
690 | } |
691 | ||
05f58131 | 692 | static int ssh_uploadpack_ls( |
120b0122 BM |
693 | ssh_subtransport *t, |
694 | const char *url, | |
695 | git_smart_subtransport_stream **stream) | |
696 | { | |
d4256ed5 CMN |
697 | const char *cmd = t->cmd_uploadpack ? t->cmd_uploadpack : cmd_uploadpack; |
698 | ||
6a0d2b43 | 699 | return _git_ssh_setup_conn(t, url, cmd, stream); |
120b0122 BM |
700 | } |
701 | ||
05f58131 | 702 | static int ssh_uploadpack( |
120b0122 BM |
703 | ssh_subtransport *t, |
704 | const char *url, | |
705 | git_smart_subtransport_stream **stream) | |
706 | { | |
707 | GIT_UNUSED(url); | |
a3c062db | 708 | |
120b0122 BM |
709 | if (t->current_stream) { |
710 | *stream = &t->current_stream->parent; | |
711 | return 0; | |
712 | } | |
a3c062db | 713 | |
ac3d33df | 714 | git_error_set(GIT_ERROR_NET, "must call UPLOADPACK_LS before UPLOADPACK"); |
120b0122 BM |
715 | return -1; |
716 | } | |
717 | ||
05f58131 | 718 | static int ssh_receivepack_ls( |
120b0122 BM |
719 | ssh_subtransport *t, |
720 | const char *url, | |
721 | git_smart_subtransport_stream **stream) | |
722 | { | |
d4256ed5 CMN |
723 | const char *cmd = t->cmd_receivepack ? t->cmd_receivepack : cmd_receivepack; |
724 | ||
a3c062db | 725 | |
2f5864c5 | 726 | return _git_ssh_setup_conn(t, url, cmd, stream); |
120b0122 BM |
727 | } |
728 | ||
05f58131 | 729 | static int ssh_receivepack( |
8ae55d94 BM |
730 | ssh_subtransport *t, |
731 | const char *url, | |
732 | git_smart_subtransport_stream **stream) | |
297758dc BM |
733 | { |
734 | GIT_UNUSED(url); | |
a3c062db | 735 | |
297758dc BM |
736 | if (t->current_stream) { |
737 | *stream = &t->current_stream->parent; | |
738 | return 0; | |
739 | } | |
a3c062db | 740 | |
ac3d33df | 741 | git_error_set(GIT_ERROR_NET, "must call RECEIVEPACK_LS before RECEIVEPACK"); |
297758dc BM |
742 | return -1; |
743 | } | |
744 | ||
67a7136c | 745 | static int _ssh_action( |
8ae55d94 BM |
746 | git_smart_subtransport_stream **stream, |
747 | git_smart_subtransport *subtransport, | |
748 | const char *url, | |
749 | git_smart_service_t action) | |
297758dc | 750 | { |
22a2d3d5 | 751 | ssh_subtransport *t = GIT_CONTAINER_OF(subtransport, ssh_subtransport, parent); |
a3c062db | 752 | |
297758dc BM |
753 | switch (action) { |
754 | case GIT_SERVICE_UPLOADPACK_LS: | |
05f58131 | 755 | return ssh_uploadpack_ls(t, url, stream); |
a3c062db | 756 | |
297758dc | 757 | case GIT_SERVICE_UPLOADPACK: |
05f58131 | 758 | return ssh_uploadpack(t, url, stream); |
a3c062db | 759 | |
297758dc | 760 | case GIT_SERVICE_RECEIVEPACK_LS: |
05f58131 | 761 | return ssh_receivepack_ls(t, url, stream); |
a3c062db | 762 | |
297758dc | 763 | case GIT_SERVICE_RECEIVEPACK: |
05f58131 | 764 | return ssh_receivepack(t, url, stream); |
297758dc | 765 | } |
a3c062db | 766 | |
297758dc BM |
767 | *stream = NULL; |
768 | return -1; | |
769 | } | |
770 | ||
67a7136c | 771 | static int _ssh_close(git_smart_subtransport *subtransport) |
297758dc | 772 | { |
22a2d3d5 | 773 | ssh_subtransport *t = GIT_CONTAINER_OF(subtransport, ssh_subtransport, parent); |
a3c062db | 774 | |
297758dc | 775 | assert(!t->current_stream); |
a3c062db | 776 | |
297758dc | 777 | GIT_UNUSED(t); |
a3c062db | 778 | |
297758dc BM |
779 | return 0; |
780 | } | |
781 | ||
67a7136c | 782 | static void _ssh_free(git_smart_subtransport *subtransport) |
297758dc | 783 | { |
22a2d3d5 | 784 | ssh_subtransport *t = GIT_CONTAINER_OF(subtransport, ssh_subtransport, parent); |
a3c062db | 785 | |
297758dc | 786 | assert(!t->current_stream); |
a3c062db | 787 | |
d4256ed5 CMN |
788 | git__free(t->cmd_uploadpack); |
789 | git__free(t->cmd_receivepack); | |
297758dc BM |
790 | git__free(t); |
791 | } | |
22618906 CMN |
792 | |
793 | #define SSH_AUTH_PUBLICKEY "publickey" | |
794 | #define SSH_AUTH_PASSWORD "password" | |
795 | #define SSH_AUTH_KEYBOARD_INTERACTIVE "keyboard-interactive" | |
796 | ||
ccb85c8f | 797 | static int list_auth_methods(int *out, LIBSSH2_SESSION *session, const char *username) |
22618906 | 798 | { |
22618906 CMN |
799 | const char *list, *ptr; |
800 | ||
801 | *out = 0; | |
802 | ||
ccb85c8f | 803 | list = libssh2_userauth_list(session, username, strlen(username)); |
22618906 CMN |
804 | |
805 | /* either error, or the remote accepts NONE auth, which is bizarre, let's punt */ | |
241414ee ML |
806 | if (list == NULL && !libssh2_userauth_authenticated(session)) { |
807 | ssh_error(session, "Failed to retrieve list of SSH authentication methods"); | |
ccb85c8f | 808 | return -1; |
241414ee | 809 | } |
22618906 CMN |
810 | |
811 | ptr = list; | |
812 | while (ptr) { | |
813 | if (*ptr == ',') | |
814 | ptr++; | |
815 | ||
816 | if (!git__prefixcmp(ptr, SSH_AUTH_PUBLICKEY)) { | |
22a2d3d5 UG |
817 | *out |= GIT_CREDENTIAL_SSH_KEY; |
818 | *out |= GIT_CREDENTIAL_SSH_CUSTOM; | |
7a8b8503 | 819 | #ifdef GIT_SSH_MEMORY_CREDENTIALS |
22a2d3d5 | 820 | *out |= GIT_CREDENTIAL_SSH_MEMORY; |
7a8b8503 | 821 | #endif |
22618906 CMN |
822 | ptr += strlen(SSH_AUTH_PUBLICKEY); |
823 | continue; | |
824 | } | |
825 | ||
826 | if (!git__prefixcmp(ptr, SSH_AUTH_PASSWORD)) { | |
22a2d3d5 | 827 | *out |= GIT_CREDENTIAL_USERPASS_PLAINTEXT; |
22618906 CMN |
828 | ptr += strlen(SSH_AUTH_PASSWORD); |
829 | continue; | |
830 | } | |
831 | ||
832 | if (!git__prefixcmp(ptr, SSH_AUTH_KEYBOARD_INTERACTIVE)) { | |
22a2d3d5 | 833 | *out |= GIT_CREDENTIAL_SSH_INTERACTIVE; |
22618906 CMN |
834 | ptr += strlen(SSH_AUTH_KEYBOARD_INTERACTIVE); |
835 | continue; | |
836 | } | |
837 | ||
838 | /* Skipt it if we don't know it */ | |
839 | ptr = strchr(ptr, ','); | |
840 | } | |
841 | ||
22618906 | 842 | return 0; |
22618906 | 843 | } |
a3c062db | 844 | #endif |
297758dc | 845 | |
a3c062db | 846 | int git_smart_subtransport_ssh( |
142e5379 | 847 | git_smart_subtransport **out, git_transport *owner, void *param) |
297758dc | 848 | { |
a3c062db | 849 | #ifdef GIT_SSH |
297758dc | 850 | ssh_subtransport *t; |
a3c062db RB |
851 | |
852 | assert(out); | |
853 | ||
142e5379 LY |
854 | GIT_UNUSED(param); |
855 | ||
297758dc | 856 | t = git__calloc(sizeof(ssh_subtransport), 1); |
ac3d33df | 857 | GIT_ERROR_CHECK_ALLOC(t); |
a3c062db | 858 | |
f7158cd7 | 859 | t->owner = (transport_smart *)owner; |
67a7136c BM |
860 | t->parent.action = _ssh_action; |
861 | t->parent.close = _ssh_close; | |
862 | t->parent.free = _ssh_free; | |
a3c062db | 863 | |
297758dc BM |
864 | *out = (git_smart_subtransport *) t; |
865 | return 0; | |
a3c062db RB |
866 | #else |
867 | GIT_UNUSED(owner); | |
142e5379 | 868 | GIT_UNUSED(param); |
a3c062db RB |
869 | |
870 | assert(out); | |
871 | *out = NULL; | |
574b86b7 | 872 | |
ac3d33df | 873 | git_error_set(GIT_ERROR_INVALID, "cannot create SSH transport. Library was built without SSH support"); |
a3c062db | 874 | return -1; |
574b86b7 | 875 | #endif |
a3c062db | 876 | } |
d4256ed5 CMN |
877 | |
878 | int git_transport_ssh_with_paths(git_transport **out, git_remote *owner, void *payload) | |
879 | { | |
880 | #ifdef GIT_SSH | |
881 | git_strarray *paths = (git_strarray *) payload; | |
882 | git_transport *transport; | |
883 | transport_smart *smart; | |
884 | ssh_subtransport *t; | |
885 | int error; | |
886 | git_smart_subtransport_definition ssh_definition = { | |
887 | git_smart_subtransport_ssh, | |
888 | 0, /* no RPC */ | |
142e5379 | 889 | NULL, |
d4256ed5 CMN |
890 | }; |
891 | ||
892 | if (paths->count != 2) { | |
ac3d33df | 893 | git_error_set(GIT_ERROR_SSH, "invalid ssh paths, must be two strings"); |
d4256ed5 CMN |
894 | return GIT_EINVALIDSPEC; |
895 | } | |
896 | ||
897 | if ((error = git_transport_smart(&transport, owner, &ssh_definition)) < 0) | |
898 | return error; | |
899 | ||
900 | smart = (transport_smart *) transport; | |
901 | t = (ssh_subtransport *) smart->wrapped; | |
902 | ||
903 | t->cmd_uploadpack = git__strdup(paths->strings[0]); | |
ac3d33df | 904 | GIT_ERROR_CHECK_ALLOC(t->cmd_uploadpack); |
d4256ed5 | 905 | t->cmd_receivepack = git__strdup(paths->strings[1]); |
ac3d33df | 906 | GIT_ERROR_CHECK_ALLOC(t->cmd_receivepack); |
d4256ed5 CMN |
907 | |
908 | *out = transport; | |
909 | return 0; | |
910 | #else | |
911 | GIT_UNUSED(owner); | |
8baeb8a4 | 912 | GIT_UNUSED(payload); |
d4256ed5 CMN |
913 | |
914 | assert(out); | |
915 | *out = NULL; | |
916 | ||
ac3d33df | 917 | git_error_set(GIT_ERROR_INVALID, "cannot create SSH transport. Library was built without SSH support"); |
d4256ed5 CMN |
918 | return -1; |
919 | #endif | |
920 | } | |
22f3d3aa | 921 | |
2ce2a48f PS |
922 | #ifdef GIT_SSH |
923 | static void shutdown_ssh(void) | |
924 | { | |
925 | libssh2_exit(); | |
926 | } | |
927 | #endif | |
928 | ||
22f3d3aa CMN |
929 | int git_transport_ssh_global_init(void) |
930 | { | |
931 | #ifdef GIT_SSH | |
8c027351 | 932 | if (libssh2_init(0) < 0) { |
ac3d33df | 933 | git_error_set(GIT_ERROR_SSH, "unable to initialize libssh2"); |
8c027351 PS |
934 | return -1; |
935 | } | |
22f3d3aa | 936 | |
2ce2a48f | 937 | git__on_shutdown(shutdown_ssh); |
22f3d3aa CMN |
938 | return 0; |
939 | ||
940 | #else | |
941 | ||
942 | /* Nothing to initialize */ | |
943 | return 0; | |
944 | ||
945 | #endif | |
946 | } |