/*
 * 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 "auth_ntlm.h"
#include "auth_negotiate.h"

#ifdef GIT_WIN32

#define SECURITY_WIN32

#include "git2.h"
#include "auth.h"
#include "git2/sys/credential.h"

#include <windows.h>
#include <security.h>

typedef struct {
	git_http_auth_context parent;
	wchar_t *target;

	const char *package_name;
	size_t package_name_len;
	wchar_t *package_name_w;
	SecPkgInfoW *package_info;
	SEC_WINNT_AUTH_IDENTITY_W identity;
	CredHandle cred;
	CtxtHandle context;

	int has_identity : 1,
	    has_credentials : 1,
	    has_context : 1,
	    complete : 1;
	git_str challenge;
} http_auth_sspi_context;

static void sspi_reset_context(http_auth_sspi_context *ctx)
{
	if (ctx->has_identity) {
		git__free(ctx->identity.User);
		git__free(ctx->identity.Domain);
		git__free(ctx->identity.Password);

		memset(&ctx->identity, 0, sizeof(SEC_WINNT_AUTH_IDENTITY_W));

		ctx->has_identity = 0;
	}

	if (ctx->has_credentials) {
		FreeCredentialsHandle(&ctx->cred);
		memset(&ctx->cred, 0, sizeof(CredHandle));

		ctx->has_credentials = 0;
	}

	if (ctx->has_context) {
		DeleteSecurityContext(&ctx->context);
		memset(&ctx->context, 0, sizeof(CtxtHandle));

		ctx->has_context = 0;
	}

	ctx->complete = 0;

	git_str_dispose(&ctx->challenge);
}

static int sspi_set_challenge(
	git_http_auth_context *c,
	const char *challenge)
{
	http_auth_sspi_context *ctx = (http_auth_sspi_context *)c;
	size_t challenge_len = strlen(challenge);

	git_str_clear(&ctx->challenge);

	if (strncmp(challenge, ctx->package_name, ctx->package_name_len) != 0) {
		git_error_set(GIT_ERROR_NET, "invalid %s challenge from server", ctx->package_name);
		return -1;
	}

	/*
	 * A package type indicator without a base64 payload indicates the
	 * mechanism; it's not an actual challenge. Ignore it.
	 */
	if (challenge[ctx->package_name_len] == 0) {
		return 0;
	} else if (challenge[ctx->package_name_len] != ' ') {
		git_error_set(GIT_ERROR_NET, "invalid %s challenge from server", ctx->package_name);
		return -1;
	}

	if (git_str_decode_base64(&ctx->challenge,
	                          challenge + (ctx->package_name_len + 1),
	                          challenge_len - (ctx->package_name_len + 1)) < 0) {
		git_error_set(GIT_ERROR_NET, "invalid %s challenge from server", ctx->package_name);
		return -1;
	}

	GIT_ASSERT(ctx->challenge.size <= ULONG_MAX);
	return 0;
}

static int create_identity(
	SEC_WINNT_AUTH_IDENTITY_W **out,
	http_auth_sspi_context *ctx,
	git_credential *cred)
{
	git_credential_userpass_plaintext *userpass;
	wchar_t *username = NULL, *domain = NULL, *password = NULL;
	int username_len = 0, domain_len = 0, password_len = 0;
	const char *sep;

	if (cred->credtype == GIT_CREDENTIAL_DEFAULT) {
		*out = NULL;
		return 0;
	}

	if (cred->credtype != GIT_CREDENTIAL_USERPASS_PLAINTEXT) {
		git_error_set(GIT_ERROR_NET, "unknown credential type: %d", cred->credtype);
		return -1;
	}

	userpass = (git_credential_userpass_plaintext *)cred;

	if ((sep = strchr(userpass->username, '\\')) != NULL) {
		GIT_ASSERT(sep - userpass->username < INT_MAX);

		username_len = git_utf8_to_16_alloc(&username, sep + 1);
		domain_len = git_utf8_to_16_alloc_with_len(&domain,
			userpass->username, (int)(sep - userpass->username));
	} else {
		username_len = git_utf8_to_16_alloc(&username,
			userpass->username);
	}

	password_len = git_utf8_to_16_alloc(&password, userpass->password);

	if (username_len < 0 || domain_len < 0 || password_len < 0) {
		git__free(username);
		git__free(domain);
		git__free(password);
		return -1;
	}

	ctx->identity.Flags = SEC_WINNT_AUTH_IDENTITY_UNICODE;
	ctx->identity.User = username;
	ctx->identity.UserLength = (unsigned long)username_len;
	ctx->identity.Password = password;
	ctx->identity.PasswordLength = (unsigned long)password_len;
	ctx->identity.Domain = domain;
	ctx->identity.DomainLength = (unsigned long)domain_len;

	ctx->has_identity = 1;

	*out = &ctx->identity;

	return 0;
}

