netcmd plugin: Implemented a fgets(3)-like wrapper around gnutls_record_recv(3).
[collectd.git] / src / netcmd.c
index 4f93d2e..d5a2909 100644 (file)
 
 #include <grp.h>
 
-#define NC_DEFAULT_PORT "25826"
+#include <gnutls/gnutls.h>
+
+#define NC_DEFAULT_SERVICE "25826"
+#define NC_TLS_DH_BITS 1024
 
 /*
  * Private data structures
@@ -55,10 +58,46 @@ struct nc_peer_s
 {
   char *node;
   char *service;
-  int fd;
+  int *fds;
+  size_t fds_num;
+
+  char *tls_cert_file;
+  char *tls_key_file;
+  char *tls_ca_file;
+  char *tls_crl_file;
+  _Bool tls_verify_peer;
+
+  gnutls_certificate_credentials_t tls_credentials;
+  gnutls_dh_params_t tls_dh_params;
+  gnutls_priority_t tls_priority;
+
 };
 typedef struct nc_peer_s nc_peer_t;
 
+#if defined(PAGESIZE)
+# define NC_READ_BUFFER_SIZE PAGESIZE
+#elif defined(PAGE_SIZE)
+# define NC_READ_BUFFER_SIZE PAGE_SIZE
+#else
+# define NC_READ_BUFFER_SIZE 4096
+#endif
+
+struct nc_connection_s
+{
+  /* TLS fields */
+  int fd;
+  char *read_buffer;
+  size_t read_buffer_fill;
+
+  /* non-TLS fields */
+  FILE *fh_in;
+  FILE *fh_out;
+
+  gnutls_session_t tls_session;
+  _Bool have_tls_session;
+};
+typedef struct nc_connection_s nc_connection_t;
+
 /*
  * Private variables
  */
@@ -77,28 +116,115 @@ static pthread_t listen_thread;
 /*
  * Functions
  */
-static int nc_register_fd (int fd, const char *path) /* {{{ */
+static nc_peer_t *nc_fd_to_peer (int fd) /* {{{ */
 {
-  struct pollfd *tmp;
+  size_t i;
 
-  tmp = realloc (pollfd, (pollfd_num + 1) * sizeof (*pollfd));
-  if (tmp == NULL)
+  for (i = 0; i < peers_num; i++)
+  {
+    size_t j;
+
+    for (j = 0; j < peers[i].fds_num; j++)
+      if (peers[i].fds[j] == fd)
+        return (peers + i);
+  }
+
+  return (NULL);
+} /* }}} nc_peer_t *nc_fd_to_peer */
+
+static int nc_register_fd (nc_peer_t *peer, int fd) /* {{{ */
+{
+  struct pollfd *poll_ptr;
+  int *fd_ptr;
+
+  poll_ptr = realloc (pollfd, (pollfd_num + 1) * sizeof (*pollfd));
+  if (poll_ptr == NULL)
   {
     ERROR ("netcmd plugin: realloc failed.");
     return (-1);
   }
-  pollfd = tmp;
+  pollfd = poll_ptr;
 
   memset (&pollfd[pollfd_num], 0, sizeof (pollfd[pollfd_num]));
   pollfd[pollfd_num].fd = fd;
   pollfd[pollfd_num].events = POLLIN | POLLPRI;
   pollfd[pollfd_num].revents = 0;
-
   pollfd_num++;
 
+  if (peer == NULL)
+    return (0);
+
+  fd_ptr = realloc (peer->fds, (peer->fds_num + 1) * sizeof (*peer->fds));
+  if (fd_ptr == NULL)
+  {
+    ERROR ("netcmd plugin: realloc failed.");
+    return (-1);
+  }
+  peer->fds = fd_ptr;
+  peer->fds[peer->fds_num] = fd;
+  peer->fds_num++;
+
   return (0);
 } /* }}} int nc_register_fd */
 
