ssh.c 13.9 KB
Newer Older
Brad Morgan committed
1 2 3 4 5 6 7
/*
 * 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.
 */

8 9 10 11
#ifdef GIT_SSH
#include <libssh2.h>
#endif

Brad Morgan committed
12 13 14
#include "git2.h"
#include "buffer.h"
#include "netops.h"
Brad Morgan committed
15
#include "smart.h"
Brad Morgan committed
16

17 18
#ifdef GIT_SSH

Brad Morgan committed
19 20
#define OWNING_SUBTRANSPORT(s) ((ssh_subtransport *)(s)->parent.subtransport)

Brad Morgan committed
21
static const char prefix_ssh[] = "ssh://";
Brad Morgan committed
22 23 24 25 26 27
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
28 29
	LIBSSH2_SESSION *session;
	LIBSSH2_CHANNEL *channel;
Brad Morgan committed
30 31 32 33 34 35 36
	const char *cmd;
	char *url;
	unsigned sent_command : 1;
} ssh_stream;

typedef struct {
	git_smart_subtransport parent;
Brad Morgan committed
37
	transport_smart *owner;
Brad Morgan committed
38
	ssh_stream *current_stream;
Brad Morgan committed
39
	git_cred *cred;
40 41
	char *cmd_uploadpack;
	char *cmd_receivepack;
Brad Morgan committed
42 43
} ssh_subtransport;

44 45 46 47 48 49 50 51
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
52 53 54
/*
 * Create a git protocol request.
 *
Brad Morgan committed
55
 * For example: git-upload-pack '/libgit2/libgit2'
Brad Morgan committed
56
 */
57
static int gen_proto(git_buf *request, const char *cmd, const char *url)
Brad Morgan committed
58
{
59
	char *repo;
60
	int len;
61

62 63 64 65 66
	if (!git__prefixcmp(url, prefix_ssh)) {
		url = url + strlen(prefix_ssh);
		repo = strchr(url, '/');
	} else {
		repo = strchr(url, ':');
67
		if (repo) repo++;
68
	}
69

70
	if (!repo) {
71
		giterr_set(GITERR_NET, "Malformed git protocol URL");
72 73
		return -1;
	}
74

75
	len = strlen(cmd) + 1 /* Space */ + 1 /* Quote */ + strlen(repo) + 1 /* Quote */ + 1;
76

Brad Morgan committed
77
	git_buf_grow(request, len);
Brad Morgan committed
78
	git_buf_printf(request, "%s '%s'", cmd, repo);
Brad Morgan committed
79
	git_buf_putc(request, '\0');
80

Brad Morgan committed
81 82
	if (git_buf_oom(request))
		return -1;
83

Brad Morgan committed
84 85 86
	return 0;
}

Brad Morgan committed
87
static int send_command(ssh_stream *s)
Brad Morgan committed
88 89 90
{
	int error;
	git_buf request = GIT_BUF_INIT;
91

92
	error = gen_proto(&request, s->cmd, s->url);
Brad Morgan committed
93 94
	if (error < 0)
		goto cleanup;
95

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

Brad Morgan committed
102
	s->sent_command = 1;
103

Brad Morgan committed
104 105 106 107 108
cleanup:
	git_buf_free(&request);
	return error;
}

Brad Morgan committed
109 110 111 112 113
static int ssh_stream_read(
	git_smart_subtransport_stream *stream,
	char *buffer,
	size_t buf_size,
	size_t *bytes_read)
Brad Morgan committed
114
{
115
	int rc;
Brad Morgan committed
116
	ssh_stream *s = (ssh_stream *)stream;
117

Brad Morgan committed
118
	*bytes_read = 0;
119

Brad Morgan committed
120 121
	if (!s->sent_command && send_command(s) < 0)
		return -1;
122

123 124
	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
125
		return -1;
126
	}
127

Brad Morgan committed
128
	*bytes_read = rc;
129

Brad Morgan committed
130 131 132
	return 0;
}

Brad Morgan committed
133 134 135 136
static int ssh_stream_write(
	git_smart_subtransport_stream *stream,
	const char *buffer,
	size_t len)
