/*
 * 2007+ Copyright (c) Evgeniy Polyakov <johnpol@2ka.mipt.ru>
 * All rights 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.
 */

#define _GNU_SOURCE
#define __USE_FILE_OFFSET64
#define __USE_LARGEFILE64
#define _FILE_OFFSET_BITS	64

#include <sys/types.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <sys/time.h>
#include <sys/xattr.h>

#include <fcntl.h>
#include <netdb.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <dirent.h>
#include <pthread.h>
#include <poll.h>
#include <signal.h>

#include <netinet/in.h>
#include <netinet/tcp.h>

#include "fserver.h"
#include "coherency.h"

static int fserver_init_socket(int family, char *addr, char *port)
{
	int s, err;
	struct addrinfo *ai, hint;

	s = socket(family, SOCK_STREAM, IPPROTO_TCP);
	if (s < 0) {
		ulog_err("Failed to create TCP socket");
		return -1;
	}

	err = 1;
	setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &err, 4);

	memset(&hint, 0, sizeof(struct addrinfo));

	hint.ai_flags = AI_NUMERICSERV;
	hint.ai_family = family;
	hint.ai_socktype = SOCK_STREAM;
	hint.ai_protocol = IPPROTO_TCP;

	err = getaddrinfo(addr, port, &hint, &ai);
	if (err) {
		ulog_err("Failed to get address info for %s:%s, err: %d", addr, port, err);
		goto err_out_close;
	}

	err = bind(s, ai->ai_addr, ai->ai_addrlen);
	if (err) {
		ulog_err("Failed to bind to %s:%s", addr, port);
		goto err_out_free;
	}

	err = listen(s, 1024);
	if (err) {
		ulog_err("Failed to listen at %s:%s", addr, port);
		goto err_out_free;
	}

	freeaddrinfo(ai);

	uloga("Server is now listening at %s:%s.\n", addr, port);

	return s;

err_out_free:
	freeaddrinfo(ai);
err_out_close:
	close(s);
	return -1;

}

static int fserver_read_key(char *key_file, void *key, unsigned int *size)
{
	int fd, err;

	fd = open(key_file, O_RDONLY);
	if (fd == -1) {
		ulog_err("%s: failed to open key file '%s'",
				__func__, key_file);
		return -1;
	}

	err = read(fd, key, *size);
	if (err <= 0) {
		ulog_err("%s: failed to read key from file '%s'",
				__func__, key_file);
		close(fd);
		return -1;
	}

	*size = err;

	close(fd);

	return 0;
}

static int fserver_crypto_init(struct fserver_crypto_engine *e, char *cipher_file, char *hash_file)
{
	int err;

	if (cipher_file) {
		e->cipher_keysize = sizeof(e->cipher_key);
		err = fserver_read_key(cipher_file, &e->cipher_key, &e->cipher_keysize);
		if (err)
			return err;
	}

	if (hash_file) {
		e->hash_keysize = sizeof(e->hash_key);
		err = fserver_read_key(hash_file, &e->hash_key, &e->hash_keysize);
		if (err)
			return err;
	}

	return 0;
}

static struct fserver *fserver_init(int family, char *addr, char *port,
		char *root, int workers,
		char *cipher_file, char *hash_file)
{
	struct fserver *f;
	int err;
	
	if (workers < 1) {
		uloga("Wrong number of workers.\n");
		return NULL;
	}

	f = malloc(sizeof(struct fserver));
	if (!f)
		return NULL;

	memset(f, 0, sizeof(struct fserver));

	f->workers_per_client = workers;

	pthread_mutex_init(&f->client_lock, NULL);
	INIT_LIST_HEAD(&f->client_list);

	f->coherency_root = RB_ROOT;
	pthread_mutex_init(&f->coherency_lock, NULL);

	err = fserver_crypto_init(&f->eng, cipher_file, hash_file);
	if (err)
		goto err_out_free;

	err = chroot(root);
	if (err) {
		ulog_err("Failed to change root directory to '%s'", root);
		goto err_out_free;
	}

	f->notify_root = fserver_notify_init(f);
	if (!f->notify_root)
		goto err_out_free;

	f->dirfd = open("/", O_RDONLY | O_DIRECTORY);
	if (f->dirfd < 0) {
		ulog_err("Failed to get directory fd for %s", root);
		goto err_out_notify_exit;
	}
	fchdir(f->dirfd);

	f->s = fserver_init_socket(family, addr, port);
	if (f->s < 0)
		goto err_out_closedir;

	return f;

err_out_closedir:
	close(f->dirfd);
err_out_notify_exit:
	fserver_notify_exit(f->notify_root);
err_out_free:
	free(f);
	return NULL;
}

static int fserver_send_data(struct fserver_client *c, void *buf, unsigned int size)
{
	size_t err;

	while (size) {
		err = send(c->cs, buf, size, 0);
		if (err <= 0) {
			if (err == 0)
				err = -ECONNRESET;
			ulog_err("%s: size: %u, err: %d", __func__, size, err);
			return err;
		}

		size -= err;
		buf += err;
	}

	return 0;
}

static int fserver_recv_data(struct fserver_client *c, void *buf, unsigned int size)
{
	size_t err;

	while (size) {
		err = recv(c->cs, buf, size, 0);
		if (err <= 0) {
			if (err == 0)
				err = -ECONNRESET;
			ulog_err("%s: size: %u, err: %d", __func__, size, err);
			return err;
		}

		size -= err;
		buf += err;
	}

	return 0;
}

/*
 * Not sure ERR_PTR magic is applicable in userspace.
 */
static void *fserver_get_client_data(struct fserver_client *c, unsigned long id, int *error)
{
	struct pollfd pfd;
	int err;
	unsigned int alloc_size, recv_size;
	void *data;
	struct netfs_cmd cmd;

	pfd.fd = c->cs;
	pfd.events = POLLIN | POLLERR | POLLHUP;
	pfd.revents = 0;

	err = poll(&pfd, 1, -1);
	if (err < 0) {
		ulog_err("Failed to poll client %s:%d", fserver_addr_convert(c), fserver_port_convert(c));
		goto err_out_exit;
	}

	if (err == 0)
		goto err_out_exit;

	pthread_mutex_lock(&c->lock);

	pfd.events = POLLIN | POLLERR | POLLHUP;
	pfd.revents = 0;

	err = poll(&pfd, 1, 0);
	if (err < 0) {
		ulog_err("Failed to poll client %s:%d", fserver_addr_convert(c), fserver_port_convert(c));
		goto err_out_unlock;
	}

	if (err == 0)
		goto err_out_unlock;

	err = fserver_recv_data(c, &cmd, sizeof(struct netfs_cmd));
	if (err)
		goto err_out_unlock;

	netfs_convert_cmd(&cmd);
	ulog("\n");
	ulog("%s: thread: %lu, cmd: %u, id: %llu, start: %llu, size: %u, ext: %u, csize: %u.\n",
		__func__, id, cmd.cmd, cmd.id, cmd.start, cmd.size, cmd.ext, cmd.csize);

	err = -EINVAL;
	if (cmd.cmd >= NETFS_CMD_MAX)
		goto err_out_unlock;

	alloc_size = recv_size = cmd.size;
	if (cmd.cmd == NETFS_READ_PAGE)
		recv_size = cmd.ext;
	if (cmd.cmd == NETFS_READ_PAGES)
		recv_size = cmd.ext;

	err = -ENOMEM;
	data = malloc(sizeof(struct netfs_cmd) + alloc_size);
	if (!data) {
		ulog_err("%s: thread: %lu, failed to allocate data cmd: %u, id: %llu, start: %llu, size: %u, ext: %u.\n",
			__func__, id, cmd.cmd, cmd.id, cmd.start, cmd.size, cmd.ext);
		goto err_out_unlock;
	}

	err = fserver_recv_data(c, data + sizeof(struct netfs_cmd), recv_size);
	if (err)
		goto err_out_unlock;

	pthread_mutex_unlock(&c->lock);

	memcpy(data, &cmd, sizeof(struct netfs_cmd));

	*error = 0;
	return data;

err_out_unlock:
	pthread_mutex_unlock(&c->lock);
err_out_exit:
	*error = err;
	return NULL;
}

