Skip to content

Instantly share code, notes, and snippets.

@Low-power
Last active May 2, 2020 02:26
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Low-power/94d2b33fc3350ef5945489b63a74651f to your computer and use it in GitHub Desktop.
Save Low-power/94d2b33fc3350ef5945489b63a74651f to your computer and use it in GitHub Desktop.
Chatroom proxy for SSHOUT and ssh-chat revision 24
/* Chatroom relay between SSHOUT and ssh-chat
* Copyright 2015-2020 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.
*/
/* Required files syncrw.c, syncrw.h and sshout/api.h can be retrieved from
* https://sourceforge.net/p/sshout/daemon-code/ci/master/tree/
*/
#define _GNU_SOURCE
#include <sys/select.h>
#include <sys/wait.h>
#include <unistd.h>
#include "syncrw.h"
#include <signal.h>
#include <string.h>
#include <stdint.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <time.h>
#include <arpa/inet.h>
#include "sshout/api.h"
#define ntoh64(a) (ntohs(1) == 1 ? (a) : ((uint64_t)htonl((a) & 0xFFFFFFFF) << 32) | ntohl((a) >> 32))
#ifndef MAX
#define MAX(A,B) ((A)>(B)?(A):(B))
#endif
#define SSH_CHAT_DEFAULT_USER_NAME "relay"
#define SSHOUT_MAX_API_VERSION 1
static char *sshout_server_name = NULL;
static uint16_t sshout_server_port = 22;
static char *ssh_chat_server_name = NULL;
static uint16_t ssh_chat_server_port = 22;
static int is_sshout_server_address_public = 0;
static int is_ssh_chat_server_address_public = 0;
static pid_t sshout_ssh_pid = -1;
static pid_t ssh_chat_ssh_pid = -1;
static void signal_handler(int sig) {
if(sig != SIGCHLD) return;
while(1) {
pid_t pid = waitpid(-1, NULL, WNOHANG);
if(pid == -1) {
if(errno == EINTR) continue;
if(errno == ECHILD) return;
perror("waitpid");
return;
}
if(!pid) return;
if(pid == sshout_ssh_pid) sshout_ssh_pid = -1;
else if(pid == ssh_chat_ssh_pid) ssh_chat_ssh_pid = -1;
}
}
static void print_usage(const char *name) {
fprintf(stderr, "Usage: %s [<options-for-sshout>] <sshout-server-url> [<options-for-ssh-chat>] <ssh-chat-server-url>\n\n"
"Options:\n"
" -i <file> Use this private key file for public key authentication\n"
" -o <ssh-option> Set ssh option, see ssh_config(5) for details\n"
" -v Be verbose\n"
" -P Allow users from another server getting this server address\n\n"
"URL format:\n"
" ssh://[<user-name>@]<server-name>[:<port>]/\n"
" The default user name is 'sshout' for SSHOUT, '%s' for ssh-chat;\n"
" default port number is 22.\n\n", name, SSH_CHAT_DEFAULT_USER_NAME);
}
static void add_arg(char ***argv, char *a) {
unsigned int count = 1;
char **p = *argv;
if(p) while(*p++) count++;
p = realloc(*argv, sizeof(char *) * (count + 1));
if(!p) {
perror("realloc");
exit(1);
}
p[count - 1] = a;
p[count] = NULL;
*argv = p;
}
static int prase_ssh_url(char **host_name, uint16_t *port, char **user_name, const char *url) {
if(strncmp(url, "ssh:", 4)) {
fputs("URL scheme is not 'ssh'\n", stderr);
return -1;
}
const char *p = url + 4;
if(*p == '/' && *++p == '/' && *++p == '/') {
fputs("Too many '/'\n", stderr);
return -1;
}
const char *end = strchr(p, '/');
if(end && end[1]) fputs("Ignoring extra path in URL\n", stderr);
const char *at = strchr(p, '@');
if(at && (!end || at < end)) {
size_t len = at - p;
*user_name = malloc(len + 1);
if(!*user_name) {
perror("malloc");
return -1;
}
memcpy(*user_name, p, len);
(*user_name)[len] = 0;
p = at + 1;
}
size_t host_name_len;
char *colon = strchr(p, ':');
if(colon && (!end || colon < end)) {
//char *port_p = colon + 1;
//size_t len = end ? end - port_p : strlen(port_p);
char *port_end_p;
*port = strtoul(colon + 1, &port_end_p, 10);
if((end && port_end_p < end) || (!end && *port_end_p)) {
fputs("Invalid port number in URL", stderr);
return -1;
}
host_name_len = colon - p;
} else {
host_name_len = end ? end - p : strlen(p);
}
*host_name = malloc(host_name_len + 1);
if(!*host_name) {
perror("malloc");
return -1;
}
memcpy(*host_name, p, host_name_len);
(*host_name)[host_name_len] = 0;
return 0;
}
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] = "ssh";
full_argv[1] = "-o";
full_argv[2] = "ServerAliveInterval=120";
full_argv[3] = "-o";
full_argv[4] = "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] = "-T";
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);
}
execvp("ssh", full_argv);
perror("ssh");
_exit(1);
}
failed:
*pipe_write_fd = -1;
*pipe_read_fd = -1;
return -1;
}
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(4 + length);
if(!packet) {
fprintf(stderr, "sshout_send_hello: out of memory\n");
return;
}
packet->length = htonl(length);
packet->type = SSHOUT_API_HELLO;
uint8_t *p = packet->data;
memcpy(p, "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");
}
free(packet);
}
static void sshout_send_request_online_users(int fd) {
uint32_t length = 1;
struct sshout_api_packet *packet = malloc(4 + length);
if(!packet) {
fprintf(stderr, "sshout_send_request_online_users: out of memory\n");
return;
}
packet->length = htonl(length);
packet->type = SSHOUT_API_GET_ONLINE_USER;
if(sync_write(fd, packet, 4 + length) < 0) {
perror("sshout_send_request_online_users: write");
}
free(packet);
}
static void write_backspaces(int fd, size_t count) {
char *chunk = NULL;;
size_t chunk_len;
while(count > 0) {
chunk_len = count > 1024 ? 1024 : count;
if(!chunk) {
chunk = malloc(chunk_len);
if(!chunk) {
char bs = '\b';
int s;
do {
s = write(fd, &bs, 1);
} while((s < 0 && errno == EINTR) || --count > 0);
return;
}
memset(chunk, '\b', chunk_len);
}
sync_write(fd, chunk, chunk_len);
count -= chunk_len;
}
free(chunk);
}
static int ssh_chat_send_sshout_online_user_list(int fd, uint8_t *p, uint32_t data_length) {
if(2 + 2 > data_length) return -1;
uint16_t my_id = ntohs(*(uint16_t *)p);
p += 2;
uint16_t count = ntohs(*(uint16_t *)p);
if(!count) return -1; // WTF?
p += 2;
char reply_prefix[28];
snprintf(reply_prefix, sizeof reply_prefix, "/reply %u online user%s: ",
(unsigned int)(count - 1), count == 2 ? "" : "s");
size_t written_len = strlen(reply_prefix);
sync_write(fd, reply_prefix, written_len);
uint16_t i = 0;
size_t v_size = 0;
int name_written = 0;
while(i++ < count) {
if(2 + 2 + v_size + 2 + 1 > data_length) {
write_backspaces(fd, written_len);
return -1;
}
uint16_t id = ntohs(*(uint16_t *)p);
p += 2;
uint8_t user_name_len = *p++;
if(2 + 2 + v_size + 2 + 1 + user_name_len + 1 > data_length) {
write_backspaces(fd, written_len);
return -1;
}
if(id != my_id) {
if(name_written) sync_write(fd, ", ", 2);
else name_written = 1;
sync_write(fd, p, user_name_len);
written_len += user_name_len;
}
p += user_name_len;
uint8_t host_name_len = *p++;
if(2 + 2 + v_size + 2 + 1 + user_name_len + 1 + host_name_len > data_length) {
write_backspaces(fd, written_len);
return -1;
}
p += host_name_len; // Skip host_name
v_size += 2 + 1 + user_name_len + 1 + host_name_len;
}
sync_write(fd, "\r\n", 2);
return 0;
}
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(4 + length);
if(!packet) {
fprintf(stderr, "sshout_send_plain_text_message_fixed_length: out of memory\n");
return;
}
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_hello: write");
}
free(packet);
}
static char *ssh_chat_user_name = NULL;
static char last_requesting_sshout_user[64];
static int do_sshout_message(int sshout_write_fd, int ssh_chat_write_fd, 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;
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(strcmp(to_user, sshout_canonical_user_name) == 0) {
if(from_user_len + 1 > sizeof last_requesting_sshout_user) {
sshout_send_plain_text_message_fixed_length(sshout_write_fd, from_user, "User name too long?", 19);
return 0;
}
if(message_type != SSHOUT_API_MESSAGE_TYPE_PLAIN) {
sshout_send_plain_text_message_fixed_length(sshout_write_fd, from_user, "Only plain text message is supported in PM", 42);
return 0;
}
const char *command = (const char *)p;
size_t len = message_len;
if(*command == '/') {
command++;
len--;
}
if(len == 4 && memcmp(command, "list", 4) == 0) {
if(*last_requesting_sshout_user) {
sshout_send_plain_text_message_fixed_length(sshout_write_fd, last_requesting_sshout_user, "Failed to process your request from ssh-chat server", 51);
}
memcpy(last_requesting_sshout_user, from_user, from_user_len + 1);
sync_write(ssh_chat_write_fd, "/list\r\n", 7);
} else if(len == 3 && memcmp(command, "msg", 3) == 0) {
sshout_send_plain_text_message_fixed_length(sshout_write_fd, from_user, "Missing user\nUsage: msg <user> <message> [<message>] [...]", 58);
} else if(len > 3 && memcmp(command, "msg ", 4) == 0) {
const char *to_user_p = command + 4;
const char *p = memchr(to_user_p, ' ', len - 4);
if(p) {
size_t to_user_name_len = p - to_user_p;
if(strncmp(to_user_p, ssh_chat_user_name, to_user_name_len) == 0 && !ssh_chat_user_name[to_user_name_len]) {
fprintf(stderr, "do_sshout_message: igroning private messaging request from '%s' to myself '%s'\n",
from_user, ssh_chat_user_name);
sshout_send_plain_text_message_fixed_length(sshout_write_fd, from_user,
"Cannot PM to myself", 19);
} else {
if(*last_requesting_sshout_user) {
sshout_send_plain_text_message_fixed_length(sshout_write_fd,
last_requesting_sshout_user,
"Failed to process your request from ssh-chat server", 51);
}
memcpy(last_requesting_sshout_user, from_user, from_user_len + 1);
p++;
len -= 4 + to_user_name_len + 1;
sync_write(ssh_chat_write_fd, "/msg ", 5);
sync_write(ssh_chat_write_fd, to_user_p, to_user_name_len);
sync_write(ssh_chat_write_fd, " ", 1);
sync_write(ssh_chat_write_fd, from_user, from_user_len);
sync_write(ssh_chat_write_fd, " to ", 4);
sync_write(ssh_chat_write_fd, to_user_p, to_user_name_len);
sync_write(ssh_chat_write_fd, ": ", 2);
sync_write(ssh_chat_write_fd, p, len);
sync_write(ssh_chat_write_fd, "\r\n", 2);
}
} else {
sshout_send_plain_text_message_fixed_length(sshout_write_fd, from_user,
"Missing message\nUsage: msg <user> <message> [<message>] [...]", 61);
}
} else if(len == 4 && memcmp(command, "peer", 4) == 0) {
if(is_ssh_chat_server_address_public) {
size_t server_name_len = strlen(ssh_chat_server_name);
// Need space for ending zero that would be written by sprintf(3).
char reply[37 + server_name_len + (ssh_chat_server_port == 22 ? 0 : 6) + 1 + 1];
size_t len;
if(ssh_chat_server_port == 22) {
memcpy(reply, "The peer is a ssh-chat server: ssh://", 37);
memcpy(reply + 37, ssh_chat_server_name, server_name_len);
reply[37 + server_name_len] = '/';
len = 37 + server_name_len + 1;
} else {
len = sprintf(reply, "The peer is a ssh-chat server: ssh://%s:%hu/",
ssh_chat_server_name, (unsigned short int)ssh_chat_server_port);
}
sshout_send_plain_text_message_fixed_length(sshout_write_fd, from_user, reply, len);
} else {
sshout_send_plain_text_message_fixed_length(sshout_write_fd, from_user,
"The peer is a ssh-chat server: [private address]", 48);
}
} else if(len == 4 && memcmp(command, "help", 4) == 0) {
sshout_send_plain_text_message_fixed_length(sshout_write_fd, from_user,
"Supported commands: list, msg, peer", 35);
} else {
char reply[17 + len + 1];
memcpy(reply, "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;
}
if(strcmp(to_user, "GLOBAL")) {
// This shouldn't happen
fprintf(stderr, "Received a private message to '%s'\n", to_user);
return 0;
}
const char *message;
switch(message_type) {
case SSHOUT_API_MESSAGE_TYPE_PLAIN:
message = (char *)p;
break;
case SSHOUT_API_MESSAGE_TYPE_RICH:
message_len = 6;
message = "[HTML]";
break;
case SSHOUT_API_MESSAGE_TYPE_IMAGE:
message_len = 7;
message = "[Image]";
break;
default:
fprintf(stderr, "unknown message type %hhu\n", message_type);
return 0;
}
sync_write(ssh_chat_write_fd, from_user, from_user_len);
sync_write(ssh_chat_write_fd, ": ", 2);
sync_write(ssh_chat_write_fd, message, message_len);
sync_write(ssh_chat_write_fd, "\r\n", 2);
return 0;
}
static struct user_join_time {
char *user_name;
size_t user_name_len;
int state;
time_t last_change;
struct user_join_time *next;
} *recent_joined_users = NULL;
// Return boolean
static int check_recent_join(const char *user_name, size_t user_name_len, int current_state) {
time_t current_time = time(NULL);
struct user_join_time *p = recent_joined_users, **pp = &recent_joined_users;
while(p) {
if(current_time - p->last_change > (p->state > 2 ? 1800 : 1200)) {
*pp = p->next;
free(p->user_name);
free(p);
p = *pp;
continue;
}
//if(strcmp(p->user_name, user_name) == 0) {
if(p->user_name_len == user_name_len && memcmp(p->user_name, user_name, user_name_len) == 0) {
p->last_change = current_time;
if(p->state > 2) return 1;
if(p->state < 2 && p->state == current_state) {
// Shouldn't happen!
char u[user_name_len + 1];
memcpy(u, user_name, user_name_len);
u[user_name_len] = 0;
fprintf(stderr, "check_recent_join: user '%s' has duplicated state %d!\n",
u, current_state);
p->state = 3;
return 1;
}
p->state = p->state > 1 ? 3 : 2;
return 0;
}
pp = &p->next;
p = p->next;
}
p = malloc(sizeof(struct user_join_time));
if(!p) {
fprintf(stderr, "check_recent_join: out of memory\n");
return 0;
}
//p->user_name = strdup(user_name);
p->user_name = malloc(user_name_len);
if(!p->user_name) {
fprintf(stderr, "check_recent_join: out of memory\n");
free(p);
return 0;
}
memcpy(p->user_name, user_name, user_name_len);
p->user_name_len = user_name_len;
p->state = current_state;
p->last_change = current_time;
p->next = recent_joined_users;
recent_joined_users = p;
return 0;
}
static void do_sshout_packet(int sshout_read_fd, int sshout_write_fd, int ssh_chat_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:
return;
case GET_PACKET_ERROR:
fprintf(stderr, "fd %d: %s\n", sshout_read_fd, strerror(errno));
if(sshout_ssh_pid > 0) kill(sshout_ssh_pid, SIGTERM);
return;
case GET_PACKET_SHORT_READ:
fprintf(stderr, "fd %d short read\n", sshout_read_fd);
if(sshout_ssh_pid > 0) kill(sshout_ssh_pid, SIGTERM);
return;
case GET_PACKET_TOO_SMALL:
fprintf(stderr, "Received API packet too small\n");
if(sshout_ssh_pid > 0) kill(sshout_ssh_pid, SIGTERM);
return;
case GET_PACKET_TOO_LARGE:
fprintf(stderr, "Received API packet too large (%u bytes)\n", length);
if(sshout_ssh_pid > 0) kill(sshout_ssh_pid, SIGTERM);
return;
case GET_PACKET_OUT_OF_MEMORY:
fprintf(stderr, "Out of memory\n");
if(sshout_ssh_pid > 0) kill(sshout_ssh_pid, SIGTERM);
return;
case 0:
break;
default:
fprintf(stderr, "Unknown error %d from sshout_get_api_packet\n", e);
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, "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;
}
break;
case SSHOUT_API_ONLINE_USERS_INFO:
if(ssh_chat_send_sshout_online_user_list(ssh_chat_write_fd, packet->data, length - 1) < 0) {
fprintf(stderr, "SSHOUT_API_ONLINE_USERS_INFO: failed\n");
if(sshout_ssh_pid > 0) kill(sshout_ssh_pid, SIGTERM);
}
break;
case SSHOUT_API_RECEIVE_MESSAGE:
if(do_sshout_message(sshout_write_fd, ssh_chat_write_fd, packet->data, length - 1) < 0) {
fprintf(stderr, "SSHOUT_API_RECEIVE_MESSAGE: failed\n");
if(sshout_ssh_pid > 0) kill(sshout_ssh_pid, SIGTERM);
}
break;
case SSHOUT_API_USER_STATE_CHANGE:
if(length < 1 + 1 + 1) {
fprintf(stderr, "SSHOUT_API_USER_STATE_CHANGE: malformed packet: too short (%u bytes)\n", (unsigned int)length);
if(sshout_ssh_pid > 0) kill(sshout_ssh_pid, SIGTERM);
break;
}
{
uint8_t state = *packet->data;
uint8_t user_name_len = packet->data[1];
if(1 + 1 + 1 + user_name_len > length) {
fprintf(stderr, "SSHOUT_API_USER_STATE_CHANGE: 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;
}
if(user_name_len == sshout_canonical_user_name_length && memcmp(packet->data + 2, sshout_canonical_user_name, user_name_len) == 0) break;
if(check_recent_join((const char *)(packet->data + 2), user_name_len, state)) {
fprintf(stderr, "SSHOUT_API_USER_STATE_CHANGE: user join/leave too often, skipping announcement to ssh-chat\n");
break;
}
sync_write(ssh_chat_write_fd, "* ", 2);
sync_write(ssh_chat_write_fd, packet->data + 2, user_name_len);
sync_write(ssh_chat_write_fd, " has ", 5);
if(state) {
sync_write(ssh_chat_write_fd, "joined", 6);
} else {
sync_write(ssh_chat_write_fd, "left", 4);
}
}
sync_write(ssh_chat_write_fd, "\r\n", 2);
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;
}
{
// gcc(1) says: dereferencing type-punned pointer will break strict-aliasing rules
//uint32_t error_code = ntohl(*(uint32_t *)packet->data);
uint32_t error_code;
memcpy(&error_code, packet->data, sizeof error_code);
error_code = ntohl(error_code);
fprintf(stderr, "SSHOUT error %u\n", (unsigned int)error_code);
if(error_code == SSHOUT_API_ERROR_USER_NOT_FOUND) {
#if 0
uint32_t len = ntohl(*(uint32_t *)(packet->data + 4));
if(1 + 4 + 4 + len < len || 1 + 4 + 4 + len > length) {
fprintf(stderr, "SSHOUT_API_ERROR: malformed packet: error_message is longer than packet (len=%u)\n", (unsigned int)len);
if(sshout_ssh_pid > 0) kill(sshout_ssh_pid, SIGTERM);
break;
}
#endif
sync_write(ssh_chat_write_fd, "/reply ", 7);
#if 0
sync_write(ssh_chat_write_fd, packet->data + 4 + 4, len);
#else
sync_write(ssh_chat_write_fd, "User not found", 14);
#endif
sync_write(ssh_chat_write_fd, "\r\n", 2);
}
}
break;
case SSHOUT_API_MOTD:
fprintf(stderr, "SSHOUT_API_MOTD: ignored\n");
break;
}
free(packet);
}
static char ssh_chat_read_buffer[4096];
static size_t ssh_chat_read_buffer_read;
static char ssh_chat_prompt[256];
static size_t ssh_chat_prompt_length;
static int need_check_ssh_chat_nick_name;
static int ssh_chat_nick_check_ok;
static void do_ssh_chat_line(const char *line, size_t line_len, int ssh_chat_write_fd, int sshout_write_fd) {
//fprintf(stderr, "function: do_ssh_chat_line(%p, %zu, %d)\n", line, line_len, sshout_write_fd);
#ifdef NEED_WORKAROUND_BROKEN_SSH_CHAT_TIMESTAMPS_IMPLEMENTATION
if(line_len > 7 && memcmp(line, "\x1b[A\x1b[2K", 7) == 0) {
// https://github.com/shazow/ssh-chat/pull/297/files#diff-7e8766c37bfedc72cfb3260489e0bf8dR165
// XXX
line += 7;
line_len -= 7;
}
#endif
if(need_check_ssh_chat_nick_name && !ssh_chat_nick_check_ok && line_len > 6 && memcmp(line, "[Guest", 6) == 0) {
if(ssh_chat_ssh_pid <= 0) {
fprintf(stderr, "do_ssh_chat_line: ssh_chat_ssh_pid = %d\n", (int)ssh_chat_ssh_pid);
return;
}
const char *sb = memchr(line + 6, ']', line_len - 6);
if(sb) {
size_t len = sb - line - 1;
char name[len + 1];
memcpy(name, line + 1, len);
name[len] = 0;
fprintf(stderr, "My nick name at ssh chat server was '%s', killing connection process %d ...\n",
name, (int)ssh_chat_ssh_pid);
} else {
fprintf(stderr, "My nick name at ssh chat server must be piao, killing connection process %d ...\n",
(int)ssh_chat_ssh_pid);
}
kill(ssh_chat_ssh_pid, SIGTERM);
sleep(1);
sleep(1);
return;
}
if(line_len * 3 < ssh_chat_prompt_length) return;
if(strncmp(ssh_chat_prompt, line, ssh_chat_prompt_length)) return;
ssh_chat_nick_check_ok = 1;
//assert(strlen(ssh_chat_prompt) <= ssh_chat_prompt_length);
const char *p = line + strlen(ssh_chat_prompt);
if(p[0] != '\x1b' || p[1] != '[') return;
if(p[2] == 'D') {
do {
p += 3;
if(p - line > line_len) return;
} while(memcmp(p, "\x1b[D", 3) == 0);
} else {
// https://github.com/shazow/ssh-chat/commit/418c991677ede73a5ff3259600c0c67f12bf43ea
char *end;
unsigned int n = strtoul(p + 2, &end, 10);
if(!end || end - p < 3 || end - p > 4) return;
if(*end != 'D') return;
if(n != ssh_chat_prompt_length) return;
p = end + 1;
}
if(p - line + 3 < line_len && memcmp(p, "\x1b[K", 3) == 0) p += 3;
if(p - line + ssh_chat_prompt_length < line_len && strncmp(ssh_chat_prompt, p, ssh_chat_prompt_length) == 0) {
// This is my own input line that being redisplayed by newer ssh-chat
// Needed after https://github.com/shazow/ssh-chat/pull/306
return;
}
if(p - line + 12 < line_len && memcmp(p, "[PM from ", 9) == 0) {
p += 9;
const char *sb = memchr(p, ']', line_len - (p - line));
if(!sb || sb - line + 2 >= line_len || sb[1] != ' ') return;
size_t user_name_len = sb - p;
char from_user_name[user_name_len + 1];
memcpy(from_user_name, p, user_name_len);
from_user_name[user_name_len] = 0;
p = sb + 2;
if(*p == '/') p++;
size_t len = line_len - (p - line);
if(len < 2) return;
if(p[len - 1] == '\r') len--;
if(p[len - 1] == '\a') len--;
if((len == 4 && memcmp(p, "list", 4) == 0) || (len == 5 && memcmp(p, "names", 5) == 0)) {
sshout_send_request_online_users(sshout_write_fd);
} else if(len == 3 && memcmp(p, "msg", 3) == 0) {
sync_write(ssh_chat_write_fd, "/msg ", 5);
sync_write(ssh_chat_write_fd, from_user_name, user_name_len);
sync_write(ssh_chat_write_fd, " Missing user\r\n", 15);
} else if(len > 3 && memcmp(p, "msg ", 4) == 0) {
const char *to_user_p = p + 4;
p = memchr(to_user_p, ' ', len - 4);
if(!p) {
sync_write(ssh_chat_write_fd, "/msg ", 5);
sync_write(ssh_chat_write_fd, from_user_name, user_name_len);
sync_write(ssh_chat_write_fd, " Missing message\r\n", 18);
return;
}
size_t to_user_name_len = p - to_user_p;
char to_user_name[to_user_name_len];
memcpy(to_user_name, to_user_p, to_user_name_len);
to_user_name[to_user_name_len] = 0;
if(strcmp(to_user_name, sshout_canonical_user_name) == 0) {
fprintf(stderr, "do_ssh_chat_line: igroning private messaging request from '%s' to myself '%s'\n",
from_user_name, sshout_canonical_user_name);
sync_write(ssh_chat_write_fd, "/msg ", 5);
sync_write(ssh_chat_write_fd, from_user_name, user_name_len);
sync_write(ssh_chat_write_fd, " Cannot PM to myself\r\n", 22);
return;
}
p++;
len -= 4 + to_user_name_len + 1;
size_t new_message_len = user_name_len + 4 + to_user_name_len + 2 + len;
char message[new_message_len];
memcpy(message, from_user_name, user_name_len);
memcpy(message + user_name_len, " to ", 4);
memcpy(message + user_name_len + 4, to_user_name, to_user_name_len);
memcpy(message + user_name_len + 4 + to_user_name_len, ": ", 2);
memcpy(message + user_name_len + 4 + to_user_name_len + 2, p, len);
sshout_send_plain_text_message_fixed_length(sshout_write_fd, to_user_name, message, new_message_len);
} else if(len == 4 && memcmp(p, "peer", 4) == 0) {
sync_write(ssh_chat_write_fd, "/msg ", 5);
sync_write(ssh_chat_write_fd, from_user_name, user_name_len);
sync_write(ssh_chat_write_fd, " The peer is a SSHOUT server: ", 30);
if(is_sshout_server_address_public) {
sync_write(ssh_chat_write_fd, "ssh://", 6);
size_t server_name_len = strlen(sshout_server_name);
sync_write(ssh_chat_write_fd, sshout_server_name, server_name_len);
if(sshout_server_port != 22) {
char buffer[1 + 5 + 1];
size_t len = sprintf(buffer, ":%hu", (unsigned short int)sshout_server_port);
sync_write(ssh_chat_write_fd, buffer, len);
}
sync_write(ssh_chat_write_fd, "/\r\n", 3);
} else {
sync_write(ssh_chat_write_fd, "[private address]\r\n", 19);
}
} else if(len == 4 && memcmp(p, "help", 4) == 0) {
sync_write(ssh_chat_write_fd, "/msg ", 5);
sync_write(ssh_chat_write_fd, from_user_name, user_name_len);
sync_write(ssh_chat_write_fd, " Supported commands: list, msg, peer\r\n", 38);
} else {
char message[len + 1];
memcpy(message, p, len);
message[len] = 0;
fprintf(stderr, "do_ssh_chat_line: igroning unrecognized command '%s' from '%s'\n",
message, from_user_name);
sync_write(ssh_chat_write_fd, "/msg ", 5);
sync_write(ssh_chat_write_fd, from_user_name, user_name_len);
sync_write(ssh_chat_write_fd, " Unknown command '", 18);
sync_write(ssh_chat_write_fd, p, len);
sync_write(ssh_chat_write_fd, "'\r\n", 3);
}
return;
}
size_t remain_len = line_len - (p - line);
if(remain_len >= 2 && (p[0] == '-' || p[0] == ' ') && p[1] == '>') {
if(!*last_requesting_sshout_user || p[0] == ' ') return;
if(remain_len > 8) {
const char *reply = p + 3;
size_t len = remain_len - 3;
if(memcmp(reply, "Err: ", 5)) {
if(len < 13) return;
if(len > 13 && memcmp(reply, "[Sent PM to ", 12) == 0 && memchr(reply + 13, ']', len - 13)) {
*last_requesting_sshout_user = 0;
return;
}
const char *colon = memchr(reply, ':', len);
if(!colon || colon - reply < 11) return;
if(memcmp(colon - 10, " connected", 10)) return;
}
if(reply[len - 1] == '\r') len--;
if(reply[len - 1] == '\a') len--;
sshout_send_plain_text_message_fixed_length(sshout_write_fd, last_requesting_sshout_user, reply, len);
*last_requesting_sshout_user = 0;
}
return;
}
if(remain_len >= 3 && (p[0] == '*' || p[0] == ' ') && p[1] == '*' && p[2] == ' ') {
p++;
remain_len--;
} else {
const char *colon = memchr(p, ':', remain_len);
if(!colon || colon - p + 1 >= remain_len || colon[1] != ' ') return;
}
if(p[remain_len - 1] == '\r') remain_len--;
if(p[remain_len - 1] == '\a') remain_len--;
#if 0
sync_write(STDERR_FILENO, p, remain_len);
sync_write(STDERR_FILENO, "\n", 1);
#endif
sshout_send_plain_text_message_fixed_length(sshout_write_fd, "GLOBAL", p, remain_len);
}
static void do_ssh_chat_data(int ssh_chat_read_fd, int ssh_chat_write_fd, int sshout_write_fd) {
int s;
if(ssh_chat_read_buffer_read >= sizeof ssh_chat_read_buffer) {
fputs("do_ssh_chat_message: no buffer space, discarding old contains\n", stderr);
ssh_chat_read_buffer_read = 0;
}
do {
s = read(ssh_chat_read_fd, ssh_chat_read_buffer + ssh_chat_read_buffer_read, sizeof ssh_chat_read_buffer - ssh_chat_read_buffer_read);
} while(s < 0 && errno == EINTR);
if(s < 0) {
perror("do_ssh_chat_message: read");
return;
}
if(!s) return;
int ss = ssh_chat_read_buffer_read < 2 ? 0 : ssh_chat_read_buffer_read - 1;
int ss_diff = ssh_chat_read_buffer_read - ss;
char *br = memmem(ssh_chat_read_buffer + ss, s + ss_diff, "\r\n", 2);
if(br) {
int skip_len = 0;
char *last_br_end;
do {
if(*br) *br = 0;
int line_len = br - ssh_chat_read_buffer - skip_len;
do_ssh_chat_line(ssh_chat_read_buffer + skip_len, line_len, ssh_chat_write_fd, sshout_write_fd);
br += 2;
last_br_end = br;
br = memmem(br, s + ss_diff - (br - (ssh_chat_read_buffer + ss)), "\r\n", 2);
skip_len += line_len + 2;
} while(br);
ss += s - skip_len;
ssh_chat_read_buffer_read = ss + ss_diff;
memmove(ssh_chat_read_buffer, last_br_end, ssh_chat_read_buffer_read);
} else {
ssh_chat_read_buffer_read += s;
}
}
int main(int argc, char **argv) {
char **sshout_ssh_options = NULL;
char *sshout_user_name = NULL;
char **ssh_chat_ssh_options = NULL;
int i = 1;
while(argv[i]) {
if(argv[i][0] == '-') {
char *o = argv[i] + 1;
if(o[1]) {
fprintf(stderr, "%s: Invalid option syntax; this program requiring arguments separated from its option\n",
argv[0]);
return -1;
}
if(sshout_server_name && ssh_chat_server_name) {
fprintf(stderr, "%s: Extra option after last server URL\n", argv[0]);
return -1;
}
char ***ssh_argv = sshout_server_name ? &ssh_chat_ssh_options : &sshout_ssh_options;
switch(*o) {
case 'i':
if(++i >= argc) {
fprintf(stderr, "%s: Option '-i' requires an argument\n", argv[0]);
return -1;
}
add_arg(ssh_argv, "-i");
add_arg(ssh_argv, argv[i]);
break;
case 'o':
if(++i >= argc) {
fprintf(stderr, "%s: Option '-o' requires an argument\n", argv[0]);
return -1;
}
add_arg(ssh_argv, "-o");
add_arg(ssh_argv, argv[i]);
break;
case 'v':
add_arg(ssh_argv, "-v");
break;
case 'P':
//*(sshout_server_name ? &is_ssh_chat_server_address_public : &is_sshout_server_address_public) = 1;
if(sshout_server_name) is_ssh_chat_server_address_public = 1;
else is_sshout_server_address_public = 1;
break;
default:
fprintf(stderr, "%s: Invalid option '-%c'\n", argv[0], *o);
return -1;
}
} else if(!sshout_server_name) {
if(prase_ssh_url(&sshout_server_name, &sshout_server_port, &sshout_user_name, argv[i]) < 0) {
fprintf(stderr, "%s: Failed to prase '%s' as a SSH URL\n", argv[0], argv[i]);
return -1;
}
} else if(!ssh_chat_server_name) {
if(prase_ssh_url(&ssh_chat_server_name, &ssh_chat_server_port, &ssh_chat_user_name, argv[i]) < 0) {
fprintf(stderr, "%s: Failed to prase '%s' as a SSH URL\n", argv[0], argv[i]);
return -1;
}
} else {
fprintf(stderr, "%s: Extra string after SSH chat server URL\n", argv[0]);
return -1;
}
i++;
}
if(!sshout_server_name || !ssh_chat_server_name) {
fprintf(stderr, "%s: need 2 URLs\n", argv[0]);
print_usage(argv[0]);
return -1;
}
if(!sshout_user_name) {
sshout_user_name = strdup("sshout");
if(!sshout_user_name) {
fprintf(stderr, "%s: Out of memory\n", argv[0]);
return 1;
}
}
if(!ssh_chat_user_name) {
ssh_chat_user_name = strdup(SSH_CHAT_DEFAULT_USER_NAME);
if(!ssh_chat_user_name) {
fprintf(stderr, "%s: Out of memory\n", argv[0]);
return 1;
}
}
need_check_ssh_chat_nick_name = strncmp(ssh_chat_user_name, "Guest", 5);
ssh_chat_prompt_length = snprintf(ssh_chat_prompt, sizeof ssh_chat_prompt, "[%s] ", ssh_chat_user_name);
struct sigaction act = { .sa_handler = SIG_IGN };
if(sigaction(SIGPIPE, &act, NULL) < 0) {
perror("sigaction: SIGPIPE");
return 1;
}
act.sa_handler = signal_handler;
if(sigaction(SIGCHLD, &act, NULL) < 0) {
perror("sigaction: SIGCHLD");
return 1;
}
int sshout_write_fd = -1, sshout_read_fd = -1;
int ssh_chat_write_fd = -1, ssh_chat_read_fd = -1;
fd_set rfdset;
while(1) {
if(sshout_ssh_pid == -1 || kill(sshout_ssh_pid, 0) < 0) {
if(sshout_write_fd != -1) close(sshout_write_fd);
if(sshout_read_fd != -1) close(sshout_read_fd);
sshout_api_version = 0;
sshout_ssh_pid = start_ssh_process(sshout_server_name, sshout_server_port, sshout_user_name, "api", sshout_ssh_options, &sshout_write_fd, &sshout_read_fd);
if(sshout_ssh_pid == -1) {
sleep(1);
continue;
}
sshout_send_hello(sshout_write_fd);
}
if(ssh_chat_ssh_pid == -1 || kill(ssh_chat_ssh_pid, 0) < 0) {
if(ssh_chat_write_fd != -1) close(ssh_chat_write_fd);
if(ssh_chat_read_fd != -1) close(ssh_chat_read_fd);
ssh_chat_nick_check_ok = 0;
ssh_chat_read_buffer_read = 0;
ssh_chat_ssh_pid = start_ssh_process(ssh_chat_server_name, ssh_chat_server_port, ssh_chat_user_name, NULL, ssh_chat_ssh_options, &ssh_chat_write_fd, &ssh_chat_read_fd);
if(ssh_chat_ssh_pid == -1) {
sleep(1);
continue;
}
sync_write(ssh_chat_write_fd, "/theme mono\r\n", 13);
}
FD_ZERO(&rfdset);
FD_SET(sshout_read_fd, &rfdset);
FD_SET(ssh_chat_read_fd, &rfdset);
int max_fd = MAX(sshout_read_fd, ssh_chat_read_fd);
if(select(max_fd + 1, &rfdset, NULL, NULL, NULL) < 0) {
perror("select");
sleep(1);
continue;
}
if(FD_ISSET(sshout_read_fd, &rfdset)) {
do_sshout_packet(sshout_read_fd, sshout_write_fd, ssh_chat_write_fd);
}
if(FD_ISSET(ssh_chat_read_fd, &rfdset)) {
do_ssh_chat_data(ssh_chat_read_fd, ssh_chat_write_fd, sshout_write_fd);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment