/* af_unix.c
 * (c) 2002 Mikulas Patocka
 * This file is a part of the Links program, released under GPL
 */

#include "links.h"

#ifndef USE_AF_UNIX

int s_unix_fd = -1;

int bind_to_af_unix(unsigned char *name)
{
	return -1;
}

void af_unix_close(void)
{
}

#else

#ifdef OS2
#ifndef HAVE_SYS_UN_H
#define HAVE_SYS_UN_H
#endif
struct os2_sockaddr_un {
	u_short sun_family;
	char sun_path[108];
};
#define sockaddr_un	os2_sockaddr_un
#ifndef AF_UNIX
#define AF_UNIX	1
#endif
#ifndef PF_UNIX
#define PF_UNIX	1
#endif
#elif defined(HAVE_SYS_UN_H)
#include <sys/un.h>
#endif

#if defined(__GNU__)
#define SOCKET_TIMEOUT_HACK
#endif

static void af_unix_connection(void *);

#define ADDR_SIZE	4096

union address {
	struct sockaddr s;
#ifdef HAVE_SYS_UN_H
	struct sockaddr_un suni;
#endif
	struct sockaddr_in sin;
	unsigned char buffer[ADDR_SIZE];
};

static union address s_unix;
static union address s_unix_acc;

static socklen_t s_unix_l;
int s_unix_fd = -1;
static int s_unix_master = 0;


#define S2C1_HANDSHAKE_LENGTH	6
#define C2S2_HANDSHAKE_LENGTH	sizeof(struct links_handshake)
#define S2C3_HANDSHAKE_LENGTH	sizeof(struct links_handshake)

static struct links_handshake {
	unsigned char version[30];
	unsigned char system_name[32];
	unsigned char system_id;
	unsigned char sizeof_long;
} links_handshake;

#define HANDSHAKE_WRITE(hndl, sz)					\
	if ((r = hard_write(hndl, (unsigned char *)&links_handshake, sz)) != (sz))
#define HANDSHAKE_READ(hndl, sz)					\
	if ((r = hard_read(hndl, (unsigned char *)&received_handshake, sz)) != (sz) || memcmp(&received_handshake, &links_handshake, sz))


#ifdef OS2
static inline int have_pf_unix(void)
{
	s_unix_fd = c_socket(PF_UNIX, SOCK_STREAM, 0);
	return s_unix_fd >= 0;
}
#endif

#if !defined(HAVE_SYS_UN_H) || defined(OS2)
static inline int get_address_localhost(unsigned char *name)
{
	unsigned short port;
	if (!name) {
		port = LINKS_PORT;
	} else if (!strcmp(cast_const_char name, "pmshell")) {
		port = LINKS_PORT + 2;
	} else {
		port = LINKS_PORT;
		while (*name) {
			port = 257 * port + (*name * 2);
			name++;
		}
		port %= LINKS_G_PORT_LEN;
		port += LINKS_G_PORT_START;
		port &= ~1;
	}
	if (anonymous) {
		port++;
	}
	memset(&s_unix, 0, sizeof s_unix);
	s_unix.sin.sin_family = AF_INET;
	s_unix.sin.sin_port = htons(port);
	s_unix.sin.sin_addr.s_addr = htonl(0x7f000001);
	s_unix_l = sizeof(struct sockaddr_in);
	/*debug("get address %d", port);*/
	return PF_INET;
}
#endif

static int get_address(unsigned char *name)
{
#ifdef HAVE_SYS_UN_H
	unsigned char *path;
#ifdef OS2
	if (!have_pf_unix()) {
		return get_address_localhost(name);
	}
	path = stracpy(cast_uchar "\\socket\\links\\");
#else
	if (!links_home) return -1;
	path = stracpy(links_home);
#endif
	add_to_strn(&path, cast_uchar LINKS_SOCK_NAME);
	if (anonymous) {
		add_to_strn(&path, cast_uchar LINKS_ANONYMOUS_SOCK_SUFFIX);
	}
	if (name) {
		unsigned char *n = stracpy(name);
		check_filename(&n);
		add_to_strn(&path, cast_uchar "-");
		add_to_strn(&path, n);
		mem_free(n);
	}
#ifdef OS2
	s_unix_l = sizeof(struct sockaddr_un);
#else
	s_unix_l = (socklen_t)((unsigned char *)&s_unix.suni.sun_path - (unsigned char *)&s_unix.suni + strlen(cast_const_char path) + 1);
#endif
	if (strlen(cast_const_char path) > sizeof(union address) || (size_t)s_unix_l > sizeof(union address)) {
		mem_free(path);
		return -1;
	}
	memset(&s_unix, 0, sizeof s_unix);
	s_unix.suni.sun_family = AF_UNIX;
	strcpy(cast_char s_unix.suni.sun_path, cast_const_char path);
	mem_free(path);
	return PF_UNIX;
#else
	return get_address_localhost(name);
#endif
}

static void unlink_unix(void)
{
#if defined(HAVE_SYS_UN_H) && !defined(OS2)
	if (s_unix.s.sa_family == AF_UNIX) {
		int rs;
		/*debug("unlink: %s", s_unix.suni.sun_path);*/
		EINTRLOOP(rs, unlink(s_unix.suni.sun_path));
		if (rs) {
			/*perror("unlink");*/
		}
	}
#endif
}

