Repository: incubator-guacamole-server Updated Branches: refs/heads/master 836fc3eaa -> 07db9808a
GUACAMOLE-303: Extend common SFTP filesystem such that arbitrary directories can be used as the root of the filesystem. Project: http://git-wip-us.apache.org/repos/asf/incubator-guacamole-server/repo Commit: http://git-wip-us.apache.org/repos/asf/incubator-guacamole-server/commit/0474f86c Tree: http://git-wip-us.apache.org/repos/asf/incubator-guacamole-server/tree/0474f86c Diff: http://git-wip-us.apache.org/repos/asf/incubator-guacamole-server/diff/0474f86c Branch: refs/heads/master Commit: 0474f86c46309ddae785359c92374820cc768525 Parents: 836fc3e Author: Michael Jumper <[email protected]> Authored: Thu Jun 29 15:28:21 2017 -0700 Committer: Michael Jumper <[email protected]> Committed: Thu Jun 29 15:36:10 2017 -0700 ---------------------------------------------------------------------- src/common-ssh/common-ssh/sftp.h | 21 ++- src/common-ssh/sftp.c | 259 ++++++++++++++++++++++++++++++++-- src/protocols/rdp/rdp.c | 2 +- src/protocols/ssh/ssh.c | 2 +- src/protocols/vnc/vnc.c | 2 +- 5 files changed, 270 insertions(+), 16 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/incubator-guacamole-server/blob/0474f86c/src/common-ssh/common-ssh/sftp.h ---------------------------------------------------------------------- diff --git a/src/common-ssh/common-ssh/sftp.h b/src/common-ssh/common-ssh/sftp.h index 038a577..0ec2d12 100644 --- a/src/common-ssh/common-ssh/sftp.h +++ b/src/common-ssh/common-ssh/sftp.h @@ -34,6 +34,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. */ @@ -55,6 +60,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 http://git-wip-us.apache.org/repos/asf/incubator-guacamole-server/blob/0474f86c/src/common-ssh/sftp.c ---------------------------------------------------------------------- diff --git a/src/common-ssh/sftp.c b/src/common-ssh/sftp.c index 4be1568..e0e029f 100644 --- a/src/common-ssh/sftp.c +++ b/src/common-ssh/sftp.c @@ -33,6 +33,111 @@ #include <string.h> /** + * 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. * @@ -184,6 +289,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 * pointer to an open LIBSSH2_SFTP_HANDLE for the file to which the data @@ -568,6 +740,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 * filesystem. @@ -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, "."); http://git-wip-us.apache.org/repos/asf/incubator-guacamole-server/blob/0474f86c/src/protocols/rdp/rdp.c ---------------------------------------------------------------------- diff --git a/src/protocols/rdp/rdp.c b/src/protocols/rdp/rdp.c index d06fad3..c1be71e 100644 --- a/src/protocols/rdp/rdp.c +++ b/src/protocols/rdp/rdp.c @@ -988,7 +988,7 @@ 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, "/"); + rdp_client->sftp_session, "/", NULL); /* Expose filesystem to connection owner */ guac_client_for_owner(client, http://git-wip-us.apache.org/repos/asf/incubator-guacamole-server/blob/0474f86c/src/protocols/ssh/ssh.c ---------------------------------------------------------------------- diff --git a/src/protocols/ssh/ssh.c b/src/protocols/ssh/ssh.c index aa9fdae..18a0dcb 100644 --- a/src/protocols/ssh/ssh.c +++ b/src/protocols/ssh/ssh.c @@ -266,7 +266,7 @@ 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, "/", NULL); /* Expose filesystem to connection owner */ guac_client_for_owner(client, http://git-wip-us.apache.org/repos/asf/incubator-guacamole-server/blob/0474f86c/src/protocols/vnc/vnc.c ---------------------------------------------------------------------- diff --git a/src/protocols/vnc/vnc.c b/src/protocols/vnc/vnc.c index 81d46f1..2b7263a 100644 --- a/src/protocols/vnc/vnc.c +++ b/src/protocols/vnc/vnc.c @@ -272,7 +272,7 @@ void* guac_vnc_client_thread(void* data) { /* Load filesystem */ vnc_client->sftp_filesystem = guac_common_ssh_create_sftp_filesystem( - vnc_client->sftp_session, "/"); + vnc_client->sftp_session, "/", NULL); /* Expose filesystem to connection owner */ guac_client_for_owner(client,