int fserver_put_client_data_direct(struct fserver_crypto_engine *e, struct fserver_client *c, struct netfs_cmd *old)
{
	int err;
	unsigned int size = old->size;
	char buf[old->size + sizeof(struct netfs_cmd) + sizeof(e->hash_output)];
	struct netfs_cmd *cmd = old;
	void *data;

	cmd->csize = 0;
	data = old->data;

	if (!e->perform_crypto || !cmd->size)
		goto out;

	if (e->evp_md && old->size) {
		cmd = (struct netfs_cmd *)buf;

		*cmd = *old;

		err = e->hash_init(e);
		if (err)
			goto err_out_exit;
		err = e->hash_update(e, data, old->size);
		if (err)
			goto err_out_exit;
		err = e->hash_final(e);
		if (err)
			goto err_out_exit;

		cmd->size += e->hash_output_size;
		cmd->csize = e->hash_output_size;

		data = cmd->data + e->hash_output_size;

		memcpy(cmd->data, e->hash_output, e->hash_output_size);
		memcpy(data, old->data, old->size);

#if 0
#ifdef FSERVER_DEBUG
		{
			unsigned int i;
			unsigned char *hash = (unsigned char *)cmd->data;

			ulog("%s: thread: %lu: cmd: %u, start: %llu, id: %llu, size: %u, csize: %u: ",
					__func__, w->id, cmd->cmd, cmd->start, cmd->id, cmd->size, cmd->csize);
			for (i=0; i<e->hash_output_size; ++i) {
				ulog("%02x ", hash[i]);
			}
			ulog("\n");
		}
#endif
		ulog("%s: thread: %lu: cmd: %u, id: %llu, start: %llu, size: %u: hashed.\n",
				__func__, w->id, cmd->cmd, cmd->id, cmd->start, cmd->size);
#endif
	}

	if (e->evp_cipher && old->size) {
		unsigned char iv[32];

		memset(iv, 0, sizeof(iv));

		cmd->iv = e->gen_iv;
		e->gen_iv++;

		memcpy(iv, &cmd->iv, sizeof(cmd->iv));
	
		err = e->cipher_init(e, iv, 1);
		if (err)
			goto err_out_exit;
		err = e->cipher_update(e, data, &size, data, old->size);
		if (err)
			goto err_out_exit;
#if 0
		err = e->cipher_final(e, data, &size);
		if (err)
			goto err_out_exit;
		ulog("%s: thread: %lu: cmd: %u, id: %llu, start: %llu, size: %u/%u: encrypted.\n",
				__func__, w->id, cmd->cmd, cmd->id, cmd->start, cmd->size, size);
#endif
	}

out:
	size = cmd->size;
	
	ulog("%s: sending: cmd: %u, start: %llu, id: %llu, csize: %u, size: %u, ext: %d/%u, perform_crypto: %d.\n",
			__func__, cmd->cmd, cmd->start, cmd->id, cmd->csize, cmd->size,
			(signed short)cmd->ext, cmd->ext, e->perform_crypto);

	netfs_convert_cmd(cmd);

	pthread_mutex_lock(&c->lock);
	err = fserver_send_data(c, cmd, size + sizeof(struct netfs_cmd));
	if (err)
		goto err_out_unlock;
	pthread_mutex_unlock(&c->lock);

	return 0;

err_out_unlock:
	pthread_mutex_unlock(&c->lock);
err_out_exit:
	return err;
}

static int fserver_put_client_data(struct fserver_worker *w, struct netfs_cmd *cmd)
{
	return fserver_put_client_data_direct(&w->eng, w->client, cmd);
}

static inline void fserver_convert_stat(struct stat *st, struct netfs_inode_info *info)
{
	info->ino = st->st_ino;
	info->mode = st->st_mode;
	info->nlink = st->st_nlink;
	info->uid = st->st_uid;
	info->gid = st->st_gid;
	info->rdev = st->st_rdev;
	info->size = st->st_size;
	info->blocksize = st->st_blksize;
	info->blocks = st->st_blocks;
}

int fserver_fill_stat_inode(int fd, char *name, struct netfs_inode_info *info)
{
	struct stat st;
	int err;

	err = fstatat(fd, name, &st, AT_SYMLINK_NOFOLLOW);
	if (err < 0) {
		if (errno != ENOENT)
			ulog_err("Failed to stat file '%s'", name);
		return err;
	}

	fserver_convert_stat(&st, info);

	return 0;
}

static int fserver_lookup(struct fserver_worker *w, struct netfs_cmd *cmd)
{
	int err;
	char buf[256 + sizeof(struct netfs_cmd) + sizeof(struct netfs_inode_info)];
	struct netfs_cmd *reply = (struct netfs_cmd *)buf;
	struct netfs_inode_info *info = (struct netfs_inode_info *)(reply + 1);
	char *name = (char *)(info + 1), *last;
	char *path = (char *)cmd->data;

	ulog("%s: id: %llu, path: '%s'.\n", __func__, cmd->id, path);
	memset(buf, 0, sizeof(buf));

	*reply = *cmd;

	err = fserver_fill_stat_inode(w->client->f->dirfd, path, info);
	if (err) {
		reply->start = errno;
		reply->size = 0;

		return fserver_put_client_data(w, reply);
	}

	ulog("%s: id: %llu, mode: %o.\n", __func__, cmd->id, info->mode);

	last = strrchr(path, '/');
	if (last) {
		last++;

		if (last && *last) {
			int rest;

			rest = reply->size - (last - path);

			reply->size = rest + 1; /* 0 symbol */
			reply->size = snprintf(name, reply->size, "%s", last) + 1;
		} else
			last = NULL;
	}

	if (!last) {
		reply->size = cmd->size;
		snprintf(name, reply->size, "%s", path);
	}
	
	reply->size += sizeof(struct netfs_inode_info);

	reply->cpad = fserver_crypto_pad(&w->eng, reply->size);
	reply->size += reply->cpad;
	
	ulog("%s: sending back: id: %llu, size: %u, path: '%s'.\n",
			__func__, reply->id, reply->size, name);

	netfs_convert_inode_info(info);

	return fserver_put_client_data(w, reply);
}

