/*
 *   Unreal Internet Relay Chat Daemon, src/url.c
 *   (C) 2021 Bram Matthys and the UnrealIRCd team
 *
 *   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 1, or (at your option)
 *   any later version.
 *
 *   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.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with this program; if not, write to the Free Software
 *   Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */

#include "unrealircd.h"

#if defined(__GNUC__)
/* Ignore these for src/dns.c & src/url_unreal.c
 * Quoting https://github.com/c-ares/c-ares/pull/732#issuecomment-2028454381:
 * "Those deprecated functions will remain available until there is an ABI
 *  break, which honestly will likely never happen. It's more to encourage
 *  integrators to move to the more modern functions."
 * Also, keep in mind that several of these 'deprecations' happened in early 2024
 * while the new function was introduced in March 2020, like for ares_getaddrinfo().
 * That isn't all that long ago, only 4 years. So we would need compatibility code
 * for both the old and new function for a while.
 * So: we can look into that in some major new UnrealIRCd version, nothing urgent,
 * and perhaps by then it is long enough that we don't need the fallback to older
 * functions.
 */
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
#endif

/* Maximum length of a single chunked-transfer chunk header
 * (the line announcing a chunk, eg. "7f\r\n", optionally with
 * chunk extensions like "7f;name=value\r\n"). This is NOT the
 * size of the chunk data itself.
 * Caps how much we will buffer while waiting for the terminating
 * LF so a server that streams a chunk header with no LF cannot
 * grow handle->lefttoparse without bound.
 */
#define HTTPS_MAX_CHUNK_HEADER_LEN	256

/* Structs */

/* Stores information about the async transfer.
 * Used to maintain information about the transfer
 * to trigger the callback upon completion.
 */
typedef struct Download Download;

struct Download
{
	Download *prev, *next;
	OutgoingWebRequest *request;
	FILE *file_fd;		/**< File open for writing (otherwise NULL) */
	char *filename;
	char *memory_data; /**< Memory for writing response (otherwise NULL) */
	long long memory_data_len; /**< Size of memory_data */
	long long memory_data_allocated; /**< Total allocated memory for 'memory_data' */
	long long bytes_written_to_file; /**< Bytes written to file_fd so far (for max_size cap) */
	char errorbuf[512];
	char *hostname;		/**< Parsed hostname (from 'url') */
	int port;		/**< Parsed port (from 'url') */
	char *username;
	char *password;
	char *document;		/**< Parsed document (from 'url') */
	char *ip4;		/**< Resolved IP (IPv4) */
	char *ip6;		/**< Resolved IP (IPv6) */
	SocketType socket_type;	/**< Socket type that we are trying (SOCKET_TYPE_IPV4 or SOCKET_TYPE_IPV6) */
	SSL *ssl;
	int fd;			/**< Socket */
	int connected;
	int got_response;
	int http_status_code;
	char *lefttoparse;
	long long lefttoparselen; /* size of data in lefttoparse (note: not used for first header parsing) */
	time_t last_modified;
	time_t download_started;
	int dns_refcnt;
	TransferEncoding transfer_encoding;
	long long chunk_remaining;
	char *redirect_new_location;
};

/* Variables */
Download *downloads = NULL;
SSL_CTX *https_ctx = NULL;

/* Forward declarations */
void url_resolve_cb(void *arg, int status, int timeouts, struct hostent *he);
void unreal_https_initiate_connect(Download *handle);
int url_parse(const char *url, char **host, int *port, char **username, char **password, char **document);
void unreal_https_connect_handshake(int fd, int revents, void *data);
int https_connect(Download *handle);
int https_fatal_tls_error(int ssl_error, int my_errno, Download *handle);
int https_connect_send_header(Download *handle);
void https_receive_response(int fd, int revents, void *data);
int https_handle_response_header(Download *handle, char *readbuf, int n);
int https_handle_response_body(Download *handle, char *readbuf, int n);
void https_done(Download *handle);
void https_done_cached(Download *handle);
void https_redirect(Download *handle);
int https_parse_header(char *buffer, int len, char **key, char **value, char **lastloc, int *end_of_request);
char *url_find_end_of_request(char *header, int totalsize, int *remaining_bytes);
int https_cancel(Download *handle, FORMAT_STRING(const char *pattern), ...)  __attribute__((format(printf,2,3)));

void url_free_handle(Download *handle)
{
	DelListItem(handle, downloads);
	if (handle->fd > 0)
	{
		fd_close(handle->fd);
		fd_unnotify(handle->fd);
	}
	if (handle->file_fd)
		fclose(handle->file_fd);
	if (handle->filename && !handle->request->keep_file)
		remove(handle->filename);
	safe_free(handle->filename);
	safe_free(handle->memory_data);
	safe_free(handle->hostname);
	safe_free(handle->username);
	safe_free(handle->password);
	safe_free(handle->document);
	safe_free(handle->ip4);
	safe_free(handle->ip6);
	if (handle->ssl)
		SSL_free(handle->ssl);
	safe_free(handle->lefttoparse);
	safe_free(handle->redirect_new_location);
	safe_free_outgoingwebrequest(handle->request);
	safe_free(handle);
}

void url_cancel_handle_by_callback_data(void *ptr)
{
	Download *d, *d_next;

	for (d = downloads; d; d = d_next)
	{
		d_next = d->next;
		if (d->request->callback_data == ptr)
		{
			d->request->callback = NULL;
			d->request->callback_data = NULL;
		}
	}
}