Brad Morgan committed
137
{
Brad Morgan committed
138
	ssh_stream *s = (ssh_stream *)stream;
139 140
	size_t off = 0;
	ssize_t ret = 0;
141

Brad Morgan committed
142 143
	if (!s->sent_command && send_command(s) < 0)
		return -1;
144

145 146 147 148 149 150 151 152 153 154
	do {
		ret = libssh2_channel_write(s->channel, buffer + off, len - off);
		if (ret < 0)
			break;

		off += ret;

	} while (off < len);

	if (ret < 0) {
155
		ssh_error(s->session, "SSH could not write data");
Brad Morgan committed
156 157
		return -1;
	}
158

159
	return 0;
Brad Morgan committed
160 161
}

Brad Morgan committed
162
static void ssh_stream_free(git_smart_subtransport_stream *stream)
Brad Morgan committed
163
{
Brad Morgan committed
164
	ssh_stream *s = (ssh_stream *)stream;
Brad Morgan committed
165 166
	ssh_subtransport *t = OWNING_SUBTRANSPORT(s);
	int ret;
167

Brad Morgan committed
168
	GIT_UNUSED(ret);
169

Brad Morgan committed
170
	t->current_stream = NULL;
171

Brad Morgan committed
172 173
	if (s->channel) {
		libssh2_channel_close(s->channel);
174 175
		libssh2_channel_free(s->channel);
		s->channel = NULL;
Brad Morgan committed
176
	}
177

Brad Morgan committed
178
	if (s->session) {
179 180
		libssh2_session_free(s->session);
		s->session = NULL;
Brad Morgan committed
181
	}
182

Brad Morgan committed
183
	if (s->socket.socket) {
184 185
		(void)gitno_close(&s->socket);
		/* can't do anything here with error return value */
Brad Morgan committed
186
	}
187

Brad Morgan committed
188 189 190 191
	git__free(s->url);
	git__free(s);
}

Brad Morgan committed
192 193 194 195 196
static int ssh_stream_alloc(
	ssh_subtransport *t,
	const char *url,
	const char *cmd,
	git_smart_subtransport_stream **stream)
Brad Morgan committed
197
{
Brad Morgan committed
198
	ssh_stream *s;
199

200
	assert(stream);
201

Brad Morgan committed
202
	s = git__calloc(sizeof(ssh_stream), 1);
Brad Morgan committed
203
	GITERR_CHECK_ALLOC(s);
204

Brad Morgan committed
205
	s->parent.subtransport = &t->parent;
Brad Morgan committed
206 207 208
	s->parent.read = ssh_stream_read;
	s->parent.write = ssh_stream_write;
	s->parent.free = ssh_stream_free;
209

Brad Morgan committed
210
	s->cmd = cmd;
211

212
	s->url = git__strdup(url);
Brad Morgan committed
213 214 215 216
	if (!s->url) {
		git__free(s);
		return -1;
	}
217

Brad Morgan committed
218 219 220 221
	*stream = &s->parent;
	return 0;
}

222
static int git_ssh_extract_url_parts(
Brad Morgan committed
223 224 225 226 227 228
	char **host,
	char **username,
	const char *url)
{
	char *colon, *at;
	const char *start;
229

230
	colon = strchr(url, ':');
231 232


233
	at = strchr(url, '@');
Brad Morgan committed
234
	if (at) {
Etienne Samson committed
235
		start = at + 1;
Brad Morgan committed
236
		*username = git__substrdup(url, at - url);
237
		GITERR_CHECK_ALLOC(*username);
Brad Morgan committed
238
	} else {
239
		start = url;
240
		*username = NULL;
Brad Morgan committed
241
	}
242

243 244 245 246 247
	if (colon == NULL || (colon < start)) {
		giterr_set(GITERR_NET, "Malformed URL");
		return -1;
	}

Brad Morgan committed
248
	*host = git__substrdup(start, colon - start);
249
	GITERR_CHECK_ALLOC(*host);
250

Brad Morgan committed
251 252 253
	return 0;
}

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 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297
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;
}

298 299
static int _git_ssh_authenticate_session(
	LIBSSH2_SESSION* session,
300
	git_cred* cred)
