diff --git a/src/common/Makefile.am b/src/common/Makefile.am index b42c7fec..785d0f22 100644 --- a/src/common/Makefile.am +++ b/src/common/Makefile.am @@ -1,5 +1,5 @@ # -# Copyright (C) 2013 Glyptodon LLC +# 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 @@ -31,6 +31,7 @@ noinst_HEADERS = \ guac_clipboard.h \ guac_dot_cursor.h \ guac_iconv.h \ + guac_json.h \ guac_list.h \ guac_pointer_cursor.h \ guac_rect.h \ @@ -42,6 +43,7 @@ libguac_common_la_SOURCES = \ guac_clipboard.c \ guac_dot_cursor.c \ guac_iconv.c \ + guac_json.c \ guac_list.c \ guac_pointer_cursor.c \ guac_rect.c \ diff --git a/src/common/guac_json.c b/src/common/guac_json.c new file mode 100644 index 00000000..2125906c --- /dev/null +++ b/src/common/guac_json.c @@ -0,0 +1,183 @@ +/* + * 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 "config.h" + +#include "guac_json.h" + +#include +#include +#include + +#include +#include +#include +#include + +void guac_common_json_flush(guac_client* client, guac_stream* stream, + guac_common_json_state* json_state) { + + /* If JSON buffer is non-empty, write contents to blob and reset */ + if (json_state->size > 0) { + guac_protocol_send_blob(client->socket, stream, + json_state->buffer, json_state->size); + + /* Reset JSON buffer size */ + json_state->size = 0; + + } + +} + +bool guac_common_json_write(guac_client* client, guac_stream* stream, + guac_common_json_state* json_state, const char* buffer, int length) { + + bool blob_written = false; + + /* + * Append to and flush the JSON buffer as necessary to write the given + * data + */ + while (length > 0) { + + /* Ensure provided data does not exceed size of buffer */ + int blob_length = length; + if (blob_length > sizeof(json_state->buffer)) + blob_length = sizeof(json_state->buffer); + + /* Flush if more room is needed */ + if (json_state->size + blob_length > sizeof(json_state->buffer)) { + guac_common_json_flush(client, stream, json_state); + blob_written = true; + } + + /* Append data to JSON buffer */ + memcpy(json_state->buffer + json_state->size, + buffer, blob_length); + + json_state->size += blob_length; + + /* Advance to next blob of data */ + buffer += blob_length; + length -= blob_length; + + } + + return blob_written; + +} + +bool guac_common_json_write_string(guac_client* client, + guac_stream* stream, guac_common_json_state* json_state, + const char* str) { + + bool blob_written = false; + + /* Write starting quote */ + blob_written |= guac_common_json_write(client, stream, + json_state, "\"", 1); + + /* Write given string, escaping as necessary */ + const char* current = str; + for (; *current != '\0'; current++) { + + /* Escape all quotes */ + if (*current == '"') { + + /* Write any string content up to current character */ + if (current != str) + blob_written |= guac_common_json_write(client, stream, + json_state, str, current - str); + + /* Escape the quote that was just read */ + blob_written |= guac_common_json_write(client, stream, + json_state, "\\", 1); + + /* Reset string */ + str = current; + + } + + } + + /* Write any remaining string content */ + if (current != str) + blob_written |= guac_common_json_write(client, stream, + json_state, str, current - str); + + /* Write ending quote */ + blob_written |= guac_common_json_write(client, stream, + json_state, "\"", 1); + + return blob_written; + +} + +bool guac_common_json_write_property(guac_client* client, guac_stream* stream, + guac_common_json_state* json_state, const char* name, + const char* value) { + + bool blob_written = false; + + /* Write leading comma if not first property */ + if (json_state->properties_written != 0) + blob_written |= guac_common_json_write(client, stream, + json_state, ",", 1); + + /* Write property name */ + blob_written |= guac_common_json_write_string(client, stream, + json_state, name); + + /* Separate name from value with colon */ + blob_written |= guac_common_json_write(client, stream, + json_state, ":", 1); + + /* Write property value */ + blob_written |= guac_common_json_write_string(client, stream, + json_state, value); + + json_state->properties_written++; + + return blob_written; + +} + +void guac_common_json_begin_object(guac_client* client, guac_stream* stream, + guac_common_json_state* json_state) { + + /* Init JSON state */ + json_state->size = 0; + json_state->properties_written = 0; + + /* Write leading brace - no blob can possibly be written by this */ + assert(!guac_common_json_write(client, stream, json_state, "{", 1)); + +} + +bool guac_common_json_end_object(guac_client* client, guac_stream* stream, + guac_common_json_state* json_state) { + + /* Write final brace of JSON object */ + return guac_common_json_write(client, stream, json_state, "}", 1); + +} + diff --git a/src/common/guac_json.h b/src/common/guac_json.h new file mode 100644 index 00000000..4bab9684 --- /dev/null +++ b/src/common/guac_json.h @@ -0,0 +1,201 @@ +/* + * 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. + */ + +#ifndef GUAC_COMMON_JSON_H +#define GUAC_COMMON_JSON_H + +#include "config.h" + +#include + +#include +#include + +/** + * The current streaming state of an arbitrary JSON object, consisting of + * any number of property name/value pairs. + */ +typedef struct guac_common_json_state { + + /** + * Buffer of partial JSON data. The individual blobs which make up the JSON + * body of the object being sent over the Guacamole protocol will be + * built here. + */ + char buffer[4096]; + + /** + * The number of bytes currently used within the JSON buffer. + */ + int size; + + /** + * The number of property name/value pairs written to the JSON object thus + * far. + */ + int properties_written; + +} guac_common_json_state; + +/** + * Given a stream, the client to which it belongs, and the current stream state + * of a JSON object, flushes the contents of the JSON buffer to a blob + * instruction. Note that this will flush the JSON buffer only, and will not + * necessarily flush the underlying guac_socket of the client. + * + * @param client + * The client to which the data will be flushed. + * + * @param stream + * The stream through which the flushed data should be sent as a blob. + * + * @param json_state + * The state object whose buffer should be flushed. + */ +void guac_common_json_flush(guac_client* client, guac_stream* stream, + guac_common_json_state* json_state); + +/** + * Given a stream, the client to which it belongs, and the current stream state + * of a JSON object, writes the contents of the given buffer to the JSON buffer + * of the stream state, flushing as necessary. + * + * @param client + * The client to which the data will be flushed as necessary. + * + * @param stream + * The stream through which the flushed data should be sent as a blob, if + * data must be flushed at all. + * + * @param json_state + * The state object containing the JSON buffer to which the given buffer + * should be written. + * + * @param buffer + * The buffer to write. + * + * @param length + * The number of bytes in the buffer. + * + * @return + * true if at least one blob was written, false otherwise. + */ +bool guac_common_json_write(guac_client* client, guac_stream* stream, + guac_common_json_state* json_state, const char* buffer, int length); + +/** + * Given a stream, the client to which it belongs, and the current stream state + * of a JSON object state, writes the given string as a proper JSON string, + * including starting and ending quotes. The contents of the string will be + * escaped as necessary. + * + * @param client + * The client to which the data will be flushed as necessary. + * + * @param stream + * The stream through which the flushed data should be sent as a blob, if + * data must be flushed at all. + * + * @param json_state + * The state object containing the JSON buffer to which the given string + * should be written as a JSON name/value pair. + * + * @param str + * The string to write. + * + * @return + * true if at least one blob was written, false otherwise. + */ +bool guac_common_json_write_string(guac_client* client, + guac_stream* stream, guac_common_json_state* json_state, + const char* str); + +/** + * Given a stream, the client to which it belongs, and the current stream state + * of a JSON object, writes the given JSON property name/value pair. The + * name and value will be written as proper JSON strings separated by a colon. + * + * @param client + * The client to which the data will be flushed as necessary. + * + * @param stream + * The stream through which the flushed data should be sent as a blob, if + * data must be flushed at all. + * + * @param json_state + * The state object containing the JSON buffer to which the given strings + * should be written as a JSON name/value pair. + * + * @param name + * The name of the property to write. + * + * @param value + * The value of the property to write. + * + * @return + * true if at least one blob was written, false otherwise. + */ +bool guac_common_json_write_property(guac_client* client, guac_stream* stream, + guac_common_json_state* json_state, const char* name, + const char* value); + +/** + * Given a stream, the client to which it belongs, and the current stream state + * of a JSON object, initializes the state for writing a new JSON object. Note + * that although the client and stream must be provided, no instruction or + * blobs will be written due to any call to this function. + * + * @param client + * The client associated with the given stream. + * + * @param stream + * The stream associated with the JSON object being written. + * + * @param json_state + * The state object to initialize. + */ +void guac_common_json_begin_object(guac_client* client, guac_stream* stream, + guac_common_json_state* json_state); + +/** + * Given a stream, the client to which it belongs, and the current stream state + * of a JSON object, completes writing that JSON object by writing the final + * terminating brace. This function must only be called following a + * corresponding call to guac_common_json_begin_object(). + * + * @param client + * The client associated with the given stream. + * + * @param stream + * The stream associated with the JSON object being written. + * + * @param json_state + * The state object whose in-progress JSON object should be terminated. + * + * @return + * true if at least one blob was written, false otherwise. + */ +bool guac_common_json_end_object(guac_client* client, guac_stream* stream, + guac_common_json_state* json_state); + +#endif + diff --git a/src/protocols/ssh/sftp.c b/src/protocols/ssh/sftp.c index 75b692a8..c7fa463b 100644 --- a/src/protocols/ssh/sftp.c +++ b/src/protocols/ssh/sftp.c @@ -22,6 +22,8 @@ #include "config.h" +#include "guac_json.h" + #include "client.h" #include "sftp.h" @@ -340,222 +342,6 @@ guac_object* guac_sftp_expose_filesystem(guac_client* client) { } -/** - * Given a stream, the client to which it belongs, and the current directory - * list state, flushes the contents of the JSON buffer to a blob instruction. - * Note that this will flush the JSON buffer only, and will not necessarily - * flush the underlying guac_socket of the client. - * - * @param client - * The client to which the data will be flushed. - * - * @param stream - * The stream through which the flushed data should be sent as a blob. - * - * @param list_state - * The directory list state. - */ -static void guac_sftp_ls_flush_json(guac_client* client, guac_stream* stream, - guac_sftp_ls_state* list_state) { - - /* If JSON buffer is non-empty, write contents to blob and reset */ - if (list_state->json_size > 0) { - guac_protocol_send_blob(client->socket, stream, - list_state->json_buffer, list_state->json_size); - - /* Reset JSON buffer size */ - list_state->json_size = 0; - - } - -} - -/** - * Given a stream, the client to which it belongs, and the current directory - * list state, writes the contents of the given buffer to the JSON buffer of - * the directory list state, flushing as necessary. - * - * @param client - * The client to which the data will be flushed as necessary. - * - * @param stream - * The stream through which the flushed data should be sent as a blob, if - * data must be flushed at all. - * - * @param list_state - * The directory list state containing the JSON buffer to which the given - * buffer should be written. - * - * @param buffer - * The buffer to write. - * - * @param length - * The number of bytes in the buffer. - * - * @return - * true if at least one blob was written, false otherwise. - */ -static bool guac_sftp_ls_write_json(guac_client* client, guac_stream* stream, - guac_sftp_ls_state* list_state, const char* buffer, int length) { - - bool blob_written = false; - - /* - * Append to and flush the JSON buffer as necessary to write the given - * data - */ - while (length > 0) { - - /* Ensure provided data does not exceed size of buffer */ - int blob_length = length; - if (blob_length > sizeof(list_state->json_buffer)) - blob_length = sizeof(list_state->json_buffer); - - /* Flush if more room is needed */ - if (list_state->json_size + blob_length > sizeof(list_state->json_buffer)) { - guac_sftp_ls_flush_json(client, stream, list_state); - blob_written = true; - } - - /* Append data to JSON buffer */ - memcpy(list_state->json_buffer + list_state->json_size, - buffer, blob_length); - - list_state->json_size += blob_length; - - /* Advance to next blob of data */ - buffer += blob_length; - length -= blob_length; - - } - - return blob_written; - -} - -/** - * Given a stream, the client to which it belongs, and the current directory - * list state, writes the given string as a proper JSON string, including - * starting and ending quotes. The contents of the string will be escaped as - * necessary. - * - * @param client - * The client to which the data will be flushed as necessary. - * - * @param stream - * The stream through which the flushed data should be sent as a blob, if - * data must be flushed at all. - * - * @param list_state - * The directory list state containing the JSON buffer to which the given - * string should be written as a proper JSON string. - * - * @param str - * The string to write. - * - * @return - * true if at least one blob was written, false otherwise. - */ -static bool guac_sftp_ls_write_json_string(guac_client* client, - guac_stream* stream, guac_sftp_ls_state* list_state, - const char* str) { - - bool blob_written = false; - - /* Write starting quote */ - blob_written |= guac_sftp_ls_write_json(client, stream, - list_state, "\"", 1); - - /* Write given string, escaping as necessary */ - const char* current = str; - for (; *current != '\0'; current++) { - - /* Escape all quotes */ - if (*current == '"') { - - /* Write any string content up to current character */ - if (current != str) - blob_written |= guac_sftp_ls_write_json(client, stream, - list_state, str, current - str); - - /* Escape the quote that was just read */ - blob_written |= guac_sftp_ls_write_json(client, stream, - list_state, "\\", 1); - - /* Reset string */ - str = current; - - } - - } - - /* Write any remaining string content */ - if (current != str) - blob_written |= guac_sftp_ls_write_json(client, stream, - list_state, str, current - str); - - /* Write ending quote */ - blob_written |= guac_sftp_ls_write_json(client, stream, - list_state, "\"", 1); - - return blob_written; - -} - -/** - * Given a stream, the client to which it belongs, and the current directory - * list state, writes the given directory entry as a JSON name/value pair. The - * name and value will be written as proper JSON strings separated by a colon. - * - * @param client - * The client to which the data will be flushed as necessary. - * - * @param stream - * The stream through which the flushed data should be sent as a blob, if - * data must be flushed at all. - * - * @param list_state - * The directory list state containing the JSON buffer to which the given - * directory should be written as a JSON name/value pair. - * - * @param mimetype - * The mimetype of the directory entry to write. - * - * @param filename - * The filename of the directory entry to write. - * - * @return - * true if at least one blob was written, false otherwise. - */ -static bool guac_sftp_ls_write_entry(guac_client* client, guac_stream* stream, - guac_sftp_ls_state* list_state, const char* mimetype, - const char* filename) { - - bool blob_written = false; - - /* Write leading comma if not first entry */ - if (list_state->entries_written != 0) - blob_written |= guac_sftp_ls_write_json(client, stream, - list_state, ",", 1); - - /* Write filename */ - blob_written |= guac_sftp_ls_write_json_string(client, stream, - list_state, filename); - - /* Separate filename from mimetype with colon */ - blob_written |= guac_sftp_ls_write_json(client, stream, - list_state, ":", 1); - - /* Write mimetype */ - blob_written |= guac_sftp_ls_write_json_string(client, stream, - list_state, mimetype); - - list_state->entries_written++; - - return blob_written; - -} - int guac_sftp_ls_ack_handler(guac_client* client, guac_stream* stream, char* message, guac_protocol_status status) { @@ -610,28 +396,18 @@ int guac_sftp_ls_ack_handler(guac_client* client, guac_stream* stream, else mimetype = "application/octet-stream"; - /* Write leading brace if just starting */ - if (list_state->entries_written == 0) - blob_written |= guac_sftp_ls_write_json(client, stream, - list_state, "{", 1); - /* Write entry */ - blob_written |= guac_sftp_ls_write_entry(client, stream, - list_state, mimetype, absolute_path); + 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) { - /* Ensure leading brace is written */ - if (list_state->entries_written == 0) - blob_written |= guac_sftp_ls_write_json(client, stream, - list_state, "{", 1); - - /* Write end of JSON */ - guac_sftp_ls_write_json(client, stream, list_state, "}", 1); - guac_sftp_ls_flush_json(client, stream, list_state); + /* 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); @@ -681,14 +457,14 @@ int guac_sftp_get_handler(guac_client* client, guac_object* object, strncpy(list_state->directory_name, name, sizeof(list_state->directory_name)); - list_state->json_size = 0; - list_state->entries_written = 0; - /* Allocate stream for body */ guac_stream* stream = guac_client_alloc_stream(client); stream->ack_handler = guac_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); diff --git a/src/protocols/ssh/sftp.h b/src/protocols/ssh/sftp.h index 73cc9f4e..ecb96714 100644 --- a/src/protocols/ssh/sftp.h +++ b/src/protocols/ssh/sftp.h @@ -26,6 +26,8 @@ #include "config.h" +#include "guac_json.h" + #include #include @@ -56,21 +58,9 @@ typedef struct guac_sftp_ls_state { char directory_name[GUAC_SFTP_MAX_PATH]; /** - * Buffer of partial JSON data. The individual blobs which make up the JSON - * body of the directory listing sent over the Guacamole protocol will be - * built here. + * The current state of the JSON directory object being written. */ - char json_buffer[4096]; - - /** - * The number of bytes currently used within the JSON buffer. - */ - int json_size; - - /** - * The number of entries written to the JSON thus far. - */ - int entries_written; + guac_common_json_state json_state; } guac_sftp_ls_state;