/*
 * 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 "common.h"
#include "git2.h"
#include "http_parser.h"
#include "vector.h"
#include "trace.h"
#include "global.h"
#include "httpclient.h"
#include "http.h"
#include "net.h"
#include "stream.h"
#include "streams/socket.h"
#include "streams/tls.h"
#include "auth.h"

#define GIT_READ_BUFFER_SIZE 8192

typedef struct {
	git_net_url url;
	git_stream *stream;
} git_http_server;

typedef enum {
	NONE = 0,
	SENDING_BODY,
	SENT_REQUEST,
	READING_RESPONSE,
	READING_BODY,
	DONE
} http_client_state;

/* Parser state */
typedef enum {
	PARSE_HEADER_NONE = 0,
	PARSE_HEADER_NAME,
	PARSE_HEADER_VALUE,
	PARSE_HEADER_COMPLETE
} parse_header_state;

typedef enum {
	PARSE_STATUS_OK,
	PARSE_STATUS_ERROR
} parse_status;

typedef struct {
	git_http_client *client;
	git_http_response *response;

	/* Temporary buffers to avoid extra mallocs */
	git_buf parse_header_name;
	git_buf parse_header_value;

	/* Parser state */
	int error;
	parse_status parse_status;

	/* Headers parsing */
	parse_header_state parse_header_state;

	/* Body parsing */
	char *output_buf;       /* Caller's output buffer */
	size_t output_size;     /* Size of caller's output buffer */
	size_t output_written;  /* Bytes we've written to output buffer */
} http_parser_context;

/* HTTP client connection */
struct git_http_client {
	git_http_client_options opts;

	http_client_state state;

	http_parser parser;

	git_http_server server;
	git_http_server proxy;

	unsigned request_count;
	unsigned connected : 1,
	         keepalive : 1;

	/* Temporary buffers to avoid extra mallocs */
	git_buf request_msg;
	git_buf read_buf;

	/* A subset of information from the request */
	size_t request_body_len,
	       request_body_remain;
};

bool git_http_response_is_redirect(git_http_response *response)
{
	return (response->status == 301 ||
	        response->status == 302 ||
	        response->status == 303 ||
		response->status == 307 ||
		response->status == 308);
}

void git_http_response_dispose(git_http_response *response)
{
	assert(response);

	git__free(response->content_type);
	git__free(response->location);

	memset(response, 0, sizeof(git_http_response));
}

static int on_header_complete(http_parser *parser)
{
	http_parser_context *ctx = (http_parser_context *) parser->data;
	git_http_response *response = ctx->response;

	git_buf *name = &ctx->parse_header_name;
	git_buf *value = &ctx->parse_header_value;

	if (!strcasecmp("Content-Type", name->ptr)) {
		if (response->content_type) {
			git_error_set(GIT_ERROR_NET,
			              "multiple content-type headers");
			return -1;
		}

		response->content_type =
			git__strndup(value->ptr, value->size);
		GIT_ERROR_CHECK_ALLOC(ctx->response->content_type);
	} else if (!strcasecmp("Content-Length", name->ptr)) {
		int64_t len;

		if (response->content_length) {
			git_error_set(GIT_ERROR_NET,
			              "multiple content-length headers");
			return -1;
		}

		if (git__strntol64(&len, value->ptr, value->size,
		                   NULL, 10) < 0 || len < 0) {
			git_error_set(GIT_ERROR_NET,
			              "invalid content-length");
			return -1;
		}

		response->content_length = (size_t)len;
	} else if (!strcasecmp("Location", name->ptr)) {
		if (response->location) {
			git_error_set(GIT_ERROR_NET,
				"multiple location headers");
			return -1;
		}

		response->location = git__strndup(value->ptr, value->size);
		GIT_ERROR_CHECK_ALLOC(response->location);
	}

	return 0;
}

