Add snprintf wrapper for GCC 8.2/3
[collectd.git] / src / utils / gce / gce.c
1 /**
2  * collectd - src/utils_gce.c
3  * ISC license
4  *
5  * Copyright (C) 2017  Florian Forster
6  *
7  * Permission to use, copy, modify, and/or distribute this software for any
8  * purpose with or without fee is hereby granted, provided that the above
9  * copyright notice and this permission notice appear in all copies.
10  *
11  * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
12  * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
13  * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
14  * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
15  * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
16  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
17  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
18  *
19  * Authors:
20  *   Florian Forster <octo at collectd.org>
21  **/
22
23 #include "collectd.h"
24
25 #include "plugin.h"
26 #include "utils/common/common.h"
27 #include "utils/gce/gce.h"
28 #include "utils/oauth/oauth.h"
29 #include "utils_time.h"
30
31 #include <curl/curl.h>
32
33 #ifndef GCP_METADATA_PREFIX
34 #define GCP_METADATA_PREFIX "http://metadata.google.internal/computeMetadata/v1"
35 #endif
36 #ifndef GCE_METADATA_HEADER
37 #define GCE_METADATA_HEADER "Metadata-Flavor: Google"
38 #endif
39
40 #ifndef GCE_INSTANCE_ID_URL
41 #define GCE_INSTANCE_ID_URL GCP_METADATA_PREFIX "/instance/id"
42 #endif
43 #ifndef GCE_PROJECT_NUM_URL
44 #define GCE_PROJECT_NUM_URL GCP_METADATA_PREFIX "/project/numeric-project-id"
45 #endif
46 #ifndef GCE_PROJECT_ID_URL
47 #define GCE_PROJECT_ID_URL GCP_METADATA_PREFIX "/project/project-id"
48 #endif
49 #ifndef GCE_ZONE_URL
50 #define GCE_ZONE_URL GCP_METADATA_PREFIX "/instance/zone"
51 #endif
52
53 #ifndef GCE_DEFAULT_SERVICE_ACCOUNT
54 #define GCE_DEFAULT_SERVICE_ACCOUNT "default"
55 #endif
56
57 #ifndef GCE_SCOPE_URL
58 #define GCE_SCOPE_URL_FORMAT                                                   \
59   GCP_METADATA_PREFIX "/instance/service-accounts/%s/scopes"
60 #endif
61 #ifndef GCE_TOKEN_URL
62 #define GCE_TOKEN_URL_FORMAT                                                   \
63   GCP_METADATA_PREFIX "/instance/service-accounts/%s/token"
64 #endif
65
66 struct blob_s {
67   char *data;
68   size_t size;
69 };
70 typedef struct blob_s blob_t;
71
72 static int on_gce = -1;
73
74 static char *token = NULL;
75 static char *token_email = NULL;
76 static cdtime_t token_valid_until = 0;
77 static pthread_mutex_t token_lock = PTHREAD_MUTEX_INITIALIZER;
78
79 static size_t write_callback(void *contents, size_t size, size_t nmemb,
80                              void *ud) /* {{{ */
81 {
82   size_t realsize = size * nmemb;
83   blob_t *blob = ud;
84
85   if ((0x7FFFFFF0 < blob->size) || (0x7FFFFFF0 - blob->size < realsize)) {
86     ERROR("utils_gce: write_callback: integer overflow");
87     return 0;
88   }
89
90   blob->data = realloc(blob->data, blob->size + realsize + 1);
91   if (blob->data == NULL) {
92     /* out of memory! */
93     ERROR(
94         "utils_gce: write_callback: not enough memory (realloc returned NULL)");
95     return 0;
96   }
97
98   memcpy(blob->data + blob->size, contents, realsize);
99   blob->size += realsize;
100   blob->data[blob->size] = 0;
101
102   return realsize;
103 } /* }}} size_t write_callback */
104
105 /* read_url will issue a GET request for the given URL, setting the magic GCE
106  * metadata header in the process. On success, the response body is returned
107  * and it's the caller's responsibility to free it. On failure, an error is
108  * logged and NULL is returned. */
109 static char *read_url(char const *url) /* {{{ */
110 {
111   CURL *curl = curl_easy_init();
112   if (!curl) {
113     ERROR("utils_gce: curl_easy_init failed.");
114     return NULL;
115   }
116
117   struct curl_slist *headers = curl_slist_append(NULL, GCE_METADATA_HEADER);
118
119   char curl_errbuf[CURL_ERROR_SIZE];
120   blob_t blob = {0};
121   curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, curl_errbuf);
122   curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L);
123   curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
124   curl_easy_setopt(curl, CURLOPT_WRITEDATA, &blob);
125   curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
126   curl_easy_setopt(curl, CURLOPT_URL, url);
127
128   int status = curl_easy_perform(curl);
129   if (status != CURLE_OK) {
130     ERROR("utils_gce: fetching %s failed: %s", url, curl_errbuf);
131     sfree(blob.data);
132     curl_easy_cleanup(curl);
133     curl_slist_free_all(headers);
134     return NULL;
135   }
136
137   long http_code = 0;
138   curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
139   if ((http_code < 200) || (http_code >= 300)) {
140     ERROR("write_gcm plugin: fetching %s failed: HTTP error %ld", url,
141           http_code);
142     sfree(blob.data);
143     curl_easy_cleanup(curl);
144     curl_slist_free_all(headers);
145     return NULL;
146   }
147
148   curl_easy_cleanup(curl);
149   curl_slist_free_all(headers);
150   return blob.data;
151 } /* }}} char *read_url */
152
153 _Bool gce_check(void) /* {{{ */
154 {
155   if (on_gce != -1)
156     return on_gce == 1;
157
158   DEBUG("utils_gce: Checking whether I'm running on GCE ...");
159
160   CURL *curl = curl_easy_init();
161   if (!curl) {
162     ERROR("utils_gce: curl_easy_init failed.");
163     return 0;
164   }
165
166   struct curl_slist *headers = curl_slist_append(NULL, GCE_METADATA_HEADER);
167
168   char curl_errbuf[CURL_ERROR_SIZE];
169   blob_t blob = {NULL, 0};
170   curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, curl_errbuf);
171   curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L);
172   curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
173   curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, write_callback);
174   curl_easy_setopt(curl, CURLOPT_WRITEHEADER, &blob);
175   curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
176   curl_easy_setopt(curl, CURLOPT_URL, GCP_METADATA_PREFIX "/");
177
178   int status = curl_easy_perform(curl);
179   if ((status != CURLE_OK) || (blob.data == NULL) ||
180       (strstr(blob.data, "Metadata-Flavor: Google") == NULL)) {
181     DEBUG("utils_gce: ... no (%s)",
182           (status != CURLE_OK)
183               ? "curl_easy_perform failed"
184               : (blob.data == NULL) ? "blob.data == NULL"
185                                     : "Metadata-Flavor header not found");
186     sfree(blob.data);
187     curl_easy_cleanup(curl);
188     curl_slist_free_all(headers);
189     on_gce = 0;
190     return 0;
191   }
192   sfree(blob.data);
193
194   long http_code = 0;
195   curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
196   if ((http_code < 200) || (http_code >= 300)) {
197     DEBUG("utils_gce: ... no (HTTP status %ld)", http_code);
198     curl_easy_cleanup(curl);
199     curl_slist_free_all(headers);
200     on_gce = 0;
201     return 0;
202   }
203
204   DEBUG("utils_gce: ... yes");
205   curl_easy_cleanup(curl);
206   curl_slist_free_all(headers);
207   on_gce = 1;
208   return 1;
209 } /* }}} _Bool gce_check */
210
211 char *gce_project_id(void) /* {{{ */
212 {
213   return read_url(GCE_PROJECT_ID_URL);
214 } /* }}} char *gce_project_id */
215
216 char *gce_instance_id(void) /* {{{ */
217 {
218   return read_url(GCE_INSTANCE_ID_URL);
219 } /* }}} char *gce_instance_id */
220
221 char *gce_zone(void) /* {{{ */
222 {
223   return read_url(GCE_ZONE_URL);
224 } /* }}} char *gce_instance_id */
225
226 char *gce_scope(char const *email) /* {{{ */
227 {
228   char url[1024];
229
230   ssnprintf(url, sizeof(url), GCE_SCOPE_URL_FORMAT,
231            (email != NULL) ? email : GCE_DEFAULT_SERVICE_ACCOUNT);
232
233   return read_url(url);
234 } /* }}} char *gce_scope */
235
236 int gce_access_token(char const *email, char *buffer,
237                      size_t buffer_size) /* {{{ */
238 {
239   char url[1024];
240   char *json;
241   cdtime_t now = cdtime();
242
243   pthread_mutex_lock(&token_lock);
244
245   if (email == NULL)
246     email = GCE_DEFAULT_SERVICE_ACCOUNT;
247
248   if ((token_email != NULL) && (strcmp(email, token_email) == 0) &&
249       (token_valid_until > now)) {
250     sstrncpy(buffer, token, buffer_size);
251     pthread_mutex_unlock(&token_lock);
252     return 0;
253   }
254
255   ssnprintf(url, sizeof(url), GCE_TOKEN_URL_FORMAT, email);
256   json = read_url(url);
257   if (json == NULL) {
258     pthread_mutex_unlock(&token_lock);
259     return -1;
260   }
261
262   char tmp[256];
263   cdtime_t expires_in = 0;
264   int status = oauth_parse_json_token(json, tmp, sizeof(tmp), &expires_in);
265   sfree(json);
266   if (status != 0) {
267     pthread_mutex_unlock(&token_lock);
268     return status;
269   }
270
271   sfree(token);
272   token = strdup(tmp);
273
274   sfree(token_email);
275   token_email = strdup(email);
276
277   /* let tokens expire a bit early */
278   expires_in = (expires_in * 95) / 100;
279   token_valid_until = now + expires_in;
280
281   sstrncpy(buffer, token, buffer_size);
282   pthread_mutex_unlock(&token_lock);
283   return 0;
284 } /* }}} char *gce_token */