Actual SSH connection and terminal emulation (testing)

This commit is contained in:
Michael Jumper 2011-08-01 13:31:48 -07:00
parent 69dbead349
commit 4f7c8c98e7
2 changed files with 424 additions and 68 deletions

View File

@ -46,6 +46,7 @@ AC_PROG_LIBTOOL
# Checks for libraries. # Checks for libraries.
AC_CHECK_LIB([guac], [guac_get_client],, AC_MSG_ERROR("libguac is required for communication via the guacamole protocol")) AC_CHECK_LIB([guac], [guac_get_client],, AC_MSG_ERROR("libguac is required for communication via the guacamole protocol"))
AC_CHECK_LIB([cairo], [cairo_create],, AC_MSG_ERROR("cairo is required for drawing instructions")) AC_CHECK_LIB([cairo], [cairo_create],, AC_MSG_ERROR("cairo is required for drawing instructions"))
AC_CHECK_LIB([ssh], [ssh_new],, AC_MSG_ERROR("libssh is required"))
PKG_CHECK_MODULES([PANGO], pango); PKG_CHECK_MODULES([PANGO], pango);
PKG_CHECK_MODULES([PANGOCAIRO], pangocairo); PKG_CHECK_MODULES([PANGOCAIRO], pangocairo);

View File

