/*
** Copyright 2002 S. Varshavchik.
** See COPYING for distribution information.
*/
#include	"config.h"
#include	"numlib/numlib.h"
#include	"liblock/config.h"
#include	"liblock/liblock.h"
#include	<stdio.h>
#include	<string.h>
#include	<stdlib.h>
#include	<errno.h>
#include	<ctype.h>

#if HAVE_SYS_STAT_H
#include	<sys/stat.h>
#endif
#if HAVE_FCNTL_H
#include	<fcntl.h>
#endif

#include	"tlscache.h"


/*
** The cache file begins with the following record:
*/

struct hdr {
	off_t filesize;		/* Size of the file, don't trust fstat */
	off_t head, tail;	/* Head and tail ptrs */
	off_t first;		/* See below */
};

#ifndef TLSCACHEMINSIZE
#define TLSCACHEMINSIZE 16384
#endif

#define BORK(p) BORK2(__LINE__, (p))

static void BORK2(int l, const char *p)
{
	fprintf(stderr, "ALERT: tlscache.c(%d): corruption detected in %s\n",
		(l), (p));
}

/*
** Cached SSL session objects are written starting at the end of the file
** the file, growing to the beginning of the file.  The head pointer
** points to the most recently added cached object.  When the beginning of
** the file is reached, it's wrapped around.
**
** After the wraparound, old SSL session objects are freed. starting with
** the tail ptr, to make room for newer object.  There may be unused space
** between struct hdr, and the first cached object, the 'first' pointer
** helps me find the first cached object, when searching.
** When searching for a cached object, begin at the head ptr (most recent,
** and continue until we find an object referenced by the tail ptr).
**
** Each cached object carries the following header.
*/

struct obj {
	size_t prev_size;	/* Size of the previous cached object,
				** this must be the first member of this
				** struct.
				*/

	size_t my_size;		/* Size of this cached object */
};

/* Read cnt number of bytes, or else */

static int my_read(int fd, void *buffer, size_t cnt)
{
	char *p=(char *)buffer;

	while (cnt > 0)
	{
		int n=read(fd, p, cnt);

		if (n <= 0)
			return n;
		p += n;
		cnt -= n;
	}
	return 1;
}

/* Write cnt number of bytes, or else */

static int my_write(int fd, const void *buffer, size_t cnt)
{
	const char *p=(const char *)buffer;

	while (cnt > 0)
	{
		int n=write(fd, p, cnt);

		if (n <= 0)
			return -1;
		p += n;
		cnt -= n;
	}
	return 0;
}

static int init(struct CACHE *, off_t s);

void tls_cache_close(struct CACHE *p)
{
	if (p->filename != NULL)
		free(p->filename);
	if (p->fd >= 0)
		close(p->fd);
	free(p);
}

/*
** Open a cache file, creating one if necessary
*/

struct CACHE *tls_cache_open(const char *filename, off_t req_size)
{
	struct CACHE *p=malloc(sizeof(struct CACHE));
	struct hdr h;
	int rc;

	if (!p) return NULL;

	if ((p->fd=open(filename, O_RDWR|O_CREAT, 0600)) < 0)
	{
		free(p);
		return NULL;
	}

	if ((p->filename=strdup(filename)) == NULL)
	{
		close(p->fd);
		free(p);
		return (NULL);
	}

	rc=my_read(p->fd, &h, sizeof(h));

	if (rc < 0)
	{
		tls_cache_close(p);
		return (NULL);
	}

	if (rc == 0 || h.filesize == 0)
	{
		/* Once again, but this time lock it */

		if (ll_lock_ex(p->fd) < 0 ||
		    lseek(p->fd, 0, SEEK_SET) < 0)
		{
			tls_cache_close(p);
			return (NULL);
		}

		rc=my_read(p->fd, &h, sizeof(h));

		if (rc < 0)
		{
			tls_cache_close(p);
			return (NULL);
		}

		if (rc == 0 || h.filesize == 0)
		{
			if (init(p, req_size))
			{
				tls_cache_close(p);
				return (NULL);
			}
		}
		ll_unlock_ex(p->fd);
	}
	return p;
}

