/*
 * (C) 2013-2026 by Christian Hesse <mail@eworm.de>
 *
 * 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 3 of the License, 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, see <https://www.gnu.org/licenses/>.
 *
 */

/* define structs and functions */
#include "pacredir.h"

const static char optstring[] = "hvV";
const static struct option options_long[] = {
	/* name		has_arg		flag	val */
	{ "help",	no_argument,	NULL,	'h' },
	{ "verbose",	no_argument,	NULL,	'v' },
	{ "version",	no_argument,	NULL,	'V' },
	{ 0, 0, 0, 0 }
};

/* global variables */
struct hosts * hosts = NULL;
struct ignore_interfaces * ignore_interfaces = NULL;
uint8_t quit = 0, systemd = 0, update = 0, verbose = 0;
unsigned int count_redirect = 0, count_not_found = 0;

/*** write_log ***/
static int write_log(uint8_t show, uint8_t priority, const char *format, ...) {
	va_list args;
	char * message = NULL;
	size_t len = 0;
	FILE * stream;

	if (show == 0)
		return EXIT_SUCCESS;

	va_start(args, format);
	len = vsnprintf(NULL, 0, format, args) + 1;
	message = malloc(len);
	va_end(args);

	va_start(args, format);
	vsnprintf(message, len, format, args);
	va_end(args);

	/* send to systemd-journald */
	sd_journal_send("MESSAGE=%s", message,
		"SYSLOG_IDENTIFIER=" PROGNAME,
		"PRIORITY=%i", priority,
		NULL);

	if (systemd)
		goto free_return;

	switch (priority) {
		case LOG_WARNING:
		case LOG_ERR:
			stream = stderr;
			break;
		default:
			stream = stdout;
			break;
	}

	fprintf(stream, "%s\n", message);
	fflush(stream);

free_return:
	free(message);

	return EXIT_SUCCESS;
}

/*** get_url ***/
static char * get_url(const char * hostname, const uint16_t port, const uint8_t dbfile, const char * uri) {
	const char * dir;
	char * url;

	dir = dbfile ? "db" : "pkg";
	url = malloc(11 /* static chars of an url & null char */
			+ strlen(hostname)
			+ 5 /* max strlen of decimal 16bit value */
			+ strlen(dir)
			+ strlen(uri));

	sprintf(url, "http://%s:%d/%s/%s", hostname, port, dir, uri);

	return url;
}

/*** update_interfaces ***/
static void update_interfaces(void) {
	struct ignore_interfaces *ignore_interfaces_ptr = ignore_interfaces;

	while (ignore_interfaces_ptr->interface != NULL) {
		ignore_interfaces_ptr->ifindex = if_nametoindex(ignore_interfaces_ptr->interface);
		ignore_interfaces_ptr = ignore_interfaces_ptr->next;
	}
}

/*** get_name ***/
static size_t get_name(const uint8_t* rr_ptr, char* name) {
	uint8_t dot = 0;
	char *name_ptr = name;

	for (;;) {
		if (*rr_ptr == 0)
			return (strlen(name) + 2);
		if (dot)
			*(name_ptr++) = '.';
		else
			dot++;
		memcpy(name_ptr, rr_ptr + 1, *rr_ptr + 1);
		name_ptr += *rr_ptr;
		rr_ptr += *rr_ptr + 1;
	}
}

/*** process_reply_record ***/
static char* process_reply_record(const void *rr, size_t sz) {
	uint16_t class, type, rdlength;
	const uint8_t *rr_ptr = rr;
	char *name;
	uint32_t ttl;

	rr_ptr += strlen((char*)rr_ptr) + 1;
	memcpy(&type, rr_ptr, sizeof(uint16_t));
	rr_ptr += sizeof(uint16_t);
	memcpy(&class, rr_ptr, sizeof(uint16_t));
	rr_ptr += sizeof(uint16_t);
	memcpy(&ttl, rr_ptr, sizeof(uint32_t));
	rr_ptr += sizeof(uint32_t);
	memcpy(&rdlength, rr_ptr, sizeof(uint16_t));
	rr_ptr += sizeof(uint16_t);
	assert(be16toh(type) == DNS_TYPE_PTR);
	assert(be16toh(class) == DNS_CLASS_IN);

	name = malloc(strlen((char*)rr_ptr) + 1);
	rr_ptr += get_name(rr_ptr, name);

	assert(rr_ptr == (const uint8_t*) rr + sz);

	return name;
}

