ssh.c 12.5 KB
Newer Older
Brad Morgan committed
1 2 3 4 5 6 7 8 9 10
/*
 * Copyright (C) the libgit2 contributors. All rights reserved.
 *
 * This file is part of libgit2, distributed under the GNU GPL v2 with
 * a Linking Exception. For full terms see the included COPYING file.
 */

#include "git2.h"
#include "buffer.h"
#include "netops.h"
Brad Morgan committed
11
#include "smart.h"
Brad Morgan committed
12

13 14
#ifdef GIT_SSH

Brad Morgan committed
15 16 17 18
#include <libssh2.h>

#define OWNING_SUBTRANSPORT(s) ((ssh_subtransport *)(s)->parent.subtransport)

Brad Morgan committed
19
static const char prefix_ssh[] = "ssh://";
Brad Morgan committed
20 21 22 23 24 25
static const char cmd_uploadpack[] = "git-upload-pack";
static const char cmd_receivepack[] = "git-receive-pack";

typedef struct {
	git_smart_subtransport_stream parent;
	gitno_socket socket;
Brad Morgan committed
26 27
	LIBSSH2_SESSION *session;
	LIBSSH2_CHANNEL *channel;
Brad Morgan committed
28 29 30 31 32 33 34
	const char *cmd;
	char *url;
	unsigned sent_command : 1;
} ssh_stream;

typedef struct {
	git_smart_subtransport parent;
Brad Morgan committed
35
	transport_smart *owner;
Brad Morgan committed
36
	ssh_stream *current_stream;
Brad Morgan committed
37
	git_cred *cred;
Brad Morgan committed
38 39
} ssh_subtransport;

40 41 42 43 44 45 46 47
static void ssh_error(LIBSSH2_SESSION *session, const char *errmsg)
{
	char *ssherr;
	libssh2_session_last_error(session, &ssherr, NULL, 0);

	giterr_set(GITERR_SSH, "%s: %s", errmsg, ssherr);
}

Brad Morgan committed
48 49 50
/*
 * Create a git protocol request.
 *
Brad Morgan committed
51
 * For example: git-upload-pack '/libgit2/libgit2'
Brad Morgan committed
52
 */
53
static int gen_proto(git_buf *request, const char *cmd, const char *url)
Brad Morgan committed
54
{
55
	char *repo;
56
	int len;
57

58 59 60 61 62
	if (!git__prefixcmp(url, prefix_ssh)) {
		url = url + strlen(prefix_ssh);
		repo = strchr(url, '/');
	} else {
		repo = strchr(url, ':');
63
		if (repo) repo++;
64
	}
65

66
	if (!repo) {
67
		giterr_set(GITERR_NET, "Malformed git protocol URL");
68 69
		return -1;
	}
70

71
	len = strlen(cmd) + 1 /* Space */ + 1 /* Quote */ + strlen(repo) + 1 /* Quote */ + 1;
72

Brad Morgan committed
73
	git_buf_grow(request, len);
Brad Morgan committed
74
	git_buf_printf(request, "%s '%s'", cmd, repo);
Brad Morgan committed
75
	git_buf_putc(request, '\0');
76

Brad Morgan committed
77 78
	if (git_buf_oom(request))
		return -1;
79

Brad Morgan committed
80 81 82
	return 0;
}

Brad Morgan committed
83
static int send_command(ssh_stream *s)
Brad Morgan committed
84 85 86
{
	int error;
	git_buf request = GIT_BUF_INIT;
87

88
	error = gen_proto(&request, s->cmd, s->url);
Brad Morgan committed
89 90
	if (error < 0)
		goto cleanup;
91

92
	error = libssh2_channel_exec(s->channel, request.ptr);
93 94
	if (error < LIBSSH2_ERROR_NONE) {
		ssh_error(s->session, "SSH could not execute request");
Brad Morgan committed
95
		goto cleanup;
96
	}
97

Brad Morgan committed
98
	s->sent_command = 1;
99

Brad Morgan committed
100 101 102 103 104
cleanup:
	git_buf_free(&request);
	return error;
}