/** Cancel and free the HTTPS request->
 * @returns Always returns -1
 */
int https_cancel(Download *handle, FORMAT_STRING(const char *pattern), ...)
{
	va_list vl;
	va_start(vl, pattern);
	vsnprintf(handle->errorbuf, sizeof(handle->errorbuf), pattern, vl);
	va_end(vl);
	url_callback(handle->request, NULL, NULL, 0, handle->errorbuf, 0, handle->request->callback_data);
	url_free_handle(handle);
	return -1;
}

void url_start_async(OutgoingWebRequest *request)
{
	char *file;
	const char *filename;
	char *tmp;
	Download *handle = NULL;
	int ipv6 = 0;
	const char *actual_url = request->actual_url ? request->actual_url : request->url;
	char *host;
	int port;
	char *username;
	char *password;
	char *document;

	/* Check for the bare minimum */
	if (!request->url || !request->http_method)
		abort();

	/* Set request defaults */
	if (request->connect_timeout == 0)
		request->connect_timeout = DOWNLOAD_CONNECT_TIMEOUT;
	if (request->transfer_timeout == 0)
		request->transfer_timeout = DOWNLOAD_TRANSFER_TIMEOUT;
	if (request->max_size <= 0)
		request->max_size = request->store_in_file ? DOWNLOAD_MAX_SIZE_FILE_BACKED : DOWNLOAD_MAX_SIZE_MEMORY_BACKED;

	handle = safe_alloc(sizeof(Download));
	handle->download_started = TStime();
	handle->request = request;
	AddListItem(handle, downloads);

	if (strncmp(actual_url, "https://", 8))
	{
		https_cancel(handle, "Only https:// is supported (either rebuild UnrealIRCd with curl support or use https)");
		return;
	}
	if (!url_parse(actual_url, &host, &port, &username, &password, &document))
	{
		https_cancel(handle, "Failed to parse HTTP url");
		return;
	}

	safe_strdup(handle->hostname, host);
	handle->port = port;
	safe_strdup(handle->username, username);
	safe_strdup(handle->password, password);
	safe_strdup(handle->document, document);

	if (request->store_in_file)
	{
		file = url_getfilename(handle->request->url);
		filename = unreal_getfilename(file);
		tmp = unreal_mktemp(TMPDIR, filename ? filename : "download.conf");

		handle->file_fd = fopen(tmp, "wb");
		if (!handle->file_fd)
		{
			https_cancel(handle, "Cannot create '%s': %s", tmp, strerror(ERRNO));
			safe_free(file);
			return;
		}

		safe_strdup(handle->filename, tmp);
		safe_free(file);
	} else {
		handle->memory_data_allocated = URL_MEMORY_BACKED_CHUNK_SIZE;
		handle->memory_data = safe_alloc(URL_MEMORY_BACKED_CHUNK_SIZE);
	}

	if (is_valid_ip(handle->hostname))
	{
		/* Nothing to resolve, eg https://127.0.0.1/ */
		if (strchr(handle->hostname, ':'))
			safe_strdup(handle->ip6, handle->hostname);
		else
			safe_strdup(handle->ip4, handle->hostname);
		unreal_https_initiate_connect(handle);
	} else {
		/* Hostname, so start resolving... */
		handle->dns_refcnt += 2;
		ares_gethostbyname(resolver_channel_https, handle->hostname, AF_INET, url_resolve_cb, handle);
		ares_gethostbyname(resolver_channel_https, handle->hostname, AF_INET6, url_resolve_cb, handle);
	}
}

void url_resolve_cb(void *arg, int status, int timeouts, struct hostent *he)
{
	Download *handle = (Download *)arg;
	int n;
	struct hostent *he2;
	char ipbuf[HOSTLEN+1];
	const char *ip = NULL;

	handle->dns_refcnt--;

	if ((status != 0) || !he->h_addr_list || !he->h_addr_list[0])
	{
		if ((handle->dns_refcnt == 0) && !handle->ip4 && !handle->ip6)
		{
			https_cancel(handle, "Unable to resolve hostname '%s'", handle->hostname);
			return;
		}
	} else
	{
		if (he->h_addr_list[0])
		{
			if (he->h_length == 16)
				ip = inetntop(AF_INET6, he->h_addr_list[0], ipbuf, sizeof(ipbuf));
			else if (he->h_length == 4)
				ip = inetntop(AF_INET, he->h_addr_list[0], ipbuf, sizeof(ipbuf));
			// else fallthrough, ip stays NULL
		}

		if (ip)
		{
			if (he->h_length == 16)
				safe_strdup(handle->ip6, ip);
			else
				safe_strdup(handle->ip4, ip);
		} else
		if ((handle->dns_refcnt == 0) && !handle->ip4 && !handle->ip6)
		{
			https_cancel(handle, "Unable to resolve hostname '%s'", handle->hostname);
			return;
		}
	}

	/* Still an outstanding request? Then we will handle things later. */
	if (handle->dns_refcnt > 0)
		return;

	/* Ok we are done resolving IPv4 and IPv6. Handle the easy "all failed" case. */
	if (!handle->ip4 && !handle->ip6)
	{
		https_cancel(handle, "Unable to resolve hostname '%s'", handle->hostname);
		return;
	}

	if (handle->ip4)
		handle->socket_type = SOCKET_TYPE_IPV4;
	else
		handle->socket_type = SOCKET_TYPE_IPV6;
	unreal_https_initiate_connect(handle);
}

