Unverified Commit f51e70dc by Edward Thomson Committed by GitHub

Merge pull request #6617 from libgit2/ethomson/openssh

Add OpenSSH support
parents da265cdf ac399148
......@@ -25,40 +25,40 @@ jobs:
strategy:
matrix:
platform:
- name: "Linux (Xenial, GCC, OpenSSL)"
- name: "Linux (Xenial, GCC, OpenSSL, libssh2)"
id: xenial-gcc-openssl
container:
name: xenial
env:
CC: gcc
CMAKE_GENERATOR: Ninja
CMAKE_OPTIONS: -DUSE_HTTPS=OpenSSL -DREGEX_BACKEND=builtin -DDEPRECATE_HARD=ON -DUSE_LEAK_CHECKER=valgrind -DUSE_GSSAPI=ON -DUSE_SSH=ON -DDEBUG_STRICT_ALLOC=ON -DDEBUG_STRICT_OPEN=ON
CMAKE_OPTIONS: -DUSE_HTTPS=OpenSSL -DREGEX_BACKEND=builtin -DDEPRECATE_HARD=ON -DUSE_LEAK_CHECKER=valgrind -DUSE_GSSAPI=ON -DUSE_SSH=libssh2 -DDEBUG_STRICT_ALLOC=ON -DDEBUG_STRICT_OPEN=ON
os: ubuntu-latest
- name: Linux (Xenial, GCC, mbedTLS)
- name: Linux (Xenial, GCC, mbedTLS, OpenSSH)
id: xenial-gcc-mbedtls
container:
name: xenial
env:
CC: gcc
CMAKE_GENERATOR: Ninja
CMAKE_OPTIONS: -DUSE_HTTPS=mbedTLS -DUSE_SHA1=HTTPS -DDEPRECATE_HARD=ON -DUSE_LEAK_CHECKER=valgrind -DUSE_GSSAPI=ON -DUSE_SSH=ON
CMAKE_OPTIONS: -DUSE_HTTPS=mbedTLS -DUSE_SHA1=HTTPS -DDEPRECATE_HARD=ON -DUSE_LEAK_CHECKER=valgrind -DUSE_GSSAPI=ON -DUSE_SSH=exec
os: ubuntu-latest
- name: "Linux (Xenial, Clang, OpenSSL)"
- name: "Linux (Xenial, Clang, OpenSSL, OpenSSH)"
id: xenial-clang-openssl
container:
name: xenial
env:
CC: clang
CMAKE_GENERATOR: Ninja
CMAKE_OPTIONS: -DUSE_HTTPS=OpenSSL -DDEPRECATE_HARD=ON -DUSE_LEAK_CHECKER=valgrind -DUSE_GSSAPI=ON -DUSE_SSH=ON
CMAKE_OPTIONS: -DUSE_HTTPS=OpenSSL -DDEPRECATE_HARD=ON -DUSE_LEAK_CHECKER=valgrind -DUSE_GSSAPI=ON -DUSE_SSH=exec
os: ubuntu-latest
- name: "Linux (Xenial, Clang, mbedTLS)"
- name: "Linux (Xenial, Clang, mbedTLS, libssh2)"
id: xenial-clang-mbedtls
container:
name: xenial
env:
CC: clang
CMAKE_OPTIONS: -DUSE_HTTPS=mbedTLS -DUSE_SHA1=HTTPS -DREGEX_BACKEND=pcre -DDEPRECATE_HARD=ON -DUSE_LEAK_CHECKER=valgrind -DUSE_GSSAPI=ON -DUSE_SSH=ON
CMAKE_OPTIONS: -DUSE_HTTPS=mbedTLS -DUSE_SHA1=HTTPS -DREGEX_BACKEND=pcre -DDEPRECATE_HARD=ON -DUSE_LEAK_CHECKER=valgrind -DUSE_GSSAPI=ON -DUSE_SSH=libssh2
CMAKE_GENERATOR: Ninja
os: ubuntu-latest
- name: "macOS"
......
......@@ -30,7 +30,7 @@ option(USE_THREADS "Use threads for parallel processing when possibl
option(USE_NSEC "Support nanosecond precision file mtimes and ctimes" ON)
# Backend selection
option(USE_SSH "Link with libssh2 to enable SSH support" OFF)
option(USE_SSH "Enable SSH support. Can be set to a specific backend" OFF)
option(USE_HTTPS "Enable HTTPS support. Can be set to a specific backend" ON)
option(USE_SHA1 "Enable SHA1. Can be set to CollisionDetection(ON)/HTTPS" ON)
option(USE_SHA256 "Enable SHA256. Can be set to HTTPS/Builtin" ON)
......
......@@ -199,6 +199,8 @@ if [ -z "$SKIP_SSH_TESTS" ]; then
PubkeyAuthentication yes
ChallengeResponseAuthentication no
StrictModes no
HostCertificate ${SSHD_DIR}/id_rsa.pub
HostKey ${SSHD_DIR}/id_rsa
# Required here as sshd will simply close connection otherwise
UsePAM no
EOF
......@@ -414,6 +416,8 @@ if [ -z "$SKIP_SSH_TESTS" ]; then
export GITTEST_REMOTE_SSH_PASSPHRASE=""
export GITTEST_REMOTE_SSH_FINGERPRINT="${SSH_FINGERPRINT}"
export GITTEST_SSH_CMD="ssh -i ${HOME}/.ssh/id_rsa -o UserKnownHostsFile=${HOME}/.ssh/known_hosts"
echo ""
echo "Running ssh tests"
echo ""
......@@ -430,6 +434,8 @@ if [ -z "$SKIP_SSH_TESTS" ]; then
run_test ssh
unset GITTEST_REMOTE_URL
unset GITTEST_SSH_CMD
unset GITTEST_REMOTE_USER
unset GITTEST_REMOTE_SSH_KEY
unset GITTEST_REMOTE_SSH_PUBKEY
......
# Optional external dependency: libssh2
if(USE_SSH)
if(USE_SSH STREQUAL "exec")
set(GIT_SSH 1)
set(GIT_SSH_EXEC 1)
add_feature_info(SSH ON "using OpenSSH exec support")
elseif(USE_SSH STREQUAL ON OR USE_SSH STREQUAL "libssh2")
find_pkglibraries(LIBSSH2 libssh2)
if(NOT LIBSSH2_FOUND)
find_package(LibSSH2)
set(LIBSSH2_INCLUDE_DIRS ${LIBSSH2_INCLUDE_DIR})
......@@ -12,30 +17,28 @@ if(USE_SSH)
if(NOT LIBSSH2_FOUND)
message(FATAL_ERROR "LIBSSH2 not found. Set CMAKE_PREFIX_PATH if it is installed outside of the default search path.")
endif()
endif()
if(LIBSSH2_FOUND)
set(GIT_SSH 1)
list(APPEND LIBGIT2_SYSTEM_INCLUDES ${LIBSSH2_INCLUDE_DIRS})
list(APPEND LIBGIT2_SYSTEM_LIBS ${LIBSSH2_LIBRARIES})
list(APPEND LIBGIT2_PC_LIBS ${LIBSSH2_LDFLAGS})
check_library_exists("${LIBSSH2_LIBRARIES}" libssh2_userauth_publickey_frommemory "${LIBSSH2_LIBRARY_DIRS}" HAVE_LIBSSH2_MEMORY_CREDENTIALS)
if(HAVE_LIBSSH2_MEMORY_CREDENTIALS)
set(GIT_SSH_MEMORY_CREDENTIALS 1)
set(GIT_SSH_LIBSSH2_MEMORY_CREDENTIALS 1)
endif()
else()
message(STATUS "LIBSSH2 not found. Set CMAKE_PREFIX_PATH if it is installed outside of the default search path.")
endif()
if(WIN32 AND EMBED_SSH_PATH)
if(WIN32 AND EMBED_SSH_PATH)
file(GLOB SSH_SRC "${EMBED_SSH_PATH}/src/*.c")
list(SORT SSH_SRC)
list(APPEND LIBGIT2_DEPENDENCY_OBJECTS ${SSH_SRC})
list(APPEND LIBGIT2_DEPENDENCY_INCLUDES "${EMBED_SSH_PATH}/include")
file(WRITE "${EMBED_SSH_PATH}/src/libssh2_config.h" "#define HAVE_WINCNG\n#define LIBSSH2_WINCNG\n#include \"../win32/libssh2_config.h\"")
endif()
set(GIT_SSH 1)
set(GIT_SSH_LIBSSH2 1)
add_feature_info(SSH ON "using libssh2")
else()
add_feature_info(SSH OFF "SSH transport support")
endif()
add_feature_info(SSH GIT_SSH "SSH transport support")
#!/bin/bash
exec valgrind --leak-check=full --show-reachable=yes --error-exitcode=125 --num-callers=50 --suppressions="$(dirname "${BASH_SOURCE[0]}")/valgrind.supp" "$@"
exec valgrind --leak-check=full --show-reachable=yes --child-silent-after-fork=yes --error-exitcode=125 --num-callers=50 --suppressions="$(dirname "${BASH_SOURCE[0]}")/valgrind.supp" "$@"
......@@ -34,7 +34,7 @@
#include "streams/socket.h"
#include "transports/smart.h"
#include "transports/http.h"
#include "transports/ssh.h"
#include "transports/ssh_libssh2.h"
#ifdef GIT_WIN32
# include "win32/w32_leakcheck.h"
......@@ -80,7 +80,7 @@ int git_libgit2_init(void)
git_sysdir_global_init,
git_filter_global_init,
git_merge_driver_global_init,
git_transport_ssh_global_init,
git_transport_ssh_libssh2_global_init,
git_stream_registry_global_init,
git_socket_stream_global_init,
git_openssl_stream_global_init,
......@@ -126,10 +126,10 @@ int git_libgit2_features(void)
#ifdef GIT_HTTPS
| GIT_FEATURE_HTTPS
#endif
#if defined(GIT_SSH)
#ifdef GIT_SSH
| GIT_FEATURE_SSH
#endif
#if defined(GIT_USE_NSEC)
#ifdef GIT_USE_NSEC
| GIT_FEATURE_NSEC
#endif
;
......
......@@ -22,6 +22,7 @@ typedef struct transport_definition {
static git_smart_subtransport_definition http_subtransport_definition = { git_smart_subtransport_http, 1, NULL };
static git_smart_subtransport_definition git_subtransport_definition = { git_smart_subtransport_git, 0, NULL };
#ifdef GIT_SSH
static git_smart_subtransport_definition ssh_subtransport_definition = { git_smart_subtransport_ssh, 0, NULL };
#endif
......@@ -33,11 +34,13 @@ static transport_definition transports[] = {
{ "http://", git_transport_smart, &http_subtransport_definition },
{ "https://", git_transport_smart, &http_subtransport_definition },
{ "file://", git_transport_local, NULL },
#ifdef GIT_SSH
{ "ssh://", git_transport_smart, &ssh_subtransport_definition },
{ "ssh+git://", git_transport_smart, &ssh_subtransport_definition },
{ "git+ssh://", git_transport_smart, &ssh_subtransport_definition },
#endif
{ NULL, 0, 0 }
};
......
......@@ -204,7 +204,7 @@ int git_credential_ssh_key_memory_new(
const char *privatekey,
const char *passphrase)
{
#ifdef GIT_SSH_MEMORY_CREDENTIALS
#ifdef GIT_SSH_LIBSSH2_MEMORY_CREDENTIALS
return git_credential_ssh_key_type_new(
cred,
username,
......
......@@ -370,17 +370,27 @@ static int git_smart__close(git_transport *transport)
git_vector *common = &t->common;
unsigned int i;
git_pkt *p;
git_smart_service_t service;
int ret;
git_smart_subtransport_stream *stream;
const char flush[] = "0000";
if (t->direction == GIT_DIRECTION_FETCH) {
service = GIT_SERVICE_UPLOADPACK;
} else if (t->direction == GIT_DIRECTION_PUSH) {
service = GIT_SERVICE_RECEIVEPACK;
} else {
git_error_set(GIT_ERROR_NET, "invalid direction");
return -1;
}
/*
* If we're still connected at this point and not using RPC,
* we should say goodbye by sending a flush, or git-daemon
* will complain that we disconnected unexpectedly.
*/
if (t->connected && !t->rpc &&
!t->wrapped->action(&stream, t->wrapped, t->url, GIT_SERVICE_UPLOADPACK)) {
!t->wrapped->action(&stream, t->wrapped, t->url, service)) {
t->current_stream->write(t->current_stream, flush, 4);
}
......@@ -513,7 +523,6 @@ int git_transport_smart(git_transport **out, git_remote *owner, void *param)
definition->callback(&t->wrapped, &t->parent, definition->param) < 0) {
git_vector_free(&t->refs);
git_vector_free(&t->heads);
t->wrapped->free(t->wrapped);
git__free(t);
return -1;
}
......
......@@ -59,7 +59,7 @@ int git_smart__store_refs(transport_smart *t, int flushes)
return recvd;
if (recvd == 0) {
git_error_set(GIT_ERROR_NET, "early EOF");
git_error_set(GIT_ERROR_NET, "could not read refs from remote repository");
return GIT_EEOF;
}
......@@ -285,7 +285,7 @@ static int recv_pkt(
if ((ret = git_smart__recv(t)) < 0) {
return ret;
} else if (ret == 0) {
git_error_set(GIT_ERROR_NET, "early EOF");
git_error_set(GIT_ERROR_NET, "could not read from remote repository");
return GIT_EEOF;
}
} while (error);
......@@ -940,7 +940,7 @@ static int parse_report(transport_smart *transport, git_push *push)
}
if (recvd == 0) {
git_error_set(GIT_ERROR_NET, "early EOF");
git_error_set(GIT_ERROR_NET, "could not read report from remote repository");
error = GIT_EEOF;
goto done;
}
......
......@@ -5,1090 +5,67 @@
* a Linking Exception. For full terms see the included COPYING file.
*/
#include "ssh.h"
#ifdef GIT_SSH
#include <libssh2.h>
#endif
#include "runtime.h"
#include "net.h"
#include "smart.h"
#include "streams/socket.h"
#include "sysdir.h"
#include "git2/credential.h"
#include "git2/sys/credential.h"
#ifdef GIT_SSH
#define OWNING_SUBTRANSPORT(s) ((ssh_subtransport *)(s)->parent.subtransport)
static const char cmd_uploadpack[] = "git-upload-pack";
static const char cmd_receivepack[] = "git-receive-pack";
typedef struct {
git_smart_subtransport_stream parent;
git_stream *io;
LIBSSH2_SESSION *session;
LIBSSH2_CHANNEL *channel;
const char *cmd;
git_net_url url;
unsigned sent_command : 1;
} ssh_stream;
typedef struct {
git_smart_subtransport parent;
transport_smart *owner;
ssh_stream *current_stream;
git_credential *cred;
char *cmd_uploadpack;
char *cmd_receivepack;
} ssh_subtransport;
static int list_auth_methods(int *out, LIBSSH2_SESSION *session, const char *username);
static void ssh_error(LIBSSH2_SESSION *session, const char *errmsg)
{
char *ssherr;
libssh2_session_last_error(session, &ssherr, NULL, 0);
git_error_set(GIT_ERROR_SSH, "%s: %s", errmsg, ssherr);
}
/*
* Create a git protocol request.
*
* For example: git-upload-pack '/libgit2/libgit2'
*/
static int gen_proto(git_str *request, const char *cmd, git_net_url *url)
{
const char *repo;
repo = url->path;
if (repo && repo[0] == '/' && repo[1] == '~')
repo++;
if (!repo || !repo[0]) {
git_error_set(GIT_ERROR_NET, "malformed git protocol URL");
return -1;
}
git_str_puts(request, cmd);
git_str_puts(request, " '");
git_str_puts(request, repo);
git_str_puts(request, "'");
if (git_str_oom(request))
return -1;
return 0;
}
static int send_command(ssh_stream *s)
{
int error;
git_str request = GIT_STR_INIT;
error = gen_proto(&request, s->cmd, &s->url);
if (error < 0)
goto cleanup;
error = libssh2_channel_exec(s->channel, request.ptr);
if (error < LIBSSH2_ERROR_NONE) {
ssh_error(s->session, "SSH could not execute request");
goto cleanup;
}
s->sent_command = 1;
cleanup:
git_str_dispose(&request);
return error;
}
static int ssh_stream_read(
git_smart_subtransport_stream *stream,
char *buffer,
size_t buf_size,
size_t *bytes_read)
{
int rc;
ssh_stream *s = GIT_CONTAINER_OF(stream, ssh_stream, parent);
*bytes_read = 0;
if (!s->sent_command && send_command(s) < 0)
return -1;
if ((rc = libssh2_channel_read(s->channel, buffer, buf_size)) < LIBSSH2_ERROR_NONE) {
ssh_error(s->session, "SSH could not read data");
return -1;
}
/*
* If we can't get anything out of stdout, it's typically a
* not-found error, so read from stderr and signal EOF on
* stderr.
*/
if (rc == 0) {
if ((rc = libssh2_channel_read_stderr(s->channel, buffer, buf_size)) > 0) {
git_error_set(GIT_ERROR_SSH, "%*s", rc, buffer);
return GIT_EEOF;
} else if (rc < LIBSSH2_ERROR_NONE) {
ssh_error(s->session, "SSH could not read stderr");
return -1;
}
}
#include "ssh_exec.h"
#include "ssh_libssh2.h"
#include "transports/smart.h"
*bytes_read = rc;
return 0;
}
static int ssh_stream_write(
git_smart_subtransport_stream *stream,
const char *buffer,
size_t len)
{
ssh_stream *s = GIT_CONTAINER_OF(stream, ssh_stream, parent);
size_t off = 0;
ssize_t ret = 0;
if (!s->sent_command && send_command(s) < 0)
return -1;
do {
ret = libssh2_channel_write(s->channel, buffer + off, len - off);
if (ret < 0)
break;
off += ret;
} while (off < len);
if (ret < 0) {
ssh_error(s->session, "SSH could not write data");
return -1;
}
return 0;
}
static void ssh_stream_free(git_smart_subtransport_stream *stream)
{
ssh_stream *s = GIT_CONTAINER_OF(stream, ssh_stream, parent);
ssh_subtransport *t;
if (!stream)
return;
t = OWNING_SUBTRANSPORT(s);
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_disconnect(s->session, "closing transport");
libssh2_session_free(s->session);
s->session = NULL;
}
if (s->io) {
git_stream_close(s->io);
git_stream_free(s->io);
s->io = NULL;
}
git_net_url_dispose(&s->url);
git__free(s);
}
static int ssh_stream_alloc(
ssh_subtransport *t,
const char *cmd,
git_smart_subtransport_stream **stream)
{
ssh_stream *s;
GIT_ASSERT_ARG(stream);
s = git__calloc(sizeof(ssh_stream), 1);
GIT_ERROR_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;
*stream = &s->parent;
return 0;
}
static int ssh_agent_auth(LIBSSH2_SESSION *session, git_credential_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) {
rc = LIBSSH2_ERROR_AUTHENTICATION_FAILED;
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;
/* rc is set to 1 whenever the ssh agent ran out of keys to check.
* Set the error code to authentication failure rather than erroring
* out with an untranslatable error code.
*/
if (rc == 1) {
rc = LIBSSH2_ERROR_AUTHENTICATION_FAILED;
goto shutdown;
}
rc = libssh2_agent_userauth(agent, c->username, curr);
if (rc == 0)
break;
prev = curr;
}
shutdown:
if (rc != LIBSSH2_ERROR_NONE)
ssh_error(session, "error authenticating");
libssh2_agent_disconnect(agent);
libssh2_agent_free(agent);
return rc;
}
static int _git_ssh_authenticate_session(
LIBSSH2_SESSION *session,
git_credential *cred)
{
int rc;
do {
git_error_clear();
switch (cred->credtype) {
case GIT_CREDENTIAL_USERPASS_PLAINTEXT: {
git_credential_userpass_plaintext *c = (git_credential_userpass_plaintext *)cred;
rc = libssh2_userauth_password(session, c->username, c->password);
break;
}
case GIT_CREDENTIAL_SSH_KEY: {
git_credential_ssh_key *c = (git_credential_ssh_key *)cred;
if (c->privatekey)
rc = libssh2_userauth_publickey_fromfile(
session, c->username, c->publickey,
c->privatekey, c->passphrase);
else
rc = ssh_agent_auth(session, c);
break;
}
case GIT_CREDENTIAL_SSH_CUSTOM: {
git_credential_ssh_custom *c = (git_credential_ssh_custom *)cred;
rc = libssh2_userauth_publickey(
session, c->username, (const unsigned char *)c->publickey,
c->publickey_len, c->sign_callback, &c->payload);
break;
}
case GIT_CREDENTIAL_SSH_INTERACTIVE: {
void **abstract = libssh2_session_abstract(session);
git_credential_ssh_interactive *c = (git_credential_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;
}
#ifdef GIT_SSH_MEMORY_CREDENTIALS
case GIT_CREDENTIAL_SSH_MEMORY: {
git_credential_ssh_key *c = (git_credential_ssh_key *)cred;
GIT_ASSERT(c->username);
GIT_ASSERT(c->privatekey);
rc = libssh2_userauth_publickey_frommemory(
session,
c->username,
strlen(c->username),
c->publickey,
c->publickey ? strlen(c->publickey) : 0,
c->privatekey,
strlen(c->privatekey),
c->passphrase);
break;
}
#endif
default:
rc = LIBSSH2_ERROR_AUTHENTICATION_FAILED;
}
} while (LIBSSH2_ERROR_EAGAIN == rc || LIBSSH2_ERROR_TIMEOUT == rc);
if (rc == LIBSSH2_ERROR_PASSWORD_EXPIRED ||
rc == LIBSSH2_ERROR_AUTHENTICATION_FAILED ||
rc == LIBSSH2_ERROR_PUBLICKEY_UNVERIFIED)
return GIT_EAUTH;
if (rc != LIBSSH2_ERROR_NONE) {
if (!git_error_last())
ssh_error(session, "Failed to authenticate SSH session");
return -1;
}
return 0;
}
static int request_creds(git_credential **out, ssh_subtransport *t, const char *user, int auth_methods)
{
int error, no_callback = 0;
git_credential *cred = NULL;
if (!t->owner->connect_opts.callbacks.credentials) {
no_callback = 1;
} else {
error = t->owner->connect_opts.callbacks.credentials(
&cred,
t->owner->url,
user,
auth_methods,
t->owner->connect_opts.callbacks.payload);
if (error == GIT_PASSTHROUGH) {
no_callback = 1;
} else if (error < 0) {
return error;
} else if (!cred) {
git_error_set(GIT_ERROR_SSH, "callback failed to initialize SSH credentials");
return -1;
}
}
if (no_callback) {
git_error_set(GIT_ERROR_SSH, "authentication required but no callback set");
return GIT_EAUTH;
}
if (!(cred->credtype & auth_methods)) {
cred->free(cred);
git_error_set(GIT_ERROR_SSH, "authentication callback returned unsupported credentials type");
return GIT_EAUTH;
}
*out = cred;
return 0;
}
#define SSH_DIR ".ssh"
#define KNOWN_HOSTS_FILE "known_hosts"
/*
* Load the known_hosts file.
*
* Returns success but leaves the output NULL if we couldn't find the file.
*/
static int load_known_hosts(LIBSSH2_KNOWNHOSTS **hosts, LIBSSH2_SESSION *session)
{
git_str path = GIT_STR_INIT, sshdir = GIT_STR_INIT;
LIBSSH2_KNOWNHOSTS *known_hosts = NULL;
int error;
GIT_ASSERT_ARG(hosts);
if ((error = git_sysdir_expand_homedir_file(&sshdir, SSH_DIR)) < 0 ||
(error = git_str_joinpath(&path, git_str_cstr(&sshdir), KNOWN_HOSTS_FILE)) < 0)
goto out;
if ((known_hosts = libssh2_knownhost_init(session)) == NULL) {
ssh_error(session, "error initializing known hosts");
error = -1;
goto out;
}
/*
* Try to read the file and consider not finding it as not trusting the
* host rather than an error.
*/
error = libssh2_knownhost_readfile(known_hosts, git_str_cstr(&path), LIBSSH2_KNOWNHOST_FILE_OPENSSH);
if (error == LIBSSH2_ERROR_FILE)
error = 0;
if (error < 0)
ssh_error(session, "error reading known_hosts");
out:
*hosts = known_hosts;
git_str_dispose(&sshdir);
git_str_dispose(&path);
return error;
}
static void add_hostkey_pref_if_avail(
LIBSSH2_KNOWNHOSTS *known_hosts,
const char *hostname,
int port,
git_str *prefs,
int type,
const char *type_name)
{
struct libssh2_knownhost *host = NULL;
const char key = '\0';
int mask = LIBSSH2_KNOWNHOST_TYPE_PLAIN | LIBSSH2_KNOWNHOST_KEYENC_RAW | type;
int error;
error = libssh2_knownhost_checkp(known_hosts, hostname, port, &key, 1, mask, &host);
if (error == LIBSSH2_KNOWNHOST_CHECK_MISMATCH) {
if (git_str_len(prefs) > 0) {
git_str_putc(prefs, ',');
}
git_str_puts(prefs, type_name);
}
}
/*
* We figure out what kind of key we want to ask the remote for by trying to
* look it up with a nonsense key and using that mismatch to figure out what key
* we do have stored for the host.
*
* Populates prefs with the string to pass to libssh2_session_method_pref.
*/
static void find_hostkey_preference(
LIBSSH2_KNOWNHOSTS *known_hosts,
const char *hostname,
int port,
git_str *prefs)
{
/*
* The order here is important as it indicates the priority of what will
* be preferred.
*/
#ifdef LIBSSH2_KNOWNHOST_KEY_ED25519
add_hostkey_pref_if_avail(known_hosts, hostname, port, prefs, LIBSSH2_KNOWNHOST_KEY_ED25519, "ssh-ed25519");
#endif
#ifdef LIBSSH2_KNOWNHOST_KEY_ECDSA_256
add_hostkey_pref_if_avail(known_hosts, hostname, port, prefs, LIBSSH2_KNOWNHOST_KEY_ECDSA_256, "ecdsa-sha2-nistp256");
add_hostkey_pref_if_avail(known_hosts, hostname, port, prefs, LIBSSH2_KNOWNHOST_KEY_ECDSA_384, "ecdsa-sha2-nistp384");
add_hostkey_pref_if_avail(known_hosts, hostname, port, prefs, LIBSSH2_KNOWNHOST_KEY_ECDSA_521, "ecdsa-sha2-nistp521");
#endif
add_hostkey_pref_if_avail(known_hosts, hostname, port, prefs, LIBSSH2_KNOWNHOST_KEY_SSHRSA, "ssh-rsa");
}
static int _git_ssh_session_create(
LIBSSH2_SESSION **session,
LIBSSH2_KNOWNHOSTS **hosts,
const char *hostname,
int port,
git_stream *io)
{
git_socket_stream *socket = GIT_CONTAINER_OF(io, git_socket_stream, parent);
LIBSSH2_SESSION *s;
LIBSSH2_KNOWNHOSTS *known_hosts;
git_str prefs = GIT_STR_INIT;
int rc = 0;
GIT_ASSERT_ARG(session);
GIT_ASSERT_ARG(hosts);
s = libssh2_session_init();
if (!s) {
git_error_set(GIT_ERROR_NET, "failed to initialize SSH session");
return -1;
}
if ((rc = load_known_hosts(&known_hosts, s)) < 0) {
ssh_error(s, "error loading known_hosts");
libssh2_session_free(s);
return -1;
}
find_hostkey_preference(known_hosts, hostname, port, &prefs);
if (git_str_len(&prefs) > 0) {
do {
rc = libssh2_session_method_pref(s, LIBSSH2_METHOD_HOSTKEY, git_str_cstr(&prefs));
} while (LIBSSH2_ERROR_EAGAIN == rc || LIBSSH2_ERROR_TIMEOUT == rc);
if (rc != LIBSSH2_ERROR_NONE) {
ssh_error(s, "failed to set hostkey preference");
goto on_error;
}
}
git_str_dispose(&prefs);
do {
rc = libssh2_session_handshake(s, socket->s);
} while (LIBSSH2_ERROR_EAGAIN == rc || LIBSSH2_ERROR_TIMEOUT == rc);
if (rc != LIBSSH2_ERROR_NONE) {
ssh_error(s, "failed to start SSH session");
goto on_error;
}
libssh2_session_set_blocking(s, 1);
*session = s;
*hosts = known_hosts;
return 0;
on_error:
libssh2_knownhost_free(known_hosts);
libssh2_session_free(s);
return -1;
}
/*
* Returns the typemask argument to pass to libssh2_knownhost_check{,p} based on
* the type of key that libssh2_session_hostkey returns.
*/
static int fingerprint_type_mask(int keytype)
{
int mask = LIBSSH2_KNOWNHOST_TYPE_PLAIN | LIBSSH2_KNOWNHOST_KEYENC_RAW;
return mask;
switch (keytype) {
case LIBSSH2_HOSTKEY_TYPE_RSA:
mask |= LIBSSH2_KNOWNHOST_KEY_SSHRSA;
break;
case LIBSSH2_HOSTKEY_TYPE_DSS:
mask |= LIBSSH2_KNOWNHOST_KEY_SSHDSS;
break;
#ifdef LIBSSH2_HOSTKEY_TYPE_ECDSA_256
case LIBSSH2_HOSTKEY_TYPE_ECDSA_256:
mask |= LIBSSH2_KNOWNHOST_KEY_ECDSA_256;
break;
case LIBSSH2_HOSTKEY_TYPE_ECDSA_384:
mask |= LIBSSH2_KNOWNHOST_KEY_ECDSA_384;
break;
case LIBSSH2_HOSTKEY_TYPE_ECDSA_521:
mask |= LIBSSH2_KNOWNHOST_KEY_ECDSA_521;
break;
#endif
#ifdef LIBSSH2_HOSTKEY_TYPE_ED25519
case LIBSSH2_HOSTKEY_TYPE_ED25519:
mask |= LIBSSH2_KNOWNHOST_KEY_ED25519;
break;
#endif
}
return mask;
}
/*
* Check the host against the user's known_hosts file.
*
* Returns 1/0 for valid/''not-valid or <0 for an error
*/
static int check_against_known_hosts(
LIBSSH2_SESSION *session,
LIBSSH2_KNOWNHOSTS *known_hosts,
const char *hostname,
int port,
const char *key,
size_t key_len,
int key_type)
{
int check, typemask, ret = 0;
struct libssh2_knownhost *host = NULL;
if (known_hosts == NULL)
return 0;
typemask = fingerprint_type_mask(key_type);
check = libssh2_knownhost_checkp(known_hosts, hostname, port, key, key_len, typemask, &host);
if (check == LIBSSH2_KNOWNHOST_CHECK_FAILURE) {
ssh_error(session, "error checking for known host");
return -1;
}
ret = check == LIBSSH2_KNOWNHOST_CHECK_MATCH ? 1 : 0;
return ret;
}
/*
* Perform the check for the session's certificate against known hosts if
* possible and then ask the user if they have a callback.
*
* Returns 1/0 for valid/not-valid or <0 for an error
*/
static int check_certificate(
LIBSSH2_SESSION *session,
LIBSSH2_KNOWNHOSTS *known_hosts,
git_transport_certificate_check_cb check_cb,
void *check_cb_payload,
const char *host,
int port)
{
git_cert_hostkey cert = {{ 0 }};
const char *key;
size_t cert_len;
int cert_type, cert_valid = 0, error = 0;
if ((key = libssh2_session_hostkey(session, &cert_len, &cert_type)) == NULL) {
ssh_error(session, "failed to retrieve hostkey");
return -1;
}
if ((cert_valid = check_against_known_hosts(session, known_hosts, host, port, key, cert_len, cert_type)) < 0)
return -1;
cert.parent.cert_type = GIT_CERT_HOSTKEY_LIBSSH2;
if (key != NULL) {
cert.type |= GIT_CERT_SSH_RAW;
cert.hostkey = key;
cert.hostkey_len = cert_len;
switch (cert_type) {
case LIBSSH2_HOSTKEY_TYPE_RSA:
cert.raw_type = GIT_CERT_SSH_RAW_TYPE_RSA;
break;
case LIBSSH2_HOSTKEY_TYPE_DSS:
cert.raw_type = GIT_CERT_SSH_RAW_TYPE_DSS;
break;
#ifdef LIBSSH2_HOSTKEY_TYPE_ECDSA_256
case LIBSSH2_HOSTKEY_TYPE_ECDSA_256:
cert.raw_type = GIT_CERT_SSH_RAW_TYPE_KEY_ECDSA_256;
break;
case LIBSSH2_HOSTKEY_TYPE_ECDSA_384:
cert.raw_type = GIT_CERT_SSH_RAW_TYPE_KEY_ECDSA_384;
break;
case LIBSSH2_KNOWNHOST_KEY_ECDSA_521:
cert.raw_type = GIT_CERT_SSH_RAW_TYPE_KEY_ECDSA_521;
break;
#endif
#ifdef LIBSSH2_HOSTKEY_TYPE_ED25519
case LIBSSH2_HOSTKEY_TYPE_ED25519:
cert.raw_type = GIT_CERT_SSH_RAW_TYPE_KEY_ED25519;
break;
#endif
default:
cert.raw_type = GIT_CERT_SSH_RAW_TYPE_UNKNOWN;
}
}
#ifdef LIBSSH2_HOSTKEY_HASH_SHA256
key = libssh2_hostkey_hash(session, LIBSSH2_HOSTKEY_HASH_SHA256);
if (key != NULL) {
cert.type |= GIT_CERT_SSH_SHA256;
memcpy(&cert.hash_sha256, key, 32);
}
#endif
key = libssh2_hostkey_hash(session, LIBSSH2_HOSTKEY_HASH_SHA1);
if (key != NULL) {
cert.type |= GIT_CERT_SSH_SHA1;
memcpy(&cert.hash_sha1, key, 20);
}
key = libssh2_hostkey_hash(session, LIBSSH2_HOSTKEY_HASH_MD5);
if (key != NULL) {
cert.type |= GIT_CERT_SSH_MD5;
memcpy(&cert.hash_md5, key, 16);
}
if (cert.type == 0) {
git_error_set(GIT_ERROR_SSH, "unable to get the host key");
return -1;
}
git_error_clear();
error = 0;
if (!cert_valid) {
git_error_set(GIT_ERROR_SSH, "invalid or unknown remote ssh hostkey");
error = GIT_ECERTIFICATE;
}
if (check_cb != NULL) {
git_cert_hostkey *cert_ptr = &cert;
git_error_state previous_error = {0};
git_error_state_capture(&previous_error, error);
error = check_cb((git_cert *) cert_ptr, cert_valid, host, check_cb_payload);
if (error == GIT_PASSTHROUGH) {
error = git_error_state_restore(&previous_error);
} else if (error < 0 && !git_error_last()) {
git_error_set(GIT_ERROR_NET, "unknown remote host key");
}
git_error_state_free(&previous_error);
}
return error;
}
#define SSH_DEFAULT_PORT "22"
static int _git_ssh_setup_conn(
ssh_subtransport *t,
const char *url,
const char *cmd,
git_smart_subtransport_stream **stream)
{
int auth_methods, error = 0, port;
ssh_stream *s;
git_credential *cred = NULL;
LIBSSH2_SESSION *session=NULL;
LIBSSH2_CHANNEL *channel=NULL;
LIBSSH2_KNOWNHOSTS *known_hosts = NULL;
t->current_stream = NULL;
*stream = NULL;
if (ssh_stream_alloc(t, cmd, stream) < 0)
return -1;
s = (ssh_stream *)*stream;
s->session = NULL;
s->channel = NULL;
if ((error = git_net_url_parse_standard_or_scp(&s->url, url)) < 0 ||
(error = git_socket_stream_new(&s->io, s->url.host, s->url.port)) < 0 ||
(error = git_stream_connect(s->io)) < 0)
goto done;
/*
* Try to parse the port as a number, if we can't then fall back to
* default. It would be nice if we could get the port that was resolved
* as part of the stream connection, but that's not something that's
* exposed.
*/
if (git__strntol32(&port, s->url.port, strlen(s->url.port), NULL, 10) < 0) {
git_error_set(GIT_ERROR_NET, "invalid port to ssh: %s", s->url.port);
error = -1;
goto done;
}
if ((error = _git_ssh_session_create(&session, &known_hosts, s->url.host, port, s->io)) < 0)
goto done;
if ((error = check_certificate(session, known_hosts, t->owner->connect_opts.callbacks.certificate_check, t->owner->connect_opts.callbacks.payload, s->url.host, port)) < 0)
goto done;
/* we need the username to ask for auth methods */
if (!s->url.username) {
if ((error = request_creds(&cred, t, NULL, GIT_CREDENTIAL_USERNAME)) < 0)
goto done;
s->url.username = git__strdup(((git_credential_username *) cred)->username);
cred->free(cred);
cred = NULL;
if (!s->url.username)
goto done;
} else if (s->url.username && s->url.password) {
if ((error = git_credential_userpass_plaintext_new(&cred, s->url.username, s->url.password)) < 0)
goto done;
}
if ((error = list_auth_methods(&auth_methods, session, s->url.username)) < 0)
goto done;
error = GIT_EAUTH;
/* if we already have something to try */
if (cred && auth_methods & cred->credtype)
error = _git_ssh_authenticate_session(session, cred);
while (error == GIT_EAUTH) {
if (cred) {
cred->free(cred);
cred = NULL;
}
if ((error = request_creds(&cred, t, s->url.username, auth_methods)) < 0)
goto done;
if (strcmp(s->url.username, git_credential_get_username(cred))) {
git_error_set(GIT_ERROR_SSH, "username does not match previous request");
error = -1;
goto done;
}
error = _git_ssh_authenticate_session(session, cred);
if (error == GIT_EAUTH) {
/* refresh auth methods */
if ((error = list_auth_methods(&auth_methods, session, s->url.username)) < 0)
goto done;
else
error = GIT_EAUTH;
}
}
if (error < 0)
goto done;
channel = libssh2_channel_open_session(session);
if (!channel) {
error = -1;
ssh_error(session, "Failed to open SSH channel");
goto done;
}
libssh2_channel_set_blocking(channel, 1);
s->session = session;
s->channel = channel;
t->current_stream = s;
done:
if (known_hosts)
libssh2_knownhost_free(known_hosts);
if (error < 0) {
ssh_stream_free(*stream);
if (session)
libssh2_session_free(session);
}
if (cred)
cred->free(cred);
return error;
}
static int ssh_uploadpack_ls(
ssh_subtransport *t,
const char *url,
git_smart_subtransport_stream **stream)
{
const char *cmd = t->cmd_uploadpack ? t->cmd_uploadpack : cmd_uploadpack;
return _git_ssh_setup_conn(t, url, cmd, stream);
}
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;
}
git_error_set(GIT_ERROR_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)
{
const char *cmd = t->cmd_receivepack ? t->cmd_receivepack : cmd_receivepack;
return _git_ssh_setup_conn(t, url, cmd, stream);
}
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;
}
git_error_set(GIT_ERROR_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 = GIT_CONTAINER_OF(subtransport, ssh_subtransport, parent);
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);
}
int git_smart_subtransport_ssh(
git_smart_subtransport **out,
git_transport *owner,
void *param)
{
#ifdef GIT_SSH_LIBSSH2
return git_smart_subtransport_ssh_libssh2(out, owner, param);
#elif GIT_SSH_EXEC
return git_smart_subtransport_ssh_exec(out, owner, param);
#else
GIT_UNUSED(out);
GIT_UNUSED(owner);
GIT_UNUSED(param);
*stream = NULL;
git_error_set(GIT_ERROR_INVALID, "cannot create SSH transport; library was built without SSH support");
return -1;
}
static int _ssh_close(git_smart_subtransport *subtransport)
{
ssh_subtransport *t = GIT_CONTAINER_OF(subtransport, ssh_subtransport, parent);
GIT_ASSERT(!t->current_stream);
GIT_UNUSED(t);
return 0;
}
static void _ssh_free(git_smart_subtransport *subtransport)
{
ssh_subtransport *t = GIT_CONTAINER_OF(subtransport, ssh_subtransport, parent);
git__free(t->cmd_uploadpack);
git__free(t->cmd_receivepack);
git__free(t);
}
#define SSH_AUTH_PUBLICKEY "publickey"
#define SSH_AUTH_PASSWORD "password"
#define SSH_AUTH_KEYBOARD_INTERACTIVE "keyboard-interactive"
static int list_auth_methods(int *out, LIBSSH2_SESSION *session, const char *username)
{
const char *list, *ptr;
*out = 0;
list = libssh2_userauth_list(session, username, strlen(username));
/* either error, or the remote accepts NONE auth, which is bizarre, let's punt */
if (list == NULL && !libssh2_userauth_authenticated(session)) {
ssh_error(session, "remote rejected authentication");
return GIT_EAUTH;
}
ptr = list;
while (ptr) {
if (*ptr == ',')
ptr++;
if (!git__prefixcmp(ptr, SSH_AUTH_PUBLICKEY)) {
*out |= GIT_CREDENTIAL_SSH_KEY;
*out |= GIT_CREDENTIAL_SSH_CUSTOM;
#ifdef GIT_SSH_MEMORY_CREDENTIALS
*out |= GIT_CREDENTIAL_SSH_MEMORY;
#endif
ptr += strlen(SSH_AUTH_PUBLICKEY);
continue;
}
if (!git__prefixcmp(ptr, SSH_AUTH_PASSWORD)) {
*out |= GIT_CREDENTIAL_USERPASS_PLAINTEXT;
ptr += strlen(SSH_AUTH_PASSWORD);
continue;
}
if (!git__prefixcmp(ptr, SSH_AUTH_KEYBOARD_INTERACTIVE)) {
*out |= GIT_CREDENTIAL_SSH_INTERACTIVE;
ptr += strlen(SSH_AUTH_KEYBOARD_INTERACTIVE);
continue;
}
/* Skip it if we don't know it */
ptr = strchr(ptr, ',');
}
return 0;
}
#endif
int git_smart_subtransport_ssh(
git_smart_subtransport **out, git_transport *owner, void *param)
static int transport_set_paths(git_transport *t, git_strarray *paths)
{
#ifdef GIT_SSH
ssh_subtransport *t;
GIT_ASSERT_ARG(out);
transport_smart *smart = (transport_smart *)t;
GIT_UNUSED(param);
t = git__calloc(sizeof(ssh_subtransport), 1);
GIT_ERROR_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;
#ifdef GIT_SSH_LIBSSH2
return git_smart_subtransport_ssh_libssh2_set_paths(
(git_smart_subtransport *)smart->wrapped,
paths->strings[0],
paths->strings[1]);
#elif GIT_SSH_EXEC
return git_smart_subtransport_ssh_exec_set_paths(
(git_smart_subtransport *)smart->wrapped,
paths->strings[0],
paths->strings[1]);
#else
GIT_UNUSED(owner);
GIT_UNUSED(param);
GIT_ASSERT_ARG(out);
*out = NULL;
GIT_UNUSED(t);
GIT_UNUSED(smart);
GIT_UNUSED(paths);
git_error_set(GIT_ERROR_INVALID, "cannot create SSH transport. Library was built without SSH support");
GIT_ASSERT(!"cannot create SSH library; library was built without SSH support");
return -1;
#endif
}
int git_transport_ssh_with_paths(git_transport **out, git_remote *owner, void *payload)
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 */
NULL,
NULL
};
if (paths->count != 2) {
......@@ -1099,49 +76,10 @@ int git_transport_ssh_with_paths(git_transport **out, git_remote *owner, void *p
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]);
GIT_ERROR_CHECK_ALLOC(t->cmd_uploadpack);
t->cmd_receivepack = git__strdup(paths->strings[1]);
GIT_ERROR_CHECK_ALLOC(t->cmd_receivepack);
if ((error = transport_set_paths(transport, paths)) < 0)
return error;
*out = transport;
return 0;
#else
GIT_UNUSED(owner);
GIT_UNUSED(payload);
GIT_ASSERT_ARG(out);
*out = NULL;
git_error_set(GIT_ERROR_INVALID, "cannot create SSH transport. Library was built without SSH support");
return -1;
#endif
}
#ifdef GIT_SSH
static void shutdown_ssh(void)
{
libssh2_exit();
}
#endif
int git_transport_ssh_global_init(void)
{
#ifdef GIT_SSH
if (libssh2_init(0) < 0) {
git_error_set(GIT_ERROR_SSH, "unable to initialize libssh2");
return -1;
}
return git_runtime_shutdown_register(shutdown_ssh);
#else
/* Nothing to initialize */
return 0;
#endif
}
/*
* 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 "ssh_exec.h"
#ifdef GIT_SSH_EXEC
#include "common.h"
#include "config.h"
#include "net.h"
#include "path.h"
#include "futils.h"
#include "process.h"
#include "transports/smart.h"
typedef struct {
git_smart_subtransport_stream parent;
} ssh_exec_subtransport_stream;
typedef struct {
git_smart_subtransport parent;
git_transport *owner;
ssh_exec_subtransport_stream *current_stream;
char *cmd_uploadpack;
char *cmd_receivepack;
git_smart_service_t action;
git_process *process;
} ssh_exec_subtransport;
static int ssh_exec_subtransport_stream_read(
git_smart_subtransport_stream *s,
char *buffer,
size_t buf_size,
size_t *bytes_read)
{
ssh_exec_subtransport *transport;
ssh_exec_subtransport_stream *stream = (ssh_exec_subtransport_stream *)s;
ssize_t ret;
GIT_ASSERT_ARG(stream);
GIT_ASSERT(stream->parent.subtransport);
transport = (ssh_exec_subtransport *)stream->parent.subtransport;
if ((ret = git_process_read(transport->process, buffer, buf_size)) < 0) {
return (int)ret;
}
*bytes_read = (size_t)ret;
return 0;
}
static int ssh_exec_subtransport_stream_write(
git_smart_subtransport_stream *s,
const char *buffer,
size_t len)
{
ssh_exec_subtransport *transport;
ssh_exec_subtransport_stream *stream = (ssh_exec_subtransport_stream *)s;
ssize_t ret;
GIT_ASSERT(stream && stream->parent.subtransport);
transport = (ssh_exec_subtransport *)stream->parent.subtransport;
while (len > 0) {
if ((ret = git_process_write(transport->process, buffer, len)) < 0)
return (int)ret;
len -= ret;
}
return 0;
}
static void ssh_exec_subtransport_stream_free(git_smart_subtransport_stream *s)
{
ssh_exec_subtransport_stream *stream = (ssh_exec_subtransport_stream *)s;
git__free(stream);
}
static int ssh_exec_subtransport_stream_init(
ssh_exec_subtransport_stream **out,
ssh_exec_subtransport *transport)
{
GIT_ASSERT_ARG(out);
*out = git__calloc(sizeof(ssh_exec_subtransport_stream), 1);
GIT_ERROR_CHECK_ALLOC(*out);
(*out)->parent.subtransport = &transport->parent;
(*out)->parent.read = ssh_exec_subtransport_stream_read;
(*out)->parent.write = ssh_exec_subtransport_stream_write;
(*out)->parent.free = ssh_exec_subtransport_stream_free;
return 0;
}
GIT_INLINE(int) ensure_transport_state(
ssh_exec_subtransport *transport,
git_smart_service_t expected,
git_smart_service_t next)
{
if (transport->action != expected && transport->action != next) {
git_error_set(GIT_ERROR_NET, "invalid transport state");
return -1;
}
return 0;
}
static int get_ssh_cmdline(
git_str *out,
ssh_exec_subtransport *transport,
git_net_url *url,
const char *command)
{
git_remote *remote = ((transport_smart *)transport->owner)->owner;
git_repository *repo = remote->repo;
git_config *cfg;
git_str ssh_cmd = GIT_STR_INIT;
const char *default_ssh_cmd = "ssh";
int error;
if ((error = git_repository_config_snapshot(&cfg, repo)) < 0)
return error;
if ((error = git__getenv(&ssh_cmd, "GIT_SSH")) == 0)
;
else if (error != GIT_ENOTFOUND)
goto done;
else if ((error = git_config__get_string_buf(&ssh_cmd, cfg, "core.sshcommand")) < 0 && error != GIT_ENOTFOUND)
goto done;
error = git_str_printf(out, "%s -p %s \"%s%s%s\" \"%s\" \"%s\"",
ssh_cmd.size > 0 ? ssh_cmd.ptr : default_ssh_cmd,
url->port,
url->username ? url->username : "",
url->username ? "@" : "",
url->host,
command,
url->path);
done:
git_str_dispose(&ssh_cmd);
git_config_free(cfg);
return error;
}
static int start_ssh(
ssh_exec_subtransport *transport,
git_smart_service_t action,
const char *sshpath)
{
const char *env[] = { "GIT_DIR=" };
git_process_options process_opts = GIT_PROCESS_OPTIONS_INIT;
git_net_url url = GIT_NET_URL_INIT;
git_str ssh_cmdline = GIT_STR_INIT;
const char *command;
int error;
process_opts.capture_in = 1;
process_opts.capture_out = 1;
process_opts.capture_err = 0;
switch (action) {
case GIT_SERVICE_UPLOADPACK_LS:
command = transport->cmd_uploadpack ?
transport->cmd_uploadpack : "git-upload-pack";
break;
case GIT_SERVICE_RECEIVEPACK_LS:
command = transport->cmd_receivepack ?
transport->cmd_receivepack : "git-receive-pack";
break;
default:
git_error_set(GIT_ERROR_NET, "invalid action");
error = -1;
goto done;
}
if (git_net_str_is_url(sshpath))
error = git_net_url_parse(&url, sshpath);
else
error = git_net_url_parse_scp(&url, sshpath);
if (error < 0)
goto done;
if ((error = get_ssh_cmdline(&ssh_cmdline, transport, &url, command)) < 0)
goto done;
if ((error = git_process_new_from_cmdline(&transport->process,
ssh_cmdline.ptr, env, ARRAY_SIZE(env), &process_opts)) < 0 ||
(error = git_process_start(transport->process)) < 0) {
git_process_free(transport->process);
transport->process = NULL;
goto done;
}
done:
git_str_dispose(&ssh_cmdline);
git_net_url_dispose(&url);
return error;
}
static int ssh_exec_subtransport_action(
git_smart_subtransport_stream **out,
git_smart_subtransport *t,
const char *sshpath,
git_smart_service_t action)
{
ssh_exec_subtransport *transport = (ssh_exec_subtransport *)t;
ssh_exec_subtransport_stream *stream = NULL;
git_smart_service_t expected;
int error;
switch (action) {
case GIT_SERVICE_UPLOADPACK_LS:
case GIT_SERVICE_RECEIVEPACK_LS:
if ((error = ensure_transport_state(transport, 0, 0)) < 0 ||
(error = ssh_exec_subtransport_stream_init(&stream, transport)) < 0 ||
(error = start_ssh(transport, action, sshpath)) < 0)
goto on_error;
transport->current_stream = stream;
break;
case GIT_SERVICE_UPLOADPACK:
case GIT_SERVICE_RECEIVEPACK:
expected = (action == GIT_SERVICE_UPLOADPACK) ?
GIT_SERVICE_UPLOADPACK_LS : GIT_SERVICE_RECEIVEPACK_LS;
if ((error = ensure_transport_state(transport, expected, action)) < 0)
goto on_error;
break;
default:
git_error_set(GIT_ERROR_INVALID, "invalid service request");
goto on_error;
}
transport->action = action;
*out = &transport->current_stream->parent;
return 0;
on_error:
if (stream != NULL)
ssh_exec_subtransport_stream_free(&stream->parent);
return -1;
}
static int ssh_exec_subtransport_close(git_smart_subtransport *t)
{
ssh_exec_subtransport *transport = (ssh_exec_subtransport *)t;
if (transport->process) {
git_process_close(transport->process);
git_process_free(transport->process);
transport->process = NULL;
}
transport->action = 0;
return 0;
}
static void ssh_exec_subtransport_free(git_smart_subtransport *t)
{
ssh_exec_subtransport *transport = (ssh_exec_subtransport *)t;
git__free(transport->cmd_uploadpack);
git__free(transport->cmd_receivepack);
git__free(transport);
}
int git_smart_subtransport_ssh_exec(
git_smart_subtransport **out,
git_transport *owner,
void *payload)
{
ssh_exec_subtransport *transport;
GIT_UNUSED(payload);
transport = git__calloc(sizeof(ssh_exec_subtransport), 1);
GIT_ERROR_CHECK_ALLOC(transport);
transport->owner = owner;
transport->parent.action = ssh_exec_subtransport_action;
transport->parent.close = ssh_exec_subtransport_close;
transport->parent.free = ssh_exec_subtransport_free;
*out = (git_smart_subtransport *) transport;
return 0;
}
int git_smart_subtransport_ssh_exec_set_paths(
git_smart_subtransport *subtransport,
const char *cmd_uploadpack,
const char *cmd_receivepack)
{
ssh_exec_subtransport *t = (ssh_exec_subtransport *)subtransport;
git__free(t->cmd_uploadpack);
git__free(t->cmd_receivepack);
t->cmd_uploadpack = git__strdup(cmd_uploadpack);
GIT_ERROR_CHECK_ALLOC(t->cmd_uploadpack);
t->cmd_receivepack = git__strdup(cmd_receivepack);
GIT_ERROR_CHECK_ALLOC(t->cmd_receivepack);
return 0;
}
#endif
......@@ -4,11 +4,23 @@
* This file is part of libgit2, distributed under the GNU GPL v2 with
* a Linking Exception. For full terms see the included COPYING file.
*/
#ifndef INCLUDE_transports_ssh_h__
#define INCLUDE_transports_ssh_h__
#ifndef INCLUDE_transports_ssh_exec_h__
#define INCLUDE_transports_ssh_exec_h__
#include "common.h"
int git_transport_ssh_global_init(void);
#include "git2.h"
#include "git2/transport.h"
#include "git2/sys/transport.h"
int git_smart_subtransport_ssh_exec(
git_smart_subtransport **out,
git_transport *owner,
void *param);
int git_smart_subtransport_ssh_exec_set_paths(
git_smart_subtransport *subtransport,
const char *cmd_uploadpack,
const char *cmd_receivepack);
#endif
/*
* 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 "ssh_libssh2.h"
#ifdef GIT_SSH_LIBSSH2
#include <libssh2.h>
#include "runtime.h"
#include "net.h"
#include "smart.h"
#include "streams/socket.h"
#include "sysdir.h"
#include "git2/credential.h"
#include "git2/sys/credential.h"
#define OWNING_SUBTRANSPORT(s) ((ssh_subtransport *)(s)->parent.subtransport)
static const char cmd_uploadpack[] = "git-upload-pack";
static const char cmd_receivepack[] = "git-receive-pack";
typedef struct {
git_smart_subtransport_stream parent;
git_stream *io;
LIBSSH2_SESSION *session;
LIBSSH2_CHANNEL *channel;
const char *cmd;
git_net_url url;
unsigned sent_command : 1;
} ssh_stream;
typedef struct {
git_smart_subtransport parent;
transport_smart *owner;
ssh_stream *current_stream;
git_credential *cred;
char *cmd_uploadpack;
char *cmd_receivepack;
} ssh_subtransport;
static int list_auth_methods(int *out, LIBSSH2_SESSION *session, const char *username);
static void ssh_error(LIBSSH2_SESSION *session, const char *errmsg)
{
char *ssherr;
libssh2_session_last_error(session, &ssherr, NULL, 0);
git_error_set(GIT_ERROR_SSH, "%s: %s", errmsg, ssherr);
}
/*
* Create a git protocol request.
*
* For example: git-upload-pack '/libgit2/libgit2'
*/
static int gen_proto(git_str *request, const char *cmd, git_net_url *url)
{
const char *repo;
repo = url->path;
if (repo && repo[0] == '/' && repo[1] == '~')
repo++;
if (!repo || !repo[0]) {
git_error_set(GIT_ERROR_NET, "malformed git protocol URL");
return -1;
}
git_str_puts(request, cmd);
git_str_puts(request, " '");
git_str_puts(request, repo);
git_str_puts(request, "'");
if (git_str_oom(request))
return -1;
return 0;
}
static int send_command(ssh_stream *s)
{
int error;
git_str request = GIT_STR_INIT;
error = gen_proto(&request, s->cmd, &s->url);
if (error < 0)
goto cleanup;
error = libssh2_channel_exec(s->channel, request.ptr);
if (error < LIBSSH2_ERROR_NONE) {
ssh_error(s->session, "SSH could not execute request");
goto cleanup;
}
s->sent_command = 1;
cleanup:
git_str_dispose(&request);
return error;
}
static int ssh_stream_read(
git_smart_subtransport_stream *stream,
char *buffer,
size_t buf_size,
size_t *bytes_read)
{
int rc;
ssh_stream *s = GIT_CONTAINER_OF(stream, ssh_stream, parent);
*bytes_read = 0;
if (!s->sent_command && send_command(s) < 0)
return -1;
if ((rc = libssh2_channel_read(s->channel, buffer, buf_size)) < LIBSSH2_ERROR_NONE) {
ssh_error(s->session, "SSH could not read data");
return -1;
}
/*
* If we can't get anything out of stdout, it's typically a
* not-found error, so read from stderr and signal EOF on
* stderr.
*/
if (rc == 0) {
if ((rc = libssh2_channel_read_stderr(s->channel, buffer, buf_size)) > 0) {
git_error_set(GIT_ERROR_SSH, "%*s", rc, buffer);
return GIT_EEOF;
} else if (rc < LIBSSH2_ERROR_NONE) {
ssh_error(s->session, "SSH could not read stderr");
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 = GIT_CONTAINER_OF(stream, ssh_stream, parent);
size_t off = 0;
ssize_t ret = 0;
if (!s->sent_command && send_command(s) < 0)
return -1;
do {
ret = libssh2_channel_write(s->channel, buffer + off, len - off);
if (ret < 0)
break;
off += ret;
} while (off < len);
if (ret < 0) {
ssh_error(s->session, "SSH could not write data");
return -1;
}
return 0;
}
static void ssh_stream_free(git_smart_subtransport_stream *stream)
{
ssh_stream *s = GIT_CONTAINER_OF(stream, ssh_stream, parent);
ssh_subtransport *t;
if (!stream)
return;
t = OWNING_SUBTRANSPORT(s);
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_disconnect(s->session, "closing transport");
libssh2_session_free(s->session);
s->session = NULL;
}
if (s->io) {
git_stream_close(s->io);
git_stream_free(s->io);
s->io = NULL;
}
git_net_url_dispose(&s->url);
git__free(s);
}
static int ssh_stream_alloc(
ssh_subtransport *t,
const char *cmd,
git_smart_subtransport_stream **stream)
{
ssh_stream *s;
GIT_ASSERT_ARG(stream);
s = git__calloc(sizeof(ssh_stream), 1);
GIT_ERROR_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;
*stream = &s->parent;
return 0;
}
static int ssh_agent_auth(LIBSSH2_SESSION *session, git_credential_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) {
rc = LIBSSH2_ERROR_AUTHENTICATION_FAILED;
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;
/* rc is set to 1 whenever the ssh agent ran out of keys to check.
* Set the error code to authentication failure rather than erroring
* out with an untranslatable error code.
*/
if (rc == 1) {
rc = LIBSSH2_ERROR_AUTHENTICATION_FAILED;
goto shutdown;
}
rc = libssh2_agent_userauth(agent, c->username, curr);
if (rc == 0)
break;
prev = curr;
}
shutdown:
if (rc != LIBSSH2_ERROR_NONE)
ssh_error(session, "error authenticating");
libssh2_agent_disconnect(agent);
libssh2_agent_free(agent);
return rc;
}
static int _git_ssh_authenticate_session(
LIBSSH2_SESSION *session,
git_credential *cred)
{
int rc;
do {
git_error_clear();
switch (cred->credtype) {
case GIT_CREDENTIAL_USERPASS_PLAINTEXT: {
git_credential_userpass_plaintext *c = (git_credential_userpass_plaintext *)cred;
rc = libssh2_userauth_password(session, c->username, c->password);
break;
}
case GIT_CREDENTIAL_SSH_KEY: {
git_credential_ssh_key *c = (git_credential_ssh_key *)cred;
if (c->privatekey)
rc = libssh2_userauth_publickey_fromfile(
session, c->username, c->publickey,
c->privatekey, c->passphrase);
else
rc = ssh_agent_auth(session, c);
break;
}
case GIT_CREDENTIAL_SSH_CUSTOM: {
git_credential_ssh_custom *c = (git_credential_ssh_custom *)cred;
rc = libssh2_userauth_publickey(
session, c->username, (const unsigned char *)c->publickey,
c->publickey_len, c->sign_callback, &c->payload);
break;
}
case GIT_CREDENTIAL_SSH_INTERACTIVE: {
void **abstract = libssh2_session_abstract(session);
git_credential_ssh_interactive *c = (git_credential_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;
}
#ifdef GIT_SSH_LIBSSH2_MEMORY_CREDENTIALS
case GIT_CREDENTIAL_SSH_MEMORY: {
git_credential_ssh_key *c = (git_credential_ssh_key *)cred;
GIT_ASSERT(c->username);
GIT_ASSERT(c->privatekey);
rc = libssh2_userauth_publickey_frommemory(
session,
c->username,
strlen(c->username),
c->publickey,
c->publickey ? strlen(c->publickey) : 0,
c->privatekey,
strlen(c->privatekey),
c->passphrase);
break;
}
#endif
default:
rc = LIBSSH2_ERROR_AUTHENTICATION_FAILED;
}
} while (LIBSSH2_ERROR_EAGAIN == rc || LIBSSH2_ERROR_TIMEOUT == rc);
if (rc == LIBSSH2_ERROR_PASSWORD_EXPIRED ||
rc == LIBSSH2_ERROR_AUTHENTICATION_FAILED ||
rc == LIBSSH2_ERROR_PUBLICKEY_UNVERIFIED)
return GIT_EAUTH;
if (rc != LIBSSH2_ERROR_NONE) {
if (!git_error_last())
ssh_error(session, "Failed to authenticate SSH session");
return -1;
}
return 0;
}
static int request_creds(git_credential **out, ssh_subtransport *t, const char *user, int auth_methods)
{
int error, no_callback = 0;
git_credential *cred = NULL;
if (!t->owner->connect_opts.callbacks.credentials) {
no_callback = 1;
} else {
error = t->owner->connect_opts.callbacks.credentials(
&cred,
t->owner->url,
user,
auth_methods,
t->owner->connect_opts.callbacks.payload);
if (error == GIT_PASSTHROUGH) {
no_callback = 1;
} else if (error < 0) {
return error;
} else if (!cred) {
git_error_set(GIT_ERROR_SSH, "callback failed to initialize SSH credentials");
return -1;
}
}
if (no_callback) {
git_error_set(GIT_ERROR_SSH, "authentication required but no callback set");
return GIT_EAUTH;
}
if (!(cred->credtype & auth_methods)) {
cred->free(cred);
git_error_set(GIT_ERROR_SSH, "authentication callback returned unsupported credentials type");
return GIT_EAUTH;
}
*out = cred;
return 0;
}
#define SSH_DIR ".ssh"
#define KNOWN_HOSTS_FILE "known_hosts"
/*
* Load the known_hosts file.
*
* Returns success but leaves the output NULL if we couldn't find the file.
*/
static int load_known_hosts(LIBSSH2_KNOWNHOSTS **hosts, LIBSSH2_SESSION *session)
{
git_str path = GIT_STR_INIT, sshdir = GIT_STR_INIT;
LIBSSH2_KNOWNHOSTS *known_hosts = NULL;
int error;
GIT_ASSERT_ARG(hosts);
if ((error = git_sysdir_expand_homedir_file(&sshdir, SSH_DIR)) < 0 ||
(error = git_str_joinpath(&path, git_str_cstr(&sshdir), KNOWN_HOSTS_FILE)) < 0)
goto out;
if ((known_hosts = libssh2_knownhost_init(session)) == NULL) {
ssh_error(session, "error initializing known hosts");
error = -1;
goto out;
}
/*
* Try to read the file and consider not finding it as not trusting the
* host rather than an error.
*/
error = libssh2_knownhost_readfile(known_hosts, git_str_cstr(&path), LIBSSH2_KNOWNHOST_FILE_OPENSSH);
if (error == LIBSSH2_ERROR_FILE)
error = 0;
if (error < 0)
ssh_error(session, "error reading known_hosts");
out:
*hosts = known_hosts;
git_str_dispose(&sshdir);
git_str_dispose(&path);
return error;
}
static void add_hostkey_pref_if_avail(
LIBSSH2_KNOWNHOSTS *known_hosts,
const char *hostname,
int port,
git_str *prefs,
int type,
const char *type_name)
{
struct libssh2_knownhost *host = NULL;
const char key = '\0';
int mask = LIBSSH2_KNOWNHOST_TYPE_PLAIN | LIBSSH2_KNOWNHOST_KEYENC_RAW | type;
int error;
error = libssh2_knownhost_checkp(known_hosts, hostname, port, &key, 1, mask, &host);
if (error == LIBSSH2_KNOWNHOST_CHECK_MISMATCH) {
if (git_str_len(prefs) > 0) {
git_str_putc(prefs, ',');
}
git_str_puts(prefs, type_name);
}
}
/*
* We figure out what kind of key we want to ask the remote for by trying to
* look it up with a nonsense key and using that mismatch to figure out what key
* we do have stored for the host.
*
* Populates prefs with the string to pass to libssh2_session_method_pref.
*/
static void find_hostkey_preference(
LIBSSH2_KNOWNHOSTS *known_hosts,
const char *hostname,
int port,
git_str *prefs)
{
/*
* The order here is important as it indicates the priority of what will
* be preferred.
*/
#ifdef LIBSSH2_KNOWNHOST_KEY_ED25519
add_hostkey_pref_if_avail(known_hosts, hostname, port, prefs, LIBSSH2_KNOWNHOST_KEY_ED25519, "ssh-ed25519");
#endif
#ifdef LIBSSH2_KNOWNHOST_KEY_ECDSA_256
add_hostkey_pref_if_avail(known_hosts, hostname, port, prefs, LIBSSH2_KNOWNHOST_KEY_ECDSA_256, "ecdsa-sha2-nistp256");
add_hostkey_pref_if_avail(known_hosts, hostname, port, prefs, LIBSSH2_KNOWNHOST_KEY_ECDSA_384, "ecdsa-sha2-nistp384");
add_hostkey_pref_if_avail(known_hosts, hostname, port, prefs, LIBSSH2_KNOWNHOST_KEY_ECDSA_521, "ecdsa-sha2-nistp521");
#endif
add_hostkey_pref_if_avail(known_hosts, hostname, port, prefs, LIBSSH2_KNOWNHOST_KEY_SSHRSA, "ssh-rsa");
}
static int _git_ssh_session_create(
LIBSSH2_SESSION **session,
LIBSSH2_KNOWNHOSTS **hosts,
const char *hostname,
int port,
git_stream *io)
{
git_socket_stream *socket = GIT_CONTAINER_OF(io, git_socket_stream, parent);
LIBSSH2_SESSION *s;
LIBSSH2_KNOWNHOSTS *known_hosts;
git_str prefs = GIT_STR_INIT;
int rc = 0;
GIT_ASSERT_ARG(session);
GIT_ASSERT_ARG(hosts);
s = libssh2_session_init();
if (!s) {
git_error_set(GIT_ERROR_NET, "failed to initialize SSH session");
return -1;
}
if ((rc = load_known_hosts(&known_hosts, s)) < 0) {
ssh_error(s, "error loading known_hosts");
libssh2_session_free(s);
return -1;
}
find_hostkey_preference(known_hosts, hostname, port, &prefs);
if (git_str_len(&prefs) > 0) {
do {
rc = libssh2_session_method_pref(s, LIBSSH2_METHOD_HOSTKEY, git_str_cstr(&prefs));
} while (LIBSSH2_ERROR_EAGAIN == rc || LIBSSH2_ERROR_TIMEOUT == rc);
if (rc != LIBSSH2_ERROR_NONE) {
ssh_error(s, "failed to set hostkey preference");
goto on_error;
}
}
git_str_dispose(&prefs);
do {
rc = libssh2_session_handshake(s, socket->s);
} while (LIBSSH2_ERROR_EAGAIN == rc || LIBSSH2_ERROR_TIMEOUT == rc);
if (rc != LIBSSH2_ERROR_NONE) {
ssh_error(s, "failed to start SSH session");
goto on_error;
}
libssh2_session_set_blocking(s, 1);
*session = s;
*hosts = known_hosts;
return 0;
on_error:
libssh2_knownhost_free(known_hosts);
libssh2_session_free(s);
return -1;
}
/*
* Returns the typemask argument to pass to libssh2_knownhost_check{,p} based on
* the type of key that libssh2_session_hostkey returns.
*/
static int fingerprint_type_mask(int keytype)
{
int mask = LIBSSH2_KNOWNHOST_TYPE_PLAIN | LIBSSH2_KNOWNHOST_KEYENC_RAW;
return mask;
switch (keytype) {
case LIBSSH2_HOSTKEY_TYPE_RSA:
mask |= LIBSSH2_KNOWNHOST_KEY_SSHRSA;
break;
case LIBSSH2_HOSTKEY_TYPE_DSS:
mask |= LIBSSH2_KNOWNHOST_KEY_SSHDSS;
break;
#ifdef LIBSSH2_HOSTKEY_TYPE_ECDSA_256
case LIBSSH2_HOSTKEY_TYPE_ECDSA_256:
mask |= LIBSSH2_KNOWNHOST_KEY_ECDSA_256;
break;
case LIBSSH2_HOSTKEY_TYPE_ECDSA_384:
mask |= LIBSSH2_KNOWNHOST_KEY_ECDSA_384;
break;
case LIBSSH2_HOSTKEY_TYPE_ECDSA_521:
mask |= LIBSSH2_KNOWNHOST_KEY_ECDSA_521;
break;
#endif
#ifdef LIBSSH2_HOSTKEY_TYPE_ED25519
case LIBSSH2_HOSTKEY_TYPE_ED25519:
mask |= LIBSSH2_KNOWNHOST_KEY_ED25519;
break;
#endif
}
return mask;
}
/*
* Check the host against the user's known_hosts file.
*
* Returns 1/0 for valid/''not-valid or <0 for an error
*/
static int check_against_known_hosts(
LIBSSH2_SESSION *session,
LIBSSH2_KNOWNHOSTS *known_hosts,
const char *hostname,
int port,
const char *key,
size_t key_len,
int key_type)
{
int check, typemask, ret = 0;
struct libssh2_knownhost *host = NULL;
if (known_hosts == NULL)
return 0;
typemask = fingerprint_type_mask(key_type);
check = libssh2_knownhost_checkp(known_hosts, hostname, port, key, key_len, typemask, &host);
if (check == LIBSSH2_KNOWNHOST_CHECK_FAILURE) {
ssh_error(session, "error checking for known host");
return -1;
}
ret = check == LIBSSH2_KNOWNHOST_CHECK_MATCH ? 1 : 0;
return ret;
}
/*
* Perform the check for the session's certificate against known hosts if
* possible and then ask the user if they have a callback.
*
* Returns 1/0 for valid/not-valid or <0 for an error
*/
static int check_certificate(
LIBSSH2_SESSION *session,
LIBSSH2_KNOWNHOSTS *known_hosts,
git_transport_certificate_check_cb check_cb,
void *check_cb_payload,
const char *host,
int port)
{
git_cert_hostkey cert = {{ 0 }};
const char *key;
size_t cert_len;
int cert_type, cert_valid = 0, error = 0;
if ((key = libssh2_session_hostkey(session, &cert_len, &cert_type)) == NULL) {
ssh_error(session, "failed to retrieve hostkey");
return -1;
}
if ((cert_valid = check_against_known_hosts(session, known_hosts, host, port, key, cert_len, cert_type)) < 0)
return -1;
cert.parent.cert_type = GIT_CERT_HOSTKEY_LIBSSH2;
if (key != NULL) {
cert.type |= GIT_CERT_SSH_RAW;
cert.hostkey = key;
cert.hostkey_len = cert_len;
switch (cert_type) {
case LIBSSH2_HOSTKEY_TYPE_RSA:
cert.raw_type = GIT_CERT_SSH_RAW_TYPE_RSA;
break;
case LIBSSH2_HOSTKEY_TYPE_DSS:
cert.raw_type = GIT_CERT_SSH_RAW_TYPE_DSS;
break;
#ifdef LIBSSH2_HOSTKEY_TYPE_ECDSA_256
case LIBSSH2_HOSTKEY_TYPE_ECDSA_256:
cert.raw_type = GIT_CERT_SSH_RAW_TYPE_KEY_ECDSA_256;
break;
case LIBSSH2_HOSTKEY_TYPE_ECDSA_384:
cert.raw_type = GIT_CERT_SSH_RAW_TYPE_KEY_ECDSA_384;
break;
case LIBSSH2_KNOWNHOST_KEY_ECDSA_521:
cert.raw_type = GIT_CERT_SSH_RAW_TYPE_KEY_ECDSA_521;
break;
#endif
#ifdef LIBSSH2_HOSTKEY_TYPE_ED25519
case LIBSSH2_HOSTKEY_TYPE_ED25519:
cert.raw_type = GIT_CERT_SSH_RAW_TYPE_KEY_ED25519;
break;
#endif
default:
cert.raw_type = GIT_CERT_SSH_RAW_TYPE_UNKNOWN;
}
}
#ifdef LIBSSH2_HOSTKEY_HASH_SHA256
key = libssh2_hostkey_hash(session, LIBSSH2_HOSTKEY_HASH_SHA256);
if (key != NULL) {
cert.type |= GIT_CERT_SSH_SHA256;
memcpy(&cert.hash_sha256, key, 32);
}
#endif
key = libssh2_hostkey_hash(session, LIBSSH2_HOSTKEY_HASH_SHA1);
if (key != NULL) {
cert.type |= GIT_CERT_SSH_SHA1;
memcpy(&cert.hash_sha1, key, 20);
}
key = libssh2_hostkey_hash(session, LIBSSH2_HOSTKEY_HASH_MD5);
if (key != NULL) {
cert.type |= GIT_CERT_SSH_MD5;
memcpy(&cert.hash_md5, key, 16);
}
if (cert.type == 0) {
git_error_set(GIT_ERROR_SSH, "unable to get the host key");
return -1;
}
git_error_clear();
error = 0;
if (!cert_valid) {
git_error_set(GIT_ERROR_SSH, "invalid or unknown remote ssh hostkey");
error = GIT_ECERTIFICATE;
}
if (check_cb != NULL) {
git_cert_hostkey *cert_ptr = &cert;
git_error_state previous_error = {0};
git_error_state_capture(&previous_error, error);
error = check_cb((git_cert *) cert_ptr, cert_valid, host, check_cb_payload);
if (error == GIT_PASSTHROUGH) {
error = git_error_state_restore(&previous_error);
} else if (error < 0 && !git_error_last()) {
git_error_set(GIT_ERROR_NET, "unknown remote host key");
}
git_error_state_free(&previous_error);
}
return error;
}
#define SSH_DEFAULT_PORT "22"
static int _git_ssh_setup_conn(
ssh_subtransport *t,
const char *url,
const char *cmd,
git_smart_subtransport_stream **stream)
{
int auth_methods, error = 0, port;
ssh_stream *s;
git_credential *cred = NULL;
LIBSSH2_SESSION *session=NULL;
LIBSSH2_CHANNEL *channel=NULL;
LIBSSH2_KNOWNHOSTS *known_hosts = NULL;
t->current_stream = NULL;
*stream = NULL;
if (ssh_stream_alloc(t, cmd, stream) < 0)
return -1;
s = (ssh_stream *)*stream;
s->session = NULL;
s->channel = NULL;
if (git_net_str_is_url(url))
error = git_net_url_parse(&s->url, url);
else
error = git_net_url_parse_scp(&s->url, url);
if (error < 0)
goto done;
if ((error = git_socket_stream_new(&s->io, s->url.host, s->url.port)) < 0 ||
(error = git_stream_connect(s->io)) < 0)
goto done;
/*
* Try to parse the port as a number, if we can't then fall back to
* default. It would be nice if we could get the port that was resolved
* as part of the stream connection, but that's not something that's
* exposed.
*/
if (git__strntol32(&port, s->url.port, strlen(s->url.port), NULL, 10) < 0)
port = -1;
if ((error = _git_ssh_session_create(&session, &known_hosts, s->url.host, port, s->io)) < 0)
goto done;
if ((error = check_certificate(session, known_hosts, t->owner->connect_opts.callbacks.certificate_check, t->owner->connect_opts.callbacks.payload, s->url.host, port)) < 0)
goto done;
/* we need the username to ask for auth methods */
if (!s->url.username) {
if ((error = request_creds(&cred, t, NULL, GIT_CREDENTIAL_USERNAME)) < 0)
goto done;
s->url.username = git__strdup(((git_credential_username *) cred)->username);
cred->free(cred);
cred = NULL;
if (!s->url.username)
goto done;
} else if (s->url.username && s->url.password) {
if ((error = git_credential_userpass_plaintext_new(&cred, s->url.username, s->url.password)) < 0)
goto done;
}
if ((error = list_auth_methods(&auth_methods, session, s->url.username)) < 0)
goto done;
error = GIT_EAUTH;
/* if we already have something to try */
if (cred && auth_methods & cred->credtype)
error = _git_ssh_authenticate_session(session, cred);
while (error == GIT_EAUTH) {
if (cred) {
cred->free(cred);
cred = NULL;
}
if ((error = request_creds(&cred, t, s->url.username, auth_methods)) < 0)
goto done;
if (strcmp(s->url.username, git_credential_get_username(cred))) {
git_error_set(GIT_ERROR_SSH, "username does not match previous request");
error = -1;
goto done;
}
error = _git_ssh_authenticate_session(session, cred);
if (error == GIT_EAUTH) {
/* refresh auth methods */
if ((error = list_auth_methods(&auth_methods, session, s->url.username)) < 0)
goto done;
else
error = GIT_EAUTH;
}
}
if (error < 0)
goto done;
channel = libssh2_channel_open_session(session);
if (!channel) {
error = -1;
ssh_error(session, "Failed to open SSH channel");
goto done;
}
libssh2_channel_set_blocking(channel, 1);
s->session = session;
s->channel = channel;
t->current_stream = s;
done:
if (known_hosts)
libssh2_knownhost_free(known_hosts);
if (error < 0) {
ssh_stream_free(*stream);
if (session)
libssh2_session_free(session);
}
if (cred)
cred->free(cred);
return error;
}
static int ssh_uploadpack_ls(
ssh_subtransport *t,
const char *url,
git_smart_subtransport_stream **stream)
{
const char *cmd = t->cmd_uploadpack ? t->cmd_uploadpack : cmd_uploadpack;
return _git_ssh_setup_conn(t, url, cmd, stream);
}
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;
}
git_error_set(GIT_ERROR_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)
{
const char *cmd = t->cmd_receivepack ? t->cmd_receivepack : cmd_receivepack;
return _git_ssh_setup_conn(t, url, cmd, stream);
}
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;
}
git_error_set(GIT_ERROR_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 = GIT_CONTAINER_OF(subtransport, ssh_subtransport, parent);
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 = GIT_CONTAINER_OF(subtransport, ssh_subtransport, parent);
GIT_ASSERT(!t->current_stream);
GIT_UNUSED(t);
return 0;
}
static void _ssh_free(git_smart_subtransport *subtransport)
{
ssh_subtransport *t = GIT_CONTAINER_OF(subtransport, ssh_subtransport, parent);
git__free(t->cmd_uploadpack);
git__free(t->cmd_receivepack);
git__free(t);
}
#define SSH_AUTH_PUBLICKEY "publickey"
#define SSH_AUTH_PASSWORD "password"
#define SSH_AUTH_KEYBOARD_INTERACTIVE "keyboard-interactive"
static int list_auth_methods(int *out, LIBSSH2_SESSION *session, const char *username)
{
const char *list, *ptr;
*out = 0;
list = libssh2_userauth_list(session, username, strlen(username));
/* either error, or the remote accepts NONE auth, which is bizarre, let's punt */
if (list == NULL && !libssh2_userauth_authenticated(session)) {
ssh_error(session, "remote rejected authentication");
return GIT_EAUTH;
}
ptr = list;
while (ptr) {
if (*ptr == ',')
ptr++;
if (!git__prefixcmp(ptr, SSH_AUTH_PUBLICKEY)) {
*out |= GIT_CREDENTIAL_SSH_KEY;
*out |= GIT_CREDENTIAL_SSH_CUSTOM;
#ifdef GIT_SSH_LIBSSH2_MEMORY_CREDENTIALS
*out |= GIT_CREDENTIAL_SSH_MEMORY;
#endif
ptr += strlen(SSH_AUTH_PUBLICKEY);
continue;
}
if (!git__prefixcmp(ptr, SSH_AUTH_PASSWORD)) {
*out |= GIT_CREDENTIAL_USERPASS_PLAINTEXT;
ptr += strlen(SSH_AUTH_PASSWORD);
continue;
}
if (!git__prefixcmp(ptr, SSH_AUTH_KEYBOARD_INTERACTIVE)) {
*out |= GIT_CREDENTIAL_SSH_INTERACTIVE;
ptr += strlen(SSH_AUTH_KEYBOARD_INTERACTIVE);
continue;
}
/* Skip it if we don't know it */
ptr = strchr(ptr, ',');
}
return 0;
}
int git_smart_subtransport_ssh_libssh2(
git_smart_subtransport **out,
git_transport *owner,
void *param)
{
ssh_subtransport *t;
GIT_ASSERT_ARG(out);
GIT_UNUSED(param);
t = git__calloc(sizeof(ssh_subtransport), 1);
GIT_ERROR_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;
}
int git_smart_subtransport_ssh_libssh2_set_paths(
git_smart_subtransport *subtransport,
const char *cmd_uploadpack,
const char *cmd_receivepack)
{
ssh_subtransport *t = (ssh_subtransport *)subtransport;
git__free(t->cmd_uploadpack);
git__free(t->cmd_receivepack);
t->cmd_uploadpack = git__strdup(cmd_uploadpack);
GIT_ERROR_CHECK_ALLOC(t->cmd_uploadpack);
t->cmd_receivepack = git__strdup(cmd_receivepack);
GIT_ERROR_CHECK_ALLOC(t->cmd_receivepack);
return 0;
}
static void shutdown_libssh2(void)
{
libssh2_exit();
}
int git_transport_ssh_libssh2_global_init(void)
{
if (libssh2_init(0) < 0) {
git_error_set(GIT_ERROR_SSH, "unable to initialize libssh2");
return -1;
}
return git_runtime_shutdown_register(shutdown_libssh2);
}
#else /* GIT_SSH */
int git_transport_ssh_libssh2_global_init(void)
{
return 0;
}
#endif
/*
* 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.
*/
#ifndef INCLUDE_transports_libssh2_h__
#define INCLUDE_transports_libssh2_h__
#include "common.h"
#include "git2.h"
#include "git2/transport.h"
#include "git2/sys/transport.h"
int git_transport_ssh_libssh2_global_init(void);
int git_smart_subtransport_ssh_libssh2(
git_smart_subtransport **out,
git_transport *owner,
void *param);
int git_smart_subtransport_ssh_libssh2_set_paths(
git_smart_subtransport *subtransport,
const char *cmd_uploadpack,
const char *cmd_receivepack);
#endif
......@@ -30,7 +30,9 @@
#cmakedefine GIT_QSORT_MSC
#cmakedefine GIT_SSH 1
#cmakedefine GIT_SSH_MEMORY_CREDENTIALS 1
#cmakedefine GIT_SSH_EXEC 1
#cmakedefine GIT_SSH_LIBSSH2 1
#cmakedefine GIT_SSH_LIBSSH2_MEMORY_CREDENTIALS 1
#cmakedefine GIT_NTLM 1
#cmakedefine GIT_GSSAPI 1
......
/*
* 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.
*/
#ifndef INCLUDE_process_h__
#define INCLUDE_process_h__
typedef struct git_process git_process;
typedef struct {
int capture_in : 1,
capture_out : 1,
capture_err : 1,
exclude_env : 1;
char *cwd;
} git_process_options;
typedef enum {
GIT_PROCESS_STATUS_NONE,
GIT_PROCESS_STATUS_NORMAL,
GIT_PROCESS_STATUS_ERROR
} git_process_result_status;
#define GIT_PROCESS_RESULT_INIT { GIT_PROCESS_STATUS_NONE }
typedef struct {
git_process_result_status status;
int exitcode;
int signal;
} git_process_result;
#define GIT_PROCESS_OPTIONS_INIT { 0 }
#ifdef GIT_WIN32
# define p_pid_t DWORD
#else
# define p_pid_t pid_t
#endif
/**
* Create a new process. The command to run should be specified as the
* element of the `arg` array, execv-style. This should be the full path
* to the command to run, the PATH is not obeyed.
*
* This function will add the given environment variables (in `env`)
* to the current environment. Operations on environment variables
* are not thread safe, so you may not modify the environment during
* this call. You can avoid this by setting `exclude_env` in the
* options and providing the entire environment yourself.
*
* @param out location to store the process
* @param args the command (with arguments) to run
* @param args_len the length of the args array
* @param env environment variables to add (or NULL)
* @param env_len the length of the env len
* @param opts the options for creating the process
* @return 0 or an error code
*/
extern int git_process_new(
git_process **out,
const char **args,
size_t args_len,
const char **env,
size_t env_len,
git_process_options *opts);
/**
* Create a new process. The command to run should be specified as the
* `cmdline` option - which is the full text of the command line as it
* would be specified or run by a user. The command to run will be
* looked up in the PATH.
*
* On Unix, this will be executed by the system's shell (`/bin/sh`)
* and may contain _Bourne-style_ shell quoting rules. On Windows,
* this will be passed to `CreateProcess`, and similarly, may
* contain _Windows-style_ shell quoting rules.
*
* This function will add the given environment variables (in `env`)
* to the current environment. Operations on environment variables
* are not thread safe, so you may not modify the environment during
* this call. You can avoid this by setting `exclude_env` in the
* options and providing the entire environment yourself.
*/
extern int git_process_new_from_cmdline(
git_process **out,
const char *cmdline,
const char **env,
size_t env_len,
git_process_options *opts);
#ifdef GIT_WIN32
extern int git_process__appname(
git_str *out,
const char *cmdline);
/* Windows path parsing is tricky; this helper function is for testing. */
extern int git_process__cmdline(
git_str *out,
const char **in,
size_t in_len);
#endif
/**
* Start the process.
*
* @param process the process to start
* @return 0 or an error code
*/
extern int git_process_start(git_process *process);
/**
* Returns the process id of the process.
*
* @param out pointer to a pid_t to store the process id
* @param process the process to query
* @return 0 or an error code
*/
extern int git_process_id(p_pid_t *out, git_process *process);
/**
* Read from the process's stdout. The process must have been created with
* `capture_out` set to true.
*
* @param process the process to read from
* @param buf the buf to read into
* @param count maximum number of bytes to read
* @return number of bytes read or an error code
*/
extern ssize_t git_process_read(git_process *process, void *buf, size_t count);
/**
* Read from the process's stderr. The process must have been created with
* `capture_err` set to true.
*
* @param process the process to read from
* @param buf the buf to read into
* @param count maximum number of bytes to read
* @return number of bytes read or an error code
*/
extern ssize_t git_process_read_err(git_process *process, void *buf, size_t count);
/**
* Write to the process's stdin. The process must have been created with
* `capture_in` set to true.
*
* @param process the process to write to
* @param buf the buf to write
* @param count maximum number of bytes to write
* @return number of bytes written or an error code
*/
extern ssize_t git_process_write(git_process *process, const void *buf, size_t count);
/**
* Wait for the process to finish.
*
* @param result the result of the process or NULL
* @param process the process to wait on
*/
extern int git_process_wait(git_process_result *result, git_process *process);
/**
* Close the input pipe from the child.
*
* @param process the process to close the pipe on
*/
extern int git_process_close_in(git_process *process);
/**
* Close the output pipe from the child.
*
* @param process the process to close the pipe on
*/
extern int git_process_close_out(git_process *process);
/**
* Close the error pipe from the child.
*
* @param process the process to close the pipe on
*/
extern int git_process_close_err(git_process *process);
/**
* Close all resources that are used by the process. This does not
* wait for the process to complete.
*
* @parma process the process to close
*/
extern int git_process_close(git_process *process);
/**
* Place a human-readable error message in the given git buffer.
*
* @param msg the buffer to store the message
* @param result the process result that produced an error
*/
extern int git_process_result_msg(git_str *msg, git_process_result *result);
/**
* Free a process structure
*
* @param process the process to free
*/
extern void git_process_free(git_process *process);
#endif
......@@ -28,6 +28,59 @@ int git_strlist_copy(char ***out, const char **in, size_t len)
return 0;
}
int git_strlist_copy_with_null(char ***out, const char **in, size_t len)
{
char **dup;
size_t new_len, i;
GIT_ERROR_CHECK_ALLOC_ADD(&new_len, len, 1);
dup = git__calloc(new_len, sizeof(char *));
GIT_ERROR_CHECK_ALLOC(dup);
for (i = 0; i < len; i++) {
dup[i] = git__strdup(in[i]);
GIT_ERROR_CHECK_ALLOC(dup[i]);
}
*out = dup;
return 0;
}
bool git_strlist_contains_prefix(
const char **strings,
size_t len,
const char *str,
size_t n)
{
size_t i;
for (i = 0; i < len; i++) {
if (strncmp(strings[i], str, n) == 0)
return true;
}
return false;
}
bool git_strlist_contains_key(
const char **strings,
size_t len,
const char *key,
char delimiter)
{
const char *c;
for (c = key; *c; c++) {
if (*c == delimiter)
break;
}
return *c ?
git_strlist_contains_prefix(strings, len, key, (c - key)) :
false;
}
void git_strlist_free(char **strings, size_t len)
{
size_t i;
......@@ -40,3 +93,16 @@ void git_strlist_free(char **strings, size_t len)
git__free(strings);
}
void git_strlist_free_with_null(char **strings)
{
char **s;
if (!strings)
return;
for (s = strings; *s; s++)
git__free(*s);
git__free(strings);
}
......@@ -11,6 +11,26 @@
#include "git2_util.h"
extern int git_strlist_copy(char ***out, const char **in, size_t len);
extern int git_strlist_copy_with_null(
char ***out,
const char **in,
size_t len);
extern bool git_strlist_contains_prefix(
const char **strings,
size_t len,
const char *str,
size_t n);
extern bool git_strlist_contains_key(
const char **strings,
size_t len,
const char *key,
char delimiter);
extern void git_strlist_free(char **strings, size_t len);
extern void git_strlist_free_with_null(char **strings);
#endif
/*
* 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 <stdio.h>
#include <sys/wait.h>
#include <signal.h>
#include <git2.h>
#include "git2_util.h"
#include "vector.h"
#include "process.h"
#include "strlist.h"
extern char **environ;
struct git_process {
char **args;
char **env;
char *cwd;
unsigned int capture_in : 1,
capture_out : 1,
capture_err : 1;
pid_t pid;
int child_in;
int child_out;
int child_err;
git_process_result_status status;
};
GIT_INLINE(bool) is_delete_env(const char *env)
{
char *c = index(env, '=');
if (c == NULL)
return false;
return *(c+1) == '\0';
}
static int merge_env(
char ***out,
const char **env,
size_t env_len,
bool exclude_env)
{
git_vector merged = GIT_VECTOR_INIT;
char **kv, *dup;
size_t max, cnt;
int error = 0;
for (max = env_len, kv = environ; !exclude_env && *kv; kv++)
max++;
if ((error = git_vector_init(&merged, max, NULL)) < 0)
goto on_error;
for (cnt = 0; env && cnt < env_len; cnt++) {
if (is_delete_env(env[cnt]))
continue;
dup = git__strdup(env[cnt]);
GIT_ERROR_CHECK_ALLOC(dup);
if ((error = git_vector_insert(&merged, dup)) < 0)
goto on_error;
}
if (!exclude_env) {
for (kv = environ; *kv; kv++) {
if (env && git_strlist_contains_key(env, env_len, *kv, '='))
continue;
dup = git__strdup(*kv);
GIT_ERROR_CHECK_ALLOC(dup);
if ((error = git_vector_insert(&merged, dup)) < 0)
goto on_error;
}
}
if (merged.length == 0) {
*out = NULL;
error = 0;
goto on_error;
}
git_vector_insert(&merged, NULL);
*out = (char **)merged.contents;
return 0;
on_error:
git_vector_free_deep(&merged);
return error;
}
int git_process_new(
git_process **out,
const char **args,
size_t args_len,
const char **env,
size_t env_len,
git_process_options *opts)
{
git_process *process;
GIT_ASSERT_ARG(out && args && args_len > 0);
*out = NULL;
process = git__calloc(sizeof(git_process), 1);
GIT_ERROR_CHECK_ALLOC(process);
if (git_strlist_copy_with_null(&process->args, args, args_len) < 0 ||
merge_env(&process->env, env, env_len, opts->exclude_env) < 0) {
git_process_free(process);
return -1;
}
if (opts) {
process->capture_in = opts->capture_in;
process->capture_out = opts->capture_out;
process->capture_err = opts->capture_err;
if (opts->cwd) {
process->cwd = git__strdup(opts->cwd);
GIT_ERROR_CHECK_ALLOC(process->cwd);
}
}
process->child_in = -1;
process->child_out = -1;
process->child_err = -1;
process->status = -1;
*out = process;
return 0;
}
extern int git_process_new_from_cmdline(
git_process **out,
const char *cmdline,
const char **env,
size_t env_len,
git_process_options *opts)
{
const char *args[] = { "/bin/sh", "-c", cmdline };
return git_process_new(out,
args, ARRAY_SIZE(args), env, env_len, opts);
}
#define CLOSE_FD(fd) \
if (fd >= 0) { \
close(fd); \
fd = -1; \
}
static int try_read_status(size_t *out, int fd, void *buf, size_t len)
{
size_t read_len = 0;
int ret = -1;
while (ret && read_len < len) {
ret = read(fd, buf + read_len, len - read_len);
if (ret < 0 && errno != EAGAIN && errno != EINTR) {
git_error_set(GIT_ERROR_OS, "could not read child status");
return -1;
}
read_len += ret;
}
*out = read_len;
return 0;
}
static int read_status(int fd)
{
size_t status_len = sizeof(int) * 3, read_len = 0;
char buffer[status_len], fn[128];
int error, fn_error, os_error, fn_len = 0;
if ((error = try_read_status(&read_len, fd, buffer, status_len)) < 0)
return error;
/* Immediate EOF indicates the exec succeeded. */
if (read_len == 0)
return 0;
if (read_len < status_len) {
git_error_set(GIT_ERROR_INVALID, "child status truncated");
return -1;
}
memcpy(&fn_error, &buffer[0], sizeof(int));
memcpy(&os_error, &buffer[sizeof(int)], sizeof(int));
memcpy(&fn_len, &buffer[sizeof(int) * 2], sizeof(int));
if (fn_len > 0) {
fn_len = min(fn_len, (int)(ARRAY_SIZE(fn) - 1));
if ((error = try_read_status(&read_len, fd, fn, fn_len)) < 0)
return error;
fn[fn_len] = '\0';
} else {
fn[0] = '\0';
}
if (fn_error) {
errno = os_error;
git_error_set(GIT_ERROR_OS, "could not %s", fn[0] ? fn : "(unknown)");
}
return fn_error;
}
static bool try_write_status(int fd, const void *buf, size_t len)
{
size_t write_len;
int ret;
for (write_len = 0; write_len < len; ) {
ret = write(fd, buf + write_len, len - write_len);
if (ret <= 0)
break;
write_len += ret;
}
return (len == write_len);
}
static void write_status(int fd, const char *fn, int error, int os_error)
{
size_t status_len = sizeof(int) * 3, fn_len;
char buffer[status_len];
fn_len = strlen(fn);
if (fn_len > INT_MAX)
fn_len = INT_MAX;
memcpy(&buffer[0], &error, sizeof(int));
memcpy(&buffer[sizeof(int)], &os_error, sizeof(int));
memcpy(&buffer[sizeof(int) * 2], &fn_len, sizeof(int));
/* Do our best effort to write all the status. */
if (!try_write_status(fd, buffer, status_len))
return;
if (fn_len)
try_write_status(fd, fn, fn_len);
}
int git_process_start(git_process *process)
{
int in[2] = { -1, -1 }, out[2] = { -1, -1 },
err[2] = { -1, -1 }, status[2] = { -1, -1 };
int fdflags, state, error;
pid_t pid;
/* Set up the pipes to read from/write to the process */
if ((process->capture_in && pipe(in) < 0) ||
(process->capture_out && pipe(out) < 0) ||
(process->capture_err && pipe(err) < 0)) {
git_error_set(GIT_ERROR_OS, "could not create pipe");
goto on_error;
}
/* Set up a self-pipe for status from the forked process. */
if (pipe(status) < 0 ||
(fdflags = fcntl(status[1], F_GETFD)) < 0 ||
fcntl(status[1], F_SETFD, fdflags | FD_CLOEXEC) < 0) {
git_error_set(GIT_ERROR_OS, "could not create pipe");
goto on_error;
}
switch (pid = fork()) {
case -1:
git_error_set(GIT_ERROR_OS, "could not fork");
goto on_error;
/* Child: start the process. */
case 0:
/* Close the opposing side of the pipes */
CLOSE_FD(status[0]);
if (process->capture_in) {
CLOSE_FD(in[1]);
dup2(in[0], STDIN_FILENO);
}
if (process->capture_out) {
CLOSE_FD(out[0]);
dup2(out[1], STDOUT_FILENO);
}
if (process->capture_err) {
CLOSE_FD(err[0]);
dup2(err[1], STDERR_FILENO);
}
if (process->cwd && (error = chdir(process->cwd)) < 0) {
write_status(status[1], "chdir", error, errno);
exit(0);
}
/*
* Exec the process and write the results back if the
* call fails. If it succeeds, we'll close the status
* pipe (via CLOEXEC) and the parent will know.
*/
error = execve(process->args[0],
process->args,
process->env);
write_status(status[1], "execve", error, errno);
exit(0);
/* Parent: make sure the child process exec'd correctly. */
default:
/* Close the opposing side of the pipes */
CLOSE_FD(status[1]);
if (process->capture_in) {
CLOSE_FD(in[0]);
process->child_in = in[1];
}
if (process->capture_out) {
CLOSE_FD(out[1]);
process->child_out = out[0];
}
if (process->capture_err) {
CLOSE_FD(err[1]);
process->child_err = err[0];
}
/* Try to read the status */
process->status = status[0];
if ((error = read_status(status[0])) < 0) {
waitpid(process->pid, &state, 0);
goto on_error;
}
process->pid = pid;
return 0;
}
on_error:
CLOSE_FD(in[0]); CLOSE_FD(in[1]);
CLOSE_FD(out[0]); CLOSE_FD(out[1]);
CLOSE_FD(err[0]); CLOSE_FD(err[1]);
CLOSE_FD(status[0]); CLOSE_FD(status[1]);
return -1;
}
int git_process_id(p_pid_t *out, git_process *process)
{
GIT_ASSERT(out && process);
if (!process->pid) {
git_error_set(GIT_ERROR_INVALID, "process not running");
return -1;
}
*out = process->pid;
return 0;
}
static ssize_t process_read(int fd, void *buf, size_t count)
{
ssize_t ret;
if (count > SSIZE_MAX)
count = SSIZE_MAX;
if ((ret = read(fd, buf, count)) < 0) {
git_error_set(GIT_ERROR_OS, "could not read from child process");
return -1;
}
return ret;
}
ssize_t git_process_read(git_process *process, void *buf, size_t count)
{
GIT_ASSERT_ARG(process);
GIT_ASSERT(process->capture_out);
return process_read(process->child_out, buf, count);
}
ssize_t git_process_read_err(git_process *process, void *buf, size_t count)
{
GIT_ASSERT_ARG(process);
GIT_ASSERT(process->capture_err);
return process_read(process->child_err, buf, count);
}
#ifdef GIT_THREADS
# define signal_state sigset_t
/*
* Since signal-handling is process-wide, we cannot simply use
* SIG_IGN to avoid SIGPIPE. Instead: http://www.microhowto.info:80/howto/ignore_sigpipe_without_affecting_other_threads_in_a_process.html
*/
GIT_INLINE(int) disable_signals(sigset_t *saved_mask)
{
sigset_t sigpipe_mask;
sigemptyset(&sigpipe_mask);
sigaddset(&sigpipe_mask, SIGPIPE);
if (pthread_sigmask(SIG_BLOCK, &sigpipe_mask, saved_mask) < 0) {
git_error_set(GIT_ERROR_OS, "could not configure signal mask");
return -1;
}
return 0;
}
GIT_INLINE(int) restore_signals(sigset_t *saved_mask)
{
sigset_t sigpipe_mask, pending;
int signal;
sigemptyset(&sigpipe_mask);
sigaddset(&sigpipe_mask, SIGPIPE);
if (sigpending(&pending) < 0) {
git_error_set(GIT_ERROR_OS, "could not examine pending signals");
return -1;
}
if (sigismember(&pending, SIGPIPE) == 1 &&
sigwait(&sigpipe_mask, &signal) < 0) {
git_error_set(GIT_ERROR_OS, "could not wait for (blocking) signal delivery");
return -1;
}
if (pthread_sigmask(SIG_SETMASK, saved_mask, 0) < 0) {
git_error_set(GIT_ERROR_OS, "could not configure signal mask");
return -1;
}
return 0;
}
#else
# define signal_state struct sigaction
GIT_INLINE(int) disable_signals(struct sigaction *saved_handler)
{
struct sigaction ign_handler = { 0 };
ign_handler.sa_handler = SIG_IGN;
if (sigaction(SIGPIPE, &ign_handler, saved_handler) < 0) {
git_error_set(GIT_ERROR_OS, "could not configure signal handler");
return -1;
}
return 0;
}
GIT_INLINE(int) restore_signals(struct sigaction *saved_handler)
{
if (sigaction(SIGPIPE, saved_handler, NULL) < 0) {
git_error_set(GIT_ERROR_OS, "could not configure signal handler");
return -1;
}
return 0;
}
#endif
ssize_t git_process_write(git_process *process, const void *buf, size_t count)
{
signal_state saved_signal;
ssize_t ret;
GIT_ASSERT_ARG(process);
GIT_ASSERT(process->capture_in);
if (count > SSIZE_MAX)
count = SSIZE_MAX;
if (disable_signals(&saved_signal) < 0)
return -1;
if ((ret = write(process->child_in, buf, count)) < 0)
git_error_set(GIT_ERROR_OS, "could not write to child process");
if (restore_signals(&saved_signal) < 0)
return -1;
return (ret < 0) ? -1 : ret;
}
int git_process_close_in(git_process *process)
{
if (!process->capture_in) {
git_error_set(GIT_ERROR_INVALID, "input is not open");
return -1;
}
CLOSE_FD(process->child_in);
return 0;
}
int git_process_close_out(git_process *process)
{
if (!process->capture_out) {
git_error_set(GIT_ERROR_INVALID, "output is not open");
return -1;
}
CLOSE_FD(process->child_out);
return 0;
}
int git_process_close_err(git_process *process)
{
if (!process->capture_err) {
git_error_set(GIT_ERROR_INVALID, "error is not open");
return -1;
}
CLOSE_FD(process->child_err);
return 0;
}
int git_process_close(git_process *process)
{
CLOSE_FD(process->child_in);
CLOSE_FD(process->child_out);
CLOSE_FD(process->child_err);
CLOSE_FD(process->status);
return 0;
}
int git_process_wait(git_process_result *result, git_process *process)
{
int state;
if (result)
memset(result, 0, sizeof(git_process_result));
if (!process->pid) {
git_error_set(GIT_ERROR_INVALID, "process is stopped");
return -1;
}
if (waitpid(process->pid, &state, 0) < 0) {
git_error_set(GIT_ERROR_OS, "could not wait for child");
return -1;
}
process->pid = 0;
if (result) {
if (WIFEXITED(state)) {
result->status = GIT_PROCESS_STATUS_NORMAL;
result->exitcode = WEXITSTATUS(state);
} else if (WIFSIGNALED(state)) {
result->status = GIT_PROCESS_STATUS_ERROR;
result->signal = WTERMSIG(state);
} else {
result->status = GIT_PROCESS_STATUS_ERROR;
}
}
return 0;
}
int git_process_result_msg(git_str *out, git_process_result *result)
{
if (result->status == GIT_PROCESS_STATUS_NONE) {
return git_str_puts(out, "process not started");
} else if (result->status == GIT_PROCESS_STATUS_NORMAL) {
return git_str_printf(out, "process exited with code %d",
result->exitcode);
} else if (result->signal) {
return git_str_printf(out, "process exited on signal %d",
result->signal);
}
return git_str_puts(out, "unknown error");
}
void git_process_free(git_process *process)
{
if (!process)
return;
if (process->pid)
git_process_close(process);
git__free(process->cwd);
git_strlist_free_with_null(process->args);
git_strlist_free_with_null(process->env);
git__free(process);
}
/*
* 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 <stdio.h>
#include <git2.h>
#include "git2_util.h"
#include "process.h"
#include "strlist.h"
#ifndef DWORD_MAX
# define DWORD_MAX INT32_MAX
#endif
#define ENV_MAX 32767
struct git_process {
wchar_t *appname;
wchar_t *cmdline;
wchar_t *env;
wchar_t *cwd;
unsigned int capture_in : 1,
capture_out : 1,
capture_err : 1;
PROCESS_INFORMATION process_info;
HANDLE child_in;
HANDLE child_out;
HANDLE child_err;
git_process_result_status status;
};
/*
* Windows processes have a single command-line that is split by the
* invoked application into arguments (instead of an array of
* command-line arguments). This command-line is split by space or
* tab delimiters, unless that whitespace is within a double quote.
* Literal double-quotes themselves can be escaped by a backslash,
* but only when not within double quotes. Literal backslashes can
* be escaped by a backslash.
*
* Effectively, this means that instead of thinking about quoting
* individual strings, think about double quotes as an escaping
* mechanism for whitespace.
*
* In other words (using ` as a string boundary):
* [ `foo`, `bar` ] => `foo bar`
* [ `foo bar` ] => `foo" "bar`
* [ `foo bar`, `foo bar` ] => `foo" "bar foo" "bar`
* [ `foo "bar" foo` ] => `foo" "\"bar\"" "foo`
*/
int git_process__cmdline(
git_str *out,
const char **in,
size_t in_len)
{
bool quoted = false;
const char *c;
size_t i;
for (i = 0; i < in_len; i++) {
/* Arguments are delimited by an unquoted space */
if (i)
git_str_putc(out, ' ');
for (c = in[i]; *c; c++) {
/* Start or stop quoting spaces within an argument */
if ((*c == ' ' || *c == '\t') && !quoted) {
git_str_putc(out, '"');
quoted = true;
} else if (*c != ' ' && *c != '\t' && quoted) {
git_str_putc(out, '"');
quoted = false;
}
/* Escape double-quotes and backslashes */
if (*c == '"' || *c == '\\')
git_str_putc(out, '\\');
git_str_putc(out, *c);
}
}
return git_str_oom(out) ? -1 : 0;
}
GIT_INLINE(bool) is_delete_env(const char *env)
{
char *c = strchr(env, '=');
if (c == NULL)
return false;
return *(c+1) == '\0';
}
static int merge_env(wchar_t **out, const char **in, size_t in_len, bool exclude_env)
{
git_str merged = GIT_STR_INIT;
wchar_t *in16 = NULL, *env = NULL, *e;
char *e8 = NULL;
size_t e_len;
int ret = 0;
size_t i;
*out = NULL;
in16 = git__malloc(ENV_MAX * sizeof(wchar_t));
GIT_ERROR_CHECK_ALLOC(in16);
e8 = git__malloc(ENV_MAX);
GIT_ERROR_CHECK_ALLOC(e8);
for (i = 0; in && i < in_len; i++) {
if (is_delete_env(in[i]))
continue;
if ((ret = git_utf8_to_16(in16, ENV_MAX, in[i])) < 0)
goto done;
git_str_put(&merged, (const char *)in16, ret * 2);
git_str_put(&merged, "\0\0", 2);
}
if (!exclude_env) {
env = GetEnvironmentStringsW();
for (e = env; *e; e += (e_len + 1)) {
e_len = wcslen(e);
if ((ret = git_utf8_from_16(e8, ENV_MAX, e)) < 0)
goto done;
if (git_strlist_contains_key(in, in_len, e8, '='))
continue;
git_str_put(&merged, (const char *)e, e_len * 2);
git_str_put(&merged, "\0\0", 2);
}
}
git_str_put(&merged, "\0\0", 2);
*out = (wchar_t *)git_str_detach(&merged);
done:
if (env)
FreeEnvironmentStringsW(env);
git_str_dispose(&merged);
git__free(e8);
git__free(in16);
return ret < 0 ? -1 : 0;
}
static int process_new(
git_process **out,
const char *appname,
const char *cmdline,
const char **env,
size_t env_len,
git_process_options *opts)
{
git_process *process;
int error = 0;
*out = NULL;
process = git__calloc(1, sizeof(git_process));
GIT_ERROR_CHECK_ALLOC(process);
if (appname &&
git_utf8_to_16_alloc(&process->appname, appname) < 0) {
error = -1;
goto done;
}
if (git_utf8_to_16_alloc(&process->cmdline, cmdline) < 0) {
error = -1;
goto done;
}
if (opts && opts->cwd &&
git_utf8_to_16_alloc(&process->cwd, opts->cwd) < 0) {
error = -1;
goto done;
}
if (env && (error = merge_env(&process->env, env, env_len, opts && opts->exclude_env) < 0))
goto done;
if (opts) {
process->capture_in = opts->capture_in;
process->capture_out = opts->capture_out;
process->capture_err = opts->capture_err;
}
done:
if (error)
git_process_free(process);
else
*out = process;
return error;
}
int git_process_new_from_cmdline(
git_process **out,
const char *cmdline,
const char **env,
size_t env_len,
git_process_options *opts)
{
GIT_ASSERT_ARG(out && cmdline);
return process_new(out, NULL, cmdline, env, env_len, opts);
}
int git_process_new(
git_process **out,
const char **args,
size_t args_len,
const char **env,
size_t env_len,
git_process_options *opts)
{
git_str cmdline = GIT_STR_INIT;
int error;
GIT_ASSERT_ARG(out && args && args_len > 0);
if ((error = git_process__cmdline(&cmdline, args, args_len)) < 0)
goto done;
error = process_new(out, args[0], cmdline.ptr, env, env_len, opts);
done:
git_str_dispose(&cmdline);
return error;
}
#define CLOSE_HANDLE(h) do { if ((h) != NULL) CloseHandle(h); } while(0)
int git_process_start(git_process *process)
{
STARTUPINFOW startup_info;
SECURITY_ATTRIBUTES security_attrs;
DWORD flags = CREATE_UNICODE_ENVIRONMENT;
HANDLE in[2] = { NULL, NULL },
out[2] = { NULL, NULL },
err[2] = { NULL, NULL };
memset(&security_attrs, 0, sizeof(SECURITY_ATTRIBUTES));
security_attrs.bInheritHandle = TRUE;
memset(&startup_info, 0, sizeof(STARTUPINFOW));
startup_info.cb = sizeof(STARTUPINFOW);
startup_info.hStdInput = GetStdHandle(STD_INPUT_HANDLE);
startup_info.hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE);
startup_info.hStdError = GetStdHandle(STD_ERROR_HANDLE);
if (process->capture_in) {
if (!CreatePipe(&in[0], &in[1], &security_attrs, 0) ||
!SetHandleInformation(in[1], HANDLE_FLAG_INHERIT, 0)) {
git_error_set(GIT_ERROR_OS, "could not create pipe");
goto on_error;
}
startup_info.hStdInput = in[0];
startup_info.dwFlags |= STARTF_USESTDHANDLES;
}
if (process->capture_out) {
if (!CreatePipe(&out[0], &out[1], &security_attrs, 0) ||
!SetHandleInformation(out[0], HANDLE_FLAG_INHERIT, 0)) {
git_error_set(GIT_ERROR_OS, "could not create pipe");
goto on_error;
}
startup_info.hStdOutput = out[1];
startup_info.dwFlags |= STARTF_USESTDHANDLES;
}
if (process->capture_err) {
if (!CreatePipe(&err[0], &err[1], &security_attrs, 0) ||
!SetHandleInformation(err[0], HANDLE_FLAG_INHERIT, 0)) {
git_error_set(GIT_ERROR_OS, "could not create pipe");
goto on_error;
}
startup_info.hStdError = err[1];
startup_info.dwFlags |= STARTF_USESTDHANDLES;
}
memset(&process->process_info, 0, sizeof(PROCESS_INFORMATION));
if (!CreateProcessW(process->appname, process->cmdline,
NULL, NULL, TRUE, flags, process->env,
process->cwd,
&startup_info,
&process->process_info)) {
git_error_set(GIT_ERROR_OS, "could not create process");
goto on_error;
}
CLOSE_HANDLE(in[0]); process->child_in = in[1];
CLOSE_HANDLE(out[1]); process->child_out = out[0];
CLOSE_HANDLE(err[1]); process->child_err = err[0];
return 0;
on_error:
CLOSE_HANDLE(in[0]); CLOSE_HANDLE(in[1]);
CLOSE_HANDLE(out[0]); CLOSE_HANDLE(out[1]);
CLOSE_HANDLE(err[0]); CLOSE_HANDLE(err[1]);
return -1;
}
int git_process_id(p_pid_t *out, git_process *process)
{
GIT_ASSERT(out && process);
if (!process->process_info.dwProcessId) {
git_error_set(GIT_ERROR_INVALID, "process not running");
return -1;
}
*out = process->process_info.dwProcessId;
return 0;
}
ssize_t git_process_read(git_process *process, void *buf, size_t count)
{
DWORD ret;
if (count > DWORD_MAX)
count = DWORD_MAX;
if (count > SSIZE_MAX)
count = SSIZE_MAX;
if (!ReadFile(process->child_out, buf, (DWORD)count, &ret, NULL)) {
if (GetLastError() == ERROR_BROKEN_PIPE)
return 0;
git_error_set(GIT_ERROR_OS, "could not read");
return -1;
}
return ret;
}
ssize_t git_process_write(git_process *process, const void *buf, size_t count)
{
DWORD ret;
if (count > DWORD_MAX)
count = DWORD_MAX;
if (count > SSIZE_MAX)
count = SSIZE_MAX;
if (!WriteFile(process->child_in, buf, (DWORD)count, &ret, NULL)) {
git_error_set(GIT_ERROR_OS, "could not write");
return -1;
}
return ret;
}
int git_process_close_in(git_process *process)
{
if (!process->capture_in) {
git_error_set(GIT_ERROR_INVALID, "input is not open");
return -1;
}
if (process->child_in) {
CloseHandle(process->child_in);
process->child_in = NULL;
}
return 0;
}
int git_process_close_out(git_process *process)
{
if (!process->capture_out) {
git_error_set(GIT_ERROR_INVALID, "output is not open");
return -1;
}
if (process->child_out) {
CloseHandle(process->child_out);
process->child_out = NULL;
}
return 0;
}
int git_process_close_err(git_process *process)
{
if (!process->capture_err) {
git_error_set(GIT_ERROR_INVALID, "error is not open");
return -1;
}
if (process->child_err) {
CloseHandle(process->child_err);
process->child_err = NULL;
}
return 0;
}
int git_process_close(git_process *process)
{
if (process->child_in) {
CloseHandle(process->child_in);
process->child_in = NULL;
}
if (process->child_out) {
CloseHandle(process->child_out);
process->child_out = NULL;
}
if (process->child_err) {
CloseHandle(process->child_err);
process->child_err = NULL;
}
CloseHandle(process->process_info.hProcess);
process->process_info.hProcess = NULL;
CloseHandle(process->process_info.hThread);
process->process_info.hThread = NULL;
return 0;
}
int git_process_wait(git_process_result *result, git_process *process)
{
DWORD exitcode;
if (result)
memset(result, 0, sizeof(git_process_result));
if (!process->process_info.dwProcessId) {
git_error_set(GIT_ERROR_INVALID, "process is stopped");
return -1;
}
if (WaitForSingleObject(process->process_info.hProcess, INFINITE) == WAIT_FAILED) {
git_error_set(GIT_ERROR_OS, "could not wait for process");
return -1;
}
if (!GetExitCodeProcess(process->process_info.hProcess, &exitcode)) {
git_error_set(GIT_ERROR_OS, "could not get process exit code");
return -1;
}
result->status = GIT_PROCESS_STATUS_NORMAL;
result->exitcode = exitcode;
memset(&process->process_info, 0, sizeof(PROCESS_INFORMATION));
return 0;
}
int git_process_result_msg(git_str *out, git_process_result *result)
{
if (result->status == GIT_PROCESS_STATUS_NONE) {
return git_str_puts(out, "process not started");
} else if (result->status == GIT_PROCESS_STATUS_NORMAL) {
return git_str_printf(out, "process exited with code %d",
result->exitcode);
} else if (result->signal) {
return git_str_printf(out, "process exited on signal %d",
result->signal);
}
return git_str_puts(out, "unknown error");
}
void git_process_free(git_process *process)
{
if (!process)
return;
if (process->process_info.hProcess)
git_process_close(process);
git__free(process->env);
git__free(process->cwd);
git__free(process->cmdline);
git__free(process->appname);
git__free(process);
}
......@@ -47,6 +47,9 @@ static char *_orig_http_proxy = NULL;
static char *_orig_https_proxy = NULL;
static char *_orig_no_proxy = NULL;
static char *_ssh_cmd = NULL;
static char *_orig_ssh_cmd = NULL;
static int ssl_cert(git_cert *cert, int valid, const char *host, void *payload)
{
GIT_UNUSED(cert);
......@@ -102,6 +105,14 @@ void test_online_clone__initialize(void)
_orig_https_proxy = cl_getenv("HTTPS_PROXY");
_orig_no_proxy = cl_getenv("NO_PROXY");
_orig_ssh_cmd = cl_getenv("GIT_SSH");
_ssh_cmd = cl_getenv("GITTEST_SSH_CMD");
if (_ssh_cmd)
cl_setenv("GIT_SSH", _ssh_cmd);
else
cl_setenv("GIT_SSH", NULL);
if (_remote_expectcontinue)
git_libgit2_opts(GIT_OPT_ENABLE_HTTP_EXPECT_CONTINUE, 1);
}
......@@ -149,6 +160,11 @@ void test_online_clone__cleanup(void)
git__free(_orig_https_proxy);
git__free(_orig_no_proxy);
cl_setenv("GIT_SSH", _orig_ssh_cmd);
git__free(_orig_ssh_cmd);
git__free(_ssh_cmd);
git_libgit2_opts(GIT_OPT_SET_SSL_CERT_LOCATIONS, NULL, NULL);
git_libgit2_opts(GIT_OPT_SET_SERVER_TIMEOUT, 0);
git_libgit2_opts(GIT_OPT_SET_SERVER_CONNECT_TIMEOUT, 0);
......@@ -653,7 +669,7 @@ void test_online_clone__ssh_auth_methods(void)
{
int with_user;
#ifndef GIT_SSH
#ifndef GIT_SSH_LIBSSH2
clar__skip();
#endif
g_options.fetch_opts.callbacks.credentials = check_ssh_auth_methods;
......@@ -675,7 +691,7 @@ void test_online_clone__ssh_auth_methods(void)
*/
void test_online_clone__ssh_certcheck_accepts_unknown(void)
{
#if !defined(GIT_SSH) || !defined(GIT_SSH_MEMORY_CREDENTIALS)
#if !defined(GIT_SSH_LIBSSH2) || !defined(GIT_SSH_MEMORY_CREDENTIALS)
clar__skip();
#endif
......@@ -793,9 +809,10 @@ static int cred_foo_bar(git_credential **cred, const char *url, const char *user
void test_online_clone__ssh_cannot_change_username(void)
{
#ifndef GIT_SSH
#ifndef GIT_SSH_LIBSSH2
clar__skip();
#endif
g_options.fetch_opts.callbacks.credentials = cred_foo_bar;
cl_git_fail(git_clone(&g_repo, "ssh://git@github.com/libgit2/TestGitRepository", "./foo", &g_options));
......@@ -837,6 +854,10 @@ static int ssh_certificate_check(git_cert *cert, int valid, const char *host, vo
void test_online_clone__ssh_cert(void)
{
#ifndef GIT_SSH_LIBSSH2
cl_skip();
#endif
g_options.fetch_opts.callbacks.certificate_check = ssh_certificate_check;
if (!_remote_ssh_fingerprint)
......@@ -908,12 +929,12 @@ void test_online_clone__certificate_invalid(void)
{
g_options.fetch_opts.callbacks.certificate_check = fail_certificate_check;
cl_git_fail_with(git_clone(&g_repo, "https://github.com/libgit2/TestGitRepository", "./foo", &g_options),
GIT_ECERTIFICATE);
cl_git_fail_with(GIT_ECERTIFICATE,
git_clone(&g_repo, "https://github.com/libgit2/TestGitRepository", "./foo", &g_options));
#ifdef GIT_SSH
cl_git_fail_with(git_clone(&g_repo, "ssh://github.com/libgit2/TestGitRepository", "./foo", &g_options),
GIT_ECERTIFICATE);
#ifdef GIT_SSH_LIBSSH2
cl_git_fail_with(GIT_ECERTIFICATE,
git_clone(&g_repo, "ssh://github.com/libgit2/TestGitRepository", "./foo", &g_options));
#endif
}
......
......@@ -20,6 +20,9 @@ static char *_remote_ssh_passphrase = NULL;
static char *_remote_default = NULL;
static char *_remote_expectcontinue = NULL;
static char *_orig_ssh_cmd = NULL;
static char *_ssh_cmd = NULL;
static int cred_acquire_cb(git_credential **, const char *, const char *, unsigned int, void *);
static git_remote *_remote;
......@@ -369,6 +372,14 @@ void test_online_push__initialize(void)
_remote_expectcontinue = cl_getenv("GITTEST_REMOTE_EXPECTCONTINUE");
_remote = NULL;
_orig_ssh_cmd = cl_getenv("GIT_SSH");
_ssh_cmd = cl_getenv("GITTEST_SSH_CMD");
if (_ssh_cmd)
cl_setenv("GIT_SSH", _ssh_cmd);
else
cl_setenv("GIT_SSH", NULL);
/* Skip the test if we're missing the remote URL */
if (!_remote_url)
cl_skip();
......@@ -423,6 +434,9 @@ void test_online_push__cleanup(void)
git__free(_remote_default);
git__free(_remote_expectcontinue);
git__free(_orig_ssh_cmd);
git__free(_ssh_cmd);
/* Freed by cl_git_sandbox_cleanup */
_repo = NULL;
......
@ECHO OFF
FOR /F "tokens=*" %%a IN ('more') DO ECHO %%a
#!/bin/sh
echo "Hello, world."
#include "clar_libgit2.h"
#include "process.h"
#include "vector.h"
static git_str env_cmd = GIT_STR_INIT;
static git_str accumulator = GIT_STR_INIT;
static git_vector env_result = GIT_VECTOR_INIT;
void test_process_env__initialize(void)
{
#ifdef GIT_WIN32
git_str_printf(&env_cmd, "%s/env.cmd", cl_fixture("process"));
#else
git_str_puts(&env_cmd, "/usr/bin/env");
#endif
cl_git_pass(git_vector_init(&env_result, 32, git__strcmp_cb));
}
void test_process_env__cleanup(void)
{
git_vector_free(&env_result);
git_str_dispose(&accumulator);
git_str_dispose(&env_cmd);
}
static void run_env(const char **env_array, size_t env_len, bool exclude_env)
{
const char *args_array[] = { env_cmd.ptr };
git_process *process;
git_process_options opts = GIT_PROCESS_OPTIONS_INIT;
git_process_result result = GIT_PROCESS_RESULT_INIT;
char buf[1024], *tok;
ssize_t ret;
opts.capture_out = 1;
opts.exclude_env = exclude_env;
cl_git_pass(git_process_new(&process, args_array, ARRAY_SIZE(args_array), env_array, env_len, &opts));
cl_git_pass(git_process_start(process));
while ((ret = git_process_read(process, buf, 1024)) > 0)
cl_git_pass(git_str_put(&accumulator, buf, (size_t)ret));
cl_assert_equal_i(0, ret);
cl_git_pass(git_process_wait(&result, process));
cl_assert_equal_i(GIT_PROCESS_STATUS_NORMAL, result.status);
cl_assert_equal_i(0, result.exitcode);
cl_assert_equal_i(0, result.signal);
for (tok = strtok(accumulator.ptr, "\n"); tok; tok = strtok(NULL, "\n")) {
#ifdef GIT_WIN32
if (strlen(tok) && tok[strlen(tok) - 1] == '\r')
tok[strlen(tok) - 1] = '\0';
#endif
cl_git_pass(git_vector_insert(&env_result, tok));
}
git_process_close(process);
git_process_free(process);
}
void test_process_env__can_add_env(void)
{
const char *env_array[] = { "TEST_NEW_ENV=added", "TEST_OTHER_ENV=also_added" };
run_env(env_array, 2, false);
cl_git_pass(git_vector_search(NULL, &env_result, "TEST_NEW_ENV=added"));
cl_git_pass(git_vector_search(NULL, &env_result, "TEST_OTHER_ENV=also_added"));
}
void test_process_env__can_propagate_env(void)
{
cl_setenv("TEST_NEW_ENV", "propagated");
run_env(NULL, 0, false);
cl_git_pass(git_vector_search(NULL, &env_result, "TEST_NEW_ENV=propagated"));
}
void test_process_env__can_remove_env(void)
{
const char *env_array[] = { "TEST_NEW_ENV=" };
char *str;
size_t i;
cl_setenv("TEST_NEW_ENV", "propagated");
run_env(env_array, 1, false);
git_vector_foreach(&env_result, i, str)
cl_assert(git__prefixcmp(str, "TEST_NEW_ENV=") != 0);
}
void test_process_env__can_clear_env(void)
{
const char *env_array[] = { "TEST_NEW_ENV=added", "TEST_OTHER_ENV=also_added" };
cl_setenv("SOME_EXISTING_ENV", "propagated");
run_env(env_array, 2, true);
/*
* We can't simply test that the environment is precisely what we
* provided. Some systems (eg win32) will add environment variables
* to all processes.
*/
cl_assert_equal_i(GIT_ENOTFOUND, git_vector_search(NULL, &env_result, "SOME_EXISTING_ENV=propagated"));
}
#include "clar_libgit2.h"
#include "process.h"
#include "vector.h"
#ifndef GIT_WIN32
# include <signal.h>
#endif
#ifndef SIGTERM
# define SIGTERM 42
#endif
#ifndef SIGPIPE
# define SIGPIPE 42
#endif
static git_str helloworld_cmd = GIT_STR_INIT;
static git_str cat_cmd = GIT_STR_INIT;
static git_str pwd_cmd = GIT_STR_INIT;
void test_process_start__initialize(void)
{
#ifdef GIT_WIN32
git_str_printf(&helloworld_cmd, "%s/helloworld.bat", cl_fixture("process"));
git_str_printf(&cat_cmd, "%s/cat.bat", cl_fixture("process"));
git_str_printf(&pwd_cmd, "%s/pwd.bat", cl_fixture("process"));
#else
git_str_printf(&helloworld_cmd, "%s/helloworld.sh", cl_fixture("process"));
#endif
}
void test_process_start__cleanup(void)
{
git_str_dispose(&pwd_cmd);
git_str_dispose(&cat_cmd);
git_str_dispose(&helloworld_cmd);
}
void test_process_start__returncode(void)
{
#ifdef GIT_WIN32
const char *args_array[] = { "C:\\Windows\\System32\\cmd.exe", "/c", "exit", "1" };
#elif __APPLE__
const char *args_array[] = { "/usr/bin/false" };
#else
const char *args_array[] = { "/bin/false" };
#endif
git_process *process;
git_process_options opts = GIT_PROCESS_OPTIONS_INIT;
git_process_result result = GIT_PROCESS_RESULT_INIT;
cl_git_pass(git_process_new(&process, args_array, ARRAY_SIZE(args_array), NULL, 0, &opts));
cl_git_pass(git_process_start(process));
cl_git_pass(git_process_wait(&result, process));
cl_assert_equal_i(GIT_PROCESS_STATUS_NORMAL, result.status);
cl_assert_equal_i(1, result.exitcode);
cl_assert_equal_i(0, result.signal);
git_process_free(process);
}
void test_process_start__not_found(void)
{
#ifdef GIT_WIN32
const char *args_array[] = { "C:\\a\\b\\z\\y\\not_found" };
#else
const char *args_array[] = { "/a/b/z/y/not_found" };
#endif
git_process *process;
git_process_options opts = GIT_PROCESS_OPTIONS_INIT;
cl_git_pass(git_process_new(&process, args_array, ARRAY_SIZE(args_array), NULL, 0, &opts));
cl_git_fail(git_process_start(process));
git_process_free(process);
}
static void write_all(git_process *process, char *buf)
{
size_t buf_len = strlen(buf);
ssize_t ret;
while (buf_len) {
ret = git_process_write(process, buf, buf_len);
cl_git_pass(ret < 0 ? (int)ret : 0);
buf += ret;
buf_len -= ret;
}
}
static void read_all(git_str *out, git_process *process)
{
char buf[32];
size_t buf_len = 32;
ssize_t ret;
while ((ret = git_process_read(process, buf, buf_len)) > 0)
cl_git_pass(git_str_put(out, buf, ret));
cl_git_pass(ret < 0 ? (int)ret : 0);
}
void test_process_start__redirect_stdio(void)
{
#ifdef GIT_WIN32
const char *args_array[] = { "C:\\Windows\\System32\\cmd.exe", "/c", cat_cmd.ptr };
#else
const char *args_array[] = { "/bin/cat" };
#endif
git_process *process;
git_process_options opts = GIT_PROCESS_OPTIONS_INIT;
git_process_result result = GIT_PROCESS_RESULT_INIT;
git_str buf = GIT_STR_INIT;
opts.capture_in = 1;
opts.capture_out = 1;
cl_git_pass(git_process_new(&process, args_array, ARRAY_SIZE(args_array), NULL, 0, &opts));
cl_git_pass(git_process_start(process));
write_all(process, "Hello, world.\r\nHello!\r\n");
cl_git_pass(git_process_close_in(process));
read_all(&buf, process);
cl_assert_equal_s("Hello, world.\r\nHello!\r\n", buf.ptr);
cl_git_pass(git_process_wait(&result, process));
cl_assert_equal_i(GIT_PROCESS_STATUS_NORMAL, result.status);
cl_assert_equal_i(0, result.exitcode);
cl_assert_equal_i(0, result.signal);
git_str_dispose(&buf);
git_process_free(process);
}
/*
void test_process_start__catch_sigterm(void)
{
const char *args_array[] = { "/bin/cat" };
git_process *process;
git_process_options opts = GIT_PROCESS_OPTIONS_INIT;
git_process_result result = GIT_PROCESS_RESULT_INIT;
p_pid_t pid;
opts.capture_out = 1;
cl_git_pass(git_process_new(&process, args_array, ARRAY_SIZE(args_array), NULL, 0, &opts));
cl_git_pass(git_process_start(process));
cl_git_pass(git_process_id(&pid, process));
cl_must_pass(kill(pid, SIGTERM));
cl_git_pass(git_process_wait(&result, process));
cl_assert_equal_i(GIT_PROCESS_STATUS_ERROR, result.status);
cl_assert_equal_i(0, result.exitcode);
cl_assert_equal_i(SIGTERM, result.signal);
git_process_free(process);
}
void test_process_start__catch_sigpipe(void)
{
const char *args_array[] = { helloworld_cmd.ptr };
git_process *process;
git_process_options opts = GIT_PROCESS_OPTIONS_INIT;
git_process_result result = GIT_PROCESS_RESULT_INIT;
opts.capture_out = 1;
cl_git_pass(git_process_new(&process, args_array, ARRAY_SIZE(args_array), NULL, 0, &opts));
cl_git_pass(git_process_start(process));
cl_git_pass(git_process_close(process));
cl_git_pass(git_process_wait(&result, process));
cl_assert_equal_i(GIT_PROCESS_STATUS_ERROR, result.status);
cl_assert_equal_i(0, result.exitcode);
cl_assert_equal_i(SIGPIPE, result.signal);
git_process_free(process);
}
*/
void test_process_start__can_chdir(void)
{
#ifdef GIT_WIN32
const char *args_array[] = { "C:\\Windows\\System32\\cmd.exe", "/c", pwd_cmd.ptr };
char *startwd = "C:\\";
#else
const char *args_array[] = { "/bin/pwd" };
char *startwd = "/";
#endif
git_process *process;
git_process_options opts = GIT_PROCESS_OPTIONS_INIT;
git_process_result result = GIT_PROCESS_RESULT_INIT;
git_str buf = GIT_STR_INIT;
opts.cwd = startwd;
opts.capture_out = 1;
cl_git_pass(git_process_new(&process, args_array, ARRAY_SIZE(args_array), NULL, 0, &opts));
cl_git_pass(git_process_start(process));
read_all(&buf, process);
git_str_rtrim(&buf);
cl_assert_equal_s(startwd, buf.ptr);
cl_git_pass(git_process_wait(&result, process));
cl_assert_equal_i(GIT_PROCESS_STATUS_NORMAL, result.status);
cl_assert_equal_i(0, result.exitcode);
cl_assert_equal_i(0, result.signal);
git_str_dispose(&buf);
git_process_free(process);
}
void test_process_start__cannot_chdir_to_nonexistent_dir(void)
{
#ifdef GIT_WIN32
const char *args_array[] = { "C:\\Windows\\System32\\cmd.exe", "/c", pwd_cmd.ptr };
char *startwd = "C:\\a\\b\\z\\y\\not_found";
#else
const char *args_array[] = { "/bin/pwd" };
char *startwd = "/a/b/z/y/not_found";
#endif
git_process *process;
git_process_options opts = GIT_PROCESS_OPTIONS_INIT;
opts.cwd = startwd;
opts.capture_out = 1;
cl_git_pass(git_process_new(&process, args_array, ARRAY_SIZE(args_array), NULL, 0, &opts));
cl_git_fail(git_process_start(process));
git_process_free(process);
}
#include "clar_libgit2.h"
#include "process.h"
#include "vector.h"
#ifdef GIT_WIN32
static git_str result;
# define assert_cmdline(expected, given) do { \
cl_git_pass(git_process__cmdline(&result, given, ARRAY_SIZE(given))); \
cl_assert_equal_s(expected, result.ptr); \
git_str_dispose(&result); \
} while(0)
#endif
void test_process_win32__cmdline_is_whitespace_delimited(void)
{
#ifdef GIT_WIN32
const char *one[] = { "one" };
const char *two[] = { "one", "two" };
const char *three[] = { "one", "two", "three" };
const char *four[] = { "one", "two", "three", "four" };
assert_cmdline("one", one);
assert_cmdline("one two", two);
assert_cmdline("one two three", three);
assert_cmdline("one two three four", four);
#endif
}
void test_process_win32__cmdline_escapes_whitespace(void)
{
#ifdef GIT_WIN32
const char *spaces[] = { "one with spaces" };
const char *tabs[] = { "one\twith\ttabs" };
const char *multiple[] = { "one with many spaces" };
assert_cmdline("one\" \"with\" \"spaces", spaces);
assert_cmdline("one\"\t\"with\"\t\"tabs", tabs);
assert_cmdline("one\" \"with\" \"many\" \"spaces", multiple);
#endif
}
void test_process_win32__cmdline_escapes_quotes(void)
{
#ifdef GIT_WIN32
const char *one[] = { "echo", "\"hello world\"" };
assert_cmdline("echo \\\"hello\" \"world\\\"", one);
#endif
}
void test_process_win32__cmdline_escapes_backslash(void)
{
#ifdef GIT_WIN32
const char *one[] = { "foo\\bar", "foo\\baz" };
const char *two[] = { "c:\\program files\\foo bar\\foo bar.exe", "c:\\path\\to\\other\\", "/a", "/b" };
assert_cmdline("foo\\\\bar foo\\\\baz", one);
assert_cmdline("c:\\\\program\" \"files\\\\foo\" \"bar\\\\foo\" \"bar.exe c:\\\\path\\\\to\\\\other\\\\ /a /b", two);
#endif
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment