/* FreeTDS - Library of routines accessing Sybase and Microsoft databases
 * Copyright (C) 2010  Frediano Ziglio
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public
 * License along with this library; if not, write to the
 * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 * Boston, MA 02111-1307, USA.
 */
#undef UNICODE
#undef _UNICODE
#define UNICODE 1
#define _UNICODE 1

/* enabled some additional definitions for getaddrinfo */
#define _WIN32_WINNT 0x601

#include <config.h>

/* fix possible bug in sspi.h header */
#define FreeCredentialHandle FreeCredentialsHandle

#if HAVE_STDLIB_H
#include <stdlib.h>
#endif /* HAVE_STDLIB_H */

#if HAVE_STRING_H
#include <string.h>
#endif /* HAVE_STRING_H */

#if HAVE_SSPI
#define SECURITY_WIN32

#include <freetds/windows.h>
#include <security.h>
#include <sspi.h>
#include <rpc.h>
#include <tchar.h>

#include <freetds/tds.h>
#include <freetds/thread.h>
#include <freetds/utils/string.h>
#include <freetds/bool.h>
#include <freetds/replacements.h>
#include <freetds/tds/iconv.h>
#include <freetds/tds/tls.h>

/**
 * \ingroup libtds
 * \defgroup auth Authentication
 * Functions for handling authentication.
 */

/**
 * \addtogroup auth
 * @{
 */

#define TDS_CALC_CB_SIZE(cb) \
    ((cb) == NULL ? \
        0 : \
        ( sizeof(SEC_CHANNEL_BINDINGS) + \
          (cb)->cbInitiatorLength + \
          (cb)->cbAcceptorLength + \
          (cb)->cbApplicationDataLength ))

typedef struct tds_sspi_auth
{
	TDSAUTHENTICATION tds_auth;
	CredHandle cred;
	CtxtHandle cred_ctx;
	TCHAR *sname;
	PSEC_CHANNEL_BINDINGS cb;
} TDSSSPIAUTH;

static HMODULE secdll = NULL;
static PSecurityFunctionTable sec_fn = NULL;
static tds_mutex sec_mutex = TDS_MUTEX_INITIALIZER;

static bool
tds_init_secdll(void)
{
	bool res = false;

	if (sec_fn)
		return true;

	tds_mutex_lock(&sec_mutex);
	for (;;) {
		if (!secdll) {
			secdll = LoadLibrary(TEXT("secur32.dll"));
			if (!secdll)
				break;
		}
		if (!sec_fn) {
			INIT_SECURITY_INTERFACE pInitSecurityInterface;

			pInitSecurityInterface = (INIT_SECURITY_INTERFACE_W) GetProcAddress(secdll, "InitSecurityInterfaceW");
			if (!pInitSecurityInterface)
				break;

			sec_fn = pInitSecurityInterface();
			if (!sec_fn)
				break;
		}
		res = true;
		break;
	}
	tds_mutex_unlock(&sec_mutex);
	return res;
}

static int
tds_sspi_free(TDSCONNECTION *conn TDS_UNUSED, TDSAUTHENTICATION *tds_auth)
{
	TDSSSPIAUTH *auth = (TDSSSPIAUTH *) tds_auth;

	if (SecIsValidHandle(&auth->cred_ctx))
		sec_fn->DeleteSecurityContext(&auth->cred_ctx);
	if (SecIsValidHandle(&auth->cred))
		sec_fn->FreeCredentialsHandle(&auth->cred);
	if (auth->tds_auth.packet)
		sec_fn->FreeContextBuffer(auth->tds_auth.packet);
	free(auth->cb);
	free(auth->sname);
	free(auth);
	return TDS_SUCCESS;
}

