GUACAMOLE-1204: Merge addition of server-side support for multi-touch events.

diff --git a/src/common/common/recording.h b/src/common/common/recording.h
index b0278f0..e090f1d 100644
--- a/src/common/common/recording.h
+++ b/src/common/common/recording.h
@@ -72,6 +72,15 @@
     int include_mouse;
 
     /**
+     * Non-zero if multi-touch events should be included in the session
+     * recording, zero otherwise. Depending on whether the remote desktop will
+     * automatically provide graphical feedback for touches, including touch
+     * events may be necessary for multi-touch interactions to be rendered in
+     * any resulting video.
+     */
+    int include_touch;
+
+    /**
      * Non-zero if keys pressed and released should be included in the session
      * recording, zero otherwise. Including key events within the recording may
      * be necessary in certain auditing contexts, but should only be done with
@@ -119,6 +128,13 @@
  *     otherwise. Including mouse state is necessary for the mouse cursor to be
  *     rendered in any resulting video.
  *
+ * @param include_touch
+ *     Non-zero if touch events should be included in the session recording,
+ *     zero otherwise. Depending on whether the remote desktop will
+ *     automatically provide graphical feedback for touches, including touch
+ *     events may be necessary for multi-touch interactions to be rendered in
+ *     any resulting video.
+ *
  * @param include_keys
  *     Non-zero if keys pressed and released should be included in the session
  *     recording, zero otherwise. Including key events within the recording may
@@ -133,7 +149,8 @@
  */
 guac_common_recording* guac_common_recording_create(guac_client* client,
         const char* path, const char* name, int create_path,
-        int include_output, int include_mouse, int include_keys);
+        int include_output, int include_mouse, int include_touch,
+        int include_keys);
 
 /**
  * Frees the resources associated with the given in-progress recording. Note
@@ -175,6 +192,44 @@
         int x, int y, int button_mask);
 
 /**
+ * Reports the current state of a touch contact within the recording.
+ *
+ * @param recording
+ *     The guac_common_recording associated with the touch contact that
+ *     has changed state.
+ *
+ * @param id
+ *     An arbitrary integer ID which uniquely identifies this contact relative
+ *     to other active contacts.
+ *
+ * @param x
+ *     The X coordinate of the center of the touch contact.
+ *
+ * @param y
+ *     The Y coordinate of the center of the touch contact.
+ *
+ * @param x_radius
+ *     The X radius of the ellipse covering the general area of the touch
+ *     contact, in pixels.
+ *
+ * @param y_radius
+ *     The Y radius of the ellipse covering the general area of the touch
+ *     contact, in pixels.
+ *
+ * @param angle
+ *     The rough angle of clockwise rotation of the general area of the touch
+ *     contact, in degrees.
+ *
+ * @param force
+ *     The relative force exerted by the touch contact, where 0 is no force
+ *     (the touch has been lifted) and 1 is maximum force (the maximum amount
+ *     of force representable by the device).
+ */
+void guac_common_recording_report_touch(guac_common_recording* recording,
+        int id, int x, int y, int x_radius, int y_radius,
+        double angle, double force);
+
+/**
  * Reports a change in the state of an individual key within the recording.
  *
  * @param recording
diff --git a/src/common/common/surface.h b/src/common/common/surface.h
index c778883..ca8b310 100644
--- a/src/common/common/surface.h
+++ b/src/common/common/surface.h
@@ -121,6 +121,12 @@
     guac_socket* socket;
 
     /**
+     * The number of simultaneous touches that this surface can accept, where 0
+     * indicates that the surface does not support touch events at all.
+     */
+    int touches;
+
+    /**
      * The X coordinate of the upper-left corner of this layer, in pixels,
      * relative to its parent layer. This is only applicable to visible
      * (non-buffer) layers which are not the default layer.
@@ -486,5 +492,23 @@
 void guac_common_surface_dup(guac_common_surface* surface, guac_user* user,
         guac_socket* socket);
 
+/**
+ * Declares that the given surface should receive touch events. By default,
+ * surfaces are assumed to not expect touch events. This value is advisory, and
+ * the client is not required to honor the declared level of touch support.
+ * Implementations are expected to safely handle or ignore any received touch
+ * events, regardless of the level of touch support declared.  regardless of
+ * the level of touch support declared.
+ *
+ * @param surface
+ *     The surface to modify.
+ *
+ * @param touches
+ *     The number of simultaneous touches that this surface can accept, where 0
+ *     indicates that the surface does not support touch events at all.
+ */
+void guac_common_surface_set_multitouch(guac_common_surface* surface,
+        int touches);
+
 #endif
 