void unreal_https_initiate_connect(Download *handle)
{
	handle->fd = fd_socket(handle->socket_type == SOCKET_TYPE_IPV6 ? AF_INET6 : AF_INET, SOCK_STREAM, 0, "HTTPS");
	if (handle->fd < 0)
	{
		/* IPv4 gave an early error (eg no IPv4 supported). Can we retry over IPv6? */
		if ((handle->socket_type == SOCKET_TYPE_IPV4) && handle->ip6 && !DISABLE_IPV6)
		{
			handle->socket_type = SOCKET_TYPE_IPV6;
			unreal_https_initiate_connect(handle);
			return;
		}
		https_cancel(handle, "Could not create socket: %s", strerror(ERRNO));
		return;
	}
	set_sock_opts(handle->fd, NULL, handle->socket_type);
	if (!unreal_connect(handle->fd,
	                    (handle->socket_type == SOCKET_TYPE_IPV4) ? handle->ip4 : handle->ip6,
	                    handle->port,
	                    handle->socket_type))
	{
		/* IPv4 gave an early error (eg no IPv4 connectivity). Can we retry over IPv6? */
		if ((handle->socket_type == SOCKET_TYPE_IPV4) && handle->ip6 && !DISABLE_IPV6)
		{
			fd_close(handle->fd);
			handle->socket_type = SOCKET_TYPE_IPV6;
			unreal_https_initiate_connect(handle);
			return;
		}
		https_cancel(handle, "Could not connect: %s", strerror(ERRNO));
		return;
	}

	fd_setselect(handle->fd, FD_SELECT_WRITE, unreal_https_connect_handshake, handle);
}

// based on unreal_tls_client_handshake()
void unreal_https_connect_handshake(int fd, int revents, void *data)
{
	Download *handle = data;
	int sockerr;
	int len = sizeof(sockerr);

	/* Let's first see if the TCP/IP connect succeeded... */
	if (!getsockopt(fd, SOL_SOCKET, SO_ERROR, (void *)&sockerr, &len) && sockerr)
	{
		/* We tried IPv4, can we retry over IPv6? Then we don't report an error and try IPv6. */
		if ((handle->socket_type == SOCKET_TYPE_IPV4) && handle->ip6 && !DISABLE_IPV6)
		{
			fd_close(handle->fd);
			fd_unnotify(handle->fd);
			handle->socket_type = SOCKET_TYPE_IPV6;
			unreal_https_initiate_connect(handle);
			return;
		} else {
			/* Fatal error */
			https_cancel(handle, "Connect failed: %s", STRERROR(sockerr));
			return;
		}
	}

	handle->ssl = SSL_new(https_ctx);
	if (!handle->ssl)
	{
		https_cancel(handle, "Failed to setup SSL");
		return;
	}
#ifdef HAS_SSL_CTX_SET_MIN_PROTO_VERSION
	if (handle->request->minimum_tls_version)
		SSL_set_min_proto_version(handle->ssl, handle->request->minimum_tls_version);
#endif
	SSL_set_fd(handle->ssl, handle->fd);
	SSL_set_connect_state(handle->ssl);
	SSL_set_nonblocking(handle->ssl);
	SSL_set_tlsext_host_name(handle->ssl, handle->hostname);

	if (https_connect(handle) < 0)
		return; /* fatal error, handle is freed */

	/* Is now connecting... */
}

// Based on unreal_tls_connect_retry
void https_connect_retry(int fd, int revents, void *data)
{
	Download *handle = data;
	https_connect(handle);
}

/* Actually do the SSL_connect()
 * Based on unreal_tls_connect() but different return values.
 * @retval 1 connected
 * @retval 0 in progress
 * @retval -1 error, handle freed!
 */
int https_connect(Download *handle)
{
	int ssl_err;
	char *errstr;

	if ((ssl_err = SSL_connect(handle->ssl)) <= 0)
	{
		ssl_err = SSL_get_error(handle->ssl, ssl_err);
		switch(ssl_err)
		{
			case SSL_ERROR_SYSCALL:
				if (ERRNO == P_EINTR || ERRNO == P_EWOULDBLOCK || ERRNO == P_EAGAIN)
				{
					/* Hmmm. This implementation is different than in unreal_tls_accept().
					 * One of them must be wrong -- better check! (TODO)
					 */
					fd_setselect(handle->fd, FD_SELECT_READ|FD_SELECT_WRITE, https_connect_retry, handle);
					return 0;
				}
				return https_fatal_tls_error(ssl_err, ERRNO, handle);
			case SSL_ERROR_WANT_READ:
				fd_setselect(handle->fd, FD_SELECT_READ, https_connect_retry, handle);
				fd_setselect(handle->fd, FD_SELECT_WRITE, NULL, handle);
				return 0;
			case SSL_ERROR_WANT_WRITE:
				fd_setselect(handle->fd, FD_SELECT_READ, NULL, handle);
				fd_setselect(handle->fd, FD_SELECT_WRITE, https_connect_retry, handle);
				return 0;
			default:
				return https_fatal_tls_error(ssl_err, ERRNO, handle); /* -1 */
		}
		/* NOTREACHED */
		return 0;
	}

	/* We are connected now. */

	if (!verify_certificate(handle->ssl, handle->hostname, &errstr))
		return https_cancel(handle, "TLS Certificate error for server: %s", errstr); /* -1 */

	return https_connect_send_header(handle);
}

