From d48a809d92fc9993590526ffc93e2f62d1ada5cd Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Fri, 28 Jul 2023 22:45:25 +0000 Subject: [PATCH] GUACAMOLE-1841: Implement MinGW build for libguac. --- .gitignore | 3 + README-windows-build.md | 116 +++++ configure.ac | 110 +++-- src/common-ssh/Makefile.am | 5 +- src/common-ssh/ssh.c | 14 +- src/common-ssh/tests/Makefile.am | 2 +- src/common/Makefile.am | 2 +- src/common/tests/Makefile.am | 3 +- src/guacd/Makefile.am | 13 +- src/guacd/connection.c | 245 +++++++++- src/guacd/connection.h | 17 + src/guacd/daemon.c | 11 +- src/guacd/log.c | 22 +- src/guacd/move-pipe.c | 107 +++++ src/guacd/move-pipe.h | 74 +++ src/guacd/proc.c | 82 +++- src/guacenc/Makefile.am | 1 + src/guacenc/encode.c | 14 +- src/guaclog/Makefile.am | 1 + src/guaclog/interpret.c | 13 +- src/libguac/Makefile.am | 28 +- src/libguac/client-internal.h | 135 ++++++ src/libguac/client.c | 250 ++++++---- src/libguac/error.c | 240 ++++++++-- src/libguac/guacamole/client.h | 92 +--- src/libguac/guacamole/error-types.h | 12 + src/libguac/guacamole/error.h | 44 +- src/libguac/guacamole/handle-helpers.h | 83 ++++ src/libguac/{ => guacamole}/id.h | 6 + src/libguac/guacamole/pipe.h | 38 ++ src/libguac/guacamole/plugin-constants.h | 18 + src/libguac/guacamole/socket-handle.h | 50 ++ src/libguac/handle-helpers.c | 118 +++++ src/libguac/id.c | 51 +- src/libguac/recording.c | 4 + src/libguac/socket-handle.c | 451 ++++++++++++++++++ src/libguac/tests/Makefile.am | 2 +- src/libguac/tests/id/generate.c | 2 +- src/libguac/user-handshake.c | 16 +- src/libguac/user.c | 2 +- src/libguac/wait-fd.c | 5 +- src/libguac/wait-handle.c | 76 +++ src/libguac/wait-handle.h | 44 ++ src/libguac/wol.c | 8 +- src/protocols/kubernetes/Makefile.am | 4 +- src/protocols/kubernetes/client.c | 10 +- src/protocols/kubernetes/tests/Makefile.am | 2 +- src/protocols/rdp/Makefile.am | 16 +- .../rdp/channels/rdpdr/rdpdr-messages.c | 20 +- .../rdp/channels/rdpdr/rdpdr-messages.h | 3 +- .../rdp/channels/rdpdr/rdpdr-printer.c | 9 +- src/protocols/rdp/client.c | 95 +++- src/protocols/rdp/fs.c | 83 +++- src/protocols/rdp/fs.h | 13 +- src/protocols/rdp/guacxpstopdf/Makefile.am | 42 ++ src/protocols/rdp/guacxpstopdf/guacxpstopdf.c | 266 +++++++++++ src/protocols/rdp/guacxpstopdf/guacxpstopdf.h | 23 + src/protocols/rdp/print-job.c | 100 +++- src/protocols/rdp/rdp.c | 2 +- src/protocols/rdp/rdp.h | 8 + src/protocols/rdp/settings.c | 14 + src/protocols/rdp/tests/Makefile.am | 2 +- src/protocols/ssh/Makefile.am | 5 +- src/protocols/ssh/client.c | 10 +- src/protocols/ssh/ssh.c | 39 +- src/protocols/telnet/Makefile.am | 5 +- src/protocols/telnet/client.c | 10 +- src/protocols/telnet/telnet.c | 40 +- src/protocols/vnc/Makefile.am | 5 +- src/protocols/vnc/cursor.c | 1 - src/protocols/vnc/display.c | 1 - src/protocols/vnc/log.c | 16 + src/protocols/vnc/log.h | 1 - src/pulse/Makefile.am | 7 +- src/terminal/Makefile.am | 10 +- src/terminal/color-scheme.c | 18 + src/terminal/display.c | 4 + src/terminal/named-colors.c | 9 + src/terminal/terminal-handlers.c | 4 + src/terminal/terminal.c | 5 + src/terminal/terminal/wcwidth.h | 39 ++ src/terminal/typescript.c | 11 +- src/terminal/wcwidth.c | 309 ++++++++++++ 83 files changed, 3501 insertions(+), 390 deletions(-) create mode 100644 README-windows-build.md create mode 100755 src/guacd/move-pipe.c create mode 100755 src/guacd/move-pipe.h create mode 100644 src/libguac/client-internal.h create mode 100755 src/libguac/guacamole/handle-helpers.h rename src/libguac/{ => guacamole}/id.h (89%) create mode 100644 src/libguac/guacamole/pipe.h create mode 100644 src/libguac/guacamole/socket-handle.h create mode 100755 src/libguac/handle-helpers.c create mode 100644 src/libguac/socket-handle.c create mode 100644 src/libguac/wait-handle.c create mode 100644 src/libguac/wait-handle.h create mode 100644 src/protocols/rdp/guacxpstopdf/Makefile.am create mode 100644 src/protocols/rdp/guacxpstopdf/guacxpstopdf.c create mode 100644 src/protocols/rdp/guacxpstopdf/guacxpstopdf.h create mode 100644 src/terminal/terminal/wcwidth.h create mode 100644 src/terminal/wcwidth.c diff --git a/.gitignore b/.gitignore index e72000d8ef..0bc7cd484f 100644 --- a/.gitignore +++ b/.gitignore @@ -48,3 +48,6 @@ doc/*/doxygen-output # IDE metadata nbproject/ + +# Crash Reports +**/*.stackdump diff --git a/README-windows-build.md b/README-windows-build.md new file mode 100644 index 0000000000..7f6c25fc63 --- /dev/null +++ b/README-windows-build.md @@ -0,0 +1,116 @@ +# Building guacamole-server for Windows +Certain portions of `guacamole-server` can be built using MinGW on Windows, specifically the libguac libraries. `guacd` itself relies on `fork()` and other functionality that has no equivalent in Windows. Theoretically, Cygwin could provide a compatibility layer for these missing functions, but so far attempts to implement such a build have not resulted in a functional `guacd`. + +This document will walk you through a known-working set of steps for building the libguac libraries for a Windows target. The following build steps were tested on a Windows Server 2022 x86_64 build node. + +### Build Steps +1. Install MSYS2 (version 20230718 used here) +2. Install MSYS2 packages: + * autoconf-wrapper + * automake-wrapper + * diffutils + * git + * libtool + * libedit-devel + * make + * pkg-config + * wget + * msys2-runtime-devel + * mingw-w64-x86_64-gcc + * mingw-w64-x86_64-libwebsockets + * mingw-w64-x86_64-libtool + * mingw-w64-x86_64-dlfcn + * mingw-w64-x86_64-pkg-config + * mingw-w64-x86_64-cairo + * mingw-w64-x86_64-gcc + * mingw-w64-x86_64-gdb + * mingw-w64-x86_64-libpng + * mingw-w64-x86_64-libjpeg-turbo + * mingw-w64-x86_64-freerdp (version < 3) + * mingw-w64-x86_64-freetds + * mingw-w64-x86_64-postgresql + * mingw-w64-x86_64-libmariadbclient + * mingw-w64-x86_64-libvncserver + * mingw-w64-x86_64-dlfcn + * mingw-w64-x86_64-libgcrypt + * mingw-w64-x86_64-libgxps + * mingw-w64-x86_64-libwebsockets + * mingw-w64-x86_64-libwebp + * mingw-w64-x86_64-libssh2 + * mingw-w64-x86_64-openssl + * mingw-w64-x86_64-libvorbis + * mingw-w64-x86_64-pulseaudio + * mingw-w64-x86_64-zlib + * +3. Build `libtelnet` from source using MSYS2 bash shell + ``` + export PKG_CONFIG_PATH="/mingw64/lib/pkgconfig" + export PATH="$PATH:/mingw64/bin:/usr/bin" + curl -s -L https://github.com/seanmiddleditch/libtelnet/releases/download/0.23/libtelnet-0.23.tar.gz | tar xz + cd libtelnet-0.23 + autoreconf -fi + + ./configure --prefix=/mingw64 --disable-static --disable-util LDFLAGS="-Wl,-no-undefined -L/mingw64/bin/ -L/mingw64/lib" || cat config.log + cat config.log + + make LDFLAGS="-no-undefined" + make install + ``` +4. Fix DLL prefixes so that the build can link against them, e.g. using this script + ``` + #!/bin/bash + + set -e + set -x + + # Enable fancy pattern matching on filenames (provides wildcards that can match + # multiple contiguous digits, among others) + shopt -s extglob + + # Strip architecture suffix + for LIB in /mingw64/bin/lib*-x64.dll; do + ln -sfv "$LIB" "$(echo "$LIB" | sed 's/-x64.dll$/.dll/')" + done + + # Automatically add symlinks that strip the library version suffix + for LIB in /mingw64/bin/lib*-+([0-9]).dll; do + ln -sfv "$LIB" "$(echo "$LIB" | sed 's/-[0-9]*\.dll$/.dll/')" + done + + # Automatically add symlinks that strip the library version suffix + for LIB in /mingw64/bin/lib*([^0-9.])@([^0-9.-])+([0-9]).dll; do + ln -sfv "$LIB" "$(echo "$LIB" | sed 's/[0-9]*\.dll$/.dll/')" + done + + ``` +5. Build `guacamole-server` from source using MSYS2 bash shell + ``` + export PKG_CONFIG_PATH="/mingw64/lib/pkgconfig:/usr/lib/pkgconfig" + export PATH="$PATH:/mingw64/bin:/usr/bin" + + # FIXME: Update this to check out master once this PR is ready for merge + git clone https://github.com/jmuehlner/guacamole-server.git + cd guacamole-server + git checkout GUACAMOLE-1841-cygwin-build-clean + + autoreconf -fi + export LDFLAGS="-L/mingw64/bin/ -L/usr/bin/ -L/mingw64/lib -lws2_32" + export CFLAGS="-isystem/mingw64/include/ \ + -I/mingw64/include/pango-1.0 \ + -I/mingw64/include/glib-2.0/ \ + -I/mingw64/lib/glib-2.0/include/ \ + -I/mingw64/include/harfbuzz/ \ + -I/mingw64/include/cairo/ \ + -I/mingw64/include/freerdp2 \ + -I/mingw64/include/winpr2 \ + -Wno-error=expansion-to-defined + -Wno-error=attributes" + + ./configure --prefix=/mingw64 --with-windows --disable-guacenc --disable-guacd --disable-guaclog || cat config.log + + make + make install + ``` + +## Closing Notes +FIXME: Update this document to explain how to configure fonts under Windows once I figure it out. diff --git a/configure.ac b/configure.ac index 4be4bcc373..2d2f8f54ce 100644 --- a/configure.ac +++ b/configure.ac @@ -40,9 +40,35 @@ AC_PROG_LIBTOOL # Headers AC_CHECK_HEADERS([fcntl.h stdlib.h string.h sys/socket.h time.h sys/time.h syslog.h unistd.h cairo/cairo.h pngstruct.h]) +# windows build +GENERAL_LDFLAGS= +GENERAL_CFLAGS=-Werror -Wall -pedantic +AC_ARG_WITH([windows], + [AS_HELP_STRING([--with-windows], + [build under/for Windows @<:@default=no@:>@])], + [with_windows=yes], [with_windows=no]) + +AM_CONDITIONAL([WINDOWS_BUILD], [test "x${with_windows}" = "xyes"]) + +if test "x$with_windows" = "xyes" +then + GENERAL_LDFLAGS=-no-undefined + GENERAL_CFLAGS=-Werror -Wall -Wno-error=expansion-to-defined + AC_DEFINE([WINDOWS_BUILD],,[Build against windows on Windows]) + + AC_CHECK_LIB([systre], [regcomp], [SYSTRE_LIBS=-lsystre], + AC_MSG_ERROR("libsystre is required for regex functionality")) +else + SYSTRE_LIBS= +fi + # Source characteristics AC_DEFINE([_XOPEN_SOURCE], [700], [Uses X/Open and POSIX APIs]) -AC_DEFINE([__BSD_VISIBLE], [1], [Uses BSD-specific APIs (if available)]) + +if test "x$with_windows" = "xno" +then + AC_DEFINE([__BSD_VISIBLE], [1], [Uses BSD-specific APIs (if available)]) +fi # Check for whether math library is required AC_CHECK_LIB([m], [cos], @@ -68,6 +94,9 @@ AC_CHECK_LIB([pthread], [pthread_create], [PTHREAD_LIBS=-lpthread AC_DEFINE([HAVE_LIBPTHREAD],, [Whether libpthread was found])]) +# Windows provides its own timer API, and does not user timer_create +if test "x$with_windows" = "xno" +then # librt AC_CHECK_FUNC([timer_create], [AC_MSG_RESULT([timer_create was found without librt.])], [AC_CHECK_LIB([rt], [timer_create], @@ -75,6 +104,7 @@ AC_CHECK_FUNC([timer_create], [AC_MSG_RESULT([timer_create was found without lib RT_LIBS=-lrt], [AC_MSG_ERROR([timer_create could not be found.])]) ]) +fi # Include libdl for dlopen() if necessary AC_CHECK_LIB([dl], [dlopen], @@ -84,9 +114,28 @@ AC_CHECK_LIB([dl], [dlopen], [#include ])]) # -# libuuid +# UUID library +# First, check librpcrt4 (Windows), then libuuid, and finally OSSP UUID # +AC_ARG_WITH([librpcrt4], + [AS_HELP_STRING([--with-librpcrt4], + [use librpcrt4 to generate unique identifiers @<:@default=check@:>@])], + [], + [with_librpcrt4=check]) + +# First, check for librpcrt4 (Windows) +have_librpcrt4=disabled +if test "x$with_librpcrt4" != "xno" +then + have_librpcrt4=yes + AC_CHECK_LIB([rpcrt4], [UuidCreate], + [UUID_LIBS=-lrpcrt4] + [AC_DEFINE([HAVE_LIBRPCRT4],, [Whether librpcrt4 is available])], + [have_librpcrt4=no]) +fi + +# Next, look for libuuid have_libuuid=disabled AC_ARG_WITH([libuuid], [AS_HELP_STRING([--with-libuuid], @@ -94,38 +143,38 @@ AC_ARG_WITH([libuuid], [], [with_libuuid=check]) -if test "x$with_libuuid" != "xno" +if test "x$with_libuuid" != "xno" -a "x$have_librpcrt4" != "xyes" then have_libuuid=yes AC_CHECK_LIB([uuid], [uuid_generate], - [UUID_LIBS=-luuid] - [AC_DEFINE([HAVE_LIBUUID],, [Whether libuuid is available])], - [have_libuuid=no]) + [UUID_LIBS=-luuid] + [AC_DEFINE([HAVE_LIBUUID],, [Whether libuuid is available])], + [have_libuuid=no]) fi -# OSSP UUID (if libuuid is unavilable) -if test "x${have_libuuid}" != "xyes" +# OSSP UUID (if librpcrt4 and libuuid are unavilable) +if test "x$have_librpcrt4" != "xyes" -a "x$have_libuuid" != "xyes" then AC_CHECK_LIB([ossp-uuid], [uuid_make], [UUID_LIBS=-lossp-uuid], - AC_CHECK_LIB([uuid], [uuid_make], [UUID_LIBS=-luuid], - AC_MSG_ERROR([ - -------------------------------------------- - Unable to find libuuid or the OSSP UUID library. - Either libuuid (from util-linux) or the OSSP UUID library is required for - guacamole-server to be built. - --------------------------------------------]))) + AC_CHECK_LIB([uuid], [uuid_make], [UUID_LIBS=-luuid], + AC_MSG_ERROR([ +-------------------------------------------- +Unable to find librpcrt4 or libuuid or the OSSP UUID library. +Either librpcrt4 (Windows) or libuuid (from util-linux) or the +OSSP UUID library is required for guacamole-server to be built. +--------------------------------------------]))) # Check for and validate OSSP uuid.h header AC_CHECK_HEADERS([ossp/uuid.h]) AC_CHECK_DECL([uuid_make],, - AC_MSG_ERROR("No OSSP uuid.h found in include path"), - [#ifdef HAVE_OSSP_UUID_H - #include - #else - #include - #endif - ]) + AC_MSG_ERROR("No OSSP uuid.h found in include path"), + [#ifdef HAVE_OSSP_UUID_H + #include + #else + #include + #endif + ]) fi # cunit @@ -140,6 +189,9 @@ AC_SUBST(RT_LIBS) AC_SUBST(PTHREAD_LIBS) AC_SUBST(UUID_LIBS) AC_SUBST(CUNIT_LIBS) +AC_SUBST(GENERAL_LDFLAGS) +AC_SUBST(GENERAL_CFLAGS) +AC_SUBST(SYSTRE_LIBS) # Library functions AC_CHECK_FUNCS([clock_gettime gettimeofday memmove memset select strdup nanosleep]) @@ -366,6 +418,7 @@ AC_SUBST(SSL_LIBS) have_winsock=disabled WINSOCK_LIBS= + AC_ARG_WITH([winsock], [AS_HELP_STRING([--with-winsock], [support Windows Sockets API @<:@default=check@:>@])], @@ -376,10 +429,10 @@ if test "x$with_winsock" != "xno" then have_winsock=yes AC_CHECK_LIB([wsock32], [main], - [WINSOCK_LIBS="-lwsock32"] - [AC_DEFINE([ENABLE_WINSOCK],, + [WINSOCK_LIBS="-lwsock32"] + [AC_DEFINE([ENABLE_WINSOCK],, [Whether Windows Socket API support is enabled])], - [have_winsock=no]) + [have_winsock=no]) fi AM_CONDITIONAL([ENABLE_WINSOCK], [test "x${have_winsock}" = "xyes"]) @@ -522,7 +575,7 @@ then # Whether libvncserver was built against libgcrypt AC_CHECK_DECL([LIBVNCSERVER_WITH_CLIENT_GCRYPT], - [AC_CHECK_HEADER(gcrypt.h,, + [AC_CHECK_HEADER(gcrypt.h, [VNC_LIBS="$VNC_LIBS -lgcrypt"], [AC_MSG_WARN([ -------------------------------------------- libvncserver appears to be built against @@ -671,7 +724,7 @@ if test "x$with_rdp" != "xno" then have_freerdp2=yes PKG_CHECK_MODULES([RDP], [freerdp2 freerdp-client2 winpr2], - [CPPFLAGS="${RDP_CFLAGS} -Werror $CPPFLAGS"] + [CPPFLAGS="${RDP_CFLAGS} -Werror -Wexpansion-to-defined $CPPFLAGS"] [AS_IF([test "x${FREERDP2_PLUGIN_DIR}" = "x"], [FREERDP2_PLUGIN_DIR="`$PKG_CONFIG --variable=libdir freerdp2`/freerdp2"])], [AC_MSG_WARN([ @@ -949,7 +1002,7 @@ then # Whether libssh2 was built against libgcrypt AC_CHECK_LIB([ssh2], [gcry_control], [AC_CHECK_HEADER(gcrypt.h, - [AC_DEFINE([LIBSSH2_USES_GCRYPT],, + [AC_DEFINE([LIBSSH2_USES_GCRYPT],[SSH_LIBS="$SSH_LIBS -lgcrypt"], [Whether libssh2 was built against libgcrypt])], [AC_MSG_WARN([ -------------------------------------------- @@ -1196,6 +1249,7 @@ AC_CONFIG_FILES([Makefile src/protocols/kubernetes/Makefile src/protocols/kubernetes/tests/Makefile src/protocols/rdp/Makefile + src/protocols/rdp/guacxpstopdf/Makefile src/protocols/rdp/tests/Makefile src/protocols/ssh/Makefile src/protocols/telnet/Makefile diff --git a/src/common-ssh/Makefile.am b/src/common-ssh/Makefile.am index 5d1a88d153..0b3748108f 100644 --- a/src/common-ssh/Makefile.am +++ b/src/common-ssh/Makefile.am @@ -44,7 +44,7 @@ noinst_HEADERS = \ common-ssh/user.h libguac_common_ssh_la_CFLAGS = \ - -Werror -Wall -pedantic \ + @GENERAL_CFLAGS@ \ @COMMON_INCLUDE@ \ @LIBGUAC_INCLUDE@ @@ -54,5 +54,6 @@ libguac_common_ssh_la_LIBADD = \ libguac_common_ssh_la_LDFLAGS = \ @PTHREAD_LIBS@ \ @SSH_LIBS@ \ - @SSL_LIBS@ + @SSL_LIBS@ \ + @GENERAL_LDFLAGS@ diff --git a/src/common-ssh/ssh.c b/src/common-ssh/ssh.c index c4af066b88..2854cec020 100644 --- a/src/common-ssh/ssh.c +++ b/src/common-ssh/ssh.c @@ -35,16 +35,22 @@ #include #include -#include -#include #include -#include #include #include #include -#include #include +#ifdef WINDOWS_BUILD +#include +#include +#else +#include +#include +#include +#include +#endif + #ifdef LIBSSH2_USES_GCRYPT GCRY_THREAD_OPTION_PTHREAD_IMPL; #endif diff --git a/src/common-ssh/tests/Makefile.am b/src/common-ssh/tests/Makefile.am index 8424268a54..9bb30fcbe1 100644 --- a/src/common-ssh/tests/Makefile.am +++ b/src/common-ssh/tests/Makefile.am @@ -37,7 +37,7 @@ test_common_ssh_SOURCES = \ sftp/normalize_path.c test_common_ssh_CFLAGS = \ - -Werror -Wall -pedantic \ + @GENERAL_CFLAGS@ \ @COMMON_INCLUDE@ \ @COMMON_SSH_INCLUDE@ \ @LIBGUAC_INCLUDE@ diff --git a/src/common/Makefile.am b/src/common/Makefile.am index 947104aeb8..00fa9b5f63 100644 --- a/src/common/Makefile.am +++ b/src/common/Makefile.am @@ -63,7 +63,7 @@ libguac_common_la_SOURCES = \ surface.c libguac_common_la_CFLAGS = \ - -Werror -Wall -pedantic \ + @GENERAL_CFLAGS@ \ @LIBGUAC_INCLUDE@ libguac_common_la_LIBADD = \ diff --git a/src/common/tests/Makefile.am b/src/common/tests/Makefile.am index 27ac75cd10..f50e0a84f0 100644 --- a/src/common/tests/Makefile.am +++ b/src/common/tests/Makefile.am @@ -51,7 +51,8 @@ test_common_SOURCES = \ test_common_CFLAGS = \ -Werror -Wall -pedantic \ @COMMON_INCLUDE@ \ - @LIBGUAC_INCLUDE@ + @LIBGUAC_INCLUDE@ \ + @GENERAL_CFLAGS@ test_common_LDADD = \ @COMMON_LTLIB@ \ diff --git a/src/guacd/Makefile.am b/src/guacd/Makefile.am index 356f72f234..0219434568 100644 --- a/src/guacd/Makefile.am +++ b/src/guacd/Makefile.am @@ -38,7 +38,6 @@ noinst_HEADERS = \ conf-parse.h \ connection.h \ log.h \ - move-fd.h \ proc.h \ proc-map.h @@ -49,12 +48,20 @@ guacd_SOURCES = \ connection.c \ daemon.c \ log.c \ - move-fd.c \ proc.c \ proc-map.c +# Windows Build +if WINDOWS_BUILD +noinst_HEADERS += move-pipe.h +guacd_SOURCES += move-pipe.c +else +noinst_HEADERS += move-fd.h +guacd_SOURCES += move-fd.c +endif + guacd_CFLAGS = \ - -Werror -Wall -pedantic \ + @GENERAL_CFLAGS@ \ @COMMON_INCLUDE@ \ @LIBGUAC_INCLUDE@ diff --git a/src/guacd/connection.c b/src/guacd/connection.c index 2c8c00d65a..976c2fb736 100644 --- a/src/guacd/connection.c +++ b/src/guacd/connection.c @@ -21,13 +21,13 @@ #include "connection.h" #include "log.h" -#include "move-fd.h" #include "proc.h" #include "proc-map.h" #include #include #include +#include #include #include #include @@ -43,9 +43,67 @@ #include #include #include -#include #include +#ifdef WINDOWS_BUILD + +#include +#include +#include + +#include +#include + +#include "move-pipe.h" + +#else + +#include "move-fd.h" +#include + +#endif + +#ifdef WINDOWS_BUILD + +/** + * Behaves exactly as write(), but writes as much as possible, returning + * successfully only if the entire buffer was written. If the write fails for + * any reason, a negative value is returned. + * + * @param handle + * The file handle to write to. + * + * @param buffer + * The buffer containing the data to be written. + * + * @param length + * The number of bytes in the buffer to write. + * + * @return + * The number of bytes written, or -1 if an error occurs. As this function + * is guaranteed to write ALL bytes, this will always be the number of + * bytes specified by length unless an error occurs. + */ +static int __write_all(HANDLE handle, char* buffer, int length) { + + /* Repeatedly write to the handle until all data is written */ + while (length > 0) { + + DWORD written; + if (guac_write_to_handle(handle, buffer, length, &written)) + return -1; + + length -= written; + buffer += written; + + } + + return length; + +} + +#else + /** * Behaves exactly as write(), but writes as much as possible, returning * successfully only if the entire buffer was written. If the write fails for @@ -84,10 +142,12 @@ static int __write_all(int fd, char* buffer, int length) { } +#endif + /** * Continuously reads from a guac_socket, writing all data read to a file - * descriptor. Any data already buffered from that guac_socket by a given - * guac_parser is read first, prior to reading further data from the + * descriptor or handle. Any data already buffered from that guac_socket by a + * given guac_parser is read first, prior to reading further data from the * guac_socket. The provided guac_parser will be freed once its buffers have * been emptied, but the guac_socket will not. * @@ -112,7 +172,13 @@ static void* guacd_connection_write_thread(void* data) { /* Read all buffered data from parser first */ while ((length = guac_parser_shift(params->parser, buffer, sizeof(buffer))) > 0) { + +#ifdef WINDOWS_BUILD + if (__write_all(params->handle, buffer, length) < 0) +#else if (__write_all(params->fd, buffer, length) < 0) +#endif + break; } @@ -121,7 +187,13 @@ static void* guacd_connection_write_thread(void* data) { /* Transfer data from file descriptor to socket */ while ((length = guac_socket_read(params->socket, buffer, sizeof(buffer))) > 0) { + +#ifdef WINDOWS_BUILD + if (__write_all(params->handle, buffer, length) < 0) +#else if (__write_all(params->fd, buffer, length) < 0) +#endif + break; } @@ -134,24 +206,49 @@ void* guacd_connection_io_thread(void* data) { guacd_connection_io_thread_params* params = (guacd_connection_io_thread_params*) data; char buffer[8192]; - int length; - pthread_t write_thread; pthread_create(&write_thread, NULL, guacd_connection_write_thread, params); +#ifdef WINDOWS_BUILD + + /* Transfer data from file handle to socket */ + while (1) { + + DWORD bytes_read; + if (guac_read_from_handle(params->handle, buffer, sizeof(buffer), &bytes_read)) + break; + + if (guac_socket_write(params->socket, buffer, bytes_read)) + break; + + guac_socket_flush(params->socket); + + } + +#else + /* Transfer data from file descriptor to socket */ + int length; while ((length = read(params->fd, buffer, sizeof(buffer))) > 0) { if (guac_socket_write(params->socket, buffer, length)) break; guac_socket_flush(params->socket); } +#endif + /* Wait for write thread to die */ pthread_join(write_thread, NULL); /* Clean up */ guac_socket_free(params->socket); + +#ifdef WINDOWS_BUILD + CloseHandle(params->handle); +#else close(params->fd); +#endif + guac_mem_free(params); return NULL; @@ -183,6 +280,135 @@ void* guacd_connection_io_thread(void* data) { */ static int guacd_add_user(guacd_proc* proc, guac_parser* parser, guac_socket* socket) { +#ifdef WINDOWS_BUILD + + SECURITY_ATTRIBUTES attributes = { 0 }; + attributes.nLength = sizeof(SECURITY_ATTRIBUTES); + + /* + * Attempt to create a Windows security descriptor that grants access only + * to the owner of this process. + */ + if (!ConvertStringSecurityDescriptorToSecurityDescriptor( + + /* + * An SDDL string that uses DACL to grant the General Access (GA) + * permission, only to the owner (OW). For more, see + * https://learn.microsoft.com/en-us/windows/win32/secauthz/security-descriptor-string-format. + */ + "D:P(A;;GA;;;OW)", + SDDL_REVISION_1, + + /* The populated security descriptor output */ + &(attributes.lpSecurityDescriptor), + + /* There's no need to capture the descriptor size */ + NULL + + )) { + guacd_log(GUAC_LOG_ERROR, "Unable to initialize named pipe security descriptor."); + return 1; + } + + char pipe_name[GUAC_PIPE_NAME_LENGTH]; + + /* Required pipe name prefix */ + memcpy(pipe_name, PIPE_NAME_PREFIX, strlen(PIPE_NAME_PREFIX)); + + /* UUID to ensure the pipe name is unique */ + char* uuid = guac_generate_id('G'); + if (uuid == NULL) { + guacd_log(GUAC_LOG_ERROR, "Unable to generate UUID for pipe name."); + return 1; + } + + memcpy(pipe_name + strlen(PIPE_NAME_PREFIX), uuid, GUAC_UUID_LEN); + + /* Null terminator */ + pipe_name[GUAC_PIPE_NAME_LENGTH - 1] = '\0'; + + /* + * Set up a named pipe for communication with the user. For more, see + * https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-createnamedpipea + */ + HANDLE pipe_handle = CreateNamedPipe( + pipe_name, + + /* + * Read/write and "overlapped" (async) modes. PIPE_WAIT ensures + * that completion actions do not occur until data is actually + * ready, i.e. it's actually possible to wait for data. + */ + PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED, + + /* Allow only one instance of this named pipe to be opened. + * PIPE_WAIT ensures that completion actions do not occur until data + * is actually ready, i.e. it's actually possible to wait for data. + * Also, allow only connections from the local machine. + */ + PIPE_TYPE_BYTE | PIPE_WAIT | PIPE_REJECT_REMOTE_CLIENTS, + + /* Only this one instance of this named pipe is needed */ + 1, + + /* Output and input buffer sizes */ + 8192, 8192, + + /* Use the default timeout for the unused function WaitNamedPipe() */ + 0, + + /* Set our custom security descriptor to allow only owner usage */ + &attributes + + ); + + LocalFree(attributes.lpSecurityDescriptor); + + if (pipe_handle == INVALID_HANDLE_VALUE) { + guacd_log(GUAC_LOG_ERROR, "Unable to create named pipe for IPC."); + return 1; + } + + /* If pipe creation failed, the error will already have been logged */ + if (pipe_handle == NULL) + return 1; + + /* Send pipe name to process so it can connect to the pipe */ + if (!guacd_send_pipe(proc->fd_socket, pipe_name)) { + CloseHandle(pipe_handle); + guacd_log(GUAC_LOG_ERROR, "Unable to add user."); + return 1; + } + + /* Create an event to monitor for pipe connection */ + HANDLE event = CreateEvent(NULL, FALSE, FALSE, NULL); + if (event == NULL) { + guacd_log(GUAC_LOG_ERROR, "Event creation failed."); + return 1; + } + + /* Wait for the other end of the pipe to connect before attempting IO */ + OVERLAPPED overlapped = { 0 }; + overlapped.hEvent = event; + ConnectNamedPipe(pipe_handle, &overlapped); + + /* Wait for 1 second for the other end to be connected */ + DWORD result = WaitForSingleObject(event, 1000); + if (result == WAIT_FAILED) { + + /* + * If the wait failed for any reason other than the pipe being + * already connected + */ + if (GetLastError() != ERROR_PIPE_CONNECTED) { + guacd_log(GUAC_LOG_ERROR, "Named pipe connection not established."); + return 1; + } + + } + +#else + int sockets[2]; /* Set up socket pair */ @@ -203,10 +429,17 @@ static int guacd_add_user(guacd_proc* proc, guac_parser* parser, guac_socket* so /* Close our end of the process file descriptor */ close(proc_fd); +#endif + guacd_connection_io_thread_params* params = guac_mem_alloc(sizeof(guacd_connection_io_thread_params)); params->parser = parser; params->socket = socket; + +#ifdef WINDOWS_BUILD + params->handle = pipe_handle; +#else params->fd = user_fd; +#endif /* Start I/O thread */ pthread_t io_thread; diff --git a/src/guacd/connection.h b/src/guacd/connection.h index 08a75266dc..24e9d24e97 100644 --- a/src/guacd/connection.h +++ b/src/guacd/connection.h @@ -28,6 +28,10 @@ #include #endif +#ifdef WINDOWS_BUILD +#include +#endif + /** * Parameters required by each connection thread. */ @@ -92,12 +96,25 @@ typedef struct guacd_connection_io_thread_params { */ guac_socket* socket; + +#ifdef WINDOWS_BUILD + + /** + * The named pipe handle which is being handled by a guac_socket within the + * connection-specific process. + */ + HANDLE handle; + +#else + /** * The file descriptor which is being handled by a guac_socket within the * connection-specific process. */ int fd; +#endif + } guacd_connection_io_thread_params; /** diff --git a/src/guacd/daemon.c b/src/guacd/daemon.c index 8bf81482ef..26e053cfda 100644 --- a/src/guacd/daemon.c +++ b/src/guacd/daemon.c @@ -35,19 +35,24 @@ #include #include #include -#include -#include #include #include #include #include #include -#include #include #include #include #include +#ifdef WINDOWS_BUILD +#include +#else +#include +#include +#include +#endif + #define GUACD_DEV_NULL "/dev/null" #define GUACD_ROOT "/" diff --git a/src/guacd/log.c b/src/guacd/log.c index 81c313b86f..159588d9d4 100644 --- a/src/guacd/log.c +++ b/src/guacd/log.c @@ -25,6 +25,7 @@ #include #include +#include #include #include @@ -89,8 +90,9 @@ void vguacd_log(guac_client_log_level level, const char* format, syslog(priority, "%s", message); /* Log to STDERR */ + pid_t pid = getpid(); fprintf(stderr, GUACD_LOG_NAME "[%i]: %s:\t%s\n", - getpid(), priority_name, message); + pid, priority_name, message); } @@ -117,10 +119,11 @@ void guacd_log_guac_error(guac_client_log_level level, const char* message) { guac_error_message); /* Otherwise just log with standard status string */ - else - guacd_log(level, "%s: %s", - message, - guac_status_string(guac_error)); + else { + char* status_string = guac_status_string(guac_error); + guacd_log(level, "%s: %s", message, status_string); + free(status_string); + } } @@ -140,10 +143,13 @@ void guacd_log_handshake_failure() { "Guacamole protocol violation. Perhaps the version of " "guacamole-client is incompatible with this version of " "guacd?"); - else + else { + + char* status_string = guac_status_string(guac_error); guacd_log(GUAC_LOG_WARNING, - "Guacamole handshake failed: %s", - guac_status_string(guac_error)); + "Guacamole handshake failed: %s", status_string); + free(status_string); + } } diff --git a/src/guacd/move-pipe.c b/src/guacd/move-pipe.c new file mode 100755 index 0000000000..b46f54f323 --- /dev/null +++ b/src/guacd/move-pipe.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 "log.h" +#include "move-pipe.h" + +#include +#include +#include +#include +#include +#include +#include + +#include + +/* Windows headers */ +#include +#include +#include +#include +#include + +int guacd_send_pipe(int sock, char* pipe_name) { + + /* Assign data buffer */ + struct iovec io_vector[1]; + io_vector[0].iov_base = pipe_name; + io_vector[0].iov_len = GUAC_PIPE_NAME_LENGTH; + + struct msghdr message = {0}; + message.msg_iov = io_vector; + message.msg_iovlen = 1; + + /* Send pipe name */ + return (sendmsg(sock, &message, 0) == GUAC_PIPE_NAME_LENGTH); + +} + +HANDLE guacd_recv_pipe(int sock) { + + /* Assign data buffer */ + char pipe_name[GUAC_PIPE_NAME_LENGTH]; + struct iovec io_vector[1]; + io_vector[0].iov_base = pipe_name; + io_vector[0].iov_len = GUAC_PIPE_NAME_LENGTH; + + struct msghdr message = {0}; + message.msg_iov = io_vector; + message.msg_iovlen = 1; + + /* Receive file descriptor */ + if (recvmsg(sock, &message, 0) == GUAC_PIPE_NAME_LENGTH) { + + /* + * Make sure the value is always null-terminated, even if an invalid + * name was sent. + */ + pipe_name[GUAC_PIPE_NAME_LENGTH - 1] = '\0'; + + return CreateFile( + + pipe_name, + + /* Desired access level */ + GENERIC_READ | GENERIC_WRITE, + + /* Sharing level - do not allow any other usage of this pipe */ + 0, + + /* Default security mode - do not allow child processes to inherir */ + NULL, + + /* Open the existing pipe; don't try to create a new one */ + OPEN_EXISTING, + + /* Open in "overlapped" (async) mode */ + FILE_FLAG_NO_BUFFERING | FILE_FLAG_OVERLAPPED, + + /* Ignored for existing pipes */ + NULL + ); + + } /* end if recvmsg() success */ + + /* Failed to get the pipe */ + return NULL; + +} + diff --git a/src/guacd/move-pipe.h b/src/guacd/move-pipe.h new file mode 100755 index 0000000000..1107a26dd5 --- /dev/null +++ b/src/guacd/move-pipe.h @@ -0,0 +1,74 @@ +/* + * 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 GUACD_MOVE_HANDLE_H +#define GUACD_MOVE_HANDLE_H + +#include +#include + +/* + * The required prefix for all pipe names in Windows. + */ +#define PIPE_NAME_PREFIX "\\\\.\\pipe\\" + +/* + * The length of a named pipe as used by guacamole. Every pipe name will consist + * of PIPE_NAME_PREFIX, plus a the length of a UUID as returned from + * guac_generate_id(), plus a null-terminator. + */ +#define GUAC_PIPE_NAME_LENGTH (strlen(PIPE_NAME_PREFIX) + GUAC_UUID_LEN + 1) + +/** + * Sends the given pipe name along the given socket. Returns non-zero on success, + * zero on error, just as a normal call to sendmsg() would. If an error does occur, + * GetLastError() will return the appropriate error. + * + * @param sock + * The file descriptor of an open UNIX domain socket along which the pipe + * name specified by pipe_name should be sent. + * + * @param pipe_name + * The null-terminated name of the pipe to send across the socket. The name + * MUST be GUAC_PIPE_NAME_LENGTH characters long, and end with a null + * terminator. + * + * @return + * Non-zero if the send operation succeeded, zero on error. + */ +int guacd_send_pipe(int sock, char* pipe_name); + +/** + * Waits for a pipe name on the given socket, returning a handle to the client + * end of the named pipe with that name. The pipe name must have been sent via + * guacd_send_pipe_name. If an error occurs, NULL is returned, and GetLastError() + * will return the appropriate error. + * + * @param sock + * The file descriptor of an open UNIX domain socket along which the file + * handle will be sent (by guacd_send_handle()). + * + * @return + * The handle to the client end of the named pipe if the operation succeeded, + * NULL otherwise. + */ +HANDLE guacd_recv_pipe(int sock); + +#endif + diff --git a/src/guacd/proc.c b/src/guacd/proc.c index 986d3596a8..3da138d328 100644 --- a/src/guacd/proc.c +++ b/src/guacd/proc.c @@ -41,9 +41,20 @@ #include #include #include -#include #include +// Don't do it +#include + +#ifdef WINDOWS_BUILD +#include "move-pipe.h" +#include "guacamole/socket-handle.h" +#include +#include +#else +#include +#endif + /** * Parameters for the user thread. */ @@ -54,11 +65,22 @@ typedef struct guacd_user_thread_params { */ guacd_proc* proc; +#ifdef WINDOWS_BUILD + + /** + * The file handle for communicating with the joining user. + */ + HANDLE handle; + +#else + /** * The file descriptor of the joining user's socket. */ int fd; +#endif + /** * Whether the joining user is the connection owner. */ @@ -85,9 +107,16 @@ static void* guacd_user_thread(void* data) { guac_client* client = proc->client; /* Get guac_socket for user's file descriptor */ +#ifdef WINDOWS_BUILD + guac_socket* socket = guac_socket_open_handle(params->handle); +#else guac_socket* socket = guac_socket_open(params->fd); - if (socket == NULL) +#endif + + if (socket == NULL) { + free(params); return NULL; + } /* Create skeleton user */ guac_user* user = guac_user_alloc(); @@ -113,6 +142,39 @@ static void* guacd_user_thread(void* data) { } +#ifdef WINDOWS_BUILD + +/** + * Begins a new user connection under a given process, using the given file + * handle. The connection will be managed by a separate and detached thread + * which is started by this function. + * + * @param proc + * The process that the user is being added to. + + * @param handle + * The handle associated with the user's connection to guacd. + * + * @param owner + * Non-zero if the user is the owner of the connection being joined (they + * are the first user to join), or zero otherwise. + */ +static void guacd_proc_add_user(guacd_proc* proc, HANDLE handle, int owner) { + + guacd_user_thread_params* params = malloc(sizeof(guacd_user_thread_params)); + params->proc = proc; + params->handle = handle; + params->owner = owner; + + /* Start user thread */ + pthread_t user_thread; + int the_return = pthread_create(&user_thread, NULL, guacd_user_thread, params); + pthread_detach(user_thread); + +} + +#else + /** * Begins a new user connection under a given process, using the given file * descriptor. The connection will be managed by a separate and detached thread @@ -143,6 +205,8 @@ static void guacd_proc_add_user(guacd_proc* proc, int fd, int owner) { } +#endif + /** * Forcibly kills all processes within the current process group, including the * current process and all child processes. This function is only safe to call @@ -361,17 +425,29 @@ static void guacd_exec_proc(guacd_proc* proc, const char* protocol) { sigaction(SIGINT, &signal_stop_action, NULL); sigaction(SIGTERM, &signal_stop_action, NULL); +#ifdef WINDOWS_BUILD + + /* Add each received file handle as a new user */ + HANDLE handle; + while ((handle = guacd_recv_pipe(proc->fd_socket)) != NULL) { + + guacd_proc_add_user(proc, handle, owner); + +#else + /* Add each received file descriptor as a new user */ int received_fd; while ((received_fd = guacd_recv_fd(proc->fd_socket)) != -1) { guacd_proc_add_user(proc, received_fd, owner); +#endif + /* Future file descriptors are not owners */ owner = 0; } - + cleanup_client: /* Request client to stop/disconnect */ diff --git a/src/guacenc/Makefile.am b/src/guacenc/Makefile.am index 34aeaf6a56..eb8a547c30 100644 --- a/src/guacenc/Makefile.am +++ b/src/guacenc/Makefile.am @@ -89,6 +89,7 @@ endif guacenc_CFLAGS = \ -Werror -Wall \ + @GENERAL_CFLAGS@ \ @AVCODEC_CFLAGS@ \ @AVFORMAT_CFLAGS@ \ @AVUTIL_CFLAGS@ \ diff --git a/src/guacenc/encode.c b/src/guacenc/encode.c index 3de9d796eb..e68b540365 100644 --- a/src/guacenc/encode.c +++ b/src/guacenc/encode.c @@ -72,8 +72,11 @@ static int guacenc_read_instructions(guacenc_display* display, /* Fail on read/parse error */ if (guac_error != GUAC_STATUS_CLOSED) { - guacenc_log(GUAC_LOG_ERROR, "%s: %s", - path, guac_status_string(guac_error)); + + char* status_string = guac_status_string(guac_error); + guacenc_log(GUAC_LOG_ERROR, "%s: %s", path, status_string); + free(status_string); + guac_parser_free(parser); return 1; } @@ -132,8 +135,11 @@ int guacenc_encode(const char* path, const char* out_path, const char* codec, /* Obtain guac_socket wrapping file descriptor */ guac_socket* socket = guac_socket_open(fd); if (socket == NULL) { - guacenc_log(GUAC_LOG_ERROR, "%s: %s", path, - guac_status_string(guac_error)); + + char* status_string = guac_status_string(guac_error); + guacenc_log(GUAC_LOG_ERROR, "%s: %s", path, status_string); + free(status_string); + close(fd); guacenc_display_free(display); return 1; diff --git a/src/guaclog/Makefile.am b/src/guaclog/Makefile.am index af5c5a7623..591f9d9152 100644 --- a/src/guaclog/Makefile.am +++ b/src/guaclog/Makefile.am @@ -49,6 +49,7 @@ guaclog_SOURCES = \ guaclog_CFLAGS = \ -Werror -Wall \ + @GENERAL_CFLAGS@ \ @LIBGUAC_INCLUDE@ guaclog_LDADD = \ diff --git a/src/guaclog/interpret.c b/src/guaclog/interpret.c index d996c24551..8c35521b04 100644 --- a/src/guaclog/interpret.c +++ b/src/guaclog/interpret.c @@ -32,6 +32,7 @@ #include #include #include +#include #include #include @@ -69,8 +70,9 @@ static int guaclog_read_instructions(guaclog_state* state, /* Fail on read/parse error */ if (guac_error != GUAC_STATUS_CLOSED) { - guaclog_log(GUAC_LOG_ERROR, "%s: %s", - path, guac_status_string(guac_error)); + char* status_string = guac_status_string(guac_error); + guaclog_log(GUAC_LOG_ERROR, "%s: %s", path, status_string); + free(status_string); guac_parser_free(parser); return 1; } @@ -127,8 +129,11 @@ int guaclog_interpret(const char* path, const char* out_path, bool force) { /* Obtain guac_socket wrapping file descriptor */ guac_socket* socket = guac_socket_open(fd); if (socket == NULL) { - guaclog_log(GUAC_LOG_ERROR, "%s: %s", path, - guac_status_string(guac_error)); + + char* status_string = guac_status_string(guac_error); + guaclog_log(GUAC_LOG_ERROR, "%s: %s", path, status_string); + free(status_string); + close(fd); guaclog_state_free(state); return 1; diff --git a/src/libguac/Makefile.am b/src/libguac/Makefile.am index baea6e8634..ec6847b9ff 100644 --- a/src/libguac/Makefile.am +++ b/src/libguac/Makefile.am @@ -50,6 +50,7 @@ libguacinc_HEADERS = \ guacamole/error-types.h \ guacamole/fips.h \ guacamole/hash.h \ + guacamole/id.h \ guacamole/layer.h \ guacamole/layer-types.h \ guacamole/mem.h \ @@ -98,12 +99,12 @@ libguacprivinc_HEADERS = \ # noinst_HEADERS = \ - id.h \ - encode-jpeg.h \ - encode-png.h \ - palette.h \ - user-handlers.h \ - raw_encoder.h \ + client-internal.h \ + encode-jpeg.h \ + encode-png.h \ + palette.h \ + user-handlers.h \ + raw_encoder.h \ wait-fd.h libguac_la_SOURCES = \ @@ -135,7 +136,7 @@ libguac_la_SOURCES = \ user.c \ user-handlers.c \ user-handshake.c \ - wait-fd.c \ + wait-fd.c \ wol.c # Compile WebP support if available @@ -157,7 +158,7 @@ libguacinc_HEADERS += guacamole/socket-wsa.h endif libguac_la_CFLAGS = \ - -Werror -Wall -pedantic + @GENERAL_CFLAGS@ libguac_la_LDFLAGS = \ -version-info 23:0:0 \ @@ -174,3 +175,14 @@ libguac_la_LDFLAGS = \ @WEBP_LIBS@ \ @WINSOCK_LIBS@ +# Windows Build +if WINDOWS_BUILD +libguac_la_SOURCES += handle-helpers.c +libguac_la_SOURCES += socket-handle.c +libguac_la_SOURCES += wait-handle.c +libguacinc_HEADERS += guacamole/socket-handle.h +libguacinc_HEADERS += guacamole/handle-helpers.h +libguacinc_HEADERS += guacamole/pipe.h +libguacinc_HEADERS += wait-handle.h +endif + diff --git a/src/libguac/client-internal.h b/src/libguac/client-internal.h new file mode 100644 index 0000000000..ec4a77db80 --- /dev/null +++ b/src/libguac/client-internal.h @@ -0,0 +1,135 @@ +/* + * 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_CLIENT_INTERNAL_H +#define _GUAC_CLIENT_INTERNAL_H + +/** + * Internal-only members of the guac_client struct. + * + * @file client-internal.h + */ + +#include "config.h" + +#include "guacamole/pool-types.h" +#include "guacamole/rwlock.h" +#include "guacamole/stream-types.h" +#include "guacamole/user-types.h" + +#include +#include +#include + +#ifdef WINDOWS_BUILD +#include +#endif + +struct guac_client_internal { + + /** + * Pool of buffer indices. Buffers are simply layers with negative indices. + * Note that because guac_pool always gives non-negative indices starting + * at 0, the output of this guac_pool will be adjusted. + */ + guac_pool* __buffer_pool; + + /** + * Pool of layer indices. Note that because guac_pool always gives + * non-negative indices starting at 0, the output of this guac_pool will + * be adjusted. + */ + guac_pool* __layer_pool; + + /** + * Pool of stream indices. + */ + guac_pool* __stream_pool; + + /** + * All available client-level output streams (data going to all connected + * users). + */ + guac_stream* __output_streams; + + /** + * Lock which is acquired when the users list is being manipulated, or when + * the users list is being iterated. + */ + guac_rwlock __users_lock; + + /** + * The first user within the list of all connected users, or NULL if no + * users are currently connected. + */ + guac_user* __users; + + /** + * Lock which is acquired when the pending users list is being manipulated, + * or when the pending users list is being iterated. + */ + guac_rwlock __pending_users_lock; + + /** + * A timer that will periodically synchronize the list of pending users, + * emptying the list once synchronization is complete. Only for internal + * use within the client. This will be NULL until the first user joins + * the connection, as it is lazily instantiated at that time. + */ +#ifdef WINDOWS_BUILD + HANDLE __pending_users_timer; +#else + timer_t __pending_users_timer; +#endif + + /** + * A flag storing the current state of the pending users timer. + */ + int __pending_users_timer_state; + + /** + * A mutex that must be acquired before modifying or checking the value of + * the timer state. + */ + pthread_mutex_t __pending_users_timer_mutex; + + /** + * The first user within the list of connected users who have not yet had + * their connection states synchronized after joining. + */ + guac_user* __pending_users; + + /** + * The user that first created this connection. This user will also have + * their "owner" flag set to a non-zero value. If the owner has left the + * connection, this will be NULL. + */ + guac_user* __owner; + + /** + * Handle to the dlopen()'d plugin, which should be given to dlclose() when + * this client is freed. This is only assigned if guac_client_load_plugin() + * is used. + */ + void* __plugin_handle; + +}; + +#endif + diff --git a/src/libguac/client.c b/src/libguac/client.c index 46796ed1fa..51a104ec63 100644 --- a/src/libguac/client.c +++ b/src/libguac/client.c @@ -19,6 +19,7 @@ #include "config.h" +#include "client-internal.h" #include "encode-jpeg.h" #include "encode-png.h" #include "encode-webp.h" @@ -35,7 +36,7 @@ #include "guacamole/string.h" #include "guacamole/timestamp.h" #include "guacamole/user.h" -#include "id.h" +#include #include #include @@ -47,6 +48,11 @@ #include #include +#ifdef WINDOWS_BUILD +#include +#include +#endif + /** * The number of nanoseconds between times that the pending users list will be * synchronized and emptied (250 milliseconds aka 1/4 second). @@ -86,7 +92,7 @@ guac_layer* guac_client_alloc_layer(guac_client* client) { /* Init new layer */ guac_layer* allocd_layer = guac_mem_alloc(sizeof(guac_layer)); - allocd_layer->index = guac_pool_next_int(client->__layer_pool)+1; + allocd_layer->index = guac_pool_next_int(client->internal->__layer_pool)+1; return allocd_layer; @@ -96,7 +102,7 @@ guac_layer* guac_client_alloc_buffer(guac_client* client) { /* Init new layer */ guac_layer* allocd_layer = guac_mem_alloc(sizeof(guac_layer)); - allocd_layer->index = -guac_pool_next_int(client->__buffer_pool) - 1; + allocd_layer->index = -guac_pool_next_int(client->internal->__buffer_pool) - 1; return allocd_layer; @@ -105,7 +111,7 @@ guac_layer* guac_client_alloc_buffer(guac_client* client) { void guac_client_free_buffer(guac_client* client, guac_layer* layer) { /* Release index to pool */ - guac_pool_free_int(client->__buffer_pool, -layer->index - 1); + guac_pool_free_int(client->internal->__buffer_pool, -layer->index - 1); /* Free layer */ guac_mem_free(layer); @@ -115,7 +121,7 @@ void guac_client_free_buffer(guac_client* client, guac_layer* layer) { void guac_client_free_layer(guac_client* client, guac_layer* layer) { /* Release index to pool */ - guac_pool_free_int(client->__layer_pool, layer->index); + guac_pool_free_int(client->internal->__layer_pool, layer->index); /* Free layer */ guac_mem_free(layer); @@ -128,14 +134,14 @@ guac_stream* guac_client_alloc_stream(guac_client* client) { int stream_index; /* Refuse to allocate beyond maximum */ - if (client->__stream_pool->active == GUAC_CLIENT_MAX_STREAMS) + if (client->internal->__stream_pool->active == GUAC_CLIENT_MAX_STREAMS) return NULL; /* Allocate stream */ - stream_index = guac_pool_next_int(client->__stream_pool); + stream_index = guac_pool_next_int(client->internal->__stream_pool); /* Initialize stream with odd index (even indices are user-level) */ - allocd_stream = &(client->__output_streams[stream_index]); + allocd_stream = &(client->internal->__output_streams[stream_index]); allocd_stream->index = (stream_index * 2) + 1; allocd_stream->data = NULL; allocd_stream->ack_handler = NULL; @@ -149,7 +155,7 @@ guac_stream* guac_client_alloc_stream(guac_client* client) { void guac_client_free_stream(guac_client* client, guac_stream* stream) { /* Release index to pool */ - guac_pool_free_int(client->__stream_pool, (stream->index - 1) / 2); + guac_pool_free_int(client->internal->__stream_pool, (stream->index - 1) / 2); /* Mark stream as closed */ stream->index = GUAC_CLIENT_CLOSED_STREAM_INDEX; @@ -162,32 +168,46 @@ void guac_client_free_stream(guac_client* client, guac_stream* stream) { * * @param data * The client for which all pending users should be promoted. + * + * @param timerOrWaitFired + * Unused - Windows only. */ -static void guac_client_promote_pending_users(union sigval data) { +static void guac_client_promote_pending_users( +#ifdef WINDOWS_BUILD + LPVOID data, + BOOLEAN timerOrWaitFired +#else + union sigval data +#endif + ) { +#ifdef WINDOWS_BUILD + guac_client* client = (guac_client*) data; +#else guac_client* client = (guac_client*) data.sival_ptr; +#endif - pthread_mutex_lock(&(client->__pending_users_timer_mutex)); + pthread_mutex_lock(&(client->internal->__pending_users_timer_mutex)); /* Check if the previous instance of this handler is still running */ int already_running = ( - client->__pending_users_timer_state + client->internal->__pending_users_timer_state == GUAC_CLIENT_PENDING_TIMER_TRIGGERED); /* Mark the handler as running if it isn't already */ - client->__pending_users_timer_state = GUAC_CLIENT_PENDING_TIMER_TRIGGERED; + client->internal->__pending_users_timer_state = GUAC_CLIENT_PENDING_TIMER_TRIGGERED; - pthread_mutex_unlock(&(client->__pending_users_timer_mutex)); + pthread_mutex_unlock(&(client->internal->__pending_users_timer_mutex)); /* Do not start the handler if the previous instance is still running */ if (already_running) return; /* Acquire the lock for reading and modifying the list of pending users */ - guac_rwlock_acquire_write_lock(&(client->__pending_users_lock)); + guac_rwlock_acquire_write_lock(&(client->internal->__pending_users_lock)); /* Skip user promotion entirely if there's no pending users */ - if (client->__pending_users == NULL) + if (client->internal->__pending_users == NULL) goto promotion_complete; /* Run the pending join handler, if one is defined */ @@ -206,7 +226,7 @@ static void guac_client_promote_pending_users(union sigval data) { } /* The first pending user in the list, if any */ - guac_user* first_user = client->__pending_users; + guac_user* first_user = client->internal->__pending_users; /* The final user in the list, if any */ guac_user* last_user = first_user; @@ -219,35 +239,35 @@ static void guac_client_promote_pending_users(union sigval data) { } /* Mark the list as empty */ - client->__pending_users = NULL; + client->internal->__pending_users = NULL; /* Acquire the lock for reading and modifying the list of full users. */ - guac_rwlock_acquire_write_lock(&(client->__users_lock)); + guac_rwlock_acquire_write_lock(&(client->internal->__users_lock)); /* If any users were removed from the pending list, promote them now */ if (last_user != NULL) { /* Add all formerly-pending users to the start of the user list */ - if (client->__users != NULL) - client->__users->__prev = last_user; + if (client->internal->__users != NULL) + client->internal->__users->__prev = last_user; - last_user->__next = client->__users; - client->__users = first_user; + last_user->__next = client->internal->__users; + client->internal->__users = first_user; } - guac_rwlock_release_lock(&(client->__users_lock)); + guac_rwlock_release_lock(&(client->internal->__users_lock)); promotion_complete: /* Release the lock (this is done AFTER updating the connected user list * to ensure that all users are always on exactly one of these lists) */ - guac_rwlock_release_lock(&(client->__pending_users_lock)); + guac_rwlock_release_lock(&(client->internal->__pending_users_lock)); /* Mark the handler as complete so the next instance can run */ - pthread_mutex_lock(&(client->__pending_users_timer_mutex)); - client->__pending_users_timer_state = GUAC_CLIENT_PENDING_TIMER_REGISTERED; - pthread_mutex_unlock(&(client->__pending_users_timer_mutex)); + pthread_mutex_lock(&(client->internal->__pending_users_timer_mutex)); + client->internal->__pending_users_timer_state = GUAC_CLIENT_PENDING_TIMER_REGISTERED; + pthread_mutex_unlock(&(client->internal->__pending_users_timer_mutex)); } @@ -266,6 +286,10 @@ guac_client* guac_client_alloc() { /* Init new client */ memset(client, 0, sizeof(guac_client)); + /* Init internal struct */ + guac_client_internal* internal = guac_mem_zalloc(sizeof(guac_client_internal)); + client->internal = internal; + client->args = __GUAC_CLIENT_NO_ARGS; client->state = GUAC_CLIENT_RUNNING; client->last_sent_timestamp = guac_timestamp_current(); @@ -278,32 +302,32 @@ guac_client* guac_client_alloc() { } /* Allocate buffer and layer pools */ - client->__buffer_pool = guac_pool_alloc(GUAC_BUFFER_POOL_INITIAL_SIZE); - client->__layer_pool = guac_pool_alloc(GUAC_BUFFER_POOL_INITIAL_SIZE); + client->internal->__buffer_pool = guac_pool_alloc(GUAC_BUFFER_POOL_INITIAL_SIZE); + client->internal->__layer_pool = guac_pool_alloc(GUAC_BUFFER_POOL_INITIAL_SIZE); /* Allocate stream pool */ - client->__stream_pool = guac_pool_alloc(0); + client->internal->__stream_pool = guac_pool_alloc(0); /* Initialize streams */ - client->__output_streams = guac_mem_alloc(sizeof(guac_stream), GUAC_CLIENT_MAX_STREAMS); + client->internal->__output_streams = guac_mem_alloc(sizeof(guac_stream), GUAC_CLIENT_MAX_STREAMS); for (i=0; i__output_streams[i].index = GUAC_CLIENT_CLOSED_STREAM_INDEX; + client->internal->__output_streams[i].index = GUAC_CLIENT_CLOSED_STREAM_INDEX; } /* Init locks */ - guac_rwlock_init(&(client->__users_lock)); - guac_rwlock_init(&(client->__pending_users_lock)); + guac_rwlock_init(&(client->internal->__users_lock)); + guac_rwlock_init(&(client->internal->__pending_users_lock)); /* Initialize the write lock flags to 0, as threads won't have yet */ - pthread_key_create(&(client->__users_lock.key), (void *) 0); - pthread_key_create(&(client->__pending_users_lock.key), (void *) 0); + pthread_key_create(&(client->internal->__users_lock.key), (void *) 0); + pthread_key_create(&(client->internal->__pending_users_lock.key), (void *) 0); /* The timer will be lazily created in the child process */ - client->__pending_users_timer_state = GUAC_CLIENT_PENDING_TIMER_UNREGISTERED; + client->internal->__pending_users_timer_state = GUAC_CLIENT_PENDING_TIMER_UNREGISTERED; /* Set up the pending user promotion mutex */ - pthread_mutex_init(&(client->__pending_users_timer_mutex), NULL); + pthread_mutex_init(&(client->internal->__pending_users_timer_mutex), NULL); /* Set up broadcast sockets */ client->socket = guac_socket_broadcast(client); @@ -316,20 +340,20 @@ guac_client* guac_client_alloc() { void guac_client_free(guac_client* client) { /* Acquire write locks before referencing user pointers */ - guac_rwlock_acquire_write_lock(&(client->__pending_users_lock)); - guac_rwlock_acquire_write_lock(&(client->__users_lock)); + guac_rwlock_acquire_write_lock(&(client->internal->__pending_users_lock)); + guac_rwlock_acquire_write_lock(&(client->internal->__users_lock)); /* Remove all pending users */ - while (client->__pending_users != NULL) - guac_client_remove_user(client, client->__pending_users); + while (client->internal->__pending_users != NULL) + guac_client_remove_user(client, client->internal->__pending_users); /* Remove all users */ - while (client->__users != NULL) - guac_client_remove_user(client, client->__users); + while (client->internal->__users != NULL) + guac_client_remove_user(client, client->internal->__users); /* Release the locks */ - guac_rwlock_release_lock(&(client->__users_lock)); - guac_rwlock_release_lock(&(client->__pending_users_lock)); + guac_rwlock_release_lock(&(client->internal->__users_lock)); + guac_rwlock_release_lock(&(client->internal->__pending_users_lock)); if (client->free_handler) { @@ -343,38 +367,44 @@ void guac_client_free(guac_client* client) { guac_socket_free(client->pending_socket); /* Free layer pools */ - guac_pool_free(client->__buffer_pool); - guac_pool_free(client->__layer_pool); + guac_pool_free(client->internal->__buffer_pool); + guac_pool_free(client->internal->__layer_pool); /* Free streams */ - guac_mem_free(client->__output_streams); + guac_mem_free(client->internal->__output_streams); /* Free stream pool */ - guac_pool_free(client->__stream_pool); + guac_pool_free(client->internal->__stream_pool); /* Close associated plugin */ - if (client->__plugin_handle != NULL) { - if (dlclose(client->__plugin_handle)) + if (client->internal->__plugin_handle != NULL) { + if (dlclose(client->internal->__plugin_handle)) guac_client_log(client, GUAC_LOG_ERROR, "Unable to close plugin: %s", dlerror()); } /* Find out if the pending user promotion timer was ever started */ - pthread_mutex_lock(&(client->__pending_users_timer_mutex)); + pthread_mutex_lock(&(client->internal->__pending_users_timer_mutex)); int was_started = ( - client->__pending_users_timer_state + client->internal->__pending_users_timer_state != GUAC_CLIENT_PENDING_TIMER_UNREGISTERED); - pthread_mutex_unlock(&(client->__pending_users_timer_mutex)); + pthread_mutex_unlock(&(client->internal->__pending_users_timer_mutex)); /* If the timer was registered, stop it before destroying the lock */ - if (was_started) - timer_delete(client->__pending_users_timer); + if (was_started) { +#ifdef WINDOWS_BUILD + DeleteTimerQueueTimer(NULL, client->internal->__pending_users_timer, NULL); +#else + timer_delete(client->internal->__pending_users_timer); +#endif + } - pthread_mutex_destroy(&(client->__pending_users_timer_mutex)); + pthread_mutex_destroy(&(client->internal->__pending_users_timer_mutex)); /* Destroy the reentrant read-write locks */ - guac_rwlock_destroy(&(client->__users_lock)); - guac_rwlock_destroy(&(client->__pending_users_lock)); + guac_rwlock_destroy(&(client->internal->__users_lock)); + guac_rwlock_destroy(&(client->internal->__pending_users_lock)); + guac_mem_free(client->internal); guac_mem_free(client->connection_id); guac_mem_free(client); } @@ -452,21 +482,21 @@ static void guac_client_add_pending_user( guac_client* client, guac_user* user) { /* Acquire the lock for modifying the list of pending users */ - guac_rwlock_acquire_write_lock(&(client->__pending_users_lock)); + guac_rwlock_acquire_write_lock(&(client->internal->__pending_users_lock)); user->__prev = NULL; - user->__next = client->__pending_users; + user->__next = client->internal->__pending_users; - if (client->__pending_users != NULL) - client->__pending_users->__prev = user; + if (client->internal->__pending_users != NULL) + client->internal->__pending_users->__prev = user; - client->__pending_users = user; + client->internal->__pending_users = user; /* Increment the user count */ client->connected_users++; /* Release the lock */ - guac_rwlock_release_lock(&(client->__pending_users_lock)); + guac_rwlock_release_lock(&(client->internal->__pending_users_lock)); } @@ -485,15 +515,31 @@ static void guac_client_add_pending_user( */ static int guac_client_start_pending_users_timer(guac_client* client) { - pthread_mutex_lock(&(client->__pending_users_timer_mutex)); + pthread_mutex_lock(&(client->internal->__pending_users_timer_mutex)); /* Return success if the timer is already created and running */ - if (client->__pending_users_timer_state + if (client->internal->__pending_users_timer_state != GUAC_CLIENT_PENDING_TIMER_UNREGISTERED) { - pthread_mutex_unlock(&(client->__pending_users_timer_mutex)); + pthread_mutex_unlock(&(client->internal->__pending_users_timer_mutex)); return 0; } +#ifdef WINDOWS_BUILD + + int refresh_millis = GUAC_CLIENT_PENDING_USERS_REFRESH_INTERVAL / 1000; + if (!CreateTimerQueueTimer( + &(client->internal->__pending_users_timer), + NULL, + guac_client_promote_pending_users, + client, + refresh_millis, + refresh_millis, + 0)) { + + return 1; + } +#else + /* Configure the timer to synchronize and clear the pending users */ struct sigevent signal_config = { .sigev_notify = SIGEV_THREAD, @@ -504,8 +550,8 @@ static int guac_client_start_pending_users_timer(guac_client* client) { if (timer_create( CLOCK_MONOTONIC, &signal_config, - &(client->__pending_users_timer))) { - pthread_mutex_unlock(&(client->__pending_users_timer_mutex)); + &(client->internal->__pending_users_timer))) { + pthread_mutex_unlock(&(client->internal->__pending_users_timer_mutex)); return 1; } @@ -517,16 +563,17 @@ static int guac_client_start_pending_users_timer(guac_client* client) { /* Start the timer */ if (timer_settime( - client->__pending_users_timer, 0, &time_config, NULL) < 0) { - timer_delete(client->__pending_users_timer); - pthread_mutex_unlock(&(client->__pending_users_timer_mutex)); + client->internal->__pending_users_timer, 0, &time_config, NULL) < 0) { + timer_delete(client->internal->__pending_users_timer); + pthread_mutex_unlock(&(client->internal->__pending_users_timer_mutex)); return 1; } +#endif /* Mark the timer as registered but not yet running */ - client->__pending_users_timer_state = GUAC_CLIENT_PENDING_TIMER_REGISTERED; + client->internal->__pending_users_timer_state = GUAC_CLIENT_PENDING_TIMER_REGISTERED; - pthread_mutex_unlock(&(client->__pending_users_timer_mutex)); + pthread_mutex_unlock(&(client->internal->__pending_users_timer_mutex)); return 0; } @@ -562,7 +609,7 @@ int guac_client_add_user(guac_client* client, guac_user* user, int argc, char** /* Update owner pointer if user is owner */ if (user->owner) - client->__owner = user; + client->internal->__owner = user; } @@ -576,16 +623,16 @@ int guac_client_add_user(guac_client* client, guac_user* user, int argc, char** void guac_client_remove_user(guac_client* client, guac_user* user) { - guac_rwlock_acquire_write_lock(&(client->__pending_users_lock)); - guac_rwlock_acquire_write_lock(&(client->__users_lock)); + guac_rwlock_acquire_write_lock(&(client->internal->__pending_users_lock)); + guac_rwlock_acquire_write_lock(&(client->internal->__users_lock)); /* Update prev / head */ if (user->__prev != NULL) user->__prev->__next = user->__next; - else if (client->__users == user) - client->__users = user->__next; - else if (client->__pending_users == user) - client->__pending_users = user->__next; + else if (client->internal->__users == user) + client->internal->__users = user->__next; + else if (client->internal->__pending_users == user) + client->internal->__pending_users = user->__next; /* Update next */ if (user->__next != NULL) @@ -595,10 +642,10 @@ void guac_client_remove_user(guac_client* client, guac_user* user) { /* Update owner pointer if user was owner */ if (user->owner) - client->__owner = NULL; + client->internal->__owner = NULL; - guac_rwlock_release_lock(&(client->__users_lock)); - guac_rwlock_release_lock(&(client->__pending_users_lock)); + guac_rwlock_release_lock(&(client->internal->__users_lock)); + guac_rwlock_release_lock(&(client->internal->__pending_users_lock)); /* Update owner of user having left the connection. */ if (!user->owner) @@ -616,16 +663,16 @@ void guac_client_foreach_user(guac_client* client, guac_user_callback* callback, guac_user* current; - guac_rwlock_acquire_read_lock(&(client->__users_lock)); + guac_rwlock_acquire_read_lock(&(client->internal->__users_lock)); /* Call function on each user */ - current = client->__users; + current = client->internal->__users; while (current != NULL) { callback(current, data); current = current->__next; } - guac_rwlock_release_lock(&(client->__users_lock)); + guac_rwlock_release_lock(&(client->internal->__users_lock)); } @@ -634,16 +681,16 @@ void guac_client_foreach_pending_user( guac_user* current; - guac_rwlock_acquire_read_lock(&(client->__pending_users_lock)); + guac_rwlock_acquire_read_lock(&(client->internal->__pending_users_lock)); /* Call function on each pending user */ - current = client->__pending_users; + current = client->internal->__pending_users; while (current != NULL) { callback(current, data); current = current->__next; } - guac_rwlock_release_lock(&(client->__pending_users_lock)); + guac_rwlock_release_lock(&(client->internal->__pending_users_lock)); } @@ -652,12 +699,12 @@ void* guac_client_for_owner(guac_client* client, guac_user_callback* callback, void* retval; - guac_rwlock_acquire_read_lock(&(client->__users_lock)); + guac_rwlock_acquire_read_lock(&(client->internal->__users_lock)); /* Invoke callback with current owner */ - retval = callback(client->__owner, data); + retval = callback(client->internal->__owner, data); - guac_rwlock_release_lock(&(client->__users_lock)); + guac_rwlock_release_lock(&(client->internal->__users_lock)); /* Return value from callback */ return retval; @@ -672,10 +719,10 @@ void* guac_client_for_user(guac_client* client, guac_user* user, int user_valid = 0; void* retval; - guac_rwlock_acquire_read_lock(&(client->__users_lock)); + guac_rwlock_acquire_read_lock(&(client->internal->__users_lock)); /* Loop through all users, searching for a pointer to the given user */ - current = client->__users; + current = client->internal->__users; while (current != NULL) { /* If the user's pointer exists in the list, they are indeed valid */ @@ -694,7 +741,7 @@ void* guac_client_for_user(guac_client* client, guac_user* user, /* Invoke callback with requested user (if they exist) */ retval = callback(user, data); - guac_rwlock_release_lock(&(client->__users_lock)); + guac_rwlock_release_lock(&(client->internal->__users_lock)); /* Return value from callback */ return retval; @@ -765,7 +812,7 @@ int guac_client_load_plugin(guac_client* client, const char* protocol) { } /* Init client */ - client->__plugin_handle = client_plugin_handle; + client->internal->__plugin_handle = client_plugin_handle; return alias.client_init(client); @@ -914,6 +961,7 @@ void guac_client_stream_webp(guac_client* client, guac_socket* socket, cairo_surface_t* surface, int quality, int lossless) { #ifdef ENABLE_WEBP + /* Allocate new stream for image */ guac_stream* stream = guac_client_alloc_stream(client); @@ -922,10 +970,10 @@ void guac_client_stream_webp(guac_client* client, guac_socket* socket, /* Write WebP data */ guac_webp_write(socket, stream, surface, quality, lossless); - /* Terminate stream */ guac_protocol_send_end(socket, stream); + /* Free allocated stream */ guac_client_free_stream(client, stream); #else diff --git a/src/libguac/error.c b/src/libguac/error.c index 21d341869e..c01413286d 100644 --- a/src/libguac/error.c +++ b/src/libguac/error.c @@ -30,6 +30,12 @@ #include #endif +#ifdef WINDOWS_BUILD +#include +#include +#include +#endif + /* * Error strings */ @@ -59,109 +65,214 @@ const char* __GUAC_STATUS_REFUSED_STR = "Operation refused"; const char* __GUAC_STATUS_TOO_MANY_STR = "Insufficient resources"; const char* __GUAC_STATUS_WOULD_BLOCK_STR = "Operation would block"; -const char* guac_status_string(guac_status status) { +#ifdef WINDOWS_BUILD + +/* + * A format for a backup message in case Windows can't produce one. + */ +#define FALLBACK_ERROR_FORMAT "Failed to get Windows error for code %ul" + +/* + * The size of newly constructed fallback error buffers. Large enough to + * contain the longest possible DWORD and a null terminator. + */ +#define FALLBACK_ERROR_SIZE 64 + +/** + * Get the error message associated with the most recent Windows error. This + * message is allocated fresh every time this function is called, and must be + * freed by the caller. + * + */ +static LPTSTR get_last_error_message() { + + /* This will be set to point to the buffer allocated by FormatMessage */ + LPTSTR error_buffer = NULL; + + /* Attempt to format message using system error message tables */ + DWORD length = FormatMessage( + + /* Fetch error messages from system message tables */ + FORMAT_MESSAGE_FROM_SYSTEM + + /* Allocate a buffer to hold the message text */ + |FORMAT_MESSAGE_ALLOCATE_BUFFER + + /* We're not supplying our own text inserts */ + |FORMAT_MESSAGE_IGNORE_INSERTS, + + /* Don't pass any custom message tables */ + NULL, + + /* Pass the error code in to get the error message */ + guac_windows_error_code, + + /* Use default language settings */ + MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), + + /* + * Cast the address of the buffer back to a LPTSTR, even though it's + * really a LPTSTR*. The function will internally treat it as a LPTSTR*, + * populating `errorText` with the message text. + */ + (LPTSTR)&error_buffer, + + /* Minimum output buffer size */ + 0, + + /* No extra arguments */ + NULL + ); + + /* If Windows failed to generate an error message */ + if (!length) { + + /* Allocate and format the fallback error message */ + error_buffer = malloc(FALLBACK_ERROR_SIZE * sizeof(TCHAR)); + + /* Format the message */ + sprintf(error_buffer, FALLBACK_ERROR_FORMAT, guac_windows_error_code); + } + + return error_buffer; + +} + +#endif + +/** + * Allocate and return a copy of the provided null-terminated string constant. + * + * @param string_constant + * The null-terminated string constant to duplicated. + * + * @return + * A newly allocated copy of the provided string constant. + */ +static char* guac_status_duplicate_constant(const char* string_constant) { + + /* Create a buffer to hold the message plus null terminator */ + char* string_copy = malloc((strlen(string_constant) + 1) * sizeof(char)); + + /* Copy the constant into the buffer */ + strcpy(string_copy, string_constant); + + /* A newly-allocated copy of the constant */ + return string_copy; + +} + +char* guac_status_string(guac_status status) { switch (status) { /* No error */ case GUAC_STATUS_SUCCESS: - return __GUAC_STATUS_SUCCESS_STR; + return guac_status_duplicate_constant(__GUAC_STATUS_SUCCESS_STR); /* Out of memory */ case GUAC_STATUS_NO_MEMORY: - return __GUAC_STATUS_NO_MEMORY_STR; + return guac_status_duplicate_constant(__GUAC_STATUS_NO_MEMORY_STR); /* End of stream */ case GUAC_STATUS_CLOSED: - return __GUAC_STATUS_CLOSED_STR; + return guac_status_duplicate_constant(__GUAC_STATUS_CLOSED_STR); /* Timeout */ case GUAC_STATUS_TIMEOUT: - return __GUAC_STATUS_TIMEOUT_STR; + return guac_status_duplicate_constant(__GUAC_STATUS_TIMEOUT_STR); /* Further information in errno */ case GUAC_STATUS_SEE_ERRNO: - return strerror(errno); + return guac_status_duplicate_constant(strerror(errno)); + +#ifdef WINDOWS_BUILD + + /* Further information available from the Windows API. */ + case GUAC_STATUS_SEE_WINDOWS_ERROR: + return get_last_error_message(); + +#endif /* Input/output error */ case GUAC_STATUS_IO_ERROR: - return __GUAC_STATUS_IO_ERROR_STR; + return guac_status_duplicate_constant(__GUAC_STATUS_IO_ERROR_STR); /* Invalid argument */ case GUAC_STATUS_INVALID_ARGUMENT: - return __GUAC_STATUS_INVALID_ARGUMENT_STR; + return guac_status_duplicate_constant(__GUAC_STATUS_INVALID_ARGUMENT_STR); /* Internal error */ case GUAC_STATUS_INTERNAL_ERROR: - return __GUAC_STATUS_INTERNAL_ERROR_STR; + return guac_status_duplicate_constant(__GUAC_STATUS_INTERNAL_ERROR_STR); /* Out of space */ case GUAC_STATUS_NO_SPACE: - return __GUAC_STATUS_NO_SPACE_STR; + return guac_status_duplicate_constant(__GUAC_STATUS_NO_SPACE_STR); /* Input too large */ case GUAC_STATUS_INPUT_TOO_LARGE: - return __GUAC_STATUS_INPUT_TOO_LARGE_STR; + return guac_status_duplicate_constant(__GUAC_STATUS_INPUT_TOO_LARGE_STR); /* Result too large */ case GUAC_STATUS_RESULT_TOO_LARGE: - return __GUAC_STATUS_RESULT_TOO_LARGE_STR; + return guac_status_duplicate_constant(__GUAC_STATUS_RESULT_TOO_LARGE_STR); /* Permission denied */ case GUAC_STATUS_PERMISSION_DENIED: - return __GUAC_STATUS_PERMISSION_DENIED_STR; + return guac_status_duplicate_constant(__GUAC_STATUS_PERMISSION_DENIED_STR); /* Resource is busy */ case GUAC_STATUS_BUSY: - return __GUAC_STATUS_BUSY_STR; + return guac_status_duplicate_constant(__GUAC_STATUS_BUSY_STR); /* Resource not available */ case GUAC_STATUS_NOT_AVAILABLE: - return __GUAC_STATUS_NOT_AVAILABLE_STR; + return guac_status_duplicate_constant(__GUAC_STATUS_NOT_AVAILABLE_STR); /* Not supported */ case GUAC_STATUS_NOT_SUPPORTED: - return __GUAC_STATUS_NOT_SUPPORTED_STR; + return guac_status_duplicate_constant(__GUAC_STATUS_NOT_SUPPORTED_STR); /* Not implemented */ case GUAC_STATUS_NOT_INPLEMENTED: - return __GUAC_STATUS_NOT_INPLEMENTED_STR; + return guac_status_duplicate_constant(__GUAC_STATUS_NOT_INPLEMENTED_STR); /* Temporary failure */ case GUAC_STATUS_TRY_AGAIN: - return __GUAC_STATUS_TRY_AGAIN_STR; + return guac_status_duplicate_constant(__GUAC_STATUS_TRY_AGAIN_STR); /* Guacamole protocol error */ case GUAC_STATUS_PROTOCOL_ERROR: - return __GUAC_STATUS_PROTOCOL_ERROR_STR; + return guac_status_duplicate_constant(__GUAC_STATUS_PROTOCOL_ERROR_STR); /* Resource not found */ case GUAC_STATUS_NOT_FOUND: - return __GUAC_STATUS_NOT_FOUND_STR; + return guac_status_duplicate_constant(__GUAC_STATUS_NOT_FOUND_STR); /* Operation canceled */ case GUAC_STATUS_CANCELED: - return __GUAC_STATUS_CANCELED_STR; + return guac_status_duplicate_constant(__GUAC_STATUS_CANCELED_STR); /* Value out of range */ case GUAC_STATUS_OUT_OF_RANGE: - return __GUAC_STATUS_OUT_OF_RANGE_STR; + return guac_status_duplicate_constant(__GUAC_STATUS_OUT_OF_RANGE_STR); /* Operation refused */ case GUAC_STATUS_REFUSED: - return __GUAC_STATUS_REFUSED_STR; + return guac_status_duplicate_constant(__GUAC_STATUS_REFUSED_STR); /* Too many resource in use */ case GUAC_STATUS_TOO_MANY: - return __GUAC_STATUS_TOO_MANY_STR; + return guac_status_duplicate_constant(__GUAC_STATUS_TOO_MANY_STR); /* Operation would block */ case GUAC_STATUS_WOULD_BLOCK: - return __GUAC_STATUS_WOULD_BLOCK_STR; + return guac_status_duplicate_constant(__GUAC_STATUS_WOULD_BLOCK_STR); /* Unknown status code */ default: - return __GUAC_STATUS_UNKNOWN_STATUS_STR; + return guac_status_duplicate_constant(__GUAC_STATUS_UNKNOWN_STATUS_STR); } @@ -219,10 +330,10 @@ guac_status* __guac_error() { } -const char** __guac_error_message() { +char** __guac_error_message() { /* Pointer for thread-local data */ - const char** message; + char** message; /* Init error message key, if not already initialized */ pthread_once( @@ -231,7 +342,7 @@ const char** __guac_error_message() { ); /* Retrieve thread-local message variable */ - message = (const char**) pthread_getspecific(__guac_error_message_key); + message = (char**) pthread_getspecific(__guac_error_message_key); /* Allocate thread-local message variable if not already allocated */ if (message == NULL) { @@ -243,22 +354,89 @@ const char** __guac_error_message() { } +#ifdef WINDOWS_BUILD + +/** + * PThread key for the thread local Windows error code associated with + * the current guac_status, if the status is GUAC_STATUS_SEE_WINDOWS_ERROR. + */ +static pthread_key_t guac_windows_code_key; + +/** + * A static pthread_once_t instance to ensure that the Windows error code + * thread local is only created once. + */ +static pthread_once_t guac_windows_code_key_init = PTHREAD_ONCE_INIT; + +/** + * Allocate the PThread key for the Windows error code associated with the + * current error. + */ +static void guac_alloc_windows_code_key() { + + /* Create key, destroy any allocated variable on thread exit */ + pthread_key_create(&guac_windows_code_key, __guac_mem_free_pointer); + +} + +DWORD* __guac_windows_code() { + + /* Pointer for thread-local data */ + DWORD* windows_code; + + /* Init error code key, if not already initialized */ + pthread_once(&guac_windows_code_key_init, guac_alloc_windows_code_key); + + /* Get the value of the current thread local */ + windows_code = (DWORD*) pthread_getspecific(guac_windows_code_key); + + /* Allocate thread-local error code variable if not already allocated */ + if (windows_code == NULL) { + windows_code = malloc(sizeof(DWORD)); + pthread_setspecific(guac_windows_code_key, windows_code); + } + + return windows_code; + +} + +#endif + #else /* Default (not-threadsafe) implementation */ static guac_status __guac_error_unsafe_storage; -static const char** __guac_error_message_unsafe_storage; +static char** __guac_error_message_unsafe_storage; guac_status* __guac_error() { return &__guac_error_unsafe_storage; } -const char** __guac_error_message() { +char** __guac_error_message() { return &__guac_error_message_unsafe_storage; } /* Warn about threadsafety */ #warn No threadsafe implementation of __guac_error exists for your platform, so a default non-threadsafe implementation has been used instead. This may lead to incorrect status codes being reported for failures. Please consider adding support for your platform, or filing a bug report with the Guacamole project. +#ifdef WINDOWS_BUILD + +/** + * Non-threadsafe storage for the Windows error code. + */ +static DWORD __guac_windows_error_unsafe_storage; + +/** + * Return non-threadsafe Windows error code. + * + * @return + * The non-threadsafe Windows error code. + */ +DWORD* __guac_windows_code() { + return &__guac_windows_error_unsafe_storage; +} + +#endif + #endif diff --git a/src/libguac/guacamole/client.h b/src/libguac/guacamole/client.h index 8582389ee6..e8e9a16d07 100644 --- a/src/libguac/guacamole/client.h +++ b/src/libguac/guacamole/client.h @@ -26,6 +26,8 @@ * @file client.h */ +#include "config.h" + #include "client-fntypes.h" #include "client-types.h" #include "client-constants.h" @@ -45,6 +47,15 @@ #include #include +#ifdef WINDOWS_BUILD +#include +#endif + +/** + * Internal values for libguac use only. + */ +typedef struct guac_client_internal guac_client_internal; + struct guac_client { /** @@ -134,31 +145,6 @@ struct guac_client { */ guac_client_log_handler* log_handler; - /** - * Pool of buffer indices. Buffers are simply layers with negative indices. - * Note that because guac_pool always gives non-negative indices starting - * at 0, the output of this guac_pool will be adjusted. - */ - guac_pool* __buffer_pool; - - /** - * Pool of layer indices. Note that because guac_pool always gives - * non-negative indices starting at 0, the output of this guac_pool will - * be adjusted. - */ - guac_pool* __layer_pool; - - /** - * Pool of stream indices. - */ - guac_pool* __stream_pool; - - /** - * All available client-level output streams (data going to all connected - * users). - */ - guac_stream* __output_streams; - /** * The unique identifier allocated for the connection, which may * be used within the Guacamole protocol to refer to this connection. @@ -168,56 +154,6 @@ struct guac_client { */ char* connection_id; - /** - * Lock which is acquired when the users list is being manipulated, or when - * the users list is being iterated. - */ - guac_rwlock __users_lock; - - /** - * The first user within the list of all connected users, or NULL if no - * users are currently connected. - */ - guac_user* __users; - - /** - * Lock which is acquired when the pending users list is being manipulated, - * or when the pending users list is being iterated. - */ - guac_rwlock __pending_users_lock; - - /** - * A timer that will periodically synchronize the list of pending users, - * emptying the list once synchronization is complete. Only for internal - * use within the client. This will be NULL until the first user joins - * the connection, as it is lazily instantiated at that time. - */ - timer_t __pending_users_timer; - - /** - * A flag storing the current state of the pending users timer. - */ - int __pending_users_timer_state; - - /** - * A mutex that must be acquired before modifying or checking the value of - * the timer state. - */ - pthread_mutex_t __pending_users_timer_mutex; - - /** - * The first user within the list of connected users who have not yet had - * their connection states synchronized after joining. - */ - guac_user* __pending_users; - - /** - * The user that first created this connection. This user will also have - * their "owner" flag set to a non-zero value. If the owner has left the - * connection, this will be NULL. - */ - guac_user* __owner; - /** * The number of currently-connected users. This value may include inactive * users if cleanup of those users has not yet finished. @@ -308,11 +244,9 @@ struct guac_client { const char** args; /** - * Handle to the dlopen()'d plugin, which should be given to dlclose() when - * this client is freed. This is only assigned if guac_client_load_plugin() - * is used. + * Internal-only client data. */ - void* __plugin_handle; + guac_client_internal* internal; }; diff --git a/src/libguac/guacamole/error-types.h b/src/libguac/guacamole/error-types.h index 4cf3499eaf..995f64fe06 100644 --- a/src/libguac/guacamole/error-types.h +++ b/src/libguac/guacamole/error-types.h @@ -20,6 +20,8 @@ #ifndef _GUAC_ERROR_TYPES_H #define _GUAC_ERROR_TYPES_H +#include "config.h" + /** * Type definitions related to return values and errors. * @@ -57,6 +59,16 @@ typedef enum guac_status { * stored in errno. */ GUAC_STATUS_SEE_ERRNO, + +#ifdef WINDOWS_BUILD + + /** + * An error occurred, and further information about the error can be + * retrieved using the GetLastError function. + */ + GUAC_STATUS_SEE_WINDOWS_ERROR, + +#endif /** * An I/O error prevented the operation from succeeding. diff --git a/src/libguac/guacamole/error.h b/src/libguac/guacamole/error.h index a511d315ff..af3a5ee598 100644 --- a/src/libguac/guacamole/error.h +++ b/src/libguac/guacamole/error.h @@ -30,10 +30,15 @@ #include "error-types.h" +#ifdef WINDOWS_BUILD +#include +#endif + /** - * Returns a human-readable explanation of the status code given. + * Returns a newly-allocated, null-terminated, and human-readable explanation + * of the status code given. */ -const char* guac_status_string(guac_status status); +char* guac_status_string(guac_status status); /** * Returns the status code associated with the error which occurred during the @@ -58,37 +63,24 @@ guac_status* __guac_error(); */ #define guac_error_message (*__guac_error_message()) -const char** __guac_error_message(); +char** __guac_error_message(); -/** - * Returns a human-readable explanation of the status code given. - */ -const char* guac_status_string(guac_status status); +#ifdef WINDOWS_BUILD /** - * Returns the status code associated with the error which occurred during the - * last function call. This value will only be set by functions documented to - * use it (most libguac functions), and is undefined if no error occurred. + * Returns an error code describing the Windows error that occured when + * attempting the Windows function call that induced the guac_error status + * being set to GUAC_STATUS_SEE_WINDOWS_ERROR. This value is meaningless if + * any other guac status is set. * - * The storage of this value is thread-local. Assignment of a status code to - * guac_error in one thread will not affect its value in another thread. + * The storage of this value is thread-local. Assignment of an error code in + * one thread will not affect its value in another thread. */ -#define guac_error (*__guac_error()) +#define guac_windows_error_code (*__guac_windows_code()) -guac_status* __guac_error(); +DWORD* __guac_windows_code(); -/** - * Returns a message describing the error which occurred during the last - * function call. If an error occurred, but no message is associated with it, - * NULL is returned. This value is undefined if no error occurred. - * - * The storage of this value is thread-local. Assignment of a message to - * guac_error_message in one thread will not affect its value in another - * thread. - */ -#define guac_error_message (*__guac_error_message()) - -const char** __guac_error_message(); +#endif #endif diff --git a/src/libguac/guacamole/handle-helpers.h b/src/libguac/guacamole/handle-helpers.h new file mode 100755 index 0000000000..b1f768de18 --- /dev/null +++ b/src/libguac/guacamole/handle-helpers.h @@ -0,0 +1,83 @@ +/* + * 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_HANDLE_HELPERS_H +#define GUAC_HANDLE_HELPERS_H + +#include + +/** + * Attempt to read the provided count of bytes from the provided handle, into the + * provided buffer. If the read is successful, 0 will be returned, and the actual + * number of bytes written to the buffer will be saved to the provided + * num_bytes_read pointer. If an error occurs while attempting to read from the + * handle, or while waiting on the results of the read attempt, the error code + * (as returned by GetLastError()) will be returned. + * + * @param handle + * The handle to read from. This handle MUST have been opened in overlapped + * mode. + * + * @param buffer + * The buffer to write the data into. It must be at least `count` bytes. + * + * @param count + * The maximum number of bytes to read from the handle. + * + * @param num_bytes_read + * The actual number of bytes read from the handle. This value is valid only + * if this function returns successfully. This value may be less than `count`. + * + * @return + * Zero on success, or the failure code (as returned by GetLastError()) if + * the read attempt, or the wait on that read attempt fails. + */ +int guac_read_from_handle( + HANDLE handle, void* buffer, DWORD count, DWORD* num_bytes_read); + +/** + * Attempt to wrtie the provided count of bytes to the provided handle, from the + * provided buffer. If the write is successful, 0 will be returned, and the actual + * number of bytes written to the handle will be saved to the provided + * num_bytes_written pointer. If an error occurs while attempting to write to the + * handle, or while waiting on the results of the write attempt, the error code + * (as returned by GetLastError()) will be returned. + * + * @param handle + * The handle to write to. This handle MUST have been opened in overlapped + * mode. + * + * @param buffer + * The buffer to write to the handle. It must be at least `count` bytes. + * + * @param count + * The maximum numer of bytes to write to the handle. + * + * @param num_bytes_written + * The actual number of bytes written to the handle. This value is valid only + * if this function returns successfully. This value may be less than `count`. + * + * @return + * Zero on success, or the failure code (as returned by GetLastError()) if + * the write attempt, or the wait on that write attempt fails. + */ +int guac_write_to_handle( + HANDLE handle, const void* buffer, DWORD count, DWORD* num_bytes_written); + +#endif \ No newline at end of file diff --git a/src/libguac/id.h b/src/libguac/guacamole/id.h similarity index 89% rename from src/libguac/id.h rename to src/libguac/guacamole/id.h index 4a54cdbfbe..adf43daaa4 100644 --- a/src/libguac/id.h +++ b/src/libguac/guacamole/id.h @@ -20,6 +20,12 @@ #ifndef __GUAC_ID_H #define __GUAC_ID_H +/** + * The length of a guac UUID in bytes. All UUIDs returned by guac_generate_id() + * are guaranteed to be 37 1-byte characters long. + */ +#define GUAC_UUID_LEN 37 + /** * Generates a guaranteed-unique identifier which is a total of 37 characters * long, having the given single-character prefix. The resulting identifier diff --git a/src/libguac/guacamole/pipe.h b/src/libguac/guacamole/pipe.h new file mode 100644 index 0000000000..4010c03bb7 --- /dev/null +++ b/src/libguac/guacamole/pipe.h @@ -0,0 +1,38 @@ +/* + * 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_PIPE_H +#define _GUAC_PIPE_H + +#include +#include + +/** + * The default amount of memory to dedicate to the pipe. For more info, see + * https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/pipe. + */ +#define DEFAULT_PIPE_MEMORY 8092 + +/** + * Replicate the behavior of the default posix pipe() function by deferring to + * the windows _pipe() API, with some sensible defaults. + */ +#define pipe(fds) (_pipe((fds), DEFAULT_PIPE_MEMORY, O_BINARY)) + +#endif diff --git a/src/libguac/guacamole/plugin-constants.h b/src/libguac/guacamole/plugin-constants.h index a67f5f54cb..db25033a87 100644 --- a/src/libguac/guacamole/plugin-constants.h +++ b/src/libguac/guacamole/plugin-constants.h @@ -20,12 +20,28 @@ #ifndef _GUAC_PLUGIN_CONSTANTS_H #define _GUAC_PLUGIN_CONSTANTS_H +#include "config.h" + /** * Constants related to client plugins. * * @file plugin-constants.h */ +#ifdef WINDOWS_BUILD + +/** + * String prefix which begins the library filename of all client plugins. + */ +#define GUAC_PROTOCOL_LIBRARY_PREFIX "msys-client-" + +/** + * String suffix which ends the library filename of all client plugins. + */ +#define GUAC_PROTOCOL_LIBRARY_SUFFIX "-0.dll" + +#else + /** * String prefix which begins the library filename of all client plugins. */ @@ -36,6 +52,8 @@ */ #define GUAC_PROTOCOL_LIBRARY_SUFFIX ".so" +#endif + /** * The maximum number of characters (COUNTING NULL TERMINATOR) to allow * for protocol names within the library filename of client plugins. diff --git a/src/libguac/guacamole/socket-handle.h b/src/libguac/guacamole/socket-handle.h new file mode 100644 index 0000000000..56c3c0c773 --- /dev/null +++ b/src/libguac/guacamole/socket-handle.h @@ -0,0 +1,50 @@ +/* + * 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_SOCKET_HANDLE_H +#define GUAC_SOCKET_HANDLE_H + +/** + * Provides an implementation of guac_socket specific to the Windows file handle + * API. This header willonly be available if libguac was built with Cgywin. + * + * @file socket-handle.h + */ + +#include "socket-types.h" + +#include + +/** + * Creates a new guac_socket which will use the Windows handle API for all + * communication. Freeing this guac_socket will automatically close the + * associated handle. + * + * @param handle + * The handle to use for the communicating with the connection underlying + * the created guac_socket. + * + * @return + * A newly-allocated guac_socket which will transparently use the Windows + * handle API for all communication. + */ +guac_socket* guac_socket_open_handle(HANDLE handle); + +#endif + diff --git a/src/libguac/handle-helpers.c b/src/libguac/handle-helpers.c new file mode 100755 index 0000000000..3c48dc795a --- /dev/null +++ b/src/libguac/handle-helpers.c @@ -0,0 +1,118 @@ +/* + * 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 "guacamole/error.h" +#include "guacamole/handle-helpers.h" + +#include +#include +#include +#include + +#include + +int guac_read_from_handle( + HANDLE handle, void* buffer, DWORD count, DWORD* num_bytes_read) { + + /* + * Overlapped structure and associated event for waiting on async call. + * The event isn't used by this function, but is required in order to + * reliably wait on an async operation anyway - see + * https://learn.microsoft.com/en-us/windows/win32/api/ioapiset/nf-ioapiset-getoverlappedresult#remarks. + */ + OVERLAPPED overlapped = { 0 }; + overlapped.hEvent = CreateEvent(NULL, FALSE, FALSE, NULL); + if (overlapped.hEvent == NULL) + return GetLastError(); + + /* Attempt to start the async read operation */ + if (!ReadFile(handle, buffer, count, NULL, &overlapped)) { + + DWORD error = GetLastError(); + + /* + * If an error other than the expected ERROR_IO_PENDING happens, + * return it as the error code immediately. + */ + if (error != ERROR_IO_PENDING) { + CloseHandle(overlapped.hEvent); + return error; + } + + } + + /* + * Wait on the result of the read. If any error occurs when waiting, + * return the error. + */ + if (!GetOverlappedResult(handle, &overlapped, num_bytes_read, TRUE)) { + DWORD error = GetLastError(); + CloseHandle(overlapped.hEvent); + return error; + } + + /* No errors occured, so the read was successful */ + CloseHandle(overlapped.hEvent); + return 0; + +} + +int guac_write_to_handle( + HANDLE handle, const void* buffer, DWORD count, DWORD* num_bytes_written) { + + /* + * Overlapped structure and associated event for waiting on async call. + */ + OVERLAPPED overlapped = { 0 }; + overlapped.hEvent = CreateEvent(NULL, FALSE, FALSE, NULL); + if (overlapped.hEvent == NULL) + return GetLastError(); + + /* Attempt to start the async write operation */ + if (!WriteFile(handle, buffer, count, NULL, &overlapped)) { + + DWORD error = GetLastError(); + + /* + * If an error other than the expected ERROR_IO_PENDING happens, + * return it as the error code immediately. + */ + if (error != ERROR_IO_PENDING) { + CloseHandle(overlapped.hEvent); + return error; + } + + } + + /* + * Wait on the result of the write. If any error occurs when waiting, + * return the error. + */ + if (!GetOverlappedResult(handle, &overlapped, num_bytes_written, TRUE)) { + DWORD error = GetLastError(); + CloseHandle(overlapped.hEvent); + return error; + } + + /* No errors occured, so the write was successful */ + CloseHandle(overlapped.hEvent); + return 0; + +} \ No newline at end of file diff --git a/src/libguac/id.c b/src/libguac/id.c index 75cde9627f..7d09a5d50f 100644 --- a/src/libguac/id.c +++ b/src/libguac/id.c @@ -21,9 +21,13 @@ #include "guacamole/mem.h" #include "guacamole/error.h" -#include "id.h" +#include -#if defined(HAVE_LIBUUID) + +#ifdef WINDOWS_BUILD +#include +#include +#elif defined(HAVE_LIBUUID) #include #elif defined(HAVE_OSSP_UUID_H) #include @@ -33,17 +37,43 @@ #include -/** - * The length of a UUID in bytes. All UUIDs are guaranteed to be 36 1-byte - * characters long. - */ -#define GUAC_UUID_LEN 36 - char* guac_generate_id(char prefix) { char* buffer; char* identifier; + /* Allocate buffer for future formatted ID */ + buffer = malloc(GUAC_UUID_LEN + 1); + if (buffer == NULL) { + guac_error = GUAC_STATUS_NO_MEMORY; + guac_error_message = "Could not allocate memory for unique ID"; + return NULL; + } + + identifier = &(buffer[1]); + +#ifdef WINDOWS_BUILD + + /* Generate a UUID using a built in windows function */ + UUID uuid; + UuidCreate(&uuid); + + /* Convert the UUID to an all-caps, null-terminated tring */ + RPC_CSTR uuid_string; + if (UuidToString(&uuid, &uuid_string) == RPC_S_OUT_OF_MEMORY) { + guac_error = GUAC_STATUS_NO_MEMORY; + guac_error_message = "Could not allocate memory for unique ID"; + return NULL; + } + + /* Copy over lowercase letters to the final target string */ + for (int i = 0; i < GUAC_UUID_LEN; i++) + identifier[i] = tolower(uuid_string[i]); + + RpcStringFree(&uuid_string); + +#else + /* Prepare object to receive generated UUID */ #ifdef HAVE_LIBUUID uuid_t uuid; @@ -85,7 +115,7 @@ char* guac_generate_id(char prefix) { #ifdef HAVE_LIBUUID uuid_unparse_lower(uuid, identifier); #else - size_t identifier_length = GUAC_UUID_LEN + 1; + size_t identifier_length = GUAC_UUID_LEN; if (uuid_export(uuid, UUID_FMT_STR, &identifier, &identifier_length) != UUID_RC_OK) { guac_mem_free(buffer); uuid_destroy(uuid); @@ -96,10 +126,11 @@ char* guac_generate_id(char prefix) { /* Clean up generated UUID */ uuid_destroy(uuid); +#endif #endif buffer[0] = prefix; - buffer[GUAC_UUID_LEN + 1] = '\0'; + buffer[GUAC_UUID_LEN] = '\0'; return buffer; } diff --git a/src/libguac/recording.c b/src/libguac/recording.c index 7544cc1441..d1e8ccb54f 100644 --- a/src/libguac/recording.c +++ b/src/libguac/recording.c @@ -37,6 +37,10 @@ #include #include +#ifdef WINDOWS_BUILD +#include +#endif + /** * Attempts to open a new recording within the given path and having the given * name. If such a file already exists, sequential numeric suffixes (.1, .2, diff --git a/src/libguac/socket-handle.c b/src/libguac/socket-handle.c new file mode 100644 index 0000000000..b752b9e97e --- /dev/null +++ b/src/libguac/socket-handle.c @@ -0,0 +1,451 @@ +/* + * 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 "guacamole/error.h" +#include "guacamole/socket.h" +#include "guacamole/handle-helpers.h" +#include "wait-handle.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +/** + * Data associated with an open socket which writes to a file handle. + */ +typedef struct guac_socket_handle_data { + + /** + * The associated file handle. + */ + HANDLE handle; + + /** + * The number of bytes currently in the main write buffer. + */ + int written; + + /** + * The main write buffer. Bytes written go here before being flushed + * to the open file descriptor. + */ + char out_buf[GUAC_SOCKET_OUTPUT_BUFFER_SIZE]; + + /** + * Lock which is acquired when an instruction is being written, and + * released when the instruction is finished being written. + */ + pthread_mutex_t socket_lock; + + /** + * Lock which protects access to the internal buffer of this socket, + * guaranteeing atomicity of writes and flushes. + */ + pthread_mutex_t buffer_lock; + +} guac_socket_handle_data; + +/** + * Writes the entire contents of the given buffer to the file handle + * associated with the given socket, retrying as necessary until the whole + * buffer is written, and aborting if an error occurs. + * + * @param socket + * The guac_socket associated with the file handle to which the given + * buffer should be written. + * + * @param buf + * The buffer of data to write to the given guac_socket. + * + * @param count + * The number of bytes within the given buffer. + * + * @return + * The number of bytes written, which will be exactly the size of the given + * buffer, or a negative value if an error occurs. + */ +static ssize_t guac_socket_handle_write(guac_socket* socket, + const void* buf, size_t count) { + + guac_socket_handle_data* data = (guac_socket_handle_data*) socket->data; + const char* buffer = buf; + + /* Write until completely written */ + while (count > 0) { + + DWORD bytes_written; + + DWORD error = guac_write_to_handle(data->handle, buffer, count, &bytes_written); + + if (error) { + guac_error = GUAC_STATUS_SEE_WINDOWS_ERROR; + guac_windows_error_code = error; + guac_error_message = "Error writing data to handle"; + return -1; + } + + /* Advance buffer to next chunk */ + buffer += bytes_written; + count -= bytes_written; + + } + + return 0; + +} + +/** + * Attempts to read from the underlying file handle of the given + * guac_socket, populating the given buffer. + * + * @param socket + * The guac_socket being read from. + * + * @param buf + * The arbitrary buffer which we must populate with data. + * + * @param count + * The maximum number of bytes to read into the buffer. + * + * @return + * The number of bytes read, or -1 if an error occurs. + */ +static ssize_t guac_socket_handle_read_handler(guac_socket* socket, + void* buf, size_t count) { + + guac_socket_handle_data* data = (guac_socket_handle_data*) socket->data; + + DWORD bytes_read; + do { + + DWORD error = guac_read_from_handle(data->handle, buf, count, &bytes_read); + if (error) { + guac_error = GUAC_STATUS_SEE_WINDOWS_ERROR; + guac_windows_error_code = error; + guac_error_message = "Error reading data from handle"; + return -1; + } + + } while(bytes_read == 0); + + return bytes_read; + +} + +/** + * 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_handle_flush(guac_socket* socket) { + + guac_socket_handle_data* data = (guac_socket_handle_data*) socket->data; + + /* Flush remaining bytes in buffer */ + if (data->written > 0) { + + /* Write ALL bytes in buffer immediately */ + if (guac_socket_handle_write(socket, data->out_buf, data->written)) + return 1; + + data->written = 0; + } + + return 0; + +} + +/** + * Flushes the internal buffer of the given guac_socket, writing all data + * to the underlying file handle. + * + * @param socket + * The guac_socket to flush. + * + * @return + * Zero if the flush operation was successful, non-zero otherwise. + */ +static ssize_t guac_socket_handle_flush_handler(guac_socket* socket) { + + int retval; + guac_socket_handle_data* data = (guac_socket_handle_data*) socket->data; + + /* Acquire exclusive access to buffer */ + pthread_mutex_lock(&(data->buffer_lock)); + + /* Flush contents of buffer */ + retval = guac_socket_handle_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_handle_write_buffered(guac_socket* socket, + const void* buf, size_t count) { + + size_t original_count = count; + const char* current = buf; + guac_socket_handle_data* data = (guac_socket_handle_data*) socket->data; + + /* Append to buffer, flush if necessary */ + while (count > 0) { + + int chunk_size; + int remaining = sizeof(data->out_buf) - data->written; + + /* If no space left in buffer, flush and retry */ + if (remaining == 0) { + + /* Abort if error occurs during flush */ + if (guac_socket_handle_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->out_buf + 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_handle_write_handler(guac_socket* socket, + const void* buf, size_t count) { + + int retval; + guac_socket_handle_data* data = (guac_socket_handle_data*) socket->data; + + /* Acquire exclusive access to buffer */ + pthread_mutex_lock(&(data->buffer_lock)); + + /* Write provided data to buffer */ + retval = guac_socket_handle_write_buffered(socket, buf, count); + + /* Relinquish exclusive access to buffer */ + pthread_mutex_unlock(&(data->buffer_lock)); + + return retval; + +} + +/** + * Waits for data on the underlying file handle of the given socket to + * become available such that the next read operation will not block. + * + * @param socket + * The guac_socket to wait for. + * + * @param usec_timeout + * The maximum amount of time to wait for data, in microseconds, or -1 to + * potentially wait forever. + * + * @return + * A positive value on success, zero if the timeout elapsed and no data is + * available, or a negative value if an error occurs. + */ +static int guac_socket_handle_select_handler(guac_socket* socket, + int usec_timeout) { + + /* Wait for data on socket */ + guac_socket_handle_data* data = (guac_socket_handle_data*) socket->data; + + int retval = guac_wait_for_handle(data->handle, usec_timeout); + + /* Properly set guac_error */ + if (retval > 0) { + guac_error = GUAC_STATUS_SEE_WINDOWS_ERROR; + guac_windows_error_code = retval; + guac_error_message = "Error while waiting for data on handle"; + return -1; + } + + else if (retval < 0) { + guac_error = GUAC_STATUS_TIMEOUT; + guac_error_message = "Timeout while waiting for data on handle"; + return 0; + } + + /* Data is ready */ + return 1; + +} + +/** + * Frees all implementation-specific data associated with the given socket, but + * not the socket object itself. + * + * @param socket + * The guac_socket whose associated data should be freed. + * + * @return + * Zero if the data was successfully freed, non-zero otherwise. This + * implementation always succeeds, and will always return zero. + */ +static int guac_socket_handle_free_handler(guac_socket* socket) { + + guac_socket_handle_data* data = (guac_socket_handle_data*) socket->data; + + /* Destroy locks */ + pthread_mutex_destroy(&(data->socket_lock)); + pthread_mutex_destroy(&(data->buffer_lock)); + + /* Close file handles */ + CloseHandle(data->handle); + + 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_handle_lock_handler(guac_socket* socket) { + + guac_socket_handle_data* data = (guac_socket_handle_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_handle_unlock_handler(guac_socket* socket) { + + guac_socket_handle_data* data = (guac_socket_handle_data*) socket->data; + + /* Relinquish exclusive access to socket */ + pthread_mutex_unlock(&(data->socket_lock)); + +} + +guac_socket* guac_socket_open_handle(HANDLE handle) { + + pthread_mutexattr_t lock_attributes; + + /* Allocate socket and associated data */ + guac_socket* socket = guac_socket_alloc(); + guac_socket_handle_data* data = malloc(sizeof(guac_socket_handle_data)); + + /* Store file handle as socket data */ + data->handle = handle; + data->written = 0; + socket->data = data; + + pthread_mutexattr_init(&lock_attributes); + pthread_mutexattr_setpshared(&lock_attributes, PTHREAD_PROCESS_SHARED); + + /* Init locks */ + pthread_mutex_init(&(data->socket_lock), &lock_attributes); + pthread_mutex_init(&(data->buffer_lock), &lock_attributes); + + /* Set read/write handlers */ + socket->read_handler = guac_socket_handle_read_handler; + socket->write_handler = guac_socket_handle_write_handler; + socket->select_handler = guac_socket_handle_select_handler; + socket->lock_handler = guac_socket_handle_lock_handler; + socket->unlock_handler = guac_socket_handle_unlock_handler; + socket->flush_handler = guac_socket_handle_flush_handler; + socket->free_handler = guac_socket_handle_free_handler; + + return socket; + +} + diff --git a/src/libguac/tests/Makefile.am b/src/libguac/tests/Makefile.am index 3596156cff..a7287b68a8 100644 --- a/src/libguac/tests/Makefile.am +++ b/src/libguac/tests/Makefile.am @@ -70,7 +70,7 @@ test_libguac_SOURCES = \ test_libguac_CFLAGS = \ - -Werror -Wall -pedantic \ + @GENERAL_CFLAGS@ \ @LIBGUAC_INCLUDE@ test_libguac_LDADD = \ diff --git a/src/libguac/tests/id/generate.c b/src/libguac/tests/id/generate.c index 4f369fdf10..c40db033d6 100644 --- a/src/libguac/tests/id/generate.c +++ b/src/libguac/tests/id/generate.c @@ -18,7 +18,7 @@ */ #include "guacamole/mem.h" -#include "id.h" +#include "guacamole/id.h" #include #include diff --git a/src/libguac/user-handshake.c b/src/libguac/user-handshake.c index 422c941b73..f40c2764ed 100644 --- a/src/libguac/user-handshake.c +++ b/src/libguac/user-handshake.c @@ -79,9 +79,11 @@ static void guac_user_log_guac_error(guac_user* user, guac_error_message); /* Otherwise just log with standard status string */ - else - guac_user_log(user, level, "%s: %s", message, - guac_status_string(guac_error)); + else { + char* status_string = guac_status_string(guac_error); + guac_user_log(user, level, "%s: %s", message, status_string); + free(status_string); + } } @@ -108,10 +110,12 @@ static void guac_user_log_handshake_failure(guac_user* user) { "Guacamole protocol violation. Perhaps the version of " "guacamole-client is incompatible with this version of " "libguac?"); - else + else { + char* status_string = guac_status_string(guac_error); guac_user_log(user, GUAC_LOG_WARNING, - "Guacamole handshake failed: %s", - guac_status_string(guac_error)); + "Guacamole handshake failed: %s", status_string); + free(status_string); + } } diff --git a/src/libguac/user.c b/src/libguac/user.c index 846d0eddd6..1ade64ad01 100644 --- a/src/libguac/user.c +++ b/src/libguac/user.c @@ -32,7 +32,7 @@ #include "guacamole/string.h" #include "guacamole/timestamp.h" #include "guacamole/user.h" -#include "id.h" +#include #include "user-handlers.h" #include diff --git a/src/libguac/wait-fd.c b/src/libguac/wait-fd.c index d6079be385..d951632636 100644 --- a/src/libguac/wait-fd.c +++ b/src/libguac/wait-fd.c @@ -19,8 +19,9 @@ #include "config.h" -#ifdef ENABLE_WINSOCK -# include +#ifdef WINDOWS_BUILD +#include +#include #else # ifdef HAVE_POLL # include diff --git a/src/libguac/wait-handle.c b/src/libguac/wait-handle.c new file mode 100644 index 0000000000..bfba875b28 --- /dev/null +++ b/src/libguac/wait-handle.c @@ -0,0 +1,76 @@ +/* + * 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 +#include +#include +#include + +int guac_wait_for_handle(HANDLE handle, int usec_timeout) { + + /* Create an event to be used to signal comm events */ + HANDLE event = CreateEvent(NULL, FALSE, FALSE, NULL); + if (event == NULL) + return GetLastError(); + + OVERLAPPED overlapped = { 0 }; + overlapped.hEvent = event; + + /* Request to wait for new data to be available */ + char buff[1]; + if (!ReadFile(handle, &buff, 0, NULL, &overlapped)) { + + DWORD error = GetLastError(); + + /* ERROR_IO_PENDING is expected in overlapped mode */ + if (error != ERROR_IO_PENDING) { + CloseHandle(event); + return error; + } + + } + + int millis = (usec_timeout + 999) / 1000; + + DWORD result = WaitForSingleObject(event, millis); + + /* The wait attempt failed */ + if (result == WAIT_FAILED) { + CloseHandle(event); + return GetLastError(); + } + + /* The event was signalled, which should indicate data is ready */ + else if (result == WAIT_OBJECT_0) { + CloseHandle(event); + return 0; + } + + /* + * If the event didn't trigger and the wait didn't fail, data just isn't + * ready yet. + */ + CloseHandle(event); + return -1; + +} diff --git a/src/libguac/wait-handle.h b/src/libguac/wait-handle.h new file mode 100644 index 0000000000..7e135f4b5b --- /dev/null +++ b/src/libguac/wait-handle.h @@ -0,0 +1,44 @@ +/* + * 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_WAIT_HANDLE_H +#define GUAC_WAIT_HANDLE_H + +#include + +/** + * Waits for data to be available for reading on a given file handle. Returns + * zero if data is available, a negative value if the wait timed out without + * data being available, or a positive Windows error code if the wait failed. + * + * @param handle + * The file handle to wait for. + * + * @param usec_timeout + * The maximum number of microseconds to wait for data, or -1 to + * potentially wait forever. + * + * @return + * Zero if data is available for reading, negative if the timeout elapsed + * and no data is available, or a positive Windows error code if an error + * occurs. + */ +int guac_wait_for_handle(HANDLE handle, int usec_timeout); + +#endif diff --git a/src/libguac/wol.c b/src/libguac/wol.c index 9d69306c90..db0f21dd22 100644 --- a/src/libguac/wol.c +++ b/src/libguac/wol.c @@ -26,9 +26,15 @@ #include #include #include + +#ifdef WINDOWS_BUILD +#include +#include +#else #include #include #include +#endif /** * Generate the magic Wake-on-LAN (WoL) packet for the specified MAC address @@ -195,4 +201,4 @@ int guac_wol_wake(const char* mac_addr, const char* broadcast_addr, return 0; return -1; -} \ No newline at end of file +} diff --git a/src/protocols/kubernetes/Makefile.am b/src/protocols/kubernetes/Makefile.am index e9316ce49b..3150c7e94f 100644 --- a/src/protocols/kubernetes/Makefile.am +++ b/src/protocols/kubernetes/Makefile.am @@ -57,6 +57,7 @@ noinst_HEADERS = \ libguac_client_kubernetes_la_CFLAGS = \ -Werror -Wall -Iinclude \ + @GENERAL_CFLAGS@ \ @LIBGUAC_INCLUDE@ \ @TERMINAL_INCLUDE@ @@ -69,5 +70,6 @@ libguac_client_kubernetes_la_LDFLAGS = \ -version-info 0:0:0 \ @PTHREAD_LIBS@ \ @SSL_LIBS@ \ - @WEBSOCKETS_LIBS@ + @WEBSOCKETS_LIBS@ \ + @GENERAL_LDFLAGS@ diff --git a/src/protocols/kubernetes/client.c b/src/protocols/kubernetes/client.c index 038da28724..22fa19eee9 100644 --- a/src/protocols/kubernetes/client.c +++ b/src/protocols/kubernetes/client.c @@ -29,7 +29,10 @@ #include #include +#ifndef WINDOWS_BUILD #include +#endif + #include #include #include @@ -136,12 +139,15 @@ int guac_client_init(guac_client* client) { guac_argv_register(GUAC_KUBERNETES_ARGV_FONT_SIZE, guac_kubernetes_argv_callback, NULL, GUAC_ARGV_OPTION_ECHO); /* Set locale and warn if not UTF-8 */ +#ifdef WINDOWS_BUILD + if(!setlocale(LC_CTYPE, ".UTF8")) +#else setlocale(LC_CTYPE, ""); - if (strcmp(nl_langinfo(CODESET), "UTF-8") != 0) { + if (strcmp(nl_langinfo(CODESET), "UTF-8") != 0) +#endif guac_client_log(client, GUAC_LOG_INFO, "Current locale does not use UTF-8. Some characters may " "not render correctly."); - } /* Success */ return 0; diff --git a/src/protocols/kubernetes/tests/Makefile.am b/src/protocols/kubernetes/tests/Makefile.am index ec0c3a8b62..94774b6f56 100644 --- a/src/protocols/kubernetes/tests/Makefile.am +++ b/src/protocols/kubernetes/tests/Makefile.am @@ -38,7 +38,7 @@ test_kubernetes_SOURCES = \ url/escape.c test_kubernetes_CFLAGS = \ - -Werror -Wall -pedantic \ + @GENERAL_CFLAGS@ \ @LIBGUAC_CLIENT_KUBERNETES_INCLUDE@ \ @LIBGUAC_INCLUDE@ diff --git a/src/protocols/rdp/Makefile.am b/src/protocols/rdp/Makefile.am index 1c393c30d8..ebf0e927f8 100644 --- a/src/protocols/rdp/Makefile.am +++ b/src/protocols/rdp/Makefile.am @@ -135,6 +135,7 @@ noinst_HEADERS = \ libguac_client_rdp_la_CFLAGS = \ -Werror -Wall -Iinclude \ + @GENERAL_CFLAGS@ \ @COMMON_INCLUDE@ \ @COMMON_SSH_INCLUDE@ \ @LIBGUAC_INCLUDE@ \ @@ -144,7 +145,8 @@ libguac_client_rdp_la_LDFLAGS = \ -version-info 0:0:0 \ @CAIRO_LIBS@ \ @PTHREAD_LIBS@ \ - @RDP_LIBS@ + @RDP_LIBS@ \ + @GENERAL_LDFLAGS@ libguac_client_rdp_la_LIBADD = \ @COMMON_LTLIB@ \ @@ -169,12 +171,14 @@ libguac_common_svc_client_la_SOURCES = \ libguac_common_svc_client_la_CFLAGS = \ -Werror -Wall -Iinclude \ + @GENERAL_CFLAGS@ \ @LIBGUAC_INCLUDE@ \ @RDP_CFLAGS@ libguac_common_svc_client_la_LDFLAGS = \ -module -avoid-version -shared \ - @RDP_LIBS@ + @RDP_LIBS@ \ + @GENERAL_LDFLAGS@ libguac_common_svc_client_la_LIBADD = \ @LIBGUAC_LTLIB@ @@ -191,6 +195,7 @@ libguacai_client_la_SOURCES = \ libguacai_client_la_CFLAGS = \ -Werror -Wall -Iinclude \ + @GENERAL_CFLAGS@ \ @COMMON_INCLUDE@ \ @COMMON_SSH_INCLUDE@ \ @LIBGUAC_INCLUDE@ \ @@ -199,7 +204,8 @@ libguacai_client_la_CFLAGS = \ libguacai_client_la_LDFLAGS = \ -module -avoid-version -shared \ @PTHREAD_LIBS@ \ - @RDP_LIBS@ + @RDP_LIBS@ \ + @GENERAL_LDFLAGS@ libguacai_client_la_LIBADD = \ @COMMON_LTLIB@ \ @@ -265,3 +271,7 @@ EXTRA_DIST = \ keymaps/generate.pl \ plugins/generate-entry-wrappers.pl +if WINDOWS_BUILD +DIST_SUBDIRS = guacxpstopdf +SUBDIRS += guacxpstopdf +endif diff --git a/src/protocols/rdp/channels/rdpdr/rdpdr-messages.c b/src/protocols/rdp/channels/rdpdr/rdpdr-messages.c index 5fee4658bc..829a797232 100644 --- a/src/protocols/rdp/channels/rdpdr/rdpdr-messages.c +++ b/src/protocols/rdp/channels/rdpdr/rdpdr-messages.c @@ -17,7 +17,10 @@ * under the License. */ +#include "config.h" + #include "channels/rdpdr/rdpdr-messages.h" +#include "channels/rdpdr/rdpdr-printer.h" #include "channels/rdpdr/rdpdr.h" #include "rdp.h" #include "settings.h" @@ -226,7 +229,7 @@ void guac_rdpdr_process_server_announce(guac_rdp_common_svc* svc, /* Must choose own client ID if minor not >= 12 */ if (minor < 12) - client_id = random() & 0xFFFF; + client_id = rand() & 0xFFFF; guac_client_log(svc->client, GUAC_LOG_INFO, "Connected to RDPDR %u.%u as client 0x%04x", major, minor, client_id); @@ -395,5 +398,20 @@ void guac_rdpdr_process_prn_cache_data(guac_rdp_common_svc* svc, void guac_rdpdr_process_prn_using_xps(guac_rdp_common_svc* svc, wStream* input_stream) { + +/* Support XPS mode only on Windows*/ +#ifdef WINDOWS_BUILD + + guac_client_log(svc->client, GUAC_LOG_INFO, "Printer switched to XPS mode"); + + /* Mark the client as XPS mode being enabled for the printer */ + guac_rdp_client* client = (guac_rdp_client*) svc->client->data; + client->xps_printer_mode_enabled = 1; + +#else + guac_client_log(svc->client, GUAC_LOG_WARNING, "Printer unexpectedly switched to XPS mode"); + +#endif + } diff --git a/src/protocols/rdp/channels/rdpdr/rdpdr-messages.h b/src/protocols/rdp/channels/rdpdr/rdpdr-messages.h index a79c83b421..7a0db544eb 100644 --- a/src/protocols/rdp/channels/rdpdr/rdpdr-messages.h +++ b/src/protocols/rdp/channels/rdpdr/rdpdr-messages.h @@ -125,8 +125,7 @@ guac_rdpdr_message_handler guac_rdpdr_process_prn_cache_data; /** * Handler which processes a received Server Printer Set XPS Mode message. The * Server Printer Set XPS Mode message is specific to printers and requests - * that the client printer be set to XPS mode. The Guacamole RDPDR - * implementation ignores any request to set the printer to XPS mode. See: + * that the client printer be set to XPS mode. See: * * https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpepc/f1789a66-bcd0-4df3-bfc2-6e7330d63145 */ diff --git a/src/protocols/rdp/channels/rdpdr/rdpdr-printer.c b/src/protocols/rdp/channels/rdpdr/rdpdr-printer.c index 4ff705355e..4cdbc75b73 100644 --- a/src/protocols/rdp/channels/rdpdr/rdpdr-printer.c +++ b/src/protocols/rdp/channels/rdpdr/rdpdr-printer.c @@ -206,7 +206,14 @@ void guac_rdpdr_register_printer(guac_rdp_common_svc* svc, char* printer_name) { /* Begin printer-specific information */ Stream_Write_UINT32(device->device_announce, RDPDR_PRINTER_ANNOUNCE_FLAG_DEFAULTPRINTER - | RDPDR_PRINTER_ANNOUNCE_FLAG_NETWORKPRINTER); /* Printer flags */ + | RDPDR_PRINTER_ANNOUNCE_FLAG_NETWORKPRINTER +#ifdef WINDOWS_BUILD + /* + * For windows, only support printing XPS to PDF using libgxps. + */ + | RDPDR_PRINTER_ANNOUNCE_FLAG_XPSFORMAT +#endif + ); /* Printer flags */ Stream_Write_UINT32(device->device_announce, 0); /* Reserved - must be 0. */ Stream_Write_UINT32(device->device_announce, 0); /* PnPName Length - ignored. */ Stream_Write_UINT32(device->device_announce, GUAC_PRINTER_DRIVER_LENGTH); diff --git a/src/protocols/rdp/client.c b/src/protocols/rdp/client.c index e98a0f3a54..6c19eb2507 100644 --- a/src/protocols/rdp/client.c +++ b/src/protocols/rdp/client.c @@ -44,12 +44,18 @@ #include #include #include -#include #include #include #include #include +#ifdef WINDOWS_BUILD +#include +#include +#else +#include +#endif + /** * Tests whether the given path refers to a directory which the current user * can write to. If the given path is not a directory, is not writable, or is @@ -65,6 +71,38 @@ */ static int is_writable_directory(const char* path) { +#ifdef WINDOWS_BUILD + + /* + * Attempt to create a file handle with the permission to add a new file at + * the provided path - this can only work for a directory that the current + * user can write to. Any other path will produce an invalid handle. + */ + HANDLE dir_handle = CreateFile( + path, FILE_ADD_FILE, FILE_SHARE_WRITE, NULL, + OPEN_EXISTING, + + /* + * From the CreateFile() API docs: + * You must set this flag to obtain a handle to a directory. A directory handle can be + * passed to some functions instead of a file handle. For more, see + * https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilea + */ + FILE_FLAG_BACKUP_SEMANTICS, + + NULL); + + /* The path is not a writeable directory */ + if (dir_handle == INVALID_HANDLE_VALUE ) + return 0; + + /* If the handle is valid, the directory exists, and is writeable */ + CloseHandle(dir_handle); + return 1; + + +#else + /* Verify path is writable */ if (faccessat(AT_FDCWD, path, W_OK, 0)) return 0; @@ -78,6 +116,8 @@ static int is_writable_directory(const char* path) { closedir(dir); return 1; +#endif + } /** @@ -147,6 +187,58 @@ int guac_client_init(guac_client* client, int argc, char** argv) { const char* current_home = getenv("HOME"); if (current_home == NULL) { +#ifdef WINDOWS_BUILD + + /* + * There appears to be no Windows equivalent of getpwuid(), so env + * variables must be the source of truth. + */ + const char* user_profile = getenv("USERPROFILE"); + const char* home_drive = getenv("HOMEDRIVE"); + const char* home_path = getenv("HOMEPATH"); + + /* If USERPROFILE is available, just use that */ + if (user_profile != NULL) + current_home = user_profile; + + /* Otherwise, concatenate HOMEDRIVE and HOMEPATH */ + else if (home_drive != NULL && home_path != NULL) { + + /* Copy both variables into a buffer that should be large enough */ + char buffer[1024]; + + size_t drive_length = strlen(home_drive); + size_t path_length = strlen(home_path); + + /* If the full path won't fit in the buffer, something is wrong*/ + if ((drive_length + path_length + 1) >= sizeof(buffer)) + guac_client_log(client, GUAC_LOG_WARNING, "FreeRDP initialization " + "may fail: The \"HOME\" and \"USERPROFILE\" environment " + "variables are unset, and the \"HOMEDRIVE\" and \"HOMEPATH\" " + "variables are invalid."); + + /* Concatenate the two variables into the buffer to get the home */ + strncpy(buffer, home_drive, drive_length); + strncpy(buffer + drive_length, home_path, path_length); + buffer[drive_length + path_length] = '\0'; + current_home = buffer; + + } + + else + guac_client_log(client, GUAC_LOG_WARNING, "FreeRDP initialization " + "may fail: The \"HOME\", \"USERPROFILE\", \"HOMEDRIVE\", " + "and \"HOMEPATH\" variables are all unset."); + + /* Attempt to set the variable for the current process */ + if (current_home && !SetEnvironmentVariable("HOME", current_home)) + guac_client_log(client, GUAC_LOG_WARNING, "FreeRDP initialization " + "may fail: The \"HOME\" environment variable is unset " + "and its correct value (detected as \"%s\") could not be " + "assigned: %u", current_home, GetLastError()); + +#else + /* Warn if the correct home directory cannot be determined */ struct passwd* passwd = getpwuid(getuid()); if (passwd == NULL) @@ -170,6 +262,7 @@ int guac_client_init(guac_client* client, int argc, char** argv) { "automatically set to \"%s\"", passwd->pw_dir); current_home = passwd->pw_dir; } +#endif } diff --git a/src/protocols/rdp/fs.c b/src/protocols/rdp/fs.c index be228728c9..341874a6b9 100644 --- a/src/protocols/rdp/fs.c +++ b/src/protocols/rdp/fs.c @@ -35,14 +35,22 @@ #include #include #include -#include #include #include #include #include -#include #include +#ifdef WINDOWS_BUILD +#include +#include +#include +#include +#include +#else +#include +#endif + guac_rdp_fs* guac_rdp_fs_alloc(guac_client* client, const char* drive_path, int create_drive_path, int disable_download, int disable_upload) { @@ -53,7 +61,13 @@ guac_rdp_fs* guac_rdp_fs_alloc(guac_client* client, const char* drive_path, __func__, drive_path); /* Log error if directory creation fails */ - if (mkdir(drive_path, S_IRWXU) && errno != EEXIST) { + if ( +#ifdef WINDOWS_BUILD + _mkdir(drive_path) +#else + mkdir(drive_path, S_IRWXU) +#endif + && errno != EEXIST) { guac_client_log(client, GUAC_LOG_ERROR, "Unable to create directory \"%s\": %s", drive_path, strerror(errno)); @@ -326,7 +340,11 @@ int guac_rdp_fs_open(guac_rdp_fs* fs, const char* path, if ((create_options & FILE_DIRECTORY_FILE) && (flags & O_CREAT)) { /* Create directory */ +#ifdef WINDOWS_BUILD + if (_mkdir(real_path)) { +#else if (mkdir(real_path, S_IRWXU)) { +#endif if (errno != EEXIST || (flags & O_EXCL)) { guac_client_log(fs->client, GUAC_LOG_DEBUG, "%s: mkdir() failed: %s", @@ -598,7 +616,25 @@ const char* guac_rdp_fs_read_dir(guac_rdp_fs* fs, int file_id) { /* Open directory if not yet open, stop if error */ if (file->dir == NULL) { +#ifdef WINDOWS_BUILD + + wchar_t file_name_buffer[1024]; + + DWORD return_value = GetFinalPathNameByHandleW( + ((HANDLE) _get_osfhandle(file->fd)), + file_name_buffer, sizeof(file_name_buffer), + FILE_NAME_NORMALIZED); + if (return_value == 0) + return NULL; + + char file_name[2048]; + WideCharToMultiByte(CP_UTF8, 0, file_name_buffer, -1, + file_name, sizeof(file_name), NULL, NULL); + + file->dir = opendir(file_name); +#else file->dir = fdopendir(file->fd); +#endif if (file->dir == NULL) return NULL; } @@ -731,11 +767,48 @@ guac_rdp_fs_file* guac_rdp_fs_get_file(guac_rdp_fs* fs, int file_id) { } int guac_rdp_fs_matches(const char* filename, const char* pattern) { - return fnmatch(pattern, filename, FNM_NOESCAPE) != 0; + return FilePatternMatchA(filename, pattern) != 0; } int guac_rdp_fs_get_info(guac_rdp_fs* fs, guac_rdp_fs_info* info) { +#ifdef WINDOWS_BUILD + + /* Variables to store the result of the call */ + DWORD sectors_per_cluster; + DWORD bytes_per_sector; + DWORD number_of_free_clusters; + DWORD number_of_total_clusters; + + if (!GetDiskFreeSpace(fs->drive_path, §ors_per_cluster, &bytes_per_sector, + &number_of_free_clusters, &number_of_total_clusters)) { + + + DWORD error_code = GetLastError(); + switch(error_code) { + + case ERROR_FILE_NOT_FOUND: + case ERROR_INVALID_DRIVE: + case ERROR_PATH_NOT_FOUND: + return GUAC_RDP_FS_ENOENT; + + case ERROR_ACCESS_DENIED: + return GUAC_RDP_FS_EACCES; + + /* Default to GUAC_RDP_FS_EINVAL for unknown errors */ + default: + return GUAC_RDP_FS_EINVAL; + } + } + + /* Treat sectors as equivalent to blocks */ + info->block_size = bytes_per_sector; + info->blocks_available = number_of_free_clusters * sectors_per_cluster; + info->blocks_total = number_of_total_clusters * sectors_per_cluster; + return 0; + +#else + /* Read FS information */ struct statvfs fs_stat; if (statvfs(fs->drive_path, &fs_stat)) @@ -747,6 +820,8 @@ int guac_rdp_fs_get_info(guac_rdp_fs* fs, guac_rdp_fs_info* info) { info->block_size = fs_stat.f_bsize; return 0; +#endif + } int guac_rdp_fs_append_filename(char* fullpath, const char* path, diff --git a/src/protocols/rdp/fs.h b/src/protocols/rdp/fs.h index 48ca2f96ac..c91bcdd279 100644 --- a/src/protocols/rdp/fs.h +++ b/src/protocols/rdp/fs.h @@ -232,17 +232,17 @@ typedef struct guac_rdp_fs_info { /** * The number of free blocks available. */ - int blocks_available; + unsigned int blocks_available; /** * The number of blocks in the filesystem. */ - int blocks_total; + unsigned int blocks_total; /** * The number of bytes per block. */ - int block_size; + unsigned int block_size; } guac_rdp_fs_info; @@ -619,12 +619,11 @@ guac_rdp_fs_file* guac_rdp_fs_get_file(guac_rdp_fs* fs, int file_id); /** * Returns whether the given filename matches the given pattern. The pattern - * given is a shell wildcard pattern as accepted by the POSIX fnmatch() - * function. Backslashes will be interpreted as literal backslashes, not - * escape characters. + * given is a wildcard pattern as accepted by the FreeRDP FilePatternMatchA() + * function. * * @param filename - * The filename to check + * The filename to check. * * @param pattern * The pattern to check the filename against. diff --git a/src/protocols/rdp/guacxpstopdf/Makefile.am b/src/protocols/rdp/guacxpstopdf/Makefile.am new file mode 100644 index 0000000000..83c29bb11f --- /dev/null +++ b/src/protocols/rdp/guacxpstopdf/Makefile.am @@ -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. +# +# 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 + +bin_PROGRAMS = guacxpstopdf + +noinst_HEADERS = \ + guacxpstopdf.h + +guacxpstopdf_SOURCES = \ + guacxpstopdf.c + +guacxpstopdf_CFLAGS = \ + -Werror -Wall \ + @GENERAL_CFLAGS@ + +guacxpstopdf_LDFLAGS = \ + -lshlwapi + diff --git a/src/protocols/rdp/guacxpstopdf/guacxpstopdf.c b/src/protocols/rdp/guacxpstopdf/guacxpstopdf.c new file mode 100644 index 0000000000..891e4a1b99 --- /dev/null +++ b/src/protocols/rdp/guacxpstopdf/guacxpstopdf.c @@ -0,0 +1,266 @@ +/* + * 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 "guacxpstopdf.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +/** + * The full path to the libgxps-provided executable that translates XPS to PDF. + * TODO: Must this be hardcoded? + */ +#define TRANSLATION_EXECUTABLE_PATH "xpstopdf.exe" + +/** + * Create a new temporary file, in the configured temporary directory, returning + * 0 if the file was succesfully created, or a non-zero value otherwise. If any + * errors occur during the creation of the file, the error will be logged to + * stderr. The file must be manually deleted after by the caller it's done being used. + * The full path to the file will be stored in the provided file_path buffer, + * which must be large enough to hold any file path (MAX_PATH characters long). + * + * @param file_path + * A pointer to a buffer into which the full path to the newly-created + * temporary file will be written. + * + * @return + * Zero if the file was succesfully created, or a non-zero value otherwise. + */ +static int create_temp_file(char* file_path) { + + /* + * First, the entire contents of the input stream must be written to a temp + * file before it can be read by libgxps. + * + * NOTE: Technically, according to the docs for GetTempPath, the maximum + * length of path that can be set is MAX_PATH + 1, since a trailing slash + * is always added. A path this long would cause GetTempFileName to fail. + */ + char temp_dir_buffer[MAX_PATH + 1]; + if (!GetTempPath(sizeof(temp_dir_buffer), temp_dir_buffer)) { + fprintf(stderr, + "Could not determine temporary directory : %lu\n", GetLastError()); + return 1; + } + + if (!GetTempFileName(temp_dir_buffer, "GUA", 0, file_path)) { + fprintf(stderr, + "Could not create temporary file : %lu\n", GetLastError()); + return 1; + } + + return 0; +} + +int main(int argc, char* argv[]) { + + /* Use the default path unless overriden on the command line */ + char* exe_path = TRANSLATION_EXECUTABLE_PATH; + if (argc >= 2) + exe_path = argv[1]; + + /* The return code for this program - only set to 0 on success */ + int retcode = 1; + + /* Make sure stdin and stdout are operating in binary mode */ + _setmode(_fileno(stdin), _O_BINARY); + _setmode(_fileno(stdout), _O_BINARY); + + /* Data structures to extract info from the subprocess */ + STARTUPINFOA startup_info = { 0 }; + startup_info.cb = sizeof(startup_info); + PROCESS_INFORMATION process_info = { 0 }; + + /* + * Create stdin / stdout handles. + * These handles do NOT need to be closed, per + * https://learn.microsoft.com/en-us/windows/console/getstdhandle#handle-disposal + */ + HANDLE* stdin_handle = GetStdHandle(STD_INPUT_HANDLE); + HANDLE* stdout_handle = GetStdHandle(STD_OUTPUT_HANDLE); + + /* A temporary file to hold the XPS data from stdin */ + char temp_xps_path[MAX_PATH]; + if(create_temp_file(temp_xps_path)) + return 1; + + /* A temporary file to hold the PDF data to be written to stdout */ + char temp_pdf_path[MAX_PATH]; + if (create_temp_file(temp_pdf_path)) { + DeleteFile(temp_xps_path); + return 1; + } + + HANDLE xps_file_handle = NULL; + HANDLE pdf_file_handle = NULL; + + /* Open the XPS temp file for writing */ + xps_file_handle = CreateFile( + temp_xps_path, GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL); + if (xps_file_handle == INVALID_HANDLE_VALUE) + goto cleanup; + + /* Buffer to hold data being read from / written to files or streams */ + char buffer[16384]; + DWORD bytes_read; + DWORD bytes_written; + + while (1) { + + BOOL read_success = ReadFile( + stdin_handle, buffer, sizeof(buffer), &bytes_read, NULL); + + if (!read_success) + goto cleanup; + + /* Stop reading if there's nothing more in the stream */ + if (bytes_read == 0) + break; + + WriteFile(xps_file_handle, buffer, bytes_read, &bytes_written, NULL); + + if (bytes_written < bytes_read) + goto cleanup; + } + + /* Close this handle now that the file is done being written */ + FlushFileBuffers(xps_file_handle); + CloseHandle(xps_file_handle); + xps_file_handle = NULL; + + /* + * The temp file is now fully written with XPS data, so it's ready to be + * translated to PDF using the libgxps-provided program. + */ + + /* Add quotes to each of the temp file paths as needed */ + PathQuoteSpaces(temp_xps_path); + PathQuoteSpaces(temp_pdf_path); + + /* The arguments to xpstopdf.exe - the quoted input and output files */ + size_t xps_path_length = strlen(temp_xps_path); + size_t pdf_path_length = strlen(temp_pdf_path); + + /* Create a single space-seperated string with the exe and arguments */ + char argument_string[(MAX_PATH * 3) + 2]; + char* next_argument; + + /* First, the executable itself */ + size_t exe_path_length = strlen(exe_path); + strncpy(argument_string, exe_path, exe_path_length); + argument_string[exe_path_length] = ' '; + next_argument = argument_string + exe_path_length + 1; + + /* Second, the input file name */ + strncpy(next_argument, temp_xps_path, xps_path_length); + next_argument[xps_path_length] = ' '; + next_argument = next_argument + xps_path_length + 1; + + /* Finally, the output file name */ + strncpy(next_argument, temp_pdf_path, pdf_path_length); + next_argument[pdf_path_length] = '\0'; + + /* Start up the translation program */ + if (!CreateProcess( + + /* Set to NULL to use the first argument as the exe */ + NULL, + + /* The arguments - both temporary files, each quoted */ + argument_string, + + /* Default arguments that we don't need to change */ + NULL, NULL, FALSE, 0, NULL, NULL, + + /* Structures to capture info about the process */ + &startup_info, &process_info + )) + { + fprintf(stderr, "Failed to run translation program: %lu\n", GetLastError() ); + goto cleanup; + } + + /* Wait for the process to complete */ + WaitForSingleObject(process_info.hProcess, INFINITE); + + /* Get the exit code for the completed process */ + DWORD exit_code; + if (!GetExitCodeProcess(process_info.hProcess, &exit_code)) { + fprintf(stderr, "Failed to get translation program exit code: %lu\n", GetLastError() ); + goto cleanup; + } + + if (exit_code != 0) { + fprintf(stderr, "Translation program failed: %lu\n", exit_code); + goto cleanup; + } + + /* Open the new PDF temp file for writing */ + pdf_file_handle = CreateFile( + temp_pdf_path, GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, NULL); + if (pdf_file_handle == INVALID_HANDLE_VALUE) { + fprintf(stderr, + "Could not open temporary PDF file: %lu\n", GetLastError()); + goto cleanup; + } + + while (ReadFile(pdf_file_handle, buffer, sizeof(buffer), &bytes_read, NULL)) { + + if (bytes_read == 0) + break; + + WriteFile(stdout_handle, buffer, bytes_read, &bytes_written, NULL); + + if (bytes_written < bytes_read) { + fprintf(stderr, + "Error while writing PDF to stdout: %lu\n", GetLastError()); + goto cleanup; + } + } + + /* The entire file was successfully written to stdout, so we're done */ + retcode = 0; + +cleanup: + + if (process_info.hProcess != NULL) + CloseHandle(process_info.hProcess); + if (process_info.hThread != NULL) + CloseHandle(process_info.hThread); + + if (xps_file_handle != NULL) + CloseHandle(xps_file_handle); + + if (pdf_file_handle != NULL) + CloseHandle(pdf_file_handle); + + DeleteFile(temp_xps_path); + DeleteFile(temp_pdf_path); + + return retcode; + +} + diff --git a/src/protocols/rdp/guacxpstopdf/guacxpstopdf.h b/src/protocols/rdp/guacxpstopdf/guacxpstopdf.h new file mode 100644 index 0000000000..184b630ba6 --- /dev/null +++ b/src/protocols/rdp/guacxpstopdf/guacxpstopdf.h @@ -0,0 +1,23 @@ +/* + * 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 GUACXPSTOPDF_H +#define GUACXPSTOPDF_H +#endif + diff --git a/src/protocols/rdp/print-job.c b/src/protocols/rdp/print-job.c index 2b6ca1aa3d..8ece99577e 100644 --- a/src/protocols/rdp/print-job.c +++ b/src/protocols/rdp/print-job.c @@ -34,6 +34,14 @@ #include #include +#ifdef WINDOWS_BUILD +#include +#include +#include +#include +#include +#endif + /** * The command to run when filtering postscript to produce PDF. This must be * a NULL-terminated array of arguments, where the first argument is the name @@ -273,10 +281,10 @@ static int guac_rdp_print_filter_ack_handler(guac_user* user, } /** - * Forks a new print filtering process which accepts PostScript input and - * produces PDF output. File descriptors for writing input and reading output - * will automatically be allocated and must be manually closed when processing - * is complete. + * Creates a new print filtering process which accepts PostScript (or XPS, if + * being built on Windows) input and produces PDF output. File descriptors for + * writing input and reading output will automatically be allocated and must be + * manually closed when processing is complete. * * @param client * The guac_client associated with the print job for which this filter @@ -300,6 +308,79 @@ static int guac_rdp_print_filter_ack_handler(guac_user* user, static pid_t guac_rdp_create_filter_process(guac_client* client, int* input_fd, int* output_fd) { +#ifdef WINDOWS_BUILD + + /* Only the XPS format is supported by the Windows implementation */ + guac_rdp_client* rdp_client = (guac_rdp_client*) client->data; + if (!rdp_client->xps_printer_mode_enabled) { + guac_client_log(client, GUAC_LOG_WARNING, + "XPS mode not enabled by server; cancelling print job."); + return -1; + } + + /* Make sure that that the pipes can be shared with the child process */ + SECURITY_ATTRIBUTES security_attributes = { 0 }; + security_attributes.nLength = sizeof(security_attributes); + security_attributes.bInheritHandle = TRUE; + security_attributes.lpSecurityDescriptor = NULL; + + /* Create pipes to flow data in and out of the created process */ + HANDLE stdin_read = NULL; + HANDLE stdin_write = NULL; + HANDLE stdout_read = NULL; + HANDLE stdout_write = NULL; + + if (!CreatePipe(&stdin_read, &stdin_write, &security_attributes, 0)) { + guac_client_log(client, GUAC_LOG_ERROR, + "Could not create stdin pipe for RDP print job: %u", + GetLastError()); + return -1; + } + + if (!CreatePipe(&stdout_read, &stdout_write, &security_attributes, 0)) { + guac_client_log(client, GUAC_LOG_ERROR, + "Could not create stdout pipe for RDP print job: %u", + GetLastError()); + CloseHandle(stdin_read); + CloseHandle(stdin_write); + return -1; + } + + /* Ensure that the write handle is _not_ inherited by the child */ + SetHandleInformation(stdin_read, HANDLE_FLAG_INHERIT, 0); + SetHandleInformation(stdout_read, HANDLE_FLAG_INHERIT, 0); + + /* Set the provided file descriptors */ + *input_fd = _open_osfhandle((intptr_t ) stdin_write, _O_APPEND); + *output_fd = _open_osfhandle((intptr_t ) stdout_read, _O_APPEND); + + /* Set up a structure with all the handles we just set up */ + STARTUPINFO startup_info = { 0 }; + startup_info.cb = sizeof(startup_info); + startup_info.hStdError = ((HANDLE) _get_osfhandle(STDERR_FILENO)); + startup_info.hStdOutput = stdout_write; + startup_info.hStdInput = stdin_read; + + PROCESS_INFORMATION process_info = { 0 }; + if (!CreateProcess("C:/mingw64/bin/guacxpstopdf.exe", + NULL, NULL, NULL, 0, STARTF_USESTDHANDLES, NULL, NULL, &startup_info, &process_info)) { + guac_client_log(client, GUAC_LOG_ERROR, + "Could not start guacxpstopdf.exe: %u", GetLastError()); + CloseHandle(stdin_read); + CloseHandle(stdin_write); + CloseHandle(stdout_read); + CloseHandle(stdout_write); + return -1; + } + + /* These handles are always opened, and must be manually closed */ + CloseHandle(process_info.hProcess); + CloseHandle(process_info.hThread); + + return process_info.dwProcessId; + +#else + int child_pid; int stdin_pipe[2]; int stdout_pipe[2]; @@ -367,13 +448,16 @@ static pid_t guac_rdp_create_filter_process(guac_client* client, /* Log fork success */ guac_client_log(client, GUAC_LOG_INFO, "Created PDF filter process " - "PID=%i", child_pid); + "PID=%i", + child_pid); /* Close unneeded ends of pipe */ close(stdin_pipe[0]); close(stdout_pipe[1]); return child_pid; +#endif + } /** @@ -667,7 +751,13 @@ void guac_rdp_print_job_free(guac_rdp_print_job* job) { void guac_rdp_print_job_kill(guac_rdp_print_job* job) { /* Forcibly kill filter process, if running */ +#ifdef WINDOWS_BUILD + HANDLE job_handle = OpenProcess(PROCESS_TERMINATE, 0, job->filter_pid); + TerminateProcess(job_handle, 1); + CloseHandle(job_handle); +#else kill(job->filter_pid, SIGKILL); +#endif /* Stop all handling of I/O */ close(job->input_fd); diff --git a/src/protocols/rdp/rdp.c b/src/protocols/rdp/rdp.c index 1a16780b5f..e564e3141e 100644 --- a/src/protocols/rdp/rdp.c +++ b/src/protocols/rdp/rdp.c @@ -477,7 +477,7 @@ static int guac_rdp_handle_connection(guac_client* client) { guac_rdp_settings* settings = rdp_client->settings; /* Init random number generator */ - srandom(time(NULL)); + srand(time(NULL)); pthread_rwlock_wrlock(&(rdp_client->lock)); diff --git a/src/protocols/rdp/rdp.h b/src/protocols/rdp/rdp.h index 9e3d3fe0c6..d233cea03e 100644 --- a/src/protocols/rdp/rdp.h +++ b/src/protocols/rdp/rdp.h @@ -196,6 +196,14 @@ typedef struct guac_rdp_client { */ pthread_mutex_t message_lock; + /** + * Non-zero if XPS mode is enabled, or zero otherwise. + * NOTE: XPS mode is currently only supported on Windows. Attempts to set + * XPS mode on linux will be ignored. For more, see: + * https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpepc/f97d26e7-d862-4cca-8ec9-98d3c69e2717 + */ + int xps_printer_mode_enabled; + } guac_rdp_client; /** diff --git a/src/protocols/rdp/settings.c b/src/protocols/rdp/settings.c index fd51463742..8e35dfa751 100644 --- a/src/protocols/rdp/settings.c +++ b/src/protocols/rdp/settings.c @@ -41,6 +41,11 @@ #include #include +#ifdef WINDOWS_BUILD +#include +#include +#endif + /** * A warning to log when NLA mode is selected while FIPS mode is active on the * guacd server. @@ -1518,10 +1523,19 @@ void guac_rdp_push_settings(guac_client* client, /* Timezone redirection */ if (guac_settings->timezone) { +#ifdef WINDOWS_BUILD + if (!SetEnvironmentVariable("TZ", guac_settings->timezone)) { +#else if (setenv("TZ", guac_settings->timezone, 1)) { +#endif + guac_client_log(client, GUAC_LOG_WARNING, "Unable to forward timezone: TZ environment variable " +#ifdef WINDOWS_BUILD + "could not be set (windows code): %u", GetLastError()); +#else "could not be set: %s", strerror(errno)); +#endif } } diff --git a/src/protocols/rdp/tests/Makefile.am b/src/protocols/rdp/tests/Makefile.am index e1e22e3699..a6039fb1a1 100644 --- a/src/protocols/rdp/tests/Makefile.am +++ b/src/protocols/rdp/tests/Makefile.am @@ -38,7 +38,7 @@ test_rdp_SOURCES = \ fs/normalize_path.c test_rdp_CFLAGS = \ - -Werror -Wall -pedantic \ + @GENERAL_CFLAGS@ \ @LIBGUAC_CLIENT_RDP_INCLUDE@ \ @LIBGUAC_INCLUDE@ diff --git a/src/protocols/ssh/Makefile.am b/src/protocols/ssh/Makefile.am index 7bb1e3498c..143738e3ea 100644 --- a/src/protocols/ssh/Makefile.am +++ b/src/protocols/ssh/Makefile.am @@ -60,6 +60,7 @@ endif libguac_client_ssh_la_CFLAGS = \ -Werror -Wall -Iinclude \ + @GENERAL_CFLAGS@ \ @COMMON_SSH_INCLUDE@ \ @LIBGUAC_INCLUDE@ \ @TERMINAL_INCLUDE@ @@ -74,5 +75,5 @@ libguac_client_ssh_la_LDFLAGS = \ -version-info 0:0:0 \ @PTHREAD_LIBS@ \ @SSH_LIBS@ \ - @SSL_LIBS@ - + @SSL_LIBS@ \ + @GENERAL_LDFLAGS@ diff --git a/src/protocols/ssh/client.c b/src/protocols/ssh/client.c index b116e6d112..a91e20c56d 100644 --- a/src/protocols/ssh/client.c +++ b/src/protocols/ssh/client.c @@ -26,7 +26,10 @@ #include "terminal/terminal.h" #include "user.h" +#ifndef WINDOWS_BUILD #include +#endif + #include #include #include @@ -85,12 +88,15 @@ int guac_client_init(guac_client* client) { guac_argv_register(GUAC_SSH_ARGV_FONT_SIZE, guac_ssh_argv_callback, NULL, GUAC_ARGV_OPTION_ECHO); /* Set locale and warn if not UTF-8 */ +#ifdef WINDOWS_BUILD + if(!setlocale(LC_CTYPE, ".UTF8")) +#else setlocale(LC_CTYPE, ""); - if (strcmp(nl_langinfo(CODESET), "UTF-8") != 0) { + if (strcmp(nl_langinfo(CODESET), "UTF-8") != 0) +#endif guac_client_log(client, GUAC_LOG_INFO, "Current locale does not use UTF-8. Some characters may " "not render correctly."); - } /* Success */ return 0; diff --git a/src/protocols/ssh/ssh.c b/src/protocols/ssh/ssh.c index 171bdaa660..2bfdae6a2d 100644 --- a/src/protocols/ssh/ssh.c +++ b/src/protocols/ssh/ssh.c @@ -48,18 +48,27 @@ #endif #include -#include -#include -#include #include #include #include #include #include #include -#include #include +#ifdef WINDOWS_BUILD +#include +#include +#else +#include +#include +# ifdef HAVE_POLL +# include +# else +# include +# endif +#endif + /** * Produces a new user object containing a username and password or private * key, prompting the user as necessary to obtain that information. @@ -523,6 +532,8 @@ void* ssh_client_thread(void* data) { /* Wait for more data if reads turn up empty */ if (total_read == 0) { +#ifdef HAVE_POLL + /* Wait on the SSH session file descriptor only */ struct pollfd fds[] = {{ .fd = ssh_client->session->fd, @@ -534,6 +545,26 @@ void* ssh_client_thread(void* data) { if (poll(fds, 1, timeout) < 0) break; +#else + + int fd = ssh_client->session->fd; + fd_set fds; + + /* Initialize fd_set with single underlying file descriptor */ + FD_ZERO(&fds); + FD_SET(fd, &fds); + + /* Handle timeout if specified */ + struct timeval timeout_struct = { + .tv_sec = timeout / 1000, + .tv_usec = timeout % 1000 + }; + + /* Wait up to computed timeout */ + if (select(fd, &fds, NULL, NULL, &timeout_struct) < 0) + break; + +#endif } } diff --git a/src/protocols/telnet/Makefile.am b/src/protocols/telnet/Makefile.am index d0264f674c..1e30e99ac2 100644 --- a/src/protocols/telnet/Makefile.am +++ b/src/protocols/telnet/Makefile.am @@ -50,6 +50,7 @@ noinst_HEADERS = \ libguac_client_telnet_la_CFLAGS = \ -Werror -Wall -Iinclude \ + @GENERAL_CFLAGS@ \ @LIBGUAC_INCLUDE@ \ @TERMINAL_INCLUDE@ @@ -61,5 +62,7 @@ libguac_client_telnet_la_LIBADD = \ libguac_client_telnet_la_LDFLAGS = \ -version-info 0:0:0 \ @PTHREAD_LIBS@ \ - @TELNET_LIBS@ + @TELNET_LIBS@ \ + @GENERAL_LDFLAGS@ \ + @SYSTRE_LIBS@ diff --git a/src/protocols/telnet/client.c b/src/protocols/telnet/client.c index ba1f20f48a..49c806cabc 100644 --- a/src/protocols/telnet/client.c +++ b/src/protocols/telnet/client.c @@ -24,7 +24,10 @@ #include "telnet.h" #include "user.h" +#ifndef WINDOWS_BUILD #include +#endif + #include #include #include @@ -89,12 +92,15 @@ int guac_client_init(guac_client* client) { guac_argv_register(GUAC_TELNET_ARGV_FONT_SIZE, guac_telnet_argv_callback, NULL, GUAC_ARGV_OPTION_ECHO); /* Set locale and warn if not UTF-8 */ +#ifdef WINDOWS_BUILD + if(!setlocale(LC_CTYPE, ".UTF8")) +#else setlocale(LC_CTYPE, ""); - if (strcmp(nl_langinfo(CODESET), "UTF-8") != 0) { + if (strcmp(nl_langinfo(CODESET), "UTF-8") != 0) +#endif guac_client_log(client, GUAC_LOG_INFO, "Current locale does not use UTF-8. Some characters may " "not render correctly."); - } /* Success */ return 0; diff --git a/src/protocols/telnet/telnet.c b/src/protocols/telnet/telnet.c index 501b1d3a35..90d4230b05 100644 --- a/src/protocols/telnet/telnet.c +++ b/src/protocols/telnet/telnet.c @@ -32,17 +32,28 @@ #include #include -#include -#include -#include #include #include #include #include -#include #include #include +#ifdef WINDOWS_BUILD +#include +#include +#include +#else +#include +#include +#include +# ifdef HAVE_POLL +# include +# else +# include +# endif +#endif + /** * Support levels for various telnet options, required for connection * negotiation by telnet_init(), part of libtelnet. @@ -560,6 +571,8 @@ void guac_telnet_send_user(telnet_t* telnet, const char* username) { */ static int __guac_telnet_wait(int socket_fd) { +#ifdef HAVE_POLL + /* Build array of file descriptors */ struct pollfd fds[] = {{ .fd = socket_fd, @@ -570,6 +583,25 @@ static int __guac_telnet_wait(int socket_fd) { /* Wait for one second */ return poll(fds, 1, 1000); +#else + + /* Initialize fd_set with single underlying file descriptor */ + fd_set fds; + FD_ZERO(&fds); + FD_SET(socket_fd, &fds); + + /* Handle timeout if specified */ + struct timeval timeout = { + .tv_sec = 1000, + .tv_usec = 0 + }; + + /* Wait up to computed timeout */ + return select(socket_fd, &fds, NULL, NULL, &timeout); + + +#endif + } void* guac_telnet_client_thread(void* data) { diff --git a/src/protocols/vnc/Makefile.am b/src/protocols/vnc/Makefile.am index 64e8147e66..3cf10dc36a 100644 --- a/src/protocols/vnc/Makefile.am +++ b/src/protocols/vnc/Makefile.am @@ -55,7 +55,7 @@ noinst_HEADERS = \ vnc.h libguac_client_vnc_la_CFLAGS = \ - -Werror -Wall -pedantic -Iinclude \ + @GENERAL_CFLAGS@ \ @COMMON_INCLUDE@ \ @COMMON_SSH_INCLUDE@ \ @LIBGUAC_INCLUDE@ \ @@ -64,7 +64,8 @@ libguac_client_vnc_la_CFLAGS = \ libguac_client_vnc_la_LDFLAGS = \ -version-info 0:0:0 \ @CAIRO_LIBS@ \ - @VNC_LIBS@ + @VNC_LIBS@ \ + @GENERAL_LDFLAGS@ libguac_client_vnc_la_LIBADD = \ @COMMON_LTLIB@ \ diff --git a/src/protocols/vnc/cursor.c b/src/protocols/vnc/cursor.c index 449fea3820..469495e578 100644 --- a/src/protocols/vnc/cursor.c +++ b/src/protocols/vnc/cursor.c @@ -43,7 +43,6 @@ #include #include #include -#include void guac_vnc_cursor(rfbClient* client, int x, int y, int w, int h, int bpp) { diff --git a/src/protocols/vnc/display.c b/src/protocols/vnc/display.c index 8848e89331..7218f78758 100644 --- a/src/protocols/vnc/display.c +++ b/src/protocols/vnc/display.c @@ -42,7 +42,6 @@ #include #include #include -#include void guac_vnc_update(rfbClient* client, int x, int y, int w, int h) { diff --git a/src/protocols/vnc/log.c b/src/protocols/vnc/log.c index ba593e81d0..6d3f85ac94 100644 --- a/src/protocols/vnc/log.c +++ b/src/protocols/vnc/log.c @@ -34,7 +34,15 @@ #include #include #include + +/* + * Syslog does not exist on Windows, so we'll just log directly to std error. + * TODO: ReportEvent() might be sort of equivalent? Look into + * https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-reporteventa + */ +#ifndef WINDOWS_BUILD #include +#endif void guac_vnc_client_log_info(const char* format, ...) { @@ -46,8 +54,12 @@ void guac_vnc_client_log_info(const char* format, ...) { vsnprintf(message, sizeof(message), format, args); va_end(args); +#ifndef WINDOWS_BUILD /* Log to syslog */ syslog(LOG_INFO, "%s", message); +#else + fprintf(stderr, "INFO: %s\n", message); +#endif } @@ -61,8 +73,12 @@ void guac_vnc_client_log_error(const char* format, ...) { vsnprintf(message, sizeof(message), format, args); va_end(args); +#ifndef WINDOWS_BUILD /* Log to syslog */ syslog(LOG_ERR, "%s", message); +#else + fprintf(stderr, "ERROR: %s\n", message); +#endif } diff --git a/src/protocols/vnc/log.h b/src/protocols/vnc/log.h index 65f3dc2d48..49fdbc0d89 100644 --- a/src/protocols/vnc/log.h +++ b/src/protocols/vnc/log.h @@ -37,7 +37,6 @@ #include #include #include -#include /** * Callback invoked by libVNCServer when an informational message needs to be diff --git a/src/pulse/Makefile.am b/src/pulse/Makefile.am index 3cb2d44cba..c1bc1d4c2e 100644 --- a/src/pulse/Makefile.am +++ b/src/pulse/Makefile.am @@ -34,13 +34,14 @@ noinst_HEADERS = \ libguac_pulse_la_SOURCES = \ pulse.c -libguac_pulse_la_CFLAGS = \ - -Werror -Wall -pedantic \ +libguac_pulse_la_CFLAGS = \ + @GENERAL_CFLAGS@ \ @LIBGUAC_INCLUDE@ libguac_pulse_la_LIBADD = \ @LIBGUAC_LTLIB@ libguac_pulse_la_LDFLAGS = \ - @PULSE_LIBS@ + @PULSE_LIBS@ \ + @GENERAL_LDFLAGS@ diff --git a/src/terminal/Makefile.am b/src/terminal/Makefile.am index f503b4c232..a8fb5dea41 100644 --- a/src/terminal/Makefile.am +++ b/src/terminal/Makefile.am @@ -65,8 +65,15 @@ libguac_terminal_la_SOURCES = \ typescript.c \ xparsecolor.c +# Windows Build +if WINDOWS_BUILD +noinst_HEADERS += terminal/wcwidth.h +libguac_terminal_la_SOURCES += wcwidth.c +endif + libguac_terminal_la_CFLAGS = \ -Werror -Wall \ + @GENERAL_CFLAGS@ \ @COMMON_INCLUDE@ \ @LIBGUAC_INCLUDE@ \ @PANGO_CFLAGS@ \ @@ -83,5 +90,6 @@ libguac_terminal_la_LDFLAGS = \ @MATH_LIBS@ \ @PANGO_LIBS@ \ @PANGOCAIRO_LIBS@ \ - @PTHREAD_LIBS@ + @PTHREAD_LIBS@ \ + @GENERAL_LDFLAGS@ diff --git a/src/terminal/color-scheme.c b/src/terminal/color-scheme.c index c1a9e3ff74..3004cdf1c6 100644 --- a/src/terminal/color-scheme.c +++ b/src/terminal/color-scheme.c @@ -17,6 +17,7 @@ * under the License. */ +#include "config.h" #include "terminal/color-scheme.h" #include "terminal/palette.h" @@ -71,6 +72,20 @@ static int guac_terminal_color_scheme_compare_token(const char* str_start, */ static void guac_terminal_color_scheme_strip_spaces(const char** str_start, const char** str_end) { + +#ifdef WINDOWS_BUILD + + /* Cast to unsigned below to avoid error: array subscript has type 'char' */ + + /* Strip leading spaces. */ + while (*str_start < *str_end && isspace((const unsigned char) (**str_start))) + (*str_start)++; + + /* Strip trailing spaces. */ + while (*str_end > *str_start && isspace((const unsigned char) *(*str_end - 1))) + (*str_end)--; + +#else /* Strip leading spaces. */ while (*str_start < *str_end && isspace(**str_start)) @@ -79,6 +94,9 @@ static void guac_terminal_color_scheme_strip_spaces(const char** str_start, /* Strip trailing spaces. */ while (*str_end > *str_start && isspace(*(*str_end - 1))) (*str_end)--; + +#endif + } /** diff --git a/src/terminal/display.c b/src/terminal/display.c index 3040ad424e..f93f9e8fc3 100644 --- a/src/terminal/display.c +++ b/src/terminal/display.c @@ -26,6 +26,10 @@ #include "terminal/terminal-priv.h" #include "terminal/types.h" +#ifdef WINDOWS_BUILD +#include +#endif + #include #include #include diff --git a/src/terminal/named-colors.c b/src/terminal/named-colors.c index 948982afe3..d51eacf007 100644 --- a/src/terminal/named-colors.c +++ b/src/terminal/named-colors.c @@ -17,6 +17,8 @@ * under the License. */ +#include "config.h" + #include "terminal/palette.h" #include @@ -758,7 +760,14 @@ static int guac_terminal_named_color_search(const void* a, const void* b) { for (; *key && *name; key++, name++) { /* Skip any spaces in key (name will never have spaces) */ + +#ifdef WINDOWS_BUILD + + /* Cast to unsigned to avoid error: array subscript has type 'char' */ + while (*key && isspace((unsigned char) *key)) key++; +#else while (*key && isspace(*key)) key++; +#endif /* Treat semi-colon as string terminator, to support parsing color names within a larger string (e.g. within the terminal color-scheme diff --git a/src/terminal/terminal-handlers.c b/src/terminal/terminal-handlers.c index bba2c38c15..0a24b32d5b 100644 --- a/src/terminal/terminal-handlers.c +++ b/src/terminal/terminal-handlers.c @@ -27,6 +27,10 @@ #include "terminal/types.h" #include "terminal/xparsecolor.h" +#ifdef WINDOWS_BUILD +#include +#endif + #include #include #include diff --git a/src/terminal/terminal.c b/src/terminal/terminal.c index 539d1e9448..070ecdabb4 100644 --- a/src/terminal/terminal.c +++ b/src/terminal/terminal.c @@ -52,6 +52,11 @@ #include #include +#ifdef WINDOWS_BUILD +#include "terminal/wcwidth.h" +#include +#endif + /** * Sets the given range of columns to the given character. */ diff --git a/src/terminal/terminal/wcwidth.h b/src/terminal/terminal/wcwidth.h new file mode 100644 index 0000000000..4cabda1614 --- /dev/null +++ b/src/terminal/terminal/wcwidth.h @@ -0,0 +1,39 @@ +/* + * 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_WCWIDTH_H +#define GUAC_TERMINAL_WCWIDTH_H + +/** + * Returns the number of columns needed to represent the provided wide characer. + * + * @param c + * The wide character to determine the width of. + * + * @return + * The number of columns needed to represent the provided wide characer. + */ +int mk_wcwidth(wchar_t c); + +/** + * Defer to the mk_wcwidth() implementation. + */ +#define wcwidth mk_wcwidth + +#endif diff --git a/src/terminal/typescript.c b/src/terminal/typescript.c index abc8486f04..fbb2549f5c 100644 --- a/src/terminal/typescript.c +++ b/src/terminal/typescript.c @@ -32,6 +32,10 @@ #include #include +#ifdef WINDOWS_BUILD +#include +#endif + /** * Attempts to open a new typescript data file within the given path and having * the given name. If such a file already exists, sequential numeric suffixes @@ -112,7 +116,12 @@ guac_terminal_typescript* guac_terminal_typescript_alloc(const char* path, const char* name, int create_path) { /* Create path if it does not exist, fail if impossible */ - if (create_path && mkdir(path, S_IRWXU | S_IRGRP | S_IXGRP) + if (create_path +#ifdef WINDOWS_BUILD + && _mkdir(path) +#else + && mkdir(path, S_IRWXU | S_IRGRP | S_IXGRP) +#endif && errno != EEXIST) return NULL; diff --git a/src/terminal/wcwidth.c b/src/terminal/wcwidth.c new file mode 100644 index 0000000000..3b799da665 --- /dev/null +++ b/src/terminal/wcwidth.c @@ -0,0 +1,309 @@ +/* + * This is an implementation of wcwidth() and wcswidth() (defined in + * IEEE Std 1002.1-2001) for Unicode. + * + * http://www.opengroup.org/onlinepubs/007904975/functions/wcwidth.html + * http://www.opengroup.org/onlinepubs/007904975/functions/wcswidth.html + * + * In fixed-width output devices, Latin characters all occupy a single + * "cell" position of equal width, whereas ideographic CJK characters + * occupy two such cells. Interoperability between terminal-line + * applications and (teletype-style) character terminals using the + * UTF-8 encoding requires agreement on which character should advance + * the cursor by how many cell positions. No established formal + * standards exist at present on which Unicode character shall occupy + * how many cell positions on character terminals. These routines are + * a first attempt of defining such behavior based on simple rules + * applied to data provided by the Unicode Consortium. + * + * For some graphical characters, the Unicode standard explicitly + * defines a character-cell width via the definition of the East Asian + * FullWidth (F), Wide (W), Half-width (H), and Narrow (Na) classes. + * In all these cases, there is no ambiguity about which width a + * terminal shall use. For characters in the East Asian Ambiguous (A) + * class, the width choice depends purely on a preference of backward + * compatibility with either historic CJK or Western practice. + * Choosing single-width for these characters is easy to justify as + * the appropriate long-term solution, as the CJK practice of + * displaying these characters as double-width comes from historic + * implementation simplicity (8-bit encoded characters were displayed + * single-width and 16-bit ones double-width, even for Greek, + * Cyrillic, etc.) and not any typographic considerations. + * + * Much less clear is the choice of width for the Not East Asian + * (Neutral) class. Existing practice does not dictate a width for any + * of these characters. It would nevertheless make sense + * typographically to allocate two character cells to characters such + * as for instance EM SPACE or VOLUME INTEGRAL, which cannot be + * represented adequately with a single-width glyph. The following + * routines at present merely assign a single-cell width to all + * neutral characters, in the interest of simplicity. This is not + * entirely satisfactory and should be reconsidered before + * establishing a formal standard in this area. At the moment, the + * decision which Not East Asian (Neutral) characters should be + * represented by double-width glyphs cannot yet be answered by + * applying a simple rule from the Unicode database content. Setting + * up a proper standard for the behavior of UTF-8 character terminals + * will require a careful analysis not only of each Unicode character, + * but also of each presentation form, something the author of these + * routines has avoided to do so far. + * + * http://www.unicode.org/unicode/reports/tr11/ + * + * Markus Kuhn -- 2007-05-26 (Unicode 5.0) + * + * Permission to use, copy, modify, and distribute this software + * for any purpose and without fee is hereby granted. The author + * disclaims all warranties with regard to this software. + * + * Latest version: http://www.cl.cam.ac.uk/~mgk25/ucs/wcwidth.c + */ + +#include + +struct interval { + int first; + int last; +}; + +/* auxiliary function for binary search in interval table */ +static int bisearch(wchar_t ucs, const struct interval *table, int max) { + int min = 0; + int mid; + + if (ucs < table[0].first || ucs > table[max].last) + return 0; + while (max >= min) { + mid = (min + max) / 2; + if (ucs > table[mid].last) + min = mid + 1; + else if (ucs < table[mid].first) + max = mid - 1; + else + return 1; + } + + return 0; +} + + +/* The following two functions define the column width of an ISO 10646 + * character as follows: + * + * - The null character (U+0000) has a column width of 0. + * + * - Other C0/C1 control characters and DEL will lead to a return + * value of -1. + * + * - Non-spacing and enclosing combining characters (general + * category code Mn or Me in the Unicode database) have a + * column width of 0. + * + * - SOFT HYPHEN (U+00AD) has a column width of 1. + * + * - Other format characters (general category code Cf in the Unicode + * database) and ZERO WIDTH SPACE (U+200B) have a column width of 0. + * + * - Hangul Jamo medial vowels and final consonants (U+1160-U+11FF) + * have a column width of 0. + * + * - Spacing characters in the East Asian Wide (W) or East Asian + * Full-width (F) category as defined in Unicode Technical + * Report #11 have a column width of 2. + * + * - All remaining characters (including all printable + * ISO 8859-1 and WGL4 characters, Unicode control characters, + * etc.) have a column width of 1. + * + * This implementation assumes that wchar_t characters are encoded + * in ISO 10646. + */ + +int mk_wcwidth(wchar_t ucs) +{ + /* sorted list of non-overlapping intervals of non-spacing characters */ + /* generated by "uniset +cat=Me +cat=Mn +cat=Cf -00AD +1160-11FF +200B c" */ + static const struct interval combining[] = { + { 0x0300, 0x036F }, { 0x0483, 0x0486 }, { 0x0488, 0x0489 }, + { 0x0591, 0x05BD }, { 0x05BF, 0x05BF }, { 0x05C1, 0x05C2 }, + { 0x05C4, 0x05C5 }, { 0x05C7, 0x05C7 }, { 0x0600, 0x0603 }, + { 0x0610, 0x0615 }, { 0x064B, 0x065E }, { 0x0670, 0x0670 }, + { 0x06D6, 0x06E4 }, { 0x06E7, 0x06E8 }, { 0x06EA, 0x06ED }, + { 0x070F, 0x070F }, { 0x0711, 0x0711 }, { 0x0730, 0x074A }, + { 0x07A6, 0x07B0 }, { 0x07EB, 0x07F3 }, { 0x0901, 0x0902 }, + { 0x093C, 0x093C }, { 0x0941, 0x0948 }, { 0x094D, 0x094D }, + { 0x0951, 0x0954 }, { 0x0962, 0x0963 }, { 0x0981, 0x0981 }, + { 0x09BC, 0x09BC }, { 0x09C1, 0x09C4 }, { 0x09CD, 0x09CD }, + { 0x09E2, 0x09E3 }, { 0x0A01, 0x0A02 }, { 0x0A3C, 0x0A3C }, + { 0x0A41, 0x0A42 }, { 0x0A47, 0x0A48 }, { 0x0A4B, 0x0A4D }, + { 0x0A70, 0x0A71 }, { 0x0A81, 0x0A82 }, { 0x0ABC, 0x0ABC }, + { 0x0AC1, 0x0AC5 }, { 0x0AC7, 0x0AC8 }, { 0x0ACD, 0x0ACD }, + { 0x0AE2, 0x0AE3 }, { 0x0B01, 0x0B01 }, { 0x0B3C, 0x0B3C }, + { 0x0B3F, 0x0B3F }, { 0x0B41, 0x0B43 }, { 0x0B4D, 0x0B4D }, + { 0x0B56, 0x0B56 }, { 0x0B82, 0x0B82 }, { 0x0BC0, 0x0BC0 }, + { 0x0BCD, 0x0BCD }, { 0x0C3E, 0x0C40 }, { 0x0C46, 0x0C48 }, + { 0x0C4A, 0x0C4D }, { 0x0C55, 0x0C56 }, { 0x0CBC, 0x0CBC }, + { 0x0CBF, 0x0CBF }, { 0x0CC6, 0x0CC6 }, { 0x0CCC, 0x0CCD }, + { 0x0CE2, 0x0CE3 }, { 0x0D41, 0x0D43 }, { 0x0D4D, 0x0D4D }, + { 0x0DCA, 0x0DCA }, { 0x0DD2, 0x0DD4 }, { 0x0DD6, 0x0DD6 }, + { 0x0E31, 0x0E31 }, { 0x0E34, 0x0E3A }, { 0x0E47, 0x0E4E }, + { 0x0EB1, 0x0EB1 }, { 0x0EB4, 0x0EB9 }, { 0x0EBB, 0x0EBC }, + { 0x0EC8, 0x0ECD }, { 0x0F18, 0x0F19 }, { 0x0F35, 0x0F35 }, + { 0x0F37, 0x0F37 }, { 0x0F39, 0x0F39 }, { 0x0F71, 0x0F7E }, + { 0x0F80, 0x0F84 }, { 0x0F86, 0x0F87 }, { 0x0F90, 0x0F97 }, + { 0x0F99, 0x0FBC }, { 0x0FC6, 0x0FC6 }, { 0x102D, 0x1030 }, + { 0x1032, 0x1032 }, { 0x1036, 0x1037 }, { 0x1039, 0x1039 }, + { 0x1058, 0x1059 }, { 0x1160, 0x11FF }, { 0x135F, 0x135F }, + { 0x1712, 0x1714 }, { 0x1732, 0x1734 }, { 0x1752, 0x1753 }, + { 0x1772, 0x1773 }, { 0x17B4, 0x17B5 }, { 0x17B7, 0x17BD }, + { 0x17C6, 0x17C6 }, { 0x17C9, 0x17D3 }, { 0x17DD, 0x17DD }, + { 0x180B, 0x180D }, { 0x18A9, 0x18A9 }, { 0x1920, 0x1922 }, + { 0x1927, 0x1928 }, { 0x1932, 0x1932 }, { 0x1939, 0x193B }, + { 0x1A17, 0x1A18 }, { 0x1B00, 0x1B03 }, { 0x1B34, 0x1B34 }, + { 0x1B36, 0x1B3A }, { 0x1B3C, 0x1B3C }, { 0x1B42, 0x1B42 }, + { 0x1B6B, 0x1B73 }, { 0x1DC0, 0x1DCA }, { 0x1DFE, 0x1DFF }, + { 0x200B, 0x200F }, { 0x202A, 0x202E }, { 0x2060, 0x2063 }, + { 0x206A, 0x206F }, { 0x20D0, 0x20EF }, { 0x302A, 0x302F }, + { 0x3099, 0x309A }, { 0xA806, 0xA806 }, { 0xA80B, 0xA80B }, + { 0xA825, 0xA826 }, { 0xFB1E, 0xFB1E }, { 0xFE00, 0xFE0F }, + { 0xFE20, 0xFE23 }, { 0xFEFF, 0xFEFF }, { 0xFFF9, 0xFFFB }, + { 0x10A01, 0x10A03 }, { 0x10A05, 0x10A06 }, { 0x10A0C, 0x10A0F }, + { 0x10A38, 0x10A3A }, { 0x10A3F, 0x10A3F }, { 0x1D167, 0x1D169 }, + { 0x1D173, 0x1D182 }, { 0x1D185, 0x1D18B }, { 0x1D1AA, 0x1D1AD }, + { 0x1D242, 0x1D244 }, { 0xE0001, 0xE0001 }, { 0xE0020, 0xE007F }, + { 0xE0100, 0xE01EF } + }; + + /* test for 8-bit control characters */ + if (ucs == 0) + return 0; + if (ucs < 32 || (ucs >= 0x7f && ucs < 0xa0)) + return -1; + + /* binary search in table of non-spacing characters */ + if (bisearch(ucs, combining, + sizeof(combining) / sizeof(struct interval) - 1)) + return 0; + + /* if we arrive here, ucs is not a combining or C0/C1 control character */ + + return 1 + + (ucs >= 0x1100 && + (ucs <= 0x115f || /* Hangul Jamo init. consonants */ + ucs == 0x2329 || ucs == 0x232a || + (ucs >= 0x2e80 && ucs <= 0xa4cf && + ucs != 0x303f) || /* CJK ... Yi */ + (ucs >= 0xac00 && ucs <= 0xd7a3) || /* Hangul Syllables */ + (ucs >= 0xf900 && ucs <= 0xfaff) || /* CJK Compatibility Ideographs */ + (ucs >= 0xfe10 && ucs <= 0xfe19) || /* Vertical forms */ + (ucs >= 0xfe30 && ucs <= 0xfe6f) || /* CJK Compatibility Forms */ + (ucs >= 0xff00 && ucs <= 0xff60) || /* Fullwidth Forms */ + (ucs >= 0xffe0 && ucs <= 0xffe6) || + (ucs >= 0x20000 && ucs <= 0x2fffd) || + (ucs >= 0x30000 && ucs <= 0x3fffd))); +} + + +int mk_wcswidth(const wchar_t *pwcs, size_t n) +{ + int w, width = 0; + + for (;*pwcs && n-- > 0; pwcs++) + if ((w = mk_wcwidth(*pwcs)) < 0) + return -1; + else + width += w; + + return width; +} + + +/* + * The following functions are the same as mk_wcwidth() and + * mk_wcswidth(), except that spacing characters in the East Asian + * Ambiguous (A) category as defined in Unicode Technical Report #11 + * have a column width of 2. This variant might be useful for users of + * CJK legacy encodings who want to migrate to UCS without changing + * the traditional terminal character-width behaviour. It is not + * otherwise recommended for general use. + */ +int mk_wcwidth_cjk(wchar_t ucs) +{ + /* sorted list of non-overlapping intervals of East Asian Ambiguous + * characters, generated by "uniset +WIDTH-A -cat=Me -cat=Mn -cat=Cf c" */ + static const struct interval ambiguous[] = { + { 0x00A1, 0x00A1 }, { 0x00A4, 0x00A4 }, { 0x00A7, 0x00A8 }, + { 0x00AA, 0x00AA }, { 0x00AE, 0x00AE }, { 0x00B0, 0x00B4 }, + { 0x00B6, 0x00BA }, { 0x00BC, 0x00BF }, { 0x00C6, 0x00C6 }, + { 0x00D0, 0x00D0 }, { 0x00D7, 0x00D8 }, { 0x00DE, 0x00E1 }, + { 0x00E6, 0x00E6 }, { 0x00E8, 0x00EA }, { 0x00EC, 0x00ED }, + { 0x00F0, 0x00F0 }, { 0x00F2, 0x00F3 }, { 0x00F7, 0x00FA }, + { 0x00FC, 0x00FC }, { 0x00FE, 0x00FE }, { 0x0101, 0x0101 }, + { 0x0111, 0x0111 }, { 0x0113, 0x0113 }, { 0x011B, 0x011B }, + { 0x0126, 0x0127 }, { 0x012B, 0x012B }, { 0x0131, 0x0133 }, + { 0x0138, 0x0138 }, { 0x013F, 0x0142 }, { 0x0144, 0x0144 }, + { 0x0148, 0x014B }, { 0x014D, 0x014D }, { 0x0152, 0x0153 }, + { 0x0166, 0x0167 }, { 0x016B, 0x016B }, { 0x01CE, 0x01CE }, + { 0x01D0, 0x01D0 }, { 0x01D2, 0x01D2 }, { 0x01D4, 0x01D4 }, + { 0x01D6, 0x01D6 }, { 0x01D8, 0x01D8 }, { 0x01DA, 0x01DA }, + { 0x01DC, 0x01DC }, { 0x0251, 0x0251 }, { 0x0261, 0x0261 }, + { 0x02C4, 0x02C4 }, { 0x02C7, 0x02C7 }, { 0x02C9, 0x02CB }, + { 0x02CD, 0x02CD }, { 0x02D0, 0x02D0 }, { 0x02D8, 0x02DB }, + { 0x02DD, 0x02DD }, { 0x02DF, 0x02DF }, { 0x0391, 0x03A1 }, + { 0x03A3, 0x03A9 }, { 0x03B1, 0x03C1 }, { 0x03C3, 0x03C9 }, + { 0x0401, 0x0401 }, { 0x0410, 0x044F }, { 0x0451, 0x0451 }, + { 0x2010, 0x2010 }, { 0x2013, 0x2016 }, { 0x2018, 0x2019 }, + { 0x201C, 0x201D }, { 0x2020, 0x2022 }, { 0x2024, 0x2027 }, + { 0x2030, 0x2030 }, { 0x2032, 0x2033 }, { 0x2035, 0x2035 }, + { 0x203B, 0x203B }, { 0x203E, 0x203E }, { 0x2074, 0x2074 }, + { 0x207F, 0x207F }, { 0x2081, 0x2084 }, { 0x20AC, 0x20AC }, + { 0x2103, 0x2103 }, { 0x2105, 0x2105 }, { 0x2109, 0x2109 }, + { 0x2113, 0x2113 }, { 0x2116, 0x2116 }, { 0x2121, 0x2122 }, + { 0x2126, 0x2126 }, { 0x212B, 0x212B }, { 0x2153, 0x2154 }, + { 0x215B, 0x215E }, { 0x2160, 0x216B }, { 0x2170, 0x2179 }, + { 0x2190, 0x2199 }, { 0x21B8, 0x21B9 }, { 0x21D2, 0x21D2 }, + { 0x21D4, 0x21D4 }, { 0x21E7, 0x21E7 }, { 0x2200, 0x2200 }, + { 0x2202, 0x2203 }, { 0x2207, 0x2208 }, { 0x220B, 0x220B }, + { 0x220F, 0x220F }, { 0x2211, 0x2211 }, { 0x2215, 0x2215 }, + { 0x221A, 0x221A }, { 0x221D, 0x2220 }, { 0x2223, 0x2223 }, + { 0x2225, 0x2225 }, { 0x2227, 0x222C }, { 0x222E, 0x222E }, + { 0x2234, 0x2237 }, { 0x223C, 0x223D }, { 0x2248, 0x2248 }, + { 0x224C, 0x224C }, { 0x2252, 0x2252 }, { 0x2260, 0x2261 }, + { 0x2264, 0x2267 }, { 0x226A, 0x226B }, { 0x226E, 0x226F }, + { 0x2282, 0x2283 }, { 0x2286, 0x2287 }, { 0x2295, 0x2295 }, + { 0x2299, 0x2299 }, { 0x22A5, 0x22A5 }, { 0x22BF, 0x22BF }, + { 0x2312, 0x2312 }, { 0x2460, 0x24E9 }, { 0x24EB, 0x254B }, + { 0x2550, 0x2573 }, { 0x2580, 0x258F }, { 0x2592, 0x2595 }, + { 0x25A0, 0x25A1 }, { 0x25A3, 0x25A9 }, { 0x25B2, 0x25B3 }, + { 0x25B6, 0x25B7 }, { 0x25BC, 0x25BD }, { 0x25C0, 0x25C1 }, + { 0x25C6, 0x25C8 }, { 0x25CB, 0x25CB }, { 0x25CE, 0x25D1 }, + { 0x25E2, 0x25E5 }, { 0x25EF, 0x25EF }, { 0x2605, 0x2606 }, + { 0x2609, 0x2609 }, { 0x260E, 0x260F }, { 0x2614, 0x2615 }, + { 0x261C, 0x261C }, { 0x261E, 0x261E }, { 0x2640, 0x2640 }, + { 0x2642, 0x2642 }, { 0x2660, 0x2661 }, { 0x2663, 0x2665 }, + { 0x2667, 0x266A }, { 0x266C, 0x266D }, { 0x266F, 0x266F }, + { 0x273D, 0x273D }, { 0x2776, 0x277F }, { 0xE000, 0xF8FF }, + { 0xFFFD, 0xFFFD }, { 0xF0000, 0xFFFFD }, { 0x100000, 0x10FFFD } + }; + + /* binary search in table of non-spacing characters */ + if (bisearch(ucs, ambiguous, + sizeof(ambiguous) / sizeof(struct interval) - 1)) + return 2; + + return mk_wcwidth(ucs); +} + + +int mk_wcswidth_cjk(const wchar_t *pwcs, size_t n) +{ + int w, width = 0; + + for (;*pwcs && n-- > 0; pwcs++) + if ((w = mk_wcwidth_cjk(*pwcs)) < 0) + return -1; + else + width += w; + + return width; +}