301 302
{
	int rc;
303

304 305
	do {
		switch (cred->credtype) {
306 307
		case GIT_CREDTYPE_USERPASS_PLAINTEXT: {
			git_cred_userpass_plaintext *c = (git_cred_userpass_plaintext *)cred;
308
			rc = libssh2_userauth_password(session, c->username, c->password);
309 310
			break;
		}
311 312
		case GIT_CREDTYPE_SSH_KEY: {
			git_cred_ssh_key *c = (git_cred_ssh_key *)cred;
313 314 315 316 317 318 319 320

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

321 322
			break;
		}
323 324
		case GIT_CREDTYPE_SSH_CUSTOM: {
			git_cred_ssh_custom *c = (git_cred_ssh_custom *)cred;
325

326
			rc = libssh2_userauth_publickey(
327
				session, c->username, (const unsigned char *)c->publickey,
328
				c->publickey_len, c->sign_callback, &c->payload);
329 330
			break;
		}
331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351
		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;
		}
352 353
		default:
			rc = LIBSSH2_ERROR_AUTHENTICATION_FAILED;
354
		}
Etienne Samson committed
355
	} while (LIBSSH2_ERROR_EAGAIN == rc || LIBSSH2_ERROR_TIMEOUT == rc);
356

357 358
	if (rc != LIBSSH2_ERROR_NONE) {
		ssh_error(session, "Failed to authenticate SSH session");
359 360 361 362
		return -1;
	}

	return 0;
363 364
}

Etienne Samson committed
365
static int _git_ssh_session_create(
Brad Morgan committed
366
	LIBSSH2_SESSION** session,
367
	gitno_socket socket)
Brad Morgan committed
368
{
369 370
	int rc = 0;
	LIBSSH2_SESSION* s;
371

372 373 374 375 376
	assert(session);

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

Etienne Samson committed
380 381 382
	do {
		rc = libssh2_session_startup(s, socket.socket);
	} while (LIBSSH2_ERROR_EAGAIN == rc || LIBSSH2_ERROR_TIMEOUT == rc);
383

384 385
	if (rc != LIBSSH2_ERROR_NONE) {
		ssh_error(s, "Failed to start SSH session");
386 387
		libssh2_session_free(s);
		return -1;
388
	}
389

Brad Morgan committed
390
	libssh2_session_set_blocking(s, 1);
391

Brad Morgan committed
392
	*session = s;
393

Brad Morgan committed
394 395 396
	return 0;
}

Brad Morgan committed
397
static int _git_ssh_setup_conn(
Brad Morgan committed
398 399
	ssh_subtransport *t,
	const char *url,
Brad Morgan committed
400
	const char *cmd,
401
	git_smart_subtransport_stream **stream)
Brad Morgan committed
402
{
Ben Straub committed
403
	char *host=NULL, *port=NULL, *path=NULL, *user=NULL, *pass=NULL;
Brad Morgan committed
404
	const char *default_port="22";
405
	int no_callback = 0;
Brad Morgan committed
406
	ssh_stream *s;
Brad Morgan committed
407 408
	LIBSSH2_SESSION* session=NULL;
	LIBSSH2_CHANNEL* channel=NULL;
409

Brad Morgan committed
410
	*stream = NULL;
Brad Morgan committed
411
	if (ssh_stream_alloc(t, url, cmd, stream) < 0)
Brad Morgan committed
412
		return -1;
413

Brad Morgan committed
414
	s = (ssh_stream *)*stream;
415

416
	if (!git__prefixcmp(url, prefix_ssh)) {
Ben Straub committed
417
		if (gitno_extract_url_parts(&host, &port, &path, &user, &pass, url, default_port) < 0)
418
			goto on_error;
419 420 421 422
	} else {
		if (git_ssh_extract_url_parts(&host, &user, url) < 0)
			goto on_error;
		port = git__strdup(default_port);
Brad Morgan committed
423
		GITERR_CHECK_ALLOC(port);
424
	}
425

426
	if (gitno_connect(&s->socket, host, port, 0) < 0)
Brad Morgan committed
427
		goto on_error;
428

429
	if (user && pass) {
Brad Morgan committed
430 431
		if (git_cred_userpass_plaintext_new(&t->cred, user, pass) < 0)
			goto on_error;
432 433 434 435 436 437 438 439 440 441 442 443 444
	} 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
445
			goto on_error;
446
		else if (!t->cred) {
447
			giterr_set(GITERR_SSH, "Callback failed to initialize SSH credentials");
448 449
			goto on_error;
		}
450 451 452 453
	}

	if (no_callback) {
		giterr_set(GITERR_SSH, "authentication required but no callback set");
454
		goto on_error;
455
	}