/**
 * Report a fatal TLS error and terminate the download.
 *
 * @param ssl_error The error as from OpenSSL.
 * @param where The location, one of the SAFE_SSL_* defines.
 * @param my_errno A preserved value of errno to pass to ssl_error_str().
 * @param client The client the error is associated with.
 * @returns Always -1
 */
int https_fatal_tls_error(int ssl_error, int my_errno, Download *handle)
{
	const char *ssl_errstr;
	unsigned long additional_errno = ERR_get_error();
	char additional_info[256];
	const char *one, *two;

#if OPENSSL_VERSION_NUMBER >= 0x30000000L
	/* Fetch additional error information from OpenSSL 3.0.0+ */
	two = ERR_reason_error_string(additional_errno);
	if (two && *two)
	{
		snprintf(additional_info, sizeof(additional_info), ": %s", two);
	} else {
		*additional_info = '\0';
	}
#else
	/* Fetch additional error information from OpenSSL. This is new as of Nov 2017 (4.0.16+) */
	one = ERR_func_error_string(additional_errno);
	two = ERR_reason_error_string(additional_errno);
	if (one && *one && two && *two)
	{
		snprintf(additional_info, sizeof(additional_info), ": %s: %s", one, two);
	} else {
		*additional_info = '\0';
	}
#endif

	ssl_errstr = ssl_error_str(ssl_error, my_errno);

	https_cancel(handle, "%s [%s]", ssl_errstr, additional_info);
	return -1;
}

// copied 100% from modulemanager parse_url()
int url_parse(const char *url, char **hostname, int *port, char **username, char **password, char **document)
{
	char *p, *p2;
	const char *q;
	static char hostbuf[256];
	static char documentbuf[512];

	*hostname = *username = *password = *document = NULL;
	*port = 443;

	if (strncmp(url, "https://", 8))
		return 0;

	/* Refuse control chars and space (would allow request-line injection). */
	for (q = url; *q; q++)
		if (*q <= ' ')
			return 0;

	url += 8; /* skip over https:// part */

	p = strchr(url, '/');
	if (!p)
		return 0;

	strlncpy(hostbuf, url, sizeof(hostbuf), p - url);

	strlcpy(documentbuf, p, sizeof(documentbuf));

	*hostname = hostbuf;
	*document = documentbuf;

	/* Actually we may still need to extract the port */
	p = strchr(hostbuf, '@');
	if (p)
	{
		*p++ = '\0';

		*username = hostbuf;
		p2 = strchr(hostbuf, ':');
		if (p2)
		{
			*p2++ = '\0';
			*password = p2;
		}
		*hostname = p;
	}
	p = strchr(*hostname, ':');
	if (p)
	{
		*p++ = '\0';
		*port = atoi(p);
	}

	return 1;
}

int https_connect_send_header(Download *handle)
{
	char buf[8192];
	char hostandport[512];
	int ssl_err;
	char *host;
	int port;
	char *document;

	handle->connected = 1;
	snprintf(hostandport, sizeof(hostandport), "%s:%d", handle->hostname, handle->port);

	/* Prepare the header */
	if (handle->request->http_method == HTTP_METHOD_GET)
	{
		snprintf(buf, sizeof(buf), "GET %s HTTP/1.1\r\n"
				    "User-Agent: UnrealIRCd %s\r\n"
				    "Host: %s\r\n"
				    "Connection: close\r\n",
				    handle->document,
				    VERSIONONLY,
				    hostandport);
	} else
	if (handle->request->http_method == HTTP_METHOD_POST)
	{
		if (!handle->request->body || !strlen(handle->request->body))
		{
			snprintf(buf, sizeof(buf), "POST %s HTTP/1.1\r\n"
					    "User-Agent: UnrealIRCd %s\r\n"
					    "Host: %s\r\n"
					    "Connection: close\r\n",
					    handle->document,
					    VERSIONONLY,
					    hostandport);
		} else {
			char add_default_content_type = 0;
			if (!find_nvplist(handle->request->headers, "Content-Type"))
				add_default_content_type = 1;

			snprintf(buf, sizeof(buf), "POST %s HTTP/1.1\r\n"
					    "User-Agent: UnrealIRCd %s\r\n"
					    "Host: %s\r\n"
					    "%s"
					    "Content-Length: %ld\r\n"
					    "Connection: close\r\n",
					    handle->document,
					    VERSIONONLY,
					    hostandport,
					    add_default_content_type ? "Content-Type: application/x-www-form-urlencoded\r\n" : "",
					    (long)strlen(handle->request->body));
		}
	} else
		abort();
	if (handle->username && handle->password)
	{
		char wbuf[128];
		char obuf[256];
		char header[512];

		snprintf(wbuf, sizeof(wbuf), "%s:%s", handle->username, handle->password);
		if (b64_encode(wbuf, strlen(wbuf), obuf, sizeof(obuf)-1) > 0)
		{
			snprintf(header, sizeof(header), "Authorization: Basic %s\r\n", obuf);
			strlcat(buf, header, sizeof(buf));
		}
	}
	if (handle->request->cachetime > 0)
	{
		const char *datestr = rfc2616_time(handle->request->cachetime);
		if (datestr)
		{
			// snprintf_append...
			snprintf(buf+strlen(buf), sizeof(buf)-strlen(buf),
				 "If-Modified-Since: %s\r\n", datestr);
		}
	}
	if (handle->request->headers)
	{
		NameValuePrioList *n;
		char nbuf[256];

		for (n = handle->request->headers; n; n = n->next)
		{
			if (n->value)
				snprintf(nbuf, sizeof(nbuf), "%s: %s\r\n", n->name, n->value);
			else
				snprintf(nbuf, sizeof(nbuf), "%s:\r\n", n->name);
			if (strlen(buf)+strlen(nbuf) > sizeof(buf)-8)
				break;
			strlcat(buf, nbuf, sizeof(buf));
		}
	}
	strlcat(buf, "\r\n", sizeof(buf));
	if (handle->request->body)
		strlcat(buf, handle->request->body, sizeof(buf));

	ssl_err = SSL_write(handle->ssl, buf, strlen(buf));
	if (ssl_err < 0)
		return https_fatal_tls_error(ssl_err, ERRNO, handle);
	fd_setselect(handle->fd, FD_SELECT_WRITE, NULL, handle);
	fd_setselect(handle->fd, FD_SELECT_READ, https_receive_response, handle);
	return 1;
}

