blob: 76ec142578267d3bc61f4a466c6524e885b71693 [file] [log] [blame]
// Copyright 2018 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Simple SSH daemon with inline shell for quick testing.
#include <cctype>
#include <codecvt>
#include <locale>
#include <map>
#include <string>
#include <vector>
#include <err.h>
#include <signal.h>
#include <stdarg.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <libssh/callbacks.h>
#include <libssh/libssh.h>
#include <libssh/server.h>
namespace {
// Command line settings.
class Options {
public:
Options();
std::string user;
std::string host;
std::string port;
int verbosity;
};
Options::Options()
: user("anon"),
host("localhost"),
port("22222"),
verbosity(0) {}
// Data passed to various SSH callbacks.
class Userdata {
public:
const Options* options;
struct ssh_channel_callbacks_struct* channel_cb;
bool authenticated;
bool tty_allocated;
ssh_channel channel;
};
// Left in for easy access for debugging.
#if 0
// A sprintf for C++.
std::string sprintf(const std::string fmt, ...) { // NOLINT(runtime/printf)
// Overestimate the size of the initial buffer.
int size = (int)fmt.size() * 2;
std::string ret;
va_list ap;
while (1) {
// Allocate the buffer we're going to write to.
ret.resize(size);
// Attempt the format to the buffer.
va_start(ap, fmt);
int len = vsnprintf(const_cast<char*>(ret.data()), size, fmt.c_str(), ap);
va_end(ap);
// If we had enough space, then shrink the buffer back and return.
if (len >= 0 && len < size) {
ret.resize(len);
break;
}
// If we need more space, increase it!
if (len < 0)
size *= 2;
else
size = len + 1;
}
return ret;
}
#endif
// Helper to write a constant string.
int ssh_channel_write_str(ssh_channel channel, const std::string& str) {
return ssh_channel_write(channel, str.c_str(), str.length());
}
// Convert a Unicode codepoint to a string.
const std::string codepointToUtf8(unsigned long codepoint) {
std::wstring_convert<std::codecvt_utf8<char32_t>, char32_t> converter;
return converter.to_bytes(codepoint);
}
// Callback when processing a NONE authorization request.
int auth_none(ssh_session session, const char* user, void* userdata) {
Userdata* data = (Userdata*)(userdata);
printf("Authenticating user '%s' via NONE ... ", user);
if (data->options->user == user) {
data->authenticated = true;
printf("OK!\n");
return SSH_AUTH_SUCCESS;
} else {
printf("FAIL: wanted '%s'\n", data->options->user.c_str());
ssh_disconnect(session);
return SSH_AUTH_DENIED;
}
}
// Callback when a tty is requested.
int pty_request(ssh_session session,
ssh_channel channel,
const char* term,
int x,
int y,
int px,
int py,
void* userdata) {
Userdata* data = (Userdata*)(userdata);
data->tty_allocated = true;
const std::string str = "Allocated terminal [" + std::to_string(x) +
" cols x " + std::to_string(y) + " rows] [" +
std::to_string(px) + "px x " + std::to_string(py) +
"px] TERM=" + std::string(term) + "\n\r";
fputs(str.c_str(), stdout);
ssh_channel_write_str(channel, str);
return 0;
}
// Callback when a shell is requested.
int shell_request(ssh_session session, ssh_channel channel, void* userdata) {
printf("Allocated shell\n");
return 0;
}
// Callback when an env var is sent.
int env_request(ssh_session session,
ssh_channel channel,
const char* env_name,
const char* env_value,
void* userdata) {
printf("Received env %s=\"%s\"\n", env_name, env_value);
return 0;
}
// Callback when a new channel is requested.
ssh_channel new_session_channel(ssh_session session, void* userdata) {
Userdata* data = (Userdata*)(userdata);
if (data->channel)
return nullptr;
printf("Allocated session channel\n");
data->channel = ssh_channel_new(session);
ssh_callbacks_init(data->channel_cb);
ssh_set_channel_callbacks(data->channel, data->channel_cb);
return data->channel;
}
// Extract a hex value from the string starting as pos. By default we
// consume exactly count bytes, but if the hex value is enclosed by braces
// (e.g. {FF} rather than FF), we allow an arbitrary number of digits.
bool parse_hex(const std::string& str,
size_t pos,
size_t count,
unsigned long* hex,
size_t* read) {
*read = 0;
if (str[pos] == '{') {
size_t end = str.find('}', pos + 1);
if (pos + 1 == end || end == std::string::npos)
return false;
*read = 2;
++pos;
count = end - pos;
}
std::string dbuf = str.substr(pos, count);
if (dbuf.length() != count)
return false;
for (auto ch : dbuf)
if (!isxdigit(ch))
return false;
*hex = std::stoul(dbuf, 0, 16);
*read += count;
return true;
}
enum {
CMD_CONTINUE = 0,
CMD_EXIT_CLIENT,
CMD_EXIT_SERVER,
};
// Handle the "osc" command.
int cmd_osc(ssh_channel chan, const std::vector<std::string>& argv) {
if (argv.size() == 1) {
ssh_channel_write_str(chan, "error: osc needs at least one argument\n\r");
return CMD_CONTINUE;
}
ssh_channel_write_str(chan, "\e]");
for (size_t i = 1; i < argv.size(); ++i) {
if (i > 1)
ssh_channel_write_str(chan, ";");
ssh_channel_write_str(chan, argv[i]);
}
ssh_channel_write_str(chan, "\a");
return CMD_CONTINUE;
}
// Handle the "print" command.
int cmd_print(ssh_channel chan, const std::vector<std::string>& argv) {
// Skip the "print" command itself in argv[0].
for (size_t argi = 1; argi < argv.size(); ++argi) {
const std::string arg = argv[argi];
if (argi > 1)
ssh_channel_write_str(chan, " ");
// Walk the arg one char at a time. Could be made faster with a search,
// but meh -- it's fast enough already for our needs.
for (size_t i = 0; i < arg.length(); ++i) {
char ch = arg[i];
if (ch == '\\') {
// Process an escape sequence.
ch = arg[++i];
switch (ch) {
case '\\': // Escape the escape!
ssh_channel_write_str(chan, "\\");
break;
case '0' ... '7': { // 1 to 3 digit octal.
std::string dbuf = {ch};
if (arg[i + 1] >= '0' && arg[i + 1] <= '7') {
dbuf.push_back(arg[++i]);
if (arg[i + 1] >= '0' && arg[i + 1] <= '7')
dbuf.push_back(arg[++i]);
}
int digit = std::stoi(dbuf, 0, 8);
if (digit > 0xff) {
ssh_channel_write_str(chan,
"\n\rprint: octal number too big\n\r");
} else {
ch = digit;
ssh_channel_write(chan, &ch, 1);
}
break;
}
case 'a': // Bell.
ssh_channel_write_str(chan, "\a");
break;
case 'b': // Backspace.
ssh_channel_write_str(chan, "\b");
break;
case 'c': // Control char. Ctrl+a == 0x01 ... Ctrl+z == 0x1a.
ch = tolower(arg[i + 1]);
if (!isalpha(ch)) {
ssh_channel_write_str(
chan, "\n\rprint: invalid control escape sequence\n\r");
} else {
++i;
ch = ch - 'a' + 1;
ssh_channel_write(chan, &ch, 1);
}
break;
case 'E':
case 'e': // Escape.
ssh_channel_write_str(chan, "\e");
break;
case 'f': // Form feed.
ssh_channel_write_str(chan, "\f");
break;
case 'n': // New line.
ssh_channel_write_str(chan, "\n");
break;
case 'r': // Carriage return.
ssh_channel_write_str(chan, "\r");
break;
case 't': // Tab.
ssh_channel_write_str(chan, "\t");
break;
case 'v': // Vertical tab.
ssh_channel_write_str(chan, "\v");
break;
case 'x': { // 2 digit hexcode.
unsigned long hex;
size_t consumed;
if (!parse_hex(arg, i + 1, 2, &hex, &consumed)) {
ssh_channel_write_str(
chan, "\n\rprint: invalid \\xHH escape sequence\n\r");
} else if (hex > 0xff) {
ssh_channel_write_str(chan, "\n\rprint: hex number too big\n\r");
} else {
ch = hex;
ssh_channel_write(chan, &ch, 1);
}
i += consumed;
break;
}
case 'u': // 4 digit unicode codepoint.
case 'U': { // 8 digit unicode codepoint.
unsigned long hex;
size_t consumed;
if (!parse_hex(arg, i + 1, ch == 'u' ? 4 : 8, &hex, &consumed)) {
ssh_channel_write_str(
chan, "\n\rprint: invalid Unicode escape sequence\n\r");
} else if (hex > 0x10FFFF) {
ssh_channel_write_str(chan,
"\n\rprint: Unicode codepoint too big\n\r");
} else {
ssh_channel_write_str(chan, codepointToUtf8(hex));
}
i += consumed;
break;
}
default:
ssh_channel_write_str(chan,
"\n\rprint: unknown escape sequence\n\r");
break;
}
} else {
ssh_channel_write(chan, &ch, 1);
}
}
}
ssh_channel_write_str(chan, "\n\r");
return CMD_CONTINUE;
}
// Handle the "quit" command.
int cmd_quit(ssh_channel chan, const std::vector<std::string>& argv) {
int status;
switch (argv.size()) {
case 1:
status = 0;
break;
case 2:
status = std::stoi(argv[1]);
break;
default:
ssh_channel_write_str(chan, "error: quit takes only one argument\n\r");
status = 255;
break;
}
ssh_channel_write_str(chan, "BYE\n\r");
ssh_channel_request_send_exit_status(chan, status); // TODO: Doesn't work.
return CMD_EXIT_CLIENT;
}
// Handle the "shutdown" command.
int cmd_shutdown(ssh_channel chan, const std::vector<std::string>& argv) {
if (argv.size() > 1)
ssh_channel_write_str(chan, "error: shutdown takes no arguments\n\r");
ssh_channel_write_str(chan, "shutting down\n\r");
ssh_channel_request_send_exit_status(chan, 0); // TODO: Doesn't work.
return CMD_EXIT_SERVER;
}
// Handle the "image" command.
// We support a few stock images of common sizes.
int cmd_image(ssh_channel chan, const std::vector<std::string>& argv) {
int img = 16;
if (argv.size() > 1)
img = std::stoi(argv[1]);
switch (img) {
case 16:
case 32:
case 64:
case 128:
case 256:
case 512:
break;
default:
ssh_channel_write_str(chan, "error: unknown image: " + argv[1] + "\n\r");
return CMD_CONTINUE;
}
ssh_channel_write_str(
chan, "\e]1337;File=name=dGVzdC5naWY=;width=8px;inline=1;height=" +
std::to_string(img) + "px");
for (size_t i = 2; i < argv.size(); ++i)
ssh_channel_write_str(chan, ";" + argv[i]);
ssh_channel_write_str(chan, ":");
switch (img) {
case 16:
ssh_channel_write_str(
chan,
"R0lGODdhCAAQAIAAAP///wAAACwAAAAACAAQAAACFkSAhpfMC1uMT1mabHWZy6t1U/"
"htQAEAOw==");
break;
case 32:
ssh_channel_write_str(
chan,
"R0lGODdhCAAgAIAAAP///wAAACwAAAAACAAgAAACI0SAhpfMC1uMT1mabHWZy6t1U/"
"hto4eVIoiS6evG7XzWLFAAADs=");
break;
case 64:
ssh_channel_write_str(
chan,
"R0lGODdhCABAAIAAAP///wAAACwAAAAACABAAAACOUSAhpfMC1uMT1mabHWZy6t1U/"
"hto4eVIoiS6evG7XzWrG3ees6vvQqE0Xa+YlCGMwqTx+FvSZQUAAA7");
break;
case 128:
ssh_channel_write_str(
chan,
"R0lGODdhCACAAIAAAP///wAAACwAAAAACACAAAACWESAhpfMC1uMT1mabHWZy6t1U/"
"hto4eVIoiS6evG7XzWrG3ees6vvQqE0Xa+YlCGMwqTx+"
"FvSWwyoU9klKq0Vp1ZrvSq7U7D3+3Yiy2LwWhy+u2Ot+fnegEAOw==");
break;
case 256:
ssh_channel_write_str(
chan,
"R0lGODdhCAAAAYAAAP///wAAACwAAAAACAAAAQACjESAhpfMC1uMT1mabHWZy6t1U/"
"hto4eVIoiS6evG7XzWrG3ees6vvQqE0Xa+YlCGMwqTx+"
"FvSWwyoU9klKq0Vp1ZrvSq7U7D3+3Yiy2LwWhy+u2Ot+fnOttuvuvz/"
"HVfDQhHt+dXGCiHZyiYeDj4t0jYyAj5iBhJqWhZ6ZjJKXmp2TkZ+"
"rk56olZKloAADs=");
break;
case 512:
ssh_channel_write_str(
chan,
"R0lGODdhCAAAAoAAAP///wAAACwAAAAACAAAAgAC10SAhpfMC1uMT1mabHWZy6t1U/"
"hto4eVIoiS6evG7XzWrG3ees6vvQqE0Xa+YlCGMwqTx+"
"FvSWwyoU9klKq0Vp1ZrvSq7U7D3+3Yiy2LwWhy+u2Ot+fnOttuvuvz/"
"HVfDQhHt+dXGCiHZyiYeDj4t0jYyAj5iBhJqWhZ6ZjJKXmp2TkZ+"
"rk56olZKgqKSpr66hrbOntay2prequby7vaqwoMS7vrWxwsi2ssnHw8/LtM3MwM/"
"YwcTa1sXe2czS19rd09Hf69Pe6NXS4Ojk6e/u4e3z5/XlcAADs=");
break;
}
ssh_channel_write_str(chan, "\a");
return CMD_CONTINUE;
}
// Forward decl.
int cmd_help(ssh_channel chan, const std::vector<std::string>& argv);
// Register all the commands available to the client.
using cmd_t = int (*)(ssh_channel chan, const std::vector<std::string>&);
struct cmd {
cmd_t func;
const std::string args;
const std::string usage;
};
std::map<const std::string, cmd> CommandMap = {
{"help", {cmd_help, "", "This help screen!"}},
{"h", {cmd_help}},
{"?", {cmd_help}},
{"print", {cmd_print, "<str>", "Print a string (w/escape sequences)"}},
{"p", {cmd_print}},
{"quit", {cmd_quit, "[code]", "Exit this loop"}},
{"q", {cmd_quit}},
{"exit", {cmd_quit}},
{"shutdown", {cmd_shutdown, "", "Shutdown the server"}},
{"stop", {cmd_shutdown}},
{"s", {cmd_shutdown}},
{"image", {cmd_image, "[name]", "Display an image"}},
{"i", {cmd_image}},
{"osc", {cmd_osc, "[args]", "Run an Operating System Command (OSC)"}},
{"o", {cmd_osc}},
};
// Handle the "help" command.
int cmd_help(ssh_channel chan, const std::vector<std::string>& argv) {
if (argv.size() > 1)
ssh_channel_write_str(chan, "error: help takes no arguments\n\r");
ssh_channel_write_str(chan, "Available commands:\n\r");
// Calculate the max LHS width.
size_t width = 0;
for (auto it : CommandMap) {
const cmd* cmd = &it.second;
if (cmd->usage.empty())
continue;
std::string lhs = it.first + " " + cmd->args;
width = std::max(width, lhs.size());
}
// Pad the right side by 3.
width += 3;
// Display all the lines now.
for (auto it : CommandMap) {
const cmd* cmd = &it.second;
if (cmd->usage.empty())
continue;
std::string lhs = it.first + " " + cmd->args;
lhs += std::string(width - lhs.size(), ' ');
ssh_channel_write_str(chan, " " + lhs + cmd->usage + "\n\r");
}
return CMD_CONTINUE;
}
// Split a string up into a command vector.
std::vector<std::string> ParseCommand(const std::string& str) {
std::vector<std::string> ret;
size_t startpos = str.find_first_not_of(' ');
if (startpos == std::string::npos)
return ret;
while (1) {
size_t pos = str.find(' ', startpos + 1);
if (pos == std::string::npos)
break;
ret.push_back(str.substr(startpos, pos - startpos));
startpos = str.find_first_not_of(' ', pos + 1);
if (startpos == std::string::npos)
return ret;
}
ret.push_back(str.substr(startpos));
return ret;
}
// The main interactive loop for the client.
int client_loop(Userdata* data) {
ssh_channel chan = data->channel;
std::string buf;
char readbuf[4096];
int readlen;
size_t pos;
ssh_channel_write_str(chan, "echosshd shell started\n\r");
ssh_channel_write_str(chan, ">>> ");
while (1) {
readlen = ssh_channel_read(chan, readbuf, sizeof(readbuf), 0);
if (readlen <= 0)
return CMD_EXIT_CLIENT;
size_t oldlen = buf.length();
buf.append(readbuf, readlen);
// Deal with non-printable sequences.
pos = oldlen;
while (pos < buf.length()) {
switch (buf[pos]) {
// Whitelist characters we accept.
case 0x04: // EOT CTRL+D
case 0x0a: // \n newline
case 0x0d: // \r return
case 0x20 ... 0x7e:
++pos;
break;
// Special case a few controls.
case 0x03: // CTRL+C
// Abort everything.
ssh_channel_write_str(chan, "^C\n\r>>> ");
buf.clear();
continue;
case 0x08: // \b backspace
case 0x7f: // DEL
if (pos == 0) {
// Start of the buffer so just eat it.
buf.erase(pos, 1);
} else {
// Start of the new part of the buffer, so back up.
if (pos == oldlen)
--oldlen;
ssh_channel_write_str(chan, "\b \b");
buf.erase(pos - 1, 2);
}
break;
case 0x0c: // CTRL+L
buf.erase(pos, 1);
// Move cursor home & clear screen.
ssh_channel_write_str(chan, "\e[H\e[2J");
// Redisplay prompt & pending buffer.
ssh_channel_write_str(chan, ">>> ");
ssh_channel_write_str(chan, buf);
break;
case 0x15: // CTRL+U
// Clear to start of line & move cursor to start of line.
ssh_channel_write_str(chan, "\e[1K\e[G");
ssh_channel_write_str(chan, ">>> ");
buf.clear();
continue;
// Throw away everything else.
default:
buf.erase(pos, 1);
break;
}
}
// Wait for the buffer to get a newline.
reparse:
pos = buf.find('\n', oldlen);
if (pos == std::string::npos) {
pos = buf.find('\r', oldlen);
if (pos == std::string::npos) {
if (buf[0] == 0x04) {
// CTRL+D for EOT.
buf = "q";
} else {
// Keep waiting for more data.
ssh_channel_write(chan, buf.c_str() + oldlen, buf.length() - oldlen);
continue;
}
}
}
// We've got a newline, so extract the command.
ssh_channel_write_str(chan, "\n\r");
std::vector<std::string> argv = ParseCommand(buf.substr(0, pos));
buf.erase(0, pos + 1);
// Dispatch the command.
if (argv.empty()) {
// Nothing to do. User hit enter.
} else {
const std::string cmd = argv[0];
auto it = CommandMap.find(cmd);
if (it != CommandMap.end()) {
int ret = it->second.func(chan, argv);
if (ret != CMD_CONTINUE)
return ret;
} else {
ssh_channel_write_str(chan, "uknown command: " + cmd + "\n\r");
}
}
// If there's more data pending, see if there are commands.
if (!buf.empty()) {
oldlen = buf.length();
goto reparse;
}
ssh_channel_write_str(chan, ">>> ");
}
}
// The main loop for the sshd to wait for a connection and start a client.
int sshd_main(ssh_bind sshbind, const Options& options) {
struct ssh_channel_callbacks_struct channel_cb;
struct ssh_server_callbacks_struct cb;
Userdata data = {
.options = &options,
.channel_cb = &channel_cb,
.authenticated = false,
.tty_allocated = false,
.channel = nullptr,
};
memset(&channel_cb, 0, sizeof(channel_cb));
channel_cb.userdata = (void*)&data;
channel_cb.channel_pty_request_function = pty_request;
channel_cb.channel_shell_request_function = shell_request;
channel_cb.channel_env_request_function = env_request;
memset(&cb, 0, sizeof(cb));
cb.userdata = (void*)&data;
cb.auth_none_function = auth_none;
cb.channel_open_request_session_function = new_session_channel;
ssh_event event;
int ret;
ssh_session session = ssh_new();
ssh_callbacks_init(&cb);
ret = ssh_bind_accept(sshbind, session);
if (ret == SSH_ERROR)
errx(1, "ssh_bind_accept: %s\n", ssh_get_error(sshbind));
switch (fork()) {
default:
// Clean up in the parent and return.
ssh_disconnect(session);
ssh_free(session);
return CMD_CONTINUE;
case 0:
// Clean up resrouces in the child to unblock the parent.
ssh_bind_free(sshbind);
signal(SIGCHLD, SIG_IGN);
break;
case -1:
err(1, "fork");
}
ssh_set_server_callbacks(session, &cb);
if (ssh_handle_key_exchange(session)) {
warn("ssh_handle_key_exchange: %s", ssh_get_error(session));
ret = CMD_CONTINUE;
goto done;
}
ssh_set_auth_methods(session, SSH_AUTH_METHOD_NONE);
event = ssh_event_new();
ssh_event_add_session(event, session);
while (!data.authenticated || !data.tty_allocated ||
data.channel == nullptr) {
ret = ssh_event_dopoll(event, -1);
if (ret == SSH_ERROR) {
ssh_disconnect(session);
errx(1, "ssh_event_dopoll: %s", ssh_get_error(session));
}
}
printf("Starting client loop\n");
ret = client_loop(&data);
done:
printf("Finishing session\n");
ssh_disconnect(session);
ssh_free(session);
exit(ret);
}
// Watch the exit status of children.
void sigchild(int signum, siginfo_t* info, void* data) {
if (info->si_status == CMD_EXIT_SERVER)
exit(0);
waitpid(info->si_pid, nullptr, WNOHANG);
}
// Show the CLI usage and exit.
void usage(const Options* options, int status) {
fprintf(status ? stderr : stdout,
"Usage: echosshd [options]\n"
"Options:\n"
" -l<host> The host to listen on (default %s)\n"
" -p<port> The port to listen on (default %s)\n"
" -u<user> The user to allow (default %s)\n"
" -h This help screen\n",
options->host.c_str(), options->port.c_str(), options->user.c_str());
exit(status);
}
// Parse the command line arguments and set up the server options.
void parse_args(int argc, char* argv[], Options* options) {
int c;
int verbosity = 0;
std::string user = options->user;
std::string host = options->host;
std::string port = options->port;
while ((c = getopt(argc, argv, "l:p:u:vh")) != -1) {
switch (c) {
case 'l':
host = optarg;
break;
case 'p':
port = optarg;
break;
case 'u':
user = optarg;
break;
case 'v':
++verbosity;
break;
case 'h':
usage(options, 0);
break;
default:
usage(options, 1);
break;
}
}
if (argc != optind)
errx(1, "no arguments accepted");
options->user = std::move(user);
options->host = std::move(host);
options->port = std::move(port);
options->verbosity = verbosity;
}
} // namespace
int main(int argc, char* argv[]) {
Options options;
ssh_bind sshbind = ssh_bind_new();
parse_args(argc, argv, &options);
ssh_bind_options_set(sshbind, SSH_BIND_OPTIONS_BINDADDR,
options.host.c_str());
ssh_bind_options_set(sshbind, SSH_BIND_OPTIONS_BINDPORT_STR,
options.port.c_str());
#if LIBSSH_VERSION_INT < SSH_VERSION_INT(0, 6, 4)
ssh_bind_options_set(sshbind, SSH_BIND_OPTIONS_RSAKEY, "host_key.rsa");
#else
ssh_bind_options_set(sshbind, SSH_BIND_OPTIONS_ECDSAKEY, "host_key.ecdsa");
#endif
ssh_bind_options_set(sshbind, SSH_BIND_OPTIONS_LOG_VERBOSITY,
&options.verbosity);
struct sigaction sa;
sa.sa_sigaction = sigchild;
sa.sa_flags = SA_NOCLDSTOP | SA_RESTART | SA_SIGINFO;
sigemptyset(&sa.sa_mask);
sigaction(SIGCHLD, &sa, nullptr);
if (ssh_bind_listen(sshbind) < 0)
errx(1, "ssh_bind_listen: %s", ssh_get_error(sshbind));
while (1) {
printf("waiting for connection on %s:%s for user %s\n",
options.host.c_str(), options.port.c_str(), options.user.c_str());
if (sshd_main(sshbind, options) == CMD_EXIT_SERVER)
break;
}
ssh_bind_free(sshbind);
ssh_finalize();
return 0;
}