/*
 *   teamd_usock.c - Teamd unix socket api
 *   Copyright (C) 2012-2015 Jiri Pirko <jiri@resnulli.us>
 *
 *   This library is free software; you can redistribute it and/or
 *   modify it under the terms of the GNU Lesser General Public
 *   License as published by the Free Software Foundation; either
 *   version 2.1 of the License, or (at your option) any later version.
 *
 *   This library 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
 *   Lesser General Public License for more details.
 *
 *   You should have received a copy of the GNU Lesser General Public
 *   License along with this library; if not, write to the Free Software
 *   Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */

#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <errno.h>
#include <ctype.h>
#include <private/misc.h>
#include <private/list.h>
#include <team.h>

#include "teamd.h"
#include "teamd_usock.h"
#include "teamd_usock_common.h"
#include "teamd_ctl.h"

struct usock_ops_priv {
	char *rcv_msg_args;
	int sock;
};

struct usock_acc_conn {
	struct list_item list;
	int sock;
};

int __strdecode(char *str)
{
	char *cur;
	char *cur2;
	bool escaped = false;

	cur = str;
	while (*cur != '\0') {
		if (!escaped && *cur == '\\') {
			escaped = true;
		} else if (escaped) {
			escaped = false;
			switch (*cur) {
			case 'n':
				*(cur - 1) = '\n';
				break;
			case '\\':
				*(cur - 1) = '\\';
				break;
			default:
				return -EINVAL;
			}
			cur2 = cur;
			while (*cur2 != '\0') {
				*cur2 = *(cur2 + 1);
				cur2++;
			}
		}
		cur++;
	}
	return 0;
}

static int usock_op_get_args(void *ops_priv, const char *fmt, ...)
{
	va_list ap;
	struct usock_ops_priv *usock_ops_priv = ops_priv;
	char **pstr;
	char *str;
	char *rest = usock_ops_priv->rcv_msg_args;
	int err = 0;

	va_start(ap, fmt);
	while (*fmt) {
		switch (*fmt++) {
		case 's': /* string */
			pstr = va_arg(ap, char **);
			str = teamd_usock_msg_getline(&rest);
			if (!str) {
				teamd_log_err("Insufficient number of arguments in message.");
				err = -EINVAL;
				goto out;
			}
			err = __strdecode(str);
			if (err) {
				teamd_log_err("Corrupted argument in message.");
				goto out;
			}
			*pstr = str;
			break;
		default:
			teamd_log_err("Unknown argument type requested");
			err = -EINVAL;
			goto out;
		}
	}
out:
	va_end(ap);
	return err;
}

static void usock_send(struct usock_ops_priv *usock_ops_priv,
		       char *buf, size_t buflen)
{
	int ret;

	ret = send(usock_ops_priv->sock, buf, buflen, 0);
	if (ret == -1)
		teamd_log_warn("Usock send failed: %s", strerror(errno));
}

static int usock_op_reply_err(void *ops_priv, const char *err_code,
			      const char *err_msg)
{
	struct usock_ops_priv *usock_ops_priv = ops_priv;
	char *strbuf;
	int err;

	err = asprintf(&strbuf, "%s\n%s\n%s\n", TEAMD_USOCK_REPLY_ERR_PREFIX,
		       err_code, err_msg);
	if (err == -1)
		return -ENOMEM;
	usock_send(usock_ops_priv, strbuf, strlen(strbuf));
	free(strbuf);
	return 0;
}

static int usock_op_reply_succ(void *ops_priv, const char *msg)
{
	struct usock_ops_priv *usock_ops_priv = ops_priv;
	char *strbuf;
	int err;

	err = asprintf(&strbuf, "%s\n%s", TEAMD_USOCK_REPLY_SUCC_PREFIX,
		       msg ? msg : "");
	if (err == -1)
		return -ENOMEM;
	usock_send(usock_ops_priv, strbuf, strlen(strbuf));
	free(strbuf);
	return 0;
}

static const struct teamd_ctl_method_ops teamd_usock_ctl_method_ops = {
	.get_args = usock_op_get_args,
	.reply_err = usock_op_reply_err,
	.reply_succ = usock_op_reply_succ,
};

static int process_rcv_msg(struct teamd_context *ctx, int sock, char *rcv_msg)
{
	struct usock_ops_priv usock_ops_priv;
	char *str;
	char *rest = rcv_msg;

	str = teamd_usock_msg_getline(&rest);
	if (!str) {
		teamd_log_dbg(ctx, "usock: Incomplete message.");
		return 0;
	}
	if (strcmp(TEAMD_USOCK_REQUEST_PREFIX, str)) {
		teamd_log_dbg(ctx, "usock: Unsupported message type.");
		return 0;
	}

	str = teamd_usock_msg_getline(&rest);
	if (!str) {
		teamd_log_dbg(ctx, "usock: Incomplete message.");
		return 0;
	}
	if (!teamd_ctl_method_exists(str)) {
		teamd_log_dbg(ctx, "usock: Unknown method \"%s\".", str);
		return 0;
	}

	usock_ops_priv.sock = sock;
	usock_ops_priv.rcv_msg_args = rest;

	teamd_log_dbg(ctx, "usock: calling method \"%s\"", str);

	return teamd_ctl_method_call(ctx, str, &teamd_usock_ctl_method_ops,
				     &usock_ops_priv);
}

static void acc_conn_destroy(struct teamd_context *ctx,
			     struct usock_acc_conn *acc_conn);

