/*
 * Copyright (C) 2014 Martin Willi
 *
 * Copyright (C) secunet Security Networks AG
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the
 * Free Software Foundation; either version 2 of the License, or (at your
 * option) any later version.  See <http://www.fsf.org/copyleft/gpl.txt>.
 *
 * This program 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 General Public License
 * for more details.
 */

#include <winsock2.h>
#include <windows.h>
#include <winhttp.h>

#include "winhttp_fetcher.h"

#include <library.h>

/**
 * Timeout for DNS resolution, in ms
 */
#define RESOLVE_TIMEOUT 5000

/**
 * Timeout for TCP connect, in ms
 */
#define CONNECT_TIMEOUT 10000

typedef struct private_winhttp_fetcher_t private_winhttp_fetcher_t;

/**
 * Private data of a winhttp_fetcher_t.
 */
struct private_winhttp_fetcher_t {

	/**
	 * Public interface
	 */
	winhttp_fetcher_t public;

	/**
	 * WinHTTP session handle
	 */
	HINTERNET session;

	/**
	 * POST request data
	 */
	chunk_t request;

	/**
	 * Optional HTTP headers, as allocated LPWSTR
	 */
	linked_list_t *headers;

	/**
	 * Callback function
	 */
	fetcher_callback_t cb;

	/**
	 * Timeout for operations, in ms
	 */
	u_long timeout;

	/**
	 * User pointer to store HTTP status code to
	 */
	u_int *result;
};

/**
 * Configure and send the HTTP request
 */
static bool send_request(private_winhttp_fetcher_t *this, HINTERNET request)
{
	WCHAR headers[512] = L"";
	LPWSTR hdr;

	/* Set timeout. By default, send/receive does not time out */
	if (!WinHttpSetTimeouts(request, RESOLVE_TIMEOUT, CONNECT_TIMEOUT,
							this->timeout, this->timeout))
	{
		DBG1(DBG_LIB, "opening HTTP request failed: %u", GetLastError());
		return FALSE;
	}
	while (this->headers->remove_first(this->headers, (void**)&hdr) == SUCCESS)
	{
		wcsncat(headers, hdr, countof(headers) - wcslen(headers) - 1);
		if (this->headers->get_count(this->headers))
		{
			wcsncat(headers, L"\r\n", countof(headers) - wcslen(headers) - 1);
		}
		free(hdr);
	}
	if (!WinHttpSendRequest(request, headers, wcslen(headers),
				this->request.ptr, this->request.len, this->request.len, 0))
	{
		DBG1(DBG_LIB, "sending HTTP request failed: %u", GetLastError());
		return FALSE;
	}
	return TRUE;
}

/**
 * Read back result and invoke receive callback
 */
static bool read_result(private_winhttp_fetcher_t *this, HINTERNET request,
						void *user)
{
	DWORD received;
	char buf[1024];
	uint32_t code;
	DWORD codelen = sizeof(code);

	if (!WinHttpReceiveResponse(request, NULL))
	{
		DBG1(DBG_LIB, "reading HTTP response header failed: %u", GetLastError());
		return FALSE;
	}
	if (!WinHttpQueryHeaders(request,
				WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER,
				NULL, &code, &codelen, NULL))
	{
		DBG1(DBG_LIB, "reading HTTP status code failed: %u", GetLastError());
		return FALSE;
	}
	if (this->result)
	{
		*this->result = code;
	}
	if (code < 200 || code >= 300)
	{	/* non-successful HTTP status code */
		if (!this->result)
		{
			DBG1(DBG_LIB, "HTTP request failed with status %u", code);
		}
		return FALSE;
	}
	if (this->cb == fetcher_default_callback)
	{
		*(chunk_t*)user = chunk_empty;
	}
	while (TRUE)
	{
		if (!WinHttpReadData(request, buf, sizeof(buf), &received))
		{
			DBG1(DBG_LIB, "reading HTTP response failed: %u", GetLastError());
			return FALSE;
		}
		if (received == 0)
		{
			/* end of response */
			break;
		}
		if (!this->cb(user, chunk_create(buf, received)))
		{
			DBG1(DBG_LIB, "processing response failed or canceled");
			return FALSE;
		}
	}
	return TRUE;
}

/**
 * Parse an uri to wide string host and path, optionally set flags and port
 */
static bool parse_uri(private_winhttp_fetcher_t *this, char *uri,
					  LPWSTR host, int hostlen, LPWSTR path, int pathlen,
					  LPWSTR user, int userlen, LPWSTR pass, int passlen,
					  DWORD *flags, INTERNET_PORT *port)
{
	WCHAR wuri[512], extra[256];
	URL_COMPONENTS comps = {
		.dwStructSize = sizeof(URL_COMPONENTS),
		.lpszHostName = host,
		.dwHostNameLength = hostlen,
		.lpszUrlPath = path,
		.dwUrlPathLength = pathlen,
		.lpszUserName = user,
		.dwUserNameLength = userlen,
		.lpszPassword = pass,
		.dwPasswordLength = passlen,
		.lpszExtraInfo = extra,
		.dwExtraInfoLength = countof(extra),
	};

	if (!MultiByteToWideChar(CP_THREAD_ACP, 0, uri, -1, wuri, countof(wuri)))
	{
		DBG1(DBG_LIB, "converting URI failed: %u", GetLastError());
		return FALSE;
	}
	if (!WinHttpCrackUrl(wuri, 0, ICU_ESCAPE, &comps))
	{
		DBG1(DBG_LIB, "cracking URI failed: %u", GetLastError());
		return FALSE;
	}
	if (comps.nScheme == INTERNET_SCHEME_HTTPS)
	{
		*flags |= WINHTTP_FLAG_SECURE;
	}
	if (comps.dwExtraInfoLength)
	{
		wcsncat(path, extra, pathlen - comps.dwUrlPathLength - 1);
	}
	if (comps.nPort)
	{
		*port = comps.nPort;
	}
	return TRUE;
}

