/*
 * libwebsockets - small server side websockets and web server implementation
 *
 * Copyright (C) 2010 - 2020 Andy Green <andy@warmcat.com>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to
 * deal in the Software without restriction, including without limitation the
 * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
 * sell copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
 * IN THE SOFTWARE.
 */

#include <libwebsockets.h>
#include "private-lib-core.h"

/* requires context->lock */
static void
__lws_peer_remove_from_peer_wait_list(struct lws_context *context,
				      struct lws_peer *peer)
{
	struct lws_peer *df;

	lws_start_foreach_llp(struct lws_peer **, p, context->peer_wait_list) {
		if (*p == peer) {
			df = *p;

			*p = df->peer_wait_list;
			df->peer_wait_list = NULL;

			return;
		}
	} lws_end_foreach_llp(p, peer_wait_list);
}

/* requires context->lock */
static void
__lws_peer_add_to_peer_wait_list(struct lws_context *context,
				 struct lws_peer *peer)
{
	__lws_peer_remove_from_peer_wait_list(context, peer);

	peer->peer_wait_list = context->peer_wait_list;
	context->peer_wait_list = peer;
}


struct lws_peer *
lws_get_or_create_peer(struct lws_vhost *vhost, lws_sockfd_type sockfd)
{
	struct lws_context *context = vhost->context;
	struct lws_peer *peer;
	lws_sockaddr46 sa46;
	socklen_t rlen = 0;
	uint32_t hash = 0;
	uint8_t *q8;
	void *q;
	int n;

	if (vhost->options & LWS_SERVER_OPTION_UNIX_SOCK)
		return NULL;

	rlen = sizeof(sa46);
	if (getpeername(sockfd, (struct sockaddr*)&sa46, &rlen))
		/* eg, udp doesn't have to have a peer */
		return NULL;

#ifdef LWS_WITH_IPV6
	if (sa46.sa4.sin_family == AF_INET6) {
		q = &sa46.sa6.sin6_addr;
		rlen = sizeof(sa46.sa6.sin6_addr);
	} else
#endif
	{
		q = &sa46.sa4.sin_addr;
		rlen = sizeof(sa46.sa4.sin_addr);
	}

	q8 = q;
	for (n = 0; n < (int)rlen; n++)
		hash = (((hash << 4) | (hash >> 28)) * n) ^ q8[n];

	hash = hash % context->pl_hash_elements;

	lws_context_lock(context, "peer search"); /* <======================= */

	lws_start_foreach_ll(struct lws_peer *, peerx,
			     context->pl_hash_table[hash]) {
		if (peerx->sa46.sa4.sin_family == sa46.sa4.sin_family) {
#if defined(LWS_WITH_IPV6)
			if (sa46.sa4.sin_family == AF_INET6 &&
			    !memcmp(q, &peerx->sa46.sa6.sin6_addr, rlen))
				goto hit;
#endif
			if (sa46.sa4.sin_family == AF_INET &&
			    !memcmp(q, &peerx->sa46.sa4.sin_addr, rlen)) {
#if defined(LWS_WITH_IPV6)
hit:
#endif
				lws_context_unlock(context); /* === */

				return peerx;
			}
		}
	} lws_end_foreach_ll(peerx, next);

	lwsl_info("%s: creating new peer\n", __func__);

	peer = lws_zalloc(sizeof(*peer), "peer");
	if (!peer) {
		lws_context_unlock(context); /* === */
		lwsl_err("%s: OOM for new peer\n", __func__);
		return NULL;
	}

	context->count_peers++;
	peer->next = context->pl_hash_table[hash];
	peer->hash = hash;
	peer->sa46 = sa46;
	context->pl_hash_table[hash] = peer;
	time(&peer->time_created);
	/*
	 * On creation, the peer has no wsi attached, so is created on the
	 * wait list.  When a wsi is added it is removed from the wait list.
	 */
	time(&peer->time_closed_all);
	__lws_peer_add_to_peer_wait_list(context, peer);

	lws_context_unlock(context); /* ====================================> */

	return peer;
}

