1 /*
2  * MIT License
3  *
4  * Copyright(c) 2019 Intel Corporation. All rights reserved.
5  *
6  * Permission is hereby granted, free of charge, to any person obtaining a copy
7  * of this software and associated documentation files (the "Software"), to deal
8  * in the Software without restriction, including without limitation the rights
9  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10  * copies of the Software, and to permit persons to whom the Software is
11  * furnished to do so, subject to the following conditions:
12  *
13  * The above copyright notice and this permission notice shall be included in
14  * all copies or substantial portions of the Software.
15  *
16  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22  * SOFTWARE.
23  *
24  * Authors:
25  * Kamil Wiatrowski <kamilx.wiatrowski@intel.com>
26  *
27  */
28 
29 #include "collectd.h"
30 
31 #include "plugin.h"
32 #include "utils/common/common.h"
33 #include "utils/dmi/dmi.h"
34 
35 #include <microhttpd.h>
36 #if MHD_VERSION >= 0x00097002
37 #define MHD_RESULT enum MHD_Result
38 #else
39 #define MHD_RESULT int
40 #endif
41 
42 #include <jansson.h>
43 #include <netdb.h>
44 
45 #define CAP_PLUGIN "capabilities"
46 #define CONTENT_TYPE_JSON "application/json"
47 #define LISTEN_BACKLOG 16
48 
49 typedef struct dmi_type_name_s {
50   dmi_type type;
51   const char *name;
52 } dmi_type_name_t;
53 
54 static char *g_cap_json = NULL;
55 static char *httpd_host = NULL;
56 static unsigned short httpd_port = 9104;
57 static struct MHD_Daemon *httpd;
58 
59 static dmi_type_name_t types_list[] = {
60     {BIOS, "BIOS"},
61     {SYSTEM, "SYSTEM"},
62     {BASEBOARD, "BASEBOARD"},
63     {PROCESSOR, "PROCESSORS"},
64     {CACHE, "CACHE"},
65     {PHYSICAL_MEMORY_ARRAY, "PHYSICAL MEMORY ARRAYS"},
66     {MEMORY_DEVICE, "MEMORY DEVICES"},
67     {IPMI_DEVICE, "IPMI DEVICE"},
68     {ONBOARD_DEVICES_EXTENDED_INFORMATION,
69      "ONBOARD DEVICES EXTENDED INFORMATION"}};
70 
cap_get_dmi_variables(json_t * parent,const dmi_type type,const char * json_name)71 static int cap_get_dmi_variables(json_t *parent, const dmi_type type,
72                                  const char *json_name) {
73   DEBUG(CAP_PLUGIN ": cap_get_dmi_variables: %d/%s.", type, json_name);
74   dmi_reader_t reader;
75   if (dmi_reader_init(&reader, type) != DMI_OK) {
76     ERROR(CAP_PLUGIN ": dmi_reader_init failed.");
77     return -1;
78   }
79 
80   json_t *section = NULL;
81   json_t *entries = NULL;
82   json_t *attributes = NULL;
83   json_t *arr = json_array();
84   if (arr == NULL) {
85     ERROR(CAP_PLUGIN ": Failed to allocate json array.");
86     dmi_reader_clean(&reader);
87     return -1;
88   }
89   if (json_object_set_new(parent, json_name, arr)) {
90     ERROR(CAP_PLUGIN ": Failed to set array to parent.");
91     dmi_reader_clean(&reader);
92     return -1;
93   }
94 
95   while (reader.current_type != DMI_ENTRY_END) {
96     int status = dmi_read_next(&reader);
97     if (status != DMI_OK) {
98       ERROR(CAP_PLUGIN ": dmi_read_next failed.");
99       return -1;
100     }
101 
102     switch (reader.current_type) {
103 
104     case DMI_ENTRY_NAME:
105       DEBUG("%s", reader.name);
106       attributes = NULL;
107       section = json_object();
108       if (section == NULL) {
109         ERROR(CAP_PLUGIN ": Failed to allocate json object.");
110         dmi_reader_clean(&reader);
111         return -1;
112       }
113       if (json_array_append_new(arr, section)) {
114         ERROR(CAP_PLUGIN ": Failed to append json entry.");
115         dmi_reader_clean(&reader);
116         return -1;
117       }
118       entries = json_object();
119       if (entries == NULL) {
120         ERROR(CAP_PLUGIN ": Failed to allocate json object.");
121         dmi_reader_clean(&reader);
122         return -1;
123       }
124       if (json_object_set_new(section, reader.name, entries)) {
125         ERROR(CAP_PLUGIN ": Failed to set json entry.");
126         dmi_reader_clean(&reader);
127         return -1;
128       }
129       break;
130 
131     case DMI_ENTRY_MAP:
132       DEBUG("    %s:%s", reader.name, reader.value);
133       attributes = NULL;
134       if (entries == NULL) {
135         ERROR(CAP_PLUGIN ": unexpected dmi output format.");
136         dmi_reader_clean(&reader);
137         return -1;
138       }
139       if (json_object_set_new(entries, reader.name,
140                               json_string(reader.value))) {
141         ERROR(CAP_PLUGIN ": Failed to set json object for entries.");
142         dmi_reader_clean(&reader);
143         return -1;
144       }
145       break;
146 
147     case DMI_ENTRY_LIST_NAME:
148       DEBUG("    %s:", reader.name);
149       if (entries == NULL) {
150         ERROR(CAP_PLUGIN ": unexpected dmi output format.");
151         dmi_reader_clean(&reader);
152         return -1;
153       }
154       attributes = json_array();
155       if (attributes == NULL) {
156         ERROR(CAP_PLUGIN ": Failed to allocate json array for attributes.");
157         dmi_reader_clean(&reader);
158         return -1;
159       }
160       if (json_object_set_new(entries, reader.name, attributes)) {
161         ERROR(CAP_PLUGIN ": Failed to set json object for entry %s.",
162               reader.name);
163         dmi_reader_clean(&reader);
164         return -1;
165       }
166       break;
167 
168     case DMI_ENTRY_LIST_VALUE:
169       DEBUG("        %s", reader.value);
170       if (attributes == NULL) {
171         ERROR(CAP_PLUGIN ": unexpected dmi output format");
172         dmi_reader_clean(&reader);
173         return -1;
174       }
175       if (json_array_append_new(attributes, json_string(reader.value))) {
176         ERROR(CAP_PLUGIN ": Failed to append json attribute.");
177         dmi_reader_clean(&reader);
178         return -1;
179       }
180       break;
181 
182     default:
183       section = NULL;
184       entries = NULL;
185       attributes = NULL;
186       break;
187     }
188   }
189 
190   return 0;
191 }
192 
193 /* http_handler is the callback called by the microhttpd library. It essentially
194  * handles all HTTP request aspects and creates an HTTP response. */
cap_http_handler(void * cls,struct MHD_Connection * connection,const char * url,const char * method,const char * version,const char * upload_data,size_t * upload_data_size,void ** connection_state)195 static MHD_RESULT cap_http_handler(void *cls, struct MHD_Connection *connection,
196                                    const char *url, const char *method,
197                                    const char *version, const char *upload_data,
198                                    size_t *upload_data_size,
199                                    void **connection_state) {
200   if (strcmp(method, MHD_HTTP_METHOD_GET) != 0) {
201     return MHD_NO;
202   }
203 
204   /* On the first call for each connection, return without anything further.
205    * The first time only the headers are valid, do not respond in the first
206    * round. The docs are not very specific on the issue. */
207   if (*connection_state == NULL) {
208     /* set to a random non-NULL pointer. */
209     *connection_state = &(int){44};
210     return MHD_YES;
211   }
212   DEBUG(CAP_PLUGIN ": formatted response: %s", g_cap_json);
213 
214 #if defined(MHD_VERSION) && MHD_VERSION >= 0x00090500
215   struct MHD_Response *res = MHD_create_response_from_buffer(
216       strlen(g_cap_json), g_cap_json, MHD_RESPMEM_PERSISTENT);
217 #else
218   struct MHD_Response *res =
219       MHD_create_response_from_data(strlen(g_cap_json), g_cap_json, 0, 0);
220 #endif
221   if (res == NULL) {
222     ERROR(CAP_PLUGIN ": MHD create response failed.");
223     return MHD_NO;
224   }
225 
226   MHD_add_response_header(res, MHD_HTTP_HEADER_CONTENT_TYPE, CONTENT_TYPE_JSON);
227   int status = MHD_queue_response(connection, MHD_HTTP_OK, res);
228 
229   MHD_destroy_response(res);
230 
231   return status;
232 }
233 
cap_logger(void * arg,char const * fmt,va_list ap)234 static void cap_logger(__attribute__((unused)) void *arg, char const *fmt,
235                        va_list ap) {
236   char errbuf[1024];
237   vsnprintf(errbuf, sizeof(errbuf), fmt, ap);
238 
239   ERROR(CAP_PLUGIN ": libmicrohttpd: %s", errbuf);
240 }
241 
242 #if defined(MHD_VERSION) && MHD_VERSION >= 0x00090000
cap_open_socket()243 static int cap_open_socket() {
244   char service[NI_MAXSERV];
245   snprintf(service, sizeof(service), "%hu", httpd_port);
246 
247   struct addrinfo *res;
248   int status = getaddrinfo(httpd_host, service,
249                            &(struct addrinfo){
250                                .ai_flags = AI_PASSIVE | AI_ADDRCONFIG,
251                                .ai_family = PF_INET,
252                                .ai_socktype = SOCK_STREAM,
253                            },
254                            &res);
255   if (status != 0) {
256     return -1;
257   }
258 
259   int fd = -1;
260   for (struct addrinfo *ai = res; ai != NULL; ai = ai->ai_next) {
261     int flags = ai->ai_socktype;
262 #ifdef SOCK_CLOEXEC
263     flags |= SOCK_CLOEXEC;
264 #endif
265 
266     fd = socket(ai->ai_family, flags, 0);
267     if (fd == -1)
268       continue;
269 
270     if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &(int){1}, sizeof(int)) != 0) {
271       WARNING(CAP_PLUGIN ": setsockopt(SO_REUSEADDR) failed: %s", STRERRNO);
272       close(fd);
273       fd = -1;
274       continue;
275     }
276 
277     if (bind(fd, ai->ai_addr, ai->ai_addrlen) != 0) {
278       INFO(CAP_PLUGIN ": bind failed: %s", STRERRNO);
279       close(fd);
280       fd = -1;
281       continue;
282     }
283 
284     if (listen(fd, LISTEN_BACKLOG) != 0) {
285       INFO(CAP_PLUGIN ": listen failed: %s", STRERRNO);
286       close(fd);
287       fd = -1;
288       continue;
289     }
290 
291     char str_node[NI_MAXHOST];
292     char str_service[NI_MAXSERV];
293 
294     getnameinfo(ai->ai_addr, ai->ai_addrlen, str_node, sizeof(str_node),
295                 str_service, sizeof(str_service),
296                 NI_NUMERICHOST | NI_NUMERICSERV);
297 
298     INFO(CAP_PLUGIN ": Listening on [%s]:%s.", str_node, str_service);
299     break;
300   }
301 
302   freeaddrinfo(res);
303 
304   return fd;
305 }
306 #endif
307 
cap_start_daemon()308 static struct MHD_Daemon *cap_start_daemon() {
309 #if defined(MHD_VERSION) && MHD_VERSION >= 0x00090000
310   int fd = cap_open_socket();
311   if (fd == -1) {
312     ERROR(CAP_PLUGIN ": Opening a listening socket for [%s]:%hu failed.",
313           (httpd_host != NULL) ? httpd_host : "0.0.0.0", httpd_port);
314     return NULL;
315   }
316 #endif
317   unsigned int flags = MHD_USE_THREAD_PER_CONNECTION | MHD_USE_DEBUG;
318 #if defined(MHD_VERSION) && MHD_VERSION >= 0x00095300
319   flags |= MHD_USE_POLL_INTERNAL_THREAD;
320 #endif
321 
322   struct MHD_Daemon *d = MHD_start_daemon(
323       flags, httpd_port, NULL, NULL, cap_http_handler, NULL,
324 #if defined(MHD_VERSION) && MHD_VERSION >= 0x00090000
325       MHD_OPTION_LISTEN_SOCKET, fd,
326 #endif
327       MHD_OPTION_EXTERNAL_LOGGER, cap_logger, NULL, MHD_OPTION_END);
328 
329   if (d == NULL)
330     ERROR(CAP_PLUGIN ": MHD_start_daemon() failed.");
331 
332   return d;
333 }
334 
cap_config(oconfig_item_t * ci)335 static int cap_config(oconfig_item_t *ci) {
336   int status = 0;
337   for (int i = 0; i < ci->children_num; i++) {
338     oconfig_item_t *child = ci->children + i;
339 
340     if (strcasecmp("Host", child->key) == 0) {
341 #if defined(MHD_VERSION) && MHD_VERSION >= 0x00090000
342       status = cf_util_get_string(child, &httpd_host);
343 #else
344       ERROR(CAP_PLUGIN ": Option `Host' not supported. Please upgrade "
345                        "libmicrohttpd to at least 0.9.0");
346       status = -1;
347 #endif
348     } else if (strcasecmp("Port", child->key) == 0) {
349       int port = cf_util_get_port_number(child);
350       if (port > 0)
351         httpd_port = (unsigned short)port;
352       else {
353         ERROR(CAP_PLUGIN ": Wrong port number, correct range is 1-65535.");
354         status = -1;
355       }
356     } else {
357       ERROR(CAP_PLUGIN ": Unknown configuration option \"%s\".", child->key);
358       status = -1;
359     }
360 
361     if (status) {
362       ERROR(CAP_PLUGIN ": Invalid configuration parameter \"%s\".", child->key);
363       sfree(httpd_host);
364       break;
365     }
366   }
367 
368   return status;
369 }
370 
cap_shutdown()371 static int cap_shutdown() {
372   if (httpd != NULL) {
373     MHD_stop_daemon(httpd);
374     httpd = NULL;
375   }
376 
377   sfree(httpd_host);
378   sfree(g_cap_json);
379   return 0;
380 }
381 
cap_init(void)382 static int cap_init(void) {
383   json_t *root = json_object();
384   if (root == NULL) {
385     ERROR(CAP_PLUGIN ": Failed to allocate json root.");
386     cap_shutdown();
387     return -1;
388   }
389 
390   for (int i = 0; i < STATIC_ARRAY_SIZE(types_list); i++)
391     if (cap_get_dmi_variables(root, types_list[i].type, types_list[i].name)) {
392       json_decref(root);
393       cap_shutdown();
394       return -1;
395     }
396 
397   g_cap_json = json_dumps(root, JSON_COMPACT);
398   json_decref(root);
399 
400   if (g_cap_json == NULL) {
401     ERROR(CAP_PLUGIN ": json_dumps() failed.");
402     cap_shutdown();
403     return -1;
404   }
405 
406   httpd = cap_start_daemon();
407   if (httpd == NULL) {
408     cap_shutdown();
409     return -1;
410   }
411 
412   return 0;
413 }
414 
module_register(void)415 void module_register(void) {
416   plugin_register_complex_config(CAP_PLUGIN, cap_config);
417   plugin_register_init(CAP_PLUGIN, cap_init);
418   plugin_register_shutdown(CAP_PLUGIN, cap_shutdown);
419 }
420