/*
 * octoi_sock.c - OCTOI Socket handling code
 *
 * (C) 2022 by Harald Welte <laforge@osmocom.org>
 *
 * All Rights Reserved
 *
 * SPDX-License-Identifier: GPL-2.0+
 *
 * 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 2 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.
 */

#include <stdint.h>
#include <errno.h>
#include <stdbool.h>
#include <unistd.h>

#include <sys/socket.h>
#include <sys/un.h>
#include <netinet/in.h>

#include <osmocom/core/linuxlist.h>
#include <osmocom/core/utils.h>
#include <osmocom/core/select.h>
#include <osmocom/core/socket.h>
#include <osmocom/core/sockaddr_str.h>
#include <osmocom/core/msgb.h>
#include <osmocom/core/logging.h>

#include <osmocom/octoi/e1oip_proto.h>

#include "octoi_sock.h"
#include "e1oip.h"

/* determine domain / AF of socket */
static int sock_get_domain(int fd)
{
	int domain;
	socklen_t dom_len = sizeof(domain);
	int rc;

	rc = getsockopt(fd, SOL_SOCKET, SO_DOMAIN, &domain, &dom_len);
	if (rc < 0)
		return rc;

	return domain;
}

/* typical number of bytes in IP + UDP header for given socket */
static int sock_get_iph_udph_overhead(int fd)
{
	int rc = sock_get_domain(fd);
	if (rc < 0) {
		LOGP(DLINP, LOGL_ERROR, "Unable to determine domain of socket %d: %s\n",
		     fd, strerror(errno));
		goto assume_ipv4;
	}

	switch (rc) {
	case AF_INET6:
		return 40 + 8;
	case AF_INET:
		return 20 + 8;
	default:
		LOGP(DLINP, LOGL_ERROR, "Unknown domain %d of socket %d\n", rc, fd);
		break;
	}

assume_ipv4:
	return 20 + 8;
}

/***********************************************************************
 * transmit to remote peer
 ***********************************************************************/

/* transmit something to an octoi peer */
int octoi_tx(struct octoi_peer *peer, uint8_t msg_type, uint8_t flags,
	     const void *data, size_t len)
{
	struct e1oip_hdr hdr = {
		.version = E1OIP_VERSION,
		.flags = flags & 0xf,
		.msg_type = msg_type,
	};
	struct iovec iov[2] = {
		{
			.iov_base = (void *) &hdr,
			.iov_len = sizeof(hdr),
		}, {
			.iov_base = (void *) data,
			.iov_len = len,
		}
	};
	struct msghdr msgh = {
		.msg_name = &peer->remote,
		.msg_namelen = sizeof(peer->remote),
		.msg_iov = iov,
		.msg_iovlen = ARRAY_SIZE(iov),
		.msg_control = NULL,
		.msg_controllen = 0,
		.msg_flags = 0,
	};
	int rc;

	rc = sendmsg(peer->sock->ofd.fd, &msgh, 0);
	if (rc < 0)
		LOGPEER(peer, LOGL_ERROR, "Error in sendmsg: %s\n", strerror(errno));
	else if (rc != (int) (sizeof(hdr) + len))
		LOGPEER(peer, LOGL_ERROR, "Short write in  sendmsg: %d != %zu\n", rc, sizeof(hdr)+len);

	return rc;
}

static int _octoi_tx_echo(struct octoi_peer *peer, bool is_req, uint16_t seq_nr,
			  const uint8_t *data, size_t data_len)
{
	enum e1oip_msgtype msgt;
	struct {
		struct e1oip_echo echo;
		uint8_t buf[data_len];
	} u;

	u.echo.seq_nr = htons(seq_nr);
	if (data && data_len)
		memcpy(u.echo.data, data, data_len);

	if (is_req)
		msgt = E1OIP_MSGT_ECHO_REQ;
	else
		msgt = E1OIP_MSGT_ECHO_RESP;

	return octoi_tx(peer, msgt, 0, &u, sizeof(u));
}

