diff --git a/.gitignore b/.gitignore index 949baec98..bba9d5d5c 100644 --- a/.gitignore +++ b/.gitignore @@ -24,8 +24,8 @@ msvc/Release/ *.sdf *.opensdf *.aps -CMake* *.cmake +!cmake/Modules/*.cmake .DS_Store *~ tags diff --git a/CMakeLists.txt b/CMakeLists.txt index 1ffe1ddd4..3aa3770b8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,6 +14,8 @@ PROJECT(libgit2 C) CMAKE_MINIMUM_REQUIRED(VERSION 2.6) +set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake/Modules/") + # Build options # OPTION( SONAME "Set the (SO)VERSION of the target" ON ) @@ -141,6 +143,15 @@ ELSEIF (NOT ZLIB_LIBRARY) FILE(GLOB SRC_ZLIB deps/zlib/*.c) ENDIF() +IF(NOT LIBSSH2_LIBRARY) + FIND_PACKAGE(LIBSSH2 QUIET) +ENDIF() +IF (LIBSSH2_FOUND) + ADD_DEFINITIONS(-DGIT_SSH) + INCLUDE_DIRECTORIES(${LIBSSH2_INCLUDE_DIR}) + SET(SSH_LIBRARIES ${LIBSSH2_LIBRARIES}) +ENDIF() + # Platform specific compilation flags IF (MSVC) @@ -292,6 +303,7 @@ ENDIF() # Compile and link libgit2 ADD_LIBRARY(git2 ${SRC_GIT2} ${SRC_OS} ${SRC_ZLIB} ${SRC_HTTP} ${SRC_REGEX} ${SRC_SHA1} ${WIN_RC}) TARGET_LINK_LIBRARIES(git2 ${SSL_LIBRARIES}) +TARGET_LINK_LIBRARIES(git2 ${SSH_LIBRARIES}) TARGET_OS_LIBRARIES(git2) # Workaround for Cmake bug #0011240 (see http://public.kitware.com/Bug/view.php?id=11240) @@ -355,6 +367,7 @@ IF (BUILD_CLAR) ADD_EXECUTABLE(libgit2_clar ${SRC_GIT2} ${SRC_OS} ${SRC_CLAR} ${SRC_TEST} ${SRC_ZLIB} ${SRC_HTTP} ${SRC_REGEX} ${SRC_SHA1}) TARGET_LINK_LIBRARIES(libgit2_clar ${SSL_LIBRARIES}) + TARGET_LINK_LIBRARIES(libgit2_clar ${SSH_LIBRARIES}) TARGET_OS_LIBRARIES(libgit2_clar) MSVC_SPLIT_SOURCES(libgit2_clar) diff --git a/cmake/Modules/FindLIBSSH2.cmake b/cmake/Modules/FindLIBSSH2.cmake new file mode 100644 index 000000000..6347d60ea --- /dev/null +++ b/cmake/Modules/FindLIBSSH2.cmake @@ -0,0 +1,44 @@ +if (LIBSSH2_LIBRARIES AND LIBSSH2_INCLUDE_DIRS) + set(LIBSSH2_FOUND TRUE) +else (LIBSSH2_LIBRARIES AND LIBSSH2_INCLUDE_DIRS) + find_path(LIBSSH2_INCLUDE_DIR + NAMES + libssh2.h + PATHS + /usr/include + /usr/local/include + /opt/local/include + /sw/include + ${CMAKE_INCLUDE_PATH} + ${CMAKE_INSTALL_PREFIX}/include + ) + + find_library(LIBSSH2_LIBRARY + NAMES + ssh2 + libssh2 + PATHS + /usr/lib + /usr/local/lib + /opt/local/lib + /sw/lib + ${CMAKE_LIBRARY_PATH} + ${CMAKE_INSTALL_PREFIX}/lib + ) + + if (LIBSSH2_INCLUDE_DIR AND LIBSSH2_LIBRARY) + set(LIBSSH2_FOUND TRUE) + endif (LIBSSH2_INCLUDE_DIR AND LIBSSH2_LIBRARY) + + if (LIBSSH2_FOUND) + set(LIBSSH2_INCLUDE_DIRS + ${LIBSSH2_INCLUDE_DIR} + ) + + set(LIBSSH2_LIBRARIES + ${LIBSSH2_LIBRARIES} + ${LIBSSH2_LIBRARY} + ) + endif (LIBSSH2_FOUND) +endif (LIBSSH2_LIBRARIES AND LIBSSH2_INCLUDE_DIRS) + diff --git a/include/git2/transport.h b/include/git2/transport.h index 5e9968363..81bb3abe1 100644 --- a/include/git2/transport.h +++ b/include/git2/transport.h @@ -11,6 +11,10 @@ #include "net.h" #include "types.h" +#ifdef GIT_SSH +#include +#endif + /** * @file git2/transport.h * @brief Git transport interfaces and functions @@ -27,6 +31,8 @@ GIT_BEGIN_DECL typedef enum { /* git_cred_userpass_plaintext */ GIT_CREDTYPE_USERPASS_PLAINTEXT = 1, + GIT_CREDTYPE_SSH_KEYFILE_PASSPHRASE = 2, + GIT_CREDTYPE_SSH_PUBLICKEY = 3, } git_credtype_t; /* The base structure for all credential types */ @@ -43,6 +49,27 @@ typedef struct git_cred_userpass_plaintext { char *password; } git_cred_userpass_plaintext; +#ifdef GIT_SSH +typedef LIBSSH2_USERAUTH_PUBLICKEY_SIGN_FUNC((*git_cred_sign_callback)); + +/* A ssh key file and passphrase */ +typedef struct git_cred_ssh_keyfile_passphrase { + git_cred parent; + char *publickey; + char *privatekey; + char *passphrase; +} git_cred_ssh_keyfile_passphrase; + +/* A ssh public key and authentication callback */ +typedef struct git_cred_ssh_publickey { + git_cred parent; + char *publickey; + size_t publickey_len; + void *sign_callback; + void *sign_data; +} git_cred_ssh_publickey; +#endif + /** * Creates a new plain-text username and password credential object. * The supplied credential parameter will be internally duplicated. @@ -57,6 +84,42 @@ GIT_EXTERN(int) git_cred_userpass_plaintext_new( const char *username, const char *password); +#ifdef GIT_SSH +/** + * Creates a new ssh key file and passphrase credential object. + * The supplied credential parameter will be internally duplicated. + * + * @param out The newly created credential object. + * @param publickey The path to the public key of the credential. + * @param privatekey The path to the private key of the credential. + * @param passphrase The passphrase of the credential. + * @return 0 for success or an error code for failure + */ +GIT_EXTERN(int) git_cred_ssh_keyfile_passphrase_new( + git_cred **out, + const char *publickey, + const char *privatekey, + const char *passphrase); + +/** + * Creates a new ssh public key credential object. + * The supplied credential parameter will be internally duplicated. + * + * @param out The newly created credential object. + * @param publickey The bytes of the public key. + * @param publickey_len The length of the public key in bytes. + * @param sign_callback The callback method for authenticating. + * @param sign_data The abstract data sent to the sign_callback method. + * @return 0 for success or an error code for failure + */ +GIT_EXTERN(int) git_cred_ssh_publickey_new( + git_cred **out, + const char *publickey, + size_t publickey_len, + git_cred_sign_callback, + void *sign_data); +#endif + /** * Signature of a function which acquires a credential object. * @@ -319,6 +382,17 @@ GIT_EXTERN(int) git_smart_subtransport_git( git_smart_subtransport **out, git_transport* owner); +/** + * Create an instance of the ssh subtransport. + * + * @param out The newly created subtransport + * @param owner The smart transport to own this subtransport + * @return 0 or an error code + */ +GIT_EXTERN(int) git_smart_subtransport_ssh( + git_smart_subtransport **out, + git_transport* owner); + /* *** End interface for subtransports for the smart transport *** */ diff --git a/src/transport.c b/src/transport.c index adb6d5355..37c244c97 100644 --- a/src/transport.c +++ b/src/transport.c @@ -18,19 +18,27 @@ typedef struct transport_definition { void *param; } transport_definition; -static transport_definition local_transport_definition = { "file://", 1, git_transport_local, NULL }; -static transport_definition dummy_transport_definition = { NULL, 1, git_transport_dummy, NULL }; - static git_smart_subtransport_definition http_subtransport_definition = { git_smart_subtransport_http, 1 }; static git_smart_subtransport_definition git_subtransport_definition = { git_smart_subtransport_git, 0 }; +#ifdef GIT_SSH +static git_smart_subtransport_definition ssh_subtransport_definition = { git_smart_subtransport_ssh, 0 }; +#endif + +static transport_definition local_transport_definition = { "file://", 1, git_transport_local, NULL }; +#ifdef GIT_SSH +static transport_definition ssh_transport_definition = { "ssh://", 1, git_transport_smart, &ssh_subtransport_definition }; +#else +static transport_definition dummy_transport_definition = { NULL, 1, git_transport_dummy, NULL }; +#endif static transport_definition transports[] = { {"git://", 1, git_transport_smart, &git_subtransport_definition}, {"http://", 1, git_transport_smart, &http_subtransport_definition}, {"https://", 1, git_transport_smart, &http_subtransport_definition}, {"file://", 1, git_transport_local, NULL}, - {"git+ssh://", 1, git_transport_dummy, NULL}, - {"ssh+git://", 1, git_transport_dummy, NULL}, +#ifdef GIT_SSH + {"ssh://", 1, git_transport_smart, &ssh_subtransport_definition}, +#endif {NULL, 0, 0} }; @@ -73,7 +81,11 @@ static int transport_find_fn(const char *url, git_transport_cb *callback, void * /* It could be a SSH remote path. Check to see if there's a : * SSH is an unsupported transport mechanism in this version of libgit2 */ if (!definition && strrchr(url, ':')) - definition = &dummy_transport_definition; +#ifdef GIT_SSH + definition = &ssh_transport_definition; +#else + definition = &dummy_transport_definition; +#endif /* Check to see if the path points to a file on the local file system */ if (!definition && git_path_exists(url) && git_path_isdir(url)) diff --git a/src/transports/cred.c b/src/transports/cred.c index ecb026062..4916c6e18 100644 --- a/src/transports/cred.c +++ b/src/transports/cred.c @@ -58,3 +58,105 @@ int git_cred_userpass_plaintext_new( *cred = &c->parent; return 0; } + +#ifdef GIT_SSH +static void ssh_keyfile_passphrase_free(struct git_cred *cred) +{ + git_cred_ssh_keyfile_passphrase *c = (git_cred_ssh_keyfile_passphrase *)cred; + size_t pass_len = strlen(c->passphrase); + + if (c->publickey) { + git__free(c->publickey); + } + + git__free(c->privatekey); + + if (c->passphrase) { + /* Zero the memory which previously held the passphrase */ + memset(c->passphrase, 0x0, pass_len); + git__free(c->passphrase); + } + + memset(c, 0, sizeof(*c)); + + git__free(c); +} + +int git_cred_ssh_keyfile_passphrase_new( + git_cred **cred, + const char *publickey, + const char *privatekey, + const char *passphrase) +{ + git_cred_ssh_keyfile_passphrase *c; + + assert(cred && privatekey); + + c = git__calloc(1, sizeof(git_cred_ssh_keyfile_passphrase)); + GITERR_CHECK_ALLOC(c); + + c->parent.credtype = GIT_CREDTYPE_SSH_KEYFILE_PASSPHRASE; + c->parent.free = ssh_keyfile_passphrase_free; + + c->privatekey = git__strdup(privatekey); + GITERR_CHECK_ALLOC(c->privatekey); + + if (publickey) { + c->publickey = git__strdup(publickey); + GITERR_CHECK_ALLOC(c->publickey); + } + + if (passphrase) { + c->passphrase = git__strdup(passphrase); + GITERR_CHECK_ALLOC(c->passphrase); + } + + *cred = &c->parent; + return 0; +} + +static void ssh_publickey_free(struct git_cred *cred) +{ + git_cred_ssh_publickey *c = (git_cred_ssh_publickey *)cred; + + git__free(c->publickey); + + c->sign_callback = NULL; + c->sign_data = NULL; + + memset(c, 0, sizeof(*c)); + + git__free(c); +} + +int git_cred_ssh_publickey_new( + git_cred **cred, + const char *publickey, + size_t publickey_len, + LIBSSH2_USERAUTH_PUBLICKEY_SIGN_FUNC((*sign_callback)), + void *sign_data) +{ + git_cred_ssh_publickey *c; + + if (!cred) + return -1; + + c = git__malloc(sizeof(git_cred_ssh_publickey)); + GITERR_CHECK_ALLOC(c); + + c->parent.credtype = GIT_CREDTYPE_SSH_PUBLICKEY; + c->parent.free = ssh_publickey_free; + + c->publickey = git__malloc(publickey_len); + GITERR_CHECK_ALLOC(c->publickey); + + memcpy(c->publickey, publickey, publickey_len); + + c->publickey_len = publickey_len; + c->sign_callback = sign_callback; + c->sign_data = sign_data; + + *cred = &c->parent; + return 0; +} +#endif diff --git a/src/transports/ssh.c b/src/transports/ssh.c new file mode 100644 index 000000000..a312c8d08 --- /dev/null +++ b/src/transports/ssh.c @@ -0,0 +1,519 @@ +/* + * 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. + */ + +#ifdef GIT_SSH + +#include "git2.h" +#include "buffer.h" +#include "netops.h" +#include "smart.h" + +#include + +#define OWNING_SUBTRANSPORT(s) ((ssh_subtransport *)(s)->parent.subtransport) + +static const char prefix_ssh[] = "ssh://"; +static const char default_user[] = "git"; +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; + LIBSSH2_SESSION *session; + LIBSSH2_CHANNEL *channel; + const char *cmd; + char *url; + unsigned sent_command : 1; +} ssh_stream; + +typedef struct { + git_smart_subtransport parent; + transport_smart *owner; + ssh_stream *current_stream; + git_cred *cred; +} ssh_subtransport; + +/* + * Create a git protocol request. + * + * For example: git-upload-pack '/libgit2/libgit2' + */ +static int gen_proto(git_buf *request, const char *cmd, const char *url) +{ + char *repo; + + if (!git__prefixcmp(url, prefix_ssh)) { + url = url + strlen(prefix_ssh); + repo = strchr(url, '/'); + } else { + repo = strchr(url, ':'); + } + + if (!repo) { + return -1; + } + + int len = strlen(cmd) + 1 /* Space */ + 1 /* Quote */ + strlen(repo) + 1 /* Quote */ + 1; + + git_buf_grow(request, len); + git_buf_printf(request, "%s '%s'", cmd, repo); + git_buf_putc(request, '\0'); + + if (git_buf_oom(request)) + return -1; + + return 0; +} + +static int send_command(ssh_stream *s) +{ + int error; + git_buf request = GIT_BUF_INIT; + + error = gen_proto(&request, s->cmd, s->url); + if (error < 0) + goto cleanup; + + error = libssh2_channel_exec( + s->channel, + request.ptr + ); + + if (0 != error) + goto cleanup; + + s->sent_command = 1; + +cleanup: + git_buf_free(&request); + return error; +} + +static int ssh_stream_read( + git_smart_subtransport_stream *stream, + char *buffer, + size_t buf_size, + size_t *bytes_read) +{ + ssh_stream *s = (ssh_stream *)stream; + + *bytes_read = 0; + + if (!s->sent_command && send_command(s) < 0) + return -1; + + int rc = libssh2_channel_read(s->channel, buffer, buf_size); + if (rc < 0) + return -1; + + *bytes_read = rc; + + return 0; +} + +static int ssh_stream_write( + git_smart_subtransport_stream *stream, + const char *buffer, + size_t len) +{ + ssh_stream *s = (ssh_stream *)stream; + + if (!s->sent_command && send_command(s) < 0) + return -1; + + int rc = libssh2_channel_write(s->channel, buffer, len); + if (rc < 0) { + return -1; + } + + return rc; +} + +static void ssh_stream_free(git_smart_subtransport_stream *stream) +{ + ssh_stream *s = (ssh_stream *)stream; + ssh_subtransport *t = OWNING_SUBTRANSPORT(s); + int ret; + + GIT_UNUSED(ret); + + t->current_stream = NULL; + + if (s->channel) { + libssh2_channel_close(s->channel); + libssh2_channel_free(s->channel); + s->channel = NULL; + } + + if (s->session) { + libssh2_session_free(s->session), s->session = NULL; + } + + if (s->socket.socket) { + ret = gitno_close(&s->socket); + assert(!ret); + } + + git__free(s->url); + git__free(s); +} + +static int ssh_stream_alloc( + ssh_subtransport *t, + const char *url, + const char *cmd, + git_smart_subtransport_stream **stream) +{ + ssh_stream *s; + + if (!stream) + return -1; + + s = git__calloc(sizeof(ssh_stream), 1); + GITERR_CHECK_ALLOC(s); + + s->parent.subtransport = &t->parent; + s->parent.read = ssh_stream_read; + s->parent.write = ssh_stream_write; + s->parent.free = ssh_stream_free; + + s->cmd = cmd; + s->url = git__strdup(url); + + if (!s->url) { + git__free(s); + return -1; + } + + *stream = &s->parent; + return 0; +} + +static int git_ssh_extract_url_parts( + char **host, + char **username, + const char *url) +{ + char *colon, *at; + const char *start; + + colon = strchr(url, ':'); + + if (colon == NULL) { + giterr_set(GITERR_NET, "Malformed URL: missing :"); + return -1; + } + + at = strchr(url, '@'); + if (at) { + start = at+1; + *username = git__substrdup(url, at - url); + } else { + start = url; + *username = git__strdup(default_user); + } + + *host = git__substrdup(start, colon - start); + + return 0; +} + +static int _git_ssh_authenticate_session( + LIBSSH2_SESSION* session, + const char *user, + git_cred* cred +) +{ + int rc; + do { + switch (cred->credtype) { + case GIT_CREDTYPE_USERPASS_PLAINTEXT: { + git_cred_userpass_plaintext *c = (git_cred_userpass_plaintext *)cred; + rc = libssh2_userauth_password( + session, + c->username, + c->password + ); + break; + } + case GIT_CREDTYPE_SSH_KEYFILE_PASSPHRASE: { + git_cred_ssh_keyfile_passphrase *c = (git_cred_ssh_keyfile_passphrase *)cred; + rc = libssh2_userauth_publickey_fromfile( + session, + user, + c->publickey, + c->privatekey, + c->passphrase + ); + break; + } + case GIT_CREDTYPE_SSH_PUBLICKEY: { + git_cred_ssh_publickey *c = (git_cred_ssh_publickey *)cred; + rc = libssh2_userauth_publickey( + session, + user, + (const unsigned char *)c->publickey, + c->publickey_len, + c->sign_callback, + &c->sign_data + ); + break; + } + default: + rc = LIBSSH2_ERROR_AUTHENTICATION_FAILED; + } + } while (LIBSSH2_ERROR_EAGAIN == rc || LIBSSH2_ERROR_TIMEOUT == rc); + + return rc; +} + +static int _git_ssh_session_create +( + LIBSSH2_SESSION** session, + gitno_socket socket +) +{ + if (!session) { + return -1; + } + + LIBSSH2_SESSION* s = libssh2_session_init(); + if (!s) + return -1; + + int rc = 0; + do { + rc = libssh2_session_startup(s, socket.socket); + } while (LIBSSH2_ERROR_EAGAIN == rc || LIBSSH2_ERROR_TIMEOUT == rc); + + if (0 != rc) { + goto on_error; + } + + libssh2_session_set_blocking(s, 1); + + *session = s; + + return 0; + +on_error: + if (s) { + libssh2_session_free(s), s = NULL; + } + + return -1; +} + +static int _git_ssh_setup_conn( + ssh_subtransport *t, + const char *url, + const char *cmd, + git_smart_subtransport_stream **stream +) +{ + char *host, *port=NULL, *user=NULL, *pass=NULL; + const char *default_port="22"; + ssh_stream *s; + LIBSSH2_SESSION* session=NULL; + LIBSSH2_CHANNEL* channel=NULL; + + *stream = NULL; + if (ssh_stream_alloc(t, url, cmd, stream) < 0) + return -1; + + s = (ssh_stream *)*stream; + + if (!git__prefixcmp(url, prefix_ssh)) { + url = url + strlen(prefix_ssh); + if (gitno_extract_url_parts(&host, &port, &user, &pass, url, default_port) < 0) + goto on_error; + } else { + if (git_ssh_extract_url_parts(&host, &user, url) < 0) + goto on_error; + port = git__strdup(default_port); + GITERR_CHECK_ALLOC(port); + } + + if (gitno_connect(&s->socket, host, port, 0) < 0) + goto on_error; + + if (user && pass) { + if (git_cred_userpass_plaintext_new(&t->cred, user, pass) < 0) + goto on_error; + } else { + if (t->owner->cred_acquire_cb(&t->cred, + t->owner->url, + user, + GIT_CREDTYPE_USERPASS_PLAINTEXT | GIT_CREDTYPE_SSH_KEYFILE_PASSPHRASE, + t->owner->cred_acquire_payload) < 0) + return -1; + } + assert(t->cred); + + if (!user) { + user = git__strdup(default_user); + } + + if (_git_ssh_session_create(&session, s->socket) < 0) + goto on_error; + + if (_git_ssh_authenticate_session(session, user, t->cred) < 0) + goto on_error; + + channel = libssh2_channel_open_session(session); + if (!channel) + goto on_error; + + libssh2_channel_set_blocking(channel, 1); + + s->session = session; + s->channel = channel; + + t->current_stream = s; + git__free(host); + git__free(port); + git__free(user); + git__free(pass); + + return 0; + +on_error: + if (*stream) + ssh_stream_free(*stream); + + git__free(host); + git__free(port); + git__free(user); + git__free(pass); + + if (session) + libssh2_session_free(session), session = NULL; + + return -1; +} + +static int ssh_uploadpack_ls( + ssh_subtransport *t, + const char *url, + git_smart_subtransport_stream **stream) +{ + if (_git_ssh_setup_conn(t, url, cmd_uploadpack, stream) < 0) + return -1; + + return 0; +} + +static int ssh_uploadpack( + ssh_subtransport *t, + const char *url, + git_smart_subtransport_stream **stream) +{ + GIT_UNUSED(url); + + if (t->current_stream) { + *stream = &t->current_stream->parent; + return 0; + } + + giterr_set(GITERR_NET, "Must call UPLOADPACK_LS before UPLOADPACK"); + return -1; +} + +static int ssh_receivepack_ls( + ssh_subtransport *t, + const char *url, + git_smart_subtransport_stream **stream) +{ + if (_git_ssh_setup_conn(t, url, cmd_receivepack, stream) < 0) + return -1; + + return 0; +} + +static int ssh_receivepack( + ssh_subtransport *t, + const char *url, + git_smart_subtransport_stream **stream) +{ + GIT_UNUSED(url); + + if (t->current_stream) { + *stream = &t->current_stream->parent; + return 0; + } + + giterr_set(GITERR_NET, "Must call RECEIVEPACK_LS before RECEIVEPACK"); + return -1; +} + +static int _ssh_action( + git_smart_subtransport_stream **stream, + git_smart_subtransport *subtransport, + const char *url, + git_smart_service_t action) +{ + ssh_subtransport *t = (ssh_subtransport *) subtransport; + + switch (action) { + case GIT_SERVICE_UPLOADPACK_LS: + return ssh_uploadpack_ls(t, url, stream); + + case GIT_SERVICE_UPLOADPACK: + return ssh_uploadpack(t, url, stream); + + case GIT_SERVICE_RECEIVEPACK_LS: + return ssh_receivepack_ls(t, url, stream); + + case GIT_SERVICE_RECEIVEPACK: + return ssh_receivepack(t, url, stream); + } + + *stream = NULL; + return -1; +} + +static int _ssh_close(git_smart_subtransport *subtransport) +{ + ssh_subtransport *t = (ssh_subtransport *) subtransport; + + assert(!t->current_stream); + + GIT_UNUSED(t); + + return 0; +} + +static void _ssh_free(git_smart_subtransport *subtransport) +{ + ssh_subtransport *t = (ssh_subtransport *) subtransport; + + assert(!t->current_stream); + + git__free(t); +} + +int git_smart_subtransport_ssh(git_smart_subtransport **out, git_transport *owner) +{ + ssh_subtransport *t; + + if (!out) + return -1; + + t = git__calloc(sizeof(ssh_subtransport), 1); + GITERR_CHECK_ALLOC(t); + + t->owner = (transport_smart *)owner; + t->parent.action = _ssh_action; + t->parent.close = _ssh_close; + t->parent.free = _ssh_free; + + *out = (git_smart_subtransport *) t; + return 0; +} + +#endif