/* requires context->lock */
static int
__lws_peer_destroy(struct lws_context *context, struct lws_peer *peer)
{
	lws_start_foreach_llp(struct lws_peer **, p,
			      context->pl_hash_table[peer->hash]) {
		if (*p == peer) {
			struct lws_peer *df = *p;
			*p = df->next;
			lws_free(df);
			context->count_peers--;

			return 0;
		}
	} lws_end_foreach_llp(p, next);

	return 1;
}

void
lws_peer_cull_peer_wait_list(struct lws_context *context)
{
	struct lws_peer *df;
	time_t t;

	time(&t);

	if (context->next_cull && t < context->next_cull)
		return;

	lws_context_lock(context, "peer cull"); /* <========================= */

	context->next_cull = t + 5;

	lws_start_foreach_llp(struct lws_peer **, p, context->peer_wait_list) {
		if (t - (*p)->time_closed_all > 10) {
			df = *p;

			/* remove us from the peer wait list */
			*p = df->peer_wait_list;
			df->peer_wait_list = NULL;

			__lws_peer_destroy(context, df);
			continue; /* we already point to next, if any */
		}
	} lws_end_foreach_llp(p, peer_wait_list);

	lws_context_unlock(context); /* ====================================> */
}

void
lws_peer_add_wsi(struct lws_context *context, struct lws_peer *peer,
		 struct lws *wsi)
{
	if (!peer)
		return;

	lws_context_lock(context, "peer add"); /* <========================== */

	peer->count_wsi++;
	wsi->peer = peer;
	__lws_peer_remove_from_peer_wait_list(context, peer);

	lws_context_unlock(context); /* ====================================> */
}

void
lws_peer_dump_from_wsi(struct lws *wsi)
{
	struct lws_peer *peer;

	if (!wsi || !wsi->peer)
		return;

	peer = wsi->peer;

#if defined(LWS_ROLE_H1) || defined(LWS_ROLE_H2)
	lwsl_notice("%s: wsi %p: created %llu: wsi: %d/%d, ah %d/%d\n",
			__func__,
			wsi, (unsigned long long)peer->time_created,
			peer->count_wsi, peer->total_wsi,
			peer->http.count_ah, peer->http.total_ah);
#else
	lwsl_notice("%s: wsi %p: created %llu: wsi: %d/%d\n", __func__,
			wsi, (unsigned long long)peer->time_created,
			peer->count_wsi, peer->total_wsi);
#endif
}

void
lws_peer_track_wsi_close(struct lws_context *context, struct lws_peer *peer)
{
	if (!peer)
		return;

	lws_context_lock(context, "peer wsi close"); /* <==================== */

	assert(peer->count_wsi);
	peer->count_wsi--;

	if (!peer->count_wsi
#if defined(LWS_ROLE_H1) || defined(LWS_ROLE_H2)
			&& !peer->http.count_ah
#endif
	) {
		/*
		 * in order that we can accumulate peer activity correctly
		 * allowing for periods when the peer has no connections,
		 * we don't synchronously destroy the peer when his last
		 * wsi closes.  Instead we mark the time his last wsi
		 * closed and add him to a peer_wait_list to be reaped
		 * later if no further activity is coming.
		 */
		time(&peer->time_closed_all);
		__lws_peer_add_to_peer_wait_list(context, peer);
	}

	lws_context_unlock(context); /* ====================================> */
}

#if defined(LWS_ROLE_H1) || defined(LWS_ROLE_H2)
int
lws_peer_confirm_ah_attach_ok(struct lws_context *context,
			      struct lws_peer *peer)
{
	if (!peer)
		return 0;

	if (context->ip_limit_ah &&
	    peer->http.count_ah >= context->ip_limit_ah) {
		lwsl_info("peer reached ah limit %d, deferring\n",
				context->ip_limit_ah);

		return 1;
	}

	return 0;
}

void
lws_peer_track_ah_detach(struct lws_context *context, struct lws_peer *peer)
{
	if (!peer)
		return;

	lws_context_lock(context, "peer ah detach"); /* <==================== */
	assert(peer->http.count_ah);
	peer->http.count_ah--;
	lws_context_unlock(context); /* ====================================> */
}
#endif
