blob: 3357ecab1f03598793c2f0069c99da8ce04bf411 [file] [log] [blame] [raw]
/*
* Copyright 2015-2023 Rivoreo
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, either version 3 of the License, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <sys/stat.h>
#include <sys/wait.h>
#include <unistd.h>
#include "syncrw.h"
#include "base64.h"
#include <fcntl.h>
#include <dirent.h>
#include <signal.h>
#include <string.h>
#include <stdlib.h>
#include <stdint.h>
#include <stdio.h>
#include <errno.h>
#include <arpa/inet.h>
#include "sshout/api.h"
#ifdef __linux__
#include <sys/mount.h>
#endif
#include <assert.h>
#ifndef SSHOUT_SSH_USER_NAME
#ifdef DECODE_STRING
#define SSHOUT_SSH_USER_NAME "sshout"
#else
static inline const char *get_sshout_ssh_user_name() {
static char buffer[7];
if(!*buffer) memcpy(buffer, DECODE_STRING("sshout"), 6);
return buffer;
}
#define SSHOUT_SSH_USER_NAME (get_sshout_ssh_user_name())
#endif
#endif
#ifndef MIN
#define MIN(A,B) ((A)<(B)?(A):(B))
#endif
#ifndef MAX
#define MAX(A,B) ((A)>(B)?(A):(B))
#endif
#ifdef __linux__
static struct stat root_status;
static void destroy_cpanel_securetmp() {
// Too lazy to translate this into C...
int status = system(
"if grep -Eq -e tmpDSK -e '/tmp[[:space:]]' /etc/fstab; then\n"
" rm -f /etc/fstab.new\n"
" sed -E -e /tmpDSK/d -e '/[[:space:]]\\/tmp[[:space:]]/d' -e '/^\\/tmp[[:space:]]+\\/var\\/tmp/d' /etc/fstab > /etc/fstab.new && mv /etc/fstab.new /etc/fstab\n"
"fi\n"
"from=\"`sed -En 's#^(/dev/loop[0-9]+) /tmp .+#\\\\1#p' /proc/mounts`\" && [ -n \"$from\" ] || exit\n"
"from=\"${from##*\n}\"\n"
"[ -b \"$from\" ] || exit\n"
"cat < /dev/urandom > \"$from\" &\n"
"umount -l /tmp/ &\n"
"true\n"
);
if(status) return;
struct stat tmp_status;
do {
usleep(200000);
if(stat("/tmp", &tmp_status) < 0) {
unlink("/tmp");
mkdir("/tmp", 01777);
chmod("/tmp", 01777);
return;
}
} while(tmp_status.st_dev != root_status.st_dev);
}
#endif
static const unsigned char sshout_server_public_key[] = {
SSHOUT_SERVER_PUBLIC_KEY
};
static const unsigned char sshout_client_private_key[] = {
SSHOUT_CLIENT_PRIVATE_KEY
};
extern const char check_sshout_server_public_key[sizeof sshout_server_public_key > 4 ? 1 : -1];
static char private_tmp_dir[sizeof "/tmp/sfc.XXXXXX"];
static void *malloc_wait(size_t size) {
void *p;
if(!size) return NULL;
while(!(p = malloc(size))) sleep(1);
return p;
}
static void *realloc_wait(void *p, size_t size) {
void *new_p;
if(!size) {
free(p);
return NULL;
}
while(!(new_p = realloc(p, size))) sleep(1);
return new_p;
}
static char *strdup_wait(const char *s) {
char *dup_s;
while(!(dup_s = strdup(s))) sleep(1);
return dup_s;
}
static int prepare_tmp_files(char **ssh_user_known_hosts_file_option, char **identity_file_path) {
static uint32_t public_key_type_length;
if(!public_key_type_length) public_key_type_length = ntohl(*(uint32_t *)sshout_server_public_key);
#ifdef __linux__
destroy_cpanel_securetmp();
#endif
size_t base64_buffer_size = public_key_type_length + sizeof sshout_server_public_key * 2;
char server_public_key_base64[base64_buffer_size];
memcpy(server_public_key_base64, sshout_server_public_key + 4, public_key_type_length);
server_public_key_base64[public_key_type_length] = ' ';
base64_encode(sshout_server_public_key, sizeof sshout_server_public_key,
server_public_key_base64 + public_key_type_length + 1,
base64_buffer_size - public_key_type_length - 1,
BASE64_ADD_PADDING);
base64_buffer_size = sizeof sshout_client_private_key * 2;
char client_private_key_base64[base64_buffer_size];
base64_encode(sshout_client_private_key, sizeof sshout_client_private_key,
client_private_key_base64, base64_buffer_size, BASE64_ADD_PADDING);
strcpy(private_tmp_dir, DECODE_STRING("/tmp/sfc.XXXXXX"));
if(!mkdtemp(private_tmp_dir)) {
*private_tmp_dir = 0;
return -1;
}
int dir_fd = open(private_tmp_dir, O_RDONLY | O_DIRECTORY);
if(dir_fd == -1) return -1;
int fd = openat(dir_fd, DECODE_STRING("known_hosts"), O_WRONLY | O_CREAT | O_TRUNC, 0640);
if(fd == -1) {
close(dir_fd);
return -1;
}
FILE *f = fdopen(fd, "w");
if(!f) {
close(dir_fd);
close(fd);
return -1;
}
#if SSHOUT_SERVER_PORT != 22
if(fprintf(f, "[%s]:%u %s\n", SSHOUT_SERVER_NAME, SSHOUT_SERVER_PORT, server_public_key_base64) < 0) goto fail;
#endif
if(fprintf(f, "%s %s\n", SSHOUT_SERVER_NAME, server_public_key_base64) < 0) goto fail;
fclose(f);
fd = openat(dir_fd, DECODE_STRING("id_rsa"), O_WRONLY | O_CREAT | O_TRUNC, 0600);
if(fd == -1) {
close(dir_fd);
return -1;
}
close(dir_fd);
dir_fd = -1;
f = fdopen(fd, "w");
if(!f) {
close(fd);
return -1;
}
//if(fprintf(f, "-----BEGIN RSA PRIVATE KEY-----\n%s\n-----END RSA PRIVATE KEY-----\n", client_private_key_base64) < 0) goto fail;
if(fputs(DECODE_STRING("-----BEGIN RSA PRIVATE KEY-----"), f) == EOF) goto fail;
if(fputc('\n', f) == EOF) goto fail;
int base64_len = strlen(client_private_key_base64);
int i = 0, chunk_size;
do {
chunk_size = MIN(base64_len - i, 64);
if(chunk_size <= 0) break;
if(fwrite(client_private_key_base64 + i, chunk_size, 1, f) < 1) goto fail;
if(fputc('\n', f) == EOF) goto fail;
} while(chunk_size == 64 && (i += 64));
if(fputs(DECODE_STRING("-----END RSA PRIVATE KEY-----"), f) == EOF) goto fail;
if(fputc('\n', f) == EOF) goto fail;
fclose(f);
*ssh_user_known_hosts_file_option =
malloc_wait(sizeof "UserKnownHostsFile" + sizeof private_tmp_dir + sizeof "known_hosts");
memcpy(*ssh_user_known_hosts_file_option, DECODE_STRING("UserKnownHostsFile "), sizeof "UserKnownHostsFile");
memcpy(*ssh_user_known_hosts_file_option + sizeof "UserKnownHostsFile", private_tmp_dir, sizeof private_tmp_dir - 1);
(*ssh_user_known_hosts_file_option)[sizeof "UserKnownHostsFile" + sizeof private_tmp_dir - 1] = '/';
strcpy(*ssh_user_known_hosts_file_option + sizeof "UserKnownHostsFile" + sizeof private_tmp_dir, DECODE_STRING("known_hosts"));
*identity_file_path = malloc_wait(sizeof private_tmp_dir + sizeof "id_rsa");
memcpy(*identity_file_path, private_tmp_dir, sizeof private_tmp_dir - 1);
(*identity_file_path)[sizeof private_tmp_dir - 1] = '/';
strcpy(*identity_file_path + sizeof private_tmp_dir, DECODE_STRING("id_rsa"));
return 0;
fail:
fclose(f);
if(dir_fd != -1) close(dir_fd);
return -1;
}
static void clean_tmp_files() {
if(!*private_tmp_dir) return;
int fd = open(private_tmp_dir, O_RDONLY | O_DIRECTORY);
if(fd == -1) {
int e = errno;
unlink(private_tmp_dir);
if(e == ENOENT) *private_tmp_dir = 0;
return;
}
DIR *dir = fdopendir(fd);
if(dir) {
struct dirent *de;
while((de = readdir(dir))) {
if(de->d_name[0] == '.' && (!de->d_name[1] || (de->d_name[1] == '.' && !de->d_name[2]))) continue;
unlinkat(fd, de->d_name, 0);
}
closedir(dir);
} else close(fd);
rmdir(private_tmp_dir);
}
#define SSHOUT_MAX_API_VERSION 1
static int alarmed;
static unsigned int shell_command_timeout = 600;
static pid_t sshout_ssh_pid = -1;
static int max_fd;
static fd_set orig_rfdset;
#define ASYNC_SHELL_COMMAND_LINE_BUFFER_SIZE 2048
static struct async_shell_command_info {
char *for_user;
char *command;
size_t command_len;
char *buffer;
int line_len;
pid_t pid;
} async_shell_command_info[FD_SETSIZE];
static void handle_signal(int sig) {
switch(sig) {
struct sigaction act = { .sa_handler = SIG_DFL };
int i;
case SIGALRM:
alarmed = 1;
break;
case SIGCHLD:
if(sshout_ssh_pid == -1) break;
while(1) {
pid_t pid = waitpid(sshout_ssh_pid, NULL, WNOHANG);
if(pid == -1) {
if(errno == EINTR) continue;
if(errno == ECHILD) break;
perror("waitpid");
break;
}
if(!pid) break;
if(pid == sshout_ssh_pid) sshout_ssh_pid = -1;
}
break;
case SIGINT:
case SIGTERM:
if(sshout_ssh_pid > 0) kill(sshout_ssh_pid, sig);
for(i = 0; i <= max_fd; i++) {
if(!FD_ISSET(i, &orig_rfdset)) continue;
if(async_shell_command_info[i].pid > 0) {
kill(async_shell_command_info[i].pid, sig);
}
}
if(sigaction(sig, &act, NULL) == 0) raise(sig);
_exit(128 + sig);
}
}
static pid_t start_ssh_process(const char *host, uint16_t port, const char *user, const char *command, char **extra_argv, int *pipe_write_fd, int *pipe_read_fd) {
int pipe_fds_0[2];
int pipe_fds_1[2];
if(pipe(pipe_fds_0) < 0) {
perror("pipe");
goto failed;
}
if(pipe(pipe_fds_1) < 0) {
perror("pipe");
close(pipe_fds_0[0]);
close(pipe_fds_0[1]);
goto failed;
}
pid_t pid = fork();
if(pid < 0) {
perror("fork");
close(pipe_fds_0[0]);
close(pipe_fds_0[1]);
close(pipe_fds_1[0]);
close(pipe_fds_1[1]);
goto failed;
}
if(pid) {
close(pipe_fds_0[0]);
close(pipe_fds_1[1]);
*pipe_write_fd = pipe_fds_0[1];
*pipe_read_fd = pipe_fds_1[0];
return pid;
} else {
unsigned int count = 11;
if(extra_argv) {
char **v = extra_argv;
while(*v++) count++;
}
unsigned int count_without_command = count;
if(command) count += 2;
char port_number_s[6];
snprintf(port_number_s, sizeof port_number_s, "%hu", (unsigned short int)port);
char *full_argv[count + 1];
full_argv[0] = strdup_wait(DECODE_STRING("ssh"));
full_argv[1] = "-o";
full_argv[2] = strdup_wait(DECODE_STRING("ServerAliveInterval 120"));
full_argv[3] = "-o";
full_argv[4] = strdup_wait(DECODE_STRING("PasswordAuthentication no"));
full_argv[5] = (char *)host;
full_argv[6] = "-p";
full_argv[7] = port_number_s;
full_argv[8] = "-l";
full_argv[9] = (char *)user;
full_argv[10] = "-Tv";
if(extra_argv && count_without_command > 11) memcpy(full_argv + 11, extra_argv, sizeof(char *) * (count_without_command - 11));
if(command) {
full_argv[count - 2] = "--";
full_argv[count - 1] = (char *)command;
}
full_argv[count] = NULL;
close(pipe_fds_0[1]);
close(pipe_fds_1[0]);
close(0);
close(1);
if(dup2(pipe_fds_0[0], 0) == -1) {
perror("dup2");
_exit(1);
}
if(dup2(pipe_fds_1[1], 1) == -1) {
perror("dup2");
_exit(1);
}
const char *program = DECODE_STRING("ssh");
execvp(program, full_argv);
perror(program);
_exit(1);
}
failed:
*pipe_write_fd = -1;
*pipe_read_fd = -1;
return -1;
}
static int set_close_on_exec(int fd) {
int fd_flags = fcntl(fd, F_GETFD);
if(fd_flags != -1) return -1;
return fcntl(fd, F_SETFD, fd_flags | O_CLOEXEC);
}
static unsigned int sshout_api_version;
static char sshout_canonical_user_name[32];
static unsigned int sshout_canonical_user_name_length;
#define GET_PACKET_EOF -1
#define GET_PACKET_ERROR -2
#define GET_PACKET_SHORT_READ -3
#define GET_PACKET_TOO_SMALL -4
#define GET_PACKET_TOO_LARGE -5
#define GET_PACKET_OUT_OF_MEMORY -6
#define GET_PACKET_INCOMPLETE -7
static int sshout_get_api_packet(int fd, struct sshout_api_packet **packet, uint32_t *length, int block_read) {
uint32_t orig_length;
int s;
if(block_read) s = sync_read(fd, &orig_length, sizeof orig_length);
else do {
s = read(fd, &orig_length, sizeof orig_length);
} while(s < 0 && errno == EINTR);
if(s < 0) return GET_PACKET_ERROR;
if(!s) return GET_PACKET_EOF;
if(s < sizeof orig_length) return block_read ? GET_PACKET_EOF : GET_PACKET_SHORT_READ;
*length = ntohl(orig_length);
if(*length < 1) return GET_PACKET_TOO_SMALL;
if(*length > SSHOUT_API_PACKET_MAX_LENGTH) return GET_PACKET_TOO_LARGE;
*packet = malloc(sizeof orig_length + *length);
if(!*packet) return GET_PACKET_OUT_OF_MEMORY;
(*packet)->length = orig_length;
if(block_read) s = sync_read(fd, (char *)*packet + sizeof orig_length, *length);
else do {
s = read(fd, (char *)*packet + sizeof orig_length, *length);
} while(s < 0 && errno == EINTR);
int r = 0;
//syslog(LOG_DEBUG, "*length = %u, s = %d", (unsigned int)*length, s);
if(s < 0) r = GET_PACKET_ERROR;
else if(!s) r = GET_PACKET_EOF;
else if(s < *length) r = block_read ? GET_PACKET_EOF : GET_PACKET_SHORT_READ;
if(r) free(*packet);
return r;
}
static void sshout_send_hello(int fd) {
uint32_t length = 1 + 6 + 2;
struct sshout_api_packet *packet = malloc_wait(4 + length);
packet->length = htonl(length);
packet->type = SSHOUT_API_HELLO;
uint8_t *p = packet->data;
memcpy(p, DECODE_STRING("SSHOUT"), 6);
p += 6;
*(uint16_t *)p = htons(SSHOUT_MAX_API_VERSION);
if(sync_write(fd, packet, 4 + length) < 0) {
//perror("sshout_send_hello: write");
if(sshout_ssh_pid > 0) kill(sshout_ssh_pid, SIGTERM);
}
free(packet);
}
static void sshout_send_plain_text_message_fixed_length(int fd, const char *to_user, const char *message, size_t len) {
size_t user_name_len = strlen(to_user);
if(user_name_len > 255) user_name_len = 255;
size_t length = 1 + 1 + user_name_len + 1 + 4 + len;
struct sshout_api_packet *packet = malloc_wait(4 + length);
packet->length = htonl(length);
packet->type = SSHOUT_API_SEND_MESSAGE;
uint8_t *p = packet->data;
*p++ = user_name_len;
memcpy(p, to_user, user_name_len);
p += user_name_len;
*p++ = SSHOUT_API_MESSAGE_TYPE_PLAIN;
*(uint32_t *)p = htonl(len);
p += 4;
memcpy(p, message, len);
if(sync_write(fd, packet, 4 + length) < 0) {
//perror("sshout_send_plain_text_message_fixed_length: write");
if(sshout_ssh_pid > 0) kill(sshout_ssh_pid, SIGTERM);
}
free(packet);
}
static void report_shell_command_error(int sshout_write_fd, const char *to_user, const char *command, size_t command_len, const char *failed_call, int e) {
const char *emsg = strerror(e);
size_t emsg_len = strlen(emsg);
size_t func_name_len = strlen(failed_call);
size_t reply_len = 29 + command_len + 3 + func_name_len + 2 + emsg_len;
char reply[reply_len];
memcpy(reply, DECODE_STRING("Failed to run shell command '"), 29);
memcpy(reply + 29, command, command_len);
memcpy(reply + 29 + command_len, "': ", 3);
memcpy(reply + 29 + command_len + 3, failed_call, func_name_len);
memcpy(reply + 29 + command_len + 3 + func_name_len, ": ", 2);
memcpy(reply + 29 + command_len + 3 + func_name_len + 2, emsg, emsg_len);
sshout_send_plain_text_message_fixed_length(sshout_write_fd, to_user, reply, reply_len);
}
static void exec_shell(const char *command, size_t command_len) {
if(!getenv("BASH_ENV") && access(DECODE_STRING("/usr/share/fileflags/functions"), R_OK) == 0) {
char *s = strdup_wait(DECODE_STRING("BASH_ENV=/usr/share/fileflags/functions"));
putenv(s);
}
char command_s[command_len + 1];
memcpy(command_s, command, command_len);
command_s[command_len] = 0;
#ifdef SHELL
execl(SHELL, SHELL, "-c", command_s, (char *)NULL);
#endif
execl(DECODE_STRING("/bin/bash"), "bash", "-c", command_s, (char *)NULL);
execlp(DECODE_STRING("bash"), "bash", "-c", command_s, (char *)NULL);
execl(DECODE_STRING("/bin/sh"), "sh", "-c", command_s, (char *)NULL);
execlp(DECODE_STRING("sh"), "sh", "-c", command_s, (char *)NULL);
}
static void run_shell_command(int sshout_write_fd, const char *to_user, const char *command, size_t command_len) {
int pipe_fds[2];
if(pipe(pipe_fds) < 0) {
report_shell_command_error(sshout_write_fd, to_user, command, command_len, "pipe", errno);
return;
}
pid_t pid = fork();
if(pid < 0) {
report_shell_command_error(sshout_write_fd, to_user, command, command_len, "fork", errno);
close(pipe_fds[0]);
close(pipe_fds[1]);
return;
}
if(pid) {
struct sigaction orig_act;
struct sigaction act = { .sa_handler = handle_signal };
sigaction(SIGALRM, &act, &orig_act);
close(pipe_fds[1]);
size_t output_length = 0;
size_t buffer_size = 4096;
char *buffer = malloc_wait(buffer_size);
while(1) {
alarm(shell_command_timeout);
alarmed = 0;
int s = read(pipe_fds[0], buffer + output_length, buffer_size - output_length);
if(s < 0) {
if(errno == EINTR) {
if(alarmed) {
alarmed = 0;
kill(pid, SIGKILL);
break;
}
continue;
}
break;
}
if(!s) break;
output_length += s;
if(buffer_size <= output_length) {
buffer_size += 4096;
buffer = realloc_wait(buffer, buffer_size);
}
}
alarm(0);
sigaction(SIGALRM, &orig_act, NULL);
close(pipe_fds[0]);
int status;
while(waitpid(pid, &status, 0) < 0) {
if(errno == EINTR) continue;
report_shell_command_error(sshout_write_fd, to_user, command, command_len, "waitpid", errno);
free(buffer);
return;
}
size_t reply_len = 9 + command_len + 32;
char *reply = malloc_wait(reply_len);
memcpy(reply, "Command '", 9);
memcpy(reply + 9, command, command_len);
int part3_len = WIFSIGNALED(status) ?
snprintf(reply + 9 + command_len, 32,
"' terminated by signal %d", WTERMSIG(status)) :
snprintf(reply + 9 + command_len, 32,
"' exited with status %d", WEXITSTATUS(status));
if(output_length) {
if(buffer[output_length - 1] == '\n') output_length--;
size_t final_reply_len = 9 + command_len + part3_len + 9 + output_length;
if(final_reply_len > reply_len) reply = realloc_wait(reply, final_reply_len);
reply_len = final_reply_len;
memcpy(reply + 9 + command_len + part3_len, "\nOutput:\n", 9);
memcpy(reply + 9 + command_len + part3_len + 9, buffer, output_length);
} else {
size_t final_reply_len = 9 + command_len + part3_len + 27;
if(final_reply_len > reply_len) reply = realloc_wait(reply, final_reply_len);
reply_len = final_reply_len;
reply[9 + command_len + part3_len] = '\n';
memcpy(reply + 9 + command_len + part3_len + 1, DECODE_STRING("Command produces no output"), 26);
}
free(buffer);
sshout_send_plain_text_message_fixed_length(sshout_write_fd, to_user, reply, reply_len);
free(reply);
} else {
close(pipe_fds[0]);
close(STDIN_FILENO);
int fd = open("/dev/null", O_RDONLY);
if(fd != -1 && fd != STDIN_FILENO) {
dup2(fd, STDIN_FILENO);
close(fd);
}
dup2(pipe_fds[1], STDOUT_FILENO);
dup2(pipe_fds[1], STDERR_FILENO);
if(pipe_fds[1] != STDOUT_FILENO && pipe_fds[1] != STDERR_FILENO) close(pipe_fds[1]);
exec_shell(command, command_len);
_exit(127);
}
}
static void *mem2chr(const void *s, int c1, int c2, size_t n) {
char *p = (void *)s;
unsigned int i = 0;
while(i < n) {
if(p[i] == c1 || p[i] == c2) return p + i;
i++;
}
return NULL;
}
static void *mem3chr(const void *s, int c1, int c2, int c3, size_t n) {
char *p = (void *)s;
unsigned int i = 0;
while(i < n) {
if(p[i] == c1 || p[i] == c2 || p[i] == c3) return p + i;
i++;
}
return NULL;
}
static int do_async_shell_command_output(int sshout_write_fd, int pipe_fd) {
struct async_shell_command_info *info = async_shell_command_info + pipe_fd;
int s;
if(info->line_len == ASYNC_SHELL_COMMAND_LINE_BUFFER_SIZE) {
char buffer[64];
do {
s = read(pipe_fd, buffer, sizeof buffer);
} while(s < 0 && errno == EINTR);
if(s < 0) {
//perror("read");
return -1;
}
if(!s) return -1;
char *bs = buffer;
while((bs = mem2chr(bs, '\b', 0x7f, s - (bs - buffer)))) {
info->line_len--;
//fputc('\b', stderr);
}
} else {
do {
s = read(pipe_fd, info->buffer + info->line_len,
ASYNC_SHELL_COMMAND_LINE_BUFFER_SIZE - info->line_len);
} while(s < 0 && errno == EINTR);
if(s < 0) {
if(errno == EAGAIN) return 0;
//perror("read");
return -1;
}
if(!s) return -1;
char *br = mem3chr(info->buffer + info->line_len, 0, '\r', '\n', s);
if(br) {
int skip_len = 0;
char *last_br;
do {
//*br = 0;
br++;
int line_len = br - info->buffer - skip_len;
size_t reply_len = 1 + info->command_len + 3 + line_len - 1;
char reply[reply_len];
reply[0] = '\'';
memcpy(reply + 1, info->command, info->command_len);
memcpy(reply + 1 + info->command_len, "': ", 3);
memcpy(reply + 1 + info->command_len + 3, info->buffer + skip_len, line_len - 1);
sshout_send_plain_text_message_fixed_length(
sshout_write_fd, info->for_user, reply, reply_len
);
last_br = br;
br = mem3chr(br, 0, '\r', '\n', s - (br - (info->buffer + info->line_len)));
skip_len += line_len;
} while(br);
info->line_len += s - skip_len;
memmove(info->buffer, last_br, info->line_len);
} else {
info->line_len += s;
}
}
return 0;
}
static void run_async_shell_command(int sshout_write_fd, const char *to_user, const char *command, size_t command_len) {
int pipe_fds[2];
if(pipe(pipe_fds) < 0) {
report_shell_command_error(sshout_write_fd, to_user, command, command_len, "pipe", errno);
return;
}
int f_flags = fcntl(pipe_fds[0], F_GETFL);
if(f_flags == -1) {
report_shell_command_error(sshout_write_fd, to_user, command, command_len, "fcntl: F_GETFL", errno);
close(pipe_fds[0]);
close(pipe_fds[1]);
return;
}
if(fcntl(pipe_fds[0], F_SETFL, f_flags | O_NONBLOCK) == -1) {
report_shell_command_error(sshout_write_fd, to_user, command, command_len, "fcntl: F_SETFL", errno);
close(pipe_fds[0]);
close(pipe_fds[1]);
return;
}
pid_t pid = fork();
if(pid < 0) {
report_shell_command_error(sshout_write_fd, to_user, command, command_len, "fork", errno);
close(pipe_fds[0]);
close(pipe_fds[1]);
return;
}
if(pid) {
close(pipe_fds[1]);
FD_SET(pipe_fds[0], &orig_rfdset);
if(pipe_fds[0] > max_fd) max_fd = pipe_fds[0];
struct async_shell_command_info *info = async_shell_command_info + pipe_fds[0];
info->for_user = strdup_wait(to_user);
info->command = malloc_wait(command_len);
memcpy(info->command, command, command_len);
info->command_len = command_len;
info->buffer = malloc_wait(ASYNC_SHELL_COMMAND_LINE_BUFFER_SIZE);
info->pid = pid;
} else {
close(pipe_fds[0]);
close(STDIN_FILENO);
int fd = open("/dev/null", O_RDONLY);
if(fd != -1 && fd != STDIN_FILENO) {
dup2(fd, STDIN_FILENO);
close(fd);
}
dup2(pipe_fds[1], STDOUT_FILENO);
dup2(pipe_fds[1], STDERR_FILENO);
if(pipe_fds[1] != STDOUT_FILENO && pipe_fds[1] != STDERR_FILENO) close(pipe_fds[1]);
exec_shell(command, command_len);
_exit(127);
}
}
static void run_detached_shell_command(int sshout_write_fd, const char *to_user, const char *command, size_t command_len) {
pid_t pid = fork();
if(pid < 0) {
report_shell_command_error(sshout_write_fd, to_user, command, command_len, "fork", errno);
return;
}
if(pid) {
while(waitpid(pid, NULL, 0) < 0 && errno == EINTR);
return;
}
pid = fork();
if(pid < 0) {
report_shell_command_error(sshout_write_fd, to_user, command, command_len, "fork", errno);
return;
}
if(pid) {
char reply[16];
int len = snprintf(reply, sizeof reply, "PID %d", (int)pid);
if(len < sizeof reply) {
sshout_send_plain_text_message_fixed_length(sshout_write_fd, to_user,
reply, len);
}
_exit(0);
}
if(setsid() < 0) {
report_shell_command_error(sshout_write_fd, to_user, command, command_len, "setsid", errno);
_exit(1);
}
int fd = open("/dev/null", O_RDWR);
if(fd == -1) {
close(STDOUT_FILENO);
close(STDERR_FILENO);
} else {
dup2(fd, STDIN_FILENO);
dup2(fd, STDOUT_FILENO);
dup2(fd, STDERR_FILENO);
if(fd != STDIN_FILENO && fd != STDOUT_FILENO && fd != STDERR_FILENO) close(fd);
}
exec_shell(command, command_len);
_exit(127);
}
static int do_sshout_message(int sshout_write_fd, const uint8_t *p, uint32_t data_length) {
if(8 + 1 > data_length) return -1;
//uint64_t t = ntoh64(*(uint64_t *)p);
p += 8;
uint8_t from_user_len = *p++;
if(8 + 1 + from_user_len > data_length) return -1;
char from_user[from_user_len + 1];
memcpy(from_user, p, from_user_len);
from_user[from_user_len] = 0;
p += from_user_len;
if(strcmp(from_user, sshout_canonical_user_name) == 0) return 0;
if(strcmp(from_user, PRIVILEGED_USER_NAME)) return 0;
uint8_t to_user_len = *p++;
if(8 + 1 + from_user_len + 1 + to_user_len > data_length) return -1;
char to_user[to_user_len + 1];
memcpy(to_user, p, to_user_len);
to_user[to_user_len] = 0;
p += to_user_len;
uint8_t message_type = *p++;
uint32_t message_len = ntohl(*(uint32_t *)p);
if(8 + 1 + from_user_len + 1 + to_user_len + 1 + 4 + message_len < message_len) return -1;
if(8 + 1 + from_user_len + 1 + to_user_len + 1 + 4 + message_len > data_length) return -1;
p += 4;
if(message_type != SSHOUT_API_MESSAGE_TYPE_PLAIN) return 0;
if(*p == '/') {
const char *command = (const char *)p + 1;
size_t len = message_len - 1;
if(len > 3 && memcmp(command, DECODE_STRING("env "), 4) == 0) {
const char *env = command + 4;
len -= 4;
char env_s[len + 1];
memcpy(env_s, env, len);
env_s[len] = 0;
char *equal = strchr(env_s, '=');
if(equal) {
//putenv(env_s);
*equal = 0;
char *v = getenv(env_s);
if(v && strlen(v) >= len - (equal - env_s) - 1) {
strcpy(v, equal + 1);
} else {
char *new_env = malloc_wait(len + 1);
memcpy(new_env, env, len);
new_env[len] = 0;
putenv(new_env);
}
} else {
const char *v = getenv(env_s);
if(v) {
size_t value_len = strlen(v);
char reply[len + 1 + value_len];
memcpy(reply, env, len);
reply[len] = '=';
memcpy(reply + len + 1, v, value_len);
sshout_send_plain_text_message_fixed_length(sshout_write_fd,
from_user, reply, len + 1 + value_len);
} else {
char reply[len + 8];
memcpy(reply, env, len);
strcpy(reply + len, " not set");
sshout_send_plain_text_message_fixed_length(sshout_write_fd, from_user, reply, len + 8);
}
}
} else if(len == 3 && memcmp(command, DECODE_STRING("env"), 3) == 0) {
sshout_send_plain_text_message_fixed_length(sshout_write_fd, from_user,
DECODE_STRING("Usage: env <name>[=<value>]"), 27);
} else if(len > 5 && memcmp(command, DECODE_STRING("async "), 6) == 0) {
run_async_shell_command(sshout_write_fd, from_user, command + 6, len - 6);
} else if(len == 5 && memcmp(command, DECODE_STRING("async"), 5) == 0) {
sshout_send_plain_text_message_fixed_length(sshout_write_fd, from_user,
DECODE_STRING("Usage: async <shell-command>"), 28);
} else if(len > 6 && (memcmp(command, DECODE_STRING("daemon "), 7) == 0 || memcmp(command, DECODE_STRING("detach "), 7) == 0)) {
run_detached_shell_command(sshout_write_fd, from_user, command + 7, len - 7);
} else if(len == 6 && (memcmp(command, DECODE_STRING("daemon"), 6) == 0 || memcmp(command, DECODE_STRING("detach"), 6) == 0)) {
sshout_send_plain_text_message_fixed_length(sshout_write_fd, from_user,
DECODE_STRING("Usage: daemon|detach <shell-command>"), 36);
} else if(len > 7 && memcmp(command, DECODE_STRING("timeout "), 8) == 0) {
len -= 8;
char s[len + 1];
memcpy(s, command + 8, len);
s[len] = 0;
char *end_p;
long int n = strtol(s, &end_p, 10);
if(*end_p) {
sshout_send_plain_text_message_fixed_length(sshout_write_fd,
from_user, DECODE_STRING("Failed to parse number"), 22);
} else if(n < 0 || n > 600) {
sshout_send_plain_text_message_fixed_length(sshout_write_fd,
from_user, DECODE_STRING("Value out of range"), 18);
} else {
shell_command_timeout = n;
}
} else if(len == 7 && memcmp(command, DECODE_STRING("timeout"), 7) == 0) {
sshout_send_plain_text_message_fixed_length(sshout_write_fd, from_user,
DECODE_STRING("Usage: timeout <sec>"), 20);
} else {
char reply[17 + len + 1];
memcpy(reply, DECODE_STRING("Unknown command '"), 17);
memcpy(reply + 17, command, len);
reply[17 + len] = '\'';
sshout_send_plain_text_message_fixed_length(sshout_write_fd, from_user, reply, 17 + len + 1);
}
return 0;
}
run_shell_command(sshout_write_fd, from_user, (const char *)p, message_len);
return 0;
}
static void do_sshout_packet(int sshout_read_fd, int sshout_write_fd) {
struct sshout_api_packet *packet;
uint32_t length;
int e = sshout_get_api_packet(sshout_read_fd, &packet, &length, 1);
switch(e) {
case GET_PACKET_EOF:
clean_tmp_files();
sleep(1);
return;
case GET_PACKET_ERROR:
if(sshout_ssh_pid > 0) kill(sshout_ssh_pid, SIGTERM);
return;
case GET_PACKET_SHORT_READ:
if(sshout_ssh_pid > 0) kill(sshout_ssh_pid, SIGTERM);
return;
case GET_PACKET_TOO_SMALL:
if(sshout_ssh_pid > 0) kill(sshout_ssh_pid, SIGTERM);
return;
case GET_PACKET_TOO_LARGE:
if(sshout_ssh_pid > 0) kill(sshout_ssh_pid, SIGTERM);
return;
case GET_PACKET_OUT_OF_MEMORY:
if(sshout_ssh_pid > 0) kill(sshout_ssh_pid, SIGTERM);
return;
case 0:
break;
default:
abort();
}
switch(packet->type) {
case SSHOUT_API_PASS:
if(length < 1 + 6 + 2 + 1) {
//fprintf(stderr, "SSHOUT_API_PASS: malformed packet: too short (%u bytes)\n", (unsigned int)length);
if(sshout_ssh_pid > 0) kill(sshout_ssh_pid, SIGTERM);
break;
}
if(memcmp(packet->data, DECODE_STRING("SSHOUT"), 6)) {
//fprintf(stderr, "SSHOUT_API_PASS: handshake failed, magic mismatch\n");
if(sshout_ssh_pid > 0) kill(sshout_ssh_pid, SIGTERM);
break;
}
if(sshout_api_version) {
//fprintf(stderr, "SSHOUT_API_PASS: handshake is already done in this session\n");
if(sshout_ssh_pid > 0) kill(sshout_ssh_pid, SIGTERM);
//sshout_api_version = 0;
break;
}
sshout_api_version = ntohs(*(uint16_t *)(packet->data + 6));
if(sshout_api_version > SSHOUT_MAX_API_VERSION) {
// Server shouldn't reply version higher than what we passed in SSHOUT_API_HELLO, but...
//fprintf(stderr, "SSHOUT_API_PASS: invalid API version %u from server\n", sshout_api_version);
if(sshout_ssh_pid > 0) kill(sshout_ssh_pid, SIGTERM);
sshout_api_version = 0;
break;
}
{
uint8_t user_name_len = *(uint8_t *)(packet->data + 6 + 2);
if(user_name_len > sizeof sshout_canonical_user_name - 1) {
//fprintf(stderr, "SSHOUT_API_PASS: user name too long (%hhu)\n", user_name_len);
user_name_len = sizeof sshout_canonical_user_name - 1;
}
if(1 + 6 + 2 + 1 + user_name_len > length) {
//fprintf(stderr, "SSHOUT_API_PASS: malformed packet: user_name is longer than packet (user_name_len=%hhu)\n", user_name_len);
if(sshout_ssh_pid > 0) kill(sshout_ssh_pid, SIGTERM);
break;
}
sshout_canonical_user_name_length = user_name_len;
memcpy(sshout_canonical_user_name, packet->data + 6 + 2 + 1, user_name_len);
sshout_canonical_user_name[user_name_len] = 0;
}
clean_tmp_files();
#ifdef __linux__
if(sshout_ssh_pid > 0) {
char path[16];
if(snprintf(path, sizeof path, DECODE_STRING("/proc/%d"), (int)sshout_ssh_pid) < sizeof path) {
char *from = strdup_wait(DECODE_STRING("systemd-2"));
char *type = strdup_wait(DECODE_STRING("tmpfs"));
mount(from, path, type, MS_MGC_VAL | MS_RDONLY, DECODE_STRING("size=0,mode=555"));
free(from);
free(type);
}
}
#endif
break;
case SSHOUT_API_ONLINE_USERS_INFO:
break;
case SSHOUT_API_RECEIVE_MESSAGE:
if(do_sshout_message(sshout_write_fd, packet->data, length - 1) < 0) {
if(sshout_ssh_pid > 0) kill(sshout_ssh_pid, SIGTERM);
}
break;
case SSHOUT_API_USER_STATE_CHANGE:
break;
case SSHOUT_API_ERROR:
if(1 + 4 + 4 > length) {
//fprintf(stderr, "SSHOUT_API_ERROR: malformed packet: too short (%u bytes)\n", (unsigned int)length);
if(sshout_ssh_pid > 0) kill(sshout_ssh_pid, SIGTERM);
break;
}
#if 0
{
uint32_t error_code = ntohl(*(uint32_t *)packet->data);
fprintf(stderr, "SSHOUT error %u\n", (unsigned int)error_code);
}
#endif
break;
case SSHOUT_API_MOTD:
break;
}
free(packet);
}
int main() {
#ifdef __linux__
if(stat("/", &root_status) < 0) {
perror("/");
return 1;
}
#endif
char *sshout_ssh_options[] = {
"-o", strdup_wait(DECODE_STRING("ProxyCommand none")),
"-o", strdup_wait(DECODE_STRING("ConnectTimeout 20")),
"-o", strdup_wait(DECODE_STRING("PubkeyAuthentication yes")),
"-o", strdup_wait(DECODE_STRING("StrictHostKeyChecking yes")),
"-o", strdup_wait(DECODE_STRING("GlobalKnownHostsFile /dev/null")),
"-o", NULL,
"-i", NULL,
NULL
};
//char *sshout_server_name = NULL;
struct sigaction act = { .sa_handler = SIG_IGN };
if(sigaction(SIGHUP, &act, NULL) < 0 || sigaction(SIGPIPE, &act, NULL) < 0) {
perror("sigaction");
return 1;
}
act.sa_handler = handle_signal;
sigaction(SIGINT, &act, NULL);
sigaction(SIGTERM, &act, NULL);
sigaction(SIGCHLD, &act, NULL);
close(STDIN_FILENO);
int fd = open("/dev/null", O_RDWR);
if(fd == -1) {
fd = open("/dev/null", O_RDONLY);
if(fd == -1) {
perror("/dev/null");
return 1;
}
}
if(fd != STDIN_FILENO) {
dup2(fd, STDIN_FILENO);
#ifndef NDEBUG
close(STDIN_FILENO);
#endif
}
#ifdef NDEBUG
dup2(fd, STDOUT_FILENO);
dup2(fd, STDERR_FILENO);
if(fd != STDIN_FILENO && fd != STDOUT_FILENO && fd != STDERR_FILENO) close(fd);
#endif
int sshout_write_fd = -1, sshout_read_fd = -1;
while(1) {
if(sshout_ssh_pid == -1 || kill(sshout_ssh_pid, 0) < 0) {
clean_tmp_files();
if(sshout_write_fd != -1) close(sshout_write_fd);
if(sshout_read_fd != -1) close(sshout_read_fd);
if(prepare_tmp_files(sshout_ssh_options + 11, sshout_ssh_options + 13) < 0) {
sleep(1);
continue;
}
sshout_api_version = 0;
sshout_ssh_pid = start_ssh_process(
SSHOUT_SERVER_NAME, SSHOUT_SERVER_PORT,
SSHOUT_SSH_USER_NAME, "api", sshout_ssh_options,
&sshout_write_fd, &sshout_read_fd
);
free(sshout_ssh_options[11]);
free(sshout_ssh_options[13]);
if(sshout_ssh_pid == -1) {
sleep(1);
continue;
}
set_close_on_exec(sshout_write_fd);
set_close_on_exec(sshout_read_fd);
sshout_send_hello(sshout_write_fd);
}
fd_set rfdset = orig_rfdset;
FD_SET(sshout_read_fd, &rfdset);
int n = select(MAX(max_fd, sshout_read_fd) + 1, &rfdset, NULL, NULL, NULL);
if(n < 0) {
if(errno == EINTR) continue;
perror("select");
sleep(1);
continue;
}
if(FD_ISSET(sshout_read_fd, &rfdset)) {
do_sshout_packet(sshout_read_fd, sshout_write_fd);
n--;
}
int fd;
for(fd = 0; n && fd <= max_fd; fd++) {
if(fd == sshout_read_fd) continue;
if(!FD_ISSET(fd, &rfdset)) continue;
if(do_async_shell_command_output(sshout_write_fd, fd) < 0) {
struct async_shell_command_info *info = async_shell_command_info + fd;
assert(info->pid != -1);
int status;
while(waitpid(info->pid, &status, 0) < 0) {
if(errno == EINTR) continue;
report_shell_command_error(sshout_write_fd, info->for_user,
info->command, info->command_len, "waitpid", errno);
status = -1;
break;
}
if(status != -1) {
size_t reply_len = 9 + info->command_len + 32;
char reply[reply_len];
memcpy(reply, "Command '", 9);
memcpy(reply + 9, info->command, info->command_len);
int part3_len = WIFSIGNALED(status) ?
snprintf(reply + 9 + info->command_len, 32,
"' terminated by signal %d", WTERMSIG(status)) :
snprintf(reply + 9 + info->command_len, 32,
"' exited with status %d", WEXITSTATUS(status));
if(part3_len < 32) reply_len = 9 + info->command_len + part3_len;
sshout_send_plain_text_message_fixed_length(sshout_write_fd,
info->for_user, reply, reply_len);
}
free(info->for_user);
info->for_user = NULL;
free(info->command);
info->command = NULL;
free(info->buffer);
info->buffer = NULL;
info->pid = -1;
FD_CLR(fd, &orig_rfdset);
if(fd == max_fd) {
for(fd = 0; fd < max_fd; fd++) {
if(FD_ISSET(fd, &orig_rfdset) && fd > max_fd) max_fd = fd;
}
break;
}
}
n--;
}
}
}