static int fserver_readdir(struct fserver_worker *w, struct netfs_cmd *cmd)
{
	DIR *dir;
	int fd, err;
	struct dirent64 *d;
	char buf[sizeof(struct netfs_cmd) + sizeof(struct netfs_inode_info) + 256];
	struct netfs_cmd *reply = (struct netfs_cmd *)buf;
	struct netfs_inode_info *info = (struct netfs_inode_info *)(reply + 1);
	char *name = (char *)(info + 1);

	ulog("%s: path: '%s'.\n", __func__, cmd->data);
	memset(buf, 0, sizeof(buf));

	fd = openat(w->client->f->dirfd, (char *)cmd->data, O_RDONLY);
	if (fd < 0) {
		err = -errno;
		goto out;
	}

	err = coherency_add_object(w->client, (char *)cmd->data, cmd->size-1, cmd->id, cmd->id, POHMELFS_READ_LOCK);
	if (err)
		goto out_close;

	dir = fdopendir(fd);
	if (dir) {
		while ((d = readdir64(dir)) != NULL) {
			unsigned int len, size;

			if (d->d_name[0] == '.' && d->d_name[1] == '\0')
				continue;
			if (d->d_name[0] == '.' && d->d_name[1] == '.' && d->d_name[2] == '\0')
				continue;

			err = fserver_fill_stat_inode(fd, d->d_name, info);
			if (err)
				continue;

			ulog("%s: id: %llu, name: '%s', mode: %o.\n", __func__, cmd->id, d->d_name, info->mode);

			len = sprintf(name, "%s", d->d_name) + 1;
			size = len + sizeof(struct netfs_inode_info);

			reply->id = cmd->id;
			reply->start = 0;
			reply->cmd = NETFS_READDIR;
			reply->cpad = fserver_crypto_pad(&w->eng, size);
			reply->size = size + reply->cpad;
			reply->ext = 0;

			netfs_convert_inode_info(info);

			err = fserver_put_client_data(w, reply);
			if (err)
				break;
		}
	}

out_close:
	close(fd);
out:
	reply->id = cmd->id;
	reply->cmd = NETFS_READDIR;
	reply->start = err;
	reply->size = 0;
	reply->ext = 1;

	return fserver_put_client_data(w, reply);
}

static int fserver_do_read_page(int fd, void *data, unsigned int size, __u64 start)
{
	int err;
	void *orig = data;

	while (size) {
		err = pread(fd, data, size, start);
		if (err < 0) {
			ulog_err("%s: dptr: %p, size: %u, start: %llu, err: %d",
					__func__, data, size, start, err);
			return -errno;
		}
		if (!err)
			break;

		size -= err;
		data += err;
		start += err;
	}

	return data - orig;
}

static int fserver_read_page(struct fserver_worker *w, struct netfs_cmd *cmd)
{
	int fd, err;
	unsigned int size, len = cmd->ext;
	struct netfs_cmd *r;
	char *path = (char *)cmd->data;

	cmd->size -= len;
	size = cmd->size + fserver_crypto_pad(&w->eng, cmd->size);

	r = malloc(sizeof(struct netfs_cmd) + size);
	if (!r)
		return -ENOMEM;
	memset(r, 0, sizeof(struct netfs_cmd) + size);

	*r = *cmd;

#if 1
	path++;
#endif

	fd = openat(w->client->f->dirfd, path, O_RDONLY | O_LARGEFILE | O_NOFOLLOW);
	if (fd < 0) {
		if (errno == ELOOP) {
			err = readlinkat(w->client->f->dirfd, path, (char *)r->data, cmd->size);
			if (err > 0) {
				if (err == (signed)cmd->size)
					err--;
				r->data[err] = '\0';
				err++;
				goto out_send;
			}
			ulog_err("Failed to open a link '%s'", path);
		} else {
			ulog_err("Failed to open file '%s'", path);
		}
		err = -errno;
		goto err_out_exit;
	}

	err = fserver_do_read_page(fd, r->data, cmd->size, r->start);
	if (err < 0)
		goto err_out_close;
	close(fd);

out_send:
	ulog("%s: path: '%s', start: %llu, size: %u, ret: %d.\n",
			__func__, path, cmd->start, cmd->size, err);

	r->ext = 0;
	r->cpad = fserver_crypto_pad(&w->eng, err);
	r->size = r->cpad + err;

	err = fserver_put_client_data(w, r);
	if (err)
		goto err_out_exit;

	free(r);
	return 0;

err_out_close:
	close(fd);
err_out_exit:
	free(r);
	return err;
}

static int fserver_do_write_page(int fd, void *data, unsigned int size, __u64 start)
{
	int err;

	while (size) {
		err = pwrite(fd, data, size, start);
		if (err < 0) {
			ulog_err("%s: dptr: %p, fd: %d, size: %u, start: %llu, err: %d",
					__func__, data, fd, size, start, err);
			return -errno;
		}
		if (!err)
			break;

		size -= err;
		data += err;
		start += err;
	}

	return 0;
}

static int fserver_write_page(struct fserver_worker *w, struct netfs_cmd *cmd)
{
	int fd, err;
	unsigned int size, len;
	void *dptr;
	char *path;
	
	path = (char *)cmd->data;
	len = cmd->ext;
	dptr = cmd->data + len + 1;
	size = cmd->size - len - 1;

#if 1
	path++;
#endif
	fd = openat(w->client->f->dirfd, path, O_RDWR | O_LARGEFILE);
	if (fd < 0) {
		ulog_err("%s: thread: %lu: Failed to open file '%s'",
				__func__, w->id, path);
		err = -errno;
		goto err_out_exit;
	}

	ulog("%s: thread: %lu, path: '%s', start: %llu, size: %u.\n",
			__func__, w->id, path, cmd->start, size);

	err = fserver_do_write_page(fd, dptr, size, cmd->start);
	if (err < 0)
		goto err_out_close;

	close(fd);

	return 0;

err_out_close:
	close(fd);
err_out_exit:
	return err;
}

static int do_fserver_create(struct fserver_worker *w, struct netfs_cmd *cmd)
{
	struct netfs_path_entry *ne;
	void *data = cmd->data;
	char name[256];
	int err = 0, fd, pfd, path_len = 0;
	char path[4096] = "";

	fd = -1;
	pfd = open("/", O_RDONLY | O_DIRECTORY);
	if (pfd < 0) {
		ulog_err("%s: thread: %lu: Failed to open root dir", __func__, w->id);
		return -errno;
	}

	while (cmd->size) {
		ne = data;

		err = -EINVAL;
		if (cmd->size < sizeof(struct netfs_path_entry))
			break;
	
		cmd->size -= sizeof(struct netfs_path_entry);
		data += sizeof(struct netfs_path_entry);

		netfs_convert_path_entry(ne);

		ulog("%s: thread: %lu: len: %u, unused: %u, size: %u.\n",
				__func__, w->id, ne->len, sizeof(ne->unused), cmd->size);

		if (ne->len > sizeof(ne->unused)) {
			if (cmd->size < ne->len)
				break;
			cmd->size -= ne->len;
			data += ne->len;
		}

		if (ne->len <= sizeof(ne->unused))
			snprintf(name, ne->len+1, "%s", ne->unused);
		else
			snprintf(name, ne->len+1, "%s", ne->data);

		ulog("%s: thread: %lu: name: '%s', mode: %o, dir: %d.\n",
				__func__, w->id, name, ne->mode, S_ISDIR(ne->mode));

		path_len += snprintf(path + path_len, sizeof(path) - path_len, "/%s", name);

		if (S_ISDIR(ne->mode)) {
			err = mkdirat(pfd, name, ne->mode);
			if (err < 0 && errno != EEXIST) {
				ulog_err("%s: thread: %lu: Failed to create directory '%s', pfd: %d, path: '%s'",
						__func__, w->id, name, pfd, path);
				err = -errno;
				break;
			}

			fd = openat(pfd, name, O_RDONLY | O_DIRECTORY);
			if (fd < 0) {
				ulog_err("%s: thread: %lu: Failed to open existing dir '%s', pfd: %d, path: '%s'",
						__func__, w->id, name, pfd, path);
				err = -errno;
				break;
			}
			err = close(pfd);
			ulog("%s: thread: %lu: closed pfd: %d, err: %d.\n", __func__, w->id, pfd, err);
			pfd = fd;
		} else {
			if (S_ISREG(ne->mode))
				fd = openat(pfd, name, O_RDWR | O_CREAT | O_LARGEFILE, ne->mode);
			else
				fd = openat(pfd, name, O_RDWR | O_CREAT | O_LARGEFILE, ne->mode);
			if (fd < 0 && errno != EEXIST) {
				ulog_err("%s: thread: %lu: Failed to create file '%s', reg: %d",
						__func__, w->id, name, S_ISREG(ne->mode));
				err = -errno;
				break;
			}
			err = close(pfd);
			ulog("%s: thread: %lu: closed pfd: %d, fd: %d, err: %d.\n", __func__, w->id, pfd, fd, err);
			pfd = fd;

			if (cmd->size) {
				uloga("%s: thread: %lu, Broken data, cmd_size: %u...\n",
						__func__, w->id, cmd->size);
				err = -EINVAL;
				break;
			}
		}

		err = 0;
	}

	if (cmd->ext) {
		cmd->size = 0;
		cmd->start = err;
		fserver_put_client_data(w, cmd);
	}

	if (!err && !S_ISDIR(ne->mode))
		return pfd;
	err = close(pfd);
	ulog("%s: thread: %lu: final closed pfd: %d, err: %d.\n", __func__, w->id, pfd, err);

	if (pfd != fd && fd) {
		ulog("%s: thread: %lu: final closed fd: %d, err: %d.\n", __func__, w->id, fd, err);
		close(fd);
	}

	return err;
}

