/* redsocks - transparent TCP-to-proxy redirector
 * Copyright (C) 2007-2011 Leonid Evdokimov <leon@darkk.net.ru>
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License.  You may obtain a copy
 * of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
 * License for the specific language governing permissions and limitations
 * under the License.
 *
 *
 * http-relay upstream module for redsocks
 */

#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <time.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "log.h"
#include "redsocks.h"
#include "http-auth.h"
#include "utils.h"

#define HTTP_HEAD_WM_HIGH (4096)

typedef enum httpr_state_t {
	httpr_new,
	httpr_recv_request_headers,
	httpr_request_sent,
	httpr_reply_came,
	httpr_headers_skipped,
	httpr_MAX,
} httpr_state;

typedef struct httpr_buffer_t {
	char *buff;
	int len;
	int max_len;
} httpr_buffer;

typedef struct httpr_client_t {
	char *firstline;
	char *host;
	int has_host;
	httpr_buffer client_buffer;
	httpr_buffer relay_buffer;
} httpr_client;

extern const char *auth_request_header;
extern const char *auth_response_header;

static void httpr_connect_relay(redsocks_client *client);

static int httpr_buffer_init(httpr_buffer *buff)
{
	buff->max_len = 4096;
	buff->len = 0;
	buff->buff = calloc(buff->max_len, 1);
	if (!buff->buff)
		return -1;
	return 0;
}

static void httpr_buffer_fini(httpr_buffer *buff)
{
	free(buff->buff);
	buff->buff = NULL;
}

static int httpr_buffer_append(httpr_buffer *buff, const char *data, int len)
{
	while (buff->len + len + 1 > buff->max_len) {
		/* double the buffer size */
		buff->max_len *= 2;
	}
	char *new_buff = calloc(buff->max_len, 1);
	if (!new_buff) {
		return -1;
	}
	memcpy(new_buff, buff->buff, buff->len);
	memcpy(new_buff + buff->len, data, len);
	buff->len += len;
	new_buff[buff->len] = '\0';
	free(buff->buff);
	buff->buff = new_buff;
	return 0;
}

static void httpr_client_init(redsocks_client *client)
{
	httpr_client *httpr = red_payload(client);

	client->state = httpr_new;
	memset(httpr, 0, sizeof(*httpr));
	httpr_buffer_init(&httpr->client_buffer);
	httpr_buffer_init(&httpr->relay_buffer);
}

static void httpr_client_fini(redsocks_client *client)
{
	httpr_client *httpr = red_payload(client);

	free(httpr->firstline);
	httpr->firstline = NULL;
	free(httpr->host);
	httpr->host = NULL;
	httpr_buffer_fini(&httpr->client_buffer);
	httpr_buffer_fini(&httpr->relay_buffer);
}

static void httpr_instance_init(redsocks_instance *instance)
{
	log_error(LOG_WARNING, "You should avoid `http-relay`, e.g. due to CVE-2009-0801");
}

static void httpr_instance_fini(redsocks_instance *instance)
{
	http_auth *auth = red_http_auth(instance);
	free(auth->last_auth_query);
	auth->last_auth_query = NULL;
}