static int on_header_field(http_parser *parser, const char *str, size_t len)
{
	http_parser_context *ctx = (http_parser_context *) parser->data;

	switch (ctx->parse_header_state) {
	/*
	 * We last saw a header value, process the name/value pair and
	 * get ready to handle this new name.
	 */
	case PARSE_HEADER_VALUE:
		if (on_header_complete(parser) < 0)
			return ctx->parse_status = PARSE_STATUS_ERROR;

		git_buf_clear(&ctx->parse_header_name);
		git_buf_clear(&ctx->parse_header_value);
		/* Fall through */

	case PARSE_HEADER_NONE:
	case PARSE_HEADER_NAME:
		ctx->parse_header_state = PARSE_HEADER_NAME;

		if (git_buf_put(&ctx->parse_header_name, str, len) < 0)
			return ctx->parse_status = PARSE_STATUS_ERROR;

		break;

	default:
		git_error_set(GIT_ERROR_NET,
		              "header name seen at unexpected time");
		return ctx->parse_status = PARSE_STATUS_ERROR;
	}

	return 0;
}

static int on_header_value(http_parser *parser, const char *str, size_t len)
{
	http_parser_context *ctx = (http_parser_context *) parser->data;

	switch (ctx->parse_header_state) {
	case PARSE_HEADER_NAME:
	case PARSE_HEADER_VALUE:
		ctx->parse_header_state = PARSE_HEADER_VALUE;

		if (git_buf_put(&ctx->parse_header_value, str, len) < 0)
			return ctx->parse_status = PARSE_STATUS_ERROR;

		break;

	default:
		git_error_set(GIT_ERROR_NET,
		              "header value seen at unexpected time");
		return ctx->parse_status = PARSE_STATUS_ERROR;
	}

	return 0;
}

static int on_headers_complete(http_parser *parser)
{
	http_parser_context *ctx = (http_parser_context *) parser->data;

	/* Finalize the last seen header */
	switch (ctx->parse_header_state) {
	case PARSE_HEADER_VALUE:
		if (on_header_complete(parser) < 0)
			return ctx->parse_status = PARSE_STATUS_ERROR;

		/* Fall through */

	case PARSE_HEADER_NONE:
		ctx->parse_header_state = PARSE_HEADER_COMPLETE;
		break;

	default:
		git_error_set(GIT_ERROR_NET,
		              "header completion at unexpected time");
		return ctx->parse_status = PARSE_STATUS_ERROR;
	}

	ctx->response->status = parser->status_code;
	ctx->client->keepalive = http_should_keep_alive(parser);

	/* Stop parsing. */
	http_parser_pause(parser, 1);

	ctx->client->state = READING_BODY;
	return 0;
}

static int on_body(http_parser *parser, const char *buf, size_t len)
{
	http_parser_context *ctx = (http_parser_context *) parser->data;
	size_t max_len;

	assert(ctx->output_size >= ctx->output_written);

	max_len = min(ctx->output_size - ctx->output_written, len);
	max_len = min(max_len, INT_MAX);

	memcpy(ctx->output_buf + ctx->output_written, buf, max_len);
	ctx->output_written += max_len;

	return 0;
}

static int on_message_complete(http_parser *parser)
{
	http_parser_context *ctx = (http_parser_context *) parser->data;

	ctx->client->state = DONE;
	return 0;
}

GIT_INLINE(int) stream_write(
	git_http_server *server,
	const char *data,
	size_t len)
{
	git_trace(GIT_TRACE_TRACE,
	          "Sending request:\n%.*s", (int)len, data);

	return git_stream__write_full(server->stream, data, len, 0);
}

const char *name_for_method(git_http_method method)
{
	switch (method) {
	case GIT_HTTP_METHOD_GET:
		return "GET";
	case GIT_HTTP_METHOD_POST:
		return "POST";
	}

	return NULL;
}

