/* socket.cc - function implementations for shevek::socket  -*- C++ -*-
 * Copyright 2003-2005 Bas Wijnen <wijnen@debian.org>
 *
 * 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 3 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, see <http://www.gnu.org/licenses/>.
 */

#include "socket.hh"
#include "error.hh"
#include <unistd.h>

namespace shevek
{
  Glib::RefPtr <socket> socket::create (Glib::RefPtr <Glib::MainContext> main)
  {
    startfunc;
    return Glib::RefPtr <socket> (new socket (main) );
  }

  socket::socket (Glib::RefPtr <Glib::MainContext> main)
    : fd (-1, main), m_listener (false), m_name (0)
  {
    startfunc;
    // default error response is to disconnect.  This can be overridden.
    set_error (sigc::mem_fun (*this, &socket::disconnect) );
  }

  socket::~socket ()
  {
    startfunc;
    disconnect ();
  }

  // convert service to (host byte order) port
  int socket::l_service_to_port (std::string const &service)
  {
    startfunc;
    int port;
    char const *c = service.c_str ();
    struct servent *serv = getservbyname (c, "tcp");
    if (serv == 0)
      {
	char *end;
	port = strtol (c, &end, 0);
	if (*c == 0 || *end != 0)
	  shevek_error (ostring ("unable to find service %s",
				  Glib::ustring (service)));
      }
    else
      port = ntohs (serv->s_port);
    return port;
  }

	void socket::listen_tcp (std::string const &service, listen_t cb, unsigned queue)
	{
		startfunc;
		Glib::RefPtr <socket> have_reference = refptr_this <socket> ();
		disconnect ();
		struct addrinfo *result;
		struct addrinfo hints;
		memset (&hints, 0, sizeof (struct addrinfo));
		hints.ai_family = AF_UNSPEC;
		hints.ai_socktype = SOCK_STREAM;
		hints.ai_protocol = 0;
		hints.ai_flags = AI_PASSIVE;
		int ret = getaddrinfo (NULL, service.c_str (), &hints, &result);
		if (ret != 0)
		{
			shevek_error (Glib::ustring (std::string (rostring ("unable to parse tcp port %s: %s", service, gai_strerror (ret)))));
			return;
		}
		for (struct addrinfo *ai = result; ai; ai = ai->ai_next)
		{
			int filedes = ::socket (ai->ai_family, ai->ai_socktype, ai->ai_protocol);
			if (filedes < 0)
				continue;
			int opt = 1;
			setsockopt (filedes, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof (opt) );
			if (::bind (filedes, ai->ai_addr, ai->ai_addrlen) == 0)
			{
				// Success.
				freeaddrinfo (result);
				if (::listen (filedes, queue) != 0)
					shevek_error_errno (Glib::ustring (std::string (rostring ("cannot listen to bound socket for %s", service))));
				set_fd (filedes);
				m_listener = true;
				read_custom (cb);
				return;
			}
		}
		freeaddrinfo (result);
		shevek_error (Glib::ustring (std::string (rostring ("unable to open listening socket for %s", service))));
	}

	void socket::listen_avahi (std::string const &service, Glib::ustring const &protocol, Glib::ustring const &name, listen_t cb, unsigned queue)
	{
		startfunc;
		Glib::RefPtr <socket> have_reference = refptr_this <socket> ();
		listen_tcp (service, cb, queue);
		m_avahi = avahi::create (name);
		m_avahi->publish (protocol, l_service_to_port (service));
	}

  void socket::listen_unix (std::string const &file, listen_t cb,
			    unsigned queue)
  {
    startfunc;
    Glib::RefPtr <socket> have_reference = refptr_this <socket> ();
    disconnect ();
    int filedes = ::socket (PF_UNIX, SOCK_STREAM, 0);
    if (filedes < 0)
      {
	shevek_error_errno ("unable to open socket");
	return;
      }
    struct sockaddr_un *addr = reinterpret_cast <struct sockaddr_un *>
      (new char [sizeof (struct sockaddr_un) + file.size () + 1]);
    addr->sun_family = AF_UNIX;
    memcpy (addr->sun_path, file.data (), file.size () );
    addr->sun_path[file.size ()] = 0;
    if (::bind (filedes, reinterpret_cast <struct sockaddr *>(addr),
		SUN_LEN (addr) ) )
      {
	delete[] addr;
	shevek_error_errno ("unable to bind to file");
	return;
      }
    delete[] addr;
    if (::listen (filedes, queue) )
      {
	::unlink (file.c_str () );
	shevek_error_errno ("unable to listen on file");
	return;
      }
    m_listener = true;
    set_fd (filedes);
    read_custom (cb);
    m_name = new std::string (file);
  }