static int doadd(struct CACHE *p, const char *val, size_t vallen);

int tls_cache_add(struct CACHE *p, const char *val, size_t vallen)
{
	int rc;

	if (p->fd < 0)
		return (0);	/* Previous error invalidated obj */

	if (ll_lock_ex(p->fd) < 0)
	{
		close(p->fd);
		p->fd= -1;
		return (-1);
	}

	rc=doadd(p, val, vallen);

	if (rc < 0 && p->fd >= 0)
	{
		close(p->fd);
		p->fd= -1;
		unlink(p->filename);	/* Blow it away, something's wrong */
		perror("ALERT: tlscache.c: ");
		fprintf(stderr, "ALERT: tlscache.c: removing %s\n",
			p->filename);
	}

	if (p->fd >= 0 && ll_unlock_ex(p->fd) < 0)
	{
		close(p->fd);
		p->fd= -1;
		rc= -1;
	}

	if (rc != 0)
		rc= -1;

	return rc;
}

/*
** Read the header, and do a simple sanity check
*/

static int readhdr(struct CACHE *p, struct hdr *h)
{
	if (lseek(p->fd, 0, SEEK_SET) < 0 ||
	    my_read(p->fd, h, sizeof(*h)) <= 0)
	{
		BORK(p->filename);
		return (-1);
	}

	if (h->filesize < TLSCACHEMINSIZE || h->head >= h->filesize ||
	    h->tail >= h->filesize || h->first >= h->filesize ||
	    h->head < 0 || h->tail < 0 || h->first < 0 ||
	    h->first > h->head || h->first > h->tail)
	{
		BORK(p->filename);
		return (-1);	/* Sanity check */
	}
	return (0);
}

static int writehdr(struct CACHE *p, const struct hdr *h)
{
	if (lseek(p->fd, 0, SEEK_SET) < 0 ||
	    my_write(p->fd, h, sizeof(*h)) < 0)
	{
		BORK(p->filename);
		return -1;
	}
	return 0;
}

/*
** Read the header of a cached object, and do a sanity check
*/

static int readobj(struct CACHE *p, off_t w,
		   const struct hdr *h, struct obj *o)
{
	if (lseek(p->fd, w, SEEK_SET) < 0 ||
	    my_read(p->fd, o, sizeof(*o)) <= 0)
	{
		BORK(p->filename);
		return (-1);
	}

	if (o->prev_size < sizeof(*o) || o->my_size < sizeof(*o) ||
	    o->prev_size >= h->filesize || o->my_size >= h->filesize ||
	    h->filesize - w < o->my_size)
	{
		BORK(p->filename);
		errno=EIO;
		return (-1);	/* Sanity check */
	}
	return 0;
}