static void httpr_relay_read_cb(struct bufferevent *buffev, void *_arg)
{
	redsocks_client *client = _arg;
	httpr_client *httpr = red_payload(client);
	int dropped = 0;

	assert(client->state >= httpr_request_sent);

	redsocks_touch_client(client);

	httpr_buffer_fini(&httpr->relay_buffer);
	httpr_buffer_init(&httpr->relay_buffer);

	if (client->state == httpr_request_sent) {
		size_t len = evbuffer_get_length(buffev->input);
		char *line = redsocks_evbuffer_readline(buffev->input);
		if (line) {
			httpr_buffer_append(&httpr->relay_buffer, line, strlen(line));
			httpr_buffer_append(&httpr->relay_buffer, "\r\n", 2);
			unsigned int code;
			if (sscanf(line, "HTTP/%*u.%*u %u", &code) == 1) { // 1 == one _assigned_ match
				if (code == 407) { // auth failed
					http_auth *auth = red_http_auth(client->instance);

					if (auth->last_auth_query != NULL && auth->last_auth_count == 1) {
						redsocks_log_error(client, LOG_NOTICE, "HTTP Proxy auth failed: %s", line);
						redsocks_drop_client(client);
						dropped = 1;
					} else if (client->instance->config.login == NULL || client->instance->config.password == NULL) {
						redsocks_log_error(client, LOG_NOTICE, "HTTP Proxy auth required, but no login/password configured: %s", line);
						redsocks_drop_client(client);
						dropped = 1;
					} else {
						free(line);
						char *auth_request = http_auth_request_header(buffev->input, NULL);

						if (!auth_request) {
							redsocks_log_error(client, LOG_NOTICE, "HTTP Proxy auth required, but no <%s> header found: %s", auth_request_header, line);
							redsocks_drop_client(client);
							dropped = 1;
						} else {
							free(auth->last_auth_query);
							char *ptr = auth_request;

							ptr += strlen(auth_request_header);
							while (isspace(*ptr))
								ptr++;

							auth->last_auth_query = calloc(strlen(ptr) + 1, 1);
							strcpy(auth->last_auth_query, ptr);
							auth->last_auth_count = 0;

							free(auth_request);

							httpr_buffer_fini(&httpr->relay_buffer);

							if (bufferevent_disable(client->relay, EV_WRITE)) {
								redsocks_log_errno(client, LOG_ERR, "bufferevent_disable");
								return;
							}

							/* close relay tunnel */
							redsocks_bufferevent_free(client->relay);

							/* set to initial state*/
							client->state = httpr_recv_request_headers;

							/* and reconnect */
							redsocks_connect_relay(client);
							return;
						}
					}
				} else if (100 <= code && code <= 999) {
					client->state = httpr_reply_came;
				} else {
					redsocks_log_error(client, LOG_NOTICE, "HTTP Proxy error: %s", line);
					redsocks_drop_client(client);
					dropped = 1;
				}
			} else {
				redsocks_log_error(client, LOG_NOTICE, "HTTP Proxy bad firstline: %s", line);
				redsocks_drop_client(client);
				dropped = 1;
			}
			free(line);
		}
		else if (len >= HTTP_HEAD_WM_HIGH) {
			redsocks_log_error(client, LOG_NOTICE, "HTTP Proxy reply is too long, %zu bytes", len);
			redsocks_drop_client(client);
			dropped = 1;
		}
	}

	if (dropped)
		return;

	while (client->state == httpr_reply_came) {
		char *line = redsocks_evbuffer_readline(buffev->input);
		if (line) {
			httpr_buffer_append(&httpr->relay_buffer, line, strlen(line));
			httpr_buffer_append(&httpr->relay_buffer, "\r\n", 2);
			if (strlen(line) == 0) {
				client->state = httpr_headers_skipped;
			}
			free(line);
		}
		else {
			break;
		}
	}

	if (client->state == httpr_headers_skipped) {
		if (bufferevent_write(client->client, httpr->relay_buffer.buff, httpr->relay_buffer.len) != 0) {
			redsocks_log_error(client, LOG_NOTICE, "bufferevent_write");
			redsocks_drop_client(client);
			return;
		}
		redsocks_start_relay(client);
	}

}