/*** update_hosts ***/
static void update_hosts(void) {
	struct if_nameindex *if_nidxs, *intf;
	struct hosts *hosts_ptr = hosts;
	sd_bus_error error = SD_BUS_ERROR_NULL;
	sd_bus_message *reply = NULL;
	sd_bus *bus = NULL;
	int r, sock;

	/* set 'present' to 0, so we later know which hosts were available, and which were not */
	while (hosts_ptr->host != NULL) {
		hosts_ptr->present = 0;
		hosts_ptr = hosts_ptr->next;
	}

	r = sd_bus_open_system(&bus);
	if (r < 0) {
		write_log(1, LOG_ERR, "Failed to open system bus: %s", strerror(-r));
		goto fast_finish;
	}

	r = sd_bus_call_method(bus, "org.freedesktop.resolve1", "/org/freedesktop/resolve1",
		"org.freedesktop.resolve1.Manager", "ResolveRecord", &error,
		&reply, "isqqt", 0 /* any */, PACSERVE "." MDNS_DOMAIN,
		DNS_CLASS_IN, DNS_TYPE_PTR, SD_RESOLVED_NO_SYNTHESIZE|SD_RESOLVED_NO_ZONE);
	if (r < 0) {
		/* EAGAIN is returned when no mDNS hosts are available - so harmless */
		write_log((errno ^ EAGAIN) | verbose, errno ^ EAGAIN ? LOG_ERR : LOG_DEBUG,
			"Failed to trigger caching record: %s (%s)",
			error.message, strerror(errno));
		sd_bus_error_free(&error);
		goto finish;
	}

	/* On empty cache systemd-resolved returns just one record on first query,
	   happened above. Similar delays are seen for new hosts.
	   Wait a moment, call again for full update, then goto finish.
	   TODO: Drop when continuous mDNS querying becomes available! */
	usleep(250000);

	if ((if_nidxs = if_nameindex()) == NULL) {
		write_log(1, LOG_ERR, "Failed to get list of interfaces.");
		goto finish;
	}

	if ((sock = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
		write_log(1, LOG_ERR, "Failed to open control socket.");
		goto finish;
	}

	for (intf = if_nidxs; intf->if_index != 0 || intf->if_name != NULL; intf++) {
		struct ifreq ifr;
		struct ignore_interfaces *ignore_interfaces_ptr = ignore_interfaces;
		uint8_t ignore = 0;

		memset(&ifr, 0, sizeof(struct ifreq));
		strncpy(ifr.ifr_name, intf->if_name, IFNAMSIZ);
		ifr.ifr_name[IFNAMSIZ-1] = 0;

		if (ioctl(sock, SIOCGIFFLAGS, &ifr) < 0) {
			write_log(1, LOG_ERR, "Failed to get flags for interface %s.", intf->if_name);
			continue;
		}

		if ((ifr.ifr_flags & IFF_UP) == 0)
			continue;

		if ((ifr.ifr_flags & IFF_LOOPBACK) > 0)
			continue;

		if ((ifr.ifr_flags & IFF_RUNNING) == 0)
			continue;

		while (ignore_interfaces_ptr->interface != NULL) {
			if (ignore_interfaces_ptr->ifindex == intf->if_index) {
				ignore++;
				break;
			}

			ignore_interfaces_ptr = ignore_interfaces_ptr->next;
		}

		if (!ignore)
			update_hosts_on_interface(bus, intf->if_index, intf->if_name);

		if (quit)
			break;
	}

	close(sock);
	if_freenameindex(if_nidxs);

finish:
	/* mark hosts offline that did not show up in query */
	hosts_ptr = hosts;
	while (hosts_ptr->host != NULL) {
		if (hosts_ptr->mdns == 1 && hosts_ptr->online == 1 && hosts_ptr->present == 0) {
			write_log(verbose, LOG_NOTICE, "Marking host %s offline", hosts_ptr->host);
			hosts_ptr->online = 0;
		}
		hosts_ptr = hosts_ptr->next;
	}

fast_finish:
	sd_bus_message_unref(reply);
	sd_bus_flush_close_unref(bus);
}

/*** update_hosts_on_interface ***/
static void update_hosts_on_interface(sd_bus *bus, const unsigned int if_index, const char *if_name) {
	sd_bus_error error = SD_BUS_ERROR_NULL;
	sd_bus_message *reply_record = NULL;
	uint64_t flags;
	int r;

	r = sd_bus_call_method(bus, "org.freedesktop.resolve1", "/org/freedesktop/resolve1",
		"org.freedesktop.resolve1.Manager", "ResolveRecord", &error,
		&reply_record, "isqqt", if_index, PACSERVE "." MDNS_DOMAIN,
		DNS_CLASS_IN, DNS_TYPE_PTR, SD_RESOLVED_NO_SYNTHESIZE|SD_RESOLVED_NO_ZONE);
	if (r < 0) {
		/* EAGAIN is returned when no mDNS hosts are available - so harmless */
		write_log((errno ^ EAGAIN) | verbose, errno ^ EAGAIN ? LOG_ERR : LOG_DEBUG,
			"Failed to resolve record on %s: %s (%s)",
			if_name, error.message, strerror(errno));
		sd_bus_error_free(&error);
		goto finish;
	}

	r = sd_bus_message_enter_container(reply_record, 'a', "(iqqay)");
	if (r < 0)
		goto parse_failure_record;

	for (;;) {
		int ifindex;
		uint16_t class, type, port;
		const void *data;
		size_t length;
		char *peer;
		const char *canonical, *discard;
		uint8_t match = 0;

		r = sd_bus_message_enter_container(reply_record, 'r', "iqqay");
		if (r < 0)
			goto parse_failure_record;
		if (r == 0)  /* Reached end of array */
			break;
		r = sd_bus_message_read(reply_record, "iqq", &ifindex, &class, &type);
		if (r < 0)
			goto parse_failure_record;
		r = sd_bus_message_read_array(reply_record, 'y', &data, &length);
		if (r < 0)
			goto parse_failure_record;
		r = sd_bus_message_exit_container(reply_record);
		if (r < 0)
			goto parse_failure_record;

		/* process the data received */
		peer = process_reply_record(data, length);

		sd_bus_message *reply_service = NULL;
		/* service START */
		r = sd_bus_call_method(bus, "org.freedesktop.resolve1", "/org/freedesktop/resolve1",
			"org.freedesktop.resolve1.Manager", "ResolveService", &error,
			&reply_service, "isssit", if_index, "", "", peer, AF_UNSPEC, UINT64_C(0));
		if (r < 0) {
			write_log(1, LOG_ERR, "Failed to resolve service '%s' on %s: %s (%s)",
				peer, if_name, error.message, strerror(errno));
			sd_bus_error_free(&error);
			goto finish_service;
		}

		r = sd_bus_message_enter_container(reply_service, 'a', "(qqqsa(iiay)s)");
		if (r < 0)
			goto parse_failure_service;

		for (;;) {
			uint16_t priority, weight;
			const char *hostname;

			r = sd_bus_message_enter_container(reply_service, 'r', "qqqsa(iiay)s");
			if (r < 0)
				goto parse_failure_service;
			if (r == 0)  /* Reached end of array */
				break;
			r = sd_bus_message_read(reply_service, "qqqs", &priority, &weight, &port, &hostname);
			if (r < 0)
				goto parse_failure_service;

			r = sd_bus_message_enter_container(reply_service, 'a', "(iiay)");
			if (r < 0)
				goto parse_failure_service;

			for (;;) {
				int ifindex, family;
				const void *data;
				size_t length;

				r = sd_bus_message_enter_container(reply_service, 'r', "iiay");
				if (r < 0)
					goto parse_failure_service;
				if (r == 0)  /* Reached end of array */
					break;
				r = sd_bus_message_read(reply_service, "ii", &ifindex, &family);
				if (r < 0)
					goto parse_failure_service;
				r = sd_bus_message_read_array(reply_service, 'y', &data, &length);
				if (r < 0)
					goto parse_failure_service;
				r = sd_bus_message_exit_container(reply_service);
				if (r < 0)
					goto parse_failure_service;
			}
			r = sd_bus_message_exit_container(reply_service);
			if (r < 0)
				goto parse_failure_service;

			r = sd_bus_message_read(reply_service, "s", &canonical);
			if (r < 0)
				goto parse_failure_service;
			r = sd_bus_message_exit_container(reply_service);
			if (r < 0)
				goto parse_failure_service;
		}

		r = sd_bus_message_exit_container(reply_service);
		if (r < 0)
			goto parse_failure_service;
		r = sd_bus_message_enter_container(reply_service, 'a', "ay");
		if (r < 0)
			goto parse_failure_service;

		for(;;) {
			const void *txt_data;
			size_t txt_len;

			r = sd_bus_message_read_array(reply_service, 'y', &txt_data, &txt_len);
			if (r < 0)
				goto parse_failure_service;
			if (r == 0)  /* Reached end of array */
				break;

			/* does the TXT data match our architecture (arch) or distribution (id)? */
			if (strncmp((char*)txt_data, "arch=" ARCH, txt_len) == 0)
				match |= DNS_SRV_TXT_MATCH_ARCH;
			if (strncmp((char*)txt_data, "id=" ID, txt_len) == 0)
				match |= DNS_SRV_TXT_MATCH_ID;
		}

		r = sd_bus_message_exit_container(reply_service);
		if (r < 0)
			goto parse_failure_service;

		r = sd_bus_message_read(reply_service, "s", &discard);
		if (r < 0)
			goto parse_failure_service;
		r = sd_bus_message_read(reply_service, "s", &discard);
		if (r < 0)
			goto parse_failure_service;
		r = sd_bus_message_read(reply_service, "s", &discard);
		if (r < 0)
			goto parse_failure_service;

		r = sd_bus_message_read(reply_service, "t", &flags);
		if (r < 0)
			goto parse_failure_service;

		if (match < DNS_SRV_TXT_MATCH_ALL) {
			write_log(verbose, LOG_INFO, "Host %s does not match distribution and/or architecture.", canonical);
			goto finish_service;
		}

		/* add the peer to our struct */
		add_host(canonical, port, 1);

		goto finish_service;

parse_failure_service:
		write_log(1, LOG_ERR, "Parse failure for service: %s", strerror(-r));

finish_service:
		free(peer);
		sd_bus_message_unref(reply_service);
	}

	r = sd_bus_message_exit_container(reply_record);
	if (r < 0)
		goto parse_failure_record;
	r = sd_bus_message_read(reply_record, "t", &flags);
	if (r < 0)
		goto parse_failure_record;

	goto finish;

parse_failure_record:
	write_log(1, LOG_ERR, "Parse failure for record: %s", strerror(-r));

finish:
	sd_bus_message_unref(reply_record);
}

/*** add_host ***/
static int add_host(const char * host, const uint16_t port, const uint8_t mdns) {
	struct hosts * hosts_ptr = hosts, * previous = NULL;

	while (hosts_ptr->host != NULL) {
		int cmp = strcmp(hosts_ptr->host, host);

		/* skip to next */
		if (cmp < 0) {
			previous = hosts_ptr;
			hosts_ptr = hosts_ptr->next;
			continue;
		}

		/* host already exists */
		if (cmp == 0) {
			/* ... but with different port */
			if (hosts_ptr->port != port) {
				previous = hosts_ptr;
				hosts_ptr = hosts_ptr->next;
				continue;
			}

			if (hosts_ptr->online < 1)
				write_log(verbose, LOG_NOTICE, "Marking host %s online", host);
			goto update;
		}

		/* insert before, or first */
		if (cmp > 0) {
			struct hosts * new = malloc(sizeof(struct hosts));
			new->next = hosts_ptr;

			/* first! */
			if (hosts_ptr == hosts) {
				hosts_ptr = hosts = new;
				goto new;
			}

			/* before, but not first */
			hosts_ptr = previous->next = new;
			goto new;
		}
	}

	/* append at the end */
	hosts_ptr->next = malloc(sizeof(struct hosts));
	hosts_ptr->next->host = NULL;
	hosts_ptr->next->next = NULL;

new:
	/* host not found, adding a new one */
	write_log(verbose, mdns ? LOG_NOTICE : LOG_INFO, "Adding host %s with port %d", host, port);

	hosts_ptr->host = strdup(host);
	hosts_ptr->port = port;
	hosts_ptr->mdns = mdns;
	hosts_ptr->badtime = 0;
	hosts_ptr->badcount = 0;
	hosts_ptr->finds = 0;
	hosts_ptr->redir = 0;

update:
	hosts_ptr->online = 1;
	hosts_ptr->present = 1;

	return EXIT_SUCCESS;
}

/*** find_best_redirect ***/
static struct request * find_best_redirect(const char * basename, uint8_t dbfile, time_t last_modified) {
	struct hosts * hosts_ptr = hosts;

	int req_idx = -1;
	struct request * best = NULL,
		** requests = NULL,
		* request = NULL;
	double best_time = INFINITY;
	char ctime[26];
	struct timeval tv;

	CURLM *curlm;
	CURLMsg *msg;
	int msgs_left, still_running = 1;

	gettimeofday(&tv, NULL);

	if ((curlm = curl_multi_init()) == NULL) {
		write_log(1, LOG_ERR, "Failed initializing curl multi handle.");
		return NULL;
	}

	/* prepare the requests */
	while (hosts_ptr->host != NULL) {
		CURL *curl = NULL;
		time_t badtime = hosts_ptr->badtime + hosts_ptr->badcount * BADTIME;

		/* skip host if offline */
		if (hosts_ptr->online == 0) {
			write_log(verbose, LOG_INFO, "Host %s is offline, skipping", hosts_ptr->host);
			hosts_ptr = hosts_ptr->next;
			continue;
		}

		/* skip host if had a bad request within last BADTIME seconds */
		if (badtime > tv.tv_sec) {
			/* write the time to buffer ctime, then strip the line break */
			ctime_r(&badtime, ctime);
			ctime[strlen(ctime) - 1] = '\0';

			write_log(verbose, LOG_INFO, "Host %s is marked bad until %s, skipping.",
					hosts_ptr->host, ctime);

			hosts_ptr = hosts_ptr->next;
			continue;
		}

		req_idx++;
		requests = realloc(requests, sizeof(size_t) * (req_idx + 1));
		request = requests[req_idx] = malloc(sizeof(struct request));

		/* prepare request struct */
		request->host = hosts_ptr;
		request->url = get_url(request->host->host, request->host->port, dbfile, basename);
		request->http_code = 0;
		request->last_modified = 0;
		request->curl = curl_easy_init();
		*request->errbuf = 0;

		if ((curl = request->curl)  == NULL) {
			write_log(1, LOG_ERR, "Failed initializing curl_easy!");
			goto curl_easy_fail;
		}

		write_log(verbose, LOG_INFO, "Trying %s: %s", request->host->host, request->url);

		curl_easy_setopt(curl, CURLOPT_URL, request->url);
		/* try to resolve addresses to all IP versions that your system allows */
		curl_easy_setopt(curl, CURLOPT_IPRESOLVE,
			request->host->mdns ? CURL_IPRESOLVE_V4 : CURL_IPRESOLVE_WHATEVER);
		/* tell libcurl to follow redirection */
		curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
		/* set user agent */
		curl_easy_setopt(curl, CURLOPT_USERAGENT, "pacredir/" VERSION " (" ID "/" ARCH ")");
		/* do not receive body */
		curl_easy_setopt(curl, CURLOPT_NOBODY, 1L);
		/* ask for filetime */
		curl_easy_setopt(curl, CURLOPT_FILETIME, 1L);
		/* set connection timeout to 2 seconds
		 * if the host needs longer we do not want to use it anyway ;) */
		curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 2L);
		/* time out if connection is established but transfer rate is low
		 * this should make curl finish after a maximum of 8 seconds */
		curl_easy_setopt(curl, CURLOPT_LOW_SPEED_LIMIT, 1L);
		curl_easy_setopt(curl, CURLOPT_LOW_SPEED_TIME, 3L);
		/* skip all signal handling */
		curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L);
		/* provide a buffer to store errors in */
		curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, request->errbuf);

		/* add easy handle to multi handle */
		curl_multi_add_handle(curlm, curl);

		hosts_ptr = hosts_ptr->next;
	}

	/* perform all the requests */
	while (still_running) {
		CURLMcode mresult = curl_multi_perform(curlm, &still_running);

		if (still_running)
			/* wait for activity, timeout or "nothing" */
			mresult = curl_multi_poll(curlm, NULL, 0, 1000, NULL);

		if (mresult)
			break;
	}

	/* see how the transfers went */
	while ((msg = curl_multi_info_read(curlm, &msgs_left)) != NULL) {
		CURL *curl = NULL;
		CURLcode res;

		if (msg->msg != CURLMSG_DONE)
			continue;

		/* Find out which handle this message is about */
		for (int idx = 0; idx <= req_idx; idx++) {
			request = requests[idx];
			curl = request->curl;
			if (msg->easy_handle == curl)
				break;
		}

		/* something went wrong... */
		if (msg->data.result != CURLE_OK) {
			write_log(1, LOG_WARNING, "Could not connect to peer %s on port %d: %s",
				request->host->host, request->host->port,
				*request->errbuf != 0 ? request->errbuf : curl_easy_strerror(msg->data.result));
			request->host->badtime = tv.tv_sec;
			request->host->badcount++;
			goto request_free;
		}

		request->host->badtime = 0;
		request->host->badcount = 0;

		/* get http status code */
		if ((res = curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &(request->http_code))) != CURLE_OK) {
			write_log(1, LOG_ERR, "curl_easy_getinfo() failed: %s", curl_easy_strerror(res));
			goto request_free;
		}

		/* get total time */
		if ((res = curl_easy_getinfo(curl, CURLINFO_TOTAL_TIME, &(request->time_total))) != CURLE_OK) {
			write_log(1, LOG_ERR, "curl_easy_getinfo() failed: %s", curl_easy_strerror(res));
			goto request_free;
		}

		/* get last modified time */
		if (request->http_code == MHD_HTTP_OK) {
			if ((res = curl_easy_getinfo(curl, CURLINFO_FILETIME, &(request->last_modified))) != CURLE_OK) {
				write_log(1, LOG_ERR, "curl_easy_getinfo() failed: %s", curl_easy_strerror(res));
				goto request_free;
			}
		}

		/* skip if http code not OK, but clean up */
		if (request->http_code != MHD_HTTP_OK) {
			write_log(verbose, LOG_INFO, "Received HTTP status code %d from %s",
					request->http_code, request->host->host);
			goto request_free;
		}

		/* found the file! */
		request->host->finds++;
		/* write the time to buffer ctime, then strip the line break */
		ctime_r(&request->last_modified, ctime);
		ctime[strlen(ctime) - 1] = '\0';

		write_log(verbose, LOG_INFO, "Found on %s (%f sec, modified: %s)",
				request->host->host, request->time_total, ctime);

		if	/* for db files choose the most recent peer when not too old */
			((dbfile == 1 && ((request->last_modified > last_modified &&
					   request->last_modified + 86400 > time(NULL)) ||
			/* but use a faster peer if available */
					  (best->url != NULL &&
					   request->last_modified >= last_modified &&
					   request->time_total < best_time))) ||
			/* for packages try to guess the fastest peer */
			 (dbfile == 0 && request->time_total < best_time)) {
			best_time = request->time_total;
			if (best != NULL) {
				free(best->url);
				free(best);
			}
			best = request;
		}