Brad Morgan committed
105 106 107 108 109
static int ssh_stream_read(
	git_smart_subtransport_stream *stream,
	char *buffer,
	size_t buf_size,
	size_t *bytes_read)
Brad Morgan committed
110
{
111
	int rc;
Brad Morgan committed
112
	ssh_stream *s = (ssh_stream *)stream;
113

Brad Morgan committed
114
	*bytes_read = 0;
115

Brad Morgan committed
116 117
	if (!s->sent_command && send_command(s) < 0)
		return -1;
118

119 120
	if ((rc = libssh2_channel_read(s->channel, buffer, buf_size)) < LIBSSH2_ERROR_NONE) {
		ssh_error(s->session, "SSH could not read data");;
Brad Morgan committed
121
		return -1;
122
	}
123

Brad Morgan committed
124
	*bytes_read = rc;
125

Brad Morgan committed
126 127 128
	return 0;
}

Brad Morgan committed
129 130 131 132
static int ssh_stream_write(
	git_smart_subtransport_stream *stream,
	const char *buffer,
	size_t len)
Brad Morgan committed
133
{
Brad Morgan committed
134
	ssh_stream *s = (ssh_stream *)stream;
135

Brad Morgan committed
136 137
	if (!s->sent_command && send_command(s) < 0)
		return -1;
138

139 140
	if (libssh2_channel_write(s->channel, buffer, len) < LIBSSH2_ERROR_NONE) {
		ssh_error(s->session, "SSH could not write data");
Brad Morgan committed
141 142
		return -1;
	}
143

144
	return 0;
Brad Morgan committed
145 146
}

Brad Morgan committed
147
static void ssh_stream_free(git_smart_subtransport_stream *stream)
Brad Morgan committed
148
{
Brad Morgan committed
149
	ssh_stream *s = (ssh_stream *)stream;
Brad Morgan committed
150 151
	ssh_subtransport *t = OWNING_SUBTRANSPORT(s);
	int ret;
152

Brad Morgan committed
153
	GIT_UNUSED(ret);
154

Brad Morgan committed
155
	t->current_stream = NULL;
156

Brad Morgan committed
157 158
	if (s->channel) {
		libssh2_channel_close(s->channel);
159 160
		libssh2_channel_free(s->channel);
		s->channel = NULL;
Brad Morgan committed
161
	}
162

Brad Morgan committed
163
	if (s->session) {
164 165
		libssh2_session_free(s->session);
		s->session = NULL;
Brad Morgan committed
166
	}
167

Brad Morgan committed
168
	if (s->socket.socket) {
169 170
		(void)gitno_close(&s->socket);
		/* can't do anything here with error return value */
Brad Morgan committed
171
	}
172

Brad Morgan committed
173 174 175 176
	git__free(s->url);
	git__free(s);
}

Brad Morgan committed
177 178 179 180 181
static int ssh_stream_alloc(
	ssh_subtransport *t,
	const char *url,
	const char *cmd,
	git_smart_subtransport_stream **stream)
Brad Morgan committed
182
{
Brad Morgan committed
183
	ssh_stream *s;
184

185
	assert(stream);
186

Brad Morgan committed
187
	s = git__calloc(sizeof(ssh_stream), 1);
Brad Morgan committed
188
	GITERR_CHECK_ALLOC(s);
189

Brad Morgan committed
190
	s->parent.subtransport = &t->parent;
Brad Morgan committed
191 192 193
	s->parent.read = ssh_stream_read;
	s->parent.write = ssh_stream_write;
	s->parent.free = ssh_stream_free;
194

Brad Morgan committed
195
	s->cmd = cmd;
196

197
	s->url = git__strdup(url);
Brad Morgan committed
198 199 200 201
	if (!s->url) {
		git__free(s);
		return -1;
	}
202

Brad Morgan committed
203 204 205 206
	*stream = &s->parent;
	return 0;
}

