diff --git a/.gitignore b/.gitignore index dec0cd10..e716ee0f 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,10 @@ *.gcov *.gcno +# Test suite output +*.log +*.trs + # Backup files *~ @@ -28,27 +32,16 @@ Makefile Makefile.in aclocal.m4 autom4te.cache/ +build-aux/ +libtool m4/* !README -compile -config.guess config.h config.h.in config.log config.status -config.sub configure -depcomp -install-sh -libtool -ltmain.sh -missing stamp-h1 -test-driver - -# Test binaries -tests/test_* -!tests/test_*.[ch] # Generated docs doc/doxygen-output diff --git a/Dockerfile b/Dockerfile index 4d463a38..094842cc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -49,6 +49,7 @@ ARG BUILD_DEPENDENCIES=" \ libtelnet-dev \ libtool \ libvncserver-dev \ + libwebsockets-dev \ libwebp-dev \ make" @@ -74,7 +75,7 @@ RUN ${PREFIX_DIR}/bin/list-dependencies.sh \ > ${PREFIX_DIR}/DEPENDENCIES # Use same Debian as the base for the runtime image -FROM debian:${DEBIAN_VERSION} +FROM debian:${DEBIAN_VERSION}-slim # Base directory for installed build artifacts. # Due to limitations of the Docker image build process, this value is @@ -89,6 +90,7 @@ ENV LD_LIBRARY_PATH=${PREFIX_DIR}/lib ENV GUACD_LOG_LEVEL=info ARG RUNTIME_DEPENDENCIES=" \ + ca-certificates \ ghostscript \ libfreerdp-plugins-standard \ fonts-liberation \ diff --git a/Makefile.am b/Makefile.am index e9233760..c75735c1 100644 --- a/Makefile.am +++ b/Makefile.am @@ -16,29 +16,34 @@ # specific language governing permissions and limitations # under the License. # +# NOTE: Parts of this file (Makefile.am) are automatically transcluded verbatim +# into Makefile.in. Though the build system (GNU Autotools) automatically adds +# its own license boilerplate to the generated Makefile.in, that boilerplate +# does not apply to the transcluded portions of Makefile.am which are licensed +# to you by the ASF under the Apache License, Version 2.0, as described above. +# ACLOCAL_AMFLAGS = -I m4 # Subprojects -DIST_SUBDIRS = \ - src/libguac \ - src/common \ - src/common-ssh \ - src/terminal \ - src/guacd \ - src/guacenc \ - src/guaclog \ - src/pulse \ - src/protocols/rdp \ - src/protocols/ssh \ - src/protocols/telnet \ - src/protocols/vnc \ - tests +DIST_SUBDIRS = \ + src/libguac \ + src/common \ + src/common-ssh \ + src/terminal \ + src/guacd \ + src/guacenc \ + src/guaclog \ + src/pulse \ + src/protocols/kubernetes \ + src/protocols/rdp \ + src/protocols/ssh \ + src/protocols/telnet \ + src/protocols/vnc SUBDIRS = \ src/libguac \ - src/common \ - tests + src/common if ENABLE_COMMON_SSH SUBDIRS += src/common-ssh @@ -52,6 +57,10 @@ if ENABLE_PULSE SUBDIRS += src/pulse endif +if ENABLE_KUBERNETES +SUBDIRS += src/protocols/kubernetes +endif + if ENABLE_RDP SUBDIRS += src/protocols/rdp endif @@ -80,13 +89,14 @@ if ENABLE_GUACLOG SUBDIRS += src/guaclog endif -EXTRA_DIST = \ - .dockerignore \ - CONTRIBUTING \ - Dockerfile \ - LICENSE \ - NOTICE \ - bin/guacctl \ - doc/Doxyfile.in \ - src/guacd-docker +EXTRA_DIST = \ + .dockerignore \ + CONTRIBUTING \ + Dockerfile \ + LICENSE \ + NOTICE \ + bin/guacctl \ + doc/Doxyfile.in \ + src/guacd-docker \ + util/generate-test-runner.pl diff --git a/NOTICE b/NOTICE index 97e61306..39de3ece 100644 --- a/NOTICE +++ b/NOTICE @@ -1,5 +1,5 @@ Apache Guacamole -Copyright 2018 The Apache Software Foundation +Copyright 2019 The Apache Software Foundation This product includes software developed at The Apache Software Foundation (http://www.apache.org/). diff --git a/README-unit-testing.md b/README-unit-testing.md new file mode 100644 index 00000000..8b4e7768 --- /dev/null +++ b/README-unit-testing.md @@ -0,0 +1,89 @@ + +Unit testing and guacamole-server +================================= + +Unit tests within guacamole-server are implemented using the following: + +* automake, which allows arbitrary tests to be declared within `Makefile.am` + and uses `make check` to run those tests. +* CUnit (libcunit), a unit testing framework. +* `util/generate-test-runner.pl`, a Perl script which generates a test runner + written in C which leverages CUnit, running the unit tests declared in each + of the given `.c` files. The generated test runner produces output in [TAP + format](https://testanything.org/) which is consumed by the TAP test driver + provided by automake. + +Writing unit tests +------------------ + +All unit tests should be within reasonably-isolated C source files, with each +logical test having its own function of the form: + + void test_SUITENAME__TESTNAME() { + ... + } + +where `TESTNAME` is the arbitrary name of the test and `SUITENAME` is the +arbitrary name of the test suite that this test belongs to. + +**This naming convention is required by `generate-test-runner.pl`.** Absolutely +all tests MUST follow the above convention if they are to be picked up and +organized by the test runner generation script. Functions which are not tests +MUST NOT follow the above convention so that they are _not_ picked up mistakenly +by the test runner generator as if they were tests. + +The `Makefile.am` for a subproject which contains such tests is typically +modified to contain a sections like the following: + + # + # Unit tests for myproj + # + + check_PROGRAMS = test_myproj + TESTS = $(check_PROGRAMS) + + test_myproj_SOURCES = \ + ...all source files... + + test_myproj_CFLAGS = \ + -Werror -Wall -pedantic \ + ...other flags... + + test_myproj_LDADD = \ + ...libraries... + + # + # Autogenerate test runner + # + + GEN_RUNNER = $(top_srcdir)/util/generate-test-runner.pl + CLEANFILES = _generated_runner.c + + _generated_runner.c: $(test_myproj_SOURCES) + $(AM_V_GEN) $(GEN_RUNNER) $(test_myproj_SOURCES) > $@ + + nodist_test_libguac_SOURCES = \ + _generated_runner.c + + # Use automake's TAP test driver for running any tests + LOG_DRIVER = \ + env AM_TAP_AWK='$(AWK)' \ + $(SHELL) $(top_srcdir)/build-aux/tap-driver.sh + +The above declares ... + +* ... that a binary, `test_myproj` should be built from the given sources. + Note that `test_myproj_SOURCES` contains only the source which was actually + written by hand while `nodist_test_myproj_SOURCES` contains only the source + which was generated by `generate-test-runner.pl`. +* ... that this `test_myproj` binary should be run to test this project when + `make check` is run, and that automake's TAP driver should be used to + consume its output. +* ... that the `_generated_runner.c` source file is generated dynamically + (through running `generate-test-runner.pl` on all non-generated test source) + and should not be distributed as part of the source archive. + +With tests following the above naming convention in place, and with the +necessary changes made to the applicable `Makefile.am`, all tests will be +run automatically when `make check` is run. + diff --git a/bin/guacctl b/bin/guacctl index 2d6d2637..3d262093 100755 --- a/bin/guacctl +++ b/bin/guacctl @@ -117,7 +117,7 @@ error() { ## usage() { cat >&2 <]) +AC_CHECK_DECL([strlcpy], + [AC_DEFINE([HAVE_STRLCPY],, + [Whether strlcpy() is defined])],, + [#include ]) + +AC_CHECK_DECL([strlcat], + [AC_DEFINE([HAVE_STRLCAT],, + [Whether strlcat() is defined])],, + [#include ]) + # Typedefs AC_TYPE_SIZE_T AC_TYPE_SSIZE_T @@ -136,6 +151,10 @@ AC_SUBST([PULSE_INCLUDE], '-I$(top_srcdir)/src/pulse') AC_SUBST([COMMON_SSH_LTLIB], '$(top_builddir)/src/common-ssh/libguac_common_ssh.la') AC_SUBST([COMMON_SSH_INCLUDE], '-I$(top_srcdir)/src/common-ssh') +# RDP support +AC_SUBST([LIBGUAC_CLIENT_RDP_LTLIB], '$(top_builddir)/src/protocols/rdp/libguac-client-rdp.la') +AC_SUBST([LIBGUAC_CLIENT_RDP_INCLUDE], '-I$(top_srcdir)/src/protocols/rdp') + # Terminal emulator AC_SUBST([TERMINAL_LTLIB], '$(top_builddir)/src/terminal/libguac_terminal.la') AC_SUBST([TERMINAL_INCLUDE], '-I$(top_srcdir)/src/terminal $(PANGO_CFLAGS) $(PANGOCAIRO_CFLAGS) $(COMMON_INCLUDE)') @@ -508,12 +527,40 @@ then fi +# +# TLS Locking Support within libVNCServer +# + +if test "x${have_libvncserver}" = "xyes" +then + + have_vnc_tls_locking=yes + AC_CHECK_MEMBERS([rfbClient.LockWriteToTLS, rfbClient.UnlockWriteToTLS], + [], [have_vnc_tls_locking=no], + [[#include ]]) + + if test "x${have_vnc_tls_locking}" = "xno" + then + AC_MSG_WARN([ + -------------------------------------------- + This version of libvncclient lacks support + for TLS locking. VNC connections that use + TLS may experience instability as documented + in GUACAMOLE-414]) + else + AC_DEFINE([ENABLE_VNC_TLS_LOCKING],, + [Whether support for TLS locking within VNC is enabled.]) + fi + +fi + # # FreeRDP # have_freerdp=disabled RDP_LIBS= +WINPR_LIBS= AC_ARG_WITH([rdp], [AS_HELP_STRING([--with-rdp], [support RDP @<:@default=check@:>@])], @@ -715,6 +762,21 @@ then [#include ])]) fi +# Find location of Stream_New and Stream_free +if test "x${have_freerdp}" = "xyes" -a "x${have_winpr}" = "xyes" +then + AC_CHECK_LIB([winpr], [Stream_New, Stream_Free], + [WINPR_LIBS="$WINPR_LIBS -lwinpr"], + [AC_CHECK_LIB([winpr-utils], [Stream_New, Stream_Free], + [WINPR_LIBS="$WINPR_LIBS -lwinpr-utils"], + [AC_MSG_WARN([ + ------------------------------------------ + Unable to locate stream functions in winpr + libraries. RDP will be disabled. + ------------------------------------------]) + have_freerdp=no])]) +fi + # Check for types in WinPR if test "x${have_freerdp}" = "xyes" then @@ -1023,6 +1085,7 @@ AM_CONDITIONAL([ENABLE_WINPR], [test "x${have_winpr}" = "xyes"]) AM_CONDITIONAL([ENABLE_RDP], [test "x${have_freerdp}" = "xyes"]) AC_SUBST(RDP_LIBS) +AC_SUBST(WINPR_LIBS) # # libssh2 @@ -1172,6 +1235,88 @@ fi AM_CONDITIONAL([ENABLE_WEBP], [test "x${have_webp}" = "xyes"]) AC_SUBST(WEBP_LIBS) +# +# libwebsockets +# + +have_libwebsockets=disabled +WEBSOCKETS_LIBS= +AC_ARG_WITH([websockets], + [AS_HELP_STRING([--with-websockets], + [support WebSockets @<:@default=check@:>@])], + [], + [with_websockets=check]) + +if test "x$with_websockets" != "xno" +then + have_libwebsockets=yes + AC_CHECK_LIB([websockets], + [lws_create_context], + [WEBSOCKETS_LIBS="$WEBSOCKETS_LIBS -lwebsockets"], + [AC_MSG_WARN([ + -------------------------------------------- + Unable to find libwebsockets. + Support for Kubernetes will be disabled. + --------------------------------------------]) + have_libwebsockets=no]) +fi + +if test "x$with_websockets" != "xno" +then + + # Check for client-specific closed event, which must be used in favor of + # the generic closed event if libwebsockets is recent enough to provide + # this + AC_CHECK_DECL([LWS_CALLBACK_CLIENT_CLOSED], + [AC_DEFINE([HAVE_LWS_CALLBACK_CLIENT_CLOSED],, + [Whether LWS_CALLBACK_CLIENT_CLOSED is defined])],, + [#include ]) + + # Older versions of libwebsockets may not define a flag for requesting + # global initialization of OpenSSL, instead performing that initialization + # by default + AC_CHECK_DECL([LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT], + [AC_DEFINE([HAVE_LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT],, + [Whether LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT is defined])],, + [#include ]) + + # Older versions of libwebsockets do not define special macros for SSL + # connection flags, instead relying on documented integer values + AC_CHECK_DECL([LCCSCF_USE_SSL], + [AC_DEFINE([HAVE_LCCSCF_USE_SSL],, + [Whether LCCSCF_USE_SSL is defined])],, + [#include ]) + + # Older versions of libwebsockets do not define a dummy callback which + # must be invoked after the main event callback is invoked; the main event + # callback must instead manually return zero + AC_CHECK_DECL([lws_callback_http_dummy], + [AC_DEFINE([HAVE_LWS_CALLBACK_HTTP_DUMMY],, + [Whether lws_callback_http_dummy() is defined])],, + [#include ]) + +fi + +AM_CONDITIONAL([ENABLE_WEBSOCKETS], + [test "x${have_libwebsockets}" = "xyes"]) + +AC_SUBST(WEBSOCKETS_LIBS) + +# +# Kubernetes +# + +AC_ARG_ENABLE([kubernetes], + [AS_HELP_STRING([--disable-kubernetes], + [do not build support for attaching to Kubernetes pods])], + [], + [enable_kubernetes=yes]) + +AM_CONDITIONAL([ENABLE_KUBERNETES], [test "x${enable_kubernetes}" = "xyes" \ + -a "x${have_libwebsockets}" = "xyes" \ + -a "x${have_ssl}" = "xyes" \ + -a "x${have_terminal}" = "xyes"]) + # # guacd # @@ -1217,11 +1362,13 @@ AM_CONDITIONAL([ENABLE_GUACLOG], [test "x${enable_guaclog}" = "xyes"]) AC_CONFIG_FILES([Makefile doc/Doxyfile - tests/Makefile src/common/Makefile + src/common/tests/Makefile src/common-ssh/Makefile + src/common-ssh/tests/Makefile src/terminal/Makefile src/libguac/Makefile + src/libguac/tests/Makefile src/guacd/Makefile src/guacd/man/guacd.8 src/guacd/man/guacd.conf.5 @@ -1230,7 +1377,9 @@ AC_CONFIG_FILES([Makefile src/guaclog/Makefile src/guaclog/man/guaclog.1 src/pulse/Makefile + src/protocols/kubernetes/Makefile src/protocols/rdp/Makefile + src/protocols/rdp/tests/Makefile src/protocols/ssh/Makefile src/protocols/telnet/Makefile src/protocols/vnc/Makefile]) @@ -1240,10 +1389,11 @@ AC_OUTPUT # Protocol build status # -AM_COND_IF([ENABLE_RDP], [build_rdp=yes], [build_rdp=no]) -AM_COND_IF([ENABLE_SSH], [build_ssh=yes], [build_ssh=no]) -AM_COND_IF([ENABLE_TELNET], [build_telnet=yes], [build_telnet=no]) -AM_COND_IF([ENABLE_VNC], [build_vnc=yes], [build_vnc=no]) +AM_COND_IF([ENABLE_KUBERNETES], [build_kubernetes=yes], [build_kubernetes=no]) +AM_COND_IF([ENABLE_RDP], [build_rdp=yes], [build_rdp=no]) +AM_COND_IF([ENABLE_SSH], [build_ssh=yes], [build_ssh=no]) +AM_COND_IF([ENABLE_TELNET], [build_telnet=yes], [build_telnet=no]) +AM_COND_IF([ENABLE_VNC], [build_vnc=yes], [build_vnc=no]) # # Service / tool build status @@ -1287,15 +1437,17 @@ $PACKAGE_NAME version $PACKAGE_VERSION libVNCServer ........ ${have_libvncserver} libvorbis ........... ${have_vorbis} libpulse ............ ${have_pulse} + libwebsockets ....... ${have_libwebsockets} libwebp ............. ${have_webp} wsock32 ............. ${have_winsock} Protocol support: - RDP ....... ${build_rdp} - SSH ....... ${build_ssh} - Telnet .... ${build_telnet} - VNC ....... ${build_vnc} + Kubernetes .... ${build_kubernetes} + RDP ........... ${build_rdp} + SSH ........... ${build_ssh} + Telnet ........ ${build_telnet} + VNC ........... ${build_vnc} Services / tools: diff --git a/src/common-ssh/.gitignore b/src/common-ssh/.gitignore new file mode 100644 index 00000000..f18cde53 --- /dev/null +++ b/src/common-ssh/.gitignore @@ -0,0 +1,5 @@ + +# Auto-generated test runner and binary +_generated_runner.c +test_common_ssh + diff --git a/src/common-ssh/Makefile.am b/src/common-ssh/Makefile.am index b839ab00..8402e5b0 100644 --- a/src/common-ssh/Makefile.am +++ b/src/common-ssh/Makefile.am @@ -16,11 +16,18 @@ # specific language governing permissions and limitations # under the License. # +# NOTE: Parts of this file (Makefile.am) are automatically transcluded verbatim +# into Makefile.in. Though the build system (GNU Autotools) automatically adds +# its own license boilerplate to the generated Makefile.in, that boilerplate +# does not apply to the transcluded portions of Makefile.am which are licensed +# to you by the ASF under the Apache License, Version 2.0, as described above. +# AUTOMAKE_OPTIONS = foreign ACLOCAL_AMFLAGS = -I m4 noinst_LTLIBRARIES = libguac_common_ssh.la +SUBDIRS = . tests libguac_common_ssh_la_SOURCES = \ buffer.c \ diff --git a/src/common-ssh/common-ssh/sftp.h b/src/common-ssh/common-ssh/sftp.h index 0ec2d12b..eafdf21f 100644 --- a/src/common-ssh/common-ssh/sftp.h +++ b/src/common-ssh/common-ssh/sftp.h @@ -255,5 +255,30 @@ int guac_common_ssh_sftp_handle_file_stream( void guac_common_ssh_sftp_set_upload_path( guac_common_ssh_sftp_filesystem* filesystem, const char* path); +/** + * 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. + */ +int guac_common_ssh_sftp_normalize_path(char* fullpath, + const char* path); + #endif diff --git a/src/common-ssh/common-ssh/ssh.h b/src/common-ssh/common-ssh/ssh.h index 672e7767..346d8abc 100644 --- a/src/common-ssh/common-ssh/ssh.h +++ b/src/common-ssh/common-ssh/ssh.h @@ -25,6 +25,23 @@ #include #include +/** + * Handler for retrieving additional credentials. + * + * @param client + * The Guacamole Client associated with this need for additional + * credentials. + * + * @param cred_name + * The name of the credential being requested, which will be shared + * with the client in order to generate a meaningful prompt. + * + * @return + * A newly-allocated string containing the credentials provided by + * the user, which must be freed by a call to free(). + */ +typedef char* guac_ssh_credential_handler(guac_client* client, char* cred_name); + /** * An SSH session, backed by libssh2 and associated with a particular * Guacamole client. @@ -50,6 +67,11 @@ typedef struct guac_common_ssh_session { * The file descriptor of the socket being used for the SSH connection. */ int fd; + + /** + * Callback function to retrieve credentials. + */ + guac_ssh_credential_handler* credential_handler; } guac_common_ssh_session; @@ -92,14 +114,32 @@ void guac_common_ssh_uninit(); * * @param user * The user to authenticate as, once connected. + * + * @param keepalive + * How frequently the connection should send keepalive packets, in + * seconds. Zero disables keepalive packets, and 2 is the minimum + * configurable value. + * + * @param host_key + * The known public host key of the server, as provided by the client. If + * provided the identity of the server will be checked against this key, + * and a mis-match between this and the server identity will cause the + * connection to fail. If not provided, no checks will be done and the + * connection will proceed. + * + * @param credential_handler + * The handler function for retrieving additional credentials from the user + * as required by the SSH server, or NULL if the user will not be asked + * for additional credentials. * * @return * A new SSH session if the connection and authentication succeed, or NULL * if the connection or authentication were not successful. */ guac_common_ssh_session* guac_common_ssh_create_session(guac_client* client, - const char* hostname, const char* port, guac_common_ssh_user* user, int keepalive, - const char* host_key); + const char* hostname, const char* port, guac_common_ssh_user* user, + int keepalive, const char* host_key, + guac_ssh_credential_handler* credential_handler); /** * Disconnects and destroys the given SSH session, freeing all associated diff --git a/src/common-ssh/sftp.c b/src/common-ssh/sftp.c index 8a53b264..b05409b5 100644 --- a/src/common-ssh/sftp.c +++ b/src/common-ssh/sftp.c @@ -24,6 +24,7 @@ #include #include #include +#include #include #include @@ -32,107 +33,70 @@ #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, +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; + return 0; - /* Skip past leading slash */ - path++; + /* 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)); - /* Copy path into component data for parsing */ - strncpy(path_component_data, path, sizeof(path_component_data) - 1); + /* Fail if provided path is too long */ + if (length >= sizeof(path_scratch)) + return 0; - /* Find path components within path */ - for (i = 0; i < sizeof(path_component_data) - 1; i++) { + /* 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_component_data[i]; + char c = path_scratch[i]; if (c == '/' || c == '\\' || c == '\0') { /* Terminate current component */ - path_component_data[i] = '\0'; + path_scratch[i] = '\0'; /* If component refers to parent, just move up in depth */ - if (strcmp(current_path_component_data, "..") == 0) { + 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_data, ".") != 0 - && strcmp(current_path_component_data, "") != 0) - path_components[path_depth++] = current_path_component_data; + else if (strcmp(current_path_component, ".") != 0 + && strcmp(current_path_component, "") != 0) { - /* If end of string, stop */ - if (c == '\0') - break; + /* 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_data = &(path_component_data[i+1]); + current_path_component = &(path_scratch[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; - } + /* Add leading slash for resulting absolute path */ + fullpath[0] = '/'; - /* Ensure last component is null-terminated */ - path_component_data[i] = 0; + /* Append normalized components to path, separated by slashes */ + guac_strljoin(fullpath + 1, path_components, path_depth, + "/", GUAC_COMMON_SSH_SFTP_MAX_PATH - 1); - /* 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; } @@ -229,7 +193,7 @@ static guac_protocol_status guac_sftp_get_status( static int guac_ssh_append_filename(char* fullpath, const char* path, const char* filename) { - int i; + int length; /* Disallow "." as a filename */ if (strcmp(filename, ".") == 0) @@ -239,49 +203,29 @@ static int guac_ssh_append_filename(char* fullpath, const char* path, 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 (; i 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; @@ -310,46 +254,30 @@ static int guac_ssh_append_filename(char* fullpath, const char* path, static int guac_ssh_append_path(char* fullpath, const char* path_a, const char* path_b) { - int i; + int length; - /* Copy path, appending a trailing slash */ - for (i = 0; i < GUAC_COMMON_SSH_SFTP_MAX_PATH; i++) { + /* 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; - 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; - - } + /* 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 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; - - } + /* 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 (i == GUAC_COMMON_SSH_SFTP_MAX_PATH) + if (length >= GUAC_COMMON_SSH_SFTP_MAX_PATH) return 0; - /* Terminate path string */ - fullpath[i] = '\0'; - /* Append was successful */ return 1; @@ -830,8 +758,17 @@ static int guac_common_ssh_sftp_get_handler(guac_user* user, list_state->directory = dir; list_state->filesystem = filesystem; - strncpy(list_state->directory_name, name, - sizeof(list_state->directory_name) - 1); + + 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); diff --git a/src/common-ssh/ssh.c b/src/common-ssh/ssh.c index 9dde5111..c03e984d 100644 --- a/src/common-ssh/ssh.c +++ b/src/common-ssh/ssh.c @@ -304,20 +304,26 @@ static int guac_common_ssh_authenticate(guac_common_ssh_session* common_session) LIBSSH2_SESSION* session = common_session->session; /* Get user credentials */ - char* username = user->username; - char* password = user->password; guac_common_ssh_key* key = user->private_key; /* Validate username provided */ - if (username == NULL) { + if (user->username == NULL) { guac_client_abort(client, GUAC_PROTOCOL_STATUS_CLIENT_UNAUTHORIZED, "SSH authentication requires a username."); return 1; } /* Get list of supported authentication methods */ - char* user_authlist = libssh2_userauth_list(session, username, - strlen(username)); + char* user_authlist = libssh2_userauth_list(session, user->username, + strlen(user->username)); + + /* If auth list is NULL, then authentication has succeeded with NONE */ + if (user_authlist == NULL) { + guac_client_log(client, GUAC_LOG_DEBUG, + "SSH NONE authentication succeeded."); + return 0; + } + guac_client_log(client, GUAC_LOG_DEBUG, "Supported authentication methods: %s", user_authlist); @@ -333,7 +339,7 @@ static int guac_common_ssh_authenticate(guac_common_ssh_session* common_session) } /* Attempt public key auth */ - if (libssh2_userauth_publickey(session, username, + if (libssh2_userauth_publickey(session, user->username, (unsigned char*) key->public_key, key->public_key_length, guac_common_ssh_sign_callback, (void**) key)) { @@ -352,14 +358,18 @@ static int guac_common_ssh_authenticate(guac_common_ssh_session* common_session) } + /* Attempt authentication with username + password. */ + if (user->password == NULL && common_session->credential_handler) + user->password = common_session->credential_handler(client, "Password: "); + /* Authenticate with password, if provided */ - else if (password != NULL) { + if (user->password != NULL) { /* Check if password auth is supported on the server */ if (strstr(user_authlist, "password") != NULL) { /* Attempt password authentication */ - if (libssh2_userauth_password(session, username, password)) { + if (libssh2_userauth_password(session, user->username, user->password)) { /* Abort on failure */ char* error_message; @@ -380,7 +390,7 @@ static int guac_common_ssh_authenticate(guac_common_ssh_session* common_session) if (strstr(user_authlist, "keyboard-interactive") != NULL) { /* Attempt keyboard-interactive auth using provided password */ - if (libssh2_userauth_keyboard_interactive(session, username, + if (libssh2_userauth_keyboard_interactive(session, user->username, &guac_common_ssh_kbd_callback)) { /* Abort on failure */ @@ -415,8 +425,9 @@ static int guac_common_ssh_authenticate(guac_common_ssh_session* common_session) } guac_common_ssh_session* guac_common_ssh_create_session(guac_client* client, - const char* hostname, const char* port, guac_common_ssh_user* user, int keepalive, - const char* host_key) { + const char* hostname, const char* port, guac_common_ssh_user* user, + int keepalive, const char* host_key, + guac_ssh_credential_handler* credential_handler) { int retval; @@ -561,6 +572,7 @@ guac_common_ssh_session* guac_common_ssh_create_session(guac_client* client, common_session->user = user; common_session->session = session; common_session->fd = fd; + common_session->credential_handler = credential_handler; /* Attempt authentication */ if (guac_common_ssh_authenticate(common_session)) { diff --git a/src/common-ssh/tests/Makefile.am b/src/common-ssh/tests/Makefile.am new file mode 100644 index 00000000..12396991 --- /dev/null +++ b/src/common-ssh/tests/Makefile.am @@ -0,0 +1,67 @@ +# +# 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. +# +# NOTE: Parts of this file (Makefile.am) are automatically transcluded verbatim +# into Makefile.in. Though the build system (GNU Autotools) automatically adds +# its own license boilerplate to the generated Makefile.in, that boilerplate +# does not apply to the transcluded portions of Makefile.am which are licensed +# to you by the ASF under the Apache License, Version 2.0, as described above. +# + +AUTOMAKE_OPTIONS = foreign +ACLOCAL_AMFLAGS = -I m4 + +# +# Unit tests for common SSH support +# + +check_PROGRAMS = test_common_ssh +TESTS = $(check_PROGRAMS) + +test_common_ssh_SOURCES = \ + sftp/normalize_path.c + +test_common_ssh_CFLAGS = \ + -Werror -Wall -pedantic \ + @COMMON_INCLUDE@ \ + @COMMON_SSH_INCLUDE@ \ + @LIBGUAC_INCLUDE@ + +test_common_ssh_LDADD = \ + @CUNIT_LIBS@ \ + @COMMON_SSH_LTLIB@ \ + @COMMON_LTLIB@ + +# +# Autogenerate test runner +# + +GEN_RUNNER = $(top_srcdir)/util/generate-test-runner.pl +CLEANFILES = _generated_runner.c + +_generated_runner.c: $(test_common_ssh_SOURCES) + $(AM_V_GEN) $(GEN_RUNNER) $(test_common_ssh_SOURCES) > $@ + +nodist_test_common_ssh_SOURCES = \ + _generated_runner.c + +# Use automake's TAP test driver for running any tests +LOG_DRIVER = \ + env AM_TAP_AWK='$(AWK)' \ + $(SHELL) $(top_srcdir)/build-aux/tap-driver.sh + diff --git a/src/common-ssh/tests/sftp/normalize_path.c b/src/common-ssh/tests/sftp/normalize_path.c new file mode 100644 index 00000000..151b1835 --- /dev/null +++ b/src/common-ssh/tests/sftp/normalize_path.c @@ -0,0 +1,263 @@ +/* + * 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 +#include + +/** + * Test which verifies absolute Windows-style paths are correctly normalized to + * absolute paths with UNIX separators and no relative components. + */ +void test_sftp__normalize_absolute_windows() { + + char normalized[GUAC_COMMON_SSH_SFTP_MAX_PATH]; + + CU_ASSERT_NOT_EQUAL(guac_common_ssh_sftp_normalize_path(normalized, "\\"), 0); + CU_ASSERT_NSTRING_EQUAL(normalized, "/", sizeof(normalized)); + + CU_ASSERT_NOT_EQUAL(guac_common_ssh_sftp_normalize_path(normalized, "\\foo\\bar\\baz"), 0); + CU_ASSERT_NSTRING_EQUAL(normalized, "/foo/bar/baz", sizeof(normalized)); + + CU_ASSERT_NOT_EQUAL(guac_common_ssh_sftp_normalize_path(normalized, "\\foo\\bar\\..\\baz\\"), 0); + CU_ASSERT_NSTRING_EQUAL(normalized, "/foo/baz", sizeof(normalized)); + + CU_ASSERT_NOT_EQUAL(guac_common_ssh_sftp_normalize_path(normalized, "\\foo\\bar\\..\\..\\baz\\a\\..\\b"), 0); + CU_ASSERT_NSTRING_EQUAL(normalized, "/baz/b", sizeof(normalized)); + + CU_ASSERT_NOT_EQUAL(guac_common_ssh_sftp_normalize_path(normalized, "\\foo\\.\\bar\\baz"), 0); + CU_ASSERT_NSTRING_EQUAL(normalized, "/foo/bar/baz", sizeof(normalized)); + + CU_ASSERT_NOT_EQUAL(guac_common_ssh_sftp_normalize_path(normalized, "\\foo\\bar\\..\\..\\..\\..\\..\\..\\baz"), 0); + CU_ASSERT_NSTRING_EQUAL(normalized, "/baz", sizeof(normalized)); + +} + +/** + * Test which verifies absolute UNIX-style paths are correctly normalized to + * absolute paths with UNIX separators and no relative components. + */ +void test_sftp__normalize_absolute_unix() { + + char normalized[GUAC_COMMON_SSH_SFTP_MAX_PATH]; + + CU_ASSERT_NOT_EQUAL(guac_common_ssh_sftp_normalize_path(normalized, "/"), 0); + CU_ASSERT_NSTRING_EQUAL(normalized, "/", sizeof(normalized)); + + CU_ASSERT_NOT_EQUAL(guac_common_ssh_sftp_normalize_path(normalized, "/foo/bar/baz"), 0); + CU_ASSERT_NSTRING_EQUAL(normalized, "/foo/bar/baz", sizeof(normalized)); + + CU_ASSERT_NOT_EQUAL(guac_common_ssh_sftp_normalize_path(normalized, "/foo/bar/../baz/"), 0); + CU_ASSERT_NSTRING_EQUAL(normalized, "/foo/baz", sizeof(normalized)); + + CU_ASSERT_NOT_EQUAL(guac_common_ssh_sftp_normalize_path(normalized, "/foo/bar/../../baz/a/../b"), 0); + CU_ASSERT_NSTRING_EQUAL(normalized, "/baz/b", sizeof(normalized)); + + CU_ASSERT_NOT_EQUAL(guac_common_ssh_sftp_normalize_path(normalized, "/foo/./bar/baz"), 0); + CU_ASSERT_NSTRING_EQUAL(normalized, "/foo/bar/baz", sizeof(normalized)); + + CU_ASSERT_NOT_EQUAL(guac_common_ssh_sftp_normalize_path(normalized, "/foo/bar/../../../../../../baz"), 0); + CU_ASSERT_NSTRING_EQUAL(normalized, "/baz", sizeof(normalized)); + +} + +/** + * Test which verifies absolute paths consisting of mixed Windows and UNIX path + * separators are correctly normalized to absolute paths with UNIX separators + * and no relative components. + */ +void test_sftp__normalize_absolute_mixed() { + + char normalized[GUAC_COMMON_SSH_SFTP_MAX_PATH]; + + CU_ASSERT_NOT_EQUAL(guac_common_ssh_sftp_normalize_path(normalized, "\\foo/bar\\baz"), 0); + CU_ASSERT_NSTRING_EQUAL(normalized, "/foo/bar/baz", sizeof(normalized)); + + CU_ASSERT_NOT_EQUAL(guac_common_ssh_sftp_normalize_path(normalized, "/foo\\bar/..\\baz/"), 0); + CU_ASSERT_NSTRING_EQUAL(normalized, "/foo/baz", sizeof(normalized)); + + CU_ASSERT_NOT_EQUAL(guac_common_ssh_sftp_normalize_path(normalized, "\\foo/bar\\../../baz\\a\\..\\b"), 0); + CU_ASSERT_NSTRING_EQUAL(normalized, "/baz/b", sizeof(normalized)); + + CU_ASSERT_NOT_EQUAL(guac_common_ssh_sftp_normalize_path(normalized, "\\foo\\.\\bar/baz"), 0); + CU_ASSERT_NSTRING_EQUAL(normalized, "/foo/bar/baz", sizeof(normalized)); + + CU_ASSERT_NOT_EQUAL(guac_common_ssh_sftp_normalize_path(normalized, "\\foo/bar\\../..\\..\\..\\../..\\baz"), 0); + CU_ASSERT_NSTRING_EQUAL(normalized, "/baz", sizeof(normalized)); + +} + +/** + * Test which verifies relative Windows-style paths are always rejected. + */ +void test_sftp__normalize_relative_windows() { + + char normalized[GUAC_COMMON_SSH_SFTP_MAX_PATH]; + + CU_ASSERT_EQUAL(guac_common_ssh_sftp_normalize_path(normalized, ""), 0); + CU_ASSERT_EQUAL(guac_common_ssh_sftp_normalize_path(normalized, "."), 0); + CU_ASSERT_EQUAL(guac_common_ssh_sftp_normalize_path(normalized, ".."), 0); + CU_ASSERT_EQUAL(guac_common_ssh_sftp_normalize_path(normalized, "foo"), 0); + CU_ASSERT_EQUAL(guac_common_ssh_sftp_normalize_path(normalized, ".\\foo"), 0); + CU_ASSERT_EQUAL(guac_common_ssh_sftp_normalize_path(normalized, "..\\foo"), 0); + CU_ASSERT_EQUAL(guac_common_ssh_sftp_normalize_path(normalized, "foo\\bar\\baz"), 0); + CU_ASSERT_EQUAL(guac_common_ssh_sftp_normalize_path(normalized, ".\\foo\\bar\\baz"), 0); + CU_ASSERT_EQUAL(guac_common_ssh_sftp_normalize_path(normalized, "..\\foo\\bar\\baz"), 0); + +} + +/** + * Test which verifies relative UNIX-style paths are always rejected. + */ +void test_sftp__normalize_relative_unix() { + + char normalized[GUAC_COMMON_SSH_SFTP_MAX_PATH]; + + CU_ASSERT_EQUAL(guac_common_ssh_sftp_normalize_path(normalized, ""), 0); + CU_ASSERT_EQUAL(guac_common_ssh_sftp_normalize_path(normalized, "."), 0); + CU_ASSERT_EQUAL(guac_common_ssh_sftp_normalize_path(normalized, ".."), 0); + CU_ASSERT_EQUAL(guac_common_ssh_sftp_normalize_path(normalized, "foo"), 0); + CU_ASSERT_EQUAL(guac_common_ssh_sftp_normalize_path(normalized, "./foo"), 0); + CU_ASSERT_EQUAL(guac_common_ssh_sftp_normalize_path(normalized, "../foo"), 0); + CU_ASSERT_EQUAL(guac_common_ssh_sftp_normalize_path(normalized, "foo/bar/baz"), 0); + CU_ASSERT_EQUAL(guac_common_ssh_sftp_normalize_path(normalized, "./foo/bar/baz"), 0); + CU_ASSERT_EQUAL(guac_common_ssh_sftp_normalize_path(normalized, "../foo/bar/baz"), 0); + +} + +/** + * Test which verifies relative paths consisting of mixed Windows and UNIX path + * separators are always rejected. + */ +void test_sftp__normalize_relative_mixed() { + + char normalized[GUAC_COMMON_SSH_SFTP_MAX_PATH]; + + CU_ASSERT_EQUAL(guac_common_ssh_sftp_normalize_path(normalized, "foo\\bar/baz"), 0); + CU_ASSERT_EQUAL(guac_common_ssh_sftp_normalize_path(normalized, ".\\foo/bar/baz"), 0); + CU_ASSERT_EQUAL(guac_common_ssh_sftp_normalize_path(normalized, "../foo\\bar\\baz"), 0); + +} + +/** + * Generates a dynamically-allocated path having the given number of bytes, not + * counting the null-terminator. The path will contain only UNIX-style path + * separators. The returned path must eventually be freed with a call to + * free(). + * + * @param length + * The number of bytes to include in the generated path, not counting the + * null-terminator. If -1, the length of the path will be automatically + * determined from the provided max_depth. + * + * @param max_depth + * The maximum number of path components to include within the generated + * path. + * + * @return + * A dynamically-allocated path containing the given number of bytes, not + * counting the null-terminator. This path must eventually be freed with a + * call to free(). + */ +static char* generate_path(int length, int max_depth) { + + /* If no length given, calculate space required from max_depth */ + if (length == -1) + length = max_depth * 2; + + int i; + char* input = malloc(length + 1); + + /* Fill path with /x/x/x/x/x/x/x/x/x/x/.../xxxxxxxxx... */ + for (i = 0; i < length; i++) { + if (max_depth > 0 && i % 2 == 0) { + input[i] = '/'; + max_depth--; + } + else + input[i] = 'x'; + } + + /* Add null terminator */ + input[length] = '\0'; + + return input; + +} + +/** + * Test which verifies that paths exceeding the maximum path length are + * rejected. + */ +void test_sftp__normalize_long() { + + char* input; + char normalized[GUAC_COMMON_SSH_SFTP_MAX_PATH]; + + /* Exceeds maximum length by a factor of 2 */ + input = generate_path(GUAC_COMMON_SSH_SFTP_MAX_PATH * 2, GUAC_COMMON_SSH_SFTP_MAX_DEPTH); + CU_ASSERT_EQUAL(guac_common_ssh_sftp_normalize_path(normalized, input), 0); + free(input); + + /* Exceeds maximum length by one byte */ + input = generate_path(GUAC_COMMON_SSH_SFTP_MAX_PATH, GUAC_COMMON_SSH_SFTP_MAX_DEPTH); + CU_ASSERT_EQUAL(guac_common_ssh_sftp_normalize_path(normalized, input), 0); + free(input); + + /* Exactly maximum length */ + input = generate_path(GUAC_COMMON_SSH_SFTP_MAX_PATH - 1, GUAC_COMMON_SSH_SFTP_MAX_DEPTH); + CU_ASSERT_NOT_EQUAL(guac_common_ssh_sftp_normalize_path(normalized, input), 0); + free(input); + +} + +/** + * Test which verifies that paths exceeding the maximum path depth are + * rejected. + */ +void test_sftp__normalize_deep() { + + char* input; + char normalized[GUAC_COMMON_SSH_SFTP_MAX_PATH]; + + /* Exceeds maximum depth by a factor of 2 */ + input = generate_path(-1, GUAC_COMMON_SSH_SFTP_MAX_DEPTH * 2); + CU_ASSERT_EQUAL(guac_common_ssh_sftp_normalize_path(normalized, input), 0); + free(input); + + /* Exceeds maximum depth by one component */ + input = generate_path(-1, GUAC_COMMON_SSH_SFTP_MAX_DEPTH + 1); + CU_ASSERT_EQUAL(guac_common_ssh_sftp_normalize_path(normalized, input), 0); + free(input); + + /* Exactly maximum depth (should still be rejected as SFTP depth limits are + * set such that a path with the maximum depth will exceed the maximum + * length) */ + input = generate_path(-1, GUAC_COMMON_SSH_SFTP_MAX_DEPTH); + CU_ASSERT_EQUAL(guac_common_ssh_sftp_normalize_path(normalized, input), 0); + free(input); + + /* Less than maximum depth */ + input = generate_path(-1, GUAC_COMMON_SSH_SFTP_MAX_DEPTH - 1); + CU_ASSERT_NOT_EQUAL(guac_common_ssh_sftp_normalize_path(normalized, input), 0); + free(input); + +} + diff --git a/src/common/.gitignore b/src/common/.gitignore new file mode 100644 index 00000000..b66a6a9a --- /dev/null +++ b/src/common/.gitignore @@ -0,0 +1,5 @@ + +# Auto-generated test runner and binary +_generated_runner.c +test_common + diff --git a/src/common/Makefile.am b/src/common/Makefile.am index 32d12836..f98054be 100644 --- a/src/common/Makefile.am +++ b/src/common/Makefile.am @@ -16,11 +16,18 @@ # specific language governing permissions and limitations # under the License. # +# NOTE: Parts of this file (Makefile.am) are automatically transcluded verbatim +# into Makefile.in. Though the build system (GNU Autotools) automatically adds +# its own license boilerplate to the generated Makefile.in, that boilerplate +# does not apply to the transcluded portions of Makefile.am which are licensed +# to you by the ASF under the Apache License, Version 2.0, as described above. +# AUTOMAKE_OPTIONS = foreign ACLOCAL_AMFLAGS = -I m4 noinst_LTLIBRARIES = libguac_common.la +SUBDIRS = . tests noinst_HEADERS = \ common/io.h \ diff --git a/src/common/clipboard.c b/src/common/clipboard.c index eb2f5480..e02a7ca4 100644 --- a/src/common/clipboard.c +++ b/src/common/clipboard.c @@ -23,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -131,8 +132,7 @@ void guac_common_clipboard_reset(guac_common_clipboard* clipboard, clipboard->length = 0; /* Assign given mimetype */ - strncpy(clipboard->mimetype, mimetype, sizeof(clipboard->mimetype) - 1); - clipboard->mimetype[sizeof(clipboard->mimetype) - 1] = '\0'; + guac_strlcpy(clipboard->mimetype, mimetype, sizeof(clipboard->mimetype)); pthread_mutex_unlock(&(clipboard->lock)); diff --git a/src/common/surface.c b/src/common/surface.c index 8c690b75..c31dfbf0 100644 --- a/src/common/surface.c +++ b/src/common/surface.c @@ -78,13 +78,6 @@ #define cairo_format_stride_for_width(format, width) (width*4) #endif -/** - * The JPEG image quality ('quantization') setting to use. Range 0-100 where - * 100 is the highest quality/largest file size, and 0 is the lowest - * quality/smallest file size. - */ -#define GUAC_SURFACE_JPEG_IMAGE_QUALITY 90 - /** * The framerate which, if exceeded, indicates that JPEG is preferred. */ @@ -96,13 +89,6 @@ */ #define GUAC_SURFACE_JPEG_MIN_BITMAP_SIZE 4096 -/** - * The WebP image quality ('quantization') setting to use. Range 0-100 where - * 100 is the highest quality/largest file size, and 0 is the lowest - * quality/smallest file size. - */ -#define GUAC_SURFACE_WEBP_IMAGE_QUALITY 90 - /** * The JPEG compression min block size. This defines the optimal rectangle block * size factor for JPEG compression. Usually 8x8 would suffice, but use 16 to @@ -1666,6 +1652,36 @@ static void __guac_common_surface_flush_to_png(guac_common_surface* surface, } +/** + * Returns an appropriate quality between 0 and 100 for lossy encoding + * depending on the current processing lag calculated for the given client. + * + * @param client + * The client for which the lossy quality is being calculated. + * + * @return + * A value between 0 and 100 inclusive which seems appropriate for the + * client based on lag measurements. + */ +static int guac_common_surface_suggest_quality(guac_client* client) { + + int lag = guac_client_get_processing_lag(client); + + /* Scale quality linearly from 90 to 30 as lag varies from 20ms to 80ms */ + int quality = 90 - (lag - 20); + + /* Do not exceed 90 for quality */ + if (quality > 90) + return 90; + + /* Do not go below 30 for quality */ + if (quality < 30) + return 30; + + return quality; + +} + /** * Flushes the bitmap update currently described by the dirty rectangle within * the given surface directly via an "img" instruction as JPEG data. The @@ -1702,7 +1718,7 @@ static void __guac_common_surface_flush_to_jpeg(guac_common_surface* surface) { /* Send JPEG for rect */ guac_client_stream_jpeg(surface->client, socket, GUAC_COMP_OVER, layer, surface->dirty_rect.x, surface->dirty_rect.y, rect, - GUAC_SURFACE_JPEG_IMAGE_QUALITY); + guac_common_surface_suggest_quality(surface->client)); cairo_surface_destroy(rect); surface->realized = 1; @@ -1764,7 +1780,7 @@ static void __guac_common_surface_flush_to_webp(guac_common_surface* surface, /* Send WebP for rect */ guac_client_stream_webp(surface->client, socket, GUAC_COMP_OVER, layer, surface->dirty_rect.x, surface->dirty_rect.y, rect, - GUAC_SURFACE_WEBP_IMAGE_QUALITY, 0); + guac_common_surface_suggest_quality(surface->client), 0); cairo_surface_destroy(rect); surface->realized = 1; diff --git a/src/common/tests/Makefile.am b/src/common/tests/Makefile.am new file mode 100644 index 00000000..a9c1b559 --- /dev/null +++ b/src/common/tests/Makefile.am @@ -0,0 +1,72 @@ +# +# 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. +# +# NOTE: Parts of this file (Makefile.am) are automatically transcluded verbatim +# into Makefile.in. Though the build system (GNU Autotools) automatically adds +# its own license boilerplate to the generated Makefile.in, that boilerplate +# does not apply to the transcluded portions of Makefile.am which are licensed +# to you by the ASF under the Apache License, Version 2.0, as described above. +# + +AUTOMAKE_OPTIONS = foreign +ACLOCAL_AMFLAGS = -I m4 + +# +# Unit tests for libguac_common +# + +check_PROGRAMS = test_common +TESTS = $(check_PROGRAMS) + +test_common_SOURCES = \ + iconv/convert.c \ + rect/clip_and_split.c \ + rect/constrain.c \ + rect/expand_to_grid.c \ + rect/extend.c \ + rect/init.c \ + rect/intersects.c \ + string/count_occurrences.c \ + string/split.c + +test_common_CFLAGS = \ + -Werror -Wall -pedantic \ + @COMMON_INCLUDE@ + +test_common_LDADD = \ + @COMMON_LTLIB@ \ + @CUNIT_LIBS@ + +# +# Autogenerate test runner +# + +GEN_RUNNER = $(top_srcdir)/util/generate-test-runner.pl +CLEANFILES = _generated_runner.c + +_generated_runner.c: $(test_common_SOURCES) + $(AM_V_GEN) $(GEN_RUNNER) $(test_common_SOURCES) > $@ + +nodist_test_common_SOURCES = \ + _generated_runner.c + +# Use automake's TAP test driver for running any tests +LOG_DRIVER = \ + env AM_TAP_AWK='$(AWK)' \ + $(SHELL) $(top_srcdir)/build-aux/tap-driver.sh + diff --git a/tests/common/guac_iconv.c b/src/common/tests/iconv/convert.c similarity index 52% rename from tests/common/guac_iconv.c rename to src/common/tests/iconv/convert.c index 6af706f9..1a00994f 100644 --- a/tests/common/guac_iconv.c +++ b/src/common/tests/iconv/convert.c @@ -17,15 +17,75 @@ * under the License. */ -#include "config.h" - -#include "common_suite.h" #include "common/iconv.h" -#include -#include +#include -static void test_conversion( +/** + * UTF8 for "papà è bello". + */ +unsigned char test_string_utf8[] = { + 'p', 'a', 'p', 0xC3, 0xA0, ' ', + 0xC3, 0xA8, ' ', + 'b', 'e', 'l', 'l', 'o', + 0x00 +}; + +/** + * UTF16 for "papà è bello". + */ +unsigned char test_string_utf16[] = { + 'p', 0x00, 'a', 0x00, 'p', 0x00, 0xE0, 0x00, ' ', 0x00, + 0xE8, 0x00, ' ', 0x00, + 'b', 0x00, 'e', 0x00, 'l', 0x00, 'l', 0x00, 'o', 0x00, + 0x00, 0x00 +}; + +/** + * ISO-8859-1 for "papà è bello". + */ +unsigned char test_string_iso8859_1[] = { + 'p', 'a', 'p', 0xE0, ' ', + 0xE8, ' ', + 'b', 'e', 'l', 'l', 'o', + 0x00 +}; + +/** + * CP1252 for "papà è bello". + */ +unsigned char test_string_cp1252[] = { + 'p', 'a', 'p', 0xE0, ' ', + 0xE8, ' ', + 'b', 'e', 'l', 'l', 'o', + 0x00 +}; + +/** + * Tests that conversion between character sets using the given guac_iconv_read + * and guac_iconv_write implementations matches expectations. + * + * @param reader + * The guac_iconv_read implementation to use to read the input string. + * + * @param in_string + * A pointer to the beginning of the input string. + * + * @param in_length + * The size of the input string in bytes. + * + * @param writer + * The guac_iconv_write implementation to use to write the output string + * (the converted input string). + * + * @param out_string + * A pointer to the beginning of a string which contains the expected + * result of the conversion. + * + * @param out_length + * The size of the expected result in bytes. + */ +static void verify_conversion( guac_iconv_read* reader, unsigned char* in_string, int in_length, guac_iconv_write* writer, unsigned char* out_string, int out_length) { @@ -50,77 +110,74 @@ static void test_conversion( } -void test_guac_iconv() { - - /* UTF8 for "papà è bello" */ - unsigned char test_string_utf8[] = { - 'p', 'a', 'p', 0xC3, 0xA0, ' ', - 0xC3, 0xA8, ' ', - 'b', 'e', 'l', 'l', 'o', - 0x00 - }; - - /* UTF16 for "papà è bello" */ - unsigned char test_string_utf16[] = { - 'p', 0x00, 'a', 0x00, 'p', 0x00, 0xE0, 0x00, ' ', 0x00, - 0xE8, 0x00, ' ', 0x00, - 'b', 0x00, 'e', 0x00, 'l', 0x00, 'l', 0x00, 'o', 0x00, - 0x00, 0x00 - }; - - /* ISO-8859-1 for "papà è bello" */ - unsigned char test_string_iso8859_1[] = { - 'p', 'a', 'p', 0xE0, ' ', - 0xE8, ' ', - 'b', 'e', 'l', 'l', 'o', - 0x00 - }; - - /* CP1252 for "papà è bello" */ - unsigned char test_string_cp1252[] = { - 'p', 'a', 'p', 0xE0, ' ', - 0xE8, ' ', - 'b', 'e', 'l', 'l', 'o', - 0x00 - }; - - /* UTF8 identity */ - test_conversion( +/** + * Tests which verifies conversion of UTF-8 to itself. + */ +void test_iconv__utf8_to_utf8() { + verify_conversion( GUAC_READ_UTF8, test_string_utf8, sizeof(test_string_utf8), GUAC_WRITE_UTF8, test_string_utf8, sizeof(test_string_utf8)); +} - /* UTF16 identity */ - test_conversion( - GUAC_READ_UTF16, test_string_utf16, sizeof(test_string_utf16), - GUAC_WRITE_UTF16, test_string_utf16, sizeof(test_string_utf16)); - - /* UTF8 to UTF16 */ - test_conversion( +/** + * Tests which verifies conversion of UTF-16 to UTF-8. + */ +void test_iconv__utf8_to_utf16() { + verify_conversion( GUAC_READ_UTF8, test_string_utf8, sizeof(test_string_utf8), GUAC_WRITE_UTF16, test_string_utf16, sizeof(test_string_utf16)); +} - /* UTF16 to UTF8 */ - test_conversion( +/** + * Tests which verifies conversion of UTF-16 to itself. + */ +void test_iconv__utf16_to_utf16() { + verify_conversion( + GUAC_READ_UTF16, test_string_utf16, sizeof(test_string_utf16), + GUAC_WRITE_UTF16, test_string_utf16, sizeof(test_string_utf16)); +} + +/** + * Tests which verifies conversion of UTF-8 to UTF-16. + */ +void test_iconv__utf16_to_utf8() { + verify_conversion( GUAC_READ_UTF16, test_string_utf16, sizeof(test_string_utf16), GUAC_WRITE_UTF8, test_string_utf8, sizeof(test_string_utf8)); +} - /* UTF16 to ISO-8859-1 */ - test_conversion( +/** + * Tests which verifies conversion of UTF-16 to ISO 8859-1. + */ +void test_iconv__utf16_to_iso8859_1() { + verify_conversion( GUAC_READ_UTF16, test_string_utf16, sizeof(test_string_utf16), GUAC_WRITE_ISO8859_1, test_string_iso8859_1, sizeof(test_string_iso8859_1)); +} - /* UTF16 to CP1252 */ - test_conversion( +/** + * Tests which verifies conversion of UTF-16 to CP1252. + */ +void test_iconv__utf16_to_cp1252() { + verify_conversion( GUAC_READ_UTF16, test_string_utf16, sizeof(test_string_utf16), GUAC_WRITE_CP1252, test_string_cp1252, sizeof(test_string_cp1252)); +} - /* CP1252 to UTF8 */ - test_conversion( +/** + * Tests which verifies conversion of CP1252 to UTF-8. + */ +void test_iconv__cp1252_to_utf8() { + verify_conversion( GUAC_READ_CP1252, test_string_cp1252, sizeof(test_string_cp1252), GUAC_WRITE_UTF8, test_string_utf8, sizeof(test_string_utf8)); +} - /* ISO-8859-1 to UTF8 */ - test_conversion( +/** + * Tests which verifies conversion of ISO 8859-1 to UTF-8. + */ +void test_iconv__iso8859_1_to_utf8() { + verify_conversion( GUAC_READ_ISO8859_1, test_string_iso8859_1, sizeof(test_string_iso8859_1), GUAC_WRITE_UTF8, test_string_utf8, sizeof(test_string_utf8)); diff --git a/src/common/tests/rect/clip_and_split.c b/src/common/tests/rect/clip_and_split.c new file mode 100644 index 00000000..e286bce9 --- /dev/null +++ b/src/common/tests/rect/clip_and_split.c @@ -0,0 +1,156 @@ +/* + * 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/rect.h" + +#include + +/** + * Test which verifies that guac_common_rect_clip_and_split() divides a + * rectangle into subrectangles after removing a "hole" rectangle. + */ +void test_rect__clip_and_split() { + + int res; + + guac_common_rect cut; + guac_common_rect min; + guac_common_rect rect; + + guac_common_rect_init(&min, 10, 10, 10, 10); + + /* Clip top */ + guac_common_rect_init(&rect, 10, 5, 10, 10); + res = guac_common_rect_clip_and_split(&rect, &min, &cut); + CU_ASSERT_EQUAL(1, res); + CU_ASSERT_EQUAL(10, cut.x); + CU_ASSERT_EQUAL(5, cut.y); + CU_ASSERT_EQUAL(10, cut.width); + CU_ASSERT_EQUAL(5, cut.height); + + CU_ASSERT_EQUAL(10, rect.x); + CU_ASSERT_EQUAL(10, rect.y); + CU_ASSERT_EQUAL(10, rect.width); + CU_ASSERT_EQUAL(5, rect.height); + + /* Clip bottom */ + guac_common_rect_init(&rect, 10, 15, 10, 10); + res = guac_common_rect_clip_and_split(&rect, &min, &cut); + CU_ASSERT_EQUAL(1, res); + CU_ASSERT_EQUAL(10, cut.x); + CU_ASSERT_EQUAL(20, cut.y); + CU_ASSERT_EQUAL(10, cut.width); + CU_ASSERT_EQUAL(5, cut.height); + + CU_ASSERT_EQUAL(10, rect.x); + CU_ASSERT_EQUAL(15, rect.y); + CU_ASSERT_EQUAL(10, rect.width); + CU_ASSERT_EQUAL(5, rect.height); + + /* Clip left */ + guac_common_rect_init(&rect, 5, 10, 10, 10); + res = guac_common_rect_clip_and_split(&rect, &min, &cut); + CU_ASSERT_EQUAL(1, res); + CU_ASSERT_EQUAL(5, cut.x); + CU_ASSERT_EQUAL(10, cut.y); + CU_ASSERT_EQUAL(5, cut.width); + CU_ASSERT_EQUAL(10, cut.height); + + CU_ASSERT_EQUAL(10, rect.x); + CU_ASSERT_EQUAL(10, rect.y); + CU_ASSERT_EQUAL(5, rect.width); + CU_ASSERT_EQUAL(10, rect.height); + + /* Clip right */ + guac_common_rect_init(&rect, 15, 10, 10, 10); + res = guac_common_rect_clip_and_split(&rect, &min, &cut); + CU_ASSERT_EQUAL(1, res); + CU_ASSERT_EQUAL(20, cut.x); + CU_ASSERT_EQUAL(10, cut.y); + CU_ASSERT_EQUAL(5, cut.width); + CU_ASSERT_EQUAL(10, cut.height); + + CU_ASSERT_EQUAL(15, rect.x); + CU_ASSERT_EQUAL(10, rect.y); + CU_ASSERT_EQUAL(5, rect.width); + CU_ASSERT_EQUAL(10, rect.height); + + /* + * Test a rectangle which completely covers the hole. + * Clip and split until done. + */ + guac_common_rect_init(&rect, 5, 5, 20, 20); + + /* Clip top */ + res = guac_common_rect_clip_and_split(&rect, &min, &cut); + CU_ASSERT_EQUAL(1, res); + CU_ASSERT_EQUAL(5, cut.x); + CU_ASSERT_EQUAL(5, cut.y); + CU_ASSERT_EQUAL(20, cut.width); + CU_ASSERT_EQUAL(5, cut.height); + + CU_ASSERT_EQUAL(5, rect.x); + CU_ASSERT_EQUAL(10, rect.y); + CU_ASSERT_EQUAL(20, rect.width); + CU_ASSERT_EQUAL(15, rect.height); + + /* Clip left */ + res = guac_common_rect_clip_and_split(&rect, &min, &cut); + CU_ASSERT_EQUAL(1, res); + CU_ASSERT_EQUAL(5, cut.x); + CU_ASSERT_EQUAL(10, cut.y); + CU_ASSERT_EQUAL(5, cut.width); + CU_ASSERT_EQUAL(15, cut.height); + + CU_ASSERT_EQUAL(10, rect.x); + CU_ASSERT_EQUAL(10, rect.y); + CU_ASSERT_EQUAL(15, rect.width); + CU_ASSERT_EQUAL(15, rect.height); + + /* Clip bottom */ + res = guac_common_rect_clip_and_split(&rect, &min, &cut); + CU_ASSERT_EQUAL(1, res); + CU_ASSERT_EQUAL(10, cut.x); + CU_ASSERT_EQUAL(20, cut.y); + CU_ASSERT_EQUAL(15, cut.width); + CU_ASSERT_EQUAL(5, cut.height); + + CU_ASSERT_EQUAL(10, rect.x); + CU_ASSERT_EQUAL(10, rect.y); + CU_ASSERT_EQUAL(15, rect.width); + CU_ASSERT_EQUAL(10, rect.height); + + /* Clip right */ + res = guac_common_rect_clip_and_split(&rect, &min, &cut); + CU_ASSERT_EQUAL(20, cut.x); + CU_ASSERT_EQUAL(10, cut.y); + CU_ASSERT_EQUAL(5, cut.width); + CU_ASSERT_EQUAL(10, cut.height); + + CU_ASSERT_EQUAL(10, rect.x); + CU_ASSERT_EQUAL(10, rect.y); + CU_ASSERT_EQUAL(10, rect.width); + CU_ASSERT_EQUAL(10, rect.height); + + /* Make sure nothing is left to do */ + res = guac_common_rect_clip_and_split(&rect, &min, &cut); + CU_ASSERT_EQUAL(0, res); + +} + diff --git a/src/common/tests/rect/constrain.c b/src/common/tests/rect/constrain.c new file mode 100644 index 00000000..793aac22 --- /dev/null +++ b/src/common/tests/rect/constrain.c @@ -0,0 +1,43 @@ +/* + * 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/rect.h" + +#include + +/** + * Test which verifies that guac_common_rect_constrain() restricts a given + * rectangle to arbitrary bounds. + */ +void test_rect__constrain() { + + guac_common_rect max; + guac_common_rect rect; + + guac_common_rect_init(&rect, -10, -10, 110, 110); + guac_common_rect_init(&max, 0, 0, 100, 100); + guac_common_rect_constrain(&rect, &max); + + CU_ASSERT_EQUAL(0, rect.x); + CU_ASSERT_EQUAL(0, rect.y); + CU_ASSERT_EQUAL(100, rect.width); + CU_ASSERT_EQUAL(100, rect.height); + +} + diff --git a/src/common/tests/rect/expand_to_grid.c b/src/common/tests/rect/expand_to_grid.c new file mode 100644 index 00000000..beef87d8 --- /dev/null +++ b/src/common/tests/rect/expand_to_grid.c @@ -0,0 +1,71 @@ +/* + * 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/rect.h" + +#include + +/** + * Test which verifies guac_common_rect_expand_to_grid() properly shifts and + * resizes rectangles to fit an NxN grid. + */ +void test_rect__expand_to_grid() { + + int cell_size = 16; + + guac_common_rect max; + guac_common_rect rect; + + /* Simple adjustment */ + guac_common_rect_init(&rect, 0, 0, 25, 25); + guac_common_rect_init(&max, 0, 0, 100, 100); + guac_common_rect_expand_to_grid(cell_size, &rect, &max); + CU_ASSERT_EQUAL(0, rect.x); + CU_ASSERT_EQUAL(0, rect.y); + CU_ASSERT_EQUAL(32, rect.width); + CU_ASSERT_EQUAL(32, rect.height); + + /* Adjustment with moving of rect */ + guac_common_rect_init(&rect, 75, 75, 25, 25); + guac_common_rect_init(&max, 0, 0, 100, 100); + guac_common_rect_expand_to_grid(cell_size, &rect, &max); + CU_ASSERT_EQUAL(max.width - 32, rect.x); + CU_ASSERT_EQUAL(max.height - 32, rect.y); + CU_ASSERT_EQUAL(32, rect.width); + CU_ASSERT_EQUAL(32, rect.height); + + guac_common_rect_init(&rect, -5, -5, 25, 25); + guac_common_rect_init(&max, 0, 0, 100, 100); + guac_common_rect_expand_to_grid(cell_size, &rect, &max); + CU_ASSERT_EQUAL(0, rect.x); + CU_ASSERT_EQUAL(0, rect.y); + CU_ASSERT_EQUAL(32, rect.width); + CU_ASSERT_EQUAL(32, rect.height); + + /* Adjustment with moving and clamping of rect */ + guac_common_rect_init(&rect, 0, 0, 25, 15); + guac_common_rect_init(&max, 0, 5, 32, 15); + guac_common_rect_expand_to_grid(cell_size, &rect, &max); + CU_ASSERT_EQUAL(max.x, rect.x); + CU_ASSERT_EQUAL(max.y, rect.y); + CU_ASSERT_EQUAL(max.width, rect.width); + CU_ASSERT_EQUAL(max.height, rect.height); + +} + diff --git a/src/common/tests/rect/extend.c b/src/common/tests/rect/extend.c new file mode 100644 index 00000000..dc2a0e30 --- /dev/null +++ b/src/common/tests/rect/extend.c @@ -0,0 +1,42 @@ +/* + * 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/rect.h" + +#include + +/** + * Test which verifies that guac_common_rect_extend() expands the given + * rectangle as necessary to contain at least the given bounds. + */ +void test_rect__extend() { + + guac_common_rect max; + guac_common_rect rect; + + guac_common_rect_init(&rect, 10, 10, 90, 90); + guac_common_rect_init(&max, 0, 0, 100, 100); + guac_common_rect_extend(&rect, &max); + CU_ASSERT_EQUAL(0, rect.x); + CU_ASSERT_EQUAL(0, rect.y); + CU_ASSERT_EQUAL(100, rect.width); + CU_ASSERT_EQUAL(100, rect.height); + +} + diff --git a/tests/test_libguac.c b/src/common/tests/rect/init.c similarity index 60% rename from tests/test_libguac.c rename to src/common/tests/rect/init.c index 06223ce9..288cd751 100644 --- a/tests/test_libguac.c +++ b/src/common/tests/rect/init.c @@ -17,31 +17,23 @@ * under the License. */ -#include "config.h" +#include "common/rect.h" -#include "client/client_suite.h" -#include "common/common_suite.h" -#include "protocol/suite.h" -#include "util/util_suite.h" +#include -#include +/** + * Test which verifies rectangle initialization via guac_common_rect_init(). + */ +void test_rect__init() { -int main() { + guac_common_rect max; - /* Init registry */ - if (CU_initialize_registry() != CUE_SUCCESS) - return CU_get_error(); + guac_common_rect_init(&max, 0, 0, 100, 100); - /* Register suites */ - register_protocol_suite(); - register_client_suite(); - register_util_suite(); - - /* Run tests */ - CU_basic_set_mode(CU_BRM_VERBOSE); - CU_basic_run_tests(); - CU_cleanup_registry(); - return CU_get_error(); + CU_ASSERT_EQUAL(0, max.x); + CU_ASSERT_EQUAL(0, max.y); + CU_ASSERT_EQUAL(100, max.width); + CU_ASSERT_EQUAL(100, max.height); } diff --git a/src/common/tests/rect/intersects.c b/src/common/tests/rect/intersects.c new file mode 100644 index 00000000..c4802684 --- /dev/null +++ b/src/common/tests/rect/intersects.c @@ -0,0 +1,91 @@ +/* + * 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/rect.h" + +#include + +/** + * Test which verifies intersection testing via guac_common_rect_intersects(). + */ +void test_rect__intersects() { + + int res; + + guac_common_rect min; + guac_common_rect rect; + + guac_common_rect_init(&min, 10, 10, 10, 10); + + /* Rectangle intersection - empty + * rectangle is outside */ + guac_common_rect_init(&rect, 25, 25, 5, 5); + res = guac_common_rect_intersects(&rect, &min); + CU_ASSERT_EQUAL(0, res); + + /* Rectangle intersection - complete + * rectangle is completely inside */ + guac_common_rect_init(&rect, 11, 11, 5, 5); + res = guac_common_rect_intersects(&rect, &min); + CU_ASSERT_EQUAL(2, res); + + /* Rectangle intersection - partial + * rectangle intersects UL */ + guac_common_rect_init(&rect, 8, 8, 5, 5); + res = guac_common_rect_intersects(&rect, &min); + CU_ASSERT_EQUAL(1, res); + + /* Rectangle intersection - partial + * rectangle intersects LR */ + guac_common_rect_init(&rect, 18, 18, 5, 5); + res = guac_common_rect_intersects(&rect, &min); + CU_ASSERT_EQUAL(1, res); + + /* Rectangle intersection - complete + * rect intersects along UL but inside */ + guac_common_rect_init(&rect, 10, 10, 5, 5); + res = guac_common_rect_intersects(&rect, &min); + CU_ASSERT_EQUAL(2, res); + + /* Rectangle intersection - partial + * rectangle intersects along L but outside */ + guac_common_rect_init(&rect, 5, 10, 5, 5); + res = guac_common_rect_intersects(&rect, &min); + CU_ASSERT_EQUAL(1, res); + + /* Rectangle intersection - complete + * rectangle intersects along LR but rest is inside */ + guac_common_rect_init(&rect, 15, 15, 5, 5); + res = guac_common_rect_intersects(&rect, &min); + CU_ASSERT_EQUAL(2, res); + + /* Rectangle intersection - partial + * rectangle intersects along R but rest is outside */ + guac_common_rect_init(&rect, 20, 10, 5, 5); + res = guac_common_rect_intersects(&rect, &min); + CU_ASSERT_EQUAL(1, res); + + /* Rectangle intersection - partial + * rectangle encloses min; which is a partial intersection */ + guac_common_rect_init(&rect, 5, 5, 20, 20); + res = guac_common_rect_intersects(&rect, &min); + CU_ASSERT_EQUAL(1, res); + +} + diff --git a/src/common/tests/string/count_occurrences.c b/src/common/tests/string/count_occurrences.c new file mode 100644 index 00000000..0c7d01de --- /dev/null +++ b/src/common/tests/string/count_occurrences.c @@ -0,0 +1,33 @@ +/* + * 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/string.h" + +#include + +/** + * Test which verifies that guac_count_occurrences() counts the number of + * occurrences of an arbitrary character within a given string. + */ +void test_string__guac_count_occurrences() { + CU_ASSERT_EQUAL(4, guac_count_occurrences("this is a test string", 's')); + CU_ASSERT_EQUAL(3, guac_count_occurrences("this is a test string", 'i')); + CU_ASSERT_EQUAL(0, guac_count_occurrences("", 's')); +} + diff --git a/tests/common/guac_string.c b/src/common/tests/string/split.c similarity index 79% rename from tests/common/guac_string.c rename to src/common/tests/string/split.c index 543a41b8..36f8f191 100644 --- a/tests/common/guac_string.c +++ b/src/common/tests/string/split.c @@ -17,26 +17,20 @@ * under the License. */ -#include "config.h" - -#include "common_suite.h" #include "common/string.h" +#include + #include -#include -void test_guac_string() { - - char** tokens; - - /* Test occurrence counting */ - CU_ASSERT_EQUAL(4, guac_count_occurrences("this is a test string", 's')); - CU_ASSERT_EQUAL(3, guac_count_occurrences("this is a test string", 'i')); - CU_ASSERT_EQUAL(0, guac_count_occurrences("", 's')); +/** + * Test which verifies that guac_split() splits a string on occurrences of a + * given character. + */ +void test_string__split() { /* Split test string */ - tokens = guac_split("this is a test string", ' '); - + char** tokens = guac_split("this is a test string", ' '); CU_ASSERT_PTR_NOT_NULL(tokens); /* Check resulting tokens */ @@ -57,7 +51,6 @@ void test_guac_string() { CU_ASSERT_PTR_NULL(tokens[5]); - /* Clean up */ free(tokens[0]); free(tokens[1]); diff --git a/src/guacd/.gitignore b/src/guacd/.gitignore index 558c3194..4f9ae75e 100644 --- a/src/guacd/.gitignore +++ b/src/guacd/.gitignore @@ -13,38 +13,3 @@ guacd.exe man/guacd.8 man/guacd.conf.5 -# Object code -*.o -*.so -*.lo -*.la - -# Backup files -*~ - -# Release files -*.tar.gz - -# Files currently being edited by vim or vi -*.swp - -# automake/autoconf -.deps/ -.libs/ -Makefile -Makefile.in -aclocal.m4 -autom4te.cache/ -m4/ -config.guess -config.log -config.status -config.sub -configure -depcomp -install-sh -libtool -ltmain.sh -missing - - diff --git a/src/guacd/Makefile.am b/src/guacd/Makefile.am index 4dcd3d22..356f72f2 100644 --- a/src/guacd/Makefile.am +++ b/src/guacd/Makefile.am @@ -16,6 +16,12 @@ # specific language governing permissions and limitations # under the License. # +# NOTE: Parts of this file (Makefile.am) are automatically transcluded verbatim +# into Makefile.in. Though the build system (GNU Autotools) automatically adds +# its own license boilerplate to the generated Makefile.in, that boilerplate +# does not apply to the transcluded portions of Makefile.am which are licensed +# to you by the ASF under the Apache License, Version 2.0, as described above. +# AUTOMAKE_OPTIONS = foreign diff --git a/src/guacenc/Makefile.am b/src/guacenc/Makefile.am index a505ff5b..8debd632 100644 --- a/src/guacenc/Makefile.am +++ b/src/guacenc/Makefile.am @@ -16,6 +16,12 @@ # specific language governing permissions and limitations # under the License. # +# NOTE: Parts of this file (Makefile.am) are automatically transcluded verbatim +# into Makefile.in. Though the build system (GNU Autotools) automatically adds +# its own license boilerplate to the generated Makefile.in, that boilerplate +# does not apply to the transcluded portions of Makefile.am which are licensed +# to you by the ASF under the Apache License, Version 2.0, as described above. +# AUTOMAKE_OPTIONS = foreign diff --git a/src/guacenc/guacenc.c b/src/guacenc/guacenc.c index e8442fd9..093623a2 100644 --- a/src/guacenc/guacenc.c +++ b/src/guacenc/guacenc.c @@ -75,8 +75,10 @@ int main(int argc, char* argv[]) { guacenc_log(GUAC_LOG_INFO, "Guacamole video encoder (guacenc) " "version " VERSION); +#if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(58, 10, 100) /* Prepare libavcodec */ avcodec_register_all(); +#endif /* Track number of overall failures */ int total_files = argc - optind; diff --git a/src/guaclog/Makefile.am b/src/guaclog/Makefile.am index 6acced30..af5c5a76 100644 --- a/src/guaclog/Makefile.am +++ b/src/guaclog/Makefile.am @@ -16,6 +16,12 @@ # specific language governing permissions and limitations # under the License. # +# NOTE: Parts of this file (Makefile.am) are automatically transcluded verbatim +# into Makefile.in. Though the build system (GNU Autotools) automatically adds +# its own license boilerplate to the generated Makefile.in, that boilerplate +# does not apply to the transcluded portions of Makefile.am which are licensed +# to you by the ASF under the Apache License, Version 2.0, as described above. +# AUTOMAKE_OPTIONS = foreign diff --git a/src/libguac/.gitignore b/src/libguac/.gitignore index 95b69b0d..9adc856b 100644 --- a/src/libguac/.gitignore +++ b/src/libguac/.gitignore @@ -1,41 +1,5 @@ -# Object code -*.o -*.so -*.lo -*.la - -# gcov files -*.gcda -*.gcov -*.gcno - -# Backup files -*~ - -# Release files -*.tar.gz - -# Files currently being edited by vim or vi -*.swp - -# automake/autoconf -.deps/ -.libs/ -Makefile -Makefile.in -aclocal.m4 -autom4te.cache/ -m4/* -!README -config.guess -config.log -config.status -config.sub -configure -depcomp -install-sh -libtool -ltmain.sh -missing +# Auto-generated test runner and binary +_generated_runner.c +test_libguac diff --git a/src/libguac/Makefile.am b/src/libguac/Makefile.am index 5123121d..a6240802 100644 --- a/src/libguac/Makefile.am +++ b/src/libguac/Makefile.am @@ -16,11 +16,18 @@ # specific language governing permissions and limitations # under the License. # +# NOTE: Parts of this file (Makefile.am) are automatically transcluded verbatim +# into Makefile.in. Though the build system (GNU Autotools) automatically adds +# its own license boilerplate to the generated Makefile.in, that boilerplate +# does not apply to the transcluded portions of Makefile.am which are licensed +# to you by the ASF under the Apache License, Version 2.0, as described above. +# AUTOMAKE_OPTIONS = foreign ACLOCAL_AMFLAGS = -I m4 lib_LTLIBRARIES = libguac.la +SUBDIRS = . tests libguacincdir = $(includedir)/guacamole @@ -47,6 +54,7 @@ libguacinc_HEADERS = \ guacamole/pool.h \ guacamole/pool-types.h \ guacamole/protocol.h \ + guacamole/protocol-constants.h \ guacamole/protocol-types.h \ guacamole/socket-constants.h \ guacamole/socket.h \ @@ -54,6 +62,7 @@ libguacinc_HEADERS = \ guacamole/socket-types.h \ guacamole/stream.h \ guacamole/stream-types.h \ + guacamole/string.h \ guacamole/timestamp.h \ guacamole/timestamp-types.h \ guacamole/unicode.h \ @@ -89,6 +98,7 @@ libguac_la_SOURCES = \ socket-fd.c \ socket-nest.c \ socket-tee.c \ + string.c \ timestamp.c \ unicode.c \ user.c \ @@ -115,10 +125,10 @@ libguacinc_HEADERS += guacamole/socket-wsa.h endif libguac_la_CFLAGS = \ - -Werror -Wall -pedantic -I$(srcdir)/guacamole + -Werror -Wall -pedantic libguac_la_LDFLAGS = \ - -version-info 16:0:0 \ + -version-info 17:0:0 \ -no-undefined \ @CAIRO_LIBS@ \ @DL_LIBS@ \ diff --git a/src/libguac/audio.c b/src/libguac/audio.c index cf0187a2..cc577c71 100644 --- a/src/libguac/audio.c +++ b/src/libguac/audio.c @@ -19,14 +19,13 @@ #include "config.h" +#include "guacamole/audio.h" +#include "guacamole/client.h" +#include "guacamole/protocol.h" +#include "guacamole/stream.h" +#include "guacamole/user.h" #include "raw_encoder.h" -#include -#include -#include -#include -#include - #include #include diff --git a/src/libguac/client.c b/src/libguac/client.c index 4f3051d9..64404534 100644 --- a/src/libguac/client.c +++ b/src/libguac/client.c @@ -19,20 +19,21 @@ #include "config.h" -#include "client.h" #include "encode-jpeg.h" #include "encode-png.h" #include "encode-webp.h" -#include "error.h" +#include "guacamole/client.h" +#include "guacamole/error.h" +#include "guacamole/layer.h" +#include "guacamole/plugin.h" +#include "guacamole/pool.h" +#include "guacamole/protocol.h" +#include "guacamole/socket.h" +#include "guacamole/stream.h" +#include "guacamole/string.h" +#include "guacamole/timestamp.h" +#include "guacamole/user.h" #include "id.h" -#include "layer.h" -#include "pool.h" -#include "plugin.h" -#include "protocol.h" -#include "socket.h" -#include "stream.h" -#include "timestamp.h" -#include "user.h" #include #include @@ -441,8 +442,13 @@ int guac_client_load_plugin(guac_client* client, const char* protocol) { } alias; /* Add protocol and .so suffix to protocol_lib */ - strncat(protocol_lib, protocol, GUAC_PROTOCOL_NAME_LIMIT-1); - strcat(protocol_lib, GUAC_PROTOCOL_LIBRARY_SUFFIX); + guac_strlcat(protocol_lib, protocol, sizeof(protocol_lib)); + if (guac_strlcat(protocol_lib, GUAC_PROTOCOL_LIBRARY_SUFFIX, + sizeof(protocol_lib)) >= sizeof(protocol_lib)) { + guac_error = GUAC_STATUS_NO_MEMORY; + guac_error_message = "Protocol name is too long"; + return -1; + } /* Load client plugin */ client_plugin_handle = dlopen(protocol_lib, RTLD_LAZY); @@ -510,6 +516,26 @@ int guac_client_get_processing_lag(guac_client* client) { } +void guac_client_stream_argv(guac_client* client, guac_socket* socket, + const char* mimetype, const char* name, const char* value) { + + /* Allocate new stream for argument value */ + guac_stream* stream = guac_client_alloc_stream(client); + + /* Declare stream as containing connection parameter data */ + guac_protocol_send_argv(socket, stream, mimetype, name); + + /* Write parameter data */ + guac_protocol_send_blobs(socket, stream, value, strlen(value)); + + /* Terminate stream */ + guac_protocol_send_end(socket, stream); + + /* Free allocated stream */ + guac_client_free_stream(client, stream); + +} + void guac_client_stream_png(guac_client* client, guac_socket* socket, guac_composite_mode mode, const guac_layer* layer, int x, int y, cairo_surface_t* surface) { diff --git a/src/libguac/encode-jpeg.c b/src/libguac/encode-jpeg.c index 5a869c72..8e145d86 100644 --- a/src/libguac/encode-jpeg.c +++ b/src/libguac/encode-jpeg.c @@ -20,10 +20,10 @@ #include "config.h" #include "encode-jpeg.h" -#include "error.h" +#include "guacamole/error.h" +#include "guacamole/protocol.h" +#include "guacamole/stream.h" #include "palette.h" -#include "protocol.h" -#include "stream.h" #include #include @@ -59,7 +59,7 @@ typedef struct guac_jpeg_destination_mgr { /** * The output buffer. */ - unsigned char buffer[6048]; + unsigned char buffer[GUAC_PROTOCOL_BLOB_MAX_LENGTH]; } guac_jpeg_destination_mgr; diff --git a/src/libguac/encode-jpeg.h b/src/libguac/encode-jpeg.h index d791ea69..05cf2cad 100644 --- a/src/libguac/encode-jpeg.h +++ b/src/libguac/encode-jpeg.h @@ -22,8 +22,8 @@ #include "config.h" -#include "socket.h" -#include "stream.h" +#include "guacamole/socket.h" +#include "guacamole/stream.h" #include diff --git a/src/libguac/encode-png.c b/src/libguac/encode-png.c index 78795577..20d2f5de 100644 --- a/src/libguac/encode-png.c +++ b/src/libguac/encode-png.c @@ -20,10 +20,10 @@ #include "config.h" #include "encode-png.h" -#include "error.h" +#include "guacamole/error.h" +#include "guacamole/protocol.h" +#include "guacamole/stream.h" #include "palette.h" -#include "protocol.h" -#include "stream.h" #include #include @@ -56,7 +56,7 @@ typedef struct guac_png_write_state { /** * Buffer of pending PNG data. */ - char buffer[6048]; + char buffer[GUAC_PROTOCOL_BLOB_MAX_LENGTH]; /** * The number of bytes currently stored in the buffer. diff --git a/src/libguac/encode-png.h b/src/libguac/encode-png.h index 916493c4..222c50ef 100644 --- a/src/libguac/encode-png.h +++ b/src/libguac/encode-png.h @@ -22,8 +22,8 @@ #include "config.h" -#include "socket.h" -#include "stream.h" +#include "guacamole/socket.h" +#include "guacamole/stream.h" #include diff --git a/src/libguac/encode-webp.c b/src/libguac/encode-webp.c index 88534db5..43c5a00c 100644 --- a/src/libguac/encode-webp.c +++ b/src/libguac/encode-webp.c @@ -20,10 +20,10 @@ #include "config.h" #include "encode-webp.h" -#include "error.h" +#include "guacamole/error.h" +#include "guacamole/protocol.h" +#include "guacamole/stream.h" #include "palette.h" -#include "protocol.h" -#include "stream.h" #include #include @@ -52,7 +52,7 @@ typedef struct guac_webp_stream_writer { /** * Buffer of pending WebP data. */ - char buffer[6048]; + char buffer[GUAC_PROTOCOL_BLOB_MAX_LENGTH]; /** * The number of bytes currently stored in the buffer. diff --git a/src/libguac/encode-webp.h b/src/libguac/encode-webp.h index 8971765c..5347f6d9 100644 --- a/src/libguac/encode-webp.h +++ b/src/libguac/encode-webp.h @@ -22,8 +22,8 @@ #include "config.h" -#include "socket.h" -#include "stream.h" +#include "guacamole/socket.h" +#include "guacamole/stream.h" #include diff --git a/src/libguac/error.c b/src/libguac/error.c index 95942e81..5561b178 100644 --- a/src/libguac/error.c +++ b/src/libguac/error.c @@ -19,7 +19,7 @@ #include "config.h" -#include "error.h" +#include "guacamole/error.h" #include #include diff --git a/src/libguac/guacamole/client.h b/src/libguac/guacamole/client.h index 2d01573c..88d1f416 100644 --- a/src/libguac/guacamole/client.h +++ b/src/libguac/guacamole/client.h @@ -548,6 +548,33 @@ int guac_client_load_plugin(guac_client* client, const char* protocol); */ int guac_client_get_processing_lag(guac_client* client); +/** + * Streams the given connection parameter value over an argument value stream + * ("argv" instruction), exposing the current value of the named connection + * parameter to all users of the given client. The argument value stream will + * be automatically allocated and freed. + * + * @param client + * The Guacamole client for which the argument value stream should be + * allocated. + * + * @param socket + * The socket over which instructions associated with the argument value + * stream should be sent. + * + * @param mimetype + * The mimetype of the data within the connection parameter value being + * sent. + * + * @param name + * The name of the connection parameter being sent. + * + * @param value + * The current value of the connection parameter being sent. + */ +void guac_client_stream_argv(guac_client* client, guac_socket* socket, + const char* mimetype, const char* name, const char* value); + /** * Streams the image data of the given surface over an image stream ("img" * instruction) as PNG-encoded data. The image stream will be automatically diff --git a/src/libguac/guacamole/protocol-constants.h b/src/libguac/guacamole/protocol-constants.h new file mode 100644 index 00000000..afa67bb9 --- /dev/null +++ b/src/libguac/guacamole/protocol-constants.h @@ -0,0 +1,53 @@ +/* + * 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. + */ + +#ifndef GUAC_PROTOCOL_CONSTANTS_H +#define GUAC_PROTOCOL_CONSTANTS_H + +/** + * Constants related to the Guacamole protocol. + * + * @file protocol-constants.h + */ + +/** + * This defines the overall protocol version that this build of libguac + * supports. The protocol version is used to provide compatibility between + * potentially different versions of Guacamole server and clients. The + * version number is a MAJOR_MINOR_PATCH version that matches the versioning + * used throughout the components of the Guacamole project. This version + * will not necessarily increment with the other components, unless additional + * functionality is introduced that affects compatibility. + * + * This version is passed by the __guac_protocol_send_args() function from the + * server to the client during the client/server handshake. + */ +#define GUACAMOLE_PROTOCOL_VERSION "VERSION_1_1_0" + +/** + * The maximum number of bytes that should be sent in any one blob instruction + * to ensure the instruction does not exceed the maximum allowed instruction + * size. + * + * @see GUAC_INSTRUCTION_MAX_LENGTH + */ +#define GUAC_PROTOCOL_BLOB_MAX_LENGTH 6048 + +#endif + diff --git a/src/libguac/guacamole/protocol.h b/src/libguac/guacamole/protocol.h index fd824af6..f4e02880 100644 --- a/src/libguac/guacamole/protocol.h +++ b/src/libguac/guacamole/protocol.h @@ -30,6 +30,7 @@ #include "layer-types.h" #include "object-types.h" +#include "protocol-constants.h" #include "protocol-types.h" #include "socket-types.h" #include "stream-types.h" @@ -214,6 +215,13 @@ int guac_protocol_send_mouse(guac_socket* socket, int x, int y, * If an error occurs sending the instruction, a non-zero value is * returned, and guac_error is set appropriately. * + * @deprecated + * The "nest" instruction and the corresponding guac_socket + * implementation are no longer necessary, having been replaced by + * the streaming instructions ("blob", "ack", "end"). Code using nested + * sockets or the "nest" instruction should instead write to a normal + * socket directly. + * * @param socket The guac_socket connection to use. * @param index The integer index of the stram to send the protocol * data over. @@ -427,6 +435,37 @@ int guac_protocol_send_pipe(guac_socket* socket, const guac_stream* stream, int guac_protocol_send_blob(guac_socket* socket, const guac_stream* stream, const void* data, int count); +/** + * Sends a series of blob instructions, splitting the given data across the + * number of instructions required to ensure the size of each blob does not + * exceed GUAC_PROTOCOL_BLOB_MAX_LENGTH. If the size of data provided is zero, + * no blob instructions are sent. + * + * If an error occurs sending any blob instruction, a non-zero value is + * returned, guac_error is set appropriately, and no further blobs are sent. + * + * @see GUAC_PROTOCOL_BLOB_MAX_LENGTH + * + * @param socket + * The guac_socket connection to use to send the blob instructions. + * + * @param stream + * The stream to associate with each blob sent. + * + * @param data + * The data which should be sent using the required number of blob + * instructions. + * + * @param count + * The number of bytes within the given buffer of data that must be + * written. + * + * @return + * Zero on success, non-zero on error. + */ +int guac_protocol_send_blobs(guac_socket* socket, const guac_stream* stream, + const void* data, int count); + /** * Sends an end instruction over the given guac_socket connection. * @@ -911,6 +950,31 @@ int guac_protocol_send_size(guac_socket* socket, const guac_layer* layer, /* TEXT INSTRUCTIONS */ +/** + * Sends an argv instruction over the given guac_socket connection. + * + * If an error occurs sending the instruction, a non-zero value is + * returned, and guac_error is set appropriately. + * + * @param socket + * The guac_socket connection to use to send the connection parameter + * value. + * + * @param stream + * The stream to use to send the connection parameter value. + * + * @param mimetype + * The mimetype of the connection parameter value being sent. + * + * @param name + * The name of the connection parameter whose current value is being sent. + * + * @return + * Zero on success, non-zero on error. + */ +int guac_protocol_send_argv(guac_socket* socket, guac_stream* stream, + const char* mimetype, const char* name); + /** * Sends a clipboard instruction over the given guac_socket connection. * diff --git a/src/libguac/guacamole/socket.h b/src/libguac/guacamole/socket.h index e83a4ebc..2da697fe 100644 --- a/src/libguac/guacamole/socket.h +++ b/src/libguac/guacamole/socket.h @@ -180,6 +180,13 @@ guac_socket* guac_socket_open(int fd); * If an error occurs while allocating the guac_socket object, NULL is returned, * and guac_error is set appropriately. * + * @deprecated + * The "nest" instruction and the corresponding guac_socket + * implementation are no longer necessary, having been replaced by + * the streaming instructions ("blob", "ack", "end"). Code using nested + * sockets or the "nest" instruction should instead write to a normal + * socket directly. + * * @param parent The guac_socket this new guac_socket should write nest * instructions to. * @param index The stream index to use for the written nest instructions. diff --git a/src/libguac/guacamole/string.h b/src/libguac/guacamole/string.h new file mode 100644 index 00000000..5e56e330 --- /dev/null +++ b/src/libguac/guacamole/string.h @@ -0,0 +1,161 @@ +/* + * 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. + */ + +#ifndef GUAC_STRING_H +#define GUAC_STRING_H + +/** + * Provides convenience functions for manipulating strings. + * + * @file string.h + */ + +#include +#include + +/** + * Copies a limited number of bytes from the given source string to the given + * destination buffer. The resulting buffer will always be null-terminated, + * even if doing so means that the intended string is truncated, unless the + * destination buffer has no space available at all. As this function always + * returns the length of the string it tried to create (the length of the + * source string), whether truncation has occurred can be detected by comparing + * the return value against the size of the destination buffer. If the value + * returned is greater than or equal to the size of the destination buffer, then + * the string has been truncated. + * + * The source and destination buffers MAY NOT overlap. + * + * @param dest + * The buffer which should receive the contents of the source string. This + * buffer will always be null terminated unless zero bytes are available + * within the buffer. + * + * @param src + * The source string to copy into the destination buffer. This string MUST + * be null terminated. + * + * @param n + * The number of bytes available within the destination buffer. If this + * value is zero, no bytes will be written to the destination buffer, and + * the destination buffer may not be null terminated. In all other cases, + * the destination buffer will always be null terminated, even if doing + * so means that the copied data from the source string will be truncated. + * + * @return + * The length of the copied string (the source string) in bytes, excluding + * the null terminator. + */ +size_t guac_strlcpy(char* restrict dest, const char* restrict src, size_t n); + +/** + * Appends the given source string after the end of the given destination + * string, writing at most the given number of bytes. Both the source and + * destination strings MUST be null-terminated. The resulting buffer will + * always be null-terminated, even if doing so means that the intended string + * is truncated, unless the destination buffer has no space available at all. + * As this function always returns the length of the string it tried to create + * (the length of destination and source strings added together), whether + * truncation has occurred can be detected by comparing the return value + * against the size of the destination buffer. If the value returned is greater + * than or equal to the size of the destination buffer, then the string has + * been truncated. + * + * The source and destination buffers MAY NOT overlap. + * + * @param dest + * The buffer which should be appended with the contents of the source + * string. This buffer MUST already be null-terminated and will always be + * null-terminated unless zero bytes are available within the buffer. + * + * As a safeguard against incorrectly-written code, in the event that the + * destination buffer is not null-terminated, this function will still stop + * before overrunning the buffer, instead behaving as if the length of the + * string in the buffer is exactly the size of the buffer. The destination + * buffer will remain untouched (and unterminated) in this case. + * + * @param src + * The source string to append to the the destination buffer. This string + * MUST be null-terminated. + * + * @param n + * The number of bytes available within the destination buffer. If this + * value is not greater than zero, no bytes will be written to the + * destination buffer, and the destination buffer may not be + * null-terminated. In all other cases, the destination buffer will always + * be null-terminated, even if doing so means that the copied data from the + * source string will be truncated. + * + * @return + * The length of the string this function tried to create (the lengths of + * the source and destination strings added together) in bytes, excluding + * the null terminator. + */ +size_t guac_strlcat(char* restrict dest, const char* restrict src, size_t n); + +/** + * Concatenates each of the given strings, separated by the given delimiter, + * storing the result within a destination buffer. The number of bytes written + * will be no more than the given number of bytes, and the destination buffer + * is guaranteed to be null-terminated, even if doing so means that one or more + * of the intended strings are truncated or omitted from the end of the result, + * unless the destination buffer has no space available at all. As this + * function always returns the length of the string it tried to create (the + * length of all source strings and all delimiters added together), whether + * truncation has occurred can be detected by comparing the return value + * against the size of the destination buffer. If the value returned is greater + * than or equal to the size of the destination buffer, then the string has + * been truncated. + * + * The source strings, delimiter string, and destination buffer MAY NOT + * overlap. + * + * @param dest + * The buffer which should receive the result of joining the given strings. + * This buffer will always be null terminated unless zero bytes are + * available within the buffer. + * + * @param elements + * The elements to concatenate together, separated by the given delimiter. + * Each element MUST be null-terminated. + * + * @param nmemb + * The number of elements within the elements array. + * + * @param delim + * The delimiter to include between each pair of elements. + * + * @param n + * The number of bytes available within the destination buffer. If this + * value is not greater than zero, no bytes will be written to the + * destination buffer, and the destination buffer may not be null + * terminated. In all other cases, the destination buffer will always be + * null terminated, even if doing so means that the result will be + * truncated. + * + * @return + * The length of the string this function tried to create (the length of + * all source strings and all delimiters added together) in bytes, + * excluding the null terminator. + */ +size_t guac_strljoin(char* restrict dest, const char* restrict const* elements, + int nmemb, const char* restrict delim, size_t n); + +#endif + diff --git a/src/libguac/guacamole/user-fntypes.h b/src/libguac/guacamole/user-fntypes.h index 13256c56..d8689322 100644 --- a/src/libguac/guacamole/user-fntypes.h +++ b/src/libguac/guacamole/user-fntypes.h @@ -237,6 +237,36 @@ typedef int guac_user_file_handler(guac_user* user, guac_stream* stream, typedef int guac_user_pipe_handler(guac_user* user, guac_stream* stream, char* mimetype, char* name); +/** + * Handler for Guacamole argument value (argv) streams received from a user. + * Argument value streams are real-time revisions to the connection parameters + * of an in-progress connection. Each such argument value stream begins when + * the user sends a "argv" instruction. To handle received data along this + * stream, implementations of this handler must assign blob and end handlers to + * the given stream object. + * + * @param user + * The user that opened the argument value stream. + * + * @param stream + * The stream object allocated by libguac to represent the argument value + * stream opened by the user. + * + * @param mimetype + * The mimetype of the data that will be sent along the stream. + * + * @param name + * The name of the connection parameter being updated. It is up to the + * implementation of this handler to decide whether and how to update a + * connection parameter. + * + * @return + * Zero if the opening of the argument value stream has been handled + * successfully, or non-zero if an error occurs. + */ +typedef int guac_user_argv_handler(guac_user* user, guac_stream* stream, + char* mimetype, char* name); + /** * Handler for Guacamole stream blobs. Each blob originates from a "blob" * instruction which was associated with a previously-created stream. diff --git a/src/libguac/guacamole/user.h b/src/libguac/guacamole/user.h index f89f8077..702160fa 100644 --- a/src/libguac/guacamole/user.h +++ b/src/libguac/guacamole/user.h @@ -88,6 +88,13 @@ struct guac_user_info { * stated resolution of the display size request is recommended. */ int optimal_resolution; + + /** + * The timezone of the remote system. If the client does not provide + * a specific timezone then this will be NULL. The format of the timezone + * is the standard tzdata naming convention. + */ + const char* timezone; }; @@ -475,6 +482,27 @@ struct guac_user { */ guac_user_audio_handler* audio_handler; + /** + * Handler for argv events (updates to the connection parameters of an + * in-progress connection) sent by the Guacamole web-client. + * + * The handler takes a guac_stream which contains the stream index and + * will persist through the duration of the transfer, the mimetype of + * the data being transferred, and the argument (connection parameter) + * name. + * + * Example: + * @code + * int argv_handler(guac_user* user, guac_stream* stream, + * char* mimetype, char* name); + * + * int guac_user_init(guac_user* user, int argc, char** argv) { + * user->argv_handler = argv_handler; + * } + * @endcode + */ + guac_user_argv_handler* argv_handler; + }; /** @@ -519,7 +547,7 @@ int guac_user_handle_connection(guac_user* user, int usec_timeout); /** * Call the appropriate handler defined by the given user for the given * instruction. A comparison is made between the instruction opcode and the - * initial handler lookup table defined in user-handlers.c. The intial handlers + * initial handler lookup table defined in user-handlers.c. The initial handlers * will in turn call the user's handler (if defined). * * @param user @@ -648,6 +676,32 @@ guac_object* guac_user_alloc_object(guac_user* user); */ void guac_user_free_object(guac_user* user, guac_object* object); +/** + * Streams the given connection parameter value over an argument value stream + * ("argv" instruction), exposing the current value of the named connection + * parameter to the given user. The argument value stream will be automatically + * allocated and freed. + * + * @param user + * The Guacamole user who should receive the connection parameter value. + * + * @param socket + * The socket over which instructions associated with the argument value + * stream should be sent. + * + * @param mimetype + * The mimetype of the data within the connection parameter value being + * sent. + * + * @param name + * The name of the connection parameter being sent. + * + * @param value + * The current value of the connection parameter being sent. + */ +void guac_user_stream_argv(guac_user* user, guac_socket* socket, + const char* mimetype, const char* name, const char* value); + /** * Streams the image data of the given surface over an image stream ("img" * instruction) as PNG-encoded data. The image stream will be automatically diff --git a/src/libguac/id.c b/src/libguac/id.c index 56ef23c2..27a714c0 100644 --- a/src/libguac/id.c +++ b/src/libguac/id.c @@ -19,8 +19,8 @@ #include "config.h" +#include "guacamole/error.h" #include "id.h" -#include "error.h" #ifdef HAVE_OSSP_UUID_H #include diff --git a/src/libguac/parser.c b/src/libguac/parser.c index 799b48ab..9645a2a3 100644 --- a/src/libguac/parser.c +++ b/src/libguac/parser.c @@ -19,10 +19,10 @@ #include "config.h" -#include "error.h" -#include "parser.h" -#include "socket.h" -#include "unicode.h" +#include "guacamole/error.h" +#include "guacamole/parser.h" +#include "guacamole/socket.h" +#include "guacamole/unicode.h" #include #include diff --git a/src/libguac/pool.c b/src/libguac/pool.c index 363c239d..9db43ada 100644 --- a/src/libguac/pool.c +++ b/src/libguac/pool.c @@ -19,7 +19,7 @@ #include "config.h" -#include "pool.h" +#include "guacamole/pool.h" #include diff --git a/src/libguac/protocol.c b/src/libguac/protocol.c index dba9c56e..ff73a558 100644 --- a/src/libguac/protocol.c +++ b/src/libguac/protocol.c @@ -19,14 +19,14 @@ #include "config.h" -#include "error.h" -#include "layer.h" -#include "object.h" +#include "guacamole/error.h" +#include "guacamole/layer.h" +#include "guacamole/object.h" +#include "guacamole/protocol.h" +#include "guacamole/socket.h" +#include "guacamole/stream.h" +#include "guacamole/unicode.h" #include "palette.h" -#include "protocol.h" -#include "socket.h" -#include "stream.h" -#include "unicode.h" #include @@ -93,6 +93,11 @@ static int __guac_protocol_send_args(guac_socket* socket, const char** args) { int i; if (guac_socket_write_string(socket, "4.args")) return -1; + + /* Send protocol version ahead of other args. */ + if (guac_socket_write_string(socket, ",") + || __guac_socket_write_length_string(socket, GUACAMOLE_PROTOCOL_VERSION)) + return -1; for (i=0; args[i] != NULL; i++) { @@ -120,6 +125,26 @@ int guac_protocol_send_args(guac_socket* socket, const char** args) { } +int guac_protocol_send_argv(guac_socket* socket, guac_stream* stream, + const char* mimetype, const char* name) { + + int ret_val; + + guac_socket_instruction_begin(socket); + ret_val = + guac_socket_write_string(socket, "4.argv,") + || __guac_socket_write_length_int(socket, stream->index) + || guac_socket_write_string(socket, ",") + || __guac_socket_write_length_string(socket, mimetype) + || guac_socket_write_string(socket, ",") + || __guac_socket_write_length_string(socket, name) + || guac_socket_write_string(socket, ";"); + + guac_socket_instruction_end(socket); + return ret_val; + +} + int guac_protocol_send_arc(guac_socket* socket, const guac_layer* layer, int x, int y, int radius, double startAngle, double endAngle, int negative) { @@ -190,6 +215,33 @@ int guac_protocol_send_blob(guac_socket* socket, const guac_stream* stream, } +int guac_protocol_send_blobs(guac_socket* socket, const guac_stream* stream, + const void* data, int count) { + + int ret_val = 0; + + /* Send blob instructions while data remains and instructions are being + * sent successfully */ + while (count > 0 && ret_val == 0) { + + /* Limit blob size to maximum allowed */ + int blob_size = count; + if (blob_size > GUAC_PROTOCOL_BLOB_MAX_LENGTH) + blob_size = GUAC_PROTOCOL_BLOB_MAX_LENGTH; + + /* Send next blob of data */ + ret_val = guac_protocol_send_blob(socket, stream, data, blob_size); + + /* Advance to next blob */ + data = (const char*) data + blob_size; + count -= blob_size; + + } + + return ret_val; + +} + int guac_protocol_send_body(guac_socket* socket, const guac_object* object, const guac_stream* stream, const char* mimetype, const char* name) { diff --git a/src/libguac/raw_encoder.c b/src/libguac/raw_encoder.c index 1dd03cc4..888bde79 100644 --- a/src/libguac/raw_encoder.c +++ b/src/libguac/raw_encoder.c @@ -19,15 +19,13 @@ #include "config.h" -#include "audio.h" +#include "guacamole/audio.h" +#include "guacamole/client.h" +#include "guacamole/protocol.h" +#include "guacamole/socket.h" +#include "guacamole/user.h" #include "raw_encoder.h" -#include -#include -#include -#include -#include - #include #include #include @@ -123,25 +121,8 @@ static void raw_encoder_flush_handler(guac_audio_stream* audio) { guac_socket* socket = audio->client->socket; guac_stream* stream = audio->stream; - unsigned char* current = state->buffer; - int remaining = state->written; - /* Flush all data in buffer as blobs */ - while (remaining > 0) { - - /* Determine size of blob to be written */ - int chunk_size = remaining; - if (chunk_size > 6048) - chunk_size = 6048; - - /* Send audio data */ - guac_protocol_send_blob(socket, stream, current, chunk_size); - - /* Advance to next blob */ - current += chunk_size; - remaining -= chunk_size; - - } + guac_protocol_send_blobs(socket, stream, state->buffer, state->written); /* All data has been flushed */ state->written = 0; diff --git a/src/libguac/raw_encoder.h b/src/libguac/raw_encoder.h index b4050406..c3af151c 100644 --- a/src/libguac/raw_encoder.h +++ b/src/libguac/raw_encoder.h @@ -23,7 +23,7 @@ #include "config.h" -#include "audio.h" +#include "guacamole/audio.h" /** * The number of bytes to send in each audio blob. diff --git a/src/libguac/socket-broadcast.c b/src/libguac/socket-broadcast.c index a3f17fd0..f551e817 100644 --- a/src/libguac/socket-broadcast.c +++ b/src/libguac/socket-broadcast.c @@ -19,10 +19,10 @@ #include "config.h" -#include "client.h" -#include "error.h" -#include "socket.h" -#include "user.h" +#include "guacamole/client.h" +#include "guacamole/error.h" +#include "guacamole/socket.h" +#include "guacamole/user.h" #include #include diff --git a/src/libguac/socket-fd.c b/src/libguac/socket-fd.c index 34898524..742cc35d 100644 --- a/src/libguac/socket-fd.c +++ b/src/libguac/socket-fd.c @@ -19,8 +19,8 @@ #include "config.h" -#include "error.h" -#include "socket.h" +#include "guacamole/error.h" +#include "guacamole/socket.h" #include "wait-fd.h" #include diff --git a/src/libguac/socket-nest.c b/src/libguac/socket-nest.c index 6acac7d9..8bc9291e 100644 --- a/src/libguac/socket-nest.c +++ b/src/libguac/socket-nest.c @@ -19,86 +19,243 @@ #include "config.h" -#include "protocol.h" -#include "socket.h" -#include "unicode.h" +#include "guacamole/protocol.h" +#include "guacamole/socket.h" +#include "guacamole/unicode.h" #include #include +#include #include -#define GUAC_SOCKET_NEST_BUFFER_SIZE 8192 +/** + * The maximum number of bytes to buffer before sending a "nest" instruction. + * As some of the 8 KB space available for each instruction will be taken up by + * the "nest" opcode and other parameters, and 1 KB will be more than enough + * space for that extra data, this space is reduced to an even 7 KB. + */ +#define GUAC_SOCKET_NEST_BUFFER_SIZE 7168 -typedef struct __guac_socket_nest_data { +/** + * Internal data associated with an open socket which writes via a series of + * "nest" instructions to some underlying, parent socket. + */ +typedef struct guac_socket_nest_data { + /** + * The underlying socket which should be used to write "nest" instructions. + */ guac_socket* parent; - char buffer[GUAC_SOCKET_NEST_BUFFER_SIZE]; + + /** + * The arbitrary index of the nested socket, assigned at time of + * allocation. + */ int index; -} __guac_socket_nest_data; + /** + * The number of bytes currently in the main write buffer. + */ + int written; -ssize_t __guac_socket_nest_write_handler(guac_socket* socket, - const void* buf, size_t count) { + /** + * The main write buffer. Bytes written go here before being flushed + * as nest instructions. Space is included for the null terminator + * required by guac_protocol_send_nest(). + */ + char buffer[GUAC_SOCKET_NEST_BUFFER_SIZE]; - __guac_socket_nest_data* data = (__guac_socket_nest_data*) socket->data; - unsigned char* source = (unsigned char*) buf; + /** + * Lock which is acquired when an instruction is being written, and + * released when the instruction is finished being written. + */ + pthread_mutex_t socket_lock; - /* Current location in destination buffer during copy */ - char* current = data->buffer; + /** + * Lock which protects access to the internal buffer of this socket, + * guaranteeing atomicity of writes and flushes. + */ + pthread_mutex_t buffer_lock; - /* Number of bytes remaining in source buffer */ - int remaining = count; +} guac_socket_nest_data; - /* If we can't actually store that many bytes, reduce number of bytes - * expected to be written */ - if (remaining > GUAC_SOCKET_NEST_BUFFER_SIZE) - remaining = GUAC_SOCKET_NEST_BUFFER_SIZE; +/** + * Flushes the contents of the output buffer of the given socket immediately, + * without first locking access to the output buffer. This function must ONLY + * be called if the buffer lock has already been acquired. + * + * @param socket + * The guac_socket to flush. + * + * @return + * Zero if the flush operation was successful, non-zero otherwise. + */ +static ssize_t guac_socket_nest_flush(guac_socket* socket) { - /* Current offset within destination buffer */ - int offset; + guac_socket_nest_data* data = (guac_socket_nest_data*) socket->data; - /* Number of characters before start of next character */ - int skip = 0; + /* Flush remaining bytes in buffer */ + if (data->written > 0) { - /* Copy UTF-8 characters into buffer */ - for (offset = 0; offset < GUAC_SOCKET_NEST_BUFFER_SIZE; offset++) { + /* Determine length of buffer containing complete UTF-8 characters + * (buffer may end with a partial, multi-byte character) */ + int length = 0; + while (length < data->written) + length += guac_utf8_charsize(*(data->buffer + length)); - /* Get next byte */ - unsigned char c = *source; - remaining--; + /* Add null terminator, preserving overwritten character for later + * restoration (guac_protocol_send_nest() requires null-terminated + * strings) */ + char overwritten = data->buffer[length]; + data->buffer[length] = '\0'; - /* If skipping, then skip */ - if (skip > 0) skip--; + /* Write ALL bytes in buffer as nest instruction */ + int retval = guac_protocol_send_nest(data->parent, data->index, data->buffer); - /* Otherwise, determine next skip value, and increment length */ - else { + /* Restore original value overwritten by null terminator */ + data->buffer[length] = overwritten; - /* Determine skip value (size in bytes of rest of character) */ - skip = guac_utf8_charsize(c) - 1; + if (retval) + return 1; - /* If not enough bytes to complete character, break */ - if (skip > remaining) - break; - - } - - /* Store byte */ - *current = c; - - /* Advance to next character */ - source++; - current++; + /* Shift any remaining data to beginning of buffer */ + memcpy(data->buffer, data->buffer + length, data->written - length); + data->written -= length; } - /* Append null-terminator */ - *current = 0; + return 0; - /* Send nest instruction containing read UTF-8 segment */ - guac_protocol_send_nest(data->parent, data->index, data->buffer); +} - /* Return number of bytes actually written */ - return offset; +/** + * Flushes the internal buffer of the given guac_socket, writing all data + * to the underlying socket using "nest" instructions. + * + * @param socket + * The guac_socket to flush. + * + * @return + * Zero if the flush operation was successful, non-zero otherwise. + */ +static ssize_t guac_socket_nest_flush_handler(guac_socket* socket) { + + int retval; + guac_socket_nest_data* data = (guac_socket_nest_data*) socket->data; + + /* Acquire exclusive access to buffer */ + pthread_mutex_lock(&(data->buffer_lock)); + + /* Flush contents of buffer */ + retval = guac_socket_nest_flush(socket); + + /* Relinquish exclusive access to buffer */ + pthread_mutex_unlock(&(data->buffer_lock)); + + return retval; + +} + +/** + * Writes the contents of the buffer to the output buffer of the given socket, + * flushing the output buffer as necessary, without first locking access to the + * output buffer. This function must ONLY be called if the buffer lock has + * already been acquired. + * + * @param socket + * The guac_socket to write the given buffer to. + * + * @param buf + * The buffer to write to the given socket. + * + * @param count + * The number of bytes in the given buffer. + * + * @return + * The number of bytes written, or a negative value if an error occurs + * during write. + */ +static ssize_t guac_socket_nest_write_buffered(guac_socket* socket, + const void* buf, size_t count) { + + size_t original_count = count; + const char* current = buf; + guac_socket_nest_data* data = (guac_socket_nest_data*) socket->data; + + /* Append to buffer, flush if necessary */ + while (count > 0) { + + int chunk_size; + + /* Calculate space remaining, including one extra byte for the null + * terminator added upon flush */ + int remaining = sizeof(data->buffer) - data->written - 1; + + /* If no space left in buffer, flush and retry */ + if (remaining == 0) { + + /* Abort if error occurs during flush */ + if (guac_socket_nest_flush(socket)) + return -1; + + /* Retry buffer append */ + continue; + + } + + /* Calculate size of chunk to be written to buffer */ + chunk_size = count; + if (chunk_size > remaining) + chunk_size = remaining; + + /* Update output buffer */ + memcpy(data->buffer + data->written, current, chunk_size); + data->written += chunk_size; + + /* Update provided buffer */ + current += chunk_size; + count -= chunk_size; + + } + + /* All bytes have been written, possibly some to the internal buffer */ + return original_count; + +} + +/** + * Appends the provided data to the internal buffer for future writing. The + * actual write attempt will occur only upon flush, or when the internal buffer + * is full. + * + * @param socket + * The guac_socket being write to. + * + * @param buf + * The arbitrary buffer containing the data to be written. + * + * @param count + * The number of bytes contained within the buffer. + * + * @return + * The number of bytes written, or -1 if an error occurs. + */ +static ssize_t guac_socket_nest_write_handler(guac_socket* socket, + const void* buf, size_t count) { + + int retval; + guac_socket_nest_data* data = (guac_socket_nest_data*) socket->data; + + /* Acquire exclusive access to buffer */ + pthread_mutex_lock(&(data->buffer_lock)); + + /* Write provided data to buffer */ + retval = guac_socket_nest_write_buffered(socket, buf, count); + + /* Relinquish exclusive access to buffer */ + pthread_mutex_unlock(&(data->buffer_lock)); + + return retval; } @@ -113,29 +270,63 @@ ssize_t __guac_socket_nest_write_handler(guac_socket* socket, * Zero if the data was successfully freed, non-zero otherwise. This * implementation always succeeds, and will always return zero. */ -static int __guac_socket_nest_free_handler(guac_socket* socket) { +static int guac_socket_nest_free_handler(guac_socket* socket) { /* Free associated data */ - __guac_socket_nest_data* data = (__guac_socket_nest_data*) socket->data; + guac_socket_nest_data* data = (guac_socket_nest_data*) socket->data; free(data); return 0; } +/** + * Acquires exclusive access to the given socket. + * + * @param socket + * The guac_socket to which exclusive access is required. + */ +static void guac_socket_nest_lock_handler(guac_socket* socket) { + + guac_socket_nest_data* data = (guac_socket_nest_data*) socket->data; + + /* Acquire exclusive access to socket */ + pthread_mutex_lock(&(data->socket_lock)); + +} + +/** + * Relinquishes exclusive access to the given socket. + * + * @param socket + * The guac_socket to which exclusive access is no longer required. + */ +static void guac_socket_nest_unlock_handler(guac_socket* socket) { + + guac_socket_nest_data* data = (guac_socket_nest_data*) socket->data; + + /* Relinquish exclusive access to socket */ + pthread_mutex_unlock(&(data->socket_lock)); + +} + guac_socket* guac_socket_nest(guac_socket* parent, int index) { /* Allocate socket and associated data */ guac_socket* socket = guac_socket_alloc(); - __guac_socket_nest_data* data = malloc(sizeof(__guac_socket_nest_data)); + guac_socket_nest_data* data = malloc(sizeof(guac_socket_nest_data)); - /* Store file descriptor as socket data */ + /* Store nested socket details as socket data */ data->parent = parent; + data->index = index; socket->data = data; - /* Set write and free handlers */ - socket->write_handler = __guac_socket_nest_write_handler; - socket->free_handler = __guac_socket_nest_free_handler; + /* Set relevant handlers */ + socket->write_handler = guac_socket_nest_write_handler; + socket->lock_handler = guac_socket_nest_lock_handler; + socket->unlock_handler = guac_socket_nest_unlock_handler; + socket->flush_handler = guac_socket_nest_flush_handler; + socket->free_handler = guac_socket_nest_free_handler; return socket; diff --git a/src/libguac/socket-ssl.c b/src/libguac/socket-ssl.c index 1a631fe1..3daa128e 100644 --- a/src/libguac/socket-ssl.c +++ b/src/libguac/socket-ssl.c @@ -19,9 +19,9 @@ #include "config.h" -#include "error.h" -#include "socket-ssl.h" -#include "socket.h" +#include "guacamole/error.h" +#include "guacamole/socket-ssl.h" +#include "guacamole/socket.h" #include "wait-fd.h" #include diff --git a/src/libguac/socket-tee.c b/src/libguac/socket-tee.c index 3f0fd6bd..cee9108c 100644 --- a/src/libguac/socket-tee.c +++ b/src/libguac/socket-tee.c @@ -19,7 +19,7 @@ #include "config.h" -#include "socket.h" +#include "guacamole/socket.h" #include diff --git a/src/libguac/socket-wsa.c b/src/libguac/socket-wsa.c index 1b59176a..f5602e39 100644 --- a/src/libguac/socket-wsa.c +++ b/src/libguac/socket-wsa.c @@ -17,8 +17,8 @@ * under the License. */ -#include "error.h" -#include "socket.h" +#include "guacamole/error.h" +#include "guacamole/socket.h" #include #include diff --git a/src/libguac/socket.c b/src/libguac/socket.c index 6bc0036b..66442d0c 100644 --- a/src/libguac/socket.c +++ b/src/libguac/socket.c @@ -19,10 +19,10 @@ #include "config.h" -#include "error.h" -#include "protocol.h" -#include "socket.h" -#include "timestamp.h" +#include "guacamole/error.h" +#include "guacamole/protocol.h" +#include "guacamole/socket.h" +#include "guacamole/timestamp.h" #include #include diff --git a/src/libguac/string.c b/src/libguac/string.c new file mode 100644 index 00000000..f05c4c06 --- /dev/null +++ b/src/libguac/string.c @@ -0,0 +1,107 @@ +/* + * 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 "config.h" + +#include +#include + +/** + * Returns the space remaining in a buffer assuming that the given number of + * bytes have already been written. If the number of bytes exceeds the size + * of the buffer, zero is returned. + * + * @param n + * The size of the buffer in bytes. + * + * @param length + * The number of bytes which have been written to the buffer so far. If + * the routine writing the bytes will automatically truncate its writes, + * this value may exceed the size of the buffer. + * + * @return + * The number of bytes remaining in the buffer. This value will always + * be non-negative. If the number of bytes written already exceeds the + * size of the buffer, zero will be returned. + */ +#define REMAINING(n, length) (((n) < (length)) ? 0 : ((n) - (length))) + +size_t guac_strlcpy(char* restrict dest, const char* restrict src, size_t n) { + +#ifdef HAVE_STRLCPY + return strlcpy(dest, src, n); +#else + /* Calculate actual length of desired string */ + size_t length = strlen(src); + + /* Copy nothing if there is no space */ + if (n == 0) + return length; + + /* Calculate length of the string which will be copied */ + size_t copy_length = length; + if (copy_length >= n) + copy_length = n - 1; + + /* Copy only as much of string as possible, manually adding a null + * terminator */ + memcpy(dest, src, copy_length); + dest[copy_length] = '\0'; + + /* Return the overall length of the desired string */ + return length; +#endif + +} + +size_t guac_strlcat(char* restrict dest, const char* restrict src, size_t n) { + +#ifdef HAVE_STRLCPY + return strlcat(dest, src, n); +#else + size_t length = strnlen(dest, n); + return length + guac_strlcpy(dest + length, src, REMAINING(n, length)); +#endif + +} + +size_t guac_strljoin(char* restrict dest, const char* restrict const* elements, + int nmemb, const char* restrict delim, size_t n) { + + size_t length = 0; + const char* restrict const* current = elements; + + /* If no elements are provided, nothing to do but ensure the destination + * buffer is null terminated */ + if (nmemb <= 0) + return guac_strlcpy(dest, "", n); + + /* Initialize destination buffer with first element */ + length += guac_strlcpy(dest, *current, n); + + /* Copy all remaining elements, separated by delimiter */ + for (current++; nmemb > 1; current++, nmemb--) { + length += guac_strlcat(dest + length, delim, REMAINING(n, length)); + length += guac_strlcat(dest + length, *current, REMAINING(n, length)); + } + + return length; + +} + diff --git a/src/libguac/tests/Makefile.am b/src/libguac/tests/Makefile.am new file mode 100644 index 00000000..4ab3269f --- /dev/null +++ b/src/libguac/tests/Makefile.am @@ -0,0 +1,79 @@ +# +# 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. +# +# NOTE: Parts of this file (Makefile.am) are automatically transcluded verbatim +# into Makefile.in. Though the build system (GNU Autotools) automatically adds +# its own license boilerplate to the generated Makefile.in, that boilerplate +# does not apply to the transcluded portions of Makefile.am which are licensed +# to you by the ASF under the Apache License, Version 2.0, as described above. +# + +AUTOMAKE_OPTIONS = foreign +ACLOCAL_AMFLAGS = -I m4 + +# +# Unit tests for libguac +# + +check_PROGRAMS = test_libguac +TESTS = $(check_PROGRAMS) + +test_libguac_SOURCES = \ + client/buffer_pool.c \ + client/layer_pool.c \ + parser/append.c \ + parser/read.c \ + pool/next_free.c \ + protocol/base64_decode.c \ + socket/fd_send_instruction.c \ + socket/nested_send_instruction.c \ + string/strlcat.c \ + string/strlcpy.c \ + string/strljoin.c \ + unicode/charsize.c \ + unicode/read.c \ + unicode/strlen.c \ + unicode/write.c + + +test_libguac_CFLAGS = \ + -Werror -Wall -pedantic \ + @LIBGUAC_INCLUDE@ + +test_libguac_LDADD = \ + @CUNIT_LIBS@ \ + @LIBGUAC_LTLIB@ + +# +# Autogenerate test runner +# + +GEN_RUNNER = $(top_srcdir)/util/generate-test-runner.pl +CLEANFILES = _generated_runner.c + +_generated_runner.c: $(test_libguac_SOURCES) + $(AM_V_GEN) $(GEN_RUNNER) $(test_libguac_SOURCES) > $@ + +nodist_test_libguac_SOURCES = \ + _generated_runner.c + +# Use automake's TAP test driver for running any tests +LOG_DRIVER = \ + env AM_TAP_AWK='$(AWK)' \ + $(SHELL) $(top_srcdir)/build-aux/tap-driver.sh + diff --git a/tests/client/buffer_pool.c b/src/libguac/tests/client/buffer_pool.c similarity index 84% rename from tests/client/buffer_pool.c rename to src/libguac/tests/client/buffer_pool.c index df27f6ef..4f3b3c12 100644 --- a/tests/client/buffer_pool.c +++ b/src/libguac/tests/client/buffer_pool.c @@ -17,20 +17,23 @@ * under the License. */ -#include "config.h" - -#include "client_suite.h" - -#include +#include #include #include -void test_buffer_pool() { +#include + +/** + * Test which verifies that buffers can be allocated and freed using the pool + * of buffers available to each guac_client, and that doing so does not disturb + * the similar pool of layers. + */ +void test_client__buffer_pool() { guac_client* client; int i; - int seen[GUAC_BUFFER_POOL_INITIAL_SIZE] = {0}; + bool seen[GUAC_BUFFER_POOL_INITIAL_SIZE] = { 0 }; guac_layer* layer; @@ -53,7 +56,7 @@ void test_buffer_pool() { /* This should be a layer we have not seen yet */ CU_ASSERT_FALSE(seen[-layer->index - 1]); - seen[-layer->index - 1] = 1; + seen[-layer->index - 1] = true; guac_client_free_buffer(client, layer); diff --git a/tests/client/layer_pool.c b/src/libguac/tests/client/layer_pool.c similarity index 84% rename from tests/client/layer_pool.c rename to src/libguac/tests/client/layer_pool.c index 8b61c866..f82e84fb 100644 --- a/tests/client/layer_pool.c +++ b/src/libguac/tests/client/layer_pool.c @@ -17,20 +17,23 @@ * under the License. */ -#include "config.h" - -#include "client_suite.h" - -#include +#include #include #include -void test_layer_pool() { +#include + +/** + * Test which verifies that layers can be allocated and freed using the pool + * of layers available to each guac_client, and that doing so does not disturb + * the similar pool of buffers. + */ +void test_client__layer_pool() { guac_client* client; int i; - int seen[GUAC_BUFFER_POOL_INITIAL_SIZE] = {0}; + bool seen[GUAC_BUFFER_POOL_INITIAL_SIZE] = {0}; guac_layer* layer; @@ -53,7 +56,7 @@ void test_layer_pool() { /* This should be a layer we have not seen yet */ CU_ASSERT_FALSE(seen[layer->index - 1]); - seen[layer->index - 1] = 1; + seen[layer->index - 1] = true; guac_client_free_layer(client, layer); diff --git a/tests/protocol/instruction_parse.c b/src/libguac/tests/parser/append.c similarity index 89% rename from tests/protocol/instruction_parse.c rename to src/libguac/tests/parser/append.c index ec679854..88515c15 100644 --- a/tests/protocol/instruction_parse.c +++ b/src/libguac/tests/parser/append.c @@ -17,18 +17,18 @@ * under the License. */ -#include "config.h" - -#include "suite.h" +#include +#include #include #include #include -#include -#include - -void test_instruction_parse() { +/** + * Test which verifies that guac_parser correctly parses Guacamole instructions + * from arbitrary blocks of data passed to guac_parser_append(). + */ +void test_parser__append() { /* Allocate parser */ guac_parser* parser = guac_parser_alloc(); @@ -52,6 +52,7 @@ void test_instruction_parse() { } + /* Parse of instruction should be complete */ CU_ASSERT_EQUAL(remaining, 18); CU_ASSERT_EQUAL(parser->state, GUAC_PARSE_COMPLETE); diff --git a/src/libguac/tests/parser/read.c b/src/libguac/tests/parser/read.c new file mode 100644 index 00000000..e3b254c3 --- /dev/null +++ b/src/libguac/tests/parser/read.c @@ -0,0 +1,145 @@ +/* + * 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 +#include +#include +#include +#include + +#include +#include + +/** + * Test string which contains exactly four Unicode characters encoded in UTF-8. + * This particular test string uses several characters which encode to multiple + * bytes in UTF-8. + */ +#define UTF8_4 "\xe7\x8a\xac\xf0\x90\xac\x80z\xc3\xa1" + +/** + * Writes a series of Guacamole instructions as raw bytes to the given file + * descriptor. The instructions written correspond to the instructions verified + * by read_expected_instructions(). The given file descriptor is automatically + * closed as a result of calling this function. + * + * @param fd + * The file descriptor to write instructions to. + */ +static void write_instructions(int fd) { + + char test_string[] = "4.test,6.a" UTF8_4 "b," + "5.12345,10.a" UTF8_4 UTF8_4 "c;" + "5.test2,10.hellohello,15.worldworldworld;"; + + char* current = test_string; + int remaining = sizeof(test_string) - 1; + + /* Write all bytes in test string */ + while (remaining > 0) { + + /* Bail out immediately if write fails (test will fail in parent + * process due to failure to read) */ + int written = write(fd, current, remaining); + if (written <= 0) + break; + + current += written; + remaining -= written; + + } + + /* Done writing */ + close(fd); + +} + +/** + * Reads and parses instructions from the given file descriptor using a + * guac_socket and guac_parser, verfying that those instructions match the + * series of Guacamole instructions expected to be written by + * write_instructions(). The given file descriptor is automatically closed as a + * result of calling this function. + * + * @param fd + * The file descriptor to read data from. + */ +static void read_expected_instructions(int fd) { + + /* Open guac socket */ + guac_socket* socket = guac_socket_open(fd); + CU_ASSERT_PTR_NOT_NULL_FATAL(socket); + + /* Allocate parser */ + guac_parser* parser = guac_parser_alloc(); + CU_ASSERT_PTR_NOT_NULL_FATAL(parser); + + /* Read and validate first instruction */ + CU_ASSERT_EQUAL_FATAL(guac_parser_read(parser, socket, 1000000), 0); + CU_ASSERT_STRING_EQUAL(parser->opcode, "test"); + CU_ASSERT_EQUAL_FATAL(parser->argc, 3); + CU_ASSERT_STRING_EQUAL(parser->argv[0], "a" UTF8_4 "b"); + CU_ASSERT_STRING_EQUAL(parser->argv[1], "12345"); + CU_ASSERT_STRING_EQUAL(parser->argv[2], "a" UTF8_4 UTF8_4 "c"); + + /* Read and validate second instruction */ + CU_ASSERT_EQUAL_FATAL(guac_parser_read(parser, socket, 1000000), 0); + CU_ASSERT_STRING_EQUAL(parser->opcode, "test2"); + CU_ASSERT_EQUAL_FATAL(parser->argc, 2); + CU_ASSERT_STRING_EQUAL(parser->argv[0], "hellohello"); + CU_ASSERT_STRING_EQUAL(parser->argv[1], "worldworldworld"); + + /* Done */ + guac_parser_free(parser); + guac_socket_free(socket); + +} + +/** + * Tests that guac_parser_read() correctly reads and parses instructions + * received over a guac_socket. A child process is forked to write a series of + * instructions which are read and verified by the parent process. + */ +void test_parser__read() { + + int fd[2]; + + /* Create pipe */ + CU_ASSERT_EQUAL_FATAL(pipe(fd), 0); + + int read_fd = fd[0]; + int write_fd = fd[1]; + + /* Fork into writer process (child) and reader process (parent) */ + int childpid; + CU_ASSERT_NOT_EQUAL_FATAL((childpid = fork()), -1); + + /* Attempt to write a series of instructions within the child process */ + if (childpid == 0) { + close(read_fd); + write_instructions(write_fd); + exit(0); + } + + /* Read and verify the expected instructions within the parent process */ + close(write_fd); + read_expected_instructions(read_fd); + +} + diff --git a/tests/util/guac_pool.c b/src/libguac/tests/pool/next_free.c similarity index 78% rename from tests/util/guac_pool.c rename to src/libguac/tests/pool/next_free.c index e24d8fc3..e8bd945e 100644 --- a/tests/util/guac_pool.c +++ b/src/libguac/tests/pool/next_free.c @@ -17,20 +17,21 @@ * under the License. */ -#include "config.h" - -#include "util_suite.h" - -#include +#include #include -#define UNSEEN 0 -#define SEEN_PHASE_1 1 -#define SEEN_PHASE_2 2 - +/** + * The number of unique integers to provide through the guac_pool instance + * being tested. + */ #define POOL_SIZE 128 -void test_guac_pool() { +/** + * Test which verifies that guac_pool provides access to a given number of + * unique integers, never repeating a retrieved integer until that integer + * is returned to the pool. + */ +void test_pool__next_free() { guac_pool* pool; @@ -53,8 +54,8 @@ void test_guac_pool() { CU_ASSERT_FATAL(value < POOL_SIZE); /* This should be an integer we have not seen yet */ - CU_ASSERT_EQUAL(UNSEEN, seen[value]); - seen[value] = SEEN_PHASE_1; + CU_ASSERT_EQUAL(0, seen[value]); + seen[value]++; /* Return value to pool */ guac_pool_free_int(pool, value); @@ -71,9 +72,9 @@ void test_guac_pool() { CU_ASSERT_FATAL(value >= 0); CU_ASSERT_FATAL(value < POOL_SIZE); - /* This should be an integer we have seen already */ - CU_ASSERT_EQUAL(SEEN_PHASE_1, seen[value]); - seen[value] = SEEN_PHASE_2; + /* This should be an integer we have seen only once */ + CU_ASSERT_EQUAL(1, seen[value]); + seen[value]++; } diff --git a/tests/protocol/base64_decode.c b/src/libguac/tests/protocol/base64_decode.c similarity index 90% rename from tests/protocol/base64_decode.c rename to src/libguac/tests/protocol/base64_decode.c index 9f8b7b7c..60c8a032 100644 --- a/tests/protocol/base64_decode.c +++ b/src/libguac/tests/protocol/base64_decode.c @@ -17,18 +17,14 @@ * under the License. */ -#include "config.h" - -#include "suite.h" - -#include -#include -#include - -#include +#include #include -void test_base64_decode() { +/** + * Tests that libguac's in-place base64 decoding function properly decodes + * valid base64 and fails for invalid base64. + */ +void test_protocol__decode_base64() { /* Test strings */ char test_HELLO[] = "SEVMTE8="; diff --git a/src/libguac/tests/socket/fd_send_instruction.c b/src/libguac/tests/socket/fd_send_instruction.c new file mode 100644 index 00000000..5a162d61 --- /dev/null +++ b/src/libguac/tests/socket/fd_send_instruction.c @@ -0,0 +1,136 @@ +/* + * 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 +#include +#include + +#include +#include + +/** + * Test string which contains exactly four Unicode characters encoded in UTF-8. + * This particular test string uses several characters which encode to multiple + * bytes in UTF-8. + */ +#define UTF8_4 "\xe7\x8a\xac\xf0\x90\xac\x80z\xc3\xa1" + +/** + * Writes a series of Guacamole instructions using a normal guac_socket + * wrapping the given file descriptor. The instructions written correspond to + * the instructions verified by read_expected_instructions(). The given file + * descriptor is automatically closed as a result of calling this function. + * + * @param fd + * The file descriptor to write instructions to. + */ +static void write_instructions(int fd) { + + /* Open guac socket */ + guac_socket* socket = guac_socket_open(fd); + + /* Write nothing if socket cannot be allocated (test will fail in parent + * process due to failure to read) */ + if (socket == NULL) { + close(fd); + return; + } + + /* Write instructions */ + guac_protocol_send_name(socket, "a" UTF8_4 "b" UTF8_4 "c"); + guac_protocol_send_sync(socket, 12345); + guac_socket_flush(socket); + + /* Close and free socket */ + guac_socket_free(socket); + +} + +/** + * Reads raw bytes from the given file descriptor until no further bytes + * remain, verfying that those bytes represent the series of Guacamole + * instructions expected to be written by write_instructions(). The given + * file descriptor is automatically closed as a result of calling this + * function. + * + * @param fd + * The file descriptor to read data from. + */ +static void read_expected_instructions(int fd) { + + char expected[] = + "4.name,11.a" UTF8_4 "b" UTF8_4 "c;" + "4.sync,5.12345;"; + + int numread; + char buffer[1024]; + int offset = 0; + + /* Read everything available into buffer */ + while ((numread = read(fd, &(buffer[offset]), + sizeof(buffer) - offset)) > 0) { + offset += numread; + } + + /* Verify length of read data */ + CU_ASSERT_EQUAL(offset, strlen(expected)); + + /* Add NULL terminator */ + buffer[offset] = '\0'; + + /* Read value should be equal to expected value */ + CU_ASSERT_STRING_EQUAL(buffer, expected); + + /* File descriptor is no longer needed */ + close(fd); + +} + +/** + * Tests that the file descriptor implementation of guac_socket properly + * implements writing of instructions. A child process is forked to write a + * series of instructions which are read and verified by the parent process. + */ +void test_socket__fd_send_instruction() { + + int fd[2]; + + /* Create pipe */ + CU_ASSERT_EQUAL_FATAL(pipe(fd), 0); + + int read_fd = fd[0]; + int write_fd = fd[1]; + + /* Fork into writer process (child) and reader process (parent) */ + int childpid; + CU_ASSERT_NOT_EQUAL_FATAL((childpid = fork()), -1); + + /* Attempt to write a series of instructions within the child process */ + if (childpid == 0) { + close(read_fd); + write_instructions(write_fd); + exit(0); + } + + /* Read and verify the expected instructions within the parent process */ + close(write_fd); + read_expected_instructions(read_fd); + +} + diff --git a/src/libguac/tests/socket/nested_send_instruction.c b/src/libguac/tests/socket/nested_send_instruction.c new file mode 100644 index 00000000..db29e2b2 --- /dev/null +++ b/src/libguac/tests/socket/nested_send_instruction.c @@ -0,0 +1,149 @@ +/* + * 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 +#include +#include + +#include +#include + +/** + * Test string which contains exactly four Unicode characters encoded in UTF-8. + * This particular test string uses several characters which encode to multiple + * bytes in UTF-8. + */ +#define UTF8_4 "\xe7\x8a\xac\xf0\x90\xac\x80z\xc3\xa1" + +/** + * Writes a series of Guacamole instructions using a nested guac_socket + * wrapping another guac_socket which writes to the given file descriptor. The + * instructions written correspond to the instructions verified by + * read_expected_instructions(). The given file descriptor is automatically + * closed as a result of calling this function. + * + * @param fd + * The file descriptor to write instructions to. + */ +static void write_instructions(int fd) { + + /* Open guac socket */ + guac_socket* socket = guac_socket_open(fd); + + /* Write nothing if socket cannot be allocated (test will fail in parent + * process due to failure to read) */ + if (socket == NULL) { + close(fd); + return; + } + + /* Nest socket */ + guac_socket* nested_socket = guac_socket_nest(socket, 123); + + /* Write nothing if nested socket cannot be allocated (test will fail in + * parent process due to failure to read) */ + if (socket == NULL) { + guac_socket_free(socket); + return; + } + + /* Write instructions */ + guac_protocol_send_name(nested_socket, "a" UTF8_4 "b" UTF8_4 "c"); + guac_protocol_send_sync(nested_socket, 12345); + + /* Close and free sockets */ + guac_socket_free(nested_socket); + guac_socket_free(socket); + +} + +/** + * Reads raw bytes from the given file descriptor until no further bytes + * remain, verfying that those bytes represent the series of Guacamole + * instructions expected to be written by write_instructions(). The given + * file descriptor is automatically closed as a result of calling this + * function. + * + * @param fd + * The file descriptor to read data from. + */ +static void read_expected_instructions(int fd) { + + char expected[] = + "4.nest,3.123,37." + "4.name,11.a" UTF8_4 "b" UTF8_4 "c;" + "4.sync,5.12345;" + ";"; + + int numread; + char buffer[1024]; + int offset = 0; + + /* Read everything available into buffer */ + while ((numread = read(fd, &(buffer[offset]), + sizeof(buffer) - offset)) > 0) { + offset += numread; + } + + /* Verify length of read data */ + CU_ASSERT_EQUAL(offset, strlen(expected)); + + /* Add NULL terminator */ + buffer[offset] = '\0'; + + /* Read value should be equal to expected value */ + CU_ASSERT_STRING_EQUAL(buffer, expected); + + /* File descriptor is no longer needed */ + close(fd); + +} + +/** + * Tests that the nested socket implementation of guac_socket properly + * implements writing of instructions. A child process is forked to write a + * series of instructions which are read and verified by the parent process. + */ +void test_socket__nested_send_instruction() { + + int fd[2]; + + /* Create pipe */ + CU_ASSERT_EQUAL_FATAL(pipe(fd), 0); + + int read_fd = fd[0]; + int write_fd = fd[1]; + + /* Fork into writer process (child) and reader process (parent) */ + int childpid; + CU_ASSERT_NOT_EQUAL_FATAL((childpid = fork()), -1); + + /* Attempt to write a series of instructions within the child process */ + if (childpid == 0) { + close(read_fd); + write_instructions(write_fd); + exit(0); + } + + /* Read and verify the expected instructions within the parent process */ + close(write_fd); + read_expected_instructions(read_fd); + +} + diff --git a/src/libguac/tests/string/strlcat.c b/src/libguac/tests/string/strlcat.c new file mode 100644 index 00000000..cf3dc330 --- /dev/null +++ b/src/libguac/tests/string/strlcat.c @@ -0,0 +1,153 @@ +/* + * 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 +#include + +#include +#include + +/** + * Verify guac_strlcat() behavior when the string fits the buffer without + * truncation. The return value of each call should be the length of the + * resulting string. Each resulting string should contain the full result of + * the concatenation, including null terminator. + */ +void test_string__strlcat() { + + char buffer[1024]; + + memset(buffer, 0xFF, sizeof(buffer)); + strcpy(buffer, "Apache "); + CU_ASSERT_EQUAL(guac_strlcat(buffer, "Guacamole", sizeof(buffer)), 16); + CU_ASSERT_STRING_EQUAL(buffer, "Apache Guacamole"); + CU_ASSERT_EQUAL(buffer[17], '\xFF'); + + memset(buffer, 0xFF, sizeof(buffer)); + strcpy(buffer, ""); + CU_ASSERT_EQUAL(guac_strlcat(buffer, "This is a test", sizeof(buffer)), 14); + CU_ASSERT_STRING_EQUAL(buffer, "This is a test"); + CU_ASSERT_EQUAL(buffer[15], '\xFF'); + + memset(buffer, 0xFF, sizeof(buffer)); + strcpy(buffer, "AB"); + CU_ASSERT_EQUAL(guac_strlcat(buffer, "X", sizeof(buffer)), 3); + CU_ASSERT_STRING_EQUAL(buffer, "ABX"); + CU_ASSERT_EQUAL(buffer[4], '\xFF'); + + memset(buffer, 0xFF, sizeof(buffer)); + strcpy(buffer, "X"); + CU_ASSERT_EQUAL(guac_strlcat(buffer, "", sizeof(buffer)), 1); + CU_ASSERT_STRING_EQUAL(buffer, "X"); + CU_ASSERT_EQUAL(buffer[2], '\xFF'); + + memset(buffer, 0xFF, sizeof(buffer)); + strcpy(buffer, ""); + CU_ASSERT_EQUAL(guac_strlcat(buffer, "", sizeof(buffer)), 0); + CU_ASSERT_STRING_EQUAL(buffer, ""); + CU_ASSERT_EQUAL(buffer[1], '\xFF'); + +} + +/** + * Verify guac_strlcat() behavior when the string must be truncated to fit the + * buffer. The return value of each call should be the length that would result + * from concatenating the strings given an infinite buffer, however only as + * many characters as can fit should be appended to the string within the + * buffer, and the buffer should be null-terminated. + */ +void test_string__strlcat_truncate() { + + char buffer[1024]; + + memset(buffer, 0xFF, sizeof(buffer)); + strcpy(buffer, "Apache "); + CU_ASSERT_EQUAL(guac_strlcat(buffer, "Guacamole", 9), 16); + CU_ASSERT_STRING_EQUAL(buffer, "Apache G"); + CU_ASSERT_EQUAL(buffer[9], '\xFF'); + + memset(buffer, 0xFF, sizeof(buffer)); + strcpy(buffer, ""); + CU_ASSERT_EQUAL(guac_strlcat(buffer, "This is a test", 10), 14); + CU_ASSERT_STRING_EQUAL(buffer, "This is a"); + CU_ASSERT_EQUAL(buffer[10], '\xFF'); + + memset(buffer, 0xFF, sizeof(buffer)); + strcpy(buffer, "This "); + CU_ASSERT_EQUAL(guac_strlcat(buffer, "is ANOTHER test", 6), 20); + CU_ASSERT_STRING_EQUAL(buffer, "This "); + CU_ASSERT_EQUAL(buffer[6], '\xFF'); + +} + +/** + * Verify guac_strlcat() behavior with zero buffer sizes. The return value of + * each call should be the size of the input string, while the buffer remains + * untouched. + */ +void test_string__strlcat_nospace() { + + /* 0-byte buffer plus 1 guard byte (to test overrun) */ + char buffer[1] = { '\xFF' }; + + CU_ASSERT_EQUAL(guac_strlcat(buffer, "Guacamole", 0), 9); + CU_ASSERT_EQUAL(buffer[0], '\xFF'); + + CU_ASSERT_EQUAL(guac_strlcat(buffer, "This is a test", 0), 14); + CU_ASSERT_EQUAL(buffer[0], '\xFF'); + + CU_ASSERT_EQUAL(guac_strlcat(buffer, "X", 0), 1); + CU_ASSERT_EQUAL(buffer[0], '\xFF'); + + CU_ASSERT_EQUAL(guac_strlcat(buffer, "", 0), 0); + CU_ASSERT_EQUAL(buffer[0], '\xFF'); + +} + +/** + * Verify guac_strlcat() behavior with unterminated buffers. With respect to + * the return value, the length of the string in the buffer should be + * considered equal to the size of the buffer, however the resulting buffer + * should not be null-terminated. + */ +void test_string__strlcat_nonull() { + + char expected[1024]; + memset(expected, 0xFF, sizeof(expected)); + + char buffer[1024]; + + memset(buffer, 0xFF, sizeof(buffer)); + CU_ASSERT_EQUAL(guac_strlcat(buffer, "Guacamole", 256), 265); + CU_ASSERT_NSTRING_EQUAL(buffer, expected, sizeof(expected)); + + memset(buffer, 0xFF, sizeof(buffer)); + CU_ASSERT_EQUAL(guac_strlcat(buffer, "This is a test", 37), 51); + CU_ASSERT_NSTRING_EQUAL(buffer, expected, sizeof(expected)); + + memset(buffer, 0xFF, sizeof(buffer)); + CU_ASSERT_EQUAL(guac_strlcat(buffer, "X", 12), 13); + CU_ASSERT_NSTRING_EQUAL(buffer, expected, sizeof(expected)); + + memset(buffer, 0xFF, sizeof(buffer)); + CU_ASSERT_EQUAL(guac_strlcat(buffer, "", 100), 100); + CU_ASSERT_NSTRING_EQUAL(buffer, expected, sizeof(expected)); + +} + diff --git a/src/libguac/tests/string/strlcpy.c b/src/libguac/tests/string/strlcpy.c new file mode 100644 index 00000000..1e8e01e3 --- /dev/null +++ b/src/libguac/tests/string/strlcpy.c @@ -0,0 +1,102 @@ +/* + * 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 +#include + +#include +#include + +/** + * Verify guac_strlcpy() behavior when the string fits the buffer without + * truncation. + */ +void test_string__strlcpy() { + + char buffer[1024]; + + memset(buffer, 0xFF, sizeof(buffer)); + CU_ASSERT_EQUAL(guac_strlcpy(buffer, "Guacamole", sizeof(buffer)), 9); + CU_ASSERT_STRING_EQUAL(buffer, "Guacamole"); + CU_ASSERT_EQUAL(buffer[10], '\xFF'); + + memset(buffer, 0xFF, sizeof(buffer)); + CU_ASSERT_EQUAL(guac_strlcpy(buffer, "This is a test", sizeof(buffer)), 14); + CU_ASSERT_STRING_EQUAL(buffer, "This is a test"); + CU_ASSERT_EQUAL(buffer[15], '\xFF'); + + memset(buffer, 0xFF, sizeof(buffer)); + CU_ASSERT_EQUAL(guac_strlcpy(buffer, "X", sizeof(buffer)), 1); + CU_ASSERT_STRING_EQUAL(buffer, "X"); + CU_ASSERT_EQUAL(buffer[2], '\xFF'); + + memset(buffer, 0xFF, sizeof(buffer)); + CU_ASSERT_EQUAL(guac_strlcpy(buffer, "", sizeof(buffer)), 0); + CU_ASSERT_STRING_EQUAL(buffer, ""); + CU_ASSERT_EQUAL(buffer[1], '\xFF'); + +} + +/** + * Verify guac_strlcpy() behavior when the string must be truncated to fit the + * buffer. + */ +void test_string__strlcpy_truncate() { + + char buffer[1024]; + + memset(buffer, 0xFF, sizeof(buffer)); + CU_ASSERT_EQUAL(guac_strlcpy(buffer, "Guacamole", 6), 9); + CU_ASSERT_STRING_EQUAL(buffer, "Guaca"); + CU_ASSERT_EQUAL(buffer[6], '\xFF'); + + memset(buffer, 0xFF, sizeof(buffer)); + CU_ASSERT_EQUAL(guac_strlcpy(buffer, "This is a test", 10), 14); + CU_ASSERT_STRING_EQUAL(buffer, "This is a"); + CU_ASSERT_EQUAL(buffer[10], '\xFF'); + + memset(buffer, 0xFF, sizeof(buffer)); + CU_ASSERT_EQUAL(guac_strlcpy(buffer, "This is ANOTHER test", 2), 20); + CU_ASSERT_STRING_EQUAL(buffer, "T"); + CU_ASSERT_EQUAL(buffer[2], '\xFF'); + +} + +/** + * Verify guac_strlcpy() behavior with zero buffer sizes. + */ +void test_string__strlcpy_nospace() { + + /* 0-byte buffer plus 1 guard byte (to test overrun) */ + char buffer[1] = { '\xFF' }; + + CU_ASSERT_EQUAL(guac_strlcpy(buffer, "Guacamole", 0), 9); + CU_ASSERT_EQUAL(buffer[0], '\xFF'); + + CU_ASSERT_EQUAL(guac_strlcpy(buffer, "This is a test", 0), 14); + CU_ASSERT_EQUAL(buffer[0], '\xFF'); + + CU_ASSERT_EQUAL(guac_strlcpy(buffer, "X", 0), 1); + CU_ASSERT_EQUAL(buffer[0], '\xFF'); + + CU_ASSERT_EQUAL(guac_strlcpy(buffer, "", 0), 0); + CU_ASSERT_EQUAL(buffer[0], '\xFF'); + +} + diff --git a/src/libguac/tests/string/strljoin.c b/src/libguac/tests/string/strljoin.c new file mode 100644 index 00000000..39932885 --- /dev/null +++ b/src/libguac/tests/string/strljoin.c @@ -0,0 +1,148 @@ +/* + * 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 +#include + +#include +#include + +/** + * Array of test elements containing the strings "Apache" and "Guacamole". + */ +const char* const apache_guacamole[] = { "Apache", "Guacamole" }; + +/** + * Array of test elements containing the strings "This", "is", "a", and "test". + */ +const char* const this_is_a_test[] = { "This", "is", "a", "test" }; + +/** + * Array of four test elements containing the strings "A" and "B", each + * preceded by an empty string (""). + */ +const char* const empty_a_empty_b[] = { "", "A", "", "B" }; + +/** + * Array of test elements containing ten empty strings. + */ +const char* const empty_x10[] = { "", "", "", "", "", "", "", "", "", "" }; + +/** + * Verify guac_strljoin() behavior when the string fits the buffer without + * truncation. The return value of each call should be the length of the + * resulting string. Each resulting string should contain the full result of + * the join operation, including null terminator. + */ +void test_string__strljoin() { + + char buffer[1024]; + + memset(buffer, 0xFF, sizeof(buffer)); + CU_ASSERT_EQUAL(guac_strljoin(buffer, apache_guacamole, 2, " ", sizeof(buffer)), 16); + CU_ASSERT_STRING_EQUAL(buffer, "Apache Guacamole"); + CU_ASSERT_EQUAL(buffer[17], '\xFF'); + + memset(buffer, 0xFF, sizeof(buffer)); + CU_ASSERT_EQUAL(guac_strljoin(buffer, this_is_a_test, 4, "", sizeof(buffer)), 11); + CU_ASSERT_STRING_EQUAL(buffer, "Thisisatest"); + CU_ASSERT_EQUAL(buffer[12], '\xFF'); + + memset(buffer, 0xFF, sizeof(buffer)); + CU_ASSERT_EQUAL(guac_strljoin(buffer, this_is_a_test, 4, "-/-", sizeof(buffer)), 20); + CU_ASSERT_STRING_EQUAL(buffer, "This-/-is-/-a-/-test"); + CU_ASSERT_EQUAL(buffer[21], '\xFF'); + + memset(buffer, 0xFF, sizeof(buffer)); + CU_ASSERT_EQUAL(guac_strljoin(buffer, empty_a_empty_b, 4, "/", sizeof(buffer)), 5); + CU_ASSERT_STRING_EQUAL(buffer, "/A//B"); + CU_ASSERT_EQUAL(buffer[6], '\xFF'); + + memset(buffer, 0xFF, sizeof(buffer)); + CU_ASSERT_EQUAL(guac_strljoin(buffer, empty_x10, 10, "/", sizeof(buffer)), 9); + CU_ASSERT_STRING_EQUAL(buffer, "/////////"); + CU_ASSERT_EQUAL(buffer[10], '\xFF'); + +} + +/** + * Verify guac_strljoin() behavior when the string must be truncated to fit the + * buffer. The return value of each call should be the length that would result + * from joining the strings given an infinite buffer, however only as many + * characters as can fit should be appended to the string within the buffer, + * and the buffer should be null-terminated. + */ +void test_string__strljoin_truncate() { + + char buffer[1024]; + + memset(buffer, 0xFF, sizeof(buffer)); + CU_ASSERT_EQUAL(guac_strljoin(buffer, apache_guacamole, 2, " ", 9), 16); + CU_ASSERT_STRING_EQUAL(buffer, "Apache G"); + CU_ASSERT_EQUAL(buffer[9], '\xFF'); + + memset(buffer, 0xFF, sizeof(buffer)); + CU_ASSERT_EQUAL(guac_strljoin(buffer, this_is_a_test, 4, "", 8), 11); + CU_ASSERT_STRING_EQUAL(buffer, "Thisisa"); + CU_ASSERT_EQUAL(buffer[8], '\xFF'); + + memset(buffer, 0xFF, sizeof(buffer)); + CU_ASSERT_EQUAL(guac_strljoin(buffer, this_is_a_test, 4, "-/-", 12), 20); + CU_ASSERT_STRING_EQUAL(buffer, "This-/-is-/"); + CU_ASSERT_EQUAL(buffer[12], '\xFF'); + + memset(buffer, 0xFF, sizeof(buffer)); + CU_ASSERT_EQUAL(guac_strljoin(buffer, empty_a_empty_b, 4, "/", 2), 5); + CU_ASSERT_STRING_EQUAL(buffer, "/"); + CU_ASSERT_EQUAL(buffer[2], '\xFF'); + + memset(buffer, 0xFF, sizeof(buffer)); + CU_ASSERT_EQUAL(guac_strljoin(buffer, empty_x10, 10, "/", 7), 9); + CU_ASSERT_STRING_EQUAL(buffer, "//////"); + CU_ASSERT_EQUAL(buffer[7], '\xFF'); + +} + +/** + * Verify guac_strljoin() behavior with zero buffer sizes. The return value of + * each call should be the size of the input string, while the buffer remains + * untouched. + */ +void test_string__strljoin_nospace() { + + /* 0-byte buffer plus 1 guard byte (to test overrun) */ + char buffer[1] = { '\xFF' }; + + CU_ASSERT_EQUAL(guac_strljoin(buffer, apache_guacamole, 2, " ", 0), 16); + CU_ASSERT_EQUAL(buffer[0], '\xFF'); + + CU_ASSERT_EQUAL(guac_strljoin(buffer, this_is_a_test, 4, "", 0), 11); + CU_ASSERT_EQUAL(buffer[0], '\xFF'); + + CU_ASSERT_EQUAL(guac_strljoin(buffer, this_is_a_test, 4, "-/-", 0), 20); + CU_ASSERT_EQUAL(buffer[0], '\xFF'); + + CU_ASSERT_EQUAL(guac_strljoin(buffer, empty_a_empty_b, 4, "/", 0), 5); + CU_ASSERT_EQUAL(buffer[0], '\xFF'); + + CU_ASSERT_EQUAL(guac_strljoin(buffer, empty_x10, 10, "/", 0), 9); + CU_ASSERT_EQUAL(buffer[0], '\xFF'); + +} + diff --git a/src/libguac/tests/unicode/charsize.c b/src/libguac/tests/unicode/charsize.c new file mode 100644 index 00000000..e636155f --- /dev/null +++ b/src/libguac/tests/unicode/charsize.c @@ -0,0 +1,33 @@ +/* + * 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 +#include + +/** + * Test which verifies that guac_utf8_charsize() correctly determines the + * length of UTF-8 characters from the leading byte of that character. + */ +void test_unicode__utf8_charsize() { + CU_ASSERT_EQUAL(1, guac_utf8_charsize('g')); + CU_ASSERT_EQUAL(2, guac_utf8_charsize('\xC4')); + CU_ASSERT_EQUAL(3, guac_utf8_charsize('\xE7')); + CU_ASSERT_EQUAL(4, guac_utf8_charsize('\xF0')); +} + diff --git a/src/libguac/tests/unicode/read.c b/src/libguac/tests/unicode/read.c new file mode 100644 index 00000000..230a721c --- /dev/null +++ b/src/libguac/tests/unicode/read.c @@ -0,0 +1,52 @@ +/* + * 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 +#include + +/** + * Test which verifies that guac_utf8_read() properly parses UTF-8. + */ +void test_unicode__utf8_read() { + + int codepoint; + + char buffer[16] = + /* U+0065 */ "\x65" + /* U+0654 */ "\xD9\x94" + /* U+0876 */ "\xE0\xA1\xB6" + /* U+12345 */ "\xF0\x92\x8D\x85"; + + CU_ASSERT_EQUAL(1, guac_utf8_read(&(buffer[0]), 10, &codepoint)); + CU_ASSERT_EQUAL(0x0065, codepoint); + + CU_ASSERT_EQUAL(2, guac_utf8_read(&(buffer[1]), 9, &codepoint)); + CU_ASSERT_EQUAL(0x0654, codepoint); + + CU_ASSERT_EQUAL(3, guac_utf8_read(&(buffer[3]), 7, &codepoint)); + CU_ASSERT_EQUAL(0x0876, codepoint); + + CU_ASSERT_EQUAL(4, guac_utf8_read(&(buffer[6]), 4, &codepoint)); + CU_ASSERT_EQUAL(0x12345, codepoint); + + CU_ASSERT_EQUAL(0, guac_utf8_read(&(buffer[10]), 0, &codepoint)); + CU_ASSERT_EQUAL(0x12345, codepoint); + +} + diff --git a/tests/util/util_suite.h b/src/libguac/tests/unicode/strlen.c similarity index 55% rename from tests/util/util_suite.h rename to src/libguac/tests/unicode/strlen.c index 3e3253b0..995a5038 100644 --- a/tests/util/util_suite.h +++ b/src/libguac/tests/unicode/strlen.c @@ -17,19 +17,8 @@ * under the License. */ - -#ifndef _GUAC_TEST_UTIL_SUITE_H -#define _GUAC_TEST_UTIL_SUITE_H - -/** - * Test suite containing unit tests for utility functions built into libguac. - * These utility functions are included for convenience rather as integral - * requirements of the core. - * - * @file util_suite.h - */ - -#include "config.h" +#include +#include /** * A single Unicode character encoded as one byte with UTF-8. @@ -52,24 +41,19 @@ #define UTF8_4b "\xf0\x90\x84\xa3" /** - * Registers the utility test suite with CUnit. + * Test which verifies that guac_utf8_strlen() properly calculates the length + * of UTF-8 strings. */ -int register_util_suite(); - -/** - * Unit test for the guac_pool structure and related functions. The guac_pool - * structure provides a consistent source of pooled integers. This unit test - * checks that the associated functions behave as documented (returning - * integers in the proper order, allocating new integers as necessary, etc.). - */ -void test_guac_pool(); - -/** - * Unit test for libguac's Unicode convenience functions. This test checks that - * the functions provided for determining string length, character length, and - * for reading and writing UTF-8 behave as specified in the documentation. - */ -void test_guac_unicode(); - -#endif +void test_unicode__utf8_strlen() { + CU_ASSERT_EQUAL(0, guac_utf8_strlen("")); + CU_ASSERT_EQUAL(1, guac_utf8_strlen(UTF8_4b)); + CU_ASSERT_EQUAL(2, guac_utf8_strlen(UTF8_4b UTF8_1b)); + CU_ASSERT_EQUAL(2, guac_utf8_strlen(UTF8_2b UTF8_3b)); + CU_ASSERT_EQUAL(3, guac_utf8_strlen(UTF8_1b UTF8_3b UTF8_4b)); + CU_ASSERT_EQUAL(3, guac_utf8_strlen(UTF8_2b UTF8_1b UTF8_3b)); + CU_ASSERT_EQUAL(3, guac_utf8_strlen(UTF8_4b UTF8_2b UTF8_1b)); + CU_ASSERT_EQUAL(3, guac_utf8_strlen(UTF8_3b UTF8_4b UTF8_2b)); + CU_ASSERT_EQUAL(5, guac_utf8_strlen("hello")); + CU_ASSERT_EQUAL(9, guac_utf8_strlen("guacamole")); +} diff --git a/tests/util/guac_unicode.c b/src/libguac/tests/unicode/write.c similarity index 50% rename from tests/util/guac_unicode.c rename to src/libguac/tests/unicode/write.c index 62f58876..8480086d 100644 --- a/tests/util/guac_unicode.c +++ b/src/libguac/tests/unicode/write.c @@ -17,36 +17,17 @@ * under the License. */ -#include "config.h" - -#include "util_suite.h" - -#include +#include #include -void test_guac_unicode() { +/** + * Test which verifies that guac_utf8_write() properly encodes Unicode + * codepoints as UTF-8. + */ +void test_unicode__utf8_write() { - int codepoint; char buffer[16]; - /* Test character length */ - CU_ASSERT_EQUAL(1, guac_utf8_charsize(UTF8_1b[0])); - CU_ASSERT_EQUAL(2, guac_utf8_charsize(UTF8_2b[0])); - CU_ASSERT_EQUAL(3, guac_utf8_charsize(UTF8_3b[0])); - CU_ASSERT_EQUAL(4, guac_utf8_charsize(UTF8_4b[0])); - - /* Test string length */ - CU_ASSERT_EQUAL(0, guac_utf8_strlen("")); - CU_ASSERT_EQUAL(1, guac_utf8_strlen(UTF8_4b)); - CU_ASSERT_EQUAL(2, guac_utf8_strlen(UTF8_4b UTF8_1b)); - CU_ASSERT_EQUAL(2, guac_utf8_strlen(UTF8_2b UTF8_3b)); - CU_ASSERT_EQUAL(3, guac_utf8_strlen(UTF8_1b UTF8_3b UTF8_4b)); - CU_ASSERT_EQUAL(3, guac_utf8_strlen(UTF8_2b UTF8_1b UTF8_3b)); - CU_ASSERT_EQUAL(3, guac_utf8_strlen(UTF8_4b UTF8_2b UTF8_1b)); - CU_ASSERT_EQUAL(3, guac_utf8_strlen(UTF8_3b UTF8_4b UTF8_2b)); - CU_ASSERT_EQUAL(5, guac_utf8_strlen("hello")); - CU_ASSERT_EQUAL(9, guac_utf8_strlen("guacamole")); - /* Test writes */ CU_ASSERT_EQUAL(1, guac_utf8_write(0x00065, &(buffer[0]), 10)); CU_ASSERT_EQUAL(2, guac_utf8_write(0x00654, &(buffer[1]), 9)); @@ -60,22 +41,5 @@ void test_guac_unicode() { CU_ASSERT(memcmp("\xE0\xA1\xB6", &(buffer[3]), 3) == 0); /* U+0876 */ CU_ASSERT(memcmp("\xF0\x92\x8D\x85", &(buffer[6]), 4) == 0); /* U+12345 */ - /* Test reads */ - - CU_ASSERT_EQUAL(1, guac_utf8_read(&(buffer[0]), 10, &codepoint)); - CU_ASSERT_EQUAL(0x0065, codepoint); - - CU_ASSERT_EQUAL(2, guac_utf8_read(&(buffer[1]), 9, &codepoint)); - CU_ASSERT_EQUAL(0x0654, codepoint); - - CU_ASSERT_EQUAL(3, guac_utf8_read(&(buffer[3]), 7, &codepoint)); - CU_ASSERT_EQUAL(0x0876, codepoint); - - CU_ASSERT_EQUAL(4, guac_utf8_read(&(buffer[6]), 4, &codepoint)); - CU_ASSERT_EQUAL(0x12345, codepoint); - - CU_ASSERT_EQUAL(0, guac_utf8_read(&(buffer[10]), 0, &codepoint)); - CU_ASSERT_EQUAL(0x12345, codepoint); - } diff --git a/src/libguac/timestamp.c b/src/libguac/timestamp.c index 0d2dc0a8..9020a6e1 100644 --- a/src/libguac/timestamp.c +++ b/src/libguac/timestamp.c @@ -19,7 +19,7 @@ #include "config.h" -#include "timestamp.h" +#include "guacamole/timestamp.h" #include diff --git a/src/libguac/unicode.c b/src/libguac/unicode.c index e7af943f..fdc0fffd 100644 --- a/src/libguac/unicode.c +++ b/src/libguac/unicode.c @@ -19,7 +19,7 @@ #include "config.h" -#include "unicode.h" +#include "guacamole/unicode.h" #include diff --git a/src/libguac/user-handlers.c b/src/libguac/user-handlers.c index 0e8ed38b..f64fd299 100644 --- a/src/libguac/user-handlers.c +++ b/src/libguac/user-handlers.c @@ -19,18 +19,19 @@ #include "config.h" -#include "client.h" -#include "object.h" -#include "protocol.h" -#include "stream.h" -#include "timestamp.h" -#include "user.h" +#include "guacamole/client.h" +#include "guacamole/object.h" +#include "guacamole/protocol.h" +#include "guacamole/stream.h" +#include "guacamole/timestamp.h" +#include "guacamole/user.h" #include "user-handlers.h" #include #include #include #include +#include /* Guacamole instruction handler map */ @@ -49,9 +50,22 @@ __guac_instruction_handler_mapping __guac_instruction_handler_map[] = { {"get", __guac_handle_get}, {"put", __guac_handle_put}, {"audio", __guac_handle_audio}, + {"argv", __guac_handle_argv}, + {"nop", __guac_handle_nop}, {NULL, NULL} }; +/* Guacamole handshake handler map */ + +__guac_instruction_handler_mapping __guac_handshake_handler_map[] = { + {"size", __guac_handshake_size_handler}, + {"audio", __guac_handshake_audio_handler}, + {"video", __guac_handshake_video_handler}, + {"image", __guac_handshake_image_handler}, + {"timezone", __guac_handshake_timezone_handler}, + {NULL, NULL} +}; + /** * Parses a 64-bit integer from the given string. It is assumed that the string * will contain only decimal digits, with an optional leading minus sign. @@ -382,6 +396,30 @@ int __guac_handle_pipe(guac_user* user, int argc, char** argv) { return 0; } +int __guac_handle_argv(guac_user* user, int argc, char** argv) { + + /* Pull corresponding stream */ + int stream_index = atoi(argv[0]); + guac_stream* stream = __init_input_stream(user, stream_index); + if (stream == NULL) + return 0; + + /* If supported, call handler */ + if (user->argv_handler) + return user->argv_handler( + user, + stream, + argv[1], /* mimetype */ + argv[2] /* name */ + ); + + /* Otherwise, abort */ + guac_protocol_send_ack(user->socket, stream, + "Reconfiguring in-progress connections unsupported", + GUAC_PROTOCOL_STATUS_UNSUPPORTED); + return 0; +} + int __guac_handle_ack(guac_user* user, int argc, char** argv) { guac_stream* stream; @@ -551,8 +589,147 @@ int __guac_handle_put(guac_user* user, int argc, char** argv) { return 0; } +int __guac_handle_nop(guac_user* user, int argc, char** argv) { + guac_user_log(user, GUAC_LOG_TRACE, + "Received nop instruction"); + return 0; +} + int __guac_handle_disconnect(guac_user* user, int argc, char** argv) { guac_user_stop(user); return 0; } +/* Guacamole handshake handler functions. */ + +int __guac_handshake_size_handler(guac_user* user, int argc, char** argv) { + + /* Validate size of instruction. */ + if (argc < 2) { + guac_user_log(user, GUAC_LOG_ERROR, "Received \"size\" " + "instruction lacked required arguments."); + return 1; + } + + /* Parse optimal screen dimensions from size instruction */ + user->info.optimal_width = atoi(argv[0]); + user->info.optimal_height = atoi(argv[1]); + + /* If DPI given, set the user resolution */ + if (argc >= 3) + user->info.optimal_resolution = atoi(argv[2]); + + /* Otherwise, use a safe default for rough backwards compatibility */ + else + user->info.optimal_resolution = 96; + + return 0; + +} + +int __guac_handshake_audio_handler(guac_user* user, int argc, char** argv) { + + guac_free_mimetypes((char **) user->info.audio_mimetypes); + + /* Store audio mimetypes */ + user->info.audio_mimetypes = (const char**) guac_copy_mimetypes(argv, argc); + + return 0; + +} + +int __guac_handshake_video_handler(guac_user* user, int argc, char** argv) { + + guac_free_mimetypes((char **) user->info.video_mimetypes); + + /* Store video mimetypes */ + user->info.video_mimetypes = (const char**) guac_copy_mimetypes(argv, argc); + + return 0; + +} + +int __guac_handshake_image_handler(guac_user* user, int argc, char** argv) { + + guac_free_mimetypes((char **) user->info.image_mimetypes); + + /* Store image mimetypes */ + user->info.image_mimetypes = (const char**) guac_copy_mimetypes(argv, argc); + + return 0; + +} + +int __guac_handshake_timezone_handler(guac_user* user, int argc, char** argv) { + + /* Free any past value */ + free((char *) user->info.timezone); + + /* Store timezone, if present */ + if (argc > 0 && strcmp(argv[0], "")) + user->info.timezone = (const char*) strdup(argv[0]); + + else + user->info.timezone = NULL; + + return 0; + +} + +char** guac_copy_mimetypes(char** mimetypes, int count) { + + int i; + + /* Allocate sufficient space for NULL-terminated array of mimetypes */ + char** mimetypes_copy = malloc(sizeof(char*) * (count+1)); + + /* Copy each provided mimetype */ + for (i = 0; i < count; i++) + mimetypes_copy[i] = strdup(mimetypes[i]); + + /* Terminate with NULL */ + mimetypes_copy[count] = NULL; + + return mimetypes_copy; + +} + +void guac_free_mimetypes(char** mimetypes) { + + if (mimetypes == NULL) + return; + + char** current_mimetype = mimetypes; + + /* Free all strings within NULL-terminated mimetype array */ + while (*current_mimetype != NULL) { + free(*current_mimetype); + current_mimetype++; + } + + /* Free the array itself, now that its contents have been freed */ + free(mimetypes); + +} + +int __guac_user_call_opcode_handler(__guac_instruction_handler_mapping* map, + guac_user* user, const char* opcode, int argc, char** argv) { + + /* For each defined instruction */ + __guac_instruction_handler_mapping* current = map; + while (current->opcode != NULL) { + + /* If recognized, call handler */ + if (strcmp(opcode, current->opcode) == 0) + return current->handler(user, argc, argv); + + current++; + } + + /* If unrecognized, log and ignore */ + guac_user_log(user, GUAC_LOG_DEBUG, "Handler not found for \"%s\"", + opcode); + return 0; + +} + diff --git a/src/libguac/user-handlers.h b/src/libguac/user-handlers.h index 7a1b623a..263c2928 100644 --- a/src/libguac/user-handlers.h +++ b/src/libguac/user-handlers.h @@ -31,8 +31,8 @@ #include "config.h" -#include "client.h" -#include "timestamp.h" +#include "guacamole/client.h" +#include "guacamole/timestamp.h" /** * Internal handler for Guacamole instructions. Instruction handlers will be @@ -120,6 +120,13 @@ __guac_instruction_handler __guac_handle_file; */ __guac_instruction_handler __guac_handle_pipe; +/** + * Internal initial handler for the argv instruction. When a argv instruction + * is received, this handler will be called. The client's argv handler will + * be invoked if defined. + */ +__guac_instruction_handler __guac_handle_argv; + /** * Internal initial handler for the ack instruction. When a ack instruction * is received, this handler will be called. The client's ack handler will @@ -170,6 +177,47 @@ __guac_instruction_handler __guac_handle_size; */ __guac_instruction_handler __guac_handle_disconnect; +/** + * Internal handler for the nop instruction. This handler will be called when + * the nop instruction is received, and will do nothing more than a TRACE level + * log of the instruction. + */ +__guac_instruction_handler __guac_handle_nop; + +/** + * Internal handler function that is called when the size instruction is + * received during the handshake process. + */ +__guac_instruction_handler __guac_handshake_size_handler; + +/** + * Internal handler function that is called when the audio instruction is + * received during the handshake process, specifying the audio mimetypes + * available to the client. + */ +__guac_instruction_handler __guac_handshake_audio_handler; + +/** + * Internal handler function that is called when the video instruction is + * received during the handshake process, specifying the video mimetypes + * available to the client. + */ +__guac_instruction_handler __guac_handshake_video_handler; + +/** + * Internal handler function that is called when the image instruction is + * received during the handshake process, specifying the image mimetypes + * available to the client. + */ +__guac_instruction_handler __guac_handshake_image_handler; + +/** + * Internal handler function that is called when the timezone instruction is + * received during the handshake process, specifying the timezone of the + * client. + */ +__guac_instruction_handler __guac_handshake_timezone_handler; + /** * Instruction handler mapping table. This is a NULL-terminated array of * __guac_instruction_handler_mapping structures, each mapping an opcode @@ -179,4 +227,72 @@ __guac_instruction_handler __guac_handle_disconnect; */ extern __guac_instruction_handler_mapping __guac_instruction_handler_map[]; +/** + * Handler mapping table for instructions (opcodes) specifically for the + * handshake portion of the connection. Each + * __guac_instruction_handler_mapping structure within this NULL-terminated + * array maps an opcode to a __guac_instruction_handler. The end of the array + * must be marked with a mapping with the opcode set to NULL. + */ +extern __guac_instruction_handler_mapping __guac_handshake_handler_map[]; + +/** + * Frees the given array of mimetypes, including the space allocated to each + * mimetype string within the array. The provided array of mimetypes MUST have + * been allocated with guac_copy_mimetypes(). + * + * @param mimetypes + * The NULL-terminated array of mimetypes to free. This array MUST have + * been previously allocated with guac_copy_mimetypes(). + */ +void guac_free_mimetypes(char** mimetypes); + +/** + * Copies the given array of mimetypes (strings) into a newly-allocated NULL- + * terminated array of strings. Both the array and the strings within the array + * are newly-allocated and must be later freed via guac_free_mimetypes(). + * + * @param mimetypes + * The array of mimetypes to copy. + * + * @param count + * The number of mimetypes in the given array. + * + * @return + * A newly-allocated, NULL-terminated array containing newly-allocated + * copies of each of the mimetypes provided in the original mimetypes + * array. + */ +char** guac_copy_mimetypes(char** mimetypes, int count); + +/** + * Call the appropriate handler defined by the given user for the given + * instruction. A comparison is made between the instruction opcode and the + * initial handler lookup table defined in the map that is provided to this + * function. If an entry for the instruction is found in the provided map, + * the handler defined in that map will be called and the value returned. If + * no match is found, it is silently ignored. + * + * @param map + * The array that holds the opcode to handler mappings. + * + * @param user + * The user whose handlers should be called. + * + * @param opcode + * The opcode of the instruction to pass to the user via the appropriate + * handler. + * + * @param argc + * The number of arguments which are part of the instruction. + * + * @param argv + * An array of all arguments which are part of the instruction. + * + * @return + * Zero if the instruction was handled successfully, or non-zero otherwise. + */ +int __guac_user_call_opcode_handler(__guac_instruction_handler_mapping* map, + guac_user* user, const char* opcode, int argc, char** argv); + #endif diff --git a/src/libguac/user-handshake.c b/src/libguac/user-handshake.c index b6018888..89afccdd 100644 --- a/src/libguac/user-handshake.c +++ b/src/libguac/user-handshake.c @@ -19,12 +19,13 @@ #include "config.h" -#include "client.h" -#include "error.h" -#include "parser.h" -#include "protocol.h" -#include "socket.h" -#include "user.h" +#include "guacamole/client.h" +#include "guacamole/error.h" +#include "guacamole/parser.h" +#include "guacamole/protocol.h" +#include "guacamole/socket.h" +#include "guacamole/user.h" +#include "user-handlers.h" #include #include @@ -113,64 +114,6 @@ static void guac_user_log_handshake_failure(guac_user* user) { } -/** - * Copies the given array of mimetypes (strings) into a newly-allocated NULL- - * terminated array of strings. Both the array and the strings within the array - * are newly-allocated and must be later freed via guac_free_mimetypes(). - * - * @param mimetypes - * The array of mimetypes to copy. - * - * @param count - * The number of mimetypes in the given array. - * - * @return - * A newly-allocated, NULL-terminated array containing newly-allocated - * copies of each of the mimetypes provided in the original mimetypes - * array. - */ -static char** guac_copy_mimetypes(char** mimetypes, int count) { - - int i; - - /* Allocate sufficient space for NULL-terminated array of mimetypes */ - char** mimetypes_copy = malloc(sizeof(char*) * (count+1)); - - /* Copy each provided mimetype */ - for (i = 0; i < count; i++) - mimetypes_copy[i] = strdup(mimetypes[i]); - - /* Terminate with NULL */ - mimetypes_copy[count] = NULL; - - return mimetypes_copy; - -} - -/** - * Frees the given array of mimetypes, including the space allocated to each - * mimetype string within the array. The provided array of mimetypes MUST have - * been allocated with guac_copy_mimetypes(). - * - * @param mimetypes - * The NULL-terminated array of mimetypes to free. This array MUST have - * been previously allocated with guac_copy_mimetypes(). - */ -static void guac_free_mimetypes(char** mimetypes) { - - char** current_mimetype = mimetypes; - - /* Free all strings within NULL-terminated mimetype array */ - while (*current_mimetype != NULL) { - free(*current_mimetype); - current_mimetype++; - } - - /* Free the array itself, now that its contents have been freed */ - free(mimetypes); - -} - /** * The thread which handles all user input, calling event handlers for received * instructions. @@ -219,7 +162,8 @@ static void* guac_user_input_thread(void* data) { guac_error_message = NULL; /* Call handler, stop on error */ - if (guac_user_handle_instruction(user, parser->opcode, parser->argc, parser->argv) < 0) { + if (__guac_user_call_opcode_handler(__guac_instruction_handler_map, + user, parser->opcode, parser->argc, parser->argv)) { /* Log error */ guac_user_log_guac_error(user, GUAC_LOG_WARNING, @@ -286,11 +230,78 @@ static int guac_user_start(guac_parser* parser, guac_user* user, } +/** + * This function loops through the received instructions during the handshake + * with the client attempting to join the connection, and runs the handlers + * for each of the opcodes, ending when the connect instruction is received. + * Returns zero if the handshake completes successfully with the connect opcode, + * or a non-zero value if an error occurs. + * + * @param user + * The guac_user attempting to join the connection. + * + * @param parser + * The parser used to examine the received data. + * + * @param usec_timeout + * The timeout, in microseconds, for reading the instructions. + * + * @return + * Zero if the handshake completes successfully with the connect opcode, + * or non-zero if an error occurs. + */ +static int __guac_user_handshake(guac_user* user, guac_parser* parser, + int usec_timeout) { + + guac_socket* socket = user->socket; + + /* Handle each of the opcodes. */ + while (guac_parser_read(parser, socket, usec_timeout) == 0) { + + /* If we receive the connect opcode, we're done. */ + if (strcmp(parser->opcode, "connect") == 0) + return 0; + + guac_user_log(user, GUAC_LOG_DEBUG, "Processing instruction: %s", + parser->opcode); + + /* Run instruction handler for opcode with arguments. */ + if (__guac_user_call_opcode_handler(__guac_handshake_handler_map, user, + parser->opcode, parser->argc, parser->argv)) { + + guac_user_log_handshake_failure(user); + guac_user_log_guac_error(user, GUAC_LOG_DEBUG, + "Error handling instruction during handshake."); + guac_user_log(user, GUAC_LOG_DEBUG, "Failed opcode: %s", + parser->opcode); + + guac_parser_free(parser); + return 1; + + } + + } + + /* If we get here it's because we never got the connect instruction. */ + guac_user_log(user, GUAC_LOG_ERROR, + "Handshake failed, \"connect\" instruction was not received."); + return 1; +} + int guac_user_handle_connection(guac_user* user, int usec_timeout) { guac_socket* socket = user->socket; guac_client* client = user->client; - + + user->info.audio_mimetypes = NULL; + user->info.image_mimetypes = NULL; + user->info.video_mimetypes = NULL; + user->info.timezone = NULL; + + /* Count number of arguments. */ + int num_args; + for (num_args = 0; client->args[num_args] != NULL; num_args++); + /* Send args */ if (guac_protocol_send_args(socket, client->args) || guac_socket_flush(socket)) { @@ -305,94 +316,8 @@ int guac_user_handle_connection(guac_user* user, int usec_timeout) { guac_parser* parser = guac_parser_alloc(); - /* Get optimal screen size */ - if (guac_parser_expect(parser, socket, usec_timeout, "size")) { - - /* Log error */ - guac_user_log_handshake_failure(user); - guac_user_log_guac_error(user, GUAC_LOG_DEBUG, - "Error reading \"size\""); - - guac_parser_free(parser); - return 1; - } - - /* Validate content of size instruction */ - if (parser->argc < 2) { - guac_user_log(user, GUAC_LOG_ERROR, "Received \"size\" " - "instruction lacked required arguments."); - guac_parser_free(parser); - return 1; - } - - /* Parse optimal screen dimensions from size instruction */ - user->info.optimal_width = atoi(parser->argv[0]); - user->info.optimal_height = atoi(parser->argv[1]); - - /* If DPI given, set the user resolution */ - if (parser->argc >= 3) - user->info.optimal_resolution = atoi(parser->argv[2]); - - /* Otherwise, use a safe default for rough backwards compatibility */ - else - user->info.optimal_resolution = 96; - - /* Get supported audio formats */ - if (guac_parser_expect(parser, socket, usec_timeout, "audio")) { - - /* Log error */ - guac_user_log_handshake_failure(user); - guac_user_log_guac_error(user, GUAC_LOG_DEBUG, - "Error reading \"audio\""); - - guac_parser_free(parser); - return 1; - } - - /* Store audio mimetypes */ - char** audio_mimetypes = guac_copy_mimetypes(parser->argv, parser->argc); - user->info.audio_mimetypes = (const char**) audio_mimetypes; - - /* Get supported video formats */ - if (guac_parser_expect(parser, socket, usec_timeout, "video")) { - - /* Log error */ - guac_user_log_handshake_failure(user); - guac_user_log_guac_error(user, GUAC_LOG_DEBUG, - "Error reading \"video\""); - - guac_parser_free(parser); - return 1; - } - - /* Store video mimetypes */ - char** video_mimetypes = guac_copy_mimetypes(parser->argv, parser->argc); - user->info.video_mimetypes = (const char**) video_mimetypes; - - /* Get supported image formats */ - if (guac_parser_expect(parser, socket, usec_timeout, "image")) { - - /* Log error */ - guac_user_log_handshake_failure(user); - guac_user_log_guac_error(user, GUAC_LOG_DEBUG, - "Error reading \"image\""); - - guac_parser_free(parser); - return 1; - } - - /* Store image mimetypes */ - char** image_mimetypes = guac_copy_mimetypes(parser->argv, parser->argc); - user->info.image_mimetypes = (const char**) image_mimetypes; - - /* Get args from connect instruction */ - if (guac_parser_expect(parser, socket, usec_timeout, "connect")) { - - /* Log error */ - guac_user_log_handshake_failure(user); - guac_user_log_guac_error(user, GUAC_LOG_DEBUG, - "Error reading \"connect\""); - + /* Perform the handshake with the client. */ + if (__guac_user_handshake(user, parser, usec_timeout)) { guac_parser_free(parser); return 1; } @@ -400,9 +325,16 @@ int guac_user_handle_connection(guac_user* user, int usec_timeout) { /* Acknowledge connection availability */ guac_protocol_send_ready(socket, client->connection_id); guac_socket_flush(socket); - - /* Attempt join */ - if (guac_client_add_user(client, user, parser->argc, parser->argv)) + + /* Verify argument count. */ + if (parser->argc != (num_args + 1)) { + guac_client_log(client, GUAC_LOG_ERROR, "Client did not return the " + "expected number of arguments."); + return 1; + } + + /* Attempt to join user to connection. */ + if (guac_client_add_user(client, user, (parser->argc - 1), parser->argv + 1)) guac_client_log(client, GUAC_LOG_ERROR, "User \"%s\" could NOT " "join connection \"%s\"", user->user_id, client->connection_id); @@ -412,6 +344,12 @@ int guac_user_handle_connection(guac_user* user, int usec_timeout) { guac_client_log(client, GUAC_LOG_INFO, "User \"%s\" joined connection " "\"%s\" (%i users now present)", user->user_id, client->connection_id, client->connected_users); + if (strcmp(parser->argv[0],"") != 0) + guac_client_log(client, GUAC_LOG_DEBUG, "Client is using protocol " + "version \"%s\"", parser->argv[0]); + else + guac_client_log(client, GUAC_LOG_DEBUG, "Client has not defined " + "its protocol version."); /* Handle user I/O, wait for connection to terminate */ guac_user_start(parser, user, usec_timeout); @@ -422,12 +360,15 @@ int guac_user_handle_connection(guac_user* user, int usec_timeout) { "users remain)", user->user_id, client->connected_users); } - - /* Free mimetype lists */ - guac_free_mimetypes(audio_mimetypes); - guac_free_mimetypes(video_mimetypes); - guac_free_mimetypes(image_mimetypes); - + + /* Free mimetype character arrays. */ + guac_free_mimetypes((char **) user->info.audio_mimetypes); + guac_free_mimetypes((char **) user->info.image_mimetypes); + guac_free_mimetypes((char **) user->info.video_mimetypes); + + /* Free timezone info. */ + free((char *) user->info.timezone); + guac_parser_free(parser); /* Successful disconnect */ diff --git a/src/libguac/user.c b/src/libguac/user.c index 14ec75bf..aaa51bb9 100644 --- a/src/libguac/user.c +++ b/src/libguac/user.c @@ -19,18 +19,18 @@ #include "config.h" -#include "client.h" #include "encode-jpeg.h" #include "encode-png.h" #include "encode-webp.h" +#include "guacamole/client.h" +#include "guacamole/object.h" +#include "guacamole/pool.h" +#include "guacamole/protocol.h" +#include "guacamole/socket.h" +#include "guacamole/stream.h" +#include "guacamole/timestamp.h" +#include "guacamole/user.h" #include "id.h" -#include "object.h" -#include "pool.h" -#include "protocol.h" -#include "socket.h" -#include "stream.h" -#include "timestamp.h" -#include "user.h" #include "user-handlers.h" #include @@ -169,19 +169,8 @@ void guac_user_free_object(guac_user* user, guac_object* object) { int guac_user_handle_instruction(guac_user* user, const char* opcode, int argc, char** argv) { - /* For each defined instruction */ - __guac_instruction_handler_mapping* current = __guac_instruction_handler_map; - while (current->opcode != NULL) { - - /* If recognized, call handler */ - if (strcmp(opcode, current->opcode) == 0) - return current->handler(user, argc, argv); - - current++; - } - - /* If unrecognized, ignore */ - return 0; + return __guac_user_call_opcode_handler(__guac_instruction_handler_map, + user, opcode, argc, argv); } @@ -240,6 +229,26 @@ void guac_user_log(guac_user* user, guac_client_log_level level, } +void guac_user_stream_argv(guac_user* user, guac_socket* socket, + const char* mimetype, const char* name, const char* value) { + + /* Allocate new stream for argument value */ + guac_stream* stream = guac_user_alloc_stream(user); + + /* Declare stream as containing connection parameter data */ + guac_protocol_send_argv(socket, stream, mimetype, name); + + /* Write parameter data */ + guac_protocol_send_blobs(socket, stream, value, strlen(value)); + + /* Terminate stream */ + guac_protocol_send_end(socket, stream); + + /* Free allocated stream */ + guac_user_free_stream(user, stream); + +} + void guac_user_stream_png(guac_user* user, guac_socket* socket, guac_composite_mode mode, const guac_layer* layer, int x, int y, cairo_surface_t* surface) { diff --git a/src/protocols/kubernetes/Makefile.am b/src/protocols/kubernetes/Makefile.am new file mode 100644 index 00000000..f22e91f8 --- /dev/null +++ b/src/protocols/kubernetes/Makefile.am @@ -0,0 +1,72 @@ +# +# 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. +# +# NOTE: Parts of this file (Makefile.am) are automatically transcluded verbatim +# into Makefile.in. Though the build system (GNU Autotools) automatically adds +# its own license boilerplate to the generated Makefile.in, that boilerplate +# does not apply to the transcluded portions of Makefile.am which are licensed +# to you by the ASF under the Apache License, Version 2.0, as described above. +# + +AUTOMAKE_OPTIONS = foreign +ACLOCAL_AMFLAGS = -I m4 + +lib_LTLIBRARIES = libguac-client-kubernetes.la + +libguac_client_kubernetes_la_SOURCES = \ + argv.c \ + client.c \ + clipboard.c \ + input.c \ + io.c \ + pipe.c \ + settings.c \ + ssl.c \ + kubernetes.c \ + url.c \ + user.c + +noinst_HEADERS = \ + argv.h \ + client.h \ + clipboard.h \ + input.h \ + io.h \ + pipe.h \ + settings.h \ + ssl.h \ + kubernetes.h \ + url.h \ + user.h + +libguac_client_kubernetes_la_CFLAGS = \ + -Werror -Wall -Iinclude \ + @LIBGUAC_INCLUDE@ \ + @TERMINAL_INCLUDE@ + +libguac_client_kubernetes_la_LIBADD = \ + @COMMON_LTLIB@ \ + @LIBGUAC_LTLIB@ \ + @TERMINAL_LTLIB@ + +libguac_client_kubernetes_la_LDFLAGS = \ + -version-info 0:0:0 \ + @PTHREAD_LIBS@ \ + @SSL_LIBS@ \ + @WEBSOCKETS_LIBS@ + diff --git a/src/protocols/kubernetes/argv.c b/src/protocols/kubernetes/argv.c new file mode 100644 index 00000000..c37595de --- /dev/null +++ b/src/protocols/kubernetes/argv.c @@ -0,0 +1,224 @@ +/* + * 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 "config.h" +#include "argv.h" +#include "kubernetes.h" +#include "terminal/terminal.h" + +#include +#include +#include + +#include +#include + +/** + * All Kubernetes connection settings which may be updated by unprivileged + * users through "argv" streams. + */ +typedef enum guac_kubernetes_argv_setting { + + /** + * The color scheme of the terminal. + */ + GUAC_KUBERNETES_ARGV_SETTING_COLOR_SCHEME, + + /** + * The name of the font family used by the terminal. + */ + GUAC_KUBERNETES_ARGV_SETTING_FONT_NAME, + + /** + * The size of the font used by the terminal, in points. + */ + GUAC_KUBERNETES_ARGV_SETTING_FONT_SIZE + +} guac_kubernetes_argv_setting; + +/** + * The value or current status of a connection parameter received over an + * "argv" stream. + */ +typedef struct guac_kubernetes_argv { + + /** + * The specific setting being updated. + */ + guac_kubernetes_argv_setting setting; + + /** + * Buffer space for containing the received argument value. + */ + char buffer[GUAC_KUBERNETES_ARGV_MAX_LENGTH]; + + /** + * The number of bytes received so far. + */ + int length; + +} guac_kubernetes_argv; + +/** + * Handler for "blob" instructions which appends the data from received blobs + * to the end of the in-progress argument value buffer. + * + * @see guac_user_blob_handler + */ +static int guac_kubernetes_argv_blob_handler(guac_user* user, + guac_stream* stream, void* data, int length) { + + guac_kubernetes_argv* argv = (guac_kubernetes_argv*) stream->data; + + /* Calculate buffer size remaining, including space for null terminator, + * adjusting received length accordingly */ + int remaining = sizeof(argv->buffer) - argv->length - 1; + if (length > remaining) + length = remaining; + + /* Append received data to end of buffer */ + memcpy(argv->buffer + argv->length, data, length); + argv->length += length; + + return 0; + +} + +/** + * Handler for "end" instructions which applies the changes specified by the + * argument value buffer associated with the stream. + * + * @see guac_user_end_handler + */ +static int guac_kubernetes_argv_end_handler(guac_user* user, + guac_stream* stream) { + + int size; + + guac_client* client = user->client; + guac_kubernetes_client* kubernetes_client = (guac_kubernetes_client*) client->data; + guac_terminal* terminal = kubernetes_client->term; + + /* Append null terminator to value */ + guac_kubernetes_argv* argv = (guac_kubernetes_argv*) stream->data; + argv->buffer[argv->length] = '\0'; + + /* Apply changes to chosen setting */ + switch (argv->setting) { + + /* Update color scheme */ + case GUAC_KUBERNETES_ARGV_SETTING_COLOR_SCHEME: + guac_terminal_apply_color_scheme(terminal, argv->buffer); + guac_client_stream_argv(client, client->socket, "text/plain", + "color-scheme", argv->buffer); + break; + + /* Update font name */ + case GUAC_KUBERNETES_ARGV_SETTING_FONT_NAME: + guac_terminal_apply_font(terminal, argv->buffer, -1, 0); + guac_client_stream_argv(client, client->socket, "text/plain", + "font-name", argv->buffer); + break; + + /* Update font size */ + case GUAC_KUBERNETES_ARGV_SETTING_FONT_SIZE: + + /* Update only if font size is sane */ + size = atoi(argv->buffer); + if (size > 0) { + guac_terminal_apply_font(terminal, NULL, size, + kubernetes_client->settings->resolution); + guac_client_stream_argv(client, client->socket, "text/plain", + "font-size", argv->buffer); + } + + break; + + } + + /* Update Kubernetes terminal size */ + guac_kubernetes_resize(client, terminal->term_height, + terminal->term_width); + + free(argv); + return 0; + +} + +int guac_kubernetes_argv_handler(guac_user* user, guac_stream* stream, + char* mimetype, char* name) { + + guac_kubernetes_argv_setting setting; + + /* Allow users to update the color scheme and font details */ + if (strcmp(name, "color-scheme") == 0) + setting = GUAC_KUBERNETES_ARGV_SETTING_COLOR_SCHEME; + else if (strcmp(name, "font-name") == 0) + setting = GUAC_KUBERNETES_ARGV_SETTING_FONT_NAME; + else if (strcmp(name, "font-size") == 0) + setting = GUAC_KUBERNETES_ARGV_SETTING_FONT_SIZE; + + /* No other connection parameters may be updated */ + else { + guac_protocol_send_ack(user->socket, stream, "Not allowed.", + GUAC_PROTOCOL_STATUS_CLIENT_FORBIDDEN); + guac_socket_flush(user->socket); + return 0; + } + + guac_kubernetes_argv* argv = malloc(sizeof(guac_kubernetes_argv)); + argv->setting = setting; + argv->length = 0; + + /* Prepare stream to receive argument value */ + stream->blob_handler = guac_kubernetes_argv_blob_handler; + stream->end_handler = guac_kubernetes_argv_end_handler; + stream->data = argv; + + /* Signal stream is ready */ + guac_protocol_send_ack(user->socket, stream, "Ready for updated " + "parameter.", GUAC_PROTOCOL_STATUS_SUCCESS); + guac_socket_flush(user->socket); + return 0; + +} + +void* guac_kubernetes_send_current_argv(guac_user* user, void* data) { + + guac_kubernetes_client* kubernetes_client = (guac_kubernetes_client*) data; + guac_terminal* terminal = kubernetes_client->term; + + /* Send current color scheme */ + guac_user_stream_argv(user, user->socket, "text/plain", "color-scheme", + terminal->color_scheme); + + /* Send current font name */ + guac_user_stream_argv(user, user->socket, "text/plain", "font-name", + terminal->font_name); + + /* Send current font size */ + char font_size[64]; + sprintf(font_size, "%i", terminal->font_size); + guac_user_stream_argv(user, user->socket, "text/plain", "font-size", + font_size); + + return NULL; + +} + diff --git a/src/protocols/kubernetes/argv.h b/src/protocols/kubernetes/argv.h new file mode 100644 index 00000000..2fc6c1ba --- /dev/null +++ b/src/protocols/kubernetes/argv.h @@ -0,0 +1,61 @@ +/* + * 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. + */ + + +#ifndef GUAC_KUBERNETES_ARGV_H +#define GUAC_KUBERNETES_ARGV_H + +#include "config.h" + +#include + +/** + * The maximum number of bytes to allow for any argument value received via an + * argv stream, including null terminator. + */ +#define GUAC_KUBERNETES_ARGV_MAX_LENGTH 16384 + +/** + * Handles an incoming stream from a Guacamole "argv" instruction, updating the + * given connection parameter if that parameter is allowed to be updated. + */ +guac_user_argv_handler guac_kubernetes_argv_handler; + +/** + * Sends the current values of all non-sensitive parameters which may be set + * while the connection is running to the given user. Note that the user + * receiving these values will not necessarily be able to set new values + * themselves if their connection is read-only. This function can be used as + * the callback for guac_client_foreach_user() and guac_client_for_owner() + * + * @param user + * The user that should receive the values of all non-sensitive parameters + * which may be set while the connection is running. + * + * @param data + * The guac_kubernetes_client instance associated with the current + * connection. + * + * @return + * Always NULL. + */ +void* guac_kubernetes_send_current_argv(guac_user* user, void* data); + +#endif + diff --git a/src/protocols/kubernetes/client.c b/src/protocols/kubernetes/client.c new file mode 100644 index 00000000..1a1eb3a7 --- /dev/null +++ b/src/protocols/kubernetes/client.c @@ -0,0 +1,133 @@ +/* + * 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 "client.h" +#include "common/clipboard.h" +#include "kubernetes.h" +#include "settings.h" +#include "user.h" + +#include +#include + +#include +#include +#include +#include +#include + +guac_client* guac_kubernetes_lws_current_client = NULL; + +/** + * Logging callback invoked by libwebsockets to log a single line of logging + * output. As libwebsockets messages are all generally low-level, the log + * level provided by libwebsockets is ignored here, with all messages logged + * instead at guacd's debug level. + * + * @param level + * The libwebsockets log level associated with the log message. This value + * is ignored by this implementation of the logging callback. + * + * @param line + * The line of logging output to log. + */ +static void guac_kubernetes_log(int level, const char* line) { + + char buffer[1024]; + + /* Drop log message if there's nowhere to log yet */ + if (guac_kubernetes_lws_current_client == NULL) + return; + + /* Trim length of line to fit buffer (plus null terminator) */ + int length = strlen(line); + if (length > sizeof(buffer) - 1) + length = sizeof(buffer) - 1; + + /* Copy as much of the received line as will fit in the buffer */ + memcpy(buffer, line, length); + + /* If the line ends with a newline character, trim the character */ + if (length > 0 && buffer[length - 1] == '\n') + length--; + + /* Null-terminate the trimmed string */ + buffer[length] = '\0'; + + /* Log using guacd's own log facilities */ + guac_client_log(guac_kubernetes_lws_current_client, GUAC_LOG_DEBUG, + "libwebsockets: %s", buffer); + +} + +int guac_client_init(guac_client* client) { + + /* Ensure reference to main guac_client remains available in all + * libwebsockets contexts */ + guac_kubernetes_lws_current_client = client; + + /* Redirect libwebsockets logging */ + lws_set_log_level(LLL_ERR | LLL_WARN | LLL_NOTICE | LLL_INFO, + guac_kubernetes_log); + + /* Set client args */ + client->args = GUAC_KUBERNETES_CLIENT_ARGS; + + /* Allocate client instance data */ + guac_kubernetes_client* kubernetes_client = calloc(1, sizeof(guac_kubernetes_client)); + client->data = kubernetes_client; + + /* Init clipboard */ + kubernetes_client->clipboard = guac_common_clipboard_alloc(GUAC_KUBERNETES_CLIPBOARD_MAX_LENGTH); + + /* Set handlers */ + client->join_handler = guac_kubernetes_user_join_handler; + client->free_handler = guac_kubernetes_client_free_handler; + + /* Set locale and warn if not UTF-8 */ + setlocale(LC_CTYPE, ""); + if (strcmp(nl_langinfo(CODESET), "UTF-8") != 0) { + guac_client_log(client, GUAC_LOG_INFO, + "Current locale does not use UTF-8. Some characters may " + "not render correctly."); + } + + /* Success */ + return 0; + +} + +int guac_kubernetes_client_free_handler(guac_client* client) { + + guac_kubernetes_client* kubernetes_client = + (guac_kubernetes_client*) client->data; + + /* Wait client thread to terminate */ + pthread_join(kubernetes_client->client_thread, NULL); + + /* Free settings */ + if (kubernetes_client->settings != NULL) + guac_kubernetes_settings_free(kubernetes_client->settings); + + guac_common_clipboard_free(kubernetes_client->clipboard); + free(kubernetes_client); + return 0; + +} + diff --git a/tests/client/client_suite.c b/src/protocols/kubernetes/client.h similarity index 54% rename from tests/client/client_suite.c rename to src/protocols/kubernetes/client.h index 18bba6bb..ec4ba326 100644 --- a/tests/client/client_suite.c +++ b/src/protocols/kubernetes/client.h @@ -17,40 +17,28 @@ * under the License. */ -#include "config.h" +#ifndef GUAC_KUBERNETES_CLIENT_H +#define GUAC_KUBERNETES_CLIENT_H -#include "client_suite.h" +#include -#include +/** + * The maximum number of bytes to allow within the clipboard. + */ +#define GUAC_KUBERNETES_CLIPBOARD_MAX_LENGTH 262144 -int client_suite_init() { - return 0; -} +/** + * Static reference to the guac_client associated with the active Kubernetes + * connection. While libwebsockets provides some means of storing and + * retrieving custom data in some structures, this is not always available. + */ +extern guac_client* guac_kubernetes_lws_current_client; -int client_suite_cleanup() { - return 0; -} +/** + * Free handler. Required by libguac and called when the guac_client is + * disconnected and must be cleaned up. + */ +guac_client_free_handler guac_kubernetes_client_free_handler; -int register_client_suite() { - - /* Add client test suite */ - CU_pSuite suite = CU_add_suite("client", - client_suite_init, client_suite_cleanup); - if (suite == NULL) { - CU_cleanup_registry(); - return CU_get_error(); - } - - /* Add tests */ - if ( - CU_add_test(suite, "layer-pool", test_layer_pool) == NULL - || CU_add_test(suite, "buffer-pool", test_buffer_pool) == NULL - ) { - CU_cleanup_registry(); - return CU_get_error(); - } - - return 0; - -} +#endif diff --git a/src/protocols/kubernetes/clipboard.c b/src/protocols/kubernetes/clipboard.c new file mode 100644 index 00000000..f1682066 --- /dev/null +++ b/src/protocols/kubernetes/clipboard.c @@ -0,0 +1,65 @@ +/* + * 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 "clipboard.h" +#include "common/clipboard.h" +#include "kubernetes.h" + +#include +#include +#include + +int guac_kubernetes_clipboard_handler(guac_user* user, guac_stream* stream, + char* mimetype) { + + guac_client* client = user->client; + guac_kubernetes_client* kubernetes_client = + (guac_kubernetes_client*) client->data; + + /* Clear clipboard and prepare for new data */ + guac_common_clipboard_reset(kubernetes_client->clipboard, mimetype); + + /* Set handlers for clipboard stream */ + stream->blob_handler = guac_kubernetes_clipboard_blob_handler; + stream->end_handler = guac_kubernetes_clipboard_end_handler; + + return 0; +} + +int guac_kubernetes_clipboard_blob_handler(guac_user* user, + guac_stream* stream, void* data, int length) { + + guac_client* client = user->client; + guac_kubernetes_client* kubernetes_client = + (guac_kubernetes_client*) client->data; + + /* Append new data */ + guac_common_clipboard_append(kubernetes_client->clipboard, data, length); + + return 0; +} + +int guac_kubernetes_clipboard_end_handler(guac_user* user, + guac_stream* stream) { + + /* Nothing to do - clipboard is implemented within client */ + + return 0; +} + diff --git a/tests/common/common_suite.h b/src/protocols/kubernetes/clipboard.h similarity index 58% rename from tests/common/common_suite.h rename to src/protocols/kubernetes/clipboard.h index 60dbe9ae..87a393cf 100644 --- a/tests/common/common_suite.h +++ b/src/protocols/kubernetes/clipboard.h @@ -17,39 +17,25 @@ * under the License. */ +#ifndef GUAC_KUBERNETES_CLIPBOARD_H +#define GUAC_KUBERNETES_CLIPBOARD_H -#ifndef _GUAC_TEST_COMMON_SUITE_H -#define _GUAC_TEST_COMMON_SUITE_H +#include /** - * Test suite containing unit tests for the "common" utility library included - * for the sake of simplifying guacamole-server development, but not included - * as part of libguac. - * - * @file common_suite.h + * Handler for inbound clipboard streams. */ - -#include "config.h" +guac_user_clipboard_handler guac_kubernetes_clipboard_handler; /** - * Registers the common test suite with CUnit. + * Handler for data received along clipboard streams. */ -int register_common_suite(); +guac_user_blob_handler guac_kubernetes_clipboard_blob_handler; /** - * Unit test for string utility functions. + * Handler for end-of-stream related to clipboard. */ -void test_guac_string(); - -/** - * Unit test for character conversion functions. - */ -void test_guac_iconv(); - -/** - * Unit test for rectangle calculation functions. - */ -void test_guac_rect(); +guac_user_end_handler guac_kubernetes_clipboard_end_handler; #endif diff --git a/src/protocols/kubernetes/input.c b/src/protocols/kubernetes/input.c new file mode 100644 index 00000000..814578ef --- /dev/null +++ b/src/protocols/kubernetes/input.c @@ -0,0 +1,94 @@ +/* + * 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/recording.h" +#include "input.h" +#include "kubernetes.h" +#include "terminal/terminal.h" + +#include +#include + +#include + +int guac_kubernetes_user_mouse_handler(guac_user* user, + int x, int y, int mask) { + + guac_client* client = user->client; + guac_kubernetes_client* kubernetes_client = + (guac_kubernetes_client*) client->data; + + /* Skip if terminal not yet ready */ + guac_terminal* term = kubernetes_client->term; + if (term == NULL) + return 0; + + /* Report mouse position within recording */ + if (kubernetes_client->recording != NULL) + guac_common_recording_report_mouse(kubernetes_client->recording, x, y, + mask); + + guac_terminal_send_mouse(term, user, x, y, mask); + return 0; + +} + +int guac_kubernetes_user_key_handler(guac_user* user, int keysym, int pressed) { + + guac_client* client = user->client; + guac_kubernetes_client* kubernetes_client = + (guac_kubernetes_client*) client->data; + + /* Report key state within recording */ + if (kubernetes_client->recording != NULL) + guac_common_recording_report_key(kubernetes_client->recording, + keysym, pressed); + + /* Skip if terminal not yet ready */ + guac_terminal* term = kubernetes_client->term; + if (term == NULL) + return 0; + + guac_terminal_send_key(term, keysym, pressed); + return 0; + +} + +int guac_kubernetes_user_size_handler(guac_user* user, int width, int height) { + + /* Get terminal */ + guac_client* client = user->client; + guac_kubernetes_client* kubernetes_client = + (guac_kubernetes_client*) client->data; + + /* Skip if terminal not yet ready */ + guac_terminal* terminal = kubernetes_client->term; + if (terminal == NULL) + return 0; + + /* Resize terminal */ + guac_terminal_resize(terminal, width, height); + + /* Update Kubernetes terminal window size if connected */ + guac_kubernetes_resize(client, terminal->term_height, + terminal->term_width); + + return 0; +} + diff --git a/tests/protocol/suite.h b/src/protocols/kubernetes/input.h similarity index 57% rename from tests/protocol/suite.h rename to src/protocols/kubernetes/input.h index 01345fac..6f24cf20 100644 --- a/tests/protocol/suite.h +++ b/src/protocols/kubernetes/input.h @@ -17,27 +17,28 @@ * under the License. */ +#ifndef GUAC_KUBERNETES_INPUT_H +#define GUAC_KUBERNETES_INPUT_H -#ifndef _GUAC_TEST_PROTOCOL_SUITE_H -#define _GUAC_TEST_PROTOCOL_SUITE_H +#include -#include "config.h" +/** + * Handler for key events. Required by libguac and called whenever key events + * are received. + */ +guac_user_key_handler guac_kubernetes_user_key_handler; -/* Unicode (UTF-8) strings */ +/** + * Handler for mouse events. Required by libguac and called whenever mouse + * events are received. + */ +guac_user_mouse_handler guac_kubernetes_user_mouse_handler; -#define UTF8_1 "\xe7\x8a\xac" /* One character */ -#define UTF8_2 UTF8_1 "\xf0\x90\xac\x80" /* Two characters */ -#define UTF8_3 UTF8_2 "z" /* Three characters */ -#define UTF8_4 UTF8_3 "\xc3\xa1" /* Four characters */ -#define UTF8_8 UTF8_4 UTF8_4 /* Eight characters */ - -int register_protocol_suite(); - -void test_base64_decode(); -void test_instruction_parse(); -void test_instruction_read(); -void test_instruction_write(); -void test_nest_write(); +/** + * Handler for size events. Required by libguac and called whenever the remote + * display (window) is resized. + */ +guac_user_size_handler guac_kubernetes_user_size_handler; #endif diff --git a/src/protocols/kubernetes/io.c b/src/protocols/kubernetes/io.c new file mode 100644 index 00000000..bfa37b1d --- /dev/null +++ b/src/protocols/kubernetes/io.c @@ -0,0 +1,143 @@ +/* + * 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 "kubernetes.h" +#include "terminal/terminal.h" + +#include +#include + +#include +#include +#include + +void guac_kubernetes_receive_data(guac_client* client, + const char* buffer, size_t length) { + + guac_kubernetes_client* kubernetes_client = + (guac_kubernetes_client*) client->data; + + /* Strip channel index from beginning of buffer */ + int channel = *(buffer++); + length--; + + switch (channel) { + + /* Write STDOUT / STDERR directly to terminal as output */ + case GUAC_KUBERNETES_CHANNEL_STDOUT: + case GUAC_KUBERNETES_CHANNEL_STDERR: + guac_terminal_write(kubernetes_client->term, buffer, length); + break; + + /* Ignore data on other channels */ + default: + guac_client_log(client, GUAC_LOG_DEBUG, "Received %i bytes along " + "channel %i.", length, channel); + + } + +} + +void guac_kubernetes_send_message(guac_client* client, + int channel, const char* data, int length) { + + guac_kubernetes_client* kubernetes_client = + (guac_kubernetes_client*) client->data; + + pthread_mutex_lock(&(kubernetes_client->outbound_message_lock)); + + /* Add message to buffer if space is available */ + if (kubernetes_client->outbound_messages_waiting + < GUAC_KUBERNETES_MAX_OUTBOUND_MESSAGES) { + + /* Calculate storage position of next message */ + int index = (kubernetes_client->outbound_messages_top + + kubernetes_client->outbound_messages_waiting) + % GUAC_KUBERNETES_MAX_OUTBOUND_MESSAGES; + + /* Obtain pointer to message slot at calculated position */ + guac_kubernetes_message* message = + &(kubernetes_client->outbound_messages[index]); + + /* Copy details of message into buffer */ + message->channel = channel; + memcpy(message->data, data, length); + message->length = length; + + /* One more message is now waiting */ + kubernetes_client->outbound_messages_waiting++; + + /* Notify libwebsockets that we need a callback to send pending + * messages */ + lws_callback_on_writable(kubernetes_client->wsi); + lws_cancel_service(kubernetes_client->context); + + } + + /* Warn if data has to be dropped */ + else + guac_client_log(client, GUAC_LOG_WARNING, "Send buffer could not be " + "flushed in time to handle additional data. Outbound " + "message dropped."); + + pthread_mutex_unlock(&(kubernetes_client->outbound_message_lock)); + +} + +bool guac_kubernetes_write_pending_message(guac_client* client) { + + bool messages_remain; + guac_kubernetes_client* kubernetes_client = + (guac_kubernetes_client*) client->data; + + pthread_mutex_lock(&(kubernetes_client->outbound_message_lock)); + + /* Send one message from top of buffer */ + if (kubernetes_client->outbound_messages_waiting > 0) { + + /* Obtain pointer to message at top */ + int top = kubernetes_client->outbound_messages_top; + guac_kubernetes_message* message = + &(kubernetes_client->outbound_messages[top]); + + /* Write message including channel index */ + lws_write(kubernetes_client->wsi, + ((unsigned char*) message) + LWS_PRE, + message->length + 1, LWS_WRITE_BINARY); + + /* Advance top to next message */ + kubernetes_client->outbound_messages_top++; + kubernetes_client->outbound_messages_top %= + GUAC_KUBERNETES_MAX_OUTBOUND_MESSAGES; + + /* One less message is waiting */ + kubernetes_client->outbound_messages_waiting--; + + } + + /* Record whether messages remained at time of completion */ + messages_remain = (kubernetes_client->outbound_messages_waiting > 0); + + pthread_mutex_unlock(&(kubernetes_client->outbound_message_lock)); + + return messages_remain; + +} + + diff --git a/src/protocols/kubernetes/io.h b/src/protocols/kubernetes/io.h new file mode 100644 index 00000000..40f2c69a --- /dev/null +++ b/src/protocols/kubernetes/io.h @@ -0,0 +1,144 @@ +/* + * 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. + */ + +#ifndef GUAC_KUBERNETES_IO_H +#define GUAC_KUBERNETES_IO_H + +#include +#include + +#include +#include + +/** + * The maximum amount of data to include in any particular WebSocket message + * to Kubernetes. This excludes the storage space required for the channel + * index. + */ +#define GUAC_KUBERNETES_MAX_MESSAGE_SIZE 1024 + +/** + * The index of the Kubernetes channel used for STDIN. + */ +#define GUAC_KUBERNETES_CHANNEL_STDIN 0 + +/** + * The index of the Kubernetes channel used for STDOUT. + */ +#define GUAC_KUBERNETES_CHANNEL_STDOUT 1 + +/** + * The index of the Kubernetes channel used for STDERR. + */ +#define GUAC_KUBERNETES_CHANNEL_STDERR 2 + +/** + * The index of the Kubernetes channel used for terminal resize messages. + */ +#define GUAC_KUBERNETES_CHANNEL_RESIZE 4 + +/** + * An outbound message to be received by Kubernetes over WebSocket. + */ +typedef struct guac_kubernetes_message { + + /** + * lws_write() requires leading padding of LWS_PRE bytes to provide + * scratch space for WebSocket framing. + */ + uint8_t _padding[LWS_PRE]; + + /** + * The index of the channel receiving the data, such as + * GUAC_KUBERNETES_CHANNEL_STDIN. + */ + uint8_t channel; + + /** + * The data that should be sent to Kubernetes (along with the channel + * index). + */ + char data[GUAC_KUBERNETES_MAX_MESSAGE_SIZE]; + + /** + * The length of the data to be sent, excluding the channel index. + */ + int length; + +} guac_kubernetes_message; + + +/** + * Handles data received from Kubernetes over WebSocket, decoding the channel + * index of the received data and forwarding that data accordingly. + * + * @param client + * The guac_client associated with the connection to Kubernetes. + * + * @param buffer + * The data received from Kubernetes. + * + * @param length + * The size of the data received from Kubernetes, in bytes. + */ +void guac_kubernetes_receive_data(guac_client* client, + const char* buffer, size_t length); + +/** + * Requests that the given data be sent along the given channel to the + * Kubernetes server when the WebSocket connection is next available for + * writing. If the WebSocket connection has not been available for writing for + * long enough that the outbound message buffer is full, the request to send + * this particular message will be dropped. + * + * @param client + * The guac_client associated with the Kubernetes connection. + * + * @param channel + * The Kubernetes channel on which to send the message, + * such as GUAC_KUBERNETES_CHANNEL_STDIN. + * + * @param data + * A buffer containing the data to send. + * + * @param length + * The number of bytes to send. + */ +void guac_kubernetes_send_message(guac_client* client, + int channel, const char* data, int length); + +/** + * Writes the oldest pending message within the outbound message queue, + * as scheduled with guac_kubernetes_send_message(), removing that message + * from the queue. This function MAY NOT be invoked outside the libwebsockets + * event callback and MUST only be invoked in the context of a + * LWS_CALLBACK_CLIENT_WRITEABLE event. If no messages are pending, this + * function has no effect. + * + * @param client + * The guac_client associated with the Kubernetes connection. + * + * @return + * true if messages still remain to be written within the outbound message + * queue, false otherwise. + */ +bool guac_kubernetes_write_pending_message(guac_client* client); + +#endif + diff --git a/src/protocols/kubernetes/kubernetes.c b/src/protocols/kubernetes/kubernetes.c new file mode 100644 index 00000000..7158a989 --- /dev/null +++ b/src/protocols/kubernetes/kubernetes.c @@ -0,0 +1,413 @@ +/* + * 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 "config.h" + +#include "argv.h" +#include "client.h" +#include "common/recording.h" +#include "io.h" +#include "kubernetes.h" +#include "ssl.h" +#include "terminal/terminal.h" +#include "url.h" + +#include +#include +#include + +#include +#include +#include + +/** + * Callback invoked by libwebsockets for events related to a WebSocket being + * used for communicating with an attached Kubernetes pod. + * + * @param wsi + * The libwebsockets handle for the WebSocket connection. + * + * @param reason + * The reason (event) that this callback was invoked. + * + * @param user + * Arbitrary data assocated with the WebSocket session. In some cases, + * this is actually event-specific data (such as the + * LWS_CALLBACK_OPENSSL_LOAD_EXTRA_CLIENT_VERIFY_CERT event). + * + * @param in + * A pointer to arbitrary, reason-specific data. + * + * @param length + * An arbitrary, reason-specific length value. + * + * @return + * An undocumented integer value related the success of handling the + * event, or -1 if the WebSocket connection should be closed. + */ +static int guac_kubernetes_lws_callback(struct lws* wsi, + enum lws_callback_reasons reason, void* user, + void* in, size_t length) { + + guac_client* client = guac_kubernetes_lws_current_client; + guac_kubernetes_client* kubernetes_client = + (guac_kubernetes_client*) client->data; + + /* Do not handle any further events if connection is closing */ + if (client->state != GUAC_CLIENT_RUNNING) { +#ifdef HAVE_LWS_CALLBACK_HTTP_DUMMY + return lws_callback_http_dummy(wsi, reason, user, in, length); +#else + return 0; +#endif + } + + switch (reason) { + + /* Complete initialization of SSL */ + case LWS_CALLBACK_OPENSSL_LOAD_EXTRA_CLIENT_VERIFY_CERTS: + guac_kubernetes_init_ssl(client, (SSL_CTX*) user); + break; + + /* Failed to connect */ + case LWS_CALLBACK_CLIENT_CONNECTION_ERROR: + guac_client_abort(client, GUAC_PROTOCOL_STATUS_UPSTREAM_NOT_FOUND, + "Error connecting to Kubernetes server: %s", + in != NULL ? (char*) in : "(no error description " + "available)"); + break; + + /* Connected / logged in */ + case LWS_CALLBACK_CLIENT_ESTABLISHED: + guac_client_log(client, GUAC_LOG_INFO, + "Kubernetes connection successful."); + + /* Allow terminal to render */ + guac_terminal_start(kubernetes_client->term); + + /* Schedule check for pending messages in case messages were added + * to the outbound message buffer prior to the connection being + * fully established */ + lws_callback_on_writable(wsi); + break; + + /* Data received via WebSocket */ + case LWS_CALLBACK_CLIENT_RECEIVE: + guac_kubernetes_receive_data(client, (const char*) in, length); + break; + + /* WebSocket is ready for writing */ + case LWS_CALLBACK_CLIENT_WRITEABLE: + + /* Send any pending messages, requesting another callback if + * yet more messages remain */ + if (guac_kubernetes_write_pending_message(client)) + lws_callback_on_writable(wsi); + break; + +#ifdef HAVE_LWS_CALLBACK_CLIENT_CLOSED + /* Connection closed (client-specific) */ + case LWS_CALLBACK_CLIENT_CLOSED: +#endif + + /* Connection closed */ + case LWS_CALLBACK_WSI_DESTROY: + case LWS_CALLBACK_CLOSED: + guac_client_stop(client); + guac_client_log(client, GUAC_LOG_DEBUG, "WebSocket connection to " + "Kubernetes server closed."); + break; + + /* No other event types are applicable */ + default: + break; + + } + +#ifdef HAVE_LWS_CALLBACK_HTTP_DUMMY + return lws_callback_http_dummy(wsi, reason, user, in, length); +#else + return 0; +#endif + +} + +/** + * List of all WebSocket protocols which should be declared as supported by + * libwebsockets during the initial WebSocket handshake, along with + * corresponding event-handling callbacks. + */ +struct lws_protocols guac_kubernetes_lws_protocols[] = { + { + .name = GUAC_KUBERNETES_LWS_PROTOCOL, + .callback = guac_kubernetes_lws_callback + }, + { 0 } +}; + +/** + * Input thread, started by the main Kubernetes client thread. This thread + * continuously reads from the terminal's STDIN and transfers all read + * data to the Kubernetes connection. + * + * @param data + * The current guac_client instance. + * + * @return + * Always NULL. + */ +static void* guac_kubernetes_input_thread(void* data) { + + guac_client* client = (guac_client*) data; + guac_kubernetes_client* kubernetes_client = + (guac_kubernetes_client*) client->data; + + char buffer[GUAC_KUBERNETES_MAX_MESSAGE_SIZE]; + int bytes_read; + + /* Write all data read */ + while ((bytes_read = guac_terminal_read_stdin(kubernetes_client->term, buffer, sizeof(buffer))) > 0) { + + /* Send received data to Kubernetes along STDIN channel */ + guac_kubernetes_send_message(client, GUAC_KUBERNETES_CHANNEL_STDIN, + buffer, bytes_read); + + } + + return NULL; + +} + +void* guac_kubernetes_client_thread(void* data) { + + guac_client* client = (guac_client*) data; + guac_kubernetes_client* kubernetes_client = + (guac_kubernetes_client*) client->data; + + guac_kubernetes_settings* settings = kubernetes_client->settings; + + pthread_t input_thread; + char endpoint_path[GUAC_KUBERNETES_MAX_ENDPOINT_LENGTH]; + + /* Verify that the pod name was specified (it's always required) */ + if (settings->kubernetes_pod == NULL) { + guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR, + "The name of the Kubernetes pod is a required parameter."); + goto fail; + } + + /* Generate endpoint for attachment URL */ + if (guac_kubernetes_endpoint_attach(endpoint_path, sizeof(endpoint_path), + settings->kubernetes_namespace, + settings->kubernetes_pod, + settings->kubernetes_container)) { + guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR, + "Unable to generate path for Kubernetes API endpoint: " + "Resulting path too long"); + goto fail; + } + + guac_client_log(client, GUAC_LOG_DEBUG, "The endpoint for attaching to " + "the requested Kubernetes pod is \"%s\".", endpoint_path); + + /* Set up screen recording, if requested */ + if (settings->recording_path != NULL) { + kubernetes_client->recording = guac_common_recording_create(client, + settings->recording_path, + settings->recording_name, + settings->create_recording_path, + !settings->recording_exclude_output, + !settings->recording_exclude_mouse, + settings->recording_include_keys); + } + + /* Create terminal */ + kubernetes_client->term = guac_terminal_create(client, + kubernetes_client->clipboard, settings->disable_copy, + settings->max_scrollback, settings->font_name, settings->font_size, + settings->resolution, settings->width, settings->height, + settings->color_scheme, settings->backspace); + + /* Fail if terminal init failed */ + if (kubernetes_client->term == NULL) { + guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR, + "Terminal initialization failed"); + goto fail; + } + + /* Send current values of exposed arguments to owner only */ + guac_client_for_owner(client, guac_kubernetes_send_current_argv, + kubernetes_client); + + /* Set up typescript, if requested */ + if (settings->typescript_path != NULL) { + guac_terminal_create_typescript(kubernetes_client->term, + settings->typescript_path, + settings->typescript_name, + settings->create_typescript_path); + } + + /* Init libwebsockets context creation parameters */ + struct lws_context_creation_info context_info = { + .port = CONTEXT_PORT_NO_LISTEN, /* We are not a WebSocket server */ + .uid = -1, + .gid = -1, + .protocols = guac_kubernetes_lws_protocols, + .user = client + }; + + /* Init WebSocket connection parameters which do not vary by Guacmaole + * connection parameters or creation of future libwebsockets objects */ + struct lws_client_connect_info connection_info = { + .host = settings->hostname, + .address = settings->hostname, + .origin = settings->hostname, + .port = settings->port, + .protocol = GUAC_KUBERNETES_LWS_PROTOCOL, + .userdata = client + }; + + /* If requested, use an SSL/TLS connection for communication with + * Kubernetes. Note that we disable hostname checks here because we + * do our own validation - libwebsockets does not validate properly if + * IP addresses are used. */ + if (settings->use_ssl) { +#ifdef HAVE_LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT + context_info.options = LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT; +#endif +#ifdef HAVE_LCCSCF_USE_SSL + connection_info.ssl_connection = LCCSCF_USE_SSL + | LCCSCF_SKIP_SERVER_CERT_HOSTNAME_CHECK; +#else + connection_info.ssl_connection = 2; /* SSL + no hostname check */ +#endif + } + + /* Create libwebsockets context */ + kubernetes_client->context = lws_create_context(&context_info); + if (!kubernetes_client->context) { + guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR, + "Initialization of libwebsockets failed"); + goto fail; + } + + /* Generate path dynamically */ + connection_info.context = kubernetes_client->context; + connection_info.path = endpoint_path; + + /* Open WebSocket connection to Kubernetes */ + kubernetes_client->wsi = lws_client_connect_via_info(&connection_info); + if (kubernetes_client->wsi == NULL) { + guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR, + "Connection via libwebsockets failed"); + goto fail; + } + + /* Init outbound message buffer */ + pthread_mutex_init(&(kubernetes_client->outbound_message_lock), NULL); + + /* Start input thread */ + if (pthread_create(&(input_thread), NULL, guac_kubernetes_input_thread, (void*) client)) { + guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR, "Unable to start input thread"); + goto fail; + } + + /* Force a redraw of the attached display (there will be no content + * otherwise, given the stream nature of attaching to a running + * container) */ + guac_kubernetes_force_redraw(client); + + /* As long as client is connected, continue polling libwebsockets */ + while (client->state == GUAC_CLIENT_RUNNING) { + + /* Cease polling libwebsockets if an error condition is signalled */ + if (lws_service(kubernetes_client->context, + GUAC_KUBERNETES_SERVICE_INTERVAL) < 0) + break; + + } + + /* Kill client and Wait for input thread to die */ + guac_terminal_stop(kubernetes_client->term); + guac_client_stop(client); + pthread_join(input_thread, NULL); + +fail: + + /* Kill and free terminal, if allocated */ + if (kubernetes_client->term != NULL) + guac_terminal_free(kubernetes_client->term); + + /* Clean up recording, if in progress */ + if (kubernetes_client->recording != NULL) + guac_common_recording_free(kubernetes_client->recording); + + /* Free WebSocket context if successfully allocated */ + if (kubernetes_client->context != NULL) + lws_context_destroy(kubernetes_client->context); + + guac_client_log(client, GUAC_LOG_INFO, "Kubernetes connection ended."); + return NULL; + +} + +void guac_kubernetes_resize(guac_client* client, int rows, int columns) { + + char buffer[64]; + + guac_kubernetes_client* kubernetes_client = + (guac_kubernetes_client*) client->data; + + /* Send request only if different from last request */ + if (kubernetes_client->rows != rows || + kubernetes_client->columns != columns) { + + kubernetes_client->rows = rows; + kubernetes_client->columns = columns; + + /* Construct terminal resize message for Kubernetes */ + int length = snprintf(buffer, sizeof(buffer), + "{\"Width\":%i,\"Height\":%i}", columns, rows); + + /* Schedule message for sending */ + guac_kubernetes_send_message(client, GUAC_KUBERNETES_CHANNEL_RESIZE, + buffer, length); + + } + +} + +void guac_kubernetes_force_redraw(guac_client* client) { + + guac_kubernetes_client* kubernetes_client = + (guac_kubernetes_client*) client->data; + + /* Get current terminal dimensions */ + guac_terminal* term = kubernetes_client->term; + int rows = term->term_height; + int columns = term->term_width; + + /* Force a redraw by increasing the terminal size by one character in + * each dimension and then resizing it back to normal (the same technique + * used by kubectl */ + guac_kubernetes_resize(client, rows + 1, columns + 1); + guac_kubernetes_resize(client, rows, columns); + +} + diff --git a/src/protocols/kubernetes/kubernetes.h b/src/protocols/kubernetes/kubernetes.h new file mode 100644 index 00000000..c37ca4cf --- /dev/null +++ b/src/protocols/kubernetes/kubernetes.h @@ -0,0 +1,168 @@ +/* + * 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. + */ + +#ifndef GUAC_KUBERNETES_H +#define GUAC_KUBERNETES_H + +#include "common/clipboard.h" +#include "common/recording.h" +#include "io.h" +#include "settings.h" +#include "terminal/terminal.h" + +#include +#include + +#include + +/** + * The name of the WebSocket protocol specific to Kubernetes which should be + * sent to the Kubernetes server when attaching to a pod. + */ +#define GUAC_KUBERNETES_LWS_PROTOCOL "v4.channel.k8s.io" + +/** + * The maximum number of messages to allow within the outbound message buffer. + * If messages are sent despite the buffer being full, those messages will be + * dropped. + */ +#define GUAC_KUBERNETES_MAX_OUTBOUND_MESSAGES 8 + +/** + * The maximum number of milliseconds to wait for a libwebsockets event to + * occur before entering another iteration of the libwebsockets event loop. + */ +#define GUAC_KUBERNETES_SERVICE_INTERVAL 1000 + +/** + * Kubernetes-specific client data. + */ +typedef struct guac_kubernetes_client { + + /** + * Kubernetes connection settings. + */ + guac_kubernetes_settings* settings; + + /** + * The libwebsockets context associated with the connected WebSocket. + */ + struct lws_context* context; + + /** + * The connected WebSocket. + */ + struct lws* wsi; + + /** + * Outbound message ring buffer for outbound WebSocket messages. As + * libwebsockets uses an event loop for all operations, outbound messages + * may be sent only in context of a particular event received via a + * callback. Until that event is received, pending data must accumulate in + * a buffer. + */ + guac_kubernetes_message outbound_messages[GUAC_KUBERNETES_MAX_OUTBOUND_MESSAGES]; + + /** + * The number of messages currently waiting in the outbound message + * buffer. + */ + int outbound_messages_waiting; + + /** + * The index of the oldest entry in the outbound message buffer. Newer + * messages follow this entry. + */ + int outbound_messages_top; + + /** + * Lock which is acquired when the outbound message buffer is being read + * or manipulated. + */ + pthread_mutex_t outbound_message_lock; + + /** + * The Kubernetes client thread. + */ + pthread_t client_thread; + + /** + * The current clipboard contents. + */ + guac_common_clipboard* clipboard; + + /** + * The terminal which will render all output from the Kubernetes pod. + */ + guac_terminal* term; + + /** + * The number of rows last sent to Kubernetes in a terminal resize + * request. + */ + int rows; + + /** + * The number of columns last sent to Kubernetes in a terminal resize + * request. + */ + int columns; + + /** + * The in-progress session recording, or NULL if no recording is in + * progress. + */ + guac_common_recording* recording; + +} guac_kubernetes_client; + +/** + * Main Kubernetes client thread, handling transfer of STDOUT/STDERR of an + * attached Kubernetes pod to STDOUT of the terminal. + */ +void* guac_kubernetes_client_thread(void* data); + +/** + * Sends a message to the Kubernetes server requesting that the terminal be + * resized to the given dimensions. This message may be queued until the + * underlying WebSocket connection is ready to send. + * + * @param client + * The guac_client associated with the Kubernetes connection. + * + * @param rows + * The new terminal size in rows. + * + * @param columns + * The new terminal size in columns. + */ +void guac_kubernetes_resize(guac_client* client, int rows, int columns); + +/** + * Sends messages to the Kubernetes server such that the terminal is forced + * to redraw. This function should be invoked at the beginning of each + * session in order to restore expected display state. + * + * @param client + * The guac_client associated with the Kubernetes connection. + */ +void guac_kubernetes_force_redraw(guac_client* client); + +#endif + diff --git a/src/protocols/kubernetes/pipe.c b/src/protocols/kubernetes/pipe.c new file mode 100644 index 00000000..8f18530a --- /dev/null +++ b/src/protocols/kubernetes/pipe.c @@ -0,0 +1,52 @@ +/* + * 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 "kubernetes.h" +#include "terminal/terminal.h" +#include "pipe.h" + +#include +#include +#include +#include +#include + +#include + +int guac_kubernetes_pipe_handler(guac_user* user, guac_stream* stream, + char* mimetype, char* name) { + + guac_client* client = user->client; + guac_kubernetes_client* kubernetes_client = + (guac_kubernetes_client*) client->data; + + /* Redirect STDIN if pipe has required name */ + if (strcmp(name, GUAC_KUBERNETES_STDIN_PIPE_NAME) == 0) { + guac_terminal_send_stream(kubernetes_client->term, user, stream); + return 0; + } + + /* No other inbound pipe streams are supported */ + guac_protocol_send_ack(user->socket, stream, "No such input stream.", + GUAC_PROTOCOL_STATUS_RESOURCE_NOT_FOUND); + guac_socket_flush(user->socket); + return 0; + +} + diff --git a/src/protocols/kubernetes/pipe.h b/src/protocols/kubernetes/pipe.h new file mode 100644 index 00000000..47565bfd --- /dev/null +++ b/src/protocols/kubernetes/pipe.h @@ -0,0 +1,40 @@ +/* + * 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. + */ + + +#ifndef GUAC_KUBERNETES_PIPE_H +#define GUAC_KUBERNETES_PIPE_H + +#include + +/** + * The name reserved for the inbound pipe stream which forces the terminal + * emulator's STDIN to be received from the pipe. + */ +#define GUAC_KUBERNETES_STDIN_PIPE_NAME "STDIN" + +/** + * Handles an incoming stream from a Guacamole "pipe" instruction. If the pipe + * is named "STDIN", the the contents of the pipe stream are redirected to + * STDIN of the terminal emulator for as long as the pipe is open. + */ +guac_user_pipe_handler guac_kubernetes_pipe_handler; + +#endif + diff --git a/src/protocols/kubernetes/settings.c b/src/protocols/kubernetes/settings.c new file mode 100644 index 00000000..4ed5f515 --- /dev/null +++ b/src/protocols/kubernetes/settings.c @@ -0,0 +1,429 @@ +/* + * 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 "settings.h" + +#include + +#include + +/* Client plugin arguments */ +const char* GUAC_KUBERNETES_CLIENT_ARGS[] = { + "hostname", + "port", + "namespace", + "pod", + "container", + "use-ssl", + "client-cert", + "client-key", + "ca-cert", + "ignore-cert", + "font-name", + "font-size", + "color-scheme", + "typescript-path", + "typescript-name", + "create-typescript-path", + "recording-path", + "recording-name", + "recording-exclude-output", + "recording-exclude-mouse", + "recording-include-keys", + "create-recording-path", + "read-only", + "backspace", + "scrollback", + "disable-copy", + "disable-paste", + NULL +}; + +enum KUBERNETES_ARGS_IDX { + + /** + * The hostname to connect to. Required. + */ + IDX_HOSTNAME, + + /** + * The port to connect to. Optional. + */ + IDX_PORT, + + /** + * The name of the Kubernetes namespace of the pod containing the container + * being attached to. If omitted, the default namespace will be used. + */ + IDX_NAMESPACE, + + /** + * The name of the Kubernetes pod containing with the container being + * attached to. Required. + */ + IDX_POD, + + /** + * The name of the container to attach to. If omitted, the first container + * in the pod will be used. + */ + IDX_CONTAINER, + + /** + * Whether SSL/TLS should be used. If omitted, SSL/TLS will not be used. + */ + IDX_USE_SSL, + + /** + * The certificate to use if performing SSL/TLS client authentication to + * authenticate with the Kubernetes server, in PEM format. If omitted, SSL + * client authentication will not be performed. + */ + IDX_CLIENT_CERT, + + /** + * The key to use if performing SSL/TLS client authentication to + * authenticate with the Kubernetes server, in PEM format. If omitted, SSL + * client authentication will not be performed. + */ + IDX_CLIENT_KEY, + + /** + * The certificate of the certificate authority that signed the certificate + * of the Kubernetes server, in PEM format. If omitted. verification of + * the Kubernetes server certificate will use the systemwide certificate + * authorities. + */ + IDX_CA_CERT, + + /** + * Whether the certificate used by the Kubernetes server for SSL/TLS should + * be ignored if it cannot be validated. + */ + IDX_IGNORE_CERT, + + /** + * The name of the font to use within the terminal. + */ + IDX_FONT_NAME, + + /** + * The size of the font to use within the terminal, in points. + */ + IDX_FONT_SIZE, + + /** + * The color scheme to use, as a series of semicolon-separated color-value + * pairs: "background: ", "foreground: ", or + * "color: ", where is a number from 0 to 255, and is + * "color" or an X11 color code (e.g. "aqua" or "rgb:12/34/56"). + * The color scheme can also be one of the special values: "black-white", + * "white-black", "gray-black", or "green-black". + */ + IDX_COLOR_SCHEME, + + /** + * The full absolute path to the directory in which typescripts should be + * written. + */ + IDX_TYPESCRIPT_PATH, + + /** + * The name that should be given to typescripts which are written in the + * given path. Each typescript will consist of two files: "NAME" and + * "NAME.timing". + */ + IDX_TYPESCRIPT_NAME, + + /** + * Whether the specified typescript path should automatically be created + * if it does not yet exist. + */ + IDX_CREATE_TYPESCRIPT_PATH, + + /** + * The full absolute path to the directory in which screen recordings + * should be written. + */ + IDX_RECORDING_PATH, + + /** + * The name that should be given to screen recordings which are written in + * the given path. + */ + IDX_RECORDING_NAME, + + /** + * Whether output which is broadcast to each connected client (graphics, + * streams, etc.) should NOT be included in the session recording. Output + * is included by default, as it is necessary for any recording which must + * later be viewable as video. + */ + IDX_RECORDING_EXCLUDE_OUTPUT, + + /** + * Whether changes to mouse state, such as position and buttons pressed or + * released, should NOT be included in the session recording. Mouse state + * is included by default, as it is necessary for the mouse cursor to be + * rendered in any resulting video. + */ + IDX_RECORDING_EXCLUDE_MOUSE, + + /** + * Whether keys pressed and released should be included in the session + * recording. Key events are NOT included by default within the recording, + * as doing so has privacy and security implications. Including key events + * may be necessary in certain auditing contexts, but should only be done + * with caution. Key events can easily contain sensitive information, such + * as passwords, credit card numbers, etc. + */ + IDX_RECORDING_INCLUDE_KEYS, + + /** + * Whether the specified screen recording path should automatically be + * created if it does not yet exist. + */ + IDX_CREATE_RECORDING_PATH, + + /** + * "true" if this connection should be read-only (user input should be + * dropped), "false" or blank otherwise. + */ + IDX_READ_ONLY, + + /** + * ASCII code, as an integer to use for the backspace key, or 127 + * if not specified. + */ + IDX_BACKSPACE, + + /** + * The maximum size of the scrollback buffer in rows. + */ + IDX_SCROLLBACK, + + /** + * Whether outbound clipboard access should be blocked. If set to "true", + * it will not be possible to copy data from the terminal to the client + * using the clipboard. By default, clipboard access is not blocked. + */ + IDX_DISABLE_COPY, + + /** + * Whether inbound clipboard access should be blocked. If set to "true", it + * will not be possible to paste data from the client to the terminal using + * the clipboard. By default, clipboard access is not blocked. + */ + IDX_DISABLE_PASTE, + + KUBERNETES_ARGS_COUNT +}; + +guac_kubernetes_settings* guac_kubernetes_parse_args(guac_user* user, + int argc, const char** argv) { + + /* Validate arg count */ + if (argc != KUBERNETES_ARGS_COUNT) { + guac_user_log(user, GUAC_LOG_WARNING, "Incorrect number of connection " + "parameters provided: expected %i, got %i.", + KUBERNETES_ARGS_COUNT, argc); + return NULL; + } + + guac_kubernetes_settings* settings = + calloc(1, sizeof(guac_kubernetes_settings)); + + /* Read hostname */ + settings->hostname = + guac_user_parse_args_string(user, GUAC_KUBERNETES_CLIENT_ARGS, argv, + IDX_HOSTNAME, ""); + + /* Read port */ + settings->port = + guac_user_parse_args_int(user, GUAC_KUBERNETES_CLIENT_ARGS, argv, + IDX_PORT, GUAC_KUBERNETES_DEFAULT_PORT); + + /* Read Kubernetes namespace */ + settings->kubernetes_namespace = + guac_user_parse_args_string(user, GUAC_KUBERNETES_CLIENT_ARGS, argv, + IDX_NAMESPACE, GUAC_KUBERNETES_DEFAULT_NAMESPACE); + + /* Read name of Kubernetes pod (required) */ + settings->kubernetes_pod = + guac_user_parse_args_string(user, GUAC_KUBERNETES_CLIENT_ARGS, argv, + IDX_POD, NULL); + + /* Read container of pod (optional) */ + settings->kubernetes_container = + guac_user_parse_args_string(user, GUAC_KUBERNETES_CLIENT_ARGS, argv, + IDX_CONTAINER, NULL); + + /* Parse whether SSL should be used */ + settings->use_ssl = + guac_user_parse_args_boolean(user, GUAC_KUBERNETES_CLIENT_ARGS, argv, + IDX_USE_SSL, false); + + /* Read SSL/TLS connection details only if enabled */ + if (settings->use_ssl) { + + settings->client_cert = + guac_user_parse_args_string(user, GUAC_KUBERNETES_CLIENT_ARGS, + argv, IDX_CLIENT_CERT, NULL); + + settings->client_key = + guac_user_parse_args_string(user, GUAC_KUBERNETES_CLIENT_ARGS, + argv, IDX_CLIENT_KEY, NULL); + + settings->ca_cert = + guac_user_parse_args_string(user, GUAC_KUBERNETES_CLIENT_ARGS, + argv, IDX_CA_CERT, NULL); + + settings->ignore_cert = + guac_user_parse_args_boolean(user, GUAC_KUBERNETES_CLIENT_ARGS, + argv, IDX_IGNORE_CERT, false); + + } + + /* Read-only mode */ + settings->read_only = + guac_user_parse_args_boolean(user, GUAC_KUBERNETES_CLIENT_ARGS, argv, + IDX_READ_ONLY, false); + + /* Read maximum scrollback size */ + settings->max_scrollback = + guac_user_parse_args_int(user, GUAC_KUBERNETES_CLIENT_ARGS, argv, + IDX_SCROLLBACK, GUAC_KUBERNETES_DEFAULT_MAX_SCROLLBACK); + + /* Read font name */ + settings->font_name = + guac_user_parse_args_string(user, GUAC_KUBERNETES_CLIENT_ARGS, argv, + IDX_FONT_NAME, GUAC_KUBERNETES_DEFAULT_FONT_NAME); + + /* Read font size */ + settings->font_size = + guac_user_parse_args_int(user, GUAC_KUBERNETES_CLIENT_ARGS, argv, + IDX_FONT_SIZE, GUAC_KUBERNETES_DEFAULT_FONT_SIZE); + + /* Copy requested color scheme */ + settings->color_scheme = + guac_user_parse_args_string(user, GUAC_KUBERNETES_CLIENT_ARGS, argv, + IDX_COLOR_SCHEME, ""); + + /* Pull width/height/resolution directly from user */ + settings->width = user->info.optimal_width; + settings->height = user->info.optimal_height; + settings->resolution = user->info.optimal_resolution; + + /* Read typescript path */ + settings->typescript_path = + guac_user_parse_args_string(user, GUAC_KUBERNETES_CLIENT_ARGS, argv, + IDX_TYPESCRIPT_PATH, NULL); + + /* Read typescript name */ + settings->typescript_name = + guac_user_parse_args_string(user, GUAC_KUBERNETES_CLIENT_ARGS, argv, + IDX_TYPESCRIPT_NAME, GUAC_KUBERNETES_DEFAULT_TYPESCRIPT_NAME); + + /* Parse path creation flag */ + settings->create_typescript_path = + guac_user_parse_args_boolean(user, GUAC_KUBERNETES_CLIENT_ARGS, argv, + IDX_CREATE_TYPESCRIPT_PATH, false); + + /* Read recording path */ + settings->recording_path = + guac_user_parse_args_string(user, GUAC_KUBERNETES_CLIENT_ARGS, argv, + IDX_RECORDING_PATH, NULL); + + /* Read recording name */ + settings->recording_name = + guac_user_parse_args_string(user, GUAC_KUBERNETES_CLIENT_ARGS, argv, + IDX_RECORDING_NAME, GUAC_KUBERNETES_DEFAULT_RECORDING_NAME); + + /* Parse output exclusion flag */ + settings->recording_exclude_output = + guac_user_parse_args_boolean(user, GUAC_KUBERNETES_CLIENT_ARGS, argv, + IDX_RECORDING_EXCLUDE_OUTPUT, false); + + /* Parse mouse exclusion flag */ + settings->recording_exclude_mouse = + guac_user_parse_args_boolean(user, GUAC_KUBERNETES_CLIENT_ARGS, argv, + IDX_RECORDING_EXCLUDE_MOUSE, false); + + /* Parse key event inclusion flag */ + settings->recording_include_keys = + guac_user_parse_args_boolean(user, GUAC_KUBERNETES_CLIENT_ARGS, argv, + IDX_RECORDING_INCLUDE_KEYS, false); + + /* Parse path creation flag */ + settings->create_recording_path = + guac_user_parse_args_boolean(user, GUAC_KUBERNETES_CLIENT_ARGS, argv, + IDX_CREATE_RECORDING_PATH, false); + + /* Parse backspace key code */ + settings->backspace = + guac_user_parse_args_int(user, GUAC_KUBERNETES_CLIENT_ARGS, argv, + IDX_BACKSPACE, 127); + + /* Parse clipboard copy disable flag */ + settings->disable_copy = + guac_user_parse_args_boolean(user, GUAC_KUBERNETES_CLIENT_ARGS, argv, + IDX_DISABLE_COPY, false); + + /* Parse clipboard paste disable flag */ + settings->disable_paste = + guac_user_parse_args_boolean(user, GUAC_KUBERNETES_CLIENT_ARGS, argv, + IDX_DISABLE_PASTE, false); + + /* Parsing was successful */ + return settings; + +} + +void guac_kubernetes_settings_free(guac_kubernetes_settings* settings) { + + /* Free network connection information */ + free(settings->hostname); + + /* Free Kubernetes pod/container details */ + free(settings->kubernetes_namespace); + free(settings->kubernetes_pod); + free(settings->kubernetes_container); + + /* Free SSL/TLS details */ + free(settings->client_cert); + free(settings->client_key); + free(settings->ca_cert); + + /* Free display preferences */ + free(settings->font_name); + free(settings->color_scheme); + + /* Free typescript settings */ + free(settings->typescript_name); + free(settings->typescript_path); + + /* Free screen recording settings */ + free(settings->recording_name); + free(settings->recording_path); + + /* Free overall structure */ + free(settings); + +} + diff --git a/src/protocols/kubernetes/settings.h b/src/protocols/kubernetes/settings.h new file mode 100644 index 00000000..eef4973e --- /dev/null +++ b/src/protocols/kubernetes/settings.h @@ -0,0 +1,293 @@ +/* + * 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. + */ + +#ifndef GUAC_KUBERNETES_SETTINGS_H +#define GUAC_KUBERNETES_SETTINGS_H + +#include + +#include + +/** + * The name of the font to use for the terminal if no name is specified. + */ +#define GUAC_KUBERNETES_DEFAULT_FONT_NAME "monospace" + +/** + * The size of the font to use for the terminal if no font size is specified, + * in points. + */ +#define GUAC_KUBERNETES_DEFAULT_FONT_SIZE 12 + +/** + * The port to connect to when initiating any Kubernetes connection, if no + * other port is specified. + */ +#define GUAC_KUBERNETES_DEFAULT_PORT 8080 + +/** + * The name of the Kubernetes namespace that should be used by default if no + * specific Kubernetes namespace is provided. + */ +#define GUAC_KUBERNETES_DEFAULT_NAMESPACE "default" + +/** + * The filename to use for the typescript, if not specified. + */ +#define GUAC_KUBERNETES_DEFAULT_TYPESCRIPT_NAME "typescript" + +/** + * The filename to use for the screen recording, if not specified. + */ +#define GUAC_KUBERNETES_DEFAULT_RECORDING_NAME "recording" + +/** + * The default maximum scrollback size in rows. + */ +#define GUAC_KUBERNETES_DEFAULT_MAX_SCROLLBACK 1000 + +/** + * Settings for the Kubernetes connection. The values for this structure are + * parsed from the arguments given during the Guacamole protocol handshake + * using the guac_kubernetes_parse_args() function. + */ +typedef struct guac_kubernetes_settings { + + /** + * The hostname of the Kubernetes server to connect to. + */ + char* hostname; + + /** + * The port of the Kubernetes server to connect to. + */ + int port; + + /** + * The name of the Kubernetes namespace of the pod containing the container + * being attached to. + */ + char* kubernetes_namespace; + + /** + * The name of the Kubernetes pod containing with the container being + * attached to. + */ + char* kubernetes_pod; + + /** + * The name of the container to attach to, or NULL to arbitrarily attach to + * the first container in the pod. + */ + char* kubernetes_container; + + /** + * Whether SSL/TLS should be used. + */ + bool use_ssl; + + /** + * The certificate to use if performing SSL/TLS client authentication to + * authenticate with the Kubernetes server, in PEM format. If omitted, SSL + * client authentication will not be performed. + */ + char* client_cert; + + /** + * The key to use if performing SSL/TLS client authentication to + * authenticate with the Kubernetes server, in PEM format. If omitted, SSL + * client authentication will not be performed. + */ + char* client_key; + + /** + * The certificate of the certificate authority that signed the certificate + * of the Kubernetes server, in PEM format. If omitted. verification of + * the Kubernetes server certificate will use the systemwide certificate + * authorities. + */ + char* ca_cert; + + /** + * Whether the certificate used by the Kubernetes server for SSL/TLS should + * be ignored if it cannot be validated. + */ + bool ignore_cert; + + /** + * Whether this connection is read-only, and user input should be dropped. + */ + bool read_only; + + /** + * The maximum size of the scrollback buffer in rows. + */ + int max_scrollback; + + /** + * The name of the font to use for display rendering. + */ + char* font_name; + + /** + * The size of the font to use, in points. + */ + int font_size; + + /** + * The name of the color scheme to use. + */ + char* color_scheme; + + /** + * The desired width of the terminal display, in pixels. + */ + int width; + + /** + * The desired height of the terminal display, in pixels. + */ + int height; + + /** + * The desired screen resolution, in DPI. + */ + int resolution; + + /** + * Whether outbound clipboard access should be blocked. If set, it will not + * be possible to copy data from the terminal to the client using the + * clipboard. + */ + bool disable_copy; + + /** + * Whether inbound clipboard access should be blocked. If set, it will not + * be possible to paste data from the client to the terminal using the + * clipboard. + */ + bool disable_paste; + + /** + * The path in which the typescript should be saved, if enabled. If no + * typescript should be saved, this will be NULL. + */ + char* typescript_path; + + /** + * The filename to use for the typescript, if enabled. + */ + char* typescript_name; + + /** + * Whether the typescript path should be automatically created if it does + * not already exist. + */ + bool create_typescript_path; + + /** + * The path in which the screen recording should be saved, if enabled. If + * no screen recording should be saved, this will be NULL. + */ + char* recording_path; + + /** + * The filename to use for the screen recording, if enabled. + */ + char* recording_name; + + /** + * Whether the screen recording path should be automatically created if it + * does not already exist. + */ + bool create_recording_path; + + /** + * Whether output which is broadcast to each connected client (graphics, + * streams, etc.) should NOT be included in the session recording. Output + * is included by default, as it is necessary for any recording which must + * later be viewable as video. + */ + bool recording_exclude_output; + + /** + * Whether changes to mouse state, such as position and buttons pressed or + * released, should NOT be included in the session recording. Mouse state + * is included by default, as it is necessary for the mouse cursor to be + * rendered in any resulting video. + */ + bool recording_exclude_mouse; + + /** + * Whether keys pressed and released should be included in the session + * recording. Key events are NOT included by default within the recording, + * as doing so has privacy and security implications. Including key events + * may be necessary in certain auditing contexts, but should only be done + * with caution. Key events can easily contain sensitive information, such + * as passwords, credit card numbers, etc. + */ + bool recording_include_keys; + + /** + * The ASCII code, as an integer, that the Kubernetes client will use when + * the backspace key is pressed. By default, this is 127, ASCII delete, if + * not specified in the client settings. + */ + int backspace; + +} guac_kubernetes_settings; + +/** + * Parses all given args, storing them in a newly-allocated settings object. If + * the args fail to parse, NULL is returned. + * + * @param user + * The user who submitted the given arguments while joining the + * connection. + * + * @param argc + * The number of arguments within the argv array. + * + * @param argv + * The values of all arguments provided by the user. + * + * @return + * A newly-allocated settings object which must be freed with + * guac_kubernetes_settings_free() when no longer needed. If the arguments + * fail to parse, NULL is returned. + */ +guac_kubernetes_settings* guac_kubernetes_parse_args(guac_user* user, + int argc, const char** argv); + +/** + * Frees the given guac_kubernetes_settings object, having been previously + * allocated via guac_kubernetes_parse_args(). + * + * @param settings + * The settings object to free. + */ +void guac_kubernetes_settings_free(guac_kubernetes_settings* settings); + +/** + * NULL-terminated array of accepted client args. + */ +extern const char* GUAC_KUBERNETES_CLIENT_ARGS[]; + +#endif + diff --git a/src/protocols/kubernetes/ssl.c b/src/protocols/kubernetes/ssl.c new file mode 100644 index 00000000..520ce8cb --- /dev/null +++ b/src/protocols/kubernetes/ssl.c @@ -0,0 +1,234 @@ +/* + * 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 "kubernetes.h" +#include "settings.h" + +#include +#include +#include +#include +#include +#include +#include + +/** + * Tests whether the given hostname is, in fact, an IP address. + * + * @param hostname + * The hostname to test. + * + * @return + * Non-zero if the given hostname is an IP address, zero otherwise. + */ +static int guac_kubernetes_is_address(const char* hostname) { + + /* Attempt to interpret the hostname as an IP address */ + ASN1_OCTET_STRING* ip = a2i_IPADDRESS(hostname); + + /* If unsuccessful, the hostname is not an IP address */ + if (ip == NULL) + return 0; + + /* Converted hostname must be freed */ + ASN1_OCTET_STRING_free(ip); + return 1; + +} + +/** + * Parses the given PEM certificate, returning a new OpenSSL X509 structure + * representing that certificate. + * + * @param pem + * The PEM certificate. + * + * @return + * An X509 structure representing the given certificate, or NULL if the + * certificate was unreadable. + */ +static X509* guac_kubernetes_read_cert(char* pem) { + + /* Prepare a BIO which provides access to the in-memory CA cert */ + BIO* bio = BIO_new_mem_buf(pem, -1); + if (bio == NULL) + return NULL; + + /* Read the CA cert as PEM */ + X509* certificate = PEM_read_bio_X509(bio, NULL, NULL, NULL); + if (certificate == NULL) { + BIO_free(bio); + return NULL; + } + + return certificate; + +} + +/** + * Parses the given PEM private key, returning a new OpenSSL EVP_PKEY structure + * representing that key. + * + * @param pem + * The PEM private key. + * + * @return + * An EVP_KEY representing the given private key, or NULL if the private + * key was unreadable. + */ +static EVP_PKEY* guac_kubernetes_read_key(char* pem) { + + /* Prepare a BIO which provides access to the in-memory key */ + BIO* bio = BIO_new_mem_buf(pem, -1); + if (bio == NULL) + return NULL; + + /* Read the private key as PEM */ + EVP_PKEY* key = PEM_read_bio_PrivateKey(bio, NULL, NULL, NULL); + if (key == NULL) { + BIO_free(bio); + return NULL; + } + + return key; + +} + +/** + * OpenSSL certificate verification callback which universally accepts all + * certificates without performing any verification at all. + * + * @param x509_ctx + * The current context of the certificate verification process. This + * parameter is ignored by this particular implementation of the callback. + * + * @param arg + * The arbitrary value passed to SSL_CTX_set_cert_verify_callback(). This + * parameter is ignored by this particular implementation of the callback. + * + * @return + * Strictly 0 if certificate verification fails, 1 if the certificate is + * verified. No other values are legal return values for this callback as + * documented by OpenSSL. + */ +static int guac_kubernetes_assume_cert_ok(X509_STORE_CTX* x509_ctx, void* arg) { + return 1; +} + +void guac_kubernetes_init_ssl(guac_client* client, SSL_CTX* context) { + + guac_kubernetes_client* kubernetes_client = + (guac_kubernetes_client*) client->data; + + guac_kubernetes_settings* settings = kubernetes_client->settings; + + /* Bypass certificate checks if requested */ + if (settings->ignore_cert) { + SSL_CTX_set_verify(context, SSL_VERIFY_PEER, NULL); + SSL_CTX_set_cert_verify_callback(context, + guac_kubernetes_assume_cert_ok, NULL); + } + + /* Otherwise use the given CA certificate to validate (if any) */ + else if (settings->ca_cert != NULL) { + + /* Read CA certificate from configuration data */ + X509* ca_cert = guac_kubernetes_read_cert(settings->ca_cert); + if (ca_cert == NULL) { + guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR, + "Provided CA certificate is unreadable"); + return; + } + + /* Add certificate to CA store */ + X509_STORE* ca_store = SSL_CTX_get_cert_store(context); + if (!X509_STORE_add_cert(ca_store, ca_cert)) { + guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR, + "Unable to add CA certificate to certificate store of " + "SSL context"); + return; + } + + } + + /* Certificate for SSL/TLS client auth */ + if (settings->client_cert != NULL) { + + /* Read client certificate from configuration data */ + X509* client_cert = guac_kubernetes_read_cert(settings->client_cert); + if (client_cert == NULL) { + guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR, + "Provided client certificate is unreadable"); + return; + } + + /* Use parsed certificate for authentication */ + if (!SSL_CTX_use_certificate(context, client_cert)) { + guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR, + "Client certificate could not be used for SSL/TLS " + "client authentication"); + return; + } + + } + + /* Private key for SSL/TLS client auth */ + if (settings->client_key != NULL) { + + /* Read client private key from configuration data */ + EVP_PKEY* client_key = guac_kubernetes_read_key(settings->client_key); + if (client_key == NULL) { + guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR, + "Provided client private key is unreadable"); + return; + } + + /* Use parsed key for authentication */ + if (!SSL_CTX_use_PrivateKey(context, client_key)) { + guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR, + "Client private key could not be used for SSL/TLS " + "client authentication"); + return; + } + + } + + /* Enable hostname checking */ + X509_VERIFY_PARAM *param = SSL_CTX_get0_param(context); + X509_VERIFY_PARAM_set_hostflags(param, + X509_CHECK_FLAG_NO_PARTIAL_WILDCARDS); + + /* Validate properly depending on whether hostname is an IP address */ + if (guac_kubernetes_is_address(settings->hostname)) { + if (!X509_VERIFY_PARAM_set1_ip_asc(param, settings->hostname)) { + guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR, + "Server IP address validation could not be enabled"); + return; + } + } + else { + if (!X509_VERIFY_PARAM_set1_host(param, settings->hostname, 0)) { + guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR, + "Server hostname validation could not be enabled"); + return; + } + } + +} + diff --git a/src/protocols/kubernetes/ssl.h b/src/protocols/kubernetes/ssl.h new file mode 100644 index 00000000..cca02bdb --- /dev/null +++ b/src/protocols/kubernetes/ssl.h @@ -0,0 +1,41 @@ +/* + * 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. + */ + +#ifndef GUAC_KUBERNETES_SSL_H +#define GUAC_KUBERNETES_SSL_H + +#include "settings.h" + +#include + +/** + * Initializes the given SSL/TLS context using the configuration parameters + * associated with the given guac_client, setting up hostname/address + * validation and client authentication. + * + * @param client + * The guac_client associated with the Kubernetes connection. + * + * @param context + * The SSL_CTX in use by libwebsockets. + */ +void guac_kubernetes_init_ssl(guac_client* client, SSL_CTX* context); + +#endif + diff --git a/src/protocols/kubernetes/url.c b/src/protocols/kubernetes/url.c new file mode 100644 index 00000000..78c116e5 --- /dev/null +++ b/src/protocols/kubernetes/url.c @@ -0,0 +1,137 @@ +/* + * 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 "url.h" + +#include +#include +#include + +/** + * Returns whether the given character is a character that need not be + * escaped when included as part of a component of a URL. + * + * @param c + * The character to test. + * + * @return + * Zero if the character does not need to be escaped when included as + * part of a component of a URL, non-zero otherwise. + */ +static int guac_kubernetes_is_url_safe(char c) { + return (c >= 'A' && c <= 'Z') + || (c >= 'a' && c <= 'z') + || (c >= '0' && c <= '9') + || strchr("-_.!~*'()", c) != NULL; +} + +int guac_kubernetes_escape_url_component(char* output, int length, + const char* str) { + + char* current = output; + while (*str != '\0') { + + char c = *str; + + /* Store alphanumeric characters verbatim */ + if (guac_kubernetes_is_url_safe(c)) { + + /* Verify space exists for single character */ + if (length < 1) + return 1; + + *(current++) = c; + length--; + + } + + /* Escape EVERYTHING else as hex */ + else { + + /* Verify space exists for hex-encoded character */ + if (length < 4) + return 1; + + snprintf(current, 4, "%%%02X", (int) c); + + current += 3; + length -= 3; + } + + /* Next character */ + str++; + + } + + /* Verify space exists for null terminator */ + if (length < 1) + return 1; + + /* Append null terminator */ + *current = '\0'; + return 0; + +} + +int guac_kubernetes_endpoint_attach(char* buffer, int length, + const char* kubernetes_namespace, const char* kubernetes_pod, + const char* kubernetes_container) { + + int written; + + char escaped_namespace[GUAC_KUBERNETES_MAX_ENDPOINT_LENGTH]; + char escaped_pod[GUAC_KUBERNETES_MAX_ENDPOINT_LENGTH]; + char escaped_container[GUAC_KUBERNETES_MAX_ENDPOINT_LENGTH]; + + /* Escape Kubernetes namespace */ + if (guac_kubernetes_escape_url_component(escaped_namespace, + sizeof(escaped_namespace), kubernetes_namespace)) + return 1; + + /* Escape name of Kubernetes pod */ + if (guac_kubernetes_escape_url_component(escaped_pod, + sizeof(escaped_pod), kubernetes_pod)) + return 1; + + /* Generate attachment endpoint URL */ + if (kubernetes_container != NULL) { + + /* Escape container name */ + if (guac_kubernetes_escape_url_component(escaped_container, + sizeof(escaped_container), kubernetes_container)) + return 1; + + written = snprintf(buffer, length, + "/api/v1/namespaces/%s/pods/%s/attach" + "?container=%s&stdin=true&stdout=true&tty=true", + escaped_namespace, escaped_pod, escaped_container); + } + else { + written = snprintf(buffer, length, + "/api/v1/namespaces/%s/pods/%s/attach" + "?stdin=true&stdout=true&tty=true", + escaped_namespace, escaped_pod); + } + + /* Endpoint URL was successfully generated if it was written to the given + * buffer without truncation */ + return !(written < length - 1); + +} + diff --git a/src/protocols/kubernetes/url.h b/src/protocols/kubernetes/url.h new file mode 100644 index 00000000..285baa21 --- /dev/null +++ b/src/protocols/kubernetes/url.h @@ -0,0 +1,85 @@ +/* + * 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. + */ + +#ifndef GUAC_KUBERNETES_URL_H +#define GUAC_KUBERNETES_URL_H + +/** + * The maximum number of characters allowed in the full path for any Kubernetes + * endpoint. + */ +#define GUAC_KUBERNETES_MAX_ENDPOINT_LENGTH 1024 + +/** + * Escapes the given string such that it can be included safely within a URL. + * This function duplicates the behavior of JavaScript's encodeURIComponent(), + * escaping all but the following characters: A-Z a-z 0-9 - _ . ! ~ * ' ( ) + * + * @param output + * The buffer which should receive the escaped string. This buffer may be + * touched even if escaping is unsuccessful. + * + * @param length + * The number of bytes available in the given output buffer. + * + * @param str + * The string to escape. + * + * @return + * Zero if the string was successfully escaped and written into the + * provided output buffer without being truncated, including null + * terminator, non-zero otherwise. + */ +int guac_kubernetes_escape_url_component(char* output, int length, + const char* str); + +/** + * Generates the full path to the Kubernetes API endpoint which handles + * attaching to running containers within specific pods. Values within the path + * will be URL-escaped as necessary. + * + * @param buffer + * The buffer which should receive the endpoint path. This buffer may be + * touched even if the endpoint path could not be generated. + * + * @param length + * The number of bytes available in the given buffer. + * + * @param kubernetes_namespace + * The name of the Kubernetes namespace of the pod containing the container + * being attached to. + * + * @param kubernetes_pod + * The name of the Kubernetes pod containing with the container being + * attached to. + * + * @param kubernetes_container + * The name of the container to attach to, or NULL to arbitrarily attach + * to the first container in the pod. + * + * @return + * Zero if the endpoint path was successfully written to the provided + * buffer, non-zero if insufficient space exists within the buffer. + */ +int guac_kubernetes_endpoint_attach(char* buffer, int length, + const char* kubernetes_namespace, const char* kubernetes_pod, + const char* kubernetes_container); + +#endif + diff --git a/src/protocols/kubernetes/user.c b/src/protocols/kubernetes/user.c new file mode 100644 index 00000000..d1fcd5da --- /dev/null +++ b/src/protocols/kubernetes/user.c @@ -0,0 +1,124 @@ +/* + * 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 "argv.h" +#include "clipboard.h" +#include "common/cursor.h" +#include "input.h" +#include "kubernetes.h" +#include "pipe.h" +#include "settings.h" +#include "terminal/terminal.h" +#include "user.h" + +#include +#include +#include +#include + +#include +#include + +int guac_kubernetes_user_join_handler(guac_user* user, int argc, char** argv) { + + guac_client* client = user->client; + guac_kubernetes_client* kubernetes_client = + (guac_kubernetes_client*) client->data; + + /* Parse provided arguments */ + guac_kubernetes_settings* settings = guac_kubernetes_parse_args(user, + argc, (const char**) argv); + + /* Fail if settings cannot be parsed */ + if (settings == NULL) { + guac_user_log(user, GUAC_LOG_INFO, + "Badly formatted client arguments."); + return 1; + } + + /* Store settings at user level */ + user->data = settings; + + /* Connect to Kubernetes if owner */ + if (user->owner) { + + /* Store owner's settings at client level */ + kubernetes_client->settings = settings; + + /* Start client thread */ + if (pthread_create(&(kubernetes_client->client_thread), NULL, + guac_kubernetes_client_thread, (void*) client)) { + guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR, + "Unable to start Kubernetes client thread"); + return 1; + } + + } + + /* If not owner, synchronize with current display */ + else { + guac_terminal_dup(kubernetes_client->term, user, user->socket); + guac_kubernetes_send_current_argv(user, kubernetes_client); + guac_socket_flush(user->socket); + } + + /* Only handle events if not read-only */ + if (!settings->read_only) { + + /* General mouse/keyboard events */ + user->key_handler = guac_kubernetes_user_key_handler; + user->mouse_handler = guac_kubernetes_user_mouse_handler; + + /* Inbound (client to server) clipboard transfer */ + if (!settings->disable_paste) + user->clipboard_handler = guac_kubernetes_clipboard_handler; + + /* STDIN redirection */ + user->pipe_handler = guac_kubernetes_pipe_handler; + + /* Updates to connection parameters */ + user->argv_handler = guac_kubernetes_argv_handler; + + /* Display size change events */ + user->size_handler = guac_kubernetes_user_size_handler; + + } + + return 0; + +} + +int guac_kubernetes_user_leave_handler(guac_user* user) { + + guac_kubernetes_client* kubernetes_client = + (guac_kubernetes_client*) user->client->data; + + /* Update shared cursor state */ + guac_common_cursor_remove_user(kubernetes_client->term->cursor, user); + + /* Free settings if not owner (owner settings will be freed with client) */ + if (!user->owner) { + guac_kubernetes_settings* settings = + (guac_kubernetes_settings*) user->data; + guac_kubernetes_settings_free(settings); + } + + return 0; +} + diff --git a/tests/client/client_suite.h b/src/protocols/kubernetes/user.h similarity index 74% rename from tests/client/client_suite.h rename to src/protocols/kubernetes/user.h index da1bc22b..55d49fdf 100644 --- a/tests/client/client_suite.h +++ b/src/protocols/kubernetes/user.h @@ -17,16 +17,20 @@ * under the License. */ +#ifndef GUAC_KUBERNETES_USER_H +#define GUAC_KUBERNETES_USER_H -#ifndef _GUAC_TEST_CLIENT_SUITE_H -#define _GUAC_TEST_CLIENT_SUITE_H +#include -#include "config.h" +/** + * Handler for joining users. + */ +guac_user_join_handler guac_kubernetes_user_join_handler; -int register_client_suite(); - -void test_layer_pool(); -void test_buffer_pool(); +/** + * Handler for leaving users. + */ +guac_user_leave_handler guac_kubernetes_user_leave_handler; #endif diff --git a/src/protocols/rdp/.gitignore b/src/protocols/rdp/.gitignore index dd8ff144..9f87ecb2 100644 --- a/src/protocols/rdp/.gitignore +++ b/src/protocols/rdp/.gitignore @@ -1,38 +1,7 @@ -# Object code -*.o -*.so -*.lo -*.la - -# Backup files -*~ - -# Release files -*.tar.gz - -# Files currently being edited by vim or vi -*.swp - -# automake/autoconf -.deps/ -.libs/ -Makefile -Makefile.in -aclocal.m4 -autom4te.cache/ -m4/* -!README -config.guess -config.log -config.status -config.sub -configure -depcomp -install-sh -libtool -ltmain.sh -missing +# Auto-generated test runner and binary +_generated_runner.c +test_rdp # Autogenerated sources _generated_keymaps.c diff --git a/src/protocols/rdp/Makefile.am b/src/protocols/rdp/Makefile.am index cffabb2c..d679a713 100644 --- a/src/protocols/rdp/Makefile.am +++ b/src/protocols/rdp/Makefile.am @@ -16,11 +16,18 @@ # specific language governing permissions and limitations # under the License. # +# NOTE: Parts of this file (Makefile.am) are automatically transcluded verbatim +# into Makefile.in. Though the build system (GNU Autotools) automatically adds +# its own license boilerplate to the generated Makefile.in, that boilerplate +# does not apply to the transcluded portions of Makefile.am which are licensed +# to you by the ASF under the Apache License, Version 2.0, as described above. +# AUTOMAKE_OPTIONS = foreign ACLOCAL_AMFLAGS = -I m4 lib_LTLIBRARIES = libguac-client-rdp.la +SUBDIRS = . tests nodist_libguac_client_rdp_la_SOURCES = \ _generated_keymaps.c @@ -149,7 +156,8 @@ libguac_client_rdp_la_LDFLAGS = \ -version-info 0:0:0 \ @CAIRO_LIBS@ \ @PTHREAD_LIBS@ \ - @RDP_LIBS@ + @RDP_LIBS@ \ + @WINPR_LIBS@ libguac_client_rdp_la_LIBADD = \ @COMMON_LTLIB@ \ @@ -168,7 +176,8 @@ guacdr_cflags = \ guacdr_ldflags = \ -module -avoid-version -shared \ @PTHREAD_LIBS@ \ - @RDP_LIBS@ + @RDP_LIBS@ \ + @WINPR_LIBS@ guacdr_libadd = \ @COMMON_LTLIB@ \ @@ -187,7 +196,8 @@ guacai_cflags = \ guacai_ldflags = \ -module -avoid-version -shared \ @PTHREAD_LIBS@ \ - @RDP_LIBS@ + @RDP_LIBS@ \ + @WINPR_LIBS@ guacai_libadd = \ @COMMON_LTLIB@ \ @@ -206,7 +216,8 @@ guacsnd_cflags = \ guacsnd_ldflags = \ -module -avoid-version -shared \ @PTHREAD_LIBS@ \ - @RDP_LIBS@ + @RDP_LIBS@ \ + @WINPR_LIBS@ guacsnd_libadd = \ @COMMON_LTLIB@ \ @@ -225,7 +236,8 @@ guacsvc_cflags = \ guacsvc_ldflags = \ -module -avoid-version -shared \ @PTHREAD_LIBS@ \ - @RDP_LIBS@ + @RDP_LIBS@ \ + @WINPR_LIBS@ guacsvc_libadd = \ @COMMON_LTLIB@ \ @@ -252,15 +264,18 @@ rdp_keymaps = \ $(srcdir)/keymaps/base.keymap \ $(srcdir)/keymaps/failsafe.keymap \ $(srcdir)/keymaps/de_de_qwertz.keymap \ + $(srcdir)/keymaps/de_ch_qwertz.keymap \ $(srcdir)/keymaps/en_gb_qwerty.keymap \ $(srcdir)/keymaps/en_us_qwerty.keymap \ $(srcdir)/keymaps/es_es_qwerty.keymap \ $(srcdir)/keymaps/fr_fr_azerty.keymap \ $(srcdir)/keymaps/fr_ch_qwertz.keymap \ + $(srcdir)/keymaps/hu_hu_qwertz.keymap \ $(srcdir)/keymaps/it_it_qwerty.keymap \ $(srcdir)/keymaps/ja_jp_qwerty.keymap \ $(srcdir)/keymaps/pt_br_qwerty.keymap \ $(srcdir)/keymaps/sv_se_qwerty.keymap \ + $(srcdir)/keymaps/da_dk_qwerty.keymap \ $(srcdir)/keymaps/tr_tr_qwerty.keymap _generated_keymaps.c: $(rdp_keymaps) diff --git a/src/protocols/rdp/guac_rdpsnd/rdpsnd_service.c b/src/protocols/rdp/guac_rdpsnd/rdpsnd_service.c index cc367c4f..45de442d 100644 --- a/src/protocols/rdp/guac_rdpsnd/rdpsnd_service.c +++ b/src/protocols/rdp/guac_rdpsnd/rdpsnd_service.c @@ -110,35 +110,36 @@ void guac_rdpsnd_process_receive(rdpSvcPlugin* plugin, * If next PDU is SNDWAVE (due to receiving WaveInfo PDU previously), * ignore the header and parse as a Wave PDU. */ - if (rdpsnd->next_pdu_is_wave) { + if (rdpsnd->next_pdu_is_wave) guac_rdpsnd_wave_handler(rdpsnd, input_stream, &header); - return; - } - + /* Dispatch message to standard handlers */ - switch (header.message_type) { + else { + switch (header.message_type) { - /* Server Audio Formats and Version PDU */ - case SNDC_FORMATS: - guac_rdpsnd_formats_handler(rdpsnd, input_stream, &header); - break; + /* Server Audio Formats and Version PDU */ + case SNDC_FORMATS: + guac_rdpsnd_formats_handler(rdpsnd, input_stream, &header); + break; - /* Training PDU */ - case SNDC_TRAINING: - guac_rdpsnd_training_handler(rdpsnd, input_stream, &header); - break; + /* Training PDU */ + case SNDC_TRAINING: + guac_rdpsnd_training_handler(rdpsnd, input_stream, &header); + break; - /* WaveInfo PDU */ - case SNDC_WAVE: - guac_rdpsnd_wave_info_handler(rdpsnd, input_stream, &header); - break; + /* WaveInfo PDU */ + case SNDC_WAVE: + guac_rdpsnd_wave_info_handler(rdpsnd, input_stream, &header); + break; - /* Close PDU */ - case SNDC_CLOSE: - guac_rdpsnd_close_handler(rdpsnd, input_stream, &header); - break; + /* Close PDU */ + case SNDC_CLOSE: + guac_rdpsnd_close_handler(rdpsnd, input_stream, &header); + break; + } } + Stream_Free(input_stream, TRUE); } diff --git a/src/protocols/rdp/guac_svc/svc_service.c b/src/protocols/rdp/guac_svc/svc_service.c index 4a38cb38..c40c6c5f 100644 --- a/src/protocols/rdp/guac_svc/svc_service.c +++ b/src/protocols/rdp/guac_svc/svc_service.c @@ -29,6 +29,7 @@ #include #include #include +#include #ifdef ENABLE_WINPR #include @@ -53,7 +54,7 @@ int VirtualChannelEntry(PCHANNEL_ENTRY_POINTS pEntryPoints) { guac_rdp_svc* svc = (guac_rdp_svc*) entry_points_ex->pExtendedData; /* Init channel def */ - strncpy(svc_plugin->plugin.channel_def.name, svc->name, + guac_strlcpy(svc_plugin->plugin.channel_def.name, svc->name, GUAC_RDP_SVC_MAX_LENGTH); svc_plugin->plugin.channel_def.options = CHANNEL_OPTION_INITIALIZED diff --git a/src/protocols/rdp/keymaps/base.keymap b/src/protocols/rdp/keymaps/base.keymap index 4e49f741..d02c5eab 100644 --- a/src/protocols/rdp/keymaps/base.keymap +++ b/src/protocols/rdp/keymaps/base.keymap @@ -42,7 +42,7 @@ map +ext 0x37 ~ 0xff61 # Print Screen # Locks map 0x45 ~ 0xff7f # Num_Lock map 0x46 ~ 0xff14 # Scroll_Lock -map +ext 0x3A ~ 0xffe5 # Caps_Lock +map 0x3A ~ 0xffe5 # Caps_Lock # Keypad numerals map -shift +num 0x52 ~ 0xffb0 # KP_0 @@ -81,7 +81,7 @@ map 0x58 ~ 0xffc9 # F12 map 0x2A ~ 0xffe1 # Shift_L map 0x36 ~ 0xffe2 # Shift_R map 0x1D ~ 0xffe3 # Control_L -map 0x9D ~ 0xffe4 # Control_R +map +ext 0x1D ~ 0xffe4 # Control_R map 0x38 ~ 0xffe9 # Alt_L map +ext 0x38 ~ 0xffea # Alt_R map +ext 0x5B ~ 0xffeb # Super_L diff --git a/src/protocols/rdp/keymaps/da_dk_qwerty.keymap b/src/protocols/rdp/keymaps/da_dk_qwerty.keymap new file mode 100644 index 00000000..c4893d1b --- /dev/null +++ b/src/protocols/rdp/keymaps/da_dk_qwerty.keymap @@ -0,0 +1,64 @@ +# +# 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. +# + +parent "base" +name "da-dk-qwerty" +freerdp "KBD_DANISH" + +# +# Basic keys +# + +map -altgr -shift 0x29 0x02..0x0C ~ "§1234567890+" +map -altgr -shift 0x10..0x1A ~ "qwertyuiopå" +map -altgr -shift 0x1E..0x28 0x2B ~ "asdfghjklæø'" +map -altgr -shift 0x56 0x2C..0x35 ~ "ZXCVBNM;:_" + +# +# Keys requiring AltGr +# + +map +altgr -shift 0x03 ~ "@" +map +altgr -shift 0x04 ~ "£" +map +altgr -shift 0x05 ~ "$" +map +altgr -shift 0x08 ~ "{" +map +altgr -shift 0x09 ~ "[" +map +altgr -shift 0x0A ~ "]" +map +altgr -shift 0x0B ~ "}" +map +altgr -shift 0x56 ~ "\" + +map +altgr -shift 0x12 ~ "€" + +map +altgr -shift 0x0D ~ "|" +map +altgr -shift 0x32 ~ "µ" + +# +# Dead keys +# + +map -altgr -shift 0x0D ~ 0xFE51 # Dead acute +map -altgr +shift 0x0D ~ 0xFE50 # Dead grave +map -altgr -shift 0x1B ~ 0xFE57 # Dead umlaut +map -altgr +shift 0x1B ~ 0xFE52 # Dead circumflex +map +altgr -shift 0x1B ~ 0xFE53 # Dead tilde diff --git a/src/protocols/rdp/keymaps/de_ch_qwertz.keymap b/src/protocols/rdp/keymaps/de_ch_qwertz.keymap new file mode 100644 index 00000000..59b9eba2 --- /dev/null +++ b/src/protocols/rdp/keymaps/de_ch_qwertz.keymap @@ -0,0 +1,58 @@ +# +# 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. +# + +parent "base" +name "de-ch-qwertz" +freerdp "KBD_SWISS_GERMAN" + +# +# Basic keys +# + +map -altgr -shift 0x29 0x02..0x0C ~ "§1234567890'" +map -altgr -shift 0x10..0x1A ~ "qwertzuiopü" +map -altgr -shift 0x1E..0x28 0x2B ~ "asdfghjklöä$" +map -altgr -shift 0x56 0x2C..0x35 ~ "YXCVBNM;:_" + +# +# Keys requiring AltGr +# + +map +altgr -shift 0x02..0x04 ~ "¦@#" +map +altgr -shift 0x07..0x09 ~ "¬|¢" +map +altgr -shift 0x1A..0x1B ~ "[]" +map +altgr -shift 0x28 ~ "{" +map +altgr -shift 0x2B ~ "}" +map +altgr -shift 0x56 ~ "\" +map +altgr -shift 0x12 ~ "€" + +# +# Dead keys +# + +map +altgr -shift 0x0C ~ 0xFE51 # Dead acute +map -altgr -shift 0x0D ~ 0xFE52 # Dead circumflex +map -altgr +shift 0x0D ~ 0xFE50 # Dead grave +map +altgr -shift 0x0D ~ 0xFE53 # Dead tilde +map -altgr -shift 0x1B ~ 0xFE57 # Dead umlaut \ No newline at end of file diff --git a/src/protocols/rdp/keymaps/generate.pl b/src/protocols/rdp/keymaps/generate.pl index d3764d6e..263b616a 100755 --- a/src/protocols/rdp/keymaps/generate.pl +++ b/src/protocols/rdp/keymaps/generate.pl @@ -1,4 +1,4 @@ -#!/usr/bin/perl +#!/usr/bin/env perl # # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file diff --git a/src/protocols/rdp/keymaps/hu_hu_qwertz.keymap b/src/protocols/rdp/keymaps/hu_hu_qwertz.keymap new file mode 100644 index 00000000..bcb6a67c --- /dev/null +++ b/src/protocols/rdp/keymaps/hu_hu_qwertz.keymap @@ -0,0 +1,100 @@ +# +# 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. +# + +parent "base" +name "hu-hu-qwertz" +freerdp "KBD_HUNGARIAN" + +# +# Basic keys +# + +map -altgr -shift 0x29 0x02..0x0D ~ "0123456789öüó" +map -altgr -shift 0x10..0x1B ~ "qwertzuıopőú" +map -altgr -shift 0x1E..0x28 0x2B ~ "asdfghjkléáű" +map -altgr -shift 0x56 0x2C..0x35 ~ "íyxcvbnm,.-" + +map -altgr +shift 0x29 0x02..0x0D ~ "§'"+!%/=()ÖÜÓ" +map -altgr +shift 0x10..0x1B ~ "QWERTZUIOPŐÚ" +map -altgr +shift 0x1E..0x28 0x2B ~ "ASDFGHJKLÉÁŰ" +map -altgr +shift 0x56 0x2C..0x35 ~ "ÍYXCVBNM?:_" + + +# +# Keys requiring AltGr +# + +map +altgr -shift 0x02 ~ "~" +map +altgr -shift 0x08 ~ "`" + +map +altgr -shift 0x10 ~ "\" +map +altgr -shift 0x11 ~ "|" +map +altgr -shift 0x12 ~ "Ä" +map +altgr -shift 0x16 ~ "€" +map +altgr -shift 0x17 ~ "Í" +map +altgr -shift 0x1A ~ "÷" +map +altgr -shift 0x1B ~ "×" + +map +altgr -shift 0x1E ~ "ä" +map +altgr -shift 0x1F ~ "đ" +map +altgr -shift 0x20 ~ "Đ" +map +altgr -shift 0x21 ~ "[" +map +altgr -shift 0x22 ~ "]" +map +altgr -shift 0x24 ~ "í" +map +altgr -shift 0x25 ~ "ł" +map +altgr -shift 0x26 ~ "Ł" +map +altgr -shift 0x27 ~ "$" +map +altgr -shift 0x28 ~ "ß" +map +altgr -shift 0x2B ~ "¤" + +map +altgr -shift 0x56 ~ "<" +map +altgr -shift 0x2C ~ ">" +map +altgr -shift 0x2D ~ "#" +map +altgr -shift 0x2E ~ "&" +map +altgr -shift 0x2F ~ "@" +map +altgr -shift 0x30 ~ "{" +map +altgr -shift 0x31 ~ "}" +map +altgr -shift 0x32 ~ "<" +map +altgr -shift 0x33 ~ ";" +map +altgr -shift 0x34 ~ ">" +map +altgr -shift 0x35 ~ "*" + + +# +# Keys requiring AltGr & Shift +# + + +# +# Dead keys +# + +map +altgr -shift 0x03 ~ 0xFE5A # Dead caron +map +altgr -shift 0x04 ~ 0xFE52 # Dead circumflex +map +altgr -shift 0x05 ~ 0xFE55 # Dead breve +map +altgr -shift 0x06 ~ 0xFE58 # Dead abovering +map +altgr -shift 0x07 ~ 0xFE5C # Dead ogonek +map +altgr -shift 0x09 ~ 0xFE56 # Dead abovedot +map +altgr -shift 0x0A ~ 0xFE51 # Dead acute +map +altgr -shift 0x0B ~ 0xFE59 # Dead doubleacute +map +altgr -shift 0x0C ~ 0xFE57 # Dead diaeresis +map +altgr -shift 0x0D ~ 0xFE5B # Dead cedilla + + +# END diff --git a/src/protocols/rdp/rdp.c b/src/protocols/rdp/rdp.c index 4d484320..d0df0531 100644 --- a/src/protocols/rdp/rdp.c +++ b/src/protocols/rdp/rdp.c @@ -240,11 +240,13 @@ BOOL rdp_freerdp_pre_connect(freerdp* instance) { guac_rdp_audio_load_plugin(instance->context, dvc_list); } - /* Load clipboard plugin */ - if (freerdp_channels_load_plugin(channels, instance->settings, - "cliprdr", NULL)) + /* Load clipboard plugin if not disabled */ + if (!(settings->disable_copy && settings->disable_paste) + && freerdp_channels_load_plugin(channels, instance->settings, + "cliprdr", NULL)) { guac_client_log(client, GUAC_LOG_WARNING, "Failed to load cliprdr plugin. Clipboard will not work."); + } /* If RDPSND/RDPDR required, load them */ if (settings->printing_enabled @@ -719,7 +721,7 @@ static int guac_rdp_handle_connection(guac_client* client) { guac_common_cursor_set_pointer(rdp_client->display->cursor); /* Push desired settings to FreeRDP */ - guac_rdp_push_settings(settings, rdp_inst); + guac_rdp_push_settings(client, settings, rdp_inst); /* Connect to RDP server */ if (!freerdp_connect(rdp_inst)) { @@ -975,7 +977,7 @@ void* guac_rdp_client_thread(void* data) { rdp_client->sftp_session = guac_common_ssh_create_session(client, settings->sftp_hostname, settings->sftp_port, rdp_client->sftp_user, settings->sftp_server_alive_interval, - settings->sftp_host_key); + settings->sftp_host_key, NULL); /* Fail if SSH connection does not succeed */ if (rdp_client->sftp_session == NULL) { diff --git a/src/protocols/rdp/rdp_cliprdr.c b/src/protocols/rdp/rdp_cliprdr.c index 53019115..903752c8 100644 --- a/src/protocols/rdp/rdp_cliprdr.c +++ b/src/protocols/rdp/rdp_cliprdr.c @@ -230,6 +230,10 @@ void guac_rdp_process_cb_data_response(guac_client* client, guac_rdp_client* rdp_client = (guac_rdp_client*) client->data; char received_data[GUAC_RDP_CLIPBOARD_MAX_LENGTH]; + /* Ignore received text if outbound clipboard transfer is disabled */ + if (rdp_client->settings->disable_copy) + return; + guac_iconv_read* reader; const char* input = (char*) event->data; char* output = received_data; diff --git a/src/protocols/rdp/rdp_fs.c b/src/protocols/rdp/rdp_fs.c index ab48cc2f..24d7c3ef 100644 --- a/src/protocols/rdp/rdp_fs.c +++ b/src/protocols/rdp/rdp_fs.c @@ -39,6 +39,7 @@ #include #include #include +#include #include guac_rdp_fs* guac_rdp_fs_alloc(guac_client* client, const char* drive_path, @@ -402,7 +403,7 @@ int guac_rdp_fs_open(guac_rdp_fs* fs, const char* path, } -int guac_rdp_fs_read(guac_rdp_fs* fs, int file_id, int offset, +int guac_rdp_fs_read(guac_rdp_fs* fs, int file_id, uint64_t offset, void* buffer, int length) { int bytes_read; @@ -426,7 +427,7 @@ int guac_rdp_fs_read(guac_rdp_fs* fs, int file_id, int offset, } -int guac_rdp_fs_write(guac_rdp_fs* fs, int file_id, int offset, +int guac_rdp_fs_write(guac_rdp_fs* fs, int file_id, uint64_t offset, void* buffer, int length) { int bytes_written; @@ -606,51 +607,55 @@ const char* guac_rdp_fs_read_dir(guac_rdp_fs* fs, int file_id) { int guac_rdp_fs_normalize_path(const char* path, char* abs_path) { - int i; int path_depth = 0; - char path_component_data[GUAC_RDP_FS_MAX_PATH]; - const char* path_components[64]; - - const char** current_path_component = &(path_components[0]); - const char* current_path_component_data = &(path_component_data[0]); + const char* path_components[GUAC_RDP_MAX_PATH_DEPTH]; /* If original path is not absolute, normalization fails */ if (path[0] != '\\' && path[0] != '/') return 1; - /* Skip past leading slash */ - path++; + /* 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_RDP_FS_MAX_PATH - 1]; + int length = guac_strlcpy(path_scratch, path + 1, + sizeof(path_scratch)); - /* Copy path into component data for parsing */ - strncpy(path_component_data, path, sizeof(path_component_data) - 1); + /* Fail if provided path is too long */ + if (length >= sizeof(path_scratch)) + return 1; - /* Find path components within path */ - for (i = 0; i < sizeof(path_component_data) - 1; i++) { + /* 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_component_data[i]; - if (c == '/' || c == '\\' || c == 0) { + char c = path_scratch[i]; + if (c == '/' || c == '\\' || c == '\0') { /* Terminate current component */ - path_component_data[i] = 0; + path_scratch[i] = '\0'; /* If component refers to parent, just move up in depth */ - if (strcmp(current_path_component_data, "..") == 0) { + 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_data, ".") != 0 - && strcmp(current_path_component_data, "") != 0) - path_components[path_depth++] = current_path_component_data; + else if (strcmp(current_path_component, ".") != 0 + && strcmp(current_path_component, "") != 0) { - /* If end of string, stop */ - if (c == 0) - break; + /* Fail normalization if path is too deep */ + if (path_depth >= GUAC_RDP_MAX_PATH_DEPTH) + return 1; + + path_components[path_depth++] = current_path_component; + + } /* Update start of next component */ - current_path_component_data = &(path_component_data[i+1]); + current_path_component = &(path_scratch[i+1]); } /* end if separator */ @@ -660,57 +665,32 @@ int guac_rdp_fs_normalize_path(const char* path, char* abs_path) { } /* end for each character */ - /* If no components, the path is simply root */ - if (path_depth == 0) { - strcpy(abs_path, "\\"); - return 0; - } + /* Add leading slash for resulting absolute path */ + abs_path[0] = '\\'; - /* Ensure last component is null-terminated */ - path_component_data[i] = 0; + /* Append normalized components to path, separated by slashes */ + guac_strljoin(abs_path + 1, path_components, path_depth, + "\\", GUAC_RDP_FS_MAX_PATH - 1); - /* Convert components back into path */ - for (; path_depth > 0; path_depth--) { - - const char* filename = *(current_path_component++); - - /* Add separator */ - *(abs_path++) = '\\'; - - /* Copy string */ - while (*filename != 0) - *(abs_path++) = *(filename++); - - } - - /* Terminate absolute path */ - *(abs_path++) = 0; return 0; } int guac_rdp_fs_convert_path(const char* parent, const char* rel_path, char* abs_path) { - int i; + int length; char combined_path[GUAC_RDP_FS_MAX_PATH]; - char* current = combined_path; /* Copy parent path */ - for (i=0; i #include +#include #include #ifdef ENABLE_WINPR @@ -35,6 +36,7 @@ #include "compat/winpr-wtypes.h" #endif +#include #include #include @@ -79,6 +81,7 @@ const char* GUAC_RDP_CLIENT_ARGS[] = { "disable-glyph-caching", "preconnection-id", "preconnection-blob", + "timezone", #ifdef ENABLE_COMMON_SSH "enable-sftp", @@ -116,11 +119,13 @@ const char* GUAC_RDP_CLIENT_ARGS[] = { "load-balance-info", #endif + "disable-copy", + "disable-paste", NULL }; enum RDP_ARGS_IDX { - + /** * The hostname to connect to. */ @@ -356,6 +361,14 @@ enum RDP_ARGS_IDX { */ IDX_PRECONNECTION_BLOB, + /** + * The timezone to pass through to the RDP connection, in IANA format, which + * will be translated into Windows formats. See the following page for + * information and list of valid values: + * https://en.wikipedia.org/wiki/List_of_tz_database_time_zones + */ + IDX_TIMEZONE, + #ifdef ENABLE_COMMON_SSH /** * "true" if SFTP should be enabled for the RDP connection, "false" or @@ -535,6 +548,20 @@ enum RDP_ARGS_IDX { IDX_LOAD_BALANCE_INFO, #endif + /** + * Whether outbound clipboard access should be blocked. If set to "true", + * it will not be possible to copy data from the remote desktop to the + * client using the clipboard. By default, clipboard access is not blocked. + */ + IDX_DISABLE_COPY, + + /** + * Whether inbound clipboard access should be blocked. If set to "true", it + * will not be possible to paste data from the client to the remote desktop + * using the clipboard. By default, clipboard access is not blocked. + */ + IDX_DISABLE_PASTE, + RDP_ARGS_COUNT }; @@ -840,6 +867,11 @@ guac_rdp_settings* guac_rdp_parse_args(guac_user* user, if (settings->server_layout == NULL) settings->server_layout = guac_rdp_keymap_find(GUAC_DEFAULT_KEYMAP); + /* Timezone if provided by client, or use handshake version */ + settings->timezone = + guac_user_parse_args_string(user, GUAC_RDP_CLIENT_ARGS, argv, + IDX_TIMEZONE, user->info.timezone); + #ifdef ENABLE_COMMON_SSH /* SFTP enable/disable */ settings->enable_sftp = @@ -992,6 +1024,16 @@ guac_rdp_settings* guac_rdp_parse_args(guac_user* user, IDX_LOAD_BALANCE_INFO, NULL); #endif + /* Parse clipboard copy disable flag */ + settings->disable_copy = + guac_user_parse_args_boolean(user, GUAC_RDP_CLIENT_ARGS, argv, + IDX_DISABLE_COPY, 0); + + /* Parse clipboard paste disable flag */ + settings->disable_paste = + guac_user_parse_args_boolean(user, GUAC_RDP_CLIENT_ARGS, argv, + IDX_DISABLE_PASTE, 0); + /* Success */ return settings; @@ -1013,6 +1055,7 @@ void guac_rdp_settings_free(guac_rdp_settings* settings) { free(settings->remote_app); free(settings->remote_app_args); free(settings->remote_app_dir); + free(settings->timezone); free(settings->username); free(settings->printer_name); @@ -1153,7 +1196,8 @@ static char* guac_rdp_strdup(const char* str) { } -void guac_rdp_push_settings(guac_rdp_settings* guac_settings, freerdp* rdp) { +void guac_rdp_push_settings(guac_client* client, + guac_rdp_settings* guac_settings, freerdp* rdp) { BOOL bitmap_cache = !guac_settings->disable_bitmap_caching; rdpSettings* rdp_settings = rdp->settings; @@ -1225,11 +1269,11 @@ void guac_rdp_push_settings(guac_rdp_settings* guac_settings, freerdp* rdp) { /* Client name */ if (guac_settings->client_name != NULL) { #ifdef LEGACY_RDPSETTINGS - strncpy(rdp_settings->client_hostname, guac_settings->client_name, - RDP_CLIENT_HOSTNAME_SIZE - 1); + guac_strlcpy(rdp_settings->client_hostname, guac_settings->client_name, + RDP_CLIENT_HOSTNAME_SIZE); #else - strncpy(rdp_settings->ClientHostname, guac_settings->client_name, - RDP_CLIENT_HOSTNAME_SIZE - 1); + guac_strlcpy(rdp_settings->ClientHostname, guac_settings->client_name, + RDP_CLIENT_HOSTNAME_SIZE); #endif } @@ -1264,6 +1308,15 @@ void guac_rdp_push_settings(guac_rdp_settings* guac_settings, freerdp* rdp) { #endif #endif + /* Timezone redirection */ + if (guac_settings->timezone) { + if (setenv("TZ", guac_settings->timezone, 1)) { + guac_client_log(client, GUAC_LOG_WARNING, + "Unable to forward timezone: TZ environment variable " + "could not be set: %s", strerror(errno)); + } + } + /* Device redirection */ #ifdef LEGACY_RDPSETTINGS #ifdef HAVE_RDPSETTINGS_DEVICEREDIRECTION diff --git a/src/protocols/rdp/rdp_settings.h b/src/protocols/rdp/rdp_settings.h index d521a87c..9edbedeb 100644 --- a/src/protocols/rdp/rdp_settings.h +++ b/src/protocols/rdp/rdp_settings.h @@ -268,6 +268,20 @@ typedef struct guac_rdp_settings { */ char** svc_names; + /** + * Whether outbound clipboard access should be blocked. If set, it will not + * be possible to copy data from the remote desktop to the client using the + * clipboard. + */ + int disable_copy; + + /** + * Whether inbound clipboard access should be blocked. If set, it will not + * be possible to paste data from the client to the remote desktop using + * the clipboard. + */ + int disable_paste; + /** * Whether the desktop wallpaper should be visible. If unset, the desktop * wallpaper will be hidden, reducing the amount of bandwidth required. @@ -341,6 +355,11 @@ typedef struct guac_rdp_settings { */ char* preconnection_blob; + /** + * The timezone to pass through to the RDP connection. + */ + char* timezone; + #ifdef ENABLE_COMMON_SSH /** * Whether SFTP should be enabled for the VNC connection. @@ -547,13 +566,17 @@ extern const char* GUAC_RDP_CLIENT_ARGS[]; /** * Save all given settings to the given freerdp instance. * + * @param client + * The guac_client object providing the settings. + * * @param guac_settings * The guac_rdp_settings object to save. * * @param rdp * The RDP instance to save settings to. */ -void guac_rdp_push_settings(guac_rdp_settings* guac_settings, freerdp* rdp); +void guac_rdp_push_settings(guac_client* client, + guac_rdp_settings* guac_settings, freerdp* rdp); /** * Returns the width of the RDP session display. diff --git a/src/protocols/rdp/rdp_stream.c b/src/protocols/rdp/rdp_stream.c index 5dae3667..533ff07e 100644 --- a/src/protocols/rdp/rdp_stream.c +++ b/src/protocols/rdp/rdp_stream.c @@ -32,6 +32,7 @@ #include #include #include +#include #ifdef HAVE_FREERDP_CLIENT_CLIPRDR_H #include @@ -504,8 +505,8 @@ int guac_rdp_download_get_handler(guac_user* user, guac_object* object, rdp_stream->type = GUAC_RDP_LS_STREAM; rdp_stream->ls_status.fs = fs; rdp_stream->ls_status.file_id = file_id; - strncpy(rdp_stream->ls_status.directory_name, name, - sizeof(rdp_stream->ls_status.directory_name) - 1); + guac_strlcpy(rdp_stream->ls_status.directory_name, name, + sizeof(rdp_stream->ls_status.directory_name)); /* Allocate stream for body */ guac_stream* stream = guac_user_alloc_stream(user); diff --git a/src/protocols/rdp/rdp_stream.h b/src/protocols/rdp/rdp_stream.h index c9c3838e..deec4f9a 100644 --- a/src/protocols/rdp/rdp_stream.h +++ b/src/protocols/rdp/rdp_stream.h @@ -57,7 +57,7 @@ typedef struct guac_rdp_upload_status { * The overall offset within the file that the next write should * occur at. */ - int offset; + uint64_t offset; /** * The ID of the file being written to. diff --git a/src/protocols/rdp/rdp_svc.c b/src/protocols/rdp/rdp_svc.c index 83537a86..0a6eb246 100644 --- a/src/protocols/rdp/rdp_svc.c +++ b/src/protocols/rdp/rdp_svc.c @@ -25,6 +25,7 @@ #include #include +#include #ifdef ENABLE_WINPR #include @@ -33,7 +34,6 @@ #endif #include -#include guac_rdp_svc* guac_rdp_alloc_svc(guac_client* client, char* name) { @@ -44,16 +44,14 @@ guac_rdp_svc* guac_rdp_alloc_svc(guac_client* client, char* name) { svc->plugin = NULL; svc->output_pipe = NULL; + /* Init name */ + int name_length = guac_strlcpy(svc->name, name, GUAC_RDP_SVC_MAX_LENGTH); + /* Warn about name length */ - if (strnlen(name, GUAC_RDP_SVC_MAX_LENGTH+1) > GUAC_RDP_SVC_MAX_LENGTH) + if (name_length >= GUAC_RDP_SVC_MAX_LENGTH) guac_client_log(client, GUAC_LOG_INFO, "Static channel name \"%s\" exceeds maximum of %i characters " - "and will be truncated", - name, GUAC_RDP_SVC_MAX_LENGTH); - - /* Init name */ - strncpy(svc->name, name, GUAC_RDP_SVC_MAX_LENGTH); - svc->name[GUAC_RDP_SVC_MAX_LENGTH] = '\0'; + "and will be truncated", name, GUAC_RDP_SVC_MAX_LENGTH - 1); return svc; } diff --git a/src/protocols/rdp/rdp_svc.h b/src/protocols/rdp/rdp_svc.h index 322c4d9a..ebb3a13f 100644 --- a/src/protocols/rdp/rdp_svc.h +++ b/src/protocols/rdp/rdp_svc.h @@ -27,9 +27,10 @@ #include /** - * The maximum number of characters to allow for each channel name. + * The maximum number of bytes to allow within each channel name, including + * null terminator. */ -#define GUAC_RDP_SVC_MAX_LENGTH 7 +#define GUAC_RDP_SVC_MAX_LENGTH 8 /** * Structure describing a static virtual channel, and the corresponding @@ -50,7 +51,7 @@ typedef struct guac_rdp_svc { /** * The name of the RDP channel in use, and the name to use for each pipe. */ - char name[GUAC_RDP_SVC_MAX_LENGTH+1]; + char name[GUAC_RDP_SVC_MAX_LENGTH]; /** * The output pipe, opened when the RDP server receives a connection to diff --git a/src/protocols/rdp/tests/Makefile.am b/src/protocols/rdp/tests/Makefile.am new file mode 100644 index 00000000..3f57bbf3 --- /dev/null +++ b/src/protocols/rdp/tests/Makefile.am @@ -0,0 +1,65 @@ +# +# 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. +# +# NOTE: Parts of this file (Makefile.am) are automatically transcluded verbatim +# into Makefile.in. Though the build system (GNU Autotools) automatically adds +# its own license boilerplate to the generated Makefile.in, that boilerplate +# does not apply to the transcluded portions of Makefile.am which are licensed +# to you by the ASF under the Apache License, Version 2.0, as described above. +# + +AUTOMAKE_OPTIONS = foreign +ACLOCAL_AMFLAGS = -I m4 + +# +# Unit tests for RDP support +# + +check_PROGRAMS = test_rdp +TESTS = $(check_PROGRAMS) + +test_rdp_SOURCES = \ + fs/normalize_path.c + +test_rdp_CFLAGS = \ + -Werror -Wall -pedantic \ + @LIBGUAC_CLIENT_RDP_INCLUDE@ \ + @LIBGUAC_INCLUDE@ + +test_rdp_LDADD = \ + @CUNIT_LIBS@ \ + @LIBGUAC_CLIENT_RDP_LTLIB@ + +# +# Autogenerate test runner +# + +GEN_RUNNER = $(top_srcdir)/util/generate-test-runner.pl +CLEANFILES = _generated_runner.c + +_generated_runner.c: $(test_rdp_SOURCES) + $(AM_V_GEN) $(GEN_RUNNER) $(test_rdp_SOURCES) > $@ + +nodist_test_rdp_SOURCES = \ + _generated_runner.c + +# Use automake's TAP test driver for running any tests +LOG_DRIVER = \ + env AM_TAP_AWK='$(AWK)' \ + $(SHELL) $(top_srcdir)/build-aux/tap-driver.sh + diff --git a/src/protocols/rdp/tests/fs/normalize_path.c b/src/protocols/rdp/tests/fs/normalize_path.c new file mode 100644 index 00000000..ccf23e01 --- /dev/null +++ b/src/protocols/rdp/tests/fs/normalize_path.c @@ -0,0 +1,256 @@ +/* + * 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 "rdp_fs.h" + +#include +#include + +/** + * Test which verifies absolute Windows-style paths are correctly normalized to + * absolute paths with Windows separators and no relative components. + */ +void test_fs__normalize_absolute_windows() { + + char normalized[GUAC_RDP_FS_MAX_PATH]; + + CU_ASSERT_EQUAL(guac_rdp_fs_normalize_path("\\", normalized), 0) + CU_ASSERT_NSTRING_EQUAL(normalized, "\\", sizeof(normalized)); + + CU_ASSERT_EQUAL(guac_rdp_fs_normalize_path("\\foo\\bar\\baz", normalized), 0) + CU_ASSERT_NSTRING_EQUAL(normalized, "\\foo\\bar\\baz", sizeof(normalized)); + + CU_ASSERT_EQUAL(guac_rdp_fs_normalize_path("\\foo\\bar\\..\\baz\\", normalized), 0) + CU_ASSERT_NSTRING_EQUAL(normalized, "\\foo\\baz", sizeof(normalized)); + + CU_ASSERT_EQUAL(guac_rdp_fs_normalize_path("\\foo\\bar\\..\\..\\baz\\a\\..\\b", normalized), 0) + CU_ASSERT_NSTRING_EQUAL(normalized, "\\baz\\b", sizeof(normalized)); + + CU_ASSERT_EQUAL(guac_rdp_fs_normalize_path("\\foo\\.\\bar\\baz", normalized), 0) + CU_ASSERT_NSTRING_EQUAL(normalized, "\\foo\\bar\\baz", sizeof(normalized)); + + CU_ASSERT_EQUAL(guac_rdp_fs_normalize_path("\\foo\\bar\\..\\..\\..\\..\\..\\..\\baz", normalized), 0) + CU_ASSERT_NSTRING_EQUAL(normalized, "\\baz", sizeof(normalized)); + +} + +/** + * Test which verifies absolute UNIX-style paths are correctly normalized to + * absolute paths with Windows separators and no relative components. + */ +void test_fs__normalize_absolute_unix() { + + char normalized[GUAC_RDP_FS_MAX_PATH]; + + CU_ASSERT_EQUAL(guac_rdp_fs_normalize_path("/", normalized), 0) + CU_ASSERT_NSTRING_EQUAL(normalized, "\\", sizeof(normalized)); + + CU_ASSERT_EQUAL(guac_rdp_fs_normalize_path("/foo/bar/baz", normalized), 0) + CU_ASSERT_NSTRING_EQUAL(normalized, "\\foo\\bar\\baz", sizeof(normalized)); + + CU_ASSERT_EQUAL(guac_rdp_fs_normalize_path("/foo/bar/../baz/", normalized), 0) + CU_ASSERT_NSTRING_EQUAL(normalized, "\\foo\\baz", sizeof(normalized)); + + CU_ASSERT_EQUAL(guac_rdp_fs_normalize_path("/foo/bar/../../baz/a/../b", normalized), 0) + CU_ASSERT_NSTRING_EQUAL(normalized, "\\baz\\b", sizeof(normalized)); + + CU_ASSERT_EQUAL(guac_rdp_fs_normalize_path("/foo/./bar/baz", normalized), 0) + CU_ASSERT_NSTRING_EQUAL(normalized, "\\foo\\bar\\baz", sizeof(normalized)); + + CU_ASSERT_EQUAL(guac_rdp_fs_normalize_path("/foo/bar/../../../../../../baz", normalized), 0) + CU_ASSERT_NSTRING_EQUAL(normalized, "\\baz", sizeof(normalized)); + +} + +/** + * Test which verifies absolute paths consisting of mixed Windows and UNIX path + * separators are correctly normalized to absolute paths with Windows + * separators and no relative components. + */ +void test_fs__normalize_absolute_mixed() { + + char normalized[GUAC_RDP_FS_MAX_PATH]; + + CU_ASSERT_EQUAL(guac_rdp_fs_normalize_path("\\foo/bar\\baz", normalized), 0) + CU_ASSERT_NSTRING_EQUAL(normalized, "\\foo\\bar\\baz", sizeof(normalized)); + + CU_ASSERT_EQUAL(guac_rdp_fs_normalize_path("/foo\\bar/..\\baz/", normalized), 0) + CU_ASSERT_NSTRING_EQUAL(normalized, "\\foo\\baz", sizeof(normalized)); + + CU_ASSERT_EQUAL(guac_rdp_fs_normalize_path("\\foo/bar\\../../baz\\a\\..\\b", normalized), 0) + CU_ASSERT_NSTRING_EQUAL(normalized, "\\baz\\b", sizeof(normalized)); + + CU_ASSERT_EQUAL(guac_rdp_fs_normalize_path("\\foo\\.\\bar/baz", normalized), 0) + CU_ASSERT_NSTRING_EQUAL(normalized, "\\foo\\bar\\baz", sizeof(normalized)); + + CU_ASSERT_EQUAL(guac_rdp_fs_normalize_path("\\foo/bar\\../..\\..\\..\\../..\\baz", normalized), 0) + CU_ASSERT_NSTRING_EQUAL(normalized, "\\baz", sizeof(normalized)); + +} + +/** + * Test which verifies relative Windows-style paths are always rejected. + */ +void test_fs__normalize_relative_windows() { + + char normalized[GUAC_RDP_FS_MAX_PATH]; + + CU_ASSERT_NOT_EQUAL(guac_rdp_fs_normalize_path("", normalized), 0) + CU_ASSERT_NOT_EQUAL(guac_rdp_fs_normalize_path(".", normalized), 0) + CU_ASSERT_NOT_EQUAL(guac_rdp_fs_normalize_path("..", normalized), 0) + CU_ASSERT_NOT_EQUAL(guac_rdp_fs_normalize_path("foo", normalized), 0) + CU_ASSERT_NOT_EQUAL(guac_rdp_fs_normalize_path(".\\foo", normalized), 0) + CU_ASSERT_NOT_EQUAL(guac_rdp_fs_normalize_path("..\\foo", normalized), 0) + CU_ASSERT_NOT_EQUAL(guac_rdp_fs_normalize_path("foo\\bar\\baz", normalized), 0) + CU_ASSERT_NOT_EQUAL(guac_rdp_fs_normalize_path(".\\foo\\bar\\baz", normalized), 0) + CU_ASSERT_NOT_EQUAL(guac_rdp_fs_normalize_path("..\\foo\\bar\\baz", normalized), 0) + +} + +/** + * Test which verifies relative UNIX-style paths are always rejected. + */ +void test_fs__normalize_relative_unix() { + + char normalized[GUAC_RDP_FS_MAX_PATH]; + + CU_ASSERT_NOT_EQUAL(guac_rdp_fs_normalize_path("", normalized), 0) + CU_ASSERT_NOT_EQUAL(guac_rdp_fs_normalize_path(".", normalized), 0) + CU_ASSERT_NOT_EQUAL(guac_rdp_fs_normalize_path("..", normalized), 0) + CU_ASSERT_NOT_EQUAL(guac_rdp_fs_normalize_path("foo", normalized), 0) + CU_ASSERT_NOT_EQUAL(guac_rdp_fs_normalize_path("./foo", normalized), 0) + CU_ASSERT_NOT_EQUAL(guac_rdp_fs_normalize_path("../foo", normalized), 0) + CU_ASSERT_NOT_EQUAL(guac_rdp_fs_normalize_path("foo/bar/baz", normalized), 0) + CU_ASSERT_NOT_EQUAL(guac_rdp_fs_normalize_path("./foo/bar/baz", normalized), 0) + CU_ASSERT_NOT_EQUAL(guac_rdp_fs_normalize_path("../foo/bar/baz", normalized), 0) + +} + +/** + * Test which verifies relative paths consisting of mixed Windows and UNIX path + * separators are always rejected. + */ +void test_fs__normalize_relative_mixed() { + + char normalized[GUAC_RDP_FS_MAX_PATH]; + + CU_ASSERT_NOT_EQUAL(guac_rdp_fs_normalize_path("foo\\bar/baz", normalized), 0) + CU_ASSERT_NOT_EQUAL(guac_rdp_fs_normalize_path(".\\foo/bar/baz", normalized), 0) + CU_ASSERT_NOT_EQUAL(guac_rdp_fs_normalize_path("../foo\\bar\\baz", normalized), 0) + +} + +/** + * Generates a dynamically-allocated path having the given number of bytes, not + * counting the null-terminator. The path will contain only Windows-style path + * separators. The returned path must eventually be freed with a call to + * free(). + * + * @param length + * The number of bytes to include in the generated path, not counting the + * null-terminator. If -1, the length of the path will be automatically + * determined from the provided max_depth. + * + * @param max_depth + * The maximum number of path components to include within the generated + * path. + * + * @return + * A dynamically-allocated path containing the given number of bytes, not + * counting the null-terminator. This path must eventually be freed with a + * call to free(). + */ +static char* generate_path(int length, int max_depth) { + + /* If no length given, calculate space required from max_depth */ + if (length == -1) + length = max_depth * 2; + + int i; + char* input = malloc(length + 1); + + /* Fill path with \x\x\x\x\x\x\x\x\x\x\...\xxxxxxxxx... */ + for (i = 0; i < length; i++) { + if (max_depth > 0 && i % 2 == 0) { + input[i] = '\\'; + max_depth--; + } + else + input[i] = 'x'; + } + + /* Add null terminator */ + input[length] = '\0'; + + return input; + +} + +/** + * Test which verifies that paths exceeding the maximum path length are + * rejected. + */ +void test_fs__normalize_long() { + + char* input; + char normalized[GUAC_RDP_FS_MAX_PATH]; + + /* Exceeds maximum length by a factor of 2 */ + input = generate_path(GUAC_RDP_FS_MAX_PATH * 2, GUAC_RDP_MAX_PATH_DEPTH); + CU_ASSERT_NOT_EQUAL(guac_rdp_fs_normalize_path(input, normalized), 0); + free(input); + + /* Exceeds maximum length by one byte */ + input = generate_path(GUAC_RDP_FS_MAX_PATH, GUAC_RDP_MAX_PATH_DEPTH); + CU_ASSERT_NOT_EQUAL(guac_rdp_fs_normalize_path(input, normalized), 0); + free(input); + + /* Exactly maximum length */ + input = generate_path(GUAC_RDP_FS_MAX_PATH - 1, GUAC_RDP_MAX_PATH_DEPTH); + CU_ASSERT_EQUAL(guac_rdp_fs_normalize_path(input, normalized), 0); + free(input); + +} + +/** + * Test which verifies that paths exceeding the maximum path depth are + * rejected. + */ +void test_fs__normalize_deep() { + + char* input; + char normalized[GUAC_RDP_FS_MAX_PATH]; + + /* Exceeds maximum depth by a factor of 2 */ + input = generate_path(-1, GUAC_RDP_MAX_PATH_DEPTH * 2); + CU_ASSERT_NOT_EQUAL(guac_rdp_fs_normalize_path(input, normalized), 0); + free(input); + + /* Exceeds maximum depth by one component */ + input = generate_path(-1, GUAC_RDP_MAX_PATH_DEPTH + 1); + CU_ASSERT_NOT_EQUAL(guac_rdp_fs_normalize_path(input, normalized), 0); + free(input); + + /* Exactly maximum depth */ + input = generate_path(-1, GUAC_RDP_MAX_PATH_DEPTH); + CU_ASSERT_EQUAL(guac_rdp_fs_normalize_path(input, normalized), 0); + free(input); + +} + diff --git a/src/protocols/rdp/user.c b/src/protocols/rdp/user.c index 6aa71ae9..025848aa 100644 --- a/src/protocols/rdp/user.c +++ b/src/protocols/rdp/user.c @@ -97,10 +97,13 @@ int guac_rdp_user_join_handler(guac_user* user, int argc, char** argv) { /* Only handle events if not read-only */ if (!settings->read_only) { - /* General mouse/keyboard/clipboard events */ - user->mouse_handler = guac_rdp_user_mouse_handler; - user->key_handler = guac_rdp_user_key_handler; - user->clipboard_handler = guac_rdp_clipboard_handler; + /* General mouse/keyboard events */ + user->mouse_handler = guac_rdp_user_mouse_handler; + user->key_handler = guac_rdp_user_key_handler; + + /* Inbound (client to server) clipboard transfer */ + if (!settings->disable_paste) + user->clipboard_handler = guac_rdp_clipboard_handler; /* Display size change events */ user->size_handler = guac_rdp_user_size_handler; diff --git a/src/protocols/ssh/.gitignore b/src/protocols/ssh/.gitignore deleted file mode 100644 index 3f725332..00000000 --- a/src/protocols/ssh/.gitignore +++ /dev/null @@ -1,36 +0,0 @@ - -# Object code -*.o -*.so -*.lo -*.la - -# Backup files -*~ - -# Release files -*.tar.gz - -# Files currently being edited by vim or vi -*.swp - -# automake/autoconf -.deps/ -.libs/ -Makefile -Makefile.in -aclocal.m4 -autom4te.cache/ -m4/* -!README -config.guess -config.log -config.status -config.sub -configure -depcomp -install-sh -libtool -ltmain.sh -missing - diff --git a/src/protocols/ssh/Makefile.am b/src/protocols/ssh/Makefile.am index 87d4a728..7bb1e349 100644 --- a/src/protocols/ssh/Makefile.am +++ b/src/protocols/ssh/Makefile.am @@ -16,6 +16,12 @@ # specific language governing permissions and limitations # under the License. # +# NOTE: Parts of this file (Makefile.am) are automatically transcluded verbatim +# into Makefile.in. Though the build system (GNU Autotools) automatically adds +# its own license boilerplate to the generated Makefile.in, that boilerplate +# does not apply to the transcluded portions of Makefile.am which are licensed +# to you by the ASF under the Apache License, Version 2.0, as described above. +# AUTOMAKE_OPTIONS = foreign ACLOCAL_AMFLAGS = -I m4 @@ -23,6 +29,7 @@ ACLOCAL_AMFLAGS = -I m4 lib_LTLIBRARIES = libguac-client-ssh.la libguac_client_ssh_la_SOURCES = \ + argv.c \ client.c \ clipboard.c \ input.c \ @@ -34,6 +41,7 @@ libguac_client_ssh_la_SOURCES = \ user.c noinst_HEADERS = \ + argv.h \ client.h \ clipboard.h \ input.h \ diff --git a/src/protocols/ssh/argv.c b/src/protocols/ssh/argv.c new file mode 100644 index 00000000..d9ce1e75 --- /dev/null +++ b/src/protocols/ssh/argv.c @@ -0,0 +1,229 @@ +/* + * 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 "config.h" +#include "argv.h" +#include "ssh.h" +#include "terminal/terminal.h" + +#include +#include +#include + +#include +#include +#include + +/** + * All SSH connection settings which may be updated by unprivileged users + * through "argv" streams. + */ +typedef enum guac_ssh_argv_setting { + + /** + * The color scheme of the terminal. + */ + GUAC_SSH_ARGV_SETTING_COLOR_SCHEME, + + /** + * The name of the font family used by the terminal. + */ + GUAC_SSH_ARGV_SETTING_FONT_NAME, + + /** + * The size of the font used by the terminal, in points. + */ + GUAC_SSH_ARGV_SETTING_FONT_SIZE + +} guac_ssh_argv_setting; + +/** + * The value or current status of a connection parameter received over an + * "argv" stream. + */ +typedef struct guac_ssh_argv { + + /** + * The specific setting being updated. + */ + guac_ssh_argv_setting setting; + + /** + * Buffer space for containing the received argument value. + */ + char buffer[GUAC_SSH_ARGV_MAX_LENGTH]; + + /** + * The number of bytes received so far. + */ + int length; + +} guac_ssh_argv; + +/** + * Handler for "blob" instructions which appends the data from received blobs + * to the end of the in-progress argument value buffer. + * + * @see guac_user_blob_handler + */ +static int guac_ssh_argv_blob_handler(guac_user* user, + guac_stream* stream, void* data, int length) { + + guac_ssh_argv* argv = (guac_ssh_argv*) stream->data; + + /* Calculate buffer size remaining, including space for null terminator, + * adjusting received length accordingly */ + int remaining = sizeof(argv->buffer) - argv->length - 1; + if (length > remaining) + length = remaining; + + /* Append received data to end of buffer */ + memcpy(argv->buffer + argv->length, data, length); + argv->length += length; + + return 0; + +} + +/** + * Handler for "end" instructions which applies the changes specified by the + * argument value buffer associated with the stream. + * + * @see guac_user_end_handler + */ +static int guac_ssh_argv_end_handler(guac_user* user, + guac_stream* stream) { + + int size; + + guac_client* client = user->client; + guac_ssh_client* ssh_client = (guac_ssh_client*) client->data; + guac_terminal* terminal = ssh_client->term; + + /* Append null terminator to value */ + guac_ssh_argv* argv = (guac_ssh_argv*) stream->data; + argv->buffer[argv->length] = '\0'; + + /* Apply changes to chosen setting */ + switch (argv->setting) { + + /* Update color scheme */ + case GUAC_SSH_ARGV_SETTING_COLOR_SCHEME: + guac_terminal_apply_color_scheme(terminal, argv->buffer); + guac_client_stream_argv(client, client->socket, "text/plain", + "color-scheme", argv->buffer); + break; + + /* Update font name */ + case GUAC_SSH_ARGV_SETTING_FONT_NAME: + guac_terminal_apply_font(terminal, argv->buffer, -1, 0); + guac_client_stream_argv(client, client->socket, "text/plain", + "font-name", argv->buffer); + break; + + /* Update font size */ + case GUAC_SSH_ARGV_SETTING_FONT_SIZE: + + /* Update only if font size is sane */ + size = atoi(argv->buffer); + if (size > 0) { + guac_terminal_apply_font(terminal, NULL, size, + ssh_client->settings->resolution); + guac_client_stream_argv(client, client->socket, "text/plain", + "font-size", argv->buffer); + } + + break; + + } + + /* Update SSH pty size if connected */ + if (ssh_client->term_channel != NULL) { + pthread_mutex_lock(&(ssh_client->term_channel_lock)); + libssh2_channel_request_pty_size(ssh_client->term_channel, + terminal->term_width, terminal->term_height); + pthread_mutex_unlock(&(ssh_client->term_channel_lock)); + } + + free(argv); + return 0; + +} + +int guac_ssh_argv_handler(guac_user* user, guac_stream* stream, + char* mimetype, char* name) { + + guac_ssh_argv_setting setting; + + /* Allow users to update the color scheme and font details */ + if (strcmp(name, "color-scheme") == 0) + setting = GUAC_SSH_ARGV_SETTING_COLOR_SCHEME; + else if (strcmp(name, "font-name") == 0) + setting = GUAC_SSH_ARGV_SETTING_FONT_NAME; + else if (strcmp(name, "font-size") == 0) + setting = GUAC_SSH_ARGV_SETTING_FONT_SIZE; + + /* No other connection parameters may be updated */ + else { + guac_protocol_send_ack(user->socket, stream, "Not allowed.", + GUAC_PROTOCOL_STATUS_CLIENT_FORBIDDEN); + guac_socket_flush(user->socket); + return 0; + } + + guac_ssh_argv* argv = malloc(sizeof(guac_ssh_argv)); + argv->setting = setting; + argv->length = 0; + + /* Prepare stream to receive argument value */ + stream->blob_handler = guac_ssh_argv_blob_handler; + stream->end_handler = guac_ssh_argv_end_handler; + stream->data = argv; + + /* Signal stream is ready */ + guac_protocol_send_ack(user->socket, stream, "Ready for updated " + "parameter.", GUAC_PROTOCOL_STATUS_SUCCESS); + guac_socket_flush(user->socket); + return 0; + +} + +void* guac_ssh_send_current_argv(guac_user* user, void* data) { + + guac_ssh_client* ssh_client = (guac_ssh_client*) data; + guac_terminal* terminal = ssh_client->term; + + /* Send current color scheme */ + guac_user_stream_argv(user, user->socket, "text/plain", "color-scheme", + terminal->color_scheme); + + /* Send current font name */ + guac_user_stream_argv(user, user->socket, "text/plain", "font-name", + terminal->font_name); + + /* Send current font size */ + char font_size[64]; + sprintf(font_size, "%i", terminal->font_size); + guac_user_stream_argv(user, user->socket, "text/plain", "font-size", + font_size); + + return NULL; + +} + diff --git a/src/protocols/ssh/argv.h b/src/protocols/ssh/argv.h new file mode 100644 index 00000000..186e0b5f --- /dev/null +++ b/src/protocols/ssh/argv.h @@ -0,0 +1,60 @@ +/* + * 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. + */ + + +#ifndef GUAC_SSH_ARGV_H +#define GUAC_SSH_ARGV_H + +#include "config.h" + +#include + +/** + * The maximum number of bytes to allow for any argument value received via an + * argv stream, including null terminator. + */ +#define GUAC_SSH_ARGV_MAX_LENGTH 16384 + +/** + * Handles an incoming stream from a Guacamole "argv" instruction, updating the + * given connection parameter if that parameter is allowed to be updated. + */ +guac_user_argv_handler guac_ssh_argv_handler; + +/** + * Sends the current values of all non-sensitive parameters which may be set + * while the connection is running to the given user. Note that the user + * receiving these values will not necessarily be able to set new values + * themselves if their connection is read-only. This function can be used as + * the callback for guac_client_foreach_user() and guac_client_for_owner() + * + * @param user + * The user that should receive the values of all non-sensitive parameters + * which may be set while the connection is running. + * + * @param data + * The guac_ssh_client instance associated with the current connection. + * + * @return + * Always NULL. + */ +void* guac_ssh_send_current_argv(guac_user* user, void* data); + +#endif + diff --git a/src/protocols/ssh/settings.c b/src/protocols/ssh/settings.c index e0be6cf3..7dab3215 100644 --- a/src/protocols/ssh/settings.c +++ b/src/protocols/ssh/settings.c @@ -60,6 +60,10 @@ const char* GUAC_SSH_CLIENT_ARGS[] = { "backspace", "terminal-type", "scrollback", + "locale", + "timezone", + "disable-copy", + "disable-paste", NULL }; @@ -238,6 +242,38 @@ enum SSH_ARGS_IDX { */ IDX_SCROLLBACK, + /** + * The locale that should be forwarded to the remote system via the LANG + * environment variable. By default, no locale is forwarded. This setting + * will only have an effect if the SSH server allows the LANG environment + * variable to be set. + */ + IDX_LOCALE, + + /** + * The timezone that is to be passed to the remote system, via the + * TZ environment variable. By default, no timezone is forwarded + * and the timezone of the remote system will be used. This + * setting will only work if the SSH server allows the TZ variable + * to be set. Timezones should be in standard IANA format, see: + * https://en.wikipedia.org/wiki/List_of_tz_database_time_zones + */ + IDX_TIMEZONE, + + /** + * Whether outbound clipboard access should be blocked. If set to "true", + * it will not be possible to copy data from the terminal to the client + * using the clipboard. By default, clipboard access is not blocked. + */ + IDX_DISABLE_COPY, + + /** + * Whether inbound clipboard access should be blocked. If set to "true", it + * will not be possible to paste data from the client to the terminal using + * the clipboard. By default, clipboard access is not blocked. + */ + IDX_DISABLE_PASTE, + SSH_ARGS_COUNT }; @@ -396,6 +432,26 @@ guac_ssh_settings* guac_ssh_parse_args(guac_user* user, guac_user_parse_args_string(user, GUAC_SSH_CLIENT_ARGS, argv, IDX_TERMINAL_TYPE, "linux"); + /* Read locale */ + settings->locale = + guac_user_parse_args_string(user, GUAC_SSH_CLIENT_ARGS, argv, + IDX_LOCALE, NULL); + + /* Read the timezone parameter, or use client handshake. */ + settings->timezone = + guac_user_parse_args_string(user, GUAC_SSH_CLIENT_ARGS, argv, + IDX_TIMEZONE, user->info.timezone); + + /* Parse clipboard copy disable flag */ + settings->disable_copy = + guac_user_parse_args_boolean(user, GUAC_SSH_CLIENT_ARGS, argv, + IDX_DISABLE_COPY, false); + + /* Parse clipboard paste disable flag */ + settings->disable_paste = + guac_user_parse_args_boolean(user, GUAC_SSH_CLIENT_ARGS, argv, + IDX_DISABLE_PASTE, false); + /* Parsing was successful */ return settings; @@ -435,6 +491,12 @@ void guac_ssh_settings_free(guac_ssh_settings* settings) { /* Free terminal emulator type. */ free(settings->terminal_type); + /* Free locale */ + free(settings->locale); + + /* Free the client timezone. */ + free(settings->timezone); + /* Free overall structure */ free(settings); diff --git a/src/protocols/ssh/settings.h b/src/protocols/ssh/settings.h index 81dcd699..bab21bdf 100644 --- a/src/protocols/ssh/settings.h +++ b/src/protocols/ssh/settings.h @@ -155,6 +155,20 @@ typedef struct guac_ssh_settings { */ int resolution; + /** + * Whether outbound clipboard access should be blocked. If set, it will not + * be possible to copy data from the terminal to the client using the + * clipboard. + */ + bool disable_copy; + + /** + * Whether inbound clipboard access should be blocked. If set, it will not + * be possible to paste data from the client to the terminal using the + * clipboard. + */ + bool disable_paste; + /** * Whether SFTP is enabled. */ @@ -248,6 +262,17 @@ typedef struct guac_ssh_settings { */ char* terminal_type; + /** + * The locale that should be forwarded to the remote system via the LANG + * environment variable. + */ + char* locale; + + /** + * The client timezone to pass to the remote system. + */ + char* timezone; + } guac_ssh_settings; /** diff --git a/src/protocols/ssh/ssh.c b/src/protocols/ssh/ssh.c index 59554fb7..89572050 100644 --- a/src/protocols/ssh/ssh.c +++ b/src/protocols/ssh/ssh.c @@ -19,6 +19,7 @@ #include "config.h" +#include "argv.h" #include "common/recording.h" #include "common-ssh/sftp.h" #include "common-ssh/ssh.h" @@ -130,17 +131,9 @@ static guac_common_ssh_user* guac_ssh_get_user(guac_client* client) { } /* end if key given */ - /* Otherwise, use password */ - else { - - /* Get password if not provided */ - if (settings->password == NULL) - settings->password = guac_terminal_prompt(ssh_client->term, - "Password: ", false); - - /* Set provided password */ + /* If available, get password from settings */ + else if (settings->password != NULL) { guac_common_ssh_user_set_password(user, settings->password); - } /* Clear screen of any prompts */ @@ -150,6 +143,29 @@ static guac_common_ssh_user* guac_ssh_get_user(guac_client* client) { } +/** + * A function used to generate a terminal prompt to gather additional + * credentials from the guac_client during a connection, and using + * the specified string to generate the prompt for the user. + * + * @param client + * The guac_client object associated with the current connection + * where additional credentials are required. + * + * @param cred_name + * The prompt text to display to the screen when prompting for the + * additional credentials. + * + * @return + * The string of credentials gathered from the user. + */ +static char* guac_ssh_get_credential(guac_client *client, char* cred_name) { + + guac_ssh_client* ssh_client = (guac_ssh_client*) client->data; + return guac_terminal_prompt(ssh_client->term, cred_name, false); + +} + void* ssh_input_thread(void* data) { guac_client* client = (guac_client*) data; @@ -207,9 +223,10 @@ void* ssh_client_thread(void* data) { /* Create terminal */ ssh_client->term = guac_terminal_create(client, ssh_client->clipboard, - settings->max_scrollback, settings->font_name, settings->font_size, - settings->resolution, settings->width, settings->height, - settings->color_scheme, settings->backspace); + settings->disable_copy, settings->max_scrollback, + settings->font_name, settings->font_size, settings->resolution, + settings->width, settings->height, settings->color_scheme, + settings->backspace); /* Fail if terminal init failed */ if (ssh_client->term == NULL) { @@ -218,6 +235,9 @@ void* ssh_client_thread(void* data) { return NULL; } + /* Send current values of exposed arguments to owner only */ + guac_client_for_owner(client, guac_ssh_send_current_argv, ssh_client); + /* Set up typescript, if requested */ if (settings->typescript_path != NULL) { guac_terminal_create_typescript(ssh_client->term, @@ -233,10 +253,13 @@ void* ssh_client_thread(void* data) { return NULL; } + /* Ensure connection is kept alive during lengthy connects */ + guac_socket_require_keep_alive(client->socket); + /* Open SSH session */ ssh_client->session = guac_common_ssh_create_session(client, settings->hostname, settings->port, ssh_client->user, settings->server_alive_interval, - settings->host_key); + settings->host_key, guac_ssh_get_credential); if (ssh_client->session == NULL) { /* Already aborted within guac_common_ssh_create_session() */ return NULL; @@ -253,6 +276,17 @@ void* ssh_client_thread(void* data) { return NULL; } + /* Set the client timezone */ + if (settings->timezone != NULL) { + if (libssh2_channel_setenv(ssh_client->term_channel, "TZ", + settings->timezone)) { + guac_client_log(client, GUAC_LOG_WARNING, + "Unable to set the timezone: SSH server " + "refused to set \"TZ\" variable."); + } + } + + #ifdef ENABLE_SSH_AGENT /* Start SSH agent forwarding, if enabled */ if (ssh_client->enable_agent) { @@ -277,7 +311,7 @@ void* ssh_client_thread(void* data) { ssh_client->sftp_session = guac_common_ssh_create_session(client, settings->hostname, settings->port, ssh_client->user, settings->server_alive_interval, - settings->host_key); + settings->host_key, NULL); if (ssh_client->sftp_session == NULL) { /* Already aborted within guac_common_ssh_create_session() */ return NULL; @@ -317,6 +351,16 @@ void* ssh_client_thread(void* data) { return NULL; } + /* Forward specified locale */ + if (settings->locale != NULL) { + if (libssh2_channel_setenv(ssh_client->term_channel, "LANG", + settings->locale)) { + guac_client_log(client, GUAC_LOG_WARNING, + "Unable to forward locale: SSH server refused to set " + "\"LANG\" environment variable."); + } + } + /* If a command is specified, run that instead of a shell */ if (settings->command != NULL) { if (libssh2_channel_exec(ssh_client->term_channel, settings->command)) { @@ -335,6 +379,7 @@ void* ssh_client_thread(void* data) { /* Logged in */ guac_client_log(client, GUAC_LOG_INFO, "SSH connection successful."); + guac_terminal_start(ssh_client->term); /* Start input thread */ if (pthread_create(&(input_thread), NULL, ssh_input_thread, (void*) client)) { diff --git a/src/protocols/ssh/user.c b/src/protocols/ssh/user.c index e4dda40e..8ea30a4c 100644 --- a/src/protocols/ssh/user.c +++ b/src/protocols/ssh/user.c @@ -19,6 +19,7 @@ #include "config.h" +#include "argv.h" #include "clipboard.h" #include "common/display.h" #include "input.h" @@ -73,20 +74,27 @@ int guac_ssh_user_join_handler(guac_user* user, int argc, char** argv) { /* If not owner, synchronize with current display */ else { guac_terminal_dup(ssh_client->term, user, user->socket); + guac_ssh_send_current_argv(user, ssh_client); guac_socket_flush(user->socket); } /* Only handle events if not read-only */ if (!settings->read_only) { - /* General mouse/keyboard/clipboard events */ - user->key_handler = guac_ssh_user_key_handler; - user->mouse_handler = guac_ssh_user_mouse_handler; - user->clipboard_handler = guac_ssh_clipboard_handler; + /* General mouse/keyboard events */ + user->key_handler = guac_ssh_user_key_handler; + user->mouse_handler = guac_ssh_user_mouse_handler; + + /* Inbound (client to server) clipboard transfer */ + if (!settings->disable_paste) + user->clipboard_handler = guac_ssh_clipboard_handler; /* STDIN redirection */ user->pipe_handler = guac_ssh_pipe_handler; + /* Updates to connection parameters */ + user->argv_handler = guac_ssh_argv_handler; + /* Display size change events */ user->size_handler = guac_ssh_user_size_handler; diff --git a/src/protocols/telnet/.gitignore b/src/protocols/telnet/.gitignore deleted file mode 100644 index 3f725332..00000000 --- a/src/protocols/telnet/.gitignore +++ /dev/null @@ -1,36 +0,0 @@ - -# Object code -*.o -*.so -*.lo -*.la - -# Backup files -*~ - -# Release files -*.tar.gz - -# Files currently being edited by vim or vi -*.swp - -# automake/autoconf -.deps/ -.libs/ -Makefile -Makefile.in -aclocal.m4 -autom4te.cache/ -m4/* -!README -config.guess -config.log -config.status -config.sub -configure -depcomp -install-sh -libtool -ltmain.sh -missing - diff --git a/src/protocols/telnet/Makefile.am b/src/protocols/telnet/Makefile.am index a44118de..d0264f67 100644 --- a/src/protocols/telnet/Makefile.am +++ b/src/protocols/telnet/Makefile.am @@ -16,6 +16,12 @@ # specific language governing permissions and limitations # under the License. # +# NOTE: Parts of this file (Makefile.am) are automatically transcluded verbatim +# into Makefile.in. Though the build system (GNU Autotools) automatically adds +# its own license boilerplate to the generated Makefile.in, that boilerplate +# does not apply to the transcluded portions of Makefile.am which are licensed +# to you by the ASF under the Apache License, Version 2.0, as described above. +# AUTOMAKE_OPTIONS = foreign ACLOCAL_AMFLAGS = -I m4 @@ -23,6 +29,7 @@ ACLOCAL_AMFLAGS = -I m4 lib_LTLIBRARIES = libguac-client-telnet.la libguac_client_telnet_la_SOURCES = \ + argv.c \ client.c \ clipboard.c \ input.c \ @@ -32,6 +39,7 @@ libguac_client_telnet_la_SOURCES = \ user.c noinst_HEADERS = \ + argv.h \ client.h \ clipboard.h \ input.h \ diff --git a/src/protocols/telnet/argv.c b/src/protocols/telnet/argv.c new file mode 100644 index 00000000..37564a60 --- /dev/null +++ b/src/protocols/telnet/argv.c @@ -0,0 +1,225 @@ +/* + * 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 "config.h" +#include "argv.h" +#include "telnet.h" +#include "terminal/terminal.h" + +#include +#include +#include + +#include +#include + +/** + * All telnet connection settings which may be updated by unprivileged users + * through "argv" streams. + */ +typedef enum guac_telnet_argv_setting { + + /** + * The color scheme of the terminal. + */ + GUAC_TELNET_ARGV_SETTING_COLOR_SCHEME, + + /** + * The name of the font family used by the terminal. + */ + GUAC_TELNET_ARGV_SETTING_FONT_NAME, + + /** + * The size of the font used by the terminal, in points. + */ + GUAC_TELNET_ARGV_SETTING_FONT_SIZE + +} guac_telnet_argv_setting; + +/** + * The value or current status of a connection parameter received over an + * "argv" stream. + */ +typedef struct guac_telnet_argv { + + /** + * The specific setting being updated. + */ + guac_telnet_argv_setting setting; + + /** + * Buffer space for containing the received argument value. + */ + char buffer[GUAC_TELNET_ARGV_MAX_LENGTH]; + + /** + * The number of bytes received so far. + */ + int length; + +} guac_telnet_argv; + +/** + * Handler for "blob" instructions which appends the data from received blobs + * to the end of the in-progress argument value buffer. + * + * @see guac_user_blob_handler + */ +static int guac_telnet_argv_blob_handler(guac_user* user, + guac_stream* stream, void* data, int length) { + + guac_telnet_argv* argv = (guac_telnet_argv*) stream->data; + + /* Calculate buffer size remaining, including space for null terminator, + * adjusting received length accordingly */ + int remaining = sizeof(argv->buffer) - argv->length - 1; + if (length > remaining) + length = remaining; + + /* Append received data to end of buffer */ + memcpy(argv->buffer + argv->length, data, length); + argv->length += length; + + return 0; + +} + +/** + * Handler for "end" instructions which applies the changes specified by the + * argument value buffer associated with the stream. + * + * @see guac_user_end_handler + */ +static int guac_telnet_argv_end_handler(guac_user* user, + guac_stream* stream) { + + int size; + + guac_client* client = user->client; + guac_telnet_client* telnet_client = (guac_telnet_client*) client->data; + guac_terminal* terminal = telnet_client->term; + + /* Append null terminator to value */ + guac_telnet_argv* argv = (guac_telnet_argv*) stream->data; + argv->buffer[argv->length] = '\0'; + + /* Apply changes to chosen setting */ + switch (argv->setting) { + + /* Update color scheme */ + case GUAC_TELNET_ARGV_SETTING_COLOR_SCHEME: + guac_terminal_apply_color_scheme(terminal, argv->buffer); + guac_client_stream_argv(client, client->socket, "text/plain", + "color-scheme", argv->buffer); + break; + + /* Update font name */ + case GUAC_TELNET_ARGV_SETTING_FONT_NAME: + guac_terminal_apply_font(terminal, argv->buffer, -1, 0); + guac_client_stream_argv(client, client->socket, "text/plain", + "font-name", argv->buffer); + break; + + /* Update font size */ + case GUAC_TELNET_ARGV_SETTING_FONT_SIZE: + + /* Update only if font size is sane */ + size = atoi(argv->buffer); + if (size > 0) { + guac_terminal_apply_font(terminal, NULL, size, + telnet_client->settings->resolution); + guac_client_stream_argv(client, client->socket, "text/plain", + "font-size", argv->buffer); + } + + break; + + } + + /* Update terminal window size if connected */ + if (telnet_client->telnet != NULL && telnet_client->naws_enabled) + guac_telnet_send_naws(telnet_client->telnet, terminal->term_width, + terminal->term_height); + + free(argv); + return 0; + +} + +int guac_telnet_argv_handler(guac_user* user, guac_stream* stream, + char* mimetype, char* name) { + + guac_telnet_argv_setting setting; + + /* Allow users to update the color scheme and font details */ + if (strcmp(name, "color-scheme") == 0) + setting = GUAC_TELNET_ARGV_SETTING_COLOR_SCHEME; + else if (strcmp(name, "font-name") == 0) + setting = GUAC_TELNET_ARGV_SETTING_FONT_NAME; + else if (strcmp(name, "font-size") == 0) + setting = GUAC_TELNET_ARGV_SETTING_FONT_SIZE; + + /* No other connection parameters may be updated */ + else { + guac_protocol_send_ack(user->socket, stream, "Not allowed.", + GUAC_PROTOCOL_STATUS_CLIENT_FORBIDDEN); + guac_socket_flush(user->socket); + return 0; + } + + guac_telnet_argv* argv = malloc(sizeof(guac_telnet_argv)); + argv->setting = setting; + argv->length = 0; + + /* Prepare stream to receive argument value */ + stream->blob_handler = guac_telnet_argv_blob_handler; + stream->end_handler = guac_telnet_argv_end_handler; + stream->data = argv; + + /* Signal stream is ready */ + guac_protocol_send_ack(user->socket, stream, "Ready for updated " + "parameter.", GUAC_PROTOCOL_STATUS_SUCCESS); + guac_socket_flush(user->socket); + return 0; + +} + +void* guac_telnet_send_current_argv(guac_user* user, void* data) { + + guac_telnet_client* telnet_client = (guac_telnet_client*) data; + guac_terminal* terminal = telnet_client->term; + + /* Send current color scheme */ + guac_user_stream_argv(user, user->socket, "text/plain", "color-scheme", + terminal->color_scheme); + + /* Send current font name */ + guac_user_stream_argv(user, user->socket, "text/plain", "font-name", + terminal->font_name); + + /* Send current font size */ + char font_size[64]; + sprintf(font_size, "%i", terminal->font_size); + guac_user_stream_argv(user, user->socket, "text/plain", "font-size", + font_size); + + return NULL; + +} + diff --git a/src/protocols/telnet/argv.h b/src/protocols/telnet/argv.h new file mode 100644 index 00000000..e362e14a --- /dev/null +++ b/src/protocols/telnet/argv.h @@ -0,0 +1,60 @@ +/* + * 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. + */ + + +#ifndef GUAC_TELNET_ARGV_H +#define GUAC_TELNET_ARGV_H + +#include "config.h" + +#include + +/** + * The maximum number of bytes to allow for any argument value received via an + * argv stream, including null terminator. + */ +#define GUAC_TELNET_ARGV_MAX_LENGTH 16384 + +/** + * Handles an incoming stream from a Guacamole "argv" instruction, updating the + * given connection parameter if that parameter is allowed to be updated. + */ +guac_user_argv_handler guac_telnet_argv_handler; + +/** + * Sends the current values of all non-sensitive parameters which may be set + * while the connection is running to the given user. Note that the user + * receiving these values will not necessarily be able to set new values + * themselves if their connection is read-only. This function can be used as + * the callback for guac_client_foreach_user() and guac_client_for_owner() + * + * @param user + * The user that should receive the values of all non-sensitive parameters + * which may be set while the connection is running. + * + * @param data + * The guac_telnet_client instance associated with the current connection. + * + * @return + * Always NULL. + */ +void* guac_telnet_send_current_argv(guac_user* user, void* data); + +#endif + diff --git a/src/protocols/telnet/settings.c b/src/protocols/telnet/settings.c index bef5cfae..e72359c9 100644 --- a/src/protocols/telnet/settings.c +++ b/src/protocols/telnet/settings.c @@ -53,11 +53,15 @@ const char* GUAC_TELNET_CLIENT_ARGS[] = { "backspace", "terminal-type", "scrollback", + "login-success-regex", + "login-failure-regex", + "disable-copy", + "disable-paste", NULL }; enum TELNET_ARGS_IDX { - + /** * The hostname to connect to. Required. */ @@ -196,12 +200,45 @@ enum TELNET_ARGS_IDX { */ IDX_SCROLLBACK, + /** + * The regular expression to use when searching for whether login was + * successful. This parameter is optional. If given, the + * "login-failure-regex" parameter must also be specified, and the first + * frame of the Guacamole connection will be withheld until login + * success/failure has been determined. + */ + IDX_LOGIN_SUCCESS_REGEX, + + /** + * The regular expression to use when searching for whether login was + * unsuccessful. This parameter is optional. If given, the + * "login-success-regex" parameter must also be specified, and the first + * frame of the Guacamole connection will be withheld until login + * success/failure has been determined. + */ + IDX_LOGIN_FAILURE_REGEX, + + /** + * Whether outbound clipboard access should be blocked. If set to "true", + * it will not be possible to copy data from the terminal to the client + * using the clipboard. By default, clipboard access is not blocked. + */ + IDX_DISABLE_COPY, + + /** + * Whether inbound clipboard access should be blocked. If set to "true", it + * will not be possible to paste data from the client to the terminal using + * the clipboard. By default, clipboard access is not blocked. + */ + IDX_DISABLE_PASTE, + TELNET_ARGS_COUNT }; /** - * Compiles the given regular expression, returning NULL if compilation fails. - * The returned regex_t must be freed with regfree() AND free(). + * Compiles the given regular expression, returning NULL if compilation fails + * or of the given regular expression is NULL. The returned regex_t must be + * freed with regfree() AND free(), or with guac_telnet_regex_free(). * * @param user * The user who provided the setting associated with the given regex @@ -211,10 +248,15 @@ enum TELNET_ARGS_IDX { * The regular expression pattern to compile. * * @return - * The compiled regular expression, or NULL if compilation fails. + * The compiled regular expression, or NULL if compilation fails or NULL + * was originally provided for the pattern. */ static regex_t* guac_telnet_compile_regex(guac_user* user, char* pattern) { + /* Nothing to compile if no pattern provided */ + if (pattern == NULL) + return NULL; + int compile_result; regex_t* regex = malloc(sizeof(regex_t)); @@ -233,6 +275,14 @@ static regex_t* guac_telnet_compile_regex(guac_user* user, char* pattern) { return regex; } +void guac_telnet_regex_free(regex_t** regex) { + if (*regex != NULL) { + regfree(*regex); + free(*regex); + *regex = NULL; + } +} + guac_telnet_settings* guac_telnet_parse_args(guac_user* user, int argc, const char** argv) { @@ -256,7 +306,7 @@ guac_telnet_settings* guac_telnet_parse_args(guac_user* user, guac_user_parse_args_string(user, GUAC_TELNET_CLIENT_ARGS, argv, IDX_USERNAME, NULL); - /* Read username regex only if password is specified */ + /* Read username regex only if username is specified */ if (settings->username != NULL) { settings->username_regex = guac_telnet_compile_regex(user, guac_user_parse_args_string(user, GUAC_TELNET_CLIENT_ARGS, argv, @@ -275,6 +325,35 @@ guac_telnet_settings* guac_telnet_parse_args(guac_user* user, IDX_PASSWORD_REGEX, GUAC_TELNET_DEFAULT_PASSWORD_REGEX)); } + /* Read optional login success detection regex */ + settings->login_success_regex = guac_telnet_compile_regex(user, + guac_user_parse_args_string(user, GUAC_TELNET_CLIENT_ARGS, argv, + IDX_LOGIN_SUCCESS_REGEX, NULL)); + + /* Read optional login failure detection regex */ + settings->login_failure_regex = guac_telnet_compile_regex(user, + guac_user_parse_args_string(user, GUAC_TELNET_CLIENT_ARGS, argv, + IDX_LOGIN_FAILURE_REGEX, NULL)); + + /* Both login success and login failure regexes must be provided if either + * is present at all */ + if (settings->login_success_regex != NULL + && settings->login_failure_regex == NULL) { + guac_telnet_regex_free(&settings->login_success_regex); + guac_user_log(user, GUAC_LOG_WARNING, "Ignoring provided value for " + "\"%s\" as \"%s\" must also be provided.", + GUAC_TELNET_CLIENT_ARGS[IDX_LOGIN_SUCCESS_REGEX], + GUAC_TELNET_CLIENT_ARGS[IDX_LOGIN_FAILURE_REGEX]); + } + else if (settings->login_failure_regex != NULL + && settings->login_success_regex == NULL) { + guac_telnet_regex_free(&settings->login_failure_regex); + guac_user_log(user, GUAC_LOG_WARNING, "Ignoring provided value for " + "\"%s\" as \"%s\" must also be provided.", + GUAC_TELNET_CLIENT_ARGS[IDX_LOGIN_FAILURE_REGEX], + GUAC_TELNET_CLIENT_ARGS[IDX_LOGIN_SUCCESS_REGEX]); + } + /* Read-only mode */ settings->read_only = guac_user_parse_args_boolean(user, GUAC_TELNET_CLIENT_ARGS, argv, @@ -365,6 +444,16 @@ guac_telnet_settings* guac_telnet_parse_args(guac_user* user, guac_user_parse_args_string(user, GUAC_TELNET_CLIENT_ARGS, argv, IDX_TERMINAL_TYPE, "linux"); + /* Parse clipboard copy disable flag */ + settings->disable_copy = + guac_user_parse_args_boolean(user, GUAC_TELNET_CLIENT_ARGS, argv, + IDX_DISABLE_COPY, false); + + /* Parse clipboard paste disable flag */ + settings->disable_paste = + guac_user_parse_args_boolean(user, GUAC_TELNET_CLIENT_ARGS, argv, + IDX_DISABLE_PASTE, false); + /* Parsing was successful */ return settings; @@ -380,17 +469,11 @@ void guac_telnet_settings_free(guac_telnet_settings* settings) { free(settings->username); free(settings->password); - /* Free username regex (if allocated) */ - if (settings->username_regex != NULL) { - regfree(settings->username_regex); - free(settings->username_regex); - } - - /* Free password regex (if allocated) */ - if (settings->password_regex != NULL) { - regfree(settings->password_regex); - free(settings->password_regex); - } + /* Free various regexes */ + guac_telnet_regex_free(&settings->username_regex); + guac_telnet_regex_free(&settings->password_regex); + guac_telnet_regex_free(&settings->login_success_regex); + guac_telnet_regex_free(&settings->login_failure_regex); /* Free display preferences */ free(settings->font_name); diff --git a/src/protocols/telnet/settings.h b/src/protocols/telnet/settings.h index 83ec4313..691669cb 100644 --- a/src/protocols/telnet/settings.h +++ b/src/protocols/telnet/settings.h @@ -117,6 +117,20 @@ typedef struct guac_telnet_settings { */ regex_t* password_regex; + /** + * The regular expression to use when searching for whether login was + * successful. If no such regex is specified, or if no login failure regex + * was specified, this will be NULL. + */ + regex_t* login_success_regex; + + /** + * The regular expression to use when searching for whether login failed. + * If no such regex is specified, or if no login success regex was + * specified, this will be NULL. + */ + regex_t* login_failure_regex; + /** * Whether this connection is read-only, and user input should be dropped. */ @@ -157,6 +171,20 @@ typedef struct guac_telnet_settings { */ int resolution; + /** + * Whether outbound clipboard access should be blocked. If set, it will not + * be possible to copy data from the terminal to the client using the + * clipboard. + */ + bool disable_copy; + + /** + * Whether inbound clipboard access should be blocked. If set, it will not + * be possible to paste data from the client to the terminal using the + * clipboard. + */ + bool disable_paste; + /** * The path in which the typescript should be saved, if enabled. If no * typescript should be saved, this will be NULL. @@ -253,6 +281,16 @@ typedef struct guac_telnet_settings { guac_telnet_settings* guac_telnet_parse_args(guac_user* user, int argc, const char** argv); +/** + * Frees the regex pointed to by the given pointer, assigning the value NULL to + * that pointer once the regex is freed. If the pointer already contains NULL, + * this function has no effect. + * + * @param regex + * The address of the pointer to the regex that should be freed. + */ +void guac_telnet_regex_free(regex_t** regex); + /** * Frees the given guac_telnet_settings object, having been previously * allocated via guac_telnet_parse_args(). diff --git a/src/protocols/telnet/telnet.c b/src/protocols/telnet/telnet.c index c9a636c2..5a5ca303 100644 --- a/src/protocols/telnet/telnet.c +++ b/src/protocols/telnet/telnet.c @@ -18,6 +18,8 @@ */ #include "config.h" + +#include "argv.h" #include "common/recording.h" #include "telnet.h" #include "terminal/terminal.h" @@ -31,6 +33,7 @@ #include #include #include +#include #include #include #include @@ -82,57 +85,178 @@ static int __guac_telnet_write_all(int fd, const char* buffer, int size) { } /** - * Searches for a line matching the stored password regex, appending the given - * buffer to the internal pattern matching buffer. The internal pattern match - * buffer is cleared whenever a newline is read. Returns TRUE if a match is found and the - * value is sent. + * Matches the given line against the given regex, returning true and sending + * the given value if a match is found. An enter keypress is automatically + * sent after the value is sent. + * + * @param client + * The guac_client associated with the telnet session. + * + * @param regex + * The regex to search for within the given line buffer. + * + * @param value + * The string value to send through STDIN of the telnet session if a + * match is found, or NULL if no value should be sent. + * + * @param line_buffer + * The line of character data to test. + * + * @return + * true if a match is found, false otherwise. */ -static bool __guac_telnet_regex_search(guac_client* client, regex_t* regex, char* value, const char* buffer, int size) { - - static char line_buffer[1024] = {0}; - static int length = 0; +static bool guac_telnet_regex_exec(guac_client* client, regex_t* regex, + const char* value, const char* line_buffer) { guac_telnet_client* telnet_client = (guac_telnet_client*) client->data; - int i; - const char* current; - - /* Ensure line buffer contains only the most recent line */ - current = buffer; - for (i = 0; i < size; i++) { - - /* Reset line buffer and shift input buffer for each newline */ - if (*(current++) == '\n') { - length = 0; - buffer += i; - size -= i; - i = 0; - } - } - - /* Truncate if necessary */ - if (size + length + 1 > sizeof(line_buffer)) - size = sizeof(line_buffer) - length - 1; - - /* Append to line */ - memcpy(&(line_buffer[length]), buffer, size); - length += size; - line_buffer[length] = '\0'; - /* Send value upon match */ if (regexec(regex, line_buffer, 0, NULL, 0) == 0) { /* Send value */ - guac_terminal_send_string(telnet_client->term, value); - guac_terminal_send_key(telnet_client->term, 0xFF0D, 1); - guac_terminal_send_key(telnet_client->term, 0xFF0D, 0); + if (value != NULL) { + guac_terminal_send_string(telnet_client->term, value); + guac_terminal_send_string(telnet_client->term, "\x0D"); + } /* Stop searching for prompt */ - return TRUE; + return true; } - return FALSE; + return false; + +} + +/** + * Matches the given line against the various stored regexes, automatically + * sending the configured username, password, or reporting login + * success/failure depending on context. If no search is in progress, either + * because no regexes have been defined or because all applicable searches have + * completed, this function has no effect. + * + * @param client + * The guac_client associated with the telnet session. + * + * @param line_buffer + * The line of character data to test. + */ +static void guac_telnet_search_line(guac_client* client, const char* line_buffer) { + + guac_telnet_client* telnet_client = (guac_telnet_client*) client->data; + guac_telnet_settings* settings = telnet_client->settings; + + /* Continue search for username prompt */ + if (settings->username_regex != NULL) { + if (guac_telnet_regex_exec(client, settings->username_regex, + settings->username, line_buffer)) { + guac_client_log(client, GUAC_LOG_DEBUG, "Username sent"); + guac_telnet_regex_free(&settings->username_regex); + } + } + + /* Continue search for password prompt */ + if (settings->password_regex != NULL) { + if (guac_telnet_regex_exec(client, settings->password_regex, + settings->password, line_buffer)) { + + guac_client_log(client, GUAC_LOG_DEBUG, "Password sent"); + + /* Do not continue searching for username/password once password is sent */ + guac_telnet_regex_free(&settings->username_regex); + guac_telnet_regex_free(&settings->password_regex); + + } + } + + /* Continue search for login success */ + if (settings->login_success_regex != NULL) { + if (guac_telnet_regex_exec(client, settings->login_success_regex, + NULL, line_buffer)) { + + /* Allow terminal to render now that login has been deemed successful */ + guac_client_log(client, GUAC_LOG_DEBUG, "Login successful"); + guac_terminal_start(telnet_client->term); + + /* Stop all searches */ + guac_telnet_regex_free(&settings->username_regex); + guac_telnet_regex_free(&settings->password_regex); + guac_telnet_regex_free(&settings->login_success_regex); + guac_telnet_regex_free(&settings->login_failure_regex); + + } + } + + /* Continue search for login failure */ + if (settings->login_failure_regex != NULL) { + if (guac_telnet_regex_exec(client, settings->login_failure_regex, + NULL, line_buffer)) { + + /* Advise that login has failed and connection should be closed */ + guac_client_abort(client, + GUAC_PROTOCOL_STATUS_CLIENT_UNAUTHORIZED, + "Login failed"); + + /* Stop all searches */ + guac_telnet_regex_free(&settings->username_regex); + guac_telnet_regex_free(&settings->password_regex); + guac_telnet_regex_free(&settings->login_success_regex); + guac_telnet_regex_free(&settings->login_failure_regex); + + } + } + +} + +/** + * Searches for a line matching the various stored regexes, automatically + * sending the configured username, password, or reporting login + * success/failure depending on context. If no search is in progress, either + * because no regexes have been defined or because all applicable searches + * have completed, this function has no effect. + * + * @param client + * The guac_client associated with the telnet session. + * + * @param buffer + * The buffer of received data to search through. + * + * @param size + * The size of the given buffer, in bytes. + */ +static void guac_telnet_search(guac_client* client, const char* buffer, int size) { + + static char line_buffer[1024] = {0}; + static int length = 0; + + /* Append all characters in buffer to current line */ + const char* current = buffer; + for (int i = 0; i < size; i++) { + + char c = *(current++); + + /* Attempt pattern match and clear buffer upon reading newline */ + if (c == '\n') { + if (length > 0) { + line_buffer[length] = '\0'; + guac_telnet_search_line(client, line_buffer); + length = 0; + } + } + + /* Append all non-newline characters to line buffer as long as space + * remains */ + else if (length < sizeof(line_buffer) - 1) + line_buffer[length++] = c; + + } + + /* Attempt pattern match if an unfinished line remains (may be a prompt) */ + if (length > 0) { + line_buffer[length] = '\0'; + guac_telnet_search_line(client, line_buffer); + } + } /** @@ -151,39 +275,7 @@ static void __guac_telnet_event_handler(telnet_t* telnet, telnet_event_t* event, /* Terminal output received */ case TELNET_EV_DATA: guac_terminal_write(telnet_client->term, event->data.buffer, event->data.size); - - /* Continue search for username prompt */ - if (settings->username_regex != NULL) { - if (__guac_telnet_regex_search(client, - settings->username_regex, settings->username, - event->data.buffer, event->data.size)) { - guac_client_log(client, GUAC_LOG_DEBUG, "Username sent"); - regfree(settings->username_regex); - free(settings->username_regex); - settings->username_regex = NULL; - } - } - - /* Continue search for password prompt */ - if (settings->password_regex != NULL) { - if (__guac_telnet_regex_search(client, - settings->password_regex, settings->password, - event->data.buffer, event->data.size)) { - - guac_client_log(client, GUAC_LOG_DEBUG, "Password sent"); - - /* Do not continue searching for username once password is sent */ - if (settings->username_regex != NULL) { - regfree(settings->username_regex); - free(settings->username_regex); - settings->username_regex = NULL; - } - - regfree(settings->password_regex); - free(settings->password_regex); - settings->password_regex = NULL; - } - } + guac_telnet_search(client, event->data.buffer, event->data.size); break; /* Data destined for remote end */ @@ -478,7 +570,7 @@ void* guac_telnet_client_thread(void* data) { /* Create terminal */ telnet_client->term = guac_terminal_create(client, - telnet_client->clipboard, + telnet_client->clipboard, settings->disable_copy, settings->max_scrollback, settings->font_name, settings->font_size, settings->resolution, settings->width, settings->height, settings->color_scheme, settings->backspace); @@ -490,6 +582,10 @@ void* guac_telnet_client_thread(void* data) { return NULL; } + /* Send current values of exposed arguments to owner only */ + guac_client_for_owner(client, guac_telnet_send_current_argv, + telnet_client); + /* Set up typescript, if requested */ if (settings->typescript_path != NULL) { guac_terminal_create_typescript(telnet_client->term, @@ -508,6 +604,12 @@ void* guac_telnet_client_thread(void* data) { /* Logged in */ guac_client_log(client, GUAC_LOG_INFO, "Telnet connection successful."); + /* Allow terminal to render if login success/failure detection is not + * enabled */ + if (settings->login_success_regex == NULL + && settings->login_failure_regex == NULL) + guac_terminal_start(telnet_client->term); + /* Start input thread */ if (pthread_create(&(input_thread), NULL, __guac_telnet_input_thread, (void*) client)) { guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR, "Unable to start input thread"); diff --git a/src/protocols/telnet/user.c b/src/protocols/telnet/user.c index 2e34f780..63408cca 100644 --- a/src/protocols/telnet/user.c +++ b/src/protocols/telnet/user.c @@ -19,6 +19,7 @@ #include "config.h" +#include "argv.h" #include "clipboard.h" #include "input.h" #include "pipe.h" @@ -72,20 +73,27 @@ int guac_telnet_user_join_handler(guac_user* user, int argc, char** argv) { /* If not owner, synchronize with current display */ else { guac_terminal_dup(telnet_client->term, user, user->socket); + guac_telnet_send_current_argv(user, telnet_client); guac_socket_flush(user->socket); } /* Only handle events if not read-only */ if (!settings->read_only) { - /* General mouse/keyboard/clipboard events */ - user->key_handler = guac_telnet_user_key_handler; - user->mouse_handler = guac_telnet_user_mouse_handler; - user->clipboard_handler = guac_telnet_clipboard_handler; + /* General mouse/keyboard events */ + user->key_handler = guac_telnet_user_key_handler; + user->mouse_handler = guac_telnet_user_mouse_handler; + + /* Inbound (client to server) clipboard transfer */ + if (!settings->disable_paste) + user->clipboard_handler = guac_telnet_clipboard_handler; /* STDIN redirection */ user->pipe_handler = guac_telnet_pipe_handler; + /* Updates to connection parameters */ + user->argv_handler = guac_telnet_argv_handler; + /* Display size change events */ user->size_handler = guac_telnet_user_size_handler; diff --git a/src/protocols/vnc/.gitignore b/src/protocols/vnc/.gitignore deleted file mode 100644 index 3f725332..00000000 --- a/src/protocols/vnc/.gitignore +++ /dev/null @@ -1,36 +0,0 @@ - -# Object code -*.o -*.so -*.lo -*.la - -# Backup files -*~ - -# Release files -*.tar.gz - -# Files currently being edited by vim or vi -*.swp - -# automake/autoconf -.deps/ -.libs/ -Makefile -Makefile.in -aclocal.m4 -autom4te.cache/ -m4/* -!README -config.guess -config.log -config.status -config.sub -configure -depcomp -install-sh -libtool -ltmain.sh -missing - diff --git a/src/protocols/vnc/Makefile.am b/src/protocols/vnc/Makefile.am index 0c5ea478..fcba33f1 100644 --- a/src/protocols/vnc/Makefile.am +++ b/src/protocols/vnc/Makefile.am @@ -16,6 +16,12 @@ # specific language governing permissions and limitations # under the License. # +# NOTE: Parts of this file (Makefile.am) are automatically transcluded verbatim +# into Makefile.in. Though the build system (GNU Autotools) automatically adds +# its own license boilerplate to the generated Makefile.in, that boilerplate +# does not apply to the transcluded portions of Makefile.am which are licensed +# to you by the ASF under the Apache License, Version 2.0, as described above. +# AUTOMAKE_OPTIONS = foreign ACLOCAL_AMFLAGS = -I m4 diff --git a/src/protocols/vnc/auth.c b/src/protocols/vnc/auth.c index e93dbc66..8cc6b1d7 100644 --- a/src/protocols/vnc/auth.c +++ b/src/protocols/vnc/auth.c @@ -31,3 +31,22 @@ char* guac_vnc_get_password(rfbClient* client) { return ((guac_vnc_client*) gc->data)->settings->password; } +rfbCredential* guac_vnc_get_credentials(rfbClient* client, int credentialType) { + guac_client* gc = rfbClientGetClientData(client, GUAC_VNC_CLIENT_KEY); + guac_vnc_settings* settings = ((guac_vnc_client*) gc->data)->settings; + + if (credentialType == rfbCredentialTypeUser) { + rfbCredential *creds = malloc(sizeof(rfbCredential)); + creds->userCredential.username = settings->username; + creds->userCredential.password = settings->password; + return creds; + } + + guac_client_abort(gc, GUAC_PROTOCOL_STATUS_SERVER_ERROR, + "Unsupported credential type requested."); + guac_client_log(gc, GUAC_LOG_DEBUG, + "Unable to provide requested type of credential: %d.", + credentialType); + return NULL; + +} diff --git a/src/protocols/vnc/auth.h b/src/protocols/vnc/auth.h index 615d80a1..155a48db 100644 --- a/src/protocols/vnc/auth.h +++ b/src/protocols/vnc/auth.h @@ -27,7 +27,7 @@ /** * Callback which is invoked by libVNCServer when it needs to read the user's - * VNC password. As ths user's password, if any, will be stored in the + * VNC password. As this user's password, if any, will be stored in the * connection settings, this function does nothing more than return that value. * * @param client @@ -38,5 +38,23 @@ */ char* guac_vnc_get_password(rfbClient* client); +/** + * Callback which is invoked by libVNCServer when it needs to read the user's + * VNC credentials. The credentials are stored in the connection settings, + * so they will be retrieved from that. + * + * @param client + * The rfbClient associated with the VNC connection requiring the + * authentication. + * + * @param credentialType + * The credential type being requested, as defined by the libVNCclient + * code in the rfbclient.h header. + * + * @return + * The rfbCredential object that contains the required credentials. + */ +rfbCredential* guac_vnc_get_credentials(rfbClient* client, int credentialType); + #endif diff --git a/src/protocols/vnc/client.c b/src/protocols/vnc/client.c index 9cc85a18..cee1d4d5 100644 --- a/src/protocols/vnc/client.c +++ b/src/protocols/vnc/client.c @@ -36,6 +36,7 @@ #include +#include #include #include @@ -48,6 +49,11 @@ int guac_client_init(guac_client* client) { guac_vnc_client* vnc_client = calloc(1, sizeof(guac_vnc_client)); client->data = vnc_client; +#ifdef ENABLE_VNC_TLS_LOCKING + /* Initialize the write lock */ + pthread_mutex_init(&(vnc_client->tls_lock), NULL); +#endif + /* Init clipboard */ vnc_client->clipboard = guac_common_clipboard_alloc(GUAC_VNC_CLIPBOARD_MAX_LENGTH); @@ -125,6 +131,11 @@ int guac_vnc_client_free_handler(guac_client* client) { if (settings != NULL) guac_vnc_settings_free(settings); +#ifdef ENABLE_VNC_TLS_LOCKING + /* Clean up TLS lock mutex. */ + pthread_mutex_destroy(&(vnc_client->tls_lock)); +#endif + /* Free generic data struct */ free(client->data); diff --git a/src/protocols/vnc/clipboard.c b/src/protocols/vnc/clipboard.c index a49f5651..c3b1bd1f 100644 --- a/src/protocols/vnc/clipboard.c +++ b/src/protocols/vnc/clipboard.c @@ -125,6 +125,10 @@ void guac_vnc_cut_text(rfbClient* client, const char* text, int textlen) { guac_client* gc = rfbClientGetClientData(client, GUAC_VNC_CLIENT_KEY); guac_vnc_client* vnc_client = (guac_vnc_client*) gc->data; + /* Ignore received text if outbound clipboard transfer is disabled */ + if (vnc_client->settings->disable_copy) + return; + char received_data[GUAC_VNC_CLIPBOARD_MAX_LENGTH]; const char* input = text; diff --git a/src/protocols/vnc/settings.c b/src/protocols/vnc/settings.c index 587864a4..21f64057 100644 --- a/src/protocols/vnc/settings.c +++ b/src/protocols/vnc/settings.c @@ -35,6 +35,7 @@ const char* GUAC_VNC_CLIENT_ARGS[] = { "port", "read-only", "encodings", + "username", "password", "swap-red-blue", "color-depth", @@ -77,12 +78,13 @@ const char* GUAC_VNC_CLIENT_ARGS[] = { "recording-exclude-mouse", "recording-include-keys", "create-recording-path", - + "disable-copy", + "disable-paste", NULL }; enum VNC_ARGS_IDX { - + /** * The hostname of the VNC server (or repeater) to connect to. */ @@ -107,6 +109,11 @@ enum VNC_ARGS_IDX { */ IDX_ENCODINGS, + /** + * The username to send to the VNC server if authentication is requested. + */ + IDX_USERNAME, + /** * The password to send to the VNC server if authentication is requested. */ @@ -197,6 +204,11 @@ enum VNC_ARGS_IDX { */ IDX_SFTP_HOSTNAME, + /** + * The public SSH host key to identify the SFTP server. + */ + IDX_SFTP_HOST_KEY, + /** * The port of the SSH server to connect to for SFTP. If blank, the default * SSH port of "22" will be used. @@ -209,11 +221,6 @@ enum VNC_ARGS_IDX { */ IDX_SFTP_USERNAME, - /** - * The public SSH host key to identify the SFTP server. - */ - IDX_SFTP_HOST_KEY, - /** * The password to provide when authenticating with the SSH server for * SFTP (if not using a private key). @@ -298,6 +305,20 @@ enum VNC_ARGS_IDX { */ IDX_CREATE_RECORDING_PATH, + /** + * Whether outbound clipboard access should be blocked. If set to "true", + * it will not be possible to copy data from the remote desktop to the + * client using the clipboard. By default, clipboard access is not blocked. + */ + IDX_DISABLE_COPY, + + /** + * Whether inbound clipboard access should be blocked. If set to "true", it + * will not be possible to paste data from the client to the remote desktop + * using the clipboard. By default, clipboard access is not blocked. + */ + IDX_DISABLE_PASTE, + VNC_ARGS_COUNT }; @@ -322,10 +343,14 @@ guac_vnc_settings* guac_vnc_parse_args(guac_user* user, guac_user_parse_args_int(user, GUAC_VNC_CLIENT_ARGS, argv, IDX_PORT, 0); + settings->username = + guac_user_parse_args_string(user, GUAC_VNC_CLIENT_ARGS, argv, + IDX_USERNAME, ""); /* NOTE: freed by libvncclient */ + settings->password = guac_user_parse_args_string(user, GUAC_VNC_CLIENT_ARGS, argv, IDX_PASSWORD, ""); /* NOTE: freed by libvncclient */ - + /* Remote cursor */ if (strcmp(argv[IDX_CURSOR], "remote") == 0) { guac_user_log(user, GUAC_LOG_INFO, "Cursor rendering: remote"); @@ -493,6 +518,16 @@ guac_vnc_settings* guac_vnc_parse_args(guac_user* user, guac_user_parse_args_boolean(user, GUAC_VNC_CLIENT_ARGS, argv, IDX_CREATE_RECORDING_PATH, false); + /* Parse clipboard copy disable flag */ + settings->disable_copy = + guac_user_parse_args_boolean(user, GUAC_VNC_CLIENT_ARGS, argv, + IDX_DISABLE_COPY, false); + + /* Parse clipboard paste disable flag */ + settings->disable_paste = + guac_user_parse_args_boolean(user, GUAC_VNC_CLIENT_ARGS, argv, + IDX_DISABLE_PASTE, false); + return settings; } diff --git a/src/protocols/vnc/settings.h b/src/protocols/vnc/settings.h index 3e2ebd5e..34c08ec9 100644 --- a/src/protocols/vnc/settings.h +++ b/src/protocols/vnc/settings.h @@ -45,6 +45,11 @@ typedef struct guac_vnc_settings { */ int port; + /** + * The username given in the arguments. + */ + char* username; + /** * The password given in the arguments. */ @@ -127,6 +132,20 @@ typedef struct guac_vnc_settings { */ char* clipboard_encoding; + /** + * Whether outbound clipboard access should be blocked. If set, it will not + * be possible to copy data from the remote desktop to the client using the + * clipboard. + */ + bool disable_copy; + + /** + * Whether inbound clipboard access should be blocked. If set, it will not + * be possible to paste data from the client to the remote desktop using + * the clipboard. + */ + bool disable_paste; + #ifdef ENABLE_COMMON_SSH /** * Whether SFTP should be enabled for the VNC connection. diff --git a/src/protocols/vnc/user.c b/src/protocols/vnc/user.c index da3b843f..0dee5043 100644 --- a/src/protocols/vnc/user.c +++ b/src/protocols/vnc/user.c @@ -91,10 +91,13 @@ int guac_vnc_user_join_handler(guac_user* user, int argc, char** argv) { /* Only handle events if not read-only */ if (!settings->read_only) { - /* General mouse/keyboard/clipboard events */ - user->mouse_handler = guac_vnc_user_mouse_handler; - user->key_handler = guac_vnc_user_key_handler; - user->clipboard_handler = guac_vnc_clipboard_handler; + /* General mouse/keyboard events */ + user->mouse_handler = guac_vnc_user_mouse_handler; + user->key_handler = guac_vnc_user_key_handler; + + /* Inbound (client to server) clipboard transfer */ + if (!settings->disable_paste) + user->clipboard_handler = guac_vnc_clipboard_handler; #ifdef ENABLE_COMMON_SSH /* Set generic (non-filesystem) file upload handler */ diff --git a/src/protocols/vnc/vnc.c b/src/protocols/vnc/vnc.c index d9f9dbbb..f33b267a 100644 --- a/src/protocols/vnc/vnc.c +++ b/src/protocols/vnc/vnc.c @@ -55,6 +55,66 @@ char* GUAC_VNC_CLIENT_KEY = "GUAC_VNC"; +#ifdef ENABLE_VNC_TLS_LOCKING +/** + * A callback function that is called by the VNC library prior to writing + * data to a TLS-encrypted socket. This returns the rfbBool FALSE value + * if there's an error locking the mutex, or rfbBool TRUE otherwise. + * + * @param rfb_client + * The rfbClient for which to lock the TLS mutex. + * + * @returns + * rfbBool FALSE if an error occurs locking the mutex, otherwise + * TRUE. + */ +static rfbBool guac_vnc_lock_write_to_tls(rfbClient* rfb_client) { + + /* Retrieve the Guacamole data structures */ + guac_client* gc = rfbClientGetClientData(rfb_client, GUAC_VNC_CLIENT_KEY); + guac_vnc_client* vnc_client = (guac_vnc_client*) gc->data; + + /* Lock write access */ + int retval = pthread_mutex_lock(&(vnc_client->tls_lock)); + if (retval) { + guac_client_log(gc, GUAC_LOG_ERROR, "Error locking TLS write mutex: %s", + strerror(retval)); + return FALSE; + } + return TRUE; + +} + +/** + * A callback function for use by the VNC library that is called once + * the client is finished writing to a TLS-encrypted socket. A rfbBool + * FALSE value is returned if an error occurs unlocking the mutex, + * otherwise TRUE is returned. + * + * @param rfb_client + * The rfbClient for which to unlock the TLS mutex. + * + * @returns + * rfbBool FALSE if an error occurs unlocking the mutex, otherwise + * TRUE. + */ +static rfbBool guac_vnc_unlock_write_to_tls(rfbClient* rfb_client) { + + /* Retrieve the Guacamole data structures */ + guac_client* gc = rfbClientGetClientData(rfb_client, GUAC_VNC_CLIENT_KEY); + guac_vnc_client* vnc_client = (guac_vnc_client*) gc->data; + + /* Unlock write access */ + int retval = pthread_mutex_unlock(&(vnc_client->tls_lock)); + if (retval) { + guac_client_log(gc, GUAC_LOG_ERROR, "Error unlocking TLS write mutex: %s", + strerror(retval)); + return FALSE; + } + return TRUE; +} +#endif + rfbClient* guac_vnc_get_client(guac_client* client) { rfbClient* rfb_client = rfbGetClient(8, 3, 4); /* 32-bpp client */ @@ -68,6 +128,12 @@ rfbClient* guac_vnc_get_client(guac_client* client) { rfb_client->GotFrameBufferUpdate = guac_vnc_update; rfb_client->GotCopyRect = guac_vnc_copyrect; +#ifdef ENABLE_VNC_TLS_LOCKING + /* TLS Locking and Unlocking */ + rfb_client->LockWriteToTLS = guac_vnc_lock_write_to_tls; + rfb_client->UnlockWriteToTLS = guac_vnc_unlock_write_to_tls; +#endif + /* Do not handle clipboard and local cursor if read-only */ if (vnc_settings->read_only == 0) { @@ -87,6 +153,9 @@ rfbClient* guac_vnc_get_client(guac_client* client) { } + /* Authentication */ + rfb_client->GetCredential = guac_vnc_get_credentials; + /* Password */ rfb_client->GetPassword = guac_vnc_get_password; @@ -262,7 +331,7 @@ void* guac_vnc_client_thread(void* data) { vnc_client->sftp_session = guac_common_ssh_create_session(client, settings->sftp_hostname, settings->sftp_port, vnc_client->sftp_user, settings->sftp_server_alive_interval, - settings->sftp_host_key); + settings->sftp_host_key, NULL); /* Fail if SSH connection does not succeed */ if (vnc_client->sftp_session == NULL) { @@ -403,4 +472,3 @@ void* guac_vnc_client_thread(void* data) { return NULL; } - diff --git a/src/protocols/vnc/vnc.h b/src/protocols/vnc/vnc.h index ce2d20af..7c189cfb 100644 --- a/src/protocols/vnc/vnc.h +++ b/src/protocols/vnc/vnc.h @@ -55,6 +55,13 @@ typedef struct guac_vnc_client { */ pthread_t client_thread; +#ifdef ENABLE_VNC_TLS_LOCKING + /** + * The TLS mutex lock for the client. + */ + pthread_mutex_t tls_lock; +#endif + /** * The underlying VNC client. */ diff --git a/src/pulse/Makefile.am b/src/pulse/Makefile.am index b7921245..3cb2d44c 100644 --- a/src/pulse/Makefile.am +++ b/src/pulse/Makefile.am @@ -16,6 +16,12 @@ # specific language governing permissions and limitations # under the License. # +# NOTE: Parts of this file (Makefile.am) are automatically transcluded verbatim +# into Makefile.in. Though the build system (GNU Autotools) automatically adds +# its own license boilerplate to the generated Makefile.in, that boilerplate +# does not apply to the transcluded portions of Makefile.am which are licensed +# to you by the ASF under the Apache License, Version 2.0, as described above. +# AUTOMAKE_OPTIONS = foreign ACLOCAL_AMFLAGS = -I m4 diff --git a/src/terminal/Makefile.am b/src/terminal/Makefile.am index 1a609324..a886479e 100644 --- a/src/terminal/Makefile.am +++ b/src/terminal/Makefile.am @@ -16,6 +16,12 @@ # specific language governing permissions and limitations # under the License. # +# NOTE: Parts of this file (Makefile.am) are automatically transcluded verbatim +# into Makefile.in. Though the build system (GNU Autotools) automatically adds +# its own license boilerplate to the generated Makefile.in, that boilerplate +# does not apply to the transcluded portions of Makefile.am which are licensed +# to you by the ASF under the Apache License, Version 2.0, as described above. +# AUTOMAKE_OPTIONS = foreign ACLOCAL_AMFLAGS = -I m4 @@ -26,6 +32,7 @@ noinst_HEADERS = \ terminal/buffer.h \ terminal/char_mappings.h \ terminal/common.h \ + terminal/color-scheme.h \ terminal/display.h \ terminal/named-colors.h \ terminal/palette.h \ @@ -40,6 +47,7 @@ noinst_HEADERS = \ libguac_terminal_la_SOURCES = \ buffer.c \ char_mappings.c \ + color-scheme.c \ common.c \ display.c \ named-colors.c \ diff --git a/src/terminal/color-scheme.c b/src/terminal/color-scheme.c new file mode 100644 index 00000000..5df181fa --- /dev/null +++ b/src/terminal/color-scheme.c @@ -0,0 +1,273 @@ +/* + * 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 "config.h" + +#include "terminal/color-scheme.h" +#include "terminal/palette.h" +#include "terminal/xparsecolor.h" + +#include +#include +#include +#include + +#include + +/** + * Compare a non-null-terminated string to a null-terminated literal, in the + * same manner as strcmp(). + * + * @param str_start + * Start of the non-null-terminated string. + * + * @param str_end + * End of the non-null-terminated string, after the last character. + * + * @param literal + * The null-terminated literal to compare against. + * + * @return + * Zero if the two strings are equal and non-zero otherwise. + */ +static int guac_terminal_color_scheme_compare_token(const char* str_start, + const char* str_end, const char* literal) { + + const int result = strncmp(literal, str_start, str_end - str_start); + if (result != 0) + return result; + + /* At this point, literal is same length or longer than + * | str_end - str_start |, so if the two are equal, literal should + * have its null-terminator at | str_end - str_start |. */ + return (int) (unsigned char) literal[str_end - str_start]; +} + +/** + * Strip the leading and trailing spaces of a bounded string. + * + * @param[in,out] str_start + * Address of a pointer to the start of the string. On return, the pointer + * is advanced to after any leading spaces. + * + * @param[in,out] str_end + * Address of a pointer to the end of the string, after the last character. + * On return, the pointer is moved back to before any trailing spaces. + */ +static void guac_terminal_color_scheme_strip_spaces(const char** str_start, + const char** str_end) { + + /* Strip leading spaces. */ + while (*str_start < *str_end && isspace(**str_start)) + (*str_start)++; + + /* Strip trailing spaces. */ + while (*str_end > *str_start && isspace(*(*str_end - 1))) + (*str_end)--; +} + +/** + * Parse the name part of the name-value pair within the color-scheme + * configuration. + * + * @param client + * The client that the terminal is connected to. + * + * @param name_start + * Start of the name string. + * + * @param name_end + * End of the name string, after the last character. + * + * @param foreground + * Pointer to the foreground color. + * + * @param background + * Pointer to the background color. + * + * @param palette + * Pointer to the palette array. + * + * @param[out] target + * On return, pointer to the color struct that corresponds to the name. + * + * @return + * Zero if successful or non-zero otherwise. + */ +static int guac_terminal_parse_color_scheme_name(guac_client* client, + const char* name_start, const char* name_end, + guac_terminal_color* foreground, guac_terminal_color* background, + guac_terminal_color (*palette)[256], + guac_terminal_color** target) { + + guac_terminal_color_scheme_strip_spaces(&name_start, &name_end); + + if (!guac_terminal_color_scheme_compare_token( + name_start, name_end, GUAC_TERMINAL_SCHEME_FOREGROUND)) { + *target = foreground; + return 0; + } + + if (!guac_terminal_color_scheme_compare_token( + name_start, name_end, GUAC_TERMINAL_SCHEME_BACKGROUND)) { + *target = background; + return 0; + } + + /* Parse color value. */ + int index = -1; + if (sscanf(name_start, GUAC_TERMINAL_SCHEME_NUMBERED "%d", &index) && + index >= 0 && index <= 255) { + *target = &(*palette)[index]; + return 0; + } + + guac_client_log(client, GUAC_LOG_WARNING, + "Unknown color name: \"%.*s\".", + name_end - name_start, name_start); + return 1; +} + +/** + * Parse the value part of the name-value pair within the color-scheme + * configuration. + * + * @param client + * The client that the terminal is connected to. + * + * @param value_start + * Start of the value string. + * + * @param value_end + * End of the value string, after the last character. + * + * @param palette + * The current color palette. + * + * @param[out] target + * On return, the parsed color. + * + * @return + * Zero if successful or non-zero otherwise. + */ +static int guac_terminal_parse_color_scheme_value(guac_client* client, + const char* value_start, const char* value_end, + const guac_terminal_color (*palette)[256], + guac_terminal_color* target) { + + guac_terminal_color_scheme_strip_spaces(&value_start, &value_end); + + /* Parse color value. */ + int index = -1; + if (sscanf(value_start, GUAC_TERMINAL_SCHEME_NUMBERED "%d", &index) && + index >= 0 && index <= 255) { + *target = (*palette)[index]; + return 0; + } + + /* Parse X11 value. */ + if (!guac_terminal_xparsecolor(value_start, target)) + return 0; + + guac_client_log(client, GUAC_LOG_WARNING, + "Invalid color value: \"%.*s\".", + value_end - value_start, value_start); + return 1; +} + +void guac_terminal_parse_color_scheme(guac_client* client, + const char* color_scheme, guac_terminal_color* foreground, + guac_terminal_color* background, + guac_terminal_color (*palette)[256]) { + + /* Special cases. */ + if (color_scheme[0] == '\0') { + /* guac_terminal_parse_color_scheme defaults to gray-black */ + } + else if (strcmp(color_scheme, GUAC_TERMINAL_SCHEME_GRAY_BLACK) == 0) { + color_scheme = "foreground:color7;background:color0"; + } + else if (strcmp(color_scheme, GUAC_TERMINAL_SCHEME_BLACK_WHITE) == 0) { + color_scheme = "foreground:color0;background:color15"; + } + else if (strcmp(color_scheme, GUAC_TERMINAL_SCHEME_GREEN_BLACK) == 0) { + color_scheme = "foreground:color2;background:color0"; + } + else if (strcmp(color_scheme, GUAC_TERMINAL_SCHEME_WHITE_BLACK) == 0) { + color_scheme = "foreground:color15;background:color0"; + } + + /* Set default gray-black color scheme and initial palette. */ + *foreground = GUAC_TERMINAL_INITIAL_PALETTE[GUAC_TERMINAL_COLOR_GRAY]; + *background = GUAC_TERMINAL_INITIAL_PALETTE[GUAC_TERMINAL_COLOR_BLACK]; + memcpy(palette, GUAC_TERMINAL_INITIAL_PALETTE, + sizeof(GUAC_TERMINAL_INITIAL_PALETTE)); + + /* Current char being parsed, or NULL if at end of parsing. */ + const char* cursor = color_scheme; + + while (cursor) { + /* Start of the current "name: value" pair. */ + const char* pair_start = cursor; + + /* End of the current name-value pair. */ + const char* pair_end = strchr(pair_start, ';'); + if (pair_end) { + cursor = pair_end + 1; + } + else { + pair_end = pair_start + strlen(pair_start); + cursor = NULL; + } + + guac_terminal_color_scheme_strip_spaces(&pair_start, &pair_end); + if (pair_start >= pair_end) + /* Allow empty pairs, which happens, e.g., when the configuration + * string ends in a semi-colon. */ + continue; + + /* End of the name part of the pair. */ + const char* name_end = memchr(pair_start, ':', pair_end - pair_start); + if (name_end == NULL) { + guac_client_log(client, GUAC_LOG_WARNING, + "Expecting colon: \"%.*s\".", + pair_end - pair_start, pair_start); + return; + } + + /* The color that the name corresponds to. */ + guac_terminal_color* color_target = NULL; + + if (guac_terminal_parse_color_scheme_name( + client, pair_start, name_end, foreground, background, + palette, &color_target)) + return; /* Parsing failed. */ + + if (guac_terminal_parse_color_scheme_value( + client, name_end + 1, pair_end, + (const guac_terminal_color(*)[256]) palette, color_target)) + return; /* Parsing failed. */ + } + + /* Persist pseudo-index for foreground/background colors */ + foreground->palette_index = GUAC_TERMINAL_COLOR_FOREGROUND; + background->palette_index = GUAC_TERMINAL_COLOR_BACKGROUND; + +} + diff --git a/src/terminal/display.c b/src/terminal/display.c index c4c74713..d148b411 100644 --- a/src/terminal/display.c +++ b/src/terminal/display.c @@ -78,7 +78,12 @@ int __guac_terminal_set_colors(guac_terminal_display* display, } display->glyph_foreground = *foreground; + guac_terminal_display_lookup_color(display, + foreground->palette_index, &display->glyph_foreground); + display->glyph_background = *background; + guac_terminal_display_lookup_color(display, + background->palette_index, &display->glyph_background); /* Modify color if half-bright (low intensity) */ if (attributes->half_bright && !attributes->bold) { @@ -199,17 +204,17 @@ int __guac_terminal_set(guac_terminal_display* display, int row, int col, int co guac_terminal_display* guac_terminal_display_alloc(guac_client* client, const char* font_name, int font_size, int dpi, guac_terminal_color* foreground, guac_terminal_color* background, - const guac_terminal_color (*palette)[256]) { - - PangoFontMap* font_map; - PangoFont* font; - PangoFontMetrics* metrics; - PangoContext* context; + guac_terminal_color (*palette)[256]) { /* Allocate display */ guac_terminal_display* display = malloc(sizeof(guac_terminal_display)); display->client = client; + /* Initially no font loaded */ + display->font_desc = NULL; + display->char_width = 0; + display->char_height = 0; + /* Create default surface */ display->display_layer = guac_client_alloc_layer(client); display->select_layer = guac_client_alloc_layer(client); @@ -220,42 +225,10 @@ guac_terminal_display* guac_terminal_display_alloc(guac_client* client, guac_protocol_send_move(client->socket, display->select_layer, display->display_layer, 0, 0, 0); - /* Get font */ - display->font_desc = pango_font_description_new(); - pango_font_description_set_family(display->font_desc, font_name); - pango_font_description_set_weight(display->font_desc, PANGO_WEIGHT_NORMAL); - pango_font_description_set_size(display->font_desc, - font_size * PANGO_SCALE * dpi / 96); - - font_map = pango_cairo_font_map_get_default(); - context = pango_font_map_create_context(font_map); - - font = pango_font_map_load_font(font_map, context, display->font_desc); - if (font == NULL) { - guac_client_abort(display->client, GUAC_PROTOCOL_STATUS_SERVER_ERROR, "Unable to get font \"%s\"", font_name); - free(display); - return NULL; - } - - metrics = pango_font_get_metrics(font, NULL); - if (metrics == NULL) { - guac_client_abort(display->client, GUAC_PROTOCOL_STATUS_SERVER_ERROR, - "Unable to get font metrics for font \"%s\"", font_name); - free(display); - return NULL; - } - display->default_foreground = display->glyph_foreground = *foreground; display->default_background = display->glyph_background = *background; display->default_palette = palette; - /* Calculate character dimensions */ - display->char_width = - pango_font_metrics_get_approximate_digit_width(metrics) / PANGO_SCALE; - display->char_height = - (pango_font_metrics_get_descent(metrics) - + pango_font_metrics_get_ascent(metrics)) / PANGO_SCALE; - /* Initially empty */ display->width = 0; display->height = 0; @@ -264,14 +237,25 @@ guac_terminal_display* guac_terminal_display_alloc(guac_client* client, /* Initially nothing selected */ display->text_selected = false; + /* Attempt to load font */ + if (guac_terminal_display_set_font(display, font_name, font_size, dpi)) { + guac_client_abort(display->client, GUAC_PROTOCOL_STATUS_SERVER_ERROR, + "Unable to set initial font \"%s\"", font_name); + free(display); + return NULL; + } + return display; } void guac_terminal_display_free(guac_terminal_display* display) { + /* Free font description */ + pango_font_description_free(display->font_desc); + /* Free default palette. */ - free((void*) display->default_palette); + free(display->default_palette); /* Free operations buffers */ free(display->operations); @@ -315,6 +299,18 @@ int guac_terminal_display_assign_color(guac_terminal_display* display, int guac_terminal_display_lookup_color(guac_terminal_display* display, int index, guac_terminal_color* color) { + /* Use default foreground if foreground pseudo-index is given */ + if (index == GUAC_TERMINAL_COLOR_FOREGROUND) { + *color = display->default_foreground; + return 0; + } + + /* Use default background if background pseudo-index is given */ + if (index == GUAC_TERMINAL_COLOR_BACKGROUND) { + *color = display->default_background; + return 0; + } + /* Lookup fails if out-of-bounds */ if (index < 0 || index > 255) return 1; @@ -658,11 +654,15 @@ void __guac_terminal_display_flush_clear(guac_terminal_display* display) { int rect_width, rect_height; /* Color of the rectangle to draw */ - const guac_terminal_color* color; + guac_terminal_color color; if (current->character.attributes.reverse != current->character.attributes.cursor) - color = ¤t->character.attributes.foreground; + color = current->character.attributes.foreground; else - color = ¤t->character.attributes.background; + color = current->character.attributes.background; + + /* Rely only on palette index if defined */ + guac_terminal_display_lookup_color(display, + color.palette_index, &color); /* Current row within a subrect */ guac_terminal_operation* rect_current_row; @@ -685,7 +685,7 @@ void __guac_terminal_display_flush_clear(guac_terminal_display* display) { /* If not identical operation, stop */ if (rect_current->type != GUAC_CHAR_SET || guac_terminal_has_glyph(rect_current->character.value) - || guac_terminal_colorcmp(joining_color, color) != 0) + || guac_terminal_colorcmp(joining_color, &color) != 0) break; /* Next column */ @@ -730,7 +730,7 @@ void __guac_terminal_display_flush_clear(guac_terminal_display* display) { /* Mark clear operations as NOP */ if (rect_current->type == GUAC_CHAR_SET && !guac_terminal_has_glyph(rect_current->character.value) - && guac_terminal_colorcmp(joining_color, color) == 0) + && guac_terminal_colorcmp(joining_color, &color) == 0) rect_current->type = GUAC_CHAR_NOP; /* Next column */ @@ -750,7 +750,7 @@ void __guac_terminal_display_flush_clear(guac_terminal_display* display) { row * display->char_height, rect_width * display->char_width, rect_height * display->char_height, - color->red, color->green, color->blue, + color.red, color.green, color.blue, 0xFF); } /* end if clear operation */ @@ -945,4 +945,78 @@ void guac_terminal_display_clear_select(guac_terminal_display* display) { } +int guac_terminal_display_set_font(guac_terminal_display* display, + const char* font_name, int font_size, int dpi) { + + PangoFontDescription* font_desc; + + /* Build off existing font description if possible */ + if (display->font_desc != NULL) + font_desc = pango_font_description_copy(display->font_desc); + + /* Create new font description if there is nothing to copy */ + else { + font_desc = pango_font_description_new(); + pango_font_description_set_weight(font_desc, PANGO_WEIGHT_NORMAL); + } + + /* Optionally update font name */ + if (font_name != NULL) + pango_font_description_set_family(font_desc, font_name); + + /* Optionally update size */ + if (font_size != -1) { + pango_font_description_set_size(font_desc, + font_size * PANGO_SCALE * dpi / 96); + } + + PangoFontMap* font_map = pango_cairo_font_map_get_default(); + PangoContext* context = pango_font_map_create_context(font_map); + + /* Load font from font map */ + PangoFont* font = pango_font_map_load_font(font_map, context, font_desc); + if (font == NULL) { + guac_client_log(display->client, GUAC_LOG_INFO, "Unable to load " + "font \"%s\"", pango_font_description_get_family(font_desc)); + pango_font_description_free(font_desc); + return 1; + } + + /* Get metrics from loaded font */ + PangoFontMetrics* metrics = pango_font_get_metrics(font, NULL); + if (metrics == NULL) { + guac_client_log(display->client, GUAC_LOG_INFO, "Unable to get font " + "metrics for font \"%s\"", + pango_font_description_get_family(font_desc)); + pango_font_description_free(font_desc); + return 1; + } + + /* Save effective size of current display */ + int pixel_width = display->width * display->char_width; + int pixel_height = display->height * display->char_height; + + /* Calculate character dimensions using metrics */ + display->char_width = + pango_font_metrics_get_approximate_digit_width(metrics) / PANGO_SCALE; + display->char_height = + (pango_font_metrics_get_descent(metrics) + + pango_font_metrics_get_ascent(metrics)) / PANGO_SCALE; + + /* Atomically replace old font description */ + PangoFontDescription* old_font_desc = display->font_desc; + display->font_desc = font_desc; + pango_font_description_free(old_font_desc); + + /* Recalculate dimensions which will fit within current surface */ + int new_width = pixel_width / display->char_width; + int new_height = pixel_height / display->char_height; + + /* Resize display if dimensions have changed */ + if (new_width != display->width || new_height != display->height) + guac_terminal_display_resize(display, new_width, new_height); + + return 0; + +} diff --git a/src/terminal/named-colors.c b/src/terminal/named-colors.c index b0e579b6..ab94153e 100644 --- a/src/terminal/named-colors.c +++ b/src/terminal/named-colors.c @@ -761,8 +761,13 @@ static int guac_terminal_named_color_search(const void* a, const void* b) { /* Skip any spaces in key (name will never have spaces) */ while (*key && isspace(*key)) key++; + /* Treat semi-colon as string terminator, to support parsing color + names within a larger string (e.g. within the terminal color-scheme + configuration string). */ + const int keyChar = (*key == ';') ? '\0' : tolower(*key); + /* Compare, ignoring case (name is already known to be lowercase) */ - int difference = tolower(*key) - *name; + int difference = keyChar - *name; if (difference) return difference; diff --git a/src/terminal/palette.c b/src/terminal/palette.c index cdbe4156..a6b2d5ae 100644 --- a/src/terminal/palette.c +++ b/src/terminal/palette.c @@ -289,6 +289,10 @@ const guac_terminal_color GUAC_TERMINAL_INITIAL_PALETTE[256] = { int guac_terminal_colorcmp(const guac_terminal_color* a, const guac_terminal_color* b) { + /* Compare palette index alone if not unknown */ + if (a->palette_index != -1 && b->palette_index != -1) + return a->palette_index - b->palette_index; + /* Consider red component highest order ... */ if (a->red != b->red) return a->red - b->red; diff --git a/src/terminal/select.c b/src/terminal/select.c index 20fc3cda..0a075ebd 100644 --- a/src/terminal/select.c +++ b/src/terminal/select.c @@ -375,8 +375,10 @@ void guac_terminal_select_end(guac_terminal* terminal) { } /* Send data */ - guac_common_clipboard_send(terminal->clipboard, client); - guac_socket_flush(socket); + if (!terminal->disable_copy) { + guac_common_clipboard_send(terminal->clipboard, client); + guac_socket_flush(socket); + } guac_terminal_notify(terminal); diff --git a/src/terminal/terminal-stdin-stream.c b/src/terminal/terminal-stdin-stream.c index 1f662d67..02f5b943 100644 --- a/src/terminal/terminal-stdin-stream.c +++ b/src/terminal/terminal-stdin-stream.c @@ -103,6 +103,22 @@ static int guac_terminal_input_stream_end_handler(guac_user* user, static int __guac_terminal_send_stream(guac_terminal* term, guac_user* user, guac_stream* stream) { + /* Deny redirecting STDIN if terminal is not started */ + if (!term->started) { + + guac_user_log(user, GUAC_LOG_DEBUG, "Attempt to direct the contents " + "of an inbound stream to STDIN denied. The terminal is not " + "yet ready for input."); + + guac_protocol_send_ack(user->socket, stream, + "Terminal not yet started.", + GUAC_PROTOCOL_STATUS_RESOURCE_CONFLICT); + + guac_socket_flush(user->socket); + return 1; + + } + /* If a stream is already being used for STDIN, deny creation of * further streams */ if (term->input_stream != NULL) { diff --git a/src/terminal/terminal.c b/src/terminal/terminal.c index 760e128f..b76a6ea8 100644 --- a/src/terminal/terminal.c +++ b/src/terminal/terminal.c @@ -22,6 +22,7 @@ #include "common/clipboard.h" #include "common/cursor.h" #include "terminal/buffer.h" +#include "terminal/color-scheme.h" #include "terminal/common.h" #include "terminal/display.h" #include "terminal/palette.h" @@ -30,7 +31,6 @@ #include "terminal/terminal_handlers.h" #include "terminal/types.h" #include "terminal/typescript.h" -#include "terminal/xparsecolor.h" #include #include @@ -305,250 +305,9 @@ void* guac_terminal_thread(void* data) { } -/** - * Compare a non-null-terminated string to a null-terminated literal, in the - * same manner as strcmp(). - * - * @param str_start - * Start of the non-null-terminated string. - * - * @param str_end - * End of the non-null-terminated string, after the last character. - * - * @param literal - * The null-terminated literal to compare against. - * - * @return - * Zero if the two strings are equal and non-zero otherwise. - */ -static int guac_terminal_color_scheme_compare_token(const char* str_start, - const char* str_end, const char* literal) { - - const int result = strncmp(literal, str_start, str_end - str_start); - if (result != 0) - return result; - - /* At this point, literal is same length or longer than - * | str_end - str_start |, so if the two are equal, literal should - * have its null-terminator at | str_end - str_start |. */ - return (int) (unsigned char) literal[str_end - str_start]; -} - -/** - * Strip the leading and trailing spaces of a bounded string. - * - * @param[in,out] str_start - * Address of a pointer to the start of the string. On return, the pointer - * is advanced to after any leading spaces. - * - * @param[in,out] str_end - * Address of a pointer to the end of the string, after the last character. - * On return, the pointer is moved back to before any trailing spaces. - */ -static void guac_terminal_color_scheme_strip_spaces(const char** str_start, - const char** str_end) { - - /* Strip leading spaces. */ - while (*str_start < *str_end && isspace(**str_start)) - (*str_start)++; - - /* Strip trailing spaces. */ - while (*str_end > *str_start && isspace(*(*str_end - 1))) - (*str_end)--; -} - -/** - * Parse the name part of the name-value pair within the color-scheme - * configuration. - * - * @param client - * The client that the terminal is connected to. - * - * @param name_start - * Start of the name string. - * - * @param name_end - * End of the name string, after the last character. - * - * @param foreground - * Pointer to the foreground color. - * - * @param background - * Pointer to the background color. - * - * @param palette - * Pointer to the palette array. - * - * @param[out] target - * On return, pointer to the color struct that corresponds to the name. - * - * @return - * Zero if successful or non-zero otherwise. - */ -static int guac_terminal_parse_color_scheme_name(guac_client* client, - const char* name_start, const char* name_end, - guac_terminal_color* foreground, guac_terminal_color* background, - guac_terminal_color (*palette)[256], - guac_terminal_color** target) { - - guac_terminal_color_scheme_strip_spaces(&name_start, &name_end); - - if (!guac_terminal_color_scheme_compare_token( - name_start, name_end, GUAC_TERMINAL_SCHEME_FOREGROUND)) { - *target = foreground; - return 0; - } - - if (!guac_terminal_color_scheme_compare_token( - name_start, name_end, GUAC_TERMINAL_SCHEME_BACKGROUND)) { - *target = background; - return 0; - } - - /* Parse color value. */ - int index = -1; - if (sscanf(name_start, GUAC_TERMINAL_SCHEME_NUMBERED "%d", &index) && - index >= 0 && index <= 255) { - *target = &(*palette)[index]; - return 0; - } - - guac_client_log(client, GUAC_LOG_WARNING, - "Unknown color name: \"%.*s\".", - name_end - name_start, name_start); - return 1; -} - -/** - * Parse the value part of the name-value pair within the color-scheme - * configuration. - * - * @param client - * The client that the terminal is connected to. - * - * @param value_start - * Start of the value string. - * - * @param value_end - * End of the value string, after the last character. - * - * @param palette - * The current color palette. - * - * @param[out] target - * On return, the parsed color. - * - * @return - * Zero if successful or non-zero otherwise. - */ -static int guac_terminal_parse_color_scheme_value(guac_client* client, - const char* value_start, const char* value_end, - const guac_terminal_color (*palette)[256], - guac_terminal_color* target) { - - guac_terminal_color_scheme_strip_spaces(&value_start, &value_end); - - /* Parse color value. */ - int index = -1; - if (sscanf(value_start, GUAC_TERMINAL_SCHEME_NUMBERED "%d", &index) && - index >= 0 && index <= 255) { - *target = (*palette)[index]; - return 0; - } - - /* Parse X11 value. */ - if (!guac_terminal_xparsecolor(value_start, target)) - return 0; - - guac_client_log(client, GUAC_LOG_WARNING, - "Invalid color value: \"%.*s\".", - value_end - value_start, value_start); - return 1; -} - -/** - * Parse a color-scheme configuration string, and return specified - * foreground/background colors and color palette. - * - * @param client - * The client that the terminal is connected to. - * - * @param color_scheme - * A semicolon-separated list of name-value pairs, i.e. - * ": [; : [; ...]]". - * For example, "color2: rgb:cc/33/22; background: color5". - * - * @param[out] foreground - * Parsed foreground color. - * - * @param[out] background - * Parsed background color. - * - * @param[in,out] palette - * Parsed color palette. The caller is responsible for allocating a mutable - * array on entry. On return, the array contains the parsed palette. - */ -static void guac_terminal_parse_color_scheme(guac_client* client, - const char* color_scheme, guac_terminal_color* foreground, - guac_terminal_color* background, - guac_terminal_color (*palette)[256]) { - - /* Set default gray-black color scheme and initial palette. */ - *foreground = GUAC_TERMINAL_INITIAL_PALETTE[GUAC_TERMINAL_COLOR_GRAY]; - *background = GUAC_TERMINAL_INITIAL_PALETTE[GUAC_TERMINAL_COLOR_BLACK]; - memcpy(palette, GUAC_TERMINAL_INITIAL_PALETTE, - sizeof(GUAC_TERMINAL_INITIAL_PALETTE)); - - /* Current char being parsed, or NULL if at end of parsing. */ - const char* cursor = color_scheme; - - while (cursor) { - /* Start of the current "name: value" pair. */ - const char* pair_start = cursor; - - /* End of the current name-value pair. */ - const char* pair_end = strchr(pair_start, ';'); - if (pair_end) { - cursor = pair_end + 1; - } - else { - pair_end = pair_start + strlen(pair_start); - cursor = NULL; - } - - guac_terminal_color_scheme_strip_spaces(&pair_start, &pair_end); - if (pair_start >= pair_end) - /* Allow empty pairs, which happens, e.g., when the configuration - * string ends in a semi-colon. */ - continue; - - /* End of the name part of the pair. */ - const char* name_end = memchr(pair_start, ':', pair_end - pair_start); - if (name_end == NULL) { - guac_client_log(client, GUAC_LOG_WARNING, - "Expecting colon: \"%.*s\".", - pair_end - pair_start, pair_start); - return; - } - - /* The color that the name corresponds to. */ - guac_terminal_color* color_target = NULL; - - if (guac_terminal_parse_color_scheme_name( - client, pair_start, name_end, foreground, background, - palette, &color_target)) - return; /* Parsing failed. */ - - if (guac_terminal_parse_color_scheme_value( - client, name_end + 1, pair_end, - (const guac_terminal_color(*)[256]) palette, color_target)) - return; /* Parsing failed. */ - } -} - guac_terminal* guac_terminal_create(guac_client* client, - guac_common_clipboard* clipboard, int max_scrollback, - const char* font_name, int font_size, int dpi, + guac_common_clipboard* clipboard, bool disable_copy, + int max_scrollback, const char* font_name, int font_size, int dpi, int width, int height, const char* color_scheme, const int backspace) { @@ -568,23 +327,6 @@ guac_terminal* guac_terminal_create(guac_client* client, guac_terminal_color (*default_palette)[256] = (guac_terminal_color(*)[256]) malloc(sizeof(guac_terminal_color[256])); - /* Special cases. */ - if (color_scheme == NULL || color_scheme[0] == '\0') { - /* guac_terminal_parse_color_scheme defaults to gray-black */ - } - else if (strcmp(color_scheme, GUAC_TERMINAL_SCHEME_GRAY_BLACK) == 0) { - color_scheme = "foreground:color7;background:color0"; - } - else if (strcmp(color_scheme, GUAC_TERMINAL_SCHEME_BLACK_WHITE) == 0) { - color_scheme = "foreground:color0;background:color15"; - } - else if (strcmp(color_scheme, GUAC_TERMINAL_SCHEME_GREEN_BLACK) == 0) { - color_scheme = "foreground:color2;background:color0"; - } - else if (strcmp(color_scheme, GUAC_TERMINAL_SCHEME_WHITE_BLACK) == 0) { - color_scheme = "foreground:color15;background:color0"; - } - guac_terminal_parse_color_scheme(client, color_scheme, &default_char.attributes.foreground, &default_char.attributes.background, @@ -596,10 +338,20 @@ guac_terminal* guac_terminal_create(guac_client* client, available_width = 0; guac_terminal* term = malloc(sizeof(guac_terminal)); + term->started = false; term->client = client; term->upload_path_handler = NULL; term->file_download_handler = NULL; + /* Copy initially-provided color scheme and font details */ + term->color_scheme = strdup(color_scheme); + term->font_name = strdup(font_name); + term->font_size = font_size; + + /* Set size of available screen area */ + term->outer_width = width; + term->outer_height = height; + /* Init modified flag and conditional */ term->modified = 0; pthread_cond_init(&(term->modified_cond), NULL); @@ -624,7 +376,7 @@ guac_terminal* guac_terminal_create(guac_client* client, font_name, font_size, dpi, &default_char.attributes.foreground, &default_char.attributes.background, - (const guac_terminal_color(*)[256]) default_palette); + (guac_terminal_color(*)[256]) default_palette); /* Fail if display init failed */ if (term->display == NULL) { @@ -640,6 +392,7 @@ guac_terminal* guac_terminal_create(guac_client* client, term->current_attributes = default_char.attributes; term->default_char = default_char; term->clipboard = clipboard; + term->disable_copy = disable_copy; /* Calculate character size */ int rows = height / term->display->char_height; @@ -723,6 +476,11 @@ guac_terminal* guac_terminal_create(guac_client* client, } +void guac_terminal_start(guac_terminal* term) { + term->started = true; + guac_terminal_notify(term); +} + void guac_terminal_stop(guac_terminal* term) { /* Close input pipe and set fds to invalid */ @@ -759,6 +517,10 @@ void guac_terminal_free(guac_terminal* term) { /* Free scrollbar */ guac_terminal_scrollbar_free(term->scrollbar); + /* Free copies of font and color scheme information */ + free((char*) term->color_scheme); + free((char*) term->font_name); + /* Free the terminal itself */ free(term); @@ -851,11 +613,13 @@ wait_complete: int guac_terminal_render_frame(guac_terminal* terminal) { + guac_client* client = terminal->client; + int wait_result; /* Wait for data to be available */ wait_result = guac_terminal_wait(terminal, 1000); - if (wait_result) { + if (wait_result || !terminal->started) { guac_timestamp frame_start = guac_timestamp_current(); @@ -867,13 +631,14 @@ int guac_terminal_render_frame(guac_terminal* terminal) { - frame_end; /* Wait again if frame remaining */ - if (frame_remaining > 0) + if (frame_remaining > 0 || !terminal->started) wait_result = guac_terminal_wait(terminal, GUAC_TERMINAL_FRAME_TIMEOUT); else break; - } while (wait_result > 0); + } while (client->state == GUAC_CLIENT_RUNNING + && (wait_result > 0 || !terminal->started)); /* Flush terminal */ guac_terminal_lock(terminal); @@ -934,6 +699,9 @@ char* guac_terminal_prompt(guac_terminal* terminal, const char* title, int pos; char in_byte; + /* Prompting implicitly requires user input */ + guac_terminal_start(terminal); + /* Print title */ guac_terminal_printf(terminal, "%s", title); @@ -1567,6 +1335,10 @@ int guac_terminal_resize(guac_terminal* terminal, int width, int height) { /* Acquire exclusive access to terminal */ guac_terminal_lock(terminal); + /* Set size of available screen area */ + terminal->outer_width = width; + terminal->outer_height = height; + /* Calculate available display area */ int available_width = width - GUAC_TERMINAL_SCROLLBAR_WIDTH; if (available_width < 0) @@ -1672,6 +1444,13 @@ int guac_terminal_send_string(guac_terminal* term, const char* data) { static int __guac_terminal_send_key(guac_terminal* term, int keysym, int pressed) { + /* Ignore user input if terminal is not started */ + if (!term->started) { + guac_client_log(term->client, GUAC_LOG_DEBUG, "Ignoring user input " + "while terminal has not yet started."); + return 0; + } + /* Hide mouse cursor if not already hidden */ if (term->current_cursor != GUAC_TERMINAL_CURSOR_BLANK) { term->current_cursor = GUAC_TERMINAL_CURSOR_BLANK; @@ -1845,6 +1624,13 @@ int guac_terminal_send_key(guac_terminal* term, int keysym, int pressed) { static int __guac_terminal_send_mouse(guac_terminal* term, guac_user* user, int x, int y, int mask) { + /* Ignore user input if terminal is not started */ + if (!term->started) { + guac_client_log(term->client, GUAC_LOG_DEBUG, "Ignoring user input " + "while terminal has not yet started."); + return 0; + } + /* Determine which buttons were just released and pressed */ int released_mask = term->mouse_mask & ~mask; int pressed_mask = ~term->mouse_mask & mask; @@ -2155,3 +1941,79 @@ void guac_terminal_dup(guac_terminal* term, guac_user* user, } +void guac_terminal_apply_color_scheme(guac_terminal* terminal, + const char* color_scheme) { + + guac_client* client = terminal->client; + guac_terminal_char* default_char = &terminal->default_char; + guac_terminal_display* display = terminal->display; + + /* Reinitialize default terminal colors with values from color scheme */ + guac_terminal_parse_color_scheme(client, color_scheme, + &default_char->attributes.foreground, + &default_char->attributes.background, + display->default_palette); + + /* Reinitialize default attributes of buffer and display */ + guac_terminal_display_reset_palette(display); + display->default_foreground = default_char->attributes.foreground; + display->default_background = default_char->attributes.background; + + /* Redraw terminal text and background */ + guac_terminal_repaint_default_layer(terminal, client->socket); + __guac_terminal_redraw_rect(terminal, 0, 0, + terminal->term_height - 1, + terminal->term_width - 1); + + /* Acquire exclusive access to terminal */ + guac_terminal_lock(terminal); + + /* Update stored copy of color scheme */ + free((char*) terminal->color_scheme); + terminal->color_scheme = strdup(color_scheme); + + /* Release terminal */ + guac_terminal_unlock(terminal); + + guac_terminal_notify(terminal); + +} + +void guac_terminal_apply_font(guac_terminal* terminal, const char* font_name, + int font_size, int dpi) { + + guac_client* client = terminal->client; + guac_terminal_display* display = terminal->display; + + if (guac_terminal_display_set_font(display, font_name, font_size, dpi)) + return; + + /* Resize terminal to fit available region, now that font metrics may be + * different */ + guac_terminal_resize(terminal, terminal->outer_width, + terminal->outer_height); + + /* Redraw terminal text and background */ + guac_terminal_repaint_default_layer(terminal, client->socket); + __guac_terminal_redraw_rect(terminal, 0, 0, + terminal->term_height - 1, + terminal->term_width - 1); + + /* Acquire exclusive access to terminal */ + guac_terminal_lock(terminal); + + /* Update stored copy of font name, if changed */ + if (font_name != NULL) + terminal->font_name = strdup(font_name); + + /* Update stored copy of font size, if changed */ + if (font_size != -1) + terminal->font_size = font_size; + + /* Release terminal */ + guac_terminal_unlock(terminal); + + guac_terminal_notify(terminal); + +} + diff --git a/src/terminal/terminal/color-scheme.h b/src/terminal/terminal/color-scheme.h new file mode 100644 index 00000000..5417ce1c --- /dev/null +++ b/src/terminal/terminal/color-scheme.h @@ -0,0 +1,101 @@ +/* + * 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. + */ + +#ifndef GUAC_TERMINAL_COLOR_SCHEME_H +#define GUAC_TERMINAL_COLOR_SCHEME_H + +#include "config.h" + +#include "terminal/palette.h" + +#include + +#include +#include + +/** + * The name of the color scheme having black foreground and white background. + */ +#define GUAC_TERMINAL_SCHEME_BLACK_WHITE "black-white" + +/** + * The name of the color scheme having gray foreground and black background. + */ +#define GUAC_TERMINAL_SCHEME_GRAY_BLACK "gray-black" + +/** + * The name of the color scheme having green foreground and black background. + */ +#define GUAC_TERMINAL_SCHEME_GREEN_BLACK "green-black" + +/** + * The name of the color scheme having white foreground and black background. + */ +#define GUAC_TERMINAL_SCHEME_WHITE_BLACK "white-black" + +/** + * Color name representing the foreground color. + */ +#define GUAC_TERMINAL_SCHEME_FOREGROUND "foreground" + +/** + * Color name representing the background color. + */ +#define GUAC_TERMINAL_SCHEME_BACKGROUND "background" + +/** + * Color name representing a numbered color. + */ +#define GUAC_TERMINAL_SCHEME_NUMBERED "color" + +/** + * Parse a color-scheme configuration string, and return specified + * foreground/background colors and color palette. + * + * @param client + * The client that the terminal is connected to. + * + * @param color_scheme + * The name of a pre-defined color scheme (one of the + * names defined by the GUAC_TERMINAL_SCHEME_* constants), or + * semicolon-separated list of name-value pairs, i.e. ": [; + * : [; ...]]". For example, "color2: rgb:cc/33/22; + * background: color5". + * + * If blank, the default scheme of GUAC_TERMINAL_SCHEME_GRAY_BLACK will be + * used. If invalid, a warning will be logged, and + * GUAC_TERMINAL_SCHEME_GRAY_BLACK will be used. + * + * @param[out] foreground + * Parsed foreground color. + * + * @param[out] background + * Parsed background color. + * + * @param[in,out] palette + * Parsed color palette. The caller is responsible for allocating a mutable + * array on entry. On return, the array contains the parsed palette. + */ +void guac_terminal_parse_color_scheme(guac_client* client, + const char* color_scheme, guac_terminal_color* foreground, + guac_terminal_color* background, + guac_terminal_color (*palette)[256]); + +#endif + diff --git a/src/terminal/terminal/display.h b/src/terminal/terminal/display.h index e54003bb..5377cb14 100644 --- a/src/terminal/terminal/display.h +++ b/src/terminal/terminal/display.h @@ -142,7 +142,7 @@ typedef struct guac_terminal_display { * The default palette. Use GUAC_TERMINAL_INITIAL_PALETTE if null. * Must free on destruction if not null. */ - const guac_terminal_color (*default_palette)[256]; + guac_terminal_color (*default_palette)[256]; /** * Default foreground color for all glyphs. @@ -215,7 +215,7 @@ typedef struct guac_terminal_display { guac_terminal_display* guac_terminal_display_alloc(guac_client* client, const char* font_name, int font_size, int dpi, guac_terminal_color* foreground, guac_terminal_color* background, - const guac_terminal_color (*palette)[256]); + guac_terminal_color (*palette)[256]); /** * Frees the given display. @@ -334,5 +334,35 @@ void guac_terminal_display_select(guac_terminal_display* display, */ void guac_terminal_display_clear_select(guac_terminal_display* display); +/** + * Alters the font of the terminal display. The available display area and the + * regular grid of character cells will be resized as necessary to compensate + * for any changes in font metrics. + * + * If successful, the terminal itself MUST be manually resized to take into + * account the new character dimensions, and MUST be manually redrawn. Failing + * to do so will result in graphical artifacts. + * + * @param display + * The display whose font family and/or size are being changed. + * + * @param font_name + * The name of the new font family, or NULL if the font family should + * remain unchanged. + * + * @param font_size + * The new font size, in points, or -1 if the font size should remain + * unchanged. + * + * @param dpi + * The resolution of the display in DPI. If the font size will not be + * changed (the font size given is -1), this value is ignored. + * + * @return + * Zero if the font was successfully changed, non-zero otherwise. + */ +int guac_terminal_display_set_font(guac_terminal_display* display, + const char* font_name, int font_size, int dpi); + #endif diff --git a/src/terminal/terminal/palette.h b/src/terminal/terminal/palette.h index 7f467258..b1524b5a 100644 --- a/src/terminal/terminal/palette.h +++ b/src/terminal/terminal/palette.h @@ -24,6 +24,20 @@ #include +/** + * The pseudo-index of the color set as the the default foreground color for + * the terminal. Regardless of what changes are made to the palette, this index + * will always return the current default foreground color. + */ +#define GUAC_TERMINAL_COLOR_FOREGROUND -2 + +/** + * The pseudo-index of the color set as the the default background color for + * the terminal. Regardless of what changes are made to the palette, this index + * will always return the current default background color. + */ +#define GUAC_TERMINAL_COLOR_BACKGROUND -3 + /** * The index of black within the terminal color palette. */ diff --git a/src/terminal/terminal/terminal.h b/src/terminal/terminal/terminal.h index bddf8e55..4185837f 100644 --- a/src/terminal/terminal/terminal.h +++ b/src/terminal/terminal/terminal.h @@ -69,41 +69,6 @@ */ #define GUAC_TERMINAL_WHEEL_SCROLL_AMOUNT 3 -/** - * The name of the color scheme having black foreground and white background. - */ -#define GUAC_TERMINAL_SCHEME_BLACK_WHITE "black-white" - -/** - * The name of the color scheme having gray foreground and black background. - */ -#define GUAC_TERMINAL_SCHEME_GRAY_BLACK "gray-black" - -/** - * The name of the color scheme having green foreground and black background. - */ -#define GUAC_TERMINAL_SCHEME_GREEN_BLACK "green-black" - -/** - * The name of the color scheme having white foreground and black background. - */ -#define GUAC_TERMINAL_SCHEME_WHITE_BLACK "white-black" - -/** - * Color name representing the foreground color. - */ -#define GUAC_TERMINAL_SCHEME_FOREGROUND "foreground" - -/** - * Color name representing the background color. - */ -#define GUAC_TERMINAL_SCHEME_BACKGROUND "background" - -/** - * Color name representing a numbered color. - */ -#define GUAC_TERMINAL_SCHEME_NUMBERED "color" - /** * Flag which specifies that terminal output should be sent to both the current * pipe stream and the user's display. By default, terminal output will be sent @@ -171,6 +136,16 @@ struct guac_terminal { */ guac_client* client; + /** + * Whether user input should be handled and this terminal should render + * frames. Initially, this will be false, user input will be ignored, and + * rendering of frames will be withheld until guac_terminal_start() has + * been invoked. The data within frames will still be rendered, and text + * data received will still be handled, however actual frame boundaries + * will not be sent. + */ + bool started; + /** * The terminal render thread. */ @@ -302,6 +277,20 @@ struct guac_terminal { */ int requested_scrollback; + /** + * The width of the space available to all components of the terminal, in + * pixels. This may include space which will not actually be used for + * character rendering. + */ + int outer_width; + + /** + * The height of the space available to all components of the terminal, in + * pixels. This may include space which will not actually be used for + * character rendering. + */ + int outer_height; + /** * The width of the terminal, in pixels. */ @@ -517,16 +506,49 @@ struct guac_terminal { */ guac_common_clipboard* clipboard; + /** + * The name of the font to use when rendering glyphs, as requested at + * creation time or via guac_terminal_apply_font(). + */ + const char* font_name; + + /** + * The size of each glyph, in points, as requested at creation time or via + * guac_terminal_apply_font(). + */ + int font_size; + + /** + * The name of the color scheme to use, as requested at creation time or + * via guac_terminal_apply_color_scheme(). This string must be in the + * format accepted by guac_terminal_parse_color_scheme(). + */ + const char* color_scheme; + /** * ASCII character to send when backspace is pressed. */ char backspace; + /** + * Whether copying from the terminal clipboard should be blocked. If set, + * the contents of the terminal can still be copied, but will be usable + * only within the terminal itself. The clipboard contents will not be + * automatically streamed to the client. + */ + bool disable_copy; + }; /** * Creates a new guac_terminal, having the given width and height, and - * rendering to the given client. + * rendering to the given client. As failover mechanisms and the Guacamole + * client implementation typically use the receipt of a "sync" message to + * denote successful connection, rendering of frames (sending of "sync") will + * be withheld until guac_terminal_start() is called, and user input will be + * ignored. The guac_terminal_start() function should be invoked only after + * either the underlying connection has truly succeeded, or until visible + * terminal output or user input is required. * * @param client * The client to which the terminal will be rendered. @@ -538,6 +560,12 @@ struct guac_terminal { * clipboard instructions. This clipboard will not be automatically * freed when this terminal is freed. * + * @param disable_copy + * Whether copying from the terminal clipboard should be blocked. If set, + * the contents of the terminal can still be copied, but will be usable + * only within the terminal itself. The clipboard contents will not be + * automatically streamed to the client. + * * @param max_scrollback * The maximum number of rows to allow within the scrollback buffer. The * user may still alter the size of the scrollback buffer using terminal @@ -565,11 +593,8 @@ struct guac_terminal { * The height of the terminal, in pixels. * * @param color_scheme - * The name of the color scheme to use. This string must be one of the - * names defined by the GUAC_TERMINAL_SCHEME_* constants. If blank or NULL, - * the default scheme of GUAC_TERMINAL_SCHEME_GRAY_BLACK will be used. If - * invalid, a warning will be logged, and the terminal will fall back on - * GUAC_TERMINAL_SCHEME_GRAY_BLACK. + * The name of the color scheme to use. This string must be in the format + * accepted by guac_terminal_parse_color_scheme(). * * @param backspace * The integer ASCII code to send when backspace is pressed in @@ -580,8 +605,8 @@ struct guac_terminal { * which renders all text to the given client. */ guac_terminal* guac_terminal_create(guac_client* client, - guac_common_clipboard* clipboard, int max_scrollback, - const char* font_name, int font_size, int dpi, + guac_common_clipboard* clipboard, bool disable_copy, + int max_scrollback, const char* font_name, int font_size, int dpi, int width, int height, const char* color_scheme, const int backspace); @@ -604,6 +629,17 @@ int guac_terminal_render_frame(guac_terminal* terminal); */ int guac_terminal_read_stdin(guac_terminal* terminal, char* c, int size); +/** + * Notifies the terminal that rendering should begin and that user input should + * now be accepted. This function must be invoked following terminal creation + * for the end of frames to be signalled with "sync" messages. Until this + * function is invoked, "sync" messages will be withheld. + * + * @param term + * The terminal to start. + */ +void guac_terminal_start(guac_terminal* term); + /** * Manually stop the terminal to forcibly unblock any pending reads/writes, * e.g. forcing guac_terminal_read_stdin() to return and cease all terminal I/O. @@ -625,7 +661,9 @@ void guac_terminal_notify(guac_terminal* terminal); /** * Reads a single line from this terminal's STDIN, storing the result in a * newly-allocated string. Input is retrieved in the same manner as - * guac_terminal_read_stdin() and the same restrictions apply. + * guac_terminal_read_stdin() and the same restrictions apply. As reading input + * naturally requires user interaction, this function will implicitly invoke + * guac_terminal_start(). * * @param terminal * The terminal to which the provided title should be output, and from @@ -1078,5 +1116,43 @@ int guac_terminal_create_typescript(guac_terminal* term, const char* path, */ int guac_terminal_available_scroll(guac_terminal* term); +/** + * Immediately applies the given color scheme to the given terminal, overriding + * the color scheme provided when the terminal was created. Valid color schemes + * are those accepted by guac_terminal_parse_color_scheme(). + * + * @param terminal + * The terminal to apply the color scheme to. + * + * @param color_scheme + * The color scheme to apply. + */ +void guac_terminal_apply_color_scheme(guac_terminal* terminal, + const char* color_scheme); + +/** + * Alters the font of the terminal. The terminal will automatically be redrawn + * and resized as necessary. If the terminal size changes, the remote side of + * the terminal session must be manually informed of that change or graphical + * artifacts may result. + * + * @param terminal + * The terminal whose font family and/or size are being changed. + * + * @param font_name + * The name of the new font family, or NULL if the font family should + * remain unchanged. + * + * @param font_size + * The new font size, in points, or -1 if the font size should remain + * unchanged. + * + * @param dpi + * The resolution of the display in DPI. If the font size will not be + * changed (the font size given is -1), this value is ignored. + */ +void guac_terminal_apply_font(guac_terminal* terminal, const char* font_name, + int font_size, int dpi); + #endif diff --git a/tests/Makefile.am b/tests/Makefile.am deleted file mode 100644 index 7c7e07bc..00000000 --- a/tests/Makefile.am +++ /dev/null @@ -1,60 +0,0 @@ -# -# 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. -# - -AUTOMAKE_OPTIONS = foreign -ACLOCAL_AMFLAGS = -I m4 - -TESTS = test_libguac -check_PROGRAMS = test_libguac - -noinst_HEADERS = \ - client/client_suite.h \ - common/common_suite.h \ - protocol/suite.h \ - util/util_suite.h - -test_libguac_SOURCES = \ - test_libguac.c \ - client/client_suite.c \ - client/buffer_pool.c \ - client/layer_pool.c \ - common/common_suite.c \ - common/guac_iconv.c \ - common/guac_string.c \ - common/guac_rect.c \ - protocol/suite.c \ - protocol/base64_decode.c \ - protocol/instruction_parse.c \ - protocol/instruction_read.c \ - protocol/instruction_write.c \ - protocol/nest_write.c \ - util/util_suite.c \ - util/guac_pool.c \ - util/guac_unicode.c - -test_libguac_CFLAGS = \ - -Werror -Wall -pedantic \ - @COMMON_INCLUDE@ \ - @LIBGUAC_INCLUDE@ - -test_libguac_LDADD = \ - @COMMON_LTLIB@ \ - @CUNIT_LIBS@ \ - @LIBGUAC_LTLIB@ - diff --git a/tests/common/common_suite.c b/tests/common/common_suite.c deleted file mode 100644 index 58ca7993..00000000 --- a/tests/common/common_suite.c +++ /dev/null @@ -1,57 +0,0 @@ -/* - * 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 "config.h" - -#include "common_suite.h" - -#include - -int common_suite_init() { - return 0; -} - -int common_suite_cleanup() { - return 0; -} - -int register_common_suite() { - - /* Add common test suite */ - CU_pSuite suite = CU_add_suite("common", - common_suite_init, common_suite_cleanup); - if (suite == NULL) { - CU_cleanup_registry(); - return CU_get_error(); - } - - /* Add tests */ - if ( - CU_add_test(suite, "guac-iconv", test_guac_iconv) == NULL - || CU_add_test(suite, "guac-string", test_guac_string) == NULL - || CU_add_test(suite, "guac-rect", test_guac_rect) == NULL - ) { - CU_cleanup_registry(); - return CU_get_error(); - } - - return 0; - -} - diff --git a/tests/common/guac_rect.c b/tests/common/guac_rect.c deleted file mode 100644 index a45ee385..00000000 --- a/tests/common/guac_rect.c +++ /dev/null @@ -1,289 +0,0 @@ -/* - * 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 "config.h" - -#include "common_suite.h" -#include "common/rect.h" - -#include -#include -#include - -void test_guac_rect() { - - guac_common_rect max; - - /* - * Test init method - */ - guac_common_rect_init(&max, 0, 0, 100, 100); - CU_ASSERT_EQUAL(0, max.x); - CU_ASSERT_EQUAL(0, max.y); - CU_ASSERT_EQUAL(100, max.width); - CU_ASSERT_EQUAL(100, max.height); - - /* - * Test constrain method - */ - guac_common_rect rect; - guac_common_rect_init(&rect, -10, -10, 110, 110); - guac_common_rect_init(&max, 0, 0, 100, 100); - guac_common_rect_constrain(&rect, &max); - CU_ASSERT_EQUAL(0, rect.x); - CU_ASSERT_EQUAL(0, rect.y); - CU_ASSERT_EQUAL(100, rect.width); - CU_ASSERT_EQUAL(100, rect.height); - - /* - * Test extend method - */ - guac_common_rect_init(&rect, 10, 10, 90, 90); - guac_common_rect_init(&max, 0, 0, 100, 100); - guac_common_rect_extend(&rect, &max); - CU_ASSERT_EQUAL(0, rect.x); - CU_ASSERT_EQUAL(0, rect.y); - CU_ASSERT_EQUAL(100, rect.width); - CU_ASSERT_EQUAL(100, rect.height); - - /* - * Test adjust method - */ - int cell_size = 16; - - /* Simple adjustment */ - guac_common_rect_init(&rect, 0, 0, 25, 25); - guac_common_rect_init(&max, 0, 0, 100, 100); - guac_common_rect_expand_to_grid(cell_size, &rect, &max); - CU_ASSERT_EQUAL(0, rect.x); - CU_ASSERT_EQUAL(0, rect.y); - CU_ASSERT_EQUAL(32, rect.width); - CU_ASSERT_EQUAL(32, rect.height); - - /* Adjustment with moving of rect */ - guac_common_rect_init(&rect, 75, 75, 25, 25); - guac_common_rect_init(&max, 0, 0, 100, 100); - guac_common_rect_expand_to_grid(cell_size, &rect, &max); - CU_ASSERT_EQUAL(max.width - 32, rect.x); - CU_ASSERT_EQUAL(max.height - 32, rect.y); - CU_ASSERT_EQUAL(32, rect.width); - CU_ASSERT_EQUAL(32, rect.height); - - guac_common_rect_init(&rect, -5, -5, 25, 25); - guac_common_rect_init(&max, 0, 0, 100, 100); - guac_common_rect_expand_to_grid(cell_size, &rect, &max); - CU_ASSERT_EQUAL(0, rect.x); - CU_ASSERT_EQUAL(0, rect.y); - CU_ASSERT_EQUAL(32, rect.width); - CU_ASSERT_EQUAL(32, rect.height); - - /* Adjustment with moving and clamping of rect */ - guac_common_rect_init(&rect, 0, 0, 25, 15); - guac_common_rect_init(&max, 0, 5, 32, 15); - guac_common_rect_expand_to_grid(cell_size, &rect, &max); - CU_ASSERT_EQUAL(max.x, rect.x); - CU_ASSERT_EQUAL(max.y, rect.y); - CU_ASSERT_EQUAL(max.width, rect.width); - CU_ASSERT_EQUAL(max.height, rect.height); - - /* - * Rectangle intersection tests - */ - guac_common_rect min; - guac_common_rect_init(&min, 10, 10, 10, 10); - - /* Rectangle intersection - empty - * rectangle is outside */ - guac_common_rect_init(&rect, 25, 25, 5, 5); - int res = guac_common_rect_intersects(&rect, &min); - CU_ASSERT_EQUAL(0, res); - - /* Rectangle intersection - complete - * rectangle is completely inside */ - guac_common_rect_init(&rect, 11, 11, 5, 5); - res = guac_common_rect_intersects(&rect, &min); - CU_ASSERT_EQUAL(2, res); - - /* Rectangle intersection - partial - * rectangle intersects UL */ - guac_common_rect_init(&rect, 8, 8, 5, 5); - res = guac_common_rect_intersects(&rect, &min); - CU_ASSERT_EQUAL(1, res); - - /* Rectangle intersection - partial - * rectangle intersects LR */ - guac_common_rect_init(&rect, 18, 18, 5, 5); - res = guac_common_rect_intersects(&rect, &min); - CU_ASSERT_EQUAL(1, res); - - /* Rectangle intersection - complete - * rect intersects along UL but inside */ - guac_common_rect_init(&rect, 10, 10, 5, 5); - res = guac_common_rect_intersects(&rect, &min); - CU_ASSERT_EQUAL(2, res); - - /* Rectangle intersection - partial - * rectangle intersects along L but outside */ - guac_common_rect_init(&rect, 5, 10, 5, 5); - res = guac_common_rect_intersects(&rect, &min); - CU_ASSERT_EQUAL(1, res); - - /* Rectangle intersection - complete - * rectangle intersects along LR but rest is inside */ - guac_common_rect_init(&rect, 15, 15, 5, 5); - res = guac_common_rect_intersects(&rect, &min); - CU_ASSERT_EQUAL(2, res); - - /* Rectangle intersection - partial - * rectangle intersects along R but rest is outside */ - guac_common_rect_init(&rect, 20, 10, 5, 5); - res = guac_common_rect_intersects(&rect, &min); - CU_ASSERT_EQUAL(1, res); - - /* Rectangle intersection - partial - * rectangle encloses min; which is a partial intersection */ - guac_common_rect_init(&rect, 5, 5, 20, 20); - res = guac_common_rect_intersects(&rect, &min); - CU_ASSERT_EQUAL(1, res); - - /* - * Basic test of clip and split method - */ - guac_common_rect_init(&min, 10, 10, 10, 10); - guac_common_rect cut; - - /* Clip top */ - guac_common_rect_init(&rect, 10, 5, 10, 10); - res = guac_common_rect_clip_and_split(&rect, &min, &cut); - CU_ASSERT_EQUAL(1, res); - CU_ASSERT_EQUAL(10, cut.x); - CU_ASSERT_EQUAL(5, cut.y); - CU_ASSERT_EQUAL(10, cut.width); - CU_ASSERT_EQUAL(5, cut.height); - - CU_ASSERT_EQUAL(10, rect.x); - CU_ASSERT_EQUAL(10, rect.y); - CU_ASSERT_EQUAL(10, rect.width); - CU_ASSERT_EQUAL(5, rect.height); - - /* Clip bottom */ - guac_common_rect_init(&rect, 10, 15, 10, 10); - res = guac_common_rect_clip_and_split(&rect, &min, &cut); - CU_ASSERT_EQUAL(1, res); - CU_ASSERT_EQUAL(10, cut.x); - CU_ASSERT_EQUAL(20, cut.y); - CU_ASSERT_EQUAL(10, cut.width); - CU_ASSERT_EQUAL(5, cut.height); - - CU_ASSERT_EQUAL(10, rect.x); - CU_ASSERT_EQUAL(15, rect.y); - CU_ASSERT_EQUAL(10, rect.width); - CU_ASSERT_EQUAL(5, rect.height); - - /* Clip left */ - guac_common_rect_init(&rect, 5, 10, 10, 10); - res = guac_common_rect_clip_and_split(&rect, &min, &cut); - CU_ASSERT_EQUAL(1, res); - CU_ASSERT_EQUAL(5, cut.x); - CU_ASSERT_EQUAL(10, cut.y); - CU_ASSERT_EQUAL(5, cut.width); - CU_ASSERT_EQUAL(10, cut.height); - - CU_ASSERT_EQUAL(10, rect.x); - CU_ASSERT_EQUAL(10, rect.y); - CU_ASSERT_EQUAL(5, rect.width); - CU_ASSERT_EQUAL(10, rect.height); - - /* Clip right */ - guac_common_rect_init(&rect, 15, 10, 10, 10); - res = guac_common_rect_clip_and_split(&rect, &min, &cut); - CU_ASSERT_EQUAL(1, res); - CU_ASSERT_EQUAL(20, cut.x); - CU_ASSERT_EQUAL(10, cut.y); - CU_ASSERT_EQUAL(5, cut.width); - CU_ASSERT_EQUAL(10, cut.height); - - CU_ASSERT_EQUAL(15, rect.x); - CU_ASSERT_EQUAL(10, rect.y); - CU_ASSERT_EQUAL(5, rect.width); - CU_ASSERT_EQUAL(10, rect.height); - - /* - * Test a rectangle which completely covers the hole. - * Clip and split until done. - */ - guac_common_rect_init(&rect, 5, 5, 20, 20); - - /* Clip top */ - res = guac_common_rect_clip_and_split(&rect, &min, &cut); - CU_ASSERT_EQUAL(1, res); - CU_ASSERT_EQUAL(5, cut.x); - CU_ASSERT_EQUAL(5, cut.y); - CU_ASSERT_EQUAL(20, cut.width); - CU_ASSERT_EQUAL(5, cut.height); - - CU_ASSERT_EQUAL(5, rect.x); - CU_ASSERT_EQUAL(10, rect.y); - CU_ASSERT_EQUAL(20, rect.width); - CU_ASSERT_EQUAL(15, rect.height); - - /* Clip left */ - res = guac_common_rect_clip_and_split(&rect, &min, &cut); - CU_ASSERT_EQUAL(1, res); - CU_ASSERT_EQUAL(5, cut.x); - CU_ASSERT_EQUAL(10, cut.y); - CU_ASSERT_EQUAL(5, cut.width); - CU_ASSERT_EQUAL(15, cut.height); - - CU_ASSERT_EQUAL(10, rect.x); - CU_ASSERT_EQUAL(10, rect.y); - CU_ASSERT_EQUAL(15, rect.width); - CU_ASSERT_EQUAL(15, rect.height); - - /* Clip bottom */ - res = guac_common_rect_clip_and_split(&rect, &min, &cut); - CU_ASSERT_EQUAL(1, res); - CU_ASSERT_EQUAL(10, cut.x); - CU_ASSERT_EQUAL(20, cut.y); - CU_ASSERT_EQUAL(15, cut.width); - CU_ASSERT_EQUAL(5, cut.height); - - CU_ASSERT_EQUAL(10, rect.x); - CU_ASSERT_EQUAL(10, rect.y); - CU_ASSERT_EQUAL(15, rect.width); - CU_ASSERT_EQUAL(10, rect.height); - - /* Clip right */ - res = guac_common_rect_clip_and_split(&rect, &min, &cut); - CU_ASSERT_EQUAL(20, cut.x); - CU_ASSERT_EQUAL(10, cut.y); - CU_ASSERT_EQUAL(5, cut.width); - CU_ASSERT_EQUAL(10, cut.height); - - CU_ASSERT_EQUAL(10, rect.x); - CU_ASSERT_EQUAL(10, rect.y); - CU_ASSERT_EQUAL(10, rect.width); - CU_ASSERT_EQUAL(10, rect.height); - - /* Make sure nothing is left to do */ - res = guac_common_rect_clip_and_split(&rect, &min, &cut); - CU_ASSERT_EQUAL(0, res); - -} - diff --git a/tests/protocol/instruction_read.c b/tests/protocol/instruction_read.c deleted file mode 100644 index f10e0017..00000000 --- a/tests/protocol/instruction_read.c +++ /dev/null @@ -1,108 +0,0 @@ -/* - * 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 "config.h" - -#include "suite.h" - -#include -#include -#include - -#include -#include -#include -#include -#include - -void test_instruction_read() { - - int rfd, wfd; - int fd[2], childpid; - - char test_string[] = "4.test,6.a" UTF8_4 "b," - "5.12345,10.a" UTF8_8 "c;" - "5.test2,10.hellohello,15.worldworldworld;"; - - /* Create pipe */ - CU_ASSERT_EQUAL_FATAL(pipe(fd), 0); - - /* File descriptors */ - rfd = fd[0]; - wfd = fd[1]; - - /* Fork */ - if ((childpid = fork()) == -1) { - /* ERROR */ - perror("fork"); - return; - } - - /* Child (pipe writer) */ - if (childpid != 0) { - close(rfd); - CU_ASSERT_EQUAL( - write(wfd, test_string, sizeof(test_string)), - sizeof(test_string) - ); - exit(0); - } - - /* Parent (unit test) */ - else { - - guac_socket* socket; - guac_parser* parser; - - close(wfd); - - /* Open guac socket */ - socket = guac_socket_open(rfd); - CU_ASSERT_PTR_NOT_NULL_FATAL(socket); - - /* Allocate parser */ - parser = guac_parser_alloc(); - CU_ASSERT_PTR_NOT_NULL_FATAL(parser); - - /* Read instruction */ - CU_ASSERT_EQUAL_FATAL(guac_parser_read(parser, socket, 1000000), 0); - - /* Validate contents */ - CU_ASSERT_STRING_EQUAL(parser->opcode, "test"); - CU_ASSERT_EQUAL_FATAL(parser->argc, 3); - CU_ASSERT_STRING_EQUAL(parser->argv[0], "a" UTF8_4 "b"); - CU_ASSERT_STRING_EQUAL(parser->argv[1], "12345"); - CU_ASSERT_STRING_EQUAL(parser->argv[2], "a" UTF8_8 "c"); - - /* Read another instruction */ - CU_ASSERT_EQUAL_FATAL(guac_parser_read(parser, socket, 1000000), 0); - - /* Validate contents */ - CU_ASSERT_STRING_EQUAL(parser->opcode, "test2"); - CU_ASSERT_EQUAL_FATAL(parser->argc, 2); - CU_ASSERT_STRING_EQUAL(parser->argv[0], "hellohello"); - CU_ASSERT_STRING_EQUAL(parser->argv[1], "worldworldworld"); - - guac_parser_free(parser); - guac_socket_free(socket); - - } - -} - diff --git a/tests/protocol/instruction_write.c b/tests/protocol/instruction_write.c deleted file mode 100644 index 5a21b608..00000000 --- a/tests/protocol/instruction_write.c +++ /dev/null @@ -1,101 +0,0 @@ -/* - * 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 "config.h" - -#include "suite.h" - -#include -#include -#include - -#include -#include -#include -#include - -void test_instruction_write() { - - int rfd, wfd; - int fd[2], childpid; - - /* Create pipe */ - CU_ASSERT_EQUAL_FATAL(pipe(fd), 0); - - /* File descriptors */ - rfd = fd[0]; - wfd = fd[1]; - - /* Fork */ - if ((childpid = fork()) == -1) { - /* ERROR */ - perror("fork"); - return; - } - - /* Child (pipe writer) */ - if (childpid != 0) { - - guac_socket* socket; - - close(rfd); - - /* Open guac socket */ - socket = guac_socket_open(wfd); - - /* Write instruction */ - guac_protocol_send_name(socket, "a" UTF8_4 "b" UTF8_4 "c"); - guac_protocol_send_sync(socket, 12345); - guac_socket_flush(socket); - - guac_socket_free(socket); - exit(0); - } - - /* Parent (unit test) */ - else { - - char expected[] = - "4.name,11.a" UTF8_4 "b" UTF8_4 "c;" - "4.sync,5.12345;"; - - int numread; - char buffer[1024]; - int offset = 0; - - close(wfd); - - /* Read everything available into buffer */ - while ((numread = - read(rfd, - &(buffer[offset]), - sizeof(buffer)-offset)) != 0) { - offset += numread; - } - - /* Add NULL terminator */ - buffer[offset] = '\0'; - - /* Read value should be equal to expected value */ - CU_ASSERT_STRING_EQUAL(buffer, expected); - - } - -} - diff --git a/tests/protocol/nest_write.c b/tests/protocol/nest_write.c deleted file mode 100644 index 5d4c231e..00000000 --- a/tests/protocol/nest_write.c +++ /dev/null @@ -1,109 +0,0 @@ -/* - * 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 "config.h" - -#include "suite.h" - -#include -#include -#include - -#include -#include -#include -#include - -void test_nest_write() { - - int rfd, wfd; - int fd[2], childpid; - - /* Create pipe */ - CU_ASSERT_EQUAL_FATAL(pipe(fd), 0); - - /* File descriptors */ - rfd = fd[0]; - wfd = fd[1]; - - /* Fork */ - if ((childpid = fork()) == -1) { - /* ERROR */ - perror("fork"); - return; - } - - /* Child (pipe writer) */ - if (childpid != 0) { - - guac_socket* nested_socket; - guac_socket* socket; - - close(rfd); - - /* Open guac socket */ - socket = guac_socket_open(wfd); - - /* Nest socket */ - nested_socket = guac_socket_nest(socket, 0); - - /* Write instruction */ - guac_protocol_send_name(nested_socket, "a" UTF8_4 "b" UTF8_4 "c"); - guac_protocol_send_sync(nested_socket, 12345); - guac_socket_flush(nested_socket); - guac_socket_flush(socket); - - guac_socket_free(nested_socket); - guac_socket_free(socket); - exit(0); - } - - /* Parent (unit test) */ - else { - - char expected[] = - "4.nest,1.0,37." - "4.name,11.a" UTF8_4 "b" UTF8_4 "c;" - "4.sync,5.12345;" - ";"; - - int numread; - char buffer[1024]; - int offset = 0; - - close(wfd); - - /* Read everything available into buffer */ - while ((numread = - read(rfd, - &(buffer[offset]), - sizeof(buffer)-offset)) != 0) { - offset += numread; - } - - /* Add NULL terminator */ - buffer[offset] = '\0'; - - /* Read value should be equal to expected value */ - CU_ASSERT_STRING_EQUAL(buffer, expected); - - } - -} - diff --git a/tests/protocol/suite.c b/tests/protocol/suite.c deleted file mode 100644 index 14cfc406..00000000 --- a/tests/protocol/suite.c +++ /dev/null @@ -1,59 +0,0 @@ -/* - * 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 "config.h" - -#include "suite.h" - -#include - -int protocol_suite_init() { - return 0; -} - -int protocol_suite_cleanup() { - return 0; -} - -int register_protocol_suite() { - - /* Add protocol test suite */ - CU_pSuite suite = CU_add_suite("protocol", - protocol_suite_init, protocol_suite_cleanup); - if (suite == NULL) { - CU_cleanup_registry(); - return CU_get_error(); - } - - /* Add tests */ - if ( - CU_add_test(suite, "base64-decode", test_base64_decode) == NULL - || CU_add_test(suite, "instruction-parse", test_instruction_parse) == NULL - || CU_add_test(suite, "instruction-read", test_instruction_read) == NULL - || CU_add_test(suite, "instruction-write", test_instruction_write) == NULL - || CU_add_test(suite, "nest-write", test_nest_write) == NULL - ) { - CU_cleanup_registry(); - return CU_get_error(); - } - - return 0; - -} - diff --git a/tests/util/util_suite.c b/tests/util/util_suite.c deleted file mode 100644 index 7213a0f2..00000000 --- a/tests/util/util_suite.c +++ /dev/null @@ -1,56 +0,0 @@ -/* - * 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 "config.h" - -#include "util_suite.h" - -#include - -int util_suite_init() { - return 0; -} - -int util_suite_cleanup() { - return 0; -} - -int register_util_suite() { - - /* Add util test suite */ - CU_pSuite suite = CU_add_suite("util", - util_suite_init, util_suite_cleanup); - if (suite == NULL) { - CU_cleanup_registry(); - return CU_get_error(); - } - - /* Add tests */ - if ( - CU_add_test(suite, "guac-pool", test_guac_pool) == NULL - || CU_add_test(suite, "guac-unicode", test_guac_unicode) == NULL - ) { - CU_cleanup_registry(); - return CU_get_error(); - } - - return 0; - -} - diff --git a/util/generate-test-runner.pl b/util/generate-test-runner.pl new file mode 100755 index 00000000..ea99f7e7 --- /dev/null +++ b/util/generate-test-runner.pl @@ -0,0 +1,199 @@ +#!/usr/bin/env perl +# +# 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. +# + +# +# generate-test-runner.pl +# +# Generates a test runner for the .c files given on the command line. Each .c +# file may declare any number of tests so long as each test uses CUnit and is +# declared with the following convention: +# +# void test_SUITENAME__TESTNAME() { +# ... +# } +# +# where TESTNAME is the arbitrary name of the test and SUITENAME is the +# arbitrary name of the test suite that this test belongs to. +# +# Absolutely all tests MUST follow the above convention if they are to be +# picked up by this script. Functions which are not tests MUST NOT follow +# the above convention. +# + +use strict; + +my $num_tests = 0; +my %test_suites = (); + +# Parse all test declarations from given file +while (<>) { + if ((my $suite_name, my $test_name) = m/^void\s+test_(\w+)__(\w+)/) { + $num_tests++; + $test_suites{$suite_name} //= (); + push @{$test_suites{$suite_name}}, $test_name; + } +} + +# Bail out if there's nothing to write +if ($num_tests == 0) { + die "No unit tests... :(\n"; +} + +# +# Common test runner header +# + +print <<'END'; +/* + * 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 +#include + +/** + * The current test number, as required by the TAP format. This value is + * automatically incremented by tap_log_test_completed() after each test is + * run. + */ +int tap_test_number = 1; + +/** + * Logs the status of a CUnit test which just completed. This implementation + * logs test completion in TAP format. + * + * @param test + * The CUnit test which just completed. + * + * @param suite + * The CUnit test suite associated with the test. + * + * @param failure + * The head element of the test failure list, or NULL if the test passed. + */ +static void tap_log_test_completed(const CU_pTest test, + const CU_pSuite suite, const CU_pFailureRecord failure) { + + /* Log success/failure in TAP format */ + if (failure == NULL) + printf("ok %i - [%s] %s: OK\n", + tap_test_number, suite->pName, test->pName); + else + printf("not ok %i - [%s] %s: Assertion failed on %s:%i: %s\n", + tap_test_number, suite->pName, test->pName, + failure->strFileName, failure->uiLineNumber, + failure->strCondition); + + tap_test_number++; + +} +END + +# +# Prototypes for all test functions +# + +while ((my $suite_name, my $test_names) = each (%test_suites)) { + print "\n/* Automatically-generated prototypes for the $suite_name suite */\n"; + foreach my $test_name (@{ $test_names }) { + print "void test_${suite_name}__${test_name}();\n"; + } +} + +# +# Beginning of main() function body for test runner +# + +print <<"END"; + +/* Automatically-generated test runner */ +int main() { + + /* Init CUnit test registry */ + if (CU_initialize_registry() != CUE_SUCCESS) + return CU_get_error(); +END + +# +# Within main(), register each test and its corresponding test suite +# + +while ((my $suite_name, my $test_names) = each (%test_suites)) { + + print <<" END"; + + /* Create and register all tests for the $suite_name suite */ + CU_pSuite $suite_name = CU_add_suite("$suite_name", NULL, NULL); + if ($suite_name == NULL + END + + foreach my $test_name (@{ $test_names }) { + print <<" END"; + || CU_add_test($suite_name, "$test_name", test_${suite_name}__${test_name}) == NULL + END + } + + print <<" END"; + ) goto cleanup; + END + +} + +# +# End of main() function +# + +print <<"END"; + + /* Force line-buffered output to ensure log messages are visible even if + * a test crashes */ + setvbuf(stdout, NULL, _IOLBF, 0); + setvbuf(stderr, NULL, _IOLBF, 0); + + /* Write TAP header */ + printf("1..$num_tests\\n"); + + /* Run all tests in all suites */ + CU_set_test_complete_handler(tap_log_test_completed); + CU_run_all_tests(); + +cleanup: + /* Tests complete */ + CU_cleanup_registry(); + return CU_get_error(); + +} +END +