int octoi_tx_echo_req(struct octoi_peer *peer, uint16_t seq_nr, const uint8_t *data, size_t data_len)
{
	LOGPEER(peer, LOGL_DEBUG, "Tx ECHO_REQ\n");
	return _octoi_tx_echo(peer, true, seq_nr, data, data_len);
}

int octoi_tx_echo_resp(struct octoi_peer *peer, uint16_t seq_nr, const uint8_t *data, size_t data_len)
{
	LOGPEER(peer, LOGL_DEBUG, "Tx ECHO_RESP\n");
	return _octoi_tx_echo(peer, false, seq_nr, data, data_len);
}

int octoi_tx_service_req(struct octoi_peer *peer, uint32_t service, const char *subscr_id,
			 const char *software_id, const char *software_version,
			 uint32_t capability_flags)
{
	struct e1oip_service_req service_req;

	memset(&service_req, 0, sizeof(service_req));
	service_req.requested_service = htonl(service);
	OSMO_STRLCPY_ARRAY(service_req.subscriber_id, subscr_id);
	OSMO_STRLCPY_ARRAY(service_req.software_id, software_id);
	OSMO_STRLCPY_ARRAY(service_req.software_version, software_version);
	service_req.capability_flags = htonl(capability_flags);

	LOGPEER(peer, LOGL_INFO, "Tx SERVICE_REQ\n");
	return octoi_tx(peer, E1OIP_MSGT_SERVICE_REQ, 0, &service_req, sizeof(service_req));
}

int octoi_tx_redir_cmd(struct octoi_peer *peer, const char *server_ip, uint16_t server_port)
{
	struct e1oip_redir_cmd redir;

	memset(&redir, 0, sizeof(redir));
	OSMO_STRLCPY_ARRAY(redir.server_ip, server_ip);
	redir.server_port = htons(server_port);

	LOGPEER(peer, LOGL_INFO, "Tx REDIR_CMD\n");
	return octoi_tx(peer, E1OIP_MSGT_REDIR_CMD, 0, &redir, sizeof(redir));
}

int octoi_tx_auth_req(struct octoi_peer *peer, uint8_t rand_len, const uint8_t *rand,
		      uint8_t autn_len, const uint8_t *autn)
{
	struct e1oip_auth_req areq;

	memset(&areq, 0, sizeof(areq));

	OSMO_ASSERT(rand_len <= sizeof(areq.rand));
	OSMO_ASSERT(autn_len <= sizeof(areq.autn));

	areq.rand_len = rand_len;
	memcpy(areq.rand, rand, rand_len);
	areq.autn_len = autn_len;
	memcpy(areq.autn, autn, autn_len);

	LOGPEER(peer, LOGL_INFO, "Tx AUTH_REQ\n");
	return octoi_tx(peer, E1OIP_MSGT_AUTH_REQ, 0, &areq, sizeof(areq));
}


int octoi_tx_auth_resp(struct octoi_peer *peer, uint8_t res_len, const uint8_t *res,
		      uint8_t auts_len, const uint8_t *auts)
{
	struct e1oip_auth_resp aresp;

	memset(&aresp, 0, sizeof(aresp));

	OSMO_ASSERT(res_len <= sizeof(aresp.res));
	OSMO_ASSERT(auts_len <= sizeof(aresp.auts));

	aresp.res_len = res_len;
	memcpy(aresp.res, res, res_len);
	aresp.auts_len = auts_len;
	memcpy(aresp.auts, auts, auts_len);

	LOGPEER(peer, LOGL_INFO, "Tx AUTH_RESP\n");
	return octoi_tx(peer, E1OIP_MSGT_AUTH_RESP, 0, &aresp, sizeof(aresp));
}