static int doadd(struct CACHE *p, const char *val, size_t vallen)
{
	struct hdr h;
	struct obj o;
	int timer=0;
	int first=0;
	char *buf;

	if (readhdr(p, &h))
		return -1;

	/* Keep trying to allocate sufficient space in the cache file */

	for (;;)
	{
		if (++timer > 100)
		{
			BORK(p->filename);
			errno=EIO;
			return (-1);	/* Sanity check */
		}

		if (h.head == 0 && h.tail == 0)	/* First time */
		{
			if (vallen + sizeof(struct obj) +
			    sizeof(struct hdr) > h.filesize)
			{
				errno=ENOSPC;
				return 1;
			}

			/* First cached object goes at the end */

			h.head=h.tail=h.filesize - vallen - sizeof(struct obj);
			first=1;
			break;
		}

		if (h.head <= h.tail)	/* Not wrapped around */
		{
			if (h.head >= sizeof(struct hdr) +
			    sizeof(struct obj) + vallen)
			{
				h.head -= sizeof(struct obj) + vallen;
				break;
				/* Room earlier in the file */
			}

			/*
			** No room before, we must now wrap around.  Find
			** where the last object ends, and see if there's
			** enough room between the end of the last object,
			** and the end of the file, to save the new object.
			*/

			if (readobj(p, h.tail, &h, &o) < 0)
				return -1;

			if (h.filesize - h.tail - o.my_size >=
			    sizeof(struct obj) + vallen)
			{
				h.first=h.head;
				h.head=h.filesize - vallen -
					sizeof(struct obj);
				/* Room to wrap around */

				break;
			}
		}
		else	/* We're currently wrapped around, so all the free
			** space is from tail to head.
			*/
		{
			if (readobj(p, h.tail, &h, &o) < 0)
				return -1;

			if (h.head >= h.tail + o.my_size +
			    sizeof(struct obj) + vallen)
			{
				h.head -= sizeof(struct obj) + vallen;
				break;
			}
		}

		if (h.head == h.tail)	/* Sanity check */
		{
			errno=ENOSPC;
			return 1;
		}

		/* Pop one off tail */

		if (readobj(p, h.tail, &h, &o))
			return -1;

		if (sizeof(h) + o.prev_size <= h.tail)
		{
			h.tail -= o.prev_size;

			if (writehdr(p, &h))
				return (-1);
			continue;
		}

		if (h.tail != h.first)
		{
			BORK(p->filename);
			errno=EIO;
			return (-1);	/* Sanity check */
		}

		h.first=0;
		h.tail=h.filesize - o.prev_size;

		if (h.tail < h.first)
		{
			BORK(p->filename);
			errno=EIO;
			return (-1);	/* Sanity check */
		}

		if (writehdr(p, &h))
			return (-1);
	}

	buf=malloc(vallen + sizeof(o) + sizeof(o.prev_size));

	if (!buf)
		return (1);

	o.prev_size=0;
	o.my_size=vallen + sizeof(o);
	memcpy(buf, &o, sizeof(o));
	memcpy(buf + sizeof(o), val, vallen);
	o.prev_size=o.my_size;
	memcpy(buf + sizeof(o) + vallen, &o.prev_size, sizeof(o.prev_size));

	if (lseek(p->fd, h.head, SEEK_SET) < 0)
		return (-1);

	if (h.head + sizeof(o) + vallen < h.filesize)
	{
		if (my_write(p->fd, buf, sizeof(o)+vallen+sizeof(o.prev_size)) < 0)
			return -1;
	}
	else
	{
		if (my_write(p->fd, buf, sizeof(o)+vallen) < 0 ||
		    (!first && (lseek(p->fd, h.first, SEEK_SET) < 0 ||
				my_write(p->fd, &o.prev_size,
					 sizeof(o.prev_size)) < 0)))
			return -1;
	}
				
	return writehdr(p, &h);
}

static int init(struct CACHE *p, off_t size)
{
	char buffer[BUFSIZ];
	off_t c;
	struct hdr h;

	if (size < TLSCACHEMINSIZE)
	{
		errno=EINVAL;
		return -1;
	}

	if (lseek(p->fd, 0, SEEK_SET) < 0)
		return -1;

	memset(buffer, 0, sizeof(buffer));

	c=size;

	while (c > 0)
	{
		int i;

		off_t n=c;

		if (n > sizeof(buffer))
			n=sizeof(buffer);

		i=write(p->fd, buffer, n);

		if (i <= 0)
			return -1;

		c -= i;
	}

	memset(&h, 0, sizeof(h));
	h.filesize=size;

	if (lseek(p->fd, 0, SEEK_SET) < 0 ||
	    my_write(p->fd, &h, sizeof(h)))
		return (-1);
	return (0);
}

static int dowalk(struct CACHE *cache,
		  int (*walk_func)(void *rec, size_t recsize,
				   int *doupdate,
				   void *arg),
		  void *arg);

int tls_cache_walk(struct CACHE *p,
		   int (*walk_func)(void *rec, size_t recsize,
				    int *doupdate,
				    void *arg),
		   void *arg)
{
	int rc;

	if (p->fd < 0)
		return (0);	/* Previous error invalidated obj */

	if (ll_lockfd(p->fd, ll_readlock||ll_whence_start|ll_wait, 0, 0) < 0)
	{
		/* Some locking methods don't support readonly locks */

		if (ll_lock_ex(p->fd) < 0)
		{
			close(p->fd);
			p->fd= -1;
			return (-1);
		}
	}

	rc=dowalk(p, walk_func, arg);

	if (rc < 0 && p->fd >= 0)
	{
		close(p->fd);
		p->fd= -1;
		unlink(p->filename);
		perror("ALERT: tlscache.c: ");
		fprintf(stderr, "ALERT: tlscache.c: removing %s\n",
			p->filename);
	}

	if (p->fd >= 0 && ll_unlock_ex(p->fd) < 0)
	{
		close(p->fd);
		p->fd= -1;
		rc= -1;
	}

	return rc;
}

