/* Copyright (C) 2009 Intel Corporation 
   Author: Andi Kleen
   Simple event-driven unix network server for client access.
   Process commands and buffer output.

   mcelog 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; version
   2.

   mcelog 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 find a copy of v2 of the GNU General Public License somewhere
   on your Linux system; if not, write to the Free Software Foundation, 
   Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */

#define _GNU_SOURCE 1
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>
#include <assert.h>
#include <ctype.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
#include <sys/un.h>
#include <signal.h>
#include <setjmp.h>
#include "mcelog.h"
#include "server.h"
#include "eventloop.h"
#include "config.h"
#include "memdb.h"
#include "memutil.h"
#include "paths.h"
#include "page.h"

#define PAIR(x) x, sizeof(x)-1

struct clientcon { 
	char *inbuf;	/* 0 terminated */
	char *inptr;
	char *outbuf;
	size_t outcur;
	size_t outlen;
};

static char *client_path = SOCKET_PATH;
static int initial_ping_timeout = 2;
static int listen_backlog = 10;
static struct config_cred acc = { .uid = 0, .gid = -1U };

static void free_outbuf(struct clientcon *cc)
{
	free(cc->outbuf);
	cc->outbuf = NULL;
	cc->outcur = cc->outlen = 0;
}

static void free_inbuf(struct clientcon *cc)
{
	free(cc->inbuf);
	cc->inbuf = NULL;
	cc->inptr = NULL;
}

static void free_cc(struct clientcon *cc)
{
	free(cc->outbuf);
	cc->outbuf = NULL;
	free(cc->inbuf);
	cc->inbuf = NULL;
	free(cc);
	cc = NULL;
}

static void sendstring(int fd, char *str)
{
	(void)send(fd, str, strlen(str), MSG_DONTWAIT|MSG_NOSIGNAL);
}

static void dispatch_dump(FILE *fh, char *s)
{
	char *p;
	enum printflags printflags = 0;

	while ((p = strsep(&s, " ")) != NULL) {
		if (!strcmp(p, "dump"))
			;
		else if (!strcmp(p, "bios"))
			printflags |= DUMP_BIOS;
		else if (!strcmp(p, "all"))
			printflags |= DUMP_ALL;
		else 
			fprintf(fh, "Unknown dump parameter\n");
	}			

	dump_memory_errors(fh, printflags);
	fprintf(fh, "done\n");
}

static void dispatch_pages(FILE *fh)
{
	dump_page_errors(fh);
	fprintf(fh, "done\n");
}

static void dispatch_commands(char *line, FILE *fh)
{
	char *s;
	while ((s = strsep(&line, "\n")) != NULL) { 
		while (isspace(*s))
			line++;
		if (!strncmp(s, "dump", 4))
			dispatch_dump(fh, s);
		else if (!strncmp(s, "pages", 5))
			dispatch_pages(fh);
		else if (!strcmp(s, "ping"))
			fprintf(fh, "pong\n");
		else if (*s != 0)
			fprintf(fh, "Unknown command\n");
	}
}

/* assumes commands don't cross records */
static void process_cmd(struct clientcon *cc)
{
	FILE *fh;

	assert(cc->outbuf == NULL);
	fh = open_memstream(&cc->outbuf, &cc->outlen);
	if (!fh)
		Enomem();
	cc->outcur = 0;
	dispatch_commands(cc->inbuf, fh);
	if (ferror(fh) || fclose(fh) != 0)
		Enomem();
}

/* check if client is allowed to access */
static int access_check(int fd, struct msghdr *msg)
{
	struct cmsghdr *cmsg;	
	struct ucred *uc;

	/* check credentials */
	cmsg = CMSG_FIRSTHDR(msg);
	if (cmsg == NULL || 
		cmsg->cmsg_level != SOL_SOCKET ||
		cmsg->cmsg_type != SCM_CREDENTIALS) { 
		Eprintf("Did not receive credentials over client unix socket %p\n",
			cmsg);
		return -1;
	}
	uc = (struct ucred *)CMSG_DATA(cmsg);
	if (uc->uid == 0 || 
		(acc.uid != -1U && uc->uid == acc.uid) ||
		(acc.gid != -1U && uc->gid == acc.gid))
		return 0;
	Eprintf("rejected client access from pid:%u uid:%u gid:%u\n",
		uc->pid, uc->uid, uc->gid);
	sendstring(fd, "permission denied\n");
	return -1;
}

/* retrieve commands from client */
static int client_input(int fd, struct clientcon *cc)
{
	char ctlbuf[CMSG_SPACE(sizeof(struct ucred))];
	struct iovec miov;
	struct msghdr msg = {
		.msg_iov = &miov,
		.msg_iovlen = 1,
		.msg_control = ctlbuf,
		.msg_controllen = sizeof(ctlbuf),
	}; 	
	int n, n2;

	assert(cc->inbuf == NULL);
	if (ioctl(fd, FIONREAD, &n) < 0)
		return -1;
	if (n == 0)
		return 0;

	cc->inbuf = xalloc_nonzero(n + 1);
	cc->inbuf[n] = 0;
	cc->inptr = cc->inbuf;

	miov.iov_base = cc->inbuf;
	miov.iov_len = n;
	n2 = recvmsg(fd, &msg, 0);
	if (n2 < n)
		return -1;

	return access_check(fd, &msg) == 0 ? n : -1;
}

