mosquitto/libcommon/file_common.c

494 lines
11 KiB
C
Raw Permalink Normal View History

/*
2021-11-03 23:08:24 +01:00
Copyright (c) 2009-2021 Roger Light <roger@atchoo.org>
All rights reserved. This program and the accompanying materials
2020-11-25 18:34:21 +01:00
are made available under the terms of the Eclipse Public License 2.0
and Eclipse Distribution License v1.0 which accompany this distribution.
The Eclipse Public License is available at
2020-11-25 18:34:21 +01:00
https://www.eclipse.org/legal/epl-2.0/
and the Eclipse Distribution License is available at
http://www.eclipse.org/org/documents/edl-v10.php.
SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
2020-12-01 19:21:59 +01:00
Contributors:
Roger Light - initial implementation and documentation.
*/
/* This contains general purpose utility functions that are not specific to
* Mosquitto/MQTT features. */
#include "config.h"
#include <ctype.h>
#include <errno.h>
#include <limits.h>
2025-10-30 00:06:33 +01:00
#include <stdarg.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#ifdef WIN32
# include <winsock2.h>
# include <aclapi.h>
# include <io.h>
# include <lmcons.h>
# include <fcntl.h>
2022-07-21 21:21:25 +02:00
# define PATH_MAX MAX_PATH
#else
# include <sys/stat.h>
# include <pwd.h>
# include <grp.h>
2022-07-21 21:21:25 +02:00
# include <unistd.h>
2023-08-01 00:45:16 +02:00
# include <fcntl.h>
#endif
#include "mosquitto.h"
2021-03-21 10:17:53 +01:00
2025-10-30 00:06:33 +01:00
void (*libcommon_vprintf)(const char *fmt, va_list va) = NULL;
void libcommon_printf(const char *fmt, ...)
{
va_list va;
va_start(va, fmt);
if(libcommon_vprintf){
libcommon_vprintf(fmt, va);
}else{
vfprintf(stderr, fmt, va);
}
va_end(va);
}
FILE *mosquitto_fopen(const char *path, const char *mode, bool restrict_read)
{
#ifdef WIN32
char buf[4096];
int rc;
int flags = 0;
rc = ExpandEnvironmentStringsA(path, buf, 4096);
if(rc == 0 || rc > 4096){
return NULL;
}else{
2022-08-01 23:47:46 +02:00
if(restrict_read){
HANDLE hfile;
SECURITY_ATTRIBUTES sec;
EXPLICIT_ACCESS_A ea;
PACL pacl = NULL;
char username[UNLEN + 1];
DWORD ulen = UNLEN;
SECURITY_DESCRIPTOR sd;
DWORD dwCreationDisposition;
DWORD dwShareMode;
int fd;
FILE *fptr;
switch(mode[0]){
case 'a':
dwCreationDisposition = OPEN_ALWAYS;
dwShareMode = GENERIC_WRITE;
flags = _O_APPEND;
break;
case 'r':
dwCreationDisposition = OPEN_EXISTING;
dwShareMode = GENERIC_READ;
flags = _O_RDONLY;
break;
case 'w':
dwCreationDisposition = CREATE_ALWAYS;
dwShareMode = GENERIC_WRITE;
break;
default:
return NULL;
}
if(mode[1] == '+'){
dwShareMode = GENERIC_READ | GENERIC_WRITE;
}
GetUserNameA(username, &ulen);
2022-08-01 23:47:46 +02:00
if(!InitializeSecurityDescriptor(&sd, SECURITY_DESCRIPTOR_REVISION)){
return NULL;
}
BuildExplicitAccessWithNameA(&ea, username, GENERIC_ALL, SET_ACCESS, NO_INHERITANCE);
2022-08-01 23:47:46 +02:00
if(SetEntriesInAclA(1, &ea, NULL, &pacl) != ERROR_SUCCESS){
return NULL;
}
2022-08-01 23:47:46 +02:00
if(!SetSecurityDescriptorDacl(&sd, TRUE, pacl, FALSE)){
LocalFree(pacl);
return NULL;
}
memset(&sec, 0, sizeof(sec));
sec.nLength = sizeof(SECURITY_ATTRIBUTES);
sec.bInheritHandle = FALSE;
sec.lpSecurityDescriptor = &sd;
hfile = CreateFileA(buf, dwShareMode, FILE_SHARE_READ,
2025-09-17 00:19:44 +02:00
&sec,
dwCreationDisposition,
FILE_ATTRIBUTE_NORMAL,
NULL);
LocalFree(pacl);
fd = _open_osfhandle((intptr_t)hfile, flags);
2022-08-01 23:47:46 +02:00
if(fd < 0){
return NULL;
}
fptr = _fdopen(fd, mode);
2022-08-01 23:47:46 +02:00
if(!fptr){
_close(fd);
return NULL;
}
if(mode[0] == 'a'){
fseek(fptr, 0, SEEK_END);
}
return fptr;
2025-09-15 12:07:28 +02:00
}else{
return fopen(buf, mode);
}
}
#else
2022-10-16 08:59:54 +02:00
FILE *fptr;
2022-10-16 09:41:30 +02:00
struct stat statbuf;
2022-08-01 23:47:46 +02:00
if(restrict_read){
mode_t old_mask;
old_mask = umask(0077);
2023-08-01 00:45:16 +02:00
int open_flags = 0;
if(!getenv("MOSQUITTO_UNSAFE_ALLOW_SYMLINKS")){
open_flags |= O_NOFOLLOW;
}
2023-08-01 00:45:16 +02:00
for(size_t i = 0; i<strlen(mode); i++){
if(mode[i] == 'r'){
open_flags |= O_RDONLY;
}else if(mode[i] == 'w'){
open_flags |= O_WRONLY;
open_flags |= (O_TRUNC | O_CREAT | O_EXCL);
}else if(mode[i] == 'a'){
open_flags |= O_WRONLY;
open_flags |= (O_APPEND | O_CREAT);
}else if(mode[i] == 't'){
}else if(mode[i] == 'b'){
}else if(mode[i] == '+'){
open_flags |= O_RDWR;
}
}
int fd = open(path, open_flags, 0600);
if(fd < 0){
return NULL;
}
2023-08-01 00:45:16 +02:00
fptr = fdopen(fd, mode);
umask(old_mask);
}else{
2022-10-16 08:59:54 +02:00
fptr = fopen(path, mode);
}
if(!fptr){
return NULL;
}
2022-10-16 08:59:54 +02:00
if(fstat(fileno(fptr), &statbuf) < 0){
fclose(fptr);
return NULL;
}
if(restrict_read){
if(statbuf.st_mode & S_IRWXO){
2025-10-30 00:06:33 +01:00
libcommon_printf(
"Warning: File %s has world readable permissions. Future versions will refuse to load this file.\n"
"To fix this, use `chmod 0700 %s`.\n",
path, path);
#if 0
return NULL;
#endif
}
if(statbuf.st_uid != getuid()){
char buf[4096];
struct passwd pw, *result;
getpwuid_r(getuid(), &pw, buf, sizeof(buf), &result);
if(result){
2025-10-30 00:06:33 +01:00
libcommon_printf(
"Warning: File %s owner is not %s. Future versions will refuse to load this file."
"To fix this, use `chown %s %s`.\n",
path, result->pw_name, result->pw_name, path);
}
#if 0
// Future version
return NULL;
#endif
}
if(statbuf.st_gid != getgid()){
char buf[4096];
struct group grp, *result;
if(getgrgid_r(getgid(), &grp, buf, sizeof(buf), &result) == 0 && result){
2025-10-30 00:06:33 +01:00
libcommon_printf(
"Warning: File %s group is not %s. Future versions will refuse to load this file.\n",
path, result->gr_name);
}
#if 0
// Future version
return NULL
#endif
}
}
if(!S_ISREG(statbuf.st_mode)){
2025-10-30 00:06:33 +01:00
libcommon_printf("Error: %s is not a file.", path);
2022-10-16 08:59:54 +02:00
fclose(fptr);
return NULL;
}
2022-10-16 08:59:54 +02:00
return fptr;
#endif
}
char *mosquitto_trimblanks(char *str)
{
char *endptr;
if(str == NULL){
return NULL;
}
2026-01-23 00:42:16 +01:00
while(isspace((unsigned char)str[0])){
str++;
}
endptr = &str[strlen(str)-1];
2026-01-23 00:42:16 +01:00
while(endptr > str && isspace((unsigned char)endptr[0])){
endptr[0] = '\0';
endptr--;
}
return str;
}
char *mosquitto_fgets(char **buf, int *buflen, FILE *stream)
{
char *rc;
char endchar;
int offset = 0;
char *newbuf;
size_t len;
if(stream == NULL || buf == NULL || buflen == NULL || *buflen < 1){
return NULL;
}
do{
rc = fgets(&((*buf)[offset]), (*buflen)-offset, stream);
if(feof(stream) || rc == NULL){
return rc;
}
len = strlen(*buf);
if(len == 0){
return rc;
}
endchar = (*buf)[len-1];
if(endchar == '\n'){
return rc;
}
if((int)(len+1) < *buflen){
/* Embedded nulls, invalid string */
return NULL;
}
/* No EOL char found, so extend buffer */
offset = (*buflen)-1;
*buflen += 1000;
2020-10-17 02:23:08 +02:00
newbuf = realloc(*buf, (size_t)*buflen);
if(!newbuf){
return NULL;
}
*buf = newbuf;
}while(1);
}
2022-08-01 23:47:46 +02:00
#define INVOKE_LOG_FN(format, ...) \
2025-09-15 15:06:55 +02:00
do{ \
if(log_fn){ \
int tmp_err_no = errno; \
char msg[2*PATH_MAX]; \
snprintf(msg, sizeof(msg), (format), __VA_ARGS__); \
msg[sizeof(msg)-1] = '\0'; \
(*log_fn)(msg); \
errno = tmp_err_no; \
} \
}while(0)
2025-09-15 12:07:28 +02:00
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))
{
int rc = 0;
FILE *fptr = NULL;
char tmp_file_path[PATH_MAX];
2022-08-01 23:47:46 +02:00
if(!target_path || !write_fn){
return MOSQ_ERR_INVAL;
}
rc = snprintf(tmp_file_path, PATH_MAX, "%s.new", target_path);
2022-08-01 23:47:46 +02:00
if(rc < 0 || rc >= PATH_MAX){
return MOSQ_ERR_INVAL;
}
#ifndef WIN32
/**
2025-04-06 23:40:48 +02:00
*
* If a system lost power during the rename operation at the
* end of this file the filesystem could potentially be left
* with a directory that looks like this after powerup:
*
* 24094 -rw-r--r-- 2 root root 4099 May 30 16:27 mosquitto.db
* 24094 -rw-r--r-- 2 root root 4099 May 30 16:27 mosquitto.db.new
*
* The 24094 shows that mosquitto.db.new is hard-linked to the
* same file as mosquitto.db. If fopen(outfile, "wb") is naively
* called then mosquitto.db will be truncated and the database
* potentially corrupted.
*
* Any existing mosquitto.db.new file must be removed prior to
* opening to guarantee that it is not hard-linked to
* mosquitto.db.
*
*/
2022-08-01 23:47:46 +02:00
if(unlink(tmp_file_path) && errno != ENOENT){
INVOKE_LOG_FN("unable to remove stale tmp file %s, error %s", tmp_file_path, strerror(errno));
return MOSQ_ERR_INVAL;
}
#endif
fptr = mosquitto_fopen(tmp_file_path, "wb", restrict_read);
if(fptr == NULL){
2022-08-01 23:47:46 +02:00
INVOKE_LOG_FN("unable to open %s for writing, error %s", tmp_file_path, strerror(errno));
return MOSQ_ERR_INVAL;
}
2022-08-01 23:47:46 +02:00
if((rc = (*write_fn)(fptr, user_data)) != MOSQ_ERR_SUCCESS){
goto error;
}
rc = MOSQ_ERR_ERRNO;
#ifndef WIN32
/**
*
* Closing a file does not guarantee that the contents are
* written to disk. Need to flush to send data from app to OS
* buffers, then fsync to deliver data from OS buffers to disk
* (as well as disk hardware permits).
*
* man close (http://linux.die.net/man/2/close, 2016-06-20):
*
* "successful close does not guarantee that the data has
* been successfully saved to disk, as the kernel defers
* writes. It is not common for a filesystem to flush
* the buffers when the stream is closed. If you need
* to be sure that the data is physically stored, use
* fsync(2). (It will depend on the disk hardware at this
* point."
*
* This guarantees that the new state file will not overwrite
* the old state file before its contents are valid.
*
*/
2022-08-01 23:47:46 +02:00
if(fflush(fptr) != 0 && errno != EINTR){
INVOKE_LOG_FN("unable to flush %s, error %s", tmp_file_path, strerror(errno));
goto error;
}
2022-08-01 23:47:46 +02:00
if(fsync(fileno(fptr)) != 0 && errno != EINTR){
INVOKE_LOG_FN("unable to sync %s to disk, error %s", tmp_file_path, strerror(errno));
goto error;
}
#endif
2022-08-01 23:47:46 +02:00
if(fclose(fptr) != 0){
INVOKE_LOG_FN("unable to close %s on disk, error %s", tmp_file_path, strerror(errno));
fptr = NULL;
goto error;
}
2022-08-01 23:50:01 +02:00
fptr = NULL;
#ifdef WIN32
if(remove(target_path) != 0 && errno != ENOENT){
2022-08-01 23:47:46 +02:00
INVOKE_LOG_FN("unable to remove %s on disk, error %s", target_path, strerror(errno));
goto error;
}
#endif
if(rename(tmp_file_path, target_path) != 0){
2022-08-01 23:47:46 +02:00
INVOKE_LOG_FN("unable to replace %s by tmp file %s, error %s", target_path, tmp_file_path, strerror(errno));
goto error;
}
return MOSQ_ERR_SUCCESS;
error:
if(fptr){
fclose(fptr);
unlink(tmp_file_path);
}
return MOSQ_ERR_ERRNO;
}
int mosquitto_read_file(const char *file, bool restrict_read, char **buf, size_t *buflen)
{
FILE *fptr;
long l;
size_t buflen_i;
*buf = NULL;
if(buflen){
*buflen = 0;
}
fptr = mosquitto_fopen(file, "rt", restrict_read);
if(fptr == NULL){
return MOSQ_ERR_ERRNO;
}
fseek(fptr, 0, SEEK_END);
l = ftell(fptr);
fseek(fptr, 0, SEEK_SET);
if(l < 0){
fclose(fptr);
return MOSQ_ERR_ERRNO;
}else if(l == 0){
fclose(fptr);
return MOSQ_ERR_SUCCESS;
}
buflen_i = (size_t)l;
*buf = mosquitto_calloc(buflen_i+1, sizeof(char));
if((*buf) == NULL){
fclose(fptr);
return MOSQ_ERR_NOMEM;
}
if(fread(*buf, 1, buflen_i, fptr) != buflen_i){
mosquitto_FREE(*buf);
fclose(fptr);
return MOSQ_ERR_INVAL;
}
fclose(fptr);
if(buflen){
*buflen = buflen_i;
}
return MOSQ_ERR_SUCCESS;
}