int octoi_tx_service_ack(struct octoi_peer *peer, uint32_t assigned_service,
			 const char *server_id, const char *software_id,
			 const char *software_version, uint32_t capability_flags)
{
	struct e1oip_service_ack service_ack;

	memset(&service_ack, 0, sizeof(service_ack));
	service_ack.assigned_service = htonl(assigned_service);
	OSMO_STRLCPY_ARRAY(service_ack.server_id, server_id);
	OSMO_STRLCPY_ARRAY(service_ack.software_id, software_id);
	OSMO_STRLCPY_ARRAY(service_ack.software_version, software_version);
	service_ack.capability_flags = htonl(capability_flags);

	LOGPEER(peer, LOGL_INFO, "Tx SERVICE_ACK\n");
	return octoi_tx(peer, E1OIP_MSGT_SERVICE_ACK, 0, &service_ack, sizeof(service_ack));
}

int octoi_tx_service_rej(struct octoi_peer *peer, uint32_t rejected_service, const char *message)
{
	struct e1oip_service_rej service_rej;

	memset(&service_rej, 0, sizeof(service_rej));
	service_rej.rejected_service = htonl(rejected_service);
	OSMO_STRLCPY_ARRAY(service_rej.reject_message, message);

	LOGPEER(peer, LOGL_INFO, "Tx SERVICE_REJ\n");
	return octoi_tx(peer, E1OIP_MSGT_SERVICE_REJ, 0, &service_rej, sizeof(service_rej));
}

int octoi_tx_error_ind(struct octoi_peer *peer, uint32_t cause, const char *message,
		       const uint8_t *orig, size_t orig_len)
{
	struct {
		struct e1oip_error_ind error_ind;
		uint8_t orig[orig_len];
	} u;

	u.error_ind.cause = htonl(cause);
	OSMO_STRLCPY_ARRAY(u.error_ind.error_message, message);
	memcpy(&u.orig, orig, orig_len);

	LOGPEER(peer, LOGL_INFO, "Tx ERROR_IND\n");
	return octoi_tx(peer, E1OIP_MSGT_ERROR_IND, 0, &u, sizeof(u));
}


/***********************************************************************
 * socket
 ***********************************************************************/

static int sockaddr_cmp(const struct sockaddr *x, const struct sockaddr *y)
{
	if (x->sa_family != y->sa_family)
		return -1;

	if (x->sa_family == AF_UNIX) {
		const struct sockaddr_un *xun = (void *)x, *yun = (void *)y;
		int r = strcmp(xun->sun_path, yun->sun_path);
		if (r != 0)
			return r;
	} else if (x->sa_family == AF_INET) {
		const struct sockaddr_in *xin = (void *)x, *yin = (void *)y;
		if (xin->sin_addr.s_addr != yin->sin_addr.s_addr)
			return -1;
		if (xin->sin_port != yin->sin_port)
			return -1;
	} else if (x->sa_family == AF_INET6) {
		const struct sockaddr_in6 *xin6 = (void *)x, *yin6 = (void *)y;
		int r = memcmp(xin6->sin6_addr.s6_addr, yin6->sin6_addr.s6_addr, sizeof(xin6->sin6_addr.s6_addr));
		if (r != 0)
			return r;
		if (xin6->sin6_port != yin6->sin6_port)
			return -1;
		if (xin6->sin6_flowinfo != yin6->sin6_flowinfo)
			return -1;
		if (xin6->sin6_scope_id != yin6->sin6_scope_id)
			return -1;
	} else {
		OSMO_ASSERT(0);
	}
	return 0;
}

static struct octoi_peer *find_peer_by_sockaddr(struct octoi_sock *sock, const struct sockaddr *sa)
{
	struct octoi_peer *peer;

	llist_for_each_entry(peer, &sock->peers, list) {
		if (!sockaddr_cmp(sa, (struct sockaddr *) &peer->remote))
			return peer;
	}
	return NULL;
}

static struct octoi_peer *
alloc_peer(struct octoi_sock *sock, const struct sockaddr *sa, socklen_t sa_len)
{
	struct octoi_peer *peer = talloc_zero(sock, struct octoi_peer);

	if (!peer)
		return NULL;

	OSMO_ASSERT(sa_len <= sizeof(peer->remote));
	memcpy(&peer->remote, sa, sa_len);

	peer->sock = sock;
	llist_add_tail(&peer->list, &sock->peers);

	return peer;
}