diff --git a/src/common/recording.c b/src/common/recording.c
index b4ad219..01a212b 100644
--- a/src/common/recording.c
+++ b/src/common/recording.c
@@ -137,7 +137,8 @@
 
 guac_common_recording* guac_common_recording_create(guac_client* client,
         const char* path, const char* name, int create_path,
-        int include_output, int include_mouse, int include_keys) {
+        int include_output, int include_mouse, int include_touch,
+        int include_keys) {
 
     char filename[GUAC_COMMON_RECORDING_MAX_NAME_LENGTH];
 
@@ -165,6 +166,7 @@
     recording->socket = guac_socket_open(fd);
     recording->include_output = include_output;
     recording->include_mouse = include_mouse;
+    recording->include_touch = include_touch;
     recording->include_keys = include_keys;
 
     /* Replace client socket with wrapped recording socket only if including
@@ -203,6 +205,17 @@
 
 }
 
+void guac_common_recording_report_touch(guac_common_recording* recording,
+        int id, int x, int y, int x_radius, int y_radius,
+        double angle, double force) {
+
+    /* Report touches only if recording should contain touch events */
+    if (recording->include_touch)
+        guac_protocol_send_touch(recording->socket, id, x, y,
+                x_radius, y_radius, angle, force, guac_timestamp_current());
+
+}
+
 void guac_common_recording_report_key(guac_common_recording* recording,
         int keysym, int pressed) {
 
diff --git a/src/common/surface.c b/src/common/surface.c
index c86ca80..183ae11 100644
--- a/src/common/surface.c
+++ b/src/common/surface.c
@@ -103,6 +103,19 @@
  */
 #define GUAC_SURFACE_WEBP_BLOCK_SIZE 8
 
+void guac_common_surface_set_multitouch(guac_common_surface* surface,
+        int touches) {
+
+    pthread_mutex_lock(&surface->_lock);
+
+    surface->touches = touches;
+    guac_protocol_send_set_int(surface->socket, surface->layer,
+            GUAC_PROTOCOL_LAYER_PARAMETER_MULTI_TOUCH, touches);
+
+    pthread_mutex_unlock(&surface->_lock);
+
+}
+
 void guac_common_surface_move(guac_common_surface* surface, int x, int y) {
 
     pthread_mutex_lock(&surface->_lock);
@@ -1981,6 +1994,11 @@
         guac_protocol_send_move(socket, surface->layer,
                 surface->parent, surface->x, surface->y, surface->z);
 
+        /* Synchronize multi-touch support level */
+        guac_protocol_send_set_int(surface->socket, surface->layer,
+                GUAC_PROTOCOL_LAYER_PARAMETER_MULTI_TOUCH,
+                surface->touches);
+
     }
 
     /* Sync size to new socket */
diff --git a/src/libguac/guacamole/protocol-constants.h b/src/libguac/guacamole/protocol-constants.h
index 5bd91b0..13d7f68 100644
--- a/src/libguac/guacamole/protocol-constants.h
+++ b/src/libguac/guacamole/protocol-constants.h
@@ -49,5 +49,20 @@
  */
 #define GUAC_PROTOCOL_BLOB_MAX_LENGTH 6048
 
+/**
+ * The name of the layer parameter defining the number of simultaneous points
+ * of contact supported by a layer. This parameter should be set to a non-zero
+ * value if the associated layer should receive touch events ("touch"
+ * instructions).
+ *
+ * This value specified for this parameter is advisory, and the client is not
+ * required to honor the declared level of touch support.  Implementations are
+ * expected to safely handle or ignore any received touch events, regardless of
+ * the level of touch support declared.
+ *
+ * @see guac_protocol_send_set_int()
+ */
+#define GUAC_PROTOCOL_LAYER_PARAMETER_MULTI_TOUCH "multi-touch"
+
 #endif
 
diff --git a/src/libguac/guacamole/protocol.h b/src/libguac/guacamole/protocol.h
index c6dbed2..362351c 100644
--- a/src/libguac/guacamole/protocol.h
+++ b/src/libguac/guacamole/protocol.h
@@ -210,6 +210,53 @@
         int button_mask, guac_timestamp timestamp);
 
 /**
+ * Sends a touch instruction over the given guac_socket connection.
+ *
+ * If an error occurs sending the instruction, a non-zero value is
+ * returned, and guac_error is set appropriately.
+ *
+ * @param socket
+ *     The guac_socket connection to use.
+ *
+ * @param id
+ *     An arbitrary integer ID which uniquely identifies this contact relative
+ *     to other active contacts.
+ *
+ * @param x
+ *     The X coordinate of the center of the touch contact.
+ *
+ * @param y
+ *     The Y coordinate of the center of the touch contact.
+ *
+ * @param x_radius
+ *     The X radius of the ellipse covering the general area of the touch
+ *     contact, in pixels.
+ *
+ * @param y_radius
+ *     The Y radius of the ellipse covering the general area of the touch
+ *     contact, in pixels.
+ *
+ * @param angle
+ *     The rough angle of clockwise rotation of the general area of the touch
+ *     contact, in degrees.
+ *
+ * @param force
+ *     The relative force exerted by the touch contact, where 0 is no force
+ *     (the touch has been lifted) and 1 is maximum force (the maximum amount
+ *     of force representable by the device).
+ *
+ * @param timestamp
+ *     The server timestamp (in milliseconds) at the point in time this touch
+ *     event was acknowledged.
+ *
+ * @return
+ *     Zero on success, non-zero on error.
+ */
+int guac_protocol_send_touch(guac_socket* socket, int id, int x, int y,
+        int x_radius, int y_radius, double angle, double force,
+        guac_timestamp timestamp);
+
+/**
  * Sends a nest instruction over the given guac_socket connection.
  *
  * If an error occurs sending the instruction, a non-zero value is
@@ -272,6 +319,32 @@
         const char* name, const char* value);
 
 /**
+ * Sends a set instruction over the given guac_socket connection. This function
+ * behavies identically to guac_protocol_send_set() except that the provided
+ * parameter value is an integer, rather than a string.
+ *
+ * If an error occurs sending the instruction, a non-zero value is
+ * returned, and guac_error is set appropriately.
+ *
+ * @param socket
+ *     The guac_socket connection to use.
+ *
+ * @param layer
+ *     The layer to set the parameter of.
+ *
+ * @param name
+ *     The name of the parameter to set.
+ *
+ * @param value
+ *     The value to set the parameter to.
+ *
+ * @return
+ *     Zero on success, non-zero on error.
+ */
+int guac_protocol_send_set_int(guac_socket* socket, const guac_layer* layer,
+        const char* name, int value);
+
+/**
  * Sends a select instruction over the given guac_socket connection.
  *
  * If an error occurs sending the instruction, a non-zero value is
diff --git a/src/libguac/guacamole/user-fntypes.h b/src/libguac/guacamole/user-fntypes.h
index d868932..95f099e 100644
--- a/src/libguac/guacamole/user-fntypes.h
+++ b/src/libguac/guacamole/user-fntypes.h
@@ -96,6 +96,51 @@
         int button_mask);
 
 /**
+ * Handler for Guacamole touch events, invoked when a "touch" instruction has
+ * been received from a user.
+ *
+ * @param user
+ *     The user that sent the touch event.
+ *
+ * @param id
+ *     An arbitrary integer ID which uniquely identifies this contact relative
+ *     to other active contacts.
+ *
+ * @param x
+ *     The X coordinate of the center of the touch contact within the display
+ *     when the event occurred, in pixels. This value is not guaranteed to be
+ *     within the bounds of the display area.
+ *
+ * @param y
+ *     The Y coordinate of the center of the touch contact within the display
+ *     when the event occurred, in pixels. This value is not guaranteed to be
+ *     within the bounds of the display area.
+ *
+ * @param x_radius
+ *     The X radius of the ellipse covering the general area of the touch
+ *     contact, in pixels.
+ *
+ * @param y_radius
+ *     The Y radius of the ellipse covering the general area of the touch
+ *     contact, in pixels.
+ *
+ * @param angle
+ *     The rough angle of clockwise rotation of the general area of the touch
+ *     contact, in degrees.
+ *
+ * @param force
+ *     The relative force exerted by the touch contact, where 0 is no force
+ *     (the touch has been lifted) and 1 is maximum force (the maximum amount
+ *     of force representable by the device).
+ *
+ * @return
+ *     Zero if the touch event was handled successfully, or non-zero if an
+ *     error occurred.
+ */
+typedef int guac_user_touch_handler(guac_user* user, int id, int x, int y,
+        int x_radius, int y_radius, double angle, double force);
+
+/**
  * Handler for Guacamole key events, invoked when a "key" event has been
  * received from a user.
  *
diff --git a/src/libguac/guacamole/user.h b/src/libguac/guacamole/user.h
index de04d2a..963dbe6 100644
--- a/src/libguac/guacamole/user.h
+++ b/src/libguac/guacamole/user.h
@@ -509,6 +509,27 @@
      */
     guac_user_argv_handler* argv_handler;
 
