/**
 * This program takes a URL as an argument, downloads its contents over HTTP,
 * and writes it to stdout.  It's fairly standard UNIX code, so it should work
 * on most operating systems, but it targets z/OS.  TLS (HTTPS) connections are
 * supported on z/OS.  The goal here is to only depend on the system C library
 * (unless using a crypto library) for building on limited systems.
**/

#define _POSIX_C_SOURCE 200112L
#define _XOPEN_SOURCE_EXTENDED 1
#include <netdb.h>
#include <stdio.h>
#include <string.h>
#include <strings.h>
#include <unistd.h>

#ifdef ENABLE_GSKSSL
# include <gskssl.h>
#endif

#define HOST_LENGTH 256
#define SERVICE_LENGTH 256

struct connection_info {
  char ssl; /* boolean */
  int fd;
#ifdef ENABLE_GSKSSL
  gsk_handle gskenv;
  gsk_handle secure_socket;
#endif
};

static int
parse_url(const char *url, char *host, char *port, char **path, char *ssl)
{
  const char *end, *start;

  if (strncasecmp (url, "http://", 7) == 0)
    *ssl = 0;
  else if (strncasecmp (url, "https://", 8) == 0)
    *ssl = 1;
  else
    {
      fprintf (stderr, "Only URLs for HTTP and HTTPS requests are allowed\n");
      return -1;
    }

  start = url + 7 + *ssl;
  end = start + strcspn (start, "/:");

  if (start == end)
    {
      fprintf (stderr, "The URL is missing its host component\n");
      return -2;
    }
  else if (end - start > HOST_LENGTH - 1)
    {
      fprintf (stderr, "The URL host component is too long\n");
      return -3;
    }

  memcpy (host, start, end - start);
  host[end - start] = '\0';

  if (*end == ':')
    {
      start = end + 1;
      end = start + strcspn (start, "/");

      if (start == end)
        {
          fprintf (stderr, "The URL is missing its port component\n");
          return -4;
        }
      else if (end - start > SERVICE_LENGTH - 1)
        {
          fprintf (stderr, "The URL port component is too long\n");
          return -5;
        }

      memcpy (port, start, end - start);
      port[end - start] = '\0';
    }
  else
    if (*ssl) { port[0] = '4'; port[1] = '4'; port[2] = '3'; port[3] = '\0'; }
    else      { port[0] = '8'; port[1] = '0'; port[2] = '\0'; }

  if (*end != '/')
    {
      fprintf (stderr, "The URL is missing its path component\n");
      return -6;
    }

  *path = (char *)end;

  return 0;
}

static int
open_socket_to_host(const char *host, const char *service)
{
  struct addrinfo hints, *info, *results;
  int fd, status;

  /* Allow either IPv4 or IPv6, and specify a stream socket (TCP). */
  memset (&hints, 0, sizeof(hints));
  hints.ai_family = AF_UNSPEC;
  hints.ai_socktype = SOCK_STREAM;

  /* Retrieve information for the given host/service. */
  status = getaddrinfo (host, service, &hints, &results);
  if (status != 0)
    {
      fprintf (stderr, "getaddrinfo: %s\n", gai_strerror (status));
      return -1;
    }

  /* Loop through the returned addresses until one successfully connects. */
  for (info = results; info != NULL; info = info->ai_next)
    {
      fd = socket (info->ai_family, info->ai_socktype, info->ai_protocol);
      if (fd == -1)
        continue;

      if (connect (fd, info->ai_addr, info->ai_addrlen) != -1)
        break;

      close (fd);
    }

  /* Release the memory allocated from resolving hosts. */
  freeaddrinfo (results);

  /* Abort if the loop was exhausted without completing a connection. */
  if (info == NULL)
    {
      fprintf (stderr, "Could not connect to a host\n");
      return -2;
    }

  /* Pass back the socket file descriptor. */
  return fd;
}