void https_receive_response(int fd, int revents, void *data)
{
	Download *handle = data;
	int n;
	char readbuf[2048];

	n = SSL_read(handle->ssl, readbuf, sizeof(readbuf)-1);
	if (n == 0)
	{
		/* Graceful close */
		https_done(handle);
		return;
	}
	if (n < 0)
	{
		int ssl_err = SSL_get_error(handle->ssl, n);
		switch (ssl_err)
		{
			case SSL_ERROR_WANT_WRITE:
				fd_setselect(fd, FD_SELECT_READ, NULL, handle);
				fd_setselect(fd, FD_SELECT_WRITE, https_receive_response, handle);
				return;
			case SSL_ERROR_WANT_READ:
				/* Wants to read more data; let it call us next time again */
				return;
			case SSL_ERROR_SYSCALL:
			case SSL_ERROR_SSL:
			default:
				https_fatal_tls_error(ssl_err, ERRNO, handle);
				return;
		}
		return;
	}
	readbuf[n] = '\0';

	//fprintf(stderr, "Got: '%s'\n", readbuf);

	if (!handle->got_response)
	{
		https_handle_response_header(handle, readbuf, n);
		return;
	} else
	if (handle->got_response)
	{
		if (!https_handle_response_body(handle, readbuf, n))
			return; /* handle is already freed! */
	}
}

// Based on websocket_handle_handshake()
int https_handle_response_header(Download *handle, char *readbuf, int n)
{
	char *key, *value;
	int r, end_of_request;
	char netbuf[4096], netbuf2[4096];
	char *lastloc = NULL;
	int maxcopy, nprefix=0;
	int totalsize;

	/* Yeah, totally paranoid: */
	memset(netbuf, 0, sizeof(netbuf));
	memset(netbuf2, 0, sizeof(netbuf2));

	/** Frame re-assembling starts here **/
	*netbuf = '\0';
	if (handle->lefttoparse)
	{
		strlcpy(netbuf, handle->lefttoparse, sizeof(netbuf));
		nprefix = strlen(netbuf);
	}
	maxcopy = sizeof(netbuf) - nprefix - 1;
	/* (Need to some manual checking here as strlen() can't be safely used
	 *  on readbuf. Same is true for strlncat since it uses strlen().)
	 */
	if (n > maxcopy)
		n = maxcopy;
	if (n <= 0)
	{
		https_cancel(handle, "Oversized line in HTTP response");
		return 0;
	}
	memcpy(netbuf+nprefix, readbuf, n); /* SAFE: see checking above */
	totalsize = n + nprefix;
	netbuf[totalsize] = '\0';
	memcpy(netbuf2, netbuf, totalsize+1); // copy, including the "always present \0 at the end just in case we use strstr etc".
	safe_free(handle->lefttoparse);

	/** Now step through the lines.. **/
	for (r = https_parse_header(netbuf, strlen(netbuf), &key, &value, &lastloc, &end_of_request);
	     r;
	     r = https_parse_header(NULL, 0, &key, &value, &lastloc, &end_of_request))
	{
		// do something actually with the header here ;)
		if (!strcasecmp(key, "RESPONSE"))
		{
			handle->http_status_code = atoi(value);
			if (handle->http_status_code == 304)
			{
				/* 304 Not Modified: cache hit */
				https_done_cached(handle);
				return 0;
			}
			else if ((handle->http_status_code >= 301) && (handle->http_status_code <= 308))
			{
				/* Redirect */
				if (handle->request->max_redirects == 0)
				{
					https_cancel(handle, "Too many HTTP redirects (%d)", DOWNLOAD_MAX_REDIRECTS);
					return 0;
				}
				/* Let it continue.. we handle it later, as we need to
				 * receive the "Location" header as well.
				 */
			}
			else if (handle->http_status_code != 200)
			{
				/* HTTP Failure code */
				https_cancel(handle, "HTTP Error: %s", value);
				return 0;
			}
		} else
		if (!strcasecmp(key, "Last-Modified") && value)
		{
			handle->last_modified = rfc2616_time_to_unix_time(value);
		} else
		if (!strcasecmp(key, "Location") && value)
		{
			safe_strdup(handle->redirect_new_location, value);
		} else
		if (!strcasecmp(key, "Transfer-Encoding") && value)
		{
			if (value && !strcasecmp(value, "chunked"))
				handle->transfer_encoding = TRANSFER_ENCODING_CHUNKED;
		}
		//fprintf(stderr, "\nHEADER '%s'\n\n", key);
	}

	if (end_of_request)
	{
		int remaining_bytes = 0;
		char *nextframe;

		safe_free(handle->lefttoparse);
		handle->got_response = 1;

		if (handle->http_status_code == 0)
		{
			https_cancel(handle, "Invalid HTTP response");
			return 0;
		}
		if (handle->http_status_code != 200)
		{
			if (handle->redirect_new_location)
			{
				https_redirect(handle);
				return 0; /* this old request dies */
			} else {
				https_cancel(handle, "HTTP Redirect encountered but no URL specified!?");
				return 0;
			}
		}

		nextframe = url_find_end_of_request(netbuf2, totalsize, &remaining_bytes);
		if (nextframe)
		{
			if (!https_handle_response_body(handle, nextframe, remaining_bytes))
				return 0;
		}
	}

	if (!end_of_request && lastloc)
	{
		/* Save a cut-off (incomplete) header line for the next packet.
		 * Only while still reading headers: once end_of_request is set,
		 * lastloc points at body data that https_handle_response_body()
		 * already owns via handle->lefttoparse/lefttoparselen, so touching
		 * it here would desync those two and cause an out-of-bounds read.
		 */
		safe_strdup(handle->lefttoparse, lastloc);
	}

	return 1;
}

