1 /*
2 * Copyright 2016 MongoDB, Inc.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 #include "mongoc-handshake-os-private.h"
18
19 #ifdef MONGOC_OS_IS_LINUX
20
21 #include <stdio.h>
22 #include <sys/utsname.h>
23
24 #include "mongoc-error.h"
25 #include "mongoc-linux-distro-scanner-private.h"
26 #include "mongoc-log.h"
27 #include "mongoc-handshake-private.h"
28 #include "mongoc-trace-private.h"
29 #include "mongoc-util-private.h"
30 #include "mongoc-version.h"
31
32 #define LINE_BUFFER_SIZE 1024
33
34 /*
35 * fgets() wrapper which removes '\n' at the end of the string
36 * Return 0 on failure or EOF.
37 */
38 static size_t
_fgets_wrapper(char * buffer,size_t buffer_size,FILE * f)39 _fgets_wrapper (char *buffer, size_t buffer_size, FILE *f)
40 {
41 char *fgets_res;
42 size_t len;
43
44 fgets_res = fgets (buffer, buffer_size, f);
45
46 if (!fgets_res) {
47 /* Didn't read anything. Empty file or error. */
48 if (ferror (f)) {
49 TRACE ("fgets() failed with error %d", errno);
50 }
51
52 return 0;
53 }
54
55 /* Chop off trailing \n */
56 len = strlen (buffer);
57
58 if (len > 0 && buffer[len - 1] == '\n') {
59 buffer[len - 1] = '\0';
60 len--;
61 } else if (len == buffer_size - 1) {
62 /* We read buffer_size bytes without hitting a newline
63 * therefore the line is super long, so we say this file is invalid.
64 * This is important since if we are in this situation, the NEXT call to
65 * fgets() will keep reading where we left off.
66 *
67 * This protects us from files like:
68 * aaaaa...DISTRIB_ID=nasal demons
69 */
70 TRACE ("Found line of length %ld, bailing out", len);
71 return 0;
72 }
73
74 return len;
75 }
76
77 static void
_process_line(const char * name_key,size_t name_key_len,char ** name,const char * version_key,size_t version_key_len,char ** version,const char * line,size_t line_len)78 _process_line (const char *name_key,
79 size_t name_key_len,
80 char **name,
81 const char *version_key,
82 size_t version_key_len,
83 char **version,
84 const char *line,
85 size_t line_len)
86 {
87 size_t key_len;
88 const char *equal_sign;
89 const char *value;
90 const char *needle = "=";
91 size_t value_len = 0;
92
93 ENTRY;
94
95 /* Figure out where = is. Everything before is the key,
96 * and everything after is the value */
97 equal_sign = strstr (line, needle);
98
99 if (equal_sign == NULL) {
100 TRACE ("Encountered malformed line: %s", line);
101 /* This line is malformed/incomplete, so skip it */
102 EXIT;
103 }
104
105 /* Should never happen since we null terminated this line */
106 BSON_ASSERT (equal_sign < line + line_len);
107
108 key_len = equal_sign - line;
109 value = equal_sign + strlen (needle);
110 value_len = strlen (value);
111 if (value_len > 2 && value[0] == '"' && value[value_len - 1] == '"') {
112 value_len -= 2;
113 value++;
114 }
115
116 /* If we find two copies of either key, the *name == NULL check will fail
117 * so we will just keep the first value encountered. */
118 if (name_key_len == key_len && strncmp (line, name_key, key_len) == 0 &&
119 !(*name)) {
120 *name = bson_strndup (value, value_len);
121 TRACE ("Found name: %s", *name);
122 } else if (version_key_len == key_len &&
123 strncmp (line, version_key, key_len) == 0 && !(*version)) {
124 *version = bson_strndup (value, value_len);
125 TRACE ("Found version: %s", *version);
126 }
127
128 EXIT;
129 }
130
131
132 /*
133 * Parse a file of the form:
134 * KEY=VALUE
135 * Looking for name_key and version_key, and storing
136 * their values into *name and *version.
137 * The values in *name and *version must be freed with bson_free.
138 */
139 void
_mongoc_linux_distro_scanner_read_key_value_file(const char * path,const char * name_key,ssize_t name_key_len,char ** name,const char * version_key,ssize_t version_key_len,char ** version)140 _mongoc_linux_distro_scanner_read_key_value_file (const char *path,
141 const char *name_key,
142 ssize_t name_key_len,
143 char **name,
144 const char *version_key,
145 ssize_t version_key_len,
146 char **version)
147 {
148 const int max_lines = 100;
149 int lines_read = 0;
150 char buffer[LINE_BUFFER_SIZE];
151 size_t buflen;
152 FILE *f;
153
154 ENTRY;
155
156 *name = NULL;
157 *version = NULL;
158
159 if (name_key_len < 0) {
160 name_key_len = strlen (name_key);
161 }
162
163 if (version_key_len < 0) {
164 version_key_len = strlen (version_key);
165 }
166
167 if (access (path, R_OK)) {
168 TRACE ("No permission to read from %s: errno: %d", path, errno);
169 EXIT;
170 }
171
172 f = fopen (path, "r");
173
174 if (!f) {
175 TRACE ("fopen failed on %s: %d", path, errno);
176 EXIT;
177 }
178
179 while (lines_read < max_lines) {
180 buflen = _fgets_wrapper (buffer, sizeof (buffer), f);
181
182 if (buflen == 0) {
183 /* Error or eof */
184 break;
185 }
186
187 _process_line (name_key,
188 name_key_len,
189 name,
190 version_key,
191 version_key_len,
192 version,
193 buffer,
194 buflen);
195
196 if (*version && *name) {
197 /* No point in reading any more */
198 break;
199 }
200
201 lines_read++;
202 }
203
204 fclose (f);
205 EXIT;
206 }
207
208 /*
209 * Find the first string in a list which is a valid file. Assumes
210 * passed in list is NULL terminated!
211 */
212 const char *
_get_first_existing(const char ** paths)213 _get_first_existing (const char **paths)
214 {
215 const char **p = &paths[0];
216
217 ENTRY;
218
219 for (; *p != NULL; p++) {
220 if (access (*p, F_OK)) {
221 /* Just doesn't exist */
222 continue;
223 }
224
225 if (access (*p, R_OK)) {
226 TRACE ("file %s exists, but cannot be read: error %d", *p, errno);
227 continue;
228 }
229
230 RETURN (*p);
231 }
232
233 RETURN (NULL);
234 }
235
236
237 /*
238 * Given a line of text, split it by the word "release." For example:
239 * Ubuntu release 14.04 =>
240 * *name = Ubuntu
241 * *version = 14.04
242 * If the word "release" isn't found then we put the whole string into *name
243 * (even if the string is empty).
244 */
245 void
_mongoc_linux_distro_scanner_split_line_by_release(const char * line,ssize_t line_len,char ** name,char ** version)246 _mongoc_linux_distro_scanner_split_line_by_release (const char *line,
247 ssize_t line_len,
248 char **name,
249 char **version)
250 {
251 const char *needle_loc;
252 const char *const needle = " release ";
253 const char *version_string;
254
255 *name = NULL;
256 *version = NULL;
257
258 if (line_len < 0) {
259 line_len = strlen (line);
260 }
261
262 needle_loc = strstr (line, needle);
263
264 if (!needle_loc) {
265 *name = bson_strdup (line);
266 return;
267 } else if (needle_loc == line) {
268 /* The file starts with the word " release "
269 * This file is weird enough we will just abandon it. */
270 return;
271 }
272
273 *name = bson_strndup (line, needle_loc - line);
274
275 version_string = needle_loc + strlen (needle);
276
277 if (version_string == line + line_len) {
278 /* Weird. The file just ended with "release " */
279 return;
280 }
281
282 *version = bson_strdup (version_string);
283 }
284
285 /*
286 * Search for a *-release file, and read its contents.
287 */
288 void
_mongoc_linux_distro_scanner_read_generic_release_file(const char ** paths,char ** name,char ** version)289 _mongoc_linux_distro_scanner_read_generic_release_file (const char **paths,
290 char **name,
291 char **version)
292 {
293 const char *path;
294 size_t buflen;
295 char buffer[LINE_BUFFER_SIZE];
296 FILE *f;
297
298 ENTRY;
299
300 *name = NULL;
301 *version = NULL;
302
303 path = _get_first_existing (paths);
304
305 if (!path) {
306 EXIT;
307 }
308
309 f = fopen (path, "r");
310
311 if (!f) {
312 TRACE ("Found %s exists and readable but couldn't open: %d", path, errno);
313 EXIT;
314 }
315
316 /* Read the first line of the file, look for the word "release" */
317 buflen = _fgets_wrapper (buffer, sizeof (buffer), f);
318
319 if (buflen > 0) {
320 TRACE ("Trying to split buffer with contents %s", buffer);
321 /* Try splitting the string. If we can't it'll store everything in
322 * *name. */
323 _mongoc_linux_distro_scanner_split_line_by_release (
324 buffer, buflen, name, version);
325 }
326
327 fclose (f);
328
329 EXIT;
330 }
331
332 static void
_get_kernel_version_from_uname(char ** version)333 _get_kernel_version_from_uname (char **version)
334 {
335 struct utsname system_info;
336
337 if (uname (&system_info) >= 0) {
338 *version = bson_strdup_printf ("kernel %s", system_info.release);
339 } else {
340 *version = NULL;
341 }
342 }
343
344 /*
345 * Some boilerplate logic that tries to set *name and *version to new_name
346 * and new_version if it's not already set. Values of new_name and new_version
347 * should not be used after this call.
348 */
349 static bool
_set_name_and_version_if_needed(char ** name,char ** version,char * new_name,char * new_version)350 _set_name_and_version_if_needed (char **name,
351 char **version,
352 char *new_name,
353 char *new_version)
354 {
355 if (new_name && !(*name)) {
356 *name = new_name;
357 } else {
358 bson_free (new_name);
359 }
360
361 if (new_version && !(*version)) {
362 *version = new_version;
363 } else {
364 bson_free (new_version);
365 }
366
367 return (*name) && (*version);
368 }
369
370 bool
_mongoc_linux_distro_scanner_get_distro(char ** name,char ** version)371 _mongoc_linux_distro_scanner_get_distro (char **name, char **version)
372 {
373 /* In case we decide to try looking up name/version again */
374 char *new_name;
375 char *new_version;
376 const char *generic_release_paths[] = {
377 "/etc/redhat-release",
378 "/etc/novell-release",
379 "/etc/gentoo-release",
380 "/etc/SuSE-release",
381 "/etc/SUSE-release",
382 "/etc/sles-release",
383 "/etc/debian_release",
384 "/etc/slackware-version",
385 "/etc/centos-release",
386 NULL,
387 };
388
389 ENTRY;
390
391 *name = NULL;
392 *version = NULL;
393
394 _mongoc_linux_distro_scanner_read_key_value_file (
395 "/etc/os-release", "NAME", -1, name, "VERSION_ID", -1, version);
396
397 if (*name && *version) {
398 RETURN (true);
399 }
400
401 _mongoc_linux_distro_scanner_read_key_value_file ("/etc/lsb-release",
402 "DISTRIB_ID",
403 -1,
404 &new_name,
405 "DISTRIB_RELEASE",
406 -1,
407 &new_version);
408
409 if (_set_name_and_version_if_needed (name, version, new_name, new_version)) {
410 RETURN (true);
411 }
412
413 /* Try to read from a generic release file */
414 _mongoc_linux_distro_scanner_read_generic_release_file (
415 generic_release_paths, &new_name, &new_version);
416
417 if (_set_name_and_version_if_needed (name, version, new_name, new_version)) {
418 RETURN (true);
419 }
420
421 if (*version == NULL) {
422 _get_kernel_version_from_uname (version);
423 }
424
425 if (*name && *version) {
426 RETURN (true);
427 }
428
429 bson_free (*name);
430 bson_free (*version);
431
432 *name = NULL;
433 *version = NULL;
434
435 RETURN (false);
436 }
437
438 #else
439 /* ensure the translation unit is not empty */
440 extern int mongoc_os_is_not_linux;
441 #endif
442