/* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #include "guac_sftp.h" #include "guac_ssh.h" #include #include #include #include #include #include #include #include #include /** * 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 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 0 && path[i-1] != '/') fullpath[i++] = '/'; break; } /* Copy character if not end of string */ fullpath[i] = c; } /* Append filename */ for (; idata; /* Attempt write */ if (libssh2_sftp_write(file, data, length) == length) { guac_client_log(client, GUAC_LOG_DEBUG, "%i bytes written", length); guac_protocol_send_ack(client->socket, stream, "SFTP: OK", GUAC_PROTOCOL_STATUS_SUCCESS); guac_socket_flush(client->socket); } /* Inform of any errors */ else { guac_client_log(client, GUAC_LOG_INFO, "Unable to write to file"); guac_protocol_send_ack(client->socket, stream, "SFTP: Write failed", GUAC_PROTOCOL_STATUS_SERVER_ERROR); guac_socket_flush(client->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 client * The client 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_client* client, 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_client_log(client, GUAC_LOG_DEBUG, "File closed"); guac_protocol_send_ack(client->socket, stream, "SFTP: OK", GUAC_PROTOCOL_STATUS_SUCCESS); guac_socket_flush(client->socket); } else { guac_client_log(client, GUAC_LOG_INFO, "Unable to close file"); guac_protocol_send_ack(client->socket, stream, "SFTP: Close failed", GUAC_PROTOCOL_STATUS_SERVER_ERROR); guac_socket_flush(client->socket); } return 0; } int guac_common_ssh_sftp_handle_file_stream(guac_object* filesystem, guac_stream* stream, char* mimetype, char* filename) { guac_common_ssh_sftp_data* sftp_data = (guac_common_ssh_sftp_data*) filesystem->data; guac_client* client = sftp_data->ssh_session->client; char fullpath[GUAC_COMMON_SSH_SFTP_MAX_PATH]; LIBSSH2_SFTP_HANDLE* file; /* Concatenate filename with path */ if (!guac_ssh_append_filename(fullpath, sftp_data->upload_path, filename)) { guac_client_log(client, GUAC_LOG_DEBUG, "Filename \"%s\" is invalid or resulting path is too long", filename); /* Abort transfer - invalid filename */ guac_protocol_send_ack(client->socket, stream, "SFTP: Illegal filename", GUAC_PROTOCOL_STATUS_CLIENT_BAD_REQUEST); guac_socket_flush(client->socket); return 0; } /* Open file via SFTP */ file = libssh2_sftp_open(sftp_data->sftp_session, fullpath, LIBSSH2_FXF_WRITE | LIBSSH2_FXF_CREAT | LIBSSH2_FXF_TRUNC, S_IRUSR | S_IWUSR); /* Inform of status */ if (file != NULL) { guac_client_log(client, GUAC_LOG_DEBUG, "File \"%s\" opened", fullpath); guac_protocol_send_ack(client->socket, stream, "SFTP: File opened", GUAC_PROTOCOL_STATUS_SUCCESS); guac_socket_flush(client->socket); } else { guac_client_log(client, GUAC_LOG_INFO, "Unable to open file \"%s\": %s", fullpath, libssh2_sftp_last_error(sftp_data->sftp_session)); guac_protocol_send_ack(client->socket, stream, "SFTP: Open failed", GUAC_PROTOCOL_STATUS_RESOURCE_NOT_FOUND); guac_socket_flush(client->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 client * The client 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_client* client, 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(client->socket, stream, buffer, bytes_read); guac_client_log(client, GUAC_LOG_DEBUG, "%i bytes sent to client", bytes_read); } /* If EOF, send end */ else if (bytes_read == 0) { guac_client_log(client, GUAC_LOG_DEBUG, "File sent"); guac_protocol_send_end(client->socket, stream); guac_client_free_stream(client, stream); } /* Otherwise, fail stream */ else { guac_client_log(client, GUAC_LOG_INFO, "Error reading file"); guac_protocol_send_end(client->socket, stream); guac_client_free_stream(client, stream); } guac_socket_flush(client->socket); } /* Otherwise, return stream to client */ else guac_client_free_stream(client, stream); return 0; } guac_stream* guac_common_ssh_sftp_download_file(guac_object* filesystem, char* filename) { guac_common_ssh_sftp_data* sftp_data = (guac_common_ssh_sftp_data*) filesystem->data; guac_client* client = sftp_data->ssh_session->client; guac_stream* stream; LIBSSH2_SFTP_HANDLE* file; /* Attempt to open file for reading */ file = libssh2_sftp_open(sftp_data->sftp_session, filename, LIBSSH2_FXF_READ, 0); if (file == NULL) { guac_client_log(client, GUAC_LOG_INFO, "Unable to read file \"%s\": %s", filename, libssh2_sftp_last_error(sftp_data->sftp_session)); return NULL; } /* Allocate stream */ stream = guac_client_alloc_stream(client); stream->ack_handler = guac_common_ssh_sftp_ack_handler; stream->data = file; /* Send stream start, strip name */ filename = basename(filename); guac_protocol_send_file(client->socket, stream, "application/octet-stream", filename); guac_socket_flush(client->socket); guac_client_log(client, GUAC_LOG_DEBUG, "Sending file \"%s\"", filename); return stream; } void guac_common_ssh_sftp_set_upload_path(guac_object* filesystem, const char* path) { guac_common_ssh_sftp_data* sftp_data = (guac_common_ssh_sftp_data*) filesystem->data; guac_client* client = sftp_data->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(sftp_data->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 client * The client 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_client* client, 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_data* sftp_data = list_state->sftp_data; LIBSSH2_SFTP* sftp = sftp_data->sftp_session; /* If unsuccessful, free stream and abort */ if (status != GUAC_PROTOCOL_STATUS_SUCCESS) { libssh2_sftp_closedir(list_state->directory); guac_client_free_stream(client, 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_client_log(client, 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_CLIENT_STREAM_INDEX_MIMETYPE; else mimetype = "application/octet-stream"; /* Write entry */ blob_written |= guac_common_json_write_property(client, 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(client, stream, &list_state->json_state); guac_common_json_flush(client, stream, &list_state->json_state); /* Clean up resources */ libssh2_sftp_closedir(list_state->directory); free(list_state); /* Signal of stream */ guac_protocol_send_end(client->socket, stream); guac_client_free_stream(client, stream); } guac_socket_flush(client->socket); return 0; } /** * 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 client * The client receiving 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_client* client, guac_object* object, char* name) { guac_common_ssh_sftp_data* sftp_data = (guac_common_ssh_sftp_data*) object->data; LIBSSH2_SFTP* sftp = sftp_data->sftp_session; LIBSSH2_SFTP_ATTRIBUTES attributes; /* Attempt to read file information */ if (libssh2_sftp_stat(sftp, name, &attributes)) { guac_client_log(client, GUAC_LOG_INFO, "Unable to read file \"%s\"", name); 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, name); if (dir == NULL) { guac_client_log(client, GUAC_LOG_INFO, "Unable to read directory \"%s\": %s", name, libssh2_sftp_last_error(sftp)); 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->sftp_data = sftp_data; strncpy(list_state->directory_name, name, sizeof(list_state->directory_name) - 1); /* Allocate stream for body */ guac_stream* stream = guac_client_alloc_stream(client); stream->ack_handler = guac_common_ssh_sftp_ls_ack_handler; stream->data = list_state; /* Init JSON object state */ guac_common_json_begin_object(client, stream, &list_state->json_state); /* Associate new stream with get request */ guac_protocol_send_body(client->socket, object, stream, GUAC_CLIENT_STREAM_INDEX_MIMETYPE, name); } /* Otherwise, send file contents */ else { /* Open as normal file */ LIBSSH2_SFTP_HANDLE* file = libssh2_sftp_open(sftp, name, LIBSSH2_FXF_READ, 0); if (file == NULL) { guac_client_log(client, GUAC_LOG_INFO, "Unable to read file \"%s\": %s", name, libssh2_sftp_last_error(sftp)); return 0; } /* Allocate stream for body */ guac_stream* stream = guac_client_alloc_stream(client); stream->ack_handler = guac_common_ssh_sftp_ack_handler; stream->data = file; /* Associate new stream with get request */ guac_protocol_send_body(client->socket, object, stream, "application/octet-stream", name); } guac_socket_flush(client->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 client * The client receiving the put message. * * @param object * The Guacamole protocol object associated with the put request itself. * * @param stream * The Guacamole protocol stream along which the client 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_client* client, guac_object* object, guac_stream* stream, char* mimetype, char* name) { guac_common_ssh_sftp_data* sftp_data = (guac_common_ssh_sftp_data*) object->data; LIBSSH2_SFTP* sftp = sftp_data->sftp_session; /* Open file via SFTP */ LIBSSH2_SFTP_HANDLE* file = libssh2_sftp_open(sftp, name, LIBSSH2_FXF_WRITE | LIBSSH2_FXF_CREAT | LIBSSH2_FXF_TRUNC, S_IRUSR | S_IWUSR); /* Acknowledge stream if successful */ if (file != NULL) { guac_client_log(client, GUAC_LOG_DEBUG, "File \"%s\" opened", name); guac_protocol_send_ack(client->socket, stream, "SFTP: File opened", GUAC_PROTOCOL_STATUS_SUCCESS); } /* Abort on failure */ else { guac_client_log(client, GUAC_LOG_INFO, "Unable to open file \"%s\": %s", name, libssh2_sftp_last_error(sftp)); guac_protocol_send_ack(client->socket, stream, "SFTP: Open failed", GUAC_PROTOCOL_STATUS_RESOURCE_NOT_FOUND); } /* 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(client->socket); return 0; } guac_object* guac_common_ssh_create_sftp_filesystem( guac_common_ssh_session* session, const char* name) { guac_client* client = session->client; /* Request SFTP */ LIBSSH2_SFTP* sftp_session = libssh2_sftp_init(session->session); if (sftp_session == NULL) { guac_client_abort(client, GUAC_PROTOCOL_STATUS_UPSTREAM_ERROR, "Unable to start SFTP session."); return NULL; } /* Allocate data for SFTP session */ guac_common_ssh_sftp_data* sftp_data = malloc(sizeof(guac_common_ssh_sftp_data)); /* Associate SSH session with SFTP data */ sftp_data->ssh_session = session; sftp_data->sftp_session = sftp_session; /* Initially upload files to current directory */ strcpy(sftp_data->upload_path, "."); /* Init filesystem */ guac_object* filesystem = guac_client_alloc_object(client); filesystem->get_handler = guac_common_ssh_sftp_get_handler; filesystem->put_handler = guac_common_ssh_sftp_put_handler; filesystem->data = sftp_data; /* Send filesystem to client */ guac_protocol_send_filesystem(client->socket, filesystem, "/"); guac_socket_flush(client->socket); /* Return allocated filesystem */ return filesystem; } void guac_common_ssh_destroy_sftp_filesystem(guac_object* filesystem) { guac_common_ssh_sftp_data* sftp_data = (guac_common_ssh_sftp_data*) filesystem->data; /* Shutdown SFTP session */ libssh2_sftp_shutdown(sftp_data->sftp_session); /* Clean up the SFTP filesystem object */ guac_client_free_object(sftp_data->ssh_session->client, filesystem); /* Disconnect SSH session corresponding to the SFTP session */ guac_common_ssh_destroy_session(sftp_data->ssh_session); }