686 lines
17 KiB
C
686 lines
17 KiB
C
/*
|
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
*/
|
|
|
|
#include <sys/epoll.h>
|
|
#include <sys/inotify.h>
|
|
#include <sys/ioctl.h>
|
|
#include <sys/socket.h>
|
|
#include <sys/un.h>
|
|
#include <sys/wait.h>
|
|
#include <assert.h>
|
|
#include <dirent.h>
|
|
#include <errno.h>
|
|
#include <signal.h>
|
|
#include <stdbool.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <time.h>
|
|
#include <unistd.h>
|
|
|
|
#include <ini.h>
|
|
|
|
#define UNUSED(x) ((void)x)
|
|
|
|
struct Request
|
|
{
|
|
uint64_t not_after;
|
|
char* file;
|
|
char* socket;
|
|
char* message;
|
|
char* icon;
|
|
pid_t pid;
|
|
bool accept_cached;
|
|
bool echo;
|
|
bool silent;
|
|
};
|
|
|
|
void Request_init(struct Request* req)
|
|
{
|
|
req->not_after = 0;
|
|
req->file = NULL;
|
|
req->socket = NULL;
|
|
req->message = NULL;
|
|
req->icon = NULL;
|
|
req->pid = 0;
|
|
req->accept_cached = false;
|
|
req->echo = false;
|
|
req->silent = false;
|
|
}
|
|
|
|
void Request_free(struct Request* req)
|
|
{
|
|
if (req->file)
|
|
free(req->file);
|
|
if (req->socket)
|
|
free(req->socket);
|
|
if (req->message)
|
|
free(req->message);
|
|
if (req->icon)
|
|
free(req->icon);
|
|
}
|
|
|
|
void Request_reset(struct Request* req)
|
|
{
|
|
Request_free(req);
|
|
Request_init(req);
|
|
}
|
|
|
|
struct Request request;
|
|
int fd_epoll, fd_inotify;
|
|
pid_t pid_unl0kr;
|
|
bool unl0kr_exited;
|
|
|
|
timer_t id_timer;
|
|
enum {
|
|
NO_ACTION,
|
|
TERMINATE_UNL0KR,
|
|
KILL_UNL0KR
|
|
} timer_action;
|
|
bool timer_expired;
|
|
|
|
void erase_and_free(char* p)
|
|
{
|
|
const size_t length = strlen(p);
|
|
for (size_t i = 0; i < length; i++)
|
|
p[i] = 0;
|
|
|
|
free(p);
|
|
}
|
|
|
|
int send_password(const char *password)
|
|
{
|
|
int fd_socket = socket(AF_UNIX, SOCK_DGRAM|SOCK_CLOEXEC, 0);
|
|
if (fd_socket < 0) {
|
|
int ret = errno;
|
|
perror("socket() is failed");
|
|
return ret;
|
|
}
|
|
|
|
struct sockaddr_un address;
|
|
address.sun_family = AF_UNIX;
|
|
strncpy(address.sun_path, request.socket, sizeof(address.sun_path) - 1);
|
|
address.sun_path[sizeof(address.sun_path) - 1] = 0;
|
|
|
|
ssize_t n = sendto(fd_socket, password, strlen(password), MSG_NOSIGNAL, (const struct sockaddr*) &address, sizeof(address));
|
|
if (n < 0) {
|
|
int ret = errno;
|
|
perror("sendto() is failed");
|
|
close(fd_socket);
|
|
return ret;
|
|
}
|
|
|
|
close(fd_socket);
|
|
return 0;
|
|
}
|
|
|
|
bool to_bool(const char* value)
|
|
{
|
|
if (strcmp(value, "true") == 0) {
|
|
return true;
|
|
} else if (strcmp(value, "false") == 0) {
|
|
return false;
|
|
} else if (strcmp(value, "1") == 0) {
|
|
return true;
|
|
} else if (strcmp(value, "0") == 0) {
|
|
return false;
|
|
} else if (strcmp(value, "yes") == 0) {
|
|
return true;
|
|
} else if (strcmp(value, "no") == 0) {
|
|
return false;
|
|
} else {
|
|
fprintf(stderr, "The value '%s' is not a boolean\n", value);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
int ini_parser(void* user, const char* section, const char* name, const char* value)
|
|
{
|
|
struct Request* d = (struct Request*) user;
|
|
|
|
if (strcmp(section, "Ask") != 0) {
|
|
fprintf(stderr, "The ini file contains unknown section: %s\n", section);
|
|
return 0;
|
|
}
|
|
|
|
if (value[0] == 0x00) {
|
|
fprintf(stderr, "The ini file contains a key without a value: %s\n", name);
|
|
return 0;
|
|
}
|
|
|
|
if (strcmp(name, "NotAfter") == 0) {
|
|
d->not_after = atol(value);
|
|
} else if (strcmp(name, "Socket") == 0) {
|
|
d->socket = strdup(value);
|
|
} else if (strcmp(name, "Message") == 0) {
|
|
d->message = strdup(value);
|
|
} else if (strcmp(name, "Icon") == 0) {
|
|
d->icon = strdup(value);
|
|
} else if (strcmp(name, "PID") == 0) {
|
|
d->pid = atoi(value);
|
|
} else if (strcmp(name, "AcceptCached") == 0) {
|
|
d->accept_cached = to_bool(value);
|
|
} else if (strcmp(name, "Echo") == 0) {
|
|
d->echo = to_bool(value);
|
|
} else if (strcmp(name, "Silent") == 0) {
|
|
d->silent = to_bool(value);
|
|
} else {
|
|
fprintf(stderr, "The ini file contains unknown key: %s = %s\n", name, value);
|
|
return 0;
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
int find_request(char** ret_file)
|
|
{
|
|
const char* ask_folder = "/run/systemd/ask-password";
|
|
const size_t ask_folder_length = strlen(ask_folder);
|
|
|
|
DIR* dir = opendir(ask_folder);
|
|
if (!dir) {
|
|
int ret = errno;
|
|
if (errno != ENOENT) {
|
|
fprintf(stderr, "Can't open '%s': %s\n", ask_folder, strerror(errno));
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
struct dirent* entry;
|
|
while ((entry = readdir(dir))) {
|
|
if (entry->d_type != DT_REG && entry->d_type != DT_LNK)
|
|
continue;
|
|
|
|
if (strncmp(entry->d_name, "ask.", 4) != 0)
|
|
continue;
|
|
|
|
break;
|
|
}
|
|
if (!entry) {
|
|
closedir(dir);
|
|
return ENOENT;
|
|
}
|
|
|
|
char* file = malloc(ask_folder_length + 1 + strlen(entry->d_name) + 1);
|
|
if (!file) {
|
|
closedir(dir);
|
|
fprintf(stderr, "Out of memory\n");
|
|
return ENOMEM;
|
|
}
|
|
|
|
strcpy(file, ask_folder);
|
|
strcpy(file + ask_folder_length, "/");
|
|
strcpy(file + ask_folder_length + 1, entry->d_name);
|
|
|
|
closedir(dir);
|
|
|
|
*ret_file = file;
|
|
return 0;
|
|
}
|
|
|
|
int process_inotify_events()
|
|
{
|
|
/* We expect only IN_DELETE_SELF and IN_IGNORED */
|
|
size_t buffer_size = sizeof(struct inotify_event) * 2;
|
|
uint8_t buffer[buffer_size];
|
|
|
|
ssize_t block_size = read(fd_inotify, buffer, buffer_size);
|
|
if (block_size < 0) {
|
|
int ret = errno;
|
|
perror("read() is failed");
|
|
return ret;
|
|
}
|
|
|
|
assert((size_t) block_size == buffer_size);
|
|
|
|
struct inotify_event* ievent1 = (struct inotify_event*) buffer;
|
|
struct inotify_event* ievent2 = ievent1 + 1;
|
|
|
|
assert(ievent1->mask & IN_DELETE_SELF);
|
|
assert(ievent2->mask & IN_IGNORED);
|
|
UNUSED(ievent2);
|
|
|
|
assert(read(fd_inotify, buffer, buffer_size) == -1 && errno == EAGAIN); // no more events
|
|
return 0;
|
|
}
|
|
|
|
void sigalarm(int signo, siginfo_t *info, void *context)
|
|
{
|
|
assert(signo == SIGALRM);
|
|
UNUSED(signo);
|
|
UNUSED(context);
|
|
|
|
if (info->si_code == SI_TIMER) {
|
|
assert(info->si_value.sival_ptr == &id_timer);
|
|
timer_expired = true;
|
|
}
|
|
|
|
switch (timer_action) {
|
|
case TERMINATE_UNL0KR:
|
|
if (kill(pid_unl0kr, SIGTERM) == 0) {
|
|
struct itimerspec spec;
|
|
spec.it_interval.tv_sec = 0;
|
|
spec.it_interval.tv_nsec = 0;
|
|
spec.it_value.tv_sec = 5;
|
|
spec.it_value.tv_nsec = 0;
|
|
timer_settime(id_timer, 0, &spec, NULL);
|
|
timer_action = KILL_UNL0KR;
|
|
}
|
|
break;
|
|
case KILL_UNL0KR:
|
|
kill(pid_unl0kr, SIGKILL);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
void sigchild(int signo, siginfo_t *info, void *context)
|
|
{
|
|
assert(signo == SIGCHLD);
|
|
UNUSED(signo);
|
|
UNUSED(context);
|
|
|
|
if (pid_unl0kr == 0)
|
|
return;
|
|
|
|
assert(info->si_pid == pid_unl0kr);
|
|
|
|
if (info->si_code == CLD_EXITED ||
|
|
info->si_code == CLD_KILLED ||
|
|
info->si_code == CLD_DUMPED ) {
|
|
unl0kr_exited = true;
|
|
}
|
|
}
|
|
|
|
int event_loop(pid_t pid)
|
|
{
|
|
int ret = 0;
|
|
int r;
|
|
|
|
if (request.not_after != 0) {
|
|
struct itimerspec spec;
|
|
spec.it_interval.tv_sec = 0;
|
|
spec.it_interval.tv_nsec = 0;
|
|
spec.it_value.tv_sec = request.not_after / 1000000;
|
|
spec.it_value.tv_nsec = (request.not_after % 1000000) * 1000;
|
|
r = timer_settime(id_timer, TIMER_ABSTIME, &spec, NULL);
|
|
if (r == -1)
|
|
perror("timer_settime() is failed");
|
|
}
|
|
|
|
struct epoll_event event;
|
|
pid_unl0kr = pid;
|
|
timer_action = TERMINATE_UNL0KR;
|
|
timer_expired = false;
|
|
unl0kr_exited = false;
|
|
|
|
sigset_t sigmask;
|
|
sigemptyset(&sigmask);
|
|
|
|
for (;;) {
|
|
r = epoll_pwait(fd_epoll, &event, 1, -1, &sigmask);
|
|
if (r == -1) {
|
|
if (errno != EINTR) {
|
|
ret = errno;
|
|
perror("epoll_pwait() is failed");
|
|
break;
|
|
}
|
|
|
|
if (unl0kr_exited)
|
|
break;
|
|
|
|
if (timer_expired && ret != ECANCELED) {
|
|
ret = ETIME;
|
|
fprintf(stderr, "The request has expired\n");
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
r = process_inotify_events();
|
|
if (r != 0) {
|
|
ret = errno;
|
|
break;
|
|
}
|
|
|
|
ret = ECANCELED;
|
|
fprintf(stderr, "The request was cancelled\n");
|
|
|
|
if (timer_expired)
|
|
continue;
|
|
|
|
if (request.not_after != 0) {
|
|
struct itimerspec spec;
|
|
spec.it_interval.tv_sec = 0;
|
|
spec.it_interval.tv_nsec = 0;
|
|
spec.it_value.tv_sec = 0;
|
|
spec.it_value.tv_nsec = 0;
|
|
r = timer_settime(id_timer, 0, &spec, NULL);
|
|
if (r == -1)
|
|
perror("Disarming the timer is failed");
|
|
}
|
|
|
|
r = raise(SIGALRM);
|
|
if (r != 0) {
|
|
ret = errno;
|
|
perror("raise() is failed");
|
|
break;
|
|
}
|
|
}
|
|
|
|
timer_action = NO_ACTION;
|
|
pid_unl0kr = 0;
|
|
|
|
/* Stop the timer unconditionally because it can be armed by sigalarm() */
|
|
struct itimerspec spec;
|
|
spec.it_interval.tv_sec = 0;
|
|
spec.it_interval.tv_nsec = 0;
|
|
spec.it_value.tv_sec = 0;
|
|
spec.it_value.tv_nsec = 0;
|
|
r = timer_settime(id_timer, 0, &spec, NULL);
|
|
if (r == -1)
|
|
perror("Disarming the timer is failed");
|
|
|
|
return ret;
|
|
}
|
|
|
|
int exec_unl0kr(char** ret_password)
|
|
{
|
|
int ret = 0;
|
|
int r;
|
|
|
|
int fd_pipe[2];
|
|
if (pipe(fd_pipe) != 0) {
|
|
ret = errno;
|
|
perror("Can't create a pipe");
|
|
return ret;
|
|
}
|
|
|
|
sigset_t used_signals;
|
|
sigemptyset(&used_signals);
|
|
sigaddset(&used_signals, SIGCHLD);
|
|
sigaddset(&used_signals, SIGALRM);
|
|
|
|
r = sigprocmask(SIG_BLOCK, &used_signals, NULL);
|
|
if (r == -1) {
|
|
ret = errno;
|
|
perror("sigprocmask(SIG_BLOCK) is failed");
|
|
goto exit1;
|
|
}
|
|
|
|
pid_t pid = fork();
|
|
if (pid == -1) {
|
|
ret = errno;
|
|
perror("fork() is failed");
|
|
goto exit2;
|
|
}
|
|
if (pid == 0) {
|
|
/* Child */
|
|
close(fd_pipe[0]);
|
|
|
|
if (dup2(fd_pipe[1], STDOUT_FILENO) == -1) {
|
|
perror("dup2() is failed");
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
|
|
char* argv[5];
|
|
int argc = 2;
|
|
argv[0] = UNL0KR_BINARY;
|
|
argv[1] = "-n";
|
|
if (request.message) {
|
|
argv[2] = "-m";
|
|
argv[3] = request.message;
|
|
argc += 2;
|
|
}
|
|
argv[argc] = NULL;
|
|
|
|
execv(UNL0KR_BINARY, argv);
|
|
|
|
perror("exec() is failed");
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
|
|
/* Parent */
|
|
r = event_loop(pid);
|
|
|
|
int status;
|
|
if (waitpid(pid, &status, 0) == -1) {
|
|
ret = errno;
|
|
perror("waitpid() is failed");
|
|
goto exit2;
|
|
}
|
|
|
|
if (r != 0) {
|
|
ret = r;
|
|
goto exit2;
|
|
}
|
|
|
|
if (!WIFEXITED(status)) {
|
|
ret = ECHILD;
|
|
fprintf(stderr, "unl0kr terminated abnormally\n");
|
|
goto exit2;
|
|
}
|
|
|
|
int password_size;
|
|
if (ioctl(fd_pipe[0], FIONREAD, &password_size) == -1) {
|
|
ret = errno;
|
|
perror("ioctl() is failed");
|
|
goto exit2;
|
|
}
|
|
|
|
char* password = malloc(1 + password_size + 1);
|
|
if (!password) {
|
|
ret = ENOMEM;
|
|
fprintf(stderr, "Out of memory\n");
|
|
goto exit2;
|
|
}
|
|
|
|
password[0] = '+';
|
|
if (password_size != 0) {
|
|
password_size = read(fd_pipe[0], password + 1, password_size);
|
|
if (password_size == -1) {
|
|
ret = errno;
|
|
perror("read() is failed");
|
|
free(password);
|
|
goto exit2;
|
|
}
|
|
}
|
|
password[1 + password_size] = 0;
|
|
|
|
*ret_password = password;
|
|
exit2:
|
|
r = sigprocmask(SIG_UNBLOCK, &used_signals, NULL);
|
|
if (r == -1)
|
|
perror("sigprocmask(SIG_UNBLOCK) is failed");
|
|
exit1:
|
|
close(fd_pipe[0]);
|
|
close(fd_pipe[1]);
|
|
return ret;
|
|
}
|
|
|
|
int wait_for_file_removed()
|
|
{
|
|
struct epoll_event event;
|
|
|
|
int r = epoll_wait(fd_epoll, &event, 1, 20000);
|
|
if (r == -1) {
|
|
int ret = errno;
|
|
perror("epoll_wait() is failed");
|
|
return ret;
|
|
} else if (r == 0) {
|
|
fprintf(stderr, "The file '%s' was not removed as expected, exiting.\n", request.file);
|
|
return ETIME;
|
|
}
|
|
|
|
return process_inotify_events();
|
|
}
|
|
|
|
int main()
|
|
{
|
|
int exit_code = EXIT_SUCCESS;
|
|
int r;
|
|
|
|
Request_init(&request);
|
|
|
|
fd_epoll = epoll_create1(EPOLL_CLOEXEC);
|
|
if (fd_epoll == -1) {
|
|
perror("epoll_create1() is failed");
|
|
exit_code = EXIT_FAILURE;
|
|
goto exit1;
|
|
}
|
|
|
|
fd_inotify = inotify_init1(IN_NONBLOCK|IN_CLOEXEC);
|
|
if (fd_inotify == -1) {
|
|
perror("inotify_init1() is failed");
|
|
exit_code = EXIT_FAILURE;
|
|
goto exit2;
|
|
}
|
|
|
|
struct epoll_event epevent_inotify;
|
|
epevent_inotify.events = EPOLLIN|EPOLLET;
|
|
epevent_inotify.data.fd = fd_inotify;
|
|
|
|
r = epoll_ctl(fd_epoll, EPOLL_CTL_ADD, fd_inotify, &epevent_inotify);
|
|
if (r == -1) {
|
|
perror("epoll_ctl() is failed");
|
|
exit_code = EXIT_FAILURE;
|
|
goto exit3;
|
|
}
|
|
|
|
struct sigevent sigevent_timer;
|
|
sigevent_timer.sigev_notify = SIGEV_SIGNAL;
|
|
sigevent_timer.sigev_signo = SIGALRM;
|
|
sigevent_timer.sigev_value.sival_ptr = &id_timer;
|
|
|
|
r = timer_create(CLOCK_MONOTONIC, &sigevent_timer, &id_timer);
|
|
if (r == -1) {
|
|
perror("timer_create() is failed");
|
|
exit_code = EXIT_FAILURE;
|
|
goto exit3;
|
|
}
|
|
|
|
struct sigaction sigaction_alarm;
|
|
sigaction_alarm.sa_sigaction = sigalarm;
|
|
sigaction_alarm.sa_flags = SA_SIGINFO;
|
|
sigemptyset(&sigaction_alarm.sa_mask);
|
|
sigaddset(&sigaction_alarm.sa_mask, SIGCHLD);
|
|
|
|
r = sigaction(SIGALRM, &sigaction_alarm, NULL);
|
|
if (r == -1) {
|
|
perror("sigaction() for SIGALRM is failed");
|
|
exit_code = EXIT_FAILURE;
|
|
goto exit4;
|
|
}
|
|
|
|
struct sigaction sigaction_child;
|
|
sigaction_child.sa_sigaction = sigchild;
|
|
sigaction_child.sa_flags = SA_SIGINFO | SA_NOCLDSTOP;
|
|
sigemptyset(&sigaction_child.sa_mask);
|
|
sigaddset(&sigaction_child.sa_mask, SIGALRM);
|
|
|
|
r = sigaction(SIGCHLD, &sigaction_child, NULL);
|
|
if (r == -1) {
|
|
perror("sigaction() for SIGCHLD is failed");
|
|
exit_code = EXIT_FAILURE;
|
|
goto exit4;
|
|
}
|
|
|
|
for (;;) {
|
|
char* file;
|
|
r = find_request(&file);
|
|
if (r != 0) {
|
|
if (r != ENOENT)
|
|
exit_code = EXIT_FAILURE;
|
|
break;
|
|
}
|
|
|
|
int wd_inotify = inotify_add_watch(fd_inotify, file, IN_DELETE_SELF | IN_DONT_FOLLOW);
|
|
if (wd_inotify == -1) {
|
|
fprintf(stderr, "inotify_add_watch() is failed for '%s': %s\n", file, strerror(errno));
|
|
free(file);
|
|
exit_code = EXIT_FAILURE;
|
|
break;
|
|
}
|
|
|
|
Request_reset(&request);
|
|
request.file = file;
|
|
|
|
r = ini_parse(file, ini_parser, &request);
|
|
if (r < 0) {
|
|
fprintf(stderr, "The file '%s' can't be parsed: %d\n", request.file, r);
|
|
exit_code = EXIT_FAILURE;
|
|
break;
|
|
}
|
|
|
|
if (request.pid != 0) {
|
|
r = kill(request.pid, 0);
|
|
if (r == -1 && errno == ESRCH) {
|
|
fprintf(stderr, "The file '%s' contains invalid PID, removing.\n", request.file);
|
|
remove(request.file);
|
|
goto loop1;
|
|
}
|
|
}
|
|
|
|
if (!request.socket) {
|
|
fprintf(stderr, "The file '%s' doesn't contain a socket, waiting for removal.\n", request.file);
|
|
goto loop1;
|
|
}
|
|
|
|
if (request.not_after != 0) {
|
|
struct timespec ts;
|
|
r = clock_gettime(CLOCK_MONOTONIC, &ts);
|
|
if (r == -1) {
|
|
perror("clock_gettime() is failed");
|
|
exit_code = EXIT_FAILURE;
|
|
break;
|
|
}
|
|
uint64_t now = ts.tv_sec * 1000000 + ts.tv_nsec / 1000;
|
|
if (request.not_after <= now) {
|
|
fprintf(stderr, "The request '%s' expired, waiting for removal.\n", request.file);
|
|
goto loop1;
|
|
}
|
|
}
|
|
|
|
char* password;
|
|
r = exec_unl0kr(&password);
|
|
if (r != 0) {
|
|
if (r == ECANCELED)
|
|
continue;
|
|
else if (r == ETIME) {
|
|
send_password("-");
|
|
goto loop1;
|
|
} else {
|
|
exit_code = EXIT_FAILURE;
|
|
break;
|
|
}
|
|
}
|
|
|
|
r = send_password(password);
|
|
erase_and_free(password);
|
|
if (r != 0) {
|
|
exit_code = EXIT_FAILURE;
|
|
break;
|
|
}
|
|
|
|
loop1:
|
|
r = wait_for_file_removed();
|
|
if (r != 0) {
|
|
exit_code = EXIT_FAILURE;
|
|
break;
|
|
}
|
|
}
|
|
|
|
exit4:
|
|
timer_delete(id_timer);
|
|
exit3:
|
|
close(fd_inotify);
|
|
exit2:
|
|
close(fd_epoll);
|
|
exit1:
|
|
Request_free(&request);
|
|
return exit_code;
|
|
}
|