Merge staging/1.1.0 changes back to master.
diff --git a/Dockerfile b/Dockerfile
index 094842c..83bce0f 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -37,9 +37,9 @@
 ARG BUILD_DEPENDENCIES="              \
         autoconf                      \
         automake                      \
+        freerdp2-dev                  \
         gcc                           \
         libcairo2-dev                 \
-        libfreerdp-dev                \
         libjpeg62-turbo-dev           \
         libossp-uuid-dev              \
         libpango1.0-dev               \
@@ -71,7 +71,7 @@
 RUN ${PREFIX_DIR}/bin/list-dependencies.sh    \
         ${PREFIX_DIR}/sbin/guacd              \
         ${PREFIX_DIR}/lib/libguac-client-*.so \
-        ${PREFIX_DIR}/lib/freerdp/guac*.so    \
+        ${PREFIX_DIR}/lib/freerdp2/guac*.so   \
         > ${PREFIX_DIR}/DEPENDENCIES
 
 # Use same Debian as the base for the runtime image
@@ -92,7 +92,6 @@
 ARG RUNTIME_DEPENDENCIES="            \
         ca-certificates               \
         ghostscript                   \
-        libfreerdp-plugins-standard   \
         fonts-liberation              \
         fonts-dejavu                  \
         xfonts-terminus"
@@ -108,7 +107,7 @@
 
 # Link FreeRDP plugins into proper path
 RUN ${PREFIX_DIR}/bin/link-freerdp-plugins.sh \
-        ${PREFIX_DIR}/lib/freerdp/guac*.so
+        ${PREFIX_DIR}/lib/freerdp2/libguac*.so
 
 # Expose the default listener port
 EXPOSE 4822
diff --git a/configure.ac b/configure.ac
index 7fc7d33..5b0fbe8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -555,537 +555,143 @@
 fi
 
 #
-# FreeRDP
+# FreeRDP 2 (libfreerdp2, libfreerdp-client2, and libwinpr2)
 #
 
-have_freerdp=disabled
-RDP_LIBS=
-WINPR_LIBS=
+have_freerdp2=disabled
 AC_ARG_WITH([rdp],
             [AS_HELP_STRING([--with-rdp],
                             [support RDP @<:@default=check@:>@])],
             [],
             [with_rdp=check])
 
+# Preserve CPPFLAGS so it can be restored later, following the addition of
+# options specific to FreeRDP tests
+OLDCPPFLAGS="$CPPFLAGS"
+
 if test "x$with_rdp" != "xno"
 then
-    have_winpr=yes
-    have_freerdp=yes
-    legacy_freerdp_extensions=no
-    rdpsettings_interface=unknown
-    rdpsettings_audiocapture=yes
-    rdpsettings_audioplayback=yes
-    rdpsettings_deviceredirection=yes
-    freerdp_interface=unknown
-    event_interface=unknown
-
-    # libfreerdp-core / libfreerdp
-    AC_CHECK_LIB([freerdp-core], [freerdp_new],
-                 [RDP_LIBS="$RDP_LIBS -lfreerdp-core"],
-                 [AC_CHECK_LIB([freerdp], [freerdp_new],
-                               [RDP_LIBS="$RDP_LIBS -lfreerdp -lfreerdp-client"],
-                               [AC_MSG_WARN([
+    have_freerdp2=yes
+    PKG_CHECK_MODULES([RDP], [freerdp2 freerdp-client2 winpr2],
+                      [CPPFLAGS="${RDP_CFLAGS} -Werror $CPPFLAGS"],
+                      [AC_MSG_WARN([
   --------------------------------------------
-   Unable to find libfreerdp-core / libfreerdp
+   Unable to find FreeRDP (libfreerdp2 / libfreerdp-client2 / libwinpr2)
    RDP will be disabled.
   --------------------------------------------])
-                  have_freerdp=no])])
-fi
-
-
-# libfreerdp-cache
-if test "x${have_freerdp}" = "xyes"
-then
-    AC_CHECK_LIB([freerdp-cache], [glyph_cache_register_callbacks],
-                 [RDP_LIBS="$RDP_LIBS -lfreerdp-cache"])
-fi
-
-# libfreerdp-channels (1.0) / libfreerdp-client + libfreerdp-core (1.1)
-if test "x${have_freerdp}" = "xyes"
-then
-    AC_CHECK_LIB([freerdp-client], [freerdp_channels_new],
-                 [RDP_LIBS="$RDP_LIBS -lfreerdp-client"],
-                 [AC_CHECK_LIB([freerdp-channels], [freerdp_channels_new],
-                               [RDP_LIBS="$RDP_LIBS -lfreerdp-channels"
-                               legacy_freerdp_extensions=yes])],
-                 [-lfreerdp-core])
-fi
-
-# libfreerdp-utils
-if test "x${have_freerdp}" = "xyes"
-then
-    AC_CHECK_LIB([freerdp-utils], [svc_plugin_init],
-                 [RDP_LIBS="$RDP_LIBS -lfreerdp-utils"])
-fi
-
-# libfreerdp-codec
-if test "x${have_freerdp}" = "xyes"
-then
-    AC_CHECK_LIB([freerdp-codec], [freerdp_image_convert],
-                 [RDP_LIBS="$RDP_LIBS -lfreerdp-codec"])
+                       have_freerdp2=no])
 fi
 
 # Available color conversion functions
-if test "x${have_freerdp}" = "xyes"
+if test "x$have_freerdp2" = "xyes"
 then
-    AC_CHECK_DECL([freerdp_convert_gdi_order_color],
-        [AC_DEFINE([HAVE_FREERDP_CONVERT_GDI_ORDER_COLOR],,
-                   [Whether freerdp_convert_gdi_order_color() is defined])],,
-        [#include <freerdp/codec/color.h>])
 
-    AC_CHECK_DECL([freerdp_color_convert_drawing_order_color_to_gdi_color],
-        [AC_DEFINE([HAVE_FREERDP_COLOR_CONVERT_DRAWING_ORDER_COLOR_TO_GDI_COLOR],,
-                   [Whether freerdp_color_convert_drawing_order_color_to_gdi_color() is defined])],,
-        [#include <freerdp/codec/color.h>])
+    # FreeRDP 2.0.0-rc3 and older referred to FreeRDPConvertColor() as
+    # ConvertColor()
+    AC_CHECK_DECL([FreeRDPConvertColor],
+                  [AC_DEFINE([HAVE_FREERDPCONVERTCOLOR],,
+                             [Whether FreeRDPConvertColor() is defined])],,
+                  [#include <freerdp/codec/color.h>])
+
 fi
 
-# Check for interval polling in plugins
-if test "x${have_freerdp}" = "xyes"
+# Glyph callback variants
+if test "x${have_freerdp2}" = "xyes"
 then
-    AC_CHECK_MEMBERS([rdpSvcPlugin.interval_ms],,,
-                     [[#include <freerdp/utils/svc_plugin.h>]])
+
+    # FreeRDP 2.0.0-rc3 and older used UINT32 for integer parameters to all
+    # rdpGlyph callbacks
+    AC_MSG_CHECKING([whether rdpGlyph callbacks accept INT32 integer parameters])
+    AC_COMPILE_IFELSE([AC_LANG_SOURCE([[
+
+        #include <freerdp/freerdp.h>
+        #include <freerdp/graphics.h>
+        #include <winpr/wtypes.h>
+
+        BOOL test_begindraw(rdpContext* context, INT32 x, INT32 y,
+                INT32 width, INT32 height, UINT32 fgcolor, UINT32 bgcolor,
+                BOOL redundant);
+
+        rdpGlyph glyph = {
+            .BeginDraw = test_begindraw
+        };
+
+        int main() {
+            return (int) glyph.BeginDraw(NULL, 0, 0, 0, 0, 0, 0, FALSE);
+        }
+
+    ]])],
+    [AC_MSG_RESULT([yes])]
+    [AC_DEFINE([FREERDP_GLYPH_CALLBACKS_ACCEPT_INT32],,
+               [Whether rdpGlyph callbacks accept INT32 integer parameters])],
+    [AC_MSG_RESULT([no])])
+
 fi
 
-# Keyboard layout header
-if test "x${have_freerdp}" = "xyes"
+# CLIPRDR callback variants
+if test "x${have_freerdp2}" = "xyes"
 then
-    AC_CHECK_HEADERS([freerdp/locale/keyboard.h],,
-                      [AC_CHECK_HEADERS([freerdp/kbd/layouts.h],,
-                                       [AC_MSG_WARN([
-  --------------------------------------------
-   Unable to find keyboard layout headers
-   RDP will be disabled.
-  --------------------------------------------])
-                                        have_freerdp=no])])
+
+    # FreeRDP 2.0.0-rc3 and older did not use const for CLIPRDR callbacks
+    AC_MSG_CHECKING([whether CLIPRDR callbacks require const for their final parameter])
+    AC_COMPILE_IFELSE([AC_LANG_SOURCE([[
+
+        #include <freerdp/client/cliprdr.h>
+        #include <winpr/wtypes.h>
+
+        UINT test_monitor_ready(CliprdrClientContext* cliprdr,
+                const CLIPRDR_MONITOR_READY* monitor_ready);
+
+        CliprdrClientContext context = {
+            .MonitorReady = test_monitor_ready
+        };
+
+        int main() {
+            return (int) context.MonitorReady(NULL, NULL);
+        }
+
+    ]])],
+    [AC_MSG_RESULT([yes])]
+    [AC_DEFINE([FREERDP_CLIPRDR_CALLBACKS_REQUIRE_CONST],,
+               [Whether CLIPRDR callbacks require const for the final parameter])],
+    [AC_MSG_RESULT([no])])
+
 fi
 
-# New headers defining addins
-if test "x${have_freerdp}" = "xyes"
+# RAIL callback variants
+if test "x${have_freerdp2}" = "xyes"
 then
-    AC_CHECK_HEADERS([freerdp/addin.h freerdp/client/channels.h])
+
+    # FreeRDP 2.0.0-rc3 and older did not use const for RAIL callbacks
+    AC_MSG_CHECKING([whether RAIL callbacks require const for their final parameter])
+    AC_COMPILE_IFELSE([AC_LANG_SOURCE([[
+
+        #include <freerdp/client/rail.h>
+        #include <winpr/wtypes.h>
+
+        UINT test_server_handshake(RailClientContext* rail,
+                const RAIL_HANDSHAKE_ORDER* handshake);
+
+        RailClientContext context = {
+            .ServerHandshake = test_server_handshake
+        };
+
+        int main() {
+            return (int) context.ServerHandshake(NULL, NULL);
+        }
+
+    ]])],
+    [AC_MSG_RESULT([yes])]
+    [AC_DEFINE([FREERDP_RAIL_CALLBACKS_REQUIRE_CONST],,
+               [Whether RAIL callbacks require const for the final parameter])],
+    [AC_MSG_RESULT([no])])
+
 fi
 
-# Header defining cliprdr
-if test "x${have_freerdp}" = "xyes"
-then
-    AC_CHECK_HEADERS([freerdp/client/cliprdr.h],,
-                     [AC_CHECK_HEADERS([freerdp/plugins/cliprdr.h],,
-                                       [AC_MSG_WARN([
-  --------------------------------------------
-   Unable to find cliprdr headers
-   RDP will be disabled.
-  --------------------------------------------])
-                                        have_freerdp=no],
-                                       [#include <freerdp/types.h>])],
-                     [#include <winpr/wtypes.h>
-                      #include <winpr/collections.h>])
-fi
+# Restore CPPFLAGS, removing FreeRDP-specific options needed for testing
+CPPFLAGS="$OLDCPPFLAGS"
 
-# Header defining display update channel
-if test "x${have_freerdp}" = "xyes"
-then
-    AC_CHECK_HEADERS([freerdp/client/disp.h],
-                     [AC_DEFINE([HAVE_FREERDP_DISPLAY_UPDATE_SUPPORT],,
-                                [Whether FreeRDP supports the display update channel])]
-                     [AC_CHECK_MEMBERS([rdpSettings.SupportDisplayControl],,,
-                                       [[#include <freerdp/freerdp.h>]])],,
-                     [#include <winpr/wtypes.h>
-                      #include <winpr/collections.h>])
-fi
-
-# Support for RDP gateways 
-if test "x${have_freerdp}" = "xyes"
-then
-    AC_CHECK_MEMBERS([rdpSettings.GatewayEnabled],
-                     [AC_DEFINE([HAVE_FREERDP_GATEWAY_SUPPORT],,
-                                [Whether FreeRDP supports RDP gateways])],,
-                     [[#include <freerdp/freerdp.h>]])
-fi
-
-# Support for load balancing via connection brokers 
-if test "x${have_freerdp}" = "xyes"
-then
-    AC_CHECK_MEMBERS([rdpSettings.LoadBalanceInfo],
-                     [AC_DEFINE([HAVE_FREERDP_LOAD_BALANCER_SUPPORT],,
-                                [Whether FreeRDP supports load balancers])],,
-                     [[#include <freerdp/freerdp.h>]])
-fi
-
-# Support for "PubSub" event system
-if test "x${have_freerdp}" = "xyes"
-then
-    AC_CHECK_DECL([PubSub_SubscribeChannelConnected],
-        [AC_DEFINE([HAVE_FREERDP_EVENT_PUBSUB],,
-                   [Whether this version of FreeRDP provides the PubSub event system])],,
-        [#include <freerdp/event.h>])
-fi
-
-# Addin registration variations
-if test "x${have_freerdp}" = "xyes"
-then
-    AC_CHECK_DECL([freerdp_register_addin_provider],
-        [AC_DEFINE([HAVE_FREERDP_REGISTER_ADDIN_PROVIDER],,
-                   [Whether freerdp_register_addin_provider() is defined])],,
-        [#include <freerdp/addin.h>])
-
-    AC_CHECK_DECL([freerdp_channels_global_init],
-        [AC_DEFINE([HAVE_FREERDP_CHANNELS_GLOBAL_INIT],,
-                   [Whether freerdp_channels_global_init() is defined])],,
-        [#include <freerdp/channels/channels.h>])
-fi
-
-# Availability of ADDIN_ARGV structure for configuring plugins
-if test "x${have_freerdp}" = "xyes"
-then
-    AC_CHECK_TYPE([ADDIN_ARGV],
-        [AC_DEFINE([HAVE_ADDIN_ARGV],,
-                   [Whether the ADDIN_ARGV type is available])],,
-        [#include <freerdp/settings.h>])
-fi
-
-#
-# FreeRDP: WinPR
-#
-
-# Check for stream support via WinPR
-if test "x${have_freerdp}" = "xyes"
-then
-    AC_CHECK_HEADER(winpr/stream.h,,
-                    [have_winpr=no,
-                     AC_CHECK_DECL([stream_write_uint8],,
-                                  [AC_MSG_WARN([
-  --------------------------------------------
-   Unable to find stream support
-   RDP will be disabled.
-  --------------------------------------------])
-                                   have_freerdp=no],
-                                  [#include <freerdp/utils/stream.h>])])
-fi
-
-# Find location of Stream_New and Stream_free
-if test "x${have_freerdp}" = "xyes" -a "x${have_winpr}" = "xyes"
-then
-    AC_CHECK_LIB([winpr], [Stream_New, Stream_Free],
-                 [WINPR_LIBS="$WINPR_LIBS -lwinpr"],
-                 [AC_CHECK_LIB([winpr-utils], [Stream_New, Stream_Free],
-                               [WINPR_LIBS="$WINPR_LIBS -lwinpr-utils"],
-                               [AC_MSG_WARN([
-  ------------------------------------------
-  Unable to locate stream functions in winpr
-  libraries.  RDP will be disabled.
-  ------------------------------------------])
-                                    have_freerdp=no])])
-fi
-
-# Check for types in WinPR
-if test "x${have_freerdp}" = "xyes"
-then
-    AC_CHECK_HEADER(winpr/wtypes.h,,
-                    [have_winpr=no,
-                     AC_CHECK_HEADER(freerdp/types.h,,
-                                     [AC_MSG_WARN([
-  --------------------------------------------
-   Unable to find type definitions
-   RDP will be disabled.
-  --------------------------------------------])
-                                      have_freerdp=no])])
-fi
-
-if test "x${have_freerdp}" = "xyes" -a "x${have_winpr}" = "xyes"
-then
-    AC_DEFINE([ENABLE_WINPR],,
-              [Whether library support for WinPR types was found])
-fi
-
-#
-# FreeRDP: freerdp 
-#
-
-if test "x${have_freerdp}" = "xyes"
-then
-    # Check for current (as of 1.1) freerdp interface
-    AC_CHECK_MEMBERS([freerdp.ContextSize],
-                     [freerdp_interface=stable],,
-                     [[#include <freerdp/freerdp.h>]])
-
-    # If not current, check for legacy interface
-    if test "x${freerdp_interface}" = "xunknown"
-    then
-        AC_CHECK_MEMBERS([freerdp.context_size],
-                         [freerdp_interface=legacy],,
-                         [[#include <freerdp/freerdp.h>]])
-    fi
-
-    # Set defines based on interface type, warn if unknown
-    if test "x${freerdp_interface}" = "xlegacy"; then
-        AC_DEFINE([LEGACY_FREERDP],,
-                  [Whether the older version of the FreeRDP API was found])
-    elif test "x${freerdp_interface}" = "xunknown"; then
-        AC_MSG_WARN([
-  --------------------------------------------
-   Unknown FreeRDP interface
-   RDP will be disabled.
-  --------------------------------------------])
-        have_freerdp=no
-    fi
-fi
-
-#
-# FreeRDP: rdpSettings
-#
-
-if test "x${have_freerdp}" = "xyes"
-then
-    # Check for current (as of 1.1) rdpSettings interface
-    AC_CHECK_MEMBERS([rdpSettings.Width,
-                      rdpSettings.Height,
-                      rdpSettings.FastPathInput,
-                      rdpSettings.FastPathOutput,
-                      rdpSettings.SendPreconnectionPdu,
-                      rdpSettings.OrderSupport],
-                     [rdpsettings_interface=stable],,
-                     [[#include <freerdp/freerdp.h>]])
-
-    # If not current, check for legacy interface
-    if test "x${rdpsettings_interface}" = "xunknown"
-    then
-        AC_CHECK_MEMBERS([rdpSettings.width,
-                          rdpSettings.height,
-                          rdpSettings.order_support],
-                         [rdpsettings_interface=legacy],,
-                         [[#include <freerdp/freerdp.h>]])
-    fi
-
-    # Set defines based on interface type, warn if unknown
-    if test "x${rdpsettings_interface}" = "xlegacy"; then
-        AC_DEFINE([LEGACY_RDPSETTINGS],,
-                  [Whether the legacy version of the rdpSettings API was found])
-
-        # Legacy interface may not have AudioPlayback settings
-        AC_CHECK_MEMBERS([rdpSettings.audio_playback],,
-                         [rdpsettings_audioplayback=no],
-                         [[#include <freerdp/freerdp.h>]])
-
-        # Legacy interface may not have AudioCapture settings
-        AC_CHECK_MEMBERS([rdpSettings.audio_capture],,
-                         [rdpsettings_audiocapture=no],
-                         [[#include <freerdp/freerdp.h>]])
-
-        # Legacy interface may not have DeviceRedirection settings
-        AC_CHECK_MEMBERS([rdpSettings.device_redirection],,
-                         [rdpsettings_deviceredirection=no],
-                         [[#include <freerdp/freerdp.h>]])
-
-    elif test "x${rdpsettings_interface}" = "xunknown"; then
-        AC_MSG_WARN([
-  --------------------------------------------
-   Unknown rdpSettings interface
-   RDP will be disabled.
-  --------------------------------------------]) 
-        have_freerdp=no
-    fi
-fi
-
-# Activate audio playback settings if present
-if test "x${have_freerdp}" = "xyes" -a "x${rdpsettings_audioplayback}" = "xyes"; then
-    AC_DEFINE([HAVE_RDPSETTINGS_AUDIOPLAYBACK],,
-              [Whether the rdpSettings structure has AudioPlayback settings])
-fi
-
-# Activate audio capture settings if present
-if test "x${have_freerdp}" = "xyes" -a "x${rdpsettings_audiocapture}" = "xyes"; then
-    AC_DEFINE([HAVE_RDPSETTINGS_AUDIOCAPTURE],,
-              [Whether the rdpSettings structure has AudioCapture settings])
-fi
-
-# Activate device redirection settings if present
-if test "x${have_freerdp}" = "xyes" -a "x${rdpsettings_deviceredirection}" = "xyes"; then
-    AC_DEFINE([HAVE_RDPSETTINGS_DEVICEREDIRECTION],,
-              [Whether the rdpSettings structure has DeviceRedirection settings])
-fi
-
-# Check if the type CHANNEL_ENTRY_POINTS_FREERDP exists, if not define it to CHANNEL_ENTRY_POINTS_EX
-if test "x${have_freerdp}" = "xyes"
-then
-    AC_CHECK_TYPE([CHANNEL_ENTRY_POINTS_FREERDP],,
-                  AC_DEFINE([CHANNEL_ENTRY_POINTS_FREERDP],[CHANNEL_ENTRY_POINTS_EX], [Type compatibility]),
-                  [[#include <freerdp/svc.h>]])
-fi
-
-# Check if the freerdp version header exists
-if test "x${have_freerdp}" = "xyes"
-then
-    AC_CHECK_HEADERS([freerdp/version.h])
-fi
-
-#
-# FreeRDP: rdpBitmap
-#
-
-if test "x${have_freerdp}" = "xyes"
-then
-    AC_MSG_CHECKING([whether rdpBitmap.Decompress() requires the codec_id])
-    AC_COMPILE_IFELSE([AC_LANG_SOURCE([[#include <winpr/wtypes.h>
-                                        #include <freerdp/freerdp.h>
-                                        void __decompress(rdpContext* context,
-                                                          rdpBitmap* bitmap,
-                                                          UINT8* data,
-                                                          int width,
-                                                          int height,
-                                                          int bpp,
-                                                          int length,
-                                                          BOOL compressed,
-                                                          int codec_id);
-                                        rdpBitmap b = { .Decompress = __decompress };]])],
-                      [AC_MSG_RESULT([yes])],
-                      [AC_MSG_RESULT([no])
-                       AC_DEFINE([LEGACY_RDPBITMAP],,
-                                 [Whether the legacy rdpBitmap API was found])])
-fi
-
-#
-# FreeRDP: IWTSVirtualChannelCallback
-#
-
-if test "x${have_freerdp}" = "xyes"
-then
-    AC_MSG_CHECKING([whether IWTSVirtualChannelCallback.OnDataReceived() uses a wStream])
-    AC_COMPILE_IFELSE([AC_LANG_SOURCE([[#include <winpr/wtypes.h>
-                                        #include <freerdp/dvc.h>
-                                        #include <freerdp/freerdp.h>
-                                        int __data_received(
-                                            IWTSVirtualChannelCallback* channel_callback,
-                                            wStream* stream);
-                                        IWTSVirtualChannelCallback cb = {
-                                            .OnDataReceived = __data_received
-                                        };
-                                        int main() {
-                                            return
-                                                cb.OnDataReceived(NULL, NULL);
-                                        }]])],
-                      [AC_MSG_RESULT([yes])],
-                      [AC_MSG_RESULT([no])
-                       AC_DEFINE([LEGACY_IWTSVIRTUALCHANNELCALLBACK],,
-                                 [Whether the legacy IWTSVirtualChannelCallback API was found])])
-fi
-
-#
-# FreeRDP: Decompression function variants
-#
-
-# Check whether interleaved_decompress() can handle the palette
-if test "x${have_freerdp}" = "xyes"
-then
-    AC_MSG_CHECKING([whether interleaved_decompress() accepts an additional palette parameter])
-    AC_COMPILE_IFELSE([AC_LANG_SOURCE([[#include <freerdp/codec/interleaved.h>
-
-                                        int main() {
-                                            BYTE* palette = NULL;
-                                            interleaved_decompress(NULL, NULL, 0, 0, NULL, 0, 0, 0, 0, 0, 0, palette);
-                                        }]])],
-                      [AC_MSG_RESULT([yes])
-                       AC_DEFINE([INTERLEAVED_DECOMPRESS_TAKES_PALETTE],,
-                                 [Whether interleaved_decompress() accepts an additional palette parameter])],
-                      [AC_MSG_RESULT([no])])
-fi
-
-# Check whether planar_decompress() will handle flipping
-if test "x${have_freerdp}" = "xyes"
-then
-    AC_MSG_CHECKING([whether planar_decompress() can flip])
-    AC_COMPILE_IFELSE([AC_LANG_SOURCE([[#include <freerdp/codec/planar.h>
-
-                                        int main() {
-                                            BOOL* flip = TRUE;
-                                            planar_decompress(NULL, NULL, 0, NULL, 0, 0, 0, 0, 0, 0, flip);
-                                        }]])],
-                      [AC_MSG_RESULT([yes])
-                       AC_DEFINE([PLANAR_DECOMPRESS_CAN_FLIP],,
-                                 [Whether planar_decompress() can flip])],
-                      [AC_MSG_RESULT([no])])
-fi
-
-#
-# FreeRDP: rdpContext
-#
-
-# Check for rdpContext.codecs
-if test "x${have_freerdp}" = "xyes"
-then
-    AC_CHECK_MEMBERS([rdpContext.codecs],
-                     [AC_DEFINE([FREERDP_BITMAP_REQUIRES_ALIGNED_MALLOC],,
-                                [Whether this version of FreeRDP requires _aligned_malloc() for bitmap data])],,
-                     [[#include <freerdp/freerdp.h>]])
-fi
-
-#
-# FreeRDP: rdpPalette
-#
-
-if test "x${have_freerdp}" = "xyes"
-then
-    AC_MSG_CHECKING([whether rdpPalette.entries is static])
-    AC_COMPILE_IFELSE([AC_LANG_SOURCE([[#include <freerdp/update.h>
-                                        rdpPalette p;
-                                        PALETTE_ENTRY* foo = p.entries;]])],
-                      [AC_MSG_RESULT([yes])],
-                      [AC_MSG_RESULT([no])
-                       AC_DEFINE([LEGACY_RDPPALETTE],,
-                                 [Whether the legacy rdpPalette API was found])])
-fi
-
-#
-# FreeRDP: rdpPointer
-#
-
-# Check for SetDefault and SetNull members of rdpPointer
-if test "x${have_freerdp}" = "xyes"
-then
-    AC_CHECK_MEMBERS([rdpPointer.SetDefault,
-                      rdpPointer.SetNull],
-                     ,,
-                     [[#include <freerdp/freerdp.h>]])
-fi
-
-#
-# FreeRDP: wMessage / RDP_EVENT
-#
-
-# Check for current (as of 1.1) wMessage interface
-if test "x${have_freerdp}" = "xyes"
-then
-    AC_CHECK_MEMBERS([wMessage.id],
-                     [event_interface=stable],,
-                     [[#include <winpr/collections.h>]])
-
-    # If not current, check for legacy (RDP_EVENT) interface
-    if test "x${event_interface}" = "xunknown"
-    then
-        AC_CHECK_MEMBERS([RDP_EVENT.event_class],
-                         [event_interface=legacy],,
-                         [[#include <freerdp/types.h>]])
-    fi
-
-    # Set defines based on interface type, warn if unknown
-    if test "x${event_interface}" = "xlegacy"; then
-        AC_DEFINE([LEGACY_EVENT],,
-                  [Whether the legacy RDP_EVENT API was found])
-    elif test "x${event_interface}" = "xunknown"; then
-        AC_MSG_WARN([
-  --------------------------------------------
-   Unknown event interface
-   RDP will be disabled.
-  --------------------------------------------])
-        have_freerdp=no
-    fi
-fi
-
-AM_CONDITIONAL([LEGACY_FREERDP_EXTENSIONS], [test "x${legacy_freerdp_extensions}" = "xyes"])
-AM_CONDITIONAL([ENABLE_WINPR], [test "x${have_winpr}"   = "xyes"])
-AM_CONDITIONAL([ENABLE_RDP],   [test "x${have_freerdp}" = "xyes"])
-
-AC_SUBST(RDP_LIBS)
-AC_SUBST(WINPR_LIBS)
+AM_CONDITIONAL([ENABLE_RDP], [test "x${have_freerdp2}" = "xyes"])
 
 #
 # libssh2
@@ -1426,7 +1032,7 @@
 
    Library status:
 
-     freerdp ............. ${have_freerdp}
+     freerdp2 ............ ${have_freerdp2}
      pango ............... ${have_pango}
      libavcodec .......... ${have_libavcodec}
      libavutil ........... ${have_libavutil}
diff --git a/src/common/surface.c b/src/common/surface.c
index c31dfbf..c86ca80 100644
--- a/src/common/surface.c
+++ b/src/common/surface.c
@@ -260,18 +260,31 @@
 
 /**
  * Returns whether the given rectangle should be combined into the existing
- * dirty rectangle, to be eventually flushed as a "png" instruction.
+ * dirty rectangle, to be eventually flushed as image data, or would be best
+ * kept independent of the current rectangle.
  *
- * @param surface The surface to be queried.
- * @param rect The update rectangle.
- * @param rect_only Non-zero if this update, by its nature, contains only
- *                  metainformation about the update's rectangle, zero if
- *                  the update also contains image data.
- * @return Non-zero if the update should be combined with any existing update,
- *         zero otherwise.
+ * @param surface
+ *     The surface being updated.
+ *
+ * @param rect
+ *     The bounding rectangle of the update being made to the surface.
+ *
+ * @param rect_only
+ *     Non-zero if this update, by its nature, contains only metainformation
+ *     about the update's bounding rectangle, zero if the update also contains
+ *     image data.
+ *
+ * @return
+ *     Non-zero if the update should be combined with any existing update, zero
+ *     otherwise.
  */
 static int __guac_common_should_combine(guac_common_surface* surface, const guac_common_rect* rect, int rect_only) {
 
+    /* Always favor combining updates if surface is currently a purely
+     * server-side scratch area */
+    if (!surface->realized)
+        return 1;
+
     if (surface->dirty) {
 
         int combined_cost, dirty_cost, update_cost;
diff --git a/src/guacd-docker/bin/link-freerdp-plugins.sh b/src/guacd-docker/bin/link-freerdp-plugins.sh
index 332d4c0..625e85b 100755
--- a/src/guacd-docker/bin/link-freerdp-plugins.sh
+++ b/src/guacd-docker/bin/link-freerdp-plugins.sh
@@ -70,7 +70,7 @@
 
     # Determine correct install location for FreeRDP plugins
     FREERDP_DIR="$(where_is_freerdp "$1")"
-    FREERDP_PLUGIN_DIR="${FREERDP_DIR}/freerdp"
+    FREERDP_PLUGIN_DIR="${FREERDP_DIR}/freerdp2"
 
     # Add symbolic link if necessary
     if [ ! -e "$FREERDP_PLUGIN_DIR/$(basename "$1")" ]; then
diff --git a/src/protocols/rdp/.gitignore b/src/protocols/rdp/.gitignore
index 9f87ecb..5cb764a 100644
--- a/src/protocols/rdp/.gitignore
+++ b/src/protocols/rdp/.gitignore
@@ -4,5 +4,6 @@
 test_rdp
 
 # Autogenerated sources
+_generated_channel_entry_wrappers.c
 _generated_keymaps.c
 
diff --git a/src/protocols/rdp/Makefile.am b/src/protocols/rdp/Makefile.am
index 97b551d..70f7f58 100644
--- a/src/protocols/rdp/Makefile.am
+++ b/src/protocols/rdp/Makefile.am
@@ -29,218 +29,172 @@
 lib_LTLIBRARIES = libguac-client-rdp.la
 SUBDIRS = . tests
 
-nodist_libguac_client_rdp_la_SOURCES = \
-    _generated_keymaps.c
-
-libguac_client_rdp_la_SOURCES = \
-    audio_input.c               \
-    client.c                    \
-    decompose.c                 \
-    dvc.c                       \
-    error.c                     \
-    input.c                     \
-    keyboard.c                  \
-    ptr_string.c                \
-    rdp.c                       \
-    rdp_bitmap.c                \
-    rdp_cliprdr.c               \
-    rdp_color.c                 \
-    rdp_disp.c                  \
-    rdp_fs.c                    \
-    rdp_gdi.c                   \
-    rdp_glyph.c                 \
-    rdp_keymap.c                \
-    rdp_print_job.c             \
-    rdp_pointer.c               \
-    rdp_rail.c                  \
-    rdp_settings.c              \
-    rdp_stream.c                \
-    rdp_svc.c                   \
-    resolution.c                \
-    unicode.c                   \
-    user.c
-
-guacai_sources =          \
-    audio_input.c         \
-    guac_ai/ai_messages.c \
-    guac_ai/ai_service.c  \
-    ptr_string.c
-
-guacsvc_sources =          \
-    guac_svc/svc_service.c \
-    rdp_svc.c
-
-guacsnd_sources =                 \
-    guac_rdpsnd/rdpsnd_messages.c \
-    guac_rdpsnd/rdpsnd_service.c
-
-guacdr_sources =                             \
-    guac_rdpdr/rdpdr_fs_messages.c           \
-    guac_rdpdr/rdpdr_fs_messages_dir_info.c  \
-    guac_rdpdr/rdpdr_fs_messages_file_info.c \
-    guac_rdpdr/rdpdr_fs_messages_vol_info.c  \
-    guac_rdpdr/rdpdr_fs_service.c            \
-    guac_rdpdr/rdpdr_messages.c              \
-    guac_rdpdr/rdpdr_printer.c               \
-    guac_rdpdr/rdpdr_service.c               \
-    rdp_fs.c                                 \
-    rdp_print_job.c                          \
-    rdp_stream.c                             \
-    unicode.c
-
-noinst_HEADERS =                             \
-    compat/client-cliprdr.h                  \
-    compat/rail.h                            \
-    guac_ai/ai_messages.h                    \
-    guac_ai/ai_service.h                     \
-    guac_rdpdr/rdpdr_fs_messages.h           \
-    guac_rdpdr/rdpdr_fs_messages_dir_info.h  \
-    guac_rdpdr/rdpdr_fs_messages_file_info.h \
-    guac_rdpdr/rdpdr_fs_messages_vol_info.h  \
-    guac_rdpdr/rdpdr_fs_service.h            \
-    guac_rdpdr/rdpdr_messages.h              \
-    guac_rdpdr/rdpdr_printer.h               \
-    guac_rdpdr/rdpdr_service.h               \
-    guac_rdpsnd/rdpsnd_messages.h            \
-    guac_rdpsnd/rdpsnd_service.h             \
-    guac_svc/svc_service.h                   \
-    audio_input.h                            \
-    client.h                                 \
-    decompose.h                              \
-    dvc.h                                    \
-    error.h                                  \
-    input.h                                  \
-    keyboard.h                               \
-    ptr_string.h                             \
-    rdp.h                                    \
-    rdp_bitmap.h                             \
-    rdp_cliprdr.h                            \
-    rdp_color.h                              \
-    rdp_disp.h                               \
-    rdp_fs.h                                 \
-    rdp_gdi.h                                \
-    rdp_glyph.h                              \
-    rdp_keymap.h                             \
-    rdp_pointer.h                            \
-    rdp_print_job.h                          \
-    rdp_rail.h                               \
-    rdp_settings.h                           \
-    rdp_status.h                             \
-    rdp_stream.h                             \
-    rdp_svc.h                                \
-    resolution.h                             \
-    unicode.h                                \
-    user.h
-
-# Add compatibility layer for WinPR if not available
-if ! ENABLE_WINPR
-noinst_HEADERS  += compat/winpr-stream.h compat/winpr-wtypes.h
-libguac_client_rdp_la_SOURCES += compat/winpr-stream.c
-guacai_sources  += compat/winpr-stream.c
-guacsvc_sources += compat/winpr-stream.c
-guacsnd_sources += compat/winpr-stream.c
-guacdr_sources  += compat/winpr-stream.c
-endif
-
 #
 # Main RDP client library
 #
 
+nodist_libguac_client_rdp_la_SOURCES =  \
+    _generated_channel_entry_wrappers.c \
+    _generated_keymaps.c
+
+libguac_client_rdp_la_SOURCES =                  \
+    bitmap.c                                     \
+    channels/audio-input/audio-buffer.c          \
+    channels/audio-input/audio-input.c           \
+    channels/cliprdr.c                           \
+    channels/common-svc.c                        \
+    channels/disp.c                              \
+    channels/pipe-svc.c                          \
+    channels/rail.c                              \
+    channels/rdpdr/rdpdr-fs-messages-dir-info.c  \
+    channels/rdpdr/rdpdr-fs-messages-file-info.c \
+    channels/rdpdr/rdpdr-fs-messages-vol-info.c  \
+    channels/rdpdr/rdpdr-fs-messages.c           \
+    channels/rdpdr/rdpdr-fs.c                    \
+    channels/rdpdr/rdpdr-messages.c              \
+    channels/rdpdr/rdpdr-printer.c               \
+    channels/rdpdr/rdpdr.c                       \
+    channels/rdpsnd/rdpsnd-messages.c            \
+    channels/rdpsnd/rdpsnd.c                     \
+    client.c                                     \
+    color.c                                      \
+    decompose.c                                  \
+    download.c                                   \
+    error.c                                      \
+    fs.c                                         \
+    gdi.c                                        \
+    glyph.c                                      \
+    input.c                                      \
+    keyboard.c                                   \
+    keymap.c                                     \
+    log.c                                        \
+    ls.c                                         \
+    plugins/channels.c                           \
+    plugins/ptr-string.c                         \
+    pointer.c                                    \
+    print-job.c                                  \
+    rdp.c                                        \
+    resolution.c                                 \
+    settings.c                                   \
+    unicode.c                                    \
+    upload.c                                     \
+    user.c
+
+noinst_HEADERS =                                 \
+    bitmap.h                                     \
+    channels/audio-input/audio-buffer.h          \
+    channels/audio-input/audio-input.h           \
+    channels/cliprdr.h                           \
+    channels/common-svc.h                        \
+    channels/disp.h                              \
+    channels/pipe-svc.h                          \
+    channels/rail.h                              \
+    channels/rdpdr/rdpdr-fs-messages-dir-info.h  \
+    channels/rdpdr/rdpdr-fs-messages-file-info.h \
+    channels/rdpdr/rdpdr-fs-messages-vol-info.h  \
+    channels/rdpdr/rdpdr-fs-messages.h           \
+    channels/rdpdr/rdpdr-fs.h                    \
+    channels/rdpdr/rdpdr-messages.h              \
+    channels/rdpdr/rdpdr-printer.h               \
+    channels/rdpdr/rdpdr.h                       \
+    channels/rdpsnd/rdpsnd-messages.h            \
+    channels/rdpsnd/rdpsnd.h                     \
+    client.h                                     \
+    color.h                                      \
+    decompose.h                                  \
+    download.h                                   \
+    error.h                                      \
+    fs.h                                         \
+    gdi.h                                        \
+    glyph.h                                      \
+    input.h                                      \
+    keyboard.h                                   \
+    keymap.h                                     \
+    log.h                                        \
+    ls.h                                         \
+    plugins/channels.h                           \
+    plugins/guacai/guacai-messages.h             \
+    plugins/guacai/guacai.h                      \
+    plugins/ptr-string.h                         \
+    pointer.h                                    \
+    print-job.h                                  \
+    rdp.h                                        \
+    resolution.h                                 \
+    settings.h                                   \
+    unicode.h                                    \
+    upload.h                                     \
+    user.h
+
 libguac_client_rdp_la_CFLAGS = \
     -Werror -Wall -Iinclude    \
     @COMMON_INCLUDE@           \
     @COMMON_SSH_INCLUDE@       \
-    @LIBGUAC_INCLUDE@
+    @LIBGUAC_INCLUDE@          \
+    @RDP_CFLAGS@
 
 libguac_client_rdp_la_LDFLAGS = \
     -version-info 0:0:0         \
     @CAIRO_LIBS@                \
     @PTHREAD_LIBS@              \
-    @RDP_LIBS@                  \
-    @WINPR_LIBS@
+    @RDP_LIBS@
 
-libguac_client_rdp_la_LIBADD =     \
-    @COMMON_LTLIB@                 \
+libguac_client_rdp_la_LIBADD = \
+    @COMMON_LTLIB@             \
     @LIBGUAC_LTLIB@
 
 #
-# RDPDR
+# Plugins for FreeRDP
 #
 
-guacdr_cflags                = \
-    -Werror -Wall -Iinclude    \
-    @COMMON_INCLUDE@           \
-    @COMMON_SSH_INCLUDE@       \
-    @LIBGUAC_INCLUDE@
+freerdp_LTLIBRARIES =            \
+    libguac-common-svc-client.la \
+    libguacai-client.la
 
-guacdr_ldflags =                   \
-    -module -avoid-version -shared \
-    @PTHREAD_LIBS@                 \
-    @RDP_LIBS@                     \
-    @WINPR_LIBS@
+freerdpdir = ${libdir}/freerdp2
 
-guacdr_libadd =     \
-    @COMMON_LTLIB@  \
+#
+# Common SVC plugin (shared by RDPDR, RDPSND, etc.)
+#
+
+libguac_common_svc_client_la_SOURCES =        \
+    plugins/guac-common-svc/guac-common-svc.c
+
+libguac_common_svc_client_la_CFLAGS = \
+    -Werror -Wall -Iinclude           \
+    @LIBGUAC_INCLUDE@                 \
+    @RDP_CFLAGS@
+
+libguac_common_svc_client_la_LDFLAGS = \
+    -module -avoid-version -shared     \
+    @RDP_LIBS@
+
+libguac_common_svc_client_la_LIBADD = \
     @LIBGUAC_LTLIB@
 
 #
 # Audio Input
 #
 
-guacai_cflags               =  \
-    -Werror -Wall -Iinclude    \
-    @COMMON_INCLUDE@           \
-    @COMMON_SSH_INCLUDE@       \
-    @LIBGUAC_INCLUDE@
+libguacai_client_la_SOURCES =           \
+    channels/audio-input/audio-buffer.c \
+    plugins/guacai/guacai-messages.c    \
+    plugins/guacai/guacai.c             \
+    plugins/ptr-string.c
 
-guacai_ldflags =                   \
+libguacai_client_la_CFLAGS = \
+    -Werror -Wall -Iinclude  \
+    @COMMON_INCLUDE@         \
+    @COMMON_SSH_INCLUDE@     \
+    @LIBGUAC_INCLUDE@        \
+    @RDP_CFLAGS@
+
+libguacai_client_la_LDFLAGS =      \
     -module -avoid-version -shared \
     @PTHREAD_LIBS@                 \
-    @RDP_LIBS@                     \
-    @WINPR_LIBS@
+    @RDP_LIBS@
 
-guacai_libadd =     \
-    @COMMON_LTLIB@  \
-    @LIBGUAC_LTLIB@
-
-#
-# RDPSND
-#
-
-guacsnd_cflags               = \
-    -Werror -Wall -Iinclude    \
-    @COMMON_INCLUDE@           \
-    @COMMON_SSH_INCLUDE@       \
-    @LIBGUAC_INCLUDE@
-
-guacsnd_ldflags =                  \
-    -module -avoid-version -shared \
-    @PTHREAD_LIBS@                 \
-    @RDP_LIBS@                     \
-    @WINPR_LIBS@
-
-guacsnd_libadd =    \
-    @COMMON_LTLIB@  \
-    @LIBGUAC_LTLIB@
-
-#
-# Static Virtual Channels
-#
-
-guacsvc_cflags               = \
-    -Werror -Wall -Iinclude    \
-    @COMMON_INCLUDE@           \
-    @COMMON_SSH_INCLUDE@       \
-    @LIBGUAC_INCLUDE@
-
-guacsvc_ldflags =                  \
-    -module -avoid-version -shared \
-    @PTHREAD_LIBS@                 \
-    @RDP_LIBS@                     \
-    @WINPR_LIBS@
-
-guacsvc_libadd =    \
-    @COMMON_LTLIB@  \
+libguacai_client_la_LIBADD = \
+    @COMMON_LTLIB@           \
     @LIBGUAC_LTLIB@
 
 #
@@ -254,13 +208,18 @@
 endif
 
 #
-# Autogenerate keymaps
+# Autogenerated keymaps and channel wrapper functions
 #
 
-CLEANFILES = _generated_keymaps.c
-BUILT_SOURCES = _generated_keymaps.c
+CLEANFILES =                            \
+    _generated_channel_entry_wrappers.c \
+    _generated_keymaps.c
 
-rdp_keymaps =                   \
+BUILT_SOURCES =                         \
+    _generated_channel_entry_wrappers.c \
+    _generated_keymaps.c
+
+rdp_keymaps =                             \
     $(srcdir)/keymaps/base.keymap         \
     $(srcdir)/keymaps/failsafe.keymap     \
     $(srcdir)/keymaps/de_de_qwertz.keymap \
@@ -280,71 +239,13 @@
     $(srcdir)/keymaps/tr_tr_qwerty.keymap
 
 _generated_keymaps.c: $(rdp_keymaps)
-	$(srcdir)/keymaps/generate.pl $(rdp_keymaps)
+	$(AM_V_GEN) $(srcdir)/keymaps/generate.pl $(rdp_keymaps)
 
-EXTRA_DIST =            \
-    $(rdp_keymaps)      \
-    keymaps/generate.pl
+_generated_channel_entry_wrappers.c: $(srcdir)/plugins/channels.h $(srcdir)/plugins/generate-entry-wrappers.pl
+	$(AM_V_GEN) $(srcdir)/plugins/generate-entry-wrappers.pl $(srcdir)/plugins/channels.h
 
-if LEGACY_FREERDP_EXTENSIONS
-
-# FreeRDP 1.0-style extensions
-freerdp_LTLIBRARIES = \
-    guacai.la         \
-    guacdr.la         \
-    guacsnd.la        \
-    guacsvc.la
-
-guacai_la_SOURCES = ${guacai_sources}
-guacai_la_CFLAGS  = ${guacai_cflags}
-guacai_la_LDFLAGS = ${guacai_ldflags}
-guacai_la_LIBADD  = ${guacai_libadd}
-
-guacdr_la_SOURCES = ${guacdr_sources}
-guacdr_la_CFLAGS  = ${guacdr_cflags}
-guacdr_la_LDFLAGS = ${guacdr_ldflags}
-guacdr_la_LIBADD  = ${guacdr_libadd}
-
-guacsnd_la_SOURCES = ${guacsnd_sources}
-guacsnd_la_CFLAGS  = ${guacsnd_cflags}
-guacsnd_la_LDFLAGS = ${guacsnd_ldflags}
-guacsnd_la_LIBADD  = ${guacsnd_libadd}
-
-guacsvc_la_SOURCES = ${guacsvc_sources}
-guacsvc_la_CFLAGS  = ${guacsvc_cflags}
-guacsvc_la_LDFLAGS = ${guacsvc_ldflags}
-guacsvc_la_LIBADD  = ${guacsvc_libadd}
-
-else
-
-# FreeRDP 1.1 (and hopefully onward) extensions
-freerdp_LTLIBRARIES = \
-    guacai-client.la  \
-    guacdr-client.la  \
-    guacsnd-client.la \
-    guacsvc-client.la
-
-guacai_client_la_SOURCES = ${guacai_sources}
-guacai_client_la_CFLAGS  = ${guacai_cflags}
-guacai_client_la_LDFLAGS = ${guacai_ldflags}
-guacai_client_la_LIBADD  = ${guacai_libadd}
-
-guacdr_client_la_SOURCES = ${guacdr_sources}
-guacdr_client_la_CFLAGS  = ${guacdr_cflags}
-guacdr_client_la_LDFLAGS = ${guacdr_ldflags}
-guacdr_client_la_LIBADD  = ${guacdr_libadd}
-
-guacsnd_client_la_SOURCES = ${guacsnd_sources}
-guacsnd_client_la_CFLAGS  = ${guacsnd_cflags}
-guacsnd_client_la_LDFLAGS = ${guacsnd_ldflags}
-guacsnd_client_la_LIBADD  = ${guacsnd_libadd}
-
-guacsvc_client_la_SOURCES = ${guacsvc_sources}
-guacsvc_client_la_CFLAGS  = ${guacsvc_cflags}
-guacsvc_client_la_LDFLAGS = ${guacsvc_ldflags}
-guacsvc_client_la_LIBADD  = ${guacsvc_libadd}
-
-endif
-
-freerdpdir = ${libdir}/freerdp
+EXTRA_DIST =                           \
+    $(rdp_keymaps)                     \
+    keymaps/generate.pl                \
+    plugins/generate-entry-wrappers.pl
 
diff --git a/src/protocols/rdp/bitmap.c b/src/protocols/rdp/bitmap.c
new file mode 100644
index 0000000..db29316
--- /dev/null
+++ b/src/protocols/rdp/bitmap.c
@@ -0,0 +1,167 @@
+/*
+ * 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 "bitmap.h"
+#include "common/display.h"
+#include "common/surface.h"
+#include "rdp.h"
+
+#include <cairo/cairo.h>
+#include <freerdp/freerdp.h>
+#include <guacamole/client.h>
+#include <winpr/crt.h>
+#include <winpr/wtypes.h>
+
+#include <stdio.h>
+#include <stdlib.h>
+
+void guac_rdp_cache_bitmap(rdpContext* context, rdpBitmap* bitmap) {
+
+    guac_client* client = ((rdp_freerdp_context*) context)->client;
+    guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
+
+    /* Allocate buffer */
+    guac_common_display_layer* buffer = guac_common_display_alloc_buffer(
+            rdp_client->display, bitmap->width, bitmap->height);
+
+    /* Cache image data if present */
+    if (bitmap->data != NULL) {
+
+        /* Create surface from image data */
+        cairo_surface_t* image = cairo_image_surface_create_for_data(
+            bitmap->data, CAIRO_FORMAT_RGB24,
+            bitmap->width, bitmap->height, 4*bitmap->width);
+
+        /* Send surface to buffer */
+        guac_common_surface_draw(buffer->surface, 0, 0, image);
+
+        /* Free surface */
+        cairo_surface_destroy(image);
+
+    }
+
+    /* Store buffer reference in bitmap */
+    ((guac_rdp_bitmap*) bitmap)->layer = buffer;
+
+}
+
+BOOL guac_rdp_bitmap_new(rdpContext* context, rdpBitmap* bitmap) {
+
+    /* No corresponding surface yet - caching is deferred. */
+    ((guac_rdp_bitmap*) bitmap)->layer = NULL;
+
+    /* Start at zero usage */
+    ((guac_rdp_bitmap*) bitmap)->used = 0;
+
+    return TRUE;
+
+}
+
+BOOL guac_rdp_bitmap_paint(rdpContext* context, rdpBitmap* bitmap) {
+
+    guac_client* client = ((rdp_freerdp_context*) context)->client;
+    guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
+
+    guac_common_display_layer* buffer = ((guac_rdp_bitmap*) bitmap)->layer;
+
+    int width = bitmap->right - bitmap->left + 1;
+    int height = bitmap->bottom - bitmap->top + 1;
+
+    /* If not cached, cache if necessary */
+    if (buffer == NULL && ((guac_rdp_bitmap*) bitmap)->used >= 1)
+        guac_rdp_cache_bitmap(context, bitmap);
+
+    /* If cached, retrieve from cache */
+    if (buffer != NULL)
+        guac_common_surface_copy(buffer->surface, 0, 0, width, height,
+                rdp_client->display->default_surface,
+                bitmap->left, bitmap->top);
+
+    /* Otherwise, draw with stored image data */
+    else if (bitmap->data != NULL) {
+
+        /* Create surface from image data */
+        cairo_surface_t* image = cairo_image_surface_create_for_data(
+            bitmap->data, CAIRO_FORMAT_RGB24,
+            width, height, 4*bitmap->width);
+
+        /* Draw image on default surface */
+        guac_common_surface_draw(rdp_client->display->default_surface,
+                bitmap->left, bitmap->top, image);
+
+        /* Free surface */
+        cairo_surface_destroy(image);
+
+    }
+
+    /* Increment usage counter */
+    ((guac_rdp_bitmap*) bitmap)->used++;
+
+    return TRUE;
+
+}
+
+void guac_rdp_bitmap_free(rdpContext* context, rdpBitmap* bitmap) {
+
+    guac_client* client = ((rdp_freerdp_context*) context)->client;
+    guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
+    guac_common_display_layer* buffer = ((guac_rdp_bitmap*) bitmap)->layer;
+
+    /* If cached, free buffer */
+    if (buffer != NULL)
+        guac_common_display_free_buffer(rdp_client->display, buffer);
+
+    /* NOTE: FreeRDP-allocated memory for the rdpBitmap will NOT be
+     * automatically released after this free handler is invoked, thus we must
+     * do so manually here */
+
+    _aligned_free(bitmap->data);
+    free(bitmap);
+
+}
+
+BOOL guac_rdp_bitmap_setsurface(rdpContext* context, rdpBitmap* bitmap, BOOL primary) {
+
+    guac_client* client = ((rdp_freerdp_context*) context)->client;
+    guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
+
+    if (primary)
+        rdp_client->current_surface = rdp_client->display->default_surface;
+
+    else {
+
+        /* Make sure that the recieved bitmap is not NULL before processing */
+        if (bitmap == NULL) {
+            guac_client_log(client, GUAC_LOG_INFO, "NULL bitmap found in bitmap_setsurface instruction.");
+            return TRUE;
+        }
+
+        /* If not available as a surface, make available. */
+        if (((guac_rdp_bitmap*) bitmap)->layer == NULL)
+            guac_rdp_cache_bitmap(context, bitmap);
+
+        rdp_client->current_surface =
+            ((guac_rdp_bitmap*) bitmap)->layer->surface;
+
+    }
+
+    return TRUE;
+
+}
+
diff --git a/src/protocols/rdp/bitmap.h b/src/protocols/rdp/bitmap.h
new file mode 100644
index 0000000..297230c
--- /dev/null
+++ b/src/protocols/rdp/bitmap.h
@@ -0,0 +1,133 @@
+/*
+ * 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_BITMAP_H
+#define GUAC_RDP_BITMAP_H
+
+#include "config.h"
+#include "common/display.h"
+
+#include <freerdp/freerdp.h>
+#include <freerdp/graphics.h>
+#include <guacamole/layer.h>
+#include <winpr/wtypes.h>
+
+/**
+ * Guacamole-specific rdpBitmap data.
+ */
+typedef struct guac_rdp_bitmap {
+
+    /**
+     * FreeRDP bitmap data - MUST GO FIRST.
+     */
+    rdpBitmap bitmap;
+
+    /**
+     * Layer containing cached image data.
+     */
+    guac_common_display_layer* layer;
+
+    /**
+     * The number of times a bitmap has been used.
+     */
+    int used;
+
+} guac_rdp_bitmap;
+
+/**
+ * Caches the given bitmap immediately, storing its data in a remote Guacamole
+ * buffer. As RDP bitmaps are frequently created, used once, and immediately
+ * destroyed, we defer actual remote-side caching of RDP bitmaps until they are
+ * used at least once.
+ *
+ * @param context
+ *     The rdpContext associated with the current RDP session.
+ *
+ * @param bitmap
+ *     The bitmap to cache.
+ */
+void guac_rdp_cache_bitmap(rdpContext* context, rdpBitmap* bitmap);
+
+/**
+ * Initializes the given newly-created rdpBitmap.
+ *
+ * @param context
+ *     The rdpContext associated with the current RDP session.
+ *
+ * @param bitmap
+ *     The bitmap to initialize.
+ *
+ * @return
+ *     TRUE if successful, FALSE otherwise.
+ */
+BOOL guac_rdp_bitmap_new(rdpContext* context, rdpBitmap* bitmap);
+
+/**
+ * Paints the given rdpBitmap on the primary display surface. Note that this
+ * operation does NOT draw to the "current" surface set by calls to
+ * guac_rdp_bitmap_setsurface().
+ *
+ * @param context
+ *     The rdpContext associated with the current RDP session.
+ *
+ * @param bitmap
+ *     The bitmap to paint. This structure will also contain the specifics of
+ *     the paint operation to perform, including the destination X/Y
+ *     coordinates.
+ *
+ * @return
+ *     TRUE if successful, FALSE otherwise.
+ */
+BOOL guac_rdp_bitmap_paint(rdpContext* context, rdpBitmap* bitmap);
+
+/**
+ * Frees any Guacamole-specific data associated with the given rdpBitmap.
+ *
+ * @param context
+ *     The rdpContext associated with the current RDP session.
+ *
+ * @param bitmap
+ *     The bitmap whose Guacamole-specific data is to be freed.
+ */
+void guac_rdp_bitmap_free(rdpContext* context, rdpBitmap* bitmap);
+
+/**
+ * Sets the given rdpBitmap as the drawing surface for future operations or,
+ * if the primary flag is set, resets the current drawing surface to the
+ * primary drawing surface of the remote display.
+ *
+ * @param context
+ *     The rdpContext associated with the current RDP session.
+ *
+ * @param bitmap
+ *     The rdpBitmap to set as the current drawing surface. This parameter is
+ *     only valid if the primary flag is FALSE.
+ *
+ * @param primary
+ *     TRUE if the bitmap parameter should be ignored, and the current drawing
+ *     surface should be reset to the primary drawing surface of the remote
+ *     display, FALSE otherwise.
+ *
+ * @return
+ *     TRUE if successful, FALSE otherwise.
+ */
+BOOL guac_rdp_bitmap_setsurface(rdpContext* context, rdpBitmap* bitmap,
+        BOOL primary);
+
+#endif
diff --git a/src/protocols/rdp/audio_input.c b/src/protocols/rdp/channels/audio-input/audio-buffer.c
similarity index 69%
rename from src/protocols/rdp/audio_input.c
rename to src/protocols/rdp/channels/audio-input/audio-buffer.c
index 014ea2d..3051341 100644
--- a/src/protocols/rdp/audio_input.c
+++ b/src/protocols/rdp/channels/audio-input/audio-buffer.c
@@ -17,181 +17,19 @@
  * under the License.
  */
 
-#include "config.h"
-#include "audio_input.h"
-#include "dvc.h"
-#include "ptr_string.h"
+#include "channels/audio-input/audio-buffer.h"
 #include "rdp.h"
 
-#include <freerdp/freerdp.h>
-#include <freerdp/channels/channels.h>
+#include <guacamole/client.h>
 #include <guacamole/protocol.h>
 #include <guacamole/socket.h>
 #include <guacamole/stream.h>
 #include <guacamole/user.h>
 
 #include <assert.h>
-#include <errno.h>
-#include <stdlib.h>
 #include <pthread.h>
-
-/**
- * Parses the given raw audio mimetype, producing the corresponding rate,
- * number of channels, and bytes per sample.
- *
- * @param mimetype
- *     The raw auduio mimetype to parse.
- *
- * @param rate
- *     A pointer to an int where the sample rate for the PCM format described
- *     by the given mimetype should be stored.
- *
- * @param channels
- *     A pointer to an int where the number of channels used by the PCM format
- *     described by the given mimetype should be stored.
- *
- * @param bps
- *     A pointer to an int where the number of bytes used the PCM format for
- *     each sample (independent of number of channels) described by the given
- *     mimetype should be stored.
- *
- * @return
- *     Zero if the given mimetype is a raw audio mimetype and has been parsed
- *     successfully, non-zero otherwise.
- */
-static int guac_rdp_audio_parse_mimetype(const char* mimetype,
-        int* rate, int* channels, int* bps) {
-
-    int parsed_rate = -1;
-    int parsed_channels = 1;
-    int parsed_bps;
-
-    /* PCM audio with one byte per sample */
-    if (strncmp(mimetype, "audio/L8;", 9) == 0) {
-        mimetype += 8; /* Advance to semicolon ONLY */
-        parsed_bps = 1;
-    }
-
-    /* PCM audio with two bytes per sample */
-    else if (strncmp(mimetype, "audio/L16;", 10) == 0) {
-        mimetype += 9; /* Advance to semicolon ONLY */
-        parsed_bps = 2;
-    }
-
-    /* Unsupported mimetype */
-    else
-        return 1;
-
-    /* Parse each parameter name/value pair within the mimetype */
-    do {
-
-        /* Advance to first character of parameter (current is either a
-         * semicolon or a comma) */
-        mimetype++;
-
-        /* Parse number of channels */
-        if (strncmp(mimetype, "channels=", 9) == 0) {
-
-            mimetype += 9;
-            parsed_channels = strtol(mimetype, (char**) &mimetype, 10);
-
-            /* Fail if value invalid / out of range */
-            if (errno == EINVAL || errno == ERANGE)
-                return 1;
-
-        }
-
-        /* Parse number of rate */
-        else if (strncmp(mimetype, "rate=", 5) == 0) {
-
-            mimetype += 5;
-            parsed_rate = strtol(mimetype, (char**) &mimetype, 10);
-
-            /* Fail if value invalid / out of range */
-            if (errno == EINVAL || errno == ERANGE)
-                return 1;
-
-        }
-
-        /* Advance to next parameter */
-        mimetype = strchr(mimetype, ',');
-
-    } while (mimetype != NULL);
-
-    /* Mimetype is invalid if rate was not specified */
-    if (parsed_rate == -1)
-        return 1;
-
-    /* Parse success */
-    *rate = parsed_rate;
-    *channels = parsed_channels;
-    *bps = parsed_bps;
-
-    return 0;
-
-}
-
-int guac_rdp_audio_handler(guac_user* user, guac_stream* stream,
-        char* mimetype) {
-
-    guac_client* client = user->client;
-    guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
-
-    int rate;
-    int channels;
-    int bps;
-
-    /* Parse mimetype, abort on parse error */
-    if (guac_rdp_audio_parse_mimetype(mimetype, &rate, &channels, &bps)) {
-        guac_user_log(user, GUAC_LOG_WARNING, "Denying user audio stream with "
-                "unsupported mimetype: \"%s\"", mimetype);
-        guac_protocol_send_ack(user->socket, stream, "Unsupported audio "
-                "mimetype", GUAC_PROTOCOL_STATUS_CLIENT_BAD_TYPE);
-        return 0;
-    }
-
-    /* Init stream data */
-    stream->blob_handler = guac_rdp_audio_blob_handler;
-    stream->end_handler = guac_rdp_audio_end_handler;
-
-    /* Associate stream with audio buffer */
-    guac_rdp_audio_buffer_set_stream(rdp_client->audio_input, user, stream,
-            rate, channels, bps);
-
-    return 0;
-
-}
-
-int guac_rdp_audio_blob_handler(guac_user* user, guac_stream* stream,
-        void* data, int length) {
-
-    guac_client* client = user->client;
-    guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
-
-    /* Write blob to audio stream, buffering if necessary */
-    guac_rdp_audio_buffer_write(rdp_client->audio_input, data, length);
-
-    return 0;
-
-}
-
-int guac_rdp_audio_end_handler(guac_user* user, guac_stream* stream) {
-
-    /* Ignore - the AUDIO_INPUT channel will simply not receive anything */
-    return 0;
-
-}
-
-void guac_rdp_audio_load_plugin(rdpContext* context, guac_rdp_dvc_list* list) {
-
-    guac_client* client = ((rdp_freerdp_context*) context)->client;
-    char client_ref[GUAC_RDP_PTR_STRING_LENGTH];
-
-    /* Add "AUDIO_INPUT" channel */
-    guac_rdp_ptr_to_string(client, client_ref);
-    guac_rdp_dvc_list_add(list, "guacai", client_ref, NULL);
-
-}
+#include <stdint.h>
+#include <stdlib.h>
 
 guac_rdp_audio_buffer* guac_rdp_audio_buffer_alloc() {
     guac_rdp_audio_buffer* buffer = calloc(1, sizeof(guac_rdp_audio_buffer));
diff --git a/src/protocols/rdp/audio_input.h b/src/protocols/rdp/channels/audio-input/audio-buffer.h
similarity index 87%
rename from src/protocols/rdp/audio_input.h
rename to src/protocols/rdp/channels/audio-input/audio-buffer.h
index 6280662..32f7def 100644
--- a/src/protocols/rdp/audio_input.h
+++ b/src/protocols/rdp/channels/audio-input/audio-buffer.h
@@ -17,16 +17,11 @@
  * under the License.
  */
 
-#ifndef GUAC_RDP_AUDIO_INPUT_H
-#define GUAC_RDP_AUDIO_INPUT_H
+#ifndef GUAC_RDP_CHANNELS_AUDIO_INPUT_AUDIO_BUFFER_H
+#define GUAC_RDP_CHANNELS_AUDIO_INPUT_AUDIO_BUFFER_H
 
-#include "config.h"
-#include "dvc.h"
-
-#include <freerdp/freerdp.h>
 #include <guacamole/stream.h>
 #include <guacamole/user.h>
-
 #include <pthread.h>
 
 /**
@@ -278,36 +273,5 @@
  */
 void guac_rdp_audio_buffer_free(guac_rdp_audio_buffer* audio_buffer);
 
-/**
- * Handler for inbound audio data (audio input).
- */
-guac_user_audio_handler guac_rdp_audio_handler;
-
-/**
- * Handler for stream data related to audio input.
- */
-guac_user_blob_handler guac_rdp_audio_blob_handler;
-
-/**
- * Handler for end-of-stream related to audio input.
- */
-guac_user_end_handler guac_rdp_audio_end_handler;
-
-/**
- * Adds Guacamole's "guacai" plugin to the list of dynamic virtual channel
- * plugins to be loaded by FreeRDP's "drdynvc" plugin. The plugin will only
- * be loaded once guac_rdp_load_drdynvc() is invoked with the guac_rdp_dvc_list
- * passed to this function. The "guacai" plugin ultimately adds support for the
- * "AUDIO_INPUT"  dynamic virtual channel.
- *
- * @param context
- *     The rdpContext associated with the active RDP session.
- *
- * @param list
- *     The guac_rdp_dvc_list to which the "guacai" plugin should be added, such
- *     that it may later be loaded by guac_rdp_load_drdynvc().
- */
-void guac_rdp_audio_load_plugin(rdpContext* context, guac_rdp_dvc_list* list);
-
 #endif
 
diff --git a/src/protocols/rdp/channels/audio-input/audio-input.c b/src/protocols/rdp/channels/audio-input/audio-input.c
new file mode 100644
index 0000000..51fa118
--- /dev/null
+++ b/src/protocols/rdp/channels/audio-input/audio-input.c
@@ -0,0 +1,195 @@
+/*
+ * 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/audio-input/audio-buffer.h"
+#include "channels/audio-input/audio-input.h"
+#include "plugins/channels.h"
+#include "plugins/ptr-string.h"
+#include "rdp.h"
+
+#include <freerdp/freerdp.h>
+#include <guacamole/client.h>
+#include <guacamole/protocol.h>
+#include <guacamole/socket.h>
+#include <guacamole/stream.h>
+#include <guacamole/user.h>
+
+#include <errno.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+
+/**
+ * Parses the given raw audio mimetype, producing the corresponding rate,
+ * number of channels, and bytes per sample.
+ *
+ * @param mimetype
+ *     The raw auduio mimetype to parse.
+ *
+ * @param rate
+ *     A pointer to an int where the sample rate for the PCM format described
+ *     by the given mimetype should be stored.
+ *
+ * @param channels
+ *     A pointer to an int where the number of channels used by the PCM format
+ *     described by the given mimetype should be stored.
+ *
+ * @param bps
+ *     A pointer to an int where the number of bytes used the PCM format for
+ *     each sample (independent of number of channels) described by the given
+ *     mimetype should be stored.
+ *
+ * @return
+ *     Zero if the given mimetype is a raw audio mimetype and has been parsed
+ *     successfully, non-zero otherwise.
+ */
+static int guac_rdp_audio_parse_mimetype(const char* mimetype,
+        int* rate, int* channels, int* bps) {
+
+    int parsed_rate = -1;
+    int parsed_channels = 1;
+    int parsed_bps;
+
+    /* PCM audio with one byte per sample */
+    if (strncmp(mimetype, "audio/L8;", 9) == 0) {
+        mimetype += 8; /* Advance to semicolon ONLY */
+        parsed_bps = 1;
+    }
+
+    /* PCM audio with two bytes per sample */
+    else if (strncmp(mimetype, "audio/L16;", 10) == 0) {
+        mimetype += 9; /* Advance to semicolon ONLY */
+        parsed_bps = 2;
+    }
+
+    /* Unsupported mimetype */
+    else
+        return 1;
+
+    /* Parse each parameter name/value pair within the mimetype */
+    do {
+
+        /* Advance to first character of parameter (current is either a
+         * semicolon or a comma) */
+        mimetype++;
+
+        /* Parse number of channels */
+        if (strncmp(mimetype, "channels=", 9) == 0) {
+
+            mimetype += 9;
+            parsed_channels = strtol(mimetype, (char**) &mimetype, 10);
+
+            /* Fail if value invalid / out of range */
+            if (errno == EINVAL || errno == ERANGE)
+                return 1;
+
+        }
+
+        /* Parse number of rate */
+        else if (strncmp(mimetype, "rate=", 5) == 0) {
+
+            mimetype += 5;
+            parsed_rate = strtol(mimetype, (char**) &mimetype, 10);
+
+            /* Fail if value invalid / out of range */
+            if (errno == EINVAL || errno == ERANGE)
+                return 1;
+
+        }
+
+        /* Advance to next parameter */
+        mimetype = strchr(mimetype, ',');
+
+    } while (mimetype != NULL);
+
+    /* Mimetype is invalid if rate was not specified */
+    if (parsed_rate == -1)
+        return 1;
+
+    /* Parse success */
+    *rate = parsed_rate;
+    *channels = parsed_channels;
+    *bps = parsed_bps;
+
+    return 0;
+
+}
+
+int guac_rdp_audio_handler(guac_user* user, guac_stream* stream,
+        char* mimetype) {
+
+    guac_client* client = user->client;
+    guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
+
+    int rate;
+    int channels;
+    int bps;
+
+    /* Parse mimetype, abort on parse error */
+    if (guac_rdp_audio_parse_mimetype(mimetype, &rate, &channels, &bps)) {
+        guac_user_log(user, GUAC_LOG_WARNING, "Denying user audio stream with "
+                "unsupported mimetype: \"%s\"", mimetype);
+        guac_protocol_send_ack(user->socket, stream, "Unsupported audio "
+                "mimetype", GUAC_PROTOCOL_STATUS_CLIENT_BAD_TYPE);
+        return 0;
+    }
+
+    /* Init stream data */
+    stream->blob_handler = guac_rdp_audio_blob_handler;
+    stream->end_handler = guac_rdp_audio_end_handler;
+
+    /* Associate stream with audio buffer */
+    guac_rdp_audio_buffer_set_stream(rdp_client->audio_input, user, stream,
+            rate, channels, bps);
+
+    return 0;
+
+}
+
+int guac_rdp_audio_blob_handler(guac_user* user, guac_stream* stream,
+        void* data, int length) {
+
+    guac_client* client = user->client;
+    guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
+
+    /* Write blob to audio stream, buffering if necessary */
+    guac_rdp_audio_buffer_write(rdp_client->audio_input, data, length);
+
+    return 0;
+
+}
+
+int guac_rdp_audio_end_handler(guac_user* user, guac_stream* stream) {
+
+    /* Ignore - the AUDIO_INPUT channel will simply not receive anything */
+    return 0;
+
+}
+
+void guac_rdp_audio_load_plugin(rdpContext* context) {
+
+    guac_client* client = ((rdp_freerdp_context*) context)->client;
+    char client_ref[GUAC_RDP_PTR_STRING_LENGTH];
+
+    /* Add "AUDIO_INPUT" channel */
+    guac_rdp_ptr_to_string(client, client_ref);
+    guac_freerdp_dynamic_channel_collection_add(context->settings, "guacai", client_ref, NULL);
+
+}
+
diff --git a/src/protocols/rdp/channels/audio-input/audio-input.h b/src/protocols/rdp/channels/audio-input/audio-input.h
new file mode 100644
index 0000000..a3b705f
--- /dev/null
+++ b/src/protocols/rdp/channels/audio-input/audio-input.h
@@ -0,0 +1,53 @@
+/*
+ * 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_AUDIO_INPUT_H
+#define GUAC_RDP_CHANNELS_AUDIO_INPUT_H
+
+#include <freerdp/freerdp.h>
+#include <guacamole/user.h>
+
+/**
+ * Handler for inbound audio data (audio input).
+ */
+guac_user_audio_handler guac_rdp_audio_handler;
+
+/**
+ * Handler for stream data related to audio input.
+ */
+guac_user_blob_handler guac_rdp_audio_blob_handler;
+
+/**
+ * Handler for end-of-stream related to audio input.
+ */
+guac_user_end_handler guac_rdp_audio_end_handler;
+
+/**
+ * Adds Guacamole's "guacai" plugin to the list of dynamic virtual channel
+ * plugins to be loaded by FreeRDP's "drdynvc" plugin. The plugin will only
+ * be loaded once the "drdynvc" plugin is loaded. The "guacai" plugin
+ * ultimately adds support for the "AUDIO_INPUT" dynamic virtual channel.
+ *
+ * @param context
+ *     The rdpContext associated with the active RDP session.
+ */
+void guac_rdp_audio_load_plugin(rdpContext* context);
+
+#endif
+
diff --git a/src/protocols/rdp/channels/cliprdr.c b/src/protocols/rdp/channels/cliprdr.c
new file mode 100644
index 0000000..6911c19
--- /dev/null
+++ b/src/protocols/rdp/channels/cliprdr.c
@@ -0,0 +1,620 @@
+/*
+ * 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/cliprdr.h"
+#include "client.h"
+#include "common/clipboard.h"
+#include "common/iconv.h"
+#include "config.h"
+#include "plugins/channels.h"
+#include "rdp.h"
+
+#include <freerdp/client/cliprdr.h>
+#include <freerdp/event.h>
+#include <freerdp/freerdp.h>
+#include <guacamole/client.h>
+#include <guacamole/stream.h>
+#include <guacamole/user.h>
+#include <winpr/wtsapi.h>
+#include <winpr/wtypes.h>
+
+#include <assert.h>
+#include <stdlib.h>
+#include <string.h>
+
+#ifdef FREERDP_CLIPRDR_CALLBACKS_REQUIRE_CONST
+/**
+ * FreeRDP 2.0.0-rc4 and newer requires the final argument for all CLIPRDR
+ * callbacks to be const.
+ */
+#define CLIPRDR_CONST const
+#else
+/**
+ * FreeRDP 2.0.0-rc3 and older requires the final argument for all CLIPRDR
+ * callbacks to NOT be const.
+ */
+#define CLIPRDR_CONST
+#endif
+
+/**
+ * Sends a Format List PDU to the RDP server containing the formats of
+ * clipboard data supported. This PDU is used both to indicate the general
+ * clipboard formats supported at the begining of an RDP session and to inform
+ * the RDP server that new clipboard data is available within the listed
+ * formats.
+ *
+ * @param cliprdr
+ *     The CliprdrClientContext structure used by FreeRDP to handle the
+ *     CLIPRDR channel for the current RDP session.
+ *
+ * @return
+ *     CHANNEL_RC_OK (zero) if the Format List PDU was sent successfully, an
+ *     error code (non-zero) otherwise.
+ */
+static UINT guac_rdp_cliprdr_send_format_list(CliprdrClientContext* cliprdr) {
+
+    /* This function is only invoked within FreeRDP-specific handlers for
+     * CLIPRDR, which are not assigned, and thus not callable, until after the
+     * relevant guac_rdp_clipboard structure is allocated and associated with
+     * the CliprdrClientContext */
+    guac_rdp_clipboard* clipboard = (guac_rdp_clipboard*) cliprdr->custom;
+    assert(clipboard != NULL);
+
+    /* We support CP-1252 and UTF-16 text */
+    CLIPRDR_FORMAT_LIST format_list = {
+        .formats = (CLIPRDR_FORMAT[]) {
+            { .formatId = CF_TEXT },
+            { .formatId = CF_UNICODETEXT }
+        },
+        .numFormats = 2
+    };
+
+    guac_client_log(clipboard->client, GUAC_LOG_TRACE, "CLIPRDR: Sending "
+            "format list");
+
+    return cliprdr->ClientFormatList(cliprdr, &format_list);
+
+}
+
+/**
+ * Sends a Clipboard Capabilities PDU to the RDP server describing the features
+ * of the CLIPRDR channel that are supported by the client.
+ *
+ * @param cliprdr
+ *     The CliprdrClientContext structure used by FreeRDP to handle the
+ *     CLIPRDR channel for the current RDP session.
+ *
+ * @return
+ *     CHANNEL_RC_OK (zero) if the Clipboard Capabilities PDU was sent
+ *     successfully, an error code (non-zero) otherwise.
+ */
+static UINT guac_rdp_cliprdr_send_capabilities(CliprdrClientContext* cliprdr) {
+
+    CLIPRDR_GENERAL_CAPABILITY_SET cap_set = {
+        .capabilitySetType = CB_CAPSTYPE_GENERAL, /* CLIPRDR specification requires that this is CB_CAPSTYPE_GENERAL, the only defined set type */
+        .capabilitySetLength = 12, /* The size of the capability set within the PDU - for CB_CAPSTYPE_GENERAL, this is ALWAYS 12 bytes */
+        .version = CB_CAPS_VERSION_2, /* The version of the CLIPRDR specification supported */
+        .generalFlags = CB_USE_LONG_FORMAT_NAMES /* Bitwise OR of all supported feature flags */
+    };
+
+    CLIPRDR_CAPABILITIES caps = {
+        .cCapabilitiesSets = 1,
+        .capabilitySets = (CLIPRDR_CAPABILITY_SET*) &cap_set
+    };
+
+    return cliprdr->ClientCapabilities(cliprdr, &caps);
+
+}
+
+/**
+ * Callback invoked by the FreeRDP CLIPRDR plugin for received Monitor Ready
+ * PDUs. The Monitor Ready PDU is sent by the RDP server only during
+ * initialization of the CLIPRDR channel. It is part of the CLIPRDR channel
+ * handshake and indicates that the RDP server's handling of clipboard
+ * redirection is ready to proceed.
+ *
+ * @param cliprdr
+ *     The CliprdrClientContext structure used by FreeRDP to handle the CLIPRDR
+ *     channel for the current RDP session.
+ *
+ * @param monitor_ready
+ *     The CLIPRDR_MONITOR_READY structure representing the Monitor Ready PDU
+ *     that was received.
+ *
+ * @return
+ *     CHANNEL_RC_OK (zero) if the PDU was handled successfully, an error code
+ *     (non-zero) otherwise.
+ */
+static UINT guac_rdp_cliprdr_monitor_ready(CliprdrClientContext* cliprdr,
+        CLIPRDR_CONST CLIPRDR_MONITOR_READY* monitor_ready) {
+
+    /* FreeRDP-specific handlers for CLIPRDR are not assigned, and thus not
+     * callable, until after the relevant guac_rdp_clipboard structure is
+     * allocated and associated with the CliprdrClientContext */
+    guac_rdp_clipboard* clipboard = (guac_rdp_clipboard*) cliprdr->custom;
+    assert(clipboard != NULL);
+
+    guac_client_log(clipboard->client, GUAC_LOG_TRACE, "CLIPRDR: Received "
+            "monitor ready.");
+
+    /* Respond with capabilities ... */
+    int status = guac_rdp_cliprdr_send_capabilities(cliprdr);
+    if (status != CHANNEL_RC_OK)
+        return status;
+
+    /* ... and supported format list */
+    return guac_rdp_cliprdr_send_format_list(cliprdr);
+
+}
+
+/**
+ * Sends a Format Data Request PDU to the RDP server, requesting that available
+ * clipboard data be sent to the client in the specified format. This PDU is
+ * sent when the server indicates that clipboard data is available via a Format
+ * List PDU.
+ *
+ * @param client
+ *     The guac_client associated with the current RDP session.
+ *
+ * @param format
+ *     The clipboard format to request. This format must be one of the
+ *     documented values used by the CLIPRDR channel for clipboard format IDs.
+ *
+ * @return
+ *     CHANNEL_RC_OK (zero) if the PDU was handled successfully, an error code
+ *     (non-zero) otherwise.
+ */
+static UINT guac_rdp_cliprdr_send_format_data_request(
+        CliprdrClientContext* cliprdr, UINT32 format) {
+
+    /* FreeRDP-specific handlers for CLIPRDR are not assigned, and thus not
+     * callable, until after the relevant guac_rdp_clipboard structure is
+     * allocated and associated with the CliprdrClientContext */
+    guac_rdp_clipboard* clipboard = (guac_rdp_clipboard*) cliprdr->custom;
+    assert(clipboard != NULL);
+
+    /* Create new data request */
+    CLIPRDR_FORMAT_DATA_REQUEST data_request = {
+        .requestedFormatId = format
+    };
+
+    /* Note the format we've requested for reference later when the requested
+     * data is received via a Format Data Response PDU */
+    clipboard->requested_format = format;
+
+    guac_client_log(clipboard->client, GUAC_LOG_TRACE, "CLIPRDR: Sending "
+            "format data request.");
+
+    /* Send request */
+    return cliprdr->ClientFormatDataRequest(cliprdr, &data_request);
+
+}
+
+/**
+ * Returns whether the given Format List PDU indicates support for the given
+ * clipboard format.
+ *
+ * @param format_list
+ *     The CLIPRDR_FORMAT_LIST structure representing the Format List PDU
+ *     being tested.
+ *
+ * @param format_id
+ *     The ID of the clipboard format to test, such as CF_TEXT or
+ *     CF_UNICODETEXT.
+ *
+ * @return
+ *     Non-zero if the given Format List PDU indicates support for the given
+ *     clipboard format, zero otherwise.
+ */
+static int guac_rdp_cliprdr_format_supported(const CLIPRDR_FORMAT_LIST* format_list,
+        UINT format_id) {
+
+    /* Search format list for matching ID */
+    for (int i = 0; i < format_list->numFormats; i++) {
+        if (format_list->formats[i].formatId == format_id)
+            return 1;
+    }
+
+    /* If no matching ID, format is not supported */
+    return 0;
+
+}
+
+/**
+ * Callback invoked by the FreeRDP CLIPRDR plugin for received Format List
+ * PDUs. The Format List PDU is sent by the RDP server to indicate that new
+ * clipboard data has been copied and is available for retrieval in the formats
+ * listed. A client wishing to retrieve that data responds with a Format Data
+ * Request PDU.
+ *
+ * @param cliprdr
+ *     The CliprdrClientContext structure used by FreeRDP to handle the CLIPRDR
+ *     channel for the current RDP session.
+ *
+ * @param format_list
+ *     The CLIPRDR_FORMAT_LIST structure representing the Format List PDU that
+ *     was received.
+ *
+ * @return
+ *     CHANNEL_RC_OK (zero) if the PDU was handled successfully, an error code
+ *     (non-zero) otherwise.
+ */
+static UINT guac_rdp_cliprdr_format_list(CliprdrClientContext* cliprdr,
+        CLIPRDR_CONST CLIPRDR_FORMAT_LIST* format_list) {
+
+    /* FreeRDP-specific handlers for CLIPRDR are not assigned, and thus not
+     * callable, until after the relevant guac_rdp_clipboard structure is
+     * allocated and associated with the CliprdrClientContext */
+    guac_rdp_clipboard* clipboard = (guac_rdp_clipboard*) cliprdr->custom;
+    assert(clipboard != NULL);
+
+    guac_client_log(clipboard->client, GUAC_LOG_TRACE, "CLIPRDR: Received "
+            "format list.");
+
+    CLIPRDR_FORMAT_LIST_RESPONSE format_list_response = {
+        .msgFlags = CB_RESPONSE_OK
+    };
+
+    /* Report successful processing of format list */
+    cliprdr->ClientFormatListResponse(cliprdr, &format_list_response);
+
+    /* Prefer Unicode (in this case, UTF-16) */
+    if (guac_rdp_cliprdr_format_supported(format_list, CF_UNICODETEXT))
+        return guac_rdp_cliprdr_send_format_data_request(cliprdr, CF_UNICODETEXT);
+
+    /* Use Windows' CP-1252 if Unicode unavailable */
+    if (guac_rdp_cliprdr_format_supported(format_list, CF_TEXT))
+        return guac_rdp_cliprdr_send_format_data_request(cliprdr, CF_TEXT);
+
+    /* Ignore any unsupported data */
+    guac_client_log(clipboard->client, GUAC_LOG_DEBUG, "Ignoring unsupported "
+            "clipboard data. Only Unicode and text clipboard formats are "
+            "currently supported.");
+
+    return CHANNEL_RC_OK;
+
+}
+
+/**
+ * Callback invoked by the FreeRDP CLIPRDR plugin for received Format Data
+ * Request PDUs. The Format Data Request PDU is sent by the RDP server when
+ * requesting that clipboard data be sent, in response to a received Format
+ * List PDU. The client is required to respond with a Format Data Response PDU
+ * containing the requested data.
+ *
+ * @param cliprdr
+ *     The CliprdrClientContext structure used by FreeRDP to handle the CLIPRDR
+ *     channel for the current RDP session.
+ *
+ * @param format_data_request
+ *     The CLIPRDR_FORMAT_DATA_REQUEST structure representing the Format Data
+ *     Request PDU that was received.
+ *
+ * @return
+ *     CHANNEL_RC_OK (zero) if the PDU was handled successfully, an error code
+ *     (non-zero) otherwise.
+ */
+static UINT guac_rdp_cliprdr_format_data_request(CliprdrClientContext* cliprdr,
+        CLIPRDR_CONST CLIPRDR_FORMAT_DATA_REQUEST* format_data_request) {
+
+    /* FreeRDP-specific handlers for CLIPRDR are not assigned, and thus not
+     * callable, until after the relevant guac_rdp_clipboard structure is
+     * allocated and associated with the CliprdrClientContext */
+    guac_rdp_clipboard* clipboard = (guac_rdp_clipboard*) cliprdr->custom;
+    assert(clipboard != NULL);
+
+    guac_client_log(clipboard->client, GUAC_LOG_TRACE, "CLIPRDR: Received "
+            "format data request.");
+
+    guac_iconv_write* writer;
+    const char* input = clipboard->clipboard->buffer;
+    char* output = malloc(GUAC_RDP_CLIPBOARD_MAX_LENGTH);
+
+    /* Map requested clipboard format to a guac_iconv writer */
+    switch (format_data_request->requestedFormatId) {
+
+        case CF_TEXT:
+            writer = GUAC_WRITE_CP1252;
+            break;
+
+        case CF_UNICODETEXT:
+            writer = GUAC_WRITE_UTF16;
+            break;
+
+        /* Warn if clipboard data cannot be sent as intended due to a violation
+         * of the CLIPRDR spec */
+        default:
+            guac_client_log(clipboard->client, GUAC_LOG_WARNING, "Received "
+                    "clipboard data cannot be sent to the RDP server because "
+                    "the RDP server has requested a clipboard format which "
+                    "was not declared as available. This violates the "
+                    "specification for the CLIPRDR channel.");
+            free(output);
+            return CHANNEL_RC_OK;
+
+    }
+
+    /* Send received clipboard data to the RDP server in the format
+     * requested */
+    BYTE* start = (BYTE*) output;
+    guac_iconv(GUAC_READ_UTF8, &input, clipboard->clipboard->length,
+               writer, &output, GUAC_RDP_CLIPBOARD_MAX_LENGTH);
+
+    CLIPRDR_FORMAT_DATA_RESPONSE data_response = {
+        .requestedFormatData = (BYTE*) start,
+        .dataLen = ((BYTE*) output) - start,
+        .msgFlags = CB_RESPONSE_OK
+    };
+
+    guac_client_log(clipboard->client, GUAC_LOG_TRACE, "CLIPRDR: Sending "
+            "format data response.");
+
+    return cliprdr->ClientFormatDataResponse(cliprdr, &data_response);
+
+}
+
+/**
+ * Callback invoked by the FreeRDP CLIPRDR plugin for received Format Data
+ * Response PDUs. The Format Data Response PDU is sent by the RDP server when
+ * fullfilling a request for clipboard data received via a Format Data Request
+ * PDU.
+ *
+ * @param cliprdr
+ *     The CliprdrClientContext structure used by FreeRDP to handle the CLIPRDR
+ *     channel for the current RDP session.
+ *
+ * @param format_data_response
+ *     The CLIPRDR_FORMAT_DATA_RESPONSE structure representing the Format Data
+ *     Response PDU that was received.
+ *
+ * @return
+ *     CHANNEL_RC_OK (zero) if the PDU was handled successfully, an error code
+ *     (non-zero) otherwise.
+ */
+static UINT guac_rdp_cliprdr_format_data_response(CliprdrClientContext* cliprdr,
+        CLIPRDR_CONST CLIPRDR_FORMAT_DATA_RESPONSE* format_data_response) {
+
+    /* FreeRDP-specific handlers for CLIPRDR are not assigned, and thus not
+     * callable, until after the relevant guac_rdp_clipboard structure is
+     * allocated and associated with the CliprdrClientContext */
+    guac_rdp_clipboard* clipboard = (guac_rdp_clipboard*) cliprdr->custom;
+    assert(clipboard != NULL);
+
+    guac_client_log(clipboard->client, GUAC_LOG_TRACE, "CLIPRDR: Received "
+            "format data response.");
+
+    char received_data[GUAC_RDP_CLIPBOARD_MAX_LENGTH];
+
+    guac_iconv_read* reader;
+    const char* input = (char*) format_data_response->requestedFormatData;
+    char* output = received_data;
+
+    /* Find correct source encoding */
+    switch (clipboard->requested_format) {
+
+        /* Non-Unicode (Windows CP-1252) */
+        case CF_TEXT:
+            reader = GUAC_READ_CP1252;
+            break;
+
+        /* Unicode (UTF-16) */
+        case CF_UNICODETEXT:
+            reader = GUAC_READ_UTF16;
+            break;
+
+        /* If the format ID stored within the guac_rdp_clipboard structure is actually
+         * not supported here, then something has been implemented incorrectly.
+         * Either incorrect values are (somehow) being stored, or support for
+         * the format indicated by that value is incomplete and must be added
+         * here. The values which may be stored within requested_format are
+         * completely within our control. */
+        default:
+            guac_client_log(clipboard->client, GUAC_LOG_DEBUG, "Requested "
+                    "clipboard data in unsupported format (0x%X).",
+                    clipboard->requested_format);
+            return CHANNEL_RC_OK;
+
+    }
+
+    /* Convert, store, and forward the clipboard data received from RDP
+     * server */
+    if (guac_iconv(reader, &input, format_data_response->dataLen,
+            GUAC_WRITE_UTF8, &output, sizeof(received_data))) {
+        int length = strnlen(received_data, sizeof(received_data));
+        guac_common_clipboard_reset(clipboard->clipboard, "text/plain");
+        guac_common_clipboard_append(clipboard->clipboard, received_data, length);
+        guac_common_clipboard_send(clipboard->clipboard, clipboard->client);
+    }
+
+    return CHANNEL_RC_OK;
+
+}
+
+/**
+ * Callback which associates handlers specific to Guacamole with the
+ * CliprdrClientContext instance allocated by FreeRDP to deal with received
+ * CLIPRDR (clipboard redirection) 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 CLIPRDR channel. This specific callback is registered with the PubSub
+ * system of the relevant rdpContext when guac_rdp_clipboard_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_cliprdr_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_clipboard* clipboard = rdp_client->clipboard;
+
+    /* FreeRDP-specific handlers for CLIPRDR are not assigned, and thus not
+     * callable, until after the relevant guac_rdp_clipboard structure is
+     * allocated and associated with the guac_rdp_client */
+    assert(clipboard != NULL);
+
+    /* Ignore connection event if it's not for the CLIPRDR channel */
+    if (strcmp(e->name, CLIPRDR_SVC_CHANNEL_NAME) != 0)
+        return;
+
+    /* The structure pointed to by pInterface is guaranteed to be a
+     * CliprdrClientContext if the channel is CLIPRDR */
+    CliprdrClientContext* cliprdr = (CliprdrClientContext*) e->pInterface;
+
+    /* Associate FreeRDP CLIPRDR context and its Guacamole counterpart with
+     * eachother */
+    cliprdr->custom = clipboard;
+    clipboard->cliprdr = cliprdr;
+
+    cliprdr->MonitorReady = guac_rdp_cliprdr_monitor_ready;
+    cliprdr->ServerFormatList = guac_rdp_cliprdr_format_list;
+    cliprdr->ServerFormatDataRequest = guac_rdp_cliprdr_format_data_request;
+    cliprdr->ServerFormatDataResponse = guac_rdp_cliprdr_format_data_response;
+
+    guac_client_log(client, GUAC_LOG_DEBUG, "CLIPRDR (clipboard redirection) "
+            "channel connected.");
+
+}
+
+guac_rdp_clipboard* guac_rdp_clipboard_alloc(guac_client* client) {
+
+    /* Allocate clipboard and underlying storage */
+    guac_rdp_clipboard* clipboard = calloc(1, sizeof(guac_rdp_clipboard));
+    clipboard->client = client;
+    clipboard->clipboard = guac_common_clipboard_alloc(GUAC_RDP_CLIPBOARD_MAX_LENGTH);
+    clipboard->requested_format = CF_TEXT;
+
+    return clipboard;
+
+}
+
+void guac_rdp_clipboard_load_plugin(guac_rdp_clipboard* clipboard,
+        rdpContext* context) {
+
+    /* Attempt to load FreeRDP support for the CLIPRDR channel */
+    if (guac_freerdp_channels_load_plugin(context, "cliprdr", NULL)) {
+        guac_client_log(clipboard->client, GUAC_LOG_WARNING,
+                "Support for the CLIPRDR channel (clipboard redirection) "
+                "could not be loaded. This support normally takes the form of "
+                "a plugin which is built into FreeRDP. Lacking this support, "
+                "clipboard will not work.");
+        return;
+    }
+
+    /* Complete RDP side of initialization when channel is connected */
+    PubSub_SubscribeChannelConnected(context->pubSub,
+            (pChannelConnectedEventHandler) guac_rdp_cliprdr_channel_connected);
+
+    guac_client_log(clipboard->client, GUAC_LOG_DEBUG, "Support for CLIPRDR "
+            "(clipboard redirection) registered. Awaiting channel "
+            "connection.");
+
+}
+
+void guac_rdp_clipboard_free(guac_rdp_clipboard* clipboard) {
+
+    /* Do nothing if the clipboard is not actually allocated */
+    if (clipboard == NULL)
+        return;
+
+    /* Free clipboard and underlying storage */
+    guac_common_clipboard_free(clipboard->clipboard);
+    free(clipboard);
+
+}
+
+int guac_rdp_clipboard_handler(guac_user* user, guac_stream* stream,
+        char* mimetype) {
+
+    guac_client* client = user->client;
+    guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
+
+    /* Ignore stream creation if no clipboard structure is available to handle
+     * received data */
+    guac_rdp_clipboard* clipboard = rdp_client->clipboard;
+    if (clipboard == NULL)
+        return 0;
+
+    /* Handle any future "blob" and "end" instructions for this stream with
+     * handlers that are aware of the RDP clipboard state */
+    stream->blob_handler = guac_rdp_clipboard_blob_handler;
+    stream->end_handler = guac_rdp_clipboard_end_handler;
+
+    /* Clear any current contents, assigning the mimetype the data which will
+     * be received */
+    guac_common_clipboard_reset(clipboard->clipboard, mimetype);
+    return 0;
+
+}
+
+int guac_rdp_clipboard_blob_handler(guac_user* user, guac_stream* stream,
+        void* data, int length) {
+
+    guac_client* client = user->client;
+    guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
+
+    /* Ignore received data if no clipboard structure is available to handle
+     * that data */
+    guac_rdp_clipboard* clipboard = rdp_client->clipboard;
+    if (clipboard == NULL)
+        return 0;
+
+    /* Append received data to current clipboard contents */
+    guac_common_clipboard_append(clipboard->clipboard, (char*) data, length);
+    return 0;
+
+}
+
+
+int guac_rdp_clipboard_end_handler(guac_user* user, guac_stream* stream) {
+
+    guac_client* client = user->client;
+    guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
+
+    /* Ignore end of stream if no clipboard structure is available to handle
+     * the data that was received */
+    guac_rdp_clipboard* clipboard = rdp_client->clipboard;
+    if (clipboard == NULL)
+        return 0;
+
+    /* Terminate clipboard data with NULL */
+    guac_common_clipboard_append(clipboard->clipboard, "", 1);
+
+    /* Notify RDP server of new data, if connected */
+    if (clipboard->cliprdr != NULL) {
+        guac_client_log(client, GUAC_LOG_DEBUG, "Clipboard data received. "
+                "Reporting availability of clipboard data to RDP server.");
+        guac_rdp_cliprdr_send_format_list(clipboard->cliprdr);
+    }
+    else
+        guac_client_log(client, GUAC_LOG_DEBUG, "Clipboard data has been "
+                "received, but cannot be sent to the RDP server because the "
+                "CLIPRDR channel is not yet connected.");
+
+    return 0;
+
+}
+
diff --git a/src/protocols/rdp/channels/cliprdr.h b/src/protocols/rdp/channels/cliprdr.h
new file mode 100644
index 0000000..4c920bf
--- /dev/null
+++ b/src/protocols/rdp/channels/cliprdr.h
@@ -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.
+ */
+
+#ifndef GUAC_RDP_CHANNELS_CLIPRDR_H
+#define GUAC_RDP_CHANNELS_CLIPRDR_H
+
+#include "common/clipboard.h"
+
+#include <freerdp/client/cliprdr.h>
+#include <freerdp/freerdp.h>
+#include <guacamole/client.h>
+#include <guacamole/user.h>
+#include <winpr/stream.h>
+#include <winpr/wtypes.h>
+
+/**
+ * RDP clipboard, leveraging the "CLIPRDR" channel.
+ */
+typedef struct guac_rdp_clipboard {
+
+    /**
+     * The guac_client associated with the RDP connection. The broadcast
+     * socket of this client will receive any clipboard data received from the
+     * RDP server.
+     */
+    guac_client* client;
+
+    /**
+     * CLIPRDR control interface.
+     */
+    CliprdrClientContext* cliprdr;
+
+    /**
+     * The current clipboard contents.
+     */
+    guac_common_clipboard* clipboard;
+
+    /**
+     * The format of the clipboard which was requested. Data received from
+     * the RDP server should conform to this format. This will be one of
+     * several legal clipboard format values defined within FreeRDP's WinPR
+     * library, such as CF_TEXT.
+     */
+    UINT requested_format;
+
+} guac_rdp_clipboard;
+
+/**
+ * Allocates a new guac_rdp_clipboard which has been initialized for processing
+ * of Guacamole clipboard data. Support for the RDP side of the clipboard (the
+ * CLIPRDR channel) must be loaded separately during FreeRDP's PreConnect event
+ * using guac_rdp_clipboard_load_plugin().
+ *
+ * @param client
+ *     The guac_client associated with the Guacamole side of the RDP
+ *     connection.
+ *
+ * @return
+ *     A newly-allocated instance of guac_rdp_clipboard which has been
+ *     initialized for processing Guacamole clipboard data.
+ */
+guac_rdp_clipboard* guac_rdp_clipboard_alloc(guac_client* client);
+
+/**
+ * Initializes clipboard support for RDP and handling of the CLIPRDR channel.
+ * If failures occur, messages noting the specifics of those failures will be
+ * logged, and the RDP side of clipboard support will not be functional.
+ *
+ * This MUST be called within the PreConnect callback of the freerdp instance
+ * for CLIPRDR support to be loaded.
+ *
+ * @param clipboard
+ *     The guac_rdp_clipboard instance which has been allocated for the current
+ *     RDP connection.
+ *
+ * @param context
+ *     The rdpContext associated with the FreeRDP side of the RDP connection.
+ */
+void guac_rdp_clipboard_load_plugin(guac_rdp_clipboard* clipboard,
+        rdpContext* context);
+
+/**
+ * Frees the resources associated with clipboard support for RDP and handling
+ * of the CLIPRDR channel. Only resources specific to Guacamole are freed.
+ * Resources specific to FreeRDP's handling of the CLIPRDR channel will be
+ * freed by FreeRDP. If the provided guac_rdp_clipboard is NULL, this function
+ * has no effect.
+ *
+ * @param clipboard
+ *     The guac_rdp_clipboard instance which was been allocated for the current
+ *     RDP connection.
+ */
+void guac_rdp_clipboard_free(guac_rdp_clipboard* clipboard);
+
+/**
+ * Handler for inbound clipboard data, received via the stream created by an
+ * inbound "clipboard" instruction. This handler will assign the
+ * stream-specific handlers for processing "blob" and "end" instructions which
+ * will eventually be received as clipboard data is sent. This specific handler
+ * is expected to be assigned to the guac_user object of any user that may send
+ * clipboard data. The guac_rdp_clipboard instance which will receive this data
+ * MUST already be stored on the guac_rdp_client structure associated with the
+ * current RDP connection.
+ */
+guac_user_clipboard_handler guac_rdp_clipboard_handler;
+
+/**
+ * Handler for stream data related to clipboard, received via "blob"
+ * instructions for a stream which has already been created with an inbound
+ * "clipboard" instruction. This specific handler is assigned to the
+ * guac_stream structure associated with that clipboard stream by
+ * guac_rdp_clipboard_handler(). The guac_rdp_clipboard instance which will
+ * receive this data MUST already be stored on the guac_rdp_client structure
+ * associated with the current RDP connection.
+ */
+guac_user_blob_handler guac_rdp_clipboard_blob_handler;
+
+/**
+ * Handler for end-of-stream related to clipboard, indicated via an "end"
+ * instruction for a stream which has already been created with an inbound
+ * "clipboard" instruction. This specific handler is assigned to the
+ * guac_stream structure associated with that clipboard stream by
+ * guac_rdp_clipboard_handler(). The guac_rdp_clipboard instance which will
+ * receive this data MUST already be stored on the guac_rdp_client structure
+ * associated with the current RDP connection.
+ */
+guac_user_end_handler guac_rdp_clipboard_end_handler;
+
+#endif
+
diff --git a/src/protocols/rdp/channels/common-svc.c b/src/protocols/rdp/channels/common-svc.c
new file mode 100644
index 0000000..b73dd6e
--- /dev/null
+++ b/src/protocols/rdp/channels/common-svc.c
@@ -0,0 +1,101 @@
+/*
+ * 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/common-svc.h"
+#include "plugins/channels.h"
+#include "rdp.h"
+
+#include <freerdp/settings.h>
+#include <guacamole/client.h>
+#include <guacamole/string.h>
+#include <winpr/stream.h>
+#include <winpr/wtsapi.h>
+#include <winpr/wtypes.h>
+
+#include <stdlib.h>
+
+int guac_rdp_common_svc_load_plugin(rdpContext* context,
+        char* name, ULONG channel_options,
+        guac_rdp_common_svc_connect_handler* connect_handler,
+        guac_rdp_common_svc_receive_handler* receive_handler,
+        guac_rdp_common_svc_terminate_handler* terminate_handler) {
+
+    guac_client* client = ((rdp_freerdp_context*) context)->client;
+
+    guac_rdp_common_svc* svc = calloc(1, sizeof(guac_rdp_common_svc));
+    svc->client = client;
+    svc->name = svc->_channel_def.name;
+    svc->_connect_handler = connect_handler;
+    svc->_receive_handler = receive_handler;
+    svc->_terminate_handler = terminate_handler;
+
+    /* Init FreeRDP channel definition */
+    int name_length = guac_strlcpy(svc->_channel_def.name, name, GUAC_RDP_SVC_MAX_LENGTH);
+    svc->_channel_def.options =
+          CHANNEL_OPTION_INITIALIZED
+        | CHANNEL_OPTION_ENCRYPT_RDP
+        | channel_options;
+
+    /* Warn about name length */
+    if (name_length >= GUAC_RDP_SVC_MAX_LENGTH)
+        guac_client_log(client, GUAC_LOG_WARNING,
+                "Static channel name \"%s\" exceeds maximum length of %i "
+                "characters and will be truncated to \"%s\".",
+                name, GUAC_RDP_SVC_MAX_LENGTH - 1, svc->name);
+
+    /* Attempt to load the common SVC plugin for new static channel */
+    int result = guac_freerdp_channels_load_plugin(context, "guac-common-svc", svc);
+    if (result) {
+        guac_client_log(client, GUAC_LOG_WARNING, "Cannot create static "
+                "channel \"%s\": failed to load \"guac-common-svc\" plugin "
+                "for FreeRDP.", svc->name);
+        free(svc);
+    }
+
+    /* Store and log on success (SVC structure will be freed on channel termination) */
+    else
+        guac_client_log(client, GUAC_LOG_DEBUG, "Support for static channel "
+                "\"%s\" loaded.", svc->name);
+
+    return result;
+
+}
+
+void guac_rdp_common_svc_write(guac_rdp_common_svc* svc,
+        wStream* output_stream) {
+
+    /* Do not write if plugin not associated */
+    if (!svc->_open_handle) {
+        guac_client_log(svc->client, GUAC_LOG_WARNING, "%i bytes of data "
+                "written to SVC \"%s\" are being dropped because the remote "
+                "desktop side of that SVC is not yet connected.",
+                Stream_Length(output_stream), svc->name);
+        return;
+    }
+
+    /* NOTE: Data sent via pVirtualChannelWriteEx MUST always be dynamically
+     * allocated, as it will be automatically freed using free(). If provided,
+     * the last parameter (user data) MUST be a pointer to a wStream, as it
+     * will automatically be freed by FreeRDP using Stream_Free() */
+    svc->_entry_points.pVirtualChannelWriteEx(svc->_init_handle,
+            svc->_open_handle, Stream_Buffer(output_stream),
+            Stream_GetPosition(output_stream), output_stream);
+
+}
+
diff --git a/src/protocols/rdp/channels/common-svc.h b/src/protocols/rdp/channels/common-svc.h
new file mode 100644
index 0000000..0a420bc
--- /dev/null
+++ b/src/protocols/rdp/channels/common-svc.h
@@ -0,0 +1,229 @@
+/*
+ * 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_COMMON_SVC_H
+#define GUAC_RDP_CHANNELS_COMMON_SVC_H
+
+#include <freerdp/freerdp.h>
+#include <freerdp/svc.h>
+#include <guacamole/client.h>
+#include <guacamole/stream.h>
+#include <winpr/stream.h>
+#include <winpr/wtsapi.h>
+#include <winpr/wtypes.h>
+
+/**
+ * The maximum number of bytes to allow within each channel name, including
+ * null terminator.
+ */
+#define GUAC_RDP_SVC_MAX_LENGTH 8
+
+/**
+ * The maximum number of bytes that the RDP server will be allowed to send
+ * within any single write operation, regardless of the number of chunks that
+ * write is split into. Bytes beyond this limit may be dropped.
+ */
+#define GUAC_SVC_MAX_ASSEMBLED_LENGTH 1048576
+
+/**
+ * Structure describing a static virtual channel, and the corresponding
+ * Guacamole pipes and FreeRDP resources.
+ */
+typedef struct guac_rdp_common_svc guac_rdp_common_svc;
+
+/**
+ * Handler which is invoked when a CHANNEL_EVENT_CONNECTED event has been
+ * processed and the connection/initialization process of the SVC is now
+ * complete.
+ *
+ * @param svc
+ *     The guac_rdp_common_svc structure representing the SVC that is now
+ *     connected.
+ */
+typedef void guac_rdp_common_svc_connect_handler(guac_rdp_common_svc* svc);
+
+/**
+ * Handler which is invoked when a logical block of data has been received
+ * along an SVC, having been reassembled from a series of
+ * CHANNEL_EVENT_DATA_RECEIVED events.
+ *
+ * @param svc
+ *     The guac_rdp_common_svc structure representing the SVC that received the
+ *     data.
+ *
+ * @param input_stream
+ *     The reassembled block of data that was received.
+ */
+typedef void guac_rdp_common_svc_receive_handler(guac_rdp_common_svc* svc, wStream* input_stream);
+
+/**
+ * Handler which is invoked when a CHANNEL_EVENT_TERMINATED event has been
+ * processed and all resources associated with the SVC must now be freed.
+ *
+ * @param svc
+ *     The guac_rdp_common_svc structure representing the SVC that has been
+ *     terminated.
+ */
+typedef void guac_rdp_common_svc_terminate_handler(guac_rdp_common_svc* svc);
+
+struct guac_rdp_common_svc {
+
+    /**
+     * Reference to the client owning this static channel.
+     */
+    guac_client* client;
+
+    /**
+     * The name of the static virtual channel, as specified to
+     * guac_rdp_common_svc_load_plugin(). This value is stored and defined
+     * internally by the CHANNEL_DEF.
+     */
+    const char* name;
+
+    /**
+     * Arbitrary channel-specific data which may be assigned and referenced by
+     * channel implementations leveraging the "guac-common-svc" plugin.
+     */
+    void* data;
+
+    /**
+     * Handler which is invoked when handling a CHANNEL_EVENT_CONNECTED event.
+     */
+    guac_rdp_common_svc_connect_handler* _connect_handler;
+
+    /**
+     * Handler which is invoked when all chunks of data for a single logical
+     * block have been received via CHANNEL_EVENT_DATA_RECEIVED events and
+     * reassembled.
+     */
+    guac_rdp_common_svc_receive_handler* _receive_handler;
+
+    /**
+     * Handler which is invokved when the SVC has been disconnected and is
+     * about to be freed.
+     */
+    guac_rdp_common_svc_terminate_handler* _terminate_handler;
+
+    /**
+     * The definition of this static virtual channel, including its name.
+     */
+    CHANNEL_DEF _channel_def;
+
+    /**
+     * Functions and data specific to the FreeRDP side of the virtual channel
+     * and plugin.
+     */
+    CHANNEL_ENTRY_POINTS_FREERDP_EX _entry_points;
+
+    /**
+     * Handle which identifies the client connection, typically referred to
+     * within the FreeRDP source as pInitHandle. This handle is provided to the
+     * channel entry point and the channel init event handler. The handle must
+     * eventually be used within the channel open event handler to obtain a
+     * handle to the channel itself.
+     */
+    PVOID _init_handle;
+
+    /**
+     * Handle which identifies the channel itself, typically referred to within
+     * the FreeRDP source as OpenHandle. This handle is obtained through a call
+     * to entry_points.pVirtualChannelOpenEx() in response to receiving a
+     * CHANNEL_EVENT_CONNECTED event via the init event handler.
+     *
+     * Data is received in CHANNEL_EVENT_DATA_RECEIVED events via the open
+     * event handler, and data is written through calls to
+     * entry_points.pVirtualChannelWriteEx().
+     */
+    DWORD _open_handle;
+
+    /**
+     * All data that has been received thus far from the current RDP server
+     * write operation. Data received along virtual channels is sent in chunks
+     * (typically 1600 bytes), and thus must be gradually reassembled as it is
+     * received.
+     */
+    wStream* _input_stream;
+
+};
+
+/**
+ * Initializes arbitrary static virtual channel (SVC) support for RDP, loading
+ * a new instance of Guacamole's arbitrary SVC plugin for FreeRDP ("guacsvc")
+ * supporting the channel having the given name. Data sent from within the RDP
+ * session using this channel will be sent along an identically-named pipe
+ * stream to the Guacamole client, and data sent along a pipe stream having the
+ * same name will be written to the SVC and received within the RDP session. If
+ * failures occur while loading the plugin, messages noting the specifics of
+ * those failures will be logged, and support for the given channel will not be
+ * functional.
+ *
+ * This MUST be called within the PreConnect callback of the freerdp instance
+ * for static virtual channel support to be loaded.
+ *
+ * @param context
+ *     The rdpContext associated with the FreeRDP side of the RDP connection.
+ *
+ * @param name
+ *     The name of the SVC which should be handled by the new instance of the
+ *     plugin.
+ *
+ * @param channel_options
+ *     Bitwise OR of any of the several CHANNEL_OPTION_* flags. Regardless of
+ *     whether specified here, the CHANNEL_OPTION_INTIALIZED and
+ *     CHANNEL_OPTION_ENCRYPT_RDP flags will automatically be set.
+ *
+ * @param connect_handler
+ *     The function to invoke when the SVC has been connected.
+ *
+ * @param receive_handler
+ *     The function to invoke when the SVC has received a logical block of
+ *     data, reassembled from perhaps several smaller chunks of data.
+ *
+ * @param terminate_handler
+ *     The function to invoke when the SVC has been disconnected and is about
+ *     to be freed.
+ *
+ * @return
+ *     Zero if the plugin was loaded successfully, non-zero if the plugin could
+ *     not be loaded.
+ */
+int guac_rdp_common_svc_load_plugin(rdpContext* context,
+        char* name, ULONG channel_options,
+        guac_rdp_common_svc_connect_handler* connect_handler,
+        guac_rdp_common_svc_receive_handler* receive_handler,
+        guac_rdp_common_svc_terminate_handler* terminate_handler);
+
+/**
+ * Writes the given data to the virtual channel such that it can be received
+ * within the RDP session. The given data MUST be dynamically allocated, as the
+ * write operation may be queued and the actual write may not occur until
+ * later. The provided wStream and the buffer it points to will be
+ * automatically freed after the write occurs.
+ *
+ * @param svc
+ *     The static virtual channel to write data to.
+ *
+ * @param output_stream
+ *     The data to write, which MUST be dynamically allocated.
+ */
+void guac_rdp_common_svc_write(guac_rdp_common_svc* svc,
+        wStream* output_stream);
+
+#endif
+
diff --git a/src/protocols/rdp/rdp_disp.c b/src/protocols/rdp/channels/disp.c
similarity index 69%
rename from src/protocols/rdp/rdp_disp.c
rename to src/protocols/rdp/channels/disp.c
index 7c7e059..a25f6fa 100644
--- a/src/protocols/rdp/rdp_disp.c
+++ b/src/protocols/rdp/channels/disp.c
@@ -17,29 +17,26 @@
  * under the License.
  */
 
-#include "config.h"
-#include "client.h"
-#include "dvc.h"
+#include "channels/disp.h"
+#include "plugins/channels.h"
 #include "rdp.h"
-#include "rdp_disp.h"
-#include "rdp_settings.h"
+#include "settings.h"
 
+#include <freerdp/client/disp.h>
 #include <freerdp/freerdp.h>
+#include <freerdp/event.h>
 #include <guacamole/client.h>
 #include <guacamole/timestamp.h>
 
-#ifdef HAVE_FREERDP_CLIENT_DISP_H
-#include <freerdp/client/disp.h>
-#endif
+#include <stdlib.h>
+#include <string.h>
 
 guac_rdp_disp* guac_rdp_disp_alloc() {
 
     guac_rdp_disp* disp = malloc(sizeof(guac_rdp_disp));
 
-#ifdef HAVE_FREERDP_DISPLAY_UPDATE_SUPPORT
     /* Not yet connected */
     disp->disp = NULL;
-#endif
 
     /* No requests have been made */
     disp->last_request = guac_timestamp_current();
@@ -55,23 +52,60 @@
     free(disp);
 }
 
-void guac_rdp_disp_load_plugin(rdpContext* context, guac_rdp_dvc_list* list) {
+/**
+ * Callback which associates handlers specific to Guacamole with the
+ * DispClientContext instance allocated by FreeRDP to deal with received
+ * Display Update (client-initiated dynamic display resizing) 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 Display Update channel. This specific callback is registered with the
+ * PubSub system of the relevant rdpContext when guac_rdp_disp_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_disp_channel_connected(rdpContext* context,
+        ChannelConnectedEventArgs* e) {
 
-#ifdef HAVE_RDPSETTINGS_SUPPORTDISPLAYCONTROL
-    context->settings->SupportDisplayControl = TRUE;
-#endif
+    guac_client* client = ((rdp_freerdp_context*) context)->client;
+    guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
+    guac_rdp_disp* guac_disp = rdp_client->disp;
+
+    /* Ignore connection event if it's not for the Display Update channel */
+    if (strcmp(e->name, DISP_DVC_CHANNEL_NAME) != 0)
+        return;
+
+    /* Init module with current display size */
+    guac_rdp_disp_set_size(guac_disp, rdp_client->settings,
+            context->instance, guac_rdp_get_width(context->instance),
+            guac_rdp_get_height(context->instance));
+
+    /* Store reference to the display update plugin once it's connected */
+    DispClientContext* disp = (DispClientContext*) e->pInterface;
+    guac_disp->disp = disp;
+
+    guac_client_log(client, GUAC_LOG_DEBUG, "Display update channel "
+            "will be used for display size changes.");
+
+}
+
+void guac_rdp_disp_load_plugin(rdpContext* context) {
+
+    /* Subscribe to and handle channel connected events */
+    PubSub_SubscribeChannelConnected(context->pubSub,
+        (pChannelConnectedEventHandler) guac_rdp_disp_channel_connected);
 
     /* Add "disp" channel */
-    guac_rdp_dvc_list_add(list, "disp", NULL);
+    guac_freerdp_dynamic_channel_collection_add(context->settings, "disp", NULL);
 
 }
 
-#ifdef HAVE_FREERDP_DISPLAY_UPDATE_SUPPORT
-void guac_rdp_disp_connect(guac_rdp_disp* guac_disp, DispClientContext* disp) {
-    guac_disp->disp = disp;
-}
-#endif
-
 /**
  * Fits a given dimension within the allowed bounds for Display Update
  * messages, adjusting the other dimension such that aspect ratio is
@@ -172,7 +206,6 @@
     }
 
     else if (settings->resize_method == GUAC_RESIZE_DISPLAY_UPDATE) {
-#ifdef HAVE_FREERDP_DISPLAY_UPDATE_SUPPORT
         DISPLAY_CONTROL_MONITOR_LAYOUT monitors[1] = {{
             .Flags  = 0x1, /* DISPLAYCONTROL_MONITOR_PRIMARY */
             .Left = 0,
@@ -189,7 +222,6 @@
         /* Send display update notification if display channel is connected */
         if (disp->disp != NULL)
             disp->disp->SendMonitorLayout(disp->disp, 1, monitors);
-#endif
     }
 
 }
diff --git a/src/protocols/rdp/rdp_disp.h b/src/protocols/rdp/channels/disp.h
similarity index 75%
rename from src/protocols/rdp/rdp_disp.h
rename to src/protocols/rdp/channels/disp.h
index 0f34fe1..2aff7c2 100644
--- a/src/protocols/rdp/rdp_disp.h
+++ b/src/protocols/rdp/channels/disp.h
@@ -17,17 +17,15 @@
  * under the License.
  */
 
-#ifndef GUAC_RDP_DISP_H
-#define GUAC_RDP_DISP_H
+#ifndef GUAC_RDP_CHANNELS_DISP_H
+#define GUAC_RDP_CHANNELS_DISP_H
 
-#include "dvc.h"
-#include "rdp_settings.h"
+#include "settings.h"
 
-#include <freerdp/freerdp.h>
-
-#ifdef HAVE_FREERDP_CLIENT_DISP_H
 #include <freerdp/client/disp.h>
-#endif
+#include <freerdp/freerdp.h>
+#include <guacamole/client.h>
+#include <guacamole/timestamp.h>
 
 /**
  * The minimum value for width or height, in pixels.
@@ -50,12 +48,10 @@
  */
 typedef struct guac_rdp_disp {
 
-#ifdef HAVE_FREERDP_DISPLAY_UPDATE_SUPPORT
     /**
      * Display control interface.
      */
     DispClientContext* disp;
-#endif
 
     /**
      * The timestamp of the last display update request, or 0 if no request
@@ -83,53 +79,43 @@
 
 /**
  * Allocates a new display update module, which will ultimately control the
- * display update channel once conected.
+ * display update channel once connected.
  *
- * @return A new display update module.
+ * @return
+ *     A newly-allocated display update module.
  */
 guac_rdp_disp* guac_rdp_disp_alloc();
 
 /**
- * Frees the given display update module.
+ * Frees the resources associated with support for the RDP Display Update
+ * channel. Only resources specific to Guacamole are freed. Resources specific
+ * to FreeRDP's handling of the Display Update channel will be freed by
+ * FreeRDP. If no resources are currently allocated for Display Update support,
+ * this function has no effect.
  *
- * @param disp The display update module to free.
+ * @param disp
+ *     The display update module to free.
  */
 void guac_rdp_disp_free(guac_rdp_disp* disp);
 
 /**
- * @param context The rdpContext associated with the active RDP session.
- */
-/**
  * Adds FreeRDP's "disp" plugin to the list of dynamic virtual channel plugins
- * to be loaded by FreeRDP's "drdynvc" plugin. The plugin will only be loaded
- * once guac_rdp_load_drdynvc() is invoked with the guac_rdp_dvc_list passed to
- * this function. The "disp" plugin ultimately adds support for the Display
- * Update channel. NOTE: It is still up to external code to detect when the
- * "disp" channel is connected, and update the guac_rdp_disp with a call to
- * guac_rdp_disp_connect().
+ * to be loaded by FreeRDP's "drdynvc" plugin. The context of the plugin will
+ * automatically be assicated with the guac_rdp_disp instance pointed to by the
+ * current guac_rdp_client. The plugin will only be loaded once the "drdynvc"
+ * plugin is loaded. The "disp" plugin ultimately adds support for the Display
+ * Update channel.
+ *
+ * If failures occur, messages noting the specifics of those failures will be
+ * logged, and the RDP side of Display Update support will not be functional.
+ *
+ * This MUST be called within the PreConnect callback of the freerdp instance
+ * for Display Update support to be loaded.
  *
  * @param context
  *     The rdpContext associated with the active RDP session.
- *
- * @param list
- *     The guac_rdp_dvc_list to which the "disp" plugin should be added, such
- *     that it may later be loaded by guac_rdp_load_drdynvc().
  */
-void guac_rdp_disp_load_plugin(rdpContext* context, guac_rdp_dvc_list* list);
-
-#ifdef HAVE_FREERDP_DISPLAY_UPDATE_SUPPORT
-/**
- * Stores the given DispClientContext within the given guac_rdp_disp, such that
- * display updates can be properly sent. Until this is called, changes to the
- * display size will be deferred.
- *
- * @param guac_disp The display update module to associate with the connected
- *                  display update channel.
- * @param disp The DispClientContext associated by FreeRDP with the connected
- *             display update channel.
- */
-void guac_rdp_disp_connect(guac_rdp_disp* guac_disp, DispClientContext* disp);
-#endif
+void guac_rdp_disp_load_plugin(rdpContext* context);
 
 /**
  * Requests a display size update, which may then be sent immediately to the
@@ -200,6 +186,10 @@
  * Returns whether a full RDP reconnect is required for display update changes
  * to take effect.
  *
+ * @param disp
+ *     The display update module that should be checked to determine whether a
+ *     reconnect is required.
+ *
  * @return
  *     Non-zero if a reconnect is needed, zero otherwise.
  */
diff --git a/src/protocols/rdp/channels/pipe-svc.c b/src/protocols/rdp/channels/pipe-svc.c
new file mode 100644
index 0000000..2db42d6
--- /dev/null
+++ b/src/protocols/rdp/channels/pipe-svc.c
@@ -0,0 +1,230 @@
+/*
+ * 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/common-svc.h"
+#include "channels/pipe-svc.h"
+#include "common/list.h"
+#include "rdp.h"
+
+#include <freerdp/settings.h>
+#include <guacamole/client.h>
+#include <guacamole/protocol.h>
+#include <guacamole/socket.h>
+#include <guacamole/stream.h>
+#include <guacamole/user.h>
+#include <winpr/stream.h>
+
+#include <stdlib.h>
+#include <string.h>
+
+void guac_rdp_pipe_svc_send_pipe(guac_socket* socket, guac_rdp_pipe_svc* pipe_svc) {
+
+    /* Send pipe instruction for the SVC's output stream */
+    guac_protocol_send_pipe(socket, pipe_svc->output_pipe,
+            "application/octet-stream", pipe_svc->svc->name);
+
+}
+
+void guac_rdp_pipe_svc_send_pipes(guac_user* user) {
+
+    guac_client* client = user->client;
+    guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
+
+    guac_common_list_lock(rdp_client->available_svc);
+
+    /* Send pipe for each allocated SVC's output stream */
+    guac_common_list_element* current = rdp_client->available_svc->head;
+    while (current != NULL) {
+        guac_rdp_pipe_svc_send_pipe(user->socket, (guac_rdp_pipe_svc*) current->data);
+        current = current->next;
+    }
+
+    guac_common_list_unlock(rdp_client->available_svc);
+
+}
+
+void guac_rdp_pipe_svc_add(guac_client* client, guac_rdp_pipe_svc* pipe_svc) {
+
+    guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
+
+    /* Add to list of available SVC */
+    guac_common_list_lock(rdp_client->available_svc);
+    guac_common_list_add(rdp_client->available_svc, pipe_svc);
+    guac_common_list_unlock(rdp_client->available_svc);
+
+}
+
+guac_rdp_pipe_svc* guac_rdp_pipe_svc_get(guac_client* client, const char* name) {
+
+    guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
+    guac_common_list_element* current;
+    guac_rdp_pipe_svc* found = NULL;
+
+    /* For each available SVC */
+    guac_common_list_lock(rdp_client->available_svc);
+    current = rdp_client->available_svc->head;
+    while (current != NULL) {
+
+        /* If name matches, found */
+        guac_rdp_pipe_svc* current_svc = (guac_rdp_pipe_svc*) current->data;
+        if (strcmp(current_svc->svc->name, name) == 0) {
+            found = current_svc;
+            break;
+        }
+
+        current = current->next;
+
+    }
+    guac_common_list_unlock(rdp_client->available_svc);
+
+    return found;
+
+}
+
+guac_rdp_pipe_svc* guac_rdp_pipe_svc_remove(guac_client* client, const char* name) {
+
+    guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
+    guac_common_list_element* current;
+    guac_rdp_pipe_svc* found = NULL;
+
+    /* For each available SVC */
+    guac_common_list_lock(rdp_client->available_svc);
+    current = rdp_client->available_svc->head;
+    while (current != NULL) {
+
+        /* If name matches, remove entry */
+        guac_rdp_pipe_svc* current_svc = (guac_rdp_pipe_svc*) current->data;
+        if (strcmp(current_svc->svc->name, name) == 0) {
+            guac_common_list_remove(rdp_client->available_svc, current);
+            found = current_svc;
+            break;
+        }
+
+        current = current->next;
+
+    }
+    guac_common_list_unlock(rdp_client->available_svc);
+
+    /* Return removed entry, if any */
+    return found;
+
+}
+
+int guac_rdp_pipe_svc_pipe_handler(guac_user* user, guac_stream* stream,
+        char* mimetype, char* name) {
+
+    guac_rdp_pipe_svc* pipe_svc = guac_rdp_pipe_svc_get(user->client, name);
+
+    /* Fail if no such SVC */
+    if (pipe_svc == NULL) {
+        guac_user_log(user, GUAC_LOG_WARNING, "User requested non-existent "
+                "pipe (no such SVC configured): \"%s\"", name);
+        guac_protocol_send_ack(user->socket, stream, "FAIL (NO SUCH PIPE)",
+                GUAC_PROTOCOL_STATUS_CLIENT_BAD_REQUEST);
+        guac_socket_flush(user->socket);
+        return 0;
+    }
+    else
+        guac_user_log(user, GUAC_LOG_DEBUG, "Inbound half of channel \"%s\" "
+                "connected.", name);
+
+    /* Init stream data */
+    stream->data = pipe_svc;
+    stream->blob_handler = guac_rdp_pipe_svc_blob_handler;
+
+    return 0;
+
+}
+
+int guac_rdp_pipe_svc_blob_handler(guac_user* user, guac_stream* stream,
+        void* data, int length) {
+
+    guac_rdp_pipe_svc* pipe_svc = (guac_rdp_pipe_svc*) stream->data;
+
+    /* Write blob data to SVC directly */
+    wStream* output_stream = Stream_New(NULL, length);
+    Stream_Write(output_stream, data, length);
+    guac_rdp_common_svc_write(pipe_svc->svc, output_stream);
+
+    guac_protocol_send_ack(user->socket, stream, "OK (DATA RECEIVED)",
+            GUAC_PROTOCOL_STATUS_SUCCESS);
+    guac_socket_flush(user->socket);
+    return 0;
+
+}
+
+void guac_rdp_pipe_svc_process_connect(guac_rdp_common_svc* svc) {
+
+    /* Associate SVC with new Guacamole pipe */
+    guac_rdp_pipe_svc* pipe_svc = malloc(sizeof(guac_rdp_pipe_svc));
+    pipe_svc->svc = svc;
+    pipe_svc->output_pipe = guac_client_alloc_stream(svc->client);
+    svc->data = pipe_svc;
+
+    /* SVC may now receive data from client */
+    guac_rdp_pipe_svc_add(svc->client, pipe_svc);
+
+    /* Notify of pipe's existence */
+    guac_rdp_pipe_svc_send_pipe(svc->client->socket, pipe_svc);
+
+}
+
+void guac_rdp_pipe_svc_process_receive(guac_rdp_common_svc* svc,
+        wStream* input_stream) {
+
+    guac_rdp_pipe_svc* pipe_svc = (guac_rdp_pipe_svc*) svc->data;
+
+    /* Fail if output not created */
+    if (pipe_svc->output_pipe == NULL) {
+        guac_client_log(svc->client, GUAC_LOG_WARNING, "%i bytes of data "
+                "received from within the remote desktop session for SVC "
+                "\"%s\" are being dropped because the outbound pipe stream "
+                "for that SVC is not yet open. This should NOT happen.",
+                Stream_Length(input_stream), svc->name);
+        return;
+    }
+
+    /* Send received data as blob */
+    guac_protocol_send_blob(svc->client->socket, pipe_svc->output_pipe, Stream_Buffer(input_stream), Stream_Length(input_stream));
+    guac_socket_flush(svc->client->socket);
+
+}
+
+void guac_rdp_pipe_svc_process_terminate(guac_rdp_common_svc* svc) {
+
+    guac_rdp_pipe_svc* pipe_svc = (guac_rdp_pipe_svc*) svc->data;
+    if (pipe_svc == NULL)
+        return;
+
+    /* Remove and free SVC */
+    guac_rdp_pipe_svc_remove(svc->client, svc->name);
+    free(pipe_svc);
+
+}
+
+void guac_rdp_pipe_svc_load_plugin(rdpContext* context, char* name) {
+
+    /* Attempt to load support for static channel */
+    guac_rdp_common_svc_load_plugin(context, name, CHANNEL_OPTION_COMPRESS_RDP,
+            guac_rdp_pipe_svc_process_connect,
+            guac_rdp_pipe_svc_process_receive,
+            guac_rdp_pipe_svc_process_terminate);
+
+}
+
diff --git a/src/protocols/rdp/channels/pipe-svc.h b/src/protocols/rdp/channels/pipe-svc.h
new file mode 100644
index 0000000..242d4e5
--- /dev/null
+++ b/src/protocols/rdp/channels/pipe-svc.h
@@ -0,0 +1,188 @@
+/*
+ * 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_PIPE_SVC_H
+#define GUAC_RDP_CHANNELS_PIPE_SVC_H
+
+#include "channels/common-svc.h"
+
+#include <freerdp/freerdp.h>
+#include <freerdp/svc.h>
+#include <guacamole/client.h>
+#include <guacamole/stream.h>
+#include <guacamole/socket.h>
+#include <guacamole/user.h>
+#include <winpr/wtsapi.h>
+
+/**
+ * The maximum number of bytes to allow within each channel name, including
+ * null terminator.
+ */
+#define GUAC_RDP_SVC_MAX_LENGTH 8
+
+/**
+ * Structure describing a static virtual channel and a corresponding Guacamole
+ * pipe stream;
+ */
+typedef struct guac_rdp_pipe_svc {
+
+    /**
+     * The output pipe, opened when the RDP server receives a connection to
+     * the static channel.
+     */
+    guac_stream* output_pipe;
+
+    /**
+     * The underlying static channel. Data written to this SVC by the RDP
+     * server will be forwarded along the pipe stream to the Guacamole client,
+     * and data written to the pipe stream by the Guacamole client will be
+     * forwarded along the SVC to the RDP server.
+     */
+    guac_rdp_common_svc* svc;
+
+} guac_rdp_pipe_svc;
+
+/**
+ * Initializes arbitrary static virtual channel (SVC) support for RDP, handling
+ * communication for the SVC having the given name. Data sent from within the
+ * RDP session using this channel will be sent along an identically-named pipe
+ * stream to the Guacamole client, and data sent along a pipe stream having the
+ * same name will be written to the SVC and received within the RDP session. If
+ * failures occur while loading the plugin, messages noting the specifics of
+ * those failures will be logged, and support for the given channel will not be
+ * functional.
+ *
+ * This MUST be called within the PreConnect callback of the freerdp instance
+ * for static virtual channel support to be loaded.
+ *
+ * @param context
+ *     The rdpContext associated with the FreeRDP side of the RDP connection.
+ *
+ * @param name
+ *     The name of the SVC which should be handled.
+ */
+void guac_rdp_pipe_svc_load_plugin(rdpContext* context, char* name);
+
+/**
+ * Sends the "pipe" instruction describing the given static virtual channel
+ * along the given socket. This pipe instruction will relate the SVC's
+ * underlying output stream with the SVC's name and the mimetype
+ * "application/octet-stream".
+ *
+ * @param socket
+ *     The socket along which the "pipe" instruction should be sent.
+ *
+ * @param svc
+ *     The static virtual channel that the "pipe" instruction should describe.
+ */
+void guac_rdp_pipe_svc_send_pipe(guac_socket* socket, guac_rdp_pipe_svc* svc);
+
+/**
+ * Sends the "pipe" instructions describing all static virtual channels
+ * available to the given user along that user's socket. Each pipe instruction
+ * will relate the associated SVC's underlying output stream with the SVC's
+ * name and the mimetype "application/octet-stream".
+ *
+ * @param user
+ *     The user to send the "pipe" instructions to.
+ */
+void guac_rdp_pipe_svc_send_pipes(guac_user* user);
+
+/**
+ * Add the given SVC to the list of all available SVCs. This function must be
+ * invoked after the SVC is connected for inbound pipe streams having that
+ * SVC's name to result in received data being sent into the RDP session.
+ *
+ * @param client
+ *     The guac_client associated with the current RDP session.
+ *
+ * @param svc
+ *     The static virtual channel to add to the list of all such channels
+ *     available.
+ */
+void guac_rdp_pipe_svc_add(guac_client* client, guac_rdp_pipe_svc* svc);
+
+/**
+ * Retrieve the SVC with the given name from the list stored in the client. The
+ * requested SVC must previously have been added using guac_rdp_pipe_svc_add().
+ *
+ * @param client
+ *     The guac_client associated with the current RDP session.
+ *
+ * @param name
+ *     The name of the static virtual channel to retrieve.
+ *
+ * @return
+ *     The static virtual channel with the given name, or NULL if no such
+ *     virtual channel exists.
+ */
+guac_rdp_pipe_svc* guac_rdp_pipe_svc_get(guac_client* client, const char* name);
+
+/**
+ * Removes the SVC with the given name from the list stored in the client.
+ * Inbound pipe streams having the given name will no longer be routed to the
+ * associated SVC.
+ *
+ * @param client
+ *     The guac_client associated with the current RDP session.
+ *
+ * @param name
+ *     The name of the static virtual channel to remove.
+ *
+ * @return
+ *     The static virtual channel that was removed, or NULL if no such virtual
+ *     channel exists.
+ */
+guac_rdp_pipe_svc* guac_rdp_pipe_svc_remove(guac_client* client, const char* name);
+
+/**
+ * Handler for "blob" instructions which writes received data to the associated
+ * SVC using guac_rdp_pipe_svc_write().
+ */
+guac_user_blob_handler guac_rdp_pipe_svc_blob_handler;
+
+/**
+ * Handler for "pipe" instructions which prepares received pipe streams to
+ * write received blobs to the SVC having the same name as the pipe stream.
+ * Received pipe streams are associated with the relevant guac_rdp_pipe_svc
+ * instance and the SVC-specific "blob" instruction handler
+ * (guac_rdp_pipe_svc_blob_handler).
+ */
+guac_user_pipe_handler guac_rdp_pipe_svc_pipe_handler;
+
+/**
+ * Handler which is invoked when an SVC associated with a Guacamole pipe stream
+ * is connected to the RDP server.
+ */
+guac_rdp_common_svc_connect_handler guac_rdp_pipe_svc_process_connect;
+
+/**
+ * Handler which is invoked when an SVC associated with a Guacamole pipe stream
+ * received data from the RDP server.
+ */
+guac_rdp_common_svc_receive_handler guac_rdp_pipe_svc_process_receive;
+
+/**
+ * Handler which is invoked when an SVC associated with a Guacamole pipe stream
+ * has disconnected and is about to be freed.
+ */
+guac_rdp_common_svc_terminate_handler guac_rdp_pipe_svc_process_terminate;
+
+#endif
+
diff --git a/src/protocols/rdp/channels/rail.c b/src/protocols/rdp/channels/rail.c
new file mode 100644
index 0000000..3e80007
--- /dev/null
+++ b/src/protocols/rdp/channels/rail.c
@@ -0,0 +1,211 @@
+/*
+ * 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/rail.h"
+#include "plugins/channels.h"
+#include "rdp.h"
+#include "settings.h"
+
+#include <freerdp/client/rail.h>
+#include <freerdp/event.h>
+#include <freerdp/freerdp.h>
+#include <freerdp/rail.h>
+#include <guacamole/client.h>
+#include <winpr/wtypes.h>
+#include <winpr/wtsapi.h>
+
+#include <stddef.h>
+#include <string.h>
+
+#ifdef FREERDP_RAIL_CALLBACKS_REQUIRE_CONST
+/**
+ * FreeRDP 2.0.0-rc4 and newer requires the final argument for all RAIL
+ * callbacks to be const.
+ */
+#define RAIL_CONST const
+#else
+/**
+ * FreeRDP 2.0.0-rc3 and older requires the final argument for all RAIL
+ * callbacks to NOT be const.
+ */
+#define RAIL_CONST
+#endif
+
+/**
+ * Completes initialization of the RemoteApp session, sending client system
+ * parameters and executing the desired RemoteApp command using the Client
+ * System Parameters Update PDU and Client Execute PDU respectively. These PDUs
+ * MUST be sent for the desired RemoteApp to run, and MUST NOT be sent until
+ * after a Handshake or HandshakeEx PDU has been received. See:
+ *
+ * https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdperp/60344497-883f-4711-8b9a-828d1c580195 (System Parameters Update PDU)
+ * https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdperp/98a6e3c3-c2a9-42cc-ad91-0d9a6c211138 (Client Execute PDU)
+ *
+ * @param rail
+ *     The RailClientContext structure used by FreeRDP to handle the RAIL
+ *     channel for the current RDP session.
+ *
+ * @return
+ *     CHANNEL_RC_OK (zero) if the PDUs were sent successfully, an error code
+ *     (non-zero) otherwise.
+ */
+static UINT guac_rdp_rail_complete_handshake(RailClientContext* rail) {
+
+    guac_client* client = (guac_client*) rail->custom;
+    guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
+
+    RAIL_SYSPARAM_ORDER sysparam = {
+        .workArea = {
+            .left   = 0,
+            .top    = 0,
+            .right  = rdp_client->settings->width,
+            .bottom = rdp_client->settings->height
+        },
+        .dragFullWindows = FALSE
+    };
+
+    /* Send client system parameters */
+    UINT status = rail->ClientSystemParam(rail, &sysparam);
+    if (status != CHANNEL_RC_OK)
+        return status;
+
+    RAIL_EXEC_ORDER exec = {
+        .RemoteApplicationProgram = rdp_client->settings->remote_app,
+        .RemoteApplicationWorkingDir = rdp_client->settings->remote_app_dir,
+        .RemoteApplicationArguments = rdp_client->settings->remote_app_args,
+    };
+
+    /* Execute desired RemoteApp command */
+    return rail->ClientExecute(rail, &exec);
+
+}
+
+/**
+ * Callback which is invoked when a Handshake PDU is received from the RDP
+ * server. No communication for RemoteApp may occur until the Handshake PDU
+ * (or, alternatively, the HandshakeEx PDU) is received. See:
+ *
+ * https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdperp/cec4eb83-b304-43c9-8378-b5b8f5e7082a
+ *
+ * @param rail
+ *     The RailClientContext structure used by FreeRDP to handle the RAIL
+ *     channel for the current RDP session.
+ *
+ * @param handshake
+ *     The RAIL_HANDSHAKE_ORDER structure representing the Handshake PDU that
+ *     was received.
+ *
+ * @return
+ *     CHANNEL_RC_OK (zero) if the PDU was handled successfully, an error code
+ *     (non-zero) otherwise.
+ */
+static UINT guac_rdp_rail_handshake(RailClientContext* rail,
+        RAIL_CONST RAIL_HANDSHAKE_ORDER* handshake) {
+    return guac_rdp_rail_complete_handshake(rail);
+}
+
+/**
+ * Callback which is invoked when a HandshakeEx PDU is received from the RDP
+ * server. No communication for RemoteApp may occur until the HandshakeEx PDU
+ * (or, alternatively, the Handshake PDU) is received. See:
+ *
+ * https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdperp/5cec5414-27de-442e-8d4a-c8f8b41f3899
+ *
+ * @param rail
+ *     The RailClientContext structure used by FreeRDP to handle the RAIL
+ *     channel for the current RDP session.
+ *
+ * @param handshake_ex
+ *     The RAIL_HANDSHAKE_EX_ORDER structure representing the HandshakeEx PDU
+ *     that was received.
+ *
+ * @return
+ *     CHANNEL_RC_OK (zero) if the PDU was handled successfully, an error code
+ *     (non-zero) otherwise.
+ */
+static UINT guac_rdp_rail_handshake_ex(RailClientContext* rail,
+        RAIL_CONST RAIL_HANDSHAKE_EX_ORDER* handshake_ex) {
+    return guac_rdp_rail_complete_handshake(rail);
+}
+
+/**
+ * Callback which associates handlers specific to Guacamole with the
+ * RailClientContext instance allocated by FreeRDP to deal with received
+ * RAIL (RemoteApp) 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 RAIL channel. This specific callback is registered with the PubSub
+ * system of the relevant rdpContext when guac_rdp_rail_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_rail_channel_connected(rdpContext* context,
+        ChannelConnectedEventArgs* e) {
+
+    guac_client* client = ((rdp_freerdp_context*) context)->client;
+
+    /* Ignore connection event if it's not for the RAIL channel */
+    if (strcmp(e->name, RAIL_SVC_CHANNEL_NAME) != 0)
+        return;
+
+    /* The structure pointed to by pInterface is guaranteed to be a
+     * RailClientContext if the channel is RAIL */
+    RailClientContext* rail = (RailClientContext*) e->pInterface;
+
+    /* Init FreeRDP RAIL context, ensuring the guac_client can be accessed from
+     * within any RAIL-specific callbacks */
+    rail->custom = client;
+    rail->ServerHandshake = guac_rdp_rail_handshake;
+    rail->ServerHandshakeEx = guac_rdp_rail_handshake_ex;
+
+    guac_client_log(client, GUAC_LOG_DEBUG, "RAIL (RemoteApp) channel "
+            "connected.");
+
+}
+
+void guac_rdp_rail_load_plugin(rdpContext* context) {
+
+    guac_client* client = ((rdp_freerdp_context*) context)->client;
+
+    /* Attempt to load FreeRDP support for the RAIL channel */
+    if (guac_freerdp_channels_load_plugin(context, "rail", context->settings)) {
+        guac_client_log(client, GUAC_LOG_WARNING,
+                "Support for the RAIL channel (RemoteApp) could not be "
+                "loaded. This support normally takes the form of a plugin "
+                "which is built into FreeRDP. Lacking this support, "
+                "RemoteApp will not work.");
+        return;
+    }
+
+    /* Complete RDP side of initialization when channel is connected */
+    PubSub_SubscribeChannelConnected(context->pubSub,
+            (pChannelConnectedEventHandler) guac_rdp_rail_channel_connected);
+
+    guac_client_log(client, GUAC_LOG_DEBUG, "Support for RAIL (RemoteApp) "
+            "registered. Awaiting channel connection.");
+
+}
+
diff --git a/src/protocols/rdp/channels/rail.h b/src/protocols/rdp/channels/rail.h
new file mode 100644
index 0000000..0856034
--- /dev/null
+++ b/src/protocols/rdp/channels/rail.h
@@ -0,0 +1,39 @@
+/*
+ * 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_RAIL_H
+#define GUAC_RDP_CHANNELS_RAIL_H
+
+#include <freerdp/freerdp.h>
+
+/**
+ * Initializes RemoteApp support for RDP and handling of the RAIL channel. If
+ * failures occur, messages noting the specifics of those failures will be
+ * logged, and RemoteApp support will not be functional.
+ *
+ * This MUST be called within the PreConnect callback of the freerdp instance
+ * for RAIL support to be loaded.
+ *
+ * @param context
+ *     The rdpContext associated with the FreeRDP side of the RDP connection.
+ */
+void guac_rdp_rail_load_plugin(rdpContext* context);
+
+#endif
+
diff --git a/src/protocols/rdp/guac_rdpdr/rdpdr_fs_messages_dir_info.c b/src/protocols/rdp/channels/rdpdr/rdpdr-fs-messages-dir-info.c
similarity index 70%
rename from src/protocols/rdp/guac_rdpdr/rdpdr_fs_messages_dir_info.c
rename to src/protocols/rdp/channels/rdpdr/rdpdr-fs-messages-dir-info.c
index c53eedc..70eb8cd 100644
--- a/src/protocols/rdp/guac_rdpdr/rdpdr_fs_messages_dir_info.c
+++ b/src/protocols/rdp/channels/rdpdr/rdpdr-fs-messages-dir-info.c
@@ -17,26 +17,22 @@
  * under the License.
  */
 
-#include "config.h"
-
-#include "rdpdr_service.h"
-#include "rdp_fs.h"
-#include "rdp_status.h"
+#include "channels/rdpdr/rdpdr-fs-messages-dir-info.h"
+#include "channels/rdpdr/rdpdr.h"
+#include "fs.h"
 #include "unicode.h"
 
-#include <freerdp/utils/svc_plugin.h>
+#include <guacamole/client.h>
 #include <guacamole/unicode.h>
-
-#ifdef ENABLE_WINPR
+#include <winpr/nt.h>
 #include <winpr/stream.h>
-#else
-#include "compat/winpr-stream.h"
-#endif
 
 #include <stddef.h>
+#include <stddef.h>
 
-void guac_rdpdr_fs_process_query_directory_info(guac_rdpdr_device* device,
-        const char* entry_name, int file_id, int completion_id) {
+void guac_rdpdr_fs_process_query_directory_info(guac_rdp_common_svc* svc,
+        guac_rdpdr_device* device, guac_rdpdr_iorequest* iorequest,
+        const char* entry_name, int entry_file_id) {
 
     guac_rdp_fs_file* file;
 
@@ -49,16 +45,17 @@
             (char*) utf16_entry_name, sizeof(utf16_entry_name));
 
     /* Get file */
-    file = guac_rdp_fs_get_file((guac_rdp_fs*) device->data, file_id);
+    file = guac_rdp_fs_get_file((guac_rdp_fs*) device->data, entry_file_id);
     if (file == NULL)
         return;
 
-    guac_client_log(device->rdpdr->client, GUAC_LOG_DEBUG,
+    guac_client_log(svc->client, GUAC_LOG_DEBUG,
             "%s: [file_id=%i (entry_name=\"%s\")]",
-            __func__, file_id, entry_name);
+            __func__, entry_file_id, entry_name);
 
-    output_stream = guac_rdpdr_new_io_completion(device, completion_id,
-            STATUS_SUCCESS, 4 + 64 + utf16_length + 2);
+    output_stream = guac_rdpdr_new_io_completion(device,
+            iorequest->completion_id, STATUS_SUCCESS,
+            4 + 64 + utf16_length + 2);
 
     Stream_Write_UINT32(output_stream,
             64 + utf16_length + 2); /* Length */
@@ -77,12 +74,13 @@
     Stream_Write(output_stream, utf16_entry_name, utf16_length); /* FileName */
     Stream_Write(output_stream, "\0\0", 2);
 
-    svc_plugin_send((rdpSvcPlugin*) device->rdpdr, output_stream);
+    guac_rdp_common_svc_write(svc, output_stream);
 
 }
 
-void guac_rdpdr_fs_process_query_full_directory_info(guac_rdpdr_device* device,
-        const char* entry_name, int file_id, int completion_id) {
+void guac_rdpdr_fs_process_query_full_directory_info(guac_rdp_common_svc* svc,
+        guac_rdpdr_device* device, guac_rdpdr_iorequest* iorequest,
+        const char* entry_name, int entry_file_id) {
 
     guac_rdp_fs_file* file;
 
@@ -95,16 +93,17 @@
             (char*) utf16_entry_name, sizeof(utf16_entry_name));
 
     /* Get file */
-    file = guac_rdp_fs_get_file((guac_rdp_fs*) device->data, file_id);
+    file = guac_rdp_fs_get_file((guac_rdp_fs*) device->data, entry_file_id);
     if (file == NULL)
         return;
 
-    guac_client_log(device->rdpdr->client, GUAC_LOG_DEBUG,
+    guac_client_log(svc->client, GUAC_LOG_DEBUG,
             "%s: [file_id=%i (entry_name=\"%s\")]",
-            __func__, file_id, entry_name);
+            __func__, entry_file_id, entry_name);
 
-    output_stream = guac_rdpdr_new_io_completion(device, completion_id,
-            STATUS_SUCCESS, 4 + 68 + utf16_length + 2);
+    output_stream = guac_rdpdr_new_io_completion(device,
+            iorequest->completion_id, STATUS_SUCCESS,
+            4 + 68 + utf16_length + 2);
 
     Stream_Write_UINT32(output_stream,
             68 + utf16_length + 2); /* Length */
@@ -124,12 +123,13 @@
     Stream_Write(output_stream, utf16_entry_name, utf16_length); /* FileName */
     Stream_Write(output_stream, "\0\0", 2);
 
-    svc_plugin_send((rdpSvcPlugin*) device->rdpdr, output_stream);
+    guac_rdp_common_svc_write(svc, output_stream);
 
 }
 
-void guac_rdpdr_fs_process_query_both_directory_info(guac_rdpdr_device* device,
-        const char* entry_name, int file_id, int completion_id) {
+void guac_rdpdr_fs_process_query_both_directory_info(guac_rdp_common_svc* svc,
+        guac_rdpdr_device* device, guac_rdpdr_iorequest* iorequest,
+        const char* entry_name, int entry_file_id) {
 
     guac_rdp_fs_file* file;
 
@@ -142,16 +142,17 @@
             (char*) utf16_entry_name, sizeof(utf16_entry_name));
 
     /* Get file */
-    file = guac_rdp_fs_get_file((guac_rdp_fs*) device->data, file_id);
+    file = guac_rdp_fs_get_file((guac_rdp_fs*) device->data, entry_file_id);
     if (file == NULL)
         return;
 
-    guac_client_log(device->rdpdr->client, GUAC_LOG_DEBUG,
+    guac_client_log(svc->client, GUAC_LOG_DEBUG,
             "%s: [file_id=%i (entry_name=\"%s\")]",
-            __func__, file_id, entry_name);
+            __func__, entry_file_id, entry_name);
 
-    output_stream = guac_rdpdr_new_io_completion(device, completion_id,
-            STATUS_SUCCESS, 4 + 69 + 24 + utf16_length + 2);
+    output_stream = guac_rdpdr_new_io_completion(device,
+            iorequest->completion_id, STATUS_SUCCESS,
+            4 + 69 + 24 + utf16_length + 2);
 
     Stream_Write_UINT32(output_stream,
             69 + 24 + utf16_length + 2); /* Length */
@@ -175,12 +176,13 @@
     Stream_Write(output_stream, utf16_entry_name, utf16_length); /* FileName */
     Stream_Write(output_stream, "\0\0", 2);
 
-    svc_plugin_send((rdpSvcPlugin*) device->rdpdr, output_stream);
+    guac_rdp_common_svc_write(svc, output_stream);
 
 }
 
-void guac_rdpdr_fs_process_query_names_info(guac_rdpdr_device* device,
-        const char* entry_name, int file_id, int completion_id) {
+void guac_rdpdr_fs_process_query_names_info(guac_rdp_common_svc* svc,
+        guac_rdpdr_device* device, guac_rdpdr_iorequest* iorequest,
+        const char* entry_name, int entry_file_id) {
 
     guac_rdp_fs_file* file;
 
@@ -193,16 +195,17 @@
             (char*) utf16_entry_name, sizeof(utf16_entry_name));
 
     /* Get file */
-    file = guac_rdp_fs_get_file((guac_rdp_fs*) device->data, file_id);
+    file = guac_rdp_fs_get_file((guac_rdp_fs*) device->data, entry_file_id);
     if (file == NULL)
         return;
 
-    guac_client_log(device->rdpdr->client, GUAC_LOG_DEBUG,
+    guac_client_log(svc->client, GUAC_LOG_DEBUG,
             "%s: [file_id=%i (entry_name=\"%s\")]",
-            __func__, file_id, entry_name);
+            __func__, entry_file_id, entry_name);
 
-    output_stream = guac_rdpdr_new_io_completion(device, completion_id,
-            STATUS_SUCCESS, 4 + 12 + utf16_length + 2);
+    output_stream = guac_rdpdr_new_io_completion(device,
+            iorequest->completion_id, STATUS_SUCCESS,
+            4 + 12 + utf16_length + 2);
 
     Stream_Write_UINT32(output_stream,
             12 + utf16_length + 2); /* Length */
@@ -213,7 +216,7 @@
     Stream_Write(output_stream, utf16_entry_name, utf16_length); /* FileName */
     Stream_Write(output_stream, "\0\0", 2);
 
-    svc_plugin_send((rdpSvcPlugin*) device->rdpdr, output_stream);
+    guac_rdp_common_svc_write(svc, output_stream);
 
 }
 
diff --git a/src/protocols/rdp/channels/rdpdr/rdpdr-fs-messages-dir-info.h b/src/protocols/rdp/channels/rdpdr/rdpdr-fs-messages-dir-info.h
new file mode 100644
index 0000000..2f259e9
--- /dev/null
+++ b/src/protocols/rdp/channels/rdpdr/rdpdr-fs-messages-dir-info.h
@@ -0,0 +1,92 @@
+/*
+ * 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_RDPDR_FS_MESSAGES_DIR_INFO_H
+#define GUAC_RDP_CHANNELS_RDPDR_FS_MESSAGES_DIR_INFO_H
+
+/**
+ * Handlers for directory queries received over the RDPDR channel via the
+ * IRP_MJ_DIRECTORY_CONTROL major function and the IRP_MN_QUERY_DIRECTORY minor
+ * function.
+ *
+ * @file rdpdr-fs-messages-dir-info.h
+ */
+
+#include "channels/common-svc.h"
+#include "channels/rdpdr/rdpdr.h"
+
+#include <winpr/stream.h>
+
+/**
+ * Handler for Device I/O Requests which query information about the files
+ * within a directory.
+ *
+ * @param svc
+ *     The guac_rdp_common_svc representing the static virtual channel being
+ *     used for RDPDR.
+ *
+ * @param device
+ *     The guac_rdpdr_device of the relevant device, as dictated by the
+ *     deviceId field of the common RDPDR header within the received PDU.
+ *     Within the guac_rdpdr_iorequest structure, the deviceId field is stored
+ *     within device_id.
+ *
+ * @param iorequest
+ *     The contents of the common RDPDR Device I/O Request header shared by all
+ *     RDPDR devices.
+ *
+ * @param entry_name
+ *     The filename of the file being queried.
+ *
+ * @param entry_file_id
+ *     The ID of the file being queried.
+ */
+typedef void guac_rdpdr_directory_query_handler(guac_rdp_common_svc* svc,
+        guac_rdpdr_device* device, guac_rdpdr_iorequest* iorequest,
+        const char* entry_name, int entry_file_id);
+
+/**
+ * Processes a query request for FileDirectoryInformation. From the
+ * documentation this is "defined as the file's name, time stamp, and size, or its
+ * attributes."
+ */
+guac_rdpdr_directory_query_handler guac_rdpdr_fs_process_query_directory_info;
+
+/**
+ * Processes a query request for FileFullDirectoryInformation. From the
+ * documentation, this is "defined as all the basic information, plus extended
+ * attribute size."
+ */
+guac_rdpdr_directory_query_handler guac_rdpdr_fs_process_query_full_directory_info;
+
+/**
+ * Processes a query request for FileBothDirectoryInformation. From the
+ * documentation, this absurdly-named request is "basic information plus
+ * extended attribute size and short name about a file or directory."
+ */
+guac_rdpdr_directory_query_handler guac_rdpdr_fs_process_query_both_directory_info;
+
+/**
+ * Processes a query request for FileNamesInformation. From the documentation,
+ * this is "detailed information on the names of files in a directory."
+ */
+guac_rdpdr_directory_query_handler guac_rdpdr_fs_process_query_names_info;
+
+#endif
+
diff --git a/src/protocols/rdp/channels/rdpdr/rdpdr-fs-messages-file-info.c b/src/protocols/rdp/channels/rdpdr/rdpdr-fs-messages-file-info.c
new file mode 100644
index 0000000..0f45303
--- /dev/null
+++ b/src/protocols/rdp/channels/rdpdr/rdpdr-fs-messages-file-info.c
@@ -0,0 +1,283 @@
+/*
+ * 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/rdpdr/rdpdr-fs-messages-file-info.h"
+#include "channels/rdpdr/rdpdr.h"
+#include "download.h"
+#include "fs.h"
+#include "unicode.h"
+
+#include <guacamole/client.h>
+#include <winpr/file.h>
+#include <winpr/nt.h>
+#include <winpr/stream.h>
+#include <winpr/wtypes.h>
+
+#include <stdint.h>
+#include <string.h>
+
+void guac_rdpdr_fs_process_query_basic_info(guac_rdp_common_svc* svc,
+        guac_rdpdr_device* device, guac_rdpdr_iorequest* iorequest,
+        wStream* input_stream) {
+
+    wStream* output_stream;
+    guac_rdp_fs_file* file;
+
+    /* Get file */
+    file = guac_rdp_fs_get_file((guac_rdp_fs*) device->data, iorequest->file_id);
+    if (file == NULL)
+        return;
+
+    guac_client_log(svc->client, GUAC_LOG_DEBUG, "%s: [file_id=%i]", __func__,
+            iorequest->file_id);
+
+    output_stream = guac_rdpdr_new_io_completion(device,
+            iorequest->completion_id, STATUS_SUCCESS, 40);
+
+    Stream_Write_UINT32(output_stream, 36);
+    Stream_Write_UINT64(output_stream, file->ctime);      /* CreationTime   */
+    Stream_Write_UINT64(output_stream, file->atime);      /* LastAccessTime */
+    Stream_Write_UINT64(output_stream, file->mtime);      /* LastWriteTime  */
+    Stream_Write_UINT64(output_stream, file->mtime);      /* ChangeTime     */
+    Stream_Write_UINT32(output_stream, file->attributes); /* FileAttributes */
+
+    /* Reserved field must not be sent */
+
+    guac_rdp_common_svc_write(svc, output_stream);
+
+}
+
+void guac_rdpdr_fs_process_query_standard_info(guac_rdp_common_svc* svc,
+        guac_rdpdr_device* device, guac_rdpdr_iorequest* iorequest,
+        wStream* input_stream) {
+
+    wStream* output_stream;
+    guac_rdp_fs_file* file;
+    BOOL is_directory = FALSE;
+
+    /* Get file */
+    file = guac_rdp_fs_get_file((guac_rdp_fs*) device->data, iorequest->file_id);
+    if (file == NULL)
+        return;
+
+    guac_client_log(svc->client, GUAC_LOG_DEBUG, "%s: [file_id=%i]", __func__,
+            iorequest->file_id);
+
+    if (file->attributes & FILE_ATTRIBUTE_DIRECTORY)
+        is_directory = TRUE;
+
+    output_stream = guac_rdpdr_new_io_completion(device,
+            iorequest->completion_id, STATUS_SUCCESS, 26);
+
+    Stream_Write_UINT32(output_stream, 22);
+    Stream_Write_UINT64(output_stream, file->size);   /* AllocationSize */
+    Stream_Write_UINT64(output_stream, file->size);   /* EndOfFile      */
+    Stream_Write_UINT32(output_stream, 1);            /* NumberOfLinks  */
+    Stream_Write_UINT8(output_stream,  0);            /* DeletePending  */
+    Stream_Write_UINT8(output_stream,  is_directory); /* Directory      */
+
+    /* Reserved field must not be sent */
+
+    guac_rdp_common_svc_write(svc, output_stream);
+
+}
+
+void guac_rdpdr_fs_process_query_attribute_tag_info(guac_rdp_common_svc* svc,
+        guac_rdpdr_device* device, guac_rdpdr_iorequest* iorequest,
+        wStream* input_stream) {
+
+    wStream* output_stream;
+    guac_rdp_fs_file* file;
+
+    /* Get file */
+    file = guac_rdp_fs_get_file((guac_rdp_fs*) device->data, iorequest->file_id);
+    if (file == NULL)
+        return;
+
+    guac_client_log(svc->client, GUAC_LOG_DEBUG, "%s: [file_id=%i]", __func__,
+            iorequest->file_id);
+
+    output_stream = guac_rdpdr_new_io_completion(device,
+            iorequest->completion_id, STATUS_SUCCESS, 12);
+
+    Stream_Write_UINT32(output_stream, 8);
+    Stream_Write_UINT32(output_stream, file->attributes); /* FileAttributes */
+    Stream_Write_UINT32(output_stream, 0);                /* ReparseTag */
+
+    /* Reserved field must not be sent */
+
+    guac_rdp_common_svc_write(svc, output_stream);
+
+}
+
+void guac_rdpdr_fs_process_set_rename_info(guac_rdp_common_svc* svc,
+        guac_rdpdr_device* device, guac_rdpdr_iorequest* iorequest,
+        int length, wStream* input_stream) {
+
+    int result;
+    int filename_length;
+    wStream* output_stream;
+    char destination_path[GUAC_RDP_FS_MAX_PATH];
+
+    /* Read structure */
+    Stream_Seek_UINT8(input_stream); /* ReplaceIfExists */
+    Stream_Seek_UINT8(input_stream); /* RootDirectory */
+    Stream_Read_UINT32(input_stream, filename_length); /* FileNameLength */
+
+    /* Convert name to UTF-8 */
+    guac_rdp_utf16_to_utf8(Stream_Pointer(input_stream), filename_length/2,
+            destination_path, sizeof(destination_path));
+
+    guac_client_log(svc->client, GUAC_LOG_DEBUG, "%s: [file_id=%i]"
+            "destination_path=\"%s\"", __func__, iorequest->file_id,
+            destination_path);
+
+    /* If file moving to \Download folder, start stream, do not move */
+    if (strncmp(destination_path, "\\Download\\", 10) == 0) {
+
+        guac_rdp_fs_file* file;
+
+        /* Get file */
+        file = guac_rdp_fs_get_file((guac_rdp_fs*) device->data, iorequest->file_id);
+        if (file == NULL)
+            return;
+
+        /* Initiate download, pretend move succeeded */
+        guac_client_for_owner(svc->client, guac_rdp_download_to_user, file->absolute_path);
+        output_stream = guac_rdpdr_new_io_completion(device,
+                iorequest->completion_id, STATUS_SUCCESS, 4);
+
+    }
+
+    /* Otherwise, rename as requested */
+    else {
+
+        result = guac_rdp_fs_rename((guac_rdp_fs*) device->data,
+                iorequest->file_id, destination_path);
+        if (result < 0)
+            output_stream = guac_rdpdr_new_io_completion(device,
+                    iorequest->completion_id, guac_rdp_fs_get_status(result), 4);
+        else
+            output_stream = guac_rdpdr_new_io_completion(device,
+                    iorequest->completion_id, STATUS_SUCCESS, 4);
+
+    }
+
+    Stream_Write_UINT32(output_stream, length);
+    guac_rdp_common_svc_write(svc, output_stream);
+
+}
+
+void guac_rdpdr_fs_process_set_allocation_info(guac_rdp_common_svc* svc,
+        guac_rdpdr_device* device, guac_rdpdr_iorequest* iorequest,
+        int length, wStream* input_stream) {
+
+    int result;
+    UINT64 size;
+    wStream* output_stream;
+
+    /* Read new size */
+    Stream_Read_UINT64(input_stream, size); /* AllocationSize */
+
+    guac_client_log(svc->client, GUAC_LOG_DEBUG, "%s: [file_id=%i] "
+            "size=%" PRIu64, __func__, iorequest->file_id, (uint64_t) size);
+
+    /* Truncate file */
+    result = guac_rdp_fs_truncate((guac_rdp_fs*) device->data, iorequest->file_id, size);
+    if (result < 0)
+        output_stream = guac_rdpdr_new_io_completion(device,
+                iorequest->completion_id, guac_rdp_fs_get_status(result), 4);
+    else
+        output_stream = guac_rdpdr_new_io_completion(device,
+                iorequest->completion_id, STATUS_SUCCESS, 4);
+
+    Stream_Write_UINT32(output_stream, length);
+    guac_rdp_common_svc_write(svc, output_stream);
+
+}
+
+void guac_rdpdr_fs_process_set_disposition_info(guac_rdp_common_svc* svc,
+        guac_rdpdr_device* device, guac_rdpdr_iorequest* iorequest,
+        int length, wStream* input_stream) {
+
+    wStream* output_stream;
+
+    /* Delete file */
+    int result = guac_rdp_fs_delete((guac_rdp_fs*) device->data, iorequest->file_id);
+    if (result < 0)
+        output_stream = guac_rdpdr_new_io_completion(device,
+                iorequest->completion_id, guac_rdp_fs_get_status(result), 4);
+    else
+        output_stream = guac_rdpdr_new_io_completion(device,
+                iorequest->completion_id, STATUS_SUCCESS, 4);
+
+    guac_client_log(svc->client, GUAC_LOG_DEBUG, "%s: [file_id=%i]", __func__,
+            iorequest->file_id);
+
+    Stream_Write_UINT32(output_stream, length);
+
+    guac_rdp_common_svc_write(svc, output_stream);
+
+}
+
+void guac_rdpdr_fs_process_set_end_of_file_info(guac_rdp_common_svc* svc,
+        guac_rdpdr_device* device, guac_rdpdr_iorequest* iorequest,
+        int length, wStream* input_stream) {
+
+    int result;
+    UINT64 size;
+    wStream* output_stream;
+
+    /* Read new size */
+    Stream_Read_UINT64(input_stream, size); /* AllocationSize */
+
+    guac_client_log(svc->client, GUAC_LOG_DEBUG, "%s: [file_id=%i] "
+            "size=%" PRIu64, __func__, iorequest->file_id, (uint64_t) size);
+
+    /* Truncate file */
+    result = guac_rdp_fs_truncate((guac_rdp_fs*) device->data, iorequest->file_id, size);
+    if (result < 0)
+        output_stream = guac_rdpdr_new_io_completion(device,
+                iorequest->completion_id, guac_rdp_fs_get_status(result), 4);
+    else
+        output_stream = guac_rdpdr_new_io_completion(device,
+                iorequest->completion_id, STATUS_SUCCESS, 4);
+
+    Stream_Write_UINT32(output_stream, length);
+    guac_rdp_common_svc_write(svc, output_stream);
+
+}
+
+void guac_rdpdr_fs_process_set_basic_info(guac_rdp_common_svc* svc,
+        guac_rdpdr_device* device, guac_rdpdr_iorequest* iorequest,
+        int length, wStream* input_stream) {
+
+    wStream* output_stream = guac_rdpdr_new_io_completion(device,
+            iorequest->completion_id, STATUS_SUCCESS, 4);
+
+    /* Currently do nothing, just respond */
+    Stream_Write_UINT32(output_stream, length);
+
+    guac_client_log(svc->client, GUAC_LOG_DEBUG, "%s: [file_id=%i] IGNORED",
+            __func__, iorequest->file_id);
+
+    guac_rdp_common_svc_write(svc, output_stream);
+
+}
+
diff --git a/src/protocols/rdp/channels/rdpdr/rdpdr-fs-messages-file-info.h b/src/protocols/rdp/channels/rdpdr/rdpdr-fs-messages-file-info.h
new file mode 100644
index 0000000..f7a8784
--- /dev/null
+++ b/src/protocols/rdp/channels/rdpdr/rdpdr-fs-messages-file-info.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_RDP_CHANNELS_RDPDR_FS_MESSAGES_FILE_INFO_H
+#define GUAC_RDP_CHANNELS_RDPDR_FS_MESSAGES_FILE_INFO_H
+
+/**
+ * Handlers for file queries received over the RDPDR channel via the
+ * IRP_MJ_QUERY_INFORMATION major function.
+ *
+ * @file rdpdr-fs-messages-file-info.h
+ */
+
+#include "channels/common-svc.h"
+#include "channels/rdpdr/rdpdr.h"
+
+#include <winpr/stream.h>
+
+/**
+ * Handler for Device I/O Requests which set/update file information.
+ *
+ * @param svc
+ *     The guac_rdp_common_svc representing the static virtual channel being
+ *     used for RDPDR.
+ *
+ * @param device
+ *     The guac_rdpdr_device of the relevant device, as dictated by the
+ *     deviceId field of the common RDPDR header within the received PDU.
+ *     Within the guac_rdpdr_iorequest structure, the deviceId field is stored
+ *     within device_id.
+ *
+ * @param iorequest
+ *     The contents of the common RDPDR Device I/O Request header shared by all
+ *     RDPDR devices.
+ *
+ * @param length
+ *     The length of the SetBuffer field of the I/O request, in bytes. Whether
+ *     the SetBuffer field is applicable to a particular request, as well as
+ *     the specific contents of that field, depend on the type of request.
+ *
+ * @param input_stream
+ *     The remaining data within the received PDU, following the common RDPDR
+ *     Device I/O Request header and length field. If the SetBuffer field is
+ *     used for this request, the first byte of SetBuffer will be the first
+ *     byte read from this stream.
+ */
+typedef void guac_rdpdr_set_information_request_handler(guac_rdp_common_svc* svc,
+        guac_rdpdr_device* device, guac_rdpdr_iorequest* iorequest,
+        int length, wStream* input_stream);
+
+/**
+ * Processes a query for FileBasicInformation. From the documentation, this is
+ * "used to query a file for the times of creation, last access, last write,
+ * and change, in addition to file attribute information."
+ */
+guac_rdpdr_device_iorequest_handler guac_rdpdr_fs_process_query_basic_info;
+
+/**
+ * Processes a query for FileStandardInformation. From the documentation, this
+ * is "used to query for file information such as allocation size, end-of-file
+ * position, and number of links."
+ */
+guac_rdpdr_device_iorequest_handler guac_rdpdr_fs_process_query_standard_info;
+
+/**
+ * Processes a query for FileAttributeTagInformation. From the documentation
+ * this is "used to query for file attribute and reparse tag information."
+ */
+guac_rdpdr_device_iorequest_handler guac_rdpdr_fs_process_query_attribute_tag_info;
+
+/**
+ * Process a set operation for FileRenameInformation. From the documentation,
+ * this operation is used to rename a file.
+ */
+guac_rdpdr_set_information_request_handler guac_rdpdr_fs_process_set_rename_info;
+
+/**
+ * Process a set operation for FileAllocationInformation. From the
+ * documentation, this operation is used to set a file's allocation size.
+ */
+guac_rdpdr_set_information_request_handler guac_rdpdr_fs_process_set_allocation_info;
+
+/**
+ * Process a set operation for FileDispositionInformation. From the
+ * documentation, this operation is used to mark a file for deletion.
+ */
+guac_rdpdr_set_information_request_handler guac_rdpdr_fs_process_set_disposition_info;
+
+/**
+ * Process a set operation for FileEndOfFileInformation. From the
+ * documentation, this operation is used "to set end-of-file information for
+ * a file."
+ */
+guac_rdpdr_set_information_request_handler guac_rdpdr_fs_process_set_end_of_file_info;
+
+/**
+ * Process a set operation for FileBasicInformation. From the documentation,
+ * this is "used to set file information such as the times of creation, last
+ * access, last write, and change, in addition to file attributes."
+ */
+guac_rdpdr_set_information_request_handler guac_rdpdr_fs_process_set_basic_info;
+
+#endif
diff --git a/src/protocols/rdp/guac_rdpdr/rdpdr_fs_messages_vol_info.c b/src/protocols/rdp/channels/rdpdr/rdpdr-fs-messages-vol-info.c
similarity index 60%
rename from src/protocols/rdp/guac_rdpdr/rdpdr_fs_messages_vol_info.c
rename to src/protocols/rdp/channels/rdpdr/rdpdr-fs-messages-vol-info.c
index c107295..0ce4684 100644
--- a/src/protocols/rdp/guac_rdpdr/rdpdr_fs_messages_vol_info.c
+++ b/src/protocols/rdp/channels/rdpdr/rdpdr-fs-messages-vol-info.c
@@ -17,33 +17,29 @@
  * under the License.
  */
 
-#include "config.h"
+#include "channels/common-svc.h"
+#include "channels/rdpdr/rdpdr-fs-messages-vol-info.h"
+#include "channels/rdpdr/rdpdr-fs.h"
+#include "channels/rdpdr/rdpdr.h"
+#include "fs.h"
 
-#include "rdpdr_messages.h"
-#include "rdpdr_service.h"
-#include "rdp_fs.h"
-#include "rdp_status.h"
-
-#include <freerdp/utils/svc_plugin.h>
+#include <guacamole/client.h>
 #include <guacamole/unicode.h>
-
-#ifdef ENABLE_WINPR
+#include <winpr/file.h>
+#include <winpr/io.h>
+#include <winpr/nt.h>
 #include <winpr/stream.h>
 #include <winpr/wtypes.h>
-#else
-#include "compat/winpr-stream.h"
-#include "compat/winpr-wtypes.h"
-#endif
 
-void guac_rdpdr_fs_process_query_volume_info(guac_rdpdr_device* device,
-        wStream* input_stream, int file_id, int completion_id) {
+void guac_rdpdr_fs_process_query_volume_info(guac_rdp_common_svc* svc,
+        guac_rdpdr_device* device, guac_rdpdr_iorequest* iorequest,
+        wStream* input_stream) {
 
     wStream* output_stream = guac_rdpdr_new_io_completion(device,
-            completion_id, STATUS_SUCCESS, 21 + GUAC_FILESYSTEM_LABEL_LENGTH);
+            iorequest->completion_id, STATUS_SUCCESS, 21 + GUAC_FILESYSTEM_LABEL_LENGTH);
 
-    guac_client_log(device->rdpdr->client, GUAC_LOG_DEBUG,
-            "%s: [file_id=%i]",
-            __func__, file_id);
+    guac_client_log(svc->client, GUAC_LOG_DEBUG, "%s: [file_id=%i]", __func__,
+            iorequest->file_id);
 
     Stream_Write_UINT32(output_stream, 17 + GUAC_FILESYSTEM_LABEL_LENGTH);
     Stream_Write_UINT64(output_stream, 0); /* VolumeCreationTime */
@@ -53,22 +49,22 @@
     /* Reserved field must not be sent */
     Stream_Write(output_stream, GUAC_FILESYSTEM_LABEL, GUAC_FILESYSTEM_LABEL_LENGTH);
 
-    svc_plugin_send((rdpSvcPlugin*) device->rdpdr, output_stream);
+    guac_rdp_common_svc_write(svc, output_stream);
 
 }
 
-void guac_rdpdr_fs_process_query_size_info(guac_rdpdr_device* device, wStream* input_stream,
-        int file_id, int completion_id) {
+void guac_rdpdr_fs_process_query_size_info(guac_rdp_common_svc* svc,
+        guac_rdpdr_device* device, guac_rdpdr_iorequest* iorequest,
+        wStream* input_stream) {
 
     guac_rdp_fs_info info = {0};
     guac_rdp_fs_get_info((guac_rdp_fs*) device->data, &info);
 
     wStream* output_stream = guac_rdpdr_new_io_completion(device,
-            completion_id, STATUS_SUCCESS, 28);
+            iorequest->completion_id, STATUS_SUCCESS, 28);
 
-    guac_client_log(device->rdpdr->client, GUAC_LOG_DEBUG,
-            "%s: [file_id=%i]",
-            __func__, file_id);
+    guac_client_log(svc->client, GUAC_LOG_DEBUG, "%s: [file_id=%i]", __func__,
+            iorequest->file_id);
 
     Stream_Write_UINT32(output_stream, 24);
     Stream_Write_UINT64(output_stream, info.blocks_total);     /* TotalAllocationUnits */
@@ -76,39 +72,39 @@
     Stream_Write_UINT32(output_stream, 1);                     /* SectorsPerAllocationUnit */
     Stream_Write_UINT32(output_stream, info.block_size);       /* BytesPerSector */
 
-    svc_plugin_send((rdpSvcPlugin*) device->rdpdr, output_stream);
+    guac_rdp_common_svc_write(svc, output_stream);
 
 }
 
-void guac_rdpdr_fs_process_query_device_info(guac_rdpdr_device* device, wStream* input_stream,
-        int file_id, int completion_id) {
+void guac_rdpdr_fs_process_query_device_info(guac_rdp_common_svc* svc,
+        guac_rdpdr_device* device, guac_rdpdr_iorequest* iorequest,
+        wStream* input_stream) {
 
     wStream* output_stream = guac_rdpdr_new_io_completion(device,
-            completion_id, STATUS_SUCCESS, 12);
+            iorequest->completion_id, STATUS_SUCCESS, 12);
 
-    guac_client_log(device->rdpdr->client, GUAC_LOG_DEBUG,
-            "%s: [file_id=%i]",
-            __func__, file_id);
+    guac_client_log(svc->client, GUAC_LOG_DEBUG, "%s: [file_id=%i]", __func__,
+            iorequest->file_id);
 
     Stream_Write_UINT32(output_stream, 8);
     Stream_Write_UINT32(output_stream, FILE_DEVICE_DISK); /* DeviceType */
     Stream_Write_UINT32(output_stream, 0); /* Characteristics */
 
-    svc_plugin_send((rdpSvcPlugin*) device->rdpdr, output_stream);
+    guac_rdp_common_svc_write(svc, output_stream);
 
 }
 
-void guac_rdpdr_fs_process_query_attribute_info(guac_rdpdr_device* device, wStream* input_stream,
-        int file_id, int completion_id) {
+void guac_rdpdr_fs_process_query_attribute_info(guac_rdp_common_svc* svc,
+        guac_rdpdr_device* device, guac_rdpdr_iorequest* iorequest,
+        wStream* input_stream) {
 
     int name_len = guac_utf8_strlen(device->device_name);
     
     wStream* output_stream = guac_rdpdr_new_io_completion(device,
-            completion_id, STATUS_SUCCESS, 16 + name_len);
+            iorequest->completion_id, STATUS_SUCCESS, 16 + name_len);
 
-    guac_client_log(device->rdpdr->client, GUAC_LOG_DEBUG,
-            "%s: [file_id=%i]",
-            __func__, file_id);
+    guac_client_log(svc->client, GUAC_LOG_DEBUG, "%s: [file_id=%i]", __func__,
+            iorequest->file_id);
 
     Stream_Write_UINT32(output_stream, 12 + name_len);
     Stream_Write_UINT32(output_stream,
@@ -119,22 +115,22 @@
     Stream_Write_UINT32(output_stream, name_len);
     Stream_Write(output_stream, device->device_name, name_len);
 
-    svc_plugin_send((rdpSvcPlugin*) device->rdpdr, output_stream);
+    guac_rdp_common_svc_write(svc, output_stream);
 
 }
 
-void guac_rdpdr_fs_process_query_full_size_info(guac_rdpdr_device* device, wStream* input_stream,
-        int file_id, int completion_id) {
+void guac_rdpdr_fs_process_query_full_size_info(guac_rdp_common_svc* svc,
+        guac_rdpdr_device* device, guac_rdpdr_iorequest* iorequest,
+        wStream* input_stream) {
 
     guac_rdp_fs_info info = {0};
     guac_rdp_fs_get_info((guac_rdp_fs*) device->data, &info);
 
     wStream* output_stream = guac_rdpdr_new_io_completion(device,
-            completion_id, STATUS_SUCCESS, 36);
+            iorequest->completion_id, STATUS_SUCCESS, 36);
 
-    guac_client_log(device->rdpdr->client, GUAC_LOG_DEBUG,
-            "%s: [file_id=%i]",
-            __func__, file_id);
+    guac_client_log(svc->client, GUAC_LOG_DEBUG, "%s: [file_id=%i]", __func__,
+            iorequest->file_id);
 
     Stream_Write_UINT32(output_stream, 32);
     Stream_Write_UINT64(output_stream, info.blocks_total);     /* TotalAllocationUnits */
@@ -143,7 +139,7 @@
     Stream_Write_UINT32(output_stream, 1);                     /* SectorsPerAllocationUnit */
     Stream_Write_UINT32(output_stream, info.block_size);       /* BytesPerSector */
 
-    svc_plugin_send((rdpSvcPlugin*) device->rdpdr, output_stream);
+    guac_rdp_common_svc_write(svc, output_stream);
 
 }
 
diff --git a/src/protocols/rdp/guac_rdpdr/rdpdr_fs_messages_vol_info.h b/src/protocols/rdp/channels/rdpdr/rdpdr-fs-messages-vol-info.h
similarity index 61%
rename from src/protocols/rdp/guac_rdpdr/rdpdr_fs_messages_vol_info.h
rename to src/protocols/rdp/channels/rdpdr/rdpdr-fs-messages-vol-info.h
index 0c82a2f..b8c9522 100644
--- a/src/protocols/rdp/guac_rdpdr/rdpdr_fs_messages_vol_info.h
+++ b/src/protocols/rdp/channels/rdpdr/rdpdr-fs-messages-vol-info.h
@@ -17,59 +17,47 @@
  * under the License.
  */
 
-
-#ifndef __GUAC_RDPDR_FS_MESSAGES_VOL_INFO_H
-#define __GUAC_RDPDR_FS_MESSAGES_VOL_INFO_H
+#ifndef GUAC_RDP_CHANNELS_RDPDR_FS_MESSAGES_VOL_INFO_H
+#define GUAC_RDP_CHANNELS_RDPDR_FS_MESSAGES_VOL_INFO_H
 
 /**
  * Handlers for directory queries received over the RDPDR channel via the
  * IRP_MJ_DIRECTORY_CONTROL major function and the IRP_MN_QUERY_DIRECTORY minor
  * function.
  *
- * @file rdpdr_fs_messages_vol_info.h
+ * @file rdpdr-fs-messages-vol-info.h
  */
 
-#include "config.h"
+#include "channels/rdpdr/rdpdr.h"
 
-#include "rdpdr_service.h"
-
-#ifdef ENABLE_WINPR
 #include <winpr/stream.h>
-#else
-#include "compat/winpr-stream.h"
-#endif
 
 /**
  * Processes a query request for FileFsVolumeInformation. According to the
  * documentation, this is "used to query information for a volume on which a
  * file system is mounted."
  */
-void guac_rdpdr_fs_process_query_volume_info(guac_rdpdr_device* device, wStream* input_stream,
-        int file_id, int completion_id);
+guac_rdpdr_device_iorequest_handler guac_rdpdr_fs_process_query_volume_info;
 
 /**
  * Processes a query request for FileFsSizeInformation.
  */
-void guac_rdpdr_fs_process_query_size_info(guac_rdpdr_device* device, wStream* input_stream,
-        int file_id, int completion_id);
+guac_rdpdr_device_iorequest_handler guac_rdpdr_fs_process_query_size_info;
 
 /**
  * Processes a query request for FileFsAttributeInformation.
  */
-void guac_rdpdr_fs_process_query_attribute_info(guac_rdpdr_device* device, wStream* input_stream,
-        int file_id, int completion_id);
+guac_rdpdr_device_iorequest_handler guac_rdpdr_fs_process_query_attribute_info;
 
 /**
  * Processes a query request for FileFsFullSizeInformation.
  */
-void guac_rdpdr_fs_process_query_full_size_info(guac_rdpdr_device* device, wStream* input_stream,
-        int file_id, int completion_id);
+guac_rdpdr_device_iorequest_handler guac_rdpdr_fs_process_query_full_size_info;
 
 /**
  * Processes a query request for FileFsDeviceInformation.
  */
-void guac_rdpdr_fs_process_query_device_info(guac_rdpdr_device* device, wStream* input_stream,
-        int file_id, int completion_id);
+guac_rdpdr_device_iorequest_handler guac_rdpdr_fs_process_query_device_info;
 
 #endif
 
diff --git a/src/protocols/rdp/channels/rdpdr/rdpdr-fs-messages.c b/src/protocols/rdp/channels/rdpdr/rdpdr-fs-messages.c
new file mode 100644
index 0000000..529eea5
--- /dev/null
+++ b/src/protocols/rdp/channels/rdpdr/rdpdr-fs-messages.c
@@ -0,0 +1,516 @@
+/*
+ * 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/common-svc.h"
+#include "channels/rdpdr/rdpdr-fs-messages-dir-info.h"
+#include "channels/rdpdr/rdpdr-fs-messages-file-info.h"
+#include "channels/rdpdr/rdpdr-fs-messages-vol-info.h"
+#include "channels/rdpdr/rdpdr-fs-messages.h"
+#include "channels/rdpdr/rdpdr.h"
+#include "download.h"
+#include "fs.h"
+#include "unicode.h"
+
+#include <freerdp/channels/rdpdr.h>
+#include <guacamole/client.h>
+#include <winpr/nt.h>
+#include <winpr/stream.h>
+#include <winpr/wtypes.h>
+
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+
+void guac_rdpdr_fs_process_create(guac_rdp_common_svc* svc,
+        guac_rdpdr_device* device, guac_rdpdr_iorequest* iorequest,
+        wStream* input_stream) {
+
+    wStream* output_stream;
+    int file_id;
+
+    int desired_access, file_attributes;
+    int create_disposition, create_options, path_length;
+    char path[GUAC_RDP_FS_MAX_PATH];
+
+    /* Read "create" information */
+    Stream_Read_UINT32(input_stream, desired_access);
+    Stream_Seek_UINT64(input_stream); /* allocation size */
+    Stream_Read_UINT32(input_stream, file_attributes);
+    Stream_Seek_UINT32(input_stream); /* shared access */
+    Stream_Read_UINT32(input_stream, create_disposition);
+    Stream_Read_UINT32(input_stream, create_options);
+    Stream_Read_UINT32(input_stream, path_length);
+
+    /* Convert path to UTF-8 */
+    guac_rdp_utf16_to_utf8(Stream_Pointer(input_stream), path_length/2 - 1,
+            path, sizeof(path));
+
+    /* Open file */
+    file_id = guac_rdp_fs_open((guac_rdp_fs*) device->data, path,
+            desired_access, file_attributes,
+            create_disposition, create_options);
+
+    guac_client_log(svc->client, GUAC_LOG_DEBUG,
+            "%s: [file_id=%i] "
+             "desired_access=0x%x, file_attributes=0x%x, "
+             "create_disposition=0x%x, create_options=0x%x, path=\"%s\"",
+             __func__, file_id,
+             desired_access, file_attributes,
+             create_disposition, create_options, path);
+
+    /* If an error occurred, notify server */
+    if (file_id < 0) {
+        guac_client_log(svc->client, GUAC_LOG_ERROR,
+                "File open refused (%i): \"%s\"", file_id, path);
+
+        output_stream = guac_rdpdr_new_io_completion(device,
+                iorequest->completion_id, guac_rdp_fs_get_status(file_id), 5);
+        Stream_Write_UINT32(output_stream, 0); /* fileId */
+        Stream_Write_UINT8(output_stream,  0); /* information */
+    }
+
+    /* Otherwise, open succeeded */
+    else {
+
+        guac_rdp_fs_file* file;
+
+        output_stream = guac_rdpdr_new_io_completion(device,
+                iorequest->completion_id, STATUS_SUCCESS, 5);
+        Stream_Write_UINT32(output_stream, file_id);    /* fileId */
+        Stream_Write_UINT8(output_stream,  0);          /* information */
+
+        /* Create \Download if it doesn't exist */
+        file = guac_rdp_fs_get_file((guac_rdp_fs*) device->data, file_id);
+        if (file != NULL && strcmp(file->absolute_path, "\\") == 0) {
+            int download_id =
+                guac_rdp_fs_open((guac_rdp_fs*) device->data, "\\Download",
+                    GENERIC_READ, 0, FILE_OPEN_IF, FILE_DIRECTORY_FILE);
+
+            if (download_id >= 0)
+                guac_rdp_fs_close((guac_rdp_fs*) device->data, download_id);
+        }
+
+    }
+
+    guac_rdp_common_svc_write(svc, output_stream);
+
+}
+
+void guac_rdpdr_fs_process_read(guac_rdp_common_svc* svc,
+        guac_rdpdr_device* device, guac_rdpdr_iorequest* iorequest,
+        wStream* input_stream) {
+
+    UINT32 length;
+    UINT64 offset;
+    char* buffer;
+    int bytes_read;
+
+    wStream* output_stream;
+
+    /* Read packet */
+    Stream_Read_UINT32(input_stream, length);
+    Stream_Read_UINT64(input_stream, offset);
+
+    guac_client_log(svc->client, GUAC_LOG_DEBUG,
+            "%s: [file_id=%i] length=%i, offset=%" PRIu64,
+             __func__, iorequest->file_id, length, (uint64_t) offset);
+
+    /* Ensure buffer size does not exceed a safe maximum */
+    if (length > GUAC_RDP_MAX_READ_BUFFER)
+        length = GUAC_RDP_MAX_READ_BUFFER;
+
+    /* Allocate buffer */
+    buffer = malloc(length);
+
+    /* Attempt read */
+    bytes_read = guac_rdp_fs_read((guac_rdp_fs*) device->data,
+            iorequest->file_id, offset, buffer, length);
+
+    /* If error, return invalid parameter */
+    if (bytes_read < 0) {
+        output_stream = guac_rdpdr_new_io_completion(device,
+                iorequest->completion_id, guac_rdp_fs_get_status(bytes_read), 4);
+        Stream_Write_UINT32(output_stream, 0); /* Length */
+    }
+
+    /* Otherwise, send bytes read */
+    else {
+        output_stream = guac_rdpdr_new_io_completion(device,
+                iorequest->completion_id, STATUS_SUCCESS, 4+bytes_read);
+        Stream_Write_UINT32(output_stream, bytes_read);  /* Length */
+        Stream_Write(output_stream, buffer, bytes_read); /* ReadData */
+    }
+
+    guac_rdp_common_svc_write(svc, output_stream);
+    free(buffer);
+
+}
+
+void guac_rdpdr_fs_process_write(guac_rdp_common_svc* svc,
+        guac_rdpdr_device* device, guac_rdpdr_iorequest* iorequest,
+        wStream* input_stream) {
+
+    UINT32 length;
+    UINT64 offset;
+    int bytes_written;
+
+    wStream* output_stream;
+
+    /* Read packet */
+    Stream_Read_UINT32(input_stream, length);
+    Stream_Read_UINT64(input_stream, offset);
+    Stream_Seek(input_stream, 20); /* Padding */
+
+    guac_client_log(svc->client, GUAC_LOG_DEBUG,
+            "%s: [file_id=%i] length=%i, offset=%" PRIu64,
+             __func__, iorequest->file_id, length, (uint64_t) offset);
+
+    /* Attempt write */
+    bytes_written = guac_rdp_fs_write((guac_rdp_fs*) device->data,
+            iorequest->file_id, offset, Stream_Pointer(input_stream), length);
+
+    /* If error, return invalid parameter */
+    if (bytes_written < 0) {
+        output_stream = guac_rdpdr_new_io_completion(device,
+                iorequest->completion_id, guac_rdp_fs_get_status(bytes_written), 5);
+        Stream_Write_UINT32(output_stream, 0); /* Length */
+        Stream_Write_UINT8(output_stream, 0);  /* Padding */
+    }
+
+    /* Otherwise, send success */
+    else {
+        output_stream = guac_rdpdr_new_io_completion(device,
+                iorequest->completion_id, STATUS_SUCCESS, 5);
+        Stream_Write_UINT32(output_stream, bytes_written); /* Length */
+        Stream_Write_UINT8(output_stream, 0);              /* Padding */
+    }
+
+    guac_rdp_common_svc_write(svc, output_stream);
+
+}
+
+void guac_rdpdr_fs_process_close(guac_rdp_common_svc* svc,
+        guac_rdpdr_device* device, guac_rdpdr_iorequest* iorequest,
+        wStream* input_stream) {
+
+    wStream* output_stream;
+    guac_rdp_fs_file* file;
+
+    guac_client_log(svc->client, GUAC_LOG_DEBUG, "%s: [file_id=%i]",
+            __func__, iorequest->file_id);
+
+    /* Get file */
+    file = guac_rdp_fs_get_file((guac_rdp_fs*) device->data, iorequest->file_id);
+    if (file == NULL)
+        return;
+
+    /* If file was written to, and it's in the \Download folder, start stream */
+    if (file->bytes_written > 0 &&
+            strncmp(file->absolute_path, "\\Download\\", 10) == 0) {
+        guac_client_for_owner(svc->client, guac_rdp_download_to_user, file->absolute_path);
+        guac_rdp_fs_delete((guac_rdp_fs*) device->data, iorequest->file_id);
+    }
+
+    /* Close file */
+    guac_rdp_fs_close((guac_rdp_fs*) device->data, iorequest->file_id);
+
+    output_stream = guac_rdpdr_new_io_completion(device,
+            iorequest->completion_id, STATUS_SUCCESS, 4);
+    Stream_Write(output_stream, "\0\0\0\0", 4); /* Padding */
+
+    guac_rdp_common_svc_write(svc, output_stream);
+
+}
+
+void guac_rdpdr_fs_process_volume_info(guac_rdp_common_svc* svc,
+        guac_rdpdr_device* device, guac_rdpdr_iorequest* iorequest,
+        wStream* input_stream) {
+
+    int fs_information_class;
+
+    Stream_Read_UINT32(input_stream, fs_information_class);
+
+    /* Dispatch to appropriate class-specific handler */
+    switch (fs_information_class) {
+
+        case FileFsVolumeInformation:
+            guac_rdpdr_fs_process_query_volume_info(svc, device, iorequest, input_stream);
+            break;
+
+        case FileFsSizeInformation:
+            guac_rdpdr_fs_process_query_size_info(svc, device, iorequest, input_stream);
+            break;
+
+        case FileFsDeviceInformation:
+            guac_rdpdr_fs_process_query_device_info(svc, device, iorequest, input_stream);
+            break;
+
+        case FileFsAttributeInformation:
+            guac_rdpdr_fs_process_query_attribute_info(svc, device, iorequest, input_stream);
+            break;
+
+        case FileFsFullSizeInformation:
+            guac_rdpdr_fs_process_query_full_size_info(svc, device, iorequest, input_stream);
+            break;
+
+        default:
+            guac_client_log(svc->client, GUAC_LOG_DEBUG,
+                    "Unknown volume information class: 0x%x", fs_information_class);
+    }
+
+}
+
+void guac_rdpdr_fs_process_file_info(guac_rdp_common_svc* svc,
+        guac_rdpdr_device* device, guac_rdpdr_iorequest* iorequest,
+        wStream* input_stream) {
+
+    int fs_information_class;
+
+    Stream_Read_UINT32(input_stream, fs_information_class);
+
+    /* Dispatch to appropriate class-specific handler */
+    switch (fs_information_class) {
+
+        case FileBasicInformation:
+            guac_rdpdr_fs_process_query_basic_info(svc, device, iorequest, input_stream);
+            break;
+
+        case FileStandardInformation:
+            guac_rdpdr_fs_process_query_standard_info(svc, device, iorequest, input_stream);
+            break;
+
+        case FileAttributeTagInformation:
+            guac_rdpdr_fs_process_query_attribute_tag_info(svc, device, iorequest, input_stream);
+            break;
+
+        default:
+            guac_client_log(svc->client, GUAC_LOG_DEBUG,
+                    "Unknown file information class: 0x%x", fs_information_class);
+    }
+
+}
+
+void guac_rdpdr_fs_process_set_volume_info(guac_rdp_common_svc* svc,
+        guac_rdpdr_device* device, guac_rdpdr_iorequest* iorequest,
+        wStream* input_stream) {
+
+    wStream* output_stream = guac_rdpdr_new_io_completion(device,
+            iorequest->completion_id, STATUS_NOT_SUPPORTED, 0);
+
+    guac_client_log(svc->client, GUAC_LOG_DEBUG,
+            "%s: [file_id=%i] Set volume info not supported",
+            __func__, iorequest->file_id);
+
+    guac_rdp_common_svc_write(svc, output_stream);
+
+}
+
+void guac_rdpdr_fs_process_set_file_info(guac_rdp_common_svc* svc,
+        guac_rdpdr_device* device, guac_rdpdr_iorequest* iorequest,
+        wStream* input_stream) {
+
+    int fs_information_class;
+    int length;
+
+    Stream_Read_UINT32(input_stream, fs_information_class);
+    Stream_Read_UINT32(input_stream, length); /* Length */
+    Stream_Seek(input_stream, 24);            /* Padding */
+
+    /* Dispatch to appropriate class-specific handler */
+    switch (fs_information_class) {
+
+        case FileBasicInformation:
+            guac_rdpdr_fs_process_set_basic_info(svc, device, iorequest, length, input_stream);
+            break;
+
+        case FileEndOfFileInformation:
+            guac_rdpdr_fs_process_set_end_of_file_info(svc, device, iorequest, length, input_stream);
+            break;
+
+        case FileDispositionInformation:
+            guac_rdpdr_fs_process_set_disposition_info(svc, device, iorequest, length, input_stream);
+            break;
+
+        case FileRenameInformation:
+            guac_rdpdr_fs_process_set_rename_info(svc, device, iorequest, length, input_stream);
+            break;
+
+        case FileAllocationInformation:
+            guac_rdpdr_fs_process_set_allocation_info(svc, device, iorequest, length, input_stream);
+            break;
+
+        default:
+            guac_client_log(svc->client, GUAC_LOG_DEBUG,
+                    "Unknown file information class: 0x%x",
+                    fs_information_class);
+    }
+
+}
+
+void guac_rdpdr_fs_process_device_control(guac_rdp_common_svc* svc,
+        guac_rdpdr_device* device, guac_rdpdr_iorequest* iorequest,
+        wStream* input_stream) {
+
+    wStream* output_stream = guac_rdpdr_new_io_completion(device,
+            iorequest->completion_id, STATUS_INVALID_PARAMETER, 4);
+
+    guac_client_log(svc->client, GUAC_LOG_DEBUG, "%s: [file_id=%i] IGNORED",
+            __func__, iorequest->file_id);
+
+    /* No content for now */
+    Stream_Write_UINT32(output_stream, 0);
+
+    guac_rdp_common_svc_write(svc, output_stream);
+
+}
+
+void guac_rdpdr_fs_process_notify_change_directory(guac_rdp_common_svc* svc,
+        guac_rdpdr_device* device, guac_rdpdr_iorequest* iorequest,
+        wStream* input_stream) {
+
+    guac_client_log(svc->client, GUAC_LOG_DEBUG, "%s: [file_id=%i] Not "
+            "implemented", __func__, iorequest->file_id);
+
+}
+
+void guac_rdpdr_fs_process_query_directory(guac_rdp_common_svc* svc,
+        guac_rdpdr_device* device, guac_rdpdr_iorequest* iorequest,
+        wStream* input_stream) {
+
+    wStream* output_stream;
+
+    guac_rdp_fs_file* file;
+    int fs_information_class, initial_query;
+    int path_length;
+
+    const char* entry_name;
+
+    /* Get file */
+    file = guac_rdp_fs_get_file((guac_rdp_fs*) device->data, iorequest->file_id);
+    if (file == NULL)
+        return;
+
+    /* Read main header */
+    Stream_Read_UINT32(input_stream, fs_information_class);
+    Stream_Read_UINT8(input_stream,  initial_query);
+    Stream_Read_UINT32(input_stream, path_length);
+
+    /* If this is the first query, the path is included after padding */
+    if (initial_query) {
+
+        Stream_Seek(input_stream, 23);       /* Padding */
+
+        /* Convert path to UTF-8 */
+        guac_rdp_utf16_to_utf8(Stream_Pointer(input_stream), path_length/2 - 1,
+                file->dir_pattern, sizeof(file->dir_pattern));
+
+    }
+
+    guac_client_log(svc->client, GUAC_LOG_DEBUG, "%s: [file_id=%i] "
+            "initial_query=%i, dir_pattern=\"%s\"", __func__,
+            iorequest->file_id, initial_query, file->dir_pattern);
+
+    /* Find first matching entry in directory */
+    while ((entry_name = guac_rdp_fs_read_dir((guac_rdp_fs*) device->data,
+                    iorequest->file_id)) != NULL) {
+
+        /* Convert to absolute path */
+        char entry_path[GUAC_RDP_FS_MAX_PATH];
+        if (guac_rdp_fs_convert_path(file->absolute_path,
+                    entry_name, entry_path) == 0) {
+
+            int entry_file_id;
+
+            /* Pattern defined and match fails, continue with next file */
+            if (guac_rdp_fs_matches(entry_path, file->dir_pattern))
+                continue;
+
+            /* Open directory entry */
+            entry_file_id = guac_rdp_fs_open((guac_rdp_fs*) device->data,
+                    entry_path, FILE_READ_DATA, 0, FILE_OPEN, 0);
+
+            if (entry_file_id >= 0) {
+
+                /* Dispatch to appropriate class-specific handler */
+                switch (fs_information_class) {
+
+                    case FileDirectoryInformation:
+                        guac_rdpdr_fs_process_query_directory_info(svc, device,
+                                iorequest, entry_name, entry_file_id);
+                        break;
+
+                    case FileFullDirectoryInformation:
+                        guac_rdpdr_fs_process_query_full_directory_info(svc,
+                                device, iorequest, entry_name, entry_file_id);
+                        break;
+
+                    case FileBothDirectoryInformation:
+                        guac_rdpdr_fs_process_query_both_directory_info(svc,
+                                device, iorequest, entry_name, entry_file_id);
+                        break;
+
+                    case FileNamesInformation:
+                        guac_rdpdr_fs_process_query_names_info(svc, device,
+                                iorequest, entry_name, entry_file_id);
+                        break;
+
+                    default:
+                        guac_client_log(svc->client, GUAC_LOG_DEBUG,
+                                "Unknown dir information class: 0x%x",
+                                fs_information_class);
+                }
+
+                guac_rdp_fs_close((guac_rdp_fs*) device->data, entry_file_id);
+                return;
+
+            } /* end if file exists */
+        } /* end if path valid */
+    } /* end if entry exists */
+
+    /*
+     * Handle errors as a lack of files.
+     */
+
+    output_stream = guac_rdpdr_new_io_completion(device,
+            iorequest->completion_id, STATUS_NO_MORE_FILES, 5);
+
+    Stream_Write_UINT32(output_stream, 0); /* Length */
+    Stream_Write_UINT8(output_stream, 0);  /* Padding */
+
+    guac_rdp_common_svc_write(svc, output_stream);
+
+}
+
+void guac_rdpdr_fs_process_lock_control(guac_rdp_common_svc* svc,
+        guac_rdpdr_device* device, guac_rdpdr_iorequest* iorequest,
+        wStream* input_stream) {
+
+    wStream* output_stream = guac_rdpdr_new_io_completion(device,
+            iorequest->completion_id, STATUS_NOT_SUPPORTED, 5);
+
+    guac_client_log(svc->client, GUAC_LOG_DEBUG,
+            "%s: [file_id=%i] Lock not supported",
+            __func__, iorequest->file_id);
+
+    Stream_Zero(output_stream, 5); /* Padding */
+
+    guac_rdp_common_svc_write(svc, output_stream);
+
+}
+
diff --git a/src/protocols/rdp/channels/rdpdr/rdpdr-fs-messages.h b/src/protocols/rdp/channels/rdpdr/rdpdr-fs-messages.h
new file mode 100644
index 0000000..63fcbb9
--- /dev/null
+++ b/src/protocols/rdp/channels/rdpdr/rdpdr-fs-messages.h
@@ -0,0 +1,110 @@
+/*
+ * 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_RDPDR_FS_MESSAGES_H
+#define GUAC_RDP_CHANNELS_RDPDR_FS_MESSAGES_H
+
+/**
+ * Handlers for core drive I/O requests. Requests handled here may be simple
+ * messages handled directly, or more complex multi-type messages handled
+ * elsewhere.
+ *
+ * @file rdpdr-fs-messages.h
+ */
+
+#include "channels/rdpdr/rdpdr.h"
+
+#include <winpr/stream.h>
+
+/**
+ * Handles a Server Create Drive Request. Despite its name, this request opens
+ * a file.
+ */
+guac_rdpdr_device_iorequest_handler guac_rdpdr_fs_process_create;
+
+/**
+ * Handles a Server Close Drive Request. This request closes an open file.
+ */
+guac_rdpdr_device_iorequest_handler guac_rdpdr_fs_process_close;
+
+/**
+ * Handles a Server Drive Read Request. This request reads from a file.
+ */
+guac_rdpdr_device_iorequest_handler guac_rdpdr_fs_process_read;
+
+/**
+ * Handles a Server Drive Write Request. This request writes to a file.
+ */
+guac_rdpdr_device_iorequest_handler guac_rdpdr_fs_process_write;
+
+/**
+ * Handles a Server Drive Control Request. This request handles one of any
+ * number of Windows FSCTL_* control functions.
+ */
+guac_rdpdr_device_iorequest_handler guac_rdpdr_fs_process_device_control;
+
+/**
+ * Handles a Server Drive Query Volume Information Request. This request
+ * queries information about the redirected volume (drive). This request
+ * has several query types which have their own handlers defined in a
+ * separate file.
+ */
+guac_rdpdr_device_iorequest_handler guac_rdpdr_fs_process_volume_info;
+
+/**
+ * Handles a Server Drive Set Volume Information Request. Currently, this
+ * RDPDR implementation does not support setting of volume information.
+ */
+guac_rdpdr_device_iorequest_handler guac_rdpdr_fs_process_set_volume_info;
+
+/**
+ * Handles a Server Drive Query Information Request. This request queries
+ * information about a specific file. This request has several query types
+ * which have their own handlers defined in a separate file.
+ */
+guac_rdpdr_device_iorequest_handler guac_rdpdr_fs_process_file_info;
+
+/**
+ * Handles a Server Drive Set Information Request. This request sets
+ * information about a specific file. Currently, this RDPDR implementation does
+ * not support setting of file information.
+ */
+guac_rdpdr_device_iorequest_handler guac_rdpdr_fs_process_set_file_info;
+
+/**
+ * Handles a Server Drive Query Directory Request. This request queries
+ * information about a specific directory. This request has several query types
+ * which have their own handlers defined in a separate file.
+ */
+guac_rdpdr_device_iorequest_handler guac_rdpdr_fs_process_query_directory;
+
+/**
+ * Handles a Server Drive NotifyChange Directory Request. This request requests
+ * directory change notification.
+ */
+guac_rdpdr_device_iorequest_handler guac_rdpdr_fs_process_notify_change_directory;
+
+/**
+ * Handles a Server Drive Lock Control Request. This request locks or unlocks
+ * portions of a file.
+ */
+guac_rdpdr_device_iorequest_handler guac_rdpdr_fs_process_lock_control;
+
+#endif
+
diff --git a/src/protocols/rdp/guac_rdpdr/rdpdr_fs_service.c b/src/protocols/rdp/channels/rdpdr/rdpdr-fs.c
similarity index 61%
rename from src/protocols/rdp/guac_rdpdr/rdpdr_fs_service.c
rename to src/protocols/rdp/channels/rdpdr/rdpdr-fs.c
index e064d4d..ed67ac6 100644
--- a/src/protocols/rdp/guac_rdpdr/rdpdr_fs_service.c
+++ b/src/protocols/rdp/channels/rdpdr/rdpdr-fs.c
@@ -17,119 +17,116 @@
  * under the License.
  */
 
-
-#include "config.h"
-
+#include "channels/rdpdr/rdpdr-fs.h"
+#include "channels/rdpdr/rdpdr-fs-messages.h"
+#include "channels/rdpdr/rdpdr.h"
 #include "rdp.h"
-#include "rdpdr_fs_messages.h"
-#include "rdpdr_messages.h"
-#include "rdpdr_service.h"
 
-#include <freerdp/utils/svc_plugin.h>
+#include <freerdp/channels/rdpdr.h>
+#include <freerdp/settings.h>
 #include <guacamole/client.h>
-#include <guacamole/protocol.h>
-#include <guacamole/socket.h>
 #include <guacamole/unicode.h>
-
-#ifdef ENABLE_WINPR
 #include <winpr/stream.h>
-#else
-#include "compat/winpr-stream.h"
-#endif
 
-static void guac_rdpdr_device_fs_iorequest_handler(guac_rdpdr_device* device,
-        wStream* input_stream, int file_id, int completion_id, int major_func, int minor_func) {
+#include <stddef.h>
 
-    switch (major_func) {
+void guac_rdpdr_device_fs_iorequest_handler(guac_rdp_common_svc* svc,
+        guac_rdpdr_device* device, guac_rdpdr_iorequest* iorequest,
+        wStream* input_stream) {
+
+    switch (iorequest->major_func) {
 
         /* File open */
         case IRP_MJ_CREATE:
-            guac_rdpdr_fs_process_create(device, input_stream, completion_id);
+            guac_rdpdr_fs_process_create(svc, device, iorequest, input_stream);
             break;
 
         /* File close */
         case IRP_MJ_CLOSE:
-            guac_rdpdr_fs_process_close(device, input_stream, file_id, completion_id);
+            guac_rdpdr_fs_process_close(svc, device, iorequest, input_stream);
             break;
 
         /* File read */
         case IRP_MJ_READ:
-            guac_rdpdr_fs_process_read(device, input_stream, file_id, completion_id);
+            guac_rdpdr_fs_process_read(svc, device, iorequest, input_stream);
             break;
 
         /* File write */
         case IRP_MJ_WRITE:
-            guac_rdpdr_fs_process_write(device, input_stream, file_id, completion_id);
+            guac_rdpdr_fs_process_write(svc, device, iorequest, input_stream);
             break;
 
         /* Device control request (Windows FSCTL_ control codes) */
         case IRP_MJ_DEVICE_CONTROL:
-            guac_rdpdr_fs_process_device_control(device, input_stream, file_id, completion_id);
+            guac_rdpdr_fs_process_device_control(svc, device, iorequest, input_stream);
             break;
 
         /* Query volume (drive) information */
         case IRP_MJ_QUERY_VOLUME_INFORMATION:
-            guac_rdpdr_fs_process_volume_info(device, input_stream, file_id, completion_id);
+            guac_rdpdr_fs_process_volume_info(svc, device, iorequest, input_stream);
             break;
 
         /* Set volume (drive) information */
         case IRP_MJ_SET_VOLUME_INFORMATION:
-            guac_rdpdr_fs_process_set_volume_info(device, input_stream, file_id, completion_id);
+            guac_rdpdr_fs_process_set_volume_info(svc, device, iorequest, input_stream);
             break;
 
         /* Query file information */
         case IRP_MJ_QUERY_INFORMATION:
-            guac_rdpdr_fs_process_file_info(device, input_stream, file_id, completion_id);
+            guac_rdpdr_fs_process_file_info(svc, device, iorequest, input_stream);
             break;
 
         /* Set file information */
         case IRP_MJ_SET_INFORMATION:
-            guac_rdpdr_fs_process_set_file_info(device, input_stream, file_id, completion_id);
+            guac_rdpdr_fs_process_set_file_info(svc, device, iorequest, input_stream);
             break;
 
         case IRP_MJ_DIRECTORY_CONTROL:
 
             /* Enumerate directory contents */
-            if (minor_func == IRP_MN_QUERY_DIRECTORY)
-                guac_rdpdr_fs_process_query_directory(device, input_stream, file_id, completion_id);
+            if (iorequest->minor_func == IRP_MN_QUERY_DIRECTORY)
+                guac_rdpdr_fs_process_query_directory(svc, device, iorequest,
+                        input_stream);
 
             /* Request notification of changes to directory */
-            else if (minor_func == IRP_MN_NOTIFY_CHANGE_DIRECTORY)
-                guac_rdpdr_fs_process_notify_change_directory(device, input_stream,
-                        file_id, completion_id);
+            else if (iorequest->minor_func == IRP_MN_NOTIFY_CHANGE_DIRECTORY)
+                guac_rdpdr_fs_process_notify_change_directory(svc, device,
+                        iorequest, input_stream);
 
             break;
 
         /* Lock/unlock portions of a file */
         case IRP_MJ_LOCK_CONTROL:
-            guac_rdpdr_fs_process_lock_control(device, input_stream, file_id, completion_id);
+            guac_rdpdr_fs_process_lock_control(svc, device, iorequest, input_stream);
             break;
 
         default:
-            guac_client_log(device->rdpdr->client, GUAC_LOG_ERROR,
+            guac_client_log(svc->client, GUAC_LOG_DEBUG,
                     "Unknown filesystem I/O request function: 0x%x/0x%x",
-                    major_func, minor_func);
+                    iorequest->major_func, iorequest->minor_func);
     }
 
 }
 
-static void guac_rdpdr_device_fs_free_handler(guac_rdpdr_device* device) {
+void guac_rdpdr_device_fs_free_handler(guac_rdp_common_svc* svc,
+        guac_rdpdr_device* device) {
 
     Stream_Free(device->device_announce, 1);
     
 }
 
-void guac_rdpdr_register_fs(guac_rdpdrPlugin* rdpdr, char* drive_name) {
+void guac_rdpdr_register_fs(guac_rdp_common_svc* svc, char* drive_name) {
 
-    guac_client* client = rdpdr->client;
+    guac_client* client = svc->client;
     guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
+
+    guac_rdpdr* rdpdr = (guac_rdpdr*) svc->data;
     int id = rdpdr->devices_registered++;
 
     /* Get new device */
     guac_rdpdr_device* device = &(rdpdr->devices[id]);
 
     /* Init device */
-    device->rdpdr       = rdpdr;
     device->device_id   = id;
     device->device_name = drive_name;
     int device_name_len = guac_utf8_strlen(device->device_name);
diff --git a/src/protocols/rdp/guac_rdpdr/rdpdr_fs_service.h b/src/protocols/rdp/channels/rdpdr/rdpdr-fs.h
similarity index 69%
rename from src/protocols/rdp/guac_rdpdr/rdpdr_fs_service.h
rename to src/protocols/rdp/channels/rdpdr/rdpdr-fs.h
index f990806..ae90045 100644
--- a/src/protocols/rdp/guac_rdpdr/rdpdr_fs_service.h
+++ b/src/protocols/rdp/channels/rdpdr/rdpdr-fs.h
@@ -17,38 +17,46 @@
  * under the License.
  */
 
-
-#ifndef __GUAC_RDPDR_FS_H
-#define __GUAC_RDPDR_FS_H
+#ifndef GUAC_RDP_RDPDR_FS_H
+#define GUAC_RDP_RDPDR_FS_H
 
 /**
  * Functions and macros specific to filesystem handling and initialization
- * independent of RDP.  The functions here may deal with the RDPDR device
+ * independent of RDP. The functions here may deal with the RDPDR device
  * directly, but their semantics must not deal with RDP protocol messaging.
  * Functions here represent a virtual Windows-style filesystem on top of UNIX
  * system calls and structures, using the guac_rdpdr_device structure as a home
  * for common data.
  *
- * @file rdpdr_fs.h 
+ * @file rdpdr-fs.h 
  */
 
-#include "config.h"
-
-#include "rdpdr_service.h"
+#include "channels/common-svc.h"
+#include "channels/rdpdr/rdpdr.h"
 
 #include <guacamole/pool.h>
 
 /**
+ * The UTF-16 string that should be sent as the label of the filesystem.
+ */
+#define GUAC_FILESYSTEM_LABEL "G\0U\0A\0C\0F\0I\0L\0E\0"
+
+/**
+ * The size of GUAC_FILESYSTEM_LABEL in bytes.
+ */
+#define GUAC_FILESYSTEM_LABEL_LENGTH 16
+
+/**
  * Registers a new filesystem device within the RDPDR plugin. This must be done
  * before RDPDR connection finishes.
  * 
- * @param rdpdr
- *     The RDP device redirection plugin with which to register the device.
+ * @param svc
+ *     The static virtual channel instance being used for RDPDR.
  * 
  * @param drive_name
  *     The name of the redirected drive to display in the RDP connection.
  */
-void guac_rdpdr_register_fs(guac_rdpdrPlugin* rdpdr, char* drive_name);
+void guac_rdpdr_register_fs(guac_rdp_common_svc* svc, char* drive_name);
 
 #endif
 
diff --git a/src/protocols/rdp/channels/rdpdr/rdpdr-messages.c b/src/protocols/rdp/channels/rdpdr/rdpdr-messages.c
new file mode 100644
index 0000000..28a27e6
--- /dev/null
+++ b/src/protocols/rdp/channels/rdpdr/rdpdr-messages.c
@@ -0,0 +1,349 @@
+/*
+ * 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/rdpdr/rdpdr-messages.h"
+#include "channels/rdpdr/rdpdr.h"
+#include "rdp.h"
+#include "settings.h"
+
+#include <freerdp/channels/rdpdr.h>
+#include <guacamole/client.h>
+#include <winpr/stream.h>
+
+#include <stdlib.h>
+#include <string.h>
+
+/**
+ * Sends a Client Announce Reply message. The Client Announce Reply message is
+ * required to be sent in response to the Server Announce Request message. See:
+ *
+ * https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/d6fe6d1b-c145-4a6f-99aa-4fe3cdcea398
+ *
+ * @param svc
+ *     The guac_rdp_common_svc representing the static virtual channel being
+ *     used for RDPDR.
+ *
+ * @param major
+ *     The major version of the RDPDR protocol in use. This value must always
+ *     be 1.
+ *
+ * @param minor
+ *     The minor version of the RDPDR protocol in use. This value must be
+ *     either 2, 5, 10, 12, or 13.
+ *
+ * @param client_id
+ *     The client ID received in the Server Announce Request, or a randomly
+ *     generated ID.
+ */
+static void guac_rdpdr_send_client_announce_reply(guac_rdp_common_svc* svc,
+        unsigned int major, unsigned int minor, unsigned int client_id) {
+
+    wStream* output_stream = Stream_New(NULL, 12);
+
+    /* Write header */
+    Stream_Write_UINT16(output_stream, RDPDR_CTYP_CORE);
+    Stream_Write_UINT16(output_stream, PAKID_CORE_CLIENTID_CONFIRM);
+
+    /* Write content */
+    Stream_Write_UINT16(output_stream, major);
+    Stream_Write_UINT16(output_stream, minor);
+    Stream_Write_UINT32(output_stream, client_id);
+
+    guac_rdp_common_svc_write(svc, output_stream);
+
+}
+
+/**
+ * Sends a Client Name Request message. The Client Name Request message is used
+ * by the client to announce its own name. See:
+ *
+ * https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/902497f1-3b1c-4aee-95f8-1668f9b7b7d2
+ *
+ * @param svc
+ *     The guac_rdp_common_svc representing the static virtual channel being
+ *     used for RDPDR.
+ *
+ * @param name
+ *     The name that should be used for the client.
+ */
+static void guac_rdpdr_send_client_name_request(guac_rdp_common_svc* svc,
+        const char* name) {
+
+    int name_bytes = strlen(name) + 1;
+    wStream* output_stream = Stream_New(NULL, 16 + name_bytes);
+
+    /* Write header */
+    Stream_Write_UINT16(output_stream, RDPDR_CTYP_CORE);
+    Stream_Write_UINT16(output_stream, PAKID_CORE_CLIENT_NAME);
+
+    /* Write content */
+    Stream_Write_UINT32(output_stream, 0); /* ASCII */
+    Stream_Write_UINT32(output_stream, 0); /* 0 required by RDPDR spec */
+    Stream_Write_UINT32(output_stream, name_bytes);
+    Stream_Write(output_stream, name, name_bytes);
+
+    guac_rdp_common_svc_write(svc, output_stream);
+
+}
+
+/**
+ * Sends a Client Core Capability Response message. The Client Core Capability
+ * Response message is used to announce the client's capabilities, in response
+ * to receiving the server's capabilities via a Server Core Capability Request.
+ * See:
+ *
+ * https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/f513bf87-cca0-488a-ac5c-18cf18f4a7e1
+ *
+ * @param svc
+ *     The guac_rdp_common_svc representing the static virtual channel being
+ *     used for RDPDR.
+ */
+static void guac_rdpdr_send_client_capability(guac_rdp_common_svc* svc) {
+
+    wStream* output_stream = Stream_New(NULL, 256);
+    guac_client_log(svc->client, GUAC_LOG_DEBUG, "Sending capabilities...");
+
+    /* Write header */
+    Stream_Write_UINT16(output_stream, RDPDR_CTYP_CORE);
+    Stream_Write_UINT16(output_stream, PAKID_CORE_CLIENT_CAPABILITY);
+
+    /* Capability count + padding */
+    Stream_Write_UINT16(output_stream, 3);
+    Stream_Write_UINT16(output_stream, 0); /* Padding */
+
+    /* General capability header */
+    Stream_Write_UINT16(output_stream, CAP_GENERAL_TYPE);
+    Stream_Write_UINT16(output_stream, 44);
+    Stream_Write_UINT32(output_stream, GENERAL_CAPABILITY_VERSION_02);
+
+    /* General capability data */
+    Stream_Write_UINT32(output_stream, GUAC_OS_TYPE);                /* osType - required to be ignored */
+    Stream_Write_UINT32(output_stream, 0);                           /* osVersion */
+    Stream_Write_UINT16(output_stream, 1);                           /* protocolMajor - must be set to 1 */
+    Stream_Write_UINT16(output_stream, RDPDR_MINOR_RDP_VERSION_5_2); /* protocolMinor */
+    Stream_Write_UINT32(output_stream, 0xFFFF);                      /* ioCode1 */
+    Stream_Write_UINT32(output_stream, 0);                           /* ioCode2 */
+    Stream_Write_UINT32(output_stream,
+                                      RDPDR_DEVICE_REMOVE_PDUS
+                                    | RDPDR_CLIENT_DISPLAY_NAME_PDU
+                                    | RDPDR_USER_LOGGEDON_PDU); /* extendedPDU */
+    Stream_Write_UINT32(output_stream, 0);                      /* extraFlags1 */
+    Stream_Write_UINT32(output_stream, 0);                      /* extraFlags2 */
+    Stream_Write_UINT32(output_stream, 0);                      /* SpecialTypeDeviceCap */
+
+    /* Printer support header */
+    Stream_Write_UINT16(output_stream, CAP_PRINTER_TYPE);
+    Stream_Write_UINT16(output_stream, 8);
+    Stream_Write_UINT32(output_stream, PRINT_CAPABILITY_VERSION_01);
+
+    /* Drive support header */
+    Stream_Write_UINT16(output_stream, CAP_DRIVE_TYPE);
+    Stream_Write_UINT16(output_stream, 8);
+    Stream_Write_UINT32(output_stream, DRIVE_CAPABILITY_VERSION_02);
+
+    guac_rdp_common_svc_write(svc, output_stream);
+    guac_client_log(svc->client, GUAC_LOG_DEBUG, "Capabilities sent.");
+
+}
+
+/**
+ * Sends a Client Device List Announce Request message. The Client Device List
+ * Announce Request message is used by the client to enumerate all devices
+ * which should be made available within the RDP session via RDPDR. See:
+ *
+ * https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/10ef9ada-cba2-4384-ab60-7b6290ed4a9a
+ *
+ * @param svc
+ *     The guac_rdp_common_svc representing the static virtual channel being
+ *     used for RDPDR.
+ */
+static void guac_rdpdr_send_client_device_list_announce_request(guac_rdp_common_svc* svc) {
+
+    guac_rdpdr* rdpdr = (guac_rdpdr*) svc->data;
+
+    /* Calculate number of bytes needed for the stream */
+    int streamBytes = 16;
+    for (int i=0; i < rdpdr->devices_registered; i++)
+        streamBytes += rdpdr->devices[i].device_announce_len;
+
+    /* Allocate the stream */
+    wStream* output_stream = Stream_New(NULL, streamBytes);
+
+    /* Write header */
+    Stream_Write_UINT16(output_stream, RDPDR_CTYP_CORE);
+    Stream_Write_UINT16(output_stream, PAKID_CORE_DEVICELIST_ANNOUNCE);
+
+    /* Get the stream for each of the devices. */
+    Stream_Write_UINT32(output_stream, rdpdr->devices_registered);
+    for (int i=0; i<rdpdr->devices_registered; i++) {
+        
+        Stream_Write(output_stream,
+                Stream_Buffer(rdpdr->devices[i].device_announce),
+                rdpdr->devices[i].device_announce_len);
+
+        guac_client_log(svc->client, GUAC_LOG_DEBUG, "Registered device %i (%s)",
+                rdpdr->devices[i].device_id, rdpdr->devices[i].device_name);
+        
+    }
+
+    guac_rdp_common_svc_write(svc, output_stream);
+    guac_client_log(svc->client, GUAC_LOG_DEBUG, "All supported devices sent.");
+
+}
+
+void guac_rdpdr_process_server_announce(guac_rdp_common_svc* svc,
+        wStream* input_stream) {
+
+    unsigned int major, minor, client_id;
+
+    Stream_Read_UINT16(input_stream, major);
+    Stream_Read_UINT16(input_stream, minor);
+    Stream_Read_UINT32(input_stream, client_id);
+
+    /* Must choose own client ID if minor not >= 12 */
+    if (minor < 12)
+        client_id = random() & 0xFFFF;
+
+    guac_client_log(svc->client, GUAC_LOG_INFO, "Connected to RDPDR %u.%u as client 0x%04x", major, minor, client_id);
+
+    /* Respond to announce */
+    guac_rdpdr_send_client_announce_reply(svc, major, minor, client_id);
+
+    /* Name request */
+    guac_rdpdr_send_client_name_request(svc, ((guac_rdp_client*) svc->client->data)->settings->client_name);
+
+}
+
+void guac_rdpdr_process_clientid_confirm(guac_rdp_common_svc* svc,
+        wStream* input_stream) {
+    guac_client_log(svc->client, GUAC_LOG_DEBUG, "Client ID confirmed");
+}
+
+void guac_rdpdr_process_device_reply(guac_rdp_common_svc* svc,
+        wStream* input_stream) {
+
+    guac_rdpdr* rdpdr = (guac_rdpdr*) svc->data;
+
+    unsigned int device_id, ntstatus;
+    int severity, c, n, facility, code;
+
+    Stream_Read_UINT32(input_stream, device_id);
+    Stream_Read_UINT32(input_stream, ntstatus);
+
+    severity = (ntstatus & 0xC0000000) >> 30;
+    c        = (ntstatus & 0x20000000) >> 29;
+    n        = (ntstatus & 0x10000000) >> 28;
+    facility = (ntstatus & 0x0FFF0000) >> 16;
+    code     =  ntstatus & 0x0000FFFF;
+
+    /* Log error / information */
+    if (device_id < rdpdr->devices_registered) {
+
+        if (severity == 0x0)
+            guac_client_log(svc->client, GUAC_LOG_DEBUG, "Device %i (%s) connected successfully",
+                    device_id, rdpdr->devices[device_id].device_name);
+
+        else
+            guac_client_log(svc->client, GUAC_LOG_ERROR, "Problem connecting device %i (%s): "
+                    "severity=0x%x, c=0x%x, n=0x%x, facility=0x%x, code=0x%x",
+                     device_id, rdpdr->devices[device_id].device_name,
+                     severity,      c,      n,      facility,      code);
+
+    }
+
+    else
+        guac_client_log(svc->client, GUAC_LOG_ERROR, "Unknown device ID: 0x%08x", device_id);
+
+}
+
+void guac_rdpdr_process_device_iorequest(guac_rdp_common_svc* svc,
+        wStream* input_stream) {
+
+    guac_rdpdr* rdpdr = (guac_rdpdr*) svc->data;
+    guac_rdpdr_iorequest iorequest;
+
+    /* Read header */
+    Stream_Read_UINT32(input_stream, iorequest.device_id);
+    Stream_Read_UINT32(input_stream, iorequest.file_id);
+    Stream_Read_UINT32(input_stream, iorequest.completion_id);
+    Stream_Read_UINT32(input_stream, iorequest.major_func);
+    Stream_Read_UINT32(input_stream, iorequest.minor_func);
+
+    /* If printer, run printer handlers */
+    if (iorequest.device_id >= 0 && iorequest.device_id < rdpdr->devices_registered) {
+
+        /* Call handler on device */
+        guac_rdpdr_device* device = &(rdpdr->devices[iorequest.device_id]);
+        device->iorequest_handler(svc, device, &iorequest, input_stream);
+
+    }
+
+    else
+        guac_client_log(svc->client, GUAC_LOG_ERROR, "Unknown device ID: "
+                "0x%08x", iorequest.device_id);
+
+}
+
+void guac_rdpdr_process_server_capability(guac_rdp_common_svc* svc,
+        wStream* input_stream) {
+
+    int count;
+    int i;
+
+    /* Read header */
+    Stream_Read_UINT16(input_stream, count);
+    Stream_Seek(input_stream, 2);
+
+    /* Parse capabilities */
+    for (i=0; i<count; i++) {
+
+        int type;
+        int length;
+
+        Stream_Read_UINT16(input_stream, type);
+        Stream_Read_UINT16(input_stream, length);
+
+        /* Ignore all for now */
+        guac_client_log(svc->client, GUAC_LOG_DEBUG, "Ignoring server capability set type=0x%04x, length=%i", type, length);
+        Stream_Seek(input_stream, length - 4);
+
+    }
+
+    /* Send own capabilities */
+    guac_rdpdr_send_client_capability(svc);
+
+}
+
+void guac_rdpdr_process_user_loggedon(guac_rdp_common_svc* svc,
+        wStream* input_stream) {
+
+    guac_client_log(svc->client, GUAC_LOG_INFO, "RDPDR user logged on");
+    guac_rdpdr_send_client_device_list_announce_request(svc);
+
+}
+
+void guac_rdpdr_process_prn_cache_data(guac_rdp_common_svc* svc,
+        wStream* input_stream) {
+    guac_client_log(svc->client, GUAC_LOG_DEBUG, "Ignoring printer cached configuration data");
+}
+
+void guac_rdpdr_process_prn_using_xps(guac_rdp_common_svc* svc,
+        wStream* input_stream) {
+    guac_client_log(svc->client, GUAC_LOG_WARNING, "Printer unexpectedly switched to XPS mode");
+}
diff --git a/src/protocols/rdp/channels/rdpdr/rdpdr-messages.h b/src/protocols/rdp/channels/rdpdr/rdpdr-messages.h
new file mode 100644
index 0000000..a79c83b
--- /dev/null
+++ b/src/protocols/rdp/channels/rdpdr/rdpdr-messages.h
@@ -0,0 +1,136 @@
+/*
+ * 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_RDPDR_MESSAGES_H
+#define GUAC_RDP_CHANNELS_RDPDR_MESSAGES_H
+
+#include "channels/common-svc.h"
+#include "channels/rdpdr/rdpdr.h"
+
+#include <winpr/stream.h>
+
+#include <stdint.h>
+
+/**
+ * A 32-bit arbitrary value for the osType field of certain requests. As this
+ * value is defined as completely arbitrary and required to be ignored by the
+ * server, we send "GUAC" as an integer.
+ */
+#define GUAC_OS_TYPE (*((uint32_t*) "GUAC"))
+
+/**
+ * Handler which processes a message specific to the RDPDR channel.
+ *
+ * @param svc
+ *     The guac_rdp_common_svc representing the static virtual channel being
+ *     used for RDPDR.
+ *
+ * @param input_stream
+ *     A wStream containing the entire received message.
+ */
+typedef void guac_rdpdr_message_handler(guac_rdp_common_svc* svc,
+        wStream* input_stream);
+
+/**
+ * Handler which processes a received Server Announce Request message. The
+ * Server Announce Request message begins the RDPDR exchange and provides a
+ * client ID which the RDPDR client may use. The client may also supply its
+ * own, randomly-generated ID, and is required to do so for older versions of
+ * RDPDR. See:
+ *
+ * https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/046047aa-62d8-49f9-bf16-7fe41880aaf4
+ */
+guac_rdpdr_message_handler guac_rdpdr_process_server_announce;
+
+/**
+ * Handler which processes a received Server Client ID Confirm message. The
+ * Server Client ID Confirm message is sent by the server to confirm the client
+ * ID requested by the client (in its response to the Server Announce Request)
+ * has been accepted. See:
+ *
+ * https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/bbbb9666-6994-4cf6-8e65-0d46eb319c6e
+ */
+guac_rdpdr_message_handler guac_rdpdr_process_clientid_confirm;
+
+/**
+ * Handler which processes a received Server Device Announce Response message.
+ * The Server Device Announce Response message is sent in response to a Client
+ * Device List Announce message to communicate the success/failure status of
+ * device creation. See:
+ *
+ * https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/a4c0b619-6e87-4721-bdc4-5d2db7f485f3
+ */
+guac_rdpdr_message_handler guac_rdpdr_process_device_reply;
+
+/**
+ * Handler which processes a received Device I/O Request message. The Device
+ * I/O Request message makes up the majority of traffic once RDPDR is
+ * established. Each I/O request consists of a device-specific major/minor
+ * function number pair, as well as several parameters. Device-specific
+ * handling of I/O requests within Guacamole is delegated to device- and
+ * function-specific implementations of yet another function type:
+ * guac_rdpdr_device_iorequest_handler.
+ *
+ * See:
+ *
+ * https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/a087ffa8-d0d5-4874-ac7b-0494f63e2d5d
+ */
+guac_rdpdr_message_handler guac_rdpdr_process_device_iorequest;
+
+/**
+ * Handler which processes a received Server Core Capability Request message.
+ * The Server Core Capability Request message is sent by the server to
+ * communicate its capabilities and to request that the client communicate the
+ * same. See:
+ *
+ * https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/702789c3-b924-4bc2-9280-3221bc7d6797
+ */
+guac_rdpdr_message_handler guac_rdpdr_process_server_capability;
+
+/**
+ * Handler which processes a received Server User Logged On message. The Server
+ * User Logged On message is sent by the server to notify that the user has
+ * logged on to the session. See:
+ *
+ * https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/dfc0e8ed-a242-4d00-bb88-e779e08f2f61
+ */
+guac_rdpdr_message_handler guac_rdpdr_process_user_loggedon;
+
+/**
+ * Handler which processes any one of several RDPDR messages specific to cached
+ * printer configuration data, each of these messages having the same
+ * PAKID_PRN_CACHE_DATA packet ID. The Guacamole RDPDR implementation ignores
+ * all PAKID_PRN_CACHE_DATA messages. See:
+ *
+ * https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpepc/7fccae60-f077-433b-9dee-9bad4238bf40
+ */
+guac_rdpdr_message_handler guac_rdpdr_process_prn_cache_data;
+
+/**
+ * Handler which processes a received Server Printer Set XPS Mode message. The
+ * Server Printer Set XPS Mode message is specific to printers and requests
+ * that the client printer be set to XPS mode. The Guacamole RDPDR
+ * implementation ignores any request to set the printer to XPS mode. See:
+ *
+ * https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpepc/f1789a66-bcd0-4df3-bfc2-6e7330d63145
+ */
+guac_rdpdr_message_handler guac_rdpdr_process_prn_using_xps;
+
+#endif
+
diff --git a/src/protocols/rdp/guac_rdpdr/rdpdr_printer.c b/src/protocols/rdp/channels/rdpdr/rdpdr-printer.c
similarity index 71%
rename from src/protocols/rdp/guac_rdpdr/rdpdr_printer.c
rename to src/protocols/rdp/channels/rdpdr/rdpdr-printer.c
index ace025c..d90116f 100644
--- a/src/protocols/rdp/guac_rdpdr/rdpdr_printer.c
+++ b/src/protocols/rdp/channels/rdpdr/rdpdr-printer.c
@@ -17,41 +17,26 @@
  * under the License.
  */
 
-#include "config.h"
-
-#include "rdpdr_messages.h"
-#include "rdpdr_printer.h"
-#include "rdpdr_service.h"
+#include "channels/rdpdr/rdpdr-printer.h"
+#include "channels/rdpdr/rdpdr.h"
+#include "print-job.h"
 #include "rdp.h"
-#include "rdp_print_job.h"
-#include "rdp_status.h"
 #include "unicode.h"
 
-#include <freerdp/utils/svc_plugin.h>
+#include <freerdp/channels/rdpdr.h>
+#include <freerdp/settings.h>
 #include <guacamole/client.h>
-#include <guacamole/protocol.h>
-#include <guacamole/socket.h>
-#include <guacamole/stream.h>
 #include <guacamole/unicode.h>
-#include <guacamole/user.h>
-
-#ifdef ENABLE_WINPR
+#include <winpr/nt.h>
 #include <winpr/stream.h>
-#else
-#include "compat/winpr-stream.h"
-#endif
 
-#include <errno.h>
-#include <pthread.h>
-#include <stdint.h>
 #include <stdlib.h>
-#include <string.h>
-#include <unistd.h>
 
-void guac_rdpdr_process_print_job_create(guac_rdpdr_device* device,
-        wStream* input_stream, int completion_id) {
+void guac_rdpdr_process_print_job_create(guac_rdp_common_svc* svc,
+        guac_rdpdr_device* device, guac_rdpdr_iorequest* iorequest,
+        wStream* input_stream) {
 
-    guac_client* client = device->rdpdr->client;
+    guac_client* client = svc->client;
     guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
 
     /* Log creation of print job */
@@ -63,17 +48,18 @@
 
     /* Respond with success */
     wStream* output_stream = guac_rdpdr_new_io_completion(device,
-            completion_id, STATUS_SUCCESS, 4);
+            iorequest->completion_id, STATUS_SUCCESS, 4);
 
     Stream_Write_UINT32(output_stream, 0); /* fileId */
-    svc_plugin_send((rdpSvcPlugin*) device->rdpdr, output_stream);
+    guac_rdp_common_svc_write(svc, output_stream);
 
 }
 
-void guac_rdpdr_process_print_job_write(guac_rdpdr_device* device,
-        wStream* input_stream, int completion_id) {
+void guac_rdpdr_process_print_job_write(guac_rdp_common_svc* svc,
+        guac_rdpdr_device* device, guac_rdpdr_iorequest* iorequest,
+        wStream* input_stream) {
 
-    guac_client* client = device->rdpdr->client;
+    guac_client* client = svc->client;
     guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
     guac_rdp_print_job* job = (guac_rdp_print_job*) rdp_client->active_job;
 
@@ -100,19 +86,20 @@
     }
 
     wStream* output_stream = guac_rdpdr_new_io_completion(device,
-            completion_id, status, 5);
+            iorequest->completion_id, status, 5);
 
     Stream_Write_UINT32(output_stream, length);
     Stream_Write_UINT8(output_stream, 0); /* Padding */
 
-    svc_plugin_send((rdpSvcPlugin*) device->rdpdr, output_stream);
+    guac_rdp_common_svc_write(svc, output_stream);
 
 }
 
-void guac_rdpdr_process_print_job_close(guac_rdpdr_device* device,
-        wStream* input_stream, int completion_id) {
+void guac_rdpdr_process_print_job_close(guac_rdp_common_svc* svc,
+        guac_rdpdr_device* device, guac_rdpdr_iorequest* iorequest,
+        wStream* input_stream) {
 
-    guac_client* client = device->rdpdr->client;
+    guac_client* client = svc->client;
     guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
     guac_rdp_print_job* job = (guac_rdp_print_job*) rdp_client->active_job;
 
@@ -123,61 +110,63 @@
     }
 
     wStream* output_stream = guac_rdpdr_new_io_completion(device,
-            completion_id, STATUS_SUCCESS, 4);
+            iorequest->completion_id, STATUS_SUCCESS, 4);
 
     Stream_Write_UINT32(output_stream, 0); /* Padding */
-    svc_plugin_send((rdpSvcPlugin*) device->rdpdr, output_stream);
+    guac_rdp_common_svc_write(svc, output_stream);
 
     /* Log end of print job */
     guac_client_log(client, GUAC_LOG_INFO, "Print job closed");
 
 }
 
-static void guac_rdpdr_device_printer_iorequest_handler(guac_rdpdr_device* device,
-        wStream* input_stream, int file_id, int completion_id, int major_func, int minor_func) {
+void guac_rdpdr_device_printer_iorequest_handler(guac_rdp_common_svc* svc,
+        guac_rdpdr_device* device, guac_rdpdr_iorequest* iorequest,
+        wStream* input_stream) {
 
-    switch (major_func) {
+    switch (iorequest->major_func) {
 
         /* Print job create */
         case IRP_MJ_CREATE:
-            guac_rdpdr_process_print_job_create(device, input_stream, completion_id);
+            guac_rdpdr_process_print_job_create(svc, device, iorequest, input_stream);
             break;
 
         /* Printer job write */
         case IRP_MJ_WRITE:
-            guac_rdpdr_process_print_job_write(device, input_stream, completion_id);
+            guac_rdpdr_process_print_job_write(svc, device, iorequest, input_stream);
             break;
 
         /* Printer job close */
         case IRP_MJ_CLOSE:
-            guac_rdpdr_process_print_job_close(device, input_stream, completion_id);
+            guac_rdpdr_process_print_job_close(svc, device, iorequest, input_stream);
             break;
 
         /* Log unknown */
         default:
-            guac_client_log(device->rdpdr->client, GUAC_LOG_ERROR,
-                    "Unknown printer I/O request function: 0x%x/0x%x",
-                    major_func, minor_func);
+            guac_client_log(svc->client, GUAC_LOG_ERROR, "Unknown printer "
+                    "I/O request function: 0x%x/0x%x", iorequest->major_func,
+                    iorequest->minor_func);
 
     }
 
 }
 
-static void guac_rdpdr_device_printer_free_handler(guac_rdpdr_device* device) {
+void guac_rdpdr_device_printer_free_handler(guac_rdp_common_svc* svc,
+        guac_rdpdr_device* device) {
 
     Stream_Free(device->device_announce, 1);
 
 }
 
-void guac_rdpdr_register_printer(guac_rdpdrPlugin* rdpdr, char* printer_name) {
+void guac_rdpdr_register_printer(guac_rdp_common_svc* svc, char* printer_name) {
 
+    guac_rdpdr* rdpdr = (guac_rdpdr*) svc->data;
     int id = rdpdr->devices_registered++;
 
     /* Get new device */
     guac_rdpdr_device* device = &(rdpdr->devices[id]);
 
     /* Init device */
-    device->rdpdr       = rdpdr;
     device->device_id   = id;
     device->device_name = printer_name;
     int device_name_len = guac_utf8_strlen(device->device_name);
diff --git a/src/protocols/rdp/channels/rdpdr/rdpdr-printer.h b/src/protocols/rdp/channels/rdpdr/rdpdr-printer.h
new file mode 100644
index 0000000..a6865d8
--- /dev/null
+++ b/src/protocols/rdp/channels/rdpdr/rdpdr-printer.h
@@ -0,0 +1,81 @@
+/*
+ * 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_RDPDR_PRINTER_H
+#define GUAC_RDP_CHANNELS_RDPDR_PRINTER_H
+
+#include "channels/common-svc.h"
+#include "channels/rdpdr/rdpdr.h"
+
+#include <winpr/stream.h>
+
+/**
+ * Name of the printer driver that should be used on the server.
+ */
+#define GUAC_PRINTER_DRIVER "M\0S\0 \0P\0u\0b\0l\0i\0s\0h\0e\0r\0 \0I\0m\0a\0g\0e\0s\0e\0t\0t\0e\0r\0\0\0"
+
+/**
+ * The size of GUAC_PRINTER_DRIVER in bytes.
+ */
+#define GUAC_PRINTER_DRIVER_LENGTH 50
+
+/**
+ * Registers a new printer device within the RDPDR plugin. This must be done
+ * before RDPDR connection finishes.
+ * 
+ * @param svc
+ *     The static virtual channel instance being used for RDPDR.
+ * 
+ * @param printer_name
+ *     The name of the printer that will be registered with the RDP
+ *     connection and passed through to the server.
+ */
+void guac_rdpdr_register_printer(guac_rdp_common_svc* svc, char* printer_name);
+
+/**
+ * I/O request handler which processes a print job creation request.
+ */
+guac_rdpdr_device_iorequest_handler guac_rdpdr_process_print_job_create;
+
+/**
+ * I/O request handler which processes a request to write data to an existing
+ * print job.
+ */
+guac_rdpdr_device_iorequest_handler guac_rdpdr_process_print_job_write;
+
+/**
+ * I/O request handler which processes a request to close an existing print
+ * job.
+ */
+guac_rdpdr_device_iorequest_handler guac_rdpdr_process_print_job_close;
+
+/**
+ * Handler for RDPDR Device I/O Requests which processes received messages on
+ * behalf of a printer device, in this case a simulated printer which produces
+ * PDF output.
+ */
+guac_rdpdr_device_iorequest_handler guac_rdpdr_device_printer_iorequest_handler;
+
+/**
+ * Free handler which frees all data specific to the simulated printer device.
+ */
+guac_rdpdr_device_free_handler guac_rdpdr_device_printer_free_handler;
+
+#endif
+
diff --git a/src/protocols/rdp/channels/rdpdr/rdpdr.c b/src/protocols/rdp/channels/rdpdr/rdpdr.c
new file mode 100644
index 0000000..e04bc9d
--- /dev/null
+++ b/src/protocols/rdp/channels/rdpdr/rdpdr.c
@@ -0,0 +1,184 @@
+/*
+ * 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/rdpdr/rdpdr.h"
+#include "channels/rdpdr/rdpdr-fs.h"
+#include "channels/rdpdr/rdpdr-messages.h"
+#include "channels/rdpdr/rdpdr-printer.h"
+#include "rdp.h"
+#include "settings.h"
+
+#include <freerdp/channels/rdpdr.h>
+#include <freerdp/freerdp.h>
+#include <freerdp/settings.h>
+#include <guacamole/client.h>
+#include <winpr/stream.h>
+
+#include <stdlib.h>
+
+void guac_rdpdr_process_receive(guac_rdp_common_svc* svc,
+        wStream* input_stream) {
+
+    int component;
+    int packet_id;
+
+    /* Read header */
+    Stream_Read_UINT16(input_stream, component);
+    Stream_Read_UINT16(input_stream, packet_id);
+
+    /* Core component */
+    if (component == RDPDR_CTYP_CORE) {
+
+        /* Dispatch handlers based on packet ID */
+        switch (packet_id) {
+
+            case PAKID_CORE_SERVER_ANNOUNCE:
+                guac_rdpdr_process_server_announce(svc, input_stream);
+                break;
+
+            case PAKID_CORE_CLIENTID_CONFIRM:
+                guac_rdpdr_process_clientid_confirm(svc, input_stream);
+                break;
+
+            case PAKID_CORE_DEVICE_REPLY:
+                guac_rdpdr_process_device_reply(svc, input_stream);
+                break;
+
+            case PAKID_CORE_DEVICE_IOREQUEST:
+                guac_rdpdr_process_device_iorequest(svc, input_stream);
+                break;
+
+            case PAKID_CORE_SERVER_CAPABILITY:
+                guac_rdpdr_process_server_capability(svc, input_stream);
+                break;
+
+            case PAKID_CORE_USER_LOGGEDON:
+                guac_rdpdr_process_user_loggedon(svc, input_stream);
+                break;
+
+            default:
+                guac_client_log(svc->client, GUAC_LOG_DEBUG, "Ignoring "
+                        "RDPDR core packet with unexpected ID: 0x%04x",
+                        packet_id);
+
+        }
+
+    } /* end if core */
+
+    /* Printer component */
+    else if (component == RDPDR_CTYP_PRN) {
+
+        /* Dispatch handlers based on packet ID */
+        switch (packet_id) {
+
+            case PAKID_PRN_CACHE_DATA:
+                guac_rdpdr_process_prn_cache_data(svc, input_stream);
+                break;
+
+            case PAKID_PRN_USING_XPS:
+                guac_rdpdr_process_prn_using_xps(svc, input_stream);
+                break;
+
+            default:
+                guac_client_log(svc->client, GUAC_LOG_DEBUG, "Ignoring RDPDR "
+                        "printer packet with unexpected ID: 0x%04x",
+                        packet_id);
+
+        }
+
+    } /* end if printer */
+
+    else
+        guac_client_log(svc->client, GUAC_LOG_DEBUG, "Ignoring packet for "
+                "unknown RDPDR component: 0x%04x", component);
+
+}
+
+wStream* guac_rdpdr_new_io_completion(guac_rdpdr_device* device,
+        int completion_id, int status, int size) {
+
+    wStream* output_stream = Stream_New(NULL, 16+size);
+
+    /* Write header */
+    Stream_Write_UINT16(output_stream, RDPDR_CTYP_CORE);
+    Stream_Write_UINT16(output_stream, PAKID_CORE_DEVICE_IOCOMPLETION);
+
+    /* Write content */
+    Stream_Write_UINT32(output_stream, device->device_id);
+    Stream_Write_UINT32(output_stream, completion_id);
+    Stream_Write_UINT32(output_stream, status);
+
+    return output_stream;
+
+}
+
+void guac_rdpdr_process_connect(guac_rdp_common_svc* svc) {
+
+    /* Get data from client */
+    guac_client* client = svc->client;
+    guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
+
+    guac_rdpdr* rdpdr = (guac_rdpdr*) calloc(1, sizeof(guac_rdpdr));
+    svc->data = rdpdr;
+
+    /* Register printer if enabled */
+    if (rdp_client->settings->printing_enabled)
+        guac_rdpdr_register_printer(svc, rdp_client->settings->printer_name);
+
+    /* Register drive if enabled */
+    if (rdp_client->settings->drive_enabled)
+        guac_rdpdr_register_fs(svc, rdp_client->settings->drive_name);
+
+}
+
+void guac_rdpdr_process_terminate(guac_rdp_common_svc* svc) {
+
+    guac_rdpdr* rdpdr = (guac_rdpdr*) svc->data;
+    if (rdpdr == NULL)
+        return;
+
+    int i;
+
+    for (i=0; i<rdpdr->devices_registered; i++) {
+        guac_rdpdr_device* device = &(rdpdr->devices[i]);
+        guac_client_log(svc->client, GUAC_LOG_DEBUG, "Unloading device %i "
+                "(%s)", device->device_id, device->device_name);
+        device->free_handler(svc, device);
+    }
+
+    free(rdpdr);
+
+}
+
+
+void guac_rdpdr_load_plugin(rdpContext* context) {
+
+    guac_client* client = ((rdp_freerdp_context*) context)->client;
+
+    /* Load support for RDPDR */
+    if (guac_rdp_common_svc_load_plugin(context, "rdpdr",
+                CHANNEL_OPTION_COMPRESS_RDP, guac_rdpdr_process_connect,
+                guac_rdpdr_process_receive, guac_rdpdr_process_terminate)) {
+        guac_client_log(client, GUAC_LOG_WARNING, "Support for the RDPDR "
+                "channel (device redirection) could not be loaded. Drive "
+                "redirection and printing will not work. Sound MAY not work.");
+    }
+
+}
+
diff --git a/src/protocols/rdp/channels/rdpdr/rdpdr.h b/src/protocols/rdp/channels/rdpdr/rdpdr.h
new file mode 100644
index 0000000..b6f9da3
--- /dev/null
+++ b/src/protocols/rdp/channels/rdpdr/rdpdr.h
@@ -0,0 +1,250 @@
+/*
+ * 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_RDPDR_H
+#define GUAC_RDP_CHANNELS_RDPDR_H
+
+#include "channels/common-svc.h"
+
+#include <freerdp/freerdp.h>
+#include <guacamole/client.h>
+#include <winpr/stream.h>
+
+#include <stdint.h>
+
+/**
+ * The maximum number of bytes to allow for a device read.
+ */
+#define GUAC_RDP_MAX_READ_BUFFER 4194304
+
+/**
+ * Arbitrary device forwarded over the RDPDR channel.
+ */
+typedef struct guac_rdpdr_device guac_rdpdr_device;
+
+/**
+ * The contents of the header common to all RDPDR Device I/O Requests. See:
+ *
+ * https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/a087ffa8-d0d5-4874-ac7b-0494f63e2d5d
+ */
+typedef struct guac_rdpdr_iorequest {
+
+    /**
+     * The unique ID assigned to the device receiving this I/O request.
+     */
+    int device_id;
+
+    /**
+     * The unique ID which identifies the relevant file, as returned when the
+     * file was opened. This field may not be relevant to all requests.
+     */
+    int file_id;
+
+    /**
+     * The unique ID that should be used to refer to this I/O request in future
+     * responses.
+     */
+    int completion_id;
+
+    /**
+     * Integer ID which identifies the function being requested, such as
+     * IRP_MJ_CREATE (open a file within a shared drive) or IRP_MJ_WRITE (write
+     * data to an open file).
+     */
+    int major_func;
+
+    /**
+     * Integer ID which identifies a variant of the function denoted by
+     * major_func. This value is only valid for IRP_MJ_DIRECTORY_CONTROL.
+     */
+    int minor_func;
+
+} guac_rdpdr_iorequest;
+
+/**
+ * Handler for Device I/O Requests. RDPDR devices must provide an
+ * implementation of this function to be able to handle inbound I/O requests.
+ *
+ * @param svc
+ *     The guac_rdp_common_svc representing the static virtual channel being
+ *     used for RDPDR.
+ *
+ * @param device
+ *     The guac_rdpdr_device of the relevant device, as dictated by the
+ *     deviceId field of the common RDPDR header within the received PDU.
+ *     Within the guac_rdpdr_iorequest structure, the deviceId field is stored
+ *     within device_id.
+ *
+ * @param iorequest
+ *     The contents of the common RDPDR Device I/O Request header shared by all
+ *     RDPDR devices.
+ *
+ * @param input_stream
+ *     The remaining data within the received PDU, following the common RDPDR
+ *     Device I/O Request header.
+ */
+typedef void guac_rdpdr_device_iorequest_handler(guac_rdp_common_svc* svc,
+        guac_rdpdr_device* device, guac_rdpdr_iorequest* iorequest,
+        wStream* input_stream);
+
+/**
+ * Handler for cleaning up the dynamically-allocated portions of a device.
+ *
+ * @param svc
+ *     The guac_rdp_common_svc representing the static virtual channel being
+ *     used for RDPDR.
+ *
+ * @param device
+ *     The guac_rdpdr_device of the device being freed.
+ */
+typedef void guac_rdpdr_device_free_handler(guac_rdp_common_svc* svc,
+        guac_rdpdr_device* device);
+
+struct guac_rdpdr_device {
+
+    /**
+     * The ID assigned to this device by the RDPDR plugin.
+     */
+    int device_id;
+
+    /**
+     * Device name, used for logging and for passthrough to the
+     * server.
+     */
+    const char* device_name;
+
+    /**
+     * The type of RDPDR device that this represents.
+     */
+    uint32_t device_type;
+
+    /**
+     * The DOS name of the device. Max 8 bytes, including terminator.
+     */
+    const char *dos_name;
+    
+    /**
+     * The stream that stores the RDPDR device announcement for this device.
+     */
+    wStream* device_announce;
+    
+    /**
+     * The length of the device_announce wStream.
+     */
+    int device_announce_len;
+
+    /**
+     * Handler which should be called for every I/O request received.
+     */
+    guac_rdpdr_device_iorequest_handler* iorequest_handler;
+
+    /**
+     * Handler which should be called when the device is being freed.
+     */
+    guac_rdpdr_device_free_handler* free_handler;
+
+    /**
+     * Arbitrary data, used internally by the handlers for this device.
+     */
+    void* data;
+
+};
+
+/**
+ * Structure representing the current state of the Guacamole RDPDR plugin for
+ * FreeRDP.
+ */
+typedef struct guac_rdpdr {
+
+    /**
+     * The number of devices registered within the devices array.
+     */
+    int devices_registered;
+
+    /**
+     * Array of registered devices.
+     */
+    guac_rdpdr_device devices[8];
+
+} guac_rdpdr;
+
+/**
+ * Creates a new stream which contains the common DR_DEVICE_IOCOMPLETION header
+ * used for virtually all responses. Depending on the specific I/O completion
+ * being sent, additional space may be reserved within the resulting stream for
+ * additional fields. See:
+ *
+ * https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/10ef9ada-cba2-4384-ab60-7b6290ed4a9a
+ *
+ * @param device
+ *     The device that completed the operation requested by a prior I/O
+ *     request.
+ *
+ * @param completion_id
+ *     The completion ID of the I/O request that requested the operation.
+ *
+ * @param status
+ *     An NTSTATUS code describing the success/failure of the operation that
+ *     was completed.
+ *
+ * @param size
+ *     The number of additional bytes to reserve at the end of the resulting
+ *     stream for additional fields to be appended.
+ *
+ * @return
+ *     A new wStream containing an I/O completion header, followed by the
+ *     requested additional free space.
+ */
+wStream* guac_rdpdr_new_io_completion(guac_rdpdr_device* device,
+        int completion_id, int status, int size);
+
+/**
+ * Initializes device redirection support (file transfer, printing, etc.) for
+ * RDP and handling of the RDPDR channel. If failures occur, messages noting
+ * the specifics of those failures will be logged, and the RDP side of
+ * device redirection support will not be functional.
+ *
+ * This MUST be called within the PreConnect callback of the freerdp instance
+ * for RDPDR support to be loaded.
+ *
+ * @param context
+ *     The rdpContext associated with the FreeRDP side of the RDP connection.
+ */
+void guac_rdpdr_load_plugin(rdpContext* context);
+
+/**
+ * Handler which is invoked when the RDPDR channel is connected to the RDP
+ * server.
+ */
+guac_rdp_common_svc_connect_handler guac_rdpdr_process_connect;
+
+/**
+ * Handler which is invoked when the RDPDR channel has received data from the
+ * RDP server.
+ */
+guac_rdp_common_svc_receive_handler guac_rdpdr_process_receive;
+
+/**
+ * Handler which is invoked when the RDPDR channel has disconnected and is
+ * about to be freed.
+ */
+guac_rdp_common_svc_terminate_handler guac_rdpdr_process_terminate;
+
+#endif
+
diff --git a/src/protocols/rdp/guac_rdpsnd/rdpsnd_messages.c b/src/protocols/rdp/channels/rdpsnd/rdpsnd-messages.c
similarity index 84%
rename from src/protocols/rdp/guac_rdpsnd/rdpsnd_messages.c
rename to src/protocols/rdp/channels/rdpsnd/rdpsnd-messages.c
index ba0224c..1d69e06 100644
--- a/src/protocols/rdp/guac_rdpsnd/rdpsnd_messages.c
+++ b/src/protocols/rdp/channels/rdpsnd/rdpsnd-messages.c
@@ -17,30 +17,20 @@
  * under the License.
  */
 
-#include "config.h"
-
+#include "channels/rdpsnd/rdpsnd-messages.h"
+#include "channels/rdpsnd/rdpsnd.h"
 #include "rdp.h"
-#include "rdpsnd_messages.h"
-#include "rdpsnd_service.h"
 
-#include <pthread.h>
-#include <stdlib.h>
-
-#include <freerdp/utils/svc_plugin.h>
+#include <freerdp/codec/audio.h>
 #include <guacamole/audio.h>
 #include <guacamole/client.h>
-
-#ifdef ENABLE_WINPR
 #include <winpr/stream.h>
 #include <winpr/wtypes.h>
-#else
-#include "compat/winpr-stream.h"
-#include "compat/winpr-wtypes.h"
-#endif
 
-/* MESSAGE HANDLERS */
+#include <stdlib.h>
+#include <string.h>
 
-void guac_rdpsnd_formats_handler(guac_rdpsndPlugin* rdpsnd,
+void guac_rdpsnd_formats_handler(guac_rdp_common_svc* svc,
         wStream* input_stream, guac_rdpsnd_pdu_header* header) {
 
     int server_format_count;
@@ -51,11 +41,10 @@
     int output_body_size;
     unsigned char* output_stream_end;
 
-    /* Get associated client data */
-    guac_client* client = rdpsnd->client;
-    guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
+    guac_client* client = svc->client;
+    guac_rdpsnd* rdpsnd = (guac_rdpsnd*) svc->data;
 
-    /* Get audio stream from client data */
+    guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
     guac_audio_stream* audio = rdp_client->audio;
 
     /* Reset own format count */
@@ -188,8 +177,7 @@
     Stream_SetPointer(output_stream, output_stream_end);
 
     /* Send accepted formats */
-    pthread_mutex_lock(&(rdp_client->rdp_lock));
-    svc_plugin_send((rdpSvcPlugin*)rdpsnd, output_stream);
+    guac_rdp_common_svc_write(svc, output_stream);
 
     /* If version greater than 6, must send Quality Mode PDU */
     if (server_version >= 6) {
@@ -202,23 +190,20 @@
         Stream_Write_UINT16(output_stream, HIGH_QUALITY);
         Stream_Write_UINT16(output_stream, 0);
 
-        svc_plugin_send((rdpSvcPlugin*)rdpsnd, output_stream);
-    }
+        guac_rdp_common_svc_write(svc, output_stream);
 
-    pthread_mutex_unlock(&(rdp_client->rdp_lock));
+    }
 
 }
 
 /* server is getting a feel of the round trip time */
-void guac_rdpsnd_training_handler(guac_rdpsndPlugin* rdpsnd,
+void guac_rdpsnd_training_handler(guac_rdp_common_svc* svc,
         wStream* input_stream, guac_rdpsnd_pdu_header* header) {
 
     int data_size;
     wStream* output_stream;
 
-    /* Get associated client data */
-    guac_client* client = rdpsnd->client;
-    guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
+    guac_rdpsnd* rdpsnd = (guac_rdpsnd*) svc->data;
 
     /* Read timestamp and data size */
     Stream_Read_UINT16(input_stream, rdpsnd->server_timestamp);
@@ -232,22 +217,19 @@
     Stream_Write_UINT16(output_stream, rdpsnd->server_timestamp);
     Stream_Write_UINT16(output_stream, data_size);
 
-    pthread_mutex_lock(&(rdp_client->rdp_lock));
-    svc_plugin_send((rdpSvcPlugin*) rdpsnd, output_stream);
-    pthread_mutex_unlock(&(rdp_client->rdp_lock));
+    guac_rdp_common_svc_write(svc, output_stream);
 
 }
 
-void guac_rdpsnd_wave_info_handler(guac_rdpsndPlugin* rdpsnd,
+void guac_rdpsnd_wave_info_handler(guac_rdp_common_svc* svc,
         wStream* input_stream, guac_rdpsnd_pdu_header* header) {
 
     int format;
 
-    /* Get associated client data */
-    guac_client* client = rdpsnd->client;
-    guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
+    guac_client* client = svc->client;
+    guac_rdpsnd* rdpsnd = (guac_rdpsnd*) svc->data;
 
-    /* Get audio stream from client data */
+    guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
     guac_audio_stream* audio = rdp_client->audio;
 
     /* Read wave information */
@@ -276,16 +258,13 @@
 
 }
 
-void guac_rdpsnd_wave_handler(guac_rdpsndPlugin* rdpsnd,
+void guac_rdpsnd_wave_handler(guac_rdp_common_svc* svc,
         wStream* input_stream, guac_rdpsnd_pdu_header* header) {
 
-    rdpSvcPlugin* plugin = (rdpSvcPlugin*)rdpsnd;
+    guac_client* client = svc->client;
+    guac_rdpsnd* rdpsnd = (guac_rdpsnd*) svc->data;
 
-    /* Get associated client data */
-    guac_client* client = rdpsnd->client;
     guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
-
-    /* Get audio stream from client data */
     guac_audio_stream* audio = rdp_client->audio;
 
     /* Wave Confirmation PDU */
@@ -313,16 +292,14 @@
     Stream_Write_UINT8(output_stream, 0);
 
     /* Send Wave Confirmation PDU */
-    pthread_mutex_lock(&(rdp_client->rdp_lock));
-    svc_plugin_send(plugin, output_stream);
-    pthread_mutex_unlock(&(rdp_client->rdp_lock));
+    guac_rdp_common_svc_write(svc, output_stream);
 
     /* We no longer expect to receive wave data */
     rdpsnd->next_pdu_is_wave = FALSE;
 
 }
 
-void guac_rdpsnd_close_handler(guac_rdpsndPlugin* rdpsnd,
+void guac_rdpsnd_close_handler(guac_rdp_common_svc* svc,
         wStream* input_stream, guac_rdpsnd_pdu_header* header) {
 
     /* Do nothing */
diff --git a/src/protocols/rdp/guac_rdpsnd/rdpsnd_messages.h b/src/protocols/rdp/channels/rdpsnd/rdpsnd-messages.h
similarity index 64%
rename from src/protocols/rdp/guac_rdpsnd/rdpsnd_messages.h
rename to src/protocols/rdp/channels/rdpsnd/rdpsnd-messages.h
index 7083b84..9271151 100644
--- a/src/protocols/rdp/guac_rdpsnd/rdpsnd_messages.h
+++ b/src/protocols/rdp/channels/rdpsnd/rdpsnd-messages.h
@@ -17,87 +17,12 @@
  * under the License.
  */
 
+#ifndef GUAC_RDP_CHANNELS_RDPSND_MESSAGES_H
+#define GUAC_RDP_CHANNELS_RDPSND_MESSAGES_H
 
-#ifndef __GUAC_RDPSND_MESSAGES_H
-#define __GUAC_RDPSND_MESSAGES_H
+#include "channels/common-svc.h"
 
-#include "config.h"
-
-#include "rdpsnd_service.h"
-
-#ifdef ENABLE_WINPR
 #include <winpr/stream.h>
-#else
-#include "compat/winpr-stream.h"
-#endif
-
-/*
- * PDU Message Types
- */
-
-/**
- * Close PDU
- */
-#define SNDC_CLOSE         1
-
-/**
- * WaveInfo PDU. This PDU is sent just before wave data is sent.
- */
-#define SNDC_WAVE          2
-
-/**
- * Wave Confirm PDU. This PDU is sent in response to the WaveInfo PDU,
- * confirming it has been received and played.
- */
-#define SNDC_WAVECONFIRM   5
-
-/**
- * Training PDU. This PDU is sent by the server occasionally and must be
- * responded to with another training PDU, similar to Guac's sync message.
- */
-#define SNDC_TRAINING      6
-
-/**
- * Server Audio Formats and Version PDU. This PDU is sent by the server to
- * advertise to the client which audio formats are supported.
- */
-#define SNDC_FORMATS       7
-
-/**
- * Quality Mode PDU. This PDU must be sent by the client to select an audio
- * quality mode if the server is at least version 6.
- */
-#define SNDC_QUALITYMODE   12
-
-/*
- * Quality Modes
- */
-
-/**
- * Dynamic Quality. The server will choose the audio quality based on its
- * perception of latency.
- */
-#define DYNAMIC_QUALITY    0x0000
-
-/**
- * Medium Quality. The server prioritizes bandwidth over quality.
- */
-#define MEDIUM_QUALITY     0x0001
-
-/**
- * High Quality. The server prioritizes quality over bandwidth.
- */
-#define HIGH_QUALITY       0x0002
-
-/*
- * Capabilities
- */
-#define TSSNDCAPS_ALIVE  1
-
-/*
- * Sound Formats
- */
-#define WAVE_FORMAT_PCM  1
 
 /**
  * The header common to all RDPSND PDUs.
@@ -121,8 +46,8 @@
  * SNDC_FORMATS PDU describes all audio formats supported by the RDP server, as
  * well as the version of RDPSND implemented.
  *
- * @param rdpsnd
- *     The Guacamole RDPSND plugin receiving the SNDC_FORMATS PDU.
+ * @param svc
+ *     The RDPSND channel receiving the SNDC_FORMATS PDU.
  *
  * @param input_stream
  *     The FreeRDP input stream containing the remaining raw bytes (after the
@@ -132,7 +57,7 @@
  *     The header content of the SNDC_FORMATS PDU. All RDPSND messages contain
  *     the same header information.
  */
-void guac_rdpsnd_formats_handler(guac_rdpsndPlugin* rdpsnd,
+void guac_rdpsnd_formats_handler(guac_rdp_common_svc* svc,
         wStream* input_stream, guac_rdpsnd_pdu_header* header);
 
 /**
@@ -142,8 +67,8 @@
  *
  * https://msdn.microsoft.com/en-us/library/cc240961.aspx
  *
- * @param rdpsnd
- *     The Guacamole RDPSND plugin receiving the SNDC_TRAINING PDU.
+ * @param svc
+ *     The RDPSND channel receiving the SNDC_TRAINING PDU.
  *
  * @param input_stream
  *     The FreeRDP input stream containing the remaining raw bytes (after the
@@ -153,7 +78,7 @@
  *     The header content of the SNDC_TRAINING PDU. All RDPSND messages contain
  *     the same header information.
  */
-void guac_rdpsnd_training_handler(guac_rdpsndPlugin* rdpsnd,
+void guac_rdpsnd_training_handler(guac_rdp_common_svc* svc,
         wStream* input_stream, guac_rdpsnd_pdu_header* header);
 
 /**
@@ -165,8 +90,8 @@
  *
  * https://msdn.microsoft.com/en-us/library/cc240963.aspx
  *
- * @param rdpsnd
- *     The Guacamole RDPSND plugin receiving the SNDC_WAVE PDU.
+ * @param svc
+ *     The RDPSND channel receiving the SNDC_WAVE PDU.
  *
  * @param input_stream
  *     The FreeRDP input stream containing the remaining raw bytes (after the
@@ -176,7 +101,7 @@
  *     The header content of the SNDC_WAVE PDU. All RDPSND messages contain
  *     the same header information.
  */
-void guac_rdpsnd_wave_info_handler(guac_rdpsndPlugin* rdpsnd,
+void guac_rdpsnd_wave_info_handler(guac_rdp_common_svc* svc,
         wStream* input_stream, guac_rdpsnd_pdu_header* header);
 
 /**
@@ -184,8 +109,8 @@
  * PDU contains the actual audio data, less the four bytes of audio data
  * included in the SNDC_WAVE PDU.
  *
- * @param rdpsnd
- *     The Guacamole RDPSND plugin receiving the SNDWAV PDU.
+ * @param svc
+ *     The RDPSND channel receiving the SNDWAV PDU.
  *
  * @param input_stream
  *     The FreeRDP input stream containing the remaining raw bytes (after the
@@ -195,7 +120,7 @@
  *     The header content of the SNDWAV PDU. All RDPSND messages contain
  *     the same header information.
  */
-void guac_rdpsnd_wave_handler(guac_rdpsndPlugin* rdpsnd,
+void guac_rdpsnd_wave_handler(guac_rdp_common_svc* svc,
         wStream* input_stream, guac_rdpsnd_pdu_header* header);
 
 /**
@@ -204,8 +129,8 @@
  *
  * https://msdn.microsoft.com/en-us/library/cc240970.aspx
  *
- * @param rdpsnd
- *     The Guacamole RDPSND plugin receiving the SNDC_CLOSE PDU.
+ * @param svc
+ *     The RDPSND channel receiving the SNDC_CLOSE PDU.
  *
  * @param input_stream
  *     The FreeRDP input stream containing the remaining raw bytes (after the
@@ -215,7 +140,7 @@
  *     The header content of the SNDC_CLOSE PDU. All RDPSND messages contain
  *     the same header information.
  */
-void guac_rdpsnd_close_handler(guac_rdpsndPlugin* rdpsnd,
+void guac_rdpsnd_close_handler(guac_rdp_common_svc* svc,
         wStream* input_stream, guac_rdpsnd_pdu_header* header);
 
 #endif
diff --git a/src/protocols/rdp/channels/rdpsnd/rdpsnd.c b/src/protocols/rdp/channels/rdpsnd/rdpsnd.c
new file mode 100644
index 0000000..be6034d
--- /dev/null
+++ b/src/protocols/rdp/channels/rdpsnd/rdpsnd.c
@@ -0,0 +1,105 @@
+/*
+ * 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/common-svc.h"
+#include "channels/rdpsnd/rdpsnd.h"
+#include "channels/rdpsnd/rdpsnd-messages.h"
+#include "rdp.h"
+
+#include <freerdp/codec/audio.h>
+#include <freerdp/freerdp.h>
+#include <guacamole/client.h>
+#include <winpr/stream.h>
+
+#include <stdlib.h>
+
+void guac_rdpsnd_process_receive(guac_rdp_common_svc* svc,
+        wStream* input_stream) {
+
+    guac_rdpsnd* rdpsnd = (guac_rdpsnd*) svc->data;
+    guac_rdpsnd_pdu_header header;
+
+    /* Read RDPSND PDU header */
+    Stream_Read_UINT8(input_stream, header.message_type);
+    Stream_Seek_UINT8(input_stream);
+    Stream_Read_UINT16(input_stream, header.body_size);
+
+    /* 
+     * If next PDU is SNDWAVE (due to receiving WaveInfo PDU previously),
+     * ignore the header and parse as a Wave PDU.
+     */
+    if (rdpsnd->next_pdu_is_wave) {
+        guac_rdpsnd_wave_handler(svc, input_stream, &header);
+        return;
+    }
+
+    /* Dispatch message to standard handlers */
+    switch (header.message_type) {
+
+        /* Server Audio Formats and Version PDU */
+        case SNDC_FORMATS:
+            guac_rdpsnd_formats_handler(svc, input_stream, &header);
+            break;
+
+        /* Training PDU */
+        case SNDC_TRAINING:
+            guac_rdpsnd_training_handler(svc, input_stream, &header);
+            break;
+
+        /* WaveInfo PDU */
+        case SNDC_WAVE:
+            guac_rdpsnd_wave_info_handler(svc, input_stream, &header);
+            break;
+
+        /* Close PDU */
+        case SNDC_CLOSE:
+            guac_rdpsnd_close_handler(svc, input_stream, &header);
+            break;
+
+    }
+
+}
+
+void guac_rdpsnd_process_connect(guac_rdp_common_svc* svc) {
+
+    guac_rdpsnd* rdpsnd = (guac_rdpsnd*) calloc(1, sizeof(guac_rdpsnd));
+    svc->data = rdpsnd;
+
+}
+
+void guac_rdpsnd_process_terminate(guac_rdp_common_svc* svc) {
+    guac_rdpsnd* rdpsnd = (guac_rdpsnd*) svc->data;
+    free(rdpsnd);
+}
+
+void guac_rdpsnd_load_plugin(rdpContext* context) {
+
+    guac_client* client = ((rdp_freerdp_context*) context)->client;
+
+    /* Load support for RDPSND */
+    if (guac_rdp_common_svc_load_plugin(context, "rdpsnd", 0,
+                guac_rdpsnd_process_connect, guac_rdpsnd_process_receive,
+                guac_rdpsnd_process_terminate)) {
+        guac_client_log(client, GUAC_LOG_WARNING, "Support for the RDPSND "
+                "channel (audio output) could not be loaded. Sound will not "
+                "work. Drive redirection and printing MAY not work.");
+    }
+
+}
+
diff --git a/src/protocols/rdp/guac_rdpsnd/rdpsnd_service.h b/src/protocols/rdp/channels/rdpsnd/rdpsnd.h
similarity index 66%
rename from src/protocols/rdp/guac_rdpsnd/rdpsnd_service.h
rename to src/protocols/rdp/channels/rdpsnd/rdpsnd.h
index b6a9396..cd7fe23 100644
--- a/src/protocols/rdp/guac_rdpsnd/rdpsnd_service.h
+++ b/src/protocols/rdp/channels/rdpsnd/rdpsnd.h
@@ -17,21 +17,14 @@
  * under the License.
  */
 
+#ifndef GUAC_RDP_CHANNELS_RDPSND_H
+#define GUAC_RDP_CHANNELS_RDPSND_H
 
-#ifndef __GUAC_RDPSND_SERVICE_H
-#define __GUAC_RDPSND_SERVICE_H
+#include "channels/common-svc.h"
 
-#include "config.h"
-
-#include <freerdp/utils/svc_plugin.h>
+#include <freerdp/freerdp.h>
 #include <guacamole/client.h>
 
-#ifdef ENABLE_WINPR
-#include <winpr/stream.h>
-#else
-#include "compat/winpr-stream.h"
-#endif
-
 /**
  * The maximum number of PCM formats to accept during the initial RDPSND
  * handshake with the RDP server.
@@ -42,7 +35,7 @@
  * Abstract representation of a PCM format, including the sample rate, number
  * of channels, and bits per sample.
  */
-typedef struct guac_pcm_format {
+typedef struct guac_rdpsnd_pcm_format {
 
     /**
      * The sample rate of this PCM format.
@@ -61,26 +54,13 @@
      */
     int bps;
 
-} guac_pcm_format;
+} guac_rdpsnd_pcm_format;
 
 /**
  * Structure representing the current state of the Guacamole RDPSND plugin for
  * FreeRDP.
  */
-typedef struct guac_rdpsndPlugin {
-
-    /**
-     * The FreeRDP parts of this plugin. This absolutely MUST be first.
-     * FreeRDP depends on accessing this structure as if it were an instance
-     * of rdpSvcPlugin.
-     */
-    rdpSvcPlugin plugin;
-
-    /**
-     * The Guacamole client associated with the guac_audio_stream that this
-     * plugin should use to stream received audio packets.
-     */
-    guac_client* client;
+typedef struct guac_rdpsnd {
 
     /**
      * The block number of the last SNDC_WAVE (WaveInfo) PDU received.
@@ -116,36 +96,45 @@
      * exchange. All of these formats will be PCM, which is the only format
      * guaranteed to be supported (based on the official RDP documentation).
      */
-    guac_pcm_format formats[GUAC_RDP_MAX_FORMATS];
+    guac_rdpsnd_pcm_format formats[GUAC_RDP_MAX_FORMATS];
 
     /**
      * The total number of formats.
      */
     int format_count;
 
-} guac_rdpsndPlugin;
+} guac_rdpsnd;
 
 /**
- * Handler called when this plugin is loaded by FreeRDP.
+ * Initializes audio output support for RDP and handling of the RDPSND channel.
+ * If failures occur, messages noting the specifics of those failures will be
+ * logged, and the RDP side of audio output support will not be functional.
+ *
+ * This MUST be called within the PreConnect callback of the freerdp instance
+ * for RDPSND support to be loaded.
+ *
+ * @param context
+ *     The rdpContext associated with the FreeRDP side of the RDP connection.
  */
-void guac_rdpsnd_process_connect(rdpSvcPlugin* plugin);
+void guac_rdpsnd_load_plugin(rdpContext* context);
 
 /**
- * Handler called when this plugin receives data along its designated channel.
+ * Handler which is invoked when the RDPSND channel is connected to the RDP
+ * server.
  */
-void guac_rdpsnd_process_receive(rdpSvcPlugin* plugin,
-        wStream* input_stream);
+guac_rdp_common_svc_connect_handler guac_rdpsnd_process_connect;
 
 /**
- * Handler called when this plugin is being unloaded.
+ * Handler which is invoked when the RDPSND channel has received data from the
+ * RDP server.
  */
-void guac_rdpsnd_process_terminate(rdpSvcPlugin* plugin);
+guac_rdp_common_svc_receive_handler guac_rdpsnd_process_receive;
 
 /**
- * Handler called when this plugin receives an event. For the sake of RDPSND,
- * all events will be ignored and simply free'd.
+ * Handler which is invoked when the RDPSND channel has disconnected and is
+ * about to be freed.
  */
-void guac_rdpsnd_process_event(rdpSvcPlugin* plugin, wMessage* event);
+guac_rdp_common_svc_terminate_handler guac_rdpsnd_process_terminate;
 
 #endif
 
diff --git a/src/protocols/rdp/client.c b/src/protocols/rdp/client.c
index 6b9a0a6..c79e59d 100644
--- a/src/protocols/rdp/client.c
+++ b/src/protocols/rdp/client.c
@@ -17,14 +17,16 @@
  * under the License.
  */
 
-#include "config.h"
-
-#include "audio_input.h"
-#include "common/recording.h"
 #include "client.h"
+#include "channels/audio-input/audio-buffer.h"
+#include "channels/cliprdr.h"
+#include "channels/disp.h"
+#include "common/recording.h"
+#include "config.h"
+#include "fs.h"
+#include "log.h"
 #include "rdp.h"
-#include "rdp_disp.h"
-#include "rdp_fs.h"
+#include "settings.h"
 #include "user.h"
 
 #ifdef ENABLE_COMMON_SSH
@@ -33,26 +35,11 @@
 #include "common-ssh/user.h"
 #endif
 
-#include <freerdp/cache/cache.h>
-#include <freerdp/channels/channels.h>
-#include <freerdp/freerdp.h>
 #include <guacamole/audio.h>
 #include <guacamole/client.h>
-#include <guacamole/socket.h>
-
-#ifdef HAVE_FREERDP_CLIENT_CLIPRDR_H
-#include <freerdp/client/cliprdr.h>
-#else
-#include "compat/client-cliprdr.h"
-#endif
-
-#ifdef HAVE_FREERDP_CLIENT_CHANNELS_H
-#include <freerdp/client/channels.h>
-#endif
 
 #include <pthread.h>
 #include <stdlib.h>
-#include <string.h>
 
 int guac_client_init(guac_client* client, int argc, char** argv) {
 
@@ -64,19 +51,19 @@
     client->data = rdp_client;
 
     /* Init clipboard */
-    rdp_client->clipboard = guac_common_clipboard_alloc(GUAC_RDP_CLIPBOARD_MAX_LENGTH);
+    rdp_client->clipboard = guac_rdp_clipboard_alloc(client);
 
     /* Init display update module */
     rdp_client->disp = guac_rdp_disp_alloc();
 
+    /* Redirect FreeRDP log messages to guac_client_log() */
+    guac_rdp_redirect_wlog(client);
+
     /* Recursive attribute for locks */
     pthread_mutexattr_init(&(rdp_client->attributes));
     pthread_mutexattr_settype(&(rdp_client->attributes),
             PTHREAD_MUTEX_RECURSIVE);
 
-    /* Init RDP lock */
-    pthread_mutex_init(&(rdp_client->rdp_lock), &(rdp_client->attributes));
-
     /* Set handlers */
     client->join_handler = guac_rdp_user_join_handler;
     client->free_handler = guac_rdp_client_free_handler;
@@ -100,6 +87,9 @@
     if (rdp_client->settings != NULL)
         guac_rdp_settings_free(rdp_client->settings);
 
+    /* Clean up clipboard */
+    guac_rdp_clipboard_free(rdp_client->clipboard);
+
     /* Free display update module */
     guac_rdp_disp_free(rdp_client->disp);
 
@@ -136,7 +126,6 @@
         guac_rdp_audio_buffer_free(rdp_client->audio_input);
 
     /* Free client data */
-    guac_common_clipboard_free(rdp_client->clipboard);
     free(rdp_client);
 
     return 0;
diff --git a/src/protocols/rdp/client.h b/src/protocols/rdp/client.h
index 4f6c266..9acd33a 100644
--- a/src/protocols/rdp/client.h
+++ b/src/protocols/rdp/client.h
@@ -20,8 +20,6 @@
 #ifndef GUAC_RDP_CLIENT_H
 #define GUAC_RDP_CLIENT_H
 
-#include "config.h"
-
 #include <guacamole/client.h>
 
 /**
diff --git a/src/protocols/rdp/color.c b/src/protocols/rdp/color.c
new file mode 100644
index 0000000..964310a
--- /dev/null
+++ b/src/protocols/rdp/color.c
@@ -0,0 +1,76 @@
+/*
+ * 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 <freerdp/codec/color.h>
+#include <freerdp/freerdp.h>
+#include <freerdp/gdi/gdi.h>
+#include <winpr/wtypes.h>
+
+#include <stdint.h>
+#include <string.h>
+
+UINT32 guac_rdp_get_native_pixel_format(BOOL alpha) {
+
+    uint32_t int_value;
+    uint8_t raw_bytes[4] = { 0x0A, 0x0B, 0x0C, 0x0D };
+
+    memcpy(&int_value, raw_bytes, sizeof(raw_bytes));
+
+    /* Local platform stores bytes in decreasing order of significance
+     * (big-endian) */
+    if (int_value == 0x0A0B0C0D)
+        return alpha ? PIXEL_FORMAT_ARGB32 : PIXEL_FORMAT_XRGB32;
+
+    /* Local platform stores bytes in increasing order of significance
+     * (little-endian) */
+    else
+        return alpha ? PIXEL_FORMAT_BGRA32 : PIXEL_FORMAT_BGRX32;
+
+}
+
+UINT32 guac_rdp_convert_color(rdpContext* context, UINT32 color) {
+
+    int depth = guac_rdp_get_depth(context->instance);
+    int src_format = gdi_get_pixel_format(depth);
+    int dst_format = guac_rdp_get_native_pixel_format(TRUE);
+    rdpGdi* gdi = context->gdi;
+
+    /* Convert provided color into the intermediate representation expected by
+     * FreeRDPConvertColor() */
+    UINT32 intermed = ReadColor((BYTE*) &color, src_format);
+
+    /* Convert color from RDP source format to the native format used by Cairo,
+     * still maintaining intermediate representation */
+#ifdef HAVE_FREERDPCONVERTCOLOR
+    intermed = FreeRDPConvertColor(intermed, src_format, dst_format,
+            &gdi->palette);
+#else
+    intermed = ConvertColor(intermed, src_format, dst_format, &gdi->palette);
+#endif
+
+    /* Convert color from intermediate representation to the actual desired
+     * format */
+    WriteColor((BYTE*) &color, dst_format, intermed);
+    return color;
+
+}
+
diff --git a/src/protocols/rdp/rdp_color.h b/src/protocols/rdp/color.h
similarity index 61%
rename from src/protocols/rdp/rdp_color.h
rename to src/protocols/rdp/color.h
index 40712ba..43910a5 100644
--- a/src/protocols/rdp/rdp_color.h
+++ b/src/protocols/rdp/color.h
@@ -21,12 +21,27 @@
 #define GUAC_RDP_COLOR_H
 
 #include <freerdp/freerdp.h>
-
-#ifdef ENABLE_WINPR
 #include <winpr/wtypes.h>
-#else
-#include "compat/winpr-wtypes.h"
-#endif
+
+/**
+ * Returns the FreeRDP pixel format ID corresponding to the 32-bit RGB format
+ * used by the Cairo library's image surfaces. Cairo handles colors in terms of
+ * integers in native endianness, with CAIRO_FORMAT_ARGB32 representing a color
+ * format where the alpha channel is stored in the most significant byte,
+ * followed by red, green, and blue. FreeRDP handles colors in terms of
+ * absolute byte order, with PIXEL_FORMAT_ARGB32 representing a color format
+ * where the alpha channel is in byte 0, followed by red at byte 1, etc.
+ *
+ * @param alpha
+ *     TRUE if the returned FreeRDP pixel format should correspond to Cairo's
+ *     CAIRO_FORMAT_ARGB32, FALSE if the returned format should correspond to
+ *     Cairo's CAIRO_FORMAT_RGB24.
+ *
+ * @return
+ *     The FreeRDP pixel format ID that corresponds to the 32-bit RGB format
+ *     used by the Cairo library.
+ */
+UINT32 guac_rdp_get_native_pixel_format(BOOL alpha);
 
 /**
  * Converts the given color to ARGB32. The color given may be an index
diff --git a/src/protocols/rdp/compat/client-cliprdr.h b/src/protocols/rdp/compat/client-cliprdr.h
deleted file mode 100644
index f1e2aab..0000000
--- a/src/protocols/rdp/compat/client-cliprdr.h
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * 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_CLIENT_CLIPRDR_COMPAT_H
-#define __GUAC_CLIENT_CLIPRDR_COMPAT_H
-
-#include "config.h"
-
-#include <freerdp/plugins/cliprdr.h>
-
-#define CliprdrChannel_Class        RDP_EVENT_CLASS_CLIPRDR
-#define CliprdrChannel_FormatList   RDP_EVENT_TYPE_CB_FORMAT_LIST
-#define CliprdrChannel_MonitorReady RDP_EVENT_TYPE_CB_MONITOR_READY
-#define CliprdrChannel_DataRequest  RDP_EVENT_TYPE_CB_DATA_REQUEST
-#define CliprdrChannel_DataResponse RDP_EVENT_TYPE_CB_DATA_RESPONSE
-
-#endif
-
diff --git a/src/protocols/rdp/compat/rail.h b/src/protocols/rdp/compat/rail.h
deleted file mode 100644
index 32d75b4..0000000
--- a/src/protocols/rdp/compat/rail.h
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * 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_RAIL_COMPAT_H
-#define __GUAC_RAIL_COMPAT_H
-
-#include "config.h"
-
-#include <freerdp/rail.h>
-
-#define RailChannel_Class                  RDP_EVENT_CLASS_RAIL
-#define RailChannel_ClientSystemParam      RDP_EVENT_TYPE_RAIL_CLIENT_SET_SYSPARAMS
-#define RailChannel_GetSystemParam         RDP_EVENT_TYPE_RAIL_CHANNEL_GET_SYSPARAMS
-#define RailChannel_ServerExecuteResult    RDP_EVENT_TYPE_RAIL_CHANNEL_EXEC_RESULTS
-#define RailChannel_ServerSystemParam      RDP_EVENT_TYPE_RAIL_CHANNEL_SERVER_SYSPARAM
-#define RailChannel_ServerMinMaxInfo       RDP_EVENT_TYPE_RAIL_CHANNEL_SERVER_MINMAXINFO
-#define RailChannel_ServerLocalMoveSize    RDP_EVENT_TYPE_RAIL_CHANNEL_SERVER_LOCALMOVESIZE
-#define RailChannel_ServerGetAppIdResponse RDP_EVENT_TYPE_RAIL_CHANNEL_APPID_RESP
-#define RailChannel_ServerLanguageBarInfo  RDP_EVENT_TYPE_RAIL_CHANNEL_LANGBARINFO
-
-#endif
-
diff --git a/src/protocols/rdp/compat/winpr-stream.c b/src/protocols/rdp/compat/winpr-stream.c
deleted file mode 100644
index 8be4546..0000000
--- a/src/protocols/rdp/compat/winpr-stream.c
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * 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 "winpr-stream.h"
-#include "winpr-wtypes.h"
-
-wStream* Stream_New(BYTE* buffer, size_t size) {
-
-    /* If no buffer is provided, allocate a new stream of the given size */
-    if (buffer == NULL)
-       return stream_new(size);
-
-    /* Otherwise allocate an empty stream and assign the given buffer */
-    wStream* stream = stream_new(0);
-    stream_attach(stream, buffer, size);
-    return stream;
-
-}
-
-void Stream_Free(wStream* s, BOOL bFreeBuffer) {
-
-    /* Disassociate buffer if it will be freed externally */
-    if (!bFreeBuffer)
-        stream_detach(s);
-
-    stream_free(s);
-
-}
-
diff --git a/src/protocols/rdp/compat/winpr-stream.h b/src/protocols/rdp/compat/winpr-stream.h
deleted file mode 100644
index e2195c0..0000000
--- a/src/protocols/rdp/compat/winpr-stream.h
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * 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_WINPR_STREAM_COMPAT_H
-#define __GUAC_WINPR_STREAM_COMPAT_H
-
-#include "config.h"
-
-#include "winpr-wtypes.h"
-
-#include <freerdp/utils/stream.h>
-
-#include <stddef.h>
-
-/* FreeRDP 1.0 streams */
-
-#define Stream_Write                   stream_write
-#define Stream_Write_UINT8             stream_write_uint8
-#define Stream_Write_UINT16            stream_write_uint16
-#define Stream_Write_UINT32            stream_write_uint32
-#define Stream_Write_UINT64            stream_write_uint64
-
-#define Stream_Read                    stream_read
-#define Stream_Read_UINT8              stream_read_uint8
-#define Stream_Read_UINT16             stream_read_uint16
-#define Stream_Read_UINT32             stream_read_uint32
-#define Stream_Read_UINT64             stream_read_uint64
-
-#define Stream_Seek                    stream_seek
-#define Stream_Seek_UINT8              stream_seek_uint8
-#define Stream_Seek_UINT16             stream_seek_uint16
-#define Stream_Seek_UINT32             stream_seek_uint32
-#define Stream_Seek_UINT64             stream_seek_uint64
-
-#define Stream_GetPointer              stream_get_mark
-#define Stream_EnsureRemainingCapacity stream_check_size
-#define Stream_Write                   stream_write
-#define Stream_Zero                    stream_write_zero
-#define Stream_Fill                    stream_set_byte
-#define Stream_GetPosition             stream_get_pos
-#define Stream_SetPosition             stream_set_pos
-#define Stream_SetPointer              stream_set_mark
-#define Stream_Buffer                  stream_get_head
-#define Stream_Pointer                 stream_get_tail
-#define Stream_Length                  stream_get_size
-
-#define wStream                        STREAM
-#define wMessage                       RDP_EVENT
-
-wStream* Stream_New(BYTE* buffer, size_t size);
-void Stream_Free(wStream* s, BOOL bFreeBuffer);
-
-#endif
-
diff --git a/src/protocols/rdp/compat/winpr-wtypes.h b/src/protocols/rdp/compat/winpr-wtypes.h
deleted file mode 100644
index e1d3e1b..0000000
--- a/src/protocols/rdp/compat/winpr-wtypes.h
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * 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_WINPR_WTYPES_COMPAT_H
-#define __GUAC_WINPR_WTYPES_COMPAT_H
-
-#include "config.h"
-
-#include <freerdp/types.h>
-
-typedef uint8   BYTE;
-typedef uint8   UINT8;
-typedef uint16  UINT16;
-typedef uint32  UINT32;
-typedef uint64  UINT64;
-typedef boolean BOOL;
-
-#define TRUE  true
-#define FALSE false
-
-#endif
-
diff --git a/src/protocols/rdp/decompose.c b/src/protocols/rdp/decompose.c
index 5f559d9..e42a769 100644
--- a/src/protocols/rdp/decompose.c
+++ b/src/protocols/rdp/decompose.c
@@ -17,7 +17,6 @@
  * under the License.
  */
 
-#include "config.h"
 #include "keyboard.h"
 
 /**
diff --git a/src/protocols/rdp/doc/svc-example/.gitignore b/src/protocols/rdp/doc/svc-example/.gitignore
new file mode 100644
index 0000000..51e8bef
--- /dev/null
+++ b/src/protocols/rdp/doc/svc-example/.gitignore
@@ -0,0 +1,2 @@
+!Makefile
+*.exe
diff --git a/src/protocols/rdp/doc/svc-example/Makefile b/src/protocols/rdp/doc/svc-example/Makefile
new file mode 100644
index 0000000..8c38e59
--- /dev/null
+++ b/src/protocols/rdp/doc/svc-example/Makefile
@@ -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.
+#
+# 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.
+#
+
+LDFLAGS=-lwtsapi32
+
+# Requires at least Windows Vista (Windows Server 2008 qualifies)
+WINVER=0x600
+
+# Windows cross compiler (MinGW)
+CC=i686-w64-mingw32-gcc
+
+all: svc-example.exe
+
+clean:
+	$(RM) svc-example.exe
+
+svc-example.exe: svc-example.c
+	$(CC) svc-example.c $(LDFLAGS) \
+		-D_WIN32_WINNT=$(WINVER)   \
+		-DWINVER=$(WINVER) -o svc-example.exe
+
diff --git a/src/protocols/rdp/doc/svc-example/README.md b/src/protocols/rdp/doc/svc-example/README.md
new file mode 100644
index 0000000..337a733
--- /dev/null
+++ b/src/protocols/rdp/doc/svc-example/README.md
@@ -0,0 +1,169 @@
+Static Virtual Channel example
+==============================
+
+Guacamole supports use of static virtual channels (SVCs) for transmission of
+arbitrary data between the JavaScript client and applications running within
+RDP sessions. This example is intended to demonstrate how bidirectional
+communication between the Guacamole client and applications within the RDP
+server can be accomplished.
+
+Arbitrary SVCs are enabled on RDP connections by specfying their names as the
+value of [the `static-channels`
+parameter](http://guacamole.apache.org/doc/gug/configuring-guacamole.html#rdp-device-redirection).
+Each name is limited to a maximum of 7 characters. Multiple names may be listed
+by separating those names with commas.
+
+This example consists of a single file, [`svc-example.c`](svc-example.c), which
+leverages the terminal server API exposed by Windows to:
+
+ 1. Open a channel called "EXAMPLE"
+ 2. Wait for blocks of data to be received
+ 3. Send each received block of data back, unmodified.
+
+Building the example
+--------------------
+
+A `Makefile` is provided which uses MinGW to build the `svc-example.exe`
+executable, and thus can be used to produce the example application on Linux.
+The `Makefile` is not platform-independent, and changes may be needed for
+`make` to succeed with your installation of MinGW. If not using MinGW, the C
+source itself is standard and should compile with other tools.
+
+To build on Linux using `make`:
+
+```console
+$ make
+i686-w64-mingw32-gcc svc-example.c -lwtsapi32 \
+	-D_WIN32_WINNT=0x600   \
+	-DWINVER=0x600 -o svc-example.exe
+$
+```
+
+You can then copy the resulting `svc-example.exe` to the remote desktop that
+you wish to test and run it within a command prompt within the remote desktop
+session.
+
+Using the example (and SVCs in general)
+---------------------------------------
+
+On the remote desktop server side (within the Windows application leveraging
+SVCs to communicate with Guacamole), the following functions are used
+specifically for reading/writing to the SVC:
+
+ * [`WTSVirtualChannelOpenEx()`](https://docs.microsoft.com/en-us/windows/win32/api/wtsapi32/nf-wtsapi32-wtsvirtualchannelopenex)
+ * [`WTSVirtualChannelRead()`](https://docs.microsoft.com/en-us/windows/win32/api/wtsapi32/nf-wtsapi32-wtsvirtualchannelread)
+ * [`WTSVirtualChannelWrite()`](https://docs.microsoft.com/en-us/windows/win32/api/wtsapi32/nf-wtsapi32-wtsvirtualchannelwrite)
+ * [`WTSVirtualChannelClose()`](https://docs.microsoft.com/en-us/windows/win32/api/wtsapi32/nf-wtsapi32-wtsvirtualchannelclose)
+
+On the Guacamole side, bidirectional communication is established using:
+
+ * The `static-channels` connection parameter (in the case of the example, this should be set to `EXAMPLE`).
+ * An [`onpipe`](http://guacamole.apache.org/doc/guacamole-common-js/Guacamole.Client.html#event:onpipe)
+   handler which handles inbound (server-to-client) pipe streams named identically
+   to the SVC. The inbound pipe stream will be received upon establishing the RDP
+   connection and is used to transmit any data sent along the SVC **from** within
+   the remote desktop session. For example:
+
+   ```js
+   client.onpipe = function pipeReceived(stream, mimetype, name) {
+   
+       // Receive output of SVC
+       if (name === 'EXAMPLE') {
+   
+           // Log start of stream
+           var reader = new Guacamole.StringReader(stream);
+           console.log('pipe: %s: stream begins', name);
+   
+           // Log each received blob of text
+           reader.ontext = function textReceived(text) {
+               console.log('pipe: %s: \"%s\"', name, text);
+           };
+   
+           // Log end of stream
+           reader.onend = function streamEnded() {
+               console.log('pipe: %s: stream ends', name);
+           };
+   
+       }
+   
+       // All other inbound pipe streams are unsupported
+       else
+           stream.sendAck('Pipe stream not supported.',
+               Guacamole.Status.Code.UNSUPPORTED);
+   
+   };
+   ```
+
+ * Calls to [`createPipeStream()`](http://guacamole.apache.org/doc/guacamole-common-js/Guacamole.Client.html#createPipeStream)
+   as needed to establish outbound (client-to-server) pipe streams named
+   identically to the SVC. Outbound pipe streams with the same name as the SVC
+   will be automatically handled by the Guacamole server, with any received data
+   sent along the SVC **to** the remote desktop session. For example:
+
+   ```js
+   var example = new Guacamole.StringWriter(client.createPipeStream('text/plain', 'EXAMPLE'));
+   example.sendText('This is a test.');
+   example.sendEnd();
+   ```
+
+   These pipe streams may be created and destroyed as desired. As long as they
+   have the same name as the SVC, data sent along the pipe stream will be sent
+   along the SVC.
+
+Example output
+--------------
+
+If the `static-channels` parameter is set to `EXAMPLE`, the successful creation
+of the "EXAMPLE" channel should be logged by guacd when the connection is
+established:
+
+```
+guacd[12057]: INFO: Created static channel "EXAMPLE"...
+guacd[12057]: INFO: Static channel "EXAMPLE" connected.
+```
+
+On the client side, the `onpipe` handler should be invoked immediately. If
+using the example code shown above, receipt of the pipe stream for the
+"EXAMPLE" channel is logged:
+
+```
+pipe: EXAMPLE: stream begins
+```
+
+Running `svc-example.exe` within a command prompt inside the remote desktop
+session, the application logs that the "EXAMPLE" channel has been successfully
+opened:
+
+```
+Microsoft Windows [Version 10.0.17763.437]
+(c) 2018 Microsoft Corporation. All rights reserved.
+
+C:\Users\test>svc-example.exe
+SVC "EXAMPLE" open. Reading...
+```
+
+Once `createPipeStream()` has been invoked on the Guacamole client side and
+using the same name as the SVC (in this case, "EXAMPLE") guacd should log the
+inbound half the channel is now connected:
+
+```
+guacd[12057]: DEBUG: Inbound half of channel "EXAMPLE" connected.
+```
+
+Sending the string `This is a test.` along the client-to-server pipe (as shown
+in the example code above) results in `svc-example.exe` logging that it
+received those 15 bytes and has resent the same 15 bytes back along the SVC:
+
+```
+Received 15 bytes.
+Wrote 15 bytes.
+```
+
+The data sent from within the remote desktop session is received on the client
+side via the server-to-client pipe stream. If using the example code shown
+above, the received data is logged:
+
+```
+pipe: EXAMPLE: "This is a test."
+```
+
diff --git a/src/protocols/rdp/doc/svc-example/svc-example.c b/src/protocols/rdp/doc/svc-example/svc-example.c
new file mode 100644
index 0000000..681b3f9
--- /dev/null
+++ b/src/protocols/rdp/doc/svc-example/svc-example.c
@@ -0,0 +1,71 @@
+/*
+ * 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 <windows.h>
+#include <winbase.h>
+#include <wtsapi32.h>
+
+#include <stdio.h>
+
+/**
+ * The name of the RDP static virtual channel (SVC).
+ */
+#define SVC_NAME "EXAMPLE"
+
+int main() {
+
+    ULONG bytes_read;
+    ULONG bytes_written;
+
+    char message[4096];
+
+    /* Open SVC */
+    HANDLE svc = WTSVirtualChannelOpenEx(WTS_CURRENT_SESSION, SVC_NAME, 0);
+
+    /* Fail if we cannot open an SVC at all */
+    if (svc == NULL) {
+        printf("Cannot open SVC \"" SVC_NAME "\"\n");
+        return 0;
+    }
+
+    printf("SVC \"" SVC_NAME "\" open. Reading...\n");
+
+    /* Continuously read from SVC */
+    while (WTSVirtualChannelRead(svc, INFINITE, message, sizeof(message), &bytes_read)) {
+
+        printf("Received %i bytes.\n", bytes_read);
+
+        /* Write all received data back to the SVC, possibly spreading the data
+         * across multiple writes */
+        char* current = message;
+        while (bytes_read > 0 && WTSVirtualChannelWrite(svc, current,
+                    bytes_read, &bytes_written)) {
+            printf("Wrote %i bytes.\n", bytes_written);
+            bytes_read -= bytes_written;
+        }
+
+    }
+
+    /* Close SVC */
+    WTSVirtualChannelClose(svc);
+    printf("SVC \"" SVC_NAME "\" closed.\n");
+    return 1;
+
+}
+
diff --git a/src/protocols/rdp/download.c b/src/protocols/rdp/download.c
new file mode 100644
index 0000000..34a8c8a
--- /dev/null
+++ b/src/protocols/rdp/download.c
@@ -0,0 +1,222 @@
+/*
+ * 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 "common/json.h"
+#include "download.h"
+#include "fs.h"
+#include "ls.h"
+#include "rdp.h"
+
+#include <guacamole/client.h>
+#include <guacamole/object.h>
+#include <guacamole/protocol.h>
+#include <guacamole/socket.h>
+#include <guacamole/stream.h>
+#include <guacamole/string.h>
+#include <guacamole/user.h>
+#include <winpr/nt.h>
+#include <winpr/shell.h>
+
+#include <stdlib.h>
+
+int guac_rdp_download_ack_handler(guac_user* user, guac_stream* stream,
+        char* message, guac_protocol_status status) {
+
+    guac_client* client = user->client;
+    guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
+    guac_rdp_download_status* download_status = (guac_rdp_download_status*) stream->data;
+
+    /* Get filesystem, return error if no filesystem */
+    guac_rdp_fs* fs = rdp_client->filesystem;
+    if (fs == NULL) {
+        guac_protocol_send_ack(user->socket, stream, "FAIL (NO FS)",
+                GUAC_PROTOCOL_STATUS_SERVER_ERROR);
+        guac_socket_flush(user->socket);
+        return 0;
+    }
+
+    /* If successful, read data */
+    if (status == GUAC_PROTOCOL_STATUS_SUCCESS) {
+
+        /* Attempt read into buffer */
+        char buffer[4096];
+        int bytes_read = guac_rdp_fs_read(fs,
+                download_status->file_id,
+                download_status->offset, buffer, sizeof(buffer));
+
+        /* If bytes read, send as blob */
+        if (bytes_read > 0) {
+            download_status->offset += bytes_read;
+            guac_protocol_send_blob(user->socket, stream,
+                    buffer, bytes_read);
+        }
+
+        /* If EOF, send end */
+        else if (bytes_read == 0) {
+            guac_protocol_send_end(user->socket, stream);
+            guac_user_free_stream(user, stream);
+            free(download_status);
+        }
+
+        /* Otherwise, fail stream */
+        else {
+            guac_user_log(user, GUAC_LOG_ERROR,
+                    "Error reading file for download");
+            guac_protocol_send_end(user->socket, stream);
+            guac_user_free_stream(user, stream);
+            free(download_status);
+        }
+
+        guac_socket_flush(user->socket);
+
+    }
+
+    /* Otherwise, return stream to user */
+    else
+        guac_user_free_stream(user, stream);
+
+    return 0;
+
+}
+
+int guac_rdp_download_get_handler(guac_user* user, guac_object* object,
+        char* name) {
+
+    guac_client* client = user->client;
+    guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
+
+    /* Get filesystem, ignore request if no filesystem */
+    guac_rdp_fs* fs = rdp_client->filesystem;
+    if (fs == NULL)
+        return 0;
+
+    /* Attempt to open file for reading */
+    int file_id = guac_rdp_fs_open(fs, name, GENERIC_READ, 0, FILE_OPEN, 0);
+    if (file_id < 0) {
+        guac_user_log(user, GUAC_LOG_INFO, "Unable to read file \"%s\"",
+                name);
+        return 0;
+    }
+
+    /* Get opened file */
+    guac_rdp_fs_file* file = guac_rdp_fs_get_file(fs, file_id);
+    if (file == NULL) {
+        guac_client_log(fs->client, GUAC_LOG_DEBUG,
+                "%s: Successful open produced bad file_id: %i",
+                __func__, file_id);
+        return 0;
+    }
+
+    /* If directory, send contents of directory */
+    if (file->attributes & FILE_ATTRIBUTE_DIRECTORY) {
+
+        /* Create stream data */
+        guac_rdp_ls_status* ls_status = malloc(sizeof(guac_rdp_ls_status));
+        ls_status->fs = fs;
+        ls_status->file_id = file_id;
+        guac_strlcpy(ls_status->directory_name, name,
+                sizeof(ls_status->directory_name));
+
+        /* Allocate stream for body */
+        guac_stream* stream = guac_user_alloc_stream(user);
+        stream->ack_handler = guac_rdp_ls_ack_handler;
+        stream->data = ls_status;
+
+        /* Init JSON object state */
+        guac_common_json_begin_object(user, stream,
+                &ls_status->json_state);
+
+        /* Associate new stream with get request */
+        guac_protocol_send_body(user->socket, object, stream,
+                GUAC_USER_STREAM_INDEX_MIMETYPE, name);
+
+    }
+
+    /* Otherwise, send file contents */
+    else {
+
+        /* Create stream data */
+        guac_rdp_download_status* download_status = malloc(sizeof(guac_rdp_download_status));
+        download_status->file_id = file_id;
+        download_status->offset = 0;
+
+        /* Allocate stream for body */
+        guac_stream* stream = guac_user_alloc_stream(user);
+        stream->data = download_status;
+        stream->ack_handler = guac_rdp_download_ack_handler;
+
+        /* Associate new stream with get request */
+        guac_protocol_send_body(user->socket, object, stream,
+                "application/octet-stream", name);
+
+    }
+
+    guac_socket_flush(user->socket);
+    return 0;
+}
+
+void* guac_rdp_download_to_user(guac_user* user, void* data) {
+
+    /* Do not bother attempting the download if the user has left */
+    if (user == NULL)
+        return NULL;
+
+    guac_client* client = user->client;
+    guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
+    guac_rdp_fs* filesystem = rdp_client->filesystem;
+
+    /* Ignore download if filesystem has been unloaded */
+    if (filesystem == NULL)
+        return NULL;
+
+    /* Attempt to open requested file */
+    char* path = (char*) data;
+    int file_id = guac_rdp_fs_open(filesystem, path,
+            FILE_READ_DATA, 0, FILE_OPEN, 0);
+
+    /* If file opened successfully, start stream */
+    if (file_id >= 0) {
+
+        /* Associate stream with transfer status */
+        guac_stream* stream = guac_user_alloc_stream(user);
+        guac_rdp_download_status* download_status = malloc(sizeof(guac_rdp_download_status));
+        stream->data = download_status;
+        stream->ack_handler = guac_rdp_download_ack_handler;
+        download_status->file_id = file_id;
+        download_status->offset = 0;
+
+        guac_user_log(user, GUAC_LOG_DEBUG, "%s: Initiating download "
+                "of \"%s\"", __func__, path);
+
+        /* Begin stream */
+        guac_protocol_send_file(user->socket, stream,
+                "application/octet-stream", guac_rdp_fs_basename(path));
+        guac_socket_flush(user->socket);
+
+        /* Download started successfully */
+        return stream;
+
+    }
+
+    /* Download failed */
+    guac_user_log(user, GUAC_LOG_ERROR, "Unable to download \"%s\"", path);
+    return NULL;
+
+}
+
diff --git a/src/protocols/rdp/download.h b/src/protocols/rdp/download.h
new file mode 100644
index 0000000..2989658
--- /dev/null
+++ b/src/protocols/rdp/download.h
@@ -0,0 +1,69 @@
+/*
+ * 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_DOWNLOAD_H
+#define GUAC_RDP_DOWNLOAD_H
+
+#include "common/json.h"
+
+#include <guacamole/protocol.h>
+#include <guacamole/stream.h>
+#include <guacamole/user.h>
+
+#include <stdint.h>
+
+/**
+ * The transfer status of a file being downloaded.
+ */
+typedef struct guac_rdp_download_status {
+
+    /**
+     * The file ID of the file being downloaded.
+     */
+    int file_id;
+
+    /**
+     * The current position within the file.
+     */
+    uint64_t offset;
+
+} guac_rdp_download_status;
+
+/**
+ * Handler for acknowledgements of receipt of data related to file downloads.
+ */
+guac_user_ack_handler guac_rdp_download_ack_handler;
+
+/**
+ * Handler for get messages. In context of downloads and the filesystem exposed
+ * via the Guacamole protocol, get messages request the body of a file within
+ * the filesystem.
+ */
+guac_user_get_handler guac_rdp_download_get_handler;
+
+/**
+ * Callback for guac_client_for_user() and similar functions which initiates a
+ * file download to a specific user if that user is still connected. The path
+ * for the file to be downloaded must be passed as the arbitrary data parameter
+ * for the function invoking this callback.
+ */
+guac_user_callback guac_rdp_download_to_user;
+
+#endif
+
diff --git a/src/protocols/rdp/dvc.c b/src/protocols/rdp/dvc.c
deleted file mode 100644
index 34406c9..0000000
--- a/src/protocols/rdp/dvc.c
+++ /dev/null
@@ -1,183 +0,0 @@
-/*
- * 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 "common/list.h"
-#include "dvc.h"
-#include "rdp.h"
-
-#include <freerdp/channels/channels.h>
-#include <freerdp/freerdp.h>
-#include <guacamole/client.h>
-
-#include <assert.h>
-#include <stdarg.h>
-
-guac_rdp_dvc_list* guac_rdp_dvc_list_alloc() {
-
-    guac_rdp_dvc_list* list = malloc(sizeof(guac_rdp_dvc_list));
-
-    /* Initialize with empty backing list */
-    list->channels = guac_common_list_alloc();
-    list->channel_count = 0;
-
-    return list;
-
-}
-
-void guac_rdp_dvc_list_add(guac_rdp_dvc_list* list, const char* name, ...) {
-
-    va_list args;
-
-    guac_rdp_dvc* dvc = malloc(sizeof(guac_rdp_dvc));
-
-    va_start(args, name);
-
-    /* Count number of arguments (excluding terminating NULL) */
-    dvc->argc = 1;
-    while (va_arg(args, char*) != NULL)
-        dvc->argc++;
-
-    /* Reset va_list */
-    va_end(args);
-    va_start(args, name);
-
-    /* Copy argument values into DVC entry */
-    dvc->argv = malloc(sizeof(char*) * dvc->argc);
-    dvc->argv[0] = strdup(name);
-    int i;
-    for (i = 1; i < dvc->argc; i++)
-        dvc->argv[i] = strdup(va_arg(args, char*));
-
-    va_end(args);
-
-    /* Add entry to DVC list */
-    guac_common_list_add(list->channels, dvc);
-
-    /* Update channel count */
-    list->channel_count++;
-
-}
-
-void guac_rdp_dvc_list_free(guac_rdp_dvc_list* list) {
-
-    /* For each channel */
-    guac_common_list_element* current = list->channels->head;
-    while (current != NULL) {
-
-        /* Free arguments declaration for current channel */
-        guac_rdp_dvc* dvc = (guac_rdp_dvc*) current->data;
-
-        /* Free the underlying arguments list if not delegated to FreeRDP */
-        if (dvc->argv != NULL) {
-
-            /* Free each argument value */
-            for (int i = 0; i < dvc->argc; i++)
-                free(dvc->argv[i]);
-
-            free(dvc->argv);
-        }
-
-        free(dvc);
-
-        current = current->next;
-
-    }
-
-    /* Free underlying list */
-    guac_common_list_free(list->channels);
-
-    /* Free the DVC list itself */
-    free(list);
-
-}
-
-int guac_rdp_load_drdynvc(rdpContext* context, guac_rdp_dvc_list* list) {
-
-    guac_client* client = ((rdp_freerdp_context*) context)->client;
-    rdpChannels* channels = context->channels;
-
-    /* Skip if no channels will be loaded */
-    if (list->channel_count == 0)
-        return 0;
-
-#ifndef HAVE_ADDIN_ARGV
-    /* Allocate plugin data array */
-    RDP_PLUGIN_DATA* all_plugin_data =
-        calloc(list->channel_count + 1, sizeof(RDP_PLUGIN_DATA));
-
-    RDP_PLUGIN_DATA* current_plugin_data = all_plugin_data;
-#endif
-
-    /* For each channel */
-    guac_common_list_element* current = list->channels->head;
-    while (current != NULL) {
-
-        /* Get channel arguments */
-        guac_rdp_dvc* dvc = (guac_rdp_dvc*) current->data;
-        current = current->next;
-
-        /* guac_rdp_dvc_list_add() guarantees at one argument */
-        assert(dvc->argc >= 1);
-
-        /* guac_rdp_load_drdynvc() MUST only be invoked once */
-        assert(dvc->argv != NULL);
-
-        /* Log registration of plugin for current channel */
-        guac_client_log(client, GUAC_LOG_DEBUG,
-                "Registering DVC plugin \"%s\"", dvc->argv[0]);
-
-#ifdef HAVE_ADDIN_ARGV
-        /* Register plugin with FreeRDP */
-        ADDIN_ARGV* args = malloc(sizeof(ADDIN_ARGV));
-        args->argc = dvc->argc;
-        args->argv = dvc->argv;
-        freerdp_dynamic_channel_collection_add(context->settings, args);
-#else
-        /* Copy all arguments */
-        for (int i = 0; i < dvc->argc; i++)
-            current_plugin_data->data[i] = dvc->argv[i];
-
-        /* Store size of entry */
-        current_plugin_data->size = sizeof(*current_plugin_data);
-
-        /* Advance to next set of plugin data */
-        current_plugin_data++;
-#endif
-
-        /* Rely on FreeRDP to free argv storage */
-        dvc->argv = NULL;
-
-    }
-
-#ifdef HAVE_ADDIN_ARGV
-    /* Load virtual channel management plugin */
-    return freerdp_channels_load_plugin(channels, context->instance->settings,
-                "drdynvc", context->instance->settings);
-#else
-    /* Terminate with empty RDP_PLUGIN_DATA element */
-    current_plugin_data->size = 0;
-
-    /* Load virtual channel management plugin */
-    return freerdp_channels_load_plugin(channels, context->instance->settings,
-                "drdynvc", all_plugin_data);
-#endif
-
-}
-
diff --git a/src/protocols/rdp/dvc.h b/src/protocols/rdp/dvc.h
deleted file mode 100644
index 63694f5..0000000
--- a/src/protocols/rdp/dvc.h
+++ /dev/null
@@ -1,138 +0,0 @@
-/*
- * 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_DVC_H
-#define GUAC_RDP_DVC_H
-
-#include "config.h"
-#include "common/list.h"
-
-#include <freerdp/freerdp.h>
-
-/**
- * The set of all arguments that should be passed to a given dynamic virtual
- * channel plugin, including the name of that plugin.
- */
-typedef struct guac_rdp_dvc {
-
-    /**
-     * The number of arguments in the argv array. This MUST be at least 1.
-     */
-    int argc;
-
-    /**
-     * The argument values being passed to the dynamic virtual channel plugin.
-     * The first entry in this array is always the name of the plugin. If
-     * guac_rdp_load_drdynvc() has been invoked, and freeing the argument
-     * values is being delegated to FreeRDP, this will be NULL.
-     */
-    char** argv;
-
-} guac_rdp_dvc;
-
-/**
- * A list of dynamic virtual channels which should be provided to the DRDYNVC
- * plugin once loaded via guac_rdp_load_drdynvc(). This interface exists purely
- * to bridge incompatibilities between differing versions of FreeRDP and its
- * DRDYNVC plugin. Any allocated guac_rdp_dvc_list is unlikely to be needed
- * after the DRDYNVC plugin has been loaded.
- */
-typedef struct guac_rdp_dvc_list {
-
-    /**
-     * Array of all dynamic virtual channels which should be registered with
-     * the DRDYNVC plugin once loaded. Each list element will point to a
-     * guac_rdp_dvc structure which must eventually be freed.
-     */
-    guac_common_list* channels;
-
-    /**
-     * The number of channels within the list.
-     */
-    int channel_count;
-
-} guac_rdp_dvc_list;
-
-/**
- * Allocates a new, empty list of dynamic virtual channels. New channels may
- * be added via guac_rdp_dvc_list_add(). The loading of those channels'
- * associated plugins will be deferred until guac_rdp_load_drdynvc() is
- * invoked.
- *
- * @return
- *     A newly-allocated, empty list of dynamic virtual channels.
- */
-guac_rdp_dvc_list* guac_rdp_dvc_list_alloc();
-
-/**
- * Adds the given dynamic virtual channel plugin name and associated arguments
- * to the list. The provied arguments list is NOT optional and MUST be
- * NULL-terminated, even if there are no arguments for the named dynamic
- * virtual channel plugin. Though FreeRDP requires that the arguments for a
- * dynamic virtual channel plugin contain the name of the plugin itself as the
- * first argument, the name must be excluded from the arguments provided here.
- * This function will automatically take care of adding the plugin name to
- * the arguments.
- *
- * @param list
- *     The guac_rdp_dvc_list to which the given plugin name and arguments
- *     should be added, for later bulk registration via
- *     guac_rdp_load_drdynvc().
- *
- * @param name
- *     The name of the dynamic virtual channel plugin that should be given
- *     the provided arguments when guac_rdp_load_drdynvc() is invoked.
- *
- * @param ...
- *     The string (char*) arguments which should be passed to the dynamic
- *     virtual channel plugin when it is loaded via guac_rdp_load_drdynvc(),
- *     excluding the plugin name itself.
- */
-void guac_rdp_dvc_list_add(guac_rdp_dvc_list* list, const char* name, ...);
-
-/**
- * Frees the given list of dynamic virtual channels. Note that, while each
- * individual entry within this list will be freed, it is partially up to
- * FreeRDP to free the storage associated with the arguments passed to the
- * virtual channels.
- *
- * @param list
- *     The list to free.
- */
-void guac_rdp_dvc_list_free(guac_rdp_dvc_list* list);
-
-/**
- * Loads FreeRDP's DRDYNVC plugin and registers the dynamic virtual channel
- * plugins described by the given guac_rdp_dvc_list. This function MUST be
- * invoked no more than once per RDP connection. Invoking this function
- * multiple times, even if the guac_rdp_dvc_list is different each time, will
- * result in undefined behavior.
- *
- * @param context
- *     The rdpContext associated with the RDP connection for which the DRDYNVC
- *     plugin should be loaded.
- *
- * @param list
- *     A guac_rdp_dvc_list describing the dynamic virtual channel plugins that
- *     should be registered with the DRDYNVC plugin, along with any arguments.
- */
-int guac_rdp_load_drdynvc(rdpContext* context, guac_rdp_dvc_list* list);
-
-#endif
-
diff --git a/src/protocols/rdp/error.c b/src/protocols/rdp/error.c
index e340362..de0e1cd 100644
--- a/src/protocols/rdp/error.c
+++ b/src/protocols/rdp/error.c
@@ -17,8 +17,6 @@
  * under the License.
  */
 
-#include "config.h"
-
 #include "error.h"
 #include "rdp.h"
 
diff --git a/src/protocols/rdp/rdp_fs.c b/src/protocols/rdp/fs.c
similarity index 95%
rename from src/protocols/rdp/rdp_fs.c
rename to src/protocols/rdp/fs.c
index 24d7c3e..5223b5a 100644
--- a/src/protocols/rdp/rdp_fs.c
+++ b/src/protocols/rdp/fs.c
@@ -17,11 +17,19 @@
  * under the License.
  */
 
-#include "config.h"
+#include "fs.h"
+#include "download.h"
+#include "upload.h"
 
-#include "rdp_fs.h"
-#include "rdp_status.h"
-#include "rdp_stream.h"
+#include <guacamole/client.h>
+#include <guacamole/object.h>
+#include <guacamole/pool.h>
+#include <guacamole/protocol.h>
+#include <guacamole/socket.h>
+#include <guacamole/string.h>
+#include <guacamole/user.h>
+#include <winpr/file.h>
+#include <winpr/nt.h>
 
 #include <dirent.h>
 #include <errno.h>
@@ -32,16 +40,8 @@
 #include <string.h>
 #include <sys/stat.h>
 #include <sys/statvfs.h>
-#include <sys/types.h>
 #include <unistd.h>
 
-#include <guacamole/client.h>
-#include <guacamole/object.h>
-#include <guacamole/pool.h>
-#include <guacamole/socket.h>
-#include <guacamole/string.h>
-#include <guacamole/user.h>
-
 guac_rdp_fs* guac_rdp_fs_alloc(guac_client* client, const char* drive_path,
         int create_drive_path) {
 
@@ -244,16 +244,16 @@
     }
 
     /* Translate access into flags */
-    if (access & ACCESS_GENERIC_ALL)
+    if (access & GENERIC_ALL)
         flags = O_RDWR;
-    else if ((access & ( ACCESS_GENERIC_WRITE
-                       | ACCESS_FILE_WRITE_DATA
-                       | ACCESS_FILE_APPEND_DATA))
-          && (access & (ACCESS_GENERIC_READ  | ACCESS_FILE_READ_DATA)))
+    else if ((access & ( GENERIC_WRITE
+                       | FILE_WRITE_DATA
+                       | FILE_APPEND_DATA))
+          && (access & (GENERIC_READ  | FILE_READ_DATA)))
         flags = O_RDWR;
-    else if (access & ( ACCESS_GENERIC_WRITE
-                      | ACCESS_FILE_WRITE_DATA
-                      | ACCESS_FILE_APPEND_DATA))
+    else if (access & ( GENERIC_WRITE
+                      | FILE_WRITE_DATA
+                      | FILE_APPEND_DATA))
         flags = O_WRONLY;
     else
         flags = O_RDONLY;
@@ -279,32 +279,32 @@
     switch (create_disposition) {
 
         /* Create if not exist, fail otherwise */
-        case DISP_FILE_CREATE:
+        case FILE_CREATE:
             flags |= O_CREAT | O_EXCL;
             break;
 
         /* Open file if exists and do not overwrite, fail otherwise */
-        case DISP_FILE_OPEN:
+        case FILE_OPEN:
             /* No flag necessary - default functionality of open */
             break;
 
         /* Open if exists, create otherwise */
-        case DISP_FILE_OPEN_IF:
+        case FILE_OPEN_IF:
             flags |= O_CREAT;
             break;
 
         /* Overwrite if exists, fail otherwise */
-        case DISP_FILE_OVERWRITE:
+        case FILE_OVERWRITE:
             flags |= O_TRUNC;
             break;
 
         /* Overwrite if exists, create otherwise */
-        case DISP_FILE_OVERWRITE_IF:
+        case FILE_OVERWRITE_IF:
             flags |= O_CREAT | O_TRUNC;
             break;
 
         /* Supersede (replace) if exists, otherwise create */
-        case DISP_FILE_SUPERSEDE:
+        case FILE_SUPERSEDE:
             unlink(real_path);
             flags |= O_CREAT | O_TRUNC;
             break;
@@ -605,6 +605,21 @@
 
 }
 
+const char* guac_rdp_fs_basename(const char* path) {
+
+    for (const char* c = path; *c != '\0'; c++) {
+
+        /* Reset beginning of path if a path separator is found */
+        if (*c == '/' || *c == '\\')
+            path = c + 1;
+
+    }
+
+    /* path now points to the first character after the last path separator */
+    return path;
+
+}
+
 int guac_rdp_fs_normalize_path(const char* path, char* abs_path) {
 
     int path_depth = 0;
diff --git a/src/protocols/rdp/rdp_fs.h b/src/protocols/rdp/fs.h
similarity index 89%
rename from src/protocols/rdp/rdp_fs.h
rename to src/protocols/rdp/fs.h
index f1e4295..83a0b3b 100644
--- a/src/protocols/rdp/rdp_fs.h
+++ b/src/protocols/rdp/fs.h
@@ -17,25 +17,24 @@
  * under the License.
  */
 
-
-#ifndef __GUAC_RDP_FS_H
-#define __GUAC_RDP_FS_H
+#ifndef GUAC_RDP_FS_H
+#define GUAC_RDP_FS_H
 
 /**
  * Functions and macros specific to filesystem handling and initialization
- * independent of RDP.  The functions here may deal with the filesystem device
+ * independent of RDP. The functions here may deal with the filesystem device
  * directly, but their semantics must not deal with RDP protocol messaging.
  * Functions here represent a virtual Windows-style filesystem on top of UNIX
  * system calls and structures, using the guac_rdp_fs structure as a home
  * for common data.
  *
- * @file rdp_fs.h 
+ * @file fs.h 
  */
 
-#include "config.h"
-
 #include <guacamole/client.h>
+#include <guacamole/object.h>
 #include <guacamole/pool.h>
+#include <guacamole/user.h>
 
 #include <dirent.h>
 #include <stdint.h>
@@ -109,73 +108,6 @@
  */
 #define GUAC_RDP_FS_ENOTSUP -10
 
-/*
- * Access constants.
- */
-#define ACCESS_GENERIC_READ       0x80000000
-#define ACCESS_GENERIC_WRITE      0x40000000
-#define ACCESS_GENERIC_ALL        0x10000000
-#define ACCESS_FILE_READ_DATA     0x00000001
-#define ACCESS_FILE_WRITE_DATA    0x00000002
-#define ACCESS_FILE_APPEND_DATA   0x00000004
-#define ACCESS_DELETE             0x00010000
-
-/*
- * Create disposition constants.
- */
-
-#define DISP_FILE_SUPERSEDE    0x00000000
-#define DISP_FILE_OPEN         0x00000001
-#define DISP_FILE_CREATE       0x00000002
-#define DISP_FILE_OPEN_IF      0x00000003
-#define DISP_FILE_OVERWRITE    0x00000004
-#define DISP_FILE_OVERWRITE_IF 0x00000005
-
-/*
- * Information constants.
- * FreeRDP 1.1+ already defines those constants
- */
-#ifdef LEGACY_FREERDP
-
-#define FILE_SUPERSEDED   0x00000000
-#define FILE_OPENED       0x00000001
-#define FILE_OVERWRITTEN  0x00000003
-
-#endif
-
-/*
- * File attributes.
- */
-
-#define FILE_ATTRIBUTE_READONLY  0x00000001 
-#define FILE_ATTRIBUTE_HIDDEN    0x00000002 
-#define FILE_ATTRIBUTE_DIRECTORY 0x00000010
-#define FILE_ATTRIBUTE_ARCHIVE   0x00000020
-#define FILE_ATTRIBUTE_NORMAL    0x00000080
-
-/*
- * Filesystem attributes.
- */
-
-#define FILE_CASE_SENSITIVE_SEARCH 0x00000001
-#define FILE_CASE_PRESERVED_NAMES  0x00000002
-#define FILE_UNICODE_ON_DISK       0x00000004
-
-/*
- * File create options.
- */
-
-#define FILE_DIRECTORY_FILE     0x00000001
-#define FILE_NON_DIRECTORY_FILE 0x00000040
-
-/*
- * File device types.
- */
-
-#define FILE_DEVICE_DISK 0x00000007
-
-#define SEC_TO_UNIX_EPOCH 11644473600
-
 /**
  * Converts a Windows timestamp (100 nanosecond intervals since Jan 1, 1601
  * UTC) to UNIX timestamp (seconds since Jan 1, 1970 UTC).
@@ -438,10 +370,10 @@
  *     The absolute path to the file within the simulated filesystem.
  *
  * @param access
- *     A bitwise-OR of various RDPDR access flags, such as ACCESS_GENERIC_ALL
- *     or ACCESS_GENERIC_WRITE. This value will ultimately be translated to a
- *     standard O_RDWR, O_WRONLY, etc. value when opening the real file on the
- *     local filesystem.
+ *     A bitwise-OR of various RDPDR access flags, such as GENERIC_ALL or
+ *     GENERIC_WRITE. This value will ultimately be translated to a standard
+ *     O_RDWR, O_WRONLY, etc. value when opening the real file on the local
+ *     filesystem.
  *
  * @param file_attributes
  *     The attributes to apply to the file, if created. This parameter is
@@ -449,9 +381,9 @@
  *
  * @param create_disposition
  *     Any one of several RDPDR file creation dispositions, such as
- *     DISP_FILE_CREATE, DISP_FILE_OPEN_IF, etc. The creation disposition
- *     dictates whether a new file should be created, whether the file can
- *     already exist, whether existing contents should be truncated, etc.
+ *     FILE_CREATE, FILE_OPEN_IF, etc. The creation disposition dictates
+ *     whether a new file should be created, whether the file can already
+ *     exist, whether existing contents should be truncated, etc.
  *
  * @param create_options
  *     A bitwise-OR of various RDPDR options dictating how a file is to be
@@ -593,6 +525,20 @@
 void guac_rdp_fs_close(guac_rdp_fs* fs, int file_id);
 
 /**
+ * Given an arbitrary path, returns a pointer to the first character following
+ * the last path separator in the path (the basename of the path). For example,
+ * given "/foo/bar/baz" or "\foo\bar\baz", this function would return a pointer
+ * to "baz".
+ *
+ * @param path
+ *     The path to determine the basename of.
+ *
+ * @return
+ *     A pointer to the first character of the basename within the path.
+ */
+const char* guac_rdp_fs_basename(const char* path);
+
+/**
  * Given an arbitrary path, which may contain ".." and ".", creates an
  * absolute path which does NOT contain ".." or ".". The given path MUST
  * be absolute.
diff --git a/src/protocols/rdp/rdp_gdi.c b/src/protocols/rdp/gdi.c
similarity index 61%
rename from src/protocols/rdp/rdp_gdi.c
rename to src/protocols/rdp/gdi.c
index 59fe64c..feb6505 100644
--- a/src/protocols/rdp/rdp_gdi.c
+++ b/src/protocols/rdp/gdi.c
@@ -17,27 +17,22 @@
  * under the License.
  */
 
-#include "config.h"
-
-#include "client.h"
+#include "bitmap.h"
+#include "common/display.h"
 #include "common/surface.h"
 #include "rdp.h"
-#include "rdp_bitmap.h"
-#include "rdp_color.h"
-#include "rdp_settings.h"
+#include "settings.h"
 
 #include <cairo/cairo.h>
 #include <freerdp/freerdp.h>
+#include <freerdp/graphics.h>
+#include <freerdp/primary.h>
 #include <guacamole/client.h>
 #include <guacamole/protocol.h>
-
-#ifdef ENABLE_WINPR
 #include <winpr/wtypes.h>
-#else
-#include "compat/winpr-wtypes.h"
-#endif
 
 #include <stddef.h>
+#include <stddef.h>
 
 guac_transfer_function guac_rdp_rop3_transfer_function(guac_client* client,
         int rop3) {
@@ -97,7 +92,7 @@
 
 }
 
-void guac_rdp_gdi_dstblt(rdpContext* context, DSTBLT_ORDER* dstblt) {
+BOOL guac_rdp_gdi_dstblt(rdpContext* context, const DSTBLT_ORDER* dstblt) {
 
     guac_client* client = ((rdp_freerdp_context*) context)->client;
     guac_common_surface* current_surface = ((guac_rdp_client*) client->data)->current_surface;
@@ -140,77 +135,11 @@
 
     }
 
-}
-
-void guac_rdp_gdi_patblt(rdpContext* context, PATBLT_ORDER* patblt) {
-
-    /*
-     * Note that this is not a full implementation of PATBLT. This is a
-     * fallback implementation which only renders a solid block of background
-     * color using the specified ROP3 operation, ignoring whatever brush
-     * was actually specified.
-     *
-     * As libguac-client-rdp explicitly tells the server not to send PATBLT,
-     * well-behaved RDP servers will not use this operation at all, while
-     * others will at least have a fallback.
-     */
-
-    /* Get client and current layer */
-    guac_client* client = ((rdp_freerdp_context*) context)->client;
-    guac_common_surface* current_surface =
-        ((guac_rdp_client*) client->data)->current_surface;
-
-    int x = patblt->nLeftRect;
-    int y = patblt->nTopRect;
-    int w = patblt->nWidth;
-    int h = patblt->nHeight;
-
-    /*
-     * Warn that rendering is a fallback, as the server should not be sending
-     * this order.
-     */
-    guac_client_log(client, GUAC_LOG_INFO, "Using fallback PATBLT (server is ignoring "
-            "negotiated client capabilities)");
-
-    /* Render rectangle based on ROP */
-    switch (patblt->bRop) {
-
-        /* If blackness, send black rectangle */
-        case 0x00:
-            guac_common_surface_set(current_surface, x, y, w, h,
-                    0x00, 0x00, 0x00, 0xFF);
-            break;
-
-        /* If NOP, do nothing */
-        case 0xAA:
-            break;
-
-        /* If operation is just a copy, send foreground only */
-        case 0xCC:
-        case 0xF0:
-            guac_common_surface_set(current_surface, x, y, w, h,
-                    (patblt->foreColor >> 16) & 0xFF,
-                    (patblt->foreColor >> 8 ) & 0xFF,
-                    (patblt->foreColor      ) & 0xFF,
-                    0xFF);
-            break;
-
-        /* If whiteness, send white rectangle */
-        case 0xFF:
-            guac_common_surface_set(current_surface, x, y, w, h,
-                    0xFF, 0xFF, 0xFF, 0xFF);
-            break;
-
-        /* Otherwise, invert entire rect */
-        default:
-            guac_common_surface_transfer(current_surface, x, y, w, h,
-                                         GUAC_TRANSFER_BINARY_NDEST, current_surface, x, y);
-
-    }
+    return TRUE;
 
 }
 
-void guac_rdp_gdi_scrblt(rdpContext* context, SCRBLT_ORDER* scrblt) {
+BOOL guac_rdp_gdi_scrblt(rdpContext* context, const SCRBLT_ORDER* scrblt) {
 
     guac_client* client = ((rdp_freerdp_context*) context)->client;
     guac_common_surface* current_surface = ((guac_rdp_client*) client->data)->current_surface;
@@ -229,9 +158,11 @@
     guac_common_surface_copy(rdp_client->display->default_surface,
             x_src, y_src, w, h, current_surface, x, y);
 
+    return TRUE;
+
 }
 
-void guac_rdp_gdi_memblt(rdpContext* context, MEMBLT_ORDER* memblt) {
+BOOL guac_rdp_gdi_memblt(rdpContext* context, MEMBLT_ORDER* memblt) {
 
     guac_client* client = ((rdp_freerdp_context*) context)->client;
     guac_common_surface* current_surface = ((guac_rdp_client*) client->data)->current_surface;
@@ -248,7 +179,7 @@
     /* Make sure that the recieved bitmap is not NULL before processing */
     if (bitmap == NULL) {
         guac_client_log(client, GUAC_LOG_INFO, "NULL bitmap found in memblt instruction.");
-        return;
+        return TRUE;
     }
 
     switch (memblt->bRop) {
@@ -321,98 +252,11 @@
 
     }
 
-}
-
-void guac_rdp_gdi_opaquerect(rdpContext* context, OPAQUE_RECT_ORDER* opaque_rect) {
-
-    /* Get client data */
-    guac_client* client = ((rdp_freerdp_context*) context)->client;
-
-    UINT32 color = guac_rdp_convert_color(context, opaque_rect->color);
-
-    guac_common_surface* current_surface = ((guac_rdp_client*) client->data)->current_surface;
-
-    int x = opaque_rect->nLeftRect;
-    int y = opaque_rect->nTopRect;
-    int w = opaque_rect->nWidth;
-    int h = opaque_rect->nHeight;
-
-    guac_common_surface_set(current_surface, x, y, w, h,
-            (color >> 16) & 0xFF,
-            (color >> 8 ) & 0xFF,
-            (color      ) & 0xFF,
-            0xFF);
+    return TRUE;
 
 }
 
-/**
- * Updates the palette within a FreeRDP CLRCONV object using the new palette
- * entries provided by an RDP palette update.
- *
- * @param clrconv
- *     The FreeRDP CLRCONV object to update.
- *
- * @param palette
- *     An RDP palette update message containing the palette to store within the
- *     given CLRCONV object.
- */
-static void guac_rdp_update_clrconv(CLRCONV* clrconv,
-        PALETTE_UPDATE* palette) {
-
-    clrconv->palette->count = palette->number;
-#ifdef LEGACY_RDPPALETTE
-    clrconv->palette->entries = palette->entries;
-#else
-    memcpy(clrconv->palette->entries, palette->entries,
-            sizeof(palette->entries));
-#endif
-
-}
-
-/**
- * Updates a raw ARGB32 palette using the new palette entries provided by an
- * RDP palette update.
- *
- * @param guac_palette
- *     An array of 256 ARGB32 colors, with each entry corresponding to an
- *     entry in the color palette.
- *
- * @param palette
- *     An RDP palette update message containing the palette to store within the
- *     given array of ARGB32 colors.
- */
-static void guac_rdp_update_palette(UINT32* guac_palette,
-        PALETTE_UPDATE* palette) {
-
-    PALETTE_ENTRY* entry = palette->entries;
-    int i;
-
-    /* Copy each palette entry as ARGB32 */
-    for (i=0; i < palette->number; i++) {
-
-        *guac_palette = 0xFF000000
-                      | (entry->red   << 16)
-                      | (entry->green << 8)
-                      |  entry->blue;
-
-        guac_palette++;
-        entry++;
-    }
-
-}
-
-void guac_rdp_gdi_palette_update(rdpContext* context, PALETTE_UPDATE* palette) {
-
-    CLRCONV* clrconv = ((rdp_freerdp_context*) context)->clrconv;
-    UINT32* guac_palette = ((rdp_freerdp_context*) context)->palette;
-
-    /* Update internal palette representations */
-    guac_rdp_update_clrconv(clrconv, palette);
-    guac_rdp_update_palette(guac_palette, palette);
-
-}
-
-void guac_rdp_gdi_set_bounds(rdpContext* context, rdpBounds* bounds) {
+BOOL guac_rdp_gdi_set_bounds(rdpContext* context, const rdpBounds* bounds) {
 
     guac_client* client = ((rdp_freerdp_context*) context)->client;
     guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
@@ -428,13 +272,16 @@
                 bounds->right - bounds->left + 1,
                 bounds->bottom - bounds->top + 1);
 
+    return TRUE;
+
 }
 
-void guac_rdp_gdi_end_paint(rdpContext* context) {
+BOOL guac_rdp_gdi_end_paint(rdpContext* context) {
     /* IGNORE */
+    return TRUE;
 }
 
-void guac_rdp_gdi_desktop_resize(rdpContext* context) {
+BOOL guac_rdp_gdi_desktop_resize(rdpContext* context) {
 
     guac_client* client = ((rdp_freerdp_context*) context)->client;
     guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
@@ -449,6 +296,8 @@
             guac_rdp_get_width(context->instance),
             guac_rdp_get_height(context->instance));
 
+    return TRUE;
+
 }
 
 
diff --git a/src/protocols/rdp/rdp_gdi.h b/src/protocols/rdp/gdi.h
similarity index 63%
rename from src/protocols/rdp/rdp_gdi.h
rename to src/protocols/rdp/gdi.h
index cbe3c75..9ad5713 100644
--- a/src/protocols/rdp/rdp_gdi.h
+++ b/src/protocols/rdp/gdi.h
@@ -17,9 +17,8 @@
  * under the License.
  */
 
-
-#ifndef _GUAC_RDP_RDP_GDI_H
-#define _GUAC_RDP_RDP_GDI_H
+#ifndef GUAC_RDP_GDI_H
+#define GUAC_RDP_GDI_H
 
 #include "config.h"
 
@@ -46,71 +45,58 @@
         int rop3);
 
 /**
- * Handler for RDP DSTBLT update.
+ * Handler for the DstBlt Primary Drawing Order. A DstBlt Primary Drawing Order
+ * paints a rectangle of image data using a raster operation which considers
+ * the destination only. See:
+ *
+ * https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpegdi/87ea30df-59d6-438e-a735-83f0225fbf91
  *
  * @param context
  *     The rdpContext associated with the current RDP session.
  *
  * @param dstblt
  *     The DSTBLT update to handle.
+ *
+ * @return
+ *     TRUE if successful, FALSE otherwise.
  */
-void guac_rdp_gdi_dstblt(rdpContext* context, DSTBLT_ORDER* dstblt);
+BOOL guac_rdp_gdi_dstblt(rdpContext* context, const DSTBLT_ORDER* dstblt);
 
 /**
- * Handler for RDP PATBLT update.
+ * Handler for the ScrBlt Primary Drawing Order. A ScrBlt Primary Drawing Order
+ * paints a rectangle of image data using a raster operation which considers
+ * the source and destination. See:
  *
- * @param context
- *     The rdpContext associated with the current RDP session.
- *
- * @param patblt
- *     The PATBLT update to handle.
- */
-void guac_rdp_gdi_patblt(rdpContext* context, PATBLT_ORDER* patblt);
-
-/**
- * Handler for RDP SCRBLT update.
+ * https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpegdi/a4e322b0-cd64-4dfc-8e1a-f24dc0edc99d
  *
  * @param context
  *     The rdpContext associated with the current RDP session.
  *
  * @param scrblt
  *     The SCRBLT update to handle.
+ *
+ * @return
+ *     TRUE if successful, FALSE otherwise.
  */
-void guac_rdp_gdi_scrblt(rdpContext* context, SCRBLT_ORDER* scrblt);
+BOOL guac_rdp_gdi_scrblt(rdpContext* context, const SCRBLT_ORDER* scrblt);
 
 /**
- * Handler for RDP MEMBLT update.
+ * Handler for the MemBlt Primary Drawing Order. A MemBlt Primary Drawing Order
+ * paints a rectangle of cached image data from a cached surface to the screen
+ * using a raster operation which considers the source and destination. See:
+ *
+ * https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpegdi/84c2ec2f-f776-405b-9b48-6894a28b1b14
  *
  * @param context
  *     The rdpContext associated with the current RDP session.
  *
  * @param memblt
  *     The MEMBLT update to handle.
+ *
+ * @return
+ *     TRUE if successful, FALSE otherwise.
  */
-void guac_rdp_gdi_memblt(rdpContext* context, MEMBLT_ORDER* memblt);
-
-/**
- * Handler for RDP OPAQUE RECT update.
- *
- * @param context
- *     The rdpContext associated with the current RDP session.
- *
- * @param opaque_rect
- *     The OPAQUE RECT update to handle.
- */
-void guac_rdp_gdi_opaquerect(rdpContext* context,
-        OPAQUE_RECT_ORDER* opaque_rect);
-
-/**
- * Handler called when the remote color palette is changing.
- *
- * @param context
- *     The rdpContext associated with the current RDP session.
- *
- * @param palette
- *     The PALETTE update containing the new palette.
- */
-void guac_rdp_gdi_palette_update(rdpContext* context, PALETTE_UPDATE* palette);
+BOOL guac_rdp_gdi_memblt(rdpContext* context, MEMBLT_ORDER* memblt);
 
 /**
  * Handler called prior to calling the handlers for specific updates when
@@ -124,8 +110,11 @@
  * @param bounds
  *     The clipping rectangle to set, or NULL to remove any applied clipping
  *     rectangle.
+ *
+ * @return
+ *     TRUE if successful, FALSE otherwise.
  */
-void guac_rdp_gdi_set_bounds(rdpContext* context, rdpBounds* bounds);
+BOOL guac_rdp_gdi_set_bounds(rdpContext* context, const rdpBounds* bounds);
 
 /**
  * Handler called when a paint operation is complete. We don't actually
@@ -133,8 +122,11 @@
  *
  * @param context
  *     The rdpContext associated with the current RDP session.
+ *
+ * @return
+ *     TRUE if successful, FALSE otherwise.
  */
-void guac_rdp_gdi_end_paint(rdpContext* context);
+BOOL guac_rdp_gdi_end_paint(rdpContext* context);
 
 /**
  * Handler called when the desktop dimensions change, either from a
@@ -147,7 +139,10 @@
  *
  * @param context
  *     The rdpContext associated with the current RDP session.
+ *
+ * @return
+ *     TRUE if successful, FALSE otherwise.
  */
-void guac_rdp_gdi_desktop_resize(rdpContext* context);
+BOOL guac_rdp_gdi_desktop_resize(rdpContext* context);
 
 #endif
diff --git a/src/protocols/rdp/rdp_glyph.c b/src/protocols/rdp/glyph.c
similarity index 77%
rename from src/protocols/rdp/rdp_glyph.c
rename to src/protocols/rdp/glyph.c
index 8ba1087..d841384 100644
--- a/src/protocols/rdp/rdp_glyph.c
+++ b/src/protocols/rdp/glyph.c
@@ -17,25 +17,16 @@
  * under the License.
  */
 
-#include "config.h"
-
-#include "client.h"
+#include "color.h"
 #include "common/surface.h"
+#include "config.h"
+#include "glyph.h"
 #include "rdp.h"
-#include "rdp_color.h"
-#include "rdp_glyph.h"
-#include "rdp_settings.h"
 
 #include <freerdp/freerdp.h>
 #include <guacamole/client.h>
-
-#ifdef ENABLE_WINPR
 #include <winpr/wtypes.h>
-#else
-#include "compat/winpr-wtypes.h"
-#endif
 
-#include <pthread.h>
 #include <stdint.h>
 #include <stdlib.h>
 
@@ -44,7 +35,7 @@
 #define cairo_format_stride_for_width(format, width) (width*4)
 #endif
 
-void guac_rdp_glyph_new(rdpContext* context, rdpGlyph* glyph) {
+BOOL guac_rdp_glyph_new(rdpContext* context, const rdpGlyph* glyph) {
 
     int x, y, i;
     int stride;
@@ -95,9 +86,15 @@
     ((guac_rdp_glyph*) glyph)->surface = cairo_image_surface_create_for_data(
             image_buffer, CAIRO_FORMAT_ARGB32, width, height, stride);
 
+    return TRUE;
+
 }
 
-void guac_rdp_glyph_draw(rdpContext* context, rdpGlyph* glyph, int x, int y) {
+BOOL guac_rdp_glyph_draw(rdpContext* context, const rdpGlyph* glyph,
+        GLYPH_CALLBACK_INT32 x, GLYPH_CALLBACK_INT32 y,
+        GLYPH_CALLBACK_INT32 w, GLYPH_CALLBACK_INT32 h,
+        GLYPH_CALLBACK_INT32 sx, GLYPH_CALLBACK_INT32 sy,
+        BOOL redundant) {
 
     guac_client* client = ((rdp_freerdp_context*) context)->client;
     guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
@@ -110,6 +107,8 @@
                                (fgcolor & 0x00FF00) >> 8,
                                 fgcolor & 0x0000FF);
 
+    return TRUE;
+
 }
 
 void guac_rdp_glyph_free(rdpContext* context, rdpGlyph* glyph) {
@@ -121,17 +120,26 @@
     cairo_surface_destroy(((guac_rdp_glyph*) glyph)->surface);
     free(image_buffer);
 
+    /* NOTE: FreeRDP-allocated memory for the rdpGlyph will NOT be
+     * automatically released after this free handler is invoked, thus we must
+     * do so manually here */
+
+    free(glyph->aj);
+    free(glyph);
+
 }
 
-void guac_rdp_glyph_begindraw(rdpContext* context,
-        int x, int y, int width, int height, UINT32 fgcolor, UINT32 bgcolor) {
+BOOL guac_rdp_glyph_begindraw(rdpContext* context,
+        GLYPH_CALLBACK_INT32 x, GLYPH_CALLBACK_INT32 y,
+        GLYPH_CALLBACK_INT32 width, GLYPH_CALLBACK_INT32 height,
+        UINT32 fgcolor, UINT32 bgcolor, BOOL redundant) {
 
     guac_client* client = ((rdp_freerdp_context*) context)->client;
     guac_rdp_client* rdp_client =
         (guac_rdp_client*) client->data;
 
     /* Fill background with color if specified */
-    if (width != 0 && height != 0) {
+    if (width != 0 && height != 0 && !redundant) {
 
         /* Convert background color */
         bgcolor = guac_rdp_convert_color(context, bgcolor);
@@ -148,10 +156,15 @@
     /* Convert foreground color */
     rdp_client->glyph_color = guac_rdp_convert_color(context, fgcolor);
 
+    return TRUE;
+
 }
 
-void guac_rdp_glyph_enddraw(rdpContext* context,
-        int x, int y, int width, int height, UINT32 fgcolor, UINT32 bgcolor) {
+BOOL guac_rdp_glyph_enddraw(rdpContext* context,
+        GLYPH_CALLBACK_INT32 x, GLYPH_CALLBACK_INT32 y,
+        GLYPH_CALLBACK_INT32 width, GLYPH_CALLBACK_INT32 height,
+        UINT32 fgcolor, UINT32 bgcolor) {
     /* IGNORE */
+    return TRUE;
 }
 
diff --git a/src/protocols/rdp/rdp_glyph.h b/src/protocols/rdp/glyph.h
similarity index 71%
rename from src/protocols/rdp/rdp_glyph.h
rename to src/protocols/rdp/glyph.h
index 0fe0eef..85f2080 100644
--- a/src/protocols/rdp/rdp_glyph.h
+++ b/src/protocols/rdp/glyph.h
@@ -17,19 +17,28 @@
  * under the License.
  */
 
-
-#ifndef _GUAC_RDP_RDP_GLYPH_H
-#define _GUAC_RDP_RDP_GLYPH_H
+#ifndef GUAC_RDP_GLYPH_H
+#define GUAC_RDP_GLYPH_H
 
 #include "config.h"
 
 #include <cairo/cairo.h>
 #include <freerdp/freerdp.h>
-
-#ifdef ENABLE_WINPR
+#include <freerdp/graphics.h>
 #include <winpr/wtypes.h>
+
+#ifdef FREERDP_GLYPH_CALLBACKS_ACCEPT_INT32
+/**
+ * FreeRDP 2.0.0-rc4 and newer requires INT32 for all integer arguments of
+ * glyph callbacks.
+ */
+#define GLYPH_CALLBACK_INT32 INT32
 #else
-#include "compat/winpr-wtypes.h"
+/**
+ * FreeRDP 2.0.0-rc3 and older requires UINT32 for all integer arguments of
+ * glyph callbacks.
+ */
+#define GLYPH_CALLBACK_INT32 UINT32
 #endif
 
 /**
@@ -58,8 +67,11 @@
  *
  * @param glyph
  *     The glyph to cache.
+ *
+ * @return
+ *     TRUE if successful, FALSE otherwise.
  */
-void guac_rdp_glyph_new(rdpContext* context, rdpGlyph* glyph);
+BOOL guac_rdp_glyph_new(rdpContext* context, const rdpGlyph* glyph);
 
 /**
  * Draws a previously-cached glyph at the given coordinates within the current
@@ -76,8 +88,32 @@
  *
  * @param y
  *     The destination Y coordinate of the upper-left corner of the glyph.
+ *
+ * @param w
+ *     The width of the glyph being drawn.
+ *
+ * @param h
+ *     The height of the glyph being drawn.
+ *
+ * @param sx
+ *     The X coordinare of the upper-left corner of the glyph within the source
+ *     cache surface containing the glyph.
+ *
+ * @param sy
+ *     The Y coordinare of the upper-left corner of the glyph within the source
+ *     cache surface containing the glyph.
+ *
+ * @param redundant
+ *     Whether the background rectangle specified is redundant (transparent).
+ *
+ * @return
+ *     TRUE if successful, FALSE otherwise.
  */
-void guac_rdp_glyph_draw(rdpContext* context, rdpGlyph* glyph, int x, int y);
+BOOL guac_rdp_glyph_draw(rdpContext* context, const rdpGlyph* glyph,
+        GLYPH_CALLBACK_INT32 x, GLYPH_CALLBACK_INT32 y,
+        GLYPH_CALLBACK_INT32 w, GLYPH_CALLBACK_INT32 h,
+        GLYPH_CALLBACK_INT32 sx, GLYPH_CALLBACK_INT32 sy,
+        BOOL redundant);
 
 /**
  * Frees any Guacamole-specific data associated with the given glyph, such that
@@ -125,9 +161,17 @@
  *     colorspace of the RDP session, and may even be a palette index, and must
  *     be translated via guac_rdp_convert_color(). If the background is
  *     transparent, this value is undefined.
+ *
+ * @param redundant
+ *     Whether the background rectangle specified is redundant (transparent).
+ *
+ * @return
+ *     TRUE if successful, FALSE otherwise.
  */
-void guac_rdp_glyph_begindraw(rdpContext* context,
-        int x, int y, int width, int height, UINT32 fgcolor, UINT32 bgcolor);
+BOOL guac_rdp_glyph_begindraw(rdpContext* context,
+        GLYPH_CALLBACK_INT32 x, GLYPH_CALLBACK_INT32 y,
+        GLYPH_CALLBACK_INT32 width, GLYPH_CALLBACK_INT32 height,
+        UINT32 fgcolor, UINT32 bgcolor, BOOL redundant);
 
 /**
  * Called immediately after rendering a series of glyphs. Unlike
@@ -162,8 +206,13 @@
  *     colorspace of the RDP session, and may even be a palette index, and must
  *     be translated via guac_rdp_convert_color(). If the background is
  *     transparent, this value is undefined.
+ *
+ * @return
+ *     TRUE if successful, FALSE otherwise.
  */
-void guac_rdp_glyph_enddraw(rdpContext* context,
-        int x, int y, int width, int height, UINT32 fgcolor, UINT32 bgcolor);
+BOOL guac_rdp_glyph_enddraw(rdpContext* context,
+        GLYPH_CALLBACK_INT32 x, GLYPH_CALLBACK_INT32 y,
+        GLYPH_CALLBACK_INT32 width, GLYPH_CALLBACK_INT32 height,
+        UINT32 fgcolor, UINT32 bgcolor);
 
 #endif
diff --git a/src/protocols/rdp/guac_rdpdr/rdpdr_fs_messages.c b/src/protocols/rdp/guac_rdpdr/rdpdr_fs_messages.c
deleted file mode 100644
index 54d3086..0000000
--- a/src/protocols/rdp/guac_rdpdr/rdpdr_fs_messages.c
+++ /dev/null
@@ -1,529 +0,0 @@
-/*
- * 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 "rdpdr_fs_messages_dir_info.h"
-#include "rdpdr_fs_messages_file_info.h"
-#include "rdpdr_fs_messages.h"
-#include "rdpdr_fs_messages_vol_info.h"
-#include "rdpdr_messages.h"
-#include "rdpdr_service.h"
-#include "rdp_fs.h"
-#include "rdp_status.h"
-#include "unicode.h"
-
-#include <freerdp/utils/svc_plugin.h>
-#include <guacamole/client.h>
-
-#ifdef ENABLE_WINPR
-#include <winpr/stream.h>
-#include <winpr/wtypes.h>
-#else
-#include "compat/winpr-stream.h"
-#include "compat/winpr-wtypes.h"
-#endif
-
-#include <inttypes.h>
-#include <stdint.h>
-#include <stdlib.h>
-#include <string.h>
-
-void guac_rdpdr_fs_process_create(guac_rdpdr_device* device,
-        wStream* input_stream, int completion_id) {
-
-    wStream* output_stream;
-    int file_id;
-
-    int desired_access, file_attributes;
-    int create_disposition, create_options, path_length;
-    char path[GUAC_RDP_FS_MAX_PATH];
-
-    /* Read "create" information */
-    Stream_Read_UINT32(input_stream, desired_access);
-    Stream_Seek_UINT64(input_stream); /* allocation size */
-    Stream_Read_UINT32(input_stream, file_attributes);
-    Stream_Seek_UINT32(input_stream); /* shared access */
-    Stream_Read_UINT32(input_stream, create_disposition);
-    Stream_Read_UINT32(input_stream, create_options);
-    Stream_Read_UINT32(input_stream, path_length);
-
-    /* Convert path to UTF-8 */
-    guac_rdp_utf16_to_utf8(Stream_Pointer(input_stream), path_length/2 - 1,
-            path, sizeof(path));
-
-    /* Open file */
-    file_id = guac_rdp_fs_open((guac_rdp_fs*) device->data, path,
-            desired_access, file_attributes,
-            create_disposition, create_options);
-
-    guac_client_log(device->rdpdr->client, GUAC_LOG_DEBUG,
-            "%s: [file_id=%i] "
-             "desired_access=0x%x, file_attributes=0x%x, "
-             "create_disposition=0x%x, create_options=0x%x, path=\"%s\"",
-             __func__, file_id,
-             desired_access, file_attributes,
-             create_disposition, create_options, path);
-
-    /* If an error occurred, notify server */
-    if (file_id < 0) {
-        guac_client_log(device->rdpdr->client, GUAC_LOG_ERROR,
-                "File open refused (%i): \"%s\"", file_id, path);
-
-        output_stream = guac_rdpdr_new_io_completion(device, completion_id,
-                guac_rdp_fs_get_status(file_id), 5);
-        Stream_Write_UINT32(output_stream, 0); /* fileId */
-        Stream_Write_UINT8(output_stream,  0); /* information */
-    }
-
-    /* Otherwise, open succeeded */
-    else {
-
-        guac_rdp_fs_file* file;
-
-        output_stream = guac_rdpdr_new_io_completion(device, completion_id,
-                STATUS_SUCCESS, 5);
-        Stream_Write_UINT32(output_stream, file_id);    /* fileId */
-        Stream_Write_UINT8(output_stream,  0);          /* information */
-
-        /* Create \Download if it doesn't exist */
-        file = guac_rdp_fs_get_file((guac_rdp_fs*) device->data, file_id);
-        if (file != NULL && strcmp(file->absolute_path, "\\") == 0) {
-            int download_id =
-                guac_rdp_fs_open((guac_rdp_fs*) device->data, "\\Download",
-                    ACCESS_GENERIC_READ, 0,
-                    DISP_FILE_OPEN_IF, FILE_DIRECTORY_FILE);
-
-            if (download_id >= 0)
-                guac_rdp_fs_close((guac_rdp_fs*) device->data, download_id);
-        }
-
-    }
-
-    svc_plugin_send((rdpSvcPlugin*) device->rdpdr, output_stream);
-
-}
-
-void guac_rdpdr_fs_process_read(guac_rdpdr_device* device,
-        wStream* input_stream, int file_id, int completion_id) {
-
-    UINT32 length;
-    UINT64 offset;
-    char* buffer;
-    int bytes_read;
-
-    wStream* output_stream;
-
-    /* Read packet */
-    Stream_Read_UINT32(input_stream, length);
-    Stream_Read_UINT64(input_stream, offset);
-
-    guac_client_log(device->rdpdr->client, GUAC_LOG_DEBUG,
-            "%s: [file_id=%i] length=%i, offset=%" PRIu64,
-             __func__, file_id, length, (uint64_t) offset);
-
-    /* Ensure buffer size does not exceed a safe maximum */
-    if (length > GUAC_RDP_MAX_READ_BUFFER)
-        length = GUAC_RDP_MAX_READ_BUFFER;
-
-    /* Allocate buffer */
-    buffer = malloc(length);
-
-    /* Attempt read */
-    bytes_read = guac_rdp_fs_read((guac_rdp_fs*) device->data, file_id, offset,
-            buffer, length);
-
-    /* If error, return invalid parameter */
-    if (bytes_read < 0) {
-        output_stream = guac_rdpdr_new_io_completion(device, completion_id,
-                guac_rdp_fs_get_status(bytes_read), 4);
-        Stream_Write_UINT32(output_stream, 0); /* Length */
-    }
-
-    /* Otherwise, send bytes read */
-    else {
-        output_stream = guac_rdpdr_new_io_completion(device, completion_id,
-                STATUS_SUCCESS, 4+bytes_read);
-        Stream_Write_UINT32(output_stream, bytes_read);  /* Length */
-        Stream_Write(output_stream, buffer, bytes_read); /* ReadData */
-    }
-
-    svc_plugin_send((rdpSvcPlugin*) device->rdpdr, output_stream);
-    free(buffer);
-
-}
-
-void guac_rdpdr_fs_process_write(guac_rdpdr_device* device,
-        wStream* input_stream, int file_id, int completion_id) {
-
-    UINT32 length;
-    UINT64 offset;
-    int bytes_written;
-
-    wStream* output_stream;
-
-    /* Read packet */
-    Stream_Read_UINT32(input_stream, length);
-    Stream_Read_UINT64(input_stream, offset);
-    Stream_Seek(input_stream, 20); /* Padding */
-
-    guac_client_log(device->rdpdr->client, GUAC_LOG_DEBUG,
-            "%s: [file_id=%i] length=%i, offset=%" PRIu64,
-             __func__, file_id, length, (uint64_t) offset);
-
-    /* Attempt write */
-    bytes_written = guac_rdp_fs_write((guac_rdp_fs*) device->data, file_id,
-            offset, Stream_Pointer(input_stream), length);
-
-    /* If error, return invalid parameter */
-    if (bytes_written < 0) {
-        output_stream = guac_rdpdr_new_io_completion(device, completion_id,
-                guac_rdp_fs_get_status(bytes_written), 5);
-        Stream_Write_UINT32(output_stream, 0); /* Length */
-        Stream_Write_UINT8(output_stream, 0);  /* Padding */
-    }
-
-    /* Otherwise, send success */
-    else {
-        output_stream = guac_rdpdr_new_io_completion(device, completion_id,
-                STATUS_SUCCESS, 5);
-        Stream_Write_UINT32(output_stream, bytes_written); /* Length */
-        Stream_Write_UINT8(output_stream, 0);              /* Padding */
-    }
-
-    svc_plugin_send((rdpSvcPlugin*) device->rdpdr, output_stream);
-
-}
-
-void guac_rdpdr_fs_process_close(guac_rdpdr_device* device,
-        wStream* input_stream, int file_id, int completion_id) {
-
-    wStream* output_stream;
-    guac_rdp_fs_file* file;
-
-    guac_client_log(device->rdpdr->client, GUAC_LOG_DEBUG,
-            "%s: [file_id=%i]",
-            __func__, file_id);
-
-    /* Get file */
-    file = guac_rdp_fs_get_file((guac_rdp_fs*) device->data, file_id);
-    if (file == NULL)
-        return;
-
-    /* If file was written to, and it's in the \Download folder, start stream */
-    if (file->bytes_written > 0 &&
-            strncmp(file->absolute_path, "\\Download\\", 10) == 0) {
-        guac_rdpdr_start_download(device, file->absolute_path);
-        guac_rdp_fs_delete((guac_rdp_fs*) device->data, file_id);
-    }
-
-    /* Close file */
-    guac_rdp_fs_close((guac_rdp_fs*) device->data, file_id);
-
-    output_stream = guac_rdpdr_new_io_completion(device, completion_id,
-            STATUS_SUCCESS, 4);
-    Stream_Write(output_stream, "\0\0\0\0", 4); /* Padding */
-
-    svc_plugin_send((rdpSvcPlugin*) device->rdpdr, output_stream);
-
-}
-
-void guac_rdpdr_fs_process_volume_info(guac_rdpdr_device* device, wStream* input_stream,
-        int file_id, int completion_id) {
-
-    int fs_information_class;
-
-    Stream_Read_UINT32(input_stream, fs_information_class);
-
-    /* Dispatch to appropriate class-specific handler */
-    switch (fs_information_class) {
-
-        case FileFsVolumeInformation:
-            guac_rdpdr_fs_process_query_volume_info(device, input_stream,
-                    file_id, completion_id);
-            break;
-
-        case FileFsSizeInformation:
-            guac_rdpdr_fs_process_query_size_info(device, input_stream,
-                    file_id, completion_id);
-            break;
-
-        case FileFsDeviceInformation:
-            guac_rdpdr_fs_process_query_device_info(device, input_stream,
-                    file_id, completion_id);
-            break;
-
-        case FileFsAttributeInformation:
-            guac_rdpdr_fs_process_query_attribute_info(device, input_stream,
-                    file_id, completion_id);
-            break;
-
-        case FileFsFullSizeInformation:
-            guac_rdpdr_fs_process_query_full_size_info(device, input_stream,
-                    file_id, completion_id);
-            break;
-
-        default:
-            guac_client_log(device->rdpdr->client, GUAC_LOG_INFO,
-                    "Unknown volume information class: 0x%x", fs_information_class);
-    }
-
-}
-
-void guac_rdpdr_fs_process_file_info(guac_rdpdr_device* device, wStream* input_stream,
-        int file_id, int completion_id) {
-
-    int fs_information_class;
-
-    Stream_Read_UINT32(input_stream, fs_information_class);
-
-    /* Dispatch to appropriate class-specific handler */
-    switch (fs_information_class) {
-
-        case FileBasicInformation:
-            guac_rdpdr_fs_process_query_basic_info(device, input_stream,
-                    file_id, completion_id);
-            break;
-
-        case FileStandardInformation:
-            guac_rdpdr_fs_process_query_standard_info(device, input_stream,
-                    file_id, completion_id);
-            break;
-
-        case FileAttributeTagInformation:
-            guac_rdpdr_fs_process_query_attribute_tag_info(device, input_stream,
-                    file_id, completion_id);
-            break;
-
-        default:
-            guac_client_log(device->rdpdr->client, GUAC_LOG_INFO,
-                    "Unknown file information class: 0x%x", fs_information_class);
-    }
-
-}
-
-void guac_rdpdr_fs_process_set_volume_info(guac_rdpdr_device* device,
-        wStream* input_stream, int file_id, int completion_id) {
-
-    wStream* output_stream = guac_rdpdr_new_io_completion(device,
-            completion_id, STATUS_NOT_SUPPORTED, 0);
-
-    guac_client_log(device->rdpdr->client, GUAC_LOG_DEBUG,
-            "%s: [file_id=%i] Set volume info not supported",
-            __func__, file_id);
-
-    svc_plugin_send((rdpSvcPlugin*) device->rdpdr, output_stream);
-
-}
-
-void guac_rdpdr_fs_process_set_file_info(guac_rdpdr_device* device,
-        wStream* input_stream, int file_id, int completion_id) {
-
-    int fs_information_class;
-    int length;
-
-    Stream_Read_UINT32(input_stream, fs_information_class);
-    Stream_Read_UINT32(input_stream, length); /* Length */
-    Stream_Seek(input_stream, 24);            /* Padding */
-
-    /* Dispatch to appropriate class-specific handler */
-    switch (fs_information_class) {
-
-        case FileBasicInformation:
-            guac_rdpdr_fs_process_set_basic_info(device, input_stream,
-                    file_id, completion_id, length);
-            break;
-
-        case FileEndOfFileInformation:
-            guac_rdpdr_fs_process_set_end_of_file_info(device, input_stream,
-                    file_id, completion_id, length);
-            break;
-
-        case FileDispositionInformation:
-            guac_rdpdr_fs_process_set_disposition_info(device, input_stream,
-                    file_id, completion_id, length);
-            break;
-
-        case FileRenameInformation:
-            guac_rdpdr_fs_process_set_rename_info(device, input_stream,
-                    file_id, completion_id, length);
-            break;
-
-        case FileAllocationInformation:
-            guac_rdpdr_fs_process_set_allocation_info(device, input_stream,
-                    file_id, completion_id, length);
-            break;
-
-        default:
-            guac_client_log(device->rdpdr->client, GUAC_LOG_INFO,
-                    "Unknown file information class: 0x%x",
-                    fs_information_class);
-    }
-
-}
-
-void guac_rdpdr_fs_process_device_control(guac_rdpdr_device* device,
-        wStream* input_stream, int file_id, int completion_id) {
-
-    wStream* output_stream = guac_rdpdr_new_io_completion(device,
-            completion_id, STATUS_INVALID_PARAMETER, 4);
-
-    guac_client_log(device->rdpdr->client, GUAC_LOG_DEBUG,
-            "%s: [file_id=%i] IGNORED",
-            __func__, file_id);
-
-    /* No content for now */
-    Stream_Write_UINT32(output_stream, 0);
-
-    svc_plugin_send((rdpSvcPlugin*) device->rdpdr, output_stream);
-
-}
-
-void guac_rdpdr_fs_process_notify_change_directory(guac_rdpdr_device* device,
-        wStream* input_stream, int file_id, int completion_id) {
-
-    guac_client_log(device->rdpdr->client, GUAC_LOG_DEBUG,
-            "%s: [file_id=%i] Not implemented",
-            __func__, file_id);
-
-}
-
-void guac_rdpdr_fs_process_query_directory(guac_rdpdr_device* device, wStream* input_stream,
-        int file_id, int completion_id) {
-
-    wStream* output_stream;
-
-    guac_rdp_fs_file* file;
-    int fs_information_class, initial_query;
-    int path_length;
-
-    const char* entry_name;
-
-    /* Get file */
-    file = guac_rdp_fs_get_file((guac_rdp_fs*) device->data, file_id);
-    if (file == NULL)
-        return;
-
-    /* Read main header */
-    Stream_Read_UINT32(input_stream, fs_information_class);
-    Stream_Read_UINT8(input_stream,  initial_query);
-    Stream_Read_UINT32(input_stream, path_length);
-
-    /* If this is the first query, the path is included after padding */
-    if (initial_query) {
-
-        Stream_Seek(input_stream, 23);       /* Padding */
-
-        /* Convert path to UTF-8 */
-        guac_rdp_utf16_to_utf8(Stream_Pointer(input_stream), path_length/2 - 1,
-                file->dir_pattern, sizeof(file->dir_pattern));
-
-    }
-
-    guac_client_log(device->rdpdr->client, GUAC_LOG_DEBUG,
-            "%s: [file_id=%i] initial_query=%i, dir_pattern=\"%s\"",
-             __func__, file_id, initial_query, file->dir_pattern);
-
-    /* Find first matching entry in directory */
-    while ((entry_name = guac_rdp_fs_read_dir((guac_rdp_fs*) device->data,
-                    file_id)) != NULL) {
-
-        /* Convert to absolute path */
-        char entry_path[GUAC_RDP_FS_MAX_PATH];
-        if (guac_rdp_fs_convert_path(file->absolute_path,
-                    entry_name, entry_path) == 0) {
-
-            int entry_file_id;
-
-            /* Pattern defined and match fails, continue with next file */
-            if (guac_rdp_fs_matches(entry_path, file->dir_pattern))
-                continue;
-
-            /* Open directory entry */
-            entry_file_id = guac_rdp_fs_open((guac_rdp_fs*) device->data,
-                    entry_path, ACCESS_FILE_READ_DATA, 0, DISP_FILE_OPEN, 0);
-
-            if (entry_file_id >= 0) {
-
-                /* Dispatch to appropriate class-specific handler */
-                switch (fs_information_class) {
-
-                    case FileDirectoryInformation:
-                        guac_rdpdr_fs_process_query_directory_info(device,
-                                entry_name, entry_file_id, completion_id);
-                        break;
-
-                    case FileFullDirectoryInformation:
-                        guac_rdpdr_fs_process_query_full_directory_info(device,
-                                entry_name, entry_file_id, completion_id);
-                        break;
-
-                    case FileBothDirectoryInformation:
-                        guac_rdpdr_fs_process_query_both_directory_info(device,
-                                entry_name, entry_file_id, completion_id);
-                        break;
-
-                    case FileNamesInformation:
-                        guac_rdpdr_fs_process_query_names_info(device,
-                                entry_name, entry_file_id, completion_id);
-                        break;
-
-                    default:
-                        guac_client_log(device->rdpdr->client, GUAC_LOG_INFO,
-                                "Unknown dir information class: 0x%x",
-                                fs_information_class);
-                }
-
-                guac_rdp_fs_close((guac_rdp_fs*) device->data, entry_file_id);
-                return;
-
-            } /* end if file exists */
-        } /* end if path valid */
-    } /* end if entry exists */
-
-    /*
-     * Handle errors as a lack of files.
-     */
-
-    output_stream = guac_rdpdr_new_io_completion(device, completion_id,
-            STATUS_NO_MORE_FILES, 5);
-
-    Stream_Write_UINT32(output_stream, 0); /* Length */
-    Stream_Write_UINT8(output_stream, 0);  /* Padding */
-
-    svc_plugin_send((rdpSvcPlugin*) device->rdpdr, output_stream);
-
-}
-
-void guac_rdpdr_fs_process_lock_control(guac_rdpdr_device* device, wStream* input_stream,
-        int file_id, int completion_id) {
-
-    wStream* output_stream = guac_rdpdr_new_io_completion(device,
-            completion_id, STATUS_NOT_SUPPORTED, 5);
-
-    guac_client_log(device->rdpdr->client, GUAC_LOG_DEBUG,
-            "%s: [file_id=%i] Lock not supported",
-            __func__, file_id);
-
-    Stream_Zero(output_stream, 5); /* Padding */
-
-    svc_plugin_send((rdpSvcPlugin*) device->rdpdr, output_stream);
-
-}
-
diff --git a/src/protocols/rdp/guac_rdpdr/rdpdr_fs_messages.h b/src/protocols/rdp/guac_rdpdr/rdpdr_fs_messages.h
deleted file mode 100644
index 4e484b4..0000000
--- a/src/protocols/rdp/guac_rdpdr/rdpdr_fs_messages.h
+++ /dev/null
@@ -1,129 +0,0 @@
-/*
- * 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_RDPDR_FS_MESSAGES_H
-#define __GUAC_RDPDR_FS_MESSAGES_H
-
-/**
- * Handlers for core drive I/O requests. Requests handled here may be simple
- * messages handled directly, or more complex multi-type messages handled
- * elsewhere.
- *
- * @file rdpdr_fs_messages.h
- */
-
-#include "config.h"
-
-#include "rdpdr_service.h"
-
-#ifdef ENABLE_WINPR
-#include <winpr/stream.h>
-#else
-#include "compat/winpr-stream.h"
-#endif
-
-/**
- * Handles a Server Create Drive Request. Despite its name, this request opens
- * a file.
- */
-void guac_rdpdr_fs_process_create(guac_rdpdr_device* device,
-        wStream* input_stream, int completion_id);
-
-/**
- * Handles a Server Close Drive Reqiest. This request closes an open file.
- */
-void guac_rdpdr_fs_process_close(guac_rdpdr_device* device,
-        wStream* input_stream, int file_id, int completion_id);
-
-/**
- * Handles a Server Drive Read Request. This request reads from a file.
- */
-void guac_rdpdr_fs_process_read(guac_rdpdr_device* device,
-        wStream* input_stream, int file_id, int completion_id);
-
-/**
- * Handles a Server Drive Write Request. This request writes to a file.
- */
-void guac_rdpdr_fs_process_write(guac_rdpdr_device* device,
-        wStream* input_stream, int file_id, int completion_id);
-
-/**
- * Handles a Server Drive Control Request. This request handles one of any
- * number of Windows FSCTL_* control functions.
- */
-void guac_rdpdr_fs_process_device_control(guac_rdpdr_device* device, wStream* input_stream,
-        int file_id, int completion_id);
-
-/**
- * Handles a Server Drive Query Volume Information Request. This request
- * queries information about the redirected volume (drive). This request
- * has several query types which have their own handlers defined in a
- * separate file.
- */
-void guac_rdpdr_fs_process_volume_info(guac_rdpdr_device* device, wStream* input_stream,
-        int file_id, int completion_id);
-
-/**
- * Handles a Server Drive Set Volume Information Request. Currently, this
- * RDPDR implementation does not support setting of volume information.
- */
-void guac_rdpdr_fs_process_set_volume_info(guac_rdpdr_device* device, wStream* input_stream,
-        int file_id, int completion_id);
-
-/**
- * Handles a Server Drive Query Information Request. This request queries
- * information about a specific file. This request has several query types
- * which have their own handlers defined in a separate file.
- */
-void guac_rdpdr_fs_process_file_info(guac_rdpdr_device* device, wStream* input_stream,
-        int file_id, int completion_id);
-
-/**
- * Handles a Server Drive Set Information Request. This request sets
- * information about a specific file. Currently, this RDPDR implementation does
- * not support setting of file information.
- */
-void guac_rdpdr_fs_process_set_file_info(guac_rdpdr_device* device, wStream* input_stream,
-        int file_id, int completion_id);
-
-/**
- * Handles a Server Drive Query Directory Request. This request queries
- * information about a specific directory. This request has several query types
- * which have their own handlers defined in a separate file.
- */
-void guac_rdpdr_fs_process_query_directory(guac_rdpdr_device* device, wStream* input_stream,
-        int file_id, int completion_id);
-
-/**
- * Handles a Server Drive NotifyChange Directory Request. This request requests
- * directory change notification.
- */
-void guac_rdpdr_fs_process_notify_change_directory(guac_rdpdr_device* device,
-        wStream* input_stream, int file_id, int completion_id);
-
-/**
- * Handles a Server Drive Lock Control Request. This request locks or unlocks
- * portions of a file.
- */
-void guac_rdpdr_fs_process_lock_control(guac_rdpdr_device* device, wStream* input_stream,
-        int file_id, int completion_id);
-
-#endif
-
diff --git a/src/protocols/rdp/guac_rdpdr/rdpdr_fs_messages_dir_info.h b/src/protocols/rdp/guac_rdpdr/rdpdr_fs_messages_dir_info.h
deleted file mode 100644
index 06e2074..0000000
--- a/src/protocols/rdp/guac_rdpdr/rdpdr_fs_messages_dir_info.h
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * 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_RDPDR_FS_MESSAGES_DIR_INFO_H
-#define __GUAC_RDPDR_FS_MESSAGES_DIR_INFO_H
-
-/**
- * Handlers for directory queries received over the RDPDR channel via the
- * IRP_MJ_DIRECTORY_CONTROL major function and the IRP_MN_QUERY_DIRECTORY minor
- * function.
- *
- * @file rdpdr_fs_messages_dir_info.h
- */
-
-#include "config.h"
-
-#include "rdpdr_service.h"
-
-#ifdef ENABLE_WINPR
-#include <winpr/stream.h>
-#else
-#include "compat/winpr-stream.h"
-#endif
-
-/**
- * Processes a query request for FileDirectoryInformation. From the
- * documentation this is "defined as the file's name, time stamp, and size, or its
- * attributes."
- */
-void guac_rdpdr_fs_process_query_directory_info(guac_rdpdr_device* device,
-        const char* entry_name, int file_id, int completion_id);
-
-/**
- * Processes a query request for FileFullDirectoryInformation. From the
- * documentation, this is "defined as all the basic information, plus extended
- * attribute size."
- */
-void guac_rdpdr_fs_process_query_full_directory_info(guac_rdpdr_device* device,
-        const char* entry_name, int file_id, int completion_id);
-
-/**
- * Processes a query request for FileBothDirectoryInformation. From the
- * documentation, this absurdly-named request is "basic information plus
- * extended attribute size and short name about a file or directory."
- */
-void guac_rdpdr_fs_process_query_both_directory_info(guac_rdpdr_device* device,
-        const char* entry_name, int file_id, int completion_id);
-
-/**
- * Processes a query request for FileNamesInformation. From the documentation,
- * this is "detailed information on the names of files in a directory."
- */
-void guac_rdpdr_fs_process_query_names_info(guac_rdpdr_device* device,
-        const char* entry_name, int file_id, int completion_id);
-
-#endif
-
diff --git a/src/protocols/rdp/guac_rdpdr/rdpdr_fs_messages_file_info.c b/src/protocols/rdp/guac_rdpdr/rdpdr_fs_messages_file_info.c
deleted file mode 100644
index b23e7bd..0000000
--- a/src/protocols/rdp/guac_rdpdr/rdpdr_fs_messages_file_info.c
+++ /dev/null
@@ -1,288 +0,0 @@
-/*
- * 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 "rdpdr_service.h"
-#include "rdp_fs.h"
-#include "rdp_status.h"
-#include "unicode.h"
-
-#include <freerdp/utils/svc_plugin.h>
-
-#ifdef ENABLE_WINPR
-#include <winpr/stream.h>
-#include <winpr/wtypes.h>
-#else
-#include "compat/winpr-stream.h"
-#include "compat/winpr-wtypes.h"
-#endif
-
-#include <inttypes.h>
-#include <stdint.h>
-#include <string.h>
-
-void guac_rdpdr_fs_process_query_basic_info(guac_rdpdr_device* device, wStream* input_stream,
-        int file_id, int completion_id) {
-
-    wStream* output_stream;
-    guac_rdp_fs_file* file;
-
-    /* Get file */
-    file = guac_rdp_fs_get_file((guac_rdp_fs*) device->data, file_id);
-    if (file == NULL)
-        return;
-
-    guac_client_log(device->rdpdr->client, GUAC_LOG_DEBUG,
-            "%s: [file_id=%i]",
-            __func__, file_id);
-
-    output_stream = guac_rdpdr_new_io_completion(device, completion_id,
-            STATUS_SUCCESS, 40);
-
-    Stream_Write_UINT32(output_stream, 36);
-    Stream_Write_UINT64(output_stream, file->ctime);      /* CreationTime   */
-    Stream_Write_UINT64(output_stream, file->atime);      /* LastAccessTime */
-    Stream_Write_UINT64(output_stream, file->mtime);      /* LastWriteTime  */
-    Stream_Write_UINT64(output_stream, file->mtime);      /* ChangeTime     */
-    Stream_Write_UINT32(output_stream, file->attributes); /* FileAttributes */
-
-    /* Reserved field must not be sent */
-
-    svc_plugin_send((rdpSvcPlugin*) device->rdpdr, output_stream);
-
-}
-
-void guac_rdpdr_fs_process_query_standard_info(guac_rdpdr_device* device, wStream* input_stream,
-        int file_id, int completion_id) {
-
-    wStream* output_stream;
-    guac_rdp_fs_file* file;
-    BOOL is_directory = FALSE;
-
-    /* Get file */
-    file = guac_rdp_fs_get_file((guac_rdp_fs*) device->data, file_id);
-    if (file == NULL)
-        return;
-
-    guac_client_log(device->rdpdr->client, GUAC_LOG_DEBUG,
-            "%s: [file_id=%i]",
-            __func__, file_id);
-
-    if (file->attributes & FILE_ATTRIBUTE_DIRECTORY)
-        is_directory = TRUE;
-
-    output_stream = guac_rdpdr_new_io_completion(device, completion_id,
-            STATUS_SUCCESS, 26);
-
-    Stream_Write_UINT32(output_stream, 22);
-    Stream_Write_UINT64(output_stream, file->size);   /* AllocationSize */
-    Stream_Write_UINT64(output_stream, file->size);   /* EndOfFile      */
-    Stream_Write_UINT32(output_stream, 1);            /* NumberOfLinks  */
-    Stream_Write_UINT8(output_stream,  0);            /* DeletePending  */
-    Stream_Write_UINT8(output_stream,  is_directory); /* Directory      */
-
-    /* Reserved field must not be sent */
-
-    svc_plugin_send((rdpSvcPlugin*) device->rdpdr, output_stream);
-
-}
-
-void guac_rdpdr_fs_process_query_attribute_tag_info(guac_rdpdr_device* device,
-        wStream* input_stream, int file_id, int completion_id) {
-
-    wStream* output_stream;
-    guac_rdp_fs_file* file;
-
-    /* Get file */
-    file = guac_rdp_fs_get_file((guac_rdp_fs*) device->data, file_id);
-    if (file == NULL)
-        return;
-
-    guac_client_log(device->rdpdr->client, GUAC_LOG_DEBUG,
-            "%s: [file_id=%i]",
-            __func__, file_id);
-
-    output_stream = guac_rdpdr_new_io_completion(device, completion_id,
-            STATUS_SUCCESS, 12);
-
-    Stream_Write_UINT32(output_stream, 8);
-    Stream_Write_UINT32(output_stream, file->attributes); /* FileAttributes */
-    Stream_Write_UINT32(output_stream, 0);                /* ReparseTag */
-
-    /* Reserved field must not be sent */
-
-    svc_plugin_send((rdpSvcPlugin*) device->rdpdr, output_stream);
-
-}
-
-void guac_rdpdr_fs_process_set_rename_info(guac_rdpdr_device* device,
-        wStream* input_stream, int file_id, int completion_id, int length) {
-
-    int result;
-    int filename_length;
-    wStream* output_stream;
-    char destination_path[GUAC_RDP_FS_MAX_PATH];
-
-    /* Read structure */
-    Stream_Seek_UINT8(input_stream); /* ReplaceIfExists */
-    Stream_Seek_UINT8(input_stream); /* RootDirectory */
-    Stream_Read_UINT32(input_stream, filename_length); /* FileNameLength */
-
-    /* Convert name to UTF-8 */
-    guac_rdp_utf16_to_utf8(Stream_Pointer(input_stream), filename_length/2,
-            destination_path, sizeof(destination_path));
-
-    guac_client_log(device->rdpdr->client, GUAC_LOG_DEBUG,
-            "%s: [file_id=%i] destination_path=\"%s\"",
-            __func__, file_id, destination_path);
-
-    /* If file moving to \Download folder, start stream, do not move */
-    if (strncmp(destination_path, "\\Download\\", 10) == 0) {
-
-        guac_rdp_fs_file* file;
-
-        /* Get file */
-        file = guac_rdp_fs_get_file((guac_rdp_fs*) device->data, file_id);
-        if (file == NULL)
-            return;
-
-        /* Initiate download, pretend move succeeded */
-        guac_rdpdr_start_download(device, file->absolute_path);
-        output_stream = guac_rdpdr_new_io_completion(device,
-                completion_id, STATUS_SUCCESS, 4);
-
-    }
-
-    /* Otherwise, rename as requested */
-    else {
-
-        result = guac_rdp_fs_rename((guac_rdp_fs*) device->data, file_id,
-                destination_path);
-        if (result < 0)
-            output_stream = guac_rdpdr_new_io_completion(device,
-                    completion_id, guac_rdp_fs_get_status(result), 4);
-        else
-            output_stream = guac_rdpdr_new_io_completion(device,
-                    completion_id, STATUS_SUCCESS, 4);
-
-    }
-
-    Stream_Write_UINT32(output_stream, length);
-    svc_plugin_send((rdpSvcPlugin*) device->rdpdr, output_stream);
-
-}
-
-void guac_rdpdr_fs_process_set_allocation_info(guac_rdpdr_device* device,
-        wStream* input_stream, int file_id, int completion_id, int length) {
-
-    int result;
-    UINT64 size;
-    wStream* output_stream;
-
-    /* Read new size */
-    Stream_Read_UINT64(input_stream, size); /* AllocationSize */
-
-    guac_client_log(device->rdpdr->client, GUAC_LOG_DEBUG,
-            "%s: [file_id=%i] size=%" PRIu64,
-            __func__, file_id, (uint64_t) size);
-
-    /* Truncate file */
-    result = guac_rdp_fs_truncate((guac_rdp_fs*) device->data, file_id, size);
-    if (result < 0)
-        output_stream = guac_rdpdr_new_io_completion(device,
-                completion_id, guac_rdp_fs_get_status(result), 4);
-    else
-        output_stream = guac_rdpdr_new_io_completion(device,
-                completion_id, STATUS_SUCCESS, 4);
-
-    Stream_Write_UINT32(output_stream, length);
-    svc_plugin_send((rdpSvcPlugin*) device->rdpdr, output_stream);
-
-}
-
-void guac_rdpdr_fs_process_set_disposition_info(guac_rdpdr_device* device,
-        wStream* input_stream, int file_id, int completion_id, int length) {
-
-    wStream* output_stream;
-
-    /* Delete file */
-    int result = guac_rdp_fs_delete((guac_rdp_fs*) device->data, file_id);
-    if (result < 0)
-        output_stream = guac_rdpdr_new_io_completion(device,
-                completion_id, guac_rdp_fs_get_status(result), 4);
-    else
-        output_stream = guac_rdpdr_new_io_completion(device,
-                completion_id, STATUS_SUCCESS, 4);
-
-    guac_client_log(device->rdpdr->client, GUAC_LOG_DEBUG,
-            "%s: [file_id=%i]",
-            __func__, file_id);
-
-    Stream_Write_UINT32(output_stream, length);
-
-    svc_plugin_send((rdpSvcPlugin*) device->rdpdr, output_stream);
-
-}
-
-void guac_rdpdr_fs_process_set_end_of_file_info(guac_rdpdr_device* device,
-        wStream* input_stream, int file_id, int completion_id, int length) {
-
-    int result;
-    UINT64 size;
-    wStream* output_stream;
-
-    /* Read new size */
-    Stream_Read_UINT64(input_stream, size); /* AllocationSize */
-
-    guac_client_log(device->rdpdr->client, GUAC_LOG_DEBUG,
-            "%s: [file_id=%i] size=%" PRIu64,
-            __func__, file_id, (uint64_t) size);
-
-    /* Truncate file */
-    result = guac_rdp_fs_truncate((guac_rdp_fs*) device->data, file_id, size);
-    if (result < 0)
-        output_stream = guac_rdpdr_new_io_completion(device,
-                completion_id, guac_rdp_fs_get_status(result), 4);
-    else
-        output_stream = guac_rdpdr_new_io_completion(device,
-                completion_id, STATUS_SUCCESS, 4);
-
-    Stream_Write_UINT32(output_stream, length);
-    svc_plugin_send((rdpSvcPlugin*) device->rdpdr, output_stream);
-
-}
-
-void guac_rdpdr_fs_process_set_basic_info(guac_rdpdr_device* device,
-        wStream* input_stream, int file_id, int completion_id, int length) {
-
-    wStream* output_stream = guac_rdpdr_new_io_completion(device,
-            completion_id, STATUS_SUCCESS, 4);
-
-    /* Currently do nothing, just respond */
-    Stream_Write_UINT32(output_stream, length);
-
-    guac_client_log(device->rdpdr->client, GUAC_LOG_DEBUG,
-            "%s: [file_id=%i] IGNORED",
-            __func__, file_id);
-
-    svc_plugin_send((rdpSvcPlugin*) device->rdpdr, output_stream);
-
-}
-
diff --git a/src/protocols/rdp/guac_rdpdr/rdpdr_fs_messages_file_info.h b/src/protocols/rdp/guac_rdpdr/rdpdr_fs_messages_file_info.h
deleted file mode 100644
index aa53ead..0000000
--- a/src/protocols/rdp/guac_rdpdr/rdpdr_fs_messages_file_info.h
+++ /dev/null
@@ -1,101 +0,0 @@
-/*
- * 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_RDPDR_FS_MESSAGES_FILE_INFO_H
-#define __GUAC_RDPDR_FS_MESSAGES_FILE_INFO_H
-
-/**
- * Handlers for file queries received over the RDPDR channel via the
- * IRP_MJ_QUERY_INFORMATION major function.
- *
- * @file rdpdr_fs_messages_file_info.h
- */
-
-#include "config.h"
-
-#include "rdpdr_service.h"
-
-#ifdef ENABLE_WINPR
-#include <winpr/stream.h>
-#else
-#include "compat/winpr-stream.h"
-#endif
-
-/**
- * Processes a query for FileBasicInformation. From the documentation, this is
- * "used to query a file for the times of creation, last access, last write,
- * and change, in addition to file attribute information."
- */
-void guac_rdpdr_fs_process_query_basic_info(guac_rdpdr_device* device, wStream* input_stream,
-        int file_id, int completion_id);
-
-/**
- * Processes a query for FileStandardInformation. From the documentation, this
- * is "used to query for file information such as allocation size, end-of-file
- * position, and number of links."
- */
-void guac_rdpdr_fs_process_query_standard_info(guac_rdpdr_device* device, wStream* input_stream,
-        int file_id, int completion_id);
-
-/**
- * Processes a query for FileAttributeTagInformation. From the documentation
- * this is "used to query for file attribute and reparse tag information."
- */
-void guac_rdpdr_fs_process_query_attribute_tag_info(guac_rdpdr_device* device,
-        wStream* input_stream, int file_id, int completion_id);
-
-/**
- * Process a set operation for FileRenameInformation. From the documentation,
- * this operation is used to rename a file.
- */
-void guac_rdpdr_fs_process_set_rename_info(guac_rdpdr_device* device,
-        wStream* input_stream, int file_id, int completion_id, int length);
-
-/**
- * Process a set operation for FileAllocationInformation. From the
- * documentation, this operation is used to set a file's allocation size.
- */
-void guac_rdpdr_fs_process_set_allocation_info(guac_rdpdr_device* device,
-        wStream* input_stream, int file_id, int completion_id, int length);
-
-/**
- * Process a set operation for FileDispositionInformation. From the
- * documentation, this operation is used to mark a file for deletion.
- */
-void guac_rdpdr_fs_process_set_disposition_info(guac_rdpdr_device* device,
-        wStream* input_stream, int file_id, int completion_id, int length);
-
-/**
- * Process a set operation for FileEndOfFileInformation. From the
- * documentation, this operation is used "to set end-of-file information for
- * a file."
- */
-void guac_rdpdr_fs_process_set_end_of_file_info(guac_rdpdr_device* device,
-        wStream* input_stream, int file_id, int completion_id, int length);
-
-/**
- * Process a set operation for FileBasicInformation. From the documentation,
- * this is "used to set file information such as the times of creation, last
- * access, last write, and change, in addition to file attributes."
- */
-void guac_rdpdr_fs_process_set_basic_info(guac_rdpdr_device* device,
-        wStream* input_stream, int file_id, int completion_id, int length);
-
-#endif
diff --git a/src/protocols/rdp/guac_rdpdr/rdpdr_messages.c b/src/protocols/rdp/guac_rdpdr/rdpdr_messages.c
deleted file mode 100644
index 66626ad..0000000
--- a/src/protocols/rdp/guac_rdpdr/rdpdr_messages.c
+++ /dev/null
@@ -1,286 +0,0 @@
-/*
- * 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 "rdp.h"
-#include "rdpdr_messages.h"
-#include "rdpdr_service.h"
-#include "unicode.h"
-
-#include <freerdp/utils/svc_plugin.h>
-#include <guacamole/client.h>
-#include <guacamole/unicode.h>
-
-#ifdef ENABLE_WINPR
-#include <winpr/stream.h>
-#else
-#include "compat/winpr-stream.h"
-#endif
-
-#include <stdlib.h>
-#include <string.h>
-
-static void guac_rdpdr_send_client_announce_reply(guac_rdpdrPlugin* rdpdr,
-        unsigned int major, unsigned int minor, unsigned int client_id) {
-
-    wStream* output_stream = Stream_New(NULL, 12);
-
-    /* Write header */
-    Stream_Write_UINT16(output_stream, RDPDR_CTYP_CORE);
-    Stream_Write_UINT16(output_stream, PAKID_CORE_CLIENTID_CONFIRM);
-
-    /* Write content */
-    Stream_Write_UINT16(output_stream, major);
-    Stream_Write_UINT16(output_stream, minor);
-    Stream_Write_UINT32(output_stream, client_id);
-
-    svc_plugin_send((rdpSvcPlugin*) rdpdr, output_stream);
-
-}
-
-static void guac_rdpdr_send_client_name_request(guac_rdpdrPlugin* rdpdr, const char* name) {
-
-    int name_bytes = strlen(name) + 1;
-    wStream* output_stream = Stream_New(NULL, 16 + name_bytes);
-
-    /* Write header */
-    Stream_Write_UINT16(output_stream, RDPDR_CTYP_CORE);
-    Stream_Write_UINT16(output_stream, PAKID_CORE_CLIENT_NAME);
-
-    /* Write content */
-    Stream_Write_UINT32(output_stream, 0); /* ASCII */
-    Stream_Write_UINT32(output_stream, 0); /* 0 required by RDPDR spec */
-    Stream_Write_UINT32(output_stream, name_bytes);
-    Stream_Write(output_stream, name, name_bytes);
-
-    svc_plugin_send((rdpSvcPlugin*) rdpdr, output_stream);
-
-}
-
-static void guac_rdpdr_send_client_capability(guac_rdpdrPlugin* rdpdr) {
-
-    wStream* output_stream = Stream_New(NULL, 256);
-    guac_client_log(rdpdr->client, GUAC_LOG_INFO, "Sending capabilities...");
-
-    /* Write header */
-    Stream_Write_UINT16(output_stream, RDPDR_CTYP_CORE);
-    Stream_Write_UINT16(output_stream, PAKID_CORE_CLIENT_CAPABILITY);
-
-    /* Capability count + padding */
-    Stream_Write_UINT16(output_stream, 3);
-    Stream_Write_UINT16(output_stream, 0); /* Padding */
-
-    /* General capability header */
-    Stream_Write_UINT16(output_stream, CAP_GENERAL_TYPE);
-    Stream_Write_UINT16(output_stream, 44);
-    Stream_Write_UINT32(output_stream, GENERAL_CAPABILITY_VERSION_02);
-
-    /* General capability data */
-    Stream_Write_UINT32(output_stream, GUAC_OS_TYPE);          /* osType - required to be ignored */
-    Stream_Write_UINT32(output_stream, 0);                     /* osVersion */
-    Stream_Write_UINT16(output_stream, RDP_CLIENT_MAJOR_ALL);  /* protocolMajor */
-    Stream_Write_UINT16(output_stream, RDP_CLIENT_MINOR_5_2);  /* protocolMinor */
-    Stream_Write_UINT32(output_stream, 0xFFFF);                /* ioCode1 */
-    Stream_Write_UINT32(output_stream, 0);                     /* ioCode2 */
-    Stream_Write_UINT32(output_stream,
-                                      RDPDR_DEVICE_REMOVE_PDUS
-                                    | RDPDR_CLIENT_DISPLAY_NAME
-                                    | RDPDR_USER_LOGGEDON_PDU); /* extendedPDU */
-    Stream_Write_UINT32(output_stream, 0);                      /* extraFlags1 */
-    Stream_Write_UINT32(output_stream, 0);                      /* extraFlags2 */
-    Stream_Write_UINT32(output_stream, 0);                      /* SpecialTypeDeviceCap */
-
-    /* Printer support header */
-    Stream_Write_UINT16(output_stream, CAP_PRINTER_TYPE);
-    Stream_Write_UINT16(output_stream, 8);
-    Stream_Write_UINT32(output_stream, PRINT_CAPABILITY_VERSION_01);
-
-    /* Drive support header */
-    Stream_Write_UINT16(output_stream, CAP_DRIVE_TYPE);
-    Stream_Write_UINT16(output_stream, 8);
-    Stream_Write_UINT32(output_stream, DRIVE_CAPABILITY_VERSION_02);
-
-    svc_plugin_send((rdpSvcPlugin*) rdpdr, output_stream);
-    guac_client_log(rdpdr->client, GUAC_LOG_INFO, "Capabilities sent.");
-
-}
-
-static void guac_rdpdr_send_client_device_list_announce_request(guac_rdpdrPlugin* rdpdr) {
-
-    /* Calculate number of bytes needed for the stream */
-    int streamBytes = 16;
-    for (int i=0; i < rdpdr->devices_registered; i++)
-        streamBytes += rdpdr->devices[i].device_announce_len;
-
-    /* Allocate the stream */
-    wStream* output_stream = Stream_New(NULL, streamBytes);
-
-    /* Write header */
-    Stream_Write_UINT16(output_stream, RDPDR_CTYP_CORE);
-    Stream_Write_UINT16(output_stream, PAKID_CORE_DEVICELIST_ANNOUNCE);
-
-    /* Get the stream for each of the devices. */
-    Stream_Write_UINT32(output_stream, rdpdr->devices_registered);
-    for (int i=0; i<rdpdr->devices_registered; i++) {
-        
-        Stream_Write(output_stream,
-                Stream_Buffer(rdpdr->devices[i].device_announce),
-                rdpdr->devices[i].device_announce_len);
-
-        guac_client_log(rdpdr->client, GUAC_LOG_INFO, "Registered device %i (%s)",
-                rdpdr->devices[i].device_id, rdpdr->devices[i].device_name);
-        
-    }
-
-    svc_plugin_send((rdpSvcPlugin*) rdpdr, output_stream);
-    guac_client_log(rdpdr->client, GUAC_LOG_INFO, "All supported devices sent.");
-
-}
-
-void guac_rdpdr_process_server_announce(guac_rdpdrPlugin* rdpdr,
-        wStream* input_stream) {
-
-    unsigned int major, minor, client_id;
-
-    Stream_Read_UINT16(input_stream, major);
-    Stream_Read_UINT16(input_stream, minor);
-    Stream_Read_UINT32(input_stream, client_id);
-
-    /* Must choose own client ID if minor not >= 12 */
-    if (minor < 12)
-        client_id = random() & 0xFFFF;
-
-    guac_client_log(rdpdr->client, GUAC_LOG_INFO, "Connected to RDPDR %u.%u as client 0x%04x", major, minor, client_id);
-
-    /* Respond to announce */
-    guac_rdpdr_send_client_announce_reply(rdpdr, major, minor, client_id);
-
-    /* Name request */
-    guac_rdpdr_send_client_name_request(rdpdr, ((guac_rdp_client *)rdpdr->client->data)->settings->client_name);
-
-}
-
-void guac_rdpdr_process_clientid_confirm(guac_rdpdrPlugin* rdpdr, wStream* input_stream) {
-    guac_client_log(rdpdr->client, GUAC_LOG_INFO, "Client ID confirmed");
-}
-
-void guac_rdpdr_process_device_reply(guac_rdpdrPlugin* rdpdr, wStream* input_stream) {
-
-    unsigned int device_id, ntstatus;
-    int severity, c, n, facility, code;
-
-    Stream_Read_UINT32(input_stream, device_id);
-    Stream_Read_UINT32(input_stream, ntstatus);
-
-    severity = (ntstatus & 0xC0000000) >> 30;
-    c        = (ntstatus & 0x20000000) >> 29;
-    n        = (ntstatus & 0x10000000) >> 28;
-    facility = (ntstatus & 0x0FFF0000) >> 16;
-    code     =  ntstatus & 0x0000FFFF;
-
-    /* Log error / information */
-    if (device_id < rdpdr->devices_registered) {
-
-        if (severity == 0x0)
-            guac_client_log(rdpdr->client, GUAC_LOG_INFO, "Device %i (%s) connected successfully",
-                    device_id, rdpdr->devices[device_id].device_name);
-
-        else
-            guac_client_log(rdpdr->client, GUAC_LOG_ERROR, "Problem connecting device %i (%s): "
-                    "severity=0x%x, c=0x%x, n=0x%x, facility=0x%x, code=0x%x",
-                     device_id, rdpdr->devices[device_id].device_name,
-                     severity,      c,      n,      facility,      code);
-
-    }
-
-    else
-        guac_client_log(rdpdr->client, GUAC_LOG_ERROR, "Unknown device ID: 0x%08x", device_id);
-
-}
-
-void guac_rdpdr_process_device_iorequest(guac_rdpdrPlugin* rdpdr, wStream* input_stream) {
-
-    int device_id, file_id, completion_id, major_func, minor_func;
-
-    /* Read header */
-    Stream_Read_UINT32(input_stream, device_id);
-    Stream_Read_UINT32(input_stream, file_id);
-    Stream_Read_UINT32(input_stream, completion_id);
-    Stream_Read_UINT32(input_stream, major_func);
-    Stream_Read_UINT32(input_stream, minor_func);
-
-    /* If printer, run printer handlers */
-    if (device_id >= 0 && device_id < rdpdr->devices_registered) {
-
-        /* Call handler on device */
-        guac_rdpdr_device* device = &(rdpdr->devices[device_id]);
-        device->iorequest_handler(device, input_stream,
-                file_id, completion_id, major_func, minor_func);
-
-    }
-
-    else
-        guac_client_log(rdpdr->client, GUAC_LOG_ERROR, "Unknown device ID: 0x%08x", device_id);
-
-}
-
-void guac_rdpdr_process_server_capability(guac_rdpdrPlugin* rdpdr, wStream* input_stream) {
-
-    int count;
-    int i;
-
-    /* Read header */
-    Stream_Read_UINT16(input_stream, count);
-    Stream_Seek(input_stream, 2);
-
-    /* Parse capabilities */
-    for (i=0; i<count; i++) {
-
-        int type;
-        int length;
-
-        Stream_Read_UINT16(input_stream, type);
-        Stream_Read_UINT16(input_stream, length);
-
-        /* Ignore all for now */
-        guac_client_log(rdpdr->client, GUAC_LOG_INFO, "Ignoring server capability set type=0x%04x, length=%i", type, length);
-        Stream_Seek(input_stream, length - 4);
-
-    }
-
-    /* Send own capabilities */
-    guac_rdpdr_send_client_capability(rdpdr);
-
-}
-
-void guac_rdpdr_process_user_loggedon(guac_rdpdrPlugin* rdpdr, wStream* input_stream) {
-
-    guac_client_log(rdpdr->client, GUAC_LOG_INFO, "User logged on");
-    guac_rdpdr_send_client_device_list_announce_request(rdpdr);
-
-}
-
-void guac_rdpdr_process_prn_cache_data(guac_rdpdrPlugin* rdpdr, wStream* input_stream) {
-    guac_client_log(rdpdr->client, GUAC_LOG_INFO, "Ignoring printer cached configuration data");
-}
-
-void guac_rdpdr_process_prn_using_xps(guac_rdpdrPlugin* rdpdr, wStream* input_stream) {
-    guac_client_log(rdpdr->client, GUAC_LOG_INFO, "Printer unexpectedly switched to XPS mode");
-}
diff --git a/src/protocols/rdp/guac_rdpdr/rdpdr_messages.h b/src/protocols/rdp/guac_rdpdr/rdpdr_messages.h
deleted file mode 100644
index 1e48924..0000000
--- a/src/protocols/rdp/guac_rdpdr/rdpdr_messages.h
+++ /dev/null
@@ -1,221 +0,0 @@
-/*
- * 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_RDPDR_MESSAGES_H
-#define __GUAC_RDPDR_MESSAGES_H
-
-#include "config.h"
-
-#include "rdpdr_service.h"
-
-#ifdef ENABLE_WINPR
-#include <winpr/stream.h>
-#else
-#include "compat/winpr-stream.h"
-#endif
-
-#include <stdint.h>
-
-/**
- * Identifies the "core" component of RDPDR as the destination of the received
- * packet.
- */
-#define RDPDR_CTYP_CORE 0x4472
-
-/**
- * Identifies the printing component of RDPDR as the destination of the
- * received packet.
- */
-#define RDPDR_CTYP_PRN 0x5052
-
-/*
- * Packet IDs as required by the RDP spec (see: [MS-RDPEFS].pdf)
- */
-
-#define PAKID_CORE_SERVER_ANNOUNCE     0x496E
-#define PAKID_CORE_CLIENTID_CONFIRM    0x4343
-#define PAKID_CORE_CLIENT_NAME         0x434E
-#define PAKID_CORE_DEVICELIST_ANNOUNCE 0x4441
-#define PAKID_CORE_DEVICE_REPLY        0x6472
-#define PAKID_CORE_DEVICE_IOREQUEST    0x4952
-#define PAKID_CORE_DEVICE_IOCOMPLETION 0x4943
-#define PAKID_CORE_SERVER_CAPABILITY   0x5350
-#define PAKID_CORE_CLIENT_CAPABILITY   0x4350
-#define PAKID_CORE_DEVICELIST_REMOVE   0x444D
-#define PAKID_PRN_CACHE_DATA           0x5043
-#define PAKID_CORE_USER_LOGGEDON       0x554C
-#define PAKID_PRN_USING_XPS            0x5543
-
-/**
- * A 32-bit arbitrary value for the osType field of certain requests. As this
- * value is defined as completely arbitrary and required to be ignored by the
- * server, we send "GUAC" as an integer.
- */
-#define GUAC_OS_TYPE (*((uint32_t*) "GUAC"))
-
-/**
- * Name of the printer driver that should be used on the server.
- */
-#define GUAC_PRINTER_DRIVER        "M\0S\0 \0P\0u\0b\0l\0i\0s\0h\0e\0r\0 \0I\0m\0a\0g\0e\0s\0e\0t\0t\0e\0r\0\0\0"
-#define GUAC_PRINTER_DRIVER_LENGTH 50
-
-/**
- * Label of the filesystem.
- */
-#define GUAC_FILESYSTEM_LABEL          "G\0U\0A\0C\0F\0I\0L\0E\0"
-#define GUAC_FILESYSTEM_LABEL_LENGTH   16
-
-/*
- * Capability types
- */
-
-#define CAP_GENERAL_TYPE    1
-#define CAP_PRINTER_TYPE    2
-#define CAP_PORT_TYPE       3
-#define CAP_DRIVE_TYPE      4
-#define CAP_SMARTCARD_TYPE  5
-
-/*
- * General capability header versions.
- */
-
-#define GENERAL_CAPABILITY_VERSION_01 1
-#define GENERAL_CAPABILITY_VERSION_02 2
-
-/*
- * Print capability header versions.
- */
-
-#define PRINT_CAPABILITY_VERSION_01   1
-
-/*
- * Drive capability header versions.
- */
-#define DRIVE_CAPABILITY_VERSION_01   1
-#define DRIVE_CAPABILITY_VERSION_02   2
-
-/*
- * Legal client major version numbers.
- */
-
-#define RDP_CLIENT_MAJOR_ALL 1
-
-/*
- * Legal client minor version numbers.
- */
-
-#define RDP_CLIENT_MINOR_6_1 0xC
-#define RDP_CLIENT_MINOR_5_2 0xA
-#define RDP_CLIENT_MINOR_5_1 0x5
-#define RDP_CLIENT_MINOR_5_0 0x2
-
-/*
- * PDU flags used by the extendedPDU field.
- */
-
-#define RDPDR_DEVICE_REMOVE_PDUS  0x1
-#define RDPDR_CLIENT_DISPLAY_NAME 0x2
-#define RDPDR_USER_LOGGEDON_PDU   0x4
-
-/*
- * Device types.
- */
-
-#define RDPDR_DTYP_SERIAL     0x00000001
-#define RDPDR_DTYP_PARALLEL   0x00000002
-#define RDPDR_DTYP_PRINT      0x00000004
-#define RDPDR_DTYP_FILESYSTEM 0x00000008
-#define RDPDR_DTYP_SMARTCARD  0x00000020
-
-/*
- * Printer flags.
- */
-
-#define RDPDR_PRINTER_ANNOUNCE_FLAG_ASCII          0x00000001
-#define RDPDR_PRINTER_ANNOUNCE_FLAG_DEFAULTPRINTER 0x00000002
-#define RDPDR_PRINTER_ANNOUNCE_FLAG_NETWORKPRINTER 0x00000004
-#define RDPDR_PRINTER_ANNOUNCE_FLAG_TSPRINTER      0x00000008
-#define RDPDR_PRINTER_ANNOUNCE_FLAG_XPSFORMAT      0x00000010
-
-/*
- * I/O requests.
- */
-
-#define IRP_MJ_CREATE                   0x00000000
-#define IRP_MJ_CLOSE                    0x00000002
-#define IRP_MJ_READ                     0x00000003
-#define IRP_MJ_WRITE                    0x00000004
-#define IRP_MJ_DEVICE_CONTROL           0x0000000E
-#define IRP_MJ_QUERY_VOLUME_INFORMATION 0x0000000A
-#define IRP_MJ_SET_VOLUME_INFORMATION   0x0000000B
-#define IRP_MJ_QUERY_INFORMATION        0x00000005
-#define IRP_MJ_SET_INFORMATION          0x00000006
-#define IRP_MJ_DIRECTORY_CONTROL        0x0000000C
-#define IRP_MJ_LOCK_CONTROL             0x00000011
-
-#define IRP_MN_QUERY_DIRECTORY         0x00000001
-#define IRP_MN_NOTIFY_CHANGE_DIRECTORY 0x00000002
-
-/*
- * Volume information constants.
- */
-
-#define FileFsVolumeInformation    0x00000001
-#define FileFsSizeInformation      0x00000003
-#define FileFsDeviceInformation    0x00000004 
-#define FileFsAttributeInformation 0x00000005 
-#define FileFsFullSizeInformation  0x00000007 
-
-/*
- * File information constants.
- */
-
-#define FileBasicInformation        0x00000004
-#define FileStandardInformation     0x00000005
-#define FileRenameInformation       0x0000000A
-#define FileDispositionInformation  0x0000000D
-#define FileAllocationInformation   0x00000013
-#define FileEndOfFileInformation    0x00000014
-#define FileAttributeTagInformation 0x00000023 
-
-/*
- * Directory information constants.
- */
-
-#define FileDirectoryInformation     0x00000001
-#define FileFullDirectoryInformation 0x00000002
-#define FileBothDirectoryInformation 0x00000003
-#define FileNamesInformation         0x0000000C
-
-/*
- * Message handlers.
- */
-
-void guac_rdpdr_process_server_announce(guac_rdpdrPlugin* rdpdr, wStream* input_stream);
-void guac_rdpdr_process_clientid_confirm(guac_rdpdrPlugin* rdpdr, wStream* input_stream);
-void guac_rdpdr_process_device_reply(guac_rdpdrPlugin* rdpdr, wStream* input_stream);
-void guac_rdpdr_process_device_iorequest(guac_rdpdrPlugin* rdpdr, wStream* input_stream);
-void guac_rdpdr_process_server_capability(guac_rdpdrPlugin* rdpdr, wStream* input_stream);
-void guac_rdpdr_process_user_loggedon(guac_rdpdrPlugin* rdpdr, wStream* input_stream);
-void guac_rdpdr_process_prn_cache_data(guac_rdpdrPlugin* rdpdr, wStream* input_stream);
-void guac_rdpdr_process_prn_using_xps(guac_rdpdrPlugin* rdpdr, wStream* input_stream);
-
-#endif
-
diff --git a/src/protocols/rdp/guac_rdpdr/rdpdr_printer.h b/src/protocols/rdp/guac_rdpdr/rdpdr_printer.h
deleted file mode 100644
index 9dbeb42..0000000
--- a/src/protocols/rdp/guac_rdpdr/rdpdr_printer.h
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * 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_RDPDR_PRINTER_H
-#define __GUAC_RDPDR_PRINTER_H
-
-#include "config.h"
-
-#include "rdpdr_service.h"
-
-#ifdef ENABLE_WINPR
-#include <winpr/stream.h>
-#else
-#include "compat/winpr-stream.h"
-#endif
-
-/**
- * Registers a new printer device within the RDPDR plugin. This must be done
- * before RDPDR connection finishes.
- * 
- * @param rdpdr
- *     The RDP device redirection plugin where the device is registered.
- * 
- * @param printer_name
- *     The name of the printer that will be registered with the RDP
- *     connection and passed through to the server.
- */
-void guac_rdpdr_register_printer(guac_rdpdrPlugin* rdpdr, char* printer_name);
-
-#endif
-
diff --git a/src/protocols/rdp/guac_rdpdr/rdpdr_service.c b/src/protocols/rdp/guac_rdpdr/rdpdr_service.c
deleted file mode 100644
index 4901dec..0000000
--- a/src/protocols/rdp/guac_rdpdr/rdpdr_service.c
+++ /dev/null
@@ -1,313 +0,0 @@
-/*
- * 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 "rdp.h"
-#include "rdp_fs.h"
-#include "rdp_settings.h"
-#include "rdp_stream.h"
-#include "rdpdr_fs_service.h"
-#include "rdpdr_messages.h"
-#include "rdpdr_printer.h"
-#include "rdpdr_service.h"
-
-#include <stdlib.h>
-#include <string.h>
-
-#include <freerdp/constants.h>
-#include <freerdp/utils/svc_plugin.h>
-#include <guacamole/client.h>
-#include <guacamole/protocol.h>
-#include <guacamole/socket.h>
-#include <guacamole/stream.h>
-
-#ifdef ENABLE_WINPR
-#include <winpr/stream.h>
-#else
-#include "compat/winpr-stream.h"
-#endif
-
-/**
- * Entry point for RDPDR virtual channel.
- */
-int VirtualChannelEntry(PCHANNEL_ENTRY_POINTS pEntryPoints) {
-
-    /* Allocate plugin */
-    guac_rdpdrPlugin* rdpdr =
-        (guac_rdpdrPlugin*) calloc(1, sizeof(guac_rdpdrPlugin));
-
-    /* Init channel def */
-    strcpy(rdpdr->plugin.channel_def.name, "rdpdr");
-    rdpdr->plugin.channel_def.options = 
-        CHANNEL_OPTION_INITIALIZED | CHANNEL_OPTION_ENCRYPT_RDP | CHANNEL_OPTION_COMPRESS_RDP;
-
-    /* Set callbacks */
-    rdpdr->plugin.connect_callback   = guac_rdpdr_process_connect;
-    rdpdr->plugin.receive_callback   = guac_rdpdr_process_receive;
-    rdpdr->plugin.event_callback     = guac_rdpdr_process_event;
-    rdpdr->plugin.terminate_callback = guac_rdpdr_process_terminate;
-
-    /* Finish init */
-    svc_plugin_init((rdpSvcPlugin*) rdpdr, pEntryPoints);
-    return 1;
-
-}
-
-/* 
- * Service Handlers
- */
-
-void guac_rdpdr_process_connect(rdpSvcPlugin* plugin) {
-
-    /* Get RDPDR plugin */
-    guac_rdpdrPlugin* rdpdr = (guac_rdpdrPlugin*) plugin;
-
-    /* Get client from plugin parameters */
-    guac_client* client = (guac_client*)
-        plugin->channel_entry_points.pExtendedData;
-
-    /* NULL out pExtendedData so we don't lose our guac_client due to an
-     * automatic free() within libfreerdp */
-    plugin->channel_entry_points.pExtendedData = NULL;
-
-    /* Get data from client */
-    guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
-
-    /* Init plugin */
-    rdpdr->client = client;
-    rdpdr->devices_registered = 0;
-
-    /* Register printer if enabled */
-    if (rdp_client->settings->printing_enabled)
-        guac_rdpdr_register_printer(rdpdr, rdp_client->settings->printer_name);
-
-    /* Register drive if enabled */
-    if (rdp_client->settings->drive_enabled)
-        guac_rdpdr_register_fs(rdpdr, rdp_client->settings->drive_name);
-
-    /* Log that printing, etc. has been loaded */
-    guac_client_log(client, GUAC_LOG_INFO, "guacdr connected.");
-
-}
-
-void guac_rdpdr_process_terminate(rdpSvcPlugin* plugin) {
-
-    guac_rdpdrPlugin* rdpdr = (guac_rdpdrPlugin*) plugin;
-    int i;
-
-    for (i=0; i<rdpdr->devices_registered; i++) {
-        guac_rdpdr_device* device = &(rdpdr->devices[i]);
-        guac_client_log(rdpdr->client, GUAC_LOG_INFO, "Unloading device %i (%s)",
-                device->device_id, device->device_name);
-        device->free_handler(device);
-    }
-
-    free(plugin);
-}
-
-void guac_rdpdr_process_event(rdpSvcPlugin* plugin, wMessage* event) {
-    freerdp_event_free(event);
-}
-
-void guac_rdpdr_process_receive(rdpSvcPlugin* plugin,
-        wStream* input_stream) {
-
-    guac_rdpdrPlugin* rdpdr = (guac_rdpdrPlugin*) plugin;
-
-    int component;
-    int packet_id;
-
-    /* Read header */
-    Stream_Read_UINT16(input_stream, component);
-    Stream_Read_UINT16(input_stream, packet_id);
-
-    /* Core component */
-    if (component == RDPDR_CTYP_CORE) {
-
-        /* Dispatch handlers based on packet ID */
-        switch (packet_id) {
-
-            case PAKID_CORE_SERVER_ANNOUNCE:
-                guac_rdpdr_process_server_announce(rdpdr, input_stream);
-                break;
-
-            case PAKID_CORE_CLIENTID_CONFIRM:
-                guac_rdpdr_process_clientid_confirm(rdpdr, input_stream);
-                break;
-
-            case PAKID_CORE_DEVICE_REPLY:
-                guac_rdpdr_process_device_reply(rdpdr, input_stream);
-                break;
-
-            case PAKID_CORE_DEVICE_IOREQUEST:
-                guac_rdpdr_process_device_iorequest(rdpdr, input_stream);
-                break;
-
-            case PAKID_CORE_SERVER_CAPABILITY:
-                guac_rdpdr_process_server_capability(rdpdr, input_stream);
-                break;
-
-            case PAKID_CORE_USER_LOGGEDON:
-                guac_rdpdr_process_user_loggedon(rdpdr, input_stream);
-                break;
-
-            default:
-                guac_client_log(rdpdr->client, GUAC_LOG_INFO, "Ignoring RDPDR core packet with unexpected ID: 0x%04x", packet_id);
-
-        }
-
-    } /* end if core */
-
-    /* Printer component */
-    else if (component == RDPDR_CTYP_PRN) {
-
-        /* Dispatch handlers based on packet ID */
-        switch (packet_id) {
-
-            case PAKID_PRN_CACHE_DATA:
-                guac_rdpdr_process_prn_cache_data(rdpdr, input_stream);
-                break;
-
-            case PAKID_PRN_USING_XPS:
-                guac_rdpdr_process_prn_using_xps(rdpdr, input_stream);
-                break;
-
-            default:
-                guac_client_log(rdpdr->client, GUAC_LOG_INFO, "Ignoring RDPDR printer packet with unexpected ID: 0x%04x", packet_id);
-
-        }
-
-    } /* end if printer */
-
-    else
-        guac_client_log(rdpdr->client, GUAC_LOG_INFO, "Ignoring packet for unknown RDPDR component: 0x%04x", component);
-
-}
-
-wStream* guac_rdpdr_new_io_completion(guac_rdpdr_device* device,
-        int completion_id, int status, int size) {
-
-    wStream* output_stream = Stream_New(NULL, 16+size);
-
-    /* Write header */
-    Stream_Write_UINT16(output_stream, RDPDR_CTYP_CORE);
-    Stream_Write_UINT16(output_stream, PAKID_CORE_DEVICE_IOCOMPLETION);
-
-    /* Write content */
-    Stream_Write_UINT32(output_stream, device->device_id);
-    Stream_Write_UINT32(output_stream, completion_id);
-    Stream_Write_UINT32(output_stream, status);
-
-    return output_stream;
-
-}
-
-/**
- * Callback invoked on the current connection owner (if any) when a file
- * download is being initiated using the magic "Download" folder.
- *
- * @param owner
- *     The guac_user that is the owner of the connection, or NULL if the
- *     connection owner has left.
- *
- * @param data
- *     The full absolute path to the file that should be downloaded.
- *
- * @return
- *     The stream allocated for the file download, or NULL if the download has
- *     failed to start.
- */
-static void* guac_rdpdr_download_to_owner(guac_user* owner, void* data) {
-
-    /* Do not bother attempting the download if the owner has left */
-    if (owner == NULL)
-        return NULL;
-
-    guac_client* client = owner->client;
-    guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
-    guac_rdp_fs* filesystem = rdp_client->filesystem;
-
-    /* Ignore download if filesystem has been unloaded */
-    if (filesystem == NULL)
-        return NULL;
-
-    /* Attempt to open requested file */
-    char* path = (char*) data;
-    int file_id = guac_rdp_fs_open(filesystem, path,
-            ACCESS_FILE_READ_DATA, 0, DISP_FILE_OPEN, 0);
-
-    /* If file opened successfully, start stream */
-    if (file_id >= 0) {
-
-        guac_rdp_stream* rdp_stream;
-        const char* basename;
-
-        int i;
-        char c;
-
-        /* Associate stream with transfer status */
-        guac_stream* stream = guac_user_alloc_stream(owner);
-        stream->data = rdp_stream = malloc(sizeof(guac_rdp_stream));
-        stream->ack_handler = guac_rdp_download_ack_handler;
-        rdp_stream->type = GUAC_RDP_DOWNLOAD_STREAM;
-        rdp_stream->download_status.file_id = file_id;
-        rdp_stream->download_status.offset = 0;
-
-        /* Get basename from absolute path */
-        i=0;
-        basename = path;
-        do {
-
-            c = path[i];
-            if (c == '/' || c == '\\')
-                basename = &(path[i+1]);
-
-            i++;
-
-        } while (c != '\0');
-
-        guac_user_log(owner, GUAC_LOG_DEBUG, "%s: Initiating download "
-                "of \"%s\"", __func__, path);
-
-        /* Begin stream */
-        guac_protocol_send_file(owner->socket, stream,
-                "application/octet-stream", basename);
-        guac_socket_flush(owner->socket);
-
-        /* Download started successfully */
-        return stream;
-
-    }
-
-    /* Download failed */
-    guac_user_log(owner, GUAC_LOG_ERROR, "Unable to download \"%s\"", path);
-    return NULL;
-
-}
-
-void guac_rdpdr_start_download(guac_rdpdr_device* device, char* path) {
-
-    guac_client* client = device->rdpdr->client;
-
-    /* Initiate download to the owner of the connection */
-    guac_client_for_owner(client, guac_rdpdr_download_to_owner, path);
-
-}
-
diff --git a/src/protocols/rdp/guac_rdpdr/rdpdr_service.h b/src/protocols/rdp/guac_rdpdr/rdpdr_service.h
deleted file mode 100644
index ea5cb57..0000000
--- a/src/protocols/rdp/guac_rdpdr/rdpdr_service.h
+++ /dev/null
@@ -1,184 +0,0 @@
-/*
- * 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_RDPDR_SERVICE_H
-#define __GUAC_RDPDR_SERVICE_H
-
-#include "config.h"
-
-#include <freerdp/utils/svc_plugin.h>
-#include <guacamole/client.h>
-
-#ifdef ENABLE_WINPR
-#include <winpr/stream.h>
-#else
-#include "compat/winpr-stream.h"
-#endif
-
-/**
- * The maximum number of bytes to allow for a device read.
- */
-#define GUAC_RDP_MAX_READ_BUFFER 4194304
-
-typedef struct guac_rdpdrPlugin guac_rdpdrPlugin;
-typedef struct guac_rdpdr_device guac_rdpdr_device;
-
-/**
- * Handler for client device list announce. Each implementing device must write
- * its announcement header and data to the given output stream.
- */
-typedef void guac_rdpdr_device_announce_handler(guac_rdpdr_device* device, wStream* output_stream,
-        int device_id);
-
-/**
- * Handler for device I/O requests.
- */
-typedef void guac_rdpdr_device_iorequest_handler(guac_rdpdr_device* device,
-        wStream* input_stream, int file_id, int completion_id, int major_func, int minor_func);
-
-/**
- * Handler for cleaning up the dynamically-allocated portions of a device.
- */
-typedef void guac_rdpdr_device_free_handler(guac_rdpdr_device* device);
-
-/**
- * Arbitrary device forwarded over the RDPDR channel.
- */
-struct guac_rdpdr_device {
-
-    /**
-     * The RDPDR plugin owning this device.
-     */
-    guac_rdpdrPlugin* rdpdr;
-
-    /**
-     * The ID assigned to this device by the RDPDR plugin.
-     */
-    int device_id;
-
-    /**
-     * Device name, used for logging and for passthrough to the
-     * server.
-     */
-    const char* device_name;
-
-    /**
-     * The type of RDPDR device that this represents.
-     */
-    uint32_t device_type;
-
-    /**
-     * The DOS name of the device.  Max 8 bytes, including terminator.
-     */
-    const char *dos_name;
-    
-    /**
-     * The stream that stores the RDPDR device announcement for this device.
-     */
-    wStream* device_announce;
-    
-    /**
-     * The length of the device_announce wStream.
-     */
-    int device_announce_len;
-
-    /**
-     * Handler which should be called for every I/O request received.
-     */
-    guac_rdpdr_device_iorequest_handler* iorequest_handler;
-
-    /**
-     * Handler which should be called when the device is being freed.
-     */
-    guac_rdpdr_device_free_handler* free_handler;
-
-    /**
-     * Arbitrary data, used internally by the handlers for this device.
-     */
-    void* data;
-
-};
-
-/**
- * Structure representing the current state of the Guacamole RDPDR plugin for
- * FreeRDP.
- */
-struct guac_rdpdrPlugin {
-
-    /**
-     * The FreeRDP parts of this plugin. This absolutely MUST be first.
-     * FreeRDP depends on accessing this structure as if it were an instance
-     * of rdpSvcPlugin.
-     */
-    rdpSvcPlugin plugin;
-
-    /**
-     * Reference to the client owning this instance of the RDPDR plugin.
-     */
-    guac_client* client;
-
-    /**
-     * The number of devices registered within the devices array.
-     */
-    int devices_registered;
-
-    /**
-     * Array of registered devices.
-     */
-    guac_rdpdr_device devices[8];
-
-};
-
-/**
- * Handler called when this plugin is loaded by FreeRDP.
- */
-void guac_rdpdr_process_connect(rdpSvcPlugin* plugin);
-
-/**
- * Handler called when this plugin receives data along its designated channel.
- */
-void guac_rdpdr_process_receive(rdpSvcPlugin* plugin,
-        wStream* input_stream);
-
-/**
- * Handler called when this plugin is being unloaded.
- */
-void guac_rdpdr_process_terminate(rdpSvcPlugin* plugin);
-
-/**
- * Handler called when this plugin receives an event. For the sake of RDPDR,
- * all events will be ignored and simply free'd.
- */
-void guac_rdpdr_process_event(rdpSvcPlugin* plugin, wMessage* event);
-
-/**
- * Creates a new stream which contains the common DR_DEVICE_IOCOMPLETION header
- * used for virtually all responses.
- */
-wStream* guac_rdpdr_new_io_completion(guac_rdpdr_device* device,
-        int completion_id, int status, int size);
-
-/**
- * Begins streaming the given file to the user via a Guacamole file stream.
- */
-void guac_rdpdr_start_download(guac_rdpdr_device* device, char* path);
-
-#endif
-
diff --git a/src/protocols/rdp/guac_rdpsnd/rdpsnd_service.c b/src/protocols/rdp/guac_rdpsnd/rdpsnd_service.c
deleted file mode 100644
index 45de442..0000000
--- a/src/protocols/rdp/guac_rdpsnd/rdpsnd_service.c
+++ /dev/null
@@ -1,145 +0,0 @@
-/*
- * 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 "rdpsnd_service.h"
-#include "rdpsnd_messages.h"
-
-#include <stdlib.h>
-#include <string.h>
-
-#include <freerdp/constants.h>
-#include <freerdp/utils/svc_plugin.h>
-#include <guacamole/client.h>
-
-#ifdef ENABLE_WINPR
-#include <winpr/stream.h>
-#else
-#include "compat/winpr-stream.h"
-#endif
-
-/**
- * Entry point for RDPSND virtual channel.
- */
-int VirtualChannelEntry(PCHANNEL_ENTRY_POINTS pEntryPoints) {
-
-    /* Allocate plugin */
-    guac_rdpsndPlugin* rdpsnd =
-        (guac_rdpsndPlugin*) calloc(1, sizeof(guac_rdpsndPlugin));
-
-    /* Init channel def */
-    strcpy(rdpsnd->plugin.channel_def.name, "rdpsnd");
-    rdpsnd->plugin.channel_def.options = 
-        CHANNEL_OPTION_INITIALIZED | CHANNEL_OPTION_ENCRYPT_RDP;
-
-    /* Set callbacks */
-    rdpsnd->plugin.connect_callback   = guac_rdpsnd_process_connect;
-    rdpsnd->plugin.receive_callback   = guac_rdpsnd_process_receive;
-    rdpsnd->plugin.event_callback     = guac_rdpsnd_process_event;
-    rdpsnd->plugin.terminate_callback = guac_rdpsnd_process_terminate;
-
-    /* Finish init */
-    svc_plugin_init((rdpSvcPlugin*) rdpsnd, pEntryPoints);
-    return 1;
-
-}
-
-/* 
- * Service Handlers
- */
-
-void guac_rdpsnd_process_connect(rdpSvcPlugin* plugin) {
-
-    guac_rdpsndPlugin* rdpsnd = (guac_rdpsndPlugin*) plugin;
-
-    /* Get client from plugin parameters */
-    guac_client* client = rdpsnd->client =
-        (guac_client*) plugin->channel_entry_points.pExtendedData;
-
-    /* NULL out pExtendedData so we don't lose our guac_client due to an
-     * automatic free() within libfreerdp */
-    plugin->channel_entry_points.pExtendedData = NULL;
-
-#ifdef RDPSVCPLUGIN_INTERVAL_MS
-    /* Update every 10 ms */
-    plugin->interval_ms = 10;
-#endif
-
-    /* Log that sound has been loaded */
-    guac_client_log(client, GUAC_LOG_INFO, "guacsnd connected.");
-
-}
-
-void guac_rdpsnd_process_terminate(rdpSvcPlugin* plugin) {
-    free(plugin);
-}
-
-void guac_rdpsnd_process_event(rdpSvcPlugin* plugin, wMessage* event) {
-    freerdp_event_free(event);
-}
-
-void guac_rdpsnd_process_receive(rdpSvcPlugin* plugin,
-        wStream* input_stream) {
-
-    guac_rdpsndPlugin* rdpsnd = (guac_rdpsndPlugin*) plugin;
-    guac_rdpsnd_pdu_header header;
-
-    /* Read RDPSND PDU header */
-    Stream_Read_UINT8(input_stream, header.message_type);
-    Stream_Seek_UINT8(input_stream);
-    Stream_Read_UINT16(input_stream, header.body_size);
-
-    /* 
-     * If next PDU is SNDWAVE (due to receiving WaveInfo PDU previously),
-     * ignore the header and parse as a Wave PDU.
-     */
-    if (rdpsnd->next_pdu_is_wave)
-        guac_rdpsnd_wave_handler(rdpsnd, input_stream, &header);
-    
-    /* Dispatch message to standard handlers */
-    else {
-        switch (header.message_type) {
-
-            /* Server Audio Formats and Version PDU */
-            case SNDC_FORMATS:
-                guac_rdpsnd_formats_handler(rdpsnd, input_stream, &header);
-                break;
-
-            /* Training PDU */
-            case SNDC_TRAINING:
-                guac_rdpsnd_training_handler(rdpsnd, input_stream, &header);
-                break;
-
-            /* WaveInfo PDU */
-            case SNDC_WAVE:
-                guac_rdpsnd_wave_info_handler(rdpsnd, input_stream, &header);
-                break;
-
-            /* Close PDU */
-            case SNDC_CLOSE:
-                guac_rdpsnd_close_handler(rdpsnd, input_stream, &header);
-                break;
-
-        }
-    }
-
-    Stream_Free(input_stream, TRUE);
-}
-
diff --git a/src/protocols/rdp/guac_svc/svc_service.c b/src/protocols/rdp/guac_svc/svc_service.c
deleted file mode 100644
index c40c6c5..0000000
--- a/src/protocols/rdp/guac_svc/svc_service.c
+++ /dev/null
@@ -1,150 +0,0 @@
-/*
- * 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 "svc_service.h"
-
-#include <stdlib.h>
-#include <string.h>
-
-#include <freerdp/constants.h>
-#include <freerdp/utils/svc_plugin.h>
-#include <guacamole/client.h>
-#include <guacamole/protocol.h>
-#include <guacamole/socket.h>
-#include <guacamole/string.h>
-
-#ifdef ENABLE_WINPR
-#include <winpr/stream.h>
-#else
-#include "compat/winpr-stream.h"
-#endif
-
-/**
- * Entry point for arbitrary SVC.
- */
-int VirtualChannelEntry(PCHANNEL_ENTRY_POINTS pEntryPoints) {
-
-    /* Gain access to plugin data */
-    CHANNEL_ENTRY_POINTS_FREERDP* entry_points_ex =
-        (CHANNEL_ENTRY_POINTS_FREERDP*) pEntryPoints;
-
-    /* Allocate plugin */
-    guac_svcPlugin* svc_plugin =
-        (guac_svcPlugin*) calloc(1, sizeof(guac_svcPlugin));
-
-    /* Get SVC descriptor from plugin parameters */
-    guac_rdp_svc* svc = (guac_rdp_svc*) entry_points_ex->pExtendedData;
-
-    /* Init channel def */
-    guac_strlcpy(svc_plugin->plugin.channel_def.name, svc->name,
-            GUAC_RDP_SVC_MAX_LENGTH);
-    svc_plugin->plugin.channel_def.options = 
-          CHANNEL_OPTION_INITIALIZED
-        | CHANNEL_OPTION_ENCRYPT_RDP
-        | CHANNEL_OPTION_COMPRESS_RDP;
-
-    /* Init plugin */
-    svc_plugin->svc = svc;
-
-    /* Set callbacks */
-    svc_plugin->plugin.connect_callback   = guac_svc_process_connect;
-    svc_plugin->plugin.receive_callback   = guac_svc_process_receive;
-    svc_plugin->plugin.event_callback     = guac_svc_process_event;
-    svc_plugin->plugin.terminate_callback = guac_svc_process_terminate;
-
-    /* Store plugin reference in SVC */
-    svc->plugin = (rdpSvcPlugin*) svc_plugin;
-
-    /* Finish init */
-    svc_plugin_init((rdpSvcPlugin*) svc_plugin, pEntryPoints);
-    return 1;
-
-}
-
-/* 
- * Service Handlers
- */
-
-void guac_svc_process_connect(rdpSvcPlugin* plugin) {
-
-    /* Get corresponding guac_rdp_svc */
-    guac_svcPlugin* svc_plugin = (guac_svcPlugin*) plugin;
-    guac_rdp_svc* svc = svc_plugin->svc;
-
-    /* NULL out pExtendedData so we don't lose our guac_rdp_svc due to an
-     * automatic free() within libfreerdp */
-    plugin->channel_entry_points.pExtendedData = NULL;
-
-    /* Create pipe */
-    svc->output_pipe = guac_client_alloc_stream(svc->client);
-
-    /* Notify of pipe's existence */
-    guac_rdp_svc_send_pipe(svc->client->socket, svc);
-
-    /* Log connection to static channel */
-    guac_client_log(svc->client, GUAC_LOG_INFO,
-            "Static channel \"%s\" connected.", svc->name);
-
-}
-
-void guac_svc_process_terminate(rdpSvcPlugin* plugin) {
-
-    /* Get corresponding guac_rdp_svc */
-    guac_svcPlugin* svc_plugin = (guac_svcPlugin*) plugin;
-    guac_rdp_svc* svc = svc_plugin->svc;
-
-    /* Remove and free SVC */
-    guac_client_log(svc->client, GUAC_LOG_INFO, "Closing channel \"%s\"...", svc->name);
-    guac_rdp_remove_svc(svc->client, svc->name);
-    free(svc);
-
-    free(plugin);
-
-}
-
-void guac_svc_process_event(rdpSvcPlugin* plugin, wMessage* event) {
-    freerdp_event_free(event);
-}
-
-void guac_svc_process_receive(rdpSvcPlugin* plugin,
-        wStream* input_stream) {
-
-    /* Get corresponding guac_rdp_svc */
-    guac_svcPlugin* svc_plugin = (guac_svcPlugin*) plugin;
-    guac_rdp_svc* svc = svc_plugin->svc;
-
-    /* Fail if output not created */
-    if (svc->output_pipe == NULL) {
-        guac_client_log(svc->client, GUAC_LOG_ERROR,
-                "Output for channel \"%s\" dropped.",
-                svc->name);
-        return;
-    }
-
-    /* Send blob */
-    guac_protocol_send_blob(svc->client->socket, svc->output_pipe,
-            Stream_Buffer(input_stream),
-            Stream_Length(input_stream));
-
-    guac_socket_flush(svc->client->socket);
-
-}
-
diff --git a/src/protocols/rdp/guac_svc/svc_service.h b/src/protocols/rdp/guac_svc/svc_service.h
deleted file mode 100644
index 219aaec..0000000
--- a/src/protocols/rdp/guac_svc/svc_service.h
+++ /dev/null
@@ -1,77 +0,0 @@
-/*
- * 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_SVC_SERVICE_H
-#define __GUAC_SVC_SERVICE_H
-
-#include "config.h"
-#include "rdp_svc.h"
-
-#include <freerdp/utils/svc_plugin.h>
-
-#ifdef ENABLE_WINPR
-#include <winpr/stream.h>
-#else
-#include "compat/winpr-stream.h"
-#endif
-
-/**
- * Structure representing the current state of an arbitrary static virtual
- * channel.
- */
-typedef struct guac_svcPlugin {
-
-    /**
-     * The FreeRDP parts of this plugin. This absolutely MUST be first.
-     * FreeRDP depends on accessing this structure as if it were an instance
-     * of rdpSvcPlugin.
-     */
-    rdpSvcPlugin plugin;
-
-    /**
-     * The Guacamole-specific SVC structure describing the channel this
-     * instance represents.
-     */
-    guac_rdp_svc* svc;
-
-} guac_svcPlugin;
-
-/**
- * Handler called when this plugin is loaded by FreeRDP.
- */
-void guac_svc_process_connect(rdpSvcPlugin* plugin);
-
-/**
- * Handler called when this plugin receives data along its designated channel.
- */
-void guac_svc_process_receive(rdpSvcPlugin* plugin,
-        wStream* input_stream);
-
-/**
- * Handler called when this plugin is being unloaded.
- */
-void guac_svc_process_terminate(rdpSvcPlugin* plugin);
-
-/**
- * Handler called when this plugin receives an event.
- */
-void guac_svc_process_event(rdpSvcPlugin* plugin, wMessage* event);
-
-#endif
-
diff --git a/src/protocols/rdp/input.c b/src/protocols/rdp/input.c
index 96507db..8f77dc0 100644
--- a/src/protocols/rdp/input.c
+++ b/src/protocols/rdp/input.c
@@ -17,20 +17,20 @@
  * under the License.
  */
 
-#include "config.h"
-
-#include "client.h"
+#include "channels/disp.h"
+#include "common/cursor.h"
+#include "common/display.h"
 #include "common/recording.h"
 #include "input.h"
 #include "keyboard.h"
 #include "rdp.h"
-#include "rdp_disp.h"
+#include "settings.h"
 
 #include <freerdp/freerdp.h>
 #include <freerdp/input.h>
 #include <guacamole/client.h>
+#include <guacamole/user.h>
 
-#include <pthread.h>
 #include <stdlib.h>
 
 int guac_rdp_user_mouse_handler(guac_user* user, int x, int y, int mask) {
@@ -38,14 +38,10 @@
     guac_client* client = user->client;
     guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
 
-    pthread_mutex_lock(&(rdp_client->rdp_lock));
-
     /* Skip if not yet connected */
     freerdp* rdp_inst = rdp_client->rdp_inst;
-    if (rdp_inst == NULL) {
-        pthread_mutex_unlock(&(rdp_client->rdp_lock));
+    if (rdp_inst == NULL)
         return 0;
-    }
 
     /* Store current mouse location/state */
     guac_common_cursor_update(rdp_client->display->cursor, user, x, y, mask);
@@ -118,8 +114,6 @@
         rdp_client->mouse_button_mask = mask;
     }
 
-    pthread_mutex_unlock(&(rdp_client->rdp_lock));
-
     return 0;
 }
 
@@ -155,9 +149,7 @@
     height = height * settings->resolution / user->info.optimal_resolution;
 
     /* Send display update */
-    pthread_mutex_lock(&(rdp_client->rdp_lock));
     guac_rdp_disp_set_size(rdp_client->disp, settings, rdp_inst, width, height);
-    pthread_mutex_unlock(&(rdp_client->rdp_lock));
 
     return 0;
 
diff --git a/src/protocols/rdp/keyboard.c b/src/protocols/rdp/keyboard.c
index 88cf2da..42cc4db 100644
--- a/src/protocols/rdp/keyboard.c
+++ b/src/protocols/rdp/keyboard.c
@@ -17,19 +17,15 @@
  * under the License.
  */
 
-#include "config.h"
-
-#include "client.h"
 #include "decompose.h"
 #include "keyboard.h"
+#include "keymap.h"
 #include "rdp.h"
-#include "rdp_keymap.h"
 
 #include <freerdp/freerdp.h>
 #include <freerdp/input.h>
 #include <guacamole/client.h>
 
-#include <pthread.h>
 #include <stdlib.h>
 
 /**
@@ -102,21 +98,15 @@
     else
         pressed_flags = KBD_FLAGS_RELEASE;
 
-    pthread_mutex_lock(&(rdp_client->rdp_lock));
-
     /* Skip if not yet connected */
     freerdp* rdp_inst = rdp_client->rdp_inst;
-    if (rdp_inst == NULL) {
-        pthread_mutex_unlock(&(rdp_client->rdp_lock));
+    if (rdp_inst == NULL)
         return;
-    }
 
     /* Send actual key */
     rdp_inst->input->KeyboardEvent(rdp_inst->input,
             flags | pressed_flags, scancode);
 
-    pthread_mutex_unlock(&(rdp_client->rdp_lock));
-
 }
 
 /**
@@ -136,22 +126,16 @@
 static void guac_rdp_send_unicode_event(guac_rdp_client* rdp_client,
         int codepoint) {
 
-    pthread_mutex_lock(&(rdp_client->rdp_lock));
-
     /* Skip if not yet connected */
     freerdp* rdp_inst = rdp_client->rdp_inst;
-    if (rdp_inst == NULL) {
-        pthread_mutex_unlock(&(rdp_client->rdp_lock));
+    if (rdp_inst == NULL)
         return;
-    }
 
     /* Send Unicode event */
     rdp_inst->input->UnicodeKeyboardEvent(
             rdp_inst->input,
             0, codepoint);
 
-    pthread_mutex_unlock(&(rdp_client->rdp_lock));
-
 }
 
 /**
@@ -171,20 +155,14 @@
 static void guac_rdp_send_synchronize_event(guac_rdp_client* rdp_client,
         int flags) {
 
-    pthread_mutex_lock(&(rdp_client->rdp_lock));
-
     /* Skip if not yet connected */
     freerdp* rdp_inst = rdp_client->rdp_inst;
-    if (rdp_inst == NULL) {
-        pthread_mutex_unlock(&(rdp_client->rdp_lock));
+    if (rdp_inst == NULL)
         return;
-    }
 
     /* Synchronize lock key states */
     rdp_inst->input->SynchronizeEvent(rdp_inst->input, flags);
 
-    pthread_mutex_unlock(&(rdp_client->rdp_lock));
-
 }
 
 /**
diff --git a/src/protocols/rdp/keyboard.h b/src/protocols/rdp/keyboard.h
index 79eed75..6296a71 100644
--- a/src/protocols/rdp/keyboard.h
+++ b/src/protocols/rdp/keyboard.h
@@ -20,7 +20,7 @@
 #ifndef GUAC_RDP_KEYBOARD_H
 #define GUAC_RDP_KEYBOARD_H
 
-#include "rdp_keymap.h"
+#include "keymap.h"
 
 #include <guacamole/client.h>
 
@@ -94,7 +94,7 @@
 
     /**
      * The local state of all keys, as well as the necessary information to
-     * translate received keysyms into scancodes or sequences  of scancodes for
+     * translate received keysyms into scancodes or sequences of scancodes for
      * RDP. The state of each key is updated based on received Guacamole key
      * events, while the information describing the behavior and scancode
      * mapping of each key is populated based on an associated keymap.
@@ -180,7 +180,7 @@
 /**
  * For every keysym in the given NULL-terminated array of keysyms, send the RDP
  * key events required to update the remote state of those keys as specified,
- * depending on the current local state of those keysyms.  For each key in the
+ * depending on the current local state of those keysyms. For each key in the
  * "from" state, that key will be updated to the "to" state. The locally-stored
  * state of each key is remains untouched.
  *
diff --git a/src/protocols/rdp/rdp_keymap.c b/src/protocols/rdp/keymap.c
similarity index 97%
rename from src/protocols/rdp/rdp_keymap.c
rename to src/protocols/rdp/keymap.c
index 3be48a7..b71b402 100644
--- a/src/protocols/rdp/rdp_keymap.c
+++ b/src/protocols/rdp/keymap.c
@@ -17,9 +17,7 @@
  * under the License.
  */
 
-#include "config.h"
-
-#include "rdp_keymap.h"
+#include "keymap.h"
 
 #include <string.h>
 
diff --git a/src/protocols/rdp/rdp_keymap.h b/src/protocols/rdp/keymap.h
similarity index 96%
rename from src/protocols/rdp/rdp_keymap.h
rename to src/protocols/rdp/keymap.h
index f15ddea..bac1a7f 100644
--- a/src/protocols/rdp/rdp_keymap.h
+++ b/src/protocols/rdp/keymap.h
@@ -17,17 +17,10 @@
  * under the License.
  */
 
+#ifndef GUAC_RDP_KEYMAP_H
+#define GUAC_RDP_KEYMAP_H
 
-#ifndef _GUAC_RDP_RDP_KEYMAP_H
-#define _GUAC_RDP_RDP_KEYMAP_H
-
-#include "config.h"
-
-#ifdef ENABLE_WINPR
 #include <winpr/wtypes.h>
-#else
-#include "compat/winpr-wtypes.h"
-#endif
 
 /**
  * Represents a keysym-to-scancode mapping for RDP, with extra information
diff --git a/src/protocols/rdp/keymaps/generate.pl b/src/protocols/rdp/keymaps/generate.pl
index 263b616..f1059a7 100755
--- a/src/protocols/rdp/keymaps/generate.pl
+++ b/src/protocols/rdp/keymaps/generate.pl
@@ -43,14 +43,9 @@
 open OUTPUT, ">", "_generated_keymaps.c";
 print OUTPUT 
        '#include "config.h"'                                . "\n"
-     . '#include "rdp_keymap.h"'                            . "\n"
+     . '#include "keymap.h"'                                . "\n"
      . '#include <freerdp/input.h>'                         . "\n"
-     .                                                        "\n"
-     . '#ifdef HAVE_FREERDP_LOCALE_KEYBOARD_H'              . "\n"
      . '#include <freerdp/locale/keyboard.h>'               . "\n"
-     . '#else'                                              . "\n"
-     . '#include <freerdp/kbd/layouts.h>'                   . "\n"
-     . '#endif'                                             . "\n"
      .                                                        "\n"
      . '#include <stddef.h>'                                . "\n"
      .                                                        "\n";
diff --git a/src/protocols/rdp/log.c b/src/protocols/rdp/log.c
new file mode 100644
index 0000000..29414c1
--- /dev/null
+++ b/src/protocols/rdp/log.c
@@ -0,0 +1,74 @@
+/*
+ * 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 <guacamole/client.h>
+#include <winpr/wlog.h>
+#include <winpr/wtypes.h>
+
+#include <stddef.h>
+
+/**
+ * The guac_client that should be used within this process for FreeRDP log
+ * messages. As all Guacamole connections are isolated at the process level,
+ * this will only ever be set to the guac_client of the current process'
+ * connection.
+ */
+static guac_client* current_client = NULL;
+
+/**
+ * Logs the text data within the given message to the logging facilities of the
+ * guac_client currently stored under current_client (the guac_client of the
+ * current process).
+ *
+ * @param message
+ *     The message to log.
+ *
+ * @return
+ *     TRUE if the message was successfully logged, FALSE otherwise.
+ */
+static BOOL guac_rdp_wlog_text_message(const wLogMessage* message) {
+
+    /* Fail if log not yet redirected */
+    if (current_client == NULL)
+        return FALSE;
+
+    /* Log all received messages at the debug level */
+    guac_client_log(current_client, GUAC_LOG_DEBUG, "%s", message->TextString);
+    return TRUE;
+
+}
+
+void guac_rdp_redirect_wlog(guac_client* client) {
+
+    wLogCallbacks callbacks = {
+        .message = guac_rdp_wlog_text_message
+    };
+
+    current_client = client;
+
+    /* Reconfigure root logger to use callback appender */
+    wLog* root = WLog_GetRoot();
+    WLog_SetLogAppenderType(root, WLOG_APPENDER_CALLBACK);
+
+    /* Set appender callbacks to our own */
+    wLogAppender* appender = WLog_GetLogAppender(root);
+    WLog_ConfigureAppender(appender, "callbacks", &callbacks);
+
+}
+
diff --git a/src/protocols/rdp/ptr_string.c b/src/protocols/rdp/log.h
similarity index 69%
copy from src/protocols/rdp/ptr_string.c
copy to src/protocols/rdp/log.h
index 5ec1b62..3b40aee 100644
--- a/src/protocols/rdp/ptr_string.c
+++ b/src/protocols/rdp/log.h
@@ -17,29 +17,19 @@
  * under the License.
  */
 
-#include "config.h"
-#include "ptr_string.h"
+#ifndef GUAC_RDP_LOG_H
+#define GUAC_RDP_LOG_H
 
 #include <guacamole/client.h>
 
-#include <stdio.h>
-#include <stdlib.h>
+/**
+ * Redirects the core FreeRDP logging facility, wLog, such that it logs all
+ * messages at the debug level using guac_client_log().
+ *
+ * @param client
+ *     The guac_client that should receive all log messages.
+ */
+void guac_rdp_redirect_wlog(guac_client* client);
 
-void guac_rdp_ptr_to_string(void* data, char* str) {
-
-    /* Convert pointer to string */
-    sprintf(str, "%p", data);
-
-}
-
-void* guac_rdp_string_to_ptr(const char* str) {
-
-    void* data;
-
-    /* Convert string to pointer */
-    sscanf(str, "%p", &data);
-
-    return data;
-
-}
+#endif
 
diff --git a/src/protocols/rdp/ls.c b/src/protocols/rdp/ls.c
new file mode 100644
index 0000000..300bf93
--- /dev/null
+++ b/src/protocols/rdp/ls.c
@@ -0,0 +1,122 @@
+/*
+ * 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 "fs.h"
+#include "ls.h"
+
+#include <guacamole/client.h>
+#include <guacamole/protocol.h>
+#include <guacamole/socket.h>
+#include <guacamole/stream.h>
+#include <guacamole/user.h>
+#include <winpr/nt.h>
+#include <winpr/shell.h>
+
+#include <stdlib.h>
+#include <string.h>
+
+int guac_rdp_ls_ack_handler(guac_user* user, guac_stream* stream,
+        char* message, guac_protocol_status status) {
+
+    int blob_written = 0;
+    const char* filename;
+
+    guac_rdp_ls_status* ls_status = (guac_rdp_ls_status*) stream->data;
+
+    /* If unsuccessful, free stream and abort */
+    if (status != GUAC_PROTOCOL_STATUS_SUCCESS) {
+        guac_rdp_fs_close(ls_status->fs, ls_status->file_id);
+        guac_user_free_stream(user, stream);
+        free(ls_status);
+        return 0;
+    }
+
+    /* While directory entries remain */
+    while ((filename = guac_rdp_fs_read_dir(ls_status->fs,
+                    ls_status->file_id)) != NULL
+            && !blob_written) {
+
+        char absolute_path[GUAC_RDP_FS_MAX_PATH];
+
+        /* Skip current and parent directory entries */
+        if (strcmp(filename, ".") == 0 || strcmp(filename, "..") == 0)
+            continue;
+
+        /* Concatenate into absolute path - skip if invalid */
+        if (!guac_rdp_fs_append_filename(absolute_path,
+                    ls_status->directory_name, filename)) {
+
+            guac_user_log(user, GUAC_LOG_DEBUG,
+                    "Skipping filename \"%s\" - filename is invalid or "
+                    "resulting path is too long", filename);
+
+            continue;
+        }
+
+        /* Attempt to open file to determine type */
+        int file_id = guac_rdp_fs_open(ls_status->fs, absolute_path,
+                GENERIC_READ, 0, FILE_OPEN, 0);
+        if (file_id < 0)
+            continue;
+
+        /* Get opened file */
+        guac_rdp_fs_file* file = guac_rdp_fs_get_file(ls_status->fs, file_id);
+        if (file == NULL) {
+            guac_user_log(user, GUAC_LOG_DEBUG, "%s: Successful open produced "
+                    "bad file_id: %i", __func__, file_id);
+            return 0;
+        }
+
+        /* Determine mimetype */
+        const char* mimetype;
+        if (file->attributes & FILE_ATTRIBUTE_DIRECTORY)
+            mimetype = GUAC_USER_STREAM_INDEX_MIMETYPE;
+        else
+            mimetype = "application/octet-stream";
+
+        /* Write entry */
+        blob_written |= guac_common_json_write_property(user, stream,
+                &ls_status->json_state, absolute_path, mimetype);
+
+        guac_rdp_fs_close(ls_status->fs, file_id);
+
+    }
+
+    /* Complete JSON and cleanup at end of directory */
+    if (filename == NULL) {
+
+        /* Complete JSON object */
+        guac_common_json_end_object(user, stream, &ls_status->json_state);
+        guac_common_json_flush(user, stream, &ls_status->json_state);
+
+        /* Clean up resources */
+        guac_rdp_fs_close(ls_status->fs, ls_status->file_id);
+        free(ls_status);
+
+        /* Signal of stream */
+        guac_protocol_send_end(user->socket, stream);
+        guac_user_free_stream(user, stream);
+
+    }
+
+    guac_socket_flush(user->socket);
+    return 0;
+
+}
+
diff --git a/src/protocols/rdp/ls.h b/src/protocols/rdp/ls.h
new file mode 100644
index 0000000..be6a10c
--- /dev/null
+++ b/src/protocols/rdp/ls.h
@@ -0,0 +1,66 @@
+/*
+ * 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_LS_H
+#define GUAC_RDP_LS_H
+
+#include "common/json.h"
+#include "fs.h"
+
+#include <guacamole/protocol.h>
+#include <guacamole/stream.h>
+#include <guacamole/user.h>
+
+#include <stdint.h>
+
+/**
+ * The current state of a directory listing operation.
+ */
+typedef struct guac_rdp_ls_status {
+
+    /**
+     * The filesystem associated with the directory being listed.
+     */
+    guac_rdp_fs* fs;
+
+    /**
+     * The file ID of the directory being listed.
+     */
+    int file_id;
+
+    /**
+     * The absolute path of the directory being listed.
+     */
+    char directory_name[GUAC_RDP_FS_MAX_PATH];
+
+    /**
+     * The current state of the JSON directory object being written.
+     */
+    guac_common_json_state json_state;
+
+} guac_rdp_ls_status;
+
+/**
+ * Handler for ack messages received due to receipt of a "body" or "blob"
+ * instruction associated with a directory list operation.
+ */
+guac_user_ack_handler guac_rdp_ls_ack_handler;
+
+#endif
+
diff --git a/src/protocols/rdp/plugins/channels.c b/src/protocols/rdp/plugins/channels.c
new file mode 100644
index 0000000..0048b89
--- /dev/null
+++ b/src/protocols/rdp/plugins/channels.c
@@ -0,0 +1,142 @@
+/*
+ * 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 "plugins/channels.h"
+#include "rdp.h"
+
+#include <freerdp/channels/channels.h>
+#include <freerdp/freerdp.h>
+#include <freerdp/addin.h>
+#include <winpr/wtypes.h>
+
+#include <stdarg.h>
+#include <stdlib.h>
+#include <string.h>
+
+int guac_rdp_wrapped_entry_ex_count = 0;
+
+int guac_rdp_wrapped_entry_count = 0;
+
+PVIRTUALCHANNELENTRYEX guac_rdp_wrapped_entry_ex[GUAC_RDP_MAX_CHANNELS] = { NULL };
+
+PVIRTUALCHANNELENTRY guac_rdp_wrapped_entry[GUAC_RDP_MAX_CHANNELS] = { NULL };
+
+PVIRTUALCHANNELENTRYEX guac_rdp_plugin_wrap_entry_ex(guac_client* client,
+        PVIRTUALCHANNELENTRYEX entry_ex) {
+
+    /* Do not wrap if there is insufficient space to store the wrapped
+     * function */
+    if (guac_rdp_wrapped_entry_ex_count == GUAC_RDP_MAX_CHANNELS) {
+        guac_client_log(client, GUAC_LOG_WARNING, "Maximum number of static "
+                "channels has been reached. Further FreeRDP plugins and "
+                "channel support may fail to load.");
+        return entry_ex;
+    }
+
+    /* Generate wrapped version of provided entry point */
+    PVIRTUALCHANNELENTRYEX wrapper = guac_rdp_entry_ex_wrappers[guac_rdp_wrapped_entry_ex_count];
+    guac_rdp_wrapped_entry_ex[guac_rdp_wrapped_entry_ex_count] = entry_ex;
+    guac_rdp_wrapped_entry_ex_count++;
+
+    return wrapper;
+
+}
+
+PVIRTUALCHANNELENTRY guac_rdp_plugin_wrap_entry(guac_client* client,
+        PVIRTUALCHANNELENTRY entry) {
+
+    /* Do not wrap if there is insufficient space to store the wrapped
+     * function */
+    if (guac_rdp_wrapped_entry_count == GUAC_RDP_MAX_CHANNELS) {
+        guac_client_log(client, GUAC_LOG_WARNING, "Maximum number of static "
+                "channels has been reached. Further FreeRDP plugins and "
+                "channel support may fail to load.");
+        return entry;
+    }
+
+    /* Generate wrapped version of provided entry point */
+    PVIRTUALCHANNELENTRY wrapper = guac_rdp_entry_wrappers[guac_rdp_wrapped_entry_count];
+    guac_rdp_wrapped_entry[guac_rdp_wrapped_entry_count] = entry;
+    guac_rdp_wrapped_entry_count++;
+
+    return wrapper;
+
+}
+
+int guac_freerdp_channels_load_plugin(rdpContext* context,
+        const char* name, void* data) {
+
+    guac_client* client = ((rdp_freerdp_context*) context)->client;
+
+    /* Load plugin using "ex" version of the channel plugin entry point, if it exists */
+    PVIRTUALCHANNELENTRYEX entry_ex = (PVIRTUALCHANNELENTRYEX) (void*) freerdp_load_channel_addin_entry(name,
+            NULL, NULL, FREERDP_ADDIN_CHANNEL_STATIC | FREERDP_ADDIN_CHANNEL_ENTRYEX);
+
+    if (entry_ex != NULL) {
+        entry_ex = guac_rdp_plugin_wrap_entry_ex(client, entry_ex);
+        return freerdp_channels_client_load_ex(context->channels, context->settings, entry_ex, data);
+    }
+
+    /* Lacking the "ex" entry point, attempt to load using the non-ex version */
+    PVIRTUALCHANNELENTRY entry = freerdp_load_channel_addin_entry(name,
+            NULL, NULL, FREERDP_ADDIN_CHANNEL_STATIC);
+
+    if (entry != NULL) {
+        entry = guac_rdp_plugin_wrap_entry(client, entry);
+        return freerdp_channels_client_load(context->channels, context->settings, entry, data);
+    }
+
+    /* The plugin does not exist / cannot be loaded */
+    return 1;
+
+}
+
+void guac_freerdp_dynamic_channel_collection_add(rdpSettings* settings,
+        const char* name, ...) {
+
+    va_list args;
+
+    ADDIN_ARGV* freerdp_args = malloc(sizeof(ADDIN_ARGV));
+
+    va_start(args, name);
+
+    /* Count number of arguments (excluding terminating NULL) */
+    freerdp_args->argc = 1;
+    while (va_arg(args, char*) != NULL)
+        freerdp_args->argc++;
+
+    /* Reset va_list */
+    va_end(args);
+    va_start(args, name);
+
+    /* Copy argument values into DVC entry */
+    freerdp_args->argv = malloc(sizeof(char*) * freerdp_args->argc);
+    freerdp_args->argv[0] = strdup(name);
+    int i;
+    for (i = 1; i < freerdp_args->argc; i++)
+        freerdp_args->argv[i] = strdup(va_arg(args, char*));
+
+    va_end(args);
+
+    /* Register plugin with FreeRDP */
+    settings->SupportDynamicChannels = TRUE;
+    freerdp_dynamic_channel_collection_add(settings, freerdp_args);
+
+}
+
diff --git a/src/protocols/rdp/plugins/channels.h b/src/protocols/rdp/plugins/channels.h
new file mode 100644
index 0000000..b1207e2
--- /dev/null
+++ b/src/protocols/rdp/plugins/channels.h
@@ -0,0 +1,207 @@
+/*
+ * 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_PLUGINS_CHANNELS_H
+#define GUAC_RDP_PLUGINS_CHANNELS_H
+
+#include <freerdp/channels/channels.h>
+#include <freerdp/freerdp.h>
+#include <freerdp/settings.h>
+#include <guacamole/client.h>
+#include <winpr/wtsapi.h>
+
+/**
+ * The maximum number of static channels supported by Guacamole's RDP support.
+ * This value should be given a value which is at least the value of FreeRDP's
+ * CHANNEL_MAX_COUNT.
+ *
+ * NOTE: The value of this macro must be specified statically (not as a
+ * reference to CHANNEL_MAX_COUNT), as its value is extracted and used by the
+ * entry point wrapper code generator (generate-entry-wrappers.pl).
+ */
+#define GUAC_RDP_MAX_CHANNELS 64
+
+/* Validate GUAC_RDP_MAX_CHANNELS is sane at compile time */
+#if GUAC_RDP_MAX_CHANNELS < CHANNEL_MAX_COUNT
+#error "GUAC_RDP_MAX_CHANNELS must not be less than CHANNEL_MAX_COUNT"
+#endif
+
+/** Loads the FreeRDP plugin having the given name. With the exception that
+ * this function requires the rdpContext rather than rdpChannels and
+ * rdpSettings, this function is essentially a drop-in replacement for
+ * freerdp_channels_load_plugin() which additionally loads plugins implementing
+ * the PVIRTUALCHANNELENTRYEX version of the channel plugin entry point. The
+ * freerdp_channels_load_plugin() function which is part of FreeRDP can load
+ * only plugins which implement the PVIRTUALCHANNELENTRY version of the entry
+ * point.
+ *
+ * This MUST be called within the PreConnect callback of the freerdp instance
+ * for the referenced plugin to be loaded correctly.
+ *
+ * @param context
+ *     The rdpContext associated with the active RDP session.
+ *
+ * @param name
+ *     The name of the plugin to load. If the plugin is not statically built
+ *     into FreeRDP, this name will determine the filename of the library to be
+ *     loaded dynamically. For a plugin named "NAME", the library called
+ *     "libNAME-client" will be loaded from the "freerdp2" subdirectory of the
+ *     main directory containing the FreeRDP libraries.
+ *
+ * @param data
+ *     Arbitrary data to be passed to the plugin entry point. For most plugins
+ *     which are built into FreeRDP, this will be another reference to the
+ *     rdpSettings struct. The source of the relevant plugin must be consulted
+ *     to determine the proper value to pass here.
+ *
+ * @return
+ *     Zero if the plugin was loaded successfully, non-zero if the plugin could
+ *     not be loaded.
+ */
+int guac_freerdp_channels_load_plugin(rdpContext* context,
+        const char* name, void* data);
+
+/**
+ * Schedules loading of the FreeRDP dynamic virtual channel plugin having the
+ * given name. This function is essentially a wrapper for
+ * freerdp_dynamic_channel_collection_add() which additionally takes care of
+ * housekeeping tasks which would otherwise need to be performed manually:
+ *
+ *  - The ADDIN_ARGV structure used to pass arguments to dynamic virtual
+ *    channel plugins is automatically allocated and populated with any given
+ *    arguments.
+ *  - The SupportDynamicChannels member of the rdpSettings structure is
+ *    automatically set to TRUE.
+ *
+ * The "drdynvc" plugin must still eventually be loaded for this function to
+ * have any effect, as it is the "drdynvc" plugin which processes the
+ * collection this function manipulates.
+ *
+ * This MUST be called within the PreConnect callback of the freerdp instance
+ * and the "drdynvc" plugin MUST be loaded at some point after this function is
+ * called for the referenced dynamic channel plugin to be loaded correctly.
+ *
+ * @param settings
+ *     The rdpSettings structure associated with the FreeRDP instance, already
+ *     populated with any settings applicable to the plugin being loaded.
+ *
+ * @param name
+ *     The name of the plugin to load. If the plugin is not statically built
+ *     into FreeRDP, this name will determine the filename of the library to be
+ *     loaded dynamically. For a plugin named "NAME", the library called
+ *     "libNAME-client" will be loaded from the "freerdp2" subdirectory of the
+ *     main directory containing the FreeRDP libraries.
+ *
+ * @param ...
+ *     Arbitrary arguments to be passed to the plugin entry point. For most
+ *     plugins which are built into FreeRDP, this will be another reference to
+ *     the rdpSettings struct or NULL. The source of the relevant plugin must
+ *     be consulted to determine the proper value(s) to pass here.
+ */
+void guac_freerdp_dynamic_channel_collection_add(rdpSettings* settings,
+        const char* name, ...);
+
+/**
+ * The number of wrapped channel entry points currently stored within
+ * guac_rdp_wrapped_entry_ex.
+ */
+extern int guac_rdp_wrapped_entry_ex_count;
+
+/**
+ * All currently wrapped entry points that use the PVIRTUALCHANNELENTRYEX
+ * variant.
+ */
+extern PVIRTUALCHANNELENTRYEX guac_rdp_wrapped_entry_ex[GUAC_RDP_MAX_CHANNELS];
+
+/**
+ * Lookup table of wrapper functions for PVIRTUALCHANNELENTRYEX entry points.
+ * Each function within this array is generated at compile time by the entry
+ * point wrapper code generator (generate-entry-wrappers.pl) and automatically
+ * invokes the corresponding wrapped entry point stored within
+ * guac_rdp_wrapped_entry_ex.
+ */
+extern PVIRTUALCHANNELENTRYEX guac_rdp_entry_ex_wrappers[GUAC_RDP_MAX_CHANNELS];
+
+/**
+ * Wraps the provided entry point function, returning a different entry point
+ * which simply invokes the original. As long as this function is not invoked
+ * more than GUAC_RDP_MAX_CHANNELS times, each returned entry point will be
+ * unique, even if the provided entry point is not. As FreeRDP will refuse to
+ * load a plugin if its entry point is already loaded, this allows a single
+ * FreeRDP plugin to be loaded multiple times.
+ *
+ * @param client
+ *     The guac_client associated with the relevant RDP session.
+ *
+ * @param entry_ex
+ *     The entry point function to wrap.
+ *
+ * @return
+ *     A wrapped version of the provided entry point, or the unwrapped entry
+ *     point if there is insufficient space remaining within
+ *     guac_rdp_entry_ex_wrappers to wrap the entry point.
+ */
+PVIRTUALCHANNELENTRYEX guac_rdp_plugin_wrap_entry_ex(guac_client* client,
+        PVIRTUALCHANNELENTRYEX entry_ex);
+
+/**
+ * The number of wrapped channel entry points currently stored within
+ * guac_rdp_wrapped_entry.
+ */
+extern int guac_rdp_wrapped_entry_count;
+
+/**
+ * All currently wrapped entry points that use the PVIRTUALCHANNELENTRY
+ * variant.
+ */
+extern PVIRTUALCHANNELENTRY guac_rdp_wrapped_entry[GUAC_RDP_MAX_CHANNELS];
+
+/**
+ * Lookup table of wrapper functions for PVIRTUALCHANNELENTRY entry points.
+ * Each function within this array is generated at compile time by the entry
+ * point wrapper code generator (generate-entry-wrappers.pl) and automatically
+ * invokes the corresponding wrapped entry point stored within
+ * guac_rdp_wrapped_entry.
+ */
+extern PVIRTUALCHANNELENTRY guac_rdp_entry_wrappers[GUAC_RDP_MAX_CHANNELS];
+
+/**
+ * Wraps the provided entry point function, returning a different entry point
+ * which simply invokes the original. As long as this function is not invoked
+ * more than GUAC_RDP_MAX_CHANNELS times, each returned entry point will be
+ * unique, even if the provided entry point is not. As FreeRDP will refuse to
+ * load a plugin if its entry point is already loaded, this allows a single
+ * FreeRDP plugin to be loaded multiple times.
+ *
+ * @param client
+ *     The guac_client associated with the relevant RDP session.
+ *
+ * @param entry
+ *     The entry point function to wrap.
+ *
+ * @return
+ *     A wrapped version of the provided entry point, or the unwrapped entry
+ *     point if there is insufficient space remaining within
+ *     guac_rdp_entry_wrappers to wrap the entry point.
+ */
+PVIRTUALCHANNELENTRY guac_rdp_plugin_wrap_entry(guac_client* client,
+        PVIRTUALCHANNELENTRY entry);
+
+#endif
+
diff --git a/src/protocols/rdp/plugins/generate-entry-wrappers.pl b/src/protocols/rdp/plugins/generate-entry-wrappers.pl
new file mode 100755
index 0000000..f3fc003
--- /dev/null
+++ b/src/protocols/rdp/plugins/generate-entry-wrappers.pl
@@ -0,0 +1,77 @@
+#!/usr/bin/env perl
+#
+# 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.
+#
+
+#
+# generate-entry-wrappers.pl
+#
+# Generates C source which defines wrapper functions for FreeRDP plugin entry
+# points, allowing multiple instances of the same plugin to be loaded despite
+# otherwise always having the same entry point.
+#
+# The resulting source is stored within "_generated_channel_entry_wrappers.c".
+#
+
+use strict;
+
+##
+## The maximum number of static channels supported by Guacamole's RDP support.
+##
+my $GUAC_RDP_MAX_CHANNELS;
+
+# Extract value of GUAC_RDP_MAX_CHANNELS macro from provided source
+while (<>) {
+    if ((my $value) = m/^\s*#define\s+GUAC_RDP_MAX_CHANNELS\s+(\d+)\s*$/) {
+        $GUAC_RDP_MAX_CHANNELS = $value;
+    }
+}
+
+open OUTPUT, ">", "_generated_channel_entry_wrappers.c";
+
+# Generate required headers
+print OUTPUT <<"EOF";
+#include "plugins/channels.h"
+#include <freerdp/channels/channels.h>
+#include <freerdp/freerdp.h>
+EOF
+
+# Generate wrapper definitions for PVIRTUALCHANNELENTRYEX entry point variant
+print OUTPUT <<"EOF" for (1..$GUAC_RDP_MAX_CHANNELS);
+static BOOL guac_rdp_plugin_entry_ex_wrapper$_(PCHANNEL_ENTRY_POINTS_EX entry_points_ex, PVOID init_handle) {
+    return guac_rdp_wrapped_entry_ex[$_ - 1](entry_points_ex, init_handle);
+}
+EOF
+
+# Generate wrapper definitions for PVIRTUALCHANNELENTRY entry point variant
+print OUTPUT <<"EOF" for (1..$GUAC_RDP_MAX_CHANNELS);
+static BOOL guac_rdp_plugin_entry_wrapper$_(PCHANNEL_ENTRY_POINTS entry_points) {
+    return guac_rdp_wrapped_entry[$_ - 1](entry_points);
+}
+EOF
+
+# Populate lookup table of PVIRTUALCHANNELENTRYEX wrapper functions
+print OUTPUT "PVIRTUALCHANNELENTRYEX guac_rdp_entry_ex_wrappers[$GUAC_RDP_MAX_CHANNELS] = {\n";
+print OUTPUT "    guac_rdp_plugin_entry_ex_wrapper$_,\n" for (1..$GUAC_RDP_MAX_CHANNELS);
+print OUTPUT "};\n";
+
+# Populate lookup table of PVIRTUALCHANNELENTRY wrapper functions
+print OUTPUT "PVIRTUALCHANNELENTRY guac_rdp_entry_wrappers[$GUAC_RDP_MAX_CHANNELS] = {\n";
+print OUTPUT "    guac_rdp_plugin_entry_wrapper$_,\n" for (1..$GUAC_RDP_MAX_CHANNELS);
+print OUTPUT "};\n";
+
diff --git a/src/protocols/rdp/plugins/guac-common-svc/guac-common-svc.c b/src/protocols/rdp/plugins/guac-common-svc/guac-common-svc.c
new file mode 100644
index 0000000..fbba484
--- /dev/null
+++ b/src/protocols/rdp/plugins/guac-common-svc/guac-common-svc.c
@@ -0,0 +1,306 @@
+/*
+ * 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/common-svc.h"
+
+#include <freerdp/svc.h>
+#include <guacamole/client.h>
+#include <winpr/stream.h>
+#include <winpr/wtsapi.h>
+#include <winpr/wtypes.h>
+
+#include <stdlib.h>
+
+/**
+ * Event handler for events which deal with data transmitted over an open SVC.
+ * This specific implementation of the event handler currently handles only the
+ * CHANNEL_EVENT_DATA_RECEIVED event, delegating actual handling of that event
+ * to guac_rdp_common_svc_process_receive().
+ *
+ * The FreeRDP requirements for this function follow those of the
+ * VirtualChannelOpenEventEx callback defined within Microsoft's RDP API:
+ *
+ * https://docs.microsoft.com/en-us/previous-versions/windows/embedded/aa514754%28v%3dmsdn.10%29
+ *
+ * @param user_param
+ *     The pointer to arbitrary data originally passed via the first parameter
+ *     of the pVirtualChannelInitEx() function call when the associated channel
+ *     was initialized. The pVirtualChannelInitEx() function is exposed within
+ *     the channel entry points structure.
+ *
+ * @param open_handle
+ *     The handle which identifies the channel itself, typically referred to
+ *     within the FreeRDP source as OpenHandle.
+ *
+ * @param event
+ *     An integer representing the event that should be handled. This will be
+ *     either CHANNEL_EVENT_DATA_RECEIVED, CHANNEL_EVENT_WRITE_CANCELLED, or
+ *     CHANNEL_EVENT_WRITE_COMPLETE.
+ *
+ * @param data
+ *     The data received, for CHANNEL_EVENT_DATA_RECEIVED events, and the value
+ *     passed as user data to pVirtualChannelWriteEx() for
+ *     CHANNEL_EVENT_WRITE_* events (note that user data for
+ *     pVirtualChannelWriteEx() as implemented by FreeRDP MUST either be NULL
+ *     or a wStream containing the data written).
+ *
+ * @param data_length
+ *     The number of bytes of event-specific data.
+ *
+ * @param total_length
+ *     The total number of bytes expected to be received from the RDP server
+ *     due to this single write (from the server's perspective). Each write may
+ *     actually be split into multiple chunks, thus resulting in multiple
+ *     receive events for the same logical block of data. The relationship
+ *     between chunks is indicated with the CHANNEL_FLAG_FIRST and
+ *     CHANNEL_FLAG_LAST flags.
+ *
+ * @param data_flags
+ *     The result of a bitwise OR of the CHANNEL_FLAG_* flags which apply to
+ *     the data received. This value is relevant only to
+ *     CHANNEL_EVENT_DATA_RECEIVED events. Valid flags are CHANNEL_FLAG_FIRST,
+ *     CHANNEL_FLAG_LAST, and CHANNEL_FLAG_ONLY. The flag CHANNEL_FLAG_MIDDLE
+ *     is not itself a flag, but the absence of both CHANNEL_FLAG_FIRST and
+ *     CHANNEL_FLAG_LAST.
+ */
+static VOID guac_rdp_common_svc_handle_open_event(LPVOID user_param,
+        DWORD open_handle, UINT event, LPVOID data, UINT32 data_length,
+        UINT32 total_length, UINT32 data_flags) {
+
+    /* Ignore all events except for received data */
+    if (event != CHANNEL_EVENT_DATA_RECEIVED)
+        return;
+
+    guac_rdp_common_svc* svc = (guac_rdp_common_svc*) user_param;
+
+    /* Validate relevant handle matches that of SVC */
+    if (open_handle != svc->_open_handle) {
+        guac_client_log(svc->client, GUAC_LOG_WARNING, "%i bytes of data "
+                "received from within the remote desktop session for SVC "
+                "\"%s\" are being dropped because the relevant open handle "
+                "(0x%X) does not match the open handle of the SVC (0x%X).",
+                data_length, svc->name, open_handle, svc->_open_handle);
+        return;
+    }
+
+    /* If receiving first chunk, allocate sufficient space for all remaining
+     * chunks */
+    if (data_flags & CHANNEL_FLAG_FIRST) {
+
+        /* Limit maximum received size */
+        if (total_length > GUAC_SVC_MAX_ASSEMBLED_LENGTH) {
+            guac_client_log(svc->client, GUAC_LOG_WARNING, "RDP server has "
+                    "requested to send a sequence of %i bytes, but this "
+                    "exceeds the maximum buffer space of %i bytes. Received "
+                    "data may be truncated.", total_length,
+                    GUAC_SVC_MAX_ASSEMBLED_LENGTH);
+            total_length = GUAC_SVC_MAX_ASSEMBLED_LENGTH;
+        }
+
+        svc->_input_stream = Stream_New(NULL, total_length);
+    }
+
+    /* Add chunk to buffer only if sufficient space remains */
+    if (Stream_EnsureRemainingCapacity(svc->_input_stream, data_length))
+        Stream_Write(svc->_input_stream, data, data_length);
+    else
+        guac_client_log(svc->client, GUAC_LOG_WARNING, "%i bytes of data "
+                "received from within the remote desktop session for SVC "
+                "\"%s\" are being dropped because the maximum available "
+                "space for received data has been exceeded.", data_length,
+                svc->name);
+
+    /* Fire event once last chunk has been received */
+    if (data_flags & CHANNEL_FLAG_LAST) {
+
+        Stream_SealLength(svc->_input_stream);
+        Stream_SetPosition(svc->_input_stream, 0);
+
+        /* Handle channel-specific data receipt tasks, if any */
+        if (svc->_receive_handler)
+            svc->_receive_handler(svc, svc->_input_stream);
+
+        Stream_Free(svc->_input_stream, TRUE);
+
+    }
+
+}
+
+/**
+ * Processes a CHANNEL_EVENT_CONNECTED event, completing the
+ * connection/initialization process of the channel.
+ *
+ * @param rdpsnd
+ *     The guac_rdp_common_svc structure representing the channel.
+ */
+static void guac_rdp_common_svc_process_connect(guac_rdp_common_svc* svc) {
+
+    /* Open FreeRDP side of connected channel */
+    UINT32 open_status =
+        svc->_entry_points.pVirtualChannelOpenEx(svc->_init_handle,
+                &svc->_open_handle, svc->_channel_def.name,
+                guac_rdp_common_svc_handle_open_event);
+
+    /* Warn if the channel cannot be opened after all */
+    if (open_status != CHANNEL_RC_OK) {
+        guac_client_log(svc->client, GUAC_LOG_WARNING, "SVC \"%s\" could not "
+                "be opened: %s (error %i)", svc->name,
+                WTSErrorToString(open_status), open_status);
+        return;
+    }
+
+    /* Handle channel-specific connect tasks, if any */
+    if (svc->_connect_handler)
+        svc->_connect_handler(svc);
+
+    /* Channel is now ready */
+    guac_client_log(svc->client, GUAC_LOG_DEBUG, "SVC \"%s\" connected.",
+            svc->name);
+
+}
+
+/**
+ * Processes a CHANNEL_EVENT_TERMINATED event, freeing all resources associated
+ * with the channel.
+ *
+ * @param svc
+ *     The guac_rdp_common_svc structure representing the channel.
+ */
+static void guac_rdp_common_svc_process_terminate(guac_rdp_common_svc* svc) {
+
+    /* Handle channel-specific termination tasks, if any */
+    if (svc->_terminate_handler)
+        svc->_terminate_handler(svc);
+
+    guac_client_log(svc->client, GUAC_LOG_DEBUG, "SVC \"%s\" disconnected.",
+            svc->name);
+    free(svc);
+
+}
+
+/**
+ * Event handler for events which deal with the overall lifecycle of an SVC.
+ * This specific implementation of the event handler currently handles only
+ * CHANNEL_EVENT_CONNECTED and CHANNEL_EVENT_TERMINATED events, delegating
+ * actual handling of those events to guac_rdp_common_svc_process_connect() and
+ * guac_rdp_common_svc_process_terminate() respectively.
+ *
+ * The FreeRDP requirements for this function follow those of the
+ * VirtualChannelInitEventEx callback defined within Microsoft's RDP API:
+ *
+ * https://docs.microsoft.com/en-us/previous-versions/windows/embedded/aa514727%28v%3dmsdn.10%29
+ *
+ * @param user_param
+ *     The pointer to arbitrary data originally passed via the first parameter
+ *     of the pVirtualChannelInitEx() function call when the associated channel
+ *     was initialized. The pVirtualChannelInitEx() function is exposed within
+ *     the channel entry points structure.
+ *
+ * @param init_handle
+ *     The handle which identifies the client connection, typically referred to
+ *     within the FreeRDP source as pInitHandle.
+ *
+ * @param event
+ *     An integer representing the event that should be handled. This will be
+ *     either CHANNEL_EVENT_CONNECTED, CHANNEL_EVENT_DISCONNECTED,
+ *     CHANNEL_EVENT_INITIALIZED, CHANNEL_EVENT_TERMINATED, or
+ *     CHANNEL_EVENT_V1_CONNECTED.
+ *
+ * @param data
+ *     NULL in all cases except the CHANNEL_EVENT_CONNECTED event, in which
+ *     case this is a null-terminated string containing the name of the server.
+ *
+ * @param data_length
+ *     The number of bytes of data, if any.
+ */
+static VOID guac_rdp_common_svc_handle_init_event(LPVOID user_param,
+        LPVOID init_handle, UINT event, LPVOID data, UINT data_length) {
+
+    guac_rdp_common_svc* svc = (guac_rdp_common_svc*) user_param;
+
+    /* Validate relevant handle matches that of SVC */
+    if (init_handle != svc->_init_handle) {
+        guac_client_log(svc->client, GUAC_LOG_WARNING, "An init event (#%i) "
+                "for SVC \"%s\" has been dropped because the relevant init "
+                "handle (0x%X) does not match the init handle of the SVC "
+                "(0x%X).", event, svc->name, init_handle, svc->_init_handle);
+        return;
+    }
+
+    switch (event) {
+
+        /* The remote desktop side of the SVC has been connected */
+        case CHANNEL_EVENT_CONNECTED:
+            guac_rdp_common_svc_process_connect(svc);
+            break;
+
+        /* The channel has disconnected and now must be cleaned up */
+        case CHANNEL_EVENT_TERMINATED:
+            guac_rdp_common_svc_process_terminate(svc);
+            break;
+
+    }
+
+}
+
+/**
+ * Entry point for FreeRDP plugins. This function is automatically invoked when
+ * the plugin is loaded.
+ *
+ * @param entry_points
+ *     Functions and data specific to the FreeRDP side of the virtual channel
+ *     and plugin. This structure must be copied within implementation-specific
+ *     storage such that the functions it references can be invoked when
+ *     needed.
+ *
+ * @param init_handle
+ *     The handle which identifies the client connection, typically referred to
+ *     within the FreeRDP source as pInitHandle. This handle is also provided
+ *     to the channel init event handler. The handle must eventually be used
+ *     within the channel open event handler to obtain a handle to the channel
+ *     itself.
+ *
+ * @return
+ *     TRUE if the plugin has initialized successfully, FALSE otherwise.
+ */
+BOOL VirtualChannelEntryEx(PCHANNEL_ENTRY_POINTS_EX entry_points,
+        PVOID init_handle) {
+
+    CHANNEL_ENTRY_POINTS_FREERDP_EX* entry_points_ex =
+        (CHANNEL_ENTRY_POINTS_FREERDP_EX*) entry_points;
+
+    /* Get structure representing the Guacamole side of the SVC from plugin
+     * parameters */
+    guac_rdp_common_svc* svc = (guac_rdp_common_svc*) entry_points_ex->pExtendedData;
+
+    /* Copy FreeRDP data into SVC structure for future reference */
+    svc->_entry_points = *entry_points_ex;
+    svc->_init_handle = init_handle;
+
+    /* Complete initialization */
+    if (svc->_entry_points.pVirtualChannelInitEx(svc, svc, init_handle,
+                &svc->_channel_def, 1, VIRTUAL_CHANNEL_VERSION_WIN2000,
+                guac_rdp_common_svc_handle_init_event) != CHANNEL_RC_OK) {
+        return FALSE;
+    }
+
+    return TRUE;
+
+}
+
diff --git a/src/protocols/rdp/guac_ai/ai_messages.c b/src/protocols/rdp/plugins/guacai/guacai-messages.c
similarity index 95%
rename from src/protocols/rdp/guac_ai/ai_messages.c
rename to src/protocols/rdp/plugins/guacai/guacai-messages.c
index c346587..38f7a7c 100644
--- a/src/protocols/rdp/guac_ai/ai_messages.c
+++ b/src/protocols/rdp/plugins/guacai/guacai-messages.c
@@ -17,24 +17,15 @@
  * under the License.
  */
 
-#include "config.h"
-
-#include "ai_messages.h"
-#include "audio_input.h"
+#include "channels/audio-input/audio-buffer.h"
+#include "plugins/guacai/guacai-messages.h"
 #include "rdp.h"
 
-#include <stdlib.h>
-
-#include <freerdp/freerdp.h>
-#include <freerdp/constants.h>
 #include <freerdp/dvc.h>
 #include <guacamole/client.h>
-
-#ifdef ENABLE_WINPR
 #include <winpr/stream.h>
-#else
-#include "compat/winpr-stream.h"
-#endif
+
+#include <stdlib.h>
 
 /**
  * Reads AUDIO_FORMAT data from the given stream into the given struct.
@@ -113,6 +104,19 @@
 
 }
 
+/**
+ * Sends a Data PDU along the given channel. A Data PDU is used by the client
+ * to send actual audio data following a Data Incoming PDU.
+ *
+ * @param channel
+ *     The channel along which the PDU should be sent.
+ *
+ * @param buffer
+ *     The audio data to send.
+ *
+ * @param length
+ *     The number of bytes of audio data to send.
+ */
 static void guac_rdp_ai_send_data(IWTSVirtualChannel* channel,
         char* buffer, int length) {
 
@@ -286,7 +290,7 @@
 
 }
 
-static void guac_rdp_ai_flush_packet(char* buffer, int length, void* data) {
+void guac_rdp_ai_flush_packet(char* buffer, int length, void* data) {
 
     IWTSVirtualChannel* channel = (IWTSVirtualChannel*) data;
 
diff --git a/src/protocols/rdp/guac_ai/ai_messages.h b/src/protocols/rdp/plugins/guacai/guacai-messages.h
similarity index 91%
rename from src/protocols/rdp/guac_ai/ai_messages.h
rename to src/protocols/rdp/plugins/guacai/guacai-messages.h
index 55cf6e9..6a2333f 100644
--- a/src/protocols/rdp/guac_ai/ai_messages.h
+++ b/src/protocols/rdp/plugins/guacai/guacai-messages.h
@@ -17,19 +17,15 @@
  * under the License.
  */
 
-#ifndef GUAC_RDP_AI_MESSAGES_H
-#define GUAC_RDP_AI_MESSAGES_H
+#ifndef GUAC_RDP_PLUGINS_GUACAI_MESSAGES_H
+#define GUAC_RDP_PLUGINS_GUACAI_MESSAGES_H
 
-#include "config.h"
+#include "channels/audio-input/audio-buffer.h"
 
 #include <freerdp/dvc.h>
 #include <guacamole/client.h>
-
-#ifdef ENABLE_WINPR
 #include <winpr/stream.h>
-#else
-#include "compat/winpr-stream.h"
-#endif
+#include <winpr/wtypes.h>
 
 /**
  * The format tag associated with raw wave audio (WAVE_FORMAT_PCM). This format
@@ -212,5 +208,14 @@
 void guac_rdp_ai_process_formatchange(guac_client* client,
         IWTSVirtualChannel* channel, wStream* stream);
 
+/**
+ * Audio buffer flush handler which sends audio data along the active audio
+ * input channel using a Data Incoming PDU and Data PDU. The arbitrary data
+ * provided to the handler by the audio buffer implementation is in this case
+ * the IWTSVirtualChannel structure representing the active audio input
+ * channel.
+ */
+guac_rdp_audio_buffer_flush_handler guac_rdp_ai_flush_packet;
+
 #endif
 
diff --git a/src/protocols/rdp/guac_ai/ai_service.c b/src/protocols/rdp/plugins/guacai/guacai.c
similarity index 81%
rename from src/protocols/rdp/guac_ai/ai_service.c
rename to src/protocols/rdp/plugins/guacai/guacai.c
index 5058ea0..15de865 100644
--- a/src/protocols/rdp/guac_ai/ai_service.c
+++ b/src/protocols/rdp/plugins/guacai/guacai.c
@@ -17,27 +17,20 @@
  * under the License.
  */
 
-#include "config.h"
-
-#include "ai_messages.h"
-#include "ai_service.h"
-#include "audio_input.h"
-#include "ptr_string.h"
+#include "channels/audio-input/audio-buffer.h"
+#include "plugins/guacai/guacai.h"
+#include "plugins/guacai/guacai-messages.h"
+#include "plugins/ptr-string.h"
 #include "rdp.h"
 
-#include <stdlib.h>
-#include <string.h>
-
-#include <freerdp/freerdp.h>
-#include <freerdp/constants.h>
 #include <freerdp/dvc.h>
+#include <freerdp/settings.h>
 #include <guacamole/client.h>
-
-#ifdef ENABLE_WINPR
 #include <winpr/stream.h>
-#else
-#include "compat/winpr-stream.h"
-#endif
+#include <winpr/wtsapi.h>
+#include <winpr/wtypes.h>
+
+#include <stdlib.h>
 
 /**
  * Handles the given data received along the AUDIO_INPUT channel of the RDP
@@ -95,43 +88,9 @@
 
 }
 
-#ifdef LEGACY_IWTSVIRTUALCHANNELCALLBACK
 /**
  * Callback which is invoked when data is received along a connection to the
- * AUDIO_INPUT plugin. This callback is specific to FreeRDP 1.1 and older.
- *
- * @param channel_callback
- *     The IWTSVirtualChannelCallback structure to which this callback was
- *     originally assigned.
- *
- * @param size
- *     The number of bytes received.
- *
- * @param buffer
- *     A buffer containing all bytes received.
- *
- * @return
- *     Always zero.
- */
-static int guac_rdp_ai_data(IWTSVirtualChannelCallback* channel_callback,
-        UINT32 size, BYTE* buffer) {
-
-    guac_rdp_ai_channel_callback* ai_channel_callback =
-        (guac_rdp_ai_channel_callback*) channel_callback;
-    IWTSVirtualChannel* channel = ai_channel_callback->channel;
-
-    /* Invoke generalized (API-independent) data handler */
-    wStream* stream = Stream_New(buffer, size);
-    guac_rdp_ai_handle_data(ai_channel_callback->client, channel, stream);
-    Stream_Free(stream, FALSE);
-
-    return 0;
-
-}
-#else
-/**
- * Callback which is invoked when data is received along a connection to the
- * AUDIO_INPUT plugin. This callback is specific to FreeRDP 1.2 and newer.
+ * AUDIO_INPUT plugin.
  *
  * @param channel_callback
  *     The IWTSVirtualChannelCallback structure to which this callback was
@@ -143,7 +102,7 @@
  * @return
  *     Always zero.
  */
-static int guac_rdp_ai_data(IWTSVirtualChannelCallback* channel_callback,
+static UINT guac_rdp_ai_data(IWTSVirtualChannelCallback* channel_callback,
         wStream* stream) {
 
     guac_rdp_ai_channel_callback* ai_channel_callback =
@@ -153,10 +112,9 @@
     /* Invoke generalized (API-independent) data handler */
     guac_rdp_ai_handle_data(ai_channel_callback->client, channel, stream);
 
-    return 0;
+    return CHANNEL_RC_OK;
 
 }
-#endif
 
 /**
  * Callback which is invoked when a connection to the AUDIO_INPUT plugin is
@@ -169,7 +127,7 @@
  * @return
  *     Always zero.
  */
-static int guac_rdp_ai_close(IWTSVirtualChannelCallback* channel_callback) {
+static UINT guac_rdp_ai_close(IWTSVirtualChannelCallback* channel_callback) {
 
     guac_rdp_ai_channel_callback* ai_channel_callback =
         (guac_rdp_ai_channel_callback*) channel_callback;
@@ -184,7 +142,7 @@
 
     guac_rdp_audio_buffer_end(audio_buffer);
     free(ai_channel_callback);
-    return 0;
+    return CHANNEL_RC_OK;
 
 }
 
@@ -219,7 +177,7 @@
  * @return
  *     Always zero.
  */
-static int guac_rdp_ai_new_connection(
+static UINT guac_rdp_ai_new_connection(
         IWTSListenerCallback* listener_callback, IWTSVirtualChannel* channel,
         BYTE* data, int* accept,
         IWTSVirtualChannelCallback** channel_callback) {
@@ -243,7 +201,8 @@
 
     /* Return callback through pointer */
     *channel_callback = (IWTSVirtualChannelCallback*) ai_channel_callback;
-    return 0;
+
+    return CHANNEL_RC_OK;
 
 }
 
@@ -261,7 +220,7 @@
  * @return
  *     Always zero.
  */
-static int guac_rdp_ai_initialize(IWTSPlugin* plugin,
+static UINT guac_rdp_ai_initialize(IWTSPlugin* plugin,
         IWTSVirtualChannelManager* manager) {
 
     /* Allocate new listener callback */
@@ -281,7 +240,7 @@
     manager->CreateListener(manager, "AUDIO_INPUT", 0,
             (IWTSListenerCallback*) ai_listener_callback, NULL);
 
-    return 0;
+    return CHANNEL_RC_OK;
 
 }
 
@@ -295,7 +254,7 @@
  * @return
  *     Always zero.
  */
-static int guac_rdp_ai_terminated(IWTSPlugin* plugin) {
+static UINT guac_rdp_ai_terminated(IWTSPlugin* plugin) {
 
     guac_rdp_ai_plugin* ai_plugin = (guac_rdp_ai_plugin*) plugin;
     guac_client* client = ai_plugin->client;
@@ -305,7 +264,7 @@
     free(ai_plugin);
 
     guac_client_log(client, GUAC_LOG_DEBUG, "AUDIO_INPUT plugin unloaded.");
-    return 0;
+    return CHANNEL_RC_OK;
 
 }
 
@@ -315,13 +274,8 @@
 int DVCPluginEntry(IDRDYNVC_ENTRY_POINTS* pEntryPoints) {
 
     /* Pull guac_client from arguments */
-#ifdef HAVE_ADDIN_ARGV
     ADDIN_ARGV* args = pEntryPoints->GetPluginData(pEntryPoints);
     guac_client* client = (guac_client*) guac_rdp_string_to_ptr(args->argv[1]);
-#else
-    RDP_PLUGIN_DATA* data = pEntryPoints->GetPluginData(pEntryPoints);
-    guac_client* client = (guac_client*) guac_rdp_string_to_ptr(data->data[1]);
-#endif
 
     /* Pull previously-allocated plugin */
     guac_rdp_ai_plugin* ai_plugin = (guac_rdp_ai_plugin*)
@@ -343,7 +297,7 @@
         guac_client_log(client, GUAC_LOG_DEBUG, "AUDIO_INPUT plugin loaded.");
     }
 
-    return 1;
+    return CHANNEL_RC_OK;
 
 }
 
diff --git a/src/protocols/rdp/guac_ai/ai_service.h b/src/protocols/rdp/plugins/guacai/guacai.h
similarity index 96%
rename from src/protocols/rdp/guac_ai/ai_service.h
rename to src/protocols/rdp/plugins/guacai/guacai.h
index 28f2227..d26563c 100644
--- a/src/protocols/rdp/guac_ai/ai_service.h
+++ b/src/protocols/rdp/plugins/guacai/guacai.h
@@ -17,14 +17,12 @@
  * under the License.
  */
 
-#ifndef GUAC_RDP_AI_SERVICE_H
-#define GUAC_RDP_AI_SERVICE_H
+#ifndef GUAC_RDP_PLUGINS_GUACAI_H
+#define GUAC_RDP_PLUGINS_GUACAI_H
 
-#include "config.h"
-
-#include <freerdp/freerdp.h>
 #include <freerdp/constants.h>
 #include <freerdp/dvc.h>
+#include <freerdp/freerdp.h>
 #include <guacamole/client.h>
 
 /**
diff --git a/src/protocols/rdp/ptr_string.c b/src/protocols/rdp/plugins/ptr-string.c
similarity index 91%
rename from src/protocols/rdp/ptr_string.c
rename to src/protocols/rdp/plugins/ptr-string.c
index 5ec1b62..324a519 100644
--- a/src/protocols/rdp/ptr_string.c
+++ b/src/protocols/rdp/plugins/ptr-string.c
@@ -17,13 +17,9 @@
  * under the License.
  */
 
-#include "config.h"
-#include "ptr_string.h"
-
-#include <guacamole/client.h>
+#include "plugins/ptr-string.h"
 
 #include <stdio.h>
-#include <stdlib.h>
 
 void guac_rdp_ptr_to_string(void* data, char* str) {
 
diff --git a/src/protocols/rdp/ptr_string.h b/src/protocols/rdp/plugins/ptr-string.h
similarity index 95%
rename from src/protocols/rdp/ptr_string.h
rename to src/protocols/rdp/plugins/ptr-string.h
index 9a9156b..ef682b1 100644
--- a/src/protocols/rdp/ptr_string.h
+++ b/src/protocols/rdp/plugins/ptr-string.h
@@ -17,10 +17,8 @@
  * under the License.
  */
 
-#ifndef GUAC_RDP_PTR_STRING_H
-#define GUAC_RDP_PTR_STRING_H
-
-#include "config.h"
+#ifndef GUAC_RDP_PLUGINS_PTR_STRING_H
+#define GUAC_RDP_PLUGINS_PTR_STRING_H
 
 #include <guacamole/client.h>
 
diff --git a/src/protocols/rdp/rdp_pointer.c b/src/protocols/rdp/pointer.c
similarity index 62%
rename from src/protocols/rdp/rdp_pointer.c
rename to src/protocols/rdp/pointer.c
index f510b57..5a72c78 100644
--- a/src/protocols/rdp/rdp_pointer.c
+++ b/src/protocols/rdp/pointer.c
@@ -17,21 +17,21 @@
  * under the License.
  */
 
-#include "config.h"
-
-#include "client.h"
+#include "color.h"
 #include "common/cursor.h"
 #include "common/display.h"
+#include "common/surface.h"
+#include "pointer.h"
 #include "rdp.h"
-#include "rdp_pointer.h"
 
 #include <cairo/cairo.h>
+#include <freerdp/codec/color.h>
 #include <freerdp/freerdp.h>
+#include <freerdp/gdi/gdi.h>
 #include <guacamole/client.h>
+#include <winpr/crt.h>
 
-#include <stdlib.h>
-
-void guac_rdp_pointer_new(rdpContext* context, rdpPointer* pointer) {
+BOOL guac_rdp_pointer_new(rdpContext* context, rdpPointer* pointer) {
 
     guac_client* client = ((rdp_freerdp_context*) context)->client;
     guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
@@ -41,17 +41,18 @@
             rdp_client->display, pointer->width, pointer->height);
 
     /* Allocate data for image */
-    unsigned char* data =
-        (unsigned char*) malloc(pointer->width * pointer->height * 4);
+    unsigned char* data = _aligned_malloc(pointer->width * pointer->height * 4, 16);
 
     cairo_surface_t* surface;
 
     /* Convert to alpha cursor if mask data present */
     if (pointer->andMaskData && pointer->xorMaskData)
-        freerdp_alpha_cursor_convert(data,
-                pointer->xorMaskData, pointer->andMaskData,
-                pointer->width, pointer->height, pointer->xorBpp,
-                ((rdp_freerdp_context*) context)->clrconv);
+        freerdp_image_copy_from_pointer_data(data,
+                guac_rdp_get_native_pixel_format(TRUE), 0, 0, 0,
+                pointer->width, pointer->height, pointer->xorMaskData,
+                pointer->lengthXorMask, pointer->andMaskData,
+                pointer->lengthAndMask, pointer->xorBpp,
+                &context->gdi->palette);
 
     /* Create surface from image data */
     surface = cairo_image_surface_create_for_data(
@@ -63,14 +64,16 @@
 
     /* Free surface */
     cairo_surface_destroy(surface);
-    free(data);
+    _aligned_free(data);
 
     /* Remember buffer */
     ((guac_rdp_pointer*) pointer)->layer = buffer;
 
+    return TRUE;
+
 }
 
-void guac_rdp_pointer_set(rdpContext* context, rdpPointer* pointer) {
+BOOL guac_rdp_pointer_set(rdpContext* context, const rdpPointer* pointer) {
 
     guac_client* client = ((rdp_freerdp_context*) context)->client;
     guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
@@ -80,6 +83,8 @@
             pointer->xPos, pointer->yPos,
             ((guac_rdp_pointer*) pointer)->layer->surface);
 
+    return TRUE;
+
 }
 
 void guac_rdp_pointer_free(rdpContext* context, rdpPointer* pointer) {
@@ -91,13 +96,31 @@
     /* Free buffer */
     guac_common_display_free_buffer(rdp_client->display, buffer);
 
+    /* NOTE: FreeRDP-allocated memory for the rdpPointer will be automatically
+     * released after this free handler is invoked */
+
 }
 
-void guac_rdp_pointer_set_null(rdpContext* context) {
-    /* STUB */
+BOOL guac_rdp_pointer_set_null(rdpContext* context) {
+
+    guac_client* client = ((rdp_freerdp_context*) context)->client;
+    guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
+
+    /* Set cursor to empty/blank graphic */
+    guac_common_cursor_set_blank(rdp_client->display->cursor);
+
+    return TRUE;
+
 }
 
-void guac_rdp_pointer_set_default(rdpContext* context) {
-    /* STUB */
+BOOL guac_rdp_pointer_set_default(rdpContext* context) {
+
+    guac_client* client = ((rdp_freerdp_context*) context)->client;
+    guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
+
+    /* Set cursor to embedded pointer */
+    guac_common_cursor_set_pointer(rdp_client->display->cursor);
+
+    return TRUE;
 }
 
diff --git a/src/protocols/rdp/rdp_pointer.h b/src/protocols/rdp/pointer.h
similarity index 80%
rename from src/protocols/rdp/rdp_pointer.h
rename to src/protocols/rdp/pointer.h
index 995c8d6..b5fa6a2 100644
--- a/src/protocols/rdp/rdp_pointer.h
+++ b/src/protocols/rdp/pointer.h
@@ -17,14 +17,14 @@
  * under the License.
  */
 
+#ifndef GUAC_RDP_POINTER_H
+#define GUAC_RDP_POINTER_H
 
-#ifndef _GUAC_RDP_RDP_POINTER_H
-#define _GUAC_RDP_RDP_POINTER_H
-
-#include "config.h"
 #include "common/display.h"
 
 #include <freerdp/freerdp.h>
+#include <freerdp/graphics.h>
+#include <winpr/wtypes.h>
 
 /**
  * Guacamole-specific rdpPointer data.
@@ -52,8 +52,11 @@
  *
  * @param pointer
  *     The pointer to cache.
+ *
+ * @return
+ *     TRUE if successful, FALSE otherwise.
  */
-void guac_rdp_pointer_new(rdpContext* context, rdpPointer* pointer);
+BOOL guac_rdp_pointer_new(rdpContext* context, rdpPointer* pointer);
 
 /**
  * Sets the given cached pointer as the current pointer. The given pointer must
@@ -64,8 +67,11 @@
  *
  * @param pointer
  *     The pointer to set as the current mouse pointer.
+ *
+ * @return
+ *     TRUE if successful, FALSE otherwise.
  */
-void guac_rdp_pointer_set(rdpContext* context, rdpPointer* pointer);
+BOOL guac_rdp_pointer_set(rdpContext* context, const rdpPointer* pointer);
 
 /**
  * Frees all Guacamole-related data associated with the given pointer, allowing
@@ -84,8 +90,11 @@
  *
  * @param context
  *     The rdpContext associated with the current RDP session.
+ *
+ * @return
+ *     TRUE if successful, FALSE otherwise.
  */
-void guac_rdp_pointer_set_null(rdpContext* context);
+BOOL guac_rdp_pointer_set_null(rdpContext* context);
 
 /**
  * Sets the system-dependent (as in dependent on the client system) default
@@ -93,7 +102,10 @@
  *
  * @param context
  *     The rdpContext associated with the current RDP session.
+ *
+ * @return
+ *     TRUE if successful, FALSE otherwise.
  */
-void guac_rdp_pointer_set_default(rdpContext* context);
+BOOL guac_rdp_pointer_set_default(rdpContext* context);
 
 #endif
diff --git a/src/protocols/rdp/rdp_print_job.c b/src/protocols/rdp/print-job.c
similarity index 99%
rename from src/protocols/rdp/rdp_print_job.c
rename to src/protocols/rdp/print-job.c
index a3b51bc..5a75c6e 100644
--- a/src/protocols/rdp/rdp_print_job.c
+++ b/src/protocols/rdp/print-job.c
@@ -17,9 +17,7 @@
  * under the License.
  */
 
-
-#include "config.h"
-#include "rdp_print_job.h"
+#include "print-job.h"
 
 #include <guacamole/client.h>
 #include <guacamole/protocol.h>
diff --git a/src/protocols/rdp/rdp_print_job.h b/src/protocols/rdp/print-job.h
similarity index 98%
rename from src/protocols/rdp/rdp_print_job.h
rename to src/protocols/rdp/print-job.h
index 5682f18..fb990a8 100644
--- a/src/protocols/rdp/rdp_print_job.h
+++ b/src/protocols/rdp/print-job.h
@@ -20,8 +20,6 @@
 #ifndef GUAC_RDP_PRINT_JOB_H
 #define GUAC_RDP_PRINT_JOB_H
 
-#include "config.h"
-
 #include <guacamole/client.h>
 #include <guacamole/stream.h>
 #include <guacamole/user.h>
@@ -79,6 +77,9 @@
  */
 typedef struct guac_rdp_print_job {
 
+    /**
+     * The Guacamole client associated with the RDP session.
+     */
     guac_client* client;
 
     /**
diff --git a/src/protocols/rdp/rdp.c b/src/protocols/rdp/rdp.c
index d0df053..6d8381d 100644
--- a/src/protocols/rdp/rdp.c
+++ b/src/protocols/rdp/rdp.c
@@ -17,28 +17,30 @@
  * under the License.
  */
 
-#include "config.h"
-
-#include "audio_input.h"
+#include "bitmap.h"
+#include "channels/audio-input/audio-buffer.h"
+#include "channels/audio-input/audio-input.h"
+#include "channels/cliprdr.h"
+#include "channels/disp.h"
+#include "channels/pipe-svc.h"
+#include "channels/rail.h"
+#include "channels/rdpdr/rdpdr.h"
+#include "channels/rdpsnd/rdpsnd.h"
 #include "client.h"
+#include "color.h"
 #include "common/cursor.h"
 #include "common/display.h"
 #include "common/recording.h"
-#include "dvc.h"
+#include "config.h"
 #include "error.h"
+#include "fs.h"
+#include "gdi.h"
+#include "glyph.h"
 #include "keyboard.h"
+#include "plugins/channels.h"
+#include "pointer.h"
+#include "print-job.h"
 #include "rdp.h"
-#include "rdp_bitmap.h"
-#include "rdp_cliprdr.h"
-#include "rdp_disp.h"
-#include "rdp_fs.h"
-#include "rdp_print_job.h"
-#include "rdp_gdi.h"
-#include "rdp_glyph.h"
-#include "rdp_pointer.h"
-#include "rdp_rail.h"
-#include "rdp_stream.h"
-#include "rdp_svc.h"
 
 #ifdef ENABLE_COMMON_SSH
 #include "common-ssh/sftp.h"
@@ -46,6 +48,7 @@
 #include "common-ssh/user.h"
 #endif
 
+#include <freerdp/addin.h>
 #include <freerdp/cache/bitmap.h>
 #include <freerdp/cache/brush.h>
 #include <freerdp/cache/glyph.h>
@@ -53,349 +56,130 @@
 #include <freerdp/cache/palette.h>
 #include <freerdp/cache/pointer.h>
 #include <freerdp/channels/channels.h>
+#include <freerdp/client/channels.h>
 #include <freerdp/freerdp.h>
+#include <freerdp/gdi/gdi.h>
+#include <freerdp/graphics.h>
+#include <freerdp/primary.h>
+#include <freerdp/settings.h>
+#include <freerdp/update.h>
 #include <guacamole/audio.h>
 #include <guacamole/client.h>
 #include <guacamole/protocol.h>
 #include <guacamole/socket.h>
 #include <guacamole/timestamp.h>
-
-#ifdef HAVE_FREERDP_CLIENT_CLIPRDR_H
-#include <freerdp/client/cliprdr.h>
-#else
-#include "compat/client-cliprdr.h"
-#endif
-
-#ifdef HAVE_FREERDP_CLIENT_DISP_H
-#include <freerdp/client/disp.h>
-#endif
-
-#ifdef HAVE_FREERDP_EVENT_PUBSUB
-#include <freerdp/event.h>
-#endif
-
-#ifdef LEGACY_FREERDP
-#include "compat/rail.h"
-#else
-#include <freerdp/rail.h>
-#endif
-
-#ifdef ENABLE_WINPR
+#include <winpr/error.h>
+#include <winpr/synch.h>
 #include <winpr/wtypes.h>
-#else
-#include "compat/winpr-wtypes.h"
-#endif
 
-#ifdef HAVE_FREERDP_ADDIN_H
-#include <freerdp/addin.h>
-#endif
-
-#ifdef HAVE_FREERDP_CLIENT_CHANNELS_H
-#include <freerdp/client/channels.h>
-#endif
-
-#ifdef HAVE_FREERDP_VERSION_H
-#include <freerdp/version.h>
-#endif
-
-#include <errno.h>
-#include <poll.h>
-#include <pthread.h>
 #include <stdlib.h>
-#include <string.h>
-#include <sys/time.h>
 #include <time.h>
 
-/**
- * Callback invoked by FreeRDP for data received along a channel. This is the
- * most recent version of the callback and uses a 16-bit unsigned integer for
- * the channel ID, as well as different type naming for the datatype of the
- * data itself. This function does nothing more than invoke
- * freerdp_channels_data() with the given arguments. The prototypes of these
- * functions are compatible in 1.2 and later, but not necessarily prior to
- * that, hence the conditional compilation of differing prototypes.
- *
- * Beware that the official purpose of these parameters is an undocumented
- * mystery. The meanings below are derived from looking at how the function is
- * used within FreeRDP.
- *
- * @param rdp_inst
- *     The RDP client instance associated with the channel receiving the data.
- *
- * @param channelId
- *     The integer ID of the channel that received the data.
- *
- * @param data
- *     A buffer containing the received data.
- *
- * @param size
- *     The number of bytes received and contained in the given buffer (the
- *     number of bytes received within the PDU that resulted in this function
- *     being inboked).
- *
- * @param flags
- *     Channel control flags, as defined by the CHANNEL_PDU_HEADER in the RDP
- *     specification.
- *
- * @param total_size
- *     The total length of the chanel data being received, which may span
- *     multiple PDUs (see the "length" field of CHANNEL_PDU_HEADER).
- *
- * @return
- *     Zero if the received channel data was successfully handled, non-zero
- *     otherwise. Note that this return value is discarded in practice.
- */
-#if defined(FREERDP_VERSION_MAJOR) \
-    && (FREERDP_VERSION_MAJOR > 1 || FREERDP_VERSION_MINOR >= 2)
-static int __guac_receive_channel_data(freerdp* rdp_inst, UINT16 channelId,
-        BYTE* data, int size, int flags, int total_size) {
-#else
-static int __guac_receive_channel_data(freerdp* rdp_inst, int channelId,
-        UINT8* data, int size, int flags, int total_size) {
-#endif
-    return freerdp_channels_data(rdp_inst, channelId,
-            data, size, flags, total_size);
-}
-
-#ifdef HAVE_FREERDP_EVENT_PUBSUB
-/**
- * Called whenever a channel connects via the PubSub event system within
- * FreeRDP.
- *
- * @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_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_settings* settings = rdp_client->settings;
-
-    if (settings->resize_method == GUAC_RESIZE_DISPLAY_UPDATE) {
-#ifdef HAVE_RDPSETTINGS_SUPPORTDISPLAYCONTROL
-        /* Store reference to the display update plugin once it's connected */
-        if (strcmp(e->name, DISP_DVC_CHANNEL_NAME) == 0) {
-
-            DispClientContext* disp = (DispClientContext*) e->pInterface;
-
-            /* Init module with current display size */
-            guac_rdp_disp_set_size(rdp_client->disp, rdp_client->settings,
-                    context->instance, guac_rdp_get_width(context->instance),
-                    guac_rdp_get_height(context->instance));
-
-            /* Store connected channel */
-            guac_rdp_disp_connect(rdp_client->disp, disp);
-            guac_client_log(client, GUAC_LOG_DEBUG,
-                    "Display update channel connected.");
-
-        }
-#endif
-    }
-
-}
-#endif
-
 BOOL rdp_freerdp_pre_connect(freerdp* instance) {
 
     rdpContext* context = instance->context;
-    rdpChannels* channels = context->channels;
+    rdpGraphics* graphics = context->graphics;
 
     guac_client* client = ((rdp_freerdp_context*) context)->client;
     guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
     guac_rdp_settings* settings = rdp_client->settings;
 
-    rdpBitmap* bitmap;
-    rdpGlyph* glyph;
-    rdpPointer* pointer;
-    rdpPrimaryUpdate* primary;
-    CLRCONV* clrconv;
+    /* Push desired settings to FreeRDP */
+    guac_rdp_push_settings(client, settings, instance);
 
-    guac_rdp_dvc_list* dvc_list = guac_rdp_dvc_list_alloc();
-
-#ifdef HAVE_FREERDP_REGISTER_ADDIN_PROVIDER
     /* Init FreeRDP add-in provider */
     freerdp_register_addin_provider(freerdp_channels_load_static_addin_entry, 0);
-#endif
 
-#ifdef HAVE_FREERDP_EVENT_PUBSUB
-    /* Subscribe to and handle channel connected events */
-    PubSub_SubscribeChannelConnected(context->pubSub,
-            (pChannelConnectedEventHandler) guac_rdp_channel_connected);
-#endif
-
-#ifdef HAVE_FREERDP_DISPLAY_UPDATE_SUPPORT
     /* Load "disp" plugin for display update */
     if (settings->resize_method == GUAC_RESIZE_DISPLAY_UPDATE)
-        guac_rdp_disp_load_plugin(instance->context, dvc_list);
-#endif
+        guac_rdp_disp_load_plugin(context);
 
     /* Load "AUDIO_INPUT" plugin for audio input*/
     if (settings->enable_audio_input) {
         rdp_client->audio_input = guac_rdp_audio_buffer_alloc();
-        guac_rdp_audio_load_plugin(instance->context, dvc_list);
+        guac_rdp_audio_load_plugin(instance->context);
     }
 
-    /* Load clipboard plugin if not disabled */
-    if (!(settings->disable_copy && settings->disable_paste)
-            && freerdp_channels_load_plugin(channels, instance->settings,
-                "cliprdr", NULL)) {
+    /* Load "cliprdr" service if not disabled */
+    if (!(settings->disable_copy && settings->disable_paste))
+        guac_rdp_clipboard_load_plugin(rdp_client->clipboard, context);
+    else
         guac_client_log(client, GUAC_LOG_WARNING,
-                "Failed to load cliprdr plugin. Clipboard will not work.");
-    }
+                "Copy and paste are both disabled.  Clipboard plugin will not be loaded.");
 
     /* If RDPSND/RDPDR required, load them */
     if (settings->printing_enabled
         || settings->drive_enabled
         || settings->audio_enabled) {
-
-        /* Load RDPDR plugin */
-        if (freerdp_channels_load_plugin(channels, instance->settings,
-                    "guacdr", client))
-            guac_client_log(client, GUAC_LOG_WARNING,
-                    "Failed to load guacdr plugin. Drive redirection and "
-                    "printing will not work. Sound MAY not work.");
-
-        /* Load RDPSND plugin */
-        if (freerdp_channels_load_plugin(channels, instance->settings,
-                    "guacsnd", client))
-            guac_client_log(client, GUAC_LOG_WARNING,
-                    "Failed to load guacsnd alongside guacdr plugin. Sound "
-                    "will not work. Drive redirection and printing MAY not "
-                    "work.");
-
+        guac_rdpdr_load_plugin(context);
+        guac_rdpsnd_load_plugin(context);
     }
 
     /* Load RAIL plugin if RemoteApp in use */
-    if (settings->remote_app != NULL) {
-
-#ifdef LEGACY_FREERDP
-        RDP_PLUGIN_DATA* plugin_data = malloc(sizeof(RDP_PLUGIN_DATA) * 2);
-
-        plugin_data[0].size = sizeof(RDP_PLUGIN_DATA);
-        plugin_data[0].data[0] = settings->remote_app;
-        plugin_data[0].data[1] = settings->remote_app_dir;
-        plugin_data[0].data[2] = settings->remote_app_args;
-        plugin_data[0].data[3] = NULL;
-
-        plugin_data[1].size = 0;
-
-        /* Attempt to load rail */
-        if (freerdp_channels_load_plugin(channels, instance->settings,
-                    "rail", plugin_data))
-            guac_client_log(client, GUAC_LOG_WARNING,
-                    "Failed to load rail plugin. RemoteApp will not work.");
-#else
-        /* Attempt to load rail */
-        if (freerdp_channels_load_plugin(channels, instance->settings,
-                    "rail", instance->settings))
-            guac_client_log(client, GUAC_LOG_WARNING,
-                    "Failed to load rail plugin. RemoteApp will not work.");
-#endif
-
-    }
+    if (settings->remote_app != NULL)
+        guac_rdp_rail_load_plugin(context);
 
     /* Load SVC plugin instances for all static channels */
     if (settings->svc_names != NULL) {
 
         char** current = settings->svc_names;
         do {
-
-            guac_rdp_svc* svc = guac_rdp_alloc_svc(client, *current);
-
-            /* Attempt to load guacsvc plugin for new static channel */
-            if (freerdp_channels_load_plugin(channels, instance->settings,
-                        "guacsvc", svc)) {
-                guac_client_log(client, GUAC_LOG_WARNING,
-                        "Cannot create static channel \"%s\": failed to load guacsvc plugin.",
-                        svc->name);
-                guac_rdp_free_svc(svc);
-            }
-
-            /* Store and log on success */
-            else {
-                guac_rdp_add_svc(client, svc);
-                guac_client_log(client, GUAC_LOG_INFO, "Created static channel \"%s\"...",
-                        svc->name);
-            }
-
+            guac_rdp_pipe_svc_load_plugin(context, *current);
         } while (*(++current) != NULL);
 
     }
 
-    /* Load DRDYNVC plugin if required */
-    if (guac_rdp_load_drdynvc(instance->context, dvc_list))
+    /* Load plugin providing Dynamic Virtual Channel support, if required */
+    if (instance->settings->SupportDynamicChannels &&
+            guac_freerdp_channels_load_plugin(context, "drdynvc",
+                instance->settings)) {
         guac_client_log(client, GUAC_LOG_WARNING,
                 "Failed to load drdynvc plugin. Display update and audio "
                 "input support will be disabled.");
+    }
 
-    /* Dynamic virtual channel list is no longer needed */
-    guac_rdp_dvc_list_free(dvc_list);
-
-    /* Init color conversion structure */
-    clrconv = calloc(1, sizeof(CLRCONV));
-    clrconv->alpha = 1;
-    clrconv->invert = 0;
-    clrconv->rgb555 = 0;
-    clrconv->palette = calloc(1, sizeof(rdpPalette));
-    ((rdp_freerdp_context*) context)->clrconv = clrconv;
-
-    /* Init FreeRDP cache */
-    instance->context->cache = cache_new(instance->settings);
+    /* Init FreeRDP internal GDI implementation */
+    if (!gdi_init(instance, guac_rdp_get_native_pixel_format(FALSE)))
+        return FALSE;
 
     /* Set up bitmap handling */
-    bitmap = calloc(1, sizeof(rdpBitmap));
-    bitmap->size = sizeof(guac_rdp_bitmap);
-    bitmap->New = guac_rdp_bitmap_new;
-    bitmap->Free = guac_rdp_bitmap_free;
-    bitmap->Paint = guac_rdp_bitmap_paint;
-    bitmap->Decompress = guac_rdp_bitmap_decompress;
-    bitmap->SetSurface = guac_rdp_bitmap_setsurface;
-    graphics_register_bitmap(context->graphics, bitmap);
-    free(bitmap);
+    rdpBitmap bitmap = *graphics->Bitmap_Prototype;
+    bitmap.size = sizeof(guac_rdp_bitmap);
+    bitmap.New = guac_rdp_bitmap_new;
+    bitmap.Free = guac_rdp_bitmap_free;
+    bitmap.Paint = guac_rdp_bitmap_paint;
+    bitmap.SetSurface = guac_rdp_bitmap_setsurface;
+    graphics_register_bitmap(graphics, &bitmap);
 
     /* Set up glyph handling */
-    glyph = calloc(1, sizeof(rdpGlyph));
-    glyph->size = sizeof(guac_rdp_glyph);
-    glyph->New = guac_rdp_glyph_new;
-    glyph->Free = guac_rdp_glyph_free;
-    glyph->Draw = guac_rdp_glyph_draw;
-    glyph->BeginDraw = guac_rdp_glyph_begindraw;
-    glyph->EndDraw = guac_rdp_glyph_enddraw;
-    graphics_register_glyph(context->graphics, glyph);
-    free(glyph);
+    rdpGlyph glyph = *graphics->Glyph_Prototype;
+    glyph.size = sizeof(guac_rdp_glyph);
+    glyph.New = guac_rdp_glyph_new;
+    glyph.Free = guac_rdp_glyph_free;
+    glyph.Draw = guac_rdp_glyph_draw;
+    glyph.BeginDraw = guac_rdp_glyph_begindraw;
+    glyph.EndDraw = guac_rdp_glyph_enddraw;
+    graphics_register_glyph(graphics, &glyph);
 
     /* Set up pointer handling */
-    pointer = calloc(1, sizeof(rdpPointer));
-    pointer->size = sizeof(guac_rdp_pointer);
-    pointer->New = guac_rdp_pointer_new;
-    pointer->Free = guac_rdp_pointer_free;
-    pointer->Set = guac_rdp_pointer_set;
-#ifdef HAVE_RDPPOINTER_SETNULL
-    pointer->SetNull = guac_rdp_pointer_set_null;
-#endif
-#ifdef HAVE_RDPPOINTER_SETDEFAULT
-    pointer->SetDefault = guac_rdp_pointer_set_default;
-#endif
-    graphics_register_pointer(context->graphics, pointer);
-    free(pointer);
+    rdpPointer pointer = *graphics->Pointer_Prototype;
+    pointer.size = sizeof(guac_rdp_pointer);
+    pointer.New = guac_rdp_pointer_new;
+    pointer.Free = guac_rdp_pointer_free;
+    pointer.Set = guac_rdp_pointer_set;
+    pointer.SetNull = guac_rdp_pointer_set_null;
+    pointer.SetDefault = guac_rdp_pointer_set_default;
+    graphics_register_pointer(graphics, &pointer);
 
     /* Set up GDI */
     instance->update->DesktopResize = guac_rdp_gdi_desktop_resize;
     instance->update->EndPaint = guac_rdp_gdi_end_paint;
-    instance->update->Palette = guac_rdp_gdi_palette_update;
     instance->update->SetBounds = guac_rdp_gdi_set_bounds;
 
-    primary = instance->update->primary;
+    rdpPrimaryUpdate* primary = instance->update->primary;
     primary->DstBlt = guac_rdp_gdi_dstblt;
-    primary->PatBlt = guac_rdp_gdi_patblt;
     primary->ScrBlt = guac_rdp_gdi_scrblt;
     primary->MemBlt = guac_rdp_gdi_memblt;
-    primary->OpaqueRect = guac_rdp_gdi_opaquerect;
 
     pointer_cache_register_callbacks(instance->update);
     glyph_cache_register_callbacks(instance->update);
@@ -404,39 +188,6 @@
     offscreen_cache_register_callbacks(instance->update);
     palette_cache_register_callbacks(instance->update);
 
-    /* Init channels (pre-connect) */
-    if (freerdp_channels_pre_connect(channels, instance)) {
-        guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR, "Error initializing RDP client channel manager");
-        return FALSE;
-    }
-
-    return TRUE;
-
-}
-
-/**
- * Callback invoked by FreeRDP just after the connection is established with
- * the RDP server. Implementations are required to manually invoke
- * freerdp_channels_post_connect().
- *
- * @param instance
- *     The FreeRDP instance that has just connected.
- *
- * @return
- *     TRUE if successful, FALSE if an error occurs.
- */
-static BOOL rdp_freerdp_post_connect(freerdp* instance) {
-
-    rdpContext* context = instance->context;
-    guac_client* client = ((rdp_freerdp_context*) context)->client;
-    rdpChannels* channels = instance->context->channels;
-
-    /* Init channels (post-connect) */
-    if (freerdp_channels_post_connect(channels, instance)) {
-        guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR, "Error initializing RDP client channel manager");
-        return FALSE;
-    }
-
     return TRUE;
 
 }
@@ -499,8 +250,9 @@
  * @return
  *     TRUE if the certificate passes verification, FALSE otherwise.
  */
-static BOOL rdp_freerdp_verify_certificate(freerdp* instance, char* subject,
-        char* issuer, char* fingerprint) {
+static DWORD rdp_freerdp_verify_certificate(freerdp* instance,
+        const char* common_name, const char* subject, const char* issuer,
+        const char* fingerprint, BOOL host_mismatch) {
 
     rdpContext* context = instance->context;
     guac_client* client = ((rdp_freerdp_context*) context)->client;
@@ -510,45 +262,15 @@
     /* Bypass validation if ignore_certificate given */
     if (rdp_client->settings->ignore_certificate) {
         guac_client_log(client, GUAC_LOG_INFO, "Certificate validation bypassed");
-        return TRUE;
+        return 2; /* Accept only for this session */
     }
 
     guac_client_log(client, GUAC_LOG_INFO, "Certificate validation failed");
-    return FALSE;
+    return 0; /* Reject certificate */
 
 }
 
 /**
- * Callback invoked by FreeRDP after a new rdpContext has been allocated and
- * associated with the current FreeRDP instance. Implementations are required
- * to manually invoke freerdp_channels_new() at this point.
- *
- * @param instance
- *     The FreeRDP instance whose context has just been allocated.
- *
- * @param context
- *     The newly-allocated FreeRDP context.
- */
-static void rdp_freerdp_context_new(freerdp* instance, rdpContext* context) {
-    context->channels = freerdp_channels_new();
-}
-
-/**
- * Callback invoked by FreeRDP when the rdpContext is being freed. This must be
- * provided, but there is no Guacamole-specific data associated with the
- * FreeRDP context, so nothing is done here.
- *
- * @param instance
- *     The FreeRDP instance whose context is being freed.
- *
- * @param context
- *     The FreeRDP context being freed.
- */
-static void rdp_freerdp_context_free(freerdp* instance, rdpContext* context) {
-    /* EMPTY */
-}
-
-/**
  * Waits for messages from the RDP server for the given number of milliseconds.
  *
  * @param client
@@ -566,78 +288,30 @@
 
     guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
     freerdp* rdp_inst = rdp_client->rdp_inst;
-    rdpChannels* channels = rdp_inst->context->channels;
 
-    int result;
-    int index;
+    HANDLE handles[GUAC_RDP_MAX_FILE_DESCRIPTORS];
+    int num_handles = freerdp_get_event_handles(rdp_inst->context, handles,
+            GUAC_RDP_MAX_FILE_DESCRIPTORS);
 
-    /* List of all file descriptors which we may read data from */
-    void* read_fds[GUAC_RDP_MAX_FILE_DESCRIPTORS];
-    int read_count = 0;
+    /* Wait for data and construct a reasonable frame */
+    int result = WaitForMultipleObjects(num_handles, handles, FALSE,
+            timeout_msecs);
 
-    /* List of all file descriptors which data may be written to. These will
-     * ultimately be ignored, but FreeRDP requires that both read and write
-     * file descriptors be retrieved simultaneously. */
-    void* write_fds[GUAC_RDP_MAX_FILE_DESCRIPTORS];
-    int write_count = 0;
+    /* Translate WaitForMultipleObjects() return values */
+    switch (result) {
 
-    struct pollfd fds[GUAC_RDP_MAX_FILE_DESCRIPTORS];
-
-    /* Get RDP file descriptors */
-    if (!freerdp_get_fds(rdp_inst, read_fds, &read_count,
-                write_fds, &write_count)) {
-        guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR,
-                "Unable to read RDP file descriptors.");
-        return -1;
-    }
-
-    /* Get RDP channel file descriptors */
-    if (!freerdp_channels_get_fds(channels, rdp_inst, read_fds, &read_count,
-                write_fds, &write_count)) {
-        guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR,
-                "Unable to read RDP channel file descriptors.");
-        return -1;
-    }
-
-    /* If no file descriptors, error */
-    if (read_count == 0) {
-        guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR,
-                "No file descriptors associated with RDP connection.");
-        return -1;
-    }
-
-    /* Populate poll() array of read file descriptors */
-    for (index = 0; index < read_count; index++) {
-
-        struct pollfd* current = &fds[index];
-
-        /* Init poll() array element with RDP file descriptor */
-        current->fd      = (int)(long) (read_fds[index]);
-        current->events  = POLLIN;
-        current->revents = 0;
-
-    }
-
-    /* Wait until data can be read from RDP file descriptors */
-    result = poll(fds, read_count, timeout_msecs);
-    if (result < 0) {
-
-        /* If error ignorable, pretend timout occurred */
-        if (errno == EAGAIN
-            || errno == EWOULDBLOCK
-            || errno == EINPROGRESS
-            || errno == EINTR)
+        /* Timeout elapsed before wait could complete */
+        case WAIT_TIMEOUT:
             return 0;
 
-        /* Otherwise, return as error */
-        guac_client_abort(client, GUAC_PROTOCOL_STATUS_UPSTREAM_UNAVAILABLE,
-                "Error waiting for file descriptor.");
-        return -1;
+        /* Attempt to wait failed due to an error */
+        case WAIT_FAILED:
+            return -1;
 
     }
 
-    /* Return wait result */
-    return result;
+    /* Wait was successful */
+    return 1;
 
 }
 
@@ -686,29 +360,16 @@
 
     rdp_client->current_surface = rdp_client->display->default_surface;
 
-    rdp_client->requested_clipboard_format = CB_FORMAT_TEXT;
     rdp_client->available_svc = guac_common_list_alloc();
 
-#ifdef HAVE_FREERDP_CHANNELS_GLOBAL_INIT
-    freerdp_channels_global_init();
-#endif
-
     /* Init client */
     freerdp* rdp_inst = freerdp_new();
     rdp_inst->PreConnect = rdp_freerdp_pre_connect;
-    rdp_inst->PostConnect = rdp_freerdp_post_connect;
     rdp_inst->Authenticate = rdp_freerdp_authenticate;
     rdp_inst->VerifyCertificate = rdp_freerdp_verify_certificate;
-    rdp_inst->ReceiveChannelData = __guac_receive_channel_data;
 
     /* Allocate FreeRDP context */
-#ifdef LEGACY_FREERDP
-    rdp_inst->context_size = sizeof(rdp_freerdp_context);
-#else
     rdp_inst->ContextSize = sizeof(rdp_freerdp_context);
-#endif
-    rdp_inst->ContextNew  = (pContextNew) rdp_freerdp_context_new;
-    rdp_inst->ContextFree = (pContextFree) rdp_freerdp_context_free;
 
     freerdp_context_new(rdp_inst);
     ((rdp_freerdp_context*) rdp_inst->context)->client = client;
@@ -720,9 +381,6 @@
     /* Set default pointer */
     guac_common_cursor_set_pointer(rdp_client->display->cursor);
 
-    /* Push desired settings to FreeRDP */
-    guac_rdp_push_settings(client, settings, rdp_inst);
-
     /* Connect to RDP server */
     if (!freerdp_connect(rdp_inst)) {
         guac_client_abort(client, GUAC_PROTOCOL_STATUS_UPSTREAM_NOT_FOUND,
@@ -732,7 +390,6 @@
 
     /* Connection complete */
     rdp_client->rdp_inst = rdp_inst;
-    rdpChannels* channels = rdp_inst->context->channels;
 
     guac_timestamp last_frame_end = guac_timestamp_current();
 
@@ -744,9 +401,7 @@
             && !guac_rdp_disp_reconnect_needed(rdp_client->disp)) {
 
         /* Update remote display size */
-        pthread_mutex_lock(&(rdp_client->rdp_lock));
         guac_rdp_disp_update_size(rdp_client->disp, settings, rdp_inst);
-        pthread_mutex_unlock(&(rdp_client->rdp_lock));
 
         /* Wait for data and construct a reasonable frame */
         int wait_result = rdp_guac_client_wait_for_messages(client,
@@ -762,42 +417,15 @@
                 guac_timestamp frame_end;
                 int frame_remaining;
 
-                pthread_mutex_lock(&(rdp_client->rdp_lock));
-
                 /* Check the libfreerdp fds */
-                if (!freerdp_check_fds(rdp_inst)
-                        || !freerdp_channels_check_fds(channels, rdp_inst)) {
+                if (!freerdp_check_event_handles(rdp_inst->context)) {
 
                     /* Flag connection failure */
                     wait_result = -1;
-                    pthread_mutex_unlock(&(rdp_client->rdp_lock));
                     break;
 
                 }
 
-                /* Check for channel events */
-                wMessage* event = freerdp_channels_pop_event(channels);
-                if (event) {
-
-                    /* Handle channel events (clipboard and RAIL) */
-#ifdef LEGACY_EVENT
-                    if (event->event_class == CliprdrChannel_Class)
-                        guac_rdp_process_cliprdr_event(client, event);
-                    else if (event->event_class == RailChannel_Class)
-                        guac_rdp_process_rail_event(client, event);
-#else
-                    if (GetMessageClass(event->id) == CliprdrChannel_Class)
-                        guac_rdp_process_cliprdr_event(client, event);
-                    else if (GetMessageClass(event->id) == RailChannel_Class)
-                        guac_rdp_process_rail_event(client, event);
-#endif
-
-                    freerdp_event_free(event);
-
-                }
-
-                pthread_mutex_unlock(&(rdp_client->rdp_lock));
-
                 /* Calculate time remaining in frame */
                 frame_end = guac_timestamp_current();
                 frame_remaining = frame_start + GUAC_RDP_FRAME_DURATION
@@ -830,9 +458,7 @@
         }
 
         /* Test whether the RDP server is closing the connection */
-        pthread_mutex_lock(&(rdp_client->rdp_lock));
         int connection_closing = freerdp_shall_disconnect(rdp_inst);
-        pthread_mutex_unlock(&(rdp_client->rdp_lock));
 
         /* Close connection cleanly if server is disconnecting */
         if (connection_closing)
@@ -858,16 +484,13 @@
         guac_rdp_print_job_free(rdp_client->active_job);
     }
 
-    pthread_mutex_lock(&(rdp_client->rdp_lock));
-
     /* Disconnect client and channels */
-    freerdp_channels_close(channels, rdp_inst);
-    freerdp_channels_free(channels);
     freerdp_disconnect(rdp_inst);
 
+    /* Clean up FreeRDP internal GDI implementation */
+    gdi_free(rdp_inst);
+
     /* Clean up RDP client context */
-    freerdp_clrconv_free(((rdp_freerdp_context*) rdp_inst->context)->clrconv);
-    cache_free(rdp_inst->context->cache);
     freerdp_context_free(rdp_inst);
 
     /* Clean up RDP client */
@@ -883,8 +506,6 @@
     /* Free display */
     guac_common_display_free(rdp_client->display);
 
-    pthread_mutex_unlock(&(rdp_client->rdp_lock));
-
     /* Client is now disconnected */
     guac_client_log(client, GUAC_LOG_INFO, "Internal RDP client disconnected");
 
diff --git a/src/protocols/rdp/rdp.h b/src/protocols/rdp/rdp.h
index 1d2eb11..32f72df 100644
--- a/src/protocols/rdp/rdp.h
+++ b/src/protocols/rdp/rdp.h
@@ -20,24 +20,19 @@
 #ifndef GUAC_RDP_H
 #define GUAC_RDP_H
 
-#include "config.h"
-
-#include "audio_input.h"
+#include "channels/audio-input/audio-buffer.h"
+#include "channels/cliprdr.h"
+#include "channels/disp.h"
 #include "common/clipboard.h"
 #include "common/display.h"
 #include "common/list.h"
 #include "common/recording.h"
 #include "common/surface.h"
+#include "config.h"
+#include "fs.h"
 #include "keyboard.h"
-#include "rdp_disp.h"
-#include "rdp_fs.h"
-#include "rdp_print_job.h"
-#include "rdp_settings.h"
-
-#include <freerdp/freerdp.h>
-#include <freerdp/codec/color.h>
-#include <guacamole/audio.h>
-#include <guacamole/client.h>
+#include "print-job.h"
+#include "settings.h"
 
 #ifdef ENABLE_COMMON_SSH
 #include "common-ssh/sftp.h"
@@ -45,6 +40,12 @@
 #include "common-ssh/user.h"
 #endif
 
+#include <freerdp/codec/color.h>
+#include <freerdp/freerdp.h>
+#include <guacamole/audio.h>
+#include <guacamole/client.h>
+#include <winpr/wtypes.h>
+
 #include <pthread.h>
 #include <stdint.h>
 
@@ -95,17 +96,9 @@
     guac_rdp_keyboard* keyboard;
 
     /**
-     * The current clipboard contents.
+     * The current state of the clipboard and the CLIPRDR channel.
      */
-    guac_common_clipboard* clipboard;
-
-    /**
-     * The format of the clipboard which was requested. Data received from
-     * the RDP server should conform to this format. This will be one of
-     * several legal clipboard format values defined within FreeRDP, such as
-     * CB_FORMAT_TEXT.
-     */
-    int requested_clipboard_format;
+    guac_rdp_clipboard* clipboard;
 
     /**
      * Audio output, if any.
@@ -161,13 +154,6 @@
     guac_common_list* available_svc;
 
     /**
-     * Lock which is locked and unlocked for each RDP message, and for each
-     * part of the RDP client instance which may be dynamically freed and
-     * reallocated during reconnection.
-     */
-    pthread_mutex_t rdp_lock;
-
-    /**
      * Common attributes for locks.
      */
     pthread_mutexattr_t attributes;
@@ -192,11 +178,6 @@
     guac_client* client;
 
     /**
-     * Color conversion structure to be used to convert RDP images to PNGs.
-     */
-    CLRCONV* clrconv;
-
-    /**
      * The current color palette, as received from the RDP server.
      */
     UINT32 palette[256];
diff --git a/src/protocols/rdp/rdp_bitmap.c b/src/protocols/rdp/rdp_bitmap.c
deleted file mode 100644
index f77bbae..0000000
--- a/src/protocols/rdp/rdp_bitmap.c
+++ /dev/null
@@ -1,263 +0,0 @@
-/*
- * 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 "client.h"
-#include "common/display.h"
-#include "common/surface.h"
-#include "rdp.h"
-#include "rdp_bitmap.h"
-#include "rdp_settings.h"
-
-#include <cairo/cairo.h>
-#include <freerdp/codec/bitmap.h>
-#include <freerdp/codec/color.h>
-#include <freerdp/freerdp.h>
-#include <guacamole/client.h>
-#include <guacamole/socket.h>
-
-#ifdef ENABLE_WINPR
-#include <winpr/wtypes.h>
-#else
-#include "compat/winpr-wtypes.h"
-#endif
-
-#include <stdio.h>
-#include <stdlib.h>
-
-void guac_rdp_cache_bitmap(rdpContext* context, rdpBitmap* bitmap) {
-
-    guac_client* client = ((rdp_freerdp_context*) context)->client;
-    guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
-
-    /* Allocate buffer */
-    guac_common_display_layer* buffer = guac_common_display_alloc_buffer(
-            rdp_client->display, bitmap->width, bitmap->height);
-
-    /* Cache image data if present */
-    if (bitmap->data != NULL) {
-
-        /* Create surface from image data */
-        cairo_surface_t* image = cairo_image_surface_create_for_data(
-            bitmap->data, CAIRO_FORMAT_RGB24,
-            bitmap->width, bitmap->height, 4*bitmap->width);
-
-        /* Send surface to buffer */
-        guac_common_surface_draw(buffer->surface, 0, 0, image);
-
-        /* Free surface */
-        cairo_surface_destroy(image);
-
-    }
-
-    /* Store buffer reference in bitmap */
-    ((guac_rdp_bitmap*) bitmap)->layer = buffer;
-
-}
-
-void guac_rdp_bitmap_new(rdpContext* context, rdpBitmap* bitmap) {
-
-    /* Convert image data if present */
-    if (bitmap->data != NULL && bitmap->bpp != 32) {
-
-        /* Convert image data to 32-bit RGB */
-        unsigned char* image_buffer = freerdp_image_convert(bitmap->data, NULL,
-                bitmap->width, bitmap->height,
-                guac_rdp_get_depth(context->instance),
-                32, ((rdp_freerdp_context*) context)->clrconv);
-
-        /* Free existing image, if any */
-        if (image_buffer != bitmap->data) {
-#ifdef FREERDP_BITMAP_REQUIRES_ALIGNED_MALLOC
-            _aligned_free(bitmap->data);
-#else
-            free(bitmap->data);
-#endif
-        }
-
-        /* Store converted image in bitmap */
-        bitmap->data = image_buffer;
-
-    }
-
-    /* No corresponding surface yet - caching is deferred. */
-    ((guac_rdp_bitmap*) bitmap)->layer = NULL;
-
-    /* Start at zero usage */
-    ((guac_rdp_bitmap*) bitmap)->used = 0;
-
-}
-
-void guac_rdp_bitmap_paint(rdpContext* context, rdpBitmap* bitmap) {
-
-    guac_client* client = ((rdp_freerdp_context*) context)->client;
-    guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
-
-    guac_common_display_layer* buffer = ((guac_rdp_bitmap*) bitmap)->layer;
-
-    int width = bitmap->right - bitmap->left + 1;
-    int height = bitmap->bottom - bitmap->top + 1;
-
-    /* If not cached, cache if necessary */
-    if (buffer == NULL && ((guac_rdp_bitmap*) bitmap)->used >= 1)
-        guac_rdp_cache_bitmap(context, bitmap);
-
-    /* If cached, retrieve from cache */
-    if (buffer != NULL)
-        guac_common_surface_copy(buffer->surface, 0, 0, width, height,
-                rdp_client->display->default_surface,
-                bitmap->left, bitmap->top);
-
-    /* Otherwise, draw with stored image data */
-    else if (bitmap->data != NULL) {
-
-        /* Create surface from image data */
-        cairo_surface_t* image = cairo_image_surface_create_for_data(
-            bitmap->data, CAIRO_FORMAT_RGB24,
-            width, height, 4*bitmap->width);
-
-        /* Draw image on default surface */
-        guac_common_surface_draw(rdp_client->display->default_surface,
-                bitmap->left, bitmap->top, image);
-
-        /* Free surface */
-        cairo_surface_destroy(image);
-
-    }
-
-    /* Increment usage counter */
-    ((guac_rdp_bitmap*) bitmap)->used++;
-
-}
-
-void guac_rdp_bitmap_free(rdpContext* context, rdpBitmap* bitmap) {
-
-    guac_client* client = ((rdp_freerdp_context*) context)->client;
-    guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
-    guac_common_display_layer* buffer = ((guac_rdp_bitmap*) bitmap)->layer;
-
-    /* If cached, free buffer */
-    if (buffer != NULL)
-        guac_common_display_free_buffer(rdp_client->display, buffer);
-
-}
-
-void guac_rdp_bitmap_setsurface(rdpContext* context, rdpBitmap* bitmap, BOOL primary) {
-
-    guac_client* client = ((rdp_freerdp_context*) context)->client;
-    guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
-
-    if (primary)
-        rdp_client->current_surface = rdp_client->display->default_surface;
-
-    else {
-
-        /* Make sure that the recieved bitmap is not NULL before processing */
-        if (bitmap == NULL) {
-            guac_client_log(client, GUAC_LOG_INFO, "NULL bitmap found in bitmap_setsurface instruction.");
-            return;
-        }
-
-        /* If not available as a surface, make available. */
-        if (((guac_rdp_bitmap*) bitmap)->layer == NULL)
-            guac_rdp_cache_bitmap(context, bitmap);
-
-        rdp_client->current_surface =
-            ((guac_rdp_bitmap*) bitmap)->layer->surface;
-
-    }
-
-}
-
-#ifdef LEGACY_RDPBITMAP
-void guac_rdp_bitmap_decompress(rdpContext* context, rdpBitmap* bitmap, UINT8* data,
-        int width, int height, int bpp, int length, BOOL compressed) {
-#else
-void guac_rdp_bitmap_decompress(rdpContext* context, rdpBitmap* bitmap, UINT8* data,
-        int width, int height, int bpp, int length, BOOL compressed, int codec_id) {
-#endif
-
-    int size = width * height * 4;
-
-#ifdef FREERDP_BITMAP_REQUIRES_ALIGNED_MALLOC
-    /* Free pre-existing data, if any (might be reused) */
-    if (bitmap->data != NULL)
-        _aligned_free(bitmap->data);
-
-    /* Allocate new data */
-    bitmap->data = (UINT8*) _aligned_malloc(size, 16);
-#else
-    /* Free pre-existing data, if any (might be reused) */
-    free(bitmap->data);
-
-    /* Allocate new data */
-    bitmap->data = (UINT8*) malloc(size);
-#endif
-
-    if (compressed) {
-
-#ifdef HAVE_RDPCONTEXT_CODECS 
-        rdpCodecs* codecs = context->codecs;
-
-        /* Decode as interleaved if less than 32 bits per pixel */
-        if (bpp < 32) {
-            freerdp_client_codecs_prepare(codecs, FREERDP_CODEC_INTERLEAVED);
-#ifdef INTERLEAVED_DECOMPRESS_TAKES_PALETTE
-            interleaved_decompress(codecs->interleaved, data, length, bpp,
-                &(bitmap->data), PIXEL_FORMAT_XRGB32, -1, 0, 0, width, height,
-                (BYTE*) ((rdp_freerdp_context*) context)->palette);
-            bitmap->bpp = 32;
-#else
-            interleaved_decompress(codecs->interleaved, data, length, bpp,
-                &(bitmap->data), PIXEL_FORMAT_XRGB32, -1, 0, 0, width, height);
-            bitmap->bpp = bpp;
-#endif
-        }
-
-        /* Otherwise, decode as planar */
-        else {
-            freerdp_client_codecs_prepare(codecs, FREERDP_CODEC_PLANAR);
-#ifdef PLANAR_DECOMPRESS_CAN_FLIP
-            planar_decompress(codecs->planar, data, length,
-                &(bitmap->data), PIXEL_FORMAT_XRGB32, -1, 0, 0, width, height,
-                TRUE);
-            bitmap->bpp = 32;
-#else
-            planar_decompress(codecs->planar, data, length,
-                &(bitmap->data), PIXEL_FORMAT_XRGB32, -1, 0, 0, width, height);
-            bitmap->bpp = bpp;
-#endif
-        }
-#else
-        bitmap_decompress(data, bitmap->data, width, height, length, bpp, bpp);
-        bitmap->bpp = bpp;
-#endif
-
-    }
-    else {
-        freerdp_image_flip(data, bitmap->data, width, height, bpp);
-        bitmap->bpp = bpp;
-    }
-
-    bitmap->compressed = FALSE;
-    bitmap->length = size;
-
-}
-
diff --git a/src/protocols/rdp/rdp_bitmap.h b/src/protocols/rdp/rdp_bitmap.h
deleted file mode 100644
index dda3f76..0000000
--- a/src/protocols/rdp/rdp_bitmap.h
+++ /dev/null
@@ -1,203 +0,0 @@
-/*
- * 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_RDP_BITMAP_H
-#define _GUAC_RDP_RDP_BITMAP_H
-
-#include "config.h"
-#include "common/display.h"
-
-#include <freerdp/freerdp.h>
-#include <guacamole/layer.h>
-
-#ifdef ENABLE_WINPR
-#include <winpr/wtypes.h>
-#else
-#include "compat/winpr-wtypes.h"
-#endif
-
-/**
- * Guacamole-specific rdpBitmap data.
- */
-typedef struct guac_rdp_bitmap {
-
-    /**
-     * FreeRDP bitmap data - MUST GO FIRST.
-     */
-    rdpBitmap bitmap;
-
-    /**
-     * Layer containing cached image data.
-     */
-    guac_common_display_layer* layer;
-
-    /**
-     * The number of times a bitmap has been used.
-     */
-    int used;
-
-} guac_rdp_bitmap;
-
-/**
- * Caches the given bitmap immediately, storing its data in a remote Guacamole
- * buffer. As RDP bitmaps are frequently created, used once, and immediately
- * destroyed, we defer actual remote-side caching of RDP bitmaps until they are
- * used at least once.
- *
- * @param context
- *     The rdpContext associated with the current RDP session.
- *
- * @param bitmap
- *     The bitmap to cache.
- */
-void guac_rdp_cache_bitmap(rdpContext* context, rdpBitmap* bitmap);
-
-/**
- * Initializes the given newly-created rdpBitmap.
- *
- * @param context
- *     The rdpContext associated with the current RDP session.
- *
- * @param bitmap
- *     The bitmap to initialize.
- */
-void guac_rdp_bitmap_new(rdpContext* context, rdpBitmap* bitmap);
-
-/**
- * Paints the given rdpBitmap on the primary display surface. Note that this
- * operation does NOT draw to the "current" surface set by calls to
- * guac_rdp_bitmap_setsurface().
- *
- * @param context
- *     The rdpContext associated with the current RDP session.
- *
- * @param bitmap
- *     The bitmap to paint. This structure will also contain the specifics of
- *     the paint operation to perform, including the destination X/Y
- *     coordinates.
- */
-void guac_rdp_bitmap_paint(rdpContext* context, rdpBitmap* bitmap);
-
-/**
- * Frees any Guacamole-specific data associated with the given rdpBitmap.
- *
- * @param context
- *     The rdpContext associated with the current RDP session.
- *
- * @param bitmap
- *     The bitmap whose Guacamole-specific data is to be freed.
- */
-void guac_rdp_bitmap_free(rdpContext* context, rdpBitmap* bitmap);
-
-/**
- * Sets the given rdpBitmap as the drawing surface for future operations or,
- * if the primary flag is set, resets the current drawing surface to the
- * primary drawing surface of the remote display.
- *
- * @param context
- *     The rdpContext associated with the current RDP session.
- *
- * @param bitmap
- *     The rdpBitmap to set as the current drawing surface. This parameter is
- *     only valid if the primary flag is FALSE.
- *
- * @param primary
- *     TRUE if the bitmap parameter should be ignored, and the current drawing
- *     surface should be reset to the primary drawing surface of the remote
- *     display, FALSE otherwise.
- */
-void guac_rdp_bitmap_setsurface(rdpContext* context, rdpBitmap* bitmap,
-        BOOL primary);
-
-#ifdef LEGACY_RDPBITMAP
-/**
- * Decompresses or copies the given image data, storing the result within the
- * given bitmap, depending on the compressed flag. Note that even if the
- * received data is not compressed, it is the duty of this function to also
- * flip received data, if the row order is backwards.
- *
- * @param context
- *     The rdpContext associated with the current RDP session.
- *
- * @param bitmap
- *     The bitmap in which the decompressed/copied data should be stored.
- *
- * @param data
- *     Possibly-compressed image data.
- *
- * @param width
- *     The width of the image data, in pixels.
- *
- * @param height
- *     The height of the image data, in pixels.
- *
- * @param bpp
- *     The number of bits per pixel in the image data.
- *
- * @param length
- *     The length of the image data, in bytes.
- *
- * @param compressed
- *     TRUE if the image data is compressed, FALSE otherwise.
- */
-void guac_rdp_bitmap_decompress(rdpContext* context, rdpBitmap* bitmap,
-        UINT8* data, int width, int height, int bpp, int length,
-        BOOL compressed);
-#else
-/**
- * Decompresses or copies the given image data, storing the result within the
- * given bitmap, depending on the compressed flag. Note that even if the
- * received data is not compressed, it is the duty of this function to also
- * flip received data, if the row order is backwards.
- *
- * @param context
- *     The rdpContext associated with the current RDP session.
- *
- * @param bitmap
- *     The bitmap in which the decompressed/copied data should be stored.
- *
- * @param data
- *     Possibly-compressed image data.
- *
- * @param width
- *     The width of the image data, in pixels.
- *
- * @param height
- *     The height of the image data, in pixels.
- *
- * @param bpp
- *     The number of bits per pixel in the image data.
- *
- * @param length
- *     The length of the image data, in bytes.
- *
- * @param compressed
- *     TRUE if the image data is compressed, FALSE otherwise.
- *
- * @param codec_id
- *     The ID of the codec used to compress the image data. This parameter is
- *     currently ignored.
- */
-void guac_rdp_bitmap_decompress(rdpContext* context, rdpBitmap* bitmap,
-        UINT8* data, int width, int height, int bpp, int length,
-        BOOL compressed, int codec_id);
-#endif
-
-#endif
diff --git a/src/protocols/rdp/rdp_cliprdr.c b/src/protocols/rdp/rdp_cliprdr.c
deleted file mode 100644
index 903752c..0000000
--- a/src/protocols/rdp/rdp_cliprdr.c
+++ /dev/null
@@ -1,274 +0,0 @@
-/*
- * 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 "client.h"
-#include "common/clipboard.h"
-#include "common/iconv.h"
-#include "rdp.h"
-#include "rdp_cliprdr.h"
-
-#include <freerdp/channels/channels.h>
-#include <freerdp/freerdp.h>
-#include <freerdp/utils/event.h>
-#include <guacamole/client.h>
-
-#ifdef ENABLE_WINPR
-#include <winpr/wtypes.h>
-#else
-#include "compat/winpr-wtypes.h"
-#endif
-
-#ifdef HAVE_FREERDP_CLIENT_CLIPRDR_H
-#include <freerdp/client/cliprdr.h>
-#else
-#include "compat/client-cliprdr.h"
-#endif
-
-#include <stdlib.h>
-#include <string.h>
-
-void guac_rdp_process_cliprdr_event(guac_client* client, wMessage* event) {
-
-#ifdef LEGACY_EVENT
-        switch (event->event_type) {
-#else
-        switch (GetMessageType(event->id)) {
-#endif
-
-            case CliprdrChannel_MonitorReady:
-                guac_rdp_process_cb_monitor_ready(client, event);
-                break;
-
-            case CliprdrChannel_FormatList:
-                guac_rdp_process_cb_format_list(client,
-                        (RDP_CB_FORMAT_LIST_EVENT*) event);
-                break;
-
-            case CliprdrChannel_DataRequest:
-                guac_rdp_process_cb_data_request(client,
-                        (RDP_CB_DATA_REQUEST_EVENT*) event);
-                break;
-
-            case CliprdrChannel_DataResponse:
-                guac_rdp_process_cb_data_response(client,
-                        (RDP_CB_DATA_RESPONSE_EVENT*) event);
-                break;
-
-            default:
-#ifdef LEGACY_EVENT
-                guac_client_log(client, GUAC_LOG_INFO,
-                        "Unknown cliprdr event type: 0x%x",
-                        event->event_type);
-#else
-                guac_client_log(client, GUAC_LOG_INFO,
-                        "Unknown cliprdr event type: 0x%x",
-                        GetMessageType(event->id));
-#endif
-
-        }
-
-}
-
-void guac_rdp_process_cb_monitor_ready(guac_client* client, wMessage* event) {
-
-    rdpChannels* channels = 
-        ((guac_rdp_client*) client->data)->rdp_inst->context->channels;
-
-    RDP_CB_FORMAT_LIST_EVENT* format_list =
-        (RDP_CB_FORMAT_LIST_EVENT*) freerdp_event_new(
-            CliprdrChannel_Class,
-            CliprdrChannel_FormatList,
-            NULL, NULL);
-
-    /* Received notification of clipboard support. */
-
-    /* Respond with supported format list */
-    format_list->formats = (UINT32*) malloc(sizeof(UINT32)*2);
-    format_list->formats[0] = CB_FORMAT_TEXT;
-    format_list->formats[1] = CB_FORMAT_UNICODETEXT;
-    format_list->num_formats = 2;
-
-    freerdp_channels_send_event(channels, (wMessage*) format_list);
-
-}
-
-/**
- * Sends a clipboard data request for the given format.
- *
- * @param client
- *     The guac_client associated with the current RDP session.
- *
- * @param format
- *     The clipboard format to request. This format must be one of the
- *     documented values used by the CLIPRDR channel for clipboard format IDs.
- */
-static void __guac_rdp_cb_request_format(guac_client* client, int format) {
-
-    guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
-    rdpChannels* channels = rdp_client->rdp_inst->context->channels;
-
-    /* Create new data request */
-    RDP_CB_DATA_REQUEST_EVENT* data_request =
-        (RDP_CB_DATA_REQUEST_EVENT*) freerdp_event_new(
-                CliprdrChannel_Class,
-                CliprdrChannel_DataRequest,
-                NULL, NULL);
-
-    /* Set to requested format */
-    rdp_client->requested_clipboard_format = format;
-    data_request->format = format;
-
-    /* Send request */
-    freerdp_channels_send_event(channels, (wMessage*) data_request);
-
-}
-
-void guac_rdp_process_cb_format_list(guac_client* client,
-        RDP_CB_FORMAT_LIST_EVENT* event) {
-
-    int formats = 0;
-
-    /* Received notification of available data */
-
-    int i;
-    for (i=0; i<event->num_formats; i++) {
-
-        /* If plain text available, request it */
-        if (event->formats[i] == CB_FORMAT_TEXT)
-            formats |= GUAC_RDP_CLIPBOARD_FORMAT_CP1252;
-        else if (event->formats[i] == CB_FORMAT_UNICODETEXT)
-            formats |= GUAC_RDP_CLIPBOARD_FORMAT_UTF16;
-
-    }
-
-    /* Prefer Unicode to plain text */
-    if (formats & GUAC_RDP_CLIPBOARD_FORMAT_UTF16) {
-        __guac_rdp_cb_request_format(client, CB_FORMAT_UNICODETEXT);
-        return;
-    }
-
-    /* Use plain text if Unicode unavailable */
-    if (formats & GUAC_RDP_CLIPBOARD_FORMAT_CP1252) {
-        __guac_rdp_cb_request_format(client, CB_FORMAT_TEXT);
-        return;
-    }
-
-    /* Ignore if no supported format available */
-    guac_client_log(client, GUAC_LOG_INFO, "Ignoring unsupported clipboard data");
-
-}
-
-void guac_rdp_process_cb_data_request(guac_client* client,
-        RDP_CB_DATA_REQUEST_EVENT* event) {
-
-    guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
-    rdpChannels* channels = rdp_client->rdp_inst->context->channels;
-
-    guac_iconv_write* writer;
-    const char* input = rdp_client->clipboard->buffer;
-    char* output = malloc(GUAC_RDP_CLIPBOARD_MAX_LENGTH);
-
-    RDP_CB_DATA_RESPONSE_EVENT* data_response;
-
-    /* Determine output encoding */
-    switch (event->format) {
-
-        case CB_FORMAT_TEXT:
-            writer = GUAC_WRITE_CP1252;
-            break;
-
-        case CB_FORMAT_UNICODETEXT:
-            writer = GUAC_WRITE_UTF16;
-            break;
-
-        default:
-            guac_client_log(client, GUAC_LOG_ERROR, 
-                    "Server requested unsupported clipboard data type");
-            free(output);
-            return;
-
-    }
-
-    /* Create new data response */
-    data_response = (RDP_CB_DATA_RESPONSE_EVENT*) freerdp_event_new(
-                CliprdrChannel_Class,
-                CliprdrChannel_DataResponse,
-                NULL, NULL);
-
-    /* Set data and size */
-    data_response->data = (BYTE*) output;
-    guac_iconv(GUAC_READ_UTF8, &input, rdp_client->clipboard->length,
-               writer, &output, GUAC_RDP_CLIPBOARD_MAX_LENGTH);
-    data_response->size = ((BYTE*) output) - data_response->data;
-
-    /* Send response */
-    freerdp_channels_send_event(channels, (wMessage*) data_response);
-
-}
-
-void guac_rdp_process_cb_data_response(guac_client* client,
-        RDP_CB_DATA_RESPONSE_EVENT* event) {
-
-    guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
-    char received_data[GUAC_RDP_CLIPBOARD_MAX_LENGTH];
-
-    /* Ignore received text if outbound clipboard transfer is disabled */
-    if (rdp_client->settings->disable_copy)
-        return;
-
-    guac_iconv_read* reader;
-    const char* input = (char*) event->data;
-    char* output = received_data;
-
-    /* Find correct source encoding */
-    switch (rdp_client->requested_clipboard_format) {
-
-        /* Non-Unicode */
-        case CB_FORMAT_TEXT:
-            reader = GUAC_READ_CP1252;
-            break;
-
-        /* Unicode (UTF-16) */
-        case CB_FORMAT_UNICODETEXT:
-            reader = GUAC_READ_UTF16;
-            break;
-
-        default:
-            guac_client_log(client, GUAC_LOG_ERROR, "Requested clipboard data in "
-                    "unsupported format %i",
-                    rdp_client->requested_clipboard_format);
-            return;
-
-    }
-
-    /* Convert send clipboard data */
-    if (guac_iconv(reader, &input, event->size,
-            GUAC_WRITE_UTF8, &output, sizeof(received_data))) {
-
-        int length = strnlen(received_data, sizeof(received_data));
-        guac_common_clipboard_reset(rdp_client->clipboard, "text/plain");
-        guac_common_clipboard_append(rdp_client->clipboard, received_data, length);
-        guac_common_clipboard_send(rdp_client->clipboard, client);
-
-    }
-
-}
-
diff --git a/src/protocols/rdp/rdp_cliprdr.h b/src/protocols/rdp/rdp_cliprdr.h
deleted file mode 100644
index b66e1b7..0000000
--- a/src/protocols/rdp/rdp_cliprdr.h
+++ /dev/null
@@ -1,120 +0,0 @@
-/*
- * 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_RDP_CLIPRDR_H
-#define __GUAC_RDP_RDP_CLIPRDR_H
-
-#include "config.h"
-
-#include <guacamole/client.h>
-
-#ifdef ENABLE_WINPR
-#include <winpr/stream.h>
-#else
-#include "compat/winpr-stream.h"
-#endif
-
-#ifdef HAVE_FREERDP_CLIENT_CLIPRDR_H
-#include <freerdp/client/cliprdr.h>
-#else
-#include "compat/client-cliprdr.h"
-#endif
-
-/**
- * Clipboard format for text encoded in Windows CP1252.
- */
-#define GUAC_RDP_CLIPBOARD_FORMAT_CP1252 1
-
-/**
- * Clipboard format for text encoded in UTF-16.
- */
-#define GUAC_RDP_CLIPBOARD_FORMAT_UTF16 2
-
-/**
- * Called within the main RDP connection thread whenever a CLIPRDR message is
- * received. This function will dispatch that message to an appropriate
- * function, specific to that message type.
- *
- * @param client
- *     The guac_client associated with the current RDP session.
- *
- * @param event
- *     The received CLIPRDR message.
- */
-void guac_rdp_process_cliprdr_event(guac_client* client, wMessage* event);
-
-/**
- * Handles the given CLIPRDR event, which MUST be a Monitor Ready event. It
- * is the responsibility of this function to respond to the Monitor Ready
- * event with a list of supported clipboard formats.
- *
- * @param client
- *     The guac_client associated with the current RDP session.
- *
- * @param event
- *     The received CLIPRDR message, which must be a Monitor Ready event.
- */
-void guac_rdp_process_cb_monitor_ready(guac_client* client, wMessage* event);
-
-/**
- * Handles the given CLIPRDR event, which MUST be a Format List event. It
- * is the responsibility of this function to respond to the Format List 
- * event with a request for clipboard data in one of the enumerated formats.
- * This event is fired whenever remote clipboard data is available.
- *
- * @param client
- *     The guac_client associated with the current RDP session.
- *
- * @param event
- *     The received CLIPRDR message, which must be a Format List event.
- */
-void guac_rdp_process_cb_format_list(guac_client* client,
-        RDP_CB_FORMAT_LIST_EVENT* event);
-
-/**
- * Handles the given CLIPRDR event, which MUST be a Data Request event. It
- * is the responsibility of this function to respond to the Data Request
- * event with a data response containing the current clipoard contents.
- *
- * @param client
- *     The guac_client associated with the current RDP session.
- *
- * @param event
- *     The received CLIPRDR message, which must be a Data Request event.
- */
-void guac_rdp_process_cb_data_request(guac_client* client,
-        RDP_CB_DATA_REQUEST_EVENT* event);
-
-/**
- * Handles the given CLIPRDR event, which MUST be a Data Response event. It
- * is the responsibility of this function to read and forward the received
- * clipboard data to connected clients.
- *
- * @param client
- *     The guac_client associated with the current RDP session.
- *
- * @param event
- *     The received CLIPRDR message, which must be a Data Response event.
- */
-void guac_rdp_process_cb_data_response(guac_client* client,
-        RDP_CB_DATA_RESPONSE_EVENT* event);
-
-#endif
-
diff --git a/src/protocols/rdp/rdp_color.c b/src/protocols/rdp/rdp_color.c
deleted file mode 100644
index 0b7de7f..0000000
--- a/src/protocols/rdp/rdp_color.c
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * 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 "client.h"
-#include "rdp.h"
-#include "rdp_settings.h"
-
-#include <freerdp/codec/color.h>
-#include <freerdp/freerdp.h>
-
-#ifdef ENABLE_WINPR
-#include <winpr/wtypes.h>
-#else
-#include "compat/winpr-wtypes.h"
-#endif
-
-UINT32 guac_rdp_convert_color(rdpContext* context, UINT32 color) {
-
-#ifdef HAVE_FREERDP_CONVERT_GDI_ORDER_COLOR
-    UINT32* palette = ((rdp_freerdp_context*) context)->palette;
-
-    /* Convert given color to ARGB32 */
-    return freerdp_convert_gdi_order_color(color,
-            guac_rdp_get_depth(context->instance), PIXEL_FORMAT_ARGB32,
-            (BYTE*) palette);
-
-#elif defined(HAVE_FREERDP_COLOR_CONVERT_DRAWING_ORDER_COLOR_TO_GDI_COLOR)
-    CLRCONV* clrconv = ((rdp_freerdp_context*) context)->clrconv;
-
-    /* Convert given color to ARGB32 */
-    return freerdp_color_convert_drawing_order_color_to_gdi_color(color,
-            guac_rdp_get_depth(context->instance), clrconv);
-
-#else
-    CLRCONV* clrconv = ((rdp_freerdp_context*) context)->clrconv;
-
-    /* Convert given color to ARGB32 */
-    return freerdp_color_convert_var(color,
-            guac_rdp_get_depth(context->instance), 32,
-            clrconv);
-#endif
-
-}
-
diff --git a/src/protocols/rdp/rdp_rail.c b/src/protocols/rdp/rdp_rail.c
deleted file mode 100644
index 671be8d..0000000
--- a/src/protocols/rdp/rdp_rail.c
+++ /dev/null
@@ -1,122 +0,0 @@
-/*
- * 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 "client.h"
-#include "rdp.h"
-#include "rdp_rail.h"
-#include "rdp_settings.h"
-
-#include <freerdp/channels/channels.h>
-#include <freerdp/freerdp.h>
-#include <freerdp/utils/event.h>
-#include <guacamole/client.h>
-
-#ifdef ENABLE_WINPR
-#include <winpr/wtypes.h>
-#else
-#include "compat/winpr-wtypes.h"
-#endif
-
-#ifdef LEGACY_FREERDP
-#include "compat/rail.h"
-#else
-#include <freerdp/rail.h>
-#endif
-
-#include <stddef.h>
-
-void guac_rdp_process_rail_event(guac_client* client, wMessage* event) {
-
-#ifdef LEGACY_EVENT
-        switch (event->event_type) {
-#else
-        switch (GetMessageType(event->id)) {
-#endif
-
-            /* Get system parameters */
-            case RailChannel_GetSystemParam:
-                guac_rdp_process_rail_get_sysparam(client, event);
-                break;
-
-            /* Currently ignored events */
-            case RailChannel_ServerSystemParam:
-            case RailChannel_ServerExecuteResult:
-            case RailChannel_ServerMinMaxInfo:
-            case RailChannel_ServerLocalMoveSize:
-            case RailChannel_ServerGetAppIdResponse:
-            case RailChannel_ServerLanguageBarInfo:
-                break;
-
-            default:
-#ifdef LEGACY_EVENT
-                guac_client_log(client, GUAC_LOG_INFO,
-                        "Unknown rail event type: 0x%x",
-                        event->event_type);
-#else
-                guac_client_log(client, GUAC_LOG_INFO,
-                        "Unknown rail event type: 0x%x",
-                        GetMessageType(event->id));
-#endif
-
-        }
-
-}
-
-void guac_rdp_process_rail_get_sysparam(guac_client* client, wMessage* event) {
-
-    wMessage* response;
-    RAIL_SYSPARAM_ORDER* sysparam;
-
-    /* Get channels */
-    guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
-    rdpChannels* channels = rdp_client->rdp_inst->context->channels;
-
-    /* Get sysparam structure */
-#ifdef LEGACY_EVENT
-    sysparam = (RAIL_SYSPARAM_ORDER*) event->user_data;
-#else
-    sysparam = (RAIL_SYSPARAM_ORDER*) event->wParam;
-#endif
-
-    response = freerdp_event_new(RailChannel_Class,
-                                 RailChannel_ClientSystemParam,
-                                 NULL,
-                                 sysparam);
-
-    /* Work area */
-    sysparam->workArea.left   = 0;
-    sysparam->workArea.top    = 0;
-    sysparam->workArea.right  = rdp_client->settings->width;
-    sysparam->workArea.bottom = rdp_client->settings->height;
-
-    /* Taskbar */
-    sysparam->taskbarPos.left   = 0;
-    sysparam->taskbarPos.top    = 0;
-    sysparam->taskbarPos.right  = 0;
-    sysparam->taskbarPos.bottom = 0;
-
-    sysparam->dragFullWindows = FALSE;
-
-    /* Send response */
-    freerdp_channels_send_event(channels, response);
-
-}
-
diff --git a/src/protocols/rdp/rdp_rail.h b/src/protocols/rdp/rdp_rail.h
deleted file mode 100644
index 949eb3e..0000000
--- a/src/protocols/rdp/rdp_rail.h
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * 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_RDP_RAIL_H
-#define __GUAC_RDP_RDP_RAIL_H
-
-#include "config.h"
-
-#include <guacamole/client.h>
-
-#ifdef ENABLE_WINPR
-#include <winpr/stream.h>
-#else
-#include "compat/winpr-stream.h"
-#endif
-
-/**
- * Dispatches a given RAIL event to the appropriate handler.
- *
- * @param client
- *     The guac_client associated with the current RDP session.
- *
- * @param event
- *     The RAIL event to process.
- */
-void guac_rdp_process_rail_event(guac_client* client, wMessage* event);
-
-/**
- * Handles the event sent when updating system parameters. The event given
- * MUST be a SYSPARAM event.
- *
- * @param client
- *     The guac_client associated with the current RDP session.
- *
- * @param event
- *     The system parameter event to process.
- */
-void guac_rdp_process_rail_get_sysparam(guac_client* client, wMessage* event);
-
-#endif
-
diff --git a/src/protocols/rdp/rdp_status.h b/src/protocols/rdp/rdp_status.h
deleted file mode 100644
index 6a2d5b6..0000000
--- a/src/protocols/rdp/rdp_status.h
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- * 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_STATUS_H
-#define __GUAC_RDP_STATUS_H
-
-/**
- * RDP-specific status constants.
- *
- * @file rdp_status.h 
- */
-
-#include "config.h"
-
-/* Include any constants from winpr/file.h, if available */
-
-#ifdef ENABLE_WINPR
-#include <winpr/file.h>
-#endif
-
-/* Constants which MAY be defined within FreeRDP */
-
-#ifndef STATUS_SUCCESS
-#define STATUS_SUCCESS                  0x00000000
-#define STATUS_NO_MORE_FILES            0x80000006
-#define STATUS_DEVICE_OFF_LINE          0x80000010
-#define STATUS_NOT_IMPLEMENTED          0xC0000002
-#define STATUS_INVALID_PARAMETER        0xC000000D
-#define STATUS_NO_SUCH_FILE             0xC000000F
-#define STATUS_END_OF_FILE              0xC0000011
-#define STATUS_ACCESS_DENIED            0xC0000022
-#define STATUS_OBJECT_NAME_COLLISION    0xC0000035
-#define STATUS_DISK_FULL                0xC000007F
-#define STATUS_FILE_INVALID             0xC0000098  
-#define STATUS_FILE_IS_A_DIRECTORY      0xC00000BA
-#define STATUS_NOT_SUPPORTED            0xC00000BB
-#define STATUS_NOT_A_DIRECTORY          0xC0000103
-#define STATUS_TOO_MANY_OPENED_FILES    0xC000011F
-#define STATUS_CANNOT_DELETE            0xC0000121
-#define STATUS_FILE_DELETED             0xC0000123
-#define STATUS_FILE_CLOSED              0xC0000128
-#endif
-
-/* Constants which are NEVER defined within FreeRDP */
-
-#define STATUS_FILE_SYSTEM_LIMITATION   0xC0000427
-#define STATUS_FILE_TOO_LARGE           0xC0000904
-
-#endif
diff --git a/src/protocols/rdp/rdp_stream.c b/src/protocols/rdp/rdp_stream.c
deleted file mode 100644
index 533ff07..0000000
--- a/src/protocols/rdp/rdp_stream.c
+++ /dev/null
@@ -1,594 +0,0 @@
-/*
- * 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 "client.h"
-#include "common/clipboard.h"
-#include "rdp.h"
-#include "rdp_fs.h"
-#include "rdp_svc.h"
-#include "rdp_stream.h"
-
-#include <freerdp/freerdp.h>
-#include <freerdp/channels/channels.h>
-#include <guacamole/client.h>
-#include <guacamole/protocol.h>
-#include <guacamole/socket.h>
-#include <guacamole/stream.h>
-#include <guacamole/string.h>
-
-#ifdef HAVE_FREERDP_CLIENT_CLIPRDR_H
-#include <freerdp/client/cliprdr.h>
-#else
-#include "compat/client-cliprdr.h"
-#endif
-
-#ifdef ENABLE_WINPR
-#include <winpr/stream.h>
-#include <winpr/wtypes.h>
-#else
-#include "compat/winpr-stream.h"
-#include "compat/winpr-wtypes.h"
-#endif
-
-#include <stdlib.h>
-
-/**
- * Writes the given filename to the given upload path, sanitizing the filename
- * and translating the filename to the root directory.
- */
-static void __generate_upload_path(const char* filename, char* path) {
-
-    int i;
-
-    /* Add initial backslash */
-    *(path++) = '\\';
-
-    for (i=1; i<GUAC_RDP_FS_MAX_PATH; i++) {
-
-        /* Get current, stop at end */
-        char c = *(filename++);
-        if (c == '\0')
-            break;
-
-        /* Replace special characters with underscores */
-        if (c == '/' || c == '\\')
-            c = '_';
-
-        *(path++) = c;
-
-    }
-
-    /* Terminate path */
-    *path = '\0';
-
-}
-
-int guac_rdp_upload_file_handler(guac_user* user, guac_stream* stream,
-        char* mimetype, char* filename) {
-
-    guac_client* client = user->client;
-    guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
-
-    int file_id;
-    guac_rdp_stream* rdp_stream;
-    char file_path[GUAC_RDP_FS_MAX_PATH];
-
-    /* Get filesystem, return error if no filesystem */
-    guac_rdp_fs* fs = rdp_client->filesystem;
-    if (fs == NULL) {
-        guac_protocol_send_ack(user->socket, stream, "FAIL (NO FS)",
-                GUAC_PROTOCOL_STATUS_SERVER_ERROR);
-        guac_socket_flush(user->socket);
-        return 0;
-    }
-
-    /* Translate name */
-    __generate_upload_path(filename, file_path);
-
-    /* Open file */
-    file_id = guac_rdp_fs_open(fs, file_path, ACCESS_GENERIC_WRITE, 0,
-            DISP_FILE_OVERWRITE_IF, 0);
-    if (file_id < 0) {
-        guac_protocol_send_ack(user->socket, stream, "FAIL (CANNOT OPEN)",
-                GUAC_PROTOCOL_STATUS_CLIENT_FORBIDDEN);
-        guac_socket_flush(user->socket);
-        return 0;
-    }
-
-    /* Init upload status */
-    rdp_stream = malloc(sizeof(guac_rdp_stream));
-    rdp_stream->type = GUAC_RDP_UPLOAD_STREAM;
-    rdp_stream->upload_status.offset = 0;
-    rdp_stream->upload_status.file_id = file_id;
-    stream->data = rdp_stream;
-    stream->blob_handler = guac_rdp_upload_blob_handler;
-    stream->end_handler = guac_rdp_upload_end_handler;
-
-    guac_protocol_send_ack(user->socket, stream, "OK (STREAM BEGIN)",
-            GUAC_PROTOCOL_STATUS_SUCCESS);
-    guac_socket_flush(user->socket);
-    return 0;
-
-}
-
-int guac_rdp_svc_pipe_handler(guac_user* user, guac_stream* stream,
-        char* mimetype, char* name) {
-
-    guac_rdp_stream* rdp_stream;
-    guac_rdp_svc* svc = guac_rdp_get_svc(user->client, name);
-
-    /* Fail if no such SVC */
-    if (svc == NULL) {
-        guac_user_log(user, GUAC_LOG_ERROR,
-                "Requested non-existent pipe: \"%s\".",
-                name);
-        guac_protocol_send_ack(user->socket, stream, "FAIL (NO SUCH PIPE)",
-                GUAC_PROTOCOL_STATUS_CLIENT_BAD_REQUEST);
-        guac_socket_flush(user->socket);
-        return 0;
-    }
-    else
-        guac_user_log(user, GUAC_LOG_ERROR,
-                "Inbound half of channel \"%s\" connected.",
-                name);
-
-    /* Init stream data */
-    stream->data = rdp_stream = malloc(sizeof(guac_rdp_stream));
-    stream->blob_handler = guac_rdp_svc_blob_handler;
-    rdp_stream->type = GUAC_RDP_INBOUND_SVC_STREAM;
-    rdp_stream->svc = svc;
-
-    return 0;
-
-}
-
-int guac_rdp_clipboard_handler(guac_user* user, guac_stream* stream,
-        char* mimetype) {
-
-    guac_client* client = user->client;
-    guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
-    guac_rdp_stream* rdp_stream;
-
-    /* Init stream data */
-    stream->data = rdp_stream = malloc(sizeof(guac_rdp_stream));
-    stream->blob_handler = guac_rdp_clipboard_blob_handler;
-    stream->end_handler = guac_rdp_clipboard_end_handler;
-    rdp_stream->type = GUAC_RDP_INBOUND_CLIPBOARD_STREAM;
-
-    guac_common_clipboard_reset(rdp_client->clipboard, mimetype);
-    return 0;
-
-}
-
-int guac_rdp_upload_blob_handler(guac_user* user, guac_stream* stream,
-        void* data, int length) {
-
-    int bytes_written;
-    guac_rdp_stream* rdp_stream = (guac_rdp_stream*) stream->data;
-
-    /* Get filesystem, return error if no filesystem 0*/
-    guac_client* client = user->client;
-    guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
-    guac_rdp_fs* fs = rdp_client->filesystem;
-    if (fs == NULL) {
-        guac_protocol_send_ack(user->socket, stream, "FAIL (NO FS)",
-                GUAC_PROTOCOL_STATUS_SERVER_ERROR);
-        guac_socket_flush(user->socket);
-        return 0;
-    }
-
-    /* Write entire block */
-    while (length > 0) {
-
-        /* Attempt write */
-        bytes_written = guac_rdp_fs_write(fs,
-                rdp_stream->upload_status.file_id,
-                rdp_stream->upload_status.offset,
-                data, length);
-
-        /* On error, abort */
-        if (bytes_written < 0) {
-            guac_protocol_send_ack(user->socket, stream,
-                    "FAIL (BAD WRITE)",
-                    GUAC_PROTOCOL_STATUS_CLIENT_FORBIDDEN);
-            guac_socket_flush(user->socket);
-            return 0;
-        }
-
-        /* Update counters */
-        rdp_stream->upload_status.offset += bytes_written;
-        data += bytes_written;
-        length -= bytes_written;
-
-    }
-
-    guac_protocol_send_ack(user->socket, stream, "OK (DATA RECEIVED)",
-            GUAC_PROTOCOL_STATUS_SUCCESS);
-    guac_socket_flush(user->socket);
-    return 0;
-
-}
-
-int guac_rdp_svc_blob_handler(guac_user* user, guac_stream* stream,
-        void* data, int length) {
-
-    guac_rdp_stream* rdp_stream = (guac_rdp_stream*) stream->data;
-
-    /* Write blob data to SVC directly */
-    guac_rdp_svc_write(rdp_stream->svc, data, length);
-
-    guac_protocol_send_ack(user->socket, stream, "OK (DATA RECEIVED)",
-            GUAC_PROTOCOL_STATUS_SUCCESS);
-    guac_socket_flush(user->socket);
-    return 0;
-
-}
-
-int guac_rdp_clipboard_blob_handler(guac_user* user, guac_stream* stream,
-        void* data, int length) {
-
-    guac_client* client = user->client;
-    guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
-    guac_common_clipboard_append(rdp_client->clipboard, (char*) data, length);
-
-    return 0;
-}
-
-int guac_rdp_upload_end_handler(guac_user* user, guac_stream* stream) {
-
-    guac_client* client = user->client;
-    guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
-    guac_rdp_stream* rdp_stream = (guac_rdp_stream*) stream->data;
-
-    /* Get filesystem, return error if no filesystem */
-    guac_rdp_fs* fs = rdp_client->filesystem;
-    if (fs == NULL) {
-        guac_protocol_send_ack(user->socket, stream, "FAIL (NO FS)",
-                GUAC_PROTOCOL_STATUS_SERVER_ERROR);
-        guac_socket_flush(user->socket);
-        return 0;
-    }
-
-    /* Close file */
-    guac_rdp_fs_close(fs, rdp_stream->upload_status.file_id);
-
-    /* Acknowledge stream end */
-    guac_protocol_send_ack(user->socket, stream, "OK (STREAM END)",
-            GUAC_PROTOCOL_STATUS_SUCCESS);
-    guac_socket_flush(user->socket);
-
-    free(rdp_stream);
-    return 0;
-
-}
-
-int guac_rdp_clipboard_end_handler(guac_user* user, guac_stream* stream) {
-
-    guac_client* client = user->client;
-    guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
-
-    /* Terminate clipboard data with NULL */
-    guac_common_clipboard_append(rdp_client->clipboard, "", 1);
-
-    /* Notify RDP server of new data, if connected */
-    freerdp* rdp_inst = rdp_client->rdp_inst;
-    if (rdp_inst != NULL) {
-
-        rdpChannels* channels = rdp_inst->context->channels;
-
-        RDP_CB_FORMAT_LIST_EVENT* format_list =
-            (RDP_CB_FORMAT_LIST_EVENT*) freerdp_event_new(
-                CliprdrChannel_Class,
-                CliprdrChannel_FormatList,
-                NULL, NULL);
-
-        /* Notify server that text data is now available */
-        format_list->formats = (UINT32*) malloc(sizeof(UINT32) * 2);
-        format_list->formats[0] = CB_FORMAT_TEXT;
-        format_list->formats[1] = CB_FORMAT_UNICODETEXT;
-        format_list->num_formats = 2;
-
-        freerdp_channels_send_event(channels, (wMessage*) format_list);
-
-    }
-
-    return 0;
-}
-
-int guac_rdp_download_ack_handler(guac_user* user, guac_stream* stream,
-        char* message, guac_protocol_status status) {
-
-    guac_client* client = user->client;
-    guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
-    guac_rdp_stream* rdp_stream = (guac_rdp_stream*) stream->data;
-
-    /* Get filesystem, return error if no filesystem */
-    guac_rdp_fs* fs = rdp_client->filesystem;
-    if (fs == NULL) {
-        guac_protocol_send_ack(user->socket, stream, "FAIL (NO FS)",
-                GUAC_PROTOCOL_STATUS_SERVER_ERROR);
-        guac_socket_flush(user->socket);
-        return 0;
-    }
-
-    /* If successful, read data */
-    if (status == GUAC_PROTOCOL_STATUS_SUCCESS) {
-
-        /* Attempt read into buffer */
-        char buffer[4096];
-        int bytes_read = guac_rdp_fs_read(fs,
-                rdp_stream->download_status.file_id,
-                rdp_stream->download_status.offset, buffer, sizeof(buffer));
-
-        /* If bytes read, send as blob */
-        if (bytes_read > 0) {
-            rdp_stream->download_status.offset += bytes_read;
-            guac_protocol_send_blob(user->socket, stream,
-                    buffer, bytes_read);
-        }
-
-        /* If EOF, send end */
-        else if (bytes_read == 0) {
-            guac_protocol_send_end(user->socket, stream);
-            guac_user_free_stream(user, stream);
-            free(rdp_stream);
-        }
-
-        /* Otherwise, fail stream */
-        else {
-            guac_user_log(user, GUAC_LOG_ERROR,
-                    "Error reading file for download");
-            guac_protocol_send_end(user->socket, stream);
-            guac_user_free_stream(user, stream);
-            free(rdp_stream);
-        }
-
-        guac_socket_flush(user->socket);
-
-    }
-
-    /* Otherwise, return stream to user */
-    else
-        guac_user_free_stream(user, stream);
-
-    return 0;
-
-}
-
-int guac_rdp_ls_ack_handler(guac_user* user, guac_stream* stream,
-        char* message, guac_protocol_status status) {
-
-    int blob_written = 0;
-    const char* filename;
-
-    guac_rdp_stream* rdp_stream = (guac_rdp_stream*) stream->data;
-
-    /* If unsuccessful, free stream and abort */
-    if (status != GUAC_PROTOCOL_STATUS_SUCCESS) {
-        guac_rdp_fs_close(rdp_stream->ls_status.fs,
-                rdp_stream->ls_status.file_id);
-        guac_user_free_stream(user, stream);
-        free(rdp_stream);
-        return 0;
-    }
-
-    /* While directory entries remain */
-    while ((filename = guac_rdp_fs_read_dir(rdp_stream->ls_status.fs,
-                    rdp_stream->ls_status.file_id)) != NULL
-            && !blob_written) {
-
-        char absolute_path[GUAC_RDP_FS_MAX_PATH];
-
-        /* Skip current and parent directory entries */
-        if (strcmp(filename, ".") == 0 || strcmp(filename, "..") == 0)
-            continue;
-
-        /* Concatenate into absolute path - skip if invalid */
-        if (!guac_rdp_fs_append_filename(absolute_path,
-                    rdp_stream->ls_status.directory_name, filename)) {
-
-            guac_user_log(user, GUAC_LOG_DEBUG,
-                    "Skipping filename \"%s\" - filename is invalid or "
-                    "resulting path is too long", filename);
-
-            continue;
-        }
-
-        /* Attempt to open file to determine type */
-        int file_id = guac_rdp_fs_open(rdp_stream->ls_status.fs, absolute_path,
-                ACCESS_GENERIC_READ, 0, DISP_FILE_OPEN, 0);
-        if (file_id < 0)
-            continue;
-
-        /* Get opened file */
-        guac_rdp_fs_file* file = guac_rdp_fs_get_file(rdp_stream->ls_status.fs,
-                file_id);
-        if (file == NULL) {
-            guac_client_log(rdp_stream->ls_status.fs->client, GUAC_LOG_DEBUG,
-                    "%s: Successful open produced bad file_id: %i",
-                    __func__, file_id);
-            return 0;
-        }
-
-        /* Determine mimetype */
-        const char* mimetype;
-        if (file->attributes & FILE_ATTRIBUTE_DIRECTORY)
-            mimetype = GUAC_USER_STREAM_INDEX_MIMETYPE;
-        else
-            mimetype = "application/octet-stream";
-
-        /* Write entry */
-        blob_written |= guac_common_json_write_property(user, stream,
-                &rdp_stream->ls_status.json_state, absolute_path, mimetype);
-
-        guac_rdp_fs_close(rdp_stream->ls_status.fs, file_id);
-
-    }
-
-    /* Complete JSON and cleanup at end of directory */
-    if (filename == NULL) {
-
-        /* Complete JSON object */
-        guac_common_json_end_object(user, stream,
-                &rdp_stream->ls_status.json_state);
-        guac_common_json_flush(user, stream,
-                &rdp_stream->ls_status.json_state);
-
-        /* Clean up resources */
-        guac_rdp_fs_close(rdp_stream->ls_status.fs,
-                rdp_stream->ls_status.file_id);
-        free(rdp_stream);
-
-        /* Signal of stream */
-        guac_protocol_send_end(user->socket, stream);
-        guac_user_free_stream(user, stream);
-
-    }
-
-    guac_socket_flush(user->socket);
-    return 0;
-
-}
-
-int guac_rdp_download_get_handler(guac_user* user, guac_object* object,
-        char* name) {
-
-    guac_client* client = user->client;
-    guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
-
-    /* Get filesystem, ignore request if no filesystem */
-    guac_rdp_fs* fs = rdp_client->filesystem;
-    if (fs == NULL)
-        return 0;
-
-    /* Attempt to open file for reading */
-    int file_id = guac_rdp_fs_open(fs, name, ACCESS_GENERIC_READ, 0,
-            DISP_FILE_OPEN, 0);
-    if (file_id < 0) {
-        guac_user_log(user, GUAC_LOG_INFO, "Unable to read file \"%s\"",
-                name);
-        return 0;
-    }
-
-    /* Get opened file */
-    guac_rdp_fs_file* file = guac_rdp_fs_get_file(fs, file_id);
-    if (file == NULL) {
-        guac_client_log(fs->client, GUAC_LOG_DEBUG,
-                "%s: Successful open produced bad file_id: %i",
-                __func__, file_id);
-        return 0;
-    }
-
-    /* If directory, send contents of directory */
-    if (file->attributes & FILE_ATTRIBUTE_DIRECTORY) {
-
-        /* Create stream data */
-        guac_rdp_stream* rdp_stream = malloc(sizeof(guac_rdp_stream));
-        rdp_stream->type = GUAC_RDP_LS_STREAM;
-        rdp_stream->ls_status.fs = fs;
-        rdp_stream->ls_status.file_id = file_id;
-        guac_strlcpy(rdp_stream->ls_status.directory_name, name,
-                sizeof(rdp_stream->ls_status.directory_name));
-
-        /* Allocate stream for body */
-        guac_stream* stream = guac_user_alloc_stream(user);
-        stream->ack_handler = guac_rdp_ls_ack_handler;
-        stream->data = rdp_stream;
-
-        /* Init JSON object state */
-        guac_common_json_begin_object(user, stream,
-                &rdp_stream->ls_status.json_state);
-
-        /* Associate new stream with get request */
-        guac_protocol_send_body(user->socket, object, stream,
-                GUAC_USER_STREAM_INDEX_MIMETYPE, name);
-
-    }
-
-    /* Otherwise, send file contents */
-    else {
-
-        /* Create stream data */
-        guac_rdp_stream* rdp_stream = malloc(sizeof(guac_rdp_stream));
-        rdp_stream->type = GUAC_RDP_DOWNLOAD_STREAM;
-        rdp_stream->download_status.file_id = file_id;
-        rdp_stream->download_status.offset = 0;
-
-        /* Allocate stream for body */
-        guac_stream* stream = guac_user_alloc_stream(user);
-        stream->data = rdp_stream;
-        stream->ack_handler = guac_rdp_download_ack_handler;
-
-        /* Associate new stream with get request */
-        guac_protocol_send_body(user->socket, object, stream,
-                "application/octet-stream", name);
-
-    }
-
-    guac_socket_flush(user->socket);
-    return 0;
-}
-
-int guac_rdp_upload_put_handler(guac_user* user, guac_object* object,
-        guac_stream* stream, char* mimetype, char* name) {
-
-    guac_client* client = user->client;
-    guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
-
-    /* Get filesystem, return error if no filesystem */
-    guac_rdp_fs* fs = rdp_client->filesystem;
-    if (fs == NULL) {
-        guac_protocol_send_ack(user->socket, stream, "FAIL (NO FS)",
-                GUAC_PROTOCOL_STATUS_SERVER_ERROR);
-        guac_socket_flush(user->socket);
-        return 0;
-    }
-
-    /* Open file */
-    int file_id = guac_rdp_fs_open(fs, name, ACCESS_GENERIC_WRITE, 0,
-            DISP_FILE_OVERWRITE_IF, 0);
-
-    /* Abort on failure */
-    if (file_id < 0) {
-        guac_protocol_send_ack(user->socket, stream, "FAIL (CANNOT OPEN)",
-                GUAC_PROTOCOL_STATUS_CLIENT_FORBIDDEN);
-        guac_socket_flush(user->socket);
-        return 0;
-    }
-
-    /* Init upload stream data */
-    guac_rdp_stream* rdp_stream = malloc(sizeof(guac_rdp_stream));
-    rdp_stream->type = GUAC_RDP_UPLOAD_STREAM;
-    rdp_stream->upload_status.offset = 0;
-    rdp_stream->upload_status.file_id = file_id;
-
-    /* Allocate stream, init for file upload */
-    stream->data = rdp_stream;
-    stream->blob_handler = guac_rdp_upload_blob_handler;
-    stream->end_handler = guac_rdp_upload_end_handler;
-
-    /* Acknowledge stream creation */
-    guac_protocol_send_ack(user->socket, stream, "OK (STREAM BEGIN)",
-            GUAC_PROTOCOL_STATUS_SUCCESS);
-    guac_socket_flush(user->socket);
-    return 0;
-}
-
diff --git a/src/protocols/rdp/rdp_stream.h b/src/protocols/rdp/rdp_stream.h
deleted file mode 100644
index deec4f9..0000000
--- a/src/protocols/rdp/rdp_stream.h
+++ /dev/null
@@ -1,226 +0,0 @@
-/*
- * 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_STREAM_H
-#define _GUAC_RDP_STREAM_H
-
-#include "config.h"
-#include "common/json.h"
-#include "rdp_svc.h"
-
-#include <guacamole/user.h>
-#include <guacamole/protocol.h>
-#include <guacamole/stream.h>
-
-#include <stdint.h>
-
-/**
- * The transfer status of a file being downloaded.
- */
-typedef struct guac_rdp_download_status {
-
-    /**
-     * The file ID of the file being downloaded.
-     */
-    int file_id;
-
-    /**
-     * The current position within the file.
-     */
-    uint64_t offset;
-
-} guac_rdp_download_status;
-
-/**
- * Structure which represents the current state of an upload.
- */
-typedef struct guac_rdp_upload_status {
-
-    /**
-     * The overall offset within the file that the next write should
-     * occur at.
-     */
-    uint64_t offset;
-
-    /**
-     * The ID of the file being written to.
-     */
-    int file_id;
-
-} guac_rdp_upload_status;
-
-/**
- * The current state of a directory listing operation.
- */
-typedef struct guac_rdp_ls_status {
-
-    /**
-     * The filesystem associated with the directory being listed.
-     */
-    guac_rdp_fs* fs;
-
-    /**
-     * The file ID of the directory being listed.
-     */
-    int file_id;
-
-    /**
-     * The absolute path of the directory being listed.
-     */
-    char directory_name[GUAC_RDP_FS_MAX_PATH];
-
-    /**
-     * The current state of the JSON directory object being written.
-     */
-    guac_common_json_state json_state;
-
-} guac_rdp_ls_status;
-
-/**
- * All available stream types.
- */
-typedef enum guac_rdp_stream_type {
-
-    /**
-     * An in-progress file upload.
-     */
-    GUAC_RDP_UPLOAD_STREAM,
-
-    /**
-     * An in-progress file download.
-     */
-    GUAC_RDP_DOWNLOAD_STREAM,
-
-    /**
-     * An in-progress stream of a directory listing.
-     */
-    GUAC_RDP_LS_STREAM,
-
-    /**
-     * The inbound half of a static virtual channel.
-     */
-    GUAC_RDP_INBOUND_SVC_STREAM,
-
-    /**
-     * An inbound stream of clipboard data.
-     */
-    GUAC_RDP_INBOUND_CLIPBOARD_STREAM
-
-} guac_rdp_stream_type;
-
-/**
- * Variable-typed stream data.
- */
-typedef struct guac_rdp_stream {
-
-    /**
-     * The type of this stream.
-     */
-    guac_rdp_stream_type type;
-
-    /**
-     * The file upload status. Only valid for GUAC_RDP_UPLOAD_STREAM.
-     */
-    guac_rdp_upload_status upload_status;
-
-    /**
-     * The file upload status. Only valid for GUAC_RDP_DOWNLOAD_STREAM.
-     */
-    guac_rdp_download_status download_status;
-
-    /**
-     * The directory list status. Only valid for GUAC_RDP_LS_STREAM.
-     */
-    guac_rdp_ls_status ls_status;
-
-    /**
-     * Associated SVC instance. Only valid for GUAC_RDP_INBOUND_SVC_STREAM.
-     */
-    guac_rdp_svc* svc;
-
-} guac_rdp_stream;
-
-/**
- * Handler for inbound files related to file uploads.
- */
-guac_user_file_handler guac_rdp_upload_file_handler;
-
-/**
- * Handler for inbound pipes related to static virtual channels.
- */
-guac_user_pipe_handler guac_rdp_svc_pipe_handler;
-
-/**
- * Handler for inbound clipboard data.
- */
-guac_user_clipboard_handler guac_rdp_clipboard_handler;
-
-/**
- * Handler for stream data related to file uploads.
- */
-guac_user_blob_handler guac_rdp_upload_blob_handler;
-
-/**
- * Handler for stream data related to static virtual channels.
- */
-guac_user_blob_handler guac_rdp_svc_blob_handler;
-
-/**
- * Handler for stream data related to clipboard.
- */
-guac_user_blob_handler guac_rdp_clipboard_blob_handler;
-
-/**
- * Handler for end-of-stream related to file uploads.
- */
-guac_user_end_handler guac_rdp_upload_end_handler;
-
-/**
- * Handler for end-of-stream related to clipboard.
- */
-guac_user_end_handler guac_rdp_clipboard_end_handler;
-
-/**
- * Handler for acknowledgements of receipt of data related to file downloads.
- */
-guac_user_ack_handler guac_rdp_download_ack_handler;
-
-/**
- * Handler for ack messages received due to receipt of a "body" or "blob"
- * instruction associated with a directory list operation.
- */
-guac_user_ack_handler guac_rdp_ls_ack_handler;
-
-/**
- * Handler for get messages. In context of downloads and the filesystem exposed
- * via the Guacamole protocol, get messages request the body of a file within
- * the filesystem.
- */
-guac_user_get_handler guac_rdp_download_get_handler;
-
-/**
- * Handler for put messages. In context of uploads and the filesystem exposed
- * via the Guacamole protocol, put messages request write access to a file
- * within the filesystem.
- */
-guac_user_put_handler guac_rdp_upload_put_handler;
-
-#endif
-
diff --git a/src/protocols/rdp/rdp_svc.c b/src/protocols/rdp/rdp_svc.c
deleted file mode 100644
index 0a6eb24..0000000
--- a/src/protocols/rdp/rdp_svc.c
+++ /dev/null
@@ -1,176 +0,0 @@
-/*
- * 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 "client.h"
-#include "common/list.h"
-#include "rdp.h"
-#include "rdp_svc.h"
-
-#include <freerdp/utils/svc_plugin.h>
-#include <guacamole/client.h>
-#include <guacamole/string.h>
-
-#ifdef ENABLE_WINPR
-#include <winpr/stream.h>
-#else
-#include "compat/winpr-stream.h"
-#endif
-
-#include <stdlib.h>
-
-guac_rdp_svc* guac_rdp_alloc_svc(guac_client* client, char* name) {
-
-    guac_rdp_svc* svc = malloc(sizeof(guac_rdp_svc));
-
-    /* Init SVC */
-    svc->client = client;
-    svc->plugin = NULL;
-    svc->output_pipe = NULL;
-
-    /* Init name */
-    int name_length = guac_strlcpy(svc->name, name, GUAC_RDP_SVC_MAX_LENGTH);
-
-    /* Warn about name length */
-    if (name_length >= GUAC_RDP_SVC_MAX_LENGTH)
-        guac_client_log(client, GUAC_LOG_INFO,
-                "Static channel name \"%s\" exceeds maximum of %i characters "
-                "and will be truncated", name, GUAC_RDP_SVC_MAX_LENGTH - 1);
-
-    return svc;
-}
-
-void guac_rdp_free_svc(guac_rdp_svc* svc) {
-    free(svc);
-}
-
-void guac_rdp_svc_send_pipe(guac_socket* socket, guac_rdp_svc* svc) {
-
-    /* Send pipe instruction for the SVC's output stream */
-    guac_protocol_send_pipe(socket, svc->output_pipe,
-            "application/octet-stream", svc->name);
-
-}
-
-void guac_rdp_svc_send_pipes(guac_user* user) {
-
-    guac_client* client = user->client;
-    guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
-
-    guac_common_list_lock(rdp_client->available_svc);
-
-    /* Send pipe for each allocated SVC's output stream */
-    guac_common_list_element* current = rdp_client->available_svc->head;
-    while (current != NULL) {
-        guac_rdp_svc_send_pipe(user->socket, (guac_rdp_svc*) current->data);
-        current = current->next;
-    }
-
-    guac_common_list_unlock(rdp_client->available_svc);
-
-}
-
-void guac_rdp_add_svc(guac_client* client, guac_rdp_svc* svc) {
-
-    guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
-
-    /* Add to list of available SVC */
-    guac_common_list_lock(rdp_client->available_svc);
-    guac_common_list_add(rdp_client->available_svc, svc);
-    guac_common_list_unlock(rdp_client->available_svc);
-
-}
-
-guac_rdp_svc* guac_rdp_get_svc(guac_client* client, const char* name) {
-
-    guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
-    guac_common_list_element* current;
-    guac_rdp_svc* found = NULL;
-
-    /* For each available SVC */
-    guac_common_list_lock(rdp_client->available_svc);
-    current = rdp_client->available_svc->head;
-    while (current != NULL) {
-
-        /* If name matches, found */
-        guac_rdp_svc* current_svc = (guac_rdp_svc*) current->data;
-        if (strcmp(current_svc->name, name) == 0) {
-            found = current_svc;
-            break;
-        }
-
-        current = current->next;
-
-    }
-    guac_common_list_unlock(rdp_client->available_svc);
-
-    return found;
-
-}
-
-guac_rdp_svc* guac_rdp_remove_svc(guac_client* client, const char* name) {
-
-    guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
-    guac_common_list_element* current;
-    guac_rdp_svc* found = NULL;
-
-    /* For each available SVC */
-    guac_common_list_lock(rdp_client->available_svc);
-    current = rdp_client->available_svc->head;
-    while (current != NULL) {
-
-        /* If name matches, remove entry */
-        guac_rdp_svc* current_svc = (guac_rdp_svc*) current->data;
-        if (strcmp(current_svc->name, name) == 0) {
-            guac_common_list_remove(rdp_client->available_svc, current);
-            found = current_svc;
-            break;
-        }
-
-        current = current->next;
-
-    }
-    guac_common_list_unlock(rdp_client->available_svc);
-
-    /* Return removed entry, if any */
-    return found;
-
-}
-
-void guac_rdp_svc_write(guac_rdp_svc* svc, void* data, int length) {
-
-    wStream* output_stream;
-
-    /* Do not write of plugin not associated */
-    if (svc->plugin == NULL) {
-        guac_client_log(svc->client, GUAC_LOG_ERROR,
-                "Channel \"%s\" output dropped.",
-                svc->name);
-        return;
-    }
-
-    /* Build packet */
-    output_stream = Stream_New(NULL, length);
-    Stream_Write(output_stream, data, length);
-
-    /* Send packet */
-    svc_plugin_send(svc->plugin, output_stream);
-
-}
-
diff --git a/src/protocols/rdp/rdp_svc.h b/src/protocols/rdp/rdp_svc.h
deleted file mode 100644
index ebb3a13..0000000
--- a/src/protocols/rdp/rdp_svc.h
+++ /dev/null
@@ -1,168 +0,0 @@
-/*
- * 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_RDP_SVC_H
-#define __GUAC_RDP_RDP_SVC_H
-
-#include "config.h"
-
-#include <freerdp/utils/svc_plugin.h>
-#include <guacamole/client.h>
-#include <guacamole/stream.h>
-
-/**
- * The maximum number of bytes to allow within each channel name, including
- * null terminator.
- */
-#define GUAC_RDP_SVC_MAX_LENGTH 8
-
-/**
- * Structure describing a static virtual channel, and the corresponding
- * Guacamole pipes.
- */
-typedef struct guac_rdp_svc {
-
-    /**
-     * Reference to the client owning this static channel.
-     */
-    guac_client* client;
-
-    /**
-     * Reference to associated SVC plugin.
-     */
-    rdpSvcPlugin* plugin;
-
-    /**
-     * The name of the RDP channel in use, and the name to use for each pipe.
-     */
-    char name[GUAC_RDP_SVC_MAX_LENGTH];
-
-    /**
-     * The output pipe, opened when the RDP server receives a connection to
-     * the static channel.
-     */
-    guac_stream* output_pipe;
-
-} guac_rdp_svc;
-
-/**
- * Allocate a new SVC with the given name.
- *
- * @param client
- *     The guac_client associated with the current RDP session.
- *
- * @param name
- *     The name of the virtual channel to allocate.
- *
- * @return
- *     A newly-allocated static virtual channel.
- */
-guac_rdp_svc* guac_rdp_alloc_svc(guac_client* client, char* name);
-
-/**
- * Free the given SVC.
- *
- * @param svc
- *     The static virtual channel to free.
- */
-void guac_rdp_free_svc(guac_rdp_svc* svc);
-
-/**
- * Sends the "pipe" instruction describing the given static virtual channel
- * along the given socket. This pipe instruction will relate the SVC's
- * underlying output stream with the SVC's name and the mimetype
- * "application/octet-stream".
- *
- * @param socket
- *     The socket along which the "pipe" instruction should be sent.
- *
- * @param svc
- *     The static virtual channel that the "pipe" instruction should describe.
- */
-void guac_rdp_svc_send_pipe(guac_socket* socket, guac_rdp_svc* svc);
-
-/**
- * Sends the "pipe" instructions describing all static virtual channels
- * available to the given user along that user's socket. Each pipe instruction
- * will relate the associated SVC's underlying output stream with the SVC's
- * name and the mimetype "application/octet-stream".
- *
- * @param user
- *     The user to send the "pipe" instructions to.
- */
-void guac_rdp_svc_send_pipes(guac_user* user);
-
-/**
- * Add the given SVC to the list of all available SVCs.
- *
- * @param client
- *     The guac_client associated with the current RDP session.
- *
- * @param svc
- *     The static virtual channel to add to the list of all such channels
- *     available.
- */
-void guac_rdp_add_svc(guac_client* client, guac_rdp_svc* svc);
-
-/**
- * Retrieve the SVC with the given name from the list stored in the client.
- *
- * @param client
- *     The guac_client associated with the current RDP session.
- *
- * @param name
- *     The name of the static virtual channel to retrieve.
- *
- * @return
- *     The static virtual channel with the given name, or NULL if no such
- *     virtual channel exists.
- */
-guac_rdp_svc* guac_rdp_get_svc(guac_client* client, const char* name);
-
-/**
- * Remove the SVC with the given name from the list stored in the client.
- *
- * @param client
- *     The guac_client associated with the current RDP session.
- *
- * @param name
- *     The name of the static virtual channel to remove.
- *
- * @return
- *     The static virtual channel that was removed, or NULL if no such virtual
- *     channel exists.
- */
-guac_rdp_svc* guac_rdp_remove_svc(guac_client* client, const char* name);
-
-/**
- * Write the given blob of data to the virtual channel.
- *
- * @param svc
- *     The static virtual channel to write data to.
- *
- * @param data
- *     The data to write.
- *
- * @param length
- *     The number of bytes to write.
- */
-void guac_rdp_svc_write(guac_rdp_svc* svc, void* data, int length);
-
-#endif
-
diff --git a/src/protocols/rdp/rdp_settings.c b/src/protocols/rdp/settings.c
similarity index 82%
rename from src/protocols/rdp/rdp_settings.c
rename to src/protocols/rdp/settings.c
index 11cc861..fe2cf67 100644
--- a/src/protocols/rdp/rdp_settings.c
+++ b/src/protocols/rdp/settings.c
@@ -17,27 +17,23 @@
  * under the License.
  */
 
-#include "config.h"
-
-#include "client.h"
 #include "common/string.h"
-#include "rdp.h"
-#include "rdp_settings.h"
+#include "config.h"
 #include "resolution.h"
+#include "settings.h"
 
 #include <freerdp/constants.h>
 #include <freerdp/settings.h>
+#include <freerdp/freerdp.h>
+#include <guacamole/client.h>
 #include <guacamole/string.h>
 #include <guacamole/user.h>
-
-#ifdef ENABLE_WINPR
+#include <winpr/crt.h>
 #include <winpr/wtypes.h>
-#else
-#include "compat/winpr-wtypes.h"
-#endif
 
 #include <errno.h>
 #include <stddef.h>
+#include <stdlib.h>
 #include <string.h>
 
 /* Client plugin arguments */
@@ -107,17 +103,13 @@
     "enable-audio-input",
     "read-only",
 
-#ifdef HAVE_FREERDP_GATEWAY_SUPPORT
     "gateway-hostname",
     "gateway-port",
     "gateway-domain",
     "gateway-username",
     "gateway-password",
-#endif
 
-#ifdef HAVE_FREERDP_LOAD_BALANCER_SUPPORT
     "load-balance-info",
-#endif
 
     "disable-copy",
     "disable-paste",
@@ -243,7 +235,8 @@
 
     /**
      * The type of security to use for the connection. Valid values are "rdp",
-     * "tls", "nla", or "any". By default, "rdp" security is used.
+     * "tls", "nla", "nla-ext", or "any". By default, the security mode is
+     * negotiated ("any").
      */
     IDX_SECURITY,
 
@@ -318,7 +311,7 @@
 
     /**
      * "true" if desktop composition (Aero) should be enabled during the
-     * session, "false" or blank otherwise.  As desktop composition provides
+     * session, "false" or blank otherwise. As desktop composition provides
      * alpha blending and other special effects, this increases the amount of
      * bandwidth used.
      */
@@ -363,7 +356,7 @@
 
     /**
      * The timezone to pass through to the RDP connection, in IANA format, which
-     * will be translated into Windows formats.  See the following page for
+     * will be translated into Windows formats. See the following page for
      * information and list of valid values:
      * https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
      */
@@ -383,7 +376,7 @@
     IDX_SFTP_HOSTNAME,
 
     /**
-     * The public SSH host key of the SFTP server.  Optional.
+     * The public SSH host key of the SFTP server. Optional.
      */
     IDX_SFTP_HOST_KEY,
 
@@ -432,7 +425,7 @@
 
     /**
      * The interval at which SSH keepalive messages are sent to the server for
-     * SFTP connections.  The default is 0 (disabling keepalives), and a value
+     * SFTP connections. The default is 0 (disabling keepalives), and a value
      * of 1 is automatically increased to 2 by libssh2 to avoid busy loop corner
      * cases.
      */
@@ -470,7 +463,7 @@
     /**
      * 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
+     * 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.
@@ -501,7 +494,6 @@
      */
     IDX_READ_ONLY,
 
-#ifdef HAVE_FREERDP_GATEWAY_SUPPORT
     /**
      * The hostname of the remote desktop gateway that should be used as an
      * intermediary for the remote desktop connection. If omitted, a gateway
@@ -538,15 +530,12 @@
      * gateway, if a gateway is being used.
      */
     IDX_GATEWAY_PASSWORD,
-#endif
 
-#ifdef HAVE_FREERDP_LOAD_BALANCER_SUPPORT
     /**
      * The load balancing information/cookie which should be provided to
      * the connection broker, if a connection broker is being used.
      */
     IDX_LOAD_BALANCE_INFO,
-#endif
 
     /**
      * Whether outbound clipboard access should be blocked. If set to "true",
@@ -604,6 +593,12 @@
         settings->security_mode = GUAC_SECURITY_NLA;
     }
 
+    /* Extended NLA security */
+    else if (strcmp(argv[IDX_SECURITY], "nla-ext") == 0) {
+        guac_user_log(user, GUAC_LOG_INFO, "Security mode: Extended NLA");
+        settings->security_mode = GUAC_SECURITY_EXTENDED_NLA;
+    }
+
     /* TLS security */
     else if (strcmp(argv[IDX_SECURITY], "tls") == 0) {
         guac_user_log(user, GUAC_LOG_INFO, "Security mode: TLS");
@@ -616,16 +611,16 @@
         settings->security_mode = GUAC_SECURITY_RDP;
     }
 
-    /* ANY security (allow server to choose) */
+    /* Negotiate security (allow server to choose) */
     else if (strcmp(argv[IDX_SECURITY], "any") == 0) {
-        guac_user_log(user, GUAC_LOG_INFO, "Security mode: ANY");
+        guac_user_log(user, GUAC_LOG_INFO, "Security mode: Negotiate (ANY)");
         settings->security_mode = GUAC_SECURITY_ANY;
     }
 
     /* If nothing given, default to RDP */
     else {
-        guac_user_log(user, GUAC_LOG_INFO, "No security mode specified. Defaulting to RDP.");
-        settings->security_mode = GUAC_SECURITY_RDP;
+        guac_user_log(user, GUAC_LOG_INFO, "No security mode specified. Defaulting to security mode negotiation with server.");
+        settings->security_mode = GUAC_SECURITY_ANY;
     }
 
     /* Set hostname */
@@ -813,7 +808,6 @@
                 "Preconnection BLOB: \"%s\"", settings->preconnection_blob);
     }
 
-#ifndef HAVE_RDPSETTINGS_SENDPRECONNECTIONPDU
     /* Warn if support for the preconnection BLOB / ID is absent */
     if (settings->preconnection_blob != NULL
             || settings->preconnection_id != -1) {
@@ -822,7 +816,6 @@
                 "preconnection PDU. The specified preconnection BLOB and/or "
                 "ID will be ignored.");
     }
-#endif
 
     /* Audio enable/disable */
     settings->audio_enabled =
@@ -990,7 +983,6 @@
         guac_user_parse_args_boolean(user, GUAC_RDP_CLIENT_ARGS, argv,
                 IDX_ENABLE_AUDIO_INPUT, 0);
 
-#ifdef HAVE_FREERDP_GATEWAY_SUPPORT
     /* Set gateway hostname */
     settings->gateway_hostname =
         guac_user_parse_args_string(user, GUAC_RDP_CLIENT_ARGS, argv,
@@ -1015,14 +1007,11 @@
     settings->gateway_password =
         guac_user_parse_args_string(user, GUAC_RDP_CLIENT_ARGS, argv,
                 IDX_GATEWAY_PASSWORD, NULL);
-#endif
 
-#ifdef HAVE_FREERDP_LOAD_BALANCER_SUPPORT
     /* Set load balance info */
     settings->load_balance_info =
         guac_user_parse_args_string(user, GUAC_RDP_CLIENT_ARGS, argv,
                 IDX_LOAD_BALANCE_INFO, NULL);
-#endif
 
     /* Parse clipboard copy disable flag */
     settings->disable_copy =
@@ -1087,18 +1076,14 @@
     free(settings->sftp_username);
 #endif
 
-#ifdef HAVE_FREERDP_GATEWAY_SUPPORT
     /* Free RD gateway information */
     free(settings->gateway_hostname);
     free(settings->gateway_domain);
     free(settings->gateway_username);
     free(settings->gateway_password);
-#endif
 
-#ifdef HAVE_FREERDP_LOAD_BALANCER_SUPPORT
     /* Free load balancer information string */
     free(settings->load_balance_info);
-#endif
 
     /* Free settings structure */
     free(settings);
@@ -1106,27 +1091,15 @@
 }
 
 int guac_rdp_get_width(freerdp* rdp) {
-#ifdef LEGACY_RDPSETTINGS
-    return rdp->settings->width;
-#else
     return rdp->settings->DesktopWidth;
-#endif
 }
 
 int guac_rdp_get_height(freerdp* rdp) {
-#ifdef LEGACY_RDPSETTINGS
-    return rdp->settings->height;
-#else
     return rdp->settings->DesktopHeight;
-#endif
 }
 
 int guac_rdp_get_depth(freerdp* rdp) {
-#ifdef LEGACY_RDPSETTINGS
-    return rdp->settings->color_depth;
-#else
     return rdp->settings->ColorDepth;
-#endif
 }
 
 /**
@@ -1199,60 +1172,25 @@
 void guac_rdp_push_settings(guac_client* client,
         guac_rdp_settings* guac_settings, freerdp* rdp) {
 
-    BOOL bitmap_cache = !guac_settings->disable_bitmap_caching;
     rdpSettings* rdp_settings = rdp->settings;
 
     /* Authentication */
-#ifdef LEGACY_RDPSETTINGS
-    rdp_settings->domain = guac_rdp_strdup(guac_settings->domain);
-    rdp_settings->username = guac_rdp_strdup(guac_settings->username);
-    rdp_settings->password = guac_rdp_strdup(guac_settings->password);
-#else
     rdp_settings->Domain = guac_rdp_strdup(guac_settings->domain);
     rdp_settings->Username = guac_rdp_strdup(guac_settings->username);
     rdp_settings->Password = guac_rdp_strdup(guac_settings->password);
-#endif
 
     /* Connection */
-#ifdef LEGACY_RDPSETTINGS
-    rdp_settings->hostname = guac_rdp_strdup(guac_settings->hostname);
-    rdp_settings->port = guac_settings->port;
-#else
     rdp_settings->ServerHostname = guac_rdp_strdup(guac_settings->hostname);
     rdp_settings->ServerPort = guac_settings->port;
-#endif
 
     /* Session */
-#ifdef LEGACY_RDPSETTINGS
-    rdp_settings->color_depth = guac_settings->color_depth;
-    rdp_settings->width = guac_settings->width;
-    rdp_settings->height = guac_settings->height;
-    rdp_settings->shell = guac_rdp_strdup(guac_settings->initial_program);
-    rdp_settings->kbd_layout = guac_settings->server_layout->freerdp_keyboard_layout;
-#else
     rdp_settings->ColorDepth = guac_settings->color_depth;
     rdp_settings->DesktopWidth = guac_settings->width;
     rdp_settings->DesktopHeight = guac_settings->height;
     rdp_settings->AlternateShell = guac_rdp_strdup(guac_settings->initial_program);
     rdp_settings->KeyboardLayout = guac_settings->server_layout->freerdp_keyboard_layout;
-#endif
 
     /* Performance flags */
-#ifdef LEGACY_RDPSETTINGS
-
-    /* Explicitly set flag value */
-    rdp_settings->performance_flags = guac_rdp_get_performance_flags(guac_settings);
-
-    /* Set individual flags - some FreeRDP versions overwrite the above */
-    rdp_settings->smooth_fonts = guac_settings->font_smoothing_enabled;
-    rdp_settings->disable_wallpaper = !guac_settings->wallpaper_enabled;
-    rdp_settings->disable_full_window_drag = !guac_settings->full_window_drag_enabled;
-    rdp_settings->disable_menu_animations = !guac_settings->menu_animations_enabled;
-    rdp_settings->disable_theming = !guac_settings->theming_enabled;
-    rdp_settings->desktop_composition = guac_settings->desktop_composition_enabled;
-
-#else
-
     /* Explicitly set flag value */
     rdp_settings->PerformanceFlags = guac_rdp_get_performance_flags(guac_settings);
 
@@ -1264,49 +1202,25 @@
     rdp_settings->DisableThemes = !guac_settings->theming_enabled;
     rdp_settings->AllowDesktopComposition = guac_settings->desktop_composition_enabled;
 
-#endif
-
     /* Client name */
     if (guac_settings->client_name != NULL) {
-#ifdef LEGACY_RDPSETTINGS
-        guac_strlcpy(rdp_settings->client_hostname, guac_settings->client_name,
-                RDP_CLIENT_HOSTNAME_SIZE);
-#else
         guac_strlcpy(rdp_settings->ClientHostname, guac_settings->client_name,
                 RDP_CLIENT_HOSTNAME_SIZE);
-#endif
     }
 
     /* Console */
-#ifdef LEGACY_RDPSETTINGS
-    rdp_settings->console_session = guac_settings->console;
-    rdp_settings->console_audio = guac_settings->console_audio;
-#else
     rdp_settings->ConsoleSession = guac_settings->console;
     rdp_settings->RemoteConsoleAudio = guac_settings->console_audio;
-#endif
 
     /* Audio */
-#ifdef LEGACY_RDPSETTINGS
-#ifdef HAVE_RDPSETTINGS_AUDIOPLAYBACK
-    rdp_settings->audio_playback = guac_settings->audio_enabled;
-#endif
-#else
-#ifdef HAVE_RDPSETTINGS_AUDIOPLAYBACK
     rdp_settings->AudioPlayback = guac_settings->audio_enabled;
-#endif
-#endif
 
     /* Audio capture */
-#ifdef LEGACY_RDPSETTINGS
-#ifdef HAVE_RDPSETTINGS_AUDIOCAPTURE
-    rdp_settings->audio_capture = guac_settings->enable_audio_input;
-#endif
-#else
-#ifdef HAVE_RDPSETTINGS_AUDIOCAPTURE
     rdp_settings->AudioCapture = guac_settings->enable_audio_input;
-#endif
-#endif
+
+    /* Display Update channel */
+    rdp_settings->SupportDisplayControl =
+        (guac_settings->resize_method == GUAC_RESIZE_DISPLAY_UPDATE);
 
     /* Timezone redirection */
     if (guac_settings->timezone) {
@@ -1318,115 +1232,75 @@
     }
 
     /* Device redirection */
-#ifdef LEGACY_RDPSETTINGS
-#ifdef HAVE_RDPSETTINGS_DEVICEREDIRECTION
-    rdp_settings->device_redirection =  guac_settings->audio_enabled
-                                     || guac_settings->drive_enabled
-                                     || guac_settings->printing_enabled;
-#endif
-#else
-#ifdef HAVE_RDPSETTINGS_DEVICEREDIRECTION
     rdp_settings->DeviceRedirection =  guac_settings->audio_enabled
                                     || guac_settings->drive_enabled
                                     || guac_settings->printing_enabled;
-#endif
-#endif
 
     /* Security */
     switch (guac_settings->security_mode) {
 
-        /* Standard RDP encryption */
+        /* Legacy RDP encryption */
         case GUAC_SECURITY_RDP:
-#ifdef LEGACY_RDPSETTINGS
-            rdp_settings->rdp_security = TRUE;
-            rdp_settings->tls_security = FALSE;
-            rdp_settings->nla_security = FALSE;
-            rdp_settings->encryption_level = ENCRYPTION_LEVEL_CLIENT_COMPATIBLE;
-            rdp_settings->encryption_method =
-                  ENCRYPTION_METHOD_40BIT
-                | ENCRYPTION_METHOD_128BIT
-                | ENCRYPTION_METHOD_FIPS;
-#else
             rdp_settings->RdpSecurity = TRUE;
             rdp_settings->TlsSecurity = FALSE;
             rdp_settings->NlaSecurity = FALSE;
+            rdp_settings->ExtSecurity = FALSE;
+            rdp_settings->UseRdpSecurityLayer = TRUE;
             rdp_settings->EncryptionLevel = ENCRYPTION_LEVEL_CLIENT_COMPATIBLE;
             rdp_settings->EncryptionMethods =
                   ENCRYPTION_METHOD_40BIT
                 | ENCRYPTION_METHOD_128BIT 
                 | ENCRYPTION_METHOD_FIPS;
-#endif
             break;
 
         /* TLS encryption */
         case GUAC_SECURITY_TLS:
-#ifdef LEGACY_RDPSETTINGS
-            rdp_settings->rdp_security = FALSE;
-            rdp_settings->tls_security = TRUE;
-            rdp_settings->nla_security = FALSE;
-#else
             rdp_settings->RdpSecurity = FALSE;
             rdp_settings->TlsSecurity = TRUE;
             rdp_settings->NlaSecurity = FALSE;
-#endif
+            rdp_settings->ExtSecurity = FALSE;
             break;
 
         /* Network level authentication */
         case GUAC_SECURITY_NLA:
-#ifdef LEGACY_RDPSETTINGS
-            rdp_settings->rdp_security = FALSE;
-            rdp_settings->tls_security = FALSE;
-            rdp_settings->nla_security = TRUE;
-#else
             rdp_settings->RdpSecurity = FALSE;
             rdp_settings->TlsSecurity = FALSE;
             rdp_settings->NlaSecurity = TRUE;
-#endif
+            rdp_settings->ExtSecurity = FALSE;
+            break;
+
+        /* Extended network level authentication */
+        case GUAC_SECURITY_EXTENDED_NLA:
+            rdp_settings->RdpSecurity = FALSE;
+            rdp_settings->TlsSecurity = FALSE;
+            rdp_settings->NlaSecurity = FALSE;
+            rdp_settings->ExtSecurity = TRUE;
             break;
 
         /* All security types */
         case GUAC_SECURITY_ANY:
-#ifdef LEGACY_RDPSETTINGS
-            rdp_settings->rdp_security = TRUE;
-            rdp_settings->tls_security = TRUE;
-            rdp_settings->nla_security = TRUE;
-#else
             rdp_settings->RdpSecurity = TRUE;
             rdp_settings->TlsSecurity = TRUE;
-            rdp_settings->NlaSecurity = TRUE;
-#endif
+            rdp_settings->NlaSecurity = guac_settings->username && guac_settings->password;
+            rdp_settings->ExtSecurity = FALSE;
             break;
 
     }
 
     /* Authentication */
-#ifdef LEGACY_RDPSETTINGS
-    rdp_settings->authentication = !guac_settings->disable_authentication;
-    rdp_settings->ignore_certificate = guac_settings->ignore_certificate;
-    rdp_settings->encryption = TRUE;
-#else
     rdp_settings->Authentication = !guac_settings->disable_authentication;
     rdp_settings->IgnoreCertificate = guac_settings->ignore_certificate;
-    rdp_settings->DisableEncryption = FALSE;
-#endif
 
     /* RemoteApp */
     if (guac_settings->remote_app != NULL) {
-#ifdef LEGACY_RDPSETTINGS
-        rdp_settings->workarea = TRUE;
-        rdp_settings->remote_app = TRUE;
-        rdp_settings->rail_langbar_supported = TRUE;
-#else
         rdp_settings->Workarea = TRUE;
         rdp_settings->RemoteApplicationMode = TRUE;
         rdp_settings->RemoteAppLanguageBarSupported = TRUE;
         rdp_settings->RemoteApplicationProgram = guac_settings->remote_app;
         rdp_settings->ShellWorkingDirectory = guac_rdp_strdup(guac_settings->remote_app_dir);
         rdp_settings->RemoteApplicationCmdLine = guac_settings->remote_app_args;
-#endif
     }
 
-#ifdef HAVE_RDPSETTINGS_SENDPRECONNECTIONPDU
     /* Preconnection ID */
     if (guac_settings->preconnection_id != -1) {
         rdp_settings->NegotiateSecurityLayer = FALSE;
@@ -1440,9 +1314,7 @@
         rdp_settings->SendPreconnectionPdu = TRUE;
         rdp_settings->PreconnectionBlob = guac_settings->preconnection_blob;
     }
-#endif
 
-#ifdef HAVE_FREERDP_GATEWAY_SUPPORT
     /* Enable use of RD gateway if a gateway hostname is provided */
     if (guac_settings->gateway_hostname != NULL) {
 
@@ -1460,80 +1332,29 @@
         rdp_settings->GatewayPassword = guac_rdp_strdup(guac_settings->gateway_password);
 
     }
-#endif
 
-#ifdef HAVE_FREERDP_LOAD_BALANCER_SUPPORT
     /* Store load balance info (and calculate length) if provided */
     if (guac_settings->load_balance_info != NULL) {
         rdp_settings->LoadBalanceInfo = (BYTE*) guac_rdp_strdup(guac_settings->load_balance_info);
         rdp_settings->LoadBalanceInfoLength = strlen(guac_settings->load_balance_info);
     }
-#endif
 
-    /* Order support */
-#ifdef LEGACY_RDPSETTINGS
-    rdp_settings->bitmap_cache = bitmap_cache;
-    rdp_settings->offscreen_bitmap_cache = !guac_settings->disable_offscreen_caching;
-    rdp_settings->glyph_cache = !guac_settings->disable_glyph_caching;
-    rdp_settings->os_major_type = OSMAJORTYPE_UNSPECIFIED;
-    rdp_settings->os_minor_type = OSMINORTYPE_UNSPECIFIED;
-    rdp_settings->desktop_resize = TRUE;
-    rdp_settings->order_support[NEG_DSTBLT_INDEX] = TRUE;
-    rdp_settings->order_support[NEG_PATBLT_INDEX] = FALSE; /* PATBLT not yet supported */
-    rdp_settings->order_support[NEG_SCRBLT_INDEX] = TRUE;
-    rdp_settings->order_support[NEG_OPAQUE_RECT_INDEX] = TRUE;
-    rdp_settings->order_support[NEG_DRAWNINEGRID_INDEX] = FALSE;
-    rdp_settings->order_support[NEG_MULTIDSTBLT_INDEX] = FALSE;
-    rdp_settings->order_support[NEG_MULTIPATBLT_INDEX] = FALSE;
-    rdp_settings->order_support[NEG_MULTISCRBLT_INDEX] = FALSE;
-    rdp_settings->order_support[NEG_MULTIOPAQUERECT_INDEX] = FALSE;
-    rdp_settings->order_support[NEG_MULTI_DRAWNINEGRID_INDEX] = FALSE;
-    rdp_settings->order_support[NEG_LINETO_INDEX] = FALSE;
-    rdp_settings->order_support[NEG_POLYLINE_INDEX] = FALSE;
-    rdp_settings->order_support[NEG_MEMBLT_INDEX] = bitmap_cache;
-    rdp_settings->order_support[NEG_MEM3BLT_INDEX] = FALSE;
-    rdp_settings->order_support[NEG_MEMBLT_V2_INDEX] = bitmap_cache;
-    rdp_settings->order_support[NEG_MEM3BLT_V2_INDEX] = FALSE;
-    rdp_settings->order_support[NEG_SAVEBITMAP_INDEX] = FALSE;
-    rdp_settings->order_support[NEG_GLYPH_INDEX_INDEX] = TRUE;
-    rdp_settings->order_support[NEG_FAST_INDEX_INDEX] = TRUE;
-    rdp_settings->order_support[NEG_FAST_GLYPH_INDEX] = TRUE;
-    rdp_settings->order_support[NEG_POLYGON_SC_INDEX] = FALSE;
-    rdp_settings->order_support[NEG_POLYGON_CB_INDEX] = FALSE;
-    rdp_settings->order_support[NEG_ELLIPSE_SC_INDEX] = FALSE;
-    rdp_settings->order_support[NEG_ELLIPSE_CB_INDEX] = FALSE;
-#else
-    rdp_settings->BitmapCacheEnabled = bitmap_cache;
+    rdp_settings->BitmapCacheEnabled = !guac_settings->disable_bitmap_caching;
     rdp_settings->OffscreenSupportLevel = !guac_settings->disable_offscreen_caching;
     rdp_settings->GlyphSupportLevel = !guac_settings->disable_glyph_caching ? GLYPH_SUPPORT_FULL : GLYPH_SUPPORT_NONE;
     rdp_settings->OsMajorType = OSMAJORTYPE_UNSPECIFIED;
     rdp_settings->OsMinorType = OSMINORTYPE_UNSPECIFIED;
     rdp_settings->DesktopResize = TRUE;
+
+    /* Claim support only for specific updates, independent of FreeRDP defaults */
+    ZeroMemory(rdp_settings->OrderSupport, GUAC_RDP_ORDER_SUPPORT_LENGTH);
     rdp_settings->OrderSupport[NEG_DSTBLT_INDEX] = TRUE;
-    rdp_settings->OrderSupport[NEG_PATBLT_INDEX] = FALSE; /* PATBLT not yet supported */
     rdp_settings->OrderSupport[NEG_SCRBLT_INDEX] = TRUE;
-    rdp_settings->OrderSupport[NEG_OPAQUE_RECT_INDEX] = TRUE;
-    rdp_settings->OrderSupport[NEG_DRAWNINEGRID_INDEX] = FALSE;
-    rdp_settings->OrderSupport[NEG_MULTIDSTBLT_INDEX] = FALSE;
-    rdp_settings->OrderSupport[NEG_MULTIPATBLT_INDEX] = FALSE;
-    rdp_settings->OrderSupport[NEG_MULTISCRBLT_INDEX] = FALSE;
-    rdp_settings->OrderSupport[NEG_MULTIOPAQUERECT_INDEX] = FALSE;
-    rdp_settings->OrderSupport[NEG_MULTI_DRAWNINEGRID_INDEX] = FALSE;
-    rdp_settings->OrderSupport[NEG_LINETO_INDEX] = FALSE;
-    rdp_settings->OrderSupport[NEG_POLYLINE_INDEX] = FALSE;
-    rdp_settings->OrderSupport[NEG_MEMBLT_INDEX] = bitmap_cache;
-    rdp_settings->OrderSupport[NEG_MEM3BLT_INDEX] = FALSE;
-    rdp_settings->OrderSupport[NEG_MEMBLT_V2_INDEX] = bitmap_cache;
-    rdp_settings->OrderSupport[NEG_MEM3BLT_V2_INDEX] = FALSE;
-    rdp_settings->OrderSupport[NEG_SAVEBITMAP_INDEX] = FALSE;
-    rdp_settings->OrderSupport[NEG_GLYPH_INDEX_INDEX] = TRUE;
-    rdp_settings->OrderSupport[NEG_FAST_INDEX_INDEX] = TRUE;
-    rdp_settings->OrderSupport[NEG_FAST_GLYPH_INDEX] = TRUE;
-    rdp_settings->OrderSupport[NEG_POLYGON_SC_INDEX] = FALSE;
-    rdp_settings->OrderSupport[NEG_POLYGON_CB_INDEX] = FALSE;
-    rdp_settings->OrderSupport[NEG_ELLIPSE_SC_INDEX] = FALSE;
-    rdp_settings->OrderSupport[NEG_ELLIPSE_CB_INDEX] = FALSE;
-#endif
+    rdp_settings->OrderSupport[NEG_MEMBLT_INDEX] = !guac_settings->disable_bitmap_caching;
+    rdp_settings->OrderSupport[NEG_MEMBLT_V2_INDEX] = !guac_settings->disable_bitmap_caching;
+    rdp_settings->OrderSupport[NEG_GLYPH_INDEX_INDEX] = !guac_settings->disable_glyph_caching;
+    rdp_settings->OrderSupport[NEG_FAST_INDEX_INDEX] = !guac_settings->disable_glyph_caching;
+    rdp_settings->OrderSupport[NEG_FAST_GLYPH_INDEX] = !guac_settings->disable_glyph_caching;
 
 }
 
diff --git a/src/protocols/rdp/rdp_settings.h b/src/protocols/rdp/settings.h
similarity index 93%
rename from src/protocols/rdp/rdp_settings.h
rename to src/protocols/rdp/settings.h
index 9edbede..e4c579e 100644
--- a/src/protocols/rdp/rdp_settings.h
+++ b/src/protocols/rdp/settings.h
@@ -17,15 +17,15 @@
  * under the License.
  */
 
-
-#ifndef __GUAC_RDP_SETTINGS_H
-#define __GUAC_RDP_SETTINGS_H
+#ifndef GUAC_RDP_SETTINGS_H
+#define GUAC_RDP_SETTINGS_H
 
 #include "config.h"
-
-#include "rdp_keymap.h"
+#include "keymap.h"
 
 #include <freerdp/freerdp.h>
+#include <guacamole/client.h>
+#include <guacamole/user.h>
 
 /**
  * The maximum number of bytes in the client hostname claimed during
@@ -59,12 +59,22 @@
 #define GUAC_RDP_DEFAULT_RECORDING_NAME "recording"
 
 /**
+ * The number of entries contained within the OrderSupport BYTE array
+ * referenced by the rdpSettings structure. This value is defined by the RDP
+ * negotiation process (there are 32 bytes available within the order
+ * negotiation field sent during the connection handshake) and is hard-coded
+ * within FreeRDP. There is no public constant for this value defined within
+ * the FreeRDP headers.
+ */
+#define GUAC_RDP_ORDER_SUPPORT_LENGTH 32
+
+/**
  * All supported combinations of security types.
  */
 typedef enum guac_rdp_security {
 
     /**
-     * Standard RDP encryption.
+     * Legacy RDP encryption.
      */
     GUAC_SECURITY_RDP,
 
@@ -79,7 +89,12 @@
     GUAC_SECURITY_NLA,
 
     /**
-     * Any method supported by the server.
+     * Extended network level authentication.
+     */
+    GUAC_SECURITY_EXTENDED_NLA,
+
+    /**
+     * Negotiate a security method supported by both server and client.
      */
     GUAC_SECURITY_ANY
 
@@ -324,19 +339,19 @@
     int menu_animations_enabled;
 
     /**
-     * Whether bitmap caching should be disabled.  By default it is
+     * Whether bitmap caching should be disabled. By default it is
      * enabled - this allows users to explicitly disable it.
      */
     int disable_bitmap_caching;
 
     /**
-     * Whether offscreen caching should be disabled.  By default it is
+     * Whether offscreen caching should be disabled. By default it is
      * enabled - this allows users to explicitly disable it.
      */
     int disable_offscreen_caching;
 
     /**
-     * Whether glyph caching should be disabled.  By default it is enabled
+     * Whether glyph caching should be disabled. By default it is enabled
      * - this allows users to explicitly disable it.
      */
     int disable_glyph_caching;
@@ -420,7 +435,7 @@
 
     /**
      * The interval at which SSH keepalive messages are sent to the server for
-     * SFTP connections.  The default is 0 (disabling keepalives), and a value
+     * SFTP connections. The default is 0 (disabling keepalives), and a value
      * of 1 is automatically increased to 2 by libssh2 to avoid busy loop corner
      * cases.
      */
@@ -480,7 +495,6 @@
      */
     int enable_audio_input;
 
-#ifdef HAVE_FREERDP_GATEWAY_SUPPORT
     /**
      * The hostname of the remote desktop gateway that should be used as an
      * intermediary for the remote desktop connection. If no gateway should
@@ -515,15 +529,12 @@
      * gateway, if a gateway is being used.
      */
     char* gateway_password;
-#endif
 
-#ifdef HAVE_FREERDP_LOAD_BALANCER_SUPPORT
     /**
      * The load balancing information/cookie which should be provided to
      * the connection broker, if a connection broker is being used.
      */
     char* load_balance_info;
-#endif
 
 } guac_rdp_settings;
 
diff --git a/src/protocols/rdp/sftp.c b/src/protocols/rdp/sftp.c
index ecfe35f..0fc2580 100644
--- a/src/protocols/rdp/sftp.c
+++ b/src/protocols/rdp/sftp.c
@@ -17,14 +17,11 @@
  * under the License.
  */
 
-#include "config.h"
-
 #include "common-ssh/sftp.h"
 #include "rdp.h"
 #include "sftp.h"
 
 #include <guacamole/client.h>
-#include <guacamole/stream.h>
 #include <guacamole/user.h>
 
 int guac_rdp_sftp_file_handler(guac_user* user, guac_stream* stream,
diff --git a/src/protocols/rdp/sftp.h b/src/protocols/rdp/sftp.h
index d768324..3f7f9e2 100644
--- a/src/protocols/rdp/sftp.h
+++ b/src/protocols/rdp/sftp.h
@@ -20,8 +20,6 @@
 #ifndef GUAC_RDP_SFTP_H
 #define GUAC_RDP_SFTP_H
 
-#include "config.h"
-
 #include <guacamole/stream.h>
 #include <guacamole/user.h>
 
diff --git a/src/protocols/rdp/tests/Makefile.am b/src/protocols/rdp/tests/Makefile.am
index 3f57bbf..3313669 100644
--- a/src/protocols/rdp/tests/Makefile.am
+++ b/src/protocols/rdp/tests/Makefile.am
@@ -34,6 +34,7 @@
 TESTS = $(check_PROGRAMS)
 
 test_rdp_SOURCES =      \
+    fs/basename.c       \
     fs/normalize_path.c
 
 test_rdp_CFLAGS =                \
diff --git a/src/protocols/rdp/tests/fs/basename.c b/src/protocols/rdp/tests/fs/basename.c
new file mode 100644
index 0000000..9ac6097
--- /dev/null
+++ b/src/protocols/rdp/tests/fs/basename.c
@@ -0,0 +1,59 @@
+/*
+ * 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 "fs.h"
+
+#include <CUnit/CUnit.h>
+#include <stdlib.h>
+
+/**
+ * Test which verifies basenames are correctly extracted from Windows-style
+ * paths.
+ */
+void test_fs__basename_windows() {
+    CU_ASSERT_STRING_EQUAL(guac_rdp_fs_basename("\\foo\\bar\\baz"), "baz")
+    CU_ASSERT_STRING_EQUAL(guac_rdp_fs_basename("\\foo\\bar\\..\\baz\\"), "")
+    CU_ASSERT_STRING_EQUAL(guac_rdp_fs_basename("bar\\..\\..\\baz\\a\\..\\b"), "b")
+    CU_ASSERT_STRING_EQUAL(guac_rdp_fs_basename(".\\bar\\potato"), "potato")
+    CU_ASSERT_STRING_EQUAL(guac_rdp_fs_basename("..\\..\\..\\..\\..\\..\\baz"), "baz")
+}
+
+/**
+ * Test which verifies basenames are correctly extracted from UNIX-style paths.
+ */
+void test_fs__basename_unix() {
+    CU_ASSERT_STRING_EQUAL(guac_rdp_fs_basename("/foo/bar/baz"), "baz")
+    CU_ASSERT_STRING_EQUAL(guac_rdp_fs_basename("/foo/bar/../baz/"), "")
+    CU_ASSERT_STRING_EQUAL(guac_rdp_fs_basename("bar/../../baz/a/../b"), "b")
+    CU_ASSERT_STRING_EQUAL(guac_rdp_fs_basename("./bar/potato"), "potato")
+    CU_ASSERT_STRING_EQUAL(guac_rdp_fs_basename("../../../../../../baz"), "baz")
+}
+
+/**
+ * Test which verifies basenames are correctly extracted from paths consisting
+ * of mixed Windows and UNIX path separators.
+ */
+void test_fs__basename_mixed() {
+    CU_ASSERT_STRING_EQUAL(guac_rdp_fs_basename("\\foo/bar\\baz"), "baz")
+    CU_ASSERT_STRING_EQUAL(guac_rdp_fs_basename("/foo\\bar/..\\baz/"), "")
+    CU_ASSERT_STRING_EQUAL(guac_rdp_fs_basename("bar\\../../baz\\a\\..\\b"), "b")
+    CU_ASSERT_STRING_EQUAL(guac_rdp_fs_basename(".\\bar/potato"), "potato")
+    CU_ASSERT_STRING_EQUAL(guac_rdp_fs_basename("../..\\..\\..\\../..\\baz"), "baz")
+}
+
diff --git a/src/protocols/rdp/tests/fs/normalize_path.c b/src/protocols/rdp/tests/fs/normalize_path.c
index ccf23e0..22a2d80 100644
--- a/src/protocols/rdp/tests/fs/normalize_path.c
+++ b/src/protocols/rdp/tests/fs/normalize_path.c
@@ -17,7 +17,7 @@
  * under the License.
  */
 
-#include "rdp_fs.h"
+#include "fs.h"
 
 #include <CUnit/CUnit.h>
 #include <stdlib.h>
diff --git a/src/protocols/rdp/unicode.c b/src/protocols/rdp/unicode.c
index 628615a..f055bea 100644
--- a/src/protocols/rdp/unicode.c
+++ b/src/protocols/rdp/unicode.c
@@ -17,12 +17,10 @@
  * under the License.
  */
 
-#include "config.h"
+#include <guacamole/unicode.h>
 
 #include <stdint.h>
 
-#include <guacamole/unicode.h>
-
 void guac_rdp_utf16_to_utf8(const unsigned char* utf16, int length,
         char* utf8, int size) {
 
diff --git a/src/protocols/rdp/upload.c b/src/protocols/rdp/upload.c
new file mode 100644
index 0000000..5317edb
--- /dev/null
+++ b/src/protocols/rdp/upload.c
@@ -0,0 +1,236 @@
+/*
+ * 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 "fs.h"
+#include "rdp.h"
+#include "upload.h"
+
+#include <guacamole/client.h>
+#include <guacamole/object.h>
+#include <guacamole/protocol.h>
+#include <guacamole/socket.h>
+#include <guacamole/stream.h>
+#include <guacamole/user.h>
+#include <winpr/nt.h>
+
+#include <stdlib.h>
+
+/**
+ * Writes the given filename to the given upload path, sanitizing the filename
+ * and translating the filename to the root directory.
+ *
+ * @param filename
+ *     The filename to sanitize and move to the root directory.
+ *
+ * @param path
+ *     A pointer to a buffer which should receive the sanitized path. The
+ *     buffer must have at least GUAC_RDP_FS_MAX_PATH bytes available.
+ */
+static void __generate_upload_path(const char* filename, char* path) {
+
+    int i;
+
+    /* Add initial backslash */
+    *(path++) = '\\';
+
+    for (i=1; i<GUAC_RDP_FS_MAX_PATH; i++) {
+
+        /* Get current, stop at end */
+        char c = *(filename++);
+        if (c == '\0')
+            break;
+
+        /* Replace special characters with underscores */
+        if (c == '/' || c == '\\')
+            c = '_';
+
+        *(path++) = c;
+
+    }
+
+    /* Terminate path */
+    *path = '\0';
+
+}
+
+int guac_rdp_upload_file_handler(guac_user* user, guac_stream* stream,
+        char* mimetype, char* filename) {
+
+    guac_client* client = user->client;
+    guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
+
+    int file_id;
+    char file_path[GUAC_RDP_FS_MAX_PATH];
+
+    /* Get filesystem, return error if no filesystem */
+    guac_rdp_fs* fs = rdp_client->filesystem;
+    if (fs == NULL) {
+        guac_protocol_send_ack(user->socket, stream, "FAIL (NO FS)",
+                GUAC_PROTOCOL_STATUS_SERVER_ERROR);
+        guac_socket_flush(user->socket);
+        return 0;
+    }
+
+    /* Translate name */
+    __generate_upload_path(filename, file_path);
+
+    /* Open file */
+    file_id = guac_rdp_fs_open(fs, file_path, GENERIC_WRITE, 0,
+            FILE_OVERWRITE_IF, 0);
+    if (file_id < 0) {
+        guac_protocol_send_ack(user->socket, stream, "FAIL (CANNOT OPEN)",
+                GUAC_PROTOCOL_STATUS_CLIENT_FORBIDDEN);
+        guac_socket_flush(user->socket);
+        return 0;
+    }
+
+    /* Init upload status */
+    guac_rdp_upload_status* upload_status = malloc(sizeof(guac_rdp_upload_status));
+    upload_status->offset = 0;
+    upload_status->file_id = file_id;
+    stream->data = upload_status;
+    stream->blob_handler = guac_rdp_upload_blob_handler;
+    stream->end_handler = guac_rdp_upload_end_handler;
+
+    guac_protocol_send_ack(user->socket, stream, "OK (STREAM BEGIN)",
+            GUAC_PROTOCOL_STATUS_SUCCESS);
+    guac_socket_flush(user->socket);
+    return 0;
+
+}
+
+int guac_rdp_upload_blob_handler(guac_user* user, guac_stream* stream,
+        void* data, int length) {
+
+    int bytes_written;
+    guac_rdp_upload_status* upload_status = (guac_rdp_upload_status*) stream->data;
+
+    /* Get filesystem, return error if no filesystem */
+    guac_client* client = user->client;
+    guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
+    guac_rdp_fs* fs = rdp_client->filesystem;
+    if (fs == NULL) {
+        guac_protocol_send_ack(user->socket, stream, "FAIL (NO FS)",
+                GUAC_PROTOCOL_STATUS_SERVER_ERROR);
+        guac_socket_flush(user->socket);
+        return 0;
+    }
+
+    /* Write entire block */
+    while (length > 0) {
+
+        /* Attempt write */
+        bytes_written = guac_rdp_fs_write(fs, upload_status->file_id,
+                upload_status->offset, data, length);
+
+        /* On error, abort */
+        if (bytes_written < 0) {
+            guac_protocol_send_ack(user->socket, stream,
+                    "FAIL (BAD WRITE)",
+                    GUAC_PROTOCOL_STATUS_CLIENT_FORBIDDEN);
+            guac_socket_flush(user->socket);
+            return 0;
+        }
+
+        /* Update counters */
+        upload_status->offset += bytes_written;
+        data += bytes_written;
+        length -= bytes_written;
+
+    }
+
+    guac_protocol_send_ack(user->socket, stream, "OK (DATA RECEIVED)",
+            GUAC_PROTOCOL_STATUS_SUCCESS);
+    guac_socket_flush(user->socket);
+    return 0;
+
+}
+
+int guac_rdp_upload_end_handler(guac_user* user, guac_stream* stream) {
+
+    guac_client* client = user->client;
+    guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
+    guac_rdp_upload_status* upload_status = (guac_rdp_upload_status*) stream->data;
+
+    /* Get filesystem, return error if no filesystem */
+    guac_rdp_fs* fs = rdp_client->filesystem;
+    if (fs == NULL) {
+        guac_protocol_send_ack(user->socket, stream, "FAIL (NO FS)",
+                GUAC_PROTOCOL_STATUS_SERVER_ERROR);
+        guac_socket_flush(user->socket);
+        return 0;
+    }
+
+    /* Close file */
+    guac_rdp_fs_close(fs, upload_status->file_id);
+
+    /* Acknowledge stream end */
+    guac_protocol_send_ack(user->socket, stream, "OK (STREAM END)",
+            GUAC_PROTOCOL_STATUS_SUCCESS);
+    guac_socket_flush(user->socket);
+
+    free(upload_status);
+    return 0;
+
+}
+
+int guac_rdp_upload_put_handler(guac_user* user, guac_object* object,
+        guac_stream* stream, char* mimetype, char* name) {
+
+    guac_client* client = user->client;
+    guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
+
+    /* Get filesystem, return error if no filesystem */
+    guac_rdp_fs* fs = rdp_client->filesystem;
+    if (fs == NULL) {
+        guac_protocol_send_ack(user->socket, stream, "FAIL (NO FS)",
+                GUAC_PROTOCOL_STATUS_SERVER_ERROR);
+        guac_socket_flush(user->socket);
+        return 0;
+    }
+
+    /* Open file */
+    int file_id = guac_rdp_fs_open(fs, name, GENERIC_WRITE, 0,
+            FILE_OVERWRITE_IF, 0);
+
+    /* Abort on failure */
+    if (file_id < 0) {
+        guac_protocol_send_ack(user->socket, stream, "FAIL (CANNOT OPEN)",
+                GUAC_PROTOCOL_STATUS_CLIENT_FORBIDDEN);
+        guac_socket_flush(user->socket);
+        return 0;
+    }
+
+    /* Init upload stream data */
+    guac_rdp_upload_status* upload_status = malloc(sizeof(guac_rdp_upload_status));
+    upload_status->offset = 0;
+    upload_status->file_id = file_id;
+
+    /* Allocate stream, init for file upload */
+    stream->data = upload_status;
+    stream->blob_handler = guac_rdp_upload_blob_handler;
+    stream->end_handler = guac_rdp_upload_end_handler;
+
+    /* Acknowledge stream creation */
+    guac_protocol_send_ack(user->socket, stream, "OK (STREAM BEGIN)",
+            GUAC_PROTOCOL_STATUS_SUCCESS);
+    guac_socket_flush(user->socket);
+    return 0;
+}
+
diff --git a/src/protocols/rdp/upload.h b/src/protocols/rdp/upload.h
new file mode 100644
index 0000000..254538f
--- /dev/null
+++ b/src/protocols/rdp/upload.h
@@ -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.
+ */
+
+#ifndef GUAC_RDP_UPLOAD_H
+#define GUAC_RDP_UPLOAD_H
+
+#include "common/json.h"
+
+#include <guacamole/protocol.h>
+#include <guacamole/stream.h>
+#include <guacamole/user.h>
+
+#include <stdint.h>
+
+/**
+ * Structure which represents the current state of an upload.
+ */
+typedef struct guac_rdp_upload_status {
+
+    /**
+     * The overall offset within the file that the next write should
+     * occur at.
+     */
+    int offset;
+
+    /**
+     * The ID of the file being written to.
+     */
+    int file_id;
+
+} guac_rdp_upload_status;
+
+/**
+ * Handler for inbound files related to file uploads.
+ */
+guac_user_file_handler guac_rdp_upload_file_handler;
+
+/**
+ * Handler for stream data related to file uploads.
+ */
+guac_user_blob_handler guac_rdp_upload_blob_handler;
+
+/**
+ * Handler for end-of-stream related to file uploads.
+ */
+guac_user_end_handler guac_rdp_upload_end_handler;
+
+/**
+ * Handler for put messages. In context of uploads and the filesystem exposed
+ * via the Guacamole protocol, put messages request write access to a file
+ * within the filesystem.
+ */
+guac_user_put_handler guac_rdp_upload_put_handler;
+
+#endif
+
diff --git a/src/protocols/rdp/user.c b/src/protocols/rdp/user.c
index 025848a..c2b487f 100644
--- a/src/protocols/rdp/user.c
+++ b/src/protocols/rdp/user.c
@@ -17,16 +17,17 @@
  * under the License.
  */
 
-#include "config.h"
-
-#include "audio_input.h"
+#include "channels/audio-input/audio-input.h"
+#include "channels/cliprdr.h"
+#include "channels/pipe-svc.h"
+#include "common/cursor.h"
 #include "common/display.h"
+#include "config.h"
 #include "input.h"
-#include "user.h"
 #include "rdp.h"
-#include "rdp_settings.h"
-#include "rdp_stream.h"
-#include "rdp_svc.h"
+#include "settings.h"
+#include "upload.h"
+#include "user.h"
 
 #ifdef ENABLE_COMMON_SSH
 #include "sftp.h"
@@ -36,9 +37,11 @@
 #include <guacamole/client.h>
 #include <guacamole/protocol.h>
 #include <guacamole/socket.h>
+#include <guacamole/stream.h>
 #include <guacamole/user.h>
 
 #include <pthread.h>
+#include <stddef.h>
 
 int guac_rdp_user_join_handler(guac_user* user, int argc, char** argv) {
 
@@ -86,7 +89,7 @@
             guac_audio_stream_add_user(rdp_client->audio, user);
 
         /* Bring user up to date with any registered static channels */
-        guac_rdp_svc_send_pipes(user);
+        guac_rdp_pipe_svc_send_pipes(user);
 
         /* Synchronize with current display */
         guac_common_display_dup(rdp_client->display, user, user->socket);
@@ -112,7 +115,7 @@
         user->file_handler = guac_rdp_user_file_handler;
 
         /* Inbound arbitrary named pipes */
-        user->pipe_handler = guac_rdp_svc_pipe_handler;
+        user->pipe_handler = guac_rdp_pipe_svc_pipe_handler;
 
     }