[WIP]: [FILES] Implement file transfers.

This commit is contained in:
Virtually Nick 2022-07-15 13:55:46 -04:00
parent 1a6ee4a438
commit 136bd9a6ab
15 changed files with 2206 additions and 19 deletions

View File

@ -39,6 +39,9 @@ libguac_client_spice_la_SOURCES = \
channels/cursor.c \ channels/cursor.c \
channels/display.c \ channels/display.c \
channels/file.c \ channels/file.c \
channels/file-download.c \
channels/file-ls.c \
channels/file-upload.c \
client.c \ client.c \
decompose.c \ decompose.c \
input.c \ input.c \
@ -57,6 +60,9 @@ noinst_HEADERS = \
channels/cursor.h \ channels/cursor.h \
channels/display.h \ channels/display.h \
channels/file.h \ channels/file.h \
channels/file-download.h \
channels/file-ls.h \
channels/file-upload.h \
client.h \ client.h \
decompose.h \ decompose.h \
input.h \ input.h \

View File

@ -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 <guacamole/client.h>
#include <guacamole/object.h>
#include <guacamole/protocol.h>
#include <guacamole/socket.h>
#include <guacamole/stream.h>
#include <guacamole/string.h>
#include <guacamole/user.h>
#include <errno.h>
#include <fcntl.h>
#include <pthread.h>
#include <stdlib.h>
#include <sys/inotify.h>
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;
}

View File

@ -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 <guacamole/protocol.h>
#include <guacamole/stream.h>
#include <guacamole/user.h>
#include <stdint.h>
/**
* The transfer status of a file being downloaded.
*/
typedef struct guac_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

View File

@ -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 <guacamole/client.h>
#include <guacamole/protocol.h>
#include <guacamole/socket.h>
#include <guacamole/stream.h>
#include <guacamole/user.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
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;
}

View File

@ -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 <guacamole/protocol.h>
#include <guacamole/stream.h>
#include <guacamole/user.h>
#include <stdint.h>
/**
* The current state of a directory listing operation.
*/
typedef struct guac_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

View File

@ -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 <guacamole/client.h>
#include <guacamole/object.h>
#include <guacamole/protocol.h>
#include <guacamole/socket.h>
#include <guacamole/stream.h>
#include <guacamole/user.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/stat.h>
/**
* Writes the given filename to the given upload path, sanitizing the filename
* and translating the filename to the root directory.
*
* @param filename
* The filename to sanitize and move to the root directory.
*
* @param path
* A pointer to a buffer which should receive the sanitized path. The
* buffer must have at least GUAC_RDP_FS_MAX_PATH bytes available.
*/
static void __generate_upload_path(const char* filename, char* path) {
int i;
/* Add initial backslash */
*(path++) = '\\';
for (i=1; i<GUAC_SPICE_FOLDER_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_spice_file_upload_file_handler(guac_user* user, guac_stream* stream,
char* mimetype, char* filename) {
guac_client* client = user->client;
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;
}

View File

@ -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 <guacamole/protocol.h>
#include <guacamole/stream.h>
#include <guacamole/user.h>
#include <stdint.h>
/**
* Structure which represents the current state of an upload.
*/
typedef struct guac_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

View File

@ -20,8 +20,676 @@
#include "config.h" #include "config.h"
#include "file.h" #include "file.h"
#include "file-download.h"
#include "file-ls.h"
#include "file-upload.h"
#include <guacamole/client.h> #include <guacamole/client.h>
#include <guacamole/protocol.h>
#include <guacamole/socket.h>
#include <guacamole/string.h>
#include <errno.h>
#include <fcntl.h>
#include <stdbool.h>
#include <sys/stat.h>
/**
* 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; i<GUAC_SPICE_FOLDER_MAX_PATH-1; i++) {
/* Break on end-of-string */
char c = *(path++);
if (c == 0)
break;
/* Copy character */
*(real_path++) = c;
}
/* Translate path */
for (; i<GUAC_SPICE_FOLDER_MAX_PATH-1; i++) {
/* Stop at end of string */
char c = *(virtual_path++);
if (c == 0)
break;
/* Translate backslashes to forward slashes */
if (c == '\\')
c = '/';
/* Store in real path buffer */
*(real_path++)= c;
}
/* Null terminator */
*real_path = 0;
guac_client_log(folder->client, 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<GUAC_SPICE_FOLDER_MAX_PATH; i++) {
/*
* Append trailing slash only if:
* 1) Trailing slash is not already present
* 2) Path is non-empty
*/
char c = path[i];
if (c == '\0') {
if (i > 0 && path[i-1] != '/' && path[i-1] != '\\')
fullpath[i++] = '/';
break;
}
/* Copy character if not end of string */
fullpath[i] = c;
}
/* Append filename */
for (; i<GUAC_SPICE_FOLDER_MAX_PATH; i++) {
char c = *(filename++);
if (c == '\0')
break;
/* Filenames may not contain slashes */
if (c == '\\' || c == '/')
return 0;
/* Append each character within filename */
fullpath[i] = c;
}
/* Verify path length is within maximum */
if (i == GUAC_SPICE_FOLDER_MAX_PATH)
return 0;
/* Terminate path string */
fullpath[i] = '\0';
/* Append was successful */
return 1;
}
const char* guac_spice_folder_basename(const char* path) {
for (const char* c = path; *c != '\0'; c++) {
/* Reset beginning of path if a path separator is found */
if (*c == '/' || *c == '\\')
path = c + 1;
}
/* path now points to the first character after the last path separator */
return path;
}
void guac_spice_folder_close(guac_spice_folder* folder, int file_id) {
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: 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, void guac_spice_client_file_transfer_handler(SpiceMainChannel* main_channel,
SpiceFileTransferTask* task, guac_client* client) { SpiceFileTransferTask* task, guac_client* client) {

View File

@ -23,10 +23,424 @@
#include "config.h" #include "config.h"
#include <guacamole/client.h> #include "spice-constants.h"
#include <guacamole/client.h>
#include <guacamole/object.h>
#include <guacamole/pool.h>
#include <guacamole/user.h>
#include <dirent.h>
#include <pthread.h>
#include <stdbool.h>
#include <stdint.h>
#include <spice-client-glib-2.0/spice-client.h> #include <spice-client-glib-2.0/spice-client.h>
/**
* 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 handler that is called when the SPICE client receives notification of
* a new file transfer task. * a new file transfer task.

View File

@ -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_client* spice_client = (guac_spice_client*) client->data;
guac_spice_settings* settings = spice_client->settings; 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_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 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. */ /* Check if this is the main channel and register handlers. */
if (SPICE_IS_MAIN_CHANNEL(channel)) { if (SPICE_IS_MAIN_CHANNEL(channel)) {
@ -367,6 +369,11 @@ void guac_spice_client_channel_handler(SpiceSession *spice_session,
} }
} }
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); guac_client_log(client, GUAC_LOG_DEBUG, "Calling spice_channel_connect for channel %d.", id);
if (!spice_channel_connect(channel)) if (!spice_channel_connect(channel))
guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR, guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR,

