netcmd plugin: Some more work towards a working TLS setup.
[collectd.git] / src / netcmd.c
1 /**
2  * collectd - src/netcmd.c
3  * Copyright (C) 2007-2011  Florian octo Forster
4  *
5  * Permission is hereby granted, free of charge, to any person obtaining a
6  * copy of this software and associated documentation files (the "Software"),
7  * to deal in the Software without restriction, including without limitation
8  * the rights to use, copy, modify, merge, publish, distribute, sublicense,
9  * and/or sell copies of the Software, and to permit persons to whom the
10  * Software is furnished to do so, subject to the following conditions:
11  *
12  * The above copyright notice and this permission notice shall be included in
13  * all copies or substantial portions of the Software.
14  *
15  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
20  * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
21  * DEALINGS IN THE SOFTWARE.
22  *
23  * Author:
24  *   Florian octo Forster <octo at collectd.org>
25  **/
26
27 #include "collectd.h"
28 #include "common.h"
29 #include "plugin.h"
30 #include "configfile.h"
31
32 #include "utils_cmd_flush.h"
33 #include "utils_cmd_getval.h"
34 #include "utils_cmd_listval.h"
35 #include "utils_cmd_putval.h"
36 #include "utils_cmd_putnotif.h"
37
38 /* Folks without pthread will need to disable this plugin. */
39 #include <pthread.h>
40
41 #include <sys/socket.h>
42 #include <sys/poll.h>
43 #include <netdb.h>
44 #include <sys/stat.h>
45 #include <sys/un.h>
46
47 #include <grp.h>
48
49 #include <gnutls/gnutls.h>
50
51 #define NC_DEFAULT_SERVICE "25826"
52 #define NC_TLS_DH_BITS 1024
53
54 /*
55  * Private data structures
56  */
57 struct nc_peer_s
58 {
59   char *node;
60   char *service;
61   int *fds;
62   size_t fds_num;
63
64   char *tls_cert_file;
65   char *tls_key_file;
66   char *tls_ca_file;
67   char *tls_crl_file;
68   _Bool tls_verify_peer;
69
70   gnutls_certificate_credentials_t tls_credentials;
71   gnutls_dh_params_t tls_dh_params;
72   gnutls_priority_t tls_priority;
73
74 };
75 typedef struct nc_peer_s nc_peer_t;
76
77 struct nc_connection_s
78 {
79   int fd;
80
81   gnutls_session_t tls_session;
82   _Bool have_tls_session;
83 };
84 typedef struct nc_connection_s nc_connection_t;
85
86 /*
87  * Private variables
88  */
89
90 /* socket configuration */
91 static nc_peer_t *peers = NULL;
92 static size_t     peers_num;
93
94 static struct pollfd  *pollfd = NULL;
95 static size_t          pollfd_num;
96
97 static int       listen_thread_loop = 0;
98 static int       listen_thread_running = 0;
99 static pthread_t listen_thread;
100
101 /*
102  * Functions
103  */
104 static nc_peer_t *nc_fd_to_peer (int fd) /* {{{ */
105 {
106   size_t i;
107
108   for (i = 0; i < peers_num; i++)
109   {
110     size_t j;
111
112     for (j = 0; j < peers[i].fds_num; j++)
113       if (peers[i].fds[j] == fd)
114         return (peers + i);
115   }
116
117   return (NULL);
118 } /* }}} nc_peer_t *nc_fd_to_peer */
119
120 static int nc_register_fd (nc_peer_t *peer, int fd) /* {{{ */
121 {
122   struct pollfd *poll_ptr;
123   int *fd_ptr;
124
125   poll_ptr = realloc (pollfd, (pollfd_num + 1) * sizeof (*pollfd));
126   if (poll_ptr == NULL)
127   {
128     ERROR ("netcmd plugin: realloc failed.");
129     return (-1);
130   }
131   pollfd = poll_ptr;
132
133   memset (&pollfd[pollfd_num], 0, sizeof (pollfd[pollfd_num]));
134   pollfd[pollfd_num].fd = fd;
135   pollfd[pollfd_num].events = POLLIN | POLLPRI;
136   pollfd[pollfd_num].revents = 0;
137   pollfd_num++;
138
139   if (peer == NULL)
140     return (0);
141
142   fd_ptr = realloc (peer->fds, (peer->fds_num + 1) * sizeof (*peer->fds));
143   if (fd_ptr == NULL)
144   {
145     ERROR ("netcmd plugin: realloc failed.");
146     return (-1);
147   }
148   peer->fds = fd_ptr;
149   peer->fds[peer->fds_num] = fd;
150   peer->fds_num++;
151
152   return (0);
153 } /* }}} int nc_register_fd */
154
155 static int nc_tls_init (nc_peer_t *peer) /* {{{ */
156 {
157   if (peer == NULL)
158     return (EINVAL);
159
160   if ((peer->tls_cert_file == NULL)
161       || (peer->tls_key_file == NULL))
162     return (0);
163
164   /* Initialize the structure holding our certificate information. */
165   gnutls_certificate_allocate_credentials (&peer->tls_credentials);
166
167   /* Set up the configured certificates. */
168   if (peer->tls_ca_file != NULL)
169     gnutls_certificate_set_x509_trust_file (peer->tls_credentials,
170         peer->tls_ca_file, GNUTLS_X509_FMT_PEM);
171   if (peer->tls_crl_file != NULL)
172       gnutls_certificate_set_x509_crl_file (peer->tls_credentials,
173           peer->tls_crl_file, GNUTLS_X509_FMT_PEM);
174   gnutls_certificate_set_x509_key_file (peer->tls_credentials,
175       peer->tls_cert_file, peer->tls_key_file, GNUTLS_X509_FMT_PEM);
176
177   /* Initialize Diffie-Hellman parameters. */
178   gnutls_dh_params_init (&peer->tls_dh_params);
179   gnutls_dh_params_generate2 (peer->tls_dh_params, NC_TLS_DH_BITS);
180   gnutls_certificate_set_dh_params (peer->tls_credentials,
181       peer->tls_dh_params);
182
183   /* Initialize a "priority cache". This will tell GNUTLS which algorithms to
184    * use and which to avoid. We use the "NORMAL" method for now. */
185   gnutls_priority_init (&peer->tls_priority,
186      /* priority = */ "NORMAL", /* errpos = */ NULL);
187
188   return (0);
189 } /* }}} int nc_tls_init */
190
191 static gnutls_session_t nc_tls_get_session (nc_peer_t *peer) /* {{{ */
192 {
193   gnutls_session_t session;
194
195   if (peer->tls_credentials == NULL)
196     return (NULL);
197
198   /* Initialize new session. */
199   gnutls_init (&session, GNUTLS_SERVER);
200
201   /* Set cipher priority and credentials based on the information stored with
202    * the peer. */
203   gnutls_priority_set (session, peer->tls_priority);
204   gnutls_credentials_set (session,
205       GNUTLS_CRD_CERTIFICATE, peer->tls_credentials);
206
207   /* Request the client certificate. */
208   gnutls_certificate_server_set_request (session, GNUTLS_CERT_REQUEST);
209
210   return (session);
211 } /* }}} gnutls_session_t nc_tls_get_session */
212
213 static int nc_open_socket (nc_peer_t *peer) /* {{{ */
214 {
215   struct addrinfo ai_hints;
216   struct addrinfo *ai_list;
217   struct addrinfo *ai_ptr;
218   int status;
219
220   const char *node = NULL;
221   const char *service = NULL;
222
223   if (peer != NULL)
224   {
225     node = peer->node;
226     service = peer->service;
227   }
228
229   if (service == NULL)
230     service = NC_DEFAULT_SERVICE;
231
232   memset (&ai_hints, 0, sizeof (ai_hints));
233 #ifdef AI_PASSIVE
234   ai_hints.ai_flags |= AI_PASSIVE;
235 #endif
236 #ifdef AI_ADDRCONFIG
237   ai_hints.ai_flags |= AI_ADDRCONFIG;
238 #endif
239   ai_hints.ai_family = AF_UNSPEC;
240   ai_hints.ai_socktype = SOCK_STREAM;
241
242   ai_list = NULL;
243
244   if (service == NULL)
245     service = NC_DEFAULT_SERVICE;
246
247   status = getaddrinfo (node, service, &ai_hints, &ai_list);
248   if (status != 0)
249   {
250     ERROR ("netcmd plugin: getaddrinfo failed: %s",
251         gai_strerror (status));
252     return (-1);
253   }
254
255   for (ai_ptr = ai_list; ai_ptr != NULL; ai_ptr = ai_ptr->ai_next)
256   {
257     char errbuf[1024];
258     int fd;
259
260     fd = socket (ai_ptr->ai_family, ai_ptr->ai_socktype,
261         ai_ptr->ai_protocol);
262     if (fd < 0)
263     {
264       ERROR ("netcmd plugin: socket(2) failed: %s",
265           sstrerror (errno, errbuf, sizeof (errbuf)));
266       continue;
267     }
268
269     status = bind (fd, ai_ptr->ai_addr, ai_ptr->ai_addrlen);
270     if (status != 0)
271     {
272       close (fd);
273       ERROR ("netcmd plugin: bind(2) failed: %s",
274           sstrerror (errno, errbuf, sizeof (errbuf)));
275       continue;
276     }
277
278     status = listen (fd, /* backlog = */ 8);
279     if (status != 0)
280     {
281       close (fd);
282       ERROR ("netcmd plugin: listen(2) failed: %s",
283           sstrerror (errno, errbuf, sizeof (errbuf)));
284       continue;
285     }
286
287     status = nc_register_fd (peer, fd);
288     if (status != 0)
289     {
290       close (fd);
291       continue;
292     }
293   } /* for (ai_next) */
294
295   freeaddrinfo (ai_list);
296
297   return (nc_tls_init (peer));
298 } /* }}} int nc_open_socket */
299
300 static void nc_connection_close (nc_connection_t *conn) /* {{{ */
301 {
302   if (conn == NULL)
303     return;
304
305   if (conn->fd >= 0)
306   {
307     close (conn->fd);
308     conn->fd = -1;
309   }
310
311   if (conn->have_tls_session)
312   {
313     gnutls_deinit (conn->tls_session);
314     conn->have_tls_session = 0;
315   }
316
317   sfree (conn);
318 } /* }}} void nc_connection_close */
319
320 static void *nc_handle_client (void *arg) /* {{{ */
321 {
322   nc_connection_t *conn;
323   FILE *fhin, *fhout;
324   char errbuf[1024];
325
326   conn = arg;
327
328   DEBUG ("netcmd plugin: nc_handle_client: Reading from fd #%i", conn->fd);
329
330   fhin  = fdopen (conn->fd, "r");
331   if (fhin == NULL)
332   {
333     ERROR ("netcmd plugin: fdopen failed: %s",
334         sstrerror (errno, errbuf, sizeof (errbuf)));
335     nc_connection_close (conn);
336     pthread_exit ((void *) 1);
337   }
338
339   /* FIXME: dup conn->fd before calling fdopen! */
340   fhout = fdopen (conn->fd, "w");
341   /* Prevent nc_connection_close from calling close(2) on this fd. */
342   conn->fd = -1;
343   if (fhout == NULL)
344   {
345     ERROR ("netcmd plugin: fdopen failed: %s",
346         sstrerror (errno, errbuf, sizeof (errbuf)));
347     fclose (fhin); /* this closes fd as well */
348     nc_connection_close (conn);
349     pthread_exit ((void *) 1);
350   }
351
352   /* change output buffer to line buffered mode */
353   if (setvbuf (fhout, NULL, _IOLBF, 0) != 0)
354   {
355     ERROR ("netcmd plugin: setvbuf failed: %s",
356         sstrerror (errno, errbuf, sizeof (errbuf)));
357     nc_connection_close (conn);
358     pthread_exit ((void *) 1);
359   }
360
361   while (42)
362   {
363     char buffer[1024];
364     char buffer_copy[1024];
365     char *fields[128];
366     int   fields_num;
367     int   len;
368
369     errno = 0;
370     if (fgets (buffer, sizeof (buffer), fhin) == NULL)
371     {
372       if (errno != 0)
373       {
374         WARNING ("netcmd plugin: failed to read from socket #%i: %s",
375             fileno (fhin),
376             sstrerror (errno, errbuf, sizeof (errbuf)));
377       }
378       break;
379     }
380
381     len = strlen (buffer);
382     while ((len > 0)
383         && ((buffer[len - 1] == '\n') || (buffer[len - 1] == '\r')))
384       buffer[--len] = '\0';
385
386     if (len == 0)
387       continue;
388
389     sstrncpy (buffer_copy, buffer, sizeof (buffer_copy));
390
391     fields_num = strsplit (buffer_copy, fields,
392         sizeof (fields) / sizeof (fields[0]));
393
394     if (fields_num < 1)
395     {
396       nc_connection_close (conn);
397       break;
398     }
399
400     if (strcasecmp (fields[0], "getval") == 0)
401     {
402       handle_getval (fhout, buffer);
403     }
404     else if (strcasecmp (fields[0], "putval") == 0)
405     {
406       handle_putval (fhout, buffer);
407     }
408     else if (strcasecmp (fields[0], "listval") == 0)
409     {
410       handle_listval (fhout, buffer);
411     }
412     else if (strcasecmp (fields[0], "putnotif") == 0)
413     {
414       handle_putnotif (fhout, buffer);
415     }
416     else if (strcasecmp (fields[0], "flush") == 0)
417     {
418       handle_flush (fhout, buffer);
419     }
420     else
421     {
422       if (fprintf (fhout, "-1 Unknown command: %s\n", fields[0]) < 0)
423       {
424         WARNING ("netcmd plugin: failed to write to socket #%i: %s",
425             fileno (fhout),
426             sstrerror (errno, errbuf, sizeof (errbuf)));
427         break;
428       }
429     }
430   } /* while (fgets) */
431
432   DEBUG ("netcmd plugin: nc_handle_client: Exiting..");
433   /* XXX: Is this calling close on the same FD twice? */
434   fclose (fhin);
435   fclose (fhout);
436   nc_connection_close (conn);
437
438   pthread_exit ((void *) 0);
439   return ((void *) 0);
440 } /* }}} void *nc_handle_client */
441
442 static void *nc_server_thread (void __attribute__((unused)) *arg) /* {{{ */
443 {
444   int  status;
445   pthread_t th;
446   pthread_attr_t th_attr;
447   char errbuf[1024];
448   size_t i;
449
450   for (i = 0; i < peers_num; i++)
451     nc_open_socket (peers + i);
452
453   if (peers_num == 0)
454     nc_open_socket (NULL);
455
456   if (pollfd_num == 0)
457   {
458     ERROR ("netcmd plugin: No sockets could be opened.");
459     pthread_exit ((void *) -1);
460   }
461
462   while (listen_thread_loop != 0)
463   {
464     status = poll (pollfd, (nfds_t) pollfd_num, /* timeout = */ -1);
465     if (status < 0)
466     {
467       if ((errno == EINTR) || (errno == EAGAIN))
468         continue;
469
470       ERROR ("netcmd plugin: poll(2) failed: %s",
471           sstrerror (errno, errbuf, sizeof (errbuf)));
472       listen_thread_loop = 0;
473       continue;
474     }
475
476     for (i = 0; i < pollfd_num; i++)
477     {
478       nc_peer_t *peer;
479       nc_connection_t *conn;
480
481       if (pollfd[i].revents == 0)
482       {
483         continue;
484       }
485       else if ((pollfd[i].revents & (POLLERR | POLLHUP | POLLNVAL))
486           != 0)
487       {
488         WARNING ("netcmd plugin: File descriptor %i failed.",
489             pollfd[i].fd);
490         close (pollfd[i].fd);
491         pollfd[i].fd = -1;
492         pollfd[i].events = 0;
493         pollfd[i].revents = 0;
494         continue;
495       }
496       pollfd[i].revents = 0;
497
498       peer = nc_fd_to_peer (pollfd[i].fd);
499       if (peer == NULL)
500       {
501         ERROR ("netcmd plugin: Unable to find peer structure for file "
502             "descriptor #%i.", pollfd[i].fd);
503         continue;
504       }
505
506       status = accept (pollfd[i].fd,
507           /* sockaddr = */ NULL,
508           /* sockaddr_len = */ NULL);
509       if (status < 0)
510       {
511         if (errno != EINTR)
512           ERROR ("netcmd plugin: accept failed: %s",
513               sstrerror (errno, errbuf, sizeof (errbuf)));
514         continue;
515       }
516
517       conn = malloc (sizeof (*conn));
518       if (conn == NULL)
519       {
520         ERROR ("netcmd plugin: malloc failed.");
521         close (status);
522         continue;
523       }
524       memset (conn, 0, sizeof (*conn));
525
526       conn->fd = status;
527       if ((peer != NULL)
528           && (peer->tls_cert_file != NULL))
529       {
530         DEBUG ("netcmd plugin: Starting TLS session on [%s]:%s",
531             (peer->node != NULL) ? peer->node : "any",
532             (peer->service != NULL) ? peer->service : NC_DEFAULT_SERVICE);
533         conn->tls_session = nc_tls_get_session (peer);
534         conn->have_tls_session = 1;
535       }
536
537       DEBUG ("Spawning child to handle connection on fd %i", conn->fd);
538
539       pthread_attr_init (&th_attr);
540       pthread_attr_setdetachstate (&th_attr, PTHREAD_CREATE_DETACHED);
541
542       status = pthread_create (&th, &th_attr, nc_handle_client,
543           conn);
544       if (status != 0)
545       {
546         WARNING ("netcmd plugin: pthread_create failed: %s",
547             sstrerror (errno, errbuf, sizeof (errbuf)));
548         nc_connection_close (conn);
549         continue;
550       }
551     }
552   } /* while (listen_thread_loop) */
553
554   for (i = 0; i < pollfd_num; i++)
555   {
556     if (pollfd[i].fd < 0)
557       continue;
558
559     close (pollfd[i].fd);
560     pollfd[i].fd = -1;
561     pollfd[i].events = 0;
562     pollfd[i].revents = 0;
563   }
564
565   sfree (pollfd);
566   pollfd_num = 0;
567
568   return ((void *) 0);
569 } /* }}} void *nc_server_thread */
570
571 /*
572  * <Plugin netcmd>
573  *   <Listen>
574  *     Address "::1"
575  *     Port "1234"
576  *     TLSCertFile "/path/to/cert"
577  *     TLSKeyFile  "/path/to/key"
578  *     TLSCAFile   "/path/to/ca"
579  *     TLSCRLFile  "/path/to/crl"
580  *     TLSVerifyPeer yes|no
581  *   </Listen>
582  * </Plugin>
583  */
584 static int nc_config_peer (const oconfig_item_t *ci) /* {{{ */
585 {
586   nc_peer_t *p;
587   int i;
588
589   p = realloc (peers, sizeof (*peers) * (peers_num + 1));
590   if (p == NULL)
591   {
592     ERROR ("netcmd plugin: realloc failed.");
593     return (ENOMEM);
594   }
595   peers = p;
596   p = peers + peers_num;
597   memset (p, 0, sizeof (*p));
598   p->node = NULL;
599   p->service = NULL;
600   p->tls_cert_file = NULL;
601   p->tls_key_file = NULL;
602   p->tls_ca_file = NULL;
603   p->tls_crl_file = NULL;
604   p->tls_verify_peer = 1;
605
606   for (i = 0; i < ci->children_num; i++)
607   {
608     oconfig_item_t *child = ci->children + i;
609
610     if (strcasecmp ("Address", child->key) == 0)
611       cf_util_get_string (child, &p->node);
612     else if (strcasecmp ("Port", child->key) == 0)
613       cf_util_get_string (child, &p->service);
614     else if (strcasecmp ("TLSCertFile", child->key) == 0)
615       cf_util_get_string (child, &p->tls_cert_file);
616     else if (strcasecmp ("TLSKeyFile", child->key) == 0)
617       cf_util_get_string (child, &p->tls_key_file);
618     else if (strcasecmp ("TLSCAFile", child->key) == 0)
619       cf_util_get_string (child, &p->tls_ca_file);
620     else if (strcasecmp ("TLSCRLFile", child->key) == 0)
621       cf_util_get_string (child, &p->tls_crl_file);
622     else
623       WARNING ("netcmd plugin: The option \"%s\" is not recognized within "
624           "a \"%s\" block.", child->key, ci->key);
625   }
626
627   DEBUG ("netcmd plugin: node = \"%s\"; service = \"%s\";", p->node, p->service);
628
629   peers_num++;
630
631   return (0);
632 } /* }}} int nc_config_peer */
633
634 static int nc_config (oconfig_item_t *ci)
635 {
636   int i;
637
638   for (i = 0; i < ci->children_num; i++)
639   {
640     oconfig_item_t *child = ci->children + i;
641
642     if (strcasecmp ("Listen", child->key) == 0)
643       nc_config_peer (child);
644     else
645       WARNING ("netcmd plugin: The option \"%s\" is not recognized.",
646           child->key);
647   }
648
649   return (0);
650 } /* int nc_config */
651
652 static int nc_init (void)
653 {
654   static int have_init = 0;
655
656   int status;
657
658   /* Initialize only once. */
659   if (have_init != 0)
660     return (0);
661   have_init = 1;
662
663   listen_thread_loop = 1;
664
665   status = pthread_create (&listen_thread, NULL, nc_server_thread, NULL);
666   if (status != 0)
667   {
668     char errbuf[1024];
669     listen_thread_loop = 0;
670     listen_thread_running = 0;
671     ERROR ("netcmd plugin: pthread_create failed: %s",
672         sstrerror (errno, errbuf, sizeof (errbuf)));
673     return (-1);
674   }
675
676   listen_thread_running = 1;
677   return (0);
678 } /* int nc_init */
679
680 static int nc_shutdown (void)
681 {
682   void *ret;
683
684   listen_thread_loop = 0;
685
686   if (listen_thread != (pthread_t) 0)
687   {
688     pthread_kill (listen_thread, SIGTERM);
689     pthread_join (listen_thread, &ret);
690     listen_thread = (pthread_t) 0;
691   }
692
693   plugin_unregister_init ("netcmd");
694   plugin_unregister_shutdown ("netcmd");
695
696   return (0);
697 } /* int nc_shutdown */
698
699 void module_register (void)
700 {
701   plugin_register_complex_config ("netcmd", nc_config);
702   plugin_register_init ("netcmd", nc_init);
703   plugin_register_shutdown ("netcmd", nc_shutdown);
704 } /* void module_register (void) */
705
706 /* vim: set sw=2 sts=2 tw=78 et fdm=marker : */