207
static int git_ssh_extract_url_parts(
Brad Morgan committed
208 209 210 211 212 213
	char **host,
	char **username,
	const char *url)
{
	char *colon, *at;
	const char *start;
214

215
	colon = strchr(url, ':');
216 217


218
	at = strchr(url, '@');
Brad Morgan committed
219
	if (at) {
Etienne Samson committed
220
		start = at + 1;
Brad Morgan committed
221
		*username = git__substrdup(url, at - url);
222
		GITERR_CHECK_ALLOC(*username);
Brad Morgan committed
223
	} else {
224
		start = url;
225
		*username = NULL;
Brad Morgan committed
226
	}
227

228 229 230 231 232
	if (colon == NULL || (colon < start)) {
		giterr_set(GITERR_NET, "Malformed URL");
		return -1;
	}

Brad Morgan committed
233
	*host = git__substrdup(start, colon - start);
234
	GITERR_CHECK_ALLOC(*host);
235

Brad Morgan committed
236 237 238
	return 0;
}

239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282
static int ssh_agent_auth(LIBSSH2_SESSION *session, git_cred_ssh_key *c) {
	int rc = LIBSSH2_ERROR_NONE;

	struct libssh2_agent_publickey *curr, *prev = NULL;

	LIBSSH2_AGENT *agent = libssh2_agent_init(session);

	if (agent == NULL)
		return -1;

	rc = libssh2_agent_connect(agent);

	if (rc != LIBSSH2_ERROR_NONE)
		goto shutdown;

	rc = libssh2_agent_list_identities(agent);

	if (rc != LIBSSH2_ERROR_NONE)
		goto shutdown;

	while (1) {
		rc = libssh2_agent_get_identity(agent, &curr, prev);

		if (rc < 0)
			goto shutdown;

		if (rc == 1)
			goto shutdown;

		rc = libssh2_agent_userauth(agent, c->username, curr);

		if (rc == 0)
			break;

		prev = curr;
	}

shutdown:
	libssh2_agent_disconnect(agent);
	libssh2_agent_free(agent);

	return rc;
}

283 284
static int _git_ssh_authenticate_session(
	LIBSSH2_SESSION* session,
285
	git_cred* cred)
286 287
{
	int rc;
288

289 290
	do {
		switch (cred->credtype) {
291 292
		case GIT_CREDTYPE_USERPASS_PLAINTEXT: {
			git_cred_userpass_plaintext *c = (git_cred_userpass_plaintext *)cred;
293
			rc = libssh2_userauth_password(session, c->username, c->password);
294 295
			break;
		}
296 297
		case GIT_CREDTYPE_SSH_KEY: {
			git_cred_ssh_key *c = (git_cred_ssh_key *)cred;
298 299 300 301 302 303 304 305

			if (c->privatekey)
				rc = libssh2_userauth_publickey_fromfile(
					session, c->username, c->publickey,
					c->privatekey, c->passphrase);
			else
				rc = ssh_agent_auth(session, c);

306 307
			break;
		}
308 309
		case GIT_CREDTYPE_SSH_CUSTOM: {
			git_cred_ssh_custom *c = (git_cred_ssh_custom *)cred;
310

311
			rc = libssh2_userauth_publickey(
312
				session, c->username, (const unsigned char *)c->publickey,
313
				c->publickey_len, c->sign_callback, &c->payload);
314 315
			break;
		}
316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336
		case GIT_CREDTYPE_SSH_INTERACTIVE: {
			void **abstract = libssh2_session_abstract(session);
			git_cred_ssh_interactive *c = (git_cred_ssh_interactive *)cred;

			/* ideally, we should be able to set this by calling
			 * libssh2_session_init_ex() instead of libssh2_session_init().
			 * libssh2's API is inconsistent here i.e. libssh2_userauth_publickey()
			 * allows you to pass the `abstract` as part of the call, whereas
			 * libssh2_userauth_keyboard_interactive() does not!
			 *
			 * The only way to set the `abstract` pointer is by calling
			 * libssh2_session_abstract(), which will replace the existing
			 * pointer as is done below. This is safe for now (at time of writing),
			 * but may not be valid in future.
			 */
			*abstract = c->payload;

			rc = libssh2_userauth_keyboard_interactive(
				session, c->username, c->prompt_callback);
			break;
		}
337 338
		default:
			rc = LIBSSH2_ERROR_AUTHENTICATION_FAILED;
339
		}
Etienne Samson committed
340
	} while (LIBSSH2_ERROR_EAGAIN == rc || LIBSSH2_ERROR_TIMEOUT == rc);
341

342 343
	if (rc != LIBSSH2_ERROR_NONE) {
		ssh_error(session, "Failed to authenticate SSH session");
344 345 346 347
		return -1;
	}

	return 0;
348 349
}