static int
tds_sspi_handle_next(TDSSOCKET *tds, TDSAUTHENTICATION *tds_auth, size_t len)
{
	SecBuffer out_buf;
	SecBufferDesc in_desc, out_desc;
	SECURITY_STATUS status;
	ULONG attrs;
	TimeStamp ts;
	uint8_t *auth_buf;
	SecBuffer in_buffers[2];
	unsigned long in_buffers_len = 1;
	unsigned long cb_len;

	TDSSSPIAUTH *auth = (TDSSSPIAUTH *) tds_auth;

	if (len < 1)
		return TDS_FAIL;

	auth_buf = tds_new(uint8_t, len);
	if (!auth_buf)
		return TDS_FAIL;
	tds_get_n(tds, auth_buf, (int)len);

	/* free previously allocated buffer */
	if (auth->tds_auth.packet) {
		sec_fn->FreeContextBuffer(auth->tds_auth.packet);
		auth->tds_auth.packet = NULL;
	}
	in_desc.ulVersion = out_desc.ulVersion = SECBUFFER_VERSION;
	in_desc.cBuffers = in_buffers_len;
	in_desc.pBuffers = in_buffers;
	out_desc.pBuffers = &out_buf;
	out_desc.cBuffers = 1;

	in_buffers[0].BufferType = SECBUFFER_TOKEN;
	in_buffers[0].pvBuffer = auth_buf;
	in_buffers[0].cbBuffer = (ULONG) len;

	cb_len = TDS_CALC_CB_SIZE(auth->cb);
	if (cb_len > 0) {
		in_buffers_len++;
		in_buffers[1].BufferType = SECBUFFER_CHANNEL_BINDINGS;
		in_buffers[1].cbBuffer = cb_len;
		in_buffers[1].pvBuffer = auth->cb;
	}

	out_buf.BufferType = SECBUFFER_TOKEN;
	out_buf.pvBuffer   = NULL;
	out_buf.cbBuffer   = 0;

	status = sec_fn->InitializeSecurityContext(&auth->cred, &auth->cred_ctx, auth->sname,
		ISC_REQ_CONFIDENTIALITY | ISC_REQ_REPLAY_DETECT | ISC_REQ_CONNECTION | ISC_REQ_ALLOCATE_MEMORY,
		0, SECURITY_NETWORK_DREP, &in_desc,
		0, &auth->cred_ctx, &out_desc,
		&attrs, &ts);

	free(auth_buf);
	auth->tds_auth.packet = out_buf.pvBuffer;

	switch (status) {
	case SEC_I_COMPLETE_AND_CONTINUE:
		sec_fn->CompleteAuthToken(&auth->cred_ctx, &out_desc);
		break;

	case SEC_I_CONTINUE_NEEDED:
	case SEC_E_OK:
		break;

	default:
		return TDS_FAIL;
	}

	if (out_buf.cbBuffer == 0)
		return TDS_SUCCESS;

	tds_put_n(tds, auth->tds_auth.packet, out_buf.cbBuffer);

	return tds_flush_packet(tds);
}

static size_t
convert_to_ucs2le_string(TDSSOCKET * tds, const char *s, size_t len, WCHAR *out, size_t out_len)
{
	const char *ib;
	char *ob;
	size_t il, ol;

	TDSICONV * char_conv = tds->conn->char_convs[client2ucs2];

	/* char_conv is only mostly const */
	TDS_ERRNO_MESSAGE_FLAGS *suppress = (TDS_ERRNO_MESSAGE_FLAGS *) & char_conv->suppress;

	/* convert */
	ib = s;
	il = len;
	ob = (char *) out;
	ol = out_len * 2;
	memset(suppress, 0, sizeof(char_conv->suppress));
	if (tds_iconv(tds, char_conv, to_server, &ib, &il, &ob, &ol) == (size_t) - 1)
		return (size_t) -1;

	return ob - (char *) out;
}

static PSEC_CHANNEL_BINDINGS
tds_sspi_get_channel_binding(TDSSOCKET *tds)
{
	unsigned char tls_unique_buf[256];
	size_t tls_unique_len;
	const size_t struct_offset = sizeof(SEC_CHANNEL_BINDINGS);
	size_t app_data_len;
	PSEC_CHANNEL_BINDINGS cb;

	/* Get tls-unique */
	tls_unique_len = tds_ssl_get_cb(tds->conn, tls_unique_buf, sizeof(tls_unique_buf));
	if (tls_unique_len == 0)
		return NULL;

	app_data_len = 11 + tls_unique_len;

	cb = (SEC_CHANNEL_BINDINGS *) calloc(1, struct_offset + app_data_len);
	if (!cb) {
		tdsdump_log(TDS_DBG_NETWORK, "tds_sspi_get_channel_binding: failed to allocate channel bindings\n");
		return NULL;
	}
	cb->dwInitiatorAddrType = 0;
	cb->cbInitiatorLength = 0;
	cb->dwInitiatorOffset = 0;
	cb->dwAcceptorAddrType = 0;
	cb->cbAcceptorLength = 0;
	cb->dwAcceptorOffset = 0;
	cb->cbApplicationDataLength = app_data_len;
	cb->dwApplicationDataOffset = struct_offset;

	memcpy((char *) cb + struct_offset, "tls-unique:", 11);
	memcpy((char *) cb + struct_offset + 11, tls_unique_buf, tls_unique_len);

	tdsdump_dump_buf(TDS_DBG_NETWORK, "SEC_CHANNEL_BINDINGS", cb, struct_offset + app_data_len);
	tdsdump_dump_buf(TDS_DBG_NETWORK, "SEC_CHANNEL_BINDINGS application_data",
			 ((char *) cb) + cb->dwApplicationDataOffset, cb->cbApplicationDataLength);
	return cb;
}

/**
 * Build a SSPI packet to send to server
 * @param tds     A pointer to the TDSSOCKET structure managing a client/server operation.
 */