  void socket::listen (std::string const &port, listen_t cb, unsigned queue)
  {
    startfunc;
    Glib::RefPtr <socket> have_reference = refptr_this <socket> ();
    // discriminate unix/tcp sockets on a '/' in their name.  There may be
    // localisations where a '/' may appear in numbers.  If so, then those
    // numbers are not supported.  Also, I don't allow /'s in service names.
    //
    // avahi sockets are specified as service-or-port|name|protocol
    if (port.find ('/') != std::string::npos)
      listen_unix (port, cb, queue);
    else
      {
	std::string::size_type p = port.find ('|');
	if (p == std::string::npos)
	  listen_tcp (port, cb, queue);
	else
	  {
	    std::string block = port.substr (p + 1);
	    std::string::size_type p2 = block.find ('|');
	    if (p2 == std::string::npos)
	      {
		shevek_error ("avahi socket needs two |'s");
		return;
	      }
	    listen_avahi (port.substr (0, p), block.substr (p2 + 1), block.substr (0, p2), cb, queue);
	  }
      }
  }

	void socket::connect_tcp (std::string const &host, std::string const &service)
	{
		startfunc;
		Glib::RefPtr <socket> have_reference = refptr_this <socket> ();
		disconnect ();
		struct addrinfo *result;
		struct addrinfo hints;
		memset (&hints, 0, sizeof (struct addrinfo));
		hints.ai_family = AF_UNSPEC;
		hints.ai_socktype = SOCK_STREAM;
		hints.ai_protocol = 0;
		hints.ai_flags = AI_V4MAPPED | AI_ADDRCONFIG;
		int ret = getaddrinfo (host.empty () ? NULL : host.c_str (), service.c_str (), &hints, &result);
		if (ret != 0)
		{
			shevek_error (Glib::ustring (std::string (rostring ("unable to parse tcp address %s, port %s: %s", host, service, gai_strerror (ret)))));
			return;
		}
		for (struct addrinfo *ai = result; ai; ai = ai->ai_next)
		{
			int filedes = ::socket (ai->ai_family, ai->ai_socktype, ai->ai_protocol);
			if (filedes < 0)
				continue;
			if (::connect (filedes, ai->ai_addr, ai->ai_addrlen) >= 0)
			{
				// Success.
				set_fd (filedes);
				freeaddrinfo (result);
				return;
			}
		}
		freeaddrinfo (result);
		shevek_error (Glib::ustring (std::string (rostring ("unable to open socket to %s for %s", host, service))));
	}

	void socket::connect_avahi (avahi::browser::owner const &target, avahi::browser::details const &details)
	{
		startfunc;
		Glib::RefPtr <socket> have_reference = refptr_this <socket> ();
		if (!details.address.empty ())
			connect_tcp (std::string (details.address), rostring ("%d", target.port));
		else
		{
			for (avahi::browser::details_list::const_iterator i = target.details.begin (); i != target.details.end (); ++i)
			{
				try
				{
					connect_tcp (std::string (i->address), rostring ("%d", target.port));
					return;
				}
				catch (...)
				{
					// Ignore.
				}
			}
			shevek_error ("unable to open avahi target");
		}
	}

  void socket::connect_unix (std::string const &unix_name)
  {
    startfunc;
    Glib::RefPtr <socket> have_reference = refptr_this <socket> ();
    disconnect ();
    int filedes = ::socket (PF_UNIX, SOCK_STREAM, 0);
    if (filedes < 0)
      {
	shevek_error_errno ("unable to open socket");
      }
    struct sockaddr_un *addr = reinterpret_cast <struct sockaddr_un *>
      (new char [sizeof (struct sockaddr_un) + unix_name.size () + 1]);
    addr->sun_family = AF_UNIX;
    memcpy (addr->sun_path, unix_name.data (), unix_name.size () );
    addr->sun_path[unix_name.size ()] = 0;
    if (::connect (filedes, reinterpret_cast <struct sockaddr *>(addr),
		   SUN_LEN (addr) ) )
      {
	delete[] reinterpret_cast <char *> (addr);
	shevek_error_errno ("unable to connect");
	throw "unable to connect";
      }
    delete[] reinterpret_cast <char *> (addr);
    set_fd (filedes);
  }