request_free:
		curl_multi_remove_handle(curlm, curl);
		curl_easy_cleanup(curl);

		if (request != best) {
			free(request->url);
			free(request);
		}
	}

	if (best == NULL) {
		if (req_idx < 0)
			write_log(verbose, LOG_NOTICE, "Currently no peers are available to check for %s.",
					basename);
		else if (dbfile > 0)
			write_log(verbose, LOG_NOTICE, "No more recent version of %s found on %d peers.",
					basename, req_idx + 1);
		else
			write_log(verbose, LOG_NOTICE, "File %s not found on %d peers, giving up.",
					basename, req_idx + 1);
	}

curl_easy_fail:
	free(requests);
	curl_multi_cleanup(curlm);

	return best;
}

/* append_string */
static char * append_string(char * string, const char *format, ...) {
	va_list args;
	size_t string_len = 0, append_len;

	if (string != NULL)
		string_len = strlen(string);

	va_start(args, format);
	append_len = vsnprintf(NULL, 0, format, args) + 1;
	va_end(args);

	string = realloc(string, string_len + append_len);

	va_start(args, format);
	vsnprintf(string + string_len, append_len, format, args);
	va_end(args);

	return string;
}
/*** status_page ***/
static char * status_page(void) {
	struct ignore_interfaces * ignore_interfaces_ptr = ignore_interfaces;
	struct hosts * hosts_ptr = hosts;
	char *page = NULL, *overall = CIRCLE_BLUE;
	char hostname[HOST_NAME_MAX];
	struct timeval tv;

	/* initialize struct timeval */
	gettimeofday(&tv, NULL);

	if (count_redirect + count_not_found)
		switch (count_redirect * 4 / (count_redirect + count_not_found)) {
			case 0:
				overall = CIRCLE_RED;
				break;
			case 1:
				overall = CIRCLE_ORANGE;
				break;
			case 2:
				overall = CIRCLE_YELLOW;
				break;
			default:
				overall = CIRCLE_GREEN;
				break;
		}

	gethostname(hostname, HOST_NAME_MAX);
	page = append_string(page, STATUS_HEAD, hostname, count_redirect, count_not_found, overall);

	page = append_string(page, STATUS_INT_HEAD);
	if (ignore_interfaces_ptr->interface == NULL)
		page = append_string(page, STATUS_INT_NONE);
	while (ignore_interfaces_ptr->interface != NULL) {
		if (ignore_interfaces_ptr->ifindex > 0)
			page = append_string(page, STATUS_INT_ONE,
				ignore_interfaces_ptr->interface, ignore_interfaces_ptr->ifindex);
		else
			page = append_string(page, STATUS_INT_ONE_NA,
				ignore_interfaces_ptr->interface);

		ignore_interfaces_ptr = ignore_interfaces_ptr->next;
	}
	page = append_string(page, STATUS_INT_FOOT);

	page = append_string(page, STATUS_HOST_HEAD);
	if (hosts_ptr->host == NULL)
		page = append_string(page, STATUS_HOST_NONE);
	while (hosts_ptr->host != NULL) {
		time_t badtime = hosts_ptr->badtime + hosts_ptr->badcount * BADTIME;
		uint8_t bad = hosts_ptr->badcount && badtime > tv.tv_sec ? 1 : 0;

		const char *state = hosts_ptr->mdns ? (hosts_ptr->online ? "online" : "offline") : "static";
		page = append_string(page, STATUS_HOST_ONE,
			state, hosts_ptr->host, hosts_ptr->port,
			hosts_ptr->mdns ? (hosts_ptr->online ? CIRCLE_GREEN : CIRCLE_RED) : CIRCLE_BLUE,
			state, hosts_ptr->redir ? CIRCLE_GREEN : CIRCLE_BLUE, hosts_ptr->redir,
			hosts_ptr->finds ? CIRCLE_GREEN : CIRCLE_BLUE, hosts_ptr->finds,
			bad ? CIRCLE_RED : CIRCLE_BLUE, hosts_ptr->badcount);

		hosts_ptr = hosts_ptr->next;
	}
	page = append_string(page, STATUS_HOST_FOOT);

	page = append_string(page, STATUS_FOOT);

	return page;
}