static void httpr_relay_write_cb(struct bufferevent *buffev, void *_arg)
{
	redsocks_client *client = _arg;
	httpr_client *httpr = red_payload(client);
	int len = 0;

	assert(client->state >= httpr_recv_request_headers);

	redsocks_touch_client(client);

	if (client->state == httpr_recv_request_headers) {
		if (httpr->firstline) {
			len = bufferevent_write(client->relay, httpr->firstline, strlen(httpr->firstline));
			if (len < 0) {
				redsocks_log_errno(client, LOG_ERR, "bufferevent_write");
				redsocks_drop_client(client);
				return;
			}
		}


		http_auth *auth = red_http_auth(client->instance);
		++auth->last_auth_count;

		const char *auth_scheme = NULL;
		char *auth_string = NULL;

		if (auth->last_auth_query != NULL) {
			/* find previous auth challange */

			if (strncasecmp(auth->last_auth_query, "Basic", 5) == 0) {
				auth_string = basic_authentication_encode(client->instance->config.login, client->instance->config.password);
				auth_scheme = "Basic";
			} else if (strncasecmp(auth->last_auth_query, "Digest", 6) == 0 && httpr->firstline) {
				/* calculate method & uri */
				char *ptr = strchr(httpr->firstline, ' '), *ptr2;
				char *method = calloc(ptr - httpr->firstline + 1, 1);
				memcpy(method, httpr->firstline, ptr - httpr->firstline);
				method[ptr - httpr->firstline] = 0;

				ptr = strchr(httpr->firstline, '/');
				if (!ptr || *++ptr != '/') {
					free(method);
					redsocks_log_error(client, LOG_NOTICE, "malformed request came");
					redsocks_drop_client(client);
					return;
				}
				if (!(ptr = strchr(++ptr, '/')) || !(ptr2 = strchr(ptr, ' '))) {
					free(method);
					redsocks_log_error(client, LOG_NOTICE, "malformed request came");
					redsocks_drop_client(client);
					return;
				}
				char *uri = calloc(ptr2 - ptr + 1, 1);
				memcpy(uri, ptr, ptr2 - ptr);
				uri[ptr2 - ptr] = 0;

				/* prepare an random string for cnounce */
				char cnounce[17];
				snprintf(cnounce, sizeof(cnounce), "%08x%08x", red_randui32(), red_randui32());

				auth_string = digest_authentication_encode(auth->last_auth_query + 7, //line
						client->instance->config.login, client->instance->config.password, //user, pass
						method, uri, auth->last_auth_count, cnounce); // method, path, nc, cnounce

				free(method);
				free(uri);
				auth_scheme = "Digest";
			}
		}

		if (auth_string != NULL) {
			len = 0;
			len |= bufferevent_write(client->relay, auth_response_header, strlen(auth_response_header));
			len |= bufferevent_write(client->relay, " ", 1);
			len |= bufferevent_write(client->relay, auth_scheme, strlen(auth_scheme));
			len |= bufferevent_write(client->relay, " ", 1);
			len |= bufferevent_write(client->relay, auth_string, strlen(auth_string));
			len |= bufferevent_write(client->relay, "\r\n", 2);
			if (len) {
				redsocks_log_errno(client, LOG_ERR, "bufferevent_write");
				redsocks_drop_client(client);
				return;
			}
		}

		free(auth_string);

		len = bufferevent_write(client->relay, httpr->client_buffer.buff, httpr->client_buffer.len);
		if (len < 0) {
			redsocks_log_errno(client, LOG_ERR, "bufferevent_write");
			redsocks_drop_client(client);
			return;
		}

		client->state = httpr_request_sent;

		bufferevent_setwatermark(buffev, EV_READ, 1, HTTP_HEAD_WM_HIGH);
		bufferevent_enable(buffev, EV_READ);
	}
}

// drops client on failure
static int httpr_append_header(redsocks_client *client, char *line)
{
	httpr_client *httpr = red_payload(client);

	if (httpr_buffer_append(&httpr->client_buffer, line, strlen(line)) != 0)
		return -1;
	if (httpr_buffer_append(&httpr->client_buffer, "\x0d\x0a", 2) != 0)
		return -1;
	return 0;
}

// This function is not reenterable
static char *fmt_http_host(struct sockaddr_in addr)
{
	static char host[] = "123.123.123.123:12345";
	if (ntohs(addr.sin_port) == 80)
		return inet_ntoa(addr.sin_addr);
	else {
		snprintf(host, sizeof(host),
				"%s:%u",
				inet_ntoa(addr.sin_addr),
				ntohs(addr.sin_port)
				);
		return host;
	}
}

static int httpr_toss_http_firstline(redsocks_client *client)
{
	httpr_client *httpr = red_payload(client);
	char *uri = NULL;
	char *host = httpr->has_host ? httpr->host : fmt_http_host(client->destaddr);

	assert(httpr->firstline);

	uri = strchr(httpr->firstline, ' ');
	if (uri)
		uri += 1; // one char further
	else {
		redsocks_log_error(client, LOG_NOTICE, "malformed request came");
		goto fail;
	}

	httpr_buffer nbuff;
	if (httpr_buffer_init(&nbuff) != 0) {
		redsocks_log_error(client, LOG_ERR, "httpr_buffer_init");
		goto fail;
	}

	if (httpr_buffer_append(&nbuff, httpr->firstline, uri - httpr->firstline) != 0)
		goto addition_fail;
	if (httpr_buffer_append(&nbuff, "http://", 7) != 0)
		goto addition_fail;
	if (httpr_buffer_append(&nbuff, host, strlen(host)) != 0)
		goto addition_fail;
	if (httpr_buffer_append(&nbuff, uri, strlen(uri)) != 0)
		goto addition_fail;
	if (httpr_buffer_append(&nbuff, "\x0d\x0a", 2) != 0)
		goto addition_fail;

	free(httpr->firstline);

	httpr->firstline = calloc(nbuff.len + 1, 1);
	strcpy(httpr->firstline, nbuff.buff);
	httpr_buffer_fini(&nbuff);
	return 0;

addition_fail:
	httpr_buffer_fini(&nbuff);
fail:
	redsocks_log_error(client, LOG_ERR, "httpr_toss_http_firstline");
	return -1;
}