long long https_handle_response_body_memory(Download *handle, const char *ptr, long long write_sz)
{
	// DUPLICATE CODE: same as src/url_curl.c, well... sortof
	long long size_required = handle->memory_data_len + write_sz;

	if (handle->memory_data == NULL)
		return 0; /* Normally does not happen as it is preallocated, but could happen upon unwinding cancels.. */

	if (size_required >= handle->memory_data_allocated - 1) // the -1 is for zero termination, even though it is binary..
	{
		long long newsize = ((size_required / URL_MEMORY_BACKED_CHUNK_SIZE)+1)*URL_MEMORY_BACKED_CHUNK_SIZE;
		char *newptr = realloc(handle->memory_data, newsize);
		if (!newptr)
		{
			unreal_log(ULOG_ERROR, "url", "URL_DOWNLOAD_MEMORY", NULL, "Async URL callback failed when reading returned data: out of memory?");
			safe_free(handle->memory_data);
			handle->memory_data_len = 0;
			handle->memory_data_allocated = 0;
			return 0;
		}
		handle->memory_data = newptr;
		handle->memory_data_allocated = newsize;
		/* fill rest with zeroes, yeah.. no trust! ;D */
		memset(handle->memory_data + handle->memory_data_len, 0, handle->memory_data_allocated - handle->memory_data_len);
	}

	memcpy(handle->memory_data + handle->memory_data_len, ptr, write_sz);
	handle->memory_data_len += write_sz;
	handle->memory_data[handle->memory_data_len] = '\0';
	return write_sz;
}

int https_handle_response_body(Download *handle, char *readbuf, int pktsize)
{
	char *buf;
	long long n;
	char *free_this_buffer = NULL;

	// TODO we fail to check for write errors ;)
	// TODO: Makes sense to track if we got everything? :D

	if (handle->transfer_encoding == TRANSFER_ENCODING_NONE)
	{
		/* Ohh.. so easy! */
		if (handle->request->store_in_file == 0)
		{
			if (handle->memory_data_len + pktsize > handle->request->max_size)
			{
				https_cancel(handle, "Response too large (maximum: %lld bytes)", handle->request->max_size);
				return 0; /* handle freed */
			}
			https_handle_response_body_memory(handle, readbuf, pktsize);
		}
		else if (handle->file_fd)
		{
			if (handle->bytes_written_to_file + pktsize > handle->request->max_size)
			{
				https_cancel(handle, "Response too large (maximum: %lld bytes)", handle->request->max_size);
				return 0; /* handle freed */
			}
			fwrite(readbuf, 1, pktsize, handle->file_fd);
			handle->bytes_written_to_file += pktsize;
		}
		return 1;
	}

	/* Fill 'buf' nd set 'buflen' with what we had + what we have now.
	 * Makes things easy.
	 */
	if (handle->lefttoparse)
	{
		n = handle->lefttoparselen + pktsize;
		free_this_buffer = buf = safe_alloc(n);
		memcpy(buf, handle->lefttoparse, handle->lefttoparselen);
		memcpy(buf+handle->lefttoparselen, readbuf, pktsize);
		safe_free(handle->lefttoparse);
		handle->lefttoparselen = 0;
	} else {
		n = pktsize;
		buf = readbuf;
	}

	/* Chunked transfers.. yayyyy.. */
	while (n > 0)
	{
		if (handle->chunk_remaining > 0)
		{
			/* Eat it */
			long long eat = MIN(handle->chunk_remaining, n);
			if (handle->request->store_in_file == 0)
			{
				if (handle->memory_data_len + eat > handle->request->max_size)
				{
					https_cancel(handle, "Response too large (maximum: %lld bytes)", handle->request->max_size);
					safe_free(free_this_buffer);
					return 0; /* handle freed */
				}
				https_handle_response_body_memory(handle, buf, eat);
			}
			else if (handle->file_fd)
			{
				if (handle->bytes_written_to_file + eat > handle->request->max_size)
				{
					https_cancel(handle, "Response too large (maximum: %lld bytes)", handle->request->max_size);
					safe_free(free_this_buffer);
					return 0; /* handle freed */
				}
				fwrite(buf, 1, eat, handle->file_fd);
				handle->bytes_written_to_file += eat;
			}
			n -= eat;
			buf += eat;
			handle->chunk_remaining -= eat;
		} else
		{
			int gotlf = 0;
			long long i;

			/* First check if it is a (trailing) empty line,
			 * eg from a previous chunk. Skip over.
			 */
			if ((n >= 2) && !strncmp(buf, "\r\n", 2))
			{
				buf += 2;
				n -= 2;
			} else
			if ((n >= 1) && !strncmp(buf, "\n", 1))
			{
				buf++;
				n--;
			}

			/* Now we are (possibly) at the chunk size line,
			 * this is or example '7f' + newline.
			 * So first, check if we have a newline at all.
			 */
			for (i=0; i < n; i++)
			{
				if (buf[i] == '\n')
				{
					gotlf = 1;
					break;
				}
			}
			if (!gotlf)
			{
				/* The line telling us the chunk size is incomplete,
				 * as it does not contain an \n. Wait for more data
				 * from the network socket.
				 */
				if (n > HTTPS_MAX_CHUNK_HEADER_LEN)
				{
					/* A chunk-size line should never be this long;
					 * refuse to keep buffering otherwise a malicious
					 * server can grow lefttoparse without bound.
					 */
					https_cancel(handle, "Chunk size line too long (%lld bytes, no LF)", n);
					safe_free(free_this_buffer);
					return 0;
				}
				if (n > 0)
				{
					/* Store what we have first.. */
					handle->lefttoparselen = n;
					handle->lefttoparse = safe_alloc(n);
					memcpy(handle->lefttoparse, buf, n);
				}
				safe_free(free_this_buffer);
				return 1; /* WE WANT MORE! */
			}
			buf[i] = '\0'; /* cut at LF */
			i++; /* point to next data */
			errno = 0;
			handle->chunk_remaining = strtoll(buf, NULL, 16);
			if (errno == ERANGE)
			{
				https_cancel(handle, "Chunk size out of range: '%s'", buf);
				safe_free(free_this_buffer);
				return 0;
			}
			if (handle->chunk_remaining < 0)
			{
				https_cancel(handle, "Negative chunk encountered (%lld)", handle->chunk_remaining);
				safe_free(free_this_buffer);
				return 0;
			}
			if (handle->chunk_remaining == 0)
			{
				https_done(handle);
				safe_free(free_this_buffer);
				return 0;
			}
			buf += i;
			n -= i;
		}
	}

	safe_free(free_this_buffer);
	return 1;
}

