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