static int generate_request(
	git_http_client *client,
	git_http_request *request)
{
	const char *method, *path, *sep, *query;
	git_buf *buf;
	size_t i;

	assert(client && request);

	git_buf_clear(&client->request_msg);
	buf = &client->request_msg;

	method = name_for_method(request->method);
	path = request->url->path ? request->url->path : "/";
	sep = request->url->query ? "?" : "";
	query = request->url->query ? request->url->query : "";

	git_buf_printf(buf, "%s %s%s%s HTTP/1.1\r\n",
		method, path, sep, query);

	git_buf_puts(buf, "User-Agent: ");
	git_http__user_agent(buf);
	git_buf_puts(buf, "\r\n");
	git_buf_printf(buf, "Host: %s", request->url->host);

	if (!git_net_url_is_default_port(request->url))
		git_buf_printf(buf, ":%s", request->url->port);

	git_buf_puts(buf, "\r\n");

	if (request->accept)
		git_buf_printf(buf, "Accept: %s\r\n", request->accept);
	else
		git_buf_puts(buf, "Accept: */*\r\n");

	if (request->content_type)
		git_buf_printf(buf, "Content-Type: %s\r\n",
			request->content_type);

	if (request->chunked)
		git_buf_puts(buf, "Transfer-Encoding: chunked\r\n");

	if (request->content_length > 0)
		git_buf_printf(buf, "Content-Length: %"PRIuZ "\r\n",
			request->content_length);

	if (request->expect_continue)
		git_buf_printf(buf, "Expect: 100-continue\r\n");

	if (request->custom_headers) {
		for (i = 0; i < request->custom_headers->count; i++) {
			const char *hdr = request->custom_headers->strings[i];

			if (hdr)
				git_buf_printf(buf, "%s\r\n", hdr);
		}
	}

	git_buf_puts(buf, "\r\n");

	if (git_buf_oom(buf))
		return -1;

	return 0;
}

static int check_certificate(
	git_stream *stream,
	git_net_url *url,
	int is_valid,
	git_transport_certificate_check_cb cert_cb,
	void *cert_cb_payload)
{
	git_cert *cert;
	git_error_state last_error = {0};
	int error;

	if ((error = git_stream_certificate(&cert, stream)) < 0)
		return error;

	git_error_state_capture(&last_error, GIT_ECERTIFICATE);

	error = cert_cb(cert, is_valid, url->host, cert_cb_payload);

	if (error == GIT_PASSTHROUGH && !is_valid)
		return git_error_state_restore(&last_error);
	else if (error == GIT_PASSTHROUGH)
		error = 0;
	else if (error && !git_error_last())
		git_error_set(GIT_ERROR_NET,
		              "user rejected certificate for %s", url->host);

	git_error_state_free(&last_error);
	return error;
}

static int stream_connect(
	git_stream *stream,
	git_net_url *url,
	git_transport_certificate_check_cb cert_cb,
	void *cb_payload)
{
	int error;

	GIT_ERROR_CHECK_VERSION(stream, GIT_STREAM_VERSION, "git_stream");

	error = git_stream_connect(stream);

	if (error && error != GIT_ECERTIFICATE)
		return error;

	if (git_stream_is_encrypted(stream) && cert_cb != NULL)
		error = check_certificate(stream, url, !error,
		                          cert_cb, cb_payload);

	return error;
}

/*
 * Updates the server data structure with the new URL; returns 1 if the server
 * has changed and we need to reconnect, returns 0 otherwise.
 */
GIT_INLINE(int) server_setup_from_url(
	git_http_server *server,
	git_net_url *url)
{
	if (!server->url.scheme || strcmp(server->url.scheme, url->scheme) ||
	    !server->url.host || strcmp(server->url.host, url->host) ||
	    !server->url.port || strcmp(server->url.port, url->port)) {
		git__free(server->url.scheme);
		git__free(server->url.host);
		git__free(server->url.port);

		server->url.scheme = git__strdup(url->scheme);
		GIT_ERROR_CHECK_ALLOC(server->url.scheme);

		server->url.host = git__strdup(url->host);
		GIT_ERROR_CHECK_ALLOC(server->url.host);

		server->url.port = git__strdup(url->port);
		GIT_ERROR_CHECK_ALLOC(server->url.port);

		return 1;
	}

	return 0;
}

