GUACAMOLE-249: Refactor away old stream.h and guac_rdp_stream.
diff --git a/src/protocols/rdp/Makefile.am b/src/protocols/rdp/Makefile.am
index 3ba1da0..19c81c2 100644
--- a/src/protocols/rdp/Makefile.am
+++ b/src/protocols/rdp/Makefile.am
@@ -57,6 +57,7 @@
     channels/rdpsnd/rdpsnd.c                     \
     client.c                                     \
     color.c                                      \
+    download.c                                   \
     error.c                                      \
     fs.c                                         \
     gdi.c                                        \
@@ -65,6 +66,7 @@
     keyboard/decompose.c                         \
     keyboard/keyboard.c                          \
     keyboard/keymap.c                            \
+    ls.c                                         \
     plugins/channels.c                           \
     plugins/ptr-string.c                         \
     pointer.c                                    \
@@ -72,8 +74,8 @@
     rdp.c                                        \
     resolution.c                                 \
     settings.c                                   \
-    stream.c                                     \
     unicode.c                                    \
+    upload.c                                     \
     user.c
 
 noinst_HEADERS =                                 \
@@ -96,6 +98,7 @@
     channels/rdpsnd/rdpsnd.h                     \
     client.h                                     \
     color.h                                      \
+    download.h                                   \
     error.h                                      \
     fs.h                                         \
     gdi.h                                        \
@@ -104,6 +107,7 @@
     keyboard/decompose.h                         \
     keyboard/keyboard.h                          \
     keyboard/keymap.h                            \
+    ls.h                                         \
     plugins/channels.h                           \
     plugins/guacai/guacai-messages.h             \
     plugins/guacai/guacai.h                      \
@@ -113,8 +117,8 @@
     rdp.h                                        \
     resolution.h                                 \
     settings.h                                   \
-    stream.h                                     \
     unicode.h                                    \
+    upload.h                                     \
     user.h
 
 libguac_client_rdp_la_CFLAGS = \
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
index 509087b..0e4f66f 100644
--- a/src/protocols/rdp/channels/rdpdr/rdpdr-fs-messages-file-info.c
+++ b/src/protocols/rdp/channels/rdpdr/rdpdr-fs-messages-file-info.c
@@ -20,6 +20,7 @@
 #include "config.h"
 #include "channels/rdpdr/rdpdr-fs-messages-file-info.h"
 #include "channels/rdpdr/rdpdr.h"
+#include "download.h"
 #include "fs.h"
 #include "unicode.h"
 
@@ -158,7 +159,7 @@
             return;
 
         /* Initiate download, pretend move succeeded */
-        guac_rdpdr_start_download(svc, device, file->absolute_path);
+        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);
 
diff --git a/src/protocols/rdp/channels/rdpdr/rdpdr-fs-messages.c b/src/protocols/rdp/channels/rdpdr/rdpdr-fs-messages.c
index 4fede57..8475ce9 100644
--- a/src/protocols/rdp/channels/rdpdr/rdpdr-fs-messages.c
+++ b/src/protocols/rdp/channels/rdpdr/rdpdr-fs-messages.c
@@ -24,6 +24,7 @@
 #include "channels/rdpdr/rdpdr-fs-messages.h"
 #include "channels/rdpdr/rdpdr-messages.h"
 #include "channels/rdpdr/rdpdr.h"
+#include "download.h"
 #include "fs.h"
 #include "unicode.h"
 
