Tentative support for GnuTLS

Add GnuTLS support for the hashing functions as well as start work on a
GnuTLS IO stream.
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 078bba6..aaa6351 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -155,6 +155,7 @@
 	FILE(GLOB SRC_HTTP deps/http-parser/*.c deps/http-parser/*.h)
 ELSE ()
 	IF (NOT AMIGA)
+		FIND_PACKAGE(GnuTLS)
 		FIND_PACKAGE(OpenSSL)
 	ENDIF ()
 
@@ -174,6 +175,9 @@
 IF (WIN32 AND NOT MINGW AND NOT SHA1_TYPE STREQUAL "builtin")
 	ADD_DEFINITIONS(-DWIN32_SHA1)
 	FILE(GLOB SRC_SHA1 src/hash/hash_win32.c)
+ELSEIF (GNUTLS_FOUND AND NOT SHA1_TYPE STREQUAL "bultin")
+	ADD_DEFINITIONS(-DGNUTLS_SHA1)
+		SET(LIBGIT2_PC_REQUIRES "${LIBGIT2_PC_REQUIRES} gnutls")
 ELSEIF (OPENSSL_FOUND AND NOT SHA1_TYPE STREQUAL "builtin")
 	ADD_DEFINITIONS(-DOPENSSL_SHA1)
 	IF (CMAKE_SYSTEM_NAME MATCHES "FreeBSD")
@@ -370,7 +374,11 @@
   INCLUDE_DIRECTORIES(${SECURITY_INCLUDE_DIR})
 ENDIF ()
 
-IF (OPENSSL_FOUND)
+IF (GNUTLS_FOUND)
+  ADD_DEFINITIONS(-DGIT_GNUTLS)
+  INCLUDE_DIRECTORIES(${GNUTLS_INCLUDE_DIR})
+  SET(SSL_LIBRARIES ${GNUTLS_LIBRARIES})
+ELSEIF(OPENSSL_FOUND)
   ADD_DEFINITIONS(-DGIT_SSL)
   INCLUDE_DIRECTORIES(${OPENSSL_INCLUDE_DIR})
   SET(SSL_LIBRARIES ${OPENSSL_LIBRARIES})
diff --git a/src/global.c b/src/global.c
index 006202a..9e35ef3 100644
--- a/src/global.c
+++ b/src/global.c
@@ -25,6 +25,10 @@
 # endif
 #endif
 
+#ifdef GIT_GNUTLS
+# include <gnutls/gnutls.h>
+#endif
+
 static git_global_shutdown_fn git__shutdown_callbacks[MAX_SHUTDOWN_CB];
 static git_atomic git__n_shutdown_callbacks;
 static git_atomic git__n_inits;
@@ -276,6 +280,11 @@
 	/* OpenSSL needs to be initialized from the main thread */
 	init_ssl();
 
+#ifdef GIT_GNUTLS
+	gnutls_global_init();
+	git__on_shutdown(gnutls_global_deinit);
+#endif
+
 	GIT_MEMORY_BARRIER;
 }
 
@@ -336,6 +345,11 @@
 		ssl_inited = 1;
 	}
 
+#ifdef GIT_GNUTLS
+	gnutls_global_init();
+	git__on_shutdown(gnutls_global_deinit);
+#endif
+
 	git_atomic_inc(&git__n_inits);
 	return 0;
 }