/*** ahc_echo ***
 * called whenever a http request is received */
static enum MHD_Result ahc_echo(void * cls,
		struct MHD_Connection * connection,
		const char * uri,
		const char * method,
		const char * version,
		const char * upload_data,
		size_t * upload_data_size,
		void ** ptr) {
	static int dummy;
	struct MHD_Response * response;
	int ret;

	struct request * request = NULL;
	char * page = NULL;
	static_file * file = NULL;
	const char * basename;
	struct timeval tv;

	struct tm tm;
	const char * if_modified_since = NULL;
	time_t last_modified = 0;
	uint8_t dbfile = 0;
	long http_code = MHD_HTTP_NOT_FOUND;

	/* initialize struct timeval */
	gettimeofday(&tv, NULL);

	/* give status page */
	if (strcmp(uri, "/") == 0) {
		http_code = MHD_HTTP_OK;
		page = status_page();
		goto response;
	}

	/* give a simple ok response for monitoring */
	if (strcmp(uri, "/check") == 0) {
		http_code = MHD_HTTP_OK;
		page = strdup("OK");
		goto response;
	}

	/* give favicon */
	if (strcmp(uri, "/favicon.png") == 0) {
		http_code = MHD_HTTP_OK;
		file = &favicon;
		goto response;
	}

	/* give style */
	if (strcmp(uri, "/style.css") == 0) {
		http_code = MHD_HTTP_OK;
		file = &style;
		goto response;
	}

	/* we want the filename, not the path */
	basename = uri;
	while (strstr(basename, "/") != NULL)
		basename = strstr(basename, "/") + 1;

	/* unexpected method */
	if (strcmp(method, "GET") != 0 && strcmp(method, "HEAD") != 0)
		return MHD_NO;

	/* The first time only the headers are valid,
	 * do not respond in the first round... */
	if (&dummy != *ptr) {
		*ptr = &dummy;
		return MHD_YES;
	}

	/* upload data in a GET!? */
	if (*upload_data_size != 0)
		return MHD_NO;

	/* clear context pointer */
	*ptr = NULL;

	/* process db file request (*.db and *.files) */
	if ((strlen(basename) > 3 && strcmp(basename + strlen(basename) - 3, ".db") == 0) ||
			(strlen(basename) > 6 && strcmp(basename + strlen(basename) - 6, ".files") == 0)) {
		dbfile = 1;

		/* get timestamp from request */
		if ((if_modified_since = MHD_lookup_connection_value(connection,
				MHD_HEADER_KIND, MHD_HTTP_HEADER_IF_MODIFIED_SINCE))) {
			if (strptime(if_modified_since, "%a, %d %b %Y %H:%M:%S %Z", &tm) != NULL) {
				last_modified = timegm(&tm);
			}
		}
	}

	/* try to find the best redirect, and count */
	if ((request = find_best_redirect(basename, dbfile, last_modified)) != NULL) {
		http_code = MHD_HTTP_TEMPORARY_REDIRECT;
		count_redirect++;
	} else {
		count_not_found++;
	}

response:
	/* give response */
	if (http_code == MHD_HTTP_TEMPORARY_REDIRECT) {
		write_log(1, LOG_NOTICE, "Redirecting to %s: %s", request->host->host, request->url);
		request->host->redir++;
		page = malloc(strlen(PAGE307) + strlen(request->url) + strlen(basename) + 1);
		sprintf(page, PAGE307, request->url, basename);
		response = MHD_create_response_from_buffer(strlen(page), (void*) page, MHD_RESPMEM_MUST_FREE);
		ret = MHD_add_response_header(response, "Location", request->url);
		free(request);
	} else if (http_code == MHD_HTTP_OK) {
		if (page != NULL) {
			write_log(1, LOG_INFO, "Sending status page.");
			response = MHD_create_response_from_buffer(strlen(page), (void*) page, MHD_RESPMEM_MUST_FREE);
			ret = MHD_add_response_header(response, "Content-Type", "text/html");
		} else {
			write_log(1, LOG_INFO, "Sending static file: %s", uri);
			response = MHD_create_response_from_buffer(file->size, file->content, MHD_RESPMEM_PERSISTENT);
			ret = MHD_add_response_header(response, "ETag", file->sha1);
			ret = MHD_add_response_header(response, "Last-Modified", file->date);
			ret = MHD_add_response_header(response, "Content-Type", file->mime);
			ret = MHD_add_response_header(response, "Cache-Control", "max-age=86400");
		}
	} else { /* MHD_HTTP_NOT_FOUND */
		write_log(1, LOG_NOTICE, "Sending 'Not Found' for: %s", basename);
		page = malloc(strlen(PAGE404) + strlen(basename) + 1);
		sprintf(page, PAGE404, basename);
		response = MHD_create_response_from_buffer(strlen(page), (void*) page, MHD_RESPMEM_MUST_FREE);
	}

	ret = MHD_add_response_header(response, "Server", PROGNAME " v" VERSION " " ID "/" ARCH);
	ret = MHD_queue_response(connection, http_code, response);
	MHD_destroy_response(response);

	/* report counts to systemd */
	sd_notifyf(0, "STATUS=%d redirects, %d not found, waiting...",
			count_redirect, count_not_found);

	return ret;
}