TDSAUTHENTICATION *
tds_sspi_get_auth(TDSSOCKET * tds)
{
	SecBuffer buf;
	SecBufferDesc desc;
	SECURITY_STATUS status;
	ULONG attrs;
	TimeStamp ts;
	SEC_WINNT_AUTH_IDENTITY identity;
	const char *server_name;
	WCHAR *p, *user_name, *passwd;
	struct addrinfo *addrs = NULL;
	static WCHAR sec_package[] = L"Negotiate";
	WCHAR user_ucs2[512], password_ucs2[256];

	TDSSSPIAUTH *auth;
	TDSLOGIN *login = tds->login;

	/* check login */
	if (!login)
		return NULL;

	if (!tds_init_secdll())
		return NULL;

	/* parse username/password information */
	memset(&identity, 0, sizeof(identity));
	if (convert_to_ucs2le_string(tds, tds_dstr_cstr(&login->user_name), tds_dstr_len(&login->user_name)+1,
				     user_ucs2, TDS_VECTOR_SIZE(user_ucs2)) == (size_t) -1)
		return NULL;
	user_name = user_ucs2;
	if (convert_to_ucs2le_string(tds, tds_dstr_cstr(&login->password), tds_dstr_len(&login->password)+1,
				     password_ucs2, TDS_VECTOR_SIZE(password_ucs2)) == (size_t) -1)
		return NULL;
	passwd = password_ucs2;
	if ((p = wcschr(user_name, L'\\')) != NULL) {
		identity.Flags = SEC_WINNT_AUTH_IDENTITY_UNICODE;
		identity.Password = passwd;
		identity.PasswordLength = (unsigned long) wcslen(passwd);
		identity.Domain = user_name;
		identity.DomainLength = (unsigned long) (p - user_name);
		user_name = p + 1;
		identity.User = user_name;
		identity.UserLength = (unsigned long) wcslen(user_name);
	}

	auth = tds_new0(TDSSSPIAUTH, 1);
	if (!auth)
		return NULL;

	auth->tds_auth.free = tds_sspi_free;
	auth->tds_auth.handle_next = tds_sspi_handle_next;
	SecInvalidateHandle(&auth->cred);
	SecInvalidateHandle(&auth->cred_ctx);

	/* using Negotiate system will use proper protocol (either NTLM or Kerberos) */
	if (sec_fn->AcquireCredentialsHandle(NULL, sec_package, SECPKG_CRED_OUTBOUND,
		NULL, identity.Domain ? &identity : NULL,
		NULL, NULL, &auth->cred, &ts) != SEC_E_OK) {
		free(auth);
		return NULL;
	}

	desc.ulVersion = SECBUFFER_VERSION;
	desc.cBuffers  = 1;
	desc.pBuffers  = &buf;

	buf.BufferType = SECBUFFER_TOKEN;
	buf.pvBuffer   = NULL;
	buf.cbBuffer   = 0;

	/* build SPN */
	server_name = tds_dstr_cstr(&login->server_host_name);
	if (strchr(server_name, '.') == NULL) {
		struct addrinfo hints;
		int res;

		memset(&hints, 0, sizeof(hints));
		hints.ai_family = AF_UNSPEC;
		hints.ai_socktype = SOCK_STREAM;
		hints.ai_flags = AI_V4MAPPED|AI_ADDRCONFIG|AI_CANONNAME|AI_FQDN;
		res = getaddrinfo(server_name, NULL, &hints, &addrs);
		if (res) {
			/* some version of Windows does not support V4MAPPED
			 * and ADDRCONFIG, try without them
			 */
			hints.ai_flags = AI_CANONNAME|AI_FQDN;
			res = getaddrinfo(server_name, NULL, &hints, &addrs);
		}
		if (!res && addrs->ai_canonname && strchr(addrs->ai_canonname, '.') != NULL)
			server_name = addrs->ai_canonname;
	}
	if (strchr(server_name, '.') != NULL) {
		auth->sname = malloc((strlen(server_name) + 24) * sizeof(TCHAR));
		if (!auth->sname) {
			if (addrs)
				freeaddrinfo(addrs);
			tds_sspi_free(tds->conn, &auth->tds_auth);
			return NULL;
		}
		wsprintf(auth->sname, TEXT("MSSQLSvc/%S:%d"), server_name, login->port);
		tdsdump_log(TDS_DBG_NETWORK, "kerberos name %S\n", auth->sname);
	}
	if (addrs)
		freeaddrinfo(addrs);

	status = sec_fn->InitializeSecurityContext(&auth->cred, NULL, auth->sname,
		ISC_REQ_CONFIDENTIALITY | ISC_REQ_REPLAY_DETECT | ISC_REQ_CONNECTION | ISC_REQ_ALLOCATE_MEMORY,
		0, SECURITY_NETWORK_DREP,
		NULL, 0,
		&auth->cred_ctx, &desc,
		&attrs, &ts);

	switch (status) {
	case SEC_I_COMPLETE_AND_CONTINUE:
		sec_fn->CompleteAuthToken(&auth->cred_ctx, &desc);
		break;

	case SEC_I_CONTINUE_NEEDED:
	case SEC_E_OK:
		break;

	default:
		tds_sspi_free(tds->conn, &auth->tds_auth);
		return NULL;
	}

	auth->tds_auth.packet_len = buf.cbBuffer;
	auth->tds_auth.packet     = buf.pvBuffer;

	auth->cb = tds_sspi_get_channel_binding(tds);

	return &auth->tds_auth;
}

/** @} */

#endif /* HAVE_SSPI */