int bind_to_af_unix(unsigned char *name)
{
	int u = 0;
	int a1 = 1;
	int cnt = 0;
	int af;
	int r;
	int rs;
	struct links_handshake received_handshake;
	memset(&links_handshake, 0, sizeof links_handshake);
	safe_strncpy(links_handshake.version, cast_uchar("Links " VERSION_STRING), sizeof links_handshake.version);
	safe_strncpy(links_handshake.system_name, system_name, sizeof links_handshake.system_name);
	links_handshake.system_id = SYSTEM_ID;
	links_handshake.sizeof_long = sizeof(long);
	if ((af = get_address(name)) == -1) {
		close_socket(&s_unix_fd);
		return -1;
	}
	again:
	if (s_unix_fd == -1) {
		s_unix_fd = c_socket(af, SOCK_STREAM, 0);
		if (s_unix_fd == -1) return -1;
	}
#if defined(SOL_SOCKET) && defined(SO_REUSEADDR)
	EINTRLOOP(rs, setsockopt(s_unix_fd, SOL_SOCKET, SO_REUSEADDR, (void *)&a1, sizeof a1));
#endif
	EINTRLOOP(rs, bind(s_unix_fd, &s_unix.s, s_unix_l));
	if (rs) {
		/*debug("bind: %d, %s", errno, strerror(errno));*/
		if (af == PF_INET && errno == EADDRNOTAVAIL) {
			/* do not try to connect if the user has not configured loopback interface */
			EINTRLOOP(rs, close(s_unix_fd));
			return -1;
		}
		EINTRLOOP(rs, close(s_unix_fd));
		s_unix_fd = c_socket(af, SOCK_STREAM, 0);
		if (s_unix_fd == -1) return -1;
#if defined(SOL_SOCKET) && defined(SO_REUSEADDR)
		EINTRLOOP(rs, setsockopt(s_unix_fd, SOL_SOCKET, SO_REUSEADDR, (void *)&a1, sizeof a1));
#endif
		EINTRLOOP(rs, connect(s_unix_fd, &s_unix.s, s_unix_l));
		if (rs) {
retry:
			/*debug("connect: %d, %s", errno, strerror(errno));*/
			if (++cnt < MAX_BIND_TRIES) {
				portable_sleep(100);
				EINTRLOOP(rs, close(s_unix_fd));
				s_unix_fd = -1;
				goto again;
			}
#ifdef SOCKET_TIMEOUT_HACK
retry_unlink:
#endif
			EINTRLOOP(rs, close(s_unix_fd));
			s_unix_fd = -1;
			if (!u) {
				unlink_unix();
				u = 1;
				goto again;
			}
			return -1;
		}
#ifdef SOCKET_TIMEOUT_HACK
		if (!can_read_timeout(s_unix_fd, AF_UNIX_SOCKET_TIMEOUT))
			goto retry_unlink;
#endif
		HANDSHAKE_READ(s_unix_fd, S2C1_HANDSHAKE_LENGTH) {
			if (r != S2C1_HANDSHAKE_LENGTH) goto retry;
			goto close_and_fail;
		}
		HANDSHAKE_WRITE(s_unix_fd, C2S2_HANDSHAKE_LENGTH)
			goto close_and_fail;
		HANDSHAKE_READ(s_unix_fd, S2C3_HANDSHAKE_LENGTH)
			goto close_and_fail;
		return s_unix_fd;
	}
	EINTRLOOP(rs, listen(s_unix_fd, 100));
	if (rs) {
		error("ERROR: listen failed: %d", errno);
		close_and_fail:
		EINTRLOOP(rs, close(s_unix_fd));
		s_unix_fd = -1;
		return -1;
	}
	s_unix_master = 1;
	set_handlers(s_unix_fd, af_unix_connection, NULL, NULL);
	return -1;
}

static void af_unix_connection(void *xxx)
{
	socklen_t l = s_unix_l;
	int ns;
	int r;
	int rs;
	struct links_handshake received_handshake;
	memset(&s_unix_acc, 0, sizeof s_unix_acc);
	ns = c_accept(s_unix_fd, &s_unix_acc.s, &l);
	if (ns == -1) return;
	HANDSHAKE_WRITE(ns, S2C1_HANDSHAKE_LENGTH) {
		EINTRLOOP(rs, close(ns));
		return;
	}
	HANDSHAKE_READ(ns, C2S2_HANDSHAKE_LENGTH) {
		portable_sleep(100);	/* workaround for a race in previous Links version */
		EINTRLOOP(rs, close(ns));
		return;
	}
	HANDSHAKE_WRITE(ns, S2C3_HANDSHAKE_LENGTH) {
		EINTRLOOP(rs, close(ns));
		return;
	}
	if (!F) {
		init_term(ns, ns, win_func);
		set_highpri();
	}
#ifdef G
	else {
		gfx_connection(ns);
	}
#endif
}

void af_unix_close(void)
{
	close_socket(&s_unix_fd);
	if (s_unix_master) {
		unlink_unix();
		s_unix_master = 0;
	}
}

#endif