| /* |
| * 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--; |
| } |
| } |
| } |