+    /**
+     * Handler for touch events sent by the Guacamole web-client.
+     *
+     * The handler takes the integer X and Y coordinates representing the
+     * center of the touch contact, as well as several parameters describing
+     * the general shape of the contact area. The force parameter indicates the
+     * amount of force exerted by the contact, including whether the contact
+     * has been lifted.
+     *
+     * Example:
+     * @code
+     *     int touch_handler(guac_user* user, int id, int x, int y,
+     *             int x_radius, int y_radius, double angle, double force);
+     *
+     *     int guac_user_init(guac_user* user, int argc, char** argv) {
+     *         user->touch_handler = touch_handler;
+     *     }
+     * @endcode
+     */
+    guac_user_touch_handler* touch_handler;
+
 };
 
 /**
diff --git a/src/libguac/protocol.c b/src/libguac/protocol.c
index 59a46d5..1c53c20 100644
--- a/src/libguac/protocol.c
+++ b/src/libguac/protocol.c
@@ -820,6 +820,37 @@
 
 }
 
+int guac_protocol_send_touch(guac_socket* socket, int id, int x, int y,
+        int x_radius, int y_radius, double angle, double force,
+        guac_timestamp timestamp) {
+
+    int ret_val;
+
+    guac_socket_instruction_begin(socket);
+    ret_val =
+           guac_socket_write_string(socket, "5.touch,")
+        || __guac_socket_write_length_int(socket, id)
+        || guac_socket_write_string(socket, ",")
+        || __guac_socket_write_length_int(socket, x)
+        || guac_socket_write_string(socket, ",")
+        || __guac_socket_write_length_int(socket, y)
+        || guac_socket_write_string(socket, ",")
+        || __guac_socket_write_length_int(socket, x_radius)
+        || guac_socket_write_string(socket, ",")
+        || __guac_socket_write_length_int(socket, y_radius)
+        || guac_socket_write_string(socket, ",")
+        || __guac_socket_write_length_double(socket, angle)
+        || guac_socket_write_string(socket, ",")
+        || __guac_socket_write_length_double(socket, force)
+        || guac_socket_write_string(socket, ",")
+        || __guac_socket_write_length_int(socket, timestamp)
+        || guac_socket_write_string(socket, ";");
+
+    guac_socket_instruction_end(socket);
+    return ret_val;
+
+}
+
 int guac_protocol_send_move(guac_socket* socket, const guac_layer* layer,
         const guac_layer* parent, int x, int y, int z) {
 
@@ -1057,6 +1088,26 @@
 
 }
 
+int guac_protocol_send_set_int(guac_socket* socket, const guac_layer* layer,
+        const char* name, int value) {
+
+    int ret_val;
+
+    guac_socket_instruction_begin(socket);
+    ret_val =
+           guac_socket_write_string(socket, "3.set,")
+        || __guac_socket_write_length_int(socket, layer->index)
+        || guac_socket_write_string(socket, ",")
+        || __guac_socket_write_length_string(socket, name)
+        || guac_socket_write_string(socket, ",")
+        || __guac_socket_write_length_int(socket, value)
+        || guac_socket_write_string(socket, ";");
+
+    guac_socket_instruction_end(socket);
+    return ret_val;
+
+}
+
 int guac_protocol_send_select(guac_socket* socket, const char* protocol) {
 
     int ret_val;
diff --git a/src/libguac/user-handlers.c b/src/libguac/user-handlers.c
index f64fd29..c5cad9d 100644
--- a/src/libguac/user-handlers.c
+++ b/src/libguac/user-handlers.c
@@ -37,6 +37,7 @@
 
 __guac_instruction_handler_mapping __guac_instruction_handler_map[] = {
    {"sync",       __guac_handle_sync},
+   {"touch",      __guac_handle_touch},
    {"mouse",      __guac_handle_mouse},
    {"key",        __guac_handle_key},
    {"clipboard",  __guac_handle_clipboard},
@@ -150,6 +151,21 @@
     return 0;
 }
 
+int __guac_handle_touch(guac_user* user, int argc, char** argv) {
+    if (user->touch_handler)
+        return user->touch_handler(
+            user,
+            atoi(argv[0]), /* id */
+            atoi(argv[1]), /* x */
+            atoi(argv[2]), /* y */
+            atoi(argv[3]), /* x_radius */
+            atoi(argv[4]), /* y_radius */
+            atof(argv[5]), /* angle */
+            atof(argv[6])  /* force */
+        );
+    return 0;
+}
+
 int __guac_handle_mouse(guac_user* user, int argc, char** argv) {
     if (user->mouse_handler)
         return user->mouse_handler(
diff --git a/src/libguac/user-handlers.h b/src/libguac/user-handlers.h
index 263c292..a51a3f8 100644
--- a/src/libguac/user-handlers.h
+++ b/src/libguac/user-handlers.h
@@ -86,6 +86,13 @@
 __guac_instruction_handler __guac_handle_mouse;
 
 /**
+ * Internal initial handler for the touch instruction. When a touch instruction
+ * is received, this handler will be called. The client's touch handler will
+ * be invoked if defined.
+ */
+__guac_instruction_handler __guac_handle_touch;
+
+/**
  * Internal initial handler for the key instruction. When a key instruction
  * is received, this handler will be called. The client's key handler will
  * be invoked if defined.
diff --git a/src/protocols/kubernetes/kubernetes.c b/src/protocols/kubernetes/kubernetes.c
index 7158a98..1998d13 100644
--- a/src/protocols/kubernetes/kubernetes.c
+++ b/src/protocols/kubernetes/kubernetes.c
@@ -234,6 +234,7 @@
                 settings->create_recording_path,
                 !settings->recording_exclude_output,
                 !settings->recording_exclude_mouse,
+                0, /* Touch events not supported */
                 settings->recording_include_keys);
     }
 
