/*
 * (C) Copyright Fuji Xerox Co., Ltd. All rights are reserved.
 * 
 * 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.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 *
 * 2018/05/06	nobuo.iwata@fujixerox.co.jp
 */

#include "fxlsm_event.h"
#include "fxlsm_list.h"
#include "fxlsm_ops.h"
#include "fxlsm_prof.h"
#include "fxlsm_log.h"
#include <linux/slab.h>
#include <linux/mm.h>
#include <linux/spinlock.h>
#include <linux/uaccess.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/poll.h>

const char *fxlsm_e_start = "START";
const char *fxlsm_e_stop = "STOP";
const char *fxlsm_e_enabled = "ENABLED";
const char *fxlsm_e_through = "THROUGH";
const char *fxlsm_e_read = "READ";
const char *fxlsm_e_write = "WRITE";
const char *fxlsm_e_exec = "EXEC";
const char *fxlsm_e_creat = "CREAT";
const char *fxlsm_e_creatx = "CREATX";
const char *fxlsm_e_delete = "DELETE";
const char *fxlsm_e_chmod = "CHMOD";
const char *fxlsm_e_chmodx = "CHMODX";
const char *fxlsm_e_chown = "CHOWN";
const char *fxlsm_e_chroot = "CHROOT";
const char *fxlsm_e_truncate = "TRUNC";
const char *fxlsm_e_bind = "BIND";
const char *fxlsm_e_connect = "CONNECT";
const char *fxlsm_e_listen = "LISTEN";
const char *fxlsm_e_accept = "ACCEPT";
const char *fxlsm_e_sendmsg = "SENDMSG";
const char *fxlsm_e_recvmsg = "RECVMSG";
const char *fxlsm_e_graft = "GRAFT";
const char *fxlsm_e_unknown = "?";

struct fxlsm_event {
	struct list_head node;
	int len;
	char buf[0];
};

static LIST_HEAD(events);
static DEFINE_SPINLOCK(lock);
static DECLARE_WAIT_QUEUE_HEAD(wait);
static DECLARE_WAIT_QUEUE_HEAD(poll);
static unsigned long count;
static unsigned long flags;
#define FXLSM_EVENT_BIT_STOPPED 0

static struct fxlsm_event *delete_one(void)
{
	struct fxlsm_event *e;

again:
	spin_lock(&lock);
	if (list_empty(&events)) {
		spin_unlock(&lock);
		if (wait_event_interruptible(wait, count))
			return ERR_PTR(-ERESTARTSYS);
		goto again;
	}
	e = list_entry(events.next, struct fxlsm_event, node);
	list_del(events.next);
	if (count > 0)
		--count;
	spin_unlock(&lock);
	return e;
}

static void clear(void)
{
	struct fxlsm_event *pos, *n;

	spin_lock(&lock);
	list_for_each_entry_safe(pos, n, &events, node) {
		list_del(&pos->node);
		kfree(pos);
	}
	spin_unlock(&lock);
}

static void set_buf(char *buf, int *off, const char *val, int len, int last)
{
	char *p = buf + *off;

	if (len > 0)
		strncpy(p, val, len);
	if (last)
		*(p + len) = '\n';
	else
		*(p + len) = '\t';
	*(p + len + 1) = 0;
	*off += len + 1;
}

static inline size_t event_size(int len)
{
	return sizeof(struct fxlsm_event) + len;
}

static atomic_t max_events = ATOMIC_INIT(0);
static atomic_t lost_events = ATOMIC_INIT(0);

int fxlsm_get_max_events(void)
{
	return atomic_read(&max_events);
}

void fxlsm_set_max_events(int max)
{
	atomic_set(&max_events, max);
}

int fxlsm_get_lost_events(void)
{
	return atomic_read(&max_events);
}

void fxlsm_notify(const char *event, const char *comm, const char *path,
		int plen, const char *src)
{
	int __max_events;
#define NUM_FIELDS 6
	int tlen, elen, clen, ulen, slen;
	int len, off;
	size_t sz;
	struct fxlsm_event *e;
	struct timeval tv;
#define ULONG_DIGITS 21
	char time[ULONG_DIGITS];
	char user[ULONG_DIGITS];

	if (test_bit(FXLSM_EVENT_BIT_STOPPED, &flags))
		return;

	__max_events = fxlsm_get_max_events();
	if (__max_events >=0 && count >= __max_events) {
		atomic_inc(&lost_events);
		return;
	}
	do_gettimeofday(&tv);
	tlen = snprintf(time, ULONG_DIGITS, "%lu", tv.tv_sec);
	tlen = (tlen >= sizeof(time)) ? sizeof(time) - 1 : tlen;
	ulen = snprintf(user, ULONG_DIGITS, "%u", current->real_cred->euid.val);
	ulen = (ulen >= sizeof(user)) ? sizeof(user) - 1 : ulen;
	plen = path ? ((!plen) ? strlen(path) : plen) : 0;
	elen = event ? strlen(event) : 0;
	clen = comm ? strlen(comm) : 0;
	slen = src ? strlen(src) : 0;
	len = tlen + plen + elen + clen + ulen + slen + NUM_FIELDS + 1;
	sz = event_size(len);
	e = (struct fxlsm_event *)kmalloc(sz, GFP_KERNEL);
	if (!e) {
		ERR("alloc event %zu", sz);
		goto out;
	}
	FXLSM_PROF_MEM_ADD(sz, FXLSM_PROF_MEM_ID_EVENTS);
	e->len = len;
	off = 0;
	set_buf(e->buf, &off, time, tlen, 0);
	set_buf(e->buf, &off, path, plen, 0);
	set_buf(e->buf, &off, event, elen, 0);
	set_buf(e->buf, &off, comm, clen, 0);
	set_buf(e->buf, &off, user, ulen, 0);
	set_buf(e->buf, &off, src, slen, 1);
	spin_lock(&lock);
	list_add_tail(&e->node, &events);
	++count;
	spin_unlock(&lock);
	wake_up(&wait);
	wake_up(&poll);
out:
	return;
}