/*** sig_callback ***/
static void sig_callback(int signal) {
	write_log(1, LOG_NOTICE, "Received signal '%s', quitting.", strsignal(signal));

	quit++;
}

/*** sighup_callback ***/
static void sighup_callback(int signal) {
	struct hosts * hosts_ptr = hosts;

	write_log(1, LOG_NOTICE, "Received signal '%s', resetting bad counts, updating interfaces and hosts.",
		strsignal(signal));

	while (hosts_ptr->host != NULL) {
		hosts_ptr->badtime = 0;
		hosts_ptr->badcount = 0;
		hosts_ptr = hosts_ptr->next;
	}

	update++;
}

/*** sigusr_callback ***/
static void sigusr_callback(int signal) {
	struct ignore_interfaces * ignore_interfaces_ptr = ignore_interfaces;
	struct hosts * hosts_ptr = hosts;
	struct timeval tv;

	/* initialize struct timeval */
	gettimeofday(&tv, NULL);

	write_log(1, LOG_NOTICE, "Received signal '%s', dumping state.", strsignal(signal));

	write_log(1, LOG_INFO, "Ignored interfaces:");
	if (ignore_interfaces_ptr->interface == NULL)
		write_log(1, LOG_INFO, " (none)");
	while (ignore_interfaces_ptr->interface != NULL) {
		if (ignore_interfaces_ptr->ifindex > 0)
			write_log(1, LOG_INFO, " -> %s (link %d)",
				ignore_interfaces_ptr->interface,  ignore_interfaces_ptr->ifindex);
		else
			write_log(1, LOG_INFO, " -> %s (N/A)", ignore_interfaces_ptr->interface);

		ignore_interfaces_ptr = ignore_interfaces_ptr->next;
	}

	write_log(1, LOG_INFO, "Known hosts:");
	if (hosts_ptr->host == NULL)
		write_log(1, LOG_INFO, " (none)");
	while (hosts_ptr->host != NULL) {
		uint8_t not_avail = (hosts_ptr->mdns && !hosts_ptr->online) || (hosts_ptr->badcount &&
			(hosts_ptr->badtime + hosts_ptr->badcount * BADTIME) > tv.tv_sec) ? 1 : 0;

		write_log(1, LOG_INFO, " -> %s%s%s (%s, %s, port: %d, redir: %d, finds: %d, bad: %d)",
			not_avail ? "[" : "", hosts_ptr->host, not_avail ? "]" : "",
			hosts_ptr->mdns ? "mdns" : "static", hosts_ptr->online ? "online" : "offline",
			hosts_ptr->port, hosts_ptr->redir, hosts_ptr->finds, hosts_ptr->badcount);

		hosts_ptr = hosts_ptr->next;
	}

	write_log(1, LOG_NOTICE, "%d redirects, %d not found.",
		count_redirect, count_not_found);
}