static int http_client_setup_hosts(
	git_http_client *client,
	git_http_request *request)
{
	int ret, diff = 0;

	assert(client && request && request->url);

	if ((ret = server_setup_from_url(&client->server, request->url)) < 0)
		return ret;

	diff |= ret;

	if (request->proxy &&
	    (ret = server_setup_from_url(&client->proxy, request->proxy)) < 0)
		return ret;

	diff |= ret;

	if (diff)
		client->connected = 0;

	return 0;
}

static void reset_parser(git_http_client *client)
{
	git_buf_clear(&client->read_buf);
}

static int http_client_connect(git_http_client *client)
{
	git_net_url *url;
	git_stream *proxy_stream = NULL, *stream = NULL;
	git_transport_certificate_check_cb cert_cb;
	void *cb_payload;
	int error;

	if (client->connected && client->keepalive &&
	    (client->state == NONE || client->state == DONE))
		return 0;

	git_trace(GIT_TRACE_DEBUG, "Connecting to %s:%s",
		client->server.url.host, client->server.url.port);

	if (client->server.stream) {
		git_stream_close(client->server.stream);
		git_stream_free(client->server.stream);
		client->server.stream = NULL;
	}

	if (client->proxy.stream) {
		git_stream_close(client->proxy.stream);
		git_stream_free(client->proxy.stream);
		client->proxy.stream = NULL;
	}

	reset_parser(client);

	client->connected = 0;
	client->keepalive = 0;
	client->request_count = 0;

	if (client->proxy.url.host) {
		url = &client->proxy.url;
		cert_cb = client->opts.proxy_certificate_check_cb;
		cb_payload = client->opts.proxy_certificate_check_payload;
	} else {
		url = &client->server.url;
		cert_cb = client->opts.server_certificate_check_cb;
		cb_payload = client->opts.server_certificate_check_payload;
	}

	if (strcasecmp(url->scheme, "https") == 0) {
		error = git_tls_stream_new(&stream, url->host, url->port);
	} else if (strcasecmp(url->scheme, "http") == 0) {
		error = git_socket_stream_new(&stream, url->host, url->port);
	} else {
		git_error_set(GIT_ERROR_NET, "unknown http scheme '%s'",
		              url->scheme);
		error = -1;
	}

	if (error < 0)
		goto on_error;

	if ((error = stream_connect(stream, url, cert_cb, cb_payload)) < 0)
		goto on_error;

	client->proxy.stream = proxy_stream;
	client->server.stream = stream;
	client->connected = 1;
	return 0;

on_error:
	if (stream) {
		git_stream_close(stream);
		git_stream_free(stream);
	}

	if (proxy_stream) {
		git_stream_close(proxy_stream);
		git_stream_free(proxy_stream);
	}

	return error;
}

int git_http_client_send_request(
	git_http_client *client,
	git_http_request *request)
{
	int error = -1;

	assert(client && request);

	http_parser_init(&client->parser, HTTP_RESPONSE);
	git_buf_clear(&client->read_buf);

	if (git_trace_level() >= GIT_TRACE_DEBUG) {
		git_buf url = GIT_BUF_INIT;
		git_net_url_fmt(&url, request->url);
		git_trace(GIT_TRACE_DEBUG, "Sending %s request to %s",
		          name_for_method(request->method),
		          url.ptr ? url.ptr : "<invalid>");
		git_buf_dispose(&url);
	}

	if ((error = http_client_setup_hosts(client, request)) < 0 ||
	    (error = http_client_connect(client)) < 0 ||
	    (error = generate_request(client, request)) < 0 ||
	    (error = stream_write(&client->server,
	                          client->request_msg.ptr,
	                          client->request_msg.size)) < 0)
		goto done;

	if (request->content_length || request->chunked) {
		client->state = SENDING_BODY;
		client->request_body_len = request->content_length;
		client->request_body_remain = request->content_length;
	} else {
		client->state = SENT_REQUEST;
	}

done:
	return error;
}