Etienne Samson committed
350
static int _git_ssh_session_create(
Brad Morgan committed
351
	LIBSSH2_SESSION** session,
352
	gitno_socket socket)
Brad Morgan committed
353
{
354 355
	int rc = 0;
	LIBSSH2_SESSION* s;
356

357 358 359 360 361
	assert(session);

	s = libssh2_session_init();
	if (!s) {
		giterr_set(GITERR_NET, "Failed to initialize SSH session");
Etienne Samson committed
362
		return -1;
363
	}
364

Etienne Samson committed
365 366 367
	do {
		rc = libssh2_session_startup(s, socket.socket);
	} while (LIBSSH2_ERROR_EAGAIN == rc || LIBSSH2_ERROR_TIMEOUT == rc);
368

369 370
	if (rc != LIBSSH2_ERROR_NONE) {
		ssh_error(s, "Failed to start SSH session");
371 372
		libssh2_session_free(s);
		return -1;
373
	}
374

Brad Morgan committed
375
	libssh2_session_set_blocking(s, 1);
376

Brad Morgan committed
377
	*session = s;
378

Brad Morgan committed
379 380 381
	return 0;
}

Brad Morgan committed
382
static int _git_ssh_setup_conn(
Brad Morgan committed
383 384
	ssh_subtransport *t,
	const char *url,
Brad Morgan committed
385
	const char *cmd,
386
	git_smart_subtransport_stream **stream)