static int fserver_create_no_trans(struct fserver_worker *w, struct netfs_cmd *cmd)
{
	int fd;

	fd = do_fserver_create(w, cmd);
	if (fd > 0) {
		close(fd);
		return 0;
	}

	return fd;
}

static int fserver_link(struct fserver_worker *w, struct netfs_cmd *cmd)
{
	char *old_path, *new_path, *lnk = (char *)cmd->data;
	int err;

	ulog("%s: thread: %lu: id: %llu, start: %llu, ext: %d, data: '%s', size: %u.\n",
			__func__, w->id, cmd->id, cmd->start, cmd->ext, lnk, cmd->size);

	new_path = lnk;
	old_path = lnk + cmd->ext;
	*old_path++ = '\0';

	if (cmd->start) {
		err = symlink(old_path, new_path);
	} else {
		err = link(old_path, new_path);
	}
	
	ulog("%s: thread: %lu: %s: '%s' -> '%s'.\n",
			__func__, w->id,
			cmd->start?"symlink":"hardlink",
			new_path, old_path);

	if (err < 0)
		ulog_err("%s: thread: %lu: Failed to create a link", __func__, w->id);

	return err;
}

static int fserver_do_rmdir(int dirfd, char *sub, int flags)
{
	int fd, err = 0;
	DIR *dir;
	struct dirent64 *d;

	if (flags != AT_REMOVEDIR)
		goto do_unlink;

	fd = openat(dirfd, sub, O_RDONLY);
	if (fd == -1) {
		ulog_err("Failed to open '%s' in %d", sub, dirfd);
		return -errno;
	}

	dir = fdopendir(fd);
	if (dir) {
		err = 0;

		while ((d = readdir64(dir)) != NULL) {
			if (d->d_name[0] == '.' && d->d_name[1] == '\0')
				continue;
			if (d->d_name[0] == '.' && d->d_name[1] == '.' && d->d_name[2] == '\0')
				continue;

			if (d->d_type == DT_DIR) {
				err = fserver_do_rmdir(fd, d->d_name, AT_REMOVEDIR);
				if (err)
					break;
			} else {
				err = unlinkat(fd, d->d_name, 0);
				if (err) {
					ulog_err("Failed to remove %s/%s", sub, d->d_name);
					break;
				}
			}
		}
	} else {
		flags = 0;
	}

	close(fd);

do_unlink:
	if (!err) {
		err = unlinkat(dirfd, sub, flags);
		if (err)
			ulog_err("Failed to remove %s", sub);
	}

	return err;
}

static int fserver_remove(struct fserver_worker *w, struct netfs_cmd *cmd)
{
	int err;
	unsigned flags = 0;
	char *path = (char *)cmd->data;

	if (cmd->ext)
		flags = AT_REMOVEDIR;

	err = fserver_do_rmdir(w->client->f->dirfd, path, flags);
	if (err) {
		ulog("%s: thread: %lu: parent: '%s', dir: %d.\n",
			__func__, w->id, path, !!cmd->ext);
	}

	return 0;
}

static int fserver_open_non_transaction(struct fserver_worker *w __unused, struct netfs_cmd *cmd __unused)
{
	ulog("%s: id: %llu, start: %llu, size: %u, ext: %u.\n",
			__func__, cmd->id, cmd->start, cmd->size, cmd->ext);
	return -EINVAL;
}

static int fserver_transaction(struct fserver_worker *w, struct netfs_cmd *cmd);

static int fserver_inode_info(struct fserver_worker *w, struct netfs_cmd *cmd)
{
	struct netfs_inode_info *info = (struct netfs_inode_info *)(cmd + 1);
	char *path = (char *)(info + 1);
	int err, fd = w->client->f->dirfd;
	struct netfs_inode_info old;

	netfs_convert_inode_info(info);

	memset(&old, 0, sizeof(struct netfs_inode_info));

	err = fserver_fill_stat_inode(fd, path, &old);
	if (err)
		goto err_out_exit;

	ulog("%s: thread: %lu: path: '%s', size: %llu -> %llu, mode: %o -> %o, uid: %u -> %u, gid: %u -> %u.\n",
			__func__, w->id, path, old.size, info->size, old.mode, info->mode,
			old.uid, info->uid, old.gid, info->gid);

	if (info->mode != old.mode) {
		//err = fchmodat(fd, path, info->mode, AT_SYMLINK_NOFOLLOW);
		err = fchmodat(fd, path, info->mode, 0);
		if (err)
			goto err_out_exit;
	}

	if (info->uid != old.uid || info->gid != old.gid) {
		err = fchownat(fd, path, info->uid, info->gid, AT_SYMLINK_NOFOLLOW);
		if (err)
			goto err_out_exit;
	}

	if (info->size != old.size && S_ISREG(info->mode)) {
		err = truncate(path, info->size);
		if (err)
			goto err_out_exit;
	}

	return 0;

err_out_exit:
	ulog_err("%s: thread: %lu: path: '%s', size: %llu -> %llu, mode: %o -> %o, uid: %u -> %u, gid: %u -> %u",
			__func__, w->id, path, old.size, info->size, old.mode, info->mode,
			old.uid, info->uid, old.gid, info->gid);
	return 0;
}

static int fserver_flush_writers(struct fserver_worker *w, unsigned int hash, unsigned int len,
		__u64 start, unsigned int size, int type)
{
	struct netfs_cmd c;

	ulog("%s: hash: %x, len: %u, start: %llu, size: %u.\n",
			__func__, hash, len, start, size);

	c.cmd = NETFS_PAGE_CACHE;
	c.start = start;
	c.size = 0;
	c.ext = type;
	c.csize = c.cpad = 0;

	return coherency_broadcast_hash(w->client->f, w->client, &w->eng, hash, len, &c, fserver_flush_writers_callback);
}