diff --git a/src/protocols/rdp/Makefile.am b/src/protocols/rdp/Makefile.am
index e8ddfeb..2612bdf 100644
--- a/src/protocols/rdp/Makefile.am
+++ b/src/protocols/rdp/Makefile.am
@@ -56,6 +56,7 @@
     channels/rdpdr/rdpdr-messages.c              \
     channels/rdpdr/rdpdr-printer.c               \
     channels/rdpdr/rdpdr.c                       \
+    channels/rdpei.c                             \
     channels/rdpsnd/rdpsnd-messages.c            \
     channels/rdpsnd/rdpsnd.c                     \
     client.c                                     \
@@ -101,6 +102,7 @@
     channels/rdpdr/rdpdr-messages.h              \
     channels/rdpdr/rdpdr-printer.h               \
     channels/rdpdr/rdpdr.h                       \
+    channels/rdpei.h                             \
     channels/rdpsnd/rdpsnd-messages.h            \
     channels/rdpsnd/rdpsnd.h                     \
     client.h                                     \
diff --git a/src/protocols/rdp/channels/rdpei.c b/src/protocols/rdp/channels/rdpei.c
new file mode 100644
index 0000000..f1c5d09
--- /dev/null
+++ b/src/protocols/rdp/channels/rdpei.c
@@ -0,0 +1,170 @@
+/*
+ * 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 "channels/rdpei.h"
+#include "common/surface.h"
+#include "plugins/channels.h"
+#include "rdp.h"
+#include "settings.h"
+
+#include <freerdp/client/rdpei.h>
+#include <freerdp/freerdp.h>
+#include <freerdp/event.h>
+#include <guacamole/client.h>
+#include <guacamole/timestamp.h>
+
+#include <stdlib.h>
+#include <string.h>
+
+guac_rdp_rdpei* guac_rdp_rdpei_alloc() {
+
+    guac_rdp_rdpei* rdpei = malloc(sizeof(guac_rdp_rdpei));
+
+    /* Not yet connected */
+    rdpei->rdpei = NULL;
+
+    /* No active touches */
+    for (int i = 0; i < GUAC_RDP_RDPEI_MAX_TOUCHES; i++)
+        rdpei->touch[i].active = 0;
+
+    return rdpei;
+
+}
+
+void guac_rdp_rdpei_free(guac_rdp_rdpei* rdpei) {
+    free(rdpei);
+}
+
+/**
+ * Callback which associates handlers specific to Guacamole with the
+ * RdpeiClientContext instance allocated by FreeRDP to deal with received
+ * RDPEI (multi-touch input) messages.
+ *
+ * This function is called whenever a channel connects via the PubSub event
+ * system within FreeRDP, but only has any effect if the connected channel is
+ * the RDPEI channel. This specific callback is registered with the
+ * PubSub system of the relevant rdpContext when guac_rdp_rdpei_load_plugin() is
+ * called.
+ *
+ * @param context
+ *     The rdpContext associated with the active RDP session.
+ *
+ * @param e
+ *     Event-specific arguments, mainly the name of the channel, and a
+ *     reference to the associated plugin loaded for that channel by FreeRDP.
+ */
+static void guac_rdp_rdpei_channel_connected(rdpContext* context,
+        ChannelConnectedEventArgs* e) {
+
+    guac_client* client = ((rdp_freerdp_context*) context)->client;
+    guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
+    guac_rdp_rdpei* guac_rdpei = rdp_client->rdpei;
+
+    /* Ignore connection event if it's not for the RDPEI channel */
+    if (strcmp(e->name, RDPEI_DVC_CHANNEL_NAME) != 0)
+        return;
+
+    /* Store reference to the RDPEI plugin once it's connected */
+    RdpeiClientContext* rdpei = (RdpeiClientContext*) e->pInterface;
+    guac_rdpei->rdpei = rdpei;
+
+    /* Declare level of multi-touch support */
+    guac_common_surface_set_multitouch(rdp_client->display->default_surface,
+            GUAC_RDP_RDPEI_MAX_TOUCHES);
+
+    guac_client_log(client, GUAC_LOG_DEBUG, "RDPEI channel will be used for "
+            "multi-touch support.");
+
+}
+
+void guac_rdp_rdpei_load_plugin(rdpContext* context) {
+
+    /* Subscribe to and handle channel connected events */
+    PubSub_SubscribeChannelConnected(context->pubSub,
+        (pChannelConnectedEventHandler) guac_rdp_rdpei_channel_connected);
+
+    /* Add "rdpei" channel */
+    guac_freerdp_dynamic_channel_collection_add(context->settings, "rdpei", NULL);
+
+}
+
+int guac_rdp_rdpei_touch_update(guac_rdp_rdpei* rdpei, int id, int x, int y,
+        double force) {
+
+    int contact_id; /* Ignored */
+
+    /* Track touches only if channel is connected */
+    RdpeiClientContext* context = rdpei->rdpei;
+    if (context == NULL)
+        return 1;
+
+    /* Locate active touch having provided ID */
+    guac_rdp_rdpei_touch* touch = NULL;
+    for (int i = 0; i < GUAC_RDP_RDPEI_MAX_TOUCHES; i++) {
+        if (rdpei->touch[i].active && rdpei->touch[i].id == id) {
+            touch = &rdpei->touch[i];
+            break;
+        }
+    }
+
+    /* If no such touch exists, add it */
+    if (touch == NULL) {
+        for (int i = 0; i < GUAC_RDP_RDPEI_MAX_TOUCHES; i++) {
+            if (!rdpei->touch[i].active) {
+                touch = &rdpei->touch[i];
+                touch->id = id;
+                break;
+            }
+        }
+    }
+
+    /* If the touch couldn't be added, we're already at maximum touch capacity.
+     * Drop the event. */
+    if (touch == NULL)
+        return 1;
+
+    /* Signal the end of an established touch if touch force has become zero
+     * (this should be a safe comparison, as zero has an exact representation
+     * in floating point, and the client side will use an exact value to
+     * represent the absence of a touch) */
+    if (force == 0.0) {
+
+        /* Ignore release of touches that we aren't tracking */
+        if (!touch->active)
+            return 1;
+
+        context->TouchEnd(context, id, x, y, &contact_id);
+        touch->active = 0;
+
+    }
+
+    /* Signal the start of a touch if this is the first we've seen it */
+    else if (!touch->active) {
+        context->TouchBegin(context, id, x, y, &contact_id);
+        touch->active = 1;
+    }
+
+    /* Established touches need only be updated */
+    else
+        context->TouchUpdate(context, id, x, y, &contact_id);
+
+    return 0;
+
+}
+
diff --git a/src/protocols/rdp/channels/rdpei.h b/src/protocols/rdp/channels/rdpei.h
new file mode 100644
index 0000000..5ca10c3
--- /dev/null
+++ b/src/protocols/rdp/channels/rdpei.h
@@ -0,0 +1,154 @@
+/*
+ * 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_RDP_CHANNELS_RDPEI_H
+#define GUAC_RDP_CHANNELS_RDPEI_H
+
+#include "settings.h"
+
+#include <freerdp/client/rdpei.h>
+#include <freerdp/freerdp.h>
+#include <guacamole/client.h>
+#include <guacamole/timestamp.h>
+
+/**
+ * The maximum number of simultaneously-tracked touches.
+ */
+#define GUAC_RDP_RDPEI_MAX_TOUCHES 10
+
+/**
+ * A single, tracked touch contact.
+ */
+typedef struct guac_rdp_rdpei_touch {
+
+    /**
+     * Whether this touch is active (1) or inactive (0). An active touch is
+     * being tracked, while an inactive touch is simple an empty space awaiting
+     * use by some future touch event.
+     */
+    int active;
+
+    /**
+     * The unique ID representing this touch contact.
+     */
+    int id;
+
+    /**
+     * The X-coordinate of this touch, in pixels.
+     */
+    int x;
+
+    /**
+     * The Y-coordinate of this touch, in pixels.
+     */
+    int y;
+
+} guac_rdp_rdpei_touch;
+
+/**
+ * Multi-touch input module.
+ */
+typedef struct guac_rdp_rdpei {
+
+    /**
+     * RDPEI control interface.
+     */
+    RdpeiClientContext* rdpei;
+
+    /**
+     * All currently-tracked touches.
+     */
+    guac_rdp_rdpei_touch touch[GUAC_RDP_RDPEI_MAX_TOUCHES];
+
+} guac_rdp_rdpei;
+
+/**
+ * Allocates a new RDPEI module, which will ultimately control the RDPEI
+ * channel once connected. The RDPEI channel allows multi-touch input
+ * events to be sent to the RDP server.
+ *
+ * @return
+ *     A newly-allocated RDPEI module.
+ */
+guac_rdp_rdpei* guac_rdp_rdpei_alloc();
+
+/**
+ * Frees the resources associated with support for the RDPEI channel. Only
+ * resources specific to Guacamole are freed. Resources specific to FreeRDP's
+ * handling of the RDPEI channel will be freed by FreeRDP. If no resources are
+ * currently allocated for RDPEI, this function has no effect.
+ *
+ * @param rdpei
+ *     The RDPEI module to free.
+ */
+void guac_rdp_rdpei_free(guac_rdp_rdpei* rdpei);
+
+/**
+ * Adds FreeRDP's "rdpei" plugin to the list of dynamic virtual channel plugins
+ * to be loaded by FreeRDP's "drdynvc" plugin. The context of the plugin will
+ * automatically be assicated with the guac_rdp_rdpei instance pointed to by the
+ * current guac_rdp_client. The plugin will only be loaded once the "drdynvc"
+ * plugin is loaded. The "rdpei" plugin ultimately adds support for multi-touch
+ * input via the RDPEI channel.
+ *
+ * If failures occur, messages noting the specifics of those failures will be
+ * logged, and the RDP side of multi-touch support will not be functional.
+ *
+ * This MUST be called within the PreConnect callback of the freerdp instance
+ * for multi-touch support to be loaded.
+ *
+ * @param context
+ *     The rdpContext associated with the active RDP session.
+ */
+void guac_rdp_rdpei_load_plugin(rdpContext* context);
+
+/**
+ * Reports to the RDP server that the status of a single touch contact has
+ * changed. Depending on the amount of force associated with the touch and
+ * whether the touch has been encountered before, this will result a new touch
+ * contact, updates to an existing contact, or removal of an existing contact.
+ * If the RDPEI channel has not yet been connected, touches will be ignored and
+ * dropped until it is connected.
+ *
+ * @param rdpei
+ *     The RDPEI module associated with the RDP session.
+ *
+ * @param id
+ *     An arbitrary integer ID unique to the touch being updated.
+ *
+ * @param x
+ *     The X-coordinate of the touch, in pixels.
+ *
+ * @param y
+ *     The Y-coordinate of the touch, in pixels.
+ *
+ * @param force
+ *     The amount of force currently being exerted on the device by the touch
+ *     contact in question, where 1.0 is the maximum amount of force
+ *     representable and 0.0 indicates the contact has been lifted.
+ *
+ * @return
+ *     Zero if the touch event was successfully processed, non-zero if the
+ *     touch event had to be dropped.
+ */
+int guac_rdp_rdpei_touch_update(guac_rdp_rdpei* rdpei, int id, int x, int y,
+        double force);
+
+#endif
+
diff --git a/src/protocols/rdp/client.c b/src/protocols/rdp/client.c
index 07c90a3..50808dc 100644
--- a/src/protocols/rdp/client.c
+++ b/src/protocols/rdp/client.c
@@ -147,6 +147,9 @@
     /* Init display update module */
     rdp_client->disp = guac_rdp_disp_alloc();
 
