/*
 * 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 "git2.h"
#include "common.h"
#include "buffer.h"
#include "auth.h"
#include "auth_ntlm.h"
#include "git2/sys/credential.h"

#ifdef GIT_NTLM

#include "ntlmclient.h"

typedef struct {
	git_http_auth_context parent;
	ntlm_client *ntlm;
	char *challenge;
	bool complete;
} http_auth_ntlm_context;

static int ntlm_set_challenge(
	git_http_auth_context *c,
	const char *challenge)
{
	http_auth_ntlm_context *ctx = (http_auth_ntlm_context *)c;

	GIT_ASSERT_ARG(ctx);
	GIT_ASSERT_ARG(challenge);

	git__free(ctx->challenge);

	ctx->challenge = git__strdup(challenge);
	GIT_ERROR_CHECK_ALLOC(ctx->challenge);

	return 0;
}

static int ntlm_set_credentials(http_auth_ntlm_context *ctx, git_credential *_cred)
{
	git_credential_userpass_plaintext *cred;
	const char *sep, *username;
	char *domain = NULL, *domainuser = NULL;
	int error = 0;

	GIT_ASSERT(_cred->credtype == GIT_CREDENTIAL_USERPASS_PLAINTEXT);
	cred = (git_credential_userpass_plaintext *)_cred;

	if ((sep = strchr(cred->username, '\\')) != NULL) {
		domain = git__strndup(cred->username, (sep - cred->username));
		GIT_ERROR_CHECK_ALLOC(domain);

		domainuser = git__strdup(sep + 1);
		GIT_ERROR_CHECK_ALLOC(domainuser);

		username = domainuser;
	} else {
		username = cred->username;
	}

	if (ntlm_client_set_credentials(ctx->ntlm,
	    username, domain, cred->password) < 0) {
		git_error_set(GIT_ERROR_NET, "could not set credentials: %s",
		    ntlm_client_errmsg(ctx->ntlm));
		error = -1;
		goto done;
	}

done:
	git__free(domain);
	git__free(domainuser);
	return error;
}

static int ntlm_next_token(
	git_buf *buf,
	git_http_auth_context *c,
	git_credential *cred)
{
	http_auth_ntlm_context *ctx = (http_auth_ntlm_context *)c;
	git_buf input_buf = GIT_BUF_INIT;
	const unsigned char *msg;
	size_t challenge_len, msg_len;
	int error = GIT_EAUTH;

	GIT_ASSERT_ARG(buf);
	GIT_ASSERT_ARG(ctx);

	GIT_ASSERT(ctx->ntlm);

	challenge_len = ctx->challenge ? strlen(ctx->challenge) : 0;

	if (ctx->complete)
		ntlm_client_reset(ctx->ntlm);

	/*
	 * Set us complete now since it's the default case; the one
	 * incomplete case (successfully created a client request)
	 * will explicitly set that it requires a second step.
	 */
	ctx->complete = true;

	if (cred && ntlm_set_credentials(ctx, cred) != 0)
		goto done;

	if (challenge_len < 4) {
		git_error_set(GIT_ERROR_NET, "no ntlm challenge sent from server");
		goto done;
	} else if (challenge_len == 4) {
		if (memcmp(ctx->challenge, "NTLM", 4) != 0) {
			git_error_set(GIT_ERROR_NET, "server did not request NTLM");
			goto done;
		}

		if (ntlm_client_negotiate(&msg, &msg_len, ctx->ntlm) != 0) {
			git_error_set(GIT_ERROR_NET, "ntlm authentication failed: %s",
				ntlm_client_errmsg(ctx->ntlm));
			goto done;
		}

		ctx->complete = false;
	} else {
		if (memcmp(ctx->challenge, "NTLM ", 5) != 0) {
			git_error_set(GIT_ERROR_NET, "challenge from server was not NTLM");
			goto done;
		}

		if (git_buf_decode_base64(&input_buf,
		    ctx->challenge + 5, challenge_len - 5) < 0) {
			git_error_set(GIT_ERROR_NET, "invalid NTLM challenge from server");
			goto done;
		}

		if (ntlm_client_set_challenge(ctx->ntlm,
		    (const unsigned char *)input_buf.ptr, input_buf.size) != 0) {
			git_error_set(GIT_ERROR_NET, "ntlm challenge failed: %s",
				ntlm_client_errmsg(ctx->ntlm));
			goto done;
		}

		if (ntlm_client_response(&msg, &msg_len, ctx->ntlm) != 0) {
			git_error_set(GIT_ERROR_NET, "ntlm authentication failed: %s",
				ntlm_client_errmsg(ctx->ntlm));
			goto done;
		}
	}

	git_buf_puts(buf, "NTLM ");
	git_buf_encode_base64(buf, (const char *)msg, msg_len);

	if (git_buf_oom(buf))
		goto done;

	error = 0;

done:
	git_buf_dispose(&input_buf);
	return error;
}

static int ntlm_is_complete(git_http_auth_context *c)
{
	http_auth_ntlm_context *ctx = (http_auth_ntlm_context *)c;

	GIT_ASSERT_ARG(ctx);
	return (ctx->complete == true);
}

static void ntlm_context_free(git_http_auth_context *c)
{
	http_auth_ntlm_context *ctx = (http_auth_ntlm_context *)c;

	ntlm_client_free(ctx->ntlm);
	git__free(ctx->challenge);
	git__free(ctx);
}

static int ntlm_init_context(
	http_auth_ntlm_context *ctx,
	const git_net_url *url)
{
	GIT_UNUSED(url);

	if ((ctx->ntlm = ntlm_client_init(NTLM_CLIENT_DEFAULTS)) == NULL) {
		git_error_set_oom();
		return -1;
	}

	return 0;
}

int git_http_auth_ntlm(
	git_http_auth_context **out,
	const git_net_url *url)
{
	http_auth_ntlm_context *ctx;

	GIT_UNUSED(url);

	*out = NULL;

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

	if (ntlm_init_context(ctx, url) < 0) {
		git__free(ctx);
		return -1;
	}

	ctx->parent.type = GIT_HTTP_AUTH_NTLM;
	ctx->parent.credtypes = GIT_CREDENTIAL_USERPASS_PLAINTEXT;
	ctx->parent.connection_affinity = 1;
	ctx->parent.set_challenge = ntlm_set_challenge;
	ctx->parent.next_token = ntlm_next_token;
	ctx->parent.is_complete = ntlm_is_complete;
	ctx->parent.free = ntlm_context_free;

	*out = (git_http_auth_context *)ctx;

	return 0;
}

#endif /* GIT_NTLM */