static int fserver_page_cache(struct fserver_worker *w __attribute__ ((unused)),
		struct netfs_cmd *cmd __attribute__ ((unused)))
{
	return -EINVAL;
}

static int fserver_send_lock_response(struct fserver *f, struct fserver_client *client, struct fserver_crypto_engine *eng,
		char *path, unsigned int len, unsigned int hash, __u64 id)
{
	char buf[sizeof(struct netfs_cmd) + sizeof(struct netfs_inode_info)];
	struct netfs_cmd *cmd = (struct netfs_cmd *)(buf);
	struct netfs_inode_info *info = (struct netfs_inode_info *)(cmd + 1);
	int err;

	cmd->cmd = NETFS_LOCK;
	cmd->size = sizeof(struct netfs_inode_info);
	cmd->start = 0;
	cmd->id = id;
	cmd->ext = 0;

	err = fserver_fill_stat_inode(f->dirfd, path, info);
	if (err) {
		cmd->start = errno;
		cmd->size = 0;
		cmd->ext = err;
	} else {
		netfs_convert_inode_info(info);
	}

	cmd->cpad = fserver_crypto_pad(eng, cmd->size);
	cmd->size += cmd->cpad;

	ulog("%s: id: %llu, path: '%s', hash: %x, len: %u.\n", __func__, id, path, hash, len);

	return coherency_broadcast_hash(f, client, eng, hash, len, cmd, NULL);
}

static int fserver_lock(struct fserver_worker *w, struct netfs_cmd *cmd)
{
	unsigned int len = cmd->ext;
	unsigned int hash = jhash(cmd->data, len, 0);
	int err = 1;
	char *name = (char *)cmd->data;
	struct netfs_lock *l = (struct netfs_lock *)(cmd->data + cmd->ext);
	int type;
	struct fserver_client *client = w->client;

	netfs_convert_lock(l);

	type = l->type & ~POHMELFS_LOCK_GRAB;

	if (l->type & POHMELFS_LOCK_GRAB) {
		ulog("%s: lock grab: path: '%s', id: %llu, start: %llu, ino: %llu, size: %u, type: %x.\n",
			__func__, name, cmd->id, l->start, l->ino, l->size, type);

		err = coherency_add_object(w->client, name, len, cmd->id, l->ino, type);
		if (err)
			return err;

		err = fserver_flush_writers(w, hash, len, l->start, l->size, type);
		if (err > 0)
			return 0;

		ulog("%s: lock granted: name: '%s', id: %llu, start: %llu, ino: %llu, size: %u, type: %x.\n",
			__func__, name, cmd->id, l->start, l->ino, l->size, l->type);
		client = NULL;
	} else {
		ulog("%s: lock release: name: '%s', id: %llu, start: %llu, ino: %llu, size: %u, type: %x.\n",
			__func__, name, cmd->id, l->start, l->ino, l->size, l->type);
		coherency_remove_object(w->client, name, len);
	}

	return fserver_send_lock_response(w->client->f, client, &w->eng, name, len, hash, cmd->id);
}

static int fserver_read_pages(struct fserver_worker *w, struct netfs_cmd *cmd)
{
	int fd, i, err = 0;
	int num = cmd->size >> 8;
	int shift = cmd->size & 0xff;
	int page_size = (1<<shift) + fserver_crypto_pad(&w->eng, 1 << shift);
	struct netfs_cmd *c;
	__u64 start = cmd->start;

	c = malloc(sizeof(struct netfs_cmd) + page_size);
	if (!c)
		return -ENOMEM;

	fd = open((const char *)cmd->data, O_RDONLY | O_LARGEFILE);
	if (fd < 0) {
		ulog_err("%s: thread: %lu: Failed to open file '%s'",
				__func__, w->id, cmd->data);
		err = -errno;
		goto err_out_free;
	}

	for (i=0; i<num; ++i) {
		err = fserver_do_read_page(fd, c->data, page_size, start);
		if (err < 0)
			break;

		c->start = start;
		c->cpad = fserver_crypto_pad(&w->eng, err);
		c->size = c->cpad + err;
		c->ext = 0;
		c->id = cmd->id;
		c->cmd = NETFS_READ_PAGE;

		start += err;

		err = fserver_put_client_data(w, c);
		if (err)
			break;

		err = 0;
	}

	ulog("%s: %d/%d, id: %llu, file: '%s', page_size: %u, err: %d.\n",
			__func__, i, num, cmd->id, (char *)cmd->data, page_size, err);

	close(fd);
err_out_free:
	free(c);
	return err;
}

static int fserver_rename(struct fserver_worker *w __unused, struct netfs_cmd *cmd)
{
	char *old_path, *new_path;
	int err;

	if (cmd->ext >= cmd->size)
		return -EINVAL;

	old_path = (char *)cmd->data;
	new_path = old_path + cmd->ext;
	*new_path = '\0';
	new_path++;

	ulog("%s: ino: %llu: '%s' -> '%s'.\n", __func__, cmd->id, old_path, new_path);

	err = rename(old_path, new_path);
	if (err) {
		ulog_err("%s: failed to rename id: %llu: '%s' -> '%s'",
				__func__, cmd->id, old_path, new_path);
		return err;
	}

	return 0;
}

static int fserver_digest_init(struct fserver_crypto_engine *e)
{
	EVP_DigestInit_ex(&e->mdctx, e->evp_md, NULL);
	return 0;
}
static int fserver_digest_update(struct fserver_crypto_engine *e, void *buf, unsigned int size)
{
	EVP_DigestUpdate(&e->mdctx, buf, size);
	return 0;
}
static int fserver_digest_final(struct fserver_crypto_engine *e)
{
	EVP_DigestFinal_ex(&e->mdctx, e->hash_output, &e->hash_output_size);
	EVP_MD_CTX_cleanup(&e->mdctx);
	return 0;
}

static int fserver_hmac_init(struct fserver_crypto_engine *e)
{
	HMAC_Init_ex(&e->hmac_ctx, e->hash_key, e->hash_keysize, e->evp_md, NULL);
	return 0;
}
static int fserver_hmac_update(struct fserver_crypto_engine *e, void *buf, unsigned int size)
{
	HMAC_Update(&e->hmac_ctx, buf, size);
	return 0;
}
static int fserver_hmac_final(struct fserver_crypto_engine *e)
{
	HMAC_Final(&e->hmac_ctx, e->hash_output, &e->hash_output_size);
#if 0
	{
		unsigned int i;

		ulog("%s: size: %u: ", __func__, e->hash_output_size);
		for (i=0; i<e->hash_output_size; ++i) {
			ulog("%02x ", e->hash_output[i]);
		}
		ulog("\n");
	}
#endif
	return 0;
}

int fserver_init_hash(struct fserver_crypto_engine *e, struct fserver_crypto_engine *meng)
{
	e->hash_keysize = meng->hash_keysize;
	memcpy(e->hash_key, meng->hash_key, e->hash_keysize);

	e->evp_md = EVP_get_digestbyname(meng->hash);
	if (!e->evp_md) {
		uloga("%s: failed to find algorithm '%s' implementation.\n",
				__func__, meng->hash);
		return -ENOENT;
	}

	if (meng->hmac) {
		HMAC_CTX_init(&e->hmac_ctx);
		e->hash_init = fserver_hmac_init;
		e->hash_update = fserver_hmac_update;
		e->hash_final = fserver_hmac_final;
	} else {
 		EVP_MD_CTX_init(&e->mdctx);
		e->hash_init = fserver_digest_init;
		e->hash_update = fserver_digest_update;
		e->hash_final = fserver_digest_final;
	}

	return 0;
}