void https_done(Download *handle)
{
	if (handle->file_fd)
	{
		fclose(handle->file_fd);
		handle->file_fd = NULL;
	}

	if (!handle->got_response)
		url_callback(handle->request, NULL, NULL, 0, "HTTPS response not received", 0, handle->request->callback_data);
	else
	{
		if ((handle->last_modified > 0) && handle->filename)
			unreal_setfilemodtime(handle->filename, handle->last_modified);
		url_callback(handle->request, handle->filename, handle->memory_data, handle->memory_data_len, NULL, 0, handle->request->callback_data);
	}
	url_free_handle(handle);
	return;
}

void https_done_cached(Download *handle)
{
	if (handle->file_fd)
	{
		fclose(handle->file_fd);
		handle->file_fd = NULL;
	}
	url_callback(handle->request, NULL, NULL, 0, NULL, 1, handle->request->callback_data);
	url_free_handle(handle);
}

void https_redirect(Download *handle)
{
	OutgoingWebRequest *r;

	if (handle->request->max_redirects == 0)
	{
		https_cancel(handle, "Too many HTTP redirects (%d)", DOWNLOAD_MAX_REDIRECTS);
		return;
	}

	r = duplicate_outgoingwebrequest(handle->request);

	/* Override actual url */
	if (handle->redirect_new_location && (*handle->redirect_new_location == '/'))
	{
		/* Convert relative URL to absolute URL */
		size_t sz = 20 + strlen(handle->hostname) + strlen(handle->redirect_new_location);
		safe_free(r->actual_url);
		r->actual_url = safe_alloc(sz);
		snprintf(r->actual_url, sz, "https://%s:%d%s", handle->hostname, handle->port, handle->redirect_new_location);
	} else {
		safe_strdup(r->actual_url, handle->redirect_new_location);
	}

	r->max_redirects--; // safe, checked to be >0 a few lines up
	url_free_handle(handle); // free old handle
	url_start_async(r); // create new one
}