/* Buffered reads when searching, for speed */

struct walkbuf {
	char buffer[BUFSIZ];
	char *bufptr;
	int left;
};

static int buf_read(int fd, struct walkbuf *w,
		    const void *buffer, size_t cnt)
{
	char *p=(char *)buffer;

	while (cnt > 0)
	{
		if (w->left <= 0)
		{
			w->left=read(fd, w->buffer, sizeof(w->buffer));

			if (w->left <= 0)
				return -1;
			w->bufptr=w->buffer;
		}

		*p++ = *w->bufptr++;
		--w->left;
		--cnt;
	}
	return 1;
}

static int dowalk(struct CACHE *p,
		  int (*walk_func)(void *rec, size_t recsize,
				   int *doupdate, void *arg),
		  void *arg)
{
	struct hdr h;
	struct obj o;
	char *buf=NULL;
	size_t bufsize=0;
	int rc;
	int counter;
	struct walkbuf wb;
	int updateflag;

	off_t pos;
	off_t lastpos;

	if (readhdr(p, &h))
		return -1;

	if (h.head == 0 && h.tail == 0)	/* First time */
		return (0);

	pos=h.head;
	if (lseek(p->fd, pos, SEEK_SET) < 0)
		return (-1);

	counter=0;
	wb.left=0;
	for (;;)
	{
		if (++counter > h.filesize / sizeof(o))
		{
			BORK(p->filename);
			return (-1);
		}

		if (h.filesize - pos < sizeof(o))
		{
			BORK(p->filename);
			errno=EIO;
			if (buf)
				free(buf);
			return (-1);	/* Sanity check */
		}

		if (buf_read(p->fd, &wb, &o, sizeof(o)) <= 0)
		{
			if (buf)
				free(buf);
			return (-1);
		}

		if (h.filesize - pos < o.my_size || o.my_size < sizeof(o))
		{
			BORK(p->filename);
			errno=EIO;
			if (buf)
				free(buf);
			return (-1);	/* Sanity check */
		}

		if (buf == NULL || bufsize < o.my_size - sizeof(o)+1)
		{
			char *newbuf;

			bufsize=o.my_size - sizeof(o)+1;

			if ((newbuf=buf ? realloc(buf, bufsize):
			     malloc(bufsize)) == NULL)
			{
				free(buf);
				return (-1);
			}
			buf=newbuf;
		}

		if (buf_read(p->fd, &wb, buf, o.my_size - sizeof(o)) <= 0)
		{
			free(buf);
			return (-1);
		}

		updateflag=0;

		rc= (*walk_func)(buf, o.my_size - sizeof(o), &updateflag, arg);

		if (updateflag && rc >= 0)
		{
			if (lseek(p->fd, pos + sizeof(o), SEEK_SET) < 0 ||
			    my_write(p->fd, buf, o.my_size - sizeof(o)) < 0)
			{
				free(buf);
				return (-1);
			}
			wb.left=0;
		}

		if (rc != 0)
		{
			free(buf);
			if (rc < 0)
				rc=1;
			return (rc);
		}

		if (pos == h.tail)
			break;

		lastpos=pos;
		pos += o.my_size;

		if (pos < h.filesize)
		{
			if (lastpos < h.tail && pos > h.tail)
			{
				BORK(p->filename);
				errno=EIO;
				free(buf);
				return (-1);
			}
		}
		else
		{
			pos=h.first;
			if (h.first < sizeof(h))
			{
				BORK(p->filename);
				free(buf);
				errno=EIO;
				return (-1);
			}

			if (lseek(p->fd, pos, SEEK_SET) < 0)
			{
				free(buf);
				return (-1);
			}
			wb.left=0;
		}
	}
	if (buf)
		free(buf);
	return (0);
}