+    /* Init multi-touch support module (RDPEI) */
+    rdp_client->rdpei = guac_rdp_rdpei_alloc();
+
     /* Redirect FreeRDP log messages to guac_client_log() */
     guac_rdp_redirect_wlog(client);
 
@@ -187,6 +190,9 @@
     /* Free display update module */
     guac_rdp_disp_free(rdp_client->disp);
 
+    /* Free multi-touch support module (RDPEI) */
+    guac_rdp_rdpei_free(rdp_client->rdpei);
+
     /* Clean up filesystem, if allocated */
     if (rdp_client->filesystem != NULL)
         guac_rdp_fs_free(rdp_client->filesystem);
diff --git a/src/protocols/rdp/input.c b/src/protocols/rdp/input.c
index cb9bb10..0c36d61 100644
--- a/src/protocols/rdp/input.c
+++ b/src/protocols/rdp/input.c
@@ -18,6 +18,7 @@
  */
 
 #include "channels/disp.h"
+#include "channels/rdpei.h"
 #include "common/cursor.h"
 #include "common/display.h"
 #include "common/recording.h"
@@ -122,6 +123,33 @@
     return 0;
 }
 
+int guac_rdp_user_touch_handler(guac_user* user, int id, int x, int y,
+        int x_radius, int y_radius, double angle, double force) {
+
+    guac_client* client = user->client;
+    guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
+
+    pthread_rwlock_rdlock(&(rdp_client->lock));
+
+    /* Skip if not yet connected */
+    freerdp* rdp_inst = rdp_client->rdp_inst;
+    if (rdp_inst == NULL)
+        goto complete;
+
+    /* Report touch event within recording */
+    if (rdp_client->recording != NULL)
+        guac_common_recording_report_touch(rdp_client->recording, id, x, y,
+                x_radius, y_radius, angle, force);
+
+    /* Forward touch event along RDPEI channel */
+    guac_rdp_rdpei_touch_update(rdp_client->rdpei, id, x, y, force);
+
+complete:
+    pthread_rwlock_unlock(&(rdp_client->lock));
+
+    return 0;
+}
+
 int guac_rdp_user_key_handler(guac_user* user, int keysym, int pressed) {
 
     guac_client* client = user->client;
diff --git a/src/protocols/rdp/input.h b/src/protocols/rdp/input.h
index 60ef064..eb9e482 100644
--- a/src/protocols/rdp/input.h
+++ b/src/protocols/rdp/input.h
@@ -28,6 +28,11 @@
 guac_user_mouse_handler guac_rdp_user_mouse_handler;
 
 /**
+ * Handler for Guacamole user touch events.
+ */
+guac_user_touch_handler guac_rdp_user_touch_handler;
+
+/**
  * Handler for Guacamole user key events.
  */
 guac_user_key_handler guac_rdp_user_key_handler;
diff --git a/src/protocols/rdp/rdp.c b/src/protocols/rdp/rdp.c
index ce6a279..db62bc4 100644
--- a/src/protocols/rdp/rdp.c
+++ b/src/protocols/rdp/rdp.c
@@ -27,6 +27,7 @@
 #include "channels/pipe-svc.h"
 #include "channels/rail.h"
 #include "channels/rdpdr/rdpdr.h"
+#include "channels/rdpei.h"
 #include "channels/rdpsnd/rdpsnd.h"
 #include "client.h"
 #include "color.h"
@@ -100,6 +101,10 @@
     if (settings->resize_method == GUAC_RESIZE_DISPLAY_UPDATE)
         guac_rdp_disp_load_plugin(context);
 
+    /* Load "rdpei" plugin for multi-touch support */
+    if (settings->enable_touch)
+        guac_rdp_rdpei_load_plugin(context);
+
     /* Load "AUDIO_INPUT" plugin for audio input*/
     if (settings->enable_audio_input) {
         rdp_client->audio_input = guac_rdp_audio_buffer_alloc();
@@ -421,6 +426,7 @@
                 settings->create_recording_path,
                 !settings->recording_exclude_output,
                 !settings->recording_exclude_mouse,
+                !settings->recording_exclude_touch,
                 settings->recording_include_keys);
     }
 