Brad Morgan committed
387
{
Ben Straub committed
388
	char *host=NULL, *port=NULL, *path=NULL, *user=NULL, *pass=NULL;
Brad Morgan committed
389
	const char *default_port="22";
390
	int no_callback = 0;
Brad Morgan committed
391
	ssh_stream *s;
Brad Morgan committed
392 393
	LIBSSH2_SESSION* session=NULL;
	LIBSSH2_CHANNEL* channel=NULL;
394

Brad Morgan committed
395
	*stream = NULL;
Brad Morgan committed
396
	if (ssh_stream_alloc(t, url, cmd, stream) < 0)
Brad Morgan committed
397
		return -1;
398

Brad Morgan committed
399
	s = (ssh_stream *)*stream;
400

401
	if (!git__prefixcmp(url, prefix_ssh)) {
Ben Straub committed
402
		if (gitno_extract_url_parts(&host, &port, &path, &user, &pass, url, default_port) < 0)
403
			goto on_error;
404 405 406 407
	} else {
		if (git_ssh_extract_url_parts(&host, &user, url) < 0)
			goto on_error;
		port = git__strdup(default_port);
Brad Morgan committed
408
		GITERR_CHECK_ALLOC(port);
409
	}
410

411
	if (gitno_connect(&s->socket, host, port, 0) < 0)
Brad Morgan committed
412
		goto on_error;
413

414
	if (user && pass) {
Brad Morgan committed
415 416
		if (git_cred_userpass_plaintext_new(&t->cred, user, pass) < 0)
			goto on_error;
417 418 419 420 421 422 423 424 425 426 427 428 429
	} else if (!t->owner->cred_acquire_cb) {
		no_callback = 1;
	} else {
		int error;
		error = t->owner->cred_acquire_cb(&t->cred, t->owner->url, user,
			GIT_CREDTYPE_USERPASS_PLAINTEXT |
			GIT_CREDTYPE_SSH_KEY | GIT_CREDTYPE_SSH_CUSTOM |
			GIT_CREDTYPE_SSH_INTERACTIVE,
			t->owner->cred_acquire_payload);

		if (error == GIT_PASSTHROUGH)
			no_callback = 1;
		else if (error < 0)
Etienne Samson committed
430
			goto on_error;
431
		else if (!t->cred) {
432
			giterr_set(GITERR_SSH, "Callback failed to initialize SSH credentials");
433 434
			goto on_error;
		}
435 436 437 438
	}

	if (no_callback) {
		giterr_set(GITERR_SSH, "authentication required but no callback set");
439
		goto on_error;
440
	}
441

Brad Morgan committed
442
	assert(t->cred);
443

444
	if (_git_ssh_session_create(&session, s->socket) < 0)
445
		goto on_error;
446

447
	if (_git_ssh_authenticate_session(session, t->cred) < 0)
Brad Morgan committed
448
		goto on_error;
449

Brad Morgan committed
450
	channel = libssh2_channel_open_session(session);
451
	if (!channel) {
452
		ssh_error(session, "Failed to open SSH channel");
453 454
		goto on_error;
	}
455

Brad Morgan committed
456
	libssh2_channel_set_blocking(channel, 1);
457

Brad Morgan committed
458 459
	s->session = session;
	s->channel = channel;
460

Brad Morgan committed
461 462
	t->current_stream = s;
	git__free(host);
Brad Morgan committed
463
	git__free(port);
Ben Straub committed
464
	git__free(path);
Brad Morgan committed
465 466
	git__free(user);
	git__free(pass);
Brad Morgan committed
467

Brad Morgan committed
468
	return 0;
469

Brad Morgan committed
470
on_error:
Etienne Samson committed
471 472 473 474
	s->session = NULL;
	s->channel = NULL;
	t->current_stream = NULL;

Brad Morgan committed
475
	if (*stream)
Brad Morgan committed
476
		ssh_stream_free(*stream);
477

Brad Morgan committed
478
	git__free(host);
Brad Morgan committed
479 480 481 482
	git__free(port);
	git__free(user);
	git__free(pass);

Brad Morgan committed
483
	if (session)
484
		libssh2_session_free(session);
Brad Morgan committed
485

Brad Morgan committed
486 487 488
	return -1;
}

Brad Morgan committed
489
static int ssh_uploadpack_ls(
Brad Morgan committed
490 491 492 493 494 495
	ssh_subtransport *t,
	const char *url,
	git_smart_subtransport_stream **stream)
{
	if (_git_ssh_setup_conn(t, url, cmd_uploadpack, stream) < 0)
		return -1;
496

Brad Morgan committed
497 498 499
	return 0;
}

Brad Morgan committed
500
static int ssh_uploadpack(
Brad Morgan committed
501 502 503 504 505
	ssh_subtransport *t,
	const char *url,
	git_smart_subtransport_stream **stream)
{
	GIT_UNUSED(url);
506

Brad Morgan committed
507 508 509 510
	if (t->current_stream) {
		*stream = &t->current_stream->parent;
		return 0;
	}
511

Brad Morgan committed
512 513 514 515
	giterr_set(GITERR_NET, "Must call UPLOADPACK_LS before UPLOADPACK");
	return -1;
}