+static int nc_tls_init (nc_peer_t *peer) /* {{{ */
+{
+  if (peer == NULL)
+    return (EINVAL);
+
+  if ((peer->tls_cert_file == NULL)
+      || (peer->tls_key_file == NULL))
+    return (0);
+
+  /* Initialize the structure holding our certificate information. */
+  gnutls_certificate_allocate_credentials (&peer->tls_credentials);
+
+  /* Set up the configured certificates. */
+  if (peer->tls_ca_file != NULL)
+    gnutls_certificate_set_x509_trust_file (peer->tls_credentials,
+        peer->tls_ca_file, GNUTLS_X509_FMT_PEM);
+  if (peer->tls_crl_file != NULL)
+      gnutls_certificate_set_x509_crl_file (peer->tls_credentials,
+          peer->tls_crl_file, GNUTLS_X509_FMT_PEM);
+  gnutls_certificate_set_x509_key_file (peer->tls_credentials,
+      peer->tls_cert_file, peer->tls_key_file, GNUTLS_X509_FMT_PEM);
+
+  /* Initialize Diffie-Hellman parameters. */
+  gnutls_dh_params_init (&peer->tls_dh_params);
+  gnutls_dh_params_generate2 (peer->tls_dh_params, NC_TLS_DH_BITS);
+  gnutls_certificate_set_dh_params (peer->tls_credentials,
+      peer->tls_dh_params);
+
+  /* Initialize a "priority cache". This will tell GNUTLS which algorithms to
+   * use and which to avoid. We use the "NORMAL" method for now. */
+  gnutls_priority_init (&peer->tls_priority,
+     /* priority = */ "NORMAL", /* errpos = */ NULL);
+
+  return (0);
+} /* }}} int nc_tls_init */
+
+static gnutls_session_t nc_tls_get_session (nc_peer_t *peer) /* {{{ */
+{
+  gnutls_session_t session;
+
+  if (peer->tls_credentials == NULL)
+    return (NULL);
+
+  /* Initialize new session. */
+  gnutls_init (&session, GNUTLS_SERVER);
+
+  /* Set cipher priority and credentials based on the information stored with
+   * the peer. */
+  gnutls_priority_set (session, peer->tls_priority);
+  gnutls_credentials_set (session,
+      GNUTLS_CRD_CERTIFICATE, peer->tls_credentials);
+
+  /* Request the client certificate. */
+  gnutls_certificate_server_set_request (session, GNUTLS_CERT_REQUEST);
+
+  return (session);
+} /* }}} gnutls_session_t nc_tls_get_session */
+
 static int nc_open_socket (nc_peer_t *peer) /* {{{ */
 {
   struct addrinfo ai_hints;
@@ -116,7 +242,7 @@ static int nc_open_socket (nc_peer_t *peer) /* {{{ */
   }
 
   if (service == NULL)
-    service = NC_DEFAULT_PORT;
+    service = NC_DEFAULT_SERVICE;
 
   memset (&ai_hints, 0, sizeof (ai_hints));
 #ifdef AI_PASSIVE
@@ -131,7 +257,7 @@ static int nc_open_socket (nc_peer_t *peer) /* {{{ */
   ai_list = NULL;
 
   if (service == NULL)
-    service = NC_DEFAULT_PORT;
+    service = NC_DEFAULT_SERVICE;
 
   status = getaddrinfo (node, service, &ai_hints, &ai_list);
   if (status != 0)
@@ -173,7 +299,7 @@ static int nc_open_socket (nc_peer_t *peer) /* {{{ */
       continue;
     }
 
-    status = nc_register_fd (fd, /* path = */ NULL);
+    status = nc_register_fd (peer, fd);
     if (status != 0)
     {
       close (fd);
@@ -183,45 +309,227 @@ static int nc_open_socket (nc_peer_t *peer) /* {{{ */
 
   freeaddrinfo (ai_list);
 
-  return (0);
+  return (nc_tls_init (peer));
 } /* }}} int nc_open_socket */
 