	void socket::connect (std::string const &port)
	{
		startfunc;
		Glib::RefPtr <socket> have_reference = refptr_this <socket> ();
		if (port.find ('/') != std::string::npos)
		{
			connect_unix (port);
			return;
		}
		std::string::size_type p = port.find_last_of (':');
		if (p != std::string::npos)
		{
			connect_tcp (port.substr (0, p), port.substr (p + 1));
			return;
		}
		p = port.find ('|');
		if (p == std::string::npos)
		{
			connect_tcp ("", port);
			return;
		}
		std::string name = port.substr (0, p);
		std::string protocol = port.substr (p + 1);
		avahi::browser::list list = avahi::browser::get_list_block (protocol, name);
		if (list.empty ())
		{
			shevek_error (Glib::ustring (std::string (rostring ("unable to connect to avahi type %s, protocol %s: no server found", name, protocol))));
			return;
		}
		if (list.size () > 1)
		{
			shevek_error (Glib::ustring (std::string (rostring ("unable to connect to avahi type %s, protocol %s: multiple servers found", name, protocol))));
			return;
		}
		connect_tcp (std::string (list.begin ()->second.details.begin ()->address), rostring ("%d", list.begin ()->second.port));
	}

  void socket::accept (Glib::RefPtr <socket> sock)
  {
    startfunc;
    Glib::RefPtr <socket> have_reference = refptr_this <socket> ();
    int filedes = ::accept (get_fd (), NULL, NULL);
    if (filedes < 0)
      shevek_error_errno ("unable to accept connection");
    sock->set_fd (filedes);
  }

  void socket::l_finish_disconnect ()
  {
    m_disconnect ();
  }

  void socket::disconnect ()
  {
    startfunc;
    if (get_fd () < 0)
      return;
    Glib::RefPtr <socket> have_reference = refptr_this <socket> ();
    m_listener = false;
    if (m_name)
      {
	dbg ("unlinking " << m_name);
	if (::unlink (m_name->c_str ()))
	  shevek_warning_errno ("failed to unlink unix socket");
	delete m_name;
	m_name = NULL;
      }
    dbg ("possibly tried to unlink");
    write_reset ();
    ::close (get_fd ());
    set_fd (-1);
    unread (true, sigc::mem_fun (*this, &socket::l_finish_disconnect) );
  }

  std::string socket::l_get_socket_info (struct sockaddr_in *addr, bool numeric) const
  {
    startfunc;
    if (addr->sin_family != AF_INET)
      {
	shevek_error ("not an ipv4 address");
	return std::string ();
      }
    std::string port_s;
    if (!numeric)
      {
	struct servent *se = getservbyport (addr->sin_port, "tcp");
	if (se)
	  port_s = se->s_name;
      }
    if (port_s.empty () )
      port_s = rostring ("%d", ntohs (addr->sin_port));
    if (!numeric)
      {
	int size = 1000;
	char *buffer = new char[size];
	struct hostent he, *hp;
	int herr;
	while (ERANGE == gethostbyaddr_r (reinterpret_cast <char const *> (&addr->sin_addr), sizeof (addr->sin_addr), AF_INET, &he, buffer, size, &hp, &herr) )
	  {
	    delete[] buffer;
	    buffer = new char[size *= 2];
	  }
	if (hp)
	  {
	    std::string ret = rostring ("%s:%s", hp->h_name, port_s);
	    delete[] buffer;
	    return ret;
	  }
      }
    unsigned char const *hack = reinterpret_cast <unsigned char const *> (&addr->sin_addr);
    return rostring ("%d.%d.%d.%d:%s", int (hack[0]), int (hack[1]), int (hack[2]), int (hack[3]), port_s);
  }

  std::string socket::get_peer_info (bool numeric) const
  {
    startfunc;
    struct sockaddr_in sock;
    socklen_t len = sizeof (sock);
    if (getpeername (get_fd (), reinterpret_cast <struct sockaddr *> (&sock),
		     &len) )
      {
	shevek_error_errno ("unable to get peer name");
	return std::string ();
      }
    return l_get_socket_info (&sock, numeric);
  }

  std::string socket::get_own_info (bool numeric) const
  {
    startfunc;
    struct sockaddr_in sock;
    socklen_t len = sizeof (sock);
    if (getsockname (get_fd (), reinterpret_cast <struct sockaddr *> (&sock),
		     &len) )
      {
	shevek_error_errno ("unable to get peer name");
	return std::string ();
      }
    return l_get_socket_info (&sock, numeric);
  }

  socket::disconnect_t socket::signal_disconnect ()
  {
    startfunc;
    return m_disconnect;
  }
}