static int fserver_init_hash_all(struct fserver_client *c, char *hash, int hmac)
{
	struct fserver_worker *w;
	struct fserver_crypto_engine *meng = &c->f->eng;
	int err;

 	OpenSSL_add_all_digests();

	snprintf(meng->hash, sizeof(meng->hash), "%s", hash);
	meng->hmac = hmac;

	pthread_mutex_lock(&c->worker_lock);
	list_for_each_entry(w, &c->worker_list, worker_entry) {
		err = fserver_init_hash(&w->eng, meng);
		uloga("%s: thread: %lu: hash: %s, hmac: %d, keysize: %u, err: %d.\n",
				__func__, w->id, meng->hash, meng->hmac, meng->hash_keysize, err);

		if (err)
			break;
		w->eng.perform_crypto = 1;
	}
	pthread_mutex_unlock(&c->worker_lock);

	return err;
}

static int fserver_cipher_init(struct fserver_crypto_engine *e, void *iv, int enc)
{
	EVP_CipherInit_ex(&e->cipher_ctx, e->evp_cipher, NULL, e->cipher_key, iv, enc);
	EVP_CIPHER_CTX_set_key_length(&e->cipher_ctx, e->cipher_keysize);
	return 0;
}
static int fserver_cipher_update(struct fserver_crypto_engine *e, void *dst, unsigned int *dsize,
				void *src, unsigned int ssize)
{
	EVP_CipherUpdate(&e->cipher_ctx, dst, (int *)dsize, src, ssize);
	return 0;
}
static int fserver_cipher_final(struct fserver_crypto_engine *e, void *dst, unsigned int *size)
{
	EVP_CipherFinal_ex(&e->cipher_ctx, dst, (int *)size);
	return 0;
}

int fserver_init_cipher(struct fserver_crypto_engine *e, struct fserver_crypto_engine *meng)
{
	e->cipher_keysize = meng->cipher_keysize;
	memcpy(e->cipher_key, meng->cipher_key, e->cipher_keysize);

	e->evp_cipher = EVP_get_cipherbyname(meng->cipher);
	if (!e->evp_cipher) {
		uloga("%s: failed to find algorithm '%s' implementation.\n",
				__func__, meng->cipher);
		return -ENOENT;
	}

	EVP_CIPHER_CTX_init(&e->cipher_ctx);

	e->cipher_init = fserver_cipher_init;
	e->cipher_update = fserver_cipher_update;
	e->cipher_final = fserver_cipher_final;

	return 0;
}

static int fserver_init_cipher_all(struct fserver_client *c, char *cipher, char *mode)
{
	struct fserver_worker *w;
	struct fserver_crypto_engine *meng = &c->f->eng, *e;
	int err;

 	OpenSSL_add_all_ciphers();
	
	snprintf(meng->cipher, sizeof(meng->cipher), "%s-%d-%s", cipher, e->cipher_keysize*8, mode);

	pthread_mutex_lock(&c->worker_lock);
	list_for_each_entry(w, &c->worker_list, worker_entry) {
		err = fserver_init_cipher(&w->eng, meng);
		uloga("%s: thread: %lu: cipher: %s, mode: %s, keysize: %u, err: %d.\n",
				__func__, w->id, cipher, mode, meng->cipher_keysize, err);
		if (err)
			break;
		w->eng.perform_crypto = 1;
	}
	pthread_mutex_unlock(&c->worker_lock);

	return err;
}

static int fserver_capabilities(struct fserver_worker *w, struct netfs_cmd *cmd)
{
	struct netfs_capabilities *cap = (struct netfs_capabilities *)cmd->data;
	char *hstr, *cstr, *tmp;
	char *hash, *cipher, *mode;
	int hmac, err, pc;
	struct fserver *f = w->client->f;
	struct fserver_crypto_engine *e = &f->eng;

	netfs_convert_capabilities(cap);

	hmac = 0;
	mode = "<null>";
	hash = cipher = "<null>";
	hstr = cstr = "none";

	if (cap->hash_strlen) {
		hstr = (char *)(cap + 1);

		if (cap->hash_strlen > 4) {
			if (!strncmp(hstr, "hmac(", 5)) {
				tmp = strrchr(hstr, ')');
				if (!tmp) {
					cap->hash_strlen = 0;
					goto cipher_process;
				}
				*tmp = '\0';
				hstr[4] = '\0';
				hash = &hstr[5];
				hmac = 1;
				if (!e->hash_keysize) {
					ulog("%s: thread: %lu: requested hmac(%s), but no keysize.\n",
							__func__, w->id, hash);
					cap->hash_strlen = 0;
					goto cipher_process;
				}
			}
		} else {
			hash = hstr;
		}
	}

cipher_process:
	if (cap->cipher_keysize != e->cipher_keysize) {
		uloga("%s: thread: %lu: received keysize (%u) does not match config (%u).\n",
				__func__, w->id, cap->cipher_keysize, e->cipher_keysize);
		cap->cipher_strlen = 0;
		err = -EINVAL;
	}

	if (cap->cipher_strlen) {
		cstr = (char *)(cap + 1) + cap->hash_strlen;

		tmp = strchr(cstr, '(');
		if (!tmp) {
			cap->cipher_strlen = 0;
			goto out;
		}
		*tmp = '\0';
		mode = cstr;
		cipher = tmp + 1;

		if (!cipher || !*cipher) {
			cap->cipher_strlen = 0;
			goto out;
		}

		tmp = strrchr(cipher, ')');
		if (!tmp) {
			cap->cipher_strlen = 0;
			goto out;
		}

		*tmp = '\0';
	}

out:
	if (cap->hash_strlen) {
		err = fserver_init_hash_all(w->client, hash, hmac);
		if (err)
			cap->hash_strlen = 0;
	}

	if (cap->cipher_strlen) {
		err = fserver_init_cipher_all(w->client, cipher, mode);
		if (err)
			cap->cipher_strlen = 0;
	}

	uloga("%s: hash: %s, hmac: %d: %s, cipher: %s, mode: %s: %s.\n",
			__func__,
		hash, hmac, (cap->hash_strlen)?"SUPPORTED":"NOT SUPPORTED",
		cipher, mode, (cap->cipher_strlen)?"SUPPORTED":"NOT SUPPORTED");

	netfs_convert_capabilities(cap);

	cmd->size = sizeof(struct netfs_capabilities);
	cmd->ext = 0;

	err = fserver_notify_update(f);
	if (err) {
		fserver_notify_exit(f->notify_root);
		f->notify_root = NULL;
	}

	if (cap->cipher_strlen || cap->hash_strlen)
		e->perform_crypto = 1;

	pc = w->eng.perform_crypto;
	w->eng.perform_crypto = 0;
	err = fserver_put_client_data(w, cmd);
	w->eng.perform_crypto = pc;

	return err;
}

static int fserver_xattr_set(struct fserver_worker *w __unused, struct netfs_cmd *cmd)
{
	int err, namelen, pathlen, size, flags;
	char *path, *name;
	void *data;

	flags = cmd->id;
	size = cmd->start;
	pathlen = cmd->ext;
	namelen = cmd->size - pathlen - size;

	path = (char *)(cmd + 1);
	name = path + pathlen;
	data = name + namelen;

	ulog("%s: path: '%s', pathlen: %u, name: '%s', namelen: %u, data size: %u, flags: %x.\n",
			__func__, path, pathlen, name, namelen, size, flags);

	err = setxattr(path, name, data, size, flags);
	if (err) {
		ulog_err("Failed to set attr: '%s', size: %d, path: '%s', err: %d",
				name, size, path, err);
		return err;
	}

	return 0;
}