Brad Morgan committed
516
static int ssh_receivepack_ls(
Brad Morgan committed
517 518 519 520 521 522
	ssh_subtransport *t,
	const char *url,
	git_smart_subtransport_stream **stream)
{
	if (_git_ssh_setup_conn(t, url, cmd_receivepack, stream) < 0)
		return -1;
523

Brad Morgan committed
524 525 526
	return 0;
}

Brad Morgan committed
527
static int ssh_receivepack(
Brad Morgan committed
528 529 530
	ssh_subtransport *t,
	const char *url,
	git_smart_subtransport_stream **stream)
Brad Morgan committed
531 532
{
	GIT_UNUSED(url);
533

Brad Morgan committed
534 535 536 537
	if (t->current_stream) {
		*stream = &t->current_stream->parent;
		return 0;
	}
538

Brad Morgan committed
539 540 541 542
	giterr_set(GITERR_NET, "Must call RECEIVEPACK_LS before RECEIVEPACK");
	return -1;
}

Brad Morgan committed
543
static int _ssh_action(
Brad Morgan committed
544 545 546 547
	git_smart_subtransport_stream **stream,
	git_smart_subtransport *subtransport,
	const char *url,
	git_smart_service_t action)
Brad Morgan committed
548 549
{
	ssh_subtransport *t = (ssh_subtransport *) subtransport;
550

Brad Morgan committed
551 552
	switch (action) {
		case GIT_SERVICE_UPLOADPACK_LS:
Brad Morgan committed
553
			return ssh_uploadpack_ls(t, url, stream);
554

Brad Morgan committed
555
		case GIT_SERVICE_UPLOADPACK:
Brad Morgan committed
556
			return ssh_uploadpack(t, url, stream);
557

Brad Morgan committed
558
		case GIT_SERVICE_RECEIVEPACK_LS:
Brad Morgan committed
559
			return ssh_receivepack_ls(t, url, stream);
560

Brad Morgan committed
561
		case GIT_SERVICE_RECEIVEPACK:
Brad Morgan committed
562
			return ssh_receivepack(t, url, stream);
Brad Morgan committed
563
	}
564

Brad Morgan committed
565 566 567 568
	*stream = NULL;
	return -1;
}

Brad Morgan committed
569
static int _ssh_close(git_smart_subtransport *subtransport)
Brad Morgan committed
570 571
{
	ssh_subtransport *t = (ssh_subtransport *) subtransport;
572

Brad Morgan committed
573
	assert(!t->current_stream);
574

Brad Morgan committed
575
	GIT_UNUSED(t);
576

Brad Morgan committed
577 578 579
	return 0;
}

Brad Morgan committed
580
static void _ssh_free(git_smart_subtransport *subtransport)
Brad Morgan committed
581 582
{
	ssh_subtransport *t = (ssh_subtransport *) subtransport;
583

Brad Morgan committed
584
	assert(!t->current_stream);
585

Brad Morgan committed
586 587
	git__free(t);
}
588
#endif
Brad Morgan committed
589

590 591
int git_smart_subtransport_ssh(
	git_smart_subtransport **out, git_transport *owner)
Brad Morgan committed
592
{
593
#ifdef GIT_SSH
Brad Morgan committed
594
	ssh_subtransport *t;
595 596 597

	assert(out);

Brad Morgan committed
598 599
	t = git__calloc(sizeof(ssh_subtransport), 1);
	GITERR_CHECK_ALLOC(t);
600

Brad Morgan committed
601
	t->owner = (transport_smart *)owner;
Brad Morgan committed
602 603 604
	t->parent.action = _ssh_action;
	t->parent.close = _ssh_close;
	t->parent.free = _ssh_free;
605

Brad Morgan committed
606 607
	*out = (git_smart_subtransport *) t;
	return 0;
608 609 610 611 612
#else
	GIT_UNUSED(owner);

	assert(out);
	*out = NULL;
613

614 615
	giterr_set(GITERR_INVALID, "Cannot create SSH transport. Library was built without SSH support");
	return -1;
616
#endif
617
}