static int octoi_fd_cb(struct osmo_fd *ofd, unsigned int what)
{
	struct octoi_sock *sock = ofd->data;
	struct msgb *msg;
	struct sockaddr_storage ss_remote;
	socklen_t ss_remote_len = sizeof(ss_remote);
	struct octoi_peer *peer;
	int rc;

	if (what & OSMO_FD_WRITE) {
		LOGP(DLINP, LOGL_INFO, "non-blocking connect succeeded\n");
		osmo_fd_write_disable(ofd);
	}


	if (what & OSMO_FD_READ) {
		msg = msgb_alloc_c(sock, 2048, "OCTOI Rx");
		OSMO_ASSERT(msg);
		rc = recvfrom(ofd->fd, msgb_data(msg), msgb_tailroom(msg), 0,
				(struct sockaddr *) &ss_remote, &ss_remote_len);
		if (rc <= 0) {
			msgb_free(msg);
			return -1;
		}
		msgb_put(msg, rc);
		msg->l1h = msg->data;
		if (msgb_l1len(msg) < sizeof(struct e1oip_hdr)) {
			msgb_free(msg);
			return -2;
		}
		msg->l2h = msg->l1h + sizeof(struct e1oip_hdr);

		/* look-up octoi_peer based on remote address */
		peer = find_peer_by_sockaddr(sock, (struct sockaddr *) &ss_remote);
		if (!peer) {
			peer = alloc_peer(sock, (struct sockaddr *) &ss_remote, ss_remote_len);
			if (peer) {
				osmo_sockaddr_str_from_sockaddr(&peer->cfg.remote, &ss_remote);
				osmo_talloc_replace_string_fmt(peer, &peer->name, OSMO_SOCKADDR_STR_FMT,
								OSMO_SOCKADDR_STR_FMT_ARGS(&peer->cfg.remote));
				LOGPEER(peer, LOGL_INFO, "peer created\n");
			}
		}
		OSMO_ASSERT(peer);

		/* dispatch received message to peer */
		rc = sock->rx_cb(peer, msg);
		if (rc < 0)
			return rc;
	}

	return 0;
}

void octoi_peer_destroy(struct octoi_peer *peer)
{
	if (!peer)
		return;

	peer->tdm_permitted = false;
	peer->sock = NULL;
	e1oip_line_destroy(peer->iline);

	llist_del(&peer->list);
	talloc_free(peer);
}

static struct octoi_sock *octoi_sock_create(void *ctx)
{
	struct octoi_sock *sock = talloc_zero(ctx, struct octoi_sock);

	if (!sock)
		return NULL;

	INIT_LLIST_HEAD(&sock->peers);
	osmo_fd_setup(&sock->ofd, -1, OSMO_FD_READ, octoi_fd_cb, sock, 0);

	return sock;
}

struct octoi_sock *octoi_sock_create_server(void *ctx, void *priv, const struct osmo_sockaddr_str *local)
{
	struct octoi_sock *sock = octoi_sock_create(ctx);
	struct sockaddr_storage sa_local;
	int rc;

	OSMO_ASSERT(sock);

	sock->priv = priv;
	sock->cfg.server_mode = true;
	sock->cfg.local = *local;

	/* bind to local addr/port; don't connect to any remote as we have many */
	osmo_sockaddr_str_to_sockaddr(&sock->cfg.local, &sa_local);
	rc = osmo_sock_init_osa_ofd(&sock->ofd, SOCK_DGRAM, IPPROTO_UDP,
				    (struct osmo_sockaddr *) &sa_local, NULL,
				    OSMO_SOCK_F_BIND | OSMO_SOCK_F_NONBLOCK);

	if (rc < 0) {
		LOGP(DLINP, LOGL_ERROR, "Unable to create OCTOI server socket\n");
		talloc_free(sock);
		return NULL;
	}

	LOGP(DLINP, LOGL_NOTICE, "OCTOI server socket at "OSMO_SOCKADDR_STR_FMT"\n",
		OSMO_SOCKADDR_STR_FMT_ARGS(local));