/* process input/out on client socket */
static void client_event(struct pollfd *pfd, void *data)
{
	int events = pfd->revents;
	struct clientcon *cc = (struct clientcon *)data;
	int n;

	if (events & ~(POLLIN|POLLOUT)) /* error/close */
		goto error;

	if (events & POLLOUT) {
		if (cc->outcur < cc->outlen) {
			n = send(pfd->fd, cc->outbuf + cc->outcur, 
				 cc->outlen - cc->outcur, 
				 MSG_DONTWAIT|MSG_NOSIGNAL);
			if (n < 0) {
				/* EAGAIN here? but should not happen */
				goto error;
			}
			cc->outcur += n;
		}
		if (cc->outcur == cc->outlen)
			free_outbuf(cc);
	}
	if (events & POLLIN) {
		n = client_input(pfd->fd, cc);
		if (n < 0)
			goto error;
		process_cmd(cc);
		free_inbuf(cc);
	}
	pfd->events = cc->outbuf ? POLLOUT : POLLIN;
	return;

error:
	if (pfd->revents & POLLERR)
		SYSERRprintf("error while reading from client");
	close(pfd->fd);
	unregister_pollcb(pfd);
	free_cc(cc);
}

/* accept a new client */
static void client_accept(struct pollfd *pfd, void *data)
{
	struct clientcon *cc = NULL;
	int nfd = accept(pfd->fd, NULL, 0);	
	int on;

	if (nfd < 0) {
		SYSERRprintf("accept failed on client socket");
		return;
	}

	on = 1;
	if (setsockopt(nfd, SOL_SOCKET, SO_PASSCRED, &on, sizeof(on)) < 0) {
		SYSERRprintf("Cannot enable credentials passing on client socket");
		goto cleanup;
	}

	cc = xalloc(sizeof(struct clientcon));
	if (register_pollcb(nfd, POLLIN, client_event, cc) < 0) {
		sendstring(nfd, "mcelog server too busy\n");
		goto cleanup;
	}
	return;

cleanup:
	free(cc);
	cc = NULL;
	close(nfd);
}

static void server_config(void)
{
	char *s;
	long v;
	int l;

	config_cred("server", "client", &acc);
	if ((s = config_string("server", "socket-path")) != NULL)
		client_path = s;
	if (config_number("server", "initial-ping-timeout", "%u", &v) == 0)
		initial_ping_timeout = v;
	if (config_number("server", "listen-backlog", "%u", &l) == 0)
		listen_backlog = l;
}

static sigjmp_buf ping_timeout_ctx;

static void ping_timeout(int sig)
{
	siglongjmp(ping_timeout_ctx, 1);
}

/* server still running? */
static int server_ping(struct sockaddr_un *un)
{
	struct sigaction oldsa;
	struct sigaction sa = { .sa_handler = ping_timeout };
	int ret, n;
	char buf[10];
	int fd = socket(PF_UNIX, SOCK_STREAM, 0);
	if (fd < 0)
		return 0;

	sigaction(SIGALRM, &sa, &oldsa);
	if (sigsetjmp(ping_timeout_ctx, 1) == 0) {
		ret = -1;
		alarm(initial_ping_timeout);
		if (connect(fd, (const struct sockaddr *)un, sizeof(struct sockaddr_un)) < 0)
			goto cleanup;
		if (write(fd, PAIR("ping\n")) < 0)
			goto cleanup;
		if ((n = read(fd, buf, 10)) < 0)
			goto cleanup;
		if (n == 5 && !memcmp(buf, "pong\n", 5))
			ret = 0;
	} else
		ret = -1;
cleanup:
	sigaction(SIGALRM, &oldsa, NULL);
	alarm(0);
	close(fd);
	return ret;
}

void server_setup(void)
{
	int fd;
	struct sockaddr_un adr; 
	int on;
	int ret_listen;

	server_config();

	if (client_path[0] == 0)
		return;

	if (strlen(client_path) >= sizeof(adr.sun_path) - 1) {
		Eprintf("Client socket path `%s' too long for unix socket",
				client_path);
		return;
	}

	memset(&adr, 0, sizeof(struct sockaddr_un));
	adr.sun_family = AF_UNIX;
	strncpy(adr.sun_path, client_path, sizeof(adr.sun_path) - 1);

	if (access(client_path, F_OK) == 0) {
		if (server_ping(&adr) == 0) {
			Eprintf("mcelog server already running\n");
			exit(1);
		}
		unlink(client_path);
	}

	fd = socket(PF_UNIX, SOCK_STREAM, 0);
	if (fd < 0) {
		SYSERRprintf("cannot open listening socket");
		return;
	}

	if (bind(fd, (struct sockaddr *)&adr, sizeof(struct sockaddr_un)) < 0) {
		SYSERRprintf("Cannot bind to client unix socket `%s'",
			client_path);
		goto cleanup;
	}


	ret_listen = listen(fd, listen_backlog);
	if (ret_listen < 0) {
		SYSERRprintf("cannot listen for connections on client unix socket");
		goto cleanup;
	}
	/* Set SO_PASSCRED to avoid race with client connecting too fast */
	/* Ignore error for old kernels */
	on = 1;
	setsockopt(fd, SOL_SOCKET, SO_PASSCRED, &on, sizeof(on));

	register_pollcb(fd, POLLIN, client_accept, NULL);
	return;

cleanup:
	close(fd);
	exit(1);
}
