[WIP]: [FILES] Implement file transfers.
This commit is contained in:
parent
1a6ee4a438
commit
136bd9a6ab
@ -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 = \
|
||||
|
321
src/protocols/spice/channels/file-download.c
Normal file
321
src/protocols/spice/channels/file-download.c
Normal 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;
|
||||
|
||||
}
|
||||
|
83
src/protocols/spice/channels/file-download.h
Normal file
83
src/protocols/spice/channels/file-download.h
Normal 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
|
||||
|
127
src/protocols/spice/channels/file-ls.c
Normal file
127
src/protocols/spice/channels/file-ls.c
Normal 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;
|
||||
|
||||
}
|
||||
|
66
src/protocols/spice/channels/file-ls.h
Normal file
66
src/protocols/spice/channels/file-ls.h
Normal 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
|
||||
|
261
src/protocols/spice/channels/file-upload.c
Normal file
261
src/protocols/spice/channels/file-upload.c
Normal 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;
|
||||
}
|
||||
|
72
src/protocols/spice/channels/file-upload.h
Normal file
72
src/protocols/spice/channels/file-upload.h
Normal 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
|
||||
|
@ -20,8 +20,676 @@
|
||||
#include "config.h"
|
||||
|
||||
#include "file.h"
|
||||
#include "file-download.h"
|
||||
#include "file-ls.h"
|
||||
#include "file-upload.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,
|
||||
SpiceFileTransferTask* task, guac_client* client) {
|
||||
|
@ -23,10 +23,424 @@
|
||||
|
||||
#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>
|
||||
|
||||
/**
|
||||
* 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.
|
||||
|
@ -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))
|
||||
|
@ -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')
|
||||
|
@ -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.
|
||||
*/
|
||||
|
@ -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.
|
||||
*/
|
||||
|
@ -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. */
|
||||
|
@ -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.
|
||||
|
Loading…
Reference in New Issue
Block a user