/**
 * Set credentials for basic authentication, if given
 */
static bool set_credentials(private_winhttp_fetcher_t *this,
							HINTERNET *request, LPWSTR user, LPWSTR pass)
{
	if (!wcslen(user) && !wcslen(pass))
	{	/* skip */
		return TRUE;
	}
	return WinHttpSetCredentials(request, WINHTTP_AUTH_TARGET_SERVER,
								 WINHTTP_AUTH_SCHEME_BASIC, user, pass, NULL);
}

METHOD(fetcher_t, fetch, status_t,
	private_winhttp_fetcher_t *this, char *uri, void *userdata)
{
	INTERNET_PORT port = INTERNET_DEFAULT_PORT;
	status_t status = FAILED;
	DWORD flags = 0;
	HINTERNET connection, request;
	WCHAR host[256], path[512], user[256], pass[256], *method;

	if (this->request.len)
	{
		method = L"POST";
	}
	else
	{
		method = L"GET";
	}

	if (this->result)
	{	/* zero-initialize for early failures */
		*this->result = 0;
	}

	if (parse_uri(this, uri, host, countof(host), path, countof(path),
				  user, countof(user), pass, countof(pass), &flags, &port))
	{
		connection = WinHttpConnect(this->session, host, port, 0);
		if (connection)
		{
			request = WinHttpOpenRequest(connection, method, path, NULL,
										 WINHTTP_NO_REFERER,
										 WINHTTP_DEFAULT_ACCEPT_TYPES, flags);
			if (request)
			{
				if (set_credentials(this, request, user, pass) &&
					send_request(this, request) &&
					read_result(this, request, userdata))
				{
					status = SUCCESS;
				}
				WinHttpCloseHandle(request);
			}
			else
			{
				DBG1(DBG_LIB, "opening request failed: %u", GetLastError());
			}
			WinHttpCloseHandle(connection);
		}
		else
		{
			DBG1(DBG_LIB, "connection failed: %u", GetLastError());
		}
	}
	return status;
}

/**
 * Append an header as wide string
 */
static bool append_header(private_winhttp_fetcher_t *this, char *name)
{
	int len;
	LPWSTR buf;

	len = MultiByteToWideChar(CP_THREAD_ACP, 0, name, -1, NULL, 0);
	if (!len)
	{
		return FALSE;
	}
	buf = calloc(len, sizeof(WCHAR));
	if (!MultiByteToWideChar(CP_THREAD_ACP, 0, name, -1, buf, len))
	{
		free(buf);
		return FALSE;
	}
	this->headers->insert_last(this->headers, buf);
	return TRUE;
}

METHOD(fetcher_t, set_option, bool,
	private_winhttp_fetcher_t *this, fetcher_option_t option, ...)
{
	bool supported = TRUE;
	char buf[128];
	va_list args;

	va_start(args, option);
	switch (option)
	{
		case FETCH_REQUEST_DATA:
			this->request = va_arg(args, chunk_t);
			break;
		case FETCH_REQUEST_TYPE:
			snprintf(buf, sizeof(buf), "Content-Type: %s", va_arg(args, char*));
			supported = append_header(this, buf);
			break;
		case FETCH_REQUEST_HEADER:
			supported = append_header(this, va_arg(args, char*));
			break;
		case FETCH_TIMEOUT:
			this->timeout = va_arg(args, u_int) * 1000;
			break;
		case FETCH_CALLBACK:
			this->cb = va_arg(args, fetcher_callback_t);
			break;
		case FETCH_RESPONSE_CODE:
			this->result = va_arg(args, u_int*);
			break;
		case FETCH_SOURCEIP:
			/* not supported, FALL */
		default:
			supported = FALSE;
			break;
	}
	va_end(args);
	return supported;
}

METHOD(fetcher_t, destroy, void,
	private_winhttp_fetcher_t *this)
{
	if (this->session)
	{
		WinHttpCloseHandle(this->session);
	}
	this->headers->destroy_function(this->headers, free);
	free(this);
}
/*
 * Described in header.
 */
winhttp_fetcher_t *winhttp_fetcher_create()
{
	private_winhttp_fetcher_t *this;

	INIT(this,
		.public = {
			.interface = {
				.fetch = _fetch,
				.set_option = _set_option,
				.destroy = _destroy,
			},
		},
		.cb = fetcher_default_callback,
		.headers = linked_list_create(),
		.session = WinHttpOpen(L"strongSwan WinHTTP fetcher",
							WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,
							WINHTTP_NO_PROXY_NAME,
							WINHTTP_NO_PROXY_BYPASS, 0),
	);

	if (!this->session)
	{
		destroy(this);
		return NULL;
	}

	return &this->public;
}