diff --git a/src/protocols/rdp/rdp.h b/src/protocols/rdp/rdp.h
index e65f702..cc53796 100644
--- a/src/protocols/rdp/rdp.h
+++ b/src/protocols/rdp/rdp.h
@@ -23,6 +23,7 @@
 #include "channels/audio-input/audio-buffer.h"
 #include "channels/cliprdr.h"
 #include "channels/disp.h"
+#include "channels/rdpei.h"
 #include "common/clipboard.h"
 #include "common/display.h"
 #include "common/list.h"
@@ -149,6 +150,11 @@
     guac_rdp_disp* disp;
 
     /**
+     * Multi-touch support module (RDPEI).
+     */
+    guac_rdp_rdpei* rdpei;
+
+    /**
      * List of all available static virtual channels.
      */
     guac_common_list* available_svc;
diff --git a/src/protocols/rdp/settings.c b/src/protocols/rdp/settings.c
index e12e8c1..f834a7f 100644
--- a/src/protocols/rdp/settings.c
+++ b/src/protocols/rdp/settings.c
@@ -104,10 +104,12 @@
     "recording-name",
     "recording-exclude-output",
     "recording-exclude-mouse",
+    "recording-exclude-touch",
     "recording-include-keys",
     "create-recording-path",
     "resize-method",
     "enable-audio-input",