456

Brad Morgan committed
457
	assert(t->cred);
458

459
	if (_git_ssh_session_create(&session, s->socket) < 0)
460
		goto on_error;
461

462
	if (_git_ssh_authenticate_session(session, t->cred) < 0)
Brad Morgan committed
463
		goto on_error;
464

Brad Morgan committed
465
	channel = libssh2_channel_open_session(session);
466
	if (!channel) {
467
		ssh_error(session, "Failed to open SSH channel");
468 469
		goto on_error;
	}
470

Brad Morgan committed
471
	libssh2_channel_set_blocking(channel, 1);
472

Brad Morgan committed
473 474
	s->session = session;
	s->channel = channel;
475

Brad Morgan committed
476 477
	t->current_stream = s;
	git__free(host);
Brad Morgan committed
478
	git__free(port);
Ben Straub committed
479
	git__free(path);
Brad Morgan committed
480 481
	git__free(user);
	git__free(pass);
Brad Morgan committed
482

Brad Morgan committed
483
	return 0;
484

Brad Morgan committed
485
on_error:
Etienne Samson committed
486 487 488 489
	s->session = NULL;
	s->channel = NULL;
	t->current_stream = NULL;

Brad Morgan committed
490
	if (*stream)
Brad Morgan committed
491
		ssh_stream_free(*stream);
492

Brad Morgan committed
493
	git__free(host);
Brad Morgan committed
494 495 496 497
	git__free(port);
	git__free(user);
	git__free(pass);

Brad Morgan committed
498
	if (session)
499
		libssh2_session_free(session);
Brad Morgan committed
500

Brad Morgan committed
501 502 503
	return -1;
}

Brad Morgan committed
504
static int ssh_uploadpack_ls(
Brad Morgan committed
505 506 507 508
	ssh_subtransport *t,
	const char *url,
	git_smart_subtransport_stream **stream)
{
509 510 511
	const char *cmd = t->cmd_uploadpack ? t->cmd_uploadpack : cmd_uploadpack;

	if (_git_ssh_setup_conn(t, url, cmd, stream) < 0)
Brad Morgan committed
512
		return -1;
513

Brad Morgan committed
514 515 516
	return 0;
}

Brad Morgan committed
517
static int ssh_uploadpack(
Brad Morgan committed
518 519 520 521 522
	ssh_subtransport *t,
	const char *url,
	git_smart_subtransport_stream **stream)
{
	GIT_UNUSED(url);
523

Brad Morgan committed
524 525 526 527
	if (t->current_stream) {
		*stream = &t->current_stream->parent;
		return 0;
	}
528

Brad Morgan committed
529 530 531 532
	giterr_set(GITERR_NET, "Must call UPLOADPACK_LS before UPLOADPACK");
	return -1;
}

Brad Morgan committed
533
static int ssh_receivepack_ls(
Brad Morgan committed
534 535 536 537
	ssh_subtransport *t,
	const char *url,
	git_smart_subtransport_stream **stream)
{
538 539 540
	const char *cmd = t->cmd_receivepack ? t->cmd_receivepack : cmd_receivepack;

	if (_git_ssh_setup_conn(t, url, cmd, stream) < 0)
Brad Morgan committed
541
		return -1;
542

Brad Morgan committed
543 544 545
	return 0;
}

Brad Morgan committed
546
static int ssh_receivepack(
Brad Morgan committed
547 548 549
	ssh_subtransport *t,
	const char *url,
	git_smart_subtransport_stream **stream)
Brad Morgan committed
550 551
{
	GIT_UNUSED(url);
552

Brad Morgan committed
553 554 555 556
	if (t->current_stream) {
		*stream = &t->current_stream->parent;
		return 0;
	}
557

Brad Morgan committed
558 559 560 561
	giterr_set(GITERR_NET, "Must call RECEIVEPACK_LS before RECEIVEPACK");
	return -1;
}

Brad Morgan committed
562
static int _ssh_action(
Brad Morgan committed
563 564 565 566
	git_smart_subtransport_stream **stream,
	git_smart_subtransport *subtransport,
	const char *url,
	git_smart_service_t action)