#ifdef ENABLE_GSKSSL
static int
open_socket_via_gskssl(struct connection_info *conn)
{
  gsk_handle *gskenv = &conn->gskenv;
  gsk_handle *secure_socket = &conn->secure_socket;
  int fd = conn->fd, rc;

  /* Create the TLS environment. */
  rc = gsk_environment_open (gskenv);
  if (rc != GSK_OK)
  {
    fprintf (stderr, "Could not create the TLS environment\n");
    gsk_environment_close (gskenv);
    return 1;
  }

  /* Set up a TLS client. */
  gsk_attribute_set_enum (*gskenv, GSK_SESSION_TYPE, GSK_CLIENT_SESSION);
#define protocol(p, s) \
    gsk_attribute_set_enum (*gskenv, GSK_PROTOCOL_##p, GSK_PROTOCOL_##p##_##s)
  protocol(SSLV2,   OFF);
  protocol(SSLV3,   OFF);
  protocol(TLSV1,   ON);
  protocol(TLSV1_1, ON);
  protocol(TLSV1_2, ON);
#undef protocol

  /* Look for a keystore in the current directory. */
  gsk_attribute_set_buffer (*gskenv, GSK_KEYRING_FILE, "key.kdb", 0);
  gsk_attribute_set_buffer (*gskenv, GSK_KEYRING_PW, "password", 0);

  /* Initialize the TLS environment. */
  rc = gsk_environment_init (*gskenv);
  if (rc != GSK_OK)
  {
    fprintf (stderr, "Could not initialize the TLS environment\n");
    gsk_environment_close (gskenv);
    return 2;
  }

  /* Create a secure socket. */
  rc = gsk_secure_socket_open (*gskenv, secure_socket);
  if (rc != GSK_OK)
  {
    fprintf (stderr, "Could not create a secure socket\n");
    gsk_environment_close (gskenv);
    return 3;
  }

  /* Run the TLS session on the given socket descriptor. */
  gsk_attribute_set_numeric_value (*secure_socket, GSK_FD, fd);

  /* Perform the handshake. */
  rc = gsk_secure_socket_init (*secure_socket);
  if (rc != GSK_OK)
  {
    fprintf (stderr, "Could not create a secure socket\n");
    gsk_environment_close (gskenv);
    return 4;
  }

  return 0;
}
#endif

static int
connection_connect(struct connection_info *conn,
                   const char *host, const char *service)
{
  int rc;

  rc = open_socket_to_host (host, service);
  if (rc < 0)
    return -1;
  conn->fd = rc;

  if (conn->ssl)
    {
#ifdef ENABLE_GSKSSL
      return open_socket_via_gskssl (conn);
#else
      fprintf (stderr, "HTTPS is not supported on this platform\n");
      return -2;
#endif
    }

  return 0;
}

static int
connection_send(struct connection_info *conn, char *buf, size_t size)
{
  int len;

  if (conn->ssl)
#ifdef ENABLE_GSKSSL
    gsk_secure_socket_write (conn->secure_socket, buf, (int)size, &len);
#else
    return -1;
#endif
  else
    len = (int)write (conn->fd, buf, size);

  return len;
}

static int
connection_recv(struct connection_info *conn, char *buf, size_t size)
{
  int len;

  if (conn->ssl)
#ifdef ENABLE_GSKSSL
    gsk_secure_socket_read (conn->secure_socket, buf, (int)size, &len);
#else
    return -1;
#endif
  else
    len = (int)read (conn->fd, buf, size);

  return len;
}

static int
connection_disconnect(struct connection_info *conn)
{
  if (conn->ssl)
    {
#ifdef ENABLE_GSKSSL
      gsk_secure_socket_close (&conn->secure_socket);
      gsk_environment_close (&conn->gskenv);
#endif
      conn->ssl = 0;
    }

  close (conn->fd);

  return 0;
}

int
main(int argc, char **argv)
{
  struct connection_info connection;
  char host[HOST_LENGTH], port[SERVICE_LENGTH], *path;
  char buf[4096], *p = NULL, skip_headers = 1;
  ssize_t count;

  if (argc != 2)
    {
      fprintf (stderr, "Usage: %s http[s]://<host>[:<port>]/<path>\n", *argv);
      return 1;
    }

  if (parse_url (argv[1], host, port, &path, &connection.ssl) < 0)
    return 2;

  if (connection_connect (&connection, host, port) != 0)
    return 3;

  count = snprintf (buf, (size_t)sizeof(buf),
                    "GET %s HTTP/1.1\r\nHost: %s\r\n\r\n", path, host);
#ifdef __MVS__
# ifdef __XPLINK__
  if (__e2a_l (buf, (size_t)count) != (size_t)count)
    return 4;
# else
#  error "Add the compiler option -qXPLINK to support communicating in ASCII."
# endif
#endif
  connection_send (&connection, buf, count);

  while ((count = connection_recv (&connection, buf, (size_t)sizeof(buf))) > 0)
    {
      if (skip_headers)
        {
          p = strstr (buf, "\x0D\x0A\x0D\x0A"); /* ASCII \r\n\r\n */
          if (p != NULL)
            {
              p += 4;
              count -= p - buf;
              skip_headers = 0;
            }
        }

      write (1, p == NULL ? buf : p, (size_t)count);
      p = NULL;
    }

  connection_disconnect (&connection);

  return 0;
}