int git_http_client_send_body(
	git_http_client *client,
	const char *buffer,
	size_t buffer_len)
{
	int error;

	assert(client && client->state == SENDING_BODY);
	assert(buffer_len <= client->request_body_remain);

	error = stream_write(&client->server, buffer, buffer_len);

	if (error == 0)
		client->request_body_remain -= buffer_len;

	return error;
}

static bool parser_settings_initialized;
static http_parser_settings parser_settings;

GIT_INLINE(http_parser_settings *) http_client_parser_settings(void)
{
	if (! parser_settings_initialized) {
		parser_settings.on_header_field = on_header_field;
		parser_settings.on_header_value = on_header_value;
		parser_settings.on_headers_complete = on_headers_complete;
		parser_settings.on_body = on_body;
		parser_settings.on_message_complete = on_message_complete;

		parser_settings_initialized = true;
	}

	return &parser_settings;
}

GIT_INLINE(int) client_read(git_http_client *client)
{
	char *buf = client->read_buf.ptr + client->read_buf.size;
	size_t max_len;
	ssize_t read_len;

	/*
	 * We use a git_buf for convenience, but statically allocate it and
	 * don't resize.  Limit our consumption to INT_MAX since calling
	 * functions use an int return type to return number of bytes read.
	 */
	max_len = client->read_buf.asize - client->read_buf.size;
	max_len = min(max_len, INT_MAX);

	if (max_len == 0) {
		git_error_set(GIT_ERROR_NET, "no room in output buffer");
		return -1;
	}

	read_len = git_stream_read(client->server.stream, buf, max_len);

	if (read_len >= 0) {
		client->read_buf.size += read_len;

		git_trace(GIT_TRACE_TRACE, "Received:\n%.*s",
		          (int)read_len, buf);
	}

	return (int)read_len;
}

GIT_INLINE(int) client_read_and_parse(git_http_client *client)
{
	http_parser *parser = &client->parser;
	http_parser_context *ctx = (http_parser_context *) parser->data;
	unsigned char http_errno;
	int read_len;
	size_t parsed_len;

	/*
	 * If we have data in our read buffer, that means we stopped early
	 * when parsing headers.  Use the data in the read buffer instead of
	 * reading more from the socket.
	 */
	if (!client->read_buf.size && (read_len = client_read(client)) < 0)
		return read_len;

	parsed_len = http_parser_execute(parser,
		http_client_parser_settings(),
		client->read_buf.ptr,
		client->read_buf.size);
	http_errno = client->parser.http_errno;

	if (parsed_len > INT_MAX) {
		git_error_set(GIT_ERROR_NET, "unexpectedly large parse");
		return -1;
	}

	if (parser->upgrade) {
		git_error_set(GIT_ERROR_NET, "server requested upgrade");
		return -1;
	}

	if (ctx->parse_status == PARSE_STATUS_ERROR) {
		client->connected = 0;
		return ctx->error ? ctx->error : -1;
	}

	/*
	 * If we finished reading the headers or body, we paused parsing.
	 * Otherwise the parser will start filling the body, or even parse
	 * a new response if the server pipelined us multiple responses.
	 * (This can happen in response to an expect/continue request,
	 * where the server gives you a 100 and 200 simultaneously.)
	 */
	if (http_errno == HPE_PAUSED) {
		/*
		 * http-parser has a "feature" where it will not deliver the
		 * final byte when paused in a callback.  Consume that byte.
		 * https://github.com/nodejs/http-parser/issues/97
		 */
		assert(client->read_buf.size > parsed_len);

		http_parser_pause(parser, 0);

		parsed_len += http_parser_execute(parser,
			http_client_parser_settings(),
			client->read_buf.ptr + parsed_len,
			1);
	}

	/* Most failures will be reported in http_errno */
	else if (parser->http_errno != HPE_OK) {
		git_error_set(GIT_ERROR_NET, "http parser error: %s",
		              http_errno_description(http_errno));
		return -1;
	}

	/* Otherwise we should have consumed the entire buffer. */
	else if (parsed_len != client->read_buf.size) {
		git_error_set(GIT_ERROR_NET,
		              "http parser did not consume entire buffer: %s",
			      http_errno_description(http_errno));
		return -1;
	}

	/* recv returned 0, the server hung up on us */
	else if (!parsed_len) {
		git_error_set(GIT_ERROR_NET, "unexpected EOF");
		return -1;
	}

	git_buf_consume_bytes(&client->read_buf, parsed_len);

	return (int)parsed_len;
}

