/* This file is part of GNU Pies.
   Copyright (C) 2019-2025 Sergey Poznyakoff

   GNU Pies 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, or (at your option)
   any later version.

   GNU Pies 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 GNU Pies.  If not, see <http://www.gnu.org/licenses/>. */

#include <config.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <sys/stat.h>
#include <netinet/in.h>
#include <netdb.h>
#include <errno.h>
#include <unistd.h>
#include <fcntl.h>
#include <libpies.h>
#include <grecs.h>

int
url_connect (struct pies_url *url, struct grecs_sockaddr *source_addr)
{
  int fd;
  union pies_sockaddr_storage addr;
  socklen_t socklen;
  int flags;
  
  if (strcmp (url->scheme, "unix") == 0
      || strcmp (url->scheme, "file") == 0
      || strcmp (url->scheme, "socket") == 0)
    {
      struct stat st;
		
      if (url->port)
	{
	  grecs_error (NULL, 0, _("%s: invalid connection type: "
				  "port is meaningless for UNIX sockets"),
		       url->string);
	  errno = EINVAL;
	  return -1;
	}
		
      if (strlen (url->path) > sizeof addr.s_un.sun_path)
	{
	  grecs_error (NULL, 0,
		       _("%s: UNIX socket name too long"),
		       url->path);
	  errno = EINVAL;
	  return -1;
	}
		
      addr.s.sa_family = PF_UNIX;
      socklen = sizeof (addr.s_un);
      strcpy (addr.s_un.sun_path, url->path);
		
      if (stat (url->path, &st))
	{
	  if (errno != ENOENT)
	    {
	      grecs_error (NULL, errno, _("%s: %s failed"), url->path, "stat");
	      return -1;
	    }
	}
      else
	{
	  if (!S_ISSOCK (st.st_mode))
	    {
	      grecs_error (NULL, 0, _("%s: not a socket"), url->path);
	      errno = EINVAL;
	      return -1;
	    }
	}
    }
  else if (strcmp (url->scheme, "inet") == 0)
    {
      struct addrinfo hints;
      struct addrinfo *res;
      int rc;
      
      memset (&hints, 0, sizeof (hints));
      hints.ai_socktype = SOCK_STREAM;
      hints.ai_flags = 0;
      hints.ai_family = AF_INET;

      rc = getaddrinfo (url->host, url->port_s, &hints, &res);
      switch (rc)
	{
	case 0:
	  break;
	  
	case EAI_SYSTEM:
	  grecs_error (NULL, errno, "%s", _("cannot parse address"));
	  return -1;
	  
	case EAI_BADFLAGS:
	case EAI_SOCKTYPE:
	  grecs_error (NULL, 0,
		       _("%s:%d: internal error while converting network address"),
		       __FILE__,__LINE__);
	  return -1;
	  
	case EAI_MEMORY:
	  grecs_alloc_die ();
	  
	default:
	  grecs_error (NULL, 0, "getaddrinfo: %s", gai_strerror(rc));
	  return -1;
	}

      socklen = sizeof (addr.s_in);
      memcpy (&addr.s_in, res->ai_addr, socklen);
      freeaddrinfo (res);
    }
      
  fd = socket (addr.s.sa_family, SOCK_STREAM, 0);
  if (fd == -1)
    {
      grecs_error (NULL, errno, _("%s: %s failed"), url->string, "socket");
      return -1;
    }
  
  if ((flags = fcntl (fd, F_GETFD, 0)) == -1
      || fcntl (fd, F_SETFD, flags | FD_CLOEXEC) == -1)
    grecs_error (NULL, 0,  _("%s: cannot set close-on-exec: %s"),
		 url->string, strerror (errno));
  
  if (source_addr)
    {
      if (source_addr->sa->sa_family != addr.s.sa_family)
	grecs_error (NULL, 0,
		     _("source and destination address family differ"));
      else if (bind (fd, source_addr->sa, source_addr->len) < 0)
	{
	  grecs_error (NULL, errno, _("%s: %s failed"), url->string, "bind");
	  close (fd);
	  return -1;
	}
    }
  
  if (connect (fd, &addr.s, socklen))
    {
      grecs_error (NULL, errno, _("%s: %s failed"), url->string, "connect");
      close (fd);
      return -1;
    }

  return fd;
}
