diff --git a/src/common-ssh/common-ssh/sftp.h b/src/common-ssh/common-ssh/sftp.h index 038a5773..0ec2d12b 100644 --- a/src/common-ssh/common-ssh/sftp.h +++ b/src/common-ssh/common-ssh/sftp.h @@ -33,6 +33,11 @@ */ #define GUAC_COMMON_SSH_SFTP_MAX_PATH 2048 +/** + * Maximum number of path components per path. + */ +#define GUAC_COMMON_SSH_SFTP_MAX_DEPTH 1024 + /** * Representation of an SFTP-driven filesystem object. Unlike guac_object, this * structure is not tied to any particular user. @@ -54,6 +59,11 @@ typedef struct guac_common_ssh_sftp_filesystem { */ LIBSSH2_SFTP* sftp_session; + /** + * The path to the directory to expose to the user as a filesystem object. + */ + char root_path[GUAC_COMMON_SSH_SFTP_MAX_PATH]; + /** * The path files will be sent to, if uploaded directly via a "file" * instruction. @@ -103,15 +113,22 @@ typedef struct guac_common_ssh_sftp_ls_state { * The session to use to provide SFTP. This session will automatically be * destroyed when this filesystem is destroyed. * + * @param root_path + * The path accessible via SFTP to consider the root path of the filesystem + * exposed to the user. Only the contents of this path will be available + * via the filesystem object. + * * @param name * The name to send as the name of the filesystem whenever it is exposed - * to a user. + * to a user, or NULL to automatically generate a name from the provided + * root_path. * * @return * A new SFTP filesystem object, not yet exposed to users. */ guac_common_ssh_sftp_filesystem* guac_common_ssh_create_sftp_filesystem( - guac_common_ssh_session* session, const char* name); + guac_common_ssh_session* session, const char* root_path, + const char* name); /** * Destroys the given filesystem object, disconnecting from SFTP and freeing diff --git a/src/common-ssh/sftp.c b/src/common-ssh/sftp.c index 4be15683..e0e029f6 100644 --- a/src/common-ssh/sftp.c +++ b/src/common-ssh/sftp.c @@ -32,6 +32,111 @@ #include #include +/** + * Given an arbitrary absolute path, which may contain "..", ".", and + * backslashes, creates an equivalent absolute path which does NOT contain + * relative path components (".." or "."), backslashes, or empty path + * components. With the exception of paths referring to the root directory, the + * resulting path is guaranteed to not contain trailing slashes. + * + * Normalization will fail if the given path is not absolute, is too long, or + * contains more than GUAC_COMMON_SSH_SFTP_MAX_DEPTH path components. + * + * @param fullpath + * The buffer to populate with the normalized path. The normalized path + * will not contain relative path components like ".." or ".", nor will it + * contain backslashes. This buffer MUST be at least + * GUAC_COMMON_SSH_SFTP_MAX_PATH bytes in size. + * + * @param path + * The absolute path to normalize. + * + * @return + * Non-zero if normalization succeeded, zero otherwise. + */ +static int guac_common_ssh_sftp_normalize_path(char* fullpath, + const char* path) { + + int i; + + int path_depth = 0; + char path_component_data[GUAC_COMMON_SSH_SFTP_MAX_PATH]; + const char* path_components[GUAC_COMMON_SSH_SFTP_MAX_DEPTH]; + + const char** current_path_component = &(path_components[0]); + const char* current_path_component_data = &(path_component_data[0]); + + /* If original path is not absolute, normalization fails */ + if (path[0] != '\\' && path[0] != '/') + return 1; + + /* Skip past leading slash */ + path++; + + /* Copy path into component data for parsing */ + strncpy(path_component_data, path, sizeof(path_component_data) - 1); + + /* Find path components within path */ + for (i = 0; i < sizeof(path_component_data); i++) { + + /* If current character is a path separator, parse as component */ + char c = path_component_data[i]; + if (c == '/' || c == '\\' || c == '\0') { + + /* Terminate current component */ + path_component_data[i] = '\0'; + + /* If component refers to parent, just move up in depth */ + if (strcmp(current_path_component_data, "..") == 0) { + if (path_depth > 0) + path_depth--; + } + + /* Otherwise, if component not current directory, add to list */ + else if (strcmp(current_path_component_data, ".") != 0 + && strcmp(current_path_component_data, "") != 0) + path_components[path_depth++] = current_path_component_data; + + /* If end of string, stop */ + if (c == '\0') + break; + + /* Update start of next component */ + current_path_component_data = &(path_component_data[i+1]); + + } /* end if separator */ + + } /* end for each character */ + + /* If no components, the path is simply root */ + if (path_depth == 0) { + strcpy(fullpath, "/"); + return 1; + } + + /* Ensure last component is null-terminated */ + path_component_data[i] = 0; + + /* Convert components back into path */ + for (; path_depth > 0; path_depth--) { + + const char* filename = *(current_path_component++); + + /* Add separator */ + *(fullpath++) = '/'; + + /* Copy string */ + while (*filename != 0) + *(fullpath++) = *(filename++); + + } + + /* Terminate absolute path */ + *(fullpath++) = 0; + return 1; + +} + /** * Translates the last error message received by the SFTP layer of an SSH * session into a Guacamole protocol status code. @@ -183,6 +288,73 @@ static int guac_ssh_append_filename(char* fullpath, const char* path, } +/** + * Concatenates the given paths, separating the two with a single forward + * slash. The full result must be no more than GUAC_COMMON_SSH_SFTP_MAX_PATH + * bytes long, counting null terminator. + * + * @param fullpath + * The buffer to store the result within. This buffer must be at least + * GUAC_COMMON_SSH_SFTP_MAX_PATH bytes long. + * + * @param path_a + * The path to place at the beginning of the resulting path. + * + * @param path_b + * The path to append after path_a within the resulting path. + * + * @return + * Non-zero if the paths were successfully concatenated together, zero + * otherwise. + */ +static int guac_ssh_append_path(char* fullpath, const char* path_a, + const char* path_b) { + + int i; + + /* Copy path, appending a trailing slash */ + for (i = 0; i < GUAC_COMMON_SSH_SFTP_MAX_PATH; i++) { + + char c = path_a[i]; + if (c == '\0') { + if (i > 0 && path_a[i-1] != '/') + fullpath[i++] = '/'; + break; + } + + /* Copy character if not end of string */ + fullpath[i] = c; + + } + + /* Skip past leading slashes in second path */ + while (*path_b == '/') + path_b++; + + /* Append path */ + for (; i < GUAC_COMMON_SSH_SFTP_MAX_PATH; i++) { + + char c = *(path_b++); + if (c == '\0') + break; + + /* Append each character within path */ + fullpath[i] = c; + + } + + /* Verify path length is within maximum */ + if (i == GUAC_COMMON_SSH_SFTP_MAX_PATH) + return 0; + + /* Terminate path string */ + fullpath[i] = '\0'; + + /* Append was successful */ + return 1; + +} + /** * Handler for blob messages which continue an inbound SFTP data transfer * (upload). The data associated with the given stream is expected to be a @@ -567,6 +739,38 @@ static int guac_common_ssh_sftp_ls_ack_handler(guac_user* user, } +/** + * Translates a stream name for the given SFTP filesystem object into the + * absolute path corresponding to the actual file it represents. + * + * @param fullpath + * The buffer to populate with the translated path. This buffer MUST be at + * least GUAC_COMMON_SSH_SFTP_MAX_PATH bytes in size. + * + * @param object + * The Guacamole protocol object associated with the SFTP filesystem. + * + * @param name + * The name of the stream (file) to translate into an absolute path. + * + * @return + * Non-zero if translation succeeded, zero otherwise. + */ +static int guac_common_ssh_sftp_translate_name(char* fullpath, + guac_object* object, char* name) { + + char normalized_name[GUAC_COMMON_SSH_SFTP_MAX_PATH]; + + guac_common_ssh_sftp_filesystem* filesystem = + (guac_common_ssh_sftp_filesystem*) object->data; + + /* Normalize stream name into a path, and append to the root path */ + return guac_common_ssh_sftp_normalize_path(normalized_name, name) + && guac_ssh_append_path(fullpath, filesystem->root_path, + normalized_name); + +} + /** * Handler for get messages. In context of SFTP and the filesystem exposed via * the Guacamole protocol, get messages request the body of a file within the @@ -587,16 +791,25 @@ static int guac_common_ssh_sftp_ls_ack_handler(guac_user* user, static int guac_common_ssh_sftp_get_handler(guac_user* user, guac_object* object, char* name) { + char fullpath[GUAC_COMMON_SSH_SFTP_MAX_PATH]; + guac_common_ssh_sftp_filesystem* filesystem = (guac_common_ssh_sftp_filesystem*) object->data; LIBSSH2_SFTP* sftp = filesystem->sftp_session; LIBSSH2_SFTP_ATTRIBUTES attributes; + /* Translate stream name into filesystem path */ + if (!guac_common_ssh_sftp_translate_name(fullpath, object, name)) { + guac_user_log(user, GUAC_LOG_INFO, "Unable to generate real path " + "for stream \"%s\"", name); + return 0; + } + /* Attempt to read file information */ - if (libssh2_sftp_stat(sftp, name, &attributes)) { + if (libssh2_sftp_stat(sftp, fullpath, &attributes)) { guac_user_log(user, GUAC_LOG_INFO, "Unable to read file \"%s\"", - name); + fullpath); return 0; } @@ -604,10 +817,10 @@ static int guac_common_ssh_sftp_get_handler(guac_user* user, if (LIBSSH2_SFTP_S_ISDIR(attributes.permissions)) { /* Open as directory */ - LIBSSH2_SFTP_HANDLE* dir = libssh2_sftp_opendir(sftp, name); + LIBSSH2_SFTP_HANDLE* dir = libssh2_sftp_opendir(sftp, fullpath); if (dir == NULL) { guac_user_log(user, GUAC_LOG_INFO, - "Unable to read directory \"%s\"", name); + "Unable to read directory \"%s\"", fullpath); return 0; } @@ -638,11 +851,11 @@ static int guac_common_ssh_sftp_get_handler(guac_user* user, else { /* Open as normal file */ - LIBSSH2_SFTP_HANDLE* file = libssh2_sftp_open(sftp, name, + LIBSSH2_SFTP_HANDLE* file = libssh2_sftp_open(sftp, fullpath, LIBSSH2_FXF_READ, 0); if (file == NULL) { guac_user_log(user, GUAC_LOG_INFO, - "Unable to read file \"%s\"", name); + "Unable to read file \"%s\"", fullpath); return 0; } @@ -688,19 +901,28 @@ static int guac_common_ssh_sftp_get_handler(guac_user* user, static int guac_common_ssh_sftp_put_handler(guac_user* user, guac_object* object, guac_stream* stream, char* mimetype, char* name) { + char fullpath[GUAC_COMMON_SSH_SFTP_MAX_PATH]; + guac_common_ssh_sftp_filesystem* filesystem = (guac_common_ssh_sftp_filesystem*) object->data; LIBSSH2_SFTP* sftp = filesystem->sftp_session; + /* Translate stream name into filesystem path */ + if (!guac_common_ssh_sftp_translate_name(fullpath, object, name)) { + guac_user_log(user, GUAC_LOG_INFO, "Unable to generate real path " + "for stream \"%s\"", name); + return 0; + } + /* Open file via SFTP */ - LIBSSH2_SFTP_HANDLE* file = libssh2_sftp_open(sftp, name, + LIBSSH2_SFTP_HANDLE* file = libssh2_sftp_open(sftp, fullpath, LIBSSH2_FXF_WRITE | LIBSSH2_FXF_CREAT | LIBSSH2_FXF_TRUNC, S_IRUSR | S_IWUSR); /* Acknowledge stream if successful */ if (file != NULL) { - guac_user_log(user, GUAC_LOG_DEBUG, "File \"%s\" opened", name); + guac_user_log(user, GUAC_LOG_DEBUG, "File \"%s\" opened", fullpath); guac_protocol_send_ack(user->socket, stream, "SFTP: File opened", GUAC_PROTOCOL_STATUS_SUCCESS); } @@ -708,7 +930,7 @@ static int guac_common_ssh_sftp_put_handler(guac_user* user, /* Abort on failure */ else { guac_user_log(user, GUAC_LOG_INFO, - "Unable to open file \"%s\"", name); + "Unable to open file \"%s\"", fullpath); guac_protocol_send_ack(user->socket, stream, "SFTP: Open failed", guac_sftp_get_status(filesystem)); } @@ -756,7 +978,8 @@ guac_object* guac_common_ssh_alloc_sftp_filesystem_object( } guac_common_ssh_sftp_filesystem* guac_common_ssh_create_sftp_filesystem( - guac_common_ssh_session* session, const char* name) { + guac_common_ssh_session* session, const char* root_path, + const char* name) { /* Request SFTP */ LIBSSH2_SFTP* sftp_session = libssh2_sftp_init(session->session); @@ -768,10 +991,24 @@ guac_common_ssh_sftp_filesystem* guac_common_ssh_create_sftp_filesystem( malloc(sizeof(guac_common_ssh_sftp_filesystem)); /* Associate SSH session with SFTP data and user */ - filesystem->name = strdup(name); filesystem->ssh_session = session; filesystem->sftp_session = sftp_session; + /* Normalize and store the provided root path */ + if (!guac_common_ssh_sftp_normalize_path(filesystem->root_path, + root_path)) { + guac_client_log(session->client, GUAC_LOG_WARNING, "Cannot create " + "SFTP filesystem - \"%s\" is not a valid path.", root_path); + free(filesystem); + return NULL; + } + + /* Generate filesystem name from root path if no name is provided */ + if (name != NULL) + filesystem->name = strdup(name); + else + filesystem->name = strdup(filesystem->root_path); + /* Initially upload files to current directory */ strcpy(filesystem->upload_path, "."); diff --git a/src/protocols/rdp/rdp.c b/src/protocols/rdp/rdp.c index d06fad32..0b15d055 100644 --- a/src/protocols/rdp/rdp.c +++ b/src/protocols/rdp/rdp.c @@ -987,8 +987,8 @@ void* guac_rdp_client_thread(void* data) { /* Load and expose filesystem */ rdp_client->sftp_filesystem = - guac_common_ssh_create_sftp_filesystem( - rdp_client->sftp_session, "/"); + guac_common_ssh_create_sftp_filesystem(rdp_client->sftp_session, + settings->sftp_root_directory, NULL); /* Expose filesystem to connection owner */ guac_client_for_owner(client, diff --git a/src/protocols/rdp/rdp_settings.c b/src/protocols/rdp/rdp_settings.c index f73ef9d9..57e60167 100644 --- a/src/protocols/rdp/rdp_settings.c +++ b/src/protocols/rdp/rdp_settings.c @@ -84,6 +84,7 @@ const char* GUAC_RDP_CLIENT_ARGS[] = { "sftp-private-key", "sftp-passphrase", "sftp-directory", + "sftp-root-directory", "sftp-server-alive-interval", #endif @@ -367,6 +368,12 @@ enum RDP_ARGS_IDX { */ IDX_SFTP_DIRECTORY, + /** + * The path of the directory within the SSH server to expose as a + * filesystem guac_object. If omitted, "/" will be used by default. + */ + IDX_SFTP_ROOT_DIRECTORY, + /** * The interval at which SSH keepalive messages are sent to the server for * SFTP connections. The default is 0 (disabling keepalives), and a value @@ -784,6 +791,11 @@ guac_rdp_settings* guac_rdp_parse_args(guac_user* user, guac_user_parse_args_string(user, GUAC_RDP_CLIENT_ARGS, argv, IDX_SFTP_DIRECTORY, NULL); + /* SFTP root directory */ + settings->sftp_root_directory = + guac_user_parse_args_string(user, GUAC_RDP_CLIENT_ARGS, argv, + IDX_SFTP_ROOT_DIRECTORY, "/"); + /* Default keepalive value */ settings->sftp_server_alive_interval = guac_user_parse_args_int(user, GUAC_RDP_CLIENT_ARGS, argv, @@ -909,6 +921,7 @@ void guac_rdp_settings_free(guac_rdp_settings* settings) { #ifdef ENABLE_COMMON_SSH /* Free SFTP settings */ free(settings->sftp_directory); + free(settings->sftp_root_directory); free(settings->sftp_hostname); free(settings->sftp_passphrase); free(settings->sftp_password); diff --git a/src/protocols/rdp/rdp_settings.h b/src/protocols/rdp/rdp_settings.h index 8edb79e4..ec540ef7 100644 --- a/src/protocols/rdp/rdp_settings.h +++ b/src/protocols/rdp/rdp_settings.h @@ -360,6 +360,12 @@ typedef struct guac_rdp_settings { */ char* sftp_directory; + /** + * The path of the directory within the SSH server to expose as a + * filesystem guac_object. + */ + char* sftp_root_directory; + /** * The interval at which SSH keepalive messages are sent to the server for * SFTP connections. The default is 0 (disabling keepalives), and a value diff --git a/src/protocols/ssh/settings.c b/src/protocols/ssh/settings.c index 8843923b..832dcfa9 100644 --- a/src/protocols/ssh/settings.c +++ b/src/protocols/ssh/settings.c @@ -37,6 +37,7 @@ const char* GUAC_SSH_CLIENT_ARGS[] = { "font-name", "font-size", "enable-sftp", + "sftp-root-directory", "private-key", "passphrase", #ifdef ENABLE_SSH_AGENT @@ -92,6 +93,12 @@ enum SSH_ARGS_IDX { */ IDX_ENABLE_SFTP, + /** + * The path of the directory within the SSH server to expose as a + * filesystem guac_object. If omitted, "/" will be used by default. + */ + IDX_SFTP_ROOT_DIRECTORY, + /** * The private key to use for authentication, if any. */ @@ -236,6 +243,11 @@ guac_ssh_settings* guac_ssh_parse_args(guac_user* user, guac_user_parse_args_boolean(user, GUAC_SSH_CLIENT_ARGS, argv, IDX_ENABLE_SFTP, false); + /* SFTP root directory */ + settings->sftp_root_directory = + guac_user_parse_args_string(user, GUAC_SSH_CLIENT_ARGS, argv, + IDX_SFTP_ROOT_DIRECTORY, "/"); + #ifdef ENABLE_SSH_AGENT settings->enable_agent = guac_user_parse_args_boolean(user, GUAC_SSH_CLIENT_ARGS, argv, @@ -316,6 +328,9 @@ void guac_ssh_settings_free(guac_ssh_settings* settings) { /* Free requested command */ free(settings->command); + /* Free SFTP settings */ + free(settings->sftp_root_directory); + /* Free typescript settings */ free(settings->typescript_name); free(settings->typescript_path); diff --git a/src/protocols/ssh/settings.h b/src/protocols/ssh/settings.h index f49d054d..f0930236 100644 --- a/src/protocols/ssh/settings.h +++ b/src/protocols/ssh/settings.h @@ -145,6 +145,12 @@ typedef struct guac_ssh_settings { */ bool enable_sftp; + /** + * The path of the directory within the SSH server to expose as a + * filesystem guac_object. + */ + char* sftp_root_directory; + #ifdef ENABLE_SSH_AGENT /** * Whether the SSH agent is enabled. diff --git a/src/protocols/ssh/ssh.c b/src/protocols/ssh/ssh.c index aa9fdaee..b9bb59b3 100644 --- a/src/protocols/ssh/ssh.c +++ b/src/protocols/ssh/ssh.c @@ -266,7 +266,8 @@ void* ssh_client_thread(void* data) { /* Request SFTP */ ssh_client->sftp_filesystem = guac_common_ssh_create_sftp_filesystem( - ssh_client->sftp_session, "/"); + ssh_client->sftp_session, settings->sftp_root_directory, + NULL); /* Expose filesystem to connection owner */ guac_client_for_owner(client, diff --git a/src/protocols/vnc/settings.c b/src/protocols/vnc/settings.c index 697466df..0bcd5abf 100644 --- a/src/protocols/vnc/settings.c +++ b/src/protocols/vnc/settings.c @@ -66,6 +66,7 @@ const char* GUAC_VNC_CLIENT_ARGS[] = { "sftp-private-key", "sftp-passphrase", "sftp-directory", + "sftp-root-directory", "sftp-server-alive-interval", #endif @@ -229,6 +230,12 @@ enum VNC_ARGS_IDX { */ IDX_SFTP_DIRECTORY, + /** + * The path of the directory within the SSH server to expose as a + * filesystem guac_object. If omitted, "/" will be used by default. + */ + IDX_SFTP_ROOT_DIRECTORY, + /** * The interval at which SSH keepalive messages are sent to the server for * SFTP connections. The default is 0 (disabling keepalives), and a value @@ -405,6 +412,11 @@ guac_vnc_settings* guac_vnc_parse_args(guac_user* user, guac_user_parse_args_string(user, GUAC_VNC_CLIENT_ARGS, argv, IDX_SFTP_DIRECTORY, NULL); + /* SFTP root directory */ + settings->sftp_root_directory = + guac_user_parse_args_string(user, GUAC_VNC_CLIENT_ARGS, argv, + IDX_SFTP_ROOT_DIRECTORY, "/"); + /* Default keepalive value */ settings->sftp_server_alive_interval = guac_user_parse_args_int(user, GUAC_VNC_CLIENT_ARGS, argv, @@ -447,6 +459,7 @@ void guac_vnc_settings_free(guac_vnc_settings* settings) { #ifdef ENABLE_COMMON_SSH /* Free SFTP settings */ free(settings->sftp_directory); + free(settings->sftp_root_directory); free(settings->sftp_hostname); free(settings->sftp_passphrase); free(settings->sftp_password); diff --git a/src/protocols/vnc/settings.h b/src/protocols/vnc/settings.h index 3c7b2587..4fa8eb4f 100644 --- a/src/protocols/vnc/settings.h +++ b/src/protocols/vnc/settings.h @@ -174,6 +174,12 @@ typedef struct guac_vnc_settings { */ char* sftp_directory; + /** + * The path of the directory within the SSH server to expose as a + * filesystem guac_object. + */ + char* sftp_root_directory; + /** * The interval at which SSH keepalive messages are sent to the server for * SFTP connections. The default is 0 (disabling keepalives), and a value diff --git a/src/protocols/vnc/vnc.c b/src/protocols/vnc/vnc.c index 81d46f1b..38c7cd60 100644 --- a/src/protocols/vnc/vnc.c +++ b/src/protocols/vnc/vnc.c @@ -271,8 +271,8 @@ void* guac_vnc_client_thread(void* data) { /* Load filesystem */ vnc_client->sftp_filesystem = - guac_common_ssh_create_sftp_filesystem( - vnc_client->sftp_session, "/"); + guac_common_ssh_create_sftp_filesystem(vnc_client->sftp_session, + settings->sftp_root_directory, NULL); /* Expose filesystem to connection owner */ guac_client_for_owner(client,