int git_http_client_read_response(
	git_http_response *response,
	git_http_client *client)
{
	http_parser_context parser_context = {0};
	int error;

	assert(response && client);

	if (client->state == SENDING_BODY) {
		if (client->request_body_len && client->request_body_remain) {
			git_error_set(GIT_ERROR_NET, "truncated write");
			return -1;
		}

		client->state = SENT_REQUEST;
	} else if (client->state != SENT_REQUEST) {
		git_error_set(GIT_ERROR_NET, "client is in invalid state");
		return -1;
	}

	client->state = READING_RESPONSE;
	client->parser.data = &parser_context;

	parser_context.client = client;
	parser_context.response = response;

	while (client->state == READING_RESPONSE) {
		if ((error = client_read_and_parse(client)) < 0)
			goto done;
	}

	assert(client->state == READING_BODY || client->state == DONE);

done:
	git_buf_dispose(&parser_context.parse_header_name);
	git_buf_dispose(&parser_context.parse_header_value);

	return error;
}

int git_http_client_read_body(
	git_http_client *client,
	char *buffer,
	size_t buffer_size)
{
	http_parser_context parser_context = {0};
	int error = 0;

	if (client->state == DONE)
		return 0;

	if (client->state != READING_BODY) {
		git_error_set(GIT_ERROR_NET, "client is in invalid state");
		return -1;
	}

	/*
	 * Now we'll read from the socket and http_parser will pipeline the
	 * data directly to the client.
	 */

	parser_context.client = client;
	parser_context.output_buf = buffer;
	parser_context.output_size = buffer_size;

	client->parser.data = &parser_context;

	/*
	 * Clients expect to get a non-zero amount of data from us.
	 * With a sufficiently small buffer, one might only read a chunk
	 * length.  Loop until we actually have data to return.
	 */
	while (!parser_context.output_written) {
		error = client_read_and_parse(client);

		if (error <= 0)
			goto done;
	}

	assert(parser_context.output_written <= INT_MAX);
	error = (int)parser_context.output_written;

done:
	if (error < 0)
		client->connected = 0;

	return error;
}

/*
 * Create an http_client capable of communicating with the given remote
 * host.
 */
int git_http_client_new(
	git_http_client **out,
	git_http_client_options *opts)
{
	git_http_client *client;

	assert(out);

	client = git__calloc(1, sizeof(git_http_client));
	GIT_ERROR_CHECK_ALLOC(client);

	git_buf_init(&client->read_buf, GIT_READ_BUFFER_SIZE);
	GIT_ERROR_CHECK_ALLOC(client->read_buf.ptr);

	if (opts)
		memcpy(&client->opts, opts, sizeof(git_http_client_options));

	*out = client;
	return 0;
}

GIT_INLINE(void) http_server_close(git_http_server *server)
{
	if (server->stream) {
		git_stream_close(server->stream);
		git_stream_free(server->stream);
		server->stream = NULL;
	}

	git_net_url_dispose(&server->url);
}

static void http_client_close(git_http_client *client)
{
	http_server_close(&client->server);
	http_server_close(&client->proxy);

	git_buf_dispose(&client->request_msg);

	client->state = 0;
	client->request_count = 0;
	client->connected = 0;
	client->keepalive = 0;
}

void git_http_client_free(git_http_client *client)
{
	if (!client)
		return;

	http_client_close(client);
	git_buf_dispose(&client->read_buf);
	git__free(client);
}