static int fserver_xattr_get(struct fserver_worker *w, struct netfs_cmd *cmd)
{
	int err, size, namelen, pathlen, error = 0;
	char *path, *name;
	void *data;

	size = cmd->start;
	pathlen = cmd->ext;
	namelen = cmd->size - pathlen;

	path = (char *)(cmd + 1);
	name = path + pathlen;

	data = cmd + 1;

	if (!size)
		data = NULL;

	ulog("%s: path: '%s', pathlen: %u, name: '%s', namelen: %u, data size: %u.\n",
			__func__, path, pathlen, name, namelen, size);

	cmd->size = 0;
	cmd->ext = 0;

	err = getxattr(path, name, data, size);
	if (err < 0) {
		error = -errno;
		ulog_err("Failed to get attr: '%s', size: %d, path: '%s', err: %d",
				name, size, path, err);
		cmd->ext = error;
	} else {
		if (size)
			cmd->size = err;
		cmd->ext = err;
	}

	err = fserver_put_client_data(w, cmd);
	if (err && !error)
		error = err;

	return error;
}

typedef int (*fserver_cmd_handler)(struct fserver_worker *w, struct netfs_cmd *cmd);

static fserver_cmd_handler fserver_cmd_handlers[] = {
	[NETFS_READDIR]		= &fserver_readdir,
	[NETFS_READ_PAGE]	= &fserver_read_page,
	[NETFS_WRITE_PAGE]	= &fserver_write_page,
	[NETFS_CREATE]		= &fserver_create_no_trans,
	[NETFS_REMOVE]		= &fserver_remove,
	[NETFS_LOOKUP]		= &fserver_lookup,
	[NETFS_LINK]		= &fserver_link,
	[NETFS_TRANS]		= &fserver_transaction,
	[NETFS_OPEN]		= &fserver_open_non_transaction,
	[NETFS_INODE_INFO]	= &fserver_inode_info,
	[NETFS_PAGE_CACHE]	= &fserver_page_cache,
	[NETFS_READ_PAGES]	= &fserver_read_pages,
	[NETFS_RENAME]		= &fserver_rename,
	[NETFS_CAPABILITIES]	= &fserver_capabilities,
	[NETFS_LOCK]		= &fserver_lock,
	[NETFS_XATTR_SET]	= &fserver_xattr_set,
	[NETFS_XATTR_GET]	= &fserver_xattr_get,
};

static int fserver_check_packet(struct fserver_worker *w, struct netfs_cmd *cmd)
{
	struct fserver_crypto_engine *e = &w->eng;
	unsigned int hash_size = cmd->csize;
	unsigned int total_size = cmd->size - hash_size;
	unsigned int sz;
	unsigned char *hash = cmd->data;
	void *data = hash + hash_size;
	int err = 0;

	if (!total_size)
		return 0;

	if (hash_size && e->evp_md) {
		err = e->hash_init(e);
		if (err)
			goto out;
	}

	while (total_size) {
		cmd = data;

		netfs_convert_cmd(cmd);

		if (cmd->cmd >= NETFS_CMD_MAX) {
			ulog("%s: thread: %lu: wrong subcommand: cmd: %u, start: %llu, size: %u, ext: %u.\n",
					__func__, w->id, cmd->cmd, cmd->start, cmd->size, cmd->ext);
			err = -EINVAL;
			goto out;
		}

		sz = cmd->size;
		if (cmd->cmd == NETFS_READ_PAGE)
			sz = cmd->ext;
		if (cmd->cmd == NETFS_READ_PAGES)
			sz = cmd->ext;
		sz += cmd->cpad;

		data += sz + sizeof(struct netfs_cmd);
		total_size -= sz + sizeof(struct netfs_cmd);

		ulog("%s: thread: %lu: total_size: %u, cmd: %u, size: %u, ext: %u, csize: %u, cpad: %u, sz: %u.\n",
				__func__, w->id, total_size, cmd->cmd, cmd->size,
				cmd->ext, cmd->csize, cmd->cpad, sz);

		if (!sz) {
			netfs_convert_cmd(cmd);
			continue;
		}

		if (e->evp_cipher) {
			unsigned char iv[32];
			unsigned int size = sz;

			memset(iv, 0, sizeof(iv));
			memcpy(iv, &cmd->iv, sizeof(cmd->iv));

			{
				unsigned int i;
				ulog("%s: thread: %lu: iv: ", __func__, w->id);
				for (i=0; i<sizeof(iv); ++i)
					uloga("%02x ", iv[i]);
				uloga("\n");
			}

			err = e->cipher_init(e, iv, 0);
			if (err)
				goto out;
			err = e->cipher_update(e, cmd->data, &size, cmd->data, size);
			if (err)
				goto out;
#if 0
			err = e->cipher_final(e, cmd->data, &size);
			if (err)
				goto out;
#endif
		}

		if (hash_size && e->evp_md) {
			err = e->hash_update(e, cmd->data, sz);
			if (err)
				goto out;
		}

		netfs_convert_cmd(cmd);
	}
	
	if (hash_size && e->evp_md) {
		unsigned char *res = e->hash_output;

		err = e->hash_final(e);
#if 1
		{
			unsigned int i;

			ulog("%s: thread: %lu, hash_size: %u, calc: ",
					__func__, w->id, hash_size);
			for (i=0; i<hash_size; ++i)
				uloga("%02x ", res[i]);
			uloga("\n");
			ulog("%s: thread: %lu, hash_size: %u, recv: ",
					__func__, w->id, hash_size);
			for (i=0; i<hash_size; ++i)
				uloga("%02x ", hash[i]);
			uloga("\n");
		}
#endif

		err = !!memcmp(res, hash, hash_size);
	}

out:
	if (err) {
		ulog("%s: thread: %lu: err: %d.\n", __func__, w->id, err);
	}
	return err;
}

static int fserver_transaction(struct fserver_worker *w, struct netfs_cmd *cmd)
{
	void *data;
	struct netfs_cmd *sub;
	int err, fd = 0;
	unsigned int len, hash, sz;
	unsigned int hash_size = cmd->ext;

	err = fserver_check_packet(w, cmd);
	if (err)
		goto out;

	data = cmd->data + hash_size;
	cmd->size -= hash_size;

	while (cmd->size) {
		sub = data;
		netfs_convert_cmd(sub);

		sub->csize = 0;
		ulog("%s: thread: %lu: trans: %llu, size: %u, sub: cmd: %u, id: %llu, start: %llu, size: %u, ext: %u, cpad: %u.\n",
				__func__, w->id, cmd->id, cmd->size, sub->cmd, sub->id, sub->start, sub->size, sub->ext, sub->cpad);

		if (sub->size > 40960000) {
			err = -EINVAL;
			break;
		}

		sz = sub->size;
		if (sub->cmd == NETFS_READ_PAGE)
			sz = sub->ext;
		if (sub->cmd == NETFS_READ_PAGES)
			sz = sub->ext;
		sz += sub->cpad;

		data += sz + sizeof(struct netfs_cmd);
		cmd->size -= sz + sizeof(struct netfs_cmd);
		
		if (sub->cmd == NETFS_CREATE) {
			if (fd)
				close(fd);
			
			fd = do_fserver_create(w, sub);
			if (fd < 0) {
				err = -errno;
				break;
			}

			continue;
		}

		if (sub->cmd == NETFS_OPEN) {
			if (fd)
				close(fd);
			fd = open((const char *)sub->data, sub->ext | O_LARGEFILE);
			if (fd < 0) {
				ulog_err("%s: thread: %lu: Failed to open file '%s', flags: %x",
						__func__, w->id, sub->data, sub->ext);
				err = -errno;
				break;
			}

			len = sub->size - 1;
			hash = jhash(sub->data, len, 0);

			continue;
		}

		if (fd) {
			if (sub->cmd == NETFS_WRITE_PAGE) {
				err = fserver_do_write_page(fd, sub->data, sub->size, sub->start);
			} else if (sub->cmd == NETFS_READ_PAGE) {
				err = fserver_do_read_page(fd, sub->data, sub->size, sub->start);
			}

			if (err)
				break;

			continue;
		}

		err = fserver_cmd_handlers[sub->cmd](w, sub);
		if (err < 0)
			break;
	}

	if (fd)
		close(fd);

out:
	cmd->size = 0;
	cmd->ext = err;

	err = fserver_put_client_data(w, cmd);
	
	/*
	 * For pretty debug of command completion.
	 */
	netfs_convert_cmd(cmd);

	return err;
}