/** Helper function to parse the HTTP header consisting of multiple 'Key: value' pairs */
int https_parse_header(char *buffer, int len, char **key, char **value, char **lastloc, int *end_of_request)
{
	static char buf[4096], *nextptr;
	char *p;
	char *k = NULL, *v = NULL;
	int foundlf = 0;

	if (buffer)
	{
		/* Initialize */
		if (len > sizeof(buf) - 1)
			len = sizeof(buf) - 1;

		memcpy(buf, buffer, len);
		buf[len] = '\0';
		nextptr = buf;
	}

	*end_of_request = 0;

	p = nextptr;

	if (!p)
	{
		*key = *value = NULL;
		return 0; /* done processing data */
	}

	if (!strncmp(p, "\n", 1) || !strncmp(p, "\r\n", 2))
	{
		*key = *value = NULL;
		*end_of_request = 1;
		// new compared to websocket handling:
		if (*p == '\n')
			*lastloc = p + 1;
		else
			*lastloc = p + 2;
		return 0;
	}

	/* Note: p *could* point to the NUL byte ('\0') */

	/* Special handling for response line itself. */
	if (!strncmp(p, "HTTP/1", 6) && (strlen(p)>=13))
	{
		k = "RESPONSE";
		p += 9;
		v = p; /* SET VALUE */
		nextptr = NULL; /* set to "we are done" in case next for loop fails */
		for (; *p; p++)
		{
			if (*p == '\r')
			{
				*p = '\0'; /* eat silently, but don't consider EOL */
			}
			else if (*p == '\n')
			{
				*p = '\0';
				nextptr = p+1; /* safe, there is data or at least a \0 there */
				break;
			}
		}
		*key = k;
		*value = v;
		return 1;
	}

	/* Header parsing starts here.
	 * Example line "Host: www.unrealircd.org"
	 */
	k = p; /* SET KEY */

	/* First check if the line contains a terminating \n. If not, don't process it
	 * as it may have been a cut header.
	 */
	for (; *p; p++)
	{
		if (*p == '\n')
		{
			foundlf = 1;
			break;
		}
	}

	if (!foundlf)
	{
		*key = *value = NULL;
		*lastloc = k;
		return 0;
	}

	p = k;

	for (; *p; p++)
	{
		if ((*p == '\n') || (*p == '\r'))
		{
			/* Reached EOL but 'value' not found */
			*p = '\0';
			break;
		}
		if (*p == ':')
		{
			*p++ = '\0';
			if (*p++ != ' ')
				break; /* missing mandatory space after ':' */

			v = p; /* SET VALUE */
			nextptr = NULL; /* set to "we are done" in case next for loop fails */
			for (; *p; p++)
			{
				if (*p == '\r')
				{
					*p = '\0'; /* eat silently, but don't consider EOL */
				}
				else if (*p == '\n')
				{
					*p = '\0';
					nextptr = p+1; /* safe, there is data or at least a \0 there */
					break;
				}
			}
			/* A key-value pair was succesfully parsed, return it */
			*key = k;
			*value = v;
			return 1;
		}
	}

	/* Fatal parse error */
	*key = *value = NULL;
	return 0;
}

/** Check if there is any data at the end of the request */
char *url_find_end_of_request(char *header, int totalsize, int *remaining_bytes)
{
	char *nextframe1;
	char *nextframe2;
	char *nextframe = NULL;

	// find first occurance, yeah this is just stupid, but it works.
	nextframe1 = strstr(header, "\r\n\r\n"); // = +4
	nextframe2 = strstr(header, "\n\n");     // = +2
	if (nextframe1 && nextframe2)
	{
		if (nextframe1 < nextframe2)
		{
			nextframe = nextframe1 + 4;
		} else {
			nextframe = nextframe2 + 2;
		}
	} else
	if (nextframe1)
	{
		nextframe = nextframe1 + 4;
	} else
	if (nextframe2)
	{
		nextframe = nextframe2 + 2;
	}
	if (nextframe)
	{
		*remaining_bytes = totalsize - (nextframe - header);
		if (*remaining_bytes > 0)
			return nextframe;
	}
	return NULL;
}

/* Handle timeouts. */
EVENT(url_socket_timeout)
{
	Download *d, *d_next;
	for (d = downloads; d; d = d_next)
	{
		d_next = d->next;
		if (d->dns_refcnt)
			continue; /* can't touch this... */
		if (!d->connected && (TStime() - d->download_started > d->request->connect_timeout))
		{
			https_cancel(d, "Connect or DNS timeout after %d seconds", d->request->connect_timeout);
			continue;
		}
		if (d->connected && (TStime() - d->download_started > d->request->transfer_timeout))
		{
			https_cancel(d, "Download timeout after %d seconds", d->request->transfer_timeout);
			continue;
		}
	}
}

void url_init(void)
{
	https_ctx = https_new_ctx();
	if (!https_ctx)
	{
		unreal_log(ULOG_ERROR, "url", "HTTPS_NEW_CTX_FAILED", NULL,
			   "Unable to initialize SSL context");
		exit(-1);
	}
	EventAdd(NULL, "url_socket_timeout", url_socket_timeout, NULL, 500, 0);
}

char *urlencode(const char *s, char *wbuf, int wlen)
{
	const char hexchars[16] = "0123456789abcdef";
	char *o = wbuf;

	if (s == NULL)
	{
		if (wlen > 0)
			*o = '\0';
		return NULL;
	}

	for (; *s; s++)
	{
		if (((*s >= 'a') && (*s <= 'z')) ||
		    ((*s >= 'A') && (*s <= 'Z')) ||
		    ((*s >= '0') && (*s <= '9')) ||
		    strchr("-._~", *s))
		{
			if (wlen-- <= 1)
				break;
			*o++ = *s;
		} else
		{
			if (wlen <= 3)
				break;
			wlen -= 3;
			*o++ = '%';
			*o++ = hexchars[(*s >> 4) & 0xF];
			*o++ = hexchars[*s & 0xF];
		}
	}
	/* And zero-terminate.. */
	if (wlen)
		*o = '\0';

	return wbuf;
}

int downloads_in_progress(void)
{
	Download *d;
	int count = 0;

	/* Bit stupid to do it this slow way, can't we maintain a counter? Needs to be accurate though */

	for (d = downloads; d; d = d->next)
		count++;

	return count;
}
