Commit 72dea256 for guacamole.apache.org
commit 72dea2561cbbd857ca47082f26bec43f51ae13a0
Author: Ciro Iriarte <ciro.iriarte@gmail.com>
Date: Tue Jul 14 11:16:49 2026 -0300
GUACAMOLE-2301: Add serial console protocol (local device, ser2net raw TCP, RFC2217)
Adds a new "serial" terminal protocol plugin for browser access to serial
consoles (switch/router/PDU administration). Three transports select via the
serial-type/network-protocol parameters: a local tty device configured with
termios, raw TCP to a ser2net endpoint, and RFC2217 (telnet COM-PORT-OPTION,
compiled only when libtelnet is available via ENABLE_SERIAL_RFC2217).
Supports configurable send-break (break-duration) through a client-agnostic
serial-control pipe, paste pacing (paste-delay), and a device-path allowlist,
reusing the shared terminal for recording/typescript/clipboard/color-scheme,
mirroring the telnet/ipmi plugin structure.
diff --git a/Makefile.am b/Makefile.am
index 6ce3814a..97983363 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -37,6 +37,7 @@ DIST_SUBDIRS = \
src/pulse \
src/protocols/kubernetes \
src/protocols/rdp \
+ src/protocols/serial \
src/protocols/ssh \
src/protocols/telnet \
src/protocols/vnc
@@ -69,6 +70,10 @@ if ENABLE_SSH
SUBDIRS += src/protocols/ssh
endif
+if ENABLE_SERIAL
+SUBDIRS += src/protocols/serial
+endif
+
if ENABLE_TELNET
SUBDIRS += src/protocols/telnet
endif
diff --git a/configure.ac b/configure.ac
index 9bd4b8b2..22948941 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1386,6 +1386,19 @@ fi
AM_CONDITIONAL([ENABLE_TELNET], [test "x${have_libtelnet}" = "xyes" \
-a "x${have_terminal}" = "xyes"])
+# Serial console support. The core plugin (local devices and raw TCP) requires
+# only the terminal. The RFC2217 network transport additionally requires
+# libtelnet and is compiled in conditionally (see ENABLE_SERIAL_RFC2217).
+AM_CONDITIONAL([ENABLE_SERIAL], [test "x${have_terminal}" = "xyes"])
+
+# RFC2217 (COM-PORT-OPTION) serial transport, built on libtelnet. When
+# libtelnet is unavailable the serial plugin still builds; only the RFC2217
+# network transport is omitted.
+AM_CONDITIONAL([ENABLE_SERIAL_RFC2217], [test "x${have_libtelnet}" = "xyes"])
+AS_IF([test "x${have_libtelnet}" = "xyes"],
+ [AC_DEFINE([ENABLE_SERIAL_RFC2217], [1],
+ [Define if RFC2217 serial transport (libtelnet) is available])])
+
AC_SUBST(TELNET_LIBS)
#
@@ -1571,6 +1584,7 @@ AC_CONFIG_FILES([Makefile
src/protocols/kubernetes/tests/Makefile
src/protocols/rdp/Makefile
src/protocols/rdp/tests/Makefile
+ src/protocols/serial/Makefile
src/protocols/ssh/Makefile
src/protocols/telnet/Makefile
src/protocols/vnc/Makefile])
@@ -1583,6 +1597,7 @@ AC_OUTPUT
AM_COND_IF([ENABLE_KUBERNETES], [build_kubernetes=yes], [build_kubernetes=no])
AM_COND_IF([ENABLE_RDP], [build_rdp=yes], [build_rdp=no])
AM_COND_IF([ENABLE_SSH], [build_ssh=yes], [build_ssh=no])
+AM_COND_IF([ENABLE_SERIAL], [build_serial=yes], [build_serial=no])
AM_COND_IF([ENABLE_TELNET], [build_telnet=yes], [build_telnet=no])
AM_COND_IF([ENABLE_VNC], [build_vnc=yes], [build_vnc=no])
@@ -1645,6 +1660,8 @@ $PACKAGE_NAME version $PACKAGE_VERSION
Kubernetes .... ${build_kubernetes}
RDP ........... ${build_rdp}
SSH ........... ${build_ssh}
+ Serial ........ ${build_serial}
+ RFC2217 (serial) .... ${have_libtelnet}
Telnet ........ ${build_telnet}
VNC ........... ${build_vnc}
diff --git a/src/protocols/serial/Makefile.am b/src/protocols/serial/Makefile.am
new file mode 100644
index 00000000..f9cbcc46
--- /dev/null
+++ b/src/protocols/serial/Makefile.am
@@ -0,0 +1,72 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+# NOTE: Parts of this file (Makefile.am) are automatically transcluded verbatim
+# into Makefile.in. Though the build system (GNU Autotools) automatically adds
+# its own license boilerplate to the generated Makefile.in, that boilerplate
+# does not apply to the transcluded portions of Makefile.am which are licensed
+# to you by the ASF under the Apache License, Version 2.0, as described above.
+#
+
+AUTOMAKE_OPTIONS = foreign
+ACLOCAL_AMFLAGS = -I m4
+
+lib_LTLIBRARIES = libguac-client-serial.la
+
+libguac_client_serial_la_SOURCES = \
+ argv.c \
+ client.c \
+ clipboard.c \
+ input.c \
+ local.c \
+ pipe.c \
+ rfc2217.c \
+ serial.c \
+ settings.c \
+ stream.c \
+ tcp.c \
+ user.c
+
+noinst_HEADERS = \
+ argv.h \
+ client.h \
+ clipboard.h \
+ input.h \
+ local.h \
+ pipe.h \
+ rfc2217.h \
+ serial.h \
+ settings.h \
+ stream.h \
+ tcp.h \
+ user.h
+
+libguac_client_serial_la_CFLAGS = \
+ -Werror -Wall -Iinclude \
+ @LIBGUAC_INCLUDE@ \
+ @TERMINAL_INCLUDE@
+
+libguac_client_serial_la_LIBADD = \
+ @COMMON_LTLIB@ \
+ @LIBGUAC_LTLIB@ \
+ @TERMINAL_LTLIB@
+
+libguac_client_serial_la_LDFLAGS = \
+ -version-info 0:0:0 \
+ @PTHREAD_LIBS@ \
+ @TELNET_LIBS@
diff --git a/src/protocols/serial/argv.c b/src/protocols/serial/argv.c
new file mode 100644
index 00000000..94a101bf
--- /dev/null
+++ b/src/protocols/serial/argv.c
@@ -0,0 +1,91 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#include "config.h"
+#include "argv.h"
+#include "serial.h"
+#include "terminal/terminal.h"
+
+#include <guacamole/protocol.h>
+#include <guacamole/socket.h>
+#include <guacamole/user.h>
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+int guac_serial_argv_callback(guac_user* user, const char* mimetype,
+ const char* name, const char* value, void* data) {
+
+ guac_client* client = user->client;
+ guac_serial_client* serial_client = (guac_serial_client*) client->data;
+ guac_terminal* terminal = serial_client->term;
+
+ /* Update color scheme */
+ if (strcmp(name, GUAC_SERIAL_ARGV_COLOR_SCHEME) == 0)
+ guac_terminal_apply_color_scheme(terminal, value);
+
+ /* Update font name */
+ else if (strcmp(name, GUAC_SERIAL_ARGV_FONT_NAME) == 0)
+ guac_terminal_apply_font(terminal, value, -1, 0);
+
+ /* Update only if font size is sane */
+ else if (strcmp(name, GUAC_SERIAL_ARGV_FONT_SIZE) == 0) {
+ int size = atoi(value);
+ if (size > 0)
+ guac_terminal_apply_font(terminal, NULL, size,
+ serial_client->settings->resolution);
+ }
+
+ return 0;
+
+}
+
+void* guac_serial_send_current_argv(guac_user* user, void* data) {
+
+ /* Defer to the batch handler, using the user's socket to send the data */
+ guac_serial_send_current_argv_batch(user->client, user->socket);
+
+ return NULL;
+
+}
+
+void guac_serial_send_current_argv_batch(
+ guac_client* client, guac_socket* socket) {
+
+ guac_serial_client* serial_client = (guac_serial_client*) client->data;
+ guac_terminal* terminal = serial_client->term;
+
+ /* Send current color scheme */
+ guac_client_stream_argv(client, socket, "text/plain",
+ GUAC_SERIAL_ARGV_COLOR_SCHEME,
+ guac_terminal_get_color_scheme(terminal));
+
+ /* Send current font name */
+ guac_client_stream_argv(client, socket, "text/plain",
+ GUAC_SERIAL_ARGV_FONT_NAME,
+ guac_terminal_get_font_name(terminal));
+
+ /* Send current font size */
+ char font_size[64];
+ sprintf(font_size, "%i", guac_terminal_get_font_size(terminal));
+ guac_client_stream_argv(client, socket, "text/plain",
+ GUAC_SERIAL_ARGV_FONT_SIZE, font_size);
+
+}
diff --git a/src/protocols/serial/argv.h b/src/protocols/serial/argv.h
new file mode 100644
index 00000000..80af66bb
--- /dev/null
+++ b/src/protocols/serial/argv.h
@@ -0,0 +1,89 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#ifndef GUAC_SERIAL_ARGV_H
+#define GUAC_SERIAL_ARGV_H
+
+#include <guacamole/argv.h>
+#include <guacamole/user.h>
+
+/**
+ * The name of the parameter that specifies/updates the color scheme used by
+ * the terminal emulator.
+ */
+#define GUAC_SERIAL_ARGV_COLOR_SCHEME "color-scheme"
+
+/**
+ * The name of the parameter that specifies/updates the name of the font used
+ * by the terminal emulator.
+ */
+#define GUAC_SERIAL_ARGV_FONT_NAME "font-name"
+
+/**
+ * The name of the parameter that specifies/updates the font size used by the
+ * terminal emulator.
+ */
+#define GUAC_SERIAL_ARGV_FONT_SIZE "font-size"
+
+/**
+ * Handles a received argument value from a Guacamole "argv" instruction,
+ * updating the given connection parameter.
+ */
+guac_argv_callback guac_serial_argv_callback;
+
+/**
+ * Sends the current values of all non-sensitive parameters which may be set
+ * while the connection is running to the given user. Note that the user
+ * receiving these values will not necessarily be able to set new values
+ * themselves if their connection is read-only. This function can be used as
+ * the callback for guac_client_foreach_user() and guac_client_for_owner()
+ *
+ * @param user
+ * The user that should receive the values of all non-sensitive parameters
+ * which may be set while the connection is running.
+ *
+ * @param data
+ * The guac_serial_client instance associated with the current connection.
+ *
+ * @return
+ * Always NULL.
+ */
+void* guac_serial_send_current_argv(guac_user* user, void* data);
+
+/**
+ * Sends the current values of all non-sensitive parameters which may be set
+ * while the connection is running to the users associated with the provided
+ * socket. Note that the users receiving these values will not necessarily be
+ * able to set new values themselves if their connection is read-only.
+ *
+ * @param client
+ * The client associated with the users that should receive the values of
+ * all non-sensitive parameters which may be set while the connection is
+ * running.
+ *
+ * @param socket
+ * The socket to send the arguments to the batch of users along.
+ *
+ * @return
+ * Always NULL.
+ */
+void guac_serial_send_current_argv_batch(
+ guac_client* client, guac_socket* socket);
+
+#endif
diff --git a/src/protocols/serial/client.c b/src/protocols/serial/client.c
new file mode 100644
index 00000000..4969bb89
--- /dev/null
+++ b/src/protocols/serial/client.c
@@ -0,0 +1,146 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#include "config.h"
+#include "argv.h"
+#include "client.h"
+#include "serial.h"
+#include "settings.h"
+#include "stream.h"
+#include "user.h"
+
+#include <langinfo.h>
+#include <locale.h>
+#include <pthread.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include <guacamole/argv.h>
+#include <guacamole/client.h>
+#include <guacamole/mem.h>
+#include <guacamole/recording.h>
+#include <guacamole/socket.h>
+
+/**
+ * A pending join handler implementation that will synchronize the connection
+ * state for all pending users prior to them being promoted to full user.
+ *
+ * @param client
+ * The client whose pending users are about to be promoted.
+ *
+ * @return
+ * Always zero.
+ */
+static int guac_serial_join_pending_handler(guac_client* client) {
+
+ guac_serial_client* serial_client = (guac_serial_client*) client->data;
+
+ /* Synchronize the terminal state to all pending users */
+ if (serial_client->term != NULL) {
+ guac_socket* broadcast_socket = client->pending_socket;
+ guac_terminal_sync_users(serial_client->term, client, broadcast_socket);
+ guac_serial_send_current_argv_batch(client, broadcast_socket);
+ guac_socket_flush(broadcast_socket);
+ }
+
+ return 0;
+
+}
+
+int guac_client_init(guac_client* client) {
+
+ /* Set client args */
+ client->args = GUAC_SERIAL_CLIENT_ARGS;
+
+ /* Allocate client instance data */
+ guac_serial_client* serial_client = guac_mem_zalloc(sizeof(guac_serial_client));
+ client->data = serial_client;
+
+ /* Set handlers */
+ client->join_handler = guac_serial_user_join_handler;
+ client->join_pending_handler = guac_serial_join_pending_handler;
+ client->free_handler = guac_serial_client_free_handler;
+ client->leave_handler = guac_serial_user_leave_handler;
+
+ /* Register handlers for argument values that may be sent after the handshake */
+ guac_argv_register(GUAC_SERIAL_ARGV_COLOR_SCHEME, guac_serial_argv_callback, NULL, GUAC_ARGV_OPTION_ECHO);
+ guac_argv_register(GUAC_SERIAL_ARGV_FONT_NAME, guac_serial_argv_callback, NULL, GUAC_ARGV_OPTION_ECHO);
+ guac_argv_register(GUAC_SERIAL_ARGV_FONT_SIZE, guac_serial_argv_callback, NULL, GUAC_ARGV_OPTION_ECHO);
+
+ /* Set locale and warn if not UTF-8 */
+ setlocale(LC_CTYPE, "");
+ if (strcmp(nl_langinfo(CODESET), "UTF-8") != 0) {
+ guac_client_log(client, GUAC_LOG_INFO,
+ "Current locale does not use UTF-8. Some characters may "
+ "not render correctly.");
+ }
+
+ /* Success */
+ return 0;
+
+}
+
+int guac_serial_client_free_handler(guac_client* client) {
+
+ guac_serial_client* serial_client = (guac_serial_client*) client->data;
+
+ /* Close the serial stream's file descriptor, unblocking the client
+ * thread's read loop. The descriptor is cleared so it is not closed a
+ * second time when the stream is later freed. */
+ if (serial_client->stream != NULL && serial_client->stream->fd != -1) {
+ close(serial_client->stream->fd);
+ serial_client->stream->fd = -1;
+ }
+
+ /* Stop the terminal — closing its input pipe to unblock the input thread —
+ * WITHOUT freeing it yet. The client thread may still be writing serial
+ * output to the terminal until it has fully exited, so the terminal must
+ * outlive the join below to avoid a use-after-free. */
+ if (serial_client->term != NULL)
+ guac_terminal_stop(serial_client->term);
+
+ /* Wait for the client thread to fully exit before touching anything it
+ * uses. Joined only when it was successfully created, so early-failure
+ * setup paths neither leak the joinable thread nor join an invalid one. */
+ if (serial_client->client_thread_valid) {
+ pthread_join(serial_client->client_thread, NULL);
+ serial_client->client_thread_valid = 0;
+ }
+
+ /* Clean up recording, if in progress */
+ if (serial_client->recording != NULL)
+ guac_recording_free(serial_client->recording);
+
+ /* With no thread left to touch it, the terminal can now be freed safely */
+ if (serial_client->term != NULL)
+ guac_terminal_free(serial_client->term);
+
+ /* Free the serial stream (frees any libtelnet session) */
+ if (serial_client->stream != NULL)
+ guac_serial_stream_close(serial_client->stream);
+
+ /* Free settings */
+ if (serial_client->settings != NULL)
+ guac_serial_settings_free(serial_client->settings);
+
+ guac_mem_free(serial_client);
+ return 0;
+
+}
diff --git a/src/protocols/serial/client.h b/src/protocols/serial/client.h
new file mode 100644
index 00000000..b31ac89b
--- /dev/null
+++ b/src/protocols/serial/client.h
@@ -0,0 +1,34 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#ifndef GUAC_SERIAL_CLIENT_H
+#define GUAC_SERIAL_CLIENT_H
+
+#include "terminal/terminal.h"
+
+#include <pthread.h>
+#include <sys/types.h>
+
+/**
+ * Free handler. Required by libguac and called when the guac_client is
+ * disconnected and must be cleaned up.
+ */
+guac_client_free_handler guac_serial_client_free_handler;
+
+#endif
diff --git a/src/protocols/serial/clipboard.c b/src/protocols/serial/clipboard.c
new file mode 100644
index 00000000..53a59456
--- /dev/null
+++ b/src/protocols/serial/clipboard.c
@@ -0,0 +1,78 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#include "config.h"
+#include "clipboard.h"
+#include "serial.h"
+#include "terminal/terminal.h"
+
+#include <guacamole/client.h>
+#include <guacamole/stream.h>
+#include <guacamole/user.h>
+
+int guac_serial_clipboard_handler(guac_user* user, guac_stream* stream,
+ char* mimetype) {
+
+ /* Clear clipboard and prepare for new data */
+ guac_client* client = user->client;
+ guac_serial_client* serial_client = (guac_serial_client*) client->data;
+ guac_terminal_clipboard_reset(serial_client->term, mimetype);
+
+ /* Set handlers for clipboard stream */
+ stream->blob_handler = guac_serial_clipboard_blob_handler;
+ stream->end_handler = guac_serial_clipboard_end_handler;
+
+ /* Report clipboard within recording */
+ if (serial_client->recording != NULL)
+ guac_recording_report_clipboard_begin(serial_client->recording, stream,
+ mimetype);
+
+ return 0;
+}
+
+int guac_serial_clipboard_blob_handler(guac_user* user, guac_stream* stream,
+ void* data, int length) {
+
+ guac_client* client = user->client;
+ guac_serial_client* serial_client = (guac_serial_client*) client->data;
+
+ /* Report clipboard blob within recording */
+ if (serial_client->recording != NULL)
+ guac_recording_report_clipboard_blob(serial_client->recording, stream,
+ data, length);
+
+ /* Append new data */
+ guac_terminal_clipboard_append(serial_client->term, data, length);
+
+ return 0;
+}
+
+int guac_serial_clipboard_end_handler(guac_user* user, guac_stream* stream) {
+
+ guac_client* client = user->client;
+ guac_serial_client* serial_client = (guac_serial_client*) client->data;
+
+ /* Report clipboard stream end within recording */
+ if (serial_client->recording != NULL)
+ guac_recording_report_clipboard_end(serial_client->recording, stream);
+
+ /* Nothing to do - clipboard is implemented within client */
+
+ return 0;
+}
diff --git a/src/protocols/serial/clipboard.h b/src/protocols/serial/clipboard.h
new file mode 100644
index 00000000..63520cae
--- /dev/null
+++ b/src/protocols/serial/clipboard.h
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#ifndef GUAC_SERIAL_CLIPBOARD_H
+#define GUAC_SERIAL_CLIPBOARD_H
+
+#include <guacamole/user.h>
+
+/**
+ * Handler for inbound clipboard streams.
+ */
+guac_user_clipboard_handler guac_serial_clipboard_handler;
+
+/**
+ * Handler for data received along clipboard streams.
+ */
+guac_user_blob_handler guac_serial_clipboard_blob_handler;
+
+/**
+ * Handler for end-of-stream related to clipboard.
+ */
+guac_user_end_handler guac_serial_clipboard_end_handler;
+
+#endif
diff --git a/src/protocols/serial/input.c b/src/protocols/serial/input.c
new file mode 100644
index 00000000..1147e242
--- /dev/null
+++ b/src/protocols/serial/input.c
@@ -0,0 +1,108 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#include "config.h"
+#include "input.h"
+#include "serial.h"
+#include "stream.h"
+#include "terminal/terminal.h"
+
+#include <guacamole/client.h>
+#include <guacamole/recording.h>
+#include <guacamole/user.h>
+
+#include <stdlib.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+int guac_serial_user_mouse_handler(guac_user* user, int x, int y, int mask) {
+
+ guac_client* client = user->client;
+ guac_serial_client* serial_client = (guac_serial_client*) client->data;
+ guac_terminal* term = serial_client->term;
+
+ /* Skip if terminal not yet ready */
+ if (term == NULL)
+ return 0;
+
+ /* Report mouse position within recording */
+ if (serial_client->recording != NULL)
+ guac_recording_report_mouse(serial_client->recording, x, y, mask);
+
+ /* Send mouse event to terminal */
+ guac_terminal_send_mouse(term, user, x, y, mask);
+
+ return 0;
+
+}
+
+int guac_serial_user_key_handler(guac_user* user, int keysym, int pressed) {
+
+ guac_client* client = user->client;
+ guac_serial_client* serial_client = (guac_serial_client*) client->data;
+ guac_terminal* term = serial_client->term;
+
+ /* Report key state within recording */
+ if (serial_client->recording != NULL)
+ guac_recording_report_key(serial_client->recording, keysym, pressed);
+
+ /* Skip if terminal not yet ready */
+ if (term == NULL)
+ return 0;
+
+ /* Intercept and handle Pause / Break / Ctrl+0 as a serial break */
+ if (pressed && (
+ keysym == 0xFF13 /* Pause */
+ || keysym == 0xFF6B /* Break */
+ || (
+ guac_terminal_get_mod_ctrl(term)
+ && keysym == '0'
+ ) /* Ctrl + 0 */
+ )) {
+
+ /* Send a serial break if the connection is established */
+ if (serial_client->stream != NULL)
+ guac_serial_stream_send_break(serial_client->stream);
+
+ return 0;
+ }
+
+ /* Send key */
+ guac_terminal_send_key(term, keysym, pressed);
+
+ return 0;
+
+}
+
+int guac_serial_user_size_handler(guac_user* user, int width, int height) {
+
+ /* Get terminal */
+ guac_client* client = user->client;
+ guac_serial_client* serial_client = (guac_serial_client*) client->data;
+ guac_terminal* terminal = serial_client->term;
+
+ /* Skip if terminal not yet ready */
+ if (terminal == NULL)
+ return 0;
+
+ /* Resize terminal */
+ guac_terminal_resize(terminal, width, height);
+
+ return 0;
+}
diff --git a/src/protocols/serial/input.h b/src/protocols/serial/input.h
new file mode 100644
index 00000000..8237ac79
--- /dev/null
+++ b/src/protocols/serial/input.h
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#ifndef GUAC_SERIAL_INPUT_H
+#define GUAC_SERIAL_INPUT_H
+
+#include <guacamole/user.h>
+
+/**
+ * Handler for key events. Required by libguac and called whenever key events
+ * are received.
+ */
+guac_user_key_handler guac_serial_user_key_handler;
+
+/**
+ * Handler for mouse events. Required by libguac and called whenever mouse
+ * events are received.
+ */
+guac_user_mouse_handler guac_serial_user_mouse_handler;
+
+/**
+ * Handler for size events. Required by libguac and called whenever the remote
+ * display (window) is resized.
+ */
+guac_user_size_handler guac_serial_user_size_handler;
+
+#endif
diff --git a/src/protocols/serial/local.c b/src/protocols/serial/local.c
new file mode 100644
index 00000000..d4b01a9b
--- /dev/null
+++ b/src/protocols/serial/local.c
@@ -0,0 +1,180 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#include "config.h"
+
+#include "local.h"
+#include "settings.h"
+#include "stream.h"
+
+#include <guacamole/client.h>
+#include <guacamole/protocol.h>
+
+#include <errno.h>
+#include <fcntl.h>
+#include <string.h>
+#include <sys/ioctl.h>
+#include <termios.h>
+#include <unistd.h>
+
+int guac_serial_local_open(guac_serial_stream* stream, guac_client* client,
+ guac_serial_settings* settings) {
+
+ /* Open device non-blocking so open() does not wait for carrier detect */
+ int fd = open(settings->device, O_RDWR | O_NOCTTY | O_NONBLOCK);
+ if (fd < 0) {
+ int status = (errno == ENOENT)
+ ? GUAC_PROTOCOL_STATUS_UPSTREAM_NOT_FOUND
+ : GUAC_PROTOCOL_STATUS_SERVER_ERROR;
+ guac_client_abort(client, status,
+ "Unable to open serial device \"%s\": %s",
+ settings->device, strerror(errno));
+ return -1;
+ }
+
+ /* Request exclusive access; warn but continue if unsupported */
+ if (ioctl(fd, TIOCEXCL) != 0)
+ guac_client_log(client, GUAC_LOG_WARNING, "Unable to obtain exclusive "
+ "access to serial device \"%s\": %s", settings->device,
+ strerror(errno));
+
+ /* Read current line settings */
+ struct termios tio;
+ if (tcgetattr(fd, &tio) != 0) {
+ guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR,
+ "Unable to read attributes of serial device \"%s\": %s",
+ settings->device, strerror(errno));
+ close(fd);
+ return -1;
+ }
+
+ /* Start from a fully raw configuration */
+ cfmakeraw(&tio);
+
+ /* Apply line speed */
+ speed_t speed = guac_serial_baud_to_speed(settings->baud_rate);
+ cfsetispeed(&tio, speed);
+ cfsetospeed(&tio, speed);
+
+ /* Ignore modem control lines and enable the receiver */
+ tio.c_cflag |= (CLOCAL | CREAD);
+
+ /* Apply data bits */
+ tio.c_cflag &= ~CSIZE;
+ switch (settings->data_bits) {
+ case 5: tio.c_cflag |= CS5; break;
+ case 6: tio.c_cflag |= CS6; break;
+ case 7: tio.c_cflag |= CS7; break;
+ default: tio.c_cflag |= CS8; break;
+ }
+
+ /* Apply parity */
+ switch (settings->parity) {
+
+ case GUAC_SERIAL_PARITY_NONE:
+ tio.c_cflag &= ~PARENB;
+ break;
+
+ case GUAC_SERIAL_PARITY_EVEN:
+ tio.c_cflag |= PARENB;
+ tio.c_cflag &= ~PARODD;
+ break;
+
+ case GUAC_SERIAL_PARITY_ODD:
+ tio.c_cflag |= (PARENB | PARODD);
+ break;
+
+ case GUAC_SERIAL_PARITY_MARK:
+#ifdef CMSPAR
+ tio.c_cflag |= (PARENB | CMSPAR | PARODD);
+#else
+ guac_client_log(client, GUAC_LOG_WARNING, "Mark parity is not "
+ "supported on this platform; using no parity.");
+ tio.c_cflag &= ~PARENB;
+#endif
+ break;
+
+ case GUAC_SERIAL_PARITY_SPACE:
+#ifdef CMSPAR
+ tio.c_cflag |= (PARENB | CMSPAR);
+ tio.c_cflag &= ~PARODD;
+#else
+ guac_client_log(client, GUAC_LOG_WARNING, "Space parity is not "
+ "supported on this platform; using no parity.");
+ tio.c_cflag &= ~PARENB;
+#endif
+ break;
+
+ }
+
+ /* Apply stop bits */
+ if (settings->stop_bits == 2)
+ tio.c_cflag |= CSTOPB;
+ else
+ tio.c_cflag &= ~CSTOPB;
+
+ /* Apply flow control */
+ switch (settings->flow_control) {
+
+ case GUAC_SERIAL_FLOW_NONE:
+ tio.c_iflag &= ~(IXON | IXOFF | IXANY);
+#ifdef CRTSCTS
+ tio.c_cflag &= ~CRTSCTS;
+#endif
+ break;
+
+ case GUAC_SERIAL_FLOW_RTS_CTS:
+ tio.c_iflag &= ~(IXON | IXOFF | IXANY);
+#ifdef CRTSCTS
+ tio.c_cflag |= CRTSCTS;
+#else
+ guac_client_log(client, GUAC_LOG_WARNING, "Hardware (RTS/CTS) flow "
+ "control is not supported on this platform; using no flow "
+ "control.");
+#endif
+ break;
+
+ case GUAC_SERIAL_FLOW_XON_XOFF:
+#ifdef CRTSCTS
+ tio.c_cflag &= ~CRTSCTS;
+#endif
+ tio.c_iflag |= (IXON | IXOFF);
+ break;
+
+ }
+
+ /* Block for at least one byte per read, with no inter-byte timeout */
+ tio.c_cc[VMIN] = 1;
+ tio.c_cc[VTIME] = 0;
+
+ /* Apply configuration immediately and discard any pending I/O */
+ if (tcsetattr(fd, TCSANOW, &tio) != 0) {
+ guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR,
+ "Unable to configure serial device \"%s\": %s",
+ settings->device, strerror(errno));
+ close(fd);
+ return -1;
+ }
+ tcflush(fd, TCIOFLUSH);
+
+ stream->backend = GUAC_SERIAL_BACKEND_LOCAL;
+ stream->fd = fd;
+ return 0;
+
+}
diff --git a/src/protocols/serial/local.h b/src/protocols/serial/local.h
new file mode 100644
index 00000000..40eccc7a
--- /dev/null
+++ b/src/protocols/serial/local.h
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#ifndef GUAC_SERIAL_LOCAL_H
+#define GUAC_SERIAL_LOCAL_H
+
+#include "settings.h"
+#include "stream.h"
+
+#include <guacamole/client.h>
+
+/**
+ * Opens the local serial device described by the given settings, configuring
+ * its termios line discipline (baud rate, data/stop bits, parity, and flow
+ * control) for raw I/O, and stores the resulting file descriptor in the given
+ * stream. On failure, the client is aborted.
+ *
+ * @param stream
+ * The serial stream to populate with the opened device.
+ *
+ * @param client
+ * The client for which the device is being opened.
+ *
+ * @param settings
+ * The parsed connection settings describing the device to open.
+ *
+ * @return
+ * Zero on success, non-zero on failure.
+ */
+int guac_serial_local_open(guac_serial_stream* stream, guac_client* client,
+ guac_serial_settings* settings);
+
+#endif
diff --git a/src/protocols/serial/pipe.c b/src/protocols/serial/pipe.c
new file mode 100644
index 00000000..8d8171d7
--- /dev/null
+++ b/src/protocols/serial/pipe.c
@@ -0,0 +1,197 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#include "config.h"
+#include "pipe.h"
+#include "serial.h"
+#include "stream.h"
+#include "terminal/terminal.h"
+
+#include <guacamole/mem.h>
+#include <guacamole/protocol.h>
+#include <guacamole/socket.h>
+#include <guacamole/user.h>
+
+#include <ctype.h>
+#include <string.h>
+
+/**
+ * The maximum length, in bytes, of a single control command token.
+ */
+#define GUAC_SERIAL_CONTROL_TOKEN_LENGTH 64
+
+/**
+ * Per-inbound-stream reassembly state for the control pipe. Control commands
+ * are whitespace-delimited plain-text tokens which may span multiple blobs.
+ */
+typedef struct guac_serial_control_stream {
+
+ /**
+ * Buffer accumulating the current (possibly partial) command token.
+ */
+ char token[GUAC_SERIAL_CONTROL_TOKEN_LENGTH];
+
+ /**
+ * The number of bytes currently held in the token buffer.
+ */
+ int length;
+
+} guac_serial_control_stream;
+
+/**
+ * Executes a single, complete control command token.
+ *
+ * @param user
+ * The user which sent the command.
+ *
+ * @param token
+ * The null-terminated command token.
+ */
+static void guac_serial_control_dispatch(guac_user* user, const char* token) {
+
+ guac_client* client = user->client;
+ guac_serial_client* serial_client = (guac_serial_client*) client->data;
+
+ /* Send a serial break */
+ if (strcmp(token, "break") == 0) {
+ if (serial_client->stream != NULL)
+ guac_serial_stream_send_break(serial_client->stream);
+ }
+
+ /* Ignore unrecognized commands */
+ else
+ guac_client_log(client, GUAC_LOG_DEBUG, "Ignoring unknown serial "
+ "control command: \"%s\".", token);
+
+}
+
+/**
+ * Blob handler for the inbound control pipe: reassembles whitespace-delimited
+ * plain-text tokens and dispatches each complete token.
+ */
+static int guac_serial_control_blob_handler(guac_user* user,
+ guac_stream* stream, void* data, int length) {
+
+ guac_serial_control_stream* cs =
+ (guac_serial_control_stream*) stream->data;
+ const char* in = (const char*) data;
+
+ for (int i = 0; i < length; i++) {
+
+ char c = in[i];
+
+ /* A whitespace character completes the current token */
+ if (isspace((unsigned char) c)) {
+ if (cs->length > 0) {
+ cs->token[cs->length] = '\0';
+ guac_serial_control_dispatch(user, cs->token);
+ cs->length = 0;
+ }
+ }
+
+ /* Accumulate non-whitespace characters into the current token */
+ else if (cs->length < GUAC_SERIAL_CONTROL_TOKEN_LENGTH - 1)
+ cs->token[cs->length++] = c;
+
+ /* Oversized token; drop it to resynchronize on the next whitespace */
+ else
+ cs->length = 0;
+
+ }
+
+ guac_protocol_send_ack(user->socket, stream, "OK",
+ GUAC_PROTOCOL_STATUS_SUCCESS);
+ guac_socket_flush(user->socket);
+ return 0;
+
+}
+
+/**
+ * End handler for the inbound control pipe. Dispatches any final unterminated
+ * token and releases the reassembly state.
+ */
+static int guac_serial_control_end_handler(guac_user* user,
+ guac_stream* stream) {
+
+ guac_serial_control_stream* cs =
+ (guac_serial_control_stream*) stream->data;
+
+ /* Dispatch any trailing token not followed by whitespace */
+ if (cs != NULL && cs->length > 0) {
+ cs->token[cs->length] = '\0';
+ guac_serial_control_dispatch(user, cs->token);
+ }
+
+ guac_mem_free(stream->data);
+ stream->data = NULL;
+ return 0;
+
+}
+
+/**
+ * Sets up an inbound "serial-control" pipe stream just opened by the given
+ * user, installing the blob/end handlers that parse and dispatch control
+ * commands.
+ *
+ * @param user
+ * The user which opened the control pipe.
+ *
+ * @param stream
+ * The inbound pipe stream.
+ */
+static void guac_serial_control_open(guac_user* user, guac_stream* stream) {
+
+ guac_serial_control_stream* cs =
+ guac_mem_zalloc(sizeof(guac_serial_control_stream));
+
+ stream->data = cs;
+ stream->blob_handler = guac_serial_control_blob_handler;
+ stream->end_handler = guac_serial_control_end_handler;
+
+ guac_protocol_send_ack(user->socket, stream, "OK",
+ GUAC_PROTOCOL_STATUS_SUCCESS);
+ guac_socket_flush(user->socket);
+
+}
+
+int guac_serial_pipe_handler(guac_user* user, guac_stream* stream,
+ char* mimetype, char* name) {
+
+ guac_client* client = user->client;
+ guac_serial_client* serial_client = (guac_serial_client*) client->data;
+
+ /* Redirect STDIN if pipe has required name */
+ if (strcmp(name, GUAC_SERIAL_STDIN_PIPE_NAME) == 0) {
+ guac_terminal_send_stream(serial_client->term, user, stream);
+ return 0;
+ }
+
+ /* Out-of-band control channel */
+ if (strcmp(name, GUAC_SERIAL_CONTROL_PIPE_NAME) == 0) {
+ guac_serial_control_open(user, stream);
+ return 0;
+ }
+
+ /* No other inbound pipe streams are supported */
+ guac_protocol_send_ack(user->socket, stream, "No such input stream.",
+ GUAC_PROTOCOL_STATUS_RESOURCE_NOT_FOUND);
+ guac_socket_flush(user->socket);
+ return 0;
+
+}
diff --git a/src/protocols/serial/pipe.h b/src/protocols/serial/pipe.h
new file mode 100644
index 00000000..c6f02cbe
--- /dev/null
+++ b/src/protocols/serial/pipe.h
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#ifndef GUAC_SERIAL_PIPE_H
+#define GUAC_SERIAL_PIPE_H
+
+#include <guacamole/user.h>
+
+/**
+ * The name reserved for the inbound pipe stream which forces the terminal
+ * emulator's STDIN to be received from the pipe.
+ */
+#define GUAC_SERIAL_STDIN_PIPE_NAME "STDIN"
+
+/**
+ * The name reserved for the inbound out-of-band control pipe stream. A client
+ * opens an inbound pipe stream with this name to send plain-text, whitespace-
+ * delimited control commands to the serial connection. The only command
+ * currently supported is "break", which sends a serial break.
+ */
+#define GUAC_SERIAL_CONTROL_PIPE_NAME "serial-control"
+
+/**
+ * Handles an incoming stream from a Guacamole "pipe" instruction. If the pipe
+ * is named "STDIN", the contents of the pipe stream are redirected to STDIN of
+ * the terminal emulator for as long as the pipe is open. If the pipe is named
+ * "serial-control", the contents are parsed as plain-text control commands.
+ */
+guac_user_pipe_handler guac_serial_pipe_handler;
+
+#endif
diff --git a/src/protocols/serial/rfc2217.c b/src/protocols/serial/rfc2217.c
new file mode 100644
index 00000000..48502df5
--- /dev/null
+++ b/src/protocols/serial/rfc2217.c
@@ -0,0 +1,361 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#include "config.h"
+
+#include "rfc2217.h"
+#include "settings.h"
+#include "stream.h"
+
+#include <guacamole/client.h>
+#include <guacamole/protocol.h>
+
+#ifdef ENABLE_SERIAL_RFC2217
+
+#include "serial.h"
+#include "terminal/terminal.h"
+
+#include <guacamole/error.h>
+#include <guacamole/tcp.h>
+#include <libtelnet.h>
+
+#include <netinet/in.h>
+#include <netinet/tcp.h>
+#include <stdint.h>
+#include <unistd.h>
+
+/**
+ * The telnet option number of the RFC2217 COM-PORT-OPTION. libtelnet does not
+ * define this option, so it is defined here.
+ */
+#ifndef TELNET_TELOPT_COMPORT
+#define TELNET_TELOPT_COMPORT 44
+#endif
+
+/**
+ * COM-PORT-OPTION client-to-server command: set the serial line speed. The
+ * data is a 4-byte big-endian unsigned integer giving the speed in bits per
+ * second.
+ */
+#define GUAC_SERIAL_RFC2217_SET_BAUDRATE 1
+
+/**
+ * COM-PORT-OPTION client-to-server command: set the number of data bits. The
+ * data is a single byte (5 through 8).
+ */
+#define GUAC_SERIAL_RFC2217_SET_DATASIZE 2
+
+/**
+ * COM-PORT-OPTION client-to-server command: set the parity. The data is a
+ * single byte (1=none, 2=odd, 3=even, 4=mark, 5=space).
+ */
+#define GUAC_SERIAL_RFC2217_SET_PARITY 3
+
+/**
+ * COM-PORT-OPTION client-to-server command: set the number of stop bits. The
+ * data is a single byte (1=1 stop bit, 2=2 stop bits).
+ */
+#define GUAC_SERIAL_RFC2217_SET_STOPSIZE 4
+
+/**
+ * COM-PORT-OPTION client-to-server command: set line/flow control and break
+ * state. The data is a single byte. For flow control: 1=none, 2=xon/xoff,
+ * 3=rts/cts. For the break signal: 5=break on, 6=break off.
+ */
+#define GUAC_SERIAL_RFC2217_SET_CONTROL 5
+
+/**
+ * Writes the entire buffer given to the specified file descriptor, retrying
+ * the write automatically if necessary.
+ *
+ * @param fd
+ * The file descriptor to write to.
+ *
+ * @param buffer
+ * The buffer to write.
+ *
+ * @param size
+ * The number of bytes from the buffer to write.
+ *
+ * @return
+ * The number of bytes written, or a value not equal to size if an error
+ * prevents all future writes.
+ */
+static int guac_serial_rfc2217_write_all(int fd, const char* buffer, int size) {
+
+ int remaining = size;
+ while (remaining > 0) {
+
+ int ret_val;
+ GUAC_RETRY_EINTR(ret_val, write(fd, buffer, remaining));
+ if (ret_val <= 0)
+ return -1;
+
+ remaining -= ret_val;
+ buffer += ret_val;
+
+ }
+
+ return size;
+
+}
+
+/**
+ * Sends a single-byte COM-PORT-OPTION subnegotiation.
+ */
+static void guac_serial_rfc2217_send_byte(telnet_t* telnet,
+ unsigned char command, unsigned char value) {
+ telnet_begin_sb(telnet, TELNET_TELOPT_COMPORT);
+ telnet_send(telnet, (char*) &command, 1);
+ telnet_send(telnet, (char*) &value, 1);
+ telnet_finish_sb(telnet);
+}
+
+/**
+ * Sends the serial line parameters described by the given settings using
+ * client-to-server COM-PORT-OPTION subnegotiations.
+ */
+static void guac_serial_rfc2217_set_line(telnet_t* telnet,
+ guac_serial_settings* settings) {
+
+ /* SET-BAUDRATE: 4-byte big-endian speed in bits per second */
+ uint32_t baud = (uint32_t) settings->baud_rate;
+ unsigned char rate[5] = {
+ GUAC_SERIAL_RFC2217_SET_BAUDRATE,
+ (unsigned char) ((baud >> 24) & 0xFF),
+ (unsigned char) ((baud >> 16) & 0xFF),
+ (unsigned char) ((baud >> 8) & 0xFF),
+ (unsigned char) ( baud & 0xFF)
+ };
+ telnet_begin_sb(telnet, TELNET_TELOPT_COMPORT);
+ telnet_send(telnet, (char*) rate, sizeof(rate));
+ telnet_finish_sb(telnet);
+
+ /* SET-DATASIZE */
+ guac_serial_rfc2217_send_byte(telnet, GUAC_SERIAL_RFC2217_SET_DATASIZE,
+ (unsigned char) settings->data_bits);
+
+ /* SET-PARITY (1=none, 2=odd, 3=even, 4=mark, 5=space) */
+ unsigned char parity;
+ switch (settings->parity) {
+ case GUAC_SERIAL_PARITY_ODD: parity = 2; break;
+ case GUAC_SERIAL_PARITY_EVEN: parity = 3; break;
+ case GUAC_SERIAL_PARITY_MARK: parity = 4; break;
+ case GUAC_SERIAL_PARITY_SPACE: parity = 5; break;
+ default: parity = 1; break;
+ }
+ guac_serial_rfc2217_send_byte(telnet, GUAC_SERIAL_RFC2217_SET_PARITY,
+ parity);
+
+ /* SET-STOPSIZE (1=1 stop bit, 2=2 stop bits) */
+ guac_serial_rfc2217_send_byte(telnet, GUAC_SERIAL_RFC2217_SET_STOPSIZE,
+ (unsigned char) settings->stop_bits);
+
+ /* SET-CONTROL flow control (1=none, 2=xon/xoff, 3=rts/cts) */
+ unsigned char flow;
+ switch (settings->flow_control) {
+ case GUAC_SERIAL_FLOW_XON_XOFF: flow = 2; break;
+ case GUAC_SERIAL_FLOW_RTS_CTS: flow = 3; break;
+ default: flow = 1; break;
+ }
+ guac_serial_rfc2217_send_byte(telnet, GUAC_SERIAL_RFC2217_SET_CONTROL,
+ flow);
+
+}
+
+/**
+ * Event handler, as defined by libtelnet. Invoked for every event fired by the
+ * RFC2217 libtelnet session. The user data is the guac_serial_stream.
+ */
+static void guac_serial_rfc2217_event_handler(telnet_t* telnet,
+ telnet_event_t* event, void* data) {
+
+ guac_serial_stream* stream = (guac_serial_stream*) data;
+ guac_client* client = stream->client;
+ guac_serial_client* serial_client = (guac_serial_client*) client->data;
+
+ switch (event->type) {
+
+ /* Terminal output received */
+ case TELNET_EV_DATA:
+ guac_terminal_write(serial_client->term, event->data.buffer,
+ event->data.size);
+ break;
+
+ /* Data destined for remote end */
+ case TELNET_EV_SEND:
+ if (guac_serial_rfc2217_write_all(stream->fd, event->data.buffer,
+ event->data.size) != (int) event->data.size)
+ guac_client_stop(client);
+ break;
+
+ /* Connection warnings */
+ case TELNET_EV_WARNING:
+ guac_client_log(client, GUAC_LOG_WARNING, "%s", event->error.msg);
+ break;
+
+ /* Connection errors */
+ case TELNET_EV_ERROR:
+ guac_client_abort(client, GUAC_PROTOCOL_STATUS_UPSTREAM_ERROR,
+ "RFC2217 connection closing with error: %s",
+ event->error.msg);
+ break;
+
+ /* Ignore other events */
+ default:
+ break;
+
+ }
+
+}
+
+int guac_serial_rfc2217_open(guac_serial_stream* stream, guac_client* client,
+ guac_serial_settings* settings) {
+
+ /* Support levels for the telnet options used by the RFC2217 session */
+ static const telnet_telopt_t rfc2217_options[] = {
+ { TELNET_TELOPT_COMPORT, TELNET_WILL, TELNET_DONT },
+ { TELNET_TELOPT_BINARY, TELNET_WILL, TELNET_DO },
+ { TELNET_TELOPT_SGA, TELNET_WILL, TELNET_DO },
+ { TELNET_TELOPT_ECHO, TELNET_WONT, TELNET_DO },
+ { -1, 0, 0 }
+ };
+
+ /* Connect to the remote serial server */
+ int fd = guac_tcp_connect(settings->hostname, settings->port,
+ settings->timeout);
+ if (fd < 0) {
+ guac_client_abort(client, GUAC_PROTOCOL_STATUS_UPSTREAM_NOT_FOUND,
+ "Unable to connect to serial server \"%s\" port \"%s\".",
+ settings->hostname, settings->port);
+ return -1;
+ }
+
+ /* Disable Nagle's algorithm so keystrokes are sent without delay */
+ int flag = 1;
+ setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &flag, sizeof(flag));
+
+ /* Store the socket before initializing libtelnet, as the event handler
+ * writes outbound data to this descriptor */
+ stream->backend = GUAC_SERIAL_BACKEND_RFC2217;
+ stream->fd = fd;
+
+ /* Initialize the libtelnet session, passing the stream as user data */
+ telnet_t* telnet = telnet_init(rfc2217_options,
+ guac_serial_rfc2217_event_handler, 0, stream);
+ if (telnet == NULL) {
+ guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR,
+ "RFC2217 client allocation failed.");
+ close(fd);
+ stream->fd = -1;
+ return -1;
+ }
+
+ stream->telnet = telnet;
+
+ /* Advertise our willingness to use COM-PORT-OPTION and binary mode */
+ telnet_negotiate(telnet, TELNET_WILL, TELNET_TELOPT_COMPORT);
+ telnet_negotiate(telnet, TELNET_WILL, TELNET_TELOPT_BINARY);
+ telnet_negotiate(telnet, TELNET_DO, TELNET_TELOPT_BINARY);
+
+ /* Push the configured serial line parameters to the remote end */
+ guac_serial_rfc2217_set_line(telnet, settings);
+
+ return 0;
+
+}
+
+void guac_serial_rfc2217_send(guac_serial_stream* stream, const char* buffer,
+ int length) {
+ telnet_send((telnet_t*) stream->telnet, buffer, length);
+}
+
+void guac_serial_rfc2217_recv(guac_serial_stream* stream, const char* buffer,
+ int length) {
+ telnet_recv((telnet_t*) stream->telnet, buffer, length);
+}
+
+int guac_serial_rfc2217_send_break(guac_serial_stream* stream) {
+
+ telnet_t* telnet = (telnet_t*) stream->telnet;
+
+ /* SET-CONTROL: break on */
+ guac_serial_rfc2217_send_byte(telnet, GUAC_SERIAL_RFC2217_SET_CONTROL, 5);
+
+ /* Hold the line low for the configured duration */
+ if (stream->break_duration > 0)
+ usleep((useconds_t) stream->break_duration * 1000);
+
+ /* SET-CONTROL: break off */
+ guac_serial_rfc2217_send_byte(telnet, GUAC_SERIAL_RFC2217_SET_CONTROL, 6);
+
+ return 0;
+
+}
+
+void guac_serial_rfc2217_free(guac_serial_stream* stream) {
+ if (stream->telnet != NULL) {
+ telnet_free((telnet_t*) stream->telnet);
+ stream->telnet = NULL;
+ }
+}
+
+#else /* ENABLE_SERIAL_RFC2217 */
+
+int guac_serial_rfc2217_open(guac_serial_stream* stream, guac_client* client,
+ guac_serial_settings* settings) {
+
+ (void) stream;
+ (void) settings;
+
+ guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR,
+ "RFC2217 support was not compiled in "
+ "(rebuild guacamole-server with libtelnet).");
+ return -1;
+
+}
+
+void guac_serial_rfc2217_send(guac_serial_stream* stream, const char* buffer,
+ int length) {
+ /* RFC2217 unavailable; a stream with this backend is never created */
+ (void) stream;
+ (void) buffer;
+ (void) length;
+}
+
+void guac_serial_rfc2217_recv(guac_serial_stream* stream, const char* buffer,
+ int length) {
+ /* RFC2217 unavailable; a stream with this backend is never created */
+ (void) stream;
+ (void) buffer;
+ (void) length;
+}
+
+int guac_serial_rfc2217_send_break(guac_serial_stream* stream) {
+ /* RFC2217 unavailable; a stream with this backend is never created */
+ (void) stream;
+ return 0;
+}
+
+void guac_serial_rfc2217_free(guac_serial_stream* stream) {
+ /* No libtelnet session can exist when RFC2217 is not compiled in */
+ (void) stream;
+}
+
+#endif /* ENABLE_SERIAL_RFC2217 */
diff --git a/src/protocols/serial/rfc2217.h b/src/protocols/serial/rfc2217.h
new file mode 100644
index 00000000..971f6a22
--- /dev/null
+++ b/src/protocols/serial/rfc2217.h
@@ -0,0 +1,119 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#ifndef GUAC_SERIAL_RFC2217_H
+#define GUAC_SERIAL_RFC2217_H
+
+#include "settings.h"
+#include "stream.h"
+
+#include <guacamole/client.h>
+
+/*
+ * This header intentionally does not depend on libtelnet. All libtelnet usage
+ * is confined to rfc2217.c and compiled only when ENABLE_SERIAL_RFC2217 is
+ * defined (i.e. libtelnet is available). When it is not, the functions below
+ * are still defined, but fail or become no-ops, so the rest of the plugin
+ * links and runs (with the RFC2217 transport unavailable).
+ */
+
+/**
+ * Opens an RFC2217 (telnet COM-PORT-OPTION) connection to the network serial
+ * server described by the given settings, negotiates the configured serial line
+ * parameters, and stores the resulting socket and libtelnet session in the
+ * given stream. On failure, the client is aborted.
+ *
+ * If RFC2217 support was not compiled in, the client is aborted and -1 is
+ * returned.
+ *
+ * @param stream
+ * The serial stream to populate with the opened socket and session.
+ *
+ * @param client
+ * The client for which the connection is being opened.
+ *
+ * @param settings
+ * The parsed connection settings describing the server to connect to and
+ * the serial line parameters to negotiate.
+ *
+ * @return
+ * Zero on success, non-zero on failure.
+ */
+int guac_serial_rfc2217_open(guac_serial_stream* stream, guac_client* client,
+ guac_serial_settings* settings);
+
+/**
+ * Sends the given buffer to the remote end of an RFC2217 connection, applying
+ * telnet IAC framing. This must only be called on a stream whose backend is
+ * GUAC_SERIAL_BACKEND_RFC2217.
+ *
+ * @param stream
+ * The RFC2217 serial stream to write to.
+ *
+ * @param buffer
+ * The buffer of data to send.
+ *
+ * @param length
+ * The number of bytes from the buffer to send.
+ */
+void guac_serial_rfc2217_send(guac_serial_stream* stream, const char* buffer,
+ int length);
+
+/**
+ * Feeds the given buffer of bytes received from the remote end into the
+ * RFC2217 libtelnet parser, which will de-frame the data and write terminal
+ * output. This must only be called on a stream whose backend is
+ * GUAC_SERIAL_BACKEND_RFC2217.
+ *
+ * @param stream
+ * The RFC2217 serial stream that received the data.
+ *
+ * @param buffer
+ * The buffer of received data.
+ *
+ * @param length
+ * The number of bytes within the buffer.
+ */
+void guac_serial_rfc2217_recv(guac_serial_stream* stream, const char* buffer,
+ int length);
+
+/**
+ * Sends a serial break to the remote end of an RFC2217 connection by holding
+ * the transmit line low for the stream's configured break duration using
+ * COM-PORT-OPTION SET-CONTROL subnegotiations. This must only be called on a
+ * stream whose backend is GUAC_SERIAL_BACKEND_RFC2217.
+ *
+ * @param stream
+ * The RFC2217 serial stream on which to send the break.
+ *
+ * @return
+ * Zero on success, non-zero on error.
+ */
+int guac_serial_rfc2217_send_break(guac_serial_stream* stream);
+
+/**
+ * Frees the libtelnet session associated with the given stream, if any. Safe
+ * to call regardless of backend; has no effect if no session is present.
+ *
+ * @param stream
+ * The serial stream whose libtelnet session should be freed.
+ */
+void guac_serial_rfc2217_free(guac_serial_stream* stream);
+
+#endif
diff --git a/src/protocols/serial/serial.c b/src/protocols/serial/serial.c
new file mode 100644
index 00000000..93c9c38c
--- /dev/null
+++ b/src/protocols/serial/serial.c
@@ -0,0 +1,228 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#include "config.h"
+
+#include "argv.h"
+#include "rfc2217.h"
+#include "serial.h"
+#include "settings.h"
+#include "stream.h"
+#include "terminal/terminal.h"
+
+#include <guacamole/client.h>
+#include <guacamole/error.h>
+#include <guacamole/mem.h>
+#include <guacamole/proctitle.h>
+#include <guacamole/protocol.h>
+#include <guacamole/recording.h>
+
+#include <poll.h>
+#include <pthread.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+/**
+ * Input thread, started by the main serial client thread. This thread
+ * continuously reads from the terminal's STDIN and transfers all read data to
+ * the serial line.
+ *
+ * @param data
+ * The current guac_client instance.
+ *
+ * @return
+ * Always NULL.
+ */
+static void* guac_serial_input_thread(void* data) {
+
+ /* Thread name serial-stdin: reads terminal STDIN and forwards it to the
+ * serial line. */
+ guac_thread_name_set("serial-stdin");
+
+ guac_client* client = (guac_client*) data;
+ guac_serial_client* serial_client = (guac_serial_client*) client->data;
+
+ char buffer[8192];
+ int bytes_read;
+
+ /* Write all data read from STDIN to the serial stream */
+ while ((bytes_read = guac_terminal_read_stdin(serial_client->term, buffer,
+ sizeof(buffer))) > 0) {
+ if (guac_serial_stream_write(serial_client->stream, buffer, bytes_read) < 0)
+ break;
+ }
+
+ return NULL;
+
+}
+
+/**
+ * Waits for data on the given file descriptor for up to one second. The return
+ * value is identical to that of poll(): 0 on timeout, < 0 on error, and > 0
+ * when the descriptor is readable or has hung up.
+ *
+ * @param fd
+ * The file descriptor to wait for.
+ *
+ * @return
+ * A value greater than zero when data is available or the descriptor has
+ * hung up, zero on timeout, and less than zero on error.
+ */
+static int guac_serial_wait(int fd) {
+
+ /* Watch for readable data as well as hangup/error conditions */
+ struct pollfd fds[] = {{
+ .fd = fd,
+ .events = POLLIN | POLLHUP | POLLERR,
+ .revents = 0,
+ }};
+
+ int wait_result;
+ GUAC_RETRY_EINTR(wait_result, poll(fds, 1, 1000));
+
+ return wait_result;
+
+}
+
+void* guac_serial_client_thread(void* data) {
+
+ /* Thread name serial-worker: main serial client thread; runs the serial
+ * session and event loop. */
+ guac_thread_name_set("serial-worker");
+
+ guac_client* client = (guac_client*) data;
+ guac_serial_client* serial_client = (guac_serial_client*) client->data;
+ guac_serial_settings* settings = serial_client->settings;
+
+ pthread_t input_thread;
+ char buffer[8192];
+ int wait_result;
+
+ /* Set process title to the device or hostname reached by this connection */
+ const char* endpoint = settings->type == GUAC_SERIAL_TYPE_LOCAL
+ ? settings->device : settings->hostname;
+ guac_process_title_set_endpoint(GUAC_SERIAL_PROCESS_TITLE_NAME,
+ NULL, endpoint, NULL);
+
+ /* Set up screen recording, if requested */
+ if (settings->recording_path != NULL) {
+ serial_client->recording = guac_recording_create(client,
+ settings->recording_path,
+ settings->recording_name,
+ settings->create_recording_path,
+ !settings->recording_exclude_output,
+ !settings->recording_exclude_mouse,
+ 0, /* Touch events not supported */
+ settings->recording_include_keys,
+ settings->recording_write_existing,
+ settings->recording_include_clipboard);
+ }
+
+ /* Create terminal options with required parameters */
+ guac_terminal_options* options = guac_terminal_options_create(
+ settings->width, settings->height, settings->resolution);
+
+ /* Set optional parameters */
+ options->clipboard_buffer_size = settings->clipboard_buffer_size;
+ options->disable_copy = settings->disable_copy;
+ options->max_scrollback = settings->max_scrollback;
+ options->font_name = settings->font_name;
+ options->font_size = settings->font_size;
+ options->color_scheme = settings->color_scheme;
+ options->backspace = settings->backspace;
+ options->linux_console_keys = (strcmp(settings->terminal_type, "linux") == 0);
+
+ /* Create terminal */
+ serial_client->term = guac_terminal_create(client, options);
+
+ /* Free options struct now that it's been used */
+ guac_mem_free(options);
+
+ /* Fail if terminal init failed */
+ if (serial_client->term == NULL) {
+ guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR,
+ "Terminal initialization failed");
+ return NULL;
+ }
+
+ /* Set up typescript, if requested */
+ if (settings->typescript_path != NULL) {
+ guac_terminal_create_typescript(serial_client->term,
+ settings->typescript_path,
+ settings->typescript_name,
+ settings->create_typescript_path,
+ settings->typescript_write_existing);
+ }
+
+ /* Open the serial stream via the configured transport */
+ serial_client->stream = guac_serial_stream_open(client, settings);
+ if (serial_client->stream == NULL) {
+ /* Already aborted within guac_serial_stream_open() */
+ return NULL;
+ }
+
+ /* Connection established */
+ guac_client_log(client, GUAC_LOG_INFO, "Serial connection established.");
+
+ /* Allow the terminal to render; there is no login detection */
+ guac_terminal_start(serial_client->term);
+
+ /* Send current values of exposed arguments to owner only */
+ guac_client_for_owner(client, guac_serial_send_current_argv, serial_client);
+
+ /* Start input thread */
+ if (pthread_create(&input_thread, NULL, guac_serial_input_thread,
+ (void*) client)) {
+ guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR,
+ "Unable to start input thread");
+ return NULL;
+ }
+
+ /* While data available, write to terminal */
+ while ((wait_result = guac_serial_wait(serial_client->stream->fd)) >= 0) {
+
+ /* Resume waiting if no data available */
+ if (wait_result == 0)
+ continue;
+
+ int bytes_read;
+ GUAC_RETRY_EINTR(bytes_read,
+ read(serial_client->stream->fd, buffer, sizeof(buffer)));
+
+ /* EOF, EIO, or hangup indicates the serial line has closed */
+ if (bytes_read <= 0)
+ break;
+
+ /* RFC2217 output must be de-framed; all other transports are raw */
+ if (serial_client->stream->backend == GUAC_SERIAL_BACKEND_RFC2217)
+ guac_serial_rfc2217_recv(serial_client->stream, buffer, bytes_read);
+ else
+ guac_terminal_write(serial_client->term, buffer, bytes_read);
+
+ }
+
+ /* Kill client and wait for input thread to die */
+ guac_client_stop(client);
+ pthread_join(input_thread, NULL);
+
+ guac_client_log(client, GUAC_LOG_INFO, "Serial connection ended.");
+ return NULL;
+
+}
diff --git a/src/protocols/serial/serial.h b/src/protocols/serial/serial.h
new file mode 100644
index 00000000..2963daa3
--- /dev/null
+++ b/src/protocols/serial/serial.h
@@ -0,0 +1,83 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#ifndef GUAC_SERIAL_H
+#define GUAC_SERIAL_H
+
+#include "settings.h"
+#include "stream.h"
+#include "terminal/terminal.h"
+
+#include <guacamole/recording.h>
+
+#include <pthread.h>
+
+/**
+ * Serial-specific client data.
+ */
+typedef struct guac_serial_client {
+
+ /**
+ * Serial connection settings.
+ */
+ guac_serial_settings* settings;
+
+ /**
+ * The serial client thread.
+ */
+ pthread_t client_thread;
+
+ /**
+ * Whether client_thread has been successfully created and must therefore
+ * be joined during cleanup.
+ */
+ int client_thread_valid;
+
+ /**
+ * The serial byte stream connected to the serial line, or NULL if no
+ * connection has been established.
+ */
+ guac_serial_stream* stream;
+
+ /**
+ * The terminal which will render all output from the serial line.
+ */
+ guac_terminal* term;
+
+ /**
+ * The in-progress session recording, or NULL if no recording is in
+ * progress.
+ */
+ guac_recording* recording;
+
+} guac_serial_client;
+
+/**
+ * Main serial client thread, handling transfer of serial output to the
+ * terminal and user input to the serial line.
+ *
+ * @param data
+ * The guac_client instance associated with the serial connection.
+ *
+ * @return
+ * Always NULL.
+ */
+void* guac_serial_client_thread(void* data);
+
+#endif
diff --git a/src/protocols/serial/settings.c b/src/protocols/serial/settings.c
new file mode 100644
index 00000000..770ff906
--- /dev/null
+++ b/src/protocols/serial/settings.c
@@ -0,0 +1,795 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#include "config.h"
+
+#include "argv.h"
+#include "common/defaults.h"
+#include "common/clipboard.h"
+#include "settings.h"
+#include "terminal/terminal.h"
+
+#include <guacamole/mem.h>
+#include <guacamole/string.h>
+#include <guacamole/user.h>
+
+#include <sys/types.h>
+#include <termios.h>
+#include <errno.h>
+#include <limits.h>
+#include <stdlib.h>
+#include <string.h>
+
+/* Client plugin arguments */
+const char* GUAC_SERIAL_CLIENT_ARGS[] = {
+ "serial-type",
+ "network-protocol",
+ "device",
+ "hostname",
+ "port",
+ "timeout",
+ "baud-rate",
+ "data-bits",
+ "stop-bits",
+ "parity",
+ "flow-control",
+ "break-duration",
+ "paste-delay",
+ "allowed-devices",
+ "read-only",
+ GUAC_SERIAL_ARGV_FONT_NAME,
+ GUAC_SERIAL_ARGV_FONT_SIZE,
+ GUAC_SERIAL_ARGV_COLOR_SCHEME,
+ "scrollback",
+ "backspace",
+ "terminal-type",
+ "clipboard-buffer-size",
+ "disable-copy",
+ "disable-paste",
+ "typescript-path",
+ "typescript-name",
+ "create-typescript-path",
+ "typescript-write-existing",
+ "recording-path",
+ "recording-name",
+ "recording-exclude-output",
+ "recording-exclude-mouse",
+ "recording-include-keys",
+ "recording-include-clipboard",
+ "create-recording-path",
+ "recording-write-existing",
+ NULL
+};
+
+enum SERIAL_ARGS_IDX {
+
+ /**
+ * The transport used to reach the serial line ("local" or "network").
+ */
+ IDX_SERIAL_TYPE,
+
+ /**
+ * The network protocol used when the transport type is "network" ("raw" or
+ * "rfc2217").
+ */
+ IDX_NETWORK_PROTOCOL,
+
+ /**
+ * The absolute path of the local serial device to open (e.g.
+ * "/dev/ttyUSB0"). Required for "local" connections.
+ */
+ IDX_DEVICE,
+
+ /**
+ * The hostname of the network serial server to connect to. Required for
+ * "network" connections.
+ */
+ IDX_HOSTNAME,
+
+ /**
+ * The port of the network serial server to connect to. Required for
+ * "network" connections.
+ */
+ IDX_PORT,
+
+ /**
+ * The number of seconds to wait for the remote server to respond. Optional.
+ */
+ IDX_TIMEOUT,
+
+ /**
+ * The serial line speed, in bits per second (e.g. 9600, 115200).
+ */
+ IDX_BAUD_RATE,
+
+ /**
+ * The number of data bits per character (5, 6, 7, or 8).
+ */
+ IDX_DATA_BITS,
+
+ /**
+ * The number of stop bits per character (1 or 2).
+ */
+ IDX_STOP_BITS,
+
+ /**
+ * The parity scheme ("none", "odd", "even", "mark", or "space").
+ */
+ IDX_PARITY,
+
+ /**
+ * The flow control scheme ("none", "rts-cts", or "xon-xoff").
+ */
+ IDX_FLOW_CONTROL,
+
+ /**
+ * The duration, in milliseconds, that the transmit line is held low when
+ * sending a serial break.
+ */
+ IDX_BREAK_DURATION,
+
+ /**
+ * The delay, in milliseconds, inserted between each byte written to the
+ * serial line. Zero disables pacing.
+ */
+ IDX_PASTE_DELAY,
+
+ /**
+ * An optional ":"-separated allowlist of local device paths or path
+ * prefixes. If set, a "local" connection whose canonicalized device does
+ * not equal or fall under one of the entries is rejected.
+ */
+ IDX_ALLOWED_DEVICES,
+
+ /**
+ * "true" if this connection should be read-only (user input should be
+ * dropped), "false" or blank otherwise.
+ */
+ IDX_READ_ONLY,
+
+ /**
+ * The name of the font to use within the terminal.
+ */
+ IDX_FONT_NAME,
+
+ /**
+ * The size of the font to use within the terminal, in points.
+ */
+ IDX_FONT_SIZE,
+
+ /**
+ * The color scheme to use, as a series of semicolon-separated color-value
+ * pairs, or one of the special color scheme names.
+ */
+ IDX_COLOR_SCHEME,
+
+ /**
+ * The maximum size of the scrollback buffer in rows.
+ */
+ IDX_SCROLLBACK,
+
+ /**
+ * ASCII code, as an integer to use for the backspace key, or
+ * GUAC_TERMINAL_DEFAULT_BACKSPACE if not specified.
+ */
+ IDX_BACKSPACE,
+
+ /**
+ * The terminal emulator type that is used for rendering (e.g. "xterm" or
+ * "xterm-256color"). "linux" is used if unspecified.
+ */
+ IDX_TERMINAL_TYPE,
+
+ /**
+ * The maximum number of bytes to allow within the clipboard.
+ */
+ IDX_CLIPBOARD_BUFFER_SIZE,
+
+ /**
+ * Whether outbound clipboard access should be blocked.
+ */
+ IDX_DISABLE_COPY,
+
+ /**
+ * Whether inbound clipboard access should be blocked.
+ */
+ IDX_DISABLE_PASTE,
+
+ /**
+ * The full absolute path to the directory in which typescripts should be
+ * written.
+ */
+ IDX_TYPESCRIPT_PATH,
+
+ /**
+ * The name that should be given to typescripts which are written in the
+ * given path.
+ */
+ IDX_TYPESCRIPT_NAME,
+
+ /**
+ * Whether the specified typescript path should automatically be created if
+ * it does not yet exist.
+ */
+ IDX_CREATE_TYPESCRIPT_PATH,
+
+ /**
+ * Whether existing files should be appended to when creating a new
+ * typescript. Disabled by default.
+ */
+ IDX_TYPESCRIPT_WRITE_EXISTING,
+
+ /**
+ * The full absolute path to the directory in which screen recordings
+ * should be written.
+ */
+ IDX_RECORDING_PATH,
+
+ /**
+ * The name that should be given to screen recordings which are written in
+ * the given path.
+ */
+ IDX_RECORDING_NAME,
+
+ /**
+ * Whether output which is broadcast to each connected client should NOT be
+ * included in the session recording.
+ */
+ IDX_RECORDING_EXCLUDE_OUTPUT,
+
+ /**
+ * Whether changes to mouse state should NOT be included in the session
+ * recording.
+ */
+ IDX_RECORDING_EXCLUDE_MOUSE,
+
+ /**
+ * Whether keys pressed and released should be included in the session
+ * recording.
+ */
+ IDX_RECORDING_INCLUDE_KEYS,
+
+ /**
+ * Whether clipboard data should be included in the session recording.
+ */
+ IDX_RECORDING_INCLUDE_CLIPBOARD,
+
+ /**
+ * Whether the specified screen recording path should automatically be
+ * created if it does not yet exist.
+ */
+ IDX_CREATE_RECORDING_PATH,
+
+ /**
+ * Whether existing files should be appended to when creating a new
+ * recording. Disabled by default.
+ */
+ IDX_RECORDING_WRITE_EXISTING,
+
+ SERIAL_ARGS_COUNT
+};
+
+speed_t guac_serial_baud_to_speed(int baud_rate) {
+
+ switch (baud_rate) {
+ case 300: return B300;
+ case 1200: return B1200;
+ case 2400: return B2400;
+ case 4800: return B4800;
+ case 9600: return B9600;
+ case 19200: return B19200;
+ case 38400: return B38400;
+ case 57600: return B57600;
+ case 115200: return B115200;
+ case 230400: return B230400;
+ default: return (speed_t) -1;
+ }
+
+}
+
+/**
+ * Parses the given transport type string into a guac_serial_type value.
+ *
+ * @param str
+ * The transport type string ("local" or "network").
+ *
+ * @param type
+ * A pointer to the guac_serial_type value that should receive the parsed
+ * result.
+ *
+ * @return
+ * Zero if the string was successfully parsed, non-zero otherwise.
+ */
+static int guac_serial_parse_type(const char* str, guac_serial_type* type) {
+ if (strcmp(str, "local") == 0) { *type = GUAC_SERIAL_TYPE_LOCAL; return 0; }
+ if (strcmp(str, "network") == 0) { *type = GUAC_SERIAL_TYPE_NETWORK; return 0; }
+ return 1;
+}
+
+/**
+ * Parses the given network protocol string into a
+ * guac_serial_network_protocol value.
+ *
+ * @param str
+ * The network protocol string ("raw" or "rfc2217").
+ *
+ * @param protocol
+ * A pointer to the value that should receive the parsed result.
+ *
+ * @return
+ * Zero if the string was successfully parsed, non-zero otherwise.
+ */
+static int guac_serial_parse_network_protocol(const char* str,
+ guac_serial_network_protocol* protocol) {
+ if (strcmp(str, "raw") == 0) { *protocol = GUAC_SERIAL_NETWORK_PROTOCOL_RAW; return 0; }
+ if (strcmp(str, "rfc2217") == 0) { *protocol = GUAC_SERIAL_NETWORK_PROTOCOL_RFC2217; return 0; }
+ return 1;
+}
+
+/**
+ * Parses the given parity string into a guac_serial_parity value.
+ *
+ * @param str
+ * The parity string ("none", "odd", "even", "mark", or "space").
+ *
+ * @param parity
+ * A pointer to the value that should receive the parsed result.
+ *
+ * @return
+ * Zero if the string was successfully parsed, non-zero otherwise.
+ */
+static int guac_serial_parse_parity(const char* str, guac_serial_parity* parity) {
+ if (strcmp(str, "none") == 0) { *parity = GUAC_SERIAL_PARITY_NONE; return 0; }
+ if (strcmp(str, "odd") == 0) { *parity = GUAC_SERIAL_PARITY_ODD; return 0; }
+ if (strcmp(str, "even") == 0) { *parity = GUAC_SERIAL_PARITY_EVEN; return 0; }
+ if (strcmp(str, "mark") == 0) { *parity = GUAC_SERIAL_PARITY_MARK; return 0; }
+ if (strcmp(str, "space") == 0) { *parity = GUAC_SERIAL_PARITY_SPACE; return 0; }
+ return 1;
+}
+
+/**
+ * Parses the given flow control string into a guac_serial_flow_control value.
+ *
+ * @param str
+ * The flow control string ("none", "rts-cts", or "xon-xoff").
+ *
+ * @param flow
+ * A pointer to the value that should receive the parsed result.
+ *
+ * @return
+ * Zero if the string was successfully parsed, non-zero otherwise.
+ */
+static int guac_serial_parse_flow(const char* str, guac_serial_flow_control* flow) {
+ if (strcmp(str, "none") == 0) { *flow = GUAC_SERIAL_FLOW_NONE; return 0; }
+ if (strcmp(str, "rts-cts") == 0) { *flow = GUAC_SERIAL_FLOW_RTS_CTS; return 0; }
+ if (strcmp(str, "xon-xoff") == 0) { *flow = GUAC_SERIAL_FLOW_XON_XOFF; return 0; }
+ return 1;
+}
+
+/**
+ * Returns whether the given local device is permitted by the given ":"-
+ * separated allowlist of device paths or path prefixes. Both the device and
+ * each allowlist entry are canonicalized with realpath() before comparison; a
+ * device is permitted if its canonical path equals a canonicalized entry or
+ * falls beneath one of the canonicalized entries.
+ *
+ * @param user
+ * The user on whose behalf error messages should be logged.
+ *
+ * @param device
+ * The local device path to test.
+ *
+ * @param allowed
+ * The ":"-separated allowlist of permitted device paths or prefixes.
+ *
+ * @return
+ * true if the device is permitted, false otherwise.
+ */
+static bool guac_serial_device_allowed(guac_user* user, const char* device,
+ const char* allowed) {
+
+ char real_device[PATH_MAX];
+ if (realpath(device, real_device) == NULL) {
+ guac_user_log(user, GUAC_LOG_ERROR, "Unable to resolve serial device "
+ "\"%s\": %s", device, strerror(errno));
+ return false;
+ }
+
+ /* Duplicate the list so it may be tokenized in place */
+ char* list = guac_strdup(allowed);
+ char* saveptr = NULL;
+ bool permitted = false;
+
+ for (char* token = strtok_r(list, ":", &saveptr); token != NULL;
+ token = strtok_r(NULL, ":", &saveptr)) {
+
+ /* Skip empty entries */
+ if (token[0] == '\0')
+ continue;
+
+ /* Canonicalize the allowlist entry; skip entries that do not resolve */
+ char real_entry[PATH_MAX];
+ if (realpath(token, real_entry) == NULL)
+ continue;
+
+ size_t entry_len = strlen(real_entry);
+
+ /* Permit an exact match */
+ if (strcmp(real_device, real_entry) == 0) {
+ permitted = true;
+ break;
+ }
+
+ /* Permit a device that falls beneath a permitted prefix */
+ if (strncmp(real_device, real_entry, entry_len) == 0
+ && real_device[entry_len] == '/') {
+ permitted = true;
+ break;
+ }
+
+ }
+
+ guac_mem_free(list);
+
+ if (!permitted)
+ guac_user_log(user, GUAC_LOG_ERROR, "Serial device \"%s\" is not "
+ "permitted by the configured allowed-devices list.", device);
+
+ return permitted;
+
+}
+
+guac_serial_settings* guac_serial_parse_args(guac_user* user,
+ int argc, const char** argv) {
+
+ /* Validate arg count */
+ if (argc != SERIAL_ARGS_COUNT) {
+ guac_user_log(user, GUAC_LOG_WARNING, "Incorrect number of connection "
+ "parameters provided: expected %i, got %i.",
+ SERIAL_ARGS_COUNT, argc);
+ return NULL;
+ }
+
+ guac_serial_settings* settings = guac_mem_zalloc(sizeof(guac_serial_settings));
+
+ /* Parse transport type */
+ char* type_str = guac_user_parse_args_string(user, GUAC_SERIAL_CLIENT_ARGS,
+ argv, IDX_SERIAL_TYPE, "local");
+ if (guac_serial_parse_type(type_str, &settings->type)) {
+ guac_user_log(user, GUAC_LOG_ERROR, "Invalid serial-type \"%s\": must "
+ "be \"local\" or \"network\".", type_str);
+ guac_mem_free(type_str);
+ guac_mem_free(settings);
+ return NULL;
+ }
+ guac_mem_free(type_str);
+
+ /* Parse network protocol */
+ char* proto_str = guac_user_parse_args_string(user, GUAC_SERIAL_CLIENT_ARGS,
+ argv, IDX_NETWORK_PROTOCOL, "raw");
+ if (guac_serial_parse_network_protocol(proto_str, &settings->network_protocol)) {
+ guac_user_log(user, GUAC_LOG_ERROR, "Invalid network-protocol \"%s\": "
+ "must be \"raw\" or \"rfc2217\".", proto_str);
+ guac_mem_free(proto_str);
+ guac_mem_free(settings);
+ return NULL;
+ }
+ guac_mem_free(proto_str);
+
+ /* Parse local device path */
+ settings->device =
+ guac_user_parse_args_string(user, GUAC_SERIAL_CLIENT_ARGS, argv,
+ IDX_DEVICE, NULL);
+
+ /* Parse network connection information */
+ settings->hostname =
+ guac_user_parse_args_string(user, GUAC_SERIAL_CLIENT_ARGS, argv,
+ IDX_HOSTNAME, NULL);
+
+ settings->port =
+ guac_user_parse_args_string(user, GUAC_SERIAL_CLIENT_ARGS, argv,
+ IDX_PORT, NULL);
+
+ settings->timeout =
+ guac_user_parse_args_int(user, GUAC_SERIAL_CLIENT_ARGS, argv,
+ IDX_TIMEOUT, GUAC_SERIAL_DEFAULT_TIMEOUT);
+
+ /* Validate transport-specific requirements */
+ if (settings->type == GUAC_SERIAL_TYPE_LOCAL) {
+ if (settings->device == NULL || settings->device[0] == '\0') {
+ guac_user_log(user, GUAC_LOG_ERROR, "A \"device\" is required for "
+ "local serial connections.");
+ guac_serial_settings_free(settings);
+ return NULL;
+ }
+ }
+ else {
+ if (settings->hostname == NULL || settings->hostname[0] == '\0') {
+ guac_user_log(user, GUAC_LOG_ERROR, "A \"hostname\" is required for "
+ "network serial connections.");
+ guac_serial_settings_free(settings);
+ return NULL;
+ }
+ if (settings->port == NULL || settings->port[0] == '\0') {
+ guac_user_log(user, GUAC_LOG_ERROR, "A \"port\" is required for "
+ "network serial connections.");
+ guac_serial_settings_free(settings);
+ return NULL;
+ }
+ }
+
+#ifndef ENABLE_SERIAL_RFC2217
+ /* Reject RFC2217 if libtelnet support was not compiled in */
+ if (settings->type == GUAC_SERIAL_TYPE_NETWORK
+ && settings->network_protocol == GUAC_SERIAL_NETWORK_PROTOCOL_RFC2217) {
+ guac_user_log(user, GUAC_LOG_ERROR, "RFC2217 transport requested but "
+ "not compiled in; rebuild with libtelnet, or use "
+ "network-protocol=raw.");
+ guac_serial_settings_free(settings);
+ return NULL;
+ }
+#endif
+
+ /* Parse serial line speed */
+ settings->baud_rate =
+ guac_user_parse_args_int(user, GUAC_SERIAL_CLIENT_ARGS, argv,
+ IDX_BAUD_RATE, 9600);
+ if (guac_serial_baud_to_speed(settings->baud_rate) == (speed_t) -1) {
+ guac_user_log(user, GUAC_LOG_ERROR, "Unsupported baud-rate: %i.",
+ settings->baud_rate);
+ guac_serial_settings_free(settings);
+ return NULL;
+ }
+
+ /* Parse data bits */
+ settings->data_bits =
+ guac_user_parse_args_int(user, GUAC_SERIAL_CLIENT_ARGS, argv,
+ IDX_DATA_BITS, 8);
+ if (settings->data_bits < 5 || settings->data_bits > 8) {
+ guac_user_log(user, GUAC_LOG_ERROR, "Invalid data-bits: %i (must be "
+ "5, 6, 7, or 8).", settings->data_bits);
+ guac_serial_settings_free(settings);
+ return NULL;
+ }
+
+ /* Parse stop bits */
+ settings->stop_bits =
+ guac_user_parse_args_int(user, GUAC_SERIAL_CLIENT_ARGS, argv,
+ IDX_STOP_BITS, 1);
+ if (settings->stop_bits != 1 && settings->stop_bits != 2) {
+ guac_user_log(user, GUAC_LOG_ERROR, "Invalid stop-bits: %i (must be "
+ "1 or 2).", settings->stop_bits);
+ guac_serial_settings_free(settings);
+ return NULL;
+ }
+
+ /* Parse parity */
+ char* parity_str = guac_user_parse_args_string(user, GUAC_SERIAL_CLIENT_ARGS,
+ argv, IDX_PARITY, "none");
+ if (guac_serial_parse_parity(parity_str, &settings->parity)) {
+ guac_user_log(user, GUAC_LOG_ERROR, "Invalid parity \"%s\": must be "
+ "\"none\", \"odd\", \"even\", \"mark\", or \"space\".",
+ parity_str);
+ guac_mem_free(parity_str);
+ guac_serial_settings_free(settings);
+ return NULL;
+ }
+ guac_mem_free(parity_str);
+
+ /* Parse flow control */
+ char* flow_str = guac_user_parse_args_string(user, GUAC_SERIAL_CLIENT_ARGS,
+ argv, IDX_FLOW_CONTROL, "none");
+ if (guac_serial_parse_flow(flow_str, &settings->flow_control)) {
+ guac_user_log(user, GUAC_LOG_ERROR, "Invalid flow-control \"%s\": must "
+ "be \"none\", \"rts-cts\", or \"xon-xoff\".", flow_str);
+ guac_mem_free(flow_str);
+ guac_serial_settings_free(settings);
+ return NULL;
+ }
+ guac_mem_free(flow_str);
+
+ /* Parse break duration */
+ settings->break_duration =
+ guac_user_parse_args_int(user, GUAC_SERIAL_CLIENT_ARGS, argv,
+ IDX_BREAK_DURATION, GUAC_SERIAL_DEFAULT_BREAK_DURATION);
+ if (settings->break_duration < 0)
+ settings->break_duration = GUAC_SERIAL_DEFAULT_BREAK_DURATION;
+
+ /* Parse paste delay */
+ settings->paste_delay =
+ guac_user_parse_args_int(user, GUAC_SERIAL_CLIENT_ARGS, argv,
+ IDX_PASTE_DELAY, 0);
+ if (settings->paste_delay < 0)
+ settings->paste_delay = 0;
+
+ /* Enforce local device allowlist, if configured */
+ char* allowed = guac_user_parse_args_string(user, GUAC_SERIAL_CLIENT_ARGS,
+ argv, IDX_ALLOWED_DEVICES, NULL);
+ if (settings->type == GUAC_SERIAL_TYPE_LOCAL
+ && allowed != NULL && allowed[0] != '\0') {
+ if (!guac_serial_device_allowed(user, settings->device, allowed)) {
+ guac_mem_free(allowed);
+ guac_serial_settings_free(settings);
+ return NULL;
+ }
+ }
+ guac_mem_free(allowed);
+
+ /* Read-only mode */
+ settings->read_only =
+ guac_user_parse_args_boolean(user, GUAC_SERIAL_CLIENT_ARGS, argv,
+ IDX_READ_ONLY, false);
+
+ /* Read maximum scrollback size */
+ settings->max_scrollback =
+ guac_user_parse_args_int(user, GUAC_SERIAL_CLIENT_ARGS, argv,
+ IDX_SCROLLBACK, GUAC_TERMINAL_DEFAULT_MAX_SCROLLBACK);
+
+ /* Read font name */
+ settings->font_name =
+ guac_user_parse_args_string(user, GUAC_SERIAL_CLIENT_ARGS, argv,
+ IDX_FONT_NAME, GUAC_TERMINAL_DEFAULT_FONT_NAME);
+
+ /* Read font size */
+ settings->font_size =
+ guac_user_parse_args_int(user, GUAC_SERIAL_CLIENT_ARGS, argv,
+ IDX_FONT_SIZE, GUAC_TERMINAL_DEFAULT_FONT_SIZE);
+
+ /* Copy requested color scheme */
+ settings->color_scheme =
+ guac_user_parse_args_string(user, GUAC_SERIAL_CLIENT_ARGS, argv,
+ IDX_COLOR_SCHEME, GUAC_TERMINAL_DEFAULT_COLOR_SCHEME);
+
+ /* Pull width/height/resolution directly from user */
+ settings->width = user->info.optimal_width;
+ settings->height = user->info.optimal_height;
+ settings->resolution = user->info.optimal_resolution;
+
+ /* Parse backspace key code */
+ settings->backspace =
+ guac_user_parse_args_int(user, GUAC_SERIAL_CLIENT_ARGS, argv,
+ IDX_BACKSPACE, GUAC_TERMINAL_DEFAULT_BACKSPACE);
+
+ /* Read terminal emulator type. */
+ settings->terminal_type =
+ guac_user_parse_args_string(user, GUAC_SERIAL_CLIENT_ARGS, argv,
+ IDX_TERMINAL_TYPE, "linux");
+
+ /* Set the maximum number of bytes to allow within the clipboard. */
+ settings->clipboard_buffer_size =
+ guac_user_parse_args_int(user, GUAC_SERIAL_CLIENT_ARGS, argv,
+ IDX_CLIPBOARD_BUFFER_SIZE, 0);
+
+ /* Use default clipboard buffer size if given one is invalid. */
+ if (settings->clipboard_buffer_size < GUAC_COMMON_CLIPBOARD_MIN_LENGTH) {
+ settings->clipboard_buffer_size = GUAC_COMMON_CLIPBOARD_MIN_LENGTH;
+ guac_user_log(user, GUAC_LOG_INFO, "Unspecified or invalid clipboard buffer "
+ "size: \"%s\". Using the default minimum size: %i.",
+ argv[IDX_CLIPBOARD_BUFFER_SIZE],
+ settings->clipboard_buffer_size);
+ }
+ else if (settings->clipboard_buffer_size > GUAC_COMMON_CLIPBOARD_MAX_LENGTH) {
+ settings->clipboard_buffer_size = GUAC_COMMON_CLIPBOARD_MAX_LENGTH;
+ guac_user_log(user, GUAC_LOG_WARNING, "Invalid clipboard buffer "
+ "size: \"%s\". Using the default maximum size: %i.",
+ argv[IDX_CLIPBOARD_BUFFER_SIZE],
+ settings->clipboard_buffer_size);
+ }
+
+ /* Parse clipboard copy disable flag */
+ settings->disable_copy =
+ guac_user_parse_args_boolean(user, GUAC_SERIAL_CLIENT_ARGS, argv,
+ IDX_DISABLE_COPY, false);
+
+ /* Parse clipboard paste disable flag */
+ settings->disable_paste =
+ guac_user_parse_args_boolean(user, GUAC_SERIAL_CLIENT_ARGS, argv,
+ IDX_DISABLE_PASTE, false);
+
+ /* Read typescript path */
+ settings->typescript_path =
+ guac_user_parse_args_string(user, GUAC_SERIAL_CLIENT_ARGS, argv,
+ IDX_TYPESCRIPT_PATH, NULL);
+
+ /* Read typescript name */
+ settings->typescript_name =
+ guac_user_parse_args_string(user, GUAC_SERIAL_CLIENT_ARGS, argv,
+ IDX_TYPESCRIPT_NAME, GUAC_SERIAL_DEFAULT_TYPESCRIPT_NAME);
+
+ /* Parse path creation flag */
+ settings->create_typescript_path =
+ guac_user_parse_args_boolean(user, GUAC_SERIAL_CLIENT_ARGS, argv,
+ IDX_CREATE_TYPESCRIPT_PATH, false);
+
+ /* Parse allow write existing file flag */
+ settings->typescript_write_existing =
+ guac_user_parse_args_boolean(user, GUAC_SERIAL_CLIENT_ARGS, argv,
+ IDX_TYPESCRIPT_WRITE_EXISTING, false);
+
+ /* Read recording path */
+ settings->recording_path =
+ guac_user_parse_args_string(user, GUAC_SERIAL_CLIENT_ARGS, argv,
+ IDX_RECORDING_PATH, NULL);
+
+ /* Read recording name */
+ settings->recording_name =
+ guac_user_parse_args_string(user, GUAC_SERIAL_CLIENT_ARGS, argv,
+ IDX_RECORDING_NAME, GUAC_SERIAL_DEFAULT_RECORDING_NAME);
+
+ /* Parse output exclusion flag */
+ settings->recording_exclude_output =
+ guac_user_parse_args_boolean(user, GUAC_SERIAL_CLIENT_ARGS, argv,
+ IDX_RECORDING_EXCLUDE_OUTPUT, false);
+
+ /* Parse mouse exclusion flag */
+ settings->recording_exclude_mouse =
+ guac_user_parse_args_boolean(user, GUAC_SERIAL_CLIENT_ARGS, argv,
+ IDX_RECORDING_EXCLUDE_MOUSE, false);
+
+ /* Parse key event inclusion flag */
+ settings->recording_include_keys =
+ guac_user_parse_args_boolean(user, GUAC_SERIAL_CLIENT_ARGS, argv,
+ IDX_RECORDING_INCLUDE_KEYS, false);
+
+ /* Parse clipboard inclusion flag */
+ settings->recording_include_clipboard =
+ guac_user_parse_args_boolean(user, GUAC_SERIAL_CLIENT_ARGS, argv,
+ IDX_RECORDING_INCLUDE_CLIPBOARD, false);
+
+ /* Parse path creation flag */
+ settings->create_recording_path =
+ guac_user_parse_args_boolean(user, GUAC_SERIAL_CLIENT_ARGS, argv,
+ IDX_CREATE_RECORDING_PATH, false);
+
+ /* Parse allow write existing file flag */
+ settings->recording_write_existing =
+ guac_user_parse_args_boolean(user, GUAC_SERIAL_CLIENT_ARGS, argv,
+ IDX_RECORDING_WRITE_EXISTING, false);
+
+ /* Parsing was successful */
+ return settings;
+
+}
+
+void guac_serial_settings_free(guac_serial_settings* settings) {
+
+ /* Free transport connection information */
+ guac_mem_free(settings->device);
+ guac_mem_free(settings->hostname);
+ guac_mem_free(settings->port);
+
+ /* Free display preferences */
+ guac_mem_free(settings->font_name);
+ guac_mem_free(settings->color_scheme);
+
+ /* Free typescript settings */
+ guac_mem_free(settings->typescript_name);
+ guac_mem_free(settings->typescript_path);
+
+ /* Free screen recording settings */
+ guac_mem_free(settings->recording_name);
+ guac_mem_free(settings->recording_path);
+
+ /* Free terminal emulator type. */
+ guac_mem_free(settings->terminal_type);
+
+ /* Free overall structure */
+ guac_mem_free(settings);
+
+}
diff --git a/src/protocols/serial/settings.h b/src/protocols/serial/settings.h
new file mode 100644
index 00000000..728c8196
--- /dev/null
+++ b/src/protocols/serial/settings.h
@@ -0,0 +1,404 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#ifndef GUAC_SERIAL_SETTINGS_H
+#define GUAC_SERIAL_SETTINGS_H
+
+#include <guacamole/user.h>
+
+#include <termios.h>
+#include <sys/types.h>
+#include <stdbool.h>
+
+/**
+ * The port to connect to when initiating any network serial connection, if no
+ * other port is specified.
+ */
+#define GUAC_SERIAL_DEFAULT_PORT "2000"
+
+/**
+ * The protocol label included in the process title (the first argument passed
+ * to guac_process_title_set_endpoint()), as seen in `ps`/`top`.
+ */
+#define GUAC_SERIAL_PROCESS_TITLE_NAME "serial"
+
+/**
+ * The default number of seconds to wait for a successful connection before
+ * timing out.
+ */
+#define GUAC_SERIAL_DEFAULT_TIMEOUT 10
+
+/**
+ * The default duration, in milliseconds, that the transmit line is held low
+ * when sending a serial break.
+ */
+#define GUAC_SERIAL_DEFAULT_BREAK_DURATION 500
+
+/**
+ * The filename to use for the typescript, if not specified.
+ */
+#define GUAC_SERIAL_DEFAULT_TYPESCRIPT_NAME "typescript"
+
+/**
+ * The filename to use for the screen recording, if not specified.
+ */
+#define GUAC_SERIAL_DEFAULT_RECORDING_NAME "recording"
+
+/**
+ * The transport used to reach the serial line.
+ */
+typedef enum guac_serial_type {
+
+ /**
+ * A local serial device accessed directly via a device node (e.g.
+ * "/dev/ttyUSB0").
+ */
+ GUAC_SERIAL_TYPE_LOCAL,
+
+ /**
+ * A remote serial line reached over the network (raw TCP or RFC2217).
+ */
+ GUAC_SERIAL_TYPE_NETWORK
+
+} guac_serial_type;
+
+/**
+ * The network protocol used when the transport type is
+ * GUAC_SERIAL_TYPE_NETWORK.
+ */
+typedef enum guac_serial_network_protocol {
+
+ /**
+ * A raw byte stream, as exposed by ser2net in "raw" mode. Serial line
+ * parameters cannot be negotiated and are purely informational.
+ */
+ GUAC_SERIAL_NETWORK_PROTOCOL_RAW,
+
+ /**
+ * The RFC2217 (telnet COM-PORT-OPTION) protocol, allowing serial line
+ * parameters and break signalling to be negotiated with the remote end.
+ */
+ GUAC_SERIAL_NETWORK_PROTOCOL_RFC2217
+
+} guac_serial_network_protocol;
+
+/**
+ * The parity scheme applied to the serial line.
+ */
+typedef enum guac_serial_parity {
+ GUAC_SERIAL_PARITY_NONE,
+ GUAC_SERIAL_PARITY_ODD,
+ GUAC_SERIAL_PARITY_EVEN,
+ GUAC_SERIAL_PARITY_MARK,
+ GUAC_SERIAL_PARITY_SPACE
+} guac_serial_parity;
+
+/**
+ * The flow control scheme applied to the serial line.
+ */
+typedef enum guac_serial_flow_control {
+ GUAC_SERIAL_FLOW_NONE,
+ GUAC_SERIAL_FLOW_RTS_CTS,
+ GUAC_SERIAL_FLOW_XON_XOFF
+} guac_serial_flow_control;
+
+/**
+ * Settings for the serial connection. The values for this structure are parsed
+ * from the arguments given during the Guacamole protocol handshake using the
+ * guac_serial_parse_args() function.
+ */
+typedef struct guac_serial_settings {
+
+ /**
+ * The transport used to reach the serial line.
+ */
+ guac_serial_type type;
+
+ /**
+ * The network protocol used when type is GUAC_SERIAL_TYPE_NETWORK.
+ */
+ guac_serial_network_protocol network_protocol;
+
+ /**
+ * The absolute path of the local serial device to open when type is
+ * GUAC_SERIAL_TYPE_LOCAL, e.g. "/dev/ttyUSB0". NULL if not a local
+ * connection.
+ */
+ char* device;
+
+ /**
+ * The hostname of the network serial server to connect to when type is
+ * GUAC_SERIAL_TYPE_NETWORK. NULL if not a network connection.
+ */
+ char* hostname;
+
+ /**
+ * The port of the network serial server to connect to when type is
+ * GUAC_SERIAL_TYPE_NETWORK.
+ */
+ char* port;
+
+ /**
+ * The number of seconds to wait for a connection before timing out.
+ */
+ int timeout;
+
+ /**
+ * The serial line speed in bits per second (e.g. 9600, 115200).
+ */
+ int baud_rate;
+
+ /**
+ * The number of data bits per character (5, 6, 7, or 8).
+ */
+ int data_bits;
+
+ /**
+ * The number of stop bits per character (1 or 2).
+ */
+ int stop_bits;
+
+ /**
+ * The parity scheme applied to the serial line.
+ */
+ guac_serial_parity parity;
+
+ /**
+ * The flow control scheme applied to the serial line.
+ */
+ guac_serial_flow_control flow_control;
+
+ /**
+ * The duration, in milliseconds, that the transmit line is held low when
+ * sending a serial break.
+ */
+ int break_duration;
+
+ /**
+ * The delay, in milliseconds, inserted between each byte written to the
+ * serial line. Zero disables pacing entirely. Used to avoid FIFO overrun
+ * when pasting to a low-baud line with no flow control.
+ */
+ int paste_delay;
+
+ /**
+ * Whether this connection is read-only, and user input should be dropped.
+ */
+ bool read_only;
+
+ /**
+ * The maximum size of the scrollback buffer in rows.
+ */
+ int max_scrollback;
+
+ /**
+ * The name of the font to use for display rendering.
+ */
+ char* font_name;
+
+ /**
+ * The size of the font to use, in points.
+ */
+ int font_size;
+
+ /**
+ * The name of the color scheme to use.
+ */
+ char* color_scheme;
+
+ /**
+ * The desired width of the terminal display, in pixels.
+ */
+ int width;
+
+ /**
+ * The desired height of the terminal display, in pixels.
+ */
+ int height;
+
+ /**
+ * The desired screen resolution, in DPI.
+ */
+ int resolution;
+
+ /**
+ * The maximum number of bytes to allow within the clipboard.
+ */
+ int clipboard_buffer_size;
+
+ /**
+ * Whether outbound clipboard access should be blocked. If set, it will not
+ * be possible to copy data from the terminal to the client using the
+ * clipboard.
+ */
+ bool disable_copy;
+
+ /**
+ * Whether inbound clipboard access should be blocked. If set, it will not
+ * be possible to paste data from the client to the terminal using the
+ * clipboard.
+ */
+ bool disable_paste;
+
+ /**
+ * The path in which the typescript should be saved, if enabled. If no
+ * typescript should be saved, this will be NULL.
+ */
+ char* typescript_path;
+
+ /**
+ * The filename to use for the typescript, if enabled.
+ */
+ char* typescript_name;
+
+ /**
+ * Whether the typescript path should be automatically created if it does
+ * not already exist.
+ */
+ bool create_typescript_path;
+
+ /**
+ * Whether existing files should be appended to when creating a new
+ * typescript. Disabled by default.
+ */
+ bool typescript_write_existing;
+
+ /**
+ * The path in which the screen recording should be saved, if enabled. If
+ * no screen recording should be saved, this will be NULL.
+ */
+ char* recording_path;
+
+ /**
+ * The filename to use for the screen recording, if enabled.
+ */
+ char* recording_name;
+
+ /**
+ * Whether the screen recording path should be automatically created if it
+ * does not already exist.
+ */
+ bool create_recording_path;
+
+ /**
+ * Whether output which is broadcast to each connected client (graphics,
+ * streams, etc.) should NOT be included in the session recording. Output
+ * is included by default, as it is necessary for any recording which must
+ * later be viewable as video.
+ */
+ bool recording_exclude_output;
+
+ /**
+ * Whether changes to mouse state, such as position and buttons pressed or
+ * released, should NOT be included in the session recording. Mouse state
+ * is included by default, as it is necessary for the mouse cursor to be
+ * rendered in any resulting video.
+ */
+ bool recording_exclude_mouse;
+
+ /**
+ * Whether keys pressed and released should be included in the session
+ * recording. Key events are NOT included by default within the recording,
+ * as doing so has privacy and security implications. Including key events
+ * may be necessary in certain auditing contexts, but should only be done
+ * with caution. Key events can easily contain sensitive information, such
+ * as passwords, credit card numbers, etc.
+ */
+ bool recording_include_keys;
+
+ /**
+ * Whether clipboard data should be included in the session recording.
+ * Clipboard data is NOT included by default within the recording,
+ * as doing so has privacy and security implications. Including clipboard
+ * data may be necessary in certain auditing contexts, but should only be
+ * done with caution. Clipboard data can easily contain sensitive
+ * information, such as passwords, credit card numbers, etc.
+ */
+ bool recording_include_clipboard;
+
+ /**
+ * Whether existing files should be appended to when creating a new
+ * recording. Disabled by default.
+ */
+ bool recording_write_existing;
+
+ /**
+ * The ASCII code, as an integer, that the terminal will use when the
+ * backspace key is pressed. By default, this is 127, ASCII delete, if
+ * not specified in the client settings.
+ */
+ int backspace;
+
+ /**
+ * The terminal emulator type that is used for rendering.
+ */
+ char* terminal_type;
+
+} guac_serial_settings;
+
+/**
+ * Parses all given args, storing them in a newly-allocated settings object. If
+ * the args fail to parse, NULL is returned.
+ *
+ * @param user
+ * The user who submitted the given arguments while joining the
+ * connection.
+ *
+ * @param argc
+ * The number of arguments within the argv array.
+ *
+ * @param argv
+ * The values of all arguments provided by the user.
+ *
+ * @return
+ * A newly-allocated settings object which must be freed with
+ * guac_serial_settings_free() when no longer needed. If the arguments fail
+ * to parse, NULL is returned.
+ */
+guac_serial_settings* guac_serial_parse_args(guac_user* user,
+ int argc, const char** argv);
+
+/**
+ * Maps the given serial line speed, in bits per second, to the corresponding
+ * termios speed_t constant (e.g. 9600 -> B9600).
+ *
+ * @param baud_rate
+ * The serial line speed, in bits per second.
+ *
+ * @return
+ * The termios speed_t constant corresponding to the given speed, or
+ * (speed_t) -1 if the given speed is not supported.
+ */
+speed_t guac_serial_baud_to_speed(int baud_rate);
+
+/**
+ * Frees the given guac_serial_settings object, having been previously
+ * allocated via guac_serial_parse_args().
+ *
+ * @param settings
+ * The settings object to free.
+ */
+void guac_serial_settings_free(guac_serial_settings* settings);
+
+/**
+ * NULL-terminated array of accepted client args.
+ */
+extern const char* GUAC_SERIAL_CLIENT_ARGS[];
+
+#endif
diff --git a/src/protocols/serial/stream.c b/src/protocols/serial/stream.c
new file mode 100644
index 00000000..1d189413
--- /dev/null
+++ b/src/protocols/serial/stream.c
@@ -0,0 +1,205 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#include "config.h"
+
+#include "local.h"
+#include "rfc2217.h"
+#include "settings.h"
+#include "stream.h"
+#include "tcp.h"
+
+#include <guacamole/client.h>
+#include <guacamole/error.h>
+#include <guacamole/mem.h>
+
+#include <errno.h>
+#include <string.h>
+#include <sys/ioctl.h>
+#include <termios.h>
+#include <unistd.h>
+
+/**
+ * Writes the entire buffer given to the specified file descriptor, retrying
+ * the write automatically if necessary.
+ *
+ * @param fd
+ * The file descriptor to write to.
+ *
+ * @param buffer
+ * The buffer to write.
+ *
+ * @param size
+ * The number of bytes from the buffer to write.
+ *
+ * @return
+ * The number of bytes written, or a value not equal to size if an error
+ * prevents all future writes.
+ */
+static int guac_serial_stream_write_all(int fd, const char* buffer, int size) {
+
+ int remaining = size;
+ while (remaining > 0) {
+
+ int ret_val;
+ GUAC_RETRY_EINTR(ret_val, write(fd, buffer, remaining));
+ if (ret_val <= 0)
+ return -1;
+
+ remaining -= ret_val;
+ buffer += ret_val;
+
+ }
+
+ return size;
+
+}
+
+guac_serial_stream* guac_serial_stream_open(guac_client* client,
+ guac_serial_settings* settings) {
+
+ guac_serial_stream* stream = guac_mem_zalloc(sizeof(guac_serial_stream));
+ stream->client = client;
+ stream->fd = -1;
+ stream->telnet = NULL;
+ stream->paste_delay = settings->paste_delay;
+ stream->break_duration = settings->break_duration;
+ pthread_mutex_init(&stream->write_lock, NULL);
+
+ /* Dispatch to the appropriate backend */
+ int result;
+ if (settings->type == GUAC_SERIAL_TYPE_LOCAL)
+ result = guac_serial_local_open(stream, client, settings);
+ else if (settings->network_protocol == GUAC_SERIAL_NETWORK_PROTOCOL_RFC2217)
+ result = guac_serial_rfc2217_open(stream, client, settings);
+ else
+ result = guac_serial_tcp_open(stream, client, settings);
+
+ /* Clean up and return NULL on failure (backend has already aborted) */
+ if (result != 0) {
+ pthread_mutex_destroy(&stream->write_lock);
+ guac_mem_free(stream);
+ return NULL;
+ }
+
+ return stream;
+
+}
+
+int guac_serial_stream_write(guac_serial_stream* stream, const char* buffer,
+ int length) {
+
+ int result = length;
+
+ pthread_mutex_lock(&stream->write_lock);
+
+ /* Pace output byte-by-byte when a paste delay is configured */
+ if (stream->paste_delay > 0) {
+
+ for (int i = 0; i < length; i++) {
+
+ if (stream->backend == GUAC_SERIAL_BACKEND_RFC2217)
+ guac_serial_rfc2217_send(stream, buffer + i, 1);
+ else if (guac_serial_stream_write_all(stream->fd, buffer + i, 1) != 1) {
+ result = -1;
+ break;
+ }
+
+ usleep((useconds_t) stream->paste_delay * 1000);
+
+ }
+
+ }
+
+ /* Otherwise, write the entire buffer at once */
+ else {
+
+ if (stream->backend == GUAC_SERIAL_BACKEND_RFC2217)
+ guac_serial_rfc2217_send(stream, buffer, length);
+ else if (guac_serial_stream_write_all(stream->fd, buffer, length) != length)
+ result = -1;
+
+ }
+
+ pthread_mutex_unlock(&stream->write_lock);
+ return result;
+
+}
+
+int guac_serial_stream_send_break(guac_serial_stream* stream) {
+
+ int result = 0;
+
+ pthread_mutex_lock(&stream->write_lock);
+
+ switch (stream->backend) {
+
+ case GUAC_SERIAL_BACKEND_LOCAL:
+#if defined(TIOCSBRK) && defined(TIOCCBRK)
+ /* Hold the transmit line low for the configured duration */
+ if (ioctl(stream->fd, TIOCSBRK) == 0) {
+ if (stream->break_duration > 0)
+ usleep((useconds_t) stream->break_duration * 1000);
+ ioctl(stream->fd, TIOCCBRK);
+ }
+
+ /* Fall back to tcsendbreak() if TIOCSBRK is unsupported */
+ else
+ tcsendbreak(stream->fd, 0);
+#else
+ tcsendbreak(stream->fd, 0);
+#endif
+ break;
+
+ case GUAC_SERIAL_BACKEND_TCP:
+ guac_client_log(stream->client, GUAC_LOG_WARNING, "Sending a serial "
+ "break is not supported on the raw TCP transport; use "
+ "rfc2217 instead.");
+ break;
+
+ case GUAC_SERIAL_BACKEND_RFC2217:
+ guac_serial_rfc2217_send_break(stream);
+ break;
+
+ }
+
+ pthread_mutex_unlock(&stream->write_lock);
+ return result;
+
+}
+
+void guac_serial_stream_close(guac_serial_stream* stream) {
+
+ /* Nothing to do if never opened */
+ if (stream == NULL)
+ return;
+
+ /* Free the libtelnet session, if any */
+ guac_serial_rfc2217_free(stream);
+
+ /* Close the underlying file descriptor, if open */
+ if (stream->fd != -1) {
+ close(stream->fd);
+ stream->fd = -1;
+ }
+
+ pthread_mutex_destroy(&stream->write_lock);
+ guac_mem_free(stream);
+
+}
diff --git a/src/protocols/serial/stream.h b/src/protocols/serial/stream.h
new file mode 100644
index 00000000..b3c7c542
--- /dev/null
+++ b/src/protocols/serial/stream.h
@@ -0,0 +1,168 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#ifndef GUAC_SERIAL_STREAM_H
+#define GUAC_SERIAL_STREAM_H
+
+#include "settings.h"
+
+#include <guacamole/client.h>
+
+#include <pthread.h>
+
+/**
+ * The transport backend providing the serial byte stream.
+ */
+typedef enum guac_serial_backend {
+
+ /**
+ * A local serial device opened directly via a device node.
+ */
+ GUAC_SERIAL_BACKEND_LOCAL,
+
+ /**
+ * A raw TCP connection to a remote serial server (e.g. ser2net in raw
+ * mode).
+ */
+ GUAC_SERIAL_BACKEND_TCP,
+
+ /**
+ * An RFC2217 (telnet COM-PORT-OPTION) connection to a remote serial server.
+ */
+ GUAC_SERIAL_BACKEND_RFC2217
+
+} guac_serial_backend;
+
+/**
+ * A transport-neutral, bidirectional serial byte stream. All reads are
+ * performed directly on the file descriptor by the main worker thread; writes
+ * and break signalling are dispatched to the appropriate backend and
+ * serialized with an internal lock.
+ */
+typedef struct guac_serial_stream {
+
+ /**
+ * The transport backend providing this stream.
+ */
+ guac_serial_backend backend;
+
+ /**
+ * The device file descriptor (local) or socket file descriptor (tcp,
+ * rfc2217), or -1 if not open.
+ */
+ int fd;
+
+ /**
+ * The libtelnet session (telnet_t*) used for IAC framing when the backend
+ * is GUAC_SERIAL_BACKEND_RFC2217, or NULL otherwise. Declared as an opaque
+ * pointer so this header does not depend on libtelnet, which is required
+ * only by the RFC2217 backend (see rfc2217.c). Only the RFC2217 backend
+ * dereferences this value.
+ */
+ void* telnet;
+
+ /**
+ * Lock serializing writes and break signalling on this stream.
+ */
+ pthread_mutex_t write_lock;
+
+ /**
+ * The delay, in milliseconds, inserted between each byte written to the
+ * serial line. Zero disables pacing. Copied from settings.
+ */
+ int paste_delay;
+
+ /**
+ * The duration, in milliseconds, that the transmit line is held low when
+ * sending a serial break. Copied from settings.
+ */
+ int break_duration;
+
+ /**
+ * The client associated with this stream, used for logging.
+ */
+ guac_client* client;
+
+} guac_serial_stream;
+
+/**
+ * Opens a serial byte stream using the transport described by the given
+ * settings, dispatching to the local, TCP, or RFC2217 backend as appropriate.
+ * On failure, the client is aborted and NULL is returned.
+ *
+ * @param client
+ * The client for which the stream is being opened.
+ *
+ * @param settings
+ * The parsed connection settings describing the transport to use.
+ *
+ * @return
+ * A newly-allocated serial stream which must be freed with
+ * guac_serial_stream_close(), or NULL if the connection could not be
+ * established.
+ */
+guac_serial_stream* guac_serial_stream_open(guac_client* client,
+ guac_serial_settings* settings);
+
+/**
+ * Writes the given buffer to the serial stream, blocking until the entire
+ * buffer has been written or an unrecoverable error occurs. If a non-zero
+ * paste delay is configured, bytes are written one at a time with the
+ * configured delay inserted between each.
+ *
+ * @param stream
+ * The serial stream to write to.
+ *
+ * @param buffer
+ * The buffer of data to write.
+ *
+ * @param length
+ * The number of bytes from the buffer to write.
+ *
+ * @return
+ * The number of bytes written on success, or a negative value if an error
+ * prevents all future writes.
+ */
+int guac_serial_stream_write(guac_serial_stream* stream, const char* buffer,
+ int length);
+
+/**
+ * Sends a serial break on the serial stream. For local and RFC2217 transports,
+ * the transmit line is held low for the configured break duration. Raw TCP
+ * transports do not support break signalling; a warning is logged and the call
+ * is a no-op.
+ *
+ * @param stream
+ * The serial stream on which to send the break.
+ *
+ * @return
+ * Zero on success, non-zero on error.
+ */
+int guac_serial_stream_send_break(guac_serial_stream* stream);
+
+/**
+ * Closes the given serial stream, releasing all associated resources. This
+ * function is idempotent and safe to call with a NULL stream.
+ *
+ * @param stream
+ * The serial stream to close.
+ */
+void guac_serial_stream_close(guac_serial_stream* stream);
+
+#endif
diff --git a/src/protocols/serial/tcp.c b/src/protocols/serial/tcp.c
new file mode 100644
index 00000000..9f825456
--- /dev/null
+++ b/src/protocols/serial/tcp.c
@@ -0,0 +1,54 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#include "config.h"
+
+#include "settings.h"
+#include "stream.h"
+#include "tcp.h"
+
+#include <guacamole/client.h>
+#include <guacamole/protocol.h>
+#include <guacamole/tcp.h>
+
+#include <netinet/in.h>
+#include <netinet/tcp.h>
+
+int guac_serial_tcp_open(guac_serial_stream* stream, guac_client* client,
+ guac_serial_settings* settings) {
+
+ /* Connect to the remote serial server */
+ int fd = guac_tcp_connect(settings->hostname, settings->port,
+ settings->timeout);
+ if (fd < 0) {
+ guac_client_abort(client, GUAC_PROTOCOL_STATUS_UPSTREAM_NOT_FOUND,
+ "Unable to connect to serial server \"%s\" port \"%s\".",
+ settings->hostname, settings->port);
+ return -1;
+ }
+
+ /* Disable Nagle's algorithm so keystrokes are sent without delay */
+ int flag = 1;
+ setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &flag, sizeof(flag));
+
+ stream->backend = GUAC_SERIAL_BACKEND_TCP;
+ stream->fd = fd;
+ return 0;
+
+}
diff --git a/src/protocols/serial/tcp.h b/src/protocols/serial/tcp.h
new file mode 100644
index 00000000..638dcefa
--- /dev/null
+++ b/src/protocols/serial/tcp.h
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#ifndef GUAC_SERIAL_TCP_H
+#define GUAC_SERIAL_TCP_H
+
+#include "settings.h"
+#include "stream.h"
+
+#include <guacamole/client.h>
+
+/**
+ * Opens a raw TCP connection to the network serial server described by the
+ * given settings (e.g. ser2net operating in raw mode) and stores the resulting
+ * socket in the given stream. On failure, the client is aborted.
+ *
+ * @param stream
+ * The serial stream to populate with the opened socket.
+ *
+ * @param client
+ * The client for which the connection is being opened.
+ *
+ * @param settings
+ * The parsed connection settings describing the server to connect to.
+ *
+ * @return
+ * Zero on success, non-zero on failure.
+ */
+int guac_serial_tcp_open(guac_serial_stream* stream, guac_client* client,
+ guac_serial_settings* settings);
+
+#endif
diff --git a/src/protocols/serial/user.c b/src/protocols/serial/user.c
new file mode 100644
index 00000000..2dce7dc4
--- /dev/null
+++ b/src/protocols/serial/user.c
@@ -0,0 +1,118 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#include "config.h"
+
+#include "argv.h"
+#include "clipboard.h"
+#include "input.h"
+#include "pipe.h"
+#include "serial.h"
+#include "settings.h"
+#include "terminal/terminal.h"
+#include "user.h"
+
+#include <guacamole/argv.h>
+#include <guacamole/client.h>
+#include <guacamole/socket.h>
+#include <guacamole/user.h>
+
+#include <pthread.h>
+#include <string.h>
+
+int guac_serial_user_join_handler(guac_user* user, int argc, char** argv) {
+
+ guac_client* client = user->client;
+ guac_serial_client* serial_client = (guac_serial_client*) client->data;
+
+ /* Parse provided arguments */
+ guac_serial_settings* settings = guac_serial_parse_args(user,
+ argc, (const char**) argv);
+
+ /* Fail if settings cannot be parsed */
+ if (settings == NULL) {
+ guac_user_log(user, GUAC_LOG_INFO,
+ "Badly formatted client arguments.");
+ return 1;
+ }
+
+ /* Store settings at user level */
+ user->data = settings;
+
+ /* Connect via serial if owner */
+ if (user->owner) {
+
+ /* Store owner's settings at client level */
+ serial_client->settings = settings;
+
+ /* Start client thread */
+ if (pthread_create(&(serial_client->client_thread), NULL,
+ guac_serial_client_thread, (void*) client)) {
+ guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR,
+ "Unable to start serial client thread");
+ return 1;
+ }
+
+ /* Thread now owns cleanup responsibilities and must be joined */
+ serial_client->client_thread_valid = 1;
+
+ }
+
+ /* Only handle events if not read-only */
+ if (!settings->read_only) {
+
+ /* General mouse/keyboard events */
+ user->key_handler = guac_serial_user_key_handler;
+ user->mouse_handler = guac_serial_user_mouse_handler;
+
+ /* Inbound (client to server) clipboard transfer */
+ if (!settings->disable_paste)
+ user->clipboard_handler = guac_serial_clipboard_handler;
+
+ /* STDIN redirection and out-of-band control channel */
+ user->pipe_handler = guac_serial_pipe_handler;
+
+ /* Updates to connection parameters */
+ user->argv_handler = guac_argv_handler;
+
+ /* Display size change events */
+ user->size_handler = guac_serial_user_size_handler;
+
+ }
+
+ return 0;
+
+}
+
+int guac_serial_user_leave_handler(guac_user* user) {
+
+ guac_serial_client* serial_client =
+ (guac_serial_client*) user->client->data;
+
+ /* Remove the user from the terminal */
+ guac_terminal_remove_user(serial_client->term, user);
+
+ /* Free settings if not owner (owner settings will be freed with client) */
+ if (!user->owner) {
+ guac_serial_settings* settings = (guac_serial_settings*) user->data;
+ guac_serial_settings_free(settings);
+ }
+
+ return 0;
+}
diff --git a/src/protocols/serial/user.h b/src/protocols/serial/user.h
new file mode 100644
index 00000000..2864af7f
--- /dev/null
+++ b/src/protocols/serial/user.h
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#ifndef GUAC_SERIAL_USER_H
+#define GUAC_SERIAL_USER_H
+
+#include <guacamole/user.h>
+
+/**
+ * Handler for joining users.
+ */
+guac_user_join_handler guac_serial_user_join_handler;
+
+/**
+ * Handler for leaving users.
+ */
+guac_user_leave_handler guac_serial_user_leave_handler;
+
+#endif