static void fserver_client_put(struct fserver_client *c)
{
	pthread_mutex_lock(&c->lock);
	uloga("Dropped thread %u for client %s:%d.\n", c->users, fserver_addr_convert(c), fserver_port_convert(c));
	if (--c->users) {
		pthread_mutex_unlock(&c->lock);
		return;
	}
	
	uloga("Disconnected client %s:%d.\n", fserver_addr_convert(c), fserver_port_convert(c));

	pthread_mutex_lock(&c->f->client_lock);
	list_del(&c->client_entry);
	pthread_mutex_unlock(&c->f->client_lock);

	coherency_remove_client_from_all_dirs(c);

	close(c->cs);
	free(c);
}

static void *fserver_worker_func(void *data)
{
	struct fserver_worker *w = data;
	struct netfs_cmd *cmd;
	int err = 0, cmd_num = 0;

	while (!w->need_exit && !err) {
		err = 0;
		cmd = fserver_get_client_data(w->client, w->id, &err);

		if (err)
			break;

		if (!cmd)
			continue;

		cmd_num++;

		err = fserver_cmd_handlers[cmd->cmd](w, cmd);
		ulog("%s: thread: %lu: completed cmd: %u.\n", __func__, w->id, cmd->cmd);
		free(cmd);
	}

	pthread_mutex_lock(&w->client->worker_lock);
	list_del_init(&w->worker_entry);
	pthread_mutex_unlock(&w->client->worker_lock);

	fserver_client_put(w->client);

	return NULL;
}

static struct fserver_worker *fserver_accept_client(struct fserver *f, int family)
{
	struct fserver_worker *w;
	int err, i;
	struct fserver_client *c;

	c = malloc(sizeof(struct fserver_client));
	if (!c)
		return NULL;
	memset(c, 0, sizeof(struct fserver_client));

	pthread_mutex_init(&c->lock, NULL);

	c->f = f;

	if (family == AF_INET) {
		c->len = sizeof(struct sockaddr_in);
		c->cs = accept(f->s, (struct sockaddr *)&c->sa, &c->len);
	} else {
		c->len = sizeof(struct sockaddr_in6);
		c->cs = accept(f->s, (struct sockaddr *)&c->sa6, &c->len);
	}

	if (c->cs < 0) {
		ulog_err("Failed to accept new client");
		goto err_out_free;
	}

	pthread_mutex_init(&c->worker_lock, NULL);
	INIT_LIST_HEAD(&c->worker_list);

	pthread_mutex_lock(&f->client_lock);
	list_add_tail(&c->client_entry, &f->client_list);
	pthread_mutex_unlock(&f->client_lock);

	pthread_mutex_lock(&c->lock);
	for (i=0; i<f->workers_per_client; ++i) {
		w = malloc(sizeof(struct fserver_worker));
		if (!w)
			return NULL;
		memset(w, 0, sizeof(struct fserver_worker));

		w->client = c;

		c->users++;

		pthread_mutex_lock(&c->worker_lock);
		list_add_tail(&w->worker_entry, &c->worker_list);
		pthread_mutex_unlock(&c->worker_lock);

		err = pthread_create(&w->id, NULL, &fserver_worker_func, w);
		if (err) {
			ulog_err("Failed to create worker thread");
			goto err_out_del;
		}
	}
	pthread_mutex_unlock(&c->lock);

	uloga("Accepted client %s:%d.\n", fserver_addr_convert(c), fserver_port_convert(c));

	return w;

err_out_del:
	pthread_mutex_unlock(&c->lock);

	pthread_mutex_lock(&c->worker_lock);
	list_del_init(&w->worker_entry);
	pthread_mutex_unlock(&c->worker_lock);
	fserver_client_put(c);
err_out_free:
	free(w);
	return 0;
}

static int fserver_background(void)
{
	pid_t pid;

	pid = fork();
	if (pid == -1) {
		ulog_err("Failed to fork to background");
		return -1;
	}

	if (pid != 0) {
		ulog("Daemon pid: %d.\n", pid);
		exit(0);
	}

	if (setsid() == -1) {
		ulog_err("Failed to change session");
		return -1;
	}

	close(1);
	close(2);

	return 0;
}


static void fserver_usage(char *p)
{
	uloga("Usage: %s <options>\n", p);
	uloga(" -A 6			- listen on ipv6 address. Default: Disabled.\n");
	uloga("	-r root			- path to root directory. Default: /tmp.\n");
	uloga("	-a addr			- listen address. Default: 0.0.0.0.\n");
	uloga("	-p port			- listen port. Default: 1025.\n");
	uloga("	-w workers		- number of workers per connected client. Default: 1.\n");
	uloga("	-k file			- cipher key file. Default: no.\n");
	uloga("	-K file			- hash key file. Default: no.\n");
	uloga("	-l log			- log file. Default: stdout.\n");
	uloga("	-d			- fork to background. Default: no.\n");
	uloga("	-h			- this help.\n");
}

int main(int argc, char *argv[])
{
	int ch, workers, family = AF_INET, daemon;
	struct fserver *fs;
	char *root, *addr, *port, *cipher_file, *hash_file;

	daemon = 0;
	workers = 1;
	root = "/tmp";
	addr = "0.0.0.0";
	port = "1025";

	cipher_file = hash_file = NULL;

	while ((ch = getopt(argc, argv, "dl:A:k:K:a:p:r:w:h")) != -1) {
		switch (ch) {
			case 'd':
				daemon = 1;
				break;
			case 'A':
				family = AF_INET6;
				addr = "::0";
				break;
			case 'K':
				hash_file = optarg;
				break;
			case 'k':
				cipher_file = optarg;
				break;
			case 'w':
				workers = atoi(optarg);
				break;
			case 'p':
				port = optarg;
				break;
			case 'a':
				addr = optarg;
				break;
			case 'r':
				root = optarg;
				break;
			case 'l':
				if (ulog_init(optarg))
					return -1;
				break;
			default:
				fserver_usage(argv[0]);
				return -1;
		}
	}

	if (daemon)
		fserver_background();

	signal(SIGPIPE, SIG_IGN);

	fs = fserver_init(family, addr, port, root, workers, cipher_file, hash_file);
	if (!fs)
		return -1;

	while (1) {
		struct fserver_worker *w;

		w = fserver_accept_client(fs, family);
	}

	return 0;
}