/*** main ***/
int main(int argc, char ** argv) {
	dictionary * ini;
	const char * inistring;
	char * values, * value;
	uint16_t port;
	struct ignore_interfaces * ignore_interfaces_ptr;
	int i, ret = 1, sleepsec = 0;
	struct MHD_Daemon * mhd;
	struct hosts * hosts_ptr;
	struct sockaddr_in address;

	unsigned int version = 0, help = 0;

	/* get the verbose status */
	while ((i = getopt_long(argc, argv, optstring, options_long, NULL)) != -1) {
		switch (i) {
			case 'h':
				help++;
				break;
			case 'v':
				verbose++;
				break;
			case 'V':
				verbose++;
				version++;
				break;
		}
	}

	/* are we running from systemd? */
	if (sd_notify(0, "READY=0") > 0)
		systemd = 1;

	write_log(verbose, LOG_NOTICE, "%s: " PROGNAME " v" VERSION " " ID "/" ARCH
			" (built: " __DATE__ ", " __TIME__ ")", argv[0]);

	if (help > 0)
		write_log(1, LOG_NOTICE, "usage: %s [-h] [-v] [-V]", argv[0]);

	if (version > 0 || help > 0)
		return EXIT_SUCCESS;

	if (getuid() == 0) {
		/* process is running as root, drop privileges */
		write_log(verbose, LOG_WARNING, "Running as root, meh! Dropping privileges.");
		if (setgid(DROP_PRIV_GID) != 0 || setuid(DROP_PRIV_UID) != 0)
			write_log(1, LOG_ERR, "Unable to drop user privileges!");
	}

	/* allocate first struct element as dummy */
	hosts = malloc(sizeof(struct hosts));
	hosts->host = NULL;
	hosts->online = 0;
	hosts->badtime = 0;
	hosts->next = NULL;

	ignore_interfaces = malloc(sizeof(struct ignore_interfaces));
	ignore_interfaces->interface = NULL;
	ignore_interfaces->ifindex = 0;
	ignore_interfaces->next = NULL;

	/* Probing for static pacserve hosts takes some time.
	 * Receiving a SIGHUP at this time could kill us. So register signal
	 * SIGHUP here before probing. */
	struct sigaction act_hup = { 0 };
	act_hup.sa_handler = sighup_callback;
	sigaction(SIGHUP, &act_hup, NULL);

	/* parse config file */
	if ((ini = iniparser_load(CONFIGFILE)) == NULL) {
		write_log(1, LOG_WARNING, "cannot parse file " CONFIGFILE ", continue anyway");
		/* continue anyway, there is nothing essential in the config file */
	} else {
		int ini_verbose;

		/* extra verbosity from config */
		ini_verbose = iniparser_getint(ini, "general:verbose", 0);
		verbose += ini_verbose;

		/* store interfaces to ignore */
		if ((inistring = iniparser_getstring(ini, "general:ignore interfaces", NULL)) != NULL) {
			values = strdup(inistring);
			ignore_interfaces_ptr = ignore_interfaces;

			value = strtok(values, DELIMITER);
			while (value != NULL) {
				write_log(verbose, LOG_INFO, "Ignoring interface: %s", value);
				ignore_interfaces_ptr->interface = strdup(value);
				ignore_interfaces_ptr->next = malloc(sizeof(struct ignore_interfaces));
				ignore_interfaces_ptr = ignore_interfaces_ptr->next;
				value = strtok(NULL, DELIMITER);
			}
			ignore_interfaces_ptr->interface = NULL;
			ignore_interfaces_ptr->next = NULL;
			free(values);
		}

		/* add static pacserve hosts */
		if ((inistring = iniparser_getstring(ini, "general:pacserve hosts", NULL)) != NULL) {
			values = strdup(inistring);
			value = strtok(values, DELIMITER);
			while (value != NULL) {
				write_log(verbose, LOG_NOTICE, "Adding static host: %s", value);

				if (strchr(value, ':') != NULL) {
					port = atoi(strchr(value, ':') + 1);
					*strchr(value, ':') = 0;
				} else
					port = PORT_PACSERVE;
				add_host(value, port, 0);
				value = strtok(NULL, DELIMITER);
			}
			free(values);
		}

		/* done reading config file, free */
		iniparser_freedict(ini);
	}

	/* prepare struct to make microhttpd listen on localhost only */
	address.sin_family = AF_INET;
	address.sin_port = htons(PORT_PACREDIR);
	address.sin_addr.s_addr = htonl(INADDR_LOOPBACK);

	/* start http server */
	if ((mhd = MHD_start_daemon(MHD_USE_THREAD_PER_CONNECTION | MHD_USE_TCP_FASTOPEN, PORT_PACREDIR,
			NULL, NULL, &ahc_echo, NULL, MHD_OPTION_SOCK_ADDR, &address, MHD_OPTION_END)) == NULL) {
		write_log(1, LOG_ERR, "Could not start daemon on port %d.", PORT_PACREDIR);
		goto fail;
	}

	write_log(verbose, LOG_NOTICE, "Listening on port %d", PORT_PACREDIR);

	/* initialize curl */
	curl_global_init(CURL_GLOBAL_ALL);

	/* register SIG{INT,KILL,TERM} signal callbacks */
	struct sigaction act = { 0 };
	act.sa_handler = sig_callback;
	sigaction(SIGINT,  &act, NULL);
	sigaction(SIGKILL, &act, NULL);
	sigaction(SIGTERM, &act, NULL);

	/* register SIGUSR[12] signal callbacks */
	struct sigaction act_usr = { 0 };
	act_usr.sa_handler = sigusr_callback;
	sigaction(SIGUSR1, &act_usr, NULL);
	sigaction(SIGUSR2, &act_usr, NULL);

	/* report ready to systemd */
	sd_notify(0, "READY=1\nSTATUS=Waiting for requests to redirect...");

	/* main loop */
	while (quit == 0) {
		sleepsec = sleep(sleepsec);

		if (sleepsec > 0 && update == 0)
			continue;

		update_interfaces();
		update_hosts();
		update = 0;
		sleepsec = 60;
	}

	/* report stopping to systemd */
	sd_notify(0, "STOPPING=1\nSTATUS=Stopping...");

	/* stop http server */
	MHD_stop_daemon(mhd);

	/* we're done with libcurl, so clean it up */
	curl_global_cleanup();

	ret = EXIT_SUCCESS;

fail:

	/* Cleanup things */
	while (hosts->host != NULL) {
		free(hosts->host);
		hosts_ptr = hosts->next;
		free(hosts);
		hosts = hosts_ptr;
	}
	free(hosts);

	while (ignore_interfaces->interface != NULL) {
		free(ignore_interfaces->interface);
		ignore_interfaces_ptr = ignore_interfaces->next;
		free(ignore_interfaces);
		ignore_interfaces = ignore_interfaces_ptr;
	}
	free(ignore_interfaces);

	sd_notify(0, "STATUS=Stopped. Bye!");

	return ret;
}
