/src/mosquitto/common/misc_mosq.c
Line | Count | Source (jump to first uncovered line) |
1 | | /* |
2 | | Copyright (c) 2009-2021 Roger Light <roger@atchoo.org> |
3 | | |
4 | | All rights reserved. This program and the accompanying materials |
5 | | are made available under the terms of the Eclipse Public License 2.0 |
6 | | and Eclipse Distribution License v1.0 which accompany this distribution. |
7 | | |
8 | | The Eclipse Public License is available at |
9 | | https://www.eclipse.org/legal/epl-2.0/ |
10 | | and the Eclipse Distribution License is available at |
11 | | http://www.eclipse.org/org/documents/edl-v10.php. |
12 | | |
13 | | SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause |
14 | | |
15 | | Contributors: |
16 | | Roger Light - initial implementation and documentation. |
17 | | */ |
18 | | |
19 | | /* This contains general purpose utility functions that are not specific to |
20 | | * Mosquitto/MQTT features. */ |
21 | | |
22 | | #include "config.h" |
23 | | |
24 | | #include <ctype.h> |
25 | | #include <errno.h> |
26 | | #include <limits.h> |
27 | | #include <stdbool.h> |
28 | | #include <stdio.h> |
29 | | #include <stdlib.h> |
30 | | #include <string.h> |
31 | | |
32 | | #ifdef WIN32 |
33 | | # include <winsock2.h> |
34 | | # include <aclapi.h> |
35 | | # include <io.h> |
36 | | # include <lmcons.h> |
37 | | # include <fcntl.h> |
38 | | # define PATH_MAX MAX_PATH |
39 | | #else |
40 | | # include <sys/stat.h> |
41 | | # include <pwd.h> |
42 | | # include <grp.h> |
43 | | # include <unistd.h> |
44 | | # include <fcntl.h> |
45 | | #endif |
46 | | |
47 | | #include "misc_mosq.h" |
48 | | #include "logging_mosq.h" |
49 | | |
50 | | |
51 | | FILE *mosquitto__fopen(const char *path, const char *mode, bool restrict_read) |
52 | 8.53k | { |
53 | | #ifdef WIN32 |
54 | | char buf[4096]; |
55 | | int rc; |
56 | | int flags = 0; |
57 | | |
58 | | rc = ExpandEnvironmentStringsA(path, buf, 4096); |
59 | | if(rc == 0 || rc > 4096){ |
60 | | return NULL; |
61 | | }else{ |
62 | | if(restrict_read){ |
63 | | HANDLE hfile; |
64 | | SECURITY_ATTRIBUTES sec; |
65 | | EXPLICIT_ACCESS_A ea; |
66 | | PACL pacl = NULL; |
67 | | char username[UNLEN + 1]; |
68 | | DWORD ulen = UNLEN; |
69 | | SECURITY_DESCRIPTOR sd; |
70 | | DWORD dwCreationDisposition; |
71 | | int fd; |
72 | | FILE *fptr; |
73 | | |
74 | | switch(mode[0]){ |
75 | | case 'a': |
76 | | dwCreationDisposition = OPEN_ALWAYS; |
77 | | flags = _O_APPEND; |
78 | | break; |
79 | | case 'r': |
80 | | dwCreationDisposition = OPEN_EXISTING; |
81 | | flags = _O_RDONLY; |
82 | | break; |
83 | | case 'w': |
84 | | dwCreationDisposition = CREATE_ALWAYS; |
85 | | break; |
86 | | default: |
87 | | return NULL; |
88 | | } |
89 | | |
90 | | GetUserNameA(username, &ulen); |
91 | | if(!InitializeSecurityDescriptor(&sd, SECURITY_DESCRIPTOR_REVISION)){ |
92 | | return NULL; |
93 | | } |
94 | | BuildExplicitAccessWithNameA(&ea, username, GENERIC_ALL, SET_ACCESS, NO_INHERITANCE); |
95 | | if(SetEntriesInAclA(1, &ea, NULL, &pacl) != ERROR_SUCCESS){ |
96 | | return NULL; |
97 | | } |
98 | | if(!SetSecurityDescriptorDacl(&sd, TRUE, pacl, FALSE)){ |
99 | | LocalFree(pacl); |
100 | | return NULL; |
101 | | } |
102 | | |
103 | | memset(&sec, 0, sizeof(sec)); |
104 | | sec.nLength = sizeof(SECURITY_ATTRIBUTES); |
105 | | sec.bInheritHandle = FALSE; |
106 | | sec.lpSecurityDescriptor = &sd; |
107 | | |
108 | | hfile = CreateFileA(buf, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ, |
109 | | &sec, |
110 | | dwCreationDisposition, |
111 | | FILE_ATTRIBUTE_NORMAL, |
112 | | NULL); |
113 | | |
114 | | LocalFree(pacl); |
115 | | |
116 | | fd = _open_osfhandle((intptr_t)hfile, flags); |
117 | | if(fd < 0){ |
118 | | return NULL; |
119 | | } |
120 | | |
121 | | fptr = _fdopen(fd, mode); |
122 | | if(!fptr){ |
123 | | _close(fd); |
124 | | return NULL; |
125 | | } |
126 | | if(mode[0] == 'a'){ |
127 | | fseek(fptr, 0, SEEK_END); |
128 | | } |
129 | | return fptr; |
130 | | |
131 | | }else { |
132 | | return fopen(buf, mode); |
133 | | } |
134 | | } |
135 | | #else |
136 | 8.53k | FILE *fptr; |
137 | 8.53k | struct stat statbuf; |
138 | | |
139 | 8.53k | if(restrict_read){ |
140 | 221 | mode_t old_mask; |
141 | | |
142 | 221 | old_mask = umask(0077); |
143 | | |
144 | 221 | int open_flags = O_NOFOLLOW; |
145 | 884 | for(size_t i = 0; i<strlen(mode); i++){ |
146 | 663 | if(mode[i] == 'r'){ |
147 | 221 | open_flags |= O_RDONLY; |
148 | 442 | }else if(mode[i] == 'w'){ |
149 | 0 | open_flags |= O_WRONLY; |
150 | 0 | open_flags |= (O_TRUNC | O_CREAT | O_EXCL); |
151 | |
|
152 | 442 | }else if(mode[i] == 'a'){ |
153 | 0 | open_flags |= O_WRONLY; |
154 | 0 | open_flags |= (O_APPEND | O_CREAT); |
155 | 442 | }else if(mode[i] == 't'){ |
156 | 221 | }else if(mode[i] == 'b'){ |
157 | 221 | }else if(mode[i] == '+'){ |
158 | 221 | open_flags |= O_RDWR; |
159 | 221 | } |
160 | 663 | } |
161 | 221 | int fd = open(path, open_flags, 0600); |
162 | 221 | if(fd < 0) return NULL; |
163 | 221 | fptr = fdopen(fd, mode); |
164 | | |
165 | 221 | umask(old_mask); |
166 | 8.31k | }else{ |
167 | 8.31k | fptr = fopen(path, mode); |
168 | 8.31k | } |
169 | 8.53k | if(!fptr) return NULL; |
170 | | |
171 | 8.53k | if(fstat(fileno(fptr), &statbuf) < 0){ |
172 | 0 | fclose(fptr); |
173 | 0 | return NULL; |
174 | 0 | } |
175 | | |
176 | 8.53k | if(restrict_read){ |
177 | 221 | if(statbuf.st_mode & S_IRWXO){ |
178 | 1 | #ifdef WITH_BROKER |
179 | 1 | log__printf(NULL, MOSQ_LOG_WARNING, |
180 | | #else |
181 | | fprintf(stderr, |
182 | | #endif |
183 | 1 | "Warning: File %s has world readable permissions. Future versions will refuse to load this file.", |
184 | 1 | path); |
185 | | #if 0 |
186 | | return NULL; |
187 | | #endif |
188 | 1 | } |
189 | 221 | if(statbuf.st_uid != getuid()){ |
190 | 0 | char buf[4096]; |
191 | 0 | struct passwd pw, *result; |
192 | |
|
193 | 0 | getpwuid_r(getuid(), &pw, buf, sizeof(buf), &result); |
194 | 0 | if(result){ |
195 | 0 | #ifdef WITH_BROKER |
196 | 0 | log__printf(NULL, MOSQ_LOG_WARNING, |
197 | | #else |
198 | | fprintf(stderr, |
199 | | #endif |
200 | 0 | "Warning: File %s owner is not %s. Future versions will refuse to load this file.", |
201 | 0 | path, result->pw_name); |
202 | 0 | } |
203 | | #if 0 |
204 | | // Future version |
205 | | return NULL; |
206 | | #endif |
207 | 0 | } |
208 | 221 | if(statbuf.st_gid != getgid()){ |
209 | 0 | char buf[4096]; |
210 | 0 | struct group grp, *result; |
211 | |
|
212 | 0 | getgrgid_r(getgid(), &grp, buf, sizeof(buf), &result); |
213 | 0 | if(result){ |
214 | 0 | #ifdef WITH_BROKER |
215 | 0 | log__printf(NULL, MOSQ_LOG_WARNING, |
216 | | #else |
217 | | fprintf(stderr, |
218 | | #endif |
219 | 0 | "Warning: File %s group is not %s. Future versions will refuse to load this file.", |
220 | 0 | path, result->gr_name); |
221 | 0 | } |
222 | | #if 0 |
223 | | // Future version |
224 | | return NULL |
225 | | #endif |
226 | 0 | } |
227 | 221 | } |
228 | | |
229 | | |
230 | 8.53k | if(!S_ISREG(statbuf.st_mode) && !S_ISLNK(statbuf.st_mode)){ |
231 | 0 | #ifdef WITH_BROKER |
232 | 0 | log__printf(NULL, MOSQ_LOG_ERR, "Error: %s is not a file.", path); |
233 | 0 | #endif |
234 | 0 | fclose(fptr); |
235 | 0 | return NULL; |
236 | 0 | } |
237 | 8.53k | return fptr; |
238 | 8.53k | #endif |
239 | 8.53k | } |
240 | | |
241 | | |
242 | | char *misc__trimblanks(char *str) |
243 | 39.3k | { |
244 | 39.3k | char *endptr; |
245 | | |
246 | 39.3k | if(str == NULL) return NULL; |
247 | | |
248 | 39.3k | while(isspace(str[0])){ |
249 | 232 | str++; |
250 | 232 | } |
251 | 39.3k | endptr = &str[strlen(str)-1]; |
252 | 59.0k | while(endptr > str && isspace(endptr[0])){ |
253 | 19.7k | endptr[0] = '\0'; |
254 | 19.7k | endptr--; |
255 | 19.7k | } |
256 | 39.3k | return str; |
257 | 39.3k | } |
258 | | |
259 | | |
260 | | char *fgets_extending(char **buf, int *buflen, FILE *stream) |
261 | 28.1k | { |
262 | 28.1k | char *rc; |
263 | 28.1k | char endchar; |
264 | 28.1k | int offset = 0; |
265 | 28.1k | char *newbuf; |
266 | 28.1k | size_t len; |
267 | | |
268 | 28.1k | if(stream == NULL || buf == NULL || buflen == NULL || *buflen < 1){ |
269 | 0 | return NULL; |
270 | 0 | } |
271 | | |
272 | 36.6k | do{ |
273 | 36.6k | rc = fgets(&((*buf)[offset]), (*buflen)-offset, stream); |
274 | 36.6k | if(feof(stream) || rc == NULL){ |
275 | 8.62k | return rc; |
276 | 8.62k | } |
277 | | |
278 | 28.0k | len = strlen(*buf); |
279 | 28.0k | if(len == 0){ |
280 | 17 | return rc; |
281 | 17 | } |
282 | 28.0k | endchar = (*buf)[len-1]; |
283 | 28.0k | if(endchar == '\n'){ |
284 | 19.5k | return rc; |
285 | 19.5k | } |
286 | | /* No EOL char found, so extend buffer */ |
287 | 8.50k | offset = (*buflen)-1; |
288 | 8.50k | *buflen += 1000; |
289 | 8.50k | newbuf = realloc(*buf, (size_t)*buflen); |
290 | 8.50k | if(!newbuf){ |
291 | 0 | return NULL; |
292 | 0 | } |
293 | 8.50k | *buf = newbuf; |
294 | 8.50k | }while(1); |
295 | 28.1k | } |
296 | | |
297 | | |
298 | | #define INVOKE_LOG_FN(format, ...) \ |
299 | 0 | do{ \ |
300 | 0 | if(log_fn){ \ |
301 | 0 | int tmp_err_no = errno; \ |
302 | 0 | char msg[2*PATH_MAX]; \ |
303 | 0 | snprintf(msg, sizeof(msg), (format), __VA_ARGS__); \ |
304 | 0 | msg[sizeof(msg)-1] = '\0'; \ |
305 | 0 | (*log_fn)(msg); \ |
306 | 0 | errno = tmp_err_no; \ |
307 | 0 | } \ |
308 | 0 | }while (0) |
309 | | |
310 | | int mosquitto_write_file(const char* target_path, bool restrict_read, int (*write_fn)(FILE* fptr, void* user_data), void* user_data, void (*log_fn)(const char* msg)) |
311 | 0 | { |
312 | 0 | int rc = 0; |
313 | 0 | FILE *fptr = NULL; |
314 | 0 | char tmp_file_path[PATH_MAX]; |
315 | |
|
316 | 0 | if(!target_path || !write_fn){ |
317 | 0 | return MOSQ_ERR_INVAL; |
318 | 0 | } |
319 | | |
320 | 0 | rc = snprintf(tmp_file_path, PATH_MAX, "%s.new", target_path); |
321 | 0 | if(rc < 0 || rc >= PATH_MAX){ |
322 | 0 | return MOSQ_ERR_INVAL; |
323 | 0 | } |
324 | | |
325 | 0 | #ifndef WIN32 |
326 | | /** |
327 | | * |
328 | | * If a system lost power during the rename operation at the |
329 | | * end of this file the filesystem could potentially be left |
330 | | * with a directory that looks like this after powerup: |
331 | | * |
332 | | * 24094 -rw-r--r-- 2 root root 4099 May 30 16:27 mosquitto.db |
333 | | * 24094 -rw-r--r-- 2 root root 4099 May 30 16:27 mosquitto.db.new |
334 | | * |
335 | | * The 24094 shows that mosquitto.db.new is hard-linked to the |
336 | | * same file as mosquitto.db. If fopen(outfile, "wb") is naively |
337 | | * called then mosquitto.db will be truncated and the database |
338 | | * potentially corrupted. |
339 | | * |
340 | | * Any existing mosquitto.db.new file must be removed prior to |
341 | | * opening to guarantee that it is not hard-linked to |
342 | | * mosquitto.db. |
343 | | * |
344 | | */ |
345 | 0 | if(unlink(tmp_file_path) && errno != ENOENT){ |
346 | 0 | INVOKE_LOG_FN("unable to remove stale tmp file %s, error %s", tmp_file_path, strerror(errno)); |
347 | 0 | return MOSQ_ERR_INVAL; |
348 | 0 | } |
349 | 0 | #endif |
350 | | |
351 | 0 | fptr = mosquitto__fopen(tmp_file_path, "wb", restrict_read); |
352 | 0 | if(fptr == NULL){ |
353 | 0 | INVOKE_LOG_FN("unable to open %s for writing, error %s", tmp_file_path, strerror(errno)); |
354 | 0 | return MOSQ_ERR_INVAL; |
355 | 0 | } |
356 | | |
357 | 0 | if((rc = (*write_fn)(fptr, user_data)) != MOSQ_ERR_SUCCESS){ |
358 | 0 | goto error; |
359 | 0 | } |
360 | | |
361 | 0 | rc = MOSQ_ERR_ERRNO; |
362 | 0 | #ifndef WIN32 |
363 | | /** |
364 | | * |
365 | | * Closing a file does not guarantee that the contents are |
366 | | * written to disk. Need to flush to send data from app to OS |
367 | | * buffers, then fsync to deliver data from OS buffers to disk |
368 | | * (as well as disk hardware permits). |
369 | | * |
370 | | * man close (http://linux.die.net/man/2/close, 2016-06-20): |
371 | | * |
372 | | * "successful close does not guarantee that the data has |
373 | | * been successfully saved to disk, as the kernel defers |
374 | | * writes. It is not common for a filesystem to flush |
375 | | * the buffers when the stream is closed. If you need |
376 | | * to be sure that the data is physically stored, use |
377 | | * fsync(2). (It will depend on the disk hardware at this |
378 | | * point." |
379 | | * |
380 | | * This guarantees that the new state file will not overwrite |
381 | | * the old state file before its contents are valid. |
382 | | * |
383 | | */ |
384 | 0 | if(fflush(fptr) != 0 && errno != EINTR){ |
385 | 0 | INVOKE_LOG_FN("unable to flush %s, error %s", tmp_file_path, strerror(errno)); |
386 | 0 | goto error; |
387 | 0 | } |
388 | | |
389 | 0 | if(fsync(fileno(fptr)) != 0 && errno != EINTR){ |
390 | 0 | INVOKE_LOG_FN("unable to sync %s to disk, error %s", tmp_file_path, strerror(errno)); |
391 | 0 | goto error; |
392 | 0 | } |
393 | 0 | #endif |
394 | | |
395 | 0 | if(fclose(fptr) != 0){ |
396 | 0 | INVOKE_LOG_FN("unable to close %s on disk, error %s", tmp_file_path, strerror(errno)); |
397 | 0 | fptr = NULL; |
398 | 0 | goto error; |
399 | 0 | } |
400 | 0 | fptr = NULL; |
401 | |
|
402 | | #ifdef WIN32 |
403 | | if(remove(target_path) != 0 && errno != ENOENT){ |
404 | | INVOKE_LOG_FN("unable to remove %s on disk, error %s", target_path, strerror(errno)); |
405 | | goto error; |
406 | | } |
407 | | #endif |
408 | |
|
409 | 0 | if(rename(tmp_file_path, target_path) != 0){ |
410 | 0 | INVOKE_LOG_FN("unable to replace %s by tmp file %s, error %s", target_path, tmp_file_path, strerror(errno)); |
411 | 0 | goto error; |
412 | 0 | } |
413 | 0 | return MOSQ_ERR_SUCCESS; |
414 | | |
415 | 0 | error: |
416 | 0 | if(fptr){ |
417 | 0 | fclose(fptr); |
418 | 0 | unlink(tmp_file_path); |
419 | 0 | } |
420 | 0 | return MOSQ_ERR_ERRNO; |
421 | 0 | } |