View File

@ -58,6 +58,9 @@ const char* GUAC_SPICE_CLIENT_ARGS[] = {
"file-transfer", "file-transfer",
"file-directory", "file-directory",
"file-transfer-ro", "file-transfer-ro",
"file-transfer-create-folder",
"disable-download",
"disable-upload",
"server-layout", "server-layout",
#ifdef ENABLE_COMMON_SSH #ifdef ENABLE_COMMON_SSH
@ -212,6 +215,24 @@ enum SPICE_ARGS_IDX {
*/ */
IDX_FILE_TRANSFER_RO, 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 * 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 * 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, guac_user_parse_args_boolean(user, GUAC_SPICE_CLIENT_ARGS, argv,
IDX_FILE_TRANSFER_RO, false); 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 */ /* Pick keymap based on argument */
settings->server_layout = NULL; settings->server_layout = NULL;
if (argv[IDX_SERVER_LAYOUT][0] != '\0') if (argv[IDX_SERVER_LAYOUT][0] != '\0')

View File

@ -128,6 +128,23 @@ typedef struct guac_spice_settings {
*/ */
bool file_transfer_ro; 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. * The keymap chosen as the layout of the server.
*/ */

View File

@ -31,6 +31,81 @@
*/ */
#define GUAC_SPICE_DEFAULT_DISPLAY_ID 0 #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 * The TLS verification value from Guacamole Client that indicates that hostname
* verification should be done. * verification should be done.
@ -79,6 +154,11 @@
*/ */
#define SPICE_PROPERTY_CHANNEL_ID "channel-id" #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 * SPICE library property that determines whether or not the sockets are provided
* by the client. * by the client.
@ -351,6 +431,11 @@
*/ */
#define SPICE_SIGNAL_RECORD_STOP "record-stop" #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. * The signal indicating that the SPICE server has gone to streaming mode.
*/ */

View File

@ -100,6 +100,24 @@ SpiceSession* guac_spice_get_session(guac_client* client) {
spice_client->keyboard = guac_spice_keyboard_alloc(client, spice_client->keyboard = guac_spice_keyboard_alloc(client,
spice_settings->server_layout); 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."); guac_client_log(client, GUAC_LOG_DEBUG, "Finished setting properties.");
/* Return the configured session. */ /* Return the configured session. */

View File

@ -22,6 +22,7 @@
#include "config.h" #include "config.h"
#include "channels/file.h"
#include "common/clipboard.h" #include "common/clipboard.h"
#include "common/display.h" #include "common/display.h"
#include "common/iconv.h" #include "common/iconv.h"
@ -114,6 +115,11 @@ typedef struct guac_spice_client {
*/ */
guac_common_clipboard* clipboard; guac_common_clipboard* clipboard;
/**
* Shared folder.
*/
guac_spice_folder* shared_folder;
#ifdef ENABLE_COMMON_SSH #ifdef ENABLE_COMMON_SSH
/** /**
* The user and credentials used to authenticate for SFTP. * The user and credentials used to authenticate for SFTP.