@ -38,6 +38,8 @@
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <libssh/libssh.h>
#include <cairo/cairo.h> #include <cairo/cairo.h>
#include <pango/pangocairo.h> #include <pango/pangocairo.h>
@ -46,15 +48,26 @@
#include <guacamole/protocol.h> #include <guacamole/protocol.h>
#include <guacamole/client.h> #include <guacamole/client.h>
#define SSH_TERM_STATE_NULL 0
#define SSH_TERM_STATE_ECHO 1
#define SSH_TERM_STATE_ESC 2
#define SSH_TERM_STATE_CSI 3
#define SSH_TERM_STATE_OSC 4
#define SSH_TERM_STATE_CHARSET 5
/* Client plugin arguments */ /* Client plugin arguments */
const char* GUAC_CLIENT_ARGS[] = { const char* GUAC_CLIENT_ARGS[] = {
"hostname", "hostname",
"port", "user",
"password",
NULL NULL
}; };
typedef struct ssh_guac_client_data { typedef struct ssh_guac_client_data {
ssh_session session;
ssh_channel term_channel;
PangoFontDescription* font_desc; PangoFontDescription* font_desc;
guac_layer* glyphs[256]; guac_layer* glyphs[256];
@ -64,14 +77,22 @@ typedef struct ssh_guac_client_data {
int term_width; int term_width;
int term_height; int term_height;
int term_state;
int term_seq_argc;
int term_seq_argv[16];
char term_seq_argv_buffer[16];
int term_seq_argv_buffer_current;
int cursor_row; int cursor_row;
int cursor_col; int cursor_col;
} ssh_guac_client_data; } ssh_guac_client_data;
int ssh_guac_client_handle_messages(guac_client* client);
int ssh_guac_client_key_handler(guac_client* client, int keysym, int pressed);
int ssh_guac_client_send_glyph(guac_client* client, int row, int col, char c); int ssh_guac_client_send_glyph(guac_client* client, int row, int col, char c);
int ssh_guac_client_print(guac_client* client, const char* c); int ssh_guac_client_write(guac_client* client, const char* c, int size);
int guac_client_init(guac_client* client, int argc, char** argv) { int guac_client_init(guac_client* client, int argc, char** argv) {
@ -87,14 +108,15 @@ int guac_client_init(guac_client* client, int argc, char** argv) {
client_data->cursor_row = 0; client_data->cursor_row = 0;
client_data->cursor_col = 0; client_data->cursor_col = 0;
client_data->term_width = 80; client_data->term_width = 160;
client_data->term_height = 25; client_data->term_height = 50;
client_data->term_state = SSH_TERM_STATE_ECHO;
/* Get font */ /* Get font */
client_data->font_desc = pango_font_description_new(); client_data->font_desc = pango_font_description_new();
pango_font_description_set_family(client_data->font_desc, "monospace"); pango_font_description_set_family(client_data->font_desc, "monospace");
pango_font_description_set_weight(client_data->font_desc, PANGO_WEIGHT_NORMAL); pango_font_description_set_weight(client_data->font_desc, PANGO_WEIGHT_NORMAL);
pango_font_description_set_size(client_data->font_desc, 16*PANGO_SCALE); pango_font_description_set_size(client_data->font_desc, 8*PANGO_SCALE);
font_map = pango_cairo_font_map_get_default(); font_map = pango_cairo_font_map_get_default();
context = pango_font_map_create_context(font_map); context = pango_font_map_create_context(font_map);
@ -128,32 +150,73 @@ int guac_client_init(guac_client* client, int argc, char** argv) {
guac_flush(io); guac_flush(io);
ssh_guac_client_print(client, "Hello World!\r\nThis is a test of the new Guacamole SSH client plugin!!!\r\n"); /* Open SSH session */
guac_flush(io); client_data->session = ssh_new();
if (client_data->session == NULL) {
guac_send_error(io, "Unable to create SSH session.");
guac_flush(io);
return 1;
}
ssh_guac_client_print(client, "ROW 1\r\n"); guac_flush(io); /* Set session options */
ssh_guac_client_print(client, "ROW 2\r\n"); guac_flush(io); ssh_options_set(client_data->session, SSH_OPTIONS_HOST, argv[0]);
ssh_guac_client_print(client, "ROW 3\r\n"); guac_flush(io); ssh_options_set(client_data->session, SSH_OPTIONS_USER, argv[1]);
ssh_guac_client_print(client, "ROW 4\r\n"); guac_flush(io);
ssh_guac_client_print(client, "ROW 5\r\n"); guac_flush(io); /* Connect */
ssh_guac_client_print(client, "ROW 6\r\n"); guac_flush(io); if (ssh_connect(client_data->session) != SSH_OK) {
ssh_guac_client_print(client, "ROW 8\r\n"); guac_flush(io); guac_send_error(io, "Unable to connect via SSH.");
ssh_guac_client_print(client, "ROW 9\r\n"); guac_flush(io); guac_flush(io);
ssh_guac_client_print(client, "ROW 10\r\n"); guac_flush(io); return 1;
ssh_guac_client_print(client, "ROW 11\r\n"); guac_flush(io); }
ssh_guac_client_print(client, "ROW 12\r\n"); guac_flush(io);
ssh_guac_client_print(client, "ROW 13\r\n"); guac_flush(io); /* Authenticate */
ssh_guac_client_print(client, "ROW 14\r\n"); guac_flush(io); if (ssh_userauth_password(client_data->session, NULL, argv[2]) != SSH_AUTH_SUCCESS) {
ssh_guac_client_print(client, "ROW 15\r\n"); guac_flush(io); guac_send_error(io, "SSH auth failed.");
ssh_guac_client_print(client, "ROW 16\r\n"); guac_flush(io); guac_flush(io);
ssh_guac_client_print(client, "ROW 17\r\n"); guac_flush(io); return 1;
ssh_guac_client_print(client, "ROW 18\r\n"); guac_flush(io); }
ssh_guac_client_print(client, "ROW 19\r\n"); guac_flush(io);
ssh_guac_client_print(client, "ROW 20\r\n"); guac_flush(io); /* Open channel for terminal */
ssh_guac_client_print(client, "ROW 21\r\n"); guac_flush(io); client_data->term_channel = channel_new(client_data->session);
ssh_guac_client_print(client, "ROW 22\r\n"); guac_flush(io); if (client_data->term_channel == NULL) {
ssh_guac_client_print(client, "ROW 23\r\n"); guac_flush(io); guac_send_error(io, "Unable to open channel.");
ssh_guac_client_print(client, "ROW 24\r\n"); guac_flush(io); guac_flush(io);
return 1;
}
/* Open session for channel */
if (channel_open_session(client_data->term_channel) != SSH_OK) {
guac_send_error(io, "Unable to open channel session.");
guac_flush(io);
return 1;
}
/* Request PTY */
if (channel_request_pty(client_data->term_channel) != SSH_OK) {
guac_send_error(io, "Unable to allocate PTY for channel.");
guac_flush(io);
return 1;
}
/* Request PTY size */
if (channel_change_pty_size(client_data->term_channel, client_data->term_width, client_data->term_height) != SSH_OK) {
guac_send_error(io, "Unable to change PTY size.");
guac_flush(io);
return 1;
}
/* Request shell */
if (channel_request_shell(client_data->term_channel) != SSH_OK) {
guac_send_error(io, "Unable to associate shell with PTY.");
guac_flush(io);
return 1;
}
guac_log_info("SSH connection successful.");
/* Set handlers */
client->handle_messages = ssh_guac_client_handle_messages;
client->key_handler = ssh_guac_client_key_handler;
/* Success */ /* Success */
return 0; return 0;
@ -208,6 +271,34 @@ guac_layer* ssh_guac_client_get_glyph(guac_client* client, char c) {
} }
int ssh_guac_client_handle_messages(guac_client* client) {
GUACIO* io = client->io;
ssh_guac_client_data* client_data = (ssh_guac_client_data*) client->data;
char buffer[8192];
/* While data available, write to terminal */
int bytes_read = 0;
while (channel_is_open(client_data->term_channel)
&& !channel_is_eof(client_data->term_channel)
&& (bytes_read = channel_read_nonblocking(client_data->term_channel, buffer, sizeof(buffer), 0)) > 0) {
ssh_guac_client_write(client, buffer, bytes_read);
guac_flush(io);
}
/* Notify on error */
if (bytes_read < 0) {
guac_send_error(io, "Error reading data.");
guac_flush(io);
return 1;
}
return 0;
}
int ssh_guac_client_send_glyph(guac_client* client, int row, int col, char c) { int ssh_guac_client_send_glyph(guac_client* client, int row, int col, char c) {
GUACIO* io = client->io; GUACIO* io = client->io;
@ -216,71 +307,335 @@ int ssh_guac_client_send_glyph(guac_client* client, int row, int col, char c) {
return guac_send_copy(io, return guac_send_copy(io,
glyph, 0, 0, client_data->char_width, client_data->char_height, glyph, 0, 0, client_data->char_width, client_data->char_height,
GUAC_COMP_OVER, GUAC_DEFAULT_LAYER, GUAC_COMP_SRC, GUAC_DEFAULT_LAYER,
client_data->char_width * col, client_data->char_width * col,
client_data->char_height * row); client_data->char_height * row);
} }
int ssh_guac_client_print(guac_client* client, const char* c) { int ssh_guac_client_write(guac_client* client, const char* c, int size) {
GUACIO* io = client->io; GUACIO* io = client->io;
ssh_guac_client_data* client_data = (ssh_guac_client_data*) client->data; ssh_guac_client_data* client_data = (ssh_guac_client_data*) client->data;
while (*c != 0) { while (size > 0) {
switch (*c) { switch (client_data->term_state) {
/* Carriage return */ case SSH_TERM_STATE_NULL:
case '\r':
client_data->cursor_col = 0;
break; break;
/* Line feed */ case SSH_TERM_STATE_ECHO:
case '\n':
client_data->cursor_row++;
break;
/* Displayable chars */ /* Wrap if necessary */
default:
ssh_guac_client_send_glyph(client,
client_data->cursor_row,
client_data->cursor_col,
*c);
/* Advance cursor, wrap if necessary */
client_data->cursor_col++;
if (client_data->cursor_col >= client_data->term_width) { if (client_data->cursor_col >= client_data->term_width) {
client_data->cursor_col = 0; client_data->cursor_col = 0;
client_data->cursor_row++; client_data->cursor_row++;
} }
}
/* Scroll up if necessary */ /* Scroll up if necessary */
if (client_data->cursor_row >= client_data->term_height) { if (client_data->cursor_row >= client_data->term_height) {
client_data->cursor_row = client_data->term_height - 1; client_data->cursor_row = client_data->term_height - 1;
/* Copy screen up by one row */ /* Copy screen up by one row */
guac_send_copy(io, guac_send_copy(io,
GUAC_DEFAULT_LAYER, 0, client_data->char_height, GUAC_DEFAULT_LAYER, 0, client_data->char_height,
client_data->char_width * client_data->term_width, client_data->char_width * client_data->term_width,
client_data->char_height * (client_data->term_height - 1), client_data->char_height * (client_data->term_height - 1),
GUAC_COMP_SRC, GUAC_DEFAULT_LAYER, 0, 0); GUAC_COMP_SRC, GUAC_DEFAULT_LAYER, 0, 0);
/* Fill bottom row with background */ /* Fill bottom row with background */
guac_send_rect(io, guac_send_rect(io,
GUAC_COMP_SRC, GUAC_DEFAULT_LAYER, GUAC_COMP_SRC, GUAC_DEFAULT_LAYER,
0, client_data->char_height * (client_data->term_height - 1), 0, client_data->char_height * (client_data->term_height - 1),
client_data->char_width * client_data->term_width, client_data->char_width * client_data->term_width,
client_data->char_height * client_data->term_height, client_data->char_height * client_data->term_height,
0, 0, 0, 255); 0, 0, 0, 255);
}
switch (*c) {
/* Bell */
case 0x07:
break;
/* Backspace */
case 0x08:
if (client_data->cursor_col >= 1)
client_data->cursor_col--;
break;
/* Carriage return */
case '\r':
client_data->cursor_col = 0;
break;
/* Line feed */
case '\n':
client_data->cursor_row++;
break;
/* ESC */
case 0x1B:
client_data->term_state = SSH_TERM_STATE_ESC;
break;
/* Displayable chars */
default:
ssh_guac_client_send_glyph(client,
client_data->cursor_row,
client_data->cursor_col,
*c);
/* Advance cursor */
client_data->cursor_col++;
}
/* End of SSH_TERM_STATE_ECHO */
break;
case SSH_TERM_STATE_CHARSET:
client_data->term_state = SSH_TERM_STATE_ECHO;
break;
case SSH_TERM_STATE_ESC:
switch (*c) {
case '(':
client_data->term_state = SSH_TERM_STATE_CHARSET;
break;
case ']':
client_data->term_state = SSH_TERM_STATE_OSC;
client_data->term_seq_argc = 0;
client_data->term_seq_argv_buffer_current = 0;
break;
case '[':
client_data->term_state = SSH_TERM_STATE_CSI;
client_data->term_seq_argc = 0;
client_data->term_seq_argv_buffer_current = 0;
break;
default:
guac_log_info("Unhandled ESC sequence: %c", *c);
client_data->term_state = SSH_TERM_STATE_ECHO;
}
/* End of SSH_TERM_STATE_ESC */
break;
case SSH_TERM_STATE_OSC:
/* TODO: Implement OSC */
if (*c == 0x9C || *c == 0x5C || *c == 0x07) /* ECMA-48 ST (String Terminator */
client_data->term_state = SSH_TERM_STATE_ECHO;
/* End of SSH_TERM_STATE_OSC */
break;
case SSH_TERM_STATE_CSI:
/* FIXME: "The sequence of parameters may be preceded by a single question mark. */
if (*c == '?')
break; /* Ignore question marks for now... */
/* Digits get concatenated into argv */
if (*c >= '0' && *c <= '9') {
/* Concatenate digit if there is space in buffer */
if (client_data->term_seq_argv_buffer_current <
sizeof(client_data->term_seq_argv_buffer)) {
client_data->term_seq_argv_buffer[
client_data->term_seq_argv_buffer_current++
] = *c;
}
}
/* Any non-digit stops the parameter, and possibly the sequence */
else {
/* At most 16 parameters */
if (client_data->term_seq_argc < 16) {
/* Finish parameter */
client_data->term_seq_argv_buffer[client_data->term_seq_argv_buffer_current] = 0;
client_data->term_seq_argv[client_data->term_seq_argc++] =
atoi(client_data->term_seq_argv_buffer);
/* Prepare for next parameter */
client_data->term_seq_argv_buffer_current = 0;
}
/* Handle CSI functions */
switch (*c) {
/* H: Move cursor */
case 'H':
client_data->cursor_row = client_data->term_seq_argv[0] - 1;
client_data->cursor_col = client_data->term_seq_argv[1] - 1;
break;
/* J: Erase display */
case 'J':
/* Erase from cursor to end of display */
if (client_data->term_seq_argv[0] == 0) {
/* Until end of line */
guac_send_rect(io,
GUAC_COMP_SRC, GUAC_DEFAULT_LAYER,
client_data->cursor_col * client_data->char_width,
client_data->cursor_row * client_data->char_height,
(client_data->term_width - client_data->cursor_col) * client_data->char_width,
client_data->char_height,
0, 0, 0, 255); /* Background color */
/* Until end of display */
if (client_data->cursor_row < client_data->term_height - 1) {
guac_send_rect(io,
GUAC_COMP_SRC, GUAC_DEFAULT_LAYER,
0,
(client_data->cursor_row+1) * client_data->char_height,
client_data->term_width * client_data->char_width,
client_data->term_height * client_data->char_height,
0, 0, 0, 255); /* Background color */
}
}
/* Erase from start to cursor */
else if (client_data->term_seq_argv[0] == 1) {
/* Until start of line */
guac_send_rect(io,
GUAC_COMP_SRC, GUAC_DEFAULT_LAYER,
0,
client_data->cursor_row * client_data->char_height,
client_data->cursor_col * client_data->char_width,
client_data->char_height,
0, 0, 0, 255); /* Background color */
/* From start */
if (client_data->cursor_row >= 1) {
guac_send_rect(io,
GUAC_COMP_SRC, GUAC_DEFAULT_LAYER,
0,
0,
client_data->term_width * client_data->char_width,
(client_data->cursor_row-1) * client_data->char_height,
0, 0, 0, 255); /* Background color */
}
}
/* Entire screen */
else if (client_data->term_seq_argv[0] == 2) {
guac_send_rect(io,
GUAC_COMP_SRC, GUAC_DEFAULT_LAYER,
0,
0,
client_data->term_width * client_data->char_width,
client_data->term_height * client_data->char_height,
0, 0, 0, 255); /* Background color */
}
break;
/* K: Erase line */
case 'K':
/* Erase from cursor to end of line */
if (client_data->term_seq_argv[0] == 0) {
guac_send_rect(io,
GUAC_COMP_SRC, GUAC_DEFAULT_LAYER,
client_data->cursor_col * client_data->char_width,
client_data->cursor_row * client_data->char_height,
(client_data->term_width - client_data->cursor_col) * client_data->char_width,
client_data->char_height,
0, 0, 0, 255); /* Background color */
}
/* Erase from start to cursor */
else if (client_data->term_seq_argv[0] == 1) {
guac_send_rect(io,
GUAC_COMP_SRC, GUAC_DEFAULT_LAYER,
0,
client_data->cursor_row * client_data->char_height,
client_data->cursor_col * client_data->char_width,
client_data->char_height,
0, 0, 0, 255); /* Background color */
}
/* Erase line */
else if (client_data->term_seq_argv[0] == 2) {
guac_send_rect(io,
GUAC_COMP_SRC, GUAC_DEFAULT_LAYER,
0,
client_data->cursor_row * client_data->char_height,
client_data->term_width * client_data->char_width,
client_data->char_height,
0, 0, 0, 255); /* Background color */
}
break;
/* Warn of unhandled codes */
default:
if (*c != ';')
guac_log_info("Unhandled CSI sequence: %c", *c);
}
/* If not a semicolon, end of CSI sequence */
if (*c != ';')
client_data->term_state = SSH_TERM_STATE_ECHO;
}
/* End of SSH_TERM_STATE_CSI */
break;
} }
c++; c++;
size--;
} }
return 0; return 0;
} }
int ssh_guac_client_key_handler(guac_client* client, int keysym, int pressed) {
ssh_guac_client_data* client_data = (ssh_guac_client_data*) client->data;
/* If key pressed */
if (pressed) {
char data;
/* If simple ASCII key */
if (keysym >= 0x00 && keysym <= 0xFF)
data = (char) keysym;
else if (keysym == 0xFF08) data = 0x08;
else if (keysym == 0xFF09) data = 0x09;
else if (keysym == 0xFF0D) data = 0x0D;
else
return 0;
return channel_write(client_data->term_channel, &data, 1);
}
return 0;
}