-static void *nc_handle_client (void *arg) /* {{{ */
+static void nc_connection_close (nc_connection_t *conn) /* {{{ */
 {
-  int fd;
-  FILE *fhin, *fhout;
+  if (conn == NULL)
+    return;
+
+  if (conn->fd >= 0)
+  {
+    close (conn->fd);
+    conn->fd = -1;
+  }
+
+  if (conn->fh_in != NULL)
+  {
+    fclose (conn->fh_in);
+    conn->fh_in = NULL;
+  }
+
+  if (conn->fh_out != NULL)
+  {
+    fclose (conn->fh_out);
+    conn->fh_out = NULL;
+  }
+
+  if (conn->have_tls_session)
+  {
+    gnutls_deinit (conn->tls_session);
+    conn->have_tls_session = 0;
+  }
+
+  sfree (conn);
+} /* }}} void nc_connection_close */
+
+static int nc_connection_init (nc_connection_t *conn) /* {{{ */
+{
+  int fd_copy;
   char errbuf[1024];
 
-  fd = *((int *) arg);
-  sfree (arg);
+  if (conn->have_tls_session)
+  {
+    conn->read_buffer = malloc (NC_READ_BUFFER_SIZE);
+    if (conn->read_buffer == NULL)
+      return (ENOMEM);
+    memset (conn->read_buffer, 0, NC_READ_BUFFER_SIZE);
+
+    gnutls_transport_set_ptr (conn->tls_session, &conn->fd);
+    return (0);
+  }
 
-  DEBUG ("netcmd plugin: nc_handle_client: Reading from fd #%i", fd);
+  /* Duplicate the file descriptor. We need two file descriptors, because we
+   * create two FILE* objects. If they pointed to the same FD and we called
+   * fclose() on each, that would call close() twice on the same FD. If
+   * another file is opened in between those two calls, it could get assigned
+   * that FD and weird stuff would happen. */
+  fd_copy = dup (conn->fd);
+  if (fd_copy < 0)
+  {
+    ERROR ("netcmd plugin: dup(2) failed: %s",
+        sstrerror (errno, errbuf, sizeof (errbuf)));
+    return (-1);
+  }
 
-  fhin  = fdopen (fd, "r");
-  if (fhin == NULL)
+  conn->fh_in  = fdopen (conn->fd, "r");
+  if (conn->fh_in == NULL)
   {
     ERROR ("netcmd plugin: fdopen failed: %s",
         sstrerror (errno, errbuf, sizeof (errbuf)));
-    close (fd);
-    pthread_exit ((void *) 1);
+    return (-1);
   }
+  /* Prevent other code from using the FD directly. */
+  conn->fd = -1;
 
-  fhout = fdopen (fd, "w");
-  if (fhout == NULL)
+  conn->fh_out = fdopen (fd_copy, "w");
+  /* Prevent nc_connection_close from calling close(2) on this fd. */
+  if (conn->fh_out == NULL)
   {
     ERROR ("netcmd plugin: fdopen failed: %s",
         sstrerror (errno, errbuf, sizeof (errbuf)));
-    fclose (fhin); /* this closes fd as well */
-    pthread_exit ((void *) 1);
+    return (-1);
   }
 
   /* change output buffer to line buffered mode */
-  if (setvbuf (fhout, NULL, _IOLBF, 0) != 0)
+  if (setvbuf (conn->fh_out, NULL, _IOLBF, 0) != 0)
   {
     ERROR ("netcmd plugin: setvbuf failed: %s",
         sstrerror (errno, errbuf, sizeof (errbuf)));
-    fclose (fhin);
-    fclose (fhout);
+    nc_connection_close (conn);
+    return (-1);
+  }
+
+  return (0);
+} /* }}} int nc_connection_init */
+
+static char *nc_connection_gets (nc_connection_t *conn, /* {{{ */
+    char *buffer, size_t buffer_size)
+{
+  ssize_t status;
+  char *orig_buffer = buffer;
+
+  if (conn == NULL)
+  {
+    errno = EINVAL;
+    return (NULL);
+  }
+
+  if (!conn->have_tls_session)
+    return (fgets (buffer, (int) buffer_size, conn->fh_in));
+
+  if ((buffer == NULL) || (buffer_size < 2))
+  {
+    errno = EINVAL;
+    return (NULL);
+  }
+
+  /* ensure null termination */
+  memset (buffer, 0, buffer_size);
+  buffer_size--;
+
+  while (42)
+  {
+    size_t max_copy_bytes;
+    size_t newline_pos;
+    _Bool found_newline;
+    size_t i;
+
+    /* If there's no more data in the read buffer, read another chunk from the
+     * socket. */
+    if (conn->read_buffer_fill < 1)
+    {
+      status = gnutls_record_recv (conn->tls_session,
+          conn->read_buffer, NC_READ_BUFFER_SIZE);
+      if (status < 0) /* error */
+      {
+        ERROR ("netcmd plugin: Error while reading from TLS stream.");
+        return (NULL);
+      }
+      else if (status == 0) /* we reached end of file */
+      {
+        if (orig_buffer == buffer) /* nothing has been written to the buffer yet */
+          return (NULL); /* end of file */
+        else
+          return (orig_buffer);
+      }
+      else
+      {
+        conn->read_buffer_fill = (size_t) status;
+      }
+    }
+    assert (conn->read_buffer_fill > 0);
+
+    /* Determine where the first newline character is in the buffer. We're not
+     * using strcspn(3) here, becaus the buffer is possibly not
+     * null-terminated. */
+    newline_pos = conn->read_buffer_fill;
+    found_newline = 0;
+    for (i = 0; i < conn->read_buffer_fill; i++)
+    {
+      if (conn->read_buffer[i] == '\n')
+      {
+        newline_pos = i;
+        found_newline = 1;
+        break;
+      }
+    }
+
+    /* Determine how many bytes to copy at most. This is MIN(buffer available,
+     * read buffer size, characters to newline). */
+    max_copy_bytes = buffer_size;
+    if (max_copy_bytes > conn->read_buffer_fill)
+      max_copy_bytes = conn->read_buffer_fill;
+    if (max_copy_bytes > (newline_pos + 1))
+      max_copy_bytes = newline_pos + 1;
+    assert (max_copy_bytes > 0);
+
+    /* Copy bytes to the output buffer. */
+    memcpy (buffer, conn->read_buffer, max_copy_bytes);
+    buffer += max_copy_bytes;
+    assert (buffer_size >= max_copy_bytes);
+    buffer_size -= max_copy_bytes;
+
+    /* If there is data left in the read buffer, move it to the front of the
+     * buffer. */
+    if (max_copy_bytes < conn->read_buffer_fill)
+    {
+      size_t data_left_size = conn->read_buffer_fill - max_copy_bytes;
+      memmove (conn->read_buffer, conn->read_buffer + max_copy_bytes,
+          data_left_size);
+      conn->read_buffer_fill -= max_copy_bytes;
+    }
+    else
+    {
+      assert (max_copy_bytes == conn->read_buffer_fill);
+      conn->read_buffer_fill = 0;
+    }
+
+    if (found_newline)
+      break;
+
+    if (buffer_size == 0) /* no more space in the output buffer */
+      break;
+  }
+
+  return (orig_buffer);
+} /* }}} char *nc_connection_gets */
+
+static void *nc_handle_client (void *arg) /* {{{ */
+{
+  nc_connection_t *conn;
+  char errbuf[1024];
+  int status;
+
+  conn = arg;
+
+  DEBUG ("netcmd plugin: nc_handle_client: Reading from fd #%i", conn->fd);
+
+  status = nc_connection_init (conn);
+  if (status != 0)
+  {
+    nc_connection_close (conn);
     pthread_exit ((void *) 1);
   }
 
@@ -234,12 +542,12 @@ static void *nc_handle_client (void *arg) /* {{{ */
     int   len;
 
     errno = 0;
-    if (fgets (buffer, sizeof (buffer), fhin) == NULL)
+    if (nc_connection_gets (conn, buffer, sizeof (buffer)) == NULL)
     {
       if (errno != 0)
       {
         WARNING ("netcmd plugin: failed to read from socket #%i: %s",
-            fileno (fhin),
+            fileno (conn->fh_in),
             sstrerror (errno, errbuf, sizeof (errbuf)));
       }
       break;
@@ -260,36 +568,36 @@ static void *nc_handle_client (void *arg) /* {{{ */
 
     if (fields_num < 1)
     {
-      close (fd);
+      nc_connection_close (conn);
       break;
     }
 
     if (strcasecmp (fields[0], "getval") == 0)
     {
-      handle_getval (fhout, buffer);
+      handle_getval (conn->fh_out, buffer);
     }
     else if (strcasecmp (fields[0], "putval") == 0)
     {
-      handle_putval (fhout, buffer);
+      handle_putval (conn->fh_out, buffer);
     }
     else if (strcasecmp (fields[0], "listval") == 0)
     {
-      handle_listval (fhout, buffer);
+      handle_listval (conn->fh_out, buffer);
     }
     else if (strcasecmp (fields[0], "putnotif") == 0)
     {
-      handle_putnotif (fhout, buffer);
+      handle_putnotif (conn->fh_out, buffer);
     }
     else if (strcasecmp (fields[0], "flush") == 0)
     {
-      handle_flush (fhout, buffer);
+      handle_flush (conn->fh_out, buffer);
     }
     else
     {
-      if (fprintf (fhout, "-1 Unknown command: %s\n", fields[0]) < 0)
+      if (fprintf (conn->fh_out, "-1 Unknown command: %s\n", fields[0]) < 0)
       {
         WARNING ("netcmd plugin: failed to write to socket #%i: %s",
-            fileno (fhout),
+            fileno (conn->fh_out),
             sstrerror (errno, errbuf, sizeof (errbuf)));
         break;
       }
@@ -297,8 +605,7 @@ static void *nc_handle_client (void *arg) /* {{{ */
   } /* while (fgets) */
 
   DEBUG ("netcmd plugin: nc_handle_client: Exiting..");
-  fclose (fhin);
-  fclose (fhout);
+  nc_connection_close (conn);
 
   pthread_exit ((void *) 0);
   return ((void *) 0);
@@ -340,7 +647,8 @@ static void *nc_server_thread (void __attribute__((unused)) *arg) /* {{{ */
 
     for (i = 0; i < pollfd_num; i++)
     {
-      int *client_fd;
+      nc_peer_t *peer;
+      nc_connection_t *conn;
 
       if (pollfd[i].revents == 0)
       {
@@ -359,40 +667,59 @@ static void *nc_server_thread (void __attribute__((unused)) *arg) /* {{{ */
       }
       pollfd[i].revents = 0;
 
+      peer = nc_fd_to_peer (pollfd[i].fd);
+      if (peer == NULL)
+      {
+        ERROR ("netcmd plugin: Unable to find peer structure for file "
+            "descriptor #%i.", pollfd[i].fd);
+        continue;
+      }
+
       status = accept (pollfd[i].fd,
           /* sockaddr = */ NULL,
           /* sockaddr_len = */ NULL);
       if (status < 0)
       {
-        if (errno == EINTR)
-          continue;
-
-        ERROR ("netcmd plugin: accept failed: %s",
-            sstrerror (errno, errbuf, sizeof (errbuf)));
+        if (errno != EINTR)
+          ERROR ("netcmd plugin: accept failed: %s",
+              sstrerror (errno, errbuf, sizeof (errbuf)));
         continue;
       }
 
-      client_fd = malloc (sizeof (*client_fd));
-      if (client_fd == NULL)
+      conn = malloc (sizeof (*conn));
+      if (conn == NULL)
       {
         ERROR ("netcmd plugin: malloc failed.");
         close (status);
         continue;
       }
-      *client_fd = status;
+      memset (conn, 0, sizeof (*conn));
+      conn->fh_in = NULL;
+      conn->fh_out = NULL;
+
+      conn->fd = status;
+      if ((peer != NULL)
+          && (peer->tls_cert_file != NULL))
+      {
+        DEBUG ("netcmd plugin: Starting TLS session on [%s]:%s",
+            (peer->node != NULL) ? peer->node : "any",
+            (peer->service != NULL) ? peer->service : NC_DEFAULT_SERVICE);
+        conn->tls_session = nc_tls_get_session (peer);
+        conn->have_tls_session = 1;
+      }
 
-      DEBUG ("Spawning child to handle connection on fd %i", *client_fd);
+      DEBUG ("Spawning child to handle connection on fd %i", conn->fd);
 
       pthread_attr_init (&th_attr);
       pthread_attr_setdetachstate (&th_attr, PTHREAD_CREATE_DETACHED);
 
       status = pthread_create (&th, &th_attr, nc_handle_client,
-          client_fd);
+          conn);
       if (status != 0)
       {
         WARNING ("netcmd plugin: pthread_create failed: %s",
             sstrerror (errno, errbuf, sizeof (errbuf)));
-        close (*client_fd);
+        nc_connection_close (conn);
         continue;
       }
     }
@@ -424,11 +751,11 @@ static void *nc_server_thread (void __attribute__((unused)) *arg) /* {{{ */
  *     TLSKeyFile  "/path/to/key"
  *     TLSCAFile   "/path/to/ca"
  *     TLSCRLFile  "/path/to/crl"
- *     VerifyPeer yes|no
+ *     TLSVerifyPeer yes|no
  *   </Listen>
  * </Plugin>
  */
-static int nc_config_peer (const oconfig_item_t *ci)
+static int nc_config_peer (const oconfig_item_t *ci) /* {{{ */
 {
   nc_peer_t *p;
   int i;
@@ -444,6 +771,11 @@ static int nc_config_peer (const oconfig_item_t *ci)
   memset (p, 0, sizeof (*p));
   p->node = NULL;
   p->service = NULL;
+  p->tls_cert_file = NULL;
+  p->tls_key_file = NULL;
+  p->tls_ca_file = NULL;
+  p->tls_crl_file = NULL;
+  p->tls_verify_peer = 1;
 
   for (i = 0; i < ci->children_num; i++)
   {
@@ -453,11 +785,23 @@ static int nc_config_peer (const oconfig_item_t *ci)
       cf_util_get_string (child, &p->node);
     else if (strcasecmp ("Port", child->key) == 0)
       cf_util_get_string (child, &p->service);
+    else if (strcasecmp ("TLSCertFile", child->key) == 0)
+      cf_util_get_string (child, &p->tls_cert_file);
+    else if (strcasecmp ("TLSKeyFile", child->key) == 0)
+      cf_util_get_string (child, &p->tls_key_file);
+    else if (strcasecmp ("TLSCAFile", child->key) == 0)
+      cf_util_get_string (child, &p->tls_ca_file);
+    else if (strcasecmp ("TLSCRLFile", child->key) == 0)
+      cf_util_get_string (child, &p->tls_crl_file);
     else
       WARNING ("netcmd plugin: The option \"%s\" is not recognized within "
           "a \"%s\" block.", child->key, ci->key);
   }
 
+  DEBUG ("netcmd plugin: node = \"%s\"; service = \"%s\";", p->node, p->service);
+
+  peers_num++;
+
   return (0);
 } /* }}} int nc_config_peer */