#define ENABLED_THROUGH_BUFLEN ((FXLSM_NUM_ACCS * 2) + 3)

static inline void fill_enabled_through_buf(char *buf)
{
	snprintf(buf, ENABLED_THROUGH_BUFLEN, "%s(%s)",
		fxlsm_get_enabled_string(),
		fxlsm_get_through_string());
}

void fxlsm_notify_started(void)
{
	char buf[ENABLED_THROUGH_BUFLEN];

	fill_enabled_through_buf(buf);
	fxlsm_notify_none(fxlsm_e_start, "void", buf);
}

void fxlsm_notify_stopped(void)
{
	char buf[ENABLED_THROUGH_BUFLEN];

	fill_enabled_through_buf(buf);
	fxlsm_notify_none(fxlsm_e_stop, "void", buf);
}

#define ENABLED_BUFLEN ((FXLSM_NUM_ACCS * 2) + 3)

void fxlsm_notify_enabled(int prev, int val)
{
	char buf[ENABLED_BUFLEN];
	char __prev[FXLSM_NUM_ACCS + 1];
	char __val[FXLSM_NUM_ACCS + 1];

	fxlsm_acc2s(prev, __prev);
	fxlsm_acc2s(val, __val);
	snprintf(buf, ENABLED_BUFLEN, "%s->%s", __prev, __val);
	fxlsm_notify_none(fxlsm_e_enabled, "void", buf);
}

#define THROUGH_BUFLEN ((FXLSM_NUM_ACCS * 2) + 5)

void fxlsm_notify_through(int prev, int val)
{
	char buf[THROUGH_BUFLEN];
	char __prev[FXLSM_NUM_ACCS + 1];
	char __val[FXLSM_NUM_ACCS + 1];

	fxlsm_acc2s(prev, __prev);
	fxlsm_acc2s(val, __val);
	snprintf(buf, THROUGH_BUFLEN, "(%s->%s)", __prev, __val);
	fxlsm_notify_none(fxlsm_e_through, "void", buf);
}

static ssize_t fxlsm_read(
		struct file *file, char *buf, size_t blen, loff_t *off)
{
	struct fxlsm_event *e;
	int len;

	*off = 0;
	e = delete_one();
	if (IS_ERR(e))
		return PTR_ERR(e);

	/*
	 * if trancated, not null terminated.
	 */
	len = (blen < e->len) ? blen : e->len;
	len -= copy_to_user(buf, e->buf, len);
	*off += len;
	FXLSM_PROF_MEM_SUB(event_size(e->len), FXLSM_PROF_MEM_ID_EVENTS);
	kfree(e);
	return(len);
}

static int fxlsm_open(struct inode *inode, struct file *file)
{
	return(0);
}

static int fxlsm_release(struct inode *inode, struct file *file)
{
	return(0);
}

static unsigned int fxlsm_poll(struct file *file, poll_table *tab)
{
	 unsigned int mask = 0;

	poll_wait(file, &poll, tab);
	spin_lock(&lock);
	if (!list_empty(&events)) {
#if LINUX_VERSION_CODE >= KERNEL_VERSION(4,12,0)
		mask |= (EPOLLIN | EPOLLRDNORM);
#else
		mask |= (POLLIN | POLLRDNORM);
#endif
	}
	spin_unlock(&lock);
	return mask;
}

static struct file_operations fxlsm_fops = {
	.owner = THIS_MODULE,
	.read = fxlsm_read,
	.open = fxlsm_open,
	.release = fxlsm_release,
	.poll = fxlsm_poll,
};
static dev_t fxlsm_dev;
static struct cdev fxlsm_cdev;
static struct class *fxlsm_class;

int fxlsm_event_init(void)
{
	int ret;
	struct device *d;

	ret = alloc_chrdev_region(&fxlsm_dev, 0, 1, "fxlsm");
	if (ret < 0)
		goto err_out;

	cdev_init(&fxlsm_cdev, &fxlsm_fops);
	fxlsm_cdev.owner = fxlsm_fops.owner;
	ret = cdev_add(&fxlsm_cdev, fxlsm_dev, 1);
	if (ret < 0) {
		kobject_put(&fxlsm_cdev.kobj);
		goto err_unregister;
	}
	fxlsm_class = class_create(THIS_MODULE, "fxlsm");
	if (IS_ERR(fxlsm_class)) {
		ret = PTR_ERR(fxlsm_class);
		goto err_cdev_del;
	}
	d = device_create(fxlsm_class, NULL, fxlsm_dev, NULL,
			"fxlsm");
	if (IS_ERR(d)) {
		ret = PTR_ERR(d);
		goto err_class_destroy;
	}
	clear_bit(FXLSM_EVENT_BIT_STOPPED, &flags);
	return 0;
err_class_destroy:
	class_destroy(fxlsm_class);
err_cdev_del:
	cdev_del(&fxlsm_cdev);
err_unregister:
	unregister_chrdev_region(fxlsm_dev, 1);
err_out:
	return ret;
}

void fxlsm_event_exit(void)
{
	set_bit(FXLSM_EVENT_BIT_STOPPED, &flags);
	clear();
	device_destroy(fxlsm_class, fxlsm_dev);
	class_destroy(fxlsm_class);
	cdev_del(&fxlsm_cdev);
	unregister_chrdev_region(fxlsm_dev, 1);
}
