From 136bd9a6abd2b208b2d53c3113bc4d67db340437 Mon Sep 17 00:00:00 2001 From: Virtually Nick Date: Fri, 15 Jul 2022 13:55:46 -0400 Subject: [PATCH] [WIP]: [FILES] Implement file transfers. --- src/protocols/spice/Makefile.am | 38 +- src/protocols/spice/channels/file-download.c | 321 +++++++++ src/protocols/spice/channels/file-download.h | 83 +++ src/protocols/spice/channels/file-ls.c | 127 ++++ src/protocols/spice/channels/file-ls.h | 66 ++ src/protocols/spice/channels/file-upload.c | 261 ++++++++ src/protocols/spice/channels/file-upload.h | 72 ++ src/protocols/spice/channels/file.c | 668 +++++++++++++++++++ src/protocols/spice/channels/file.h | 416 +++++++++++- src/protocols/spice/client.c | 11 +- src/protocols/spice/settings.c | 36 + src/protocols/spice/settings.h | 17 + src/protocols/spice/spice-constants.h | 85 +++ src/protocols/spice/spice.c | 18 + src/protocols/spice/spice.h | 6 + 15 files changed, 2206 insertions(+), 19 deletions(-) create mode 100644 src/protocols/spice/channels/file-download.c create mode 100644 src/protocols/spice/channels/file-download.h create mode 100644 src/protocols/spice/channels/file-ls.c create mode 100644 src/protocols/spice/channels/file-ls.h create mode 100644 src/protocols/spice/channels/file-upload.c create mode 100644 src/protocols/spice/channels/file-upload.h diff --git a/src/protocols/spice/Makefile.am b/src/protocols/spice/Makefile.am index 0ffa94b9..902e42bd 100644 --- a/src/protocols/spice/Makefile.am +++ b/src/protocols/spice/Makefile.am @@ -39,6 +39,9 @@ libguac_client_spice_la_SOURCES = \ channels/cursor.c \ channels/display.c \ channels/file.c \ + channels/file-download.c \ + channels/file-ls.c \ + channels/file-upload.c \ client.c \ decompose.c \ input.c \ @@ -49,22 +52,25 @@ libguac_client_spice_la_SOURCES = \ spice.c \ user.c -noinst_HEADERS = \ - argv.h \ - auth.h \ - channels/audio.h \ - channels/clipboard.h \ - channels/cursor.h \ - channels/display.h \ - channels/file.h \ - client.h \ - decompose.h \ - input.h \ - keyboard.h \ - keymap.h \ - log.h \ - settings.h \ - spice.h \ +noinst_HEADERS = \ + argv.h \ + auth.h \ + channels/audio.h \ + channels/clipboard.h \ + channels/cursor.h \ + channels/display.h \ + channels/file.h \ + channels/file-download.h \ + channels/file-ls.h \ + channels/file-upload.h \ + client.h \ + decompose.h \ + input.h \ + keyboard.h \ + keymap.h \ + log.h \ + settings.h \ + spice.h \ user.h libguac_client_spice_la_CFLAGS = \ diff --git a/src/protocols/spice/channels/file-download.c b/src/protocols/spice/channels/file-download.c new file mode 100644 index 00000000..1cda4c16 --- /dev/null +++ b/src/protocols/spice/channels/file-download.c @@ -0,0 +1,321 @@ +/* + * 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 "file-download.h" +#include "file-ls.h" +#include "file.h" +#include "spice.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +void* guac_spice_file_download_monitor(void* data) { + + guac_spice_folder* folder = (guac_spice_folder*) data; + char download_path[GUAC_SPICE_FOLDER_MAX_PATH]; + char download_events[GUAC_SPICE_FOLDER_MAX_EVENTS]; + char file_path[GUAC_SPICE_FOLDER_MAX_PATH]; + const struct inotify_event *event; + + guac_client_log(folder->client, GUAC_LOG_DEBUG, "%s: Starting up file monitor thread.", __func__); + + /* If folder has already been freed, or isn't open, yet, don't do anything. */ + if (folder == NULL) + return NULL; + + download_path[0] = '\0'; + guac_strlcat(download_path, folder->path, GUAC_SPICE_FOLDER_MAX_PATH); + guac_strlcat(download_path, "/Download", GUAC_SPICE_FOLDER_MAX_PATH); + + guac_client_log(folder->client, GUAC_LOG_DEBUG, "%s: Watching folder at path \"%s\".", __func__, download_path); + + int notify = inotify_init(); + + if (notify == -1) { + guac_client_log(folder->client, GUAC_LOG_ERROR, + "%s: Failed to start inotify, automatic downloads will not work: %s", + __func__, strerror(errno)); + return NULL; + } + + if(inotify_add_watch(notify, download_path, IN_CREATE | IN_ATTRIB | IN_CLOSE_WRITE | IN_MOVED_TO | IN_ONLYDIR | IN_EXCL_UNLINK) == -1) { + guac_client_log(folder->client, GUAC_LOG_ERROR, + "%s: Failed to set inotify flags for \"%s\".", + __func__, download_path); + return NULL; + } + + while (true) { + int events = read(notify, download_events, sizeof(download_events)); + if (events == -1 && errno != EAGAIN) { + guac_client_log(folder->client, GUAC_LOG_ERROR, + "%s: Failed to read inotify events: %s", + __func__, strerror(errno)); + return NULL; + } + + if (events <= 0) + continue; + + + for (char* ptr = download_events; ptr < download_events + events; ptr += sizeof(struct inotify_event) + event->len) { + + event = (const struct inotify_event *) ptr; + + if (event->mask & IN_ISDIR) { + guac_client_log(folder->client, GUAC_LOG_DEBUG, "%s: Ignoring event 0x%x for directory %s.", __func__, event->mask, event->name); + continue; + } + + guac_client_log(folder->client, GUAC_LOG_ERROR, + "%s: 0x%x - Downloading the file: %s", __func__, event->mask, event->name, event->cookie); + + file_path[0] = '\0'; + guac_strlcat(file_path, "/Download/", GUAC_SPICE_FOLDER_MAX_PATH); + guac_strlcat(file_path, event->name, GUAC_SPICE_FOLDER_MAX_PATH); + // guac_client_for_owner(folder->client, guac_spice_file_download_to_user, file_path); + //int fileid = guac_spice_folder_open(folder, file_path, O_WRONLY, 0, 0); + // guac_spice_folder_delete(folder, fileid); + + + } + + } + + return NULL; + +} + +int guac_spice_file_download_ack_handler(guac_user* user, guac_stream* stream, + char* message, guac_protocol_status status) { + + guac_client* client = user->client; + guac_spice_client* spice_client = (guac_spice_client*) client->data; + guac_spice_file_download_status* download_status = (guac_spice_file_download_status*) stream->data; + + /* Get folder, return error if no folder */ + guac_spice_folder* folder = spice_client->shared_folder; + if (folder == NULL) { + guac_protocol_send_ack(user->socket, stream, "FAIL (NO FOLDER)", + 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_spice_folder_read(folder, + 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_spice_file_download_get_handler(guac_user* user, guac_object* object, + char* name) { + + guac_client* client = user->client; + guac_spice_client* spice_client = (guac_spice_client*) client->data; + int flags = 0; + + /* Get folder, ignore request if no folder */ + guac_spice_folder* folder = spice_client->shared_folder; + if (folder == NULL) + return 0; + + flags |= O_RDONLY; + + guac_user_log(user, GUAC_LOG_DEBUG, "%s: folder->path=%s, name=%s", __func__, folder->path, name); + + /* Attempt to open file for reading */ + int file_id = guac_spice_folder_open(folder, name, flags, 0, 0); + if (file_id < 0) { + guac_user_log(user, GUAC_LOG_INFO, "Unable to read file \"%s\"", + name); + return 0; + } + + /* Get opened file */ + guac_spice_folder_file* file = guac_spice_folder_get_file(folder, file_id); + if (file == NULL) { + guac_client_log(folder->client, GUAC_LOG_DEBUG, + "%s: Successful open produced bad file_id: %i", + __func__, file_id); + return 0; + } + + /* If directory, send contents of directory */ + if (S_ISDIR(file->stmode)) { + + /* Create stream data */ + guac_spice_file_ls_status* ls_status = malloc(sizeof(guac_spice_file_ls_status)); + ls_status->folder = folder; + 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_spice_file_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 if downloads are allowed */ + else if (!folder->disable_download) { + + /* Create stream data */ + guac_spice_file_download_status* download_status = malloc(sizeof(guac_spice_file_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_spice_file_download_ack_handler; + + /* Associate new stream with get request */ + guac_protocol_send_body(user->socket, object, stream, + "application/octet-stream", name); + + } + + else + guac_client_log(client, GUAC_LOG_INFO, "Unable to download file " + "\"%s\", file downloads have been disabled.", name); + + guac_socket_flush(user->socket); + return 0; +} + +void* guac_spice_file_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_spice_client* spice_client = (guac_spice_client*) client->data; + guac_spice_folder* folder = spice_client->shared_folder; + int flags = 0; + + /* Ignore download if folder has been unloaded */ + if (folder == NULL) + return NULL; + + /* Ignore download if downloads have been disabled */ + if (folder->disable_download) { + guac_client_log(client, GUAC_LOG_WARNING, "A download attempt has " + "been blocked due to downloads being disabled, however it " + "should have been blocked at a higher level. This is likely " + "a bug."); + return NULL; + } + + /* Attempt to open requested file */ + char* path = (char*) data; + flags |= O_RDONLY; + int file_id = guac_spice_folder_open(folder, path, + flags, 0, 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_spice_file_download_status* download_status = malloc(sizeof(guac_spice_file_download_status)); + stream->data = download_status; + stream->ack_handler = guac_spice_file_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_spice_folder_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/spice/channels/file-download.h b/src/protocols/spice/channels/file-download.h new file mode 100644 index 00000000..2d539ba3 --- /dev/null +++ b/src/protocols/spice/channels/file-download.h @@ -0,0 +1,83 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#ifndef GUAC_SPICE_FILE_DOWNLOAD_H +#define GUAC_SPICE_FILE_DOWNLOAD_H + +#include "common/json.h" + +#include +#include +#include + +#include + +/** + * The transfer status of a file being downloaded. + */ +typedef struct guac_spice_file_download_status { + + /** + * The file ID of the file being downloaded. + */ + int file_id; + + /** + * The current position within the file. + */ + uint64_t offset; + +} guac_spice_file_download_status; + +/** + * Function which uses Linux's fanotify facility to monitor the "Download" + * directory of a shared folder for changes and trigger the automatic download + * of that data to the Guacamole user who has access to the shared folder. + * + * @param data + * A pointer to the guac_spice_folder structure in which the Download + * folder is located. + * + * @return + * Always NULL + */ +void* guac_spice_file_download_monitor(void* data); + +/** + * Handler for acknowledgements of receipt of data related to file downloads. + */ +guac_user_ack_handler guac_spice_file_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_spice_file_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_spice_file_download_to_user; + +#endif + diff --git a/src/protocols/spice/channels/file-ls.c b/src/protocols/spice/channels/file-ls.c new file mode 100644 index 00000000..b6bee65f --- /dev/null +++ b/src/protocols/spice/channels/file-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 "file.h" +#include "file-ls.h" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +int guac_spice_file_ls_ack_handler(guac_user* user, guac_stream* stream, + char* message, guac_protocol_status status) { + + int blob_written = 0; + const char* filename; + + guac_spice_file_ls_status* ls_status = (guac_spice_file_ls_status*) stream->data; + + guac_user_log(user, GUAC_LOG_DEBUG, "%s: folder=\"%s\"", __func__, ls_status->folder->path); + + /* If unsuccessful, free stream and abort */ + if (status != GUAC_PROTOCOL_STATUS_SUCCESS) { + guac_spice_folder_close(ls_status->folder, ls_status->file_id); + guac_user_free_stream(user, stream); + free(ls_status); + return 0; + } + + /* While directory entries remain */ + while ((filename = guac_spice_folder_read_dir(ls_status->folder, + ls_status->file_id)) != NULL + && !blob_written) { + + char absolute_path[GUAC_SPICE_FOLDER_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_spice_folder_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; + } + + guac_user_log(user, GUAC_LOG_DEBUG, "%s: absolute_path=\"%s\"", __func__, absolute_path); + + /* Attempt to open file to determine type */ + int flags = (0 | O_RDONLY); + int file_id = guac_spice_folder_open(ls_status->folder, absolute_path, + flags, 0, 0); + if (file_id < 0) + continue; + + /* Get opened file */ + guac_spice_folder_file* file = guac_spice_folder_get_file(ls_status->folder, 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 (S_ISDIR(file->stmode)) + 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_spice_folder_close(ls_status->folder, 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_spice_folder_close(ls_status->folder, 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/spice/channels/file-ls.h b/src/protocols/spice/channels/file-ls.h new file mode 100644 index 00000000..f9d00c1f --- /dev/null +++ b/src/protocols/spice/channels/file-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 "file.h" + +#include +#include +#include + +#include + +/** + * The current state of a directory listing operation. + */ +typedef struct guac_spice_file_ls_status { + + /** + * The filesystem associated with the directory being listed. + */ + guac_spice_folder* folder; + + /** + * The file ID of the directory being listed. + */ + int file_id; + + /** + * The absolute path of the directory being listed. + */ + char directory_name[GUAC_SPICE_FOLDER_MAX_PATH]; + + /** + * The current state of the JSON directory object being written. + */ + guac_common_json_state json_state; + +} guac_spice_file_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_spice_file_ls_ack_handler; + +#endif + diff --git a/src/protocols/spice/channels/file-upload.c b/src/protocols/spice/channels/file-upload.c new file mode 100644 index 00000000..8a2b1575 --- /dev/null +++ b/src/protocols/spice/channels/file-upload.c @@ -0,0 +1,261 @@ +/* + * 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 "file.h" +#include "spice.h" +#include "file-upload.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +/** + * 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; iclient; + guac_spice_client* spice_client = (guac_spice_client*) client->data; + + int file_id; + char file_path[GUAC_SPICE_FOLDER_MAX_PATH]; + + /* Get filesystem, return error if no filesystem */ + guac_spice_folder* folder = spice_client->shared_folder; + if (folder == NULL) { + guac_protocol_send_ack(user->socket, stream, "FAIL (NO FS)", + GUAC_PROTOCOL_STATUS_SERVER_ERROR); + guac_socket_flush(user->socket); + return 0; + } + + /* Ignore upload if uploads have been disabled */ + if (folder->disable_upload) { + guac_client_log(client, GUAC_LOG_WARNING, "A upload attempt has " + "been blocked due to uploads being disabled, however it " + "should have been blocked at a higher level. This is likely " + "a bug."); + guac_protocol_send_ack(user->socket, stream, "FAIL (UPLOAD DISABLED)", + GUAC_PROTOCOL_STATUS_CLIENT_FORBIDDEN); + guac_socket_flush(user->socket); + return 0; + } + + /* Translate name */ + __generate_upload_path(filename, file_path); + + /* Open file */ + file_id = guac_spice_folder_open(folder, file_path, (O_WRONLY | O_CREAT | O_TRUNC), + 1, 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_spice_file_upload_status* upload_status = malloc(sizeof(guac_spice_file_upload_status)); + upload_status->offset = 0; + upload_status->file_id = file_id; + stream->data = upload_status; + stream->blob_handler = guac_spice_file_upload_blob_handler; + stream->end_handler = guac_spice_file_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_spice_file_upload_blob_handler(guac_user* user, guac_stream* stream, + void* data, int length) { + + int bytes_written; + guac_spice_file_upload_status* upload_status = (guac_spice_file_upload_status*) stream->data; + + /* Get filesystem, return error if no filesystem */ + guac_client* client = user->client; + guac_spice_client* spice_client = (guac_spice_client*) client->data; + guac_spice_folder* folder = spice_client->shared_folder; + if (folder == NULL) { + guac_protocol_send_ack(user->socket, stream, "FAIL (NO FOLDER)", + GUAC_PROTOCOL_STATUS_SERVER_ERROR); + guac_socket_flush(user->socket); + return 0; + } + + /* Write entire block */ + while (length > 0) { + + /* Attempt write */ + bytes_written = guac_spice_folder_write(folder, 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 = (char *)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_spice_file_upload_end_handler(guac_user* user, guac_stream* stream) { + + guac_client* client = user->client; + guac_spice_client* spice_client = (guac_spice_client*) client->data; + guac_spice_file_upload_status* upload_status = (guac_spice_file_upload_status*) stream->data; + + /* Get folder, return error if no filesystem */ + guac_spice_folder* folder = spice_client->shared_folder; + if (folder == NULL) { + guac_protocol_send_ack(user->socket, stream, "FAIL (NO FOLDER)", + GUAC_PROTOCOL_STATUS_SERVER_ERROR); + guac_socket_flush(user->socket); + return 0; + } + + /* Close file */ + guac_spice_folder_close(folder, 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_spice_file_upload_put_handler(guac_user* user, guac_object* object, + guac_stream* stream, char* mimetype, char* name) { + + guac_client* client = user->client; + guac_spice_client* spice_client = (guac_spice_client*) client->data; + + /* Get folder, return error if no filesystem */ + guac_spice_folder* folder = spice_client->shared_folder; + if (folder == NULL) { + guac_protocol_send_ack(user->socket, stream, "FAIL (NO FOLDER)", + GUAC_PROTOCOL_STATUS_SERVER_ERROR); + guac_socket_flush(user->socket); + return 0; + } + + /* Ignore upload if uploads have been disabled */ + if (folder->disable_upload) { + guac_client_log(client, GUAC_LOG_WARNING, "A upload attempt has " + "been blocked due to uploads being disabled, however it " + "should have been blocked at a higher level. This is likely " + "a bug."); + guac_protocol_send_ack(user->socket, stream, "FAIL (UPLOAD DISABLED)", + GUAC_PROTOCOL_STATUS_CLIENT_FORBIDDEN); + guac_socket_flush(user->socket); + return 0; + } + + /* Open file */ + int file_id = guac_spice_folder_open(folder, name, (O_WRONLY | O_CREAT | O_TRUNC), + 1, 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_spice_file_upload_status* upload_status = malloc(sizeof(guac_spice_file_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_spice_file_upload_blob_handler; + stream->end_handler = guac_spice_file_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/spice/channels/file-upload.h b/src/protocols/spice/channels/file-upload.h new file mode 100644 index 00000000..d17e170b --- /dev/null +++ b/src/protocols/spice/channels/file-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_SPICE_FILE_UPLOAD_H +#define GUAC_SPICE_FILE_UPLOAD_H + +#include "common/json.h" + +#include +#include +#include + +#include + +/** + * Structure which represents the current state of an upload. + */ +typedef struct guac_spice_file_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_spice_file_upload_status; + +/** + * Handler for inbound files related to file uploads. + */ +guac_user_file_handler guac_spice_file_upload_file_handler; + +/** + * Handler for stream data related to file uploads. + */ +guac_user_blob_handler guac_spice_file_upload_blob_handler; + +/** + * Handler for end-of-stream related to file uploads. + */ +guac_user_end_handler guac_spice_file_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_spice_file_upload_put_handler; + +#endif + diff --git a/src/protocols/spice/channels/file.c b/src/protocols/spice/channels/file.c index 9439c818..4a409242 100644 --- a/src/protocols/spice/channels/file.c +++ b/src/protocols/spice/channels/file.c @@ -20,8 +20,676 @@ #include "config.h" #include "file.h" +#include "file-download.h" +#include "file-ls.h" +#include "file-upload.h" #include +#include +#include +#include + +#include +#include +#include +#include + +/** + * Translates an absolute path for a shared folder to an absolute path which is + * within the real "shared folder" path specified in the connection settings. + * No checking is performed on the path provided, which is assumed to have + * already been normalized and validated as absolute. + * + * @param folder + * The folder containing the file whose path is being translated. + * + * @param virtual_path + * The absolute path to the file on the simulated folder, relative to the + * shared folder root. + * + * @param real_path + * The buffer in which to store the absolute path to the real file on the + * local filesystem. + */ +static void __guac_spice_folder_translate_path(guac_spice_folder* folder, + const char* virtual_path, char* real_path) { + + guac_client_log(folder->client, GUAC_LOG_DEBUG, "%s: virtual_path=\"%s\", drive_path=\"%s\"", __func__, virtual_path, folder->path); + + /* Get drive path */ + char* path = folder->path; + + int i; + + /* Start with path from settings */ + for (i=0; iclient, GUAC_LOG_DEBUG, "%s: virtual_path=\"%s\", real_path=\"%s\"", __func__, virtual_path, real_path); + +} + +guac_spice_folder* guac_spice_folder_alloc(guac_client* client, const char* folder_path, + int create_folder, int disable_download, int disable_upload) { + + guac_client_log(client, GUAC_LOG_DEBUG, "Initializing shared folder at " + "\"%s\".", folder_path); + + /* Create folder if it does not exist */ + if (create_folder) { + guac_client_log(client, GUAC_LOG_DEBUG, + "%s: Creating folder \"%s\" if necessary.", + __func__, folder_path); + + /* Log error if directory creation fails */ + if (mkdir(folder_path, S_IRWXU) && errno != EEXIST) { + guac_client_log(client, GUAC_LOG_ERROR, + "Unable to create folder \"%s\": %s", + folder_path, strerror(errno)); + } + } + + guac_spice_folder* folder = malloc(sizeof(guac_spice_folder)); + + folder->client = client; + folder->path = strdup(folder_path); + folder->file_id_pool = guac_pool_alloc(0); + folder->open_files = 0; + folder->disable_download = disable_download; + folder->disable_upload = disable_upload; + + /* Set up Download directory and watch it. */ + if (!disable_download) { + + guac_client_log(client, GUAC_LOG_DEBUG, "%s: Setting up Download/ folder watch.", __func__); + + if (create_folder) { + guac_client_log(client, GUAC_LOG_DEBUG, "%s: Creating Download/ folder.", + __func__); + + char *download_path; + download_path = guac_strdup(folder_path); + + guac_strlcat(download_path, "/Download", GUAC_SPICE_FOLDER_MAX_PATH); + + if (mkdir(folder_path, S_IRWXU) && errno != EEXIST) { + guac_client_log(client, GUAC_LOG_ERROR, + "%s: Unable to create folder \"%s\": %s", __func__, + download_path, strerror(errno)); + } + + } + + if(pthread_create(&(folder->download_thread), NULL, guac_spice_file_download_monitor, (void*) folder)) { + guac_client_log(client, GUAC_LOG_ERROR, + "%s: Unable to create Download folder thread monitor.", __func__); + } + + } + + return folder; + +} + +void guac_spice_folder_free(guac_spice_folder* folder) { + guac_pool_free(folder->file_id_pool); + free(folder->path); + free(folder); +} + +guac_object* guac_spice_folder_alloc_object(guac_spice_folder *folder, guac_user* user) { + + /* Init folder */ + guac_object* folder_object = guac_user_alloc_object(user); + folder_object->get_handler = guac_spice_file_download_get_handler; + + /* Assign upload handler only if uploads are not disabled. */ + if (!folder->disable_upload) + folder_object->put_handler = guac_spice_file_upload_put_handler; + + folder_object->data = folder; + + /* Send filesystem to user */ + guac_protocol_send_filesystem(user->socket, folder_object, "Shared Folder"); + guac_socket_flush(user->socket); + + return folder_object; + +} + +int guac_spice_folder_append_filename(char* fullpath, const char* path, + const char* filename) { + + int i; + + /* Disallow "." as a filename */ + if (strcmp(filename, ".") == 0) + return 0; + + /* Disallow ".." as a filename */ + if (strcmp(filename, "..") == 0) + return 0; + + /* Copy path, append trailing slash */ + for (i=0; i 0 && path[i-1] != '/' && path[i-1] != '\\') + fullpath[i++] = '/'; + break; + } + + /* Copy character if not end of string */ + fullpath[i] = c; + + } + + /* Append filename */ + for (; iclient, GUAC_LOG_DEBUG, + "%s: Ignoring close for bad file_id: %i", + __func__, file_id); + return; + } + + file = &(folder->files[file_id]); + + guac_client_log(folder->client, GUAC_LOG_DEBUG, + "%s: Closed \"%s\" (file_id=%i)", + __func__, file->absolute_path, file_id); + + /* Close directory, if open */ + if (file->dir != NULL) + closedir(file->dir); + + /* Close file */ + close(file->fd); + + /* Free paths */ + free(file->absolute_path); + free(file->real_path); + + /* Free ID back to pool */ + guac_pool_free_int(folder->file_id_pool, file_id); + folder->open_files--; + +} + +int guac_spice_folder_delete(guac_spice_folder* folder, int file_id) { + + /* Get file */ + guac_spice_folder_file* file = guac_spice_folder_get_file(folder, file_id); + if (file == NULL) { + guac_client_log(folder->client, GUAC_LOG_DEBUG, + "%s: Delete of bad file_id: %i", __func__, file_id); + return GUAC_SPICE_FOLDER_EINVAL; + } + + /* If directory, attempt removal */ + if (S_ISDIR(file->stmode)) { + if (rmdir(file->real_path)) { + guac_client_log(folder->client, GUAC_LOG_DEBUG, + "%s: rmdir() failed: \"%s\"", __func__, file->real_path); + return guac_spice_folder_get_errorcode(errno); + } + } + + /* Otherwise, attempt deletion */ + else if (unlink(file->real_path)) { + guac_client_log(folder->client, GUAC_LOG_DEBUG, + "%s: unlink() failed: \"%s\"", __func__, file->real_path); + return guac_spice_folder_get_errorcode(errno); + } + + return 0; + +} + +void* guac_spice_folder_expose(guac_user* user, void* data) { + + guac_spice_folder* folder = (guac_spice_folder*) data; + + guac_user_log(user, GUAC_LOG_DEBUG, "%s: Exposing folder \"%s\" to user.", __func__, folder->path); + + /* No need to expose if there is no folder or the user has left */ + if (user == NULL || folder == NULL) + return NULL; + + /* Allocate and expose folder object for user */ + return guac_spice_folder_alloc_object(folder, user); + +} + +int guac_spice_folder_get_errorcode(int err) { + + /* Translate errno codes to GUAC_SPICE_FOLDER codes */ + switch(err) { + case ENFILE: + return GUAC_SPICE_FOLDER_ENFILE; + + case ENOENT: + return GUAC_SPICE_FOLDER_ENOENT; + + case ENOTDIR: + return GUAC_SPICE_FOLDER_ENOTDIR; + + case ENOSPC: + return GUAC_SPICE_FOLDER_ENOSPC; + + case EISDIR: + return GUAC_SPICE_FOLDER_EISDIR; + + case EACCES: + return GUAC_SPICE_FOLDER_EACCES; + + case EEXIST: + return GUAC_SPICE_FOLDER_EEXIST; + + case EINVAL: + return GUAC_SPICE_FOLDER_EINVAL; + + case ENOSYS: + return GUAC_SPICE_FOLDER_ENOSYS; + + case ENOTSUP: + return GUAC_SPICE_FOLDER_ENOTSUP; + + default: + return GUAC_SPICE_FOLDER_EINVAL; + + } + +} + +guac_spice_folder_file* guac_spice_folder_get_file(guac_spice_folder* folder, + int file_id) { + + /* Validate ID */ + if (file_id < 0 || file_id >= GUAC_SPICE_FOLDER_MAX_FILES) + return NULL; + + /* Return file at given ID */ + return &(folder->files[file_id]); + +} + +int guac_spice_folder_normalize_path(const char* path, char* abs_path) { + + int path_depth = 0; + const char* path_components[GUAC_SPICE_FOLDER_MAX_PATH_DEPTH]; + + /* If original path is not absolute, normalization fails */ + if (path[0] != '/') + return 1; + + /* Create scratch copy of path excluding leading slash (we will be + * replacing path separators with null terminators and referencing those + * substrings directly as path components) */ + char path_scratch[GUAC_SPICE_FOLDER_MAX_PATH - 1]; + int length = guac_strlcpy(path_scratch, path + 1, + sizeof(path_scratch)); + + /* Fail if provided path is too long */ + if (length >= sizeof(path_scratch)) + return 1; + + /* Locate all path components within path */ + const char* current_path_component = &(path_scratch[0]); + for (int i = 0; i <= length; i++) { + + /* If current character is a path separator, parse as component */ + char c = path_scratch[i]; + if (c == '/' || c == '\0') { + + /* Terminate current component */ + path_scratch[i] = '\0'; + + /* If component refers to parent, just move up in depth */ + if (strcmp(current_path_component, "..") == 0) { + if (path_depth > 0) + path_depth--; + } + + /* Otherwise, if component not current directory, add to list */ + else if (strcmp(current_path_component, ".") != 0 + && strcmp(current_path_component, "") != 0) { + + /* Fail normalization if path is too deep */ + if (path_depth >= GUAC_SPICE_FOLDER_MAX_PATH_DEPTH) + return 1; + + path_components[path_depth++] = current_path_component; + + } + + /* Update start of next component */ + current_path_component = &(path_scratch[i+1]); + + } /* end if separator */ + + /* We do not currently support named streams */ + else if (c == ':') + return 1; + + } /* end for each character */ + + /* Add leading slash for resulting absolute path */ + abs_path[0] = '/'; + + /* Append normalized components to path, separated by slashes */ + guac_strljoin(abs_path + 1, path_components, path_depth, + "/", GUAC_SPICE_FOLDER_MAX_PATH - 1); + + return 0; + +} + +int guac_spice_folder_open(guac_spice_folder* folder, const char* path, + int flags, bool overwrite, bool directory) { + + char real_path[GUAC_SPICE_FOLDER_MAX_PATH]; + char normalized_path[GUAC_SPICE_FOLDER_MAX_PATH]; + + struct stat file_stat; + int fd; + int file_id; + guac_spice_folder_file* file; + + guac_client_log(folder->client, GUAC_LOG_DEBUG, + "%s: path=\"%s\", flags=0x%x, overwrite=0x%x, " + "directory=0x%x", __func__, path, flags, overwrite, directory); + + /* If no files available, return too many open */ + if (folder->open_files >= GUAC_SPICE_FOLDER_MAX_FILES) { + guac_client_log(folder->client, GUAC_LOG_DEBUG, + "%s: Too many open files.", + __func__, path); + return GUAC_SPICE_FOLDER_ENFILE; + } + + /* If path empty, return an error */ + if (path[0] == '\0') + return GUAC_SPICE_FOLDER_EINVAL; + + /* If path is relative, the file does not exist */ + else if (path[0] != '\\' && path[0] != '/') { + guac_client_log(folder->client, GUAC_LOG_DEBUG, + "%s: Access denied - supplied path \"%s\" is relative.", + __func__, path); + return GUAC_SPICE_FOLDER_ENOENT; + } + + /* Translate access into flags */ + if (directory) + flags |= O_DIRECTORY; + + else if (overwrite) + flags |= O_TRUNC; + + /* Normalize path, return no-such-file if invalid */ + if (guac_spice_folder_normalize_path(path, normalized_path)) { + guac_client_log(folder->client, GUAC_LOG_DEBUG, + "%s: Normalization of path \"%s\" failed.", __func__, path); + return GUAC_SPICE_FOLDER_ENOENT; + } + + guac_client_log(folder->client, GUAC_LOG_DEBUG, + "%s: Normalized path \"%s\" to \"%s\".", + __func__, path, normalized_path); + + /* Translate normalized path to real path */ + __guac_spice_folder_translate_path(folder, normalized_path, real_path); + + guac_client_log(folder->client, GUAC_LOG_DEBUG, + "%s: Translated path \"%s\" to \"%s\".", + __func__, normalized_path, real_path); + + /* Create directory first, if necessary */ + if (directory && (flags & O_CREAT)) { + + /* Create directory */ + if (mkdir(real_path, S_IRWXU)) { + if (errno != EEXIST || (flags & O_EXCL)) { + guac_client_log(folder->client, GUAC_LOG_DEBUG, + "%s: mkdir() failed: %s", + __func__, strerror(errno)); + return guac_spice_folder_get_errorcode(errno); + } + } + + /* Unset O_CREAT and O_EXCL as directory must exist before open() */ + flags &= ~(O_CREAT | O_EXCL); + + } + + guac_client_log(folder->client, GUAC_LOG_DEBUG, + "%s: native open: real_path=\"%s\", flags=0x%x", + __func__, real_path, flags); + + /* Open file */ + fd = open(real_path, flags, S_IRUSR | S_IWUSR); + + /* If file open failed as we're trying to write a dir, retry as read-only */ + if (fd == -1 && errno == EISDIR) { + flags &= ~(O_WRONLY | O_RDWR); + flags |= O_RDONLY; + fd = open(real_path, flags, S_IRUSR | S_IWUSR); + } + + if (fd == -1) { + guac_client_log(folder->client, GUAC_LOG_DEBUG, + "%s: open() failed: %s", __func__, strerror(errno)); + return guac_spice_folder_get_errorcode(errno); + } + + /* Get file ID, init file */ + file_id = guac_pool_next_int(folder->file_id_pool); + file = &(folder->files[file_id]); + file->id = file_id; + file->fd = fd; + file->dir = NULL; + file->dir_pattern[0] = '\0'; + file->absolute_path = strdup(normalized_path); + file->real_path = strdup(real_path); + file->bytes_written = 0; + + guac_client_log(folder->client, GUAC_LOG_DEBUG, + "%s: Opened \"%s\" as file_id=%i", + __func__, normalized_path, file_id); + + /* Attempt to pull file information */ + if (fstat(fd, &file_stat) == 0) { + + /* Load size and times */ + file->size = file_stat.st_size; + file->ctime = file_stat.st_ctime; + file->mtime = file_stat.st_mtime; + file->atime = file_stat.st_atime; + file->stmode = file_stat.st_mode; + + } + + /* If information cannot be retrieved, fake it */ + else { + + /* Init information to 0, lacking any alternative */ + file->size = 0; + file->ctime = 0; + file->mtime = 0; + file->atime = 0; + file->stmode = 0; + + } + + folder->open_files++; + + return file_id; + +} + +int guac_spice_folder_read(guac_spice_folder* folder, int file_id, uint64_t offset, + void* buffer, int length) { + + guac_client_log(folder->client, GUAC_LOG_DEBUG, "%s: Attempt to read from file: %s", __func__, folder->path); + + int bytes_read; + + guac_spice_folder_file* file = guac_spice_folder_get_file(folder, file_id); + if (file == NULL) { + guac_client_log(folder->client, GUAC_LOG_DEBUG, + "%s: Read from bad file_id: %i", __func__, file_id); + return GUAC_SPICE_FOLDER_EINVAL; + } + + /* Attempt read */ + lseek(file->fd, offset, SEEK_SET); + bytes_read = read(file->fd, buffer, length); + + /* Translate errno on error */ + if (bytes_read < 0) + return guac_spice_folder_get_errorcode(errno); + + return bytes_read; + +} + +const char* guac_spice_folder_read_dir(guac_spice_folder* folder, int file_id) { + + guac_client_log(folder->client, GUAC_LOG_DEBUG, "%s: Attempt to read directory: %s", __func__, folder->path); + + guac_spice_folder_file* file; + + struct dirent* result; + + /* Only read if file ID is valid */ + if (file_id < 0 || file_id >= GUAC_SPICE_FOLDER_MAX_FILES) + return NULL; + + file = &(folder->files[file_id]); + + /* Open directory if not yet open, stop if error */ + if (file->dir == NULL) { + file->dir = fdopendir(file->fd); + if (file->dir == NULL) + return NULL; + } + + /* Read next entry, stop if error or no more entries */ + if ((result = readdir(file->dir)) == NULL) + return NULL; + + /* Return filename */ + return result->d_name; + +} + +int guac_spice_folder_write(guac_spice_folder* folder, int file_id, uint64_t offset, + void* buffer, int length) { + + guac_client_log(folder->client, GUAC_LOG_DEBUG, "%s: Attempt to write file: %s", __func__, folder->path); + + int bytes_written; + + guac_spice_folder_file* file = guac_spice_folder_get_file(folder, file_id); + if (file == NULL) { + guac_client_log(folder->client, GUAC_LOG_DEBUG, + "%s: Write to bad file_id: %i", __func__, file_id); + return GUAC_SPICE_FOLDER_EINVAL; + } + + /* Attempt write */ + lseek(file->fd, offset, SEEK_SET); + bytes_written = write(file->fd, buffer, length); + + /* Translate errno on error */ + if (bytes_written < 0) + return guac_spice_folder_get_errorcode(errno); + + file->bytes_written += bytes_written; + return bytes_written; + +} void guac_spice_client_file_transfer_handler(SpiceMainChannel* main_channel, SpiceFileTransferTask* task, guac_client* client) { diff --git a/src/protocols/spice/channels/file.h b/src/protocols/spice/channels/file.h index 67df0e35..2905c753 100644 --- a/src/protocols/spice/channels/file.h +++ b/src/protocols/spice/channels/file.h @@ -23,10 +23,424 @@ #include "config.h" -#include +#include "spice-constants.h" +#include +#include +#include +#include + +#include +#include +#include +#include #include +/** + * An arbitrary file on the shared folder. + */ +typedef struct guac_spice_folder_file { + + /** + * The ID of this file. + */ + int id; + + /** + * The absolute path, including filename, of this file on the simulated filesystem. + */ + char* absolute_path; + + /** + * The real path, including filename, of this file on the local filesystem. + */ + char* real_path; + + /** + * Associated local file descriptor. + */ + int fd; + + /** + * Associated directory stream, if any. This field only applies + * if the file is being used as a directory. + */ + DIR* dir; + + /** + * The pattern the check directory contents against, if any. + */ + char dir_pattern[GUAC_SPICE_FOLDER_MAX_PATH]; + + /** + * The size of this file, in bytes. + */ + uint64_t size; + + /** + * The time this file was created, as a UNIX timestamp. + */ + uint64_t ctime; + + /** + * The time this file was last modified, as a UNIX timestamp. + */ + uint64_t mtime; + + /** + * The time this file was last accessed, as a UNIX timestamp. + */ + uint64_t atime; + + /** + * THe mode field of the file, as retrieved by a call to the stat() family + * of functions; + */ + mode_t stmode; + + /** + * The number of bytes written to the file. + */ + uint64_t bytes_written; + +} guac_spice_folder_file; + +/** + * A shared folder for the Spice protocol. + */ +typedef struct guac_spice_folder { + + /** + * The guac_client object this folder is associated with. + */ + guac_client* client; + + /** + * The path to the shared folder. + */ + char* path; + + /** + * The number of currently open files in the folder. + */ + int open_files; + + /** + * A pool of file IDs. + */ + guac_pool* file_id_pool; + + /** + * All available file structures. + */ + guac_spice_folder_file files[GUAC_SPICE_FOLDER_MAX_FILES]; + + /** + * Whether uploads from the client to the shared folder should be disabled. + */ + int disable_download; + + /** + * Whether downloads from the shared folder to the client should be disabled. + */ + int disable_upload; + + /** + * Thread which watches the Download folder and triggers the automatic + * download of files within this subfolder. + */ + pthread_t download_thread; + +} guac_spice_folder; + +/** + * Allocates a new filesystem given a root path which will be shared with the + * user and the remote server via WebDAV. + * + * @param client + * The guac_client associated with the current RDP session. + * + * @param folder_path + * The local directory to use as the root directory of the shared folder. + * + * @param create_folder + * Non-zero if the folder at the path specified should be automatically + * created if it does not yet exist, zero otherwise. + * + * @param disable_download + * Non-zero if downloads from the remote server to the local browser should + * be disabled. + * + * @param disable_upload + * Non-zero if uploads from the browser to the remote server should be + * disabled. + * + * @return + * The newly-allocated filesystem. + */ +guac_spice_folder* guac_spice_folder_alloc(guac_client* client, const char* folder_path, + int create_folder, int disable_download, int disable_upload); + +/** + * Frees the given filesystem. + * + * @param folder + * The folder to free. + */ +void guac_spice_folder_free(guac_spice_folder* folder); + +/** + * Creates and exposes a new filesystem guac_object to the given user, + * providing access to the files within the given Spice shared folder. The + * allocated guac_object must eventually be freed via guac_user_free_object(). + * + * @param folder + * The guac_spice_folder object to expose. + * + * @param user + * The user that the folder should be exposed to. + * + * @return + * A new Guacamole filesystem object, configured to use Spice for uploading + * and downloading files. + */ +guac_object* guac_spice_folder_alloc_object(guac_spice_folder* folder, guac_user* user); + +/** + * Concatenates the given filename with the given path, separating the two + * with a single forward slash. The full result must be no more than + * GUAC_SPICE_FOLDER_MAX_PATH bytes long, counting null terminator. + * + * @param fullpath + * The buffer to store the result within. This buffer must be at least + * GUAC_SPICE_FOLDER_MAX_PATH bytes long. + * + * @param path + * The path to append the filename to. + * + * @param filename + * The filename to append to the path. + * + * @return + * Non-zero if the filename is valid and was successfully appended to the + * path, zero otherwise. + */ +int guac_spice_folder_append_filename(char* fullpath, const char* path, + const char* filename); + +/** + * 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", 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_spice_folder_basename(const char* path); + +/** + * Frees the given file ID, allowing future open operations to reuse it. + * + * @param folder + * The folder containing the file to close. + * + * @param file_id + * The ID of the file to close, as returned by guac_spice_folder_open(). + */ +void guac_spice_folder_close(guac_spice_folder* folder, int file_id); + +/** + * Deletes the file with the given ID. + * + * @param folder + * The folder containing the file to delete. + * + * @param file_id + * The ID of the file to delete, as returned by guac_spice_folder_open(). + * + * @return + * Zero if deletion succeeded, or an error code if an error occurs. All + * error codes are negative values and correspond to GUAC_SPICE_FOLDER + * constants, such as GUAC_SPICE_FOLDER_ENOENT. + */ +int guac_spice_folder_delete(guac_spice_folder* folder, int file_id); + +/** + * Allocates a new filesystem guac_object for the given user, returning the + * resulting guac_object. This function is provided for convenience, as it is + * can be used as the callback for guac_client_foreach_user() or + * guac_client_for_owner(). Note that this guac_object will be tracked + * internally by libguac, will be provided to us in the parameters of handlers + * related to that guac_object, and will automatically be freed when the + * associated guac_user is freed, so the return value of this function can + * safely be ignored. + * + * If either the given user or the given filesystem are NULL, then this + * function has no effect. + * + * @param user + * The use to expose the filesystem to, or NULL if nothing should be + * exposed. + * + * @param data + * A pointer to the guac_spice_folder instance to expose to the given user, + * or NULL if nothing should be exposed. + * + * @return + * The guac_object allocated for the newly-exposed filesystem, or NULL if + * no filesystem object could be allocated. + */ +void* guac_spice_folder_expose(guac_user* user, void* data); + +/** + * Translates the given errno error code to a GUAC_SPICE_FOLDER error code. + * + * @param err + * The error code, as returned within errno by a system call. + * + * @return + * A GUAC_SPICE_FOLDER error code, such as GUAC_SPICE_FOLDER_ENFILE, + * GUAC_SPICE_FOLDER_ENOENT, etc. + */ +int guac_spice_folder_get_errorcode(int err); + +/** + * Returns the file having the given ID, or NULL if no such file exists. + * + * @param folder + * The folder containing the desired file. + * + * @param file_id + * The ID of the desired, as returned by guac_spice_folder_open(). + * + * @return + * The file having the given ID, or NULL is no such file exists. + */ +guac_spice_folder_file* guac_spice_folder_get_file(guac_spice_folder* folder, + int file_id); + +/** + * Given an arbitrary path, which may contain ".." and ".", creates an + * absolute path which does NOT contain ".." or ".". The given path MUST + * be absolute. + * + * @param path + * The path to normalize. + * + * @param abs_path + * The buffer to populate with the normalized path. The normalized path + * will not contain relative path components like ".." or ".". + * + * @return + * Zero if normalization succeeded, non-zero otherwise. + */ +int guac_spice_folder_normalize_path(const char* path, char* abs_path); + +/** + * Opens the given file, returning the a new file ID, or an error code less + * than zero if an error occurs. The given path MUST be absolute, and will be + * translated to be relative to the drive path of the simulated filesystem. + * + * @param folder + * The shared folder to use when opening the file. + * + * @param path + * The absolute path to the file within the simulated filesystem. + * + * @param flags + * A bitwise-OR of various standard POSIX flags to use when opening the + * file or directory. + * + * @param overwrite + * True if the file should be overwritten when opening it, otherwise false. + * + * @param directory + * True if the path specified is a directory, otherwise false. + * + * @return + * A new file ID, which will always be a positive value, or an error code + * if an error occurs. All error codes are negative values and correspond + * to GUAC_SPICE_FOLDER constants, such as GUAC_SPICE_FOLDER_ENOENT. + */ +int guac_spice_folder_open(guac_spice_folder* folder, const char* path, + int flags, bool overwrite, bool directory); + +/** + * Reads up to the given length of bytes from the given offset within the + * file having the given ID. Returns the number of bytes read, zero on EOF, + * and an error code if an error occurs. + * + * @param folder + * The folder containing the file from which data is to be read. + * + * @param file_id + * The ID of the file to read data from, as returned by guac_spice_folder_open(). + * + * @param offset + * The byte offset within the file to start reading from. + * + * @param buffer + * The buffer to fill with data from the file. + * + * @param length + * The maximum number of bytes to read from the file. + * + * @return + * The number of bytes actually read, zero on EOF, or an error code if an + * error occurs. All error codes are negative values and correspond to + * GUAC_SPICE_FOLDER constants, such as GUAC_SPICE_FOLDER_ENOENT. + */ +int guac_spice_folder_read(guac_spice_folder* folder, int file_id, uint64_t offset, + void* buffer, int length); + +/** + * Returns the next filename within the directory having the given file ID, + * or NULL if no more files. + * + * @param folder + * The foleer containing the file to read directory entries from. + * + * @param file_id + * The ID of the file to read directory entries from, as returned by + * guac_spice_folder_open(). + * + * @return + * The name of the next filename within the directory, or NULL if the last + * file in the directory has already been returned by a previous call. + */ +const char* guac_spice_folder_read_dir(guac_spice_folder* folder, int file_id); + +/** + * Writes up to the given length of bytes from the given offset within the + * file having the given ID. Returns the number of bytes written, and an + * error code if an error occurs. + * + * @param folder + * The folder containing the file to which data is to be written. + * + * @param file_id + * The ID of the file to write data to, as returned by guac_spice_folder_open(). + * + * @param offset + * The byte offset within the file to start writinging at. + * + * @param buffer + * The buffer containing the data to write. + * + * @param length + * The maximum number of bytes to write to the file. + * + * @return + * The number of bytes actually written, or an error code if an error + * occurs. All error codes are negative values and correspond to + * GUAC_SPICE_FOLDER constants, such as GUAC_SPICE_FOLDER_ENOENT. + */ +int guac_spice_folder_write(guac_spice_folder* folder, int file_id, uint64_t offset, + void* buffer, int length); + /** * A handler that is called when the SPICE client receives notification of * a new file transfer task. diff --git a/src/protocols/spice/client.c b/src/protocols/spice/client.c index 34666dc5..02903f65 100644 --- a/src/protocols/spice/client.c +++ b/src/protocols/spice/client.c @@ -238,12 +238,14 @@ void guac_spice_client_channel_handler(SpiceSession *spice_session, guac_spice_client* spice_client = (guac_spice_client*) client->data; guac_spice_settings* settings = spice_client->settings; - int id; + int id, type; - /* Get the channel ID. */ + /* Get the channel ID and type. */ g_object_get(channel, SPICE_PROPERTY_CHANNEL_ID, &id, NULL); + g_object_get(channel, SPICE_PROPERTY_CHANNEL_TYPE, &type, NULL); guac_client_log(client, GUAC_LOG_DEBUG, "New channel created: %i", id); + guac_client_log(client, GUAC_LOG_DEBUG, "New channel type: %i", type); /* Check if this is the main channel and register handlers. */ if (SPICE_IS_MAIN_CHANNEL(channel)) { @@ -366,6 +368,11 @@ void guac_spice_client_channel_handler(SpiceSession *spice_session, && strcmp(settings->file_directory, "") != 0) { } } + + if (SPICE_IS_USBREDIR_CHANNEL(channel)) { + guac_client_log(client, GUAC_LOG_DEBUG, "USB redirection is not yet implemented."); + return; + } guac_client_log(client, GUAC_LOG_DEBUG, "Calling spice_channel_connect for channel %d.", id); if (!spice_channel_connect(channel)) diff --git a/src/protocols/spice/settings.c b/src/protocols/spice/settings.c index e15d596e..b128bb29 100644 --- a/src/protocols/spice/settings.c +++ b/src/protocols/spice/settings.c @@ -58,6 +58,9 @@ const char* GUAC_SPICE_CLIENT_ARGS[] = { "file-transfer", "file-directory", "file-transfer-ro", + "file-transfer-create-folder", + "disable-download", + "disable-upload", "server-layout", #ifdef ENABLE_COMMON_SSH @@ -212,6 +215,24 @@ enum SPICE_ARGS_IDX { */ IDX_FILE_TRANSFER_RO, + /** + * Whether or not Guacamole should attempt to create the shared folder + * if it does not already exist. + */ + IDX_FILE_TRANSFER_CREATE_FOLDER, + + /** + * "true" if downloads from the remote server to Guacamole client should + * be disabled, otherwise false or blank. + */ + IDX_DISABLE_DOWNLOAD, + + /** + * "true" if uploads from Guacamole Client to the shared folder should be + * disabled, otherwise false or blank. + */ + IDX_DISABLE_UPLOAD, + /** * The name of the keymap chosen as the layout of the server. Legal names * are defined within the *.keymap files in the "keymaps" directory of the @@ -468,6 +489,21 @@ guac_spice_settings* guac_spice_parse_args(guac_user* user, guac_user_parse_args_boolean(user, GUAC_SPICE_CLIENT_ARGS, argv, IDX_FILE_TRANSFER_RO, false); + /* Whether or not Guacamole should attempt to create a non-existent folder. */ + settings->file_transfer_create_folder = + guac_user_parse_args_boolean(user, GUAC_SPICE_CLIENT_ARGS, argv, + IDX_FILE_TRANSFER_CREATE_FOLDER, false); + + /* Whether or not downloads (Server -> Client) should be disabled. */ + settings->disable_download = + guac_user_parse_args_boolean(user, GUAC_SPICE_CLIENT_ARGS, argv, + IDX_DISABLE_DOWNLOAD, false); + + /* Whether or not uploads (Client -> Server) should be disabled. */ + settings->disable_upload = + guac_user_parse_args_boolean(user, GUAC_SPICE_CLIENT_ARGS, argv, + IDX_DISABLE_UPLOAD, false); + /* Pick keymap based on argument */ settings->server_layout = NULL; if (argv[IDX_SERVER_LAYOUT][0] != '\0') diff --git a/src/protocols/spice/settings.h b/src/protocols/spice/settings.h index 85465202..2e467155 100644 --- a/src/protocols/spice/settings.h +++ b/src/protocols/spice/settings.h @@ -128,6 +128,23 @@ typedef struct guac_spice_settings { */ bool file_transfer_ro; + /** + * If the folder does not exist and this setting is set to True, guacd + * will attempt to create the folder. + */ + bool file_transfer_create_folder; + + /** + * True if downloads (Remote Server -> Guacamole Client) should be + * disabled. + */ + bool disable_download; + + /** + * True if uploads (Guacamole Client -> Remote Server) should be disabled. + */ + bool disable_upload; + /** * The keymap chosen as the layout of the server. */ diff --git a/src/protocols/spice/spice-constants.h b/src/protocols/spice/spice-constants.h index 88c7780c..b2e1f188 100644 --- a/src/protocols/spice/spice-constants.h +++ b/src/protocols/spice/spice-constants.h @@ -31,6 +31,81 @@ */ #define GUAC_SPICE_DEFAULT_DISPLAY_ID 0 +/** + * Error code returned when no more file IDs can be allocated. + */ +#define GUAC_SPICE_FOLDER_ENFILE -1 + +/** + * Error code returned when no such file exists. + */ +#define GUAC_SPICE_FOLDER_ENOENT -2 + +/** + * Error code returned when the operation required a directory + * but the file was not a directory. + */ +#define GUAC_SPICE_FOLDER_ENOTDIR -3 + +/** + * Error code returned when insufficient space exists to complete + * the operation. + */ +#define GUAC_SPICE_FOLDER_ENOSPC -4 + +/** + * Error code returned when the operation requires a normal file but + * a directory was given. + */ +#define GUAC_SPICE_FOLDER_EISDIR -5 + +/** + * Error code returned when permission is denied. + */ +#define GUAC_SPICE_FOLDER_EACCES -6 + +/** + * Error code returned when the operation cannot be completed because the + * file already exists. + */ +#define GUAC_SPICE_FOLDER_EEXIST -7 + +/** + * Error code returned when invalid parameters were given. + */ +#define GUAC_SPICE_FOLDER_EINVAL -8 + +/** + * Error code returned when the operation is not implemented. + */ +#define GUAC_SPICE_FOLDER_ENOSYS -9 + +/** + * Error code returned when the operation is not supported. + */ +#define GUAC_SPICE_FOLDER_ENOTSUP -10 + +/** + * The maximum number of events that can be monitored at a given time for + * the Spice shared folder Download folder monitor. + */ +#define GUAC_SPICE_FOLDER_MAX_EVENTS 256 + +/** + * The maximum length of a path in a shared folder. + */ +#define GUAC_SPICE_FOLDER_MAX_PATH 4096 + +/** + * The maximum number of open files in a shared folder. + */ +#define GUAC_SPICE_FOLDER_MAX_FILES 128 + +/** + * The maximum level of folder deptch in a shared folder. + */ +#define GUAC_SPICE_FOLDER_MAX_PATH_DEPTH 64 + /** * The TLS verification value from Guacamole Client that indicates that hostname * verification should be done. @@ -79,6 +154,11 @@ */ #define SPICE_PROPERTY_CHANNEL_ID "channel-id" +/** + * THe SPICE client channel property that stores the type of the channel. + */ +#define SPICE_PROPERTY_CHANNEL_TYPE "channel-type" + /** * SPICE library property that determines whether or not the sockets are provided * by the client. @@ -351,6 +431,11 @@ */ #define SPICE_SIGNAL_RECORD_STOP "record-stop" +/** + * A signal indicating that a share folder is available. + */ +#define SPICE_SIGNAL_SHARE_FOLDER "notify::share-folder" + /** * The signal indicating that the SPICE server has gone to streaming mode. */ diff --git a/src/protocols/spice/spice.c b/src/protocols/spice/spice.c index c4c811e6..0bd74c9d 100644 --- a/src/protocols/spice/spice.c +++ b/src/protocols/spice/spice.c @@ -100,6 +100,24 @@ SpiceSession* guac_spice_get_session(guac_client* client) { spice_client->keyboard = guac_spice_keyboard_alloc(client, spice_settings->server_layout); + if (spice_settings->file_transfer) { + guac_client_log(client, GUAC_LOG_DEBUG, "File transfer enabled, configuring Spice client."); + g_object_set(spice_session, SPICE_PROPERTY_SHARED_DIR, spice_settings->file_directory, NULL); + g_object_set(spice_session, SPICE_PROPERTY_SHARED_DIR_RO, spice_settings->file_transfer_ro, NULL); + spice_client->shared_folder = guac_spice_folder_alloc(client, + spice_settings->file_directory, + spice_settings->file_transfer_create_folder, + spice_settings->disable_download, + spice_settings->disable_upload + ); + guac_client_for_owner(client, guac_spice_folder_expose, + spice_client->shared_folder); + } + else { + guac_client_log(client, GUAC_LOG_DEBUG, "Disabling file transfer."); + g_object_set(spice_session, SPICE_PROPERTY_SHARED_DIR, NULL, NULL); + } + guac_client_log(client, GUAC_LOG_DEBUG, "Finished setting properties."); /* Return the configured session. */ diff --git a/src/protocols/spice/spice.h b/src/protocols/spice/spice.h index 0d920f2f..ca40349b 100644 --- a/src/protocols/spice/spice.h +++ b/src/protocols/spice/spice.h @@ -22,6 +22,7 @@ #include "config.h" +#include "channels/file.h" #include "common/clipboard.h" #include "common/display.h" #include "common/iconv.h" @@ -114,6 +115,11 @@ typedef struct guac_spice_client { */ guac_common_clipboard* clipboard; + /** + * Shared folder. + */ + guac_spice_folder* shared_folder; + #ifdef ENABLE_COMMON_SSH /** * The user and credentials used to authenticate for SFTP.