+    "enable-touch",
     "read-only",
 
     "gateway-hostname",
@@ -500,6 +502,13 @@
     IDX_RECORDING_EXCLUDE_MOUSE,
 
     /**
+     * Whether changes to touch contact state should NOT be included in the
+     * session recording. Touch state is included by default, as it may be
+     * necessary for touch interactions to be rendered in any resulting video.
+     */
+    IDX_RECORDING_EXCLUDE_TOUCH,
+
+    /**
      * 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
@@ -528,6 +537,12 @@
     IDX_ENABLE_AUDIO_INPUT,
 
     /**
+     * "true" if multi-touch support should be enabled for the RDP connection,
+     * "false" or blank otherwise.
+     */
+    IDX_ENABLE_TOUCH,
+
+    /**
      * "true" if this connection should be read-only (user input should be
      * dropped), "false" or blank otherwise.
      */
@@ -1050,6 +1065,11 @@
         guac_user_parse_args_boolean(user, GUAC_RDP_CLIENT_ARGS, argv,
                 IDX_RECORDING_EXCLUDE_MOUSE, 0);
 
+    /* Parse touch exclusion flag */
+    settings->recording_exclude_touch =
+        guac_user_parse_args_boolean(user, GUAC_RDP_CLIENT_ARGS, argv,
+                IDX_RECORDING_EXCLUDE_TOUCH, 0);
+
     /* Parse key event inclusion flag */
     settings->recording_include_keys =
         guac_user_parse_args_boolean(user, GUAC_RDP_CLIENT_ARGS, argv,
@@ -1085,6 +1105,11 @@
         settings->resize_method = GUAC_RESIZE_NONE;
     }
 