@@ -224,7 +225,7 @@
     /* 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(svc, device, file->absolute_path);
+        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);
     }
 
diff --git a/src/protocols/rdp/channels/rdpdr/rdpdr.c b/src/protocols/rdp/channels/rdpdr/rdpdr.c
index 39da918..d0d3143 100644
--- a/src/protocols/rdp/channels/rdpdr/rdpdr.c
+++ b/src/protocols/rdp/channels/rdpdr/rdpdr.c
@@ -26,7 +26,6 @@
 #include "plugins/channels.h"
 #include "rdp.h"
 #include "settings.h"
-#include "stream.h"
 
 #include <freerdp/channels/rdpdr.h>
 #include <freerdp/freerdp.h>
@@ -135,99 +134,6 @@
 
 }
 
-/**
- * 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,
-            FILE_READ_DATA, 0, 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_rdp_common_svc* svc,
-        guac_rdpdr_device* device, char* path) {
-
-    guac_client* client = svc->client;
-
-    /* Initiate download to the owner of the connection */
-    guac_client_for_owner(client, guac_rdpdr_download_to_owner, path);
-
-}
-
 void guac_rdpdr_process_connect(guac_rdp_common_svc* svc) {
 
     /* Get data from client */
diff --git a/src/protocols/rdp/channels/rdpdr/rdpdr.h b/src/protocols/rdp/channels/rdpdr/rdpdr.h
index 19f178d..fc494ee 100644
--- a/src/protocols/rdp/channels/rdpdr/rdpdr.h
+++ b/src/protocols/rdp/channels/rdpdr/rdpdr.h
@@ -213,12 +213,6 @@
         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_rdp_common_svc* svc,
-        guac_rdpdr_device* device, char* path);
-
-/**
  * 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
diff --git a/src/protocols/rdp/download.c b/src/protocols/rdp/download.c
new file mode 100644
index 0000000..34f7e2e
--- /dev/null
+++ b/src/protocols/rdp/download.c
@@ -0,0 +1,242 @@
+/*
+ * 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 "download.h"
+#include "fs.h"
+#include "ls.h"
+#include "rdp.h"
+
+#include <freerdp/channels/channels.h>
+#include <freerdp/client/cliprdr.h>
+#include <freerdp/freerdp.h>
+#include <guacamole/client.h>
+#include <guacamole/protocol.h>
+#include <guacamole/socket.h>
+#include <guacamole/stream.h>
+#include <guacamole/string.h>
+#include <winpr/stream.h>
+#include <winpr/wtypes.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) {
+
+        guac_rdp_download_status* download_status;
+        const char* basename;
+
+        int i;
+        char c;
+
+        /* Associate stream with transfer status */
+        guac_stream* stream = guac_user_alloc_stream(user);
+        stream->data = download_status = malloc(sizeof(guac_rdp_download_status));
+        stream->ack_handler = guac_rdp_download_ack_handler;
+        download_status->file_id = file_id;
+        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(user, GUAC_LOG_DEBUG, "%s: Initiating download "
+                "of \"%s\"", __func__, path);
+
+        /* Begin stream */
+        guac_protocol_send_file(user->socket, stream,
+                "application/octet-stream", basename);
+        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..c15dba6
--- /dev/null
+++ b/src/protocols/rdp/download.h
@@ -0,0 +1,70 @@
+/*
+ * 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 "config.h"
+#include "common/json.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;
+
+/**
+ * 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/fs.c b/src/protocols/rdp/fs.c
index 7dd3ea7..8b48b94 100644
--- a/src/protocols/rdp/fs.c
+++ b/src/protocols/rdp/fs.c
@@ -19,8 +19,9 @@
 
 #include "config.h"
 
+#include "download.h"
 #include "fs.h"
-#include "stream.h"
+#include "upload.h"
 
 #include <guacamole/client.h>
 #include <guacamole/object.h>
diff --git a/src/protocols/rdp/ls.c b/src/protocols/rdp/ls.c
new file mode 100644
index 0000000..f008da9
--- /dev/null
+++ b/src/protocols/rdp/ls.c
@@ -0,0 +1,127 @@
+/*
+ * 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 "fs.h"
+#include "ls.h"
+#include "rdp.h"
+
+#include <freerdp/channels/channels.h>
+#include <freerdp/client/cliprdr.h>
+#include <freerdp/freerdp.h>
+#include <guacamole/client.h>
+#include <guacamole/protocol.h>
+#include <guacamole/socket.h>
+#include <guacamole/stream.h>
+#include <guacamole/string.h>
+#include <winpr/stream.h>
+#include <winpr/wtypes.h>
+
+#include <stdlib.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..21a2eb8
--- /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 "config.h"
+#include "common/json.h"
+
+#include <guacamole/user.h>
+#include <guacamole/protocol.h>
+#include <guacamole/stream.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/rdp.c b/src/protocols/rdp/rdp.c
index 7e91ef1..5a6fb87 100644
--- a/src/protocols/rdp/rdp.c
+++ b/src/protocols/rdp/rdp.c
@@ -39,7 +39,6 @@
 #include "pointer.h"
 #include "print-job.h"
 #include "rdp.h"
-#include "stream.h"
 
 #ifdef ENABLE_COMMON_SSH
 #include "common-ssh/sftp.h"
diff --git a/src/protocols/rdp/stream.c b/src/protocols/rdp/stream.c
deleted file mode 100644
index 71fd035..0000000
--- a/src/protocols/rdp/stream.c
+++ /dev/null
@@ -1,479 +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 "fs.h"
-#include "rdp.h"
-#include "stream.h"
-
-#include <freerdp/channels/channels.h>
-#include <freerdp/client/cliprdr.h>
-#include <freerdp/freerdp.h>
-#include <guacamole/client.h>
-#include <guacamole/protocol.h>
-#include <guacamole/socket.h>
-#include <guacamole/stream.h>
-#include <guacamole/string.h>
-#include <winpr/stream.h>
-#include <winpr/wtypes.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 hav 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;
-    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, 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 */
-    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_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_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_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,
-                GENERIC_READ, 0, 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, 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_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, 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_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/stream.h b/src/protocols/rdp/stream.h
deleted file mode 100644
index 3317c6c..0000000
--- a/src/protocols/rdp/stream.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_RDP_STREAM_H
-#define GUAC_RDP_STREAM_H
-
-#include "config.h"
-#include "common/json.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.
-     */
-    int 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
-
-} 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;
-
-} guac_rdp_stream;
-
-/**
- * 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 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/upload.c b/src/protocols/rdp/upload.c
new file mode 100644
index 0000000..24295ec
--- /dev/null
+++ b/src/protocols/rdp/upload.c
@@ -0,0 +1,241 @@
+/*
+ * 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 "fs.h"
+#include "rdp.h"
+#include "upload.h"
+
+#include <freerdp/channels/channels.h>
+#include <freerdp/client/cliprdr.h>
+#include <freerdp/freerdp.h>
+#include <guacamole/client.h>
+#include <guacamole/protocol.h>
+#include <guacamole/socket.h>
+#include <guacamole/stream.h>
+#include <guacamole/string.h>
+#include <winpr/stream.h>
+#include <winpr/wtypes.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 hav 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 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, 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..b7563ff
--- /dev/null
+++ b/src/protocols/rdp/upload.h
@@ -0,0 +1,73 @@
+/*
+ * 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 "config.h"
+#include "common/json.h"
+
+#include <guacamole/user.h>
+#include <guacamole/protocol.h>
+#include <guacamole/stream.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 a22d138..b070b4a 100644
--- a/src/protocols/rdp/user.c
+++ b/src/protocols/rdp/user.c
@@ -24,7 +24,7 @@
 #include "input.h"
 #include "rdp.h"
 #include "settings.h"
-#include "stream.h"
+#include "upload.h"
 #include "user.h"
 
 #ifdef ENABLE_COMMON_SSH