diff --git a/src/gnutls_stream.c b/src/gnutls_stream.c
new file mode 100644
index 0000000..37fc100
--- /dev/null
+++ b/src/gnutls_stream.c
@@ -0,0 +1,165 @@
+/*
+ * Copyright (C) the libgit2 contributors. All rights reserved.
+ *
+ * This file is part of libgit2, distributed under the GNU GPL v2 with
+ * a Linking Exception. For full terms see the included COPYING file.
+ */
+
+#ifdef GIT_GNUTLS
+
+#include <gnutls/gnutls.h>
+
+#include "stream.h"
+#include "socket_stream.h"
+#include "git2/transport.h"
+
+void set_gnutls_error(int error)
+{
+	giterr_set(GITERR_SSL, "gnutls: %s", gnutls_strerror(error));
+}
+
+typedef struct {
+	git_stream parent;
+	git_socket_stream *socket;
+	gnutls_session_t session;
+	git_cert_x509 cert_info;
+} gnutls_stream;
+
+static int verify_server_cert(gnutls_session_t session, const char *host)
+{
+	int error;
+	unsigned int status = 0;
+
+	if ((error = gnutls_certificate_verify_peers3(session, host, &status)) < 0) {
+		set_gnutls_error(error);
+		return -1;
+	}
+
+	if (!status)
+		return 0;
+	else
+		return GIT_ECERTIFICATE;
+}
+
+static ssize_t gnutls_stream_write(git_stream *stream, void *data, size_t len, int flags)
+{
+	ssize_t ret;
+	size_t off;
+	gnutls_stream *st = (gnutls_stream *) stream;
+
+	GIT_UNUSED(flags);
+
+	while (off < len) {
+		ret = gnutls_record_send(st->session, data + off, len - off);
+		if (ret == GNUTLS_E_INTERRUPTED || ret == GNUTLS_E_AGAIN)
+			continue; /* try again with the same params */
+
+		if (ret < 0) {
+			set_gnutls_error(ret);
+			return -1;
+		}
+
+		off += ret;
+	}
+
+	return ret;
+}
+
+static ssize_t gnutls_stream_read(git_stream *stream, void *data, size_t len)
+{
+	ssize_t ret;
+	gnutls_stream *st = (gnutls_stream *) stream;
+
+	do {
+		ret = gnutls_record_recv(st->session, data, len);
+	} while (ret == GNUTLS_E_INTERRUPTED || ret == GNUTLS_E_AGAIN);
+
+	if (ret < 0) {
+		set_gnutls_error(ret);
+		return -1;
+	}
+
+	return ret;
+}
+
+static int gnutls_connect(git_stream *stream)
+{
+	int error;
+	gnutls_stream *st = (gnutls_stream *) stream;
+
+	if ((error = git_stream_connect((git_stream *)st->socket)) < 0)
+		return error;
+
+	/* ideally we'd have functions that talk to the socket, but for now */
+	gnutls_transport_set_int(st->session, st->socket->s);
+
+	do {
+		error = gnutls_handshake(st->session);
+	} while (!gnutls_error_is_fatal(error));
+
+	if (error < 0) {
+		set_gnutls_error(error);
+		return -1;
+	}
+
+	return verify_server_cert(st->session, st->socket->host);
+}
+
+static int gnutls_stream_close(git_stream *stream)
+{
+	gnutls_stream *st = (gnutls_stream *) stream;
+	int error;
+
+	do {
+		error = gnutls_bye(st->session, GNUTLS_SHUT_RDWR);
+	} while (error == GNUTLS_E_INTERRUPTED || error == GNUTLS_E_AGAIN);
+
+	return git_stream_close((git_stream *)st->socket);
+}
+
+static void gnutls_stream_free(git_stream *stream)
+{
+	gnutls_stream *st = (gnutls_stream *) stream;
+
+	gnutls_deinit(st->session);
+	git__free(st);
+}
+
+int git_gnutls_stream_new(git_stream **out, const char *host, const char *port)
+{
+	gnutls_stream *st;
+	int error;
+
+	st = git__calloc(1, sizeof(gnutls_stream));
+	GITERR_CHECK_ALLOC(st);
+
+	if (git_socket_stream_new((git_stream **) &st->socket, host, port))
+		return -1;
+
+	if ((error = gnutls_init(&st->session, GNUTLS_CLIENT)) < 0) {
+		git_stream_free((git_stream *) st->socket);
+		git__free(st);
+		return -1;
+	}
+
+	st->parent.encrypted = 1;
+	st->parent.connect = gnutls_connect;
+	st->parent.certificate = gnutls_certificate;
+	st->parent.read = gnutls_stream_read;
+	st->parent.write = gnutls_stream_write;
+	st->parent.close = gnutls_stream_close;
+	st->parent.free = gnutls_stream_free;
+
+	*out = (git_stream *) st;
+	return 0;
+}
+
+#else
+
+int git_gnutls_stream_new(git_stream **out, const char *host, const char *port)
+{
+	giterr_set(GITERR_SSL, "GnuTLS is not supported in this version");
+	return -1;
+}
+
+#endif
diff --git a/src/hash.h b/src/hash.h
index 0bc02a8..c373633 100644
--- a/src/hash.h
+++ b/src/hash.h
@@ -18,6 +18,8 @@
 
 #if defined(GIT_COMMON_CRYPTO)
 # include "hash/hash_common_crypto.h"
+#elif defined(GNUTLS_SHA1)
+# include "hash/hash_gnutls.h"
 #elif defined(OPENSSL_SHA1)
 # include "hash/hash_openssl.h"
 #elif defined(WIN32_SHA1)
diff --git a/src/hash/hash_gnutls.h b/src/hash/hash_gnutls.h
new file mode 100644
index 0000000..f4a78a8
--- /dev/null
+++ b/src/hash/hash_gnutls.h
@@ -0,0 +1,58 @@
+/*
+ * 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_hash_gnutls_h__
+#define INCLUDE_hash_gnutls_h__
+
+#include "hash.h"
+
+#include <gnutls/gnutls.h>
+#include <gnutls/crypto.h>
+
+struct git_hash_ctx {
+	gnutls_hash_hd_t c;
+};
+
+#define git_hash_global_init() 0
+#define git_hash_ctx_init(ctx) git_hash_init(ctx)
+#define git_hash_ctx_cleanup(ctx) git_hash_cleanup(ctx)
+
+GIT_INLINE(int) git_hash_init(git_hash_ctx *ctx)
+{
+	int error;
+	assert(ctx);
+	if ((error = gnutls_hash_init(&ctx->c, GNUTLS_MAC_SHA1)) < 0) {
+		giterr_set(GITERR_SSL, "gnutls: %s", gnutls_strerror(error));
+		return -1;
+	}
+	return 0;
+}
+
+GIT_INLINE(int) git_hash_update(git_hash_ctx *ctx, const void *data, size_t len)
+{
+	int error;
+	assert(ctx);
+	if ((error = gnutls_hash(ctx->c, data, len)) < 0) {
+		giterr_set(GITERR_SSL, "gnutls: %s", gnutls_strerror(error));
+		return -1;
+	}
+	return 0;
+}
+
+GIT_INLINE(int) git_hash_final(git_oid *out, git_hash_ctx *ctx)
+{
+	assert(ctx);
+	gnutls_hash_output(ctx->c, out);
+	return 0;
+}
+
+GIT_INLINE(void) git_hash_cleanup(git_hash_ctx *ctx)
+{
+	gnutls_hash_deinit(ctx->c, NULL);
+}
+
+#endif