+    /* Multi-touch input enable/disable */
+    settings->enable_touch =
+        guac_user_parse_args_boolean(user, GUAC_RDP_CLIENT_ARGS, argv,
+                IDX_ENABLE_TOUCH, 0);
+
     /* Audio input enable/disable */
     settings->enable_audio_input =
         guac_user_parse_args_boolean(user, GUAC_RDP_CLIENT_ARGS, argv,
diff --git a/src/protocols/rdp/settings.h b/src/protocols/rdp/settings.h
index 323ba5d..c540fcc 100644
--- a/src/protocols/rdp/settings.h
+++ b/src/protocols/rdp/settings.h
@@ -506,6 +506,14 @@
     int recording_exclude_mouse;
 
     /**
+     * Non-zero if changes to touch state should NOT be included in the session
+     * recording, zero otherwise. Touch state is included by default, as it may
+     * be necessary for touch interactions to be rendered in any resulting
+     * video.
+     */
+    int recording_exclude_touch;
+
+    /**
      * Non-zero if keys pressed and released should be included in the session
      * recording, zero otherwise. Key events are NOT included by default within
      * the recording, as doing so has privacy and security implications.
@@ -526,6 +534,11 @@
     int enable_audio_input;
 
     /**
+     * Whether multi-touch support is enabled.
+     */
+    int enable_touch;
+
+    /**
      * The hostname of the remote desktop gateway that should be used as an
      * intermediary for the remote desktop connection. If no gateway should
      * be used, this will be NULL.
diff --git a/src/protocols/rdp/user.c b/src/protocols/rdp/user.c
index 351c67e..992e551 100644
--- a/src/protocols/rdp/user.c
+++ b/src/protocols/rdp/user.c
@@ -105,6 +105,10 @@
         user->mouse_handler = guac_rdp_user_mouse_handler;
         user->key_handler = guac_rdp_user_key_handler;
 
+        /* Multi-touch events */
+        if (settings->enable_touch)
+            user->touch_handler = guac_rdp_user_touch_handler;
+
         /* Inbound (client to server) clipboard transfer */
         if (!settings->disable_paste)
             user->clipboard_handler = guac_rdp_clipboard_handler;
diff --git a/src/protocols/ssh/ssh.c b/src/protocols/ssh/ssh.c
index aaa5a8e..81fb085 100644
--- a/src/protocols/ssh/ssh.c
+++ b/src/protocols/ssh/ssh.c
@@ -234,6 +234,7 @@
                 settings->create_recording_path,
                 !settings->recording_exclude_output,
                 !settings->recording_exclude_mouse,
+                0, /* Touch events not supported */
                 settings->recording_include_keys);
     }
 
diff --git a/src/protocols/telnet/telnet.c b/src/protocols/telnet/telnet.c
index b2b3106..f6ea46b 100644
--- a/src/protocols/telnet/telnet.c
+++ b/src/protocols/telnet/telnet.c
@@ -581,6 +581,7 @@
                 settings->create_recording_path,
                 !settings->recording_exclude_output,
                 !settings->recording_exclude_mouse,
+                0, /* Touch events not supported */
                 settings->recording_include_keys);
     }
 
diff --git a/src/protocols/vnc/vnc.c b/src/protocols/vnc/vnc.c
index eadaa8b..ade8278 100644
--- a/src/protocols/vnc/vnc.c
+++ b/src/protocols/vnc/vnc.c
@@ -427,6 +427,7 @@
                 settings->create_recording_path,
                 !settings->recording_exclude_output,
                 !settings->recording_exclude_mouse,
+                0, /* Touch events not supported */
                 settings->recording_include_keys);
     }