尽管 Antti Haapala 已经完全回答了这个问题,但我认为关于该方法的一些评论以及一个使安全使用变得微不足道的示例函数可能会有用。
xdg-open
是来自 freedesktop.org 的桌面集成实用程序的一部分,是Portland 项目的一部分。人们可以期望它们可以在任何运行参与 freedesktop.org的桌面环境的计算机上使用。这包括 GNOME、KDE 和 Xfce。
简而言之,这是在使用桌面环境时打开资源(无论是文件还是 URL)的推荐方式,无论用户喜欢什么应用程序。
如果没有使用桌面环境,那么也没有理由期望xdg-open
它可用。
对于 Linux,我建议使用一个专用函数,可能遵循以下几行。首先,几个内部辅助函数:
#define _POSIX_C_SOURCE 200809L
#define _GNU_SOURCE
//
// SPDX-License-Identifier: CC0-1.0
//
#include <stdlib.h>
#include <unistd.h>
#include <limits.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <dirent.h>
#include <fcntl.h>
#include <string.h>
#include <stdio.h>
#include <errno.h>
/* Number of bits in an unsigned long. */
#define ULONG_BITS (CHAR_BIT * sizeof (unsigned long))
/* Helper function to open /dev/null to a specific descriptor.
*/
static inline int devnullfd(const int fd)
{
int tempfd;
/* Sanity check. */
if (fd == -1)
return errno = EINVAL;
do {
tempfd = open("/dev/null", O_RDWR | O_NOCTTY);
} while (tempfd == -1 && errno == EINTR);
if (tempfd == -1)
return errno;
if (tempfd != fd) {
if (dup2(tempfd, fd) == -1) {
const int saved_errno = errno;
close(tempfd);
return errno = saved_errno;
}
if (close(tempfd) == -1)
return errno;
}
return 0;
}
/* Helper function to close all except small descriptors
specified in the mask. For obvious reasons, this is not
thread safe, and is only intended to be used in recently
forked child processes. */
static void closeall(const unsigned long mask)
{
DIR *dir;
struct dirent *ent;
int dfd;
dir = opendir("/proc/self/fd/");
if (!dir) {
/* Cannot list open descriptors. Just try and close all. */
const long fd_max = sysconf(_SC_OPEN_MAX);
long fd;
for (fd = 0; fd < ULONG_BITS; fd++)
if (!(mask & (1uL << fd)))
close(fd);
for (fd = ULONG_BITS; fd <= fd_max; fd++)
close(fd);
return;
}
dfd = dirfd(dir);
while ((ent = readdir(dir)))
if (ent->d_name[0] >= '0' && ent->d_name[0] <= '9') {
const char *p = &ent->d_name[1];
int fd = ent->d_name[0] - '0';
while (*p >= '0' && *p <= '9')
fd = (10 * fd) + *(p++) - '0';
if (*p)
continue;
if (fd == dfd)
continue;
if (fd < ULONG_MAX && (mask & (1uL << fd)))
continue;
close(fd);
}
closedir(dir);
}
closeall(0)
努力关闭所有打开的文件描述符,并devnullfd(fd)
尝试打开fd
到/dev/null
. 这些用于确保即使用户欺骗xdg-open
,也不会泄露文件描述符;仅传递文件名或 URL。
在非 Linux POSIXy 系统上,您可以将它们替换为更合适的东西。在 BSD 上,使用closefrom()
并处理ULONG_MAX
循环中的第一个描述符。
函数xdg_open(file-or-url)
本身类似于
/* Launch the user-preferred application to open a file or URL.
Returns 0 if success, an errno error code otherwise.
*/
int xdg_open(const char *file_or_url)
{
pid_t child, p;
int status;
/* Sanity check. */
if (!file_or_url || !*file_or_url)
return errno = EINVAL;
/* Fork the child process. */
child = fork();
if (child == -1)
return errno;
else
if (!child) {
/* Child process. */
uid_t uid = getuid(); /* Real, not effective, user. */
gid_t gid = getgid(); /* Real, not effective, group. */
/* Close all open file descriptors. */
closeall(0);
/* Redirect standard streams, if possible. */
devnullfd(STDIN_FILENO);
devnullfd(STDOUT_FILENO);
devnullfd(STDERR_FILENO);
/* Drop elevated privileges, if any. */
if (setresgid(gid, gid, gid) == -1 ||
setresuid(uid, uid, uid) == -1)
_Exit(98);
/* Have the child process execute in a new process group. */
setsid();
/* Execute xdg-open. */
execlp("xdg-open", "xdg-open", file_or_url, (char *)0);
/* Failed. xdg-open uses 0-5, we return 99. */
_Exit(99);
}
/* Reap the child. */
do {
status = 0;
p = waitpid(child, &status, 0);
} while (p == -1 && errno == EINTR);
if (p == -1)
return errno;
if (!WIFEXITED(status)) {
/* Killed by a signal. Best we can do is I/O error, I think. */
return errno = EIO;
}
switch (WEXITSTATUS(status)) {
case 0: /* No error. */
return errno = 0; /* It is unusual, but robust to explicitly clear errno. */
case 1: /* Error in command line syntax. */
return errno = EINVAL; /* Invalid argument */
case 2: /* File does not exist. */
return errno = ENOENT; /* No such file or directory */
case 3: /* A required tool could not be found. */
return errno = ENOSYS; /* Not implemented */
case 4: /* Action failed. */
return errno = EPROTO; /* Protocol error */
case 98: /* Identity shenanigans. */
return errno = EACCES; /* Permission denied */
case 99: /* xdg-open does not exist. */
return errno = ENOPKG; /* Package not installed */
default:
/* None of the other values should occur. */
return errno = ENOSYS; /* Not implemented */
}
}
如前所述,它会努力关闭所有打开的文件描述符,将标准流重定向到/dev/null
,确保有效和真实的身份匹配(如果在 setuid 二进制文件中使用),并使用子进程退出状态传递成功/失败.
setresuid()
和setresgid()
调用仅在保存了用户和组 ID 的操作系统上可用。在其他人身上,使用seteuid(uid)
andsetegid()
代替。
此实现试图平衡用户可配置性和安全性。用户可以设置PATH
他们最喜欢xdg-open
的执行,但该功能试图确保没有敏感信息或特权泄露给该进程。
(环境变量可以被过滤,但它们首先不应该包含敏感信息,而且我们并不真正知道桌面环境使用哪些变量。所以最好不要乱用它们,以尽量减少用户的意外。)
作为最小测试main()
,请尝试以下操作:
int main(int argc, char *argv[])
{
int arg, status;
if (argc < 2 || !strcmp(argv[1], "-h") || !strcmp(argv[1], "--help")) {
fprintf(stderr, "\n");
fprintf(stderr, "Usage: %s [ -h | --help ]\n", argv[0]);
fprintf(stderr, " %s FILE-OR-URL ...\n", argv[0]);
fprintf(stderr, "\n");
fprintf(stderr, "This example program opens each specified file or URL\n");
fprintf(stderr, "xdg-open(1), and outputs success or failure for each.\n");
fprintf(stderr, "\n");
return EXIT_SUCCESS;
}
status = EXIT_SUCCESS;
for (arg = 1; arg < argc; arg++)
if (xdg_open(argv[arg])) {
printf("%s: %s.\n", argv[arg], strerror(errno));
status = EXIT_FAILURE;
} else
printf("%s: Opened.\n", argv[arg]);
return status;
}
正如 SPDX 许可证标识符所述,此示例代码在Creative Commons Zero 1.0下获得许可。以任何你想要的方式使用它,在你想要的任何代码中。