static int sspi_next_token(
	git_str *buf,
	git_http_auth_context *c,
	git_credential *cred)
{
	http_auth_sspi_context *ctx = (http_auth_sspi_context *)c;
	SEC_WINNT_AUTH_IDENTITY_W *identity = NULL;
	TimeStamp timestamp;
	DWORD context_flags;
	SecBuffer input_buf = { 0, SECBUFFER_TOKEN, NULL };
	SecBuffer output_buf = { 0, SECBUFFER_TOKEN, NULL };
	SecBufferDesc input_buf_desc = { SECBUFFER_VERSION, 1, &input_buf };
	SecBufferDesc output_buf_desc = { SECBUFFER_VERSION, 1, &output_buf };
	SECURITY_STATUS status;

	if (ctx->complete)
		sspi_reset_context(ctx);

	if (!ctx->has_context) {
		if (create_identity(&identity, ctx, cred) < 0)
			return -1;

		status = AcquireCredentialsHandleW(NULL, ctx->package_name_w,
				SECPKG_CRED_BOTH, NULL, identity, NULL,
				NULL, &ctx->cred, &timestamp);

		if (status != SEC_E_OK) {
			git_error_set(GIT_ERROR_OS, "could not acquire credentials");
			return -1;
		}

		ctx->has_credentials = 1;
	}

	context_flags = ISC_REQ_ALLOCATE_MEMORY |
	                ISC_REQ_CONFIDENTIALITY |
	                ISC_REQ_MUTUAL_AUTH;

	if (ctx->challenge.size > 0) {
		input_buf.BufferType = SECBUFFER_TOKEN;
		input_buf.cbBuffer = (unsigned long)ctx->challenge.size;
		input_buf.pvBuffer = ctx->challenge.ptr;
	}

	status = InitializeSecurityContextW(&ctx->cred,
		ctx->has_context ? &ctx->context : NULL,
		ctx->target,
		context_flags,
		0,
		SECURITY_NETWORK_DREP,
		ctx->has_context ? &input_buf_desc : NULL,
		0,
		ctx->has_context ? NULL : &ctx->context,
		&output_buf_desc,
		&context_flags,
		NULL);

	if (status == SEC_I_COMPLETE_AND_CONTINUE ||
	    status == SEC_I_COMPLETE_NEEDED)
		status = CompleteAuthToken(&ctx->context, &output_buf_desc);

	if (status == SEC_E_OK) {
		ctx->complete = 1;
	} else if (status != SEC_I_CONTINUE_NEEDED) {
		git_error_set(GIT_ERROR_OS, "could not initialize security context");
		return -1;
	}

	ctx->has_context = 1;
	git_str_clear(&ctx->challenge);

	if (output_buf.cbBuffer > 0) {
		git_str_put(buf, ctx->package_name, ctx->package_name_len);
		git_str_putc(buf, ' ');
		git_str_encode_base64(buf, output_buf.pvBuffer, output_buf.cbBuffer);

		FreeContextBuffer(output_buf.pvBuffer);

		if (git_str_oom(buf))
			return -1;
	}

	return 0;
}

static int sspi_is_complete(git_http_auth_context *c)
{
	http_auth_sspi_context *ctx = (http_auth_sspi_context *)c;

	return ctx->complete;
}

static void sspi_context_free(git_http_auth_context *c)
{
	http_auth_sspi_context *ctx = (http_auth_sspi_context *)c;

	sspi_reset_context(ctx);

	FreeContextBuffer(ctx->package_info);
	git__free(ctx->target);
	git__free(ctx);
}

static int sspi_init_context(
	git_http_auth_context **out,
	git_http_auth_t type,
	const git_net_url *url)
{
	http_auth_sspi_context *ctx;
	git_str target = GIT_STR_INIT;

	*out = NULL;

	ctx = git__calloc(1, sizeof(http_auth_sspi_context));
	GIT_ERROR_CHECK_ALLOC(ctx);

	switch (type) {
	case GIT_HTTP_AUTH_NTLM:
		ctx->package_name = "NTLM";
		ctx->package_name_len = CONST_STRLEN("NTLM");
		ctx->package_name_w = L"NTLM";
		ctx->parent.credtypes = GIT_CREDENTIAL_USERPASS_PLAINTEXT |
	                                GIT_CREDENTIAL_DEFAULT;
		break;
	case GIT_HTTP_AUTH_NEGOTIATE:
		ctx->package_name = "Negotiate";
		ctx->package_name_len = CONST_STRLEN("Negotiate");
		ctx->package_name_w = L"Negotiate";
		ctx->parent.credtypes = GIT_CREDENTIAL_DEFAULT;
		break;
	default:
		git_error_set(GIT_ERROR_NET, "unknown SSPI auth type: %d", ctx->parent.type);
		git__free(ctx);
		return -1;
	}

	if (QuerySecurityPackageInfoW(ctx->package_name_w, &ctx->package_info) != SEC_E_OK) {
		git_error_set(GIT_ERROR_OS, "could not query security package");
		git__free(ctx);
		return -1;
	}

	if (git_str_printf(&target, "http/%s", url->host) < 0 ||
	    git_utf8_to_16_alloc(&ctx->target, target.ptr) < 0) {
		FreeContextBuffer(ctx->package_info);
		git__free(ctx);
		return -1;
	}

	ctx->parent.type = type;
	ctx->parent.connection_affinity = 1;
	ctx->parent.set_challenge = sspi_set_challenge;
	ctx->parent.next_token = sspi_next_token;
	ctx->parent.is_complete = sspi_is_complete;
	ctx->parent.free = sspi_context_free;

	*out = (git_http_auth_context *)ctx;

	git_str_dispose(&target);
	return 0;
}

#if defined(GIT_GSSAPI) || defined(GIT_GSSFRAMEWORK) || defined(GIT_WIN32)
int git_http_auth_negotiate(
	git_http_auth_context **out,
	const git_net_url *url)
{
	return sspi_init_context(out, GIT_HTTP_AUTH_NEGOTIATE, url);
}
#endif

int git_http_auth_ntlm(
	git_http_auth_context **out,
	const git_net_url *url)
{
	return sspi_init_context(out, GIT_HTTP_AUTH_NTLM, url);
}

#endif /* GIT_WIN32 */
