| /* |
| * 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-ssh/sftp.h" |
| #include "common-ssh/ssh.h" |
| |
| #include <guacamole/client.h> |
| #include <guacamole/object.h> |
| #include <guacamole/protocol.h> |
| #include <guacamole/socket.h> |
| #include <guacamole/string.h> |
| #include <guacamole/user.h> |
| #include <libssh2.h> |
| |
| #include <fcntl.h> |
| #include <libgen.h> |
| #include <stdlib.h> |
| #include <string.h> |
| |
| int guac_common_ssh_sftp_normalize_path(char* fullpath, |
| const char* path) { |
| |
| int path_depth = 0; |
| const char* path_components[GUAC_COMMON_SSH_SFTP_MAX_DEPTH]; |
| |
| /* If original path is not absolute, normalization fails */ |
| if (path[0] != '\\' && path[0] != '/') |
| return 0; |
| |
| /* 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_COMMON_SSH_SFTP_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 0; |
| |
| /* 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 == '\\' || 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_COMMON_SSH_SFTP_MAX_DEPTH) |
| return 0; |
| |
| path_components[path_depth++] = current_path_component; |
| |
| } |
| |
| /* Update start of next component */ |
| current_path_component = &(path_scratch[i+1]); |
| |
| } /* end if separator */ |
| |
| } /* end for each character */ |
| |
| /* Add leading slash for resulting absolute path */ |
| fullpath[0] = '/'; |
| |
| /* Append normalized components to path, separated by slashes */ |
| guac_strljoin(fullpath + 1, path_components, path_depth, |
| "/", GUAC_COMMON_SSH_SFTP_MAX_PATH - 1); |
| |
| return 1; |
| |
| } |
| |
| /** |
| * Translates the last error message received by the SFTP layer of an SSH |
| * session into a Guacamole protocol status code. |
| * |
| * @param filesystem |
| * The object (not guac_object) defining the filesystem associated with the |
| * SFTP and SSH sessions. |
| * |
| * @return |
| * The Guacamole protocol status code corresponding to the last reported |
| * error of the SFTP layer, if nay, or GUAC_PROTOCOL_STATUS_SUCCESS if no |
| * error has occurred. |
| */ |
| static guac_protocol_status guac_sftp_get_status( |
| guac_common_ssh_sftp_filesystem* filesystem) { |
| |
| /* Get libssh2 objects */ |
| LIBSSH2_SFTP* sftp = filesystem->sftp_session; |
| LIBSSH2_SESSION* session = filesystem->ssh_session->session; |
| |
| /* Return success code if no error occurred */ |
| if (libssh2_session_last_errno(session) != LIBSSH2_ERROR_SFTP_PROTOCOL) |
| return GUAC_PROTOCOL_STATUS_SUCCESS; |
| |
| /* Translate SFTP error codes defined by |
| * https://tools.ietf.org/html/draft-ietf-secsh-filexfer-02 (the most |
| * commonly-implemented standard) */ |
| switch (libssh2_sftp_last_error(sftp)) { |
| |
| /* SSH_FX_OK (not an error) */ |
| case 0: |
| return GUAC_PROTOCOL_STATUS_SUCCESS; |
| |
| /* SSH_FX_EOF (technically not an error) */ |
| case 1: |
| return GUAC_PROTOCOL_STATUS_SUCCESS; |
| |
| /* SSH_FX_NO_SUCH_FILE */ |
| case 2: |
| return GUAC_PROTOCOL_STATUS_RESOURCE_NOT_FOUND; |
| |
| /* SSH_FX_PERMISSION_DENIED */ |
| case 3: |
| return GUAC_PROTOCOL_STATUS_CLIENT_FORBIDDEN; |
| |
| /* SSH_FX_FAILURE */ |
| case 4: |
| return GUAC_PROTOCOL_STATUS_UPSTREAM_ERROR; |
| |
| /* SSH_FX_BAD_MESSAGE */ |
| case 5: |
| return GUAC_PROTOCOL_STATUS_SERVER_ERROR; |
| |
| /* SSH_FX_NO_CONNECTION / SSH_FX_CONNECTION_LOST */ |
| case 6: |
| case 7: |
| return GUAC_PROTOCOL_STATUS_UPSTREAM_TIMEOUT; |
| |
| /* SSH_FX_OP_UNSUPPORTED */ |
| case 8: |
| return GUAC_PROTOCOL_STATUS_UNSUPPORTED; |
| |
| /* Return generic error if cause unknown */ |
| default: |
| return GUAC_PROTOCOL_STATUS_UPSTREAM_ERROR; |
| |
| } |
| |
| } |
| |
| /** |
| * 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_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 |
| * 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. |
| */ |
| static int guac_ssh_append_filename(char* fullpath, const char* path, |
| const char* filename) { |
| |
| int length; |
| |
| /* Disallow "." as a filename */ |
| if (strcmp(filename, ".") == 0) |
| return 0; |
| |
| /* Disallow ".." as a filename */ |
| if (strcmp(filename, "..") == 0) |
| return 0; |
| |
| /* Filenames may not contain slashes */ |
| if (strchr(filename, '/') != NULL) |
| return 0; |
| |
| /* Copy base path */ |
| length = guac_strlcpy(fullpath, path, GUAC_COMMON_SSH_SFTP_MAX_PATH); |
| |
| /* |
| * Append trailing slash only if: |
| * 1) Trailing slash is not already present |
| * 2) Path is non-empty |
| */ |
| if (length > 0 && fullpath[length - 1] != '/') |
| length += guac_strlcpy(fullpath + length, "/", |
| GUAC_COMMON_SSH_SFTP_MAX_PATH - length); |
| |
| /* Append filename */ |
| length += guac_strlcpy(fullpath + length, filename, |
| GUAC_COMMON_SSH_SFTP_MAX_PATH - length); |
| |
| /* Verify path length is within maximum */ |
| if (length >= GUAC_COMMON_SSH_SFTP_MAX_PATH) |
| return 0; |
| |
| /* Append was successful */ |
| return 1; |
| |
| } |
| |
| /** |
| * 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 length; |
| |
| /* Copy first half of path */ |
| length = guac_strlcpy(fullpath, path_a, GUAC_COMMON_SSH_SFTP_MAX_PATH); |
| if (length >= GUAC_COMMON_SSH_SFTP_MAX_PATH) |
| return 0; |
| |
| /* Ensure path ends with trailing slash */ |
| if (length == 0 || fullpath[length - 1] != '/') |
| length += guac_strlcpy(fullpath + length, "/", |
| GUAC_COMMON_SSH_SFTP_MAX_PATH - length); |
| |
| /* Skip past leading slashes in second path */ |
| while (*path_b == '/') |
| path_b++; |
| |
| /* Append final half of path */ |
| length += guac_strlcpy(fullpath + length, path_b, |
| GUAC_COMMON_SSH_SFTP_MAX_PATH - length); |
| |
| /* Verify path length is within maximum */ |
| if (length >= GUAC_COMMON_SSH_SFTP_MAX_PATH) |
| return 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 |
| * should be written. |
| * |
| * @param user |
| * The user receiving the blob message. |
| * |
| * @param stream |
| * The Guacamole protocol stream associated with the received blob message. |
| * |
| * @param data |
| * The data received within the blob. |
| * |
| * @param length |
| * The length of the received data, in bytes. |
| * |
| * @return |
| * Zero if the blob is handled successfully, or non-zero on error. |
| */ |
| static int guac_common_ssh_sftp_blob_handler(guac_user* user, |
| guac_stream* stream, void* data, int length) { |
| |
| /* Pull file from stream */ |
| LIBSSH2_SFTP_HANDLE* file = (LIBSSH2_SFTP_HANDLE*) stream->data; |
| |
| /* Attempt write */ |
| if (libssh2_sftp_write(file, data, length) == length) { |
| guac_user_log(user, GUAC_LOG_DEBUG, "%i bytes written", length); |
| guac_protocol_send_ack(user->socket, stream, "SFTP: OK", |
| GUAC_PROTOCOL_STATUS_SUCCESS); |
| guac_socket_flush(user->socket); |
| } |
| |
| /* Inform of any errors */ |
| else { |
| guac_user_log(user, GUAC_LOG_INFO, "Unable to write to file"); |
| guac_protocol_send_ack(user->socket, stream, "SFTP: Write failed", |
| GUAC_PROTOCOL_STATUS_SERVER_ERROR); |
| guac_socket_flush(user->socket); |
| } |
| |
| return 0; |
| |
| } |
| |
| /** |
| * Handler for end messages which terminate 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 |
| * has been written and which should now be closed. |
| * |
| * @param user |
| * The user receiving the end message. |
| * |
| * @param stream |
| * The Guacamole protocol stream associated with the received end message. |
| * |
| * @return |
| * Zero if the file is closed successfully, or non-zero on error. |
| */ |
| static int guac_common_ssh_sftp_end_handler(guac_user* user, |
| guac_stream* stream) { |
| |
| /* Pull file from stream */ |
| LIBSSH2_SFTP_HANDLE* file = (LIBSSH2_SFTP_HANDLE*) stream->data; |
| |
| /* Attempt to close file */ |
| if (libssh2_sftp_close(file) == 0) { |
| guac_user_log(user, GUAC_LOG_DEBUG, "File closed"); |
| guac_protocol_send_ack(user->socket, stream, "SFTP: OK", |
| GUAC_PROTOCOL_STATUS_SUCCESS); |
| guac_socket_flush(user->socket); |
| } |
| else { |
| guac_user_log(user, GUAC_LOG_INFO, "Unable to close file"); |
| guac_protocol_send_ack(user->socket, stream, "SFTP: Close failed", |
| GUAC_PROTOCOL_STATUS_SERVER_ERROR); |
| guac_socket_flush(user->socket); |
| } |
| |
| return 0; |
| |
| } |
| |
| int guac_common_ssh_sftp_handle_file_stream( |
| guac_common_ssh_sftp_filesystem* filesystem, guac_user* user, |
| guac_stream* stream, char* mimetype, char* filename) { |
| |
| char fullpath[GUAC_COMMON_SSH_SFTP_MAX_PATH]; |
| LIBSSH2_SFTP_HANDLE* file; |
| |
| /* Concatenate filename with path */ |
| if (!guac_ssh_append_filename(fullpath, filesystem->upload_path, |
| filename)) { |
| |
| guac_user_log(user, GUAC_LOG_DEBUG, |
| "Filename \"%s\" is invalid or resulting path is too long", |
| filename); |
| |
| /* Abort transfer - invalid filename */ |
| guac_protocol_send_ack(user->socket, stream, |
| "SFTP: Illegal filename", |
| GUAC_PROTOCOL_STATUS_CLIENT_BAD_REQUEST); |
| |
| guac_socket_flush(user->socket); |
| return 0; |
| } |
| |
| /* Open file via SFTP */ |
| file = libssh2_sftp_open(filesystem->sftp_session, fullpath, |
| LIBSSH2_FXF_WRITE | LIBSSH2_FXF_CREAT | LIBSSH2_FXF_TRUNC, |
| S_IRUSR | S_IWUSR); |
| |
| /* Inform of status */ |
| if (file != NULL) { |
| |
| 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); |
| guac_socket_flush(user->socket); |
| } |
| else { |
| guac_user_log(user, GUAC_LOG_INFO, |
| "Unable to open file \"%s\"", fullpath); |
| guac_protocol_send_ack(user->socket, stream, "SFTP: Open failed", |
| guac_sftp_get_status(filesystem)); |
| guac_socket_flush(user->socket); |
| } |
| |
| /* Set handlers for file stream */ |
| stream->blob_handler = guac_common_ssh_sftp_blob_handler; |
| stream->end_handler = guac_common_ssh_sftp_end_handler; |
| |
| /* Store file within stream */ |
| stream->data = file; |
| return 0; |
| |
| } |
| |
| /** |
| * Handler for ack messages which continue an outbound SFTP data transfer |
| * (download), signalling the current status and requesting additional data. |
| * The data associated with the given stream is expected to be a pointer to an |
| * open LIBSSH2_SFTP_HANDLE for the file from which the data is to be read. |
| * |
| * @param user |
| * The user receiving the ack message. |
| * |
| * @param stream |
| * The Guacamole protocol stream associated with the received ack message. |
| * |
| * @param message |
| * An arbitrary human-readable message describing the nature of the |
| * success or failure denoted by the ack message. |
| * |
| * @param status |
| * The status code associated with the ack message, which may indicate |
| * success or an error. |
| * |
| * @return |
| * Zero if the file is read from successfully, or non-zero on error. |
| */ |
| static int guac_common_ssh_sftp_ack_handler(guac_user* user, |
| guac_stream* stream, char* message, guac_protocol_status status) { |
| |
| /* Pull file from stream */ |
| LIBSSH2_SFTP_HANDLE* file = (LIBSSH2_SFTP_HANDLE*) stream->data; |
| |
| /* If successful, read data */ |
| if (status == GUAC_PROTOCOL_STATUS_SUCCESS) { |
| |
| /* Attempt read into buffer */ |
| char buffer[4096]; |
| int bytes_read = libssh2_sftp_read(file, buffer, sizeof(buffer)); |
| |
| /* If bytes read, send as blob */ |
| if (bytes_read > 0) { |
| guac_protocol_send_blob(user->socket, stream, |
| buffer, bytes_read); |
| |
| guac_user_log(user, GUAC_LOG_DEBUG, "%i bytes sent to user", |
| bytes_read); |
| |
| } |
| |
| /* If bytes could not be read, handle EOF or error condition */ |
| else { |
| |
| /* If EOF, send end */ |
| if (bytes_read == 0) { |
| guac_user_log(user, GUAC_LOG_DEBUG, "File sent"); |
| guac_protocol_send_end(user->socket, stream); |
| guac_user_free_stream(user, stream); |
| } |
| |
| /* Otherwise, fail stream */ |
| else { |
| guac_user_log(user, GUAC_LOG_INFO, "Error reading file"); |
| guac_protocol_send_end(user->socket, stream); |
| guac_user_free_stream(user, stream); |
| } |
| |
| /* Close file */ |
| if (libssh2_sftp_close(file) == 0) |
| guac_user_log(user, GUAC_LOG_DEBUG, "File closed"); |
| else |
| guac_user_log(user, GUAC_LOG_INFO, "Unable to close file"); |
| |
| } |
| |
| guac_socket_flush(user->socket); |
| |
| } |
| |
| /* Otherwise, return stream to user */ |
| else |
| guac_user_free_stream(user, stream); |
| |
| return 0; |
| } |
| |
| guac_stream* guac_common_ssh_sftp_download_file( |
| guac_common_ssh_sftp_filesystem* filesystem, guac_user* user, |
| char* filename) { |
| |
| guac_stream* stream; |
| LIBSSH2_SFTP_HANDLE* file; |
| |
| /* Attempt to open file for reading */ |
| file = libssh2_sftp_open(filesystem->sftp_session, filename, |
| LIBSSH2_FXF_READ, 0); |
| if (file == NULL) { |
| guac_user_log(user, GUAC_LOG_INFO, |
| "Unable to read file \"%s\"", filename); |
| return NULL; |
| } |
| |
| /* Allocate stream */ |
| stream = guac_user_alloc_stream(user); |
| stream->ack_handler = guac_common_ssh_sftp_ack_handler; |
| stream->data = file; |
| |
| /* Send stream start, strip name */ |
| filename = basename(filename); |
| guac_protocol_send_file(user->socket, stream, |
| "application/octet-stream", filename); |
| guac_socket_flush(user->socket); |
| |
| guac_user_log(user, GUAC_LOG_DEBUG, "Sending file \"%s\"", filename); |
| return stream; |
| |
| } |
| |
| void guac_common_ssh_sftp_set_upload_path( |
| guac_common_ssh_sftp_filesystem* filesystem, const char* path) { |
| |
| guac_client* client = filesystem->ssh_session->client; |
| |
| /* Ignore requests which exceed maximum-allowed path */ |
| int length = strnlen(path, GUAC_COMMON_SSH_SFTP_MAX_PATH)+1; |
| if (length > GUAC_COMMON_SSH_SFTP_MAX_PATH) { |
| guac_client_log(client, GUAC_LOG_ERROR, |
| "Submitted path exceeds limit of %i bytes", |
| GUAC_COMMON_SSH_SFTP_MAX_PATH); |
| return; |
| } |
| |
| /* Copy path */ |
| memcpy(filesystem->upload_path, path, length); |
| guac_client_log(client, GUAC_LOG_DEBUG, "Upload path set to \"%s\"", path); |
| |
| } |
| |
| /** |
| * Handler for ack messages received due to receipt of a "body" or "blob" |
| * instruction associated with a SFTP directory list operation. |
| * |
| * @param user |
| * The user receiving the ack message. |
| * |
| * @param stream |
| * The Guacamole protocol stream associated with the received ack message. |
| * |
| * @param message |
| * An arbitrary human-readable message describing the nature of the |
| * success or failure denoted by this ack message. |
| * |
| * @param status |
| * The status code associated with this ack message, which may indicate |
| * success or an error. |
| * |
| * @return |
| * Zero on success, non-zero on error. |
| */ |
| static int guac_common_ssh_sftp_ls_ack_handler(guac_user* user, |
| guac_stream* stream, char* message, guac_protocol_status status) { |
| |
| int bytes_read; |
| int blob_written = 0; |
| |
| char filename[GUAC_COMMON_SSH_SFTP_MAX_PATH]; |
| LIBSSH2_SFTP_ATTRIBUTES attributes; |
| |
| guac_common_ssh_sftp_ls_state* list_state = |
| (guac_common_ssh_sftp_ls_state*) stream->data; |
| |
| guac_common_ssh_sftp_filesystem* filesystem = list_state->filesystem; |
| |
| LIBSSH2_SFTP* sftp = filesystem->sftp_session; |
| |
| /* If unsuccessful, free stream and abort */ |
| if (status != GUAC_PROTOCOL_STATUS_SUCCESS) { |
| libssh2_sftp_closedir(list_state->directory); |
| guac_user_free_stream(user, stream); |
| free(list_state); |
| return 0; |
| } |
| |
| /* While directory entries remain */ |
| while ((bytes_read = libssh2_sftp_readdir(list_state->directory, |
| filename, sizeof(filename), &attributes)) > 0 |
| && !blob_written) { |
| |
| char absolute_path[GUAC_COMMON_SSH_SFTP_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_ssh_append_filename(absolute_path, |
| list_state->directory_name, filename)) { |
| |
| guac_user_log(user, GUAC_LOG_DEBUG, |
| "Skipping filename \"%s\" - filename is invalid or " |
| "resulting path is too long", filename); |
| |
| continue; |
| } |
| |
| /* Stat explicitly if symbolic link (might point to directory) */ |
| if (LIBSSH2_SFTP_S_ISLNK(attributes.permissions)) |
| libssh2_sftp_stat(sftp, absolute_path, &attributes); |
| |
| /* Determine mimetype */ |
| const char* mimetype; |
| if (LIBSSH2_SFTP_S_ISDIR(attributes.permissions)) |
| mimetype = GUAC_USER_STREAM_INDEX_MIMETYPE; |
| else |
| mimetype = "application/octet-stream"; |
| |
| /* Write entry */ |
| blob_written |= guac_common_json_write_property(user, stream, |
| &list_state->json_state, absolute_path, mimetype); |
| |
| } |
| |
| /* Complete JSON and cleanup at end of directory */ |
| if (bytes_read <= 0) { |
| |
| /* Complete JSON object */ |
| guac_common_json_end_object(user, stream, &list_state->json_state); |
| guac_common_json_flush(user, stream, &list_state->json_state); |
| |
| /* Clean up resources */ |
| libssh2_sftp_closedir(list_state->directory); |
| free(list_state); |
| |
| /* Signal of stream */ |
| guac_protocol_send_end(user->socket, stream); |
| guac_user_free_stream(user, stream); |
| |
| } |
| |
| guac_socket_flush(user->socket); |
| return 0; |
| |
| } |
| |
| /** |
| * 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. |
| * |
| * @param user |
| * The user who sent the get message. |
| * |
| * @param object |
| * The Guacamole protocol object associated with the get request itself. |
| * |
| * @param name |
| * The name of the input stream (file) being requested. |
| * |
| * @return |
| * Zero on success, non-zero on error. |
| */ |
| 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, fullpath, &attributes)) { |
| guac_user_log(user, GUAC_LOG_INFO, "Unable to read file \"%s\"", |
| fullpath); |
| return 0; |
| } |
| |
| /* If directory, send contents of directory */ |
| if (LIBSSH2_SFTP_S_ISDIR(attributes.permissions)) { |
| |
| /* Open as directory */ |
| LIBSSH2_SFTP_HANDLE* dir = libssh2_sftp_opendir(sftp, fullpath); |
| if (dir == NULL) { |
| guac_user_log(user, GUAC_LOG_INFO, |
| "Unable to read directory \"%s\"", fullpath); |
| return 0; |
| } |
| |
| /* Init directory listing state */ |
| guac_common_ssh_sftp_ls_state* list_state = |
| malloc(sizeof(guac_common_ssh_sftp_ls_state)); |
| |
| list_state->directory = dir; |
| list_state->filesystem = filesystem; |
| |
| int length = guac_strlcpy(list_state->directory_name, name, |
| sizeof(list_state->directory_name)); |
| |
| /* Bail out if directory name is too long to store */ |
| if (length >= sizeof(list_state->directory_name)) { |
| guac_user_log(user, GUAC_LOG_INFO, "Unable to read directory " |
| "\"%s\": Path too long", fullpath); |
| free(list_state); |
| return 0; |
| } |
| |
| /* Allocate stream for body */ |
| guac_stream* stream = guac_user_alloc_stream(user); |
| stream->ack_handler = guac_common_ssh_sftp_ls_ack_handler; |
| stream->data = list_state; |
| |
| /* Init JSON object state */ |
| guac_common_json_begin_object(user, stream, &list_state->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 */ |
| else { |
| |
| /* Open as normal file */ |
| 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\"", fullpath); |
| return 0; |
| } |
| |
| /* Allocate stream for body */ |
| guac_stream* stream = guac_user_alloc_stream(user); |
| stream->ack_handler = guac_common_ssh_sftp_ack_handler; |
| stream->data = file; |
| |
| /* Associate new stream with get request */ |
| guac_protocol_send_body(user->socket, object, stream, |
| "application/octet-stream", name); |
| |
| } |
| |
| guac_socket_flush(user->socket); |
| return 0; |
| } |
| |
| /** |
| * Handler for put messages. In context of SFTP and the filesystem exposed via |
| * the Guacamole protocol, put messages request write access to a file within |
| * the filesystem. |
| * |
| * @param user |
| * The user who sent the put message. |
| * |
| * @param object |
| * The Guacamole protocol object associated with the put request itself. |
| * |
| * @param stream |
| * The Guacamole protocol stream along which the user will be sending |
| * file data. |
| * |
| * @param mimetype |
| * The mimetype of the data being send along the stream. |
| * |
| * @param name |
| * The name of the input stream (file) being requested. |
| * |
| * @return |
| * Zero on success, non-zero on error. |
| */ |
| 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, 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", fullpath); |
| guac_protocol_send_ack(user->socket, stream, "SFTP: File opened", |
| GUAC_PROTOCOL_STATUS_SUCCESS); |
| } |
| |
| /* Abort on failure */ |
| else { |
| guac_user_log(user, GUAC_LOG_INFO, |
| "Unable to open file \"%s\"", fullpath); |
| guac_protocol_send_ack(user->socket, stream, "SFTP: Open failed", |
| guac_sftp_get_status(filesystem)); |
| } |
| |
| /* Set handlers for file stream */ |
| stream->blob_handler = guac_common_ssh_sftp_blob_handler; |
| stream->end_handler = guac_common_ssh_sftp_end_handler; |
| |
| /* Store file within stream */ |
| stream->data = file; |
| |
| guac_socket_flush(user->socket); |
| return 0; |
| } |
| |
| void* guac_common_ssh_expose_sftp_filesystem(guac_user* user, void* data) { |
| |
| guac_common_ssh_sftp_filesystem* filesystem = |
| (guac_common_ssh_sftp_filesystem*) data; |
| |
| /* No need to expose if there is no filesystem or the user has left */ |
| if (user == NULL || filesystem == NULL) |
| return NULL; |
| |
| /* Allocate and expose filesystem object for user */ |
| return guac_common_ssh_alloc_sftp_filesystem_object(filesystem, user); |
| |
| } |
| |
| guac_object* guac_common_ssh_alloc_sftp_filesystem_object( |
| guac_common_ssh_sftp_filesystem* filesystem, guac_user* user) { |
| |
| /* Init filesystem */ |
| guac_object* fs_object = guac_user_alloc_object(user); |
| fs_object->get_handler = guac_common_ssh_sftp_get_handler; |
| fs_object->put_handler = guac_common_ssh_sftp_put_handler; |
| fs_object->data = filesystem; |
| |
| /* Send filesystem to user */ |
| guac_protocol_send_filesystem(user->socket, fs_object, filesystem->name); |
| guac_socket_flush(user->socket); |
| |
| return fs_object; |
| |
| } |
| |
| guac_common_ssh_sftp_filesystem* guac_common_ssh_create_sftp_filesystem( |
| guac_common_ssh_session* session, const char* root_path, |
| const char* name) { |
| |
| /* Request SFTP */ |
| LIBSSH2_SFTP* sftp_session = libssh2_sftp_init(session->session); |
| if (sftp_session == NULL) |
| return NULL; |
| |
| /* Allocate data for SFTP session */ |
| guac_common_ssh_sftp_filesystem* filesystem = |
| malloc(sizeof(guac_common_ssh_sftp_filesystem)); |
| |
| /* Associate SSH session with SFTP data and user */ |
| 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, "."); |
| |
| /* Return allocated filesystem */ |
| return filesystem; |
| |
| } |
| |
| void guac_common_ssh_destroy_sftp_filesystem( |
| guac_common_ssh_sftp_filesystem* filesystem) { |
| |
| /* Shutdown SFTP session */ |
| libssh2_sftp_shutdown(filesystem->sftp_session); |
| |
| /* Free associated memory */ |
| free(filesystem->name); |
| free(filesystem); |
| |
| } |
| |