static void httpr_client_read_content(struct bufferevent *buffev, redsocks_client *client)
{
	httpr_client *httpr = red_payload(client);

	static int post_buffer_len = 64 * 1024;
	char *post_buffer = calloc(post_buffer_len, 1);
	if (!post_buffer) {
		redsocks_log_error(client, LOG_ERR, "run out of memory");
		redsocks_drop_client(client);
		return;
	}
	int error;
	while (true) {
		error = evbuffer_remove(buffev->input, post_buffer, post_buffer_len);
		if (error < 0) {
			free(post_buffer);
			redsocks_log_error(client, LOG_ERR, "evbuffer_remove");
			redsocks_drop_client(client);
			return;
		}
		if (error == 0)
			break;
		httpr_buffer_append(&httpr->client_buffer, post_buffer, error);
		if (client->relay && client->state >= httpr_request_sent) {
			if (bufferevent_write(client->relay, post_buffer, error) != 0) {
				free(post_buffer);
				redsocks_log_error(client, LOG_ERR, "bufferevent_write");
				redsocks_drop_client(client);
				return;
			}
		}

	}
	free(post_buffer);
}

static void httpr_client_read_cb(struct bufferevent *buffev, void *_arg)
{
	redsocks_client *client = _arg;
	httpr_client *httpr = red_payload(client);

	redsocks_touch_client(client);

	if (client->state >= httpr_recv_request_headers) {
		httpr_client_read_content(buffev, client);
		return;
	}

	char *line = NULL;
	int connect_relay = 0;

	while (!connect_relay && (line = redsocks_evbuffer_readline(buffev->input))) {
		int skip_line = 0;
		int do_drop = 0;

		if (strlen(line) > 0) {
			if (!httpr->firstline) {
				httpr->firstline = line;
				line = 0;
			}
			else if (strncasecmp(line, "Host", 4) == 0) {
				httpr->has_host = 1;
				char *ptr = line + 5;
				while (*ptr && isspace(*ptr))
					ptr ++;
				httpr->host = calloc(strlen(ptr) + 1, 1);
				strcpy(httpr->host, ptr);
			} else if (strncasecmp(line, "Proxy-Connection", 16) == 0)
				skip_line = 1;
			else if (strncasecmp(line, "Connection", 10) == 0)
				skip_line = 1;

		}
		else { // last line of request
			if (!httpr->firstline || httpr_toss_http_firstline(client) < 0)
				do_drop = 1;

			if (!httpr->has_host) {
				char host[32]; // "Host: 123.456.789.012:34567"
				int written_wo_null = snprintf(host, sizeof(host), "Host: %s",
				                               fmt_http_host(client->destaddr));
				UNUSED(written_wo_null);
				assert(0 < written_wo_null && written_wo_null < sizeof(host));
				if (httpr_append_header(client, host) < 0)
					do_drop = 1;
			}

			if (httpr_append_header(client, "Proxy-Connection: close") < 0)
				do_drop = 1;

			if (httpr_append_header(client, "Connection: close") < 0)
				do_drop = 1;

			connect_relay = 1;
		}

		if (line && !skip_line)
			if (httpr_append_header(client, line) < 0)
				do_drop = 1;

		free(line);

		if (do_drop) {
			redsocks_drop_client(client);
			return;
		}
	}

	if (connect_relay) {
		client->state = httpr_recv_request_headers;
		httpr_client_read_content(buffev, client);
		redsocks_connect_relay(client);
	}
}

static void httpr_connect_relay(redsocks_client *client)
{
	int error;

	client->client->readcb = httpr_client_read_cb;
	error = bufferevent_enable(client->client, EV_READ);
	if (error) {
		redsocks_log_errno(client, LOG_ERR, "bufferevent_enable");
		redsocks_drop_client(client);
	}
}

relay_subsys http_relay_subsys =
{
	.name                 = "http-relay",
	.payload_len          = sizeof(httpr_client),
	.instance_payload_len = sizeof(http_auth),
	.init                 = httpr_client_init,
	.fini                 = httpr_client_fini,
	.connect_relay        = httpr_connect_relay,
	.readcb               = httpr_relay_read_cb,
	.writecb              = httpr_relay_write_cb,
	.instance_init        = httpr_instance_init,
	.instance_fini        = httpr_instance_fini,
};

/* vim:set tabstop=4 softtabstop=4 shiftwidth=4: */
/* vim:set foldmethod=marker foldlevel=32 foldmarker={,}: */