	sock->iph_udph_size = sock_get_iph_udph_overhead(sock->ofd.fd);

	return sock;
}

struct octoi_sock *octoi_sock_create_client(void *ctx, void *priv, const struct osmo_sockaddr_str *local,
					    const struct osmo_sockaddr_str *remote)
{
	struct octoi_sock *sock = octoi_sock_create(ctx);
	struct sockaddr_storage sa_remote;
	struct octoi_peer *peer;
	int rc;

	OSMO_ASSERT(sock);

	sock->priv = priv;
	sock->cfg.server_mode = false;
	if (local)
		sock->cfg.local = *local;

	/* bind to local addr/port; don't connect to any remote as we have many */
	osmo_sockaddr_str_to_sockaddr(remote, &sa_remote);
	if (local) {
		struct sockaddr_storage sa_local;
		osmo_sockaddr_str_to_sockaddr(&sock->cfg.local, &sa_local);
		rc = osmo_sock_init_osa_ofd(&sock->ofd, SOCK_DGRAM, IPPROTO_UDP,
					    (struct osmo_sockaddr *) &sa_local,
					    (struct osmo_sockaddr *) &sa_remote,
					    OSMO_SOCK_F_CONNECT | OSMO_SOCK_F_BIND | OSMO_SOCK_F_NONBLOCK);
	} else {
		rc = osmo_sock_init_osa_ofd(&sock->ofd, SOCK_DGRAM, IPPROTO_UDP,
					    NULL, (struct osmo_sockaddr *) &sa_remote,
					    OSMO_SOCK_F_CONNECT | OSMO_SOCK_F_NONBLOCK);
	}
	if (rc < 0) {
		LOGP(DLINP, LOGL_ERROR, "Unable to create OCTOI client socket\n");
		talloc_free(sock);
		return NULL;
	}

	LOGP(DLINP, LOGL_NOTICE, "OCTOI client socket to "OSMO_SOCKADDR_STR_FMT"\n",
		OSMO_SOCKADDR_STR_FMT_ARGS(remote));

	sock->iph_udph_size = sock_get_iph_udph_overhead(sock->ofd.fd);

	/* create [the only] peer */
	peer = alloc_peer(sock, (struct sockaddr *) &sa_remote, sizeof(sa_remote));
	peer->cfg.remote = *remote;
	osmo_talloc_replace_string_fmt(peer, &peer->name, OSMO_SOCKADDR_STR_FMT,
					OSMO_SOCKADDR_STR_FMT_ARGS(remote));

	return sock;
}

int octoi_sock_set_dscp(struct octoi_sock *sock, uint8_t dscp)
{
	return osmo_sock_set_dscp(sock->ofd.fd, dscp);
}

int octoi_sock_set_priority(struct octoi_sock *sock, uint8_t priority)
{
	return osmo_sock_set_priority(sock->ofd.fd, priority);
}

void octoi_sock_destroy(struct octoi_sock *sock)
{
	struct octoi_peer *p1, *p2;

	if (!sock)
		return;

	llist_for_each_entry_safe(p1, p2, &sock->peers, list) {
		OSMO_ASSERT(p1->sock == sock);
		p1->sock = NULL;
		/* FIXME: destroy FSM / priv */
		llist_del(&p1->list);
		talloc_free(p1);
	}

	osmo_fd_unregister(&sock->ofd);
	close(sock->ofd.fd);

	LOGP(DLINP, LOGL_NOTICE, "OCTOI %s socket destroyed\n",
		sock->cfg.server_mode ? "server" : "client");

	talloc_free(sock);
}

/* return the (only) peer of a octoi_sock client */
struct octoi_peer *octoi_sock_client_get_peer(struct octoi_sock *sock)
{
	if (!sock)
		return NULL;
	OSMO_ASSERT(!sock->cfg.server_mode);
	OSMO_ASSERT(llist_count(&sock->peers) == 1);
	return llist_entry(sock->peers.next, struct octoi_peer, list);
}