static int callback_usock_acc_conn(struct teamd_context *ctx, int events,
				   void *priv)
{
	struct usock_acc_conn *acc_conn = priv;
	char *msg = NULL; /* gcc needs this initialized */
	int err;

	err = teamd_usock_recv_msg(acc_conn->sock, &msg);
	if (err == -EPIPE || err == -ECONNRESET) {
		acc_conn_destroy(ctx, acc_conn);
		return 0;
	} else if (err) {
		teamd_log_err("usock: Failed to receive data from connection.");
		return err;
	}
	err = process_rcv_msg(ctx, acc_conn->sock, msg);
	free(msg);
	return err;
}

#define USOCK_ACC_CONN_CB_NAME "usock_acc_conn"

static int acc_conn_create(struct teamd_context *ctx, int sock)
{
	struct usock_acc_conn *acc_conn;
	int err;

	acc_conn = myzalloc(sizeof(*acc_conn));
	if (!acc_conn) {
		teamd_log_err("usock: No memory to allocate new connection structure.");
		return -ENOMEM;
	}
	acc_conn->sock = sock;
	err = teamd_loop_callback_fd_add(ctx, USOCK_ACC_CONN_CB_NAME, acc_conn,
					 callback_usock_acc_conn,
					 acc_conn->sock,
					 TEAMD_LOOP_FD_EVENT_READ);
	if (err)
		goto free_acc_conn;
	teamd_loop_callback_enable(ctx, USOCK_ACC_CONN_CB_NAME, acc_conn);
	list_add(&ctx->usock.acc_conn_list, &acc_conn->list);
	return 0;

free_acc_conn:
	free(acc_conn);
	return err;
}

static void acc_conn_destroy(struct teamd_context *ctx,
			     struct usock_acc_conn *acc_conn)
{

	teamd_loop_callback_del(ctx, USOCK_ACC_CONN_CB_NAME, acc_conn);
	close(acc_conn->sock);
	list_del(&acc_conn->list);
	free(acc_conn);
}

static void acc_conn_destroy_all(struct teamd_context *ctx)
{
	struct usock_acc_conn *acc_conn;
	struct usock_acc_conn *tmp;

	list_for_each_node_entry_safe(acc_conn, tmp,
				      &ctx->usock.acc_conn_list, list)
		acc_conn_destroy(ctx, acc_conn);
}

static int callback_usock(struct teamd_context *ctx, int events, void *priv)
{
	struct sockaddr_un addr;
	socklen_t alen;
	int sock;
	int err;

	alen = sizeof(addr);
	sock = accept(ctx->usock.sock, &addr, &alen);
	if (sock == -1) {
		teamd_log_err("usock: Failed to accept connection.");
		return -errno;
	}
	err = acc_conn_create(ctx, sock);
	if (err) {
		close(sock);
		return err;
	}
	return 0;
}

#define USOCK_MAX_CLIENT_COUNT 10

static int teamd_usock_sock_open(struct teamd_context *ctx)
{
	struct sockaddr_un addr;
	int sock;
	int err;

	sock = socket(AF_UNIX, SOCK_SEQPACKET, 0);
	if (sock == -1) {
		teamd_log_err("usock: Failed to create socket.");
		return -errno;
	}

	err = fchmod(sock, 0700);
	if (err == -1) {
		teamd_log_err("usock: Failed to change socket permissions.");
		err = -errno;
		goto close_sock;
	}

	addr.sun_family = AF_UNIX;
	teamd_usock_get_sockpath(addr.sun_path, sizeof(addr.sun_path),
				 ctx->team_devname);

	teamd_log_dbg(ctx, "usock: Using sockpath \"%s\"", addr.sun_path);
	err = unlink(addr.sun_path);
	if (err == -1 && errno != ENOENT) {
		teamd_log_err("usock: Failed to remove socket file.");
		err = -errno;
		goto close_sock;
	}

	err = bind(sock, (struct sockaddr *) &addr,
	           strlen(addr.sun_path) + sizeof(addr.sun_family));
	if (err == -1) {
		teamd_log_err("usock: Failed to bind socket.");
		err = -errno;
		goto close_sock;
	}
	listen(sock, USOCK_MAX_CLIENT_COUNT);

	ctx->usock.sock = sock;
	ctx->usock.addr = addr;
	return 0;

close_sock:
	close(sock);
	return err;
}

static void teamd_usock_sock_close(struct teamd_context *ctx)
{
	close(ctx->usock.sock);
	unlink(ctx->usock.addr.sun_path);
}

#define USOCK_CB_NAME "usock"

int teamd_usock_init(struct teamd_context *ctx)
{
	int err;

	if (!ctx->usock.enabled)
		return 0;
	list_init(&ctx->usock.acc_conn_list);
	err = teamd_usock_sock_open(ctx);
	if (err)
		return err;
	err = teamd_loop_callback_fd_add(ctx, USOCK_CB_NAME, ctx,
					 callback_usock, ctx->usock.sock,
					 TEAMD_LOOP_FD_EVENT_READ);
	if (err)
		goto sock_close;
	teamd_loop_callback_enable(ctx, USOCK_CB_NAME, ctx);
	return 0;
sock_close:
	teamd_usock_sock_close(ctx);
	return err;
}

void teamd_usock_fini(struct teamd_context *ctx)
{
	if (!ctx->usock.enabled)
		return;
	acc_conn_destroy_all(ctx);
	teamd_loop_callback_del(ctx, USOCK_CB_NAME, ctx);
	teamd_usock_sock_close(ctx);
}