Brad Morgan committed
567 568
{
	ssh_subtransport *t = (ssh_subtransport *) subtransport;
569

Brad Morgan committed
570 571
	switch (action) {
		case GIT_SERVICE_UPLOADPACK_LS:
Brad Morgan committed
572
			return ssh_uploadpack_ls(t, url, stream);
573

Brad Morgan committed
574
		case GIT_SERVICE_UPLOADPACK:
Brad Morgan committed
575
			return ssh_uploadpack(t, url, stream);
576

Brad Morgan committed
577
		case GIT_SERVICE_RECEIVEPACK_LS:
Brad Morgan committed
578
			return ssh_receivepack_ls(t, url, stream);
579

Brad Morgan committed
580
		case GIT_SERVICE_RECEIVEPACK:
Brad Morgan committed
581
			return ssh_receivepack(t, url, stream);
Brad Morgan committed
582
	}
583

Brad Morgan committed
584 585 586 587
	*stream = NULL;
	return -1;
}

Brad Morgan committed
588
static int _ssh_close(git_smart_subtransport *subtransport)
Brad Morgan committed
589 590
{
	ssh_subtransport *t = (ssh_subtransport *) subtransport;
591

Brad Morgan committed
592
	assert(!t->current_stream);
593

Brad Morgan committed
594
	GIT_UNUSED(t);
595

Brad Morgan committed
596 597 598
	return 0;
}

Brad Morgan committed
599
static void _ssh_free(git_smart_subtransport *subtransport)
Brad Morgan committed
600 601
{
	ssh_subtransport *t = (ssh_subtransport *) subtransport;
602

Brad Morgan committed
603
	assert(!t->current_stream);
604

605 606
	git__free(t->cmd_uploadpack);
	git__free(t->cmd_receivepack);
Brad Morgan committed
607 608
	git__free(t);
}
609
#endif
Brad Morgan committed
610

611 612
int git_smart_subtransport_ssh(
	git_smart_subtransport **out, git_transport *owner)
Brad Morgan committed
613
{
614
#ifdef GIT_SSH
Brad Morgan committed
615
	ssh_subtransport *t;
616 617 618

	assert(out);

Brad Morgan committed
619 620
	t = git__calloc(sizeof(ssh_subtransport), 1);
	GITERR_CHECK_ALLOC(t);
621

Brad Morgan committed
622
	t->owner = (transport_smart *)owner;
Brad Morgan committed
623 624 625
	t->parent.action = _ssh_action;
	t->parent.close = _ssh_close;
	t->parent.free = _ssh_free;
626

Brad Morgan committed
627 628
	*out = (git_smart_subtransport *) t;
	return 0;
629 630 631 632 633
#else
	GIT_UNUSED(owner);

	assert(out);
	*out = NULL;
634

635 636
	giterr_set(GITERR_INVALID, "Cannot create SSH transport. Library was built without SSH support");
	return -1;
637
#endif
638
}
639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672

int git_transport_ssh_with_paths(git_transport **out, git_remote *owner, void *payload)
{
#ifdef GIT_SSH
	git_strarray *paths = (git_strarray *) payload;
	git_transport *transport;
	transport_smart *smart;
	ssh_subtransport *t;
	int error;
	git_smart_subtransport_definition ssh_definition = {
		git_smart_subtransport_ssh,
		0, /* no RPC */
	};

	if (paths->count != 2) {
		giterr_set(GITERR_SSH, "invalid ssh paths, must be two strings");
		return GIT_EINVALIDSPEC;
	}

	if ((error = git_transport_smart(&transport, owner, &ssh_definition)) < 0)
		return error;

	smart = (transport_smart *) transport;
	t = (ssh_subtransport *) smart->wrapped;

	t->cmd_uploadpack = git__strdup(paths->strings[0]);
	GITERR_CHECK_ALLOC(t->cmd_uploadpack);
	t->cmd_receivepack = git__strdup(paths->strings[1]);
	GITERR_CHECK_ALLOC(t->cmd_receivepack);

	*out = transport;
	return 0;
#else
	GIT_UNUSED(owner);
Vicent Marti committed
673
	GIT_UNUSED(payload);
674 675 676 677 678 679 680 681

	assert(out);
	*out = NULL;

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