From de0fdeebc12d1830cfcdd50119fcba6ad3c3a9fe Mon Sep 17 00:00:00 2001 From: zhouganqing Date: Mon, 21 Nov 2022 15:16:05 +0800 Subject: [PATCH] Import Upstream version 2.8.1 --- .clang-format | 125 + .github/ISSUE_TEMPLATE.md | 25 + .github/ISSUE_TEMPLATE/bug_report.md | 56 + .github/ISSUE_TEMPLATE/feature_request.md | 17 + .github/PULL_REQUEST_TEMPLATE.md | 25 + .github/workflows/codeql-analysis.yml | 71 + .gitignore | 154 + .source_version | 1 + .travis.yml | 56 + CMakeCPack.cmake | 102 + CMakeCPackOptions.cmake.in | 10 + CMakeLists.txt | 1117 ++ ChangeLog | 638 ++ LICENSE | 202 + README.md | 30 + buildflags.h.in | 11 + channels/CMakeLists.txt | 346 + channels/ainput/CMakeLists.txt | 27 + channels/ainput/ChannelOptions.cmake | 13 + channels/ainput/client/CMakeLists.txt | 34 + channels/ainput/client/ainput_main.c | 315 + channels/ainput/client/ainput_main.h | 43 + channels/ainput/common/ainput_common.h | 59 + channels/ainput/server/CMakeLists.txt | 27 + channels/ainput/server/ainput_main.c | 587 ++ channels/audin/CMakeLists.txt | 26 + channels/audin/ChannelOptions.cmake | 17 + channels/audin/client/CMakeLists.txt | 58 + channels/audin/client/alsa/CMakeLists.txt | 32 + channels/audin/client/alsa/audin_alsa.c | 459 + channels/audin/client/audin_main.c | 1099 ++ channels/audin/client/audin_main.h | 35 + channels/audin/client/mac/CMakeLists.txt | 35 + channels/audin/client/mac/audin_mac.m | 466 + channels/audin/client/opensles/CMakeLists.txt | 33 + .../audin/client/opensles/audin_opensl_es.c | 342 + channels/audin/client/opensles/opensl_io.c | 388 + channels/audin/client/opensles/opensl_io.h | 65 + channels/audin/client/oss/CMakeLists.txt | 33 + channels/audin/client/oss/audin_oss.c | 488 + channels/audin/client/pulse/CMakeLists.txt | 31 + channels/audin/client/pulse/audin_pulse.c | 572 + channels/audin/client/winmm/CMakeLists.txt | 38 + channels/audin/client/winmm/audin_winmm.c | 568 + channels/audin/server/CMakeLists.txt | 29 + channels/audin/server/audin.c | 692 ++ channels/client/.gitignore | 2 + channels/client/CMakeLists.txt | 117 + channels/client/addin.c | 470 + channels/client/addin.h | 18 + channels/client/tables.c.in | 32 + channels/client/tables.h | 51 + channels/cliprdr/CMakeLists.txt | 26 + channels/cliprdr/ChannelOptions.cmake | 13 + channels/cliprdr/client/CMakeLists.txt | 33 + channels/cliprdr/client/cliprdr_format.c | 175 + channels/cliprdr/client/cliprdr_format.h | 35 + channels/cliprdr/client/cliprdr_main.c | 1222 +++ channels/cliprdr/client/cliprdr_main.h | 67 + channels/cliprdr/cliprdr_common.c | 588 ++ channels/cliprdr/cliprdr_common.h | 61 + channels/cliprdr/server/CMakeLists.txt | 31 + channels/cliprdr/server/cliprdr_main.c | 1420 +++ channels/cliprdr/server/cliprdr_main.h | 48 + channels/disp/CMakeLists.txt | 26 + channels/disp/ChannelOptions.cmake | 12 + channels/disp/client/CMakeLists.txt | 41 + channels/disp/client/disp_main.c | 419 + channels/disp/client/disp_main.h | 38 + channels/disp/disp_common.c | 60 + channels/disp/disp_common.h | 32 + channels/disp/server/CMakeLists.txt | 32 + channels/disp/server/disp_main.c | 597 ++ channels/disp/server/disp_main.h | 37 + channels/drdynvc/CMakeLists.txt | 26 + channels/drdynvc/ChannelOptions.cmake | 13 + channels/drdynvc/client/CMakeLists.txt | 27 + channels/drdynvc/client/drdynvc_main.c | 1826 ++++ channels/drdynvc/client/drdynvc_main.h | 135 + channels/drdynvc/server/CMakeLists.txt | 31 + channels/drdynvc/server/drdynvc_main.c | 205 + channels/drdynvc/server/drdynvc_main.h | 37 + channels/drive/CMakeLists.txt | 23 + channels/drive/ChannelOptions.cmake | 13 + channels/drive/client/CMakeLists.txt | 36 + channels/drive/client/drive_file.c | 934 ++ channels/drive/client/drive_file.h | 69 + channels/drive/client/drive_main.c | 1112 ++ channels/echo/CMakeLists.txt | 26 + channels/echo/ChannelOptions.cmake | 13 + channels/echo/client/CMakeLists.txt | 33 + channels/echo/client/echo_main.c | 216 + channels/echo/client/echo_main.h | 42 + channels/echo/server/CMakeLists.txt | 30 + channels/echo/server/echo_main.c | 372 + channels/encomsp/CMakeLists.txt | 26 + channels/encomsp/ChannelOptions.cmake | 13 + channels/encomsp/client/CMakeLists.txt | 29 + channels/encomsp/client/encomsp_main.c | 1349 +++ channels/encomsp/client/encomsp_main.h | 42 + channels/encomsp/server/CMakeLists.txt | 35 + channels/encomsp/server/encomsp_main.c | 379 + channels/encomsp/server/encomsp_main.h | 36 + channels/geometry/CMakeLists.txt | 22 + channels/geometry/ChannelOptions.cmake | 11 + channels/geometry/client/CMakeLists.txt | 40 + channels/geometry/client/geometry_main.c | 501 + channels/geometry/client/geometry_main.h | 32 + channels/parallel/CMakeLists.txt | 22 + channels/parallel/ChannelOptions.cmake | 23 + channels/parallel/client/CMakeLists.txt | 34 + channels/parallel/client/parallel_main.c | 497 + channels/printer/CMakeLists.txt | 22 + channels/printer/ChannelOptions.cmake | 24 + channels/printer/client/CMakeLists.txt | 40 + channels/printer/client/cups/CMakeLists.txt | 31 + channels/printer/client/cups/printer_cups.c | 410 + channels/printer/client/printer_main.c | 1073 ++ channels/printer/client/win/CMakeLists.txt | 29 + channels/printer/client/win/printer_win.c | 459 + channels/printer/printer.h | 36 + channels/rail/CMakeLists.txt | 26 + channels/rail/ChannelOptions.cmake | 13 + channels/rail/client/CMakeLists.txt | 35 + channels/rail/client/rail_main.c | 895 ++ channels/rail/client/rail_main.h | 63 + channels/rail/client/rail_orders.c | 1591 +++ channels/rail/client/rail_orders.h | 60 + channels/rail/rail_common.c | 618 ++ channels/rail/rail_common.h | 76 + channels/rail/server/CMakeLists.txt | 32 + channels/rail/server/rail_main.c | 1688 +++ channels/rail/server/rail_main.h | 44 + channels/rdp2tcp/CMakeLists.txt | 22 + channels/rdp2tcp/ChannelOptions.cmake | 10 + channels/rdp2tcp/client/CMakeLists.txt | 27 + channels/rdp2tcp/client/rdp2tcp_main.c | 327 + channels/rdpdr/CMakeLists.txt | 26 + channels/rdpdr/ChannelOptions.cmake | 13 + channels/rdpdr/client/CMakeLists.txt | 43 + channels/rdpdr/client/devman.c | 231 + channels/rdpdr/client/devman.h | 36 + channels/rdpdr/client/irp.c | 150 + channels/rdpdr/client/irp.h | 28 + channels/rdpdr/client/rdpdr_capabilities.c | 281 + channels/rdpdr/client/rdpdr_capabilities.h | 31 + channels/rdpdr/client/rdpdr_main.c | 1922 ++++ channels/rdpdr/client/rdpdr_main.h | 85 + channels/rdpdr/server/CMakeLists.txt | 31 + channels/rdpdr/server/rdpdr_main.c | 2626 +++++ channels/rdpdr/server/rdpdr_main.h | 91 + channels/rdpecam/CMakeLists.txt | 22 + channels/rdpecam/ChannelOptions.cmake | 12 + channels/rdpecam/server/CMakeLists.txt | 27 + .../server/camera_device_enumerator_main.c | 612 ++ channels/rdpecam/server/camera_device_main.c | 971 ++ channels/rdpei/CMakeLists.txt | 26 + channels/rdpei/ChannelOptions.cmake | 13 + channels/rdpei/client/CMakeLists.txt | 38 + channels/rdpei/client/rdpei_main.c | 1391 +++ channels/rdpei/client/rdpei_main.h | 93 + channels/rdpei/rdpei_common.c | 643 ++ channels/rdpei/rdpei_common.h | 57 + channels/rdpei/server/CMakeLists.txt | 35 + channels/rdpei/server/rdpei_main.c | 746 ++ channels/rdpei/server/rdpei_main.h | 32 + channels/rdpgfx/CMakeLists.txt | 26 + channels/rdpgfx/ChannelOptions.cmake | 13 + channels/rdpgfx/client/CMakeLists.txt | 41 + channels/rdpgfx/client/rdpgfx_codec.c | 309 + channels/rdpgfx/client/rdpgfx_codec.h | 35 + channels/rdpgfx/client/rdpgfx_main.c | 2219 ++++ channels/rdpgfx/client/rdpgfx_main.h | 92 + channels/rdpgfx/rdpgfx_common.c | 251 + channels/rdpgfx/rdpgfx_common.h | 55 + channels/rdpgfx/server/CMakeLists.txt | 34 + channels/rdpgfx/server/rdpgfx_main.c | 1720 +++ channels/rdpgfx/server/rdpgfx_main.h | 40 + channels/rdpsnd/CMakeLists.txt | 29 + channels/rdpsnd/ChannelOptions.cmake | 13 + channels/rdpsnd/client/CMakeLists.txt | 64 + channels/rdpsnd/client/alsa/CMakeLists.txt | 36 + channels/rdpsnd/client/alsa/rdpsnd_alsa.c | 576 + channels/rdpsnd/client/fake/CMakeLists.txt | 33 + channels/rdpsnd/client/fake/rdpsnd_fake.c | 155 + channels/rdpsnd/client/ios/CMakeLists.txt | 45 + channels/rdpsnd/client/ios/TPCircularBuffer.c | 153 + channels/rdpsnd/client/ios/TPCircularBuffer.h | 217 + channels/rdpsnd/client/ios/rdpsnd_ios.c | 296 + channels/rdpsnd/client/mac/CMakeLists.txt | 49 + channels/rdpsnd/client/mac/rdpsnd_mac.m | 403 + .../rdpsnd/client/opensles/CMakeLists.txt | 33 + channels/rdpsnd/client/opensles/opensl_io.c | 421 + channels/rdpsnd/client/opensles/opensl_io.h | 110 + .../rdpsnd/client/opensles/rdpsnd_opensles.c | 386 + channels/rdpsnd/client/oss/CMakeLists.txt | 36 + channels/rdpsnd/client/oss/rdpsnd_oss.c | 475 + channels/rdpsnd/client/proxy/CMakeLists.txt | 33 + channels/rdpsnd/client/proxy/rdpsnd_proxy.c | 144 + channels/rdpsnd/client/pulse/CMakeLists.txt | 34 + channels/rdpsnd/client/pulse/rdpsnd_pulse.c | 631 ++ channels/rdpsnd/client/rdpsnd_main.c | 1706 +++ channels/rdpsnd/client/rdpsnd_main.h | 43 + channels/rdpsnd/client/winmm/CMakeLists.txt | 39 + channels/rdpsnd/client/winmm/rdpsnd_winmm.c | 356 + channels/rdpsnd/common/CMakeLists.txt | 24 + channels/rdpsnd/common/rdpsnd_common.h | 43 + channels/rdpsnd/server/CMakeLists.txt | 26 + channels/rdpsnd/server/rdpsnd_main.c | 1228 +++ channels/rdpsnd/server/rdpsnd_main.h | 59 + channels/remdesk/CMakeLists.txt | 26 + channels/remdesk/ChannelOptions.cmake | 12 + channels/remdesk/client/CMakeLists.txt | 31 + channels/remdesk/client/remdesk_main.c | 1071 ++ channels/remdesk/client/remdesk_main.h | 63 + channels/remdesk/server/CMakeLists.txt | 31 + channels/remdesk/server/remdesk_main.c | 787 ++ channels/remdesk/server/remdesk_main.h | 41 + channels/serial/CMakeLists.txt | 23 + channels/serial/ChannelOptions.cmake | 23 + channels/serial/client/CMakeLists.txt | 34 + channels/serial/client/serial_main.c | 968 ++ channels/server/CMakeLists.txt | 43 + channels/server/channels.c | 131 + channels/server/channels.h | 24 + channels/smartcard/CMakeLists.txt | 22 + channels/smartcard/ChannelOptions.cmake | 12 + channels/smartcard/client/CMakeLists.txt | 35 + channels/smartcard/client/smartcard_main.c | 810 ++ channels/smartcard/client/smartcard_main.h | 73 + .../smartcard/client/smartcard_operations.c | 2695 +++++ .../smartcard/client/smartcard_operations.h | 546 + channels/smartcard/client/smartcard_pack.c | 3888 +++++++ channels/smartcard/client/smartcard_pack.h | 196 + channels/sshagent/CMakeLists.txt | 23 + channels/sshagent/ChannelOptions.cmake | 12 + channels/sshagent/client/CMakeLists.txt | 34 + channels/sshagent/client/sshagent_main.c | 388 + channels/sshagent/client/sshagent_main.h | 44 + channels/telemetry/CMakeLists.txt | 22 + channels/telemetry/ChannelOptions.cmake | 12 + channels/telemetry/server/CMakeLists.txt | 26 + channels/telemetry/server/telemetry_main.c | 443 + channels/tsmf/CMakeLists.txt | 22 + channels/tsmf/ChannelOptions.cmake | 23 + channels/tsmf/client/CMakeLists.txt | 114 + channels/tsmf/client/alsa/CMakeLists.txt | 29 + channels/tsmf/client/alsa/tsmf_alsa.c | 248 + channels/tsmf/client/ffmpeg/CMakeLists.txt | 45 + channels/tsmf/client/ffmpeg/tsmf_ffmpeg.c | 667 ++ channels/tsmf/client/gstreamer/CMakeLists.txt | 65 + channels/tsmf/client/gstreamer/tsmf_X11.c | 506 + .../tsmf/client/gstreamer/tsmf_gstreamer.c | 1072 ++ .../tsmf/client/gstreamer/tsmf_platform.h | 85 + channels/tsmf/client/oss/CMakeLists.txt | 28 + channels/tsmf/client/oss/tsmf_oss.c | 252 + channels/tsmf/client/pulse/CMakeLists.txt | 27 + channels/tsmf/client/pulse/tsmf_pulse.c | 422 + channels/tsmf/client/tsmf_audio.c | 99 + channels/tsmf/client/tsmf_audio.h | 51 + channels/tsmf/client/tsmf_codec.c | 612 ++ channels/tsmf/client/tsmf_codec.h | 28 + channels/tsmf/client/tsmf_constants.h | 139 + channels/tsmf/client/tsmf_decoder.c | 122 + channels/tsmf/client/tsmf_decoder.h | 78 + channels/tsmf/client/tsmf_ifman.c | 841 ++ channels/tsmf/client/tsmf_ifman.h | 69 + channels/tsmf/client/tsmf_main.c | 624 ++ channels/tsmf/client/tsmf_main.h | 71 + channels/tsmf/client/tsmf_media.c | 1552 +++ channels/tsmf/client/tsmf_media.h | 72 + channels/tsmf/client/tsmf_types.h | 63 + channels/urbdrc/CMakeLists.txt | 30 + channels/urbdrc/ChannelOptions.cmake | 18 + channels/urbdrc/client/CMakeLists.txt | 47 + channels/urbdrc/client/data_transfer.c | 1856 ++++ channels/urbdrc/client/data_transfer.h | 36 + channels/urbdrc/client/libusb/CMakeLists.txt | 46 + .../urbdrc/client/libusb/libusb_udevice.c | 1796 ++++ .../urbdrc/client/libusb/libusb_udevice.h | 78 + .../urbdrc/client/libusb/libusb_udevman.c | 975 ++ channels/urbdrc/client/urbdrc_main.c | 1017 ++ channels/urbdrc/client/urbdrc_main.h | 247 + channels/urbdrc/common/CMakeLists.txt | 26 + channels/urbdrc/common/msusb.c | 402 + channels/urbdrc/common/msusb.h | 97 + channels/urbdrc/common/urbdrc_helpers.c | 421 + channels/urbdrc/common/urbdrc_helpers.h | 45 + channels/urbdrc/common/urbdrc_types.h | 308 + channels/video/CMakeLists.txt | 22 + channels/video/ChannelOptions.cmake | 12 + channels/video/client/CMakeLists.txt | 39 + channels/video/client/video_main.c | 1190 +++ channels/video/client/video_main.h | 33 + ci/cmake-preloads/config-android.txt | 7 + ci/cmake-preloads/config-debian-squeeze.txt | 11 + ci/cmake-preloads/config-ios.txt | 8 + ci/cmake-preloads/config-linux-all.txt | 53 + ci/cmake-preloads/config-macosx.txt | 8 + ci/cmake-preloads/config-ubuntu-1204.txt | 11 + ci/cmake-preloads/config-windows.txt | 6 + client/.gitignore | 12 + client/Android/BuildFlags.java.in | 6 + client/Android/CMakeLists.txt | 54 + client/Android/Studio/.gitignore | 37 + client/Android/Studio/aFreeRDP/build.gradle | 52 + client/Android/Studio/aFreeRDP/lint.xml | 2 + .../aFreeRDP/src/main/AndroidManifest.xml | 108 + .../aFreeRDP/src/main/assets/FreeRDP_Logo.png | Bin 0 -> 30329 bytes .../Studio/aFreeRDP/src/main/assets/about.css | 147 + .../src/main/assets/about_page/about.html | 397 + .../main/assets/about_page/about_phone.html | 397 + .../aFreeRDP/src/main/assets/background.jpg | Bin 0 -> 118109 bytes .../src/main/assets/de_about_page/about.html | 410 + .../assets/de_about_page/about_phone.html | 412 + .../main/assets/de_help_page/gestures.html | 33 + .../src/main/assets/de_help_page/gestures.png | Bin 0 -> 43781 bytes .../assets/de_help_page/gestures_phone.html | 38 + .../assets/de_help_page/gestures_phone.png | Bin 0 -> 27770 bytes .../main/assets/de_help_page/nav_gestures.png | Bin 0 -> 2394 bytes .../main/assets/de_help_page/nav_toolbar.png | Bin 0 -> 2022 bytes .../assets/de_help_page/nav_touch_pointer.png | Bin 0 -> 2301 bytes .../src/main/assets/de_help_page/toolbar.html | 49 + .../src/main/assets/de_help_page/toolbar.png | Bin 0 -> 7010 bytes .../assets/de_help_page/toolbar_phone.html | 49 + .../assets/de_help_page/toolbar_phone.png | Bin 0 -> 5555 bytes .../assets/de_help_page/touch_pointer.html | 29 + .../assets/de_help_page/touch_pointer.png | Bin 0 -> 110449 bytes .../de_help_page/touch_pointer_phone.html | 30 + .../de_help_page/touch_pointer_phone.png | Bin 0 -> 87793 bytes .../Studio/aFreeRDP/src/main/assets/help.css | 100 + .../src/main/assets/help_page/gestures.html | 32 + .../src/main/assets/help_page/gestures.png | Bin 0 -> 43781 bytes .../main/assets/help_page/gestures_phone.html | 33 + .../main/assets/help_page/gestures_phone.png | Bin 0 -> 27770 bytes .../main/assets/help_page/nav_gestures.png | Bin 0 -> 2394 bytes .../src/main/assets/help_page/nav_toolbar.png | Bin 0 -> 2022 bytes .../assets/help_page/nav_touch_pointer.png | Bin 0 -> 2301 bytes .../src/main/assets/help_page/toolbar.html | 49 + .../src/main/assets/help_page/toolbar.png | Bin 0 -> 7010 bytes .../main/assets/help_page/toolbar_phone.html | 50 + .../main/assets/help_page/toolbar_phone.png | Bin 0 -> 5555 bytes .../main/assets/help_page/touch_pointer.html | 29 + .../main/assets/help_page/touch_pointer.png | Bin 0 -> 110449 bytes .../assets/help_page/touch_pointer_phone.html | 30 + .../assets/help_page/touch_pointer_phone.png | Bin 0 -> 87793 bytes .../afreerdp/application/GlobalApp.java | 5 + .../drawable-hdpi/icon_launcher_freerdp.png | Bin 0 -> 2858 bytes .../drawable-ldpi/icon_launcher_freerdp.png | Bin 0 -> 1200 bytes .../drawable-mdpi/icon_launcher_freerdp.png | Bin 0 -> 1784 bytes .../main/res/drawable/button_background.xml | 31 + .../res/drawable/icon_launcher_freerdp.png | Bin 0 -> 4196 bytes .../res/drawable/separator_background.xml | 19 + .../src/main/res/values-de/strings.xml | 4 + .../src/main/res/values-es/strings.xml | 4 + .../src/main/res/values-fr/strings.xml | 4 + .../src/main/res/values-nl/strings.xml | 4 + .../src/main/res/values-zh/strings.xml | 4 + .../aFreeRDP/src/main/res/values/strings.xml | 7 + .../aFreeRDP/src/main/res/xml/searchable.xml | 22 + client/Android/Studio/build.gradle | 52 + .../Android/Studio/freeRDPCore/build.gradle | 29 + client/Android/Studio/freeRDPCore/lint.xml | 2 + .../freeRDPCore/src/main/AndroidManifest.xml | 111 + .../freerdpcore/application/GlobalApp.java | 211 + .../application/NetworkStateReceiver.java | 58 + .../application/ScreenReceiver.java | 30 + .../freerdpcore/application/SessionState.java | 129 + .../freerdpcore/domain/BookmarkBase.java | 1063 ++ .../domain/ConnectionReference.java | 85 + .../freerdpcore/domain/ManualBookmark.java | 255 + .../domain/PlaceholderBookmark.java | 84 + .../domain/QuickConnectBookmark.java | 70 + .../presentation/AboutActivity.java | 121 + .../ApplicationSettingsActivity.java | 300 + .../presentation/BookmarkActivity.java | 743 ++ .../presentation/HelpActivity.java | 77 + .../presentation/HomeActivity.java | 399 + .../presentation/ScrollView2D.java | 1349 +++ .../presentation/SessionActivity.java | 1433 +++ .../freerdpcore/presentation/SessionView.java | 411 + .../presentation/ShortcutsActivity.java | 160 + .../presentation/TouchPointerView.java | 385 + .../services/BookmarkBaseGateway.java | 617 ++ .../freerdpcore/services/BookmarkDB.java | 422 + .../services/FreeRDPSuggestionProvider.java | 134 + .../freerdpcore/services/HistoryDB.java | 46 + .../freerdpcore/services/LibFreeRDP.java | 656 ++ .../services/ManualBookmarkGateway.java | 131 + .../services/QuickConnectHistoryGateway.java | 121 + .../SessionRequestHandlerActivity.java | 77 + .../utils/AppCompatPreferenceActivity.java | 112 + .../utils/BookmarkArrayAdapter.java | 135 + .../freerdpcore/utils/ButtonPreference.java | 96 + .../utils/ClipboardManagerProxy.java | 100 + .../utils/DoubleGestureDetector.java | 349 + .../freerdpcore/utils/GestureDetector.java | 620 ++ .../utils/IntEditTextPreference.java | 101 + .../freerdpcore/utils/IntListPreference.java | 39 + .../freerdpcore/utils/KeyboardMapper.java | 725 ++ .../com/freerdp/freerdpcore/utils/Mouse.java | 64 + .../freerdpcore/utils/RDPFileParser.java | 111 + .../utils/SeparatedListAdapter.java | 208 + .../res/drawable-hdpi/icon_button_add.png | Bin 0 -> 1086 bytes .../res/drawable-hdpi/icon_edittext_clear.png | Bin 0 -> 374 bytes .../drawable-hdpi/icon_edittext_search.png | Bin 0 -> 1299 bytes .../drawable-hdpi/icon_launcher_freerdp.png | Bin 0 -> 2858 bytes .../res/drawable-hdpi/icon_menu_about.png | Bin 0 -> 2182 bytes .../main/res/drawable-hdpi/icon_menu_add.png | Bin 0 -> 2550 bytes .../res/drawable-hdpi/icon_menu_close.png | Bin 0 -> 3105 bytes .../drawable-hdpi/icon_menu_disconnect.png | Bin 0 -> 4424 bytes .../drawable-hdpi/icon_menu_ext_keyboard.png | Bin 0 -> 1215 bytes .../main/res/drawable-hdpi/icon_menu_help.png | Bin 0 -> 2590 bytes .../drawable-hdpi/icon_menu_preferences.png | Bin 0 -> 2668 bytes .../res/drawable-hdpi/icon_menu_settings.png | Bin 0 -> 2311 bytes .../drawable-hdpi/icon_menu_sys_keyboard.png | Bin 0 -> 1397 bytes .../drawable-hdpi/icon_menu_touch_pointer.png | Bin 0 -> 2083 bytes .../main/res/drawable-hdpi/icon_star_off.png | Bin 0 -> 1406 bytes .../main/res/drawable-hdpi/icon_star_on.png | Bin 0 -> 2265 bytes .../main/res/drawable-hdpi/search_plate.9.png | Bin 0 -> 238 bytes .../res/drawable-hdpi/sym_keyboard_delete.png | Bin 0 -> 1520 bytes .../sym_keyboard_feedback_delete.png | Bin 0 -> 1002 bytes .../sym_keyboard_feedback_return.png | Bin 0 -> 636 bytes .../res/drawable-hdpi/sym_keyboard_return.png | Bin 0 -> 720 bytes .../res/drawable-ldpi/icon_button_add.png | Bin 0 -> 466 bytes .../drawable-ldpi/icon_edittext_search.png | Bin 0 -> 783 bytes .../drawable-ldpi/icon_launcher_freerdp.png | Bin 0 -> 1200 bytes .../res/drawable-ldpi/icon_menu_about.png | Bin 0 -> 1182 bytes .../main/res/drawable-ldpi/icon_menu_add.png | Bin 0 -> 1299 bytes .../drawable-ldpi/icon_menu_disconnect.png | Bin 0 -> 1994 bytes .../main/res/drawable-ldpi/icon_menu_exit.png | Bin 0 -> 1498 bytes .../drawable-ldpi/icon_menu_ext_keyboard.png | Bin 0 -> 768 bytes .../main/res/drawable-ldpi/icon_menu_help.png | Bin 0 -> 1313 bytes .../drawable-ldpi/icon_menu_preferences.png | Bin 0 -> 1341 bytes .../res/drawable-ldpi/icon_menu_settings.png | Bin 0 -> 1088 bytes .../drawable-ldpi/icon_menu_sys_keyboard.png | Bin 0 -> 1049 bytes .../drawable-ldpi/icon_menu_touch_pointer.png | Bin 0 -> 1246 bytes .../main/res/drawable-ldpi/icon_star_off.png | Bin 0 -> 551 bytes .../main/res/drawable-ldpi/icon_star_on.png | Bin 0 -> 932 bytes .../main/res/drawable-ldpi/search_plate.9.png | Bin 0 -> 182 bytes .../res/drawable-ldpi/sym_keyboard_delete.png | Bin 0 -> 670 bytes .../sym_keyboard_feedback_delete.png | Bin 0 -> 445 bytes .../sym_keyboard_feedback_return.png | Bin 0 -> 267 bytes .../res/drawable-ldpi/sym_keyboard_return.png | Bin 0 -> 330 bytes .../res/drawable-mdpi/icon_button_add.png | Bin 0 -> 717 bytes .../res/drawable-mdpi/icon_edittext_clear.png | Bin 0 -> 374 bytes .../drawable-mdpi/icon_edittext_search.png | Bin 0 -> 926 bytes .../drawable-mdpi/icon_launcher_freerdp.png | Bin 0 -> 1784 bytes .../res/drawable-mdpi/icon_menu_about.png | Bin 0 -> 1473 bytes .../main/res/drawable-mdpi/icon_menu_add.png | Bin 0 -> 1687 bytes .../drawable-mdpi/icon_menu_disconnect.png | Bin 0 -> 2534 bytes .../main/res/drawable-mdpi/icon_menu_exit.png | Bin 0 -> 1949 bytes .../drawable-mdpi/icon_menu_ext_keyboard.png | Bin 0 -> 993 bytes .../main/res/drawable-mdpi/icon_menu_help.png | Bin 0 -> 1620 bytes .../drawable-mdpi/icon_menu_preferences.png | Bin 0 -> 1724 bytes .../res/drawable-mdpi/icon_menu_settings.png | Bin 0 -> 1438 bytes .../drawable-mdpi/icon_menu_sys_keyboard.png | Bin 0 -> 1154 bytes .../drawable-mdpi/icon_menu_touch_pointer.png | Bin 0 -> 1458 bytes .../main/res/drawable-mdpi/icon_star_off.png | Bin 0 -> 1158 bytes .../main/res/drawable-mdpi/icon_star_on.png | Bin 0 -> 1219 bytes .../main/res/drawable-mdpi/search_plate.9.png | Bin 0 -> 214 bytes .../res/drawable-mdpi/sym_keyboard_delete.png | Bin 0 -> 615 bytes .../sym_keyboard_feedback_delete.png | Bin 0 -> 267 bytes .../sym_keyboard_feedback_return.png | Bin 0 -> 230 bytes .../res/drawable-mdpi/sym_keyboard_return.png | Bin 0 -> 631 bytes .../main/res/drawable/button_background.xml | 31 + .../main/res/drawable/icon_button_cancel.png | Bin 0 -> 1994 bytes .../res/drawable/icon_launcher_freerdp.png | Bin 0 -> 4196 bytes .../res/drawable/separator_background.xml | 19 + .../main/res/drawable/sym_keyboard_arrows.png | Bin 0 -> 605 bytes .../drawable/sym_keyboard_arrows_black.png | Bin 0 -> 567 bytes .../res/drawable/sym_keyboard_down_arrow.png | Bin 0 -> 321 bytes .../sym_keyboard_down_arrow_black.png | Bin 0 -> 312 bytes .../res/drawable/sym_keyboard_left_arrow.png | Bin 0 -> 317 bytes .../sym_keyboard_left_arrow_black.png | Bin 0 -> 306 bytes .../main/res/drawable/sym_keyboard_menu.png | Bin 0 -> 951 bytes .../res/drawable/sym_keyboard_menu_black.png | Bin 0 -> 1039 bytes .../res/drawable/sym_keyboard_right_arrow.png | Bin 0 -> 327 bytes .../sym_keyboard_right_arrow_black.png | Bin 0 -> 302 bytes .../res/drawable/sym_keyboard_up_arrow.png | Bin 0 -> 331 bytes .../drawable/sym_keyboard_up_arrow_black.png | Bin 0 -> 322 bytes .../main/res/drawable/sym_keyboard_winkey.png | Bin 0 -> 701 bytes .../drawable/sym_keyboard_winkey_black.png | Bin 0 -> 668 bytes .../res/drawable/touch_pointer_active.png | Bin 0 -> 9981 bytes .../res/drawable/touch_pointer_default.png | Bin 0 -> 9305 bytes .../drawable/touch_pointer_extkeyboard.png | Bin 0 -> 9555 bytes .../res/drawable/touch_pointer_keyboard.png | Bin 0 -> 9596 bytes .../res/drawable/touch_pointer_lclick.png | Bin 0 -> 9609 bytes .../res/drawable/touch_pointer_rclick.png | Bin 0 -> 9464 bytes .../main/res/drawable/touch_pointer_reset.png | Bin 0 -> 9549 bytes .../res/drawable/touch_pointer_scroll.png | Bin 0 -> 11773 bytes .../src/main/res/layout/activity_about.xml | 12 + .../main/res/layout/bookmark_list_item.xml | 61 + .../src/main/res/layout/button_preference.xml | 62 + .../src/main/res/layout/credentials.xml | 58 + .../res/layout/dont_show_again_dialog.xml | 33 + .../freeRDPCore/src/main/res/layout/home.xml | 36 + .../src/main/res/layout/list_header.xml | 20 + .../src/main/res/layout/session.xml | 67 + .../src/main/res/layout/session_list_item.xml | 66 + .../src/main/res/layout/super_bar.xml | 48 + .../main/res/menu/bookmark_context_menu.xml | 19 + .../src/main/res/menu/home_menu.xml | 24 + .../src/main/res/menu/session_menu.xml | 31 + .../src/main/res/values-de/strings.xml | 175 + .../src/main/res/values-es/strings.xml | 176 + .../src/main/res/values-fr/strings.xml | 175 + .../src/main/res/values-ja/strings.xml | 177 + .../src/main/res/values-ko/strings.xml | 251 + .../src/main/res/values-land/dimens.xml | 4 + .../src/main/res/values-nb-rNO/strings.xml | 251 + .../src/main/res/values-nl/strings.xml | 176 + .../src/main/res/values-w820dp/dimens.xml | 6 + .../src/main/res/values-zh/strings.xml | 175 + .../freeRDPCore/src/main/res/values/attrs.xml | 14 + .../src/main/res/values/dimens.xml | 7 + .../src/main/res/values/integers.xml | 72 + .../src/main/res/values/strings.xml | 251 + .../freeRDPCore/src/main/res/values/theme.xml | 61 + .../src/main/res/xml/advanced_settings.xml | 81 + .../src/main/res/xml/bookmark_settings.xml | 96 + .../src/main/res/xml/credentials_settings.xml | 28 + .../src/main/res/xml/cursor_keyboard.xml | 68 + .../src/main/res/xml/debug_settings.xml | 38 + .../src/main/res/xml/gateway_settings.xml | 43 + .../src/main/res/xml/modifiers_keyboard.xml | 86 + .../src/main/res/xml/numpad_keyboard.xml | 119 + .../src/main/res/xml/performance_flags.xml | 42 + .../src/main/res/xml/performance_flags_3g.xml | 42 + .../src/main/res/xml/screen_settings.xml | 41 + .../src/main/res/xml/screen_settings_3g.xml | 41 + .../src/main/res/xml/settings_app_client.xml | 7 + .../src/main/res/xml/settings_app_headers.xml | 22 + .../src/main/res/xml/settings_app_power.xml | 13 + .../main/res/xml/settings_app_security.xml | 9 + .../src/main/res/xml/settings_app_ui.xml | 30 + .../src/main/res/xml/specialkeys_keyboard.xml | 123 + client/Android/Studio/gradle.properties | 3 + .../Studio/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 53637 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 + client/Android/Studio/gradlew | 160 + client/Android/Studio/gradlew.bat | 90 + client/Android/Studio/settings.gradle | 2 + client/Android/android_cliprdr.c | 518 + client/Android/android_cliprdr.h | 33 + client/Android/android_event.c | 385 + client/Android/android_event.h | 79 + client/Android/android_freerdp.c | 1132 ++ client/Android/android_freerdp.h | 44 + client/Android/android_freerdp_jni.h | 27 + client/Android/android_jni_callback.c | 227 + client/Android/android_jni_callback.h | 28 + client/Android/android_jni_utils.c | 192 + client/Android/android_jni_utils.h | 36 + client/CMakeLists.txt | 105 + client/FreeRDP-ClientConfig.cmake.in | 10 + client/Mac/.gitignore | 3 + client/Mac/CMakeLists.txt | 187 + client/Mac/CertificateDialog.h | 60 + client/Mac/CertificateDialog.m | 135 + client/Mac/CertificateDialog.xib | 169 + client/Mac/Clipboard.h | 31 + client/Mac/Clipboard.m | 433 + client/Mac/Credits.rtf | 21 + client/Mac/Info.plist | 30 + client/Mac/Keyboard.h | 27 + client/Mac/Keyboard.m | 241 + client/Mac/MRDPCursor.h | 34 + client/Mac/MRDPCursor.m | 24 + client/Mac/MRDPView.h | 91 + client/Mac/MRDPView.m | 1423 +++ client/Mac/ModuleOptions.cmake | 4 + client/Mac/PasswordDialog.h | 49 + client/Mac/PasswordDialog.m | 134 + client/Mac/PasswordDialog.xib | 132 + client/Mac/cli/AppDelegate.h | 26 + client/Mac/cli/AppDelegate.m | 307 + client/Mac/cli/CMakeLists.txt | 114 + client/Mac/cli/FreeRDP.icns | Bin 0 -> 60681 bytes client/Mac/cli/Info.plist | 38 + client/Mac/cli/MacClient2-Info.plist | 32 + client/Mac/cli/MacClient2-Prefix.pch | 7 + client/Mac/cli/MainMenu.xib | 120 + client/Mac/cli/en.lproj/Credits.rtf | 29 + client/Mac/cli/en.lproj/InfoPlist.strings | 2 + client/Mac/cli/en.lproj/MainMenu.xib | 3299 ++++++ client/Mac/cli/main.m | 14 + client/Mac/en.lproj/InfoPlist.strings | 2 + client/Mac/main.m | 25 + client/Mac/mf_client.h | 49 + client/Mac/mf_client.m | 216 + client/Mac/mfreerdp.h | 91 + client/Sample/CMakeLists.txt | 51 + client/Sample/ModuleOptions.cmake | 4 + client/Sample/tf_channels.c | 115 + client/Sample/tf_channels.h | 33 + client/Sample/tf_freerdp.c | 359 + client/Sample/tf_freerdp.h | 42 + client/Wayland/CMakeLists.txt | 50 + client/Wayland/wlf_channels.c | 156 + client/Wayland/wlf_channels.h | 37 + client/Wayland/wlf_cliprdr.c | 921 ++ client/Wayland/wlf_cliprdr.h | 36 + client/Wayland/wlf_disp.c | 401 + client/Wayland/wlf_disp.h | 38 + client/Wayland/wlf_input.c | 401 + client/Wayland/wlf_input.h | 41 + client/Wayland/wlf_pointer.c | 170 + client/Wayland/wlf_pointer.h | 28 + client/Wayland/wlfreerdp.1.in | 38 + client/Wayland/wlfreerdp.c | 752 ++ client/Wayland/wlfreerdp.h | 63 + client/Windows/CMakeLists.txt | 98 + client/Windows/FreeRDP.ico | Bin 0 -> 30160 bytes client/Windows/ModuleOptions.cmake | 4 + client/Windows/cli/CMakeLists.txt | 54 + client/Windows/cli/wfreerdp.c | 144 + client/Windows/cli/wfreerdp.h | 27 + client/Windows/resource.h | 12 + client/Windows/resource/close.bmp | Bin 0 -> 1986 bytes client/Windows/resource/close_active.bmp | Bin 0 -> 1986 bytes client/Windows/resource/lock.bmp | Bin 0 -> 1986 bytes client/Windows/resource/lock_active.bmp | Bin 0 -> 1986 bytes client/Windows/resource/minimize.bmp | Bin 0 -> 1986 bytes client/Windows/resource/minimize_active.bmp | Bin 0 -> 1986 bytes client/Windows/resource/restore.bmp | Bin 0 -> 1986 bytes client/Windows/resource/restore_active.bmp | Bin 0 -> 1986 bytes client/Windows/resource/unlock.bmp | Bin 0 -> 1986 bytes client/Windows/resource/unlock_active.bmp | Bin 0 -> 1986 bytes client/Windows/wf_channels.c | 85 + client/Windows/wf_channels.h | 34 + client/Windows/wf_client.c | 1164 ++ client/Windows/wf_client.h | 151 + client/Windows/wf_cliprdr.c | 2552 +++++ client/Windows/wf_cliprdr.h | 27 + client/Windows/wf_event.c | 778 ++ client/Windows/wf_event.h | 43 + client/Windows/wf_floatbar.c | 745 ++ client/Windows/wf_floatbar.h | 33 + client/Windows/wf_gdi.c | 827 ++ client/Windows/wf_gdi.h | 38 + client/Windows/wf_graphics.c | 370 + client/Windows/wf_graphics.h | 34 + client/Windows/wf_rail.c | 1073 ++ client/Windows/wf_rail.h | 33 + client/Windows/wfreerdp.rc | Bin 0 -> 1670 bytes client/X11/.gitignore | 2 + client/X11/CMakeLists.txt | 249 + client/X11/ModuleOptions.cmake | 4 + client/X11/cli/.gitignore | 2 + client/X11/cli/CMakeLists.txt | 38 + client/X11/cli/xfreerdp.c | 85 + client/X11/generate_argument_docbook.c | 271 + client/X11/resource/close.xbm | 11 + client/X11/resource/lock.xbm | 11 + client/X11/resource/minimize.xbm | 11 + client/X11/resource/restore.xbm | 11 + client/X11/resource/unlock.xbm | 11 + client/X11/xf_channels.c | 137 + client/X11/xf_channels.h | 37 + client/X11/xf_client.c | 2096 ++++ client/X11/xf_client.h | 53 + client/X11/xf_cliprdr.c | 1978 ++++ client/X11/xf_cliprdr.h | 36 + client/X11/xf_disp.c | 480 + client/X11/xf_disp.h | 37 + client/X11/xf_event.c | 1152 ++ client/X11/xf_event.h | 43 + client/X11/xf_floatbar.c | 813 ++ client/X11/xf_floatbar.h | 34 + client/X11/xf_gdi.c | 1114 ++ client/X11/xf_gdi.h | 32 + client/X11/xf_gfx.c | 418 + client/X11/xf_gfx.h | 45 + client/X11/xf_graphics.c | 790 ++ client/X11/xf_graphics.h | 34 + client/X11/xf_input.c | 669 ++ client/X11/xf_input.h | 33 + client/X11/xf_keyboard.c | 655 ++ client/X11/xf_keyboard.h | 63 + client/X11/xf_monitor.c | 573 + client/X11/xf_monitor.h | 50 + client/X11/xf_rail.c | 1200 +++ client/X11/xf_rail.h | 49 + client/X11/xf_tsmf.c | 475 + client/X11/xf_tsmf.h | 29 + client/X11/xf_video.c | 107 + client/X11/xf_video.h | 33 + client/X11/xf_window.c | 1143 ++ client/X11/xf_window.h | 192 + client/X11/xfreerdp-channels.1.xml | 0 client/X11/xfreerdp-envvar.1.xml | 15 + client/X11/xfreerdp-examples.1.xml | 95 + client/X11/xfreerdp.1.xml.in | 63 + client/X11/xfreerdp.h | 365 + client/common/CMakeLists.txt | 94 + client/common/client.c | 805 ++ client/common/cmdline.c | 3907 +++++++ client/common/cmdline.h | 395 + client/common/compatibility.c | 896 ++ client/common/compatibility.h | 34 + client/common/file.c | 2063 ++++ client/common/geometry.c | 42 + client/common/test/.gitignore | 3 + client/common/test/CMakeLists.txt | 30 + client/common/test/TestClientChannels.c | 88 + client/common/test/TestClientCmdLine.c | 269 + client/common/test/TestClientRdpFile.c | 435 + client/freerdp-client.pc.in | 15 + client/iOS/.gitignore | 10 + client/iOS/Additions/OrderedDictionary.h | 40 + client/iOS/Additions/OrderedDictionary.m | 167 + client/iOS/Additions/TSXAdditions.h | 34 + client/iOS/Additions/TSXAdditions.m | 238 + client/iOS/Additions/Toast+UIView.h | 60 + client/iOS/Additions/Toast+UIView.m | 367 + client/iOS/AppDelegate.h | 24 + client/iOS/AppDelegate.m | 127 + client/iOS/CMakeLists.txt | 132 + client/iOS/Controllers/AboutController.h | 19 + client/iOS/Controllers/AboutController.m | 120 + .../AdvancedBookmarkEditorController.h | 26 + .../AdvancedBookmarkEditorController.m | 407 + .../iOS/Controllers/AppSettingsController.h | 15 + .../iOS/Controllers/AppSettingsController.m | 360 + .../Controllers/BookmarkEditorController.h | 38 + .../Controllers/BookmarkEditorController.m | 427 + .../BookmarkGatewaySettingsController.h | 26 + .../BookmarkGatewaySettingsController.m | 242 + .../iOS/Controllers/BookmarkListController.h | 56 + .../iOS/Controllers/BookmarkListController.m | 957 ++ .../Controllers/CredentialsEditorController.h | 26 + .../Controllers/CredentialsEditorController.m | 215 + .../Controllers/CredentialsInputController.h | 35 + .../Controllers/CredentialsInputController.m | 160 + client/iOS/Controllers/EditorBaseController.h | 44 + client/iOS/Controllers/EditorBaseController.m | 110 + .../Controllers/EditorSelectionController.h | 34 + .../Controllers/EditorSelectionController.m | 143 + client/iOS/Controllers/EncryptionController.h | 25 + client/iOS/Controllers/EncryptionController.m | 155 + client/iOS/Controllers/HelpController.h | 17 + client/iOS/Controllers/HelpController.m | 76 + client/iOS/Controllers/MainTabBarController.h | 18 + client/iOS/Controllers/MainTabBarController.m | 20 + .../Controllers/PerformanceEditorController.h | 25 + .../Controllers/PerformanceEditorController.m | 244 + .../Controllers/RDPSessionViewController.h | 75 + .../Controllers/RDPSessionViewController.m | 1113 ++ .../Controllers/ScreenSelectionController.h | 34 + .../Controllers/ScreenSelectionController.m | 253 + .../Controllers/VerifyCertificateController.h | 33 + .../Controllers/VerifyCertificateController.m | 86 + client/iOS/Defaults.plist | 92 + client/iOS/FreeRDP/ios_freerdp.h | 69 + client/iOS/FreeRDP/ios_freerdp.m | 462 + client/iOS/FreeRDP/ios_freerdp_events.h | 27 + client/iOS/FreeRDP/ios_freerdp_events.m | 153 + client/iOS/FreeRDP/ios_freerdp_ui.h | 26 + client/iOS/FreeRDP/ios_freerdp_ui.m | 207 + client/iOS/Misc/Reachability.h | 87 + client/iOS/Misc/Reachability.m | 277 + client/iOS/Misc/SFHFKeychainUtils.h | 48 + client/iOS/Misc/SFHFKeychainUtils.m | 501 + client/iOS/Misc/TSXTypes.h | 57 + client/iOS/Misc/Utils.h | 80 + client/iOS/Misc/Utils.m | 397 + client/iOS/Models/Bookmark.h | 48 + client/iOS/Models/Bookmark.m | 312 + client/iOS/Models/ConnectionParams.h | 46 + client/iOS/Models/ConnectionParams.m | 258 + client/iOS/Models/Encryptor.h | 43 + client/iOS/Models/Encryptor.m | 206 + client/iOS/Models/GlobalDefaults.h | 30 + client/iOS/Models/GlobalDefaults.m | 88 + client/iOS/Models/RDPKeyboard.h | 76 + client/iOS/Models/RDPKeyboard.m | 310 + client/iOS/Models/RDPSession.h | 109 + client/iOS/Models/RDPSession.m | 531 + client/iOS/ModuleOptions.cmake | 4 + client/iOS/Resources/BookmarkListView.xib | 365 + .../iOS/Resources/BookmarkTableViewCell.xib | 428 + client/iOS/Resources/CredentialsInputView.xib | 481 + client/iOS/Resources/Default-568h@2x.png | Bin 0 -> 8805 bytes .../Resources/Default-Landscape@2x~ipad.png | Bin 0 -> 16161 bytes .../iOS/Resources/Default-Landscape~ipad.png | Bin 0 -> 6776 bytes .../Resources/Default-Portrait@2x~ipad.png | Bin 0 -> 18847 bytes .../iOS/Resources/Default-Portrait~ipad.png | Bin 0 -> 9104 bytes client/iOS/Resources/Default.png | Bin 0 -> 4050 bytes client/iOS/Resources/Default@2x.png | Bin 0 -> 8781 bytes .../iOS/Resources/EditButtonTableViewCell.xib | 408 + .../iOS/Resources/EditFlagTableViewCell.xib | 208 + .../Resources/EditSecretTextTableViewCell.xib | 288 + .../Resources/EditSelectionTableViewCell.xib | 347 + .../Resources/EditSubEditTableViewCell.xib | 179 + .../iOS/Resources/EditTextTableViewCell.xib | 364 + client/iOS/Resources/Icon-72.png | Bin 0 -> 1074 bytes client/iOS/Resources/Icon-72@2x.png | Bin 0 -> 1837 bytes client/iOS/Resources/Icon.png | Bin 0 -> 912 bytes client/iOS/Resources/Icon@2x.png | Bin 0 -> 1502 bytes client/iOS/Resources/MainWindow.xib | 36 + client/iOS/Resources/RDPConnectingView.xib | 342 + client/iOS/Resources/RDPSessionView.xib | 553 + client/iOS/Resources/SessionTableViewCell.xib | 574 + .../iOS/Resources/VerifyCertificateView.xib | 386 + .../iOS/Resources/about_page/FreeRDP_Logo.png | Bin 0 -> 30329 bytes client/iOS/Resources/about_page/about.html | 203 + .../iOS/Resources/about_page/about_phone.html | 201 + client/iOS/Resources/about_page/back.jpg | Bin 0 -> 118109 bytes .../about_page/background_transparent.png | Bin 0 -> 74 bytes client/iOS/Resources/alert-black-button.png | Bin 0 -> 608 bytes .../iOS/Resources/alert-black-button@2x.png | Bin 0 -> 1142 bytes client/iOS/Resources/alert-gray-button.png | Bin 0 -> 634 bytes client/iOS/Resources/alert-gray-button@2x.png | Bin 0 -> 1353 bytes client/iOS/Resources/alert-red-button.png | Bin 0 -> 477 bytes client/iOS/Resources/alert-red-button@2x.png | Bin 0 -> 919 bytes .../iOS/Resources/alert-window-landscape.png | Bin 0 -> 2287 bytes .../Resources/alert-window-landscape@2x.png | Bin 0 -> 4303 bytes client/iOS/Resources/alert-window.png | Bin 0 -> 1870 bytes client/iOS/Resources/alert-window@2x.png | Bin 0 -> 4153 bytes .../Resources/cancel_button_background.png | Bin 0 -> 317 bytes .../Resources/en.lproj/Localizable.strings | 274 + client/iOS/Resources/help_page/back.jpg | Bin 0 -> 118109 bytes client/iOS/Resources/help_page/gestures.html | 159 + client/iOS/Resources/help_page/gestures.png | Bin 0 -> 43781 bytes .../Resources/help_page/gestures_phone.html | 159 + .../Resources/help_page/gestures_phone.png | Bin 0 -> 27770 bytes .../iOS/Resources/help_page/nav_gestures.png | Bin 0 -> 2394 bytes .../iOS/Resources/help_page/nav_toolbar.png | Bin 0 -> 2022 bytes .../Resources/help_page/nav_touch_pointer.png | Bin 0 -> 2301 bytes client/iOS/Resources/help_page/toolbar.html | 178 + client/iOS/Resources/help_page/toolbar.png | Bin 0 -> 7010 bytes .../Resources/help_page/toolbar_phone.html | 176 + .../iOS/Resources/help_page/toolbar_phone.png | Bin 0 -> 5555 bytes .../Resources/help_page/touch_pointer.html | 164 + .../iOS/Resources/help_page/touch_pointer.png | Bin 0 -> 110449 bytes .../help_page/touch_pointer_phone.html | 161 + .../help_page/touch_pointer_phone.png | Bin 0 -> 87793 bytes .../iOS/Resources/icon_accessory_star_off.png | Bin 0 -> 551 bytes .../iOS/Resources/icon_accessory_star_on.png | Bin 0 -> 932 bytes client/iOS/Resources/icon_key_arrow_down.png | Bin 0 -> 122 bytes client/iOS/Resources/icon_key_arrow_left.png | Bin 0 -> 126 bytes client/iOS/Resources/icon_key_arrow_right.png | Bin 0 -> 128 bytes client/iOS/Resources/icon_key_arrow_up.png | Bin 0 -> 124 bytes client/iOS/Resources/icon_key_arrows.png | Bin 0 -> 185 bytes client/iOS/Resources/icon_key_backspace.png | Bin 0 -> 441 bytes client/iOS/Resources/icon_key_menu.png | Bin 0 -> 453 bytes client/iOS/Resources/icon_key_return.png | Bin 0 -> 137 bytes client/iOS/Resources/icon_key_win.png | Bin 0 -> 401 bytes .../Resources/keyboard_button_background.png | Bin 0 -> 242 bytes client/iOS/Resources/tabbar_icon_about.png | Bin 0 -> 237 bytes client/iOS/Resources/tabbar_icon_help.png | Bin 0 -> 279 bytes client/iOS/Resources/tabbar_icon_settings.png | Bin 0 -> 328 bytes .../iOS/Resources/toolbar_icon_disconnect.png | Bin 0 -> 609 bytes .../iOS/Resources/toolbar_icon_extkeyboad.png | Bin 0 -> 349 bytes client/iOS/Resources/toolbar_icon_home.png | Bin 0 -> 368 bytes .../iOS/Resources/toolbar_icon_keyboard.png | Bin 0 -> 550 bytes .../Resources/toolbar_icon_touchpointer.png | Bin 0 -> 415 bytes client/iOS/Resources/toolbar_icon_win.png | Bin 0 -> 408 bytes client/iOS/Resources/touch_pointer_active.png | Bin 0 -> 9965 bytes .../iOS/Resources/touch_pointer_default.png | Bin 0 -> 9309 bytes .../Resources/touch_pointer_extkeyboard.png | Bin 0 -> 9536 bytes .../iOS/Resources/touch_pointer_keyboard.png | Bin 0 -> 9580 bytes client/iOS/Resources/touch_pointer_lclick.png | Bin 0 -> 9670 bytes client/iOS/Resources/touch_pointer_rclick.png | Bin 0 -> 9430 bytes client/iOS/Resources/touch_pointer_reset.png | Bin 0 -> 9522 bytes client/iOS/Resources/touch_pointer_scroll.png | Bin 0 -> 11762 bytes client/iOS/Views/AdvancedKeyboardView.h | 48 + client/iOS/Views/AdvancedKeyboardView.m | 345 + client/iOS/Views/BlockAlertView.h | 42 + client/iOS/Views/BlockAlertView.m | 475 + client/iOS/Views/BlockBackground.h | 26 + client/iOS/Views/BlockBackground.m | 238 + client/iOS/Views/BlockUI.h | 74 + client/iOS/Views/BookmarkTableCell.h | 24 + client/iOS/Views/BookmarkTableCell.m | 39 + client/iOS/Views/EditButtonTableViewCell.h | 22 + client/iOS/Views/EditButtonTableViewCell.m | 34 + client/iOS/Views/EditFlagTableViewCell.h | 22 + client/iOS/Views/EditFlagTableViewCell.m | 34 + .../iOS/Views/EditSecretTextTableViewCell.h | 25 + .../iOS/Views/EditSecretTextTableViewCell.m | 61 + client/iOS/Views/EditSelectionTableViewCell.h | 22 + client/iOS/Views/EditSelectionTableViewCell.m | 34 + client/iOS/Views/EditSubEditTableViewCell.h | 20 + client/iOS/Views/EditSubEditTableViewCell.m | 34 + client/iOS/Views/EditTextTableViewCell.h | 22 + client/iOS/Views/EditTextTableViewCell.m | 34 + client/iOS/Views/RDPSessionView.h | 21 + client/iOS/Views/RDPSessionView.m | 58 + client/iOS/Views/SessionTableCell.h | 28 + client/iOS/Views/SessionTableCell.m | 42 + client/iOS/Views/TouchPointerView.h | 75 + client/iOS/Views/TouchPointerView.m | 359 + client/iOS/iFreeRDP-Prefix.pch | 23 + client/iOS/iFreeRDP.plist | 53 + client/iOS/main.m | 19 + cmake/CheckCmakeCompat.cmake | 42 + cmake/ClangFormat.cmake | 48 + cmake/ClangToolchain.cmake | 16 + cmake/ComplexLibrary.cmake | 94 + cmake/ConfigOptions.cmake | 183 + cmake/ConfigOptionsAndroid.cmake | 22 + cmake/ConfigOptionsiOS.cmake | 23 + cmake/EchoTarget.cmake | 178 + cmake/FindCairo.cmake | 166 + cmake/FindDBus.cmake | 73 + cmake/FindDbusGlib.cmake | 42 + cmake/FindDevD.cmake | 31 + cmake/FindDocBookXSL.cmake | 52 + cmake/FindFAAC.cmake | 13 + cmake/FindFAAD2.cmake | 13 + cmake/FindFFmpeg.cmake | 78 + cmake/FindFeature.cmake | 59 + cmake/FindGSM.cmake | 13 + cmake/FindGSSAPI.cmake | 453 + cmake/FindGStreamer_0_10.cmake | 118 + cmake/FindGStreamer_1_0.cmake | 153 + cmake/FindGlib.cmake | 42 + cmake/FindIPP.cmake | 397 + cmake/FindImageMagick.cmake | 237 + cmake/FindLAME.cmake | 13 + cmake/FindMbedTLS.cmake | 38 + cmake/FindOSS.cmake | 44 + cmake/FindOpenH264.cmake | 46 + cmake/FindOpenSLES.cmake | 36 + cmake/FindOpenSSL.cmake | 357 + cmake/FindPAM.cmake | 40 + cmake/FindPCSC.cmake | 28 + cmake/FindPCSCWinPR.cmake | 15 + cmake/FindPixman.cmake | 40 + cmake/FindPulse.cmake | 32 + cmake/FindSWScale.cmake | 14 + cmake/FindUDev.cmake | 53 + cmake/FindUUID.cmake | 116 + cmake/FindWayland.cmake | 69 + cmake/FindX11.cmake | 60 + cmake/FindXKBFile.cmake | 51 + cmake/FindXRandR.cmake | 49 + cmake/FindXShm.cmake | 51 + cmake/FindXTest.cmake | 51 + cmake/FindXcursor.cmake | 51 + cmake/FindXdamage.cmake | 51 + cmake/FindXext.cmake | 51 + cmake/FindXfixes.cmake | 51 + cmake/FindXi.cmake | 59 + cmake/FindXinerama.cmake | 51 + cmake/FindXmlto.cmake | 35 + cmake/FindXrender.cmake | 47 + cmake/FindXv.cmake | 50 + cmake/Findlibsystemd.cmake | 44 + cmake/Findlibusb-1.0.cmake | 98 + cmake/Findsoxr.cmake | 62 + cmake/GNUInstallDirsWrapper.cmake | 21 + cmake/GetGitRevisionDescription.cmake | 135 + cmake/GetGitRevisionDescription.cmake.in | 38 + cmake/InstallFreeRDPMan.cmake | 9 + cmake/LibFindMacros.cmake | 116 + cmake/MSVCRuntime.cmake | 60 + cmake/MergeStaticLibs.cmake | 151 + cmake/SetFreeRDPCMakeInstallDir.cmake | 7 + cmake/WindowsDLLVersion.rc.in | 35 + ...asicConfigVersion-AnyNewerVersion.cmake.in | 31 + .../BasicConfigVersion-ExactVersion.cmake.in | 47 + ...sicConfigVersion-SameMajorVersion.cmake.in | 46 + .../CMakePackageConfigHelpers.cmake | 248 + .../WriteBasicConfigVersionFile.cmake | 50 + cmake/compat_2.8.2/FindPkgConfig.cmake | 373 + cmake/compat_2.8.3/CMakeParseArguments.cmake | 138 + .../FindPackageHandleStandardArgs.cmake | 296 + cmake/compat_2.8.6/FeatureSummary.cmake | 466 + cmake/compat_3.7.0/FindICU.cmake | 349 + cmake/ios.toolchain.cmake | 945 ++ cmake/today.cmake | 19 + config.h.in | 186 + docs/Doxyfile | 1515 +++ docs/FreeRDP.vsd | Bin 0 -> 214528 bytes docs/PrintFormatSpecifiers.md | 131 + docs/README.android | 86 + docs/README.ios | 100 + docs/README.macOS | 7 + docs/README.timezones | 12 + docs/valgrind.supp | 132 + docs/version_detection.md | 35 + docs/wlog.md | 151 + external/README | 5 + include/CMakeLists.txt | 37 + include/freerdp/addin.h | 80 + include/freerdp/altsec.h | 230 + include/freerdp/api.h | 97 + include/freerdp/assistance.h | 66 + include/freerdp/autodetect.h | 60 + include/freerdp/build-config.h.in | 22 + include/freerdp/cache/bitmap.h | 75 + include/freerdp/cache/brush.h | 51 + include/freerdp/cache/cache.h | 60 + include/freerdp/cache/glyph.h | 79 + include/freerdp/cache/nine_grid.h | 48 + include/freerdp/cache/offscreen.h | 48 + include/freerdp/cache/palette.h | 64 + include/freerdp/cache/pointer.h | 60 + include/freerdp/channels/ainput.h | 61 + include/freerdp/channels/audin.h | 29 + include/freerdp/channels/channels.h | 65 + include/freerdp/channels/cliprdr.h | 233 + include/freerdp/channels/disp.h | 82 + include/freerdp/channels/echo.h | 30 + include/freerdp/channels/encomsp.h | 186 + include/freerdp/channels/geometry.h | 63 + include/freerdp/channels/log.h | 28 + include/freerdp/channels/rail.h | 26 + include/freerdp/channels/rdpdr.h | 385 + include/freerdp/channels/rdpecam.h | 336 + include/freerdp/channels/rdpei.h | 146 + include/freerdp/channels/rdpgfx.h | 435 + include/freerdp/channels/rdpsnd.h | 28 + include/freerdp/channels/remdesk.h | 160 + include/freerdp/channels/telemetry.h | 38 + include/freerdp/channels/tsmf.h | 35 + include/freerdp/channels/urbdrc.h | 30 + include/freerdp/channels/video.h | 118 + include/freerdp/channels/wtsvc.h | 94 + include/freerdp/client.h | 140 + include/freerdp/client/ainput.h | 38 + include/freerdp/client/audin.h | 62 + include/freerdp/client/channels.h | 47 + include/freerdp/client/cliprdr.h | 203 + include/freerdp/client/cmdline.h | 64 + include/freerdp/client/disp.h | 43 + include/freerdp/client/drdynvc.h | 53 + include/freerdp/client/encomsp.h | 76 + include/freerdp/client/file.h | 75 + include/freerdp/client/geometry.h | 74 + include/freerdp/client/printer.h | 80 + include/freerdp/client/rail.h | 130 + include/freerdp/client/rdpei.h | 78 + include/freerdp/client/rdpgfx.h | 156 + include/freerdp/client/rdpsnd.h | 78 + include/freerdp/client/remdesk.h | 37 + include/freerdp/client/sshagent.h | 86 + include/freerdp/client/tsmf.h | 71 + include/freerdp/client/video.h | 58 + include/freerdp/codec/audio.h | 220 + include/freerdp/codec/bitmap.h | 44 + include/freerdp/codec/bulk.h | 39 + include/freerdp/codec/clear.h | 54 + include/freerdp/codec/color.h | 1052 ++ include/freerdp/codec/dsp.h | 49 + include/freerdp/codec/h264.h | 117 + include/freerdp/codec/interleaved.h | 68 + include/freerdp/codec/jpeg.h | 38 + include/freerdp/codec/mppc.h | 53 + include/freerdp/codec/ncrush.h | 51 + include/freerdp/codec/nsc.h | 75 + include/freerdp/codec/planar.h | 133 + include/freerdp/codec/progressive.h | 79 + include/freerdp/codec/region.h | 153 + include/freerdp/codec/rfx.h | 215 + include/freerdp/codec/xcrush.h | 49 + include/freerdp/codec/yuv.h | 48 + include/freerdp/codec/zgfx.h | 59 + include/freerdp/codecs.h | 76 + include/freerdp/constants.h | 69 + include/freerdp/crypto/ber.h | 92 + include/freerdp/crypto/certificate.h | 78 + include/freerdp/crypto/crypto.h | 117 + include/freerdp/crypto/der.h | 44 + include/freerdp/crypto/er.h | 97 + include/freerdp/crypto/per.h | 59 + include/freerdp/crypto/tls.h | 107 + include/freerdp/display.h | 37 + include/freerdp/dvc.h | 158 + include/freerdp/error.h | 383 + include/freerdp/event.h | 132 + include/freerdp/extension.h | 60 + include/freerdp/freerdp.h | 534 + include/freerdp/gdi/bitmap.h | 54 + include/freerdp/gdi/dc.h | 44 + include/freerdp/gdi/gdi.h | 559 + include/freerdp/gdi/gfx.h | 75 + include/freerdp/gdi/pen.h | 39 + include/freerdp/gdi/region.h | 64 + include/freerdp/gdi/shape.h | 44 + include/freerdp/gdi/video.h | 42 + include/freerdp/graphics.h | 172 + include/freerdp/heartbeat.h | 46 + include/freerdp/input.h | 120 + include/freerdp/license.h | 163 + include/freerdp/listener.h | 74 + include/freerdp/locale/keyboard.h | 241 + include/freerdp/locale/locale.h | 250 + include/freerdp/log.h | 29 + include/freerdp/message.h | 376 + include/freerdp/metrics.h | 49 + include/freerdp/peer.h | 156 + include/freerdp/pointer.h | 113 + include/freerdp/primary.h | 527 + include/freerdp/primitives.h | 218 + include/freerdp/rail.h | 615 ++ include/freerdp/scancode.h | 237 + include/freerdp/secondary.h | 200 + include/freerdp/server/ainput.h | 122 + include/freerdp/server/audin.h | 123 + include/freerdp/server/channels.h | 25 + include/freerdp/server/cliprdr.h | 144 + include/freerdp/server/disp.h | 77 + include/freerdp/server/drdynvc.h | 61 + include/freerdp/server/echo.h | 100 + include/freerdp/server/encomsp.h | 102 + include/freerdp/server/rail.h | 150 + include/freerdp/server/rdpdr.h | 180 + include/freerdp/server/rdpecam-enumerator.h | 134 + include/freerdp/server/rdpecam.h | 278 + include/freerdp/server/rdpei.h | 83 + include/freerdp/server/rdpgfx.h | 138 + include/freerdp/server/rdpsnd.h | 189 + include/freerdp/server/remdesk.h | 65 + include/freerdp/server/server-common.h | 31 + include/freerdp/server/shadow.h | 331 + include/freerdp/server/telemetry.h | 108 + include/freerdp/session.h | 50 + include/freerdp/settings.h | 1719 +++ include/freerdp/svc.h | 76 + include/freerdp/types.h | 82 + include/freerdp/update.h | 269 + include/freerdp/utils/cliprdr_utils.h | 48 + include/freerdp/utils/passphrase.h | 38 + include/freerdp/utils/pcap.h | 94 + include/freerdp/utils/profiler.h | 99 + include/freerdp/utils/ringbuffer.h | 126 + include/freerdp/utils/signal.h | 47 + include/freerdp/utils/stopwatch.h | 55 + include/freerdp/version.h.in | 32 + include/freerdp/window.h | 293 + libfreerdp/CMakeLists.txt | 429 + libfreerdp/FreeRDPConfig.cmake.in | 10 + libfreerdp/cache/CMakeLists.txt | 36 + libfreerdp/cache/bitmap.c | 504 + libfreerdp/cache/bitmap.h | 41 + libfreerdp/cache/brush.c | 280 + libfreerdp/cache/brush.h | 31 + libfreerdp/cache/cache.c | 144 + libfreerdp/cache/cache.h | 36 + libfreerdp/cache/glyph.c | 856 ++ libfreerdp/cache/glyph.h | 35 + libfreerdp/cache/nine_grid.c | 162 + libfreerdp/cache/offscreen.c | 227 + libfreerdp/cache/palette.c | 140 + libfreerdp/cache/palette.h | 30 + libfreerdp/cache/pointer.c | 525 + libfreerdp/cache/pointer.h | 52 + libfreerdp/codec/audio.c | 301 + libfreerdp/codec/bitmap.c | 1091 ++ libfreerdp/codec/clear.c | 1217 +++ libfreerdp/codec/color.c | 867 ++ libfreerdp/codec/dsp.c | 1349 +++ libfreerdp/codec/dsp.h | 34 + libfreerdp/codec/dsp_ffmpeg.c | 679 ++ libfreerdp/codec/dsp_ffmpeg.h | 45 + libfreerdp/codec/h264.c | 606 ++ libfreerdp/codec/h264.h | 30 + libfreerdp/codec/h264_ffmpeg.c | 610 ++ libfreerdp/codec/h264_mediacodec.c | 647 ++ libfreerdp/codec/h264_mf.c | 649 ++ libfreerdp/codec/h264_openh264.c | 594 ++ libfreerdp/codec/include/bitmap.c | 428 + libfreerdp/codec/interleaved.c | 514 + libfreerdp/codec/jpeg.c | 154 + libfreerdp/codec/mppc.c | 827 ++ libfreerdp/codec/ncrush.c | 2895 +++++ libfreerdp/codec/nsc.c | 491 + libfreerdp/codec/nsc_encode.c | 538 + libfreerdp/codec/nsc_encode.h | 29 + libfreerdp/codec/nsc_sse2.c | 390 + libfreerdp/codec/nsc_sse2.h | 34 + libfreerdp/codec/nsc_types.h | 78 + libfreerdp/codec/planar.c | 1562 +++ libfreerdp/codec/progressive.c | 2765 +++++ libfreerdp/codec/progressive.h | 239 + libfreerdp/codec/region.c | 812 ++ libfreerdp/codec/rfx.c | 1905 ++++ libfreerdp/codec/rfx_bitstream.h | 108 + libfreerdp/codec/rfx_constants.h | 59 + libfreerdp/codec/rfx_decode.c | 95 + libfreerdp/codec/rfx_decode.h | 31 + libfreerdp/codec/rfx_differential.h | 51 + libfreerdp/codec/rfx_dwt.c | 206 + libfreerdp/codec/rfx_dwt.h | 29 + libfreerdp/codec/rfx_encode.c | 301 + libfreerdp/codec/rfx_encode.h | 28 + libfreerdp/codec/rfx_neon.c | 244 + libfreerdp/codec/rfx_neon.h | 34 + libfreerdp/codec/rfx_quantization.c | 102 + libfreerdp/codec/rfx_quantization.h | 29 + libfreerdp/codec/rfx_rlgr.c | 759 ++ libfreerdp/codec/rfx_rlgr.h | 32 + libfreerdp/codec/rfx_sse2.c | 471 + libfreerdp/codec/rfx_sse2.h | 34 + libfreerdp/codec/rfx_types.h | 83 + libfreerdp/codec/test/.gitignore | 3 + libfreerdp/codec/test/CMakeLists.txt | 37 + libfreerdp/codec/test/TestFreeRDPCodecClear.c | 90 + .../codec/test/TestFreeRDPCodecInterleaved.c | 215 + libfreerdp/codec/test/TestFreeRDPCodecMppc.c | 1114 ++ .../codec/test/TestFreeRDPCodecNCrush.c | 128 + .../codec/test/TestFreeRDPCodecPlanar.c | 5782 ++++++++++ .../codec/test/TestFreeRDPCodecProgressive.c | 1160 ++ .../codec/test/TestFreeRDPCodecRemoteFX.c | 888 ++ .../codec/test/TestFreeRDPCodecXCrush.c | 150 + libfreerdp/codec/test/TestFreeRDPCodecZGfx.c | 273 + libfreerdp/codec/test/TestFreeRDPRegion.c | 862 ++ libfreerdp/codec/test/progressive.bmp | Bin 0 -> 3506618 bytes libfreerdp/codec/test/rfx.bmp | Bin 0 -> 16438 bytes libfreerdp/codec/test/test01.bmp | Bin 0 -> 4150 bytes libfreerdp/codec/xcrush.c | 1127 ++ libfreerdp/codec/yuv.c | 177 + libfreerdp/codec/zgfx.c | 599 ++ libfreerdp/common/CMakeLists.txt | 32 + libfreerdp/common/addin.c | 360 + libfreerdp/common/assistance.c | 1391 +++ libfreerdp/common/settings.c | 950 ++ libfreerdp/common/settings_getters.c | 3020 ++++++ libfreerdp/common/settings_str.c | 752 ++ libfreerdp/common/test/.gitignore | 3 + libfreerdp/common/test/CMakeLists.txt | 26 + libfreerdp/common/test/TestCommonAssistance.c | 188 + libfreerdp/core/CMakeLists.txt | 156 + libfreerdp/core/activation.c | 487 + libfreerdp/core/activation.h | 60 + libfreerdp/core/autodetect.c | 730 ++ libfreerdp/core/autodetect.h | 56 + libfreerdp/core/bulk.c | 335 + libfreerdp/core/bulk.h | 47 + libfreerdp/core/capabilities.c | 4263 ++++++++ libfreerdp/core/capabilities.h | 175 + libfreerdp/core/certificate.c | 986 ++ libfreerdp/core/certificate.h | 71 + libfreerdp/core/channels.c | 300 + libfreerdp/core/channels.h | 32 + libfreerdp/core/client.c | 1297 +++ libfreerdp/core/client.h | 125 + libfreerdp/core/codecs.c | 250 + libfreerdp/core/connection.c | 1620 +++ libfreerdp/core/connection.h | 83 + libfreerdp/core/display.c | 84 + libfreerdp/core/display.h | 30 + libfreerdp/core/errbase.c | 103 + libfreerdp/core/errconnect.c | 189 + libfreerdp/core/errinfo.c | 704 ++ libfreerdp/core/errinfo.h | 37 + libfreerdp/core/fastpath.c | 1289 +++ libfreerdp/core/fastpath.h | 177 + libfreerdp/core/freerdp.c | 1137 ++ libfreerdp/core/gateway/http.c | 1155 ++ libfreerdp/core/gateway/http.h | 92 + libfreerdp/core/gateway/ncacn_http.c | 280 + libfreerdp/core/gateway/ncacn_http.h | 40 + libfreerdp/core/gateway/ntlm.c | 464 + libfreerdp/core/gateway/ntlm.h | 52 + libfreerdp/core/gateway/rdg.c | 2664 +++++ libfreerdp/core/gateway/rdg.h | 50 + libfreerdp/core/gateway/rpc.c | 918 ++ libfreerdp/core/gateway/rpc.h | 802 ++ libfreerdp/core/gateway/rpc_bind.c | 432 + libfreerdp/core/gateway/rpc_bind.h | 41 + libfreerdp/core/gateway/rpc_client.c | 1172 +++ libfreerdp/core/gateway/rpc_client.h | 47 + libfreerdp/core/gateway/rpc_fault.c | 430 + libfreerdp/core/gateway/rpc_fault.h | 30 + libfreerdp/core/gateway/rts.c | 2201 ++++ libfreerdp/core/gateway/rts.h | 108 + libfreerdp/core/gateway/rts_signature.c | 405 + libfreerdp/core/gateway/rts_signature.h | 189 + libfreerdp/core/gateway/tsg.c | 2793 +++++ libfreerdp/core/gateway/tsg.h | 121 + libfreerdp/core/gcc.c | 2034 ++++ libfreerdp/core/gcc.h | 40 + libfreerdp/core/graphics.c | 236 + libfreerdp/core/graphics.h | 30 + libfreerdp/core/heartbeat.c | 95 + libfreerdp/core/heartbeat.h | 39 + libfreerdp/core/info.c | 1457 +++ libfreerdp/core/info.h | 67 + libfreerdp/core/input.c | 780 ++ libfreerdp/core/input.h | 41 + libfreerdp/core/license.c | 1710 +++ libfreerdp/core/license.h | 112 + libfreerdp/core/listener.c | 417 + libfreerdp/core/listener.h | 43 + libfreerdp/core/mcs.c | 1317 +++ libfreerdp/core/mcs.h | 182 + libfreerdp/core/message.c | 3061 ++++++ libfreerdp/core/message.h | 167 + libfreerdp/core/metrics.c | 59 + libfreerdp/core/multitransport.c | 52 + libfreerdp/core/multitransport.h | 42 + libfreerdp/core/nego.c | 1489 +++ libfreerdp/core/nego.h | 143 + libfreerdp/core/nla.c | 2520 +++++ libfreerdp/core/nla.h | 69 + libfreerdp/core/orders.c | 4046 +++++++ libfreerdp/core/orders.h | 287 + libfreerdp/core/peer.c | 1003 ++ libfreerdp/core/peer.h | 29 + libfreerdp/core/proxy.c | 673 ++ libfreerdp/core/proxy.h | 32 + libfreerdp/core/rdp.c | 1965 ++++ libfreerdp/core/rdp.h | 245 + libfreerdp/core/redirection.c | 554 + libfreerdp/core/redirection.h | 51 + libfreerdp/core/security.c | 871 ++ libfreerdp/core/security.h | 56 + libfreerdp/core/server.c | 1717 +++ libfreerdp/core/server.h | 269 + libfreerdp/core/settings.c | 1058 ++ libfreerdp/core/settings.h | 35 + libfreerdp/core/surface.c | 349 + libfreerdp/core/surface.h | 37 + libfreerdp/core/tcp.c | 1237 +++ libfreerdp/core/tcp.h | 75 + libfreerdp/core/test/.gitignore | 1 + libfreerdp/core/test/CMakeLists.txt | 38 + libfreerdp/core/test/TestConnect.c | 263 + libfreerdp/core/test/TestSettings.c | 204 + libfreerdp/core/test/TestVersion.c | 47 + .../core/test/settings_property_lists.h | 408 + libfreerdp/core/timezone.c | 153 + libfreerdp/core/timezone.h | 48 + libfreerdp/core/tpdu.c | 250 + libfreerdp/core/tpdu.h | 54 + libfreerdp/core/tpkt.c | 159 + libfreerdp/core/tpkt.h | 38 + libfreerdp/core/transport.c | 1225 +++ libfreerdp/core/transport.h | 115 + libfreerdp/core/update.c | 3026 ++++++ libfreerdp/core/update.h | 72 + libfreerdp/core/utils.c | 45 + libfreerdp/core/utils.h | 30 + libfreerdp/core/window.c | 1050 ++ libfreerdp/core/window.h | 43 + libfreerdp/crypto/CMakeLists.txt | 47 + libfreerdp/crypto/base64.c | 198 + libfreerdp/crypto/ber.c | 503 + libfreerdp/crypto/certificate.c | 767 ++ libfreerdp/crypto/crypto.c | 945 ++ libfreerdp/crypto/der.c | 106 + libfreerdp/crypto/er.c | 451 + libfreerdp/crypto/opensslcompat.c | 45 + libfreerdp/crypto/opensslcompat.h | 66 + libfreerdp/crypto/per.c | 495 + libfreerdp/crypto/test/.gitignore | 1 + libfreerdp/crypto/test/CMakeLists.txt | 33 + libfreerdp/crypto/test/TestBase64.c | 112 + libfreerdp/crypto/test/TestKnownHosts.c | 350 + libfreerdp/crypto/test/Test_x509_cert_info.c | 158 + .../crypto/test/Test_x509_cert_info.pem | 41 + .../crypto/test/known_hosts/known_hosts | 2 + .../crypto/test/known_hosts/known_hosts.v2 | 2 + libfreerdp/crypto/tls.c | 1795 ++++ libfreerdp/freerdp.pc.in | 15 + libfreerdp/gdi/CMakeLists.txt | 45 + libfreerdp/gdi/bitmap.c | 643 ++ libfreerdp/gdi/brush.c | 868 ++ libfreerdp/gdi/brush.h | 51 + libfreerdp/gdi/clipping.c | 165 + libfreerdp/gdi/clipping.h | 44 + libfreerdp/gdi/dc.c | 257 + libfreerdp/gdi/drawing.c | 140 + libfreerdp/gdi/drawing.h | 45 + libfreerdp/gdi/gdi.c | 1348 +++ libfreerdp/gdi/gdi.h | 93 + libfreerdp/gdi/gfx.c | 1565 +++ libfreerdp/gdi/graphics.c | 431 + libfreerdp/gdi/graphics.h | 34 + libfreerdp/gdi/line.c | 311 + libfreerdp/gdi/line.h | 44 + libfreerdp/gdi/pen.c | 62 + libfreerdp/gdi/region.c | 665 ++ libfreerdp/gdi/shape.c | 279 + libfreerdp/gdi/test/.gitignore | 2 + libfreerdp/gdi/test/CMakeLists.txt | 38 + libfreerdp/gdi/test/TestGdiBitBlt.c | 568 + libfreerdp/gdi/test/TestGdiClip.c | 345 + libfreerdp/gdi/test/TestGdiCreate.c | 589 ++ libfreerdp/gdi/test/TestGdiEllipse.c | 170 + libfreerdp/gdi/test/TestGdiLine.c | 725 ++ libfreerdp/gdi/test/TestGdiRect.c | 169 + libfreerdp/gdi/test/TestGdiRegion.c | 250 + libfreerdp/gdi/test/TestGdiRop3.c | 204 + libfreerdp/gdi/test/helpers.c | 136 + libfreerdp/gdi/test/helpers.h | 37 + libfreerdp/gdi/video.c | 175 + libfreerdp/locale/CMakeLists.txt | 86 + libfreerdp/locale/keyboard.c | 229 + libfreerdp/locale/keyboard_apple.c | 244 + libfreerdp/locale/keyboard_apple.h | 28 + libfreerdp/locale/keyboard_layout.c | 1042 ++ libfreerdp/locale/keyboard_sun.c | 288 + libfreerdp/locale/keyboard_sun.h | 27 + libfreerdp/locale/keyboard_x11.c | 214 + libfreerdp/locale/keyboard_x11.h | 27 + libfreerdp/locale/keyboard_xkbfile.c | 476 + libfreerdp/locale/keyboard_xkbfile.h | 30 + libfreerdp/locale/liblocale.h | 49 + libfreerdp/locale/locale.c | 867 ++ libfreerdp/locale/xkb_layout_ids.c | 861 ++ libfreerdp/locale/xkb_layout_ids.h | 28 + libfreerdp/primitives/README.txt | 101 + libfreerdp/primitives/prim_YCoCg.c | 76 + libfreerdp/primitives/prim_YCoCg_opt.c | 577 + libfreerdp/primitives/prim_YUV.c | 1755 +++ libfreerdp/primitives/prim_YUV_neon.c | 751 ++ libfreerdp/primitives/prim_YUV_opencl.c | 387 + libfreerdp/primitives/prim_YUV_ssse3.c | 1467 +++ libfreerdp/primitives/prim_add.c | 50 + libfreerdp/primitives/prim_add_opt.c | 63 + libfreerdp/primitives/prim_alphaComp.c | 98 + libfreerdp/primitives/prim_alphaComp_opt.c | 238 + libfreerdp/primitives/prim_andor.c | 59 + libfreerdp/primitives/prim_andor_opt.c | 65 + libfreerdp/primitives/prim_colors.c | 519 + libfreerdp/primitives/prim_colors_opt.c | 1516 +++ libfreerdp/primitives/prim_copy.c | 178 + libfreerdp/primitives/prim_internal.h | 286 + libfreerdp/primitives/prim_set.c | 122 + libfreerdp/primitives/prim_set_opt.c | 257 + libfreerdp/primitives/prim_shift.c | 117 + libfreerdp/primitives/prim_shift_opt.c | 82 + libfreerdp/primitives/prim_sign.c | 44 + libfreerdp/primitives/prim_sign_opt.c | 172 + libfreerdp/primitives/prim_templates.h | 451 + libfreerdp/primitives/primitives.c | 400 + libfreerdp/primitives/primitives.cl | 138 + libfreerdp/primitives/test/.gitignore | 3 + libfreerdp/primitives/test/CMakeLists.txt | 45 + .../primitives/test/TestPrimitivesAdd.c | 81 + .../primitives/test/TestPrimitivesAlphaComp.c | 210 + .../primitives/test/TestPrimitivesAndOr.c | 174 + .../primitives/test/TestPrimitivesColors.c | 297 + .../primitives/test/TestPrimitivesCopy.c | 98 + .../primitives/test/TestPrimitivesSet.c | 295 + .../primitives/test/TestPrimitivesShift.c | 468 + .../primitives/test/TestPrimitivesSign.c | 97 + .../primitives/test/TestPrimitivesYCbCr.c | 1827 ++++ .../primitives/test/TestPrimitivesYCoCg.c | 149 + .../primitives/test/TestPrimitivesYUV.c | 960 ++ libfreerdp/primitives/test/measure.h | 146 + libfreerdp/primitives/test/prim_test.c | 116 + libfreerdp/primitives/test/prim_test.h | 59 + libfreerdp/utils/CMakeLists.txt | 44 + libfreerdp/utils/cliprdr_utils.c | 235 + libfreerdp/utils/passphrase.c | 139 + libfreerdp/utils/pcap.c | 237 + libfreerdp/utils/profiler.c | 100 + libfreerdp/utils/ringbuffer.c | 295 + libfreerdp/utils/signal.c | 127 + libfreerdp/utils/stopwatch.c | 100 + libfreerdp/utils/test/.gitignore | 1 + libfreerdp/utils/test/CMakeLists.txt | 26 + libfreerdp/utils/test/TestRingBuffer.c | 226 + packaging/deb/freerdp-nightly/changelog | 17 + packaging/deb/freerdp-nightly/compat | 1 + packaging/deb/freerdp-nightly/control | 92 + packaging/deb/freerdp-nightly/copyright | 0 .../freerdp-nightly-dbg.lintian-overrides | 1 + .../freerdp-nightly-dev.install | 4 + .../freerdp-nightly-dev.lintian-overrides | 1 + .../freerdp-nightly/freerdp-nightly.install | 4 + .../freerdp-nightly.lintian-overrides | 1 + .../deb/freerdp-nightly/lintian-overrides | 1 + packaging/deb/freerdp-nightly/rules | 52 + packaging/deb/freerdp-nightly/source/format | 1 + packaging/flatpak/build-bundle.sh | 33 + packaging/flatpak/com.freerdp.FreeRDP.json | 166 + packaging/flatpak/freerdp.sh | 18 + packaging/rpm/freerdp-nightly-rpmlintrc | 13 + packaging/rpm/freerdp-nightly.spec | 192 + .../scripts/prepare_deb_freerdp-nightly.sh | 4 + .../scripts/prepare_rpm_freerdp-nightly.sh | 3 + rdtk/CMakeLists.txt | 63 + rdtk/include/rdtk/api.h | 50 + rdtk/include/rdtk/rdtk.h | 83 + rdtk/librdtk/CMakeLists.txt | 54 + rdtk/librdtk/rdtk_button.c | 112 + rdtk/librdtk/rdtk_button.h | 50 + rdtk/librdtk/rdtk_engine.c | 58 + rdtk/librdtk/rdtk_engine.h | 46 + rdtk/librdtk/rdtk_font.c | 755 ++ rdtk/librdtk/rdtk_font.h | 72 + rdtk/librdtk/rdtk_label.c | 97 + rdtk/librdtk/rdtk_label.h | 48 + rdtk/librdtk/rdtk_nine_patch.c | 443 + rdtk/librdtk/rdtk_nine_patch.h | 75 + rdtk/librdtk/rdtk_resources.c | 1033 ++ rdtk/librdtk/rdtk_resources.h | 37 + rdtk/librdtk/rdtk_surface.c | 84 + rdtk/librdtk/rdtk_surface.h | 36 + rdtk/librdtk/rdtk_text_field.c | 112 + rdtk/librdtk/rdtk_text_field.h | 50 + rdtk/librdtk/test/.gitignore | 4 + rdtk/librdtk/test/CMakeLists.txt | 25 + rdtk/librdtk/test/TestRdTkNinePatch.c | 56 + rdtk/sample/.gitignore | 2 + rdtk/sample/CMakeLists.txt | 35 + rdtk/sample/rdtk_x11.c | 155 + resources/FreeRDP-fav.ico | Bin 0 -> 1150 bytes resources/FreeRDP.ico | Bin 0 -> 30160 bytes resources/FreeRDP_Icon.png | Bin 0 -> 27065 bytes resources/FreeRDP_Icon.svg | 120 + resources/FreeRDP_Icon_256px.h | 9365 +++++++++++++++++ resources/FreeRDP_Icon_256px.png | Bin 0 -> 3021 bytes resources/FreeRDP_Icon_256px.xpm | 347 + resources/FreeRDP_Icon_96px.ico | Bin 0 -> 38078 bytes resources/FreeRDP_Install.bmp | Bin 0 -> 25818 bytes resources/FreeRDP_Logo.png | Bin 0 -> 30329 bytes resources/FreeRDP_Logo.svg | 157 + resources/FreeRDP_Logo_Icon.ai | 4650 ++++++++ resources/FreeRDP_Logo_Icon.svg | 76 + resources/FreeRDP_OSX.icns | Bin 0 -> 60681 bytes resources/conv_to_ewm_prop.py | 64 + scripts/.gitignore | 4 + scripts/LECHash.c | 105 + scripts/LOMHash.c | 81 + scripts/OpenSSL-DownloadAndBuild.command | 153 + scripts/TimeZones.csx | 205 + scripts/android-build-32.conf | 33 + scripts/android-build-64.conf | 33 + scripts/android-build-common.sh | 320 + scripts/android-build-ffmpeg.sh | 179 + scripts/android-build-freerdp.sh | 189 + scripts/android-build-openh264.sh | 65 + scripts/android-build-openssl.sh | 77 + scripts/android-build-release.conf | 33 + scripts/android-build.conf | 33 + scripts/blacklist-address-sanitizer.txt | 0 scripts/blacklist-memory-sanitizer.txt | 1 + scripts/blacklist-thread-sanitizer.txt | 0 scripts/create_release_taball.sh | 49 + scripts/fetch_language_identifiers.py | 129 + scripts/gprof_generate.sh.cmake | 56 + scripts/specBytesToCode.py | 69 + scripts/test-scard.cpp | 921 ++ scripts/toolchains_path.py | 49 + scripts/update-rdpSettings | 51 + scripts/update-settings-tests | 391 + scripts/update-windows-zones.py | 41 + scripts/xcode.sh | 74 + scripts/xkb.pl | 303 + server/.gitignore | 10 + server/CMakeLists.txt | 97 + server/FreeRDP-ServerConfig.cmake.in | 10 + server/Mac/.gitignore | 2 + server/Mac/CMakeLists.txt | 75 + server/Mac/ModuleOptions.cmake | 4 + server/Mac/mf_audin.c | 77 + server/Mac/mf_audin.h | 32 + server/Mac/mf_event.c | 222 + server/Mac/mf_event.h | 75 + server/Mac/mf_info.c | 234 + server/Mac/mf_info.h | 49 + server/Mac/mf_input.c | 513 + server/Mac/mf_input.h | 36 + server/Mac/mf_interface.c | 0 server/Mac/mf_interface.h | 104 + server/Mac/mf_mountain_lion.c | 270 + server/Mac/mf_mountain_lion.h | 38 + server/Mac/mf_peer.c | 438 + server/Mac/mf_peer.h | 27 + server/Mac/mf_rdpsnd.c | 201 + server/Mac/mf_rdpsnd.h | 60 + server/Mac/mfreerdp.c | 122 + server/Mac/mfreerdp.h | 28 + server/Mac/server.crt | 17 + server/Mac/server.key | 27 + server/Sample/CMakeLists.txt | 58 + server/Sample/ModuleOptions.cmake | 4 + server/Sample/rfx_test.pcap | Bin 0 -> 994948 bytes server/Sample/server.crt | 17 + server/Sample/server.key | 28 + server/Sample/sf_audin.c | 117 + server/Sample/sf_audin.h | 36 + server/Sample/sf_encomsp.c | 40 + server/Sample/sf_encomsp.h | 31 + server/Sample/sf_rdpsnd.c | 59 + server/Sample/sf_rdpsnd.h | 31 + server/Sample/sfreerdp.c | 1000 ++ server/Sample/sfreerdp.h | 63 + server/Sample/test_icon.ppm | 5572 ++++++++++ server/Windows/CMakeLists.txt | 118 + server/Windows/ModuleOptions.cmake | 4 + server/Windows/cli/CMakeLists.txt | 55 + server/Windows/cli/wfreerdp.c | 173 + server/Windows/cli/wfreerdp.h | 25 + server/Windows/server.crt | 17 + server/Windows/server.key | 27 + server/Windows/wf_directsound.c | 219 + server/Windows/wf_directsound.h | 13 + server/Windows/wf_dxgi.c | 490 + server/Windows/wf_dxgi.h | 41 + server/Windows/wf_info.c | 406 + server/Windows/wf_info.h | 46 + server/Windows/wf_input.c | 225 + server/Windows/wf_input.h | 36 + server/Windows/wf_interface.c | 354 + server/Windows/wf_interface.h | 140 + server/Windows/wf_mirage.c | 362 + server/Windows/wf_mirage.h | 219 + server/Windows/wf_peer.c | 372 + server/Windows/wf_peer.h | 29 + server/Windows/wf_rdpsnd.c | 154 + server/Windows/wf_rdpsnd.h | 33 + server/Windows/wf_settings.c | 104 + server/Windows/wf_settings.h | 28 + server/Windows/wf_update.c | 242 + server/Windows/wf_update.h | 37 + server/Windows/wf_wasapi.c | 330 + server/Windows/wf_wasapi.h | 15 + server/common/CMakeLists.txt | 73 + server/common/server.c | 163 + server/freerdp-server.pc.in | 15 + server/proxy/CMakeLists.txt | 95 + server/proxy/config.ini | 54 + server/proxy/freerdp_proxy.c | 132 + server/proxy/modules/CMakeLists.txt | 33 + server/proxy/modules/README.md | 27 + server/proxy/modules/capture/CMakeLists.txt | 33 + server/proxy/modules/capture/cap_config.c | 97 + server/proxy/modules/capture/cap_config.h | 29 + server/proxy/modules/capture/cap_main.c | 288 + server/proxy/modules/capture/cap_protocol.c | 57 + server/proxy/modules/capture/cap_protocol.h | 35 + server/proxy/modules/demo/CMakeLists.txt | 30 + server/proxy/modules/demo/demo.cpp | 73 + server/proxy/modules/modules_api.h | 125 + server/proxy/pf_capture.c | 168 + server/proxy/pf_capture.h | 28 + server/proxy/pf_channels.c | 303 + server/proxy/pf_channels.h | 36 + server/proxy/pf_client.c | 702 ++ server/proxy/pf_client.h | 31 + server/proxy/pf_cliprdr.c | 432 + server/proxy/pf_cliprdr.h | 33 + server/proxy/pf_config.c | 377 + server/proxy/pf_config.h | 102 + server/proxy/pf_context.c | 297 + server/proxy/pf_context.h | 132 + server/proxy/pf_disp.c | 73 + server/proxy/pf_disp.h | 32 + server/proxy/pf_gdi.c | 143 + server/proxy/pf_gdi.h | 29 + server/proxy/pf_graphics.c | 166 + server/proxy/pf_graphics.h | 31 + server/proxy/pf_input.c | 103 + server/proxy/pf_input.h | 29 + server/proxy/pf_log.h | 50 + server/proxy/pf_modules.c | 419 + server/proxy/pf_modules.h | 62 + server/proxy/pf_rail.c | 345 + server/proxy/pf_rail.h | 33 + server/proxy/pf_rdpgfx.c | 503 + server/proxy/pf_rdpgfx.h | 35 + server/proxy/pf_rdpsnd.c | 80 + server/proxy/pf_rdpsnd.h | 31 + server/proxy/pf_server.c | 605 ++ server/proxy/pf_server.h | 47 + server/proxy/pf_update.c | 401 + server/proxy/pf_update.h | 34 + server/proxy/server.crt | 17 + server/proxy/server.key | 28 + .../generate_video_from_frames.py | 47 + server/proxy/session-capture/requirements.txt | 1 + server/shadow/.gitignore | 2 + server/shadow/CMakeLists.txt | 365 + server/shadow/FreeRDP-ShadowConfig.cmake.in | 10 + server/shadow/Mac/mac_shadow.c | 673 ++ server/shadow/Mac/mac_shadow.h | 64 + server/shadow/Win/win_dxgi.c | 798 ++ server/shadow/Win/win_dxgi.h | 61 + server/shadow/Win/win_rdp.c | 401 + server/shadow/Win/win_rdp.h | 57 + server/shadow/Win/win_shadow.c | 543 + server/shadow/Win/win_shadow.h | 89 + server/shadow/Win/win_wds.c | 854 ++ server/shadow/Win/win_wds.h | 48 + server/shadow/X11/x11_shadow.c | 1463 +++ server/shadow/X11/x11_shadow.h | 114 + server/shadow/freerdp-shadow-cli.1.in | 85 + server/shadow/freerdp-shadow.pc.in | 15 + server/shadow/shadow.c | 113 + server/shadow/shadow.h | 44 + server/shadow/shadow_audin.c | 150 + server/shadow/shadow_audin.h | 39 + server/shadow/shadow_capture.c | 247 + server/shadow/shadow_capture.h | 49 + server/shadow/shadow_channels.c | 68 + server/shadow/shadow_channels.h | 45 + server/shadow/shadow_client.c | 2057 ++++ server/shadow/shadow_client.h | 35 + server/shadow/shadow_encoder.c | 482 + server/shadow/shadow_encoder.h | 78 + server/shadow/shadow_encomsp.c | 131 + server/shadow/shadow_encomsp.h | 39 + server/shadow/shadow_input.c | 116 + server/shadow/shadow_input.h | 35 + server/shadow/shadow_lobby.c | 79 + server/shadow/shadow_lobby.h | 40 + server/shadow/shadow_mcevent.c | 358 + server/shadow/shadow_mcevent.h | 53 + server/shadow/shadow_rdpgfx.c | 53 + server/shadow/shadow_rdpgfx.h | 39 + server/shadow/shadow_rdpsnd.c | 101 + server/shadow/shadow_rdpsnd.h | 39 + server/shadow/shadow_remdesk.c | 52 + server/shadow/shadow_remdesk.h | 39 + server/shadow/shadow_screen.c | 145 + server/shadow/shadow_screen.h | 53 + server/shadow/shadow_server.c | 926 ++ server/shadow/shadow_subsystem.c | 279 + server/shadow/shadow_subsystem.h | 45 + server/shadow/shadow_subsystem_builtin.c | 97 + server/shadow/shadow_surface.c | 102 + server/shadow/shadow_surface.h | 41 + third-party/.gitignore | 5 + third-party/CMakeLists.txt | 32 + uwac/CMakeLists.txt | 52 + uwac/include/CMakeLists.txt | 19 + uwac/include/uwac/uwac-tools.h | 60 + uwac/include/uwac/uwac.h | 646 ++ uwac/libuwac/.gitignore | 7 + uwac/libuwac/CMakeLists.txt | 88 + uwac/libuwac/uwac-clipboard.c | 277 + uwac/libuwac/uwac-display.c | 779 ++ uwac/libuwac/uwac-input.c | 1165 ++ uwac/libuwac/uwac-os.c | 281 + uwac/libuwac/uwac-os.h | 45 + uwac/libuwac/uwac-output.c | 143 + uwac/libuwac/uwac-priv.h | 280 + uwac/libuwac/uwac-tools.c | 107 + uwac/libuwac/uwac-utils.c | 67 + uwac/libuwac/uwac-utils.h | 56 + uwac/libuwac/uwac-window.c | 825 ++ .../fullscreen-shell-unstable-v1.xml | 220 + uwac/protocols/ivi-application.xml | 100 + ...keyboard-shortcuts-inhibit-unstable-v1.xml | 143 + uwac/protocols/server-decoration.xml | 96 + uwac/protocols/xdg-decoration-unstable-v1.xml | 156 + uwac/protocols/xdg-shell.xml | 1144 ++ uwac/uwac.pc.in | 15 + uwac/uwacConfig.cmake.in | 9 + uwac/uwacVersion.cmake | 46 + winpr/.gitignore | 1 + winpr/CMakeLists.txt | 259 + winpr/WinPRConfig.cmake.in | 10 + winpr/include/CMakeLists.txt | 26 + winpr/include/winpr/.gitignore | 2 + winpr/include/winpr/asn1.h | 561 + winpr/include/winpr/assert.h | 59 + winpr/include/winpr/bcrypt.h | 141 + winpr/include/winpr/bitstream.h | 176 + winpr/include/winpr/clipboard.h | 109 + winpr/include/winpr/cmdline.h | 182 + winpr/include/winpr/collections.h | 675 ++ winpr/include/winpr/comm.h | 565 + winpr/include/winpr/credentials.h | 289 + winpr/include/winpr/credui.h | 162 + winpr/include/winpr/crt.h | 171 + winpr/include/winpr/crypto.h | 850 ++ winpr/include/winpr/debug.h | 45 + winpr/include/winpr/dsparse.h | 135 + winpr/include/winpr/endian.h | 178 + winpr/include/winpr/environment.h | 141 + winpr/include/winpr/error.h | 3040 ++++++ winpr/include/winpr/file.h | 530 + winpr/include/winpr/handle.h | 64 + winpr/include/winpr/heap.h | 52 + winpr/include/winpr/image.h | 104 + winpr/include/winpr/ini.h | 58 + winpr/include/winpr/input.h | 890 ++ winpr/include/winpr/interlocked.h | 212 + winpr/include/winpr/intrin.h | 90 + winpr/include/winpr/io.h | 253 + winpr/include/winpr/library.h | 107 + winpr/include/winpr/locale.h | 507 + winpr/include/winpr/memory.h | 77 + winpr/include/winpr/midl.h | 41 + winpr/include/winpr/ndr.h | 541 + winpr/include/winpr/nt.h | 1609 +++ winpr/include/winpr/ntlm.h | 68 + winpr/include/winpr/pack.h | 100 + winpr/include/winpr/path.h | 351 + winpr/include/winpr/pipe.h | 127 + winpr/include/winpr/platform.h | 290 + winpr/include/winpr/pool.h | 275 + winpr/include/winpr/print.h | 53 + winpr/include/winpr/registry.h | 423 + winpr/include/winpr/rpc.h | 723 ++ winpr/include/winpr/sam.h | 59 + winpr/include/winpr/schannel.h | 289 + winpr/include/winpr/security.h | 449 + winpr/include/winpr/shell.h | 124 + winpr/include/winpr/smartcard.h | 1189 +++ winpr/include/winpr/spec.h | 977 ++ winpr/include/winpr/ssl.h | 48 + winpr/include/winpr/sspi.h | 1184 +++ winpr/include/winpr/sspicli.h | 147 + winpr/include/winpr/stream.h | 449 + winpr/include/winpr/string.h | 218 + winpr/include/winpr/strlst.h | 41 + winpr/include/winpr/synch.h | 411 + winpr/include/winpr/sysinfo.h | 351 + winpr/include/winpr/tchar.h | 70 + winpr/include/winpr/thread.h | 257 + winpr/include/winpr/timezone.h | 115 + winpr/include/winpr/tools/makecert.h | 45 + winpr/include/winpr/user.h | 296 + winpr/include/winpr/version.h.in | 32 + winpr/include/winpr/windows.h | 130 + winpr/include/winpr/winhttp.h | 690 ++ winpr/include/winpr/winpr.h | 87 + winpr/include/winpr/winsock.h | 360 + winpr/include/winpr/wlog.h | 223 + winpr/include/winpr/wnd.h | 596 ++ winpr/include/winpr/wtsapi.h | 1508 +++ winpr/include/winpr/wtypes.h.in | 572 + winpr/libwinpr/CMakeLists.txt | 152 + winpr/libwinpr/asn1/CMakeLists.txt | 22 + winpr/libwinpr/asn1/ModuleOptions.cmake | 9 + winpr/libwinpr/asn1/asn1.c | 328 + winpr/libwinpr/asn1/test/.gitignore | 2 + winpr/libwinpr/asn1/test/CMakeLists.txt | 34 + winpr/libwinpr/asn1/test/TestAsn1BerDec.c | 10 + winpr/libwinpr/asn1/test/TestAsn1BerEnc.c | 10 + winpr/libwinpr/asn1/test/TestAsn1Compare.c | 10 + winpr/libwinpr/asn1/test/TestAsn1Decode.c | 10 + winpr/libwinpr/asn1/test/TestAsn1Decoder.c | 10 + winpr/libwinpr/asn1/test/TestAsn1DerDec.c | 10 + winpr/libwinpr/asn1/test/TestAsn1DerEnc.c | 10 + winpr/libwinpr/asn1/test/TestAsn1Encode.c | 10 + winpr/libwinpr/asn1/test/TestAsn1Encoder.c | 10 + winpr/libwinpr/asn1/test/TestAsn1Integer.c | 10 + winpr/libwinpr/asn1/test/TestAsn1Module.c | 10 + winpr/libwinpr/asn1/test/TestAsn1String.c | 10 + winpr/libwinpr/bcrypt/CMakeLists.txt | 19 + winpr/libwinpr/bcrypt/ModuleOptions.cmake | 9 + winpr/libwinpr/bcrypt/bcrypt.c | 113 + winpr/libwinpr/clipboard/CMakeLists.txt | 34 + winpr/libwinpr/clipboard/ModuleOptions.cmake | 9 + winpr/libwinpr/clipboard/clipboard.c | 610 ++ winpr/libwinpr/clipboard/clipboard.h | 75 + winpr/libwinpr/clipboard/posix.c | 974 ++ winpr/libwinpr/clipboard/posix.h | 27 + winpr/libwinpr/clipboard/synthetic.c | 640 ++ winpr/libwinpr/clipboard/test/.gitignore | 3 + winpr/libwinpr/clipboard/test/CMakeLists.txt | 25 + .../clipboard/test/TestClipboardFormats.c | 86 + winpr/libwinpr/comm/CMakeLists.txt | 40 + winpr/libwinpr/comm/ModuleOptions.cmake | 9 + winpr/libwinpr/comm/comm.c | 1476 +++ winpr/libwinpr/comm/comm.h | 109 + winpr/libwinpr/comm/comm_io.c | 566 + winpr/libwinpr/comm/comm_ioctl.c | 742 ++ winpr/libwinpr/comm/comm_ioctl.h | 236 + winpr/libwinpr/comm/comm_sercx2_sys.c | 197 + winpr/libwinpr/comm/comm_sercx2_sys.h | 40 + winpr/libwinpr/comm/comm_sercx_sys.c | 263 + winpr/libwinpr/comm/comm_sercx_sys.h | 40 + winpr/libwinpr/comm/comm_serial_sys.c | 1617 +++ winpr/libwinpr/comm/comm_serial_sys.h | 40 + winpr/libwinpr/comm/test/.gitignore | 2 + winpr/libwinpr/comm/test/CMakeLists.txt | 35 + winpr/libwinpr/comm/test/TestCommConfig.c | 150 + winpr/libwinpr/comm/test/TestCommDevice.c | 115 + winpr/libwinpr/comm/test/TestCommMonitor.c | 71 + .../libwinpr/comm/test/TestControlSettings.c | 123 + winpr/libwinpr/comm/test/TestGetCommState.c | 138 + winpr/libwinpr/comm/test/TestHandflow.c | 92 + winpr/libwinpr/comm/test/TestSerialChars.c | 181 + winpr/libwinpr/comm/test/TestSetCommState.c | 330 + winpr/libwinpr/comm/test/TestTimeouts.c | 139 + winpr/libwinpr/credentials/CMakeLists.txt | 19 + .../libwinpr/credentials/ModuleOptions.cmake | 9 + winpr/libwinpr/credentials/credentials.c | 209 + winpr/libwinpr/credui/CMakeLists.txt | 26 + winpr/libwinpr/credui/ModuleOptions.cmake | 9 + winpr/libwinpr/credui/credui.c | 109 + winpr/libwinpr/credui/test/.gitignore | 3 + winpr/libwinpr/credui/test/CMakeLists.txt | 29 + .../TestCredUICmdLinePromptForCredentials.c | 36 + .../test/TestCredUIConfirmCredentials.c | 11 + .../credui/test/TestCredUIParseUserName.c | 42 + .../test/TestCredUIPromptForCredentials.c | 39 + winpr/libwinpr/crt/CMakeLists.txt | 41 + winpr/libwinpr/crt/ModuleOptions.cmake | 9 + winpr/libwinpr/crt/alignment.c | 245 + winpr/libwinpr/crt/buffer.c | 52 + winpr/libwinpr/crt/casing.c | 726 ++ winpr/libwinpr/crt/conversion.c | 48 + winpr/libwinpr/crt/memory.c | 44 + winpr/libwinpr/crt/string.c | 597 ++ winpr/libwinpr/crt/test/.gitignore | 3 + winpr/libwinpr/crt/test/CMakeLists.txt | 30 + winpr/libwinpr/crt/test/TestAlignment.c | 84 + .../libwinpr/crt/test/TestFormatSpecifiers.c | 166 + winpr/libwinpr/crt/test/TestString.c | 121 + winpr/libwinpr/crt/test/TestTypes.c | 112 + .../libwinpr/crt/test/TestUnicodeConversion.c | 650 ++ winpr/libwinpr/crt/unicode.c | 521 + winpr/libwinpr/crt/utf.c | 879 ++ winpr/libwinpr/crt/utf.h | 150 + winpr/libwinpr/crypto/CMakeLists.txt | 42 + winpr/libwinpr/crypto/ModuleOptions.cmake | 9 + winpr/libwinpr/crypto/cert.c | 224 + winpr/libwinpr/crypto/cipher.c | 812 ++ winpr/libwinpr/crypto/crypto.c | 302 + winpr/libwinpr/crypto/crypto.h | 45 + winpr/libwinpr/crypto/hash.c | 538 + winpr/libwinpr/crypto/rand.c | 59 + winpr/libwinpr/crypto/test/.gitignore | 3 + winpr/libwinpr/crypto/test/CMakeLists.txt | 34 + .../TestCryptoCertEnumCertificatesInStore.c | 83 + winpr/libwinpr/crypto/test/TestCryptoCipher.c | 234 + winpr/libwinpr/crypto/test/TestCryptoHash.c | 304 + .../crypto/test/TestCryptoProtectData.c | 10 + .../crypto/test/TestCryptoProtectMemory.c | 51 + winpr/libwinpr/crypto/test/TestCryptoRand.c | 25 + winpr/libwinpr/dsparse/CMakeLists.txt | 26 + winpr/libwinpr/dsparse/ModuleOptions.cmake | 9 + winpr/libwinpr/dsparse/dsparse.c | 94 + winpr/libwinpr/dsparse/test/.gitignore | 4 + winpr/libwinpr/dsparse/test/CMakeLists.txt | 26 + .../libwinpr/dsparse/test/TestDsCrackNames.c | 49 + winpr/libwinpr/dsparse/test/TestDsMakeSpn.c | 71 + winpr/libwinpr/dummy.c | 5 + winpr/libwinpr/environment/CMakeLists.txt | 22 + .../libwinpr/environment/ModuleOptions.cmake | 9 + winpr/libwinpr/environment/environment.c | 647 ++ winpr/libwinpr/environment/test/.gitignore | 3 + .../libwinpr/environment/test/CMakeLists.txt | 28 + .../TestEnvironmentGetEnvironmentStrings.c | 43 + .../test/TestEnvironmentGetSetEB.c | 134 + .../TestEnvironmentMergeEnvironmentStrings.c | 30 + .../TestEnvironmentSetEnvironmentVariable.c | 68 + winpr/libwinpr/error/CMakeLists.txt | 22 + winpr/libwinpr/error/ModuleOptions.cmake | 9 + winpr/libwinpr/error/error.c | 101 + winpr/libwinpr/error/test/.gitignore | 3 + winpr/libwinpr/error/test/CMakeLists.txt | 26 + .../error/test/TestErrorSetLastError.c | 132 + winpr/libwinpr/file/CMakeLists.txt | 22 + winpr/libwinpr/file/ModuleOptions.cmake | 9 + winpr/libwinpr/file/file.c | 1387 +++ winpr/libwinpr/file/file.h | 63 + winpr/libwinpr/file/generic.c | 1382 +++ winpr/libwinpr/file/namedPipeClient.c | 315 + winpr/libwinpr/file/pattern.c | 373 + winpr/libwinpr/file/test/.gitignore | 3 + winpr/libwinpr/file/test/CMakeLists.txt | 58 + winpr/libwinpr/file/test/TestFileCreateFile.c | 92 + winpr/libwinpr/file/test/TestFileDeleteFile.c | 10 + .../file/test/TestFileFindFirstFile.c | 68 + .../file/test/TestFileFindFirstFileEx.c | 10 + .../libwinpr/file/test/TestFileFindNextFile.c | 102 + .../libwinpr/file/test/TestFileGetStdHandle.c | 48 + .../libwinpr/file/test/TestFilePatternMatch.c | 181 + winpr/libwinpr/file/test/TestFileReadFile.c | 10 + winpr/libwinpr/file/test/TestFileWriteFile.c | 10 + .../file/test/TestSetFileAttributes.c | 149 + winpr/libwinpr/handle/CMakeLists.txt | 23 + winpr/libwinpr/handle/ModuleOptions.cmake | 9 + winpr/libwinpr/handle/handle.c | 84 + winpr/libwinpr/handle/handle.h | 179 + winpr/libwinpr/handle/nonehandle.c | 92 + winpr/libwinpr/handle/nonehandle.h | 40 + winpr/libwinpr/heap/CMakeLists.txt | 18 + winpr/libwinpr/heap/ModuleOptions.cmake | 9 + winpr/libwinpr/heap/heap.c | 65 + winpr/libwinpr/input/CMakeLists.txt | 21 + winpr/libwinpr/input/ModuleOptions.cmake | 9 + winpr/libwinpr/input/keycode.c | 630 ++ winpr/libwinpr/input/scancode.c | 200 + winpr/libwinpr/input/virtualkey.c | 467 + winpr/libwinpr/interlocked/CMakeLists.txt | 22 + .../libwinpr/interlocked/ModuleOptions.cmake | 9 + winpr/libwinpr/interlocked/interlocked.c | 468 + winpr/libwinpr/interlocked/module_5.1.def | 13 + winpr/libwinpr/interlocked/test/.gitignore | 3 + .../libwinpr/interlocked/test/CMakeLists.txt | 28 + .../interlocked/test/TestInterlockedAccess.c | 200 + .../interlocked/test/TestInterlockedDList.c | 76 + .../interlocked/test/TestInterlockedSList.c | 84 + winpr/libwinpr/io/CMakeLists.txt | 22 + winpr/libwinpr/io/ModuleOptions.cmake | 9 + winpr/libwinpr/io/device.c | 237 + winpr/libwinpr/io/io.c | 279 + winpr/libwinpr/io/io.h | 39 + winpr/libwinpr/io/test/.gitignore | 3 + winpr/libwinpr/io/test/CMakeLists.txt | 26 + winpr/libwinpr/io/test/TestIoDevice.c | 28 + .../io/test/TestIoGetOverlappedResult.c | 10 + winpr/libwinpr/library/CMakeLists.txt | 22 + winpr/libwinpr/library/ModuleOptions.cmake | 9 + winpr/libwinpr/library/library.c | 345 + winpr/libwinpr/library/test/.gitignore | 3 + winpr/libwinpr/library/test/CMakeLists.txt | 33 + .../library/test/TestLibraryA/CMakeLists.txt | 29 + .../library/test/TestLibraryA/TestLibraryA.c | 14 + .../library/test/TestLibraryB/CMakeLists.txt | 30 + .../library/test/TestLibraryB/TestLibraryB.c | 14 + .../test/TestLibraryGetModuleFileName.c | 55 + .../library/test/TestLibraryGetProcAddress.c | 86 + .../library/test/TestLibraryLoadLibrary.c | 50 + winpr/libwinpr/locale/CMakeLists.txt | 26 + winpr/libwinpr/locale/ModuleOptions.cmake | 8 + winpr/libwinpr/locale/locale.c | 105 + winpr/libwinpr/locale/test/.gitignore | 2 + winpr/libwinpr/locale/test/CMakeLists.txt | 23 + .../locale/test/TestLocaleFormatMessage.c | 7 + winpr/libwinpr/log.h | 27 + winpr/libwinpr/memory/CMakeLists.txt | 22 + winpr/libwinpr/memory/ModuleOptions.cmake | 9 + winpr/libwinpr/memory/memory.c | 130 + winpr/libwinpr/memory/memory.h | 30 + winpr/libwinpr/memory/test/.gitignore | 2 + winpr/libwinpr/memory/test/CMakeLists.txt | 23 + .../memory/test/TestMemoryCreateFileMapping.c | 8 + winpr/libwinpr/nt/CMakeLists.txt | 30 + winpr/libwinpr/nt/ModuleOptions.cmake | 8 + winpr/libwinpr/nt/nt.c | 618 ++ winpr/libwinpr/nt/ntstatus.c | 1863 ++++ winpr/libwinpr/nt/test/.gitignore | 3 + winpr/libwinpr/nt/test/CMakeLists.txt | 27 + winpr/libwinpr/nt/test/TestNtCreateFile.c | 75 + winpr/libwinpr/nt/test/TestNtCurrentTeb.c | 20 + winpr/libwinpr/path/CMakeLists.txt | 26 + winpr/libwinpr/path/ModuleOptions.cmake | 9 + .../libwinpr/path/include/PathAllocCombine.c | 178 + .../path/include/PathCchAddExtension.c | 99 + .../path/include/PathCchAddSeparator.c | 64 + .../path/include/PathCchAddSeparatorEx.c | 66 + winpr/libwinpr/path/include/PathCchAppend.c | 124 + winpr/libwinpr/path/path.c | 1066 ++ winpr/libwinpr/path/shell.c | 723 ++ winpr/libwinpr/path/shell_ios.h | 9 + winpr/libwinpr/path/shell_ios.m | 56 + winpr/libwinpr/path/test/.gitignore | 3 + winpr/libwinpr/path/test/CMakeLists.txt | 48 + .../path/test/TestPathAllocCanonicalize.c | 12 + .../libwinpr/path/test/TestPathAllocCombine.c | 95 + .../path/test/TestPathCchAddBackslash.c | 97 + .../path/test/TestPathCchAddBackslashEx.c | 100 + .../path/test/TestPathCchAddExtension.c | 137 + winpr/libwinpr/path/test/TestPathCchAppend.c | 149 + .../libwinpr/path/test/TestPathCchAppendEx.c | 12 + .../path/test/TestPathCchCanonicalize.c | 12 + .../path/test/TestPathCchCanonicalizeEx.c | 12 + winpr/libwinpr/path/test/TestPathCchCombine.c | 12 + .../libwinpr/path/test/TestPathCchCombineEx.c | 12 + .../path/test/TestPathCchFindExtension.c | 113 + winpr/libwinpr/path/test/TestPathCchIsRoot.c | 12 + .../path/test/TestPathCchRemoveBackslash.c | 12 + .../path/test/TestPathCchRemoveBackslashEx.c | 12 + .../path/test/TestPathCchRemoveExtension.c | 12 + .../path/test/TestPathCchRemoveFileSpec.c | 12 + .../path/test/TestPathCchRenameExtension.c | 12 + .../libwinpr/path/test/TestPathCchSkipRoot.c | 12 + .../path/test/TestPathCchStripPrefix.c | 126 + .../path/test/TestPathCchStripToRoot.c | 12 + winpr/libwinpr/path/test/TestPathIsUNCEx.c | 49 + winpr/libwinpr/path/test/TestPathMakePath.c | 81 + winpr/libwinpr/path/test/TestPathShell.c | 54 + winpr/libwinpr/pipe/CMakeLists.txt | 22 + winpr/libwinpr/pipe/ModuleOptions.cmake | 9 + winpr/libwinpr/pipe/pipe.c | 926 ++ winpr/libwinpr/pipe/pipe.h | 75 + winpr/libwinpr/pipe/test/.gitignore | 3 + winpr/libwinpr/pipe/test/CMakeLists.txt | 27 + .../pipe/test/TestPipeCreateNamedPipe.c | 496 + .../test/TestPipeCreateNamedPipeOverlapped.c | 396 + winpr/libwinpr/pipe/test/TestPipeCreatePipe.c | 72 + winpr/libwinpr/pool/CMakeLists.txt | 41 + winpr/libwinpr/pool/ModuleOptions.cmake | 9 + winpr/libwinpr/pool/callback.c | 56 + winpr/libwinpr/pool/callback_cleanup.c | 142 + winpr/libwinpr/pool/cleanup_group.c | 142 + winpr/libwinpr/pool/io.c | 51 + winpr/libwinpr/pool/pool.c | 263 + winpr/libwinpr/pool/pool.h | 77 + winpr/libwinpr/pool/synch.c | 46 + winpr/libwinpr/pool/test/.gitignore | 2 + winpr/libwinpr/pool/test/CMakeLists.txt | 30 + winpr/libwinpr/pool/test/TestPoolIO.c | 8 + winpr/libwinpr/pool/test/TestPoolSynch.c | 8 + winpr/libwinpr/pool/test/TestPoolThread.c | 38 + winpr/libwinpr/pool/test/TestPoolTimer.c | 8 + winpr/libwinpr/pool/test/TestPoolWork.c | 135 + winpr/libwinpr/pool/timer.c | 52 + winpr/libwinpr/pool/work.c | 188 + winpr/libwinpr/registry/CMakeLists.txt | 21 + winpr/libwinpr/registry/ModuleOptions.cmake | 9 + winpr/libwinpr/registry/registry.c | 405 + winpr/libwinpr/registry/registry_reg.c | 523 + winpr/libwinpr/registry/registry_reg.h | 69 + winpr/libwinpr/rpc/CMakeLists.txt | 47 + winpr/libwinpr/rpc/ModuleOptions.cmake | 9 + winpr/libwinpr/rpc/midl.c | 41 + winpr/libwinpr/rpc/ndr.c | 353 + winpr/libwinpr/rpc/ndr_array.c | 143 + winpr/libwinpr/rpc/ndr_array.h | 40 + winpr/libwinpr/rpc/ndr_context.c | 76 + winpr/libwinpr/rpc/ndr_context.h | 32 + winpr/libwinpr/rpc/ndr_correlation.c | 203 + winpr/libwinpr/rpc/ndr_correlation.h | 36 + winpr/libwinpr/rpc/ndr_pointer.c | 319 + winpr/libwinpr/rpc/ndr_pointer.h | 46 + winpr/libwinpr/rpc/ndr_private.c | 627 ++ winpr/libwinpr/rpc/ndr_private.h | 49 + winpr/libwinpr/rpc/ndr_simple.c | 198 + winpr/libwinpr/rpc/ndr_simple.h | 42 + winpr/libwinpr/rpc/ndr_string.c | 46 + winpr/libwinpr/rpc/ndr_string.h | 34 + winpr/libwinpr/rpc/ndr_structure.c | 310 + winpr/libwinpr/rpc/ndr_structure.h | 38 + winpr/libwinpr/rpc/ndr_union.c | 46 + winpr/libwinpr/rpc/ndr_union.h | 34 + winpr/libwinpr/rpc/rpc.c | 932 ++ winpr/libwinpr/security/CMakeLists.txt | 22 + winpr/libwinpr/security/ModuleOptions.cmake | 8 + winpr/libwinpr/security/security.c | 214 + winpr/libwinpr/security/security.h | 43 + winpr/libwinpr/security/test/.gitignore | 3 + winpr/libwinpr/security/test/CMakeLists.txt | 23 + .../security/test/TestSecurityToken.c | 9 + winpr/libwinpr/shell/CMakeLists.txt | 20 + winpr/libwinpr/shell/ModuleOptions.cmake | 9 + winpr/libwinpr/shell/shell.c | 143 + winpr/libwinpr/smartcard/CMakeLists.txt | 51 + winpr/libwinpr/smartcard/ModuleOptions.cmake | 9 + winpr/libwinpr/smartcard/smartcard.c | 1093 ++ winpr/libwinpr/smartcard/smartcard.h | 31 + winpr/libwinpr/smartcard/smartcard_inspect.c | 1321 +++ winpr/libwinpr/smartcard/smartcard_inspect.h | 30 + winpr/libwinpr/smartcard/smartcard_pcsc.c | 3233 ++++++ winpr/libwinpr/smartcard/smartcard_pcsc.h | 175 + winpr/libwinpr/smartcard/smartcard_winscard.c | 207 + winpr/libwinpr/smartcard/smartcard_winscard.h | 35 + winpr/libwinpr/smartcard/test/.gitignore | 2 + winpr/libwinpr/smartcard/test/CMakeLists.txt | 26 + .../smartcard/test/TestSmartCardListReaders.c | 53 + .../smartcard/test/TestSmartCardStatus.c | 160 + winpr/libwinpr/sspi/CMakeLists.txt | 85 + winpr/libwinpr/sspi/CredSSP/credssp.c | 298 + winpr/libwinpr/sspi/CredSSP/credssp.h | 36 + winpr/libwinpr/sspi/Kerberos/kerberos.c | 800 ++ winpr/libwinpr/sspi/Kerberos/kerberos.h | 37 + winpr/libwinpr/sspi/ModuleOptions.cmake | 9 + winpr/libwinpr/sspi/NTLM/ntlm.c | 1372 +++ winpr/libwinpr/sspi/NTLM/ntlm.h | 294 + winpr/libwinpr/sspi/NTLM/ntlm_av_pairs.c | 775 ++ winpr/libwinpr/sspi/NTLM/ntlm_av_pairs.h | 36 + winpr/libwinpr/sspi/NTLM/ntlm_compute.c | 876 ++ winpr/libwinpr/sspi/NTLM/ntlm_compute.h | 64 + winpr/libwinpr/sspi/NTLM/ntlm_message.c | 1482 +++ winpr/libwinpr/sspi/NTLM/ntlm_message.h | 36 + winpr/libwinpr/sspi/Negotiate/negotiate.c | 624 ++ winpr/libwinpr/sspi/Negotiate/negotiate.h | 53 + winpr/libwinpr/sspi/Schannel/schannel.c | 446 + winpr/libwinpr/sspi/Schannel/schannel.h | 48 + .../libwinpr/sspi/Schannel/schannel_openssl.c | 612 ++ .../libwinpr/sspi/Schannel/schannel_openssl.h | 50 + winpr/libwinpr/sspi/sspi.c | 1125 ++ winpr/libwinpr/sspi/sspi.h | 91 + winpr/libwinpr/sspi/sspi_export.c | 320 + winpr/libwinpr/sspi/sspi_gss.c | 1044 ++ winpr/libwinpr/sspi/sspi_gss.h | 680 ++ winpr/libwinpr/sspi/sspi_winpr.c | 1692 +++ winpr/libwinpr/sspi/sspi_winpr.h | 28 + winpr/libwinpr/sspi/test/.gitignore | 3 + winpr/libwinpr/sspi/test/CMakeLists.txt | 38 + .../sspi/test/TestAcquireCredentialsHandle.c | 56 + winpr/libwinpr/sspi/test/TestCredSSP.c | 8 + .../sspi/test/TestEnumerateSecurityPackages.c | 36 + .../sspi/test/TestInitializeSecurityContext.c | 111 + winpr/libwinpr/sspi/test/TestNTLM.c | 703 ++ .../sspi/test/TestQuerySecurityPackageInfo.c | 27 + winpr/libwinpr/sspi/test/TestSchannel.c | 864 ++ winpr/libwinpr/sspicli/CMakeLists.txt | 18 + winpr/libwinpr/sspicli/ModuleOptions.cmake | 9 + winpr/libwinpr/sspicli/sspicli.c | 287 + winpr/libwinpr/synch/CMakeLists.txt | 40 + winpr/libwinpr/synch/ModuleOptions.cmake | 9 + winpr/libwinpr/synch/address.c | 48 + winpr/libwinpr/synch/barrier.c | 263 + winpr/libwinpr/synch/critical.c | 262 + winpr/libwinpr/synch/event.c | 469 + winpr/libwinpr/synch/event.h | 56 + winpr/libwinpr/synch/init.c | 98 + winpr/libwinpr/synch/mutex.c | 243 + winpr/libwinpr/synch/pollset.c | 223 + winpr/libwinpr/synch/pollset.h | 73 + winpr/libwinpr/synch/semaphore.c | 264 + winpr/libwinpr/synch/sleep.c | 142 + winpr/libwinpr/synch/synch.h | 161 + winpr/libwinpr/synch/test/.gitignore | 3 + winpr/libwinpr/synch/test/CMakeLists.txt | 41 + winpr/libwinpr/synch/test/TestSynchAPC.c | 174 + winpr/libwinpr/synch/test/TestSynchBarrier.c | 232 + winpr/libwinpr/synch/test/TestSynchCritical.c | 360 + winpr/libwinpr/synch/test/TestSynchEvent.c | 94 + winpr/libwinpr/synch/test/TestSynchInit.c | 147 + .../synch/test/TestSynchMultipleThreads.c | 251 + winpr/libwinpr/synch/test/TestSynchMutex.c | 257 + .../libwinpr/synch/test/TestSynchSemaphore.c | 20 + winpr/libwinpr/synch/test/TestSynchThread.c | 115 + .../libwinpr/synch/test/TestSynchTimerQueue.c | 122 + .../synch/test/TestSynchWaitableTimer.c | 83 + .../synch/test/TestSynchWaitableTimerAPC.c | 92 + winpr/libwinpr/synch/timer.c | 1045 ++ winpr/libwinpr/synch/wait.c | 539 + winpr/libwinpr/sysinfo/CMakeLists.txt | 31 + winpr/libwinpr/sysinfo/ModuleOptions.cmake | 9 + .../sysinfo/cpufeatures/CMakeLists.txt | 20 + winpr/libwinpr/sysinfo/cpufeatures/NOTICE | 13 + winpr/libwinpr/sysinfo/cpufeatures/README | 4 + .../sysinfo/cpufeatures/cpu-features.c | 1417 +++ .../sysinfo/cpufeatures/cpu-features.h | 324 + winpr/libwinpr/sysinfo/sysinfo.c | 990 ++ winpr/libwinpr/sysinfo/test/.gitignore | 3 + winpr/libwinpr/sysinfo/test/CMakeLists.txt | 30 + winpr/libwinpr/sysinfo/test/TestCPUFeatures.c | 50 + .../sysinfo/test/TestGetComputerName.c | 362 + .../sysinfo/test/TestGetNativeSystemInfo.c | 26 + winpr/libwinpr/sysinfo/test/TestLocalTime.c | 18 + winpr/libwinpr/sysinfo/test/TestSystemTime.c | 18 + winpr/libwinpr/thread/CMakeLists.txt | 34 + winpr/libwinpr/thread/ModuleOptions.cmake | 9 + winpr/libwinpr/thread/apc.c | 244 + winpr/libwinpr/thread/apc.h | 85 + winpr/libwinpr/thread/argv.c | 283 + winpr/libwinpr/thread/process.c | 538 + winpr/libwinpr/thread/processor.c | 43 + winpr/libwinpr/thread/test/.gitignore | 3 + winpr/libwinpr/thread/test/CMakeLists.txt | 27 + .../thread/test/TestThreadCommandLineToArgv.c | 70 + .../thread/test/TestThreadCreateProcess.c | 154 + .../thread/test/TestThreadExitThread.c | 49 + winpr/libwinpr/thread/thread.c | 1046 ++ winpr/libwinpr/thread/thread.h | 95 + winpr/libwinpr/thread/tls.c | 74 + winpr/libwinpr/timezone/CMakeLists.txt | 18 + winpr/libwinpr/timezone/ModuleOptions.cmake | 9 + winpr/libwinpr/timezone/TimeZones.c | 3900 +++++++ winpr/libwinpr/timezone/TimeZones.h | 36 + winpr/libwinpr/timezone/WindowsZones.c | 530 + winpr/libwinpr/timezone/WindowsZones.h | 19 + winpr/libwinpr/timezone/timezone.c | 536 + winpr/libwinpr/utils/CMakeLists.txt | 184 + winpr/libwinpr/utils/ModuleOptions.cmake | 9 + winpr/libwinpr/utils/cmdline.c | 560 + winpr/libwinpr/utils/collections/ArrayList.c | 488 + winpr/libwinpr/utils/collections/BipBuffer.c | 433 + winpr/libwinpr/utils/collections/BitStream.c | 288 + winpr/libwinpr/utils/collections/BufferPool.c | 522 + .../utils/collections/CountdownEvent.c | 193 + winpr/libwinpr/utils/collections/HashTable.c | 638 ++ winpr/libwinpr/utils/collections/LinkedList.c | 360 + .../utils/collections/ListDictionary.c | 480 + .../libwinpr/utils/collections/MessagePipe.c | 82 + .../libwinpr/utils/collections/MessageQueue.c | 269 + winpr/libwinpr/utils/collections/ObjectPool.c | 162 + winpr/libwinpr/utils/collections/PubSub.c | 238 + winpr/libwinpr/utils/collections/Queue.c | 299 + winpr/libwinpr/utils/collections/Reference.c | 196 + winpr/libwinpr/utils/collections/Stack.c | 234 + winpr/libwinpr/utils/collections/StreamPool.c | 424 + winpr/libwinpr/utils/corkscrew/backtrace.h | 120 + winpr/libwinpr/utils/corkscrew/debug.c | 262 + winpr/libwinpr/utils/corkscrew/debug.h | 40 + winpr/libwinpr/utils/corkscrew/demangle.h | 43 + winpr/libwinpr/utils/corkscrew/map_info.h | 76 + winpr/libwinpr/utils/corkscrew/ptrace.h | 139 + winpr/libwinpr/utils/corkscrew/symbol_table.h | 62 + winpr/libwinpr/utils/debug.c | 211 + winpr/libwinpr/utils/execinfo/debug.c | 88 + winpr/libwinpr/utils/execinfo/debug.h | 40 + winpr/libwinpr/utils/image.c | 494 + winpr/libwinpr/utils/ini.c | 825 ++ winpr/libwinpr/utils/lodepng/lodepng.c | 6659 ++++++++++++ winpr/libwinpr/utils/lodepng/lodepng.h | 1569 +++ winpr/libwinpr/utils/ntlm.c | 184 + winpr/libwinpr/utils/print.c | 206 + winpr/libwinpr/utils/sam.c | 392 + winpr/libwinpr/utils/ssl.c | 403 + winpr/libwinpr/utils/stream.c | 199 + winpr/libwinpr/utils/strlst.c | 82 + winpr/libwinpr/utils/test/.gitignore | 3 + winpr/libwinpr/utils/test/CMakeLists.txt | 48 + winpr/libwinpr/utils/test/TestArrayList.c | 80 + winpr/libwinpr/utils/test/TestBacktrace.c | 31 + winpr/libwinpr/utils/test/TestBipBuffer.c | 23 + winpr/libwinpr/utils/test/TestBitStream.c | 84 + winpr/libwinpr/utils/test/TestBufferPool.c | 65 + winpr/libwinpr/utils/test/TestCmdLine.c | 207 + winpr/libwinpr/utils/test/TestHashTable.c | 282 + winpr/libwinpr/utils/test/TestImage.c | 300 + winpr/libwinpr/utils/test/TestIni.c | 157 + winpr/libwinpr/utils/test/TestLinkedList.c | 121 + .../libwinpr/utils/test/TestListDictionary.c | 175 + winpr/libwinpr/utils/test/TestMessagePipe.c | 104 + winpr/libwinpr/utils/test/TestMessageQueue.c | 55 + winpr/libwinpr/utils/test/TestPrint.c | 109 + winpr/libwinpr/utils/test/TestPubSub.c | 70 + winpr/libwinpr/utils/test/TestQueue.c | 56 + winpr/libwinpr/utils/test/TestStream.c | 683 ++ winpr/libwinpr/utils/test/TestStreamPool.c | 100 + winpr/libwinpr/utils/test/TestVersion.c | 50 + winpr/libwinpr/utils/test/TestWLog.c | 66 + winpr/libwinpr/utils/test/TestWLogCallback.c | 125 + winpr/libwinpr/utils/test/lodepng_32bit.bmp | Bin 0 -> 16522 bytes winpr/libwinpr/utils/test/lodepng_32bit.png | Bin 0 -> 3968 bytes winpr/libwinpr/utils/trio/strio.h | 74 + winpr/libwinpr/utils/trio/trio.c | 7194 +++++++++++++ winpr/libwinpr/utils/trio/trio.h | 263 + winpr/libwinpr/utils/trio/triodef.h | 370 + winpr/libwinpr/utils/trio/trionan.c | 1168 ++ winpr/libwinpr/utils/trio/trionan.h | 164 + winpr/libwinpr/utils/trio/triop.h | 496 + winpr/libwinpr/utils/trio/triostr.c | 2116 ++++ winpr/libwinpr/utils/trio/triostr.h | 570 + winpr/libwinpr/utils/unwind/debug.c | 128 + winpr/libwinpr/utils/unwind/debug.h | 41 + winpr/libwinpr/utils/windows/debug.c | 169 + winpr/libwinpr/utils/windows/debug.h | 40 + winpr/libwinpr/utils/winpr.c | 76 + winpr/libwinpr/utils/wlog/Appender.c | 179 + winpr/libwinpr/utils/wlog/Appender.h | 39 + winpr/libwinpr/utils/wlog/BinaryAppender.c | 238 + winpr/libwinpr/utils/wlog/BinaryAppender.h | 27 + winpr/libwinpr/utils/wlog/CallbackAppender.c | 171 + winpr/libwinpr/utils/wlog/CallbackAppender.h | 27 + winpr/libwinpr/utils/wlog/ConsoleAppender.c | 282 + winpr/libwinpr/utils/wlog/ConsoleAppender.h | 27 + winpr/libwinpr/utils/wlog/DataMessage.c | 50 + winpr/libwinpr/utils/wlog/DataMessage.h | 25 + winpr/libwinpr/utils/wlog/FileAppender.c | 291 + winpr/libwinpr/utils/wlog/FileAppender.h | 27 + winpr/libwinpr/utils/wlog/ImageMessage.c | 39 + winpr/libwinpr/utils/wlog/ImageMessage.h | 25 + winpr/libwinpr/utils/wlog/JournaldAppender.c | 213 + winpr/libwinpr/utils/wlog/JournaldAppender.h | 31 + winpr/libwinpr/utils/wlog/Layout.c | 414 + winpr/libwinpr/utils/wlog/Layout.h | 41 + winpr/libwinpr/utils/wlog/Message.c | 66 + winpr/libwinpr/utils/wlog/Message.h | 29 + winpr/libwinpr/utils/wlog/PacketMessage.c | 484 + winpr/libwinpr/utils/wlog/PacketMessage.h | 119 + winpr/libwinpr/utils/wlog/SyslogAppender.c | 140 + winpr/libwinpr/utils/wlog/SyslogAppender.h | 31 + winpr/libwinpr/utils/wlog/UdpAppender.c | 225 + winpr/libwinpr/utils/wlog/UdpAppender.h | 33 + winpr/libwinpr/utils/wlog/wlog.c | 1023 ++ winpr/libwinpr/utils/wlog/wlog.h | 88 + winpr/libwinpr/winhttp/CMakeLists.txt | 18 + winpr/libwinpr/winhttp/ModuleOptions.cmake | 9 + winpr/libwinpr/winhttp/winhttp.c | 239 + winpr/libwinpr/winsock/CMakeLists.txt | 22 + winpr/libwinpr/winsock/ModuleOptions.cmake | 8 + winpr/libwinpr/winsock/winsock.c | 1285 +++ winpr/libwinpr/wnd/CMakeLists.txt | 23 + winpr/libwinpr/wnd/ModuleOptions.cmake | 9 + winpr/libwinpr/wnd/test/.gitignore | 3 + winpr/libwinpr/wnd/test/CMakeLists.txt | 27 + .../libwinpr/wnd/test/TestWndCreateWindowEx.c | 94 + winpr/libwinpr/wnd/test/TestWndWmCopyData.c | 83 + winpr/libwinpr/wnd/wnd.c | 449 + winpr/libwinpr/wnd/wnd.h | 41 + winpr/libwinpr/wtsapi/CMakeLists.txt | 26 + winpr/libwinpr/wtsapi/ModuleOptions.cmake | 9 + winpr/libwinpr/wtsapi/test/.gitignore | 4 + winpr/libwinpr/wtsapi/test/CMakeLists.txt | 74 + .../test/TestWtsApiEnumerateProcesses.c | 47 + .../wtsapi/test/TestWtsApiEnumerateSessions.c | 48 + .../test/TestWtsApiExtraDisconnectSession.c | 22 + .../TestWtsApiExtraDynamicVirtualChannel.c | 53 + .../test/TestWtsApiExtraLogoffSession.c | 22 + .../wtsapi/test/TestWtsApiExtraSendMessage.c | 30 + .../TestWtsApiExtraStartRemoteSessionEx.c | 31 + .../test/TestWtsApiExtraVirtualChannel.c | 53 + .../test/TestWtsApiQuerySessionInformation.c | 223 + .../test/TestWtsApiSessionNotification.c | 60 + .../wtsapi/test/TestWtsApiShutdownSystem.c | 32 + .../wtsapi/test/TestWtsApiWaitSystemEvent.c | 36 + winpr/libwinpr/wtsapi/wtsapi.c | 794 ++ winpr/libwinpr/wtsapi/wtsapi_win32.c | 815 ++ winpr/libwinpr/wtsapi/wtsapi_win32.h | 27 + winpr/test/.gitignore | 1 + winpr/test/CMakeLists.txt | 24 + winpr/test/TestIntrinsics.c | 117 + winpr/test/TestTypes.c | 118 + winpr/tools/.gitignore | 4 + winpr/tools/CMakeLists.txt | 127 + winpr/tools/hash-cli/CMakeLists.txt | 54 + winpr/tools/hash-cli/hash.c | 210 + winpr/tools/hash-cli/winpr-hash.1.in | 42 + winpr/tools/makecert-cli/CMakeLists.txt | 55 + winpr/tools/makecert-cli/main.c | 45 + winpr/tools/makecert-cli/winpr-makecert.1.in | 116 + winpr/tools/makecert/.gitignore | 2 + winpr/tools/makecert/CMakeLists.txt | 49 + winpr/tools/makecert/makecert.c | 1107 ++ winpr/tools/winpr-tools.pc.in | 15 + winpr/winpr.pc.in | 15 + winpr/wlog.7 | 149 + 2412 files changed, 569827 insertions(+) create mode 100644 .clang-format create mode 100644 .github/ISSUE_TEMPLATE.md create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/workflows/codeql-analysis.yml create mode 100644 .gitignore create mode 100644 .source_version create mode 100644 .travis.yml create mode 100644 CMakeCPack.cmake create mode 100644 CMakeCPackOptions.cmake.in create mode 100644 CMakeLists.txt create mode 100644 ChangeLog create mode 100644 LICENSE create mode 100644 README.md create mode 100644 buildflags.h.in create mode 100644 channels/CMakeLists.txt create mode 100644 channels/ainput/CMakeLists.txt create mode 100644 channels/ainput/ChannelOptions.cmake create mode 100644 channels/ainput/client/CMakeLists.txt create mode 100644 channels/ainput/client/ainput_main.c create mode 100644 channels/ainput/client/ainput_main.h create mode 100644 channels/ainput/common/ainput_common.h create mode 100644 channels/ainput/server/CMakeLists.txt create mode 100644 channels/ainput/server/ainput_main.c create mode 100644 channels/audin/CMakeLists.txt create mode 100644 channels/audin/ChannelOptions.cmake create mode 100644 channels/audin/client/CMakeLists.txt create mode 100644 channels/audin/client/alsa/CMakeLists.txt create mode 100644 channels/audin/client/alsa/audin_alsa.c create mode 100644 channels/audin/client/audin_main.c create mode 100644 channels/audin/client/audin_main.h create mode 100644 channels/audin/client/mac/CMakeLists.txt create mode 100644 channels/audin/client/mac/audin_mac.m create mode 100644 channels/audin/client/opensles/CMakeLists.txt create mode 100644 channels/audin/client/opensles/audin_opensl_es.c create mode 100644 channels/audin/client/opensles/opensl_io.c create mode 100644 channels/audin/client/opensles/opensl_io.h create mode 100644 channels/audin/client/oss/CMakeLists.txt create mode 100644 channels/audin/client/oss/audin_oss.c create mode 100644 channels/audin/client/pulse/CMakeLists.txt create mode 100644 channels/audin/client/pulse/audin_pulse.c create mode 100644 channels/audin/client/winmm/CMakeLists.txt create mode 100644 channels/audin/client/winmm/audin_winmm.c create mode 100644 channels/audin/server/CMakeLists.txt create mode 100644 channels/audin/server/audin.c create mode 100644 channels/client/.gitignore create mode 100644 channels/client/CMakeLists.txt create mode 100644 channels/client/addin.c create mode 100644 channels/client/addin.h create mode 100644 channels/client/tables.c.in create mode 100644 channels/client/tables.h create mode 100644 channels/cliprdr/CMakeLists.txt create mode 100644 channels/cliprdr/ChannelOptions.cmake create mode 100644 channels/cliprdr/client/CMakeLists.txt create mode 100644 channels/cliprdr/client/cliprdr_format.c create mode 100644 channels/cliprdr/client/cliprdr_format.h create mode 100644 channels/cliprdr/client/cliprdr_main.c create mode 100644 channels/cliprdr/client/cliprdr_main.h create mode 100644 channels/cliprdr/cliprdr_common.c create mode 100644 channels/cliprdr/cliprdr_common.h create mode 100644 channels/cliprdr/server/CMakeLists.txt create mode 100644 channels/cliprdr/server/cliprdr_main.c create mode 100644 channels/cliprdr/server/cliprdr_main.h create mode 100644 channels/disp/CMakeLists.txt create mode 100644 channels/disp/ChannelOptions.cmake create mode 100644 channels/disp/client/CMakeLists.txt create mode 100644 channels/disp/client/disp_main.c create mode 100644 channels/disp/client/disp_main.h create mode 100644 channels/disp/disp_common.c create mode 100644 channels/disp/disp_common.h create mode 100644 channels/disp/server/CMakeLists.txt create mode 100644 channels/disp/server/disp_main.c create mode 100644 channels/disp/server/disp_main.h create mode 100644 channels/drdynvc/CMakeLists.txt create mode 100644 channels/drdynvc/ChannelOptions.cmake create mode 100644 channels/drdynvc/client/CMakeLists.txt create mode 100644 channels/drdynvc/client/drdynvc_main.c create mode 100644 channels/drdynvc/client/drdynvc_main.h create mode 100644 channels/drdynvc/server/CMakeLists.txt create mode 100644 channels/drdynvc/server/drdynvc_main.c create mode 100644 channels/drdynvc/server/drdynvc_main.h create mode 100644 channels/drive/CMakeLists.txt create mode 100644 channels/drive/ChannelOptions.cmake create mode 100644 channels/drive/client/CMakeLists.txt create mode 100644 channels/drive/client/drive_file.c create mode 100644 channels/drive/client/drive_file.h create mode 100644 channels/drive/client/drive_main.c create mode 100644 channels/echo/CMakeLists.txt create mode 100644 channels/echo/ChannelOptions.cmake create mode 100644 channels/echo/client/CMakeLists.txt create mode 100644 channels/echo/client/echo_main.c create mode 100644 channels/echo/client/echo_main.h create mode 100644 channels/echo/server/CMakeLists.txt create mode 100644 channels/echo/server/echo_main.c create mode 100644 channels/encomsp/CMakeLists.txt create mode 100644 channels/encomsp/ChannelOptions.cmake create mode 100644 channels/encomsp/client/CMakeLists.txt create mode 100644 channels/encomsp/client/encomsp_main.c create mode 100644 channels/encomsp/client/encomsp_main.h create mode 100644 channels/encomsp/server/CMakeLists.txt create mode 100644 channels/encomsp/server/encomsp_main.c create mode 100644 channels/encomsp/server/encomsp_main.h create mode 100644 channels/geometry/CMakeLists.txt create mode 100644 channels/geometry/ChannelOptions.cmake create mode 100644 channels/geometry/client/CMakeLists.txt create mode 100644 channels/geometry/client/geometry_main.c create mode 100644 channels/geometry/client/geometry_main.h create mode 100644 channels/parallel/CMakeLists.txt create mode 100644 channels/parallel/ChannelOptions.cmake create mode 100644 channels/parallel/client/CMakeLists.txt create mode 100644 channels/parallel/client/parallel_main.c create mode 100644 channels/printer/CMakeLists.txt create mode 100644 channels/printer/ChannelOptions.cmake create mode 100644 channels/printer/client/CMakeLists.txt create mode 100644 channels/printer/client/cups/CMakeLists.txt create mode 100644 channels/printer/client/cups/printer_cups.c create mode 100644 channels/printer/client/printer_main.c create mode 100644 channels/printer/client/win/CMakeLists.txt create mode 100644 channels/printer/client/win/printer_win.c create mode 100644 channels/printer/printer.h create mode 100644 channels/rail/CMakeLists.txt create mode 100644 channels/rail/ChannelOptions.cmake create mode 100644 channels/rail/client/CMakeLists.txt create mode 100644 channels/rail/client/rail_main.c create mode 100644 channels/rail/client/rail_main.h create mode 100644 channels/rail/client/rail_orders.c create mode 100644 channels/rail/client/rail_orders.h create mode 100644 channels/rail/rail_common.c create mode 100644 channels/rail/rail_common.h create mode 100644 channels/rail/server/CMakeLists.txt create mode 100644 channels/rail/server/rail_main.c create mode 100644 channels/rail/server/rail_main.h create mode 100644 channels/rdp2tcp/CMakeLists.txt create mode 100644 channels/rdp2tcp/ChannelOptions.cmake create mode 100644 channels/rdp2tcp/client/CMakeLists.txt create mode 100644 channels/rdp2tcp/client/rdp2tcp_main.c create mode 100644 channels/rdpdr/CMakeLists.txt create mode 100644 channels/rdpdr/ChannelOptions.cmake create mode 100644 channels/rdpdr/client/CMakeLists.txt create mode 100644 channels/rdpdr/client/devman.c create mode 100644 channels/rdpdr/client/devman.h create mode 100644 channels/rdpdr/client/irp.c create mode 100644 channels/rdpdr/client/irp.h create mode 100644 channels/rdpdr/client/rdpdr_capabilities.c create mode 100644 channels/rdpdr/client/rdpdr_capabilities.h create mode 100644 channels/rdpdr/client/rdpdr_main.c create mode 100644 channels/rdpdr/client/rdpdr_main.h create mode 100644 channels/rdpdr/server/CMakeLists.txt create mode 100644 channels/rdpdr/server/rdpdr_main.c create mode 100644 channels/rdpdr/server/rdpdr_main.h create mode 100644 channels/rdpecam/CMakeLists.txt create mode 100644 channels/rdpecam/ChannelOptions.cmake create mode 100644 channels/rdpecam/server/CMakeLists.txt create mode 100644 channels/rdpecam/server/camera_device_enumerator_main.c create mode 100644 channels/rdpecam/server/camera_device_main.c create mode 100644 channels/rdpei/CMakeLists.txt create mode 100644 channels/rdpei/ChannelOptions.cmake create mode 100644 channels/rdpei/client/CMakeLists.txt create mode 100644 channels/rdpei/client/rdpei_main.c create mode 100644 channels/rdpei/client/rdpei_main.h create mode 100644 channels/rdpei/rdpei_common.c create mode 100644 channels/rdpei/rdpei_common.h create mode 100644 channels/rdpei/server/CMakeLists.txt create mode 100644 channels/rdpei/server/rdpei_main.c create mode 100644 channels/rdpei/server/rdpei_main.h create mode 100644 channels/rdpgfx/CMakeLists.txt create mode 100644 channels/rdpgfx/ChannelOptions.cmake create mode 100644 channels/rdpgfx/client/CMakeLists.txt create mode 100644 channels/rdpgfx/client/rdpgfx_codec.c create mode 100644 channels/rdpgfx/client/rdpgfx_codec.h create mode 100644 channels/rdpgfx/client/rdpgfx_main.c create mode 100644 channels/rdpgfx/client/rdpgfx_main.h create mode 100644 channels/rdpgfx/rdpgfx_common.c create mode 100644 channels/rdpgfx/rdpgfx_common.h create mode 100644 channels/rdpgfx/server/CMakeLists.txt create mode 100644 channels/rdpgfx/server/rdpgfx_main.c create mode 100644 channels/rdpgfx/server/rdpgfx_main.h create mode 100644 channels/rdpsnd/CMakeLists.txt create mode 100644 channels/rdpsnd/ChannelOptions.cmake create mode 100644 channels/rdpsnd/client/CMakeLists.txt create mode 100644 channels/rdpsnd/client/alsa/CMakeLists.txt create mode 100644 channels/rdpsnd/client/alsa/rdpsnd_alsa.c create mode 100644 channels/rdpsnd/client/fake/CMakeLists.txt create mode 100644 channels/rdpsnd/client/fake/rdpsnd_fake.c create mode 100644 channels/rdpsnd/client/ios/CMakeLists.txt create mode 100644 channels/rdpsnd/client/ios/TPCircularBuffer.c create mode 100644 channels/rdpsnd/client/ios/TPCircularBuffer.h create mode 100644 channels/rdpsnd/client/ios/rdpsnd_ios.c create mode 100644 channels/rdpsnd/client/mac/CMakeLists.txt create mode 100644 channels/rdpsnd/client/mac/rdpsnd_mac.m create mode 100644 channels/rdpsnd/client/opensles/CMakeLists.txt create mode 100644 channels/rdpsnd/client/opensles/opensl_io.c create mode 100644 channels/rdpsnd/client/opensles/opensl_io.h create mode 100644 channels/rdpsnd/client/opensles/rdpsnd_opensles.c create mode 100644 channels/rdpsnd/client/oss/CMakeLists.txt create mode 100644 channels/rdpsnd/client/oss/rdpsnd_oss.c create mode 100644 channels/rdpsnd/client/proxy/CMakeLists.txt create mode 100644 channels/rdpsnd/client/proxy/rdpsnd_proxy.c create mode 100644 channels/rdpsnd/client/pulse/CMakeLists.txt create mode 100644 channels/rdpsnd/client/pulse/rdpsnd_pulse.c create mode 100644 channels/rdpsnd/client/rdpsnd_main.c create mode 100644 channels/rdpsnd/client/rdpsnd_main.h create mode 100644 channels/rdpsnd/client/winmm/CMakeLists.txt create mode 100644 channels/rdpsnd/client/winmm/rdpsnd_winmm.c create mode 100644 channels/rdpsnd/common/CMakeLists.txt create mode 100644 channels/rdpsnd/common/rdpsnd_common.h create mode 100644 channels/rdpsnd/server/CMakeLists.txt create mode 100644 channels/rdpsnd/server/rdpsnd_main.c create mode 100644 channels/rdpsnd/server/rdpsnd_main.h create mode 100644 channels/remdesk/CMakeLists.txt create mode 100644 channels/remdesk/ChannelOptions.cmake create mode 100644 channels/remdesk/client/CMakeLists.txt create mode 100644 channels/remdesk/client/remdesk_main.c create mode 100644 channels/remdesk/client/remdesk_main.h create mode 100644 channels/remdesk/server/CMakeLists.txt create mode 100644 channels/remdesk/server/remdesk_main.c create mode 100644 channels/remdesk/server/remdesk_main.h create mode 100644 channels/serial/CMakeLists.txt create mode 100644 channels/serial/ChannelOptions.cmake create mode 100644 channels/serial/client/CMakeLists.txt create mode 100644 channels/serial/client/serial_main.c create mode 100644 channels/server/CMakeLists.txt create mode 100644 channels/server/channels.c create mode 100644 channels/server/channels.h create mode 100644 channels/smartcard/CMakeLists.txt create mode 100644 channels/smartcard/ChannelOptions.cmake create mode 100644 channels/smartcard/client/CMakeLists.txt create mode 100644 channels/smartcard/client/smartcard_main.c create mode 100644 channels/smartcard/client/smartcard_main.h create mode 100644 channels/smartcard/client/smartcard_operations.c create mode 100644 channels/smartcard/client/smartcard_operations.h create mode 100644 channels/smartcard/client/smartcard_pack.c create mode 100644 channels/smartcard/client/smartcard_pack.h create mode 100644 channels/sshagent/CMakeLists.txt create mode 100644 channels/sshagent/ChannelOptions.cmake create mode 100644 channels/sshagent/client/CMakeLists.txt create mode 100644 channels/sshagent/client/sshagent_main.c create mode 100644 channels/sshagent/client/sshagent_main.h create mode 100644 channels/telemetry/CMakeLists.txt create mode 100644 channels/telemetry/ChannelOptions.cmake create mode 100644 channels/telemetry/server/CMakeLists.txt create mode 100644 channels/telemetry/server/telemetry_main.c create mode 100644 channels/tsmf/CMakeLists.txt create mode 100644 channels/tsmf/ChannelOptions.cmake create mode 100644 channels/tsmf/client/CMakeLists.txt create mode 100644 channels/tsmf/client/alsa/CMakeLists.txt create mode 100644 channels/tsmf/client/alsa/tsmf_alsa.c create mode 100644 channels/tsmf/client/ffmpeg/CMakeLists.txt create mode 100644 channels/tsmf/client/ffmpeg/tsmf_ffmpeg.c create mode 100644 channels/tsmf/client/gstreamer/CMakeLists.txt create mode 100644 channels/tsmf/client/gstreamer/tsmf_X11.c create mode 100644 channels/tsmf/client/gstreamer/tsmf_gstreamer.c create mode 100644 channels/tsmf/client/gstreamer/tsmf_platform.h create mode 100644 channels/tsmf/client/oss/CMakeLists.txt create mode 100644 channels/tsmf/client/oss/tsmf_oss.c create mode 100644 channels/tsmf/client/pulse/CMakeLists.txt create mode 100644 channels/tsmf/client/pulse/tsmf_pulse.c create mode 100644 channels/tsmf/client/tsmf_audio.c create mode 100644 channels/tsmf/client/tsmf_audio.h create mode 100644 channels/tsmf/client/tsmf_codec.c create mode 100644 channels/tsmf/client/tsmf_codec.h create mode 100644 channels/tsmf/client/tsmf_constants.h create mode 100644 channels/tsmf/client/tsmf_decoder.c create mode 100644 channels/tsmf/client/tsmf_decoder.h create mode 100644 channels/tsmf/client/tsmf_ifman.c create mode 100644 channels/tsmf/client/tsmf_ifman.h create mode 100644 channels/tsmf/client/tsmf_main.c create mode 100644 channels/tsmf/client/tsmf_main.h create mode 100644 channels/tsmf/client/tsmf_media.c create mode 100644 channels/tsmf/client/tsmf_media.h create mode 100644 channels/tsmf/client/tsmf_types.h create mode 100644 channels/urbdrc/CMakeLists.txt create mode 100644 channels/urbdrc/ChannelOptions.cmake create mode 100644 channels/urbdrc/client/CMakeLists.txt create mode 100644 channels/urbdrc/client/data_transfer.c create mode 100644 channels/urbdrc/client/data_transfer.h create mode 100644 channels/urbdrc/client/libusb/CMakeLists.txt create mode 100644 channels/urbdrc/client/libusb/libusb_udevice.c create mode 100644 channels/urbdrc/client/libusb/libusb_udevice.h create mode 100644 channels/urbdrc/client/libusb/libusb_udevman.c create mode 100644 channels/urbdrc/client/urbdrc_main.c create mode 100644 channels/urbdrc/client/urbdrc_main.h create mode 100644 channels/urbdrc/common/CMakeLists.txt create mode 100644 channels/urbdrc/common/msusb.c create mode 100644 channels/urbdrc/common/msusb.h create mode 100644 channels/urbdrc/common/urbdrc_helpers.c create mode 100644 channels/urbdrc/common/urbdrc_helpers.h create mode 100644 channels/urbdrc/common/urbdrc_types.h create mode 100644 channels/video/CMakeLists.txt create mode 100644 channels/video/ChannelOptions.cmake create mode 100644 channels/video/client/CMakeLists.txt create mode 100644 channels/video/client/video_main.c create mode 100644 channels/video/client/video_main.h create mode 100644 ci/cmake-preloads/config-android.txt create mode 100644 ci/cmake-preloads/config-debian-squeeze.txt create mode 100644 ci/cmake-preloads/config-ios.txt create mode 100644 ci/cmake-preloads/config-linux-all.txt create mode 100644 ci/cmake-preloads/config-macosx.txt create mode 100644 ci/cmake-preloads/config-ubuntu-1204.txt create mode 100644 ci/cmake-preloads/config-windows.txt create mode 100644 client/.gitignore create mode 100644 client/Android/BuildFlags.java.in create mode 100644 client/Android/CMakeLists.txt create mode 100644 client/Android/Studio/.gitignore create mode 100644 client/Android/Studio/aFreeRDP/build.gradle create mode 100644 client/Android/Studio/aFreeRDP/lint.xml create mode 100644 client/Android/Studio/aFreeRDP/src/main/AndroidManifest.xml create mode 100644 client/Android/Studio/aFreeRDP/src/main/assets/FreeRDP_Logo.png create mode 100644 client/Android/Studio/aFreeRDP/src/main/assets/about.css create mode 100644 client/Android/Studio/aFreeRDP/src/main/assets/about_page/about.html create mode 100644 client/Android/Studio/aFreeRDP/src/main/assets/about_page/about_phone.html create mode 100644 client/Android/Studio/aFreeRDP/src/main/assets/background.jpg create mode 100644 client/Android/Studio/aFreeRDP/src/main/assets/de_about_page/about.html create mode 100644 client/Android/Studio/aFreeRDP/src/main/assets/de_about_page/about_phone.html create mode 100644 client/Android/Studio/aFreeRDP/src/main/assets/de_help_page/gestures.html create mode 100644 client/Android/Studio/aFreeRDP/src/main/assets/de_help_page/gestures.png create mode 100644 client/Android/Studio/aFreeRDP/src/main/assets/de_help_page/gestures_phone.html create mode 100644 client/Android/Studio/aFreeRDP/src/main/assets/de_help_page/gestures_phone.png create mode 100644 client/Android/Studio/aFreeRDP/src/main/assets/de_help_page/nav_gestures.png create mode 100644 client/Android/Studio/aFreeRDP/src/main/assets/de_help_page/nav_toolbar.png create mode 100644 client/Android/Studio/aFreeRDP/src/main/assets/de_help_page/nav_touch_pointer.png create mode 100644 client/Android/Studio/aFreeRDP/src/main/assets/de_help_page/toolbar.html create mode 100644 client/Android/Studio/aFreeRDP/src/main/assets/de_help_page/toolbar.png create mode 100644 client/Android/Studio/aFreeRDP/src/main/assets/de_help_page/toolbar_phone.html create mode 100644 client/Android/Studio/aFreeRDP/src/main/assets/de_help_page/toolbar_phone.png create mode 100644 client/Android/Studio/aFreeRDP/src/main/assets/de_help_page/touch_pointer.html create mode 100644 client/Android/Studio/aFreeRDP/src/main/assets/de_help_page/touch_pointer.png create mode 100644 client/Android/Studio/aFreeRDP/src/main/assets/de_help_page/touch_pointer_phone.html create mode 100644 client/Android/Studio/aFreeRDP/src/main/assets/de_help_page/touch_pointer_phone.png create mode 100644 client/Android/Studio/aFreeRDP/src/main/assets/help.css create mode 100644 client/Android/Studio/aFreeRDP/src/main/assets/help_page/gestures.html create mode 100644 client/Android/Studio/aFreeRDP/src/main/assets/help_page/gestures.png create mode 100644 client/Android/Studio/aFreeRDP/src/main/assets/help_page/gestures_phone.html create mode 100644 client/Android/Studio/aFreeRDP/src/main/assets/help_page/gestures_phone.png create mode 100644 client/Android/Studio/aFreeRDP/src/main/assets/help_page/nav_gestures.png create mode 100644 client/Android/Studio/aFreeRDP/src/main/assets/help_page/nav_toolbar.png create mode 100644 client/Android/Studio/aFreeRDP/src/main/assets/help_page/nav_touch_pointer.png create mode 100644 client/Android/Studio/aFreeRDP/src/main/assets/help_page/toolbar.html create mode 100644 client/Android/Studio/aFreeRDP/src/main/assets/help_page/toolbar.png create mode 100644 client/Android/Studio/aFreeRDP/src/main/assets/help_page/toolbar_phone.html create mode 100644 client/Android/Studio/aFreeRDP/src/main/assets/help_page/toolbar_phone.png create mode 100644 client/Android/Studio/aFreeRDP/src/main/assets/help_page/touch_pointer.html create mode 100644 client/Android/Studio/aFreeRDP/src/main/assets/help_page/touch_pointer.png create mode 100644 client/Android/Studio/aFreeRDP/src/main/assets/help_page/touch_pointer_phone.html create mode 100644 client/Android/Studio/aFreeRDP/src/main/assets/help_page/touch_pointer_phone.png create mode 100644 client/Android/Studio/aFreeRDP/src/main/java/com/freerdp/afreerdp/application/GlobalApp.java create mode 100644 client/Android/Studio/aFreeRDP/src/main/res/drawable-hdpi/icon_launcher_freerdp.png create mode 100644 client/Android/Studio/aFreeRDP/src/main/res/drawable-ldpi/icon_launcher_freerdp.png create mode 100644 client/Android/Studio/aFreeRDP/src/main/res/drawable-mdpi/icon_launcher_freerdp.png create mode 100644 client/Android/Studio/aFreeRDP/src/main/res/drawable/button_background.xml create mode 100644 client/Android/Studio/aFreeRDP/src/main/res/drawable/icon_launcher_freerdp.png create mode 100644 client/Android/Studio/aFreeRDP/src/main/res/drawable/separator_background.xml create mode 100644 client/Android/Studio/aFreeRDP/src/main/res/values-de/strings.xml create mode 100644 client/Android/Studio/aFreeRDP/src/main/res/values-es/strings.xml create mode 100644 client/Android/Studio/aFreeRDP/src/main/res/values-fr/strings.xml create mode 100644 client/Android/Studio/aFreeRDP/src/main/res/values-nl/strings.xml create mode 100644 client/Android/Studio/aFreeRDP/src/main/res/values-zh/strings.xml create mode 100644 client/Android/Studio/aFreeRDP/src/main/res/values/strings.xml create mode 100644 client/Android/Studio/aFreeRDP/src/main/res/xml/searchable.xml create mode 100644 client/Android/Studio/build.gradle create mode 100644 client/Android/Studio/freeRDPCore/build.gradle create mode 100644 client/Android/Studio/freeRDPCore/lint.xml create mode 100644 client/Android/Studio/freeRDPCore/src/main/AndroidManifest.xml create mode 100644 client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/application/GlobalApp.java create mode 100644 client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/application/NetworkStateReceiver.java create mode 100644 client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/application/ScreenReceiver.java create mode 100644 client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/application/SessionState.java create mode 100644 client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/domain/BookmarkBase.java create mode 100644 client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/domain/ConnectionReference.java create mode 100644 client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/domain/ManualBookmark.java create mode 100644 client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/domain/PlaceholderBookmark.java create mode 100644 client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/domain/QuickConnectBookmark.java create mode 100644 client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/presentation/AboutActivity.java create mode 100644 client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/presentation/ApplicationSettingsActivity.java create mode 100644 client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/presentation/BookmarkActivity.java create mode 100644 client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/presentation/HelpActivity.java create mode 100644 client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/presentation/HomeActivity.java create mode 100644 client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/presentation/ScrollView2D.java create mode 100644 client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/presentation/SessionActivity.java create mode 100644 client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/presentation/SessionView.java create mode 100644 client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/presentation/ShortcutsActivity.java create mode 100644 client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/presentation/TouchPointerView.java create mode 100644 client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/services/BookmarkBaseGateway.java create mode 100644 client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/services/BookmarkDB.java create mode 100644 client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/services/FreeRDPSuggestionProvider.java create mode 100644 client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/services/HistoryDB.java create mode 100644 client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/services/LibFreeRDP.java create mode 100644 client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/services/ManualBookmarkGateway.java create mode 100644 client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/services/QuickConnectHistoryGateway.java create mode 100644 client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/services/SessionRequestHandlerActivity.java create mode 100644 client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/utils/AppCompatPreferenceActivity.java create mode 100644 client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/utils/BookmarkArrayAdapter.java create mode 100644 client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/utils/ButtonPreference.java create mode 100644 client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/utils/ClipboardManagerProxy.java create mode 100644 client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/utils/DoubleGestureDetector.java create mode 100644 client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/utils/GestureDetector.java create mode 100644 client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/utils/IntEditTextPreference.java create mode 100644 client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/utils/IntListPreference.java create mode 100644 client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/utils/KeyboardMapper.java create mode 100644 client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/utils/Mouse.java create mode 100644 client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/utils/RDPFileParser.java create mode 100644 client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/utils/SeparatedListAdapter.java create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/drawable-hdpi/icon_button_add.png create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/drawable-hdpi/icon_edittext_clear.png create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/drawable-hdpi/icon_edittext_search.png create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/drawable-hdpi/icon_launcher_freerdp.png create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/drawable-hdpi/icon_menu_about.png create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/drawable-hdpi/icon_menu_add.png create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/drawable-hdpi/icon_menu_close.png create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/drawable-hdpi/icon_menu_disconnect.png create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/drawable-hdpi/icon_menu_ext_keyboard.png create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/drawable-hdpi/icon_menu_help.png create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/drawable-hdpi/icon_menu_preferences.png create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/drawable-hdpi/icon_menu_settings.png create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/drawable-hdpi/icon_menu_sys_keyboard.png create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/drawable-hdpi/icon_menu_touch_pointer.png create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/drawable-hdpi/icon_star_off.png create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/drawable-hdpi/icon_star_on.png create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/drawable-hdpi/search_plate.9.png create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/drawable-hdpi/sym_keyboard_delete.png create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/drawable-hdpi/sym_keyboard_feedback_delete.png create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/drawable-hdpi/sym_keyboard_feedback_return.png create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/drawable-hdpi/sym_keyboard_return.png create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/drawable-ldpi/icon_button_add.png create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/drawable-ldpi/icon_edittext_search.png create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/drawable-ldpi/icon_launcher_freerdp.png create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/drawable-ldpi/icon_menu_about.png create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/drawable-ldpi/icon_menu_add.png create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/drawable-ldpi/icon_menu_disconnect.png create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/drawable-ldpi/icon_menu_exit.png create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/drawable-ldpi/icon_menu_ext_keyboard.png create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/drawable-ldpi/icon_menu_help.png create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/drawable-ldpi/icon_menu_preferences.png create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/drawable-ldpi/icon_menu_settings.png create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/drawable-ldpi/icon_menu_sys_keyboard.png create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/drawable-ldpi/icon_menu_touch_pointer.png create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/drawable-ldpi/icon_star_off.png create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/drawable-ldpi/icon_star_on.png create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/drawable-ldpi/search_plate.9.png create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/drawable-ldpi/sym_keyboard_delete.png create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/drawable-ldpi/sym_keyboard_feedback_delete.png create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/drawable-ldpi/sym_keyboard_feedback_return.png create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/drawable-ldpi/sym_keyboard_return.png create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/drawable-mdpi/icon_button_add.png create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/drawable-mdpi/icon_edittext_clear.png create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/drawable-mdpi/icon_edittext_search.png create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/drawable-mdpi/icon_launcher_freerdp.png create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/drawable-mdpi/icon_menu_about.png create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/drawable-mdpi/icon_menu_add.png create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/drawable-mdpi/icon_menu_disconnect.png create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/drawable-mdpi/icon_menu_exit.png create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/drawable-mdpi/icon_menu_ext_keyboard.png create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/drawable-mdpi/icon_menu_help.png create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/drawable-mdpi/icon_menu_preferences.png create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/drawable-mdpi/icon_menu_settings.png create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/drawable-mdpi/icon_menu_sys_keyboard.png create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/drawable-mdpi/icon_menu_touch_pointer.png create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/drawable-mdpi/icon_star_off.png create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/drawable-mdpi/icon_star_on.png create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/drawable-mdpi/search_plate.9.png create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/drawable-mdpi/sym_keyboard_delete.png create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/drawable-mdpi/sym_keyboard_feedback_delete.png create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/drawable-mdpi/sym_keyboard_feedback_return.png create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/drawable-mdpi/sym_keyboard_return.png create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/drawable/button_background.xml create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/drawable/icon_button_cancel.png create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/drawable/icon_launcher_freerdp.png create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/drawable/separator_background.xml create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/drawable/sym_keyboard_arrows.png create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/drawable/sym_keyboard_arrows_black.png create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/drawable/sym_keyboard_down_arrow.png create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/drawable/sym_keyboard_down_arrow_black.png create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/drawable/sym_keyboard_left_arrow.png create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/drawable/sym_keyboard_left_arrow_black.png create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/drawable/sym_keyboard_menu.png create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/drawable/sym_keyboard_menu_black.png create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/drawable/sym_keyboard_right_arrow.png create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/drawable/sym_keyboard_right_arrow_black.png create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/drawable/sym_keyboard_up_arrow.png create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/drawable/sym_keyboard_up_arrow_black.png create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/drawable/sym_keyboard_winkey.png create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/drawable/sym_keyboard_winkey_black.png create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/drawable/touch_pointer_active.png create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/drawable/touch_pointer_default.png create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/drawable/touch_pointer_extkeyboard.png create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/drawable/touch_pointer_keyboard.png create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/drawable/touch_pointer_lclick.png create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/drawable/touch_pointer_rclick.png create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/drawable/touch_pointer_reset.png create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/drawable/touch_pointer_scroll.png create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/layout/activity_about.xml create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/layout/bookmark_list_item.xml create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/layout/button_preference.xml create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/layout/credentials.xml create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/layout/dont_show_again_dialog.xml create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/layout/home.xml create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/layout/list_header.xml create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/layout/session.xml create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/layout/session_list_item.xml create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/layout/super_bar.xml create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/menu/bookmark_context_menu.xml create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/menu/home_menu.xml create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/menu/session_menu.xml create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/values-de/strings.xml create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/values-es/strings.xml create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/values-fr/strings.xml create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/values-ja/strings.xml create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/values-ko/strings.xml create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/values-land/dimens.xml create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/values-nb-rNO/strings.xml create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/values-nl/strings.xml create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/values-w820dp/dimens.xml create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/values-zh/strings.xml create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/values/attrs.xml create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/values/dimens.xml create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/values/integers.xml create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/values/strings.xml create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/values/theme.xml create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/xml/advanced_settings.xml create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/xml/bookmark_settings.xml create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/xml/credentials_settings.xml create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/xml/cursor_keyboard.xml create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/xml/debug_settings.xml create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/xml/gateway_settings.xml create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/xml/modifiers_keyboard.xml create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/xml/numpad_keyboard.xml create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/xml/performance_flags.xml create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/xml/performance_flags_3g.xml create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/xml/screen_settings.xml create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/xml/screen_settings_3g.xml create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/xml/settings_app_client.xml create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/xml/settings_app_headers.xml create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/xml/settings_app_power.xml create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/xml/settings_app_security.xml create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/xml/settings_app_ui.xml create mode 100644 client/Android/Studio/freeRDPCore/src/main/res/xml/specialkeys_keyboard.xml create mode 100644 client/Android/Studio/gradle.properties create mode 100644 client/Android/Studio/gradle/wrapper/gradle-wrapper.jar create mode 100644 client/Android/Studio/gradle/wrapper/gradle-wrapper.properties create mode 100755 client/Android/Studio/gradlew create mode 100644 client/Android/Studio/gradlew.bat create mode 100644 client/Android/Studio/settings.gradle create mode 100644 client/Android/android_cliprdr.c create mode 100644 client/Android/android_cliprdr.h create mode 100644 client/Android/android_event.c create mode 100644 client/Android/android_event.h create mode 100644 client/Android/android_freerdp.c create mode 100644 client/Android/android_freerdp.h create mode 100644 client/Android/android_freerdp_jni.h create mode 100644 client/Android/android_jni_callback.c create mode 100644 client/Android/android_jni_callback.h create mode 100644 client/Android/android_jni_utils.c create mode 100644 client/Android/android_jni_utils.h create mode 100644 client/CMakeLists.txt create mode 100644 client/FreeRDP-ClientConfig.cmake.in create mode 100644 client/Mac/.gitignore create mode 100644 client/Mac/CMakeLists.txt create mode 100644 client/Mac/CertificateDialog.h create mode 100644 client/Mac/CertificateDialog.m create mode 100644 client/Mac/CertificateDialog.xib create mode 100644 client/Mac/Clipboard.h create mode 100644 client/Mac/Clipboard.m create mode 100644 client/Mac/Credits.rtf create mode 100644 client/Mac/Info.plist create mode 100644 client/Mac/Keyboard.h create mode 100644 client/Mac/Keyboard.m create mode 100644 client/Mac/MRDPCursor.h create mode 100644 client/Mac/MRDPCursor.m create mode 100644 client/Mac/MRDPView.h create mode 100644 client/Mac/MRDPView.m create mode 100644 client/Mac/ModuleOptions.cmake create mode 100644 client/Mac/PasswordDialog.h create mode 100644 client/Mac/PasswordDialog.m create mode 100644 client/Mac/PasswordDialog.xib create mode 100644 client/Mac/cli/AppDelegate.h create mode 100644 client/Mac/cli/AppDelegate.m create mode 100644 client/Mac/cli/CMakeLists.txt create mode 100644 client/Mac/cli/FreeRDP.icns create mode 100644 client/Mac/cli/Info.plist create mode 100644 client/Mac/cli/MacClient2-Info.plist create mode 100644 client/Mac/cli/MacClient2-Prefix.pch create mode 100644 client/Mac/cli/MainMenu.xib create mode 100644 client/Mac/cli/en.lproj/Credits.rtf create mode 100644 client/Mac/cli/en.lproj/InfoPlist.strings create mode 100644 client/Mac/cli/en.lproj/MainMenu.xib create mode 100644 client/Mac/cli/main.m create mode 100644 client/Mac/en.lproj/InfoPlist.strings create mode 100644 client/Mac/main.m create mode 100644 client/Mac/mf_client.h create mode 100644 client/Mac/mf_client.m create mode 100644 client/Mac/mfreerdp.h create mode 100644 client/Sample/CMakeLists.txt create mode 100644 client/Sample/ModuleOptions.cmake create mode 100644 client/Sample/tf_channels.c create mode 100644 client/Sample/tf_channels.h create mode 100644 client/Sample/tf_freerdp.c create mode 100644 client/Sample/tf_freerdp.h create mode 100644 client/Wayland/CMakeLists.txt create mode 100644 client/Wayland/wlf_channels.c create mode 100644 client/Wayland/wlf_channels.h create mode 100644 client/Wayland/wlf_cliprdr.c create mode 100644 client/Wayland/wlf_cliprdr.h create mode 100644 client/Wayland/wlf_disp.c create mode 100644 client/Wayland/wlf_disp.h create mode 100644 client/Wayland/wlf_input.c create mode 100644 client/Wayland/wlf_input.h create mode 100644 client/Wayland/wlf_pointer.c create mode 100644 client/Wayland/wlf_pointer.h create mode 100644 client/Wayland/wlfreerdp.1.in create mode 100644 client/Wayland/wlfreerdp.c create mode 100644 client/Wayland/wlfreerdp.h create mode 100644 client/Windows/CMakeLists.txt create mode 100644 client/Windows/FreeRDP.ico create mode 100644 client/Windows/ModuleOptions.cmake create mode 100644 client/Windows/cli/CMakeLists.txt create mode 100644 client/Windows/cli/wfreerdp.c create mode 100644 client/Windows/cli/wfreerdp.h create mode 100644 client/Windows/resource.h create mode 100644 client/Windows/resource/close.bmp create mode 100644 client/Windows/resource/close_active.bmp create mode 100644 client/Windows/resource/lock.bmp create mode 100644 client/Windows/resource/lock_active.bmp create mode 100644 client/Windows/resource/minimize.bmp create mode 100644 client/Windows/resource/minimize_active.bmp create mode 100644 client/Windows/resource/restore.bmp create mode 100644 client/Windows/resource/restore_active.bmp create mode 100644 client/Windows/resource/unlock.bmp create mode 100644 client/Windows/resource/unlock_active.bmp create mode 100644 client/Windows/wf_channels.c create mode 100644 client/Windows/wf_channels.h create mode 100644 client/Windows/wf_client.c create mode 100644 client/Windows/wf_client.h create mode 100644 client/Windows/wf_cliprdr.c create mode 100644 client/Windows/wf_cliprdr.h create mode 100644 client/Windows/wf_event.c create mode 100644 client/Windows/wf_event.h create mode 100644 client/Windows/wf_floatbar.c create mode 100644 client/Windows/wf_floatbar.h create mode 100644 client/Windows/wf_gdi.c create mode 100644 client/Windows/wf_gdi.h create mode 100644 client/Windows/wf_graphics.c create mode 100644 client/Windows/wf_graphics.h create mode 100644 client/Windows/wf_rail.c create mode 100644 client/Windows/wf_rail.h create mode 100644 client/Windows/wfreerdp.rc create mode 100644 client/X11/.gitignore create mode 100644 client/X11/CMakeLists.txt create mode 100644 client/X11/ModuleOptions.cmake create mode 100644 client/X11/cli/.gitignore create mode 100644 client/X11/cli/CMakeLists.txt create mode 100644 client/X11/cli/xfreerdp.c create mode 100644 client/X11/generate_argument_docbook.c create mode 100644 client/X11/resource/close.xbm create mode 100644 client/X11/resource/lock.xbm create mode 100644 client/X11/resource/minimize.xbm create mode 100644 client/X11/resource/restore.xbm create mode 100644 client/X11/resource/unlock.xbm create mode 100644 client/X11/xf_channels.c create mode 100644 client/X11/xf_channels.h create mode 100644 client/X11/xf_client.c create mode 100644 client/X11/xf_client.h create mode 100644 client/X11/xf_cliprdr.c create mode 100644 client/X11/xf_cliprdr.h create mode 100644 client/X11/xf_disp.c create mode 100644 client/X11/xf_disp.h create mode 100644 client/X11/xf_event.c create mode 100644 client/X11/xf_event.h create mode 100644 client/X11/xf_floatbar.c create mode 100644 client/X11/xf_floatbar.h create mode 100644 client/X11/xf_gdi.c create mode 100644 client/X11/xf_gdi.h create mode 100644 client/X11/xf_gfx.c create mode 100644 client/X11/xf_gfx.h create mode 100644 client/X11/xf_graphics.c create mode 100644 client/X11/xf_graphics.h create mode 100644 client/X11/xf_input.c create mode 100644 client/X11/xf_input.h create mode 100644 client/X11/xf_keyboard.c create mode 100644 client/X11/xf_keyboard.h create mode 100644 client/X11/xf_monitor.c create mode 100644 client/X11/xf_monitor.h create mode 100644 client/X11/xf_rail.c create mode 100644 client/X11/xf_rail.h create mode 100644 client/X11/xf_tsmf.c create mode 100644 client/X11/xf_tsmf.h create mode 100644 client/X11/xf_video.c create mode 100644 client/X11/xf_video.h create mode 100644 client/X11/xf_window.c create mode 100644 client/X11/xf_window.h create mode 100644 client/X11/xfreerdp-channels.1.xml create mode 100644 client/X11/xfreerdp-envvar.1.xml create mode 100644 client/X11/xfreerdp-examples.1.xml create mode 100644 client/X11/xfreerdp.1.xml.in create mode 100644 client/X11/xfreerdp.h create mode 100644 client/common/CMakeLists.txt create mode 100644 client/common/client.c create mode 100644 client/common/cmdline.c create mode 100644 client/common/cmdline.h create mode 100644 client/common/compatibility.c create mode 100644 client/common/compatibility.h create mode 100644 client/common/file.c create mode 100644 client/common/geometry.c create mode 100644 client/common/test/.gitignore create mode 100644 client/common/test/CMakeLists.txt create mode 100644 client/common/test/TestClientChannels.c create mode 100644 client/common/test/TestClientCmdLine.c create mode 100644 client/common/test/TestClientRdpFile.c create mode 100644 client/freerdp-client.pc.in create mode 100644 client/iOS/.gitignore create mode 100644 client/iOS/Additions/OrderedDictionary.h create mode 100644 client/iOS/Additions/OrderedDictionary.m create mode 100644 client/iOS/Additions/TSXAdditions.h create mode 100644 client/iOS/Additions/TSXAdditions.m create mode 100644 client/iOS/Additions/Toast+UIView.h create mode 100644 client/iOS/Additions/Toast+UIView.m create mode 100644 client/iOS/AppDelegate.h create mode 100644 client/iOS/AppDelegate.m create mode 100644 client/iOS/CMakeLists.txt create mode 100644 client/iOS/Controllers/AboutController.h create mode 100644 client/iOS/Controllers/AboutController.m create mode 100644 client/iOS/Controllers/AdvancedBookmarkEditorController.h create mode 100644 client/iOS/Controllers/AdvancedBookmarkEditorController.m create mode 100644 client/iOS/Controllers/AppSettingsController.h create mode 100644 client/iOS/Controllers/AppSettingsController.m create mode 100644 client/iOS/Controllers/BookmarkEditorController.h create mode 100644 client/iOS/Controllers/BookmarkEditorController.m create mode 100644 client/iOS/Controllers/BookmarkGatewaySettingsController.h create mode 100644 client/iOS/Controllers/BookmarkGatewaySettingsController.m create mode 100644 client/iOS/Controllers/BookmarkListController.h create mode 100644 client/iOS/Controllers/BookmarkListController.m create mode 100644 client/iOS/Controllers/CredentialsEditorController.h create mode 100644 client/iOS/Controllers/CredentialsEditorController.m create mode 100644 client/iOS/Controllers/CredentialsInputController.h create mode 100644 client/iOS/Controllers/CredentialsInputController.m create mode 100644 client/iOS/Controllers/EditorBaseController.h create mode 100644 client/iOS/Controllers/EditorBaseController.m create mode 100644 client/iOS/Controllers/EditorSelectionController.h create mode 100644 client/iOS/Controllers/EditorSelectionController.m create mode 100644 client/iOS/Controllers/EncryptionController.h create mode 100644 client/iOS/Controllers/EncryptionController.m create mode 100644 client/iOS/Controllers/HelpController.h create mode 100644 client/iOS/Controllers/HelpController.m create mode 100644 client/iOS/Controllers/MainTabBarController.h create mode 100644 client/iOS/Controllers/MainTabBarController.m create mode 100644 client/iOS/Controllers/PerformanceEditorController.h create mode 100644 client/iOS/Controllers/PerformanceEditorController.m create mode 100644 client/iOS/Controllers/RDPSessionViewController.h create mode 100644 client/iOS/Controllers/RDPSessionViewController.m create mode 100644 client/iOS/Controllers/ScreenSelectionController.h create mode 100644 client/iOS/Controllers/ScreenSelectionController.m create mode 100644 client/iOS/Controllers/VerifyCertificateController.h create mode 100644 client/iOS/Controllers/VerifyCertificateController.m create mode 100644 client/iOS/Defaults.plist create mode 100644 client/iOS/FreeRDP/ios_freerdp.h create mode 100644 client/iOS/FreeRDP/ios_freerdp.m create mode 100644 client/iOS/FreeRDP/ios_freerdp_events.h create mode 100644 client/iOS/FreeRDP/ios_freerdp_events.m create mode 100644 client/iOS/FreeRDP/ios_freerdp_ui.h create mode 100644 client/iOS/FreeRDP/ios_freerdp_ui.m create mode 100644 client/iOS/Misc/Reachability.h create mode 100644 client/iOS/Misc/Reachability.m create mode 100644 client/iOS/Misc/SFHFKeychainUtils.h create mode 100644 client/iOS/Misc/SFHFKeychainUtils.m create mode 100644 client/iOS/Misc/TSXTypes.h create mode 100644 client/iOS/Misc/Utils.h create mode 100644 client/iOS/Misc/Utils.m create mode 100644 client/iOS/Models/Bookmark.h create mode 100644 client/iOS/Models/Bookmark.m create mode 100644 client/iOS/Models/ConnectionParams.h create mode 100644 client/iOS/Models/ConnectionParams.m create mode 100644 client/iOS/Models/Encryptor.h create mode 100644 client/iOS/Models/Encryptor.m create mode 100644 client/iOS/Models/GlobalDefaults.h create mode 100644 client/iOS/Models/GlobalDefaults.m create mode 100644 client/iOS/Models/RDPKeyboard.h create mode 100644 client/iOS/Models/RDPKeyboard.m create mode 100644 client/iOS/Models/RDPSession.h create mode 100644 client/iOS/Models/RDPSession.m create mode 100644 client/iOS/ModuleOptions.cmake create mode 100644 client/iOS/Resources/BookmarkListView.xib create mode 100644 client/iOS/Resources/BookmarkTableViewCell.xib create mode 100644 client/iOS/Resources/CredentialsInputView.xib create mode 100644 client/iOS/Resources/Default-568h@2x.png create mode 100644 client/iOS/Resources/Default-Landscape@2x~ipad.png create mode 100644 client/iOS/Resources/Default-Landscape~ipad.png create mode 100644 client/iOS/Resources/Default-Portrait@2x~ipad.png create mode 100644 client/iOS/Resources/Default-Portrait~ipad.png create mode 100644 client/iOS/Resources/Default.png create mode 100644 client/iOS/Resources/Default@2x.png create mode 100644 client/iOS/Resources/EditButtonTableViewCell.xib create mode 100644 client/iOS/Resources/EditFlagTableViewCell.xib create mode 100644 client/iOS/Resources/EditSecretTextTableViewCell.xib create mode 100644 client/iOS/Resources/EditSelectionTableViewCell.xib create mode 100644 client/iOS/Resources/EditSubEditTableViewCell.xib create mode 100644 client/iOS/Resources/EditTextTableViewCell.xib create mode 100644 client/iOS/Resources/Icon-72.png create mode 100644 client/iOS/Resources/Icon-72@2x.png create mode 100644 client/iOS/Resources/Icon.png create mode 100644 client/iOS/Resources/Icon@2x.png create mode 100644 client/iOS/Resources/MainWindow.xib create mode 100644 client/iOS/Resources/RDPConnectingView.xib create mode 100644 client/iOS/Resources/RDPSessionView.xib create mode 100644 client/iOS/Resources/SessionTableViewCell.xib create mode 100644 client/iOS/Resources/VerifyCertificateView.xib create mode 100644 client/iOS/Resources/about_page/FreeRDP_Logo.png create mode 100644 client/iOS/Resources/about_page/about.html create mode 100644 client/iOS/Resources/about_page/about_phone.html create mode 100644 client/iOS/Resources/about_page/back.jpg create mode 100644 client/iOS/Resources/about_page/background_transparent.png create mode 100644 client/iOS/Resources/alert-black-button.png create mode 100644 client/iOS/Resources/alert-black-button@2x.png create mode 100644 client/iOS/Resources/alert-gray-button.png create mode 100644 client/iOS/Resources/alert-gray-button@2x.png create mode 100644 client/iOS/Resources/alert-red-button.png create mode 100644 client/iOS/Resources/alert-red-button@2x.png create mode 100644 client/iOS/Resources/alert-window-landscape.png create mode 100644 client/iOS/Resources/alert-window-landscape@2x.png create mode 100644 client/iOS/Resources/alert-window.png create mode 100644 client/iOS/Resources/alert-window@2x.png create mode 100644 client/iOS/Resources/cancel_button_background.png create mode 100644 client/iOS/Resources/en.lproj/Localizable.strings create mode 100644 client/iOS/Resources/help_page/back.jpg create mode 100644 client/iOS/Resources/help_page/gestures.html create mode 100644 client/iOS/Resources/help_page/gestures.png create mode 100644 client/iOS/Resources/help_page/gestures_phone.html create mode 100644 client/iOS/Resources/help_page/gestures_phone.png create mode 100644 client/iOS/Resources/help_page/nav_gestures.png create mode 100644 client/iOS/Resources/help_page/nav_toolbar.png create mode 100644 client/iOS/Resources/help_page/nav_touch_pointer.png create mode 100644 client/iOS/Resources/help_page/toolbar.html create mode 100644 client/iOS/Resources/help_page/toolbar.png create mode 100644 client/iOS/Resources/help_page/toolbar_phone.html create mode 100644 client/iOS/Resources/help_page/toolbar_phone.png create mode 100644 client/iOS/Resources/help_page/touch_pointer.html create mode 100644 client/iOS/Resources/help_page/touch_pointer.png create mode 100644 client/iOS/Resources/help_page/touch_pointer_phone.html create mode 100644 client/iOS/Resources/help_page/touch_pointer_phone.png create mode 100644 client/iOS/Resources/icon_accessory_star_off.png create mode 100644 client/iOS/Resources/icon_accessory_star_on.png create mode 100644 client/iOS/Resources/icon_key_arrow_down.png create mode 100644 client/iOS/Resources/icon_key_arrow_left.png create mode 100644 client/iOS/Resources/icon_key_arrow_right.png create mode 100644 client/iOS/Resources/icon_key_arrow_up.png create mode 100644 client/iOS/Resources/icon_key_arrows.png create mode 100644 client/iOS/Resources/icon_key_backspace.png create mode 100644 client/iOS/Resources/icon_key_menu.png create mode 100644 client/iOS/Resources/icon_key_return.png create mode 100644 client/iOS/Resources/icon_key_win.png create mode 100644 client/iOS/Resources/keyboard_button_background.png create mode 100644 client/iOS/Resources/tabbar_icon_about.png create mode 100644 client/iOS/Resources/tabbar_icon_help.png create mode 100644 client/iOS/Resources/tabbar_icon_settings.png create mode 100644 client/iOS/Resources/toolbar_icon_disconnect.png create mode 100644 client/iOS/Resources/toolbar_icon_extkeyboad.png create mode 100644 client/iOS/Resources/toolbar_icon_home.png create mode 100644 client/iOS/Resources/toolbar_icon_keyboard.png create mode 100644 client/iOS/Resources/toolbar_icon_touchpointer.png create mode 100644 client/iOS/Resources/toolbar_icon_win.png create mode 100644 client/iOS/Resources/touch_pointer_active.png create mode 100644 client/iOS/Resources/touch_pointer_default.png create mode 100644 client/iOS/Resources/touch_pointer_extkeyboard.png create mode 100644 client/iOS/Resources/touch_pointer_keyboard.png create mode 100644 client/iOS/Resources/touch_pointer_lclick.png create mode 100644 client/iOS/Resources/touch_pointer_rclick.png create mode 100644 client/iOS/Resources/touch_pointer_reset.png create mode 100644 client/iOS/Resources/touch_pointer_scroll.png create mode 100644 client/iOS/Views/AdvancedKeyboardView.h create mode 100644 client/iOS/Views/AdvancedKeyboardView.m create mode 100644 client/iOS/Views/BlockAlertView.h create mode 100644 client/iOS/Views/BlockAlertView.m create mode 100644 client/iOS/Views/BlockBackground.h create mode 100644 client/iOS/Views/BlockBackground.m create mode 100644 client/iOS/Views/BlockUI.h create mode 100644 client/iOS/Views/BookmarkTableCell.h create mode 100644 client/iOS/Views/BookmarkTableCell.m create mode 100644 client/iOS/Views/EditButtonTableViewCell.h create mode 100644 client/iOS/Views/EditButtonTableViewCell.m create mode 100644 client/iOS/Views/EditFlagTableViewCell.h create mode 100644 client/iOS/Views/EditFlagTableViewCell.m create mode 100644 client/iOS/Views/EditSecretTextTableViewCell.h create mode 100644 client/iOS/Views/EditSecretTextTableViewCell.m create mode 100644 client/iOS/Views/EditSelectionTableViewCell.h create mode 100644 client/iOS/Views/EditSelectionTableViewCell.m create mode 100644 client/iOS/Views/EditSubEditTableViewCell.h create mode 100644 client/iOS/Views/EditSubEditTableViewCell.m create mode 100644 client/iOS/Views/EditTextTableViewCell.h create mode 100644 client/iOS/Views/EditTextTableViewCell.m create mode 100644 client/iOS/Views/RDPSessionView.h create mode 100644 client/iOS/Views/RDPSessionView.m create mode 100644 client/iOS/Views/SessionTableCell.h create mode 100644 client/iOS/Views/SessionTableCell.m create mode 100644 client/iOS/Views/TouchPointerView.h create mode 100644 client/iOS/Views/TouchPointerView.m create mode 100644 client/iOS/iFreeRDP-Prefix.pch create mode 100644 client/iOS/iFreeRDP.plist create mode 100644 client/iOS/main.m create mode 100644 cmake/CheckCmakeCompat.cmake create mode 100644 cmake/ClangFormat.cmake create mode 100644 cmake/ClangToolchain.cmake create mode 100644 cmake/ComplexLibrary.cmake create mode 100644 cmake/ConfigOptions.cmake create mode 100644 cmake/ConfigOptionsAndroid.cmake create mode 100644 cmake/ConfigOptionsiOS.cmake create mode 100644 cmake/EchoTarget.cmake create mode 100644 cmake/FindCairo.cmake create mode 100644 cmake/FindDBus.cmake create mode 100644 cmake/FindDbusGlib.cmake create mode 100644 cmake/FindDevD.cmake create mode 100644 cmake/FindDocBookXSL.cmake create mode 100644 cmake/FindFAAC.cmake create mode 100644 cmake/FindFAAD2.cmake create mode 100644 cmake/FindFFmpeg.cmake create mode 100644 cmake/FindFeature.cmake create mode 100644 cmake/FindGSM.cmake create mode 100644 cmake/FindGSSAPI.cmake create mode 100644 cmake/FindGStreamer_0_10.cmake create mode 100644 cmake/FindGStreamer_1_0.cmake create mode 100644 cmake/FindGlib.cmake create mode 100644 cmake/FindIPP.cmake create mode 100644 cmake/FindImageMagick.cmake create mode 100644 cmake/FindLAME.cmake create mode 100644 cmake/FindMbedTLS.cmake create mode 100644 cmake/FindOSS.cmake create mode 100644 cmake/FindOpenH264.cmake create mode 100644 cmake/FindOpenSLES.cmake create mode 100644 cmake/FindOpenSSL.cmake create mode 100644 cmake/FindPAM.cmake create mode 100644 cmake/FindPCSC.cmake create mode 100644 cmake/FindPCSCWinPR.cmake create mode 100644 cmake/FindPixman.cmake create mode 100644 cmake/FindPulse.cmake create mode 100644 cmake/FindSWScale.cmake create mode 100644 cmake/FindUDev.cmake create mode 100644 cmake/FindUUID.cmake create mode 100644 cmake/FindWayland.cmake create mode 100644 cmake/FindX11.cmake create mode 100644 cmake/FindXKBFile.cmake create mode 100644 cmake/FindXRandR.cmake create mode 100644 cmake/FindXShm.cmake create mode 100644 cmake/FindXTest.cmake create mode 100644 cmake/FindXcursor.cmake create mode 100644 cmake/FindXdamage.cmake create mode 100644 cmake/FindXext.cmake create mode 100644 cmake/FindXfixes.cmake create mode 100644 cmake/FindXi.cmake create mode 100644 cmake/FindXinerama.cmake create mode 100644 cmake/FindXmlto.cmake create mode 100644 cmake/FindXrender.cmake create mode 100644 cmake/FindXv.cmake create mode 100644 cmake/Findlibsystemd.cmake create mode 100644 cmake/Findlibusb-1.0.cmake create mode 100644 cmake/Findsoxr.cmake create mode 100644 cmake/GNUInstallDirsWrapper.cmake create mode 100644 cmake/GetGitRevisionDescription.cmake create mode 100644 cmake/GetGitRevisionDescription.cmake.in create mode 100644 cmake/InstallFreeRDPMan.cmake create mode 100644 cmake/LibFindMacros.cmake create mode 100644 cmake/MSVCRuntime.cmake create mode 100644 cmake/MergeStaticLibs.cmake create mode 100644 cmake/SetFreeRDPCMakeInstallDir.cmake create mode 100644 cmake/WindowsDLLVersion.rc.in create mode 100644 cmake/compat_2.8.11/BasicConfigVersion-AnyNewerVersion.cmake.in create mode 100644 cmake/compat_2.8.11/BasicConfigVersion-ExactVersion.cmake.in create mode 100644 cmake/compat_2.8.11/BasicConfigVersion-SameMajorVersion.cmake.in create mode 100644 cmake/compat_2.8.11/CMakePackageConfigHelpers.cmake create mode 100644 cmake/compat_2.8.11/WriteBasicConfigVersionFile.cmake create mode 100644 cmake/compat_2.8.2/FindPkgConfig.cmake create mode 100644 cmake/compat_2.8.3/CMakeParseArguments.cmake create mode 100644 cmake/compat_2.8.3/FindPackageHandleStandardArgs.cmake create mode 100644 cmake/compat_2.8.6/FeatureSummary.cmake create mode 100644 cmake/compat_3.7.0/FindICU.cmake create mode 100644 cmake/ios.toolchain.cmake create mode 100644 cmake/today.cmake create mode 100644 config.h.in create mode 100644 docs/Doxyfile create mode 100644 docs/FreeRDP.vsd create mode 100644 docs/PrintFormatSpecifiers.md create mode 100644 docs/README.android create mode 100644 docs/README.ios create mode 100644 docs/README.macOS create mode 100644 docs/README.timezones create mode 100644 docs/valgrind.supp create mode 100644 docs/version_detection.md create mode 100644 docs/wlog.md create mode 100644 external/README create mode 100644 include/CMakeLists.txt create mode 100644 include/freerdp/addin.h create mode 100644 include/freerdp/altsec.h create mode 100644 include/freerdp/api.h create mode 100644 include/freerdp/assistance.h create mode 100644 include/freerdp/autodetect.h create mode 100644 include/freerdp/build-config.h.in create mode 100644 include/freerdp/cache/bitmap.h create mode 100644 include/freerdp/cache/brush.h create mode 100644 include/freerdp/cache/cache.h create mode 100644 include/freerdp/cache/glyph.h create mode 100644 include/freerdp/cache/nine_grid.h create mode 100644 include/freerdp/cache/offscreen.h create mode 100644 include/freerdp/cache/palette.h create mode 100644 include/freerdp/cache/pointer.h create mode 100644 include/freerdp/channels/ainput.h create mode 100644 include/freerdp/channels/audin.h create mode 100644 include/freerdp/channels/channels.h create mode 100644 include/freerdp/channels/cliprdr.h create mode 100644 include/freerdp/channels/disp.h create mode 100644 include/freerdp/channels/echo.h create mode 100644 include/freerdp/channels/encomsp.h create mode 100644 include/freerdp/channels/geometry.h create mode 100644 include/freerdp/channels/log.h create mode 100644 include/freerdp/channels/rail.h create mode 100644 include/freerdp/channels/rdpdr.h create mode 100644 include/freerdp/channels/rdpecam.h create mode 100644 include/freerdp/channels/rdpei.h create mode 100644 include/freerdp/channels/rdpgfx.h create mode 100644 include/freerdp/channels/rdpsnd.h create mode 100644 include/freerdp/channels/remdesk.h create mode 100644 include/freerdp/channels/telemetry.h create mode 100644 include/freerdp/channels/tsmf.h create mode 100644 include/freerdp/channels/urbdrc.h create mode 100644 include/freerdp/channels/video.h create mode 100644 include/freerdp/channels/wtsvc.h create mode 100644 include/freerdp/client.h create mode 100644 include/freerdp/client/ainput.h create mode 100644 include/freerdp/client/audin.h create mode 100644 include/freerdp/client/channels.h create mode 100644 include/freerdp/client/cliprdr.h create mode 100644 include/freerdp/client/cmdline.h create mode 100644 include/freerdp/client/disp.h create mode 100644 include/freerdp/client/drdynvc.h create mode 100644 include/freerdp/client/encomsp.h create mode 100644 include/freerdp/client/file.h create mode 100644 include/freerdp/client/geometry.h create mode 100644 include/freerdp/client/printer.h create mode 100644 include/freerdp/client/rail.h create mode 100644 include/freerdp/client/rdpei.h create mode 100644 include/freerdp/client/rdpgfx.h create mode 100644 include/freerdp/client/rdpsnd.h create mode 100644 include/freerdp/client/remdesk.h create mode 100644 include/freerdp/client/sshagent.h create mode 100644 include/freerdp/client/tsmf.h create mode 100644 include/freerdp/client/video.h create mode 100644 include/freerdp/codec/audio.h create mode 100644 include/freerdp/codec/bitmap.h create mode 100644 include/freerdp/codec/bulk.h create mode 100644 include/freerdp/codec/clear.h create mode 100644 include/freerdp/codec/color.h create mode 100644 include/freerdp/codec/dsp.h create mode 100644 include/freerdp/codec/h264.h create mode 100644 include/freerdp/codec/interleaved.h create mode 100644 include/freerdp/codec/jpeg.h create mode 100644 include/freerdp/codec/mppc.h create mode 100644 include/freerdp/codec/ncrush.h create mode 100644 include/freerdp/codec/nsc.h create mode 100644 include/freerdp/codec/planar.h create mode 100644 include/freerdp/codec/progressive.h create mode 100644 include/freerdp/codec/region.h create mode 100644 include/freerdp/codec/rfx.h create mode 100644 include/freerdp/codec/xcrush.h create mode 100644 include/freerdp/codec/yuv.h create mode 100644 include/freerdp/codec/zgfx.h create mode 100644 include/freerdp/codecs.h create mode 100644 include/freerdp/constants.h create mode 100644 include/freerdp/crypto/ber.h create mode 100644 include/freerdp/crypto/certificate.h create mode 100644 include/freerdp/crypto/crypto.h create mode 100644 include/freerdp/crypto/der.h create mode 100644 include/freerdp/crypto/er.h create mode 100644 include/freerdp/crypto/per.h create mode 100644 include/freerdp/crypto/tls.h create mode 100644 include/freerdp/display.h create mode 100644 include/freerdp/dvc.h create mode 100644 include/freerdp/error.h create mode 100644 include/freerdp/event.h create mode 100644 include/freerdp/extension.h create mode 100644 include/freerdp/freerdp.h create mode 100644 include/freerdp/gdi/bitmap.h create mode 100644 include/freerdp/gdi/dc.h create mode 100644 include/freerdp/gdi/gdi.h create mode 100644 include/freerdp/gdi/gfx.h create mode 100644 include/freerdp/gdi/pen.h create mode 100644 include/freerdp/gdi/region.h create mode 100644 include/freerdp/gdi/shape.h create mode 100644 include/freerdp/gdi/video.h create mode 100644 include/freerdp/graphics.h create mode 100644 include/freerdp/heartbeat.h create mode 100644 include/freerdp/input.h create mode 100644 include/freerdp/license.h create mode 100644 include/freerdp/listener.h create mode 100644 include/freerdp/locale/keyboard.h create mode 100644 include/freerdp/locale/locale.h create mode 100644 include/freerdp/log.h create mode 100644 include/freerdp/message.h create mode 100644 include/freerdp/metrics.h create mode 100644 include/freerdp/peer.h create mode 100644 include/freerdp/pointer.h create mode 100644 include/freerdp/primary.h create mode 100644 include/freerdp/primitives.h create mode 100644 include/freerdp/rail.h create mode 100644 include/freerdp/scancode.h create mode 100644 include/freerdp/secondary.h create mode 100644 include/freerdp/server/ainput.h create mode 100644 include/freerdp/server/audin.h create mode 100644 include/freerdp/server/channels.h create mode 100644 include/freerdp/server/cliprdr.h create mode 100644 include/freerdp/server/disp.h create mode 100644 include/freerdp/server/drdynvc.h create mode 100644 include/freerdp/server/echo.h create mode 100644 include/freerdp/server/encomsp.h create mode 100644 include/freerdp/server/rail.h create mode 100644 include/freerdp/server/rdpdr.h create mode 100644 include/freerdp/server/rdpecam-enumerator.h create mode 100644 include/freerdp/server/rdpecam.h create mode 100644 include/freerdp/server/rdpei.h create mode 100644 include/freerdp/server/rdpgfx.h create mode 100644 include/freerdp/server/rdpsnd.h create mode 100644 include/freerdp/server/remdesk.h create mode 100644 include/freerdp/server/server-common.h create mode 100644 include/freerdp/server/shadow.h create mode 100644 include/freerdp/server/telemetry.h create mode 100644 include/freerdp/session.h create mode 100644 include/freerdp/settings.h create mode 100644 include/freerdp/svc.h create mode 100644 include/freerdp/types.h create mode 100644 include/freerdp/update.h create mode 100644 include/freerdp/utils/cliprdr_utils.h create mode 100644 include/freerdp/utils/passphrase.h create mode 100644 include/freerdp/utils/pcap.h create mode 100644 include/freerdp/utils/profiler.h create mode 100644 include/freerdp/utils/ringbuffer.h create mode 100644 include/freerdp/utils/signal.h create mode 100644 include/freerdp/utils/stopwatch.h create mode 100644 include/freerdp/version.h.in create mode 100644 include/freerdp/window.h create mode 100644 libfreerdp/CMakeLists.txt create mode 100644 libfreerdp/FreeRDPConfig.cmake.in create mode 100644 libfreerdp/cache/CMakeLists.txt create mode 100644 libfreerdp/cache/bitmap.c create mode 100644 libfreerdp/cache/bitmap.h create mode 100644 libfreerdp/cache/brush.c create mode 100644 libfreerdp/cache/brush.h create mode 100644 libfreerdp/cache/cache.c create mode 100644 libfreerdp/cache/cache.h create mode 100644 libfreerdp/cache/glyph.c create mode 100644 libfreerdp/cache/glyph.h create mode 100644 libfreerdp/cache/nine_grid.c create mode 100644 libfreerdp/cache/offscreen.c create mode 100644 libfreerdp/cache/palette.c create mode 100644 libfreerdp/cache/palette.h create mode 100644 libfreerdp/cache/pointer.c create mode 100644 libfreerdp/cache/pointer.h create mode 100644 libfreerdp/codec/audio.c create mode 100644 libfreerdp/codec/bitmap.c create mode 100644 libfreerdp/codec/clear.c create mode 100644 libfreerdp/codec/color.c create mode 100644 libfreerdp/codec/dsp.c create mode 100644 libfreerdp/codec/dsp.h create mode 100644 libfreerdp/codec/dsp_ffmpeg.c create mode 100644 libfreerdp/codec/dsp_ffmpeg.h create mode 100644 libfreerdp/codec/h264.c create mode 100644 libfreerdp/codec/h264.h create mode 100644 libfreerdp/codec/h264_ffmpeg.c create mode 100644 libfreerdp/codec/h264_mediacodec.c create mode 100644 libfreerdp/codec/h264_mf.c create mode 100644 libfreerdp/codec/h264_openh264.c create mode 100644 libfreerdp/codec/include/bitmap.c create mode 100644 libfreerdp/codec/interleaved.c create mode 100644 libfreerdp/codec/jpeg.c create mode 100644 libfreerdp/codec/mppc.c create mode 100644 libfreerdp/codec/ncrush.c create mode 100644 libfreerdp/codec/nsc.c create mode 100644 libfreerdp/codec/nsc_encode.c create mode 100644 libfreerdp/codec/nsc_encode.h create mode 100644 libfreerdp/codec/nsc_sse2.c create mode 100644 libfreerdp/codec/nsc_sse2.h create mode 100644 libfreerdp/codec/nsc_types.h create mode 100644 libfreerdp/codec/planar.c create mode 100644 libfreerdp/codec/progressive.c create mode 100644 libfreerdp/codec/progressive.h create mode 100644 libfreerdp/codec/region.c create mode 100644 libfreerdp/codec/rfx.c create mode 100644 libfreerdp/codec/rfx_bitstream.h create mode 100644 libfreerdp/codec/rfx_constants.h create mode 100644 libfreerdp/codec/rfx_decode.c create mode 100644 libfreerdp/codec/rfx_decode.h create mode 100644 libfreerdp/codec/rfx_differential.h create mode 100644 libfreerdp/codec/rfx_dwt.c create mode 100644 libfreerdp/codec/rfx_dwt.h create mode 100644 libfreerdp/codec/rfx_encode.c create mode 100644 libfreerdp/codec/rfx_encode.h create mode 100644 libfreerdp/codec/rfx_neon.c create mode 100644 libfreerdp/codec/rfx_neon.h create mode 100644 libfreerdp/codec/rfx_quantization.c create mode 100644 libfreerdp/codec/rfx_quantization.h create mode 100644 libfreerdp/codec/rfx_rlgr.c create mode 100644 libfreerdp/codec/rfx_rlgr.h create mode 100644 libfreerdp/codec/rfx_sse2.c create mode 100644 libfreerdp/codec/rfx_sse2.h create mode 100644 libfreerdp/codec/rfx_types.h create mode 100644 libfreerdp/codec/test/.gitignore create mode 100644 libfreerdp/codec/test/CMakeLists.txt create mode 100644 libfreerdp/codec/test/TestFreeRDPCodecClear.c create mode 100644 libfreerdp/codec/test/TestFreeRDPCodecInterleaved.c create mode 100644 libfreerdp/codec/test/TestFreeRDPCodecMppc.c create mode 100644 libfreerdp/codec/test/TestFreeRDPCodecNCrush.c create mode 100644 libfreerdp/codec/test/TestFreeRDPCodecPlanar.c create mode 100644 libfreerdp/codec/test/TestFreeRDPCodecProgressive.c create mode 100644 libfreerdp/codec/test/TestFreeRDPCodecRemoteFX.c create mode 100644 libfreerdp/codec/test/TestFreeRDPCodecXCrush.c create mode 100644 libfreerdp/codec/test/TestFreeRDPCodecZGfx.c create mode 100644 libfreerdp/codec/test/TestFreeRDPRegion.c create mode 100644 libfreerdp/codec/test/progressive.bmp create mode 100644 libfreerdp/codec/test/rfx.bmp create mode 100644 libfreerdp/codec/test/test01.bmp create mode 100644 libfreerdp/codec/xcrush.c create mode 100644 libfreerdp/codec/yuv.c create mode 100644 libfreerdp/codec/zgfx.c create mode 100644 libfreerdp/common/CMakeLists.txt create mode 100644 libfreerdp/common/addin.c create mode 100644 libfreerdp/common/assistance.c create mode 100644 libfreerdp/common/settings.c create mode 100644 libfreerdp/common/settings_getters.c create mode 100644 libfreerdp/common/settings_str.c create mode 100644 libfreerdp/common/test/.gitignore create mode 100644 libfreerdp/common/test/CMakeLists.txt create mode 100644 libfreerdp/common/test/TestCommonAssistance.c create mode 100644 libfreerdp/core/CMakeLists.txt create mode 100644 libfreerdp/core/activation.c create mode 100644 libfreerdp/core/activation.h create mode 100644 libfreerdp/core/autodetect.c create mode 100644 libfreerdp/core/autodetect.h create mode 100644 libfreerdp/core/bulk.c create mode 100644 libfreerdp/core/bulk.h create mode 100644 libfreerdp/core/capabilities.c create mode 100644 libfreerdp/core/capabilities.h create mode 100644 libfreerdp/core/certificate.c create mode 100644 libfreerdp/core/certificate.h create mode 100644 libfreerdp/core/channels.c create mode 100644 libfreerdp/core/channels.h create mode 100644 libfreerdp/core/client.c create mode 100644 libfreerdp/core/client.h create mode 100644 libfreerdp/core/codecs.c create mode 100644 libfreerdp/core/connection.c create mode 100644 libfreerdp/core/connection.h create mode 100644 libfreerdp/core/display.c create mode 100644 libfreerdp/core/display.h create mode 100644 libfreerdp/core/errbase.c create mode 100644 libfreerdp/core/errconnect.c create mode 100644 libfreerdp/core/errinfo.c create mode 100644 libfreerdp/core/errinfo.h create mode 100644 libfreerdp/core/fastpath.c create mode 100644 libfreerdp/core/fastpath.h create mode 100644 libfreerdp/core/freerdp.c create mode 100644 libfreerdp/core/gateway/http.c create mode 100644 libfreerdp/core/gateway/http.h create mode 100644 libfreerdp/core/gateway/ncacn_http.c create mode 100644 libfreerdp/core/gateway/ncacn_http.h create mode 100644 libfreerdp/core/gateway/ntlm.c create mode 100644 libfreerdp/core/gateway/ntlm.h create mode 100644 libfreerdp/core/gateway/rdg.c create mode 100644 libfreerdp/core/gateway/rdg.h create mode 100644 libfreerdp/core/gateway/rpc.c create mode 100644 libfreerdp/core/gateway/rpc.h create mode 100644 libfreerdp/core/gateway/rpc_bind.c create mode 100644 libfreerdp/core/gateway/rpc_bind.h create mode 100644 libfreerdp/core/gateway/rpc_client.c create mode 100644 libfreerdp/core/gateway/rpc_client.h create mode 100644 libfreerdp/core/gateway/rpc_fault.c create mode 100644 libfreerdp/core/gateway/rpc_fault.h create mode 100644 libfreerdp/core/gateway/rts.c create mode 100644 libfreerdp/core/gateway/rts.h create mode 100644 libfreerdp/core/gateway/rts_signature.c create mode 100644 libfreerdp/core/gateway/rts_signature.h create mode 100644 libfreerdp/core/gateway/tsg.c create mode 100644 libfreerdp/core/gateway/tsg.h create mode 100644 libfreerdp/core/gcc.c create mode 100644 libfreerdp/core/gcc.h create mode 100644 libfreerdp/core/graphics.c create mode 100644 libfreerdp/core/graphics.h create mode 100644 libfreerdp/core/heartbeat.c create mode 100644 libfreerdp/core/heartbeat.h create mode 100644 libfreerdp/core/info.c create mode 100644 libfreerdp/core/info.h create mode 100644 libfreerdp/core/input.c create mode 100644 libfreerdp/core/input.h create mode 100644 libfreerdp/core/license.c create mode 100644 libfreerdp/core/license.h create mode 100644 libfreerdp/core/listener.c create mode 100644 libfreerdp/core/listener.h create mode 100644 libfreerdp/core/mcs.c create mode 100644 libfreerdp/core/mcs.h create mode 100644 libfreerdp/core/message.c create mode 100644 libfreerdp/core/message.h create mode 100644 libfreerdp/core/metrics.c create mode 100644 libfreerdp/core/multitransport.c create mode 100644 libfreerdp/core/multitransport.h create mode 100644 libfreerdp/core/nego.c create mode 100644 libfreerdp/core/nego.h create mode 100644 libfreerdp/core/nla.c create mode 100644 libfreerdp/core/nla.h create mode 100644 libfreerdp/core/orders.c create mode 100644 libfreerdp/core/orders.h create mode 100644 libfreerdp/core/peer.c create mode 100644 libfreerdp/core/peer.h create mode 100644 libfreerdp/core/proxy.c create mode 100644 libfreerdp/core/proxy.h create mode 100644 libfreerdp/core/rdp.c create mode 100644 libfreerdp/core/rdp.h create mode 100644 libfreerdp/core/redirection.c create mode 100644 libfreerdp/core/redirection.h create mode 100644 libfreerdp/core/security.c create mode 100644 libfreerdp/core/security.h create mode 100644 libfreerdp/core/server.c create mode 100644 libfreerdp/core/server.h create mode 100644 libfreerdp/core/settings.c create mode 100644 libfreerdp/core/settings.h create mode 100644 libfreerdp/core/surface.c create mode 100644 libfreerdp/core/surface.h create mode 100644 libfreerdp/core/tcp.c create mode 100644 libfreerdp/core/tcp.h create mode 100644 libfreerdp/core/test/.gitignore create mode 100644 libfreerdp/core/test/CMakeLists.txt create mode 100644 libfreerdp/core/test/TestConnect.c create mode 100644 libfreerdp/core/test/TestSettings.c create mode 100644 libfreerdp/core/test/TestVersion.c create mode 100644 libfreerdp/core/test/settings_property_lists.h create mode 100644 libfreerdp/core/timezone.c create mode 100644 libfreerdp/core/timezone.h create mode 100644 libfreerdp/core/tpdu.c create mode 100644 libfreerdp/core/tpdu.h create mode 100644 libfreerdp/core/tpkt.c create mode 100644 libfreerdp/core/tpkt.h create mode 100644 libfreerdp/core/transport.c create mode 100644 libfreerdp/core/transport.h create mode 100644 libfreerdp/core/update.c create mode 100644 libfreerdp/core/update.h create mode 100644 libfreerdp/core/utils.c create mode 100644 libfreerdp/core/utils.h create mode 100644 libfreerdp/core/window.c create mode 100644 libfreerdp/core/window.h create mode 100644 libfreerdp/crypto/CMakeLists.txt create mode 100644 libfreerdp/crypto/base64.c create mode 100644 libfreerdp/crypto/ber.c create mode 100644 libfreerdp/crypto/certificate.c create mode 100644 libfreerdp/crypto/crypto.c create mode 100644 libfreerdp/crypto/der.c create mode 100644 libfreerdp/crypto/er.c create mode 100644 libfreerdp/crypto/opensslcompat.c create mode 100644 libfreerdp/crypto/opensslcompat.h create mode 100644 libfreerdp/crypto/per.c create mode 100644 libfreerdp/crypto/test/.gitignore create mode 100644 libfreerdp/crypto/test/CMakeLists.txt create mode 100644 libfreerdp/crypto/test/TestBase64.c create mode 100644 libfreerdp/crypto/test/TestKnownHosts.c create mode 100644 libfreerdp/crypto/test/Test_x509_cert_info.c create mode 100644 libfreerdp/crypto/test/Test_x509_cert_info.pem create mode 100644 libfreerdp/crypto/test/known_hosts/known_hosts create mode 100644 libfreerdp/crypto/test/known_hosts/known_hosts.v2 create mode 100644 libfreerdp/crypto/tls.c create mode 100644 libfreerdp/freerdp.pc.in create mode 100644 libfreerdp/gdi/CMakeLists.txt create mode 100644 libfreerdp/gdi/bitmap.c create mode 100644 libfreerdp/gdi/brush.c create mode 100644 libfreerdp/gdi/brush.h create mode 100644 libfreerdp/gdi/clipping.c create mode 100644 libfreerdp/gdi/clipping.h create mode 100644 libfreerdp/gdi/dc.c create mode 100644 libfreerdp/gdi/drawing.c create mode 100644 libfreerdp/gdi/drawing.h create mode 100644 libfreerdp/gdi/gdi.c create mode 100644 libfreerdp/gdi/gdi.h create mode 100644 libfreerdp/gdi/gfx.c create mode 100644 libfreerdp/gdi/graphics.c create mode 100644 libfreerdp/gdi/graphics.h create mode 100644 libfreerdp/gdi/line.c create mode 100644 libfreerdp/gdi/line.h create mode 100644 libfreerdp/gdi/pen.c create mode 100644 libfreerdp/gdi/region.c create mode 100644 libfreerdp/gdi/shape.c create mode 100644 libfreerdp/gdi/test/.gitignore create mode 100644 libfreerdp/gdi/test/CMakeLists.txt create mode 100644 libfreerdp/gdi/test/TestGdiBitBlt.c create mode 100644 libfreerdp/gdi/test/TestGdiClip.c create mode 100644 libfreerdp/gdi/test/TestGdiCreate.c create mode 100644 libfreerdp/gdi/test/TestGdiEllipse.c create mode 100644 libfreerdp/gdi/test/TestGdiLine.c create mode 100644 libfreerdp/gdi/test/TestGdiRect.c create mode 100644 libfreerdp/gdi/test/TestGdiRegion.c create mode 100644 libfreerdp/gdi/test/TestGdiRop3.c create mode 100644 libfreerdp/gdi/test/helpers.c create mode 100644 libfreerdp/gdi/test/helpers.h create mode 100644 libfreerdp/gdi/video.c create mode 100644 libfreerdp/locale/CMakeLists.txt create mode 100644 libfreerdp/locale/keyboard.c create mode 100644 libfreerdp/locale/keyboard_apple.c create mode 100644 libfreerdp/locale/keyboard_apple.h create mode 100644 libfreerdp/locale/keyboard_layout.c create mode 100644 libfreerdp/locale/keyboard_sun.c create mode 100644 libfreerdp/locale/keyboard_sun.h create mode 100644 libfreerdp/locale/keyboard_x11.c create mode 100644 libfreerdp/locale/keyboard_x11.h create mode 100644 libfreerdp/locale/keyboard_xkbfile.c create mode 100644 libfreerdp/locale/keyboard_xkbfile.h create mode 100644 libfreerdp/locale/liblocale.h create mode 100644 libfreerdp/locale/locale.c create mode 100644 libfreerdp/locale/xkb_layout_ids.c create mode 100644 libfreerdp/locale/xkb_layout_ids.h create mode 100644 libfreerdp/primitives/README.txt create mode 100644 libfreerdp/primitives/prim_YCoCg.c create mode 100644 libfreerdp/primitives/prim_YCoCg_opt.c create mode 100644 libfreerdp/primitives/prim_YUV.c create mode 100644 libfreerdp/primitives/prim_YUV_neon.c create mode 100644 libfreerdp/primitives/prim_YUV_opencl.c create mode 100644 libfreerdp/primitives/prim_YUV_ssse3.c create mode 100644 libfreerdp/primitives/prim_add.c create mode 100644 libfreerdp/primitives/prim_add_opt.c create mode 100644 libfreerdp/primitives/prim_alphaComp.c create mode 100644 libfreerdp/primitives/prim_alphaComp_opt.c create mode 100644 libfreerdp/primitives/prim_andor.c create mode 100644 libfreerdp/primitives/prim_andor_opt.c create mode 100644 libfreerdp/primitives/prim_colors.c create mode 100644 libfreerdp/primitives/prim_colors_opt.c create mode 100644 libfreerdp/primitives/prim_copy.c create mode 100644 libfreerdp/primitives/prim_internal.h create mode 100644 libfreerdp/primitives/prim_set.c create mode 100644 libfreerdp/primitives/prim_set_opt.c create mode 100644 libfreerdp/primitives/prim_shift.c create mode 100644 libfreerdp/primitives/prim_shift_opt.c create mode 100644 libfreerdp/primitives/prim_sign.c create mode 100644 libfreerdp/primitives/prim_sign_opt.c create mode 100644 libfreerdp/primitives/prim_templates.h create mode 100644 libfreerdp/primitives/primitives.c create mode 100644 libfreerdp/primitives/primitives.cl create mode 100644 libfreerdp/primitives/test/.gitignore create mode 100644 libfreerdp/primitives/test/CMakeLists.txt create mode 100644 libfreerdp/primitives/test/TestPrimitivesAdd.c create mode 100644 libfreerdp/primitives/test/TestPrimitivesAlphaComp.c create mode 100644 libfreerdp/primitives/test/TestPrimitivesAndOr.c create mode 100644 libfreerdp/primitives/test/TestPrimitivesColors.c create mode 100644 libfreerdp/primitives/test/TestPrimitivesCopy.c create mode 100644 libfreerdp/primitives/test/TestPrimitivesSet.c create mode 100644 libfreerdp/primitives/test/TestPrimitivesShift.c create mode 100644 libfreerdp/primitives/test/TestPrimitivesSign.c create mode 100644 libfreerdp/primitives/test/TestPrimitivesYCbCr.c create mode 100644 libfreerdp/primitives/test/TestPrimitivesYCoCg.c create mode 100644 libfreerdp/primitives/test/TestPrimitivesYUV.c create mode 100644 libfreerdp/primitives/test/measure.h create mode 100644 libfreerdp/primitives/test/prim_test.c create mode 100644 libfreerdp/primitives/test/prim_test.h create mode 100644 libfreerdp/utils/CMakeLists.txt create mode 100644 libfreerdp/utils/cliprdr_utils.c create mode 100644 libfreerdp/utils/passphrase.c create mode 100644 libfreerdp/utils/pcap.c create mode 100644 libfreerdp/utils/profiler.c create mode 100644 libfreerdp/utils/ringbuffer.c create mode 100644 libfreerdp/utils/signal.c create mode 100644 libfreerdp/utils/stopwatch.c create mode 100644 libfreerdp/utils/test/.gitignore create mode 100644 libfreerdp/utils/test/CMakeLists.txt create mode 100644 libfreerdp/utils/test/TestRingBuffer.c create mode 100644 packaging/deb/freerdp-nightly/changelog create mode 100644 packaging/deb/freerdp-nightly/compat create mode 100644 packaging/deb/freerdp-nightly/control create mode 100644 packaging/deb/freerdp-nightly/copyright create mode 100644 packaging/deb/freerdp-nightly/freerdp-nightly-dbg.lintian-overrides create mode 100644 packaging/deb/freerdp-nightly/freerdp-nightly-dev.install create mode 100644 packaging/deb/freerdp-nightly/freerdp-nightly-dev.lintian-overrides create mode 100644 packaging/deb/freerdp-nightly/freerdp-nightly.install create mode 100644 packaging/deb/freerdp-nightly/freerdp-nightly.lintian-overrides create mode 100644 packaging/deb/freerdp-nightly/lintian-overrides create mode 100755 packaging/deb/freerdp-nightly/rules create mode 100644 packaging/deb/freerdp-nightly/source/format create mode 100755 packaging/flatpak/build-bundle.sh create mode 100644 packaging/flatpak/com.freerdp.FreeRDP.json create mode 100755 packaging/flatpak/freerdp.sh create mode 100644 packaging/rpm/freerdp-nightly-rpmlintrc create mode 100644 packaging/rpm/freerdp-nightly.spec create mode 100755 packaging/scripts/prepare_deb_freerdp-nightly.sh create mode 100755 packaging/scripts/prepare_rpm_freerdp-nightly.sh create mode 100644 rdtk/CMakeLists.txt create mode 100644 rdtk/include/rdtk/api.h create mode 100644 rdtk/include/rdtk/rdtk.h create mode 100644 rdtk/librdtk/CMakeLists.txt create mode 100644 rdtk/librdtk/rdtk_button.c create mode 100644 rdtk/librdtk/rdtk_button.h create mode 100644 rdtk/librdtk/rdtk_engine.c create mode 100644 rdtk/librdtk/rdtk_engine.h create mode 100644 rdtk/librdtk/rdtk_font.c create mode 100644 rdtk/librdtk/rdtk_font.h create mode 100644 rdtk/librdtk/rdtk_label.c create mode 100644 rdtk/librdtk/rdtk_label.h create mode 100644 rdtk/librdtk/rdtk_nine_patch.c create mode 100644 rdtk/librdtk/rdtk_nine_patch.h create mode 100644 rdtk/librdtk/rdtk_resources.c create mode 100644 rdtk/librdtk/rdtk_resources.h create mode 100644 rdtk/librdtk/rdtk_surface.c create mode 100644 rdtk/librdtk/rdtk_surface.h create mode 100644 rdtk/librdtk/rdtk_text_field.c create mode 100644 rdtk/librdtk/rdtk_text_field.h create mode 100644 rdtk/librdtk/test/.gitignore create mode 100644 rdtk/librdtk/test/CMakeLists.txt create mode 100644 rdtk/librdtk/test/TestRdTkNinePatch.c create mode 100644 rdtk/sample/.gitignore create mode 100644 rdtk/sample/CMakeLists.txt create mode 100644 rdtk/sample/rdtk_x11.c create mode 100644 resources/FreeRDP-fav.ico create mode 100644 resources/FreeRDP.ico create mode 100644 resources/FreeRDP_Icon.png create mode 100644 resources/FreeRDP_Icon.svg create mode 100644 resources/FreeRDP_Icon_256px.h create mode 100644 resources/FreeRDP_Icon_256px.png create mode 100644 resources/FreeRDP_Icon_256px.xpm create mode 100644 resources/FreeRDP_Icon_96px.ico create mode 100644 resources/FreeRDP_Install.bmp create mode 100644 resources/FreeRDP_Logo.png create mode 100644 resources/FreeRDP_Logo.svg create mode 100644 resources/FreeRDP_Logo_Icon.ai create mode 100644 resources/FreeRDP_Logo_Icon.svg create mode 100644 resources/FreeRDP_OSX.icns create mode 100755 resources/conv_to_ewm_prop.py create mode 100644 scripts/.gitignore create mode 100644 scripts/LECHash.c create mode 100644 scripts/LOMHash.c create mode 100755 scripts/OpenSSL-DownloadAndBuild.command create mode 100644 scripts/TimeZones.csx create mode 100644 scripts/android-build-32.conf create mode 100644 scripts/android-build-64.conf create mode 100644 scripts/android-build-common.sh create mode 100755 scripts/android-build-ffmpeg.sh create mode 100755 scripts/android-build-freerdp.sh create mode 100755 scripts/android-build-openh264.sh create mode 100755 scripts/android-build-openssl.sh create mode 100644 scripts/android-build-release.conf create mode 100644 scripts/android-build.conf create mode 100644 scripts/blacklist-address-sanitizer.txt create mode 100644 scripts/blacklist-memory-sanitizer.txt create mode 100644 scripts/blacklist-thread-sanitizer.txt create mode 100755 scripts/create_release_taball.sh create mode 100755 scripts/fetch_language_identifiers.py create mode 100755 scripts/gprof_generate.sh.cmake create mode 100644 scripts/specBytesToCode.py create mode 100644 scripts/test-scard.cpp create mode 100755 scripts/toolchains_path.py create mode 100755 scripts/update-rdpSettings create mode 100755 scripts/update-settings-tests create mode 100755 scripts/update-windows-zones.py create mode 100755 scripts/xcode.sh create mode 100755 scripts/xkb.pl create mode 100644 server/.gitignore create mode 100644 server/CMakeLists.txt create mode 100644 server/FreeRDP-ServerConfig.cmake.in create mode 100644 server/Mac/.gitignore create mode 100644 server/Mac/CMakeLists.txt create mode 100644 server/Mac/ModuleOptions.cmake create mode 100644 server/Mac/mf_audin.c create mode 100644 server/Mac/mf_audin.h create mode 100644 server/Mac/mf_event.c create mode 100644 server/Mac/mf_event.h create mode 100644 server/Mac/mf_info.c create mode 100644 server/Mac/mf_info.h create mode 100644 server/Mac/mf_input.c create mode 100644 server/Mac/mf_input.h create mode 100644 server/Mac/mf_interface.c create mode 100644 server/Mac/mf_interface.h create mode 100644 server/Mac/mf_mountain_lion.c create mode 100644 server/Mac/mf_mountain_lion.h create mode 100644 server/Mac/mf_peer.c create mode 100644 server/Mac/mf_peer.h create mode 100644 server/Mac/mf_rdpsnd.c create mode 100644 server/Mac/mf_rdpsnd.h create mode 100644 server/Mac/mfreerdp.c create mode 100644 server/Mac/mfreerdp.h create mode 100644 server/Mac/server.crt create mode 100644 server/Mac/server.key create mode 100644 server/Sample/CMakeLists.txt create mode 100644 server/Sample/ModuleOptions.cmake create mode 100644 server/Sample/rfx_test.pcap create mode 100644 server/Sample/server.crt create mode 100644 server/Sample/server.key create mode 100644 server/Sample/sf_audin.c create mode 100644 server/Sample/sf_audin.h create mode 100644 server/Sample/sf_encomsp.c create mode 100644 server/Sample/sf_encomsp.h create mode 100644 server/Sample/sf_rdpsnd.c create mode 100644 server/Sample/sf_rdpsnd.h create mode 100644 server/Sample/sfreerdp.c create mode 100644 server/Sample/sfreerdp.h create mode 100644 server/Sample/test_icon.ppm create mode 100644 server/Windows/CMakeLists.txt create mode 100644 server/Windows/ModuleOptions.cmake create mode 100644 server/Windows/cli/CMakeLists.txt create mode 100644 server/Windows/cli/wfreerdp.c create mode 100644 server/Windows/cli/wfreerdp.h create mode 100644 server/Windows/server.crt create mode 100644 server/Windows/server.key create mode 100644 server/Windows/wf_directsound.c create mode 100644 server/Windows/wf_directsound.h create mode 100644 server/Windows/wf_dxgi.c create mode 100644 server/Windows/wf_dxgi.h create mode 100644 server/Windows/wf_info.c create mode 100644 server/Windows/wf_info.h create mode 100644 server/Windows/wf_input.c create mode 100644 server/Windows/wf_input.h create mode 100644 server/Windows/wf_interface.c create mode 100644 server/Windows/wf_interface.h create mode 100644 server/Windows/wf_mirage.c create mode 100644 server/Windows/wf_mirage.h create mode 100644 server/Windows/wf_peer.c create mode 100644 server/Windows/wf_peer.h create mode 100644 server/Windows/wf_rdpsnd.c create mode 100644 server/Windows/wf_rdpsnd.h create mode 100644 server/Windows/wf_settings.c create mode 100644 server/Windows/wf_settings.h create mode 100644 server/Windows/wf_update.c create mode 100644 server/Windows/wf_update.h create mode 100644 server/Windows/wf_wasapi.c create mode 100644 server/Windows/wf_wasapi.h create mode 100644 server/common/CMakeLists.txt create mode 100644 server/common/server.c create mode 100644 server/freerdp-server.pc.in create mode 100644 server/proxy/CMakeLists.txt create mode 100644 server/proxy/config.ini create mode 100644 server/proxy/freerdp_proxy.c create mode 100644 server/proxy/modules/CMakeLists.txt create mode 100644 server/proxy/modules/README.md create mode 100644 server/proxy/modules/capture/CMakeLists.txt create mode 100644 server/proxy/modules/capture/cap_config.c create mode 100644 server/proxy/modules/capture/cap_config.h create mode 100644 server/proxy/modules/capture/cap_main.c create mode 100644 server/proxy/modules/capture/cap_protocol.c create mode 100644 server/proxy/modules/capture/cap_protocol.h create mode 100644 server/proxy/modules/demo/CMakeLists.txt create mode 100644 server/proxy/modules/demo/demo.cpp create mode 100644 server/proxy/modules/modules_api.h create mode 100644 server/proxy/pf_capture.c create mode 100644 server/proxy/pf_capture.h create mode 100644 server/proxy/pf_channels.c create mode 100644 server/proxy/pf_channels.h create mode 100644 server/proxy/pf_client.c create mode 100644 server/proxy/pf_client.h create mode 100644 server/proxy/pf_cliprdr.c create mode 100644 server/proxy/pf_cliprdr.h create mode 100644 server/proxy/pf_config.c create mode 100644 server/proxy/pf_config.h create mode 100644 server/proxy/pf_context.c create mode 100644 server/proxy/pf_context.h create mode 100644 server/proxy/pf_disp.c create mode 100644 server/proxy/pf_disp.h create mode 100644 server/proxy/pf_gdi.c create mode 100644 server/proxy/pf_gdi.h create mode 100644 server/proxy/pf_graphics.c create mode 100644 server/proxy/pf_graphics.h create mode 100644 server/proxy/pf_input.c create mode 100644 server/proxy/pf_input.h create mode 100644 server/proxy/pf_log.h create mode 100644 server/proxy/pf_modules.c create mode 100644 server/proxy/pf_modules.h create mode 100644 server/proxy/pf_rail.c create mode 100644 server/proxy/pf_rail.h create mode 100644 server/proxy/pf_rdpgfx.c create mode 100644 server/proxy/pf_rdpgfx.h create mode 100644 server/proxy/pf_rdpsnd.c create mode 100644 server/proxy/pf_rdpsnd.h create mode 100644 server/proxy/pf_server.c create mode 100644 server/proxy/pf_server.h create mode 100644 server/proxy/pf_update.c create mode 100644 server/proxy/pf_update.h create mode 100644 server/proxy/server.crt create mode 100644 server/proxy/server.key create mode 100644 server/proxy/session-capture/generate_video_from_frames.py create mode 100644 server/proxy/session-capture/requirements.txt create mode 100644 server/shadow/.gitignore create mode 100644 server/shadow/CMakeLists.txt create mode 100644 server/shadow/FreeRDP-ShadowConfig.cmake.in create mode 100644 server/shadow/Mac/mac_shadow.c create mode 100644 server/shadow/Mac/mac_shadow.h create mode 100644 server/shadow/Win/win_dxgi.c create mode 100644 server/shadow/Win/win_dxgi.h create mode 100644 server/shadow/Win/win_rdp.c create mode 100644 server/shadow/Win/win_rdp.h create mode 100644 server/shadow/Win/win_shadow.c create mode 100644 server/shadow/Win/win_shadow.h create mode 100644 server/shadow/Win/win_wds.c create mode 100644 server/shadow/Win/win_wds.h create mode 100644 server/shadow/X11/x11_shadow.c create mode 100644 server/shadow/X11/x11_shadow.h create mode 100644 server/shadow/freerdp-shadow-cli.1.in create mode 100644 server/shadow/freerdp-shadow.pc.in create mode 100644 server/shadow/shadow.c create mode 100644 server/shadow/shadow.h create mode 100644 server/shadow/shadow_audin.c create mode 100644 server/shadow/shadow_audin.h create mode 100644 server/shadow/shadow_capture.c create mode 100644 server/shadow/shadow_capture.h create mode 100644 server/shadow/shadow_channels.c create mode 100644 server/shadow/shadow_channels.h create mode 100644 server/shadow/shadow_client.c create mode 100644 server/shadow/shadow_client.h create mode 100644 server/shadow/shadow_encoder.c create mode 100644 server/shadow/shadow_encoder.h create mode 100644 server/shadow/shadow_encomsp.c create mode 100644 server/shadow/shadow_encomsp.h create mode 100644 server/shadow/shadow_input.c create mode 100644 server/shadow/shadow_input.h create mode 100644 server/shadow/shadow_lobby.c create mode 100644 server/shadow/shadow_lobby.h create mode 100644 server/shadow/shadow_mcevent.c create mode 100644 server/shadow/shadow_mcevent.h create mode 100644 server/shadow/shadow_rdpgfx.c create mode 100644 server/shadow/shadow_rdpgfx.h create mode 100644 server/shadow/shadow_rdpsnd.c create mode 100644 server/shadow/shadow_rdpsnd.h create mode 100644 server/shadow/shadow_remdesk.c create mode 100644 server/shadow/shadow_remdesk.h create mode 100644 server/shadow/shadow_screen.c create mode 100644 server/shadow/shadow_screen.h create mode 100644 server/shadow/shadow_server.c create mode 100644 server/shadow/shadow_subsystem.c create mode 100644 server/shadow/shadow_subsystem.h create mode 100644 server/shadow/shadow_subsystem_builtin.c create mode 100644 server/shadow/shadow_surface.c create mode 100644 server/shadow/shadow_surface.h create mode 100644 third-party/.gitignore create mode 100644 third-party/CMakeLists.txt create mode 100644 uwac/CMakeLists.txt create mode 100644 uwac/include/CMakeLists.txt create mode 100644 uwac/include/uwac/uwac-tools.h create mode 100644 uwac/include/uwac/uwac.h create mode 100644 uwac/libuwac/.gitignore create mode 100644 uwac/libuwac/CMakeLists.txt create mode 100644 uwac/libuwac/uwac-clipboard.c create mode 100644 uwac/libuwac/uwac-display.c create mode 100644 uwac/libuwac/uwac-input.c create mode 100644 uwac/libuwac/uwac-os.c create mode 100644 uwac/libuwac/uwac-os.h create mode 100644 uwac/libuwac/uwac-output.c create mode 100644 uwac/libuwac/uwac-priv.h create mode 100644 uwac/libuwac/uwac-tools.c create mode 100644 uwac/libuwac/uwac-utils.c create mode 100644 uwac/libuwac/uwac-utils.h create mode 100644 uwac/libuwac/uwac-window.c create mode 100644 uwac/protocols/fullscreen-shell-unstable-v1.xml create mode 100644 uwac/protocols/ivi-application.xml create mode 100644 uwac/protocols/keyboard-shortcuts-inhibit-unstable-v1.xml create mode 100644 uwac/protocols/server-decoration.xml create mode 100644 uwac/protocols/xdg-decoration-unstable-v1.xml create mode 100644 uwac/protocols/xdg-shell.xml create mode 100644 uwac/uwac.pc.in create mode 100644 uwac/uwacConfig.cmake.in create mode 100644 uwac/uwacVersion.cmake create mode 100644 winpr/.gitignore create mode 100644 winpr/CMakeLists.txt create mode 100644 winpr/WinPRConfig.cmake.in create mode 100644 winpr/include/CMakeLists.txt create mode 100644 winpr/include/winpr/.gitignore create mode 100644 winpr/include/winpr/asn1.h create mode 100644 winpr/include/winpr/assert.h create mode 100644 winpr/include/winpr/bcrypt.h create mode 100644 winpr/include/winpr/bitstream.h create mode 100644 winpr/include/winpr/clipboard.h create mode 100644 winpr/include/winpr/cmdline.h create mode 100644 winpr/include/winpr/collections.h create mode 100644 winpr/include/winpr/comm.h create mode 100644 winpr/include/winpr/credentials.h create mode 100644 winpr/include/winpr/credui.h create mode 100644 winpr/include/winpr/crt.h create mode 100644 winpr/include/winpr/crypto.h create mode 100644 winpr/include/winpr/debug.h create mode 100644 winpr/include/winpr/dsparse.h create mode 100644 winpr/include/winpr/endian.h create mode 100644 winpr/include/winpr/environment.h create mode 100644 winpr/include/winpr/error.h create mode 100644 winpr/include/winpr/file.h create mode 100644 winpr/include/winpr/handle.h create mode 100644 winpr/include/winpr/heap.h create mode 100644 winpr/include/winpr/image.h create mode 100644 winpr/include/winpr/ini.h create mode 100644 winpr/include/winpr/input.h create mode 100644 winpr/include/winpr/interlocked.h create mode 100644 winpr/include/winpr/intrin.h create mode 100644 winpr/include/winpr/io.h create mode 100644 winpr/include/winpr/library.h create mode 100644 winpr/include/winpr/locale.h create mode 100644 winpr/include/winpr/memory.h create mode 100644 winpr/include/winpr/midl.h create mode 100644 winpr/include/winpr/ndr.h create mode 100644 winpr/include/winpr/nt.h create mode 100644 winpr/include/winpr/ntlm.h create mode 100644 winpr/include/winpr/pack.h create mode 100644 winpr/include/winpr/path.h create mode 100644 winpr/include/winpr/pipe.h create mode 100644 winpr/include/winpr/platform.h create mode 100644 winpr/include/winpr/pool.h create mode 100644 winpr/include/winpr/print.h create mode 100644 winpr/include/winpr/registry.h create mode 100644 winpr/include/winpr/rpc.h create mode 100644 winpr/include/winpr/sam.h create mode 100644 winpr/include/winpr/schannel.h create mode 100644 winpr/include/winpr/security.h create mode 100644 winpr/include/winpr/shell.h create mode 100644 winpr/include/winpr/smartcard.h create mode 100644 winpr/include/winpr/spec.h create mode 100644 winpr/include/winpr/ssl.h create mode 100644 winpr/include/winpr/sspi.h create mode 100644 winpr/include/winpr/sspicli.h create mode 100644 winpr/include/winpr/stream.h create mode 100644 winpr/include/winpr/string.h create mode 100644 winpr/include/winpr/strlst.h create mode 100644 winpr/include/winpr/synch.h create mode 100644 winpr/include/winpr/sysinfo.h create mode 100644 winpr/include/winpr/tchar.h create mode 100644 winpr/include/winpr/thread.h create mode 100644 winpr/include/winpr/timezone.h create mode 100644 winpr/include/winpr/tools/makecert.h create mode 100644 winpr/include/winpr/user.h create mode 100644 winpr/include/winpr/version.h.in create mode 100644 winpr/include/winpr/windows.h create mode 100644 winpr/include/winpr/winhttp.h create mode 100644 winpr/include/winpr/winpr.h create mode 100644 winpr/include/winpr/winsock.h create mode 100644 winpr/include/winpr/wlog.h create mode 100644 winpr/include/winpr/wnd.h create mode 100644 winpr/include/winpr/wtsapi.h create mode 100644 winpr/include/winpr/wtypes.h.in create mode 100644 winpr/libwinpr/CMakeLists.txt create mode 100644 winpr/libwinpr/asn1/CMakeLists.txt create mode 100644 winpr/libwinpr/asn1/ModuleOptions.cmake create mode 100644 winpr/libwinpr/asn1/asn1.c create mode 100644 winpr/libwinpr/asn1/test/.gitignore create mode 100644 winpr/libwinpr/asn1/test/CMakeLists.txt create mode 100644 winpr/libwinpr/asn1/test/TestAsn1BerDec.c create mode 100644 winpr/libwinpr/asn1/test/TestAsn1BerEnc.c create mode 100644 winpr/libwinpr/asn1/test/TestAsn1Compare.c create mode 100644 winpr/libwinpr/asn1/test/TestAsn1Decode.c create mode 100644 winpr/libwinpr/asn1/test/TestAsn1Decoder.c create mode 100644 winpr/libwinpr/asn1/test/TestAsn1DerDec.c create mode 100644 winpr/libwinpr/asn1/test/TestAsn1DerEnc.c create mode 100644 winpr/libwinpr/asn1/test/TestAsn1Encode.c create mode 100644 winpr/libwinpr/asn1/test/TestAsn1Encoder.c create mode 100644 winpr/libwinpr/asn1/test/TestAsn1Integer.c create mode 100644 winpr/libwinpr/asn1/test/TestAsn1Module.c create mode 100644 winpr/libwinpr/asn1/test/TestAsn1String.c create mode 100644 winpr/libwinpr/bcrypt/CMakeLists.txt create mode 100644 winpr/libwinpr/bcrypt/ModuleOptions.cmake create mode 100644 winpr/libwinpr/bcrypt/bcrypt.c create mode 100644 winpr/libwinpr/clipboard/CMakeLists.txt create mode 100644 winpr/libwinpr/clipboard/ModuleOptions.cmake create mode 100644 winpr/libwinpr/clipboard/clipboard.c create mode 100644 winpr/libwinpr/clipboard/clipboard.h create mode 100644 winpr/libwinpr/clipboard/posix.c create mode 100644 winpr/libwinpr/clipboard/posix.h create mode 100644 winpr/libwinpr/clipboard/synthetic.c create mode 100644 winpr/libwinpr/clipboard/test/.gitignore create mode 100644 winpr/libwinpr/clipboard/test/CMakeLists.txt create mode 100644 winpr/libwinpr/clipboard/test/TestClipboardFormats.c create mode 100644 winpr/libwinpr/comm/CMakeLists.txt create mode 100644 winpr/libwinpr/comm/ModuleOptions.cmake create mode 100644 winpr/libwinpr/comm/comm.c create mode 100644 winpr/libwinpr/comm/comm.h create mode 100644 winpr/libwinpr/comm/comm_io.c create mode 100644 winpr/libwinpr/comm/comm_ioctl.c create mode 100644 winpr/libwinpr/comm/comm_ioctl.h create mode 100644 winpr/libwinpr/comm/comm_sercx2_sys.c create mode 100644 winpr/libwinpr/comm/comm_sercx2_sys.h create mode 100644 winpr/libwinpr/comm/comm_sercx_sys.c create mode 100644 winpr/libwinpr/comm/comm_sercx_sys.h create mode 100644 winpr/libwinpr/comm/comm_serial_sys.c create mode 100644 winpr/libwinpr/comm/comm_serial_sys.h create mode 100644 winpr/libwinpr/comm/test/.gitignore create mode 100644 winpr/libwinpr/comm/test/CMakeLists.txt create mode 100644 winpr/libwinpr/comm/test/TestCommConfig.c create mode 100644 winpr/libwinpr/comm/test/TestCommDevice.c create mode 100644 winpr/libwinpr/comm/test/TestCommMonitor.c create mode 100644 winpr/libwinpr/comm/test/TestControlSettings.c create mode 100644 winpr/libwinpr/comm/test/TestGetCommState.c create mode 100644 winpr/libwinpr/comm/test/TestHandflow.c create mode 100644 winpr/libwinpr/comm/test/TestSerialChars.c create mode 100644 winpr/libwinpr/comm/test/TestSetCommState.c create mode 100644 winpr/libwinpr/comm/test/TestTimeouts.c create mode 100644 winpr/libwinpr/credentials/CMakeLists.txt create mode 100644 winpr/libwinpr/credentials/ModuleOptions.cmake create mode 100644 winpr/libwinpr/credentials/credentials.c create mode 100644 winpr/libwinpr/credui/CMakeLists.txt create mode 100644 winpr/libwinpr/credui/ModuleOptions.cmake create mode 100644 winpr/libwinpr/credui/credui.c create mode 100644 winpr/libwinpr/credui/test/.gitignore create mode 100644 winpr/libwinpr/credui/test/CMakeLists.txt create mode 100644 winpr/libwinpr/credui/test/TestCredUICmdLinePromptForCredentials.c create mode 100644 winpr/libwinpr/credui/test/TestCredUIConfirmCredentials.c create mode 100644 winpr/libwinpr/credui/test/TestCredUIParseUserName.c create mode 100644 winpr/libwinpr/credui/test/TestCredUIPromptForCredentials.c create mode 100644 winpr/libwinpr/crt/CMakeLists.txt create mode 100644 winpr/libwinpr/crt/ModuleOptions.cmake create mode 100644 winpr/libwinpr/crt/alignment.c create mode 100644 winpr/libwinpr/crt/buffer.c create mode 100644 winpr/libwinpr/crt/casing.c create mode 100644 winpr/libwinpr/crt/conversion.c create mode 100644 winpr/libwinpr/crt/memory.c create mode 100644 winpr/libwinpr/crt/string.c create mode 100644 winpr/libwinpr/crt/test/.gitignore create mode 100644 winpr/libwinpr/crt/test/CMakeLists.txt create mode 100644 winpr/libwinpr/crt/test/TestAlignment.c create mode 100644 winpr/libwinpr/crt/test/TestFormatSpecifiers.c create mode 100644 winpr/libwinpr/crt/test/TestString.c create mode 100644 winpr/libwinpr/crt/test/TestTypes.c create mode 100644 winpr/libwinpr/crt/test/TestUnicodeConversion.c create mode 100644 winpr/libwinpr/crt/unicode.c create mode 100644 winpr/libwinpr/crt/utf.c create mode 100644 winpr/libwinpr/crt/utf.h create mode 100644 winpr/libwinpr/crypto/CMakeLists.txt create mode 100644 winpr/libwinpr/crypto/ModuleOptions.cmake create mode 100644 winpr/libwinpr/crypto/cert.c create mode 100644 winpr/libwinpr/crypto/cipher.c create mode 100644 winpr/libwinpr/crypto/crypto.c create mode 100644 winpr/libwinpr/crypto/crypto.h create mode 100644 winpr/libwinpr/crypto/hash.c create mode 100644 winpr/libwinpr/crypto/rand.c create mode 100644 winpr/libwinpr/crypto/test/.gitignore create mode 100644 winpr/libwinpr/crypto/test/CMakeLists.txt create mode 100644 winpr/libwinpr/crypto/test/TestCryptoCertEnumCertificatesInStore.c create mode 100644 winpr/libwinpr/crypto/test/TestCryptoCipher.c create mode 100644 winpr/libwinpr/crypto/test/TestCryptoHash.c create mode 100644 winpr/libwinpr/crypto/test/TestCryptoProtectData.c create mode 100644 winpr/libwinpr/crypto/test/TestCryptoProtectMemory.c create mode 100644 winpr/libwinpr/crypto/test/TestCryptoRand.c create mode 100644 winpr/libwinpr/dsparse/CMakeLists.txt create mode 100644 winpr/libwinpr/dsparse/ModuleOptions.cmake create mode 100644 winpr/libwinpr/dsparse/dsparse.c create mode 100644 winpr/libwinpr/dsparse/test/.gitignore create mode 100644 winpr/libwinpr/dsparse/test/CMakeLists.txt create mode 100644 winpr/libwinpr/dsparse/test/TestDsCrackNames.c create mode 100644 winpr/libwinpr/dsparse/test/TestDsMakeSpn.c create mode 100644 winpr/libwinpr/dummy.c create mode 100644 winpr/libwinpr/environment/CMakeLists.txt create mode 100644 winpr/libwinpr/environment/ModuleOptions.cmake create mode 100644 winpr/libwinpr/environment/environment.c create mode 100644 winpr/libwinpr/environment/test/.gitignore create mode 100644 winpr/libwinpr/environment/test/CMakeLists.txt create mode 100644 winpr/libwinpr/environment/test/TestEnvironmentGetEnvironmentStrings.c create mode 100644 winpr/libwinpr/environment/test/TestEnvironmentGetSetEB.c create mode 100644 winpr/libwinpr/environment/test/TestEnvironmentMergeEnvironmentStrings.c create mode 100644 winpr/libwinpr/environment/test/TestEnvironmentSetEnvironmentVariable.c create mode 100644 winpr/libwinpr/error/CMakeLists.txt create mode 100644 winpr/libwinpr/error/ModuleOptions.cmake create mode 100644 winpr/libwinpr/error/error.c create mode 100644 winpr/libwinpr/error/test/.gitignore create mode 100644 winpr/libwinpr/error/test/CMakeLists.txt create mode 100644 winpr/libwinpr/error/test/TestErrorSetLastError.c create mode 100644 winpr/libwinpr/file/CMakeLists.txt create mode 100644 winpr/libwinpr/file/ModuleOptions.cmake create mode 100644 winpr/libwinpr/file/file.c create mode 100644 winpr/libwinpr/file/file.h create mode 100644 winpr/libwinpr/file/generic.c create mode 100644 winpr/libwinpr/file/namedPipeClient.c create mode 100644 winpr/libwinpr/file/pattern.c create mode 100644 winpr/libwinpr/file/test/.gitignore create mode 100644 winpr/libwinpr/file/test/CMakeLists.txt create mode 100644 winpr/libwinpr/file/test/TestFileCreateFile.c create mode 100644 winpr/libwinpr/file/test/TestFileDeleteFile.c create mode 100644 winpr/libwinpr/file/test/TestFileFindFirstFile.c create mode 100644 winpr/libwinpr/file/test/TestFileFindFirstFileEx.c create mode 100644 winpr/libwinpr/file/test/TestFileFindNextFile.c create mode 100644 winpr/libwinpr/file/test/TestFileGetStdHandle.c create mode 100644 winpr/libwinpr/file/test/TestFilePatternMatch.c create mode 100644 winpr/libwinpr/file/test/TestFileReadFile.c create mode 100644 winpr/libwinpr/file/test/TestFileWriteFile.c create mode 100644 winpr/libwinpr/file/test/TestSetFileAttributes.c create mode 100644 winpr/libwinpr/handle/CMakeLists.txt create mode 100644 winpr/libwinpr/handle/ModuleOptions.cmake create mode 100644 winpr/libwinpr/handle/handle.c create mode 100644 winpr/libwinpr/handle/handle.h create mode 100644 winpr/libwinpr/handle/nonehandle.c create mode 100644 winpr/libwinpr/handle/nonehandle.h create mode 100644 winpr/libwinpr/heap/CMakeLists.txt create mode 100644 winpr/libwinpr/heap/ModuleOptions.cmake create mode 100644 winpr/libwinpr/heap/heap.c create mode 100644 winpr/libwinpr/input/CMakeLists.txt create mode 100644 winpr/libwinpr/input/ModuleOptions.cmake create mode 100644 winpr/libwinpr/input/keycode.c create mode 100644 winpr/libwinpr/input/scancode.c create mode 100644 winpr/libwinpr/input/virtualkey.c create mode 100644 winpr/libwinpr/interlocked/CMakeLists.txt create mode 100644 winpr/libwinpr/interlocked/ModuleOptions.cmake create mode 100644 winpr/libwinpr/interlocked/interlocked.c create mode 100644 winpr/libwinpr/interlocked/module_5.1.def create mode 100644 winpr/libwinpr/interlocked/test/.gitignore create mode 100644 winpr/libwinpr/interlocked/test/CMakeLists.txt create mode 100644 winpr/libwinpr/interlocked/test/TestInterlockedAccess.c create mode 100644 winpr/libwinpr/interlocked/test/TestInterlockedDList.c create mode 100644 winpr/libwinpr/interlocked/test/TestInterlockedSList.c create mode 100644 winpr/libwinpr/io/CMakeLists.txt create mode 100644 winpr/libwinpr/io/ModuleOptions.cmake create mode 100644 winpr/libwinpr/io/device.c create mode 100644 winpr/libwinpr/io/io.c create mode 100644 winpr/libwinpr/io/io.h create mode 100644 winpr/libwinpr/io/test/.gitignore create mode 100644 winpr/libwinpr/io/test/CMakeLists.txt create mode 100644 winpr/libwinpr/io/test/TestIoDevice.c create mode 100644 winpr/libwinpr/io/test/TestIoGetOverlappedResult.c create mode 100644 winpr/libwinpr/library/CMakeLists.txt create mode 100644 winpr/libwinpr/library/ModuleOptions.cmake create mode 100644 winpr/libwinpr/library/library.c create mode 100644 winpr/libwinpr/library/test/.gitignore create mode 100644 winpr/libwinpr/library/test/CMakeLists.txt create mode 100644 winpr/libwinpr/library/test/TestLibraryA/CMakeLists.txt create mode 100644 winpr/libwinpr/library/test/TestLibraryA/TestLibraryA.c create mode 100644 winpr/libwinpr/library/test/TestLibraryB/CMakeLists.txt create mode 100644 winpr/libwinpr/library/test/TestLibraryB/TestLibraryB.c create mode 100644 winpr/libwinpr/library/test/TestLibraryGetModuleFileName.c create mode 100644 winpr/libwinpr/library/test/TestLibraryGetProcAddress.c create mode 100644 winpr/libwinpr/library/test/TestLibraryLoadLibrary.c create mode 100644 winpr/libwinpr/locale/CMakeLists.txt create mode 100644 winpr/libwinpr/locale/ModuleOptions.cmake create mode 100644 winpr/libwinpr/locale/locale.c create mode 100644 winpr/libwinpr/locale/test/.gitignore create mode 100644 winpr/libwinpr/locale/test/CMakeLists.txt create mode 100644 winpr/libwinpr/locale/test/TestLocaleFormatMessage.c create mode 100644 winpr/libwinpr/log.h create mode 100644 winpr/libwinpr/memory/CMakeLists.txt create mode 100644 winpr/libwinpr/memory/ModuleOptions.cmake create mode 100644 winpr/libwinpr/memory/memory.c create mode 100644 winpr/libwinpr/memory/memory.h create mode 100644 winpr/libwinpr/memory/test/.gitignore create mode 100644 winpr/libwinpr/memory/test/CMakeLists.txt create mode 100644 winpr/libwinpr/memory/test/TestMemoryCreateFileMapping.c create mode 100644 winpr/libwinpr/nt/CMakeLists.txt create mode 100644 winpr/libwinpr/nt/ModuleOptions.cmake create mode 100644 winpr/libwinpr/nt/nt.c create mode 100644 winpr/libwinpr/nt/ntstatus.c create mode 100644 winpr/libwinpr/nt/test/.gitignore create mode 100644 winpr/libwinpr/nt/test/CMakeLists.txt create mode 100644 winpr/libwinpr/nt/test/TestNtCreateFile.c create mode 100644 winpr/libwinpr/nt/test/TestNtCurrentTeb.c create mode 100644 winpr/libwinpr/path/CMakeLists.txt create mode 100644 winpr/libwinpr/path/ModuleOptions.cmake create mode 100644 winpr/libwinpr/path/include/PathAllocCombine.c create mode 100644 winpr/libwinpr/path/include/PathCchAddExtension.c create mode 100644 winpr/libwinpr/path/include/PathCchAddSeparator.c create mode 100644 winpr/libwinpr/path/include/PathCchAddSeparatorEx.c create mode 100644 winpr/libwinpr/path/include/PathCchAppend.c create mode 100644 winpr/libwinpr/path/path.c create mode 100644 winpr/libwinpr/path/shell.c create mode 100644 winpr/libwinpr/path/shell_ios.h create mode 100644 winpr/libwinpr/path/shell_ios.m create mode 100644 winpr/libwinpr/path/test/.gitignore create mode 100644 winpr/libwinpr/path/test/CMakeLists.txt create mode 100644 winpr/libwinpr/path/test/TestPathAllocCanonicalize.c create mode 100644 winpr/libwinpr/path/test/TestPathAllocCombine.c create mode 100644 winpr/libwinpr/path/test/TestPathCchAddBackslash.c create mode 100644 winpr/libwinpr/path/test/TestPathCchAddBackslashEx.c create mode 100644 winpr/libwinpr/path/test/TestPathCchAddExtension.c create mode 100644 winpr/libwinpr/path/test/TestPathCchAppend.c create mode 100644 winpr/libwinpr/path/test/TestPathCchAppendEx.c create mode 100644 winpr/libwinpr/path/test/TestPathCchCanonicalize.c create mode 100644 winpr/libwinpr/path/test/TestPathCchCanonicalizeEx.c create mode 100644 winpr/libwinpr/path/test/TestPathCchCombine.c create mode 100644 winpr/libwinpr/path/test/TestPathCchCombineEx.c create mode 100644 winpr/libwinpr/path/test/TestPathCchFindExtension.c create mode 100644 winpr/libwinpr/path/test/TestPathCchIsRoot.c create mode 100644 winpr/libwinpr/path/test/TestPathCchRemoveBackslash.c create mode 100644 winpr/libwinpr/path/test/TestPathCchRemoveBackslashEx.c create mode 100644 winpr/libwinpr/path/test/TestPathCchRemoveExtension.c create mode 100644 winpr/libwinpr/path/test/TestPathCchRemoveFileSpec.c create mode 100644 winpr/libwinpr/path/test/TestPathCchRenameExtension.c create mode 100644 winpr/libwinpr/path/test/TestPathCchSkipRoot.c create mode 100644 winpr/libwinpr/path/test/TestPathCchStripPrefix.c create mode 100644 winpr/libwinpr/path/test/TestPathCchStripToRoot.c create mode 100644 winpr/libwinpr/path/test/TestPathIsUNCEx.c create mode 100644 winpr/libwinpr/path/test/TestPathMakePath.c create mode 100644 winpr/libwinpr/path/test/TestPathShell.c create mode 100644 winpr/libwinpr/pipe/CMakeLists.txt create mode 100644 winpr/libwinpr/pipe/ModuleOptions.cmake create mode 100644 winpr/libwinpr/pipe/pipe.c create mode 100644 winpr/libwinpr/pipe/pipe.h create mode 100644 winpr/libwinpr/pipe/test/.gitignore create mode 100644 winpr/libwinpr/pipe/test/CMakeLists.txt create mode 100644 winpr/libwinpr/pipe/test/TestPipeCreateNamedPipe.c create mode 100644 winpr/libwinpr/pipe/test/TestPipeCreateNamedPipeOverlapped.c create mode 100644 winpr/libwinpr/pipe/test/TestPipeCreatePipe.c create mode 100644 winpr/libwinpr/pool/CMakeLists.txt create mode 100644 winpr/libwinpr/pool/ModuleOptions.cmake create mode 100644 winpr/libwinpr/pool/callback.c create mode 100644 winpr/libwinpr/pool/callback_cleanup.c create mode 100644 winpr/libwinpr/pool/cleanup_group.c create mode 100644 winpr/libwinpr/pool/io.c create mode 100644 winpr/libwinpr/pool/pool.c create mode 100644 winpr/libwinpr/pool/pool.h create mode 100644 winpr/libwinpr/pool/synch.c create mode 100644 winpr/libwinpr/pool/test/.gitignore create mode 100644 winpr/libwinpr/pool/test/CMakeLists.txt create mode 100644 winpr/libwinpr/pool/test/TestPoolIO.c create mode 100644 winpr/libwinpr/pool/test/TestPoolSynch.c create mode 100644 winpr/libwinpr/pool/test/TestPoolThread.c create mode 100644 winpr/libwinpr/pool/test/TestPoolTimer.c create mode 100644 winpr/libwinpr/pool/test/TestPoolWork.c create mode 100644 winpr/libwinpr/pool/timer.c create mode 100644 winpr/libwinpr/pool/work.c create mode 100644 winpr/libwinpr/registry/CMakeLists.txt create mode 100644 winpr/libwinpr/registry/ModuleOptions.cmake create mode 100644 winpr/libwinpr/registry/registry.c create mode 100644 winpr/libwinpr/registry/registry_reg.c create mode 100644 winpr/libwinpr/registry/registry_reg.h create mode 100644 winpr/libwinpr/rpc/CMakeLists.txt create mode 100644 winpr/libwinpr/rpc/ModuleOptions.cmake create mode 100644 winpr/libwinpr/rpc/midl.c create mode 100644 winpr/libwinpr/rpc/ndr.c create mode 100644 winpr/libwinpr/rpc/ndr_array.c create mode 100644 winpr/libwinpr/rpc/ndr_array.h create mode 100644 winpr/libwinpr/rpc/ndr_context.c create mode 100644 winpr/libwinpr/rpc/ndr_context.h create mode 100644 winpr/libwinpr/rpc/ndr_correlation.c create mode 100644 winpr/libwinpr/rpc/ndr_correlation.h create mode 100644 winpr/libwinpr/rpc/ndr_pointer.c create mode 100644 winpr/libwinpr/rpc/ndr_pointer.h create mode 100644 winpr/libwinpr/rpc/ndr_private.c create mode 100644 winpr/libwinpr/rpc/ndr_private.h create mode 100644 winpr/libwinpr/rpc/ndr_simple.c create mode 100644 winpr/libwinpr/rpc/ndr_simple.h create mode 100644 winpr/libwinpr/rpc/ndr_string.c create mode 100644 winpr/libwinpr/rpc/ndr_string.h create mode 100644 winpr/libwinpr/rpc/ndr_structure.c create mode 100644 winpr/libwinpr/rpc/ndr_structure.h create mode 100644 winpr/libwinpr/rpc/ndr_union.c create mode 100644 winpr/libwinpr/rpc/ndr_union.h create mode 100644 winpr/libwinpr/rpc/rpc.c create mode 100644 winpr/libwinpr/security/CMakeLists.txt create mode 100644 winpr/libwinpr/security/ModuleOptions.cmake create mode 100644 winpr/libwinpr/security/security.c create mode 100644 winpr/libwinpr/security/security.h create mode 100644 winpr/libwinpr/security/test/.gitignore create mode 100644 winpr/libwinpr/security/test/CMakeLists.txt create mode 100644 winpr/libwinpr/security/test/TestSecurityToken.c create mode 100644 winpr/libwinpr/shell/CMakeLists.txt create mode 100644 winpr/libwinpr/shell/ModuleOptions.cmake create mode 100644 winpr/libwinpr/shell/shell.c create mode 100644 winpr/libwinpr/smartcard/CMakeLists.txt create mode 100644 winpr/libwinpr/smartcard/ModuleOptions.cmake create mode 100644 winpr/libwinpr/smartcard/smartcard.c create mode 100644 winpr/libwinpr/smartcard/smartcard.h create mode 100644 winpr/libwinpr/smartcard/smartcard_inspect.c create mode 100644 winpr/libwinpr/smartcard/smartcard_inspect.h create mode 100644 winpr/libwinpr/smartcard/smartcard_pcsc.c create mode 100644 winpr/libwinpr/smartcard/smartcard_pcsc.h create mode 100644 winpr/libwinpr/smartcard/smartcard_winscard.c create mode 100644 winpr/libwinpr/smartcard/smartcard_winscard.h create mode 100644 winpr/libwinpr/smartcard/test/.gitignore create mode 100644 winpr/libwinpr/smartcard/test/CMakeLists.txt create mode 100644 winpr/libwinpr/smartcard/test/TestSmartCardListReaders.c create mode 100644 winpr/libwinpr/smartcard/test/TestSmartCardStatus.c create mode 100644 winpr/libwinpr/sspi/CMakeLists.txt create mode 100644 winpr/libwinpr/sspi/CredSSP/credssp.c create mode 100644 winpr/libwinpr/sspi/CredSSP/credssp.h create mode 100644 winpr/libwinpr/sspi/Kerberos/kerberos.c create mode 100644 winpr/libwinpr/sspi/Kerberos/kerberos.h create mode 100644 winpr/libwinpr/sspi/ModuleOptions.cmake create mode 100644 winpr/libwinpr/sspi/NTLM/ntlm.c create mode 100644 winpr/libwinpr/sspi/NTLM/ntlm.h create mode 100644 winpr/libwinpr/sspi/NTLM/ntlm_av_pairs.c create mode 100644 winpr/libwinpr/sspi/NTLM/ntlm_av_pairs.h create mode 100644 winpr/libwinpr/sspi/NTLM/ntlm_compute.c create mode 100644 winpr/libwinpr/sspi/NTLM/ntlm_compute.h create mode 100644 winpr/libwinpr/sspi/NTLM/ntlm_message.c create mode 100644 winpr/libwinpr/sspi/NTLM/ntlm_message.h create mode 100644 winpr/libwinpr/sspi/Negotiate/negotiate.c create mode 100644 winpr/libwinpr/sspi/Negotiate/negotiate.h create mode 100644 winpr/libwinpr/sspi/Schannel/schannel.c create mode 100644 winpr/libwinpr/sspi/Schannel/schannel.h create mode 100644 winpr/libwinpr/sspi/Schannel/schannel_openssl.c create mode 100644 winpr/libwinpr/sspi/Schannel/schannel_openssl.h create mode 100644 winpr/libwinpr/sspi/sspi.c create mode 100644 winpr/libwinpr/sspi/sspi.h create mode 100644 winpr/libwinpr/sspi/sspi_export.c create mode 100644 winpr/libwinpr/sspi/sspi_gss.c create mode 100644 winpr/libwinpr/sspi/sspi_gss.h create mode 100644 winpr/libwinpr/sspi/sspi_winpr.c create mode 100644 winpr/libwinpr/sspi/sspi_winpr.h create mode 100644 winpr/libwinpr/sspi/test/.gitignore create mode 100644 winpr/libwinpr/sspi/test/CMakeLists.txt create mode 100644 winpr/libwinpr/sspi/test/TestAcquireCredentialsHandle.c create mode 100644 winpr/libwinpr/sspi/test/TestCredSSP.c create mode 100644 winpr/libwinpr/sspi/test/TestEnumerateSecurityPackages.c create mode 100644 winpr/libwinpr/sspi/test/TestInitializeSecurityContext.c create mode 100644 winpr/libwinpr/sspi/test/TestNTLM.c create mode 100644 winpr/libwinpr/sspi/test/TestQuerySecurityPackageInfo.c create mode 100644 winpr/libwinpr/sspi/test/TestSchannel.c create mode 100644 winpr/libwinpr/sspicli/CMakeLists.txt create mode 100644 winpr/libwinpr/sspicli/ModuleOptions.cmake create mode 100644 winpr/libwinpr/sspicli/sspicli.c create mode 100644 winpr/libwinpr/synch/CMakeLists.txt create mode 100644 winpr/libwinpr/synch/ModuleOptions.cmake create mode 100644 winpr/libwinpr/synch/address.c create mode 100644 winpr/libwinpr/synch/barrier.c create mode 100644 winpr/libwinpr/synch/critical.c create mode 100644 winpr/libwinpr/synch/event.c create mode 100644 winpr/libwinpr/synch/event.h create mode 100644 winpr/libwinpr/synch/init.c create mode 100644 winpr/libwinpr/synch/mutex.c create mode 100644 winpr/libwinpr/synch/pollset.c create mode 100644 winpr/libwinpr/synch/pollset.h create mode 100644 winpr/libwinpr/synch/semaphore.c create mode 100644 winpr/libwinpr/synch/sleep.c create mode 100644 winpr/libwinpr/synch/synch.h create mode 100644 winpr/libwinpr/synch/test/.gitignore create mode 100644 winpr/libwinpr/synch/test/CMakeLists.txt create mode 100644 winpr/libwinpr/synch/test/TestSynchAPC.c create mode 100644 winpr/libwinpr/synch/test/TestSynchBarrier.c create mode 100644 winpr/libwinpr/synch/test/TestSynchCritical.c create mode 100644 winpr/libwinpr/synch/test/TestSynchEvent.c create mode 100644 winpr/libwinpr/synch/test/TestSynchInit.c create mode 100644 winpr/libwinpr/synch/test/TestSynchMultipleThreads.c create mode 100644 winpr/libwinpr/synch/test/TestSynchMutex.c create mode 100644 winpr/libwinpr/synch/test/TestSynchSemaphore.c create mode 100644 winpr/libwinpr/synch/test/TestSynchThread.c create mode 100644 winpr/libwinpr/synch/test/TestSynchTimerQueue.c create mode 100644 winpr/libwinpr/synch/test/TestSynchWaitableTimer.c create mode 100644 winpr/libwinpr/synch/test/TestSynchWaitableTimerAPC.c create mode 100644 winpr/libwinpr/synch/timer.c create mode 100644 winpr/libwinpr/synch/wait.c create mode 100644 winpr/libwinpr/sysinfo/CMakeLists.txt create mode 100644 winpr/libwinpr/sysinfo/ModuleOptions.cmake create mode 100644 winpr/libwinpr/sysinfo/cpufeatures/CMakeLists.txt create mode 100644 winpr/libwinpr/sysinfo/cpufeatures/NOTICE create mode 100644 winpr/libwinpr/sysinfo/cpufeatures/README create mode 100644 winpr/libwinpr/sysinfo/cpufeatures/cpu-features.c create mode 100644 winpr/libwinpr/sysinfo/cpufeatures/cpu-features.h create mode 100644 winpr/libwinpr/sysinfo/sysinfo.c create mode 100644 winpr/libwinpr/sysinfo/test/.gitignore create mode 100644 winpr/libwinpr/sysinfo/test/CMakeLists.txt create mode 100644 winpr/libwinpr/sysinfo/test/TestCPUFeatures.c create mode 100644 winpr/libwinpr/sysinfo/test/TestGetComputerName.c create mode 100644 winpr/libwinpr/sysinfo/test/TestGetNativeSystemInfo.c create mode 100644 winpr/libwinpr/sysinfo/test/TestLocalTime.c create mode 100644 winpr/libwinpr/sysinfo/test/TestSystemTime.c create mode 100644 winpr/libwinpr/thread/CMakeLists.txt create mode 100644 winpr/libwinpr/thread/ModuleOptions.cmake create mode 100644 winpr/libwinpr/thread/apc.c create mode 100644 winpr/libwinpr/thread/apc.h create mode 100644 winpr/libwinpr/thread/argv.c create mode 100644 winpr/libwinpr/thread/process.c create mode 100644 winpr/libwinpr/thread/processor.c create mode 100644 winpr/libwinpr/thread/test/.gitignore create mode 100644 winpr/libwinpr/thread/test/CMakeLists.txt create mode 100644 winpr/libwinpr/thread/test/TestThreadCommandLineToArgv.c create mode 100644 winpr/libwinpr/thread/test/TestThreadCreateProcess.c create mode 100644 winpr/libwinpr/thread/test/TestThreadExitThread.c create mode 100644 winpr/libwinpr/thread/thread.c create mode 100644 winpr/libwinpr/thread/thread.h create mode 100644 winpr/libwinpr/thread/tls.c create mode 100644 winpr/libwinpr/timezone/CMakeLists.txt create mode 100644 winpr/libwinpr/timezone/ModuleOptions.cmake create mode 100644 winpr/libwinpr/timezone/TimeZones.c create mode 100644 winpr/libwinpr/timezone/TimeZones.h create mode 100644 winpr/libwinpr/timezone/WindowsZones.c create mode 100644 winpr/libwinpr/timezone/WindowsZones.h create mode 100644 winpr/libwinpr/timezone/timezone.c create mode 100644 winpr/libwinpr/utils/CMakeLists.txt create mode 100644 winpr/libwinpr/utils/ModuleOptions.cmake create mode 100644 winpr/libwinpr/utils/cmdline.c create mode 100644 winpr/libwinpr/utils/collections/ArrayList.c create mode 100644 winpr/libwinpr/utils/collections/BipBuffer.c create mode 100644 winpr/libwinpr/utils/collections/BitStream.c create mode 100644 winpr/libwinpr/utils/collections/BufferPool.c create mode 100644 winpr/libwinpr/utils/collections/CountdownEvent.c create mode 100644 winpr/libwinpr/utils/collections/HashTable.c create mode 100644 winpr/libwinpr/utils/collections/LinkedList.c create mode 100644 winpr/libwinpr/utils/collections/ListDictionary.c create mode 100644 winpr/libwinpr/utils/collections/MessagePipe.c create mode 100644 winpr/libwinpr/utils/collections/MessageQueue.c create mode 100644 winpr/libwinpr/utils/collections/ObjectPool.c create mode 100644 winpr/libwinpr/utils/collections/PubSub.c create mode 100644 winpr/libwinpr/utils/collections/Queue.c create mode 100644 winpr/libwinpr/utils/collections/Reference.c create mode 100644 winpr/libwinpr/utils/collections/Stack.c create mode 100644 winpr/libwinpr/utils/collections/StreamPool.c create mode 100644 winpr/libwinpr/utils/corkscrew/backtrace.h create mode 100644 winpr/libwinpr/utils/corkscrew/debug.c create mode 100644 winpr/libwinpr/utils/corkscrew/debug.h create mode 100644 winpr/libwinpr/utils/corkscrew/demangle.h create mode 100644 winpr/libwinpr/utils/corkscrew/map_info.h create mode 100644 winpr/libwinpr/utils/corkscrew/ptrace.h create mode 100644 winpr/libwinpr/utils/corkscrew/symbol_table.h create mode 100644 winpr/libwinpr/utils/debug.c create mode 100644 winpr/libwinpr/utils/execinfo/debug.c create mode 100644 winpr/libwinpr/utils/execinfo/debug.h create mode 100644 winpr/libwinpr/utils/image.c create mode 100644 winpr/libwinpr/utils/ini.c create mode 100644 winpr/libwinpr/utils/lodepng/lodepng.c create mode 100644 winpr/libwinpr/utils/lodepng/lodepng.h create mode 100644 winpr/libwinpr/utils/ntlm.c create mode 100644 winpr/libwinpr/utils/print.c create mode 100644 winpr/libwinpr/utils/sam.c create mode 100644 winpr/libwinpr/utils/ssl.c create mode 100644 winpr/libwinpr/utils/stream.c create mode 100644 winpr/libwinpr/utils/strlst.c create mode 100644 winpr/libwinpr/utils/test/.gitignore create mode 100644 winpr/libwinpr/utils/test/CMakeLists.txt create mode 100644 winpr/libwinpr/utils/test/TestArrayList.c create mode 100644 winpr/libwinpr/utils/test/TestBacktrace.c create mode 100644 winpr/libwinpr/utils/test/TestBipBuffer.c create mode 100644 winpr/libwinpr/utils/test/TestBitStream.c create mode 100644 winpr/libwinpr/utils/test/TestBufferPool.c create mode 100644 winpr/libwinpr/utils/test/TestCmdLine.c create mode 100644 winpr/libwinpr/utils/test/TestHashTable.c create mode 100644 winpr/libwinpr/utils/test/TestImage.c create mode 100644 winpr/libwinpr/utils/test/TestIni.c create mode 100644 winpr/libwinpr/utils/test/TestLinkedList.c create mode 100644 winpr/libwinpr/utils/test/TestListDictionary.c create mode 100644 winpr/libwinpr/utils/test/TestMessagePipe.c create mode 100644 winpr/libwinpr/utils/test/TestMessageQueue.c create mode 100644 winpr/libwinpr/utils/test/TestPrint.c create mode 100644 winpr/libwinpr/utils/test/TestPubSub.c create mode 100644 winpr/libwinpr/utils/test/TestQueue.c create mode 100644 winpr/libwinpr/utils/test/TestStream.c create mode 100644 winpr/libwinpr/utils/test/TestStreamPool.c create mode 100644 winpr/libwinpr/utils/test/TestVersion.c create mode 100644 winpr/libwinpr/utils/test/TestWLog.c create mode 100644 winpr/libwinpr/utils/test/TestWLogCallback.c create mode 100644 winpr/libwinpr/utils/test/lodepng_32bit.bmp create mode 100644 winpr/libwinpr/utils/test/lodepng_32bit.png create mode 100644 winpr/libwinpr/utils/trio/strio.h create mode 100644 winpr/libwinpr/utils/trio/trio.c create mode 100644 winpr/libwinpr/utils/trio/trio.h create mode 100644 winpr/libwinpr/utils/trio/triodef.h create mode 100644 winpr/libwinpr/utils/trio/trionan.c create mode 100644 winpr/libwinpr/utils/trio/trionan.h create mode 100644 winpr/libwinpr/utils/trio/triop.h create mode 100644 winpr/libwinpr/utils/trio/triostr.c create mode 100644 winpr/libwinpr/utils/trio/triostr.h create mode 100644 winpr/libwinpr/utils/unwind/debug.c create mode 100644 winpr/libwinpr/utils/unwind/debug.h create mode 100644 winpr/libwinpr/utils/windows/debug.c create mode 100644 winpr/libwinpr/utils/windows/debug.h create mode 100644 winpr/libwinpr/utils/winpr.c create mode 100644 winpr/libwinpr/utils/wlog/Appender.c create mode 100644 winpr/libwinpr/utils/wlog/Appender.h create mode 100644 winpr/libwinpr/utils/wlog/BinaryAppender.c create mode 100644 winpr/libwinpr/utils/wlog/BinaryAppender.h create mode 100644 winpr/libwinpr/utils/wlog/CallbackAppender.c create mode 100644 winpr/libwinpr/utils/wlog/CallbackAppender.h create mode 100644 winpr/libwinpr/utils/wlog/ConsoleAppender.c create mode 100644 winpr/libwinpr/utils/wlog/ConsoleAppender.h create mode 100644 winpr/libwinpr/utils/wlog/DataMessage.c create mode 100644 winpr/libwinpr/utils/wlog/DataMessage.h create mode 100644 winpr/libwinpr/utils/wlog/FileAppender.c create mode 100644 winpr/libwinpr/utils/wlog/FileAppender.h create mode 100644 winpr/libwinpr/utils/wlog/ImageMessage.c create mode 100644 winpr/libwinpr/utils/wlog/ImageMessage.h create mode 100644 winpr/libwinpr/utils/wlog/JournaldAppender.c create mode 100644 winpr/libwinpr/utils/wlog/JournaldAppender.h create mode 100644 winpr/libwinpr/utils/wlog/Layout.c create mode 100644 winpr/libwinpr/utils/wlog/Layout.h create mode 100644 winpr/libwinpr/utils/wlog/Message.c create mode 100644 winpr/libwinpr/utils/wlog/Message.h create mode 100644 winpr/libwinpr/utils/wlog/PacketMessage.c create mode 100644 winpr/libwinpr/utils/wlog/PacketMessage.h create mode 100644 winpr/libwinpr/utils/wlog/SyslogAppender.c create mode 100644 winpr/libwinpr/utils/wlog/SyslogAppender.h create mode 100644 winpr/libwinpr/utils/wlog/UdpAppender.c create mode 100644 winpr/libwinpr/utils/wlog/UdpAppender.h create mode 100644 winpr/libwinpr/utils/wlog/wlog.c create mode 100644 winpr/libwinpr/utils/wlog/wlog.h create mode 100644 winpr/libwinpr/winhttp/CMakeLists.txt create mode 100644 winpr/libwinpr/winhttp/ModuleOptions.cmake create mode 100644 winpr/libwinpr/winhttp/winhttp.c create mode 100644 winpr/libwinpr/winsock/CMakeLists.txt create mode 100644 winpr/libwinpr/winsock/ModuleOptions.cmake create mode 100644 winpr/libwinpr/winsock/winsock.c create mode 100644 winpr/libwinpr/wnd/CMakeLists.txt create mode 100644 winpr/libwinpr/wnd/ModuleOptions.cmake create mode 100644 winpr/libwinpr/wnd/test/.gitignore create mode 100644 winpr/libwinpr/wnd/test/CMakeLists.txt create mode 100644 winpr/libwinpr/wnd/test/TestWndCreateWindowEx.c create mode 100644 winpr/libwinpr/wnd/test/TestWndWmCopyData.c create mode 100644 winpr/libwinpr/wnd/wnd.c create mode 100644 winpr/libwinpr/wnd/wnd.h create mode 100644 winpr/libwinpr/wtsapi/CMakeLists.txt create mode 100644 winpr/libwinpr/wtsapi/ModuleOptions.cmake create mode 100644 winpr/libwinpr/wtsapi/test/.gitignore create mode 100644 winpr/libwinpr/wtsapi/test/CMakeLists.txt create mode 100644 winpr/libwinpr/wtsapi/test/TestWtsApiEnumerateProcesses.c create mode 100644 winpr/libwinpr/wtsapi/test/TestWtsApiEnumerateSessions.c create mode 100644 winpr/libwinpr/wtsapi/test/TestWtsApiExtraDisconnectSession.c create mode 100644 winpr/libwinpr/wtsapi/test/TestWtsApiExtraDynamicVirtualChannel.c create mode 100644 winpr/libwinpr/wtsapi/test/TestWtsApiExtraLogoffSession.c create mode 100644 winpr/libwinpr/wtsapi/test/TestWtsApiExtraSendMessage.c create mode 100644 winpr/libwinpr/wtsapi/test/TestWtsApiExtraStartRemoteSessionEx.c create mode 100644 winpr/libwinpr/wtsapi/test/TestWtsApiExtraVirtualChannel.c create mode 100644 winpr/libwinpr/wtsapi/test/TestWtsApiQuerySessionInformation.c create mode 100644 winpr/libwinpr/wtsapi/test/TestWtsApiSessionNotification.c create mode 100644 winpr/libwinpr/wtsapi/test/TestWtsApiShutdownSystem.c create mode 100644 winpr/libwinpr/wtsapi/test/TestWtsApiWaitSystemEvent.c create mode 100644 winpr/libwinpr/wtsapi/wtsapi.c create mode 100644 winpr/libwinpr/wtsapi/wtsapi_win32.c create mode 100644 winpr/libwinpr/wtsapi/wtsapi_win32.h create mode 100644 winpr/test/.gitignore create mode 100644 winpr/test/CMakeLists.txt create mode 100644 winpr/test/TestIntrinsics.c create mode 100644 winpr/test/TestTypes.c create mode 100644 winpr/tools/.gitignore create mode 100644 winpr/tools/CMakeLists.txt create mode 100644 winpr/tools/hash-cli/CMakeLists.txt create mode 100644 winpr/tools/hash-cli/hash.c create mode 100644 winpr/tools/hash-cli/winpr-hash.1.in create mode 100644 winpr/tools/makecert-cli/CMakeLists.txt create mode 100644 winpr/tools/makecert-cli/main.c create mode 100644 winpr/tools/makecert-cli/winpr-makecert.1.in create mode 100644 winpr/tools/makecert/.gitignore create mode 100644 winpr/tools/makecert/CMakeLists.txt create mode 100644 winpr/tools/makecert/makecert.c create mode 100644 winpr/tools/winpr-tools.pc.in create mode 100644 winpr/winpr.pc.in create mode 100644 winpr/wlog.7 diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..33a6584 --- /dev/null +++ b/.clang-format @@ -0,0 +1,125 @@ +--- +AccessModifierOffset: -2 +AlignAfterOpenBracket: Align +AlignConsecutiveAssignments: false +AlignConsecutiveDeclarations: false +AlignEscapedNewlines: Left +AlignOperands: true +AlignTrailingComments: true +AllowAllParametersOfDeclarationOnNextLine: true +AllowShortBlocksOnASingleLine: false +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: None +AllowShortIfStatementsOnASingleLine: false +AllowShortLoopsOnASingleLine: false +AlwaysBreakAfterDefinitionReturnType: None +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: false +AlwaysBreakTemplateDeclarations: false +BinPackArguments: true +BinPackParameters: true +BraceWrapping: + AfterClass: true + AfterControlStatement: true + AfterEnum: true + AfterFunction: true + AfterNamespace: true + AfterObjCDeclaration: true + AfterStruct: true + AfterUnion: true + AfterExternBlock: true + BeforeCatch: true + BeforeElse: true + IndentBraces: false + SplitEmptyFunction: true + SplitEmptyRecord: true + SplitEmptyNamespace: true +BreakBeforeBinaryOperators: None +BreakBeforeBraces: Allman +BreakBeforeInheritanceComma: false +BreakBeforeTernaryOperators: true +BreakConstructorInitializersBeforeComma: false +BreakConstructorInitializers: BeforeColon +BreakStringLiterals: true +ColumnLimit: 100 +CommentPragmas: '^ IWYU pragma:' +CompactNamespaces: false +ConstructorInitializerAllOnOneLineOrOnePerLine: false +ConstructorInitializerIndentWidth: 4 +ContinuationIndentWidth: 4 +Cpp11BracedListStyle: false +DerivePointerAlignment: false +DisableFormat: false +ExperimentalAutoDetectBinPacking: false +FixNamespaceComments: false +IncludeBlocks: Preserve +IncludeCategories: + - Regex: '^"(llvm|llvm-c|clang|clang-c)/' + Priority: 2 + - Regex: '^(<|"(gtest|gmock|isl|json)/)' + Priority: 3 + - Regex: '.*' + Priority: 1 +IncludeIsMainRegex: '(Test)?$' +IndentCaseLabels: true +IndentPPDirectives: None +IndentWidth: 4 +IndentWrappedFunctionNames: false +KeepEmptyLinesAtTheStartOfBlocks: true +MacroBlockBegin: '' +MacroBlockEnd: '' +MaxEmptyLinesToKeep: 1 +PenaltyBreakAssignment: 2 +PenaltyBreakBeforeFirstCallParameter: 19 +PenaltyBreakComment: 300 +PenaltyBreakFirstLessLess: 120 +PenaltyBreakString: 1000 +PenaltyExcessCharacter: 1000000 +PenaltyReturnTypeOnItsOwnLine: 60 +PointerAlignment: Left +ReflowComments: true +SortIncludes: false +SortUsingDeclarations: true +SpaceAfterCStyleCast: false +SpaceAfterTemplateKeyword: true +SpaceBeforeAssignmentOperators: true +SpaceBeforeParens: ControlStatements +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 1 +SpacesInAngles: false +SpacesInContainerLiterals: false +SpacesInCStyleCastParentheses: false +SpacesInParentheses: false +SpacesInSquareBrackets: false +TabWidth: 4 +UseTab: ForIndentation +... +Language: Cpp +Standard: Auto +NamespaceIndentation: All +ForEachMacros: + - foreach + - Q_FOREACH + - BOOST_FOREACH +... +Language: ObjC +PointerBindsToType: false +ObjCSpaceAfterProperty: true +SortIncludes: false +ObjCBlockIndentWidth: 4 +ObjCSpaceAfterProperty: false +ObjCSpaceBeforeProtocolList: true +... +Language: Java +BreakAfterJavaFieldAnnotations: false +... +Language: JavaScript +JavaScriptQuotes: Leave +JavaScriptWrapImports: true +... +Language: Proto +... +Language: TableGen +... +Language: TextProto +... diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 0000000..77704ee --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,25 @@ +## Found a bug? - We would like to help you and smash the bug away. +1. __Please don't "report" questions as bugs.__ + * We are reachable via IRC _#freerdp on freenode_ + * We are reachable via mailing list + * Try our mailing list for discussions/questions +1. Before reporting a bug have a look into our issue tracker to see if the bug was already reported and you can add some additional information. +1. If it's a __new__ bug - create a new issue. +1. For more details see https://github.com/FreeRDP/FreeRDP/wiki/BugReporting + +## To save time and help us identify the issue a bug report should at least contain the following: + * a useful description of the bug - "It's not working" isn't good enough - you must try harder ;) + * the steps to reproduce the bug + * command line you have used + * to what system did you connect to? (win8, 2008, ..) + * what did you expect to happen? + * what actually happened? + * freerdp version (e.g. xfreerdp --version) or package version or git commit + * freerdp configuration (e.g. xfreerdp --buildconfig) + * operating System, architecture, distribution e.g. linux, amd64, debian + * if you built it yourself add some notes which branch you have used, also your cmake parameters can help + * extra information helping us to find the bug + +## Please remove this text before submitting your issue! + +_Thank you for reporting a bug!_ diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..8ee9f39 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,56 @@ +--- +name: Bug report +about: Create a report to help us improve + +--- + +**Found a bug? - We would like to help you and smash the bug away.** +1. __Please don't "report" questions as bugs. For these (questions/build instructions/...) please use one of the following means of contact:__ + * We are reachable via IRC _#freerdp on freenode_ + * We are reachable via mailing list + * Try our mailing list for discussions/questions +1. Before reporting a bug have a look into our issue tracker to see if the bug was already reported and you can add some additional information. +1. If it's a __new__ bug - create a new issue. +1. For more details see https://github.com/FreeRDP/FreeRDP/wiki/BugReporting + + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Application details** +* Version of FreeRDP +* Command line used +* output of `/buildconfig` +* OS version connecting to +* If available the log output from a run with `/log-level:trace` + +**Desktop (please complete the following information):** + - OS: [e.g. iOS] + - Browser [e.g. chrome, safari] + - Version [e.g. 22] + +**Smartphone (please complete the following information):** + - Device: [e.g. iPhone6] + - OS: [e.g. iOS8.1] + - Browser [e.g. stock browser, safari] + - Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. + +** Please remove this text before submitting your issue! + +_Thank you for reporting a bug!_ diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..066b2d9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,17 @@ +--- +name: Feature request +about: Suggest an idea for this project + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..91a1cef --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,25 @@ +## This is how are pull requests handled by FreeRDP +1. Every new pull request needs to build and pass the unit tests at https://ci.freerdp.com +1. At least 1 (better two) people need to review and test a pull request and agree to accept + +## Preparations before creating a pull +* Rebase your branch to current master, no merges allowed! +* Try to clean up your commit history, group changes to commits +* Check your formatting! A _clang-format_ script can be found at ```.clang-format``` + * The cmake target ```clangformat``` reformats the whole codebase +* Optional (but higly recommended) + * Run a clang scanbuild before and after your changes to avoid introducing new bugs + * Run your compiler at pedantic level to check for new warnings + +## To ease accepting your contribution +* Give the pull request a proper name so people looking at it have an basic idea what it is for +* Add at least a brief description what it does (or should do :) and what it's good for +* Give instructions on how to test your changes +* Ideally add unit tests if adding new features + +## What you should be prepared for +* fix issues found during the review phase +* Joining IRC _#freerdp_ to talk to other developers or help them test your pull might accelerate acceptance +* Joining our mailing list may be helpful too. + +## Please remove this text before submitting your pull! diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 0000000..aa6df82 --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,71 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ master, stable* ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ master, stable* ] + schedule: + - cron: '30 8 * * 6' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + language: [ 'cpp' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] + # Learn more: + # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v1 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + # - name: Autobuild + # uses: github/codeql-action/autobuild@v1 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + - run: | + sudo apt update + sudo apt install libxrandr-dev libxinerama-dev libusb-1.0-0-dev xserver-xorg-dev libswscale-dev libswresample-dev libavutil-dev libavcodec-dev libcups2-dev libpulse-dev libasound2-dev libpcsclite-dev xsltproc libxcb-cursor-dev libxcursor-dev libcairo2-dev libfaac-dev libfaad-dev libjpeg-dev libgsm1-dev ninja-build libxfixes-dev libxkbcommon-dev libwayland-dev libpam0g-dev libxdamage-dev libxcb-damage0-dev ccache libxtst-dev libfuse-dev libsystemd-dev libcairo2-dev libsoxr-dev + mkdir ci-build + cd ci-build + cmake -GNinja ../ci/cmake-preloads/config-linux-all.txt .. + cmake --build . + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v1 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c844922 --- /dev/null +++ b/.gitignore @@ -0,0 +1,154 @@ +#ninja +.ninja_deps +.ninja_log +build.ninja +rules.ninja + +# CMake +CMakeFiles/ +CMakeScripts/ +CMakeCache.txt +config.h +install_manifest*.txt +CTestTestfile.cmake +*.pc +Makefile +Testing +cmake_install.cmake +CPackConfig.cmake +CPackSourceConfig.cmake +DartConfiguration.tcl +CMakeCPackOptions.cmake +_CPack_Packages +LICENSE.txt +/external/* +!external/README +*Config.cmake +*ConfigVersion.cmake +include/freerdp/version.h +include/freerdp/build-config.h +buildflags.h + +*.a.objlist.cmake +*.a.objlist +*.a.objdir +*_dummy.c +*_dummy.c.base + +# Eclipse +*.project +*.cproject +*.settings + +nbproject/ +compile_commands.json + +# .rdp files +*.rdp +*.RDP + +# Documentation +docs/api +client/X11/xfreerdp.1 +client/X11/xfreerdp.1.xml + +# Mac OS X +.DS_Store +*.xcodeproj/ +DerivedData/ + +# iOS +FreeRDP.build +Debug-* +Release-* + +# Windows +*.vcxproj +*.vcxproj.* +*.vcproj +*.vcproj.* +*.aps +*.sdf +*.sln +*.suo +*.ncb +*.opensdf +Thumbs.db +ipch +Debug +RelWithDebInfo +*.lib +*.exp +*.pdb +*.dll +*.ilk +*.resource.txt +*.embed.manifest* +*.intermediate.manifest* +version.rc +*.VC.db +*.VC.opendb + +# Binaries +*.a +*.o +*.so +*.so.* +*.dylib +bin +libs +cunit/test_freerdp +client/X11/xfreerdp +client/Mac/xcode +client/Sample/sfreerdp +client/Wayland/wlfreerdp +server/Sample/sfreerdp-server +server/X11/xfreerdp-server +server/proxy/freerdp-proxy +xcode +libfreerdp/codec/test/TestOpenH264ASM + +# Other +*~ +*.dir +Release +Win32 +build*/ +*.orig +*.msrcIncident + +default.log +*Amplifier XE* +*Inspector XE* + +*.cbp +*.txt.user + +*.autosave + +# etags +TAGS + +# generated packages +*.zip +*.exe +#*.sh +*.deb +*.rpm +*.dmg +*.tar.Z +*.tar.gz + +# packaging related files +!packaging/**.sh +packaging/deb/freerdp-nightly/freerdp-nightly +packaging/deb/freerdp-nightly/freerdp-nightly-dev +packaging/deb/freerdp-nightly/freerdp-nightly-dbg +.source_version + +# +.idea + +# VisualStudio Code +.vscode +cache/ diff --git a/.source_version b/.source_version new file mode 100644 index 0000000..dbe5900 --- /dev/null +++ b/.source_version @@ -0,0 +1 @@ +2.8.1 diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..b5dc259 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,56 @@ +sudo: required +dist: trusty + +os: linux + +language: c + +compiler: + - gcc + +matrix: + include: + - os: linux + compiler: gcc + - os: linux + compiler: clang + exclude: + - compiler: gcc + +addons: + apt: + packages: + - gdb + - libx11-dev + - libxrandr-dev + - libxi-dev + - libxv-dev + - libcups2-dev + - libxdamage-dev + - libxcursor-dev + - libxext-dev + - libxinerama-dev + - libxkbcommon-dev + - libxkbfile-dev + - libxml2-dev + - libasound2-dev + - libgstreamer1.0-dev + - libgstreamer-plugins-base1.0-dev + - libpulse-dev + - libpcsclite-dev + - libgsm1-dev + - libavcodec-dev + - libavutil-dev + - libxext-dev + - ninja-build + - libsystemd-dev + - libwayland-dev + +before_script: + - ulimit -c unlimited -S + +script: + - sudo hostname travis-ci.local + - cmake -G Ninja -C ci/cmake-preloads/config-linux-all.txt -D CMAKE_BUILD_TYPE=Debug . + - make + - make test diff --git a/CMakeCPack.cmake b/CMakeCPack.cmake new file mode 100644 index 0000000..ca749c0 --- /dev/null +++ b/CMakeCPack.cmake @@ -0,0 +1,102 @@ + +# Generate .txt license file for CPack (PackageMaker requires a file extension) +configure_file(${CMAKE_CURRENT_SOURCE_DIR}/LICENSE ${CMAKE_CURRENT_BINARY_DIR}/LICENSE.txt @ONLY) + +SET(CPACK_BINARY_ZIP "ON") + +# Workaround to remove c++ compiler macros and defines for Eclipse. +# If c++ macros/defines are set __cplusplus is also set which causes +# problems when compiling freerdp/jni. To prevent this problem we set the macros to "". + +if (ANDROID AND CMAKE_EXTRA_GENERATOR STREQUAL "Eclipse CDT4") + set(CMAKE_EXTRA_GENERATOR_CXX_SYSTEM_DEFINED_MACROS "") + message(STATUS "Disabled CXX system defines for eclipse (workaround).") +endif() + +set(CPACK_SOURCE_IGNORE_FILES "/\\\\.git/;/\\\\.gitignore;/CMakeCache.txt") + +if(NOT WIN32) + if(APPLE AND (NOT IOS)) + + if(WITH_SERVER) + set(CPACK_PACKAGE_EXECUTABLES ${CPACK_PACKAGE_EXECUTABLES} "mfreerdp-server") + endif() + endif() + + if(WITH_X11) + set(CPACK_PACKAGE_EXECUTABLES "xfreerdp") + + if(WITH_SERVER) + set(CPACK_PACKAGE_EXECUTABLES ${CPACK_PACKAGE_EXECUTABLES} "xfreerdp-server") + endif() + endif() +endif() + +set(CPACK_SYSTEM_NAME "${CMAKE_SYSTEM_NAME}-${CMAKE_SYSTEM_PROCESSOR}") +set(CPACK_TOPLEVEL_TAG "${CMAKE_SYSTEM_NAME}-${CMAKE_SYSTEM_PROCESSOR}") + +string(TOLOWER ${CMAKE_PROJECT_NAME} CMAKE_PROJECT_NAME_lower) +set(CPACK_PACKAGE_FILE_NAME "${CMAKE_PROJECT_NAME_lower}-${FREERDP_VERSION_FULL}-${CPACK_SYSTEM_NAME}") +set(CPACK_SOURCE_PACKAGE_FILE_NAME "${CMAKE_PROJECT_NAME_lower}-${FREERDP_VERSION_FULL}-${CPACK_SYSTEM_NAME}") + +set(CPACK_PACKAGE_NAME "FreeRDP") +set(CPACK_PACKAGE_VENDOR "FreeRDP") +set(CPACK_PACKAGE_VERSION ${FREERDP_VERSION_FULL}) +set(CPACK_PACKAGE_VERSION_MAJOR ${FREERDP_VERSION_MAJOR}) +set(CPACK_PACKAGE_VERSION_MINOR ${FREERDP_VERSION_MINOR}) +set(CPACK_PACKAGE_VERSION_PATCH ${FREERDP_VERSION_REVISION}) +SET(CPACK_PACKAGE_DESCRIPTION_SUMMARY "FreeRDP: A Remote Desktop Protocol Implementation") + +set(CPACK_PACKAGE_CONTACT "Marc-Andre Moreau") +set(CPACK_DEBIAN_PACKAGE_MAINTAINER "marcandre.moreau@gmail.com") +set(CPACK_DEBIAN_ARCHITECTURE ${CMAKE_SYSTEM_PROCESSOR}) + +set(CPACK_PACKAGE_INSTALL_DIRECTORY "FreeRDP") +set(CPACK_PACKAGE_DESCRIPTION_FILE "${CMAKE_CURRENT_BINARY_DIR}/LICENSE.txt") +set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_CURRENT_BINARY_DIR}/LICENSE.txt") + +set(CPACK_NSIS_MODIFY_PATH ON) +set(CPACK_PACKAGE_ICON "${CMAKE_SOURCE_DIR}/resources\\\\FreeRDP_Install.bmp") +set(CPACK_NSIS_MUI_ICON "${CMAKE_SOURCE_DIR}/resources\\\\FreeRDP_Icon_96px.ico") +set(CPACK_NSIS_MUI_UNICON "${CMAKE_SOURCE_DIR}/resource\\\\FreeRDP_Icon_96px.ico") + +set(CPACK_COMPONENTS_ALL client server libraries headers symbols tools) + +if(MSVC) + if(MSVC_RUNTIME STREQUAL "dynamic") + set(CMAKE_INSTALL_SYSTEM_RUNTIME_LIBS_SKIP TRUE) + include(InstallRequiredSystemLibraries) + + install(PROGRAMS ${CMAKE_INSTALL_SYSTEM_RUNTIME_LIBS} + DESTINATION ${CMAKE_INSTALL_BINDIR} + COMPONENT libraries) + endif() +endif() + +set(CPACK_COMPONENT_CLIENT_DISPLAY_NAME "Client") +set(CPACK_COMPONENT_CLIENT_GROUP "Applications") + +set(CPACK_COMPONENT_SERVER_DISPLAY_NAME "Server") +set(CPACK_COMPONENT_SERVER_GROUP "Applications") + +set(CPACK_COMPONENT_LIBRARIES_DISPLAY_NAME "Libraries") +set(CPACK_COMPONENT_LIBRARIES_GROUP "Runtime") + +set(CPACK_COMPONENT_HEADERS_DISPLAY_NAME "Headers") +set(CPACK_COMPONENT_HEADERS_GROUP "Development") + +set(CPACK_COMPONENT_SYMBOLS_DISPLAY_NAME "Symbols") +set(CPACK_COMPONENT_SYMBOLS_GROUP "Development") + +set(CPACK_COMPONENT_TOOLS_DISPLAY_NAME "Tools") +set(CPACK_COMPONENT_TOOLS_GROUP "Applications") + +set(CPACK_COMPONENT_GROUP_RUNTIME_DESCRIPTION "Runtime") +set(CPACK_COMPONENT_GROUP_APPLICATIONS_DESCRIPTION "Applications") +set(CPACK_COMPONENT_GROUP_DEVELOPMENT_DESCRIPTION "Development") + +configure_file("${CMAKE_SOURCE_DIR}/CMakeCPackOptions.cmake.in" + "${CMAKE_BINARY_DIR}/CMakeCPackOptions.cmake" @ONLY) +set(CPACK_PROJECT_CONFIG_FILE "${CMAKE_BINARY_DIR}/CMakeCPackOptions.cmake") + +include(CPack) diff --git a/CMakeCPackOptions.cmake.in b/CMakeCPackOptions.cmake.in new file mode 100644 index 0000000..826eaa1 --- /dev/null +++ b/CMakeCPackOptions.cmake.in @@ -0,0 +1,10 @@ +# This file is configured at cmake time, and loaded at cpack time. +# To pass variables to cpack from cmake, they must be configured in this file. + +if("${CPACK_GENERATOR}" STREQUAL "PackageMaker") + if(CMAKE_PACKAGE_QTGUI) + set(CPACK_PACKAGE_DEFAULT_LOCATION "/Applications") + else() + set(CPACK_PACKAGE_DEFAULT_LOCATION "/usr") + endif() +endif() diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..aa1131d --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,1117 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2011 O.S. Systems Software Ltda. +# Copyright 2011 Otavio Salvador +# Copyright 2011 Marc-Andre Moreau +# Copyright 2012 HP Development Company, LLC +# +# Licensed 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. + +cmake_minimum_required(VERSION 2.8) + +project(FreeRDP C CXX) + +if(NOT DEFINED VENDOR) + set(VENDOR "FreeRDP" CACHE STRING "FreeRDP package vendor") +endif() + +if(NOT DEFINED PRODUCT) + set(PRODUCT "FreeRDP" CACHE STRING "FreeRDP package name") +endif() + +if(NOT DEFINED FREERDP_VENDOR) + set(FREERDP_VENDOR 1) +endif() + +set(CMAKE_COLOR_MAKEFILE ON) + +set(CMAKE_POSITION_INDEPENDENT_CODE ON) + +# Include our extra modules +set(CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake/) + +if((CMAKE_SYSTEM_NAME MATCHES "WindowsStore") AND (CMAKE_SYSTEM_VERSION MATCHES "10.0")) + set(UWP 1) + add_definitions("-D_UWP") + set(CMAKE_WINDOWS_VERSION "WIN10") +endif() + +# Check for cmake compatibility (enable/disable features) +include(CheckCmakeCompat) + +# Include cmake modules +if(WITH_CLANG_FORMAT) + include(ClangFormat) +endif() + +include(CheckIncludeFiles) +include(CheckLibraryExists) +include(CheckSymbolExists) +include(CheckStructHasMember) +include(FindPkgConfig) +include(TestBigEndian) + +include(FindFeature) +include(ConfigOptions) +include(ComplexLibrary) +include(FeatureSummary) +include(CheckCCompilerFlag) +include(CheckCXXCompilerFlag) +include(GNUInstallDirsWrapper) +include(CMakePackageConfigHelpers) +include(InstallFreeRDPMan) +include(GetGitRevisionDescription) +include(SetFreeRDPCMakeInstallDir) + +if (DEFINE_NO_DEPRECATED) + add_definitions(-DDEFINE_NO_DEPRECATED) +endif() + +# Soname versioning +set(BUILD_NUMBER 0) +if ($ENV{BUILD_NUMBER}) + set(BUILD_NUMBER $ENV{BUILD_NUMBER}) +endif() +set(WITH_LIBRARY_VERSIONING "ON") + +set(RAW_VERSION_STRING "2.8.1") +if(EXISTS "${CMAKE_SOURCE_DIR}/.source_tag") + file(READ ${CMAKE_SOURCE_DIR}/.source_tag RAW_VERSION_STRING) +elseif(USE_VERSION_FROM_GIT_TAG) + git_get_exact_tag(_GIT_TAG --tags --always) + if (NOT ${_GIT_TAG} STREQUAL "n/a") + set(RAW_VERSION_STRING ${_GIT_TAG}) + endif() +endif() +string(STRIP ${RAW_VERSION_STRING} RAW_VERSION_STRING) + +set(VERSION_REGEX "^.?([0-9]+)\\.([0-9]+)\\.([0-9]+)-?(.*)") +string(REGEX REPLACE "${VERSION_REGEX}" "\\1" FREERDP_VERSION_MAJOR "${RAW_VERSION_STRING}") +string(REGEX REPLACE "${VERSION_REGEX}" "\\2" FREERDP_VERSION_MINOR "${RAW_VERSION_STRING}") +string(REGEX REPLACE "${VERSION_REGEX}" "\\3" FREERDP_VERSION_REVISION "${RAW_VERSION_STRING}") +string(REGEX REPLACE "${VERSION_REGEX}" "\\4" FREERDP_VERSION_SUFFIX "${RAW_VERSION_STRING}") + +set(FREERDP_API_VERSION "${FREERDP_VERSION_MAJOR}") +set(FREERDP_VERSION "${FREERDP_VERSION_MAJOR}.${FREERDP_VERSION_MINOR}.${FREERDP_VERSION_REVISION}") +if (FREERDP_VERSION_SUFFIX) + set(FREERDP_VERSION_FULL "${FREERDP_VERSION}-${FREERDP_VERSION_SUFFIX}") +else() + set(FREERDP_VERSION_FULL "${FREERDP_VERSION}") +endif() +message("FREERDP_VERSION=${FREERDP_VERSION_FULL}") + +if(EXISTS "${PROJECT_SOURCE_DIR}/.source_version" ) + file(READ ${PROJECT_SOURCE_DIR}/.source_version GIT_REVISION) + + string(STRIP ${GIT_REVISION} GIT_REVISION) +elseif(USE_VERSION_FROM_GIT_TAG) + git_get_exact_tag(GIT_REVISION --tags --always) + + if (${GIT_REVISION} STREQUAL "n/a") + git_rev_parse (GIT_REVISION --short) + endif() +endif() + +if (NOT GIT_REVISION) + set(GIT_REVISION ${FREERDP_VERSION}) +endif() + +message(STATUS "Git Revision ${GIT_REVISION}") + +set(FREERDP_INCLUDE_DIR "include/freerdp${FREERDP_VERSION_MAJOR}/") + +# Compatibility options +if(DEFINED STATIC_CHANNELS) + message(WARNING "STATIC_CHANNELS is obsolete, please use BUILTIN_CHANNELS instead") + set(BUILTIN_CHANNELS ${STATIC_CHANNELS} CACHE BOOL "" FORCE) +endif() + +# Make paths absolute +if (CMAKE_INSTALL_PREFIX) + get_filename_component(CMAKE_INSTALL_PREFIX "${CMAKE_INSTALL_PREFIX}" ABSOLUTE) +endif() +if (FREERDP_EXTERNAL_PATH) + get_filename_component (FREERDP_EXTERNAL_PATH "${FREERDP_EXTERNAL_PATH}" ABSOLUTE) +endif() + +# Allow to search the host machine for git/ccache +if(CMAKE_CROSSCOMPILING) + SET(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM BOTH) +endif(CMAKE_CROSSCOMPILING) + +find_program(CCACHE ccache) +if(CCACHE AND WITH_CCACHE) + if(CMAKE_VERSION VERSION_GREATER 3.3.2) + if(NOT DEFINED CMAKE_C_COMPILER_LAUNCHER) + SET(CMAKE_C_COMPILER_LAUNCHER ${CCACHE}) + endif(NOT DEFINED CMAKE_C_COMPILER_LAUNCHER) + if(NOT DEFINED CMAKE_CXX_COMPILER_LAUNCHER) + SET(CMAKE_CXX_COMPILER_LAUNCHER ${CCACHE}) + endif(NOT DEFINED CMAKE_CXX_COMPILER_LAUNCHER) + else() + set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE ${CCACHE}) + set_property(GLOBAL PROPERTY RULE_LAUNCH_LINK ${CCACHE}) + endif() +endif(CCACHE AND WITH_CCACHE) + +if(CMAKE_CROSSCOMPILING) + SET (CMAKE_FIND_ROOT_PATH_MODE_PROGRAM ONLY) +endif(CMAKE_CROSSCOMPILING) +# /Allow to search the host machine for git/ccache + +# Turn on solution folders (2.8.4+) +set_property(GLOBAL PROPERTY USE_FOLDERS ON) + +# Default to release build type +if(NOT CMAKE_BUILD_TYPE) + set(CMAKE_BUILD_TYPE "Release") +endif() + +if(NOT DEFINED BUILD_SHARED_LIBS) + if(IOS) + set(BUILD_SHARED_LIBS OFF) + else() + set(BUILD_SHARED_LIBS ON) + endif() +endif() + +if(BUILD_TESTING) + set(EXPORT_ALL_SYMBOLS TRUE) +elseif(NOT DEFINED EXPORT_ALL_SYMBOLS) + set(EXPORT_ALL_SYMBOLS FALSE) +endif() + +if (EXPORT_ALL_SYMBOLS) +# set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON) + add_definitions(-DFREERDP_TEST_EXPORTS -DBUILD_TESTING) +endif(EXPORT_ALL_SYMBOLS) + +# BSD +if(${CMAKE_SYSTEM_NAME} MATCHES "BSD") + set(BSD TRUE) + if(${CMAKE_SYSTEM_NAME} MATCHES "FreeBSD") + set(FREEBSD TRUE) + endif() + if(${CMAKE_SYSTEM_NAME} MATCHES "kFreeBSD") + set(KFREEBSD TRUE) + endif() + if(${CMAKE_SYSTEM_NAME} MATCHES "OpenBSD") + set(OPENBSD TRUE) + endif() +endif() + +if(${CMAKE_SYSTEM_NAME} MATCHES "DragonFly") + set(BSD TRUE) + set(FREEBSD TRUE) +endif() + +if(FREEBSD) + find_path(EPOLLSHIM_INCLUDE_DIR NAMES sys/epoll.h sys/timerfd.h HINTS /usr/local/include/libepoll-shim) + find_library(EPOLLSHIM_LIBS NAMES epoll-shim libepoll-shim HINTS /usr/local/lib) +endif() + +# Configure MSVC Runtime +if(MSVC) + include(MSVCRuntime) + if(NOT DEFINED MSVC_RUNTIME) + set(MSVC_RUNTIME "dynamic") + endif() + if(MSVC_RUNTIME STREQUAL "static") + if(BUILD_SHARED_LIBS) + message(FATAL_ERROR "Static CRT is only supported in a fully static build") + endif() + message(STATUS "Use the MSVC static runtime option carefully!") + message(STATUS "OpenSSL uses /MD by default, and is very picky") + message(STATUS "Random freeing errors are a common sign of runtime issues") + endif() + configure_msvc_runtime() + + if(NOT DEFINED CMAKE_SUPPRESS_REGENERATION) + set(CMAKE_SUPPRESS_REGENERATION ON) + endif() +endif() + +# Enable 64bit file support on linux and FreeBSD. +if("${CMAKE_SYSTEM_NAME}" MATCHES "Linux" OR FREEBSD) + add_definitions("-D_FILE_OFFSET_BITS=64") +endif() + +# Use Standard conforming getpwnam_r() on Solaris. +if("${CMAKE_SYSTEM_NAME}" MATCHES "SunOS") + add_definitions("-D_POSIX_PTHREAD_SEMANTICS") +endif() + +# Compiler-specific flags +if(CMAKE_COMPILER_IS_GNUCC) + if(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64" OR CMAKE_SYSTEM_PROCESSOR MATCHES "i686") + CHECK_SYMBOL_EXISTS(__x86_64__ "" IS_X86_64) + if(IS_X86_64) + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fPIC") + else() + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -march=i686") + endif() + else() + if(CMAKE_POSITION_INDEPENDENT_CODE) + if(${CMAKE_VERSION} VERSION_LESS 2.8.9) + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fPIC") + endif() + endif() + endif() + + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall") + + CHECK_C_COMPILER_FLAG (-Wno-unused-result Wno-unused-result) + if(Wno-unused-result) + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wno-unused-result") + endif() + CHECK_C_COMPILER_FLAG (-Wno-unused-but-set-variable Wno-unused-but-set-variable) + if(Wno-unused-but-set-variable) + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wno-unused-but-set-variable") + endif() + CHECK_C_COMPILER_FLAG(-Wno-deprecated-declarations Wno-deprecated-declarations) + if(Wno-deprecated-declarations) + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wno-deprecated-declarations") + endif() + CHECK_CXX_COMPILER_FLAG(-Wno-deprecated-declarations Wno-deprecated-declarationsCXX) + if(Wno-deprecated-declarationsCXX) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-deprecated-declarations") + endif() + + if(NOT EXPORT_ALL_SYMBOLS) + message(STATUS "GCC default symbol visibility: hidden") + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fvisibility=hidden") + endif() + if(BUILD_TESTING) + CHECK_C_COMPILER_FLAG(-Wno-format Wno-format) + if(Wno-format) + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wno-format") + endif() + endif() + CHECK_C_COMPILER_FLAG (-Wimplicit-function-declaration Wimplicit-function-declaration) + if(Wimplicit-function-declaration) + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wimplicit-function-declaration") + endif() + + if (NOT OPENBSD) + CHECK_C_COMPILER_FLAG (-Wredundant-decls Wredundant-decls) + if(Wredundant-decls) + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wredundant-decls") + endif() + endif() + if(CMAKE_BUILD_TYPE STREQUAL "Release") + add_definitions(-DNDEBUG) + else() + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -g") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -g") + endif() +endif() + +# When building with Unix Makefiles and doing any release builds +# try to set __FILE__ to relative paths via a make specific macro +if (CMAKE_GENERATOR MATCHES "Unix Makefile*") + if(CMAKE_BUILD_TYPE STREQUAL "Release" OR CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo") + string(TOUPPER ${CMAKE_BUILD_TYPE} UPPER_BUILD_TYPE) + CHECK_C_COMPILER_FLAG (-Wno-builtin-macro-redefined Wno-builtin-macro-redefined) + if(Wno-builtin-macro-redefined) + set(CMAKE_C_FLAGS_${UPPER_BUILD_TYPE} "${CMAKE_C_FLAGS_${UPPER_BUILD_TYPE}} -Wno-builtin-macro-redefined -D__FILE__='\"$(subst ${CMAKE_BINARY_DIR}/,,$(subst ${CMAKE_SOURCE_DIR}/,,$(abspath $<)))\"'") + endif() + + CHECK_CXX_COMPILER_FLAG (-Wno-builtin-macro-redefined Wno-builtin-macro-redefinedCXX) + if(Wno-builtin-macro-redefinedCXX) + set(CMAKE_CXX_FLAGS_${UPPER_BUILD_TYPE} "${CMAKE_CXX_FLAGS_${UPPER_BUILD_TYPE}} -Wno-builtin-macro-redefined -D__FILE__='\"$(subst ${CMAKE_BINARY_DIR}/,,$(subst ${CMAKE_SOURCE_DIR}/,,$(abspath $<)))\"'") + endif() + endif() +endif() + +if(${CMAKE_C_COMPILER_ID} STREQUAL "Clang") + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wno-unused-parameter") + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wno-unused-macros -Wno-padded") + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wno-c11-extensions -Wno-gnu") + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wno-unused-command-line-argument") + CHECK_C_COMPILER_FLAG(-Wno-deprecated-declarations Wno-deprecated-declarations) + if(Wno-deprecated-declarations) + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wno-deprecated-declarations") + endif() + CHECK_CXX_COMPILER_FLAG(-Wno-deprecated-declarations Wno-deprecated-declarationsCXX) + if(Wno-deprecated-declarationsCXX) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-deprecated-declarations") + endif() +endif() + +set(THREAD_PREFER_PTHREAD_FLAG TRUE) + +if(NOT IOS) + find_package(Threads REQUIRED) +endif() + +if(NOT WIN32 AND NOT IOS) + CHECK_SYMBOL_EXISTS(pthread_mutex_timedlock pthread.h HAVE_PTHREAD_MUTEX_TIMEDLOCK_SYMBOL) + if (NOT HAVE_PTHREAD_MUTEX_TIMEDLOCK_SYMBOL) + CHECK_LIBRARY_EXISTS(pthread pthread_mutex_timedlock "" HAVE_PTHREAD_MUTEX_TIMEDLOCK_LIB) + endif (NOT HAVE_PTHREAD_MUTEX_TIMEDLOCK_SYMBOL) + if (NOT HAVE_PTHREAD_MUTEX_TIMEDLOCK_LIB) + CHECK_LIBRARY_EXISTS(pthreads pthread_mutex_timedlock "" HAVE_PTHREAD_MUTEX_TIMEDLOCK_LIBS) + endif (NOT HAVE_PTHREAD_MUTEX_TIMEDLOCK_LIB) + + if (HAVE_PTHREAD_MUTEX_TIMEDLOCK_SYMBOL OR HAVE_PTHREAD_MUTEX_TIMEDLOCK_LIB OR HAVE_PTHREAD_MUTEX_TIMEDLOCK_LIBS) + set(HAVE_PTHREAD_MUTEX_TIMEDLOCK ON) + endif (HAVE_PTHREAD_MUTEX_TIMEDLOCK_SYMBOL OR HAVE_PTHREAD_MUTEX_TIMEDLOCK_LIB OR HAVE_PTHREAD_MUTEX_TIMEDLOCK_LIBS) +endif() + +# Enable address sanitizer, where supported and when required +if(${CMAKE_C_COMPILER_ID} STREQUAL "Clang" OR CMAKE_COMPILER_IS_GNUCC) + CHECK_C_COMPILER_FLAG ("-fno-omit-frame-pointer" fno-omit-frame-pointer) + + if (fno-omit-frame-pointer) + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fno-omit-frame-pointer") + endif() + + set(CMAKE_REQUIRED_LINK_OPTIONS_SAVED ${CMAKE_REQUIRED_LINK_OPTIONS}) + file(WRITE ${CMAKE_BINARY_DIR}/foo.txt "") + if(WITH_SANITIZE_ADDRESS) + list(APPEND CMAKE_REQUIRED_LINK_OPTIONS "-fsanitize=address") + CHECK_C_COMPILER_FLAG ("-fsanitize=address" fsanitize-address) + CHECK_C_COMPILER_FLAG ("-fsanitize-blacklist=${CMAKE_BINARY_DIR}/foo.txt" fsanitize-blacklist) + CHECK_C_COMPILER_FLAG ("-fsanitize-address-use-after-scope" fsanitize-address-use-after-scope) + + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fsanitize=address") + set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -fsanitize=address") + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fsanitize=address") + + if(fsanitize-blacklist) + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fsanitize-blacklist=${CMAKE_SOURCE_DIR}/scripts/blacklist-address-sanitizer.txt") + endif(fsanitize-blacklist) + + if(fsanitize-address-use-after-scope) + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fsanitize-address-use-after-scope") + endif(fsanitize-address-use-after-scope) + elseif(WITH_SANITIZE_MEMORY) + list(APPEND CMAKE_REQUIRED_LINK_OPTIONS "-fsanitize=memory") + CHECK_C_COMPILER_FLAG ("-fsanitize=memory" fsanitize-memory) + CHECK_C_COMPILER_FLAG ("-fsanitize-blacklist=${CMAKE_BINARY_DIR}/foo.txt" fsanitize-blacklist) + CHECK_C_COMPILER_FLAG ("-fsanitize-memory-use-after-dtor" fsanitize-memory-use-after-dtor) + CHECK_C_COMPILER_FLAG ("-fsanitize-memory-track-origins" fsanitize-memory-track-origins) + + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fsanitize=memory") + set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -fsanitize=memory") + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fsanitize=memory") + + if(fsanitize-blacklist) + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fsanitize-blacklist=${CMAKE_SOURCE_DIR}/scripts/blacklist-memory-sanitizer.txt") + endif(fsanitize-blacklist) + + if (fsanitize-memory-use-after-dtor) + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fsanitize-memory-use-after-dtor") + endif(fsanitize-memory-use-after-dtor) + + if (fsanitize-memory-track-origins) + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fsanitize-memory-track-origins") + endif(fsanitize-memory-track-origins) + elseif(WITH_SANITIZE_THREAD) + list(APPEND CMAKE_REQUIRED_LINK_OPTIONS "-fsanitize=thread") + CHECK_C_COMPILER_FLAG ("-fsanitize=thread" fsanitize-thread) + CHECK_C_COMPILER_FLAG ("-fsanitize-blacklist=${CMAKE_BINARY_DIR}/foo.txt" fsanitize-blacklist) + + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fsanitize=thread") + if(fsanitize-blacklist) + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fsanitize-blacklist=${CMAKE_SOURCE_DIR}/scripts/blacklist-thread-sanitizer.txt") + endif(fsanitize-blacklist) + set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -fsanitize=thread") + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fsanitize=thread") + endif() + + file(REMOVE ${CMAKE_BINARY_DIR}/foo.txt) + set(CMAKE_REQUIRED_LINK_OPTIONS ${CMAKE_REQUIRED_LINK_OPTIONS_SAVED}) + + if (WITH_NO_UNDEFINED) + CHECK_C_COMPILER_FLAG (-Wl,--no-undefined no-undefined) + + if(no-undefined) + SET(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,--no-undefined" ) + SET(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,--no-undefined" ) + endif() + endif() +endif() + +if(MSVC) + # Remove previous warning definitions, + # NMake is otherwise complaining. + foreach (flags_var_to_scrub + CMAKE_C_FLAGS + CMAKE_CXX_FLAGS + CMAKE_CXX_FLAGS_RELEASE + CMAKE_CXX_FLAGS_RELWITHDEBINFO + CMAKE_CXX_FLAGS_MINSIZEREL + CMAKE_C_FLAGS_RELEASE + CMAKE_C_FLAGS_RELWITHDEBINFO + CMAKE_C_FLAGS_MINSIZEREL) + string (REGEX REPLACE "(^| )[/-]W[ ]*[1-9]" " " + "${flags_var_to_scrub}" "${${flags_var_to_scrub}}") + endforeach() + + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /Gd") + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /W3") + + if(CMAKE_SIZEOF_VOID_P EQUAL 8) + add_definitions(-D_AMD64_) + else() + add_definitions(-D_X86_) + endif() + + set(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}) + set(LIBRARY_OUTPUT_PATH ${PROJECT_BINARY_DIR}) + + if(CMAKE_BUILD_TYPE STREQUAL "Release") + else() + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /Zi") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /Zi") + endif() + +endif() + +if(WIN32) + add_definitions(-DUNICODE -D_UNICODE) + add_definitions(-D_CRT_SECURE_NO_WARNINGS) + add_definitions(-DWIN32_LEAN_AND_MEAN) + add_definitions(-D_WINSOCK_DEPRECATED_NO_WARNINGS) + + set(CMAKE_USE_RELATIVE_PATH ON) + if (${CMAKE_GENERATOR} MATCHES "NMake Makefile*" OR ${CMAKE_GENERATOR} MATCHES "Ninja*" OR ${CMAKE_GENERATOR} MATCHES "Unix Makefiles") + set(CMAKE_PDB_BINARY_DIR ${CMAKE_BINARY_DIR}) + elseif (${CMAKE_GENERATOR} MATCHES "Visual Studio*") + set(CMAKE_PDB_BINARY_DIR "${CMAKE_BINARY_DIR}/\${CMAKE_INSTALL_CONFIG_NAME}") + else() + message(FATAL_ERROR "Unknown generator ${CMAKE_GENERATOR}") + endif() + + string(TIMESTAMP RC_VERSION_YEAR "%Y") + + if(NOT DEFINED CMAKE_WINDOWS_VERSION) + set(CMAKE_WINDOWS_VERSION "WIN7") + endif() + + if(CMAKE_WINDOWS_VERSION STREQUAL "WINXP") + add_definitions(-DWINVER=0x0501 -D_WIN32_WINNT=0x0501) + elseif(CMAKE_WINDOWS_VERSION STREQUAL "WIN7") + add_definitions(-DWINVER=0x0601 -D_WIN32_WINNT=0x0601) + elseif(CMAKE_WINDOWS_VERSION STREQUAL "WIN8") + add_definitions(-DWINVER=0x0602 -D_WIN32_WINNT=0x0602) + elseif(CMAKE_WINDOWS_VERSION STREQUAL "WIN10") + add_definitions(-DWINVER=0x0A00 -D_WIN32_WINNT=0x0A00) + endif() + + # Set product and vendor for dll and exe version information. + set(RC_VERSION_VENDOR ${VENDOR}) + set(RC_VERSION_PRODUCT ${PRODUCT}) + set(RC_VERSION_PATCH ${BUILD_NUMBER}) + set(RC_VERSION_DESCRIPTION "${FREERDP_VERSION_FULL} ${GIT_REVISION} ${CMAKE_WINDOWS_VERSION} ${CMAKE_SYSTEM_PROCESSOR}") + + if (FREERDP_EXTERNAL_SSL_PATH) + set(OPENSSL_ROOT_DIR ${FREERDP_EXTERNAL_SSL_PATH}) + endif() +endif() + +if(IOS) + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -isysroot ${CMAKE_IOS_SDK_ROOT} -g") +endif() + +add_definitions(-DWINPR_EXPORTS -DFREERDP_EXPORTS) + +# Include files +if(NOT IOS) + check_include_files(fcntl.h HAVE_FCNTL_H) + check_include_files(unistd.h HAVE_UNISTD_H) + check_include_files(inttypes.h HAVE_INTTYPES_H) + check_include_files(sys/modem.h HAVE_SYS_MODEM_H) + check_include_files(sys/filio.h HAVE_SYS_FILIO_H) + check_include_files(sys/sockio.h HAVE_SYS_SOCKIO_H) + check_include_files(sys/strtio.h HAVE_SYS_STRTIO_H) + check_include_files(sys/select.h HAVE_SYS_SELECT_H) + check_include_files(syslog.h HAVE_SYSLOG_H) + check_include_files(execinfo.h HAVE_EXECINFO_HEADER) + if (HAVE_EXECINFO_HEADER) + check_symbol_exists(backtrace execinfo.h HAVE_EXECINFO_BACKTRACE) + check_symbol_exists(backtrace_symbols execinfo.h HAVE_EXECINFO_BACKTRACE_SYMBOLS) + check_symbol_exists(backtrace_symbols_fd execinfo.h HAVE_EXECINFO_BACKTRACE_SYMBOLS_FD) + + # Some implementations (e.g. Android NDK API < 33) provide execinfo.h but do not define + # the backtrace functions. Disable detection for these cases + if (HAVE_EXECINFO_BACKTRACE AND HAVE_EXECINFO_BACKTRACE_SYMBOLS AND HAVE_EXECINFO_BACKTRACE_SYMBOLS_FD) + set(HAVE_EXECINFO_H ON) + endif() + endif() +else() + set(HAVE_FCNTL_H 1) + set(HAVE_UNISTD_H 1) + set(HAVE_INTTYPES_H 1) + set(HAVE_SYS_FILIO_H 1) +endif() + +if(NOT IOS) + check_struct_has_member("struct tm" tm_gmtoff time.h HAVE_TM_GMTOFF) +else() + set(HAVE_TM_GMTOFF 1) +endif() + +# Mac OS X +if(APPLE) + if(IOS) + if (NOT FREERDP_IOS_EXTERNAL_SSL_PATH) + message(STATUS "FREERDP_IOS_EXTERNAL_SSL_PATH not set! Required if openssl is not found in the iOS SDK (which usually isn't") + endif() + set(CMAKE_FIND_ROOT_PATH ${CMAKE_FIND_ROOT_PATH} ${FREERDP_IOS_EXTERNAL_SSL_PATH}) + set_property(GLOBAL PROPERTY XCODE_ATTRIBUTE_SKIP_INSTALL YES) + else(IOS) + if(NOT DEFINED CMAKE_OSX_ARCHITECTURES) + set(CMAKE_OSX_ARCHITECTURES i386 x86_64) + endif() + endif(IOS) + +# Temporarily disabled, causes the cmake script to be reexecuted, causing the compilation to fail. +# Workaround: specify the parameter in the command-line +# if(WITH_CLANG) +# set(CMAKE_C_COMPILER "clang") +# endif() + + if (WITH_VERBOSE) + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -v") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -v") + endif() +endif(APPLE) + +# OpenBSD +if(OPENBSD) + set(WITH_MANPAGES "ON") + set(WITH_ALSA "OFF") + set(WITH_PULSE "OFF") + set(WITH_OSS "ON") + set(WITH_WAYLAND "OFF") +endif() + +# Android +if(ANDROID) + set(WITH_LIBRARY_VERSIONING "OFF") + + set_property( GLOBAL PROPERTY FIND_LIBRARY_USE_LIB64_PATHS ${ANDROID_LIBRARY_USE_LIB64_PATHS} ) + + if (${ANDROID_ABI} STREQUAL "armeabi") + set (WITH_NEON OFF) + endif() + + if(ANDROID_ABI STREQUAL arm64-v8a) + # https://github.com/android/ndk/issues/910 + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -mfloat-abi=softfp") + endif() + + if("${CMAKE_BUILD_TYPE}" STREQUAL "Debug") + add_definitions(-DNDK_DEBUG=1) + + # NOTE: Manually add -gdwarf-3, as newer toolchains default to -gdwarf-4, + # which is not supported by the gdbserver binary shipped with + # the android NDK (tested with r9b) + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${CMAKE_C_FLAGS_DEBUG} -gdwarf-3") + endif() + set(CMAKE_C_LINK_FLAGS "${CMAKE_C_LINK_FLAGS} -llog") + + if (NOT FREERDP_EXTERNAL_PATH) + if (IS_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/external/") + set (FREERDP_EXTERNAL_PATH "${CMAKE_CURRENT_SOURCE_DIR}/external/") + else() + message(STATUS "FREERDP_EXTERNAL_PATH not set!") + endif() + endif() + + list (APPEND CMAKE_INCLUDE_PATH ${FREERDP_EXTERNAL_PATH}/${ANDROID_ABI}/include) + list (APPEND CMAKE_LIBRARY_PATH ${FREERDP_EXTERNAL_PATH}/${ANDROID_ABI}/ ) + set( CMAKE_FIND_ROOT_PATH_MODE_LIBRARY BOTH ) + set( CMAKE_FIND_ROOT_PATH_MODE_INCLUDE BOTH ) + + if (WITH_GPROF) + CONFIGURE_FILE(${CMAKE_SOURCE_DIR}/scripts/gprof_generate.sh.cmake + ${CMAKE_BINARY_DIR}/scripts/gprof_generate.sh @ONLY) + endif(WITH_GPROF) +endif() + +if(WITH_VALGRIND_MEMCHECK) + check_include_files(valgrind/memcheck.h HAVE_VALGRIND_MEMCHECK_H) +else() + unset(HAVE_VALGRIND_MEMCHECK_H CACHE) +endif() + +if(UNIX OR CYGWIN) + check_include_files(aio.h HAVE_AIO_H) + check_include_files(sys/eventfd.h HAVE_SYS_EVENTFD_H) + if (HAVE_SYS_EVENTFD_H) + check_symbol_exists(eventfd_read sys/eventfd.h WITH_EVENTFD_READ_WRITE) + endif() + if (FREEBSD) + list(APPEND CMAKE_REQUIRED_INCLUDES ${EPOLLSHIM_INCLUDE_DIR}) + endif() + check_include_files(sys/timerfd.h HAVE_SYS_TIMERFD_H) + if (FREEBSD) + list(REMOVE_ITEM CMAKE_REQUIRED_INCLUDES ${EPOLLSHIM_INCLUDE_DIR}) + endif() + check_include_files(poll.h HAVE_POLL_H) + list(APPEND CMAKE_REQUIRED_LIBRARIES m) + check_symbol_exists(ceill math.h HAVE_MATH_C99_LONG_DOUBLE) + list(REMOVE_ITEM CMAKE_REQUIRED_LIBRARIES m) + set(X11_FEATURE_TYPE "RECOMMENDED") + set(WAYLAND_FEATURE_TYPE "RECOMMENDED") + + include(CheckFunctionExists) + + check_function_exists(getlogin_r HAVE_GETLOGIN_R) + check_function_exists(getpwuid_r HAVE_GETPWUID_R) +else() + set(X11_FEATURE_TYPE "DISABLED") + set(WAYLAND_FEATURE_TYPE "DISABLED") +endif() + +if(WITH_PCSC_WINPR) + find_package(PCSCWinPR) +endif() + +set(X11_FEATURE_PURPOSE "X11") +set(X11_FEATURE_DESCRIPTION "X11 client and server") + +set(WAYLAND_FEATURE_PURPOSE "Wayland") +set(WAYLAND_FEATURE_DESCRIPTION "Wayland client") + +set(ZLIB_FEATURE_TYPE "REQUIRED") +set(ZLIB_FEATURE_PURPOSE "compression") +set(ZLIB_FEATURE_DESCRIPTION "data compression") + +set(OPENSSL_FEATURE_TYPE "REQUIRED") +set(OPENSSL_FEATURE_PURPOSE "cryptography") +set(OPENSSL_FEATURE_DESCRIPTION "encryption, certificate validation, hashing functions") + +set(MBEDTLS_FEATURE_TYPE "OPTIONAL") +set(MBEDTLS_FEATURE_PURPOSE "cryptography") +set(MBEDTLS_FEATURE_DESCRIPTION "encryption, certificate validation, hashing functions") + +set(OPENSLES_FEATURE_TYPE "OPTIONAL") +set(OPENSLES_FEATURE_PURPOSE "multimedia") +set(OPENSLES_FEATURE_DESCRIPTION "OpenSLES audio / video") + +set(OSS_FEATURE_TYPE "RECOMMENDED") +set(OSS_FEATURE_PURPOSE "sound") +set(OSS_FEATURE_DESCRIPTION "audio input, audio output and multimedia redirection") + +set(ALSA_FEATURE_TYPE "RECOMMENDED") +set(ALSA_FEATURE_PURPOSE "sound") +set(ALSA_FEATURE_DESCRIPTION "audio input, audio output and multimedia redirection") + +set(PULSE_FEATURE_TYPE "RECOMMENDED") +set(PULSE_FEATURE_PURPOSE "sound") +set(PULSE_FEATURE_DESCRIPTION "audio input, audio output and multimedia redirection") + +set(CUPS_FEATURE_TYPE "RECOMMENDED") +set(CUPS_FEATURE_PURPOSE "printing") +set(CUPS_FEATURE_DESCRIPTION "printer device redirection") + +set(PCSC_FEATURE_TYPE "RECOMMENDED") +set(PCSC_FEATURE_PURPOSE "smart card") +set(PCSC_FEATURE_DESCRIPTION "smart card device redirection") + +set(FFMPEG_FEATURE_TYPE "RECOMMENDED") +set(FFMPEG_FEATURE_PURPOSE "multimedia") +set(FFMPEG_FEATURE_DESCRIPTION "multimedia redirection, audio and video playback") + +set(VAAPI_FEATURE_TYPE "OPTIONAL") +set(VAAPI_FEATURE_PURPOSE "multimedia") +set(VAAPI_FEATURE_DESCRIPTION "[experimental] VA-API hardware acceleration for video playback") + +set(IPP_FEATURE_TYPE "OPTIONAL") +set(IPP_FEATURE_PURPOSE "performance") +set(IPP_FEATURE_DESCRIPTION "Intel Integrated Performance Primitives library") + +set(JPEG_FEATURE_TYPE "OPTIONAL") +set(JPEG_FEATURE_PURPOSE "codec") +set(JPEG_FEATURE_DESCRIPTION "use JPEG library") + +set(OPENH264_FEATURE_TYPE "OPTIONAL") +set(OPENH264_FEATURE_PURPOSE "codec") +set(OPENH264_FEATURE_DESCRIPTION "use OpenH264 library") + +set(OPENCL_FEATURE_TYPE "OPTIONAL") +set(OPENCL_FEATURE_PURPOSE "codec") +set(OPENCL_FEATURE_DESCRIPTION "[experimental] use OpenCL library") + +set(GSM_FEATURE_TYPE "OPTIONAL") +set(GSM_FEATURE_PURPOSE "codec") +set(GSM_FEATURE_DESCRIPTION "GSM audio codec library") + +set(LAME_FEATURE_TYPE "OPTIONAL") +set(LAME_FEATURE_PURPOSE "codec") +set(LAME_FEATURE_DESCRIPTION "lame MP3 audio codec library") + +set(FAAD2_FEATURE_TYPE "OPTIONAL") +set(FAAD2_FEATURE_PURPOSE "codec") +set(FAAD2_FEATURE_DESCRIPTION "FAAD2 AAC audio codec library") + +set(FAAC_FEATURE_TYPE "OPTIONAL") +set(FAAC_FEATURE_PURPOSE "codec") +set(FAAC_FEATURE_DESCRIPTION "[experimental] FAAC AAC audio codec library") + +set(SOXR_FEATURE_TYPE "OPTIONAL") +set(SOXR_FEATURE_PURPOSE "codec") +set(SOXR_FEATURE_DESCRIPTION "SOX audio resample library") + +set(GSSAPI_FEATURE_TYPE "OPTIONAL") +set(GSSAPI_FEATURE_PURPOSE "auth") +set(GSSAPI_FEATURE_DESCRIPTION "[experimental] add kerberos support") + +if(WIN32) + set(X11_FEATURE_TYPE "DISABLED") + set(WAYLAND_FEATURE_TYPE "DISABLED") + set(ZLIB_FEATURE_TYPE "DISABLED") + set(OSS_FEATURE_TYPE "DISABLED") + set(ALSA_FEATURE_TYPE "DISABLED") + set(PULSE_FEATURE_TYPE "DISABLED") + set(CUPS_FEATURE_TYPE "DISABLED") + set(PCSC_FEATURE_TYPE "DISABLED") + set(FFMPEG_FEATURE_TYPE "DISABLED") + set(VAAPI_FEATURE_TYPE "DISABLED") + set(OPENSLES_FEATURE_TYPE "DISABLED") +endif() + +if(APPLE) + set(FFMPEG_FEATURE_TYPE "OPTIONAL") + set(VAAPI_FEATURE_TYPE "DISABLED") + set(X11_FEATURE_TYPE "OPTIONAL") + set(WAYLAND_FEATURE_TYPE "DISABLED") + set(OSS_FEATURE_TYPE "DISABLED") + set(ALSA_FEATURE_TYPE "DISABLED") + if(IOS) + set(X11_FEATURE_TYPE "DISABLED") + set(PULSE_FEATURE_TYPE "DISABLED") + set(CUPS_FEATURE_TYPE "DISABLED") + set(PCSC_FEATURE_TYPE "DISABLED") + endif() + set(OPENSLES_FEATURE_TYPE "DISABLED") +endif() + +if(UNIX AND NOT ANDROID) + set(WLOG_SYSTEMD_JOURNAL_FEATURE_TYPE "RECOMMENDED") + set(WLOG_SYSTEMD_JOURNAL_FEATURE_PURPOSE "systemd journal appender") + set(WLOG_SYSTEMD_JOURNAL_FEATURE_DESCRIPTION "allows to export wLog to systemd journal") + + #include(Findlibsystemd) + find_feature(libsystemd ${WLOG_SYSTEMD_JOURNAL_FEATURE_TYPE} ${WLOG_SYSTEMD_JOURNAL_FEATURE_PURPOSE} ${WLOG_SYSTEMD_JOURNAL_FEATURE_DESCRIPTION}) + + if(LIBSYSTEMD_FOUND) + set(HAVE_JOURNALD_H TRUE) + else() + unset(HAVE_JOURNALD_H) + endif() +endif(UNIX AND NOT ANDROID) + +if(ANDROID) + set(X11_FEATURE_TYPE "DISABLED") + set(WAYLAND_FEATURE_TYPE "DISABLED") + set(OSS_FEATURE_TYPE "DISABLED") + set(ALSA_FEATURE_TYPE "DISABLED") + set(PULSE_FEATURE_TYPE "DISABLED") + set(CUPS_FEATURE_TYPE "DISABLED") + set(PCSC_FEATURE_TYPE "DISABLED") + set(VAAPI_FEATURE_TYPE "DISABLED") + set(OPENSLES_FEATURE_TYPE "REQUIRED") +endif() + +find_feature(X11 ${X11_FEATURE_TYPE} ${X11_FEATURE_PURPOSE} ${X11_FEATURE_DESCRIPTION}) +find_feature(Wayland ${WAYLAND_FEATURE_TYPE} ${WAYLAND_FEATURE_PURPOSE} ${WAYLAND_FEATURE_DESCRIPTION}) + +find_feature(ZLIB ${ZLIB_FEATURE_TYPE} ${ZLIB_FEATURE_PURPOSE} ${ZLIB_FEATURE_DESCRIPTION}) +find_feature(OpenSSL ${OPENSSL_FEATURE_TYPE} ${OPENSSL_FEATURE_PURPOSE} ${OPENSSL_FEATURE_DESCRIPTION}) +find_feature(MbedTLS ${MBEDTLS_FEATURE_TYPE} ${MBEDTLS_FEATURE_PURPOSE} ${MBEDTLS_FEATURE_DESCRIPTION}) +find_feature(OpenSLES ${OPENSLES_FEATURE_TYPE} ${OPENSLES_FEATURE_PURPOSE} ${OPENSLES_FEATURE_DESCRIPTION}) + +find_feature(OSS ${OSS_FEATURE_TYPE} ${OSS_FEATURE_PURPOSE} ${OSS_FEATURE_DESCRIPTION}) +find_feature(ALSA ${ALSA_FEATURE_TYPE} ${ALSA_FEATURE_PURPOSE} ${ALSA_FEATURE_DESCRIPTION}) +find_feature(Pulse ${PULSE_FEATURE_TYPE} ${PULSE_FEATURE_PURPOSE} ${PULSE_FEATURE_DESCRIPTION}) + +find_feature(Cups ${CUPS_FEATURE_TYPE} ${CUPS_FEATURE_PURPOSE} ${CUPS_FEATURE_DESCRIPTION}) +find_feature(PCSC ${PCSC_FEATURE_TYPE} ${PCSC_FEATURE_PURPOSE} ${PCSC_FEATURE_DESCRIPTION}) + +find_feature(FFmpeg ${FFMPEG_FEATURE_TYPE} ${FFMPEG_FEATURE_PURPOSE} ${FFMPEG_FEATURE_DESCRIPTION}) + +find_feature(JPEG ${JPEG_FEATURE_TYPE} ${JPEG_FEATURE_PURPOSE} ${JPEG_FEATURE_DESCRIPTION}) +find_feature(OpenH264 ${OPENH264_FEATURE_TYPE} ${OPENH264_FEATURE_PURPOSE} ${OPENH264_FEATURE_DESCRIPTION}) +find_feature(OpenCL ${OPENCL_FEATURE_TYPE} ${OPENCL_FEATURE_PURPOSE} ${OPENCL_FEATURE_DESCRIPTION}) +find_feature(GSM ${GSM_FEATURE_TYPE} ${GSM_FEATURE_PURPOSE} ${GSM_FEATURE_DESCRIPTION}) +find_feature(LAME ${LAME_FEATURE_TYPE} ${LAME_FEATURE_PURPOSE} ${LAME_FEATURE_DESCRIPTION}) +find_feature(FAAD2 ${FAAD2_FEATURE_TYPE} ${FAAD2_FEATURE_PURPOSE} ${FAAD2_FEATURE_DESCRIPTION}) +find_feature(FAAC ${FAAC_FEATURE_TYPE} ${FAAC_FEATURE_PURPOSE} ${FAAC_FEATURE_DESCRIPTION}) +find_feature(soxr ${SOXR_FEATURE_TYPE} ${SOXR_FEATURE_PURPOSE} ${SOXR_FEATURE_DESCRIPTION}) + +find_feature(GSSAPI ${GSSAPI_FEATURE_TYPE} ${GSSAPI_FEATURE_PURPOSE} ${GSSAPI_FEATURE_DESCRIPTION}) + +if (WITH_OPENH264 AND NOT WITH_OPENH264_LOADING) + option(WITH_OPENH264_LOADING "Use LoadLibrary to load openh264 at runtime" OFF) +endif (WITH_OPENH264 AND NOT WITH_OPENH264_LOADING) + +if ((WITH_FFMPEG OR WITH_DSP_FFMPEG) AND NOT FFMPEG_FOUND) + message(FATAL_ERROR "FFMPEG support requested but not detected") +endif() +set(WITH_FFMPEG ${FFMPEG_FOUND}) + +# Version check, if we have detected FFMPEG but the version is too old +# deactivate it as sound backend. +if (WITH_DSP_FFMPEG) + # Deactivate FFmpeg backend for sound, if the version is too old. + # See libfreerdp/codec/dsp_ffmpeg.h + file(STRINGS "${AVCODEC_INCLUDE_DIR}/libavcodec/version.h" AV_VERSION_FILE REGEX "LIBAVCODEC_VERSION_M[A-Z]+[\t ]*[0-9]+") + if (EXISTS "${AVCODEC_INCLUDE_DIR}/libavcodec/version_major.h") + file(STRINGS "${AVCODEC_INCLUDE_DIR}/libavcodec/version_major.h" AV_VERSION_FILE2 REGEX "LIBAVCODEC_VERSION_M[A-Z]+[\t ]*[0-9]+") + list(APPEND AV_VERSION_FILE ${AV_VERSION_FILE2}) + endif() + + FOREACH(item ${AV_VERSION_FILE}) + STRING(REGEX MATCH "LIBAVCODEC_VERSION_M[A-Z]+[\t ]*[0-9]+" litem ${item}) + IF(litem) + string(REGEX REPLACE "[ \t]+" ";" VSPLIT_LINE ${litem}) + list(LENGTH VSPLIT_LINE VSPLIT_LINE_LEN) + if (NOT "${VSPLIT_LINE_LEN}" EQUAL "2") + message(ERROR "invalid entry in libavcodec version header ${item}") + endif(NOT "${VSPLIT_LINE_LEN}" EQUAL "2") + list(GET VSPLIT_LINE 0 VNAME) + list(GET VSPLIT_LINE 1 VVALUE) + set(${VNAME} ${VVALUE}) + ENDIF(litem) + ENDFOREACH(item ${AV_VERSION_FILE}) + + set(AVCODEC_VERSION "${LIBAVCODEC_VERSION_MAJOR}.${LIBAVCODEC_VERSION_MINOR}.${LIBAVCODEC_VERSION_MICRO}") + if (AVCODEC_VERSION VERSION_LESS "57.48.101") + message(WARNING "FFmpeg version detected (${AVCODEC_VERSION}) is too old. (Require at least 57.48.101 for sound). Deactivating") + set(WITH_DSP_FFMPEG OFF) + endif() +endif (WITH_DSP_FFMPEG) + +if (WITH_OPENH264 AND NOT OPENH264_FOUND) + message(FATAL_ERROR "OpenH264 support requested but not detected") +endif() +set(WITH_OPENH264 ${OPENH264_FOUND}) + +if ( (WITH_GSSAPI) AND (NOT GSS_FOUND)) + message(WARNING "-DWITH_GSSAPI=ON is set, but not GSSAPI implementation was found, disabling") +elseif(WITH_GSSAPI) + if(GSS_FLAVOUR STREQUAL "MIT") + add_definitions("-DWITH_GSSAPI -DWITH_GSSAPI_MIT") + if(GSS_VERSION_1_13) + add_definitions("-DHAVE_AT_LEAST_KRB_V1_13") + endif() + include_directories(${_GSS_INCLUDE_DIR}) + elseif(GSS_FLAVOUR STREQUAL "Heimdal") + add_definitions("-DWITH_GSSAPI -DWITH_GSSAPI_HEIMDAL") + include_directories(${_GSS_INCLUDE_DIR}) + else() + message(WARNING "Kerberos version not detected") + endif() +endif() + +if(TARGET_ARCH MATCHES "x86|x64") + if (NOT APPLE) + # Intel Performance Primitives + find_feature(IPP ${IPP_FEATURE_TYPE} ${IPP_FEATURE_PURPOSE} ${IPP_FEATURE_DESCRIPTION}) + endif() +endif() + +if(OPENSSL_FOUND) + add_definitions("-DWITH_OPENSSL") + message(STATUS "Using OpenSSL Version: ${OPENSSL_VERSION}") + include_directories(${OPENSSL_INCLUDE_DIR}) +endif() + +if(MBEDTLS_FOUND) + add_definitions("-DWITH_MBEDTLS") +endif() + +if (WITH_OPENH264 OR WITH_MEDIA_FOUNDATION OR WITH_FFMPEG OR WITH_MEDIACODEC) + set(WITH_GFX_H264 ON) +else() + set(WITH_GFX_H264 OFF) +endif() + +# Android expects all libraries to be loadable +# without paths. +if (ANDROID OR WIN32 OR MAC_BUNDLE) + set(FREERDP_DATA_PATH "share") + if (NOT FREERDP_INSTALL_PREFIX) + set(FREERDP_INSTALL_PREFIX ".") + endif() + set(FREERDP_LIBRARY_PATH ".") + set(FREERDP_PLUGIN_PATH ".") +else() + set(FREERDP_DATA_PATH "${CMAKE_INSTALL_PREFIX}/share/freerdp${FREERDP_VERSION_MAJOR}") + if (NOT FREERDP_INSTALL_PREFIX) + set(FREERDP_INSTALL_PREFIX "${CMAKE_INSTALL_PREFIX}") + endif() + set(FREERDP_LIBRARY_PATH "${CMAKE_INSTALL_LIBDIR}") + set(FREERDP_PLUGIN_PATH "${CMAKE_INSTALL_LIBDIR}/freerdp${FREERDP_VERSION_MAJOR}") +endif() +set(FREERDP_ADDIN_PATH "${FREERDP_PLUGIN_PATH}") + +# Path to put extensions +set(FREERDP_EXTENSION_PATH "${CMAKE_INSTALL_FULL_LIBDIR}/freerdp${FREERDP_VERSION_MAJOR}/extensions") + +# Proxy plugins path +if(NOT DEFINED PROXY_PLUGINDIR) + message("using default plugins location") + set(FREERDP_PROXY_PLUGINDIR "${CMAKE_BINARY_DIR}/server/proxy/plugins") +else() + set(FREERDP_PROXY_PLUGINDIR "${PROXY_PLUGINDIR}") +endif() + +# Declare we have config.h, generated later on. +add_definitions("-DHAVE_CONFIG_H") + +# Include directories +include_directories(${CMAKE_CURRENT_BINARY_DIR}) +include_directories(${CMAKE_CURRENT_BINARY_DIR}/include) +include_directories(${CMAKE_CURRENT_SOURCE_DIR}/include) + +# RPATH configuration +set(CMAKE_SKIP_BUILD_RPATH FALSE) +set(CMAKE_BUILD_WITH_INSTALL_RPATH FALSE) +if (APPLE) + set(CMAKE_INSTALL_RPATH_USE_LINK_PATH FALSE) + set(CMAKE_INSTALL_RPATH "@loader_path/../Frameworks") +else (APPLE) + set(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE) + if (NOT FREEBSD) + set(CMAKE_INSTALL_RPATH "\$ORIGIN/../${CMAKE_INSTALL_LIBDIR}:\$ORIGIN/..") + endif() +endif(APPLE) + +if (BUILD_SHARED_LIBS) + set(CMAKE_MACOSX_RPATH ON) +endif() + +# Android profiling +if(ANDROID) + if(WITH_GPROF) + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -pg") + set(PROFILER_LIBRARIES + "${FREERDP_EXTERNAL_PROFILER_PATH}/obj/local/${ANDROID_ABI}/libandroid-ndk-profiler.a") + include_directories("${FREERDP_EXTERNAL_PROFILER_PATH}") + endif() +endif() + +# Unit Tests + +include(CTest) + +if(BUILD_TESTING) + enable_testing() + + if(MSVC) + set(TESTING_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}") + else() + set(TESTING_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/Testing") + endif() +endif() + +# WinPR +include_directories("${CMAKE_SOURCE_DIR}/winpr/include") +include_directories("${CMAKE_BINARY_DIR}/winpr/include") + +if (${CMAKE_VERSION} VERSION_LESS 2.8.12) + set(PUBLIC_KEYWORD "") + set(PRIVATE_KEYWORD "") +else() + set(PUBLIC_KEYWORD "PUBLIC") + set(PRIVATE_KEYWORD "PRIVATE") +endif() + +if(BUILD_SHARED_LIBS) + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DWINPR_DLL") +endif() + +add_subdirectory(winpr) + +# Sub-directories + +if(WITH_THIRD_PARTY) + add_subdirectory(third-party) + if (NOT "${THIRD_PARTY_INCLUDES}" STREQUAL "") + include_directories(${THIRD_PARTY_INCLUDES}) + endif() +endif() + +add_subdirectory(include) + +add_subdirectory(libfreerdp) + +# RdTk +include_directories("${CMAKE_SOURCE_DIR}/rdtk/include") +include_directories("${CMAKE_BINARY_DIR}/rdtk/include") + +add_subdirectory(rdtk) + +if(WAYLAND_FOUND) + add_subdirectory(uwac) +endif() + +if(BSD) + if(IS_DIRECTORY /usr/local/include) + include_directories(/usr/local/include) + link_directories(/usr/local/lib) + endif() + if(OPENBSD) + if(IS_DIRECTORY /usr/X11R6/include) + include_directories(/usr/X11R6/include) + endif() + endif() +endif() + +if(WITH_CHANNELS) + add_subdirectory(channels) +endif() + +if(WITH_CLIENT_COMMON OR WITH_CLIENT) +add_subdirectory(client) +endif() + +if(WITH_SERVER) + add_subdirectory(server) +endif() + +# Configure files - Add last so all symbols are defined +configure_file(${CMAKE_CURRENT_SOURCE_DIR}/config.h.in ${CMAKE_CURRENT_BINARY_DIR}/config.h) + +# Packaging + +set(CMAKE_CPACK_INCLUDE_FILE "CMakeCPack.cmake") + +if(NOT (VENDOR MATCHES "FreeRDP")) + if(DEFINED CLIENT_VENDOR_PATH) + if(EXISTS "${CMAKE_SOURCE_DIR}/${CLIENT_VENDOR_PATH}/CMakeCPack.cmake") + set(CMAKE_CPACK_INCLUDE_FILE "${CLIENT_VENDOR_PATH}/CMakeCPack.cmake") + endif() + endif() +endif() + +#message("VENDOR: ${VENDOR} CLIENT_VENDOR_PATH: ${CLIENT_VENDOR_PATH} CMAKE_CPACK_INCLUDE_FILE: ${CMAKE_CPACK_INCLUDE_FILE}") + +include(${CMAKE_CPACK_INCLUDE_FILE}) + +set(FREERDP_BUILD_CONFIG_LIST "") +GET_CMAKE_PROPERTY(res VARIABLES) +FOREACH(var ${res}) + IF (var MATCHES "^WITH_*|^BUILD_TESTING|^BUILTIN_CHANNELS|^HAVE_*") + LIST(APPEND FREERDP_BUILD_CONFIG_LIST "${var}=${${var}}") + ENDIF() +ENDFOREACH() +string(REPLACE ";" " " FREERDP_BUILD_CONFIG "${FREERDP_BUILD_CONFIG_LIST}") +configure_file(${CMAKE_CURRENT_SOURCE_DIR}/buildflags.h.in ${CMAKE_CURRENT_BINARY_DIR}/buildflags.h) diff --git a/ChangeLog b/ChangeLog new file mode 100644 index 0000000..b878eac --- /dev/null +++ b/ChangeLog @@ -0,0 +1,638 @@ +# 2022-10-12 Version 2.8.1 + +Notewhorth changes: +* Fixed CVE-2022-39282 +* Fixed CVE-2022-39283 +* Added missing commit for backported #8041: Remove ALAW/ULAW codecs from linux backends (unreliable) +* Added hash checks for android build script dependencies + +Fixed issues: +* Backported #8190: Fix build break with newer FFMPEG versions +* Backported #8234: Updated flatpak with build script +* Backported #8210: Better execinfo support check for android +* Backported #7708: Header now defines DumpThreadHandles +* Backported #8176: Check fullscreen state and not setting +* Backported #8236: Send resize on window state change +* Backported #7611: Audin macOS monterey fix +* Backported #8291: Android build script update + +# 2022-07-28 Version 2.8.0 + +Noteworthy changes: + +* Backported API to get peer accepted channel option flags +* Backported API to get peer accepted channel names +* Backported Stream_CheckAndLogRequiredLength +* Backported #7954: Add server side handling for [MS-RDPET] +* Backported #8010: Add server side handling for [MS-RDPECAM] +* Backported #8041: Remove ALAW/ULAW codecs from linux backends (unreliable) +* Backported #8051: Relieve CLIPRDR filename restriction when connecting to non-MS Windows servers +* Backported #8048: TLS version control +* Backported #7987: Add a new command line arg to enforce tls1.2 + +Fixed issues: + +* Fixed #7837: Prevent out of bound reads for FFMPEG +* Backported #7859 and #7861: Unwind support for backtrace generation +* Backported #7440: wlfreerdp appid +* Backported #7832: RAIL window restore +* Backported #7833: Refactored WinPR thread locking +* Backported #7893: Mac rdpsnd memory leak fixes +* Backported #7895: Mac audin memory leak fixes +* Backported #7898: Automatic android versioning +* Backported #7916: GFX 10.7 capability support +* Backported #7949: Server RDPSND API improvements +* Backported #7957: Server DVC API improvements +* Backported #7760: Fixed osMinorType values +* Backported #8013: Add missing osMajorType values +* Backported #8076: Fix wrong usage of subband diffing flag (tile artifact fix) + +For a complete and detailed change log since the last release run: +git log 2.7.0..2.8.0 + + +# 2022-04-25 Version 2.7.0 + +Noteworthy changes: +* Backported OpenSSL3 gateway support (#7822) +* Backported various NTLM fixes +* Backported WINPR_ASSERT to ease future backports + +Fixed issues: +* Backported #6786: Use /network:auto by default +* Backported #7714: Workaround for broken surface frame marker +* Backported #7733: Support 10bit X11 color (BGRX32 only) +* Backported #7745: GFX progressive double free +* Backported #7808: Disable websockets with /gt:rpc +* Backported #7815: RAIL expect LOGON_MSG_SESSION_CONTINUE + +Important notes: + +For a complete and detailed change log since the last release run: +git log 2.6.1..2.7.0 + +# 2022-03-07 Version 2.6.1 + +Noteworthy changes: + +Fixed issues: +* Backported freerdp_abort_connect during freerdp_connect fix (#7700) +* Backported improved version dection see docs/version_detection.md for details +* Backported various rdpsnd fixes (#7695) + +Important notes: + +For a complete and detailed change log since the last release run: +git log 2.0.0..2.6.1 + +# 2022-02-22 Version 2.6.0 + +Noteworthy changes: +* Backported android FFMPEG build scripts +* Updated android build dependencies + +Fixed issues: +* Backported #7303: Fix PDU length for RDPINPUT_PROTOCOL_V300 +* Backported #7658: Sanitize optional physical monitor size values +* Backported #7426: Wayland memory corruption +* Backported #7293: Remove unused codec x264 +* Backported #7541: Allow resolutions larger 2048x2048 +* Backported #7574: FFMPEG 5.0 support +* Backported #7578: FFMPEG 5.0 support +* Backported #7580: Fixed device hotplugging +* Backported #7583: GetUserNameExA: Prefer getpwuid_r over getlogin_r over getlogin +* Backported #7585: Android Mediacodec support + +Important notes: + +For a complete and detailed change log since the last release run: +git log 2.5.0..2.6.0 + +# 2022-01-12 Version 2.5.0 + +Noteworthy changes: +* Fixed smartcard login in case a redirection occurs the pin was lost +* Backported windows client drawing fixes +* Backported improved macOS keyboard layout detection +* Backported TcpConnectTimeout +* Backported LibreSSL compatibility patches +* Backported signal handler backtrace +* Backported OpenSSL 3.0 support + +Fixed issues: +* Backport #7539: Wayland client clipboard issues +* Backport #7509: Various fixes regarding registry emulation, addin loader + and updated locale detection +* Backport #7466: Android android_register_pointer missing initialization + +Important notes: + +For a complete and detailed change log since the last release run: +git log 2.4.1..2.5.0 + +# 2021-10-20 Version 2.4.1 + +Noteworthy changes: +* Refactored RPC gateway parsing code +* OpenSSL 3.0 compatibility fixes +* USB redirection: fixed transfer lengths + +Fixed issues: +* #7363: Length checks in ConvertUTF8toUTF16 +* #7349: Added checks for bitmap width and heigth values + +Important notes: +* CVE-2021-41159: Improper client input validation for gateway connections allows to overwrite memory +* CVE-2021-41160: Improper region checks in all clients allow out of bound write to memory + +For a complete and detailed change log since the last release run: +git log 2.4.0..2.4.1 + +# 2021-07-27 Version 2.4.0 + +Noteworthy changes: +* Backported multithreadded progressive decoder (#7036) +* Backported clipboard fixes (#6924) +* Fixed remote file read (#7185) + +Fixed issues: +* #6938: RAILS clipboard remote -> local +* #6985: Support newer FFMPEG builds +* #6989: Use OpenSSL default certificate store settings +* #7073: Planar alignment fixes + +# 2021-03-15 Version 2.3.2 + +For a complete and detailed change log since the last release run: +git log 2.3.2..2.4.0 + +Noteworthy changes: +* Fixed autoreconnect printer backend loading +* Fixed compilation on older mac os versions < 10.14 +* Fixed mouse pointer move with smart-sizing +* Added command line option to disable websocket gateway support +* Fixed drive hotplugging issues with windows +* Fixed smartcard issues on mac + +Fixed issues: +* #6900: Transparency issues with aFreeRDP +* #6848: Invalid format string in smartcard trace +* #6846: Fixed static builds +* #6888: Crash due to missing bounds checks +* #6882: Use default sound devoce on mac + +For a complete and detailed change log since the last release run: +git log 2.3.1..2.3.2 + +# 2021-03-01 Version 2.3.1 + +Noteworthy changes: +* This is a compatibility bugfix release readding some (deprecated) + symbols/defines +* Also add some more EXPERIMENTAL warnings to CMake flags as some were not + clear enough. +* Fixed a memory leak in xfreerdp (mouse pointer updates) +* No longer activating some compile time debug options with -DWITH_DEBUG_ALL=ON + which might leak sensitive information. +* Added -DDEFINE_NO_DEPRECATED for developers to detect use of deprecated + symbols + +For a complete and detailed change log since the last release run: +git log 2.3.0..2.3.1 + + +# 2021-02-24 Version 2.3.0 + +Important notes: +* CMake option WITH_PROXY_MODULES is currently experimental, do not use in +production. +* The clipboard struct FILEDESCRIPTOR was replaced by FILEDESCRIPTORW with + proper data types. They are binary compatible and the former is kept for + compatibility but compilers will emit warnings. + +Noteworthy changes: +* Websocket support for proxy connections +* Progressive codec improvements. Reduces graphical glitches against windows +and ogon servers +* Fixed +glyph-cache, now working properly without disconnects +* Huge file support in clipboard +* XWayland support for xfreerdp (keyboard grabbing) +* Improved wlfreerdp (wayland client) +* Option to allow keyboard scancodes to be remapped manually +* Improved mouse wheel behaviour when scrolling +* Improved dynamic channel behaviour, more stable event detection +* New connection state PubSub notification: Clients can now monitor current + connection state + +Fixed issues: +* #6626: Fixed parsing of FastGlyph order. +* #6624: Added support for xwayland keyboard grab +* #6492: Added clipboard CB_HUGE_FILE_SUPPORT_ENABLED flag +* #6428: Improve NLA error code logging. +* #6416: Http gateway message support +* #6753: List of pull requests to backport for stable-next + +For a complete and detailed change log since the last release run: +git log 2.2.0..2.3.0 + + +# 2020-07-20 Version 2.2.0 + +Important notes: +* CVE-2020-15103 - Integer overflow due to missing input sanitation in rdpegfx channel + +Noteworty changes: +* fix: memory leak in nsc +* urbdrc + * some fixes and improvements +* build + * use cmake to detect getlogin_r + * improve asan checks/detection +* server/proxy + * new: support for heartbeats + * new: support for rail handshake ex flags + * fix: possible race condition with redirects + +Fixed issues: +* #6263 Sound & mic - filter GSM codec for microphone redirection +* #6335: windows client title length +* #6370 - "Alternate Secondary Drawing Order UNKNOWN" +* #6298 - remoteapp with dialog is disconnecting when it loses focus +* #6299 - v2.1.2: Can't connect to Windows7 + +For a complete and detailed change log since the last release run: +git log 2.1.2..2.2.0 + + +# 2020-06-22 Version 2.1.2 + +Important notes: +* CVE-2020-4033 Out of bound read in RLEDECOMPRESS +* CVE-2020-4031 Use-After-Free in gdi_SelectObject +* CVE-2020-4032 Integer casting vulnerability in `update_recv_secondary_order` +* CVE-2020-4030 OOB read in `TrioParse` +* CVE-2020-11099 OOB Read in license_read_new_or_upgrade_license_packet +* CVE-2020-11098 Out-of-bound read in glyph_cache_put +* CVE-2020-11097 OOB read in ntlm_av_pair_get +* CVE-2020-11095 Global OOB read in update_recv_primary_order +* CVE-2020-11096 Global OOB read in update_read_cache_bitmap_v3_order +* Gateway RPC fixes for windows +* Fixed resource fee race resulting in double free in USB redirection +* Fixed wayland client crashes +* Fixed X11 client mouse mapping issues (X11 mapping on/off) +* Some proxy related improvements (capture module) +* Code cleanup (use getlogin_r, ...) + +For a complete and detailed change log since the last release candidate run: +git log 2.1.1..2.1.2 + + +# 2020-05-20 Version 2.1.1 + +Important notes: +* CVE: GHSL-2020-100 OOB Read in ntlm_read_ChallengeMessage +* CVE: GHSL-2020-101 OOB Read in security_fips_decrypt due to uninitialized value +* CVE: GHSL-2020-102 OOB Write in crypto_rsa_common +* Enforce synchronous legacy RDP encryption count (#6156) +* Fixed some leaks and crashes missed in 2.1.0 +* Removed dynamic channel listener limits +* Lots of resource cleanup fixes (clang sanitizers) +* A couple of performance improvements +* Various small annoyances eliminated (typos, prefilled username for windows client, ...) + + +For a complete and detailed change log since the last release candidate run: +git log 2.1.0..2.1.1 + + +# 2020-05-05 Version 2.1.0 + +Important notes: + +* fix multiple CVEs: CVE-2020-11039, CVE-2020-11038, CVE-2020-11043, CVE-2020-11040, CVE-2020-11041, + CVE-2020-11019, CVE-2020-11017, CVE-2020-11018 +* fix multiple leak and crash issues (#6129, #6128, #6127, #6110, #6081, #6077) + +Noteworthy features and improvements: +* Fixed sound issues (#6043) +* New expert command line options /tune and /tune-list to modify all client + settings in a generic way. +* Fixes for smartcard cache, this improves compatibility of smartcard devices + with newer smartcard channel. +* Shadow server can now be instructed to listen to multiple interfaces. +* Improved server certificate support (#6052) +* Various fixes for wayland client (fullscreen, mouse wheel, ...) +* Fixed large mouse pointer support, now mouse pointers > 96x96 pixel are visible. +* USB redirection command line improvements (filter options) +* Various translation improvements for android and ios clients + +For a complete and detailed change log since the last release candidate run: +git log 2.0.0..2.1.0 + + +# 2020-04-09 Version 2.0.0 + +Important notes: + +* fix multiple CVEs: CVE-2020-11521 CVE-2020-11522 CVE-2020-11523 CVE-2020-11524 CVE-2020-11525 CVE-2020-11526 +* fix multiple other security related issues (#6005, #6006, #6007, #6008, #6009, #6010, #6011, #6012, #6013) +* sha256 is now used instead of sha1 to fingerprint certificates. This will + invalidate all hosts in FreeRDP known_hosts2 file and causes a prompt if a + new connection is established after the update + +Noteworthy features and improvements: + +* First version of the RDP proxy was added (#5372) - thanks to @kubistika +* Smartcard received some refactoring. Missing functions were added and input + validation was improved (#5884) +* A new option /cert that unifies all certificate related options (#5880) + The old options (cert-ignore, cert-deny, cert-name, cert-tofu) are still + available but marked as deprecated +* Support for Remote Assistance Protocol Version 2 [MS-RA] +* The DirectFB client was removed because it was unmaintained +* Unified initialization of OrderSupport +* Fix for licensing against Windows Server 2003 +* Font smoothing is now enabled per default +* Flatpack support was added +* Smart scaling for Wayland using libcairo was added (#5215) +* Unified update->BeginPaint and update->EndPaint +* An image scaling API for software drawing was added +* Rail was updated to the latest spec version 28.0 +* Support for H.264 in the shadow server is now detected at runtime +* Add mask= option for /gfx and /gfx-h264 (#5771) +* Code reformatting (#5667) +* A new option /timeout was added to adjust the TCP ACK timeout (#5987) + +For a complete and detailed change log since the last release candidate run: +git log 2.0.0-rc4..2.0.0 + + +# 2018-11-19 Version 2.0.0-rc4 + +FreeRDP 2.0.0-rc4 is the fifth release candidate for 2.0.0. Although it mainly +addresses security and stability there are some new features as well. + +Noteworthy features and improvements: + +* fix multiple reported CVEs (#5031) +* gateway: multiple fixes and improvements (#3600, #4787, #4902, #4964, #4947, + #4952, #4938) +* client/X11: support for rail (remote app) icons was added (#5012) +* licensing: the licensing code was re-worked. Per-device licenses + are now saved on the client and used on re-connect. + WARNING: this is a change in FreeRDP behavior regarding licensing. If the old + behavior is required, or no licenses should be saved use the + new command line option +old-license (#4979) +* improve order handling - only orders that were enabled + during capability exchange are accepted (#4926). + WARNING and NOTE: some servers do improperly send orders that weren't negotiated, + for such cases the new command line option /relax-order-checks was added to + disable the strict order checking. If connecting to xrdp the options + /relax-order-checks *and* +glyph-cache are required. +* /smartcard has now support for substring filters (#4840) + for details see https://github.com/FreeRDP/FreeRDP/wiki/smartcard-logon +* add support to set tls security level (for openssl >= 1.1.0) + - default level is set to 1 + - the new command line option /tls-seclevel:[LEVEL] allows to set + a different level if required +* add new command line option /smartcard-logon to allow + smartcard login (currently only with RDP security) (#4842) +* new command line option: /window-position to allow positioning + the window on startup (#5018) +* client/X11: set window title before mapping (#5023) +* rdpsnd/audin (mostly server side) add support for audio re-sampling using soxr or ffmpeg +* client/Android: add Japanese translation (#4906) +* client/Android: add Korean translation (#5029) + +For a complete and detailed change log since the last release candidate run: +git log 2.0.0-rc3..2.0.0-rc4 + + +# 2018-08-01 Version 2.0.0-rc3 + +FreeRDP 2.0.0-rc3 is the fourth release candidate for 2.0.0. +For a complete and detailed change log since the last release candidate run: +git log 2.0.0-rc2..2.0.0-rc3 + +Noteworthy features and improvements: + +* Updated and improved sound and microphone redirection format support (AAC) +* Improved reliability of reconnect and redirection support +* Fixed memory leaks with +async-update +* Improved connection error reporting +* Improved gateway support (various fixes for HTTP and RDG) +* SOCKS proxy support (all clients) +* More reliable resolution switching with /dynamic-resolution (MS-RDPEVOR) (xfreerdp) + +Fixed github issues (excerpt): + +* #1924, #4132, #4511 Fixed redirection +* #4165 AAC and MP3 codec support for sound and microphone redirection +* #4222 Gateway connections prefer IP to hostname +* #4550 Fixed issues with +async-update +* #4634 Comment support in known_hosts file +* #4684 /drive and +drives don't work togehter +* #4735 Automatically reconnect if connection timed out waiting for user interaction + +See https://github.com/FreeRDP/FreeRDP/milestone/9 for a complete list. + + +# 2017-11-28 Version 2.0.0-rc2 + +FreeRDP 2.0.0-rc2 is the third release candidate for 2.0.0. +For a complete and detailed change log since the last release candidate run: +git log 2.0.0-rc1..2.0.0-rc2 + +Noteworthy features and improvements: + +* IMPORTANT: add support CredSSP v6 - this fixes KB4088776 see #4449, #4488 +* basic support for the "Video Optimized Remoting Virtual Channel Extension" (MS-RDPEVOR) was added +* many smart card related fixes and cleanups #4312 +* fix ccache support +* fix OpenSSL 1.1.0 detection on Windows +* fix IPv6 handling on Windows +* add support for memory and thread sanitizer +* support for dynamic resloution changes was added in xfreerdp #4313 +* support for gateway access token (command line option /gat) was added +* initial support for travis-ci.org was added +* SSE optimization version of RGB to AVC444 frame split was added +* build: -msse2/-msse3 are not enabled globally anymore + +Fixed github issues (excerpt): + +* #4227 Convert settings->Password to binary blob +* #4231 freerdp-2.0.0_rc0: 5 tests failed out of 184 on ppc +* #4276 Big endian fixes +* #4291 xfreerdp “Segmentation fault” when connecting to freerdp-shadow-cli +* #4293 [X11] shadow server memory corruption with /monitors:2 #4293 +* #4296 drive redirection - raise an error if the directory can't be founde +* #4306 Cannot connect to shadow server with NLA auth: SEC_E_OUT_OF_SEQUENCE +* #4447 Apple rpath namespace fixes +* #4457 Fix /size: /w: /h: with /monitors: (Fix custom sizes) +* #4527 pre-connection blob (pcb) support in .rdp files +* #4552 Fix Windows 10 cursors drawing as black +* smartcard related: #3521, #3431, #3474, #3488, #775, #1424 + +See https://github.com/FreeRDP/FreeRDP/milestone/8 for a complete list. + + +# 2017-11-28 Version 2.0.0-rc1 + +FreeRDP 2.0.0-rc1 is the second release candidate for 2.0.0. +For a complete and detailed change log since the last release candidate run: +git log 2.0.0-rc0..master + +Noteworthy features and improvements: + +* support for FIPS mode was added (option +fipsmode) +* initial client side kerberos support (run cmake with WITH_GSSAPI) +* support for ssh-agent redirection (as rdp channel) +* the man page(s) and /help were updated an improved +* build: support for GNU/kFreeBSD +* add support for ICU for unicode conversion (-DWITH_ICU=ON) +* client add option to force password prompt before connection (/from-stdin[:force]) +* add Samsung DeX support +* extend /size to allow width or height percentages (#4146) +* add support for "password is pin" +* clipboard is now enabled per default (use -clipboard to disable) + +Fixed github issues (excerpt): + +* #4281: Added option to prefer IPv6 over IPv4 +* #3890: Point to OpenSSL doc for private CA +* #3378: support 31 static channels as described in the spec +* #1536: fix clipboard on mac +* #4253: Rfx decode tile width. +* #3267: fix parsing of drivestoredirect +* #4257: Proper error checks for /kbd argument +* #4249: Corruption due to recursive parser +* #4111: 15bpp color handling for brush. +* #3509: Added Ctrl+Alt+Enter description +* #3211: Return freerdp error from main. +* #3513: add better description for drive redirection +* #4199: ConvertFindDataAToW string length +* #4135: client/x11: fix colors on big endian +* #4089: fix h264 context leak when DeleteSurface +* #4117: possible segfault +* #4091: fix a regression with remote program + +See https://github.com/FreeRDP/FreeRDP/milestone/7 for a complete list. + + +2012-02-07 Version 1.0.1 + +FreeRDP 1.0.1 is a maintenance release to address a certain number of +issues found in 1.0.0. This release also brings corrective measures +to certificate validation which were required for inclusion in Ubuntu. + +* Certificate Validation + * Improved validation logic and robustness + * Added validation of certificate name against hostname + +* Token-based Server Redirection + * Fixed redirection logic + * HAProxy load-balancer support + +* xfreerdp-server + * better event handling + * capture performance improvements + +* wfreerdp + * Fix RemoteFX support + * Fix mingw64 compilation + +* libfreerdp-core: + * Fix severe TCP sending bug + * Added server-side Standard RDP security + +2012-01-16 Version 1.0.0 + +License: + +FreeRDP 1.0 is the first release of FreeRDP under the Apache License 2.0. +The FreeRDP 1.x series is a rewrite, meaning there is no continuity with +the previous FreeRDP 0.x series which were released under GPLv2. + +New Features: + +* RemoteFX + * Both encoder and decoder + * SSE2 and NEON optimization +* NSCodec +* RemoteApp + * Working, minor glitches +* Multimedia Redirection + * ffmpeg support +* Network Level Authentication (NLA) + * NTLMv2 +* Certificate validation +* FIPS-compliant RDP security +* new build system (cmake) +* added official logo and icon + +New Architecture: + +* libfreerdp-core + * core protocol + * highly portable + * both client and server +* libfreerdp-cache + * caching operations +* libfreerdp-codec + * bitmap decompression + * codec encoding/decoding +* libfreerdp-kbd + * keyboard mapping +* libfreerdp-channels + * virtual channel management + * client and server side support +* libfreerdp-gdi + * extensively unit tested + * portable software GDI implementation +* libfreerdp-rail + * RemoteApp library +* libfreerdp-utils + * shared utility library + +FreeRDP Clients: + +* client/X11 (xfreerdp) + * official client + * RemoteApp support + * X11 GDI implementation +* client/DirectFB (dfreerdp) + * DirectFB support + * software-based GDI (libfreerdp-gdi) +* client/Windows (wfreerdp) + * Native Win32 support + +FreeRDP Servers (experimental): + +* server/X11 (xfreerdp-server) + * RemoteFX-only + * no authentication + * highly experimental + * keyboard and mouse input supported + +Virtual Channels: + +* cliprdr (Clipboard Redirection) +* rail (RemoteApp) +* drdynvc (Dynamic Virtual Channels) + * audin (Audio Input Redirection) + * alsa support + * pulse support + * tsmf (Multimedia Redirection) + * alsa support + * pulse support + * ffmpeg support +* rdpdr (Device Redirection) + * disk (Disk Redirection) + * parallel (Parallel Port Redirection) + * serial (Serial Port Redirection) + * printer (Printer Redirection) + * CUPS support + * smartcard (Smartcard Redirection) +* rdpsnd (Sound Redirection) + * alsa support + * pulse support diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed 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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..70cf40d --- /dev/null +++ b/README.md @@ -0,0 +1,30 @@ +# FreeRDP: A Remote Desktop Protocol Implementation + +FreeRDP is a free implementation of the Remote Desktop Protocol (RDP), released under the Apache license. +Enjoy the freedom of using your software wherever you want, the way you want it, in a world where +interoperability can finally liberate your computing experience. + +## Resources + +Project website: https://www.freerdp.com/ +Issue tracker: https://github.com/FreeRDP/FreeRDP/issues +Sources: https://github.com/FreeRDP/FreeRDP/ +Downloads: https://pub.freerdp.com/releases/ +Wiki: https://github.com/FreeRDP/FreeRDP/wiki +API documentation: https://pub.freerdp.com/api/ + +IRC channel: #freerdp @ irc.freenode.net +Mailing list: https://lists.sourceforge.net/lists/listinfo/freerdp-devel + +## Microsoft Open Specifications + +Information regarding the Microsoft Open Specifications can be found at: +http://www.microsoft.com/openspecifications/ + +A list of reference documentation is maintained here: +https://github.com/FreeRDP/FreeRDP/wiki/Reference-Documentation + +## Compilation + +Instructions on how to get started compiling FreeRDP can be found on the wiki: +https://github.com/FreeRDP/FreeRDP/wiki/Compilation diff --git a/buildflags.h.in b/buildflags.h.in new file mode 100644 index 0000000..0cc4a64 --- /dev/null +++ b/buildflags.h.in @@ -0,0 +1,11 @@ +#ifndef FREERDP_BUILD_FLAGS_H +#define FREERDP_BUILD_FLAGS_H + +#define CFLAGS "${CMAKE_C_FLAGS}" +#define COMPILER_ID "${CMAKE_C_COMPILER_ID}" +#define COMPILER_VERSION "${CMAKE_C_COMPILER_VERSION}" +#define TARGET_ARCH "${TARGET_ARCH}" +#define BUILD_CONFIG "${FREERDP_BUILD_CONFIG}" +#define BUILD_TYPE "${CMAKE_BUILD_TYPE}" + +#endif /* FREERDP_BUILD_FLAGS_H */ diff --git a/channels/CMakeLists.txt b/channels/CMakeLists.txt new file mode 100644 index 0000000..882fef7 --- /dev/null +++ b/channels/CMakeLists.txt @@ -0,0 +1,346 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2012 Marc-Andre Moreau +# +# Licensed 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. + +set(CMAKE_POSITION_INDEPENDENT_CODE ON) + +include(CMakeParseArguments) +include(CMakeDependentOption) + +macro(define_channel_options) + set(PREFIX "CHANNEL") + + cmake_parse_arguments(${PREFIX} + "" + "NAME;TYPE;DESCRIPTION;SPECIFICATIONS;DEFAULT" + "" + ${ARGN}) + + string(TOUPPER "CHANNEL_${CHANNEL_NAME}" CHANNEL_OPTION) + string(TOUPPER "CHANNEL_${CHANNEL_NAME}_CLIENT" CHANNEL_CLIENT_OPTION) + string(TOUPPER "CHANNEL_${CHANNEL_NAME}_SERVER" CHANNEL_SERVER_OPTION) + string(TOUPPER "${CHANNEL_TYPE}" CHANNEL_TYPE) + + if(${${CHANNEL_CLIENT_OPTION}}) + set(OPTION_CLIENT_DEFAULT ${${CHANNEL_CLIENT_OPTION}}) + endif() + + if(${${CHANNEL_SERVER_OPTION}}) + set(OPTION_SERVER_DEFAULT ${${CHANNEL_SERVER_OPTION}}) + endif() + + if(${${CHANNEL_OPTION}}) + set(OPTION_DEFAULT ${${CHANNEL_OPTION}}) + endif() + + if(${OPTION_CLIENT_DEFAULT} OR ${OPTION_SERVER_DEFAULT}) + set(OPTION_DEFAULT "ON") + endif() + + set(CHANNEL_DEFAULT ${OPTION_DEFAULT}) + + set(CHANNEL_OPTION_DOC "Build ${CHANNEL_NAME} ${CHANNEL_TYPE} channel") + + if ("${CHANNEL_TYPE}" STREQUAL "DYNAMIC") + CMAKE_DEPENDENT_OPTION(${CHANNEL_OPTION} "${CHANNEL_OPTION_DOC}" ${CHANNEL_DEFAULT} "CHANNEL_DRDYNVC" OFF) + else() + option(${CHANNEL_OPTION} "${CHANNEL_OPTION_DOC}" ${CHANNEL_DEFAULT}) + endif() + +endmacro(define_channel_options) + +macro(define_channel_client_options _channel_client_default) + string(TOUPPER "CHANNEL_${CHANNEL_NAME}_CLIENT" CHANNEL_CLIENT_OPTION) + string(TOUPPER "CHANNEL_${CHANNEL_NAME}" CHANNEL_OPTION) + set(CHANNEL_CLIENT_OPTION_DOC "Build ${CHANNEL_NAME} ${CHANNEL_TYPE} channel client") + + CMAKE_DEPENDENT_OPTION(${CHANNEL_CLIENT_OPTION} "${CHANNEL_CLIENT_OPTION_DOC}" + ${_channel_client_default} "${CHANNEL_OPTION}" OFF) +endmacro(define_channel_client_options) + +macro(define_channel_server_options _channel_server_default) + string(TOUPPER "CHANNEL_${CHANNEL_NAME}_SERVER" CHANNEL_SERVER_OPTION) + string(TOUPPER "CHANNEL_${CHANNEL_NAME}" CHANNEL_OPTION) + set(CHANNEL_SERVER_OPTION_DOC "Build ${CHANNEL_NAME} ${CHANNEL_TYPE} channel server") + + CMAKE_DEPENDENT_OPTION(${CHANNEL_SERVER_OPTION} "${CHANNEL_SERVER_OPTION_DOC}" + ${_channel_server_default} "${CHANNEL_OPTION}" OFF) +endmacro(define_channel_server_options) + +macro(define_channel _channel_name) + set(CHANNEL_NAME ${_channel_name}) + set(MODULE_NAME ${CHANNEL_NAME}) + string(TOUPPER "CHANNEL_${CHANNEL_NAME}" MODULE_PREFIX) +endmacro(define_channel) + +macro(define_channel_client _channel_name) + set(CHANNEL_NAME ${_channel_name}) + set(MODULE_NAME "${CHANNEL_NAME}-client") + string(TOUPPER "CHANNEL_${CHANNEL_NAME}_CLIENT" MODULE_PREFIX) +endmacro(define_channel_client) + +macro(define_channel_server _channel_name) + set(CHANNEL_NAME ${_channel_name}) + set(MODULE_NAME "${CHANNEL_NAME}-server") + string(TOUPPER "CHANNEL_${CHANNEL_NAME}_SERVER" MODULE_PREFIX) +endmacro(define_channel_server) + +macro(define_channel_client_subsystem _channel_name _subsystem _type) + set(CHANNEL_NAME ${_channel_name}) + set(CHANNEL_SUBSYSTEM ${_subsystem}) + string(LENGTH "${_type}" _type_length) + string(TOUPPER "CHANNEL_${CHANNEL_NAME}_CLIENT" CHANNEL_PREFIX) + if(_type_length GREATER 0) + set(SUBSYSTEM_TYPE ${_type}) + set(MODULE_NAME "${CHANNEL_NAME}-client-${CHANNEL_SUBSYSTEM}-${SUBSYSTEM_TYPE}") + string(TOUPPER "CHANNEL_${CHANNEL_NAME}_CLIENT_${CHANNEL_SUBSYSTEM}_${SUBSYSTEM_TYPE}" MODULE_PREFIX) + else() + set(MODULE_NAME "${CHANNEL_NAME}-client-${CHANNEL_SUBSYSTEM}") + string(TOUPPER "CHANNEL_${CHANNEL_NAME}_CLIENT_${CHANNEL_SUBSYSTEM}" MODULE_PREFIX) + endif() +endmacro(define_channel_client_subsystem) + +macro(define_channel_server_subsystem _channel_name _subsystem _type) + set(CHANNEL_NAME ${_channel_name}) + set(CHANNEL_SUBSYSTEM ${_subsystem}) + set(MODULE_NAME "${CHANNEL_NAME}-server-${CHANNEL_SUBSYSTEM}") + string(TOUPPER "CHANNEL_${CHANNEL_NAME}_server_${CHANNEL_SUBSYSTEM}" MODULE_PREFIX) +endmacro(define_channel_server_subsystem) + +macro(add_channel_client _channel_prefix _channel_name) + add_subdirectory(client) + if(${${_channel_prefix}_CLIENT_STATIC}) + set(CHANNEL_STATIC_CLIENT_MODULES ${CHANNEL_STATIC_CLIENT_MODULES} ${_channel_prefix} PARENT_SCOPE) + set(${_channel_prefix}_CLIENT_NAME ${${_channel_prefix}_CLIENT_NAME} PARENT_SCOPE) + set(${_channel_prefix}_CLIENT_CHANNEL ${${_channel_prefix}_CLIENT_CHANNEL} PARENT_SCOPE) + set(${_channel_prefix}_CLIENT_ENTRY ${${_channel_prefix}_CLIENT_ENTRY} PARENT_SCOPE) + set(CHANNEL_STATIC_CLIENT_ENTRIES ${CHANNEL_STATIC_CLIENT_ENTRIES} ${${_channel_prefix}_CLIENT_ENTRY} PARENT_SCOPE) + endif() +endmacro(add_channel_client) + +macro(add_channel_server _channel_prefix _channel_name) + add_subdirectory(server) + if(${${_channel_prefix}_SERVER_STATIC}) + set(CHANNEL_STATIC_SERVER_MODULES ${CHANNEL_STATIC_SERVER_MODULES} ${_channel_prefix} PARENT_SCOPE) + set(${_channel_prefix}_SERVER_NAME ${${_channel_prefix}_SERVER_NAME} PARENT_SCOPE) + set(${_channel_prefix}_SERVER_CHANNEL ${${_channel_prefix}_SERVER_CHANNEL} PARENT_SCOPE) + set(${_channel_prefix}_SERVER_ENTRY ${${_channel_prefix}_SERVER_ENTRY} PARENT_SCOPE) + set(CHANNEL_STATIC_SERVER_ENTRIES ${CHANNEL_STATIC_SERVER_ENTRIES} ${${_channel_prefix}_SERVER_ENTRY} PARENT_SCOPE) + endif() +endmacro(add_channel_server) + +macro(add_channel_client_subsystem _channel_prefix _channel_name _subsystem _type) + add_subdirectory(${_subsystem}) + set(_channel_module_name "${_channel_name}-client") + string(LENGTH "${_type}" _type_length) + if(_type_length GREATER 0) + string(TOUPPER "CHANNEL_${_channel_name}_CLIENT_${_subsystem}_${_type}" _subsystem_prefix) + else() + string(TOUPPER "CHANNEL_${_channel_name}_CLIENT_${_subsystem}" _subsystem_prefix) + endif() + if(${${_subsystem_prefix}_STATIC}) + get_target_property(CHANNEL_SUBSYSTEMS ${_channel_module_name} SUBSYSTEMS) + if(_type_length GREATER 0) + set(SUBSYSTEMS ${SUBSYSTEMS} "${_subsystem}-${_type}") + else() + set(SUBSYSTEMS ${SUBSYSTEMS} ${_subsystem}) + endif() + set_target_properties(${_channel_module_name} PROPERTIES SUBSYSTEMS "${SUBSYSTEMS}") + endif() +endmacro(add_channel_client_subsystem) + +macro(channel_install _targets _destination _export_target) + install(TARGETS ${_targets} DESTINATION ${_destination} EXPORT ${_export_target}) +endmacro(channel_install) + +macro(server_channel_install _targets _destination) + channel_install(${_targets} ${_destination} "FreeRDP-ServerTargets") +endmacro(server_channel_install) + +macro(client_channel_install _targets _destination) + channel_install(${_targets} ${_destination} "FreeRDP-ClientTargets") +endmacro(client_channel_install) + +macro(add_channel_client_library _module_prefix _module_name _channel_name _dynamic _entry) + set(_lnk_dir ${${_module_prefix}_LINK_DIRS}) + if (NOT "${_lnk_dir}" STREQUAL "") + link_directories(${_lnk_dir}) + endif() + + if(${_dynamic} AND (NOT BUILTIN_CHANNELS)) +# On windows create dll version information. +# Vendor, product and year are already set in top level CMakeLists.txt + if (WIN32) + set (RC_VERSION_MAJOR ${FREERDP_VERSION_MAJOR}) + set (RC_VERSION_MINOR ${FREERDP_VERSION_MINOR}) + set (RC_VERSION_BUILD ${FREERDP_VERSION_REVISION}) + set (RC_VERSION_PATCH 0) + set (RC_VERSION_FILE "${CMAKE_SHARED_LIBRARY_PREFIX}${_module_name}${CMAKE_SHARED_LIBRARY_SUFFIX}" ) + + configure_file( + ${CMAKE_SOURCE_DIR}/cmake/WindowsDLLVersion.rc.in + ${CMAKE_CURRENT_BINARY_DIR}/version.rc + @ONLY) + + set ( ${_module_prefix}_SRCS ${${_module_prefix}_SRCS} ${CMAKE_CURRENT_BINARY_DIR}/version.rc) + endif() + + add_library(${_module_name} ${${_module_prefix}_SRCS}) + target_link_libraries(${_module_name} ${${_module_prefix}_LIBS}) + client_channel_install(${_module_name} ${FREERDP_ADDIN_PATH}) + else() + set(${_module_prefix}_STATIC ON PARENT_SCOPE) + set(${_module_prefix}_NAME ${_module_name} PARENT_SCOPE) + set(${_module_prefix}_CHANNEL ${_channel_name} PARENT_SCOPE) + set(${_module_prefix}_ENTRY ${_entry} PARENT_SCOPE) + add_library(${_module_name} STATIC ${${_module_prefix}_SRCS}) + target_link_libraries(${_module_name} ${${_module_prefix}_LIBS}) + + if (${CMAKE_VERSION} VERSION_LESS 2.8.12 OR NOT BUILD_SHARED_LIBS) + client_channel_install(${_module_name} ${FREERDP_ADDIN_PATH}) + endif() + endif() +endmacro(add_channel_client_library) + +macro(add_channel_client_subsystem_library _module_prefix _module_name _channel_name _type _dynamic _entry) + set(_lnk_dir ${${_module_prefix}_LINK_DIRS}) + if (NOT "${_lnk_dir}" STREQUAL "") + link_directories(${_lnk_dir}) + endif() + + if(${_dynamic} AND (NOT BUILTIN_CHANNELS)) +# On windows create dll version information. +# Vendor, product and year are already set in top level CMakeLists.txt + if (WIN32) + set (RC_VERSION_MAJOR ${FREERDP_VERSION_MAJOR}) + set (RC_VERSION_MINOR ${FREERDP_VERSION_MINOR}) + set (RC_VERSION_BUILD ${FREERDP_VERSION_REVISION}) + set (RC_VERSION_PATCH 0) + set (RC_VERSION_FILE "${CMAKE_SHARED_LIBRARY_PREFIX}${_module_name}${CMAKE_SHARED_LIBRARY_SUFFIX}" ) + + configure_file( + ${CMAKE_SOURCE_DIR}/cmake/WindowsDLLVersion.rc.in + ${CMAKE_CURRENT_BINARY_DIR}/version.rc + @ONLY) + + set ( ${_module_prefix}_SRCS ${${_module_prefix}_SRCS} ${CMAKE_CURRENT_BINARY_DIR}/version.rc) + endif() + + add_library(${_module_name} ${${_module_prefix}_SRCS}) + target_link_libraries(${_module_name} ${${_module_prefix}_LIBS}) + client_channel_install(${_module_name} ${FREERDP_ADDIN_PATH}) + else() + set(${_module_prefix}_STATIC ON PARENT_SCOPE) + set(${_module_prefix}_NAME ${_module_name} PARENT_SCOPE) + set(${_module_prefix}_TYPE ${_type} PARENT_SCOPE) + + add_library(${_module_name} STATIC ${${_module_prefix}_SRCS}) + target_link_libraries(${_module_name} ${${_module_prefix}_LIBS}) + if (${CMAKE_VERSION} VERSION_LESS 2.8.12 OR NOT BUILD_SHARED_LIBS) + client_channel_install(${_module_name} ${FREERDP_ADDIN_PATH}) + endif() + endif() +endmacro(add_channel_client_subsystem_library) + +macro(add_channel_server_library _module_prefix _module_name _channel_name _dynamic _entry) + set(_lnk_dir ${${_module_prefix}_LINK_DIRS}) + if (NOT "${_lnk_dir}" STREQUAL "") + link_directories(${_lnk_dir}) + endif() + + if(${_dynamic} AND (NOT BUILTIN_CHANNELS)) +# On windows create dll version information. +# Vendor, product and year are already set in top level CMakeLists.txt + if (WIN32) + set (RC_VERSION_MAJOR ${FREERDP_VERSION_MAJOR}) + set (RC_VERSION_MINOR ${FREERDP_VERSION_MINOR}) + set (RC_VERSION_BUILD ${FREERDP_VERSION_REVISION}) + set (RC_VERSION_FILE "${CMAKE_SHARED_LIBRARY_PREFIX}${_module_name}${CMAKE_SHARED_LIBRARY_SUFFIX}" ) + + configure_file( + ${CMAKE_SOURCE_DIR}/cmake/WindowsDLLVersion.rc.in + ${CMAKE_CURRENT_BINARY_DIR}/version.rc + @ONLY) + + set ( ${_module_prefix}_SRCS ${${_module_prefix}_SRCS} ${CMAKE_CURRENT_BINARY_DIR}/version.rc) + endif() + + add_library(${_module_name} ${${_module_prefix}_SRCS}) + server_channel_install(${_module_name} ${FREERDP_ADDIN_PATH}) + else() + set(${_module_prefix}_STATIC ON PARENT_SCOPE) + set(${_module_prefix}_NAME ${_module_name} PARENT_SCOPE) + set(${_module_prefix}_CHANNEL ${_channel_name} PARENT_SCOPE) + set(${_module_prefix}_ENTRY ${_entry} PARENT_SCOPE) + add_library(${_module_name} STATIC ${${_module_prefix}_SRCS}) + if (${CMAKE_VERSION} VERSION_LESS 2.8.12 OR NOT BUILD_SHARED_LIBS) + server_channel_install(${_module_name} ${FREERDP_ADDIN_PATH}) + endif() + endif() +endmacro(add_channel_server_library) + +set(FILENAME "ChannelOptions.cmake") +file(GLOB FILEPATHS RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} "*/${FILENAME}") + +# We need special treatement for drdynvc: +# It needs to be the first entry so that every +# dynamic channel has the dependent options available. +set(DRDYNVC_MATCH "") + +foreach(FILEPATH ${FILEPATHS}) + if(${FILEPATH} MATCHES "^([^/]*)drdynvc/+${FILENAME}") + set(DRDYNVC_MATCH ${FILEPATH}) + endif() +endforeach() + +if (NOT "${DRDYNVC_MATCH}" STREQUAL "") + list(REMOVE_ITEM FILEPATHS ${DRDYNVC_MATCH}) + list(APPEND FILEPATHS ${DRDYNVC_MATCH}) + list(REVERSE FILEPATHS) # list PREPEND is not available on old CMake3 +endif() + +foreach(FILEPATH ${FILEPATHS}) + if(${FILEPATH} MATCHES "^([^/]*)/+${FILENAME}") + string(REGEX REPLACE "^([^/]*)/+${FILENAME}" "\\1" DIR ${FILEPATH}) + set(CHANNEL_OPTION) + include(${FILEPATH}) + if(${CHANNEL_OPTION}) + set(CHANNEL_MESSAGE "Adding ${CHANNEL_TYPE} channel") + if(${CHANNEL_CLIENT_OPTION}) + set(CHANNEL_MESSAGE "${CHANNEL_MESSAGE} client") + endif() + if(${CHANNEL_SERVER_OPTION}) + set(CHANNEL_MESSAGE "${CHANNEL_MESSAGE} server") + endif() + set(CHANNEL_MESSAGE "${CHANNEL_MESSAGE} \"${CHANNEL_NAME}\"") + set(CHANNEL_MESSAGE "${CHANNEL_MESSAGE}: ${CHANNEL_DESCRIPTION}") + message(STATUS "${CHANNEL_MESSAGE}") + add_subdirectory(${DIR}) + endif() + endif() +endforeach(FILEPATH) + +if(WITH_CLIENT_CHANNELS) + add_subdirectory(client) + set(FREERDP_CHANNELS_CLIENT_SRCS ${FREERDP_CHANNELS_CLIENT_SRCS} PARENT_SCOPE) + set(FREERDP_CHANNELS_CLIENT_LIBS ${FREERDP_CHANNELS_CLIENT_LIBS} PARENT_SCOPE) +endif() + +if(WITH_SERVER_CHANNELS) + add_subdirectory(server) + set(FREERDP_CHANNELS_SERVER_SRCS ${FREERDP_CHANNELS_SERVER_SRCS} PARENT_SCOPE) + set(FREERDP_CHANNELS_SERVER_LIBS ${FREERDP_CHANNELS_SERVER_LIBS} PARENT_SCOPE) +endif() diff --git a/channels/ainput/CMakeLists.txt b/channels/ainput/CMakeLists.txt new file mode 100644 index 0000000..b1d1a6a --- /dev/null +++ b/channels/ainput/CMakeLists.txt @@ -0,0 +1,27 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2022 Armin Novak +# Copyright 2022 Thincast Technologies GmbH +# +# Licensed 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. + +define_channel("ainput") + +if(WITH_CLIENT_CHANNELS) + add_channel_client(${MODULE_PREFIX} ${CHANNEL_NAME}) +endif() + +if(WITH_SERVER_CHANNELS) + add_channel_server(${MODULE_PREFIX} ${CHANNEL_NAME}) +endif() diff --git a/channels/ainput/ChannelOptions.cmake b/channels/ainput/ChannelOptions.cmake new file mode 100644 index 0000000..eafc6c0 --- /dev/null +++ b/channels/ainput/ChannelOptions.cmake @@ -0,0 +1,13 @@ + +set(OPTION_DEFAULT OFF) +set(OPTION_CLIENT_DEFAULT ON) +set(OPTION_SERVER_DEFAULT ON) + +define_channel_options(NAME "ainput" TYPE "dynamic" + DESCRIPTION "Advanced Input Virtual Channel Extension" + SPECIFICATIONS "[XXXXX]" + DEFAULT ${OPTION_DEFAULT}) + +define_channel_client_options(${OPTION_CLIENT_DEFAULT}) +define_channel_server_options(${OPTION_SERVER_DEFAULT}) + diff --git a/channels/ainput/client/CMakeLists.txt b/channels/ainput/client/CMakeLists.txt new file mode 100644 index 0000000..4cf2d22 --- /dev/null +++ b/channels/ainput/client/CMakeLists.txt @@ -0,0 +1,34 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2022 Armin Novak +# Copyright 2022 Thincast Technologies GmbH +# +# Licensed 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. + +define_channel_client("ainput") + +set(${MODULE_PREFIX}_SRCS + ainput_main.c + ainput_main.h) + +include_directories(..) + +add_channel_client_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} TRUE "DVCPluginEntry") + +if (WITH_DEBUG_SYMBOLS AND MSVC AND NOT BUILTIN_CHANNELS AND BUILD_SHARED_LIBS) + install(FILES ${PROJECT_BINARY_DIR}/${MODULE_NAME}.pdb DESTINATION ${FREERDP_ADDIN_PATH} COMPONENT symbols) +endif() + +target_link_libraries(${MODULE_NAME} winpr) +set_property(TARGET ${MODULE_NAME} PROPERTY FOLDER "Channels/${CHANNEL_NAME}/Client") diff --git a/channels/ainput/client/ainput_main.c b/channels/ainput/client/ainput_main.c new file mode 100644 index 0000000..d8eb8ec --- /dev/null +++ b/channels/ainput/client/ainput_main.c @@ -0,0 +1,315 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Advanced Input Virtual Channel Extension + * + * Copyright 2022 Armin Novak + * Copyright 2022 Thincast Technologies GmbH + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include + +#include +#include +#include +#include + +#include "ainput_main.h" +#include +#include +#include + +#include "../common/ainput_common.h" + +#define TAG CHANNELS_TAG("ainput.client") + +typedef struct AINPUT_CHANNEL_CALLBACK_ AINPUT_CHANNEL_CALLBACK; +struct AINPUT_CHANNEL_CALLBACK_ +{ + IWTSVirtualChannelCallback iface; + + IWTSPlugin* plugin; + IWTSVirtualChannelManager* channel_mgr; + IWTSVirtualChannel* channel; +}; + +typedef struct AINPUT_LISTENER_CALLBACK_ AINPUT_LISTENER_CALLBACK; +struct AINPUT_LISTENER_CALLBACK_ +{ + IWTSListenerCallback iface; + + IWTSPlugin* plugin; + IWTSVirtualChannelManager* channel_mgr; + AINPUT_CHANNEL_CALLBACK* channel_callback; +}; + +typedef struct AINPUT_PLUGIN_ AINPUT_PLUGIN; +struct AINPUT_PLUGIN_ +{ + IWTSPlugin iface; + + AINPUT_LISTENER_CALLBACK* listener_callback; + IWTSListener* listener; + UINT32 MajorVersion; + UINT32 MinorVersion; + BOOL initialized; +}; + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT ainput_on_data_received(IWTSVirtualChannelCallback* pChannelCallback, wStream* data) +{ + UINT16 type; + AINPUT_PLUGIN* ainput; + AINPUT_CHANNEL_CALLBACK* callback = (AINPUT_CHANNEL_CALLBACK*)pChannelCallback; + + WINPR_ASSERT(callback); + WINPR_ASSERT(data); + + ainput = (AINPUT_PLUGIN*)callback->plugin; + WINPR_ASSERT(ainput); + + if (Stream_GetRemainingLength(data) < 2) + return ERROR_NO_DATA; + Stream_Read_UINT16(data, type); + switch (type) + { + case MSG_AINPUT_VERSION: + if (Stream_GetRemainingLength(data) < 8) + return ERROR_NO_DATA; + Stream_Read_UINT32(data, ainput->MajorVersion); + Stream_Read_UINT32(data, ainput->MinorVersion); + break; + default: + WLog_WARN(TAG, "Received unsupported message type 0x%04" PRIx16, type); + break; + } + + return CHANNEL_RC_OK; +} + +static UINT ainput_send_input_event(AInputClientContext* context, UINT64 flags, INT32 x, INT32 y) +{ + AINPUT_PLUGIN* ainput; + AINPUT_CHANNEL_CALLBACK* callback; + BYTE buffer[32] = { 0 }; + UINT64 time; + wStream sbuffer = { 0 }; + wStream* s = &sbuffer; + + Stream_StaticInit(&sbuffer, buffer, sizeof(buffer)); + + WINPR_ASSERT(s); + WINPR_ASSERT(context); + + time = GetTickCount64(); + ainput = (AINPUT_PLUGIN*)context->handle; + WINPR_ASSERT(ainput); + WINPR_ASSERT(ainput->listener_callback); + + if (ainput->MajorVersion != AINPUT_VERSION_MAJOR) + { + WLog_WARN(TAG, "Unsupported channel version %" PRIu32 ".%" PRIu32 ", aborting.", + ainput->MajorVersion, ainput->MinorVersion); + return CHANNEL_RC_UNSUPPORTED_VERSION; + } + callback = ainput->listener_callback->channel_callback; + WINPR_ASSERT(callback); + + { + char buffer[128] = { 0 }; + WLog_VRB(TAG, "[%s] sending timestamp=0x%08" PRIx64 ", flags=%s, %" PRId32 "x%" PRId32, + __FUNCTION__, time, ainput_flags_to_string(flags, buffer, sizeof(buffer)), x, y); + } + + /* Message type */ + Stream_Write_UINT16(s, MSG_AINPUT_MOUSE); + + /* Event data */ + Stream_Write_UINT64(s, time); + Stream_Write_UINT64(s, flags); + Stream_Write_INT32(s, x); + Stream_Write_INT32(s, y); + Stream_SealLength(s); + + /* ainput back what we have received. AINPUT does not have any message IDs. */ + WINPR_ASSERT(callback->channel); + WINPR_ASSERT(callback->channel->Write); + return callback->channel->Write(callback->channel, (ULONG)Stream_Length(s), Stream_Buffer(s), + NULL); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT ainput_on_close(IWTSVirtualChannelCallback* pChannelCallback) +{ + AINPUT_CHANNEL_CALLBACK* callback = (AINPUT_CHANNEL_CALLBACK*)pChannelCallback; + + free(callback); + + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT ainput_on_new_channel_connection(IWTSListenerCallback* pListenerCallback, + IWTSVirtualChannel* pChannel, BYTE* Data, + BOOL* pbAccept, + IWTSVirtualChannelCallback** ppCallback) +{ + AINPUT_CHANNEL_CALLBACK* callback; + AINPUT_LISTENER_CALLBACK* listener_callback = (AINPUT_LISTENER_CALLBACK*)pListenerCallback; + + WINPR_ASSERT(listener_callback); + WINPR_UNUSED(Data); + WINPR_UNUSED(pbAccept); + + callback = (AINPUT_CHANNEL_CALLBACK*)calloc(1, sizeof(AINPUT_CHANNEL_CALLBACK)); + + if (!callback) + { + WLog_ERR(TAG, "calloc failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + callback->iface.OnDataReceived = ainput_on_data_received; + callback->iface.OnClose = ainput_on_close; + callback->plugin = listener_callback->plugin; + callback->channel_mgr = listener_callback->channel_mgr; + callback->channel = pChannel; + listener_callback->channel_callback = callback; + + *ppCallback = &callback->iface; + + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT ainput_plugin_initialize(IWTSPlugin* pPlugin, IWTSVirtualChannelManager* pChannelMgr) +{ + UINT status; + AINPUT_PLUGIN* ainput = (AINPUT_PLUGIN*)pPlugin; + + WINPR_ASSERT(ainput); + + if (ainput->initialized) + { + WLog_ERR(TAG, "[%s] channel initialized twice, aborting", AINPUT_DVC_CHANNEL_NAME); + return ERROR_INVALID_DATA; + } + ainput->listener_callback = + (AINPUT_LISTENER_CALLBACK*)calloc(1, sizeof(AINPUT_LISTENER_CALLBACK)); + + if (!ainput->listener_callback) + { + WLog_ERR(TAG, "calloc failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + ainput->listener_callback->iface.OnNewChannelConnection = ainput_on_new_channel_connection; + ainput->listener_callback->plugin = pPlugin; + ainput->listener_callback->channel_mgr = pChannelMgr; + + status = pChannelMgr->CreateListener(pChannelMgr, AINPUT_DVC_CHANNEL_NAME, 0, + &ainput->listener_callback->iface, &ainput->listener); + + ainput->listener->pInterface = ainput->iface.pInterface; + ainput->initialized = status == CHANNEL_RC_OK; + return status; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT ainput_plugin_terminated(IWTSPlugin* pPlugin) +{ + AINPUT_PLUGIN* ainput = (AINPUT_PLUGIN*)pPlugin; + if (ainput && ainput->listener_callback) + { + IWTSVirtualChannelManager* mgr = ainput->listener_callback->channel_mgr; + if (mgr) + IFCALL(mgr->DestroyListener, mgr, ainput->listener); + } + if (ainput) + { + free(ainput->listener_callback); + free(ainput->iface.pInterface); + } + free(ainput); + + return CHANNEL_RC_OK; +} + +#ifdef BUILTIN_CHANNELS +#define DVCPluginEntry ainput_DVCPluginEntry +#else +#define DVCPluginEntry FREERDP_API DVCPluginEntry +#endif + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT DVCPluginEntry(IDRDYNVC_ENTRY_POINTS* pEntryPoints) +{ + UINT status = CHANNEL_RC_OK; + AINPUT_PLUGIN* ainput = (AINPUT_PLUGIN*)pEntryPoints->GetPlugin(pEntryPoints, "ainput"); + + if (!ainput) + { + AInputClientContext* context = (AInputClientContext*)calloc(1, sizeof(AInputClientContext)); + ainput = (AINPUT_PLUGIN*)calloc(1, sizeof(AINPUT_PLUGIN)); + + if (!ainput || !context) + { + free(context); + free(ainput); + + WLog_ERR(TAG, "calloc failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + ainput->iface.Initialize = ainput_plugin_initialize; + ainput->iface.Terminated = ainput_plugin_terminated; + + context->handle = (void*)ainput; + context->AInputSendInputEvent = ainput_send_input_event; + ainput->iface.pInterface = (void*)context; + + status = pEntryPoints->RegisterPlugin(pEntryPoints, AINPUT_CHANNEL_NAME, &ainput->iface); + } + + return status; +} diff --git a/channels/ainput/client/ainput_main.h b/channels/ainput/client/ainput_main.h new file mode 100644 index 0000000..8a19ad9 --- /dev/null +++ b/channels/ainput/client/ainput_main.h @@ -0,0 +1,43 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Advanced Input Virtual Channel Extension + * + * Copyright 2022 Armin Novak + * Copyright 2022 Thincast Technologies GmbH + * + * Licensed 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 FREERDP_CHANNEL_AINPUT_CLIENT_MAIN_H +#define FREERDP_CHANNEL_AINPUT_CLIENT_MAIN_H + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include +#include + +#define DVC_TAG CHANNELS_TAG("ainput.client") +#ifdef WITH_DEBUG_DVC +#define DEBUG_DVC(...) WLog_DBG(DVC_TAG, __VA_ARGS__) +#else +#define DEBUG_DVC(...) \ + do \ + { \ + } while (0) +#endif + +#endif /* FREERDP_CHANNEL_AINPUT_CLIENT_MAIN_H */ diff --git a/channels/ainput/common/ainput_common.h b/channels/ainput/common/ainput_common.h new file mode 100644 index 0000000..34442f7 --- /dev/null +++ b/channels/ainput/common/ainput_common.h @@ -0,0 +1,59 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Audio Input Redirection Virtual Channel + * + * Copyright 2022 Armin Novak + * Copyright 2022 Thincast Technologies GmbH + * + * Licensed 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 FREERDP_INT_AINPUT_COMMON_H +#define FREERDP_INT_AINPUT_COMMON_H + +#include + +#include + +static INLINE const char* ainput_flags_to_string(UINT64 flags, char* buffer, size_t size) +{ + char number[32] = { 0 }; + + if (flags & AINPUT_FLAGS_HAVE_REL) + winpr_str_append("AINPUT_FLAGS_HAVE_REL", buffer, size, "|"); + if (flags & AINPUT_FLAGS_WHEEL) + winpr_str_append("AINPUT_FLAGS_WHEEL", buffer, size, "|"); + if (flags & AINPUT_FLAGS_MOVE) + winpr_str_append("AINPUT_FLAGS_MOVE", buffer, size, "|"); + if (flags & AINPUT_FLAGS_DOWN) + winpr_str_append("AINPUT_FLAGS_DOWN", buffer, size, "|"); + if (flags & AINPUT_FLAGS_REL) + winpr_str_append("AINPUT_FLAGS_REL", buffer, size, "|"); + if (flags & AINPUT_FLAGS_BUTTON1) + winpr_str_append("AINPUT_FLAGS_BUTTON1", buffer, size, "|"); + if (flags & AINPUT_FLAGS_BUTTON2) + winpr_str_append("AINPUT_FLAGS_BUTTON2", buffer, size, "|"); + if (flags & AINPUT_FLAGS_BUTTON3) + winpr_str_append("AINPUT_FLAGS_BUTTON3", buffer, size, "|"); + if (flags & AINPUT_XFLAGS_BUTTON1) + winpr_str_append("AINPUT_XFLAGS_BUTTON1", buffer, size, "|"); + if (flags & AINPUT_XFLAGS_BUTTON2) + winpr_str_append("AINPUT_XFLAGS_BUTTON2", buffer, size, "|"); + + _snprintf(number, sizeof(number), "[0x%08" PRIx64 "]", flags); + winpr_str_append(number, buffer, size, " "); + + return buffer; +} + +#endif /* FREERDP_INT_AINPUT_COMMON_H */ diff --git a/channels/ainput/server/CMakeLists.txt b/channels/ainput/server/CMakeLists.txt new file mode 100644 index 0000000..59be263 --- /dev/null +++ b/channels/ainput/server/CMakeLists.txt @@ -0,0 +1,27 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2022 Armin Novak +# Copyright 2022 Thincast Technologies GmbH +# +# Licensed 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. + +define_channel_server("ainput") + +set(${MODULE_PREFIX}_SRCS + ainput_main.c) + +add_channel_server_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} FALSE "DVCPluginEntry") + +target_link_libraries(${MODULE_NAME} freerdp) +set_property(TARGET ${MODULE_NAME} PROPERTY FOLDER "Channels/${CHANNEL_NAME}/Server") diff --git a/channels/ainput/server/ainput_main.c b/channels/ainput/server/ainput_main.c new file mode 100644 index 0000000..943d0fa --- /dev/null +++ b/channels/ainput/server/ainput_main.c @@ -0,0 +1,587 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Advanced Input Virtual Channel Extension + * + * Copyright 2022 Armin Novak + * Copyright 2022 Thincast Technologies GmbH + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "../common/ainput_common.h" + +#define TAG CHANNELS_TAG("ainput.server") + +typedef enum +{ + AINPUT_INITIAL, + AINPUT_OPENED, + AINPUT_VERSION_SENT, +} eAInputChannelState; + +typedef struct +{ + ainput_server_context context; + + BOOL opened; + + HANDLE stopEvent; + + HANDLE thread; + void* ainput_channel; + + DWORD SessionId; + + BOOL isOpened; + BOOL externalThread; + + /* Channel state */ + eAInputChannelState state; + + wStream* buffer; +} ainput_server; + +static UINT ainput_server_context_poll(ainput_server_context* context); +static BOOL ainput_server_context_handle(ainput_server_context* context, HANDLE* handle); +static UINT ainput_server_context_poll_int(ainput_server_context* context); + +static BOOL ainput_server_is_open(ainput_server_context* context) +{ + ainput_server* ainput = (ainput_server*)context; + + WINPR_ASSERT(ainput); + return ainput->isOpened; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT ainput_server_open_channel(ainput_server* ainput) +{ + DWORD Error; + HANDLE hEvent; + DWORD StartTick; + DWORD BytesReturned = 0; + PULONG pSessionId = NULL; + + WINPR_ASSERT(ainput); + + if (WTSQuerySessionInformationA(ainput->context.vcm, WTS_CURRENT_SESSION, WTSSessionId, + (LPSTR*)&pSessionId, &BytesReturned) == FALSE) + { + WLog_ERR(TAG, "WTSQuerySessionInformationA failed!"); + return ERROR_INTERNAL_ERROR; + } + + ainput->SessionId = (DWORD)*pSessionId; + WTSFreeMemory(pSessionId); + hEvent = WTSVirtualChannelManagerGetEventHandle(ainput->context.vcm); + StartTick = GetTickCount(); + + while (ainput->ainput_channel == NULL) + { + if (WaitForSingleObject(hEvent, 1000) == WAIT_FAILED) + { + Error = GetLastError(); + WLog_ERR(TAG, "WaitForSingleObject failed with error %" PRIu32 "!", Error); + return Error; + } + + ainput->ainput_channel = WTSVirtualChannelOpenEx(ainput->SessionId, AINPUT_DVC_CHANNEL_NAME, + WTS_CHANNEL_OPTION_DYNAMIC); + + Error = GetLastError(); + + if (Error == ERROR_NOT_FOUND) + { + WLog_DBG(TAG, "Channel %s not found", AINPUT_DVC_CHANNEL_NAME); + break; + } + + if (ainput->ainput_channel) + { + UINT32 channelId; + BOOL status = TRUE; + + channelId = WTSChannelGetIdByHandle(ainput->ainput_channel); + + IFCALLRET(ainput->context.ChannelIdAssigned, status, &ainput->context, channelId); + if (!status) + { + WLog_ERR(TAG, "context->ChannelIdAssigned failed!"); + return ERROR_INTERNAL_ERROR; + } + + break; + } + + if (GetTickCount() - StartTick > 5000) + { + WLog_WARN(TAG, "Timeout opening channel %s", AINPUT_DVC_CHANNEL_NAME); + break; + } + } + + return ainput->ainput_channel ? CHANNEL_RC_OK : ERROR_INTERNAL_ERROR; +} + +static UINT ainput_server_send_version(ainput_server* ainput) +{ + ULONG written; + wStream* s; + + WINPR_ASSERT(ainput); + + s = ainput->buffer; + WINPR_ASSERT(s); + + Stream_SetPosition(s, 0); + if (!Stream_EnsureCapacity(s, 10)) + { + WLog_WARN(TAG, "[%s] out of memory", AINPUT_DVC_CHANNEL_NAME); + return ERROR_OUTOFMEMORY; + } + + Stream_Write_UINT16(s, MSG_AINPUT_VERSION); + Stream_Write_UINT32(s, AINPUT_VERSION_MAJOR); /* Version (4 bytes) */ + Stream_Write_UINT32(s, AINPUT_VERSION_MINOR); /* Version (4 bytes) */ + + WINPR_ASSERT(Stream_GetPosition(s) <= ULONG_MAX); + if (!WTSVirtualChannelWrite(ainput->ainput_channel, (PCHAR)Stream_Buffer(s), + (ULONG)Stream_GetPosition(s), &written)) + { + WLog_ERR(TAG, "WTSVirtualChannelWrite failed!"); + return ERROR_INTERNAL_ERROR; + } + + return CHANNEL_RC_OK; +} + +static UINT ainput_server_recv_mouse_event(ainput_server* ainput, wStream* s) +{ + UINT error = CHANNEL_RC_OK; + UINT64 flags, time; + INT32 x, y; + char buffer[128] = { 0 }; + + WINPR_ASSERT(ainput); + WINPR_ASSERT(s); + + if (!Stream_CheckAndLogRequiredLength(TAG, s, 24)) + return ERROR_NO_DATA; + + Stream_Read_UINT64(s, time); + Stream_Read_UINT64(s, flags); + Stream_Read_INT32(s, x); + Stream_Read_INT32(s, y); + + WLog_VRB(TAG, "[%s] received: time=0x%08" PRIx64 ", flags=%s, %" PRId32 "x%" PRId32, + __FUNCTION__, time, ainput_flags_to_string(flags, buffer, sizeof(buffer)), x, y); + IFCALLRET(ainput->context.MouseEvent, error, &ainput->context, time, flags, x, y); + + return error; +} + +static HANDLE ainput_server_get_channel_handle(ainput_server* ainput) +{ + BYTE* buffer = NULL; + DWORD BytesReturned = 0; + HANDLE ChannelEvent = NULL; + + WINPR_ASSERT(ainput); + + if (WTSVirtualChannelQuery(ainput->ainput_channel, WTSVirtualEventHandle, &buffer, + &BytesReturned) == TRUE) + { + if (BytesReturned == sizeof(HANDLE)) + CopyMemory(&ChannelEvent, buffer, sizeof(HANDLE)); + + WTSFreeMemory(buffer); + } + + return ChannelEvent; +} + +static DWORD WINAPI ainput_server_thread_func(LPVOID arg) +{ + DWORD nCount; + HANDLE events[2] = { 0 }; + ainput_server* ainput = (ainput_server*)arg; + UINT error = CHANNEL_RC_OK; + DWORD status; + + WINPR_ASSERT(ainput); + + nCount = 0; + events[nCount++] = ainput->stopEvent; + + while ((error == CHANNEL_RC_OK) && (WaitForSingleObject(events[0], 0) != WAIT_OBJECT_0)) + { + switch (ainput->state) + { + case AINPUT_OPENED: + events[1] = ainput_server_get_channel_handle(ainput); + nCount = 2; + status = WaitForMultipleObjects(nCount, events, FALSE, 100); + switch (status) + { + case WAIT_TIMEOUT: + case WAIT_OBJECT_0 + 1: + case WAIT_OBJECT_0: + error = ainput_server_context_poll_int(&ainput->context); + break; + + case WAIT_FAILED: + default: + WLog_WARN(TAG, "[%s] Wait for open failed", AINPUT_DVC_CHANNEL_NAME); + error = ERROR_INTERNAL_ERROR; + break; + } + break; + case AINPUT_VERSION_SENT: + status = WaitForMultipleObjects(nCount, events, FALSE, INFINITE); + switch (status) + { + case WAIT_TIMEOUT: + case WAIT_OBJECT_0 + 1: + case WAIT_OBJECT_0: + error = ainput_server_context_poll_int(&ainput->context); + break; + case WAIT_FAILED: + default: + WLog_WARN(TAG, "[%s] Wait for version failed", AINPUT_DVC_CHANNEL_NAME); + error = ERROR_INTERNAL_ERROR; + break; + } + break; + default: + error = ainput_server_context_poll_int(&ainput->context); + break; + } + } + + WTSVirtualChannelClose(ainput->ainput_channel); + ainput->ainput_channel = NULL; + + if (error && ainput->context.rdpcontext) + setChannelError(ainput->context.rdpcontext, error, + "ainput_server_thread_func reported an error"); + + ExitThread(error); + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT ainput_server_open(ainput_server_context* context) +{ + ainput_server* ainput = (ainput_server*)context; + + WINPR_ASSERT(ainput); + + if (!ainput->externalThread && (ainput->thread == NULL)) + { + ainput->stopEvent = CreateEvent(NULL, TRUE, FALSE, NULL); + if (!ainput->stopEvent) + { + WLog_ERR(TAG, "CreateEvent failed!"); + return ERROR_INTERNAL_ERROR; + } + + ainput->thread = CreateThread(NULL, 0, ainput_server_thread_func, ainput, 0, NULL); + if (!ainput->thread) + { + WLog_ERR(TAG, "CreateEvent failed!"); + CloseHandle(ainput->stopEvent); + ainput->stopEvent = NULL; + return ERROR_INTERNAL_ERROR; + } + } + ainput->isOpened = TRUE; + + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT ainput_server_close(ainput_server_context* context) +{ + UINT error = CHANNEL_RC_OK; + ainput_server* ainput = (ainput_server*)context; + + WINPR_ASSERT(ainput); + + if (!ainput->externalThread && ainput->thread) + { + SetEvent(ainput->stopEvent); + + if (WaitForSingleObject(ainput->thread, INFINITE) == WAIT_FAILED) + { + error = GetLastError(); + WLog_ERR(TAG, "WaitForSingleObject failed with error %" PRIu32 "", error); + return error; + } + + CloseHandle(ainput->thread); + CloseHandle(ainput->stopEvent); + ainput->thread = NULL; + ainput->stopEvent = NULL; + } + if (ainput->externalThread) + { + if (ainput->state != AINPUT_INITIAL) + { + WTSVirtualChannelClose(ainput->ainput_channel); + ainput->ainput_channel = NULL; + ainput->state = AINPUT_INITIAL; + } + } + ainput->isOpened = FALSE; + + return error; +} + +static UINT ainput_server_initialize(ainput_server_context* context, BOOL externalThread) +{ + UINT error = CHANNEL_RC_OK; + ainput_server* ainput = (ainput_server*)context; + + WINPR_ASSERT(ainput); + + if (ainput->isOpened) + { + WLog_WARN(TAG, "Application error: AINPUT channel already initialized, calling in this " + "state is not possible!"); + return ERROR_INVALID_STATE; + } + ainput->externalThread = externalThread; + return error; +} + +ainput_server_context* ainput_server_context_new(HANDLE vcm) +{ + ainput_server* ainput = (ainput_server*)calloc(1, sizeof(ainput_server)); + + if (!ainput) + return NULL; + + ainput->context.vcm = vcm; + ainput->context.Open = ainput_server_open; + ainput->context.IsOpen = ainput_server_is_open; + ainput->context.Close = ainput_server_close; + ainput->context.Initialize = ainput_server_initialize; + ainput->context.Poll = ainput_server_context_poll; + ainput->context.ChannelHandle = ainput_server_context_handle; + + ainput->buffer = Stream_New(NULL, 4096); + if (!ainput->buffer) + goto fail; + return &ainput->context; +fail: + ainput_server_context_free(ainput); + return NULL; +} + +void ainput_server_context_free(ainput_server_context* context) +{ + ainput_server* ainput = (ainput_server*)context; + if (ainput) + { + ainput_server_close(context); + Stream_Free(ainput->buffer, TRUE); + } + free(ainput); +} + +static UINT ainput_process_message(ainput_server* ainput) +{ + BOOL rc; + UINT error = ERROR_INTERNAL_ERROR; + ULONG BytesReturned, ActualBytesReturned; + UINT16 MessageId; + wStream* s; + + WINPR_ASSERT(ainput); + WINPR_ASSERT(ainput->ainput_channel); + + s = ainput->buffer; + WINPR_ASSERT(s); + + Stream_SetPosition(s, 0); + rc = WTSVirtualChannelRead(ainput->ainput_channel, 0, NULL, 0, &BytesReturned); + if (!rc) + goto out; + + if (BytesReturned < 2) + { + error = CHANNEL_RC_OK; + goto out; + } + + if (!Stream_EnsureRemainingCapacity(s, BytesReturned)) + { + WLog_ERR(TAG, "Stream_EnsureRemainingCapacity failed!"); + error = CHANNEL_RC_NO_MEMORY; + goto out; + } + + if (WTSVirtualChannelRead(ainput->ainput_channel, 0, (PCHAR)Stream_Buffer(s), + (ULONG)Stream_Capacity(s), &ActualBytesReturned) == FALSE) + { + WLog_ERR(TAG, "WTSVirtualChannelRead failed!"); + goto out; + } + + if (BytesReturned != ActualBytesReturned) + { + WLog_ERR(TAG, "WTSVirtualChannelRead size mismatch %" PRId32 ", expected %" PRId32, + ActualBytesReturned, BytesReturned); + goto out; + } + + Stream_SetLength(s, ActualBytesReturned); + Stream_Read_UINT16(s, MessageId); + + switch (MessageId) + { + case MSG_AINPUT_MOUSE: + error = ainput_server_recv_mouse_event(ainput, s); + break; + + default: + WLog_ERR(TAG, "audin_server_thread_func: unknown MessageId %" PRIu8 "", MessageId); + break; + } + +out: + if (error) + WLog_ERR(TAG, "Response failed with error %" PRIu32 "!", error); + + return error; +} + +BOOL ainput_server_context_handle(ainput_server_context* context, HANDLE* handle) +{ + ainput_server* ainput = (ainput_server*)context; + WINPR_ASSERT(ainput); + WINPR_ASSERT(handle); + + if (!ainput->externalThread) + { + WLog_WARN(TAG, "[%s] externalThread fail!", AINPUT_DVC_CHANNEL_NAME); + return FALSE; + } + if (ainput->state == AINPUT_INITIAL) + { + WLog_WARN(TAG, "[%s] state fail!", AINPUT_DVC_CHANNEL_NAME); + return FALSE; + } + *handle = ainput_server_get_channel_handle(ainput); + return TRUE; +} + +UINT ainput_server_context_poll_int(ainput_server_context* context) +{ + ainput_server* ainput = (ainput_server*)context; + UINT error = ERROR_INTERNAL_ERROR; + + WINPR_ASSERT(ainput); + + switch (ainput->state) + { + case AINPUT_INITIAL: + error = ainput_server_open_channel(ainput); + if (error) + WLog_ERR(TAG, "ainput_server_open_channel failed with error %" PRIu32 "!", error); + else + ainput->state = AINPUT_OPENED; + break; + case AINPUT_OPENED: + { + BYTE* buffer = NULL; + DWORD BytesReturned = 0; + + if (WTSVirtualChannelQuery(ainput->ainput_channel, WTSVirtualChannelReady, &buffer, + &BytesReturned) != TRUE) + { + WLog_ERR(TAG, "WTSVirtualChannelReady failed,"); + } + else + { + if (*buffer != 0) + { + error = ainput_server_send_version(ainput); + if (error) + WLog_ERR(TAG, "audin_server_send_version failed with error %" PRIu32 "!", + error); + else + ainput->state = AINPUT_VERSION_SENT; + } + else + error = CHANNEL_RC_OK; + } + WTSFreeMemory(buffer); + } + break; + case AINPUT_VERSION_SENT: + error = ainput_process_message(ainput); + break; + + default: + WLog_ERR(TAG, "AINPUT chanel is in invalid state %d", ainput->state); + break; + } + + return error; +} + +UINT ainput_server_context_poll(ainput_server_context* context) +{ + ainput_server* ainput = (ainput_server*)context; + + WINPR_ASSERT(ainput); + if (!ainput->externalThread) + { + WLog_WARN(TAG, "[%s] externalThread fail!", AINPUT_DVC_CHANNEL_NAME); + return ERROR_INTERNAL_ERROR; + } + return ainput_server_context_poll_int(context); +} diff --git a/channels/audin/CMakeLists.txt b/channels/audin/CMakeLists.txt new file mode 100644 index 0000000..d72b102 --- /dev/null +++ b/channels/audin/CMakeLists.txt @@ -0,0 +1,26 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2012 Marc-Andre Moreau +# +# Licensed 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. + +define_channel("audin") + +if(WITH_CLIENT_CHANNELS) + add_channel_client(${MODULE_PREFIX} ${CHANNEL_NAME}) +endif() + +if(WITH_SERVER_CHANNELS) + add_channel_server(${MODULE_PREFIX} ${CHANNEL_NAME}) +endif() diff --git a/channels/audin/ChannelOptions.cmake b/channels/audin/ChannelOptions.cmake new file mode 100644 index 0000000..39ca402 --- /dev/null +++ b/channels/audin/ChannelOptions.cmake @@ -0,0 +1,17 @@ + +set(OPTION_DEFAULT OFF) +set(OPTION_CLIENT_DEFAULT ON) +set(OPTION_SERVER_DEFAULT ON) + +if(ANDROID) + set(OPTION_SERVER_DEFAULT OFF) +endif() + +define_channel_options(NAME "audin" TYPE "dynamic" + DESCRIPTION "Audio Input Redirection Virtual Channel Extension" + SPECIFICATIONS "[MS-RDPEAI]" + DEFAULT ${OPTION_DEFAULT}) + +define_channel_client_options(${OPTION_CLIENT_DEFAULT}) +define_channel_server_options(${OPTION_SERVER_DEFAULT}) + diff --git a/channels/audin/client/CMakeLists.txt b/channels/audin/client/CMakeLists.txt new file mode 100644 index 0000000..0c2e393 --- /dev/null +++ b/channels/audin/client/CMakeLists.txt @@ -0,0 +1,58 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2012 Marc-Andre Moreau +# +# Licensed 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. + +define_channel_client("audin") + +set(${MODULE_PREFIX}_SRCS + audin_main.c + audin_main.h) + +include_directories(..) + +add_channel_client_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} TRUE "DVCPluginEntry") + +target_link_libraries(${MODULE_NAME} freerdp winpr) + +if (WITH_DEBUG_SYMBOLS AND MSVC AND NOT BUILTIN_CHANNELS AND BUILD_SHARED_LIBS) + install(FILES ${CMAKE_BINARY_DIR}/${MODULE_NAME}.pdb DESTINATION ${FREERDP_ADDIN_PATH} COMPONENT symbols) +endif() + +set_property(TARGET ${MODULE_NAME} PROPERTY FOLDER "Channels/${CHANNEL_NAME}/Client") + +if(WITH_OSS) + add_channel_client_subsystem(${MODULE_PREFIX} ${CHANNEL_NAME} "oss" "") +endif() + +if(WITH_ALSA) + add_channel_client_subsystem(${MODULE_PREFIX} ${CHANNEL_NAME} "alsa" "") +endif() + +if(WITH_PULSE) + add_channel_client_subsystem(${MODULE_PREFIX} ${CHANNEL_NAME} "pulse" "") +endif() + +if(WITH_OPENSLES) + add_channel_client_subsystem(${MODULE_PREFIX} ${CHANNEL_NAME} "opensles" "") +endif() + +if(WITH_WINMM) + add_channel_client_subsystem(${MODULE_PREFIX} ${CHANNEL_NAME} "winmm" "") +endif() + +if(WITH_MACAUDIO) + add_channel_client_subsystem(${MODULE_PREFIX} ${CHANNEL_NAME} "mac" "") +endif() diff --git a/channels/audin/client/alsa/CMakeLists.txt b/channels/audin/client/alsa/CMakeLists.txt new file mode 100644 index 0000000..c9f4a5f --- /dev/null +++ b/channels/audin/client/alsa/CMakeLists.txt @@ -0,0 +1,32 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2012 Marc-Andre Moreau +# +# Licensed 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. + +define_channel_client_subsystem("audin" "alsa" "") + +set(${MODULE_PREFIX}_SRCS + audin_alsa.c) + +include_directories(..) +include_directories(${ALSA_INCLUDE_DIRS}) + +add_channel_client_subsystem_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} "" TRUE "") + + + +set(${MODULE_PREFIX}_LIBS freerdp winpr ${ALSA_LIBRARIES}) + +target_link_libraries(${MODULE_NAME} ${${MODULE_PREFIX}_LIBS}) diff --git a/channels/audin/client/alsa/audin_alsa.c b/channels/audin/client/alsa/audin_alsa.c new file mode 100644 index 0000000..53d4e9b --- /dev/null +++ b/channels/audin/client/alsa/audin_alsa.c @@ -0,0 +1,459 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Audio Input Redirection Virtual Channel - ALSA implementation + * + * Copyright 2010-2011 Vic Lee + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include + +#include +#include + +#include "audin_main.h" + +typedef struct _AudinALSADevice +{ + IAudinDevice iface; + + char* device_name; + UINT32 frames_per_packet; + AUDIO_FORMAT aformat; + + HANDLE thread; + HANDLE stopEvent; + + AudinReceive receive; + void* user_data; + + rdpContext* rdpcontext; + wLog* log; + int bytes_per_frame; +} AudinALSADevice; + +static snd_pcm_format_t audin_alsa_format(UINT32 wFormatTag, UINT32 bitPerChannel) +{ + switch (wFormatTag) + { + case WAVE_FORMAT_PCM: + switch (bitPerChannel) + { + case 16: + return SND_PCM_FORMAT_S16_LE; + + case 8: + return SND_PCM_FORMAT_S8; + + default: + return SND_PCM_FORMAT_UNKNOWN; + } + + case WAVE_FORMAT_ALAW: + return SND_PCM_FORMAT_A_LAW; + + case WAVE_FORMAT_MULAW: + return SND_PCM_FORMAT_MU_LAW; + + default: + return SND_PCM_FORMAT_UNKNOWN; + } +} + +static BOOL audin_alsa_set_params(AudinALSADevice* alsa, snd_pcm_t* capture_handle) +{ + int error; + SSIZE_T s; + UINT32 channels = alsa->aformat.nChannels; + snd_pcm_hw_params_t* hw_params; + snd_pcm_format_t format = + audin_alsa_format(alsa->aformat.wFormatTag, alsa->aformat.wBitsPerSample); + + if ((error = snd_pcm_hw_params_malloc(&hw_params)) < 0) + { + WLog_Print(alsa->log, WLOG_ERROR, "snd_pcm_hw_params_malloc (%s)", snd_strerror(error)); + return FALSE; + } + + snd_pcm_hw_params_any(capture_handle, hw_params); + snd_pcm_hw_params_set_access(capture_handle, hw_params, SND_PCM_ACCESS_RW_INTERLEAVED); + snd_pcm_hw_params_set_format(capture_handle, hw_params, format); + snd_pcm_hw_params_set_rate_near(capture_handle, hw_params, &alsa->aformat.nSamplesPerSec, NULL); + snd_pcm_hw_params_set_channels_near(capture_handle, hw_params, &channels); + snd_pcm_hw_params(capture_handle, hw_params); + snd_pcm_hw_params_free(hw_params); + snd_pcm_prepare(capture_handle); + if (channels > UINT16_MAX) + return FALSE; + s = snd_pcm_format_size(format, 1); + if ((s < 0) || (s > UINT16_MAX)) + return FALSE; + alsa->aformat.nChannels = (UINT16)channels; + alsa->bytes_per_frame = (size_t)s * channels; + return TRUE; +} + +static DWORD WINAPI audin_alsa_thread_func(LPVOID arg) +{ + long error; + BYTE* buffer; + snd_pcm_t* capture_handle = NULL; + AudinALSADevice* alsa = (AudinALSADevice*)arg; + DWORD status; + WLog_Print(alsa->log, WLOG_DEBUG, "in"); + + if ((error = snd_pcm_open(&capture_handle, alsa->device_name, SND_PCM_STREAM_CAPTURE, 0)) < 0) + { + WLog_Print(alsa->log, WLOG_ERROR, "snd_pcm_open (%s)", snd_strerror(error)); + error = CHANNEL_RC_INITIALIZATION_ERROR; + goto out; + } + + if (!audin_alsa_set_params(alsa, capture_handle)) + { + WLog_Print(alsa->log, WLOG_ERROR, "audin_alsa_set_params failed"); + goto out; + } + + buffer = + (BYTE*)calloc(alsa->frames_per_packet + alsa->aformat.nBlockAlign, alsa->bytes_per_frame); + + if (!buffer) + { + WLog_Print(alsa->log, WLOG_ERROR, "calloc failed!"); + error = CHANNEL_RC_NO_MEMORY; + goto out; + } + + while (1) + { + size_t frames = alsa->frames_per_packet; + status = WaitForSingleObject(alsa->stopEvent, 0); + + if (status == WAIT_FAILED) + { + error = GetLastError(); + WLog_Print(alsa->log, WLOG_ERROR, "WaitForSingleObject failed with error %ld!", error); + break; + } + + if (status == WAIT_OBJECT_0) + break; + + error = snd_pcm_readi(capture_handle, buffer, frames); + + if (error == 0) + continue; + + if (error == -EPIPE) + { + snd_pcm_recover(capture_handle, error, 0); + continue; + } + else if (error < 0) + { + WLog_Print(alsa->log, WLOG_ERROR, "snd_pcm_readi (%s)", snd_strerror(error)); + break; + } + + error = + alsa->receive(&alsa->aformat, buffer, error * alsa->bytes_per_frame, alsa->user_data); + + if (error) + { + WLog_Print(alsa->log, WLOG_ERROR, "audin_alsa_thread_receive failed with error %ld", + error); + break; + } + } + + free(buffer); + + if (capture_handle) + snd_pcm_close(capture_handle); + +out: + WLog_Print(alsa->log, WLOG_DEBUG, "out"); + + if (error && alsa->rdpcontext) + setChannelError(alsa->rdpcontext, error, "audin_alsa_thread_func reported an error"); + + ExitThread(error); + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_alsa_free(IAudinDevice* device) +{ + AudinALSADevice* alsa = (AudinALSADevice*)device; + + if (alsa) + free(alsa->device_name); + + free(alsa); + return CHANNEL_RC_OK; +} + +static BOOL audin_alsa_format_supported(IAudinDevice* device, const AUDIO_FORMAT* format) +{ + if (!device || !format) + return FALSE; + + switch (format->wFormatTag) + { + case WAVE_FORMAT_PCM: + if (format->cbSize == 0 && (format->nSamplesPerSec <= 48000) && + (format->wBitsPerSample == 8 || format->wBitsPerSample == 16) && + (format->nChannels == 1 || format->nChannels == 2)) + { + return TRUE; + } + + break; + + default: + return FALSE; + } + + return FALSE; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_alsa_set_format(IAudinDevice* device, const AUDIO_FORMAT* format, + UINT32 FramesPerPacket) +{ + AudinALSADevice* alsa = (AudinALSADevice*)device; + + if (!alsa || !format) + return ERROR_INVALID_PARAMETER; + + alsa->aformat = *format; + alsa->frames_per_packet = FramesPerPacket; + + if (audin_alsa_format(format->wFormatTag, format->wBitsPerSample) == SND_PCM_FORMAT_UNKNOWN) + return ERROR_INTERNAL_ERROR; + + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_alsa_open(IAudinDevice* device, AudinReceive receive, void* user_data) +{ + AudinALSADevice* alsa = (AudinALSADevice*)device; + + if (!device || !receive || !user_data) + return ERROR_INVALID_PARAMETER; + + alsa->receive = receive; + alsa->user_data = user_data; + + if (!(alsa->stopEvent = CreateEvent(NULL, TRUE, FALSE, NULL))) + { + WLog_Print(alsa->log, WLOG_ERROR, "CreateEvent failed!"); + goto error_out; + } + + if (!(alsa->thread = CreateThread(NULL, 0, audin_alsa_thread_func, alsa, 0, NULL))) + { + WLog_Print(alsa->log, WLOG_ERROR, "CreateThread failed!"); + goto error_out; + } + + return CHANNEL_RC_OK; +error_out: + CloseHandle(alsa->stopEvent); + alsa->stopEvent = NULL; + return ERROR_INTERNAL_ERROR; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_alsa_close(IAudinDevice* device) +{ + UINT error = CHANNEL_RC_OK; + AudinALSADevice* alsa = (AudinALSADevice*)device; + + if (!alsa) + return ERROR_INVALID_PARAMETER; + + if (alsa->stopEvent) + { + SetEvent(alsa->stopEvent); + + if (WaitForSingleObject(alsa->thread, INFINITE) == WAIT_FAILED) + { + error = GetLastError(); + WLog_Print(alsa->log, WLOG_ERROR, "WaitForSingleObject failed with error %" PRIu32 "", + error); + return error; + } + + CloseHandle(alsa->stopEvent); + alsa->stopEvent = NULL; + CloseHandle(alsa->thread); + alsa->thread = NULL; + } + + alsa->receive = NULL; + alsa->user_data = NULL; + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_alsa_parse_addin_args(AudinALSADevice* device, ADDIN_ARGV* args) +{ + int status; + DWORD flags; + COMMAND_LINE_ARGUMENT_A* arg; + AudinALSADevice* alsa = (AudinALSADevice*)device; + COMMAND_LINE_ARGUMENT_A audin_alsa_args[] = { { "dev", COMMAND_LINE_VALUE_REQUIRED, "", + NULL, NULL, -1, NULL, "audio device name" }, + { NULL, 0, NULL, NULL, NULL, -1, NULL, NULL } }; + flags = + COMMAND_LINE_SIGIL_NONE | COMMAND_LINE_SEPARATOR_COLON | COMMAND_LINE_IGN_UNKNOWN_KEYWORD; + status = CommandLineParseArgumentsA(args->argc, args->argv, audin_alsa_args, flags, alsa, NULL, + NULL); + + if (status < 0) + return ERROR_INVALID_PARAMETER; + + arg = audin_alsa_args; + + do + { + if (!(arg->Flags & COMMAND_LINE_VALUE_PRESENT)) + continue; + + CommandLineSwitchStart(arg) CommandLineSwitchCase(arg, "dev") + { + alsa->device_name = _strdup(arg->Value); + + if (!alsa->device_name) + { + WLog_Print(alsa->log, WLOG_ERROR, "_strdup failed!"); + return CHANNEL_RC_NO_MEMORY; + } + } + CommandLineSwitchEnd(arg) + } while ((arg = CommandLineFindNextArgumentA(arg)) != NULL); + + return CHANNEL_RC_OK; +} + +#ifdef BUILTIN_CHANNELS +#define freerdp_audin_client_subsystem_entry alsa_freerdp_audin_client_subsystem_entry +#else +#define freerdp_audin_client_subsystem_entry FREERDP_API freerdp_audin_client_subsystem_entry +#endif + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT freerdp_audin_client_subsystem_entry(PFREERDP_AUDIN_DEVICE_ENTRY_POINTS pEntryPoints) +{ + ADDIN_ARGV* args; + AudinALSADevice* alsa; + UINT error; + alsa = (AudinALSADevice*)calloc(1, sizeof(AudinALSADevice)); + + if (!alsa) + { + WLog_ERR(TAG, "calloc failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + alsa->log = WLog_Get(TAG); + alsa->iface.Open = audin_alsa_open; + alsa->iface.FormatSupported = audin_alsa_format_supported; + alsa->iface.SetFormat = audin_alsa_set_format; + alsa->iface.Close = audin_alsa_close; + alsa->iface.Free = audin_alsa_free; + alsa->rdpcontext = pEntryPoints->rdpcontext; + args = pEntryPoints->args; + + if ((error = audin_alsa_parse_addin_args(alsa, args))) + { + WLog_Print(alsa->log, WLOG_ERROR, + "audin_alsa_parse_addin_args failed with errorcode %" PRIu32 "!", error); + goto error_out; + } + + if (!alsa->device_name) + { + alsa->device_name = _strdup("default"); + + if (!alsa->device_name) + { + WLog_Print(alsa->log, WLOG_ERROR, "_strdup failed!"); + error = CHANNEL_RC_NO_MEMORY; + goto error_out; + } + } + + alsa->frames_per_packet = 128; + alsa->aformat.nChannels = 2; + alsa->aformat.wBitsPerSample = 16; + alsa->aformat.wFormatTag = WAVE_FORMAT_PCM; + alsa->aformat.nSamplesPerSec = 44100; + + if ((error = pEntryPoints->pRegisterAudinDevice(pEntryPoints->plugin, (IAudinDevice*)alsa))) + { + WLog_Print(alsa->log, WLOG_ERROR, "RegisterAudinDevice failed with error %" PRIu32 "!", + error); + goto error_out; + } + + return CHANNEL_RC_OK; +error_out: + free(alsa->device_name); + free(alsa); + return error; +} diff --git a/channels/audin/client/audin_main.c b/channels/audin/client/audin_main.c new file mode 100644 index 0000000..81a6e8f --- /dev/null +++ b/channels/audin/client/audin_main.c @@ -0,0 +1,1099 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Audio Input Redirection Virtual Channel + * + * Copyright 2010-2011 Vic Lee + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger + * Copyright 2015 Armin Novak + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include + +#include +#include +#include +#include + +#include "audin_main.h" + +#define MSG_SNDIN_VERSION 0x01 +#define MSG_SNDIN_FORMATS 0x02 +#define MSG_SNDIN_OPEN 0x03 +#define MSG_SNDIN_OPEN_REPLY 0x04 +#define MSG_SNDIN_DATA_INCOMING 0x05 +#define MSG_SNDIN_DATA 0x06 +#define MSG_SNDIN_FORMATCHANGE 0x07 + +typedef struct _AUDIN_LISTENER_CALLBACK AUDIN_LISTENER_CALLBACK; +struct _AUDIN_LISTENER_CALLBACK +{ + IWTSListenerCallback iface; + + IWTSPlugin* plugin; + IWTSVirtualChannelManager* channel_mgr; +}; + +typedef struct _AUDIN_CHANNEL_CALLBACK AUDIN_CHANNEL_CALLBACK; +struct _AUDIN_CHANNEL_CALLBACK +{ + IWTSVirtualChannelCallback iface; + + IWTSPlugin* plugin; + IWTSVirtualChannelManager* channel_mgr; + IWTSVirtualChannel* channel; + + /** + * The supported format list sent back to the server, which needs to + * be stored as reference when the server sends the format index in + * Open PDU and Format Change PDU + */ + AUDIO_FORMAT* formats; + UINT32 formats_count; +}; + +typedef struct _AUDIN_PLUGIN AUDIN_PLUGIN; +struct _AUDIN_PLUGIN +{ + IWTSPlugin iface; + + AUDIN_LISTENER_CALLBACK* listener_callback; + + /* Parsed plugin data */ + AUDIO_FORMAT* fixed_format; + char* subsystem; + char* device_name; + + /* Device interface */ + IAudinDevice* device; + + rdpContext* rdpcontext; + BOOL attached; + wStream* data; + AUDIO_FORMAT* format; + UINT32 FramesPerPacket; + + FREERDP_DSP_CONTEXT* dsp_context; + wLog* log; + + IWTSListener* listener; + + BOOL initialized; +}; + +static BOOL audin_process_addin_args(AUDIN_PLUGIN* audin, ADDIN_ARGV* args); + +static UINT audin_channel_write_and_free(AUDIN_CHANNEL_CALLBACK* callback, wStream* out, + BOOL freeStream) +{ + UINT error; + + if (!callback || !out) + return ERROR_INVALID_PARAMETER; + + if (!callback->channel || !callback->channel->Write) + return ERROR_INTERNAL_ERROR; + + Stream_SealLength(out); + error = + callback->channel->Write(callback->channel, Stream_Length(out), Stream_Buffer(out), NULL); + + if (freeStream) + Stream_Free(out, TRUE); + + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_process_version(AUDIN_PLUGIN* audin, AUDIN_CHANNEL_CALLBACK* callback, wStream* s) +{ + wStream* out; + const UINT32 ClientVersion = 0x01; + UINT32 ServerVersion; + + if (Stream_GetRemainingLength(s) < 4) + return ERROR_INVALID_DATA; + + Stream_Read_UINT32(s, ServerVersion); + WLog_Print(audin->log, WLOG_DEBUG, "ServerVersion=%" PRIu32 ", ClientVersion=%" PRIu32, + ServerVersion, ClientVersion); + + /* Do not answer server packet, we do not support the channel version. */ + if (ServerVersion != ClientVersion) + { + WLog_Print(audin->log, WLOG_WARN, + "Incompatible channel version server=%" PRIu32 + ", client supports version=%" PRIu32, + ServerVersion, ClientVersion); + return CHANNEL_RC_OK; + } + + out = Stream_New(NULL, 5); + + if (!out) + { + WLog_Print(audin->log, WLOG_ERROR, "Stream_New failed!"); + return ERROR_OUTOFMEMORY; + } + + Stream_Write_UINT8(out, MSG_SNDIN_VERSION); + Stream_Write_UINT32(out, ClientVersion); + return audin_channel_write_and_free(callback, out, TRUE); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_send_incoming_data_pdu(AUDIN_CHANNEL_CALLBACK* callback) +{ + BYTE out_data[1] = { MSG_SNDIN_DATA_INCOMING }; + + if (!callback || !callback->channel || !callback->channel->Write) + return ERROR_INTERNAL_ERROR; + + return callback->channel->Write(callback->channel, 1, out_data, NULL); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_process_formats(AUDIN_PLUGIN* audin, AUDIN_CHANNEL_CALLBACK* callback, wStream* s) +{ + UINT32 i; + UINT error; + wStream* out; + UINT32 NumFormats; + UINT32 cbSizeFormatsPacket; + + if (Stream_GetRemainingLength(s) < 8) + return ERROR_INVALID_DATA; + + Stream_Read_UINT32(s, NumFormats); + WLog_Print(audin->log, WLOG_DEBUG, "NumFormats %" PRIu32 "", NumFormats); + + if ((NumFormats < 1) || (NumFormats > 1000)) + { + WLog_Print(audin->log, WLOG_ERROR, "bad NumFormats %" PRIu32 "", NumFormats); + return ERROR_INVALID_DATA; + } + + Stream_Seek_UINT32(s); /* cbSizeFormatsPacket */ + callback->formats = audio_formats_new(NumFormats); + + if (!callback->formats) + { + WLog_Print(audin->log, WLOG_ERROR, "calloc failed!"); + return ERROR_INVALID_DATA; + } + + out = Stream_New(NULL, 9); + + if (!out) + { + error = CHANNEL_RC_NO_MEMORY; + WLog_Print(audin->log, WLOG_ERROR, "Stream_New failed!"); + goto out; + } + + Stream_Seek(out, 9); + + /* SoundFormats (variable) */ + for (i = 0; i < NumFormats; i++) + { + AUDIO_FORMAT format = { 0 }; + + if (!audio_format_read(s, &format)) + { + error = ERROR_INVALID_DATA; + goto out; + } + + audio_format_print(audin->log, WLOG_DEBUG, &format); + + if (!audio_format_compatible(audin->fixed_format, &format)) + { + audio_format_free(&format); + continue; + } + + if (freerdp_dsp_supports_format(&format, TRUE) || + audin->device->FormatSupported(audin->device, &format)) + { + /* Store the agreed format in the corresponding index */ + callback->formats[callback->formats_count++] = format; + + if (!audio_format_write(out, &format)) + { + error = CHANNEL_RC_NO_MEMORY; + WLog_Print(audin->log, WLOG_ERROR, "Stream_EnsureRemainingCapacity failed!"); + goto out; + } + } + else + { + audio_format_free(&format); + } + } + + if ((error = audin_send_incoming_data_pdu(callback))) + { + WLog_Print(audin->log, WLOG_ERROR, "audin_send_incoming_data_pdu failed!"); + goto out; + } + + cbSizeFormatsPacket = (UINT32)Stream_GetPosition(out); + Stream_SetPosition(out, 0); + Stream_Write_UINT8(out, MSG_SNDIN_FORMATS); /* Header (1 byte) */ + Stream_Write_UINT32(out, callback->formats_count); /* NumFormats (4 bytes) */ + Stream_Write_UINT32(out, cbSizeFormatsPacket); /* cbSizeFormatsPacket (4 bytes) */ + Stream_SetPosition(out, cbSizeFormatsPacket); + error = audin_channel_write_and_free(callback, out, FALSE); +out: + + if (error != CHANNEL_RC_OK) + { + audio_formats_free(callback->formats, NumFormats); + callback->formats = NULL; + } + + Stream_Free(out, TRUE); + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_send_format_change_pdu(AUDIN_PLUGIN* audin, AUDIN_CHANNEL_CALLBACK* callback, + UINT32 NewFormat) +{ + wStream* out = Stream_New(NULL, 5); + + if (!out) + { + WLog_Print(audin->log, WLOG_ERROR, "Stream_New failed!"); + return CHANNEL_RC_OK; + } + + Stream_Write_UINT8(out, MSG_SNDIN_FORMATCHANGE); + Stream_Write_UINT32(out, NewFormat); + return audin_channel_write_and_free(callback, out, TRUE); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_send_open_reply_pdu(AUDIN_PLUGIN* audin, AUDIN_CHANNEL_CALLBACK* callback, + UINT32 Result) +{ + wStream* out = Stream_New(NULL, 5); + + if (!out) + { + WLog_Print(audin->log, WLOG_ERROR, "Stream_New failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + Stream_Write_UINT8(out, MSG_SNDIN_OPEN_REPLY); + Stream_Write_UINT32(out, Result); + return audin_channel_write_and_free(callback, out, TRUE); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_receive_wave_data(const AUDIO_FORMAT* format, const BYTE* data, size_t size, + void* user_data) +{ + UINT error; + BOOL compatible; + AUDIN_PLUGIN* audin; + AUDIN_CHANNEL_CALLBACK* callback = (AUDIN_CHANNEL_CALLBACK*)user_data; + + if (!callback) + return CHANNEL_RC_BAD_CHANNEL_HANDLE; + + audin = (AUDIN_PLUGIN*)callback->plugin; + + if (!audin) + return CHANNEL_RC_BAD_CHANNEL_HANDLE; + + if (!audin->attached) + return CHANNEL_RC_OK; + + Stream_SetPosition(audin->data, 0); + + if (!Stream_EnsureRemainingCapacity(audin->data, 1)) + return CHANNEL_RC_NO_MEMORY; + + Stream_Write_UINT8(audin->data, MSG_SNDIN_DATA); + + compatible = audio_format_compatible(format, audin->format); + if (compatible && audin->device->FormatSupported(audin->device, audin->format)) + { + if (!Stream_EnsureRemainingCapacity(audin->data, size)) + return CHANNEL_RC_NO_MEMORY; + + Stream_Write(audin->data, data, size); + } + else + { + if (!freerdp_dsp_encode(audin->dsp_context, format, data, size, audin->data)) + return ERROR_INTERNAL_ERROR; + } + + /* Did not encode anything, skip this, the codec is not ready for output. */ + if (Stream_GetPosition(audin->data) <= 1) + return CHANNEL_RC_OK; + + audio_format_print(audin->log, WLOG_TRACE, audin->format); + WLog_Print(audin->log, WLOG_TRACE, "[%" PRIdz "/%" PRIdz "]", size, + Stream_GetPosition(audin->data) - 1); + + if ((error = audin_send_incoming_data_pdu(callback))) + { + WLog_Print(audin->log, WLOG_ERROR, "audin_send_incoming_data_pdu failed!"); + return error; + } + + return audin_channel_write_and_free(callback, audin->data, FALSE); +} + +static BOOL audin_open_device(AUDIN_PLUGIN* audin, AUDIN_CHANNEL_CALLBACK* callback) +{ + UINT error = ERROR_INTERNAL_ERROR; + BOOL supported; + AUDIO_FORMAT format; + + if (!audin || !audin->device) + return FALSE; + + format = *audin->format; + supported = IFCALLRESULT(FALSE, audin->device->FormatSupported, audin->device, &format); + WLog_Print(audin->log, WLOG_DEBUG, "microphone uses %s codec", + audio_format_get_tag_string(format.wFormatTag)); + + if (!supported) + { + /* Default sample rates supported by most backends. */ + const UINT32 samplerates[] = { 96000, 48000, 44100, 22050 }; + BOOL test = FALSE; + + format.wFormatTag = WAVE_FORMAT_PCM; + format.wBitsPerSample = 16; + test = IFCALLRESULT(FALSE, audin->device->FormatSupported, audin->device, &format); + if (!test) + { + size_t x; + for (x = 0; x < ARRAYSIZE(samplerates); x++) + { + format.nSamplesPerSec = samplerates[x]; + test = IFCALLRESULT(FALSE, audin->device->FormatSupported, audin->device, &format); + if (test) + break; + } + } + if (!test) + return FALSE; + } + + IFCALLRET(audin->device->SetFormat, error, audin->device, &format, audin->FramesPerPacket); + + if (error != CHANNEL_RC_OK) + { + WLog_ERR(TAG, "SetFormat failed with errorcode %" PRIu32 "", error); + return FALSE; + } + + if (!freerdp_dsp_context_reset(audin->dsp_context, audin->format)) + return FALSE; + + IFCALLRET(audin->device->Open, error, audin->device, audin_receive_wave_data, callback); + + if (error != CHANNEL_RC_OK) + { + WLog_ERR(TAG, "Open failed with errorcode %" PRIu32 "", error); + return FALSE; + } + + return TRUE; +} +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_process_open(AUDIN_PLUGIN* audin, AUDIN_CHANNEL_CALLBACK* callback, wStream* s) +{ + UINT32 initialFormat; + UINT32 FramesPerPacket; + UINT error = CHANNEL_RC_OK; + + if (Stream_GetRemainingLength(s) < 8) + return ERROR_INVALID_DATA; + + Stream_Read_UINT32(s, FramesPerPacket); + Stream_Read_UINT32(s, initialFormat); + WLog_Print(audin->log, WLOG_DEBUG, "FramesPerPacket=%" PRIu32 " initialFormat=%" PRIu32 "", + FramesPerPacket, initialFormat); + audin->FramesPerPacket = FramesPerPacket; + + if (initialFormat >= callback->formats_count) + { + WLog_Print(audin->log, WLOG_ERROR, "invalid format index %" PRIu32 " (total %d)", + initialFormat, callback->formats_count); + return ERROR_INVALID_DATA; + } + + audin->format = &callback->formats[initialFormat]; + + if (!audin_open_device(audin, callback)) + return ERROR_INTERNAL_ERROR; + + if ((error = audin_send_format_change_pdu(audin, callback, initialFormat))) + { + WLog_Print(audin->log, WLOG_ERROR, "audin_send_format_change_pdu failed!"); + return error; + } + + if ((error = audin_send_open_reply_pdu(audin, callback, 0))) + WLog_Print(audin->log, WLOG_ERROR, "audin_send_open_reply_pdu failed!"); + + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_process_format_change(AUDIN_PLUGIN* audin, AUDIN_CHANNEL_CALLBACK* callback, + wStream* s) +{ + UINT32 NewFormat; + UINT error = CHANNEL_RC_OK; + + if (Stream_GetRemainingLength(s) < 4) + return ERROR_INVALID_DATA; + + Stream_Read_UINT32(s, NewFormat); + WLog_Print(audin->log, WLOG_DEBUG, "NewFormat=%" PRIu32 "", NewFormat); + + if (NewFormat >= callback->formats_count) + { + WLog_Print(audin->log, WLOG_ERROR, "invalid format index %" PRIu32 " (total %d)", NewFormat, + callback->formats_count); + return ERROR_INVALID_DATA; + } + + audin->format = &callback->formats[NewFormat]; + + if (audin->device) + { + IFCALLRET(audin->device->Close, error, audin->device); + + if (error != CHANNEL_RC_OK) + { + WLog_ERR(TAG, "Close failed with errorcode %" PRIu32 "", error); + return error; + } + } + + if (!audin_open_device(audin, callback)) + return ERROR_INTERNAL_ERROR; + + if ((error = audin_send_format_change_pdu(audin, callback, NewFormat))) + WLog_ERR(TAG, "audin_send_format_change_pdu failed!"); + + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_on_data_received(IWTSVirtualChannelCallback* pChannelCallback, wStream* data) +{ + UINT error; + BYTE MessageId; + AUDIN_PLUGIN* audin; + AUDIN_CHANNEL_CALLBACK* callback = (AUDIN_CHANNEL_CALLBACK*)pChannelCallback; + + if (!callback || !data) + return ERROR_INVALID_PARAMETER; + + audin = (AUDIN_PLUGIN*)callback->plugin; + + if (!audin) + return ERROR_INTERNAL_ERROR; + + if (Stream_GetRemainingCapacity(data) < 1) + return ERROR_NO_DATA; + + Stream_Read_UINT8(data, MessageId); + WLog_Print(audin->log, WLOG_DEBUG, "MessageId=0x%02" PRIx8 "", MessageId); + + switch (MessageId) + { + case MSG_SNDIN_VERSION: + error = audin_process_version(audin, callback, data); + break; + + case MSG_SNDIN_FORMATS: + error = audin_process_formats(audin, callback, data); + break; + + case MSG_SNDIN_OPEN: + error = audin_process_open(audin, callback, data); + break; + + case MSG_SNDIN_FORMATCHANGE: + error = audin_process_format_change(audin, callback, data); + break; + + default: + WLog_Print(audin->log, WLOG_ERROR, "unknown MessageId=0x%02" PRIx8 "", MessageId); + error = ERROR_INVALID_DATA; + break; + } + + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_on_close(IWTSVirtualChannelCallback* pChannelCallback) +{ + AUDIN_CHANNEL_CALLBACK* callback = (AUDIN_CHANNEL_CALLBACK*)pChannelCallback; + AUDIN_PLUGIN* audin = (AUDIN_PLUGIN*)callback->plugin; + UINT error = CHANNEL_RC_OK; + WLog_Print(audin->log, WLOG_TRACE, "..."); + + if (audin->device) + { + IFCALLRET(audin->device->Close, error, audin->device); + + if (error != CHANNEL_RC_OK) + WLog_Print(audin->log, WLOG_ERROR, "Close failed with errorcode %" PRIu32 "", error); + } + + audin->format = NULL; + audio_formats_free(callback->formats, callback->formats_count); + free(callback); + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_on_new_channel_connection(IWTSListenerCallback* pListenerCallback, + IWTSVirtualChannel* pChannel, BYTE* Data, + BOOL* pbAccept, IWTSVirtualChannelCallback** ppCallback) +{ + AUDIN_CHANNEL_CALLBACK* callback; + AUDIN_PLUGIN* audin; + AUDIN_LISTENER_CALLBACK* listener_callback = (AUDIN_LISTENER_CALLBACK*)pListenerCallback; + + if (!listener_callback || !listener_callback->plugin) + return ERROR_INTERNAL_ERROR; + + audin = (AUDIN_PLUGIN*)listener_callback->plugin; + WLog_Print(audin->log, WLOG_TRACE, "..."); + callback = (AUDIN_CHANNEL_CALLBACK*)calloc(1, sizeof(AUDIN_CHANNEL_CALLBACK)); + + if (!callback) + { + WLog_Print(audin->log, WLOG_ERROR, "calloc failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + callback->iface.OnDataReceived = audin_on_data_received; + callback->iface.OnClose = audin_on_close; + callback->plugin = listener_callback->plugin; + callback->channel_mgr = listener_callback->channel_mgr; + callback->channel = pChannel; + *ppCallback = (IWTSVirtualChannelCallback*)callback; + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_plugin_initialize(IWTSPlugin* pPlugin, IWTSVirtualChannelManager* pChannelMgr) +{ + UINT rc; + AUDIN_PLUGIN* audin = (AUDIN_PLUGIN*)pPlugin; + + if (!audin) + return CHANNEL_RC_BAD_CHANNEL_HANDLE; + + if (!pChannelMgr) + return ERROR_INVALID_PARAMETER; + + if (audin->initialized) + { + WLog_ERR(TAG, "[%s] channel initialized twice, aborting", AUDIN_DVC_CHANNEL_NAME); + return ERROR_INVALID_DATA; + } + + WLog_Print(audin->log, WLOG_TRACE, "..."); + audin->listener_callback = (AUDIN_LISTENER_CALLBACK*)calloc(1, sizeof(AUDIN_LISTENER_CALLBACK)); + + if (!audin->listener_callback) + { + WLog_Print(audin->log, WLOG_ERROR, "calloc failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + audin->listener_callback->iface.OnNewChannelConnection = audin_on_new_channel_connection; + audin->listener_callback->plugin = pPlugin; + audin->listener_callback->channel_mgr = pChannelMgr; + rc = pChannelMgr->CreateListener(pChannelMgr, AUDIN_DVC_CHANNEL_NAME, 0, + &audin->listener_callback->iface, &audin->listener); + + audin->initialized = rc == CHANNEL_RC_OK; + return rc; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_plugin_terminated(IWTSPlugin* pPlugin) +{ + AUDIN_PLUGIN* audin = (AUDIN_PLUGIN*)pPlugin; + UINT error = CHANNEL_RC_OK; + + if (!audin) + return CHANNEL_RC_BAD_CHANNEL_HANDLE; + + WLog_Print(audin->log, WLOG_TRACE, "..."); + + if (audin->listener_callback) + { + IWTSVirtualChannelManager* mgr = audin->listener_callback->channel_mgr; + if (mgr) + IFCALL(mgr->DestroyListener, mgr, audin->listener); + } + audio_formats_free(audin->fixed_format, 1); + + if (audin->device) + { + IFCALLRET(audin->device->Free, error, audin->device); + + if (error != CHANNEL_RC_OK) + { + WLog_Print(audin->log, WLOG_ERROR, "Free failed with errorcode %" PRIu32 "", error); + // dont stop on error + } + + audin->device = NULL; + } + + freerdp_dsp_context_free(audin->dsp_context); + Stream_Free(audin->data, TRUE); + free(audin->subsystem); + free(audin->device_name); + free(audin->listener_callback); + free(audin); + return CHANNEL_RC_OK; +} + +static UINT audin_plugin_attached(IWTSPlugin* pPlugin) +{ + AUDIN_PLUGIN* audin = (AUDIN_PLUGIN*)pPlugin; + UINT error = CHANNEL_RC_OK; + + if (!audin) + return CHANNEL_RC_BAD_CHANNEL_HANDLE; + + audin->attached = TRUE; + return error; +} + +static UINT audin_plugin_detached(IWTSPlugin* pPlugin) +{ + AUDIN_PLUGIN* audin = (AUDIN_PLUGIN*)pPlugin; + UINT error = CHANNEL_RC_OK; + + if (!audin) + return CHANNEL_RC_BAD_CHANNEL_HANDLE; + + audin->attached = FALSE; + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_register_device_plugin(IWTSPlugin* pPlugin, IAudinDevice* device) +{ + AUDIN_PLUGIN* audin = (AUDIN_PLUGIN*)pPlugin; + + if (audin->device) + { + WLog_Print(audin->log, WLOG_ERROR, "existing device, abort."); + return ERROR_ALREADY_EXISTS; + } + + WLog_Print(audin->log, WLOG_DEBUG, "device registered."); + audin->device = device; + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_load_device_plugin(AUDIN_PLUGIN* audin, char* name, ADDIN_ARGV* args) +{ + PFREERDP_AUDIN_DEVICE_ENTRY entry; + FREERDP_AUDIN_DEVICE_ENTRY_POINTS entryPoints; + UINT error; + entry = (PFREERDP_AUDIN_DEVICE_ENTRY)freerdp_load_channel_addin_entry("audin", (LPSTR)name, + NULL, 0); + + if (entry == NULL) + { + WLog_Print(audin->log, WLOG_ERROR, + "freerdp_load_channel_addin_entry did not return any function pointers for %s ", + name); + return ERROR_INVALID_FUNCTION; + } + + entryPoints.plugin = (IWTSPlugin*)audin; + entryPoints.pRegisterAudinDevice = audin_register_device_plugin; + entryPoints.args = args; + entryPoints.rdpcontext = audin->rdpcontext; + + if ((error = entry(&entryPoints))) + { + WLog_Print(audin->log, WLOG_ERROR, "%s entry returned error %" PRIu32 ".", name, error); + return error; + } + + WLog_Print(audin->log, WLOG_INFO, "Loaded %s backend for audin", name); + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_set_subsystem(AUDIN_PLUGIN* audin, const char* subsystem) +{ + free(audin->subsystem); + audin->subsystem = _strdup(subsystem); + + if (!audin->subsystem) + { + WLog_Print(audin->log, WLOG_ERROR, "_strdup failed!"); + return ERROR_NOT_ENOUGH_MEMORY; + } + + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_set_device_name(AUDIN_PLUGIN* audin, const char* device_name) +{ + free(audin->device_name); + audin->device_name = _strdup(device_name); + + if (!audin->device_name) + { + WLog_Print(audin->log, WLOG_ERROR, "_strdup failed!"); + return ERROR_NOT_ENOUGH_MEMORY; + } + + return CHANNEL_RC_OK; +} + +BOOL audin_process_addin_args(AUDIN_PLUGIN* audin, ADDIN_ARGV* args) +{ + int status; + DWORD flags; + COMMAND_LINE_ARGUMENT_A* arg; + UINT error; + COMMAND_LINE_ARGUMENT_A audin_args[] = { + { "sys", COMMAND_LINE_VALUE_REQUIRED, "", NULL, NULL, -1, NULL, "subsystem" }, + { "dev", COMMAND_LINE_VALUE_REQUIRED, "", NULL, NULL, -1, NULL, "device" }, + { "format", COMMAND_LINE_VALUE_REQUIRED, "", NULL, NULL, -1, NULL, "format" }, + { "rate", COMMAND_LINE_VALUE_REQUIRED, "", NULL, NULL, -1, NULL, "rate" }, + { "channel", COMMAND_LINE_VALUE_REQUIRED, "", NULL, NULL, -1, NULL, "channel" }, + { NULL, 0, NULL, NULL, NULL, -1, NULL, NULL } + }; + + if (!args || args->argc == 1) + return TRUE; + + flags = + COMMAND_LINE_SIGIL_NONE | COMMAND_LINE_SEPARATOR_COLON | COMMAND_LINE_IGN_UNKNOWN_KEYWORD; + status = + CommandLineParseArgumentsA(args->argc, args->argv, audin_args, flags, audin, NULL, NULL); + + if (status != 0) + return FALSE; + + arg = audin_args; + errno = 0; + + do + { + if (!(arg->Flags & COMMAND_LINE_VALUE_PRESENT)) + continue; + + CommandLineSwitchStart(arg) CommandLineSwitchCase(arg, "sys") + { + if ((error = audin_set_subsystem(audin, arg->Value))) + { + WLog_Print(audin->log, WLOG_ERROR, + "audin_set_subsystem failed with error %" PRIu32 "!", error); + return FALSE; + } + } + CommandLineSwitchCase(arg, "dev") + { + if ((error = audin_set_device_name(audin, arg->Value))) + { + WLog_Print(audin->log, WLOG_ERROR, + "audin_set_device_name failed with error %" PRIu32 "!", error); + return FALSE; + } + } + CommandLineSwitchCase(arg, "format") + { + unsigned long val = strtoul(arg->Value, NULL, 0); + + if ((errno != 0) || (val > UINT16_MAX)) + return FALSE; + + audin->fixed_format->wFormatTag = val; + } + CommandLineSwitchCase(arg, "rate") + { + long val = strtol(arg->Value, NULL, 0); + + if ((errno != 0) || (val < INT32_MIN) || (val > INT32_MAX)) + return FALSE; + + audin->fixed_format->nSamplesPerSec = val; + } + CommandLineSwitchCase(arg, "channel") + { + unsigned long val = strtoul(arg->Value, NULL, 0); + + if ((errno != 0) || (val > UINT16_MAX)) + audin->fixed_format->nChannels = val; + } + CommandLineSwitchDefault(arg) + { + } + CommandLineSwitchEnd(arg) + } while ((arg = CommandLineFindNextArgumentA(arg)) != NULL); + + return TRUE; +} + +#ifdef BUILTIN_CHANNELS +#define DVCPluginEntry audin_DVCPluginEntry +#else +#define DVCPluginEntry FREERDP_API DVCPluginEntry +#endif + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT DVCPluginEntry(IDRDYNVC_ENTRY_POINTS* pEntryPoints) +{ + struct SubsystemEntry + { + char* subsystem; + char* device; + }; + UINT error = CHANNEL_RC_INITIALIZATION_ERROR; + ADDIN_ARGV* args; + AUDIN_PLUGIN* audin; + struct SubsystemEntry entries[] = + { +#if defined(WITH_PULSE) + { "pulse", "" }, +#endif +#if defined(WITH_OSS) + { "oss", "default" }, +#endif +#if defined(WITH_ALSA) + { "alsa", "default" }, +#endif +#if defined(WITH_OPENSLES) + { "opensles", "default" }, +#endif +#if defined(WITH_WINMM) + { "winmm", "default" }, +#endif +#if defined(WITH_MACAUDIO) + { "mac", "default" }, +#endif + { NULL, NULL } + }; + struct SubsystemEntry* entry = &entries[0]; + assert(pEntryPoints); + assert(pEntryPoints->GetPlugin); + audin = (AUDIN_PLUGIN*)pEntryPoints->GetPlugin(pEntryPoints, "audin"); + + if (audin != NULL) + return CHANNEL_RC_ALREADY_INITIALIZED; + + audin = (AUDIN_PLUGIN*)calloc(1, sizeof(AUDIN_PLUGIN)); + + if (!audin) + { + WLog_ERR(TAG, "calloc failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + audin->log = WLog_Get(TAG); + audin->data = Stream_New(NULL, 4096); + audin->fixed_format = audio_format_new(); + + if (!audin->fixed_format) + goto out; + + if (!audin->data) + goto out; + + audin->dsp_context = freerdp_dsp_context_new(TRUE); + + if (!audin->dsp_context) + goto out; + + audin->attached = TRUE; + audin->iface.Initialize = audin_plugin_initialize; + audin->iface.Connected = NULL; + audin->iface.Disconnected = NULL; + audin->iface.Terminated = audin_plugin_terminated; + audin->iface.Attached = audin_plugin_attached; + audin->iface.Detached = audin_plugin_detached; + args = pEntryPoints->GetPluginData(pEntryPoints); + audin->rdpcontext = + ((freerdp*)((rdpSettings*)pEntryPoints->GetRdpSettings(pEntryPoints))->instance)->context; + + if (args) + { + if (!audin_process_addin_args(audin, args)) + goto out; + } + + if (audin->subsystem) + { + if ((error = audin_load_device_plugin(audin, audin->subsystem, args))) + { + WLog_Print( + audin->log, WLOG_ERROR, + "Unable to load microphone redirection subsystem %s because of error %" PRIu32 "", + audin->subsystem, error); + goto out; + } + } + else + { + while (entry && entry->subsystem && !audin->device) + { + if ((error = audin_set_subsystem(audin, entry->subsystem))) + { + WLog_Print(audin->log, WLOG_ERROR, + "audin_set_subsystem for %s failed with error %" PRIu32 "!", + entry->subsystem, error); + } + else if ((error = audin_set_device_name(audin, entry->device))) + { + WLog_Print(audin->log, WLOG_ERROR, + "audin_set_device_name for %s failed with error %" PRIu32 "!", + entry->subsystem, error); + } + else if ((error = audin_load_device_plugin(audin, audin->subsystem, args))) + { + WLog_Print(audin->log, WLOG_ERROR, + "audin_load_device_plugin %s failed with error %" PRIu32 "!", + entry->subsystem, error); + } + + entry++; + } + } + + if (audin->device == NULL) + { + /* If we have no audin device do not register plugin but still return OK or the client will + * just disconnect due to a missing microphone. */ + WLog_Print(audin->log, WLOG_ERROR, "No microphone device could be found."); + error = CHANNEL_RC_OK; + goto out; + } + + error = pEntryPoints->RegisterPlugin(pEntryPoints, "audin", (IWTSPlugin*)audin); + if (error == CHANNEL_RC_OK) + return error; + +out: + audin_plugin_terminated((IWTSPlugin*)audin); + return error; +} diff --git a/channels/audin/client/audin_main.h b/channels/audin/client/audin_main.h new file mode 100644 index 0000000..760d3c8 --- /dev/null +++ b/channels/audin/client/audin_main.h @@ -0,0 +1,35 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Audio Input Redirection Virtual Channel + * + * Copyright 2010-2011 Vic Lee + * + * Licensed 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 FREERDP_CHANNEL_AUDIN_CLIENT_MAIN_H +#define FREERDP_CHANNEL_AUDIN_CLIENT_MAIN_H + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include +#include +#include + +#define TAG CHANNELS_TAG("audin.client") + +#endif /* FREERDP_CHANNEL_AUDIN_CLIENT_MAIN_H */ diff --git a/channels/audin/client/mac/CMakeLists.txt b/channels/audin/client/mac/CMakeLists.txt new file mode 100644 index 0000000..f07e9f0 --- /dev/null +++ b/channels/audin/client/mac/CMakeLists.txt @@ -0,0 +1,35 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright (c) 2015 Armin Novak +# Copyright (c) 2015 Thincast Technologies GmbH +# +# Licensed 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. + +define_channel_client_subsystem("audin" "mac" "") +FIND_LIBRARY(CORE_AUDIO CoreAudio) +FIND_LIBRARY(AVFOUNDATION AVFoundation) +FIND_LIBRARY(AUDIO_TOOL AudioToolbox) +FIND_LIBRARY(APP_SERVICES ApplicationServices) + +set(${MODULE_PREFIX}_SRCS + audin_mac.m) + +include_directories(..) +include_directories(${MAC_INCLUDE_DIRS}) + +add_channel_client_subsystem_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} "" TRUE "") + +set(${MODULE_PREFIX}_LIBS freerdp ${AVFOUNDATION} ${CORE_AUDIO} ${AUDIO_TOOL} ${APP_SERVICES} winpr) + +target_link_libraries(${MODULE_NAME} ${${MODULE_PREFIX}_LIBS}) diff --git a/channels/audin/client/mac/audin_mac.m b/channels/audin/client/mac/audin_mac.m new file mode 100644 index 0000000..b81e551 --- /dev/null +++ b/channels/audin/client/mac/audin_mac.m @@ -0,0 +1,466 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Audio Input Redirection Virtual Channel - Mac OS X implementation + * + * Copyright (c) 2015 Armin Novak + * Copyright 2015 Thincast Technologies GmbH + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#import + +#define __COREFOUNDATION_CFPLUGINCOM__ 1 +#define IUNKNOWN_C_GUTS \ + void *_reserved; \ + void *QueryInterface; \ + void *AddRef; \ + void *Release + +#include +#include +#include +#include + +#include +#include + +#include "audin_main.h" + +#define MAC_AUDIO_QUEUE_NUM_BUFFERS 100 + +/* Fix for #4462: Provide type alias if not declared (Mac OS < 10.10) + * https://developer.apple.com/documentation/coreaudio/audioformatid + */ +#ifndef AudioFormatID +typedef UInt32 AudioFormatID; +#endif + +#ifndef AudioFormatFlags +typedef UInt32 AudioFormatFlags; +#endif + +typedef struct _AudinMacDevice +{ + IAudinDevice iface; + + AUDIO_FORMAT format; + UINT32 FramesPerPacket; + int dev_unit; + + AudinReceive receive; + void *user_data; + + rdpContext *rdpcontext; + + bool isAuthorized; + bool isOpen; + AudioQueueRef audioQueue; + AudioStreamBasicDescription audioFormat; + AudioQueueBufferRef audioBuffers[MAC_AUDIO_QUEUE_NUM_BUFFERS]; +} AudinMacDevice; + +static AudioFormatID audin_mac_get_format(const AUDIO_FORMAT *format) +{ + switch (format->wFormatTag) + { + case WAVE_FORMAT_PCM: + return kAudioFormatLinearPCM; + + default: + return 0; + } +} + +static AudioFormatFlags audin_mac_get_flags_for_format(const AUDIO_FORMAT *format) +{ + switch (format->wFormatTag) + { + case WAVE_FORMAT_PCM: + return kAudioFormatFlagIsSignedInteger; + + default: + return 0; + } +} + +static BOOL audin_mac_format_supported(IAudinDevice *device, const AUDIO_FORMAT *format) +{ + AudinMacDevice *mac = (AudinMacDevice *)device; + AudioFormatID req_fmt = 0; + + if (!mac->isAuthorized) + return FALSE; + + if (device == NULL || format == NULL) + return FALSE; + + req_fmt = audin_mac_get_format(format); + + if (req_fmt == 0) + return FALSE; + + return TRUE; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_mac_set_format(IAudinDevice *device, const AUDIO_FORMAT *format, + UINT32 FramesPerPacket) +{ + AudinMacDevice *mac = (AudinMacDevice *)device; + + if (!mac->isAuthorized) + return ERROR_INTERNAL_ERROR; + + if (device == NULL || format == NULL) + return ERROR_INVALID_PARAMETER; + + mac->FramesPerPacket = FramesPerPacket; + mac->format = *format; + WLog_INFO(TAG, "Audio Format %s [channels=%d, samples=%d, bits=%d]", + audio_format_get_tag_string(format->wFormatTag), format->nChannels, + format->nSamplesPerSec, format->wBitsPerSample); + mac->audioFormat.mBitsPerChannel = format->wBitsPerSample; + + if (format->wBitsPerSample == 0) + mac->audioFormat.mBitsPerChannel = 16; + + mac->audioFormat.mChannelsPerFrame = mac->format.nChannels; + mac->audioFormat.mFramesPerPacket = 1; + + mac->audioFormat.mBytesPerFrame = + mac->audioFormat.mChannelsPerFrame * (mac->audioFormat.mBitsPerChannel / 8); + mac->audioFormat.mBytesPerPacket = + mac->audioFormat.mBytesPerFrame * mac->audioFormat.mFramesPerPacket; + + mac->audioFormat.mFormatFlags = audin_mac_get_flags_for_format(format); + mac->audioFormat.mFormatID = audin_mac_get_format(format); + mac->audioFormat.mReserved = 0; + mac->audioFormat.mSampleRate = mac->format.nSamplesPerSec; + return CHANNEL_RC_OK; +} + +static void mac_audio_queue_input_cb(void *aqData, AudioQueueRef inAQ, AudioQueueBufferRef inBuffer, + const AudioTimeStamp *inStartTime, UInt32 inNumPackets, + const AudioStreamPacketDescription *inPacketDesc) +{ + AudinMacDevice *mac = (AudinMacDevice *)aqData; + UINT error = CHANNEL_RC_OK; + const BYTE *buffer = inBuffer->mAudioData; + int buffer_size = inBuffer->mAudioDataByteSize; + (void)inAQ; + (void)inStartTime; + (void)inNumPackets; + (void)inPacketDesc; + + if (buffer_size > 0) + error = mac->receive(&mac->format, buffer, buffer_size, mac->user_data); + + AudioQueueEnqueueBuffer(inAQ, inBuffer, 0, NULL); + + if (error) + { + WLog_ERR(TAG, "mac->receive failed with error %" PRIu32 "", error); + SetLastError(ERROR_INTERNAL_ERROR); + } +} + +static UINT audin_mac_close(IAudinDevice *device) +{ + UINT errCode = CHANNEL_RC_OK; + char errString[1024]; + OSStatus devStat; + AudinMacDevice *mac = (AudinMacDevice *)device; + + if (!mac->isAuthorized) + return ERROR_INTERNAL_ERROR; + + if (device == NULL) + return ERROR_INVALID_PARAMETER; + + if (mac->isOpen) + { + devStat = AudioQueueStop(mac->audioQueue, true); + + if (devStat != 0) + { + errCode = GetLastError(); + WLog_ERR(TAG, "AudioQueueStop failed with %s [%" PRIu32 "]", + winpr_strerror(errCode, errString, sizeof(errString)), errCode); + } + + mac->isOpen = false; + } + + if (mac->audioQueue) + { + devStat = AudioQueueDispose(mac->audioQueue, true); + + if (devStat != 0) + { + errCode = GetLastError(); + WLog_ERR(TAG, "AudioQueueDispose failed with %s [%" PRIu32 "]", + winpr_strerror(errCode, errString, sizeof(errString)), errCode); + } + + mac->audioQueue = NULL; + } + + mac->receive = NULL; + mac->user_data = NULL; + return errCode; +} + +static UINT audin_mac_open(IAudinDevice *device, AudinReceive receive, void *user_data) +{ + AudinMacDevice *mac = (AudinMacDevice *)device; + DWORD errCode; + char errString[1024]; + OSStatus devStat; + size_t index; + + if (!mac->isAuthorized) + return ERROR_INTERNAL_ERROR; + + mac->receive = receive; + mac->user_data = user_data; + devStat = AudioQueueNewInput(&(mac->audioFormat), mac_audio_queue_input_cb, mac, NULL, + kCFRunLoopCommonModes, 0, &(mac->audioQueue)); + + if (devStat != 0) + { + errCode = GetLastError(); + WLog_ERR(TAG, "AudioQueueNewInput failed with %s [%" PRIu32 "]", + winpr_strerror(errCode, errString, sizeof(errString)), errCode); + goto err_out; + } + + for (index = 0; index < MAC_AUDIO_QUEUE_NUM_BUFFERS; index++) + { + devStat = AudioQueueAllocateBuffer(mac->audioQueue, + mac->FramesPerPacket * 2 * mac->format.nChannels, + &mac->audioBuffers[index]); + + if (devStat != 0) + { + errCode = GetLastError(); + WLog_ERR(TAG, "AudioQueueAllocateBuffer failed with %s [%" PRIu32 "]", + winpr_strerror(errCode, errString, sizeof(errString)), errCode); + goto err_out; + } + + devStat = AudioQueueEnqueueBuffer(mac->audioQueue, mac->audioBuffers[index], 0, NULL); + + if (devStat != 0) + { + errCode = GetLastError(); + WLog_ERR(TAG, "AudioQueueEnqueueBuffer failed with %s [%" PRIu32 "]", + winpr_strerror(errCode, errString, sizeof(errString)), errCode); + goto err_out; + } + } + + devStat = AudioQueueStart(mac->audioQueue, NULL); + + if (devStat != 0) + { + errCode = GetLastError(); + WLog_ERR(TAG, "AudioQueueStart failed with %s [%" PRIu32 "]", + winpr_strerror(errCode, errString, sizeof(errString)), errCode); + goto err_out; + } + + mac->isOpen = true; + return CHANNEL_RC_OK; +err_out: + audin_mac_close(device); + return CHANNEL_RC_INITIALIZATION_ERROR; +} + +static UINT audin_mac_free(IAudinDevice *device) +{ + AudinMacDevice *mac = (AudinMacDevice *)device; + int error; + + if (device == NULL) + return ERROR_INVALID_PARAMETER; + + if ((error = audin_mac_close(device))) + { + WLog_ERR(TAG, "audin_oss_close failed with error code %d!", error); + } + + free(mac); + return CHANNEL_RC_OK; +} + +static UINT audin_mac_parse_addin_args(AudinMacDevice *device, ADDIN_ARGV *args) +{ + DWORD errCode; + char errString[1024]; + int status; + char *str_num, *eptr; + DWORD flags; + COMMAND_LINE_ARGUMENT_A *arg; + COMMAND_LINE_ARGUMENT_A audin_mac_args[] = { { "dev", COMMAND_LINE_VALUE_REQUIRED, "", + NULL, NULL, -1, NULL, "audio device name" }, + { NULL, 0, NULL, NULL, NULL, -1, NULL, NULL } }; + + AudinMacDevice *mac = (AudinMacDevice *)device; + + if (args->argc == 1) + return CHANNEL_RC_OK; + + flags = + COMMAND_LINE_SIGIL_NONE | COMMAND_LINE_SEPARATOR_COLON | COMMAND_LINE_IGN_UNKNOWN_KEYWORD; + status = + CommandLineParseArgumentsA(args->argc, args->argv, audin_mac_args, flags, mac, NULL, NULL); + + if (status < 0) + return ERROR_INVALID_PARAMETER; + + arg = audin_mac_args; + + do + { + if (!(arg->Flags & COMMAND_LINE_VALUE_PRESENT)) + continue; + + CommandLineSwitchStart(arg) CommandLineSwitchCase(arg, "dev") + { + str_num = _strdup(arg->Value); + + if (!str_num) + { + errCode = GetLastError(); + WLog_ERR(TAG, "_strdup failed with %s [%d]", + winpr_strerror(errCode, errString, sizeof(errString)), errCode); + return CHANNEL_RC_NO_MEMORY; + } + + mac->dev_unit = strtol(str_num, &eptr, 10); + + if (mac->dev_unit < 0 || *eptr != '\0') + mac->dev_unit = -1; + + free(str_num); + } + CommandLineSwitchEnd(arg) + } while ((arg = CommandLineFindNextArgumentA(arg)) != NULL); + + return CHANNEL_RC_OK; +} + +#ifdef BUILTIN_CHANNELS +#define freerdp_audin_client_subsystem_entry mac_freerdp_audin_client_subsystem_entry +#else +#define freerdp_audin_client_subsystem_entry FREERDP_API freerdp_audin_client_subsystem_entry +#endif + +UINT freerdp_audin_client_subsystem_entry(PFREERDP_AUDIN_DEVICE_ENTRY_POINTS pEntryPoints) +{ + DWORD errCode; + char errString[1024]; + ADDIN_ARGV *args; + AudinMacDevice *mac; + UINT error; + mac = (AudinMacDevice *)calloc(1, sizeof(AudinMacDevice)); + + if (!mac) + { + errCode = GetLastError(); + WLog_ERR(TAG, "calloc failed with %s [%" PRIu32 "]", + winpr_strerror(errCode, errString, sizeof(errString)), errCode); + return CHANNEL_RC_NO_MEMORY; + } + + mac->iface.Open = audin_mac_open; + mac->iface.FormatSupported = audin_mac_format_supported; + mac->iface.SetFormat = audin_mac_set_format; + mac->iface.Close = audin_mac_close; + mac->iface.Free = audin_mac_free; + mac->rdpcontext = pEntryPoints->rdpcontext; + mac->dev_unit = -1; + args = pEntryPoints->args; + + if ((error = audin_mac_parse_addin_args(mac, args))) + { + WLog_ERR(TAG, "audin_mac_parse_addin_args failed with %" PRIu32 "!", error); + goto error_out; + } + + if ((error = pEntryPoints->pRegisterAudinDevice(pEntryPoints->plugin, (IAudinDevice *)mac))) + { + WLog_ERR(TAG, "RegisterAudinDevice failed with error %" PRIu32 "!", error); + goto error_out; + } + +#if defined(MAC_OS_X_VERSION_10_14) + if (@available(macOS 10.14, *)) + { + @autoreleasepool { + AVAuthorizationStatus status = + [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeAudio]; + switch (status) + { + case AVAuthorizationStatusAuthorized: + mac->isAuthorized = TRUE; + break; + case AVAuthorizationStatusNotDetermined: + [AVCaptureDevice requestAccessForMediaType:AVMediaTypeAudio + completionHandler:^(BOOL granted) { + if (granted == YES) + { + mac->isAuthorized = TRUE; + } + else + WLog_WARN(TAG, "Microphone access denied by user"); + }]; + break; + case AVAuthorizationStatusRestricted: + WLog_WARN(TAG, "Microphone access restricted by policy"); + break; + case AVAuthorizationStatusDenied: + WLog_WARN(TAG, "Microphone access denied by policy"); + break; + default: + break; + } + } + } +#endif + + return CHANNEL_RC_OK; +error_out: + free(mac); + return error; +} diff --git a/channels/audin/client/opensles/CMakeLists.txt b/channels/audin/client/opensles/CMakeLists.txt new file mode 100644 index 0000000..abc6921 --- /dev/null +++ b/channels/audin/client/opensles/CMakeLists.txt @@ -0,0 +1,33 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2013 Armin Novak +# +# Licensed 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. + +define_channel_client_subsystem("audin" "opensles" "") + +set(${MODULE_PREFIX}_SRCS + opensl_io.c + audin_opensl_es.c) + +include_directories(..) +include_directories(${OPENSLES_INCLUDE_DIRS}) + +add_channel_client_subsystem_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} "" TRUE "") + + + +set(${MODULE_PREFIX}_LIBS freerdp ${OPENSLES_LIBRARIES}) + +target_link_libraries(${MODULE_NAME} ${${MODULE_PREFIX}_LIBS}) diff --git a/channels/audin/client/opensles/audin_opensl_es.c b/channels/audin/client/opensles/audin_opensl_es.c new file mode 100644 index 0000000..a3b7c3c --- /dev/null +++ b/channels/audin/client/opensles/audin_opensl_es.c @@ -0,0 +1,342 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Audio Input Redirection Virtual Channel - OpenSL ES implementation + * + * Copyright 2013 Armin Novak + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include +#include + +#include +#include + +#include +#include + +#include +#include + +#include "audin_main.h" +#include "opensl_io.h" + +typedef struct _AudinOpenSLESDevice +{ + IAudinDevice iface; + + char* device_name; + OPENSL_STREAM* stream; + + AUDIO_FORMAT format; + UINT32 frames_per_packet; + + UINT32 bytes_per_channel; + + AudinReceive receive; + + void* user_data; + + rdpContext* rdpcontext; + wLog* log; +} AudinOpenSLESDevice; + +static UINT audin_opensles_close(IAudinDevice* device); + +static void audin_receive(void* context, const void* data, size_t size) +{ + UINT error; + AudinOpenSLESDevice* opensles = (AudinOpenSLESDevice*)context; + + if (!opensles || !data) + { + WLog_ERR(TAG, "[%s] Invalid arguments context=%p, data=%p", __FUNCTION__, opensles, data); + return; + } + + error = opensles->receive(&opensles->format, data, size, opensles->user_data); + + if (error && opensles->rdpcontext) + setChannelError(opensles->rdpcontext, error, "audin_receive reported an error"); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_opensles_free(IAudinDevice* device) +{ + AudinOpenSLESDevice* opensles = (AudinOpenSLESDevice*)device; + + if (!opensles) + return ERROR_INVALID_PARAMETER; + + WLog_Print(opensles->log, WLOG_DEBUG, "device=%p", (void*)device); + + free(opensles->device_name); + free(opensles); + return CHANNEL_RC_OK; +} + +static BOOL audin_opensles_format_supported(IAudinDevice* device, const AUDIO_FORMAT* format) +{ + AudinOpenSLESDevice* opensles = (AudinOpenSLESDevice*)device; + + if (!opensles || !format) + return FALSE; + + WLog_Print(opensles->log, WLOG_DEBUG, "device=%p, format=%p", (void*)opensles, (void*)format); + assert(format); + + switch (format->wFormatTag) + { + case WAVE_FORMAT_PCM: /* PCM */ + if (format->cbSize == 0 && (format->nSamplesPerSec <= 48000) && + (format->wBitsPerSample == 8 || format->wBitsPerSample == 16) && + (format->nChannels >= 1 && format->nChannels <= 2)) + { + return TRUE; + } + + break; + + default: + WLog_Print(opensles->log, WLOG_DEBUG, "Encoding '%s' [0x%04X" PRIX16 "] not supported", + audio_format_get_tag_string(format->wFormatTag), format->wFormatTag); + break; + } + + return FALSE; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_opensles_set_format(IAudinDevice* device, const AUDIO_FORMAT* format, + UINT32 FramesPerPacket) +{ + AudinOpenSLESDevice* opensles = (AudinOpenSLESDevice*)device; + + if (!opensles || !format) + return ERROR_INVALID_PARAMETER; + + WLog_Print(opensles->log, WLOG_DEBUG, "device=%p, format=%p, FramesPerPacket=%" PRIu32 "", + (void*)device, (void*)format, FramesPerPacket); + assert(format); + + opensles->format = *format; + + switch (format->wFormatTag) + { + case WAVE_FORMAT_PCM: + opensles->frames_per_packet = FramesPerPacket; + + switch (format->wBitsPerSample) + { + case 4: + opensles->bytes_per_channel = 1; + break; + + case 8: + opensles->bytes_per_channel = 1; + break; + + case 16: + opensles->bytes_per_channel = 2; + break; + + default: + return ERROR_UNSUPPORTED_TYPE; + } + + break; + + default: + WLog_Print(opensles->log, WLOG_ERROR, + "Encoding '%" PRIu16 "' [%04" PRIX16 "] not supported", format->wFormatTag, + format->wFormatTag); + return ERROR_UNSUPPORTED_TYPE; + } + + WLog_Print(opensles->log, WLOG_DEBUG, "frames_per_packet=%" PRIu32, + opensles->frames_per_packet); + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_opensles_open(IAudinDevice* device, AudinReceive receive, void* user_data) +{ + AudinOpenSLESDevice* opensles = (AudinOpenSLESDevice*)device; + + if (!opensles || !receive || !user_data) + return ERROR_INVALID_PARAMETER; + + WLog_Print(opensles->log, WLOG_DEBUG, "device=%p, receive=%p, user_data=%p", (void*)device, + (void*)receive, (void*)user_data); + + if (opensles->stream) + goto error_out; + + if (!(opensles->stream = android_OpenRecDevice( + opensles, audin_receive, opensles->format.nSamplesPerSec, opensles->format.nChannels, + opensles->frames_per_packet, opensles->format.wBitsPerSample))) + { + WLog_Print(opensles->log, WLOG_ERROR, "android_OpenRecDevice failed!"); + goto error_out; + } + + opensles->receive = receive; + opensles->user_data = user_data; + return CHANNEL_RC_OK; +error_out: + audin_opensles_close(device); + return ERROR_INTERNAL_ERROR; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT audin_opensles_close(IAudinDevice* device) +{ + AudinOpenSLESDevice* opensles = (AudinOpenSLESDevice*)device; + + if (!opensles) + return ERROR_INVALID_PARAMETER; + + WLog_Print(opensles->log, WLOG_DEBUG, "device=%p", (void*)device); + android_CloseRecDevice(opensles->stream); + opensles->receive = NULL; + opensles->user_data = NULL; + opensles->stream = NULL; + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_opensles_parse_addin_args(AudinOpenSLESDevice* device, ADDIN_ARGV* args) +{ + UINT status; + DWORD flags; + const COMMAND_LINE_ARGUMENT_A* arg; + AudinOpenSLESDevice* opensles = (AudinOpenSLESDevice*)device; + COMMAND_LINE_ARGUMENT_A audin_opensles_args[] = { + { "dev", COMMAND_LINE_VALUE_REQUIRED, "", NULL, NULL, -1, NULL, + "audio device name" }, + { NULL, 0, NULL, NULL, NULL, -1, NULL, NULL } + }; + + WLog_Print(opensles->log, WLOG_DEBUG, "device=%p, args=%p", (void*)device, (void*)args); + flags = + COMMAND_LINE_SIGIL_NONE | COMMAND_LINE_SEPARATOR_COLON | COMMAND_LINE_IGN_UNKNOWN_KEYWORD; + status = CommandLineParseArgumentsA(args->argc, args->argv, audin_opensles_args, flags, + opensles, NULL, NULL); + + if (status < 0) + return status; + + arg = audin_opensles_args; + + do + { + if (!(arg->Flags & COMMAND_LINE_VALUE_PRESENT)) + continue; + + CommandLineSwitchStart(arg) CommandLineSwitchCase(arg, "dev") + { + opensles->device_name = _strdup(arg->Value); + + if (!opensles->device_name) + { + WLog_Print(opensles->log, WLOG_ERROR, "_strdup failed!"); + return CHANNEL_RC_NO_MEMORY; + } + } + CommandLineSwitchEnd(arg) + } while ((arg = CommandLineFindNextArgumentA(arg)) != NULL); + + return CHANNEL_RC_OK; +} + +#ifdef BUILTIN_CHANNELS +#define freerdp_audin_client_subsystem_entry opensles_freerdp_audin_client_subsystem_entry +#else +#define freerdp_audin_client_subsystem_entry FREERDP_API freerdp_audin_client_subsystem_entry +#endif + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT freerdp_audin_client_subsystem_entry(PFREERDP_AUDIN_DEVICE_ENTRY_POINTS pEntryPoints) +{ + ADDIN_ARGV* args; + AudinOpenSLESDevice* opensles; + UINT error; + opensles = (AudinOpenSLESDevice*)calloc(1, sizeof(AudinOpenSLESDevice)); + + if (!opensles) + { + WLog_ERR(TAG, "calloc failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + opensles->log = WLog_Get(TAG); + opensles->iface.Open = audin_opensles_open; + opensles->iface.FormatSupported = audin_opensles_format_supported; + opensles->iface.SetFormat = audin_opensles_set_format; + opensles->iface.Close = audin_opensles_close; + opensles->iface.Free = audin_opensles_free; + opensles->rdpcontext = pEntryPoints->rdpcontext; + args = pEntryPoints->args; + + if ((error = audin_opensles_parse_addin_args(opensles, args))) + { + WLog_Print(opensles->log, WLOG_ERROR, + "audin_opensles_parse_addin_args failed with errorcode %" PRIu32 "!", error); + goto error_out; + } + + if ((error = pEntryPoints->pRegisterAudinDevice(pEntryPoints->plugin, (IAudinDevice*)opensles))) + { + WLog_Print(opensles->log, WLOG_ERROR, "RegisterAudinDevice failed with error %" PRIu32 "!", + error); + goto error_out; + } + + return CHANNEL_RC_OK; +error_out: + free(opensles); + return error; +} diff --git a/channels/audin/client/opensles/opensl_io.c b/channels/audin/client/opensles/opensl_io.c new file mode 100644 index 0000000..be3e0b4 --- /dev/null +++ b/channels/audin/client/opensles/opensl_io.c @@ -0,0 +1,388 @@ +/* +opensl_io.c: +Android OpenSL input/output module +Copyright (c) 2012, Victor Lazzarini +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include + +#include "audin_main.h" +#include "opensl_io.h" +#define CONV16BIT 32768 +#define CONVMYFLT (1. / 32768.) + +typedef struct +{ + size_t size; + void* data; +} queue_element; + +struct opensl_stream +{ + // engine interfaces + SLObjectItf engineObject; + SLEngineItf engineEngine; + + // device interfaces + SLDeviceVolumeItf deviceVolume; + + // recorder interfaces + SLObjectItf recorderObject; + SLRecordItf recorderRecord; + SLAndroidSimpleBufferQueueItf recorderBufferQueue; + + unsigned int inchannels; + unsigned int sr; + unsigned int buffersize; + unsigned int bits_per_sample; + + queue_element* prep; + queue_element* next; + + void* context; + opensl_receive_t receive; +}; + +static void bqRecorderCallback(SLAndroidSimpleBufferQueueItf bq, void* context); + +// creates the OpenSL ES audio engine +static SLresult openSLCreateEngine(OPENSL_STREAM* p) +{ + SLresult result; + // create engine + result = slCreateEngine(&(p->engineObject), 0, NULL, 0, NULL, NULL); + + if (result != SL_RESULT_SUCCESS) + goto engine_end; + + // realize the engine + result = (*p->engineObject)->Realize(p->engineObject, SL_BOOLEAN_FALSE); + + if (result != SL_RESULT_SUCCESS) + goto engine_end; + + // get the engine interface, which is needed in order to create other objects + result = (*p->engineObject)->GetInterface(p->engineObject, SL_IID_ENGINE, &(p->engineEngine)); + + if (result != SL_RESULT_SUCCESS) + goto engine_end; + + // get the volume interface - important, this is optional! + result = + (*p->engineObject)->GetInterface(p->engineObject, SL_IID_DEVICEVOLUME, &(p->deviceVolume)); + + if (result != SL_RESULT_SUCCESS) + { + p->deviceVolume = NULL; + result = SL_RESULT_SUCCESS; + } + +engine_end: + assert(SL_RESULT_SUCCESS == result); + return result; +} + +// Open the OpenSL ES device for input +static SLresult openSLRecOpen(OPENSL_STREAM* p) +{ + SLresult result; + SLuint32 sr = p->sr; + SLuint32 channels = p->inchannels; + assert(!p->recorderObject); + + if (channels) + { + switch (sr) + { + case 8000: + sr = SL_SAMPLINGRATE_8; + break; + + case 11025: + sr = SL_SAMPLINGRATE_11_025; + break; + + case 16000: + sr = SL_SAMPLINGRATE_16; + break; + + case 22050: + sr = SL_SAMPLINGRATE_22_05; + break; + + case 24000: + sr = SL_SAMPLINGRATE_24; + break; + + case 32000: + sr = SL_SAMPLINGRATE_32; + break; + + case 44100: + sr = SL_SAMPLINGRATE_44_1; + break; + + case 48000: + sr = SL_SAMPLINGRATE_48; + break; + + case 64000: + sr = SL_SAMPLINGRATE_64; + break; + + case 88200: + sr = SL_SAMPLINGRATE_88_2; + break; + + case 96000: + sr = SL_SAMPLINGRATE_96; + break; + + case 192000: + sr = SL_SAMPLINGRATE_192; + break; + + default: + return -1; + } + + // configure audio source + SLDataLocator_IODevice loc_dev = { SL_DATALOCATOR_IODEVICE, SL_IODEVICE_AUDIOINPUT, + SL_DEFAULTDEVICEID_AUDIOINPUT, NULL }; + SLDataSource audioSrc = { &loc_dev, NULL }; + // configure audio sink + int speakers; + + if (channels > 1) + speakers = SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT; + else + speakers = SL_SPEAKER_FRONT_CENTER; + + SLDataLocator_AndroidSimpleBufferQueue loc_bq = { SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, + 2 }; + SLDataFormat_PCM format_pcm; + format_pcm.formatType = SL_DATAFORMAT_PCM; + format_pcm.numChannels = channels; + format_pcm.samplesPerSec = sr; + format_pcm.channelMask = speakers; + format_pcm.endianness = SL_BYTEORDER_LITTLEENDIAN; + + if (16 == p->bits_per_sample) + { + format_pcm.bitsPerSample = SL_PCMSAMPLEFORMAT_FIXED_16; + format_pcm.containerSize = 16; + } + else if (8 == p->bits_per_sample) + { + format_pcm.bitsPerSample = SL_PCMSAMPLEFORMAT_FIXED_8; + format_pcm.containerSize = 8; + } + else + assert(0); + + SLDataSink audioSnk = { &loc_bq, &format_pcm }; + // create audio recorder + // (requires the RECORD_AUDIO permission) + const SLInterfaceID id[] = { SL_IID_ANDROIDSIMPLEBUFFERQUEUE }; + const SLboolean req[] = { SL_BOOLEAN_TRUE }; + result = (*p->engineEngine) + ->CreateAudioRecorder(p->engineEngine, &(p->recorderObject), &audioSrc, + &audioSnk, 1, id, req); + assert(!result); + + if (SL_RESULT_SUCCESS != result) + goto end_recopen; + + // realize the audio recorder + result = (*p->recorderObject)->Realize(p->recorderObject, SL_BOOLEAN_FALSE); + assert(!result); + + if (SL_RESULT_SUCCESS != result) + goto end_recopen; + + // get the record interface + result = (*p->recorderObject) + ->GetInterface(p->recorderObject, SL_IID_RECORD, &(p->recorderRecord)); + assert(!result); + + if (SL_RESULT_SUCCESS != result) + goto end_recopen; + + // get the buffer queue interface + result = (*p->recorderObject) + ->GetInterface(p->recorderObject, SL_IID_ANDROIDSIMPLEBUFFERQUEUE, + &(p->recorderBufferQueue)); + assert(!result); + + if (SL_RESULT_SUCCESS != result) + goto end_recopen; + + // register callback on the buffer queue + result = (*p->recorderBufferQueue) + ->RegisterCallback(p->recorderBufferQueue, bqRecorderCallback, p); + assert(!result); + + if (SL_RESULT_SUCCESS != result) + goto end_recopen; + + end_recopen: + return result; + } + else + return SL_RESULT_SUCCESS; +} + +// close the OpenSL IO and destroy the audio engine +static void openSLDestroyEngine(OPENSL_STREAM* p) +{ + // destroy audio recorder object, and invalidate all associated interfaces + if (p->recorderObject != NULL) + { + (*p->recorderObject)->Destroy(p->recorderObject); + p->recorderObject = NULL; + p->recorderRecord = NULL; + p->recorderBufferQueue = NULL; + } + + // destroy engine object, and invalidate all associated interfaces + if (p->engineObject != NULL) + { + (*p->engineObject)->Destroy(p->engineObject); + p->engineObject = NULL; + p->engineEngine = NULL; + } +} + +static queue_element* opensles_queue_element_new(size_t size) +{ + queue_element* q = calloc(1, sizeof(queue_element)); + + if (!q) + goto fail; + + q->size = size; + q->data = malloc(size); + + if (!q->data) + goto fail; + + return q; +fail: + free(q); + return NULL; +} + +static void opensles_queue_element_free(void* obj) +{ + queue_element* e = (queue_element*)obj; + + if (e) + free(e->data); + + free(e); +} + +// open the android audio device for input +OPENSL_STREAM* android_OpenRecDevice(void* context, opensl_receive_t receive, int sr, + int inchannels, int bufferframes, int bits_per_sample) +{ + OPENSL_STREAM* p; + + if (!context || !receive) + return NULL; + + p = (OPENSL_STREAM*)calloc(1, sizeof(OPENSL_STREAM)); + + if (!p) + return NULL; + + p->context = context; + p->receive = receive; + p->inchannels = inchannels; + p->sr = sr; + p->buffersize = bufferframes; + p->bits_per_sample = bits_per_sample; + + if ((p->bits_per_sample != 8) && (p->bits_per_sample != 16)) + goto fail; + + if (openSLCreateEngine(p) != SL_RESULT_SUCCESS) + goto fail; + + if (openSLRecOpen(p) != SL_RESULT_SUCCESS) + goto fail; + + /* Create receive buffers, prepare them and start recording */ + p->prep = opensles_queue_element_new(p->buffersize * p->bits_per_sample / 8); + p->next = opensles_queue_element_new(p->buffersize * p->bits_per_sample / 8); + + if (!p->prep || !p->next) + goto fail; + + (*p->recorderBufferQueue)->Enqueue(p->recorderBufferQueue, p->next->data, p->next->size); + (*p->recorderBufferQueue)->Enqueue(p->recorderBufferQueue, p->prep->data, p->prep->size); + (*p->recorderRecord)->SetRecordState(p->recorderRecord, SL_RECORDSTATE_RECORDING); + return p; +fail: + android_CloseRecDevice(p); + return NULL; +} + +// close the android audio device +void android_CloseRecDevice(OPENSL_STREAM* p) +{ + if (p == NULL) + return; + + opensles_queue_element_free(p->next); + opensles_queue_element_free(p->prep); + openSLDestroyEngine(p); + free(p); +} + +// this callback handler is called every time a buffer finishes recording +static void bqRecorderCallback(SLAndroidSimpleBufferQueueItf bq, void* context) +{ + OPENSL_STREAM* p = (OPENSL_STREAM*)context; + queue_element* e; + + if (!p) + return; + + e = p->next; + + if (!e) + return; + + if (!p->context || !p->receive) + WLog_WARN(TAG, "Missing receive callback=%p, context=%p", p->receive, p->context); + else + p->receive(p->context, e->data, e->size); + + p->next = p->prep; + p->prep = e; + (*p->recorderBufferQueue)->Enqueue(p->recorderBufferQueue, e->data, e->size); +} diff --git a/channels/audin/client/opensles/opensl_io.h b/channels/audin/client/opensles/opensl_io.h new file mode 100644 index 0000000..e99522c --- /dev/null +++ b/channels/audin/client/opensles/opensl_io.h @@ -0,0 +1,65 @@ +/* +opensl_io.c: +Android OpenSL input/output module header +Copyright (c) 2012, Victor Lazzarini +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#ifndef FREERDP_CHANNEL_AUDIN_CLIENT_OPENSL_IO_H +#define FREERDP_CHANNEL_AUDIN_CLIENT_OPENSL_IO_H + +#include +#include + +#include + +#include + +#ifdef __cplusplus +extern "C" +{ +#endif + + typedef struct opensl_stream OPENSL_STREAM; + + typedef void (*opensl_receive_t)(void* context, const void* data, size_t size); + + /* + Open the audio device with a given sampling rate (sr), input and output channels and IO buffer + size in frames. Returns a handle to the OpenSL stream + */ + FREERDP_LOCAL OPENSL_STREAM* android_OpenRecDevice(void* context, opensl_receive_t receive, + int sr, int inchannels, int bufferframes, + int bits_per_sample); + /* + Close the audio device + */ + FREERDP_LOCAL void android_CloseRecDevice(OPENSL_STREAM* p); + +#ifdef __cplusplus +}; +#endif + +#endif /* FREERDP_CHANNEL_AUDIN_CLIENT_OPENSL_IO_H */ diff --git a/channels/audin/client/oss/CMakeLists.txt b/channels/audin/client/oss/CMakeLists.txt new file mode 100644 index 0000000..bf51ce0 --- /dev/null +++ b/channels/audin/client/oss/CMakeLists.txt @@ -0,0 +1,33 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright (c) 2015 Rozhuk Ivan +# +# Licensed 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. + +define_channel_client_subsystem("audin" "oss" "") + +set(${MODULE_PREFIX}_SRCS + audin_oss.c) + +include_directories(..) +include_directories(${OSS_INCLUDE_DIRS}) + +add_channel_client_subsystem_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} "" TRUE "") + + + +set(${MODULE_PREFIX}_LIBS freerdp winpr ${OSS_LIBRARIES}) + +target_link_libraries(${MODULE_NAME} ${${MODULE_PREFIX}_LIBS}) + diff --git a/channels/audin/client/oss/audin_oss.c b/channels/audin/client/oss/audin_oss.c new file mode 100644 index 0000000..305686c --- /dev/null +++ b/channels/audin/client/oss/audin_oss.c @@ -0,0 +1,488 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Audio Input Redirection Virtual Channel - OSS implementation + * + * Copyright (c) 2015 Rozhuk Ivan + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#if defined(__OpenBSD__) +#include +#else +#include +#endif +#include + +#include +#include + +#include "audin_main.h" + +typedef struct _AudinOSSDevice +{ + IAudinDevice iface; + + HANDLE thread; + HANDLE stopEvent; + + AUDIO_FORMAT format; + UINT32 FramesPerPacket; + int dev_unit; + + AudinReceive receive; + void* user_data; + + rdpContext* rdpcontext; +} AudinOSSDevice; + +#define OSS_LOG_ERR(_text, _error) \ + if (_error != 0) \ + WLog_ERR(TAG, "%s: %i - %s\n", _text, _error, strerror(_error)); + +static UINT32 audin_oss_get_format(const AUDIO_FORMAT* format) +{ + switch (format->wFormatTag) + { + case WAVE_FORMAT_PCM: + switch (format->wBitsPerSample) + { + case 8: + return AFMT_S8; + + case 16: + return AFMT_S16_LE; + } + + break; + + case WAVE_FORMAT_ALAW: + return AFMT_A_LAW; + + case WAVE_FORMAT_MULAW: + return AFMT_MU_LAW; + } + + return 0; +} + +static BOOL audin_oss_format_supported(IAudinDevice* device, const AUDIO_FORMAT* format) +{ + if (device == NULL || format == NULL) + return FALSE; + + switch (format->wFormatTag) + { + case WAVE_FORMAT_PCM: + if (format->cbSize != 0 || format->nSamplesPerSec > 48000 || + (format->wBitsPerSample != 8 && format->wBitsPerSample != 16) || + (format->nChannels != 1 && format->nChannels != 2)) + return FALSE; + + break; + + default: + return FALSE; + } + + return TRUE; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_oss_set_format(IAudinDevice* device, const AUDIO_FORMAT* format, + UINT32 FramesPerPacket) +{ + AudinOSSDevice* oss = (AudinOSSDevice*)device; + + if (device == NULL || format == NULL) + return ERROR_INVALID_PARAMETER; + + oss->FramesPerPacket = FramesPerPacket; + oss->format = *format; + return CHANNEL_RC_OK; +} + +static DWORD WINAPI audin_oss_thread_func(LPVOID arg) +{ + char dev_name[PATH_MAX] = "/dev/dsp"; + char mixer_name[PATH_MAX] = "/dev/mixer"; + int pcm_handle = -1, mixer_handle; + BYTE* buffer = NULL; + unsigned long tmp; + size_t buffer_size; + AudinOSSDevice* oss = (AudinOSSDevice*)arg; + UINT error = 0; + DWORD status; + + if (oss == NULL) + { + error = ERROR_INVALID_PARAMETER; + goto err_out; + } + + if (oss->dev_unit != -1) + { + sprintf_s(dev_name, (PATH_MAX - 1), "/dev/dsp%i", oss->dev_unit); + sprintf_s(mixer_name, PATH_MAX - 1, "/dev/mixer%i", oss->dev_unit); + } + + WLog_INFO(TAG, "open: %s", dev_name); + + if ((pcm_handle = open(dev_name, O_RDONLY)) < 0) + { + OSS_LOG_ERR("sound dev open failed", errno); + error = ERROR_INTERNAL_ERROR; + goto err_out; + } + + /* Set rec volume to 100%. */ + if ((mixer_handle = open(mixer_name, O_RDWR)) < 0) + { + OSS_LOG_ERR("mixer open failed, not critical", errno); + } + else + { + tmp = (100 | (100 << 8)); + + if (ioctl(mixer_handle, MIXER_WRITE(SOUND_MIXER_MIC), &tmp) == -1) + OSS_LOG_ERR("WRITE_MIXER - SOUND_MIXER_MIC, not critical", errno); + + tmp = (100 | (100 << 8)); + + if (ioctl(mixer_handle, MIXER_WRITE(SOUND_MIXER_RECLEV), &tmp) == -1) + OSS_LOG_ERR("WRITE_MIXER - SOUND_MIXER_RECLEV, not critical", errno); + + close(mixer_handle); + } + +#if 0 /* FreeBSD OSS implementation at this moment (2015.03) does not set PCM_CAP_INPUT flag. */ + tmp = 0; + + if (ioctl(pcm_handle, SNDCTL_DSP_GETCAPS, &tmp) == -1) + { + OSS_LOG_ERR("SNDCTL_DSP_GETCAPS failed, try ignory", errno); + } + else if ((tmp & PCM_CAP_INPUT) == 0) + { + OSS_LOG_ERR("Device does not supports playback", EOPNOTSUPP); + goto err_out; + } + +#endif + /* Set format. */ + tmp = audin_oss_get_format(&oss->format); + + if (ioctl(pcm_handle, SNDCTL_DSP_SETFMT, &tmp) == -1) + OSS_LOG_ERR("SNDCTL_DSP_SETFMT failed", errno); + + tmp = oss->format.nChannels; + + if (ioctl(pcm_handle, SNDCTL_DSP_CHANNELS, &tmp) == -1) + OSS_LOG_ERR("SNDCTL_DSP_CHANNELS failed", errno); + + tmp = oss->format.nSamplesPerSec; + + if (ioctl(pcm_handle, SNDCTL_DSP_SPEED, &tmp) == -1) + OSS_LOG_ERR("SNDCTL_DSP_SPEED failed", errno); + + tmp = oss->format.nBlockAlign; + + if (ioctl(pcm_handle, SNDCTL_DSP_SETFRAGMENT, &tmp) == -1) + OSS_LOG_ERR("SNDCTL_DSP_SETFRAGMENT failed", errno); + + buffer_size = (oss->FramesPerPacket * oss->format.nChannels * (oss->format.wBitsPerSample / 8)); + buffer = (BYTE*)calloc((buffer_size + sizeof(void*)), sizeof(BYTE)); + + if (NULL == buffer) + { + OSS_LOG_ERR("malloc() fail", errno); + error = ERROR_NOT_ENOUGH_MEMORY; + goto err_out; + } + + while (1) + { + SSIZE_T stmp; + status = WaitForSingleObject(oss->stopEvent, 0); + + if (status == WAIT_FAILED) + { + error = GetLastError(); + WLog_ERR(TAG, "WaitForSingleObject failed with error %" PRIu32 "", error); + goto err_out; + } + + if (status == WAIT_OBJECT_0) + break; + + stmp = read(pcm_handle, buffer, buffer_size); + + /* Error happen. */ + if (stmp < 0) + { + OSS_LOG_ERR("read() error", errno); + continue; + } + + if ((size_t)stmp < buffer_size) /* Not enouth data. */ + continue; + + if ((error = oss->receive(&oss->format, buffer, buffer_size, oss->user_data))) + { + WLog_ERR(TAG, "oss->receive failed with error %" PRIu32 "", error); + break; + } + } + +err_out: + + if (error && oss && oss->rdpcontext) + setChannelError(oss->rdpcontext, error, "audin_oss_thread_func reported an error"); + + if (pcm_handle != -1) + { + WLog_INFO(TAG, "close: %s", dev_name); + close(pcm_handle); + } + + free(buffer); + ExitThread(error); + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_oss_open(IAudinDevice* device, AudinReceive receive, void* user_data) +{ + AudinOSSDevice* oss = (AudinOSSDevice*)device; + oss->receive = receive; + oss->user_data = user_data; + + if (!(oss->stopEvent = CreateEvent(NULL, TRUE, FALSE, NULL))) + { + WLog_ERR(TAG, "CreateEvent failed!"); + return ERROR_INTERNAL_ERROR; + } + + if (!(oss->thread = CreateThread(NULL, 0, audin_oss_thread_func, oss, 0, NULL))) + { + WLog_ERR(TAG, "CreateThread failed!"); + CloseHandle(oss->stopEvent); + oss->stopEvent = NULL; + return ERROR_INTERNAL_ERROR; + } + + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_oss_close(IAudinDevice* device) +{ + UINT error; + AudinOSSDevice* oss = (AudinOSSDevice*)device; + + if (device == NULL) + return ERROR_INVALID_PARAMETER; + + if (oss->stopEvent != NULL) + { + SetEvent(oss->stopEvent); + + if (WaitForSingleObject(oss->thread, INFINITE) == WAIT_FAILED) + { + error = GetLastError(); + WLog_ERR(TAG, "WaitForSingleObject failed with error %" PRIu32 "", error); + return error; + } + + CloseHandle(oss->stopEvent); + oss->stopEvent = NULL; + CloseHandle(oss->thread); + oss->thread = NULL; + } + + oss->receive = NULL; + oss->user_data = NULL; + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_oss_free(IAudinDevice* device) +{ + AudinOSSDevice* oss = (AudinOSSDevice*)device; + UINT error; + + if (device == NULL) + return ERROR_INVALID_PARAMETER; + + if ((error = audin_oss_close(device))) + { + WLog_ERR(TAG, "audin_oss_close failed with error code %d!", error); + } + + free(oss); + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_oss_parse_addin_args(AudinOSSDevice* device, ADDIN_ARGV* args) +{ + int status; + char *str_num, *eptr; + DWORD flags; + COMMAND_LINE_ARGUMENT_A* arg; + AudinOSSDevice* oss = (AudinOSSDevice*)device; + COMMAND_LINE_ARGUMENT_A audin_oss_args[] = { { "dev", COMMAND_LINE_VALUE_REQUIRED, "", + NULL, NULL, -1, NULL, "audio device name" }, + { NULL, 0, NULL, NULL, NULL, -1, NULL, NULL } }; + + flags = + COMMAND_LINE_SIGIL_NONE | COMMAND_LINE_SEPARATOR_COLON | COMMAND_LINE_IGN_UNKNOWN_KEYWORD; + status = + CommandLineParseArgumentsA(args->argc, args->argv, audin_oss_args, flags, oss, NULL, NULL); + + if (status < 0) + return ERROR_INVALID_PARAMETER; + + arg = audin_oss_args; + errno = 0; + + do + { + if (!(arg->Flags & COMMAND_LINE_VALUE_PRESENT)) + continue; + + CommandLineSwitchStart(arg) CommandLineSwitchCase(arg, "dev") + { + str_num = _strdup(arg->Value); + + if (!str_num) + { + WLog_ERR(TAG, "_strdup failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + { + long val = strtol(str_num, &eptr, 10); + + if ((errno != 0) || (val < INT32_MIN) || (val > INT32_MAX)) + { + free(str_num); + return CHANNEL_RC_NULL_DATA; + } + + oss->dev_unit = (INT32)val; + } + + if (oss->dev_unit < 0 || *eptr != '\0') + oss->dev_unit = -1; + + free(str_num); + } + CommandLineSwitchEnd(arg) + } while ((arg = CommandLineFindNextArgumentA(arg)) != NULL); + + return CHANNEL_RC_OK; +} + +#ifdef BUILTIN_CHANNELS +#define freerdp_audin_client_subsystem_entry oss_freerdp_audin_client_subsystem_entry +#else +#define freerdp_audin_client_subsystem_entry FREERDP_API freerdp_audin_client_subsystem_entry +#endif + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT freerdp_audin_client_subsystem_entry(PFREERDP_AUDIN_DEVICE_ENTRY_POINTS pEntryPoints) +{ + ADDIN_ARGV* args; + AudinOSSDevice* oss; + UINT error; + oss = (AudinOSSDevice*)calloc(1, sizeof(AudinOSSDevice)); + + if (!oss) + { + WLog_ERR(TAG, "calloc failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + oss->iface.Open = audin_oss_open; + oss->iface.FormatSupported = audin_oss_format_supported; + oss->iface.SetFormat = audin_oss_set_format; + oss->iface.Close = audin_oss_close; + oss->iface.Free = audin_oss_free; + oss->rdpcontext = pEntryPoints->rdpcontext; + oss->dev_unit = -1; + args = pEntryPoints->args; + + if ((error = audin_oss_parse_addin_args(oss, args))) + { + WLog_ERR(TAG, "audin_oss_parse_addin_args failed with errorcode %" PRIu32 "!", error); + goto error_out; + } + + if ((error = pEntryPoints->pRegisterAudinDevice(pEntryPoints->plugin, (IAudinDevice*)oss))) + { + WLog_ERR(TAG, "RegisterAudinDevice failed with error %" PRIu32 "!", error); + goto error_out; + } + + return CHANNEL_RC_OK; +error_out: + free(oss); + return error; +} diff --git a/channels/audin/client/pulse/CMakeLists.txt b/channels/audin/client/pulse/CMakeLists.txt new file mode 100644 index 0000000..e50f79a --- /dev/null +++ b/channels/audin/client/pulse/CMakeLists.txt @@ -0,0 +1,31 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2012 Marc-Andre Moreau +# +# Licensed 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. + +define_channel_client_subsystem("audin" "pulse" "") + +set(${MODULE_PREFIX}_SRCS + audin_pulse.c) + +include_directories(..) +include_directories(${PULSE_INCLUDE_DIR}) + +add_channel_client_subsystem_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} "" TRUE "") + + +set(${MODULE_PREFIX}_LIBS freerdp ${PULSE_LIBRARY} winpr) + +target_link_libraries(${MODULE_NAME} ${${MODULE_PREFIX}_LIBS}) diff --git a/channels/audin/client/pulse/audin_pulse.c b/channels/audin/client/pulse/audin_pulse.c new file mode 100644 index 0000000..fb8fdb7 --- /dev/null +++ b/channels/audin/client/pulse/audin_pulse.c @@ -0,0 +1,572 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Audio Input Redirection Virtual Channel - PulseAudio implementation + * + * Copyright 2010-2011 Vic Lee + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include + +#include +#include +#include + +#include + +#include +#include +#include +#include + +#include "audin_main.h" + +typedef struct _AudinPulseDevice +{ + IAudinDevice iface; + + char* device_name; + UINT32 frames_per_packet; + pa_threaded_mainloop* mainloop; + pa_context* context; + pa_sample_spec sample_spec; + pa_stream* stream; + AUDIO_FORMAT format; + + size_t bytes_per_frame; + size_t buffer_frames; + + AudinReceive receive; + void* user_data; + + rdpContext* rdpcontext; + wLog* log; +} AudinPulseDevice; + +static const char* pulse_context_state_string(pa_context_state_t state) +{ + switch (state) + { + case PA_CONTEXT_UNCONNECTED: + return "PA_CONTEXT_UNCONNECTED"; + case PA_CONTEXT_CONNECTING: + return "PA_CONTEXT_CONNECTING"; + case PA_CONTEXT_AUTHORIZING: + return "PA_CONTEXT_AUTHORIZING"; + case PA_CONTEXT_SETTING_NAME: + return "PA_CONTEXT_SETTING_NAME"; + case PA_CONTEXT_READY: + return "PA_CONTEXT_READY"; + case PA_CONTEXT_FAILED: + return "PA_CONTEXT_FAILED"; + case PA_CONTEXT_TERMINATED: + return "PA_CONTEXT_TERMINATED"; + default: + return "UNKNOWN"; + } +} + +static const char* pulse_stream_state_string(pa_stream_state_t state) +{ + switch (state) + { + case PA_STREAM_UNCONNECTED: + return "PA_STREAM_UNCONNECTED"; + case PA_STREAM_CREATING: + return "PA_STREAM_CREATING"; + case PA_STREAM_READY: + return "PA_STREAM_READY"; + case PA_STREAM_FAILED: + return "PA_STREAM_FAILED"; + case PA_STREAM_TERMINATED: + return "PA_STREAM_TERMINATED"; + default: + return "UNKNOWN"; + } +} + +static void audin_pulse_context_state_callback(pa_context* context, void* userdata) +{ + pa_context_state_t state; + AudinPulseDevice* pulse = (AudinPulseDevice*)userdata; + state = pa_context_get_state(context); + + WLog_Print(pulse->log, WLOG_DEBUG, "context state %s", pulse_context_state_string(state)); + switch (state) + { + case PA_CONTEXT_READY: + pa_threaded_mainloop_signal(pulse->mainloop, 0); + break; + + case PA_CONTEXT_FAILED: + case PA_CONTEXT_TERMINATED: + pa_threaded_mainloop_signal(pulse->mainloop, 0); + break; + + default: + break; + } +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_pulse_connect(IAudinDevice* device) +{ + pa_context_state_t state; + AudinPulseDevice* pulse = (AudinPulseDevice*)device; + + if (!pulse->context) + return ERROR_INVALID_PARAMETER; + + if (pa_context_connect(pulse->context, NULL, 0, NULL)) + { + WLog_Print(pulse->log, WLOG_ERROR, "pa_context_connect failed (%d)", + pa_context_errno(pulse->context)); + return ERROR_INTERNAL_ERROR; + } + + pa_threaded_mainloop_lock(pulse->mainloop); + + if (pa_threaded_mainloop_start(pulse->mainloop) < 0) + { + pa_threaded_mainloop_unlock(pulse->mainloop); + WLog_Print(pulse->log, WLOG_ERROR, "pa_threaded_mainloop_start failed (%d)", + pa_context_errno(pulse->context)); + return ERROR_INTERNAL_ERROR; + } + + for (;;) + { + state = pa_context_get_state(pulse->context); + + if (state == PA_CONTEXT_READY) + break; + + if (!PA_CONTEXT_IS_GOOD(state)) + { + WLog_Print(pulse->log, WLOG_ERROR, "bad context state (%s: %d)", + pulse_context_state_string(state), pa_context_errno(pulse->context)); + pa_context_disconnect(pulse->context); + return ERROR_INVALID_STATE; + } + + pa_threaded_mainloop_wait(pulse->mainloop); + } + + pa_threaded_mainloop_unlock(pulse->mainloop); + WLog_Print(pulse->log, WLOG_DEBUG, "connected"); + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_pulse_free(IAudinDevice* device) +{ + AudinPulseDevice* pulse = (AudinPulseDevice*)device; + + if (!pulse) + return ERROR_INVALID_PARAMETER; + + if (pulse->mainloop) + { + pa_threaded_mainloop_stop(pulse->mainloop); + } + + if (pulse->context) + { + pa_context_disconnect(pulse->context); + pa_context_unref(pulse->context); + pulse->context = NULL; + } + + if (pulse->mainloop) + { + pa_threaded_mainloop_free(pulse->mainloop); + pulse->mainloop = NULL; + } + + free(pulse); + return CHANNEL_RC_OK; +} + +static BOOL audin_pulse_format_supported(IAudinDevice* device, const AUDIO_FORMAT* format) +{ + AudinPulseDevice* pulse = (AudinPulseDevice*)device; + + if (!pulse || !format) + return FALSE; + + if (!pulse->context) + return 0; + + switch (format->wFormatTag) + { + case WAVE_FORMAT_PCM: + if (format->cbSize == 0 && (format->nSamplesPerSec <= PA_RATE_MAX) && + (format->wBitsPerSample == 8 || format->wBitsPerSample == 16) && + (format->nChannels >= 1 && format->nChannels <= PA_CHANNELS_MAX)) + { + return TRUE; + } + + break; + + default: + return FALSE; + } + + return FALSE; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_pulse_set_format(IAudinDevice* device, const AUDIO_FORMAT* format, + UINT32 FramesPerPacket) +{ + pa_sample_spec sample_spec = { 0 }; + AudinPulseDevice* pulse = (AudinPulseDevice*)device; + + if (!pulse || !format) + return ERROR_INVALID_PARAMETER; + + if (!pulse->context) + return ERROR_INVALID_PARAMETER; + + if (FramesPerPacket > 0) + pulse->frames_per_packet = FramesPerPacket; + + sample_spec.rate = format->nSamplesPerSec; + sample_spec.channels = format->nChannels; + + switch (format->wFormatTag) + { + case WAVE_FORMAT_PCM: /* PCM */ + switch (format->wBitsPerSample) + { + case 8: + sample_spec.format = PA_SAMPLE_U8; + break; + + case 16: + sample_spec.format = PA_SAMPLE_S16LE; + break; + + default: + return ERROR_INTERNAL_ERROR; + } + + break; + + case WAVE_FORMAT_ALAW: /* A-LAW */ + sample_spec.format = PA_SAMPLE_ALAW; + break; + + case WAVE_FORMAT_MULAW: /* U-LAW */ + sample_spec.format = PA_SAMPLE_ULAW; + break; + + default: + return ERROR_INTERNAL_ERROR; + } + + pulse->sample_spec = sample_spec; + pulse->format = *format; + return CHANNEL_RC_OK; +} + +static void audin_pulse_stream_state_callback(pa_stream* stream, void* userdata) +{ + pa_stream_state_t state; + AudinPulseDevice* pulse = (AudinPulseDevice*)userdata; + state = pa_stream_get_state(stream); + + WLog_Print(pulse->log, WLOG_DEBUG, "stream state %s", pulse_stream_state_string(state)); + switch (state) + { + case PA_STREAM_READY: + pa_threaded_mainloop_signal(pulse->mainloop, 0); + break; + + case PA_STREAM_FAILED: + case PA_STREAM_TERMINATED: + pa_threaded_mainloop_signal(pulse->mainloop, 0); + break; + + default: + break; + } +} + +static void audin_pulse_stream_request_callback(pa_stream* stream, size_t length, void* userdata) +{ + const void* data; + AudinPulseDevice* pulse = (AudinPulseDevice*)userdata; + UINT error = CHANNEL_RC_OK; + pa_stream_peek(stream, &data, &length); + error = + IFCALLRESULT(CHANNEL_RC_OK, pulse->receive, &pulse->format, data, length, pulse->user_data); + pa_stream_drop(stream); + + if (error && pulse->rdpcontext) + setChannelError(pulse->rdpcontext, error, "audin_pulse_thread_func reported an error"); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_pulse_close(IAudinDevice* device) +{ + AudinPulseDevice* pulse = (AudinPulseDevice*)device; + + if (!pulse) + return ERROR_INVALID_PARAMETER; + + if (pulse->stream) + { + pa_threaded_mainloop_lock(pulse->mainloop); + pa_stream_disconnect(pulse->stream); + pa_stream_unref(pulse->stream); + pulse->stream = NULL; + pa_threaded_mainloop_unlock(pulse->mainloop); + } + + pulse->receive = NULL; + pulse->user_data = NULL; + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_pulse_open(IAudinDevice* device, AudinReceive receive, void* user_data) +{ + pa_stream_state_t state; + pa_buffer_attr buffer_attr = { 0 }; + AudinPulseDevice* pulse = (AudinPulseDevice*)device; + + if (!pulse || !receive || !user_data) + return ERROR_INVALID_PARAMETER; + + if (!pulse->context) + return ERROR_INVALID_PARAMETER; + + if (!pulse->sample_spec.rate || pulse->stream) + return ERROR_INVALID_PARAMETER; + + pulse->receive = receive; + pulse->user_data = user_data; + pa_threaded_mainloop_lock(pulse->mainloop); + pulse->stream = pa_stream_new(pulse->context, "freerdp_audin", &pulse->sample_spec, NULL); + + if (!pulse->stream) + { + pa_threaded_mainloop_unlock(pulse->mainloop); + WLog_Print(pulse->log, WLOG_DEBUG, "pa_stream_new failed (%d)", + pa_context_errno(pulse->context)); + return pa_context_errno(pulse->context); + } + + pulse->bytes_per_frame = pa_frame_size(&pulse->sample_spec); + pa_stream_set_state_callback(pulse->stream, audin_pulse_stream_state_callback, pulse); + pa_stream_set_read_callback(pulse->stream, audin_pulse_stream_request_callback, pulse); + buffer_attr.maxlength = (UINT32)-1; + buffer_attr.tlength = (UINT32)-1; + buffer_attr.prebuf = (UINT32)-1; + buffer_attr.minreq = (UINT32)-1; + /* 500ms latency */ + buffer_attr.fragsize = pulse->bytes_per_frame * pulse->frames_per_packet; + + if (buffer_attr.fragsize % pulse->format.nBlockAlign) + buffer_attr.fragsize += + pulse->format.nBlockAlign - buffer_attr.fragsize % pulse->format.nBlockAlign; + + if (pa_stream_connect_record(pulse->stream, pulse->device_name, &buffer_attr, + PA_STREAM_ADJUST_LATENCY) < 0) + { + pa_threaded_mainloop_unlock(pulse->mainloop); + WLog_Print(pulse->log, WLOG_ERROR, "pa_stream_connect_playback failed (%d)", + pa_context_errno(pulse->context)); + return pa_context_errno(pulse->context); + } + + for (;;) + { + state = pa_stream_get_state(pulse->stream); + + if (state == PA_STREAM_READY) + break; + + if (!PA_STREAM_IS_GOOD(state)) + { + audin_pulse_close(device); + WLog_Print(pulse->log, WLOG_ERROR, "bad stream state (%s: %d)", + pulse_stream_state_string(state), pa_context_errno(pulse->context)); + pa_threaded_mainloop_unlock(pulse->mainloop); + return pa_context_errno(pulse->context); + } + + pa_threaded_mainloop_wait(pulse->mainloop); + } + + pa_threaded_mainloop_unlock(pulse->mainloop); + pulse->buffer_frames = 0; + WLog_Print(pulse->log, WLOG_DEBUG, "connected"); + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_pulse_parse_addin_args(AudinPulseDevice* device, ADDIN_ARGV* args) +{ + int status; + DWORD flags; + COMMAND_LINE_ARGUMENT_A* arg; + AudinPulseDevice* pulse = (AudinPulseDevice*)device; + COMMAND_LINE_ARGUMENT_A audin_pulse_args[] = { { "dev", COMMAND_LINE_VALUE_REQUIRED, "", + NULL, NULL, -1, NULL, "audio device name" }, + { NULL, 0, NULL, NULL, NULL, -1, NULL, NULL } }; + + flags = + COMMAND_LINE_SIGIL_NONE | COMMAND_LINE_SEPARATOR_COLON | COMMAND_LINE_IGN_UNKNOWN_KEYWORD; + status = CommandLineParseArgumentsA(args->argc, args->argv, audin_pulse_args, flags, pulse, + NULL, NULL); + + if (status < 0) + return ERROR_INVALID_PARAMETER; + + arg = audin_pulse_args; + + do + { + if (!(arg->Flags & COMMAND_LINE_VALUE_PRESENT)) + continue; + + CommandLineSwitchStart(arg) CommandLineSwitchCase(arg, "dev") + { + pulse->device_name = _strdup(arg->Value); + + if (!pulse->device_name) + { + WLog_Print(pulse->log, WLOG_ERROR, "_strdup failed!"); + return CHANNEL_RC_NO_MEMORY; + } + } + CommandLineSwitchEnd(arg) + } while ((arg = CommandLineFindNextArgumentA(arg)) != NULL); + + return CHANNEL_RC_OK; +} + +#ifdef BUILTIN_CHANNELS +#define freerdp_audin_client_subsystem_entry pulse_freerdp_audin_client_subsystem_entry +#else +#define freerdp_audin_client_subsystem_entry FREERDP_API freerdp_audin_client_subsystem_entry +#endif + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT freerdp_audin_client_subsystem_entry(PFREERDP_AUDIN_DEVICE_ENTRY_POINTS pEntryPoints) +{ + ADDIN_ARGV* args; + AudinPulseDevice* pulse; + UINT error; + pulse = (AudinPulseDevice*)calloc(1, sizeof(AudinPulseDevice)); + + if (!pulse) + { + WLog_ERR(TAG, "calloc failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + pulse->log = WLog_Get(TAG); + pulse->iface.Open = audin_pulse_open; + pulse->iface.FormatSupported = audin_pulse_format_supported; + pulse->iface.SetFormat = audin_pulse_set_format; + pulse->iface.Close = audin_pulse_close; + pulse->iface.Free = audin_pulse_free; + pulse->rdpcontext = pEntryPoints->rdpcontext; + args = pEntryPoints->args; + + if ((error = audin_pulse_parse_addin_args(pulse, args))) + { + WLog_Print(pulse->log, WLOG_ERROR, + "audin_pulse_parse_addin_args failed with error %" PRIu32 "!", error); + goto error_out; + } + + pulse->mainloop = pa_threaded_mainloop_new(); + + if (!pulse->mainloop) + { + WLog_Print(pulse->log, WLOG_ERROR, "pa_threaded_mainloop_new failed"); + error = CHANNEL_RC_NO_MEMORY; + goto error_out; + } + + pulse->context = pa_context_new(pa_threaded_mainloop_get_api(pulse->mainloop), "freerdp"); + + if (!pulse->context) + { + WLog_Print(pulse->log, WLOG_ERROR, "pa_context_new failed"); + error = CHANNEL_RC_NO_MEMORY; + goto error_out; + } + + pa_context_set_state_callback(pulse->context, audin_pulse_context_state_callback, pulse); + + if ((error = audin_pulse_connect(&pulse->iface))) + { + WLog_Print(pulse->log, WLOG_ERROR, "audin_pulse_connect failed"); + goto error_out; + } + + if ((error = pEntryPoints->pRegisterAudinDevice(pEntryPoints->plugin, &pulse->iface))) + { + WLog_Print(pulse->log, WLOG_ERROR, "RegisterAudinDevice failed with error %" PRIu32 "!", + error); + goto error_out; + } + + return CHANNEL_RC_OK; +error_out: + audin_pulse_free(&pulse->iface); + return error; +} diff --git a/channels/audin/client/winmm/CMakeLists.txt b/channels/audin/client/winmm/CMakeLists.txt new file mode 100644 index 0000000..2eddb8e --- /dev/null +++ b/channels/audin/client/winmm/CMakeLists.txt @@ -0,0 +1,38 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2012 Marc-Andre Moreau +# +# Licensed 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. + +define_channel_client_subsystem("audin" "winmm" "") + +set(${MODULE_PREFIX}_SRCS + audin_winmm.c) + +include_directories(..) + +add_channel_client_subsystem_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} "" TRUE "") + + + +set(${MODULE_PREFIX}_LIBS freerdp winpr winmm.lib) + +target_link_libraries(${MODULE_NAME} ${${MODULE_PREFIX}_LIBS}) + + +if (WITH_DEBUG_SYMBOLS AND MSVC AND NOT BUILTIN_CHANNELS AND BUILD_SHARED_LIBS) + install(FILES ${CMAKE_BINARY_DIR}/${MODULE_NAME}.pdb DESTINATION ${FREERDP_ADDIN_PATH} COMPONENT symbols) +endif() + +set_property(TARGET ${MODULE_NAME} PROPERTY FOLDER "Channels/${CHANNEL_NAME}/Client/winmm") diff --git a/channels/audin/client/winmm/audin_winmm.c b/channels/audin/client/winmm/audin_winmm.c new file mode 100644 index 0000000..7538672 --- /dev/null +++ b/channels/audin/client/winmm/audin_winmm.c @@ -0,0 +1,568 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Audio Input Redirection Virtual Channel - WinMM implementation + * + * Copyright 2013 Zhang Zhaolong + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include + +#include +#include + +#include +#include +#include +#include + +#include "audin_main.h" + +typedef struct _AudinWinmmDevice +{ + IAudinDevice iface; + + char* device_name; + AudinReceive receive; + void* user_data; + HANDLE thread; + HANDLE stopEvent; + HWAVEIN hWaveIn; + PWAVEFORMATEX* ppwfx; + PWAVEFORMATEX pwfx_cur; + UINT32 ppwfx_size; + UINT32 cFormats; + UINT32 frames_per_packet; + rdpContext* rdpcontext; + wLog* log; +} AudinWinmmDevice; + +static void CALLBACK waveInProc(HWAVEIN hWaveIn, UINT uMsg, DWORD_PTR dwInstance, + DWORD_PTR dwParam1, DWORD_PTR dwParam2) +{ + AudinWinmmDevice* winmm = (AudinWinmmDevice*)dwInstance; + PWAVEHDR pWaveHdr; + UINT error = CHANNEL_RC_OK; + MMRESULT mmResult; + + switch (uMsg) + { + case WIM_CLOSE: + break; + + case WIM_DATA: + pWaveHdr = (WAVEHDR*)dwParam1; + + if (WHDR_DONE == (WHDR_DONE & pWaveHdr->dwFlags)) + { + if (pWaveHdr->dwBytesRecorded && + !(WaitForSingleObject(winmm->stopEvent, 0) == WAIT_OBJECT_0)) + { + AUDIO_FORMAT format; + format.cbSize = winmm->pwfx_cur->cbSize; + format.nBlockAlign = winmm->pwfx_cur->nBlockAlign; + format.nAvgBytesPerSec = winmm->pwfx_cur->nAvgBytesPerSec; + format.nChannels = winmm->pwfx_cur->nChannels; + format.nSamplesPerSec = winmm->pwfx_cur->nSamplesPerSec; + format.wBitsPerSample = winmm->pwfx_cur->wBitsPerSample; + format.wFormatTag = winmm->pwfx_cur->wFormatTag; + + if ((error = winmm->receive(&format, pWaveHdr->lpData, + pWaveHdr->dwBytesRecorded, winmm->user_data))) + break; + + mmResult = waveInAddBuffer(hWaveIn, pWaveHdr, sizeof(WAVEHDR)); + + if (mmResult != MMSYSERR_NOERROR) + error = ERROR_INTERNAL_ERROR; + } + } + + break; + + case WIM_OPEN: + break; + + default: + break; + } + + if (error && winmm->rdpcontext) + setChannelError(winmm->rdpcontext, error, "waveInProc reported an error"); +} + +static BOOL log_mmresult(AudinWinmmDevice* winmm, const char* what, MMRESULT result) +{ + if (result != MMSYSERR_NOERROR) + { + CHAR buffer[8192] = { 0 }; + CHAR msg[8192] = { 0 }; + CHAR cmsg[8192] = { 0 }; + waveInGetErrorTextA(result, buffer, sizeof(buffer)); + + _snprintf(msg, sizeof(msg) - 1, "%s failed. %" PRIu32 " [%s]", what, result, buffer); + _snprintf(cmsg, sizeof(cmsg) - 1, "audin_winmm_thread_func reported an error '%s'", msg); + WLog_Print(winmm->log, WLOG_DEBUG, "%s", msg); + if (winmm->rdpcontext) + setChannelError(winmm->rdpcontext, ERROR_INTERNAL_ERROR, cmsg); + return FALSE; + } + return TRUE; +} + +static BOOL test_format_supported(const PWAVEFORMATEX pwfx) +{ + MMRESULT rc; + WAVEINCAPSA caps = { 0 }; + + rc = waveInGetDevCapsA(WAVE_MAPPER, &caps, sizeof(caps)); + if (rc != MMSYSERR_NOERROR) + return FALSE; + + switch (pwfx->nChannels) + { + case 1: + if ((caps.dwFormats & + (WAVE_FORMAT_1M08 | WAVE_FORMAT_2M08 | WAVE_FORMAT_4M08 | WAVE_FORMAT_96M08 | + WAVE_FORMAT_1M16 | WAVE_FORMAT_2M16 | WAVE_FORMAT_4M16 | WAVE_FORMAT_96M16)) == 0) + return FALSE; + break; + case 2: + if ((caps.dwFormats & + (WAVE_FORMAT_1S08 | WAVE_FORMAT_2S08 | WAVE_FORMAT_4S08 | WAVE_FORMAT_96S08 | + WAVE_FORMAT_1S16 | WAVE_FORMAT_2S16 | WAVE_FORMAT_4S16 | WAVE_FORMAT_96S16)) == 0) + return FALSE; + break; + default: + return FALSE; + } + + rc = waveInOpen(NULL, WAVE_MAPPER, pwfx, 0, 0, + WAVE_FORMAT_QUERY | WAVE_MAPPED_DEFAULT_COMMUNICATION_DEVICE); + return (rc == MMSYSERR_NOERROR); +} + +static DWORD WINAPI audin_winmm_thread_func(LPVOID arg) +{ + AudinWinmmDevice* winmm = (AudinWinmmDevice*)arg; + char* buffer; + int size, i; + WAVEHDR waveHdr[4] = { 0 }; + DWORD status; + MMRESULT rc; + + if (!winmm->hWaveIn) + { + MMRESULT rc; + rc = waveInOpen(&winmm->hWaveIn, WAVE_MAPPER, winmm->pwfx_cur, (DWORD_PTR)waveInProc, + (DWORD_PTR)winmm, + CALLBACK_FUNCTION | WAVE_MAPPED_DEFAULT_COMMUNICATION_DEVICE); + if (!log_mmresult(winmm, "waveInOpen", rc)) + return ERROR_INTERNAL_ERROR; + } + + size = + (winmm->pwfx_cur->wBitsPerSample * winmm->pwfx_cur->nChannels * winmm->frames_per_packet + + 7) / + 8; + + for (i = 0; i < 4; i++) + { + buffer = (char*)malloc(size); + + if (!buffer) + return CHANNEL_RC_NO_MEMORY; + + waveHdr[i].dwBufferLength = size; + waveHdr[i].dwFlags = 0; + waveHdr[i].lpData = buffer; + rc = waveInPrepareHeader(winmm->hWaveIn, &waveHdr[i], sizeof(waveHdr[i])); + + if (!log_mmresult(winmm, "waveInPrepareHeader", rc)) + { + + } + + rc = waveInAddBuffer(winmm->hWaveIn, &waveHdr[i], sizeof(waveHdr[i])); + + if (!log_mmresult(winmm, "waveInAddBuffer", rc)) + { + } + } + + rc = waveInStart(winmm->hWaveIn); + + if (!log_mmresult(winmm, "waveInStart", rc)) + { + } + + status = WaitForSingleObject(winmm->stopEvent, INFINITE); + + if (status == WAIT_FAILED) + { + WLog_Print(winmm->log, WLOG_DEBUG, "WaitForSingleObject failed."); + + if (winmm->rdpcontext) + setChannelError(winmm->rdpcontext, ERROR_INTERNAL_ERROR, + "audin_winmm_thread_func reported an error"); + } + + rc = waveInReset(winmm->hWaveIn); + + if (!log_mmresult(winmm, "waveInReset", rc)) + { + } + + for (i = 0; i < 4; i++) + { + rc = waveInUnprepareHeader(winmm->hWaveIn, &waveHdr[i], sizeof(waveHdr[i])); + + if (!log_mmresult(winmm, "waveInUnprepareHeader", rc)) + { + + } + + free(waveHdr[i].lpData); + } + + rc = waveInClose(winmm->hWaveIn); + + if (!log_mmresult(winmm, "waveInClose", rc)) + { + } + + winmm->hWaveIn = NULL; + return 0; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_winmm_free(IAudinDevice* device) +{ + UINT32 i; + AudinWinmmDevice* winmm = (AudinWinmmDevice*)device; + + if (!winmm) + return ERROR_INVALID_PARAMETER; + + for (i = 0; i < winmm->cFormats; i++) + { + free(winmm->ppwfx[i]); + } + + free(winmm->ppwfx); + free(winmm->device_name); + free(winmm); + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_winmm_close(IAudinDevice* device) +{ + DWORD status; + UINT error = CHANNEL_RC_OK; + AudinWinmmDevice* winmm = (AudinWinmmDevice*)device; + + if (!winmm) + return ERROR_INVALID_PARAMETER; + + SetEvent(winmm->stopEvent); + status = WaitForSingleObject(winmm->thread, INFINITE); + + if (status == WAIT_FAILED) + { + error = GetLastError(); + WLog_Print(winmm->log, WLOG_ERROR, "WaitForSingleObject failed with error %" PRIu32 "!", + error); + return error; + } + + CloseHandle(winmm->thread); + CloseHandle(winmm->stopEvent); + winmm->thread = NULL; + winmm->stopEvent = NULL; + winmm->receive = NULL; + winmm->user_data = NULL; + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_winmm_set_format(IAudinDevice* device, const AUDIO_FORMAT* format, + UINT32 FramesPerPacket) +{ + UINT32 i; + AudinWinmmDevice* winmm = (AudinWinmmDevice*)device; + + if (!winmm || !format) + return ERROR_INVALID_PARAMETER; + + winmm->frames_per_packet = FramesPerPacket; + + for (i = 0; i < winmm->cFormats; i++) + { + const PWAVEFORMATEX ppwfx = winmm->ppwfx[i]; + if ((ppwfx->wFormatTag == format->wFormatTag) && (ppwfx->nChannels == format->nChannels) && + (ppwfx->wBitsPerSample == format->wBitsPerSample) && + (ppwfx->nSamplesPerSec == format->nSamplesPerSec)) + { + /* BUG: Many devices report to support stereo recording but fail here. + * Ensure we always use mono. */ + if (ppwfx->nChannels > 1) + { + ppwfx->nChannels = 1; + } + + if (ppwfx->nBlockAlign != 2) + { + ppwfx->nBlockAlign = 2; + ppwfx->nAvgBytesPerSec = ppwfx->nSamplesPerSec * ppwfx->nBlockAlign; + } + + if (!test_format_supported(ppwfx)) + return ERROR_INVALID_PARAMETER; + winmm->pwfx_cur = ppwfx; + return CHANNEL_RC_OK; + } + } + + return ERROR_INVALID_PARAMETER; +} + +static BOOL audin_winmm_format_supported(IAudinDevice* device, const AUDIO_FORMAT* format) +{ + AudinWinmmDevice* winmm = (AudinWinmmDevice*)device; + PWAVEFORMATEX pwfx; + BYTE* data; + + if (!winmm || !format) + return FALSE; + + if (format->wFormatTag != WAVE_FORMAT_PCM) + return FALSE; + + pwfx = (PWAVEFORMATEX)malloc(sizeof(WAVEFORMATEX) + format->cbSize); + + if (!pwfx) + return FALSE; + + pwfx->cbSize = format->cbSize; + pwfx->wFormatTag = format->wFormatTag; + pwfx->nChannels = format->nChannels; + pwfx->nSamplesPerSec = format->nSamplesPerSec; + pwfx->nBlockAlign = format->nBlockAlign; + pwfx->wBitsPerSample = format->wBitsPerSample; + data = (BYTE*)pwfx + sizeof(WAVEFORMATEX); + memcpy(data, format->data, format->cbSize); + + pwfx->nAvgBytesPerSec = pwfx->nSamplesPerSec * pwfx->nBlockAlign; + + if (!test_format_supported(pwfx)) + goto fail; + + if (winmm->cFormats >= winmm->ppwfx_size) + { + PWAVEFORMATEX* tmp_ppwfx; + tmp_ppwfx = realloc(winmm->ppwfx, sizeof(PWAVEFORMATEX) * winmm->ppwfx_size * 2); + + if (!tmp_ppwfx) + goto fail; + + winmm->ppwfx_size *= 2; + winmm->ppwfx = tmp_ppwfx; + } + + winmm->ppwfx[winmm->cFormats++] = pwfx; + return TRUE; + +fail: + free(pwfx); + return FALSE; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_winmm_open(IAudinDevice* device, AudinReceive receive, void* user_data) +{ + AudinWinmmDevice* winmm = (AudinWinmmDevice*)device; + + if (!winmm || !receive || !user_data) + return ERROR_INVALID_PARAMETER; + + winmm->receive = receive; + winmm->user_data = user_data; + + if (!(winmm->stopEvent = CreateEvent(NULL, TRUE, FALSE, NULL))) + { + WLog_Print(winmm->log, WLOG_ERROR, "CreateEvent failed!"); + return ERROR_INTERNAL_ERROR; + } + + if (!(winmm->thread = CreateThread(NULL, 0, audin_winmm_thread_func, winmm, 0, NULL))) + { + WLog_Print(winmm->log, WLOG_ERROR, "CreateThread failed!"); + CloseHandle(winmm->stopEvent); + winmm->stopEvent = NULL; + return ERROR_INTERNAL_ERROR; + } + + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_winmm_parse_addin_args(AudinWinmmDevice* device, ADDIN_ARGV* args) +{ + int status; + DWORD flags; + COMMAND_LINE_ARGUMENT_A* arg; + AudinWinmmDevice* winmm = (AudinWinmmDevice*)device; + COMMAND_LINE_ARGUMENT_A audin_winmm_args[] = { { "dev", COMMAND_LINE_VALUE_REQUIRED, "", + NULL, NULL, -1, NULL, "audio device name" }, + { NULL, 0, NULL, NULL, NULL, -1, NULL, NULL } }; + + flags = + COMMAND_LINE_SIGIL_NONE | COMMAND_LINE_SEPARATOR_COLON | COMMAND_LINE_IGN_UNKNOWN_KEYWORD; + status = CommandLineParseArgumentsA(args->argc, args->argv, audin_winmm_args, flags, winmm, + NULL, NULL); + arg = audin_winmm_args; + + do + { + if (!(arg->Flags & COMMAND_LINE_VALUE_PRESENT)) + continue; + + CommandLineSwitchStart(arg) CommandLineSwitchCase(arg, "dev") + { + winmm->device_name = _strdup(arg->Value); + + if (!winmm->device_name) + { + WLog_Print(winmm->log, WLOG_ERROR, "_strdup failed!"); + return CHANNEL_RC_NO_MEMORY; + } + } + CommandLineSwitchEnd(arg) + } while ((arg = CommandLineFindNextArgumentA(arg)) != NULL); + + return CHANNEL_RC_OK; +} + +#ifdef BUILTIN_CHANNELS +#define freerdp_audin_client_subsystem_entry winmm_freerdp_audin_client_subsystem_entry +#else +#define freerdp_audin_client_subsystem_entry FREERDP_API freerdp_audin_client_subsystem_entry +#endif + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT freerdp_audin_client_subsystem_entry(PFREERDP_AUDIN_DEVICE_ENTRY_POINTS pEntryPoints) +{ + ADDIN_ARGV* args; + AudinWinmmDevice* winmm; + UINT error; + + if (waveInGetNumDevs() == 0) + { + WLog_Print(WLog_Get(TAG), WLOG_ERROR, "No microphone available!"); + return ERROR_DEVICE_NOT_AVAILABLE; + } + + winmm = (AudinWinmmDevice*)calloc(1, sizeof(AudinWinmmDevice)); + + if (!winmm) + { + WLog_ERR(TAG, "calloc failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + winmm->log = WLog_Get(TAG); + winmm->iface.Open = audin_winmm_open; + winmm->iface.FormatSupported = audin_winmm_format_supported; + winmm->iface.SetFormat = audin_winmm_set_format; + winmm->iface.Close = audin_winmm_close; + winmm->iface.Free = audin_winmm_free; + winmm->rdpcontext = pEntryPoints->rdpcontext; + args = pEntryPoints->args; + + if ((error = audin_winmm_parse_addin_args(winmm, args))) + { + WLog_Print(winmm->log, WLOG_ERROR, + "audin_winmm_parse_addin_args failed with error %" PRIu32 "!", error); + goto error_out; + } + + if (!winmm->device_name) + { + winmm->device_name = _strdup("default"); + + if (!winmm->device_name) + { + WLog_Print(winmm->log, WLOG_ERROR, "_strdup failed!"); + error = CHANNEL_RC_NO_MEMORY; + goto error_out; + } + } + + winmm->ppwfx_size = 10; + winmm->ppwfx = calloc(winmm->ppwfx_size, sizeof(PWAVEFORMATEX)); + + if (!winmm->ppwfx) + { + WLog_Print(winmm->log, WLOG_ERROR, "malloc failed!"); + error = CHANNEL_RC_NO_MEMORY; + goto error_out; + } + + if ((error = pEntryPoints->pRegisterAudinDevice(pEntryPoints->plugin, &winmm->iface))) + { + WLog_Print(winmm->log, WLOG_ERROR, "RegisterAudinDevice failed with error %" PRIu32 "!", + error); + goto error_out; + } + + return CHANNEL_RC_OK; +error_out: + free(winmm->ppwfx); + free(winmm->device_name); + free(winmm); + return error; +} diff --git a/channels/audin/server/CMakeLists.txt b/channels/audin/server/CMakeLists.txt new file mode 100644 index 0000000..4d4004f --- /dev/null +++ b/channels/audin/server/CMakeLists.txt @@ -0,0 +1,29 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2012 Marc-Andre Moreau +# +# Licensed 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. + +define_channel_server("audin") + +set(${MODULE_PREFIX}_SRCS + audin.c) + +add_channel_server_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} FALSE "DVCPluginEntry") + + + +target_link_libraries(${MODULE_NAME} freerdp) + +set_property(TARGET ${MODULE_NAME} PROPERTY FOLDER "Channels/${CHANNEL_NAME}/Server") diff --git a/channels/audin/server/audin.c b/channels/audin/server/audin.c new file mode 100644 index 0000000..8252236 --- /dev/null +++ b/channels/audin/server/audin.c @@ -0,0 +1,692 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Server Audio Input Virtual Channel + * + * Copyright 2012 Vic Lee + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#define TAG CHANNELS_TAG("audin.server") +#define MSG_SNDIN_VERSION 0x01 +#define MSG_SNDIN_FORMATS 0x02 +#define MSG_SNDIN_OPEN 0x03 +#define MSG_SNDIN_OPEN_REPLY 0x04 +#define MSG_SNDIN_DATA_INCOMING 0x05 +#define MSG_SNDIN_DATA 0x06 +#define MSG_SNDIN_FORMATCHANGE 0x07 + +typedef struct _audin_server +{ + audin_server_context context; + + BOOL opened; + + HANDLE stopEvent; + + HANDLE thread; + void* audin_channel; + + DWORD SessionId; + + FREERDP_DSP_CONTEXT* dsp_context; + +} audin_server; + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_server_select_format(audin_server_context* context, size_t client_format_index) +{ + audin_server* audin = (audin_server*)context; + + if (client_format_index >= context->num_client_formats) + { + WLog_ERR(TAG, "error in protocol: client_format_index >= context->num_client_formats!"); + return ERROR_INVALID_DATA; + } + + context->selected_client_format = (SSIZE_T)client_format_index; + + if (!freerdp_dsp_context_reset(audin->dsp_context, + &audin->context.client_formats[client_format_index])) + { + WLog_ERR(TAG, "Failed to reset dsp context format!"); + return ERROR_INTERNAL_ERROR; + } + + if (audin->opened) + { + /* TODO: send MSG_SNDIN_FORMATCHANGE */ + } + + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_server_send_version(audin_server* audin, wStream* s) +{ + ULONG written; + Stream_Write_UINT8(s, MSG_SNDIN_VERSION); + Stream_Write_UINT32(s, 1); /* Version (4 bytes) */ + + if (!WTSVirtualChannelWrite(audin->audin_channel, (PCHAR)Stream_Buffer(s), + Stream_GetPosition(s), &written)) + { + WLog_ERR(TAG, "WTSVirtualChannelWrite failed!"); + return ERROR_INTERNAL_ERROR; + } + + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_server_recv_version(audin_server* audin, wStream* s, UINT32 length) +{ + UINT32 Version; + + if (length < 4) + { + WLog_ERR(TAG, "error parsing version info: expected at least 4 bytes, got %" PRIu32 "", + length); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT32(s, Version); + + if (Version < 1) + { + WLog_ERR(TAG, "expected Version > 0 but got %" PRIu32 "", Version); + return ERROR_INVALID_DATA; + } + + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_server_send_formats(audin_server* audin, wStream* s) +{ + size_t i; + ULONG written; + Stream_SetPosition(s, 0); + Stream_Write_UINT8(s, MSG_SNDIN_FORMATS); + Stream_Write_UINT32(s, audin->context.num_server_formats); /* NumFormats (4 bytes) */ + Stream_Write_UINT32(s, 0); /* cbSizeFormatsPacket (4 bytes), client-to-server only */ + + for (i = 0; i < audin->context.num_server_formats; i++) + { + AUDIO_FORMAT format = audin->context.server_formats[i]; + + if (!audio_format_write(s, &format)) + { + WLog_ERR(TAG, "Stream_EnsureRemainingCapacity failed!"); + return CHANNEL_RC_NO_MEMORY; + } + } + + return WTSVirtualChannelWrite(audin->audin_channel, (PCHAR)Stream_Buffer(s), + Stream_GetPosition(s), &written) + ? CHANNEL_RC_OK + : ERROR_INTERNAL_ERROR; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_server_recv_formats(audin_server* audin, wStream* s, UINT32 length) +{ + size_t i; + UINT success = CHANNEL_RC_OK; + + if (length < 8) + { + WLog_ERR(TAG, "error parsing rec formats: expected at least 8 bytes, got %" PRIu32 "", + length); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT32(s, audin->context.num_client_formats); /* NumFormats (4 bytes) */ + Stream_Seek_UINT32(s); /* cbSizeFormatsPacket (4 bytes) */ + length -= 8; + + if (audin->context.num_client_formats <= 0) + { + WLog_ERR(TAG, "num_client_formats expected > 0 but got %d", + audin->context.num_client_formats); + return ERROR_INVALID_DATA; + } + + audin->context.client_formats = audio_formats_new(audin->context.num_client_formats); + + if (!audin->context.client_formats) + return ERROR_NOT_ENOUGH_MEMORY; + + for (i = 0; i < audin->context.num_client_formats; i++) + { + AUDIO_FORMAT* format = &audin->context.client_formats[i]; + + if (!audio_format_read(s, format)) + { + audio_formats_free(audin->context.client_formats, i); + audin->context.client_formats = NULL; + WLog_ERR(TAG, "expected length at least 18, but got %" PRIu32 "", length); + return ERROR_INVALID_DATA; + } + + audio_format_print(WLog_Get(TAG), WLOG_DEBUG, format); + } + + IFCALLRET(audin->context.Opening, success, &audin->context); + + if (success) + WLog_ERR(TAG, "context.Opening failed with error %" PRIu32 "", success); + + return success; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_server_send_open(audin_server* audin, wStream* s) +{ + ULONG written; + + if (audin->context.selected_client_format < 0) + { + WLog_ERR(TAG, "audin->context.selected_client_format = %d", + audin->context.selected_client_format); + return ERROR_INVALID_DATA; + } + + audin->opened = TRUE; + Stream_SetPosition(s, 0); + Stream_Write_UINT8(s, MSG_SNDIN_OPEN); + Stream_Write_UINT32(s, audin->context.frames_per_packet); /* FramesPerPacket (4 bytes) */ + Stream_Write_UINT32(s, audin->context.selected_client_format); /* initialFormat (4 bytes) */ + /* + * [MS-RDPEAI] 3.2.5.1.6 + * The second format specify the format that SHOULD be used to capture data from + * the actual audio input device. + */ + Stream_Write_UINT16(s, 1); /* wFormatTag = PCM */ + Stream_Write_UINT16(s, 2); /* nChannels */ + Stream_Write_UINT32(s, 44100); /* nSamplesPerSec */ + Stream_Write_UINT32(s, 44100 * 2 * 2); /* nAvgBytesPerSec */ + Stream_Write_UINT16(s, 4); /* nBlockAlign */ + Stream_Write_UINT16(s, 16); /* wBitsPerSample */ + Stream_Write_UINT16(s, 0); /* cbSize */ + return WTSVirtualChannelWrite(audin->audin_channel, (PCHAR)Stream_Buffer(s), + Stream_GetPosition(s), &written) + ? CHANNEL_RC_OK + : ERROR_INTERNAL_ERROR; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_server_recv_open_reply(audin_server* audin, wStream* s, UINT32 length) +{ + UINT32 Result; + UINT success = CHANNEL_RC_OK; + + if (length < 4) + { + WLog_ERR(TAG, "error parsing version info: expected at least 4 bytes, got %" PRIu32 "", + length); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT32(s, Result); + IFCALLRET(audin->context.OpenResult, success, &audin->context, Result); + + if (success) + WLog_ERR(TAG, "context.OpenResult failed with error %" PRIu32 "", success); + + return success; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT audin_server_recv_data(audin_server* audin, wStream* s, UINT32 length) +{ + AUDIO_FORMAT* format; + int sbytes_per_sample; + int sbytes_per_frame; + int frames; + wStream* out; + UINT success = ERROR_INTERNAL_ERROR; + + if (audin->context.selected_client_format < 0) + { + WLog_ERR(TAG, "audin->context.selected_client_format = %d", + audin->context.selected_client_format); + return ERROR_INVALID_DATA; + } + + out = Stream_New(NULL, 4096); + + if (!out) + return ERROR_OUTOFMEMORY; + + format = &audin->context.client_formats[audin->context.selected_client_format]; + + if (freerdp_dsp_decode(audin->dsp_context, format, Stream_Pointer(s), length, out)) + { + AUDIO_FORMAT dformat = *format; + dformat.wFormatTag = WAVE_FORMAT_PCM; + dformat.wBitsPerSample = 16; + Stream_SealLength(out); + Stream_SetPosition(out, 0); + sbytes_per_sample = format->wBitsPerSample / 8; + sbytes_per_frame = format->nChannels * sbytes_per_sample; + frames = Stream_Length(out) / sbytes_per_frame; + IFCALLRET(audin->context.ReceiveSamples, success, &audin->context, &dformat, out, frames); + + if (success) + WLog_ERR(TAG, "context.ReceiveSamples failed with error %" PRIu32 "", success); + } + else + WLog_ERR(TAG, "freerdp_dsp_decode failed!"); + + Stream_Free(out, TRUE); + return success; +} + +static DWORD WINAPI audin_server_thread_func(LPVOID arg) +{ + wStream* s; + void* buffer; + DWORD nCount; + BYTE MessageId; + HANDLE events[8]; + BOOL ready = FALSE; + HANDLE ChannelEvent; + DWORD BytesReturned = 0; + audin_server* audin = (audin_server*)arg; + UINT error = CHANNEL_RC_OK; + DWORD status; + buffer = NULL; + BytesReturned = 0; + ChannelEvent = NULL; + + if (WTSVirtualChannelQuery(audin->audin_channel, WTSVirtualEventHandle, &buffer, + &BytesReturned) == TRUE) + { + if (BytesReturned == sizeof(HANDLE)) + CopyMemory(&ChannelEvent, buffer, sizeof(HANDLE)); + + WTSFreeMemory(buffer); + } + else + { + WLog_ERR(TAG, "WTSVirtualChannelQuery failed"); + error = ERROR_INTERNAL_ERROR; + goto out; + } + + nCount = 0; + events[nCount++] = audin->stopEvent; + events[nCount++] = ChannelEvent; + + /* Wait for the client to confirm that the Audio Input dynamic channel is ready */ + + while (1) + { + if ((status = WaitForMultipleObjects(nCount, events, FALSE, 100)) == WAIT_OBJECT_0) + goto out; + + if (status == WAIT_FAILED) + { + error = GetLastError(); + WLog_ERR(TAG, "WaitForMultipleObjects failed with error %" PRIu32 "", error); + goto out; + } + + if (WTSVirtualChannelQuery(audin->audin_channel, WTSVirtualChannelReady, &buffer, + &BytesReturned) == FALSE) + { + WLog_ERR(TAG, "WTSVirtualChannelQuery failed"); + error = ERROR_INTERNAL_ERROR; + goto out; + } + + ready = *((BOOL*)buffer); + WTSFreeMemory(buffer); + + if (ready) + break; + } + + s = Stream_New(NULL, 4096); + + if (!s) + { + WLog_ERR(TAG, "Stream_New failed!"); + error = CHANNEL_RC_NO_MEMORY; + goto out; + } + + if (ready) + { + if ((error = audin_server_send_version(audin, s))) + { + WLog_ERR(TAG, "audin_server_send_version failed with error %" PRIu32 "!", error); + goto out_capacity; + } + } + + while (ready) + { + if ((status = WaitForMultipleObjects(nCount, events, FALSE, INFINITE)) == WAIT_OBJECT_0) + break; + + if (status == WAIT_FAILED) + { + error = GetLastError(); + WLog_ERR(TAG, "WaitForMultipleObjects failed with error %" PRIu32 "", error); + goto out; + } + + Stream_SetPosition(s, 0); + + if (!WTSVirtualChannelRead(audin->audin_channel, 0, NULL, 0, &BytesReturned)) + { + WLog_ERR(TAG, "WTSVirtualChannelRead failed!"); + error = ERROR_INTERNAL_ERROR; + break; + } + + if (BytesReturned < 1) + continue; + + if (!Stream_EnsureRemainingCapacity(s, BytesReturned)) + break; + + if (WTSVirtualChannelRead(audin->audin_channel, 0, (PCHAR)Stream_Buffer(s), + Stream_Capacity(s), &BytesReturned) == FALSE) + { + WLog_ERR(TAG, "WTSVirtualChannelRead failed!"); + error = ERROR_INTERNAL_ERROR; + break; + } + + Stream_Read_UINT8(s, MessageId); + BytesReturned--; + + switch (MessageId) + { + case MSG_SNDIN_VERSION: + if ((error = audin_server_recv_version(audin, s, BytesReturned))) + { + WLog_ERR(TAG, "audin_server_recv_version failed with error %" PRIu32 "!", + error); + goto out_capacity; + } + + if ((error = audin_server_send_formats(audin, s))) + { + WLog_ERR(TAG, "audin_server_send_formats failed with error %" PRIu32 "!", + error); + goto out_capacity; + } + + break; + + case MSG_SNDIN_FORMATS: + if ((error = audin_server_recv_formats(audin, s, BytesReturned))) + { + WLog_ERR(TAG, "audin_server_recv_formats failed with error %" PRIu32 "!", + error); + goto out_capacity; + } + + if ((error = audin_server_send_open(audin, s))) + { + WLog_ERR(TAG, "audin_server_send_open failed with error %" PRIu32 "!", error); + goto out_capacity; + } + + break; + + case MSG_SNDIN_OPEN_REPLY: + if ((error = audin_server_recv_open_reply(audin, s, BytesReturned))) + { + WLog_ERR(TAG, "audin_server_recv_open_reply failed with error %" PRIu32 "!", + error); + goto out_capacity; + } + + break; + + case MSG_SNDIN_DATA_INCOMING: + break; + + case MSG_SNDIN_DATA: + if ((error = audin_server_recv_data(audin, s, BytesReturned))) + { + WLog_ERR(TAG, "audin_server_recv_data failed with error %" PRIu32 "!", error); + goto out_capacity; + }; + + break; + + case MSG_SNDIN_FORMATCHANGE: + break; + + default: + WLog_ERR(TAG, "audin_server_thread_func: unknown MessageId %" PRIu8 "", MessageId); + break; + } + } + +out_capacity: + Stream_Free(s, TRUE); +out: + WTSVirtualChannelClose(audin->audin_channel); + audin->audin_channel = NULL; + + if (error && audin->context.rdpcontext) + setChannelError(audin->context.rdpcontext, error, + "audin_server_thread_func reported an error"); + + ExitThread(error); + return error; +} + +static BOOL audin_server_open(audin_server_context* context) +{ + audin_server* audin = (audin_server*)context; + + if (!audin->thread) + { + PULONG pSessionId = NULL; + DWORD BytesReturned = 0; + audin->SessionId = WTS_CURRENT_SESSION; + UINT32 channelId; + BOOL status = TRUE; + + if (WTSQuerySessionInformationA(context->vcm, WTS_CURRENT_SESSION, WTSSessionId, + (LPSTR*)&pSessionId, &BytesReturned)) + { + audin->SessionId = (DWORD)*pSessionId; + WTSFreeMemory(pSessionId); + } + + audin->audin_channel = WTSVirtualChannelOpenEx(audin->SessionId, AUDIN_DVC_CHANNEL_NAME, + WTS_CHANNEL_OPTION_DYNAMIC); + + if (!audin->audin_channel) + { + WLog_ERR(TAG, "WTSVirtualChannelOpenEx failed!"); + return FALSE; + } + + channelId = WTSChannelGetIdByHandle(audin->audin_channel); + + IFCALLRET(context->ChannelIdAssigned, status, context, channelId); + if (!status) + { + WLog_ERR(TAG, "context->ChannelIdAssigned failed!"); + return ERROR_INTERNAL_ERROR; + } + + if (!(audin->stopEvent = CreateEvent(NULL, TRUE, FALSE, NULL))) + { + WLog_ERR(TAG, "CreateEvent failed!"); + return FALSE; + } + + if (!(audin->thread = + CreateThread(NULL, 0, audin_server_thread_func, (void*)audin, 0, NULL))) + { + WLog_ERR(TAG, "CreateThread failed!"); + CloseHandle(audin->stopEvent); + audin->stopEvent = NULL; + return FALSE; + } + + return TRUE; + } + + WLog_ERR(TAG, "thread already running!"); + return FALSE; +} + +static BOOL audin_server_is_open(audin_server_context* context) +{ + audin_server* audin = (audin_server*)context; + + if (!audin) + return FALSE; + + return audin->thread != NULL; +} + +static BOOL audin_server_close(audin_server_context* context) +{ + audin_server* audin = (audin_server*)context; + + if (audin->thread) + { + SetEvent(audin->stopEvent); + + if (WaitForSingleObject(audin->thread, INFINITE) == WAIT_FAILED) + { + WLog_ERR(TAG, "WaitForSingleObject failed with error %" PRIu32 "", GetLastError()); + return FALSE; + } + + CloseHandle(audin->thread); + CloseHandle(audin->stopEvent); + audin->thread = NULL; + audin->stopEvent = NULL; + } + + if (audin->audin_channel) + { + WTSVirtualChannelClose(audin->audin_channel); + audin->audin_channel = NULL; + } + + audin->context.selected_client_format = -1; + return TRUE; +} + +audin_server_context* audin_server_context_new(HANDLE vcm) +{ + audin_server* audin; + audin = (audin_server*)calloc(1, sizeof(audin_server)); + + if (!audin) + { + WLog_ERR(TAG, "calloc failed!"); + return NULL; + } + + audin->context.vcm = vcm; + audin->context.selected_client_format = -1; + audin->context.frames_per_packet = 4096; + audin->context.SelectFormat = audin_server_select_format; + audin->context.Open = audin_server_open; + audin->context.IsOpen = audin_server_is_open; + audin->context.Close = audin_server_close; + audin->dsp_context = freerdp_dsp_context_new(FALSE); + + if (!audin->dsp_context) + { + WLog_ERR(TAG, "freerdp_dsp_context_new failed!"); + free(audin); + return NULL; + } + + return (audin_server_context*)audin; +} + +void audin_server_context_free(audin_server_context* context) +{ + audin_server* audin = (audin_server*)context; + + if (!audin) + return; + + audin_server_close(context); + freerdp_dsp_context_free(audin->dsp_context); + audio_formats_free(audin->context.client_formats, audin->context.num_client_formats); + audio_formats_free(audin->context.server_formats, audin->context.num_server_formats); + free(audin); +} diff --git a/channels/client/.gitignore b/channels/client/.gitignore new file mode 100644 index 0000000..aa03c94 --- /dev/null +++ b/channels/client/.gitignore @@ -0,0 +1,2 @@ +tables.c + diff --git a/channels/client/CMakeLists.txt b/channels/client/CMakeLists.txt new file mode 100644 index 0000000..eb0c80f --- /dev/null +++ b/channels/client/CMakeLists.txt @@ -0,0 +1,117 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2012 Marc-Andre Moreau +# +# Licensed 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. + +set(MODULE_NAME "freerdp-channels-client") +set(MODULE_PREFIX "FREERDP_CHANNELS_CLIENT") + +set(${MODULE_PREFIX}_SRCS + ${CMAKE_CURRENT_BINARY_DIR}/tables.c + ${CMAKE_CURRENT_SOURCE_DIR}/tables.h + ${CMAKE_CURRENT_SOURCE_DIR}/addin.c + ${CMAKE_CURRENT_SOURCE_DIR}/addin.h) + +if(CHANNEL_STATIC_CLIENT_ENTRIES) + list(REMOVE_DUPLICATES CHANNEL_STATIC_CLIENT_ENTRIES) +endif() + +set(CLIENT_STATIC_TYPEDEFS "typedef UINT (*static_entry_fkt)();\n") +set(CLIENT_STATIC_TYPEDEFS "${CLIENT_STATIC_TYPEDEFS}typedef UINT (*static_addin_fkt)();\n") + +foreach(STATIC_ENTRY ${CHANNEL_STATIC_CLIENT_ENTRIES}) + foreach(STATIC_MODULE ${CHANNEL_STATIC_CLIENT_MODULES}) + foreach(ENTRY ${${STATIC_MODULE}_CLIENT_ENTRY}) + if(${ENTRY} STREQUAL ${STATIC_ENTRY}) + set(STATIC_MODULE_NAME ${${STATIC_MODULE}_CLIENT_NAME}) + set(STATIC_MODULE_CHANNEL ${${STATIC_MODULE}_CLIENT_CHANNEL}) + set(${MODULE_PREFIX}_LIBS ${${MODULE_PREFIX}_LIBS} ${STATIC_MODULE_NAME}) + + set(ENTRY_POINT_NAME "${STATIC_MODULE_CHANNEL}_${ENTRY}") + if(${ENTRY} STREQUAL "VirtualChannelEntry") + set(ENTRY_POINT_IMPORT "extern BOOL VCAPITYPE ${ENTRY_POINT_NAME}(PCHANNEL_ENTRY_POINTS);") + elseif(${ENTRY} STREQUAL "VirtualChannelEntryEx") + set(ENTRY_POINT_IMPORT "extern BOOL VCAPITYPE ${ENTRY_POINT_NAME}(PCHANNEL_ENTRY_POINTS,PVOID);") + elseif(${ENTRY} MATCHES "DVCPluginEntry$") + set(ENTRY_POINT_IMPORT "extern UINT ${ENTRY_POINT_NAME}(IDRDYNVC_ENTRY_POINTS* pEntryPoints);") + elseif(${ENTRY} MATCHES "DeviceServiceEntry$") + set(ENTRY_POINT_IMPORT "extern UINT ${ENTRY_POINT_NAME}(PDEVICE_SERVICE_ENTRY_POINTS pEntryPoints);") + else() + set(ENTRY_POINT_IMPORT "extern UINT ${ENTRY_POINT_NAME}(void);") + endif() + set(${STATIC_ENTRY}_IMPORTS "${${STATIC_ENTRY}_IMPORTS}\n${ENTRY_POINT_IMPORT}") + set(${STATIC_ENTRY}_TABLE "${${STATIC_ENTRY}_TABLE}\n\t{ \"${STATIC_MODULE_CHANNEL}\", (static_entry_fkt)${ENTRY_POINT_NAME} },") + endif() + endforeach() + endforeach() +endforeach() + +set(CLIENT_STATIC_ENTRY_TABLES_LIST "${CLIENT_STATIC_ENTRY_TABLES_LIST}\nconst STATIC_ENTRY_TABLE CLIENT_STATIC_ENTRY_TABLES[] =\n{") + +foreach(STATIC_ENTRY ${CHANNEL_STATIC_CLIENT_ENTRIES}) + set(CLIENT_STATIC_ENTRY_IMPORTS "${CLIENT_STATIC_ENTRY_IMPORTS}\n${${STATIC_ENTRY}_IMPORTS}") + set(CLIENT_STATIC_ENTRY_TABLES "${CLIENT_STATIC_ENTRY_TABLES}\nconst STATIC_ENTRY CLIENT_${STATIC_ENTRY}_TABLE[] =\n{") + set(CLIENT_STATIC_ENTRY_TABLES "${CLIENT_STATIC_ENTRY_TABLES}\n${${STATIC_ENTRY}_TABLE}") + set(CLIENT_STATIC_ENTRY_TABLES "${CLIENT_STATIC_ENTRY_TABLES}\n\t{ NULL, NULL }\n};") + set(CLIENT_STATIC_ENTRY_TABLES_LIST "${CLIENT_STATIC_ENTRY_TABLES_LIST}\n\t{ \"${STATIC_ENTRY}\", CLIENT_${STATIC_ENTRY}_TABLE },") +endforeach() + +set(CLIENT_STATIC_ENTRY_TABLES_LIST "${CLIENT_STATIC_ENTRY_TABLES_LIST}\n\t{ NULL, NULL }\n};") + +set(CLIENT_STATIC_ADDIN_TABLE "const STATIC_ADDIN_TABLE CLIENT_STATIC_ADDIN_TABLE[] =\n{") +foreach(STATIC_MODULE ${CHANNEL_STATIC_CLIENT_MODULES}) + set(STATIC_MODULE_NAME ${${STATIC_MODULE}_CLIENT_NAME}) + set(STATIC_MODULE_CHANNEL ${${STATIC_MODULE}_CLIENT_CHANNEL}) + string(TOUPPER "CLIENT_${STATIC_MODULE_CHANNEL}_SUBSYSTEM_TABLE" SUBSYSTEM_TABLE_NAME) + set(SUBSYSTEM_TABLE "const STATIC_SUBSYSTEM_ENTRY ${SUBSYSTEM_TABLE_NAME}[] =\n{") + get_target_property(CHANNEL_SUBSYSTEMS ${STATIC_MODULE_NAME} SUBSYSTEMS) + if(CHANNEL_SUBSYSTEMS MATCHES "NOTFOUND") + set(CHANNEL_SUBSYSTEMS "") + endif() + foreach(STATIC_SUBSYSTEM ${CHANNEL_SUBSYSTEMS}) + if(${STATIC_SUBSYSTEM} MATCHES "^([^-]*)-(.*)") + string(REGEX REPLACE "^([^-]*)-(.*)" "\\1" STATIC_SUBSYSTEM_NAME ${STATIC_SUBSYSTEM}) + string(REGEX REPLACE "^([^-]*)-(.*)" "\\2" STATIC_SUBSYSTEM_TYPE ${STATIC_SUBSYSTEM}) + else() + set(STATIC_SUBSYSTEM_NAME "${STATIC_SUBSYSTEM}") + set(STATIC_SUBSYSTEM_TYPE "") + endif() + string(LENGTH "${STATIC_SUBSYSTEM_TYPE}" _type_length) + set(SUBSYSTEM_MODULE_NAME "${STATIC_MODULE_NAME}-${STATIC_SUBSYSTEM}") + set(${MODULE_PREFIX}_LIBS ${${MODULE_PREFIX}_LIBS} ${SUBSYSTEM_MODULE_NAME}) + if(_type_length GREATER 0) + set(STATIC_SUBSYSTEM_ENTRY "${STATIC_SUBSYSTEM_NAME}_freerdp_${STATIC_MODULE_CHANNEL}_client_${STATIC_SUBSYSTEM_TYPE}_subsystem_entry") + else() + set(STATIC_SUBSYSTEM_ENTRY "${STATIC_SUBSYSTEM_NAME}_freerdp_${STATIC_MODULE_CHANNEL}_client_subsystem_entry") + endif() + set(SUBSYSTEM_TABLE "${SUBSYSTEM_TABLE}\n\t{ \"${STATIC_SUBSYSTEM_NAME}\", \"${STATIC_SUBSYSTEM_TYPE}\", ${STATIC_SUBSYSTEM_ENTRY} },") + set(SUBSYSTEM_IMPORT "extern UINT ${STATIC_SUBSYSTEM_ENTRY}(void*);") + set(CLIENT_STATIC_SUBSYSTEM_IMPORTS "${CLIENT_STATIC_SUBSYSTEM_IMPORTS}\n${SUBSYSTEM_IMPORT}") + endforeach() + set(SUBSYSTEM_TABLE "${SUBSYSTEM_TABLE}\n\t{ NULL, NULL, NULL }\n};") + set(CLIENT_STATIC_SUBSYSTEM_TABLES "${CLIENT_STATIC_SUBSYSTEM_TABLES}\n${SUBSYSTEM_TABLE}") + foreach(ENTRY ${${STATIC_MODULE}_CLIENT_ENTRY}) + set (ENTRY_POINT_NAME ${STATIC_MODULE_CHANNEL}_${ENTRY}) + set(CLIENT_STATIC_ADDIN_TABLE "${CLIENT_STATIC_ADDIN_TABLE}\n\t{ \"${STATIC_MODULE_CHANNEL}\", \"${ENTRY}\", (static_addin_fkt)${ENTRY_POINT_NAME}, ${SUBSYSTEM_TABLE_NAME} },") + endforeach() +endforeach() +set(CLIENT_STATIC_ADDIN_TABLE "${CLIENT_STATIC_ADDIN_TABLE}\n\t{ NULL, NULL, NULL, NULL }\n};") + +configure_file(${CMAKE_CURRENT_SOURCE_DIR}/tables.c.in ${CMAKE_CURRENT_BINARY_DIR}/tables.c) + +set(${MODULE_PREFIX}_LIBS ${${MODULE_PREFIX}_LIBS} freerdp winpr) + +set(${MODULE_PREFIX}_SRCS ${${MODULE_PREFIX}_SRCS} PARENT_SCOPE) +set(${MODULE_PREFIX}_LIBS ${${MODULE_PREFIX}_LIBS} PARENT_SCOPE) diff --git a/channels/client/addin.c b/channels/client/addin.c new file mode 100644 index 0000000..db32ff8 --- /dev/null +++ b/channels/client/addin.c @@ -0,0 +1,470 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Channel Addins + * + * Copyright 2012 Marc-Andre Moreau + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "tables.h" + +#include "addin.h" + +#include +#define TAG CHANNELS_TAG("addin") + +extern const STATIC_ENTRY_TABLE CLIENT_STATIC_ENTRY_TABLES[]; + +static void* freerdp_channels_find_static_entry_in_table(const STATIC_ENTRY_TABLE* table, + const char* identifier) +{ + size_t index = 0; + STATIC_ENTRY* pEntry; + pEntry = (STATIC_ENTRY*)&table->table[index++]; + + while (pEntry->entry != NULL) + { + if (strcmp(pEntry->name, identifier) == 0) + { + return (void*)pEntry->entry; + } + + pEntry = (STATIC_ENTRY*)&table->table[index++]; + } + + return NULL; +} + +void* freerdp_channels_client_find_static_entry(const char* name, const char* identifier) +{ + size_t index = 0; + STATIC_ENTRY_TABLE* pEntry; + pEntry = (STATIC_ENTRY_TABLE*)&CLIENT_STATIC_ENTRY_TABLES[index++]; + + while (pEntry->table != NULL) + { + if (strcmp(pEntry->name, name) == 0) + { + return freerdp_channels_find_static_entry_in_table(pEntry, identifier); + } + + pEntry = (STATIC_ENTRY_TABLE*)&CLIENT_STATIC_ENTRY_TABLES[index++]; + } + + return NULL; +} + +extern const STATIC_ADDIN_TABLE CLIENT_STATIC_ADDIN_TABLE[]; + +static FREERDP_ADDIN** freerdp_channels_list_client_static_addins(LPCSTR pszName, + LPCSTR pszSubsystem, + LPCSTR pszType, DWORD dwFlags) +{ + size_t i, j; + DWORD nAddins; + FREERDP_ADDIN** ppAddins = NULL; + STATIC_SUBSYSTEM_ENTRY* subsystems; + nAddins = 0; + ppAddins = (FREERDP_ADDIN**)calloc(128, sizeof(FREERDP_ADDIN*)); + + if (!ppAddins) + { + WLog_ERR(TAG, "calloc failed!"); + return NULL; + } + + ppAddins[nAddins] = NULL; + + for (i = 0; CLIENT_STATIC_ADDIN_TABLE[i].name != NULL; i++) + { + FREERDP_ADDIN* pAddin = (FREERDP_ADDIN*)calloc(1, sizeof(FREERDP_ADDIN)); + + if (!pAddin) + { + WLog_ERR(TAG, "calloc failed!"); + goto error_out; + } + + sprintf_s(pAddin->cName, ARRAYSIZE(pAddin->cName), "%s", CLIENT_STATIC_ADDIN_TABLE[i].name); + pAddin->dwFlags = FREERDP_ADDIN_CLIENT; + pAddin->dwFlags |= FREERDP_ADDIN_STATIC; + pAddin->dwFlags |= FREERDP_ADDIN_NAME; + ppAddins[nAddins++] = pAddin; + subsystems = (STATIC_SUBSYSTEM_ENTRY*)CLIENT_STATIC_ADDIN_TABLE[i].table; + + for (j = 0; subsystems[j].name != NULL; j++) + { + pAddin = (FREERDP_ADDIN*)calloc(1, sizeof(FREERDP_ADDIN)); + + if (!pAddin) + { + WLog_ERR(TAG, "calloc failed!"); + goto error_out; + } + + sprintf_s(pAddin->cName, ARRAYSIZE(pAddin->cName), "%s", + CLIENT_STATIC_ADDIN_TABLE[i].name); + sprintf_s(pAddin->cSubsystem, ARRAYSIZE(pAddin->cSubsystem), "%s", subsystems[j].name); + pAddin->dwFlags = FREERDP_ADDIN_CLIENT; + pAddin->dwFlags |= FREERDP_ADDIN_STATIC; + pAddin->dwFlags |= FREERDP_ADDIN_NAME; + pAddin->dwFlags |= FREERDP_ADDIN_SUBSYSTEM; + ppAddins[nAddins++] = pAddin; + } + } + + return ppAddins; +error_out: + freerdp_channels_addin_list_free(ppAddins); + return NULL; +} + +static FREERDP_ADDIN** freerdp_channels_list_dynamic_addins(LPCSTR pszName, LPCSTR pszSubsystem, + LPCSTR pszType, DWORD dwFlags) +{ + int index; + int nDashes; + HANDLE hFind; + DWORD nAddins; + LPSTR pszPattern; + size_t cchPattern; + LPCSTR pszAddinPath = FREERDP_ADDIN_PATH; + LPCSTR pszInstallPrefix = FREERDP_INSTALL_PREFIX; + LPCSTR pszExtension; + LPSTR pszSearchPath; + size_t cchSearchPath; + size_t cchAddinPath; + size_t cchInstallPrefix; + FREERDP_ADDIN** ppAddins; + WIN32_FIND_DATAA FindData; + cchAddinPath = strnlen(pszAddinPath, sizeof(FREERDP_ADDIN_PATH)); + cchInstallPrefix = strnlen(pszInstallPrefix, sizeof(FREERDP_INSTALL_PREFIX)); + pszExtension = PathGetSharedLibraryExtensionA(0); + cchPattern = 128 + strnlen(pszExtension, MAX_PATH) + 2; + pszPattern = (LPSTR)malloc(cchPattern + 1); + + if (!pszPattern) + { + WLog_ERR(TAG, "malloc failed!"); + return NULL; + } + + if (pszName && pszSubsystem && pszType) + { + sprintf_s(pszPattern, cchPattern, FREERDP_SHARED_LIBRARY_PREFIX "%s-client-%s-%s.%s", + pszName, pszSubsystem, pszType, pszExtension); + } + else if (pszName && pszType) + { + sprintf_s(pszPattern, cchPattern, FREERDP_SHARED_LIBRARY_PREFIX "%s-client-?-%s.%s", + pszName, pszType, pszExtension); + } + else if (pszName) + { + sprintf_s(pszPattern, cchPattern, FREERDP_SHARED_LIBRARY_PREFIX "%s-client*.%s", pszName, + pszExtension); + } + else + { + sprintf_s(pszPattern, cchPattern, FREERDP_SHARED_LIBRARY_PREFIX "?-client*.%s", + pszExtension); + } + + cchPattern = strnlen(pszPattern, cchPattern); + cchSearchPath = cchInstallPrefix + cchAddinPath + cchPattern + 3; + pszSearchPath = (LPSTR)malloc(cchSearchPath + 1); + + if (!pszSearchPath) + { + WLog_ERR(TAG, "malloc failed!"); + free(pszPattern); + return NULL; + } + + CopyMemory(pszSearchPath, pszInstallPrefix, cchInstallPrefix); + pszSearchPath[cchInstallPrefix] = '\0'; + NativePathCchAppendA(pszSearchPath, cchSearchPath + 1, pszAddinPath); + NativePathCchAppendA(pszSearchPath, cchSearchPath + 1, pszPattern); + free(pszPattern); + hFind = FindFirstFileA(pszSearchPath, &FindData); + free(pszSearchPath); + nAddins = 0; + ppAddins = (FREERDP_ADDIN**)calloc(128, sizeof(FREERDP_ADDIN*)); + + if (!ppAddins) + { + FindClose(hFind); + WLog_ERR(TAG, "calloc failed!"); + return NULL; + } + + if (hFind == INVALID_HANDLE_VALUE) + return ppAddins; + + do + { + BOOL used = FALSE; + FREERDP_ADDIN* pAddin = (FREERDP_ADDIN*)calloc(1, sizeof(FREERDP_ADDIN)); + + if (!pAddin) + { + WLog_ERR(TAG, "calloc failed!"); + goto error_out; + } + + nDashes = 0; + for (index = 0; FindData.cFileName[index]; index++) + nDashes += (FindData.cFileName[index] == '-') ? 1 : 0; + + if (nDashes == 1) + { + size_t len; + char* p[2] = { 0 }; + /* -client. */ + p[0] = FindData.cFileName; + p[1] = strchr(p[0], '-') + 1; + + len = p[1] - p[0]; + if (len < 1) + { + WLog_WARN(TAG, "Skipping file '%s', invalid format", FindData.cFileName); + goto skip; + } + strncpy(pAddin->cName, p[0], MIN(ARRAYSIZE(pAddin->cName), len - 1)); + + pAddin->dwFlags = FREERDP_ADDIN_CLIENT; + pAddin->dwFlags |= FREERDP_ADDIN_DYNAMIC; + pAddin->dwFlags |= FREERDP_ADDIN_NAME; + ppAddins[nAddins++] = pAddin; + + used = TRUE; + } + else if (nDashes == 2) + { + size_t len; + char* p[4] = { 0 }; + /* -client-. */ + p[0] = FindData.cFileName; + p[1] = strchr(p[0], '-') + 1; + p[2] = strchr(p[1], '-') + 1; + p[3] = strchr(p[2], '.') + 1; + + len = p[1] - p[0]; + if (len < 1) + { + WLog_WARN(TAG, "Skipping file '%s', invalid format", FindData.cFileName); + goto skip; + } + strncpy(pAddin->cName, p[0], MIN(ARRAYSIZE(pAddin->cName), len - 1)); + + len = p[3] - p[2]; + if (len < 1) + { + WLog_WARN(TAG, "Skipping file '%s', invalid format", FindData.cFileName); + goto skip; + } + strncpy(pAddin->cSubsystem, p[2], MIN(ARRAYSIZE(pAddin->cSubsystem), len - 1)); + + pAddin->dwFlags = FREERDP_ADDIN_CLIENT; + pAddin->dwFlags |= FREERDP_ADDIN_DYNAMIC; + pAddin->dwFlags |= FREERDP_ADDIN_NAME; + pAddin->dwFlags |= FREERDP_ADDIN_SUBSYSTEM; + ppAddins[nAddins++] = pAddin; + + used = TRUE; + } + else if (nDashes == 3) + { + size_t len; + char* p[5] = { 0 }; + /* -client--. */ + p[0] = FindData.cFileName; + p[1] = strchr(p[0], '-') + 1; + p[2] = strchr(p[1], '-') + 1; + p[3] = strchr(p[2], '-') + 1; + p[4] = strchr(p[3], '.') + 1; + + len = p[1] - p[0]; + if (len < 1) + { + WLog_WARN(TAG, "Skipping file '%s', invalid format", FindData.cFileName); + goto skip; + } + strncpy(pAddin->cName, p[0], MIN(ARRAYSIZE(pAddin->cName), len - 1)); + + len = p[3] - p[2]; + if (len < 1) + { + WLog_WARN(TAG, "Skipping file '%s', invalid format", FindData.cFileName); + goto skip; + } + strncpy(pAddin->cSubsystem, p[2], MIN(ARRAYSIZE(pAddin->cSubsystem), len - 1)); + + len = p[4] - p[3]; + if (len < 1) + { + WLog_WARN(TAG, "Skipping file '%s', invalid format", FindData.cFileName); + goto skip; + } + strncpy(pAddin->cType, p[3], MIN(ARRAYSIZE(pAddin->cType), len - 1)); + + pAddin->dwFlags = FREERDP_ADDIN_CLIENT; + pAddin->dwFlags |= FREERDP_ADDIN_DYNAMIC; + pAddin->dwFlags |= FREERDP_ADDIN_NAME; + pAddin->dwFlags |= FREERDP_ADDIN_SUBSYSTEM; + pAddin->dwFlags |= FREERDP_ADDIN_TYPE; + ppAddins[nAddins++] = pAddin; + + used = TRUE; + } + + skip: + if (!used) + free(pAddin); + + } while (FindNextFileA(hFind, &FindData)); + + FindClose(hFind); + ppAddins[nAddins] = NULL; + return ppAddins; +error_out: + FindClose(hFind); + freerdp_channels_addin_list_free(ppAddins); + return NULL; +} + +FREERDP_ADDIN** freerdp_channels_list_addins(LPCSTR pszName, LPCSTR pszSubsystem, LPCSTR pszType, + DWORD dwFlags) +{ + if (dwFlags & FREERDP_ADDIN_STATIC) + return freerdp_channels_list_client_static_addins(pszName, pszSubsystem, pszType, dwFlags); + else if (dwFlags & FREERDP_ADDIN_DYNAMIC) + return freerdp_channels_list_dynamic_addins(pszName, pszSubsystem, pszType, dwFlags); + + return NULL; +} + +void freerdp_channels_addin_list_free(FREERDP_ADDIN** ppAddins) +{ + size_t index; + + if (!ppAddins) + return; + + for (index = 0; ppAddins[index] != NULL; index++) + free(ppAddins[index]); + + free(ppAddins); +} + +extern const STATIC_ENTRY CLIENT_VirtualChannelEntryEx_TABLE[]; + +static BOOL freerdp_channels_is_virtual_channel_entry_ex(LPCSTR pszName) +{ + size_t i; + + for (i = 0; CLIENT_VirtualChannelEntryEx_TABLE[i].name != NULL; i++) + { + const STATIC_ENTRY* entry = &CLIENT_VirtualChannelEntryEx_TABLE[i]; + + if (!strncmp(entry->name, pszName, MAX_PATH)) + return TRUE; + } + + return FALSE; +} + +PVIRTUALCHANNELENTRY freerdp_channels_load_static_addin_entry(LPCSTR pszName, LPCSTR pszSubsystem, + LPCSTR pszType, DWORD dwFlags) +{ + const STATIC_ADDIN_TABLE* table = CLIENT_STATIC_ADDIN_TABLE; + const char* type = NULL; + + if (!pszName) + return NULL; + + if (dwFlags & FREERDP_ADDIN_CHANNEL_DYNAMIC) + type = "DVCPluginEntry"; + else if (dwFlags & FREERDP_ADDIN_CHANNEL_DEVICE) + type = "DeviceServiceEntry"; + else if (dwFlags & FREERDP_ADDIN_CHANNEL_STATIC) + { + if (dwFlags & FREERDP_ADDIN_CHANNEL_ENTRYEX) + type = "VirtualChannelEntryEx"; + else + type = "VirtualChannelEntry"; + } + + for (; table->name != NULL; table++) + { + if (strncmp(table->name, pszName, MAX_PATH) == 0) + { + if (type && strncmp(table->type, type, MAX_PATH)) + continue; + + if (pszSubsystem != NULL) + { + const STATIC_SUBSYSTEM_ENTRY* subsystems = table->table; + + for (; subsystems->name != NULL; subsystems++) + { + /* If the pszSubsystem is an empty string use the default backend. */ + if ((strnlen(pszSubsystem, 1) == + 0) || /* we only want to know if strnlen is > 0 */ + (strncmp(subsystems->name, pszSubsystem, MAX_PATH) == 0)) + { + if (pszType) + { + if (strncmp(subsystems->type, pszType, MAX_PATH) == 0) + return (PVIRTUALCHANNELENTRY)subsystems->entry; + } + else + { + return (PVIRTUALCHANNELENTRY)subsystems->entry; + } + } + } + } + else + { + if (dwFlags & FREERDP_ADDIN_CHANNEL_ENTRYEX) + { + if (!freerdp_channels_is_virtual_channel_entry_ex(pszName)) + return NULL; + } + + return (PVIRTUALCHANNELENTRY)table->entry; + } + } + } + + return NULL; +} diff --git a/channels/client/addin.h b/channels/client/addin.h new file mode 100644 index 0000000..849b11f --- /dev/null +++ b/channels/client/addin.h @@ -0,0 +1,18 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Channel Addins + * + * Copyright 2012 Marc-Andre Moreau + * + * Licensed 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. + */ diff --git a/channels/client/tables.c.in b/channels/client/tables.c.in new file mode 100644 index 0000000..aafc71d --- /dev/null +++ b/channels/client/tables.c.in @@ -0,0 +1,32 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Static Entry Point Tables + * + * Copyright 2012 Marc-Andre Moreau + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include "tables.h" + +${CLIENT_STATIC_TYPEDEFS} +${CLIENT_STATIC_ENTRY_IMPORTS} +${CLIENT_STATIC_ENTRY_TABLES} +${CLIENT_STATIC_ENTRY_TABLES_LIST} +${CLIENT_STATIC_SUBSYSTEM_IMPORTS} +${CLIENT_STATIC_SUBSYSTEM_TABLES} +${CLIENT_STATIC_ADDIN_TABLE} + diff --git a/channels/client/tables.h b/channels/client/tables.h new file mode 100644 index 0000000..b6b3f9c --- /dev/null +++ b/channels/client/tables.h @@ -0,0 +1,51 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Static Entry Point Tables + * + * Copyright 2012 Marc-Andre Moreau + * + * Licensed 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 + +struct _STATIC_ENTRY +{ + const char* name; + UINT (*entry)(); +}; +typedef struct _STATIC_ENTRY STATIC_ENTRY; + +struct _STATIC_ENTRY_TABLE +{ + const char* name; + const STATIC_ENTRY* table; +}; +typedef struct _STATIC_ENTRY_TABLE STATIC_ENTRY_TABLE; + +struct _STATIC_SUBSYSTEM_ENTRY +{ + const char* name; + const char* type; + UINT (*entry)(); +}; +typedef struct _STATIC_SUBSYSTEM_ENTRY STATIC_SUBSYSTEM_ENTRY; + +struct _STATIC_ADDIN_TABLE +{ + const char* name; + const char* type; + UINT (*entry)(); + const STATIC_SUBSYSTEM_ENTRY* table; +}; +typedef struct _STATIC_ADDIN_TABLE STATIC_ADDIN_TABLE; diff --git a/channels/cliprdr/CMakeLists.txt b/channels/cliprdr/CMakeLists.txt new file mode 100644 index 0000000..c5cfd72 --- /dev/null +++ b/channels/cliprdr/CMakeLists.txt @@ -0,0 +1,26 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2012 Marc-Andre Moreau +# +# Licensed 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. + +define_channel("cliprdr") + +if(WITH_CLIENT_CHANNELS) + add_channel_client(${MODULE_PREFIX} ${CHANNEL_NAME}) +endif() + +if(WITH_SERVER_CHANNELS) + add_channel_server(${MODULE_PREFIX} ${CHANNEL_NAME}) +endif() diff --git a/channels/cliprdr/ChannelOptions.cmake b/channels/cliprdr/ChannelOptions.cmake new file mode 100644 index 0000000..f175f3f --- /dev/null +++ b/channels/cliprdr/ChannelOptions.cmake @@ -0,0 +1,13 @@ + +set(OPTION_DEFAULT OFF) +set(OPTION_CLIENT_DEFAULT ON) +set(OPTION_SERVER_DEFAULT ON) + +define_channel_options(NAME "cliprdr" TYPE "static" + DESCRIPTION "Clipboard Virtual Channel Extension" + SPECIFICATIONS "[MS-RDPECLIP]" + DEFAULT ${OPTION_DEFAULT}) + +define_channel_client_options(${OPTION_CLIENT_DEFAULT}) +define_channel_server_options(${OPTION_SERVER_DEFAULT}) + diff --git a/channels/cliprdr/client/CMakeLists.txt b/channels/cliprdr/client/CMakeLists.txt new file mode 100644 index 0000000..4511e08 --- /dev/null +++ b/channels/cliprdr/client/CMakeLists.txt @@ -0,0 +1,33 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2012 Marc-Andre Moreau +# +# Licensed 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. + +define_channel_client("cliprdr") + +set(${MODULE_PREFIX}_SRCS + cliprdr_format.c + cliprdr_format.h + cliprdr_main.c + cliprdr_main.h + ../cliprdr_common.h + ../cliprdr_common.c + ) + +add_channel_client_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} FALSE "VirtualChannelEntryEx") + +set(${MODULE_PREFIX}_LIBS freerdp winpr) + +set_property(TARGET ${MODULE_NAME} PROPERTY FOLDER "Channels/${CHANNEL_NAME}/Client") diff --git a/channels/cliprdr/client/cliprdr_format.c b/channels/cliprdr/client/cliprdr_format.c new file mode 100644 index 0000000..0b6111b --- /dev/null +++ b/channels/cliprdr/client/cliprdr_format.c @@ -0,0 +1,175 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Clipboard Virtual Channel + * + * Copyright 2009-2011 Jay Sorg + * Copyright 2010-2011 Vic Lee + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include + +#include +#include +#include + +#include "cliprdr_main.h" +#include "cliprdr_format.h" +#include "../cliprdr_common.h" + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT cliprdr_process_format_list(cliprdrPlugin* cliprdr, wStream* s, UINT32 dataLen, + UINT16 msgFlags) +{ + CLIPRDR_FORMAT_LIST formatList = { 0 }; + CliprdrClientContext* context = cliprdr_get_client_interface(cliprdr); + UINT error = CHANNEL_RC_OK; + + if (!context->custom) + { + WLog_ERR(TAG, "context->custom not set!"); + return ERROR_INTERNAL_ERROR; + } + + formatList.msgType = CB_FORMAT_LIST; + formatList.msgFlags = msgFlags; + formatList.dataLen = dataLen; + + if ((error = cliprdr_read_format_list(s, &formatList, cliprdr->useLongFormatNames))) + goto error_out; + + WLog_Print(cliprdr->log, WLOG_DEBUG, "ServerFormatList: numFormats: %" PRIu32 "", + formatList.numFormats); + + if (context->ServerFormatList) + { + if ((error = context->ServerFormatList(context, &formatList))) + WLog_ERR(TAG, "ServerFormatList failed with error %" PRIu32 "", error); + } + +error_out: + cliprdr_free_format_list(&formatList); + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT cliprdr_process_format_list_response(cliprdrPlugin* cliprdr, wStream* s, UINT32 dataLen, + UINT16 msgFlags) +{ + CLIPRDR_FORMAT_LIST_RESPONSE formatListResponse = { 0 }; + CliprdrClientContext* context = cliprdr_get_client_interface(cliprdr); + UINT error = CHANNEL_RC_OK; + + WLog_Print(cliprdr->log, WLOG_DEBUG, "ServerFormatListResponse"); + + if (!context->custom) + { + WLog_ERR(TAG, "context->custom not set!"); + return ERROR_INTERNAL_ERROR; + } + + formatListResponse.msgType = CB_FORMAT_LIST_RESPONSE; + formatListResponse.msgFlags = msgFlags; + formatListResponse.dataLen = dataLen; + + IFCALLRET(context->ServerFormatListResponse, error, context, &formatListResponse); + if (error) + WLog_ERR(TAG, "ServerFormatListResponse failed with error %" PRIu32 "!", error); + + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT cliprdr_process_format_data_request(cliprdrPlugin* cliprdr, wStream* s, UINT32 dataLen, + UINT16 msgFlags) +{ + CLIPRDR_FORMAT_DATA_REQUEST formatDataRequest; + CliprdrClientContext* context = cliprdr_get_client_interface(cliprdr); + UINT error = CHANNEL_RC_OK; + + WLog_Print(cliprdr->log, WLOG_DEBUG, "ServerFormatDataRequest"); + + if (!context->custom) + { + WLog_ERR(TAG, "context->custom not set!"); + return ERROR_INTERNAL_ERROR; + } + + formatDataRequest.msgType = CB_FORMAT_DATA_REQUEST; + formatDataRequest.msgFlags = msgFlags; + formatDataRequest.dataLen = dataLen; + + if ((error = cliprdr_read_format_data_request(s, &formatDataRequest))) + return error; + + context->lastRequestedFormatId = formatDataRequest.requestedFormatId; + IFCALLRET(context->ServerFormatDataRequest, error, context, &formatDataRequest); + if (error) + WLog_ERR(TAG, "ServerFormatDataRequest failed with error %" PRIu32 "!", error); + + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT cliprdr_process_format_data_response(cliprdrPlugin* cliprdr, wStream* s, UINT32 dataLen, + UINT16 msgFlags) +{ + CLIPRDR_FORMAT_DATA_RESPONSE formatDataResponse; + CliprdrClientContext* context = cliprdr_get_client_interface(cliprdr); + UINT error = CHANNEL_RC_OK; + + WLog_Print(cliprdr->log, WLOG_DEBUG, "ServerFormatDataResponse"); + + if (!context->custom) + { + WLog_ERR(TAG, "context->custom not set!"); + return ERROR_INTERNAL_ERROR; + } + + formatDataResponse.msgType = CB_FORMAT_DATA_RESPONSE; + formatDataResponse.msgFlags = msgFlags; + formatDataResponse.dataLen = dataLen; + + if ((error = cliprdr_read_format_data_response(s, &formatDataResponse))) + return error; + + IFCALLRET(context->ServerFormatDataResponse, error, context, &formatDataResponse); + if (error) + WLog_ERR(TAG, "ServerFormatDataResponse failed with error %" PRIu32 "!", error); + + return error; +} diff --git a/channels/cliprdr/client/cliprdr_format.h b/channels/cliprdr/client/cliprdr_format.h new file mode 100644 index 0000000..a068d6f --- /dev/null +++ b/channels/cliprdr/client/cliprdr_format.h @@ -0,0 +1,35 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Clipboard Virtual Channel + * + * Copyright 2009-2011 Jay Sorg + * Copyright 2010-2011 Vic Lee + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger + * + * Licensed 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 FREERDP_CHANNEL_CLIPRDR_CLIENT_FORMAT_H +#define FREERDP_CHANNEL_CLIPRDR_CLIENT_FORMAT_H + +UINT cliprdr_process_format_list(cliprdrPlugin* cliprdr, wStream* s, UINT32 dataLen, + UINT16 msgFlags); +UINT cliprdr_process_format_list_response(cliprdrPlugin* cliprdr, wStream* s, UINT32 dataLen, + UINT16 msgFlags); +UINT cliprdr_process_format_data_request(cliprdrPlugin* cliprdr, wStream* s, UINT32 dataLen, + UINT16 msgFlags); +UINT cliprdr_process_format_data_response(cliprdrPlugin* cliprdr, wStream* s, UINT32 dataLen, + UINT16 msgFlags); + +#endif /* FREERDP_CHANNEL_CLIPRDR_CLIENT_FORMAT_H */ diff --git a/channels/cliprdr/client/cliprdr_main.c b/channels/cliprdr/client/cliprdr_main.c new file mode 100644 index 0000000..5aed224 --- /dev/null +++ b/channels/cliprdr/client/cliprdr_main.c @@ -0,0 +1,1222 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Clipboard Virtual Channel + * + * Copyright 2009-2011 Jay Sorg + * Copyright 2010-2011 Vic Lee + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include + +#include +#include +#include + +#include "cliprdr_main.h" +#include "cliprdr_format.h" +#include "../cliprdr_common.h" + +#ifdef WITH_DEBUG_CLIPRDR +static const char* CB_MSG_TYPE_STRINGS(UINT32 type) +{ + switch (type) + { + case CB_MONITOR_READY: + return "CB_MONITOR_READY"; + case CB_FORMAT_LIST: + return "CB_FORMAT_LIST"; + case CB_FORMAT_LIST_RESPONSE: + return "CB_FORMAT_LIST_RESPONSE"; + case CB_FORMAT_DATA_REQUEST: + return "CB_FORMAT_DATA_REQUEST"; + case CB_FORMAT_DATA_RESPONSE: + return "CB_FORMAT_DATA_RESPONSE"; + case CB_TEMP_DIRECTORY: + return "CB_TEMP_DIRECTORY"; + case CB_CLIP_CAPS: + return "CB_CLIP_CAPS"; + case CB_FILECONTENTS_REQUEST: + return "CB_FILECONTENTS_REQUEST"; + case CB_FILECONTENTS_RESPONSE: + return "CB_FILECONTENTS_RESPONSE"; + case CB_LOCK_CLIPDATA: + return "CB_LOCK_CLIPDATA"; + case CB_UNLOCK_CLIPDATA: + return "CB_UNLOCK_CLIPDATA"; + default: + return "UNKNOWN"; + } +} +#endif + +CliprdrClientContext* cliprdr_get_client_interface(cliprdrPlugin* cliprdr) +{ + CliprdrClientContext* pInterface; + + if (!cliprdr) + return NULL; + + pInterface = (CliprdrClientContext*)cliprdr->channelEntryPoints.pInterface; + return pInterface; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT cliprdr_packet_send(cliprdrPlugin* cliprdr, wStream* s) +{ + size_t pos; + UINT32 dataLen; + UINT status = CHANNEL_RC_OK; + pos = Stream_GetPosition(s); + dataLen = pos - 8; + Stream_SetPosition(s, 4); + Stream_Write_UINT32(s, dataLen); + Stream_SetPosition(s, pos); +#ifdef WITH_DEBUG_CLIPRDR + WLog_DBG(TAG, "Cliprdr Sending (%" PRIu32 " bytes)", dataLen + 8); + winpr_HexDump(TAG, WLOG_DEBUG, Stream_Buffer(s), dataLen + 8); +#endif + + if (!cliprdr) + { + status = CHANNEL_RC_BAD_INIT_HANDLE; + } + else + { + status = cliprdr->channelEntryPoints.pVirtualChannelWriteEx( + cliprdr->InitHandle, cliprdr->OpenHandle, Stream_Buffer(s), + (UINT32)Stream_GetPosition(s), s); + } + + if (status != CHANNEL_RC_OK) + { + Stream_Free(s, TRUE); + WLog_ERR(TAG, "VirtualChannelWrite failed with %s [%08" PRIX32 "]", + WTSErrorToString(status), status); + } + + return status; +} + +#ifdef WITH_DEBUG_CLIPRDR +static void cliprdr_print_general_capability_flags(UINT32 flags) +{ + WLog_INFO(TAG, "generalFlags (0x%08" PRIX32 ") {", flags); + + if (flags & CB_USE_LONG_FORMAT_NAMES) + WLog_INFO(TAG, "\tCB_USE_LONG_FORMAT_NAMES"); + + if (flags & CB_STREAM_FILECLIP_ENABLED) + WLog_INFO(TAG, "\tCB_STREAM_FILECLIP_ENABLED"); + + if (flags & CB_FILECLIP_NO_FILE_PATHS) + WLog_INFO(TAG, "\tCB_FILECLIP_NO_FILE_PATHS"); + + if (flags & CB_CAN_LOCK_CLIPDATA) + WLog_INFO(TAG, "\tCB_CAN_LOCK_CLIPDATA"); + + if (flags & CB_HUGE_FILE_SUPPORT_ENABLED) + WLog_INFO(TAG, "\tCB_HUGE_FILE_SUPPORT_ENABLED"); + + WLog_INFO(TAG, "}"); +} +#endif + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT cliprdr_process_general_capability(cliprdrPlugin* cliprdr, wStream* s) +{ + UINT32 version; + UINT32 generalFlags; + CLIPRDR_CAPABILITIES capabilities; + CLIPRDR_GENERAL_CAPABILITY_SET generalCapabilitySet; + CliprdrClientContext* context = cliprdr_get_client_interface(cliprdr); + UINT error = CHANNEL_RC_OK; + + if (!context) + { + WLog_ERR(TAG, "cliprdr_get_client_interface failed!"); + return ERROR_INTERNAL_ERROR; + } + + if (Stream_GetRemainingLength(s) < 8) + return ERROR_INVALID_DATA; + + Stream_Read_UINT32(s, version); /* version (4 bytes) */ + Stream_Read_UINT32(s, generalFlags); /* generalFlags (4 bytes) */ + DEBUG_CLIPRDR("Version: %" PRIu32 "", version); +#ifdef WITH_DEBUG_CLIPRDR + cliprdr_print_general_capability_flags(generalFlags); +#endif + + cliprdr->useLongFormatNames = (generalFlags & CB_USE_LONG_FORMAT_NAMES); + cliprdr->streamFileClipEnabled = (generalFlags & CB_STREAM_FILECLIP_ENABLED); + cliprdr->fileClipNoFilePaths = (generalFlags & CB_FILECLIP_NO_FILE_PATHS); + cliprdr->canLockClipData = (generalFlags & CB_CAN_LOCK_CLIPDATA); + cliprdr->hasHugeFileSupport = (generalFlags & CB_HUGE_FILE_SUPPORT_ENABLED); + cliprdr->capabilitiesReceived = TRUE; + + if (!context->custom) + { + WLog_ERR(TAG, "context->custom not set!"); + return ERROR_INTERNAL_ERROR; + } + + capabilities.msgType = CB_CLIP_CAPS; + capabilities.cCapabilitiesSets = 1; + capabilities.capabilitySets = (CLIPRDR_CAPABILITY_SET*)&(generalCapabilitySet); + generalCapabilitySet.capabilitySetType = CB_CAPSTYPE_GENERAL; + generalCapabilitySet.capabilitySetLength = 12; + generalCapabilitySet.version = version; + generalCapabilitySet.generalFlags = generalFlags; + IFCALLRET(context->ServerCapabilities, error, context, &capabilities); + + if (error) + WLog_ERR(TAG, "ServerCapabilities failed with error %" PRIu32 "!", error); + + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT cliprdr_process_clip_caps(cliprdrPlugin* cliprdr, wStream* s, UINT32 length, + UINT16 flags) +{ + UINT16 index; + UINT16 lengthCapability; + UINT16 cCapabilitiesSets; + UINT16 capabilitySetType; + UINT error = CHANNEL_RC_OK; + + if (Stream_GetRemainingLength(s) < 4) + return ERROR_INVALID_DATA; + + Stream_Read_UINT16(s, cCapabilitiesSets); /* cCapabilitiesSets (2 bytes) */ + Stream_Seek_UINT16(s); /* pad1 (2 bytes) */ + WLog_Print(cliprdr->log, WLOG_DEBUG, "ServerCapabilities"); + + for (index = 0; index < cCapabilitiesSets; index++) + { + if (Stream_GetRemainingLength(s) < 4) + return ERROR_INVALID_DATA; + + Stream_Read_UINT16(s, capabilitySetType); /* capabilitySetType (2 bytes) */ + Stream_Read_UINT16(s, lengthCapability); /* lengthCapability (2 bytes) */ + + if ((lengthCapability < 4) || (Stream_GetRemainingLength(s) < (lengthCapability - 4U))) + return ERROR_INVALID_DATA; + + switch (capabilitySetType) + { + case CB_CAPSTYPE_GENERAL: + if ((error = cliprdr_process_general_capability(cliprdr, s))) + { + WLog_ERR(TAG, + "cliprdr_process_general_capability failed with error %" PRIu32 "!", + error); + return error; + } + + break; + + default: + WLog_ERR(TAG, "unknown cliprdr capability set: %" PRIu16 "", capabilitySetType); + return CHANNEL_RC_BAD_PROC; + } + } + + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT cliprdr_process_monitor_ready(cliprdrPlugin* cliprdr, wStream* s, UINT32 length, + UINT16 flags) +{ + CLIPRDR_MONITOR_READY monitorReady; + CliprdrClientContext* context = cliprdr_get_client_interface(cliprdr); + UINT error = CHANNEL_RC_OK; + WLog_Print(cliprdr->log, WLOG_DEBUG, "MonitorReady"); + + if (!context->custom) + { + WLog_ERR(TAG, "context->custom not set!"); + return ERROR_INTERNAL_ERROR; + } + + if (!cliprdr->capabilitiesReceived) + { + /** + * The clipboard capabilities pdu from server to client is optional, + * but a server using it must send it before sending the monitor ready pdu. + * When the server capabilities pdu is not used, default capabilities + * corresponding to a generalFlags field set to zero are assumed. + */ + cliprdr->useLongFormatNames = FALSE; + cliprdr->streamFileClipEnabled = FALSE; + cliprdr->fileClipNoFilePaths = TRUE; + cliprdr->canLockClipData = FALSE; + } + + monitorReady.msgType = CB_MONITOR_READY; + monitorReady.msgFlags = flags; + monitorReady.dataLen = length; + IFCALLRET(context->MonitorReady, error, context, &monitorReady); + + if (error) + WLog_ERR(TAG, "MonitorReady failed with error %" PRIu32 "!", error); + + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT cliprdr_process_filecontents_request(cliprdrPlugin* cliprdr, wStream* s, UINT32 length, + UINT16 flags) +{ + CLIPRDR_FILE_CONTENTS_REQUEST request; + CliprdrClientContext* context = cliprdr_get_client_interface(cliprdr); + UINT error = CHANNEL_RC_OK; + WLog_Print(cliprdr->log, WLOG_DEBUG, "FileContentsRequest"); + + if (!context->custom) + { + WLog_ERR(TAG, "context->custom not set!"); + return ERROR_INTERNAL_ERROR; + } + + request.msgType = CB_FILECONTENTS_REQUEST; + request.msgFlags = flags; + request.dataLen = length; + + if ((error = cliprdr_read_file_contents_request(s, &request))) + return error; + + IFCALLRET(context->ServerFileContentsRequest, error, context, &request); + + if (error) + WLog_ERR(TAG, "ServerFileContentsRequest failed with error %" PRIu32 "!", error); + + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT cliprdr_process_filecontents_response(cliprdrPlugin* cliprdr, wStream* s, UINT32 length, + UINT16 flags) +{ + CLIPRDR_FILE_CONTENTS_RESPONSE response; + CliprdrClientContext* context = cliprdr_get_client_interface(cliprdr); + UINT error = CHANNEL_RC_OK; + WLog_Print(cliprdr->log, WLOG_DEBUG, "FileContentsResponse"); + + if (!context->custom) + { + WLog_ERR(TAG, "context->custom not set!"); + return ERROR_INTERNAL_ERROR; + } + + response.msgType = CB_FILECONTENTS_RESPONSE; + response.msgFlags = flags; + response.dataLen = length; + + if ((error = cliprdr_read_file_contents_response(s, &response))) + return error; + + IFCALLRET(context->ServerFileContentsResponse, error, context, &response); + + if (error) + WLog_ERR(TAG, "ServerFileContentsResponse failed with error %" PRIu32 "!", error); + + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT cliprdr_process_lock_clipdata(cliprdrPlugin* cliprdr, wStream* s, UINT32 length, + UINT16 flags) +{ + CLIPRDR_LOCK_CLIPBOARD_DATA lockClipboardData; + CliprdrClientContext* context = cliprdr_get_client_interface(cliprdr); + UINT error = CHANNEL_RC_OK; + WLog_Print(cliprdr->log, WLOG_DEBUG, "LockClipData"); + + if (!context->custom) + { + WLog_ERR(TAG, "context->custom not set!"); + return ERROR_INTERNAL_ERROR; + } + + if (Stream_GetRemainingLength(s) < 4) + { + WLog_ERR(TAG, "not enough remaining data"); + return ERROR_INVALID_DATA; + } + + lockClipboardData.msgType = CB_LOCK_CLIPDATA; + lockClipboardData.msgFlags = flags; + lockClipboardData.dataLen = length; + Stream_Read_UINT32(s, lockClipboardData.clipDataId); /* clipDataId (4 bytes) */ + IFCALLRET(context->ServerLockClipboardData, error, context, &lockClipboardData); + + if (error) + WLog_ERR(TAG, "ServerLockClipboardData failed with error %" PRIu32 "!", error); + + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT cliprdr_process_unlock_clipdata(cliprdrPlugin* cliprdr, wStream* s, UINT32 length, + UINT16 flags) +{ + CLIPRDR_UNLOCK_CLIPBOARD_DATA unlockClipboardData; + CliprdrClientContext* context = cliprdr_get_client_interface(cliprdr); + UINT error = CHANNEL_RC_OK; + WLog_Print(cliprdr->log, WLOG_DEBUG, "UnlockClipData"); + + if (!context->custom) + { + WLog_ERR(TAG, "context->custom not set!"); + return ERROR_INTERNAL_ERROR; + } + + if ((error = cliprdr_read_unlock_clipdata(s, &unlockClipboardData))) + return error; + + unlockClipboardData.msgType = CB_UNLOCK_CLIPDATA; + unlockClipboardData.msgFlags = flags; + unlockClipboardData.dataLen = length; + + IFCALLRET(context->ServerUnlockClipboardData, error, context, &unlockClipboardData); + + if (error) + WLog_ERR(TAG, "ServerUnlockClipboardData failed with error %" PRIu32 "!", error); + + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT cliprdr_order_recv(cliprdrPlugin* cliprdr, wStream* s) +{ + UINT16 msgType; + UINT16 msgFlags; + UINT32 dataLen; + UINT error; + + if (Stream_GetRemainingLength(s) < 8) + return ERROR_INVALID_DATA; + + Stream_Read_UINT16(s, msgType); /* msgType (2 bytes) */ + Stream_Read_UINT16(s, msgFlags); /* msgFlags (2 bytes) */ + Stream_Read_UINT32(s, dataLen); /* dataLen (4 bytes) */ + + if (Stream_GetRemainingLength(s) < dataLen) + return ERROR_INVALID_DATA; + +#ifdef WITH_DEBUG_CLIPRDR + WLog_DBG(TAG, "msgType: %s (%" PRIu16 "), msgFlags: %" PRIu16 " dataLen: %" PRIu32 "", + CB_MSG_TYPE_STRINGS(msgType), msgType, msgFlags, dataLen); + winpr_HexDump(TAG, WLOG_DEBUG, Stream_Buffer(s), dataLen + 8); +#endif + + switch (msgType) + { + case CB_CLIP_CAPS: + if ((error = cliprdr_process_clip_caps(cliprdr, s, dataLen, msgFlags))) + WLog_ERR(TAG, "cliprdr_process_clip_caps failed with error %" PRIu32 "!", error); + + break; + + case CB_MONITOR_READY: + if ((error = cliprdr_process_monitor_ready(cliprdr, s, dataLen, msgFlags))) + WLog_ERR(TAG, "cliprdr_process_monitor_ready failed with error %" PRIu32 "!", + error); + + break; + + case CB_FORMAT_LIST: + if ((error = cliprdr_process_format_list(cliprdr, s, dataLen, msgFlags))) + WLog_ERR(TAG, "cliprdr_process_format_list failed with error %" PRIu32 "!", error); + + break; + + case CB_FORMAT_LIST_RESPONSE: + if ((error = cliprdr_process_format_list_response(cliprdr, s, dataLen, msgFlags))) + WLog_ERR(TAG, "cliprdr_process_format_list_response failed with error %" PRIu32 "!", + error); + + break; + + case CB_FORMAT_DATA_REQUEST: + if ((error = cliprdr_process_format_data_request(cliprdr, s, dataLen, msgFlags))) + WLog_ERR(TAG, "cliprdr_process_format_data_request failed with error %" PRIu32 "!", + error); + + break; + + case CB_FORMAT_DATA_RESPONSE: + if ((error = cliprdr_process_format_data_response(cliprdr, s, dataLen, msgFlags))) + WLog_ERR(TAG, "cliprdr_process_format_data_response failed with error %" PRIu32 "!", + error); + + break; + + case CB_FILECONTENTS_REQUEST: + if ((error = cliprdr_process_filecontents_request(cliprdr, s, dataLen, msgFlags))) + WLog_ERR(TAG, "cliprdr_process_filecontents_request failed with error %" PRIu32 "!", + error); + + break; + + case CB_FILECONTENTS_RESPONSE: + if ((error = cliprdr_process_filecontents_response(cliprdr, s, dataLen, msgFlags))) + WLog_ERR(TAG, + "cliprdr_process_filecontents_response failed with error %" PRIu32 "!", + error); + + break; + + case CB_LOCK_CLIPDATA: + if ((error = cliprdr_process_lock_clipdata(cliprdr, s, dataLen, msgFlags))) + WLog_ERR(TAG, "cliprdr_process_lock_clipdata failed with error %" PRIu32 "!", + error); + + break; + + case CB_UNLOCK_CLIPDATA: + if ((error = cliprdr_process_unlock_clipdata(cliprdr, s, dataLen, msgFlags))) + WLog_ERR(TAG, "cliprdr_process_lock_clipdata failed with error %" PRIu32 "!", + error); + + break; + + default: + error = CHANNEL_RC_BAD_PROC; + WLog_ERR(TAG, "unknown msgType %" PRIu16 "", msgType); + break; + } + + Stream_Free(s, TRUE); + return error; +} + +/** + * Callback Interface + */ + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT cliprdr_client_capabilities(CliprdrClientContext* context, + const CLIPRDR_CAPABILITIES* capabilities) +{ + wStream* s; + UINT32 flags; + const CLIPRDR_GENERAL_CAPABILITY_SET* generalCapabilitySet; + cliprdrPlugin* cliprdr = (cliprdrPlugin*)context->handle; + + s = cliprdr_packet_new(CB_CLIP_CAPS, 0, 4 + CB_CAPSTYPE_GENERAL_LEN); + + if (!s) + { + WLog_ERR(TAG, "cliprdr_packet_new failed!"); + return ERROR_INTERNAL_ERROR; + } + + Stream_Write_UINT16(s, 1); /* cCapabilitiesSets */ + Stream_Write_UINT16(s, 0); /* pad1 */ + generalCapabilitySet = (const CLIPRDR_GENERAL_CAPABILITY_SET*)capabilities->capabilitySets; + Stream_Write_UINT16(s, generalCapabilitySet->capabilitySetType); /* capabilitySetType */ + Stream_Write_UINT16(s, generalCapabilitySet->capabilitySetLength); /* lengthCapability */ + Stream_Write_UINT32(s, generalCapabilitySet->version); /* version */ + flags = generalCapabilitySet->generalFlags; + + /* Client capabilities are sent in response to server capabilities. + * -> Do not request features the server does not support. + * -> Update clipboard context feature state to what was agreed upon. + */ + if (!cliprdr->useLongFormatNames) + flags &= ~CB_USE_LONG_FORMAT_NAMES; + if (!cliprdr->streamFileClipEnabled) + flags &= ~CB_STREAM_FILECLIP_ENABLED; + if (!cliprdr->fileClipNoFilePaths) + flags &= ~CB_FILECLIP_NO_FILE_PATHS; + if (!cliprdr->canLockClipData) + flags &= ~CB_CAN_LOCK_CLIPDATA; + if (!cliprdr->hasHugeFileSupport) + flags &= ~CB_HUGE_FILE_SUPPORT_ENABLED; + + cliprdr->useLongFormatNames = flags & CB_USE_LONG_FORMAT_NAMES; + cliprdr->streamFileClipEnabled = flags & CB_STREAM_FILECLIP_ENABLED; + cliprdr->fileClipNoFilePaths = flags & CB_FILECLIP_NO_FILE_PATHS; + cliprdr->canLockClipData = flags & CB_CAN_LOCK_CLIPDATA; + cliprdr->hasHugeFileSupport = flags & CB_HUGE_FILE_SUPPORT_ENABLED; + + Stream_Write_UINT32(s, flags); /* generalFlags */ + WLog_Print(cliprdr->log, WLOG_DEBUG, "ClientCapabilities"); + return cliprdr_packet_send(cliprdr, s); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT cliprdr_temp_directory(CliprdrClientContext* context, + const CLIPRDR_TEMP_DIRECTORY* tempDirectory) +{ + int length; + wStream* s; + WCHAR* wszTempDir = NULL; + cliprdrPlugin* cliprdr = (cliprdrPlugin*)context->handle; + s = cliprdr_packet_new(CB_TEMP_DIRECTORY, 0, 260 * sizeof(WCHAR)); + + if (!s) + { + WLog_ERR(TAG, "cliprdr_packet_new failed!"); + return ERROR_INTERNAL_ERROR; + } + + length = ConvertToUnicode(CP_UTF8, 0, tempDirectory->szTempDir, -1, &wszTempDir, 0); + + if (length < 0) + return ERROR_INTERNAL_ERROR; + + /* Path must be 260 UTF16 characters with '\0' termination. + * ensure this here */ + if (length >= 260) + length = 259; + + Stream_Write_UTF16_String(s, wszTempDir, length); + Stream_Zero(s, 520 - (length * sizeof(WCHAR))); + free(wszTempDir); + WLog_Print(cliprdr->log, WLOG_DEBUG, "TempDirectory: %s", tempDirectory->szTempDir); + return cliprdr_packet_send(cliprdr, s); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT cliprdr_client_format_list(CliprdrClientContext* context, + const CLIPRDR_FORMAT_LIST* formatList) +{ + wStream* s; + cliprdrPlugin* cliprdr = (cliprdrPlugin*)context->handle; + + s = cliprdr_packet_format_list_new(formatList, cliprdr->useLongFormatNames); + if (!s) + { + WLog_ERR(TAG, "cliprdr_packet_format_list_new failed!"); + return ERROR_INTERNAL_ERROR; + } + + WLog_Print(cliprdr->log, WLOG_DEBUG, "ClientFormatList: numFormats: %" PRIu32 "", + formatList->numFormats); + return cliprdr_packet_send(cliprdr, s); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT +cliprdr_client_format_list_response(CliprdrClientContext* context, + const CLIPRDR_FORMAT_LIST_RESPONSE* formatListResponse) +{ + wStream* s; + cliprdrPlugin* cliprdr = (cliprdrPlugin*)context->handle; + s = cliprdr_packet_new(CB_FORMAT_LIST_RESPONSE, formatListResponse->msgFlags, 0); + + if (!s) + { + WLog_ERR(TAG, "cliprdr_packet_new failed!"); + return ERROR_INTERNAL_ERROR; + } + + WLog_Print(cliprdr->log, WLOG_DEBUG, "ClientFormatListResponse"); + return cliprdr_packet_send(cliprdr, s); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT cliprdr_client_lock_clipboard_data(CliprdrClientContext* context, + const CLIPRDR_LOCK_CLIPBOARD_DATA* lockClipboardData) +{ + wStream* s; + cliprdrPlugin* cliprdr = (cliprdrPlugin*)context->handle; + s = cliprdr_packet_lock_clipdata_new(lockClipboardData); + + if (!s) + { + WLog_ERR(TAG, "cliprdr_packet_new failed!"); + return ERROR_INTERNAL_ERROR; + } + + WLog_Print(cliprdr->log, WLOG_DEBUG, "ClientLockClipboardData: clipDataId: 0x%08" PRIX32 "", + lockClipboardData->clipDataId); + return cliprdr_packet_send(cliprdr, s); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT +cliprdr_client_unlock_clipboard_data(CliprdrClientContext* context, + const CLIPRDR_UNLOCK_CLIPBOARD_DATA* unlockClipboardData) +{ + wStream* s; + cliprdrPlugin* cliprdr = (cliprdrPlugin*)context->handle; + s = cliprdr_packet_unlock_clipdata_new(unlockClipboardData); + + if (!s) + { + WLog_ERR(TAG, "cliprdr_packet_new failed!"); + return ERROR_INTERNAL_ERROR; + } + + WLog_Print(cliprdr->log, WLOG_DEBUG, "ClientUnlockClipboardData: clipDataId: 0x%08" PRIX32 "", + unlockClipboardData->clipDataId); + return cliprdr_packet_send(cliprdr, s); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT cliprdr_client_format_data_request(CliprdrClientContext* context, + const CLIPRDR_FORMAT_DATA_REQUEST* formatDataRequest) +{ + wStream* s; + cliprdrPlugin* cliprdr = (cliprdrPlugin*)context->handle; + + s = cliprdr_packet_new(CB_FORMAT_DATA_REQUEST, 0, 4); + + if (!s) + { + WLog_ERR(TAG, "cliprdr_packet_new failed!"); + return ERROR_INTERNAL_ERROR; + } + + Stream_Write_UINT32(s, formatDataRequest->requestedFormatId); /* requestedFormatId (4 bytes) */ + WLog_Print(cliprdr->log, WLOG_DEBUG, "ClientFormatDataRequest"); + return cliprdr_packet_send(cliprdr, s); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT +cliprdr_client_format_data_response(CliprdrClientContext* context, + const CLIPRDR_FORMAT_DATA_RESPONSE* formatDataResponse) +{ + wStream* s; + cliprdrPlugin* cliprdr = (cliprdrPlugin*)context->handle; + + s = cliprdr_packet_new(CB_FORMAT_DATA_RESPONSE, formatDataResponse->msgFlags, + formatDataResponse->dataLen); + + if (!s) + { + WLog_ERR(TAG, "cliprdr_packet_new failed!"); + return ERROR_INTERNAL_ERROR; + } + + Stream_Write(s, formatDataResponse->requestedFormatData, formatDataResponse->dataLen); + WLog_Print(cliprdr->log, WLOG_DEBUG, "ClientFormatDataResponse"); + return cliprdr_packet_send(cliprdr, s); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT +cliprdr_client_file_contents_request(CliprdrClientContext* context, + const CLIPRDR_FILE_CONTENTS_REQUEST* fileContentsRequest) +{ + wStream* s; + cliprdrPlugin* cliprdr = (cliprdrPlugin*)context->handle; + + if (!cliprdr) + return ERROR_INTERNAL_ERROR; + + if (!cliprdr->hasHugeFileSupport) + { + if (((UINT64)fileContentsRequest->cbRequested + fileContentsRequest->nPositionLow) > + UINT32_MAX) + return ERROR_INVALID_PARAMETER; + if (fileContentsRequest->nPositionHigh != 0) + return ERROR_INVALID_PARAMETER; + } + + s = cliprdr_packet_file_contents_request_new(fileContentsRequest); + + if (!s) + { + WLog_ERR(TAG, "cliprdr_packet_file_contents_request_new failed!"); + return ERROR_INTERNAL_ERROR; + } + + WLog_Print(cliprdr->log, WLOG_DEBUG, "ClientFileContentsRequest: streamId: 0x%08" PRIX32 "", + fileContentsRequest->streamId); + return cliprdr_packet_send(cliprdr, s); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT +cliprdr_client_file_contents_response(CliprdrClientContext* context, + const CLIPRDR_FILE_CONTENTS_RESPONSE* fileContentsResponse) +{ + wStream* s; + cliprdrPlugin* cliprdr = (cliprdrPlugin*)context->handle; + s = cliprdr_packet_file_contents_response_new(fileContentsResponse); + + if (!s) + { + WLog_ERR(TAG, "cliprdr_packet_new failed!"); + return ERROR_INTERNAL_ERROR; + } + + WLog_Print(cliprdr->log, WLOG_DEBUG, "ClientFileContentsResponse: streamId: 0x%08" PRIX32 "", + fileContentsResponse->streamId); + return cliprdr_packet_send(cliprdr, s); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT cliprdr_virtual_channel_event_data_received(cliprdrPlugin* cliprdr, void* pData, + UINT32 dataLength, UINT32 totalLength, + UINT32 dataFlags) +{ + wStream* data_in; + + if ((dataFlags & CHANNEL_FLAG_SUSPEND) || (dataFlags & CHANNEL_FLAG_RESUME)) + { + return CHANNEL_RC_OK; + } + + if (dataFlags & CHANNEL_FLAG_FIRST) + { + if (cliprdr->data_in) + Stream_Free(cliprdr->data_in, TRUE); + + cliprdr->data_in = Stream_New(NULL, totalLength); + } + + if (!(data_in = cliprdr->data_in)) + { + WLog_ERR(TAG, "Stream_New failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + if (!Stream_EnsureRemainingCapacity(data_in, dataLength)) + { + Stream_Free(cliprdr->data_in, TRUE); + cliprdr->data_in = NULL; + return CHANNEL_RC_NO_MEMORY; + } + + Stream_Write(data_in, pData, dataLength); + + if (dataFlags & CHANNEL_FLAG_LAST) + { + if (Stream_Capacity(data_in) != Stream_GetPosition(data_in)) + { + WLog_ERR(TAG, "cliprdr_plugin_process_received: read error"); + return ERROR_INTERNAL_ERROR; + } + + cliprdr->data_in = NULL; + Stream_SealLength(data_in); + Stream_SetPosition(data_in, 0); + + if (!MessageQueue_Post(cliprdr->queue, NULL, 0, (void*)data_in, NULL)) + { + WLog_ERR(TAG, "MessageQueue_Post failed!"); + return ERROR_INTERNAL_ERROR; + } + } + + return CHANNEL_RC_OK; +} + +static VOID VCAPITYPE cliprdr_virtual_channel_open_event_ex(LPVOID lpUserParam, DWORD openHandle, + UINT event, LPVOID pData, + UINT32 dataLength, UINT32 totalLength, + UINT32 dataFlags) +{ + UINT error = CHANNEL_RC_OK; + cliprdrPlugin* cliprdr = (cliprdrPlugin*)lpUserParam; + + switch (event) + { + case CHANNEL_EVENT_DATA_RECEIVED: + if (!cliprdr || (cliprdr->OpenHandle != openHandle)) + { + WLog_ERR(TAG, "error no match"); + return; + } + if ((error = cliprdr_virtual_channel_event_data_received(cliprdr, pData, dataLength, + totalLength, dataFlags))) + WLog_ERR(TAG, "failed with error %" PRIu32 "", error); + + break; + + case CHANNEL_EVENT_WRITE_CANCELLED: + case CHANNEL_EVENT_WRITE_COMPLETE: + { + wStream* s = (wStream*)pData; + Stream_Free(s, TRUE); + } + break; + + case CHANNEL_EVENT_USER: + break; + } + + if (error && cliprdr && cliprdr->context->rdpcontext) + setChannelError(cliprdr->context->rdpcontext, error, + "cliprdr_virtual_channel_open_event_ex reported an error"); +} + +static DWORD WINAPI cliprdr_virtual_channel_client_thread(LPVOID arg) +{ + wStream* data; + wMessage message; + cliprdrPlugin* cliprdr = (cliprdrPlugin*)arg; + UINT error = CHANNEL_RC_OK; + + while (1) + { + if (!MessageQueue_Wait(cliprdr->queue)) + { + WLog_ERR(TAG, "MessageQueue_Wait failed!"); + error = ERROR_INTERNAL_ERROR; + break; + } + + if (!MessageQueue_Peek(cliprdr->queue, &message, TRUE)) + { + WLog_ERR(TAG, "MessageQueue_Peek failed!"); + error = ERROR_INTERNAL_ERROR; + break; + } + + if (message.id == WMQ_QUIT) + break; + + if (message.id == 0) + { + data = (wStream*)message.wParam; + + if ((error = cliprdr_order_recv(cliprdr, data))) + { + WLog_ERR(TAG, "cliprdr_order_recv failed with error %" PRIu32 "!", error); + break; + } + } + } + + if (error && cliprdr->context->rdpcontext) + setChannelError(cliprdr->context->rdpcontext, error, + "cliprdr_virtual_channel_client_thread reported an error"); + + ExitThread(error); + return error; +} + +static void cliprdr_free_msg(void* obj) +{ + wMessage* msg = (wMessage*)obj; + + if (msg) + { + wStream* s = (wStream*)msg->wParam; + Stream_Free(s, TRUE); + } +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT cliprdr_virtual_channel_event_connected(cliprdrPlugin* cliprdr, LPVOID pData, + UINT32 dataLength) +{ + UINT32 status; + wObject obj = { 0 }; + status = cliprdr->channelEntryPoints.pVirtualChannelOpenEx( + cliprdr->InitHandle, &cliprdr->OpenHandle, cliprdr->channelDef.name, + cliprdr_virtual_channel_open_event_ex); + + if (status != CHANNEL_RC_OK) + { + WLog_ERR(TAG, "pVirtualChannelOpen failed with %s [%08" PRIX32 "]", + WTSErrorToString(status), status); + return status; + } + + obj.fnObjectFree = cliprdr_free_msg; + cliprdr->queue = MessageQueue_New(&obj); + + if (!cliprdr->queue) + { + WLog_ERR(TAG, "MessageQueue_New failed!"); + return ERROR_NOT_ENOUGH_MEMORY; + } + + if (!(cliprdr->thread = CreateThread(NULL, 0, cliprdr_virtual_channel_client_thread, + (void*)cliprdr, 0, NULL))) + { + WLog_ERR(TAG, "CreateThread failed!"); + MessageQueue_Free(cliprdr->queue); + cliprdr->queue = NULL; + return ERROR_INTERNAL_ERROR; + } + + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT cliprdr_virtual_channel_event_disconnected(cliprdrPlugin* cliprdr) +{ + UINT rc; + + if (cliprdr->OpenHandle == 0) + return CHANNEL_RC_OK; + + if (MessageQueue_PostQuit(cliprdr->queue, 0) && + (WaitForSingleObject(cliprdr->thread, INFINITE) == WAIT_FAILED)) + { + rc = GetLastError(); + WLog_ERR(TAG, "WaitForSingleObject failed with error %" PRIu32 "", rc); + return rc; + } + + MessageQueue_Free(cliprdr->queue); + CloseHandle(cliprdr->thread); + rc = cliprdr->channelEntryPoints.pVirtualChannelCloseEx(cliprdr->InitHandle, + cliprdr->OpenHandle); + + if (CHANNEL_RC_OK != rc) + { + WLog_ERR(TAG, "pVirtualChannelClose failed with %s [%08" PRIX32 "]", WTSErrorToString(rc), + rc); + return rc; + } + + cliprdr->OpenHandle = 0; + + if (cliprdr->data_in) + { + Stream_Free(cliprdr->data_in, TRUE); + cliprdr->data_in = NULL; + } + + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT cliprdr_virtual_channel_event_terminated(cliprdrPlugin* cliprdr) +{ + cliprdr->InitHandle = 0; + free(cliprdr->context); + free(cliprdr); + return CHANNEL_RC_OK; +} + +static VOID VCAPITYPE cliprdr_virtual_channel_init_event_ex(LPVOID lpUserParam, LPVOID pInitHandle, + UINT event, LPVOID pData, + UINT dataLength) +{ + UINT error = CHANNEL_RC_OK; + cliprdrPlugin* cliprdr = (cliprdrPlugin*)lpUserParam; + + if (!cliprdr || (cliprdr->InitHandle != pInitHandle)) + { + WLog_ERR(TAG, "error no match"); + return; + } + + switch (event) + { + case CHANNEL_EVENT_CONNECTED: + if ((error = cliprdr_virtual_channel_event_connected(cliprdr, pData, dataLength))) + WLog_ERR(TAG, + "cliprdr_virtual_channel_event_connected failed with error %" PRIu32 "!", + error); + + break; + + case CHANNEL_EVENT_DISCONNECTED: + if ((error = cliprdr_virtual_channel_event_disconnected(cliprdr))) + WLog_ERR(TAG, + "cliprdr_virtual_channel_event_disconnected failed with error %" PRIu32 + "!", + error); + + break; + + case CHANNEL_EVENT_TERMINATED: + if ((error = cliprdr_virtual_channel_event_terminated(cliprdr))) + WLog_ERR(TAG, + "cliprdr_virtual_channel_event_terminated failed with error %" PRIu32 "!", + error); + + break; + } + + if (error && cliprdr->context->rdpcontext) + setChannelError(cliprdr->context->rdpcontext, error, + "cliprdr_virtual_channel_init_event reported an error"); +} + +/* cliprdr is always built-in */ +#define VirtualChannelEntryEx cliprdr_VirtualChannelEntryEx + +BOOL VCAPITYPE VirtualChannelEntryEx(PCHANNEL_ENTRY_POINTS pEntryPoints, PVOID pInitHandle) +{ + UINT rc; + cliprdrPlugin* cliprdr; + CliprdrClientContext* context = NULL; + CHANNEL_ENTRY_POINTS_FREERDP_EX* pEntryPointsEx; + cliprdr = (cliprdrPlugin*)calloc(1, sizeof(cliprdrPlugin)); + + if (!cliprdr) + { + WLog_ERR(TAG, "calloc failed!"); + return FALSE; + } + + cliprdr->channelDef.options = CHANNEL_OPTION_INITIALIZED | CHANNEL_OPTION_ENCRYPT_RDP | + CHANNEL_OPTION_COMPRESS_RDP | CHANNEL_OPTION_SHOW_PROTOCOL; + sprintf_s(cliprdr->channelDef.name, ARRAYSIZE(cliprdr->channelDef.name), "cliprdr"); + pEntryPointsEx = (CHANNEL_ENTRY_POINTS_FREERDP_EX*)pEntryPoints; + + if ((pEntryPointsEx->cbSize >= sizeof(CHANNEL_ENTRY_POINTS_FREERDP_EX)) && + (pEntryPointsEx->MagicNumber == FREERDP_CHANNEL_MAGIC_NUMBER)) + { + context = (CliprdrClientContext*)calloc(1, sizeof(CliprdrClientContext)); + + if (!context) + { + free(cliprdr); + WLog_ERR(TAG, "calloc failed!"); + return FALSE; + } + + context->handle = (void*)cliprdr; + context->custom = NULL; + context->ClientCapabilities = cliprdr_client_capabilities; + context->TempDirectory = cliprdr_temp_directory; + context->ClientFormatList = cliprdr_client_format_list; + context->ClientFormatListResponse = cliprdr_client_format_list_response; + context->ClientLockClipboardData = cliprdr_client_lock_clipboard_data; + context->ClientUnlockClipboardData = cliprdr_client_unlock_clipboard_data; + context->ClientFormatDataRequest = cliprdr_client_format_data_request; + context->ClientFormatDataResponse = cliprdr_client_format_data_response; + context->ClientFileContentsRequest = cliprdr_client_file_contents_request; + context->ClientFileContentsResponse = cliprdr_client_file_contents_response; + cliprdr->context = context; + context->rdpcontext = pEntryPointsEx->context; + } + + cliprdr->log = WLog_Get("com.freerdp.channels.cliprdr.client"); + WLog_Print(cliprdr->log, WLOG_DEBUG, "VirtualChannelEntryEx"); + CopyMemory(&(cliprdr->channelEntryPoints), pEntryPoints, + sizeof(CHANNEL_ENTRY_POINTS_FREERDP_EX)); + cliprdr->InitHandle = pInitHandle; + rc = cliprdr->channelEntryPoints.pVirtualChannelInitEx( + cliprdr, context, pInitHandle, &cliprdr->channelDef, 1, VIRTUAL_CHANNEL_VERSION_WIN2000, + cliprdr_virtual_channel_init_event_ex); + + if (CHANNEL_RC_OK != rc) + { + WLog_ERR(TAG, "pVirtualChannelInit failed with %s [%08" PRIX32 "]", WTSErrorToString(rc), + rc); + free(cliprdr->context); + free(cliprdr); + return FALSE; + } + + cliprdr->channelEntryPoints.pInterface = context; + return TRUE; +} diff --git a/channels/cliprdr/client/cliprdr_main.h b/channels/cliprdr/client/cliprdr_main.h new file mode 100644 index 0000000..b6cd7db --- /dev/null +++ b/channels/cliprdr/client/cliprdr_main.h @@ -0,0 +1,67 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Clipboard Virtual Channel + * + * Copyright 2009-2011 Jay Sorg + * Copyright 2010-2011 Vic Lee + * Copyright 2015 DI (FH) Martin Haimberger + * + * Licensed 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 FREERDP_CHANNEL_CLIPRDR_CLIENT_MAIN_H +#define FREERDP_CHANNEL_CLIPRDR_CLIENT_MAIN_H + +#include + +#include +#include +#include + +#define TAG CHANNELS_TAG("cliprdr.client") + +struct cliprdr_plugin +{ + CHANNEL_DEF channelDef; + CHANNEL_ENTRY_POINTS_FREERDP_EX channelEntryPoints; + + CliprdrClientContext* context; + + wLog* log; + HANDLE thread; + wStream* data_in; + void* InitHandle; + DWORD OpenHandle; + wMessageQueue* queue; + + BOOL capabilitiesReceived; + BOOL useLongFormatNames; + BOOL streamFileClipEnabled; + BOOL fileClipNoFilePaths; + BOOL canLockClipData; + BOOL hasHugeFileSupport; +}; +typedef struct cliprdr_plugin cliprdrPlugin; + +CliprdrClientContext* cliprdr_get_client_interface(cliprdrPlugin* cliprdr); + +#ifdef WITH_DEBUG_CLIPRDR +#define DEBUG_CLIPRDR(...) WLog_DBG(TAG, __VA_ARGS__) +#else +#define DEBUG_CLIPRDR(...) \ + do \ + { \ + } while (0) +#endif + +#endif /* FREERDP_CHANNEL_CLIPRDR_CLIENT_MAIN_H */ diff --git a/channels/cliprdr/cliprdr_common.c b/channels/cliprdr/cliprdr_common.c new file mode 100644 index 0000000..69157ad --- /dev/null +++ b/channels/cliprdr/cliprdr_common.c @@ -0,0 +1,588 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Cliprdr common + * + * Copyright 2013 Marc-Andre Moreau + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger + * Copyright 2019 Kobi Mizrachi + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include + +#define TAG CHANNELS_TAG("cliprdr.common") + +#include "cliprdr_common.h" + +static BOOL cliprdr_validate_file_contents_request(const CLIPRDR_FILE_CONTENTS_REQUEST* request) +{ + /* + * [MS-RDPECLIP] 2.2.5.3 File Contents Request PDU (CLIPRDR_FILECONTENTS_REQUEST). + * + * A request for the size of the file identified by the lindex field. The size MUST be + * returned as a 64-bit, unsigned integer. The cbRequested field MUST be set to + * 0x00000008 and both the nPositionLow and nPositionHigh fields MUST be + * set to 0x00000000. + */ + + if (request->dwFlags & FILECONTENTS_SIZE) + { + if (request->cbRequested != sizeof(UINT64)) + { + WLog_ERR(TAG, "[%s]: cbRequested must be %" PRIu32 ", got %" PRIu32 "", __FUNCTION__, + sizeof(UINT64), request->cbRequested); + return FALSE; + } + + if (request->nPositionHigh != 0 || request->nPositionLow != 0) + { + WLog_ERR(TAG, "[%s]: nPositionHigh and nPositionLow must be set to 0", __FUNCTION__); + return FALSE; + } + } + + return TRUE; +} + +wStream* cliprdr_packet_new(UINT16 msgType, UINT16 msgFlags, UINT32 dataLen) +{ + wStream* s; + s = Stream_New(NULL, dataLen + 8); + + if (!s) + { + WLog_ERR(TAG, "Stream_New failed!"); + return NULL; + } + + Stream_Write_UINT16(s, msgType); + Stream_Write_UINT16(s, msgFlags); + /* Write actual length after the entire packet has been constructed. */ + Stream_Seek(s, 4); + return s; +} + +static void cliprdr_write_file_contents_request(wStream* s, + const CLIPRDR_FILE_CONTENTS_REQUEST* request) +{ + Stream_Write_UINT32(s, request->streamId); /* streamId (4 bytes) */ + Stream_Write_UINT32(s, request->listIndex); /* listIndex (4 bytes) */ + Stream_Write_UINT32(s, request->dwFlags); /* dwFlags (4 bytes) */ + Stream_Write_UINT32(s, request->nPositionLow); /* nPositionLow (4 bytes) */ + Stream_Write_UINT32(s, request->nPositionHigh); /* nPositionHigh (4 bytes) */ + Stream_Write_UINT32(s, request->cbRequested); /* cbRequested (4 bytes) */ + + if (request->haveClipDataId) + Stream_Write_UINT32(s, request->clipDataId); /* clipDataId (4 bytes) */ +} + +static INLINE void cliprdr_write_lock_unlock_clipdata(wStream* s, UINT32 clipDataId) +{ + Stream_Write_UINT32(s, clipDataId); +} + +static void cliprdr_write_lock_clipdata(wStream* s, + const CLIPRDR_LOCK_CLIPBOARD_DATA* lockClipboardData) +{ + cliprdr_write_lock_unlock_clipdata(s, lockClipboardData->clipDataId); +} + +static void cliprdr_write_unlock_clipdata(wStream* s, + const CLIPRDR_UNLOCK_CLIPBOARD_DATA* unlockClipboardData) +{ + cliprdr_write_lock_unlock_clipdata(s, unlockClipboardData->clipDataId); +} + +static void cliprdr_write_file_contents_response(wStream* s, + const CLIPRDR_FILE_CONTENTS_RESPONSE* response) +{ + Stream_Write_UINT32(s, response->streamId); /* streamId (4 bytes) */ + Stream_Write(s, response->requestedData, response->cbRequested); +} + +wStream* cliprdr_packet_lock_clipdata_new(const CLIPRDR_LOCK_CLIPBOARD_DATA* lockClipboardData) +{ + wStream* s; + + if (!lockClipboardData) + return NULL; + + s = cliprdr_packet_new(CB_LOCK_CLIPDATA, 0, 4); + + if (!s) + return NULL; + + cliprdr_write_lock_clipdata(s, lockClipboardData); + return s; +} + +wStream* +cliprdr_packet_unlock_clipdata_new(const CLIPRDR_UNLOCK_CLIPBOARD_DATA* unlockClipboardData) +{ + wStream* s; + + if (!unlockClipboardData) + return NULL; + + s = cliprdr_packet_new(CB_LOCK_CLIPDATA, 0, 4); + + if (!s) + return NULL; + + cliprdr_write_unlock_clipdata(s, unlockClipboardData); + return s; +} + +wStream* cliprdr_packet_file_contents_request_new(const CLIPRDR_FILE_CONTENTS_REQUEST* request) +{ + wStream* s; + + if (!request) + return NULL; + + s = cliprdr_packet_new(CB_FILECONTENTS_REQUEST, 0, 28); + + if (!s) + return NULL; + + cliprdr_write_file_contents_request(s, request); + return s; +} + +wStream* cliprdr_packet_file_contents_response_new(const CLIPRDR_FILE_CONTENTS_RESPONSE* response) +{ + wStream* s; + + if (!response) + return NULL; + + s = cliprdr_packet_new(CB_FILECONTENTS_RESPONSE, response->msgFlags, 4 + response->cbRequested); + + if (!s) + return NULL; + + cliprdr_write_file_contents_response(s, response); + return s; +} + +wStream* cliprdr_packet_format_list_new(const CLIPRDR_FORMAT_LIST* formatList, + BOOL useLongFormatNames) +{ + wStream* s; + UINT32 index; + int cchWideChar; + LPWSTR lpWideCharStr; + int formatNameSize; + char* szFormatName; + WCHAR* wszFormatName; + BOOL asciiNames = FALSE; + CLIPRDR_FORMAT* format; + UINT32 length; + + if (formatList->msgType != CB_FORMAT_LIST) + WLog_WARN(TAG, "[%s] called with invalid type %08" PRIx32, __FUNCTION__, + formatList->msgType); + + if (!useLongFormatNames) + { + length = formatList->numFormats * 36; + s = cliprdr_packet_new(CB_FORMAT_LIST, 0, length); + + if (!s) + { + WLog_ERR(TAG, "cliprdr_packet_new failed!"); + return NULL; + } + + for (index = 0; index < formatList->numFormats; index++) + { + size_t formatNameLength = 0; + format = (CLIPRDR_FORMAT*)&(formatList->formats[index]); + Stream_Write_UINT32(s, format->formatId); /* formatId (4 bytes) */ + formatNameSize = 0; + + szFormatName = format->formatName; + + if (asciiNames) + { + if (szFormatName) + formatNameLength = strnlen(szFormatName, 32); + + if (formatNameLength > 31) + formatNameLength = 31; + + Stream_Write(s, szFormatName, formatNameLength); + Stream_Zero(s, 32 - formatNameLength); + } + else + { + wszFormatName = NULL; + + if (szFormatName) + formatNameSize = + ConvertToUnicode(CP_UTF8, 0, szFormatName, -1, &wszFormatName, 0); + + if (formatNameSize < 0) + { + Stream_Free(s, TRUE); + return NULL; + } + + if (formatNameSize > 15) + formatNameSize = 15; + + /* size in bytes instead of wchar */ + formatNameSize *= 2; + + if (wszFormatName) + Stream_Write(s, wszFormatName, (size_t)formatNameSize); + + Stream_Zero(s, (size_t)(32 - formatNameSize)); + free(wszFormatName); + } + } + } + else + { + length = 0; + for (index = 0; index < formatList->numFormats; index++) + { + format = (CLIPRDR_FORMAT*)&(formatList->formats[index]); + length += 4; + formatNameSize = 2; + + if (format->formatName) + formatNameSize = + MultiByteToWideChar(CP_UTF8, 0, format->formatName, -1, NULL, 0) * 2; + + if (formatNameSize < 0) + return NULL; + + length += (UINT32)formatNameSize; + } + + s = cliprdr_packet_new(CB_FORMAT_LIST, 0, length); + + if (!s) + { + WLog_ERR(TAG, "cliprdr_packet_new failed!"); + return NULL; + } + + for (index = 0; index < formatList->numFormats; index++) + { + format = (CLIPRDR_FORMAT*)&(formatList->formats[index]); + Stream_Write_UINT32(s, format->formatId); /* formatId (4 bytes) */ + + if (format->formatName) + { + const size_t cap = Stream_Capacity(s); + const size_t pos = Stream_GetPosition(s); + const size_t rem = cap - pos; + if ((cap < pos) || ((rem / 2) > INT_MAX)) + { + Stream_Free(s, TRUE); + return NULL; + } + + lpWideCharStr = (LPWSTR)Stream_Pointer(s); + cchWideChar = (int)(rem / 2); + formatNameSize = MultiByteToWideChar(CP_UTF8, 0, format->formatName, -1, + lpWideCharStr, cchWideChar) * + 2; + if (formatNameSize < 0) + { + Stream_Free(s, TRUE); + return NULL; + } + Stream_Seek(s, (size_t)formatNameSize); + } + else + { + Stream_Write_UINT16(s, 0); + } + } + } + + return s; +} +UINT cliprdr_read_unlock_clipdata(wStream* s, CLIPRDR_UNLOCK_CLIPBOARD_DATA* unlockClipboardData) +{ + if (Stream_GetRemainingLength(s) < 4) + { + WLog_ERR(TAG, "not enough remaining data"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT32(s, unlockClipboardData->clipDataId); /* clipDataId (4 bytes) */ + return CHANNEL_RC_OK; +} + +UINT cliprdr_read_format_data_request(wStream* s, CLIPRDR_FORMAT_DATA_REQUEST* request) +{ + if (Stream_GetRemainingLength(s) < 4) + { + WLog_ERR(TAG, "not enough data in stream!"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT32(s, request->requestedFormatId); /* requestedFormatId (4 bytes) */ + return CHANNEL_RC_OK; +} + +UINT cliprdr_read_format_data_response(wStream* s, CLIPRDR_FORMAT_DATA_RESPONSE* response) +{ + response->requestedFormatData = NULL; + + if (Stream_GetRemainingLength(s) < response->dataLen) + { + WLog_ERR(TAG, "not enough data in stream!"); + return ERROR_INVALID_DATA; + } + + if (response->dataLen) + response->requestedFormatData = Stream_Pointer(s); + + return CHANNEL_RC_OK; +} + +UINT cliprdr_read_file_contents_request(wStream* s, CLIPRDR_FILE_CONTENTS_REQUEST* request) +{ + if (Stream_GetRemainingLength(s) < 24) + { + WLog_ERR(TAG, "not enough remaining data"); + return ERROR_INVALID_DATA; + } + + request->haveClipDataId = FALSE; + Stream_Read_UINT32(s, request->streamId); /* streamId (4 bytes) */ + Stream_Read_UINT32(s, request->listIndex); /* listIndex (4 bytes) */ + Stream_Read_UINT32(s, request->dwFlags); /* dwFlags (4 bytes) */ + Stream_Read_UINT32(s, request->nPositionLow); /* nPositionLow (4 bytes) */ + Stream_Read_UINT32(s, request->nPositionHigh); /* nPositionHigh (4 bytes) */ + Stream_Read_UINT32(s, request->cbRequested); /* cbRequested (4 bytes) */ + + if (Stream_GetRemainingLength(s) >= 4) + { + Stream_Read_UINT32(s, request->clipDataId); /* clipDataId (4 bytes) */ + request->haveClipDataId = TRUE; + } + + if (!cliprdr_validate_file_contents_request(request)) + return ERROR_BAD_ARGUMENTS; + + return CHANNEL_RC_OK; +} + +UINT cliprdr_read_file_contents_response(wStream* s, CLIPRDR_FILE_CONTENTS_RESPONSE* response) +{ + if (Stream_GetRemainingLength(s) < 4) + { + WLog_ERR(TAG, "not enough remaining data"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT32(s, response->streamId); /* streamId (4 bytes) */ + response->requestedData = Stream_Pointer(s); /* requestedFileContentsData */ + response->cbRequested = response->dataLen - 4; + return CHANNEL_RC_OK; +} + +UINT cliprdr_read_format_list(wStream* s, CLIPRDR_FORMAT_LIST* formatList, BOOL useLongFormatNames) +{ + UINT32 index; + BOOL asciiNames; + int formatNameLength; + char* szFormatName; + WCHAR* wszFormatName; + wStream sub1, sub2; + CLIPRDR_FORMAT* formats = NULL; + UINT error = ERROR_INTERNAL_ERROR; + + asciiNames = (formatList->msgFlags & CB_ASCII_NAMES) ? TRUE : FALSE; + + index = 0; + /* empty format list */ + formatList->formats = NULL; + formatList->numFormats = 0; + + Stream_StaticInit(&sub1, Stream_Pointer(s), formatList->dataLen); + if (!Stream_SafeSeek(s, formatList->dataLen)) + return ERROR_INVALID_DATA; + + if (!formatList->dataLen) + { + } + else if (!useLongFormatNames) + { + const size_t cap = Stream_Capacity(&sub1); + formatList->numFormats = (cap / 36); + + if ((formatList->numFormats * 36) != cap) + { + WLog_ERR(TAG, "Invalid short format list length: %" PRIuz "", cap); + return ERROR_INTERNAL_ERROR; + } + + if (formatList->numFormats) + formats = (CLIPRDR_FORMAT*)calloc(formatList->numFormats, sizeof(CLIPRDR_FORMAT)); + + if (!formats) + { + WLog_ERR(TAG, "calloc failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + formatList->formats = formats; + + while (Stream_GetRemainingLength(&sub1) >= 4) + { + Stream_Read_UINT32(&sub1, formats[index].formatId); /* formatId (4 bytes) */ + + formats[index].formatName = NULL; + + /* According to MS-RDPECLIP 2.2.3.1.1.1 formatName is "a 32-byte block containing + * the *null-terminated* name assigned to the Clipboard Format: (32 ASCII 8 characters + * or 16 Unicode characters)" + * However, both Windows RDSH and mstsc violate this specs as seen in the following + * example of a transferred short format name string: [R.i.c.h. .T.e.x.t. .F.o.r.m.a.t.] + * These are 16 unicode charaters - *without* terminating null ! + */ + + szFormatName = (char*)Stream_Pointer(&sub1); + wszFormatName = (WCHAR*)Stream_Pointer(&sub1); + if (!Stream_SafeSeek(&sub1, 32)) + goto error_out; + if (asciiNames) + { + if (szFormatName[0]) + { + /* ensure null termination */ + formats[index].formatName = (char*)malloc(32 + 1); + if (!formats[index].formatName) + { + WLog_ERR(TAG, "malloc failed!"); + error = CHANNEL_RC_NO_MEMORY; + goto error_out; + } + CopyMemory(formats[index].formatName, szFormatName, 32); + formats[index].formatName[32] = '\0'; + } + } + else + { + if (wszFormatName[0]) + { + /* ConvertFromUnicode always returns a null-terminated + * string on success, even if the source string isn't. + */ + if (ConvertFromUnicode(CP_UTF8, 0, wszFormatName, 16, + &(formats[index].formatName), 0, NULL, NULL) < 1) + { + WLog_ERR(TAG, "failed to convert short clipboard format name"); + error = ERROR_INTERNAL_ERROR; + goto error_out; + } + } + } + + index++; + } + } + else + { + sub2 = sub1; + while (Stream_GetRemainingLength(&sub1) > 0) + { + size_t rest; + if (!Stream_SafeSeek(&sub1, 4)) /* formatId (4 bytes) */ + goto error_out; + + wszFormatName = (WCHAR*)Stream_Pointer(&sub1); + rest = Stream_GetRemainingLength(&sub1); + formatNameLength = _wcsnlen(wszFormatName, rest / sizeof(WCHAR)); + + if (!Stream_SafeSeek(&sub1, (formatNameLength + 1) * sizeof(WCHAR))) + goto error_out; + formatList->numFormats++; + } + + if (formatList->numFormats) + formats = (CLIPRDR_FORMAT*)calloc(formatList->numFormats, sizeof(CLIPRDR_FORMAT)); + + if (!formats) + { + WLog_ERR(TAG, "calloc failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + formatList->formats = formats; + + while (Stream_GetRemainingLength(&sub2) >= 4) + { + size_t rest; + Stream_Read_UINT32(&sub2, formats[index].formatId); /* formatId (4 bytes) */ + + formats[index].formatName = NULL; + + wszFormatName = (WCHAR*)Stream_Pointer(&sub2); + rest = Stream_GetRemainingLength(&sub2); + formatNameLength = _wcsnlen(wszFormatName, rest / sizeof(WCHAR)); + if (!Stream_SafeSeek(&sub2, (formatNameLength + 1) * sizeof(WCHAR))) + goto error_out; + + if (formatNameLength) + { + if (ConvertFromUnicode(CP_UTF8, 0, wszFormatName, formatNameLength, + &(formats[index].formatName), 0, NULL, NULL) < 1) + { + WLog_ERR(TAG, "failed to convert long clipboard format name"); + error = ERROR_INTERNAL_ERROR; + goto error_out; + } + } + + index++; + } + } + + return CHANNEL_RC_OK; + +error_out: + cliprdr_free_format_list(formatList); + return error; +} + +void cliprdr_free_format_list(CLIPRDR_FORMAT_LIST* formatList) +{ + UINT index = 0; + + if (formatList == NULL) + return; + + if (formatList->formats) + { + for (index = 0; index < formatList->numFormats; index++) + { + free(formatList->formats[index].formatName); + } + + free(formatList->formats); + formatList->formats = NULL; + formatList->numFormats = 0; + } +} diff --git a/channels/cliprdr/cliprdr_common.h b/channels/cliprdr/cliprdr_common.h new file mode 100644 index 0000000..b5d36b9 --- /dev/null +++ b/channels/cliprdr/cliprdr_common.h @@ -0,0 +1,61 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Cliprdr common + * + * Copyright 2013 Marc-Andre Moreau + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger + * Copyright 2019 Kobi Mizrachi + * + * Licensed 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 FREERDP_CHANNEL_RDPECLIP_COMMON_H +#define FREERDP_CHANNEL_RDPECLIP_COMMON_H + +#include +#include + +#include +#include + +FREERDP_LOCAL wStream* cliprdr_packet_new(UINT16 msgType, UINT16 msgFlags, UINT32 dataLen); +FREERDP_LOCAL wStream* +cliprdr_packet_lock_clipdata_new(const CLIPRDR_LOCK_CLIPBOARD_DATA* lockClipboardData); +FREERDP_LOCAL wStream* +cliprdr_packet_unlock_clipdata_new(const CLIPRDR_UNLOCK_CLIPBOARD_DATA* unlockClipboardData); +FREERDP_LOCAL wStream* +cliprdr_packet_file_contents_request_new(const CLIPRDR_FILE_CONTENTS_REQUEST* request); +FREERDP_LOCAL wStream* +cliprdr_packet_file_contents_response_new(const CLIPRDR_FILE_CONTENTS_RESPONSE* response); +FREERDP_LOCAL wStream* cliprdr_packet_format_list_new(const CLIPRDR_FORMAT_LIST* formatList, + BOOL useLongFormatNames); + +FREERDP_LOCAL UINT cliprdr_read_lock_clipdata(wStream* s, + CLIPRDR_LOCK_CLIPBOARD_DATA* lockClipboardData); +FREERDP_LOCAL UINT cliprdr_read_unlock_clipdata(wStream* s, + CLIPRDR_UNLOCK_CLIPBOARD_DATA* unlockClipboardData); +FREERDP_LOCAL UINT cliprdr_read_format_data_request(wStream* s, + CLIPRDR_FORMAT_DATA_REQUEST* formatDataRequest); +FREERDP_LOCAL UINT cliprdr_read_format_data_response(wStream* s, + CLIPRDR_FORMAT_DATA_RESPONSE* response); +FREERDP_LOCAL UINT +cliprdr_read_file_contents_request(wStream* s, CLIPRDR_FILE_CONTENTS_REQUEST* fileContentsRequest); +FREERDP_LOCAL UINT cliprdr_read_file_contents_response(wStream* s, + CLIPRDR_FILE_CONTENTS_RESPONSE* response); +FREERDP_LOCAL UINT cliprdr_read_format_list(wStream* s, CLIPRDR_FORMAT_LIST* formatList, + BOOL useLongFormatNames); + +FREERDP_LOCAL void cliprdr_free_format_list(CLIPRDR_FORMAT_LIST* formatList); + +#endif /* FREERDP_CHANNEL_RDPECLIP_COMMON_H */ diff --git a/channels/cliprdr/server/CMakeLists.txt b/channels/cliprdr/server/CMakeLists.txt new file mode 100644 index 0000000..32ffbaa --- /dev/null +++ b/channels/cliprdr/server/CMakeLists.txt @@ -0,0 +1,31 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2012 Marc-Andre Moreau +# +# Licensed 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. + +define_channel_server("cliprdr") + +set(${MODULE_PREFIX}_SRCS + cliprdr_main.c + cliprdr_main.h + ../cliprdr_common.h + ../cliprdr_common.c + ) + +add_channel_server_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} FALSE "VirtualChannelEntry") + +target_link_libraries(${MODULE_NAME} freerdp) + +set_property(TARGET ${MODULE_NAME} PROPERTY FOLDER "Channels/${CHANNEL_NAME}/Server") diff --git a/channels/cliprdr/server/cliprdr_main.c b/channels/cliprdr/server/cliprdr_main.c new file mode 100644 index 0000000..7e0b681 --- /dev/null +++ b/channels/cliprdr/server/cliprdr_main.c @@ -0,0 +1,1420 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Clipboard Virtual Channel Extension + * + * Copyright 2013 Marc-Andre Moreau + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include + +#include +#include "cliprdr_main.h" +#include "../cliprdr_common.h" + +/** + * Initialization Sequence\n + * Client Server\n + * | |\n + * |<----------------------Server Clipboard Capabilities PDU-----------------|\n + * |<-----------------------------Monitor Ready PDU--------------------------|\n + * |-----------------------Client Clipboard Capabilities PDU---------------->|\n + * |---------------------------Temporary Directory PDU---------------------->|\n + * |-------------------------------Format List PDU-------------------------->|\n + * |<--------------------------Format List Response PDU----------------------|\n + * + */ + +/** + * Data Transfer Sequences\n + * Shared Local\n + * Clipboard Owner Clipboard Owner\n + * | |\n + * |-------------------------------------------------------------------------|\n _ + * |-------------------------------Format List PDU-------------------------->|\n | + * |<--------------------------Format List Response PDU----------------------|\n _| Copy + * Sequence + * |<---------------------Lock Clipboard Data PDU (Optional)-----------------|\n + * |-------------------------------------------------------------------------|\n + * |-------------------------------------------------------------------------|\n _ + * |<--------------------------Format Data Request PDU-----------------------|\n | Paste + * Sequence Palette, + * |---------------------------Format Data Response PDU--------------------->|\n _| Metafile, + * File List Data + * |-------------------------------------------------------------------------|\n + * |-------------------------------------------------------------------------|\n _ + * |<------------------------Format Contents Request PDU---------------------|\n | Paste + * Sequence + * |-------------------------Format Contents Response PDU------------------->|\n _| File + * Stream Data + * |<---------------------Lock Clipboard Data PDU (Optional)-----------------|\n + * |-------------------------------------------------------------------------|\n + * + */ + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT cliprdr_server_packet_send(CliprdrServerPrivate* cliprdr, wStream* s) +{ + UINT rc; + size_t pos, size; + BOOL status; + UINT32 dataLen; + UINT32 written; + pos = Stream_GetPosition(s); + if ((pos < 8) || (pos > UINT32_MAX)) + { + rc = ERROR_NO_DATA; + goto fail; + } + + dataLen = (UINT32)(pos - 8); + Stream_SetPosition(s, 4); + Stream_Write_UINT32(s, dataLen); + Stream_SetPosition(s, pos); + size = Stream_Length(s); + if (size > UINT32_MAX) + { + rc = ERROR_INVALID_DATA; + goto fail; + } + + status = WTSVirtualChannelWrite(cliprdr->ChannelHandle, (PCHAR)Stream_Buffer(s), (UINT32)size, + &written); + rc = status ? CHANNEL_RC_OK : ERROR_INTERNAL_ERROR; +fail: + Stream_Free(s, TRUE); + return rc; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT cliprdr_server_capabilities(CliprdrServerContext* context, + const CLIPRDR_CAPABILITIES* capabilities) +{ + size_t offset = 0; + UINT32 x; + wStream* s; + CliprdrServerPrivate* cliprdr = (CliprdrServerPrivate*)context->handle; + + if (capabilities->msgType != CB_CLIP_CAPS) + WLog_WARN(TAG, "[%s] called with invalid type %08" PRIx32, __FUNCTION__, + capabilities->msgType); + + if (capabilities->cCapabilitiesSets > UINT16_MAX) + { + WLog_ERR(TAG, "Invalid number of capability sets in clipboard caps"); + return ERROR_INVALID_PARAMETER; + } + + s = cliprdr_packet_new(CB_CLIP_CAPS, 0, 4 + CB_CAPSTYPE_GENERAL_LEN); + + if (!s) + { + WLog_ERR(TAG, "cliprdr_packet_new failed!"); + return ERROR_INTERNAL_ERROR; + } + + Stream_Write_UINT16(s, + (UINT16)capabilities->cCapabilitiesSets); /* cCapabilitiesSets (2 bytes) */ + Stream_Write_UINT16(s, 0); /* pad1 (2 bytes) */ + for (x = 0; x < capabilities->cCapabilitiesSets; x++) + { + const CLIPRDR_CAPABILITY_SET* cap = + (const CLIPRDR_CAPABILITY_SET*)(((const BYTE*)capabilities->capabilitySets) + offset); + offset += cap->capabilitySetLength; + + switch (cap->capabilitySetType) + { + case CB_CAPSTYPE_GENERAL: + { + const CLIPRDR_GENERAL_CAPABILITY_SET* generalCapabilitySet = + (const CLIPRDR_GENERAL_CAPABILITY_SET*)cap; + Stream_Write_UINT16( + s, generalCapabilitySet->capabilitySetType); /* capabilitySetType (2 bytes) */ + Stream_Write_UINT16( + s, generalCapabilitySet->capabilitySetLength); /* lengthCapability (2 bytes) */ + Stream_Write_UINT32(s, generalCapabilitySet->version); /* version (4 bytes) */ + Stream_Write_UINT32( + s, generalCapabilitySet->generalFlags); /* generalFlags (4 bytes) */ + } + break; + + default: + WLog_WARN(TAG, "Unknown capability set type %08" PRIx16, cap->capabilitySetType); + if (!Stream_SafeSeek(s, cap->capabilitySetLength)) + { + WLog_ERR(TAG, "%s: short stream", __FUNCTION__); + return ERROR_NO_DATA; + } + break; + } + } + WLog_DBG(TAG, "ServerCapabilities"); + return cliprdr_server_packet_send(cliprdr, s); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT cliprdr_server_monitor_ready(CliprdrServerContext* context, + const CLIPRDR_MONITOR_READY* monitorReady) +{ + wStream* s; + CliprdrServerPrivate* cliprdr = (CliprdrServerPrivate*)context->handle; + + if (monitorReady->msgType != CB_MONITOR_READY) + WLog_WARN(TAG, "[%s] called with invalid type %08" PRIx32, __FUNCTION__, + monitorReady->msgType); + + s = cliprdr_packet_new(CB_MONITOR_READY, monitorReady->msgFlags, monitorReady->dataLen); + + if (!s) + { + WLog_ERR(TAG, "cliprdr_packet_new failed!"); + return ERROR_INTERNAL_ERROR; + } + + WLog_DBG(TAG, "ServerMonitorReady"); + return cliprdr_server_packet_send(cliprdr, s); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT cliprdr_server_format_list(CliprdrServerContext* context, + const CLIPRDR_FORMAT_LIST* formatList) +{ + wStream* s; + CliprdrServerPrivate* cliprdr = (CliprdrServerPrivate*)context->handle; + + s = cliprdr_packet_format_list_new(formatList, context->useLongFormatNames); + if (!s) + { + WLog_ERR(TAG, "cliprdr_packet_format_list_new failed!"); + return ERROR_INTERNAL_ERROR; + } + + WLog_DBG(TAG, "ServerFormatList: numFormats: %" PRIu32 "", formatList->numFormats); + return cliprdr_server_packet_send(cliprdr, s); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT +cliprdr_server_format_list_response(CliprdrServerContext* context, + const CLIPRDR_FORMAT_LIST_RESPONSE* formatListResponse) +{ + wStream* s; + CliprdrServerPrivate* cliprdr = (CliprdrServerPrivate*)context->handle; + if (formatListResponse->msgType != CB_FORMAT_LIST_RESPONSE) + WLog_WARN(TAG, "[%s] called with invalid type %08" PRIx32, __FUNCTION__, + formatListResponse->msgType); + + s = cliprdr_packet_new(CB_FORMAT_LIST_RESPONSE, formatListResponse->msgFlags, + formatListResponse->dataLen); + + if (!s) + { + WLog_ERR(TAG, "cliprdr_packet_new failed!"); + return ERROR_INTERNAL_ERROR; + } + + WLog_DBG(TAG, "ServerFormatListResponse"); + return cliprdr_server_packet_send(cliprdr, s); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT cliprdr_server_lock_clipboard_data(CliprdrServerContext* context, + const CLIPRDR_LOCK_CLIPBOARD_DATA* lockClipboardData) +{ + wStream* s; + CliprdrServerPrivate* cliprdr = (CliprdrServerPrivate*)context->handle; + if (lockClipboardData->msgType != CB_LOCK_CLIPDATA) + WLog_WARN(TAG, "[%s] called with invalid type %08" PRIx32, __FUNCTION__, + lockClipboardData->msgType); + + s = cliprdr_packet_lock_clipdata_new(lockClipboardData); + if (!s) + { + WLog_ERR(TAG, "cliprdr_packet_lock_clipdata_new failed!"); + return ERROR_INTERNAL_ERROR; + } + + WLog_DBG(TAG, "ServerLockClipboardData: clipDataId: 0x%08" PRIX32 "", + lockClipboardData->clipDataId); + return cliprdr_server_packet_send(cliprdr, s); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT +cliprdr_server_unlock_clipboard_data(CliprdrServerContext* context, + const CLIPRDR_UNLOCK_CLIPBOARD_DATA* unlockClipboardData) +{ + wStream* s; + CliprdrServerPrivate* cliprdr = (CliprdrServerPrivate*)context->handle; + if (unlockClipboardData->msgType != CB_UNLOCK_CLIPDATA) + WLog_WARN(TAG, "[%s] called with invalid type %08" PRIx32, __FUNCTION__, + unlockClipboardData->msgType); + + s = cliprdr_packet_unlock_clipdata_new(unlockClipboardData); + + if (!s) + { + WLog_ERR(TAG, "cliprdr_packet_unlock_clipdata_new failed!"); + return ERROR_INTERNAL_ERROR; + } + + WLog_DBG(TAG, "ServerUnlockClipboardData: clipDataId: 0x%08" PRIX32 "", + unlockClipboardData->clipDataId); + return cliprdr_server_packet_send(cliprdr, s); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT cliprdr_server_format_data_request(CliprdrServerContext* context, + const CLIPRDR_FORMAT_DATA_REQUEST* formatDataRequest) +{ + wStream* s; + CliprdrServerPrivate* cliprdr = (CliprdrServerPrivate*)context->handle; + if (formatDataRequest->msgType != CB_FORMAT_DATA_REQUEST) + WLog_WARN(TAG, "[%s] called with invalid type %08" PRIx32, __FUNCTION__, + formatDataRequest->msgType); + + s = cliprdr_packet_new(CB_FORMAT_DATA_REQUEST, formatDataRequest->msgFlags, + formatDataRequest->dataLen); + + if (!s) + { + WLog_ERR(TAG, "cliprdr_packet_new failed!"); + return ERROR_INTERNAL_ERROR; + } + + Stream_Write_UINT32(s, formatDataRequest->requestedFormatId); /* requestedFormatId (4 bytes) */ + WLog_DBG(TAG, "ClientFormatDataRequest"); + return cliprdr_server_packet_send(cliprdr, s); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT +cliprdr_server_format_data_response(CliprdrServerContext* context, + const CLIPRDR_FORMAT_DATA_RESPONSE* formatDataResponse) +{ + wStream* s; + CliprdrServerPrivate* cliprdr = (CliprdrServerPrivate*)context->handle; + + if (formatDataResponse->msgType != CB_FORMAT_DATA_RESPONSE) + WLog_WARN(TAG, "[%s] called with invalid type %08" PRIx32, __FUNCTION__, + formatDataResponse->msgType); + + s = cliprdr_packet_new(CB_FORMAT_DATA_RESPONSE, formatDataResponse->msgFlags, + formatDataResponse->dataLen); + + if (!s) + { + WLog_ERR(TAG, "cliprdr_packet_new failed!"); + return ERROR_INTERNAL_ERROR; + } + + Stream_Write(s, formatDataResponse->requestedFormatData, formatDataResponse->dataLen); + WLog_DBG(TAG, "ServerFormatDataResponse"); + return cliprdr_server_packet_send(cliprdr, s); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT +cliprdr_server_file_contents_request(CliprdrServerContext* context, + const CLIPRDR_FILE_CONTENTS_REQUEST* fileContentsRequest) +{ + wStream* s; + CliprdrServerPrivate* cliprdr = (CliprdrServerPrivate*)context->handle; + + if (fileContentsRequest->msgType != CB_FILECONTENTS_REQUEST) + WLog_WARN(TAG, "[%s] called with invalid type %08" PRIx32, __FUNCTION__, + fileContentsRequest->msgType); + + s = cliprdr_packet_file_contents_request_new(fileContentsRequest); + if (!s) + { + WLog_ERR(TAG, "cliprdr_packet_file_contents_request_new failed!"); + return ERROR_INTERNAL_ERROR; + } + + WLog_DBG(TAG, "ServerFileContentsRequest: streamId: 0x%08" PRIX32 "", + fileContentsRequest->streamId); + return cliprdr_server_packet_send(cliprdr, s); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT +cliprdr_server_file_contents_response(CliprdrServerContext* context, + const CLIPRDR_FILE_CONTENTS_RESPONSE* fileContentsResponse) +{ + wStream* s; + CliprdrServerPrivate* cliprdr = (CliprdrServerPrivate*)context->handle; + + if (fileContentsResponse->msgType != CB_FILECONTENTS_RESPONSE) + WLog_WARN(TAG, "[%s] called with invalid type %08" PRIx32, __FUNCTION__, + fileContentsResponse->msgType); + + s = cliprdr_packet_file_contents_response_new(fileContentsResponse); + if (!s) + { + WLog_ERR(TAG, "cliprdr_packet_file_contents_response_new failed!"); + return ERROR_INTERNAL_ERROR; + } + + WLog_DBG(TAG, "ServerFileContentsResponse: streamId: 0x%08" PRIX32 "", + fileContentsResponse->streamId); + return cliprdr_server_packet_send(cliprdr, s); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT cliprdr_server_receive_general_capability(CliprdrServerContext* context, wStream* s, + CLIPRDR_GENERAL_CAPABILITY_SET* cap_set) +{ + if (Stream_GetRemainingLength(s) < 8) + return ERROR_INVALID_DATA; + + Stream_Read_UINT32(s, cap_set->version); /* version (4 bytes) */ + Stream_Read_UINT32(s, cap_set->generalFlags); /* generalFlags (4 bytes) */ + + if (context->useLongFormatNames) + context->useLongFormatNames = + (cap_set->generalFlags & CB_USE_LONG_FORMAT_NAMES) ? TRUE : FALSE; + + if (context->streamFileClipEnabled) + context->streamFileClipEnabled = + (cap_set->generalFlags & CB_STREAM_FILECLIP_ENABLED) ? TRUE : FALSE; + + if (context->fileClipNoFilePaths) + context->fileClipNoFilePaths = + (cap_set->generalFlags & CB_FILECLIP_NO_FILE_PATHS) ? TRUE : FALSE; + + if (context->canLockClipData) + context->canLockClipData = (cap_set->generalFlags & CB_CAN_LOCK_CLIPDATA) ? TRUE : FALSE; + + if (context->hasHugeFileSupport) + context->hasHugeFileSupport = + (cap_set->generalFlags & CB_HUGE_FILE_SUPPORT_ENABLED) ? TRUE : FALSE; + + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT cliprdr_server_receive_capabilities(CliprdrServerContext* context, wStream* s, + const CLIPRDR_HEADER* header) +{ + UINT16 index; + UINT16 capabilitySetType; + UINT16 capabilitySetLength; + UINT error = ERROR_INVALID_DATA; + size_t cap_sets_size = 0; + CLIPRDR_CAPABILITIES capabilities = { 0 }; + CLIPRDR_CAPABILITY_SET* capSet; + + WINPR_UNUSED(header); + + + WLog_DBG(TAG, "CliprdrClientCapabilities"); + if (Stream_GetRemainingLength(s) < 4) + return ERROR_INVALID_DATA; + + Stream_Read_UINT16(s, capabilities.cCapabilitiesSets); /* cCapabilitiesSets (2 bytes) */ + Stream_Seek_UINT16(s); /* pad1 (2 bytes) */ + + for (index = 0; index < capabilities.cCapabilitiesSets; index++) + { + void* tmp = NULL; + if (Stream_GetRemainingLength(s) < 4) + goto out; + Stream_Read_UINT16(s, capabilitySetType); /* capabilitySetType (2 bytes) */ + Stream_Read_UINT16(s, capabilitySetLength); /* capabilitySetLength (2 bytes) */ + + cap_sets_size += capabilitySetLength; + + if (cap_sets_size > 0) + tmp = realloc(capabilities.capabilitySets, cap_sets_size); + if (tmp == NULL) + { + WLog_ERR(TAG, "capabilities.capabilitySets realloc failed!"); + free(capabilities.capabilitySets); + return CHANNEL_RC_NO_MEMORY; + } + + capabilities.capabilitySets = (CLIPRDR_CAPABILITY_SET*)tmp; + + capSet = &(capabilities.capabilitySets[index]); + + capSet->capabilitySetType = capabilitySetType; + capSet->capabilitySetLength = capabilitySetLength; + + switch (capSet->capabilitySetType) + { + case CB_CAPSTYPE_GENERAL: + error = cliprdr_server_receive_general_capability( + context, s, (CLIPRDR_GENERAL_CAPABILITY_SET*)capSet); + if (error) + { + WLog_ERR(TAG, + "cliprdr_server_receive_general_capability failed with error %" PRIu32 + "", + error); + goto out; + } + break; + + default: + WLog_ERR(TAG, "unknown cliprdr capability set: %" PRIu16 "", + capSet->capabilitySetType); + goto out; + } + } + + error = CHANNEL_RC_OK; + IFCALLRET(context->ClientCapabilities, error, context, &capabilities); +out: + free(capabilities.capabilitySets); + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT cliprdr_server_receive_temporary_directory(CliprdrServerContext* context, wStream* s, + const CLIPRDR_HEADER* header) +{ + size_t length; + WCHAR* wszTempDir; + CLIPRDR_TEMP_DIRECTORY tempDirectory; + CliprdrServerPrivate* cliprdr = (CliprdrServerPrivate*)context->handle; + size_t slength; + UINT error = CHANNEL_RC_OK; + + WINPR_UNUSED(header); + if ((slength = Stream_GetRemainingLength(s)) < 260 * sizeof(WCHAR)) + { + WLog_ERR(TAG, "Stream_GetRemainingLength returned %" PRIuz " but should at least be 520", + slength); + return CHANNEL_RC_NO_MEMORY; + } + + wszTempDir = (WCHAR*)Stream_Pointer(s); + + if (wszTempDir[259] != 0) + { + WLog_ERR(TAG, "wszTempDir[259] was not 0"); + return ERROR_INVALID_DATA; + } + + free(cliprdr->temporaryDirectory); + cliprdr->temporaryDirectory = NULL; + + if (ConvertFromUnicode(CP_UTF8, 0, wszTempDir, -1, &(cliprdr->temporaryDirectory), 0, NULL, + NULL) < 1) + { + WLog_ERR(TAG, "failed to convert temporary directory name"); + return ERROR_INVALID_DATA; + } + + length = strnlen(cliprdr->temporaryDirectory, 260); + + if (length >= 260) + length = 259; + + CopyMemory(tempDirectory.szTempDir, cliprdr->temporaryDirectory, length); + tempDirectory.szTempDir[length] = '\0'; + WLog_DBG(TAG, "CliprdrTemporaryDirectory: %s", cliprdr->temporaryDirectory); + IFCALLRET(context->TempDirectory, error, context, &tempDirectory); + + if (error) + WLog_ERR(TAG, "TempDirectory failed with error %" PRIu32 "!", error); + + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT cliprdr_server_receive_format_list(CliprdrServerContext* context, wStream* s, + const CLIPRDR_HEADER* header) +{ + CLIPRDR_FORMAT_LIST formatList; + UINT error = CHANNEL_RC_OK; + + formatList.msgType = CB_FORMAT_LIST; + formatList.msgFlags = header->msgFlags; + formatList.dataLen = header->dataLen; + + if ((error = cliprdr_read_format_list(s, &formatList, context->useLongFormatNames))) + goto out; + + WLog_DBG(TAG, "ClientFormatList: numFormats: %" PRIu32 "", formatList.numFormats); + IFCALLRET(context->ClientFormatList, error, context, &formatList); + + if (error) + WLog_ERR(TAG, "ClientFormatList failed with error %" PRIu32 "!", error); + +out: + cliprdr_free_format_list(&formatList); + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT cliprdr_server_receive_format_list_response(CliprdrServerContext* context, wStream* s, + const CLIPRDR_HEADER* header) +{ + CLIPRDR_FORMAT_LIST_RESPONSE formatListResponse; + UINT error = CHANNEL_RC_OK; + + WINPR_UNUSED(s); + WLog_DBG(TAG, "CliprdrClientFormatListResponse"); + formatListResponse.msgType = CB_FORMAT_LIST_RESPONSE; + formatListResponse.msgFlags = header->msgFlags; + formatListResponse.dataLen = header->dataLen; + IFCALLRET(context->ClientFormatListResponse, error, context, &formatListResponse); + + if (error) + WLog_ERR(TAG, "ClientFormatListResponse failed with error %" PRIu32 "!", error); + + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT cliprdr_server_receive_lock_clipdata(CliprdrServerContext* context, wStream* s, + const CLIPRDR_HEADER* header) +{ + CLIPRDR_LOCK_CLIPBOARD_DATA lockClipboardData; + UINT error = CHANNEL_RC_OK; + WLog_DBG(TAG, "CliprdrClientLockClipData"); + + if (Stream_GetRemainingLength(s) < 4) + { + WLog_ERR(TAG, "not enough data in stream!"); + return ERROR_INVALID_DATA; + } + + lockClipboardData.msgType = CB_LOCK_CLIPDATA; + lockClipboardData.msgFlags = header->msgFlags; + lockClipboardData.dataLen = header->dataLen; + Stream_Read_UINT32(s, lockClipboardData.clipDataId); /* clipDataId (4 bytes) */ + IFCALLRET(context->ClientLockClipboardData, error, context, &lockClipboardData); + + if (error) + WLog_ERR(TAG, "ClientLockClipboardData failed with error %" PRIu32 "!", error); + + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT cliprdr_server_receive_unlock_clipdata(CliprdrServerContext* context, wStream* s, + const CLIPRDR_HEADER* header) +{ + CLIPRDR_UNLOCK_CLIPBOARD_DATA unlockClipboardData; + UINT error = CHANNEL_RC_OK; + WLog_DBG(TAG, "CliprdrClientUnlockClipData"); + + unlockClipboardData.msgType = CB_UNLOCK_CLIPDATA; + unlockClipboardData.msgFlags = header->msgFlags; + unlockClipboardData.dataLen = header->dataLen; + + if ((error = cliprdr_read_unlock_clipdata(s, &unlockClipboardData))) + return error; + + IFCALLRET(context->ClientUnlockClipboardData, error, context, &unlockClipboardData); + + if (error) + WLog_ERR(TAG, "ClientUnlockClipboardData failed with error %" PRIu32 "!", error); + + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT cliprdr_server_receive_format_data_request(CliprdrServerContext* context, wStream* s, + const CLIPRDR_HEADER* header) +{ + CLIPRDR_FORMAT_DATA_REQUEST formatDataRequest; + UINT error = CHANNEL_RC_OK; + WLog_DBG(TAG, "CliprdrClientFormatDataRequest"); + formatDataRequest.msgType = CB_FORMAT_DATA_REQUEST; + formatDataRequest.msgFlags = header->msgFlags; + formatDataRequest.dataLen = header->dataLen; + + if ((error = cliprdr_read_format_data_request(s, &formatDataRequest))) + return error; + + context->lastRequestedFormatId = formatDataRequest.requestedFormatId; + IFCALLRET(context->ClientFormatDataRequest, error, context, &formatDataRequest); + + if (error) + WLog_ERR(TAG, "ClientFormatDataRequest failed with error %" PRIu32 "!", error); + + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT cliprdr_server_receive_format_data_response(CliprdrServerContext* context, wStream* s, + const CLIPRDR_HEADER* header) +{ + CLIPRDR_FORMAT_DATA_RESPONSE formatDataResponse; + UINT error = CHANNEL_RC_OK; + + WLog_DBG(TAG, "CliprdrClientFormatDataResponse"); + formatDataResponse.msgType = CB_FORMAT_DATA_RESPONSE; + formatDataResponse.msgFlags = header->msgFlags; + formatDataResponse.dataLen = header->dataLen; + + if ((error = cliprdr_read_format_data_response(s, &formatDataResponse))) + return error; + + IFCALLRET(context->ClientFormatDataResponse, error, context, &formatDataResponse); + + if (error) + WLog_ERR(TAG, "ClientFormatDataResponse failed with error %" PRIu32 "!", error); + + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT cliprdr_server_receive_filecontents_request(CliprdrServerContext* context, wStream* s, + const CLIPRDR_HEADER* header) +{ + CLIPRDR_FILE_CONTENTS_REQUEST request; + UINT error = CHANNEL_RC_OK; + WLog_DBG(TAG, "CliprdrClientFileContentsRequest"); + request.msgType = CB_FILECONTENTS_REQUEST; + request.msgFlags = header->msgFlags; + request.dataLen = header->dataLen; + + if ((error = cliprdr_read_file_contents_request(s, &request))) + return error; + + if (!context->hasHugeFileSupport) + { + if (request.nPositionHigh > 0) + return ERROR_INVALID_DATA; + if ((UINT64)request.nPositionLow + request.cbRequested > UINT32_MAX) + return ERROR_INVALID_DATA; + } + IFCALLRET(context->ClientFileContentsRequest, error, context, &request); + + if (error) + WLog_ERR(TAG, "ClientFileContentsRequest failed with error %" PRIu32 "!", error); + + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT cliprdr_server_receive_filecontents_response(CliprdrServerContext* context, wStream* s, + const CLIPRDR_HEADER* header) +{ + CLIPRDR_FILE_CONTENTS_RESPONSE response; + UINT error = CHANNEL_RC_OK; + WLog_DBG(TAG, "CliprdrClientFileContentsResponse"); + + response.msgType = CB_FILECONTENTS_RESPONSE; + response.msgFlags = header->msgFlags; + response.dataLen = header->dataLen; + + if ((error = cliprdr_read_file_contents_response(s, &response))) + return error; + + IFCALLRET(context->ClientFileContentsResponse, error, context, &response); + + if (error) + WLog_ERR(TAG, "ClientFileContentsResponse failed with error %" PRIu32 "!", error); + + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT cliprdr_server_receive_pdu(CliprdrServerContext* context, wStream* s, + const CLIPRDR_HEADER* header) +{ + UINT error; + WLog_DBG(TAG, + "CliprdrServerReceivePdu: msgType: %" PRIu16 " msgFlags: 0x%04" PRIX16 + " dataLen: %" PRIu32 "", + header->msgType, header->msgFlags, header->dataLen); + + switch (header->msgType) + { + case CB_CLIP_CAPS: + if ((error = cliprdr_server_receive_capabilities(context, s, header))) + WLog_ERR(TAG, "cliprdr_server_receive_capabilities failed with error %" PRIu32 "!", + error); + + break; + + case CB_TEMP_DIRECTORY: + if ((error = cliprdr_server_receive_temporary_directory(context, s, header))) + WLog_ERR(TAG, + "cliprdr_server_receive_temporary_directory failed with error %" PRIu32 + "!", + error); + + break; + + case CB_FORMAT_LIST: + if ((error = cliprdr_server_receive_format_list(context, s, header))) + WLog_ERR(TAG, "cliprdr_server_receive_format_list failed with error %" PRIu32 "!", + error); + + break; + + case CB_FORMAT_LIST_RESPONSE: + if ((error = cliprdr_server_receive_format_list_response(context, s, header))) + WLog_ERR(TAG, + "cliprdr_server_receive_format_list_response failed with error %" PRIu32 + "!", + error); + + break; + + case CB_LOCK_CLIPDATA: + if ((error = cliprdr_server_receive_lock_clipdata(context, s, header))) + WLog_ERR(TAG, "cliprdr_server_receive_lock_clipdata failed with error %" PRIu32 "!", + error); + + break; + + case CB_UNLOCK_CLIPDATA: + if ((error = cliprdr_server_receive_unlock_clipdata(context, s, header))) + WLog_ERR(TAG, + "cliprdr_server_receive_unlock_clipdata failed with error %" PRIu32 "!", + error); + + break; + + case CB_FORMAT_DATA_REQUEST: + if ((error = cliprdr_server_receive_format_data_request(context, s, header))) + WLog_ERR(TAG, + "cliprdr_server_receive_format_data_request failed with error %" PRIu32 + "!", + error); + + break; + + case CB_FORMAT_DATA_RESPONSE: + if ((error = cliprdr_server_receive_format_data_response(context, s, header))) + WLog_ERR(TAG, + "cliprdr_server_receive_format_data_response failed with error %" PRIu32 + "!", + error); + + break; + + case CB_FILECONTENTS_REQUEST: + if ((error = cliprdr_server_receive_filecontents_request(context, s, header))) + WLog_ERR(TAG, + "cliprdr_server_receive_filecontents_request failed with error %" PRIu32 + "!", + error); + + break; + + case CB_FILECONTENTS_RESPONSE: + if ((error = cliprdr_server_receive_filecontents_response(context, s, header))) + WLog_ERR(TAG, + "cliprdr_server_receive_filecontents_response failed with error %" PRIu32 + "!", + error); + + break; + + default: + error = ERROR_INVALID_DATA; + WLog_ERR(TAG, "Unexpected clipboard PDU type: %" PRIu16 "", header->msgType); + break; + } + + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT cliprdr_server_init(CliprdrServerContext* context) +{ + UINT32 generalFlags; + CLIPRDR_GENERAL_CAPABILITY_SET generalCapabilitySet; + UINT error; + CLIPRDR_MONITOR_READY monitorReady = { 0 }; + CLIPRDR_CAPABILITIES capabilities = { 0 }; + + generalFlags = 0; + monitorReady.msgType = CB_MONITOR_READY; + capabilities.msgType = CB_CLIP_CAPS; + + if (context->useLongFormatNames) + generalFlags |= CB_USE_LONG_FORMAT_NAMES; + + if (context->streamFileClipEnabled) + generalFlags |= CB_STREAM_FILECLIP_ENABLED; + + if (context->fileClipNoFilePaths) + generalFlags |= CB_FILECLIP_NO_FILE_PATHS; + + if (context->canLockClipData) + generalFlags |= CB_CAN_LOCK_CLIPDATA; + + if (context->hasHugeFileSupport) + generalFlags |= CB_HUGE_FILE_SUPPORT_ENABLED; + + capabilities.msgType = CB_CLIP_CAPS; + capabilities.msgFlags = 0; + capabilities.dataLen = 4 + CB_CAPSTYPE_GENERAL_LEN; + capabilities.cCapabilitiesSets = 1; + capabilities.capabilitySets = (CLIPRDR_CAPABILITY_SET*)&generalCapabilitySet; + generalCapabilitySet.capabilitySetType = CB_CAPSTYPE_GENERAL; + generalCapabilitySet.capabilitySetLength = CB_CAPSTYPE_GENERAL_LEN; + generalCapabilitySet.version = CB_CAPS_VERSION_2; + generalCapabilitySet.generalFlags = generalFlags; + + if ((error = context->ServerCapabilities(context, &capabilities))) + { + WLog_ERR(TAG, "ServerCapabilities failed with error %" PRIu32 "!", error); + return error; + } + + if ((error = context->MonitorReady(context, &monitorReady))) + { + WLog_ERR(TAG, "MonitorReady failed with error %" PRIu32 "!", error); + return error; + } + + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT cliprdr_server_read(CliprdrServerContext* context) +{ + wStream* s; + size_t position; + DWORD BytesToRead; + DWORD BytesReturned; + CLIPRDR_HEADER header; + CliprdrServerPrivate* cliprdr = (CliprdrServerPrivate*)context->handle; + UINT error; + DWORD status; + s = cliprdr->s; + + if (Stream_GetPosition(s) < CLIPRDR_HEADER_LENGTH) + { + BytesReturned = 0; + BytesToRead = (UINT32)(CLIPRDR_HEADER_LENGTH - Stream_GetPosition(s)); + status = WaitForSingleObject(cliprdr->ChannelEvent, 0); + + if (status == WAIT_FAILED) + { + error = GetLastError(); + WLog_ERR(TAG, "WaitForSingleObject failed with error %" PRIu32 "", error); + return error; + } + + if (status == WAIT_TIMEOUT) + return CHANNEL_RC_OK; + + if (!WTSVirtualChannelRead(cliprdr->ChannelHandle, 0, (PCHAR)Stream_Pointer(s), BytesToRead, + &BytesReturned)) + { + WLog_ERR(TAG, "WTSVirtualChannelRead failed!"); + return ERROR_INTERNAL_ERROR; + } + + Stream_Seek(s, BytesReturned); + } + + if (Stream_GetPosition(s) >= CLIPRDR_HEADER_LENGTH) + { + position = Stream_GetPosition(s); + Stream_SetPosition(s, 0); + Stream_Read_UINT16(s, header.msgType); /* msgType (2 bytes) */ + Stream_Read_UINT16(s, header.msgFlags); /* msgFlags (2 bytes) */ + Stream_Read_UINT32(s, header.dataLen); /* dataLen (4 bytes) */ + + if (!Stream_EnsureCapacity(s, (header.dataLen + CLIPRDR_HEADER_LENGTH))) + { + WLog_ERR(TAG, "Stream_EnsureCapacity failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + Stream_SetPosition(s, position); + + if (Stream_GetPosition(s) < (header.dataLen + CLIPRDR_HEADER_LENGTH)) + { + BytesReturned = 0; + BytesToRead = + (UINT32)((header.dataLen + CLIPRDR_HEADER_LENGTH) - Stream_GetPosition(s)); + status = WaitForSingleObject(cliprdr->ChannelEvent, 0); + + if (status == WAIT_FAILED) + { + error = GetLastError(); + WLog_ERR(TAG, "WaitForSingleObject failed with error %" PRIu32 "", error); + return error; + } + + if (status == WAIT_TIMEOUT) + return CHANNEL_RC_OK; + + if (!WTSVirtualChannelRead(cliprdr->ChannelHandle, 0, (PCHAR)Stream_Pointer(s), + BytesToRead, &BytesReturned)) + { + WLog_ERR(TAG, "WTSVirtualChannelRead failed!"); + return ERROR_INTERNAL_ERROR; + } + + Stream_Seek(s, BytesReturned); + } + + if (Stream_GetPosition(s) >= (header.dataLen + CLIPRDR_HEADER_LENGTH)) + { + Stream_SetPosition(s, (header.dataLen + CLIPRDR_HEADER_LENGTH)); + Stream_SealLength(s); + Stream_SetPosition(s, CLIPRDR_HEADER_LENGTH); + + if ((error = cliprdr_server_receive_pdu(context, s, &header))) + { + WLog_ERR(TAG, "cliprdr_server_receive_pdu failed with error code %" PRIu32 "!", + error); + return error; + } + + Stream_SetPosition(s, 0); + /* check for trailing zero bytes */ + status = WaitForSingleObject(cliprdr->ChannelEvent, 0); + + if (status == WAIT_FAILED) + { + error = GetLastError(); + WLog_ERR(TAG, "WaitForSingleObject failed with error %" PRIu32 "", error); + return error; + } + + if (status == WAIT_TIMEOUT) + return CHANNEL_RC_OK; + + BytesReturned = 0; + BytesToRead = 4; + + if (!WTSVirtualChannelRead(cliprdr->ChannelHandle, 0, (PCHAR)Stream_Pointer(s), + BytesToRead, &BytesReturned)) + { + WLog_ERR(TAG, "WTSVirtualChannelRead failed!"); + return ERROR_INTERNAL_ERROR; + } + + if (BytesReturned == 4) + { + Stream_Read_UINT16(s, header.msgType); /* msgType (2 bytes) */ + Stream_Read_UINT16(s, header.msgFlags); /* msgFlags (2 bytes) */ + + if (!header.msgType) + { + /* ignore trailing bytes */ + Stream_SetPosition(s, 0); + } + } + else + { + Stream_Seek(s, BytesReturned); + } + } + } + + return CHANNEL_RC_OK; +} + +static DWORD WINAPI cliprdr_server_thread(LPVOID arg) +{ + DWORD status; + DWORD nCount; + HANDLE events[8]; + HANDLE ChannelEvent; + CliprdrServerContext* context = (CliprdrServerContext*)arg; + CliprdrServerPrivate* cliprdr = (CliprdrServerPrivate*)context->handle; + UINT error = CHANNEL_RC_OK; + + ChannelEvent = context->GetEventHandle(context); + nCount = 0; + events[nCount++] = cliprdr->StopEvent; + events[nCount++] = ChannelEvent; + + if (context->autoInitializationSequence) + { + if ((error = cliprdr_server_init(context))) + { + WLog_ERR(TAG, "cliprdr_server_init failed with error %" PRIu32 "!", error); + goto out; + } + } + + while (1) + { + status = WaitForMultipleObjects(nCount, events, FALSE, INFINITE); + + if (status == WAIT_FAILED) + { + error = GetLastError(); + WLog_ERR(TAG, "WaitForMultipleObjects failed with error %" PRIu32 "", error); + goto out; + } + + status = WaitForSingleObject(cliprdr->StopEvent, 0); + + if (status == WAIT_FAILED) + { + error = GetLastError(); + WLog_ERR(TAG, "WaitForSingleObject failed with error %" PRIu32 "", error); + goto out; + } + + if (status == WAIT_OBJECT_0) + break; + + status = WaitForSingleObject(ChannelEvent, 0); + + if (status == WAIT_FAILED) + { + error = GetLastError(); + WLog_ERR(TAG, "WaitForSingleObject failed with error %" PRIu32 "", error); + goto out; + } + + if (status == WAIT_OBJECT_0) + { + if ((error = context->CheckEventHandle(context))) + { + WLog_ERR(TAG, "CheckEventHandle failed with error %" PRIu32 "!", error); + break; + } + } + } + +out: + + if (error && context->rdpcontext) + setChannelError(context->rdpcontext, error, "cliprdr_server_thread reported an error"); + + ExitThread(error); + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT cliprdr_server_open(CliprdrServerContext* context) +{ + void* buffer = NULL; + DWORD BytesReturned = 0; + CliprdrServerPrivate* cliprdr = (CliprdrServerPrivate*)context->handle; + cliprdr->ChannelHandle = WTSVirtualChannelOpen(cliprdr->vcm, WTS_CURRENT_SESSION, "cliprdr"); + + if (!cliprdr->ChannelHandle) + { + WLog_ERR(TAG, "WTSVirtualChannelOpen for cliprdr failed!"); + return ERROR_INTERNAL_ERROR; + } + + cliprdr->ChannelEvent = NULL; + + if (WTSVirtualChannelQuery(cliprdr->ChannelHandle, WTSVirtualEventHandle, &buffer, + &BytesReturned)) + { + if (BytesReturned != sizeof(HANDLE)) + { + WLog_ERR(TAG, "BytesReturned has not size of HANDLE!"); + return ERROR_INTERNAL_ERROR; + } + + CopyMemory(&(cliprdr->ChannelEvent), buffer, sizeof(HANDLE)); + WTSFreeMemory(buffer); + } + + if (!cliprdr->ChannelEvent) + { + WLog_ERR(TAG, "WTSVirtualChannelQuery for cliprdr failed!"); + return ERROR_INTERNAL_ERROR; + } + + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT cliprdr_server_close(CliprdrServerContext* context) +{ + CliprdrServerPrivate* cliprdr = (CliprdrServerPrivate*)context->handle; + + if (cliprdr->ChannelHandle) + { + WTSVirtualChannelClose(cliprdr->ChannelHandle); + cliprdr->ChannelHandle = NULL; + } + + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT cliprdr_server_start(CliprdrServerContext* context) +{ + CliprdrServerPrivate* cliprdr = (CliprdrServerPrivate*)context->handle; + UINT error; + + if (!cliprdr->ChannelHandle) + { + if ((error = context->Open(context))) + { + WLog_ERR(TAG, "Open failed!"); + return error; + } + } + + if (!(cliprdr->StopEvent = CreateEvent(NULL, TRUE, FALSE, NULL))) + { + WLog_ERR(TAG, "CreateEvent failed!"); + return ERROR_INTERNAL_ERROR; + } + + if (!(cliprdr->Thread = CreateThread(NULL, 0, cliprdr_server_thread, (void*)context, 0, NULL))) + { + WLog_ERR(TAG, "CreateThread failed!"); + CloseHandle(cliprdr->StopEvent); + cliprdr->StopEvent = NULL; + return ERROR_INTERNAL_ERROR; + } + + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT cliprdr_server_stop(CliprdrServerContext* context) +{ + UINT error = CHANNEL_RC_OK; + CliprdrServerPrivate* cliprdr = (CliprdrServerPrivate*)context->handle; + + if (cliprdr->StopEvent) + { + SetEvent(cliprdr->StopEvent); + + if (WaitForSingleObject(cliprdr->Thread, INFINITE) == WAIT_FAILED) + { + error = GetLastError(); + WLog_ERR(TAG, "WaitForSingleObject failed with error %" PRIu32 "", error); + return error; + } + + CloseHandle(cliprdr->Thread); + CloseHandle(cliprdr->StopEvent); + } + + if (cliprdr->ChannelHandle) + return context->Close(context); + + return error; +} + +static HANDLE cliprdr_server_get_event_handle(CliprdrServerContext* context) +{ + CliprdrServerPrivate* cliprdr = (CliprdrServerPrivate*)context->handle; + return cliprdr->ChannelEvent; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT cliprdr_server_check_event_handle(CliprdrServerContext* context) +{ + return cliprdr_server_read(context); +} + +CliprdrServerContext* cliprdr_server_context_new(HANDLE vcm) +{ + CliprdrServerContext* context; + CliprdrServerPrivate* cliprdr; + context = (CliprdrServerContext*)calloc(1, sizeof(CliprdrServerContext)); + + if (context) + { + context->autoInitializationSequence = TRUE; + context->Open = cliprdr_server_open; + context->Close = cliprdr_server_close; + context->Start = cliprdr_server_start; + context->Stop = cliprdr_server_stop; + context->GetEventHandle = cliprdr_server_get_event_handle; + context->CheckEventHandle = cliprdr_server_check_event_handle; + context->ServerCapabilities = cliprdr_server_capabilities; + context->MonitorReady = cliprdr_server_monitor_ready; + context->ServerFormatList = cliprdr_server_format_list; + context->ServerFormatListResponse = cliprdr_server_format_list_response; + context->ServerLockClipboardData = cliprdr_server_lock_clipboard_data; + context->ServerUnlockClipboardData = cliprdr_server_unlock_clipboard_data; + context->ServerFormatDataRequest = cliprdr_server_format_data_request; + context->ServerFormatDataResponse = cliprdr_server_format_data_response; + context->ServerFileContentsRequest = cliprdr_server_file_contents_request; + context->ServerFileContentsResponse = cliprdr_server_file_contents_response; + cliprdr = context->handle = (CliprdrServerPrivate*)calloc(1, sizeof(CliprdrServerPrivate)); + + if (cliprdr) + { + cliprdr->vcm = vcm; + cliprdr->s = Stream_New(NULL, 4096); + + if (!cliprdr->s) + { + WLog_ERR(TAG, "Stream_New failed!"); + free(context->handle); + free(context); + return NULL; + } + } + else + { + WLog_ERR(TAG, "calloc failed!"); + free(context); + return NULL; + } + } + + return context; +} + +void cliprdr_server_context_free(CliprdrServerContext* context) +{ + CliprdrServerPrivate* cliprdr; + + if (!context) + return; + + cliprdr = (CliprdrServerPrivate*)context->handle; + + if (cliprdr) + { + Stream_Free(cliprdr->s, TRUE); + free(cliprdr->temporaryDirectory); + } + + free(context->handle); + free(context); +} diff --git a/channels/cliprdr/server/cliprdr_main.h b/channels/cliprdr/server/cliprdr_main.h new file mode 100644 index 0000000..fce0ddb --- /dev/null +++ b/channels/cliprdr/server/cliprdr_main.h @@ -0,0 +1,48 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Clipboard Virtual Channel Extension + * + * Copyright 2013 Marc-Andre Moreau + * + * Licensed 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 FREERDP_CHANNEL_CLIPRDR_SERVER_MAIN_H +#define FREERDP_CHANNEL_CLIPRDR_SERVER_MAIN_H + +#include +#include +#include +#include + +#include +#include + +#define TAG CHANNELS_TAG("cliprdr.server") + +#define CLIPRDR_HEADER_LENGTH 8 + +struct _cliprdr_server_private +{ + HANDLE vcm; + HANDLE Thread; + HANDLE StopEvent; + void* ChannelHandle; + HANDLE ChannelEvent; + + wStream* s; + char* temporaryDirectory; +}; +typedef struct _cliprdr_server_private CliprdrServerPrivate; + +#endif /* FREERDP_CHANNEL_CLIPRDR_SERVER_MAIN_H */ diff --git a/channels/disp/CMakeLists.txt b/channels/disp/CMakeLists.txt new file mode 100644 index 0000000..44afe99 --- /dev/null +++ b/channels/disp/CMakeLists.txt @@ -0,0 +1,26 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2012 Marc-Andre Moreau +# +# Licensed 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. + +define_channel("disp") + +if(WITH_CLIENT_CHANNELS) + add_channel_client(${MODULE_PREFIX} ${CHANNEL_NAME}) +endif() + +if(WITH_SERVER_CHANNELS) + add_channel_server(${MODULE_PREFIX} ${CHANNEL_NAME}) +endif() diff --git a/channels/disp/ChannelOptions.cmake b/channels/disp/ChannelOptions.cmake new file mode 100644 index 0000000..0e254ad --- /dev/null +++ b/channels/disp/ChannelOptions.cmake @@ -0,0 +1,12 @@ + +set(OPTION_DEFAULT OFF) +set(OPTION_CLIENT_DEFAULT ON) +set(OPTION_SERVER_DEFAULT OFF) + +define_channel_options(NAME "disp" TYPE "dynamic" + DESCRIPTION "Display Update Virtual Channel Extension" + SPECIFICATIONS "[MS-RDPEDISP]" + DEFAULT ${OPTION_DEFAULT}) + +define_channel_client_options(${OPTION_CLIENT_DEFAULT}) +define_channel_server_options(${OPTION_SERVER_DEFAULT}) diff --git a/channels/disp/client/CMakeLists.txt b/channels/disp/client/CMakeLists.txt new file mode 100644 index 0000000..6376f6e --- /dev/null +++ b/channels/disp/client/CMakeLists.txt @@ -0,0 +1,41 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2013 Marc-Andre Moreau +# +# Licensed 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. + +define_channel_client("disp") + +set(${MODULE_PREFIX}_SRCS + disp_main.c + disp_main.h + ../disp_common.c + ../disp_common.h) + +include_directories(..) + +add_channel_client_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} TRUE "DVCPluginEntry") + + + +set(${MODULE_PREFIX}_LIBS ${${MODULE_PREFIX}_LIBS} winpr) + +target_link_libraries(${MODULE_NAME} ${${MODULE_PREFIX}_LIBS}) + + +if (WITH_DEBUG_SYMBOLS AND MSVC AND NOT BUILTIN_CHANNELS AND BUILD_SHARED_LIBS) + install(FILES ${CMAKE_BINARY_DIR}/${MODULE_NAME}.pdb DESTINATION ${FREERDP_ADDIN_PATH} COMPONENT symbols) +endif() + +set_property(TARGET ${MODULE_NAME} PROPERTY FOLDER "Channels/${CHANNEL_NAME}/Client") diff --git a/channels/disp/client/disp_main.c b/channels/disp/client/disp_main.c new file mode 100644 index 0000000..d718958 --- /dev/null +++ b/channels/disp/client/disp_main.c @@ -0,0 +1,419 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Display Update Virtual Channel Extension + * + * Copyright 2013 Marc-Andre Moreau + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger + * Copyright 2016 David PHAM-VAN + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "disp_main.h" +#include "../disp_common.h" + +struct _DISP_CHANNEL_CALLBACK +{ + IWTSVirtualChannelCallback iface; + + IWTSPlugin* plugin; + IWTSVirtualChannelManager* channel_mgr; + IWTSVirtualChannel* channel; +}; +typedef struct _DISP_CHANNEL_CALLBACK DISP_CHANNEL_CALLBACK; + +struct _DISP_LISTENER_CALLBACK +{ + IWTSListenerCallback iface; + + IWTSPlugin* plugin; + IWTSVirtualChannelManager* channel_mgr; + DISP_CHANNEL_CALLBACK* channel_callback; +}; +typedef struct _DISP_LISTENER_CALLBACK DISP_LISTENER_CALLBACK; + +struct _DISP_PLUGIN +{ + IWTSPlugin iface; + + IWTSListener* listener; + DISP_LISTENER_CALLBACK* listener_callback; + + UINT32 MaxNumMonitors; + UINT32 MaxMonitorAreaFactorA; + UINT32 MaxMonitorAreaFactorB; + BOOL initialized; +}; +typedef struct _DISP_PLUGIN DISP_PLUGIN; + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT disp_send_display_control_monitor_layout_pdu(DISP_CHANNEL_CALLBACK* callback, + UINT32 NumMonitors, + DISPLAY_CONTROL_MONITOR_LAYOUT* Monitors) +{ + UINT status; + wStream* s; + UINT32 index; + DISP_PLUGIN* disp; + UINT32 MonitorLayoutSize; + DISPLAY_CONTROL_HEADER header; + disp = (DISP_PLUGIN*)callback->plugin; + MonitorLayoutSize = DISPLAY_CONTROL_MONITOR_LAYOUT_SIZE; + header.length = 8 + 8 + (NumMonitors * MonitorLayoutSize); + header.type = DISPLAY_CONTROL_PDU_TYPE_MONITOR_LAYOUT; + + s = Stream_New(NULL, header.length); + + if (!s) + { + WLog_ERR(TAG, "Stream_New failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + if ((status = disp_write_header(s, &header))) + { + WLog_ERR(TAG, "Failed to write header with error %" PRIu32 "!", status); + goto out; + } + + if (NumMonitors > disp->MaxNumMonitors) + NumMonitors = disp->MaxNumMonitors; + + Stream_Write_UINT32(s, MonitorLayoutSize); /* MonitorLayoutSize (4 bytes) */ + Stream_Write_UINT32(s, NumMonitors); /* NumMonitors (4 bytes) */ + WLog_DBG(TAG, "disp_send_display_control_monitor_layout_pdu: NumMonitors=%" PRIu32 "", + NumMonitors); + + for (index = 0; index < NumMonitors; index++) + { + Monitors[index].Width -= (Monitors[index].Width % 2); + + if (Monitors[index].Width < 200) + Monitors[index].Width = 200; + + if (Monitors[index].Width > 8192) + Monitors[index].Width = 8192; + + if (Monitors[index].Width % 2) + Monitors[index].Width++; + + if (Monitors[index].Height < 200) + Monitors[index].Height = 200; + + if (Monitors[index].Height > 8192) + Monitors[index].Height = 8192; + + Stream_Write_UINT32(s, Monitors[index].Flags); /* Flags (4 bytes) */ + Stream_Write_UINT32(s, Monitors[index].Left); /* Left (4 bytes) */ + Stream_Write_UINT32(s, Monitors[index].Top); /* Top (4 bytes) */ + Stream_Write_UINT32(s, Monitors[index].Width); /* Width (4 bytes) */ + Stream_Write_UINT32(s, Monitors[index].Height); /* Height (4 bytes) */ + Stream_Write_UINT32(s, Monitors[index].PhysicalWidth); /* PhysicalWidth (4 bytes) */ + Stream_Write_UINT32(s, Monitors[index].PhysicalHeight); /* PhysicalHeight (4 bytes) */ + Stream_Write_UINT32(s, Monitors[index].Orientation); /* Orientation (4 bytes) */ + Stream_Write_UINT32(s, + Monitors[index].DesktopScaleFactor); /* DesktopScaleFactor (4 bytes) */ + Stream_Write_UINT32(s, Monitors[index].DeviceScaleFactor); /* DeviceScaleFactor (4 bytes) */ + WLog_DBG(TAG, + "\t%d : Flags: 0x%08" PRIX32 " Left/Top: (%" PRId32 ",%" PRId32 ") W/H=%" PRIu32 + "x%" PRIu32 ")", + index, Monitors[index].Flags, Monitors[index].Left, Monitors[index].Top, + Monitors[index].Width, Monitors[index].Height); + WLog_DBG(TAG, + "\t PhysicalWidth: %" PRIu32 " PhysicalHeight: %" PRIu32 " Orientation: %" PRIu32 + "", + Monitors[index].PhysicalWidth, Monitors[index].PhysicalHeight, + Monitors[index].Orientation); + } + +out: + Stream_SealLength(s); + status = callback->channel->Write(callback->channel, (UINT32)Stream_Length(s), Stream_Buffer(s), + NULL); + Stream_Free(s, TRUE); + return status; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT disp_recv_display_control_caps_pdu(DISP_CHANNEL_CALLBACK* callback, wStream* s) +{ + DISP_PLUGIN* disp; + DispClientContext* context; + UINT ret = CHANNEL_RC_OK; + disp = (DISP_PLUGIN*)callback->plugin; + context = (DispClientContext*)disp->iface.pInterface; + + if (Stream_GetRemainingLength(s) < 12) + { + WLog_ERR(TAG, "not enough remaining data"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT32(s, disp->MaxNumMonitors); /* MaxNumMonitors (4 bytes) */ + Stream_Read_UINT32(s, disp->MaxMonitorAreaFactorA); /* MaxMonitorAreaFactorA (4 bytes) */ + Stream_Read_UINT32(s, disp->MaxMonitorAreaFactorB); /* MaxMonitorAreaFactorB (4 bytes) */ + + if (context->DisplayControlCaps) + ret = context->DisplayControlCaps(context, disp->MaxNumMonitors, + disp->MaxMonitorAreaFactorA, disp->MaxMonitorAreaFactorB); + + return ret; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT disp_recv_pdu(DISP_CHANNEL_CALLBACK* callback, wStream* s) +{ + UINT32 error; + DISPLAY_CONTROL_HEADER header; + + if (Stream_GetRemainingLength(s) < 8) + { + WLog_ERR(TAG, "not enough remaining data"); + return ERROR_INVALID_DATA; + } + + if ((error = disp_read_header(s, &header))) + { + WLog_ERR(TAG, "disp_read_header failed with error %" PRIu32 "!", error); + return error; + } + + if (!Stream_EnsureRemainingCapacity(s, header.length)) + { + WLog_ERR(TAG, "not enough remaining data"); + return ERROR_INVALID_DATA; + } + + switch (header.type) + { + case DISPLAY_CONTROL_PDU_TYPE_CAPS: + return disp_recv_display_control_caps_pdu(callback, s); + + default: + WLog_ERR(TAG, "Type %" PRIu32 " not recognized!", header.type); + return ERROR_INTERNAL_ERROR; + } +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT disp_on_data_received(IWTSVirtualChannelCallback* pChannelCallback, wStream* data) +{ + DISP_CHANNEL_CALLBACK* callback = (DISP_CHANNEL_CALLBACK*)pChannelCallback; + return disp_recv_pdu(callback, data); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT disp_on_close(IWTSVirtualChannelCallback* pChannelCallback) +{ + free(pChannelCallback); + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT disp_on_new_channel_connection(IWTSListenerCallback* pListenerCallback, + IWTSVirtualChannel* pChannel, BYTE* Data, BOOL* pbAccept, + IWTSVirtualChannelCallback** ppCallback) +{ + DISP_CHANNEL_CALLBACK* callback; + DISP_LISTENER_CALLBACK* listener_callback = (DISP_LISTENER_CALLBACK*)pListenerCallback; + callback = (DISP_CHANNEL_CALLBACK*)calloc(1, sizeof(DISP_CHANNEL_CALLBACK)); + + if (!callback) + { + WLog_ERR(TAG, "calloc failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + callback->iface.OnDataReceived = disp_on_data_received; + callback->iface.OnClose = disp_on_close; + callback->plugin = listener_callback->plugin; + callback->channel_mgr = listener_callback->channel_mgr; + callback->channel = pChannel; + listener_callback->channel_callback = callback; + *ppCallback = (IWTSVirtualChannelCallback*)callback; + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT disp_plugin_initialize(IWTSPlugin* pPlugin, IWTSVirtualChannelManager* pChannelMgr) +{ + UINT status; + DISP_PLUGIN* disp = (DISP_PLUGIN*)pPlugin; + if (disp->initialized) + { + WLog_ERR(TAG, "[%s] channel initialized twice, aborting", DISP_DVC_CHANNEL_NAME); + return ERROR_INVALID_DATA; + } + disp->listener_callback = (DISP_LISTENER_CALLBACK*)calloc(1, sizeof(DISP_LISTENER_CALLBACK)); + + if (!disp->listener_callback) + { + WLog_ERR(TAG, "calloc failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + disp->listener_callback->iface.OnNewChannelConnection = disp_on_new_channel_connection; + disp->listener_callback->plugin = pPlugin; + disp->listener_callback->channel_mgr = pChannelMgr; + status = pChannelMgr->CreateListener(pChannelMgr, DISP_DVC_CHANNEL_NAME, 0, + &disp->listener_callback->iface, &(disp->listener)); + disp->listener->pInterface = disp->iface.pInterface; + + disp->initialized = status == CHANNEL_RC_OK; + return status; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT disp_plugin_terminated(IWTSPlugin* pPlugin) +{ + DISP_PLUGIN* disp = (DISP_PLUGIN*)pPlugin; + + if (disp && disp->listener_callback) + { + IWTSVirtualChannelManager* mgr = disp->listener_callback->channel_mgr; + if (mgr) + IFCALL(mgr->DestroyListener, mgr, disp->listener); + } + + free(disp->listener_callback); + free(disp->iface.pInterface); + free(pPlugin); + return CHANNEL_RC_OK; +} + +/** + * Channel Client Interface + */ + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT disp_send_monitor_layout(DispClientContext* context, UINT32 NumMonitors, + DISPLAY_CONTROL_MONITOR_LAYOUT* Monitors) +{ + DISP_PLUGIN* disp = (DISP_PLUGIN*)context->handle; + DISP_CHANNEL_CALLBACK* callback = disp->listener_callback->channel_callback; + return disp_send_display_control_monitor_layout_pdu(callback, NumMonitors, Monitors); +} + +#ifdef BUILTIN_CHANNELS +#define DVCPluginEntry disp_DVCPluginEntry +#else +#define DVCPluginEntry FREERDP_API DVCPluginEntry +#endif + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT DVCPluginEntry(IDRDYNVC_ENTRY_POINTS* pEntryPoints) +{ + UINT error = CHANNEL_RC_OK; + DISP_PLUGIN* disp; + DispClientContext* context; + disp = (DISP_PLUGIN*)pEntryPoints->GetPlugin(pEntryPoints, "disp"); + + if (!disp) + { + disp = (DISP_PLUGIN*)calloc(1, sizeof(DISP_PLUGIN)); + + if (!disp) + { + WLog_ERR(TAG, "calloc failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + disp->iface.Initialize = disp_plugin_initialize; + disp->iface.Connected = NULL; + disp->iface.Disconnected = NULL; + disp->iface.Terminated = disp_plugin_terminated; + disp->MaxNumMonitors = 16; + disp->MaxMonitorAreaFactorA = 8192; + disp->MaxMonitorAreaFactorB = 8192; + context = (DispClientContext*)calloc(1, sizeof(DispClientContext)); + + if (!context) + { + WLog_ERR(TAG, "calloc failed!"); + free(disp); + return CHANNEL_RC_NO_MEMORY; + } + + context->handle = (void*)disp; + context->SendMonitorLayout = disp_send_monitor_layout; + disp->iface.pInterface = (void*)context; + error = pEntryPoints->RegisterPlugin(pEntryPoints, "disp", (IWTSPlugin*)disp); + } + else + { + WLog_ERR(TAG, "could not get disp Plugin."); + return CHANNEL_RC_BAD_CHANNEL; + } + + return error; +} diff --git a/channels/disp/client/disp_main.h b/channels/disp/client/disp_main.h new file mode 100644 index 0000000..45a4830 --- /dev/null +++ b/channels/disp/client/disp_main.h @@ -0,0 +1,38 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Display Update Virtual Channel Extension + * + * Copyright 2013 Marc-Andre Moreau + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger + * + * Licensed 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 FREERDP_CHANNEL_DISP_CLIENT_MAIN_H +#define FREERDP_CHANNEL_DISP_CLIENT_MAIN_H + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include +#include + +#include + +#define TAG CHANNELS_TAG("disp.client") + +#endif /* FREERDP_CHANNEL_DISP_CLIENT_MAIN_H */ diff --git a/channels/disp/disp_common.c b/channels/disp/disp_common.c new file mode 100644 index 0000000..f4313d1 --- /dev/null +++ b/channels/disp/disp_common.c @@ -0,0 +1,60 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * RDPEDISP Virtual Channel Extension + * + * Copyright 2019 Kobi Mizrachi + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include + +#define TAG CHANNELS_TAG("disp.common") + +#include "disp_common.h" + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT disp_read_header(wStream* s, DISPLAY_CONTROL_HEADER* header) +{ + if (Stream_GetRemainingLength(s) < 8) + { + WLog_ERR(TAG, "header parsing failed: not enough data!"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT32(s, header->type); + Stream_Read_UINT32(s, header->length); + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT disp_write_header(wStream* s, const DISPLAY_CONTROL_HEADER* header) +{ + Stream_Write_UINT32(s, header->type); + Stream_Write_UINT32(s, header->length); + return CHANNEL_RC_OK; +} diff --git a/channels/disp/disp_common.h b/channels/disp/disp_common.h new file mode 100644 index 0000000..386b8b3 --- /dev/null +++ b/channels/disp/disp_common.h @@ -0,0 +1,32 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * RDPEDISP Virtual Channel Extension + * + * Copyright 2019 Kobi Mizrachi + * + * Licensed 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 FREERDP_CHANNEL_DISP_COMMON_H +#define FREERDP_CHANNEL_DISP_COMMON_H + +#include +#include + +#include +#include + +FREERDP_LOCAL UINT disp_read_header(wStream* s, DISPLAY_CONTROL_HEADER* header); +FREERDP_LOCAL UINT disp_write_header(wStream* s, const DISPLAY_CONTROL_HEADER* header); + +#endif /* FREERDP_CHANNEL_DISP_COMMON_H */ diff --git a/channels/disp/server/CMakeLists.txt b/channels/disp/server/CMakeLists.txt new file mode 100644 index 0000000..dddc15b --- /dev/null +++ b/channels/disp/server/CMakeLists.txt @@ -0,0 +1,32 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2019 Kobi Mizrachi +# +# Licensed 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. + +define_channel_server("disp") + +set(${MODULE_PREFIX}_SRCS + disp_main.c + disp_main.h + ../disp_common.c + ../disp_common.h + ) + +include_directories(..) + +add_channel_server_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} FALSE "DVCPluginEntry") + +target_link_libraries(${MODULE_NAME} freerdp) +set_property(TARGET ${MODULE_NAME} PROPERTY FOLDER "Channels/${CHANNEL_NAME}/Server") diff --git a/channels/disp/server/disp_main.c b/channels/disp/server/disp_main.c new file mode 100644 index 0000000..9caec45 --- /dev/null +++ b/channels/disp/server/disp_main.c @@ -0,0 +1,597 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * RDPEDISP Virtual Channel Extension + * + * Copyright 2019 Kobi Mizrachi + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "disp_main.h" +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include "../disp_common.h" + +#define TAG CHANNELS_TAG("rdpedisp.server") + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ + +static wStream* disp_server_single_packet_new(UINT32 type, UINT32 length) +{ + UINT error; + DISPLAY_CONTROL_HEADER header; + wStream* s = Stream_New(NULL, DISPLAY_CONTROL_HEADER_LENGTH + length); + + if (!s) + { + WLog_ERR(TAG, "Stream_New failed!"); + goto error; + } + + header.type = type; + header.length = DISPLAY_CONTROL_HEADER_LENGTH + length; + + if ((error = disp_write_header(s, &header))) + { + WLog_ERR(TAG, "Failed to write header with error %" PRIu32 "!", error); + goto error; + } + + return s; +error: + Stream_Free(s, TRUE); + return NULL; +} + +static void disp_server_sanitize_monitor_layout(DISPLAY_CONTROL_MONITOR_LAYOUT* monitor) +{ + if (monitor->PhysicalWidth < DISPLAY_CONTROL_MIN_PHYSICAL_MONITOR_WIDTH || + monitor->PhysicalWidth > DISPLAY_CONTROL_MAX_PHYSICAL_MONITOR_WIDTH || + monitor->PhysicalHeight < DISPLAY_CONTROL_MIN_PHYSICAL_MONITOR_HEIGHT || + monitor->PhysicalHeight > DISPLAY_CONTROL_MAX_PHYSICAL_MONITOR_HEIGHT) + { + if (monitor->PhysicalWidth != 0 || monitor->PhysicalHeight != 0) + WLog_DBG( + TAG, + "Sanitizing invalid physical monitor size. Old physical monitor size: [%" PRIu32 + ", %" PRIu32 "]", + monitor->PhysicalWidth, monitor->PhysicalHeight); + + monitor->PhysicalWidth = monitor->PhysicalHeight = 0; + } +} + +static BOOL disp_server_is_monitor_layout_valid(DISPLAY_CONTROL_MONITOR_LAYOUT* monitor) +{ + if (monitor->Width < DISPLAY_CONTROL_MIN_MONITOR_WIDTH || + monitor->Width > DISPLAY_CONTROL_MAX_MONITOR_WIDTH) + { + WLog_WARN(TAG, "Received invalid value for monitor->Width: %" PRIu32 "", monitor->Width); + return FALSE; + } + + if (monitor->Height < DISPLAY_CONTROL_MIN_MONITOR_HEIGHT || + monitor->Height > DISPLAY_CONTROL_MAX_MONITOR_HEIGHT) + { + WLog_WARN(TAG, "Received invalid value for monitor->Height: %" PRIu32 "", monitor->Width); + return FALSE; + } + + switch (monitor->Orientation) + { + case ORIENTATION_LANDSCAPE: + case ORIENTATION_PORTRAIT: + case ORIENTATION_LANDSCAPE_FLIPPED: + case ORIENTATION_PORTRAIT_FLIPPED: + break; + + default: + WLog_WARN(TAG, "Received incorrect value for monitor->Orientation: %" PRIu32 "", + monitor->Orientation); + return FALSE; + } + + return TRUE; +} + +static UINT disp_recv_display_control_monitor_layout_pdu(wStream* s, DispServerContext* context) +{ + UINT32 error = CHANNEL_RC_OK; + UINT32 index; + DISPLAY_CONTROL_MONITOR_LAYOUT_PDU pdu; + DISPLAY_CONTROL_MONITOR_LAYOUT* monitor; + + if (Stream_GetRemainingLength(s) < 8) + { + WLog_ERR(TAG, "not enough data!"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT32(s, pdu.MonitorLayoutSize); /* MonitorLayoutSize (4 bytes) */ + + if (pdu.MonitorLayoutSize != DISPLAY_CONTROL_MONITOR_LAYOUT_SIZE) + { + WLog_ERR(TAG, "MonitorLayoutSize is set to %" PRIu32 ". expected %" PRIu32 "", + pdu.MonitorLayoutSize, DISPLAY_CONTROL_MONITOR_LAYOUT_SIZE); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT32(s, pdu.NumMonitors); /* NumMonitors (4 bytes) */ + + if (pdu.NumMonitors > context->MaxNumMonitors) + { + WLog_ERR(TAG, "NumMonitors (%" PRIu32 ")> server MaxNumMonitors (%" PRIu32 ")", + pdu.NumMonitors, context->MaxNumMonitors); + return ERROR_INVALID_DATA; + } + + if (Stream_GetRemainingLength(s) < DISPLAY_CONTROL_MONITOR_LAYOUT_SIZE * pdu.NumMonitors) + { + WLog_ERR(TAG, "not enough data!"); + return ERROR_INVALID_DATA; + } + + pdu.Monitors = (DISPLAY_CONTROL_MONITOR_LAYOUT*)calloc(pdu.NumMonitors, + sizeof(DISPLAY_CONTROL_MONITOR_LAYOUT)); + + if (!pdu.Monitors) + { + WLog_ERR(TAG, "disp_recv_display_control_monitor_layout_pdu(): calloc failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + WLog_DBG(TAG, "disp_recv_display_control_monitor_layout_pdu: NumMonitors=%" PRIu32 "", + pdu.NumMonitors); + + for (index = 0; index < pdu.NumMonitors; index++) + { + monitor = &(pdu.Monitors[index]); + Stream_Read_UINT32(s, monitor->Flags); /* Flags (4 bytes) */ + Stream_Read_UINT32(s, monitor->Left); /* Left (4 bytes) */ + Stream_Read_UINT32(s, monitor->Top); /* Top (4 bytes) */ + Stream_Read_UINT32(s, monitor->Width); /* Width (4 bytes) */ + Stream_Read_UINT32(s, monitor->Height); /* Height (4 bytes) */ + Stream_Read_UINT32(s, monitor->PhysicalWidth); /* PhysicalWidth (4 bytes) */ + Stream_Read_UINT32(s, monitor->PhysicalHeight); /* PhysicalHeight (4 bytes) */ + Stream_Read_UINT32(s, monitor->Orientation); /* Orientation (4 bytes) */ + Stream_Read_UINT32(s, monitor->DesktopScaleFactor); /* DesktopScaleFactor (4 bytes) */ + Stream_Read_UINT32(s, monitor->DeviceScaleFactor); /* DeviceScaleFactor (4 bytes) */ + + disp_server_sanitize_monitor_layout(monitor); + WLog_DBG(TAG, + "\t%d : Flags: 0x%08" PRIX32 " Left/Top: (%" PRId32 ",%" PRId32 ") W/H=%" PRIu32 + "x%" PRIu32 ")", + index, monitor->Flags, monitor->Left, monitor->Top, monitor->Width, + monitor->Height); + WLog_DBG(TAG, + "\t PhysicalWidth: %" PRIu32 " PhysicalHeight: %" PRIu32 " Orientation: %" PRIu32 + "", + monitor->PhysicalWidth, monitor->PhysicalHeight, monitor->Orientation); + + if (!disp_server_is_monitor_layout_valid(monitor)) + { + error = ERROR_INVALID_DATA; + goto out; + } + } + + if (context) + IFCALLRET(context->DispMonitorLayout, error, context, &pdu); + +out: + free(pdu.Monitors); + return error; +} + +static UINT disp_server_receive_pdu(DispServerContext* context, wStream* s) +{ + UINT error = CHANNEL_RC_OK; + size_t beg, end; + DISPLAY_CONTROL_HEADER header; + beg = Stream_GetPosition(s); + + if ((error = disp_read_header(s, &header))) + { + WLog_ERR(TAG, "disp_read_header failed with error %" PRIu32 "!", error); + return error; + } + + switch (header.type) + { + case DISPLAY_CONTROL_PDU_TYPE_MONITOR_LAYOUT: + if ((error = disp_recv_display_control_monitor_layout_pdu(s, context))) + WLog_ERR(TAG, + "disp_recv_display_control_monitor_layout_pdu " + "failed with error %" PRIu32 "!", + error); + + break; + + default: + error = CHANNEL_RC_BAD_PROC; + WLog_WARN(TAG, "Received unknown PDU type: %" PRIu32 "", header.type); + break; + } + + end = Stream_GetPosition(s); + + if (end != (beg + header.length)) + { + WLog_ERR(TAG, "Unexpected DISP pdu end: Actual: %d, Expected: %" PRIu32 "", end, + (beg + header.length)); + Stream_SetPosition(s, (beg + header.length)); + } + + return error; +} + +static UINT disp_server_handle_messages(DispServerContext* context) +{ + DWORD BytesReturned; + void* buffer; + UINT ret = CHANNEL_RC_OK; + DispServerPrivate* priv = context->priv; + wStream* s = priv->input_stream; + + /* Check whether the dynamic channel is ready */ + if (!priv->isReady) + { + if (WTSVirtualChannelQuery(priv->disp_channel, WTSVirtualChannelReady, &buffer, + &BytesReturned) == FALSE) + { + if (GetLastError() == ERROR_NO_DATA) + return ERROR_NO_DATA; + + WLog_ERR(TAG, "WTSVirtualChannelQuery failed"); + return ERROR_INTERNAL_ERROR; + } + + priv->isReady = *((BOOL*)buffer); + WTSFreeMemory(buffer); + } + + /* Consume channel event only after the disp dynamic channel is ready */ + Stream_SetPosition(s, 0); + + if (!WTSVirtualChannelRead(priv->disp_channel, 0, NULL, 0, &BytesReturned)) + { + if (GetLastError() == ERROR_NO_DATA) + return ERROR_NO_DATA; + + WLog_ERR(TAG, "WTSVirtualChannelRead failed!"); + return ERROR_INTERNAL_ERROR; + } + + if (BytesReturned < 1) + return CHANNEL_RC_OK; + + if (!Stream_EnsureRemainingCapacity(s, BytesReturned)) + { + WLog_ERR(TAG, "Stream_EnsureRemainingCapacity failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + if (WTSVirtualChannelRead(priv->disp_channel, 0, (PCHAR)Stream_Buffer(s), Stream_Capacity(s), + &BytesReturned) == FALSE) + { + WLog_ERR(TAG, "WTSVirtualChannelRead failed!"); + return ERROR_INTERNAL_ERROR; + } + + Stream_SetLength(s, BytesReturned); + Stream_SetPosition(s, 0); + + while (Stream_GetPosition(s) < Stream_Length(s)) + { + if ((ret = disp_server_receive_pdu(context, s))) + { + WLog_ERR(TAG, + "disp_server_receive_pdu " + "failed with error %" PRIu32 "!", + ret); + return ret; + } + } + + return ret; +} + +static DWORD WINAPI disp_server_thread_func(LPVOID arg) +{ + DispServerContext* context = (DispServerContext*)arg; + DispServerPrivate* priv = context->priv; + DWORD status; + DWORD nCount; + HANDLE events[8]; + UINT error = CHANNEL_RC_OK; + nCount = 0; + events[nCount++] = priv->stopEvent; + events[nCount++] = priv->channelEvent; + + /* Main virtual channel loop. RDPEDISP do not need version negotiation */ + while (TRUE) + { + status = WaitForMultipleObjects(nCount, events, FALSE, INFINITE); + + if (status == WAIT_FAILED) + { + error = GetLastError(); + WLog_ERR(TAG, "WaitForMultipleObjects failed with error %" PRIu32 "", error); + break; + } + + /* Stop Event */ + if (status == WAIT_OBJECT_0) + break; + + if ((error = disp_server_handle_messages(context))) + { + WLog_ERR(TAG, "disp_server_handle_messages failed with error %" PRIu32 "", error); + break; + } + } + + ExitThread(error); + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT disp_server_open(DispServerContext* context) +{ + UINT rc = ERROR_INTERNAL_ERROR; + DispServerPrivate* priv = context->priv; + DWORD BytesReturned = 0; + PULONG pSessionId = NULL; + void* buffer; + buffer = NULL; + priv->SessionId = WTS_CURRENT_SESSION; + UINT32 channelId; + BOOL status = TRUE; + + if (WTSQuerySessionInformationA(context->vcm, WTS_CURRENT_SESSION, WTSSessionId, + (LPSTR*)&pSessionId, &BytesReturned) == FALSE) + { + WLog_ERR(TAG, "WTSQuerySessionInformationA failed!"); + rc = ERROR_INTERNAL_ERROR; + goto out_close; + } + + priv->SessionId = (DWORD)*pSessionId; + WTSFreeMemory(pSessionId); + priv->disp_channel = (HANDLE)WTSVirtualChannelOpenEx(priv->SessionId, DISP_DVC_CHANNEL_NAME, + WTS_CHANNEL_OPTION_DYNAMIC); + + if (!priv->disp_channel) + { + WLog_ERR(TAG, "WTSVirtualChannelOpenEx failed!"); + rc = GetLastError(); + goto out_close; + } + + channelId = WTSChannelGetIdByHandle(priv->disp_channel); + + IFCALLRET(context->ChannelIdAssigned, status, context, channelId); + if (!status) + { + WLog_ERR(TAG, "context->ChannelIdAssigned failed!"); + rc = ERROR_INTERNAL_ERROR; + goto out_close; + } + + /* Query for channel event handle */ + if (!WTSVirtualChannelQuery(priv->disp_channel, WTSVirtualEventHandle, &buffer, + &BytesReturned) || + (BytesReturned != sizeof(HANDLE))) + { + WLog_ERR(TAG, + "WTSVirtualChannelQuery failed " + "or invalid returned size(%" PRIu32 ")", + BytesReturned); + + if (buffer) + WTSFreeMemory(buffer); + + rc = ERROR_INTERNAL_ERROR; + goto out_close; + } + + CopyMemory(&priv->channelEvent, buffer, sizeof(HANDLE)); + WTSFreeMemory(buffer); + + if (priv->thread == NULL) + { + if (!(priv->stopEvent = CreateEvent(NULL, TRUE, FALSE, NULL))) + { + WLog_ERR(TAG, "CreateEvent failed!"); + rc = ERROR_INTERNAL_ERROR; + } + + if (!(priv->thread = + CreateThread(NULL, 0, disp_server_thread_func, (void*)context, 0, NULL))) + { + WLog_ERR(TAG, "CreateEvent failed!"); + CloseHandle(priv->stopEvent); + priv->stopEvent = NULL; + rc = ERROR_INTERNAL_ERROR; + } + } + + return CHANNEL_RC_OK; +out_close: + WTSVirtualChannelClose(priv->disp_channel); + priv->disp_channel = NULL; + priv->channelEvent = NULL; + return rc; +} + +static UINT disp_server_packet_send(DispServerContext* context, wStream* s) +{ + UINT ret; + ULONG written; + + if (!WTSVirtualChannelWrite(context->priv->disp_channel, (PCHAR)Stream_Buffer(s), + Stream_GetPosition(s), &written)) + { + WLog_ERR(TAG, "WTSVirtualChannelWrite failed!"); + ret = ERROR_INTERNAL_ERROR; + goto out; + } + + if (written < Stream_GetPosition(s)) + { + WLog_WARN(TAG, "Unexpected bytes written: %" PRIu32 "/%" PRIuz "", written, + Stream_GetPosition(s)); + } + + ret = CHANNEL_RC_OK; +out: + Stream_Free(s, TRUE); + return ret; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT disp_server_send_caps_pdu(DispServerContext* context) +{ + wStream* s = disp_server_single_packet_new(DISPLAY_CONTROL_PDU_TYPE_CAPS, 12); + + if (!s) + { + WLog_ERR(TAG, "disp_server_single_packet_new failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + Stream_Write_UINT32(s, context->MaxNumMonitors); /* MaxNumMonitors (4 bytes) */ + Stream_Write_UINT32(s, context->MaxMonitorAreaFactorA); /* MaxMonitorAreaFactorA (4 bytes) */ + Stream_Write_UINT32(s, context->MaxMonitorAreaFactorB); /* MaxMonitorAreaFactorB (4 bytes) */ + return disp_server_packet_send(context, s); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT disp_server_close(DispServerContext* context) +{ + UINT error = CHANNEL_RC_OK; + DispServerPrivate* priv = context->priv; + + if (priv->thread) + { + SetEvent(priv->stopEvent); + + if (WaitForSingleObject(priv->thread, INFINITE) == WAIT_FAILED) + { + error = GetLastError(); + WLog_ERR(TAG, "WaitForSingleObject failed with error %" PRIu32 "", error); + return error; + } + + CloseHandle(priv->thread); + CloseHandle(priv->stopEvent); + priv->thread = NULL; + priv->stopEvent = NULL; + } + + if (priv->disp_channel) + { + WTSVirtualChannelClose(priv->disp_channel); + priv->disp_channel = NULL; + } + + return error; +} + +DispServerContext* disp_server_context_new(HANDLE vcm) +{ + DispServerContext* context; + DispServerPrivate* priv; + context = (DispServerContext*)calloc(1, sizeof(DispServerContext)); + + if (!context) + { + WLog_ERR(TAG, "disp_server_context_new(): calloc DispServerContext failed!"); + goto out_free; + } + + priv = context->priv = (DispServerPrivate*)calloc(1, sizeof(DispServerPrivate)); + + if (!context->priv) + { + WLog_ERR(TAG, "disp_server_context_new(): calloc DispServerPrivate failed!"); + goto out_free; + } + + priv->input_stream = Stream_New(NULL, 4); + + if (!priv->input_stream) + { + WLog_ERR(TAG, "Stream_New failed!"); + goto out_free_priv; + } + + context->vcm = vcm; + context->Open = disp_server_open; + context->Close = disp_server_close; + context->DisplayControlCaps = disp_server_send_caps_pdu; + priv->isReady = FALSE; + return context; +out_free_priv: + free(context->priv); +out_free: + free(context); + return NULL; +} + +void disp_server_context_free(DispServerContext* context) +{ + if (!context) + return; + + disp_server_close(context); + + if (context->priv) + { + Stream_Free(context->priv->input_stream, TRUE); + free(context->priv); + } + + free(context); +} diff --git a/channels/disp/server/disp_main.h b/channels/disp/server/disp_main.h new file mode 100644 index 0000000..c47462b --- /dev/null +++ b/channels/disp/server/disp_main.h @@ -0,0 +1,37 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * RDPEDISP Virtual Channel Extension + * + * Copyright 2019 Kobi Mizrachi + * + * Licensed 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 FREERDP_CHANNEL_DISP_SERVER_MAIN_H +#define FREERDP_CHANNEL_DISP_SERVER_MAIN_H + +#include + +struct _disp_server_private +{ + BOOL isReady; + wStream* input_stream; + HANDLE channelEvent; + HANDLE thread; + HANDLE stopEvent; + DWORD SessionId; + + void* disp_channel; +}; + +#endif /* FREERDP_CHANNEL_DISP_SERVER_MAIN_H */ diff --git a/channels/drdynvc/CMakeLists.txt b/channels/drdynvc/CMakeLists.txt new file mode 100644 index 0000000..9a6ee1f --- /dev/null +++ b/channels/drdynvc/CMakeLists.txt @@ -0,0 +1,26 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2012 Marc-Andre Moreau +# +# Licensed 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. + +define_channel("drdynvc") + +if(WITH_CLIENT_CHANNELS) + add_channel_client(${MODULE_PREFIX} ${CHANNEL_NAME}) +endif() + +if(WITH_SERVER_CHANNELS) + add_channel_server(${MODULE_PREFIX} ${CHANNEL_NAME}) +endif() diff --git a/channels/drdynvc/ChannelOptions.cmake b/channels/drdynvc/ChannelOptions.cmake new file mode 100644 index 0000000..76376b6 --- /dev/null +++ b/channels/drdynvc/ChannelOptions.cmake @@ -0,0 +1,13 @@ + +set(OPTION_DEFAULT OFF) +set(OPTION_CLIENT_DEFAULT ON) +set(OPTION_SERVER_DEFAULT ON) + +define_channel_options(NAME "drdynvc" TYPE "static" + DESCRIPTION "Dynamic Virtual Channel Extension" + SPECIFICATIONS "[MS-RDPEDYC]" + DEFAULT ${OPTION_DEFAULT}) + +define_channel_client_options(${OPTION_CLIENT_DEFAULT}) +define_channel_server_options(${OPTION_SERVER_DEFAULT}) + diff --git a/channels/drdynvc/client/CMakeLists.txt b/channels/drdynvc/client/CMakeLists.txt new file mode 100644 index 0000000..abf9185 --- /dev/null +++ b/channels/drdynvc/client/CMakeLists.txt @@ -0,0 +1,27 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2012 Marc-Andre Moreau +# +# Licensed 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. + +define_channel_client("drdynvc") + +set(${MODULE_PREFIX}_SRCS + drdynvc_main.c + drdynvc_main.h) + +add_channel_client_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} FALSE "VirtualChannelEntryEx") + +set_property(TARGET ${MODULE_NAME} PROPERTY FOLDER "Channels/${CHANNEL_NAME}/Client") + diff --git a/channels/drdynvc/client/drdynvc_main.c b/channels/drdynvc/client/drdynvc_main.c new file mode 100644 index 0000000..b2005a9 --- /dev/null +++ b/channels/drdynvc/client/drdynvc_main.c @@ -0,0 +1,1826 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Dynamic Virtual Channel + * + * Copyright 2010-2011 Vic Lee + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include + +#include "drdynvc_main.h" + +#define TAG CHANNELS_TAG("drdynvc.client") + +static UINT dvcman_close_channel(IWTSVirtualChannelManager* pChannelMgr, UINT32 ChannelId, + BOOL bSendClosePDU); +static void dvcman_free(drdynvcPlugin* drdynvc, IWTSVirtualChannelManager* pChannelMgr); +static void dvcman_channel_free(void* channel); +static UINT drdynvc_write_data(drdynvcPlugin* drdynvc, UINT32 ChannelId, const BYTE* data, + UINT32 dataSize, BOOL* close); +static UINT drdynvc_send(drdynvcPlugin* drdynvc, wStream* s); + +static void dvcman_wtslistener_free(DVCMAN_LISTENER* listener) +{ + if (listener) + free(listener->channel_name); + + free(listener); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT dvcman_get_configuration(IWTSListener* pListener, void** ppPropertyBag) +{ + WINPR_UNUSED(pListener); + *ppPropertyBag = NULL; + return ERROR_INTERNAL_ERROR; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT dvcman_create_listener(IWTSVirtualChannelManager* pChannelMgr, + const char* pszChannelName, ULONG ulFlags, + IWTSListenerCallback* pListenerCallback, + IWTSListener** ppListener) +{ + DVCMAN* dvcman = (DVCMAN*)pChannelMgr; + DVCMAN_LISTENER* listener; + + WLog_DBG(TAG, "create_listener: %d.%s.", ArrayList_Count(dvcman->listeners) + 1, + pszChannelName); + listener = (DVCMAN_LISTENER*)calloc(1, sizeof(DVCMAN_LISTENER)); + + if (!listener) + { + WLog_ERR(TAG, "calloc failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + listener->iface.GetConfiguration = dvcman_get_configuration; + listener->iface.pInterface = NULL; + listener->dvcman = dvcman; + listener->channel_name = _strdup(pszChannelName); + + if (!listener->channel_name) + { + WLog_ERR(TAG, "_strdup failed!"); + dvcman_wtslistener_free(listener); + return CHANNEL_RC_NO_MEMORY; + } + + listener->flags = ulFlags; + listener->listener_callback = pListenerCallback; + + if (ppListener) + *ppListener = (IWTSListener*)listener; + + if (ArrayList_Add(dvcman->listeners, listener) < 0) + return ERROR_INTERNAL_ERROR; + return CHANNEL_RC_OK; +} + +static UINT dvcman_destroy_listener(IWTSVirtualChannelManager* pChannelMgr, IWTSListener* pListener) +{ + DVCMAN_LISTENER* listener = (DVCMAN_LISTENER*)pListener; + + WINPR_UNUSED(pChannelMgr); + + if (listener) + { + DVCMAN* dvcman = listener->dvcman; + if (dvcman) + ArrayList_Remove(dvcman->listeners, listener); + } + + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT dvcman_register_plugin(IDRDYNVC_ENTRY_POINTS* pEntryPoints, const char* name, + IWTSPlugin* pPlugin) +{ + DVCMAN* dvcman = ((DVCMAN_ENTRY_POINTS*)pEntryPoints)->dvcman; + + if (ArrayList_Add(dvcman->plugin_names, _strdup(name)) < 0) + return ERROR_INTERNAL_ERROR; + if (ArrayList_Add(dvcman->plugins, pPlugin) < 0) + return ERROR_INTERNAL_ERROR; + + WLog_DBG(TAG, "register_plugin: num_plugins %d", ArrayList_Count(dvcman->plugins)); + return CHANNEL_RC_OK; +} + +static IWTSPlugin* dvcman_get_plugin(IDRDYNVC_ENTRY_POINTS* pEntryPoints, const char* name) +{ + IWTSPlugin* plugin = NULL; + size_t i, nc, pc; + DVCMAN* dvcman = ((DVCMAN_ENTRY_POINTS*)pEntryPoints)->dvcman; + if (!dvcman || !pEntryPoints || !name) + return NULL; + + nc = ArrayList_Count(dvcman->plugin_names); + pc = ArrayList_Count(dvcman->plugins); + if (nc != pc) + return NULL; + + ArrayList_Lock(dvcman->plugin_names); + ArrayList_Lock(dvcman->plugins); + for (i = 0; i < pc; i++) + { + const char* cur = ArrayList_GetItem(dvcman->plugin_names, i); + if (strcmp(cur, name) == 0) + { + plugin = ArrayList_GetItem(dvcman->plugins, i); + break; + } + } + ArrayList_Unlock(dvcman->plugin_names); + ArrayList_Unlock(dvcman->plugins); + return plugin; +} + +static ADDIN_ARGV* dvcman_get_plugin_data(IDRDYNVC_ENTRY_POINTS* pEntryPoints) +{ + return ((DVCMAN_ENTRY_POINTS*)pEntryPoints)->args; +} + +static void* dvcman_get_rdp_settings(IDRDYNVC_ENTRY_POINTS* pEntryPoints) +{ + return (void*)((DVCMAN_ENTRY_POINTS*)pEntryPoints)->settings; +} + +static UINT32 dvcman_get_channel_id(IWTSVirtualChannel* channel) +{ + DVCMAN_CHANNEL* dvc = (DVCMAN_CHANNEL*)channel; + return dvc->channel_id; +} + +static const char* dvcman_get_channel_name(IWTSVirtualChannel* channel) +{ + DVCMAN_CHANNEL* dvc = (DVCMAN_CHANNEL*)channel; + return dvc->channel_name; +} + +static IWTSVirtualChannel* dvcman_find_channel_by_id(IWTSVirtualChannelManager* pChannelMgr, + UINT32 ChannelId) +{ + int index; + IWTSVirtualChannel* channel = NULL; + DVCMAN* dvcman = (DVCMAN*)pChannelMgr; + ArrayList_Lock(dvcman->channels); + for (index = 0; index < ArrayList_Count(dvcman->channels); index++) + { + DVCMAN_CHANNEL* cur = (DVCMAN_CHANNEL*)ArrayList_GetItem(dvcman->channels, index); + if (cur->channel_id == ChannelId) + { + channel = &cur->iface; + break; + } + } + + ArrayList_Unlock(dvcman->channels); + return channel; +} + +static void dvcman_plugin_terminate(void* plugin) +{ + IWTSPlugin* pPlugin = plugin; + + UINT error = IFCALLRESULT(CHANNEL_RC_OK, pPlugin->Terminated, pPlugin); + if (error != CHANNEL_RC_OK) + WLog_ERR(TAG, "Terminated failed with error %" PRIu32 "!", error); +} + +static void wts_listener_free(void* arg) +{ + DVCMAN_LISTENER* listener = (DVCMAN_LISTENER*)arg; + dvcman_wtslistener_free(listener); +} +static IWTSVirtualChannelManager* dvcman_new(drdynvcPlugin* plugin) +{ + wObject* obj; + DVCMAN* dvcman; + dvcman = (DVCMAN*)calloc(1, sizeof(DVCMAN)); + + if (!dvcman) + return NULL; + + dvcman->iface.CreateListener = dvcman_create_listener; + dvcman->iface.DestroyListener = dvcman_destroy_listener; + dvcman->iface.FindChannelById = dvcman_find_channel_by_id; + dvcman->iface.GetChannelId = dvcman_get_channel_id; + dvcman->iface.GetChannelName = dvcman_get_channel_name; + dvcman->drdynvc = plugin; + dvcman->channels = ArrayList_New(TRUE); + + if (!dvcman->channels) + goto fail; + + obj = ArrayList_Object(dvcman->channels); + obj->fnObjectFree = dvcman_channel_free; + + dvcman->pool = StreamPool_New(TRUE, 10); + if (!dvcman->pool) + goto fail; + + dvcman->listeners = ArrayList_New(TRUE); + if (!dvcman->listeners) + goto fail; + obj = ArrayList_Object(dvcman->listeners); + obj->fnObjectFree = wts_listener_free; + + dvcman->plugin_names = ArrayList_New(TRUE); + if (!dvcman->plugin_names) + goto fail; + obj = ArrayList_Object(dvcman->plugin_names); + obj->fnObjectFree = free; + + dvcman->plugins = ArrayList_New(TRUE); + if (!dvcman->plugins) + goto fail; + obj = ArrayList_Object(dvcman->plugins); + obj->fnObjectFree = dvcman_plugin_terminate; + return &dvcman->iface; +fail: + dvcman_free(plugin, &dvcman->iface); + return NULL; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT dvcman_load_addin(drdynvcPlugin* drdynvc, IWTSVirtualChannelManager* pChannelMgr, + ADDIN_ARGV* args, rdpSettings* settings) +{ + DVCMAN_ENTRY_POINTS entryPoints; + PDVC_PLUGIN_ENTRY pDVCPluginEntry = NULL; + WLog_Print(drdynvc->log, WLOG_INFO, "Loading Dynamic Virtual Channel %s", args->argv[0]); + pDVCPluginEntry = (PDVC_PLUGIN_ENTRY)freerdp_load_channel_addin_entry( + args->argv[0], NULL, NULL, FREERDP_ADDIN_CHANNEL_DYNAMIC); + + if (pDVCPluginEntry) + { + entryPoints.iface.RegisterPlugin = dvcman_register_plugin; + entryPoints.iface.GetPlugin = dvcman_get_plugin; + entryPoints.iface.GetPluginData = dvcman_get_plugin_data; + entryPoints.iface.GetRdpSettings = dvcman_get_rdp_settings; + entryPoints.dvcman = (DVCMAN*)pChannelMgr; + entryPoints.args = args; + entryPoints.settings = settings; + return pDVCPluginEntry(&entryPoints.iface); + } + + return ERROR_INVALID_FUNCTION; +} + +static DVCMAN_CHANNEL* dvcman_channel_new(drdynvcPlugin* drdynvc, + IWTSVirtualChannelManager* pChannelMgr, UINT32 ChannelId, + const char* ChannelName) +{ + DVCMAN_CHANNEL* channel; + + if (dvcman_find_channel_by_id(pChannelMgr, ChannelId)) + { + WLog_Print(drdynvc->log, WLOG_ERROR, + "Protocol error: Duplicated ChannelId %" PRIu32 " (%s)!", ChannelId, + ChannelName); + return NULL; + } + + channel = (DVCMAN_CHANNEL*)calloc(1, sizeof(DVCMAN_CHANNEL)); + + if (!channel) + goto fail; + + channel->dvcman = (DVCMAN*)pChannelMgr; + channel->channel_id = ChannelId; + channel->channel_name = _strdup(ChannelName); + + if (!channel->channel_name) + goto fail; + + if (!InitializeCriticalSectionEx(&(channel->lock), 0, 0)) + goto fail; + + return channel; +fail: + dvcman_channel_free(channel); + return NULL; +} + +static void dvcman_channel_free(void* arg) +{ + DVCMAN_CHANNEL* channel = (DVCMAN_CHANNEL*)arg; + UINT error = CHANNEL_RC_OK; + + if (channel) + { + if (channel->channel_callback) + { + IFCALL(channel->channel_callback->OnClose, channel->channel_callback); + channel->channel_callback = NULL; + } + + if (channel->status == CHANNEL_RC_OK) + { + IWTSVirtualChannel* ichannel = (IWTSVirtualChannel*)channel; + + if (channel->dvcman && channel->dvcman->drdynvc) + { + DrdynvcClientContext* context = channel->dvcman->drdynvc->context; + + if (context) + { + IFCALLRET(context->OnChannelDisconnected, error, context, channel->channel_name, + channel->pInterface); + } + } + + error = IFCALLRESULT(CHANNEL_RC_OK, ichannel->Close, ichannel); + + if (error != CHANNEL_RC_OK) + WLog_ERR(TAG, "Close failed with error %" PRIu32 "!", error); + } + + if (channel->dvc_data) + Stream_Release(channel->dvc_data); + + DeleteCriticalSection(&(channel->lock)); + free(channel->channel_name); + } + + free(channel); +} + +static void dvcman_clear(drdynvcPlugin* drdynvc, IWTSVirtualChannelManager* pChannelMgr) +{ + DVCMAN* dvcman = (DVCMAN*)pChannelMgr; + + WINPR_UNUSED(drdynvc); + + ArrayList_Clear(dvcman->plugins); + ArrayList_Clear(dvcman->channels); + ArrayList_Clear(dvcman->plugin_names); + ArrayList_Clear(dvcman->listeners); +} + +static void dvcman_free(drdynvcPlugin* drdynvc, IWTSVirtualChannelManager* pChannelMgr) +{ + DVCMAN* dvcman = (DVCMAN*)pChannelMgr; + + WINPR_UNUSED(drdynvc); + + ArrayList_Free(dvcman->plugins); + ArrayList_Free(dvcman->channels); + ArrayList_Free(dvcman->plugin_names); + ArrayList_Free(dvcman->listeners); + + StreamPool_Free(dvcman->pool); + free(dvcman); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT dvcman_init(drdynvcPlugin* drdynvc, IWTSVirtualChannelManager* pChannelMgr) +{ + int i; + DVCMAN* dvcman = (DVCMAN*)pChannelMgr; + UINT error = CHANNEL_RC_OK; + + ArrayList_Lock(dvcman->plugins); + for (i = 0; i < ArrayList_Count(dvcman->plugins); i++) + { + IWTSPlugin* pPlugin = ArrayList_GetItem(dvcman->plugins, i); + + error = IFCALLRESULT(CHANNEL_RC_OK, pPlugin->Initialize, pPlugin, pChannelMgr); + if (error != CHANNEL_RC_OK) + { + WLog_Print(drdynvc->log, WLOG_ERROR, "Initialize failed with error %" PRIu32 "!", + error); + goto fail; + } + } + +fail: + ArrayList_Unlock(dvcman->plugins); + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT dvcman_write_channel(IWTSVirtualChannel* pChannel, ULONG cbSize, const BYTE* pBuffer, + void* pReserved) +{ + BOOL close = FALSE; + UINT status; + DVCMAN_CHANNEL* channel = (DVCMAN_CHANNEL*)pChannel; + + WINPR_UNUSED(pReserved); + if (!channel || !channel->dvcman) + return CHANNEL_RC_BAD_CHANNEL; + + EnterCriticalSection(&(channel->lock)); + status = + drdynvc_write_data(channel->dvcman->drdynvc, channel->channel_id, pBuffer, cbSize, &close); + LeaveCriticalSection(&(channel->lock)); + /* Close delayed, it removes the channel struct */ + if (close) + dvcman_close_channel(channel->dvcman->drdynvc->channel_mgr, channel->channel_id, TRUE); + return status; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT dvcman_close_channel_iface(IWTSVirtualChannel* pChannel) +{ + DVCMAN_CHANNEL* channel = (DVCMAN_CHANNEL*)pChannel; + + if (!channel) + return CHANNEL_RC_BAD_CHANNEL; + + WLog_DBG(TAG, "close_channel_iface: id=%" PRIu32 "", channel->channel_id); + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT dvcman_create_channel(drdynvcPlugin* drdynvc, IWTSVirtualChannelManager* pChannelMgr, + UINT32 ChannelId, const char* ChannelName) +{ + int i; + BOOL bAccept; + DVCMAN_CHANNEL* channel; + DrdynvcClientContext* context; + DVCMAN* dvcman = (DVCMAN*)pChannelMgr; + UINT error; + + if (!(channel = dvcman_channel_new(drdynvc, pChannelMgr, ChannelId, ChannelName))) + { + WLog_Print(drdynvc->log, WLOG_ERROR, "dvcman_channel_new failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + channel->status = ERROR_NOT_CONNECTED; + if (ArrayList_Add(dvcman->channels, channel) < 0) + return ERROR_INTERNAL_ERROR; + + ArrayList_Lock(dvcman->listeners); + for (i = 0; i < ArrayList_Count(dvcman->listeners); i++) + { + DVCMAN_LISTENER* listener = (DVCMAN_LISTENER*)ArrayList_GetItem(dvcman->listeners, i); + + if (strcmp(listener->channel_name, ChannelName) == 0) + { + IWTSVirtualChannelCallback* pCallback = NULL; + channel->iface.Write = dvcman_write_channel; + channel->iface.Close = dvcman_close_channel_iface; + bAccept = TRUE; + + if ((error = listener->listener_callback->OnNewChannelConnection( + listener->listener_callback, &channel->iface, NULL, &bAccept, &pCallback)) == + CHANNEL_RC_OK && + bAccept) + { + WLog_Print(drdynvc->log, WLOG_DEBUG, "listener %s created new channel %" PRIu32 "", + listener->channel_name, channel->channel_id); + channel->status = CHANNEL_RC_OK; + channel->channel_callback = pCallback; + channel->pInterface = listener->iface.pInterface; + context = dvcman->drdynvc->context; + IFCALLRET(context->OnChannelConnected, error, context, ChannelName, + listener->iface.pInterface); + + if (error) + WLog_Print(drdynvc->log, WLOG_ERROR, + "context.OnChannelConnected failed with error %" PRIu32 "", error); + + goto fail; + } + else + { + if (error) + { + WLog_Print(drdynvc->log, WLOG_ERROR, + "OnNewChannelConnection failed with error %" PRIu32 "!", error); + goto fail; + } + else + { + WLog_Print(drdynvc->log, WLOG_ERROR, + "OnNewChannelConnection returned with bAccept FALSE!"); + error = ERROR_INTERNAL_ERROR; + goto fail; + } + } + } + } + error = ERROR_INTERNAL_ERROR; +fail: + ArrayList_Unlock(dvcman->listeners); + + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT dvcman_open_channel(drdynvcPlugin* drdynvc, IWTSVirtualChannelManager* pChannelMgr, + UINT32 ChannelId) +{ + DVCMAN_CHANNEL* channel; + IWTSVirtualChannelCallback* pCallback; + UINT error; + channel = (DVCMAN_CHANNEL*)dvcman_find_channel_by_id(pChannelMgr, ChannelId); + + if (!channel) + { + WLog_Print(drdynvc->log, WLOG_ERROR, "ChannelId %" PRIu32 " not found!", ChannelId); + return ERROR_INTERNAL_ERROR; + } + + if (channel->status == CHANNEL_RC_OK) + { + pCallback = channel->channel_callback; + + if (pCallback->OnOpen) + { + error = pCallback->OnOpen(pCallback); + if (error) + { + WLog_Print(drdynvc->log, WLOG_ERROR, "OnOpen failed with error %" PRIu32 "!", + error); + return error; + } + } + + WLog_Print(drdynvc->log, WLOG_DEBUG, "open_channel: ChannelId %" PRIu32 "", ChannelId); + } + + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT dvcman_close_channel(IWTSVirtualChannelManager* pChannelMgr, UINT32 ChannelId, + BOOL bSendClosePDU) +{ + DVCMAN_CHANNEL* channel; + UINT error = CHANNEL_RC_OK; + DVCMAN* dvcman = (DVCMAN*)pChannelMgr; + drdynvcPlugin* drdynvc = dvcman->drdynvc; + channel = (DVCMAN_CHANNEL*)dvcman_find_channel_by_id(pChannelMgr, ChannelId); + + if (!channel) + { + // WLog_Print(drdynvc->log, WLOG_ERROR, "ChannelId %"PRIu32" not found!", ChannelId); + /** + * Windows 8 / Windows Server 2012 send close requests for channels that failed to be + * created. Do not warn, simply return success here. + */ + return CHANNEL_RC_OK; + } + + if (drdynvc && bSendClosePDU) + { + wStream* s = StreamPool_Take(dvcman->pool, 5); + if (!s) + { + WLog_Print(drdynvc->log, WLOG_ERROR, "StreamPool_Take failed!"); + error = CHANNEL_RC_NO_MEMORY; + } + else + { + Stream_Write_UINT8(s, (CLOSE_REQUEST_PDU << 4) | 0x02); + Stream_Write_UINT32(s, ChannelId); + error = drdynvc_send(drdynvc, s); + } + } + + ArrayList_Remove(dvcman->channels, channel); + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT dvcman_receive_channel_data_first(drdynvcPlugin* drdynvc, + IWTSVirtualChannelManager* pChannelMgr, + UINT32 ChannelId, UINT32 length) +{ + DVCMAN_CHANNEL* channel; + channel = (DVCMAN_CHANNEL*)dvcman_find_channel_by_id(pChannelMgr, ChannelId); + + if (!channel) + { + /** + * Windows Server 2012 R2 can send some messages over + * Microsoft::Windows::RDS::Geometry::v08.01 even if the dynamic virtual channel wasn't + * registered on our side. Ignoring it works. + */ + WLog_Print(drdynvc->log, WLOG_ERROR, "ChannelId %" PRIu32 " not found!", ChannelId); + return CHANNEL_RC_OK; + } + + if (channel->dvc_data) + Stream_Release(channel->dvc_data); + + channel->dvc_data = StreamPool_Take(channel->dvcman->pool, length); + + if (!channel->dvc_data) + { + WLog_Print(drdynvc->log, WLOG_ERROR, "StreamPool_Take failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + channel->dvc_data_length = length; + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT dvcman_receive_channel_data(drdynvcPlugin* drdynvc, + IWTSVirtualChannelManager* pChannelMgr, UINT32 ChannelId, + wStream* data) +{ + UINT status = CHANNEL_RC_OK; + DVCMAN_CHANNEL* channel; + size_t dataSize = Stream_GetRemainingLength(data); + channel = (DVCMAN_CHANNEL*)dvcman_find_channel_by_id(pChannelMgr, ChannelId); + + if (!channel) + { + /* Windows 8.1 tries to open channels not created. + * Ignore cases like this. */ + WLog_Print(drdynvc->log, WLOG_ERROR, "ChannelId %" PRIu32 " not found!", ChannelId); + return CHANNEL_RC_OK; + } + + if (channel->dvc_data) + { + /* Fragmented data */ + if (Stream_GetPosition(channel->dvc_data) + dataSize > channel->dvc_data_length) + { + WLog_Print(drdynvc->log, WLOG_ERROR, "data exceeding declared length!"); + Stream_Release(channel->dvc_data); + channel->dvc_data = NULL; + return ERROR_INVALID_DATA; + } + + Stream_Copy(data, channel->dvc_data, dataSize); + + if (Stream_GetPosition(channel->dvc_data) >= channel->dvc_data_length) + { + Stream_SealLength(channel->dvc_data); + Stream_SetPosition(channel->dvc_data, 0); + status = channel->channel_callback->OnDataReceived(channel->channel_callback, + channel->dvc_data); + Stream_Release(channel->dvc_data); + channel->dvc_data = NULL; + } + } + else + { + status = channel->channel_callback->OnDataReceived(channel->channel_callback, data); + } + + return status; +} + +static UINT8 drdynvc_write_variable_uint(wStream* s, UINT32 val) +{ + UINT8 cb; + + if (val <= 0xFF) + { + cb = 0; + Stream_Write_UINT8(s, (UINT8)val); + } + else if (val <= 0xFFFF) + { + cb = 1; + Stream_Write_UINT16(s, (UINT16)val); + } + else + { + cb = 2; + Stream_Write_UINT32(s, val); + } + + return cb; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT drdynvc_send(drdynvcPlugin* drdynvc, wStream* s) +{ + UINT status; + + if (!drdynvc) + status = CHANNEL_RC_BAD_CHANNEL_HANDLE; + else + { + status = drdynvc->channelEntryPoints.pVirtualChannelWriteEx( + drdynvc->InitHandle, drdynvc->OpenHandle, Stream_Buffer(s), + (UINT32)Stream_GetPosition(s), s); + } + + switch (status) + { + case CHANNEL_RC_OK: + return CHANNEL_RC_OK; + + case CHANNEL_RC_NOT_CONNECTED: + Stream_Release(s); + return CHANNEL_RC_OK; + + case CHANNEL_RC_BAD_CHANNEL_HANDLE: + Stream_Release(s); + WLog_ERR(TAG, "VirtualChannelWriteEx failed with CHANNEL_RC_BAD_CHANNEL_HANDLE"); + return status; + + default: + Stream_Release(s); + WLog_Print(drdynvc->log, WLOG_ERROR, + "VirtualChannelWriteEx failed with %s [%08" PRIX32 "]", + WTSErrorToString(status), status); + return status; + } +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT drdynvc_write_data(drdynvcPlugin* drdynvc, UINT32 ChannelId, const BYTE* data, + UINT32 dataSize, BOOL* close) +{ + wStream* data_out; + size_t pos; + UINT8 cbChId; + UINT8 cbLen; + unsigned long chunkLength; + UINT status = CHANNEL_RC_BAD_INIT_HANDLE; + DVCMAN* dvcman; + + if (!drdynvc) + return CHANNEL_RC_BAD_CHANNEL_HANDLE; + + dvcman = (DVCMAN*)drdynvc->channel_mgr; + + WLog_Print(drdynvc->log, WLOG_DEBUG, "write_data: ChannelId=%" PRIu32 " size=%" PRIu32 "", + ChannelId, dataSize); + data_out = StreamPool_Take(dvcman->pool, CHANNEL_CHUNK_LENGTH); + + if (!data_out) + { + WLog_Print(drdynvc->log, WLOG_ERROR, "StreamPool_Take failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + Stream_SetPosition(data_out, 1); + cbChId = drdynvc_write_variable_uint(data_out, ChannelId); + pos = Stream_GetPosition(data_out); + + if (dataSize == 0) + { + *close = TRUE; + Stream_Release(data_out); + } + else if (dataSize <= CHANNEL_CHUNK_LENGTH - pos) + { + Stream_SetPosition(data_out, 0); + Stream_Write_UINT8(data_out, (DATA_PDU << 4) | cbChId); + Stream_SetPosition(data_out, pos); + Stream_Write(data_out, data, dataSize); + status = drdynvc_send(drdynvc, data_out); + } + else + { + /* Fragment the data */ + cbLen = drdynvc_write_variable_uint(data_out, dataSize); + pos = Stream_GetPosition(data_out); + Stream_SetPosition(data_out, 0); + Stream_Write_UINT8(data_out, (DATA_FIRST_PDU << 4) | cbChId | (cbLen << 2)); + Stream_SetPosition(data_out, pos); + chunkLength = CHANNEL_CHUNK_LENGTH - pos; + Stream_Write(data_out, data, chunkLength); + data += chunkLength; + dataSize -= chunkLength; + status = drdynvc_send(drdynvc, data_out); + + while (status == CHANNEL_RC_OK && dataSize > 0) + { + data_out = StreamPool_Take(dvcman->pool, CHANNEL_CHUNK_LENGTH); + + if (!data_out) + { + WLog_Print(drdynvc->log, WLOG_ERROR, "StreamPool_Take failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + Stream_SetPosition(data_out, 1); + cbChId = drdynvc_write_variable_uint(data_out, ChannelId); + pos = Stream_GetPosition(data_out); + Stream_SetPosition(data_out, 0); + Stream_Write_UINT8(data_out, (DATA_PDU << 4) | cbChId); + Stream_SetPosition(data_out, pos); + chunkLength = dataSize; + + if (chunkLength > CHANNEL_CHUNK_LENGTH - pos) + chunkLength = CHANNEL_CHUNK_LENGTH - pos; + + Stream_Write(data_out, data, chunkLength); + data += chunkLength; + dataSize -= chunkLength; + status = drdynvc_send(drdynvc, data_out); + } + } + + if (status != CHANNEL_RC_OK) + { + WLog_Print(drdynvc->log, WLOG_ERROR, "VirtualChannelWriteEx failed with %s [%08" PRIX32 "]", + WTSErrorToString(status), status); + return status; + } + + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT drdynvc_send_capability_response(drdynvcPlugin* drdynvc) +{ + UINT status; + wStream* s; + DVCMAN* dvcman; + + if (!drdynvc) + return CHANNEL_RC_BAD_CHANNEL_HANDLE; + + dvcman = (DVCMAN*)drdynvc->channel_mgr; + WLog_Print(drdynvc->log, WLOG_TRACE, "capability_response"); + s = StreamPool_Take(dvcman->pool, 4); + + if (!s) + { + WLog_Print(drdynvc->log, WLOG_ERROR, "Stream_Ndrdynvc_write_variable_uintew failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + Stream_Write_UINT16(s, 0x0050); /* Cmd+Sp+cbChId+Pad. Note: MSTSC sends 0x005c */ + Stream_Write_UINT16(s, drdynvc->version); + status = drdynvc_send(drdynvc, s); + + if (status != CHANNEL_RC_OK) + { + WLog_Print(drdynvc->log, WLOG_ERROR, "VirtualChannelWriteEx failed with %s [%08" PRIX32 "]", + WTSErrorToString(status), status); + } + + return status; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT drdynvc_process_capability_request(drdynvcPlugin* drdynvc, int Sp, int cbChId, + wStream* s) +{ + UINT status; + + if (!drdynvc) + return CHANNEL_RC_BAD_INIT_HANDLE; + + if (Stream_GetRemainingLength(s) < 3) + return ERROR_INVALID_DATA; + + WLog_Print(drdynvc->log, WLOG_TRACE, "capability_request Sp=%d cbChId=%d", Sp, cbChId); + Stream_Seek(s, 1); /* pad */ + Stream_Read_UINT16(s, drdynvc->version); + + /* RDP8 servers offer version 3, though Microsoft forgot to document it + * in their early documents. It behaves the same as version 2. + */ + if ((drdynvc->version == 2) || (drdynvc->version == 3)) + { + if (Stream_GetRemainingLength(s) < 8) + return ERROR_INVALID_DATA; + + Stream_Read_UINT16(s, drdynvc->PriorityCharge0); + Stream_Read_UINT16(s, drdynvc->PriorityCharge1); + Stream_Read_UINT16(s, drdynvc->PriorityCharge2); + Stream_Read_UINT16(s, drdynvc->PriorityCharge3); + } + + status = drdynvc_send_capability_response(drdynvc); + drdynvc->state = DRDYNVC_STATE_READY; + return status; +} + +static UINT32 drdynvc_cblen_to_bytes(int cbLen) +{ + switch (cbLen) + { + case 0: + return 1; + + case 1: + return 2; + + default: + return 4; + } +} + +static UINT32 drdynvc_read_variable_uint(wStream* s, int cbLen) +{ + UINT32 val; + + switch (cbLen) + { + case 0: + Stream_Read_UINT8(s, val); + break; + + case 1: + Stream_Read_UINT16(s, val); + break; + + default: + Stream_Read_UINT32(s, val); + break; + } + + return val; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT drdynvc_process_create_request(drdynvcPlugin* drdynvc, int Sp, int cbChId, wStream* s) +{ + size_t pos; + UINT status; + UINT32 ChannelId; + wStream* data_out; + UINT channel_status; + char* name; + size_t length; + DVCMAN* dvcman; + + WINPR_UNUSED(Sp); + if (!drdynvc) + return CHANNEL_RC_BAD_CHANNEL_HANDLE; + + dvcman = (DVCMAN*)drdynvc->channel_mgr; + if (drdynvc->state == DRDYNVC_STATE_CAPABILITIES) + { + /** + * For some reason the server does not always send the + * capabilities pdu as it should. When this happens, + * send a capabilities response. + */ + drdynvc->version = 3; + + if ((status = drdynvc_send_capability_response(drdynvc))) + { + WLog_Print(drdynvc->log, WLOG_ERROR, "drdynvc_send_capability_response failed!"); + return status; + } + + drdynvc->state = DRDYNVC_STATE_READY; + } + + if (Stream_GetRemainingLength(s) < drdynvc_cblen_to_bytes(cbChId)) + return ERROR_INVALID_DATA; + + ChannelId = drdynvc_read_variable_uint(s, cbChId); + pos = Stream_GetPosition(s); + name = (char*)Stream_Pointer(s); + length = Stream_GetRemainingLength(s); + + if (strnlen(name, length) >= length) + return ERROR_INVALID_DATA; + + WLog_Print(drdynvc->log, WLOG_DEBUG, + "process_create_request: ChannelId=%" PRIu32 " ChannelName=%s", ChannelId, name); + channel_status = dvcman_create_channel(drdynvc, drdynvc->channel_mgr, ChannelId, name); + data_out = StreamPool_Take(dvcman->pool, pos + 4); + + if (!data_out) + { + WLog_Print(drdynvc->log, WLOG_ERROR, "StreamPool_Take failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + Stream_Write_UINT8(data_out, (CREATE_REQUEST_PDU << 4) | cbChId); + Stream_SetPosition(s, 1); + Stream_Copy(s, data_out, pos - 1); + + if (channel_status == CHANNEL_RC_OK) + { + WLog_Print(drdynvc->log, WLOG_DEBUG, "channel created"); + Stream_Write_UINT32(data_out, 0); + } + else + { + WLog_Print(drdynvc->log, WLOG_DEBUG, "no listener"); + Stream_Write_UINT32(data_out, (UINT32)0xC0000001); /* same code used by mstsc */ + } + + status = drdynvc_send(drdynvc, data_out); + + if (status != CHANNEL_RC_OK) + { + WLog_Print(drdynvc->log, WLOG_ERROR, "VirtualChannelWriteEx failed with %s [%08" PRIX32 "]", + WTSErrorToString(status), status); + return status; + } + + if (channel_status == CHANNEL_RC_OK) + { + if ((status = dvcman_open_channel(drdynvc, drdynvc->channel_mgr, ChannelId))) + { + WLog_Print(drdynvc->log, WLOG_ERROR, + "dvcman_open_channel failed with error %" PRIu32 "!", status); + return status; + } + } + else + { + if ((status = dvcman_close_channel(drdynvc->channel_mgr, ChannelId, FALSE))) + WLog_Print(drdynvc->log, WLOG_ERROR, + "dvcman_close_channel failed with error %" PRIu32 "!", status); + } + + return status; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT drdynvc_process_data_first(drdynvcPlugin* drdynvc, int Sp, int cbChId, wStream* s) +{ + UINT status; + UINT32 Length; + UINT32 ChannelId; + + if (Stream_GetRemainingLength(s) < drdynvc_cblen_to_bytes(cbChId) + drdynvc_cblen_to_bytes(Sp)) + return ERROR_INVALID_DATA; + + ChannelId = drdynvc_read_variable_uint(s, cbChId); + Length = drdynvc_read_variable_uint(s, Sp); + WLog_Print(drdynvc->log, WLOG_DEBUG, + "process_data_first: Sp=%d cbChId=%d, ChannelId=%" PRIu32 " Length=%" PRIu32 "", Sp, + cbChId, ChannelId, Length); + status = dvcman_receive_channel_data_first(drdynvc, drdynvc->channel_mgr, ChannelId, Length); + + if (status == CHANNEL_RC_OK) + status = dvcman_receive_channel_data(drdynvc, drdynvc->channel_mgr, ChannelId, s); + + if (status != CHANNEL_RC_OK) + status = dvcman_close_channel(drdynvc->channel_mgr, ChannelId, TRUE); + + return status; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT drdynvc_process_data(drdynvcPlugin* drdynvc, int Sp, int cbChId, wStream* s) +{ + UINT32 ChannelId; + UINT status; + + if (Stream_GetRemainingLength(s) < drdynvc_cblen_to_bytes(cbChId)) + return ERROR_INVALID_DATA; + + ChannelId = drdynvc_read_variable_uint(s, cbChId); + WLog_Print(drdynvc->log, WLOG_TRACE, "process_data: Sp=%d cbChId=%d, ChannelId=%" PRIu32 "", Sp, + cbChId, ChannelId); + status = dvcman_receive_channel_data(drdynvc, drdynvc->channel_mgr, ChannelId, s); + + if (status != CHANNEL_RC_OK) + status = dvcman_close_channel(drdynvc->channel_mgr, ChannelId, TRUE); + + return status; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT drdynvc_process_close_request(drdynvcPlugin* drdynvc, int Sp, int cbChId, wStream* s) +{ + UINT error; + UINT32 ChannelId; + + if (Stream_GetRemainingLength(s) < drdynvc_cblen_to_bytes(cbChId)) + return ERROR_INVALID_DATA; + + ChannelId = drdynvc_read_variable_uint(s, cbChId); + WLog_Print(drdynvc->log, WLOG_DEBUG, + "process_close_request: Sp=%d cbChId=%d, ChannelId=%" PRIu32 "", Sp, cbChId, + ChannelId); + + if ((error = dvcman_close_channel(drdynvc->channel_mgr, ChannelId, TRUE))) + WLog_Print(drdynvc->log, WLOG_ERROR, "dvcman_close_channel failed with error %" PRIu32 "!", + error); + + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT drdynvc_order_recv(drdynvcPlugin* drdynvc, wStream* s) +{ + int value; + int Cmd; + int Sp; + int cbChId; + + if (Stream_GetRemainingLength(s) < 1) + return ERROR_INVALID_DATA; + + Stream_Read_UINT8(s, value); + Cmd = (value & 0xf0) >> 4; + Sp = (value & 0x0c) >> 2; + cbChId = (value & 0x03) >> 0; + WLog_Print(drdynvc->log, WLOG_DEBUG, "order_recv: Cmd=0x%x, Sp=%d cbChId=%d", Cmd, Sp, cbChId); + + switch (Cmd) + { + case CAPABILITY_REQUEST_PDU: + return drdynvc_process_capability_request(drdynvc, Sp, cbChId, s); + + case CREATE_REQUEST_PDU: + return drdynvc_process_create_request(drdynvc, Sp, cbChId, s); + + case DATA_FIRST_PDU: + return drdynvc_process_data_first(drdynvc, Sp, cbChId, s); + + case DATA_PDU: + return drdynvc_process_data(drdynvc, Sp, cbChId, s); + + case CLOSE_REQUEST_PDU: + return drdynvc_process_close_request(drdynvc, Sp, cbChId, s); + + default: + WLog_Print(drdynvc->log, WLOG_ERROR, "unknown drdynvc cmd 0x%x", Cmd); + return ERROR_INTERNAL_ERROR; + } +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT drdynvc_virtual_channel_event_data_received(drdynvcPlugin* drdynvc, void* pData, + UINT32 dataLength, UINT32 totalLength, + UINT32 dataFlags) +{ + wStream* data_in; + + if ((dataFlags & CHANNEL_FLAG_SUSPEND) || (dataFlags & CHANNEL_FLAG_RESUME)) + { + return CHANNEL_RC_OK; + } + + if (dataFlags & CHANNEL_FLAG_FIRST) + { + DVCMAN* mgr = (DVCMAN*)drdynvc->channel_mgr; + if (drdynvc->data_in) + Stream_Release(drdynvc->data_in); + + drdynvc->data_in = StreamPool_Take(mgr->pool, totalLength); + } + + if (!(data_in = drdynvc->data_in)) + { + WLog_Print(drdynvc->log, WLOG_ERROR, "StreamPool_Take failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + if (!Stream_EnsureRemainingCapacity(data_in, dataLength)) + { + WLog_Print(drdynvc->log, WLOG_ERROR, "Stream_EnsureRemainingCapacity failed!"); + Stream_Release(drdynvc->data_in); + drdynvc->data_in = NULL; + return ERROR_INTERNAL_ERROR; + } + + Stream_Write(data_in, pData, dataLength); + + if (dataFlags & CHANNEL_FLAG_LAST) + { + const size_t cap = Stream_Capacity(data_in); + const size_t pos = Stream_GetPosition(data_in); + if (cap < pos) + { + WLog_Print(drdynvc->log, WLOG_ERROR, "drdynvc_plugin_process_received: read error"); + return ERROR_INVALID_DATA; + } + + drdynvc->data_in = NULL; + Stream_SealLength(data_in); + Stream_SetPosition(data_in, 0); + + if (!MessageQueue_Post(drdynvc->queue, NULL, 0, (void*)data_in, NULL)) + { + WLog_Print(drdynvc->log, WLOG_ERROR, "MessageQueue_Post failed!"); + return ERROR_INTERNAL_ERROR; + } + } + + return CHANNEL_RC_OK; +} + +static void VCAPITYPE drdynvc_virtual_channel_open_event_ex(LPVOID lpUserParam, DWORD openHandle, + UINT event, LPVOID pData, + UINT32 dataLength, UINT32 totalLength, + UINT32 dataFlags) +{ + UINT error = CHANNEL_RC_OK; + drdynvcPlugin* drdynvc = (drdynvcPlugin*)lpUserParam; + + switch (event) + { + case CHANNEL_EVENT_DATA_RECEIVED: + if (!drdynvc || (drdynvc->OpenHandle != openHandle)) + { + WLog_ERR(TAG, "drdynvc_virtual_channel_open_event: error no match"); + return; + } + if ((error = drdynvc_virtual_channel_event_data_received(drdynvc, pData, dataLength, + totalLength, dataFlags))) + WLog_Print(drdynvc->log, WLOG_ERROR, + "drdynvc_virtual_channel_event_data_received failed with error %" PRIu32 + "", + error); + + break; + + case CHANNEL_EVENT_WRITE_CANCELLED: + case CHANNEL_EVENT_WRITE_COMPLETE: + { + wStream* s = (wStream*)pData; + Stream_Release(s); + } + break; + + case CHANNEL_EVENT_USER: + break; + } + + if (error && drdynvc && drdynvc->rdpcontext) + setChannelError(drdynvc->rdpcontext, error, + "drdynvc_virtual_channel_open_event reported an error"); +} + +static DWORD WINAPI drdynvc_virtual_channel_client_thread(LPVOID arg) +{ + wStream* data; + wMessage message; + UINT error = CHANNEL_RC_OK; + drdynvcPlugin* drdynvc = (drdynvcPlugin*)arg; + + if (!drdynvc) + { + ExitThread((DWORD)CHANNEL_RC_BAD_CHANNEL_HANDLE); + return CHANNEL_RC_BAD_CHANNEL_HANDLE; + } + + while (1) + { + if (!MessageQueue_Wait(drdynvc->queue)) + { + WLog_Print(drdynvc->log, WLOG_ERROR, "MessageQueue_Wait failed!"); + error = ERROR_INTERNAL_ERROR; + break; + } + + if (!MessageQueue_Peek(drdynvc->queue, &message, TRUE)) + { + WLog_Print(drdynvc->log, WLOG_ERROR, "MessageQueue_Peek failed!"); + error = ERROR_INTERNAL_ERROR; + break; + } + + if (message.id == WMQ_QUIT) + break; + + if (message.id == 0) + { + data = (wStream*)message.wParam; + + if ((error = drdynvc_order_recv(drdynvc, data))) + { + WLog_Print(drdynvc->log, WLOG_WARN, + "drdynvc_order_recv failed with error %" PRIu32 "!", error); + } + + Stream_Release(data); + } + } + + { + /* Disconnect remaining dynamic channels that the server did not. + * This is required to properly shut down channels by calling the appropriate + * event handlers. */ + size_t count = 0; + DVCMAN* drdynvcMgr = (DVCMAN*)drdynvc->channel_mgr; + + do + { + ArrayList_Lock(drdynvcMgr->channels); + count = ArrayList_Count(drdynvcMgr->channels); + if (count > 0) + { + IWTSVirtualChannel* channel = + (IWTSVirtualChannel*)ArrayList_GetItem(drdynvcMgr->channels, 0); + const UINT32 ChannelId = drdynvc->channel_mgr->GetChannelId(channel); + dvcman_close_channel(drdynvc->channel_mgr, ChannelId, FALSE); + count--; + } + ArrayList_Unlock(drdynvcMgr->channels); + } while (count > 0); + } + + if (error && drdynvc->rdpcontext) + setChannelError(drdynvc->rdpcontext, error, + "drdynvc_virtual_channel_client_thread reported an error"); + + ExitThread((DWORD)error); + return error; +} + +static void drdynvc_queue_object_free(void* obj) +{ + wStream* s; + wMessage* msg = (wMessage*)obj; + + if (!msg || (msg->id != 0)) + return; + + s = (wStream*)msg->wParam; + + if (s) + Stream_Release(s); +} + +static UINT drdynvc_virtual_channel_event_initialized(drdynvcPlugin* drdynvc, LPVOID pData, + UINT32 dataLength) +{ + UINT error = CHANNEL_RC_OK; + WINPR_UNUSED(pData); + WINPR_UNUSED(dataLength); + + if (!drdynvc) + goto error; + + drdynvc->queue = MessageQueue_New(NULL); + + if (!drdynvc->queue) + { + error = CHANNEL_RC_NO_MEMORY; + WLog_Print(drdynvc->log, WLOG_ERROR, "MessageQueue_New failed!"); + goto error; + } + + drdynvc->queue->object.fnObjectFree = drdynvc_queue_object_free; + drdynvc->channel_mgr = dvcman_new(drdynvc); + + if (!drdynvc->channel_mgr) + { + error = CHANNEL_RC_NO_MEMORY; + WLog_Print(drdynvc->log, WLOG_ERROR, "dvcman_new failed!"); + goto error; + } + + return CHANNEL_RC_OK; +error: + return ERROR_INTERNAL_ERROR; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT drdynvc_virtual_channel_event_connected(drdynvcPlugin* drdynvc, LPVOID pData, + UINT32 dataLength) +{ + UINT error; + UINT32 status; + UINT32 index; + ADDIN_ARGV* args; + rdpSettings* settings; + + WINPR_UNUSED(pData); + WINPR_UNUSED(dataLength); + + if (!drdynvc) + return CHANNEL_RC_BAD_CHANNEL_HANDLE; + + status = drdynvc->channelEntryPoints.pVirtualChannelOpenEx( + drdynvc->InitHandle, &drdynvc->OpenHandle, drdynvc->channelDef.name, + drdynvc_virtual_channel_open_event_ex); + + if (status != CHANNEL_RC_OK) + { + WLog_Print(drdynvc->log, WLOG_ERROR, "pVirtualChannelOpen failed with %s [%08" PRIX32 "]", + WTSErrorToString(status), status); + return status; + } + + settings = (rdpSettings*)drdynvc->channelEntryPoints.pExtendedData; + + for (index = 0; index < settings->DynamicChannelCount; index++) + { + args = settings->DynamicChannelArray[index]; + error = dvcman_load_addin(drdynvc, drdynvc->channel_mgr, args, settings); + + if (CHANNEL_RC_OK != error) + goto error; + } + + if ((error = dvcman_init(drdynvc, drdynvc->channel_mgr))) + { + WLog_Print(drdynvc->log, WLOG_ERROR, "dvcman_init failed with error %" PRIu32 "!", error); + goto error; + } + + drdynvc->state = DRDYNVC_STATE_CAPABILITIES; + + if (!(drdynvc->thread = CreateThread(NULL, 0, drdynvc_virtual_channel_client_thread, + (void*)drdynvc, 0, NULL))) + { + error = ERROR_INTERNAL_ERROR; + WLog_Print(drdynvc->log, WLOG_ERROR, "CreateThread failed!"); + goto error; + } + +error: + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT drdynvc_virtual_channel_event_disconnected(drdynvcPlugin* drdynvc) +{ + UINT status; + + if (!drdynvc) + return CHANNEL_RC_BAD_CHANNEL_HANDLE; + + if (drdynvc->OpenHandle == 0) + return CHANNEL_RC_OK; + + if (!MessageQueue_PostQuit(drdynvc->queue, 0)) + { + status = GetLastError(); + WLog_Print(drdynvc->log, WLOG_ERROR, "MessageQueue_PostQuit failed with error %" PRIu32 "", + status); + return status; + } + + if (WaitForSingleObject(drdynvc->thread, INFINITE) != WAIT_OBJECT_0) + { + status = GetLastError(); + WLog_Print(drdynvc->log, WLOG_ERROR, "WaitForSingleObject failed with error %" PRIu32 "", + status); + return status; + } + + CloseHandle(drdynvc->thread); + drdynvc->thread = NULL; + + status = drdynvc->channelEntryPoints.pVirtualChannelCloseEx(drdynvc->InitHandle, + drdynvc->OpenHandle); + + if (status != CHANNEL_RC_OK) + { + WLog_Print(drdynvc->log, WLOG_ERROR, "pVirtualChannelClose failed with %s [%08" PRIX32 "]", + WTSErrorToString(status), status); + } + + dvcman_clear(drdynvc, drdynvc->channel_mgr); + MessageQueue_Clear(drdynvc->queue); + drdynvc->OpenHandle = 0; + + if (drdynvc->data_in) + { + Stream_Release(drdynvc->data_in); + drdynvc->data_in = NULL; + } + + return status; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT drdynvc_virtual_channel_event_terminated(drdynvcPlugin* drdynvc) +{ + if (!drdynvc) + return CHANNEL_RC_BAD_CHANNEL_HANDLE; + + MessageQueue_Free(drdynvc->queue); + drdynvc->queue = NULL; + + if (drdynvc->channel_mgr) + { + dvcman_free(drdynvc, drdynvc->channel_mgr); + drdynvc->channel_mgr = NULL; + } + + drdynvc->InitHandle = 0; + free(drdynvc->context); + free(drdynvc); + return CHANNEL_RC_OK; +} + +static UINT drdynvc_virtual_channel_event_attached(drdynvcPlugin* drdynvc) +{ + UINT error = CHANNEL_RC_OK; + int i; + DVCMAN* dvcman; + + if (!drdynvc) + return CHANNEL_RC_BAD_CHANNEL_HANDLE; + + dvcman = (DVCMAN*)drdynvc->channel_mgr; + + if (!dvcman) + return CHANNEL_RC_BAD_CHANNEL_HANDLE; + + ArrayList_Lock(dvcman->plugins); + for (i = 0; i < ArrayList_Count(dvcman->plugins); i++) + { + IWTSPlugin* pPlugin = ArrayList_GetItem(dvcman->plugins, i); + + error = IFCALLRESULT(CHANNEL_RC_OK, pPlugin->Attached, pPlugin); + if (error != CHANNEL_RC_OK) + { + WLog_Print(drdynvc->log, WLOG_ERROR, "Attach failed with error %" PRIu32 "!", error); + goto fail; + } + } + +fail: + ArrayList_Unlock(dvcman->plugins); + return error; +} + +static UINT drdynvc_virtual_channel_event_detached(drdynvcPlugin* drdynvc) +{ + UINT error = CHANNEL_RC_OK; + int i; + DVCMAN* dvcman; + + if (!drdynvc) + return CHANNEL_RC_BAD_CHANNEL_HANDLE; + + dvcman = (DVCMAN*)drdynvc->channel_mgr; + + if (!dvcman) + return CHANNEL_RC_BAD_CHANNEL_HANDLE; + + ArrayList_Lock(dvcman->plugins); + for (i = 0; i < ArrayList_Count(dvcman->plugins); i++) + { + IWTSPlugin* pPlugin = ArrayList_GetItem(dvcman->plugins, i); + + error = IFCALLRESULT(CHANNEL_RC_OK, pPlugin->Detached, pPlugin); + if (error != CHANNEL_RC_OK) + { + WLog_Print(drdynvc->log, WLOG_ERROR, "Detach failed with error %" PRIu32 "!", error); + goto fail; + } + } + +fail: + ArrayList_Unlock(dvcman->plugins); + + return error; +} + +static VOID VCAPITYPE drdynvc_virtual_channel_init_event_ex(LPVOID lpUserParam, LPVOID pInitHandle, + UINT event, LPVOID pData, + UINT dataLength) +{ + UINT error = CHANNEL_RC_OK; + drdynvcPlugin* drdynvc = (drdynvcPlugin*)lpUserParam; + + if (!drdynvc || (drdynvc->InitHandle != pInitHandle)) + { + WLog_ERR(TAG, "drdynvc_virtual_channel_init_event: error no match"); + return; + } + + switch (event) + { + case CHANNEL_EVENT_INITIALIZED: + error = drdynvc_virtual_channel_event_initialized(drdynvc, pData, dataLength); + break; + case CHANNEL_EVENT_CONNECTED: + if ((error = drdynvc_virtual_channel_event_connected(drdynvc, pData, dataLength))) + WLog_Print(drdynvc->log, WLOG_ERROR, + "drdynvc_virtual_channel_event_connected failed with error %" PRIu32 "", + error); + + break; + + case CHANNEL_EVENT_DISCONNECTED: + if ((error = drdynvc_virtual_channel_event_disconnected(drdynvc))) + WLog_Print(drdynvc->log, WLOG_ERROR, + "drdynvc_virtual_channel_event_disconnected failed with error %" PRIu32 + "", + error); + + break; + + case CHANNEL_EVENT_TERMINATED: + if ((error = drdynvc_virtual_channel_event_terminated(drdynvc))) + WLog_Print(drdynvc->log, WLOG_ERROR, + "drdynvc_virtual_channel_event_terminated failed with error %" PRIu32 "", + error); + + break; + + case CHANNEL_EVENT_ATTACHED: + if ((error = drdynvc_virtual_channel_event_attached(drdynvc))) + WLog_Print(drdynvc->log, WLOG_ERROR, + "drdynvc_virtual_channel_event_attached failed with error %" PRIu32 "", + error); + + break; + + case CHANNEL_EVENT_DETACHED: + if ((error = drdynvc_virtual_channel_event_detached(drdynvc))) + WLog_Print(drdynvc->log, WLOG_ERROR, + "drdynvc_virtual_channel_event_detached failed with error %" PRIu32 "", + error); + + break; + + default: + break; + } + + if (error && drdynvc->rdpcontext) + setChannelError(drdynvc->rdpcontext, error, + "drdynvc_virtual_channel_init_event_ex reported an error"); +} + +/** + * Channel Client Interface + */ + +static int drdynvc_get_version(DrdynvcClientContext* context) +{ + drdynvcPlugin* drdynvc = (drdynvcPlugin*)context->handle; + return drdynvc->version; +} + +/* drdynvc is always built-in */ +#define VirtualChannelEntryEx drdynvc_VirtualChannelEntryEx + +BOOL VCAPITYPE VirtualChannelEntryEx(PCHANNEL_ENTRY_POINTS_EX pEntryPoints, PVOID pInitHandle) +{ + UINT rc; + drdynvcPlugin* drdynvc; + DrdynvcClientContext* context = NULL; + CHANNEL_ENTRY_POINTS_FREERDP_EX* pEntryPointsEx; + drdynvc = (drdynvcPlugin*)calloc(1, sizeof(drdynvcPlugin)); + + if (!drdynvc) + { + WLog_ERR(TAG, "calloc failed!"); + return FALSE; + } + + drdynvc->channelDef.options = + CHANNEL_OPTION_INITIALIZED | CHANNEL_OPTION_ENCRYPT_RDP | CHANNEL_OPTION_COMPRESS_RDP; + sprintf_s(drdynvc->channelDef.name, ARRAYSIZE(drdynvc->channelDef.name), "drdynvc"); + drdynvc->state = DRDYNVC_STATE_INITIAL; + pEntryPointsEx = (CHANNEL_ENTRY_POINTS_FREERDP_EX*)pEntryPoints; + + if ((pEntryPointsEx->cbSize >= sizeof(CHANNEL_ENTRY_POINTS_FREERDP_EX)) && + (pEntryPointsEx->MagicNumber == FREERDP_CHANNEL_MAGIC_NUMBER)) + { + context = (DrdynvcClientContext*)calloc(1, sizeof(DrdynvcClientContext)); + + if (!context) + { + WLog_Print(drdynvc->log, WLOG_ERROR, "calloc failed!"); + free(drdynvc); + return FALSE; + } + + context->handle = (void*)drdynvc; + context->custom = NULL; + drdynvc->context = context; + context->GetVersion = drdynvc_get_version; + drdynvc->rdpcontext = pEntryPointsEx->context; + } + + drdynvc->log = WLog_Get(TAG); + WLog_Print(drdynvc->log, WLOG_DEBUG, "VirtualChannelEntryEx"); + CopyMemory(&(drdynvc->channelEntryPoints), pEntryPoints, + sizeof(CHANNEL_ENTRY_POINTS_FREERDP_EX)); + drdynvc->InitHandle = pInitHandle; + rc = drdynvc->channelEntryPoints.pVirtualChannelInitEx( + drdynvc, context, pInitHandle, &drdynvc->channelDef, 1, VIRTUAL_CHANNEL_VERSION_WIN2000, + drdynvc_virtual_channel_init_event_ex); + + if (CHANNEL_RC_OK != rc) + { + WLog_Print(drdynvc->log, WLOG_ERROR, "pVirtualChannelInit failed with %s [%08" PRIX32 "]", + WTSErrorToString(rc), rc); + free(drdynvc->context); + free(drdynvc); + return FALSE; + } + + drdynvc->channelEntryPoints.pInterface = context; + return TRUE; +} diff --git a/channels/drdynvc/client/drdynvc_main.h b/channels/drdynvc/client/drdynvc_main.h new file mode 100644 index 0000000..646c8fd --- /dev/null +++ b/channels/drdynvc/client/drdynvc_main.h @@ -0,0 +1,135 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Dynamic Virtual Channel + * + * Copyright 2010-2011 Vic Lee + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger + * + * Licensed 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 FREERDP_CHANNEL_DRDYNVC_CLIENT_MAIN_H +#define FREERDP_CHANNEL_DRDYNVC_CLIENT_MAIN_H + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +typedef struct drdynvc_plugin drdynvcPlugin; + +struct _DVCMAN +{ + IWTSVirtualChannelManager iface; + + drdynvcPlugin* drdynvc; + + wArrayList* plugin_names; + wArrayList* plugins; + + wArrayList* listeners; + wArrayList* channels; + wStreamPool* pool; +}; +typedef struct _DVCMAN DVCMAN; + +struct _DVCMAN_LISTENER +{ + IWTSListener iface; + + DVCMAN* dvcman; + char* channel_name; + UINT32 flags; + IWTSListenerCallback* listener_callback; +}; +typedef struct _DVCMAN_LISTENER DVCMAN_LISTENER; + +struct _DVCMAN_ENTRY_POINTS +{ + IDRDYNVC_ENTRY_POINTS iface; + + DVCMAN* dvcman; + ADDIN_ARGV* args; + rdpSettings* settings; +}; +typedef struct _DVCMAN_ENTRY_POINTS DVCMAN_ENTRY_POINTS; + +struct _DVCMAN_CHANNEL +{ + IWTSVirtualChannel iface; + + int status; + DVCMAN* dvcman; + void* pInterface; + UINT32 channel_id; + char* channel_name; + IWTSVirtualChannelCallback* channel_callback; + + wStream* dvc_data; + UINT32 dvc_data_length; + CRITICAL_SECTION lock; +}; +typedef struct _DVCMAN_CHANNEL DVCMAN_CHANNEL; + +enum _DRDYNVC_STATE +{ + DRDYNVC_STATE_INITIAL, + DRDYNVC_STATE_CAPABILITIES, + DRDYNVC_STATE_READY, + DRDYNVC_STATE_OPENING_CHANNEL, + DRDYNVC_STATE_SEND_RECEIVE, + DRDYNVC_STATE_FINAL +}; +typedef enum _DRDYNVC_STATE DRDYNVC_STATE; + +#define CREATE_REQUEST_PDU 0x01 +#define DATA_FIRST_PDU 0x02 +#define DATA_PDU 0x03 +#define CLOSE_REQUEST_PDU 0x04 +#define CAPABILITY_REQUEST_PDU 0x05 + +struct drdynvc_plugin +{ + CHANNEL_DEF channelDef; + CHANNEL_ENTRY_POINTS_FREERDP_EX channelEntryPoints; + + wLog* log; + HANDLE thread; + wStream* data_in; + void* InitHandle; + DWORD OpenHandle; + wMessageQueue* queue; + + DRDYNVC_STATE state; + DrdynvcClientContext* context; + + UINT16 version; + int PriorityCharge0; + int PriorityCharge1; + int PriorityCharge2; + int PriorityCharge3; + rdpContext* rdpcontext; + + IWTSVirtualChannelManager* channel_mgr; +}; + +#endif /* FREERDP_CHANNEL_DRDYNVC_CLIENT_MAIN_H */ diff --git a/channels/drdynvc/server/CMakeLists.txt b/channels/drdynvc/server/CMakeLists.txt new file mode 100644 index 0000000..fe2bd61 --- /dev/null +++ b/channels/drdynvc/server/CMakeLists.txt @@ -0,0 +1,31 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2012 Marc-Andre Moreau +# +# Licensed 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. + +define_channel_server("drdynvc") + +set(${MODULE_PREFIX}_SRCS + drdynvc_main.c + drdynvc_main.h) + +add_channel_server_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} FALSE "VirtualChannelEntry") + + + +target_link_libraries(${MODULE_NAME} freerdp) + + +set_property(TARGET ${MODULE_NAME} PROPERTY FOLDER "Channels/${CHANNEL_NAME}/Server") diff --git a/channels/drdynvc/server/drdynvc_main.c b/channels/drdynvc/server/drdynvc_main.c new file mode 100644 index 0000000..66440b2 --- /dev/null +++ b/channels/drdynvc/server/drdynvc_main.c @@ -0,0 +1,205 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Dynamic Virtual Channel Extension + * + * Copyright 2013 Marc-Andre Moreau + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include +#include + +#include "drdynvc_main.h" + +#define TAG CHANNELS_TAG("drdynvc.server") + +static DWORD WINAPI drdynvc_server_thread(LPVOID arg) +{ +#if 0 + wStream* s; + DWORD status; + DWORD nCount; + void* buffer; + HANDLE events[8]; + HANDLE ChannelEvent; + DWORD BytesReturned; + DrdynvcServerContext* context; + UINT error = ERROR_INTERNAL_ERROR; + context = (DrdynvcServerContext*) arg; + buffer = NULL; + BytesReturned = 0; + ChannelEvent = NULL; + + s = Stream_New(NULL, 4096); + + if (!s) + { + WLog_ERR(TAG, "Stream_New failed!"); + ExitThread((DWORD) CHANNEL_RC_NO_MEMORY); + return CHANNEL_RC_NO_MEMORY; + } + + if (WTSVirtualChannelQuery(context->priv->ChannelHandle, WTSVirtualEventHandle, + &buffer, &BytesReturned) == TRUE) + { + if (BytesReturned == sizeof(HANDLE)) + CopyMemory(&ChannelEvent, buffer, sizeof(HANDLE)); + + WTSFreeMemory(buffer); + } + + nCount = 0; + events[nCount++] = ChannelEvent; + events[nCount++] = context->priv->StopEvent; + + while (1) + { + status = WaitForMultipleObjects(nCount, events, FALSE, INFINITE); + + if (WaitForSingleObject(context->priv->StopEvent, 0) == WAIT_OBJECT_0) + { + error = CHANNEL_RC_OK; + break; + } + + if (!WTSVirtualChannelRead(context->priv->ChannelHandle, 0, NULL, 0, + &BytesReturned)) + { + WLog_ERR(TAG, "WTSVirtualChannelRead failed!"); + break; + } + + if (BytesReturned < 1) + continue; + + if (!Stream_EnsureRemainingCapacity(s, BytesReturned)) + { + WLog_ERR(TAG, "Stream_EnsureRemainingCapacity failed!"); + break; + } + + if (!WTSVirtualChannelRead(context->priv->ChannelHandle, 0, + (PCHAR) Stream_Buffer(s), Stream_Capacity(s), &BytesReturned)) + { + WLog_ERR(TAG, "WTSVirtualChannelRead failed!"); + break; + } + } + + Stream_Free(s, TRUE); + ExitThread((DWORD) error); +#endif + // WTF ... this code only reads data into the stream until there is no more memory + ExitThread(0); + return 0; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT drdynvc_server_start(DrdynvcServerContext* context) +{ + context->priv->ChannelHandle = + WTSVirtualChannelOpen(context->vcm, WTS_CURRENT_SESSION, "drdynvc"); + + if (!context->priv->ChannelHandle) + { + WLog_ERR(TAG, "WTSVirtualChannelOpen failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + if (!(context->priv->StopEvent = CreateEvent(NULL, TRUE, FALSE, NULL))) + { + WLog_ERR(TAG, "CreateEvent failed!"); + return ERROR_INTERNAL_ERROR; + } + + if (!(context->priv->Thread = + CreateThread(NULL, 0, drdynvc_server_thread, (void*)context, 0, NULL))) + { + WLog_ERR(TAG, "CreateThread failed!"); + CloseHandle(context->priv->StopEvent); + context->priv->StopEvent = NULL; + return ERROR_INTERNAL_ERROR; + } + + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT drdynvc_server_stop(DrdynvcServerContext* context) +{ + UINT error; + SetEvent(context->priv->StopEvent); + + if (WaitForSingleObject(context->priv->Thread, INFINITE) == WAIT_FAILED) + { + error = GetLastError(); + WLog_ERR(TAG, "WaitForSingleObject failed with error %" PRIu32 "!", error); + return error; + } + + CloseHandle(context->priv->Thread); + return CHANNEL_RC_OK; +} + +DrdynvcServerContext* drdynvc_server_context_new(HANDLE vcm) +{ + DrdynvcServerContext* context; + context = (DrdynvcServerContext*)calloc(1, sizeof(DrdynvcServerContext)); + + if (context) + { + context->vcm = vcm; + context->Start = drdynvc_server_start; + context->Stop = drdynvc_server_stop; + context->priv = (DrdynvcServerPrivate*)calloc(1, sizeof(DrdynvcServerPrivate)); + + if (!context->priv) + { + WLog_ERR(TAG, "calloc failed!"); + free(context); + return NULL; + } + } + else + { + WLog_ERR(TAG, "calloc failed!"); + } + + return context; +} + +void drdynvc_server_context_free(DrdynvcServerContext* context) +{ + if (context) + { + free(context->priv); + free(context); + } +} diff --git a/channels/drdynvc/server/drdynvc_main.h b/channels/drdynvc/server/drdynvc_main.h new file mode 100644 index 0000000..8e17f89 --- /dev/null +++ b/channels/drdynvc/server/drdynvc_main.h @@ -0,0 +1,37 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Dynamic Virtual Channel Extension + * + * Copyright 2013 Marc-Andre Moreau + * + * Licensed 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 FREERDP_CHANNEL_DRDYNVC_SERVER_MAIN_H +#define FREERDP_CHANNEL_DRDYNVC_SERVER_MAIN_H + +#include +#include +#include + +#include +#include + +struct _drdynvc_server_private +{ + HANDLE Thread; + HANDLE StopEvent; + void* ChannelHandle; +}; + +#endif /* FREERDP_CHANNEL_DRDYNVC_SERVER_MAIN_H */ diff --git a/channels/drive/CMakeLists.txt b/channels/drive/CMakeLists.txt new file mode 100644 index 0000000..f2d2c5a --- /dev/null +++ b/channels/drive/CMakeLists.txt @@ -0,0 +1,23 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2012 Marc-Andre Moreau +# +# Licensed 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. + +define_channel("drive") + +if(WITH_CLIENT_CHANNELS) + add_channel_client(${MODULE_PREFIX} ${CHANNEL_NAME}) +endif() + diff --git a/channels/drive/ChannelOptions.cmake b/channels/drive/ChannelOptions.cmake new file mode 100644 index 0000000..0792c1e --- /dev/null +++ b/channels/drive/ChannelOptions.cmake @@ -0,0 +1,13 @@ + +set(OPTION_DEFAULT OFF) +set(OPTION_CLIENT_DEFAULT ON) +set(OPTION_SERVER_DEFAULT OFF) + +define_channel_options(NAME "drive" TYPE "device" + DESCRIPTION "Drive Redirection Virtual Channel Extension" + SPECIFICATIONS "[MS-RDPEFS]" + DEFAULT ${OPTION_DEFAULT}) + +define_channel_client_options(${OPTION_CLIENT_DEFAULT}) +define_channel_server_options(${OPTION_SERVER_DEFAULT}) + diff --git a/channels/drive/client/CMakeLists.txt b/channels/drive/client/CMakeLists.txt new file mode 100644 index 0000000..2c2be39 --- /dev/null +++ b/channels/drive/client/CMakeLists.txt @@ -0,0 +1,36 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2012 Marc-Andre Moreau +# +# Licensed 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. + +define_channel_client("drive") + +set(${MODULE_PREFIX}_SRCS + drive_file.c + drive_file.h + drive_main.c) + +add_channel_client_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} TRUE "DeviceServiceEntry") + + + +target_link_libraries(${MODULE_NAME} winpr freerdp) + + +if (WITH_DEBUG_SYMBOLS AND MSVC AND NOT BUILTIN_CHANNELS AND BUILD_SHARED_LIBS) + install(FILES ${CMAKE_BINARY_DIR}/${MODULE_NAME}.pdb DESTINATION ${FREERDP_ADDIN_PATH} COMPONENT symbols) +endif() + +set_property(TARGET ${MODULE_NAME} PROPERTY FOLDER "Channels/${CHANNEL_NAME}/Client") diff --git a/channels/drive/client/drive_file.c b/channels/drive/client/drive_file.c new file mode 100644 index 0000000..3054385 --- /dev/null +++ b/channels/drive/client/drive_file.c @@ -0,0 +1,934 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * File System Virtual Channel + * + * Copyright 2010-2012 Marc-Andre Moreau + * Copyright 2010-2011 Vic Lee + * Copyright 2012 Gerald Richter + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger + * Copyright 2016 Inuvika Inc. + * Copyright 2016 David PHAM-VAN + * Copyright 2017 Armin Novak + * Copyright 2017 Thincast Technologies GmbH + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include + +#include "drive_file.h" + +#ifdef WITH_DEBUG_RDPDR +#define DEBUG_WSTR(msg, wstr) \ + do \ + { \ + LPSTR lpstr; \ + ConvertFromUnicode(CP_UTF8, 0, wstr, -1, &lpstr, 0, NULL, NULL); \ + WLog_DBG(TAG, msg, lpstr); \ + free(lpstr); \ + } while (0) +#else +#define DEBUG_WSTR(msg, wstr) \ + do \ + { \ + } while (0) +#endif + +static void drive_file_fix_path(WCHAR* path) +{ + size_t i; + size_t length = _wcslen(path); + + for (i = 0; i < length; i++) + { + if (path[i] == L'\\') + path[i] = L'/'; + } + +#ifdef WIN32 + + if ((length == 3) && (path[1] == L':') && (path[2] == L'/')) + return; + +#else + + if ((length == 1) && (path[0] == L'/')) + return; + +#endif + + if ((length > 0) && (path[length - 1] == L'/')) + path[length - 1] = L'\0'; +} + +static WCHAR* drive_file_combine_fullpath(const WCHAR* base_path, const WCHAR* path, + size_t PathLength) +{ + WCHAR* fullpath; + size_t base_path_length; + + if (!base_path || (!path && (PathLength > 0))) + return NULL; + + base_path_length = _wcslen(base_path) * 2; + fullpath = (WCHAR*)calloc(1, base_path_length + PathLength + sizeof(WCHAR)); + + if (!fullpath) + { + WLog_ERR(TAG, "malloc failed!"); + return NULL; + } + + CopyMemory(fullpath, base_path, base_path_length); + if (path) + CopyMemory((char*)fullpath + base_path_length, path, PathLength); + drive_file_fix_path(fullpath); + return fullpath; +} + +static BOOL drive_file_remove_dir(const WCHAR* path) +{ + WIN32_FIND_DATAW findFileData; + BOOL ret = TRUE; + HANDLE dir; + WCHAR* fullpath; + WCHAR* path_slash; + size_t base_path_length; + + if (!path) + return FALSE; + + base_path_length = _wcslen(path) * 2; + path_slash = (WCHAR*)calloc(1, base_path_length + sizeof(WCHAR) * 3); + + if (!path_slash) + { + WLog_ERR(TAG, "malloc failed!"); + return FALSE; + } + + CopyMemory(path_slash, path, base_path_length); + path_slash[base_path_length / 2] = L'/'; + path_slash[base_path_length / 2 + 1] = L'*'; + DEBUG_WSTR("Search in %s", path_slash); + dir = FindFirstFileW(path_slash, &findFileData); + path_slash[base_path_length / 2 + 1] = 0; + + if (dir == INVALID_HANDLE_VALUE) + { + free(path_slash); + return FALSE; + } + + do + { + size_t len = _wcslen(findFileData.cFileName); + + if ((len == 1 && findFileData.cFileName[0] == L'.') || + (len == 2 && findFileData.cFileName[0] == L'.' && findFileData.cFileName[1] == L'.')) + { + continue; + } + + fullpath = drive_file_combine_fullpath(path_slash, findFileData.cFileName, len * 2); + DEBUG_WSTR("Delete %s", fullpath); + + if (findFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) + { + ret = drive_file_remove_dir(fullpath); + } + else + { + ret = DeleteFileW(fullpath); + } + + free(fullpath); + + if (!ret) + break; + } while (ret && FindNextFileW(dir, &findFileData) != 0); + + FindClose(dir); + + if (ret) + { + if (!RemoveDirectoryW(path)) + { + ret = FALSE; + } + } + + free(path_slash); + return ret; +} + +static BOOL drive_file_set_fullpath(DRIVE_FILE* file, WCHAR* fullpath) +{ + if (!file || !fullpath) + return FALSE; + + free(file->fullpath); + file->fullpath = fullpath; + file->filename = _wcsrchr(file->fullpath, L'/'); + + if (file->filename == NULL) + file->filename = file->fullpath; + else + file->filename += 1; + + return TRUE; +} + +static BOOL drive_file_init(DRIVE_FILE* file) +{ + UINT CreateDisposition = 0; + DWORD dwAttr = GetFileAttributesW(file->fullpath); + + if (dwAttr != INVALID_FILE_ATTRIBUTES) + { + /* The file exists */ + file->is_dir = (dwAttr & FILE_ATTRIBUTE_DIRECTORY) != 0; + + if (file->is_dir) + { + if (file->CreateDisposition == FILE_CREATE) + { + SetLastError(ERROR_ALREADY_EXISTS); + return FALSE; + } + + if (file->CreateOptions & FILE_NON_DIRECTORY_FILE) + { + SetLastError(ERROR_ACCESS_DENIED); + return FALSE; + } + + return TRUE; + } + else + { + if (file->CreateOptions & FILE_DIRECTORY_FILE) + { + SetLastError(ERROR_DIRECTORY); + return FALSE; + } + } + } + else + { + file->is_dir = ((file->CreateOptions & FILE_DIRECTORY_FILE) ? TRUE : FALSE); + + if (file->is_dir) + { + /* Should only create the directory if the disposition allows for it */ + if ((file->CreateDisposition == FILE_OPEN_IF) || + (file->CreateDisposition == FILE_CREATE)) + { + if (CreateDirectoryW(file->fullpath, NULL) != 0) + { + return TRUE; + } + } + + SetLastError(ERROR_FILE_NOT_FOUND); + return FALSE; + } + } + + if (file->file_handle == INVALID_HANDLE_VALUE) + { + switch (file->CreateDisposition) + { + case FILE_SUPERSEDE: /* If the file already exists, replace it with the given file. If + it does not, create the given file. */ + CreateDisposition = CREATE_ALWAYS; + break; + + case FILE_OPEN: /* If the file already exists, open it instead of creating a new file. + If it does not, fail the request and do not create a new file. */ + CreateDisposition = OPEN_EXISTING; + break; + + case FILE_CREATE: /* If the file already exists, fail the request and do not create or + open the given file. If it does not, create the given file. */ + CreateDisposition = CREATE_NEW; + break; + + case FILE_OPEN_IF: /* If the file already exists, open it. If it does not, create the + given file. */ + CreateDisposition = OPEN_ALWAYS; + break; + + case FILE_OVERWRITE: /* If the file already exists, open it and overwrite it. If it does + not, fail the request. */ + CreateDisposition = TRUNCATE_EXISTING; + break; + + case FILE_OVERWRITE_IF: /* If the file already exists, open it and overwrite it. If it + does not, create the given file. */ + CreateDisposition = CREATE_ALWAYS; + break; + + default: + break; + } + +#ifndef WIN32 + file->SharedAccess = 0; +#endif + file->file_handle = CreateFileW(file->fullpath, file->DesiredAccess, file->SharedAccess, + NULL, CreateDisposition, file->FileAttributes, NULL); + } + +#ifdef WIN32 + if (file->file_handle == INVALID_HANDLE_VALUE) + { + /* Get the error message, if any. */ + DWORD errorMessageID = GetLastError(); + + if (errorMessageID != 0) + { + LPSTR messageBuffer = NULL; + size_t size = + FormatMessageA(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | + FORMAT_MESSAGE_IGNORE_INSERTS, + NULL, errorMessageID, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), + (LPSTR)&messageBuffer, 0, NULL); + WLog_ERR(TAG, "Error in drive_file_init: %s %s", messageBuffer, file->fullpath); + /* Free the buffer. */ + LocalFree(messageBuffer); + /* restore original error code */ + SetLastError(errorMessageID); + } + } +#endif + + return file->file_handle != INVALID_HANDLE_VALUE; +} + +DRIVE_FILE* drive_file_new(const WCHAR* base_path, const WCHAR* path, UINT32 PathLength, UINT32 id, + UINT32 DesiredAccess, UINT32 CreateDisposition, UINT32 CreateOptions, + UINT32 FileAttributes, UINT32 SharedAccess) +{ + DRIVE_FILE* file; + + if (!base_path || (!path && (PathLength > 0))) + return NULL; + + file = (DRIVE_FILE*)calloc(1, sizeof(DRIVE_FILE)); + + if (!file) + { + WLog_ERR(TAG, "calloc failed!"); + return NULL; + } + + file->file_handle = INVALID_HANDLE_VALUE; + file->find_handle = INVALID_HANDLE_VALUE; + file->id = id; + file->basepath = base_path; + file->FileAttributes = FileAttributes; + file->DesiredAccess = DesiredAccess; + file->CreateDisposition = CreateDisposition; + file->CreateOptions = CreateOptions; + file->SharedAccess = SharedAccess; + drive_file_set_fullpath(file, drive_file_combine_fullpath(base_path, path, PathLength)); + + if (!drive_file_init(file)) + { + DWORD lastError = GetLastError(); + drive_file_free(file); + SetLastError(lastError); + return NULL; + } + + return file; +} + +BOOL drive_file_free(DRIVE_FILE* file) +{ + BOOL rc = FALSE; + + if (!file) + return FALSE; + + if (file->file_handle != INVALID_HANDLE_VALUE) + { + CloseHandle(file->file_handle); + file->file_handle = INVALID_HANDLE_VALUE; + } + + if (file->find_handle != INVALID_HANDLE_VALUE) + { + FindClose(file->find_handle); + file->find_handle = INVALID_HANDLE_VALUE; + } + + if (file->delete_pending) + { + if (file->is_dir) + { + if (!drive_file_remove_dir(file->fullpath)) + goto fail; + } + else if (!DeleteFileW(file->fullpath)) + goto fail; + } + + rc = TRUE; +fail: + DEBUG_WSTR("Free %s", file->fullpath); + free(file->fullpath); + free(file); + return rc; +} + +BOOL drive_file_seek(DRIVE_FILE* file, UINT64 Offset) +{ + LARGE_INTEGER loffset; + + if (!file) + return FALSE; + + if (Offset > INT64_MAX) + return FALSE; + + loffset.QuadPart = (LONGLONG)Offset; + return SetFilePointerEx(file->file_handle, loffset, NULL, FILE_BEGIN); +} + +BOOL drive_file_read(DRIVE_FILE* file, BYTE* buffer, UINT32* Length) +{ + UINT32 read; + + if (!file || !buffer || !Length) + return FALSE; + + DEBUG_WSTR("Read file %s", file->fullpath); + + if (ReadFile(file->file_handle, buffer, *Length, &read, NULL)) + { + *Length = read; + return TRUE; + } + + return FALSE; +} + +BOOL drive_file_write(DRIVE_FILE* file, BYTE* buffer, UINT32 Length) +{ + UINT32 written; + + if (!file || !buffer) + return FALSE; + + DEBUG_WSTR("Write file %s", file->fullpath); + + while (Length > 0) + { + if (!WriteFile(file->file_handle, buffer, Length, &written, NULL)) + return FALSE; + + Length -= written; + buffer += written; + } + + return TRUE; +} + +BOOL drive_file_query_information(DRIVE_FILE* file, UINT32 FsInformationClass, wStream* output) +{ + WIN32_FILE_ATTRIBUTE_DATA fileAttributes; + DEBUG_WSTR("FindFirstFile %s", file->fullpath); + + if (!file || !output) + return FALSE; + + if (!GetFileAttributesExW(file->fullpath, GetFileExInfoStandard, &fileAttributes)) + goto out_fail; + + switch (FsInformationClass) + { + case FileBasicInformation: + + /* http://msdn.microsoft.com/en-us/library/cc232094.aspx */ + if (!Stream_EnsureRemainingCapacity(output, 4 + 36)) + goto out_fail; + + Stream_Write_UINT32(output, 36); /* Length */ + Stream_Write_UINT32(output, + fileAttributes.ftCreationTime.dwLowDateTime); /* CreationTime */ + Stream_Write_UINT32(output, + fileAttributes.ftCreationTime.dwHighDateTime); /* CreationTime */ + Stream_Write_UINT32(output, + fileAttributes.ftLastAccessTime.dwLowDateTime); /* LastAccessTime */ + Stream_Write_UINT32( + output, fileAttributes.ftLastAccessTime.dwHighDateTime); /* LastAccessTime */ + Stream_Write_UINT32(output, + fileAttributes.ftLastWriteTime.dwLowDateTime); /* LastWriteTime */ + Stream_Write_UINT32(output, + fileAttributes.ftLastWriteTime.dwHighDateTime); /* LastWriteTime */ + Stream_Write_UINT32(output, + fileAttributes.ftLastWriteTime.dwLowDateTime); /* ChangeTime */ + Stream_Write_UINT32(output, + fileAttributes.ftLastWriteTime.dwHighDateTime); /* ChangeTime */ + Stream_Write_UINT32(output, fileAttributes.dwFileAttributes); /* FileAttributes */ + /* Reserved(4), MUST NOT be added! */ + break; + + case FileStandardInformation: + + /* http://msdn.microsoft.com/en-us/library/cc232088.aspx */ + if (!Stream_EnsureRemainingCapacity(output, 4 + 22)) + goto out_fail; + + Stream_Write_UINT32(output, 22); /* Length */ + Stream_Write_UINT32(output, fileAttributes.nFileSizeLow); /* AllocationSize */ + Stream_Write_UINT32(output, fileAttributes.nFileSizeHigh); /* AllocationSize */ + Stream_Write_UINT32(output, fileAttributes.nFileSizeLow); /* EndOfFile */ + Stream_Write_UINT32(output, fileAttributes.nFileSizeHigh); /* EndOfFile */ + Stream_Write_UINT32(output, 0); /* NumberOfLinks */ + Stream_Write_UINT8(output, file->delete_pending ? 1 : 0); /* DeletePending */ + Stream_Write_UINT8(output, fileAttributes.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY + ? TRUE + : FALSE); /* Directory */ + /* Reserved(2), MUST NOT be added! */ + break; + + case FileAttributeTagInformation: + + /* http://msdn.microsoft.com/en-us/library/cc232093.aspx */ + if (!Stream_EnsureRemainingCapacity(output, 4 + 8)) + goto out_fail; + + Stream_Write_UINT32(output, 8); /* Length */ + Stream_Write_UINT32(output, fileAttributes.dwFileAttributes); /* FileAttributes */ + Stream_Write_UINT32(output, 0); /* ReparseTag */ + break; + + default: + /* Unhandled FsInformationClass */ + goto out_fail; + } + + return TRUE; +out_fail: + Stream_Write_UINT32(output, 0); /* Length */ + return FALSE; +} + +BOOL drive_file_set_information(DRIVE_FILE* file, UINT32 FsInformationClass, UINT32 Length, + wStream* input) +{ + INT64 size; + WCHAR* fullpath; + ULARGE_INTEGER liCreationTime; + ULARGE_INTEGER liLastAccessTime; + ULARGE_INTEGER liLastWriteTime; + ULARGE_INTEGER liChangeTime; + FILETIME ftCreationTime; + FILETIME ftLastAccessTime; + FILETIME ftLastWriteTime; + FILETIME* pftCreationTime = NULL; + FILETIME* pftLastAccessTime = NULL; + FILETIME* pftLastWriteTime = NULL; + UINT32 FileAttributes; + UINT32 FileNameLength; + LARGE_INTEGER liSize; + UINT8 delete_pending; + UINT8 ReplaceIfExists; + DWORD attr; + + if (!file || !input) + return FALSE; + + switch (FsInformationClass) + { + case FileBasicInformation: + if (Stream_GetRemainingLength(input) < 36) + return FALSE; + + /* http://msdn.microsoft.com/en-us/library/cc232094.aspx */ + Stream_Read_UINT64(input, liCreationTime.QuadPart); + Stream_Read_UINT64(input, liLastAccessTime.QuadPart); + Stream_Read_UINT64(input, liLastWriteTime.QuadPart); + Stream_Read_UINT64(input, liChangeTime.QuadPart); + Stream_Read_UINT32(input, FileAttributes); + + if (!PathFileExistsW(file->fullpath)) + return FALSE; + + if (file->file_handle == INVALID_HANDLE_VALUE) + { + WLog_ERR(TAG, "Unable to set file time %s (%" PRId32 ")", file->fullpath, + GetLastError()); + return FALSE; + } + + if (liCreationTime.QuadPart != 0) + { + ftCreationTime.dwHighDateTime = liCreationTime.u.HighPart; + ftCreationTime.dwLowDateTime = liCreationTime.u.LowPart; + pftCreationTime = &ftCreationTime; + } + + if (liLastAccessTime.QuadPart != 0) + { + ftLastAccessTime.dwHighDateTime = liLastAccessTime.u.HighPart; + ftLastAccessTime.dwLowDateTime = liLastAccessTime.u.LowPart; + pftLastAccessTime = &ftLastAccessTime; + } + + if (liLastWriteTime.QuadPart != 0) + { + ftLastWriteTime.dwHighDateTime = liLastWriteTime.u.HighPart; + ftLastWriteTime.dwLowDateTime = liLastWriteTime.u.LowPart; + pftLastWriteTime = &ftLastWriteTime; + } + + if (liChangeTime.QuadPart != 0 && liChangeTime.QuadPart > liLastWriteTime.QuadPart) + { + ftLastWriteTime.dwHighDateTime = liChangeTime.u.HighPart; + ftLastWriteTime.dwLowDateTime = liChangeTime.u.LowPart; + pftLastWriteTime = &ftLastWriteTime; + } + + DEBUG_WSTR("SetFileTime %s", file->fullpath); + + SetFileAttributesW(file->fullpath, FileAttributes); + if (!SetFileTime(file->file_handle, pftCreationTime, pftLastAccessTime, + pftLastWriteTime)) + { + WLog_ERR(TAG, "Unable to set file time to %s", file->fullpath); + return FALSE; + } + + break; + + case FileEndOfFileInformation: + + /* http://msdn.microsoft.com/en-us/library/cc232067.aspx */ + case FileAllocationInformation: + if (Stream_GetRemainingLength(input) < 8) + return FALSE; + + /* http://msdn.microsoft.com/en-us/library/cc232076.aspx */ + Stream_Read_INT64(input, size); + + if (file->file_handle == INVALID_HANDLE_VALUE) + { + WLog_ERR(TAG, "Unable to truncate %s to %" PRId64 " (%" PRId32 ")", file->fullpath, + size, GetLastError()); + return FALSE; + } + + liSize.QuadPart = size; + + if (!SetFilePointerEx(file->file_handle, liSize, NULL, FILE_BEGIN)) + { + WLog_ERR(TAG, "Unable to truncate %s to %d (%" PRId32 ")", file->fullpath, size, + GetLastError()); + return FALSE; + } + + DEBUG_WSTR("Truncate %s", file->fullpath); + + if (SetEndOfFile(file->file_handle) == 0) + { + WLog_ERR(TAG, "Unable to truncate %s to %d (%" PRId32 ")", file->fullpath, size, + GetLastError()); + return FALSE; + } + + break; + + case FileDispositionInformation: + + /* http://msdn.microsoft.com/en-us/library/cc232098.aspx */ + /* http://msdn.microsoft.com/en-us/library/cc241371.aspx */ + if (file->is_dir && !PathIsDirectoryEmptyW(file->fullpath)) + break; /* TODO: SetLastError ??? */ + + if (Length) + { + if (Stream_GetRemainingLength(input) < 1) + return FALSE; + + Stream_Read_UINT8(input, delete_pending); + } + else + delete_pending = 1; + + if (delete_pending) + { + DEBUG_WSTR("SetDeletePending %s", file->fullpath); + attr = GetFileAttributesW(file->fullpath); + + if (attr & FILE_ATTRIBUTE_READONLY) + { + SetLastError(ERROR_ACCESS_DENIED); + return FALSE; + } + } + + file->delete_pending = delete_pending; + break; + + case FileRenameInformation: + if (Stream_GetRemainingLength(input) < 6) + return FALSE; + + /* http://msdn.microsoft.com/en-us/library/cc232085.aspx */ + Stream_Read_UINT8(input, ReplaceIfExists); + Stream_Seek_UINT8(input); /* RootDirectory */ + Stream_Read_UINT32(input, FileNameLength); + + if (Stream_GetRemainingLength(input) < FileNameLength) + return FALSE; + + fullpath = drive_file_combine_fullpath(file->basepath, (WCHAR*)Stream_Pointer(input), + FileNameLength); + + if (!fullpath) + { + WLog_ERR(TAG, "drive_file_combine_fullpath failed!"); + return FALSE; + } + +#ifdef _WIN32 + + if (file->file_handle != INVALID_HANDLE_VALUE) + { + CloseHandle(file->file_handle); + file->file_handle = INVALID_HANDLE_VALUE; + } + +#endif + DEBUG_WSTR("MoveFileExW %s", file->fullpath); + + if (MoveFileExW(file->fullpath, fullpath, + MOVEFILE_COPY_ALLOWED | + (ReplaceIfExists ? MOVEFILE_REPLACE_EXISTING : 0))) + { + if (!drive_file_set_fullpath(file, fullpath)) + return FALSE; + } + else + { + free(fullpath); + return FALSE; + } + +#ifdef _WIN32 + drive_file_init(file); +#endif + break; + + default: + return FALSE; + } + + return TRUE; +} + +BOOL drive_file_query_directory(DRIVE_FILE* file, UINT32 FsInformationClass, BYTE InitialQuery, + const WCHAR* path, UINT32 PathLength, wStream* output) +{ + size_t length; + WCHAR* ent_path; + + if (!file || !path || !output) + return FALSE; + + if (InitialQuery != 0) + { + /* release search handle */ + if (file->find_handle != INVALID_HANDLE_VALUE) + FindClose(file->find_handle); + + ent_path = drive_file_combine_fullpath(file->basepath, path, PathLength); + /* open new search handle and retrieve the first entry */ + file->find_handle = FindFirstFileW(ent_path, &file->find_data); + free(ent_path); + + if (file->find_handle == INVALID_HANDLE_VALUE) + goto out_fail; + } + else if (!FindNextFileW(file->find_handle, &file->find_data)) + goto out_fail; + + length = _wcslen(file->find_data.cFileName) * 2; + + switch (FsInformationClass) + { + case FileDirectoryInformation: + + /* http://msdn.microsoft.com/en-us/library/cc232097.aspx */ + if (!Stream_EnsureRemainingCapacity(output, 4 + 64 + length)) + goto out_fail; + + if (length > UINT32_MAX - 64) + goto out_fail; + + Stream_Write_UINT32(output, (UINT32)(64 + length)); /* Length */ + Stream_Write_UINT32(output, 0); /* NextEntryOffset */ + Stream_Write_UINT32(output, 0); /* FileIndex */ + Stream_Write_UINT32(output, + file->find_data.ftCreationTime.dwLowDateTime); /* CreationTime */ + Stream_Write_UINT32(output, + file->find_data.ftCreationTime.dwHighDateTime); /* CreationTime */ + Stream_Write_UINT32( + output, file->find_data.ftLastAccessTime.dwLowDateTime); /* LastAccessTime */ + Stream_Write_UINT32( + output, file->find_data.ftLastAccessTime.dwHighDateTime); /* LastAccessTime */ + Stream_Write_UINT32(output, + file->find_data.ftLastWriteTime.dwLowDateTime); /* LastWriteTime */ + Stream_Write_UINT32(output, + file->find_data.ftLastWriteTime.dwHighDateTime); /* LastWriteTime */ + Stream_Write_UINT32(output, + file->find_data.ftLastWriteTime.dwLowDateTime); /* ChangeTime */ + Stream_Write_UINT32(output, + file->find_data.ftLastWriteTime.dwHighDateTime); /* ChangeTime */ + Stream_Write_UINT32(output, file->find_data.nFileSizeLow); /* EndOfFile */ + Stream_Write_UINT32(output, file->find_data.nFileSizeHigh); /* EndOfFile */ + Stream_Write_UINT32(output, file->find_data.nFileSizeLow); /* AllocationSize */ + Stream_Write_UINT32(output, file->find_data.nFileSizeHigh); /* AllocationSize */ + Stream_Write_UINT32(output, file->find_data.dwFileAttributes); /* FileAttributes */ + Stream_Write_UINT32(output, (UINT32)length); /* FileNameLength */ + Stream_Write(output, file->find_data.cFileName, length); + break; + + case FileFullDirectoryInformation: + + /* http://msdn.microsoft.com/en-us/library/cc232068.aspx */ + if (!Stream_EnsureRemainingCapacity(output, 4 + 68 + length)) + goto out_fail; + + if (length > UINT32_MAX - 68) + goto out_fail; + + Stream_Write_UINT32(output, (UINT32)(68 + length)); /* Length */ + Stream_Write_UINT32(output, 0); /* NextEntryOffset */ + Stream_Write_UINT32(output, 0); /* FileIndex */ + Stream_Write_UINT32(output, + file->find_data.ftCreationTime.dwLowDateTime); /* CreationTime */ + Stream_Write_UINT32(output, + file->find_data.ftCreationTime.dwHighDateTime); /* CreationTime */ + Stream_Write_UINT32( + output, file->find_data.ftLastAccessTime.dwLowDateTime); /* LastAccessTime */ + Stream_Write_UINT32( + output, file->find_data.ftLastAccessTime.dwHighDateTime); /* LastAccessTime */ + Stream_Write_UINT32(output, + file->find_data.ftLastWriteTime.dwLowDateTime); /* LastWriteTime */ + Stream_Write_UINT32(output, + file->find_data.ftLastWriteTime.dwHighDateTime); /* LastWriteTime */ + Stream_Write_UINT32(output, + file->find_data.ftLastWriteTime.dwLowDateTime); /* ChangeTime */ + Stream_Write_UINT32(output, + file->find_data.ftLastWriteTime.dwHighDateTime); /* ChangeTime */ + Stream_Write_UINT32(output, file->find_data.nFileSizeLow); /* EndOfFile */ + Stream_Write_UINT32(output, file->find_data.nFileSizeHigh); /* EndOfFile */ + Stream_Write_UINT32(output, file->find_data.nFileSizeLow); /* AllocationSize */ + Stream_Write_UINT32(output, file->find_data.nFileSizeHigh); /* AllocationSize */ + Stream_Write_UINT32(output, file->find_data.dwFileAttributes); /* FileAttributes */ + Stream_Write_UINT32(output, (UINT32)length); /* FileNameLength */ + Stream_Write_UINT32(output, 0); /* EaSize */ + Stream_Write(output, file->find_data.cFileName, length); + break; + + case FileBothDirectoryInformation: + + /* http://msdn.microsoft.com/en-us/library/cc232095.aspx */ + if (!Stream_EnsureRemainingCapacity(output, 4 + 93 + length)) + goto out_fail; + + if (length > UINT32_MAX - 93) + goto out_fail; + + Stream_Write_UINT32(output, (UINT32)(93 + length)); /* Length */ + Stream_Write_UINT32(output, 0); /* NextEntryOffset */ + Stream_Write_UINT32(output, 0); /* FileIndex */ + Stream_Write_UINT32(output, + file->find_data.ftCreationTime.dwLowDateTime); /* CreationTime */ + Stream_Write_UINT32(output, + file->find_data.ftCreationTime.dwHighDateTime); /* CreationTime */ + Stream_Write_UINT32( + output, file->find_data.ftLastAccessTime.dwLowDateTime); /* LastAccessTime */ + Stream_Write_UINT32( + output, file->find_data.ftLastAccessTime.dwHighDateTime); /* LastAccessTime */ + Stream_Write_UINT32(output, + file->find_data.ftLastWriteTime.dwLowDateTime); /* LastWriteTime */ + Stream_Write_UINT32(output, + file->find_data.ftLastWriteTime.dwHighDateTime); /* LastWriteTime */ + Stream_Write_UINT32(output, + file->find_data.ftLastWriteTime.dwLowDateTime); /* ChangeTime */ + Stream_Write_UINT32(output, + file->find_data.ftLastWriteTime.dwHighDateTime); /* ChangeTime */ + Stream_Write_UINT32(output, file->find_data.nFileSizeLow); /* EndOfFile */ + Stream_Write_UINT32(output, file->find_data.nFileSizeHigh); /* EndOfFile */ + Stream_Write_UINT32(output, file->find_data.nFileSizeLow); /* AllocationSize */ + Stream_Write_UINT32(output, file->find_data.nFileSizeHigh); /* AllocationSize */ + Stream_Write_UINT32(output, file->find_data.dwFileAttributes); /* FileAttributes */ + Stream_Write_UINT32(output, (UINT32)length); /* FileNameLength */ + Stream_Write_UINT32(output, 0); /* EaSize */ + Stream_Write_UINT8(output, 0); /* ShortNameLength */ + /* Reserved(1), MUST NOT be added! */ + Stream_Zero(output, 24); /* ShortName */ + Stream_Write(output, file->find_data.cFileName, length); + break; + + case FileNamesInformation: + + /* http://msdn.microsoft.com/en-us/library/cc232077.aspx */ + if (!Stream_EnsureRemainingCapacity(output, 4 + 12 + length)) + goto out_fail; + + if (length > UINT32_MAX - 12) + goto out_fail; + + Stream_Write_UINT32(output, (UINT32)(12 + length)); /* Length */ + Stream_Write_UINT32(output, 0); /* NextEntryOffset */ + Stream_Write_UINT32(output, 0); /* FileIndex */ + Stream_Write_UINT32(output, (UINT32)length); /* FileNameLength */ + Stream_Write(output, file->find_data.cFileName, length); + break; + + default: + WLog_ERR(TAG, "unhandled FsInformationClass %" PRIu32, FsInformationClass); + /* Unhandled FsInformationClass */ + goto out_fail; + } + + return TRUE; +out_fail: + Stream_Write_UINT32(output, 0); /* Length */ + Stream_Write_UINT8(output, 0); /* Padding */ + return FALSE; +} diff --git a/channels/drive/client/drive_file.h b/channels/drive/client/drive_file.h new file mode 100644 index 0000000..ed789d6 --- /dev/null +++ b/channels/drive/client/drive_file.h @@ -0,0 +1,69 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * File System Virtual Channel + * + * Copyright 2010-2012 Marc-Andre Moreau + * Copyright 2010-2011 Vic Lee + * Copyright 2012 Gerald Richter + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger + * Copyright 2016 Inuvika Inc. + * Copyright 2016 David PHAM-VAN + * + * Licensed 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 FREERDP_CHANNEL_DRIVE_CLIENT_FILE_H +#define FREERDP_CHANNEL_DRIVE_CLIENT_FILE_H + +#include +#include + +#define TAG CHANNELS_TAG("drive.client") + +typedef struct _DRIVE_FILE DRIVE_FILE; + +struct _DRIVE_FILE +{ + UINT32 id; + BOOL is_dir; + HANDLE file_handle; + HANDLE find_handle; + WIN32_FIND_DATAW find_data; + const WCHAR* basepath; + WCHAR* fullpath; + WCHAR* filename; + BOOL delete_pending; + UINT32 FileAttributes; + UINT32 SharedAccess; + UINT32 DesiredAccess; + UINT32 CreateDisposition; + UINT32 CreateOptions; +}; + +DRIVE_FILE* drive_file_new(const WCHAR* base_path, const WCHAR* path, UINT32 PathLength, UINT32 id, + UINT32 DesiredAccess, UINT32 CreateDisposition, UINT32 CreateOptions, + UINT32 FileAttributes, UINT32 SharedAccess); +BOOL drive_file_free(DRIVE_FILE* file); + +BOOL drive_file_open(DRIVE_FILE* file); +BOOL drive_file_seek(DRIVE_FILE* file, UINT64 Offset); +BOOL drive_file_read(DRIVE_FILE* file, BYTE* buffer, UINT32* Length); +BOOL drive_file_write(DRIVE_FILE* file, BYTE* buffer, UINT32 Length); +BOOL drive_file_query_information(DRIVE_FILE* file, UINT32 FsInformationClass, wStream* output); +BOOL drive_file_set_information(DRIVE_FILE* file, UINT32 FsInformationClass, UINT32 Length, + wStream* input); +BOOL drive_file_query_directory(DRIVE_FILE* file, UINT32 FsInformationClass, BYTE InitialQuery, + const WCHAR* path, UINT32 PathLength, wStream* output); + +#endif /* FREERDP_CHANNEL_DRIVE_FILE_H */ diff --git a/channels/drive/client/drive_main.c b/channels/drive/client/drive_main.c new file mode 100644 index 0000000..1b54225 --- /dev/null +++ b/channels/drive/client/drive_main.c @@ -0,0 +1,1112 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * File System Virtual Channel + * + * Copyright 2010-2011 Vic Lee + * Copyright 2010-2012 Marc-Andre Moreau + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger + * Copyright 2016 David PHAM-VAN + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "drive_file.h" + +typedef struct _DRIVE_DEVICE DRIVE_DEVICE; + +struct _DRIVE_DEVICE +{ + DEVICE device; + + WCHAR* path; + BOOL automount; + UINT32 PathLength; + wListDictionary* files; + + HANDLE thread; + wMessageQueue* IrpQueue; + + DEVMAN* devman; + + rdpContext* rdpcontext; +}; + +static UINT sys_code_page = 0; + +static DWORD drive_map_windows_err(DWORD fs_errno) +{ + DWORD rc; + + /* try to return NTSTATUS version of error code */ + + switch (fs_errno) + { + case STATUS_SUCCESS: + rc = STATUS_SUCCESS; + break; + + case ERROR_ACCESS_DENIED: + case ERROR_SHARING_VIOLATION: + rc = STATUS_ACCESS_DENIED; + break; + + case ERROR_FILE_NOT_FOUND: + rc = STATUS_NO_SUCH_FILE; + break; + + case ERROR_BUSY_DRIVE: + rc = STATUS_DEVICE_BUSY; + break; + + case ERROR_INVALID_DRIVE: + rc = STATUS_NO_SUCH_DEVICE; + break; + + case ERROR_NOT_READY: + rc = STATUS_NO_SUCH_DEVICE; + break; + + case ERROR_FILE_EXISTS: + case ERROR_ALREADY_EXISTS: + rc = STATUS_OBJECT_NAME_COLLISION; + break; + + case ERROR_INVALID_NAME: + rc = STATUS_NO_SUCH_FILE; + break; + + case ERROR_INVALID_HANDLE: + rc = STATUS_INVALID_HANDLE; + break; + + case ERROR_NO_MORE_FILES: + rc = STATUS_NO_MORE_FILES; + break; + + case ERROR_DIRECTORY: + rc = STATUS_NOT_A_DIRECTORY; + break; + + case ERROR_PATH_NOT_FOUND: + rc = STATUS_OBJECT_PATH_NOT_FOUND; + break; + + default: + rc = STATUS_UNSUCCESSFUL; + WLog_ERR(TAG, "Error code not found: %" PRIu32 "", fs_errno); + break; + } + + return rc; +} + +static DRIVE_FILE* drive_get_file_by_id(DRIVE_DEVICE* drive, UINT32 id) +{ + DRIVE_FILE* file = NULL; + void* key = (void*)(size_t)id; + + if (!drive) + return NULL; + + file = (DRIVE_FILE*)ListDictionary_GetItemValue(drive->files, key); + return file; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT drive_process_irp_create(DRIVE_DEVICE* drive, IRP* irp) +{ + UINT32 FileId; + DRIVE_FILE* file; + BYTE Information; + UINT32 FileAttributes; + UINT32 SharedAccess; + UINT32 DesiredAccess; + UINT32 CreateDisposition; + UINT32 CreateOptions; + UINT32 PathLength; + UINT64 allocationSize; + const WCHAR* path; + + if (!drive || !irp || !irp->devman || !irp->Complete) + return ERROR_INVALID_PARAMETER; + + if (Stream_GetRemainingLength(irp->input) < 6 * 4 + 8) + return ERROR_INVALID_DATA; + + Stream_Read_UINT32(irp->input, DesiredAccess); + Stream_Read_UINT64(irp->input, allocationSize); + Stream_Read_UINT32(irp->input, FileAttributes); + Stream_Read_UINT32(irp->input, SharedAccess); + Stream_Read_UINT32(irp->input, CreateDisposition); + Stream_Read_UINT32(irp->input, CreateOptions); + Stream_Read_UINT32(irp->input, PathLength); + + if (Stream_GetRemainingLength(irp->input) < PathLength) + return ERROR_INVALID_DATA; + + path = (const WCHAR*)Stream_Pointer(irp->input); + FileId = irp->devman->id_sequence++; + file = drive_file_new(drive->path, path, PathLength, FileId, DesiredAccess, CreateDisposition, + CreateOptions, FileAttributes, SharedAccess); + + if (!file) + { + irp->IoStatus = drive_map_windows_err(GetLastError()); + FileId = 0; + Information = 0; + } + else + { + void* key = (void*)(size_t)file->id; + + if (!ListDictionary_Add(drive->files, key, file)) + { + WLog_ERR(TAG, "ListDictionary_Add failed!"); + return ERROR_INTERNAL_ERROR; + } + + switch (CreateDisposition) + { + case FILE_SUPERSEDE: + case FILE_OPEN: + case FILE_CREATE: + case FILE_OVERWRITE: + Information = FILE_SUPERSEDED; + break; + + case FILE_OPEN_IF: + Information = FILE_OPENED; + break; + + case FILE_OVERWRITE_IF: + Information = FILE_OVERWRITTEN; + break; + + default: + Information = 0; + break; + } + } + + Stream_Write_UINT32(irp->output, FileId); + Stream_Write_UINT8(irp->output, Information); + return irp->Complete(irp); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT drive_process_irp_close(DRIVE_DEVICE* drive, IRP* irp) +{ + void* key; + DRIVE_FILE* file; + + if (!drive || !irp || !irp->Complete || !irp->output) + return ERROR_INVALID_PARAMETER; + + file = drive_get_file_by_id(drive, irp->FileId); + key = (void*)(size_t)irp->FileId; + + if (!file) + irp->IoStatus = STATUS_UNSUCCESSFUL; + else + { + ListDictionary_Remove(drive->files, key); + + if (drive_file_free(file)) + irp->IoStatus = STATUS_SUCCESS; + else + irp->IoStatus = drive_map_windows_err(GetLastError()); + } + + Stream_Zero(irp->output, 5); /* Padding(5) */ + return irp->Complete(irp); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT drive_process_irp_read(DRIVE_DEVICE* drive, IRP* irp) +{ + DRIVE_FILE* file; + UINT32 Length; + UINT64 Offset; + + if (!drive || !irp || !irp->output || !irp->Complete) + return ERROR_INVALID_PARAMETER; + + if (Stream_GetRemainingLength(irp->input) < 12) + return ERROR_INVALID_DATA; + + Stream_Read_UINT32(irp->input, Length); + Stream_Read_UINT64(irp->input, Offset); + file = drive_get_file_by_id(drive, irp->FileId); + + if (!file) + { + irp->IoStatus = STATUS_UNSUCCESSFUL; + Length = 0; + } + else if (!drive_file_seek(file, Offset)) + { + irp->IoStatus = drive_map_windows_err(GetLastError()); + Length = 0; + } + + if (!Stream_EnsureRemainingCapacity(irp->output, Length + 4)) + { + WLog_ERR(TAG, "Stream_EnsureRemainingCapacity failed!"); + return ERROR_INTERNAL_ERROR; + } + else if (Length == 0) + Stream_Write_UINT32(irp->output, 0); + else + { + BYTE* buffer = Stream_Pointer(irp->output) + sizeof(UINT32); + + if (!drive_file_read(file, buffer, &Length)) + { + irp->IoStatus = drive_map_windows_err(GetLastError()); + Stream_Write_UINT32(irp->output, 0); + } + else + { + Stream_Write_UINT32(irp->output, Length); + Stream_Seek(irp->output, Length); + } + } + + return irp->Complete(irp); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT drive_process_irp_write(DRIVE_DEVICE* drive, IRP* irp) +{ + DRIVE_FILE* file; + UINT32 Length; + UINT64 Offset; + void* ptr; + + if (!drive || !irp || !irp->input || !irp->output || !irp->Complete) + return ERROR_INVALID_PARAMETER; + + if (Stream_GetRemainingLength(irp->input) < 32) + return ERROR_INVALID_DATA; + + Stream_Read_UINT32(irp->input, Length); + Stream_Read_UINT64(irp->input, Offset); + Stream_Seek(irp->input, 20); /* Padding */ + ptr = Stream_Pointer(irp->input); + if (!Stream_SafeSeek(irp->input, Length)) + return ERROR_INVALID_DATA; + file = drive_get_file_by_id(drive, irp->FileId); + + if (!file) + { + irp->IoStatus = STATUS_UNSUCCESSFUL; + Length = 0; + } + else if (!drive_file_seek(file, Offset)) + { + irp->IoStatus = drive_map_windows_err(GetLastError()); + Length = 0; + } + else if (!drive_file_write(file, ptr, Length)) + { + irp->IoStatus = drive_map_windows_err(GetLastError()); + Length = 0; + } + + Stream_Write_UINT32(irp->output, Length); + Stream_Write_UINT8(irp->output, 0); /* Padding */ + return irp->Complete(irp); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT drive_process_irp_query_information(DRIVE_DEVICE* drive, IRP* irp) +{ + DRIVE_FILE* file; + UINT32 FsInformationClass; + + if (!drive || !irp || !irp->Complete) + return ERROR_INVALID_PARAMETER; + + if (Stream_GetRemainingLength(irp->input) < 4) + return ERROR_INVALID_DATA; + + Stream_Read_UINT32(irp->input, FsInformationClass); + file = drive_get_file_by_id(drive, irp->FileId); + + if (!file) + { + irp->IoStatus = STATUS_UNSUCCESSFUL; + } + else if (!drive_file_query_information(file, FsInformationClass, irp->output)) + { + irp->IoStatus = drive_map_windows_err(GetLastError()); + } + + return irp->Complete(irp); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT drive_process_irp_set_information(DRIVE_DEVICE* drive, IRP* irp) +{ + DRIVE_FILE* file; + UINT32 FsInformationClass; + UINT32 Length; + + if (!drive || !irp || !irp->Complete || !irp->input || !irp->output) + return ERROR_INVALID_PARAMETER; + + if (Stream_GetRemainingLength(irp->input) < 32) + return ERROR_INVALID_DATA; + + Stream_Read_UINT32(irp->input, FsInformationClass); + Stream_Read_UINT32(irp->input, Length); + Stream_Seek(irp->input, 24); /* Padding */ + file = drive_get_file_by_id(drive, irp->FileId); + + if (!file) + { + irp->IoStatus = STATUS_UNSUCCESSFUL; + } + else if (!drive_file_set_information(file, FsInformationClass, Length, irp->input)) + { + irp->IoStatus = drive_map_windows_err(GetLastError()); + } + + if (file && file->is_dir && !PathIsDirectoryEmptyW(file->fullpath)) + irp->IoStatus = STATUS_DIRECTORY_NOT_EMPTY; + + Stream_Write_UINT32(irp->output, Length); + return irp->Complete(irp); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT drive_process_irp_query_volume_information(DRIVE_DEVICE* drive, IRP* irp) +{ + UINT32 FsInformationClass; + wStream* output = NULL; + char* volumeLabel = { "FREERDP" }; + char* diskType = { "FAT32" }; + WCHAR* outStr = NULL; + int length; + DWORD lpSectorsPerCluster; + DWORD lpBytesPerSector; + DWORD lpNumberOfFreeClusters; + DWORD lpTotalNumberOfClusters; + WIN32_FILE_ATTRIBUTE_DATA wfad; + + if (!drive || !irp) + return ERROR_INVALID_PARAMETER; + + output = irp->output; + + if (Stream_GetRemainingLength(irp->input) < 4) + return ERROR_INVALID_DATA; + + Stream_Read_UINT32(irp->input, FsInformationClass); + GetDiskFreeSpaceW(drive->path, &lpSectorsPerCluster, &lpBytesPerSector, &lpNumberOfFreeClusters, + &lpTotalNumberOfClusters); + + switch (FsInformationClass) + { + case FileFsVolumeInformation: + + /* http://msdn.microsoft.com/en-us/library/cc232108.aspx */ + if ((length = ConvertToUnicode(sys_code_page, 0, volumeLabel, -1, &outStr, 0) * 2) <= 0) + { + WLog_ERR(TAG, "ConvertToUnicode failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + Stream_Write_UINT32(output, 17 + length); /* Length */ + + if (!Stream_EnsureRemainingCapacity(output, 17 + length)) + { + WLog_ERR(TAG, "Stream_EnsureRemainingCapacity failed!"); + free(outStr); + return CHANNEL_RC_NO_MEMORY; + } + + GetFileAttributesExW(drive->path, GetFileExInfoStandard, &wfad); + Stream_Write_UINT32(output, wfad.ftCreationTime.dwLowDateTime); /* VolumeCreationTime */ + Stream_Write_UINT32(output, + wfad.ftCreationTime.dwHighDateTime); /* VolumeCreationTime */ + Stream_Write_UINT32(output, lpNumberOfFreeClusters & 0xffff); /* VolumeSerialNumber */ + Stream_Write_UINT32(output, length); /* VolumeLabelLength */ + Stream_Write_UINT8(output, 0); /* SupportsObjects */ + /* Reserved(1), MUST NOT be added! */ + Stream_Write(output, outStr, length); /* VolumeLabel (Unicode) */ + free(outStr); + break; + + case FileFsSizeInformation: + /* http://msdn.microsoft.com/en-us/library/cc232107.aspx */ + Stream_Write_UINT32(output, 24); /* Length */ + + if (!Stream_EnsureRemainingCapacity(output, 24)) + { + WLog_ERR(TAG, "Stream_EnsureRemainingCapacity failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + Stream_Write_UINT64(output, lpTotalNumberOfClusters); /* TotalAllocationUnits */ + Stream_Write_UINT64(output, lpNumberOfFreeClusters); /* AvailableAllocationUnits */ + Stream_Write_UINT32(output, lpSectorsPerCluster); /* SectorsPerAllocationUnit */ + Stream_Write_UINT32(output, lpBytesPerSector); /* BytesPerSector */ + break; + + case FileFsAttributeInformation: + + /* http://msdn.microsoft.com/en-us/library/cc232101.aspx */ + if ((length = ConvertToUnicode(sys_code_page, 0, diskType, -1, &outStr, 0) * 2) <= 0) + { + WLog_ERR(TAG, "ConvertToUnicode failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + Stream_Write_UINT32(output, 12 + length); /* Length */ + + if (!Stream_EnsureRemainingCapacity(output, 12 + length)) + { + free(outStr); + WLog_ERR(TAG, "Stream_EnsureRemainingCapacity failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + Stream_Write_UINT32(output, FILE_CASE_SENSITIVE_SEARCH | FILE_CASE_PRESERVED_NAMES | + FILE_UNICODE_ON_DISK); /* FileSystemAttributes */ + Stream_Write_UINT32(output, MAX_PATH); /* MaximumComponentNameLength */ + Stream_Write_UINT32(output, length); /* FileSystemNameLength */ + Stream_Write(output, outStr, length); /* FileSystemName (Unicode) */ + free(outStr); + break; + + case FileFsFullSizeInformation: + /* http://msdn.microsoft.com/en-us/library/cc232104.aspx */ + Stream_Write_UINT32(output, 32); /* Length */ + + if (!Stream_EnsureRemainingCapacity(output, 32)) + { + WLog_ERR(TAG, "Stream_EnsureRemainingCapacity failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + Stream_Write_UINT64(output, lpTotalNumberOfClusters); /* TotalAllocationUnits */ + Stream_Write_UINT64(output, + lpNumberOfFreeClusters); /* CallerAvailableAllocationUnits */ + Stream_Write_UINT64(output, lpNumberOfFreeClusters); /* AvailableAllocationUnits */ + Stream_Write_UINT32(output, lpSectorsPerCluster); /* SectorsPerAllocationUnit */ + Stream_Write_UINT32(output, lpBytesPerSector); /* BytesPerSector */ + break; + + case FileFsDeviceInformation: + /* http://msdn.microsoft.com/en-us/library/cc232109.aspx */ + Stream_Write_UINT32(output, 8); /* Length */ + + if (!Stream_EnsureRemainingCapacity(output, 8)) + { + WLog_ERR(TAG, "Stream_EnsureRemainingCapacity failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + Stream_Write_UINT32(output, FILE_DEVICE_DISK); /* DeviceType */ + Stream_Write_UINT32(output, 0); /* Characteristics */ + break; + + default: + irp->IoStatus = STATUS_UNSUCCESSFUL; + Stream_Write_UINT32(output, 0); /* Length */ + break; + } + + return irp->Complete(irp); +} + +/* http://msdn.microsoft.com/en-us/library/cc241518.aspx */ + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT drive_process_irp_silent_ignore(DRIVE_DEVICE* drive, IRP* irp) +{ + UINT32 FsInformationClass; + + if (!drive || !irp || !irp->output || !irp->Complete) + return ERROR_INVALID_PARAMETER; + + if (Stream_GetRemainingLength(irp->input) < 4) + return ERROR_INVALID_DATA; + + Stream_Read_UINT32(irp->input, FsInformationClass); + Stream_Write_UINT32(irp->output, 0); /* Length */ + return irp->Complete(irp); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT drive_process_irp_query_directory(DRIVE_DEVICE* drive, IRP* irp) +{ + const WCHAR* path; + DRIVE_FILE* file; + BYTE InitialQuery; + UINT32 PathLength; + UINT32 FsInformationClass; + + if (!drive || !irp || !irp->Complete) + return ERROR_INVALID_PARAMETER; + + if (Stream_GetRemainingLength(irp->input) < 32) + return ERROR_INVALID_DATA; + + Stream_Read_UINT32(irp->input, FsInformationClass); + Stream_Read_UINT8(irp->input, InitialQuery); + Stream_Read_UINT32(irp->input, PathLength); + Stream_Seek(irp->input, 23); /* Padding */ + path = (WCHAR*)Stream_Pointer(irp->input); + file = drive_get_file_by_id(drive, irp->FileId); + + if (file == NULL) + { + irp->IoStatus = STATUS_UNSUCCESSFUL; + Stream_Write_UINT32(irp->output, 0); /* Length */ + } + else if (!drive_file_query_directory(file, FsInformationClass, InitialQuery, path, PathLength, + irp->output)) + { + irp->IoStatus = drive_map_windows_err(GetLastError()); + } + + return irp->Complete(irp); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT drive_process_irp_directory_control(DRIVE_DEVICE* drive, IRP* irp) +{ + if (!drive || !irp) + return ERROR_INVALID_PARAMETER; + + switch (irp->MinorFunction) + { + case IRP_MN_QUERY_DIRECTORY: + return drive_process_irp_query_directory(drive, irp); + + case IRP_MN_NOTIFY_CHANGE_DIRECTORY: /* TODO */ + return irp->Discard(irp); + + default: + irp->IoStatus = STATUS_NOT_SUPPORTED; + Stream_Write_UINT32(irp->output, 0); /* Length */ + return irp->Complete(irp); + } + + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT drive_process_irp_device_control(DRIVE_DEVICE* drive, IRP* irp) +{ + if (!drive || !irp) + return ERROR_INVALID_PARAMETER; + + Stream_Write_UINT32(irp->output, 0); /* OutputBufferLength */ + return irp->Complete(irp); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT drive_process_irp(DRIVE_DEVICE* drive, IRP* irp) +{ + UINT error; + + if (!drive || !irp) + return ERROR_INVALID_PARAMETER; + + irp->IoStatus = STATUS_SUCCESS; + + switch (irp->MajorFunction) + { + case IRP_MJ_CREATE: + error = drive_process_irp_create(drive, irp); + break; + + case IRP_MJ_CLOSE: + error = drive_process_irp_close(drive, irp); + break; + + case IRP_MJ_READ: + error = drive_process_irp_read(drive, irp); + break; + + case IRP_MJ_WRITE: + error = drive_process_irp_write(drive, irp); + break; + + case IRP_MJ_QUERY_INFORMATION: + error = drive_process_irp_query_information(drive, irp); + break; + + case IRP_MJ_SET_INFORMATION: + error = drive_process_irp_set_information(drive, irp); + break; + + case IRP_MJ_QUERY_VOLUME_INFORMATION: + error = drive_process_irp_query_volume_information(drive, irp); + break; + + case IRP_MJ_LOCK_CONTROL: + error = drive_process_irp_silent_ignore(drive, irp); + break; + + case IRP_MJ_DIRECTORY_CONTROL: + error = drive_process_irp_directory_control(drive, irp); + break; + + case IRP_MJ_DEVICE_CONTROL: + error = drive_process_irp_device_control(drive, irp); + break; + + default: + irp->IoStatus = STATUS_NOT_SUPPORTED; + error = irp->Complete(irp); + break; + } + + return error; +} + +static DWORD WINAPI drive_thread_func(LPVOID arg) +{ + IRP* irp; + wMessage message; + DRIVE_DEVICE* drive = (DRIVE_DEVICE*)arg; + UINT error = CHANNEL_RC_OK; + + if (!drive) + { + error = ERROR_INVALID_PARAMETER; + goto fail; + } + + while (1) + { + if (!MessageQueue_Wait(drive->IrpQueue)) + { + WLog_ERR(TAG, "MessageQueue_Wait failed!"); + error = ERROR_INTERNAL_ERROR; + break; + } + + if (!MessageQueue_Peek(drive->IrpQueue, &message, TRUE)) + { + WLog_ERR(TAG, "MessageQueue_Peek failed!"); + error = ERROR_INTERNAL_ERROR; + break; + } + + if (message.id == WMQ_QUIT) + break; + + irp = (IRP*)message.wParam; + + if (irp) + { + if ((error = drive_process_irp(drive, irp))) + { + WLog_ERR(TAG, "drive_process_irp failed with error %" PRIu32 "!", error); + break; + } + } + } + +fail: + + if (error && drive && drive->rdpcontext) + setChannelError(drive->rdpcontext, error, "drive_thread_func reported an error"); + + ExitThread(error); + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT drive_irp_request(DEVICE* device, IRP* irp) +{ + DRIVE_DEVICE* drive = (DRIVE_DEVICE*)device; + + if (!drive) + return ERROR_INVALID_PARAMETER; + + if (!MessageQueue_Post(drive->IrpQueue, NULL, 0, (void*)irp, NULL)) + { + WLog_ERR(TAG, "MessageQueue_Post failed!"); + return ERROR_INTERNAL_ERROR; + } + + return CHANNEL_RC_OK; +} + +static UINT drive_free_int(DRIVE_DEVICE* drive) +{ + UINT error = CHANNEL_RC_OK; + + if (!drive) + return ERROR_INVALID_PARAMETER; + + CloseHandle(drive->thread); + ListDictionary_Free(drive->files); + MessageQueue_Free(drive->IrpQueue); + Stream_Free(drive->device.data, TRUE); + free(drive->path); + free(drive); + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT drive_free(DEVICE* device) +{ + DRIVE_DEVICE* drive = (DRIVE_DEVICE*)device; + UINT error = CHANNEL_RC_OK; + + if (!drive) + return ERROR_INVALID_PARAMETER; + + if (MessageQueue_PostQuit(drive->IrpQueue, 0) && + (WaitForSingleObject(drive->thread, INFINITE) == WAIT_FAILED)) + { + error = GetLastError(); + WLog_ERR(TAG, "WaitForSingleObject failed with error %" PRIu32 "", error); + return error; + } + + return drive_free_int(drive); +} + +/** + * Helper function used for freeing list dictionary value object + */ +static void drive_file_objfree(void* obj) +{ + drive_file_free((DRIVE_FILE*)obj); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT drive_register_drive_path(PDEVICE_SERVICE_ENTRY_POINTS pEntryPoints, const char* name, + const char* path, BOOL automount) +{ + size_t i, length; + DRIVE_DEVICE* drive; + UINT error = ERROR_INTERNAL_ERROR; + + if (!pEntryPoints || !name || !path) + { + WLog_ERR(TAG, "[%s] Invalid parameters: pEntryPoints=%p, name=%p, path=%p", pEntryPoints, + name, path); + return ERROR_INVALID_PARAMETER; + } + + if (name[0] && path[0]) + { + size_t pathLength = strnlen(path, MAX_PATH); + drive = (DRIVE_DEVICE*)calloc(1, sizeof(DRIVE_DEVICE)); + + if (!drive) + { + WLog_ERR(TAG, "calloc failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + drive->device.type = RDPDR_DTYP_FILESYSTEM; + drive->device.IRPRequest = drive_irp_request; + drive->device.Free = drive_free; + drive->rdpcontext = pEntryPoints->rdpcontext; + drive->automount = automount; + length = strlen(name); + drive->device.data = Stream_New(NULL, length + 1); + + if (!drive->device.data) + { + WLog_ERR(TAG, "Stream_New failed!"); + error = CHANNEL_RC_NO_MEMORY; + goto out_error; + } + + for (i = 0; i < length; i++) + { + /* Filter 2.2.1.3 Device Announce Header (DEVICE_ANNOUNCE) forbidden symbols */ + switch (name[i]) + { + case ':': + case '<': + case '>': + case '\"': + case '/': + case '\\': + case '|': + case ' ': + Stream_Write_UINT8(drive->device.data, '_'); + break; + default: + Stream_Write_UINT8(drive->device.data, (BYTE)name[i]); + break; + } + } + Stream_Write_UINT8(drive->device.data, '\0'); + + drive->device.name = (const char*)Stream_Buffer(drive->device.data); + if (!drive->device.name) + goto out_error; + + if ((pathLength > 1) && (path[pathLength - 1] == '/')) + pathLength--; + + if (ConvertToUnicode(sys_code_page, 0, path, pathLength, &drive->path, 0) <= 0) + { + WLog_ERR(TAG, "ConvertToUnicode failed!"); + error = CHANNEL_RC_NO_MEMORY; + goto out_error; + } + + drive->files = ListDictionary_New(TRUE); + + if (!drive->files) + { + WLog_ERR(TAG, "ListDictionary_New failed!"); + error = CHANNEL_RC_NO_MEMORY; + goto out_error; + } + + ListDictionary_ValueObject(drive->files)->fnObjectFree = drive_file_objfree; + drive->IrpQueue = MessageQueue_New(NULL); + + if (!drive->IrpQueue) + { + WLog_ERR(TAG, "ListDictionary_New failed!"); + error = CHANNEL_RC_NO_MEMORY; + goto out_error; + } + + if ((error = pEntryPoints->RegisterDevice(pEntryPoints->devman, (DEVICE*)drive))) + { + WLog_ERR(TAG, "RegisterDevice failed with error %" PRIu32 "!", error); + goto out_error; + } + + if (!(drive->thread = + CreateThread(NULL, 0, drive_thread_func, drive, CREATE_SUSPENDED, NULL))) + { + WLog_ERR(TAG, "CreateThread failed!"); + goto out_error; + } + + ResumeThread(drive->thread); + } + + return CHANNEL_RC_OK; +out_error: + drive_free_int(drive); + return error; +} + +#ifdef BUILTIN_CHANNELS +#define DeviceServiceEntry drive_DeviceServiceEntry +#else +#define DeviceServiceEntry FREERDP_API DeviceServiceEntry +#endif + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT DeviceServiceEntry(PDEVICE_SERVICE_ENTRY_POINTS pEntryPoints) +{ + RDPDR_DRIVE* drive; + UINT error; +#ifdef WIN32 + char* dev; + int len; + char devlist[512], buf[512]; + char* bufdup; + char* devdup; +#endif + drive = (RDPDR_DRIVE*)pEntryPoints->device; +#ifndef WIN32 + sys_code_page = CP_UTF8; + + if (strcmp(drive->Path, "*") == 0) + { + /* all drives */ + free(drive->Path); + drive->Path = _strdup("/"); + + if (!drive->Path) + { + WLog_ERR(TAG, "_strdup failed!"); + return CHANNEL_RC_NO_MEMORY; + } + } + else if (strcmp(drive->Path, "%") == 0) + { + free(drive->Path); + drive->Path = GetKnownPath(KNOWN_PATH_HOME); + + if (!drive->Path) + { + WLog_ERR(TAG, "_strdup failed!"); + return CHANNEL_RC_NO_MEMORY; + } + } + + error = drive_register_drive_path(pEntryPoints, drive->Name, drive->Path, drive->automount); +#else + sys_code_page = GetACP(); + + /* Special case: path[0] == '*' -> export all drives */ + /* Special case: path[0] == '%' -> user home dir */ + if (strcmp(drive->Path, "%") == 0) + { + GetEnvironmentVariableA("USERPROFILE", buf, sizeof(buf)); + PathCchAddBackslashA(buf, sizeof(buf)); + free(drive->Path); + drive->Path = _strdup(buf); + + if (!drive->Path) + { + WLog_ERR(TAG, "_strdup failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + error = drive_register_drive_path(pEntryPoints, drive->Name, drive->Path, drive->automount); + } + else if (strcmp(drive->Path, "*") == 0) + { + int i; + /* Enumerate all devices: */ + GetLogicalDriveStringsA(sizeof(devlist) - 1, devlist); + + for (dev = devlist, i = 0; *dev; dev += 4, i++) + { + if (*dev > 'B') + { + /* Suppress disk drives A and B to avoid pesty messages */ + len = sprintf_s(buf, sizeof(buf) - 4, "%s", drive->Name); + buf[len] = '_'; + buf[len + 1] = dev[0]; + buf[len + 2] = 0; + buf[len + 3] = 0; + + if (!(bufdup = _strdup(buf))) + { + WLog_ERR(TAG, "_strdup failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + if (!(devdup = _strdup(dev))) + { + WLog_ERR(TAG, "_strdup failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + if ((error = drive_register_drive_path(pEntryPoints, bufdup, devdup, TRUE))) + { + break; + } + } + } + } + else + { + error = drive_register_drive_path(pEntryPoints, drive->Name, drive->Path, drive->automount); + } + +#endif + return error; +} diff --git a/channels/echo/CMakeLists.txt b/channels/echo/CMakeLists.txt new file mode 100644 index 0000000..bfa8297 --- /dev/null +++ b/channels/echo/CMakeLists.txt @@ -0,0 +1,26 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2012 Marc-Andre Moreau +# +# Licensed 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. + +define_channel("echo") + +if(WITH_CLIENT_CHANNELS) + add_channel_client(${MODULE_PREFIX} ${CHANNEL_NAME}) +endif() + +if(WITH_SERVER_CHANNELS) + add_channel_server(${MODULE_PREFIX} ${CHANNEL_NAME}) +endif() diff --git a/channels/echo/ChannelOptions.cmake b/channels/echo/ChannelOptions.cmake new file mode 100644 index 0000000..eb4950a --- /dev/null +++ b/channels/echo/ChannelOptions.cmake @@ -0,0 +1,13 @@ + +set(OPTION_DEFAULT OFF) +set(OPTION_CLIENT_DEFAULT ON) +set(OPTION_SERVER_DEFAULT ON) + +define_channel_options(NAME "echo" TYPE "dynamic" + DESCRIPTION "Echo Virtual Channel Extension" + SPECIFICATIONS "[MS-RDPEECO]" + DEFAULT ${OPTION_DEFAULT}) + +define_channel_client_options(${OPTION_CLIENT_DEFAULT}) +define_channel_server_options(${OPTION_SERVER_DEFAULT}) + diff --git a/channels/echo/client/CMakeLists.txt b/channels/echo/client/CMakeLists.txt new file mode 100644 index 0000000..149fbbf --- /dev/null +++ b/channels/echo/client/CMakeLists.txt @@ -0,0 +1,33 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2012 Marc-Andre Moreau +# +# Licensed 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. + +define_channel_client("echo") + +set(${MODULE_PREFIX}_SRCS + echo_main.c + echo_main.h) + +include_directories(..) + +add_channel_client_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} TRUE "DVCPluginEntry") + +if (WITH_DEBUG_SYMBOLS AND MSVC AND NOT BUILTIN_CHANNELS AND BUILD_SHARED_LIBS) + install(FILES ${CMAKE_BINARY_DIR}/${MODULE_NAME}.pdb DESTINATION ${FREERDP_ADDIN_PATH} COMPONENT symbols) +endif() + +target_link_libraries(${MODULE_NAME} winpr) +set_property(TARGET ${MODULE_NAME} PROPERTY FOLDER "Channels/${CHANNEL_NAME}/Client") diff --git a/channels/echo/client/echo_main.c b/channels/echo/client/echo_main.c new file mode 100644 index 0000000..7142940 --- /dev/null +++ b/channels/echo/client/echo_main.c @@ -0,0 +1,216 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Echo Virtual Channel Extension + * + * Copyright 2013 Christian Hofstaedtler + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include + +#include +#include + +#include "echo_main.h" +#include +#include + +#define TAG CHANNELS_TAG("echo.client") + +typedef struct _ECHO_LISTENER_CALLBACK ECHO_LISTENER_CALLBACK; +struct _ECHO_LISTENER_CALLBACK +{ + IWTSListenerCallback iface; + + IWTSPlugin* plugin; + IWTSVirtualChannelManager* channel_mgr; +}; + +typedef struct _ECHO_CHANNEL_CALLBACK ECHO_CHANNEL_CALLBACK; +struct _ECHO_CHANNEL_CALLBACK +{ + IWTSVirtualChannelCallback iface; + + IWTSPlugin* plugin; + IWTSVirtualChannelManager* channel_mgr; + IWTSVirtualChannel* channel; +}; + +typedef struct _ECHO_PLUGIN ECHO_PLUGIN; +struct _ECHO_PLUGIN +{ + IWTSPlugin iface; + + ECHO_LISTENER_CALLBACK* listener_callback; + IWTSListener* listener; + BOOL initialized; +}; + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT echo_on_data_received(IWTSVirtualChannelCallback* pChannelCallback, wStream* data) +{ + ECHO_CHANNEL_CALLBACK* callback = (ECHO_CHANNEL_CALLBACK*)pChannelCallback; + BYTE* pBuffer = Stream_Pointer(data); + UINT32 cbSize = Stream_GetRemainingLength(data); + + /* echo back what we have received. ECHO does not have any message IDs. */ + return callback->channel->Write(callback->channel, cbSize, pBuffer, NULL); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT echo_on_close(IWTSVirtualChannelCallback* pChannelCallback) +{ + ECHO_CHANNEL_CALLBACK* callback = (ECHO_CHANNEL_CALLBACK*)pChannelCallback; + + free(callback); + + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT echo_on_new_channel_connection(IWTSListenerCallback* pListenerCallback, + IWTSVirtualChannel* pChannel, BYTE* Data, BOOL* pbAccept, + IWTSVirtualChannelCallback** ppCallback) +{ + ECHO_CHANNEL_CALLBACK* callback; + ECHO_LISTENER_CALLBACK* listener_callback = (ECHO_LISTENER_CALLBACK*)pListenerCallback; + + callback = (ECHO_CHANNEL_CALLBACK*)calloc(1, sizeof(ECHO_CHANNEL_CALLBACK)); + + if (!callback) + { + WLog_ERR(TAG, "calloc failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + callback->iface.OnDataReceived = echo_on_data_received; + callback->iface.OnClose = echo_on_close; + callback->plugin = listener_callback->plugin; + callback->channel_mgr = listener_callback->channel_mgr; + callback->channel = pChannel; + + *ppCallback = (IWTSVirtualChannelCallback*)callback; + + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT echo_plugin_initialize(IWTSPlugin* pPlugin, IWTSVirtualChannelManager* pChannelMgr) +{ + UINT status; + ECHO_PLUGIN* echo = (ECHO_PLUGIN*)pPlugin; + if (echo->initialized) + { + WLog_ERR(TAG, "[%s] channel initialized twice, aborting", ECHO_DVC_CHANNEL_NAME); + return ERROR_INVALID_DATA; + } + echo->listener_callback = (ECHO_LISTENER_CALLBACK*)calloc(1, sizeof(ECHO_LISTENER_CALLBACK)); + + if (!echo->listener_callback) + { + WLog_ERR(TAG, "calloc failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + echo->listener_callback->iface.OnNewChannelConnection = echo_on_new_channel_connection; + echo->listener_callback->plugin = pPlugin; + echo->listener_callback->channel_mgr = pChannelMgr; + + status = pChannelMgr->CreateListener(pChannelMgr, ECHO_DVC_CHANNEL_NAME, 0, + &echo->listener_callback->iface, &echo->listener); + + echo->initialized = status == CHANNEL_RC_OK; + return status; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT echo_plugin_terminated(IWTSPlugin* pPlugin) +{ + ECHO_PLUGIN* echo = (ECHO_PLUGIN*)pPlugin; + if (echo && echo->listener_callback) + { + IWTSVirtualChannelManager* mgr = echo->listener_callback->channel_mgr; + if (mgr) + IFCALL(mgr->DestroyListener, mgr, echo->listener); + } + free(echo); + + return CHANNEL_RC_OK; +} + +#ifdef BUILTIN_CHANNELS +#define DVCPluginEntry echo_DVCPluginEntry +#else +#define DVCPluginEntry FREERDP_API DVCPluginEntry +#endif + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT DVCPluginEntry(IDRDYNVC_ENTRY_POINTS* pEntryPoints) +{ + UINT status = CHANNEL_RC_OK; + ECHO_PLUGIN* echo; + + echo = (ECHO_PLUGIN*)pEntryPoints->GetPlugin(pEntryPoints, "echo"); + + if (!echo) + { + echo = (ECHO_PLUGIN*)calloc(1, sizeof(ECHO_PLUGIN)); + + if (!echo) + { + WLog_ERR(TAG, "calloc failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + echo->iface.Initialize = echo_plugin_initialize; + echo->iface.Connected = NULL; + echo->iface.Disconnected = NULL; + echo->iface.Terminated = echo_plugin_terminated; + + status = pEntryPoints->RegisterPlugin(pEntryPoints, "echo", (IWTSPlugin*)echo); + } + + return status; +} diff --git a/channels/echo/client/echo_main.h b/channels/echo/client/echo_main.h new file mode 100644 index 0000000..06262b1 --- /dev/null +++ b/channels/echo/client/echo_main.h @@ -0,0 +1,42 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Echo Virtual Channel Extension + * + * Copyright 2013 Christian Hofstaedtler + * + * Licensed 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 FREERDP_CHANNEL_ECHO_CLIENT_MAIN_H +#define FREERDP_CHANNEL_ECHO_CLIENT_MAIN_H + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include +#include + +#define DVC_TAG CHANNELS_TAG("echo.client") +#ifdef WITH_DEBUG_DVC +#define DEBUG_DVC(...) WLog_DBG(DVC_TAG, __VA_ARGS__) +#else +#define DEBUG_DVC(...) \ + do \ + { \ + } while (0) +#endif + +#endif /* FREERDP_CHANNEL_ECHO_CLIENT_MAIN_H */ diff --git a/channels/echo/server/CMakeLists.txt b/channels/echo/server/CMakeLists.txt new file mode 100644 index 0000000..e69b555 --- /dev/null +++ b/channels/echo/server/CMakeLists.txt @@ -0,0 +1,30 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2012 Marc-Andre Moreau +# +# Licensed 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. + +define_channel_server("echo") + +set(${MODULE_PREFIX}_SRCS + echo_main.c) + +add_channel_server_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} FALSE "DVCPluginEntry") + + + +target_link_libraries(${MODULE_NAME} freerdp) + + +set_property(TARGET ${MODULE_NAME} PROPERTY FOLDER "Channels/${CHANNEL_NAME}/Server") diff --git a/channels/echo/server/echo_main.c b/channels/echo/server/echo_main.c new file mode 100644 index 0000000..1da2894 --- /dev/null +++ b/channels/echo/server/echo_main.c @@ -0,0 +1,372 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Echo Virtual Channel Extension + * + * Copyright 2014 Vic Lee + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include + +#define TAG CHANNELS_TAG("echo.server") + +typedef struct _echo_server +{ + echo_server_context context; + + BOOL opened; + + HANDLE stopEvent; + + HANDLE thread; + void* echo_channel; + + DWORD SessionId; + +} echo_server; + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT echo_server_open_channel(echo_server* echo) +{ + DWORD Error; + HANDLE hEvent; + DWORD StartTick; + DWORD BytesReturned = 0; + PULONG pSessionId = NULL; + + if (WTSQuerySessionInformationA(echo->context.vcm, WTS_CURRENT_SESSION, WTSSessionId, + (LPSTR*)&pSessionId, &BytesReturned) == FALSE) + { + WLog_ERR(TAG, "WTSQuerySessionInformationA failed!"); + return ERROR_INTERNAL_ERROR; + } + + echo->SessionId = (DWORD)*pSessionId; + WTSFreeMemory(pSessionId); + hEvent = WTSVirtualChannelManagerGetEventHandle(echo->context.vcm); + StartTick = GetTickCount(); + + while (echo->echo_channel == NULL) + { + if (WaitForSingleObject(hEvent, 1000) == WAIT_FAILED) + { + Error = GetLastError(); + WLog_ERR(TAG, "WaitForSingleObject failed with error %" PRIu32 "!", Error); + return Error; + } + + echo->echo_channel = + WTSVirtualChannelOpenEx(echo->SessionId, "ECHO", WTS_CHANNEL_OPTION_DYNAMIC); + + if (echo->echo_channel) + { + UINT32 channelId; + BOOL status = TRUE; + + channelId = WTSChannelGetIdByHandle(echo->echo_channel); + + IFCALLRET(echo->context.ChannelIdAssigned, status, &echo->context, channelId); + if (!status) + { + WLog_ERR(TAG, "context->ChannelIdAssigned failed!"); + return ERROR_INTERNAL_ERROR; + } + + break; + } + + Error = GetLastError(); + + if (Error == ERROR_NOT_FOUND) + break; + + if (GetTickCount() - StartTick > 5000) + break; + } + + return echo->echo_channel ? CHANNEL_RC_OK : ERROR_INTERNAL_ERROR; +} + +static DWORD WINAPI echo_server_thread_func(LPVOID arg) +{ + wStream* s; + void* buffer; + DWORD nCount; + HANDLE events[8]; + BOOL ready = FALSE; + HANDLE ChannelEvent; + DWORD BytesReturned = 0; + echo_server* echo = (echo_server*)arg; + UINT error; + DWORD status; + + if ((error = echo_server_open_channel(echo))) + { + UINT error2 = 0; + WLog_ERR(TAG, "echo_server_open_channel failed with error %" PRIu32 "!", error); + IFCALLRET(echo->context.OpenResult, error2, &echo->context, + ECHO_SERVER_OPEN_RESULT_NOTSUPPORTED); + + if (error2) + WLog_ERR(TAG, "echo server's OpenResult callback failed with error %" PRIu32 "", + error2); + + goto out; + } + + buffer = NULL; + BytesReturned = 0; + ChannelEvent = NULL; + + if (WTSVirtualChannelQuery(echo->echo_channel, WTSVirtualEventHandle, &buffer, + &BytesReturned) == TRUE) + { + if (BytesReturned == sizeof(HANDLE)) + CopyMemory(&ChannelEvent, buffer, sizeof(HANDLE)); + + WTSFreeMemory(buffer); + } + + nCount = 0; + events[nCount++] = echo->stopEvent; + events[nCount++] = ChannelEvent; + + /* Wait for the client to confirm that the Graphics Pipeline dynamic channel is ready */ + + while (1) + { + status = WaitForMultipleObjects(nCount, events, FALSE, 100); + + if (status == WAIT_FAILED) + { + error = GetLastError(); + WLog_ERR(TAG, "WaitForMultipleObjects failed with error %" PRIu32 "", error); + break; + } + + if (status == WAIT_OBJECT_0) + { + IFCALLRET(echo->context.OpenResult, error, &echo->context, + ECHO_SERVER_OPEN_RESULT_CLOSED); + + if (error) + WLog_ERR(TAG, "OpenResult failed with error %" PRIu32 "!", error); + + break; + } + + if (WTSVirtualChannelQuery(echo->echo_channel, WTSVirtualChannelReady, &buffer, + &BytesReturned) == FALSE) + { + IFCALLRET(echo->context.OpenResult, error, &echo->context, + ECHO_SERVER_OPEN_RESULT_ERROR); + + if (error) + WLog_ERR(TAG, "OpenResult failed with error %" PRIu32 "!", error); + + break; + } + + ready = *((BOOL*)buffer); + WTSFreeMemory(buffer); + + if (ready) + { + IFCALLRET(echo->context.OpenResult, error, &echo->context, ECHO_SERVER_OPEN_RESULT_OK); + + if (error) + WLog_ERR(TAG, "OpenResult failed with error %" PRIu32 "!", error); + + break; + } + } + + s = Stream_New(NULL, 4096); + + if (!s) + { + WLog_ERR(TAG, "Stream_New failed!"); + WTSVirtualChannelClose(echo->echo_channel); + ExitThread(ERROR_NOT_ENOUGH_MEMORY); + return ERROR_NOT_ENOUGH_MEMORY; + } + + while (ready) + { + status = WaitForMultipleObjects(nCount, events, FALSE, INFINITE); + + if (status == WAIT_FAILED) + { + error = GetLastError(); + WLog_ERR(TAG, "WaitForMultipleObjects failed with error %" PRIu32 "", error); + break; + } + + if (status == WAIT_OBJECT_0) + break; + + Stream_SetPosition(s, 0); + WTSVirtualChannelRead(echo->echo_channel, 0, NULL, 0, &BytesReturned); + + if (BytesReturned < 1) + continue; + + if (!Stream_EnsureRemainingCapacity(s, BytesReturned)) + { + WLog_ERR(TAG, "Stream_EnsureRemainingCapacity failed!"); + error = CHANNEL_RC_NO_MEMORY; + break; + } + + if (WTSVirtualChannelRead(echo->echo_channel, 0, (PCHAR)Stream_Buffer(s), + (ULONG)Stream_Capacity(s), &BytesReturned) == FALSE) + { + WLog_ERR(TAG, "WTSVirtualChannelRead failed!"); + error = ERROR_INTERNAL_ERROR; + break; + } + + IFCALLRET(echo->context.Response, error, &echo->context, (BYTE*)Stream_Buffer(s), + BytesReturned); + + if (error) + { + WLog_ERR(TAG, "Response failed with error %" PRIu32 "!", error); + break; + } + } + + Stream_Free(s, TRUE); + WTSVirtualChannelClose(echo->echo_channel); + echo->echo_channel = NULL; +out: + + if (error && echo->context.rdpcontext) + setChannelError(echo->context.rdpcontext, error, + "echo_server_thread_func reported an error"); + + ExitThread(error); + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT echo_server_open(echo_server_context* context) +{ + echo_server* echo = (echo_server*)context; + + if (echo->thread == NULL) + { + if (!(echo->stopEvent = CreateEvent(NULL, TRUE, FALSE, NULL))) + { + WLog_ERR(TAG, "CreateEvent failed!"); + return ERROR_INTERNAL_ERROR; + } + + if (!(echo->thread = CreateThread(NULL, 0, echo_server_thread_func, (void*)echo, 0, NULL))) + { + WLog_ERR(TAG, "CreateEvent failed!"); + CloseHandle(echo->stopEvent); + echo->stopEvent = NULL; + return ERROR_INTERNAL_ERROR; + } + } + + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT echo_server_close(echo_server_context* context) +{ + UINT error = CHANNEL_RC_OK; + echo_server* echo = (echo_server*)context; + + if (echo->thread) + { + SetEvent(echo->stopEvent); + + if (WaitForSingleObject(echo->thread, INFINITE) == WAIT_FAILED) + { + error = GetLastError(); + WLog_ERR(TAG, "WaitForSingleObject failed with error %" PRIu32 "", error); + return error; + } + + CloseHandle(echo->thread); + CloseHandle(echo->stopEvent); + echo->thread = NULL; + echo->stopEvent = NULL; + } + + return error; +} + +static BOOL echo_server_request(echo_server_context* context, const BYTE* buffer, UINT32 length) +{ + echo_server* echo = (echo_server*)context; + return WTSVirtualChannelWrite(echo->echo_channel, (PCHAR)buffer, length, NULL); +} + +echo_server_context* echo_server_context_new(HANDLE vcm) +{ + echo_server* echo; + echo = (echo_server*)calloc(1, sizeof(echo_server)); + + if (echo) + { + echo->context.vcm = vcm; + echo->context.Open = echo_server_open; + echo->context.Close = echo_server_close; + echo->context.Request = echo_server_request; + } + else + WLog_ERR(TAG, "calloc failed!"); + + return (echo_server_context*)echo; +} + +void echo_server_context_free(echo_server_context* context) +{ + echo_server* echo = (echo_server*)context; + echo_server_close(context); + free(echo); +} diff --git a/channels/encomsp/CMakeLists.txt b/channels/encomsp/CMakeLists.txt new file mode 100644 index 0000000..be2d374 --- /dev/null +++ b/channels/encomsp/CMakeLists.txt @@ -0,0 +1,26 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2012 Marc-Andre Moreau +# +# Licensed 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. + +define_channel("encomsp") + +if(WITH_CLIENT_CHANNELS) + add_channel_client(${MODULE_PREFIX} ${CHANNEL_NAME}) +endif() + +if(WITH_SERVER_CHANNELS) + add_channel_server(${MODULE_PREFIX} ${CHANNEL_NAME}) +endif() diff --git a/channels/encomsp/ChannelOptions.cmake b/channels/encomsp/ChannelOptions.cmake new file mode 100644 index 0000000..82ef07e --- /dev/null +++ b/channels/encomsp/ChannelOptions.cmake @@ -0,0 +1,13 @@ + +set(OPTION_DEFAULT OFF) +set(OPTION_CLIENT_DEFAULT ON) +set(OPTION_SERVER_DEFAULT ON) + +define_channel_options(NAME "encomsp" TYPE "static" + DESCRIPTION "Multiparty Virtual Channel Extension" + SPECIFICATIONS "[MS-RDPEMC]" + DEFAULT ${OPTION_DEFAULT}) + +define_channel_client_options(${OPTION_CLIENT_DEFAULT}) +define_channel_server_options(${OPTION_SERVER_DEFAULT}) + diff --git a/channels/encomsp/client/CMakeLists.txt b/channels/encomsp/client/CMakeLists.txt new file mode 100644 index 0000000..92ee1f8 --- /dev/null +++ b/channels/encomsp/client/CMakeLists.txt @@ -0,0 +1,29 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2012 Marc-Andre Moreau +# +# Licensed 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. + +define_channel_client("encomsp") + +include_directories(..) + +set(${MODULE_PREFIX}_SRCS + encomsp_main.c + encomsp_main.h) + +add_channel_client_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} FALSE "VirtualChannelEntryEx") + + +set_property(TARGET ${MODULE_NAME} PROPERTY FOLDER "Channels/${CHANNEL_NAME}/Client") diff --git a/channels/encomsp/client/encomsp_main.c b/channels/encomsp/client/encomsp_main.c new file mode 100644 index 0000000..b384337 --- /dev/null +++ b/channels/encomsp/client/encomsp_main.c @@ -0,0 +1,1349 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Multiparty Virtual Channel + * + * Copyright 2014 Marc-Andre Moreau + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include + +#include +#include + +#include "encomsp_main.h" + +struct encomsp_plugin +{ + CHANNEL_DEF channelDef; + CHANNEL_ENTRY_POINTS_FREERDP_EX channelEntryPoints; + + EncomspClientContext* context; + + HANDLE thread; + wStream* data_in; + void* InitHandle; + DWORD OpenHandle; + wMessageQueue* queue; + rdpContext* rdpcontext; +}; + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT encomsp_read_header(wStream* s, ENCOMSP_ORDER_HEADER* header) +{ + if (Stream_GetRemainingLength(s) < ENCOMSP_ORDER_HEADER_SIZE) + { + WLog_ERR(TAG, "Not enough data!"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT16(s, header->Type); /* Type (2 bytes) */ + Stream_Read_UINT16(s, header->Length); /* Length (2 bytes) */ + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT encomsp_write_header(wStream* s, const ENCOMSP_ORDER_HEADER* header) +{ + Stream_Write_UINT16(s, header->Type); /* Type (2 bytes) */ + Stream_Write_UINT16(s, header->Length); /* Length (2 bytes) */ + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT encomsp_read_unicode_string(wStream* s, ENCOMSP_UNICODE_STRING* str) +{ + ZeroMemory(str, sizeof(ENCOMSP_UNICODE_STRING)); + + if (Stream_GetRemainingLength(s) < 2) + { + WLog_ERR(TAG, "Not enough data!"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT16(s, str->cchString); /* cchString (2 bytes) */ + + if (str->cchString > 1024) + { + WLog_ERR(TAG, "cchString was %" PRIu16 " but has to be < 1025!", str->cchString); + return ERROR_INVALID_DATA; + } + + if (Stream_GetRemainingLength(s) < (size_t)(str->cchString * 2)) + { + WLog_ERR(TAG, "Not enough data!"); + return ERROR_INVALID_DATA; + } + + Stream_Read(s, &(str->wString), (str->cchString * 2)); /* String (variable) */ + return CHANNEL_RC_OK; +} + +static EncomspClientContext* encomsp_get_client_interface(encomspPlugin* encomsp) +{ + EncomspClientContext* pInterface; + pInterface = (EncomspClientContext*)encomsp->channelEntryPoints.pInterface; + return pInterface; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT encomsp_virtual_channel_write(encomspPlugin* encomsp, wStream* s) +{ + UINT status; + + if (!encomsp) + { + Stream_Free(s, TRUE); + return ERROR_INVALID_HANDLE; + } + +#if 0 + WLog_INFO(TAG, "EncomspWrite (%"PRIuz")", Stream_Length(s)); + winpr_HexDump(Stream_Buffer(s), Stream_Length(s)); +#endif + status = encomsp->channelEntryPoints.pVirtualChannelWriteEx( + encomsp->InitHandle, encomsp->OpenHandle, Stream_Buffer(s), (UINT32)Stream_Length(s), s); + + if (status != CHANNEL_RC_OK) + { + Stream_Free(s, TRUE); + WLog_ERR(TAG, "VirtualChannelWriteEx failed with %s [%08" PRIX32 "]", + WTSErrorToString(status), status); + } + return status; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT encomsp_recv_filter_updated_pdu(encomspPlugin* encomsp, wStream* s, + const ENCOMSP_ORDER_HEADER* header) +{ + size_t beg, end, pos; + EncomspClientContext* context; + ENCOMSP_FILTER_UPDATED_PDU pdu; + UINT error = CHANNEL_RC_OK; + context = encomsp_get_client_interface(encomsp); + + if (!context) + return ERROR_INVALID_HANDLE; + + pos = Stream_GetPosition(s); + if (pos < ENCOMSP_ORDER_HEADER_SIZE) + return ERROR_INVALID_DATA; + beg = pos - ENCOMSP_ORDER_HEADER_SIZE; + CopyMemory(&pdu, header, sizeof(ENCOMSP_ORDER_HEADER)); + + if (Stream_GetRemainingLength(s) < 1) + { + WLog_ERR(TAG, "Not enough data!"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT8(s, pdu.Flags); /* Flags (1 byte) */ + end = Stream_GetPosition(s); + + if ((beg + header->Length) < end) + { + WLog_ERR(TAG, "Not enough data!"); + return ERROR_INVALID_DATA; + } + + if ((beg + header->Length) > end) + { + if (Stream_GetRemainingLength(s) < (size_t)((beg + header->Length) - end)) + { + WLog_ERR(TAG, "Not enough data!"); + return ERROR_INVALID_DATA; + } + + Stream_SetPosition(s, (beg + header->Length)); + } + + IFCALLRET(context->FilterUpdated, error, context, &pdu); + + if (error) + WLog_ERR(TAG, "context->FilterUpdated failed with error %" PRIu32 "", error); + + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT encomsp_recv_application_created_pdu(encomspPlugin* encomsp, wStream* s, + const ENCOMSP_ORDER_HEADER* header) +{ + size_t beg, end, pos; + EncomspClientContext* context; + ENCOMSP_APPLICATION_CREATED_PDU pdu; + UINT error; + context = encomsp_get_client_interface(encomsp); + + if (!context) + return ERROR_INVALID_HANDLE; + + if (Stream_GetRemainingLength(s) < 6) + { + WLog_ERR(TAG, "Not enough data!"); + return ERROR_INVALID_DATA; + } + + pos = Stream_GetPosition(s); + if (pos < ENCOMSP_ORDER_HEADER_SIZE) + return ERROR_INVALID_DATA; + beg = pos - ENCOMSP_ORDER_HEADER_SIZE; + CopyMemory(&pdu, header, sizeof(ENCOMSP_ORDER_HEADER)); + + Stream_Read_UINT16(s, pdu.Flags); /* Flags (2 bytes) */ + Stream_Read_UINT32(s, pdu.AppId); /* AppId (4 bytes) */ + + if ((error = encomsp_read_unicode_string(s, &(pdu.Name)))) + { + WLog_ERR(TAG, "encomsp_read_unicode_string failed with error %" PRIu32 "", error); + return error; + } + + end = Stream_GetPosition(s); + + if ((beg + header->Length) < end) + { + WLog_ERR(TAG, "Not enough data!"); + return ERROR_INVALID_DATA; + } + + if ((beg + header->Length) > end) + { + if (Stream_GetRemainingLength(s) < (size_t)((beg + header->Length) - end)) + { + WLog_ERR(TAG, "Not enough data!"); + return ERROR_INVALID_DATA; + } + + Stream_SetPosition(s, (beg + header->Length)); + } + + IFCALLRET(context->ApplicationCreated, error, context, &pdu); + + if (error) + WLog_ERR(TAG, "context->ApplicationCreated failed with error %" PRIu32 "", error); + + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT encomsp_recv_application_removed_pdu(encomspPlugin* encomsp, wStream* s, + const ENCOMSP_ORDER_HEADER* header) +{ + size_t beg, end, pos; + EncomspClientContext* context; + ENCOMSP_APPLICATION_REMOVED_PDU pdu; + UINT error = CHANNEL_RC_OK; + context = encomsp_get_client_interface(encomsp); + + if (!context) + return ERROR_INVALID_HANDLE; + + pos = Stream_GetPosition(s); + if (pos < ENCOMSP_ORDER_HEADER_SIZE) + return ERROR_INVALID_DATA; + beg = pos - ENCOMSP_ORDER_HEADER_SIZE; + CopyMemory(&pdu, header, sizeof(ENCOMSP_ORDER_HEADER)); + + if (Stream_GetRemainingLength(s) < 4) + { + WLog_ERR(TAG, "Not enough data!"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT32(s, pdu.AppId); /* AppId (4 bytes) */ + end = Stream_GetPosition(s); + + if ((beg + header->Length) < end) + { + WLog_ERR(TAG, "Not enough data!"); + return ERROR_INVALID_DATA; + } + + if ((beg + header->Length) > end) + { + if (Stream_GetRemainingLength(s) < (size_t)((beg + header->Length) - end)) + { + WLog_ERR(TAG, "Not enough data!"); + return ERROR_INVALID_DATA; + } + + Stream_SetPosition(s, (beg + header->Length)); + } + + IFCALLRET(context->ApplicationRemoved, error, context, &pdu); + + if (error) + WLog_ERR(TAG, "context->ApplicationRemoved failed with error %" PRIu32 "", error); + + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT encomsp_recv_window_created_pdu(encomspPlugin* encomsp, wStream* s, + const ENCOMSP_ORDER_HEADER* header) +{ + size_t beg, end, pos; + EncomspClientContext* context; + ENCOMSP_WINDOW_CREATED_PDU pdu; + UINT error; + context = encomsp_get_client_interface(encomsp); + + if (!context) + return ERROR_INVALID_HANDLE; + + pos = Stream_GetPosition(s); + if (pos < ENCOMSP_ORDER_HEADER_SIZE) + return ERROR_INVALID_DATA; + beg = pos - ENCOMSP_ORDER_HEADER_SIZE; + CopyMemory(&pdu, header, sizeof(ENCOMSP_ORDER_HEADER)); + + if (Stream_GetRemainingLength(s) < 10) + { + WLog_ERR(TAG, "Not enough data!"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT16(s, pdu.Flags); /* Flags (2 bytes) */ + Stream_Read_UINT32(s, pdu.AppId); /* AppId (4 bytes) */ + Stream_Read_UINT32(s, pdu.WndId); /* WndId (4 bytes) */ + + if ((error = encomsp_read_unicode_string(s, &(pdu.Name)))) + { + WLog_ERR(TAG, "encomsp_read_unicode_string failed with error %" PRIu32 "", error); + return error; + } + + end = Stream_GetPosition(s); + + if ((beg + header->Length) < end) + { + WLog_ERR(TAG, "Not enough data!"); + return ERROR_INVALID_DATA; + } + + if ((beg + header->Length) > end) + { + if (Stream_GetRemainingLength(s) < (size_t)((beg + header->Length) - end)) + { + WLog_ERR(TAG, "Not enough data!"); + return ERROR_INVALID_DATA; + } + + Stream_SetPosition(s, (beg + header->Length)); + } + + IFCALLRET(context->WindowCreated, error, context, &pdu); + + if (error) + WLog_ERR(TAG, "context->WindowCreated failed with error %" PRIu32 "", error); + + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT encomsp_recv_window_removed_pdu(encomspPlugin* encomsp, wStream* s, + const ENCOMSP_ORDER_HEADER* header) +{ + size_t beg, end, pos; + EncomspClientContext* context; + ENCOMSP_WINDOW_REMOVED_PDU pdu; + UINT error = CHANNEL_RC_OK; + context = encomsp_get_client_interface(encomsp); + + if (!context) + return ERROR_INVALID_HANDLE; + + pos = Stream_GetPosition(s); + if (pos < ENCOMSP_ORDER_HEADER_SIZE) + return ERROR_INVALID_DATA; + beg = pos - ENCOMSP_ORDER_HEADER_SIZE; + CopyMemory(&pdu, header, sizeof(ENCOMSP_ORDER_HEADER)); + + if (Stream_GetRemainingLength(s) < 4) + { + WLog_ERR(TAG, "Not enough data!"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT32(s, pdu.WndId); /* WndId (4 bytes) */ + end = Stream_GetPosition(s); + + if ((beg + header->Length) < end) + { + WLog_ERR(TAG, "Not enough data!"); + return ERROR_INVALID_DATA; + } + + if ((beg + header->Length) > end) + { + if (Stream_GetRemainingLength(s) < (size_t)((beg + header->Length) - end)) + { + WLog_ERR(TAG, "Not enough data!"); + return ERROR_INVALID_DATA; + } + + Stream_SetPosition(s, (beg + header->Length)); + } + + IFCALLRET(context->WindowRemoved, error, context, &pdu); + + if (error) + WLog_ERR(TAG, "context->WindowRemoved failed with error %" PRIu32 "", error); + + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT encomsp_recv_show_window_pdu(encomspPlugin* encomsp, wStream* s, + const ENCOMSP_ORDER_HEADER* header) +{ + size_t beg, end, pos; + EncomspClientContext* context; + ENCOMSP_SHOW_WINDOW_PDU pdu; + UINT error = CHANNEL_RC_OK; + context = encomsp_get_client_interface(encomsp); + + if (!context) + return ERROR_INVALID_HANDLE; + + pos = Stream_GetPosition(s); + if (pos < ENCOMSP_ORDER_HEADER_SIZE) + return ERROR_INVALID_DATA; + beg = pos - ENCOMSP_ORDER_HEADER_SIZE; + CopyMemory(&pdu, header, sizeof(ENCOMSP_ORDER_HEADER)); + + if (Stream_GetRemainingLength(s) < 4) + { + WLog_ERR(TAG, "Not enough data!"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT32(s, pdu.WndId); /* WndId (4 bytes) */ + end = Stream_GetPosition(s); + + if ((beg + header->Length) < end) + { + WLog_ERR(TAG, "Not enough data!"); + return ERROR_INVALID_DATA; + } + + if ((beg + header->Length) > end) + { + if (Stream_GetRemainingLength(s) < (size_t)((beg + header->Length) - end)) + { + WLog_ERR(TAG, "Not enough data!"); + return ERROR_INVALID_DATA; + } + + Stream_SetPosition(s, (beg + header->Length)); + } + + IFCALLRET(context->ShowWindow, error, context, &pdu); + + if (error) + WLog_ERR(TAG, "context->ShowWindow failed with error %" PRIu32 "", error); + + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT encomsp_recv_participant_created_pdu(encomspPlugin* encomsp, wStream* s, + const ENCOMSP_ORDER_HEADER* header) +{ + size_t beg, end, pos; + EncomspClientContext* context; + ENCOMSP_PARTICIPANT_CREATED_PDU pdu; + UINT error; + context = encomsp_get_client_interface(encomsp); + + if (!context) + return ERROR_INVALID_HANDLE; + + pos = Stream_GetPosition(s); + if (pos < ENCOMSP_ORDER_HEADER_SIZE) + return ERROR_INVALID_DATA; + beg = pos - ENCOMSP_ORDER_HEADER_SIZE; + CopyMemory(&pdu, header, sizeof(ENCOMSP_ORDER_HEADER)); + + if (Stream_GetRemainingLength(s) < 10) + { + WLog_ERR(TAG, "Not enough data!"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT32(s, pdu.ParticipantId); /* ParticipantId (4 bytes) */ + Stream_Read_UINT32(s, pdu.GroupId); /* GroupId (4 bytes) */ + Stream_Read_UINT16(s, pdu.Flags); /* Flags (2 bytes) */ + + if ((error = encomsp_read_unicode_string(s, &(pdu.FriendlyName)))) + { + WLog_ERR(TAG, "encomsp_read_unicode_string failed with error %" PRIu32 "", error); + return error; + } + + end = Stream_GetPosition(s); + + if ((beg + header->Length) < end) + { + WLog_ERR(TAG, "Not enough data!"); + return ERROR_INVALID_DATA; + } + + if ((beg + header->Length) > end) + { + if (Stream_GetRemainingLength(s) < (size_t)((beg + header->Length) - end)) + { + WLog_ERR(TAG, "Not enough data!"); + return ERROR_INVALID_DATA; + } + + Stream_SetPosition(s, (beg + header->Length)); + } + + IFCALLRET(context->ParticipantCreated, error, context, &pdu); + + if (error) + WLog_ERR(TAG, "context->ParticipantCreated failed with error %" PRIu32 "", error); + + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT encomsp_recv_participant_removed_pdu(encomspPlugin* encomsp, wStream* s, + const ENCOMSP_ORDER_HEADER* header) +{ + size_t beg, end; + EncomspClientContext* context; + ENCOMSP_PARTICIPANT_REMOVED_PDU pdu; + UINT error = CHANNEL_RC_OK; + context = encomsp_get_client_interface(encomsp); + + if (!context) + return ERROR_INVALID_HANDLE; + + if (Stream_GetRemainingLength(s) < 12) + { + WLog_ERR(TAG, "Not enough data!"); + return ERROR_INVALID_DATA; + } + + beg = (Stream_GetPosition(s)) - ENCOMSP_ORDER_HEADER_SIZE; + CopyMemory(&pdu, header, sizeof(ENCOMSP_ORDER_HEADER)); + + Stream_Read_UINT32(s, pdu.ParticipantId); /* ParticipantId (4 bytes) */ + Stream_Read_UINT32(s, pdu.DiscType); /* DiscType (4 bytes) */ + Stream_Read_UINT32(s, pdu.DiscCode); /* DiscCode (4 bytes) */ + end = Stream_GetPosition(s); + + if ((beg + header->Length) < end) + { + WLog_ERR(TAG, "Not enough data!"); + return ERROR_INVALID_DATA; + } + + if ((beg + header->Length) > end) + { + if (Stream_GetRemainingLength(s) < (size_t)((beg + header->Length) - end)) + { + WLog_ERR(TAG, "Not enough data!"); + return ERROR_INVALID_DATA; + } + + Stream_SetPosition(s, (beg + header->Length)); + } + + IFCALLRET(context->ParticipantRemoved, error, context, &pdu); + + if (error) + WLog_ERR(TAG, "context->ParticipantRemoved failed with error %" PRIu32 "", error); + + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT encomsp_recv_change_participant_control_level_pdu(encomspPlugin* encomsp, wStream* s, + const ENCOMSP_ORDER_HEADER* header) +{ + size_t beg, end, pos; + EncomspClientContext* context; + ENCOMSP_CHANGE_PARTICIPANT_CONTROL_LEVEL_PDU pdu; + UINT error = CHANNEL_RC_OK; + context = encomsp_get_client_interface(encomsp); + + if (!context) + return ERROR_INVALID_HANDLE; + + pos = Stream_GetPosition(s); + if (pos < ENCOMSP_ORDER_HEADER_SIZE) + return ERROR_INVALID_DATA; + beg = pos - ENCOMSP_ORDER_HEADER_SIZE; + CopyMemory(&pdu, header, sizeof(ENCOMSP_ORDER_HEADER)); + + if (Stream_GetRemainingLength(s) < 6) + { + WLog_ERR(TAG, "Not enough data!"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT16(s, pdu.Flags); /* Flags (2 bytes) */ + Stream_Read_UINT32(s, pdu.ParticipantId); /* ParticipantId (4 bytes) */ + end = Stream_GetPosition(s); + + if ((beg + header->Length) < end) + { + WLog_ERR(TAG, "Not enough data!"); + return ERROR_INVALID_DATA; + } + + if ((beg + header->Length) > end) + { + if (Stream_GetRemainingLength(s) < (size_t)((beg + header->Length) - end)) + { + WLog_ERR(TAG, "Not enough data!"); + return ERROR_INVALID_DATA; + } + + Stream_SetPosition(s, (beg + header->Length)); + } + + IFCALLRET(context->ChangeParticipantControlLevel, error, context, &pdu); + + if (error) + WLog_ERR(TAG, "context->ChangeParticipantControlLevel failed with error %" PRIu32 "", + error); + + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT encomsp_send_change_participant_control_level_pdu( + EncomspClientContext* context, const ENCOMSP_CHANGE_PARTICIPANT_CONTROL_LEVEL_PDU* pdu) +{ + wStream* s; + encomspPlugin* encomsp; + UINT error; + ENCOMSP_ORDER_HEADER header; + + encomsp = (encomspPlugin*)context->handle; + header.Type = ODTYPE_PARTICIPANT_CTRL_CHANGED; + header.Length = ENCOMSP_ORDER_HEADER_SIZE + 6; + s = Stream_New(NULL, header.Length); + + if (!s) + { + WLog_ERR(TAG, "Stream_New failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + if ((error = encomsp_write_header(s, &header))) + { + WLog_ERR(TAG, "encomsp_write_header failed with error %" PRIu32 "!", error); + return error; + } + + Stream_Write_UINT16(s, pdu->Flags); /* Flags (2 bytes) */ + Stream_Write_UINT32(s, pdu->ParticipantId); /* ParticipantId (4 bytes) */ + Stream_SealLength(s); + return encomsp_virtual_channel_write(encomsp, s); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT encomsp_recv_graphics_stream_paused_pdu(encomspPlugin* encomsp, wStream* s, + const ENCOMSP_ORDER_HEADER* header) +{ + size_t beg, end, pos; + EncomspClientContext* context; + ENCOMSP_GRAPHICS_STREAM_PAUSED_PDU pdu; + UINT error = CHANNEL_RC_OK; + context = encomsp_get_client_interface(encomsp); + + if (!context) + return ERROR_INVALID_HANDLE; + + pos = Stream_GetPosition(s); + if (pos < ENCOMSP_ORDER_HEADER_SIZE) + return ERROR_INVALID_DATA; + beg = pos - ENCOMSP_ORDER_HEADER_SIZE; + CopyMemory(&pdu, header, sizeof(ENCOMSP_ORDER_HEADER)); + end = Stream_GetPosition(s); + + if ((beg + header->Length) < end) + { + WLog_ERR(TAG, "Not enough data!"); + return ERROR_INVALID_DATA; + } + + if ((beg + header->Length) > end) + { + if (Stream_GetRemainingLength(s) < (size_t)((beg + header->Length) - end)) + { + WLog_ERR(TAG, "Not enough data!"); + return ERROR_INVALID_DATA; + } + + Stream_SetPosition(s, (beg + header->Length)); + } + + IFCALLRET(context->GraphicsStreamPaused, error, context, &pdu); + + if (error) + WLog_ERR(TAG, "context->GraphicsStreamPaused failed with error %" PRIu32 "", error); + + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT encomsp_recv_graphics_stream_resumed_pdu(encomspPlugin* encomsp, wStream* s, + const ENCOMSP_ORDER_HEADER* header) +{ + size_t beg, end, pos; + EncomspClientContext* context; + ENCOMSP_GRAPHICS_STREAM_RESUMED_PDU pdu; + UINT error = CHANNEL_RC_OK; + context = encomsp_get_client_interface(encomsp); + + if (!context) + return ERROR_INVALID_HANDLE; + + pos = Stream_GetPosition(s); + if (pos < ENCOMSP_ORDER_HEADER_SIZE) + return ERROR_INVALID_DATA; + beg = pos - ENCOMSP_ORDER_HEADER_SIZE; + CopyMemory(&pdu, header, sizeof(ENCOMSP_ORDER_HEADER)); + end = Stream_GetPosition(s); + + if ((beg + header->Length) < end) + { + WLog_ERR(TAG, "Not enough data!"); + return ERROR_INVALID_DATA; + } + + if ((beg + header->Length) > end) + { + if (Stream_GetRemainingLength(s) < (size_t)((beg + header->Length) - end)) + { + WLog_ERR(TAG, "Not enough data!"); + return ERROR_INVALID_DATA; + } + + Stream_SetPosition(s, (beg + header->Length)); + } + + IFCALLRET(context->GraphicsStreamResumed, error, context, &pdu); + + if (error) + WLog_ERR(TAG, "context->GraphicsStreamResumed failed with error %" PRIu32 "", error); + + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT encomsp_process_receive(encomspPlugin* encomsp, wStream* s) +{ + UINT error = CHANNEL_RC_OK; + ENCOMSP_ORDER_HEADER header; + + while (Stream_GetRemainingLength(s) > 0) + { + if ((error = encomsp_read_header(s, &header))) + { + WLog_ERR(TAG, "encomsp_read_header failed with error %" PRIu32 "!", error); + return error; + } + + // WLog_DBG(TAG, "EncomspReceive: Type: %"PRIu16" Length: %"PRIu16"", header.Type, + // header.Length); + + switch (header.Type) + { + case ODTYPE_FILTER_STATE_UPDATED: + if ((error = encomsp_recv_filter_updated_pdu(encomsp, s, &header))) + { + WLog_ERR(TAG, "encomsp_recv_filter_updated_pdu failed with error %" PRIu32 "!", + error); + return error; + } + + break; + + case ODTYPE_APP_REMOVED: + if ((error = encomsp_recv_application_removed_pdu(encomsp, s, &header))) + { + WLog_ERR(TAG, + "encomsp_recv_application_removed_pdu failed with error %" PRIu32 "!", + error); + return error; + } + + break; + + case ODTYPE_APP_CREATED: + if ((error = encomsp_recv_application_created_pdu(encomsp, s, &header))) + { + WLog_ERR(TAG, + "encomsp_recv_application_removed_pdu failed with error %" PRIu32 "!", + error); + return error; + } + + break; + + case ODTYPE_WND_REMOVED: + if ((error = encomsp_recv_window_removed_pdu(encomsp, s, &header))) + { + WLog_ERR(TAG, "encomsp_recv_window_removed_pdu failed with error %" PRIu32 "!", + error); + return error; + } + + break; + + case ODTYPE_WND_CREATED: + if ((error = encomsp_recv_window_created_pdu(encomsp, s, &header))) + { + WLog_ERR(TAG, "encomsp_recv_window_created_pdu failed with error %" PRIu32 "!", + error); + return error; + } + + break; + + case ODTYPE_WND_SHOW: + if ((error = encomsp_recv_show_window_pdu(encomsp, s, &header))) + { + WLog_ERR(TAG, "encomsp_recv_show_window_pdu failed with error %" PRIu32 "!", + error); + return error; + } + + break; + + case ODTYPE_PARTICIPANT_REMOVED: + if ((error = encomsp_recv_participant_removed_pdu(encomsp, s, &header))) + { + WLog_ERR(TAG, + "encomsp_recv_participant_removed_pdu failed with error %" PRIu32 "!", + error); + return error; + } + + break; + + case ODTYPE_PARTICIPANT_CREATED: + if ((error = encomsp_recv_participant_created_pdu(encomsp, s, &header))) + { + WLog_ERR(TAG, + "encomsp_recv_participant_created_pdu failed with error %" PRIu32 "!", + error); + return error; + } + + break; + + case ODTYPE_PARTICIPANT_CTRL_CHANGED: + if ((error = + encomsp_recv_change_participant_control_level_pdu(encomsp, s, &header))) + { + WLog_ERR(TAG, + "encomsp_recv_change_participant_control_level_pdu failed with error " + "%" PRIu32 "!", + error); + return error; + } + + break; + + case ODTYPE_GRAPHICS_STREAM_PAUSED: + if ((error = encomsp_recv_graphics_stream_paused_pdu(encomsp, s, &header))) + { + WLog_ERR(TAG, + "encomsp_recv_graphics_stream_paused_pdu failed with error %" PRIu32 + "!", + error); + return error; + } + + break; + + case ODTYPE_GRAPHICS_STREAM_RESUMED: + if ((error = encomsp_recv_graphics_stream_resumed_pdu(encomsp, s, &header))) + { + WLog_ERR(TAG, + "encomsp_recv_graphics_stream_resumed_pdu failed with error %" PRIu32 + "!", + error); + return error; + } + + break; + + default: + WLog_ERR(TAG, "header.Type %" PRIu16 " not found", header.Type); + return ERROR_INVALID_DATA; + } + } + + return error; +} + +static void encomsp_process_connect(encomspPlugin* encomsp) +{ +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT encomsp_virtual_channel_event_data_received(encomspPlugin* encomsp, const void* pData, + UINT32 dataLength, UINT32 totalLength, + UINT32 dataFlags) +{ + wStream* data_in; + + if ((dataFlags & CHANNEL_FLAG_SUSPEND) || (dataFlags & CHANNEL_FLAG_RESUME)) + return CHANNEL_RC_OK; + + if (dataFlags & CHANNEL_FLAG_FIRST) + { + if (encomsp->data_in) + Stream_Free(encomsp->data_in, TRUE); + + encomsp->data_in = Stream_New(NULL, totalLength); + + if (!encomsp->data_in) + { + WLog_ERR(TAG, "Stream_New failed!"); + return CHANNEL_RC_NO_MEMORY; + } + } + + data_in = encomsp->data_in; + + if (!Stream_EnsureRemainingCapacity(data_in, dataLength)) + { + WLog_ERR(TAG, "Stream_EnsureRemainingCapacity failed!"); + return ERROR_INTERNAL_ERROR; + } + + Stream_Write(data_in, pData, dataLength); + + if (dataFlags & CHANNEL_FLAG_LAST) + { + if (Stream_Capacity(data_in) != Stream_GetPosition(data_in)) + { + WLog_ERR(TAG, "encomsp_plugin_process_received: read error"); + return ERROR_INVALID_DATA; + } + + encomsp->data_in = NULL; + Stream_SealLength(data_in); + Stream_SetPosition(data_in, 0); + + if (!MessageQueue_Post(encomsp->queue, NULL, 0, (void*)data_in, NULL)) + { + WLog_ERR(TAG, "MessageQueue_Post failed!"); + return ERROR_INTERNAL_ERROR; + } + } + + return CHANNEL_RC_OK; +} + +static VOID VCAPITYPE encomsp_virtual_channel_open_event_ex(LPVOID lpUserParam, DWORD openHandle, + UINT event, LPVOID pData, + UINT32 dataLength, UINT32 totalLength, + UINT32 dataFlags) +{ + UINT error = CHANNEL_RC_OK; + encomspPlugin* encomsp = (encomspPlugin*)lpUserParam; + + switch (event) + { + case CHANNEL_EVENT_DATA_RECEIVED: + if (!encomsp || (encomsp->OpenHandle != openHandle)) + { + WLog_ERR(TAG, "error no match"); + return; + } + if ((error = encomsp_virtual_channel_event_data_received(encomsp, pData, dataLength, + totalLength, dataFlags))) + WLog_ERR(TAG, + "encomsp_virtual_channel_event_data_received failed with error %" PRIu32 + "", + error); + + break; + + case CHANNEL_EVENT_WRITE_CANCELLED: + case CHANNEL_EVENT_WRITE_COMPLETE: + { + wStream* s = (wStream*)pData; + Stream_Free(s, TRUE); + } + break; + + case CHANNEL_EVENT_USER: + break; + } + + if (error && encomsp && encomsp->rdpcontext) + setChannelError(encomsp->rdpcontext, error, + "encomsp_virtual_channel_open_event reported an error"); + + return; +} + +static DWORD WINAPI encomsp_virtual_channel_client_thread(LPVOID arg) +{ + wStream* data; + wMessage message; + encomspPlugin* encomsp = (encomspPlugin*)arg; + UINT error = CHANNEL_RC_OK; + encomsp_process_connect(encomsp); + + while (1) + { + if (!MessageQueue_Wait(encomsp->queue)) + { + WLog_ERR(TAG, "MessageQueue_Wait failed!"); + error = ERROR_INTERNAL_ERROR; + break; + } + + if (!MessageQueue_Peek(encomsp->queue, &message, TRUE)) + { + WLog_ERR(TAG, "MessageQueue_Peek failed!"); + error = ERROR_INTERNAL_ERROR; + break; + } + + if (message.id == WMQ_QUIT) + break; + + if (message.id == 0) + { + data = (wStream*)message.wParam; + + if ((error = encomsp_process_receive(encomsp, data))) + { + WLog_ERR(TAG, "encomsp_process_receive failed with error %" PRIu32 "!", error); + Stream_Free(data, TRUE); + break; + } + + Stream_Free(data, TRUE); + } + } + + if (error && encomsp->rdpcontext) + setChannelError(encomsp->rdpcontext, error, + "encomsp_virtual_channel_client_thread reported an error"); + + ExitThread(error); + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT encomsp_virtual_channel_event_connected(encomspPlugin* encomsp, LPVOID pData, + UINT32 dataLength) +{ + UINT32 status; + status = encomsp->channelEntryPoints.pVirtualChannelOpenEx( + encomsp->InitHandle, &encomsp->OpenHandle, encomsp->channelDef.name, + encomsp_virtual_channel_open_event_ex); + + if (status != CHANNEL_RC_OK) + { + WLog_ERR(TAG, "pVirtualChannelOpen failed with %s [%08" PRIX32 "]", + WTSErrorToString(status), status); + return status; + } + + encomsp->queue = MessageQueue_New(NULL); + + if (!encomsp->queue) + { + WLog_ERR(TAG, "MessageQueue_New failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + if (!(encomsp->thread = CreateThread(NULL, 0, encomsp_virtual_channel_client_thread, + (void*)encomsp, 0, NULL))) + { + WLog_ERR(TAG, "CreateThread failed!"); + MessageQueue_Free(encomsp->queue); + return ERROR_INTERNAL_ERROR; + } + + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT encomsp_virtual_channel_event_disconnected(encomspPlugin* encomsp) +{ + UINT rc; + + if (encomsp->OpenHandle == 0) + return CHANNEL_RC_OK; + + if (MessageQueue_PostQuit(encomsp->queue, 0) && + (WaitForSingleObject(encomsp->thread, INFINITE) == WAIT_FAILED)) + { + rc = GetLastError(); + WLog_ERR(TAG, "WaitForSingleObject failed with error %" PRIu32 "", rc); + return rc; + } + + MessageQueue_Free(encomsp->queue); + CloseHandle(encomsp->thread); + encomsp->queue = NULL; + encomsp->thread = NULL; + rc = encomsp->channelEntryPoints.pVirtualChannelCloseEx(encomsp->InitHandle, + encomsp->OpenHandle); + + if (CHANNEL_RC_OK != rc) + { + WLog_ERR(TAG, "pVirtualChannelClose failed with %s [%08" PRIX32 "]", WTSErrorToString(rc), + rc); + return rc; + } + + encomsp->OpenHandle = 0; + + if (encomsp->data_in) + { + Stream_Free(encomsp->data_in, TRUE); + encomsp->data_in = NULL; + } + + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT encomsp_virtual_channel_event_terminated(encomspPlugin* encomsp) +{ + encomsp->InitHandle = 0; + free(encomsp->context); + free(encomsp); + return CHANNEL_RC_OK; +} + +static VOID VCAPITYPE encomsp_virtual_channel_init_event_ex(LPVOID lpUserParam, LPVOID pInitHandle, + UINT event, LPVOID pData, + UINT dataLength) +{ + UINT error = CHANNEL_RC_OK; + encomspPlugin* encomsp = (encomspPlugin*)lpUserParam; + + if (!encomsp || (encomsp->InitHandle != pInitHandle)) + { + WLog_ERR(TAG, "error no match"); + return; + } + + switch (event) + { + case CHANNEL_EVENT_CONNECTED: + if ((error = encomsp_virtual_channel_event_connected(encomsp, pData, dataLength))) + WLog_ERR(TAG, + "encomsp_virtual_channel_event_connected failed with error %" PRIu32 "", + error); + + break; + + case CHANNEL_EVENT_DISCONNECTED: + if ((error = encomsp_virtual_channel_event_disconnected(encomsp))) + WLog_ERR(TAG, + "encomsp_virtual_channel_event_disconnected failed with error %" PRIu32 "", + error); + + break; + + case CHANNEL_EVENT_TERMINATED: + encomsp_virtual_channel_event_terminated(encomsp); + break; + + default: + break; + } + + if (error && encomsp->rdpcontext) + setChannelError(encomsp->rdpcontext, error, + "encomsp_virtual_channel_init_event reported an error"); +} + +/* encomsp is always built-in */ +#define VirtualChannelEntryEx encomsp_VirtualChannelEntryEx + +BOOL VCAPITYPE VirtualChannelEntryEx(PCHANNEL_ENTRY_POINTS_EX pEntryPoints, PVOID pInitHandle) +{ + UINT rc; + encomspPlugin* encomsp; + EncomspClientContext* context = NULL; + CHANNEL_ENTRY_POINTS_FREERDP_EX* pEntryPointsEx; + BOOL isFreerdp = FALSE; + encomsp = (encomspPlugin*)calloc(1, sizeof(encomspPlugin)); + + if (!encomsp) + { + WLog_ERR(TAG, "calloc failed!"); + return FALSE; + } + + encomsp->channelDef.options = CHANNEL_OPTION_INITIALIZED | CHANNEL_OPTION_ENCRYPT_RDP | + CHANNEL_OPTION_COMPRESS_RDP | CHANNEL_OPTION_SHOW_PROTOCOL; + sprintf_s(encomsp->channelDef.name, ARRAYSIZE(encomsp->channelDef.name), "encomsp"); + pEntryPointsEx = (CHANNEL_ENTRY_POINTS_FREERDP_EX*)pEntryPoints; + + if ((pEntryPointsEx->cbSize >= sizeof(CHANNEL_ENTRY_POINTS_FREERDP_EX)) && + (pEntryPointsEx->MagicNumber == FREERDP_CHANNEL_MAGIC_NUMBER)) + { + context = (EncomspClientContext*)calloc(1, sizeof(EncomspClientContext)); + + if (!context) + { + WLog_ERR(TAG, "calloc failed!"); + goto error_out; + } + + context->handle = (void*)encomsp; + context->FilterUpdated = NULL; + context->ApplicationCreated = NULL; + context->ApplicationRemoved = NULL; + context->WindowCreated = NULL; + context->WindowRemoved = NULL; + context->ShowWindow = NULL; + context->ParticipantCreated = NULL; + context->ParticipantRemoved = NULL; + context->ChangeParticipantControlLevel = encomsp_send_change_participant_control_level_pdu; + context->GraphicsStreamPaused = NULL; + context->GraphicsStreamResumed = NULL; + encomsp->context = context; + encomsp->rdpcontext = pEntryPointsEx->context; + isFreerdp = TRUE; + } + + CopyMemory(&(encomsp->channelEntryPoints), pEntryPoints, + sizeof(CHANNEL_ENTRY_POINTS_FREERDP_EX)); + encomsp->InitHandle = pInitHandle; + rc = encomsp->channelEntryPoints.pVirtualChannelInitEx( + encomsp, context, pInitHandle, &encomsp->channelDef, 1, VIRTUAL_CHANNEL_VERSION_WIN2000, + encomsp_virtual_channel_init_event_ex); + + if (CHANNEL_RC_OK != rc) + { + WLog_ERR(TAG, "failed with %s [%08" PRIX32 "]", WTSErrorToString(rc), rc); + goto error_out; + } + + encomsp->channelEntryPoints.pInterface = context; + return TRUE; +error_out: + + if (isFreerdp) + free(encomsp->context); + + free(encomsp); + return FALSE; +} diff --git a/channels/encomsp/client/encomsp_main.h b/channels/encomsp/client/encomsp_main.h new file mode 100644 index 0000000..ad43cea --- /dev/null +++ b/channels/encomsp/client/encomsp_main.h @@ -0,0 +1,42 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Multiparty Virtual Channel + * + * Copyright 2014 Marc-Andre Moreau + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger + * + * Licensed 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 FREERDP_CHANNEL_ENCOMSP_CLIENT_MAIN_H +#define FREERDP_CHANNEL_ENCOMSP_CLIENT_MAIN_H + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include + +#define TAG CHANNELS_TAG("encomsp.client") + +typedef struct encomsp_plugin encomspPlugin; + +#endif /* FREERDP_CHANNEL_ENCOMSP_CLIENT_MAIN_H */ diff --git a/channels/encomsp/server/CMakeLists.txt b/channels/encomsp/server/CMakeLists.txt new file mode 100644 index 0000000..10ac0c6 --- /dev/null +++ b/channels/encomsp/server/CMakeLists.txt @@ -0,0 +1,35 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2012 Marc-Andre Moreau +# +# Licensed 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. + +define_channel_server("encomsp") + +include_directories(..) + +set(${MODULE_PREFIX}_SRCS + encomsp_main.c + encomsp_main.h) + +add_channel_server_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} FALSE "VirtualChannelEntry") + + + +set(${MODULE_PREFIX}_LIBS ${${MODULE_PREFIX}_LIBS} winpr) + +target_link_libraries(${MODULE_NAME} ${${MODULE_PREFIX}_LIBS}) + + +set_property(TARGET ${MODULE_NAME} PROPERTY FOLDER "Channels/${CHANNEL_NAME}/Server") diff --git a/channels/encomsp/server/encomsp_main.c b/channels/encomsp/server/encomsp_main.c new file mode 100644 index 0000000..9b1693b --- /dev/null +++ b/channels/encomsp/server/encomsp_main.c @@ -0,0 +1,379 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Multiparty Virtual Channel + * + * Copyright 2014 Marc-Andre Moreau + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include + +#include + +#include "encomsp_main.h" + +#define TAG CHANNELS_TAG("encomsp.server") + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT encomsp_read_header(wStream* s, ENCOMSP_ORDER_HEADER* header) +{ + if (Stream_GetRemainingLength(s) < ENCOMSP_ORDER_HEADER_SIZE) + return ERROR_INVALID_DATA; + + Stream_Read_UINT16(s, header->Type); /* Type (2 bytes) */ + Stream_Read_UINT16(s, header->Length); /* Length (2 bytes) */ + return CHANNEL_RC_OK; +} + +#if 0 + +static int encomsp_write_header(wStream* s, ENCOMSP_ORDER_HEADER* header) +{ + Stream_Write_UINT16(s, header->Type); /* Type (2 bytes) */ + Stream_Write_UINT16(s, header->Length); /* Length (2 bytes) */ + return 1; +} + +static int encomsp_read_unicode_string(wStream* s, ENCOMSP_UNICODE_STRING* str) +{ + ZeroMemory(str, sizeof(ENCOMSP_UNICODE_STRING)); + + if (Stream_GetRemainingLength(s) < 2) + return -1; + + Stream_Read_UINT16(s, str->cchString); /* cchString (2 bytes) */ + + if (str->cchString > 1024) + return -1; + + if (Stream_GetRemainingLength(s) < (str->cchString * 2)) + return -1; + + Stream_Read(s, &(str->wString), (str->cchString * 2)); /* String (variable) */ + return 1; +} + +#endif + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT encomsp_recv_change_participant_control_level_pdu(EncomspServerContext* context, + wStream* s, + ENCOMSP_ORDER_HEADER* header) +{ + int beg, end; + ENCOMSP_CHANGE_PARTICIPANT_CONTROL_LEVEL_PDU pdu; + UINT error = CHANNEL_RC_OK; + beg = ((int)Stream_GetPosition(s)) - ENCOMSP_ORDER_HEADER_SIZE; + CopyMemory(&pdu, header, sizeof(ENCOMSP_ORDER_HEADER)); + + if (Stream_GetRemainingLength(s) < 6) + { + WLog_ERR(TAG, "Not enough data!"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT16(s, pdu.Flags); /* Flags (2 bytes) */ + Stream_Read_UINT32(s, pdu.ParticipantId); /* ParticipantId (4 bytes) */ + end = (int)Stream_GetPosition(s); + + if ((beg + header->Length) < end) + { + WLog_ERR(TAG, "Not enough data!"); + return ERROR_INVALID_DATA; + } + + if ((beg + header->Length) > end) + { + if (Stream_GetRemainingLength(s) < (size_t)((beg + header->Length) - end)) + { + WLog_ERR(TAG, "Not enough data!"); + return ERROR_INVALID_DATA; + } + + Stream_SetPosition(s, (beg + header->Length)); + } + + IFCALLRET(context->ChangeParticipantControlLevel, error, context, &pdu); + + if (error) + WLog_ERR(TAG, "context->ChangeParticipantControlLevel failed with error %" PRIu32 "", + error); + + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT encomsp_server_receive_pdu(EncomspServerContext* context, wStream* s) +{ + UINT error = CHANNEL_RC_OK; + ENCOMSP_ORDER_HEADER header; + + while (Stream_GetRemainingLength(s) > 0) + { + if ((error = encomsp_read_header(s, &header))) + { + WLog_ERR(TAG, "encomsp_read_header failed with error %" PRIu32 "!", error); + return error; + } + + WLog_INFO(TAG, "EncomspReceive: Type: %" PRIu16 " Length: %" PRIu16 "", header.Type, + header.Length); + + switch (header.Type) + { + case ODTYPE_PARTICIPANT_CTRL_CHANGED: + if ((error = + encomsp_recv_change_participant_control_level_pdu(context, s, &header))) + { + WLog_ERR(TAG, + "encomsp_recv_change_participant_control_level_pdu failed with error " + "%" PRIu32 "!", + error); + return error; + } + + break; + + default: + WLog_ERR(TAG, "header.Type unknown %" PRIu16 "!", header.Type); + return ERROR_INVALID_DATA; + break; + } + } + + return error; +} + +static DWORD WINAPI encomsp_server_thread(LPVOID arg) +{ + wStream* s; + DWORD nCount; + void* buffer; + HANDLE events[8]; + HANDLE ChannelEvent; + DWORD BytesReturned; + ENCOMSP_ORDER_HEADER* header; + EncomspServerContext* context; + UINT error = CHANNEL_RC_OK; + DWORD status; + context = (EncomspServerContext*)arg; + + buffer = NULL; + BytesReturned = 0; + ChannelEvent = NULL; + s = Stream_New(NULL, 4096); + + if (!s) + { + WLog_ERR(TAG, "Stream_New failed!"); + error = CHANNEL_RC_NO_MEMORY; + goto out; + } + + if (WTSVirtualChannelQuery(context->priv->ChannelHandle, WTSVirtualEventHandle, &buffer, + &BytesReturned) == TRUE) + { + if (BytesReturned == sizeof(HANDLE)) + CopyMemory(&ChannelEvent, buffer, sizeof(HANDLE)); + + WTSFreeMemory(buffer); + } + + nCount = 0; + events[nCount++] = ChannelEvent; + events[nCount++] = context->priv->StopEvent; + + while (1) + { + status = WaitForMultipleObjects(nCount, events, FALSE, INFINITE); + + if (status == WAIT_FAILED) + { + error = GetLastError(); + WLog_ERR(TAG, "WaitForMultipleObjects failed with error %" PRIu32 "", error); + break; + } + + status = WaitForSingleObject(context->priv->StopEvent, 0); + + if (status == WAIT_FAILED) + { + error = GetLastError(); + WLog_ERR(TAG, "WaitForSingleObject failed with error %" PRIu32 "", error); + break; + } + + if (status == WAIT_OBJECT_0) + { + break; + } + + WTSVirtualChannelRead(context->priv->ChannelHandle, 0, NULL, 0, &BytesReturned); + + if (BytesReturned < 1) + continue; + + if (!Stream_EnsureRemainingCapacity(s, BytesReturned)) + { + WLog_ERR(TAG, "Stream_EnsureRemainingCapacity failed!"); + error = CHANNEL_RC_NO_MEMORY; + break; + } + + if (!WTSVirtualChannelRead(context->priv->ChannelHandle, 0, (PCHAR)Stream_Buffer(s), + Stream_Capacity(s), &BytesReturned)) + { + WLog_ERR(TAG, "WTSVirtualChannelRead failed!"); + error = ERROR_INTERNAL_ERROR; + break; + } + + if (Stream_GetPosition(s) >= ENCOMSP_ORDER_HEADER_SIZE) + { + header = (ENCOMSP_ORDER_HEADER*)Stream_Buffer(s); + + if (header->Length >= Stream_GetPosition(s)) + { + Stream_SealLength(s); + Stream_SetPosition(s, 0); + + if ((error = encomsp_server_receive_pdu(context, s))) + { + WLog_ERR(TAG, "encomsp_server_receive_pdu failed with error %" PRIu32 "!", + error); + break; + } + + Stream_SetPosition(s, 0); + } + } + } + + Stream_Free(s, TRUE); +out: + + if (error && context->rdpcontext) + setChannelError(context->rdpcontext, error, "encomsp_server_thread reported an error"); + + ExitThread(error); + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT encomsp_server_start(EncomspServerContext* context) +{ + context->priv->ChannelHandle = + WTSVirtualChannelOpen(context->vcm, WTS_CURRENT_SESSION, "encomsp"); + + if (!context->priv->ChannelHandle) + return CHANNEL_RC_BAD_CHANNEL; + + if (!(context->priv->StopEvent = CreateEvent(NULL, TRUE, FALSE, NULL))) + { + WLog_ERR(TAG, "CreateEvent failed!"); + return ERROR_INTERNAL_ERROR; + } + + if (!(context->priv->Thread = + CreateThread(NULL, 0, encomsp_server_thread, (void*)context, 0, NULL))) + { + WLog_ERR(TAG, "CreateThread failed!"); + CloseHandle(context->priv->StopEvent); + context->priv->StopEvent = NULL; + return ERROR_INTERNAL_ERROR; + } + + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT encomsp_server_stop(EncomspServerContext* context) +{ + UINT error = CHANNEL_RC_OK; + SetEvent(context->priv->StopEvent); + + if (WaitForSingleObject(context->priv->Thread, INFINITE) == WAIT_FAILED) + { + error = GetLastError(); + WLog_ERR(TAG, "WaitForSingleObject failed with error %" PRIu32 "", error); + return error; + } + + CloseHandle(context->priv->Thread); + CloseHandle(context->priv->StopEvent); + return error; +} + +EncomspServerContext* encomsp_server_context_new(HANDLE vcm) +{ + EncomspServerContext* context; + context = (EncomspServerContext*)calloc(1, sizeof(EncomspServerContext)); + + if (context) + { + context->vcm = vcm; + context->Start = encomsp_server_start; + context->Stop = encomsp_server_stop; + context->priv = (EncomspServerPrivate*)calloc(1, sizeof(EncomspServerPrivate)); + + if (!context->priv) + { + WLog_ERR(TAG, "calloc failed!"); + free(context); + return NULL; + } + } + + return context; +} + +void encomsp_server_context_free(EncomspServerContext* context) +{ + if (context) + { + if (context->priv->ChannelHandle != INVALID_HANDLE_VALUE) + WTSVirtualChannelClose(context->priv->ChannelHandle); + + free(context->priv); + free(context); + } +} diff --git a/channels/encomsp/server/encomsp_main.h b/channels/encomsp/server/encomsp_main.h new file mode 100644 index 0000000..18daf72 --- /dev/null +++ b/channels/encomsp/server/encomsp_main.h @@ -0,0 +1,36 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Multiparty Virtual Channel + * + * Copyright 2014 Marc-Andre Moreau + * + * Licensed 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 FREERDP_CHANNEL_ENCOMSP_SERVER_MAIN_H +#define FREERDP_CHANNEL_ENCOMSP_SERVER_MAIN_H + +#include +#include +#include + +#include + +struct _encomsp_server_private +{ + HANDLE Thread; + HANDLE StopEvent; + void* ChannelHandle; +}; + +#endif /* FREERDP_CHANNEL_ENCOMSP_SERVER_MAIN_H */ diff --git a/channels/geometry/CMakeLists.txt b/channels/geometry/CMakeLists.txt new file mode 100644 index 0000000..7ddea6d --- /dev/null +++ b/channels/geometry/CMakeLists.txt @@ -0,0 +1,22 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2017 David Fort +# +# Licensed 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. + +define_channel("geometry") + +if(WITH_CLIENT_CHANNELS) + add_channel_client(${MODULE_PREFIX} ${CHANNEL_NAME}) +endif() diff --git a/channels/geometry/ChannelOptions.cmake b/channels/geometry/ChannelOptions.cmake new file mode 100644 index 0000000..8e8163b --- /dev/null +++ b/channels/geometry/ChannelOptions.cmake @@ -0,0 +1,11 @@ + +set(OPTION_DEFAULT OFF) +set(OPTION_CLIENT_DEFAULT ON) +set(OPTION_SERVER_DEFAULT OFF) + +define_channel_options(NAME "geometry" TYPE "dynamic" + DESCRIPTION "Geometry tracking Virtual Channel Extension" + SPECIFICATIONS "[MS-RDPEGT]" + DEFAULT ${OPTION_DEFAULT}) + +define_channel_client_options(${OPTION_CLIENT_DEFAULT}) diff --git a/channels/geometry/client/CMakeLists.txt b/channels/geometry/client/CMakeLists.txt new file mode 100644 index 0000000..ac9fdc4 --- /dev/null +++ b/channels/geometry/client/CMakeLists.txt @@ -0,0 +1,40 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2017 David Fort +# +# Licensed 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. + +define_channel_client("geometry") + +set(${MODULE_PREFIX}_SRCS + geometry_main.c + geometry_main.h) + +include_directories(..) + +add_channel_client_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} TRUE "DVCPluginEntry") + +set(${MODULE_PREFIX}_LIBS ${${MODULE_PREFIX}_LIBS} winpr) +if (NOT BUILTIN_CHANNELS OR NOT BUILD_SHARED_LIBS) + set(${MODULE_PREFIX}_LIBS ${${MODULE_PREFIX}_LIBS} freerdp-client) +endif() + +target_link_libraries(${MODULE_NAME} ${${MODULE_PREFIX}_LIBS}) + + +if (WITH_DEBUG_SYMBOLS AND MSVC AND NOT BUILTIN_CHANNELS AND BUILD_SHARED_LIBS) + install(FILES ${CMAKE_BINARY_DIR}/${MODULE_NAME}.pdb DESTINATION ${FREERDP_ADDIN_PATH} COMPONENT symbols) +endif() + +set_property(TARGET ${MODULE_NAME} PROPERTY FOLDER "Channels/${CHANNEL_NAME}/Client") diff --git a/channels/geometry/client/geometry_main.c b/channels/geometry/client/geometry_main.c new file mode 100644 index 0000000..1258806 --- /dev/null +++ b/channels/geometry/client/geometry_main.c @@ -0,0 +1,501 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Geometry tracking Virtual Channel Extension + * + * Copyright 2017 David Fort + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#define TAG CHANNELS_TAG("geometry.client") + +#include "geometry_main.h" + +struct _GEOMETRY_CHANNEL_CALLBACK +{ + IWTSVirtualChannelCallback iface; + + IWTSPlugin* plugin; + IWTSVirtualChannelManager* channel_mgr; + IWTSVirtualChannel* channel; +}; +typedef struct _GEOMETRY_CHANNEL_CALLBACK GEOMETRY_CHANNEL_CALLBACK; + +struct _GEOMETRY_LISTENER_CALLBACK +{ + IWTSListenerCallback iface; + + IWTSPlugin* plugin; + IWTSVirtualChannelManager* channel_mgr; + GEOMETRY_CHANNEL_CALLBACK* channel_callback; +}; +typedef struct _GEOMETRY_LISTENER_CALLBACK GEOMETRY_LISTENER_CALLBACK; + +struct _GEOMETRY_PLUGIN +{ + IWTSPlugin iface; + + IWTSListener* listener; + GEOMETRY_LISTENER_CALLBACK* listener_callback; + + GeometryClientContext* context; + BOOL initialized; +}; +typedef struct _GEOMETRY_PLUGIN GEOMETRY_PLUGIN; + +static UINT32 mappedGeometryHash(UINT64* g) +{ + return (UINT32)((*g >> 32) + (*g & 0xffffffff)); +} + +static BOOL mappedGeometryKeyCompare(UINT64* g1, UINT64* g2) +{ + return *g1 == *g2; +} + +static void freerdp_rgndata_reset(FREERDP_RGNDATA* data) +{ + data->nRectCount = 0; +} + +static UINT32 geometry_read_RGNDATA(wStream* s, UINT32 len, FREERDP_RGNDATA* rgndata) +{ + UINT32 dwSize, iType; + INT32 right, bottom; + INT32 x, y, w, h; + + if (len < 32) + { + WLog_ERR(TAG, "invalid RGNDATA"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT32(s, dwSize); + + if (dwSize != 32) + { + WLog_ERR(TAG, "invalid RGNDATA dwSize"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT32(s, iType); + + if (iType != RDH_RECTANGLE) + { + WLog_ERR(TAG, "iType %" PRIu32 " for RGNDATA is not supported", iType); + return ERROR_UNSUPPORTED_TYPE; + } + + Stream_Read_UINT32(s, rgndata->nRectCount); + Stream_Seek_UINT32(s); /* nRgnSize IGNORED */ + Stream_Read_INT32(s, x); + Stream_Read_INT32(s, y); + Stream_Read_INT32(s, right); + Stream_Read_INT32(s, bottom); + if ((abs(x) > INT16_MAX) || (abs(y) > INT16_MAX)) + return ERROR_INVALID_DATA; + w = right - x; + h = bottom - y; + if ((abs(w) > INT16_MAX) || (abs(h) > INT16_MAX)) + return ERROR_INVALID_DATA; + rgndata->boundingRect.x = (INT16)x; + rgndata->boundingRect.y = (INT16)y; + rgndata->boundingRect.width = (INT16)w; + rgndata->boundingRect.height = (INT16)h; + len -= 32; + + if (len / (4 * 4) < rgndata->nRectCount) + { + WLog_ERR(TAG, "not enough data for region rectangles"); + } + + if (rgndata->nRectCount) + { + UINT32 i; + RDP_RECT* tmp = realloc(rgndata->rects, rgndata->nRectCount * sizeof(RDP_RECT)); + + if (!tmp) + { + WLog_ERR(TAG, "unable to allocate memory for %" PRIu32 " RECTs", rgndata->nRectCount); + return CHANNEL_RC_NO_MEMORY; + } + rgndata->rects = tmp; + + for (i = 0; i < rgndata->nRectCount; i++) + { + Stream_Read_INT32(s, x); + Stream_Read_INT32(s, y); + Stream_Read_INT32(s, right); + Stream_Read_INT32(s, bottom); + if ((abs(x) > INT16_MAX) || (abs(y) > INT16_MAX)) + return ERROR_INVALID_DATA; + w = right - x; + h = bottom - y; + if ((abs(w) > INT16_MAX) || (abs(h) > INT16_MAX)) + return ERROR_INVALID_DATA; + rgndata->rects[i].x = (INT16)x; + rgndata->rects[i].y = (INT16)y; + rgndata->rects[i].width = (INT16)w; + rgndata->rects[i].height = (INT16)h; + } + } + + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT geometry_recv_pdu(GEOMETRY_CHANNEL_CALLBACK* callback, wStream* s) +{ + UINT32 length, cbGeometryBuffer; + MAPPED_GEOMETRY* mappedGeometry; + GEOMETRY_PLUGIN* geometry; + GeometryClientContext* context; + UINT ret = CHANNEL_RC_OK; + UINT32 version, updateType, geometryType; + UINT64 id; + + geometry = (GEOMETRY_PLUGIN*)callback->plugin; + context = (GeometryClientContext*)geometry->iface.pInterface; + + if (Stream_GetRemainingLength(s) < 4) + { + WLog_ERR(TAG, "not enough remaining data"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT32(s, length); /* Length (4 bytes) */ + + if (length < 73 || Stream_GetRemainingLength(s) < (length - 4)) + { + WLog_ERR(TAG, "invalid packet length"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT32(s, version); + Stream_Read_UINT64(s, id); + Stream_Read_UINT32(s, updateType); + Stream_Seek_UINT32(s); /* flags */ + + mappedGeometry = HashTable_GetItemValue(context->geometries, &id); + + if (updateType == GEOMETRY_CLEAR) + { + if (!mappedGeometry) + { + WLog_ERR(TAG, "geometry 0x%" PRIx64 " not found here, ignoring clear command", id); + return CHANNEL_RC_OK; + } + + WLog_DBG(TAG, "clearing geometry 0x%" PRIx64 "", id); + + if (mappedGeometry->MappedGeometryClear && + !mappedGeometry->MappedGeometryClear(mappedGeometry)) + return ERROR_INTERNAL_ERROR; + + if (!HashTable_Remove(context->geometries, &id)) + WLog_ERR(TAG, "geometry not removed from geometries"); + } + else if (updateType == GEOMETRY_UPDATE) + { + BOOL newOne = FALSE; + + if (!mappedGeometry) + { + newOne = TRUE; + WLog_DBG(TAG, "creating geometry 0x%" PRIx64 "", id); + mappedGeometry = calloc(1, sizeof(MAPPED_GEOMETRY)); + if (!mappedGeometry) + return CHANNEL_RC_NO_MEMORY; + + mappedGeometry->refCounter = 1; + mappedGeometry->mappingId = id; + + if (HashTable_Add(context->geometries, &(mappedGeometry->mappingId), mappedGeometry) < + 0) + { + WLog_ERR(TAG, "unable to register geometry 0x%" PRIx64 " in the table", id); + free(mappedGeometry); + return CHANNEL_RC_NO_MEMORY; + } + } + else + { + WLog_DBG(TAG, "updating geometry 0x%" PRIx64 "", id); + } + + Stream_Read_UINT64(s, mappedGeometry->topLevelId); + + Stream_Read_INT32(s, mappedGeometry->left); + Stream_Read_INT32(s, mappedGeometry->top); + Stream_Read_INT32(s, mappedGeometry->right); + Stream_Read_INT32(s, mappedGeometry->bottom); + + Stream_Read_INT32(s, mappedGeometry->topLevelLeft); + Stream_Read_INT32(s, mappedGeometry->topLevelTop); + Stream_Read_INT32(s, mappedGeometry->topLevelRight); + Stream_Read_INT32(s, mappedGeometry->topLevelBottom); + + Stream_Read_UINT32(s, geometryType); + + Stream_Read_UINT32(s, cbGeometryBuffer); + if (Stream_GetRemainingLength(s) < cbGeometryBuffer) + { + WLog_ERR(TAG, "invalid packet length"); + return ERROR_INVALID_DATA; + } + + if (cbGeometryBuffer) + { + ret = geometry_read_RGNDATA(s, cbGeometryBuffer, &mappedGeometry->geometry); + if (ret != CHANNEL_RC_OK) + return ret; + } + else + { + freerdp_rgndata_reset(&mappedGeometry->geometry); + } + + if (newOne) + { + if (context->MappedGeometryAdded && + !context->MappedGeometryAdded(context, mappedGeometry)) + { + WLog_ERR(TAG, "geometry added callback failed"); + ret = ERROR_INTERNAL_ERROR; + } + } + else + { + if (mappedGeometry->MappedGeometryUpdate && + !mappedGeometry->MappedGeometryUpdate(mappedGeometry)) + { + WLog_ERR(TAG, "geometry update callback failed"); + ret = ERROR_INTERNAL_ERROR; + } + } + } + else + { + WLog_ERR(TAG, "unknown updateType=%" PRIu32 "", updateType); + ret = CHANNEL_RC_OK; + } + + return ret; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT geometry_on_data_received(IWTSVirtualChannelCallback* pChannelCallback, wStream* data) +{ + GEOMETRY_CHANNEL_CALLBACK* callback = (GEOMETRY_CHANNEL_CALLBACK*)pChannelCallback; + return geometry_recv_pdu(callback, data); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT geometry_on_close(IWTSVirtualChannelCallback* pChannelCallback) +{ + free(pChannelCallback); + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT geometry_on_new_channel_connection(IWTSListenerCallback* pListenerCallback, + IWTSVirtualChannel* pChannel, BYTE* Data, + BOOL* pbAccept, + IWTSVirtualChannelCallback** ppCallback) +{ + GEOMETRY_CHANNEL_CALLBACK* callback; + GEOMETRY_LISTENER_CALLBACK* listener_callback = (GEOMETRY_LISTENER_CALLBACK*)pListenerCallback; + + WINPR_UNUSED(Data); + WINPR_UNUSED(pbAccept); + + callback = (GEOMETRY_CHANNEL_CALLBACK*)calloc(1, sizeof(GEOMETRY_CHANNEL_CALLBACK)); + + if (!callback) + { + WLog_ERR(TAG, "calloc failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + callback->iface.OnDataReceived = geometry_on_data_received; + callback->iface.OnClose = geometry_on_close; + callback->plugin = listener_callback->plugin; + callback->channel_mgr = listener_callback->channel_mgr; + callback->channel = pChannel; + listener_callback->channel_callback = callback; + *ppCallback = (IWTSVirtualChannelCallback*)callback; + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT geometry_plugin_initialize(IWTSPlugin* pPlugin, IWTSVirtualChannelManager* pChannelMgr) +{ + UINT status; + GEOMETRY_PLUGIN* geometry = (GEOMETRY_PLUGIN*)pPlugin; + if (geometry->initialized) + { + WLog_ERR(TAG, "[%s] channel initialized twice, aborting", GEOMETRY_DVC_CHANNEL_NAME); + return ERROR_INVALID_DATA; + } + geometry->listener_callback = + (GEOMETRY_LISTENER_CALLBACK*)calloc(1, sizeof(GEOMETRY_LISTENER_CALLBACK)); + + if (!geometry->listener_callback) + { + WLog_ERR(TAG, "calloc failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + geometry->listener_callback->iface.OnNewChannelConnection = geometry_on_new_channel_connection; + geometry->listener_callback->plugin = pPlugin; + geometry->listener_callback->channel_mgr = pChannelMgr; + status = + pChannelMgr->CreateListener(pChannelMgr, GEOMETRY_DVC_CHANNEL_NAME, 0, + &geometry->listener_callback->iface, &(geometry->listener)); + geometry->listener->pInterface = geometry->iface.pInterface; + + geometry->initialized = status == CHANNEL_RC_OK; + return status; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT geometry_plugin_terminated(IWTSPlugin* pPlugin) +{ + GEOMETRY_PLUGIN* geometry = (GEOMETRY_PLUGIN*)pPlugin; + GeometryClientContext* context = (GeometryClientContext*)geometry->iface.pInterface; + + if (geometry && geometry->listener_callback) + { + IWTSVirtualChannelManager* mgr = geometry->listener_callback->channel_mgr; + if (mgr) + IFCALL(mgr->DestroyListener, mgr, geometry->listener); + } + + if (context) + HashTable_Free(context->geometries); + + free(geometry->listener_callback); + free(geometry->iface.pInterface); + free(pPlugin); + return CHANNEL_RC_OK; +} + +/** + * Channel Client Interface + */ + +#ifdef BUILTIN_CHANNELS +#define DVCPluginEntry geometry_DVCPluginEntry +#else +#define DVCPluginEntry FREERDP_API DVCPluginEntry +#endif + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT DVCPluginEntry(IDRDYNVC_ENTRY_POINTS* pEntryPoints) +{ + UINT error = CHANNEL_RC_OK; + GEOMETRY_PLUGIN* geometry; + GeometryClientContext* context; + geometry = (GEOMETRY_PLUGIN*)pEntryPoints->GetPlugin(pEntryPoints, "geometry"); + + if (!geometry) + { + geometry = (GEOMETRY_PLUGIN*)calloc(1, sizeof(GEOMETRY_PLUGIN)); + + if (!geometry) + { + WLog_ERR(TAG, "calloc failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + geometry->iface.Initialize = geometry_plugin_initialize; + geometry->iface.Connected = NULL; + geometry->iface.Disconnected = NULL; + geometry->iface.Terminated = geometry_plugin_terminated; + context = (GeometryClientContext*)calloc(1, sizeof(GeometryClientContext)); + + if (!context) + { + WLog_ERR(TAG, "calloc failed!"); + goto error_context; + } + + context->geometries = HashTable_New(FALSE); + context->geometries->hash = (HASH_TABLE_HASH_FN)mappedGeometryHash; + context->geometries->keyCompare = (HASH_TABLE_KEY_COMPARE_FN)mappedGeometryKeyCompare; + context->geometries->valueFree = (HASH_TABLE_VALUE_FREE_FN)mappedGeometryUnref; + + context->handle = (void*)geometry; + geometry->iface.pInterface = (void*)context; + geometry->context = context; + error = pEntryPoints->RegisterPlugin(pEntryPoints, "geometry", (IWTSPlugin*)geometry); + } + else + { + WLog_ERR(TAG, "could not get geometry Plugin."); + return CHANNEL_RC_BAD_CHANNEL; + } + + return error; + +error_context: + free(geometry); + return CHANNEL_RC_NO_MEMORY; +} diff --git a/channels/geometry/client/geometry_main.h b/channels/geometry/client/geometry_main.h new file mode 100644 index 0000000..ff6add2 --- /dev/null +++ b/channels/geometry/client/geometry_main.h @@ -0,0 +1,32 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Geometry tracking virtual channel extension + * + * Copyright 2017 David Fort + * + * Licensed 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 FREERDP_CHANNEL_GEOMETRY_CLIENT_MAIN_H +#define FREERDP_CHANNEL_GEOMETRY_CLIENT_MAIN_H + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include +#include + +#endif /* FREERDP_CHANNEL_GEOMETRY_CLIENT_MAIN_H */ diff --git a/channels/parallel/CMakeLists.txt b/channels/parallel/CMakeLists.txt new file mode 100644 index 0000000..0faabb5 --- /dev/null +++ b/channels/parallel/CMakeLists.txt @@ -0,0 +1,22 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2012 Marc-Andre Moreau +# +# Licensed 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. + +define_channel("parallel") + +if(WITH_CLIENT_CHANNELS) + add_channel_client(${MODULE_PREFIX} ${CHANNEL_NAME}) +endif() diff --git a/channels/parallel/ChannelOptions.cmake b/channels/parallel/ChannelOptions.cmake new file mode 100644 index 0000000..42a8669 --- /dev/null +++ b/channels/parallel/ChannelOptions.cmake @@ -0,0 +1,23 @@ + +set(OPTION_DEFAULT OFF) +set(OPTION_CLIENT_DEFAULT ON) +set(OPTION_SERVER_DEFAULT OFF) + +if(WIN32) + set(OPTION_CLIENT_DEFAULT OFF) + set(OPTION_SERVER_DEFAULT OFF) +endif() + +if(ANDROID) + set(OPTION_CLIENT_DEFAULT OFF) + set(OPTION_SERVER_DEFAULT OFF) +endif() + +define_channel_options(NAME "parallel" TYPE "device" + DESCRIPTION "Parallel Port Virtual Channel Extension" + SPECIFICATIONS "[MS-RDPESP]" + DEFAULT ${OPTION_DEFAULT}) + +define_channel_client_options(${OPTION_CLIENT_DEFAULT}) +define_channel_server_options(${OPTION_SERVER_DEFAULT}) + diff --git a/channels/parallel/client/CMakeLists.txt b/channels/parallel/client/CMakeLists.txt new file mode 100644 index 0000000..255435b --- /dev/null +++ b/channels/parallel/client/CMakeLists.txt @@ -0,0 +1,34 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2012 Marc-Andre Moreau +# +# Licensed 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. + +define_channel_client("parallel") + +set(${MODULE_PREFIX}_SRCS + parallel_main.c) + +add_channel_client_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} TRUE "DeviceServiceEntry") + + + +target_link_libraries(${MODULE_NAME} freerdp winpr) + + +if (WITH_DEBUG_SYMBOLS AND MSVC AND NOT BUILTIN_CHANNELS AND BUILD_SHARED_LIBS) + install(FILES ${CMAKE_BINARY_DIR}/${MODULE_NAME}.pdb DESTINATION ${FREERDP_ADDIN_PATH} COMPONENT symbols) +endif() + +set_property(TARGET ${MODULE_NAME} PROPERTY FOLDER "Channels/${CHANNEL_NAME}/Client") diff --git a/channels/parallel/client/parallel_main.c b/channels/parallel/client/parallel_main.c new file mode 100644 index 0000000..993605a --- /dev/null +++ b/channels/parallel/client/parallel_main.c @@ -0,0 +1,497 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Redirected Parallel Port Device Service + * + * Copyright 2010 O.S. Systems Software Ltda. + * Copyright 2010 Eduardo Fiss Beloni + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include + +#ifdef HAVE_UNISTD_H +#include +#endif + +#include +#include + +#ifndef _WIN32 +#include +#include +#include +#endif + +#ifdef __LINUX__ +#include +#include +#endif + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#define TAG CHANNELS_TAG("drive.client") + +struct _PARALLEL_DEVICE +{ + DEVICE device; + + int file; + char* path; + UINT32 id; + + HANDLE thread; + wMessageQueue* queue; + rdpContext* rdpcontext; +}; +typedef struct _PARALLEL_DEVICE PARALLEL_DEVICE; + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT parallel_process_irp_create(PARALLEL_DEVICE* parallel, IRP* irp) +{ + char* path = NULL; + int status; + WCHAR* ptr; + UINT32 PathLength; + if (!Stream_SafeSeek(irp->input, 28)) + return ERROR_INVALID_DATA; + /* DesiredAccess(4) AllocationSize(8), FileAttributes(4) */ + /* SharedAccess(4) CreateDisposition(4), CreateOptions(4) */ + if (Stream_GetRemainingLength(irp->input) < 4) + return ERROR_INVALID_DATA; + Stream_Read_UINT32(irp->input, PathLength); + ptr = (WCHAR*)Stream_Pointer(irp->input); + if (!Stream_SafeSeek(irp->input, PathLength)) + return ERROR_INVALID_DATA; + status = ConvertFromUnicode(CP_UTF8, 0, ptr, PathLength / 2, &path, 0, NULL, NULL); + + if (status < 1) + if (!(path = (char*)calloc(1, 1))) + { + WLog_ERR(TAG, "calloc failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + parallel->id = irp->devman->id_sequence++; + parallel->file = open(parallel->path, O_RDWR); + + if (parallel->file < 0) + { + irp->IoStatus = STATUS_ACCESS_DENIED; + parallel->id = 0; + } + else + { + /* all read and write operations should be non-blocking */ + if (fcntl(parallel->file, F_SETFL, O_NONBLOCK) == -1) + { + } + } + + Stream_Write_UINT32(irp->output, parallel->id); + Stream_Write_UINT8(irp->output, 0); + free(path); + return irp->Complete(irp); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT parallel_process_irp_close(PARALLEL_DEVICE* parallel, IRP* irp) +{ + if (close(parallel->file) < 0) + { + } + else + { + } + + Stream_Zero(irp->output, 5); /* Padding(5) */ + return irp->Complete(irp); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT parallel_process_irp_read(PARALLEL_DEVICE* parallel, IRP* irp) +{ + UINT32 Length; + UINT64 Offset; + ssize_t status; + BYTE* buffer = NULL; + if (Stream_GetRemainingLength(irp->input) < 12) + return ERROR_INVALID_DATA; + Stream_Read_UINT32(irp->input, Length); + Stream_Read_UINT64(irp->input, Offset); + buffer = (BYTE*)calloc(Length, sizeof(BYTE)); + + if (!buffer) + { + WLog_ERR(TAG, "malloc failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + status = read(parallel->file, buffer, Length); + + if (status < 0) + { + irp->IoStatus = STATUS_UNSUCCESSFUL; + free(buffer); + buffer = NULL; + Length = 0; + } + else + { + Length = status; + } + + Stream_Write_UINT32(irp->output, Length); + + if (Length > 0) + { + if (!Stream_EnsureRemainingCapacity(irp->output, Length)) + { + WLog_ERR(TAG, "Stream_EnsureRemainingCapacity failed!"); + free(buffer); + return CHANNEL_RC_NO_MEMORY; + } + + Stream_Write(irp->output, buffer, Length); + } + + free(buffer); + return irp->Complete(irp); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT parallel_process_irp_write(PARALLEL_DEVICE* parallel, IRP* irp) +{ + UINT32 len; + UINT32 Length; + UINT64 Offset; + ssize_t status; + void* ptr; + if (Stream_GetRemainingLength(irp->input) > 12) + return ERROR_INVALID_DATA; + + Stream_Read_UINT32(irp->input, Length); + Stream_Read_UINT64(irp->input, Offset); + if (!Stream_SafeSeek(irp->input, 20)) /* Padding */ + return ERROR_INVALID_DATA; + ptr = Stream_Pointer(irp->input); + if (!Stream_SafeSeek(irp->input, Length)) + return ERROR_INVALID_DATA; + len = Length; + + while (len > 0) + { + status = write(parallel->file, ptr, len); + + if (status < 0) + { + irp->IoStatus = STATUS_UNSUCCESSFUL; + Length = 0; + break; + } + + Stream_Seek(irp->input, status); + len -= status; + } + + Stream_Write_UINT32(irp->output, Length); + Stream_Write_UINT8(irp->output, 0); /* Padding */ + return irp->Complete(irp); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT parallel_process_irp_device_control(PARALLEL_DEVICE* parallel, IRP* irp) +{ + Stream_Write_UINT32(irp->output, 0); /* OutputBufferLength */ + return irp->Complete(irp); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT parallel_process_irp(PARALLEL_DEVICE* parallel, IRP* irp) +{ + UINT error; + + switch (irp->MajorFunction) + { + case IRP_MJ_CREATE: + if ((error = parallel_process_irp_create(parallel, irp))) + { + WLog_ERR(TAG, "parallel_process_irp_create failed with error %" PRIu32 "!", error); + return error; + } + + break; + + case IRP_MJ_CLOSE: + if ((error = parallel_process_irp_close(parallel, irp))) + { + WLog_ERR(TAG, "parallel_process_irp_close failed with error %" PRIu32 "!", error); + return error; + } + + break; + + case IRP_MJ_READ: + if ((error = parallel_process_irp_read(parallel, irp))) + { + WLog_ERR(TAG, "parallel_process_irp_read failed with error %" PRIu32 "!", error); + return error; + } + + break; + + case IRP_MJ_WRITE: + if ((error = parallel_process_irp_write(parallel, irp))) + { + WLog_ERR(TAG, "parallel_process_irp_write failed with error %" PRIu32 "!", error); + return error; + } + + break; + + case IRP_MJ_DEVICE_CONTROL: + if ((error = parallel_process_irp_device_control(parallel, irp))) + { + WLog_ERR(TAG, "parallel_process_irp_device_control failed with error %" PRIu32 "!", + error); + return error; + } + + break; + + default: + irp->IoStatus = STATUS_NOT_SUPPORTED; + return irp->Complete(irp); + break; + } + + return CHANNEL_RC_OK; +} + +static DWORD WINAPI parallel_thread_func(LPVOID arg) +{ + IRP* irp; + wMessage message; + PARALLEL_DEVICE* parallel = (PARALLEL_DEVICE*)arg; + UINT error = CHANNEL_RC_OK; + + while (1) + { + if (!MessageQueue_Wait(parallel->queue)) + { + WLog_ERR(TAG, "MessageQueue_Wait failed!"); + error = ERROR_INTERNAL_ERROR; + break; + } + + if (!MessageQueue_Peek(parallel->queue, &message, TRUE)) + { + WLog_ERR(TAG, "MessageQueue_Peek failed!"); + error = ERROR_INTERNAL_ERROR; + break; + } + + if (message.id == WMQ_QUIT) + break; + + irp = (IRP*)message.wParam; + + if ((error = parallel_process_irp(parallel, irp))) + { + WLog_ERR(TAG, "parallel_process_irp failed with error %" PRIu32 "!", error); + break; + } + } + + if (error && parallel->rdpcontext) + setChannelError(parallel->rdpcontext, error, "parallel_thread_func reported an error"); + + ExitThread(error); + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT parallel_irp_request(DEVICE* device, IRP* irp) +{ + PARALLEL_DEVICE* parallel = (PARALLEL_DEVICE*)device; + + if (!MessageQueue_Post(parallel->queue, NULL, 0, (void*)irp, NULL)) + { + WLog_ERR(TAG, "MessageQueue_Post failed!"); + return ERROR_INTERNAL_ERROR; + } + + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT parallel_free(DEVICE* device) +{ + UINT error; + PARALLEL_DEVICE* parallel = (PARALLEL_DEVICE*)device; + + if (!MessageQueue_PostQuit(parallel->queue, 0) || + (WaitForSingleObject(parallel->thread, INFINITE) == WAIT_FAILED)) + { + error = GetLastError(); + WLog_ERR(TAG, "WaitForSingleObject failed with error %" PRIu32 "!", error); + return error; + } + + CloseHandle(parallel->thread); + Stream_Free(parallel->device.data, TRUE); + MessageQueue_Free(parallel->queue); + free(parallel); + return CHANNEL_RC_OK; +} + +#ifdef BUILTIN_CHANNELS +#define DeviceServiceEntry parallel_DeviceServiceEntry +#else +#define DeviceServiceEntry FREERDP_API DeviceServiceEntry +#endif + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT DeviceServiceEntry(PDEVICE_SERVICE_ENTRY_POINTS pEntryPoints) +{ + char* name; + char* path; + size_t i; + size_t length; + RDPDR_PARALLEL* device; + PARALLEL_DEVICE* parallel; + UINT error; + device = (RDPDR_PARALLEL*)pEntryPoints->device; + name = device->Name; + path = device->Path; + + if (!name || (name[0] == '*') || !path) + { + /* TODO: implement auto detection of parallel ports */ + return CHANNEL_RC_INITIALIZATION_ERROR; + } + + if (name[0] && path[0]) + { + parallel = (PARALLEL_DEVICE*)calloc(1, sizeof(PARALLEL_DEVICE)); + + if (!parallel) + { + WLog_ERR(TAG, "calloc failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + parallel->device.type = RDPDR_DTYP_PARALLEL; + parallel->device.name = name; + parallel->device.IRPRequest = parallel_irp_request; + parallel->device.Free = parallel_free; + parallel->rdpcontext = pEntryPoints->rdpcontext; + length = strlen(name); + parallel->device.data = Stream_New(NULL, length + 1); + + if (!parallel->device.data) + { + WLog_ERR(TAG, "Stream_New failed!"); + error = CHANNEL_RC_NO_MEMORY; + goto error_out; + } + + for (i = 0; i <= length; i++) + Stream_Write_UINT8(parallel->device.data, name[i] < 0 ? '_' : name[i]); + + parallel->path = path; + parallel->queue = MessageQueue_New(NULL); + + if (!parallel->queue) + { + WLog_ERR(TAG, "MessageQueue_New failed!"); + error = CHANNEL_RC_NO_MEMORY; + goto error_out; + } + + if ((error = pEntryPoints->RegisterDevice(pEntryPoints->devman, (DEVICE*)parallel))) + { + WLog_ERR(TAG, "RegisterDevice failed with error %" PRIu32 "!", error); + goto error_out; + } + + if (!(parallel->thread = + CreateThread(NULL, 0, parallel_thread_func, (void*)parallel, 0, NULL))) + { + WLog_ERR(TAG, "CreateThread failed!"); + error = ERROR_INTERNAL_ERROR; + goto error_out; + } + } + + return CHANNEL_RC_OK; +error_out: + MessageQueue_Free(parallel->queue); + Stream_Free(parallel->device.data, TRUE); + free(parallel); + return error; +} diff --git a/channels/printer/CMakeLists.txt b/channels/printer/CMakeLists.txt new file mode 100644 index 0000000..73cb415 --- /dev/null +++ b/channels/printer/CMakeLists.txt @@ -0,0 +1,22 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2012 Marc-Andre Moreau +# +# Licensed 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. + +define_channel("printer") + +if(WITH_CLIENT_CHANNELS) + add_channel_client(${MODULE_PREFIX} ${CHANNEL_NAME}) +endif() \ No newline at end of file diff --git a/channels/printer/ChannelOptions.cmake b/channels/printer/ChannelOptions.cmake new file mode 100644 index 0000000..86dad03 --- /dev/null +++ b/channels/printer/ChannelOptions.cmake @@ -0,0 +1,24 @@ + +set(OPTION_DEFAULT OFF) +set(OPTION_CLIENT_DEFAULT ON) +set(OPTION_SERVER_DEFAULT OFF) + +if(WIN32) + set(OPTION_CLIENT_DEFAULT ON) + set(OPTION_SERVER_DEFAULT OFF) +elseif(WITH_CUPS) + set(OPTION_CLIENT_DEFAULT ON) + set(OPTION_SERVER_DEFAULT OFF) +else() + set(OPTION_CLIENT_DEFAULT OFF) + set(OPTION_SERVER_DEFAULT OFF) +endif() + +define_channel_options(NAME "printer" TYPE "device" + DESCRIPTION "Print Virtual Channel Extension" + SPECIFICATIONS "[MS-RDPEPC]" + DEFAULT ${OPTION_DEFAULT}) + +define_channel_client_options(${OPTION_CLIENT_DEFAULT}) +define_channel_server_options(${OPTION_SERVER_DEFAULT}) + diff --git a/channels/printer/client/CMakeLists.txt b/channels/printer/client/CMakeLists.txt new file mode 100644 index 0000000..58d44f9 --- /dev/null +++ b/channels/printer/client/CMakeLists.txt @@ -0,0 +1,40 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2012 Marc-Andre Moreau +# +# Licensed 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. + +define_channel_client("printer") + +set(${MODULE_PREFIX}_SRCS + printer_main.c) + +add_channel_client_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} TRUE "DeviceServiceEntry") + +set(${MODULE_PREFIX}_LIBS ${${MODULE_PREFIX}_LIBS} winpr freerdp) +target_link_libraries(${MODULE_NAME} ${${MODULE_PREFIX}_LIBS}) + +if (WITH_DEBUG_SYMBOLS AND MSVC AND NOT BUILTIN_CHANNELS AND BUILD_SHARED_LIBS) + install(FILES ${CMAKE_BINARY_DIR}/${MODULE_NAME}.pdb DESTINATION ${FREERDP_ADDIN_PATH} COMPONENT symbols) +endif() + +set_property(TARGET ${MODULE_NAME} PROPERTY FOLDER "Channels/${CHANNEL_NAME}/Client") + +if(WITH_CUPS) + add_channel_client_subsystem(${MODULE_PREFIX} ${CHANNEL_NAME} "cups" "") +endif() + +if(WIN32 AND NOT UWP) + add_channel_client_subsystem(${MODULE_PREFIX} ${CHANNEL_NAME} "win" "") +endif() diff --git a/channels/printer/client/cups/CMakeLists.txt b/channels/printer/client/cups/CMakeLists.txt new file mode 100644 index 0000000..50d599e --- /dev/null +++ b/channels/printer/client/cups/CMakeLists.txt @@ -0,0 +1,31 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2019 Armin Novak +# Copyright 2019 Thincast Technologies GmbH +# +# Licensed 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. +define_channel_client_subsystem("printer" "cups" "") + +set(${MODULE_PREFIX}_SRCS + printer_cups.c) + +include_directories(..) +include_directories(${CUPS_INCLUDE_DIR}) + +add_channel_client_subsystem_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} "" TRUE "") + +set(${MODULE_PREFIX}_LIBS ${${MODULE_PREFIX}_LIBS} winpr freerdp) +set(${MODULE_PREFIX}_LIBS ${${MODULE_PREFIX}_LIBS} ${CUPS_LIBRARIES}) + +target_link_libraries(${MODULE_NAME} ${${MODULE_PREFIX}_LIBS}) diff --git a/channels/printer/client/cups/printer_cups.c b/channels/printer/client/cups/printer_cups.c new file mode 100644 index 0000000..baa46e5 --- /dev/null +++ b/channels/printer/client/cups/printer_cups.c @@ -0,0 +1,410 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Print Virtual Channel - CUPS driver + * + * Copyright 2010-2011 Vic Lee + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger + * Copyright 2016 Armin Novak + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include +#include + +#include +#include + +#include +#include +#include + +#include + +#include + +typedef struct rdp_cups_printer_driver rdpCupsPrinterDriver; +typedef struct rdp_cups_printer rdpCupsPrinter; +typedef struct rdp_cups_print_job rdpCupsPrintJob; + +struct rdp_cups_printer_driver +{ + rdpPrinterDriver driver; + + int id_sequence; + size_t references; +}; + +struct rdp_cups_printer +{ + rdpPrinter printer; + + rdpCupsPrintJob* printjob; +}; + +struct rdp_cups_print_job +{ + rdpPrintJob printjob; + + void* printjob_object; + int printjob_id; +}; + +static void printer_cups_get_printjob_name(char* buf, size_t size, size_t id) +{ + time_t tt; + struct tm tres; + struct tm* t; + + tt = time(NULL); + t = localtime_r(&tt, &tres); + sprintf_s(buf, size - 1, "FreeRDP Print %04d-%02d-%02d %02d-%02d-%02d - Job %" PRIdz, + t->tm_year + 1900, t->tm_mon + 1, t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec, id); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT printer_cups_write_printjob(rdpPrintJob* printjob, const BYTE* data, size_t size) +{ + rdpCupsPrintJob* cups_printjob = (rdpCupsPrintJob*)printjob; + +#ifndef _CUPS_API_1_4 + + { + FILE* fp; + + fp = winpr_fopen((const char*)cups_printjob->printjob_object, "a+b"); + + if (!fp) + return ERROR_INTERNAL_ERROR; + + if (fwrite(data, 1, size, fp) < size) + { + fclose(fp); + return ERROR_INTERNAL_ERROR; + // FIXME once this function doesn't return void anymore! + } + + fclose(fp); + } + +#else + + cupsWriteRequestData((http_t*)cups_printjob->printjob_object, (const char*)data, size); + +#endif + + return CHANNEL_RC_OK; +} + +static void printer_cups_close_printjob(rdpPrintJob* printjob) +{ + rdpCupsPrintJob* cups_printjob = (rdpCupsPrintJob*)printjob; + +#ifndef _CUPS_API_1_4 + + { + char buf[100]; + + printer_cups_get_printjob_name(buf, sizeof(buf), printjob->id); + + if (cupsPrintFile(printjob->printer->name, (const char*)cups_printjob->printjob_object, buf, + 0, NULL) == 0) + { + } + + unlink(cups_printjob->printjob_object); + free(cups_printjob->printjob_object); + } + +#else + + cupsFinishDocument((http_t*)cups_printjob->printjob_object, printjob->printer->name); + cups_printjob->printjob_id = 0; + httpClose((http_t*)cups_printjob->printjob_object); + +#endif + + ((rdpCupsPrinter*)printjob->printer)->printjob = NULL; + free(cups_printjob); +} + +static rdpPrintJob* printer_cups_create_printjob(rdpPrinter* printer, UINT32 id) +{ + rdpCupsPrinter* cups_printer = (rdpCupsPrinter*)printer; + rdpCupsPrintJob* cups_printjob; + + if (cups_printer->printjob != NULL) + return NULL; + + cups_printjob = (rdpCupsPrintJob*)calloc(1, sizeof(rdpCupsPrintJob)); + if (!cups_printjob) + return NULL; + + cups_printjob->printjob.id = id; + cups_printjob->printjob.printer = printer; + + cups_printjob->printjob.Write = printer_cups_write_printjob; + cups_printjob->printjob.Close = printer_cups_close_printjob; + +#ifndef _CUPS_API_1_4 + + cups_printjob->printjob_object = _strdup(tmpnam(NULL)); + if (!cups_printjob->printjob_object) + { + free(cups_printjob); + return NULL; + } + +#else + { + char buf[100]; + +#if !defined(_CUPS_API_1_7) + cups_printjob->printjob_object = + httpConnectEncrypt(cupsServer(), ippPort(), HTTP_ENCRYPT_IF_REQUESTED); +#else + cups_printjob->printjob_object = httpConnect2(cupsServer(), ippPort(), NULL, AF_UNSPEC, + HTTP_ENCRYPT_IF_REQUESTED, 1, 10000, NULL); +#endif + if (!cups_printjob->printjob_object) + { + free(cups_printjob); + return NULL; + } + + printer_cups_get_printjob_name(buf, sizeof(buf), cups_printjob->printjob.id); + + cups_printjob->printjob_id = + cupsCreateJob((http_t*)cups_printjob->printjob_object, printer->name, buf, 0, NULL); + + if (!cups_printjob->printjob_id) + { + httpClose((http_t*)cups_printjob->printjob_object); + free(cups_printjob); + return NULL; + } + + cupsStartDocument((http_t*)cups_printjob->printjob_object, printer->name, + cups_printjob->printjob_id, buf, CUPS_FORMAT_AUTO, 1); + } + +#endif + + cups_printer->printjob = cups_printjob; + + return (rdpPrintJob*)cups_printjob; +} + +static rdpPrintJob* printer_cups_find_printjob(rdpPrinter* printer, UINT32 id) +{ + rdpCupsPrinter* cups_printer = (rdpCupsPrinter*)printer; + + if (cups_printer->printjob == NULL) + return NULL; + if (cups_printer->printjob->printjob.id != id) + return NULL; + + return (rdpPrintJob*)cups_printer->printjob; +} + +static void printer_cups_free_printer(rdpPrinter* printer) +{ + rdpCupsPrinter* cups_printer = (rdpCupsPrinter*)printer; + + if (cups_printer->printjob) + cups_printer->printjob->printjob.Close((rdpPrintJob*)cups_printer->printjob); + + if (printer->backend) + printer->backend->ReleaseRef(printer->backend); + free(printer->name); + free(printer->driver); + free(printer); +} + +static void printer_cups_add_ref_printer(rdpPrinter* printer) +{ + if (printer) + printer->references++; +} + +static void printer_cups_release_ref_printer(rdpPrinter* printer) +{ + if (!printer) + return; + if (printer->references <= 1) + printer_cups_free_printer(printer); + else + printer->references--; +} + +static rdpPrinter* printer_cups_new_printer(rdpCupsPrinterDriver* cups_driver, const char* name, + const char* driverName, BOOL is_default) +{ + rdpCupsPrinter* cups_printer; + + cups_printer = (rdpCupsPrinter*)calloc(1, sizeof(rdpCupsPrinter)); + if (!cups_printer) + return NULL; + + cups_printer->printer.backend = &cups_driver->driver; + + cups_printer->printer.id = cups_driver->id_sequence++; + cups_printer->printer.name = _strdup(name); + if (!cups_printer->printer.name) + { + free(cups_printer); + return NULL; + } + + if (driverName) + cups_printer->printer.driver = _strdup(driverName); + else + cups_printer->printer.driver = _strdup("MS Publisher Imagesetter"); + if (!cups_printer->printer.driver) + { + free(cups_printer->printer.name); + free(cups_printer); + return NULL; + } + cups_printer->printer.is_default = is_default; + + cups_printer->printer.CreatePrintJob = printer_cups_create_printjob; + cups_printer->printer.FindPrintJob = printer_cups_find_printjob; + cups_printer->printer.AddRef = printer_cups_add_ref_printer; + cups_printer->printer.ReleaseRef = printer_cups_release_ref_printer; + + cups_printer->printer.AddRef(&cups_printer->printer); + cups_printer->printer.backend->AddRef(cups_printer->printer.backend); + return &cups_printer->printer; +} + +static void printer_cups_release_enum_printers(rdpPrinter** printers) +{ + rdpPrinter** cur = printers; + + while ((cur != NULL) && ((*cur) != NULL)) + { + if ((*cur)->ReleaseRef) + (*cur)->ReleaseRef(*cur); + cur++; + } + free(printers); +} + +static rdpPrinter** printer_cups_enum_printers(rdpPrinterDriver* driver) +{ + rdpPrinter** printers; + int num_printers; + cups_dest_t* dests; + cups_dest_t* dest; + int num_dests; + int i; + + num_dests = cupsGetDests(&dests); + printers = (rdpPrinter**)calloc(num_dests + 1, sizeof(rdpPrinter*)); + if (!printers) + return NULL; + + num_printers = 0; + + for (i = 0, dest = dests; i < num_dests; i++, dest++) + { + if (dest->instance == NULL) + { + rdpPrinter* current = printer_cups_new_printer((rdpCupsPrinterDriver*)driver, + dest->name, NULL, dest->is_default); + if (!current) + { + printer_cups_release_enum_printers(printers); + printers = NULL; + break; + } + + printers[num_printers++] = current; + } + } + cupsFreeDests(num_dests, dests); + + return printers; +} + +static rdpPrinter* printer_cups_get_printer(rdpPrinterDriver* driver, const char* name, + const char* driverName) +{ + rdpCupsPrinterDriver* cups_driver = (rdpCupsPrinterDriver*)driver; + + return printer_cups_new_printer(cups_driver, name, driverName, + cups_driver->id_sequence == 1 ? TRUE : FALSE); +} + +static void printer_cups_add_ref_driver(rdpPrinterDriver* driver) +{ + rdpCupsPrinterDriver* cups_driver = (rdpCupsPrinterDriver*)driver; + if (cups_driver) + cups_driver->references++; +} + +/* Singleton */ +static rdpCupsPrinterDriver* uniq_cups_driver = NULL; + +static void printer_cups_release_ref_driver(rdpPrinterDriver* driver) +{ + rdpCupsPrinterDriver* cups_driver = (rdpCupsPrinterDriver*)driver; + if (cups_driver->references <= 1) + { + if (uniq_cups_driver == cups_driver) + uniq_cups_driver = NULL; + free(cups_driver); + cups_driver = NULL; + } + else + cups_driver->references--; +} + +#ifdef BUILTIN_CHANNELS +rdpPrinterDriver* cups_freerdp_printer_client_subsystem_entry(void) +#else +FREERDP_API rdpPrinterDriver* freerdp_printer_client_subsystem_entry(void) +#endif +{ + if (!uniq_cups_driver) + { + uniq_cups_driver = (rdpCupsPrinterDriver*)calloc(1, sizeof(rdpCupsPrinterDriver)); + + if (!uniq_cups_driver) + return NULL; + + uniq_cups_driver->driver.EnumPrinters = printer_cups_enum_printers; + uniq_cups_driver->driver.ReleaseEnumPrinters = printer_cups_release_enum_printers; + uniq_cups_driver->driver.GetPrinter = printer_cups_get_printer; + + uniq_cups_driver->driver.AddRef = printer_cups_add_ref_driver; + uniq_cups_driver->driver.ReleaseRef = printer_cups_release_ref_driver; + + uniq_cups_driver->id_sequence = 1; + uniq_cups_driver->driver.AddRef(&uniq_cups_driver->driver); + } + + return &uniq_cups_driver->driver; +} diff --git a/channels/printer/client/printer_main.c b/channels/printer/client/printer_main.c new file mode 100644 index 0000000..8019968 --- /dev/null +++ b/channels/printer/client/printer_main.c @@ -0,0 +1,1073 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Print Virtual Channel + * + * Copyright 2010-2011 Vic Lee + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger + * Copyright 2016 Armin Novak + * Copyright 2016 David PHAM-VAN + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "../printer.h" + +#include + +#include + +#define TAG CHANNELS_TAG("printer.client") + +typedef struct _PRINTER_DEVICE PRINTER_DEVICE; +struct _PRINTER_DEVICE +{ + DEVICE device; + + rdpPrinter* printer; + + WINPR_PSLIST_HEADER pIrpList; + + HANDLE event; + HANDLE stopEvent; + + HANDLE thread; + rdpContext* rdpcontext; + char port[64]; +}; + +typedef enum +{ + PRN_CONF_PORT = 0, + PRN_CONF_PNP = 1, + PRN_CONF_DRIVER = 2, + PRN_CONF_DATA = 3 +} prn_conf_t; + +static const char* filemap[] = { "PortDosName", "PnPName", "DriverName", + "CachedPrinterConfigData" }; + +static char* get_printer_config_path(const rdpSettings* settings, const WCHAR* name, size_t length) +{ + char* dir = GetCombinedPath(settings->ConfigPath, "printers"); + char* bname = crypto_base64_encode((const BYTE*)name, (int)length); + char* config = GetCombinedPath(dir, bname); + + if (config && !winpr_PathFileExists(config)) + { + if (!winpr_PathMakePath(config, NULL)) + { + free(config); + config = NULL; + } + } + + free(dir); + free(bname); + return config; +} + +static BOOL printer_write_setting(const char* path, prn_conf_t type, const void* data, + size_t length) +{ + DWORD written = 0; + BOOL rc = FALSE; + HANDLE file; + size_t b64len; + char* base64 = NULL; + const char* name = filemap[type]; + char* abs = GetCombinedPath(path, name); + + if (!abs || (length > INT32_MAX)) + return FALSE; + + file = CreateFileA(abs, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); + free(abs); + + if (file == INVALID_HANDLE_VALUE) + return FALSE; + + if (length > 0) + { + base64 = crypto_base64_encode(data, length); + + if (!base64) + goto fail; + + /* base64 char represents 6bit -> 4*(n/3) is the length which is + * always smaller than 2*n */ + b64len = strnlen(base64, 2 * length); + rc = WriteFile(file, base64, b64len, &written, NULL); + + if (b64len != written) + rc = FALSE; + } + else + rc = TRUE; + +fail: + CloseHandle(file); + free(base64); + return rc; +} + +static BOOL printer_config_valid(const char* path) +{ + if (!path) + return FALSE; + + if (!winpr_PathFileExists(path)) + return FALSE; + + return TRUE; +} + +static BOOL printer_read_setting(const char* path, prn_conf_t type, void** data, UINT32* length) +{ + DWORD lowSize, highSize; + DWORD read = 0; + BOOL rc = FALSE; + HANDLE file; + char* fdata = NULL; + const char* name = filemap[type]; + char* abs = GetCombinedPath(path, name); + + if (!abs) + return FALSE; + + file = CreateFileA(abs, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); + free(abs); + + if (file == INVALID_HANDLE_VALUE) + return FALSE; + + lowSize = GetFileSize(file, &highSize); + + if ((lowSize == INVALID_FILE_SIZE) || (highSize != 0)) + goto fail; + + if (lowSize != 0) + { + fdata = malloc(lowSize); + + if (!fdata) + goto fail; + + rc = ReadFile(file, fdata, lowSize, &read, NULL); + + if (lowSize != read) + rc = FALSE; + } + +fail: + CloseHandle(file); + + if (rc && (lowSize <= INT_MAX)) + { + int blen = 0; + crypto_base64_decode(fdata, (int)lowSize, (BYTE**)data, &blen); + + if (*data && (blen > 0)) + *length = (UINT32)blen; + else + { + rc = FALSE; + *length = 0; + } + } + else + { + *length = 0; + *data = NULL; + } + + free(fdata); + return rc; +} + +static BOOL printer_save_to_config(const rdpSettings* settings, const char* PortDosName, + size_t PortDosNameLen, const WCHAR* PnPName, size_t PnPNameLen, + const WCHAR* DriverName, size_t DriverNameLen, + const WCHAR* PrinterName, size_t PrintNameLen, + const BYTE* CachedPrinterConfigData, size_t CacheFieldsLen) +{ + BOOL rc = FALSE; + char* path = get_printer_config_path(settings, PrinterName, PrintNameLen); + + if (!path) + goto fail; + + if (!printer_write_setting(path, PRN_CONF_PORT, PortDosName, PortDosNameLen)) + goto fail; + + if (!printer_write_setting(path, PRN_CONF_PNP, PnPName, PnPNameLen)) + goto fail; + + if (!printer_write_setting(path, PRN_CONF_DRIVER, DriverName, DriverNameLen)) + goto fail; + + if (!printer_write_setting(path, PRN_CONF_DATA, CachedPrinterConfigData, CacheFieldsLen)) + goto fail; + +fail: + free(path); + return rc; +} + +static BOOL printer_update_to_config(const rdpSettings* settings, const WCHAR* name, size_t length, + const BYTE* data, size_t datalen) +{ + BOOL rc = FALSE; + char* path = get_printer_config_path(settings, name, length); + rc = printer_write_setting(path, PRN_CONF_DATA, data, datalen); + free(path); + return rc; +} + +static BOOL printer_remove_config(const rdpSettings* settings, const WCHAR* name, size_t length) +{ + BOOL rc = FALSE; + char* path = get_printer_config_path(settings, name, length); + + if (!printer_config_valid(path)) + goto fail; + + rc = winpr_RemoveDirectory(path); +fail: + free(path); + return rc; +} + +static BOOL printer_move_config(const rdpSettings* settings, const WCHAR* oldName, size_t oldLength, + const WCHAR* newName, size_t newLength) +{ + BOOL rc = FALSE; + char* oldPath = get_printer_config_path(settings, oldName, oldLength); + char* newPath = get_printer_config_path(settings, newName, newLength); + + if (printer_config_valid(oldPath)) + rc = winpr_MoveFile(oldPath, newPath); + + free(oldPath); + free(newPath); + return rc; +} + +static BOOL printer_load_from_config(const rdpSettings* settings, rdpPrinter* printer, + PRINTER_DEVICE* printer_dev) +{ + BOOL res = FALSE; + WCHAR* wname = NULL; + size_t wlen; + char* path = NULL; + int rc; + UINT32 flags = 0; + void* DriverName = NULL; + UINT32 DriverNameLen = 0; + void* PnPName = NULL; + UINT32 PnPNameLen = 0; + void* CachedPrinterConfigData = NULL; + UINT32 CachedFieldsLen = 0; + UINT32 PrinterNameLen = 0; + + if (!settings || !printer) + return FALSE; + + rc = ConvertToUnicode(CP_UTF8, 0, printer->name, -1, &wname, 0); + + if (rc <= 0) + goto fail; + + wlen = _wcslen(wname) + 1; + path = get_printer_config_path(settings, wname, wlen * sizeof(WCHAR)); + PrinterNameLen = (wlen + 1) * sizeof(WCHAR); + + if (!path) + goto fail; + + if (printer->is_default) + flags |= RDPDR_PRINTER_ANNOUNCE_FLAG_DEFAULTPRINTER; + + if (!printer_read_setting(path, PRN_CONF_PNP, &PnPName, &PnPNameLen)) + { + } + + if (!printer_read_setting(path, PRN_CONF_DRIVER, &DriverName, &DriverNameLen)) + { + DriverNameLen = + ConvertToUnicode(CP_UTF8, 0, printer->driver, -1, (LPWSTR*)&DriverName, 0) * 2 + 1; + } + + if (!printer_read_setting(path, PRN_CONF_DATA, &CachedPrinterConfigData, &CachedFieldsLen)) + { + } + + Stream_SetPosition(printer_dev->device.data, 0); + + if (!Stream_EnsureRemainingCapacity(printer_dev->device.data, 24)) + goto fail; + + Stream_Write_UINT32(printer_dev->device.data, flags); + Stream_Write_UINT32(printer_dev->device.data, 0); /* CodePage, reserved */ + Stream_Write_UINT32(printer_dev->device.data, PnPNameLen); /* PnPNameLen */ + Stream_Write_UINT32(printer_dev->device.data, DriverNameLen); + Stream_Write_UINT32(printer_dev->device.data, PrinterNameLen); + Stream_Write_UINT32(printer_dev->device.data, CachedFieldsLen); + + if (!Stream_EnsureRemainingCapacity(printer_dev->device.data, PnPNameLen)) + goto fail; + + if (PnPNameLen > 0) + Stream_Write(printer_dev->device.data, PnPName, PnPNameLen); + + if (!Stream_EnsureRemainingCapacity(printer_dev->device.data, DriverNameLen)) + goto fail; + + Stream_Write(printer_dev->device.data, DriverName, DriverNameLen); + + if (!Stream_EnsureRemainingCapacity(printer_dev->device.data, PrinterNameLen)) + goto fail; + + Stream_Write(printer_dev->device.data, wname, PrinterNameLen); + + if (!Stream_EnsureRemainingCapacity(printer_dev->device.data, CachedFieldsLen)) + goto fail; + + Stream_Write(printer_dev->device.data, CachedPrinterConfigData, CachedFieldsLen); + res = TRUE; +fail: + free(path); + free(wname); + free(PnPName); + free(DriverName); + free(CachedPrinterConfigData); + return res; +} + +static BOOL printer_save_default_config(const rdpSettings* settings, rdpPrinter* printer) +{ + BOOL res = FALSE; + WCHAR* wname = NULL; + WCHAR* driver = NULL; + size_t wlen, dlen; + char* path = NULL; + int rc; + + if (!settings || !printer) + return FALSE; + + rc = ConvertToUnicode(CP_UTF8, 0, printer->name, -1, &wname, 0); + + if (rc <= 0) + goto fail; + + rc = ConvertToUnicode(CP_UTF8, 0, printer->driver, -1, &driver, 0); + + if (rc <= 0) + goto fail; + + wlen = _wcslen(wname) + 1; + dlen = _wcslen(driver) + 1; + path = get_printer_config_path(settings, wname, wlen * sizeof(WCHAR)); + + if (!path) + goto fail; + + if (dlen > 1) + { + if (!printer_write_setting(path, PRN_CONF_DRIVER, driver, dlen * sizeof(WCHAR))) + goto fail; + } + + res = TRUE; +fail: + free(path); + free(wname); + free(driver); + return res; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT printer_process_irp_create(PRINTER_DEVICE* printer_dev, IRP* irp) +{ + rdpPrintJob* printjob = NULL; + + if (printer_dev->printer) + printjob = + printer_dev->printer->CreatePrintJob(printer_dev->printer, irp->devman->id_sequence++); + + if (printjob) + { + Stream_Write_UINT32(irp->output, printjob->id); /* FileId */ + } + else + { + Stream_Write_UINT32(irp->output, 0); /* FileId */ + irp->IoStatus = STATUS_PRINT_QUEUE_FULL; + } + + return irp->Complete(irp); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT printer_process_irp_close(PRINTER_DEVICE* printer_dev, IRP* irp) +{ + rdpPrintJob* printjob = NULL; + + if (printer_dev->printer) + printjob = printer_dev->printer->FindPrintJob(printer_dev->printer, irp->FileId); + + if (!printjob) + { + irp->IoStatus = STATUS_UNSUCCESSFUL; + } + else + { + printjob->Close(printjob); + } + + Stream_Zero(irp->output, 4); /* Padding(4) */ + return irp->Complete(irp); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT printer_process_irp_write(PRINTER_DEVICE* printer_dev, IRP* irp) +{ + UINT32 Length; + UINT64 Offset; + rdpPrintJob* printjob = NULL; + UINT error = CHANNEL_RC_OK; + void* ptr; + + if (Stream_GetRemainingLength(irp->input) < 32) + return ERROR_INVALID_DATA; + Stream_Read_UINT32(irp->input, Length); + Stream_Read_UINT64(irp->input, Offset); + Stream_Seek(irp->input, 20); /* Padding */ + ptr = Stream_Pointer(irp->input); + if (!Stream_SafeSeek(irp->input, Length)) + return ERROR_INVALID_DATA; + if (printer_dev->printer) + printjob = printer_dev->printer->FindPrintJob(printer_dev->printer, irp->FileId); + + if (!printjob) + { + irp->IoStatus = STATUS_UNSUCCESSFUL; + Length = 0; + } + else + { + error = printjob->Write(printjob, ptr, Length); + } + + if (error) + { + WLog_ERR(TAG, "printjob->Write failed with error %" PRIu32 "!", error); + return error; + } + + Stream_Write_UINT32(irp->output, Length); + Stream_Write_UINT8(irp->output, 0); /* Padding */ + return irp->Complete(irp); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT printer_process_irp_device_control(PRINTER_DEVICE* printer_dev, IRP* irp) +{ + Stream_Write_UINT32(irp->output, 0); /* OutputBufferLength */ + return irp->Complete(irp); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT printer_process_irp(PRINTER_DEVICE* printer_dev, IRP* irp) +{ + UINT error; + + switch (irp->MajorFunction) + { + case IRP_MJ_CREATE: + if ((error = printer_process_irp_create(printer_dev, irp))) + { + WLog_ERR(TAG, "printer_process_irp_create failed with error %" PRIu32 "!", error); + return error; + } + + break; + + case IRP_MJ_CLOSE: + if ((error = printer_process_irp_close(printer_dev, irp))) + { + WLog_ERR(TAG, "printer_process_irp_close failed with error %" PRIu32 "!", error); + return error; + } + + break; + + case IRP_MJ_WRITE: + if ((error = printer_process_irp_write(printer_dev, irp))) + { + WLog_ERR(TAG, "printer_process_irp_write failed with error %" PRIu32 "!", error); + return error; + } + + break; + + case IRP_MJ_DEVICE_CONTROL: + if ((error = printer_process_irp_device_control(printer_dev, irp))) + { + WLog_ERR(TAG, "printer_process_irp_device_control failed with error %" PRIu32 "!", + error); + return error; + } + + break; + + default: + irp->IoStatus = STATUS_NOT_SUPPORTED; + return irp->Complete(irp); + break; + } + + return CHANNEL_RC_OK; +} + +static DWORD WINAPI printer_thread_func(LPVOID arg) +{ + IRP* irp; + PRINTER_DEVICE* printer_dev = (PRINTER_DEVICE*)arg; + HANDLE obj[] = { printer_dev->event, printer_dev->stopEvent }; + UINT error = CHANNEL_RC_OK; + + while (1) + { + DWORD rc = WaitForMultipleObjects(2, obj, FALSE, INFINITE); + + if (rc == WAIT_FAILED) + { + error = GetLastError(); + WLog_ERR(TAG, "WaitForMultipleObjects failed with error %" PRIu32 "!", error); + break; + } + + if (rc == WAIT_OBJECT_0 + 1) + break; + else if (rc != WAIT_OBJECT_0) + continue; + + ResetEvent(printer_dev->event); + irp = (IRP*)InterlockedPopEntrySList(printer_dev->pIrpList); + + if (irp == NULL) + { + WLog_ERR(TAG, "InterlockedPopEntrySList failed!"); + error = ERROR_INTERNAL_ERROR; + break; + } + + if ((error = printer_process_irp(printer_dev, irp))) + { + WLog_ERR(TAG, "printer_process_irp failed with error %" PRIu32 "!", error); + break; + } + } + + if (error && printer_dev->rdpcontext) + setChannelError(printer_dev->rdpcontext, error, "printer_thread_func reported an error"); + + ExitThread(error); + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT printer_irp_request(DEVICE* device, IRP* irp) +{ + PRINTER_DEVICE* printer_dev = (PRINTER_DEVICE*)device; + InterlockedPushEntrySList(printer_dev->pIrpList, &(irp->ItemEntry)); + SetEvent(printer_dev->event); + return CHANNEL_RC_OK; +} + +static UINT printer_custom_component(DEVICE* device, UINT16 component, UINT16 packetId, wStream* s) +{ + UINT32 eventID; + PRINTER_DEVICE* printer_dev = (PRINTER_DEVICE*)device; + const rdpSettings* settings = printer_dev->rdpcontext->settings; + + if (component != RDPDR_CTYP_PRN) + return ERROR_INVALID_DATA; + + if (Stream_GetRemainingLength(s) < 4) + return ERROR_INVALID_DATA; + + Stream_Read_UINT32(s, eventID); + + switch (packetId) + { + case PAKID_PRN_CACHE_DATA: + switch (eventID) + { + case RDPDR_ADD_PRINTER_EVENT: + { + char PortDosName[8]; + UINT32 PnPNameLen, DriverNameLen, PrintNameLen, CacheFieldsLen; + const WCHAR *PnPName, *DriverName, *PrinterName; + const BYTE* CachedPrinterConfigData; + + if (Stream_GetRemainingLength(s) < 24) + return ERROR_INVALID_DATA; + + Stream_Read(s, PortDosName, sizeof(PortDosName)); + Stream_Read_UINT32(s, PnPNameLen); + Stream_Read_UINT32(s, DriverNameLen); + Stream_Read_UINT32(s, PrintNameLen); + Stream_Read_UINT32(s, CacheFieldsLen); + + if (Stream_GetRemainingLength(s) < PnPNameLen) + return ERROR_INVALID_DATA; + + PnPName = (const WCHAR*)Stream_Pointer(s); + Stream_Seek(s, PnPNameLen); + + if (Stream_GetRemainingLength(s) < DriverNameLen) + return ERROR_INVALID_DATA; + + DriverName = (const WCHAR*)Stream_Pointer(s); + Stream_Seek(s, DriverNameLen); + + if (Stream_GetRemainingLength(s) < PrintNameLen) + return ERROR_INVALID_DATA; + + PrinterName = (const WCHAR*)Stream_Pointer(s); + Stream_Seek(s, PrintNameLen); + + if (Stream_GetRemainingLength(s) < CacheFieldsLen) + return ERROR_INVALID_DATA; + + CachedPrinterConfigData = Stream_Pointer(s); + Stream_Seek(s, CacheFieldsLen); + + if (!printer_save_to_config(settings, PortDosName, sizeof(PortDosName), PnPName, + PnPNameLen, DriverName, DriverNameLen, PrinterName, + PrintNameLen, CachedPrinterConfigData, + CacheFieldsLen)) + return ERROR_INTERNAL_ERROR; + } + break; + + case RDPDR_UPDATE_PRINTER_EVENT: + { + UINT32 PrinterNameLen, ConfigDataLen; + const WCHAR* PrinterName; + const BYTE* ConfigData; + + if (Stream_GetRemainingLength(s) < 8) + return ERROR_INVALID_DATA; + + Stream_Read_UINT32(s, PrinterNameLen); + Stream_Read_UINT32(s, ConfigDataLen); + + if (Stream_GetRemainingLength(s) < PrinterNameLen) + return ERROR_INVALID_DATA; + + PrinterName = (const WCHAR*)Stream_Pointer(s); + Stream_Seek(s, PrinterNameLen); + + if (Stream_GetRemainingLength(s) < ConfigDataLen) + return ERROR_INVALID_DATA; + + ConfigData = Stream_Pointer(s); + Stream_Seek(s, ConfigDataLen); + + if (!printer_update_to_config(settings, PrinterName, PrinterNameLen, ConfigData, + ConfigDataLen)) + return ERROR_INTERNAL_ERROR; + } + break; + + case RDPDR_DELETE_PRINTER_EVENT: + { + UINT32 PrinterNameLen; + const WCHAR* PrinterName; + + if (Stream_GetRemainingLength(s) < 4) + return ERROR_INVALID_DATA; + + Stream_Read_UINT32(s, PrinterNameLen); + + if (Stream_GetRemainingLength(s) < PrinterNameLen) + return ERROR_INVALID_DATA; + + PrinterName = (const WCHAR*)Stream_Pointer(s); + Stream_Seek(s, PrinterNameLen); + printer_remove_config(settings, PrinterName, PrinterNameLen); + } + break; + + case RDPDR_RENAME_PRINTER_EVENT: + { + UINT32 OldPrinterNameLen, NewPrinterNameLen; + const WCHAR* OldPrinterName; + const WCHAR* NewPrinterName; + + if (Stream_GetRemainingLength(s) < 8) + return ERROR_INVALID_DATA; + + Stream_Read_UINT32(s, OldPrinterNameLen); + Stream_Read_UINT32(s, NewPrinterNameLen); + + if (Stream_GetRemainingLength(s) < OldPrinterNameLen) + return ERROR_INVALID_DATA; + + OldPrinterName = (const WCHAR*)Stream_Pointer(s); + Stream_Seek(s, OldPrinterNameLen); + + if (Stream_GetRemainingLength(s) < NewPrinterNameLen) + return ERROR_INVALID_DATA; + + NewPrinterName = (const WCHAR*)Stream_Pointer(s); + Stream_Seek(s, NewPrinterNameLen); + + if (!printer_move_config(settings, OldPrinterName, OldPrinterNameLen, + NewPrinterName, NewPrinterNameLen)) + return ERROR_INTERNAL_ERROR; + } + break; + + default: + WLog_ERR(TAG, "Unknown cache data eventID: 0x%08" PRIX32 "", eventID); + return ERROR_INVALID_DATA; + } + + break; + + case PAKID_PRN_USING_XPS: + { + UINT32 flags; + + if (Stream_GetRemainingLength(s) < 4) + return ERROR_INVALID_DATA; + + Stream_Read_UINT32(s, flags); + WLog_ERR(TAG, + "Ignoring unhandled message PAKID_PRN_USING_XPS [printerID=%08" PRIx32 + ", flags=%08" PRIx32 "]", + eventID, flags); + } + break; + + default: + WLog_ERR(TAG, "Unknown printing component packetID: 0x%04" PRIX16 "", packetId); + return ERROR_INVALID_DATA; + } + + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT printer_free(DEVICE* device) +{ + IRP* irp; + PRINTER_DEVICE* printer_dev = (PRINTER_DEVICE*)device; + UINT error; + SetEvent(printer_dev->stopEvent); + + if (WaitForSingleObject(printer_dev->thread, INFINITE) == WAIT_FAILED) + { + error = GetLastError(); + WLog_ERR(TAG, "WaitForSingleObject failed with error %" PRIu32 "", error); + + /* The analyzer is confused by this premature return value. + * Since this case can not be handled gracefully silence the + * analyzer here. */ +#ifndef __clang_analyzer__ + return error; +#endif + } + + while ((irp = (IRP*)InterlockedPopEntrySList(printer_dev->pIrpList)) != NULL) + irp->Discard(irp); + + CloseHandle(printer_dev->thread); + CloseHandle(printer_dev->stopEvent); + CloseHandle(printer_dev->event); + _aligned_free(printer_dev->pIrpList); + + if (printer_dev->printer) + printer_dev->printer->ReleaseRef(printer_dev->printer); + + Stream_Free(printer_dev->device.data, TRUE); + free(printer_dev); + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT printer_register(PDEVICE_SERVICE_ENTRY_POINTS pEntryPoints, rdpPrinter* printer) +{ + PRINTER_DEVICE* printer_dev; + UINT error = ERROR_INTERNAL_ERROR; + printer_dev = (PRINTER_DEVICE*)calloc(1, sizeof(PRINTER_DEVICE)); + + if (!printer_dev) + { + WLog_ERR(TAG, "calloc failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + printer_dev->device.data = Stream_New(NULL, 1024); + + if (!printer_dev->device.data) + goto error_out; + + sprintf_s(printer_dev->port, sizeof(printer_dev->port), "PRN%" PRIdz, printer->id); + printer_dev->device.type = RDPDR_DTYP_PRINT; + printer_dev->device.name = printer_dev->port; + printer_dev->device.IRPRequest = printer_irp_request; + printer_dev->device.CustomComponentRequest = printer_custom_component; + printer_dev->device.Free = printer_free; + printer_dev->rdpcontext = pEntryPoints->rdpcontext; + printer_dev->printer = printer; + printer_dev->pIrpList = (WINPR_PSLIST_HEADER)_aligned_malloc(sizeof(WINPR_SLIST_HEADER), + MEMORY_ALLOCATION_ALIGNMENT); + + if (!printer_dev->pIrpList) + { + WLog_ERR(TAG, "_aligned_malloc failed!"); + error = CHANNEL_RC_NO_MEMORY; + goto error_out; + } + + if (!printer_load_from_config(pEntryPoints->rdpcontext->settings, printer, printer_dev)) + goto error_out; + + InitializeSListHead(printer_dev->pIrpList); + + if (!(printer_dev->event = CreateEvent(NULL, TRUE, FALSE, NULL))) + { + WLog_ERR(TAG, "CreateEvent failed!"); + error = ERROR_INTERNAL_ERROR; + goto error_out; + } + + if (!(printer_dev->stopEvent = CreateEvent(NULL, TRUE, FALSE, NULL))) + { + WLog_ERR(TAG, "CreateEvent failed!"); + error = ERROR_INTERNAL_ERROR; + goto error_out; + } + + if ((error = pEntryPoints->RegisterDevice(pEntryPoints->devman, (DEVICE*)printer_dev))) + { + WLog_ERR(TAG, "RegisterDevice failed with error %" PRIu32 "!", error); + goto error_out; + } + + if (!(printer_dev->thread = + CreateThread(NULL, 0, printer_thread_func, (void*)printer_dev, 0, NULL))) + { + WLog_ERR(TAG, "CreateThread failed!"); + error = ERROR_INTERNAL_ERROR; + goto error_out; + } + + printer->AddRef(printer); + return CHANNEL_RC_OK; +error_out: + printer_free(&printer_dev->device); + return error; +} + +static rdpPrinterDriver* printer_load_backend(const char* backend) +{ + typedef rdpPrinterDriver* (*backend_load_t)(void); + union { + PVIRTUALCHANNELENTRY entry; + backend_load_t backend; + } fktconv; + + fktconv.entry = freerdp_load_channel_addin_entry("printer", backend, NULL, 0); + if (!fktconv.entry) + return NULL; + + return fktconv.backend(); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT +#ifdef BUILTIN_CHANNELS +printer_DeviceServiceEntry +#else + FREERDP_API + DeviceServiceEntry +#endif + (PDEVICE_SERVICE_ENTRY_POINTS pEntryPoints) +{ + int i; + char* name; + char* driver_name; + BOOL default_backend = TRUE; + RDPDR_PRINTER* device = NULL; + rdpPrinterDriver* driver = NULL; + UINT error = CHANNEL_RC_OK; + + if (!pEntryPoints || !pEntryPoints->device) + return ERROR_INVALID_PARAMETER; + + device = (RDPDR_PRINTER*)pEntryPoints->device; + name = device->Name; + driver_name = _strdup(device->DriverName); + + /* Secondary argument is one of the following: + * + * ... name of a printer driver + * : ... name of a printer driver and local printer backend to use + */ + if (driver_name) + { + char* sep = strstr(driver_name, ":"); + if (sep) + { + const char* backend = sep + 1; + *sep = '\0'; + driver = printer_load_backend(backend); + default_backend = FALSE; + } + } + + if (!driver && default_backend) + { + const char* backend = +#if defined(WITH_CUPS) + "cups" +#elif defined(_WIN32) + "win" +#else + "" +#endif + ; + + driver = printer_load_backend(backend); + } + + if (!driver) + { + WLog_ERR(TAG, "Could not get a printer driver!"); + error = CHANNEL_RC_INITIALIZATION_ERROR; + goto fail; + } + + if (name && name[0]) + { + rdpPrinter* printer = driver->GetPrinter(driver, name, driver_name); + + if (!printer) + { + WLog_ERR(TAG, "Could not get printer %s!", name); + error = CHANNEL_RC_INITIALIZATION_ERROR; + goto fail; + } + + if (!printer_save_default_config(pEntryPoints->rdpcontext->settings, printer)) + { + error = CHANNEL_RC_INITIALIZATION_ERROR; + printer->ReleaseRef(printer); + goto fail; + } + + if ((error = printer_register(pEntryPoints, printer))) + { + WLog_ERR(TAG, "printer_register failed with error %" PRIu32 "!", error); + printer->ReleaseRef(printer); + goto fail; + } + } + else + { + rdpPrinter** printers = driver->EnumPrinters(driver); + rdpPrinter** current = printers; + + for (i = 0; current[i]; i++) + { + rdpPrinter* printer = current[i]; + + if ((error = printer_register(pEntryPoints, printer))) + { + WLog_ERR(TAG, "printer_register failed with error %" PRIu32 "!", error); + break; + } + } + + driver->ReleaseEnumPrinters(printers); + } + +fail: + free(driver_name); + if (driver) + driver->ReleaseRef(driver); + + return error; +} diff --git a/channels/printer/client/win/CMakeLists.txt b/channels/printer/client/win/CMakeLists.txt new file mode 100644 index 0000000..aa648fd --- /dev/null +++ b/channels/printer/client/win/CMakeLists.txt @@ -0,0 +1,29 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2019 Armin Novak +# Copyright 2019 Thincast Technologies GmbH +# +# Licensed 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. +define_channel_client_subsystem("printer" "win" "") + +set(${MODULE_PREFIX}_SRCS + printer_win.c) + +include_directories(..) + +add_channel_client_subsystem_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} "" TRUE "") + +set(${MODULE_PREFIX}_LIBS ${${MODULE_PREFIX}_LIBS} winpr freerdp) + +target_link_libraries(${MODULE_NAME} ${${MODULE_PREFIX}_LIBS}) diff --git a/channels/printer/client/win/printer_win.c b/channels/printer/client/win/printer_win.c new file mode 100644 index 0000000..86b5e66 --- /dev/null +++ b/channels/printer/client/win/printer_win.c @@ -0,0 +1,459 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Print Virtual Channel - WIN driver + * + * Copyright 2012 Gerald Richter + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger + * Copyright 2016 Armin Novak + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include + +#define PRINTER_TAG CHANNELS_TAG("printer.client") +#ifdef WITH_DEBUG_WINPR +#define DEBUG_WINPR(...) WLog_DBG(PRINTER_TAG, __VA_ARGS__) +#else +#define DEBUG_WINPR(...) \ + do \ + { \ + } while (0) +#endif + +typedef struct rdp_win_printer_driver rdpWinPrinterDriver; +typedef struct rdp_win_printer rdpWinPrinter; +typedef struct rdp_win_print_job rdpWinPrintJob; + +struct rdp_win_printer_driver +{ + rdpPrinterDriver driver; + + size_t id_sequence; + size_t references; +}; + +struct rdp_win_printer +{ + rdpPrinter printer; + HANDLE hPrinter; + rdpWinPrintJob* printjob; +}; + +struct rdp_win_print_job +{ + rdpPrintJob printjob; + DOC_INFO_1 di; + DWORD handle; + + void* printjob_object; + int printjob_id; +}; + +static WCHAR* printer_win_get_printjob_name(size_t id) +{ + time_t tt; + struct tm tres; + struct tm* t; + WCHAR* str; + size_t len = 1024; + int rc; + + tt = time(NULL); + t = localtime_s(&tt, &tres); + + str = calloc(len, sizeof(WCHAR)); + if (!str) + return NULL; + + rc = swprintf_s(str, len, L"FreeRDP Print %04d-%02d-%02d% 02d-%02d-%02d - Job %lu\0", + t->tm_year + 1900, t->tm_mon + 1, t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec, + id); + + return str; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT printer_win_write_printjob(rdpPrintJob* printjob, const BYTE* data, size_t size) +{ + rdpWinPrinter* printer; + LPCVOID pBuf = data; + DWORD cbBuf = size; + DWORD pcWritten; + + if (!printjob || !data) + return ERROR_BAD_ARGUMENTS; + + printer = (rdpWinPrinter*)printjob->printer; + if (!printer) + return ERROR_BAD_ARGUMENTS; + + if (!WritePrinter(printer->hPrinter, pBuf, cbBuf, &pcWritten)) + return ERROR_INTERNAL_ERROR; + return CHANNEL_RC_OK; +} + +static void printer_win_close_printjob(rdpPrintJob* printjob) +{ + rdpWinPrintJob* win_printjob = (rdpWinPrintJob*)printjob; + rdpWinPrinter* win_printer; + + if (!printjob) + return; + + win_printer = (rdpWinPrinter*)printjob->printer; + if (!win_printer) + return; + + if (!EndPagePrinter(win_printer->hPrinter)) + { + } + + if (!ClosePrinter(win_printer->hPrinter)) + { + } + + win_printer->printjob = NULL; + + free(win_printjob->di.pDocName); + free(win_printjob); +} + +static rdpPrintJob* printer_win_create_printjob(rdpPrinter* printer, UINT32 id) +{ + rdpWinPrinter* win_printer = (rdpWinPrinter*)printer; + rdpWinPrintJob* win_printjob; + + if (win_printer->printjob != NULL) + return NULL; + + win_printjob = (rdpWinPrintJob*)calloc(1, sizeof(rdpWinPrintJob)); + if (!win_printjob) + return NULL; + + win_printjob->printjob.id = id; + win_printjob->printjob.printer = printer; + win_printjob->di.pDocName = printer_win_get_printjob_name(id); + win_printjob->di.pDatatype = NULL; + win_printjob->di.pOutputFile = NULL; + + win_printjob->handle = StartDocPrinter(win_printer->hPrinter, 1, (LPBYTE) & (win_printjob->di)); + + if (!win_printjob->handle) + { + free(win_printjob->di.pDocName); + free(win_printjob); + return NULL; + } + + if (!StartPagePrinter(win_printer->hPrinter)) + { + free(win_printjob->di.pDocName); + free(win_printjob); + return NULL; + } + + win_printjob->printjob.Write = printer_win_write_printjob; + win_printjob->printjob.Close = printer_win_close_printjob; + + win_printer->printjob = win_printjob; + + return &win_printjob->printjob; +} + +static rdpPrintJob* printer_win_find_printjob(rdpPrinter* printer, UINT32 id) +{ + rdpWinPrinter* win_printer = (rdpWinPrinter*)printer; + + if (!win_printer->printjob) + return NULL; + + if (win_printer->printjob->printjob.id != id) + return NULL; + + return (rdpPrintJob*)win_printer->printjob; +} + +static void printer_win_free_printer(rdpPrinter* printer) +{ + rdpWinPrinter* win_printer = (rdpWinPrinter*)printer; + + if (win_printer->printjob) + win_printer->printjob->printjob.Close((rdpPrintJob*)win_printer->printjob); + + if (printer->backend) + printer->backend->ReleaseRef(printer->backend); + + free(printer->name); + free(printer->driver); + free(printer); +} + +static void printer_win_add_ref_printer(rdpPrinter* printer) +{ + if (printer) + printer->references++; +} + +static void printer_win_release_ref_printer(rdpPrinter* printer) +{ + if (!printer) + return; + if (printer->references <= 1) + printer_win_free_printer(printer); + else + printer->references--; +} + +static rdpPrinter* printer_win_new_printer(rdpWinPrinterDriver* win_driver, const WCHAR* name, + const WCHAR* drivername, BOOL is_default) +{ + rdpWinPrinter* win_printer; + DWORD needed = 0; + int status; + PRINTER_INFO_2* prninfo = NULL; + + win_printer = (rdpWinPrinter*)calloc(1, sizeof(rdpWinPrinter)); + if (!win_printer) + return NULL; + + win_printer->printer.backend = &win_driver->driver; + win_printer->printer.id = win_driver->id_sequence++; + if (ConvertFromUnicode(CP_UTF8, 0, name, -1, &win_printer->printer.name, 0, NULL, NULL) < 1) + { + free(win_printer); + return NULL; + } + + if (!win_printer->printer.name) + { + free(win_printer); + return NULL; + } + win_printer->printer.is_default = is_default; + + win_printer->printer.CreatePrintJob = printer_win_create_printjob; + win_printer->printer.FindPrintJob = printer_win_find_printjob; + win_printer->printer.AddRef = printer_win_add_ref_printer; + win_printer->printer.ReleaseRef = printer_win_release_ref_printer; + + if (!OpenPrinter(name, &(win_printer->hPrinter), NULL)) + { + free(win_printer->printer.name); + free(win_printer); + return NULL; + } + + /* How many memory should be allocated for printer data */ + GetPrinter(win_printer->hPrinter, 2, (LPBYTE)prninfo, 0, &needed); + if (needed == 0) + { + free(win_printer->printer.name); + free(win_printer); + return NULL; + } + + prninfo = (PRINTER_INFO_2*)GlobalAlloc(GPTR, needed); + if (!prninfo) + { + free(win_printer->printer.name); + free(win_printer); + return NULL; + } + + if (!GetPrinter(win_printer->hPrinter, 2, (LPBYTE)prninfo, needed, &needed)) + { + GlobalFree(prninfo); + free(win_printer->printer.name); + free(win_printer); + return NULL; + } + + if (drivername) + status = ConvertFromUnicode(CP_UTF8, 0, drivername, -1, &win_printer->printer.driver, 0, + NULL, NULL); + else + status = ConvertFromUnicode(CP_UTF8, 0, prninfo->pDriverName, -1, + &win_printer->printer.driver, 0, NULL, NULL); + if (!win_printer->printer.driver || (status <= 0)) + { + GlobalFree(prninfo); + free(win_printer->printer.name); + free(win_printer); + return NULL; + } + + win_printer->printer.AddRef(&win_printer->printer); + win_printer->printer.backend->AddRef(win_printer->printer.backend); + return &win_printer->printer; +} + +static void printer_win_release_enum_printers(rdpPrinter** printers) +{ + rdpPrinter** cur = printers; + + while ((cur != NULL) && ((*cur) != NULL)) + { + if ((*cur)->ReleaseRef) + (*cur)->ReleaseRef(*cur); + cur++; + } + free(printers); +} + +static rdpPrinter** printer_win_enum_printers(rdpPrinterDriver* driver) +{ + rdpPrinter** printers; + int num_printers; + int i; + PRINTER_INFO_2* prninfo = NULL; + DWORD needed, returned; + + /* find required size for the buffer */ + EnumPrinters(PRINTER_ENUM_LOCAL | PRINTER_ENUM_CONNECTIONS, NULL, 2, NULL, 0, &needed, + &returned); + + /* allocate array of PRINTER_INFO structures */ + prninfo = (PRINTER_INFO_2*)GlobalAlloc(GPTR, needed); + if (!prninfo) + return NULL; + + /* call again */ + if (!EnumPrinters(PRINTER_ENUM_LOCAL | PRINTER_ENUM_CONNECTIONS, NULL, 2, (LPBYTE)prninfo, + needed, &needed, &returned)) + { + } + + printers = (rdpPrinter**)calloc((returned + 1), sizeof(rdpPrinter*)); + if (!printers) + { + GlobalFree(prninfo); + return NULL; + } + + num_printers = 0; + + for (i = 0; i < (int)returned; i++) + { + rdpPrinter* current = printers[num_printers]; + current = printer_win_new_printer((rdpWinPrinterDriver*)driver, prninfo[i].pPrinterName, + prninfo[i].pDriverName, 0); + if (!current) + { + printer_win_release_enum_printers(printers); + printers = NULL; + break; + } + printers[num_printers++] = current; + } + + GlobalFree(prninfo); + return printers; +} + +static rdpPrinter* printer_win_get_printer(rdpPrinterDriver* driver, const char* name, + const char* driverName) +{ + WCHAR* driverNameW = NULL; + WCHAR* nameW = NULL; + rdpWinPrinterDriver* win_driver = (rdpWinPrinterDriver*)driver; + rdpPrinter* myPrinter = NULL; + + if (name) + { + ConvertToUnicode(CP_UTF8, 0, name, -1, &nameW, 0); + if (!driverNameW) + return NULL; + } + if (driverName) + { + ConvertToUnicode(CP_UTF8, 0, driverName, -1, &driverNameW, 0); + if (!driverNameW) + return NULL; + } + + myPrinter = printer_win_new_printer(win_driver, nameW, driverNameW, + win_driver->id_sequence == 1 ? TRUE : FALSE); + free(driverNameW); + free(nameW); + + return myPrinter; +} + +static void printer_win_add_ref_driver(rdpPrinterDriver* driver) +{ + rdpWinPrinterDriver* win = (rdpWinPrinterDriver*)driver; + if (win) + win->references++; +} + +/* Singleton */ +static rdpWinPrinterDriver* win_driver = NULL; + +static void printer_win_release_ref_driver(rdpPrinterDriver* driver) +{ + rdpWinPrinterDriver* win = (rdpWinPrinterDriver*)driver; + if (win->references <= 1) + { + free(win); + win_driver = NULL; + } + else + win->references--; +} + +#ifdef BUILTIN_CHANNELS +rdpPrinterDriver* win_freerdp_printer_client_subsystem_entry(void) +#else +FREERDP_API rdpPrinterDriver* freerdp_printer_client_subsystem_entry(void) +#endif +{ + if (!win_driver) + { + win_driver = (rdpWinPrinterDriver*)calloc(1, sizeof(rdpWinPrinterDriver)); + + if (!win_driver) + return NULL; + + win_driver->driver.EnumPrinters = printer_win_enum_printers; + win_driver->driver.ReleaseEnumPrinters = printer_win_release_enum_printers; + win_driver->driver.GetPrinter = printer_win_get_printer; + + win_driver->driver.AddRef = printer_win_add_ref_driver; + win_driver->driver.ReleaseRef = printer_win_release_ref_driver; + + win_driver->id_sequence = 1; + win_driver->driver.AddRef(&win_driver->driver); + } + + return &win_driver->driver; +} diff --git a/channels/printer/printer.h b/channels/printer/printer.h new file mode 100644 index 0000000..ae0902d --- /dev/null +++ b/channels/printer/printer.h @@ -0,0 +1,36 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Definition for the printer channel + * + * Copyright 2016 David Fort + * + * Licensed 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 FREERDP_CHANNEL_PRINTER_PRINTER_H +#define FREERDP_CHANNEL_PRINTER_PRINTER_H + +/* SERVER_PRINTER_CACHE_EVENT.cachedata */ +#define RDPDR_ADD_PRINTER_EVENT 0x00000001 +#define RDPDR_UPDATE_PRINTER_EVENT 0x00000002 +#define RDPDR_DELETE_PRINTER_EVENT 0x00000003 +#define RDPDR_RENAME_PRINTER_EVENT 0x00000004 + +/* DR_PRN_DEVICE_ANNOUNCE.Flags */ +#define RDPDR_PRINTER_ANNOUNCE_FLAG_ASCII 0x00000001 +#define RDPDR_PRINTER_ANNOUNCE_FLAG_DEFAULTPRINTER 0x00000002 +#define RDPDR_PRINTER_ANNOUNCE_FLAG_NETWORKPRINTER 0x00000004 +#define RDPDR_PRINTER_ANNOUNCE_FLAG_TSPRINTER 0x00000008 +#define RDPDR_PRINTER_ANNOUNCE_FLAG_XPSFORMAT 0x00000010 + +#endif /* FREERDP_CHANNEL_PRINTER_PRINTER_H */ diff --git a/channels/rail/CMakeLists.txt b/channels/rail/CMakeLists.txt new file mode 100644 index 0000000..d372dda --- /dev/null +++ b/channels/rail/CMakeLists.txt @@ -0,0 +1,26 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2012 Marc-Andre Moreau +# +# Licensed 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. + +define_channel("rail") + +if(WITH_CLIENT_CHANNELS) + add_channel_client(${MODULE_PREFIX} ${CHANNEL_NAME}) +endif() + +if(WITH_SERVER_CHANNELS) + add_channel_server(${MODULE_PREFIX} ${CHANNEL_NAME}) +endif() diff --git a/channels/rail/ChannelOptions.cmake b/channels/rail/ChannelOptions.cmake new file mode 100644 index 0000000..76f8571 --- /dev/null +++ b/channels/rail/ChannelOptions.cmake @@ -0,0 +1,13 @@ + +set(OPTION_DEFAULT OFF) +set(OPTION_CLIENT_DEFAULT ON) +set(OPTION_SERVER_DEFAULT OFF) + +define_channel_options(NAME "rail" TYPE "static" + DESCRIPTION "Remote Programs Virtual Channel Extension" + SPECIFICATIONS "[MS-RDPERP]" + DEFAULT ${OPTION_DEFAULT}) + +define_channel_client_options(${OPTION_CLIENT_DEFAULT}) +define_channel_server_options(${OPTION_SERVER_DEFAULT}) + diff --git a/channels/rail/client/CMakeLists.txt b/channels/rail/client/CMakeLists.txt new file mode 100644 index 0000000..c87fd2f --- /dev/null +++ b/channels/rail/client/CMakeLists.txt @@ -0,0 +1,35 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2012 Marc-Andre Moreau +# +# Licensed 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. + +define_channel_client("rail") + +set(${MODULE_PREFIX}_SRCS + ../rail_common.h + ../rail_common.c + rail_main.c + rail_main.h + rail_orders.c + rail_orders.h) + +add_channel_client_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} FALSE "VirtualChannelEntryEx") + + + +target_link_libraries(${MODULE_NAME} freerdp) + + +set_property(TARGET ${MODULE_NAME} PROPERTY FOLDER "Channels/${CHANNEL_NAME}/Client") diff --git a/channels/rail/client/rail_main.c b/channels/rail/client/rail_main.c new file mode 100644 index 0000000..e19cb92 --- /dev/null +++ b/channels/rail/client/rail_main.c @@ -0,0 +1,895 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * RAIL Virtual Channel Plugin + * + * Copyright 2011 Marc-Andre Moreau + * Copyright 2011 Roman Barabanov + * Copyright 2011 Vic Lee + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger + * Copyright 2017 Armin Novak + * Copyright 2017 Thincast Technologies GmbH + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include + +#include +#include + +#include "rail_orders.h" +#include "rail_main.h" + +RailClientContext* rail_get_client_interface(railPlugin* rail) +{ + RailClientContext* pInterface; + + if (!rail) + return NULL; + + pInterface = (RailClientContext*)rail->channelEntryPoints.pInterface; + return pInterface; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rail_send(railPlugin* rail, wStream* s) +{ + UINT status; + + if (!rail) + { + Stream_Free(s, TRUE); + return CHANNEL_RC_BAD_INIT_HANDLE; + } + + status = rail->channelEntryPoints.pVirtualChannelWriteEx( + rail->InitHandle, rail->OpenHandle, Stream_Buffer(s), (UINT32)Stream_GetPosition(s), s); + + if (status != CHANNEL_RC_OK) + { + Stream_Free(s, TRUE); + WLog_ERR(TAG, "pVirtualChannelWriteEx failed with %s [%08" PRIX32 "]", + WTSErrorToString(status), status); + } + + return status; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT rail_send_channel_data(railPlugin* rail, wStream* src) +{ + wStream* s; + size_t length; + + if (!rail || !src) + return ERROR_INVALID_PARAMETER; + + length = Stream_GetPosition(src); + s = Stream_New(NULL, length); + + if (!s) + { + WLog_ERR(TAG, "Stream_New failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + Stream_Write(s, Stream_Buffer(src), length); + return rail_send(rail, s); +} + +/** + * Callback Interface + */ + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rail_client_execute(RailClientContext* context, const RAIL_EXEC_ORDER* exec) +{ + char* exeOrFile; + UINT error; + railPlugin* rail; + UINT16 flags; + RAIL_UNICODE_STRING ruExeOrFile = { 0 }; + RAIL_UNICODE_STRING ruWorkingDir = { 0 }; + RAIL_UNICODE_STRING ruArguments = { 0 }; + + if (!context || !exec) + return ERROR_INVALID_PARAMETER; + + rail = (railPlugin*)context->handle; + exeOrFile = exec->RemoteApplicationProgram; + flags = exec->flags; + + if (!exeOrFile) + return ERROR_INVALID_PARAMETER; + + if (!utf8_string_to_rail_string(exec->RemoteApplicationProgram, + &ruExeOrFile) || /* RemoteApplicationProgram */ + !utf8_string_to_rail_string(exec->RemoteApplicationWorkingDir, + &ruWorkingDir) || /* ShellWorkingDirectory */ + !utf8_string_to_rail_string(exec->RemoteApplicationArguments, + &ruArguments)) /* RemoteApplicationCmdLine */ + error = ERROR_INTERNAL_ERROR; + else + error = rail_send_client_exec_order(rail, flags, &ruExeOrFile, &ruWorkingDir, &ruArguments); + + free(ruExeOrFile.string); + free(ruWorkingDir.string); + free(ruArguments.string); + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rail_client_activate(RailClientContext* context, const RAIL_ACTIVATE_ORDER* activate) +{ + railPlugin* rail; + + if (!context || !activate) + return ERROR_INVALID_PARAMETER; + + rail = (railPlugin*)context->handle; + return rail_send_client_activate_order(rail, activate); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rail_send_client_sysparam(RailClientContext* context, RAIL_SYSPARAM_ORDER* sysparam) +{ + wStream* s; + size_t length = RAIL_SYSPARAM_ORDER_LENGTH; + railPlugin* rail; + UINT error; + BOOL extendedSpiSupported; + + if (!context || !sysparam) + return ERROR_INVALID_PARAMETER; + + rail = (railPlugin*)context->handle; + + switch (sysparam->param) + { + case SPI_SET_DRAG_FULL_WINDOWS: + case SPI_SET_KEYBOARD_CUES: + case SPI_SET_KEYBOARD_PREF: + case SPI_SET_MOUSE_BUTTON_SWAP: + length += 1; + break; + + case SPI_SET_WORK_AREA: + case SPI_DISPLAY_CHANGE: + case SPI_TASKBAR_POS: + length += 8; + break; + + case SPI_SET_HIGH_CONTRAST: + length += sysparam->highContrast.colorSchemeLength + 10; + break; + + case SPI_SETFILTERKEYS: + length += 20; + break; + + case SPI_SETSTICKYKEYS: + case SPI_SETCARETWIDTH: + case SPI_SETTOGGLEKEYS: + length += 4; + break; + + default: + return ERROR_BAD_ARGUMENTS; + } + + s = rail_pdu_init(length); + + if (!s) + { + WLog_ERR(TAG, "rail_pdu_init failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + extendedSpiSupported = rail_is_extended_spi_supported(rail->channelFlags); + if ((error = rail_write_sysparam_order(s, sysparam, extendedSpiSupported))) + { + WLog_ERR(TAG, "rail_write_client_sysparam_order failed with error %" PRIu32 "!", error); + Stream_Free(s, TRUE); + return error; + } + + if ((error = rail_send_pdu(rail, s, TS_RAIL_ORDER_SYSPARAM))) + { + WLog_ERR(TAG, "rail_send_pdu failed with error %" PRIu32 "!", error); + } + + Stream_Free(s, TRUE); + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rail_client_system_param(RailClientContext* context, + const RAIL_SYSPARAM_ORDER* sysInParam) +{ + UINT error = CHANNEL_RC_OK; + RAIL_SYSPARAM_ORDER sysparam; + + if (!context || !sysInParam) + return ERROR_INVALID_PARAMETER; + + sysparam = *sysInParam; + + if (sysparam.params & SPI_MASK_SET_HIGH_CONTRAST) + { + sysparam.param = SPI_SET_HIGH_CONTRAST; + + if ((error = rail_send_client_sysparam(context, &sysparam))) + { + WLog_ERR(TAG, "rail_send_client_sysparam failed with error %" PRIu32 "!", error); + return error; + } + } + + if (sysparam.params & SPI_MASK_TASKBAR_POS) + { + sysparam.param = SPI_TASKBAR_POS; + + if ((error = rail_send_client_sysparam(context, &sysparam))) + { + WLog_ERR(TAG, "rail_send_client_sysparam failed with error %" PRIu32 "!", error); + return error; + } + } + + if (sysparam.params & SPI_MASK_SET_MOUSE_BUTTON_SWAP) + { + sysparam.param = SPI_SET_MOUSE_BUTTON_SWAP; + + if ((error = rail_send_client_sysparam(context, &sysparam))) + { + WLog_ERR(TAG, "rail_send_client_sysparam failed with error %" PRIu32 "!", error); + return error; + } + } + + if (sysparam.params & SPI_MASK_SET_KEYBOARD_PREF) + { + sysparam.param = SPI_SET_KEYBOARD_PREF; + + if ((error = rail_send_client_sysparam(context, &sysparam))) + { + WLog_ERR(TAG, "rail_send_client_sysparam failed with error %" PRIu32 "!", error); + return error; + } + } + + if (sysparam.params & SPI_MASK_SET_DRAG_FULL_WINDOWS) + { + sysparam.param = SPI_SET_DRAG_FULL_WINDOWS; + + if ((error = rail_send_client_sysparam(context, &sysparam))) + { + WLog_ERR(TAG, "rail_send_client_sysparam failed with error %" PRIu32 "!", error); + return error; + } + } + + if (sysparam.params & SPI_MASK_SET_KEYBOARD_CUES) + { + sysparam.param = SPI_SET_KEYBOARD_CUES; + + if ((error = rail_send_client_sysparam(context, &sysparam))) + { + WLog_ERR(TAG, "rail_send_client_sysparam failed with error %" PRIu32 "!", error); + return error; + } + } + + if (sysparam.params & SPI_MASK_SET_WORK_AREA) + { + sysparam.param = SPI_SET_WORK_AREA; + + if ((error = rail_send_client_sysparam(context, &sysparam))) + { + WLog_ERR(TAG, "rail_send_client_sysparam failed with error %" PRIu32 "!", error); + return error; + } + } + + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rail_client_system_command(RailClientContext* context, + const RAIL_SYSCOMMAND_ORDER* syscommand) +{ + railPlugin* rail; + + if (!context || !syscommand) + return ERROR_INVALID_PARAMETER; + + rail = (railPlugin*)context->handle; + return rail_send_client_syscommand_order(rail, syscommand); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rail_client_handshake(RailClientContext* context, const RAIL_HANDSHAKE_ORDER* handshake) +{ + railPlugin* rail; + + if (!context || !handshake) + return ERROR_INVALID_PARAMETER; + + rail = (railPlugin*)context->handle; + return rail_send_handshake_order(rail, handshake); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rail_client_notify_event(RailClientContext* context, + const RAIL_NOTIFY_EVENT_ORDER* notifyEvent) +{ + railPlugin* rail; + + if (!context || !notifyEvent) + return ERROR_INVALID_PARAMETER; + + rail = (railPlugin*)context->handle; + return rail_send_client_notify_event_order(rail, notifyEvent); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rail_client_window_move(RailClientContext* context, + const RAIL_WINDOW_MOVE_ORDER* windowMove) +{ + railPlugin* rail; + + if (!context || !windowMove) + return ERROR_INVALID_PARAMETER; + + rail = (railPlugin*)context->handle; + return rail_send_client_window_move_order(rail, windowMove); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rail_client_information(RailClientContext* context, + const RAIL_CLIENT_STATUS_ORDER* clientStatus) +{ + railPlugin* rail; + + if (!context || !clientStatus) + return ERROR_INVALID_PARAMETER; + + rail = (railPlugin*)context->handle; + return rail_send_client_status_order(rail, clientStatus); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rail_client_system_menu(RailClientContext* context, const RAIL_SYSMENU_ORDER* sysmenu) +{ + railPlugin* rail; + + if (!context || !sysmenu) + return ERROR_INVALID_PARAMETER; + + rail = (railPlugin*)context->handle; + return rail_send_client_sysmenu_order(rail, sysmenu); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rail_client_language_bar_info(RailClientContext* context, + const RAIL_LANGBAR_INFO_ORDER* langBarInfo) +{ + railPlugin* rail; + + if (!context || !langBarInfo) + return ERROR_INVALID_PARAMETER; + + rail = (railPlugin*)context->handle; + return rail_send_client_langbar_info_order(rail, langBarInfo); +} + +static UINT rail_client_language_ime_info(RailClientContext* context, + const RAIL_LANGUAGEIME_INFO_ORDER* langImeInfo) +{ + railPlugin* rail; + + if (!context || !langImeInfo) + return ERROR_INVALID_PARAMETER; + + rail = (railPlugin*)context->handle; + return rail_send_client_languageime_info_order(rail, langImeInfo); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rail_client_get_appid_request(RailClientContext* context, + const RAIL_GET_APPID_REQ_ORDER* getAppIdReq) +{ + railPlugin* rail; + + if (!context || !getAppIdReq || !context->handle) + return ERROR_INVALID_PARAMETER; + + rail = (railPlugin*)context->handle; + return rail_send_client_get_appid_req_order(rail, getAppIdReq); +} + +static UINT rail_client_compartment_info(RailClientContext* context, + const RAIL_COMPARTMENT_INFO_ORDER* compartmentInfo) +{ + railPlugin* rail; + + if (!context || !compartmentInfo || !context->handle) + return ERROR_INVALID_PARAMETER; + + rail = (railPlugin*)context->handle; + return rail_send_client_compartment_info_order(rail, compartmentInfo); +} + +static UINT rail_client_cloak(RailClientContext* context, const RAIL_CLOAK* cloak) +{ + railPlugin* rail; + + if (!context || !cloak || !context->handle) + return ERROR_INVALID_PARAMETER; + + rail = (railPlugin*)context->handle; + return rail_send_client_cloak_order(rail, cloak); +} + +static UINT rail_client_snap_arrange(RailClientContext* context, const RAIL_SNAP_ARRANGE* snap) +{ + railPlugin* rail; + + if (!context || !snap || !context->handle) + return ERROR_INVALID_PARAMETER; + + rail = (railPlugin*)context->handle; + return rail_send_client_snap_arrange_order(rail, snap); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rail_virtual_channel_event_data_received(railPlugin* rail, void* pData, + UINT32 dataLength, UINT32 totalLength, + UINT32 dataFlags) +{ + wStream* data_in; + + if ((dataFlags & CHANNEL_FLAG_SUSPEND) || (dataFlags & CHANNEL_FLAG_RESUME)) + { + return CHANNEL_RC_OK; + } + + if (dataFlags & CHANNEL_FLAG_FIRST) + { + if (rail->data_in) + Stream_Free(rail->data_in, TRUE); + + rail->data_in = Stream_New(NULL, totalLength); + + if (!rail->data_in) + { + WLog_ERR(TAG, "Stream_New failed!"); + return CHANNEL_RC_NO_MEMORY; + } + } + + data_in = rail->data_in; + + if (!Stream_EnsureRemainingCapacity(data_in, dataLength)) + { + WLog_ERR(TAG, "Stream_EnsureRemainingCapacity failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + Stream_Write(data_in, pData, dataLength); + + if (dataFlags & CHANNEL_FLAG_LAST) + { + if (Stream_Capacity(data_in) != Stream_GetPosition(data_in)) + { + WLog_ERR(TAG, "rail_plugin_process_received: read error"); + return ERROR_INTERNAL_ERROR; + } + + rail->data_in = NULL; + Stream_SealLength(data_in); + Stream_SetPosition(data_in, 0); + + if (!MessageQueue_Post(rail->queue, NULL, 0, (void*)data_in, NULL)) + { + WLog_ERR(TAG, "MessageQueue_Post failed!"); + return ERROR_INTERNAL_ERROR; + } + } + + return CHANNEL_RC_OK; +} + +static VOID VCAPITYPE rail_virtual_channel_open_event_ex(LPVOID lpUserParam, DWORD openHandle, + UINT event, LPVOID pData, + UINT32 dataLength, UINT32 totalLength, + UINT32 dataFlags) +{ + UINT error = CHANNEL_RC_OK; + railPlugin* rail = (railPlugin*)lpUserParam; + + switch (event) + { + case CHANNEL_EVENT_DATA_RECEIVED: + if (!rail || (rail->OpenHandle != openHandle)) + { + WLog_ERR(TAG, "error no match"); + return; + } + if ((error = rail_virtual_channel_event_data_received(rail, pData, dataLength, + totalLength, dataFlags))) + WLog_ERR(TAG, + "rail_virtual_channel_event_data_received failed with error %" PRIu32 "!", + error); + + break; + + case CHANNEL_EVENT_WRITE_CANCELLED: + case CHANNEL_EVENT_WRITE_COMPLETE: + { + wStream* s = (wStream*)pData; + Stream_Free(s, TRUE); + } + break; + + case CHANNEL_EVENT_USER: + break; + } + + if (error && rail && rail->rdpcontext) + setChannelError(rail->rdpcontext, error, + "rail_virtual_channel_open_event reported an error"); + + return; +} + +static DWORD WINAPI rail_virtual_channel_client_thread(LPVOID arg) +{ + wStream* data; + wMessage message; + railPlugin* rail = (railPlugin*)arg; + UINT error = CHANNEL_RC_OK; + + while (1) + { + if (!MessageQueue_Wait(rail->queue)) + { + WLog_ERR(TAG, "MessageQueue_Wait failed!"); + error = ERROR_INTERNAL_ERROR; + break; + } + + if (!MessageQueue_Peek(rail->queue, &message, TRUE)) + { + WLog_ERR(TAG, "MessageQueue_Peek failed!"); + error = ERROR_INTERNAL_ERROR; + break; + } + + if (message.id == WMQ_QUIT) + break; + + if (message.id == 0) + { + data = (wStream*)message.wParam; + error = rail_order_recv(rail, data); + Stream_Free(data, TRUE); + + if (error) + { + WLog_ERR(TAG, "rail_order_recv failed with error %" PRIu32 "!", error); + break; + } + } + } + + if (error && rail->rdpcontext) + setChannelError(rail->rdpcontext, error, + "rail_virtual_channel_client_thread reported an error"); + + ExitThread(error); + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rail_virtual_channel_event_connected(railPlugin* rail, LPVOID pData, UINT32 dataLength) +{ + RailClientContext* context = rail_get_client_interface(rail); + UINT status; + status = rail->channelEntryPoints.pVirtualChannelOpenEx(rail->InitHandle, &rail->OpenHandle, + rail->channelDef.name, + rail_virtual_channel_open_event_ex); + + if (status != CHANNEL_RC_OK) + { + WLog_ERR(TAG, "pVirtualChannelOpen failed with %s [%08" PRIX32 "]", + WTSErrorToString(status), status); + return status; + } + + if (context) + { + IFCALLRET(context->OnOpen, status, context, &rail->sendHandshake); + + if (status != CHANNEL_RC_OK) + WLog_ERR(TAG, "context->OnOpen failed with %s [%08" PRIX32 "]", + WTSErrorToString(status), status); + } + + rail->queue = MessageQueue_New(NULL); + + if (!rail->queue) + { + WLog_ERR(TAG, "MessageQueue_New failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + if (!(rail->thread = + CreateThread(NULL, 0, rail_virtual_channel_client_thread, (void*)rail, 0, NULL))) + { + WLog_ERR(TAG, "CreateThread failed!"); + MessageQueue_Free(rail->queue); + rail->queue = NULL; + return ERROR_INTERNAL_ERROR; + } + + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rail_virtual_channel_event_disconnected(railPlugin* rail) +{ + UINT rc; + + if (rail->OpenHandle == 0) + return CHANNEL_RC_OK; + + if (MessageQueue_PostQuit(rail->queue, 0) && + (WaitForSingleObject(rail->thread, INFINITE) == WAIT_FAILED)) + { + rc = GetLastError(); + WLog_ERR(TAG, "WaitForSingleObject failed with error %" PRIu32 "", rc); + return rc; + } + + MessageQueue_Free(rail->queue); + CloseHandle(rail->thread); + rail->queue = NULL; + rail->thread = NULL; + rc = rail->channelEntryPoints.pVirtualChannelCloseEx(rail->InitHandle, rail->OpenHandle); + + if (CHANNEL_RC_OK != rc) + { + WLog_ERR(TAG, "pVirtualChannelCloseEx failed with %s [%08" PRIX32 "]", WTSErrorToString(rc), + rc); + return rc; + } + + rail->OpenHandle = 0; + + if (rail->data_in) + { + Stream_Free(rail->data_in, TRUE); + rail->data_in = NULL; + } + + return CHANNEL_RC_OK; +} + +static void rail_virtual_channel_event_terminated(railPlugin* rail) +{ + rail->InitHandle = 0; + free(rail->context); + free(rail); +} + +static VOID VCAPITYPE rail_virtual_channel_init_event_ex(LPVOID lpUserParam, LPVOID pInitHandle, + UINT event, LPVOID pData, UINT dataLength) +{ + UINT error = CHANNEL_RC_OK; + railPlugin* rail = (railPlugin*)lpUserParam; + + if (!rail || (rail->InitHandle != pInitHandle)) + { + WLog_ERR(TAG, "error no match"); + return; + } + + switch (event) + { + case CHANNEL_EVENT_CONNECTED: + if ((error = rail_virtual_channel_event_connected(rail, pData, dataLength))) + WLog_ERR(TAG, "rail_virtual_channel_event_connected failed with error %" PRIu32 "!", + error); + + break; + + case CHANNEL_EVENT_DISCONNECTED: + if ((error = rail_virtual_channel_event_disconnected(rail))) + WLog_ERR(TAG, + "rail_virtual_channel_event_disconnected failed with error %" PRIu32 "!", + error); + + break; + + case CHANNEL_EVENT_TERMINATED: + rail_virtual_channel_event_terminated(rail); + break; + + case CHANNEL_EVENT_ATTACHED: + case CHANNEL_EVENT_DETACHED: + default: + break; + } + + if (error && rail->rdpcontext) + setChannelError(rail->rdpcontext, error, + "rail_virtual_channel_init_event_ex reported an error"); +} + +/* rail is always built-in */ +#define VirtualChannelEntryEx rail_VirtualChannelEntryEx + +BOOL VCAPITYPE VirtualChannelEntryEx(PCHANNEL_ENTRY_POINTS pEntryPoints, PVOID pInitHandle) +{ + UINT rc; + railPlugin* rail; + RailClientContext* context = NULL; + CHANNEL_ENTRY_POINTS_FREERDP_EX* pEntryPointsEx; + BOOL isFreerdp = FALSE; + rail = (railPlugin*)calloc(1, sizeof(railPlugin)); + + if (!rail) + { + WLog_ERR(TAG, "calloc failed!"); + return FALSE; + } + + /* Default to automatically replying to server handshakes */ + rail->sendHandshake = TRUE; + rail->channelDef.options = CHANNEL_OPTION_INITIALIZED | CHANNEL_OPTION_ENCRYPT_RDP | + CHANNEL_OPTION_COMPRESS_RDP | CHANNEL_OPTION_SHOW_PROTOCOL; + sprintf_s(rail->channelDef.name, ARRAYSIZE(rail->channelDef.name), RAIL_SVC_CHANNEL_NAME); + pEntryPointsEx = (CHANNEL_ENTRY_POINTS_FREERDP_EX*)pEntryPoints; + + if ((pEntryPointsEx->cbSize >= sizeof(CHANNEL_ENTRY_POINTS_FREERDP_EX)) && + (pEntryPointsEx->MagicNumber == FREERDP_CHANNEL_MAGIC_NUMBER)) + { + context = (RailClientContext*)calloc(1, sizeof(RailClientContext)); + + if (!context) + { + WLog_ERR(TAG, "calloc failed!"); + free(rail); + return FALSE; + } + + context->handle = (void*)rail; + context->custom = NULL; + context->ClientExecute = rail_client_execute; + context->ClientActivate = rail_client_activate; + context->ClientSystemParam = rail_client_system_param; + context->ClientSystemCommand = rail_client_system_command; + context->ClientHandshake = rail_client_handshake; + context->ClientNotifyEvent = rail_client_notify_event; + context->ClientWindowMove = rail_client_window_move; + context->ClientInformation = rail_client_information; + context->ClientSystemMenu = rail_client_system_menu; + context->ClientLanguageBarInfo = rail_client_language_bar_info; + context->ClientLanguageIMEInfo = rail_client_language_ime_info; + context->ClientGetAppIdRequest = rail_client_get_appid_request; + context->ClientSnapArrange = rail_client_snap_arrange; + context->ClientCloak = rail_client_cloak; + context->ClientCompartmentInfo = rail_client_compartment_info; + rail->rdpcontext = pEntryPointsEx->context; + rail->context = context; + isFreerdp = TRUE; + } + + rail->log = WLog_Get("com.freerdp.channels.rail.client"); + WLog_Print(rail->log, WLOG_DEBUG, "VirtualChannelEntryEx"); + CopyMemory(&(rail->channelEntryPoints), pEntryPoints, sizeof(CHANNEL_ENTRY_POINTS_FREERDP_EX)); + rail->InitHandle = pInitHandle; + rc = rail->channelEntryPoints.pVirtualChannelInitEx( + rail, context, pInitHandle, &rail->channelDef, 1, VIRTUAL_CHANNEL_VERSION_WIN2000, + rail_virtual_channel_init_event_ex); + + if (CHANNEL_RC_OK != rc) + { + WLog_ERR(TAG, "failed with %s [%08" PRIX32 "]", WTSErrorToString(rc), rc); + goto error_out; + } + + rail->channelEntryPoints.pInterface = context; + return TRUE; +error_out: + + if (isFreerdp) + free(rail->context); + + free(rail); + return FALSE; +} diff --git a/channels/rail/client/rail_main.h b/channels/rail/client/rail_main.h new file mode 100644 index 0000000..63e522e --- /dev/null +++ b/channels/rail/client/rail_main.h @@ -0,0 +1,63 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * RAIL Virtual Channel Plugin + * + * Copyright 2011 Marc-Andre Moreau + * Copyright 2011 Roman Barabanov + * Copyright 2011 Vic Lee + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger + * + * Licensed 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 FREERDP_CHANNEL_RAIL_CLIENT_MAIN_H +#define FREERDP_CHANNEL_RAIL_CLIENT_MAIN_H + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "../rail_common.h" + +struct rail_plugin +{ + CHANNEL_DEF channelDef; + CHANNEL_ENTRY_POINTS_FREERDP_EX channelEntryPoints; + + RailClientContext* context; + + wLog* log; + HANDLE thread; + wStream* data_in; + void* InitHandle; + DWORD OpenHandle; + wMessageQueue* queue; + rdpContext* rdpcontext; + DWORD channelBuildNumber; + DWORD channelFlags; + RAIL_CLIENT_STATUS_ORDER clientStatus; + BOOL sendHandshake; +}; +typedef struct rail_plugin railPlugin; + +RailClientContext* rail_get_client_interface(railPlugin* rail); +UINT rail_send_channel_data(railPlugin* rail, wStream* s); + +#endif /* FREERDP_CHANNEL_RAIL_CLIENT_MAIN_H */ diff --git a/channels/rail/client/rail_orders.c b/channels/rail/client/rail_orders.c new file mode 100644 index 0000000..4070b0f --- /dev/null +++ b/channels/rail/client/rail_orders.c @@ -0,0 +1,1591 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Remote Applications Integrated Locally (RAIL) Orders + * + * Copyright 2009 Marc-Andre Moreau + * Copyright 2011 Roman Barabanov + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger + * Copyright 2017 Armin Novak + * Copyright 2017 Thincast Technologies GmbH + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include + +#include + +#include "rail_orders.h" + +static BOOL rail_is_feature_supported(const rdpContext* context, UINT32 featureMask); + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT rail_send_pdu(railPlugin* rail, wStream* s, UINT16 orderType) +{ + char buffer[128] = { 0 }; + UINT16 orderLength; + + if (!rail || !s) + return ERROR_INVALID_PARAMETER; + + orderLength = (UINT16)Stream_GetPosition(s); + Stream_SetPosition(s, 0); + rail_write_pdu_header(s, orderType, orderLength); + Stream_SetPosition(s, orderLength); + WLog_Print(rail->log, WLOG_DEBUG, "Sending %s PDU, length: %" PRIu16 "", + rail_get_order_type_string_full(orderType, buffer, sizeof(buffer)), orderLength); + return rail_send_channel_data(rail, s); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rail_read_server_exec_result_order(wStream* s, RAIL_EXEC_RESULT_ORDER* execResult) +{ + if (!s || !execResult) + return ERROR_INVALID_PARAMETER; + + if (Stream_GetRemainingLength(s) < RAIL_EXEC_RESULT_ORDER_LENGTH) + { + WLog_ERR(TAG, "Stream_GetRemainingLength failed!"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT16(s, execResult->flags); /* flags (2 bytes) */ + Stream_Read_UINT16(s, execResult->execResult); /* execResult (2 bytes) */ + Stream_Read_UINT32(s, execResult->rawResult); /* rawResult (4 bytes) */ + Stream_Seek_UINT16(s); /* padding (2 bytes) */ + return rail_read_unicode_string(s, &execResult->exeOrFile) + ? CHANNEL_RC_OK + : ERROR_INTERNAL_ERROR; /* exeOrFile */ +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rail_read_server_minmaxinfo_order(wStream* s, RAIL_MINMAXINFO_ORDER* minmaxinfo) +{ + if (!s || !minmaxinfo) + return ERROR_INVALID_PARAMETER; + + if (Stream_GetRemainingLength(s) < RAIL_MINMAXINFO_ORDER_LENGTH) + { + WLog_ERR(TAG, "Stream_GetRemainingLength failed!"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT32(s, minmaxinfo->windowId); /* windowId (4 bytes) */ + Stream_Read_INT16(s, minmaxinfo->maxWidth); /* maxWidth (2 bytes) */ + Stream_Read_INT16(s, minmaxinfo->maxHeight); /* maxHeight (2 bytes) */ + Stream_Read_INT16(s, minmaxinfo->maxPosX); /* maxPosX (2 bytes) */ + Stream_Read_INT16(s, minmaxinfo->maxPosY); /* maxPosY (2 bytes) */ + Stream_Read_INT16(s, minmaxinfo->minTrackWidth); /* minTrackWidth (2 bytes) */ + Stream_Read_INT16(s, minmaxinfo->minTrackHeight); /* minTrackHeight (2 bytes) */ + Stream_Read_INT16(s, minmaxinfo->maxTrackWidth); /* maxTrackWidth (2 bytes) */ + Stream_Read_INT16(s, minmaxinfo->maxTrackHeight); /* maxTrackHeight (2 bytes) */ + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rail_read_server_localmovesize_order(wStream* s, + RAIL_LOCALMOVESIZE_ORDER* localMoveSize) +{ + UINT16 isMoveSizeStart; + + if (!s || !localMoveSize) + return ERROR_INVALID_PARAMETER; + + if (Stream_GetRemainingLength(s) < RAIL_LOCALMOVESIZE_ORDER_LENGTH) + { + WLog_ERR(TAG, "Stream_GetRemainingLength failed!"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT32(s, localMoveSize->windowId); /* windowId (4 bytes) */ + Stream_Read_UINT16(s, isMoveSizeStart); /* isMoveSizeStart (2 bytes) */ + localMoveSize->isMoveSizeStart = (isMoveSizeStart != 0) ? TRUE : FALSE; + Stream_Read_UINT16(s, localMoveSize->moveSizeType); /* moveSizeType (2 bytes) */ + Stream_Read_INT16(s, localMoveSize->posX); /* posX (2 bytes) */ + Stream_Read_INT16(s, localMoveSize->posY); /* posY (2 bytes) */ + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rail_read_server_get_appid_resp_order(wStream* s, + RAIL_GET_APPID_RESP_ORDER* getAppidResp) +{ + if (!s || !getAppidResp) + return ERROR_INVALID_PARAMETER; + + if (Stream_GetRemainingLength(s) < RAIL_GET_APPID_RESP_ORDER_LENGTH) + { + WLog_ERR(TAG, "Stream_GetRemainingLength failed!"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT32(s, getAppidResp->windowId); /* windowId (4 bytes) */ + Stream_Read_UTF16_String( + s, getAppidResp->applicationId, + ARRAYSIZE(getAppidResp->applicationId)); /* applicationId (260 UNICODE chars) */ + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rail_read_langbar_info_order(wStream* s, RAIL_LANGBAR_INFO_ORDER* langbarInfo) +{ + if (!s || !langbarInfo) + return ERROR_INVALID_PARAMETER; + + if (Stream_GetRemainingLength(s) < RAIL_LANGBAR_INFO_ORDER_LENGTH) + { + WLog_ERR(TAG, "Stream_GetRemainingLength failed!"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT32(s, langbarInfo->languageBarStatus); /* languageBarStatus (4 bytes) */ + return CHANNEL_RC_OK; +} + +static UINT rail_write_client_status_order(wStream* s, const RAIL_CLIENT_STATUS_ORDER* clientStatus) +{ + if (!s || !clientStatus) + return ERROR_INVALID_PARAMETER; + + Stream_Write_UINT32(s, clientStatus->flags); /* flags (4 bytes) */ + return ERROR_SUCCESS; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rail_write_client_exec_order(wStream* s, UINT16 flags, + const RAIL_UNICODE_STRING* exeOrFile, + const RAIL_UNICODE_STRING* workingDir, + const RAIL_UNICODE_STRING* arguments) +{ + UINT error; + + if (!s || !exeOrFile || !workingDir || !arguments) + return ERROR_INVALID_PARAMETER; + + /* [MS-RDPERP] 2.2.2.3.1 Client Execute PDU (TS_RAIL_ORDER_EXEC) + * Check argument limits */ + if ((exeOrFile->length > 520) || (workingDir->length > 520) || (arguments->length > 16000)) + { + WLog_ERR(TAG, + "TS_RAIL_ORDER_EXEC argument limits exceeded: ExeOrFile=%" PRIu16 + " [max=520], WorkingDir=%" PRIu16 " [max=520], Arguments=%" PRIu16 " [max=16000]", + exeOrFile->length, workingDir->length, arguments->length); + return ERROR_BAD_ARGUMENTS; + } + + Stream_Write_UINT16(s, flags); /* flags (2 bytes) */ + Stream_Write_UINT16(s, exeOrFile->length); /* exeOrFileLength (2 bytes) */ + Stream_Write_UINT16(s, workingDir->length); /* workingDirLength (2 bytes) */ + Stream_Write_UINT16(s, arguments->length); /* argumentsLength (2 bytes) */ + + if ((error = rail_write_unicode_string_value(s, exeOrFile))) + { + WLog_ERR(TAG, "rail_write_unicode_string_value failed with error %" PRIu32 "", error); + return error; + } + + if ((error = rail_write_unicode_string_value(s, workingDir))) + { + WLog_ERR(TAG, "rail_write_unicode_string_value failed with error %" PRIu32 "", error); + return error; + } + + if ((error = rail_write_unicode_string_value(s, arguments))) + { + WLog_ERR(TAG, "rail_write_unicode_string_value failed with error %" PRIu32 "", error); + return error; + } + + return error; +} + +static UINT rail_write_client_activate_order(wStream* s, const RAIL_ACTIVATE_ORDER* activate) +{ + BYTE enabled; + + if (!s || !activate) + return ERROR_INVALID_PARAMETER; + + Stream_Write_UINT32(s, activate->windowId); /* windowId (4 bytes) */ + enabled = activate->enabled ? 1 : 0; + Stream_Write_UINT8(s, enabled); /* enabled (1 byte) */ + return ERROR_SUCCESS; +} + +static UINT rail_write_client_sysmenu_order(wStream* s, const RAIL_SYSMENU_ORDER* sysmenu) +{ + if (!s || !sysmenu) + return ERROR_INVALID_PARAMETER; + + Stream_Write_UINT32(s, sysmenu->windowId); /* windowId (4 bytes) */ + Stream_Write_INT16(s, sysmenu->left); /* left (2 bytes) */ + Stream_Write_INT16(s, sysmenu->top); /* top (2 bytes) */ + return ERROR_SUCCESS; +} + +static UINT rail_write_client_syscommand_order(wStream* s, const RAIL_SYSCOMMAND_ORDER* syscommand) +{ + if (!s || !syscommand) + return ERROR_INVALID_PARAMETER; + + Stream_Write_UINT32(s, syscommand->windowId); /* windowId (4 bytes) */ + Stream_Write_UINT16(s, syscommand->command); /* command (2 bytes) */ + return ERROR_SUCCESS; +} + +static UINT rail_write_client_notify_event_order(wStream* s, + const RAIL_NOTIFY_EVENT_ORDER* notifyEvent) +{ + if (!s || !notifyEvent) + return ERROR_INVALID_PARAMETER; + + Stream_Write_UINT32(s, notifyEvent->windowId); /* windowId (4 bytes) */ + Stream_Write_UINT32(s, notifyEvent->notifyIconId); /* notifyIconId (4 bytes) */ + Stream_Write_UINT32(s, notifyEvent->message); /* notifyIconId (4 bytes) */ + return ERROR_SUCCESS; +} + +static UINT rail_write_client_window_move_order(wStream* s, + const RAIL_WINDOW_MOVE_ORDER* windowMove) +{ + if (!s || !windowMove) + return ERROR_INVALID_PARAMETER; + + Stream_Write_UINT32(s, windowMove->windowId); /* windowId (4 bytes) */ + Stream_Write_INT16(s, windowMove->left); /* left (2 bytes) */ + Stream_Write_INT16(s, windowMove->top); /* top (2 bytes) */ + Stream_Write_INT16(s, windowMove->right); /* right (2 bytes) */ + Stream_Write_INT16(s, windowMove->bottom); /* bottom (2 bytes) */ + return ERROR_SUCCESS; +} + +static UINT rail_write_client_get_appid_req_order(wStream* s, + const RAIL_GET_APPID_REQ_ORDER* getAppidReq) +{ + if (!s || !getAppidReq) + return ERROR_INVALID_PARAMETER; + + Stream_Write_UINT32(s, getAppidReq->windowId); /* windowId (4 bytes) */ + return ERROR_SUCCESS; +} + +static UINT rail_write_langbar_info_order(wStream* s, const RAIL_LANGBAR_INFO_ORDER* langbarInfo) +{ + if (!s || !langbarInfo) + return ERROR_INVALID_PARAMETER; + + Stream_Write_UINT32(s, langbarInfo->languageBarStatus); /* languageBarStatus (4 bytes) */ + return ERROR_SUCCESS; +} + +static UINT rail_write_languageime_info_order(wStream* s, + const RAIL_LANGUAGEIME_INFO_ORDER* langImeInfo) +{ + if (!s || !langImeInfo) + return ERROR_INVALID_PARAMETER; + + Stream_Write_UINT32(s, langImeInfo->ProfileType); + Stream_Write_UINT16(s, langImeInfo->LanguageID); + Stream_Write(s, &langImeInfo->LanguageProfileCLSID, sizeof(langImeInfo->LanguageProfileCLSID)); + Stream_Write(s, &langImeInfo->ProfileGUID, sizeof(langImeInfo->ProfileGUID)); + Stream_Write_UINT32(s, langImeInfo->KeyboardLayout); + return ERROR_SUCCESS; +} + +static UINT rail_write_compartment_info_order(wStream* s, + const RAIL_COMPARTMENT_INFO_ORDER* compartmentInfo) +{ + if (!s || !compartmentInfo) + return ERROR_INVALID_PARAMETER; + + Stream_Write_UINT32(s, compartmentInfo->ImeState); + Stream_Write_UINT32(s, compartmentInfo->ImeConvMode); + Stream_Write_UINT32(s, compartmentInfo->ImeSentenceMode); + Stream_Write_UINT32(s, compartmentInfo->KanaMode); + return ERROR_SUCCESS; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rail_recv_handshake_order(railPlugin* rail, wStream* s) +{ + RailClientContext* context = rail_get_client_interface(rail); + RAIL_HANDSHAKE_ORDER serverHandshake = { 0 }; + UINT error; + + if (!context || !s) + return ERROR_INVALID_PARAMETER; + + if ((error = rail_read_handshake_order(s, &serverHandshake))) + { + WLog_ERR(TAG, "rail_read_handshake_order failed with error %" PRIu32 "!", error); + return error; + } + + rail->channelBuildNumber = serverHandshake.buildNumber; + + if (rail->sendHandshake) + { + RAIL_HANDSHAKE_ORDER clientHandshake = { 0 }; + clientHandshake.buildNumber = 0x00001DB0; + error = context->ClientHandshake(context, &clientHandshake); + } + + if (error != CHANNEL_RC_OK) + return error; + + if (context->custom) + { + IFCALLRET(context->ServerHandshake, error, context, &serverHandshake); + + if (error) + WLog_ERR(TAG, "context.ServerHandshake failed with error %" PRIu32 "", error); + } + + return error; +} + +static UINT rail_read_compartment_info_order(wStream* s, + RAIL_COMPARTMENT_INFO_ORDER* compartmentInfo) +{ + if (!Stream_CheckAndLogRequiredLength(TAG, s, RAIL_COMPARTMENT_INFO_ORDER_LENGTH)) + return ERROR_INVALID_DATA; + + Stream_Read_UINT32(s, compartmentInfo->ImeState); /* ImeState (4 bytes) */ + Stream_Read_UINT32(s, compartmentInfo->ImeConvMode); /* ImeConvMode (4 bytes) */ + Stream_Read_UINT32(s, compartmentInfo->ImeSentenceMode); /* ImeSentenceMode (4 bytes) */ + Stream_Read_UINT32(s, compartmentInfo->KanaMode); /* KANAMode (4 bytes) */ + return CHANNEL_RC_OK; +} + +static UINT rail_recv_compartmentinfo_order(railPlugin* rail, wStream* s) +{ + RailClientContext* context = rail_get_client_interface(rail); + RAIL_COMPARTMENT_INFO_ORDER pdu = { 0 }; + UINT error; + + if (!context || !s) + return ERROR_INVALID_PARAMETER; + + if (!rail_is_feature_supported(rail->rdpcontext, RAIL_LEVEL_LANGUAGE_IME_SYNC_SUPPORTED)) + return ERROR_BAD_CONFIGURATION; + + if ((error = rail_read_compartment_info_order(s, &pdu))) + return error; + + if (context->custom) + { + IFCALLRET(context->ClientCompartmentInfo, error, context, &pdu); + + if (error) + WLog_ERR(TAG, "context.ClientCompartmentInfo failed with error %" PRIu32 "", error); + } + + return error; +} + +BOOL rail_is_feature_supported(const rdpContext* context, UINT32 featureMask) +{ + UINT32 supported, masked; + + if (!context || !context->settings) + return FALSE; + + supported = context->settings->RemoteApplicationSupportLevel & + context->settings->RemoteApplicationSupportMask; + masked = (supported & featureMask); + + if (masked != featureMask) + { + char mask[256] = { 0 }; + char actual[256] = { 0 }; + + WLog_WARN(TAG, "[%s] have %s, require %s", __func__, + freerdp_rail_support_flags_to_string(supported, actual, sizeof(actual)), + freerdp_rail_support_flags_to_string(featureMask, mask, sizeof(mask))); + return FALSE; + } + + return TRUE; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rail_recv_handshake_ex_order(railPlugin* rail, wStream* s) +{ + RailClientContext* context = rail_get_client_interface(rail); + RAIL_HANDSHAKE_EX_ORDER serverHandshake = { 0 }; + UINT error; + + if (!rail || !context || !s) + return ERROR_INVALID_PARAMETER; + + if (!rail_is_feature_supported(rail->rdpcontext, RAIL_LEVEL_HANDSHAKE_EX_SUPPORTED)) + return ERROR_BAD_CONFIGURATION; + + if ((error = rail_read_handshake_ex_order(s, &serverHandshake))) + { + WLog_ERR(TAG, "rail_read_handshake_ex_order failed with error %" PRIu32 "!", error); + return error; + } + + rail->channelBuildNumber = serverHandshake.buildNumber; + rail->channelFlags = serverHandshake.railHandshakeFlags; + + if (rail->sendHandshake) + { + RAIL_HANDSHAKE_ORDER clientHandshake = { 0 }; + clientHandshake.buildNumber = 0x00001DB0; + /* 2.2.2.2.3 HandshakeEx PDU (TS_RAIL_ORDER_HANDSHAKE_EX) + * Client response is really a Handshake PDU */ + error = context->ClientHandshake(context, &clientHandshake); + } + + if (error != CHANNEL_RC_OK) + return error; + + if (context->custom) + { + IFCALLRET(context->ServerHandshakeEx, error, context, &serverHandshake); + + if (error) + WLog_ERR(TAG, "context.ServerHandshakeEx failed with error %" PRIu32 "", error); + } + + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rail_recv_exec_result_order(railPlugin* rail, wStream* s) +{ + RailClientContext* context = rail_get_client_interface(rail); + RAIL_EXEC_RESULT_ORDER execResult = { 0 }; + UINT error; + + if (!context || !s) + return ERROR_INVALID_PARAMETER; + + if ((error = rail_read_server_exec_result_order(s, &execResult))) + { + WLog_ERR(TAG, "rail_read_server_exec_result_order failed with error %" PRIu32 "!", error); + goto fail; + } + + if (context->custom) + { + IFCALLRET(context->ServerExecuteResult, error, context, &execResult); + + if (error) + WLog_ERR(TAG, "context.ServerExecuteResult failed with error %" PRIu32 "", error); + } + +fail: + free(execResult.exeOrFile.string); + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rail_recv_server_sysparam_order(railPlugin* rail, wStream* s) +{ + RailClientContext* context = rail_get_client_interface(rail); + RAIL_SYSPARAM_ORDER sysparam; + UINT error; + BOOL extendedSpiSupported; + + if (!context || !s) + return ERROR_INVALID_PARAMETER; + + extendedSpiSupported = rail_is_extended_spi_supported(rail->channelFlags); + if ((error = rail_read_sysparam_order(s, &sysparam, extendedSpiSupported))) + { + WLog_ERR(TAG, "rail_read_sysparam_order failed with error %" PRIu32 "!", error); + return error; + } + + if (context->custom) + { + IFCALLRET(context->ServerSystemParam, error, context, &sysparam); + + if (error) + WLog_ERR(TAG, "context.ServerSystemParam failed with error %" PRIu32 "", error); + } + + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rail_recv_server_minmaxinfo_order(railPlugin* rail, wStream* s) +{ + RailClientContext* context = rail_get_client_interface(rail); + RAIL_MINMAXINFO_ORDER minMaxInfo = { 0 }; + UINT error; + + if (!context || !s) + return ERROR_INVALID_PARAMETER; + + if ((error = rail_read_server_minmaxinfo_order(s, &minMaxInfo))) + { + WLog_ERR(TAG, "rail_read_server_minmaxinfo_order failed with error %" PRIu32 "!", error); + return error; + } + + if (context->custom) + { + IFCALLRET(context->ServerMinMaxInfo, error, context, &minMaxInfo); + + if (error) + WLog_ERR(TAG, "context.ServerMinMaxInfo failed with error %" PRIu32 "", error); + } + + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rail_recv_server_localmovesize_order(railPlugin* rail, wStream* s) +{ + RailClientContext* context = rail_get_client_interface(rail); + RAIL_LOCALMOVESIZE_ORDER localMoveSize = { 0 }; + UINT error; + + if (!context || !s) + return ERROR_INVALID_PARAMETER; + + if ((error = rail_read_server_localmovesize_order(s, &localMoveSize))) + { + WLog_ERR(TAG, "rail_read_server_localmovesize_order failed with error %" PRIu32 "!", error); + return error; + } + + if (context->custom) + { + IFCALLRET(context->ServerLocalMoveSize, error, context, &localMoveSize); + + if (error) + WLog_ERR(TAG, "context.ServerLocalMoveSize failed with error %" PRIu32 "", error); + } + + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rail_recv_server_get_appid_resp_order(railPlugin* rail, wStream* s) +{ + RailClientContext* context = rail_get_client_interface(rail); + RAIL_GET_APPID_RESP_ORDER getAppIdResp = { 0 }; + UINT error; + + if (!context || !s) + return ERROR_INVALID_PARAMETER; + + if ((error = rail_read_server_get_appid_resp_order(s, &getAppIdResp))) + { + WLog_ERR(TAG, "rail_read_server_get_appid_resp_order failed with error %" PRIu32 "!", + error); + return error; + } + + if (context->custom) + { + IFCALLRET(context->ServerGetAppIdResponse, error, context, &getAppIdResp); + + if (error) + WLog_ERR(TAG, "context.ServerGetAppIdResponse failed with error %" PRIu32 "", error); + } + + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rail_recv_langbar_info_order(railPlugin* rail, wStream* s) +{ + RailClientContext* context = rail_get_client_interface(rail); + RAIL_LANGBAR_INFO_ORDER langBarInfo = { 0 }; + UINT error; + + if (!context) + return ERROR_INVALID_PARAMETER; + + if (!rail_is_feature_supported(rail->rdpcontext, RAIL_LEVEL_DOCKED_LANGBAR_SUPPORTED)) + return ERROR_BAD_CONFIGURATION; + + if ((error = rail_read_langbar_info_order(s, &langBarInfo))) + { + WLog_ERR(TAG, "rail_read_langbar_info_order failed with error %" PRIu32 "!", error); + return error; + } + + if (context->custom) + { + IFCALLRET(context->ServerLanguageBarInfo, error, context, &langBarInfo); + + if (error) + WLog_ERR(TAG, "context.ServerLanguageBarInfo failed with error %" PRIu32 "", error); + } + + return error; +} + +static UINT rail_read_taskbar_info_order(wStream* s, RAIL_TASKBAR_INFO_ORDER* taskbarInfo) +{ + if (!s || !taskbarInfo) + return ERROR_INVALID_PARAMETER; + + if (Stream_GetRemainingLength(s) < RAIL_TASKBAR_INFO_ORDER_LENGTH) + { + WLog_ERR(TAG, "Stream_GetRemainingLength failed!"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT32(s, taskbarInfo->TaskbarMessage); + Stream_Read_UINT32(s, taskbarInfo->WindowIdTab); + Stream_Read_UINT32(s, taskbarInfo->Body); + return CHANNEL_RC_OK; +} + +static UINT rail_recv_taskbar_info_order(railPlugin* rail, wStream* s) +{ + RailClientContext* context = rail_get_client_interface(rail); + RAIL_TASKBAR_INFO_ORDER taskBarInfo = { 0 }; + UINT error; + + if (!context) + return ERROR_INVALID_PARAMETER; + + /* 2.2.2.14.1 Taskbar Tab Info PDU (TS_RAIL_ORDER_TASKBARINFO) + * server -> client message only supported if announced. */ + if (!rail_is_feature_supported(rail->rdpcontext, RAIL_LEVEL_SHELL_INTEGRATION_SUPPORTED)) + return ERROR_BAD_CONFIGURATION; + + if ((error = rail_read_taskbar_info_order(s, &taskBarInfo))) + { + WLog_ERR(TAG, "rail_read_langbar_info_order failed with error %" PRIu32 "!", error); + return error; + } + + if (context->custom) + { + IFCALLRET(context->ServerTaskBarInfo, error, context, &taskBarInfo); + + if (error) + WLog_ERR(TAG, "context.ServerTaskBarInfo failed with error %" PRIu32 "", error); + } + + return error; +} + +static UINT rail_read_zorder_sync_order(wStream* s, RAIL_ZORDER_SYNC* zorder) +{ + if (!s || !zorder) + return ERROR_INVALID_PARAMETER; + + if (Stream_GetRemainingLength(s) < RAIL_Z_ORDER_SYNC_ORDER_LENGTH) + { + WLog_ERR(TAG, "Stream_GetRemainingLength failed!"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT32(s, zorder->windowIdMarker); + return CHANNEL_RC_OK; +} + +static UINT rail_recv_zorder_sync_order(railPlugin* rail, wStream* s) +{ + RailClientContext* context = rail_get_client_interface(rail); + RAIL_ZORDER_SYNC zorder = { 0 }; + UINT error; + + if (!context) + return ERROR_INVALID_PARAMETER; + + if ((rail->clientStatus.flags & TS_RAIL_CLIENTSTATUS_ZORDER_SYNC) == 0) + return ERROR_INVALID_DATA; + + if ((error = rail_read_zorder_sync_order(s, &zorder))) + { + WLog_ERR(TAG, "rail_read_zorder_sync_order failed with error %" PRIu32 "!", error); + return error; + } + + if (context->custom) + { + IFCALLRET(context->ServerZOrderSync, error, context, &zorder); + + if (error) + WLog_ERR(TAG, "context.ServerZOrderSync failed with error %" PRIu32 "", error); + } + + return error; +} + +static UINT rail_read_cloak_order(wStream* s, RAIL_CLOAK* cloak) +{ + BYTE cloaked; + + if (Stream_GetRemainingLength(s) < RAIL_CLOAK_ORDER_LENGTH) + { + WLog_ERR(TAG, "Stream_GetRemainingLength failed!"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT32(s, cloak->windowId); /* WindowId (4 bytes) */ + Stream_Read_UINT8(s, cloaked); /* Cloaked (1 byte) */ + cloak->cloak = (cloaked != 0) ? TRUE : FALSE; + return CHANNEL_RC_OK; +} + +static UINT rail_recv_cloak_order(railPlugin* rail, wStream* s) +{ + RailClientContext* context = rail_get_client_interface(rail); + RAIL_CLOAK cloak = { 0 }; + UINT error; + + if (!context) + return ERROR_INVALID_PARAMETER; + + /* 2.2.2.12.1 Window Cloak State Change PDU (TS_RAIL_ORDER_CLOAK) + * server -> client message only supported if announced. */ + if ((rail->clientStatus.flags & TS_RAIL_CLIENTSTATUS_BIDIRECTIONAL_CLOAK_SUPPORTED) == 0) + return ERROR_INVALID_DATA; + + if ((error = rail_read_cloak_order(s, &cloak))) + { + WLog_ERR(TAG, "rail_read_zorder_sync_order failed with error %" PRIu32 "!", error); + return error; + } + + if (context->custom) + { + IFCALLRET(context->ServerCloak, error, context, &cloak); + + if (error) + WLog_ERR(TAG, "context.ServerZOrderSync failed with error %" PRIu32 "", error); + } + + return error; +} + +static UINT rail_read_power_display_request_order(wStream* s, RAIL_POWER_DISPLAY_REQUEST* power) +{ + UINT32 active; + + if (!s || !power) + return ERROR_INVALID_PARAMETER; + + if (Stream_GetRemainingLength(s) < RAIL_POWER_DISPLAY_REQUEST_ORDER_LENGTH) + { + WLog_ERR(TAG, "Stream_GetRemainingLength failed!"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT32(s, active); + power->active = active != 0; + return CHANNEL_RC_OK; +} + +static UINT rail_recv_power_display_request_order(railPlugin* rail, wStream* s) +{ + RailClientContext* context = rail_get_client_interface(rail); + RAIL_POWER_DISPLAY_REQUEST power = { 0 }; + UINT error; + + if (!context) + return ERROR_INVALID_PARAMETER; + + /* 2.2.2.13.1 Power Display Request PDU(TS_RAIL_ORDER_POWER_DISPLAY_REQUEST) + */ + if ((rail->clientStatus.flags & TS_RAIL_CLIENTSTATUS_POWER_DISPLAY_REQUEST_SUPPORTED) == 0) + return ERROR_INVALID_DATA; + + if ((error = rail_read_power_display_request_order(s, &power))) + { + WLog_ERR(TAG, "rail_read_zorder_sync_order failed with error %" PRIu32 "!", error); + return error; + } + + if (context->custom) + { + IFCALLRET(context->ServerPowerDisplayRequest, error, context, &power); + + if (error) + WLog_ERR(TAG, "context.ServerPowerDisplayRequest failed with error %" PRIu32 "", error); + } + + return error; +} + +static UINT rail_read_get_application_id_extended_response_order(wStream* s, + RAIL_GET_APPID_RESP_EX* id) +{ + if (!s || !id) + return ERROR_INVALID_PARAMETER; + + if (Stream_GetRemainingLength(s) < 4) + { + WLog_ERR(TAG, "Stream_GetRemainingLength failed!"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT32(s, id->windowID); + + if (!Stream_Read_UTF16_String(s, id->applicationID, ARRAYSIZE(id->applicationID))) + return ERROR_INVALID_DATA; + + if (_wcsnlen(id->applicationID, ARRAYSIZE(id->applicationID)) >= ARRAYSIZE(id->applicationID)) + return ERROR_INVALID_DATA; + + if (Stream_GetRemainingLength(s) < 4) + { + WLog_ERR(TAG, "Stream_GetRemainingLength failed!"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT32(s, id->processId); + + if (!Stream_Read_UTF16_String(s, id->processImageName, ARRAYSIZE(id->processImageName))) + return ERROR_INVALID_DATA; + + if (_wcsnlen(id->applicationID, ARRAYSIZE(id->processImageName)) >= + ARRAYSIZE(id->processImageName)) + return ERROR_INVALID_DATA; + + return CHANNEL_RC_OK; +} + +static UINT rail_recv_get_application_id_extended_response_order(railPlugin* rail, wStream* s) +{ + RailClientContext* context = rail_get_client_interface(rail); + RAIL_GET_APPID_RESP_EX id = { 0 }; + UINT error; + + if (!context) + return ERROR_INVALID_PARAMETER; + + if ((error = rail_read_get_application_id_extended_response_order(s, &id))) + { + WLog_ERR(TAG, + "rail_read_get_application_id_extended_response_order failed with error %" PRIu32 + "!", + error); + return error; + } + + if (context->custom) + { + IFCALLRET(context->ServerGetAppidResponseExtended, error, context, &id); + + if (error) + WLog_ERR(TAG, "context.ServerGetAppidResponseExtended failed with error %" PRIu32 "", + error); + } + + return error; +} + +static UINT rail_read_textscaleinfo_order(wStream* s, UINT32* pTextScaleFactor) +{ + WINPR_ASSERT(pTextScaleFactor); + + if (!Stream_CheckAndLogRequiredLength(TAG, s, 4)) + return ERROR_INVALID_DATA; + + Stream_Read_UINT32(s, *pTextScaleFactor); + return CHANNEL_RC_OK; +} + +static UINT rail_recv_textscaleinfo_order(railPlugin* rail, wStream* s) +{ + RailClientContext* context = rail_get_client_interface(rail); + UINT32 TextScaleFactor = 0; + UINT error; + + if (!context) + return ERROR_INVALID_PARAMETER; + + if ((error = rail_read_textscaleinfo_order(s, &TextScaleFactor))) + return error; + + if (context->custom) + { + IFCALLRET(context->ClientTextScale, error, context, TextScaleFactor); + + if (error) + WLog_ERR(TAG, "context.ClientTextScale failed with error %" PRIu32 "", error); + } + + return error; +} + +static UINT rail_read_caretblinkinfo_order(wStream* s, UINT32* pCaretBlinkRate) +{ + WINPR_ASSERT(pCaretBlinkRate); + + if (!Stream_CheckAndLogRequiredLength(TAG, s, 4)) + return ERROR_INVALID_DATA; + + Stream_Read_UINT32(s, *pCaretBlinkRate); + return CHANNEL_RC_OK; +} + +static UINT rail_recv_caretblinkinfo_order(railPlugin* rail, wStream* s) +{ + RailClientContext* context = rail_get_client_interface(rail); + UINT32 CaretBlinkRate = 0; + UINT error; + + if (!context) + return ERROR_INVALID_PARAMETER; + if ((error = rail_read_caretblinkinfo_order(s, &CaretBlinkRate))) + return error; + + if (context->custom) + { + IFCALLRET(context->ClientCaretBlinkRate, error, context, CaretBlinkRate); + + if (error) + WLog_ERR(TAG, "context.ClientCaretBlinkRate failed with error %" PRIu32 "", error); + } + + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT rail_order_recv(railPlugin* rail, wStream* s) +{ + char buffer[128] = { 0 }; + UINT16 orderType; + UINT16 orderLength; + UINT error = CHANNEL_RC_OK; + + if (!rail || !s) + return ERROR_INVALID_PARAMETER; + + if ((error = rail_read_pdu_header(s, &orderType, &orderLength))) + { + WLog_ERR(TAG, "rail_read_pdu_header failed with error %" PRIu32 "!", error); + return error; + } + + WLog_Print(rail->log, WLOG_DEBUG, "Received %s PDU, length:%" PRIu16 "", + rail_get_order_type_string_full(orderType, buffer, sizeof(buffer)), orderLength); + + switch (orderType) + { + case TS_RAIL_ORDER_HANDSHAKE: + error = rail_recv_handshake_order(rail, s); + break; + + case TS_RAIL_ORDER_COMPARTMENTINFO: + error = rail_recv_compartmentinfo_order(rail, s); + break; + + case TS_RAIL_ORDER_HANDSHAKE_EX: + error = rail_recv_handshake_ex_order(rail, s); + break; + + case TS_RAIL_ORDER_EXEC_RESULT: + error = rail_recv_exec_result_order(rail, s); + break; + + case TS_RAIL_ORDER_SYSPARAM: + error = rail_recv_server_sysparam_order(rail, s); + break; + + case TS_RAIL_ORDER_MINMAXINFO: + error = rail_recv_server_minmaxinfo_order(rail, s); + break; + + case TS_RAIL_ORDER_LOCALMOVESIZE: + error = rail_recv_server_localmovesize_order(rail, s); + break; + + case TS_RAIL_ORDER_GET_APPID_RESP: + error = rail_recv_server_get_appid_resp_order(rail, s); + break; + + case TS_RAIL_ORDER_LANGBARINFO: + error = rail_recv_langbar_info_order(rail, s); + break; + + case TS_RAIL_ORDER_TASKBARINFO: + error = rail_recv_taskbar_info_order(rail, s); + break; + + case TS_RAIL_ORDER_ZORDER_SYNC: + error = rail_recv_zorder_sync_order(rail, s); + break; + + case TS_RAIL_ORDER_CLOAK: + error = rail_recv_cloak_order(rail, s); + break; + + case TS_RAIL_ORDER_POWER_DISPLAY_REQUEST: + error = rail_recv_power_display_request_order(rail, s); + break; + + case TS_RAIL_ORDER_GET_APPID_RESP_EX: + error = rail_recv_get_application_id_extended_response_order(rail, s); + break; + + case TS_RAIL_ORDER_TEXTSCALEINFO: + error = rail_recv_textscaleinfo_order(rail, s); + break; + + case TS_RAIL_ORDER_CARETBLINKINFO: + error = rail_recv_caretblinkinfo_order(rail, s); + break; + + default: + WLog_ERR(TAG, "Unknown RAIL PDU %s received.", + rail_get_order_type_string_full(orderType, buffer, sizeof(buffer))); + return ERROR_INVALID_DATA; + } + + if (error != CHANNEL_RC_OK) + { + char buffer[128] = { 0 }; + WLog_Print(rail->log, WLOG_ERROR, "Failed to process rail %s PDU, length:%" PRIu16 "", + rail_get_order_type_string_full(orderType, buffer, sizeof(buffer)), orderLength); + } + + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT rail_send_handshake_order(railPlugin* rail, const RAIL_HANDSHAKE_ORDER* handshake) +{ + wStream* s; + UINT error; + + if (!rail || !handshake) + return ERROR_INVALID_PARAMETER; + + s = rail_pdu_init(RAIL_HANDSHAKE_ORDER_LENGTH); + + if (!s) + { + WLog_ERR(TAG, "rail_pdu_init failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + rail_write_handshake_order(s, handshake); + error = rail_send_pdu(rail, s, TS_RAIL_ORDER_HANDSHAKE); + Stream_Free(s, TRUE); + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT rail_send_handshake_ex_order(railPlugin* rail, const RAIL_HANDSHAKE_EX_ORDER* handshakeEx) +{ + wStream* s; + UINT error; + + if (!rail || !handshakeEx) + return ERROR_INVALID_PARAMETER; + + s = rail_pdu_init(RAIL_HANDSHAKE_EX_ORDER_LENGTH); + + if (!s) + { + WLog_ERR(TAG, "rail_pdu_init failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + rail_write_handshake_ex_order(s, handshakeEx); + error = rail_send_pdu(rail, s, TS_RAIL_ORDER_HANDSHAKE_EX); + Stream_Free(s, TRUE); + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT rail_send_client_status_order(railPlugin* rail, const RAIL_CLIENT_STATUS_ORDER* clientStatus) +{ + wStream* s; + UINT error; + + if (!rail || !clientStatus) + return ERROR_INVALID_PARAMETER; + + rail->clientStatus = *clientStatus; + s = rail_pdu_init(RAIL_CLIENT_STATUS_ORDER_LENGTH); + + if (!s) + { + WLog_ERR(TAG, "rail_pdu_init failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + error = rail_write_client_status_order(s, clientStatus); + + if (error == ERROR_SUCCESS) + error = rail_send_pdu(rail, s, TS_RAIL_ORDER_CLIENTSTATUS); + + Stream_Free(s, TRUE); + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT rail_send_client_exec_order(railPlugin* rail, UINT16 flags, + const RAIL_UNICODE_STRING* exeOrFile, + const RAIL_UNICODE_STRING* workingDir, + const RAIL_UNICODE_STRING* arguments) +{ + wStream* s; + UINT error; + size_t length; + + if (!rail || !exeOrFile || !workingDir || !arguments) + return ERROR_INVALID_PARAMETER; + + length = RAIL_EXEC_ORDER_LENGTH + exeOrFile->length + workingDir->length + arguments->length; + s = rail_pdu_init(length); + + if (!s) + { + WLog_ERR(TAG, "rail_pdu_init failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + if ((error = rail_write_client_exec_order(s, flags, exeOrFile, workingDir, arguments))) + { + WLog_ERR(TAG, "rail_write_client_exec_order failed with error %" PRIu32 "!", error); + goto out; + } + + if ((error = rail_send_pdu(rail, s, TS_RAIL_ORDER_EXEC))) + { + WLog_ERR(TAG, "rail_send_pdu failed with error %" PRIu32 "!", error); + goto out; + } + +out: + Stream_Free(s, TRUE); + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT rail_send_client_activate_order(railPlugin* rail, const RAIL_ACTIVATE_ORDER* activate) +{ + wStream* s; + UINT error; + + if (!rail || !activate) + return ERROR_INVALID_PARAMETER; + + s = rail_pdu_init(RAIL_ACTIVATE_ORDER_LENGTH); + + if (!s) + { + WLog_ERR(TAG, "rail_pdu_init failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + error = rail_write_client_activate_order(s, activate); + + if (error == ERROR_SUCCESS) + error = rail_send_pdu(rail, s, TS_RAIL_ORDER_ACTIVATE); + + Stream_Free(s, TRUE); + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT rail_send_client_sysmenu_order(railPlugin* rail, const RAIL_SYSMENU_ORDER* sysmenu) +{ + wStream* s; + UINT error; + + if (!rail || !sysmenu) + return ERROR_INVALID_PARAMETER; + + s = rail_pdu_init(RAIL_SYSMENU_ORDER_LENGTH); + + if (!s) + { + WLog_ERR(TAG, "rail_pdu_init failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + error = rail_write_client_sysmenu_order(s, sysmenu); + + if (error == ERROR_SUCCESS) + error = rail_send_pdu(rail, s, TS_RAIL_ORDER_SYSMENU); + + Stream_Free(s, TRUE); + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT rail_send_client_syscommand_order(railPlugin* rail, const RAIL_SYSCOMMAND_ORDER* syscommand) +{ + wStream* s; + UINT error; + + if (!rail || !syscommand) + return ERROR_INVALID_PARAMETER; + + s = rail_pdu_init(RAIL_SYSCOMMAND_ORDER_LENGTH); + + if (!s) + { + WLog_ERR(TAG, "rail_pdu_init failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + error = rail_write_client_syscommand_order(s, syscommand); + + if (error == ERROR_SUCCESS) + error = rail_send_pdu(rail, s, TS_RAIL_ORDER_SYSCOMMAND); + + Stream_Free(s, TRUE); + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT rail_send_client_notify_event_order(railPlugin* rail, + const RAIL_NOTIFY_EVENT_ORDER* notifyEvent) +{ + wStream* s; + UINT error; + + if (!rail || !notifyEvent) + return ERROR_INVALID_PARAMETER; + + s = rail_pdu_init(RAIL_NOTIFY_EVENT_ORDER_LENGTH); + + if (!s) + { + WLog_ERR(TAG, "rail_pdu_init failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + error = rail_write_client_notify_event_order(s, notifyEvent); + + if (ERROR_SUCCESS == error) + error = rail_send_pdu(rail, s, TS_RAIL_ORDER_NOTIFY_EVENT); + + Stream_Free(s, TRUE); + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT rail_send_client_window_move_order(railPlugin* rail, const RAIL_WINDOW_MOVE_ORDER* windowMove) +{ + wStream* s; + UINT error; + + if (!rail || !windowMove) + return ERROR_INVALID_PARAMETER; + + s = rail_pdu_init(RAIL_WINDOW_MOVE_ORDER_LENGTH); + + if (!s) + { + WLog_ERR(TAG, "rail_pdu_init failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + error = rail_write_client_window_move_order(s, windowMove); + + if (error == ERROR_SUCCESS) + error = rail_send_pdu(rail, s, TS_RAIL_ORDER_WINDOWMOVE); + + Stream_Free(s, TRUE); + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT rail_send_client_get_appid_req_order(railPlugin* rail, + const RAIL_GET_APPID_REQ_ORDER* getAppIdReq) +{ + wStream* s; + UINT error; + + if (!rail || !getAppIdReq) + return ERROR_INVALID_PARAMETER; + + s = rail_pdu_init(RAIL_GET_APPID_REQ_ORDER_LENGTH); + + if (!s) + { + WLog_ERR(TAG, "rail_pdu_init failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + error = rail_write_client_get_appid_req_order(s, getAppIdReq); + + if (error == ERROR_SUCCESS) + error = rail_send_pdu(rail, s, TS_RAIL_ORDER_GET_APPID_REQ); + + Stream_Free(s, TRUE); + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT rail_send_client_langbar_info_order(railPlugin* rail, + const RAIL_LANGBAR_INFO_ORDER* langBarInfo) +{ + wStream* s; + UINT error; + + if (!rail || !langBarInfo) + return ERROR_INVALID_PARAMETER; + + if (!rail_is_feature_supported(rail->rdpcontext, RAIL_LEVEL_DOCKED_LANGBAR_SUPPORTED)) + return ERROR_BAD_CONFIGURATION; + + s = rail_pdu_init(RAIL_LANGBAR_INFO_ORDER_LENGTH); + + if (!s) + { + WLog_ERR(TAG, "rail_pdu_init failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + error = rail_write_langbar_info_order(s, langBarInfo); + + if (ERROR_SUCCESS == error) + error = rail_send_pdu(rail, s, TS_RAIL_ORDER_LANGBARINFO); + + Stream_Free(s, TRUE); + return error; +} + +UINT rail_send_client_languageime_info_order(railPlugin* rail, + const RAIL_LANGUAGEIME_INFO_ORDER* langImeInfo) +{ + wStream* s; + UINT error; + + if (!rail || !langImeInfo) + return ERROR_INVALID_PARAMETER; + + if (!rail_is_feature_supported(rail->rdpcontext, RAIL_LEVEL_LANGUAGE_IME_SYNC_SUPPORTED)) + return ERROR_BAD_CONFIGURATION; + + s = rail_pdu_init(RAIL_LANGUAGEIME_INFO_ORDER_LENGTH); + + if (!s) + { + WLog_ERR(TAG, "rail_pdu_init failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + error = rail_write_languageime_info_order(s, langImeInfo); + + if (ERROR_SUCCESS == error) + error = rail_send_pdu(rail, s, TS_RAIL_ORDER_LANGUAGEIMEINFO); + + Stream_Free(s, TRUE); + return error; +} + +UINT rail_send_client_compartment_info_order(railPlugin* rail, + const RAIL_COMPARTMENT_INFO_ORDER* compartmentInfo) +{ + wStream* s; + UINT error; + + if (!rail || !compartmentInfo) + return ERROR_INVALID_PARAMETER; + + if (!rail_is_feature_supported(rail->rdpcontext, RAIL_LEVEL_LANGUAGE_IME_SYNC_SUPPORTED)) + return ERROR_BAD_CONFIGURATION; + + s = rail_pdu_init(RAIL_COMPARTMENT_INFO_ORDER_LENGTH); + + if (!s) + { + WLog_ERR(TAG, "rail_pdu_init failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + error = rail_write_compartment_info_order(s, compartmentInfo); + + if (ERROR_SUCCESS == error) + error = rail_send_pdu(rail, s, TS_RAIL_ORDER_COMPARTMENTINFO); + + Stream_Free(s, TRUE); + return error; +} + +UINT rail_send_client_cloak_order(railPlugin* rail, const RAIL_CLOAK* cloak) +{ + wStream* s; + UINT error; + + if (!rail || !cloak) + return ERROR_INVALID_PARAMETER; + + s = rail_pdu_init(5); + + if (!s) + { + WLog_ERR(TAG, "rail_pdu_init failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + Stream_Write_UINT32(s, cloak->windowId); + Stream_Write_UINT8(s, cloak->cloak ? 1 : 0); + error = rail_send_pdu(rail, s, TS_RAIL_ORDER_CLOAK); + Stream_Free(s, TRUE); + return error; +} + +UINT rail_send_client_snap_arrange_order(railPlugin* rail, const RAIL_SNAP_ARRANGE* snap) +{ + wStream* s; + UINT error; + + if (!rail) + return ERROR_INVALID_PARAMETER; + + /* 2.2.2.7.5 Client Window Snap PDU (TS_RAIL_ORDER_SNAP_ARRANGE) */ + if ((rail->channelFlags & TS_RAIL_ORDER_HANDSHAKE_EX_FLAGS_SNAP_ARRANGE_SUPPORTED) == 0) + { + RAIL_WINDOW_MOVE_ORDER move = { 0 }; + move.top = snap->top; + move.left = snap->left; + move.right = snap->right; + move.bottom = snap->bottom; + move.windowId = snap->windowId; + return rail_send_client_window_move_order(rail, &move); + } + + s = rail_pdu_init(12); + + if (!s) + { + WLog_ERR(TAG, "rail_pdu_init failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + Stream_Write_UINT32(s, snap->windowId); + Stream_Write_INT16(s, snap->left); + Stream_Write_INT16(s, snap->top); + Stream_Write_INT16(s, snap->right); + Stream_Write_INT16(s, snap->bottom); + error = rail_send_pdu(rail, s, TS_RAIL_ORDER_SNAP_ARRANGE); + Stream_Free(s, TRUE); + return error; +} diff --git a/channels/rail/client/rail_orders.h b/channels/rail/client/rail_orders.h new file mode 100644 index 0000000..89ba6cc --- /dev/null +++ b/channels/rail/client/rail_orders.h @@ -0,0 +1,60 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Remote Applications Integrated Locally (RAIL) + * + * Copyright 2009 Marc-Andre Moreau + * Copyright 2011 Roman Barabanov + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger + * + * Licensed 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 FREERDP_CHANNEL_RAIL_CLIENT_ORDERS_H +#define FREERDP_CHANNEL_RAIL_CLIENT_ORDERS_H + +#include + +#include "rail_main.h" + +#define TAG CHANNELS_TAG("rail.client") + +UINT rail_order_recv(railPlugin* rail, wStream* s); +UINT rail_send_pdu(railPlugin* rail, wStream* s, UINT16 orderType); + +UINT rail_send_handshake_order(railPlugin* rail, const RAIL_HANDSHAKE_ORDER* handshake); +UINT rail_send_handshake_ex_order(railPlugin* rail, const RAIL_HANDSHAKE_EX_ORDER* handshakeEx); +UINT rail_send_client_status_order(railPlugin* rail, const RAIL_CLIENT_STATUS_ORDER* clientStatus); +UINT rail_send_client_exec_order(railPlugin* rail, UINT16 flags, + const RAIL_UNICODE_STRING* exeOrFile, + const RAIL_UNICODE_STRING* workingDir, + const RAIL_UNICODE_STRING* arguments); +UINT rail_send_client_activate_order(railPlugin* rail, const RAIL_ACTIVATE_ORDER* activate); +UINT rail_send_client_sysmenu_order(railPlugin* rail, const RAIL_SYSMENU_ORDER* sysmenu); +UINT rail_send_client_syscommand_order(railPlugin* rail, const RAIL_SYSCOMMAND_ORDER* syscommand); + +UINT rail_send_client_notify_event_order(railPlugin* rail, + const RAIL_NOTIFY_EVENT_ORDER* notifyEvent); +UINT rail_send_client_window_move_order(railPlugin* rail, const RAIL_WINDOW_MOVE_ORDER* windowMove); +UINT rail_send_client_get_appid_req_order(railPlugin* rail, + const RAIL_GET_APPID_REQ_ORDER* getAppIdReq); +UINT rail_send_client_langbar_info_order(railPlugin* rail, + const RAIL_LANGBAR_INFO_ORDER* langBarInfo); +UINT rail_send_client_languageime_info_order(railPlugin* rail, + const RAIL_LANGUAGEIME_INFO_ORDER* langImeInfo); +UINT rail_send_client_cloak_order(railPlugin* rail, const RAIL_CLOAK* cloak); +UINT rail_send_client_snap_arrange_order(railPlugin* rail, const RAIL_SNAP_ARRANGE* snap); +UINT rail_send_client_compartment_info_order(railPlugin* rail, + const RAIL_COMPARTMENT_INFO_ORDER* compartmentInfo); + +#endif /* FREERDP_CHANNEL_RAIL_CLIENT_ORDERS_H */ diff --git a/channels/rail/rail_common.c b/channels/rail/rail_common.c new file mode 100644 index 0000000..e77616d --- /dev/null +++ b/channels/rail/rail_common.c @@ -0,0 +1,618 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * RAIL common functions + * + * Copyright 2011 Marc-Andre Moreau + * Copyright 2011 Roman Barabanov + * Copyright 2011 Vic Lee + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger + * + * Licensed 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 "rail_common.h" + +#include +#include + +#define TAG CHANNELS_TAG("rail.common") + +const char* rail_get_order_type_string(UINT16 orderType) +{ + switch (orderType) + { + case TS_RAIL_ORDER_EXEC: + return "TS_RAIL_ORDER_EXEC"; + case TS_RAIL_ORDER_ACTIVATE: + return "TS_RAIL_ORDER_ACTIVATE"; + case TS_RAIL_ORDER_SYSPARAM: + return "TS_RAIL_ORDER_SYSPARAM"; + case TS_RAIL_ORDER_SYSCOMMAND: + return "TS_RAIL_ORDER_SYSCOMMAND"; + case TS_RAIL_ORDER_HANDSHAKE: + return "TS_RAIL_ORDER_HANDSHAKE"; + case TS_RAIL_ORDER_NOTIFY_EVENT: + return "TS_RAIL_ORDER_NOTIFY_EVENT"; + case TS_RAIL_ORDER_WINDOWMOVE: + return "TS_RAIL_ORDER_WINDOWMOVE"; + case TS_RAIL_ORDER_LOCALMOVESIZE: + return "TS_RAIL_ORDER_LOCALMOVESIZE"; + case TS_RAIL_ORDER_MINMAXINFO: + return "TS_RAIL_ORDER_MINMAXINFO"; + case TS_RAIL_ORDER_CLIENTSTATUS: + return "TS_RAIL_ORDER_CLIENTSTATUS"; + case TS_RAIL_ORDER_SYSMENU: + return "TS_RAIL_ORDER_SYSMENU"; + case TS_RAIL_ORDER_LANGBARINFO: + return "TS_RAIL_ORDER_LANGBARINFO"; + case TS_RAIL_ORDER_GET_APPID_REQ: + return "TS_RAIL_ORDER_GET_APPID_REQ"; + case TS_RAIL_ORDER_GET_APPID_RESP: + return "TS_RAIL_ORDER_GET_APPID_RESP"; + case TS_RAIL_ORDER_TASKBARINFO: + return "TS_RAIL_ORDER_TASKBARINFO"; + case TS_RAIL_ORDER_LANGUAGEIMEINFO: + return "TS_RAIL_ORDER_LANGUAGEIMEINFO"; + case TS_RAIL_ORDER_COMPARTMENTINFO: + return "TS_RAIL_ORDER_COMPARTMENTINFO"; + case TS_RAIL_ORDER_HANDSHAKE_EX: + return "TS_RAIL_ORDER_HANDSHAKE_EX"; + case TS_RAIL_ORDER_ZORDER_SYNC: + return "TS_RAIL_ORDER_ZORDER_SYNC"; + case TS_RAIL_ORDER_CLOAK: + return "TS_RAIL_ORDER_CLOAK"; + case TS_RAIL_ORDER_POWER_DISPLAY_REQUEST: + return "TS_RAIL_ORDER_POWER_DISPLAY_REQUEST"; + case TS_RAIL_ORDER_SNAP_ARRANGE: + return "TS_RAIL_ORDER_SNAP_ARRANGE"; + case TS_RAIL_ORDER_GET_APPID_RESP_EX: + return "TS_RAIL_ORDER_GET_APPID_RESP_EX"; + case TS_RAIL_ORDER_EXEC_RESULT: + return "TS_RAIL_ORDER_EXEC_RESULT"; + case TS_RAIL_ORDER_TEXTSCALEINFO: + return "TS_RAIL_ORDER_TEXTSCALEINFO"; + case TS_RAIL_ORDER_CARETBLINKINFO: + return "TS_RAIL_ORDER_CARETBLINKINFO"; + default: + return "TS_RAIL_ORDER_UNKNOWN"; + } +} + +const char* rail_get_order_type_string_full(UINT16 orderType, char* buffer, size_t length) +{ + _snprintf(buffer, length, "%s[0x%04" PRIx16 "]", rail_get_order_type_string(orderType), + orderType); + return buffer; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT rail_read_pdu_header(wStream* s, UINT16* orderType, UINT16* orderLength) +{ + if (!s || !orderType || !orderLength) + return ERROR_INVALID_PARAMETER; + + if (Stream_GetRemainingLength(s) < 4) + return ERROR_INVALID_DATA; + + Stream_Read_UINT16(s, *orderType); /* orderType (2 bytes) */ + Stream_Read_UINT16(s, *orderLength); /* orderLength (2 bytes) */ + return CHANNEL_RC_OK; +} + +void rail_write_pdu_header(wStream* s, UINT16 orderType, UINT16 orderLength) +{ + Stream_Write_UINT16(s, orderType); /* orderType (2 bytes) */ + Stream_Write_UINT16(s, orderLength); /* orderLength (2 bytes) */ +} + +wStream* rail_pdu_init(size_t length) +{ + wStream* s; + s = Stream_New(NULL, length + RAIL_PDU_HEADER_LENGTH); + + if (!s) + return NULL; + + Stream_Seek(s, RAIL_PDU_HEADER_LENGTH); + return s; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT rail_read_handshake_order(wStream* s, RAIL_HANDSHAKE_ORDER* handshake) +{ + if (Stream_GetRemainingLength(s) < 4) + return ERROR_INVALID_DATA; + + Stream_Read_UINT32(s, handshake->buildNumber); /* buildNumber (4 bytes) */ + return CHANNEL_RC_OK; +} + +void rail_write_handshake_order(wStream* s, const RAIL_HANDSHAKE_ORDER* handshake) +{ + Stream_Write_UINT32(s, handshake->buildNumber); /* buildNumber (4 bytes) */ +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT rail_read_handshake_ex_order(wStream* s, RAIL_HANDSHAKE_EX_ORDER* handshakeEx) +{ + if (Stream_GetRemainingLength(s) < 8) + return ERROR_INVALID_DATA; + + Stream_Read_UINT32(s, handshakeEx->buildNumber); /* buildNumber (4 bytes) */ + Stream_Read_UINT32(s, handshakeEx->railHandshakeFlags); /* railHandshakeFlags (4 bytes) */ + return CHANNEL_RC_OK; +} + +void rail_write_handshake_ex_order(wStream* s, const RAIL_HANDSHAKE_EX_ORDER* handshakeEx) +{ + Stream_Write_UINT32(s, handshakeEx->buildNumber); /* buildNumber (4 bytes) */ + Stream_Write_UINT32(s, handshakeEx->railHandshakeFlags); /* railHandshakeFlags (4 bytes) */ +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT rail_write_unicode_string(wStream* s, const RAIL_UNICODE_STRING* unicode_string) +{ + if (!s || !unicode_string) + return ERROR_INVALID_PARAMETER; + + if (!Stream_EnsureRemainingCapacity(s, 2 + unicode_string->length)) + { + WLog_ERR(TAG, "Stream_EnsureRemainingCapacity failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + Stream_Write_UINT16(s, unicode_string->length); /* cbString (2 bytes) */ + Stream_Write(s, unicode_string->string, unicode_string->length); /* string */ + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT rail_write_unicode_string_value(wStream* s, const RAIL_UNICODE_STRING* unicode_string) +{ + size_t length; + + if (!s || !unicode_string) + return ERROR_INVALID_PARAMETER; + + length = unicode_string->length; + + if (length > 0) + { + if (!Stream_EnsureRemainingCapacity(s, length)) + { + WLog_ERR(TAG, "Stream_EnsureRemainingCapacity failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + Stream_Write(s, unicode_string->string, length); /* string */ + } + + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rail_read_high_contrast(wStream* s, RAIL_HIGH_CONTRAST* highContrast) +{ + if (!s || !highContrast) + return ERROR_INVALID_PARAMETER; + + if (Stream_GetRemainingLength(s) < 8) + return ERROR_INVALID_DATA; + + Stream_Read_UINT32(s, highContrast->flags); /* flags (4 bytes) */ + Stream_Read_UINT32(s, highContrast->colorSchemeLength); /* colorSchemeLength (4 bytes) */ + + if (!rail_read_unicode_string(s, &highContrast->colorScheme)) /* colorScheme */ + return ERROR_INTERNAL_ERROR; + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rail_write_high_contrast(wStream* s, const RAIL_HIGH_CONTRAST* highContrast) +{ + UINT32 colorSchemeLength; + + if (!s || !highContrast) + return ERROR_INVALID_PARAMETER; + + if (!Stream_EnsureRemainingCapacity(s, 8)) + return CHANNEL_RC_NO_MEMORY; + + colorSchemeLength = highContrast->colorScheme.length + 2; + Stream_Write_UINT32(s, highContrast->flags); /* flags (4 bytes) */ + Stream_Write_UINT32(s, colorSchemeLength); /* colorSchemeLength (4 bytes) */ + return rail_write_unicode_string(s, &highContrast->colorScheme); /* colorScheme */ +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rail_read_filterkeys(wStream* s, TS_FILTERKEYS* filterKeys) +{ + if (!s || !filterKeys) + return ERROR_INVALID_PARAMETER; + + if (Stream_GetRemainingLength(s) < 20) + return ERROR_INVALID_DATA; + + Stream_Read_UINT32(s, filterKeys->Flags); + Stream_Read_UINT32(s, filterKeys->WaitTime); + Stream_Read_UINT32(s, filterKeys->DelayTime); + Stream_Read_UINT32(s, filterKeys->RepeatTime); + Stream_Read_UINT32(s, filterKeys->BounceTime); + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rail_write_filterkeys(wStream* s, const TS_FILTERKEYS* filterKeys) +{ + if (!s || !filterKeys) + return ERROR_INVALID_PARAMETER; + + if (!Stream_EnsureRemainingCapacity(s, 20)) + return CHANNEL_RC_NO_MEMORY; + + Stream_Write_UINT32(s, filterKeys->Flags); + Stream_Write_UINT32(s, filterKeys->WaitTime); + Stream_Write_UINT32(s, filterKeys->DelayTime); + Stream_Write_UINT32(s, filterKeys->RepeatTime); + Stream_Write_UINT32(s, filterKeys->BounceTime); + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT rail_read_sysparam_order(wStream* s, RAIL_SYSPARAM_ORDER* sysparam, BOOL extendedSpiSupported) +{ + BYTE body; + UINT error = CHANNEL_RC_OK; + + if (!s || !sysparam) + return ERROR_INVALID_PARAMETER; + + if (Stream_GetRemainingLength(s) < 5) + { + WLog_ERR(TAG, "Stream_GetRemainingLength failed!"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT32(s, sysparam->param); /* systemParam (4 bytes) */ + + sysparam->params = 0; /* bitflags of received params */ + + switch (sysparam->param) + { + /* Client sysparams */ + case SPI_SET_DRAG_FULL_WINDOWS: + sysparam->params |= SPI_MASK_SET_DRAG_FULL_WINDOWS; + Stream_Read_UINT8(s, body); /* body (1 byte) */ + sysparam->dragFullWindows = body != 0; + break; + + case SPI_SET_KEYBOARD_CUES: + sysparam->params |= SPI_MASK_SET_KEYBOARD_CUES; + Stream_Read_UINT8(s, body); /* body (1 byte) */ + sysparam->keyboardCues = body != 0; + break; + + case SPI_SET_KEYBOARD_PREF: + sysparam->params |= SPI_MASK_SET_KEYBOARD_PREF; + Stream_Read_UINT8(s, body); /* body (1 byte) */ + sysparam->keyboardPref = body != 0; + break; + + case SPI_SET_MOUSE_BUTTON_SWAP: + sysparam->params |= SPI_MASK_SET_MOUSE_BUTTON_SWAP; + Stream_Read_UINT8(s, body); /* body (1 byte) */ + sysparam->mouseButtonSwap = body != 0; + break; + + case SPI_SET_WORK_AREA: + sysparam->params |= SPI_MASK_SET_WORK_AREA; + + if (Stream_GetRemainingLength(s) < 8) + { + WLog_ERR(TAG, "Stream_GetRemainingLength failed!"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT16(s, sysparam->workArea.left); /* left (2 bytes) */ + Stream_Read_UINT16(s, sysparam->workArea.top); /* top (2 bytes) */ + Stream_Read_UINT16(s, sysparam->workArea.right); /* right (2 bytes) */ + Stream_Read_UINT16(s, sysparam->workArea.bottom); /* bottom (2 bytes) */ + break; + + case SPI_DISPLAY_CHANGE: + sysparam->params |= SPI_MASK_DISPLAY_CHANGE; + + if (Stream_GetRemainingLength(s) < 8) + { + WLog_ERR(TAG, "Stream_GetRemainingLength failed!"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT16(s, sysparam->displayChange.left); /* left (2 bytes) */ + Stream_Read_UINT16(s, sysparam->displayChange.top); /* top (2 bytes) */ + Stream_Read_UINT16(s, sysparam->displayChange.right); /* right (2 bytes) */ + Stream_Read_UINT16(s, sysparam->displayChange.bottom); /* bottom (2 bytes) */ + break; + + case SPI_TASKBAR_POS: + sysparam->params |= SPI_MASK_TASKBAR_POS; + + if (Stream_GetRemainingLength(s) < 8) + { + WLog_ERR(TAG, "Stream_GetRemainingLength failed!"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT16(s, sysparam->taskbarPos.left); /* left (2 bytes) */ + Stream_Read_UINT16(s, sysparam->taskbarPos.top); /* top (2 bytes) */ + Stream_Read_UINT16(s, sysparam->taskbarPos.right); /* right (2 bytes) */ + Stream_Read_UINT16(s, sysparam->taskbarPos.bottom); /* bottom (2 bytes) */ + break; + + case SPI_SET_HIGH_CONTRAST: + sysparam->params |= SPI_MASK_SET_HIGH_CONTRAST; + if (Stream_GetRemainingLength(s) < 8) + { + WLog_ERR(TAG, "Stream_GetRemainingLength failed!"); + return ERROR_INVALID_DATA; + } + + error = rail_read_high_contrast(s, &sysparam->highContrast); + break; + + case SPI_SETCARETWIDTH: + sysparam->params |= SPI_MASK_SET_CARET_WIDTH; + + if (!extendedSpiSupported) + return ERROR_INVALID_DATA; + + if (Stream_GetRemainingLength(s) < 4) + { + WLog_ERR(TAG, "Stream_GetRemainingLength failed!"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT32(s, sysparam->caretWidth); + + if (sysparam->caretWidth < 0x0001) + return ERROR_INVALID_DATA; + + break; + + case SPI_SETSTICKYKEYS: + sysparam->params |= SPI_MASK_SET_STICKY_KEYS; + + if (!extendedSpiSupported) + return ERROR_INVALID_DATA; + + if (Stream_GetRemainingLength(s) < 4) + { + WLog_ERR(TAG, "Stream_GetRemainingLength failed!"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT32(s, sysparam->stickyKeys); + break; + + case SPI_SETTOGGLEKEYS: + sysparam->params |= SPI_MASK_SET_TOGGLE_KEYS; + + if (!extendedSpiSupported) + return ERROR_INVALID_DATA; + + if (Stream_GetRemainingLength(s) < 4) + { + WLog_ERR(TAG, "Stream_GetRemainingLength failed!"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT32(s, sysparam->toggleKeys); + break; + + case SPI_SETFILTERKEYS: + sysparam->params |= SPI_MASK_SET_FILTER_KEYS; + + if (!extendedSpiSupported) + return ERROR_INVALID_DATA; + + if (Stream_GetRemainingLength(s) < 20) + { + WLog_ERR(TAG, "Stream_GetRemainingLength failed!"); + return ERROR_INVALID_DATA; + } + + error = rail_read_filterkeys(s, &sysparam->filterKeys); + break; + + /* Server sysparams */ + case SPI_SETSCREENSAVEACTIVE: + sysparam->params |= SPI_MASK_SET_SCREEN_SAVE_ACTIVE; + + Stream_Read_UINT8(s, body); /* body (1 byte) */ + sysparam->setScreenSaveActive = body != 0; + break; + + case SPI_SETSCREENSAVESECURE: + sysparam->params |= SPI_MASK_SET_SET_SCREEN_SAVE_SECURE; + + Stream_Read_UINT8(s, body); /* body (1 byte) */ + sysparam->setScreenSaveSecure = body != 0; + break; + + default: + break; + } + + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 err2or code + */ +UINT rail_write_sysparam_order(wStream* s, const RAIL_SYSPARAM_ORDER* sysparam, + BOOL extendedSpiSupported) +{ + BYTE body; + UINT error = CHANNEL_RC_OK; + + if (!s || !sysparam) + return ERROR_INVALID_PARAMETER; + + if (!Stream_EnsureRemainingCapacity(s, 12)) + return CHANNEL_RC_NO_MEMORY; + + Stream_Write_UINT32(s, sysparam->param); /* systemParam (4 bytes) */ + + switch (sysparam->param) + { + /* Client sysparams */ + case SPI_SET_DRAG_FULL_WINDOWS: + body = sysparam->dragFullWindows ? 1 : 0; + Stream_Write_UINT8(s, body); + break; + + case SPI_SET_KEYBOARD_CUES: + body = sysparam->keyboardCues ? 1 : 0; + Stream_Write_UINT8(s, body); + break; + + case SPI_SET_KEYBOARD_PREF: + body = sysparam->keyboardPref ? 1 : 0; + Stream_Write_UINT8(s, body); + break; + + case SPI_SET_MOUSE_BUTTON_SWAP: + body = sysparam->mouseButtonSwap ? 1 : 0; + Stream_Write_UINT8(s, body); + break; + + case SPI_SET_WORK_AREA: + Stream_Write_UINT16(s, sysparam->workArea.left); /* left (2 bytes) */ + Stream_Write_UINT16(s, sysparam->workArea.top); /* top (2 bytes) */ + Stream_Write_UINT16(s, sysparam->workArea.right); /* right (2 bytes) */ + Stream_Write_UINT16(s, sysparam->workArea.bottom); /* bottom (2 bytes) */ + break; + + case SPI_DISPLAY_CHANGE: + Stream_Write_UINT16(s, sysparam->displayChange.left); /* left (2 bytes) */ + Stream_Write_UINT16(s, sysparam->displayChange.top); /* top (2 bytes) */ + Stream_Write_UINT16(s, sysparam->displayChange.right); /* right (2 bytes) */ + Stream_Write_UINT16(s, sysparam->displayChange.bottom); /* bottom (2 bytes) */ + break; + + case SPI_TASKBAR_POS: + Stream_Write_UINT16(s, sysparam->taskbarPos.left); /* left (2 bytes) */ + Stream_Write_UINT16(s, sysparam->taskbarPos.top); /* top (2 bytes) */ + Stream_Write_UINT16(s, sysparam->taskbarPos.right); /* right (2 bytes) */ + Stream_Write_UINT16(s, sysparam->taskbarPos.bottom); /* bottom (2 bytes) */ + break; + + case SPI_SET_HIGH_CONTRAST: + error = rail_write_high_contrast(s, &sysparam->highContrast); + break; + + case SPI_SETCARETWIDTH: + if (!extendedSpiSupported) + return ERROR_INVALID_DATA; + + if (sysparam->caretWidth < 0x0001) + return ERROR_INVALID_DATA; + + Stream_Write_UINT32(s, sysparam->caretWidth); + break; + + case SPI_SETSTICKYKEYS: + if (!extendedSpiSupported) + return ERROR_INVALID_DATA; + + Stream_Write_UINT32(s, sysparam->stickyKeys); + break; + + case SPI_SETTOGGLEKEYS: + if (!extendedSpiSupported) + return ERROR_INVALID_DATA; + + Stream_Write_UINT32(s, sysparam->toggleKeys); + break; + + case SPI_SETFILTERKEYS: + if (!extendedSpiSupported) + return ERROR_INVALID_DATA; + + error = rail_write_filterkeys(s, &sysparam->filterKeys); + break; + + /* Server sysparams */ + case SPI_SETSCREENSAVEACTIVE: + body = sysparam->setScreenSaveActive ? 1 : 0; + Stream_Write_UINT8(s, body); + break; + + case SPI_SETSCREENSAVESECURE: + body = sysparam->setScreenSaveSecure ? 1 : 0; + Stream_Write_UINT8(s, body); + break; + + default: + return ERROR_INVALID_PARAMETER; + } + + return error; +} + +BOOL rail_is_extended_spi_supported(UINT32 channelFlags) +{ + return channelFlags & TS_RAIL_ORDER_HANDSHAKE_EX_FLAGS_EXTENDED_SPI_SUPPORTED; +} diff --git a/channels/rail/rail_common.h b/channels/rail/rail_common.h new file mode 100644 index 0000000..34b6fa0 --- /dev/null +++ b/channels/rail/rail_common.h @@ -0,0 +1,76 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * RAIL Virtual Channel Plugin + * + * Copyright 2011 Marc-Andre Moreau + * Copyright 2011 Roman Barabanov + * Copyright 2011 Vic Lee + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger + * + * Licensed 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 FREERDP_CHANNEL_RAIL_COMMON_H +#define FREERDP_CHANNEL_RAIL_COMMON_H + +#include + +#define RAIL_PDU_HEADER_LENGTH 4 + +/* Fixed length of PDUs, excluding variable lengths */ +#define RAIL_HANDSHAKE_ORDER_LENGTH 4 /* fixed */ +#define RAIL_HANDSHAKE_EX_ORDER_LENGTH 8 /* fixed */ +#define RAIL_CLIENT_STATUS_ORDER_LENGTH 4 /* fixed */ +#define RAIL_EXEC_ORDER_LENGTH 8 /* variable */ +#define RAIL_EXEC_RESULT_ORDER_LENGTH 12 /* variable */ +#define RAIL_SYSPARAM_ORDER_LENGTH 4 /* variable */ +#define RAIL_MINMAXINFO_ORDER_LENGTH 20 /* fixed */ +#define RAIL_LOCALMOVESIZE_ORDER_LENGTH 12 /* fixed */ +#define RAIL_ACTIVATE_ORDER_LENGTH 5 /* fixed */ +#define RAIL_SYSMENU_ORDER_LENGTH 8 /* fixed */ +#define RAIL_SYSCOMMAND_ORDER_LENGTH 6 /* fixed */ +#define RAIL_NOTIFY_EVENT_ORDER_LENGTH 12 /* fixed */ +#define RAIL_WINDOW_MOVE_ORDER_LENGTH 12 /* fixed */ +#define RAIL_SNAP_ARRANGE_ORDER_LENGTH 12 /* fixed */ +#define RAIL_GET_APPID_REQ_ORDER_LENGTH 4 /* fixed */ +#define RAIL_LANGBAR_INFO_ORDER_LENGTH 4 /* fixed */ +#define RAIL_LANGUAGEIME_INFO_ORDER_LENGTH 42 /* fixed */ +#define RAIL_COMPARTMENT_INFO_ORDER_LENGTH 16 /* fixed */ +#define RAIL_CLOAK_ORDER_LENGTH 5 /* fixed */ +#define RAIL_TASKBAR_INFO_ORDER_LENGTH 12 /* fixed */ +#define RAIL_Z_ORDER_SYNC_ORDER_LENGTH 4 /* fixed */ +#define RAIL_POWER_DISPLAY_REQUEST_ORDER_LENGTH 4 /* fixed */ +#define RAIL_GET_APPID_RESP_ORDER_LENGTH 524 /* fixed */ +#define RAIL_GET_APPID_RESP_EX_ORDER_LENGTH 1048 /* fixed */ + +UINT rail_read_handshake_order(wStream* s, RAIL_HANDSHAKE_ORDER* handshake); +void rail_write_handshake_order(wStream* s, const RAIL_HANDSHAKE_ORDER* handshake); +UINT rail_read_handshake_ex_order(wStream* s, RAIL_HANDSHAKE_EX_ORDER* handshakeEx); +void rail_write_handshake_ex_order(wStream* s, const RAIL_HANDSHAKE_EX_ORDER* handshakeEx); + +wStream* rail_pdu_init(size_t length); +UINT rail_read_pdu_header(wStream* s, UINT16* orderType, UINT16* orderLength); +void rail_write_pdu_header(wStream* s, UINT16 orderType, UINT16 orderLength); + +UINT rail_write_unicode_string(wStream* s, const RAIL_UNICODE_STRING* unicode_string); +UINT rail_write_unicode_string_value(wStream* s, const RAIL_UNICODE_STRING* unicode_string); + +UINT rail_read_sysparam_order(wStream* s, RAIL_SYSPARAM_ORDER* sysparam, BOOL extendedSpiSupported); +UINT rail_write_sysparam_order(wStream* s, const RAIL_SYSPARAM_ORDER* sysparam, + BOOL extendedSpiSupported); +BOOL rail_is_extended_spi_supported(UINT32 channelsFlags); +const char* rail_get_order_type_string(UINT16 orderType); +const char* rail_get_order_type_string_full(UINT16 orderType, char* buffer, size_t length); + +#endif /* FREERDP_CHANNEL_RAIL_COMMON_H */ diff --git a/channels/rail/server/CMakeLists.txt b/channels/rail/server/CMakeLists.txt new file mode 100644 index 0000000..c7f1c7a --- /dev/null +++ b/channels/rail/server/CMakeLists.txt @@ -0,0 +1,32 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2019 Mati Shabtay +# +# Licensed 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. + +define_channel_server("rail") + +set(${MODULE_PREFIX}_SRCS + ../rail_common.c + ../rail_common.h + rail_main.c + rail_main.h) + +include_directories(..) + +add_channel_server_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} FALSE "VirtualChannelEntryEx") + +target_link_libraries(${MODULE_NAME} freerdp) + +set_property(TARGET ${MODULE_NAME} PROPERTY FOLDER "Channels/${CHANNEL_NAME}/Server") diff --git a/channels/rail/server/rail_main.c b/channels/rail/server/rail_main.c new file mode 100644 index 0000000..4949fb7 --- /dev/null +++ b/channels/rail/server/rail_main.c @@ -0,0 +1,1688 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * RAIL Virtual Channel Plugin + * + * Copyright 2019 Mati Shabtay + * + + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +#include + +#include +#include +#include +#include + +#include "rail_main.h" + +#define TAG CHANNELS_TAG("rail.server") + +/** + * Sends a single rail PDU on the channel + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rail_send(RailServerContext* context, wStream* s, ULONG length) +{ + UINT status = CHANNEL_RC_OK; + ULONG written; + + if (!context) + return CHANNEL_RC_BAD_INIT_HANDLE; + + if (!WTSVirtualChannelWrite(context->priv->rail_channel, (PCHAR)Stream_Buffer(s), length, + &written)) + { + WLog_ERR(TAG, "WTSVirtualChannelWrite failed!"); + status = ERROR_INTERNAL_ERROR; + } + + return status; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rail_server_send_pdu(RailServerContext* context, wStream* s, UINT16 orderType) +{ + char buffer[128] = { 0 }; + UINT16 orderLength; + + if (!context || !s) + return ERROR_INVALID_PARAMETER; + + orderLength = (UINT16)Stream_GetPosition(s); + Stream_SetPosition(s, 0); + rail_write_pdu_header(s, orderType, orderLength); + Stream_SetPosition(s, orderLength); + WLog_DBG(TAG, "Sending %s PDU, length: %" PRIu16 "", + rail_get_order_type_string_full(orderType, buffer, sizeof(buffer)), orderLength); + return rail_send(context, s, orderLength); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rail_write_local_move_size_order(wStream* s, + const RAIL_LOCALMOVESIZE_ORDER* localMoveSize) +{ + if (!s || !localMoveSize) + return ERROR_INVALID_PARAMETER; + + Stream_Write_UINT32(s, localMoveSize->windowId); /* WindowId (4 bytes) */ + Stream_Write_UINT16(s, localMoveSize->isMoveSizeStart ? 1 : 0); /* IsMoveSizeStart (2 bytes) */ + Stream_Write_UINT16(s, localMoveSize->moveSizeType); /* MoveSizeType (2 bytes) */ + Stream_Write_UINT16(s, localMoveSize->posX); /* PosX (2 bytes) */ + Stream_Write_UINT16(s, localMoveSize->posY); /* PosY (2 bytes) */ + return ERROR_SUCCESS; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rail_write_min_max_info_order(wStream* s, const RAIL_MINMAXINFO_ORDER* minMaxInfo) +{ + if (!s || !minMaxInfo) + return ERROR_INVALID_PARAMETER; + + Stream_Write_UINT32(s, minMaxInfo->windowId); /* WindowId (4 bytes) */ + Stream_Write_INT16(s, minMaxInfo->maxWidth); /* MaxWidth (2 bytes) */ + Stream_Write_INT16(s, minMaxInfo->maxHeight); /* MaxHeight (2 bytes) */ + Stream_Write_INT16(s, minMaxInfo->maxPosX); /* MaxPosX (2 bytes) */ + Stream_Write_INT16(s, minMaxInfo->maxPosY); /* MaxPosY (2 bytes) */ + Stream_Write_INT16(s, minMaxInfo->minTrackWidth); /* MinTrackWidth (2 bytes) */ + Stream_Write_INT16(s, minMaxInfo->minTrackHeight); /* MinTrackHeight (2 bytes) */ + Stream_Write_INT16(s, minMaxInfo->maxTrackWidth); /* MaxTrackWidth (2 bytes) */ + Stream_Write_INT16(s, minMaxInfo->maxTrackHeight); /* MaxTrackHeight (2 bytes) */ + return ERROR_SUCCESS; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rail_write_taskbar_info_order(wStream* s, const RAIL_TASKBAR_INFO_ORDER* taskbarInfo) +{ + if (!s || !taskbarInfo) + return ERROR_INVALID_PARAMETER; + + Stream_Write_UINT32(s, taskbarInfo->TaskbarMessage); /* TaskbarMessage (4 bytes) */ + Stream_Write_UINT32(s, taskbarInfo->WindowIdTab); /* WindowIdTab (4 bytes) */ + Stream_Write_UINT32(s, taskbarInfo->Body); /* Body (4 bytes) */ + return ERROR_SUCCESS; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rail_write_langbar_info_order(wStream* s, const RAIL_LANGBAR_INFO_ORDER* langbarInfo) +{ + if (!s || !langbarInfo) + return ERROR_INVALID_PARAMETER; + + Stream_Write_UINT32(s, langbarInfo->languageBarStatus); /* LanguageBarStatus (4 bytes) */ + return ERROR_SUCCESS; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rail_write_exec_result_order(wStream* s, const RAIL_EXEC_RESULT_ORDER* execResult) +{ + if (!s || !execResult) + return ERROR_INVALID_PARAMETER; + + if (execResult->exeOrFile.length > 520 || execResult->exeOrFile.length < 1) + return ERROR_INVALID_DATA; + + Stream_Write_UINT16(s, execResult->flags); /* Flags (2 bytes) */ + Stream_Write_UINT16(s, execResult->execResult); /* ExecResult (2 bytes) */ + Stream_Write_UINT32(s, execResult->rawResult); /* RawResult (4 bytes) */ + Stream_Write_UINT16(s, 0); /* Padding (2 bytes) */ + Stream_Write_UINT16(s, execResult->exeOrFile.length); /* ExeOrFileLength (2 bytes) */ + Stream_Write(s, execResult->exeOrFile.string, + execResult->exeOrFile.length); /* ExeOrFile (variable) */ + return ERROR_SUCCESS; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rail_write_z_order_sync_order(wStream* s, const RAIL_ZORDER_SYNC* zOrderSync) +{ + if (!s || !zOrderSync) + return ERROR_INVALID_PARAMETER; + + Stream_Write_UINT32(s, zOrderSync->windowIdMarker); /* WindowIdMarker (4 bytes) */ + return ERROR_SUCCESS; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rail_write_cloak_order(wStream* s, const RAIL_CLOAK* cloak) +{ + if (!s || !cloak) + return ERROR_INVALID_PARAMETER; + + Stream_Write_UINT32(s, cloak->windowId); /* WindowId (4 bytes) */ + Stream_Write_UINT8(s, cloak->cloak ? 1 : 0); /* Cloaked (1 byte) */ + return ERROR_SUCCESS; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT +rail_write_power_display_request_order(wStream* s, + const RAIL_POWER_DISPLAY_REQUEST* powerDisplayRequest) +{ + if (!s || !powerDisplayRequest) + return ERROR_INVALID_PARAMETER; + + Stream_Write_UINT32(s, powerDisplayRequest->active ? 1 : 0); /* Active (4 bytes) */ + return ERROR_SUCCESS; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rail_write_get_app_id_resp_order(wStream* s, + const RAIL_GET_APPID_RESP_ORDER* getAppidResp) +{ + if (!s || !getAppidResp) + return ERROR_INVALID_PARAMETER; + + Stream_Write_UINT32(s, getAppidResp->windowId); /* WindowId (4 bytes) */ + Stream_Write_UTF16_String( + s, getAppidResp->applicationId, + ARRAYSIZE(getAppidResp->applicationId)); /* ApplicationId (512 bytes) */ + return ERROR_SUCCESS; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rail_write_get_appid_resp_ex_order(wStream* s, + const RAIL_GET_APPID_RESP_EX* getAppidRespEx) +{ + if (!s || !getAppidRespEx) + return ERROR_INVALID_PARAMETER; + + Stream_Write_UINT32(s, getAppidRespEx->windowID); /* WindowId (4 bytes) */ + Stream_Write_UTF16_String( + s, getAppidRespEx->applicationID, + ARRAYSIZE(getAppidRespEx->applicationID)); /* ApplicationId (520 bytes) */ + Stream_Write_UINT32(s, getAppidRespEx->processId); /* ProcessId (4 bytes) */ + Stream_Write_UTF16_String( + s, getAppidRespEx->processImageName, + ARRAYSIZE(getAppidRespEx->processImageName)); /* ProcessImageName (520 bytes) */ + return ERROR_SUCCESS; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rail_send_server_handshake(RailServerContext* context, + const RAIL_HANDSHAKE_ORDER* handshake) +{ + wStream* s; + UINT error; + + if (!context || !handshake) + return ERROR_INVALID_PARAMETER; + + s = rail_pdu_init(RAIL_HANDSHAKE_ORDER_LENGTH); + + if (!s) + { + WLog_ERR(TAG, "rail_pdu_init failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + rail_write_handshake_order(s, handshake); + error = rail_server_send_pdu(context, s, TS_RAIL_ORDER_HANDSHAKE); + Stream_Free(s, TRUE); + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rail_send_server_handshake_ex(RailServerContext* context, + const RAIL_HANDSHAKE_EX_ORDER* handshakeEx) +{ + wStream* s; + UINT error; + + if (!context || !handshakeEx || !context->priv) + return ERROR_INVALID_PARAMETER; + + s = rail_pdu_init(RAIL_HANDSHAKE_EX_ORDER_LENGTH); + + if (!s) + { + WLog_ERR(TAG, "rail_pdu_init failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + rail_server_set_handshake_ex_flags(context, handshakeEx->railHandshakeFlags); + + rail_write_handshake_ex_order(s, handshakeEx); + error = rail_server_send_pdu(context, s, TS_RAIL_ORDER_HANDSHAKE_EX); + Stream_Free(s, TRUE); + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rail_send_server_sysparam(RailServerContext* context, + const RAIL_SYSPARAM_ORDER* sysparam) +{ + wStream* s; + UINT error; + RailServerPrivate* priv; + BOOL extendedSpiSupported; + + if (!context || !sysparam) + return ERROR_INVALID_PARAMETER; + + priv = context->priv; + + if (!priv) + return ERROR_INVALID_PARAMETER; + + extendedSpiSupported = rail_is_extended_spi_supported(context->priv->channelFlags); + s = rail_pdu_init(RAIL_SYSPARAM_ORDER_LENGTH); + + if (!s) + { + WLog_ERR(TAG, "rail_pdu_init failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + rail_write_sysparam_order(s, sysparam, extendedSpiSupported); + error = rail_server_send_pdu(context, s, TS_RAIL_ORDER_SYSPARAM); + Stream_Free(s, TRUE); + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rail_send_server_local_move_size(RailServerContext* context, + const RAIL_LOCALMOVESIZE_ORDER* localMoveSize) +{ + wStream* s; + UINT error; + + if (!context || !localMoveSize) + return ERROR_INVALID_PARAMETER; + + s = rail_pdu_init(RAIL_LOCALMOVESIZE_ORDER_LENGTH); + + if (!s) + { + WLog_ERR(TAG, "rail_pdu_init failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + rail_write_local_move_size_order(s, localMoveSize); + error = rail_server_send_pdu(context, s, TS_RAIL_ORDER_LOCALMOVESIZE); + Stream_Free(s, TRUE); + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rail_send_server_min_max_info(RailServerContext* context, + const RAIL_MINMAXINFO_ORDER* minMaxInfo) +{ + wStream* s; + UINT error; + + if (!context || !minMaxInfo) + return ERROR_INVALID_PARAMETER; + + s = rail_pdu_init(RAIL_MINMAXINFO_ORDER_LENGTH); + + if (!s) + { + WLog_ERR(TAG, "rail_pdu_init failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + rail_write_min_max_info_order(s, minMaxInfo); + error = rail_server_send_pdu(context, s, TS_RAIL_ORDER_MINMAXINFO); + Stream_Free(s, TRUE); + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rail_send_server_taskbar_info(RailServerContext* context, + const RAIL_TASKBAR_INFO_ORDER* taskbarInfo) +{ + wStream* s; + UINT error; + + if (!context || !taskbarInfo) + return ERROR_INVALID_PARAMETER; + + s = rail_pdu_init(RAIL_TASKBAR_INFO_ORDER_LENGTH); + + if (!s) + { + WLog_ERR(TAG, "rail_pdu_init failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + rail_write_taskbar_info_order(s, taskbarInfo); + error = rail_server_send_pdu(context, s, TS_RAIL_ORDER_TASKBARINFO); + Stream_Free(s, TRUE); + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rail_send_server_langbar_info(RailServerContext* context, + const RAIL_LANGBAR_INFO_ORDER* langbarInfo) +{ + wStream* s; + UINT error; + + if (!context || !langbarInfo) + return ERROR_INVALID_PARAMETER; + + s = rail_pdu_init(RAIL_LANGBAR_INFO_ORDER_LENGTH); + + if (!s) + { + WLog_ERR(TAG, "rail_pdu_init failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + rail_write_langbar_info_order(s, langbarInfo); + error = rail_server_send_pdu(context, s, TS_RAIL_ORDER_LANGBARINFO); + Stream_Free(s, TRUE); + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rail_send_server_exec_result(RailServerContext* context, + const RAIL_EXEC_RESULT_ORDER* execResult) +{ + wStream* s; + UINT error; + + if (!context || !execResult) + return ERROR_INVALID_PARAMETER; + + s = rail_pdu_init(RAIL_EXEC_RESULT_ORDER_LENGTH + execResult->exeOrFile.length); + + if (!s) + { + WLog_ERR(TAG, "rail_pdu_init failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + rail_write_exec_result_order(s, execResult); + error = rail_server_send_pdu(context, s, TS_RAIL_ORDER_EXEC_RESULT); + Stream_Free(s, TRUE); + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rail_send_server_z_order_sync(RailServerContext* context, + const RAIL_ZORDER_SYNC* zOrderSync) +{ + wStream* s; + UINT error; + + if (!context || !zOrderSync) + return ERROR_INVALID_PARAMETER; + + s = rail_pdu_init(RAIL_Z_ORDER_SYNC_ORDER_LENGTH); + + if (!s) + { + WLog_ERR(TAG, "rail_pdu_init failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + rail_write_z_order_sync_order(s, zOrderSync); + error = rail_server_send_pdu(context, s, TS_RAIL_ORDER_ZORDER_SYNC); + Stream_Free(s, TRUE); + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rail_send_server_cloak(RailServerContext* context, const RAIL_CLOAK* cloak) +{ + wStream* s; + UINT error; + + if (!context || !cloak) + return ERROR_INVALID_PARAMETER; + + s = rail_pdu_init(RAIL_CLOAK_ORDER_LENGTH); + + if (!s) + { + WLog_ERR(TAG, "rail_pdu_init failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + rail_write_cloak_order(s, cloak); + error = rail_server_send_pdu(context, s, TS_RAIL_ORDER_CLOAK); + Stream_Free(s, TRUE); + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT +rail_send_server_power_display_request(RailServerContext* context, + const RAIL_POWER_DISPLAY_REQUEST* powerDisplayRequest) +{ + wStream* s; + UINT error; + + if (!context || !powerDisplayRequest) + return ERROR_INVALID_PARAMETER; + + s = rail_pdu_init(RAIL_POWER_DISPLAY_REQUEST_ORDER_LENGTH); + + if (!s) + { + WLog_ERR(TAG, "rail_pdu_init failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + rail_write_power_display_request_order(s, powerDisplayRequest); + error = rail_server_send_pdu(context, s, TS_RAIL_ORDER_POWER_DISPLAY_REQUEST); + Stream_Free(s, TRUE); + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error coie + */ +static UINT rail_send_server_get_app_id_resp(RailServerContext* context, + const RAIL_GET_APPID_RESP_ORDER* getAppidResp) +{ + wStream* s; + UINT error; + + if (!context || !getAppidResp) + return ERROR_INVALID_PARAMETER; + + s = rail_pdu_init(RAIL_GET_APPID_RESP_ORDER_LENGTH); + + if (!s) + { + WLog_ERR(TAG, "rail_pdu_init failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + rail_write_get_app_id_resp_order(s, getAppidResp); + error = rail_server_send_pdu(context, s, TS_RAIL_ORDER_GET_APPID_RESP); + Stream_Free(s, TRUE); + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rail_send_server_get_appid_resp_ex(RailServerContext* context, + const RAIL_GET_APPID_RESP_EX* getAppidRespEx) +{ + wStream* s; + UINT error; + + if (!context || !getAppidRespEx) + return ERROR_INVALID_PARAMETER; + + s = rail_pdu_init(RAIL_GET_APPID_RESP_EX_ORDER_LENGTH); + + if (!s) + { + WLog_ERR(TAG, "rail_pdu_init failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + rail_write_get_appid_resp_ex_order(s, getAppidRespEx); + error = rail_server_send_pdu(context, s, TS_RAIL_ORDER_GET_APPID_RESP_EX); + Stream_Free(s, TRUE); + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rail_read_client_status_order(wStream* s, RAIL_CLIENT_STATUS_ORDER* clientStatus) +{ + if (Stream_GetRemainingLength(s) < RAIL_CLIENT_STATUS_ORDER_LENGTH) + return ERROR_INVALID_DATA; + + Stream_Read_UINT32(s, clientStatus->flags); /* Flags (4 bytes) */ + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rail_read_exec_order(wStream* s, RAIL_EXEC_ORDER* exec) +{ + RAIL_EXEC_ORDER order = { 0 }; + UINT16 exeLen, workLen, argLen; + + if (Stream_GetRemainingLength(s) < RAIL_EXEC_ORDER_LENGTH) + return ERROR_INVALID_DATA; + + Stream_Read_UINT16(s, exec->flags); /* Flags (2 bytes) */ + Stream_Read_UINT16(s, exeLen); /* ExeOrFileLength (2 bytes) */ + Stream_Read_UINT16(s, workLen); /* WorkingDirLength (2 bytes) */ + Stream_Read_UINT16(s, argLen); /* ArgumentsLength (2 bytes) */ + + if (Stream_GetRemainingLength(s) < (size_t)exeLen + workLen + argLen) + return ERROR_INVALID_DATA; + + { + const int len = exeLen / sizeof(WCHAR); + int rc; + const WCHAR* str = (const WCHAR*)Stream_Pointer(s); + rc = ConvertFromUnicode(CP_UTF8, 0, str, len, &exec->RemoteApplicationProgram, 0, NULL, + NULL); + if (rc != len) + goto fail; + Stream_Seek(s, exeLen); + } + { + const int len = workLen / sizeof(WCHAR); + int rc; + + const WCHAR* str = (const WCHAR*)Stream_Pointer(s); + rc = ConvertFromUnicode(CP_UTF8, 0, str, len, &exec->RemoteApplicationProgram, 0, NULL, + NULL); + if (rc != len) + goto fail; + Stream_Seek(s, workLen); + } + { + const int len = argLen / sizeof(WCHAR); + int rc; + const WCHAR* str = (const WCHAR*)Stream_Pointer(s); + rc = ConvertFromUnicode(CP_UTF8, 0, str, len, &exec->RemoteApplicationProgram, 0, NULL, + NULL); + if (rc != len) + goto fail; + Stream_Seek(s, argLen); + } + + return CHANNEL_RC_OK; +fail: + free(exec->RemoteApplicationProgram); + free(exec->RemoteApplicationArguments); + free(exec->RemoteApplicationWorkingDir); + *exec = order; + return ERROR_INTERNAL_ERROR; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rail_read_activate_order(wStream* s, RAIL_ACTIVATE_ORDER* activate) +{ + BYTE enabled; + + if (Stream_GetRemainingLength(s) < RAIL_ACTIVATE_ORDER_LENGTH) + return ERROR_INVALID_DATA; + + Stream_Read_UINT32(s, activate->windowId); /* WindowId (4 bytes) */ + Stream_Read_UINT8(s, enabled); /* Enabled (1 byte) */ + activate->enabled = (enabled != 0) ? TRUE : FALSE; + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rail_read_sysmenu_order(wStream* s, RAIL_SYSMENU_ORDER* sysmenu) +{ + if (Stream_GetRemainingLength(s) < RAIL_SYSMENU_ORDER_LENGTH) + return ERROR_INVALID_DATA; + + Stream_Read_UINT32(s, sysmenu->windowId); /* WindowId (4 bytes) */ + Stream_Read_INT16(s, sysmenu->left); /* Left (2 bytes) */ + Stream_Read_INT16(s, sysmenu->top); /* Top (2 bytes) */ + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rail_read_syscommand_order(wStream* s, RAIL_SYSCOMMAND_ORDER* syscommand) +{ + if (Stream_GetRemainingLength(s) < RAIL_SYSCOMMAND_ORDER_LENGTH) + return ERROR_INVALID_DATA; + + Stream_Read_UINT32(s, syscommand->windowId); /* WindowId (4 bytes) */ + Stream_Read_UINT16(s, syscommand->command); /* Command (2 bytes) */ + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rail_read_notify_event_order(wStream* s, RAIL_NOTIFY_EVENT_ORDER* notifyEvent) +{ + if (Stream_GetRemainingLength(s) < RAIL_NOTIFY_EVENT_ORDER_LENGTH) + return ERROR_INVALID_DATA; + + Stream_Read_UINT32(s, notifyEvent->windowId); /* WindowId (4 bytes) */ + Stream_Read_UINT32(s, notifyEvent->notifyIconId); /* NotifyIconId (4 bytes) */ + Stream_Read_UINT32(s, notifyEvent->message); /* Message (4 bytes) */ + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rail_read_get_appid_req_order(wStream* s, RAIL_GET_APPID_REQ_ORDER* getAppidReq) +{ + if (Stream_GetRemainingLength(s) < RAIL_GET_APPID_REQ_ORDER_LENGTH) + return ERROR_INVALID_DATA; + + Stream_Read_UINT32(s, getAppidReq->windowId); /* WindowId (4 bytes) */ + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rail_read_window_move_order(wStream* s, RAIL_WINDOW_MOVE_ORDER* windowMove) +{ + if (Stream_GetRemainingLength(s) < RAIL_WINDOW_MOVE_ORDER_LENGTH) + return ERROR_INVALID_DATA; + + Stream_Read_UINT32(s, windowMove->windowId); /* WindowId (4 bytes) */ + Stream_Read_INT16(s, windowMove->left); /* Left (2 bytes) */ + Stream_Read_INT16(s, windowMove->top); /* Top (2 bytes) */ + Stream_Read_INT16(s, windowMove->right); /* Right (2 bytes) */ + Stream_Read_INT16(s, windowMove->bottom); /* Bottom (2 bytes) */ + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rail_read_snap_arange_order(wStream* s, RAIL_SNAP_ARRANGE* snapArrange) +{ + if (Stream_GetRemainingLength(s) < RAIL_SNAP_ARRANGE_ORDER_LENGTH) + return ERROR_INVALID_DATA; + + Stream_Read_UINT32(s, snapArrange->windowId); /* WindowId (4 bytes) */ + Stream_Read_INT16(s, snapArrange->left); /* Left (2 bytes) */ + Stream_Read_INT16(s, snapArrange->top); /* Top (2 bytes) */ + Stream_Read_INT16(s, snapArrange->right); /* Right (2 bytes) */ + Stream_Read_INT16(s, snapArrange->bottom); /* Bottom (2 bytes) */ + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rail_read_langbar_info_order(wStream* s, RAIL_LANGBAR_INFO_ORDER* langbarInfo) +{ + if (Stream_GetRemainingLength(s) < RAIL_LANGBAR_INFO_ORDER_LENGTH) + return ERROR_INVALID_DATA; + + Stream_Read_UINT32(s, langbarInfo->languageBarStatus); /* LanguageBarStatus (4 bytes) */ + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rail_read_language_ime_info_order(wStream* s, + RAIL_LANGUAGEIME_INFO_ORDER* languageImeInfo) +{ + if (Stream_GetRemainingLength(s) < RAIL_LANGUAGEIME_INFO_ORDER_LENGTH) + return ERROR_INVALID_DATA; + + Stream_Read_UINT32(s, languageImeInfo->ProfileType); /* ProfileType (4 bytes) */ + Stream_Read_UINT16(s, languageImeInfo->LanguageID); /* LanguageID (2 bytes) */ + Stream_Read( + s, &languageImeInfo->LanguageProfileCLSID, + sizeof(languageImeInfo->LanguageProfileCLSID)); /* LanguageProfileCLSID (16 bytes) */ + Stream_Read(s, &languageImeInfo->ProfileGUID, + sizeof(languageImeInfo->ProfileGUID)); /* ProfileGUID (16 bytes) */ + Stream_Read_UINT32(s, languageImeInfo->KeyboardLayout); /* KeyboardLayout (4 bytes) */ + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rail_read_compartment_info_order(wStream* s, + RAIL_COMPARTMENT_INFO_ORDER* compartmentInfo) +{ + if (Stream_GetRemainingLength(s) < RAIL_COMPARTMENT_INFO_ORDER_LENGTH) + return ERROR_INVALID_DATA; + + Stream_Read_UINT32(s, compartmentInfo->ImeState); /* ImeState (4 bytes) */ + Stream_Read_UINT32(s, compartmentInfo->ImeConvMode); /* ImeConvMode (4 bytes) */ + Stream_Read_UINT32(s, compartmentInfo->ImeSentenceMode); /* ImeSentenceMode (4 bytes) */ + Stream_Read_UINT32(s, compartmentInfo->KanaMode); /* KANAMode (4 bytes) */ + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rail_read_cloak_order(wStream* s, RAIL_CLOAK* cloak) +{ + BYTE cloaked; + + if (Stream_GetRemainingLength(s) < RAIL_CLOAK_ORDER_LENGTH) + return ERROR_INVALID_DATA; + + Stream_Read_UINT32(s, cloak->windowId); /* WindowId (4 bytes) */ + Stream_Read_UINT8(s, cloaked); /* Cloaked (1 byte) */ + cloak->cloak = (cloaked != 0) ? TRUE : FALSE; + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rail_recv_client_handshake_order(RailServerContext* context, + RAIL_HANDSHAKE_ORDER* handshake, wStream* s) +{ + UINT error; + + if (!context || !handshake || !s) + return ERROR_INVALID_PARAMETER; + + if ((error = rail_read_handshake_order(s, handshake))) + { + WLog_ERR(TAG, "rail_read_handshake_order failed with error %" PRIu32 "!", error); + return error; + } + + IFCALLRET(context->ClientHandshake, error, context, handshake); + + if (error) + WLog_ERR(TAG, "context.ClientHandshake failed with error %" PRIu32 "", error); + + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rail_recv_client_client_status_order(RailServerContext* context, + RAIL_CLIENT_STATUS_ORDER* clientStatus, wStream* s) +{ + UINT error; + + if (!context || !clientStatus || !s) + return ERROR_INVALID_PARAMETER; + + if ((error = rail_read_client_status_order(s, clientStatus))) + { + WLog_ERR(TAG, "rail_read_client_status_order failed with error %" PRIu32 "!", error); + return error; + } + + IFCALLRET(context->ClientClientStatus, error, context, clientStatus); + + if (error) + WLog_ERR(TAG, "context.ClientClientStatus failed with error %" PRIu32 "", error); + + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rail_recv_client_exec_order(RailServerContext* context, wStream* s) +{ + UINT error; + RAIL_EXEC_ORDER exec = { 0 }; + + if (!context || !s) + return ERROR_INVALID_PARAMETER; + + if ((error = rail_read_exec_order(s, &exec))) + { + WLog_ERR(TAG, "rail_read_client_status_order failed with error %" PRIu32 "!", error); + return error; + } + + IFCALLRET(context->ClientExec, error, context, &exec); + + if (error) + WLog_ERR(TAG, "context.Exec failed with error %" PRIu32 "", error); + + free(exec.RemoteApplicationProgram); + free(exec.RemoteApplicationArguments); + free(exec.RemoteApplicationWorkingDir); + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rail_recv_client_sysparam_order(RailServerContext* context, + RAIL_SYSPARAM_ORDER* sysparam, wStream* s) +{ + UINT error; + BOOL extendedSpiSupported; + + if (!context || !sysparam || !s) + return ERROR_INVALID_PARAMETER; + + extendedSpiSupported = rail_is_extended_spi_supported(context->priv->channelFlags); + if ((error = rail_read_sysparam_order(s, sysparam, extendedSpiSupported))) + { + WLog_ERR(TAG, "rail_read_sysparam_order failed with error %" PRIu32 "!", error); + return error; + } + + IFCALLRET(context->ClientSysparam, error, context, sysparam); + + if (error) + WLog_ERR(TAG, "context.ClientSysparam failed with error %" PRIu32 "", error); + + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rail_recv_client_activate_order(RailServerContext* context, + RAIL_ACTIVATE_ORDER* activate, wStream* s) +{ + UINT error; + + if (!context || !activate || !s) + return ERROR_INVALID_PARAMETER; + + if ((error = rail_read_activate_order(s, activate))) + { + WLog_ERR(TAG, "rail_read_activate_order failed with error %" PRIu32 "!", error); + return error; + } + + IFCALLRET(context->ClientActivate, error, context, activate); + + if (error) + WLog_ERR(TAG, "context.ClientActivate failed with error %" PRIu32 "", error); + + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rail_recv_client_sysmenu_order(RailServerContext* context, RAIL_SYSMENU_ORDER* sysmenu, + wStream* s) +{ + UINT error; + + if (!context || !sysmenu || !s) + return ERROR_INVALID_PARAMETER; + + if ((error = rail_read_sysmenu_order(s, sysmenu))) + { + WLog_ERR(TAG, "rail_read_sysmenu_order failed with error %" PRIu32 "!", error); + return error; + } + + IFCALLRET(context->ClientSysmenu, error, context, sysmenu); + + if (error) + WLog_ERR(TAG, "context.ClientSysmenu failed with error %" PRIu32 "", error); + + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rail_recv_client_syscommand_order(RailServerContext* context, + RAIL_SYSCOMMAND_ORDER* syscommand, wStream* s) +{ + UINT error; + + if (!context || !syscommand || !s) + return ERROR_INVALID_PARAMETER; + + if ((error = rail_read_syscommand_order(s, syscommand))) + { + WLog_ERR(TAG, "rail_read_syscommand_order failed with error %" PRIu32 "!", error); + return error; + } + + IFCALLRET(context->ClientSyscommand, error, context, syscommand); + + if (error) + WLog_ERR(TAG, "context.ClientSyscommand failed with error %" PRIu32 "", error); + + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rail_recv_client_notify_event_order(RailServerContext* context, + RAIL_NOTIFY_EVENT_ORDER* notifyEvent, wStream* s) +{ + UINT error; + + if (!context || !notifyEvent || !s) + return ERROR_INVALID_PARAMETER; + + if ((error = rail_read_notify_event_order(s, notifyEvent))) + { + WLog_ERR(TAG, "rail_read_notify_event_order failed with error %" PRIu32 "!", error); + return error; + } + + IFCALLRET(context->ClientNotifyEvent, error, context, notifyEvent); + + if (error) + WLog_ERR(TAG, "context.ClientNotifyEvent failed with error %" PRIu32 "", error); + + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rail_recv_client_window_move_order(RailServerContext* context, + RAIL_WINDOW_MOVE_ORDER* windowMove, wStream* s) +{ + UINT error; + + if (!context || !windowMove || !s) + return ERROR_INVALID_PARAMETER; + + if ((error = rail_read_window_move_order(s, windowMove))) + { + WLog_ERR(TAG, "rail_read_window_move_order failed with error %" PRIu32 "!", error); + return error; + } + + IFCALLRET(context->ClientWindowMove, error, context, windowMove); + + if (error) + WLog_ERR(TAG, "context.ClientWindowMove failed with error %" PRIu32 "", error); + + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rail_recv_client_snap_arrange_order(RailServerContext* context, + RAIL_SNAP_ARRANGE* snapArrange, wStream* s) +{ + UINT error; + + if (!context || !snapArrange || !s) + return ERROR_INVALID_PARAMETER; + + if ((error = rail_read_snap_arange_order(s, snapArrange))) + { + WLog_ERR(TAG, "rail_read_snap_arange_order failed with error %" PRIu32 "!", error); + return error; + } + + IFCALLRET(context->ClientSnapArrange, error, context, snapArrange); + + if (error) + WLog_ERR(TAG, "context.ClientSnapArrange failed with error %" PRIu32 "", error); + + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rail_recv_client_get_appid_req_order(RailServerContext* context, + RAIL_GET_APPID_REQ_ORDER* getAppidReq, wStream* s) +{ + UINT error; + + if (!context || !getAppidReq || !s) + return ERROR_INVALID_PARAMETER; + + if ((error = rail_read_get_appid_req_order(s, getAppidReq))) + { + WLog_ERR(TAG, "rail_read_get_appid_req_order failed with error %" PRIu32 "!", error); + return error; + } + + IFCALLRET(context->ClientGetAppidReq, error, context, getAppidReq); + + if (error) + WLog_ERR(TAG, "context.ClientGetAppidReq failed with error %" PRIu32 "", error); + + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rail_recv_client_langbar_info_order(RailServerContext* context, + RAIL_LANGBAR_INFO_ORDER* langbarInfo, wStream* s) +{ + UINT error; + + if (!context || !langbarInfo || !s) + return ERROR_INVALID_PARAMETER; + + if ((error = rail_read_langbar_info_order(s, langbarInfo))) + { + WLog_ERR(TAG, "rail_read_langbar_info_order failed with error %" PRIu32 "!", error); + return error; + } + + IFCALLRET(context->ClientLangbarInfo, error, context, langbarInfo); + + if (error) + WLog_ERR(TAG, "context.ClientLangbarInfo failed with error %" PRIu32 "", error); + + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rail_recv_client_language_ime_info_order(RailServerContext* context, + RAIL_LANGUAGEIME_INFO_ORDER* languageImeInfo, + wStream* s) +{ + UINT error; + + if (!context || !languageImeInfo || !s) + return ERROR_INVALID_PARAMETER; + + if ((error = rail_read_language_ime_info_order(s, languageImeInfo))) + { + WLog_ERR(TAG, "rail_read_language_ime_info_order failed with error %" PRIu32 "!", error); + return error; + } + + IFCALLRET(context->ClientLanguageImeInfo, error, context, languageImeInfo); + + if (error) + WLog_ERR(TAG, "context.ClientLanguageImeInfo failed with error %" PRIu32 "", error); + + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rail_recv_client_compartment_info(RailServerContext* context, + RAIL_COMPARTMENT_INFO_ORDER* compartmentInfo, + wStream* s) +{ + UINT error; + + if (!context || !compartmentInfo || !s) + return ERROR_INVALID_PARAMETER; + + if ((error = rail_read_compartment_info_order(s, compartmentInfo))) + { + WLog_ERR(TAG, "rail_read_compartment_info_order failed with error %" PRIu32 "!", error); + return error; + } + + IFCALLRET(context->ClientCompartmentInfo, error, context, compartmentInfo); + + if (error) + WLog_ERR(TAG, "context.ClientCompartmentInfo failed with error %" PRIu32 "", error); + + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rail_recv_client_cloak_order(RailServerContext* context, RAIL_CLOAK* cloak, wStream* s) +{ + UINT error; + + if (!context || !cloak || !s) + return ERROR_INVALID_PARAMETER; + + if ((error = rail_read_cloak_order(s, cloak))) + { + WLog_ERR(TAG, "rail_read_cloak_order failed with error %" PRIu32 "!", error); + return error; + } + + IFCALLRET(context->ClientCloak, error, context, cloak); + + if (error) + WLog_ERR(TAG, "context.Cloak failed with error %" PRIu32 "", error); + + return error; +} + +static DWORD WINAPI rail_server_thread(LPVOID arg) +{ + RailServerContext* context = (RailServerContext*)arg; + RailServerPrivate* priv = context->priv; + DWORD status; + DWORD nCount = 0; + HANDLE events[8]; + UINT error = CHANNEL_RC_OK; + events[nCount++] = priv->channelEvent; + events[nCount++] = priv->stopEvent; + + while (TRUE) + { + status = WaitForMultipleObjects(nCount, events, FALSE, INFINITE); + + if (status == WAIT_FAILED) + { + error = GetLastError(); + WLog_ERR(TAG, "WaitForMultipleObjects failed with error %" PRIu32 "!", error); + break; + } + + status = WaitForSingleObject(context->priv->stopEvent, 0); + + if (status == WAIT_FAILED) + { + error = GetLastError(); + WLog_ERR(TAG, "WaitForSingleObject failed with error %" PRIu32 "!", error); + break; + } + + if (status == WAIT_OBJECT_0) + break; + + status = WaitForSingleObject(context->priv->channelEvent, 0); + + if (status == WAIT_FAILED) + { + error = GetLastError(); + WLog_ERR( + TAG, + "WaitForSingleObject(context->priv->channelEvent, 0) failed with error %" PRIu32 + "!", + error); + break; + } + + if (status == WAIT_OBJECT_0) + { + if ((error = rail_server_handle_messages(context))) + { + WLog_ERR(TAG, "rail_server_handle_messages failed with error %" PRIu32 "", error); + break; + } + } + } + + if (error && context->rdpcontext) + setChannelError(context->rdpcontext, error, "rail_server_thread reported an error"); + + ExitThread(error); + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rail_server_start(RailServerContext* context) +{ + void* buffer = NULL; + DWORD bytesReturned; + RailServerPrivate* priv = context->priv; + UINT error = ERROR_INTERNAL_ERROR; + priv->rail_channel = + WTSVirtualChannelOpen(context->vcm, WTS_CURRENT_SESSION, RAIL_SVC_CHANNEL_NAME); + + if (!priv->rail_channel) + { + WLog_ERR(TAG, "WTSVirtualChannelOpen failed!"); + return error; + } + + if (!WTSVirtualChannelQuery(priv->rail_channel, WTSVirtualEventHandle, &buffer, + &bytesReturned) || + (bytesReturned != sizeof(HANDLE))) + { + WLog_ERR(TAG, + "error during WTSVirtualChannelQuery(WTSVirtualEventHandle) or invalid returned " + "size(%" PRIu32 ")", + bytesReturned); + + if (buffer) + WTSFreeMemory(buffer); + + goto out_close; + } + + CopyMemory(&priv->channelEvent, buffer, sizeof(HANDLE)); + WTSFreeMemory(buffer); + context->priv->stopEvent = CreateEvent(NULL, TRUE, FALSE, NULL); + + if (!context->priv->stopEvent) + { + WLog_ERR(TAG, "CreateEvent failed!"); + goto out_close; + } + + context->priv->thread = CreateThread(NULL, 0, rail_server_thread, (void*)context, 0, NULL); + + if (!context->priv->thread) + { + WLog_ERR(TAG, "CreateThread failed!"); + goto out_stop_event; + } + + return CHANNEL_RC_OK; +out_stop_event: + CloseHandle(context->priv->stopEvent); + context->priv->stopEvent = NULL; +out_close: + WTSVirtualChannelClose(context->priv->rail_channel); + context->priv->rail_channel = NULL; + return error; +} + +static BOOL rail_server_stop(RailServerContext* context) +{ + RailServerPrivate* priv = (RailServerPrivate*)context->priv; + + if (priv->thread) + { + SetEvent(priv->stopEvent); + + if (WaitForSingleObject(priv->thread, INFINITE) == WAIT_FAILED) + { + WLog_ERR(TAG, "WaitForSingleObject failed with error %" PRIu32 "", GetLastError()); + return FALSE; + } + + CloseHandle(priv->thread); + CloseHandle(priv->stopEvent); + priv->thread = NULL; + priv->stopEvent = NULL; + } + + if (priv->rail_channel) + { + WTSVirtualChannelClose(priv->rail_channel); + priv->rail_channel = NULL; + } + + priv->channelEvent = NULL; + return TRUE; +} + +RailServerContext* rail_server_context_new(HANDLE vcm) +{ + RailServerContext* context; + RailServerPrivate* priv; + context = (RailServerContext*)calloc(1, sizeof(RailServerContext)); + + if (!context) + { + WLog_ERR(TAG, "calloc failed!"); + return NULL; + } + + context->vcm = vcm; + context->Start = rail_server_start; + context->Stop = rail_server_stop; + context->ServerHandshake = rail_send_server_handshake; + context->ServerHandshakeEx = rail_send_server_handshake_ex; + context->ServerSysparam = rail_send_server_sysparam; + context->ServerLocalMoveSize = rail_send_server_local_move_size; + context->ServerMinMaxInfo = rail_send_server_min_max_info; + context->ServerTaskbarInfo = rail_send_server_taskbar_info; + context->ServerLangbarInfo = rail_send_server_langbar_info; + context->ServerExecResult = rail_send_server_exec_result; + context->ServerGetAppidResp = rail_send_server_get_app_id_resp; + context->ServerZOrderSync = rail_send_server_z_order_sync; + context->ServerCloak = rail_send_server_cloak; + context->ServerPowerDisplayRequest = rail_send_server_power_display_request; + context->ServerGetAppidRespEx = rail_send_server_get_appid_resp_ex; + context->priv = priv = (RailServerPrivate*)calloc(1, sizeof(RailServerPrivate)); + + if (!priv) + { + WLog_ERR(TAG, "calloc failed!"); + goto out_free; + } + + /* Create shared input stream */ + priv->input_stream = Stream_New(NULL, 4096); + + if (!priv->input_stream) + { + WLog_ERR(TAG, "Stream_New failed!"); + goto out_free_priv; + } + + return context; +out_free_priv: + free(context->priv); +out_free: + free(context); + return NULL; +} + +void rail_server_context_free(RailServerContext* context) +{ + if (context->priv) + Stream_Free(context->priv->input_stream, TRUE); + + free(context->priv); + free(context); +} + +void rail_server_set_handshake_ex_flags(RailServerContext* context, DWORD flags) +{ + RailServerPrivate* priv; + + if (!context || !context->priv) + return; + + priv = context->priv; + priv->channelFlags = flags; +} + +UINT rail_server_handle_messages(RailServerContext* context) +{ + char buffer[128] = { 0 }; + UINT status = CHANNEL_RC_OK; + DWORD bytesReturned; + UINT16 orderType; + UINT16 orderLength; + RailServerPrivate* priv = context->priv; + wStream* s = priv->input_stream; + + /* Read header */ + if (!Stream_EnsureRemainingCapacity(s, RAIL_PDU_HEADER_LENGTH)) + { + WLog_ERR(TAG, "Stream_EnsureRemainingCapacity failed, RAIL_PDU_HEADER_LENGTH"); + return CHANNEL_RC_NO_MEMORY; + } + + if (!WTSVirtualChannelRead(priv->rail_channel, 0, (PCHAR)Stream_Pointer(s), + RAIL_PDU_HEADER_LENGTH, &bytesReturned)) + { + if (GetLastError() == ERROR_NO_DATA) + return ERROR_NO_DATA; + + WLog_ERR(TAG, "channel connection closed"); + return ERROR_INTERNAL_ERROR; + } + + /* Parse header */ + if ((status = rail_read_pdu_header(s, &orderType, &orderLength)) != CHANNEL_RC_OK) + { + WLog_ERR(TAG, "rail_read_pdu_header failed with error %" PRIu32 "!", status); + return status; + } + + if (!Stream_EnsureRemainingCapacity(s, orderLength - RAIL_PDU_HEADER_LENGTH)) + { + WLog_ERR(TAG, + "Stream_EnsureRemainingCapacity failed, orderLength - RAIL_PDU_HEADER_LENGTH"); + return CHANNEL_RC_NO_MEMORY; + } + + /* Read body */ + if (!WTSVirtualChannelRead(priv->rail_channel, 0, (PCHAR)Stream_Pointer(s), + orderLength - RAIL_PDU_HEADER_LENGTH, &bytesReturned)) + { + if (GetLastError() == ERROR_NO_DATA) + return ERROR_NO_DATA; + + WLog_ERR(TAG, "channel connection closed"); + return ERROR_INTERNAL_ERROR; + } + + WLog_DBG(TAG, "Received %s PDU, length:%" PRIu16 "", + rail_get_order_type_string_full(orderType, buffer, sizeof(buffer)), orderLength); + + switch (orderType) + { + case TS_RAIL_ORDER_HANDSHAKE: + { + RAIL_HANDSHAKE_ORDER handshake; + return rail_recv_client_handshake_order(context, &handshake, s); + } + + case TS_RAIL_ORDER_CLIENTSTATUS: + { + RAIL_CLIENT_STATUS_ORDER clientStatus; + return rail_recv_client_client_status_order(context, &clientStatus, s); + } + + case TS_RAIL_ORDER_EXEC: + return rail_recv_client_exec_order(context, s); + + case TS_RAIL_ORDER_SYSPARAM: + { + RAIL_SYSPARAM_ORDER sysparam = { 0 }; + return rail_recv_client_sysparam_order(context, &sysparam, s); + } + + case TS_RAIL_ORDER_ACTIVATE: + { + RAIL_ACTIVATE_ORDER activate; + return rail_recv_client_activate_order(context, &activate, s); + } + + case TS_RAIL_ORDER_SYSMENU: + { + RAIL_SYSMENU_ORDER sysmenu; + return rail_recv_client_sysmenu_order(context, &sysmenu, s); + } + + case TS_RAIL_ORDER_SYSCOMMAND: + { + RAIL_SYSCOMMAND_ORDER syscommand; + return rail_recv_client_syscommand_order(context, &syscommand, s); + } + + case TS_RAIL_ORDER_NOTIFY_EVENT: + { + RAIL_NOTIFY_EVENT_ORDER notifyEvent; + return rail_recv_client_notify_event_order(context, ¬ifyEvent, s); + } + + case TS_RAIL_ORDER_WINDOWMOVE: + { + RAIL_WINDOW_MOVE_ORDER windowMove; + return rail_recv_client_window_move_order(context, &windowMove, s); + } + + case TS_RAIL_ORDER_SNAP_ARRANGE: + { + RAIL_SNAP_ARRANGE snapArrange; + return rail_recv_client_snap_arrange_order(context, &snapArrange, s); + } + + case TS_RAIL_ORDER_GET_APPID_REQ: + { + RAIL_GET_APPID_REQ_ORDER getAppidReq; + return rail_recv_client_get_appid_req_order(context, &getAppidReq, s); + } + + case TS_RAIL_ORDER_LANGBARINFO: + { + RAIL_LANGBAR_INFO_ORDER langbarInfo; + return rail_recv_client_langbar_info_order(context, &langbarInfo, s); + } + + case TS_RAIL_ORDER_LANGUAGEIMEINFO: + { + RAIL_LANGUAGEIME_INFO_ORDER languageImeInfo; + return rail_recv_client_language_ime_info_order(context, &languageImeInfo, s); + } + + case TS_RAIL_ORDER_COMPARTMENTINFO: + { + RAIL_COMPARTMENT_INFO_ORDER compartmentInfo; + return rail_recv_client_compartment_info(context, &compartmentInfo, s); + } + + case TS_RAIL_ORDER_CLOAK: + { + RAIL_CLOAK cloak; + return rail_recv_client_cloak_order(context, &cloak, s); + } + + default: + WLog_ERR(TAG, "Unknown RAIL PDU order received."); + return ERROR_INVALID_DATA; + } + + Stream_SetPosition(s, 0); + return status; +} diff --git a/channels/rail/server/rail_main.h b/channels/rail/server/rail_main.h new file mode 100644 index 0000000..5c56331 --- /dev/null +++ b/channels/rail/server/rail_main.h @@ -0,0 +1,44 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * RAIL Virtual Channel Plugin + * + * Copyright 2019 Mati Shabtay + * + * Licensed 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 FREERDP_CHANNEL_RAIL_SERVER_MAIN_H +#define FREERDP_CHANNEL_RAIL_SERVER_MAIN_H + +#include +#include + +#include +#include +#include + +#include "../rail_common.h" + +struct _rail_server_private +{ + HANDLE thread; + HANDLE stopEvent; + HANDLE channelEvent; + void* rail_channel; + + wStream* input_stream; + + DWORD channelFlags; +}; + +#endif /* FREERDP_CHANNEL_RAIL_SERVER_MAIN_H */ \ No newline at end of file diff --git a/channels/rdp2tcp/CMakeLists.txt b/channels/rdp2tcp/CMakeLists.txt new file mode 100644 index 0000000..31de127 --- /dev/null +++ b/channels/rdp2tcp/CMakeLists.txt @@ -0,0 +1,22 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2012 Marc-Andre Moreau +# +# Licensed 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. + +define_channel("rdp2tcp") + +if(WITH_CLIENT_CHANNELS) + add_channel_client(${MODULE_PREFIX} ${CHANNEL_NAME}) +endif() diff --git a/channels/rdp2tcp/ChannelOptions.cmake b/channels/rdp2tcp/ChannelOptions.cmake new file mode 100644 index 0000000..3cad9b1 --- /dev/null +++ b/channels/rdp2tcp/ChannelOptions.cmake @@ -0,0 +1,10 @@ +set(OPTION_DEFAULT OFF) +set(OPTION_CLIENT_DEFAULT ON) +set(OPTION_SERVER_DEFAULT OFF) + +define_channel_options(NAME "rdp2tcp" TYPE "static" + DESCRIPTION "Tunneling TCP over RDP" + DEFAULT ${OPTION_DEFAULT}) + +define_channel_client_options(${OPTION_CLIENT_DEFAULT}) +define_channel_server_options(${OPTION_SERVER_DEFAULT}) diff --git a/channels/rdp2tcp/client/CMakeLists.txt b/channels/rdp2tcp/client/CMakeLists.txt new file mode 100644 index 0000000..2e51020 --- /dev/null +++ b/channels/rdp2tcp/client/CMakeLists.txt @@ -0,0 +1,27 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2012 Marc-Andre Moreau +# +# Licensed 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. + +define_channel_client("rdp2tcp") + +set(${MODULE_PREFIX}_SRCS + rdp2tcp_main.c) + +add_channel_client_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} TRUE "VirtualChannelEntryEx") + +target_link_libraries(${MODULE_NAME} freerdp) + +set_property(TARGET ${MODULE_NAME} PROPERTY FOLDER "Channels/${CHANNEL_NAME}/Client") diff --git a/channels/rdp2tcp/client/rdp2tcp_main.c b/channels/rdp2tcp/client/rdp2tcp_main.c new file mode 100644 index 0000000..58a2ef5 --- /dev/null +++ b/channels/rdp2tcp/client/rdp2tcp_main.c @@ -0,0 +1,327 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * rdp2tcp Virtual Channel Extension + * + * Copyright 2017 Artur Zaprzala + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +#include +#include +#include + +#include + +#define RDP2TCP_CHAN_NAME "rdp2tcp" + +#include +#define TAG CLIENT_TAG(RDP2TCP_CHAN_NAME) + +static int const debug = 0; + +typedef struct +{ + HANDLE hStdOutputRead; + HANDLE hStdInputWrite; + HANDLE hProcess; + HANDLE copyThread; + HANDLE writeComplete; + DWORD openHandle; + void* initHandle; + CHANNEL_ENTRY_POINTS_FREERDP_EX channelEntryPoints; + char buffer[16 * 1024]; +} Plugin; + +static int init_external_addin(Plugin* plugin) +{ + SECURITY_ATTRIBUTES saAttr; + STARTUPINFO siStartInfo; + PROCESS_INFORMATION procInfo; + saAttr.nLength = sizeof(SECURITY_ATTRIBUTES); + saAttr.bInheritHandle = TRUE; + saAttr.lpSecurityDescriptor = NULL; + siStartInfo.cb = sizeof(STARTUPINFO); + siStartInfo.hStdError = GetStdHandle(STD_ERROR_HANDLE); + siStartInfo.dwFlags = STARTF_USESTDHANDLES; + + // Create pipes + if (!CreatePipe(&plugin->hStdOutputRead, &siStartInfo.hStdOutput, &saAttr, 0)) + { + WLog_ERR(TAG, "stdout CreatePipe"); + return -1; + } + + if (!SetHandleInformation(plugin->hStdOutputRead, HANDLE_FLAG_INHERIT, 0)) + { + WLog_ERR(TAG, "stdout SetHandleInformation"); + return -1; + } + + if (!CreatePipe(&siStartInfo.hStdInput, &plugin->hStdInputWrite, &saAttr, 0)) + { + WLog_ERR(TAG, "stdin CreatePipe"); + return -1; + } + + if (!SetHandleInformation(plugin->hStdInputWrite, HANDLE_FLAG_INHERIT, 0)) + { + WLog_ERR(TAG, "stdin SetHandleInformation"); + return -1; + } + + // Execute plugin + if (!CreateProcess(NULL, + plugin->channelEntryPoints.pExtendedData, // command line + NULL, // process security attributes + NULL, // primary thread security attributes + TRUE, // handles are inherited + 0, // creation flags + NULL, // use parent's environment + NULL, // use parent's current directory + &siStartInfo, // STARTUPINFO pointer + &procInfo // receives PROCESS_INFORMATION + )) + { + WLog_ERR(TAG, "fork for addin"); + return -1; + } + + plugin->hProcess = procInfo.hProcess; + CloseHandle(procInfo.hThread); + CloseHandle(siStartInfo.hStdOutput); + CloseHandle(siStartInfo.hStdInput); + return 0; +} + +static void dumpData(char* data, unsigned length) +{ + unsigned const limit = 98; + unsigned l = length > limit ? limit / 2 : length; + unsigned i; + + for (i = 0; i < l; ++i) + { + printf("%02hhx", data[i]); + } + + if (length > limit) + { + printf("..."); + + for (i = length - l; i < length; ++i) + printf("%02hhx", data[i]); + } + + puts(""); +} + +static DWORD WINAPI copyThread(void* data) +{ + Plugin* plugin = (Plugin*)data; + size_t const bufsize = 16 * 1024; + + while (1) + { + DWORD dwRead; + char* buffer = malloc(bufsize); + + if (!buffer) + { + fprintf(stderr, "rdp2tcp copyThread: malloc failed\n"); + goto fail; + } + + // if (!ReadFile(plugin->hStdOutputRead, plugin->buffer, sizeof plugin->buffer, &dwRead, + // NULL)) + if (!ReadFile(plugin->hStdOutputRead, buffer, bufsize, &dwRead, NULL)) + { + free(buffer); + goto fail; + } + + if (debug > 1) + { + printf(">%8u ", (unsigned)dwRead); + dumpData(buffer, dwRead); + } + + if (plugin->channelEntryPoints.pVirtualChannelWriteEx( + plugin->initHandle, plugin->openHandle, buffer, dwRead, buffer) != CHANNEL_RC_OK) + { + free(buffer); + fprintf(stderr, "rdp2tcp copyThread failed %i\n", (int)dwRead); + goto fail; + } + + WaitForSingleObject(plugin->writeComplete, INFINITE); + ResetEvent(plugin->writeComplete); + } + +fail: + ExitThread(0); + return 0; +} + +static void closeChannel(Plugin* plugin) +{ + if (debug) + puts("rdp2tcp closing channel"); + + plugin->channelEntryPoints.pVirtualChannelCloseEx(plugin->initHandle, plugin->openHandle); +} + +static void dataReceived(Plugin* plugin, void* pData, UINT32 dataLength, UINT32 totalLength, + UINT32 dataFlags) +{ + DWORD dwWritten; + + if (dataFlags & CHANNEL_FLAG_SUSPEND) + { + if (debug) + puts("rdp2tcp Channel Suspend"); + + return; + } + + if (dataFlags & CHANNEL_FLAG_RESUME) + { + if (debug) + puts("rdp2tcp Channel Resume"); + + return; + } + + if (debug > 1) + { + printf("<%c%3u/%3u ", dataFlags & CHANNEL_FLAG_FIRST ? ' ' : '+', totalLength, dataLength); + dumpData(pData, dataLength); + } + + if (dataFlags & CHANNEL_FLAG_FIRST) + { + if (!WriteFile(plugin->hStdInputWrite, &totalLength, sizeof(totalLength), &dwWritten, NULL)) + closeChannel(plugin); + } + + if (!WriteFile(plugin->hStdInputWrite, pData, dataLength, &dwWritten, NULL)) + closeChannel(plugin); +} + +static void VCAPITYPE VirtualChannelOpenEventEx(LPVOID lpUserParam, DWORD openHandle, UINT event, + LPVOID pData, UINT32 dataLength, UINT32 totalLength, + UINT32 dataFlags) +{ + Plugin* plugin = (Plugin*)lpUserParam; + + switch (event) + { + case CHANNEL_EVENT_DATA_RECEIVED: + dataReceived(plugin, pData, dataLength, totalLength, dataFlags); + break; + + case CHANNEL_EVENT_WRITE_CANCELLED: + free(pData); + break; + case CHANNEL_EVENT_WRITE_COMPLETE: + SetEvent(plugin->writeComplete); + free(pData); + break; + } +} + +static VOID VCAPITYPE VirtualChannelInitEventEx(LPVOID lpUserParam, LPVOID pInitHandle, UINT event, + LPVOID pData, UINT dataLength) +{ + Plugin* plugin = (Plugin*)lpUserParam; + + switch (event) + { + case CHANNEL_EVENT_CONNECTED: + if (debug) + puts("rdp2tcp connected"); + + plugin->writeComplete = CreateEvent(NULL, TRUE, FALSE, NULL); + plugin->copyThread = CreateThread(NULL, 0, copyThread, plugin, 0, NULL); + + if (plugin->channelEntryPoints.pVirtualChannelOpenEx( + pInitHandle, &plugin->openHandle, RDP2TCP_CHAN_NAME, + VirtualChannelOpenEventEx) != CHANNEL_RC_OK) + return; + + break; + + case CHANNEL_EVENT_DISCONNECTED: + if (debug) + puts("rdp2tcp disconnected"); + + break; + + case CHANNEL_EVENT_TERMINATED: + if (debug) + puts("rdp2tcp terminated"); + + if (plugin->copyThread) + { + TerminateThread(plugin->copyThread, 0); + CloseHandle(plugin->writeComplete); + } + + CloseHandle(plugin->hStdInputWrite); + CloseHandle(plugin->hStdOutputRead); + TerminateProcess(plugin->hProcess, 0); + CloseHandle(plugin->hProcess); + free(plugin); + break; + } +} + +#if 1 +#define VirtualChannelEntryEx rdp2tcp_VirtualChannelEntryEx +#else +#define VirtualChannelEntryEx FREERDP_API VirtualChannelEntryEx +#endif +BOOL VCAPITYPE VirtualChannelEntryEx(PCHANNEL_ENTRY_POINTS pEntryPoints, PVOID pInitHandle) +{ + CHANNEL_ENTRY_POINTS_FREERDP_EX* pEntryPointsEx; + CHANNEL_DEF channelDef; + Plugin* plugin = (Plugin*)calloc(1, sizeof(Plugin)); + + if (!plugin) + return FALSE; + + pEntryPointsEx = (CHANNEL_ENTRY_POINTS_FREERDP_EX*)pEntryPoints; + assert(pEntryPointsEx->cbSize >= sizeof(CHANNEL_ENTRY_POINTS_FREERDP_EX) && + pEntryPointsEx->MagicNumber == FREERDP_CHANNEL_MAGIC_NUMBER); + plugin->initHandle = pInitHandle; + plugin->channelEntryPoints = *pEntryPointsEx; + + if (init_external_addin(plugin) < 0) + return FALSE; + + strncpy(channelDef.name, RDP2TCP_CHAN_NAME, sizeof(channelDef.name)); + channelDef.options = + CHANNEL_OPTION_INITIALIZED | CHANNEL_OPTION_ENCRYPT_RDP | CHANNEL_OPTION_COMPRESS_RDP; + + if (pEntryPointsEx->pVirtualChannelInitEx(plugin, NULL, pInitHandle, &channelDef, 1, + VIRTUAL_CHANNEL_VERSION_WIN2000, + VirtualChannelInitEventEx) != CHANNEL_RC_OK) + return FALSE; + + return TRUE; +} + +// vim:ts=4 diff --git a/channels/rdpdr/CMakeLists.txt b/channels/rdpdr/CMakeLists.txt new file mode 100644 index 0000000..e9b4181 --- /dev/null +++ b/channels/rdpdr/CMakeLists.txt @@ -0,0 +1,26 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2012 Marc-Andre Moreau +# +# Licensed 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. + +define_channel("rdpdr") + +if(WITH_CLIENT_CHANNELS) + add_channel_client(${MODULE_PREFIX} ${CHANNEL_NAME}) +endif() + +if(WITH_SERVER_CHANNELS) + add_channel_server(${MODULE_PREFIX} ${CHANNEL_NAME}) +endif() diff --git a/channels/rdpdr/ChannelOptions.cmake b/channels/rdpdr/ChannelOptions.cmake new file mode 100644 index 0000000..9a96676 --- /dev/null +++ b/channels/rdpdr/ChannelOptions.cmake @@ -0,0 +1,13 @@ + +set(OPTION_DEFAULT OFF) +set(OPTION_CLIENT_DEFAULT ON) +set(OPTION_SERVER_DEFAULT ON) + +define_channel_options(NAME "rdpdr" TYPE "static" + DESCRIPTION "Device Redirection Virtual Channel Extension" + SPECIFICATIONS "[MS-RDPEFS] [MS-RDPEPC] [MS-RDPESC] [MS-RDPESP]" + DEFAULT ${OPTION_DEFAULT}) + +define_channel_client_options(${OPTION_CLIENT_DEFAULT}) +define_channel_server_options(${OPTION_SERVER_DEFAULT}) + diff --git a/channels/rdpdr/client/CMakeLists.txt b/channels/rdpdr/client/CMakeLists.txt new file mode 100644 index 0000000..e41cc3c --- /dev/null +++ b/channels/rdpdr/client/CMakeLists.txt @@ -0,0 +1,43 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2012 Marc-Andre Moreau +# Copyright 2016 Inuvika Inc. +# Copyright 2016 David PHAM-VAN +# +# Licensed 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. + +define_channel_client("rdpdr") + +set(${MODULE_PREFIX}_SRCS + irp.c + irp.h + devman.c + devman.h + rdpdr_main.c + rdpdr_main.h + rdpdr_capabilities.c + rdpdr_capabilities.h) + +add_channel_client_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} FALSE "VirtualChannelEntryEx") + + + +target_link_libraries(${MODULE_NAME} winpr freerdp) +if(APPLE AND (NOT IOS)) + find_library(CORESERVICES_LIBRARY CoreServices) + target_link_libraries(${MODULE_NAME} ${CORESERVICES_LIBRARY}) +endif() + + +set_property(TARGET ${MODULE_NAME} PROPERTY FOLDER "Channels/${CHANNEL_NAME}/Client") diff --git a/channels/rdpdr/client/devman.c b/channels/rdpdr/client/devman.c new file mode 100644 index 0000000..af3cdd6 --- /dev/null +++ b/channels/rdpdr/client/devman.c @@ -0,0 +1,231 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Device Redirection Virtual Channel + * + * Copyright 2010-2011 Vic Lee + * Copyright 2010-2012 Marc-Andre Moreau + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger + * Copyright 2016 Armin Novak + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include + +#include +#include + +#include +#include +#include +#include + +#include "rdpdr_main.h" + +#include "devman.h" + +static void devman_device_free(void* obj) +{ + DEVICE* device = (DEVICE*)obj; + + if (!device) + return; + + IFCALL(device->Free, device); +} + +DEVMAN* devman_new(rdpdrPlugin* rdpdr) +{ + DEVMAN* devman; + + if (!rdpdr) + return NULL; + + devman = (DEVMAN*)calloc(1, sizeof(DEVMAN)); + + if (!devman) + { + WLog_INFO(TAG, "calloc failed!"); + return NULL; + } + + devman->plugin = (void*)rdpdr; + devman->id_sequence = 1; + devman->devices = ListDictionary_New(TRUE); + + if (!devman->devices) + { + WLog_INFO(TAG, "ListDictionary_New failed!"); + free(devman); + return NULL; + } + + ListDictionary_ValueObject(devman->devices)->fnObjectFree = devman_device_free; + return devman; +} + +void devman_free(DEVMAN* devman) +{ + ListDictionary_Free(devman->devices); + free(devman); +} + +void devman_unregister_device(DEVMAN* devman, void* key) +{ + DEVICE* device; + + if (!devman || !key) + return; + + device = (DEVICE*)ListDictionary_Remove(devman->devices, key); + + if (device) + devman_device_free(device); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT devman_register_device(DEVMAN* devman, DEVICE* device) +{ + void* key = NULL; + + if (!devman || !device) + return ERROR_INVALID_PARAMETER; + + device->id = devman->id_sequence++; + key = (void*)(size_t)device->id; + + if (!ListDictionary_Add(devman->devices, key, device)) + { + WLog_INFO(TAG, "ListDictionary_Add failed!"); + return ERROR_INTERNAL_ERROR; + } + + return CHANNEL_RC_OK; +} + +DEVICE* devman_get_device_by_id(DEVMAN* devman, UINT32 id) +{ + DEVICE* device = NULL; + void* key = (void*)(size_t)id; + + if (!devman) + return NULL; + + device = (DEVICE*)ListDictionary_GetItemValue(devman->devices, key); + return device; +} + +DEVICE* devman_get_device_by_type(DEVMAN* devman, UINT32 type) +{ + DEVICE* device = NULL; + ULONG_PTR* keys; + int count, x; + + if (!devman) + return NULL; + + ListDictionary_Lock(devman->devices); + count = ListDictionary_GetKeys(devman->devices, &keys); + + for (x = 0; x < count; x++) + { + DEVICE* cur = (DEVICE*)ListDictionary_GetItemValue(devman->devices, (void*)keys[x]); + + if (!cur) + continue; + + if (cur->type != type) + continue; + + device = cur; + break; + } + + free(keys); + ListDictionary_Unlock(devman->devices); + return device; +} + +static const char DRIVE_SERVICE_NAME[] = "drive"; +static const char PRINTER_SERVICE_NAME[] = "printer"; +static const char SMARTCARD_SERVICE_NAME[] = "smartcard"; +static const char SERIAL_SERVICE_NAME[] = "serial"; +static const char PARALLEL_SERVICE_NAME[] = "parallel"; + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT devman_load_device_service(DEVMAN* devman, const RDPDR_DEVICE* device, rdpContext* rdpcontext) +{ + const char* ServiceName = NULL; + DEVICE_SERVICE_ENTRY_POINTS ep; + PDEVICE_SERVICE_ENTRY entry = NULL; + union { + const RDPDR_DEVICE* cdp; + RDPDR_DEVICE* dp; + } devconv; + + devconv.cdp = device; + if (!devman || !device || !rdpcontext) + return ERROR_INVALID_PARAMETER; + + if (device->Type == RDPDR_DTYP_FILESYSTEM) + ServiceName = DRIVE_SERVICE_NAME; + else if (device->Type == RDPDR_DTYP_PRINT) + ServiceName = PRINTER_SERVICE_NAME; + else if (device->Type == RDPDR_DTYP_SMARTCARD) + ServiceName = SMARTCARD_SERVICE_NAME; + else if (device->Type == RDPDR_DTYP_SERIAL) + ServiceName = SERIAL_SERVICE_NAME; + else if (device->Type == RDPDR_DTYP_PARALLEL) + ServiceName = PARALLEL_SERVICE_NAME; + + if (!ServiceName) + { + WLog_INFO(TAG, "ServiceName %s did not match!", ServiceName); + return ERROR_INVALID_NAME; + } + + if (device->Name) + WLog_INFO(TAG, "Loading device service %s [%s] (static)", ServiceName, device->Name); + else + WLog_INFO(TAG, "Loading device service %s (static)", ServiceName); + + entry = (PDEVICE_SERVICE_ENTRY)freerdp_load_channel_addin_entry(ServiceName, NULL, + "DeviceServiceEntry", 0); + + if (!entry) + { + WLog_INFO(TAG, "freerdp_load_channel_addin_entry failed!"); + return ERROR_INTERNAL_ERROR; + } + + ep.devman = devman; + ep.RegisterDevice = devman_register_device; + ep.device = devconv.dp; + ep.rdpcontext = rdpcontext; + return entry(&ep); +} diff --git a/channels/rdpdr/client/devman.h b/channels/rdpdr/client/devman.h new file mode 100644 index 0000000..2e6019e --- /dev/null +++ b/channels/rdpdr/client/devman.h @@ -0,0 +1,36 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Device Redirection Virtual Channel + * + * Copyright 2010-2011 Vic Lee + * Copyright 2010-2012 Marc-Andre Moreau + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger + * + * Licensed 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 FREERDP_CHANNEL_RDPDR_CLIENT_DEVMAN_H +#define FREERDP_CHANNEL_RDPDR_CLIENT_DEVMAN_H + +#include "rdpdr_main.h" + +void devman_unregister_device(DEVMAN* devman, void* key); +UINT devman_load_device_service(DEVMAN* devman, const RDPDR_DEVICE* device, rdpContext* rdpcontext); +DEVICE* devman_get_device_by_id(DEVMAN* devman, UINT32 id); +DEVICE* devman_get_device_by_type(DEVMAN* devman, UINT32 type); + +DEVMAN* devman_new(rdpdrPlugin* rdpdr); +void devman_free(DEVMAN* devman); + +#endif /* FREERDP_CHANNEL_RDPDR_CLIENT_DEVMAN_H */ diff --git a/channels/rdpdr/client/irp.c b/channels/rdpdr/client/irp.c new file mode 100644 index 0000000..b818b67 --- /dev/null +++ b/channels/rdpdr/client/irp.c @@ -0,0 +1,150 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Device Redirection Virtual Channel + * + * Copyright 2010-2011 Vic Lee + * Copyright 2010-2012 Marc-Andre Moreau + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include + +#include +#include + +#include "rdpdr_main.h" +#include "devman.h" +#include "irp.h" + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT irp_free(IRP* irp) +{ + if (!irp) + return CHANNEL_RC_OK; + + Stream_Free(irp->input, TRUE); + Stream_Free(irp->output, TRUE); + + _aligned_free(irp); + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT irp_complete(IRP* irp) +{ + size_t pos; + rdpdrPlugin* rdpdr; + UINT error; + + rdpdr = (rdpdrPlugin*)irp->devman->plugin; + + pos = Stream_GetPosition(irp->output); + Stream_SetPosition(irp->output, RDPDR_DEVICE_IO_RESPONSE_LENGTH - 4); + Stream_Write_UINT32(irp->output, irp->IoStatus); /* IoStatus (4 bytes) */ + Stream_SetPosition(irp->output, pos); + + error = rdpdr_send(rdpdr, irp->output); + irp->output = NULL; + + return irp_free(irp); +} + +IRP* irp_new(DEVMAN* devman, wStream* s, UINT* error) +{ + IRP* irp; + DEVICE* device; + UINT32 DeviceId; + + if (Stream_GetRemainingLength(s) < 20) + { + if (error) + *error = ERROR_INVALID_DATA; + return NULL; + } + + Stream_Read_UINT32(s, DeviceId); /* DeviceId (4 bytes) */ + device = devman_get_device_by_id(devman, DeviceId); + + if (!device) + { + WLog_WARN(TAG, "devman_get_device_by_id failed!"); + if (error) + *error = CHANNEL_RC_OK; + + return NULL; + }; + + irp = (IRP*)_aligned_malloc(sizeof(IRP), MEMORY_ALLOCATION_ALIGNMENT); + + if (!irp) + { + WLog_ERR(TAG, "_aligned_malloc failed!"); + if (error) + *error = CHANNEL_RC_NO_MEMORY; + return NULL; + } + + ZeroMemory(irp, sizeof(IRP)); + + irp->input = s; + irp->device = device; + irp->devman = devman; + + Stream_Read_UINT32(s, irp->FileId); /* FileId (4 bytes) */ + Stream_Read_UINT32(s, irp->CompletionId); /* CompletionId (4 bytes) */ + Stream_Read_UINT32(s, irp->MajorFunction); /* MajorFunction (4 bytes) */ + Stream_Read_UINT32(s, irp->MinorFunction); /* MinorFunction (4 bytes) */ + + irp->output = Stream_New(NULL, 256); + if (!irp->output) + { + WLog_ERR(TAG, "Stream_New failed!"); + _aligned_free(irp); + if (error) + *error = CHANNEL_RC_NO_MEMORY; + return NULL; + } + Stream_Write_UINT16(irp->output, RDPDR_CTYP_CORE); /* Component (2 bytes) */ + Stream_Write_UINT16(irp->output, PAKID_CORE_DEVICE_IOCOMPLETION); /* PacketId (2 bytes) */ + Stream_Write_UINT32(irp->output, DeviceId); /* DeviceId (4 bytes) */ + Stream_Write_UINT32(irp->output, irp->CompletionId); /* CompletionId (4 bytes) */ + Stream_Write_UINT32(irp->output, 0); /* IoStatus (4 bytes) */ + + irp->Complete = irp_complete; + irp->Discard = irp_free; + + irp->thread = NULL; + irp->cancelled = FALSE; + + if (error) + *error = CHANNEL_RC_OK; + + return irp; +} diff --git a/channels/rdpdr/client/irp.h b/channels/rdpdr/client/irp.h new file mode 100644 index 0000000..17d75ac --- /dev/null +++ b/channels/rdpdr/client/irp.h @@ -0,0 +1,28 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Device Redirection Virtual Channel + * + * Copyright 2010-2011 Vic Lee + * Copyright 2010-2012 Marc-Andre Moreau + * + * Licensed 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 FREERDP_CHANNEL_RDPDR_CLIENT_IRP_H +#define FREERDP_CHANNEL_RDPDR_CLIENT_IRP_H + +#include "rdpdr_main.h" + +IRP* irp_new(DEVMAN* devman, wStream* s, UINT* error); + +#endif /* FREERDP_CHANNEL_RDPDR_CLIENT_IRP_H */ diff --git a/channels/rdpdr/client/rdpdr_capabilities.c b/channels/rdpdr/client/rdpdr_capabilities.c new file mode 100644 index 0000000..97c876b --- /dev/null +++ b/channels/rdpdr/client/rdpdr_capabilities.c @@ -0,0 +1,281 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Device Redirection Virtual Channel + * + * Copyright 2010-2011 Vic Lee + * Copyright 2010-2012 Marc-Andre Moreau + * Copyright 2015-2016 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger + * Copyright 2016 Armin Novak + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include + +#include +#include + +#include "rdpdr_main.h" +#include "rdpdr_capabilities.h" + +/* Output device redirection capability set header */ +static void rdpdr_write_capset_header(wStream* s, UINT16 capabilityType, UINT16 capabilityLength, + UINT32 version) +{ + Stream_Write_UINT16(s, capabilityType); + Stream_Write_UINT16(s, capabilityLength); + Stream_Write_UINT32(s, version); +} + +/* Output device direction general capability set */ +static void rdpdr_write_general_capset(rdpdrPlugin* rdpdr, wStream* s) +{ + WINPR_UNUSED(rdpdr); + rdpdr_write_capset_header(s, CAP_GENERAL_TYPE, 44, GENERAL_CAPABILITY_VERSION_02); + Stream_Write_UINT32(s, 0); /* osType, ignored on receipt */ + Stream_Write_UINT32(s, 0); /* osVersion, unused and must be set to zero */ + Stream_Write_UINT16(s, 1); /* protocolMajorVersion, must be set to 1 */ + Stream_Write_UINT16(s, RDPDR_MINOR_RDP_VERSION_5_2); /* protocolMinorVersion */ + Stream_Write_UINT32(s, 0x0000FFFF); /* ioCode1 */ + Stream_Write_UINT32(s, 0); /* ioCode2, must be set to zero, reserved for future use */ + Stream_Write_UINT32(s, RDPDR_DEVICE_REMOVE_PDUS | RDPDR_CLIENT_DISPLAY_NAME_PDU | + RDPDR_USER_LOGGEDON_PDU); /* extendedPDU */ + Stream_Write_UINT32(s, ENABLE_ASYNCIO); /* extraFlags1 */ + Stream_Write_UINT32(s, 0); /* extraFlags2, must be set to zero, reserved for future use */ + Stream_Write_UINT32( + s, 0); /* SpecialTypeDeviceCap, number of special devices to be redirected before logon */ +} + +/* Process device direction general capability set */ +static UINT rdpdr_process_general_capset(rdpdrPlugin* rdpdr, wStream* s) +{ + UINT16 capabilityLength; + WINPR_UNUSED(rdpdr); + + if (Stream_GetRemainingLength(s) < 2) + return ERROR_INVALID_DATA; + + Stream_Read_UINT16(s, capabilityLength); + + if (capabilityLength < 4) + return ERROR_INVALID_DATA; + + if (Stream_GetRemainingLength(s) < capabilityLength - 4U) + return ERROR_INVALID_DATA; + + Stream_Seek(s, capabilityLength - 4U); + return CHANNEL_RC_OK; +} + +/* Output printer direction capability set */ +static void rdpdr_write_printer_capset(rdpdrPlugin* rdpdr, wStream* s) +{ + WINPR_UNUSED(rdpdr); + rdpdr_write_capset_header(s, CAP_PRINTER_TYPE, 8, PRINT_CAPABILITY_VERSION_01); +} + +/* Process printer direction capability set */ +static UINT rdpdr_process_printer_capset(rdpdrPlugin* rdpdr, wStream* s) +{ + UINT16 capabilityLength; + WINPR_UNUSED(rdpdr); + + if (Stream_GetRemainingLength(s) < 2) + return ERROR_INVALID_DATA; + + Stream_Read_UINT16(s, capabilityLength); + + if (capabilityLength < 4) + return ERROR_INVALID_DATA; + + if (Stream_GetRemainingLength(s) < capabilityLength - 4U) + return ERROR_INVALID_DATA; + + Stream_Seek(s, capabilityLength - 4U); + return CHANNEL_RC_OK; +} + +/* Output port redirection capability set */ +static void rdpdr_write_port_capset(rdpdrPlugin* rdpdr, wStream* s) +{ + WINPR_UNUSED(rdpdr); + rdpdr_write_capset_header(s, CAP_PORT_TYPE, 8, PORT_CAPABILITY_VERSION_01); +} + +/* Process port redirection capability set */ +static UINT rdpdr_process_port_capset(rdpdrPlugin* rdpdr, wStream* s) +{ + UINT16 capabilityLength; + WINPR_UNUSED(rdpdr); + + if (Stream_GetRemainingLength(s) < 2) + return ERROR_INVALID_DATA; + + Stream_Read_UINT16(s, capabilityLength); + + if (capabilityLength < 4U) + return ERROR_INVALID_DATA; + + if (Stream_GetRemainingLength(s) < capabilityLength - 4U) + return ERROR_INVALID_DATA; + + Stream_Seek(s, capabilityLength - 4U); + return CHANNEL_RC_OK; +} + +/* Output drive redirection capability set */ +static void rdpdr_write_drive_capset(rdpdrPlugin* rdpdr, wStream* s) +{ + WINPR_UNUSED(rdpdr); + rdpdr_write_capset_header(s, CAP_DRIVE_TYPE, 8, DRIVE_CAPABILITY_VERSION_02); +} + +/* Process drive redirection capability set */ +static UINT rdpdr_process_drive_capset(rdpdrPlugin* rdpdr, wStream* s) +{ + UINT16 capabilityLength; + WINPR_UNUSED(rdpdr); + + if (Stream_GetRemainingLength(s) < 2) + return ERROR_INVALID_DATA; + + Stream_Read_UINT16(s, capabilityLength); + + if (capabilityLength < 4) + return ERROR_INVALID_DATA; + + if (Stream_GetRemainingLength(s) < capabilityLength - 4U) + return ERROR_INVALID_DATA; + + Stream_Seek(s, capabilityLength - 4U); + return CHANNEL_RC_OK; +} + +/* Output smart card redirection capability set */ +static void rdpdr_write_smartcard_capset(rdpdrPlugin* rdpdr, wStream* s) +{ + WINPR_UNUSED(rdpdr); + rdpdr_write_capset_header(s, CAP_SMARTCARD_TYPE, 8, SMARTCARD_CAPABILITY_VERSION_01); +} + +/* Process smartcard redirection capability set */ +static UINT rdpdr_process_smartcard_capset(rdpdrPlugin* rdpdr, wStream* s) +{ + UINT16 capabilityLength; + WINPR_UNUSED(rdpdr); + + if (Stream_GetRemainingLength(s) < 2) + return ERROR_INVALID_DATA; + + Stream_Read_UINT16(s, capabilityLength); + + if (capabilityLength < 4) + return ERROR_INVALID_DATA; + + if (Stream_GetRemainingLength(s) < capabilityLength - 4U) + return ERROR_INVALID_DATA; + + Stream_Seek(s, capabilityLength - 4U); + return CHANNEL_RC_OK; +} + +UINT rdpdr_process_capability_request(rdpdrPlugin* rdpdr, wStream* s) +{ + UINT status = CHANNEL_RC_OK; + UINT16 i; + UINT16 numCapabilities; + UINT16 capabilityType; + + if (!rdpdr || !s) + return CHANNEL_RC_NULL_DATA; + + if (Stream_GetRemainingLength(s) < 4) + return ERROR_INVALID_DATA; + + Stream_Read_UINT16(s, numCapabilities); + Stream_Seek(s, 2); /* pad (2 bytes) */ + + for (i = 0; i < numCapabilities; i++) + { + if (Stream_GetRemainingLength(s) < sizeof(UINT16)) + return ERROR_INVALID_DATA; + + Stream_Read_UINT16(s, capabilityType); + + switch (capabilityType) + { + case CAP_GENERAL_TYPE: + status = rdpdr_process_general_capset(rdpdr, s); + break; + + case CAP_PRINTER_TYPE: + status = rdpdr_process_printer_capset(rdpdr, s); + break; + + case CAP_PORT_TYPE: + status = rdpdr_process_port_capset(rdpdr, s); + break; + + case CAP_DRIVE_TYPE: + status = rdpdr_process_drive_capset(rdpdr, s); + break; + + case CAP_SMARTCARD_TYPE: + status = rdpdr_process_smartcard_capset(rdpdr, s); + break; + + default: + break; + } + + if (status != CHANNEL_RC_OK) + return status; + } + + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT rdpdr_send_capability_response(rdpdrPlugin* rdpdr) +{ + wStream* s; + s = Stream_New(NULL, 256); + + if (!s) + { + WLog_ERR(TAG, "Stream_New failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + Stream_Write_UINT16(s, RDPDR_CTYP_CORE); + Stream_Write_UINT16(s, PAKID_CORE_CLIENT_CAPABILITY); + Stream_Write_UINT16(s, 5); /* numCapabilities */ + Stream_Write_UINT16(s, 0); /* pad */ + rdpdr_write_general_capset(rdpdr, s); + rdpdr_write_printer_capset(rdpdr, s); + rdpdr_write_port_capset(rdpdr, s); + rdpdr_write_drive_capset(rdpdr, s); + rdpdr_write_smartcard_capset(rdpdr, s); + return rdpdr_send(rdpdr, s); +} diff --git a/channels/rdpdr/client/rdpdr_capabilities.h b/channels/rdpdr/client/rdpdr_capabilities.h new file mode 100644 index 0000000..d4e1ecb --- /dev/null +++ b/channels/rdpdr/client/rdpdr_capabilities.h @@ -0,0 +1,31 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Device Redirection Virtual Channel + * + * Copyright 2010-2011 Vic Lee + * Copyright 2010-2012 Marc-Andre Moreau + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger + * + * Licensed 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 FREERDP_CHANNEL_RDPDR_CLIENT_CAPABILITIES_H +#define FREERDP_CHANNEL_RDPDR_CLIENT_CAPABILITIES_H + +#include "rdpdr_main.h" + +UINT rdpdr_process_capability_request(rdpdrPlugin* rdpdr, wStream* s); +UINT rdpdr_send_capability_response(rdpdrPlugin* rdpdr); + +#endif /* FREERDP_CHANNEL_RDPDR_CLIENT_CAPABILITIES_H */ diff --git a/channels/rdpdr/client/rdpdr_main.c b/channels/rdpdr/client/rdpdr_main.c new file mode 100644 index 0000000..681ebce --- /dev/null +++ b/channels/rdpdr/client/rdpdr_main.c @@ -0,0 +1,1922 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Device Redirection Virtual Channel + * + * Copyright 2010-2011 Vic Lee + * Copyright 2010-2012 Marc-Andre Moreau + * Copyright 2015-2016 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger + * Copyright 2016 Armin Novak + * Copyright 2016 David PHAM-VAN + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include + +#include +#include +#include + +#include + +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#include +#else +#include +#include +#include +#endif + +#ifdef __MACOSX__ +#include +#include +#include +#include +#include +#include +#endif + +#ifdef HAVE_UNISTD_H +#include +#endif + +#include "rdpdr_capabilities.h" + +#include "devman.h" +#include "irp.h" + +#include "rdpdr_main.h" + +typedef struct _DEVICE_DRIVE_EXT DEVICE_DRIVE_EXT; +/* IMPORTANT: Keep in sync with DRIVE_DEVICE */ +struct _DEVICE_DRIVE_EXT +{ + DEVICE device; + WCHAR* path; + BOOL automount; +}; + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpdr_send_device_list_announce_request(rdpdrPlugin* rdpdr, BOOL userLoggedOn); + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpdr_send_device_list_remove_request(rdpdrPlugin* rdpdr, UINT32 count, UINT32 ids[]) +{ + UINT32 i; + wStream* s; + s = Stream_New(NULL, count * sizeof(UINT32) + 8); + + if (!s) + { + WLog_ERR(TAG, "Stream_New failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + Stream_Write_UINT16(s, RDPDR_CTYP_CORE); + Stream_Write_UINT16(s, PAKID_CORE_DEVICELIST_REMOVE); + Stream_Write_UINT32(s, count); + + for (i = 0; i < count; i++) + Stream_Write_UINT32(s, ids[i]); + + Stream_SealLength(s); + return rdpdr_send(rdpdr, s); +} + +#if defined(_UWP) || defined(__IOS__) + +void first_hotplug(rdpdrPlugin* rdpdr) +{ +} + +static DWORD WINAPI drive_hotplug_thread_func(LPVOID arg) +{ + return CHANNEL_RC_OK; +} + +static UINT drive_hotplug_thread_terminate(rdpdrPlugin* rdpdr) +{ + return CHANNEL_RC_OK; +} + +#elif _WIN32 + +BOOL check_path(const char* path) +{ + UINT type = GetDriveTypeA(path); + + if (!(type == DRIVE_FIXED || type == DRIVE_REMOVABLE || type == DRIVE_CDROM || + type == DRIVE_REMOTE)) + return FALSE; + + return GetVolumeInformationA(path, NULL, 0, NULL, NULL, NULL, NULL, 0); +} + +void first_hotplug(rdpdrPlugin* rdpdr) +{ + size_t i; + DWORD unitmask = GetLogicalDrives(); + + for (i = 0; i < 26; i++) + { + if (unitmask & 0x01) + { + char drive_path[] = { 'c', ':', '\\', '\0' }; + char drive_name[] = { 'c', '\0' }; + RDPDR_DRIVE drive = { 0 }; + drive_path[0] = 'A' + (char)i; + drive_name[0] = 'A' + (char)i; + + if (check_path(drive_path)) + { + drive.Type = RDPDR_DTYP_FILESYSTEM; + drive.Path = drive_path; + drive.Name = drive_name; + drive.automount = TRUE; + devman_load_device_service(rdpdr->devman, (const RDPDR_DEVICE*)&drive, + rdpdr->rdpcontext); + } + } + + unitmask = unitmask >> 1; + } +} + +LRESULT CALLBACK hotplug_proc(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam) +{ + rdpdrPlugin* rdpdr; + PDEV_BROADCAST_HDR lpdb = (PDEV_BROADCAST_HDR)lParam; + UINT error; + rdpdr = (rdpdrPlugin*)GetWindowLongPtr(hWnd, GWLP_USERDATA); + + switch (Msg) + { + case WM_DEVICECHANGE: + switch (wParam) + { + case DBT_DEVICEARRIVAL: + if (lpdb->dbch_devicetype == DBT_DEVTYP_VOLUME) + { + PDEV_BROADCAST_VOLUME lpdbv = (PDEV_BROADCAST_VOLUME)lpdb; + DWORD unitmask = lpdbv->dbcv_unitmask; + int i; + + for (i = 0; i < 26; i++) + { + if (unitmask & 0x01) + { + char drive_path[] = { 'c', ':', '/', '\0' }; + char drive_name[] = { 'c', '\0' }; + drive_path[0] = 'A' + (char)i; + drive_name[0] = 'A' + (char)i; + + if (check_path(drive_path)) + { + RDPDR_DRIVE drive = { 0 }; + + drive.Type = RDPDR_DTYP_FILESYSTEM; + drive.Path = drive_path; + drive.automount = TRUE; + drive.Name = drive_name; + devman_load_device_service(rdpdr->devman, + (const RDPDR_DEVICE*)&drive, + rdpdr->rdpcontext); + rdpdr_send_device_list_announce_request(rdpdr, TRUE); + } + } + + unitmask = unitmask >> 1; + } + } + + break; + + case DBT_DEVICEREMOVECOMPLETE: + if (lpdb->dbch_devicetype == DBT_DEVTYP_VOLUME) + { + PDEV_BROADCAST_VOLUME lpdbv = (PDEV_BROADCAST_VOLUME)lpdb; + DWORD unitmask = lpdbv->dbcv_unitmask; + int i, j, count; + char drive_name_upper, drive_name_lower; + ULONG_PTR* keys = NULL; + DEVICE_DRIVE_EXT* device_ext; + UINT32 ids[1]; + + for (i = 0; i < 26; i++) + { + if (unitmask & 0x01) + { + drive_name_upper = 'A' + i; + drive_name_lower = 'a' + i; + count = ListDictionary_GetKeys(rdpdr->devman->devices, &keys); + + for (j = 0; j < count; j++) + { + device_ext = (DEVICE_DRIVE_EXT*)ListDictionary_GetItemValue( + rdpdr->devman->devices, (void*)keys[j]); + + if (device_ext->device.type != RDPDR_DTYP_FILESYSTEM) + continue; + + if (device_ext->path[0] == drive_name_upper || + device_ext->path[0] == drive_name_lower) + { + if (device_ext->automount) + { + devman_unregister_device(rdpdr->devman, (void*)keys[j]); + ids[0] = keys[j]; + + if ((error = rdpdr_send_device_list_remove_request( + rdpdr, 1, ids))) + { + // dont end on error, just report ? + WLog_ERR( + TAG, + "rdpdr_send_device_list_remove_request failed " + "with error %" PRIu32 "!", + error); + } + + break; + } + } + } + + free(keys); + } + + unitmask = unitmask >> 1; + } + } + + break; + + default: + break; + } + + break; + + default: + return DefWindowProc(hWnd, Msg, wParam, lParam); + } + + return DefWindowProc(hWnd, Msg, wParam, lParam); +} + +static DWORD WINAPI drive_hotplug_thread_func(LPVOID arg) +{ + rdpdrPlugin* rdpdr; + WNDCLASSEX wnd_cls; + HWND hwnd; + MSG msg; + BOOL bRet; + DEV_BROADCAST_HANDLE NotificationFilter; + HDEVNOTIFY hDevNotify; + rdpdr = (rdpdrPlugin*)arg; + /* init windows class */ + wnd_cls.cbSize = sizeof(WNDCLASSEX); + wnd_cls.style = CS_HREDRAW | CS_VREDRAW; + wnd_cls.lpfnWndProc = hotplug_proc; + wnd_cls.cbClsExtra = 0; + wnd_cls.cbWndExtra = 0; + wnd_cls.hIcon = LoadIcon(NULL, IDI_APPLICATION); + wnd_cls.hCursor = NULL; + wnd_cls.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH); + wnd_cls.lpszMenuName = NULL; + wnd_cls.lpszClassName = L"DRIVE_HOTPLUG"; + wnd_cls.hInstance = NULL; + wnd_cls.hIconSm = LoadIcon(NULL, IDI_APPLICATION); + RegisterClassEx(&wnd_cls); + /* create window */ + hwnd = CreateWindowEx(0, L"DRIVE_HOTPLUG", NULL, 0, 0, 0, 0, 0, NULL, NULL, NULL, NULL); + SetWindowLongPtr(hwnd, GWLP_USERDATA, (LONG_PTR)rdpdr); + rdpdr->hotplug_wnd = hwnd; + /* register device interface to hwnd */ + NotificationFilter.dbch_size = sizeof(DEV_BROADCAST_HANDLE); + NotificationFilter.dbch_devicetype = DBT_DEVTYP_HANDLE; + hDevNotify = RegisterDeviceNotification(hwnd, &NotificationFilter, DEVICE_NOTIFY_WINDOW_HANDLE); + + /* message loop */ + while ((bRet = GetMessage(&msg, 0, 0, 0)) != 0) + { + if (bRet == -1) + { + break; + } + else + { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + } + + UnregisterDeviceNotification(hDevNotify); + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT drive_hotplug_thread_terminate(rdpdrPlugin* rdpdr) +{ + UINT error = CHANNEL_RC_OK; + + if (rdpdr->hotplug_wnd && !PostMessage(rdpdr->hotplug_wnd, WM_QUIT, 0, 0)) + { + error = GetLastError(); + WLog_ERR(TAG, "PostMessage failed with error %" PRIu32 "", error); + } + + return error; +} + +#elif defined(__MACOSX__) + +#define MAX_USB_DEVICES 100 + +typedef struct _hotplug_dev +{ + char* path; + BOOL to_add; +} hotplug_dev; + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT handle_hotplug(rdpdrPlugin* rdpdr) +{ + struct dirent* pDirent; + DIR* pDir; + char fullpath[PATH_MAX]; + char* szdir = (char*)"/Volumes"; + struct stat buf; + hotplug_dev dev_array[MAX_USB_DEVICES]; + int count; + DEVICE_DRIVE_EXT* device_ext; + ULONG_PTR* keys = NULL; + int i, j; + int size = 0; + UINT error; + UINT32 ids[1]; + pDir = opendir(szdir); + + if (pDir == NULL) + { + printf("Cannot open directory\n"); + return ERROR_OPEN_FAILED; + } + + while ((pDirent = readdir(pDir)) != NULL) + { + if (pDirent->d_name[0] != '.') + { + sprintf_s(fullpath, ARRAYSIZE(fullpath), "%s/%s", szdir, pDirent->d_name); + if (stat(fullpath, &buf) != 0) + continue; + + if (S_ISDIR(buf.st_mode)) + { + dev_array[size].path = _strdup(fullpath); + + if (!dev_array[size].path) + { + closedir(pDir); + error = CHANNEL_RC_NO_MEMORY; + goto cleanup; + } + + dev_array[size++].to_add = TRUE; + } + } + } + + closedir(pDir); + /* delete removed devices */ + count = ListDictionary_GetKeys(rdpdr->devman->devices, &keys); + + for (j = 0; j < count; j++) + { + char* path = NULL; + BOOL dev_found = FALSE; + device_ext = + (DEVICE_DRIVE_EXT*)ListDictionary_GetItemValue(rdpdr->devman->devices, (void*)keys[j]); + + if (!device_ext || !device_ext->automount) + continue; + + if (device_ext->device.type != RDPDR_DTYP_FILESYSTEM) + continue; + + if (device_ext->path == NULL) + continue; + + if (ConvertFromUnicode(CP_UTF8, 0, device_ext->path, -1, &path, 0, NULL, FALSE) <= 0) + continue; + + /* not plugable device */ + if (strstr(path, "/Volumes/") == NULL) + { + free(path); + continue; + } + + for (i = 0; i < size; i++) + { + if (strstr(path, dev_array[i].path) != NULL) + { + dev_found = TRUE; + dev_array[i].to_add = FALSE; + break; + } + } + + free(path); + + if (!dev_found) + { + devman_unregister_device(rdpdr->devman, (void*)keys[j]); + ids[0] = keys[j]; + + if ((error = rdpdr_send_device_list_remove_request(rdpdr, 1, ids))) + { + WLog_ERR(TAG, + "rdpdr_send_device_list_remove_request failed with error %" PRIu32 "!", + error); + goto cleanup; + } + } + } + + /* add new devices */ + for (i = 0; i < size; i++) + { + if (dev_array[i].to_add) + { + RDPDR_DRIVE drive = { 0 }; + char* name; + + drive.Type = RDPDR_DTYP_FILESYSTEM; + drive.Path = dev_array[i].path; + drive.automount = TRUE; + name = strrchr(drive.Path, '/') + 1; + drive.Name = name; + + if (!drive.Name) + { + error = CHANNEL_RC_NO_MEMORY; + goto cleanup; + } + + if ((error = devman_load_device_service(rdpdr->devman, (RDPDR_DEVICE*)&drive, + rdpdr->rdpcontext))) + { + WLog_ERR(TAG, "devman_load_device_service failed!"); + error = CHANNEL_RC_NO_MEMORY; + goto cleanup; + } + } + } + +cleanup: + free(keys); + + for (i = 0; i < size; i++) + free(dev_array[i].path); + + return error; +} + +static void drive_hotplug_fsevent_callback(ConstFSEventStreamRef streamRef, + void* clientCallBackInfo, size_t numEvents, + void* eventPaths, + const FSEventStreamEventFlags eventFlags[], + const FSEventStreamEventId eventIds[]) +{ + rdpdrPlugin* rdpdr; + size_t i; + UINT error; + char** paths = (char**)eventPaths; + rdpdr = (rdpdrPlugin*)clientCallBackInfo; + + for (i = 0; i < numEvents; i++) + { + if (strcmp(paths[i], "/Volumes/") == 0) + { + if ((error = handle_hotplug(rdpdr))) + { + WLog_ERR(TAG, "handle_hotplug failed with error %" PRIu32 "!", error); + } + else + rdpdr_send_device_list_announce_request(rdpdr, TRUE); + + return; + } + } +} + +void first_hotplug(rdpdrPlugin* rdpdr) +{ + UINT error; + + if ((error = handle_hotplug(rdpdr))) + { + WLog_ERR(TAG, "handle_hotplug failed with error %" PRIu32 "!", error); + } +} + +static DWORD WINAPI drive_hotplug_thread_func(LPVOID arg) +{ + rdpdrPlugin* rdpdr; + FSEventStreamRef fsev; + rdpdr = (rdpdrPlugin*)arg; + CFStringRef path = CFSTR("/Volumes/"); + CFArrayRef pathsToWatch = CFArrayCreate(kCFAllocatorMalloc, (const void**)&path, 1, NULL); + FSEventStreamContext ctx; + ZeroMemory(&ctx, sizeof(ctx)); + ctx.info = arg; + fsev = + FSEventStreamCreate(kCFAllocatorMalloc, drive_hotplug_fsevent_callback, &ctx, pathsToWatch, + kFSEventStreamEventIdSinceNow, 1, kFSEventStreamCreateFlagNone); + rdpdr->runLoop = CFRunLoopGetCurrent(); + FSEventStreamScheduleWithRunLoop(fsev, rdpdr->runLoop, kCFRunLoopDefaultMode); + FSEventStreamStart(fsev); + CFRunLoopRun(); + FSEventStreamStop(fsev); + FSEventStreamRelease(fsev); + ExitThread(CHANNEL_RC_OK); + return CHANNEL_RC_OK; +} + +#else + +static const char* automountLocations[] = { "/run/user/%lu/gvfs", "/run/media/%s", "/media/%s", + "/media", "/mnt" }; + +static BOOL isAutomountLocation(const char* path) +{ + const size_t nrLocations = sizeof(automountLocations) / sizeof(automountLocations[0]); + size_t x; + char buffer[MAX_PATH] = { 0 }; + uid_t uid = getuid(); + char uname[MAX_PATH] = { 0 }; + ULONG size = sizeof(uname) - 1; + + if (!GetUserNameExA(NameSamCompatible, uname, &size)) + return FALSE; + + if (!path) + return FALSE; + + for (x = 0; x < nrLocations; x++) + { + const char* location = automountLocations[x]; + size_t length; + + if (strstr(location, "%lu")) + snprintf(buffer, sizeof(buffer), location, (unsigned long)uid); + else if (strstr(location, "%s")) + snprintf(buffer, sizeof(buffer), location, uname); + else + snprintf(buffer, sizeof(buffer), "%s", location); + + length = strnlen(buffer, sizeof(buffer)); + + if (strncmp(buffer, path, length) == 0) + { + const char* rest = &path[length]; + + /* Only consider mount locations with max depth of 1 below the + * base path or the base path itself. */ + if (*rest == '\0') + return TRUE; + else if (*rest == '/') + { + const char* token = strstr(&rest[1], "/"); + + if (!token || (token[1] == '\0')) + return TRUE; + } + } + } + + return FALSE; +} + +#define MAX_USB_DEVICES 100 + +typedef struct _hotplug_dev +{ + char* path; + BOOL to_add; +} hotplug_dev; + +static void handle_mountpoint(hotplug_dev* dev_array, size_t* size, const char* mountpoint) +{ + if (!mountpoint) + return; + /* copy hotpluged device mount point to the dev_array */ + if (isAutomountLocation(mountpoint) && (*size < MAX_USB_DEVICES)) + { + dev_array[*size].path = _strdup(mountpoint); + dev_array[*size].to_add = TRUE; + (*size)++; + } +} + +#ifdef __sun +#include +static UINT handle_platform_mounts_sun(hotplug_dev* dev_array, size_t* size) +{ + FILE* f; + struct mnttab ent; + f = fopen("/etc/mnttab", "r"); + if (f == NULL) + { + WLog_ERR(TAG, "fopen failed!"); + return ERROR_OPEN_FAILED; + } + while (getmntent(f, &ent) == 0) + { + handle_mountpoint(dev_array, size, ent.mnt_mountp); + } + fclose(f); + return ERROR_SUCCESS; +} +#endif + +#if defined(__FreeBSD__) || defined(__OpenBSD__) +#include +static UINT handle_platform_mounts_bsd(hotplug_dev* dev_array, size_t* size) +{ + int mntsize; + size_t idx; + struct statfs* mntbuf = NULL; + + mntsize = getmntinfo(&mntbuf, MNT_NOWAIT); + if (!mntsize) + { + /* TODO: handle 'errno' */ + WLog_ERR(TAG, "getmntinfo failed!"); + return ERROR_OPEN_FAILED; + } + for (idx = 0; idx < (size_t)mntsize; idx++) + { + handle_mountpoint(dev_array, size, mntbuf[idx].f_mntonname); + } + free(mntbuf); + return ERROR_SUCCESS; +} +#endif + +#if defined(__LINUX__) || defined(__linux__) +#include +static UINT handle_platform_mounts_linux(hotplug_dev* dev_array, size_t* size) +{ + FILE* f; + struct mntent* ent; + f = fopen("/proc/mounts", "r"); + if (f == NULL) + { + WLog_ERR(TAG, "fopen failed!"); + return ERROR_OPEN_FAILED; + } + while ((ent = getmntent(f)) != NULL) + { + handle_mountpoint(dev_array, size, ent->mnt_dir); + } + fclose(f); + return ERROR_SUCCESS; +} +#endif + +static UINT handle_platform_mounts(hotplug_dev* dev_array, size_t* size) +{ +#ifdef __sun + return handle_platform_mounts_sun(dev_array, size); +#elif defined(__FreeBSD__) || defined(__OpenBSD__) + return handle_platform_mounts_bsd(dev_array, size); +#elif defined(__LINUX__) || defined(__linux__) + return handle_platform_mounts_linux(dev_array, size); +#endif + return ERROR_CALL_NOT_IMPLEMENTED; +} + +static BOOL device_already_plugged(rdpdrPlugin* rdpdr, const hotplug_dev* device) +{ + BOOL rc = FALSE; + int count, x; + ULONG_PTR* keys = NULL; + WCHAR* path = NULL; + int status; + + if (!rdpdr || !device) + return TRUE; + if (!device->to_add) + return TRUE; + + status = ConvertToUnicode(CP_UTF8, 0, device->path, -1, &path, 0); + if (status <= 0) + return TRUE; + + ListDictionary_Lock(rdpdr->devman->devices); + count = ListDictionary_GetKeys(rdpdr->devman->devices, &keys); + for (x = 0; x < count; x++) + { + DEVICE_DRIVE_EXT* device_ext = + (DEVICE_DRIVE_EXT*)ListDictionary_GetItemValue(rdpdr->devman->devices, (void*)keys[x]); + + if (!device_ext || (device_ext->device.type != RDPDR_DTYP_FILESYSTEM) || !device_ext->path) + continue; + if (_wcscmp(device_ext->path, path) == 0) + { + rc = TRUE; + break; + } + } + free(keys); + free(path); + ListDictionary_Unlock(rdpdr->devman->devices); + return rc; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT handle_hotplug(rdpdrPlugin* rdpdr) +{ + hotplug_dev dev_array[MAX_USB_DEVICES] = { 0 }; + size_t i; + size_t size = 0; + int count, j; + ULONG_PTR* keys = NULL; + UINT32 ids[1]; + UINT error = ERROR_SUCCESS; + + error = handle_platform_mounts(dev_array, &size); + + /* delete removed devices */ + count = ListDictionary_GetKeys(rdpdr->devman->devices, &keys); + + for (j = 0; j < count; j++) + { + char* path = NULL; + BOOL dev_found = FALSE; + DEVICE_DRIVE_EXT* device_ext = + (DEVICE_DRIVE_EXT*)ListDictionary_GetItemValue(rdpdr->devman->devices, (void*)keys[j]); + + if (!device_ext || (device_ext->device.type != RDPDR_DTYP_FILESYSTEM) || + !device_ext->path || !device_ext->automount) + continue; + + ConvertFromUnicode(CP_UTF8, 0, device_ext->path, -1, &path, 0, NULL, NULL); + + if (!path) + continue; + + /* not plugable device */ + if (isAutomountLocation(path)) + { + for (i = 0; i < size; i++) + { + if (strstr(path, dev_array[i].path) != NULL) + { + dev_found = TRUE; + dev_array[i].to_add = FALSE; + break; + } + } + } + + free(path); + + if (!dev_found) + { + devman_unregister_device(rdpdr->devman, (void*)keys[j]); + ids[0] = keys[j]; + + if ((error = rdpdr_send_device_list_remove_request(rdpdr, 1, ids))) + { + WLog_ERR(TAG, + "rdpdr_send_device_list_remove_request failed with error %" PRIu32 "!", + error); + goto cleanup; + } + } + } + + /* add new devices */ + for (i = 0; i < size; i++) + { + if (!device_already_plugged(rdpdr, &dev_array[i])) + { + RDPDR_DRIVE drive = { 0 }; + char* name; + + drive.Type = RDPDR_DTYP_FILESYSTEM; + drive.Path = dev_array[i].path; + drive.automount = TRUE; + name = strrchr(drive.Path, '/') + 1; + drive.Name = name; + + if (!drive.Name) + { + WLog_ERR(TAG, "_strdup failed!"); + error = CHANNEL_RC_NO_MEMORY; + goto cleanup; + } + + if ((error = devman_load_device_service(rdpdr->devman, (const RDPDR_DEVICE*)&drive, + rdpdr->rdpcontext))) + { + WLog_ERR(TAG, "devman_load_device_service failed!"); + goto cleanup; + } + } + } + +cleanup: + free(keys); + + for (i = 0; i < size; i++) + free(dev_array[i].path); + + return error; +} + +static void first_hotplug(rdpdrPlugin* rdpdr) +{ + UINT error; + + if ((error = handle_hotplug(rdpdr))) + { + WLog_ERR(TAG, "handle_hotplug failed with error %" PRIu32 "!", error); + } +} + +static DWORD WINAPI drive_hotplug_thread_func(LPVOID arg) +{ + rdpdrPlugin* rdpdr; + int mfd; + fd_set rfds; + struct timeval tv; + int rv; + UINT error = 0; + DWORD status; + rdpdr = (rdpdrPlugin*)arg; + mfd = open("/proc/mounts", O_RDONLY, 0); + + if (mfd < 0) + { + WLog_ERR(TAG, "ERROR: Unable to open /proc/mounts."); + error = ERROR_INTERNAL_ERROR; + goto out; + } + + FD_ZERO(&rfds); + FD_SET(mfd, &rfds); + tv.tv_sec = 1; + tv.tv_usec = 0; + + while ((rv = select(mfd + 1, NULL, NULL, &rfds, &tv)) >= 0) + { + status = WaitForSingleObject(rdpdr->stopEvent, 0); + + if (status == WAIT_FAILED) + { + error = GetLastError(); + WLog_ERR(TAG, "WaitForSingleObject failed with error %" PRIu32 "!", error); + goto out; + } + + if (status == WAIT_OBJECT_0) + break; + + if (FD_ISSET(mfd, &rfds)) + { + /* file /proc/mounts changed, handle this */ + if ((error = handle_hotplug(rdpdr))) + { + WLog_ERR(TAG, "handle_hotplug failed with error %" PRIu32 "!", error); + goto out; + } + else + rdpdr_send_device_list_announce_request(rdpdr, TRUE); + } + + FD_ZERO(&rfds); + FD_SET(mfd, &rfds); + tv.tv_sec = 1; + tv.tv_usec = 0; + } + +out: + + if (error && rdpdr->rdpcontext) + setChannelError(rdpdr->rdpcontext, error, "drive_hotplug_thread_func reported an error"); + + ExitThread(error); + return error; +} + +#endif + +#if !defined(_WIN32) && !defined(__IOS__) +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT drive_hotplug_thread_terminate(rdpdrPlugin* rdpdr) +{ + UINT error; + + if (rdpdr->hotplugThread) + { + SetEvent(rdpdr->stopEvent); +#ifdef __MACOSX__ + CFRunLoopStop(rdpdr->runLoop); +#endif + + if (WaitForSingleObject(rdpdr->hotplugThread, INFINITE) == WAIT_FAILED) + { + error = GetLastError(); + WLog_ERR(TAG, "WaitForSingleObject failed with error %" PRIu32 "!", error); + return error; + } + + CloseHandle(rdpdr->hotplugThread); + CloseHandle(rdpdr->stopEvent); + rdpdr->stopEvent = NULL; + rdpdr->hotplugThread = NULL; + } + + return CHANNEL_RC_OK; +} + +#endif + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpdr_process_connect(rdpdrPlugin* rdpdr) +{ + UINT32 index; + rdpSettings* settings; + UINT error = CHANNEL_RC_OK; + rdpdr->devman = devman_new(rdpdr); + + if (!rdpdr->devman) + { + WLog_ERR(TAG, "devman_new failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + settings = (rdpSettings*)rdpdr->channelEntryPoints.pExtendedData; + + if (settings->ClientHostname) + strncpy(rdpdr->computerName, settings->ClientHostname, sizeof(rdpdr->computerName) - 1); + else + strncpy(rdpdr->computerName, settings->ComputerName, sizeof(rdpdr->computerName) - 1); + + for (index = 0; index < settings->DeviceCount; index++) + { + const RDPDR_DEVICE* device = settings->DeviceArray[index]; + + if (device->Type == RDPDR_DTYP_FILESYSTEM) + { + const char DynamicDrives[] = "DynamicDrives"; + const RDPDR_DRIVE* drive = (const RDPDR_DRIVE*)device; + BOOL hotplugAll = strncmp(drive->Path, "*", 2) == 0; + BOOL hotplugLater = strncmp(drive->Path, DynamicDrives, sizeof(DynamicDrives)) == 0; + if (drive->Path && (hotplugAll || hotplugLater)) + { + if (hotplugAll) + first_hotplug(rdpdr); +#ifndef _WIN32 + + if (!(rdpdr->stopEvent = CreateEvent(NULL, TRUE, FALSE, NULL))) + { + WLog_ERR(TAG, "CreateEvent failed!"); + return ERROR_INTERNAL_ERROR; + } + +#endif + + if (!(rdpdr->hotplugThread = + CreateThread(NULL, 0, drive_hotplug_thread_func, rdpdr, 0, NULL))) + { + WLog_ERR(TAG, "CreateThread failed!"); +#ifndef _WIN32 + CloseHandle(rdpdr->stopEvent); + rdpdr->stopEvent = NULL; +#endif + return ERROR_INTERNAL_ERROR; + } + + continue; + } + } + + if ((error = devman_load_device_service(rdpdr->devman, device, rdpdr->rdpcontext))) + { + WLog_ERR(TAG, "devman_load_device_service failed with error %" PRIu32 "!", error); + return error; + } + } + + return error; +} + +static UINT rdpdr_process_server_announce_request(rdpdrPlugin* rdpdr, wStream* s) +{ + if (Stream_GetRemainingLength(s) < 8) + return ERROR_INVALID_DATA; + + Stream_Read_UINT16(s, rdpdr->versionMajor); + Stream_Read_UINT16(s, rdpdr->versionMinor); + Stream_Read_UINT32(s, rdpdr->clientID); + rdpdr->sequenceId++; + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpdr_send_client_announce_reply(rdpdrPlugin* rdpdr) +{ + wStream* s; + s = Stream_New(NULL, 12); + + if (!s) + { + WLog_ERR(TAG, "Stream_New failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + Stream_Write_UINT16(s, RDPDR_CTYP_CORE); /* Component (2 bytes) */ + Stream_Write_UINT16(s, PAKID_CORE_CLIENTID_CONFIRM); /* PacketId (2 bytes) */ + Stream_Write_UINT16(s, rdpdr->versionMajor); + Stream_Write_UINT16(s, rdpdr->versionMinor); + Stream_Write_UINT32(s, (UINT32)rdpdr->clientID); + return rdpdr_send(rdpdr, s); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpdr_send_client_name_request(rdpdrPlugin* rdpdr) +{ + wStream* s; + WCHAR* computerNameW = NULL; + size_t computerNameLenW; + + if (!rdpdr->computerName[0]) + { + DWORD size = sizeof(rdpdr->computerName) - 1; + GetComputerNameA(rdpdr->computerName, &size); + } + + computerNameLenW = ConvertToUnicode(CP_UTF8, 0, rdpdr->computerName, -1, &computerNameW, 0) * 2; + s = Stream_New(NULL, 16 + computerNameLenW + 2); + + if (!s) + { + free(computerNameW); + WLog_ERR(TAG, "Stream_New failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + Stream_Write_UINT16(s, RDPDR_CTYP_CORE); /* Component (2 bytes) */ + Stream_Write_UINT16(s, PAKID_CORE_CLIENT_NAME); /* PacketId (2 bytes) */ + Stream_Write_UINT32(s, 1); /* unicodeFlag, 0 for ASCII and 1 for Unicode */ + Stream_Write_UINT32(s, 0); /* codePage, must be set to zero */ + Stream_Write_UINT32(s, computerNameLenW + 2); /* computerNameLen, including null terminator */ + Stream_Write(s, computerNameW, computerNameLenW); + Stream_Write_UINT16(s, 0); /* null terminator */ + free(computerNameW); + return rdpdr_send(rdpdr, s); +} + +static UINT rdpdr_process_server_clientid_confirm(rdpdrPlugin* rdpdr, wStream* s) +{ + UINT16 versionMajor; + UINT16 versionMinor; + UINT32 clientID; + + if (Stream_GetRemainingLength(s) < 8) + return ERROR_INVALID_DATA; + + Stream_Read_UINT16(s, versionMajor); + Stream_Read_UINT16(s, versionMinor); + Stream_Read_UINT32(s, clientID); + + if (versionMajor != rdpdr->versionMajor || versionMinor != rdpdr->versionMinor) + { + rdpdr->versionMajor = versionMajor; + rdpdr->versionMinor = versionMinor; + } + + if (clientID != rdpdr->clientID) + rdpdr->clientID = clientID; + + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpdr_send_device_list_announce_request(rdpdrPlugin* rdpdr, BOOL userLoggedOn) +{ + int i; + BYTE c; + size_t pos; + int index; + wStream* s; + UINT32 count; + size_t data_len; + size_t count_pos; + DEVICE* device; + int keyCount; + ULONG_PTR* pKeys = NULL; + s = Stream_New(NULL, 256); + + if (!s) + { + WLog_ERR(TAG, "Stream_New failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + Stream_Write_UINT16(s, RDPDR_CTYP_CORE); /* Component (2 bytes) */ + Stream_Write_UINT16(s, PAKID_CORE_DEVICELIST_ANNOUNCE); /* PacketId (2 bytes) */ + count_pos = Stream_GetPosition(s); + count = 0; + Stream_Seek_UINT32(s); /* deviceCount */ + pKeys = NULL; + keyCount = ListDictionary_GetKeys(rdpdr->devman->devices, &pKeys); + + for (index = 0; index < keyCount; index++) + { + device = (DEVICE*)ListDictionary_GetItemValue(rdpdr->devman->devices, (void*)pKeys[index]); + + /** + * 1. versionMinor 0x0005 doesn't send PAKID_CORE_USER_LOGGEDON + * so all devices should be sent regardless of user_loggedon + * 2. smartcard devices should be always sent + * 3. other devices are sent only after user_loggedon + */ + + if ((rdpdr->versionMinor == 0x0005) || (device->type == RDPDR_DTYP_SMARTCARD) || + userLoggedOn) + { + data_len = (device->data == NULL ? 0 : Stream_GetPosition(device->data)); + + if (!Stream_EnsureRemainingCapacity(s, 20 + data_len)) + { + free(pKeys); + Stream_Free(s, TRUE); + WLog_ERR(TAG, "Stream_EnsureRemainingCapacity failed!"); + return ERROR_INVALID_DATA; + } + + Stream_Write_UINT32(s, device->type); /* deviceType */ + Stream_Write_UINT32(s, device->id); /* deviceID */ + strncpy((char*)Stream_Pointer(s), device->name, 8); + + for (i = 0; i < 8; i++) + { + Stream_Peek_UINT8(s, c); + + if (c > 0x7F) + Stream_Write_UINT8(s, '_'); + else + Stream_Seek_UINT8(s); + } + + Stream_Write_UINT32(s, data_len); + + if (data_len > 0) + Stream_Write(s, Stream_Buffer(device->data), data_len); + + count++; + WLog_INFO(TAG, "registered device #%" PRIu32 ": %s (type=%" PRIu32 " id=%" PRIu32 ")", + count, device->name, device->type, device->id); + } + } + + free(pKeys); + pos = Stream_GetPosition(s); + Stream_SetPosition(s, count_pos); + Stream_Write_UINT32(s, count); + Stream_SetPosition(s, pos); + Stream_SealLength(s); + return rdpdr_send(rdpdr, s); +} + +static UINT dummy_irp_response(rdpdrPlugin* rdpdr, wStream* s) +{ + + UINT32 DeviceId; + UINT32 FileId; + UINT32 CompletionId; + + wStream* output = Stream_New(NULL, 256); // RDPDR_DEVICE_IO_RESPONSE_LENGTH + if (!output) + { + WLog_ERR(TAG, "Stream_New failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + Stream_SetPosition(s, 4); /* see "rdpdr_process_receive" */ + + Stream_Read_UINT32(s, DeviceId); /* DeviceId (4 bytes) */ + Stream_Read_UINT32(s, FileId); /* FileId (4 bytes) */ + Stream_Read_UINT32(s, CompletionId); /* CompletionId (4 bytes) */ + + Stream_Write_UINT16(output, RDPDR_CTYP_CORE); /* Component (2 bytes) */ + Stream_Write_UINT16(output, PAKID_CORE_DEVICE_IOCOMPLETION); /* PacketId (2 bytes) */ + Stream_Write_UINT32(output, DeviceId); /* DeviceId (4 bytes) */ + Stream_Write_UINT32(output, CompletionId); /* CompletionId (4 bytes) */ + Stream_Write_UINT32(output, STATUS_UNSUCCESSFUL); /* IoStatus (4 bytes) */ + + Stream_Zero(output, 256 - RDPDR_DEVICE_IO_RESPONSE_LENGTH); + // or usage + // Stream_Write_UINT32(output, 0); /* Length */ + // Stream_Write_UINT8(output, 0); /* Padding */ + + return rdpdr_send(rdpdr, output); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpdr_process_irp(rdpdrPlugin* rdpdr, wStream* s) +{ + IRP* irp; + UINT error = CHANNEL_RC_OK; + irp = irp_new(rdpdr->devman, s, &error); + + if (!irp) + { + WLog_ERR(TAG, "irp_new failed with %" PRIu32 "!", error); + + if (error == CHANNEL_RC_OK) + { + return dummy_irp_response(rdpdr, s); + } + + return error; + } + + IFCALLRET(irp->device->IRPRequest, error, irp->device, irp); + + if (error) + WLog_ERR(TAG, "device->IRPRequest failed with error %" PRIu32 "", error); + + return error; +} + +static UINT rdpdr_process_component(rdpdrPlugin* rdpdr, UINT16 component, UINT16 packetId, + wStream* s) +{ + UINT32 type; + DEVICE* device; + + switch (component) + { + case RDPDR_CTYP_PRN: + type = RDPDR_DTYP_PRINT; + break; + + default: + return ERROR_INVALID_DATA; + } + + device = devman_get_device_by_type(rdpdr->devman, type); + + if (!device) + return ERROR_INVALID_PARAMETER; + + return IFCALLRESULT(ERROR_INVALID_PARAMETER, device->CustomComponentRequest, device, component, + packetId, s); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpdr_process_init(rdpdrPlugin* rdpdr) +{ + int index; + int keyCount; + DEVICE* device; + ULONG_PTR* pKeys = NULL; + UINT error = CHANNEL_RC_OK; + pKeys = NULL; + keyCount = ListDictionary_GetKeys(rdpdr->devman->devices, &pKeys); + + for (index = 0; index < keyCount; index++) + { + device = (DEVICE*)ListDictionary_GetItemValue(rdpdr->devman->devices, (void*)pKeys[index]); + IFCALLRET(device->Init, error, device); + + if (error != CHANNEL_RC_OK) + { + WLog_ERR(TAG, "Init failed!"); + free(pKeys); + return error; + } + } + + free(pKeys); + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpdr_process_receive(rdpdrPlugin* rdpdr, wStream* s) +{ + UINT16 component; + UINT16 packetId; + UINT32 deviceId; + UINT32 status; + UINT error = ERROR_INVALID_DATA; + + if (!rdpdr || !s) + return CHANNEL_RC_NULL_DATA; + + if (Stream_GetRemainingLength(s) >= 4) + { + Stream_Read_UINT16(s, component); /* Component (2 bytes) */ + Stream_Read_UINT16(s, packetId); /* PacketId (2 bytes) */ + + if (component == RDPDR_CTYP_CORE) + { + switch (packetId) + { + case PAKID_CORE_SERVER_ANNOUNCE: + if ((error = rdpdr_process_server_announce_request(rdpdr, s))) + { + } + else if ((error = rdpdr_send_client_announce_reply(rdpdr))) + { + WLog_ERR(TAG, + "rdpdr_send_client_announce_reply failed with error %" PRIu32 "", + error); + } + else if ((error = rdpdr_send_client_name_request(rdpdr))) + { + WLog_ERR(TAG, + "rdpdr_send_client_name_request failed with error %" PRIu32 "", + error); + } + else if ((error = rdpdr_process_init(rdpdr))) + { + WLog_ERR(TAG, "rdpdr_process_init failed with error %" PRIu32 "", error); + } + + break; + + case PAKID_CORE_SERVER_CAPABILITY: + if ((error = rdpdr_process_capability_request(rdpdr, s))) + { + } + else if ((error = rdpdr_send_capability_response(rdpdr))) + { + WLog_ERR(TAG, + "rdpdr_send_capability_response failed with error %" PRIu32 "", + error); + } + + break; + + case PAKID_CORE_CLIENTID_CONFIRM: + if ((error = rdpdr_process_server_clientid_confirm(rdpdr, s))) + { + } + else if ((error = rdpdr_send_device_list_announce_request(rdpdr, FALSE))) + { + WLog_ERR( + TAG, + "rdpdr_send_device_list_announce_request failed with error %" PRIu32 "", + error); + } + + break; + + case PAKID_CORE_USER_LOGGEDON: + if ((error = rdpdr_send_device_list_announce_request(rdpdr, TRUE))) + { + WLog_ERR( + TAG, + "rdpdr_send_device_list_announce_request failed with error %" PRIu32 "", + error); + } + + break; + + case PAKID_CORE_DEVICE_REPLY: + + /* connect to a specific resource */ + if (Stream_GetRemainingLength(s) >= 8) + { + Stream_Read_UINT32(s, deviceId); + Stream_Read_UINT32(s, status); + error = CHANNEL_RC_OK; + } + + break; + + case PAKID_CORE_DEVICE_IOREQUEST: + if ((error = rdpdr_process_irp(rdpdr, s))) + { + WLog_ERR(TAG, "rdpdr_process_irp failed with error %" PRIu32 "", error); + return error; + } + else + s = NULL; + + break; + + default: + WLog_ERR(TAG, "RDPDR_CTYP_CORE unknown PacketId: 0x%04" PRIX16 "", packetId); + error = ERROR_INVALID_DATA; + break; + } + } + else + { + error = rdpdr_process_component(rdpdr, component, packetId, s); + + if (error != CHANNEL_RC_OK) + { + WLog_ERR(TAG, + "Unknown message: Component: 0x%04" PRIX16 " PacketId: 0x%04" PRIX16 "", + component, packetId); + } + } + } + + Stream_Free(s, TRUE); + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT rdpdr_send(rdpdrPlugin* rdpdr, wStream* s) +{ + UINT status; + rdpdrPlugin* plugin = (rdpdrPlugin*)rdpdr; + + if (!rdpdr || !s) + { + Stream_Free(s, TRUE); + return CHANNEL_RC_NULL_DATA; + } + + if (!plugin) + { + Stream_Free(s, TRUE); + status = CHANNEL_RC_BAD_INIT_HANDLE; + } + else + { + status = plugin->channelEntryPoints.pVirtualChannelWriteEx( + plugin->InitHandle, plugin->OpenHandle, Stream_Buffer(s), (UINT32)Stream_GetPosition(s), + s); + } + + if (status != CHANNEL_RC_OK) + { + Stream_Free(s, TRUE); + WLog_ERR(TAG, "pVirtualChannelWriteEx failed with %s [%08" PRIX32 "]", + WTSErrorToString(status), status); + } + + return status; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpdr_virtual_channel_event_data_received(rdpdrPlugin* rdpdr, void* pData, + UINT32 dataLength, UINT32 totalLength, + UINT32 dataFlags) +{ + wStream* data_in; + + if ((dataFlags & CHANNEL_FLAG_SUSPEND) || (dataFlags & CHANNEL_FLAG_RESUME)) + { + /* + * According to MS-RDPBCGR 2.2.6.1, "All virtual channel traffic MUST be suspended. + * This flag is only valid in server-to-client virtual channel traffic. It MUST be + * ignored in client-to-server data." Thus it would be best practice to cease data + * transmission. However, simply returning here avoids a crash. + */ + return CHANNEL_RC_OK; + } + + if (dataFlags & CHANNEL_FLAG_FIRST) + { + if (rdpdr->data_in != NULL) + Stream_Free(rdpdr->data_in, TRUE); + + rdpdr->data_in = Stream_New(NULL, totalLength); + + if (!rdpdr->data_in) + { + WLog_ERR(TAG, "Stream_New failed!"); + return CHANNEL_RC_NO_MEMORY; + } + } + + data_in = rdpdr->data_in; + + if (!Stream_EnsureRemainingCapacity(data_in, dataLength)) + { + WLog_ERR(TAG, "Stream_EnsureRemainingCapacity failed!"); + return ERROR_INVALID_DATA; + } + + Stream_Write(data_in, pData, dataLength); + + if (dataFlags & CHANNEL_FLAG_LAST) + { + if (Stream_Capacity(data_in) != Stream_GetPosition(data_in)) + { + WLog_ERR(TAG, "rdpdr_virtual_channel_event_data_received: read error"); + return ERROR_INTERNAL_ERROR; + } + + rdpdr->data_in = NULL; + Stream_SealLength(data_in); + Stream_SetPosition(data_in, 0); + + if (!MessageQueue_Post(rdpdr->queue, NULL, 0, (void*)data_in, NULL)) + { + WLog_ERR(TAG, "MessageQueue_Post failed!"); + return ERROR_INTERNAL_ERROR; + } + } + + return CHANNEL_RC_OK; +} + +static VOID VCAPITYPE rdpdr_virtual_channel_open_event_ex(LPVOID lpUserParam, DWORD openHandle, + UINT event, LPVOID pData, + UINT32 dataLength, UINT32 totalLength, + UINT32 dataFlags) +{ + UINT error = CHANNEL_RC_OK; + rdpdrPlugin* rdpdr = (rdpdrPlugin*)lpUserParam; + + switch (event) + { + case CHANNEL_EVENT_DATA_RECEIVED: + if (!rdpdr || !pData || (rdpdr->OpenHandle != openHandle)) + { + WLog_ERR(TAG, "error no match"); + return; + } + if ((error = rdpdr_virtual_channel_event_data_received(rdpdr, pData, dataLength, + totalLength, dataFlags))) + WLog_ERR(TAG, + "rdpdr_virtual_channel_event_data_received failed with error %" PRIu32 "!", + error); + + break; + + case CHANNEL_EVENT_WRITE_CANCELLED: + case CHANNEL_EVENT_WRITE_COMPLETE: + { + wStream* s = (wStream*)pData; + Stream_Free(s, TRUE); + } + break; + + case CHANNEL_EVENT_USER: + break; + } + + if (error && rdpdr && rdpdr->rdpcontext) + setChannelError(rdpdr->rdpcontext, error, + "rdpdr_virtual_channel_open_event_ex reported an error"); + + return; +} + +static DWORD WINAPI rdpdr_virtual_channel_client_thread(LPVOID arg) +{ + wStream* data; + wMessage message; + rdpdrPlugin* rdpdr = (rdpdrPlugin*)arg; + UINT error; + + if (!rdpdr) + { + ExitThread((DWORD)CHANNEL_RC_NULL_DATA); + return CHANNEL_RC_NULL_DATA; + } + + if ((error = rdpdr_process_connect(rdpdr))) + { + WLog_ERR(TAG, "rdpdr_process_connect failed with error %" PRIu32 "!", error); + + if (rdpdr->rdpcontext) + setChannelError(rdpdr->rdpcontext, error, + "rdpdr_virtual_channel_client_thread reported an error"); + + ExitThread(error); + return error; + } + + while (1) + { + if (!MessageQueue_Wait(rdpdr->queue)) + break; + + if (MessageQueue_Peek(rdpdr->queue, &message, TRUE)) + { + if (message.id == WMQ_QUIT) + break; + + if (message.id == 0) + { + data = (wStream*)message.wParam; + + if ((error = rdpdr_process_receive(rdpdr, data))) + { + WLog_ERR(TAG, "rdpdr_process_receive failed with error %" PRIu32 "!", error); + + if (rdpdr->rdpcontext) + setChannelError(rdpdr->rdpcontext, error, + "rdpdr_virtual_channel_client_thread reported an error"); + + ExitThread((DWORD)error); + return error; + } + } + } + } + + ExitThread(0); + return 0; +} + +static void queue_free(void* obj) +{ + wStream* s = obj; + Stream_Free(s, TRUE); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpdr_virtual_channel_event_connected(rdpdrPlugin* rdpdr, LPVOID pData, + UINT32 dataLength) +{ + UINT32 status; + status = rdpdr->channelEntryPoints.pVirtualChannelOpenEx(rdpdr->InitHandle, &rdpdr->OpenHandle, + rdpdr->channelDef.name, + rdpdr_virtual_channel_open_event_ex); + + if (status != CHANNEL_RC_OK) + { + WLog_ERR(TAG, "pVirtualChannelOpenEx failed with %s [%08" PRIX32 "]", + WTSErrorToString(status), status); + return status; + } + + rdpdr->queue = MessageQueue_New(NULL); + + if (!rdpdr->queue) + { + WLog_ERR(TAG, "MessageQueue_New failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + rdpdr->queue->object.fnObjectFree = queue_free; + + if (!(rdpdr->thread = + CreateThread(NULL, 0, rdpdr_virtual_channel_client_thread, (void*)rdpdr, 0, NULL))) + { + WLog_ERR(TAG, "CreateThread failed!"); + return ERROR_INTERNAL_ERROR; + } + + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpdr_virtual_channel_event_disconnected(rdpdrPlugin* rdpdr) +{ + UINT error; + + if (rdpdr->OpenHandle == 0) + return CHANNEL_RC_OK; + + if (MessageQueue_PostQuit(rdpdr->queue, 0) && + (WaitForSingleObject(rdpdr->thread, INFINITE) == WAIT_FAILED)) + { + error = GetLastError(); + WLog_ERR(TAG, "WaitForSingleObject failed with error %" PRIu32 "!", error); + return error; + } + + MessageQueue_Free(rdpdr->queue); + CloseHandle(rdpdr->thread); + rdpdr->queue = NULL; + rdpdr->thread = NULL; + + if ((error = drive_hotplug_thread_terminate(rdpdr))) + { + WLog_ERR(TAG, "drive_hotplug_thread_terminate failed with error %" PRIu32 "!", error); + return error; + } + + error = rdpdr->channelEntryPoints.pVirtualChannelCloseEx(rdpdr->InitHandle, rdpdr->OpenHandle); + + if (CHANNEL_RC_OK != error) + { + WLog_ERR(TAG, "pVirtualChannelCloseEx failed with %s [%08" PRIX32 "]", + WTSErrorToString(error), error); + } + + rdpdr->OpenHandle = 0; + + if (rdpdr->data_in) + { + Stream_Free(rdpdr->data_in, TRUE); + rdpdr->data_in = NULL; + } + + if (rdpdr->devman) + { + devman_free(rdpdr->devman); + rdpdr->devman = NULL; + } + + return error; +} + +static void rdpdr_virtual_channel_event_terminated(rdpdrPlugin* rdpdr) +{ + rdpdr->InitHandle = 0; + free(rdpdr); +} + +static VOID VCAPITYPE rdpdr_virtual_channel_init_event_ex(LPVOID lpUserParam, LPVOID pInitHandle, + UINT event, LPVOID pData, UINT dataLength) +{ + UINT error = CHANNEL_RC_OK; + rdpdrPlugin* rdpdr = (rdpdrPlugin*)lpUserParam; + + if (!rdpdr || (rdpdr->InitHandle != pInitHandle)) + { + WLog_ERR(TAG, "error no match"); + return; + } + + switch (event) + { + case CHANNEL_EVENT_INITIALIZED: + break; + + case CHANNEL_EVENT_CONNECTED: + if ((error = rdpdr_virtual_channel_event_connected(rdpdr, pData, dataLength))) + WLog_ERR(TAG, + "rdpdr_virtual_channel_event_connected failed with error %" PRIu32 "!", + error); + + break; + + case CHANNEL_EVENT_DISCONNECTED: + if ((error = rdpdr_virtual_channel_event_disconnected(rdpdr))) + WLog_ERR(TAG, + "rdpdr_virtual_channel_event_disconnected failed with error %" PRIu32 "!", + error); + + break; + + case CHANNEL_EVENT_TERMINATED: + rdpdr_virtual_channel_event_terminated(rdpdr); + break; + + case CHANNEL_EVENT_ATTACHED: + case CHANNEL_EVENT_DETACHED: + default: + WLog_ERR(TAG, "unknown event %" PRIu32 "!", event); + break; + } + + if (error && rdpdr->rdpcontext) + setChannelError(rdpdr->rdpcontext, error, + "rdpdr_virtual_channel_init_event_ex reported an error"); +} + +/* rdpdr is always built-in */ +#define VirtualChannelEntryEx rdpdr_VirtualChannelEntryEx + +BOOL VCAPITYPE VirtualChannelEntryEx(PCHANNEL_ENTRY_POINTS pEntryPoints, PVOID pInitHandle) +{ + UINT rc; + rdpdrPlugin* rdpdr; + CHANNEL_ENTRY_POINTS_FREERDP_EX* pEntryPointsEx; + rdpdr = (rdpdrPlugin*)calloc(1, sizeof(rdpdrPlugin)); + + if (!rdpdr) + { + WLog_ERR(TAG, "calloc failed!"); + return FALSE; + } + + rdpdr->channelDef.options = + CHANNEL_OPTION_INITIALIZED | CHANNEL_OPTION_ENCRYPT_RDP | CHANNEL_OPTION_COMPRESS_RDP; + sprintf_s(rdpdr->channelDef.name, ARRAYSIZE(rdpdr->channelDef.name), "rdpdr"); + rdpdr->sequenceId = 0; + pEntryPointsEx = (CHANNEL_ENTRY_POINTS_FREERDP_EX*)pEntryPoints; + + if ((pEntryPointsEx->cbSize >= sizeof(CHANNEL_ENTRY_POINTS_FREERDP_EX)) && + (pEntryPointsEx->MagicNumber == FREERDP_CHANNEL_MAGIC_NUMBER)) + { + rdpdr->rdpcontext = pEntryPointsEx->context; + } + + CopyMemory(&(rdpdr->channelEntryPoints), pEntryPoints, sizeof(CHANNEL_ENTRY_POINTS_FREERDP_EX)); + rdpdr->InitHandle = pInitHandle; + rc = rdpdr->channelEntryPoints.pVirtualChannelInitEx( + rdpdr, NULL, pInitHandle, &rdpdr->channelDef, 1, VIRTUAL_CHANNEL_VERSION_WIN2000, + rdpdr_virtual_channel_init_event_ex); + + if (CHANNEL_RC_OK != rc) + { + WLog_ERR(TAG, "pVirtualChannelInitEx failed with %s [%08" PRIX32 "]", WTSErrorToString(rc), + rc); + free(rdpdr); + return FALSE; + } + + return TRUE; +} diff --git a/channels/rdpdr/client/rdpdr_main.h b/channels/rdpdr/client/rdpdr_main.h new file mode 100644 index 0000000..4c372da --- /dev/null +++ b/channels/rdpdr/client/rdpdr_main.h @@ -0,0 +1,85 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Device Redirection Virtual Channel + * + * Copyright 2010-2011 Vic Lee + * Copyright 2010-2012 Marc-Andre Moreau + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger + * Copyright 2016 Inuvika Inc. + * Copyright 2016 David PHAM-VAN + * + * Licensed 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 FREERDP_CHANNEL_RDPDR_CLIENT_MAIN_H +#define FREERDP_CHANNEL_RDPDR_CLIENT_MAIN_H + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include + +#ifdef __MACOSX__ +#include +#endif + +#define TAG CHANNELS_TAG("rdpdr.client") + +typedef struct rdpdr_plugin rdpdrPlugin; + +struct rdpdr_plugin +{ + CHANNEL_DEF channelDef; + CHANNEL_ENTRY_POINTS_FREERDP_EX channelEntryPoints; + + HANDLE thread; + wStream* data_in; + void* InitHandle; + DWORD OpenHandle; + wMessageQueue* queue; + + DEVMAN* devman; + + UINT16 versionMajor; + UINT16 versionMinor; + UINT16 clientID; + char computerName[256]; + + UINT32 sequenceId; + + /* hotplug support */ + HANDLE hotplugThread; +#ifdef _WIN32 + HWND hotplug_wnd; +#endif +#ifdef __MACOSX__ + CFRunLoopRef runLoop; +#endif +#ifndef _WIN32 + HANDLE stopEvent; +#endif + rdpContext* rdpcontext; +}; + +UINT rdpdr_send(rdpdrPlugin* rdpdr, wStream* s); + +#endif /* FREERDP_CHANNEL_RDPDR_CLIENT_MAIN_H */ diff --git a/channels/rdpdr/server/CMakeLists.txt b/channels/rdpdr/server/CMakeLists.txt new file mode 100644 index 0000000..63f8a04 --- /dev/null +++ b/channels/rdpdr/server/CMakeLists.txt @@ -0,0 +1,31 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2012 Marc-Andre Moreau +# +# Licensed 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. + +define_channel_server("rdpdr") + +set(${MODULE_PREFIX}_SRCS + rdpdr_main.c + rdpdr_main.h) + +add_channel_server_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} FALSE "VirtualChannelEntry") + + + +target_link_libraries(${MODULE_NAME} freerdp) + + +set_property(TARGET ${MODULE_NAME} PROPERTY FOLDER "Channels/${CHANNEL_NAME}/Server") diff --git a/channels/rdpdr/server/rdpdr_main.c b/channels/rdpdr/server/rdpdr_main.c new file mode 100644 index 0000000..2dcb2a0 --- /dev/null +++ b/channels/rdpdr/server/rdpdr_main.c @@ -0,0 +1,2626 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Device Redirection Virtual Channel Extension + * + * Copyright 2014 Dell Software + * Copyright 2013 Marc-Andre Moreau + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include +#include + +#include +#include "rdpdr_main.h" + +#define TAG "rdpdr.server" + +static UINT32 g_ClientId = 0; + +static RDPDR_IRP* rdpdr_server_irp_new() +{ + RDPDR_IRP* irp; + irp = (RDPDR_IRP*)calloc(1, sizeof(RDPDR_IRP)); + return irp; +} + +static void rdpdr_server_irp_free(RDPDR_IRP* irp) +{ + free(irp); +} + +static BOOL rdpdr_server_enqueue_irp(RdpdrServerContext* context, RDPDR_IRP* irp) +{ + return ListDictionary_Add(context->priv->IrpList, (void*)(size_t)irp->CompletionId, irp); +} + +static RDPDR_IRP* rdpdr_server_dequeue_irp(RdpdrServerContext* context, UINT32 completionId) +{ + RDPDR_IRP* irp; + irp = (RDPDR_IRP*)ListDictionary_Remove(context->priv->IrpList, (void*)(size_t)completionId); + return irp; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpdr_server_send_announce_request(RdpdrServerContext* context) +{ + wStream* s; + BOOL status; + RDPDR_HEADER header; + ULONG written; + WLog_DBG(TAG, "RdpdrServerSendAnnounceRequest"); + header.Component = RDPDR_CTYP_CORE; + header.PacketId = PAKID_CORE_SERVER_ANNOUNCE; + s = Stream_New(NULL, RDPDR_HEADER_LENGTH + 8); + + if (!s) + { + WLog_ERR(TAG, "Stream_New failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + Stream_Write_UINT16(s, header.Component); /* Component (2 bytes) */ + Stream_Write_UINT16(s, header.PacketId); /* PacketId (2 bytes) */ + Stream_Write_UINT16(s, context->priv->VersionMajor); /* VersionMajor (2 bytes) */ + Stream_Write_UINT16(s, context->priv->VersionMinor); /* VersionMinor (2 bytes) */ + Stream_Write_UINT32(s, context->priv->ClientId); /* ClientId (4 bytes) */ + Stream_SealLength(s); + winpr_HexDump(TAG, WLOG_DEBUG, Stream_Buffer(s), Stream_Length(s)); + status = WTSVirtualChannelWrite(context->priv->ChannelHandle, (PCHAR)Stream_Buffer(s), + Stream_Length(s), &written); + Stream_Free(s, TRUE); + return status ? CHANNEL_RC_OK : ERROR_INTERNAL_ERROR; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpdr_server_receive_announce_response(RdpdrServerContext* context, wStream* s, + RDPDR_HEADER* header) +{ + UINT32 ClientId; + UINT16 VersionMajor; + UINT16 VersionMinor; + + if (Stream_GetRemainingLength(s) < 8) + { + WLog_ERR(TAG, "not enough data in stream!"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT16(s, VersionMajor); /* VersionMajor (2 bytes) */ + Stream_Read_UINT16(s, VersionMinor); /* VersionMinor (2 bytes) */ + Stream_Read_UINT32(s, ClientId); /* ClientId (4 bytes) */ + WLog_DBG(TAG, + "Client Announce Response: VersionMajor: 0x%08" PRIX16 " VersionMinor: 0x%04" PRIX16 + " ClientId: 0x%08" PRIX32 "", + VersionMajor, VersionMinor, ClientId); + context->priv->ClientId = ClientId; + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpdr_server_receive_client_name_request(RdpdrServerContext* context, wStream* s, + RDPDR_HEADER* header) +{ + UINT32 UnicodeFlag; + UINT32 ComputerNameLen; + + if (Stream_GetRemainingLength(s) < 12) + { + WLog_ERR(TAG, "not enough data in stream!"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT32(s, UnicodeFlag); /* UnicodeFlag (4 bytes) */ + Stream_Seek_UINT32(s); /* CodePage (4 bytes), MUST be set to zero */ + Stream_Read_UINT32(s, ComputerNameLen); /* ComputerNameLen (4 bytes) */ + /* UnicodeFlag is either 0 or 1, the other 31 bits must be ignored. + */ + UnicodeFlag = UnicodeFlag & 0x00000001; + + /** + * Caution: ComputerNameLen is given *bytes*, + * not in characters, including the NULL terminator! + */ + + if (UnicodeFlag) + { + if ((ComputerNameLen % 2) || ComputerNameLen > 512 || ComputerNameLen < 2) + { + WLog_ERR(TAG, "invalid unicode computer name length: %" PRIu32 "", ComputerNameLen); + return ERROR_INVALID_DATA; + } + } + else + { + if (ComputerNameLen > 256 || ComputerNameLen < 1) + { + WLog_ERR(TAG, "invalid ascii computer name length: %" PRIu32 "", ComputerNameLen); + return ERROR_INVALID_DATA; + } + } + + if (Stream_GetRemainingLength(s) < ComputerNameLen) + { + WLog_ERR(TAG, "not enough data in stream!"); + return ERROR_INVALID_DATA; + } + + /* ComputerName must be null terminated, check if it really is */ + + if (Stream_Pointer(s)[ComputerNameLen - 1] || + (UnicodeFlag && Stream_Pointer(s)[ComputerNameLen - 2])) + { + WLog_ERR(TAG, "computer name must be null terminated"); + return ERROR_INVALID_DATA; + } + + if (context->priv->ClientComputerName) + { + free(context->priv->ClientComputerName); + context->priv->ClientComputerName = NULL; + } + + if (UnicodeFlag) + { + if (ConvertFromUnicode(CP_UTF8, 0, (WCHAR*)Stream_Pointer(s), -1, + &(context->priv->ClientComputerName), 0, NULL, NULL) < 1) + { + WLog_ERR(TAG, "failed to convert client computer name"); + return ERROR_INVALID_DATA; + } + } + else + { + context->priv->ClientComputerName = _strdup((char*)Stream_Pointer(s)); + + if (!context->priv->ClientComputerName) + { + WLog_ERR(TAG, "failed to duplicate client computer name"); + return CHANNEL_RC_NO_MEMORY; + } + } + + Stream_Seek(s, ComputerNameLen); + WLog_DBG(TAG, "ClientComputerName: %s", context->priv->ClientComputerName); + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpdr_server_read_capability_set_header(wStream* s, RDPDR_CAPABILITY_HEADER* header) +{ + if (Stream_GetRemainingLength(s) < 8) + { + WLog_ERR(TAG, "not enough data in stream!"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT16(s, header->CapabilityType); /* CapabilityType (2 bytes) */ + Stream_Read_UINT16(s, header->CapabilityLength); /* CapabilityLength (2 bytes) */ + Stream_Read_UINT32(s, header->Version); /* Version (4 bytes) */ + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpdr_server_write_capability_set_header(wStream* s, RDPDR_CAPABILITY_HEADER* header) +{ + if (!Stream_EnsureRemainingCapacity(s, 8)) + { + WLog_ERR(TAG, "not enough data in stream!"); + return ERROR_INVALID_DATA; + } + + Stream_Write_UINT16(s, header->CapabilityType); /* CapabilityType (2 bytes) */ + Stream_Write_UINT16(s, header->CapabilityLength); /* CapabilityLength (2 bytes) */ + Stream_Write_UINT32(s, header->Version); /* Version (4 bytes) */ + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpdr_server_read_general_capability_set(RdpdrServerContext* context, wStream* s, + RDPDR_CAPABILITY_HEADER* header) +{ + UINT32 ioCode1; + UINT32 extraFlags1; + UINT32 extendedPdu; + UINT16 VersionMajor; + UINT16 VersionMinor; + UINT32 SpecialTypeDeviceCap; + + if (Stream_GetRemainingLength(s) < 32) + { + WLog_ERR(TAG, "not enough data in stream!"); + return ERROR_INVALID_DATA; + } + + Stream_Seek_UINT32(s); /* osType (4 bytes), ignored on receipt */ + Stream_Seek_UINT32(s); /* osVersion (4 bytes), unused and must be set to zero */ + Stream_Read_UINT16(s, VersionMajor); /* protocolMajorVersion (2 bytes) */ + Stream_Read_UINT16(s, VersionMinor); /* protocolMinorVersion (2 bytes) */ + Stream_Read_UINT32(s, ioCode1); /* ioCode1 (4 bytes) */ + Stream_Seek_UINT32(s); /* ioCode2 (4 bytes), must be set to zero, reserved for future use */ + Stream_Read_UINT32(s, extendedPdu); /* extendedPdu (4 bytes) */ + Stream_Read_UINT32(s, extraFlags1); /* extraFlags1 (4 bytes) */ + Stream_Seek_UINT32(s); /* extraFlags2 (4 bytes), must be set to zero, reserved for future use */ + + if (header->Version == GENERAL_CAPABILITY_VERSION_02) + { + if (Stream_GetRemainingLength(s) < 4) + { + WLog_ERR(TAG, "not enough data in stream!"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT32(s, SpecialTypeDeviceCap); /* SpecialTypeDeviceCap (4 bytes) */ + } + + context->priv->UserLoggedOnPdu = (extendedPdu & RDPDR_USER_LOGGEDON_PDU) ? TRUE : FALSE; + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpdr_server_write_general_capability_set(RdpdrServerContext* context, wStream* s) +{ + UINT32 ioCode1; + UINT32 extendedPdu; + UINT32 extraFlags1; + UINT32 SpecialTypeDeviceCap; + RDPDR_CAPABILITY_HEADER header; + header.CapabilityType = CAP_GENERAL_TYPE; + header.CapabilityLength = RDPDR_CAPABILITY_HEADER_LENGTH + 36; + header.Version = GENERAL_CAPABILITY_VERSION_02; + ioCode1 = 0; + ioCode1 |= RDPDR_IRP_MJ_CREATE; /* always set */ + ioCode1 |= RDPDR_IRP_MJ_CLEANUP; /* always set */ + ioCode1 |= RDPDR_IRP_MJ_CLOSE; /* always set */ + ioCode1 |= RDPDR_IRP_MJ_READ; /* always set */ + ioCode1 |= RDPDR_IRP_MJ_WRITE; /* always set */ + ioCode1 |= RDPDR_IRP_MJ_FLUSH_BUFFERS; /* always set */ + ioCode1 |= RDPDR_IRP_MJ_SHUTDOWN; /* always set */ + ioCode1 |= RDPDR_IRP_MJ_DEVICE_CONTROL; /* always set */ + ioCode1 |= RDPDR_IRP_MJ_QUERY_VOLUME_INFORMATION; /* always set */ + ioCode1 |= RDPDR_IRP_MJ_SET_VOLUME_INFORMATION; /* always set */ + ioCode1 |= RDPDR_IRP_MJ_QUERY_INFORMATION; /* always set */ + ioCode1 |= RDPDR_IRP_MJ_SET_INFORMATION; /* always set */ + ioCode1 |= RDPDR_IRP_MJ_DIRECTORY_CONTROL; /* always set */ + ioCode1 |= RDPDR_IRP_MJ_LOCK_CONTROL; /* always set */ + ioCode1 |= RDPDR_IRP_MJ_QUERY_SECURITY; /* optional */ + ioCode1 |= RDPDR_IRP_MJ_SET_SECURITY; /* optional */ + extendedPdu = 0; + extendedPdu |= RDPDR_CLIENT_DISPLAY_NAME_PDU; /* always set */ + extendedPdu |= RDPDR_DEVICE_REMOVE_PDUS; /* optional */ + + if (context->priv->UserLoggedOnPdu) + extendedPdu |= RDPDR_USER_LOGGEDON_PDU; /* optional */ + + extraFlags1 = 0; + extraFlags1 |= ENABLE_ASYNCIO; /* optional */ + SpecialTypeDeviceCap = 0; + + if (!Stream_EnsureRemainingCapacity(s, header.CapabilityLength)) + { + WLog_ERR(TAG, "Stream_EnsureRemainingCapacity failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + rdpdr_server_write_capability_set_header(s, &header); + Stream_Write_UINT32(s, 0); /* osType (4 bytes), ignored on receipt */ + Stream_Write_UINT32(s, 0); /* osVersion (4 bytes), unused and must be set to zero */ + Stream_Write_UINT16(s, context->priv->VersionMajor); /* protocolMajorVersion (2 bytes) */ + Stream_Write_UINT16(s, context->priv->VersionMinor); /* protocolMinorVersion (2 bytes) */ + Stream_Write_UINT32(s, ioCode1); /* ioCode1 (4 bytes) */ + Stream_Write_UINT32(s, 0); /* ioCode2 (4 bytes), must be set to zero, reserved for future use */ + Stream_Write_UINT32(s, extendedPdu); /* extendedPdu (4 bytes) */ + Stream_Write_UINT32(s, extraFlags1); /* extraFlags1 (4 bytes) */ + Stream_Write_UINT32( + s, 0); /* extraFlags2 (4 bytes), must be set to zero, reserved for future use */ + Stream_Write_UINT32(s, SpecialTypeDeviceCap); /* SpecialTypeDeviceCap (4 bytes) */ + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpdr_server_read_printer_capability_set(RdpdrServerContext* context, wStream* s, + RDPDR_CAPABILITY_HEADER* header) +{ + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpdr_server_write_printer_capability_set(RdpdrServerContext* context, wStream* s) +{ + RDPDR_CAPABILITY_HEADER header; + header.CapabilityType = CAP_PRINTER_TYPE; + header.CapabilityLength = RDPDR_CAPABILITY_HEADER_LENGTH; + header.Version = PRINT_CAPABILITY_VERSION_01; + + if (!Stream_EnsureRemainingCapacity(s, header.CapabilityLength)) + { + WLog_ERR(TAG, "Stream_EnsureRemainingCapacity failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + return rdpdr_server_write_capability_set_header(s, &header); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpdr_server_read_port_capability_set(RdpdrServerContext* context, wStream* s, + RDPDR_CAPABILITY_HEADER* header) +{ + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpdr_server_write_port_capability_set(RdpdrServerContext* context, wStream* s) +{ + RDPDR_CAPABILITY_HEADER header; + header.CapabilityType = CAP_PORT_TYPE; + header.CapabilityLength = RDPDR_CAPABILITY_HEADER_LENGTH; + header.Version = PORT_CAPABILITY_VERSION_01; + + if (!Stream_EnsureRemainingCapacity(s, header.CapabilityLength)) + { + WLog_ERR(TAG, "Stream_EnsureRemainingCapacity failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + return rdpdr_server_write_capability_set_header(s, &header); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpdr_server_read_drive_capability_set(RdpdrServerContext* context, wStream* s, + RDPDR_CAPABILITY_HEADER* header) +{ + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpdr_server_write_drive_capability_set(RdpdrServerContext* context, wStream* s) +{ + RDPDR_CAPABILITY_HEADER header; + header.CapabilityType = CAP_DRIVE_TYPE; + header.CapabilityLength = RDPDR_CAPABILITY_HEADER_LENGTH; + header.Version = DRIVE_CAPABILITY_VERSION_02; + + if (!Stream_EnsureRemainingCapacity(s, header.CapabilityLength)) + { + WLog_ERR(TAG, "Stream_EnsureRemainingCapacity failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + return rdpdr_server_write_capability_set_header(s, &header); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpdr_server_read_smartcard_capability_set(RdpdrServerContext* context, wStream* s, + RDPDR_CAPABILITY_HEADER* header) +{ + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpdr_server_write_smartcard_capability_set(RdpdrServerContext* context, wStream* s) +{ + RDPDR_CAPABILITY_HEADER header; + header.CapabilityType = CAP_SMARTCARD_TYPE; + header.CapabilityLength = RDPDR_CAPABILITY_HEADER_LENGTH; + header.Version = SMARTCARD_CAPABILITY_VERSION_01; + + if (!Stream_EnsureRemainingCapacity(s, header.CapabilityLength)) + { + WLog_ERR(TAG, "Stream_EnsureRemainingCapacity failed!"); + return CHANNEL_RC_OK; + } + + return rdpdr_server_write_capability_set_header(s, &header); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpdr_server_send_core_capability_request(RdpdrServerContext* context) +{ + wStream* s; + BOOL status; + RDPDR_HEADER header; + UINT16 numCapabilities; + ULONG written; + UINT error; + WLog_DBG(TAG, "RdpdrServerSendCoreCapabilityRequest"); + header.Component = RDPDR_CTYP_CORE; + header.PacketId = PAKID_CORE_SERVER_CAPABILITY; + numCapabilities = 1; + + if (context->supportsDrives) + numCapabilities++; + + if (context->supportsPorts) + numCapabilities++; + + if (context->supportsPrinters) + numCapabilities++; + + if (context->supportsSmartcards) + numCapabilities++; + + s = Stream_New(NULL, RDPDR_HEADER_LENGTH + 512); + + if (!s) + { + WLog_ERR(TAG, "Stream_New failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + Stream_Write_UINT16(s, header.Component); /* Component (2 bytes) */ + Stream_Write_UINT16(s, header.PacketId); /* PacketId (2 bytes) */ + Stream_Write_UINT16(s, numCapabilities); /* numCapabilities (2 bytes) */ + Stream_Write_UINT16(s, 0); /* Padding (2 bytes) */ + + if ((error = rdpdr_server_write_general_capability_set(context, s))) + { + WLog_ERR(TAG, "rdpdr_server_write_general_capability_set failed with error %" PRIu32 "!", + error); + goto out; + } + + if (context->supportsDrives) + { + if ((error = rdpdr_server_write_drive_capability_set(context, s))) + { + WLog_ERR(TAG, "rdpdr_server_write_drive_capability_set failed with error %" PRIu32 "!", + error); + goto out; + } + } + + if (context->supportsPorts) + { + if ((error = rdpdr_server_write_port_capability_set(context, s))) + { + WLog_ERR(TAG, "rdpdr_server_write_port_capability_set failed with error %" PRIu32 "!", + error); + goto out; + } + } + + if (context->supportsPrinters) + { + if ((error = rdpdr_server_write_printer_capability_set(context, s))) + { + WLog_ERR(TAG, + "rdpdr_server_write_printer_capability_set failed with error %" PRIu32 "!", + error); + goto out; + } + } + + if (context->supportsSmartcards) + { + if ((error = rdpdr_server_write_smartcard_capability_set(context, s))) + { + WLog_ERR(TAG, + "rdpdr_server_write_printer_capability_set failed with error %" PRIu32 "!", + error); + goto out; + } + } + + Stream_SealLength(s); + winpr_HexDump(TAG, WLOG_DEBUG, Stream_Buffer(s), Stream_Length(s)); + status = WTSVirtualChannelWrite(context->priv->ChannelHandle, (PCHAR)Stream_Buffer(s), + Stream_Length(s), &written); + Stream_Free(s, TRUE); + return status ? CHANNEL_RC_OK : ERROR_INTERNAL_ERROR; +out: + Stream_Free(s, TRUE); + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpdr_server_receive_core_capability_response(RdpdrServerContext* context, wStream* s, + RDPDR_HEADER* header) +{ + int i; + UINT status; + UINT16 numCapabilities; + RDPDR_CAPABILITY_HEADER capabilityHeader; + + if (Stream_GetRemainingLength(s) < 4) + { + WLog_ERR(TAG, "not enough data in stream!"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT16(s, numCapabilities); /* numCapabilities (2 bytes) */ + Stream_Seek_UINT16(s); /* Padding (2 bytes) */ + + for (i = 0; i < numCapabilities; i++) + { + if ((status = rdpdr_server_read_capability_set_header(s, &capabilityHeader))) + { + WLog_ERR(TAG, "rdpdr_server_read_capability_set_header failed with error %" PRIu32 "!", + status); + return status; + } + + switch (capabilityHeader.CapabilityType) + { + case CAP_GENERAL_TYPE: + if ((status = + rdpdr_server_read_general_capability_set(context, s, &capabilityHeader))) + { + WLog_ERR(TAG, + "rdpdr_server_read_general_capability_set failed with error %" PRIu32 + "!", + status); + return status; + } + + break; + + case CAP_PRINTER_TYPE: + if ((status = + rdpdr_server_read_printer_capability_set(context, s, &capabilityHeader))) + { + WLog_ERR(TAG, + "rdpdr_server_read_printer_capability_set failed with error %" PRIu32 + "!", + status); + return status; + } + + break; + + case CAP_PORT_TYPE: + if ((status = rdpdr_server_read_port_capability_set(context, s, &capabilityHeader))) + { + WLog_ERR(TAG, + "rdpdr_server_read_port_capability_set failed with error %" PRIu32 "!", + status); + return status; + } + + break; + + case CAP_DRIVE_TYPE: + if ((status = + rdpdr_server_read_drive_capability_set(context, s, &capabilityHeader))) + { + WLog_ERR(TAG, + "rdpdr_server_read_drive_capability_set failed with error %" PRIu32 + "!", + status); + return status; + } + + break; + + case CAP_SMARTCARD_TYPE: + if ((status = + rdpdr_server_read_smartcard_capability_set(context, s, &capabilityHeader))) + { + WLog_ERR(TAG, + "rdpdr_server_read_smartcard_capability_set failed with error %" PRIu32 + "!", + status); + return status; + } + + break; + + default: + WLog_DBG(TAG, "Unknown capabilityType %" PRIu16 "", + capabilityHeader.CapabilityType); + Stream_Seek(s, capabilityHeader.CapabilityLength - RDPDR_CAPABILITY_HEADER_LENGTH); + return ERROR_INVALID_DATA; + break; + } + } + + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpdr_server_send_client_id_confirm(RdpdrServerContext* context) +{ + wStream* s; + BOOL status; + RDPDR_HEADER header; + ULONG written; + WLog_DBG(TAG, "RdpdrServerSendClientIdConfirm"); + header.Component = RDPDR_CTYP_CORE; + header.PacketId = PAKID_CORE_CLIENTID_CONFIRM; + s = Stream_New(NULL, RDPDR_HEADER_LENGTH + 8); + + if (!s) + { + WLog_ERR(TAG, "Stream_New failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + Stream_Write_UINT16(s, header.Component); /* Component (2 bytes) */ + Stream_Write_UINT16(s, header.PacketId); /* PacketId (2 bytes) */ + Stream_Write_UINT16(s, context->priv->VersionMajor); /* VersionMajor (2 bytes) */ + Stream_Write_UINT16(s, context->priv->VersionMinor); /* VersionMinor (2 bytes) */ + Stream_Write_UINT32(s, context->priv->ClientId); /* ClientId (4 bytes) */ + Stream_SealLength(s); + winpr_HexDump(TAG, WLOG_DEBUG, Stream_Buffer(s), Stream_Length(s)); + status = WTSVirtualChannelWrite(context->priv->ChannelHandle, (PCHAR)Stream_Buffer(s), + Stream_Length(s), &written); + Stream_Free(s, TRUE); + return status ? CHANNEL_RC_OK : ERROR_INTERNAL_ERROR; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpdr_server_receive_device_list_announce_request(RdpdrServerContext* context, + wStream* s, RDPDR_HEADER* header) +{ + UINT32 i; + UINT32 DeviceCount; + UINT32 DeviceType; + UINT32 DeviceId; + char PreferredDosName[9]; + UINT32 DeviceDataLength; + + if (Stream_GetRemainingLength(s) < 4) + { + WLog_ERR(TAG, "not enough data in stream!"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT32(s, DeviceCount); /* DeviceCount (4 bytes) */ + WLog_DBG(TAG, "DeviceCount: %" PRIu32 "", DeviceCount); + + for (i = 0; i < DeviceCount; i++) + { + ZeroMemory(PreferredDosName, sizeof(PreferredDosName)); + + if (Stream_GetRemainingLength(s) < 20) + { + WLog_ERR(TAG, "not enough data in stream!"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT32(s, DeviceType); /* DeviceType (4 bytes) */ + Stream_Read_UINT32(s, DeviceId); /* DeviceId (4 bytes) */ + Stream_Read(s, PreferredDosName, 8); /* PreferredDosName (8 bytes) */ + Stream_Read_UINT32(s, DeviceDataLength); /* DeviceDataLength (4 bytes) */ + + if (Stream_GetRemainingLength(s) < DeviceDataLength) + { + WLog_ERR(TAG, "not enough data in stream!"); + return ERROR_INVALID_DATA; + } + + WLog_DBG(TAG, "Device %d Name: %s Id: 0x%08" PRIX32 " DataLength: %" PRIu32 "", i, + PreferredDosName, DeviceId, DeviceDataLength); + + switch (DeviceType) + { + case RDPDR_DTYP_FILESYSTEM: + if (context->supportsDrives) + { + IFCALL(context->OnDriveCreate, context, DeviceId, PreferredDosName); + } + + break; + + case RDPDR_DTYP_PRINT: + if (context->supportsPrinters) + { + IFCALL(context->OnPrinterCreate, context, DeviceId, PreferredDosName); + } + + break; + + case RDPDR_DTYP_SERIAL: + case RDPDR_DTYP_PARALLEL: + if (context->supportsPorts) + { + IFCALL(context->OnPortCreate, context, DeviceId, PreferredDosName); + } + + break; + + case RDPDR_DTYP_SMARTCARD: + if (context->supportsSmartcards) + { + IFCALL(context->OnSmartcardCreate, context, DeviceId, PreferredDosName); + } + + break; + + default: + break; + } + + Stream_Seek(s, DeviceDataLength); + } + + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpdr_server_receive_device_list_remove_request(RdpdrServerContext* context, wStream* s, + RDPDR_HEADER* header) +{ + UINT32 i; + UINT32 DeviceCount; + UINT32 DeviceType; + UINT32 DeviceId; + + if (Stream_GetRemainingLength(s) < 4) + { + WLog_ERR(TAG, "not enough data in stream!"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT32(s, DeviceCount); /* DeviceCount (4 bytes) */ + WLog_DBG(TAG, "DeviceCount: %" PRIu32 "", DeviceCount); + + for (i = 0; i < DeviceCount; i++) + { + if (Stream_GetRemainingLength(s) < 4) + { + WLog_ERR(TAG, "not enough data in stream!"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT32(s, DeviceId); /* DeviceId (4 bytes) */ + WLog_DBG(TAG, "Device %d Id: 0x%08" PRIX32 "", i, DeviceId); + DeviceType = 0; /* TODO: Save the device type on the announce request. */ + + switch (DeviceType) + { + case RDPDR_DTYP_FILESYSTEM: + if (context->supportsDrives) + { + IFCALL(context->OnDriveDelete, context, DeviceId); + } + + break; + + case RDPDR_DTYP_PRINT: + if (context->supportsPrinters) + { + IFCALL(context->OnPrinterDelete, context, DeviceId); + } + + break; + + case RDPDR_DTYP_SERIAL: + case RDPDR_DTYP_PARALLEL: + if (context->supportsPorts) + { + IFCALL(context->OnPortDelete, context, DeviceId); + } + + break; + + case RDPDR_DTYP_SMARTCARD: + if (context->supportsSmartcards) + { + IFCALL(context->OnSmartcardDelete, context, DeviceId); + } + + break; + + default: + break; + } + } + + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpdr_server_receive_device_io_completion(RdpdrServerContext* context, wStream* s, + RDPDR_HEADER* header) +{ + UINT32 deviceId; + UINT32 completionId; + UINT32 ioStatus; + RDPDR_IRP* irp; + UINT error = CHANNEL_RC_OK; + + if (Stream_GetRemainingLength(s) < 12) + { + WLog_ERR(TAG, "not enough data in stream!"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT32(s, deviceId); + Stream_Read_UINT32(s, completionId); + Stream_Read_UINT32(s, ioStatus); + WLog_DBG(TAG, "deviceId=%" PRIu32 ", completionId=0x%" PRIx32 ", ioStatus=0x%" PRIx32 "", + deviceId, completionId, ioStatus); + irp = rdpdr_server_dequeue_irp(context, completionId); + + if (!irp) + { + WLog_ERR(TAG, "IRP not found for completionId=0x%" PRIx32 "", completionId); + return ERROR_INTERNAL_ERROR; + } + + /* Invoke the callback. */ + if (irp->Callback) + { + error = (*irp->Callback)(context, s, irp, deviceId, completionId, ioStatus); + } + + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpdr_server_send_user_logged_on(RdpdrServerContext* context) +{ + wStream* s; + BOOL status; + RDPDR_HEADER header; + ULONG written; + WLog_DBG(TAG, "RdpdrServerSendUserLoggedOn"); + header.Component = RDPDR_CTYP_CORE; + header.PacketId = PAKID_CORE_USER_LOGGEDON; + s = Stream_New(NULL, RDPDR_HEADER_LENGTH); + + if (!s) + { + WLog_ERR(TAG, "Stream_New failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + Stream_Write_UINT16(s, header.Component); /* Component (2 bytes) */ + Stream_Write_UINT16(s, header.PacketId); /* PacketId (2 bytes) */ + Stream_SealLength(s); + winpr_HexDump(TAG, WLOG_DEBUG, Stream_Buffer(s), Stream_Length(s)); + status = WTSVirtualChannelWrite(context->priv->ChannelHandle, (PCHAR)Stream_Buffer(s), + Stream_Length(s), &written); + Stream_Free(s, TRUE); + return status ? CHANNEL_RC_OK : ERROR_INTERNAL_ERROR; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpdr_server_receive_pdu(RdpdrServerContext* context, wStream* s, RDPDR_HEADER* header) +{ + UINT error = CHANNEL_RC_OK; + WLog_DBG(TAG, "RdpdrServerReceivePdu: Component: 0x%04" PRIX16 " PacketId: 0x%04" PRIX16 "", + header->Component, header->PacketId); + winpr_HexDump(TAG, WLOG_DEBUG, Stream_Buffer(s), Stream_Length(s)); + + if (header->Component == RDPDR_CTYP_CORE) + { + switch (header->PacketId) + { + case PAKID_CORE_CLIENTID_CONFIRM: + if ((error = rdpdr_server_receive_announce_response(context, s, header))) + { + WLog_ERR(TAG, + "rdpdr_server_receive_announce_response failed with error %" PRIu32 + "!", + error); + return error; + } + + break; + + case PAKID_CORE_CLIENT_NAME: + if ((error = rdpdr_server_receive_client_name_request(context, s, header))) + { + WLog_ERR(TAG, + "rdpdr_server_receive_client_name_request failed with error %" PRIu32 + "!", + error); + return error; + } + + if ((error = rdpdr_server_send_core_capability_request(context))) + { + WLog_ERR(TAG, + "rdpdr_server_send_core_capability_request failed with error %" PRIu32 + "!", + error); + return error; + } + + if ((error = rdpdr_server_send_client_id_confirm(context))) + { + WLog_ERR(TAG, + "rdpdr_server_send_client_id_confirm failed with error %" PRIu32 "!", + error); + return error; + } + + break; + + case PAKID_CORE_CLIENT_CAPABILITY: + if ((error = rdpdr_server_receive_core_capability_response(context, s, header))) + { + WLog_ERR( + TAG, + "rdpdr_server_receive_core_capability_response failed with error %" PRIu32 + "!", + error); + return error; + } + + if (context->priv->UserLoggedOnPdu) + if ((error = rdpdr_server_send_user_logged_on(context))) + { + WLog_ERR(TAG, + "rdpdr_server_send_user_logged_on failed with error %" PRIu32 "!", + error); + return error; + } + + break; + + case PAKID_CORE_DEVICELIST_ANNOUNCE: + if ((error = rdpdr_server_receive_device_list_announce_request(context, s, header))) + { + WLog_ERR(TAG, + "rdpdr_server_receive_device_list_announce_request failed with error " + "%" PRIu32 "!", + error); + return error; + } + + break; + + case PAKID_CORE_DEVICE_REPLY: + break; + + case PAKID_CORE_DEVICE_IOREQUEST: + break; + + case PAKID_CORE_DEVICE_IOCOMPLETION: + if ((error = rdpdr_server_receive_device_io_completion(context, s, header))) + { + WLog_ERR(TAG, + "rdpdr_server_receive_device_io_completion failed with error %" PRIu32 + "!", + error); + return error; + } + + break; + + case PAKID_CORE_DEVICELIST_REMOVE: + if ((error = rdpdr_server_receive_device_list_remove_request(context, s, header))) + { + WLog_ERR(TAG, + "rdpdr_server_receive_device_io_completion failed with error %" PRIu32 + "!", + error); + return error; + } + + break; + + default: + break; + } + } + else if (header->Component == RDPDR_CTYP_PRN) + { + switch (header->PacketId) + { + case PAKID_PRN_CACHE_DATA: + break; + + case PAKID_PRN_USING_XPS: + break; + + default: + break; + } + } + else + { + WLog_WARN(TAG, "Unknown RDPDR_HEADER.Component: 0x%04" PRIX16 "", header->Component); + return ERROR_INVALID_DATA; + } + + return error; +} + +static DWORD WINAPI rdpdr_server_thread(LPVOID arg) +{ + wStream* s; + DWORD status; + DWORD nCount; + void* buffer; + HANDLE events[8]; + RDPDR_HEADER header; + HANDLE ChannelEvent; + DWORD BytesReturned; + RdpdrServerContext* context; + UINT error; + context = (RdpdrServerContext*)arg; + buffer = NULL; + BytesReturned = 0; + ChannelEvent = NULL; + s = Stream_New(NULL, 4096); + + if (!s) + { + WLog_ERR(TAG, "Stream_New failed!"); + error = CHANNEL_RC_NO_MEMORY; + goto out; + } + + if (WTSVirtualChannelQuery(context->priv->ChannelHandle, WTSVirtualEventHandle, &buffer, + &BytesReturned) == TRUE) + { + if (BytesReturned == sizeof(HANDLE)) + CopyMemory(&ChannelEvent, buffer, sizeof(HANDLE)); + + WTSFreeMemory(buffer); + } + + nCount = 0; + events[nCount++] = ChannelEvent; + events[nCount++] = context->priv->StopEvent; + + if ((error = rdpdr_server_send_announce_request(context))) + { + WLog_ERR(TAG, "rdpdr_server_send_announce_request failed with error %" PRIu32 "!", error); + goto out_stream; + } + + while (1) + { + BytesReturned = 0; + status = WaitForMultipleObjects(nCount, events, FALSE, INFINITE); + + if (status == WAIT_FAILED) + { + error = GetLastError(); + WLog_ERR(TAG, "WaitForMultipleObjects failed with error %" PRIu32 "!", error); + goto out_stream; + } + + status = WaitForSingleObject(context->priv->StopEvent, 0); + + if (status == WAIT_FAILED) + { + error = GetLastError(); + WLog_ERR(TAG, "WaitForSingleObject failed with error %" PRIu32 "!", error); + goto out_stream; + } + + if (status == WAIT_OBJECT_0) + break; + + if (!WTSVirtualChannelRead(context->priv->ChannelHandle, 0, (PCHAR)Stream_Buffer(s), + Stream_Capacity(s), &BytesReturned)) + { + WLog_ERR(TAG, "WTSVirtualChannelRead failed!"); + error = ERROR_INTERNAL_ERROR; + break; + } + + if (BytesReturned >= RDPDR_HEADER_LENGTH) + { + Stream_SetPosition(s, 0); + Stream_SetLength(s, BytesReturned); + + while (Stream_GetRemainingLength(s) >= RDPDR_HEADER_LENGTH) + { + Stream_Read_UINT16(s, header.Component); /* Component (2 bytes) */ + Stream_Read_UINT16(s, header.PacketId); /* PacketId (2 bytes) */ + + if ((error = rdpdr_server_receive_pdu(context, s, &header))) + { + WLog_ERR(TAG, "rdpdr_server_receive_pdu failed with error %" PRIu32 "!", error); + goto out_stream; + } + } + } + } + +out_stream: + Stream_Free(s, TRUE); +out: + + if (error && context->rdpcontext) + setChannelError(context->rdpcontext, error, "rdpdr_server_thread reported an error"); + + ExitThread(error); + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpdr_server_start(RdpdrServerContext* context) +{ + context->priv->ChannelHandle = + WTSVirtualChannelOpen(context->vcm, WTS_CURRENT_SESSION, "rdpdr"); + + if (!context->priv->ChannelHandle) + { + WLog_ERR(TAG, "WTSVirtualChannelOpen failed!"); + return CHANNEL_RC_BAD_CHANNEL; + } + + if (!(context->priv->StopEvent = CreateEvent(NULL, TRUE, FALSE, NULL))) + { + WLog_ERR(TAG, "CreateEvent failed!"); + return ERROR_INTERNAL_ERROR; + } + + if (!(context->priv->Thread = + CreateThread(NULL, 0, rdpdr_server_thread, (void*)context, 0, NULL))) + { + WLog_ERR(TAG, "CreateThread failed!"); + CloseHandle(context->priv->StopEvent); + context->priv->StopEvent = NULL; + return ERROR_INTERNAL_ERROR; + } + + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpdr_server_stop(RdpdrServerContext* context) +{ + UINT error; + + if (context->priv->StopEvent) + { + SetEvent(context->priv->StopEvent); + + if (WaitForSingleObject(context->priv->Thread, INFINITE) == WAIT_FAILED) + { + error = GetLastError(); + WLog_ERR(TAG, "WaitForSingleObject failed with error %" PRIu32 "!", error); + return error; + } + + CloseHandle(context->priv->Thread); + context->priv->Thread = NULL; + CloseHandle(context->priv->StopEvent); + context->priv->StopEvent = NULL; + } + + return CHANNEL_RC_OK; +} + +static void rdpdr_server_write_device_iorequest(wStream* s, UINT32 deviceId, UINT32 fileId, + UINT32 completionId, UINT32 majorFunction, + UINT32 minorFunction) +{ + Stream_Write_UINT16(s, RDPDR_CTYP_CORE); /* Component (2 bytes) */ + Stream_Write_UINT16(s, PAKID_CORE_DEVICE_IOREQUEST); /* PacketId (2 bytes) */ + Stream_Write_UINT32(s, deviceId); /* DeviceId (4 bytes) */ + Stream_Write_UINT32(s, fileId); /* FileId (4 bytes) */ + Stream_Write_UINT32(s, completionId); /* CompletionId (4 bytes) */ + Stream_Write_UINT32(s, majorFunction); /* MajorFunction (4 bytes) */ + Stream_Write_UINT32(s, minorFunction); /* MinorFunction (4 bytes) */ +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpdr_server_read_file_directory_information(wStream* s, + FILE_DIRECTORY_INFORMATION* fdi) +{ + UINT32 fileNameLength; + ZeroMemory(fdi, sizeof(FILE_DIRECTORY_INFORMATION)); + + if (Stream_GetRemainingLength(s) < 64) + { + WLog_ERR(TAG, "not enough data in stream!"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT32(s, fdi->NextEntryOffset); /* NextEntryOffset (4 bytes) */ + Stream_Read_UINT32(s, fdi->FileIndex); /* FileIndex (4 bytes) */ + Stream_Read_UINT64(s, fdi->CreationTime); /* CreationTime (8 bytes) */ + Stream_Read_UINT64(s, fdi->LastAccessTime); /* LastAccessTime (8 bytes) */ + Stream_Read_UINT64(s, fdi->LastWriteTime); /* LastWriteTime (8 bytes) */ + Stream_Read_UINT64(s, fdi->ChangeTime); /* ChangeTime (8 bytes) */ + Stream_Read_UINT64(s, fdi->EndOfFile); /* EndOfFile (8 bytes) */ + Stream_Read_UINT64(s, fdi->AllocationSize); /* AllocationSize (8 bytes) */ + Stream_Read_UINT32(s, fdi->FileAttributes); /* FileAttributes (4 bytes) */ + Stream_Read_UINT32(s, fileNameLength); /* FileNameLength (4 bytes) */ + + if (Stream_GetRemainingLength(s) < fileNameLength) + { + WLog_ERR(TAG, "not enough data in stream!"); + return ERROR_INVALID_DATA; + } + + WideCharToMultiByte(CP_ACP, 0, (LPCWSTR)Stream_Pointer(s), fileNameLength / 2, fdi->FileName, + sizeof(fdi->FileName), NULL, NULL); + Stream_Seek(s, fileNameLength); + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpdr_server_send_device_create_request(RdpdrServerContext* context, UINT32 deviceId, + UINT32 completionId, const char* path, + UINT32 desiredAccess, UINT32 createOptions, + UINT32 createDisposition) +{ + UINT32 pathLength; + ULONG written; + BOOL status; + wStream* s; + WLog_DBG(TAG, + "RdpdrServerSendDeviceCreateRequest: deviceId=%" PRIu32 + ", path=%s, desiredAccess=0x%" PRIx32 " createOptions=0x%" PRIx32 + " createDisposition=0x%" PRIx32 "", + deviceId, path, desiredAccess, createOptions, createDisposition); + /* Compute the required Unicode size. */ + pathLength = (strlen(path) + 1) * sizeof(WCHAR); + s = Stream_New(NULL, 256 + pathLength); + + if (!s) + { + WLog_ERR(TAG, "Stream_New failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + rdpdr_server_write_device_iorequest(s, deviceId, 0, completionId, IRP_MJ_CREATE, 0); + Stream_Write_UINT32(s, desiredAccess); /* DesiredAccess (4 bytes) */ + Stream_Write_UINT32(s, 0); /* AllocationSize (8 bytes) */ + Stream_Write_UINT32(s, 0); + Stream_Write_UINT32(s, 0); /* FileAttributes (4 bytes) */ + Stream_Write_UINT32(s, 3); /* SharedAccess (4 bytes) */ + Stream_Write_UINT32(s, createDisposition); /* CreateDisposition (4 bytes) */ + Stream_Write_UINT32(s, createOptions); /* CreateOptions (4 bytes) */ + Stream_Write_UINT32(s, pathLength); /* PathLength (4 bytes) */ + /* Convert the path to Unicode. */ + MultiByteToWideChar(CP_ACP, 0, path, -1, (LPWSTR)Stream_Pointer(s), pathLength); + Stream_Seek(s, pathLength); + Stream_SealLength(s); + status = WTSVirtualChannelWrite(context->priv->ChannelHandle, (PCHAR)Stream_Buffer(s), + Stream_Length(s), &written); + Stream_Free(s, TRUE); + return status ? CHANNEL_RC_OK : ERROR_INTERNAL_ERROR; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpdr_server_send_device_close_request(RdpdrServerContext* context, UINT32 deviceId, + UINT32 fileId, UINT32 completionId) +{ + ULONG written; + BOOL status; + wStream* s; + WLog_DBG(TAG, "RdpdrServerSendDeviceCloseRequest: deviceId=%" PRIu32 ", fileId=%" PRIu32 "", + deviceId, fileId); + s = Stream_New(NULL, 128); + + if (!s) + { + WLog_ERR(TAG, "Stream_New failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + rdpdr_server_write_device_iorequest(s, deviceId, fileId, completionId, IRP_MJ_CLOSE, 0); + Stream_Zero(s, 32); /* Padding (32 bytes) */ + Stream_SealLength(s); + status = WTSVirtualChannelWrite(context->priv->ChannelHandle, (PCHAR)Stream_Buffer(s), + Stream_Length(s), &written); + Stream_Free(s, TRUE); + return status ? CHANNEL_RC_OK : ERROR_INTERNAL_ERROR; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpdr_server_send_device_read_request(RdpdrServerContext* context, UINT32 deviceId, + UINT32 fileId, UINT32 completionId, UINT32 length, + UINT32 offset) +{ + ULONG written; + BOOL status; + wStream* s; + WLog_DBG(TAG, + "RdpdrServerSendDeviceReadRequest: deviceId=%" PRIu32 ", fileId=%" PRIu32 + ", length=%" PRIu32 ", offset=%" PRIu32 "", + deviceId, fileId, length, offset); + s = Stream_New(NULL, 128); + + if (!s) + { + WLog_ERR(TAG, "Stream_New failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + rdpdr_server_write_device_iorequest(s, deviceId, fileId, completionId, IRP_MJ_READ, 0); + Stream_Write_UINT32(s, length); /* Length (4 bytes) */ + Stream_Write_UINT32(s, offset); /* Offset (8 bytes) */ + Stream_Write_UINT32(s, 0); + Stream_Zero(s, 20); /* Padding (20 bytes) */ + Stream_SealLength(s); + status = WTSVirtualChannelWrite(context->priv->ChannelHandle, (PCHAR)Stream_Buffer(s), + Stream_Length(s), &written); + Stream_Free(s, TRUE); + return status ? CHANNEL_RC_OK : ERROR_INTERNAL_ERROR; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpdr_server_send_device_write_request(RdpdrServerContext* context, UINT32 deviceId, + UINT32 fileId, UINT32 completionId, + const char* data, UINT32 length, UINT32 offset) +{ + ULONG written; + BOOL status; + wStream* s; + WLog_DBG(TAG, + "RdpdrServerSendDeviceWriteRequest: deviceId=%" PRIu32 ", fileId=%" PRIu32 + ", length=%" PRIu32 ", offset=%" PRIu32 "", + deviceId, fileId, length, offset); + s = Stream_New(NULL, 64 + length); + + if (!s) + { + WLog_ERR(TAG, "Stream_New failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + rdpdr_server_write_device_iorequest(s, deviceId, fileId, completionId, IRP_MJ_WRITE, 0); + Stream_Write_UINT32(s, length); /* Length (4 bytes) */ + Stream_Write_UINT32(s, offset); /* Offset (8 bytes) */ + Stream_Write_UINT32(s, 0); + Stream_Zero(s, 20); /* Padding (20 bytes) */ + Stream_Write(s, data, length); /* WriteData (variable) */ + Stream_SealLength(s); + status = WTSVirtualChannelWrite(context->priv->ChannelHandle, (PCHAR)Stream_Buffer(s), + Stream_Length(s), &written); + Stream_Free(s, TRUE); + return status ? CHANNEL_RC_OK : ERROR_INTERNAL_ERROR; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpdr_server_send_device_query_directory_request(RdpdrServerContext* context, + UINT32 deviceId, UINT32 fileId, + UINT32 completionId, const char* path) +{ + UINT32 pathLength; + ULONG written; + BOOL status; + wStream* s; + WLog_DBG(TAG, + "RdpdrServerSendDeviceQueryDirectoryRequest: deviceId=%" PRIu32 ", fileId=%" PRIu32 + ", path=%s", + deviceId, fileId, path); + /* Compute the required Unicode size. */ + pathLength = path ? (strlen(path) + 1) * sizeof(WCHAR) : 0; + s = Stream_New(NULL, 64 + pathLength); + + if (!s) + { + WLog_ERR(TAG, "Stream_New failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + rdpdr_server_write_device_iorequest(s, deviceId, fileId, completionId, IRP_MJ_DIRECTORY_CONTROL, + IRP_MN_QUERY_DIRECTORY); + Stream_Write_UINT32(s, FileDirectoryInformation); /* FsInformationClass (4 bytes) */ + Stream_Write_UINT8(s, path ? 1 : 0); /* InitialQuery (1 byte) */ + Stream_Write_UINT32(s, pathLength); /* PathLength (4 bytes) */ + Stream_Zero(s, 23); /* Padding (23 bytes) */ + + /* Convert the path to Unicode. */ + if (pathLength > 0) + { + MultiByteToWideChar(CP_ACP, 0, path, -1, (LPWSTR)Stream_Pointer(s), pathLength); + Stream_Seek(s, pathLength); + } + + Stream_SealLength(s); + status = WTSVirtualChannelWrite(context->priv->ChannelHandle, (PCHAR)Stream_Buffer(s), + Stream_Length(s), &written); + Stream_Free(s, TRUE); + return status ? CHANNEL_RC_OK : ERROR_INTERNAL_ERROR; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpdr_server_send_device_file_rename_request(RdpdrServerContext* context, + UINT32 deviceId, UINT32 fileId, + UINT32 completionId, const char* path) +{ + UINT32 pathLength; + ULONG written; + BOOL status; + wStream* s; + WLog_DBG(TAG, + "RdpdrServerSendDeviceFileNameRequest: deviceId=%" PRIu32 ", fileId=%" PRIu32 + ", path=%s", + deviceId, fileId, path); + /* Compute the required Unicode size. */ + pathLength = path ? (strlen(path) + 1) * sizeof(WCHAR) : 0; + s = Stream_New(NULL, 64 + pathLength); + + if (!s) + { + WLog_ERR(TAG, "Stream_New failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + rdpdr_server_write_device_iorequest(s, deviceId, fileId, completionId, IRP_MJ_SET_INFORMATION, + 0); + Stream_Write_UINT32(s, FileRenameInformation); /* FsInformationClass (4 bytes) */ + Stream_Write_UINT32(s, pathLength + 6); /* Length (4 bytes) */ + Stream_Zero(s, 24); /* Padding (24 bytes) */ + /* RDP_FILE_RENAME_INFORMATION */ + Stream_Write_UINT8(s, 0); /* ReplaceIfExists (1 byte) */ + Stream_Write_UINT8(s, 0); /* RootDirectory (1 byte) */ + Stream_Write_UINT32(s, pathLength); /* FileNameLength (4 bytes) */ + + /* Convert the path to Unicode. */ + if (pathLength > 0) + { + MultiByteToWideChar(CP_ACP, 0, path, -1, (LPWSTR)Stream_Pointer(s), pathLength); + Stream_Seek(s, pathLength); + } + + Stream_SealLength(s); + status = WTSVirtualChannelWrite(context->priv->ChannelHandle, (PCHAR)Stream_Buffer(s), + Stream_Length(s), &written); + Stream_Free(s, TRUE); + return status ? CHANNEL_RC_OK : ERROR_INTERNAL_ERROR; +} + +static void rdpdr_server_convert_slashes(char* path, int size) +{ + int i; + + for (i = 0; (i < size) && (path[i] != '\0'); i++) + { + if (path[i] == '/') + path[i] = '\\'; + } +} + +/************************************************* + * Drive Create Directory + ************************************************/ + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpdr_server_drive_create_directory_callback2(RdpdrServerContext* context, wStream* s, + RDPDR_IRP* irp, UINT32 deviceId, + UINT32 completionId, UINT32 ioStatus) +{ + WLog_DBG(TAG, + "RdpdrServerDriveCreateDirectoryCallback2: deviceId=%" PRIu32 ", completionId=%" PRIu32 + ", ioStatus=0x%" PRIx32 "", + deviceId, completionId, ioStatus); + /* Invoke the create directory completion routine. */ + context->OnDriveCreateDirectoryComplete(context, irp->CallbackData, ioStatus); + /* Destroy the IRP. */ + rdpdr_server_irp_free(irp); + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpdr_server_drive_create_directory_callback1(RdpdrServerContext* context, wStream* s, + RDPDR_IRP* irp, UINT32 deviceId, + UINT32 completionId, UINT32 ioStatus) +{ + UINT32 fileId; + UINT8 information; + WLog_DBG(TAG, + "RdpdrServerDriveCreateDirectoryCallback1: deviceId=%" PRIu32 ", completionId=%" PRIu32 + ", ioStatus=0x%" PRIx32 "", + deviceId, completionId, ioStatus); + + if (ioStatus != STATUS_SUCCESS) + { + /* Invoke the create directory completion routine. */ + context->OnDriveCreateDirectoryComplete(context, irp->CallbackData, ioStatus); + /* Destroy the IRP. */ + rdpdr_server_irp_free(irp); + return CHANNEL_RC_OK; + } + + if (Stream_GetRemainingLength(s) < 5) + { + WLog_ERR(TAG, "not enough data in stream!"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT32(s, fileId); /* FileId (4 bytes) */ + Stream_Read_UINT8(s, information); /* Information (1 byte) */ + /* Setup the IRP. */ + irp->CompletionId = context->priv->NextCompletionId++; + irp->Callback = rdpdr_server_drive_create_directory_callback2; + irp->DeviceId = deviceId; + irp->FileId = fileId; + + if (!rdpdr_server_enqueue_irp(context, irp)) + { + WLog_ERR(TAG, "rdpdr_server_enqueue_irp failed!"); + rdpdr_server_irp_free(irp); + return ERROR_INTERNAL_ERROR; + } + + /* Send a request to close the file */ + return rdpdr_server_send_device_close_request(context, deviceId, fileId, irp->CompletionId); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpdr_server_drive_create_directory(RdpdrServerContext* context, void* callbackData, + UINT32 deviceId, const char* path) +{ + RDPDR_IRP* irp; + irp = rdpdr_server_irp_new(); + + if (!irp) + { + WLog_ERR(TAG, "rdpdr_server_irp_new failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + irp->CompletionId = context->priv->NextCompletionId++; + irp->Callback = rdpdr_server_drive_create_directory_callback1; + irp->CallbackData = callbackData; + irp->DeviceId = deviceId; + strncpy(irp->PathName, path, sizeof(irp->PathName) - 1); + rdpdr_server_convert_slashes(irp->PathName, sizeof(irp->PathName)); + + if (!rdpdr_server_enqueue_irp(context, irp)) + { + WLog_ERR(TAG, "rdpdr_server_enqueue_irp failed!"); + rdpdr_server_irp_free(irp); + return ERROR_INTERNAL_ERROR; + } + + /* Send a request to open the file. */ + return rdpdr_server_send_device_create_request( + context, deviceId, irp->CompletionId, irp->PathName, FILE_READ_DATA | SYNCHRONIZE, + FILE_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT, FILE_CREATE); +} + +/************************************************* + * Drive Delete Directory + ************************************************/ + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpdr_server_drive_delete_directory_callback2(RdpdrServerContext* context, wStream* s, + RDPDR_IRP* irp, UINT32 deviceId, + UINT32 completionId, UINT32 ioStatus) +{ + WLog_DBG(TAG, + "RdpdrServerDriveDeleteDirectoryCallback2: deviceId=%" PRIu32 ", completionId=%" PRIu32 + ", ioStatus=0x%" PRIx32 "", + deviceId, completionId, ioStatus); + /* Invoke the delete directory completion routine. */ + context->OnDriveDeleteDirectoryComplete(context, irp->CallbackData, ioStatus); + /* Destroy the IRP. */ + rdpdr_server_irp_free(irp); + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpdr_server_drive_delete_directory_callback1(RdpdrServerContext* context, wStream* s, + RDPDR_IRP* irp, UINT32 deviceId, + UINT32 completionId, UINT32 ioStatus) +{ + UINT32 fileId; + UINT8 information; + WLog_DBG(TAG, + "RdpdrServerDriveDeleteDirectoryCallback1: deviceId=%" PRIu32 ", completionId=%" PRIu32 + ", ioStatus=0x%" PRIx32 "", + deviceId, completionId, ioStatus); + + if (ioStatus != STATUS_SUCCESS) + { + /* Invoke the delete directory completion routine. */ + context->OnDriveDeleteFileComplete(context, irp->CallbackData, ioStatus); + /* Destroy the IRP. */ + rdpdr_server_irp_free(irp); + return CHANNEL_RC_OK; + } + + if (Stream_GetRemainingLength(s) < 5) + { + WLog_ERR(TAG, "not enough data in stream!"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT32(s, fileId); /* FileId (4 bytes) */ + Stream_Read_UINT8(s, information); /* Information (1 byte) */ + /* Setup the IRP. */ + irp->CompletionId = context->priv->NextCompletionId++; + irp->Callback = rdpdr_server_drive_delete_directory_callback2; + irp->DeviceId = deviceId; + irp->FileId = fileId; + + if (!rdpdr_server_enqueue_irp(context, irp)) + { + WLog_ERR(TAG, "rdpdr_server_enqueue_irp failed!"); + rdpdr_server_irp_free(irp); + return ERROR_INTERNAL_ERROR; + } + + /* Send a request to close the file */ + return rdpdr_server_send_device_close_request(context, deviceId, fileId, irp->CompletionId); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpdr_server_drive_delete_directory(RdpdrServerContext* context, void* callbackData, + UINT32 deviceId, const char* path) +{ + RDPDR_IRP* irp; + irp = rdpdr_server_irp_new(); + + if (!irp) + { + WLog_ERR(TAG, "rdpdr_server_irp_new failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + irp->CompletionId = context->priv->NextCompletionId++; + irp->Callback = rdpdr_server_drive_delete_directory_callback1; + irp->CallbackData = callbackData; + irp->DeviceId = deviceId; + strncpy(irp->PathName, path, sizeof(irp->PathName) - 1); + rdpdr_server_convert_slashes(irp->PathName, sizeof(irp->PathName)); + + if (!rdpdr_server_enqueue_irp(context, irp)) + { + WLog_ERR(TAG, "rdpdr_server_enqueue_irp failed!"); + rdpdr_server_irp_free(irp); + return ERROR_INTERNAL_ERROR; + } + + /* Send a request to open the file. */ + return rdpdr_server_send_device_create_request( + context, deviceId, irp->CompletionId, irp->PathName, DELETE | SYNCHRONIZE, + FILE_DIRECTORY_FILE | FILE_DELETE_ON_CLOSE | FILE_SYNCHRONOUS_IO_NONALERT, FILE_OPEN); +} + +/************************************************* + * Drive Query Directory + ************************************************/ + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpdr_server_drive_query_directory_callback2(RdpdrServerContext* context, wStream* s, + RDPDR_IRP* irp, UINT32 deviceId, + UINT32 completionId, UINT32 ioStatus) +{ + UINT error; + UINT32 length; + FILE_DIRECTORY_INFORMATION fdi; + WLog_DBG(TAG, + "RdpdrServerDriveQueryDirectoryCallback2: deviceId=%" PRIu32 ", completionId=%" PRIu32 + ", ioStatus=0x%" PRIx32 "", + deviceId, completionId, ioStatus); + + if (Stream_GetRemainingLength(s) < 4) + { + WLog_ERR(TAG, "not enough data in stream!"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT32(s, length); /* Length (4 bytes) */ + + if (length > 0) + { + if ((error = rdpdr_server_read_file_directory_information(s, &fdi))) + { + WLog_ERR(TAG, + "rdpdr_server_read_file_directory_information failed with error %" PRIu32 "!", + error); + return error; + } + } + else + { + if (Stream_GetRemainingLength(s) < 1) + { + WLog_ERR(TAG, "not enough data in stream!"); + return ERROR_INVALID_DATA; + } + + Stream_Seek(s, 1); /* Padding (1 byte) */ + } + + if (ioStatus == STATUS_SUCCESS) + { + /* Invoke the query directory completion routine. */ + context->OnDriveQueryDirectoryComplete(context, irp->CallbackData, ioStatus, + length > 0 ? &fdi : NULL); + /* Setup the IRP. */ + irp->CompletionId = context->priv->NextCompletionId++; + irp->Callback = rdpdr_server_drive_query_directory_callback2; + + if (!rdpdr_server_enqueue_irp(context, irp)) + { + WLog_ERR(TAG, "rdpdr_server_enqueue_irp failed!"); + rdpdr_server_irp_free(irp); + return ERROR_INTERNAL_ERROR; + } + + /* Send a request to query the directory. */ + return rdpdr_server_send_device_query_directory_request(context, irp->DeviceId, irp->FileId, + irp->CompletionId, NULL); + } + else + { + /* Invoke the query directory completion routine. */ + context->OnDriveQueryDirectoryComplete(context, irp->CallbackData, ioStatus, NULL); + /* Destroy the IRP. */ + rdpdr_server_irp_free(irp); + } + + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpdr_server_drive_query_directory_callback1(RdpdrServerContext* context, wStream* s, + RDPDR_IRP* irp, UINT32 deviceId, + UINT32 completionId, UINT32 ioStatus) +{ + UINT32 fileId; + WLog_DBG(TAG, + "RdpdrServerDriveQueryDirectoryCallback1: deviceId=%" PRIu32 ", completionId=%" PRIu32 + ", ioStatus=0x%" PRIx32 "", + deviceId, completionId, ioStatus); + + if (ioStatus != STATUS_SUCCESS) + { + /* Invoke the query directory completion routine. */ + context->OnDriveQueryDirectoryComplete(context, irp->CallbackData, ioStatus, NULL); + /* Destroy the IRP. */ + rdpdr_server_irp_free(irp); + return CHANNEL_RC_OK; + } + + if (Stream_GetRemainingLength(s) < 4) + { + WLog_ERR(TAG, "not enough data in stream!"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT32(s, fileId); + /* Setup the IRP. */ + irp->CompletionId = context->priv->NextCompletionId++; + irp->Callback = rdpdr_server_drive_query_directory_callback2; + irp->DeviceId = deviceId; + irp->FileId = fileId; + winpr_str_append("\\*.*", irp->PathName, ARRAYSIZE(irp->PathName), NULL); + + if (!rdpdr_server_enqueue_irp(context, irp)) + { + WLog_ERR(TAG, "rdpdr_server_enqueue_irp failed!"); + rdpdr_server_irp_free(irp); + return ERROR_INTERNAL_ERROR; + } + + /* Send a request to query the directory. */ + return rdpdr_server_send_device_query_directory_request(context, deviceId, fileId, + irp->CompletionId, irp->PathName); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpdr_server_drive_query_directory(RdpdrServerContext* context, void* callbackData, + UINT32 deviceId, const char* path) +{ + RDPDR_IRP* irp; + irp = rdpdr_server_irp_new(); + + if (!irp) + { + WLog_ERR(TAG, "rdpdr_server_irp_new failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + irp->CompletionId = context->priv->NextCompletionId++; + irp->Callback = rdpdr_server_drive_query_directory_callback1; + irp->CallbackData = callbackData; + irp->DeviceId = deviceId; + strncpy(irp->PathName, path, sizeof(irp->PathName) - 1); + rdpdr_server_convert_slashes(irp->PathName, sizeof(irp->PathName)); + + if (!rdpdr_server_enqueue_irp(context, irp)) + { + WLog_ERR(TAG, "rdpdr_server_enqueue_irp failed!"); + rdpdr_server_irp_free(irp); + return ERROR_INTERNAL_ERROR; + } + + /* Send a request to open the directory. */ + return rdpdr_server_send_device_create_request( + context, deviceId, irp->CompletionId, irp->PathName, FILE_READ_DATA | SYNCHRONIZE, + FILE_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT, FILE_OPEN); +} + +/************************************************* + * Drive Open File + ************************************************/ + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpdr_server_drive_open_file_callback(RdpdrServerContext* context, wStream* s, + RDPDR_IRP* irp, UINT32 deviceId, + UINT32 completionId, UINT32 ioStatus) +{ + UINT32 fileId; + UINT8 information; + WLog_DBG(TAG, + "RdpdrServerDriveOpenFileCallback: deviceId=%" PRIu32 ", completionId=%" PRIu32 + ", ioStatus=0x%" PRIx32 "", + deviceId, completionId, ioStatus); + + if (Stream_GetRemainingLength(s) < 5) + { + WLog_ERR(TAG, "not enough data in stream!"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT32(s, fileId); /* FileId (4 bytes) */ + Stream_Read_UINT8(s, information); /* Information (1 byte) */ + /* Invoke the open file completion routine. */ + context->OnDriveOpenFileComplete(context, irp->CallbackData, ioStatus, deviceId, fileId); + /* Destroy the IRP. */ + rdpdr_server_irp_free(irp); + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpdr_server_drive_open_file(RdpdrServerContext* context, void* callbackData, + UINT32 deviceId, const char* path, UINT32 desiredAccess, + UINT32 createDisposition) +{ + RDPDR_IRP* irp; + irp = rdpdr_server_irp_new(); + + if (!irp) + { + WLog_ERR(TAG, "rdpdr_server_irp_new failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + irp->CompletionId = context->priv->NextCompletionId++; + irp->Callback = rdpdr_server_drive_open_file_callback; + irp->CallbackData = callbackData; + irp->DeviceId = deviceId; + strncpy(irp->PathName, path, sizeof(irp->PathName) - 1); + rdpdr_server_convert_slashes(irp->PathName, sizeof(irp->PathName)); + + if (!rdpdr_server_enqueue_irp(context, irp)) + { + WLog_ERR(TAG, "rdpdr_server_enqueue_irp failed!"); + rdpdr_server_irp_free(irp); + return ERROR_INTERNAL_ERROR; + } + + /* Send a request to open the file. */ + return rdpdr_server_send_device_create_request(context, deviceId, irp->CompletionId, + irp->PathName, desiredAccess | SYNCHRONIZE, + FILE_SYNCHRONOUS_IO_NONALERT, createDisposition); +} + +/************************************************* + * Drive Read File + ************************************************/ + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpdr_server_drive_read_file_callback(RdpdrServerContext* context, wStream* s, + RDPDR_IRP* irp, UINT32 deviceId, + UINT32 completionId, UINT32 ioStatus) +{ + UINT32 length; + char* buffer = NULL; + WLog_DBG(TAG, + "RdpdrServerDriveReadFileCallback: deviceId=%" PRIu32 ", completionId=%" PRIu32 + ", ioStatus=0x%" PRIx32 "", + deviceId, completionId, ioStatus); + + if (Stream_GetRemainingLength(s) < 4) + { + WLog_ERR(TAG, "not enough data in stream!"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT32(s, length); /* Length (4 bytes) */ + + if (Stream_GetRemainingLength(s) < length) + { + WLog_ERR(TAG, "not enough data in stream!"); + return ERROR_INVALID_DATA; + } + + if (length > 0) + { + buffer = (char*)Stream_Pointer(s); + Stream_Seek(s, length); + } + + /* Invoke the read file completion routine. */ + context->OnDriveReadFileComplete(context, irp->CallbackData, ioStatus, buffer, length); + /* Destroy the IRP. */ + rdpdr_server_irp_free(irp); + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpdr_server_drive_read_file(RdpdrServerContext* context, void* callbackData, + UINT32 deviceId, UINT32 fileId, UINT32 length, + UINT32 offset) +{ + RDPDR_IRP* irp; + irp = rdpdr_server_irp_new(); + + if (!irp) + { + WLog_ERR(TAG, "rdpdr_server_irp_new failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + irp->CompletionId = context->priv->NextCompletionId++; + irp->Callback = rdpdr_server_drive_read_file_callback; + irp->CallbackData = callbackData; + irp->DeviceId = deviceId; + irp->FileId = fileId; + + if (!rdpdr_server_enqueue_irp(context, irp)) + { + WLog_ERR(TAG, "rdpdr_server_enqueue_irp failed!"); + rdpdr_server_irp_free(irp); + return ERROR_INTERNAL_ERROR; + } + + /* Send a request to open the directory. */ + return rdpdr_server_send_device_read_request(context, deviceId, fileId, irp->CompletionId, + length, offset); +} + +/************************************************* + * Drive Write File + ************************************************/ + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpdr_server_drive_write_file_callback(RdpdrServerContext* context, wStream* s, + RDPDR_IRP* irp, UINT32 deviceId, + UINT32 completionId, UINT32 ioStatus) +{ + UINT32 length; + WLog_DBG(TAG, + "RdpdrServerDriveWriteFileCallback: deviceId=%" PRIu32 ", completionId=%" PRIu32 + ", ioStatus=0x%" PRIx32 "", + deviceId, completionId, ioStatus); + + if (Stream_GetRemainingLength(s) < 5) + { + WLog_ERR(TAG, "not enough data in stream!"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT32(s, length); /* Length (4 bytes) */ + Stream_Seek(s, 1); /* Padding (1 byte) */ + + if (Stream_GetRemainingLength(s) < length) + { + WLog_ERR(TAG, "not enough data in stream!"); + return ERROR_INVALID_DATA; + } + + /* Invoke the write file completion routine. */ + context->OnDriveWriteFileComplete(context, irp->CallbackData, ioStatus, length); + /* Destroy the IRP. */ + rdpdr_server_irp_free(irp); + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpdr_server_drive_write_file(RdpdrServerContext* context, void* callbackData, + UINT32 deviceId, UINT32 fileId, const char* buffer, + UINT32 length, UINT32 offset) +{ + RDPDR_IRP* irp; + irp = rdpdr_server_irp_new(); + + if (!irp) + { + WLog_ERR(TAG, "rdpdr_server_irp_new failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + irp->CompletionId = context->priv->NextCompletionId++; + irp->Callback = rdpdr_server_drive_write_file_callback; + irp->CallbackData = callbackData; + irp->DeviceId = deviceId; + irp->FileId = fileId; + + if (!rdpdr_server_enqueue_irp(context, irp)) + { + WLog_ERR(TAG, "rdpdr_server_enqueue_irp failed!"); + rdpdr_server_irp_free(irp); + return ERROR_INTERNAL_ERROR; + } + + /* Send a request to open the directory. */ + return rdpdr_server_send_device_write_request(context, deviceId, fileId, irp->CompletionId, + buffer, length, offset); +} + +/************************************************* + * Drive Close File + ************************************************/ + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpdr_server_drive_close_file_callback(RdpdrServerContext* context, wStream* s, + RDPDR_IRP* irp, UINT32 deviceId, + UINT32 completionId, UINT32 ioStatus) +{ + WLog_DBG(TAG, + "RdpdrServerDriveCloseFileCallback: deviceId=%" PRIu32 ", completionId=%" PRIu32 + ", ioStatus=0x%" PRIx32 "", + deviceId, completionId, ioStatus); + /* Invoke the close file completion routine. */ + context->OnDriveCloseFileComplete(context, irp->CallbackData, ioStatus); + /* Destroy the IRP. */ + rdpdr_server_irp_free(irp); + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpdr_server_drive_close_file(RdpdrServerContext* context, void* callbackData, + UINT32 deviceId, UINT32 fileId) +{ + RDPDR_IRP* irp; + irp = rdpdr_server_irp_new(); + + if (!irp) + { + WLog_ERR(TAG, "rdpdr_server_irp_new failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + irp->CompletionId = context->priv->NextCompletionId++; + irp->Callback = rdpdr_server_drive_close_file_callback; + irp->CallbackData = callbackData; + irp->DeviceId = deviceId; + irp->FileId = fileId; + + if (!rdpdr_server_enqueue_irp(context, irp)) + { + WLog_ERR(TAG, "rdpdr_server_enqueue_irp failed!"); + rdpdr_server_irp_free(irp); + return ERROR_INTERNAL_ERROR; + } + + /* Send a request to open the directory. */ + return rdpdr_server_send_device_close_request(context, deviceId, fileId, irp->CompletionId); +} + +/************************************************* + * Drive Delete File + ************************************************/ + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpdr_server_drive_delete_file_callback2(RdpdrServerContext* context, wStream* s, + RDPDR_IRP* irp, UINT32 deviceId, + UINT32 completionId, UINT32 ioStatus) +{ + WLog_DBG(TAG, + "RdpdrServerDriveDeleteFileCallback2: deviceId=%" PRIu32 ", completionId=%" PRIu32 + ", ioStatus=0x%" PRIx32 "", + deviceId, completionId, ioStatus); + /* Invoke the delete file completion routine. */ + context->OnDriveDeleteFileComplete(context, irp->CallbackData, ioStatus); + /* Destroy the IRP. */ + rdpdr_server_irp_free(irp); + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpdr_server_drive_delete_file_callback1(RdpdrServerContext* context, wStream* s, + RDPDR_IRP* irp, UINT32 deviceId, + UINT32 completionId, UINT32 ioStatus) +{ + UINT32 fileId; + UINT8 information; + WLog_DBG(TAG, + "RdpdrServerDriveDeleteFileCallback1: deviceId=%" PRIu32 ", completionId=%" PRIu32 + ", ioStatus=0x%" PRIx32 "", + deviceId, completionId, ioStatus); + + if (ioStatus != STATUS_SUCCESS) + { + /* Invoke the close file completion routine. */ + context->OnDriveDeleteFileComplete(context, irp->CallbackData, ioStatus); + /* Destroy the IRP. */ + rdpdr_server_irp_free(irp); + return CHANNEL_RC_OK; + } + + if (Stream_GetRemainingLength(s) < 5) + { + WLog_ERR(TAG, "not enough data in stream!"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT32(s, fileId); /* FileId (4 bytes) */ + Stream_Read_UINT8(s, information); /* Information (1 byte) */ + /* Setup the IRP. */ + irp->CompletionId = context->priv->NextCompletionId++; + irp->Callback = rdpdr_server_drive_delete_file_callback2; + irp->DeviceId = deviceId; + irp->FileId = fileId; + + if (!rdpdr_server_enqueue_irp(context, irp)) + { + WLog_ERR(TAG, "rdpdr_server_enqueue_irp failed!"); + rdpdr_server_irp_free(irp); + return ERROR_INTERNAL_ERROR; + } + + /* Send a request to close the file */ + return rdpdr_server_send_device_close_request(context, deviceId, fileId, irp->CompletionId); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpdr_server_drive_delete_file(RdpdrServerContext* context, void* callbackData, + UINT32 deviceId, const char* path) +{ + RDPDR_IRP* irp; + irp = rdpdr_server_irp_new(); + + if (!irp) + { + WLog_ERR(TAG, "rdpdr_server_irp_new failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + irp->CompletionId = context->priv->NextCompletionId++; + irp->Callback = rdpdr_server_drive_delete_file_callback1; + irp->CallbackData = callbackData; + irp->DeviceId = deviceId; + strncpy(irp->PathName, path, sizeof(irp->PathName) - 1); + rdpdr_server_convert_slashes(irp->PathName, sizeof(irp->PathName)); + + if (!rdpdr_server_enqueue_irp(context, irp)) + { + WLog_ERR(TAG, "rdpdr_server_enqueue_irp failed!"); + rdpdr_server_irp_free(irp); + return ERROR_INTERNAL_ERROR; + } + + /* Send a request to open the file. */ + return rdpdr_server_send_device_create_request( + context, deviceId, irp->CompletionId, irp->PathName, FILE_READ_DATA | SYNCHRONIZE, + FILE_DELETE_ON_CLOSE | FILE_SYNCHRONOUS_IO_NONALERT, FILE_OPEN); +} + +/************************************************* + * Drive Rename File + ************************************************/ + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpdr_server_drive_rename_file_callback3(RdpdrServerContext* context, wStream* s, + RDPDR_IRP* irp, UINT32 deviceId, + UINT32 completionId, UINT32 ioStatus) +{ + WLog_DBG(TAG, + "RdpdrServerDriveRenameFileCallback3: deviceId=%" PRIu32 ", completionId=%" PRIu32 + ", ioStatus=0x%" PRIx32 "", + deviceId, completionId, ioStatus); + /* Destroy the IRP. */ + rdpdr_server_irp_free(irp); + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpdr_server_drive_rename_file_callback2(RdpdrServerContext* context, wStream* s, + RDPDR_IRP* irp, UINT32 deviceId, + UINT32 completionId, UINT32 ioStatus) +{ + UINT32 length; + WLog_DBG(TAG, + "RdpdrServerDriveRenameFileCallback2: deviceId=%" PRIu32 ", completionId=%" PRIu32 + ", ioStatus=0x%" PRIx32 "", + deviceId, completionId, ioStatus); + + if (Stream_GetRemainingLength(s) < 5) + { + WLog_ERR(TAG, "not enough data in stream!"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT32(s, length); /* Length (4 bytes) */ + Stream_Seek(s, 1); /* Padding (1 byte) */ + /* Invoke the rename file completion routine. */ + context->OnDriveRenameFileComplete(context, irp->CallbackData, ioStatus); + /* Setup the IRP. */ + irp->CompletionId = context->priv->NextCompletionId++; + irp->Callback = rdpdr_server_drive_rename_file_callback3; + irp->DeviceId = deviceId; + + if (!rdpdr_server_enqueue_irp(context, irp)) + { + WLog_ERR(TAG, "rdpdr_server_enqueue_irp failed!"); + rdpdr_server_irp_free(irp); + return ERROR_INTERNAL_ERROR; + } + + /* Send a request to close the file */ + return rdpdr_server_send_device_close_request(context, deviceId, irp->FileId, + irp->CompletionId); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpdr_server_drive_rename_file_callback1(RdpdrServerContext* context, wStream* s, + RDPDR_IRP* irp, UINT32 deviceId, + UINT32 completionId, UINT32 ioStatus) +{ + UINT32 fileId; + UINT8 information; + WLog_DBG(TAG, + "RdpdrServerDriveRenameFileCallback1: deviceId=%" PRIu32 ", completionId=%" PRIu32 + ", ioStatus=0x%" PRIx32 "", + deviceId, completionId, ioStatus); + + if (ioStatus != STATUS_SUCCESS) + { + /* Invoke the rename file completion routine. */ + context->OnDriveRenameFileComplete(context, irp->CallbackData, ioStatus); + /* Destroy the IRP. */ + rdpdr_server_irp_free(irp); + return CHANNEL_RC_OK; + } + + if (Stream_GetRemainingLength(s) < 5) + { + WLog_ERR(TAG, "not enough data in stream!"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT32(s, fileId); /* FileId (4 bytes) */ + Stream_Read_UINT8(s, information); /* Information (1 byte) */ + /* Setup the IRP. */ + irp->CompletionId = context->priv->NextCompletionId++; + irp->Callback = rdpdr_server_drive_rename_file_callback2; + irp->DeviceId = deviceId; + irp->FileId = fileId; + + if (!rdpdr_server_enqueue_irp(context, irp)) + { + WLog_ERR(TAG, "rdpdr_server_enqueue_irp failed!"); + rdpdr_server_irp_free(irp); + return ERROR_INTERNAL_ERROR; + } + + /* Send a request to rename the file */ + return rdpdr_server_send_device_file_rename_request(context, deviceId, fileId, + irp->CompletionId, irp->ExtraBuffer); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpdr_server_drive_rename_file(RdpdrServerContext* context, void* callbackData, + UINT32 deviceId, const char* oldPath, + const char* newPath) +{ + RDPDR_IRP* irp; + irp = rdpdr_server_irp_new(); + + if (!irp) + { + WLog_ERR(TAG, "rdpdr_server_irp_new failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + irp->CompletionId = context->priv->NextCompletionId++; + irp->Callback = rdpdr_server_drive_rename_file_callback1; + irp->CallbackData = callbackData; + irp->DeviceId = deviceId; + strncpy(irp->PathName, oldPath, sizeof(irp->PathName) - 1); + strncpy(irp->ExtraBuffer, newPath, sizeof(irp->ExtraBuffer) - 1); + rdpdr_server_convert_slashes(irp->PathName, sizeof(irp->PathName)); + rdpdr_server_convert_slashes(irp->ExtraBuffer, sizeof(irp->ExtraBuffer)); + + if (!rdpdr_server_enqueue_irp(context, irp)) + { + WLog_ERR(TAG, "rdpdr_server_enqueue_irp failed!"); + rdpdr_server_irp_free(irp); + return ERROR_INTERNAL_ERROR; + } + + /* Send a request to open the file. */ + return rdpdr_server_send_device_create_request(context, deviceId, irp->CompletionId, + irp->PathName, FILE_READ_DATA | SYNCHRONIZE, + FILE_SYNCHRONOUS_IO_NONALERT, FILE_OPEN); +} + +RdpdrServerContext* rdpdr_server_context_new(HANDLE vcm) +{ + RdpdrServerContext* context; + context = (RdpdrServerContext*)calloc(1, sizeof(RdpdrServerContext)); + + if (context) + { + context->vcm = vcm; + context->Start = rdpdr_server_start; + context->Stop = rdpdr_server_stop; + context->DriveCreateDirectory = rdpdr_server_drive_create_directory; + context->DriveDeleteDirectory = rdpdr_server_drive_delete_directory; + context->DriveQueryDirectory = rdpdr_server_drive_query_directory; + context->DriveOpenFile = rdpdr_server_drive_open_file; + context->DriveReadFile = rdpdr_server_drive_read_file; + context->DriveWriteFile = rdpdr_server_drive_write_file; + context->DriveCloseFile = rdpdr_server_drive_close_file; + context->DriveDeleteFile = rdpdr_server_drive_delete_file; + context->DriveRenameFile = rdpdr_server_drive_rename_file; + context->priv = (RdpdrServerPrivate*)calloc(1, sizeof(RdpdrServerPrivate)); + + if (!context->priv) + { + WLog_ERR(TAG, "calloc failed!"); + free(context); + return NULL; + } + + context->priv->VersionMajor = RDPDR_VERSION_MAJOR; + context->priv->VersionMinor = RDPDR_VERSION_MINOR_RDP6X; + context->priv->ClientId = g_ClientId++; + context->priv->UserLoggedOnPdu = TRUE; + context->priv->NextCompletionId = 1; + context->priv->IrpList = ListDictionary_New(TRUE); + + if (!context->priv->IrpList) + { + WLog_ERR(TAG, "ListDictionary_New failed!"); + free(context->priv); + free(context); + return NULL; + } + } + else + { + WLog_ERR(TAG, "calloc failed!"); + } + + return context; +} + +void rdpdr_server_context_free(RdpdrServerContext* context) +{ + if (context) + { + if (context->priv) + { + ListDictionary_Free(context->priv->IrpList); + free(context->priv); + } + + free(context); + } +} diff --git a/channels/rdpdr/server/rdpdr_main.h b/channels/rdpdr/server/rdpdr_main.h new file mode 100644 index 0000000..f3f54cc --- /dev/null +++ b/channels/rdpdr/server/rdpdr_main.h @@ -0,0 +1,91 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Device Redirection Virtual Channel Extension + * + * Copyright 2014 Dell Software + * Copyright 2013 Marc-Andre Moreau + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger + * + * Licensed 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 FREERDP_CHANNEL_RDPDR_SERVER_MAIN_H +#define FREERDP_CHANNEL_RDPDR_SERVER_MAIN_H + +#include +#include +#include +#include + +#include +#include + +struct _rdpdr_server_private +{ + HANDLE Thread; + HANDLE StopEvent; + void* ChannelHandle; + + UINT32 ClientId; + UINT16 VersionMajor; + UINT16 VersionMinor; + char* ClientComputerName; + + BOOL UserLoggedOnPdu; + + wListDictionary* IrpList; + UINT32 NextCompletionId; +}; + +#define RDPDR_HEADER_LENGTH 4 + +struct _RDPDR_HEADER +{ + UINT16 Component; + UINT16 PacketId; +}; +typedef struct _RDPDR_HEADER RDPDR_HEADER; + +#define RDPDR_VERSION_MAJOR 0x0001 + +#define RDPDR_VERSION_MINOR_RDP50 0x0002 +#define RDPDR_VERSION_MINOR_RDP51 0x0005 +#define RDPDR_VERSION_MINOR_RDP52 0x000A +#define RDPDR_VERSION_MINOR_RDP6X 0x000C + +#define RDPDR_CAPABILITY_HEADER_LENGTH 8 + +struct _RDPDR_CAPABILITY_HEADER +{ + UINT16 CapabilityType; + UINT16 CapabilityLength; + UINT32 Version; +}; +typedef struct _RDPDR_CAPABILITY_HEADER RDPDR_CAPABILITY_HEADER; + +struct _RDPDR_IRP +{ + UINT32 CompletionId; + UINT32 DeviceId; + UINT32 FileId; + char PathName[256]; + char ExtraBuffer[256]; + void* CallbackData; + UINT(*Callback) + (RdpdrServerContext* context, wStream* s, struct _RDPDR_IRP* irp, UINT32 deviceId, + UINT32 completionId, UINT32 ioStatus); +}; +typedef struct _RDPDR_IRP RDPDR_IRP; + +#endif /* FREERDP_CHANNEL_RDPDR_SERVER_MAIN_H */ diff --git a/channels/rdpecam/CMakeLists.txt b/channels/rdpecam/CMakeLists.txt new file mode 100644 index 0000000..63ed410 --- /dev/null +++ b/channels/rdpecam/CMakeLists.txt @@ -0,0 +1,22 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2022 Pascal Nowack +# +# Licensed 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. + +define_channel("rdpecam") + +if(WITH_SERVER_CHANNELS) + add_channel_server(${MODULE_PREFIX} ${CHANNEL_NAME}) +endif() diff --git a/channels/rdpecam/ChannelOptions.cmake b/channels/rdpecam/ChannelOptions.cmake new file mode 100644 index 0000000..7528d11 --- /dev/null +++ b/channels/rdpecam/ChannelOptions.cmake @@ -0,0 +1,12 @@ + +set(OPTION_DEFAULT ON) +set(OPTION_CLIENT_DEFAULT OFF) +set(OPTION_SERVER_DEFAULT ON) + +define_channel_options(NAME "rdpecam" TYPE "dynamic" + DESCRIPTION "Video Capture Virtual Channel Extension" + SPECIFICATIONS "[MS-RDPECAM]" + DEFAULT ${OPTION_DEFAULT}) + +define_channel_server_options(${OPTION_SERVER_DEFAULT}) + diff --git a/channels/rdpecam/server/CMakeLists.txt b/channels/rdpecam/server/CMakeLists.txt new file mode 100644 index 0000000..2a65ba7 --- /dev/null +++ b/channels/rdpecam/server/CMakeLists.txt @@ -0,0 +1,27 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2022 Pascal Nowack +# +# Licensed 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. + +define_channel_server("rdpecam") + +set(${MODULE_PREFIX}_SRCS + camera_device_enumerator_main.c + camera_device_main.c) + +add_channel_server_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} FALSE "DVCPluginEntry") + +target_link_libraries(${MODULE_NAME} freerdp) +set_property(TARGET ${MODULE_NAME} PROPERTY FOLDER "Channels/${CHANNEL_NAME}/Server") diff --git a/channels/rdpecam/server/camera_device_enumerator_main.c b/channels/rdpecam/server/camera_device_enumerator_main.c new file mode 100644 index 0000000..a17eeee --- /dev/null +++ b/channels/rdpecam/server/camera_device_enumerator_main.c @@ -0,0 +1,612 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Video Capture Virtual Channel Extension + * + * Copyright 2022 Pascal Nowack + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include + +#define TAG CHANNELS_TAG("rdpecam-enumerator.server") + +typedef enum +{ + ENUMERATOR_INITIAL, + ENUMERATOR_OPENED, +} eEnumeratorChannelState; + +typedef struct +{ + CamDevEnumServerContext context; + + HANDLE stopEvent; + + HANDLE thread; + void* enumerator_channel; + + DWORD SessionId; + + BOOL isOpened; + BOOL externalThread; + + /* Channel state */ + eEnumeratorChannelState state; + + wStream* buffer; +} enumerator_server; + +static UINT enumerator_server_initialize(CamDevEnumServerContext* context, BOOL externalThread) +{ + UINT error = CHANNEL_RC_OK; + enumerator_server* enumerator = (enumerator_server*)context; + + WINPR_ASSERT(enumerator); + + if (enumerator->isOpened) + { + WLog_WARN(TAG, "Application error: Camera Device Enumerator channel already initialized, " + "calling in this state is not possible!"); + return ERROR_INVALID_STATE; + } + + enumerator->externalThread = externalThread; + + return error; +} + +static UINT enumerator_server_open_channel(enumerator_server* enumerator) +{ + CamDevEnumServerContext* context = &enumerator->context; + DWORD Error = ERROR_SUCCESS; + HANDLE hEvent; + DWORD BytesReturned = 0; + PULONG pSessionId = NULL; + UINT32 channelId; + BOOL status = TRUE; + + WINPR_ASSERT(enumerator); + + if (WTSQuerySessionInformationA(enumerator->context.vcm, WTS_CURRENT_SESSION, WTSSessionId, + (LPSTR*)&pSessionId, &BytesReturned) == FALSE) + { + WLog_ERR(TAG, "WTSQuerySessionInformationA failed!"); + return ERROR_INTERNAL_ERROR; + } + + enumerator->SessionId = (DWORD)*pSessionId; + WTSFreeMemory(pSessionId); + hEvent = WTSVirtualChannelManagerGetEventHandle(enumerator->context.vcm); + + if (WaitForSingleObject(hEvent, 1000) == WAIT_FAILED) + { + Error = GetLastError(); + WLog_ERR(TAG, "WaitForSingleObject failed with error %" PRIu32 "!", Error); + return Error; + } + + enumerator->enumerator_channel = WTSVirtualChannelOpenEx( + enumerator->SessionId, RDPECAM_CONTROL_DVC_CHANNEL_NAME, WTS_CHANNEL_OPTION_DYNAMIC); + if (!enumerator->enumerator_channel) + { + Error = GetLastError(); + WLog_ERR(TAG, "WTSVirtualChannelOpenEx failed with error %" PRIu32 "!", Error); + return Error; + } + + channelId = WTSChannelGetIdByHandle(enumerator->enumerator_channel); + + IFCALLRET(context->ChannelIdAssigned, status, context, channelId); + if (!status) + { + WLog_ERR(TAG, "context->ChannelIdAssigned failed!"); + return ERROR_INTERNAL_ERROR; + } + + return Error; +} + +static UINT enumerator_server_handle_select_version_request(CamDevEnumServerContext* context, + wStream* s, + const CAM_SHARED_MSG_HEADER* header) +{ + CAM_SELECT_VERSION_REQUEST pdu = { 0 }; + UINT error = CHANNEL_RC_OK; + + WINPR_ASSERT(context); + WINPR_ASSERT(header); + + pdu.Header = *header; + + IFCALLRET(context->SelectVersionRequest, error, context, &pdu); + if (error) + WLog_ERR(TAG, "context->SelectVersionRequest failed with error %" PRIu32 "", error); + + return error; +} + +static UINT enumerator_server_recv_device_added_notification(CamDevEnumServerContext* context, + wStream* s, + const CAM_SHARED_MSG_HEADER* header) +{ + CAM_DEVICE_ADDED_NOTIFICATION pdu; + UINT error = CHANNEL_RC_OK; + size_t remaining_length; + WCHAR* channel_name_start; + char* tmp; + size_t i; + + WINPR_ASSERT(context); + WINPR_ASSERT(header); + + pdu.Header = *header; + + /* + * RequiredLength 4: + * + * Nullterminator DeviceName (2), + * VirtualChannelName (>= 1), + * Nullterminator VirtualChannelName (1) + */ + if (!Stream_CheckAndLogRequiredLength(TAG, s, 4)) + return ERROR_NO_DATA; + + pdu.DeviceName = (WCHAR*)Stream_Pointer(s); + + remaining_length = Stream_GetRemainingLength(s); + channel_name_start = (WCHAR*)Stream_Pointer(s); + + /* Search for null terminator of DeviceName */ + for (i = 0; i < remaining_length; i += sizeof(WCHAR), ++channel_name_start) + { + if (*channel_name_start == L'\0') + break; + } + + if (*channel_name_start != L'\0') + { + WLog_ERR(TAG, "enumerator_server_recv_device_added_notification: Invalid DeviceName!"); + return ERROR_INVALID_DATA; + } + + pdu.VirtualChannelName = (char*)++channel_name_start; + ++i; + + if (i >= remaining_length || *pdu.VirtualChannelName == '\0') + { + WLog_ERR(TAG, + "enumerator_server_recv_device_added_notification: Invalid VirtualChannelName!"); + return ERROR_INVALID_DATA; + } + + tmp = pdu.VirtualChannelName; + for (; i < remaining_length; ++i, ++tmp) + { + if (*tmp == '\0') + break; + } + + if (*tmp != '\0') + { + WLog_ERR(TAG, + "enumerator_server_recv_device_added_notification: Invalid VirtualChannelName!"); + return ERROR_INVALID_DATA; + } + + IFCALLRET(context->DeviceAddedNotification, error, context, &pdu); + if (error) + WLog_ERR(TAG, "context->DeviceAddedNotification failed with error %" PRIu32 "", error); + + return error; +} + +static UINT enumerator_server_recv_device_removed_notification(CamDevEnumServerContext* context, + wStream* s, + const CAM_SHARED_MSG_HEADER* header) +{ + CAM_DEVICE_REMOVED_NOTIFICATION pdu; + UINT error = CHANNEL_RC_OK; + size_t remaining_length; + char* tmp; + size_t i; + + WINPR_ASSERT(context); + WINPR_ASSERT(header); + + pdu.Header = *header; + + if (!Stream_CheckAndLogRequiredLength(TAG, s, 2)) + return ERROR_NO_DATA; + + pdu.VirtualChannelName = (char*)Stream_Pointer(s); + + remaining_length = Stream_GetRemainingLength(s); + tmp = (char*)(Stream_Pointer(s) + 1); + + for (i = 1; i < remaining_length; ++i, ++tmp) + { + if (*tmp == '\0') + break; + } + + if (*tmp != '\0') + { + WLog_ERR(TAG, + "enumerator_server_recv_device_removed_notification: Invalid VirtualChannelName!"); + return ERROR_INVALID_DATA; + } + + IFCALLRET(context->DeviceRemovedNotification, error, context, &pdu); + if (error) + WLog_ERR(TAG, "context->DeviceRemovedNotification failed with error %" PRIu32 "", error); + + return error; +} + +static UINT enumerator_process_message(enumerator_server* enumerator) +{ + BOOL rc; + UINT error = ERROR_INTERNAL_ERROR; + ULONG BytesReturned; + CAM_SHARED_MSG_HEADER header = { 0 }; + wStream* s; + + WINPR_ASSERT(enumerator); + WINPR_ASSERT(enumerator->enumerator_channel); + + s = enumerator->buffer; + WINPR_ASSERT(s); + + Stream_SetPosition(s, 0); + rc = WTSVirtualChannelRead(enumerator->enumerator_channel, 0, NULL, 0, &BytesReturned); + if (!rc) + goto out; + + if (BytesReturned < 1) + { + error = CHANNEL_RC_OK; + goto out; + } + + if (!Stream_EnsureRemainingCapacity(s, BytesReturned)) + { + WLog_ERR(TAG, "Stream_EnsureRemainingCapacity failed!"); + error = CHANNEL_RC_NO_MEMORY; + goto out; + } + + if (WTSVirtualChannelRead(enumerator->enumerator_channel, 0, (PCHAR)Stream_Buffer(s), + (ULONG)Stream_Capacity(s), &BytesReturned) == FALSE) + { + WLog_ERR(TAG, "WTSVirtualChannelRead failed!"); + goto out; + } + + Stream_SetLength(s, BytesReturned); + if (!Stream_CheckAndLogRequiredLength(TAG, s, CAM_HEADER_SIZE)) + return ERROR_NO_DATA; + + Stream_Read_UINT8(s, header.Version); + Stream_Read_UINT8(s, header.MessageId); + + switch (header.MessageId) + { + case CAM_MSG_ID_SelectVersionRequest: + error = + enumerator_server_handle_select_version_request(&enumerator->context, s, &header); + break; + case CAM_MSG_ID_DeviceAddedNotification: + error = + enumerator_server_recv_device_added_notification(&enumerator->context, s, &header); + break; + case CAM_MSG_ID_DeviceRemovedNotification: + error = enumerator_server_recv_device_removed_notification(&enumerator->context, s, + &header); + break; + default: + WLog_ERR(TAG, "enumerator_process_message: unknown or invalid MessageId %" PRIu8 "", + header.MessageId); + break; + } + +out: + if (error) + WLog_ERR(TAG, "Response failed with error %" PRIu32 "!", error); + + return error; +} + +static UINT enumerator_server_context_poll_int(CamDevEnumServerContext* context) +{ + enumerator_server* enumerator = (enumerator_server*)context; + UINT error = ERROR_INTERNAL_ERROR; + + WINPR_ASSERT(enumerator); + + switch (enumerator->state) + { + case ENUMERATOR_INITIAL: + error = enumerator_server_open_channel(enumerator); + if (error) + WLog_ERR(TAG, "enumerator_server_open_channel failed with error %" PRIu32 "!", + error); + else + enumerator->state = ENUMERATOR_OPENED; + break; + case ENUMERATOR_OPENED: + error = enumerator_process_message(enumerator); + break; + } + + return error; +} + +static HANDLE enumerator_server_get_channel_handle(enumerator_server* enumerator) +{ + void* buffer = NULL; + DWORD BytesReturned = 0; + HANDLE ChannelEvent = NULL; + + WINPR_ASSERT(enumerator); + + if (WTSVirtualChannelQuery(enumerator->enumerator_channel, WTSVirtualEventHandle, &buffer, + &BytesReturned) == TRUE) + { + if (BytesReturned == sizeof(HANDLE)) + CopyMemory(&ChannelEvent, buffer, sizeof(HANDLE)); + + WTSFreeMemory(buffer); + } + + return ChannelEvent; +} + +static DWORD WINAPI enumerator_server_thread_func(LPVOID arg) +{ + DWORD nCount; + HANDLE events[2] = { 0 }; + enumerator_server* enumerator = (enumerator_server*)arg; + UINT error = CHANNEL_RC_OK; + DWORD status; + + WINPR_ASSERT(enumerator); + + nCount = 0; + events[nCount++] = enumerator->stopEvent; + + while ((error == CHANNEL_RC_OK) && (WaitForSingleObject(events[0], 0) != WAIT_OBJECT_0)) + { + switch (enumerator->state) + { + case ENUMERATOR_INITIAL: + error = enumerator_server_context_poll_int(&enumerator->context); + if (error == CHANNEL_RC_OK) + { + events[1] = enumerator_server_get_channel_handle(enumerator); + nCount = 2; + } + break; + case ENUMERATOR_OPENED: + status = WaitForMultipleObjects(nCount, events, FALSE, INFINITE); + switch (status) + { + case WAIT_OBJECT_0: + break; + case WAIT_OBJECT_0 + 1: + case WAIT_TIMEOUT: + error = enumerator_server_context_poll_int(&enumerator->context); + break; + + case WAIT_FAILED: + default: + error = ERROR_INTERNAL_ERROR; + break; + } + break; + } + } + + WTSVirtualChannelClose(enumerator->enumerator_channel); + enumerator->enumerator_channel = NULL; + + if (error && enumerator->context.rdpcontext) + setChannelError(enumerator->context.rdpcontext, error, + "enumerator_server_thread_func reported an error"); + + ExitThread(error); + return error; +} + +static UINT enumerator_server_open(CamDevEnumServerContext* context) +{ + enumerator_server* enumerator = (enumerator_server*)context; + + WINPR_ASSERT(enumerator); + + if (!enumerator->externalThread && (enumerator->thread == NULL)) + { + enumerator->stopEvent = CreateEvent(NULL, TRUE, FALSE, NULL); + if (!enumerator->stopEvent) + { + WLog_ERR(TAG, "CreateEvent failed!"); + return ERROR_INTERNAL_ERROR; + } + + enumerator->thread = + CreateThread(NULL, 0, enumerator_server_thread_func, enumerator, 0, NULL); + if (!enumerator->thread) + { + WLog_ERR(TAG, "CreateThread failed!"); + CloseHandle(enumerator->stopEvent); + enumerator->stopEvent = NULL; + return ERROR_INTERNAL_ERROR; + } + } + enumerator->isOpened = TRUE; + + return CHANNEL_RC_OK; +} + +static UINT enumerator_server_close(CamDevEnumServerContext* context) +{ + UINT error = CHANNEL_RC_OK; + enumerator_server* enumerator = (enumerator_server*)context; + + WINPR_ASSERT(enumerator); + + if (!enumerator->externalThread && enumerator->thread) + { + SetEvent(enumerator->stopEvent); + + if (WaitForSingleObject(enumerator->thread, INFINITE) == WAIT_FAILED) + { + error = GetLastError(); + WLog_ERR(TAG, "WaitForSingleObject failed with error %" PRIu32 "", error); + return error; + } + + CloseHandle(enumerator->thread); + CloseHandle(enumerator->stopEvent); + enumerator->thread = NULL; + enumerator->stopEvent = NULL; + } + if (enumerator->externalThread) + { + if (enumerator->state != ENUMERATOR_INITIAL) + { + WTSVirtualChannelClose(enumerator->enumerator_channel); + enumerator->enumerator_channel = NULL; + enumerator->state = ENUMERATOR_INITIAL; + } + } + enumerator->isOpened = FALSE; + + return error; +} + +static UINT enumerator_server_context_poll(CamDevEnumServerContext* context) +{ + enumerator_server* enumerator = (enumerator_server*)context; + + WINPR_ASSERT(enumerator); + + if (!enumerator->externalThread) + return ERROR_INTERNAL_ERROR; + + return enumerator_server_context_poll_int(context); +} + +static BOOL enumerator_server_context_handle(CamDevEnumServerContext* context, HANDLE* handle) +{ + enumerator_server* enumerator = (enumerator_server*)context; + + WINPR_ASSERT(enumerator); + WINPR_ASSERT(handle); + + if (!enumerator->externalThread) + return FALSE; + if (enumerator->state == ENUMERATOR_INITIAL) + return FALSE; + + *handle = enumerator_server_get_channel_handle(enumerator); + + return TRUE; +} + +static UINT enumerator_server_packet_send(CamDevEnumServerContext* context, wStream* s) +{ + enumerator_server* enumerator = (enumerator_server*)context; + UINT error = CHANNEL_RC_OK; + ULONG written; + + if (!WTSVirtualChannelWrite(enumerator->enumerator_channel, (PCHAR)Stream_Buffer(s), + Stream_GetPosition(s), &written)) + { + WLog_ERR(TAG, "WTSVirtualChannelWrite failed!"); + error = ERROR_INTERNAL_ERROR; + goto out; + } + + if (written < Stream_GetPosition(s)) + { + WLog_WARN(TAG, "Unexpected bytes written: %" PRIu32 "/%" PRIuz "", written, + Stream_GetPosition(s)); + } + +out: + Stream_Free(s, TRUE); + return error; +} + +static UINT enumerator_send_select_version_response_pdu( + CamDevEnumServerContext* context, const CAM_SELECT_VERSION_RESPONSE* selectVersionResponse) +{ + wStream* s; + + s = Stream_New(NULL, CAM_HEADER_SIZE); + if (!s) + { + WLog_ERR(TAG, "Stream_New failed!"); + return ERROR_NOT_ENOUGH_MEMORY; + } + + Stream_Write_UINT8(s, selectVersionResponse->Header.Version); + Stream_Write_UINT8(s, selectVersionResponse->Header.MessageId); + + return enumerator_server_packet_send(context, s); +} + +CamDevEnumServerContext* cam_dev_enum_server_context_new(HANDLE vcm) +{ + enumerator_server* enumerator = (enumerator_server*)calloc(1, sizeof(enumerator_server)); + + if (!enumerator) + return NULL; + + enumerator->context.vcm = vcm; + enumerator->context.Initialize = enumerator_server_initialize; + enumerator->context.Open = enumerator_server_open; + enumerator->context.Close = enumerator_server_close; + enumerator->context.Poll = enumerator_server_context_poll; + enumerator->context.ChannelHandle = enumerator_server_context_handle; + + enumerator->context.SelectVersionResponse = enumerator_send_select_version_response_pdu; + + enumerator->buffer = Stream_New(NULL, 4096); + if (!enumerator->buffer) + goto fail; + + return &enumerator->context; +fail: + cam_dev_enum_server_context_free(&enumerator->context); + return NULL; +} + +void cam_dev_enum_server_context_free(CamDevEnumServerContext* context) +{ + enumerator_server* enumerator = (enumerator_server*)context; + + if (enumerator) + { + enumerator_server_close(context); + Stream_Free(enumerator->buffer, TRUE); + } + + free(enumerator); +} diff --git a/channels/rdpecam/server/camera_device_main.c b/channels/rdpecam/server/camera_device_main.c new file mode 100644 index 0000000..00bea94 --- /dev/null +++ b/channels/rdpecam/server/camera_device_main.c @@ -0,0 +1,971 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Video Capture Virtual Channel Extension + * + * Copyright 2022 Pascal Nowack + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include + +#define TAG CHANNELS_TAG("rdpecam.server") + +typedef enum +{ + CAMERA_DEVICE_INITIAL, + CAMERA_DEVICE_OPENED, +} eCameraDeviceChannelState; + +typedef struct +{ + CameraDeviceServerContext context; + + HANDLE stopEvent; + + HANDLE thread; + void* device_channel; + + DWORD SessionId; + + BOOL isOpened; + BOOL externalThread; + + /* Channel state */ + eCameraDeviceChannelState state; + + wStream* buffer; +} device_server; + +static UINT device_server_initialize(CameraDeviceServerContext* context, BOOL externalThread) +{ + UINT error = CHANNEL_RC_OK; + device_server* device = (device_server*)context; + + WINPR_ASSERT(device); + + if (device->isOpened) + { + WLog_WARN(TAG, "Application error: Camera channel already initialized, " + "calling in this state is not possible!"); + return ERROR_INVALID_STATE; + } + + device->externalThread = externalThread; + + return error; +} + +static UINT device_server_open_channel(device_server* device) +{ + CameraDeviceServerContext* context = &device->context; + DWORD Error = ERROR_SUCCESS; + HANDLE hEvent; + DWORD BytesReturned = 0; + PULONG pSessionId = NULL; + UINT32 channelId; + BOOL status = TRUE; + + WINPR_ASSERT(device); + + if (WTSQuerySessionInformationA(device->context.vcm, WTS_CURRENT_SESSION, WTSSessionId, + (LPSTR*)&pSessionId, &BytesReturned) == FALSE) + { + WLog_ERR(TAG, "WTSQuerySessionInformationA failed!"); + return ERROR_INTERNAL_ERROR; + } + + device->SessionId = (DWORD)*pSessionId; + WTSFreeMemory(pSessionId); + hEvent = WTSVirtualChannelManagerGetEventHandle(device->context.vcm); + + if (WaitForSingleObject(hEvent, 1000) == WAIT_FAILED) + { + Error = GetLastError(); + WLog_ERR(TAG, "WaitForSingleObject failed with error %" PRIu32 "!", Error); + return Error; + } + + device->device_channel = WTSVirtualChannelOpenEx(device->SessionId, context->virtualChannelName, + WTS_CHANNEL_OPTION_DYNAMIC); + if (!device->device_channel) + { + Error = GetLastError(); + WLog_ERR(TAG, "WTSVirtualChannelOpenEx failed with error %" PRIu32 "!", Error); + return Error; + } + + channelId = WTSChannelGetIdByHandle(device->device_channel); + + IFCALLRET(context->ChannelIdAssigned, status, context, channelId); + if (!status) + { + WLog_ERR(TAG, "context->ChannelIdAssigned failed!"); + return ERROR_INTERNAL_ERROR; + } + + return Error; +} + +static UINT device_server_handle_success_response(CameraDeviceServerContext* context, wStream* s, + const CAM_SHARED_MSG_HEADER* header) +{ + CAM_SUCCESS_RESPONSE pdu = { 0 }; + UINT error = CHANNEL_RC_OK; + + WINPR_ASSERT(context); + WINPR_ASSERT(header); + + pdu.Header = *header; + + IFCALLRET(context->SuccessResponse, error, context, &pdu); + if (error) + WLog_ERR(TAG, "context->SuccessResponse failed with error %" PRIu32 "", error); + + return error; +} + +static UINT device_server_recv_error_response(CameraDeviceServerContext* context, wStream* s, + const CAM_SHARED_MSG_HEADER* header) +{ + CAM_ERROR_RESPONSE pdu = { 0 }; + UINT error = CHANNEL_RC_OK; + + WINPR_ASSERT(context); + WINPR_ASSERT(header); + + pdu.Header = *header; + + if (!Stream_CheckAndLogRequiredLength(TAG, s, 4)) + return ERROR_NO_DATA; + + Stream_Read_UINT32(s, pdu.ErrorCode); + + IFCALLRET(context->ErrorResponse, error, context, &pdu); + if (error) + WLog_ERR(TAG, "context->ErrorResponse failed with error %" PRIu32 "", error); + + return error; +} + +static UINT device_server_recv_stream_list_response(CameraDeviceServerContext* context, wStream* s, + const CAM_SHARED_MSG_HEADER* header) +{ + CAM_STREAM_LIST_RESPONSE pdu = { 0 }; + UINT error = CHANNEL_RC_OK; + BYTE i; + + WINPR_ASSERT(context); + WINPR_ASSERT(header); + + pdu.Header = *header; + + if (!Stream_CheckAndLogRequiredLength(TAG, s, 5)) + return ERROR_NO_DATA; + + pdu.N_Descriptions = MIN(Stream_GetRemainingLength(s) / 5, 255); + + for (i = 0; i < pdu.N_Descriptions; ++i) + { + CAM_STREAM_DESCRIPTION* StreamDescription = &pdu.StreamDescriptions[i]; + + Stream_Read_UINT16(s, StreamDescription->FrameSourceTypes); + Stream_Read_UINT8(s, StreamDescription->StreamCategory); + Stream_Read_UINT8(s, StreamDescription->Selected); + Stream_Read_UINT8(s, StreamDescription->CanBeShared); + } + + IFCALLRET(context->StreamListResponse, error, context, &pdu); + if (error) + WLog_ERR(TAG, "context->StreamListResponse failed with error %" PRIu32 "", error); + + return error; +} + +static UINT device_server_recv_media_type_list_response(CameraDeviceServerContext* context, + wStream* s, + const CAM_SHARED_MSG_HEADER* header) +{ + CAM_MEDIA_TYPE_LIST_RESPONSE pdu = { 0 }; + UINT error = CHANNEL_RC_OK; + BYTE i; + + WINPR_ASSERT(context); + WINPR_ASSERT(header); + + pdu.Header = *header; + + if (!Stream_CheckAndLogRequiredLength(TAG, s, 26)) + return ERROR_NO_DATA; + + pdu.N_Descriptions = Stream_GetRemainingLength(s) / 26; + + pdu.MediaTypeDescriptions = calloc(pdu.N_Descriptions, sizeof(CAM_MEDIA_TYPE_DESCRIPTION)); + if (!pdu.MediaTypeDescriptions) + { + WLog_ERR(TAG, "Failed to allocate %zu CAM_MEDIA_TYPE_DESCRIPTION structs", + pdu.N_Descriptions); + return ERROR_NOT_ENOUGH_MEMORY; + } + + for (i = 0; i < pdu.N_Descriptions; ++i) + { + CAM_MEDIA_TYPE_DESCRIPTION* MediaTypeDescriptions = &pdu.MediaTypeDescriptions[i]; + + Stream_Read_UINT8(s, MediaTypeDescriptions->Format); + Stream_Read_UINT32(s, MediaTypeDescriptions->Width); + Stream_Read_UINT32(s, MediaTypeDescriptions->Height); + Stream_Read_UINT32(s, MediaTypeDescriptions->FrameRateNumerator); + Stream_Read_UINT32(s, MediaTypeDescriptions->FrameRateDenominator); + Stream_Read_UINT32(s, MediaTypeDescriptions->PixelAspectRatioNumerator); + Stream_Read_UINT32(s, MediaTypeDescriptions->PixelAspectRatioDenominator); + Stream_Read_UINT8(s, MediaTypeDescriptions->Flags); + } + + IFCALLRET(context->MediaTypeListResponse, error, context, &pdu); + if (error) + WLog_ERR(TAG, "context->MediaTypeListResponse failed with error %" PRIu32 "", error); + + free(pdu.MediaTypeDescriptions); + + return error; +} + +static UINT device_server_recv_current_media_type_response(CameraDeviceServerContext* context, + wStream* s, + const CAM_SHARED_MSG_HEADER* header) +{ + CAM_CURRENT_MEDIA_TYPE_RESPONSE pdu = { 0 }; + UINT error = CHANNEL_RC_OK; + + WINPR_ASSERT(context); + WINPR_ASSERT(header); + + pdu.Header = *header; + + if (!Stream_CheckAndLogRequiredLength(TAG, s, 26)) + return ERROR_NO_DATA; + + Stream_Read_UINT8(s, pdu.MediaTypeDescription.Format); + Stream_Read_UINT32(s, pdu.MediaTypeDescription.Width); + Stream_Read_UINT32(s, pdu.MediaTypeDescription.Height); + Stream_Read_UINT32(s, pdu.MediaTypeDescription.FrameRateNumerator); + Stream_Read_UINT32(s, pdu.MediaTypeDescription.FrameRateDenominator); + Stream_Read_UINT32(s, pdu.MediaTypeDescription.PixelAspectRatioNumerator); + Stream_Read_UINT32(s, pdu.MediaTypeDescription.PixelAspectRatioDenominator); + Stream_Read_UINT8(s, pdu.MediaTypeDescription.Flags); + + IFCALLRET(context->CurrentMediaTypeResponse, error, context, &pdu); + if (error) + WLog_ERR(TAG, "context->CurrentMediaTypeResponse failed with error %" PRIu32 "", error); + + return error; +} + +static UINT device_server_recv_sample_response(CameraDeviceServerContext* context, wStream* s, + const CAM_SHARED_MSG_HEADER* header) +{ + CAM_SAMPLE_RESPONSE pdu = { 0 }; + UINT error = CHANNEL_RC_OK; + + WINPR_ASSERT(context); + WINPR_ASSERT(header); + + pdu.Header = *header; + + if (!Stream_CheckAndLogRequiredLength(TAG, s, 1)) + return ERROR_NO_DATA; + + Stream_Read_UINT8(s, pdu.StreamIndex); + + pdu.SampleSize = Stream_GetRemainingLength(s); + pdu.Sample = Stream_Pointer(s); + + IFCALLRET(context->SampleResponse, error, context, &pdu); + if (error) + WLog_ERR(TAG, "context->SampleResponse failed with error %" PRIu32 "", error); + + return error; +} + +static UINT device_server_recv_sample_error_response(CameraDeviceServerContext* context, wStream* s, + const CAM_SHARED_MSG_HEADER* header) +{ + CAM_SAMPLE_ERROR_RESPONSE pdu = { 0 }; + UINT error = CHANNEL_RC_OK; + + WINPR_ASSERT(context); + WINPR_ASSERT(header); + + pdu.Header = *header; + + if (!Stream_CheckAndLogRequiredLength(TAG, s, 5)) + return ERROR_NO_DATA; + + Stream_Read_UINT8(s, pdu.StreamIndex); + Stream_Read_UINT32(s, pdu.ErrorCode); + + IFCALLRET(context->SampleErrorResponse, error, context, &pdu); + if (error) + WLog_ERR(TAG, "context->SampleErrorResponse failed with error %" PRIu32 "", error); + + return error; +} + +static UINT device_server_recv_property_list_response(CameraDeviceServerContext* context, + wStream* s, + const CAM_SHARED_MSG_HEADER* header) +{ + CAM_PROPERTY_LIST_RESPONSE pdu = { 0 }; + UINT error = CHANNEL_RC_OK; + + WINPR_ASSERT(context); + WINPR_ASSERT(header); + + pdu.Header = *header; + + pdu.N_Properties = Stream_GetRemainingLength(s) / 19; + + if (pdu.N_Properties > 0) + { + size_t i; + + pdu.Properties = calloc(pdu.N_Properties, sizeof(CAM_PROPERTY_DESCRIPTION)); + if (!pdu.Properties) + { + WLog_ERR(TAG, "Failed to allocate %zu CAM_PROPERTY_DESCRIPTION structs", + pdu.N_Properties); + return ERROR_NOT_ENOUGH_MEMORY; + } + + for (i = 0; i < pdu.N_Properties; ++i) + { + Stream_Read_UINT8(s, pdu.Properties[i].PropertySet); + Stream_Read_UINT8(s, pdu.Properties[i].PropertyId); + Stream_Read_UINT8(s, pdu.Properties[i].Capabilities); + Stream_Read_INT32(s, pdu.Properties[i].MinValue); + Stream_Read_INT32(s, pdu.Properties[i].MaxValue); + Stream_Read_INT32(s, pdu.Properties[i].Step); + Stream_Read_INT32(s, pdu.Properties[i].DefaultValue); + } + } + + IFCALLRET(context->PropertyListResponse, error, context, &pdu); + if (error) + WLog_ERR(TAG, "context->PropertyListResponse failed with error %" PRIu32 "", error); + + free(pdu.Properties); + + return error; +} + +static UINT device_server_recv_property_value_response(CameraDeviceServerContext* context, + wStream* s, + const CAM_SHARED_MSG_HEADER* header) +{ + CAM_PROPERTY_VALUE_RESPONSE pdu = { 0 }; + UINT error = CHANNEL_RC_OK; + + WINPR_ASSERT(context); + WINPR_ASSERT(header); + + pdu.Header = *header; + + if (!Stream_CheckAndLogRequiredLength(TAG, s, 5)) + return ERROR_NO_DATA; + + Stream_Read_UINT8(s, pdu.PropertyValue.Mode); + Stream_Read_INT32(s, pdu.PropertyValue.Value); + + IFCALLRET(context->PropertyValueResponse, error, context, &pdu); + if (error) + WLog_ERR(TAG, "context->PropertyValueResponse failed with error %" PRIu32 "", error); + + return error; +} + +static UINT device_process_message(device_server* device) +{ + BOOL rc; + UINT error = ERROR_INTERNAL_ERROR; + ULONG BytesReturned; + CAM_SHARED_MSG_HEADER header = { 0 }; + wStream* s; + + WINPR_ASSERT(device); + WINPR_ASSERT(device->device_channel); + + s = device->buffer; + WINPR_ASSERT(s); + + Stream_SetPosition(s, 0); + rc = WTSVirtualChannelRead(device->device_channel, 0, NULL, 0, &BytesReturned); + if (!rc) + goto out; + + if (BytesReturned < 1) + { + error = CHANNEL_RC_OK; + goto out; + } + + if (!Stream_EnsureRemainingCapacity(s, BytesReturned)) + { + WLog_ERR(TAG, "Stream_EnsureRemainingCapacity failed!"); + error = CHANNEL_RC_NO_MEMORY; + goto out; + } + + if (WTSVirtualChannelRead(device->device_channel, 0, (PCHAR)Stream_Buffer(s), + (ULONG)Stream_Capacity(s), &BytesReturned) == FALSE) + { + WLog_ERR(TAG, "WTSVirtualChannelRead failed!"); + goto out; + } + + Stream_SetLength(s, BytesReturned); + if (!Stream_CheckAndLogRequiredLength(TAG, s, CAM_HEADER_SIZE)) + return ERROR_NO_DATA; + + Stream_Read_UINT8(s, header.Version); + Stream_Read_UINT8(s, header.MessageId); + + switch (header.MessageId) + { + case CAM_MSG_ID_SuccessResponse: + error = device_server_handle_success_response(&device->context, s, &header); + break; + case CAM_MSG_ID_ErrorResponse: + error = device_server_recv_error_response(&device->context, s, &header); + break; + case CAM_MSG_ID_StreamListResponse: + error = device_server_recv_stream_list_response(&device->context, s, &header); + break; + case CAM_MSG_ID_MediaTypeListResponse: + error = device_server_recv_media_type_list_response(&device->context, s, &header); + break; + case CAM_MSG_ID_CurrentMediaTypeResponse: + error = device_server_recv_current_media_type_response(&device->context, s, &header); + break; + case CAM_MSG_ID_SampleResponse: + error = device_server_recv_sample_response(&device->context, s, &header); + break; + case CAM_MSG_ID_SampleErrorResponse: + error = device_server_recv_sample_error_response(&device->context, s, &header); + break; + case CAM_MSG_ID_PropertyListResponse: + error = device_server_recv_property_list_response(&device->context, s, &header); + break; + case CAM_MSG_ID_PropertyValueResponse: + error = device_server_recv_property_value_response(&device->context, s, &header); + break; + default: + WLog_ERR(TAG, "device_process_message: unknown or invalid MessageId %" PRIu8 "", + header.MessageId); + break; + } + +out: + if (error) + WLog_ERR(TAG, "Response failed with error %" PRIu32 "!", error); + + return error; +} + +static UINT device_server_context_poll_int(CameraDeviceServerContext* context) +{ + device_server* device = (device_server*)context; + UINT error = ERROR_INTERNAL_ERROR; + + WINPR_ASSERT(device); + + switch (device->state) + { + case CAMERA_DEVICE_INITIAL: + error = device_server_open_channel(device); + if (error) + WLog_ERR(TAG, "device_server_open_channel failed with error %" PRIu32 "!", error); + else + device->state = CAMERA_DEVICE_OPENED; + break; + case CAMERA_DEVICE_OPENED: + error = device_process_message(device); + break; + } + + return error; +} + +static HANDLE device_server_get_channel_handle(device_server* device) +{ + void* buffer = NULL; + DWORD BytesReturned = 0; + HANDLE ChannelEvent = NULL; + + WINPR_ASSERT(device); + + if (WTSVirtualChannelQuery(device->device_channel, WTSVirtualEventHandle, &buffer, + &BytesReturned) == TRUE) + { + if (BytesReturned == sizeof(HANDLE)) + CopyMemory(&ChannelEvent, buffer, sizeof(HANDLE)); + + WTSFreeMemory(buffer); + } + + return ChannelEvent; +} + +static DWORD WINAPI device_server_thread_func(LPVOID arg) +{ + DWORD nCount; + HANDLE events[2] = { 0 }; + device_server* device = (device_server*)arg; + UINT error = CHANNEL_RC_OK; + DWORD status; + + WINPR_ASSERT(device); + + nCount = 0; + events[nCount++] = device->stopEvent; + + while ((error == CHANNEL_RC_OK) && (WaitForSingleObject(events[0], 0) != WAIT_OBJECT_0)) + { + switch (device->state) + { + case CAMERA_DEVICE_INITIAL: + error = device_server_context_poll_int(&device->context); + if (error == CHANNEL_RC_OK) + { + events[1] = device_server_get_channel_handle(device); + nCount = 2; + } + break; + case CAMERA_DEVICE_OPENED: + status = WaitForMultipleObjects(nCount, events, FALSE, INFINITE); + switch (status) + { + case WAIT_OBJECT_0: + break; + case WAIT_OBJECT_0 + 1: + case WAIT_TIMEOUT: + error = device_server_context_poll_int(&device->context); + break; + + case WAIT_FAILED: + default: + error = ERROR_INTERNAL_ERROR; + break; + } + break; + } + } + + WTSVirtualChannelClose(device->device_channel); + device->device_channel = NULL; + + if (error && device->context.rdpcontext) + setChannelError(device->context.rdpcontext, error, + "device_server_thread_func reported an error"); + + ExitThread(error); + return error; +} + +static UINT device_server_open(CameraDeviceServerContext* context) +{ + device_server* device = (device_server*)context; + + WINPR_ASSERT(device); + + if (!device->externalThread && (device->thread == NULL)) + { + device->stopEvent = CreateEvent(NULL, TRUE, FALSE, NULL); + if (!device->stopEvent) + { + WLog_ERR(TAG, "CreateEvent failed!"); + return ERROR_INTERNAL_ERROR; + } + + device->thread = CreateThread(NULL, 0, device_server_thread_func, device, 0, NULL); + if (!device->thread) + { + WLog_ERR(TAG, "CreateThread failed!"); + CloseHandle(device->stopEvent); + device->stopEvent = NULL; + return ERROR_INTERNAL_ERROR; + } + } + device->isOpened = TRUE; + + return CHANNEL_RC_OK; +} + +static UINT device_server_close(CameraDeviceServerContext* context) +{ + UINT error = CHANNEL_RC_OK; + device_server* device = (device_server*)context; + + WINPR_ASSERT(device); + + if (!device->externalThread && device->thread) + { + SetEvent(device->stopEvent); + + if (WaitForSingleObject(device->thread, INFINITE) == WAIT_FAILED) + { + error = GetLastError(); + WLog_ERR(TAG, "WaitForSingleObject failed with error %" PRIu32 "", error); + return error; + } + + CloseHandle(device->thread); + CloseHandle(device->stopEvent); + device->thread = NULL; + device->stopEvent = NULL; + } + if (device->externalThread) + { + if (device->state != CAMERA_DEVICE_INITIAL) + { + WTSVirtualChannelClose(device->device_channel); + device->device_channel = NULL; + device->state = CAMERA_DEVICE_INITIAL; + } + } + device->isOpened = FALSE; + + return error; +} + +static UINT device_server_context_poll(CameraDeviceServerContext* context) +{ + device_server* device = (device_server*)context; + + WINPR_ASSERT(device); + + if (!device->externalThread) + return ERROR_INTERNAL_ERROR; + + return device_server_context_poll_int(context); +} + +static BOOL device_server_context_handle(CameraDeviceServerContext* context, HANDLE* handle) +{ + device_server* device = (device_server*)context; + + WINPR_ASSERT(device); + WINPR_ASSERT(handle); + + if (!device->externalThread) + return FALSE; + if (device->state == CAMERA_DEVICE_INITIAL) + return FALSE; + + *handle = device_server_get_channel_handle(device); + + return TRUE; +} + +static wStream* device_server_packet_new(size_t size, BYTE version, BYTE messageId) +{ + wStream* s; + + WINPR_ASSERT(size > 0); + + /* Allocate what we need plus header bytes */ + s = Stream_New(NULL, size + CAM_HEADER_SIZE); + if (!s) + { + WLog_ERR(TAG, "Stream_New failed!"); + return NULL; + } + + Stream_Write_UINT8(s, version); + Stream_Write_UINT8(s, messageId); + + return s; +} + +static UINT device_server_packet_send(CameraDeviceServerContext* context, wStream* s) +{ + device_server* device = (device_server*)context; + UINT error = CHANNEL_RC_OK; + ULONG written; + + WINPR_ASSERT(context); + WINPR_ASSERT(s); + + if (!WTSVirtualChannelWrite(device->device_channel, (PCHAR)Stream_Buffer(s), + Stream_GetPosition(s), &written)) + { + WLog_ERR(TAG, "WTSVirtualChannelWrite failed!"); + error = ERROR_INTERNAL_ERROR; + goto out; + } + + if (written < Stream_GetPosition(s)) + { + WLog_WARN(TAG, "Unexpected bytes written: %" PRIu32 "/%" PRIuz "", written, + Stream_GetPosition(s)); + } + +out: + Stream_Free(s, TRUE); + return error; +} + +static UINT device_server_write_and_send_header(CameraDeviceServerContext* context, BYTE messageId) +{ + wStream* s; + + WINPR_ASSERT(context); + + s = device_server_packet_new(0, context->protocolVersion, messageId); + if (!s) + return ERROR_NOT_ENOUGH_MEMORY; + + return device_server_packet_send(context, s); +} + +static UINT +device_send_activate_device_request_pdu(CameraDeviceServerContext* context, + const CAM_ACTIVATE_DEVICE_REQUEST* activateDeviceRequest) +{ + WINPR_ASSERT(context); + + return device_server_write_and_send_header(context, CAM_MSG_ID_ActivateDeviceRequest); +} + +static UINT device_send_deactivate_device_request_pdu( + CameraDeviceServerContext* context, + const CAM_DEACTIVATE_DEVICE_REQUEST* deactivateDeviceRequest) +{ + WINPR_ASSERT(context); + + return device_server_write_and_send_header(context, CAM_MSG_ID_DeactivateDeviceRequest); +} + +static UINT device_send_stream_list_request_pdu(CameraDeviceServerContext* context, + const CAM_STREAM_LIST_REQUEST* streamListRequest) +{ + WINPR_ASSERT(context); + + return device_server_write_and_send_header(context, CAM_MSG_ID_StreamListRequest); +} + +static UINT +device_send_media_type_list_request_pdu(CameraDeviceServerContext* context, + const CAM_MEDIA_TYPE_LIST_REQUEST* mediaTypeListRequest) +{ + wStream* s; + + WINPR_ASSERT(context); + WINPR_ASSERT(mediaTypeListRequest); + + s = device_server_packet_new(1, context->protocolVersion, CAM_MSG_ID_MediaTypeListRequest); + if (!s) + return ERROR_NOT_ENOUGH_MEMORY; + + Stream_Write_UINT8(s, mediaTypeListRequest->StreamIndex); + + return device_server_packet_send(context, s); +} + +static UINT device_send_current_media_type_request_pdu( + CameraDeviceServerContext* context, + const CAM_CURRENT_MEDIA_TYPE_REQUEST* currentMediaTypeRequest) +{ + wStream* s; + + WINPR_ASSERT(context); + WINPR_ASSERT(currentMediaTypeRequest); + + s = device_server_packet_new(1, context->protocolVersion, CAM_MSG_ID_CurrentMediaTypeRequest); + if (!s) + return ERROR_NOT_ENOUGH_MEMORY; + + Stream_Write_UINT8(s, currentMediaTypeRequest->StreamIndex); + + return device_server_packet_send(context, s); +} + +static UINT +device_send_start_streams_request_pdu(CameraDeviceServerContext* context, + const CAM_START_STREAMS_REQUEST* startStreamsRequest) +{ + wStream* s; + size_t i; + + WINPR_ASSERT(context); + WINPR_ASSERT(startStreamsRequest); + + s = device_server_packet_new(startStreamsRequest->N_Infos * 27ul, context->protocolVersion, + CAM_MSG_ID_StartStreamsRequest); + if (!s) + return ERROR_NOT_ENOUGH_MEMORY; + + for (i = 0; i < startStreamsRequest->N_Infos; ++i) + { + const CAM_START_STREAM_INFO* info = &startStreamsRequest->StartStreamsInfo[i]; + const CAM_MEDIA_TYPE_DESCRIPTION* description = &info->MediaTypeDescription; + + Stream_Write_UINT8(s, info->StreamIndex); + + Stream_Write_UINT8(s, description->Format); + Stream_Write_UINT32(s, description->Width); + Stream_Write_UINT32(s, description->Height); + Stream_Write_UINT32(s, description->FrameRateNumerator); + Stream_Write_UINT32(s, description->FrameRateDenominator); + Stream_Write_UINT32(s, description->PixelAspectRatioNumerator); + Stream_Write_UINT32(s, description->PixelAspectRatioDenominator); + Stream_Write_UINT8(s, description->Flags); + } + + return device_server_packet_send(context, s); +} + +static UINT device_send_stop_streams_request_pdu(CameraDeviceServerContext* context, + const CAM_STOP_STREAMS_REQUEST* stopStreamsRequest) +{ + WINPR_ASSERT(context); + + return device_server_write_and_send_header(context, CAM_MSG_ID_StopStreamsRequest); +} + +static UINT device_send_sample_request_pdu(CameraDeviceServerContext* context, + const CAM_SAMPLE_REQUEST* sampleRequest) +{ + wStream* s; + + WINPR_ASSERT(context); + WINPR_ASSERT(sampleRequest); + + s = device_server_packet_new(1, context->protocolVersion, CAM_MSG_ID_SampleRequest); + if (!s) + return ERROR_NOT_ENOUGH_MEMORY; + + Stream_Write_UINT8(s, sampleRequest->StreamIndex); + + return device_server_packet_send(context, s); +} + +static UINT +device_send_property_list_request_pdu(CameraDeviceServerContext* context, + const CAM_PROPERTY_LIST_REQUEST* propertyListRequest) +{ + WINPR_ASSERT(context); + + return device_server_write_and_send_header(context, CAM_MSG_ID_PropertyListRequest); +} + +static UINT +device_send_property_value_request_pdu(CameraDeviceServerContext* context, + const CAM_PROPERTY_VALUE_REQUEST* propertyValueRequest) +{ + wStream* s; + + WINPR_ASSERT(context); + WINPR_ASSERT(propertyValueRequest); + + s = device_server_packet_new(2, context->protocolVersion, CAM_MSG_ID_PropertyValueRequest); + if (!s) + return ERROR_NOT_ENOUGH_MEMORY; + + Stream_Write_UINT8(s, propertyValueRequest->PropertySet); + Stream_Write_UINT8(s, propertyValueRequest->PropertyId); + + return device_server_packet_send(context, s); +} + +static UINT device_send_set_property_value_request_pdu( + CameraDeviceServerContext* context, + const CAM_SET_PROPERTY_VALUE_REQUEST* setPropertyValueRequest) +{ + wStream* s; + + WINPR_ASSERT(context); + WINPR_ASSERT(setPropertyValueRequest); + + s = device_server_packet_new(2 + 5, context->protocolVersion, + CAM_MSG_ID_SetPropertyValueRequest); + if (!s) + return ERROR_NOT_ENOUGH_MEMORY; + + Stream_Write_UINT8(s, setPropertyValueRequest->PropertySet); + Stream_Write_UINT8(s, setPropertyValueRequest->PropertyId); + + Stream_Write_UINT8(s, setPropertyValueRequest->PropertyValue.Mode); + Stream_Write_INT32(s, setPropertyValueRequest->PropertyValue.Value); + + return device_server_packet_send(context, s); +} + +CameraDeviceServerContext* camera_device_server_context_new(HANDLE vcm) +{ + device_server* device = (device_server*)calloc(1, sizeof(device_server)); + + if (!device) + return NULL; + + device->context.vcm = vcm; + device->context.Initialize = device_server_initialize; + device->context.Open = device_server_open; + device->context.Close = device_server_close; + device->context.Poll = device_server_context_poll; + device->context.ChannelHandle = device_server_context_handle; + + device->context.ActivateDeviceRequest = device_send_activate_device_request_pdu; + device->context.DeactivateDeviceRequest = device_send_deactivate_device_request_pdu; + + device->context.StreamListRequest = device_send_stream_list_request_pdu; + device->context.MediaTypeListRequest = device_send_media_type_list_request_pdu; + device->context.CurrentMediaTypeRequest = device_send_current_media_type_request_pdu; + + device->context.StartStreamsRequest = device_send_start_streams_request_pdu; + device->context.StopStreamsRequest = device_send_stop_streams_request_pdu; + device->context.SampleRequest = device_send_sample_request_pdu; + + device->context.PropertyListRequest = device_send_property_list_request_pdu; + device->context.PropertyValueRequest = device_send_property_value_request_pdu; + device->context.SetPropertyValueRequest = device_send_set_property_value_request_pdu; + + device->buffer = Stream_New(NULL, 4096); + if (!device->buffer) + goto fail; + + return &device->context; +fail: + camera_device_server_context_free(&device->context); + return NULL; +} + +void camera_device_server_context_free(CameraDeviceServerContext* context) +{ + device_server* device = (device_server*)context; + + if (device) + { + device_server_close(context); + Stream_Free(device->buffer, TRUE); + } + + free(context->virtualChannelName); + + free(device); +} diff --git a/channels/rdpei/CMakeLists.txt b/channels/rdpei/CMakeLists.txt new file mode 100644 index 0000000..a93af67 --- /dev/null +++ b/channels/rdpei/CMakeLists.txt @@ -0,0 +1,26 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2012 Marc-Andre Moreau +# +# Licensed 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. + +define_channel("rdpei") + +if(WITH_CLIENT_CHANNELS) + add_channel_client(${MODULE_PREFIX} ${CHANNEL_NAME}) +endif() + +if(WITH_SERVER_CHANNELS) + add_channel_server(${MODULE_PREFIX} ${CHANNEL_NAME}) +endif() \ No newline at end of file diff --git a/channels/rdpei/ChannelOptions.cmake b/channels/rdpei/ChannelOptions.cmake new file mode 100644 index 0000000..d3f8743 --- /dev/null +++ b/channels/rdpei/ChannelOptions.cmake @@ -0,0 +1,13 @@ + +set(OPTION_DEFAULT OFF) +set(OPTION_CLIENT_DEFAULT ON) +set(OPTION_SERVER_DEFAULT OFF) + +define_channel_options(NAME "rdpei" TYPE "dynamic" + DESCRIPTION "Input Virtual Channel Extension" + SPECIFICATIONS "[MS-RDPEI]" + DEFAULT ${OPTION_DEFAULT}) + +define_channel_client_options(${OPTION_CLIENT_DEFAULT}) +define_channel_server_options(${OPTION_SERVER_DEFAULT}) + diff --git a/channels/rdpei/client/CMakeLists.txt b/channels/rdpei/client/CMakeLists.txt new file mode 100644 index 0000000..79cc5a1 --- /dev/null +++ b/channels/rdpei/client/CMakeLists.txt @@ -0,0 +1,38 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2013 Marc-Andre Moreau +# +# Licensed 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. + +define_channel_client("rdpei") + +set(${MODULE_PREFIX}_SRCS + rdpei_main.c + rdpei_main.h + ../rdpei_common.c + ../rdpei_common.h) + +include_directories(..) +add_channel_client_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} TRUE "DVCPluginEntry") + + + +target_link_libraries(${MODULE_NAME} winpr freerdp) + + +if (WITH_DEBUG_SYMBOLS AND MSVC AND NOT BUILTIN_CHANNELS AND BUILD_SHARED_LIBS) + install(FILES ${CMAKE_BINARY_DIR}/${MODULE_NAME}.pdb DESTINATION ${FREERDP_ADDIN_PATH} COMPONENT symbols) +endif() + +set_property(TARGET ${MODULE_NAME} PROPERTY FOLDER "Channels/${CHANNEL_NAME}/Client") diff --git a/channels/rdpei/client/rdpei_main.c b/channels/rdpei/client/rdpei_main.c new file mode 100644 index 0000000..1d95054 --- /dev/null +++ b/channels/rdpei/client/rdpei_main.c @@ -0,0 +1,1391 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Input Virtual Channel Extension + * + * Copyright 2013 Marc-Andre Moreau + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "rdpei_common.h" + +#include "rdpei_main.h" + +/** + * Touch Input + * http://msdn.microsoft.com/en-us/library/windows/desktop/dd562197/ + * + * Windows Touch Input + * http://msdn.microsoft.com/en-us/library/windows/desktop/dd317321/ + * + * Input: Touch injection sample + * http://code.msdn.microsoft.com/windowsdesktop/Touch-Injection-Sample-444d9bf7 + * + * Pointer Input Message Reference + * http://msdn.microsoft.com/en-us/library/hh454916/ + * + * POINTER_INFO Structure + * http://msdn.microsoft.com/en-us/library/hh454907/ + * + * POINTER_TOUCH_INFO Structure + * http://msdn.microsoft.com/en-us/library/hh454910/ + */ + +#define MAX_CONTACTS 64 +#define MAX_PEN_CONTACTS 4 + +struct _RDPEI_CHANNEL_CALLBACK +{ + IWTSVirtualChannelCallback iface; + + IWTSPlugin* plugin; + IWTSVirtualChannelManager* channel_mgr; + IWTSVirtualChannel* channel; +}; +typedef struct _RDPEI_CHANNEL_CALLBACK RDPEI_CHANNEL_CALLBACK; + +struct _RDPEI_LISTENER_CALLBACK +{ + IWTSListenerCallback iface; + + IWTSPlugin* plugin; + IWTSVirtualChannelManager* channel_mgr; + RDPEI_CHANNEL_CALLBACK* channel_callback; +}; +typedef struct _RDPEI_LISTENER_CALLBACK RDPEI_LISTENER_CALLBACK; + +struct _RDPEI_PLUGIN +{ + IWTSPlugin iface; + + IWTSListener* listener; + RDPEI_LISTENER_CALLBACK* listener_callback; + + RdpeiClientContext* context; + + UINT32 version; + UINT32 features; + UINT16 maxTouchContacts; + UINT64 currentFrameTime; + UINT64 previousFrameTime; + RDPINPUT_CONTACT_POINT contactPoints[MAX_CONTACTS]; + + UINT64 currentPenFrameTime; + UINT64 previousPenFrameTime; + UINT16 maxPenContacts; + RDPINPUT_PEN_CONTACT_POINT penContactPoints[MAX_PEN_CONTACTS]; + + CRITICAL_SECTION lock; + rdpContext* rdpcontext; + BOOL initialized; + HANDLE thread; + HANDLE event; +}; +typedef struct _RDPEI_PLUGIN RDPEI_PLUGIN; + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpei_send_frame(RdpeiClientContext* context, RDPINPUT_TOUCH_FRAME* frame); + +#ifdef WITH_DEBUG_RDPEI +static const char* rdpei_eventid_string(UINT16 event) +{ + switch (event) + { + case EVENTID_SC_READY: + return "EVENTID_SC_READY"; + case EVENTID_CS_READY: + return "EVENTID_CS_READY"; + case EVENTID_TOUCH: + return "EVENTID_TOUCH"; + case EVENTID_SUSPEND_TOUCH: + return "EVENTID_SUSPEND_TOUCH"; + case EVENTID_RESUME_TOUCH: + return "EVENTID_RESUME_TOUCH"; + case EVENTID_DISMISS_HOVERING_CONTACT: + return "EVENTID_DISMISS_HOVERING_CONTACT"; + case EVENTID_PEN: + return "EVENTID_PEN"; + default: + return "EVENTID_UNKNOWN"; + } +} +#endif + +static RDPINPUT_CONTACT_POINT* rdpei_contact(RDPEI_PLUGIN* rdpei, INT32 externalId, BOOL active) +{ + UINT16 i; + + for (i = 0; i < rdpei->maxTouchContacts; i++) + { + RDPINPUT_CONTACT_POINT* contactPoint = &rdpei->contactPoints[i]; + + if (!contactPoint->active && active) + continue; + else if (!contactPoint->active && !active) + { + contactPoint->contactId = i; + contactPoint->externalId = externalId; + contactPoint->active = TRUE; + return contactPoint; + } + else if (contactPoint->externalId == externalId) + { + return contactPoint; + } + } + return NULL; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpei_add_frame(RdpeiClientContext* context) +{ + UINT16 i; + RDPEI_PLUGIN* rdpei; + RDPINPUT_TOUCH_FRAME frame = { 0 }; + RDPINPUT_CONTACT_DATA contacts[MAX_CONTACTS] = { 0 }; + + if (!context || !context->handle) + return ERROR_INTERNAL_ERROR; + + rdpei = (RDPEI_PLUGIN*)context->handle; + frame.contacts = contacts; + + for (i = 0; i < rdpei->maxTouchContacts; i++) + { + RDPINPUT_CONTACT_POINT* contactPoint = &rdpei->contactPoints[i]; + RDPINPUT_CONTACT_DATA* contact = &contactPoint->data; + + if (contactPoint->dirty) + { + contacts[frame.contactCount] = *contact; + rdpei->contactPoints[i].dirty = FALSE; + frame.contactCount++; + } + else if (contactPoint->active) + { + if (contact->contactFlags & CONTACT_FLAG_DOWN) + { + contact->contactFlags = CONTACT_FLAG_UPDATE; + contact->contactFlags |= CONTACT_FLAG_INRANGE; + contact->contactFlags |= CONTACT_FLAG_INCONTACT; + } + + contacts[frame.contactCount] = *contact; + frame.contactCount++; + } + if (contact->contactFlags & CONTACT_FLAG_UP) + { + contactPoint->active = FALSE; + contactPoint->externalId = 0; + contactPoint->contactId = 0; + } + } + + if (frame.contactCount > 0) + { + UINT error = rdpei_send_frame(context, &frame); + if (error != CHANNEL_RC_OK) + { + WLog_ERR(TAG, "rdpei_send_frame failed with error %" PRIu32 "!", error); + return error; + } + } + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpei_send_pdu(RDPEI_CHANNEL_CALLBACK* callback, wStream* s, UINT16 eventId, + UINT32 pduLength) +{ + UINT status; + if (!callback || !s || !callback->channel || !callback->channel->Write) + return ERROR_INTERNAL_ERROR; + + Stream_SetPosition(s, 0); + Stream_Write_UINT16(s, eventId); /* eventId (2 bytes) */ + Stream_Write_UINT32(s, pduLength); /* pduLength (4 bytes) */ + Stream_SetPosition(s, Stream_Length(s)); + status = callback->channel->Write(callback->channel, (UINT32)Stream_Length(s), Stream_Buffer(s), + NULL); +#ifdef WITH_DEBUG_RDPEI + WLog_DBG(TAG, + "rdpei_send_pdu: eventId: %" PRIu16 " (%s) length: %" PRIu32 " status: %" PRIu32 "", + eventId, rdpei_eventid_string(eventId), pduLength, status); +#endif + return status; +} + +static UINT rdpei_write_pen_frame(wStream* s, const RDPINPUT_PEN_FRAME* frame) +{ + UINT16 x; + if (!s || !frame) + return ERROR_INTERNAL_ERROR; + + if (!rdpei_write_2byte_unsigned(s, frame->contactCount)) + return ERROR_OUTOFMEMORY; + if (!rdpei_write_8byte_unsigned(s, frame->frameOffset)) + return ERROR_OUTOFMEMORY; + for (x = 0; x < frame->contactCount; x++) + { + const RDPINPUT_PEN_CONTACT* contact = &frame->contacts[x]; + + if (!Stream_EnsureRemainingCapacity(s, 1)) + return ERROR_OUTOFMEMORY; + Stream_Write_UINT8(s, contact->deviceId); + if (!rdpei_write_2byte_unsigned(s, contact->fieldsPresent)) + return ERROR_OUTOFMEMORY; + if (!rdpei_write_4byte_signed(s, contact->x)) + return ERROR_OUTOFMEMORY; + if (!rdpei_write_4byte_signed(s, contact->y)) + return ERROR_OUTOFMEMORY; + if (!rdpei_write_4byte_unsigned(s, contact->contactFlags)) + return ERROR_OUTOFMEMORY; + if (contact->fieldsPresent & PEN_CONTACT_PENFLAGS_PRESENT) + { + if (!rdpei_write_4byte_unsigned(s, contact->penFlags)) + return ERROR_OUTOFMEMORY; + } + if (contact->fieldsPresent & PEN_CONTACT_PRESSURE_PRESENT) + { + if (!rdpei_write_4byte_unsigned(s, contact->pressure)) + return ERROR_OUTOFMEMORY; + } + if (contact->fieldsPresent & PEN_CONTACT_ROTATION_PRESENT) + { + if (!rdpei_write_2byte_unsigned(s, contact->rotation)) + return ERROR_OUTOFMEMORY; + } + if (contact->fieldsPresent & PEN_CONTACT_TILTX_PRESENT) + { + if (!rdpei_write_2byte_signed(s, contact->tiltX)) + return ERROR_OUTOFMEMORY; + } + if (contact->fieldsPresent & PEN_CONTACT_TILTY_PRESENT) + { + if (!rdpei_write_2byte_signed(s, contact->tiltY)) + return ERROR_OUTOFMEMORY; + } + } + return CHANNEL_RC_OK; +} + +static UINT rdpei_send_pen_event_pdu(RDPEI_CHANNEL_CALLBACK* callback, UINT32 frameOffset, + const RDPINPUT_PEN_FRAME* frames, UINT16 count) +{ + UINT status; + wStream* s; + UINT16 x; + + if (!frames || (count == 0)) + return ERROR_INTERNAL_ERROR; + + s = Stream_New(NULL, 64); + + if (!s) + { + WLog_ERR(TAG, "Stream_New failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + Stream_Seek(s, RDPINPUT_HEADER_LENGTH); + /** + * the time that has elapsed (in milliseconds) from when the oldest touch frame + * was generated to when it was encoded for transmission by the client. + */ + rdpei_write_4byte_unsigned(s, frameOffset); /* encodeTime (FOUR_BYTE_UNSIGNED_INTEGER) */ + rdpei_write_2byte_unsigned(s, count); /* (frameCount) TWO_BYTE_UNSIGNED_INTEGER */ + + for (x = 0; x < count; x++) + { + if ((status = rdpei_write_pen_frame(s, &frames[x]))) + { + WLog_ERR(TAG, "rdpei_write_touch_frame failed with error %" PRIu32 "!", status); + Stream_Free(s, TRUE); + return status; + } + } + Stream_SealLength(s); + + status = rdpei_send_pdu(callback, s, EVENTID_PEN, Stream_Length(s)); + Stream_Free(s, TRUE); + return status; +} + +static UINT rdpei_send_pen_frame(RdpeiClientContext* context, RDPINPUT_PEN_FRAME* frame) +{ + const UINT64 currentTime = GetTickCount64(); + RDPEI_PLUGIN* rdpei; + RDPEI_CHANNEL_CALLBACK* callback; + UINT error; + + if (!context) + return ERROR_INTERNAL_ERROR; + rdpei = (RDPEI_PLUGIN*)context->handle; + if (!rdpei || !rdpei->listener_callback) + return ERROR_INTERNAL_ERROR; + + callback = rdpei->listener_callback->channel_callback; + + if (!rdpei->previousPenFrameTime && !rdpei->currentPenFrameTime) + { + rdpei->currentPenFrameTime = currentTime; + frame->frameOffset = 0; + } + else + { + rdpei->currentPenFrameTime = currentTime; + frame->frameOffset = rdpei->currentPenFrameTime - rdpei->previousPenFrameTime; + } + + if ((error = rdpei_send_pen_event_pdu(callback, frame->frameOffset, frame, 1))) + return error; + + rdpei->previousPenFrameTime = rdpei->currentPenFrameTime; + return error; +} + +static UINT rdpei_add_pen_frame(RdpeiClientContext* context) +{ + UINT16 i; + RDPEI_PLUGIN* rdpei; + RDPINPUT_PEN_FRAME penFrame = { 0 }; + RDPINPUT_PEN_CONTACT penContacts[MAX_PEN_CONTACTS] = { 0 }; + + if (!context || !context->handle) + return ERROR_INTERNAL_ERROR; + + rdpei = (RDPEI_PLUGIN*)context->handle; + + penFrame.contacts = penContacts; + + for (i = 0; i < rdpei->maxPenContacts; i++) + { + RDPINPUT_PEN_CONTACT_POINT* contact = &(rdpei->penContactPoints[i]); + + if (contact->dirty) + { + penContacts[penFrame.contactCount++] = contact->data; + contact->dirty = FALSE; + } + else if (contact->active) + { + if (contact->data.contactFlags & CONTACT_FLAG_DOWN) + { + contact->data.contactFlags = CONTACT_FLAG_UPDATE; + contact->data.contactFlags |= CONTACT_FLAG_INRANGE; + contact->data.contactFlags |= CONTACT_FLAG_INCONTACT; + } + + penContacts[penFrame.contactCount++] = contact->data; + } + if (contact->data.contactFlags & CONTACT_FLAG_UP) + { + contact->externalId = 0; + contact->active = FALSE; + } + } + + if (penFrame.contactCount > 0) + return rdpei_send_pen_frame(context, &penFrame); + return CHANNEL_RC_OK; +} + +static UINT rdpei_update(RdpeiClientContext* context) +{ + UINT error = rdpei_add_frame(context); + if (error != CHANNEL_RC_OK) + { + WLog_ERR(TAG, "rdpei_add_frame failed with error %" PRIu32 "!", error); + return error; + } + + return rdpei_add_pen_frame(context); +} + +static DWORD WINAPI rdpei_periodic_update(LPVOID arg) +{ + DWORD status; + RDPEI_PLUGIN* rdpei = (RDPEI_PLUGIN*)arg; + UINT error = CHANNEL_RC_OK; + RdpeiClientContext* context; + + if (!rdpei) + { + error = ERROR_INVALID_PARAMETER; + goto out; + } + + context = (RdpeiClientContext*)rdpei->iface.pInterface; + + if (!context) + { + error = ERROR_INVALID_PARAMETER; + goto out; + } + + while (rdpei->initialized) + { + status = WaitForSingleObject(rdpei->event, 20); + + if (status == WAIT_FAILED) + { + error = GetLastError(); + WLog_ERR(TAG, "WaitForMultipleObjects failed with error %" PRIu32 "!", error); + break; + } + + EnterCriticalSection(&rdpei->lock); + + error = rdpei_update(context); + if (error != CHANNEL_RC_OK) + { + WLog_ERR(TAG, "rdpei_add_frame failed with error %" PRIu32 "!", error); + break; + } + + if (status == WAIT_OBJECT_0) + ResetEvent(rdpei->event); + + LeaveCriticalSection(&rdpei->lock); + } + +out: + + if (error && rdpei && rdpei->rdpcontext) + setChannelError(rdpei->rdpcontext, error, "rdpei_schedule_thread reported an error"); + + ExitThread(error); + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpei_send_cs_ready_pdu(RDPEI_CHANNEL_CALLBACK* callback) +{ + UINT status; + wStream* s; + UINT32 flags; + UINT32 pduLength; + RDPEI_PLUGIN* rdpei; + + if (!callback || !callback->plugin) + return ERROR_INTERNAL_ERROR; + + rdpei = (RDPEI_PLUGIN*)callback->plugin; + flags = 0; + flags |= CS_READY_FLAGS_SHOW_TOUCH_VISUALS; + if (rdpei->version > RDPINPUT_PROTOCOL_V10) + flags |= CS_READY_FLAGS_DISABLE_TIMESTAMP_INJECTION; + flags |= CS_READY_FLAGS_ENABLE_MULTIPEN_INJECTION; + pduLength = RDPINPUT_HEADER_LENGTH + 10; + s = Stream_New(NULL, pduLength); + + if (!s) + { + WLog_ERR(TAG, "Stream_New failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + Stream_Seek(s, RDPINPUT_HEADER_LENGTH); + Stream_Write_UINT32(s, flags); /* flags (4 bytes) */ + Stream_Write_UINT32(s, rdpei->version); /* protocolVersion (4 bytes) */ + Stream_Write_UINT16(s, rdpei->maxTouchContacts); /* maxTouchContacts (2 bytes) */ + Stream_SealLength(s); + status = rdpei_send_pdu(callback, s, EVENTID_CS_READY, pduLength); + Stream_Free(s, TRUE); + return status; +} + +static void rdpei_print_contact_flags(UINT32 contactFlags) +{ + if (contactFlags & CONTACT_FLAG_DOWN) + WLog_DBG(TAG, " CONTACT_FLAG_DOWN"); + + if (contactFlags & CONTACT_FLAG_UPDATE) + WLog_DBG(TAG, " CONTACT_FLAG_UPDATE"); + + if (contactFlags & CONTACT_FLAG_UP) + WLog_DBG(TAG, " CONTACT_FLAG_UP"); + + if (contactFlags & CONTACT_FLAG_INRANGE) + WLog_DBG(TAG, " CONTACT_FLAG_INRANGE"); + + if (contactFlags & CONTACT_FLAG_INCONTACT) + WLog_DBG(TAG, " CONTACT_FLAG_INCONTACT"); + + if (contactFlags & CONTACT_FLAG_CANCELED) + WLog_DBG(TAG, " CONTACT_FLAG_CANCELED"); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpei_write_touch_frame(wStream* s, RDPINPUT_TOUCH_FRAME* frame) +{ + UINT32 index; + int rectSize = 2; + RDPINPUT_CONTACT_DATA* contact; + if (!s || !frame) + return ERROR_INTERNAL_ERROR; +#ifdef WITH_DEBUG_RDPEI + WLog_DBG(TAG, "contactCount: %" PRIu32 "", frame->contactCount); + WLog_DBG(TAG, "frameOffset: 0x%016" PRIX64 "", frame->frameOffset); +#endif + rdpei_write_2byte_unsigned(s, + frame->contactCount); /* contactCount (TWO_BYTE_UNSIGNED_INTEGER) */ + /** + * the time offset from the previous frame (in microseconds). + * If this is the first frame being transmitted then this field MUST be set to zero. + */ + rdpei_write_8byte_unsigned(s, frame->frameOffset * + 1000); /* frameOffset (EIGHT_BYTE_UNSIGNED_INTEGER) */ + + if (!Stream_EnsureRemainingCapacity(s, (size_t)frame->contactCount * 64)) + { + WLog_ERR(TAG, "Stream_EnsureRemainingCapacity failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + for (index = 0; index < frame->contactCount; index++) + { + contact = &frame->contacts[index]; + contact->fieldsPresent |= CONTACT_DATA_CONTACTRECT_PRESENT; + contact->contactRectLeft = contact->x - rectSize; + contact->contactRectTop = contact->y - rectSize; + contact->contactRectRight = contact->x + rectSize; + contact->contactRectBottom = contact->y + rectSize; +#ifdef WITH_DEBUG_RDPEI + WLog_DBG(TAG, "contact[%" PRIu32 "].contactId: %" PRIu32 "", index, contact->contactId); + WLog_DBG(TAG, "contact[%" PRIu32 "].fieldsPresent: %" PRIu32 "", index, + contact->fieldsPresent); + WLog_DBG(TAG, "contact[%" PRIu32 "].x: %" PRId32 "", index, contact->x); + WLog_DBG(TAG, "contact[%" PRIu32 "].y: %" PRId32 "", index, contact->y); + WLog_DBG(TAG, "contact[%" PRIu32 "].contactFlags: 0x%08" PRIX32 "", index, + contact->contactFlags); + rdpei_print_contact_flags(contact->contactFlags); +#endif + Stream_Write_UINT8(s, contact->contactId); /* contactId (1 byte) */ + /* fieldsPresent (TWO_BYTE_UNSIGNED_INTEGER) */ + rdpei_write_2byte_unsigned(s, contact->fieldsPresent); + rdpei_write_4byte_signed(s, contact->x); /* x (FOUR_BYTE_SIGNED_INTEGER) */ + rdpei_write_4byte_signed(s, contact->y); /* y (FOUR_BYTE_SIGNED_INTEGER) */ + /* contactFlags (FOUR_BYTE_UNSIGNED_INTEGER) */ + rdpei_write_4byte_unsigned(s, contact->contactFlags); + + if (contact->fieldsPresent & CONTACT_DATA_CONTACTRECT_PRESENT) + { + /* contactRectLeft (TWO_BYTE_SIGNED_INTEGER) */ + rdpei_write_2byte_signed(s, contact->contactRectLeft); + /* contactRectTop (TWO_BYTE_SIGNED_INTEGER) */ + rdpei_write_2byte_signed(s, contact->contactRectTop); + /* contactRectRight (TWO_BYTE_SIGNED_INTEGER) */ + rdpei_write_2byte_signed(s, contact->contactRectRight); + /* contactRectBottom (TWO_BYTE_SIGNED_INTEGER) */ + rdpei_write_2byte_signed(s, contact->contactRectBottom); + } + + if (contact->fieldsPresent & CONTACT_DATA_ORIENTATION_PRESENT) + { + /* orientation (FOUR_BYTE_UNSIGNED_INTEGER) */ + rdpei_write_4byte_unsigned(s, contact->orientation); + } + + if (contact->fieldsPresent & CONTACT_DATA_PRESSURE_PRESENT) + { + /* pressure (FOUR_BYTE_UNSIGNED_INTEGER) */ + rdpei_write_4byte_unsigned(s, contact->pressure); + } + } + + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpei_send_touch_event_pdu(RDPEI_CHANNEL_CALLBACK* callback, + RDPINPUT_TOUCH_FRAME* frame) +{ + UINT status; + wStream* s; + UINT32 pduLength; + if (!frame) + return ERROR_INTERNAL_ERROR; + pduLength = 64 + (frame->contactCount * 64); + s = Stream_New(NULL, pduLength); + + if (!s) + { + WLog_ERR(TAG, "Stream_New failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + Stream_Seek(s, RDPINPUT_HEADER_LENGTH); + /** + * the time that has elapsed (in milliseconds) from when the oldest touch frame + * was generated to when it was encoded for transmission by the client. + */ + rdpei_write_4byte_unsigned( + s, (UINT32)frame->frameOffset); /* encodeTime (FOUR_BYTE_UNSIGNED_INTEGER) */ + rdpei_write_2byte_unsigned(s, 1); /* (frameCount) TWO_BYTE_UNSIGNED_INTEGER */ + + if ((status = rdpei_write_touch_frame(s, frame))) + { + WLog_ERR(TAG, "rdpei_write_touch_frame failed with error %" PRIu32 "!", status); + Stream_Free(s, TRUE); + return status; + } + + Stream_SealLength(s); + pduLength = Stream_Length(s); + status = rdpei_send_pdu(callback, s, EVENTID_TOUCH, pduLength); + Stream_Free(s, TRUE); + return status; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpei_recv_sc_ready_pdu(RDPEI_CHANNEL_CALLBACK* callback, wStream* s) +{ + UINT32 features = 0; + UINT32 size; + UINT32 protocolVersion; + RDPEI_PLUGIN* rdpei; + + if (!callback || !callback->plugin) + return ERROR_INTERNAL_ERROR; + + rdpei = (RDPEI_PLUGIN*)callback->plugin; + + size = Stream_GetRemainingLength(s); + if (size < 4) + return ERROR_INVALID_DATA; + Stream_Read_UINT32(s, protocolVersion); /* protocolVersion (4 bytes) */ + + if (protocolVersion >= RDPINPUT_PROTOCOL_V300) + { + if (size < 8) + return ERROR_INVALID_DATA; + } + if (size >= 9) + Stream_Read_UINT32(s, features); + + if (rdpei->version > protocolVersion) + rdpei->version = protocolVersion; + rdpei->features = features; +#if 0 + + if (protocolVersion != RDPINPUT_PROTOCOL_V10) + { + WLog_ERR(TAG, "Unknown [MS-RDPEI] protocolVersion: 0x%08"PRIX32"", protocolVersion); + return -1; + } + +#endif + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpei_recv_suspend_touch_pdu(RDPEI_CHANNEL_CALLBACK* callback, wStream* s) +{ + UINT error = CHANNEL_RC_OK; + RdpeiClientContext* rdpei; + + WINPR_UNUSED(s); + + if (!callback || !callback->plugin) + return ERROR_INTERNAL_ERROR; + rdpei = (RdpeiClientContext*)callback->plugin->pInterface; + if (!rdpei) + return ERROR_INTERNAL_ERROR; + + IFCALLRET(rdpei->SuspendTouch, error, rdpei); + + if (error) + WLog_ERR(TAG, "rdpei->SuspendTouch failed with error %" PRIu32 "!", error); + + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpei_recv_resume_touch_pdu(RDPEI_CHANNEL_CALLBACK* callback, wStream* s) +{ + RdpeiClientContext* rdpei; + UINT error = CHANNEL_RC_OK; + if (!s || !callback || !callback->plugin) + return ERROR_INTERNAL_ERROR; + rdpei = (RdpeiClientContext*)callback->plugin->pInterface; + if (!rdpei) + return ERROR_INTERNAL_ERROR; + + IFCALLRET(rdpei->ResumeTouch, error, rdpei); + + if (error) + WLog_ERR(TAG, "rdpei->ResumeTouch failed with error %" PRIu32 "!", error); + + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpei_recv_pdu(RDPEI_CHANNEL_CALLBACK* callback, wStream* s) +{ + UINT16 eventId; + UINT32 pduLength; + UINT error; + if (!s) + return ERROR_INTERNAL_ERROR; + if (Stream_GetRemainingLength(s) < 6) + return ERROR_INVALID_DATA; + + Stream_Read_UINT16(s, eventId); /* eventId (2 bytes) */ + Stream_Read_UINT32(s, pduLength); /* pduLength (4 bytes) */ +#ifdef WITH_DEBUG_RDPEI + WLog_DBG(TAG, "rdpei_recv_pdu: eventId: %" PRIu16 " (%s) length: %" PRIu32 "", eventId, + rdpei_eventid_string(eventId), pduLength); +#endif + + switch (eventId) + { + case EVENTID_SC_READY: + if ((error = rdpei_recv_sc_ready_pdu(callback, s))) + { + WLog_ERR(TAG, "rdpei_recv_sc_ready_pdu failed with error %" PRIu32 "!", error); + return error; + } + + if ((error = rdpei_send_cs_ready_pdu(callback))) + { + WLog_ERR(TAG, "rdpei_send_cs_ready_pdu failed with error %" PRIu32 "!", error); + return error; + } + + break; + + case EVENTID_SUSPEND_TOUCH: + if ((error = rdpei_recv_suspend_touch_pdu(callback, s))) + { + WLog_ERR(TAG, "rdpei_recv_suspend_touch_pdu failed with error %" PRIu32 "!", error); + return error; + } + + break; + + case EVENTID_RESUME_TOUCH: + if ((error = rdpei_recv_resume_touch_pdu(callback, s))) + { + WLog_ERR(TAG, "rdpei_recv_resume_touch_pdu failed with error %" PRIu32 "!", error); + return error; + } + + break; + + default: + break; + } + + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpei_on_data_received(IWTSVirtualChannelCallback* pChannelCallback, wStream* data) +{ + RDPEI_CHANNEL_CALLBACK* callback = (RDPEI_CHANNEL_CALLBACK*)pChannelCallback; + return rdpei_recv_pdu(callback, data); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpei_on_close(IWTSVirtualChannelCallback* pChannelCallback) +{ + RDPEI_CHANNEL_CALLBACK* callback = (RDPEI_CHANNEL_CALLBACK*)pChannelCallback; + free(callback); + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpei_on_new_channel_connection(IWTSListenerCallback* pListenerCallback, + IWTSVirtualChannel* pChannel, BYTE* Data, + BOOL* pbAccept, IWTSVirtualChannelCallback** ppCallback) +{ + RDPEI_CHANNEL_CALLBACK* callback; + RDPEI_LISTENER_CALLBACK* listener_callback = (RDPEI_LISTENER_CALLBACK*)pListenerCallback; + if (!listener_callback) + return ERROR_INTERNAL_ERROR; + callback = (RDPEI_CHANNEL_CALLBACK*)calloc(1, sizeof(RDPEI_CHANNEL_CALLBACK)); + + WINPR_UNUSED(Data); + WINPR_UNUSED(pbAccept); + + if (!callback) + { + WLog_ERR(TAG, "calloc failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + callback->iface.OnDataReceived = rdpei_on_data_received; + callback->iface.OnClose = rdpei_on_close; + callback->plugin = listener_callback->plugin; + callback->channel_mgr = listener_callback->channel_mgr; + callback->channel = pChannel; + listener_callback->channel_callback = callback; + *ppCallback = (IWTSVirtualChannelCallback*)callback; + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpei_plugin_terminated(IWTSPlugin* pPlugin) +{ + RDPEI_PLUGIN* rdpei = (RDPEI_PLUGIN*)pPlugin; + + if (!pPlugin) + return ERROR_INVALID_PARAMETER; + + if (rdpei && rdpei->listener_callback) + { + IWTSVirtualChannelManager* mgr = rdpei->listener_callback->channel_mgr; + if (mgr) + IFCALL(mgr->DestroyListener, mgr, rdpei->listener); + + rdpei->initialized = FALSE; + if (rdpei->event) + SetEvent(rdpei->event); + + if (rdpei->thread) + { + WaitForSingleObject(rdpei->thread, INFINITE); + CloseHandle(rdpei->thread); + } + if (rdpei->event) + CloseHandle(rdpei->event); + } + DeleteCriticalSection(&rdpei->lock); + free(rdpei->listener_callback); + free(rdpei->context); + free(rdpei); + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpei_plugin_initialize(IWTSPlugin* pPlugin, IWTSVirtualChannelManager* pChannelMgr) +{ + UINT error; + RDPEI_PLUGIN* rdpei = (RDPEI_PLUGIN*)pPlugin; + + if (rdpei->initialized) + { + WLog_ERR(TAG, "[%s] channel initialized twice, aborting", RDPEI_DVC_CHANNEL_NAME); + return ERROR_INVALID_DATA; + } + rdpei->listener_callback = (RDPEI_LISTENER_CALLBACK*)calloc(1, sizeof(RDPEI_LISTENER_CALLBACK)); + + if (!rdpei->listener_callback) + { + WLog_ERR(TAG, "calloc failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + rdpei->listener_callback->iface.OnNewChannelConnection = rdpei_on_new_channel_connection; + rdpei->listener_callback->plugin = pPlugin; + rdpei->listener_callback->channel_mgr = pChannelMgr; + + if ((error = pChannelMgr->CreateListener(pChannelMgr, RDPEI_DVC_CHANNEL_NAME, 0, + &rdpei->listener_callback->iface, &(rdpei->listener)))) + { + WLog_ERR(TAG, "ChannelMgr->CreateListener failed with error %" PRIu32 "!", error); + goto error_out; + } + + rdpei->listener->pInterface = rdpei->iface.pInterface; + + InitializeCriticalSection(&rdpei->lock); + rdpei->event = CreateEventA(NULL, TRUE, FALSE, NULL); + if (!rdpei->event) + goto error_out; + rdpei->thread = CreateThread(NULL, 0, rdpei_periodic_update, rdpei, 0, NULL); + if (!rdpei->thread) + goto error_out; + rdpei->initialized = TRUE; + return error; +error_out: + rdpei_plugin_terminated(pPlugin); + return error; +} + +/** + * Channel Client Interface + */ + +static UINT32 rdpei_get_version(RdpeiClientContext* context) +{ + RDPEI_PLUGIN* rdpei; + if (!context || !context->handle) + return -1; + rdpei = (RDPEI_PLUGIN*)context->handle; + return rdpei->version; +} + +static UINT32 rdpei_get_features(RdpeiClientContext* context) +{ + RDPEI_PLUGIN* rdpei; + if (!context || !context->handle) + return -1; + rdpei = (RDPEI_PLUGIN*)context->handle; + return rdpei->features; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT rdpei_send_frame(RdpeiClientContext* context, RDPINPUT_TOUCH_FRAME* frame) +{ + UINT64 currentTime; + RDPEI_PLUGIN* rdpei = (RDPEI_PLUGIN*)context->handle; + RDPEI_CHANNEL_CALLBACK* callback = rdpei->listener_callback->channel_callback; + UINT error; + currentTime = GetTickCount64(); + + if (!rdpei->previousFrameTime && !rdpei->currentFrameTime) + { + rdpei->currentFrameTime = currentTime; + frame->frameOffset = 0; + } + else + { + rdpei->currentFrameTime = currentTime; + frame->frameOffset = rdpei->currentFrameTime - rdpei->previousFrameTime; + } + + if ((error = rdpei_send_touch_event_pdu(callback, frame))) + { + WLog_ERR(TAG, "rdpei_send_touch_event_pdu failed with error %" PRIu32 "!", error); + return error; + } + + rdpei->previousFrameTime = rdpei->currentFrameTime; + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpei_add_contact(RdpeiClientContext* context, const RDPINPUT_CONTACT_DATA* contact) +{ + RDPINPUT_CONTACT_POINT* contactPoint; + RDPEI_PLUGIN* rdpei; + if (!context || !contact || !context->handle) + return ERROR_INTERNAL_ERROR; + + rdpei = (RDPEI_PLUGIN*)context->handle; + + EnterCriticalSection(&rdpei->lock); + contactPoint = &rdpei->contactPoints[contact->contactId]; + contactPoint->data = *contact; + contactPoint->dirty = TRUE; + SetEvent(rdpei->event); + LeaveCriticalSection(&rdpei->lock); + + return CHANNEL_RC_OK; +} + +static UINT rdpei_touch_process(RdpeiClientContext* context, INT32 externalId, UINT32 contactFlags, + INT32 x, INT32 y, INT32* contactId) +{ + INT64 contactIdlocal = -1; + RDPINPUT_CONTACT_POINT* contactPoint; + RDPEI_PLUGIN* rdpei; + BOOL begin; + UINT error = CHANNEL_RC_OK; + + if (!context || !contactId || !context->handle) + return ERROR_INTERNAL_ERROR; + + rdpei = (RDPEI_PLUGIN*)context->handle; + /* Create a new contact point in an empty slot */ + EnterCriticalSection(&rdpei->lock); + begin = contactFlags & CONTACT_FLAG_DOWN; + contactPoint = rdpei_contact(rdpei, externalId, !begin); + if (contactPoint) + contactIdlocal = contactPoint->contactId; + LeaveCriticalSection(&rdpei->lock); + + if (contactIdlocal >= 0) + { + RDPINPUT_CONTACT_DATA contact = { 0 }; + contact.x = x; + contact.y = y; + contact.contactId = contactIdlocal; + contact.contactFlags = contactFlags; + error = context->AddContact(context, &contact); + } + + if (contactId) + *contactId = contactIdlocal; + return error; +} +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpei_touch_begin(RdpeiClientContext* context, INT32 externalId, INT32 x, INT32 y, + INT32* contactId) +{ + return rdpei_touch_process(context, externalId, + CONTACT_FLAG_DOWN | CONTACT_FLAG_INRANGE | CONTACT_FLAG_INCONTACT, x, + y, contactId); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpei_touch_update(RdpeiClientContext* context, INT32 externalId, INT32 x, INT32 y, + INT32* contactId) +{ + return rdpei_touch_process(context, externalId, + CONTACT_FLAG_UPDATE | CONTACT_FLAG_INRANGE | CONTACT_FLAG_INCONTACT, + x, y, contactId); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpei_touch_end(RdpeiClientContext* context, INT32 externalId, INT32 x, INT32 y, + INT32* contactId) +{ + UINT error = rdpei_touch_process( + context, externalId, CONTACT_FLAG_UPDATE | CONTACT_FLAG_INRANGE | CONTACT_FLAG_INCONTACT, x, + y, contactId); + if (error != CHANNEL_RC_OK) + return error; + return rdpei_touch_process(context, externalId, CONTACT_FLAG_UP, x, y, contactId); +} + +static RDPINPUT_PEN_CONTACT_POINT* rdpei_pen_contact(RDPEI_PLUGIN* rdpei, INT32 externalId, + BOOL active) +{ + UINT32 x; + if (!rdpei) + return NULL; + + for (x = 0; x < rdpei->maxPenContacts; x++) + { + RDPINPUT_PEN_CONTACT_POINT* contact = &rdpei->penContactPoints[x]; + if (active) + { + if (contact->active) + { + if (contact->externalId == externalId) + return contact; + } + } + else + { + if (!contact->active) + { + contact->externalId = externalId; + contact->active = TRUE; + return contact; + } + } + } + return NULL; +} + +static UINT rdpei_add_pen(RdpeiClientContext* context, INT32 externalId, + const RDPINPUT_PEN_CONTACT* contact) +{ + RDPEI_PLUGIN* rdpei; + RDPINPUT_PEN_CONTACT_POINT* contactPoint; + + if (!context || !contact || !context->handle) + return ERROR_INTERNAL_ERROR; + + rdpei = (RDPEI_PLUGIN*)context->handle; + + EnterCriticalSection(&rdpei->lock); + contactPoint = rdpei_pen_contact(rdpei, externalId, TRUE); + if (contactPoint) + { + contactPoint->data = *contact; + contactPoint->dirty = TRUE; + SetEvent(rdpei->event); + } + LeaveCriticalSection(&rdpei->lock); + + return CHANNEL_RC_OK; +} + +static UINT rdpei_pen_process(RdpeiClientContext* context, INT32 externalId, UINT32 contactFlags, + UINT32 fieldFlags, INT32 x, INT32 y, va_list ap) +{ + RDPINPUT_PEN_CONTACT_POINT* contactPoint; + RDPEI_PLUGIN* rdpei; + BOOL begin; + UINT error = CHANNEL_RC_OK; + + if (!context || !context->handle) + return ERROR_INTERNAL_ERROR; + + rdpei = (RDPEI_PLUGIN*)context->handle; + begin = contactFlags & CONTACT_FLAG_DOWN; + + EnterCriticalSection(&rdpei->lock); + contactPoint = rdpei_pen_contact(rdpei, externalId, !begin); + LeaveCriticalSection(&rdpei->lock); + if (contactPoint != NULL) + { + RDPINPUT_PEN_CONTACT contact = { 0 }; + + contact.x = x; + contact.y = y; + contact.fieldsPresent = fieldFlags; + + contact.contactFlags = contactFlags; + if (fieldFlags & PEN_CONTACT_PENFLAGS_PRESENT) + contact.penFlags = va_arg(ap, UINT32); + if (fieldFlags & PEN_CONTACT_PRESSURE_PRESENT) + contact.pressure = va_arg(ap, UINT32); + if (fieldFlags & PEN_CONTACT_ROTATION_PRESENT) + contact.rotation = va_arg(ap, UINT32); + if (fieldFlags & PEN_CONTACT_TILTX_PRESENT) + contact.tiltX = va_arg(ap, INT32); + if (fieldFlags & PEN_CONTACT_TILTY_PRESENT) + contact.tiltY = va_arg(ap, INT32); + + error = context->AddPen(context, externalId, &contact); + } + + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpei_pen_begin(RdpeiClientContext* context, INT32 externalId, UINT32 fieldFlags, + INT32 x, INT32 y, ...) +{ + UINT error; + va_list ap; + + va_start(ap, y); + error = rdpei_pen_process(context, externalId, + CONTACT_FLAG_DOWN | CONTACT_FLAG_INRANGE | CONTACT_FLAG_INCONTACT, + fieldFlags, x, y, ap); + va_end(ap); + + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpei_pen_update(RdpeiClientContext* context, INT32 externalId, UINT32 fieldFlags, + INT32 x, INT32 y, ...) +{ + UINT error; + va_list ap; + + va_start(ap, y); + error = rdpei_pen_process(context, externalId, + CONTACT_FLAG_UPDATE | CONTACT_FLAG_INRANGE | CONTACT_FLAG_INCONTACT, + fieldFlags, x, y, ap); + va_end(ap); + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpei_pen_end(RdpeiClientContext* context, INT32 externalId, UINT32 fieldFlags, INT32 x, + INT32 y, ...) +{ + UINT error; + va_list ap; + + va_start(ap, y); + error = rdpei_pen_process(context, externalId, + CONTACT_FLAG_UPDATE | CONTACT_FLAG_INRANGE | CONTACT_FLAG_INCONTACT, + fieldFlags, x, y, ap); + if (error == CHANNEL_RC_OK) + error = rdpei_pen_process(context, externalId, CONTACT_FLAG_UP, fieldFlags, x, y, ap); + va_end(ap); + return error; +} + +#ifdef BUILTIN_CHANNELS +#define DVCPluginEntry rdpei_DVCPluginEntry +#else +#define DVCPluginEntry FREERDP_API DVCPluginEntry +#endif + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT DVCPluginEntry(IDRDYNVC_ENTRY_POINTS* pEntryPoints) +{ + UINT error; + RDPEI_PLUGIN* rdpei = NULL; + RdpeiClientContext* context = NULL; + rdpei = (RDPEI_PLUGIN*)pEntryPoints->GetPlugin(pEntryPoints, "rdpei"); + + if (!rdpei) + { + rdpei = (RDPEI_PLUGIN*)calloc(1, sizeof(RDPEI_PLUGIN)); + + if (!rdpei) + { + WLog_ERR(TAG, "calloc failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + rdpei->iface.Initialize = rdpei_plugin_initialize; + rdpei->iface.Connected = NULL; + rdpei->iface.Disconnected = NULL; + rdpei->iface.Terminated = rdpei_plugin_terminated; + rdpei->version = RDPINPUT_PROTOCOL_V300; + rdpei->currentFrameTime = 0; + rdpei->previousFrameTime = 0; + rdpei->maxTouchContacts = MAX_CONTACTS; + rdpei->maxPenContacts = MAX_PEN_CONTACTS; + rdpei->rdpcontext = + ((freerdp*)((rdpSettings*)pEntryPoints->GetRdpSettings(pEntryPoints))->instance) + ->context; + + context = (RdpeiClientContext*)calloc(1, sizeof(RdpeiClientContext)); + + if (!context) + { + WLog_ERR(TAG, "calloc failed!"); + error = CHANNEL_RC_NO_MEMORY; + goto error_out; + } + + context->handle = (void*)rdpei; + context->GetVersion = rdpei_get_version; + context->GetFeatures = rdpei_get_features; + context->AddContact = rdpei_add_contact; + context->TouchBegin = rdpei_touch_begin; + context->TouchUpdate = rdpei_touch_update; + context->TouchEnd = rdpei_touch_end; + context->AddPen = rdpei_add_pen; + context->PenBegin = rdpei_pen_begin; + context->PenUpdate = rdpei_pen_update; + context->PenEnd = rdpei_pen_end; + rdpei->iface.pInterface = (void*)context; + + if ((error = pEntryPoints->RegisterPlugin(pEntryPoints, "rdpei", (IWTSPlugin*)rdpei))) + { + WLog_ERR(TAG, "EntryPoints->RegisterPlugin failed with error %" PRIu32 "!", error); + error = CHANNEL_RC_NO_MEMORY; + goto error_out; + } + + rdpei->context = context; + } + + return CHANNEL_RC_OK; +error_out: + free(context); + free(rdpei); + return error; +} diff --git a/channels/rdpei/client/rdpei_main.h b/channels/rdpei/client/rdpei_main.h new file mode 100644 index 0000000..01ecd5f --- /dev/null +++ b/channels/rdpei/client/rdpei_main.h @@ -0,0 +1,93 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Input Virtual Channel Extension + * + * Copyright 2013 Marc-Andre Moreau + * + * Licensed 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 FREERDP_CHANNEL_RDPEI_CLIENT_MAIN_H +#define FREERDP_CHANNEL_RDPEI_CLIENT_MAIN_H + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include +#include + +#include +#include + +#define TAG CHANNELS_TAG("rdpei.client") + +/** + * Touch Contact State Transitions + * + * ENGAGED -> UPDATE | INRANGE | INCONTACT -> ENGAGED + * ENGAGED -> UP | INRANGE -> HOVERING + * ENGAGED -> UP -> OUT_OF_RANGE + * ENGAGED -> UP | CANCELED -> OUT_OF_RANGE + * + * HOVERING -> UPDATE | INRANGE -> HOVERING + * HOVERING -> DOWN | INRANGE | INCONTACT -> ENGAGED + * HOVERING -> UPDATE -> OUT_OF_RANGE + * HOVERING -> UPDATE | CANCELED -> OUT_OF_RANGE + * + * OUT_OF_RANGE -> DOWN | INRANGE | INCONTACT -> ENGAGED + * OUT_OF_RANGE -> UPDATE | INRANGE -> HOVERING + * + * When a contact is in the "hovering" or "engaged" state, it is referred to as being "active". + * "Hovering" contacts are in range of the digitizer, while "engaged" contacts are in range of + * the digitizer and in contact with the digitizer surface. MS-RDPEI remotes only active contacts + * and contacts that are transitioning to the "out of range" state; see section 2.2.3.3.1.1 for + * an enumeration of valid state flags combinations. + * + * When transitioning from the "engaged" state to the "hovering" state, or from the "engaged" + * state to the "out of range" state, the contact position cannot change; it is only allowed + * to change after the transition has taken place. + * + */ + +struct _RDPINPUT_CONTACT_POINT +{ + BOOL dirty; + BOOL active; + UINT32 contactId; + INT32 externalId; + RDPINPUT_CONTACT_DATA data; +}; +typedef struct _RDPINPUT_CONTACT_POINT RDPINPUT_CONTACT_POINT; + +struct _RDPINPUT_PEN_CONTACT_POINT +{ + BOOL dirty; + BOOL active; + INT32 externalId; + RDPINPUT_PEN_CONTACT data; +}; +typedef struct _RDPINPUT_PEN_CONTACT_POINT RDPINPUT_PEN_CONTACT_POINT; + +#ifdef WITH_DEBUG_DVC +#define DEBUG_DVC(...) WLog_DBG(TAG, __VA_ARGS__) +#else +#define DEBUG_DVC(...) \ + do \ + { \ + } while (0) +#endif + +#endif /* FREERDP_CHANNEL_RDPEI_CLIENT_MAIN_H */ diff --git a/channels/rdpei/rdpei_common.c b/channels/rdpei/rdpei_common.c new file mode 100644 index 0000000..32df8a0 --- /dev/null +++ b/channels/rdpei/rdpei_common.c @@ -0,0 +1,643 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Input Virtual Channel Extension + * + * Copyright 2013 Marc-Andre Moreau + * Copyright 2014 David Fort + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include + +#include "rdpei_common.h" + +BOOL rdpei_read_2byte_unsigned(wStream* s, UINT16* value) +{ + BYTE byte; + + if (Stream_GetRemainingLength(s) < 1) + return FALSE; + + Stream_Read_UINT8(s, byte); + + if (byte & 0x80) + { + if (Stream_GetRemainingLength(s) < 1) + return FALSE; + + *value = (byte & 0x7F) << 8; + Stream_Read_UINT8(s, byte); + *value |= byte; + } + else + { + *value = (byte & 0x7F); + } + + return TRUE; +} + +BOOL rdpei_write_2byte_unsigned(wStream* s, UINT16 value) +{ + BYTE byte; + + if (!Stream_EnsureRemainingCapacity(s, 2)) + return FALSE; + + if (value > 0x7FFF) + return FALSE; + + if (value >= 0x7F) + { + byte = ((value & 0x7F00) >> 8); + Stream_Write_UINT8(s, byte | 0x80); + byte = (value & 0xFF); + Stream_Write_UINT8(s, byte); + } + else + { + byte = (value & 0x7F); + Stream_Write_UINT8(s, byte); + } + + return TRUE; +} + +BOOL rdpei_read_2byte_signed(wStream* s, INT16* value) +{ + BYTE byte; + BOOL negative; + + if (Stream_GetRemainingLength(s) < 1) + return FALSE; + + Stream_Read_UINT8(s, byte); + + negative = (byte & 0x40) ? TRUE : FALSE; + + *value = (byte & 0x3F); + + if (byte & 0x80) + { + if (Stream_GetRemainingLength(s) < 1) + return FALSE; + + Stream_Read_UINT8(s, byte); + *value = (*value << 8) | byte; + } + + if (negative) + *value *= -1; + + return TRUE; +} + +BOOL rdpei_write_2byte_signed(wStream* s, INT16 value) +{ + BYTE byte; + BOOL negative = FALSE; + + if (!Stream_EnsureRemainingCapacity(s, 2)) + return FALSE; + + if (value < 0) + { + negative = TRUE; + value *= -1; + } + + if (value > 0x3FFF) + return FALSE; + + if (value >= 0x3F) + { + byte = ((value & 0x3F00) >> 8); + + if (negative) + byte |= 0x40; + + Stream_Write_UINT8(s, byte | 0x80); + byte = (value & 0xFF); + Stream_Write_UINT8(s, byte); + } + else + { + byte = (value & 0x3F); + + if (negative) + byte |= 0x40; + + Stream_Write_UINT8(s, byte); + } + + return TRUE; +} + +BOOL rdpei_read_4byte_unsigned(wStream* s, UINT32* value) +{ + BYTE byte; + BYTE count; + + if (Stream_GetRemainingLength(s) < 1) + return FALSE; + + Stream_Read_UINT8(s, byte); + + count = (byte & 0xC0) >> 6; + + if (Stream_GetRemainingLength(s) < count) + return FALSE; + + switch (count) + { + case 0: + *value = (byte & 0x3F); + break; + + case 1: + *value = (byte & 0x3F) << 8; + Stream_Read_UINT8(s, byte); + *value |= byte; + break; + + case 2: + *value = (byte & 0x3F) << 16; + Stream_Read_UINT8(s, byte); + *value |= (byte << 8); + Stream_Read_UINT8(s, byte); + *value |= byte; + break; + + case 3: + *value = (byte & 0x3F) << 24; + Stream_Read_UINT8(s, byte); + *value |= (byte << 16); + Stream_Read_UINT8(s, byte); + *value |= (byte << 8); + Stream_Read_UINT8(s, byte); + *value |= byte; + break; + + default: + break; + } + + return TRUE; +} + +BOOL rdpei_write_4byte_unsigned(wStream* s, UINT32 value) +{ + BYTE byte; + + if (!Stream_EnsureRemainingCapacity(s, 4)) + return FALSE; + + if (value <= 0x3FUL) + { + Stream_Write_UINT8(s, value); + } + else if (value <= 0x3FFFUL) + { + byte = (value >> 8) & 0x3F; + Stream_Write_UINT8(s, byte | 0x40); + byte = (value & 0xFF); + Stream_Write_UINT8(s, byte); + } + else if (value <= 0x3FFFFFUL) + { + byte = (value >> 16) & 0x3F; + Stream_Write_UINT8(s, byte | 0x80); + byte = (value >> 8) & 0xFF; + Stream_Write_UINT8(s, byte); + byte = (value & 0xFF); + Stream_Write_UINT8(s, byte); + } + else if (value <= 0x3FFFFFFFUL) + { + byte = (value >> 24) & 0x3F; + Stream_Write_UINT8(s, byte | 0xC0); + byte = (value >> 16) & 0xFF; + Stream_Write_UINT8(s, byte); + byte = (value >> 8) & 0xFF; + Stream_Write_UINT8(s, byte); + byte = (value & 0xFF); + Stream_Write_UINT8(s, byte); + } + else + { + return FALSE; + } + + return TRUE; +} + +BOOL rdpei_read_4byte_signed(wStream* s, INT32* value) +{ + BYTE byte; + BYTE count; + BOOL negative; + + if (Stream_GetRemainingLength(s) < 1) + return FALSE; + + Stream_Read_UINT8(s, byte); + + count = (byte & 0xC0) >> 6; + negative = (byte & 0x20); + + if (Stream_GetRemainingLength(s) < count) + return FALSE; + + switch (count) + { + case 0: + *value = (byte & 0x1F); + break; + + case 1: + *value = (byte & 0x1F) << 8; + Stream_Read_UINT8(s, byte); + *value |= byte; + break; + + case 2: + *value = (byte & 0x1F) << 16; + Stream_Read_UINT8(s, byte); + *value |= (byte << 8); + Stream_Read_UINT8(s, byte); + *value |= byte; + break; + + case 3: + *value = (byte & 0x1F) << 24; + Stream_Read_UINT8(s, byte); + *value |= (byte << 16); + Stream_Read_UINT8(s, byte); + *value |= (byte << 8); + Stream_Read_UINT8(s, byte); + *value |= byte; + break; + + default: + break; + } + + if (negative) + *value *= -1; + + return TRUE; +} + +BOOL rdpei_write_4byte_signed(wStream* s, INT32 value) +{ + BYTE byte; + BOOL negative = FALSE; + + if (!Stream_EnsureRemainingCapacity(s, 4)) + return FALSE; + + if (value < 0) + { + negative = TRUE; + value *= -1; + } + + if (value <= 0x1FL) + { + byte = value & 0x1F; + + if (negative) + byte |= 0x20; + + Stream_Write_UINT8(s, byte); + } + else if (value <= 0x1FFFL) + { + byte = (value >> 8) & 0x1F; + + if (negative) + byte |= 0x20; + + Stream_Write_UINT8(s, byte | 0x40); + byte = (value & 0xFF); + Stream_Write_UINT8(s, byte); + } + else if (value <= 0x1FFFFFL) + { + byte = (value >> 16) & 0x1F; + + if (negative) + byte |= 0x20; + + Stream_Write_UINT8(s, byte | 0x80); + byte = (value >> 8) & 0xFF; + Stream_Write_UINT8(s, byte); + byte = (value & 0xFF); + Stream_Write_UINT8(s, byte); + } + else if (value <= 0x1FFFFFFFL) + { + byte = (value >> 24) & 0x1F; + + if (negative) + byte |= 0x20; + + Stream_Write_UINT8(s, byte | 0xC0); + byte = (value >> 16) & 0xFF; + Stream_Write_UINT8(s, byte); + byte = (value >> 8) & 0xFF; + Stream_Write_UINT8(s, byte); + byte = (value & 0xFF); + Stream_Write_UINT8(s, byte); + } + else + { + return FALSE; + } + + return TRUE; +} + +BOOL rdpei_read_8byte_unsigned(wStream* s, UINT64* value) +{ + BYTE byte; + BYTE count; + + if (Stream_GetRemainingLength(s) < 1) + return FALSE; + + Stream_Read_UINT8(s, byte); + + count = (byte & 0xE0) >> 5; + + if (Stream_GetRemainingLength(s) < count) + return FALSE; + + switch (count) + { + case 0: + *value = (byte & 0x1F); + break; + + case 1: + *value = (byte & 0x1F) << 8; + Stream_Read_UINT8(s, byte); + *value |= byte; + break; + + case 2: + *value = (byte & 0x1F) << 16; + Stream_Read_UINT8(s, byte); + *value |= (byte << 8); + Stream_Read_UINT8(s, byte); + *value |= byte; + break; + + case 3: + *value = (byte & 0x1F) << 24; + Stream_Read_UINT8(s, byte); + *value |= (byte << 16); + Stream_Read_UINT8(s, byte); + *value |= (byte << 8); + Stream_Read_UINT8(s, byte); + *value |= byte; + break; + + case 4: + *value = ((UINT64)(byte & 0x1F)) << 32; + Stream_Read_UINT8(s, byte); + *value |= (byte << 24); + Stream_Read_UINT8(s, byte); + *value |= (byte << 16); + Stream_Read_UINT8(s, byte); + *value |= (byte << 8); + Stream_Read_UINT8(s, byte); + *value |= byte; + break; + + case 5: + *value = ((UINT64)(byte & 0x1F)) << 40; + Stream_Read_UINT8(s, byte); + *value |= (((UINT64)byte) << 32); + Stream_Read_UINT8(s, byte); + *value |= (byte << 24); + Stream_Read_UINT8(s, byte); + *value |= (byte << 16); + Stream_Read_UINT8(s, byte); + *value |= (byte << 8); + Stream_Read_UINT8(s, byte); + *value |= byte; + break; + + case 6: + *value = ((UINT64)(byte & 0x1F)) << 48; + Stream_Read_UINT8(s, byte); + *value |= (((UINT64)byte) << 40); + Stream_Read_UINT8(s, byte); + *value |= (((UINT64)byte) << 32); + Stream_Read_UINT8(s, byte); + *value |= (byte << 24); + Stream_Read_UINT8(s, byte); + *value |= (byte << 16); + Stream_Read_UINT8(s, byte); + *value |= (byte << 8); + Stream_Read_UINT8(s, byte); + *value |= byte; + break; + + case 7: + *value = ((UINT64)(byte & 0x1F)) << 56; + Stream_Read_UINT8(s, byte); + *value |= (((UINT64)byte) << 48); + Stream_Read_UINT8(s, byte); + *value |= (((UINT64)byte) << 40); + Stream_Read_UINT8(s, byte); + *value |= (((UINT64)byte) << 32); + Stream_Read_UINT8(s, byte); + *value |= (byte << 24); + Stream_Read_UINT8(s, byte); + *value |= (byte << 16); + Stream_Read_UINT8(s, byte); + *value |= (byte << 8); + Stream_Read_UINT8(s, byte); + *value |= byte; + break; + + default: + break; + } + + return TRUE; +} + +BOOL rdpei_write_8byte_unsigned(wStream* s, UINT64 value) +{ + BYTE byte; + + if (!Stream_EnsureRemainingCapacity(s, 8)) + return FALSE; + + if (value <= 0x1FULL) + { + byte = value & 0x1F; + Stream_Write_UINT8(s, byte); + } + else if (value <= 0x1FFFULL) + { + byte = (value >> 8) & 0x1F; + byte |= (1 << 5); + Stream_Write_UINT8(s, byte); + byte = (value & 0xFF); + Stream_Write_UINT8(s, byte); + } + else if (value <= 0x1FFFFFULL) + { + byte = (value >> 16) & 0x1F; + byte |= (2 << 5); + Stream_Write_UINT8(s, byte); + byte = (value >> 8) & 0xFF; + Stream_Write_UINT8(s, byte); + byte = (value & 0xFF); + Stream_Write_UINT8(s, byte); + } + else if (value <= 0x1FFFFFFFULL) + { + byte = (value >> 24) & 0x1F; + byte |= (3 << 5); + Stream_Write_UINT8(s, byte); + byte = (value >> 16) & 0xFF; + Stream_Write_UINT8(s, byte); + byte = (value >> 8) & 0xFF; + Stream_Write_UINT8(s, byte); + byte = (value & 0xFF); + Stream_Write_UINT8(s, byte); + } + else if (value <= 0x1FFFFFFFFFULL) + { + byte = (value >> 32) & 0x1F; + byte |= (4 << 5); + Stream_Write_UINT8(s, byte); + byte = (value >> 24) & 0x1F; + Stream_Write_UINT8(s, byte); + byte = (value >> 16) & 0xFF; + Stream_Write_UINT8(s, byte); + byte = (value >> 8) & 0xFF; + Stream_Write_UINT8(s, byte); + byte = (value & 0xFF); + Stream_Write_UINT8(s, byte); + } + else if (value <= 0x1FFFFFFFFFFFULL) + { + byte = (value >> 40) & 0x1F; + byte |= (5 << 5); + Stream_Write_UINT8(s, byte); + byte = (value >> 32) & 0x1F; + Stream_Write_UINT8(s, byte); + byte = (value >> 24) & 0x1F; + Stream_Write_UINT8(s, byte); + byte = (value >> 16) & 0xFF; + Stream_Write_UINT8(s, byte); + byte = (value >> 8) & 0xFF; + Stream_Write_UINT8(s, byte); + byte = (value & 0xFF); + Stream_Write_UINT8(s, byte); + } + else if (value <= 0x1FFFFFFFFFFFFFULL) + { + byte = (value >> 48) & 0x1F; + byte |= (6 << 5); + Stream_Write_UINT8(s, byte); + byte = (value >> 40) & 0x1F; + Stream_Write_UINT8(s, byte); + byte = (value >> 32) & 0x1F; + Stream_Write_UINT8(s, byte); + byte = (value >> 24) & 0x1F; + Stream_Write_UINT8(s, byte); + byte = (value >> 16) & 0xFF; + Stream_Write_UINT8(s, byte); + byte = (value >> 8) & 0xFF; + Stream_Write_UINT8(s, byte); + byte = (value & 0xFF); + Stream_Write_UINT8(s, byte); + } + else if (value <= 0x1FFFFFFFFFFFFFFFULL) + { + byte = (value >> 56) & 0x1F; + byte |= (7 << 5); + Stream_Write_UINT8(s, byte); + byte = (value >> 48) & 0x1F; + Stream_Write_UINT8(s, byte); + byte = (value >> 40) & 0x1F; + Stream_Write_UINT8(s, byte); + byte = (value >> 32) & 0x1F; + Stream_Write_UINT8(s, byte); + byte = (value >> 24) & 0x1F; + Stream_Write_UINT8(s, byte); + byte = (value >> 16) & 0xFF; + Stream_Write_UINT8(s, byte); + byte = (value >> 8) & 0xFF; + Stream_Write_UINT8(s, byte); + byte = (value & 0xFF); + Stream_Write_UINT8(s, byte); + } + else + { + return FALSE; + } + + return TRUE; +} + +void touch_event_reset(RDPINPUT_TOUCH_EVENT* event) +{ + UINT16 i; + + for (i = 0; i < event->frameCount; i++) + touch_frame_reset(&event->frames[i]); + + free(event->frames); + event->frames = NULL; + event->frameCount = 0; +} + +void touch_frame_reset(RDPINPUT_TOUCH_FRAME* frame) +{ + free(frame->contacts); + frame->contacts = NULL; + frame->contactCount = 0; +} + +void pen_event_reset(RDPINPUT_PEN_EVENT* event) +{ + UINT16 i; + + for (i = 0; i < event->frameCount; i++) + pen_frame_reset(&event->frames[i]); + + free(event->frames); + event->frames = NULL; + event->frameCount = 0; +} + +void pen_frame_reset(RDPINPUT_PEN_FRAME* frame) +{ + free(frame->contacts); + frame->contacts = NULL; + frame->contactCount = 0; +} diff --git a/channels/rdpei/rdpei_common.h b/channels/rdpei/rdpei_common.h new file mode 100644 index 0000000..3a5362f --- /dev/null +++ b/channels/rdpei/rdpei_common.h @@ -0,0 +1,57 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Input Virtual Channel Extension + * + * Copyright 2013 Marc-Andre Moreau + * Copyright 2014 David Fort + * + * Licensed 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 FREERDP_CHANNEL_RDPEI_COMMON_H +#define FREERDP_CHANNEL_RDPEI_COMMON_H + +#include +#include +#include + +/** @brief input event ids */ +enum +{ + EVENTID_SC_READY = 0x0001, + EVENTID_CS_READY = 0x0002, + EVENTID_TOUCH = 0x0003, + EVENTID_SUSPEND_TOUCH = 0x0004, + EVENTID_RESUME_TOUCH = 0x0005, + EVENTID_DISMISS_HOVERING_CONTACT = 0x0006, + EVENTID_PEN = 0x0008 +}; + +BOOL rdpei_read_2byte_unsigned(wStream* s, UINT16* value); +BOOL rdpei_write_2byte_unsigned(wStream* s, UINT16 value); +BOOL rdpei_read_2byte_signed(wStream* s, INT16* value); +BOOL rdpei_write_2byte_signed(wStream* s, INT16 value); +BOOL rdpei_read_4byte_unsigned(wStream* s, UINT32* value); +BOOL rdpei_write_4byte_unsigned(wStream* s, UINT32 value); +BOOL rdpei_read_4byte_signed(wStream* s, INT32* value); +BOOL rdpei_write_4byte_signed(wStream* s, INT32 value); +BOOL rdpei_read_8byte_unsigned(wStream* s, UINT64* value); +BOOL rdpei_write_8byte_unsigned(wStream* s, UINT64 value); + +void touch_event_reset(RDPINPUT_TOUCH_EVENT* event); +void touch_frame_reset(RDPINPUT_TOUCH_FRAME* frame); + +void pen_event_reset(RDPINPUT_PEN_EVENT* event); +void pen_frame_reset(RDPINPUT_PEN_FRAME* frame); + +#endif /* FREERDP_CHANNEL_RDPEI_COMMON_H */ diff --git a/channels/rdpei/server/CMakeLists.txt b/channels/rdpei/server/CMakeLists.txt new file mode 100644 index 0000000..b2a464d --- /dev/null +++ b/channels/rdpei/server/CMakeLists.txt @@ -0,0 +1,35 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2014 Thincast Technologies Gmbh. +# Copyright 2014 David FORT +# +# Licensed 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. + +define_channel_server("rdpei") + +set(${MODULE_PREFIX}_SRCS + rdpei_main.c + rdpei_main.h + ../rdpei_common.c + ../rdpei_common.h +) + +add_channel_server_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} FALSE "VirtualChannelEntry") + +set(${MODULE_PREFIX}_LIBS ${${MODULE_PREFIX}_LIBS} winpr) + +target_link_libraries(${MODULE_NAME} ${${MODULE_PREFIX}_LIBS}) + + +set_property(TARGET ${MODULE_NAME} PROPERTY FOLDER "Channels/${CHANNEL_NAME}/Server") diff --git a/channels/rdpei/server/rdpei_main.c b/channels/rdpei/server/rdpei_main.c new file mode 100644 index 0000000..433b2cf --- /dev/null +++ b/channels/rdpei/server/rdpei_main.c @@ -0,0 +1,746 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Extended Input channel server-side implementation + * + * Copyright 2014 Thincast Technologies Gmbh. + * Copyright 2014 David FORT + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include + +#include "rdpei_main.h" +#include "../rdpei_common.h" +#include +#include + +/** @brief */ +enum RdpEiState +{ + STATE_INITIAL, + STATE_WAITING_CLIENT_READY, + STATE_WAITING_FRAME, + STATE_SUSPENDED, +}; + +struct _rdpei_server_private +{ + HANDLE channelHandle; + HANDLE eventHandle; + + UINT32 expectedBytes; + BOOL waitingHeaders; + wStream* inputStream; + wStream* outputStream; + + UINT16 currentMsgType; + + RDPINPUT_TOUCH_EVENT touchEvent; + RDPINPUT_PEN_EVENT penEvent; + + enum RdpEiState automataState; +}; + +RdpeiServerContext* rdpei_server_context_new(HANDLE vcm) +{ + RdpeiServerContext* ret = calloc(1, sizeof(*ret)); + RdpeiServerPrivate* priv; + + if (!ret) + return NULL; + + ret->priv = priv = calloc(1, sizeof(*ret->priv)); + if (!priv) + goto fail; + + priv->inputStream = Stream_New(NULL, 256); + if (!priv->inputStream) + goto fail; + + priv->outputStream = Stream_New(NULL, 200); + if (!priv->inputStream) + goto fail; + + ret->vcm = vcm; + rdpei_server_context_reset(ret); + return ret; + +fail: + rdpei_server_context_free(ret); + return NULL; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT rdpei_server_init(RdpeiServerContext* context) +{ + void* buffer = NULL; + DWORD bytesReturned; + RdpeiServerPrivate* priv = context->priv; + UINT32 channelId; + BOOL status = TRUE; + + priv->channelHandle = WTSVirtualChannelOpenEx(WTS_CURRENT_SESSION, RDPEI_DVC_CHANNEL_NAME, + WTS_CHANNEL_OPTION_DYNAMIC); + if (!priv->channelHandle) + { + WLog_ERR(TAG, "WTSVirtualChannelOpenEx failed!"); + return CHANNEL_RC_INITIALIZATION_ERROR; + } + + channelId = WTSChannelGetIdByHandle(priv->channelHandle); + + IFCALLRET(context->onChannelIdAssigned, status, context, channelId); + if (!status) + { + WLog_ERR(TAG, "context->onChannelIdAssigned failed!"); + goto out_close; + } + + if (!WTSVirtualChannelQuery(priv->channelHandle, WTSVirtualEventHandle, &buffer, + &bytesReturned) || + (bytesReturned != sizeof(HANDLE))) + { + WLog_ERR(TAG, + "WTSVirtualChannelQuery failed or invalid invalid returned size(%" PRIu32 ")!", + bytesReturned); + if (buffer) + WTSFreeMemory(buffer); + goto out_close; + } + CopyMemory(&priv->eventHandle, buffer, sizeof(HANDLE)); + WTSFreeMemory(buffer); + + return CHANNEL_RC_OK; + +out_close: + WTSVirtualChannelClose(priv->channelHandle); + return CHANNEL_RC_INITIALIZATION_ERROR; +} + +void rdpei_server_context_reset(RdpeiServerContext* context) +{ + RdpeiServerPrivate* priv = context->priv; + + priv->channelHandle = INVALID_HANDLE_VALUE; + priv->expectedBytes = RDPINPUT_HEADER_LENGTH; + priv->waitingHeaders = TRUE; + priv->automataState = STATE_INITIAL; + Stream_SetPosition(priv->inputStream, 0); +} + +void rdpei_server_context_free(RdpeiServerContext* context) +{ + RdpeiServerPrivate* priv; + + if (!context) + return; + priv = context->priv; + if (priv) + { + if (priv->channelHandle != INVALID_HANDLE_VALUE) + WTSVirtualChannelClose(priv->channelHandle); + Stream_Free(priv->inputStream, TRUE); + } + free(priv); + free(context); +} + +HANDLE rdpei_server_get_event_handle(RdpeiServerContext* context) +{ + return context->priv->eventHandle; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT read_cs_ready_message(RdpeiServerContext* context, wStream* s) +{ + UINT error = CHANNEL_RC_OK; + if (Stream_GetRemainingLength(s) < 10) + { + WLog_ERR(TAG, "Not enough data!"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT32(s, context->protocolFlags); + Stream_Read_UINT32(s, context->clientVersion); + Stream_Read_UINT16(s, context->maxTouchPoints); + + switch (context->clientVersion) + { + case RDPINPUT_PROTOCOL_V10: + case RDPINPUT_PROTOCOL_V101: + case RDPINPUT_PROTOCOL_V200: + case RDPINPUT_PROTOCOL_V300: + break; + default: + WLog_ERR(TAG, "unhandled RPDEI protocol version 0x%" PRIx32 "", context->clientVersion); + break; + } + + IFCALLRET(context->onClientReady, error, context); + if (error) + WLog_ERR(TAG, "context->onClientReady failed with error %" PRIu32 "", error); + + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT read_touch_contact_data(RdpeiServerContext* context, wStream* s, + RDPINPUT_CONTACT_DATA* contactData) +{ + UINT16 tmp; + WINPR_UNUSED(context); + if (Stream_GetRemainingLength(s) < 1) + { + WLog_ERR(TAG, "Not enough data!"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT8(s, contactData->contactId); + if (!rdpei_read_2byte_unsigned(s, &tmp) || !rdpei_read_4byte_signed(s, &contactData->x) || + !rdpei_read_4byte_signed(s, &contactData->y) || + !rdpei_read_4byte_unsigned(s, &contactData->contactFlags)) + { + WLog_ERR(TAG, "rdpei_read_ failed!"); + return ERROR_INTERNAL_ERROR; + } + contactData->fieldsPresent = tmp; + + if (contactData->fieldsPresent & CONTACT_DATA_CONTACTRECT_PRESENT) + { + INT16 tmp[4] = { 0 }; + if (!rdpei_read_2byte_signed(s, &tmp[0]) || !rdpei_read_2byte_signed(s, &tmp[1]) || + !rdpei_read_2byte_signed(s, &tmp[2]) || !rdpei_read_2byte_signed(s, &tmp[3])) + { + WLog_ERR(TAG, "rdpei_read_ failed!"); + return ERROR_INTERNAL_ERROR; + } + contactData->contactRectLeft = tmp[0]; + contactData->contactRectTop = tmp[1]; + contactData->contactRectRight = tmp[2]; + contactData->contactRectBottom = tmp[3]; + } + + if ((contactData->fieldsPresent & CONTACT_DATA_ORIENTATION_PRESENT) && + !rdpei_read_4byte_unsigned(s, &contactData->orientation)) + { + WLog_ERR(TAG, "rdpei_read_ failed!"); + return ERROR_INTERNAL_ERROR; + } + + if ((contactData->fieldsPresent & CONTACT_DATA_PRESSURE_PRESENT) && + !rdpei_read_4byte_unsigned(s, &contactData->pressure)) + { + WLog_ERR(TAG, "rdpei_read_ failed!"); + return ERROR_INTERNAL_ERROR; + } + + return CHANNEL_RC_OK; +} + +static UINT read_pen_contact(RdpeiServerContext* context, wStream* s, + RDPINPUT_PEN_CONTACT* contactData) +{ + WINPR_UNUSED(context); + if (Stream_GetRemainingLength(s) < 1) + { + WLog_ERR(TAG, "Not enough data!"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT8(s, contactData->deviceId); + if (!rdpei_read_2byte_unsigned(s, &contactData->fieldsPresent) || + !rdpei_read_4byte_signed(s, &contactData->x) || + !rdpei_read_4byte_signed(s, &contactData->y) || + !rdpei_read_4byte_unsigned(s, &contactData->contactFlags)) + { + WLog_ERR(TAG, "rdpei_read_ failed!"); + return ERROR_INTERNAL_ERROR; + } + + if (contactData->fieldsPresent & PEN_CONTACT_PENFLAGS_PRESENT) + { + if (!rdpei_read_4byte_unsigned(s, &contactData->penFlags)) + return ERROR_INVALID_DATA; + } + if (contactData->fieldsPresent & PEN_CONTACT_PRESSURE_PRESENT) + { + if (!rdpei_read_4byte_unsigned(s, &contactData->pressure)) + return ERROR_INVALID_DATA; + } + if (contactData->fieldsPresent & PEN_CONTACT_ROTATION_PRESENT) + { + if (!rdpei_read_2byte_unsigned(s, &contactData->rotation)) + return ERROR_INVALID_DATA; + } + if (contactData->fieldsPresent & PEN_CONTACT_TILTX_PRESENT) + { + if (!rdpei_read_2byte_signed(s, &contactData->tiltX)) + return ERROR_INVALID_DATA; + } + if (contactData->fieldsPresent & PEN_CONTACT_TILTY_PRESENT) + { + if (!rdpei_read_2byte_signed(s, &contactData->tiltY)) + return ERROR_INVALID_DATA; + } + + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT read_touch_frame(RdpeiServerContext* context, wStream* s, RDPINPUT_TOUCH_FRAME* frame) +{ + UINT32 i; + UINT16 tmp; + RDPINPUT_CONTACT_DATA* contact; + UINT error; + + if (!rdpei_read_2byte_unsigned(s, &tmp) || !rdpei_read_8byte_unsigned(s, &frame->frameOffset)) + { + WLog_ERR(TAG, "rdpei_read_ failed!"); + return ERROR_INTERNAL_ERROR; + } + frame->contactCount = tmp; + + frame->contacts = contact = calloc(frame->contactCount, sizeof(RDPINPUT_CONTACT_DATA)); + if (!frame->contacts) + { + WLog_ERR(TAG, "calloc failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + for (i = 0; i < frame->contactCount; i++, contact++) + { + if ((error = read_touch_contact_data(context, s, contact))) + { + WLog_ERR(TAG, "read_touch_contact_data failed with error %" PRIu32 "!", error); + frame->contactCount = i; + touch_frame_reset(frame); + return error; + } + } + return CHANNEL_RC_OK; +} + +static UINT read_pen_frame(RdpeiServerContext* context, wStream* s, RDPINPUT_PEN_FRAME* frame) +{ + UINT32 i; + RDPINPUT_PEN_CONTACT* contact; + UINT error; + + if (!rdpei_read_2byte_unsigned(s, &frame->contactCount) || + !rdpei_read_8byte_unsigned(s, &frame->frameOffset)) + { + WLog_ERR(TAG, "rdpei_read_ failed!"); + return ERROR_INTERNAL_ERROR; + } + + frame->contacts = contact = calloc(frame->contactCount, sizeof(RDPINPUT_CONTACT_DATA)); + if (!frame->contacts) + { + WLog_ERR(TAG, "calloc failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + for (i = 0; i < frame->contactCount; i++, contact++) + { + if ((error = read_pen_contact(context, s, contact))) + { + WLog_ERR(TAG, "read_touch_contact_data failed with error %" PRIu32 "!", error); + frame->contactCount = i; + pen_frame_reset(frame); + return error; + } + } + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT read_touch_event(RdpeiServerContext* context, wStream* s) +{ + UINT16 frameCount; + UINT32 i; + RDPINPUT_TOUCH_EVENT* event = &context->priv->touchEvent; + RDPINPUT_TOUCH_FRAME* frame; + UINT error = CHANNEL_RC_OK; + + if (!rdpei_read_4byte_unsigned(s, &event->encodeTime) || + !rdpei_read_2byte_unsigned(s, &frameCount)) + { + WLog_ERR(TAG, "rdpei_read_ failed!"); + return ERROR_INTERNAL_ERROR; + } + + event->frameCount = frameCount; + event->frames = frame = calloc(event->frameCount, sizeof(RDPINPUT_TOUCH_FRAME)); + if (!event->frames) + { + WLog_ERR(TAG, "calloc failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + for (i = 0; i < frameCount; i++, frame++) + { + if ((error = read_touch_frame(context, s, frame))) + { + WLog_ERR(TAG, "read_touch_contact_data failed with error %" PRIu32 "!", error); + event->frameCount = i; + goto out_cleanup; + } + } + + IFCALLRET(context->onTouchEvent, error, context, event); + if (error) + WLog_ERR(TAG, "context->onTouchEvent failed with error %" PRIu32 "", error); + +out_cleanup: + touch_event_reset(event); + return error; +} + +static UINT read_pen_event(RdpeiServerContext* context, wStream* s) +{ + UINT16 frameCount; + UINT32 i; + RDPINPUT_PEN_EVENT* event = &context->priv->penEvent; + RDPINPUT_PEN_FRAME* frame; + UINT error = CHANNEL_RC_OK; + + if (!rdpei_read_4byte_unsigned(s, &event->encodeTime) || + !rdpei_read_2byte_unsigned(s, &frameCount)) + { + WLog_ERR(TAG, "rdpei_read_ failed!"); + return ERROR_INTERNAL_ERROR; + } + + event->frameCount = frameCount; + event->frames = frame = calloc(event->frameCount, sizeof(RDPINPUT_PEN_FRAME)); + if (!event->frames) + { + WLog_ERR(TAG, "calloc failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + for (i = 0; i < frameCount; i++, frame++) + { + if ((error = read_pen_frame(context, s, frame))) + { + WLog_ERR(TAG, "read_pen_frame failed with error %" PRIu32 "!", error); + event->frameCount = i; + goto out_cleanup; + } + } + + IFCALLRET(context->onPenEvent, error, context, event); + if (error) + WLog_ERR(TAG, "context->onPenEvent failed with error %" PRIu32 "", error); + +out_cleanup: + pen_event_reset(event); + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT read_dismiss_hovering_contact(RdpeiServerContext* context, wStream* s) +{ + BYTE contactId; + UINT error = CHANNEL_RC_OK; + + if (Stream_GetRemainingLength(s) < 1) + { + WLog_ERR(TAG, "Not enough data!"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT8(s, contactId); + + IFCALLRET(context->onTouchReleased, error, context, contactId); + if (error) + WLog_ERR(TAG, "context->onTouchReleased failed with error %" PRIu32 "", error); + + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT rdpei_server_handle_messages(RdpeiServerContext* context) +{ + DWORD bytesReturned; + RdpeiServerPrivate* priv = context->priv; + wStream* s = priv->inputStream; + UINT error = CHANNEL_RC_OK; + + if (!WTSVirtualChannelRead(priv->channelHandle, 0, (PCHAR)Stream_Pointer(s), + priv->expectedBytes, &bytesReturned)) + { + if (GetLastError() == ERROR_NO_DATA) + return ERROR_READ_FAULT; + + WLog_DBG(TAG, "channel connection closed"); + return CHANNEL_RC_OK; + } + priv->expectedBytes -= bytesReturned; + Stream_Seek(s, bytesReturned); + + if (priv->expectedBytes) + return CHANNEL_RC_OK; + + Stream_SealLength(s); + Stream_SetPosition(s, 0); + + if (priv->waitingHeaders) + { + UINT32 pduLen; + + /* header case */ + Stream_Read_UINT16(s, priv->currentMsgType); + Stream_Read_UINT16(s, pduLen); + + if (pduLen < RDPINPUT_HEADER_LENGTH) + { + WLog_ERR(TAG, "invalid pduLength %" PRIu32 "", pduLen); + return ERROR_INVALID_DATA; + } + priv->expectedBytes = pduLen - RDPINPUT_HEADER_LENGTH; + priv->waitingHeaders = FALSE; + Stream_SetPosition(s, 0); + if (priv->expectedBytes) + { + if (!Stream_EnsureCapacity(s, priv->expectedBytes)) + { + WLog_ERR(TAG, "Stream_EnsureCapacity failed!"); + return CHANNEL_RC_NO_MEMORY; + } + return CHANNEL_RC_OK; + } + } + + /* when here we have the header + the body */ + switch (priv->currentMsgType) + { + case EVENTID_CS_READY: + if (priv->automataState != STATE_WAITING_CLIENT_READY) + { + WLog_ERR(TAG, "not expecting a CS_READY packet in this state(%d)", + priv->automataState); + return ERROR_INVALID_STATE; + } + + if ((error = read_cs_ready_message(context, s))) + { + WLog_ERR(TAG, "read_cs_ready_message failed with error %" PRIu32 "", error); + return error; + } + break; + + case EVENTID_TOUCH: + if ((error = read_touch_event(context, s))) + { + WLog_ERR(TAG, "read_touch_event failed with error %" PRIu32 "", error); + return error; + } + break; + case EVENTID_DISMISS_HOVERING_CONTACT: + if ((error = read_dismiss_hovering_contact(context, s))) + { + WLog_ERR(TAG, "read_dismiss_hovering_contact failed with error %" PRIu32 "", error); + return error; + } + break; + case EVENTID_PEN: + if ((error = read_pen_event(context, s))) + { + WLog_ERR(TAG, "read_pen_event failed with error %" PRIu32 "", error); + return error; + } + break; + default: + WLog_ERR(TAG, "unexpected message type 0x%" PRIx16 "", priv->currentMsgType); + } + + Stream_SetPosition(s, 0); + priv->waitingHeaders = TRUE; + priv->expectedBytes = RDPINPUT_HEADER_LENGTH; + return error; +} + +UINT rdpei_server_send_sc_ready(RdpeiServerContext* context, UINT32 version) +{ + return rdpei_server_send_sc_ready_ex(context, version, 0); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT rdpei_server_send_sc_ready_ex(RdpeiServerContext* context, UINT32 version, UINT32 features) +{ + ULONG written; + RdpeiServerPrivate* priv = context->priv; + UINT32 pduLen = 4; + + if (priv->automataState != STATE_INITIAL) + { + WLog_ERR(TAG, "called from unexpected state %d", priv->automataState); + return ERROR_INVALID_STATE; + } + + Stream_SetPosition(priv->outputStream, 0); + + if (version >= RDPINPUT_PROTOCOL_V300) + pduLen += 4; + + if (!Stream_EnsureCapacity(priv->outputStream, RDPINPUT_HEADER_LENGTH + pduLen)) + { + WLog_ERR(TAG, "Stream_EnsureCapacity failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + Stream_Write_UINT16(priv->outputStream, EVENTID_SC_READY); + Stream_Write_UINT32(priv->outputStream, RDPINPUT_HEADER_LENGTH + pduLen); + Stream_Write_UINT32(priv->outputStream, version); + if (version >= RDPINPUT_PROTOCOL_V300) + Stream_Write_UINT32(priv->outputStream, features); + + if (!WTSVirtualChannelWrite(priv->channelHandle, (PCHAR)Stream_Buffer(priv->outputStream), + Stream_GetPosition(priv->outputStream), &written)) + { + WLog_ERR(TAG, "WTSVirtualChannelWrite failed!"); + return ERROR_INTERNAL_ERROR; + } + + priv->automataState = STATE_WAITING_CLIENT_READY; + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT rdpei_server_suspend(RdpeiServerContext* context) +{ + ULONG written; + RdpeiServerPrivate* priv = context->priv; + + switch (priv->automataState) + { + case STATE_SUSPENDED: + WLog_ERR(TAG, "already suspended"); + return CHANNEL_RC_OK; + case STATE_WAITING_FRAME: + break; + default: + WLog_ERR(TAG, "called from unexpected state %d", priv->automataState); + return ERROR_INVALID_STATE; + } + + Stream_SetPosition(priv->outputStream, 0); + if (!Stream_EnsureCapacity(priv->outputStream, RDPINPUT_HEADER_LENGTH)) + { + WLog_ERR(TAG, "Stream_EnsureCapacity failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + Stream_Write_UINT16(priv->outputStream, EVENTID_SUSPEND_TOUCH); + Stream_Write_UINT32(priv->outputStream, RDPINPUT_HEADER_LENGTH); + + if (!WTSVirtualChannelWrite(priv->channelHandle, (PCHAR)Stream_Buffer(priv->outputStream), + Stream_GetPosition(priv->outputStream), &written)) + { + WLog_ERR(TAG, "WTSVirtualChannelWrite failed!"); + return ERROR_INTERNAL_ERROR; + } + + priv->automataState = STATE_SUSPENDED; + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT rdpei_server_resume(RdpeiServerContext* context) +{ + ULONG written; + RdpeiServerPrivate* priv = context->priv; + + switch (priv->automataState) + { + case STATE_WAITING_FRAME: + WLog_ERR(TAG, "not suspended"); + return CHANNEL_RC_OK; + case STATE_SUSPENDED: + break; + default: + WLog_ERR(TAG, "called from unexpected state %d", priv->automataState); + return ERROR_INVALID_STATE; + } + + Stream_SetPosition(priv->outputStream, 0); + if (!Stream_EnsureCapacity(priv->outputStream, RDPINPUT_HEADER_LENGTH)) + { + WLog_ERR(TAG, "Stream_EnsureCapacity failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + Stream_Write_UINT16(priv->outputStream, EVENTID_RESUME_TOUCH); + Stream_Write_UINT32(priv->outputStream, RDPINPUT_HEADER_LENGTH); + + if (!WTSVirtualChannelWrite(priv->channelHandle, (PCHAR)Stream_Buffer(priv->outputStream), + Stream_GetPosition(priv->outputStream), &written)) + { + WLog_ERR(TAG, "WTSVirtualChannelWrite failed!"); + return ERROR_INTERNAL_ERROR; + } + + priv->automataState = STATE_WAITING_FRAME; + return CHANNEL_RC_OK; +} diff --git a/channels/rdpei/server/rdpei_main.h b/channels/rdpei/server/rdpei_main.h new file mode 100644 index 0000000..cf3e3cb --- /dev/null +++ b/channels/rdpei/server/rdpei_main.h @@ -0,0 +1,32 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Extended Input channel server-side implementation + * + * Copyright 2014 David Fort + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger + * + * Licensed 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 FREERDP_CHANNEL_RDPEI_SERVER_MAIN_H +#define FREERDP_CHANNEL_RDPEI_SERVER_MAIN_H + +#include +#include +#include +#include + +#define TAG CHANNELS_TAG("rdpei.server") + +#endif /* FREERDP_CHANNEL_RDPEI_SERVER_MAIN_H */ diff --git a/channels/rdpgfx/CMakeLists.txt b/channels/rdpgfx/CMakeLists.txt new file mode 100644 index 0000000..04820de --- /dev/null +++ b/channels/rdpgfx/CMakeLists.txt @@ -0,0 +1,26 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2012 Marc-Andre Moreau +# +# Licensed 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. + +define_channel("rdpgfx") + +if(WITH_CLIENT_CHANNELS) + add_channel_client(${MODULE_PREFIX} ${CHANNEL_NAME}) +endif() + +if(WITH_SERVER_CHANNELS) + add_channel_server(${MODULE_PREFIX} ${CHANNEL_NAME}) +endif() diff --git a/channels/rdpgfx/ChannelOptions.cmake b/channels/rdpgfx/ChannelOptions.cmake new file mode 100644 index 0000000..acb8de8 --- /dev/null +++ b/channels/rdpgfx/ChannelOptions.cmake @@ -0,0 +1,13 @@ + +set(OPTION_DEFAULT OFF) +set(OPTION_CLIENT_DEFAULT ON) +set(OPTION_SERVER_DEFAULT OFF) + +define_channel_options(NAME "rdpgfx" TYPE "dynamic" + DESCRIPTION "Graphics Pipeline Extension" + SPECIFICATIONS "[MS-RDPEGFX]" + DEFAULT ${OPTION_DEFAULT}) + +define_channel_client_options(${OPTION_CLIENT_DEFAULT}) +define_channel_server_options(${OPTION_SERVER_DEFAULT}) + diff --git a/channels/rdpgfx/client/CMakeLists.txt b/channels/rdpgfx/client/CMakeLists.txt new file mode 100644 index 0000000..0de358a --- /dev/null +++ b/channels/rdpgfx/client/CMakeLists.txt @@ -0,0 +1,41 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2013 Marc-Andre Moreau +# +# Licensed 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. + +define_channel_client("rdpgfx") + +set(${MODULE_PREFIX}_SRCS + rdpgfx_main.c + rdpgfx_main.h + rdpgfx_codec.c + rdpgfx_codec.h + ../rdpgfx_common.c + ../rdpgfx_common.h) + +include_directories(..) + +add_channel_client_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} TRUE "DVCPluginEntry") + + + +target_link_libraries(${MODULE_NAME} winpr freerdp) + +if (WITH_DEBUG_SYMBOLS AND MSVC AND NOT BUILTIN_CHANNELS AND BUILD_SHARED_LIBS) + install(FILES ${CMAKE_BINARY_DIR}/${MODULE_NAME}.pdb DESTINATION ${FREERDP_ADDIN_PATH} COMPONENT symbols) +endif() + +set_property(TARGET ${MODULE_NAME} PROPERTY FOLDER "Channels/${CHANNEL_NAME}/Client") + diff --git a/channels/rdpgfx/client/rdpgfx_codec.c b/channels/rdpgfx/client/rdpgfx_codec.c new file mode 100644 index 0000000..d89278c --- /dev/null +++ b/channels/rdpgfx/client/rdpgfx_codec.c @@ -0,0 +1,309 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Graphics Pipeline Extension + * + * Copyright 2014 Marc-Andre Moreau + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include +#include + +#include "rdpgfx_common.h" + +#include "rdpgfx_codec.h" + +#define TAG CHANNELS_TAG("rdpgfx.client") + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpgfx_read_h264_metablock(RDPGFX_PLUGIN* gfx, wStream* s, RDPGFX_H264_METABLOCK* meta) +{ + UINT32 index; + RECTANGLE_16* regionRect; + RDPGFX_H264_QUANT_QUALITY* quantQualityVal; + UINT error = ERROR_INVALID_DATA; + meta->regionRects = NULL; + meta->quantQualityVals = NULL; + + if (Stream_GetRemainingLength(s) < 4) + { + WLog_ERR(TAG, "not enough data!"); + goto error_out; + } + + Stream_Read_UINT32(s, meta->numRegionRects); /* numRegionRects (4 bytes) */ + + if (Stream_GetRemainingLength(s) < (meta->numRegionRects * 8)) + { + WLog_ERR(TAG, "not enough data!"); + goto error_out; + } + + meta->regionRects = (RECTANGLE_16*)calloc(meta->numRegionRects, sizeof(RECTANGLE_16)); + + if (!meta->regionRects) + { + WLog_ERR(TAG, "malloc failed!"); + error = CHANNEL_RC_NO_MEMORY; + goto error_out; + } + + meta->quantQualityVals = + (RDPGFX_H264_QUANT_QUALITY*)calloc(meta->numRegionRects, sizeof(RDPGFX_H264_QUANT_QUALITY)); + + if (!meta->quantQualityVals) + { + WLog_ERR(TAG, "malloc failed!"); + error = CHANNEL_RC_NO_MEMORY; + goto error_out; + } + + WLog_DBG(TAG, "H264_METABLOCK: numRegionRects: %" PRIu32 "", meta->numRegionRects); + + for (index = 0; index < meta->numRegionRects; index++) + { + regionRect = &(meta->regionRects[index]); + + if ((error = rdpgfx_read_rect16(s, regionRect))) + { + WLog_ERR(TAG, "rdpgfx_read_rect16 failed with error %" PRIu32 "!", error); + goto error_out; + } + + WLog_DBG(TAG, + "regionRects[%" PRIu32 "]: left: %" PRIu16 " top: %" PRIu16 " right: %" PRIu16 + " bottom: %" PRIu16 "", + index, regionRect->left, regionRect->top, regionRect->right, regionRect->bottom); + } + + if (Stream_GetRemainingLength(s) < (meta->numRegionRects * 2)) + { + WLog_ERR(TAG, "not enough data!"); + error = ERROR_INVALID_DATA; + goto error_out; + } + + for (index = 0; index < meta->numRegionRects; index++) + { + quantQualityVal = &(meta->quantQualityVals[index]); + Stream_Read_UINT8(s, quantQualityVal->qpVal); /* qpVal (1 byte) */ + Stream_Read_UINT8(s, quantQualityVal->qualityVal); /* qualityVal (1 byte) */ + quantQualityVal->qp = quantQualityVal->qpVal & 0x3F; + quantQualityVal->r = (quantQualityVal->qpVal >> 6) & 1; + quantQualityVal->p = (quantQualityVal->qpVal >> 7) & 1; + WLog_DBG(TAG, + "quantQualityVals[%" PRIu32 "]: qp: %" PRIu8 " r: %" PRIu8 " p: %" PRIu8 + " qualityVal: %" PRIu8 "", + index, quantQualityVal->qp, quantQualityVal->r, quantQualityVal->p, + quantQualityVal->qualityVal); + } + + return CHANNEL_RC_OK; +error_out: + free(meta->regionRects); + meta->regionRects = NULL; + free(meta->quantQualityVals); + meta->quantQualityVals = NULL; + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpgfx_decode_AVC420(RDPGFX_PLUGIN* gfx, RDPGFX_SURFACE_COMMAND* cmd) +{ + UINT error; + wStream* s; + RDPGFX_AVC420_BITMAP_STREAM h264; + RdpgfxClientContext* context = (RdpgfxClientContext*)gfx->iface.pInterface; + s = Stream_New(cmd->data, cmd->length); + + if (!s) + { + WLog_ERR(TAG, "Stream_New failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + if ((error = rdpgfx_read_h264_metablock(gfx, s, &(h264.meta)))) + { + Stream_Free(s, FALSE); + WLog_ERR(TAG, "rdpgfx_read_h264_metablock failed with error %" PRIu32 "!", error); + return error; + } + + h264.data = Stream_Pointer(s); + h264.length = (UINT32)Stream_GetRemainingLength(s); + Stream_Free(s, FALSE); + cmd->extra = (void*)&h264; + + if (context) + { + IFCALLRET(context->SurfaceCommand, error, context, cmd); + + if (error) + WLog_ERR(TAG, "context->SurfaceCommand failed with error %" PRIu32 "", error); + } + + free(h264.meta.regionRects); + free(h264.meta.quantQualityVals); + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpgfx_decode_AVC444(RDPGFX_PLUGIN* gfx, RDPGFX_SURFACE_COMMAND* cmd) +{ + UINT error; + UINT32 tmp; + size_t pos1, pos2; + wStream* s; + RDPGFX_AVC444_BITMAP_STREAM h264 = { 0 }; + RdpgfxClientContext* context = (RdpgfxClientContext*)gfx->iface.pInterface; + s = Stream_New(cmd->data, cmd->length); + + if (!s) + { + WLog_ERR(TAG, "Stream_New failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + if (Stream_GetRemainingLength(s) < 4) + { + error = ERROR_INVALID_DATA; + goto fail; + } + + Stream_Read_UINT32(s, tmp); + h264.cbAvc420EncodedBitstream1 = tmp & 0x3FFFFFFFUL; + h264.LC = (tmp >> 30UL) & 0x03UL; + + if (h264.LC == 0x03) + { + error = ERROR_INVALID_DATA; + goto fail; + } + + pos1 = Stream_GetPosition(s); + + if ((error = rdpgfx_read_h264_metablock(gfx, s, &(h264.bitstream[0].meta)))) + { + WLog_ERR(TAG, "rdpgfx_read_h264_metablock failed with error %" PRIu32 "!", error); + goto fail; + } + + pos2 = Stream_GetPosition(s); + h264.bitstream[0].data = Stream_Pointer(s); + + if (h264.LC == 0) + { + tmp = h264.cbAvc420EncodedBitstream1 - pos2 + pos1; + + if (Stream_GetRemainingLength(s) < tmp) + { + error = ERROR_INVALID_DATA; + goto fail; + } + + h264.bitstream[0].length = tmp; + Stream_Seek(s, tmp); + + if ((error = rdpgfx_read_h264_metablock(gfx, s, &(h264.bitstream[1].meta)))) + { + WLog_ERR(TAG, "rdpgfx_read_h264_metablock failed with error %" PRIu32 "!", error); + goto fail; + } + + h264.bitstream[1].data = Stream_Pointer(s); + h264.bitstream[1].length = Stream_GetRemainingLength(s); + } + else + h264.bitstream[0].length = Stream_GetRemainingLength(s); + + cmd->extra = (void*)&h264; + + if (context) + { + IFCALLRET(context->SurfaceCommand, error, context, cmd); + + if (error) + WLog_ERR(TAG, "context->SurfaceCommand failed with error %" PRIu32 "", error); + } + +fail: + Stream_Free(s, FALSE); + free(h264.bitstream[0].meta.regionRects); + free(h264.bitstream[0].meta.quantQualityVals); + free(h264.bitstream[1].meta.regionRects); + free(h264.bitstream[1].meta.quantQualityVals); + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT rdpgfx_decode(RDPGFX_PLUGIN* gfx, RDPGFX_SURFACE_COMMAND* cmd) +{ + UINT error = CHANNEL_RC_OK; + RdpgfxClientContext* context = (RdpgfxClientContext*)gfx->iface.pInterface; + PROFILER_ENTER(context->SurfaceProfiler) + + switch (cmd->codecId) + { + case RDPGFX_CODECID_AVC420: + if ((error = rdpgfx_decode_AVC420(gfx, cmd))) + WLog_ERR(TAG, "rdpgfx_decode_AVC420 failed with error %" PRIu32 "", error); + + break; + + case RDPGFX_CODECID_AVC444: + case RDPGFX_CODECID_AVC444v2: + if ((error = rdpgfx_decode_AVC444(gfx, cmd))) + WLog_ERR(TAG, "rdpgfx_decode_AVC444 failed with error %" PRIu32 "", error); + + break; + + default: + if (context) + { + IFCALLRET(context->SurfaceCommand, error, context, cmd); + + if (error) + WLog_ERR(TAG, "context->SurfaceCommand failed with error %" PRIu32 "", error); + } + + break; + } + + PROFILER_EXIT(context->SurfaceProfiler) + return error; +} diff --git a/channels/rdpgfx/client/rdpgfx_codec.h b/channels/rdpgfx/client/rdpgfx_codec.h new file mode 100644 index 0000000..03d1bac --- /dev/null +++ b/channels/rdpgfx/client/rdpgfx_codec.h @@ -0,0 +1,35 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Graphics Pipeline Extension + * + * Copyright 2014 Marc-Andre Moreau + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger + * + * Licensed 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 FREERDP_CHANNEL_RDPGFX_CLIENT_CODEC_H +#define FREERDP_CHANNEL_RDPGFX_CLIENT_CODEC_H + +#include +#include + +#include +#include + +#include "rdpgfx_main.h" + +FREERDP_LOCAL UINT rdpgfx_decode(RDPGFX_PLUGIN* gfx, RDPGFX_SURFACE_COMMAND* cmd); + +#endif /* FREERDP_CHANNEL_RDPGFX_CLIENT_CODEC_H */ diff --git a/channels/rdpgfx/client/rdpgfx_main.c b/channels/rdpgfx/client/rdpgfx_main.c new file mode 100644 index 0000000..ceb4c18 --- /dev/null +++ b/channels/rdpgfx/client/rdpgfx_main.c @@ -0,0 +1,2219 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Graphics Pipeline Extension + * + * Copyright 2013-2014 Marc-Andre Moreau + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger + * Copyright 2016 Thincast Technologies GmbH + * Copyright 2016 Armin Novak + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "rdpgfx_common.h" +#include "rdpgfx_codec.h" + +#include "rdpgfx_main.h" + +#define TAG CHANNELS_TAG("rdpgfx.client") + +static void free_surfaces(RdpgfxClientContext* context, wHashTable* SurfaceTable) +{ + UINT error = 0; + ULONG_PTR* pKeys = NULL; + int count; + int index; + + count = HashTable_GetKeys(SurfaceTable, &pKeys); + + for (index = 0; index < count; index++) + { + RDPGFX_DELETE_SURFACE_PDU pdu; + pdu.surfaceId = ((UINT16)pKeys[index]) - 1; + + if (context) + { + IFCALLRET(context->DeleteSurface, error, context, &pdu); + + if (error) + { + WLog_ERR(TAG, "context->DeleteSurface failed with error %" PRIu32 "", error); + } + } + } + + free(pKeys); +} + +static void evict_cache_slots(RdpgfxClientContext* context, UINT16 MaxCacheSlots, void** CacheSlots) +{ + UINT16 index; + + for (index = 0; index < MaxCacheSlots; index++) + { + if (CacheSlots[index]) + { + RDPGFX_EVICT_CACHE_ENTRY_PDU pdu; + pdu.cacheSlot = (UINT16)index + 1; + + if (context && context->EvictCacheEntry) + { + context->EvictCacheEntry(context, &pdu); + } + + CacheSlots[index] = NULL; + } + } +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpgfx_send_caps_advertise_pdu(RdpgfxClientContext* context, + const RDPGFX_CAPS_ADVERTISE_PDU* pdu) +{ + UINT error = CHANNEL_RC_OK; + UINT16 index; + RDPGFX_HEADER header; + RDPGFX_CAPSET* capsSet; + RDPGFX_PLUGIN* gfx; + RDPGFX_CHANNEL_CALLBACK* callback; + wStream* s; + gfx = (RDPGFX_PLUGIN*)context->handle; + + if (!gfx || !gfx->listener_callback) + return ERROR_BAD_ARGUMENTS; + + callback = gfx->listener_callback->channel_callback; + + header.flags = 0; + header.cmdId = RDPGFX_CMDID_CAPSADVERTISE; + header.pduLength = RDPGFX_HEADER_SIZE + 2; + + for (index = 0; index < pdu->capsSetCount; index++) + { + capsSet = &(pdu->capsSets[index]); + header.pduLength += RDPGFX_CAPSET_BASE_SIZE + capsSet->length; + } + + DEBUG_RDPGFX(gfx->log, "SendCapsAdvertisePdu %" PRIu16 "", pdu->capsSetCount); + s = Stream_New(NULL, header.pduLength); + + if (!s) + { + WLog_ERR(TAG, "Stream_New failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + if ((error = rdpgfx_write_header(s, &header))) + goto fail; + + /* RDPGFX_CAPS_ADVERTISE_PDU */ + Stream_Write_UINT16(s, pdu->capsSetCount); /* capsSetCount (2 bytes) */ + + for (index = 0; index < pdu->capsSetCount; index++) + { + capsSet = &(pdu->capsSets[index]); + Stream_Write_UINT32(s, capsSet->version); /* version (4 bytes) */ + Stream_Write_UINT32(s, capsSet->length); /* capsDataLength (4 bytes) */ + Stream_Write_UINT32(s, capsSet->flags); /* capsData (4 bytes) */ + Stream_Zero(s, capsSet->length - 4); + } + + Stream_SealLength(s); + error = callback->channel->Write(callback->channel, (UINT32)Stream_Length(s), Stream_Buffer(s), + NULL); +fail: + Stream_Free(s, TRUE); + return error; +} + +static BOOL rdpgfx_is_capability_filtered(RDPGFX_PLUGIN* gfx, UINT32 caps) +{ + const UINT32 filter = gfx->capsFilter; + const UINT32 capList[] = { RDPGFX_CAPVERSION_8, RDPGFX_CAPVERSION_81, + RDPGFX_CAPVERSION_10, RDPGFX_CAPVERSION_101, + RDPGFX_CAPVERSION_102, RDPGFX_CAPVERSION_103, + RDPGFX_CAPVERSION_104, RDPGFX_CAPVERSION_105, + RDPGFX_CAPVERSION_106, RDPGFX_CAPVERSION_106_ERR, + RDPGFX_CAPVERSION_107 }; + UINT32 x; + + for (x = 0; x < ARRAYSIZE(capList); x++) + { + if (caps == capList[x]) + return (filter & (1 << x)) != 0; + } + + return TRUE; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpgfx_send_supported_caps(RDPGFX_CHANNEL_CALLBACK* callback) +{ + RDPGFX_PLUGIN* gfx; + RdpgfxClientContext* context; + RDPGFX_CAPSET* capsSet; + RDPGFX_CAPSET capsSets[RDPGFX_NUMBER_CAPSETS] = { 0 }; + RDPGFX_CAPS_ADVERTISE_PDU pdu = { 0 }; + + if (!callback) + return ERROR_BAD_ARGUMENTS; + + gfx = (RDPGFX_PLUGIN*)callback->plugin; + + if (!gfx) + return ERROR_BAD_CONFIGURATION; + + context = (RdpgfxClientContext*)gfx->iface.pInterface; + + if (!context) + return ERROR_BAD_CONFIGURATION; + + pdu.capsSetCount = 0; + pdu.capsSets = (RDPGFX_CAPSET*)capsSets; + + if (!rdpgfx_is_capability_filtered(gfx, RDPGFX_CAPVERSION_8)) + { + capsSet = &capsSets[pdu.capsSetCount++]; + capsSet->version = RDPGFX_CAPVERSION_8; + capsSet->length = 4; + capsSet->flags = 0; + + if (gfx->ThinClient) + capsSet->flags |= RDPGFX_CAPS_FLAG_THINCLIENT; + + /* in CAPVERSION_8 the spec says that we should not have both + * thinclient and smallcache (and thinclient implies a small cache) + */ + if (gfx->SmallCache && !gfx->ThinClient) + capsSet->flags |= RDPGFX_CAPS_FLAG_SMALL_CACHE; + } + + if (!rdpgfx_is_capability_filtered(gfx, RDPGFX_CAPVERSION_81)) + { + capsSet = &capsSets[pdu.capsSetCount++]; + capsSet->version = RDPGFX_CAPVERSION_81; + capsSet->length = 4; + capsSet->flags = 0; + + if (gfx->ThinClient) + capsSet->flags |= RDPGFX_CAPS_FLAG_THINCLIENT; + + if (gfx->SmallCache) + capsSet->flags |= RDPGFX_CAPS_FLAG_SMALL_CACHE; + +#ifdef WITH_GFX_H264 + + if (gfx->H264) + capsSet->flags |= RDPGFX_CAPS_FLAG_AVC420_ENABLED; + +#endif + } + + if (!gfx->H264 || gfx->AVC444) + { + UINT32 caps10Flags = 0; + + if (gfx->SmallCache) + caps10Flags |= RDPGFX_CAPS_FLAG_SMALL_CACHE; + +#ifdef WITH_GFX_H264 + + if (!gfx->AVC444) + caps10Flags |= RDPGFX_CAPS_FLAG_AVC_DISABLED; + +#else + caps10Flags |= RDPGFX_CAPS_FLAG_AVC_DISABLED; +#endif + + if (!rdpgfx_is_capability_filtered(gfx, RDPGFX_CAPVERSION_10)) + { + capsSet = &capsSets[pdu.capsSetCount++]; + capsSet->version = RDPGFX_CAPVERSION_10; + capsSet->length = 4; + capsSet->flags = caps10Flags; + } + + if (!rdpgfx_is_capability_filtered(gfx, RDPGFX_CAPVERSION_101)) + { + capsSet = &capsSets[pdu.capsSetCount++]; + capsSet->version = RDPGFX_CAPVERSION_101; + capsSet->length = 0x10; + capsSet->flags = 0; + } + + if (!rdpgfx_is_capability_filtered(gfx, RDPGFX_CAPVERSION_102)) + { + capsSet = &capsSets[pdu.capsSetCount++]; + capsSet->version = RDPGFX_CAPVERSION_102; + capsSet->length = 0x4; + capsSet->flags = caps10Flags; + } + + if (gfx->ThinClient) + { + if ((caps10Flags & RDPGFX_CAPS_FLAG_AVC_DISABLED) == 0) + caps10Flags |= RDPGFX_CAPS_FLAG_AVC_THINCLIENT; + } + + if (!rdpgfx_is_capability_filtered(gfx, RDPGFX_CAPVERSION_103)) + { + capsSet = &capsSets[pdu.capsSetCount++]; + capsSet->version = RDPGFX_CAPVERSION_103; + capsSet->length = 0x4; + capsSet->flags = caps10Flags & ~RDPGFX_CAPS_FLAG_SMALL_CACHE; + } + + if (!rdpgfx_is_capability_filtered(gfx, RDPGFX_CAPVERSION_104)) + { + capsSet = &capsSets[pdu.capsSetCount++]; + capsSet->version = RDPGFX_CAPVERSION_104; + capsSet->length = 0x4; + capsSet->flags = caps10Flags; + } + + if (!rdpgfx_is_capability_filtered(gfx, RDPGFX_CAPVERSION_105)) + { + capsSet = &capsSets[pdu.capsSetCount++]; + capsSet->version = RDPGFX_CAPVERSION_105; + capsSet->length = 0x4; + capsSet->flags = caps10Flags; + } + + if (!rdpgfx_is_capability_filtered(gfx, RDPGFX_CAPVERSION_106)) + { + capsSet = &capsSets[pdu.capsSetCount++]; + capsSet->version = RDPGFX_CAPVERSION_106; + capsSet->length = 0x4; + capsSet->flags = caps10Flags; + } + + if (!rdpgfx_is_capability_filtered(gfx, RDPGFX_CAPVERSION_106_ERR)) + { + capsSet = &capsSets[pdu.capsSetCount++]; + capsSet->version = RDPGFX_CAPVERSION_106_ERR; + capsSet->length = 0x4; + capsSet->flags = caps10Flags; + } + + if (!rdpgfx_is_capability_filtered(gfx, RDPGFX_CAPVERSION_107)) + { + capsSet = &capsSets[pdu.capsSetCount++]; + capsSet->version = RDPGFX_CAPVERSION_107; + capsSet->length = 0x4; + capsSet->flags = caps10Flags; +#if !defined(CAIRO_FOUND) && !defined(SWSCALE_FOUND) + capsSet->flags |= RDPGFX_CAPS_FLAG_SCALEDMAP_DISABLE; +#endif + } + } + + return IFCALLRESULT(ERROR_BAD_CONFIGURATION, context->CapsAdvertise, context, &pdu); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpgfx_recv_caps_confirm_pdu(RDPGFX_CHANNEL_CALLBACK* callback, wStream* s) +{ + RDPGFX_CAPSET capsSet; + RDPGFX_CAPS_CONFIRM_PDU pdu; + RDPGFX_PLUGIN* gfx = (RDPGFX_PLUGIN*)callback->plugin; + RdpgfxClientContext* context = (RdpgfxClientContext*)gfx->iface.pInterface; + pdu.capsSet = &capsSet; + + if (Stream_GetRemainingLength(s) < 12) + { + WLog_ERR(TAG, "not enough data!"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT32(s, capsSet.version); /* version (4 bytes) */ + Stream_Read_UINT32(s, capsSet.length); /* capsDataLength (4 bytes) */ + Stream_Read_UINT32(s, capsSet.flags); /* capsData (4 bytes) */ + gfx->ConnectionCaps = capsSet; + DEBUG_RDPGFX(gfx->log, "RecvCapsConfirmPdu: version: 0x%08" PRIX32 " flags: 0x%08" PRIX32 "", + capsSet.version, capsSet.flags); + + if (!context) + return ERROR_BAD_CONFIGURATION; + + return IFCALLRESULT(CHANNEL_RC_OK, context->CapsConfirm, context, &pdu); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpgfx_send_frame_acknowledge_pdu(RdpgfxClientContext* context, + const RDPGFX_FRAME_ACKNOWLEDGE_PDU* pdu) +{ + UINT error; + wStream* s; + RDPGFX_HEADER header; + RDPGFX_PLUGIN* gfx; + RDPGFX_CHANNEL_CALLBACK* callback; + + if (!context || !pdu) + return ERROR_BAD_ARGUMENTS; + + gfx = (RDPGFX_PLUGIN*)context->handle; + + if (!gfx || !gfx->listener_callback) + return ERROR_BAD_CONFIGURATION; + + callback = gfx->listener_callback->channel_callback; + + if (!callback) + return ERROR_BAD_CONFIGURATION; + + header.flags = 0; + header.cmdId = RDPGFX_CMDID_FRAMEACKNOWLEDGE; + header.pduLength = RDPGFX_HEADER_SIZE + 12; + DEBUG_RDPGFX(gfx->log, "SendFrameAcknowledgePdu: %" PRIu32 "", pdu->frameId); + s = Stream_New(NULL, header.pduLength); + + if (!s) + { + WLog_ERR(TAG, "Stream_New failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + if ((error = rdpgfx_write_header(s, &header))) + goto fail; + + /* RDPGFX_FRAME_ACKNOWLEDGE_PDU */ + Stream_Write_UINT32(s, pdu->queueDepth); /* queueDepth (4 bytes) */ + Stream_Write_UINT32(s, pdu->frameId); /* frameId (4 bytes) */ + Stream_Write_UINT32(s, pdu->totalFramesDecoded); /* totalFramesDecoded (4 bytes) */ + error = callback->channel->Write(callback->channel, (UINT32)Stream_Length(s), Stream_Buffer(s), + NULL); + + if (error == CHANNEL_RC_OK) /* frame successfully acked */ + gfx->UnacknowledgedFrames--; + +fail: + Stream_Free(s, TRUE); + return error; +} + +static UINT rdpgfx_send_qoe_frame_acknowledge_pdu(RdpgfxClientContext* context, + const RDPGFX_QOE_FRAME_ACKNOWLEDGE_PDU* pdu) +{ + UINT error; + wStream* s; + RDPGFX_HEADER header; + RDPGFX_CHANNEL_CALLBACK* callback; + RDPGFX_PLUGIN* gfx; + header.flags = 0; + header.cmdId = RDPGFX_CMDID_QOEFRAMEACKNOWLEDGE; + header.pduLength = RDPGFX_HEADER_SIZE + 12; + + if (!context || !pdu) + return ERROR_BAD_ARGUMENTS; + + gfx = (RDPGFX_PLUGIN*)context->handle; + + if (!gfx || !gfx->listener_callback) + return ERROR_BAD_CONFIGURATION; + + callback = gfx->listener_callback->channel_callback; + + if (!callback) + return ERROR_BAD_CONFIGURATION; + + DEBUG_RDPGFX(gfx->log, "SendQoeFrameAcknowledgePdu: %" PRIu32 "", pdu->frameId); + s = Stream_New(NULL, header.pduLength); + + if (!s) + { + WLog_ERR(TAG, "Stream_New failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + if ((error = rdpgfx_write_header(s, &header))) + goto fail; + + /* RDPGFX_FRAME_ACKNOWLEDGE_PDU */ + Stream_Write_UINT32(s, pdu->frameId); + Stream_Write_UINT32(s, pdu->timestamp); + Stream_Write_UINT16(s, pdu->timeDiffSE); + Stream_Write_UINT16(s, pdu->timeDiffEDR); + error = callback->channel->Write(callback->channel, (UINT32)Stream_Length(s), Stream_Buffer(s), + NULL); +fail: + Stream_Free(s, TRUE); + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpgfx_send_cache_import_offer_pdu(RdpgfxClientContext* context, + const RDPGFX_CACHE_IMPORT_OFFER_PDU* pdu) +{ + UINT16 index; + UINT error = CHANNEL_RC_OK; + wStream* s; + RDPGFX_PLUGIN* gfx; + RDPGFX_CHANNEL_CALLBACK* callback; + RDPGFX_HEADER header; + RDPGFX_CACHE_ENTRY_METADATA* cacheEntries; + + if (!context || !pdu) + return ERROR_BAD_ARGUMENTS; + + gfx = (RDPGFX_PLUGIN*)context->handle; + + if (!gfx || !gfx->listener_callback) + return ERROR_BAD_CONFIGURATION; + + callback = gfx->listener_callback->channel_callback; + + if (!callback) + return ERROR_BAD_CONFIGURATION; + + header.flags = 0; + header.cmdId = RDPGFX_CMDID_CACHEIMPORTOFFER; + header.pduLength = RDPGFX_HEADER_SIZE + 2 + pdu->cacheEntriesCount * 12; + DEBUG_RDPGFX(gfx->log, "SendCacheImportOfferPdu: cacheEntriesCount: %" PRIu16 "", + pdu->cacheEntriesCount); + s = Stream_New(NULL, header.pduLength); + + if (!s) + { + WLog_ERR(TAG, "Stream_New failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + if ((error = rdpgfx_write_header(s, &header))) + goto fail; + + if (pdu->cacheEntriesCount <= 0) + { + WLog_ERR(TAG, "Invalid cacheEntriesCount: %" PRIu16 "", pdu->cacheEntriesCount); + error = ERROR_INVALID_DATA; + goto fail; + } + + /* cacheEntriesCount (2 bytes) */ + Stream_Write_UINT16(s, pdu->cacheEntriesCount); + + for (index = 0; index < pdu->cacheEntriesCount; index++) + { + cacheEntries = &(pdu->cacheEntries[index]); + Stream_Write_UINT64(s, cacheEntries->cacheKey); /* cacheKey (8 bytes) */ + Stream_Write_UINT32(s, cacheEntries->bitmapLength); /* bitmapLength (4 bytes) */ + } + + error = callback->channel->Write(callback->channel, (UINT32)Stream_Length(s), Stream_Buffer(s), + NULL); + +fail: + Stream_Free(s, TRUE); + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpgfx_recv_reset_graphics_pdu(RDPGFX_CHANNEL_CALLBACK* callback, wStream* s) +{ + int pad; + UINT32 index; + MONITOR_DEF* monitor; + RDPGFX_RESET_GRAPHICS_PDU pdu; + RDPGFX_PLUGIN* gfx = (RDPGFX_PLUGIN*)callback->plugin; + RdpgfxClientContext* context = (RdpgfxClientContext*)gfx->iface.pInterface; + UINT error = CHANNEL_RC_OK; + GraphicsResetEventArgs graphicsReset; + + if (Stream_GetRemainingLength(s) < 12) + { + WLog_Print(gfx->log, WLOG_ERROR, "not enough data!"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT32(s, pdu.width); /* width (4 bytes) */ + Stream_Read_UINT32(s, pdu.height); /* height (4 bytes) */ + Stream_Read_UINT32(s, pdu.monitorCount); /* monitorCount (4 bytes) */ + + if (Stream_GetRemainingLength(s) < (pdu.monitorCount * 20)) + { + WLog_Print(gfx->log, WLOG_ERROR, "not enough data!"); + return ERROR_INVALID_DATA; + } + + pdu.monitorDefArray = (MONITOR_DEF*)calloc(pdu.monitorCount, sizeof(MONITOR_DEF)); + + if (!pdu.monitorDefArray) + { + WLog_Print(gfx->log, WLOG_ERROR, "calloc failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + for (index = 0; index < pdu.monitorCount; index++) + { + monitor = &(pdu.monitorDefArray[index]); + Stream_Read_UINT32(s, monitor->left); /* left (4 bytes) */ + Stream_Read_UINT32(s, monitor->top); /* top (4 bytes) */ + Stream_Read_UINT32(s, monitor->right); /* right (4 bytes) */ + Stream_Read_UINT32(s, monitor->bottom); /* bottom (4 bytes) */ + Stream_Read_UINT32(s, monitor->flags); /* flags (4 bytes) */ + } + + pad = 340 - (RDPGFX_HEADER_SIZE + 12 + (pdu.monitorCount * 20)); + + if (Stream_GetRemainingLength(s) < (size_t)pad) + { + WLog_Print(gfx->log, WLOG_ERROR, "Stream_GetRemainingLength failed!"); + free(pdu.monitorDefArray); + return CHANNEL_RC_NO_MEMORY; + } + + Stream_Seek(s, pad); /* pad (total size is 340 bytes) */ + DEBUG_RDPGFX(gfx->log, + "RecvResetGraphicsPdu: width: %" PRIu32 " height: %" PRIu32 " count: %" PRIu32 "", + pdu.width, pdu.height, pdu.monitorCount); + + for (index = 0; index < pdu.monitorCount; index++) + { + monitor = &(pdu.monitorDefArray[index]); + DEBUG_RDPGFX(gfx->log, + "RecvResetGraphicsPdu: monitor left:%" PRIi32 " top:%" PRIi32 " right:%" PRIi32 + " bottom:%" PRIi32 " flags:0x%" PRIx32 "", + monitor->left, monitor->top, monitor->right, monitor->bottom, monitor->flags); + } + + if (context) + { + IFCALLRET(context->ResetGraphics, error, context, &pdu); + + if (error) + WLog_Print(gfx->log, WLOG_ERROR, "context->ResetGraphics failed with error %" PRIu32 "", + error); + } + + /* some listeners may be interested (namely the display channel) */ + EventArgsInit(&graphicsReset, "libfreerdp"); + graphicsReset.width = pdu.width; + graphicsReset.height = pdu.height; + PubSub_OnGraphicsReset(gfx->rdpcontext->pubSub, gfx->rdpcontext, &graphicsReset); + free(pdu.monitorDefArray); + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpgfx_recv_evict_cache_entry_pdu(RDPGFX_CHANNEL_CALLBACK* callback, wStream* s) +{ + RDPGFX_EVICT_CACHE_ENTRY_PDU pdu; + RDPGFX_PLUGIN* gfx = (RDPGFX_PLUGIN*)callback->plugin; + RdpgfxClientContext* context = (RdpgfxClientContext*)gfx->iface.pInterface; + UINT error = CHANNEL_RC_OK; + + if (Stream_GetRemainingLength(s) < 2) + { + WLog_Print(gfx->log, WLOG_ERROR, "not enough data!"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT16(s, pdu.cacheSlot); /* cacheSlot (2 bytes) */ + WLog_Print(gfx->log, WLOG_DEBUG, "RecvEvictCacheEntryPdu: cacheSlot: %" PRIu16 "", + pdu.cacheSlot); + + if (context) + { + IFCALLRET(context->EvictCacheEntry, error, context, &pdu); + + if (error) + WLog_Print(gfx->log, WLOG_ERROR, + "context->EvictCacheEntry failed with error %" PRIu32 "", error); + } + + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpgfx_recv_cache_import_reply_pdu(RDPGFX_CHANNEL_CALLBACK* callback, wStream* s) +{ + UINT16 index; + RDPGFX_CACHE_IMPORT_REPLY_PDU pdu; + RDPGFX_PLUGIN* gfx = (RDPGFX_PLUGIN*)callback->plugin; + RdpgfxClientContext* context = (RdpgfxClientContext*)gfx->iface.pInterface; + UINT error = CHANNEL_RC_OK; + + if (Stream_GetRemainingLength(s) < 2) + { + WLog_Print(gfx->log, WLOG_ERROR, "not enough data!"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT16(s, pdu.importedEntriesCount); /* cacheSlot (2 bytes) */ + + if (Stream_GetRemainingLength(s) < (size_t)(pdu.importedEntriesCount * 2)) + { + WLog_Print(gfx->log, WLOG_ERROR, "not enough data!"); + return ERROR_INVALID_DATA; + } + + pdu.cacheSlots = (UINT16*)calloc(pdu.importedEntriesCount, sizeof(UINT16)); + + if (!pdu.cacheSlots) + { + WLog_Print(gfx->log, WLOG_ERROR, "calloc failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + for (index = 0; index < pdu.importedEntriesCount; index++) + { + Stream_Read_UINT16(s, pdu.cacheSlots[index]); /* cacheSlot (2 bytes) */ + } + + DEBUG_RDPGFX(gfx->log, "RecvCacheImportReplyPdu: importedEntriesCount: %" PRIu16 "", + pdu.importedEntriesCount); + + if (context) + { + IFCALLRET(context->CacheImportReply, error, context, &pdu); + + if (error) + WLog_Print(gfx->log, WLOG_ERROR, + "context->CacheImportReply failed with error %" PRIu32 "", error); + } + + free(pdu.cacheSlots); + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpgfx_recv_create_surface_pdu(RDPGFX_CHANNEL_CALLBACK* callback, wStream* s) +{ + RDPGFX_CREATE_SURFACE_PDU pdu; + RDPGFX_PLUGIN* gfx = (RDPGFX_PLUGIN*)callback->plugin; + RdpgfxClientContext* context = (RdpgfxClientContext*)gfx->iface.pInterface; + UINT error = CHANNEL_RC_OK; + + if (Stream_GetRemainingLength(s) < 7) + { + WLog_Print(gfx->log, WLOG_ERROR, "not enough data!"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT16(s, pdu.surfaceId); /* surfaceId (2 bytes) */ + Stream_Read_UINT16(s, pdu.width); /* width (2 bytes) */ + Stream_Read_UINT16(s, pdu.height); /* height (2 bytes) */ + Stream_Read_UINT8(s, pdu.pixelFormat); /* RDPGFX_PIXELFORMAT (1 byte) */ + DEBUG_RDPGFX(gfx->log, + "RecvCreateSurfacePdu: surfaceId: %" PRIu16 " width: %" PRIu16 " height: %" PRIu16 + " pixelFormat: 0x%02" PRIX8 "", + pdu.surfaceId, pdu.width, pdu.height, pdu.pixelFormat); + + if (context) + { + IFCALLRET(context->CreateSurface, error, context, &pdu); + + if (error) + WLog_Print(gfx->log, WLOG_ERROR, "context->CreateSurface failed with error %" PRIu32 "", + error); + } + + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpgfx_recv_delete_surface_pdu(RDPGFX_CHANNEL_CALLBACK* callback, wStream* s) +{ + RDPGFX_DELETE_SURFACE_PDU pdu; + RDPGFX_PLUGIN* gfx = (RDPGFX_PLUGIN*)callback->plugin; + RdpgfxClientContext* context = (RdpgfxClientContext*)gfx->iface.pInterface; + UINT error = CHANNEL_RC_OK; + + if (Stream_GetRemainingLength(s) < 2) + { + WLog_Print(gfx->log, WLOG_ERROR, "not enough data!"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT16(s, pdu.surfaceId); /* surfaceId (2 bytes) */ + DEBUG_RDPGFX(gfx->log, "RecvDeleteSurfacePdu: surfaceId: %" PRIu16 "", pdu.surfaceId); + + if (context) + { + IFCALLRET(context->DeleteSurface, error, context, &pdu); + + if (error) + WLog_Print(gfx->log, WLOG_ERROR, "context->DeleteSurface failed with error %" PRIu32 "", + error); + } + + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpgfx_recv_start_frame_pdu(RDPGFX_CHANNEL_CALLBACK* callback, wStream* s) +{ + RDPGFX_START_FRAME_PDU pdu; + RDPGFX_PLUGIN* gfx = (RDPGFX_PLUGIN*)callback->plugin; + RdpgfxClientContext* context = (RdpgfxClientContext*)gfx->iface.pInterface; + UINT error = CHANNEL_RC_OK; + + if (Stream_GetRemainingLength(s) < RDPGFX_START_FRAME_PDU_SIZE) + { + WLog_Print(gfx->log, WLOG_ERROR, "not enough data!"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT32(s, pdu.timestamp); /* timestamp (4 bytes) */ + Stream_Read_UINT32(s, pdu.frameId); /* frameId (4 bytes) */ + DEBUG_RDPGFX(gfx->log, "RecvStartFramePdu: frameId: %" PRIu32 " timestamp: 0x%08" PRIX32 "", + pdu.frameId, pdu.timestamp); + gfx->StartDecodingTime = GetTickCount64(); + + if (context) + { + IFCALLRET(context->StartFrame, error, context, &pdu); + + if (error) + WLog_Print(gfx->log, WLOG_ERROR, "context->StartFrame failed with error %" PRIu32 "", + error); + } + + gfx->UnacknowledgedFrames++; + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpgfx_recv_end_frame_pdu(RDPGFX_CHANNEL_CALLBACK* callback, wStream* s) +{ + RDPGFX_END_FRAME_PDU pdu; + RDPGFX_FRAME_ACKNOWLEDGE_PDU ack; + RDPGFX_PLUGIN* gfx = (RDPGFX_PLUGIN*)callback->plugin; + RdpgfxClientContext* context = (RdpgfxClientContext*)gfx->iface.pInterface; + UINT error = CHANNEL_RC_OK; + + if (Stream_GetRemainingLength(s) < RDPGFX_END_FRAME_PDU_SIZE) + { + WLog_Print(gfx->log, WLOG_ERROR, "not enough data!"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT32(s, pdu.frameId); /* frameId (4 bytes) */ + DEBUG_RDPGFX(gfx->log, "RecvEndFramePdu: frameId: %" PRIu32 "", pdu.frameId); + + if (context) + { + IFCALLRET(context->EndFrame, error, context, &pdu); + + if (error) + { + WLog_Print(gfx->log, WLOG_ERROR, "context->EndFrame failed with error %" PRIu32 "", + error); + return error; + } + } + + gfx->TotalDecodedFrames++; + + if (!gfx->sendFrameAcks) + return error; + + ack.frameId = pdu.frameId; + ack.totalFramesDecoded = gfx->TotalDecodedFrames; + + if (gfx->suspendFrameAcks) + { + ack.queueDepth = SUSPEND_FRAME_ACKNOWLEDGEMENT; + + if (gfx->TotalDecodedFrames == 1) + if ((error = rdpgfx_send_frame_acknowledge_pdu(context, &ack))) + WLog_Print(gfx->log, WLOG_ERROR, + "rdpgfx_send_frame_acknowledge_pdu failed with error %" PRIu32 "", + error); + } + else + { + ack.queueDepth = QUEUE_DEPTH_UNAVAILABLE; + + if ((error = rdpgfx_send_frame_acknowledge_pdu(context, &ack))) + WLog_Print(gfx->log, WLOG_ERROR, + "rdpgfx_send_frame_acknowledge_pdu failed with error %" PRIu32 "", error); + } + + switch (gfx->ConnectionCaps.version) + { + case RDPGFX_CAPVERSION_10: + case RDPGFX_CAPVERSION_102: + case RDPGFX_CAPVERSION_103: + case RDPGFX_CAPVERSION_104: + case RDPGFX_CAPVERSION_105: + case RDPGFX_CAPVERSION_106: + case RDPGFX_CAPVERSION_106_ERR: + case RDPGFX_CAPVERSION_107: + if (gfx->SendQoeAck) + { + RDPGFX_QOE_FRAME_ACKNOWLEDGE_PDU qoe; + UINT64 diff = (GetTickCount64() - gfx->StartDecodingTime); + + if (diff > 65000) + diff = 0; + + qoe.frameId = pdu.frameId; + qoe.timestamp = gfx->StartDecodingTime; + qoe.timeDiffSE = diff; + qoe.timeDiffEDR = 1; + + if ((error = rdpgfx_send_qoe_frame_acknowledge_pdu(context, &qoe))) + WLog_Print(gfx->log, WLOG_ERROR, + "rdpgfx_send_qoe_frame_acknowledge_pdu failed with error %" PRIu32 + "", + error); + } + + break; + + default: + break; + } + + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpgfx_recv_wire_to_surface_1_pdu(RDPGFX_CHANNEL_CALLBACK* callback, wStream* s) +{ + RDPGFX_SURFACE_COMMAND cmd; + RDPGFX_WIRE_TO_SURFACE_PDU_1 pdu; + RDPGFX_PLUGIN* gfx = (RDPGFX_PLUGIN*)callback->plugin; + UINT error; + + if (Stream_GetRemainingLength(s) < RDPGFX_WIRE_TO_SURFACE_PDU_1_SIZE) + { + WLog_Print(gfx->log, WLOG_ERROR, "not enough data!"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT16(s, pdu.surfaceId); /* surfaceId (2 bytes) */ + Stream_Read_UINT16(s, pdu.codecId); /* codecId (2 bytes) */ + Stream_Read_UINT8(s, pdu.pixelFormat); /* pixelFormat (1 byte) */ + + if ((error = rdpgfx_read_rect16(s, &(pdu.destRect)))) /* destRect (8 bytes) */ + { + WLog_Print(gfx->log, WLOG_ERROR, "rdpgfx_read_rect16 failed with error %" PRIu32 "", error); + return error; + } + + Stream_Read_UINT32(s, pdu.bitmapDataLength); /* bitmapDataLength (4 bytes) */ + + if (pdu.bitmapDataLength > Stream_GetRemainingLength(s)) + { + WLog_Print(gfx->log, WLOG_ERROR, "not enough data!"); + return ERROR_INVALID_DATA; + } + + pdu.bitmapData = Stream_Pointer(s); + Stream_Seek(s, pdu.bitmapDataLength); + + DEBUG_RDPGFX(gfx->log, + "RecvWireToSurface1Pdu: surfaceId: %" PRIu16 " codecId: %s (0x%04" PRIX16 + ") pixelFormat: 0x%02" PRIX8 " " + "destRect: left: %" PRIu16 " top: %" PRIu16 " right: %" PRIu16 " bottom: %" PRIu16 + " bitmapDataLength: %" PRIu32 "", + pdu.surfaceId, rdpgfx_get_codec_id_string(pdu.codecId), pdu.codecId, + pdu.pixelFormat, pdu.destRect.left, pdu.destRect.top, pdu.destRect.right, + pdu.destRect.bottom, pdu.bitmapDataLength); + cmd.surfaceId = pdu.surfaceId; + cmd.codecId = pdu.codecId; + cmd.contextId = 0; + + switch (pdu.pixelFormat) + { + case GFX_PIXEL_FORMAT_XRGB_8888: + cmd.format = PIXEL_FORMAT_BGRX32; + break; + + case GFX_PIXEL_FORMAT_ARGB_8888: + cmd.format = PIXEL_FORMAT_BGRA32; + break; + + default: + return ERROR_INVALID_DATA; + } + + cmd.left = pdu.destRect.left; + cmd.top = pdu.destRect.top; + cmd.right = pdu.destRect.right; + cmd.bottom = pdu.destRect.bottom; + cmd.width = cmd.right - cmd.left; + cmd.height = cmd.bottom - cmd.top; + cmd.length = pdu.bitmapDataLength; + cmd.data = pdu.bitmapData; + cmd.extra = NULL; + + if (cmd.right < cmd.left) + { + WLog_Print(gfx->log, WLOG_ERROR, "RecvWireToSurface1Pdu right=%" PRIu32 " < left=%" PRIu32, + cmd.right, cmd.left); + return ERROR_INVALID_DATA; + } + if (cmd.bottom < cmd.top) + { + WLog_Print(gfx->log, WLOG_ERROR, "RecvWireToSurface1Pdu bottom=%" PRIu32 " < top=%" PRIu32, + cmd.bottom, cmd.top); + return ERROR_INVALID_DATA; + } + + if ((error = rdpgfx_decode(gfx, &cmd))) + WLog_Print(gfx->log, WLOG_ERROR, "rdpgfx_decode failed with error %" PRIu32 "!", error); + + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpgfx_recv_wire_to_surface_2_pdu(RDPGFX_CHANNEL_CALLBACK* callback, wStream* s) +{ + RDPGFX_SURFACE_COMMAND cmd; + RDPGFX_WIRE_TO_SURFACE_PDU_2 pdu; + RDPGFX_PLUGIN* gfx = (RDPGFX_PLUGIN*)callback->plugin; + RdpgfxClientContext* context = (RdpgfxClientContext*)gfx->iface.pInterface; + UINT error = CHANNEL_RC_OK; + + if (Stream_GetRemainingLength(s) < RDPGFX_WIRE_TO_SURFACE_PDU_2_SIZE) + { + WLog_Print(gfx->log, WLOG_ERROR, "not enough data!"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT16(s, pdu.surfaceId); /* surfaceId (2 bytes) */ + Stream_Read_UINT16(s, pdu.codecId); /* codecId (2 bytes) */ + Stream_Read_UINT32(s, pdu.codecContextId); /* codecContextId (4 bytes) */ + Stream_Read_UINT8(s, pdu.pixelFormat); /* pixelFormat (1 byte) */ + Stream_Read_UINT32(s, pdu.bitmapDataLength); /* bitmapDataLength (4 bytes) */ + pdu.bitmapData = Stream_Pointer(s); + Stream_Seek(s, pdu.bitmapDataLength); + DEBUG_RDPGFX(gfx->log, + "RecvWireToSurface2Pdu: surfaceId: %" PRIu16 " codecId: %s (0x%04" PRIX16 ") " + "codecContextId: %" PRIu32 " pixelFormat: 0x%02" PRIX8 + " bitmapDataLength: %" PRIu32 "", + pdu.surfaceId, rdpgfx_get_codec_id_string(pdu.codecId), pdu.codecId, + pdu.codecContextId, pdu.pixelFormat, pdu.bitmapDataLength); + + cmd.surfaceId = pdu.surfaceId; + cmd.codecId = pdu.codecId; + cmd.contextId = pdu.codecContextId; + + switch (pdu.pixelFormat) + { + case GFX_PIXEL_FORMAT_XRGB_8888: + cmd.format = PIXEL_FORMAT_BGRX32; + break; + + case GFX_PIXEL_FORMAT_ARGB_8888: + cmd.format = PIXEL_FORMAT_BGRA32; + break; + + default: + return ERROR_INVALID_DATA; + } + + cmd.left = 0; + cmd.top = 0; + cmd.right = 0; + cmd.bottom = 0; + cmd.width = 0; + cmd.height = 0; + cmd.length = pdu.bitmapDataLength; + cmd.data = pdu.bitmapData; + cmd.extra = NULL; + + if (context) + { + IFCALLRET(context->SurfaceCommand, error, context, &cmd); + + if (error) + WLog_Print(gfx->log, WLOG_ERROR, + "context->SurfaceCommand failed with error %" PRIu32 "", error); + } + + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpgfx_recv_delete_encoding_context_pdu(RDPGFX_CHANNEL_CALLBACK* callback, wStream* s) +{ + RDPGFX_DELETE_ENCODING_CONTEXT_PDU pdu; + RDPGFX_PLUGIN* gfx = (RDPGFX_PLUGIN*)callback->plugin; + RdpgfxClientContext* context = (RdpgfxClientContext*)gfx->iface.pInterface; + UINT error = CHANNEL_RC_OK; + + if (Stream_GetRemainingLength(s) < 6) + { + WLog_Print(gfx->log, WLOG_ERROR, "not enough data!"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT16(s, pdu.surfaceId); /* surfaceId (2 bytes) */ + Stream_Read_UINT32(s, pdu.codecContextId); /* codecContextId (4 bytes) */ + + DEBUG_RDPGFX(gfx->log, + "RecvDeleteEncodingContextPdu: surfaceId: %" PRIu16 " codecContextId: %" PRIu32 "", + pdu.surfaceId, pdu.codecContextId); + + if (context) + { + IFCALLRET(context->DeleteEncodingContext, error, context, &pdu); + + if (error) + WLog_Print(gfx->log, WLOG_ERROR, + "context->DeleteEncodingContext failed with error %" PRIu32 "", error); + } + + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpgfx_recv_solid_fill_pdu(RDPGFX_CHANNEL_CALLBACK* callback, wStream* s) +{ + UINT16 index; + RECTANGLE_16* fillRect; + RDPGFX_SOLID_FILL_PDU pdu; + RDPGFX_PLUGIN* gfx = (RDPGFX_PLUGIN*)callback->plugin; + RdpgfxClientContext* context = (RdpgfxClientContext*)gfx->iface.pInterface; + UINT error; + + if (Stream_GetRemainingLength(s) < 8) + { + WLog_Print(gfx->log, WLOG_ERROR, "not enough data!"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT16(s, pdu.surfaceId); /* surfaceId (2 bytes) */ + + if ((error = rdpgfx_read_color32(s, &(pdu.fillPixel)))) /* fillPixel (4 bytes) */ + { + WLog_Print(gfx->log, WLOG_ERROR, "rdpgfx_read_color32 failed with error %" PRIu32 "!", + error); + return error; + } + + Stream_Read_UINT16(s, pdu.fillRectCount); /* fillRectCount (2 bytes) */ + + if (Stream_GetRemainingLength(s) < (size_t)(pdu.fillRectCount * 8)) + { + WLog_Print(gfx->log, WLOG_ERROR, "not enough data!"); + return ERROR_INVALID_DATA; + } + + pdu.fillRects = (RECTANGLE_16*)calloc(pdu.fillRectCount, sizeof(RECTANGLE_16)); + + if (!pdu.fillRects) + { + WLog_Print(gfx->log, WLOG_ERROR, "calloc failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + for (index = 0; index < pdu.fillRectCount; index++) + { + fillRect = &(pdu.fillRects[index]); + + if ((error = rdpgfx_read_rect16(s, fillRect))) + { + WLog_Print(gfx->log, WLOG_ERROR, "rdpgfx_read_rect16 failed with error %" PRIu32 "!", + error); + free(pdu.fillRects); + return error; + } + } + DEBUG_RDPGFX(gfx->log, "RecvSolidFillPdu: surfaceId: %" PRIu16 " fillRectCount: %" PRIu16 "", + pdu.surfaceId, pdu.fillRectCount); + + if (context) + { + IFCALLRET(context->SolidFill, error, context, &pdu); + + if (error) + WLog_Print(gfx->log, WLOG_ERROR, "context->SolidFill failed with error %" PRIu32 "", + error); + } + + free(pdu.fillRects); + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpgfx_recv_surface_to_surface_pdu(RDPGFX_CHANNEL_CALLBACK* callback, wStream* s) +{ + UINT16 index; + RDPGFX_POINT16* destPt; + RDPGFX_SURFACE_TO_SURFACE_PDU pdu; + RDPGFX_PLUGIN* gfx = (RDPGFX_PLUGIN*)callback->plugin; + RdpgfxClientContext* context = (RdpgfxClientContext*)gfx->iface.pInterface; + UINT error; + + if (Stream_GetRemainingLength(s) < 14) + { + WLog_Print(gfx->log, WLOG_ERROR, "not enough data!"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT16(s, pdu.surfaceIdSrc); /* surfaceIdSrc (2 bytes) */ + Stream_Read_UINT16(s, pdu.surfaceIdDest); /* surfaceIdDest (2 bytes) */ + + if ((error = rdpgfx_read_rect16(s, &(pdu.rectSrc)))) /* rectSrc (8 bytes ) */ + { + WLog_Print(gfx->log, WLOG_ERROR, "rdpgfx_read_rect16 failed with error %" PRIu32 "!", + error); + return error; + } + + Stream_Read_UINT16(s, pdu.destPtsCount); /* destPtsCount (2 bytes) */ + + if (Stream_GetRemainingLength(s) < (size_t)(pdu.destPtsCount * 4)) + { + WLog_Print(gfx->log, WLOG_ERROR, "not enough data!"); + return ERROR_INVALID_DATA; + } + + pdu.destPts = (RDPGFX_POINT16*)calloc(pdu.destPtsCount, sizeof(RDPGFX_POINT16)); + + if (!pdu.destPts) + { + WLog_Print(gfx->log, WLOG_ERROR, "calloc failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + for (index = 0; index < pdu.destPtsCount; index++) + { + destPt = &(pdu.destPts[index]); + + if ((error = rdpgfx_read_point16(s, destPt))) + { + WLog_Print(gfx->log, WLOG_ERROR, "rdpgfx_read_point16 failed with error %" PRIu32 "!", + error); + free(pdu.destPts); + return error; + } + } + + DEBUG_RDPGFX(gfx->log, + "RecvSurfaceToSurfacePdu: surfaceIdSrc: %" PRIu16 " surfaceIdDest: %" PRIu16 " " + "left: %" PRIu16 " top: %" PRIu16 " right: %" PRIu16 " bottom: %" PRIu16 + " destPtsCount: %" PRIu16 "", + pdu.surfaceIdSrc, pdu.surfaceIdDest, pdu.rectSrc.left, pdu.rectSrc.top, + pdu.rectSrc.right, pdu.rectSrc.bottom, pdu.destPtsCount); + + if (context) + { + IFCALLRET(context->SurfaceToSurface, error, context, &pdu); + + if (error) + WLog_Print(gfx->log, WLOG_ERROR, + "context->SurfaceToSurface failed with error %" PRIu32 "", error); + } + + free(pdu.destPts); + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpgfx_recv_surface_to_cache_pdu(RDPGFX_CHANNEL_CALLBACK* callback, wStream* s) +{ + RDPGFX_SURFACE_TO_CACHE_PDU pdu; + RDPGFX_PLUGIN* gfx = (RDPGFX_PLUGIN*)callback->plugin; + RdpgfxClientContext* context = (RdpgfxClientContext*)gfx->iface.pInterface; + UINT error; + + if (Stream_GetRemainingLength(s) < 20) + { + WLog_Print(gfx->log, WLOG_ERROR, "not enough data!"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT16(s, pdu.surfaceId); /* surfaceId (2 bytes) */ + Stream_Read_UINT64(s, pdu.cacheKey); /* cacheKey (8 bytes) */ + Stream_Read_UINT16(s, pdu.cacheSlot); /* cacheSlot (2 bytes) */ + + if ((error = rdpgfx_read_rect16(s, &(pdu.rectSrc)))) /* rectSrc (8 bytes ) */ + { + WLog_Print(gfx->log, WLOG_ERROR, "rdpgfx_read_rect16 failed with error %" PRIu32 "!", + error); + return error; + } + + DEBUG_RDPGFX(gfx->log, + "RecvSurfaceToCachePdu: surfaceId: %" PRIu16 " cacheKey: 0x%016" PRIX64 + " cacheSlot: %" PRIu16 " " + "left: %" PRIu16 " top: %" PRIu16 " right: %" PRIu16 " bottom: %" PRIu16 "", + pdu.surfaceId, pdu.cacheKey, pdu.cacheSlot, pdu.rectSrc.left, pdu.rectSrc.top, + pdu.rectSrc.right, pdu.rectSrc.bottom); + + if (context) + { + IFCALLRET(context->SurfaceToCache, error, context, &pdu); + + if (error) + WLog_Print(gfx->log, WLOG_ERROR, + "context->SurfaceToCache failed with error %" PRIu32 "", error); + } + + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpgfx_recv_cache_to_surface_pdu(RDPGFX_CHANNEL_CALLBACK* callback, wStream* s) +{ + UINT16 index; + RDPGFX_POINT16* destPt; + RDPGFX_CACHE_TO_SURFACE_PDU pdu; + RDPGFX_PLUGIN* gfx = (RDPGFX_PLUGIN*)callback->plugin; + RdpgfxClientContext* context = (RdpgfxClientContext*)gfx->iface.pInterface; + UINT error = CHANNEL_RC_OK; + + if (Stream_GetRemainingLength(s) < 6) + { + WLog_Print(gfx->log, WLOG_ERROR, "not enough data!"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT16(s, pdu.cacheSlot); /* cacheSlot (2 bytes) */ + Stream_Read_UINT16(s, pdu.surfaceId); /* surfaceId (2 bytes) */ + Stream_Read_UINT16(s, pdu.destPtsCount); /* destPtsCount (2 bytes) */ + + if (Stream_GetRemainingLength(s) < (size_t)(pdu.destPtsCount * 4)) + { + WLog_Print(gfx->log, WLOG_ERROR, "not enough data!"); + return ERROR_INVALID_DATA; + } + + pdu.destPts = (RDPGFX_POINT16*)calloc(pdu.destPtsCount, sizeof(RDPGFX_POINT16)); + + if (!pdu.destPts) + { + WLog_Print(gfx->log, WLOG_ERROR, "calloc failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + for (index = 0; index < pdu.destPtsCount; index++) + { + destPt = &(pdu.destPts[index]); + + if ((error = rdpgfx_read_point16(s, destPt))) + { + WLog_Print(gfx->log, WLOG_ERROR, "rdpgfx_read_point16 failed with error %" PRIu32 "", + error); + free(pdu.destPts); + return error; + } + } + + DEBUG_RDPGFX(gfx->log, + "RdpGfxRecvCacheToSurfacePdu: cacheSlot: %" PRIu16 " surfaceId: %" PRIu16 + " destPtsCount: %" PRIu16 "", + pdu.cacheSlot, pdu.surfaceId, pdu.destPtsCount); + + if (context) + { + IFCALLRET(context->CacheToSurface, error, context, &pdu); + + if (error) + WLog_Print(gfx->log, WLOG_ERROR, + "context->CacheToSurface failed with error %" PRIu32 "", error); + } + + free(pdu.destPts); + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpgfx_recv_map_surface_to_output_pdu(RDPGFX_CHANNEL_CALLBACK* callback, wStream* s) +{ + RDPGFX_MAP_SURFACE_TO_OUTPUT_PDU pdu; + RDPGFX_PLUGIN* gfx = (RDPGFX_PLUGIN*)callback->plugin; + RdpgfxClientContext* context = (RdpgfxClientContext*)gfx->iface.pInterface; + UINT error = CHANNEL_RC_OK; + + if (Stream_GetRemainingLength(s) < 12) + { + WLog_Print(gfx->log, WLOG_ERROR, "not enough data!"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT16(s, pdu.surfaceId); /* surfaceId (2 bytes) */ + Stream_Read_UINT16(s, pdu.reserved); /* reserved (2 bytes) */ + Stream_Read_UINT32(s, pdu.outputOriginX); /* outputOriginX (4 bytes) */ + Stream_Read_UINT32(s, pdu.outputOriginY); /* outputOriginY (4 bytes) */ + DEBUG_RDPGFX(gfx->log, + "RecvMapSurfaceToOutputPdu: surfaceId: %" PRIu16 " outputOriginX: %" PRIu32 + " outputOriginY: %" PRIu32 "", + pdu.surfaceId, pdu.outputOriginX, pdu.outputOriginY); + + if (context) + { + IFCALLRET(context->MapSurfaceToOutput, error, context, &pdu); + + if (error) + WLog_Print(gfx->log, WLOG_ERROR, + "context->MapSurfaceToOutput failed with error %" PRIu32 "", error); + } + + return error; +} + +static UINT rdpgfx_recv_map_surface_to_scaled_output_pdu(RDPGFX_CHANNEL_CALLBACK* callback, + wStream* s) +{ + RDPGFX_MAP_SURFACE_TO_SCALED_OUTPUT_PDU pdu; + RDPGFX_PLUGIN* gfx = (RDPGFX_PLUGIN*)callback->plugin; + RdpgfxClientContext* context = (RdpgfxClientContext*)gfx->iface.pInterface; + UINT error = CHANNEL_RC_OK; + + if (Stream_GetRemainingLength(s) < 20) + { + WLog_Print(gfx->log, WLOG_ERROR, "not enough data!"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT16(s, pdu.surfaceId); /* surfaceId (2 bytes) */ + Stream_Read_UINT16(s, pdu.reserved); /* reserved (2 bytes) */ + Stream_Read_UINT32(s, pdu.outputOriginX); /* outputOriginX (4 bytes) */ + Stream_Read_UINT32(s, pdu.outputOriginY); /* outputOriginY (4 bytes) */ + Stream_Read_UINT32(s, pdu.targetWidth); /* targetWidth (4 bytes) */ + Stream_Read_UINT32(s, pdu.targetHeight); /* targetHeight (4 bytes) */ + DEBUG_RDPGFX(gfx->log, + "RecvMapSurfaceToScaledOutputPdu: surfaceId: %" PRIu16 " outputOriginX: %" PRIu32 + " outputOriginY: %" PRIu32 " targetWidth: %" PRIu32 " targetHeight: %" PRIu32, + pdu.surfaceId, pdu.outputOriginX, pdu.outputOriginY, pdu.targetWidth, + pdu.targetHeight); + + if (context) + { + IFCALLRET(context->MapSurfaceToScaledOutput, error, context, &pdu); + + if (error) + WLog_Print(gfx->log, WLOG_ERROR, + "context->MapSurfaceToScaledOutput failed with error %" PRIu32 "", error); + } + + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpgfx_recv_map_surface_to_window_pdu(RDPGFX_CHANNEL_CALLBACK* callback, wStream* s) +{ + RDPGFX_MAP_SURFACE_TO_WINDOW_PDU pdu; + RDPGFX_PLUGIN* gfx = (RDPGFX_PLUGIN*)callback->plugin; + RdpgfxClientContext* context = (RdpgfxClientContext*)gfx->iface.pInterface; + UINT error = CHANNEL_RC_OK; + + if (Stream_GetRemainingLength(s) < 18) + { + WLog_Print(gfx->log, WLOG_ERROR, "not enough data!"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT16(s, pdu.surfaceId); /* surfaceId (2 bytes) */ + Stream_Read_UINT64(s, pdu.windowId); /* windowId (8 bytes) */ + Stream_Read_UINT32(s, pdu.mappedWidth); /* mappedWidth (4 bytes) */ + Stream_Read_UINT32(s, pdu.mappedHeight); /* mappedHeight (4 bytes) */ + DEBUG_RDPGFX(gfx->log, + "RecvMapSurfaceToWindowPdu: surfaceId: %" PRIu16 " windowId: 0x%016" PRIX64 + " mappedWidth: %" PRIu32 " mappedHeight: %" PRIu32 "", + pdu.surfaceId, pdu.windowId, pdu.mappedWidth, pdu.mappedHeight); + + if (context && context->MapSurfaceToWindow) + { + IFCALLRET(context->MapSurfaceToWindow, error, context, &pdu); + + if (error) + WLog_Print(gfx->log, WLOG_ERROR, + "context->MapSurfaceToWindow failed with error %" PRIu32 "", error); + } + + return error; +} + +static UINT rdpgfx_recv_map_surface_to_scaled_window_pdu(RDPGFX_CHANNEL_CALLBACK* callback, + wStream* s) +{ + RDPGFX_MAP_SURFACE_TO_SCALED_WINDOW_PDU pdu; + RDPGFX_PLUGIN* gfx = (RDPGFX_PLUGIN*)callback->plugin; + RdpgfxClientContext* context = (RdpgfxClientContext*)gfx->iface.pInterface; + UINT error = CHANNEL_RC_OK; + + if (Stream_GetRemainingLength(s) < 26) + { + WLog_Print(gfx->log, WLOG_ERROR, "not enough data!"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT16(s, pdu.surfaceId); /* surfaceId (2 bytes) */ + Stream_Read_UINT64(s, pdu.windowId); /* windowId (8 bytes) */ + Stream_Read_UINT32(s, pdu.mappedWidth); /* mappedWidth (4 bytes) */ + Stream_Read_UINT32(s, pdu.mappedHeight); /* mappedHeight (4 bytes) */ + Stream_Read_UINT32(s, pdu.targetWidth); /* targetWidth (4 bytes) */ + Stream_Read_UINT32(s, pdu.targetHeight); /* targetHeight (4 bytes) */ + DEBUG_RDPGFX(gfx->log, + "RecvMapSurfaceToScaledWindowPdu: surfaceId: %" PRIu16 " windowId: 0x%016" PRIX64 + " mappedWidth: %" PRIu32 " mappedHeight: %" PRIu32 " targetWidth: %" PRIu32 + " targetHeight: %" PRIu32 "", + pdu.surfaceId, pdu.windowId, pdu.mappedWidth, pdu.mappedHeight, pdu.targetWidth, + pdu.targetHeight); + + if (context && context->MapSurfaceToScaledWindow) + { + IFCALLRET(context->MapSurfaceToScaledWindow, error, context, &pdu); + + if (error) + WLog_Print(gfx->log, WLOG_ERROR, + "context->MapSurfaceToScaledWindow failed with error %" PRIu32 "", error); + } + + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpgfx_recv_pdu(RDPGFX_CHANNEL_CALLBACK* callback, wStream* s) +{ + size_t beg, end; + RDPGFX_HEADER header; + UINT error; + RDPGFX_PLUGIN* gfx = (RDPGFX_PLUGIN*)callback->plugin; + beg = Stream_GetPosition(s); + + if ((error = rdpgfx_read_header(s, &header))) + { + WLog_Print(gfx->log, WLOG_ERROR, "rdpgfx_read_header failed with error %" PRIu32 "!", + error); + return error; + } + + DEBUG_RDPGFX( + gfx->log, "cmdId: %s (0x%04" PRIX16 ") flags: 0x%04" PRIX16 " pduLength: %" PRIu32 "", + rdpgfx_get_cmd_id_string(header.cmdId), header.cmdId, header.flags, header.pduLength); + + switch (header.cmdId) + { + case RDPGFX_CMDID_WIRETOSURFACE_1: + if ((error = rdpgfx_recv_wire_to_surface_1_pdu(callback, s))) + WLog_Print(gfx->log, WLOG_ERROR, + "rdpgfx_recv_wire_to_surface_1_pdu failed with error %" PRIu32 "!", + error); + + break; + + case RDPGFX_CMDID_WIRETOSURFACE_2: + if ((error = rdpgfx_recv_wire_to_surface_2_pdu(callback, s))) + WLog_Print(gfx->log, WLOG_ERROR, + "rdpgfx_recv_wire_to_surface_2_pdu failed with error %" PRIu32 "!", + error); + + break; + + case RDPGFX_CMDID_DELETEENCODINGCONTEXT: + if ((error = rdpgfx_recv_delete_encoding_context_pdu(callback, s))) + WLog_Print(gfx->log, WLOG_ERROR, + "rdpgfx_recv_delete_encoding_context_pdu failed with error %" PRIu32 "!", + error); + + break; + + case RDPGFX_CMDID_SOLIDFILL: + if ((error = rdpgfx_recv_solid_fill_pdu(callback, s))) + WLog_Print(gfx->log, WLOG_ERROR, + "rdpgfx_recv_solid_fill_pdu failed with error %" PRIu32 "!", error); + + break; + + case RDPGFX_CMDID_SURFACETOSURFACE: + if ((error = rdpgfx_recv_surface_to_surface_pdu(callback, s))) + WLog_Print(gfx->log, WLOG_ERROR, + "rdpgfx_recv_surface_to_surface_pdu failed with error %" PRIu32 "!", + error); + + break; + + case RDPGFX_CMDID_SURFACETOCACHE: + if ((error = rdpgfx_recv_surface_to_cache_pdu(callback, s))) + WLog_Print(gfx->log, WLOG_ERROR, + "rdpgfx_recv_surface_to_cache_pdu failed with error %" PRIu32 "!", + error); + + break; + + case RDPGFX_CMDID_CACHETOSURFACE: + if ((error = rdpgfx_recv_cache_to_surface_pdu(callback, s))) + WLog_Print(gfx->log, WLOG_ERROR, + "rdpgfx_recv_cache_to_surface_pdu failed with error %" PRIu32 "!", + error); + + break; + + case RDPGFX_CMDID_EVICTCACHEENTRY: + if ((error = rdpgfx_recv_evict_cache_entry_pdu(callback, s))) + WLog_Print(gfx->log, WLOG_ERROR, + "rdpgfx_recv_evict_cache_entry_pdu failed with error %" PRIu32 "!", + error); + + break; + + case RDPGFX_CMDID_CREATESURFACE: + if ((error = rdpgfx_recv_create_surface_pdu(callback, s))) + WLog_Print(gfx->log, WLOG_ERROR, + "rdpgfx_recv_create_surface_pdu failed with error %" PRIu32 "!", error); + + break; + + case RDPGFX_CMDID_DELETESURFACE: + if ((error = rdpgfx_recv_delete_surface_pdu(callback, s))) + WLog_Print(gfx->log, WLOG_ERROR, + "rdpgfx_recv_delete_surface_pdu failed with error %" PRIu32 "!", error); + + break; + + case RDPGFX_CMDID_STARTFRAME: + if ((error = rdpgfx_recv_start_frame_pdu(callback, s))) + WLog_Print(gfx->log, WLOG_ERROR, + "rdpgfx_recv_start_frame_pdu failed with error %" PRIu32 "!", error); + + break; + + case RDPGFX_CMDID_ENDFRAME: + if ((error = rdpgfx_recv_end_frame_pdu(callback, s))) + WLog_Print(gfx->log, WLOG_ERROR, + "rdpgfx_recv_end_frame_pdu failed with error %" PRIu32 "!", error); + + break; + + case RDPGFX_CMDID_RESETGRAPHICS: + if ((error = rdpgfx_recv_reset_graphics_pdu(callback, s))) + WLog_Print(gfx->log, WLOG_ERROR, + "rdpgfx_recv_reset_graphics_pdu failed with error %" PRIu32 "!", error); + + break; + + case RDPGFX_CMDID_MAPSURFACETOOUTPUT: + if ((error = rdpgfx_recv_map_surface_to_output_pdu(callback, s))) + WLog_Print(gfx->log, WLOG_ERROR, + "rdpgfx_recv_map_surface_to_output_pdu failed with error %" PRIu32 "!", + error); + + break; + + case RDPGFX_CMDID_CACHEIMPORTREPLY: + if ((error = rdpgfx_recv_cache_import_reply_pdu(callback, s))) + WLog_Print(gfx->log, WLOG_ERROR, + "rdpgfx_recv_cache_import_reply_pdu failed with error %" PRIu32 "!", + error); + + break; + + case RDPGFX_CMDID_CAPSCONFIRM: + if ((error = rdpgfx_recv_caps_confirm_pdu(callback, s))) + WLog_Print(gfx->log, WLOG_ERROR, + "rdpgfx_recv_caps_confirm_pdu failed with error %" PRIu32 "!", error); + + break; + + case RDPGFX_CMDID_MAPSURFACETOWINDOW: + if ((error = rdpgfx_recv_map_surface_to_window_pdu(callback, s))) + WLog_Print(gfx->log, WLOG_ERROR, + "rdpgfx_recv_map_surface_to_window_pdu failed with error %" PRIu32 "!", + error); + + break; + + case RDPGFX_CMDID_MAPSURFACETOSCALEDWINDOW: + if ((error = rdpgfx_recv_map_surface_to_scaled_window_pdu(callback, s))) + WLog_Print(gfx->log, WLOG_ERROR, + "rdpgfx_recv_map_surface_to_scaled_window_pdu failed with error %" PRIu32 + "!", + error); + + break; + + case RDPGFX_CMDID_MAPSURFACETOSCALEDOUTPUT: + if ((error = rdpgfx_recv_map_surface_to_scaled_output_pdu(callback, s))) + WLog_Print(gfx->log, WLOG_ERROR, + "rdpgfx_recv_map_surface_to_scaled_output_pdu failed with error %" PRIu32 + "!", + error); + + break; + + default: + error = CHANNEL_RC_BAD_PROC; + break; + } + + if (error) + { + WLog_Print(gfx->log, WLOG_ERROR, "Error while processing GFX cmdId: %s (0x%04" PRIX16 ")", + rdpgfx_get_cmd_id_string(header.cmdId), header.cmdId); + return error; + } + + end = Stream_GetPosition(s); + + if (end != (beg + header.pduLength)) + { + WLog_Print(gfx->log, WLOG_ERROR, + "Unexpected gfx pdu end: Actual: %d, Expected: %" PRIu32 "", end, + (beg + header.pduLength)); + Stream_SetPosition(s, (beg + header.pduLength)); + } + + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpgfx_on_data_received(IWTSVirtualChannelCallback* pChannelCallback, wStream* data) +{ + wStream* s; + int status = 0; + UINT32 DstSize = 0; + BYTE* pDstData = NULL; + RDPGFX_CHANNEL_CALLBACK* callback = (RDPGFX_CHANNEL_CALLBACK*)pChannelCallback; + RDPGFX_PLUGIN* gfx = (RDPGFX_PLUGIN*)callback->plugin; + UINT error = CHANNEL_RC_OK; + status = zgfx_decompress(gfx->zgfx, Stream_Pointer(data), Stream_GetRemainingLength(data), + &pDstData, &DstSize, 0); + + if (status < 0) + { + WLog_Print(gfx->log, WLOG_ERROR, "zgfx_decompress failure! status: %d", status); + return ERROR_INTERNAL_ERROR; + } + + s = Stream_New(pDstData, DstSize); + + if (!s) + { + WLog_Print(gfx->log, WLOG_ERROR, "calloc failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + while (Stream_GetPosition(s) < Stream_Length(s)) + { + if ((error = rdpgfx_recv_pdu(callback, s))) + { + WLog_Print(gfx->log, WLOG_ERROR, "rdpgfx_recv_pdu failed with error %" PRIu32 "!", + error); + break; + } + } + + Stream_Free(s, TRUE); + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpgfx_on_open(IWTSVirtualChannelCallback* pChannelCallback) +{ + RDPGFX_CHANNEL_CALLBACK* callback = (RDPGFX_CHANNEL_CALLBACK*)pChannelCallback; + RDPGFX_PLUGIN* gfx = (RDPGFX_PLUGIN*)callback->plugin; + RdpgfxClientContext* context = (RdpgfxClientContext*)gfx->iface.pInterface; + UINT error = CHANNEL_RC_OK; + BOOL do_caps_advertise = TRUE; + gfx->sendFrameAcks = TRUE; + + if (context) + { + IFCALLRET(context->OnOpen, error, context, &do_caps_advertise, &gfx->sendFrameAcks); + + if (error) + WLog_Print(gfx->log, WLOG_ERROR, "context->OnOpen failed with error %" PRIu32 "", + error); + } + + if (do_caps_advertise) + error = rdpgfx_send_supported_caps(callback); + + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpgfx_on_close(IWTSVirtualChannelCallback* pChannelCallback) +{ + RDPGFX_CHANNEL_CALLBACK* callback = (RDPGFX_CHANNEL_CALLBACK*)pChannelCallback; + RDPGFX_PLUGIN* gfx = (RDPGFX_PLUGIN*)callback->plugin; + RdpgfxClientContext* context = (RdpgfxClientContext*)gfx->iface.pInterface; + + DEBUG_RDPGFX(gfx->log, "OnClose"); + free_surfaces(context, gfx->SurfaceTable); + evict_cache_slots(context, gfx->MaxCacheSlots, gfx->CacheSlots); + + free(callback); + gfx->UnacknowledgedFrames = 0; + gfx->TotalDecodedFrames = 0; + + if (context) + { + IFCALL(context->OnClose, context); + } + + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpgfx_on_new_channel_connection(IWTSListenerCallback* pListenerCallback, + IWTSVirtualChannel* pChannel, BYTE* Data, + BOOL* pbAccept, + IWTSVirtualChannelCallback** ppCallback) +{ + RDPGFX_CHANNEL_CALLBACK* callback; + RDPGFX_LISTENER_CALLBACK* listener_callback = (RDPGFX_LISTENER_CALLBACK*)pListenerCallback; + callback = (RDPGFX_CHANNEL_CALLBACK*)calloc(1, sizeof(RDPGFX_CHANNEL_CALLBACK)); + + if (!callback) + { + WLog_ERR(TAG, "calloc failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + callback->iface.OnDataReceived = rdpgfx_on_data_received; + callback->iface.OnOpen = rdpgfx_on_open; + callback->iface.OnClose = rdpgfx_on_close; + callback->plugin = listener_callback->plugin; + callback->channel_mgr = listener_callback->channel_mgr; + callback->channel = pChannel; + listener_callback->channel_callback = callback; + *ppCallback = (IWTSVirtualChannelCallback*)callback; + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpgfx_plugin_initialize(IWTSPlugin* pPlugin, IWTSVirtualChannelManager* pChannelMgr) +{ + UINT error; + RDPGFX_PLUGIN* gfx = (RDPGFX_PLUGIN*)pPlugin; + if (gfx->initialized) + { + WLog_ERR(TAG, "[%s] channel initialized twice, aborting", RDPGFX_DVC_CHANNEL_NAME); + return ERROR_INVALID_DATA; + } + gfx->listener_callback = (RDPGFX_LISTENER_CALLBACK*)calloc(1, sizeof(RDPGFX_LISTENER_CALLBACK)); + + if (!gfx->listener_callback) + { + WLog_Print(gfx->log, WLOG_ERROR, "calloc failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + gfx->listener_callback->iface.OnNewChannelConnection = rdpgfx_on_new_channel_connection; + gfx->listener_callback->plugin = pPlugin; + gfx->listener_callback->channel_mgr = pChannelMgr; + error = pChannelMgr->CreateListener(pChannelMgr, RDPGFX_DVC_CHANNEL_NAME, 0, + &gfx->listener_callback->iface, &(gfx->listener)); + gfx->listener->pInterface = gfx->iface.pInterface; + DEBUG_RDPGFX(gfx->log, "Initialize"); + + gfx->initialized = error == CHANNEL_RC_OK; + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpgfx_plugin_terminated(IWTSPlugin* pPlugin) +{ + RDPGFX_PLUGIN* gfx = (RDPGFX_PLUGIN*)pPlugin; + RdpgfxClientContext* context = (RdpgfxClientContext*)gfx->iface.pInterface; + DEBUG_RDPGFX(gfx->log, "Terminated"); + if (gfx && gfx->listener_callback) + { + IWTSVirtualChannelManager* mgr = gfx->listener_callback->channel_mgr; + if (mgr) + IFCALL(mgr->DestroyListener, mgr, gfx->listener); + } + rdpgfx_client_context_free(context); + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpgfx_set_surface_data(RdpgfxClientContext* context, UINT16 surfaceId, void* pData) +{ + ULONG_PTR key; + RDPGFX_PLUGIN* gfx = (RDPGFX_PLUGIN*)context->handle; + key = ((ULONG_PTR)surfaceId) + 1; + + if (pData) + HashTable_Add(gfx->SurfaceTable, (void*)key, pData); + else + HashTable_Remove(gfx->SurfaceTable, (void*)key); + + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpgfx_get_surface_ids(RdpgfxClientContext* context, UINT16** ppSurfaceIds, + UINT16* count_out) +{ + int count; + int index; + UINT16* pSurfaceIds; + ULONG_PTR* pKeys = NULL; + RDPGFX_PLUGIN* gfx = (RDPGFX_PLUGIN*)context->handle; + count = HashTable_GetKeys(gfx->SurfaceTable, &pKeys); + + if (count < 1) + { + *count_out = 0; + return CHANNEL_RC_OK; + } + + pSurfaceIds = (UINT16*)calloc(count, sizeof(UINT16)); + + if (!pSurfaceIds) + { + WLog_Print(gfx->log, WLOG_ERROR, "calloc failed!"); + free(pKeys); + return CHANNEL_RC_NO_MEMORY; + } + + for (index = 0; index < count; index++) + { + pSurfaceIds[index] = pKeys[index] - 1; + } + + free(pKeys); + *ppSurfaceIds = pSurfaceIds; + *count_out = (UINT16)count; + return CHANNEL_RC_OK; +} + +static void* rdpgfx_get_surface_data(RdpgfxClientContext* context, UINT16 surfaceId) +{ + ULONG_PTR key; + void* pData = NULL; + RDPGFX_PLUGIN* gfx = (RDPGFX_PLUGIN*)context->handle; + key = ((ULONG_PTR)surfaceId) + 1; + pData = HashTable_GetItemValue(gfx->SurfaceTable, (void*)key); + return pData; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpgfx_set_cache_slot_data(RdpgfxClientContext* context, UINT16 cacheSlot, void* pData) +{ + RDPGFX_PLUGIN* gfx = (RDPGFX_PLUGIN*)context->handle; + + /* Microsoft uses 1-based indexing for the egfx bitmap cache ! */ + if (cacheSlot == 0 || cacheSlot > gfx->MaxCacheSlots) + { + WLog_ERR(TAG, "%s: invalid cache slot %" PRIu16 ", must be between 1 and %" PRIu16 "", + __FUNCTION__, cacheSlot, gfx->MaxCacheSlots); + return ERROR_INVALID_INDEX; + } + + gfx->CacheSlots[cacheSlot - 1] = pData; + return CHANNEL_RC_OK; +} + +static void* rdpgfx_get_cache_slot_data(RdpgfxClientContext* context, UINT16 cacheSlot) +{ + void* pData = NULL; + RDPGFX_PLUGIN* gfx = (RDPGFX_PLUGIN*)context->handle; + + /* Microsoft uses 1-based indexing for the egfx bitmap cache ! */ + if (cacheSlot == 0 || cacheSlot > gfx->MaxCacheSlots) + { + WLog_ERR(TAG, "%s: invalid cache slot %" PRIu16 ", must be between 1 and %" PRIu16 "", + __FUNCTION__, cacheSlot, gfx->MaxCacheSlots); + return NULL; + } + + pData = gfx->CacheSlots[cacheSlot - 1]; + return pData; +} + +#ifdef BUILTIN_CHANNELS +#define DVCPluginEntry rdpgfx_DVCPluginEntry +#else +#define DVCPluginEntry FREERDP_API DVCPluginEntry +#endif + +RdpgfxClientContext* rdpgfx_client_context_new(rdpSettings* settings) +{ + RDPGFX_PLUGIN* gfx; + RdpgfxClientContext* context; + + gfx = (RDPGFX_PLUGIN*)calloc(1, sizeof(RDPGFX_PLUGIN)); + + if (!gfx) + { + WLog_ERR(TAG, "calloc failed!"); + return NULL; + } + + gfx->log = WLog_Get(TAG); + + if (!gfx->log) + { + free(gfx); + WLog_ERR(TAG, "Failed to acquire reference to WLog %s", TAG); + return NULL; + } + + gfx->settings = settings; + gfx->rdpcontext = ((freerdp*)gfx->settings->instance)->context; + gfx->SurfaceTable = HashTable_New(TRUE); + + if (!gfx->SurfaceTable) + { + free(gfx); + WLog_ERR(TAG, "HashTable_New failed!"); + return NULL; + } + + gfx->ThinClient = gfx->settings->GfxThinClient; + gfx->SmallCache = gfx->settings->GfxSmallCache; + gfx->Progressive = gfx->settings->GfxProgressive; + gfx->ProgressiveV2 = gfx->settings->GfxProgressiveV2; + gfx->H264 = gfx->settings->GfxH264; + gfx->AVC444 = gfx->settings->GfxAVC444; + gfx->SendQoeAck = gfx->settings->GfxSendQoeAck; + gfx->capsFilter = gfx->settings->GfxCapsFilter; + + if (gfx->H264) + gfx->SmallCache = TRUE; + + gfx->MaxCacheSlots = gfx->SmallCache ? 4096 : 25600; + context = (RdpgfxClientContext*)calloc(1, sizeof(RdpgfxClientContext)); + + if (!context) + { + free(gfx); + WLog_ERR(TAG, "calloc failed!"); + return NULL; + } + + context->handle = (void*)gfx; + context->GetSurfaceIds = rdpgfx_get_surface_ids; + context->SetSurfaceData = rdpgfx_set_surface_data; + context->GetSurfaceData = rdpgfx_get_surface_data; + context->SetCacheSlotData = rdpgfx_set_cache_slot_data; + context->GetCacheSlotData = rdpgfx_get_cache_slot_data; + context->CapsAdvertise = rdpgfx_send_caps_advertise_pdu; + context->FrameAcknowledge = rdpgfx_send_frame_acknowledge_pdu; + context->CacheImportOffer = rdpgfx_send_cache_import_offer_pdu; + context->QoeFrameAcknowledge = rdpgfx_send_qoe_frame_acknowledge_pdu; + + gfx->iface.pInterface = (void*)context; + gfx->zgfx = zgfx_context_new(FALSE); + + if (!gfx->zgfx) + { + free(gfx); + free(context); + WLog_ERR(TAG, "zgfx_context_new failed!"); + return NULL; + } + + return context; +} + +void rdpgfx_client_context_free(RdpgfxClientContext* context) +{ + + RDPGFX_PLUGIN* gfx; + + if (!context) + return; + + gfx = (RDPGFX_PLUGIN*)context->handle; + + free_surfaces(context, gfx->SurfaceTable); + evict_cache_slots(context, gfx->MaxCacheSlots, gfx->CacheSlots); + + if (gfx->listener_callback) + { + free(gfx->listener_callback); + gfx->listener_callback = NULL; + } + + if (gfx->zgfx) + { + zgfx_context_free(gfx->zgfx); + gfx->zgfx = NULL; + } + + HashTable_Free(gfx->SurfaceTable); + free(context); + free(gfx); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT DVCPluginEntry(IDRDYNVC_ENTRY_POINTS* pEntryPoints) +{ + UINT error = CHANNEL_RC_OK; + RDPGFX_PLUGIN* gfx; + RdpgfxClientContext* context; + gfx = (RDPGFX_PLUGIN*)pEntryPoints->GetPlugin(pEntryPoints, "rdpgfx"); + + if (!gfx) + { + context = + rdpgfx_client_context_new((rdpSettings*)pEntryPoints->GetRdpSettings(pEntryPoints)); + + if (!context) + { + WLog_ERR(TAG, "rdpgfx_client_context_new failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + gfx = (RDPGFX_PLUGIN*)context->handle; + + gfx->iface.Initialize = rdpgfx_plugin_initialize; + gfx->iface.Connected = NULL; + gfx->iface.Disconnected = NULL; + gfx->iface.Terminated = rdpgfx_plugin_terminated; + + error = pEntryPoints->RegisterPlugin(pEntryPoints, "rdpgfx", (IWTSPlugin*)gfx); + } + + return error; +} diff --git a/channels/rdpgfx/client/rdpgfx_main.h b/channels/rdpgfx/client/rdpgfx_main.h new file mode 100644 index 0000000..760d1be --- /dev/null +++ b/channels/rdpgfx/client/rdpgfx_main.h @@ -0,0 +1,92 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Graphics Pipeline Extension + * + * Copyright 2013-2014 Marc-Andre Moreau + * + * Licensed 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 FREERDP_CHANNEL_RDPGFX_CLIENT_MAIN_H +#define FREERDP_CHANNEL_RDPGFX_CLIENT_MAIN_H + +#include +#include +#include + +#include +#include + +#include +#include +#include +#include + +struct _RDPGFX_CHANNEL_CALLBACK +{ + IWTSVirtualChannelCallback iface; + + IWTSPlugin* plugin; + IWTSVirtualChannelManager* channel_mgr; + IWTSVirtualChannel* channel; +}; +typedef struct _RDPGFX_CHANNEL_CALLBACK RDPGFX_CHANNEL_CALLBACK; + +struct _RDPGFX_LISTENER_CALLBACK +{ + IWTSListenerCallback iface; + + IWTSPlugin* plugin; + IWTSVirtualChannelManager* channel_mgr; + RDPGFX_CHANNEL_CALLBACK* channel_callback; +}; +typedef struct _RDPGFX_LISTENER_CALLBACK RDPGFX_LISTENER_CALLBACK; + +struct _RDPGFX_PLUGIN +{ + IWTSPlugin iface; + + IWTSListener* listener; + RDPGFX_LISTENER_CALLBACK* listener_callback; + + rdpSettings* settings; + + BOOL ThinClient; + BOOL SmallCache; + BOOL Progressive; + BOOL ProgressiveV2; + BOOL H264; + BOOL AVC444; + UINT32 capsFilter; + + ZGFX_CONTEXT* zgfx; + UINT32 UnacknowledgedFrames; + UINT32 TotalDecodedFrames; + UINT64 StartDecodingTime; + BOOL suspendFrameAcks; + BOOL sendFrameAcks; + + wHashTable* SurfaceTable; + + UINT16 MaxCacheSlots; + void* CacheSlots[25600]; + rdpContext* rdpcontext; + + wLog* log; + RDPGFX_CAPSET ConnectionCaps; + BOOL SendQoeAck; + BOOL initialized; +}; +typedef struct _RDPGFX_PLUGIN RDPGFX_PLUGIN; + +#endif /* FREERDP_CHANNEL_RDPGFX_CLIENT_MAIN_H */ diff --git a/channels/rdpgfx/rdpgfx_common.c b/channels/rdpgfx/rdpgfx_common.c new file mode 100644 index 0000000..03fc97c --- /dev/null +++ b/channels/rdpgfx/rdpgfx_common.c @@ -0,0 +1,251 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Graphics Pipeline Extension + * + * Copyright 2013 Marc-Andre Moreau + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include +#include + +#define TAG CHANNELS_TAG("rdpgfx.common") + +#include "rdpgfx_common.h" + +static const char* RDPGFX_CMDID_STRINGS[] = { "RDPGFX_CMDID_UNUSED_0000", + "RDPGFX_CMDID_WIRETOSURFACE_1", + "RDPGFX_CMDID_WIRETOSURFACE_2", + "RDPGFX_CMDID_DELETEENCODINGCONTEXT", + "RDPGFX_CMDID_SOLIDFILL", + "RDPGFX_CMDID_SURFACETOSURFACE", + "RDPGFX_CMDID_SURFACETOCACHE", + "RDPGFX_CMDID_CACHETOSURFACE", + "RDPGFX_CMDID_EVICTCACHEENTRY", + "RDPGFX_CMDID_CREATESURFACE", + "RDPGFX_CMDID_DELETESURFACE", + "RDPGFX_CMDID_STARTFRAME", + "RDPGFX_CMDID_ENDFRAME", + "RDPGFX_CMDID_FRAMEACKNOWLEDGE", + "RDPGFX_CMDID_RESETGRAPHICS", + "RDPGFX_CMDID_MAPSURFACETOOUTPUT", + "RDPGFX_CMDID_CACHEIMPORTOFFER", + "RDPGFX_CMDID_CACHEIMPORTREPLY", + "RDPGFX_CMDID_CAPSADVERTISE", + "RDPGFX_CMDID_CAPSCONFIRM", + "RDPGFX_CMDID_UNUSED_0014", + "RDPGFX_CMDID_MAPSURFACETOWINDOW", + "RDPGFX_CMDID_QOEFRAMEACKNOWLEDGE", + "RDPGFX_CMDID_MAPSURFACETOSCALEDOUTPUT", + "RDPGFX_CMDID_MAPSURFACETOSCALEDWINDOW" }; + +const char* rdpgfx_get_cmd_id_string(UINT16 cmdId) +{ + if (cmdId <= RDPGFX_CMDID_MAPSURFACETOSCALEDWINDOW) + return RDPGFX_CMDID_STRINGS[cmdId]; + else + return "RDPGFX_CMDID_UNKNOWN"; +} + +const char* rdpgfx_get_codec_id_string(UINT16 codecId) +{ + switch (codecId) + { + case RDPGFX_CODECID_UNCOMPRESSED: + return "RDPGFX_CODECID_UNCOMPRESSED"; + + case RDPGFX_CODECID_CAVIDEO: + return "RDPGFX_CODECID_CAVIDEO"; + + case RDPGFX_CODECID_CLEARCODEC: + return "RDPGFX_CODECID_CLEARCODEC"; + + case RDPGFX_CODECID_PLANAR: + return "RDPGFX_CODECID_PLANAR"; + + case RDPGFX_CODECID_AVC420: + return "RDPGFX_CODECID_AVC420"; + + case RDPGFX_CODECID_AVC444: + return "RDPGFX_CODECID_AVC444"; + + case RDPGFX_CODECID_AVC444v2: + return "RDPGFX_CODECID_AVC444v2"; + + case RDPGFX_CODECID_ALPHA: + return "RDPGFX_CODECID_ALPHA"; + + case RDPGFX_CODECID_CAPROGRESSIVE: + return "RDPGFX_CODECID_CAPROGRESSIVE"; + + case RDPGFX_CODECID_CAPROGRESSIVE_V2: + return "RDPGFX_CODECID_CAPROGRESSIVE_V2"; + } + + return "RDPGFX_CODECID_UNKNOWN"; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT rdpgfx_read_header(wStream* s, RDPGFX_HEADER* header) +{ + WINPR_ASSERT(s); + WINPR_ASSERT(header); + + if (Stream_GetRemainingLength(s) < 8) + { + WLog_ERR(TAG, "calloc failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + Stream_Read_UINT16(s, header->cmdId); /* cmdId (2 bytes) */ + Stream_Read_UINT16(s, header->flags); /* flags (2 bytes) */ + Stream_Read_UINT32(s, header->pduLength); /* pduLength (4 bytes) */ + + if ((header->pduLength < 8) || (Stream_GetRemainingLength(s) < (header->pduLength - 8))) + { + WLog_ERR(TAG, "header->pduLength %u less than 8!", header->pduLength); + return ERROR_INVALID_DATA; + } + + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT rdpgfx_write_header(wStream* s, const RDPGFX_HEADER* header) +{ + if (!Stream_EnsureRemainingCapacity(s, 8)) + return ERROR_INTERNAL_ERROR; + Stream_Write_UINT16(s, header->cmdId); /* cmdId (2 bytes) */ + Stream_Write_UINT16(s, header->flags); /* flags (2 bytes) */ + Stream_Write_UINT32(s, header->pduLength); /* pduLength (4 bytes) */ + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT rdpgfx_read_point16(wStream* s, RDPGFX_POINT16* pt16) +{ + if (Stream_GetRemainingLength(s) < 4) + { + WLog_ERR(TAG, "not enough data!"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT16(s, pt16->x); /* x (2 bytes) */ + Stream_Read_UINT16(s, pt16->y); /* y (2 bytes) */ + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT rdpgfx_write_point16(wStream* s, const RDPGFX_POINT16* point16) +{ + Stream_Write_UINT16(s, point16->x); /* x (2 bytes) */ + Stream_Write_UINT16(s, point16->y); /* y (2 bytes) */ + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT rdpgfx_read_rect16(wStream* s, RECTANGLE_16* rect16) +{ + if (Stream_GetRemainingLength(s) < 8) + { + WLog_ERR(TAG, "not enough data!"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT16(s, rect16->left); /* left (2 bytes) */ + Stream_Read_UINT16(s, rect16->top); /* top (2 bytes) */ + Stream_Read_UINT16(s, rect16->right); /* right (2 bytes) */ + Stream_Read_UINT16(s, rect16->bottom); /* bottom (2 bytes) */ + if (rect16->left >= rect16->right) + return ERROR_INVALID_DATA; + if (rect16->top >= rect16->bottom) + return ERROR_INVALID_DATA; + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT rdpgfx_write_rect16(wStream* s, const RECTANGLE_16* rect16) +{ + Stream_Write_UINT16(s, rect16->left); /* left (2 bytes) */ + Stream_Write_UINT16(s, rect16->top); /* top (2 bytes) */ + Stream_Write_UINT16(s, rect16->right); /* right (2 bytes) */ + Stream_Write_UINT16(s, rect16->bottom); /* bottom (2 bytes) */ + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT rdpgfx_read_color32(wStream* s, RDPGFX_COLOR32* color32) +{ + if (Stream_GetRemainingLength(s) < 4) + { + WLog_ERR(TAG, "not enough data!"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT8(s, color32->B); /* B (1 byte) */ + Stream_Read_UINT8(s, color32->G); /* G (1 byte) */ + Stream_Read_UINT8(s, color32->R); /* R (1 byte) */ + Stream_Read_UINT8(s, color32->XA); /* XA (1 byte) */ + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT rdpgfx_write_color32(wStream* s, const RDPGFX_COLOR32* color32) +{ + Stream_Write_UINT8(s, color32->B); /* B (1 byte) */ + Stream_Write_UINT8(s, color32->G); /* G (1 byte) */ + Stream_Write_UINT8(s, color32->R); /* R (1 byte) */ + Stream_Write_UINT8(s, color32->XA); /* XA (1 byte) */ + return CHANNEL_RC_OK; +} diff --git a/channels/rdpgfx/rdpgfx_common.h b/channels/rdpgfx/rdpgfx_common.h new file mode 100644 index 0000000..664c9cc --- /dev/null +++ b/channels/rdpgfx/rdpgfx_common.h @@ -0,0 +1,55 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Graphics Pipeline Extension + * + * Copyright 2013 Marc-Andre Moreau + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger + * + * Licensed 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 FREERDP_CHANNEL_RDPGFX_COMMON_H +#define FREERDP_CHANNEL_RDPGFX_COMMON_H + +#include +#include + +#include +#include + +FREERDP_LOCAL const char* rdpgfx_get_cmd_id_string(UINT16 cmdId); +FREERDP_LOCAL const char* rdpgfx_get_codec_id_string(UINT16 codecId); + +FREERDP_LOCAL UINT rdpgfx_read_header(wStream* s, RDPGFX_HEADER* header); +FREERDP_LOCAL UINT rdpgfx_write_header(wStream* s, const RDPGFX_HEADER* header); + +FREERDP_LOCAL UINT rdpgfx_read_point16(wStream* s, RDPGFX_POINT16* pt16); +FREERDP_LOCAL UINT rdpgfx_write_point16(wStream* s, const RDPGFX_POINT16* point16); + +FREERDP_LOCAL UINT rdpgfx_read_rect16(wStream* s, RECTANGLE_16* rect16); +FREERDP_LOCAL UINT rdpgfx_write_rect16(wStream* s, const RECTANGLE_16* rect16); + +FREERDP_LOCAL UINT rdpgfx_read_color32(wStream* s, RDPGFX_COLOR32* color32); +FREERDP_LOCAL UINT rdpgfx_write_color32(wStream* s, const RDPGFX_COLOR32* color32); + +#ifdef WITH_DEBUG_RDPGFX +#define DEBUG_RDPGFX(_LOGGER, ...) WLog_Print(_LOGGER, WLOG_DEBUG, __VA_ARGS__) +#else +#define DEBUG_RDPGFX(_LOGGER, ...) \ + do \ + { \ + } while (0) +#endif + +#endif /* FREERDP_CHANNEL_RDPGFX_COMMON_H */ diff --git a/channels/rdpgfx/server/CMakeLists.txt b/channels/rdpgfx/server/CMakeLists.txt new file mode 100644 index 0000000..1b1f48b --- /dev/null +++ b/channels/rdpgfx/server/CMakeLists.txt @@ -0,0 +1,34 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2016 Jiang Zihao +# +# Licensed 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. + +define_channel_server("rdpgfx") + +set(${MODULE_PREFIX}_SRCS + rdpgfx_main.c + rdpgfx_main.h + ../rdpgfx_common.c + ../rdpgfx_common.h) + +include_directories(..) + +add_channel_server_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} FALSE "DVCPluginEntry") + + + +target_link_libraries(${MODULE_NAME} freerdp) + +set_property(TARGET ${MODULE_NAME} PROPERTY FOLDER "Channels/${CHANNEL_NAME}/Server") diff --git a/channels/rdpgfx/server/rdpgfx_main.c b/channels/rdpgfx/server/rdpgfx_main.c new file mode 100644 index 0000000..283bb88 --- /dev/null +++ b/channels/rdpgfx/server/rdpgfx_main.c @@ -0,0 +1,1720 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Graphics Pipeline Extension + * + * Copyright 2016 Jiang Zihao + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include + +#include "rdpgfx_common.h" +#include "rdpgfx_main.h" + +#define TAG CHANNELS_TAG("rdpgfx.server") +#define RDPGFX_RESET_GRAPHICS_PDU_SIZE 340 + +/** + * Function description + * Calculate packet size from data length. + * It would be data length + header. + * + * @param dataLen estimated data length without header + * + * @return new stream + */ +static INLINE UINT32 rdpgfx_pdu_length(UINT32 dataLen) +{ + return RDPGFX_HEADER_SIZE + dataLen; +} + +static INLINE UINT rdpgfx_server_packet_init_header(wStream* s, UINT16 cmdId, UINT32 pduLength) +{ + RDPGFX_HEADER header; + header.flags = 0; + header.cmdId = cmdId; + header.pduLength = pduLength; + /* Write header. Note that actual length might be changed + * after the entire packet has been constructed. */ + return rdpgfx_write_header(s, &header); +} + +/** + * Function description + * Complete the rdpgfx packet header. + * + * @param s stream + * @param start saved start pos of the packet in the stream + */ +static INLINE BOOL rdpgfx_server_packet_complete_header(wStream* s, size_t start) +{ + const size_t current = Stream_GetPosition(s); + const size_t cap = Stream_Capacity(s); + if (cap < start + RDPGFX_HEADER_SIZE) + return FALSE; + /* Fill actual length */ + Stream_SetPosition(s, start + RDPGFX_HEADER_SIZE - sizeof(UINT32)); + Stream_Write_UINT32(s, current - start); /* pduLength (4 bytes) */ + Stream_SetPosition(s, current); + return TRUE; +} + +/** + * Function description + * Send the stream for rdpgfx server packet. + * The packet would be compressed according to [MS-RDPEGFX]. + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpgfx_server_packet_send(RdpgfxServerContext* context, wStream* s) +{ + UINT error; + UINT32 flags = 0; + ULONG written; + BYTE* pSrcData = Stream_Buffer(s); + UINT32 SrcSize = Stream_GetPosition(s); + wStream* fs; + /* Allocate new stream with enough capacity. Additional overhead is + * descriptor (1 bytes) + segmentCount (2 bytes) + uncompressedSize (4 bytes) + * + segmentCount * size (4 bytes) */ + fs = Stream_New(NULL, SrcSize + 7 + (SrcSize / ZGFX_SEGMENTED_MAXSIZE + 1) * 4); + + if (!fs) + { + WLog_ERR(TAG, "Stream_New failed!"); + error = CHANNEL_RC_NO_MEMORY; + goto out; + } + + if (zgfx_compress_to_stream(context->priv->zgfx, fs, pSrcData, SrcSize, &flags) < 0) + { + WLog_ERR(TAG, "zgfx_compress_to_stream failed!"); + error = ERROR_INTERNAL_ERROR; + goto out; + } + + if (!WTSVirtualChannelWrite(context->priv->rdpgfx_channel, (PCHAR)Stream_Buffer(fs), + Stream_GetPosition(fs), &written)) + { + WLog_ERR(TAG, "WTSVirtualChannelWrite failed!"); + error = ERROR_INTERNAL_ERROR; + goto out; + } + + if (written < Stream_GetPosition(fs)) + { + WLog_WARN(TAG, "Unexpected bytes written: %" PRIu32 "/%" PRIuz "", written, + Stream_GetPosition(fs)); + } + + error = CHANNEL_RC_OK; +out: + Stream_Free(fs, TRUE); + Stream_Free(s, TRUE); + return error; +} + +/** + * Function description + * Create new stream for single rdpgfx packet. The new stream length + * would be required data length + header. The header will be written + * to the stream before return, but the pduLength field might be + * changed in rdpgfx_server_single_packet_send. + * + * @param cmdId + * @param dataLen estimated data length without header + * + * @return new stream + */ +static wStream* rdpgfx_server_single_packet_new(UINT16 cmdId, UINT32 dataLen) +{ + UINT error; + wStream* s; + UINT32 pduLength = rdpgfx_pdu_length(dataLen); + s = Stream_New(NULL, pduLength); + + if (!s) + { + WLog_ERR(TAG, "Stream_New failed!"); + goto error; + } + + if ((error = rdpgfx_server_packet_init_header(s, cmdId, pduLength))) + { + WLog_ERR(TAG, "Failed to init header with error %" PRIu32 "!", error); + goto error; + } + + return s; +error: + Stream_Free(s, TRUE); + return NULL; +} + +/** + * Function description + * Send the stream for single rdpgfx packet. + * The header will be filled with actual length. + * The packet would be compressed according to [MS-RDPEGFX]. + * + * @return 0 on success, otherwise a Win32 error code + */ +static INLINE UINT rdpgfx_server_single_packet_send(RdpgfxServerContext* context, wStream* s) +{ + /* Fill actual length */ + rdpgfx_server_packet_complete_header(s, 0); + return rdpgfx_server_packet_send(context, s); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpgfx_send_caps_confirm_pdu(RdpgfxServerContext* context, + const RDPGFX_CAPS_CONFIRM_PDU* capsConfirm) +{ + RDPGFX_CAPSET* capsSet = capsConfirm->capsSet; + wStream* s = rdpgfx_server_single_packet_new(RDPGFX_CMDID_CAPSCONFIRM, + RDPGFX_CAPSET_BASE_SIZE + capsSet->length); + + if (!s) + { + WLog_ERR(TAG, "rdpgfx_server_single_packet_new failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + Stream_Write_UINT32(s, capsSet->version); /* version (4 bytes) */ + Stream_Write_UINT32(s, capsSet->length); /* capsDataLength (4 bytes) */ + + if (capsSet->length >= 4) + { + Stream_Write_UINT32(s, capsSet->flags); /* capsData (4 bytes) */ + Stream_Zero(s, capsSet->length - 4); + } + else + Stream_Zero(s, capsSet->length); + + return rdpgfx_server_single_packet_send(context, s); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpgfx_send_reset_graphics_pdu(RdpgfxServerContext* context, + const RDPGFX_RESET_GRAPHICS_PDU* pdu) +{ + UINT32 index; + MONITOR_DEF* monitor; + wStream* s; + + /* Check monitorCount. This ensures total size within 340 bytes) */ + if (pdu->monitorCount >= 16) + { + WLog_ERR(TAG, "Monitor count MUST be less than or equal to 16: %" PRIu32 "", + pdu->monitorCount); + return ERROR_INVALID_DATA; + } + + s = rdpgfx_server_single_packet_new(RDPGFX_CMDID_RESETGRAPHICS, + RDPGFX_RESET_GRAPHICS_PDU_SIZE - RDPGFX_HEADER_SIZE); + + if (!s) + { + WLog_ERR(TAG, "rdpgfx_server_single_packet_new failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + Stream_Write_UINT32(s, pdu->width); /* width (4 bytes) */ + Stream_Write_UINT32(s, pdu->height); /* height (4 bytes) */ + Stream_Write_UINT32(s, pdu->monitorCount); /* monitorCount (4 bytes) */ + + for (index = 0; index < pdu->monitorCount; index++) + { + monitor = &(pdu->monitorDefArray[index]); + Stream_Write_UINT32(s, monitor->left); /* left (4 bytes) */ + Stream_Write_UINT32(s, monitor->top); /* top (4 bytes) */ + Stream_Write_UINT32(s, monitor->right); /* right (4 bytes) */ + Stream_Write_UINT32(s, monitor->bottom); /* bottom (4 bytes) */ + Stream_Write_UINT32(s, monitor->flags); /* flags (4 bytes) */ + } + + /* pad (total size must be 340 bytes) */ + Stream_SetPosition(s, RDPGFX_RESET_GRAPHICS_PDU_SIZE); + return rdpgfx_server_single_packet_send(context, s); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpgfx_send_evict_cache_entry_pdu(RdpgfxServerContext* context, + const RDPGFX_EVICT_CACHE_ENTRY_PDU* pdu) +{ + wStream* s = rdpgfx_server_single_packet_new(RDPGFX_CMDID_EVICTCACHEENTRY, 2); + + if (!s) + { + WLog_ERR(TAG, "rdpgfx_server_single_packet_new failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + Stream_Write_UINT16(s, pdu->cacheSlot); /* cacheSlot (2 bytes) */ + return rdpgfx_server_single_packet_send(context, s); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpgfx_send_cache_import_reply_pdu(RdpgfxServerContext* context, + const RDPGFX_CACHE_IMPORT_REPLY_PDU* pdu) +{ + UINT16 index; + wStream* s = rdpgfx_server_single_packet_new(RDPGFX_CMDID_CACHEIMPORTREPLY, + 2 + 2 * pdu->importedEntriesCount); + + if (!s) + { + WLog_ERR(TAG, "rdpgfx_server_single_packet_new failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + /* importedEntriesCount (2 bytes) */ + Stream_Write_UINT16(s, pdu->importedEntriesCount); + + for (index = 0; index < pdu->importedEntriesCount; index++) + { + Stream_Write_UINT16(s, pdu->cacheSlots[index]); /* cacheSlot (2 bytes) */ + } + + return rdpgfx_server_single_packet_send(context, s); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpgfx_send_create_surface_pdu(RdpgfxServerContext* context, + const RDPGFX_CREATE_SURFACE_PDU* pdu) +{ + wStream* s = rdpgfx_server_single_packet_new(RDPGFX_CMDID_CREATESURFACE, 7); + + if (!s) + { + WLog_ERR(TAG, "rdpgfx_server_single_packet_new failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + Stream_Write_UINT16(s, pdu->surfaceId); /* surfaceId (2 bytes) */ + Stream_Write_UINT16(s, pdu->width); /* width (2 bytes) */ + Stream_Write_UINT16(s, pdu->height); /* height (2 bytes) */ + Stream_Write_UINT8(s, pdu->pixelFormat); /* RDPGFX_PIXELFORMAT (1 byte) */ + return rdpgfx_server_single_packet_send(context, s); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpgfx_send_delete_surface_pdu(RdpgfxServerContext* context, + const RDPGFX_DELETE_SURFACE_PDU* pdu) +{ + wStream* s = rdpgfx_server_single_packet_new(RDPGFX_CMDID_DELETESURFACE, 2); + + if (!s) + { + WLog_ERR(TAG, "rdpgfx_server_single_packet_new failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + Stream_Write_UINT16(s, pdu->surfaceId); /* surfaceId (2 bytes) */ + return rdpgfx_server_single_packet_send(context, s); +} + +static INLINE BOOL rdpgfx_write_start_frame_pdu(wStream* s, const RDPGFX_START_FRAME_PDU* pdu) +{ + if (!Stream_EnsureRemainingCapacity(s, 8)) + return FALSE; + Stream_Write_UINT32(s, pdu->timestamp); /* timestamp (4 bytes) */ + Stream_Write_UINT32(s, pdu->frameId); /* frameId (4 bytes) */ + return TRUE; +} + +static INLINE BOOL rdpgfx_write_end_frame_pdu(wStream* s, const RDPGFX_END_FRAME_PDU* pdu) +{ + if (!Stream_EnsureRemainingCapacity(s, 4)) + return FALSE; + Stream_Write_UINT32(s, pdu->frameId); /* frameId (4 bytes) */ + return TRUE; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpgfx_send_start_frame_pdu(RdpgfxServerContext* context, + const RDPGFX_START_FRAME_PDU* pdu) +{ + wStream* s = + rdpgfx_server_single_packet_new(RDPGFX_CMDID_STARTFRAME, RDPGFX_START_FRAME_PDU_SIZE); + + if (!s) + { + WLog_ERR(TAG, "rdpgfx_server_single_packet_new failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + rdpgfx_write_start_frame_pdu(s, pdu); + return rdpgfx_server_single_packet_send(context, s); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpgfx_send_end_frame_pdu(RdpgfxServerContext* context, const RDPGFX_END_FRAME_PDU* pdu) +{ + wStream* s = rdpgfx_server_single_packet_new(RDPGFX_CMDID_ENDFRAME, RDPGFX_END_FRAME_PDU_SIZE); + + if (!s) + { + WLog_ERR(TAG, "rdpgfx_server_single_packet_new failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + rdpgfx_write_end_frame_pdu(s, pdu); + return rdpgfx_server_single_packet_send(context, s); +} + +/** + * Function description + * Estimate RFX_AVC420_BITMAP_STREAM structure size in stream + * + * @return estimated size + */ +static INLINE UINT32 rdpgfx_estimate_h264_avc420(const RDPGFX_AVC420_BITMAP_STREAM* havc420) +{ + /* H264 metadata + H264 stream. See rdpgfx_write_h264_avc420 */ + return sizeof(UINT32) /* numRegionRects */ + + 10 /* regionRects + quantQualityVals */ + * havc420->meta.numRegionRects + + havc420->length; +} + +/** + * Function description + * Estimate surface command packet size in stream without header + * + * @return estimated size + */ +static INLINE UINT32 rdpgfx_estimate_surface_command(const RDPGFX_SURFACE_COMMAND* cmd) +{ + RDPGFX_AVC420_BITMAP_STREAM* havc420 = NULL; + RDPGFX_AVC444_BITMAP_STREAM* havc444 = NULL; + UINT32 h264Size = 0; + + /* Estimate stream size according to codec. */ + switch (cmd->codecId) + { + case RDPGFX_CODECID_CAPROGRESSIVE: + case RDPGFX_CODECID_CAPROGRESSIVE_V2: + return RDPGFX_WIRE_TO_SURFACE_PDU_2_SIZE + cmd->length; + + case RDPGFX_CODECID_AVC420: + havc420 = (RDPGFX_AVC420_BITMAP_STREAM*)cmd->extra; + h264Size = rdpgfx_estimate_h264_avc420(havc420); + return RDPGFX_WIRE_TO_SURFACE_PDU_1_SIZE + h264Size; + + case RDPGFX_CODECID_AVC444: + havc444 = (RDPGFX_AVC444_BITMAP_STREAM*)cmd->extra; + h264Size = sizeof(UINT32); /* cbAvc420EncodedBitstream1 */ + /* avc420EncodedBitstream1 */ + havc420 = &(havc444->bitstream[0]); + h264Size += rdpgfx_estimate_h264_avc420(havc420); + + /* avc420EncodedBitstream2 */ + if (havc444->LC == 0) + { + havc420 = &(havc444->bitstream[1]); + h264Size += rdpgfx_estimate_h264_avc420(havc420); + } + + return RDPGFX_WIRE_TO_SURFACE_PDU_1_SIZE + h264Size; + + default: + return RDPGFX_WIRE_TO_SURFACE_PDU_1_SIZE + cmd->length; + } +} + +/** + * Function description + * Resolve RDPGFX_CMDID_WIRETOSURFACE_1 or RDPGFX_CMDID_WIRETOSURFACE_2 + * according to codecId + * + * @return 0 on success, otherwise a Win32 error code + */ +static INLINE UINT16 rdpgfx_surface_command_cmdid(const RDPGFX_SURFACE_COMMAND* cmd) +{ + if (cmd->codecId == RDPGFX_CODECID_CAPROGRESSIVE || + cmd->codecId == RDPGFX_CODECID_CAPROGRESSIVE_V2) + { + return RDPGFX_CMDID_WIRETOSURFACE_2; + } + + return RDPGFX_CMDID_WIRETOSURFACE_1; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpgfx_write_h264_metablock(wStream* s, const RDPGFX_H264_METABLOCK* meta) +{ + UINT32 index; + RECTANGLE_16* regionRect; + RDPGFX_H264_QUANT_QUALITY* quantQualityVal; + UINT error = CHANNEL_RC_OK; + + if (!Stream_EnsureRemainingCapacity(s, 4 + meta->numRegionRects * 10)) + return ERROR_OUTOFMEMORY; + + Stream_Write_UINT32(s, meta->numRegionRects); /* numRegionRects (4 bytes) */ + + for (index = 0; index < meta->numRegionRects; index++) + { + regionRect = &(meta->regionRects[index]); + + if ((error = rdpgfx_write_rect16(s, regionRect))) + { + WLog_ERR(TAG, "rdpgfx_write_rect16 failed with error %" PRIu32 "!", error); + return error; + } + } + + for (index = 0; index < meta->numRegionRects; index++) + { + quantQualityVal = &(meta->quantQualityVals[index]); + Stream_Write_UINT8(s, quantQualityVal->qp | (quantQualityVal->r << 6) | + (quantQualityVal->p << 7)); /* qpVal (1 byte) */ + /* qualityVal (1 byte) */ + Stream_Write_UINT8(s, quantQualityVal->qualityVal); + } + + return error; +} + +/** + * Function description + * Write RFX_AVC420_BITMAP_STREAM structure to stream + * + * @return 0 on success, otherwise a Win32 error code + */ +static INLINE UINT rdpgfx_write_h264_avc420(wStream* s, RDPGFX_AVC420_BITMAP_STREAM* havc420) +{ + UINT error = CHANNEL_RC_OK; + + if ((error = rdpgfx_write_h264_metablock(s, &(havc420->meta)))) + { + WLog_ERR(TAG, "rdpgfx_write_h264_metablock failed with error %" PRIu32 "!", error); + return error; + } + + if (!Stream_EnsureRemainingCapacity(s, havc420->length)) + return ERROR_OUTOFMEMORY; + + Stream_Write(s, havc420->data, havc420->length); + return error; +} + +/** + * Function description + * Write RDPGFX_CMDID_WIRETOSURFACE_1 or RDPGFX_CMDID_WIRETOSURFACE_2 + * to the stream according to RDPGFX_SURFACE_COMMAND message + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpgfx_write_surface_command(wStream* s, const RDPGFX_SURFACE_COMMAND* cmd) +{ + UINT error = CHANNEL_RC_OK; + RDPGFX_AVC420_BITMAP_STREAM* havc420 = NULL; + RDPGFX_AVC444_BITMAP_STREAM* havc444 = NULL; + UINT32 bitmapDataStart = 0; + UINT32 bitmapDataLength = 0; + UINT8 pixelFormat = 0; + + switch (cmd->format) + { + case PIXEL_FORMAT_BGRX32: + pixelFormat = GFX_PIXEL_FORMAT_XRGB_8888; + break; + + case PIXEL_FORMAT_BGRA32: + pixelFormat = GFX_PIXEL_FORMAT_ARGB_8888; + break; + + default: + WLog_ERR(TAG, "Format %s not supported!", FreeRDPGetColorFormatName(cmd->format)); + return ERROR_INVALID_DATA; + } + + if (cmd->codecId == RDPGFX_CODECID_CAPROGRESSIVE || + cmd->codecId == RDPGFX_CODECID_CAPROGRESSIVE_V2) + { + if (!Stream_EnsureRemainingCapacity(s, 13 + cmd->length)) + return ERROR_INTERNAL_ERROR; + /* Write RDPGFX_CMDID_WIRETOSURFACE_2 format for CAPROGRESSIVE */ + Stream_Write_UINT16(s, cmd->surfaceId); /* surfaceId (2 bytes) */ + Stream_Write_UINT16(s, cmd->codecId); /* codecId (2 bytes) */ + Stream_Write_UINT32(s, cmd->contextId); /* codecContextId (4 bytes) */ + Stream_Write_UINT8(s, pixelFormat); /* pixelFormat (1 byte) */ + Stream_Write_UINT32(s, cmd->length); /* bitmapDataLength (4 bytes) */ + Stream_Write(s, cmd->data, cmd->length); + } + else + { + /* Write RDPGFX_CMDID_WIRETOSURFACE_1 format for others */ + if (!Stream_EnsureRemainingCapacity(s, 17)) + return ERROR_INTERNAL_ERROR; + Stream_Write_UINT16(s, cmd->surfaceId); /* surfaceId (2 bytes) */ + Stream_Write_UINT16(s, cmd->codecId); /* codecId (2 bytes) */ + Stream_Write_UINT8(s, pixelFormat); /* pixelFormat (1 byte) */ + Stream_Write_UINT16(s, cmd->left); /* left (2 bytes) */ + Stream_Write_UINT16(s, cmd->top); /* top (2 bytes) */ + Stream_Write_UINT16(s, cmd->right); /* right (2 bytes) */ + Stream_Write_UINT16(s, cmd->bottom); /* bottom (2 bytes) */ + Stream_Write_UINT32(s, cmd->length); /* bitmapDataLength (4 bytes) */ + bitmapDataStart = Stream_GetPosition(s); + + if (cmd->codecId == RDPGFX_CODECID_AVC420) + { + havc420 = (RDPGFX_AVC420_BITMAP_STREAM*)cmd->extra; + error = rdpgfx_write_h264_avc420(s, havc420); + + if (error != CHANNEL_RC_OK) + { + WLog_ERR(TAG, "rdpgfx_write_h264_avc420 failed!"); + return error; + } + } + else if ((cmd->codecId == RDPGFX_CODECID_AVC444) || + (cmd->codecId == RDPGFX_CODECID_AVC444v2)) + { + havc444 = (RDPGFX_AVC444_BITMAP_STREAM*)cmd->extra; + havc420 = &(havc444->bitstream[0]); /* avc420EncodedBitstreamInfo (4 bytes) */ + if (!Stream_EnsureRemainingCapacity(s, 4)) + return ERROR_INTERNAL_ERROR; + Stream_Write_UINT32(s, havc444->cbAvc420EncodedBitstream1 | (havc444->LC << 30UL)); + /* avc420EncodedBitstream1 */ + error = rdpgfx_write_h264_avc420(s, havc420); + + if (error != CHANNEL_RC_OK) + { + WLog_ERR(TAG, "rdpgfx_write_h264_avc420 failed!"); + return error; + } + + /* avc420EncodedBitstream2 */ + if (havc444->LC == 0) + { + havc420 = &(havc444->bitstream[1]); + error = rdpgfx_write_h264_avc420(s, havc420); + + if (error != CHANNEL_RC_OK) + { + WLog_ERR(TAG, "rdpgfx_write_h264_avc420 failed!"); + return error; + } + } + } + else + { + if (!Stream_EnsureRemainingCapacity(s, cmd->length)) + return ERROR_INTERNAL_ERROR; + Stream_Write(s, cmd->data, cmd->length); + } + + /* Fill actual bitmap data length */ + bitmapDataLength = Stream_GetPosition(s) - bitmapDataStart; + Stream_SetPosition(s, bitmapDataStart - sizeof(UINT32)); + if (!Stream_EnsureRemainingCapacity(s, 4)) + return ERROR_INTERNAL_ERROR; + Stream_Write_UINT32(s, bitmapDataLength); /* bitmapDataLength (4 bytes) */ + if (!Stream_SafeSeek(s, bitmapDataLength)) + return ERROR_INTERNAL_ERROR; + } + + return error; +} + +/** + * Function description + * Send RDPGFX_CMDID_WIRETOSURFACE_1 or RDPGFX_CMDID_WIRETOSURFACE_2 + * message according to codecId + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpgfx_send_surface_command(RdpgfxServerContext* context, + const RDPGFX_SURFACE_COMMAND* cmd) +{ + UINT error = CHANNEL_RC_OK; + wStream* s; + s = rdpgfx_server_single_packet_new(rdpgfx_surface_command_cmdid(cmd), + rdpgfx_estimate_surface_command(cmd)); + + if (!s) + { + WLog_ERR(TAG, "rdpgfx_server_single_packet_new failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + error = rdpgfx_write_surface_command(s, cmd); + + if (error != CHANNEL_RC_OK) + { + WLog_ERR(TAG, "rdpgfx_write_surface_command failed!"); + goto error; + } + + return rdpgfx_server_single_packet_send(context, s); +error: + Stream_Free(s, TRUE); + return error; +} + +/** + * Function description + * Send RDPGFX_CMDID_WIRETOSURFACE_1 or RDPGFX_CMDID_WIRETOSURFACE_2 + * message according to codecId. + * Prepend/append start/end frame message in same packet if exists. + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpgfx_send_surface_frame_command(RdpgfxServerContext* context, + const RDPGFX_SURFACE_COMMAND* cmd, + const RDPGFX_START_FRAME_PDU* startFrame, + const RDPGFX_END_FRAME_PDU* endFrame) + +{ + UINT error = CHANNEL_RC_OK; + wStream* s; + UINT32 position = 0; + UINT32 size = rdpgfx_pdu_length(rdpgfx_estimate_surface_command(cmd)); + + if (startFrame) + { + size += rdpgfx_pdu_length(RDPGFX_START_FRAME_PDU_SIZE); + } + + if (endFrame) + { + size += rdpgfx_pdu_length(RDPGFX_END_FRAME_PDU_SIZE); + } + + s = Stream_New(NULL, size); + + if (!s) + { + WLog_ERR(TAG, "Stream_New failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + /* Write start frame if exists */ + if (startFrame) + { + position = Stream_GetPosition(s); + error = rdpgfx_server_packet_init_header(s, RDPGFX_CMDID_STARTFRAME, 0); + + if (error != CHANNEL_RC_OK) + { + WLog_ERR(TAG, "Failed to init header with error %" PRIu32 "!", error); + goto error; + } + + if (!rdpgfx_write_start_frame_pdu(s, startFrame) || + !rdpgfx_server_packet_complete_header(s, position)) + goto error; + } + + /* Write RDPGFX_CMDID_WIRETOSURFACE_1 or RDPGFX_CMDID_WIRETOSURFACE_2 */ + position = Stream_GetPosition(s); + error = rdpgfx_server_packet_init_header(s, rdpgfx_surface_command_cmdid(cmd), + 0); // Actual length will be filled later + + if (error != CHANNEL_RC_OK) + { + WLog_ERR(TAG, "Failed to init header with error %" PRIu32 "!", error); + goto error; + } + + error = rdpgfx_write_surface_command(s, cmd); + + if (error != CHANNEL_RC_OK) + { + WLog_ERR(TAG, "rdpgfx_write_surface_command failed!"); + goto error; + } + + if (!rdpgfx_server_packet_complete_header(s, position)) + goto error; + + /* Write end frame if exists */ + if (endFrame) + { + position = Stream_GetPosition(s); + error = rdpgfx_server_packet_init_header(s, RDPGFX_CMDID_ENDFRAME, 0); + + if (error != CHANNEL_RC_OK) + { + WLog_ERR(TAG, "Failed to init header with error %" PRIu32 "!", error); + goto error; + } + + if (!rdpgfx_write_end_frame_pdu(s, endFrame) || + !rdpgfx_server_packet_complete_header(s, position)) + goto error; + } + + return rdpgfx_server_packet_send(context, s); +error: + Stream_Free(s, TRUE); + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpgfx_send_delete_encoding_context_pdu(RdpgfxServerContext* context, + const RDPGFX_DELETE_ENCODING_CONTEXT_PDU* pdu) +{ + wStream* s = rdpgfx_server_single_packet_new(RDPGFX_CMDID_DELETEENCODINGCONTEXT, 6); + + if (!s) + { + WLog_ERR(TAG, "rdpgfx_server_single_packet_new failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + Stream_Write_UINT16(s, pdu->surfaceId); /* surfaceId (2 bytes) */ + Stream_Write_UINT32(s, pdu->codecContextId); /* codecContextId (4 bytes) */ + return rdpgfx_server_single_packet_send(context, s); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpgfx_send_solid_fill_pdu(RdpgfxServerContext* context, + const RDPGFX_SOLID_FILL_PDU* pdu) +{ + UINT error = CHANNEL_RC_OK; + UINT16 index; + RECTANGLE_16* fillRect; + wStream* s = + rdpgfx_server_single_packet_new(RDPGFX_CMDID_SOLIDFILL, 8 + 8 * pdu->fillRectCount); + + if (!s) + { + WLog_ERR(TAG, "rdpgfx_server_single_packet_new failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + Stream_Write_UINT16(s, pdu->surfaceId); /* surfaceId (2 bytes) */ + + /* fillPixel (4 bytes) */ + if ((error = rdpgfx_write_color32(s, &(pdu->fillPixel)))) + { + WLog_ERR(TAG, "rdpgfx_write_color32 failed with error %" PRIu32 "!", error); + goto error; + } + + Stream_Write_UINT16(s, pdu->fillRectCount); /* fillRectCount (2 bytes) */ + + for (index = 0; index < pdu->fillRectCount; index++) + { + fillRect = &(pdu->fillRects[index]); + + if ((error = rdpgfx_write_rect16(s, fillRect))) + { + WLog_ERR(TAG, "rdpgfx_write_rect16 failed with error %" PRIu32 "!", error); + goto error; + } + } + + return rdpgfx_server_single_packet_send(context, s); +error: + Stream_Free(s, TRUE); + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpgfx_send_surface_to_surface_pdu(RdpgfxServerContext* context, + const RDPGFX_SURFACE_TO_SURFACE_PDU* pdu) +{ + UINT error = CHANNEL_RC_OK; + UINT16 index; + RDPGFX_POINT16* destPt; + wStream* s = + rdpgfx_server_single_packet_new(RDPGFX_CMDID_SURFACETOSURFACE, 14 + 4 * pdu->destPtsCount); + + if (!s) + { + WLog_ERR(TAG, "rdpgfx_server_single_packet_new failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + Stream_Write_UINT16(s, pdu->surfaceIdSrc); /* surfaceIdSrc (2 bytes) */ + Stream_Write_UINT16(s, pdu->surfaceIdDest); /* surfaceIdDest (2 bytes) */ + + /* rectSrc (8 bytes ) */ + if ((error = rdpgfx_write_rect16(s, &(pdu->rectSrc)))) + { + WLog_ERR(TAG, "rdpgfx_write_rect16 failed with error %" PRIu32 "!", error); + goto error; + } + + Stream_Write_UINT16(s, pdu->destPtsCount); /* destPtsCount (2 bytes) */ + + for (index = 0; index < pdu->destPtsCount; index++) + { + destPt = &(pdu->destPts[index]); + + if ((error = rdpgfx_write_point16(s, destPt))) + { + WLog_ERR(TAG, "rdpgfx_write_point16 failed with error %" PRIu32 "!", error); + goto error; + } + } + + return rdpgfx_server_single_packet_send(context, s); +error: + Stream_Free(s, TRUE); + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpgfx_send_surface_to_cache_pdu(RdpgfxServerContext* context, + const RDPGFX_SURFACE_TO_CACHE_PDU* pdu) +{ + UINT error = CHANNEL_RC_OK; + wStream* s = rdpgfx_server_single_packet_new(RDPGFX_CMDID_SURFACETOCACHE, 20); + + if (!s) + { + WLog_ERR(TAG, "rdpgfx_server_single_packet_new failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + Stream_Write_UINT16(s, pdu->surfaceId); /* surfaceId (2 bytes) */ + Stream_Write_UINT64(s, pdu->cacheKey); /* cacheKey (8 bytes) */ + Stream_Write_UINT16(s, pdu->cacheSlot); /* cacheSlot (2 bytes) */ + + /* rectSrc (8 bytes ) */ + if ((error = rdpgfx_write_rect16(s, &(pdu->rectSrc)))) + { + WLog_ERR(TAG, "rdpgfx_write_rect16 failed with error %" PRIu32 "!", error); + goto error; + } + + return rdpgfx_server_single_packet_send(context, s); +error: + Stream_Free(s, TRUE); + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpgfx_send_cache_to_surface_pdu(RdpgfxServerContext* context, + const RDPGFX_CACHE_TO_SURFACE_PDU* pdu) +{ + UINT error = CHANNEL_RC_OK; + UINT16 index; + RDPGFX_POINT16* destPt; + wStream* s = + rdpgfx_server_single_packet_new(RDPGFX_CMDID_CACHETOSURFACE, 6 + 4 * pdu->destPtsCount); + + if (!s) + { + WLog_ERR(TAG, "rdpgfx_server_single_packet_new failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + Stream_Write_UINT16(s, pdu->cacheSlot); /* cacheSlot (2 bytes) */ + Stream_Write_UINT16(s, pdu->surfaceId); /* surfaceId (2 bytes) */ + Stream_Write_UINT16(s, pdu->destPtsCount); /* destPtsCount (2 bytes) */ + + for (index = 0; index < pdu->destPtsCount; index++) + { + destPt = &(pdu->destPts[index]); + + if ((error = rdpgfx_write_point16(s, destPt))) + { + WLog_ERR(TAG, "rdpgfx_write_point16 failed with error %" PRIu32 "", error); + goto error; + } + } + + return rdpgfx_server_single_packet_send(context, s); +error: + Stream_Free(s, TRUE); + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpgfx_send_map_surface_to_output_pdu(RdpgfxServerContext* context, + const RDPGFX_MAP_SURFACE_TO_OUTPUT_PDU* pdu) +{ + wStream* s = rdpgfx_server_single_packet_new(RDPGFX_CMDID_MAPSURFACETOOUTPUT, 12); + + if (!s) + { + WLog_ERR(TAG, "rdpgfx_server_single_packet_new failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + Stream_Write_UINT16(s, pdu->surfaceId); /* surfaceId (2 bytes) */ + Stream_Write_UINT16(s, 0); /* reserved (2 bytes). Must be 0 */ + Stream_Write_UINT32(s, pdu->outputOriginX); /* outputOriginX (4 bytes) */ + Stream_Write_UINT32(s, pdu->outputOriginY); /* outputOriginY (4 bytes) */ + return rdpgfx_server_single_packet_send(context, s); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpgfx_send_map_surface_to_window_pdu(RdpgfxServerContext* context, + const RDPGFX_MAP_SURFACE_TO_WINDOW_PDU* pdu) +{ + wStream* s = rdpgfx_server_single_packet_new(RDPGFX_CMDID_MAPSURFACETOWINDOW, 18); + + if (!s) + { + WLog_ERR(TAG, "rdpgfx_server_single_packet_new failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + Stream_Write_UINT16(s, pdu->surfaceId); /* surfaceId (2 bytes) */ + Stream_Write_UINT64(s, pdu->windowId); /* windowId (8 bytes) */ + Stream_Write_UINT32(s, pdu->mappedWidth); /* mappedWidth (4 bytes) */ + Stream_Write_UINT32(s, pdu->mappedHeight); /* mappedHeight (4 bytes) */ + return rdpgfx_server_single_packet_send(context, s); +} + +static UINT +rdpgfx_send_map_surface_to_scaled_window_pdu(RdpgfxServerContext* context, + const RDPGFX_MAP_SURFACE_TO_SCALED_WINDOW_PDU* pdu) +{ + wStream* s = rdpgfx_server_single_packet_new(RDPGFX_CMDID_MAPSURFACETOSCALEDWINDOW, 26); + + if (!s) + { + WLog_ERR(TAG, "rdpgfx_server_single_packet_new failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + Stream_Write_UINT16(s, pdu->surfaceId); /* surfaceId (2 bytes) */ + Stream_Write_UINT64(s, pdu->windowId); /* windowId (8 bytes) */ + Stream_Write_UINT32(s, pdu->mappedWidth); /* mappedWidth (4 bytes) */ + Stream_Write_UINT32(s, pdu->mappedHeight); /* mappedHeight (4 bytes) */ + Stream_Write_UINT32(s, pdu->targetWidth); /* targetWidth (4 bytes) */ + Stream_Write_UINT32(s, pdu->targetHeight); /* targetHeight (4 bytes) */ + return rdpgfx_server_single_packet_send(context, s); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpgfx_recv_frame_acknowledge_pdu(RdpgfxServerContext* context, wStream* s) +{ + RDPGFX_FRAME_ACKNOWLEDGE_PDU pdu; + UINT error = CHANNEL_RC_OK; + + if (Stream_GetRemainingLength(s) < 12) + { + WLog_ERR(TAG, "not enough data!"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT32(s, pdu.queueDepth); /* queueDepth (4 bytes) */ + Stream_Read_UINT32(s, pdu.frameId); /* frameId (4 bytes) */ + Stream_Read_UINT32(s, pdu.totalFramesDecoded); /* totalFramesDecoded (4 bytes) */ + + if (context) + { + IFCALLRET(context->FrameAcknowledge, error, context, &pdu); + + if (error) + WLog_ERR(TAG, "context->FrameAcknowledge failed with error %" PRIu32 "", error); + } + + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpgfx_recv_cache_import_offer_pdu(RdpgfxServerContext* context, wStream* s) +{ + UINT16 index; + RDPGFX_CACHE_IMPORT_OFFER_PDU pdu = { 0 }; + RDPGFX_CACHE_ENTRY_METADATA* cacheEntries; + UINT error = CHANNEL_RC_OK; + + if (Stream_GetRemainingLength(s) < 2) + { + WLog_ERR(TAG, "not enough data!"); + return ERROR_INVALID_DATA; + } + + /* cacheEntriesCount (2 bytes) */ + Stream_Read_UINT16(s, pdu.cacheEntriesCount); + + /* 2.2.2.16 RDPGFX_CACHE_IMPORT_OFFER_PDU */ + if (pdu.cacheEntriesCount >= 5462) + { + WLog_ERR(TAG, "Invalid cacheEntriesCount: %" PRIu16 "", pdu.cacheEntriesCount); + return ERROR_INVALID_DATA; + } + + if (Stream_GetRemainingLength(s) < (pdu.cacheEntriesCount * 12)) + { + WLog_ERR(TAG, "not enough data!"); + return ERROR_INVALID_DATA; + } + + if (pdu.cacheEntriesCount > 0) + { + pdu.cacheEntries = (RDPGFX_CACHE_ENTRY_METADATA*)calloc( + pdu.cacheEntriesCount, sizeof(RDPGFX_CACHE_ENTRY_METADATA)); + + if (!pdu.cacheEntries) + { + WLog_ERR(TAG, "calloc failed!"); + return CHANNEL_RC_NO_MEMORY; + } + } + + for (index = 0; index < pdu.cacheEntriesCount; index++) + { + cacheEntries = &(pdu.cacheEntries[index]); + Stream_Read_UINT64(s, cacheEntries->cacheKey); /* cacheKey (8 bytes) */ + Stream_Read_UINT32(s, cacheEntries->bitmapLength); /* bitmapLength (4 bytes) */ + } + + if (context) + { + IFCALLRET(context->CacheImportOffer, error, context, &pdu); + + if (error) + WLog_ERR(TAG, "context->CacheImportOffer failed with error %" PRIu32 "", error); + } + + free(pdu.cacheEntries); + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpgfx_recv_caps_advertise_pdu(RdpgfxServerContext* context, wStream* s) +{ + UINT16 index; + RDPGFX_CAPSET* capsSets = NULL; + RDPGFX_CAPS_ADVERTISE_PDU pdu = { 0 }; + UINT error = ERROR_INVALID_DATA; + + if (!context) + return ERROR_BAD_ARGUMENTS; + + if (Stream_GetRemainingLength(s) < 2) + { + WLog_ERR(TAG, "not enough data!"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT16(s, pdu.capsSetCount); /* capsSetCount (2 bytes) */ + if (pdu.capsSetCount > 0) + { + capsSets = calloc(pdu.capsSetCount, (RDPGFX_CAPSET_BASE_SIZE + 4)); + if (!capsSets) + return ERROR_OUTOFMEMORY; + } + + pdu.capsSets = capsSets; + + for (index = 0; index < pdu.capsSetCount; index++) + { + RDPGFX_CAPSET* capsSet = &(pdu.capsSets[index]); + + if (Stream_GetRemainingLength(s) < 8) + goto fail; + + Stream_Read_UINT32(s, capsSet->version); /* version (4 bytes) */ + Stream_Read_UINT32(s, capsSet->length); /* capsDataLength (4 bytes) */ + + if (capsSet->length >= 4) + { + if (Stream_GetRemainingLength(s) < 4) + goto fail; + + Stream_Peek_UINT32(s, capsSet->flags); /* capsData (4 bytes) */ + } + + if (!Stream_SafeSeek(s, capsSet->length)) + goto fail; + } + + error = ERROR_BAD_CONFIGURATION; + IFCALLRET(context->CapsAdvertise, error, context, &pdu); + + if (error) + WLog_ERR(TAG, "context->CapsAdvertise failed with error %" PRIu32 "", error); + +fail: + free(capsSets); + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpgfx_recv_qoe_frame_acknowledge_pdu(RdpgfxServerContext* context, wStream* s) +{ + RDPGFX_QOE_FRAME_ACKNOWLEDGE_PDU pdu; + UINT error = CHANNEL_RC_OK; + + if (Stream_GetRemainingLength(s) < 12) + { + WLog_ERR(TAG, "not enough data!"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT32(s, pdu.frameId); /* frameId (4 bytes) */ + Stream_Read_UINT32(s, pdu.timestamp); /* timestamp (4 bytes) */ + Stream_Read_UINT16(s, pdu.timeDiffSE); /* timeDiffSE (2 bytes) */ + Stream_Read_UINT16(s, pdu.timeDiffEDR); /* timeDiffEDR (2 bytes) */ + + if (context) + { + IFCALLRET(context->QoeFrameAcknowledge, error, context, &pdu); + + if (error) + WLog_ERR(TAG, "context->QoeFrameAcknowledge failed with error %" PRIu32 "", error); + } + + return error; +} + +static UINT +rdpgfx_send_map_surface_to_scaled_output_pdu(RdpgfxServerContext* context, + const RDPGFX_MAP_SURFACE_TO_SCALED_OUTPUT_PDU* pdu) +{ + wStream* s = rdpgfx_server_single_packet_new(RDPGFX_CMDID_MAPSURFACETOSCALEDOUTPUT, 20); + + if (!s) + { + WLog_ERR(TAG, "rdpgfx_server_single_packet_new failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + Stream_Write_UINT16(s, pdu->surfaceId); /* surfaceId (2 bytes) */ + Stream_Write_UINT16(s, 0); /* reserved (2 bytes). Must be 0 */ + Stream_Write_UINT32(s, pdu->outputOriginX); /* outputOriginX (4 bytes) */ + Stream_Write_UINT32(s, pdu->outputOriginY); /* outputOriginY (4 bytes) */ + Stream_Write_UINT32(s, pdu->targetWidth); /* targetWidth (4 bytes) */ + Stream_Write_UINT32(s, pdu->targetHeight); /* targetHeight (4 bytes) */ + return rdpgfx_server_single_packet_send(context, s); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpgfx_server_receive_pdu(RdpgfxServerContext* context, wStream* s) +{ + size_t beg, end; + RDPGFX_HEADER header; + UINT error = CHANNEL_RC_OK; + beg = Stream_GetPosition(s); + + if ((error = rdpgfx_read_header(s, &header))) + { + WLog_ERR(TAG, "rdpgfx_read_header failed with error %" PRIu32 "!", error); + return error; + } + +#ifdef WITH_DEBUG_RDPGFX + WLog_DBG(TAG, "cmdId: %s (0x%04" PRIX16 ") flags: 0x%04" PRIX16 " pduLength: %" PRIu32 "", + rdpgfx_get_cmd_id_string(header.cmdId), header.cmdId, header.flags, header.pduLength); +#endif + + switch (header.cmdId) + { + case RDPGFX_CMDID_FRAMEACKNOWLEDGE: + if ((error = rdpgfx_recv_frame_acknowledge_pdu(context, s))) + WLog_ERR(TAG, + "rdpgfx_recv_frame_acknowledge_pdu " + "failed with error %" PRIu32 "!", + error); + + break; + + case RDPGFX_CMDID_CACHEIMPORTOFFER: + if ((error = rdpgfx_recv_cache_import_offer_pdu(context, s))) + WLog_ERR(TAG, + "rdpgfx_recv_cache_import_offer_pdu " + "failed with error %" PRIu32 "!", + error); + + break; + + case RDPGFX_CMDID_CAPSADVERTISE: + if ((error = rdpgfx_recv_caps_advertise_pdu(context, s))) + WLog_ERR(TAG, + "rdpgfx_recv_caps_advertise_pdu " + "failed with error %" PRIu32 "!", + error); + + break; + + case RDPGFX_CMDID_QOEFRAMEACKNOWLEDGE: + if ((error = rdpgfx_recv_qoe_frame_acknowledge_pdu(context, s))) + WLog_ERR(TAG, + "rdpgfx_recv_qoe_frame_acknowledge_pdu " + "failed with error %" PRIu32 "!", + error); + + break; + + default: + error = CHANNEL_RC_BAD_PROC; + break; + } + + if (error) + { + WLog_ERR(TAG, "Error while parsing GFX cmdId: %s (0x%04" PRIX16 ")", + rdpgfx_get_cmd_id_string(header.cmdId), header.cmdId); + return error; + } + + end = Stream_GetPosition(s); + + if (end != (beg + header.pduLength)) + { + WLog_ERR(TAG, "Unexpected gfx pdu end: Actual: %d, Expected: %" PRIu32 "", end, + (beg + header.pduLength)); + Stream_SetPosition(s, (beg + header.pduLength)); + } + + return error; +} + +static DWORD WINAPI rdpgfx_server_thread_func(LPVOID arg) +{ + RdpgfxServerContext* context = (RdpgfxServerContext*)arg; + RdpgfxServerPrivate* priv = context->priv; + DWORD status; + DWORD nCount; + void* buffer; + HANDLE events[8]; + UINT error = CHANNEL_RC_OK; + buffer = NULL; + nCount = 0; + events[nCount++] = priv->stopEvent; + events[nCount++] = priv->channelEvent; + + /* Main virtual channel loop. RDPGFX do not need version negotiation */ + while (TRUE) + { + status = WaitForMultipleObjects(nCount, events, FALSE, INFINITE); + + if (status == WAIT_FAILED) + { + error = GetLastError(); + WLog_ERR(TAG, "WaitForMultipleObjects failed with error %" PRIu32 "", error); + break; + } + + /* Stop Event */ + if (status == WAIT_OBJECT_0) + break; + + if ((error = rdpgfx_server_handle_messages(context))) + { + WLog_ERR(TAG, "rdpgfx_server_handle_messages failed with error %" PRIu32 "", error); + break; + } + } + + if (error && context->rdpcontext) + setChannelError(context->rdpcontext, error, "rdpgfx_server_thread_func reported an error"); + + ExitThread(error); + return error; +} + +static BOOL rdpgfx_server_open(RdpgfxServerContext* context) +{ + RdpgfxServerPrivate* priv = (RdpgfxServerPrivate*)context->priv; + void* buffer = NULL; + + if (!priv->isOpened) + { + PULONG pSessionId = NULL; + DWORD BytesReturned = 0; + priv->SessionId = WTS_CURRENT_SESSION; + UINT32 channelId; + BOOL status = TRUE; + + if (WTSQuerySessionInformationA(context->vcm, WTS_CURRENT_SESSION, WTSSessionId, + (LPSTR*)&pSessionId, &BytesReturned) == FALSE) + { + WLog_ERR(TAG, "WTSQuerySessionInformationA failed!"); + return FALSE; + } + + priv->SessionId = (DWORD)*pSessionId; + WTSFreeMemory(pSessionId); + priv->rdpgfx_channel = WTSVirtualChannelOpenEx(priv->SessionId, RDPGFX_DVC_CHANNEL_NAME, + WTS_CHANNEL_OPTION_DYNAMIC); + + if (!priv->rdpgfx_channel) + { + WLog_ERR(TAG, "WTSVirtualChannelOpenEx failed!"); + return FALSE; + } + + channelId = WTSChannelGetIdByHandle(priv->rdpgfx_channel); + + IFCALLRET(context->ChannelIdAssigned, status, context, channelId); + if (!status) + { + WLog_ERR(TAG, "context->ChannelIdAssigned failed!"); + goto out_close; + } + + /* Query for channel event handle */ + if (!WTSVirtualChannelQuery(priv->rdpgfx_channel, WTSVirtualEventHandle, &buffer, + &BytesReturned) || + (BytesReturned != sizeof(HANDLE))) + { + WLog_ERR(TAG, + "WTSVirtualChannelQuery failed " + "or invalid returned size(%" PRIu32 ")", + BytesReturned); + + if (buffer) + WTSFreeMemory(buffer); + + goto out_close; + } + + CopyMemory(&priv->channelEvent, buffer, sizeof(HANDLE)); + WTSFreeMemory(buffer); + + if (!(priv->zgfx = zgfx_context_new(TRUE))) + { + WLog_ERR(TAG, "Create zgfx context failed!"); + goto out_close; + } + + if (priv->ownThread) + { + if (!(priv->stopEvent = CreateEvent(NULL, TRUE, FALSE, NULL))) + { + WLog_ERR(TAG, "CreateEvent failed!"); + goto out_zgfx; + } + + if (!(priv->thread = + CreateThread(NULL, 0, rdpgfx_server_thread_func, (void*)context, 0, NULL))) + { + WLog_ERR(TAG, "CreateThread failed!"); + goto out_stopEvent; + } + } + + priv->isOpened = TRUE; + priv->isReady = FALSE; + return TRUE; + } + + WLog_ERR(TAG, "RDPGFX channel is already opened!"); + return FALSE; +out_stopEvent: + CloseHandle(priv->stopEvent); + priv->stopEvent = NULL; +out_zgfx: + zgfx_context_free(priv->zgfx); + priv->zgfx = NULL; +out_close: + WTSVirtualChannelClose(priv->rdpgfx_channel); + priv->rdpgfx_channel = NULL; + priv->channelEvent = NULL; + return FALSE; +} + +static BOOL rdpgfx_server_close(RdpgfxServerContext* context) +{ + RdpgfxServerPrivate* priv = (RdpgfxServerPrivate*)context->priv; + + if (priv->ownThread && priv->thread) + { + SetEvent(priv->stopEvent); + + if (WaitForSingleObject(priv->thread, INFINITE) == WAIT_FAILED) + { + WLog_ERR(TAG, "WaitForSingleObject failed with error %" PRIu32 "", GetLastError()); + return FALSE; + } + + CloseHandle(priv->thread); + CloseHandle(priv->stopEvent); + priv->thread = NULL; + priv->stopEvent = NULL; + } + + zgfx_context_free(priv->zgfx); + priv->zgfx = NULL; + + if (priv->rdpgfx_channel) + { + WTSVirtualChannelClose(priv->rdpgfx_channel); + priv->rdpgfx_channel = NULL; + } + + priv->channelEvent = NULL; + priv->isOpened = FALSE; + priv->isReady = FALSE; + return TRUE; +} + +RdpgfxServerContext* rdpgfx_server_context_new(HANDLE vcm) +{ + RdpgfxServerContext* context; + RdpgfxServerPrivate* priv; + context = (RdpgfxServerContext*)calloc(1, sizeof(RdpgfxServerContext)); + + if (!context) + { + WLog_ERR(TAG, "calloc failed!"); + return NULL; + } + + context->vcm = vcm; + context->Open = rdpgfx_server_open; + context->Close = rdpgfx_server_close; + context->ResetGraphics = rdpgfx_send_reset_graphics_pdu; + context->StartFrame = rdpgfx_send_start_frame_pdu; + context->EndFrame = rdpgfx_send_end_frame_pdu; + context->SurfaceCommand = rdpgfx_send_surface_command; + context->SurfaceFrameCommand = rdpgfx_send_surface_frame_command; + context->DeleteEncodingContext = rdpgfx_send_delete_encoding_context_pdu; + context->CreateSurface = rdpgfx_send_create_surface_pdu; + context->DeleteSurface = rdpgfx_send_delete_surface_pdu; + context->SolidFill = rdpgfx_send_solid_fill_pdu; + context->SurfaceToSurface = rdpgfx_send_surface_to_surface_pdu; + context->SurfaceToCache = rdpgfx_send_surface_to_cache_pdu; + context->CacheToSurface = rdpgfx_send_cache_to_surface_pdu; + context->CacheImportOffer = NULL; + context->CacheImportReply = rdpgfx_send_cache_import_reply_pdu; + context->EvictCacheEntry = rdpgfx_send_evict_cache_entry_pdu; + context->MapSurfaceToOutput = rdpgfx_send_map_surface_to_output_pdu; + context->MapSurfaceToWindow = rdpgfx_send_map_surface_to_window_pdu; + context->MapSurfaceToScaledOutput = rdpgfx_send_map_surface_to_scaled_output_pdu; + context->MapSurfaceToScaledWindow = rdpgfx_send_map_surface_to_scaled_window_pdu; + context->CapsAdvertise = NULL; + context->CapsConfirm = rdpgfx_send_caps_confirm_pdu; + context->FrameAcknowledge = NULL; + context->QoeFrameAcknowledge = NULL; + context->priv = priv = (RdpgfxServerPrivate*)calloc(1, sizeof(RdpgfxServerPrivate)); + + if (!priv) + { + WLog_ERR(TAG, "calloc failed!"); + goto out_free; + } + + /* Create shared input stream */ + priv->input_stream = Stream_New(NULL, 4); + + if (!priv->input_stream) + { + WLog_ERR(TAG, "Stream_New failed!"); + goto out_free_priv; + } + + priv->isOpened = FALSE; + priv->isReady = FALSE; + priv->ownThread = TRUE; + return (RdpgfxServerContext*)context; +out_free_priv: + free(context->priv); +out_free: + free(context); + return NULL; +} + +void rdpgfx_server_context_free(RdpgfxServerContext* context) +{ + rdpgfx_server_close(context); + + if (context->priv) + Stream_Free(context->priv->input_stream, TRUE); + + free(context->priv); + free(context); +} + +HANDLE rdpgfx_server_get_event_handle(RdpgfxServerContext* context) +{ + return context->priv->channelEvent; +} + +/* + * Handle rpdgfx messages - server side + * + * @param Server side context + * + * @return 0 on success + * ERROR_NO_DATA if no data could be read this time + * otherwise a Win32 error code + */ +UINT rdpgfx_server_handle_messages(RdpgfxServerContext* context) +{ + DWORD BytesReturned; + void* buffer; + UINT ret = CHANNEL_RC_OK; + RdpgfxServerPrivate* priv = context->priv; + wStream* s = priv->input_stream; + + /* Check whether the dynamic channel is ready */ + if (!priv->isReady) + { + if (WTSVirtualChannelQuery(priv->rdpgfx_channel, WTSVirtualChannelReady, &buffer, + &BytesReturned) == FALSE) + { + if (GetLastError() == ERROR_NO_DATA) + return ERROR_NO_DATA; + + WLog_ERR(TAG, "WTSVirtualChannelQuery failed"); + return ERROR_INTERNAL_ERROR; + } + + priv->isReady = *((BOOL*)buffer); + WTSFreeMemory(buffer); + } + + /* Consume channel event only after the gfx dynamic channel is ready */ + if (priv->isReady) + { + Stream_SetPosition(s, 0); + + if (!WTSVirtualChannelRead(priv->rdpgfx_channel, 0, NULL, 0, &BytesReturned)) + { + if (GetLastError() == ERROR_NO_DATA) + return ERROR_NO_DATA; + + WLog_ERR(TAG, "WTSVirtualChannelRead failed!"); + return ERROR_INTERNAL_ERROR; + } + + if (BytesReturned < 1) + return CHANNEL_RC_OK; + + if (!Stream_EnsureRemainingCapacity(s, BytesReturned)) + { + WLog_ERR(TAG, "Stream_EnsureRemainingCapacity failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + if (WTSVirtualChannelRead(priv->rdpgfx_channel, 0, (PCHAR)Stream_Buffer(s), + Stream_Capacity(s), &BytesReturned) == FALSE) + { + WLog_ERR(TAG, "WTSVirtualChannelRead failed!"); + return ERROR_INTERNAL_ERROR; + } + + Stream_SetLength(s, BytesReturned); + Stream_SetPosition(s, 0); + + while (Stream_GetPosition(s) < Stream_Length(s)) + { + if ((ret = rdpgfx_server_receive_pdu(context, s))) + { + WLog_ERR(TAG, + "rdpgfx_server_receive_pdu " + "failed with error %" PRIu32 "!", + ret); + return ret; + } + } + } + + return ret; +} diff --git a/channels/rdpgfx/server/rdpgfx_main.h b/channels/rdpgfx/server/rdpgfx_main.h new file mode 100644 index 0000000..be29b76 --- /dev/null +++ b/channels/rdpgfx/server/rdpgfx_main.h @@ -0,0 +1,40 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Graphics Pipeline Extension + * + * Copyright 2016 Jiang Zihao + * + * Licensed 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 FREERDP_CHANNEL_RDPGFX_SERVER_MAIN_H +#define FREERDP_CHANNEL_RDPGFX_SERVER_MAIN_H + +#include +#include + +struct _rdpgfx_server_private +{ + ZGFX_CONTEXT* zgfx; + BOOL ownThread; + HANDLE thread; + HANDLE stopEvent; + HANDLE channelEvent; + void* rdpgfx_channel; + DWORD SessionId; + wStream* input_stream; + BOOL isOpened; + BOOL isReady; +}; + +#endif /* FREERDP_CHANNEL_RDPGFX_SERVER_MAIN_H */ diff --git a/channels/rdpsnd/CMakeLists.txt b/channels/rdpsnd/CMakeLists.txt new file mode 100644 index 0000000..08b6836 --- /dev/null +++ b/channels/rdpsnd/CMakeLists.txt @@ -0,0 +1,29 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2012 Marc-Andre Moreau +# +# Licensed 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. + +define_channel("rdpsnd") + +include_directories(common) +add_subdirectory(common) + +if(WITH_CLIENT_CHANNELS) + add_channel_client(${MODULE_PREFIX} ${CHANNEL_NAME}) +endif() + +if(WITH_SERVER_CHANNELS) + add_channel_server(${MODULE_PREFIX} ${CHANNEL_NAME}) +endif() diff --git a/channels/rdpsnd/ChannelOptions.cmake b/channels/rdpsnd/ChannelOptions.cmake new file mode 100644 index 0000000..948ba97 --- /dev/null +++ b/channels/rdpsnd/ChannelOptions.cmake @@ -0,0 +1,13 @@ + +set(OPTION_DEFAULT OFF) +set(OPTION_CLIENT_DEFAULT ON) +set(OPTION_SERVER_DEFAULT ON) + +define_channel_options(NAME "rdpsnd" TYPE "static;dynamic" + DESCRIPTION "Audio Output Virtual Channel Extension" + SPECIFICATIONS "[MS-RDPEA]" + DEFAULT ${OPTION_DEFAULT}) + +define_channel_client_options(${OPTION_CLIENT_DEFAULT}) +define_channel_server_options(${OPTION_SERVER_DEFAULT}) + diff --git a/channels/rdpsnd/client/CMakeLists.txt b/channels/rdpsnd/client/CMakeLists.txt new file mode 100644 index 0000000..70f4aa2 --- /dev/null +++ b/channels/rdpsnd/client/CMakeLists.txt @@ -0,0 +1,64 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2012 Marc-Andre Moreau +# +# Licensed 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. + +define_channel_client("rdpsnd") + +set(${MODULE_PREFIX}_SRCS + rdpsnd_main.c + rdpsnd_main.h) + +add_channel_client_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} FALSE "VirtualChannelEntryEx;DVCPluginEntry") + +target_link_libraries(${MODULE_NAME} + winpr freerdp ${CMAKE_THREAD_LIBS_INIT} +) + +set_property(TARGET ${MODULE_NAME} PROPERTY FOLDER "Channels/${CHANNEL_NAME}/Client") + +if(WITH_OSS) + add_channel_client_subsystem(${MODULE_PREFIX} ${CHANNEL_NAME} "oss" "") +endif() + +if(WITH_ALSA) + add_channel_client_subsystem(${MODULE_PREFIX} ${CHANNEL_NAME} "alsa" "") +endif() + +if(WITH_IOSAUDIO) + add_channel_client_subsystem(${MODULE_PREFIX} ${CHANNEL_NAME} "ios" "") +endif() + +if(WITH_PULSE) + add_channel_client_subsystem(${MODULE_PREFIX} ${CHANNEL_NAME} "pulse" "") +endif() + +if(WITH_MACAUDIO) + add_channel_client_subsystem(${MODULE_PREFIX} ${CHANNEL_NAME} "mac" "") +endif() + +if(WITH_WINMM) + add_channel_client_subsystem(${MODULE_PREFIX} ${CHANNEL_NAME} "winmm" "") +endif() + +if(WITH_OPENSLES) + add_channel_client_subsystem(${MODULE_PREFIX} ${CHANNEL_NAME} "opensles" "") +endif() + +if (WITH_SERVER) + add_channel_client_subsystem(${MODULE_PREFIX} ${CHANNEL_NAME} "proxy" "") +endif() + +add_channel_client_subsystem(${MODULE_PREFIX} ${CHANNEL_NAME} "fake" "") diff --git a/channels/rdpsnd/client/alsa/CMakeLists.txt b/channels/rdpsnd/client/alsa/CMakeLists.txt new file mode 100644 index 0000000..761a639 --- /dev/null +++ b/channels/rdpsnd/client/alsa/CMakeLists.txt @@ -0,0 +1,36 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2012 Marc-Andre Moreau +# +# Licensed 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. + +define_channel_client_subsystem("rdpsnd" "alsa" "") + +set(${MODULE_PREFIX}_SRCS + rdpsnd_alsa.c) + +include_directories(..) +include_directories(${ALSA_INCLUDE_DIRS}) + +add_channel_client_subsystem_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} "" TRUE "") + + + +set(${MODULE_PREFIX}_LIBS ${${MODULE_PREFIX}_LIBS} winpr freerdp) + +set(${MODULE_PREFIX}_LIBS ${${MODULE_PREFIX}_LIBS} ${ALSA_LIBRARIES}) + +target_link_libraries(${MODULE_NAME} ${${MODULE_PREFIX}_LIBS}) + +set_property(TARGET ${MODULE_NAME} PROPERTY FOLDER "Channels/${CHANNEL_NAME}/Client/ALSA") diff --git a/channels/rdpsnd/client/alsa/rdpsnd_alsa.c b/channels/rdpsnd/client/alsa/rdpsnd_alsa.c new file mode 100644 index 0000000..a903596 --- /dev/null +++ b/channels/rdpsnd/client/alsa/rdpsnd_alsa.c @@ -0,0 +1,576 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Audio Output Virtual Channel + * + * Copyright 2009-2011 Jay Sorg + * Copyright 2010-2011 Vic Lee + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include + +#include +#include +#include +#include + +#include + +#include +#include +#include + +#include "rdpsnd_main.h" + +typedef struct rdpsnd_alsa_plugin rdpsndAlsaPlugin; + +struct rdpsnd_alsa_plugin +{ + rdpsndDevicePlugin device; + + UINT32 latency; + AUDIO_FORMAT aformat; + char* device_name; + snd_pcm_t* pcm_handle; + snd_mixer_t* mixer_handle; + + UINT32 actual_rate; + snd_pcm_format_t format; + UINT32 actual_channels; + + snd_pcm_uframes_t buffer_size; + snd_pcm_uframes_t period_size; +}; + +#define SND_PCM_CHECK(_func, _status) \ + if (_status < 0) \ + { \ + WLog_ERR(TAG, "%s: %d\n", _func, _status); \ + return -1; \ + } + +static int rdpsnd_alsa_set_hw_params(rdpsndAlsaPlugin* alsa) +{ + int status; + snd_pcm_hw_params_t* hw_params; + snd_pcm_uframes_t buffer_size_max; + status = snd_pcm_hw_params_malloc(&hw_params); + SND_PCM_CHECK("snd_pcm_hw_params_malloc", status); + status = snd_pcm_hw_params_any(alsa->pcm_handle, hw_params); + SND_PCM_CHECK("snd_pcm_hw_params_any", status); + /* Set interleaved read/write access */ + status = + snd_pcm_hw_params_set_access(alsa->pcm_handle, hw_params, SND_PCM_ACCESS_RW_INTERLEAVED); + SND_PCM_CHECK("snd_pcm_hw_params_set_access", status); + /* Set sample format */ + status = snd_pcm_hw_params_set_format(alsa->pcm_handle, hw_params, alsa->format); + SND_PCM_CHECK("snd_pcm_hw_params_set_format", status); + /* Set sample rate */ + status = snd_pcm_hw_params_set_rate_near(alsa->pcm_handle, hw_params, &alsa->actual_rate, NULL); + SND_PCM_CHECK("snd_pcm_hw_params_set_rate_near", status); + /* Set number of channels */ + status = snd_pcm_hw_params_set_channels(alsa->pcm_handle, hw_params, alsa->actual_channels); + SND_PCM_CHECK("snd_pcm_hw_params_set_channels", status); + /* Get maximum buffer size */ + status = snd_pcm_hw_params_get_buffer_size_max(hw_params, &buffer_size_max); + SND_PCM_CHECK("snd_pcm_hw_params_get_buffer_size_max", status); + /** + * ALSA Parameters + * + * http://www.alsa-project.org/main/index.php/FramesPeriods + * + * buffer_size = period_size * periods + * period_bytes = period_size * bytes_per_frame + * bytes_per_frame = channels * bytes_per_sample + * + * A frame is equivalent of one sample being played, + * irrespective of the number of channels or the number of bits + * + * A period is the number of frames in between each hardware interrupt. + * + * The buffer size always has to be greater than one period size. + * Commonly this is (2 * period_size), but some hardware can do 8 periods per buffer. + * It is also possible for the buffer size to not be an integer multiple of the period size. + */ + int interrupts_per_sec_near = 50; + int bytes_per_sec = + (alsa->actual_rate * alsa->aformat.wBitsPerSample / 8 * alsa->actual_channels); + alsa->buffer_size = buffer_size_max; + alsa->period_size = (bytes_per_sec / interrupts_per_sec_near); + + if (alsa->period_size > buffer_size_max) + { + WLog_ERR(TAG, "Warning: requested sound buffer size %lu, got %lu instead\n", + alsa->buffer_size, buffer_size_max); + alsa->period_size = (buffer_size_max / 8); + } + + /* Set buffer size */ + status = + snd_pcm_hw_params_set_buffer_size_near(alsa->pcm_handle, hw_params, &alsa->buffer_size); + SND_PCM_CHECK("snd_pcm_hw_params_set_buffer_size_near", status); + /* Set period size */ + status = snd_pcm_hw_params_set_period_size_near(alsa->pcm_handle, hw_params, &alsa->period_size, + NULL); + SND_PCM_CHECK("snd_pcm_hw_params_set_period_size_near", status); + status = snd_pcm_hw_params(alsa->pcm_handle, hw_params); + SND_PCM_CHECK("snd_pcm_hw_params", status); + snd_pcm_hw_params_free(hw_params); + return 0; +} + +static int rdpsnd_alsa_set_sw_params(rdpsndAlsaPlugin* alsa) +{ + int status; + snd_pcm_sw_params_t* sw_params; + status = snd_pcm_sw_params_malloc(&sw_params); + SND_PCM_CHECK("snd_pcm_sw_params_malloc", status); + status = snd_pcm_sw_params_current(alsa->pcm_handle, sw_params); + SND_PCM_CHECK("snd_pcm_sw_params_current", status); + status = snd_pcm_sw_params_set_avail_min(alsa->pcm_handle, sw_params, + (alsa->aformat.nChannels * alsa->actual_channels)); + SND_PCM_CHECK("snd_pcm_sw_params_set_avail_min", status); + status = snd_pcm_sw_params_set_start_threshold(alsa->pcm_handle, sw_params, + alsa->aformat.nBlockAlign); + SND_PCM_CHECK("snd_pcm_sw_params_set_start_threshold", status); + status = snd_pcm_sw_params(alsa->pcm_handle, sw_params); + SND_PCM_CHECK("snd_pcm_sw_params", status); + snd_pcm_sw_params_free(sw_params); + status = snd_pcm_prepare(alsa->pcm_handle); + SND_PCM_CHECK("snd_pcm_prepare", status); + return 0; +} + +static int rdpsnd_alsa_validate_params(rdpsndAlsaPlugin* alsa) +{ + int status; + snd_pcm_uframes_t buffer_size; + snd_pcm_uframes_t period_size; + status = snd_pcm_get_params(alsa->pcm_handle, &buffer_size, &period_size); + SND_PCM_CHECK("snd_pcm_get_params", status); + return 0; +} + +static int rdpsnd_alsa_set_params(rdpsndAlsaPlugin* alsa) +{ + snd_pcm_drop(alsa->pcm_handle); + + if (rdpsnd_alsa_set_hw_params(alsa) < 0) + return -1; + + if (rdpsnd_alsa_set_sw_params(alsa) < 0) + return -1; + + return rdpsnd_alsa_validate_params(alsa); +} + +static BOOL rdpsnd_alsa_set_format(rdpsndDevicePlugin* device, const AUDIO_FORMAT* format, + UINT32 latency) +{ + rdpsndAlsaPlugin* alsa = (rdpsndAlsaPlugin*)device; + + if (format) + { + alsa->aformat = *format; + alsa->actual_rate = format->nSamplesPerSec; + alsa->actual_channels = format->nChannels; + + switch (format->wFormatTag) + { + case WAVE_FORMAT_PCM: + switch (format->wBitsPerSample) + { + case 8: + alsa->format = SND_PCM_FORMAT_S8; + break; + + case 16: + alsa->format = SND_PCM_FORMAT_S16_LE; + break; + + default: + return FALSE; + } + + break; + + default: + return FALSE; + } + } + + alsa->latency = latency; + return (rdpsnd_alsa_set_params(alsa) == 0); +} + +static void rdpsnd_alsa_close_mixer(rdpsndAlsaPlugin* alsa) +{ + if (alsa && alsa->mixer_handle) + { + snd_mixer_close(alsa->mixer_handle); + alsa->mixer_handle = NULL; + } +} + +static BOOL rdpsnd_alsa_open_mixer(rdpsndAlsaPlugin* alsa) +{ + int status; + + if (alsa->mixer_handle) + return TRUE; + + status = snd_mixer_open(&alsa->mixer_handle, 0); + + if (status < 0) + { + WLog_ERR(TAG, "snd_mixer_open failed"); + goto fail; + } + + status = snd_mixer_attach(alsa->mixer_handle, alsa->device_name); + + if (status < 0) + { + WLog_ERR(TAG, "snd_mixer_attach failed"); + goto fail; + } + + status = snd_mixer_selem_register(alsa->mixer_handle, NULL, NULL); + + if (status < 0) + { + WLog_ERR(TAG, "snd_mixer_selem_register failed"); + goto fail; + } + + status = snd_mixer_load(alsa->mixer_handle); + + if (status < 0) + { + WLog_ERR(TAG, "snd_mixer_load failed"); + goto fail; + } + + return TRUE; +fail: + rdpsnd_alsa_close_mixer(alsa); + return FALSE; +} + +static void rdpsnd_alsa_pcm_close(rdpsndAlsaPlugin* alsa) +{ + if (alsa && alsa->pcm_handle) + { + snd_pcm_drain(alsa->pcm_handle); + snd_pcm_close(alsa->pcm_handle); + alsa->pcm_handle = 0; + } +} + +static BOOL rdpsnd_alsa_open(rdpsndDevicePlugin* device, const AUDIO_FORMAT* format, UINT32 latency) +{ + int mode; + int status; + rdpsndAlsaPlugin* alsa = (rdpsndAlsaPlugin*)device; + + if (alsa->pcm_handle) + return TRUE; + + mode = 0; + /*mode |= SND_PCM_NONBLOCK;*/ + status = snd_pcm_open(&alsa->pcm_handle, alsa->device_name, SND_PCM_STREAM_PLAYBACK, mode); + + if (status < 0) + { + WLog_ERR(TAG, "snd_pcm_open failed"); + return FALSE; + } + + return rdpsnd_alsa_set_format(device, format, latency) && rdpsnd_alsa_open_mixer(alsa); +} + +static void rdpsnd_alsa_close(rdpsndDevicePlugin* device) +{ + rdpsndAlsaPlugin* alsa = (rdpsndAlsaPlugin*)device; + + if (!alsa) + return; + + rdpsnd_alsa_close_mixer(alsa); +} + +static void rdpsnd_alsa_free(rdpsndDevicePlugin* device) +{ + rdpsndAlsaPlugin* alsa = (rdpsndAlsaPlugin*)device; + rdpsnd_alsa_pcm_close(alsa); + rdpsnd_alsa_close_mixer(alsa); + free(alsa->device_name); + free(alsa); +} + +static BOOL rdpsnd_alsa_format_supported(rdpsndDevicePlugin* device, const AUDIO_FORMAT* format) +{ + switch (format->wFormatTag) + { + case WAVE_FORMAT_PCM: + if (format->cbSize == 0 && format->nSamplesPerSec <= 48000 && + (format->wBitsPerSample == 8 || format->wBitsPerSample == 16) && + (format->nChannels == 1 || format->nChannels == 2)) + { + return TRUE; + } + + break; + } + + return FALSE; +} + +static UINT32 rdpsnd_alsa_get_volume(rdpsndDevicePlugin* device) +{ + long volume_min; + long volume_max; + long volume_left; + long volume_right; + UINT32 dwVolume; + UINT16 dwVolumeLeft; + UINT16 dwVolumeRight; + snd_mixer_elem_t* elem; + rdpsndAlsaPlugin* alsa = (rdpsndAlsaPlugin*)device; + dwVolumeLeft = ((50 * 0xFFFF) / 100); /* 50% */ + dwVolumeRight = ((50 * 0xFFFF) / 100); /* 50% */ + + if (!rdpsnd_alsa_open_mixer(alsa)) + return 0; + + for (elem = snd_mixer_first_elem(alsa->mixer_handle); elem; elem = snd_mixer_elem_next(elem)) + { + if (snd_mixer_selem_has_playback_volume(elem)) + { + snd_mixer_selem_get_playback_volume_range(elem, &volume_min, &volume_max); + snd_mixer_selem_get_playback_volume(elem, SND_MIXER_SCHN_FRONT_LEFT, &volume_left); + snd_mixer_selem_get_playback_volume(elem, SND_MIXER_SCHN_FRONT_RIGHT, &volume_right); + dwVolumeLeft = + (UINT16)(((volume_left * 0xFFFF) - volume_min) / (volume_max - volume_min)); + dwVolumeRight = + (UINT16)(((volume_right * 0xFFFF) - volume_min) / (volume_max - volume_min)); + break; + } + } + + dwVolume = (dwVolumeLeft << 16) | dwVolumeRight; + return dwVolume; +} + +static BOOL rdpsnd_alsa_set_volume(rdpsndDevicePlugin* device, UINT32 value) +{ + long left; + long right; + long volume_min; + long volume_max; + long volume_left; + long volume_right; + snd_mixer_elem_t* elem; + rdpsndAlsaPlugin* alsa = (rdpsndAlsaPlugin*)device; + + if (!rdpsnd_alsa_open_mixer(alsa)) + return FALSE; + + left = (value & 0xFFFF); + right = ((value >> 16) & 0xFFFF); + + for (elem = snd_mixer_first_elem(alsa->mixer_handle); elem; elem = snd_mixer_elem_next(elem)) + { + if (snd_mixer_selem_has_playback_volume(elem)) + { + snd_mixer_selem_get_playback_volume_range(elem, &volume_min, &volume_max); + volume_left = volume_min + (left * (volume_max - volume_min)) / 0xFFFF; + volume_right = volume_min + (right * (volume_max - volume_min)) / 0xFFFF; + + if ((snd_mixer_selem_set_playback_volume(elem, SND_MIXER_SCHN_FRONT_LEFT, volume_left) < + 0) || + (snd_mixer_selem_set_playback_volume(elem, SND_MIXER_SCHN_FRONT_RIGHT, + volume_right) < 0)) + { + WLog_ERR(TAG, "error setting the volume\n"); + return FALSE; + } + } + } + + return TRUE; +} + +static UINT rdpsnd_alsa_play(rdpsndDevicePlugin* device, const BYTE* data, size_t size) +{ + UINT latency; + size_t offset; + int frame_size; + rdpsndAlsaPlugin* alsa = (rdpsndAlsaPlugin*)device; + offset = 0; + frame_size = alsa->actual_channels * alsa->aformat.wBitsPerSample / 8; + + while (offset < size) + { + snd_pcm_sframes_t status = + snd_pcm_writei(alsa->pcm_handle, &data[offset], (size - offset) / frame_size); + + if (status < 0) + status = snd_pcm_recover(alsa->pcm_handle, status, 0); + + if (status < 0) + { + WLog_ERR(TAG, "status: %d\n", status); + rdpsnd_alsa_close(device); + rdpsnd_alsa_open(device, NULL, alsa->latency); + break; + } + + offset += status * frame_size; + } + + { + snd_pcm_sframes_t available, delay; + int rc = snd_pcm_avail_delay(alsa->pcm_handle, &available, &delay); + + if (rc != 0) + latency = 0; + else if (available == 0) /* Get [ms] from number of samples */ + latency = delay * 1000 / alsa->actual_rate; + else + latency = 0; + } + + return latency + alsa->latency; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpsnd_alsa_parse_addin_args(rdpsndDevicePlugin* device, ADDIN_ARGV* args) +{ + int status; + DWORD flags; + COMMAND_LINE_ARGUMENT_A* arg; + rdpsndAlsaPlugin* alsa = (rdpsndAlsaPlugin*)device; + COMMAND_LINE_ARGUMENT_A rdpsnd_alsa_args[] = { { "dev", COMMAND_LINE_VALUE_REQUIRED, "", + NULL, NULL, -1, NULL, "device" }, + { NULL, 0, NULL, NULL, NULL, -1, NULL, NULL } }; + flags = + COMMAND_LINE_SIGIL_NONE | COMMAND_LINE_SEPARATOR_COLON | COMMAND_LINE_IGN_UNKNOWN_KEYWORD; + status = CommandLineParseArgumentsA(args->argc, args->argv, rdpsnd_alsa_args, flags, alsa, NULL, + NULL); + + if (status < 0) + { + WLog_ERR(TAG, "CommandLineParseArgumentsA failed!"); + return CHANNEL_RC_INITIALIZATION_ERROR; + } + + arg = rdpsnd_alsa_args; + + do + { + if (!(arg->Flags & COMMAND_LINE_VALUE_PRESENT)) + continue; + + CommandLineSwitchStart(arg) CommandLineSwitchCase(arg, "dev") + { + alsa->device_name = _strdup(arg->Value); + + if (!alsa->device_name) + return CHANNEL_RC_NO_MEMORY; + } + CommandLineSwitchEnd(arg) + } while ((arg = CommandLineFindNextArgumentA(arg)) != NULL); + + return CHANNEL_RC_OK; +} + +#ifdef BUILTIN_CHANNELS +#define freerdp_rdpsnd_client_subsystem_entry alsa_freerdp_rdpsnd_client_subsystem_entry +#else +#define freerdp_rdpsnd_client_subsystem_entry FREERDP_API freerdp_rdpsnd_client_subsystem_entry +#endif + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT freerdp_rdpsnd_client_subsystem_entry(PFREERDP_RDPSND_DEVICE_ENTRY_POINTS pEntryPoints) +{ + ADDIN_ARGV* args; + rdpsndAlsaPlugin* alsa; + UINT error; + alsa = (rdpsndAlsaPlugin*)calloc(1, sizeof(rdpsndAlsaPlugin)); + + if (!alsa) + { + WLog_ERR(TAG, "calloc failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + alsa->device.Open = rdpsnd_alsa_open; + alsa->device.FormatSupported = rdpsnd_alsa_format_supported; + alsa->device.GetVolume = rdpsnd_alsa_get_volume; + alsa->device.SetVolume = rdpsnd_alsa_set_volume; + alsa->device.Play = rdpsnd_alsa_play; + alsa->device.Close = rdpsnd_alsa_close; + alsa->device.Free = rdpsnd_alsa_free; + args = pEntryPoints->args; + + if (args->argc > 1) + { + if ((error = rdpsnd_alsa_parse_addin_args((rdpsndDevicePlugin*)alsa, args))) + { + WLog_ERR(TAG, "rdpsnd_alsa_parse_addin_args failed with error %" PRIu32 "", error); + goto error_parse_args; + } + } + + if (!alsa->device_name) + { + alsa->device_name = _strdup("default"); + + if (!alsa->device_name) + { + WLog_ERR(TAG, "_strdup failed!"); + error = CHANNEL_RC_NO_MEMORY; + goto error_strdup; + } + } + + alsa->pcm_handle = 0; + alsa->actual_rate = 22050; + alsa->format = SND_PCM_FORMAT_S16_LE; + alsa->actual_channels = 2; + pEntryPoints->pRegisterRdpsndDevice(pEntryPoints->rdpsnd, (rdpsndDevicePlugin*)alsa); + return CHANNEL_RC_OK; +error_strdup: + free(alsa->device_name); +error_parse_args: + free(alsa); + return error; +} diff --git a/channels/rdpsnd/client/fake/CMakeLists.txt b/channels/rdpsnd/client/fake/CMakeLists.txt new file mode 100644 index 0000000..fd0240a --- /dev/null +++ b/channels/rdpsnd/client/fake/CMakeLists.txt @@ -0,0 +1,33 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2019 Armin Novak +# Copyright 2019 Thincast Technologies GmbH +# +# Licensed 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. + +define_channel_client_subsystem("rdpsnd" "fake" "") + +set(${MODULE_PREFIX}_SRCS + rdpsnd_fake.c) + +include_directories(..) + +add_channel_client_subsystem_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} "" TRUE "") + +list(APPEND ${MODULE_PREFIX}_LIBS freerdp) +list(APPEND ${MODULE_PREFIX}_LIBS winpr) + +target_link_libraries(${MODULE_NAME} ${${MODULE_PREFIX}_LIBS}) + +set_property(TARGET ${MODULE_NAME} PROPERTY FOLDER "Channels/${CHANNEL_NAME}/Client/Fake") diff --git a/channels/rdpsnd/client/fake/rdpsnd_fake.c b/channels/rdpsnd/client/fake/rdpsnd_fake.c new file mode 100644 index 0000000..fe77cda --- /dev/null +++ b/channels/rdpsnd/client/fake/rdpsnd_fake.c @@ -0,0 +1,155 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Audio Output Virtual Channel + * + * Copyright 2019 Armin Novak + * Copyright 2019 Thincast Technologies GmbH + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include + +#include +#include +#include + +#include + +#include "rdpsnd_main.h" + +typedef struct rdpsnd_fake_plugin rdpsndFakePlugin; + +struct rdpsnd_fake_plugin +{ + rdpsndDevicePlugin device; +}; + +static BOOL rdpsnd_fake_open(rdpsndDevicePlugin* device, const AUDIO_FORMAT* format, UINT32 latency) +{ + return TRUE; +} + +static void rdpsnd_fake_close(rdpsndDevicePlugin* device) +{ +} + +static BOOL rdpsnd_fake_set_volume(rdpsndDevicePlugin* device, UINT32 value) +{ + return TRUE; +} + +static void rdpsnd_fake_free(rdpsndDevicePlugin* device) +{ + rdpsndFakePlugin* fake = (rdpsndFakePlugin*)device; + + if (!fake) + return; + + free(fake); +} + +static BOOL rdpsnd_fake_format_supported(rdpsndDevicePlugin* device, const AUDIO_FORMAT* format) +{ + return TRUE; +} + +static UINT rdpsnd_fake_play(rdpsndDevicePlugin* device, const BYTE* data, size_t size) +{ + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpsnd_fake_parse_addin_args(rdpsndFakePlugin* fake, ADDIN_ARGV* args) +{ + int status; + DWORD flags; + COMMAND_LINE_ARGUMENT_A* arg; + COMMAND_LINE_ARGUMENT_A rdpsnd_fake_args[] = { { NULL, 0, NULL, NULL, NULL, -1, NULL, NULL } }; + flags = + COMMAND_LINE_SIGIL_NONE | COMMAND_LINE_SEPARATOR_COLON | COMMAND_LINE_IGN_UNKNOWN_KEYWORD; + status = CommandLineParseArgumentsA(args->argc, args->argv, rdpsnd_fake_args, flags, fake, NULL, + NULL); + + if (status < 0) + return ERROR_INVALID_DATA; + + arg = rdpsnd_fake_args; + + do + { + if (!(arg->Flags & COMMAND_LINE_VALUE_PRESENT)) + continue; + + CommandLineSwitchStart(arg) CommandLineSwitchEnd(arg) + } while ((arg = CommandLineFindNextArgumentA(arg)) != NULL); + + return CHANNEL_RC_OK; +} + +#ifdef BUILTIN_CHANNELS +#define freerdp_rdpsnd_client_subsystem_entry fake_freerdp_rdpsnd_client_subsystem_entry +#else +#define freerdp_rdpsnd_client_subsystem_entry FREERDP_API freerdp_rdpsnd_client_subsystem_entry +#endif + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT freerdp_rdpsnd_client_subsystem_entry(PFREERDP_RDPSND_DEVICE_ENTRY_POINTS pEntryPoints) +{ + ADDIN_ARGV* args; + rdpsndFakePlugin* fake; + UINT ret = CHANNEL_RC_OK; + fake = (rdpsndFakePlugin*)calloc(1, sizeof(rdpsndFakePlugin)); + + if (!fake) + return CHANNEL_RC_NO_MEMORY; + + fake->device.Open = rdpsnd_fake_open; + fake->device.FormatSupported = rdpsnd_fake_format_supported; + fake->device.SetVolume = rdpsnd_fake_set_volume; + fake->device.Play = rdpsnd_fake_play; + fake->device.Close = rdpsnd_fake_close; + fake->device.Free = rdpsnd_fake_free; + args = pEntryPoints->args; + + if (args->argc > 1) + { + ret = rdpsnd_fake_parse_addin_args(fake, args); + + if (ret != CHANNEL_RC_OK) + { + WLog_ERR(TAG, "error parsing arguments"); + goto error; + } + } + + pEntryPoints->pRegisterRdpsndDevice(pEntryPoints->rdpsnd, &fake->device); + return ret; +error: + rdpsnd_fake_free(&fake->device); + return ret; +} diff --git a/channels/rdpsnd/client/ios/CMakeLists.txt b/channels/rdpsnd/client/ios/CMakeLists.txt new file mode 100644 index 0000000..ae9f9a7 --- /dev/null +++ b/channels/rdpsnd/client/ios/CMakeLists.txt @@ -0,0 +1,45 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2012 Laxmikant Rashinkar +# Copyright 2012 Marc-Andre Moreau +# Copyright 2013 Corey Clayton +# +# Licensed 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. + +define_channel_client_subsystem("rdpsnd" "ios" "") + +FIND_LIBRARY(CORE_AUDIO CoreAudio) +FIND_LIBRARY(AUDIO_TOOL AudioToolbox) +FIND_LIBRARY(CORE_FOUNDATION CoreFoundation) + +set(${MODULE_PREFIX}_SRCS + rdpsnd_ios.c + TPCircularBuffer.c) + +include_directories(..) + +add_channel_client_subsystem_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} "" TRUE "") + + + +set(${MODULE_PREFIX}_LIBS ${${MODULE_PREFIX}_LIBS} + ${AUDIO_TOOL} + ${CORE_AUDIO} + ${CORE_FOUNDATION}) + +set(${MODULE_PREFIX}_LIBS ${${MODULE_PREFIX}_LIBS} freerdp) + +target_link_libraries(${MODULE_NAME} ${${MODULE_PREFIX}_LIBS}) + +set_property(TARGET ${MODULE_NAME} PROPERTY FOLDER "Channels/${CHANNEL_NAME}/Client/ios") diff --git a/channels/rdpsnd/client/ios/TPCircularBuffer.c b/channels/rdpsnd/client/ios/TPCircularBuffer.c new file mode 100644 index 0000000..b29f611 --- /dev/null +++ b/channels/rdpsnd/client/ios/TPCircularBuffer.c @@ -0,0 +1,153 @@ +// +// TPCircularBuffer.c +// Circular/Ring buffer implementation +// +// https://github.com/michaeltyson/TPCircularBuffer +// +// Created by Michael Tyson on 10/12/2011. +// +// Copyright (C) 2012-2013 A Tasty Pixel +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// +// 3. This notice may not be removed or altered from any source distribution. +// + +#include + +#include "TPCircularBuffer.h" +#include "rdpsnd_main.h" + +#include +#include + +#define reportResult(result, operation) (_reportResult((result), (operation), __FILE__, __LINE__)) +static inline bool _reportResult(kern_return_t result, const char* operation, const char* file, + int line) +{ + if (result != ERR_SUCCESS) + { + WLog_DBG(TAG, "%s:%d: %s: %s\n", file, line, operation, mach_error_string(result)); + return false; + } + return true; +} + +bool TPCircularBufferInit(TPCircularBuffer* buffer, int length) +{ + + // Keep trying until we get our buffer, needed to handle race conditions + int retries = 3; + while (true) + { + + buffer->length = round_page(length); // We need whole page sizes + + // Temporarily allocate twice the length, so we have the contiguous address space to + // support a second instance of the buffer directly after + vm_address_t bufferAddress; + kern_return_t result = vm_allocate(mach_task_self(), &bufferAddress, buffer->length * 2, + VM_FLAGS_ANYWHERE); // allocate anywhere it'll fit + if (result != ERR_SUCCESS) + { + if (retries-- == 0) + { + reportResult(result, "Buffer allocation"); + return false; + } + // Try again if we fail + continue; + } + + // Now replace the second half of the allocation with a virtual copy of the first half. + // Deallocate the second half... + result = vm_deallocate(mach_task_self(), bufferAddress + buffer->length, buffer->length); + if (result != ERR_SUCCESS) + { + if (retries-- == 0) + { + reportResult(result, "Buffer deallocation"); + return false; + } + // If this fails somehow, deallocate the whole region and try again + vm_deallocate(mach_task_self(), bufferAddress, buffer->length); + continue; + } + + // Re-map the buffer to the address space immediately after the buffer + vm_address_t virtualAddress = bufferAddress + buffer->length; + vm_prot_t cur_prot, max_prot; + result = vm_remap(mach_task_self(), + &virtualAddress, // mirror target + buffer->length, // size of mirror + 0, // auto alignment + 0, // force remapping to virtualAddress + mach_task_self(), // same task + bufferAddress, // mirror source + 0, // MAP READ-WRITE, NOT COPY + &cur_prot, // unused protection struct + &max_prot, // unused protection struct + VM_INHERIT_DEFAULT); + if (result != ERR_SUCCESS) + { + if (retries-- == 0) + { + reportResult(result, "Remap buffer memory"); + return false; + } + // If this remap failed, we hit a race condition, so deallocate and try again + vm_deallocate(mach_task_self(), bufferAddress, buffer->length); + continue; + } + + if (virtualAddress != bufferAddress + buffer->length) + { + // If the memory is not contiguous, clean up both allocated buffers and try again + if (retries-- == 0) + { + WLog_DBG(TAG, "Couldn't map buffer memory to end of buffer"); + return false; + } + + vm_deallocate(mach_task_self(), virtualAddress, buffer->length); + vm_deallocate(mach_task_self(), bufferAddress, buffer->length); + continue; + } + + buffer->buffer = (void*)bufferAddress; + buffer->fillCount = 0; + buffer->head = buffer->tail = 0; + + return true; + } + return false; +} + +void TPCircularBufferCleanup(TPCircularBuffer* buffer) +{ + vm_deallocate(mach_task_self(), (vm_address_t)buffer->buffer, buffer->length * 2); + memset(buffer, 0, sizeof(TPCircularBuffer)); +} + +void TPCircularBufferClear(TPCircularBuffer* buffer) +{ + int32_t fillCount; + if (TPCircularBufferTail(buffer, &fillCount)) + { + TPCircularBufferConsume(buffer, fillCount); + } +} diff --git a/channels/rdpsnd/client/ios/TPCircularBuffer.h b/channels/rdpsnd/client/ios/TPCircularBuffer.h new file mode 100644 index 0000000..d246efa --- /dev/null +++ b/channels/rdpsnd/client/ios/TPCircularBuffer.h @@ -0,0 +1,217 @@ +// +// TPCircularBuffer.h +// Circular/Ring buffer implementation +// +// https://github.com/michaeltyson/TPCircularBuffer +// +// Created by Michael Tyson on 10/12/2011. +// +// +// This implementation makes use of a virtual memory mapping technique that inserts a virtual copy +// of the buffer memory directly after the buffer's end, negating the need for any buffer +// wrap-around logic. Clients can simply use the returned memory address as if it were contiguous +// space. +// +// The implementation is thread-safe in the case of a single producer and single consumer. +// +// Virtual memory technique originally proposed by Philip Howard (http://vrb.slashusr.org/), and +// adapted to Darwin by Kurt Revis (http://www.snoize.com, +// http://www.snoize.com/Code/PlayBufferedSoundFile.tar.gz) +// +// +// Copyright (C) 2012-2013 A Tasty Pixel +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// +// 3. This notice may not be removed or altered from any source distribution. +// + +#ifndef TPCircularBuffer_h +#define TPCircularBuffer_h + +#include +#include +#include + +#ifdef __cplusplus +extern "C" +{ +#endif + + typedef struct + { + void* buffer; + int32_t length; + int32_t tail; + int32_t head; + volatile int32_t fillCount; + } TPCircularBuffer; + + /*! + * Initialise buffer + * + * Note that the length is advisory only: Because of the way the + * memory mirroring technique works, the true buffer length will + * be multiples of the device page size (e.g. 4096 bytes) + * + * @param buffer Circular buffer + * @param length Length of buffer + */ + bool TPCircularBufferInit(TPCircularBuffer* buffer, int32_t length); + + /*! + * Cleanup buffer + * + * Releases buffer resources. + */ + void TPCircularBufferCleanup(TPCircularBuffer* buffer); + + /*! + * Clear buffer + * + * Resets buffer to original, empty state. + * + * This is safe for use by consumer while producer is accessing + * buffer. + */ + void TPCircularBufferClear(TPCircularBuffer* buffer); + + // Reading (consuming) + + /*! + * Access end of buffer + * + * This gives you a pointer to the end of the buffer, ready + * for reading, and the number of available bytes to read. + * + * @param buffer Circular buffer + * @param availableBytes On output, the number of bytes ready for reading + * @return Pointer to the first bytes ready for reading, or NULL if buffer is empty + */ + static __inline__ __attribute__((always_inline)) void* + TPCircularBufferTail(TPCircularBuffer* buffer, int32_t* availableBytes) + { + *availableBytes = buffer->fillCount; + if (*availableBytes == 0) + return NULL; + return (void*)((char*)buffer->buffer + buffer->tail); + } + + /*! + * Consume bytes in buffer + * + * This frees up the just-read bytes, ready for writing again. + * + * @param buffer Circular buffer + * @param amount Number of bytes to consume + */ + static __inline__ __attribute__((always_inline)) void + TPCircularBufferConsume(TPCircularBuffer* buffer, int32_t amount) + { + buffer->tail = (buffer->tail + amount) % buffer->length; + OSAtomicAdd32Barrier(-amount, &buffer->fillCount); + assert(buffer->fillCount >= 0); + } + + /*! + * Version of TPCircularBufferConsume without the memory barrier, for more optimal use in + * single-threaded contexts + */ + static __inline__ __attribute__((always_inline)) void + TPCircularBufferConsumeNoBarrier(TPCircularBuffer* buffer, int32_t amount) + { + buffer->tail = (buffer->tail + amount) % buffer->length; + buffer->fillCount -= amount; + assert(buffer->fillCount >= 0); + } + + /*! + * Access front of buffer + * + * This gives you a pointer to the front of the buffer, ready + * for writing, and the number of available bytes to write. + * + * @param buffer Circular buffer + * @param availableBytes On output, the number of bytes ready for writing + * @return Pointer to the first bytes ready for writing, or NULL if buffer is full + */ + static __inline__ __attribute__((always_inline)) void* + TPCircularBufferHead(TPCircularBuffer* buffer, int32_t* availableBytes) + { + *availableBytes = (buffer->length - buffer->fillCount); + if (*availableBytes == 0) + return NULL; + return (void*)((char*)buffer->buffer + buffer->head); + } + + // Writing (producing) + + /*! + * Produce bytes in buffer + * + * This marks the given section of the buffer ready for reading. + * + * @param buffer Circular buffer + * @param amount Number of bytes to produce + */ + static __inline__ __attribute__((always_inline)) void + TPCircularBufferProduce(TPCircularBuffer* buffer, int amount) + { + buffer->head = (buffer->head + amount) % buffer->length; + OSAtomicAdd32Barrier(amount, &buffer->fillCount); + assert(buffer->fillCount <= buffer->length); + } + + /*! + * Version of TPCircularBufferProduce without the memory barrier, for more optimal use in + * single-threaded contexts + */ + static __inline__ __attribute__((always_inline)) void + TPCircularBufferProduceNoBarrier(TPCircularBuffer* buffer, int amount) + { + buffer->head = (buffer->head + amount) % buffer->length; + buffer->fillCount += amount; + assert(buffer->fillCount <= buffer->length); + } + + /*! + * Helper routine to copy bytes to buffer + * + * This copies the given bytes to the buffer, and marks them ready for writing. + * + * @param buffer Circular buffer + * @param src Source buffer + * @param len Number of bytes in source buffer + * @return true if bytes copied, false if there was insufficient space + */ + static __inline__ __attribute__((always_inline)) bool + TPCircularBufferProduceBytes(TPCircularBuffer* buffer, const void* src, int32_t len) + { + int32_t space; + void* ptr = TPCircularBufferHead(buffer, &space); + if (space < len) + return false; + memcpy(ptr, src, len); + TPCircularBufferProduce(buffer, len); + return true; + } + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/channels/rdpsnd/client/ios/rdpsnd_ios.c b/channels/rdpsnd/client/ios/rdpsnd_ios.c new file mode 100644 index 0000000..29feb97 --- /dev/null +++ b/channels/rdpsnd/client/ios/rdpsnd_ios.c @@ -0,0 +1,296 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Audio Output Virtual Channel + * + * Copyright 2013 Dell Software + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include + +#include +#include + +#import + +#include "rdpsnd_main.h" +#include "TPCircularBuffer.h" + +#define INPUT_BUFFER_SIZE 32768 +#define CIRCULAR_BUFFER_SIZE (INPUT_BUFFER_SIZE * 4) + +typedef struct rdpsnd_ios_plugin +{ + rdpsndDevicePlugin device; + AudioComponentInstance audio_unit; + TPCircularBuffer buffer; + BOOL is_opened; + BOOL is_playing; +} rdpsndIOSPlugin; + +#define THIS(__ptr) ((rdpsndIOSPlugin*)__ptr) + +static OSStatus rdpsnd_ios_render_cb(void* inRefCon, + AudioUnitRenderActionFlags __unused* ioActionFlags, + const AudioTimeStamp __unused* inTimeStamp, UInt32 inBusNumber, + UInt32 __unused inNumberFrames, AudioBufferList* ioData) +{ + unsigned int i; + + if (inBusNumber != 0) + { + return noErr; + } + + rdpsndIOSPlugin* p = THIS(inRefCon); + + for (i = 0; i < ioData->mNumberBuffers; i++) + { + AudioBuffer* target_buffer = &ioData->mBuffers[i]; + int32_t available_bytes = 0; + const void* buffer = TPCircularBufferTail(&p->buffer, &available_bytes); + + if (buffer != NULL && available_bytes > 0) + { + const int bytes_to_copy = MIN((int32_t)target_buffer->mDataByteSize, available_bytes); + memcpy(target_buffer->mData, buffer, bytes_to_copy); + target_buffer->mDataByteSize = bytes_to_copy; + TPCircularBufferConsume(&p->buffer, bytes_to_copy); + } + else + { + target_buffer->mDataByteSize = 0; + AudioOutputUnitStop(p->audio_unit); + p->is_playing = 0; + } + } + + return noErr; +} + +static BOOL rdpsnd_ios_format_supported(rdpsndDevicePlugin* __unused device, AUDIO_FORMAT* format) +{ + if (format->wFormatTag == WAVE_FORMAT_PCM) + { + return 1; + } + + return 0; +} + +static BOOL rdpsnd_ios_set_format(rdpsndDevicePlugin* __unused device, + AUDIO_FORMAT* __unused format, int __unused latency) +{ + return TRUE; +} + +static BOOL rdpsnd_ios_set_volume(rdpsndDevicePlugin* __unused device, UINT32 __unused value) +{ + return TRUE; +} + +static void rdpsnd_ios_start(rdpsndDevicePlugin* device) +{ + rdpsndIOSPlugin* p = THIS(device); + + /* If this device is not playing... */ + if (!p->is_playing) + { + /* Start the device. */ + int32_t available_bytes = 0; + TPCircularBufferTail(&p->buffer, &available_bytes); + + if (available_bytes > 0) + { + p->is_playing = 1; + AudioOutputUnitStart(p->audio_unit); + } + } +} + +static void rdpsnd_ios_stop(rdpsndDevicePlugin* __unused device) +{ + rdpsndIOSPlugin* p = THIS(device); + + /* If the device is playing... */ + if (p->is_playing) + { + /* Stop the device. */ + AudioOutputUnitStop(p->audio_unit); + p->is_playing = 0; + /* Free all buffers. */ + TPCircularBufferClear(&p->buffer); + } +} + +static UINT rdpsnd_ios_play(rdpsndDevicePlugin* device, BYTE* data, int size) +{ + rdpsndIOSPlugin* p = THIS(device); + const BOOL ok = TPCircularBufferProduceBytes(&p->buffer, data, size); + + if (!ok) + return 0; + + rdpsnd_ios_start(device); + return 10; /* TODO: Get real latencry in [ms] */ +} + +static BOOL rdpsnd_ios_open(rdpsndDevicePlugin* device, AUDIO_FORMAT* format, int __unused latency) +{ + rdpsndIOSPlugin* p = THIS(device); + + if (p->is_opened) + return TRUE; + + /* Find the output audio unit. */ + AudioComponentDescription desc; + desc.componentManufacturer = kAudioUnitManufacturer_Apple; + desc.componentType = kAudioUnitType_Output; + desc.componentSubType = kAudioUnitSubType_RemoteIO; + desc.componentFlags = 0; + desc.componentFlagsMask = 0; + AudioComponent audioComponent = AudioComponentFindNext(NULL, &desc); + + if (audioComponent == NULL) + return FALSE; + + /* Open the audio unit. */ + OSStatus status = AudioComponentInstanceNew(audioComponent, &p->audio_unit); + + if (status != 0) + return FALSE; + + /* Set the format for the AudioUnit. */ + AudioStreamBasicDescription audioFormat = { 0 }; + audioFormat.mSampleRate = format->nSamplesPerSec; + audioFormat.mFormatID = kAudioFormatLinearPCM; + audioFormat.mFormatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked; + audioFormat.mFramesPerPacket = 1; /* imminent property of the Linear PCM */ + audioFormat.mChannelsPerFrame = format->nChannels; + audioFormat.mBitsPerChannel = format->wBitsPerSample; + audioFormat.mBytesPerFrame = (format->wBitsPerSample * format->nChannels) / 8; + audioFormat.mBytesPerPacket = audioFormat.mBytesPerFrame * audioFormat.mFramesPerPacket; + status = AudioUnitSetProperty(p->audio_unit, kAudioUnitProperty_StreamFormat, + kAudioUnitScope_Input, 0, &audioFormat, sizeof(audioFormat)); + + if (status != 0) + { + AudioComponentInstanceDispose(p->audio_unit); + p->audio_unit = NULL; + return FALSE; + } + + /* Set up the AudioUnit callback. */ + AURenderCallbackStruct callbackStruct = { 0 }; + callbackStruct.inputProc = rdpsnd_ios_render_cb; + callbackStruct.inputProcRefCon = p; + status = + AudioUnitSetProperty(p->audio_unit, kAudioUnitProperty_SetRenderCallback, + kAudioUnitScope_Input, 0, &callbackStruct, sizeof(callbackStruct)); + + if (status != 0) + { + AudioComponentInstanceDispose(p->audio_unit); + p->audio_unit = NULL; + return FALSE; + } + + /* Initialize the AudioUnit. */ + status = AudioUnitInitialize(p->audio_unit); + + if (status != 0) + { + AudioComponentInstanceDispose(p->audio_unit); + p->audio_unit = NULL; + return FALSE; + } + + /* Allocate the circular buffer. */ + const BOOL ok = TPCircularBufferInit(&p->buffer, CIRCULAR_BUFFER_SIZE); + + if (!ok) + { + AudioUnitUninitialize(p->audio_unit); + AudioComponentInstanceDispose(p->audio_unit); + p->audio_unit = NULL; + return FALSE; + } + + p->is_opened = 1; + return TRUE; +} + +static void rdpsnd_ios_close(rdpsndDevicePlugin* device) +{ + rdpsndIOSPlugin* p = THIS(device); + /* Make sure the device is stopped. */ + rdpsnd_ios_stop(device); + + /* If the device is open... */ + if (p->is_opened) + { + /* Close the device. */ + AudioUnitUninitialize(p->audio_unit); + AudioComponentInstanceDispose(p->audio_unit); + p->audio_unit = NULL; + p->is_opened = 0; + /* Destroy the circular buffer. */ + TPCircularBufferCleanup(&p->buffer); + } +} + +static void rdpsnd_ios_free(rdpsndDevicePlugin* device) +{ + rdpsndIOSPlugin* p = THIS(device); + /* Ensure the device is closed. */ + rdpsnd_ios_close(device); + /* Free memory associated with the device. */ + free(p); +} + +#ifdef BUILTIN_CHANNELS +#define freerdp_rdpsnd_client_subsystem_entry ios_freerdp_rdpsnd_client_subsystem_entry +#else +#define freerdp_rdpsnd_client_subsystem_entry FREERDP_API freerdp_rdpsnd_client_subsystem_entry +#endif + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT freerdp_rdpsnd_client_subsystem_entry(PFREERDP_RDPSND_DEVICE_ENTRY_POINTS pEntryPoints) +{ + rdpsndIOSPlugin* p = (rdpsndIOSPlugin*)calloc(1, sizeof(rdpsndIOSPlugin)); + + if (!p) + return CHANNEL_RC_NO_MEMORY; + + p->device.Open = rdpsnd_ios_open; + p->device.FormatSupported = rdpsnd_ios_format_supported; + p->device.SetFormat = rdpsnd_ios_set_format; + p->device.SetVolume = rdpsnd_ios_set_volume; + p->device.Play = rdpsnd_ios_play; + p->device.Start = rdpsnd_ios_start; + p->device.Close = rdpsnd_ios_close; + p->device.Free = rdpsnd_ios_free; + pEntryPoints->pRegisterRdpsndDevice(pEntryPoints->rdpsnd, (rdpsndDevicePlugin*)p); + return CHANNEL_RC_OK; +} diff --git a/channels/rdpsnd/client/mac/CMakeLists.txt b/channels/rdpsnd/client/mac/CMakeLists.txt new file mode 100644 index 0000000..8b84656 --- /dev/null +++ b/channels/rdpsnd/client/mac/CMakeLists.txt @@ -0,0 +1,49 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2012 Laxmikant Rashinkar +# Copyright 2012 Marc-Andre Moreau +# Copyright 2013 Corey Clayton +# +# Licensed 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. + +define_channel_client_subsystem("rdpsnd" "mac" "") + +find_library(COCOA_LIBRARY Cocoa REQUIRED) +FIND_LIBRARY(CORE_FOUNDATION CoreFoundation) +FIND_LIBRARY(CORE_AUDIO CoreAudio REQUIRED) +FIND_LIBRARY(AUDIO_TOOL AudioToolbox REQUIRED) +FIND_LIBRARY(AV_FOUNDATION AVFoundation REQUIRED) + +set(${MODULE_PREFIX}_SRCS + rdpsnd_mac.m) + +include_directories(..) +include_directories(${MACAUDIO_INCLUDE_DIRS}) + +add_channel_client_subsystem_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} "" TRUE "") + + + +set(${MODULE_PREFIX}_LIBS ${${MODULE_PREFIX}_LIBS} + ${AUDIO_TOOL} + ${AV_FOUNDATION} + ${CORE_AUDIO} + ${COCOA_LIBRARY} + ${CORE_FOUNDATION}) + +set(${MODULE_PREFIX}_LIBS ${${MODULE_PREFIX}_LIBS} freerdp winpr) + +target_link_libraries(${MODULE_NAME} ${${MODULE_PREFIX}_LIBS}) + +set_property(TARGET ${MODULE_NAME} PROPERTY FOLDER "Channels/${CHANNEL_NAME}/Client/Mac") diff --git a/channels/rdpsnd/client/mac/rdpsnd_mac.m b/channels/rdpsnd/client/mac/rdpsnd_mac.m new file mode 100644 index 0000000..94ef150 --- /dev/null +++ b/channels/rdpsnd/client/mac/rdpsnd_mac.m @@ -0,0 +1,403 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Audio Output Virtual Channel + * + * Copyright 2012 Laxmikant Rashinkar + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger + * Copyright 2016 Inuvika Inc. + * Copyright 2016 David PHAM-VAN + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include + +#include +#include + +#include + +#include +#include + +#include "rdpsnd_main.h" + +struct rdpsnd_mac_plugin +{ + rdpsndDevicePlugin device; + + BOOL isOpen; + BOOL isPlaying; + + UINT32 latency; + AUDIO_FORMAT format; + + AVAudioEngine *engine; + AVAudioPlayerNode *player; + UINT64 diff; +}; +typedef struct rdpsnd_mac_plugin rdpsndMacPlugin; + +static BOOL rdpsnd_mac_set_format(rdpsndDevicePlugin *device, const AUDIO_FORMAT *format, + UINT32 latency) +{ + rdpsndMacPlugin *mac = (rdpsndMacPlugin *)device; + if (!mac || !format) + return FALSE; + + mac->latency = latency; + mac->format = *format; + + audio_format_print(WLog_Get(TAG), WLOG_DEBUG, format); + return TRUE; +} + +static char *FormatError(OSStatus st) +{ + switch (st) + { + case kAudioFileUnspecifiedError: + return "kAudioFileUnspecifiedError"; + + case kAudioFileUnsupportedFileTypeError: + return "kAudioFileUnsupportedFileTypeError"; + + case kAudioFileUnsupportedDataFormatError: + return "kAudioFileUnsupportedDataFormatError"; + + case kAudioFileUnsupportedPropertyError: + return "kAudioFileUnsupportedPropertyError"; + + case kAudioFileBadPropertySizeError: + return "kAudioFileBadPropertySizeError"; + + case kAudioFilePermissionsError: + return "kAudioFilePermissionsError"; + + case kAudioFileNotOptimizedError: + return "kAudioFileNotOptimizedError"; + + case kAudioFileInvalidChunkError: + return "kAudioFileInvalidChunkError"; + + case kAudioFileDoesNotAllow64BitDataSizeError: + return "kAudioFileDoesNotAllow64BitDataSizeError"; + + case kAudioFileInvalidPacketOffsetError: + return "kAudioFileInvalidPacketOffsetError"; + + case kAudioFileInvalidFileError: + return "kAudioFileInvalidFileError"; + + case kAudioFileOperationNotSupportedError: + return "kAudioFileOperationNotSupportedError"; + + case kAudioFileNotOpenError: + return "kAudioFileNotOpenError"; + + case kAudioFileEndOfFileError: + return "kAudioFileEndOfFileError"; + + case kAudioFilePositionError: + return "kAudioFilePositionError"; + + case kAudioFileFileNotFoundError: + return "kAudioFileFileNotFoundError"; + + default: + return "unknown error"; + } +} + +static void rdpsnd_mac_release(rdpsndMacPlugin *mac) +{ + if (mac->player) + [mac->player release]; + mac->player = NULL; + + if (mac->engine) + [mac->engine release]; + mac->engine = NULL; +} + +static BOOL rdpsnd_mac_open(rdpsndDevicePlugin *device, const AUDIO_FORMAT *format, UINT32 latency) +{ + @autoreleasepool + { + AudioDeviceID outputDeviceID; + UInt32 propertySize; + OSStatus err; + NSError *error; + rdpsndMacPlugin *mac = (rdpsndMacPlugin *)device; + AudioObjectPropertyAddress propertyAddress = { + kAudioHardwarePropertyDefaultOutputDevice, + kAudioObjectPropertyScopeGlobal, +#if defined(MAC_OS_VERSION_12_0) + kAudioObjectPropertyElementMain +#else + kAudioObjectPropertyElementMaster +#endif + }; + + if (mac->isOpen) + return TRUE; + + if (!rdpsnd_mac_set_format(device, format, latency)) + return FALSE; + + propertySize = sizeof(outputDeviceID); + err = AudioObjectGetPropertyData(kAudioObjectSystemObject, &propertyAddress, 0, NULL, + &propertySize, &outputDeviceID); + if (err) + { + WLog_ERR(TAG, "AudioHardwareGetProperty: %s", FormatError(err)); + return FALSE; + } + + mac->engine = [[AVAudioEngine alloc] init]; + if (!mac->engine) + return FALSE; + + err = AudioUnitSetProperty(mac->engine.outputNode.audioUnit, + kAudioOutputUnitProperty_CurrentDevice, kAudioUnitScope_Global, + 0, &outputDeviceID, sizeof(outputDeviceID)); + if (err) + { + rdpsnd_mac_release(mac); + WLog_ERR(TAG, "AudioUnitSetProperty: %s", FormatError(err)); + return FALSE; + } + + mac->player = [[AVAudioPlayerNode alloc] init]; + if (!mac->player) + { + rdpsnd_mac_release(mac); + WLog_ERR(TAG, "AVAudioPlayerNode::init() failed"); + return FALSE; + } + + [mac->engine attachNode:mac->player]; + + [mac->engine connect:mac->player to:mac->engine.mainMixerNode format:nil]; + + [mac->engine prepare]; + + if (![mac->engine startAndReturnError:&error]) + { + device->Close(device); + WLog_ERR(TAG, "Failed to start audio player %s", + [error.localizedDescription UTF8String]); + return FALSE; + } + + mac->isOpen = TRUE; + return TRUE; + } +} + +static void rdpsnd_mac_close(rdpsndDevicePlugin *device) +{ + @autoreleasepool + { + rdpsndMacPlugin *mac = (rdpsndMacPlugin *)device; + + if (mac->isPlaying) + { + [mac->player stop]; + mac->isPlaying = FALSE; + } + + if (mac->isOpen) + { + [mac->engine stop]; + mac->isOpen = FALSE; + } + + rdpsnd_mac_release(mac); + } +} + +static void rdpsnd_mac_free(rdpsndDevicePlugin *device) +{ + rdpsndMacPlugin *mac = (rdpsndMacPlugin *)device; + device->Close(device); + free(mac); +} + +static BOOL rdpsnd_mac_format_supported(rdpsndDevicePlugin *device, const AUDIO_FORMAT *format) +{ + WINPR_UNUSED(device); + + switch (format->wFormatTag) + { + case WAVE_FORMAT_PCM: + if (format->wBitsPerSample != 16) + return FALSE; + return TRUE; + + default: + return FALSE; + } +} + +static BOOL rdpsnd_mac_set_volume(rdpsndDevicePlugin *device, UINT32 value) +{ + @autoreleasepool + { + Float32 fVolume; + UINT16 volumeLeft; + UINT16 volumeRight; + rdpsndMacPlugin *mac = (rdpsndMacPlugin *)device; + + if (!mac->player) + return FALSE; + + volumeLeft = (value & 0xFFFF); + volumeRight = ((value >> 16) & 0xFFFF); + fVolume = ((float)volumeLeft) / 65535.0f; + + mac->player.volume = fVolume; + + return TRUE; + } +} + +static void rdpsnd_mac_start(rdpsndDevicePlugin *device) +{ + @autoreleasepool + { + rdpsndMacPlugin *mac = (rdpsndMacPlugin *)device; + + if (!mac->isPlaying) + { + if (!mac->engine.isRunning) + { + NSError *error; + + if (![mac->engine startAndReturnError:&error]) + { + device->Close(device); + WLog_ERR(TAG, "Failed to start audio player %s", + [error.localizedDescription UTF8String]); + return; + } + } + + [mac->player play]; + + mac->isPlaying = TRUE; + mac->diff = 100; /* Initial latency, corrected after first sample is played. */ + } + } +} + +static UINT rdpsnd_mac_play(rdpsndDevicePlugin *device, const BYTE *data, size_t size) +{ + @autoreleasepool + { + rdpsndMacPlugin *mac = (rdpsndMacPlugin *)device; + AVAudioPCMBuffer *buffer; + AVAudioFormat *format; + float *const *db; + size_t pos, step, x; + AVAudioFrameCount count; + UINT64 start = GetTickCount64(); + + if (!mac->isOpen) + return 0; + + step = 2 * mac->format.nChannels; + + count = size / step; + format = [[AVAudioFormat alloc] initWithCommonFormat:AVAudioPCMFormatFloat32 + sampleRate:mac->format.nSamplesPerSec + channels:mac->format.nChannels + interleaved:NO]; + if (!format) + { + WLog_WARN(TAG, "AVAudioFormat::init() failed"); + return 0; + } + buffer = [[AVAudioPCMBuffer alloc] initWithPCMFormat:format frameCapacity:count]; + [format release]; + if (!buffer) + { + WLog_WARN(TAG, "AVAudioPCMBuffer::init() failed"); + return 0; + } + + buffer.frameLength = buffer.frameCapacity; + db = buffer.floatChannelData; + + for (pos = 0; pos < count; pos++) + { + const BYTE *d = &data[pos * step]; + for (x = 0; x < mac->format.nChannels; x++) + { + const float val = (int16_t)((uint16_t)d[0] | ((uint16_t)d[1] << 8)) / 32768.0f; + db[x][pos] = val; + d += sizeof(int16_t); + } + } + + rdpsnd_mac_start(device); + + [mac->player scheduleBuffer:buffer + completionHandler:^{ + UINT64 stop = GetTickCount64(); + if (start > stop) + mac->diff = 0; + else + mac->diff = stop - start; + }]; + [buffer release]; + + return mac->diff > UINT_MAX ? UINT_MAX : mac->diff; + } +} + +#ifdef BUILTIN_CHANNELS +#define freerdp_rdpsnd_client_subsystem_entry mac_freerdp_rdpsnd_client_subsystem_entry +#else +#define freerdp_rdpsnd_client_subsystem_entry FREERDP_API freerdp_rdpsnd_client_subsystem_entry +#endif + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT freerdp_rdpsnd_client_subsystem_entry(PFREERDP_RDPSND_DEVICE_ENTRY_POINTS pEntryPoints) +{ + rdpsndMacPlugin *mac; + mac = (rdpsndMacPlugin *)calloc(1, sizeof(rdpsndMacPlugin)); + + if (!mac) + return CHANNEL_RC_NO_MEMORY; + + mac->device.Open = rdpsnd_mac_open; + mac->device.FormatSupported = rdpsnd_mac_format_supported; + mac->device.SetVolume = rdpsnd_mac_set_volume; + mac->device.Play = rdpsnd_mac_play; + mac->device.Close = rdpsnd_mac_close; + mac->device.Free = rdpsnd_mac_free; + pEntryPoints->pRegisterRdpsndDevice(pEntryPoints->rdpsnd, (rdpsndDevicePlugin *)mac); + return CHANNEL_RC_OK; +} diff --git a/channels/rdpsnd/client/opensles/CMakeLists.txt b/channels/rdpsnd/client/opensles/CMakeLists.txt new file mode 100644 index 0000000..410a4b4 --- /dev/null +++ b/channels/rdpsnd/client/opensles/CMakeLists.txt @@ -0,0 +1,33 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2013 Armin Novak +# +# Licensed 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. + +define_channel_client_subsystem("rdpsnd" "opensles" "") + +set(${MODULE_PREFIX}_SRCS + opensl_io.c + rdpsnd_opensles.c) + +include_directories(..) +include_directories(${OPENSLES_INCLUDE_DIRS}) + +add_channel_client_subsystem_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} "" TRUE "") + + + +set(${MODULE_PREFIX}_LIBS freerdp ${OPENSLES_LIBRARIES}) + +target_link_libraries(${MODULE_NAME} ${${MODULE_PREFIX}_LIBS}) diff --git a/channels/rdpsnd/client/opensles/opensl_io.c b/channels/rdpsnd/client/opensles/opensl_io.c new file mode 100644 index 0000000..61afc10 --- /dev/null +++ b/channels/rdpsnd/client/opensles/opensl_io.c @@ -0,0 +1,421 @@ +/* +opensl_io.c: +Android OpenSL input/output module +Copyright (c) 2012, Victor Lazzarini +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include + +#include "rdpsnd_main.h" +#include "opensl_io.h" +#define CONV16BIT 32768 +#define CONVMYFLT (1. / 32768.) + +static void bqPlayerCallback(SLAndroidSimpleBufferQueueItf bq, void* context); + +// creates the OpenSL ES audio engine +static SLresult openSLCreateEngine(OPENSL_STREAM* p) +{ + SLresult result; + // create engine + result = slCreateEngine(&(p->engineObject), 0, NULL, 0, NULL, NULL); + DEBUG_SND("engineObject=%p", (void*)p->engineObject); + + if (result != SL_RESULT_SUCCESS) + goto engine_end; + + // realize the engine + result = (*p->engineObject)->Realize(p->engineObject, SL_BOOLEAN_FALSE); + DEBUG_SND("Realize=%" PRIu32 "", result); + + if (result != SL_RESULT_SUCCESS) + goto engine_end; + + // get the engine interface, which is needed in order to create other objects + result = (*p->engineObject)->GetInterface(p->engineObject, SL_IID_ENGINE, &(p->engineEngine)); + DEBUG_SND("engineEngine=%p", (void*)p->engineEngine); + + if (result != SL_RESULT_SUCCESS) + goto engine_end; + +engine_end: + return result; +} + +// opens the OpenSL ES device for output +static SLresult openSLPlayOpen(OPENSL_STREAM* p) +{ + SLresult result; + SLuint32 sr = p->sr; + SLuint32 channels = p->outchannels; + assert(p->engineObject); + assert(p->engineEngine); + + if (channels) + { + // configure audio source + SLDataLocator_AndroidSimpleBufferQueue loc_bufq = { SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, + p->queuesize }; + + switch (sr) + { + case 8000: + sr = SL_SAMPLINGRATE_8; + break; + + case 11025: + sr = SL_SAMPLINGRATE_11_025; + break; + + case 16000: + sr = SL_SAMPLINGRATE_16; + break; + + case 22050: + sr = SL_SAMPLINGRATE_22_05; + break; + + case 24000: + sr = SL_SAMPLINGRATE_24; + break; + + case 32000: + sr = SL_SAMPLINGRATE_32; + break; + + case 44100: + sr = SL_SAMPLINGRATE_44_1; + break; + + case 48000: + sr = SL_SAMPLINGRATE_48; + break; + + case 64000: + sr = SL_SAMPLINGRATE_64; + break; + + case 88200: + sr = SL_SAMPLINGRATE_88_2; + break; + + case 96000: + sr = SL_SAMPLINGRATE_96; + break; + + case 192000: + sr = SL_SAMPLINGRATE_192; + break; + + default: + return -1; + } + + const SLInterfaceID ids[] = { SL_IID_VOLUME }; + const SLboolean req[] = { SL_BOOLEAN_FALSE }; + result = (*p->engineEngine) + ->CreateOutputMix(p->engineEngine, &(p->outputMixObject), 1, ids, req); + DEBUG_SND("engineEngine=%p", (void*)p->engineEngine); + assert(!result); + + if (result != SL_RESULT_SUCCESS) + goto end_openaudio; + + // realize the output mix + result = (*p->outputMixObject)->Realize(p->outputMixObject, SL_BOOLEAN_FALSE); + DEBUG_SND("Realize=%" PRIu32 "", result); + assert(!result); + + if (result != SL_RESULT_SUCCESS) + goto end_openaudio; + + int speakers; + + if (channels > 1) + speakers = SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT; + else + speakers = SL_SPEAKER_FRONT_CENTER; + + SLDataFormat_PCM format_pcm = { SL_DATAFORMAT_PCM, + channels, + sr, + SL_PCMSAMPLEFORMAT_FIXED_16, + SL_PCMSAMPLEFORMAT_FIXED_16, + speakers, + SL_BYTEORDER_LITTLEENDIAN }; + SLDataSource audioSrc = { &loc_bufq, &format_pcm }; + // configure audio sink + SLDataLocator_OutputMix loc_outmix = { SL_DATALOCATOR_OUTPUTMIX, p->outputMixObject }; + SLDataSink audioSnk = { &loc_outmix, NULL }; + // create audio player + const SLInterfaceID ids1[] = { SL_IID_ANDROIDSIMPLEBUFFERQUEUE, SL_IID_VOLUME }; + const SLboolean req1[] = { SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE }; + result = (*p->engineEngine) + ->CreateAudioPlayer(p->engineEngine, &(p->bqPlayerObject), &audioSrc, + &audioSnk, 2, ids1, req1); + DEBUG_SND("bqPlayerObject=%p", (void*)p->bqPlayerObject); + assert(!result); + + if (result != SL_RESULT_SUCCESS) + goto end_openaudio; + + // realize the player + result = (*p->bqPlayerObject)->Realize(p->bqPlayerObject, SL_BOOLEAN_FALSE); + DEBUG_SND("Realize=%" PRIu32 "", result); + assert(!result); + + if (result != SL_RESULT_SUCCESS) + goto end_openaudio; + + // get the play interface + result = + (*p->bqPlayerObject)->GetInterface(p->bqPlayerObject, SL_IID_PLAY, &(p->bqPlayerPlay)); + DEBUG_SND("bqPlayerPlay=%p", (void*)p->bqPlayerPlay); + assert(!result); + + if (result != SL_RESULT_SUCCESS) + goto end_openaudio; + + // get the volume interface + result = (*p->bqPlayerObject) + ->GetInterface(p->bqPlayerObject, SL_IID_VOLUME, &(p->bqPlayerVolume)); + DEBUG_SND("bqPlayerVolume=%p", (void*)p->bqPlayerVolume); + assert(!result); + + if (result != SL_RESULT_SUCCESS) + goto end_openaudio; + + // get the buffer queue interface + result = (*p->bqPlayerObject) + ->GetInterface(p->bqPlayerObject, SL_IID_ANDROIDSIMPLEBUFFERQUEUE, + &(p->bqPlayerBufferQueue)); + DEBUG_SND("bqPlayerBufferQueue=%p", (void*)p->bqPlayerBufferQueue); + assert(!result); + + if (result != SL_RESULT_SUCCESS) + goto end_openaudio; + + // register callback on the buffer queue + result = (*p->bqPlayerBufferQueue) + ->RegisterCallback(p->bqPlayerBufferQueue, bqPlayerCallback, p); + DEBUG_SND("bqPlayerCallback=%p", (void*)p->bqPlayerCallback); + assert(!result); + + if (result != SL_RESULT_SUCCESS) + goto end_openaudio; + + // set the player's state to playing + result = (*p->bqPlayerPlay)->SetPlayState(p->bqPlayerPlay, SL_PLAYSTATE_PLAYING); + DEBUG_SND("SetPlayState=%" PRIu32 "", result); + assert(!result); + end_openaudio: + assert(!result); + return result; + } + + return SL_RESULT_SUCCESS; +} + +// close the OpenSL IO and destroy the audio engine +static void openSLDestroyEngine(OPENSL_STREAM* p) +{ + // destroy buffer queue audio player object, and invalidate all associated interfaces + if (p->bqPlayerObject != NULL) + { + (*p->bqPlayerObject)->Destroy(p->bqPlayerObject); + p->bqPlayerObject = NULL; + p->bqPlayerVolume = NULL; + p->bqPlayerPlay = NULL; + p->bqPlayerBufferQueue = NULL; + p->bqPlayerEffectSend = NULL; + } + + // destroy output mix object, and invalidate all associated interfaces + if (p->outputMixObject != NULL) + { + (*p->outputMixObject)->Destroy(p->outputMixObject); + p->outputMixObject = NULL; + } + + // destroy engine object, and invalidate all associated interfaces + if (p->engineObject != NULL) + { + (*p->engineObject)->Destroy(p->engineObject); + p->engineObject = NULL; + p->engineEngine = NULL; + } +} + +// open the android audio device for and/or output +OPENSL_STREAM* android_OpenAudioDevice(int sr, int outchannels, int bufferframes) +{ + OPENSL_STREAM* p; + p = (OPENSL_STREAM*)calloc(1, sizeof(OPENSL_STREAM)); + + if (!p) + return NULL; + + p->queuesize = bufferframes; + p->outchannels = outchannels; + p->sr = sr; + + if (openSLCreateEngine(p) != SL_RESULT_SUCCESS) + { + android_CloseAudioDevice(p); + return NULL; + } + + if (openSLPlayOpen(p) != SL_RESULT_SUCCESS) + { + android_CloseAudioDevice(p); + return NULL; + } + + p->queue = Queue_New(TRUE, -1, -1); + + if (!p->queue) + { + android_CloseAudioDevice(p); + return NULL; + } + + return p; +} + +// close the android audio device +void android_CloseAudioDevice(OPENSL_STREAM* p) +{ + if (p == NULL) + return; + + openSLDestroyEngine(p); + + if (p->queue) + Queue_Free(p->queue); + + free(p); +} + +// this callback handler is called every time a buffer finishes playing +static void bqPlayerCallback(SLAndroidSimpleBufferQueueItf bq, void* context) +{ + OPENSL_STREAM* p = (OPENSL_STREAM*)context; + assert(p); + assert(p->queue); + void* data = Queue_Dequeue(p->queue); + free(data); +} + +// puts a buffer of size samples to the device +int android_AudioOut(OPENSL_STREAM* p, const short* buffer, int size) +{ + assert(p); + assert(buffer); + assert(size > 0); + + /* Assure, that the queue is not full. */ + if (p->queuesize <= Queue_Count(p->queue) && + WaitForSingleObject(p->queue->event, INFINITE) == WAIT_FAILED) + { + DEBUG_SND("WaitForSingleObject failed!"); + return -1; + } + + void* data = calloc(size, sizeof(short)); + + if (!data) + { + DEBUG_SND("unable to allocate a buffer"); + return -1; + } + + memcpy(data, buffer, size * sizeof(short)); + Queue_Enqueue(p->queue, data); + (*p->bqPlayerBufferQueue)->Enqueue(p->bqPlayerBufferQueue, data, sizeof(short) * size); + return size; +} + +int android_GetOutputMute(OPENSL_STREAM* p) +{ + SLboolean mute; + assert(p); + assert(p->bqPlayerVolume); + SLresult rc = (*p->bqPlayerVolume)->GetMute(p->bqPlayerVolume, &mute); + + if (SL_RESULT_SUCCESS != rc) + return SL_BOOLEAN_FALSE; + + return mute; +} + +BOOL android_SetOutputMute(OPENSL_STREAM* p, BOOL _mute) +{ + SLboolean mute = _mute; + assert(p); + assert(p->bqPlayerVolume); + SLresult rc = (*p->bqPlayerVolume)->SetMute(p->bqPlayerVolume, mute); + + if (SL_RESULT_SUCCESS != rc) + return FALSE; + + return TRUE; +} + +int android_GetOutputVolume(OPENSL_STREAM* p) +{ + SLmillibel level; + assert(p); + assert(p->bqPlayerVolume); + SLresult rc = (*p->bqPlayerVolume)->GetVolumeLevel(p->bqPlayerVolume, &level); + + if (SL_RESULT_SUCCESS != rc) + return 0; + + return level; +} + +int android_GetOutputVolumeMax(OPENSL_STREAM* p) +{ + SLmillibel level; + assert(p); + assert(p->bqPlayerVolume); + SLresult rc = (*p->bqPlayerVolume)->GetMaxVolumeLevel(p->bqPlayerVolume, &level); + + if (SL_RESULT_SUCCESS != rc) + return 0; + + return level; +} + +BOOL android_SetOutputVolume(OPENSL_STREAM* p, int level) +{ + SLresult rc = (*p->bqPlayerVolume)->SetVolumeLevel(p->bqPlayerVolume, level); + + if (SL_RESULT_SUCCESS != rc) + return FALSE; + + return TRUE; +} diff --git a/channels/rdpsnd/client/opensles/opensl_io.h b/channels/rdpsnd/client/opensles/opensl_io.h new file mode 100644 index 0000000..57dc048 --- /dev/null +++ b/channels/rdpsnd/client/opensles/opensl_io.h @@ -0,0 +1,110 @@ +/* +opensl_io.c: +Android OpenSL input/output module header +Copyright (c) 2012, Victor Lazzarini +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#ifndef FREERDP_CHANNEL_RDPSND_CLIENT_OPENSL_IO_H +#define FREERDP_CHANNEL_RDPSND_CLIENT_OPENSL_IO_H + +#include +#include +#include +#include + +#include + +#ifdef __cplusplus +extern "C" +{ +#endif + + typedef struct opensl_stream + { + // engine interfaces + SLObjectItf engineObject; + SLEngineItf engineEngine; + + // output mix interfaces + SLObjectItf outputMixObject; + + // buffer queue player interfaces + SLObjectItf bqPlayerObject; + SLPlayItf bqPlayerPlay; + SLVolumeItf bqPlayerVolume; + SLAndroidSimpleBufferQueueItf bqPlayerBufferQueue; + SLEffectSendItf bqPlayerEffectSend; + + unsigned int outchannels; + unsigned int sr; + + unsigned int queuesize; + wQueue* queue; + } OPENSL_STREAM; + + /* + Open the audio device with a given sampling rate (sr), output channels and IO buffer size + in frames. Returns a handle to the OpenSL stream + */ + FREERDP_LOCAL OPENSL_STREAM* android_OpenAudioDevice(int sr, int outchannels, int bufferframes); + /* + Close the audio device + */ + FREERDP_LOCAL void android_CloseAudioDevice(OPENSL_STREAM* p); + /* + Write a buffer to the OpenSL stream *p, of size samples. Returns the number of samples written. + */ + FREERDP_LOCAL int android_AudioOut(OPENSL_STREAM* p, const short* buffer, int size); + /* + * Set the volume input level. + */ + FREERDP_LOCAL void android_SetInputVolume(OPENSL_STREAM* p, int level); + /* + * Get the current output mute setting. + */ + FREERDP_LOCAL int android_GetOutputMute(OPENSL_STREAM* p); + /* + * Change the current output mute setting. + */ + FREERDP_LOCAL BOOL android_SetOutputMute(OPENSL_STREAM* p, BOOL mute); + /* + * Get the current output volume level. + */ + FREERDP_LOCAL int android_GetOutputVolume(OPENSL_STREAM* p); + /* + * Get the maximum output volume level. + */ + FREERDP_LOCAL int android_GetOutputVolumeMax(OPENSL_STREAM* p); + + /* + * Set the volume output level. + */ + FREERDP_LOCAL BOOL android_SetOutputVolume(OPENSL_STREAM* p, int level); +#ifdef __cplusplus +}; +#endif + +#endif /* FREERDP_CHANNEL_RDPSND_CLIENT_OPENSL_IO_H */ diff --git a/channels/rdpsnd/client/opensles/rdpsnd_opensles.c b/channels/rdpsnd/client/opensles/rdpsnd_opensles.c new file mode 100644 index 0000000..3481d91 --- /dev/null +++ b/channels/rdpsnd/client/opensles/rdpsnd_opensles.c @@ -0,0 +1,386 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Audio Output Virtual Channel + * + * Copyright 2013 Armin Novak + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include + +#include "opensl_io.h" +#include "rdpsnd_main.h" + +typedef struct rdpsnd_opensles_plugin rdpsndopenslesPlugin; + +struct rdpsnd_opensles_plugin +{ + rdpsndDevicePlugin device; + + UINT32 latency; + int wformat; + int block_size; + char* device_name; + + OPENSL_STREAM* stream; + + UINT32 volume; + + UINT32 rate; + UINT32 channels; + int format; +}; + +static int rdpsnd_opensles_volume_to_millibel(unsigned short level, int max) +{ + const int min = SL_MILLIBEL_MIN; + const int step = max - min; + const int rc = (level * step / 0xFFFF) + min; + DEBUG_SND("level=%hu, min=%d, max=%d, step=%d, result=%d", level, min, max, step, rc); + return rc; +} + +static unsigned short rdpsnd_opensles_millibel_to_volume(int millibel, int max) +{ + const int min = SL_MILLIBEL_MIN; + const int range = max - min; + const int rc = ((millibel - min) * 0xFFFF + range / 2 + 1) / range; + DEBUG_SND("millibel=%d, min=%d, max=%d, range=%d, result=%d", millibel, min, max, range, rc); + return rc; +} + +static bool rdpsnd_opensles_check_handle(const rdpsndopenslesPlugin* hdl) +{ + bool rc = true; + + if (!hdl) + rc = false; + else + { + if (!hdl->stream) + rc = false; + } + + return rc; +} + +static BOOL rdpsnd_opensles_set_volume(rdpsndDevicePlugin* device, UINT32 volume); + +static int rdpsnd_opensles_set_params(rdpsndopenslesPlugin* opensles) +{ + DEBUG_SND("opensles=%p", (void*)opensles); + + if (!rdpsnd_opensles_check_handle(opensles)) + return 0; + + if (opensles->stream) + android_CloseAudioDevice(opensles->stream); + + opensles->stream = android_OpenAudioDevice(opensles->rate, opensles->channels, 20); + return 0; +} + +static BOOL rdpsnd_opensles_set_format(rdpsndDevicePlugin* device, const AUDIO_FORMAT* format, + UINT32 latency) +{ + rdpsndopenslesPlugin* opensles = (rdpsndopenslesPlugin*)device; + rdpsnd_opensles_check_handle(opensles); + DEBUG_SND("opensles=%p format=%p, latency=%" PRIu32, (void*)opensles, (void*)format, latency); + + if (format) + { + DEBUG_SND("format=%" PRIu16 ", cbsize=%" PRIu16 ", samples=%" PRIu32 ", bits=%" PRIu16 + ", channels=%" PRIu16 ", align=%" PRIu16 "", + format->wFormatTag, format->cbSize, format->nSamplesPerSec, + format->wBitsPerSample, format->nChannels, format->nBlockAlign); + opensles->rate = format->nSamplesPerSec; + opensles->channels = format->nChannels; + opensles->format = format->wFormatTag; + opensles->wformat = format->wFormatTag; + opensles->block_size = format->nBlockAlign; + } + + opensles->latency = latency; + return (rdpsnd_opensles_set_params(opensles) == 0); +} + +static BOOL rdpsnd_opensles_open(rdpsndDevicePlugin* device, const AUDIO_FORMAT* format, + UINT32 latency) +{ + rdpsndopenslesPlugin* opensles = (rdpsndopenslesPlugin*)device; + DEBUG_SND("opensles=%p format=%p, latency=%" PRIu32 ", rate=%" PRIu32 "", (void*)opensles, + (void*)format, latency, opensles->rate); + + if (rdpsnd_opensles_check_handle(opensles)) + return TRUE; + + opensles->stream = android_OpenAudioDevice(opensles->rate, opensles->channels, 20); + assert(opensles->stream); + + if (!opensles->stream) + WLog_ERR(TAG, "android_OpenAudioDevice failed"); + else + rdpsnd_opensles_set_volume(device, opensles->volume); + + return rdpsnd_opensles_set_format(device, format, latency); +} + +static void rdpsnd_opensles_close(rdpsndDevicePlugin* device) +{ + rdpsndopenslesPlugin* opensles = (rdpsndopenslesPlugin*)device; + DEBUG_SND("opensles=%p", (void*)opensles); + + if (!rdpsnd_opensles_check_handle(opensles)) + return; + + android_CloseAudioDevice(opensles->stream); + opensles->stream = NULL; +} + +static void rdpsnd_opensles_free(rdpsndDevicePlugin* device) +{ + rdpsndopenslesPlugin* opensles = (rdpsndopenslesPlugin*)device; + DEBUG_SND("opensles=%p", (void*)opensles); + assert(opensles); + assert(opensles->device_name); + free(opensles->device_name); + free(opensles); +} + +static BOOL rdpsnd_opensles_format_supported(rdpsndDevicePlugin* device, const AUDIO_FORMAT* format) +{ + DEBUG_SND("format=%" PRIu16 ", cbsize=%" PRIu16 ", samples=%" PRIu32 ", bits=%" PRIu16 + ", channels=%" PRIu16 ", align=%" PRIu16 "", + format->wFormatTag, format->cbSize, format->nSamplesPerSec, format->wBitsPerSample, + format->nChannels, format->nBlockAlign); + assert(device); + assert(format); + + switch (format->wFormatTag) + { + case WAVE_FORMAT_PCM: + if (format->cbSize == 0 && format->nSamplesPerSec <= 48000 && + (format->wBitsPerSample == 8 || format->wBitsPerSample == 16) && + (format->nChannels == 1 || format->nChannels == 2)) + { + return TRUE; + } + + break; + + default: + break; + } + + return FALSE; +} + +static UINT32 rdpsnd_opensles_get_volume(rdpsndDevicePlugin* device) +{ + rdpsndopenslesPlugin* opensles = (rdpsndopenslesPlugin*)device; + DEBUG_SND("opensles=%p", (void*)opensles); + assert(opensles); + + if (opensles->stream) + { + const int max = android_GetOutputVolumeMax(opensles->stream); + const int rc = android_GetOutputVolume(opensles->stream); + + if (android_GetOutputMute(opensles->stream)) + opensles->volume = 0; + else + { + const unsigned short vol = rdpsnd_opensles_millibel_to_volume(rc, max); + opensles->volume = (vol << 16) | (vol & 0xFFFF); + } + } + + return opensles->volume; +} + +static BOOL rdpsnd_opensles_set_volume(rdpsndDevicePlugin* device, UINT32 value) +{ + rdpsndopenslesPlugin* opensles = (rdpsndopenslesPlugin*)device; + DEBUG_SND("opensles=%p, value=%" PRIu32 "", (void*)opensles, value); + assert(opensles); + opensles->volume = value; + + if (opensles->stream) + { + if (0 == opensles->volume) + return android_SetOutputMute(opensles->stream, true); + else + { + const int max = android_GetOutputVolumeMax(opensles->stream); + const int vol = rdpsnd_opensles_volume_to_millibel(value & 0xFFFF, max); + + if (!android_SetOutputMute(opensles->stream, false)) + return FALSE; + + if (!android_SetOutputVolume(opensles->stream, vol)) + return FALSE; + } + } + + return TRUE; +} + +static UINT rdpsnd_opensles_play(rdpsndDevicePlugin* device, const BYTE* data, size_t size) +{ + union { + const BYTE* b; + const short* s; + } src; + int ret; + rdpsndopenslesPlugin* opensles = (rdpsndopenslesPlugin*)device; + DEBUG_SND("opensles=%p, data=%p, size=%d", (void*)opensles, (void*)data, size); + + if (!rdpsnd_opensles_check_handle(opensles)) + return 0; + + src.b = data; + DEBUG_SND("size=%d, src=%p", size, (void*)src.b); + assert(0 == size % 2); + assert(size > 0); + assert(src.b); + ret = android_AudioOut(opensles->stream, src.s, size / 2); + + if (ret < 0) + WLog_ERR(TAG, "android_AudioOut failed (%d)", ret); + + return 10; /* TODO: Get real latencry in [ms] */ +} + +static void rdpsnd_opensles_start(rdpsndDevicePlugin* device) +{ + rdpsndopenslesPlugin* opensles = (rdpsndopenslesPlugin*)device; + rdpsnd_opensles_check_handle(opensles); + DEBUG_SND("opensles=%p", (void*)opensles); +} + +static int rdpsnd_opensles_parse_addin_args(rdpsndDevicePlugin* device, ADDIN_ARGV* args) +{ + int status; + DWORD flags; + COMMAND_LINE_ARGUMENT_A* arg; + rdpsndopenslesPlugin* opensles = (rdpsndopenslesPlugin*)device; + COMMAND_LINE_ARGUMENT_A rdpsnd_opensles_args[] = { + { "dev", COMMAND_LINE_VALUE_REQUIRED, "", NULL, NULL, -1, NULL, "device" }, + { NULL, 0, NULL, NULL, NULL, -1, NULL, NULL } + }; + + assert(opensles); + assert(args); + DEBUG_SND("opensles=%p, args=%p", (void*)opensles, (void*)args); + flags = + COMMAND_LINE_SIGIL_NONE | COMMAND_LINE_SEPARATOR_COLON | COMMAND_LINE_IGN_UNKNOWN_KEYWORD; + status = CommandLineParseArgumentsA(args->argc, args->argv, rdpsnd_opensles_args, flags, + opensles, NULL, NULL); + + if (status < 0) + return status; + + arg = rdpsnd_opensles_args; + + do + { + if (!(arg->Flags & COMMAND_LINE_VALUE_PRESENT)) + continue; + + CommandLineSwitchStart(arg) CommandLineSwitchCase(arg, "dev") + { + opensles->device_name = _strdup(arg->Value); + + if (!opensles->device_name) + return ERROR_OUTOFMEMORY; + } + CommandLineSwitchEnd(arg) + } while ((arg = CommandLineFindNextArgumentA(arg)) != NULL); + + return status; +} + +#ifdef BUILTIN_CHANNELS +#define freerdp_rdpsnd_client_subsystem_entry opensles_freerdp_rdpsnd_client_subsystem_entry +#else +#define freerdp_rdpsnd_client_subsystem_entry FREERDP_API freerdp_rdpsnd_client_subsystem_entry +#endif + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT freerdp_rdpsnd_client_subsystem_entry(PFREERDP_RDPSND_DEVICE_ENTRY_POINTS pEntryPoints) +{ + ADDIN_ARGV* args; + rdpsndopenslesPlugin* opensles; + UINT error; + DEBUG_SND("pEntryPoints=%p", (void*)pEntryPoints); + opensles = (rdpsndopenslesPlugin*)calloc(1, sizeof(rdpsndopenslesPlugin)); + + if (!opensles) + return CHANNEL_RC_NO_MEMORY; + + opensles->device.Open = rdpsnd_opensles_open; + opensles->device.FormatSupported = rdpsnd_opensles_format_supported; + opensles->device.GetVolume = rdpsnd_opensles_get_volume; + opensles->device.SetVolume = rdpsnd_opensles_set_volume; + opensles->device.Start = rdpsnd_opensles_start; + opensles->device.Play = rdpsnd_opensles_play; + opensles->device.Close = rdpsnd_opensles_close; + opensles->device.Free = rdpsnd_opensles_free; + args = pEntryPoints->args; + rdpsnd_opensles_parse_addin_args((rdpsndDevicePlugin*)opensles, args); + + if (!opensles->device_name) + { + opensles->device_name = _strdup("default"); + + if (!opensles->device_name) + { + error = CHANNEL_RC_NO_MEMORY; + goto outstrdup; + } + } + + opensles->rate = 44100; + opensles->channels = 2; + opensles->format = WAVE_FORMAT_ADPCM; + pEntryPoints->pRegisterRdpsndDevice(pEntryPoints->rdpsnd, (rdpsndDevicePlugin*)opensles); + DEBUG_SND("success"); + return CHANNEL_RC_OK; +outstrdup: + free(opensles); + return error; +} diff --git a/channels/rdpsnd/client/oss/CMakeLists.txt b/channels/rdpsnd/client/oss/CMakeLists.txt new file mode 100644 index 0000000..53ae5fa --- /dev/null +++ b/channels/rdpsnd/client/oss/CMakeLists.txt @@ -0,0 +1,36 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright (c) 2015 Rozhuk Ivan +# +# Licensed 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. + +define_channel_client_subsystem("rdpsnd" "oss" "") + +set(${MODULE_PREFIX}_SRCS + rdpsnd_oss.c) + +include_directories(..) +include_directories(${OSS_INCLUDE_DIRS}) + +add_channel_client_subsystem_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} "" TRUE "") + + + +set(${MODULE_PREFIX}_LIBS ${${MODULE_PREFIX}_LIBS} winpr freerdp) + +set(${MODULE_PREFIX}_LIBS ${${MODULE_PREFIX}_LIBS} ${OSS_LIBRARIES}) + +target_link_libraries(${MODULE_NAME} ${${MODULE_PREFIX}_LIBS}) + +set_property(TARGET ${MODULE_NAME} PROPERTY FOLDER "Channels/${CHANNEL_NAME}/Client/OSS") diff --git a/channels/rdpsnd/client/oss/rdpsnd_oss.c b/channels/rdpsnd/client/oss/rdpsnd_oss.c new file mode 100644 index 0000000..f8e15d2 --- /dev/null +++ b/channels/rdpsnd/client/oss/rdpsnd_oss.c @@ -0,0 +1,475 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Audio Output Virtual Channel + * + * Copyright (c) 2015 Rozhuk Ivan + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#if defined(__OpenBSD__) +#include +#else +#include +#endif +#include + +#include +#include + +#include "rdpsnd_main.h" + +typedef struct rdpsnd_oss_plugin rdpsndOssPlugin; + +struct rdpsnd_oss_plugin +{ + rdpsndDevicePlugin device; + + int pcm_handle; + int mixer_handle; + int dev_unit; + + int supported_formats; + + UINT32 latency; + AUDIO_FORMAT format; +}; + +#define OSS_LOG_ERR(_text, _error) \ + { \ + if (_error != 0) \ + WLog_ERR(TAG, "%s: %i - %s", _text, _error, strerror(_error)); \ + } + +static int rdpsnd_oss_get_format(const AUDIO_FORMAT* format) +{ + switch (format->wFormatTag) + { + case WAVE_FORMAT_PCM: + switch (format->wBitsPerSample) + { + case 8: + return AFMT_S8; + + case 16: + return AFMT_S16_LE; + } + + break; + + case WAVE_FORMAT_ALAW: + return AFMT_A_LAW; + + case WAVE_FORMAT_MULAW: + return AFMT_MU_LAW; + } + + return 0; +} + +static BOOL rdpsnd_oss_format_supported(rdpsndDevicePlugin* device, const AUDIO_FORMAT* format) +{ + int req_fmt = 0; + rdpsndOssPlugin* oss = (rdpsndOssPlugin*)device; + + if (device == NULL || format == NULL) + return FALSE; + + switch (format->wFormatTag) + { + case WAVE_FORMAT_PCM: + if (format->cbSize != 0 || format->nSamplesPerSec > 48000 || + (format->wBitsPerSample != 8 && format->wBitsPerSample != 16) || + (format->nChannels != 1 && format->nChannels != 2)) + return FALSE; + + break; + + default: + return FALSE; + } + + req_fmt = rdpsnd_oss_get_format(format); + + /* Check really supported formats by dev. */ + if (oss->pcm_handle != -1) + { + if ((req_fmt & oss->supported_formats) == 0) + return FALSE; + } + else + { + if (req_fmt == 0) + return FALSE; + } + + return TRUE; +} + +static BOOL rdpsnd_oss_set_format(rdpsndDevicePlugin* device, const AUDIO_FORMAT* format, + UINT32 latency) +{ + int tmp; + rdpsndOssPlugin* oss = (rdpsndOssPlugin*)device; + + if (device == NULL || oss->pcm_handle == -1 || format == NULL) + return FALSE; + + oss->latency = latency; + CopyMemory(&(oss->format), format, sizeof(AUDIO_FORMAT)); + tmp = rdpsnd_oss_get_format(format); + + if (ioctl(oss->pcm_handle, SNDCTL_DSP_SETFMT, &tmp) == -1) + { + OSS_LOG_ERR("SNDCTL_DSP_SETFMT failed", errno); + return FALSE; + } + + tmp = format->nChannels; + + if (ioctl(oss->pcm_handle, SNDCTL_DSP_CHANNELS, &tmp) == -1) + { + OSS_LOG_ERR("SNDCTL_DSP_CHANNELS failed", errno); + return FALSE; + } + + tmp = format->nSamplesPerSec; + + if (ioctl(oss->pcm_handle, SNDCTL_DSP_SPEED, &tmp) == -1) + { + OSS_LOG_ERR("SNDCTL_DSP_SPEED failed", errno); + return FALSE; + } + + tmp = format->nBlockAlign; + + if (ioctl(oss->pcm_handle, SNDCTL_DSP_SETFRAGMENT, &tmp) == -1) + { + OSS_LOG_ERR("SNDCTL_DSP_SETFRAGMENT failed", errno); + return FALSE; + } + + return TRUE; +} + +static void rdpsnd_oss_open_mixer(rdpsndOssPlugin* oss) +{ + int devmask = 0; + char mixer_name[PATH_MAX] = "/dev/mixer"; + + if (oss->mixer_handle != -1) + return; + + if (oss->dev_unit != -1) + sprintf_s(mixer_name, PATH_MAX - 1, "/dev/mixer%i", oss->dev_unit); + + if ((oss->mixer_handle = open(mixer_name, O_RDWR)) < 0) + { + OSS_LOG_ERR("mixer open failed", errno); + oss->mixer_handle = -1; + return; + } + + if (ioctl(oss->mixer_handle, SOUND_MIXER_READ_DEVMASK, &devmask) == -1) + { + OSS_LOG_ERR("SOUND_MIXER_READ_DEVMASK failed", errno); + close(oss->mixer_handle); + oss->mixer_handle = -1; + return; + } +} + +static BOOL rdpsnd_oss_open(rdpsndDevicePlugin* device, const AUDIO_FORMAT* format, UINT32 latency) +{ + char dev_name[PATH_MAX] = "/dev/dsp"; + rdpsndOssPlugin* oss = (rdpsndOssPlugin*)device; + + if (device == NULL || oss->pcm_handle != -1) + return TRUE; + + if (oss->dev_unit != -1) + sprintf_s(dev_name, PATH_MAX - 1, "/dev/dsp%i", oss->dev_unit); + + WLog_INFO(TAG, "open: %s", dev_name); + + if ((oss->pcm_handle = open(dev_name, O_WRONLY)) < 0) + { + OSS_LOG_ERR("sound dev open failed", errno); + oss->pcm_handle = -1; + return FALSE; + } + +#if 0 /* FreeBSD OSS implementation at this moment (2015.03) does not set PCM_CAP_OUTPUT flag. */ + int mask = 0; + + if (ioctl(oss->pcm_handle, SNDCTL_DSP_GETCAPS, &mask) == -1) + { + OSS_LOG_ERR("SNDCTL_DSP_GETCAPS failed, try ignory", errno); + } + else if ((mask & PCM_CAP_OUTPUT) == 0) + { + OSS_LOG_ERR("Device does not supports playback", EOPNOTSUPP); + close(oss->pcm_handle); + oss->pcm_handle = -1; + return; + } + +#endif + + if (ioctl(oss->pcm_handle, SNDCTL_DSP_GETFMTS, &oss->supported_formats) == -1) + { + OSS_LOG_ERR("SNDCTL_DSP_GETFMTS failed", errno); + close(oss->pcm_handle); + oss->pcm_handle = -1; + return FALSE; + } + + rdpsnd_oss_set_format(device, format, latency); + rdpsnd_oss_open_mixer(oss); + return TRUE; +} + +static void rdpsnd_oss_close(rdpsndDevicePlugin* device) +{ + rdpsndOssPlugin* oss = (rdpsndOssPlugin*)device; + + if (device == NULL) + return; + + if (oss->pcm_handle != -1) + { + WLog_INFO(TAG, "close: dsp"); + close(oss->pcm_handle); + oss->pcm_handle = -1; + } + + if (oss->mixer_handle != -1) + { + WLog_INFO(TAG, "close: mixer"); + close(oss->mixer_handle); + oss->mixer_handle = -1; + } +} + +static void rdpsnd_oss_free(rdpsndDevicePlugin* device) +{ + rdpsndOssPlugin* oss = (rdpsndOssPlugin*)device; + + if (device == NULL) + return; + + rdpsnd_oss_close(device); + free(oss); +} + +static UINT32 rdpsnd_oss_get_volume(rdpsndDevicePlugin* device) +{ + int vol; + UINT32 dwVolume; + UINT16 dwVolumeLeft, dwVolumeRight; + rdpsndOssPlugin* oss = (rdpsndOssPlugin*)device; + /* On error return 50% volume. */ + dwVolumeLeft = ((50 * 0xFFFF) / 100); /* 50% */ + dwVolumeRight = ((50 * 0xFFFF) / 100); /* 50% */ + dwVolume = ((dwVolumeLeft << 16) | dwVolumeRight); + + if (device == NULL || oss->mixer_handle == -1) + return dwVolume; + + if (ioctl(oss->mixer_handle, MIXER_READ(SOUND_MIXER_VOLUME), &vol) == -1) + { + OSS_LOG_ERR("MIXER_READ", errno); + return dwVolume; + } + + dwVolumeLeft = (((vol & 0x7f) * 0xFFFF) / 100); + dwVolumeRight = ((((vol >> 8) & 0x7f) * 0xFFFF) / 100); + dwVolume = ((dwVolumeLeft << 16) | dwVolumeRight); + return dwVolume; +} + +static BOOL rdpsnd_oss_set_volume(rdpsndDevicePlugin* device, UINT32 value) +{ + int left, right; + rdpsndOssPlugin* oss = (rdpsndOssPlugin*)device; + + if (device == NULL || oss->mixer_handle == -1) + return FALSE; + + left = (((value & 0xFFFF) * 100) / 0xFFFF); + right = ((((value >> 16) & 0xFFFF) * 100) / 0xFFFF); + + if (left < 0) + left = 0; + else if (left > 100) + left = 100; + + if (right < 0) + right = 0; + else if (right > 100) + right = 100; + + left |= (right << 8); + + if (ioctl(oss->mixer_handle, MIXER_WRITE(SOUND_MIXER_VOLUME), &left) == -1) + { + OSS_LOG_ERR("WRITE_MIXER", errno); + return FALSE; + } + + return TRUE; +} + +static UINT rdpsnd_oss_play(rdpsndDevicePlugin* device, const BYTE* data, size_t size) +{ + rdpsndOssPlugin* oss = (rdpsndOssPlugin*)device; + + if (device == NULL || oss->mixer_handle == -1) + return 0; + + while (size > 0) + { + ssize_t status = write(oss->pcm_handle, data, size); + + if (status < 0) + { + OSS_LOG_ERR("write fail", errno); + rdpsnd_oss_close(device); + rdpsnd_oss_open(device, NULL, oss->latency); + break; + } + + data += status; + + if ((size_t)status <= size) + size -= (size_t)status; + else + size = 0; + } + + return 10; /* TODO: Get real latency in [ms] */ +} + +static int rdpsnd_oss_parse_addin_args(rdpsndDevicePlugin* device, ADDIN_ARGV* args) +{ + int status; + char *str_num, *eptr; + DWORD flags; + COMMAND_LINE_ARGUMENT_A* arg; + rdpsndOssPlugin* oss = (rdpsndOssPlugin*)device; + COMMAND_LINE_ARGUMENT_A rdpsnd_oss_args[] = { { "dev", COMMAND_LINE_VALUE_REQUIRED, "", + NULL, NULL, -1, NULL, "device" }, + { NULL, 0, NULL, NULL, NULL, -1, NULL, NULL } }; + flags = + COMMAND_LINE_SIGIL_NONE | COMMAND_LINE_SEPARATOR_COLON | COMMAND_LINE_IGN_UNKNOWN_KEYWORD; + status = + CommandLineParseArgumentsA(args->argc, args->argv, rdpsnd_oss_args, flags, oss, NULL, NULL); + + if (status < 0) + return status; + + arg = rdpsnd_oss_args; + errno = 0; + + do + { + if (!(arg->Flags & COMMAND_LINE_VALUE_PRESENT)) + continue; + + CommandLineSwitchStart(arg) CommandLineSwitchCase(arg, "dev") + { + str_num = _strdup(arg->Value); + + if (!str_num) + return ERROR_OUTOFMEMORY; + + { + long val = strtol(str_num, &eptr, 10); + + if ((errno != 0) || (val < INT32_MIN) || (val > INT32_MAX)) + { + free(str_num); + return CHANNEL_RC_NULL_DATA; + } + + oss->dev_unit = val; + } + + if (oss->dev_unit < 0 || *eptr != '\0') + oss->dev_unit = -1; + + free(str_num); + } + CommandLineSwitchEnd(arg) + } while ((arg = CommandLineFindNextArgumentA(arg)) != NULL); + + return status; +} + +#ifdef BUILTIN_CHANNELS +#define freerdp_rdpsnd_client_subsystem_entry oss_freerdp_rdpsnd_client_subsystem_entry +#else +#define freerdp_rdpsnd_client_subsystem_entry FREERDP_API freerdp_rdpsnd_client_subsystem_entry +#endif + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT freerdp_rdpsnd_client_subsystem_entry(PFREERDP_RDPSND_DEVICE_ENTRY_POINTS pEntryPoints) +{ + ADDIN_ARGV* args; + rdpsndOssPlugin* oss; + oss = (rdpsndOssPlugin*)calloc(1, sizeof(rdpsndOssPlugin)); + + if (!oss) + return CHANNEL_RC_NO_MEMORY; + + oss->device.Open = rdpsnd_oss_open; + oss->device.FormatSupported = rdpsnd_oss_format_supported; + oss->device.GetVolume = rdpsnd_oss_get_volume; + oss->device.SetVolume = rdpsnd_oss_set_volume; + oss->device.Play = rdpsnd_oss_play; + oss->device.Close = rdpsnd_oss_close; + oss->device.Free = rdpsnd_oss_free; + oss->pcm_handle = -1; + oss->mixer_handle = -1; + oss->dev_unit = -1; + args = pEntryPoints->args; + rdpsnd_oss_parse_addin_args((rdpsndDevicePlugin*)oss, args); + pEntryPoints->pRegisterRdpsndDevice(pEntryPoints->rdpsnd, (rdpsndDevicePlugin*)oss); + return CHANNEL_RC_OK; +} diff --git a/channels/rdpsnd/client/proxy/CMakeLists.txt b/channels/rdpsnd/client/proxy/CMakeLists.txt new file mode 100644 index 0000000..2d40750 --- /dev/null +++ b/channels/rdpsnd/client/proxy/CMakeLists.txt @@ -0,0 +1,33 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2019 Armin Novak +# Copyright 2019 Thincast Technologies GmbH +# +# Licensed 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. + +define_channel_client_subsystem("rdpsnd" "proxy" "") + +set(${MODULE_PREFIX}_SRCS + rdpsnd_proxy.c) + +include_directories(..) + +add_channel_client_subsystem_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} "" TRUE "") + +list(APPEND ${MODULE_PREFIX}_LIBS freerdp) +list(APPEND ${MODULE_PREFIX}_LIBS winpr) + +target_link_libraries(${MODULE_NAME} ${${MODULE_PREFIX}_LIBS}) + +set_property(TARGET ${MODULE_NAME} PROPERTY FOLDER "Channels/${CHANNEL_NAME}/Client/proxy") diff --git a/channels/rdpsnd/client/proxy/rdpsnd_proxy.c b/channels/rdpsnd/client/proxy/rdpsnd_proxy.c new file mode 100644 index 0000000..dd6af04 --- /dev/null +++ b/channels/rdpsnd/client/proxy/rdpsnd_proxy.c @@ -0,0 +1,144 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * FreeRDP rdpsnd proxy subsystem + * + * Copyright 2019 Kobi Mizrachi + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "rdpsnd_main.h" +#include "../../../../server/proxy/pf_context.h" + +typedef struct rdpsnd_proxy_plugin rdpsndProxyPlugin; + +struct rdpsnd_proxy_plugin +{ + rdpsndDevicePlugin device; + RdpsndServerContext* rdpsnd_server; +}; + +static BOOL rdpsnd_proxy_open(rdpsndDevicePlugin* device, const AUDIO_FORMAT* format, + UINT32 latency) +{ + rdpsndProxyPlugin* proxy = (rdpsndProxyPlugin*)device; + + /* update proxy's rdpsnd server latency */ + proxy->rdpsnd_server->latency = latency; + return TRUE; +} + +static void rdpsnd_proxy_close(rdpsndDevicePlugin* device) +{ + /* do nothing */ +} + +static BOOL rdpsnd_proxy_set_volume(rdpsndDevicePlugin* device, UINT32 value) +{ + rdpsndProxyPlugin* proxy = (rdpsndProxyPlugin*)device; + proxy->rdpsnd_server->SetVolume(proxy->rdpsnd_server, value, value); + return TRUE; +} + +static void rdpsnd_proxy_free(rdpsndDevicePlugin* device) +{ + rdpsndProxyPlugin* proxy = (rdpsndProxyPlugin*)device; + + if (!proxy) + return; + + free(proxy); +} + +static BOOL rdpsnd_proxy_format_supported(rdpsndDevicePlugin* device, const AUDIO_FORMAT* format) +{ + rdpsndProxyPlugin* proxy = (rdpsndProxyPlugin*)device; + + /* use the same format that proxy's server used */ + if (proxy->rdpsnd_server->selected_client_format == format->wFormatTag) + return TRUE; + + return FALSE; +} + +static UINT rdpsnd_proxy_play(rdpsndDevicePlugin* device, const BYTE* data, size_t size) +{ + rdpsndProxyPlugin* proxy = (rdpsndProxyPlugin*)device; + UINT64 start = GetTickCount(); + proxy->rdpsnd_server->SendSamples(proxy->rdpsnd_server, data, size / 4, start); + return GetTickCount() - start; +} + + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ + +#ifdef BUILTIN_CHANNELS +#define freerdp_rdpsnd_client_subsystem_entry proxy_freerdp_rdpsnd_client_subsystem_entry +#else +#define freerdp_rdpsnd_client_subsystem_entry FREERDP_API freerdp_rdpsnd_client_subsystem_entry +#endif + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT freerdp_rdpsnd_client_subsystem_entry(PFREERDP_RDPSND_DEVICE_ENTRY_POINTS pEntryPoints) +{ + ADDIN_ARGV* args; + rdpsndProxyPlugin* proxy; + pClientContext* pc; + proxy = (rdpsndProxyPlugin*)calloc(1, sizeof(rdpsndProxyPlugin)); + + if (!proxy) + return CHANNEL_RC_NO_MEMORY; + + proxy->device.Open = rdpsnd_proxy_open; + proxy->device.FormatSupported = rdpsnd_proxy_format_supported; + proxy->device.SetVolume = rdpsnd_proxy_set_volume; + proxy->device.Play = rdpsnd_proxy_play; + proxy->device.Close = rdpsnd_proxy_close; + proxy->device.Free = rdpsnd_proxy_free; + args = pEntryPoints->args; + + pEntryPoints->pRegisterRdpsndDevice(pEntryPoints->rdpsnd, &proxy->device); + pc = (pClientContext*)freerdp_rdpsnd_get_context(pEntryPoints->rdpsnd); + if (pc == NULL) + { + free(proxy); + return ERROR_INTERNAL_ERROR; + } + + proxy->rdpsnd_server = pc->pdata->ps->rdpsnd; + return CHANNEL_RC_OK; +} diff --git a/channels/rdpsnd/client/pulse/CMakeLists.txt b/channels/rdpsnd/client/pulse/CMakeLists.txt new file mode 100644 index 0000000..3a57448 --- /dev/null +++ b/channels/rdpsnd/client/pulse/CMakeLists.txt @@ -0,0 +1,34 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2012 Marc-Andre Moreau +# +# Licensed 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. + +define_channel_client_subsystem("rdpsnd" "pulse" "") + +set(${MODULE_PREFIX}_SRCS + rdpsnd_pulse.c) + +include_directories(..) +include_directories(${PULSE_INCLUDE_DIR}) + +add_channel_client_subsystem_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} "" TRUE "") + +list(APPEND ${MODULE_PREFIX}_LIBS ${PULSE_LIBRARY}) +list(APPEND ${MODULE_PREFIX}_LIBS freerdp) +list(APPEND ${MODULE_PREFIX}_LIBS winpr) + +target_link_libraries(${MODULE_NAME} ${${MODULE_PREFIX}_LIBS}) + +set_property(TARGET ${MODULE_NAME} PROPERTY FOLDER "Channels/${CHANNEL_NAME}/Client/Pulse") diff --git a/channels/rdpsnd/client/pulse/rdpsnd_pulse.c b/channels/rdpsnd/client/pulse/rdpsnd_pulse.c new file mode 100644 index 0000000..7a95470 --- /dev/null +++ b/channels/rdpsnd/client/pulse/rdpsnd_pulse.c @@ -0,0 +1,631 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Audio Output Virtual Channel + * + * Copyright 2011 Vic Lee + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include + +#include +#include +#include + +#include + +#include +#include + +#include "rdpsnd_main.h" + +typedef struct rdpsnd_pulse_plugin rdpsndPulsePlugin; + +struct rdpsnd_pulse_plugin +{ + rdpsndDevicePlugin device; + + char* device_name; + pa_threaded_mainloop* mainloop; + pa_context* context; + pa_sample_spec sample_spec; + pa_stream* stream; + UINT32 latency; + UINT32 volume; +}; + +static BOOL rdpsnd_pulse_format_supported(rdpsndDevicePlugin* device, const AUDIO_FORMAT* format); + +static void rdpsnd_pulse_get_sink_info(pa_context* c, const pa_sink_info* i, int eol, + void* userdata) +{ + uint8_t x; + UINT16 dwVolumeLeft = ((50 * 0xFFFF) / 100); /* 50% */ + ; + UINT16 dwVolumeRight = ((50 * 0xFFFF) / 100); /* 50% */ + ; + rdpsndPulsePlugin* pulse = (rdpsndPulsePlugin*)userdata; + + if (!pulse || !c || !i) + return; + + for (x = 0; x < i->volume.channels; x++) + { + pa_volume_t volume = i->volume.values[x]; + + if (volume >= PA_VOLUME_NORM) + volume = PA_VOLUME_NORM - 1; + + switch (x) + { + case 0: + dwVolumeLeft = (UINT16)volume; + break; + + case 1: + dwVolumeRight = (UINT16)volume; + break; + + default: + break; + } + } + + pulse->volume = ((UINT32)dwVolumeLeft << 16U) | dwVolumeRight; +} + +static void rdpsnd_pulse_context_state_callback(pa_context* context, void* userdata) +{ + pa_context_state_t state; + rdpsndPulsePlugin* pulse = (rdpsndPulsePlugin*)userdata; + state = pa_context_get_state(context); + + switch (state) + { + case PA_CONTEXT_READY: + pa_threaded_mainloop_signal(pulse->mainloop, 0); + break; + + case PA_CONTEXT_FAILED: + case PA_CONTEXT_TERMINATED: + pa_threaded_mainloop_signal(pulse->mainloop, 0); + break; + + default: + break; + } +} + +static BOOL rdpsnd_pulse_connect(rdpsndDevicePlugin* device) +{ + pa_operation* o; + pa_context_state_t state; + rdpsndPulsePlugin* pulse = (rdpsndPulsePlugin*)device; + + if (!pulse->context) + return FALSE; + + if (pa_context_connect(pulse->context, NULL, 0, NULL)) + { + return FALSE; + } + + pa_threaded_mainloop_lock(pulse->mainloop); + + if (pa_threaded_mainloop_start(pulse->mainloop) < 0) + { + pa_threaded_mainloop_unlock(pulse->mainloop); + return FALSE; + } + + for (;;) + { + state = pa_context_get_state(pulse->context); + + if (state == PA_CONTEXT_READY) + break; + + if (!PA_CONTEXT_IS_GOOD(state)) + { + break; + } + + pa_threaded_mainloop_wait(pulse->mainloop); + } + + o = pa_context_get_sink_info_by_index(pulse->context, 0, rdpsnd_pulse_get_sink_info, pulse); + + if (o) + pa_operation_unref(o); + + pa_threaded_mainloop_unlock(pulse->mainloop); + + if (state == PA_CONTEXT_READY) + { + return TRUE; + } + else + { + pa_context_disconnect(pulse->context); + return FALSE; + } +} + +static void rdpsnd_pulse_stream_success_callback(pa_stream* stream, int success, void* userdata) +{ + rdpsndPulsePlugin* pulse = (rdpsndPulsePlugin*)userdata; + pa_threaded_mainloop_signal(pulse->mainloop, 0); +} + +static void rdpsnd_pulse_wait_for_operation(rdpsndPulsePlugin* pulse, pa_operation* operation) +{ + if (!operation) + return; + + while (pa_operation_get_state(operation) == PA_OPERATION_RUNNING) + { + pa_threaded_mainloop_wait(pulse->mainloop); + } + + pa_operation_unref(operation); +} + +static void rdpsnd_pulse_stream_state_callback(pa_stream* stream, void* userdata) +{ + pa_stream_state_t state; + rdpsndPulsePlugin* pulse = (rdpsndPulsePlugin*)userdata; + state = pa_stream_get_state(stream); + + switch (state) + { + case PA_STREAM_READY: + pa_threaded_mainloop_signal(pulse->mainloop, 0); + break; + + case PA_STREAM_FAILED: + case PA_STREAM_TERMINATED: + pa_threaded_mainloop_signal(pulse->mainloop, 0); + break; + + default: + break; + } +} + +static void rdpsnd_pulse_stream_request_callback(pa_stream* stream, size_t length, void* userdata) +{ + rdpsndPulsePlugin* pulse = (rdpsndPulsePlugin*)userdata; + pa_threaded_mainloop_signal(pulse->mainloop, 0); +} + +static void rdpsnd_pulse_close(rdpsndDevicePlugin* device) +{ + rdpsndPulsePlugin* pulse = (rdpsndPulsePlugin*)device; + + if (!pulse->context || !pulse->stream) + return; + + pa_threaded_mainloop_lock(pulse->mainloop); + rdpsnd_pulse_wait_for_operation( + pulse, pa_stream_drain(pulse->stream, rdpsnd_pulse_stream_success_callback, pulse)); + pa_stream_disconnect(pulse->stream); + pa_stream_unref(pulse->stream); + pulse->stream = NULL; + pa_threaded_mainloop_unlock(pulse->mainloop); +} + +static BOOL rdpsnd_pulse_set_format_spec(rdpsndPulsePlugin* pulse, const AUDIO_FORMAT* format) +{ + pa_sample_spec sample_spec = { 0 }; + + if (!pulse->context) + return FALSE; + + if (!rdpsnd_pulse_format_supported(&pulse->device, format)) + return FALSE; + + sample_spec.rate = format->nSamplesPerSec; + sample_spec.channels = format->nChannels; + + switch (format->wFormatTag) + { + case WAVE_FORMAT_PCM: + switch (format->wBitsPerSample) + { + case 8: + sample_spec.format = PA_SAMPLE_U8; + break; + + case 16: + sample_spec.format = PA_SAMPLE_S16LE; + break; + + default: + return FALSE; + } + + break; + + case WAVE_FORMAT_ALAW: + sample_spec.format = PA_SAMPLE_ALAW; + break; + + case WAVE_FORMAT_MULAW: + sample_spec.format = PA_SAMPLE_ULAW; + break; + + default: + return FALSE; + } + + pulse->sample_spec = sample_spec; + return TRUE; +} + +static BOOL rdpsnd_pulse_open(rdpsndDevicePlugin* device, const AUDIO_FORMAT* format, + UINT32 latency) +{ + pa_stream_state_t state; + pa_stream_flags_t flags; + pa_buffer_attr buffer_attr = { 0 }; + char ss[PA_SAMPLE_SPEC_SNPRINT_MAX]; + rdpsndPulsePlugin* pulse = (rdpsndPulsePlugin*)device; + + if (!pulse->context || pulse->stream) + return TRUE; + + if (!rdpsnd_pulse_set_format_spec(pulse, format)) + return FALSE; + + pulse->latency = latency; + + if (pa_sample_spec_valid(&pulse->sample_spec) == 0) + { + pa_sample_spec_snprint(ss, sizeof(ss), &pulse->sample_spec); + return TRUE; + } + + pa_threaded_mainloop_lock(pulse->mainloop); + pulse->stream = pa_stream_new(pulse->context, "freerdp", &pulse->sample_spec, NULL); + + if (!pulse->stream) + { + pa_threaded_mainloop_unlock(pulse->mainloop); + return FALSE; + } + + /* register essential callbacks */ + pa_stream_set_state_callback(pulse->stream, rdpsnd_pulse_stream_state_callback, pulse); + pa_stream_set_write_callback(pulse->stream, rdpsnd_pulse_stream_request_callback, pulse); + flags = PA_STREAM_INTERPOLATE_TIMING | PA_STREAM_AUTO_TIMING_UPDATE; + + if (pulse->latency > 0) + { + buffer_attr.maxlength = pa_usec_to_bytes(pulse->latency * 2 * 1000, &pulse->sample_spec); + buffer_attr.tlength = pa_usec_to_bytes(pulse->latency * 1000, &pulse->sample_spec); + buffer_attr.prebuf = (UINT32)-1; + buffer_attr.minreq = (UINT32)-1; + buffer_attr.fragsize = (UINT32)-1; + flags |= PA_STREAM_ADJUST_LATENCY; + } + + if (pa_stream_connect_playback(pulse->stream, pulse->device_name, + pulse->latency > 0 ? &buffer_attr : NULL, flags, NULL, NULL) < 0) + { + pa_threaded_mainloop_unlock(pulse->mainloop); + return TRUE; + } + + for (;;) + { + state = pa_stream_get_state(pulse->stream); + + if (state == PA_STREAM_READY) + break; + + if (!PA_STREAM_IS_GOOD(state)) + { + break; + } + + pa_threaded_mainloop_wait(pulse->mainloop); + } + + pa_threaded_mainloop_unlock(pulse->mainloop); + + if (state == PA_STREAM_READY) + return TRUE; + + rdpsnd_pulse_close(device); + return FALSE; +} + +static void rdpsnd_pulse_free(rdpsndDevicePlugin* device) +{ + rdpsndPulsePlugin* pulse = (rdpsndPulsePlugin*)device; + + if (!pulse) + return; + + rdpsnd_pulse_close(device); + + if (pulse->mainloop) + { + pa_threaded_mainloop_stop(pulse->mainloop); + } + + if (pulse->context) + { + pa_context_disconnect(pulse->context); + pa_context_unref(pulse->context); + pulse->context = NULL; + } + + if (pulse->mainloop) + { + pa_threaded_mainloop_free(pulse->mainloop); + pulse->mainloop = NULL; + } + + free(pulse->device_name); + free(pulse); +} + +static BOOL rdpsnd_pulse_default_format(rdpsndDevicePlugin* device, const AUDIO_FORMAT* desired, + AUDIO_FORMAT* defaultFormat) +{ + rdpsndPulsePlugin* pulse = (rdpsndPulsePlugin*)device; + if (!pulse || !defaultFormat) + return FALSE; + + *defaultFormat = *desired; + defaultFormat->data = NULL; + defaultFormat->cbSize = 0; + defaultFormat->wFormatTag = WAVE_FORMAT_PCM; + if ((defaultFormat->nChannels < 1) || (defaultFormat->nChannels > PA_CHANNELS_MAX)) + defaultFormat->nChannels = 2; + if ((defaultFormat->nSamplesPerSec < 1) || (defaultFormat->nSamplesPerSec > PA_RATE_MAX)) + defaultFormat->nSamplesPerSec = 44100; + if ((defaultFormat->wBitsPerSample != 8) && (defaultFormat->wBitsPerSample != 16)) + defaultFormat->wBitsPerSample = 16; + + defaultFormat->nBlockAlign = defaultFormat->nChannels * defaultFormat->wBitsPerSample / 8; + defaultFormat->nAvgBytesPerSec = defaultFormat->nBlockAlign * defaultFormat->nSamplesPerSec; + return TRUE; +} + +BOOL rdpsnd_pulse_format_supported(rdpsndDevicePlugin* device, const AUDIO_FORMAT* format) +{ + switch (format->wFormatTag) + { + case WAVE_FORMAT_PCM: + if (format->cbSize == 0 && (format->nSamplesPerSec <= PA_RATE_MAX) && + (format->wBitsPerSample == 8 || format->wBitsPerSample == 16) && + (format->nChannels >= 1 && format->nChannels <= PA_CHANNELS_MAX)) + { + return TRUE; + } + + break; + + default: + break; + } + + return FALSE; +} + +static UINT32 rdpsnd_pulse_get_volume(rdpsndDevicePlugin* device) +{ + pa_operation* o; + rdpsndPulsePlugin* pulse = (rdpsndPulsePlugin*)device; + + if (!pulse) + return 0; + + if (!pulse->context || !pulse->mainloop) + return 0; + + pa_threaded_mainloop_lock(pulse->mainloop); + o = pa_context_get_sink_info_by_index(pulse->context, 0, rdpsnd_pulse_get_sink_info, pulse); + pa_operation_unref(o); + pa_threaded_mainloop_unlock(pulse->mainloop); + return pulse->volume; +} + +static BOOL rdpsnd_pulse_set_volume(rdpsndDevicePlugin* device, UINT32 value) +{ + pa_cvolume cv; + pa_volume_t left; + pa_volume_t right; + pa_operation* operation; + rdpsndPulsePlugin* pulse = (rdpsndPulsePlugin*)device; + + if (!pulse->context || !pulse->stream) + return FALSE; + + left = (pa_volume_t)(value & 0xFFFF); + right = (pa_volume_t)((value >> 16) & 0xFFFF); + pa_cvolume_init(&cv); + cv.channels = 2; + cv.values[0] = PA_VOLUME_MUTED + (left * (PA_VOLUME_NORM - PA_VOLUME_MUTED)) / 0xFFFF; + cv.values[1] = PA_VOLUME_MUTED + (right * (PA_VOLUME_NORM - PA_VOLUME_MUTED)) / 0xFFFF; + pa_threaded_mainloop_lock(pulse->mainloop); + operation = pa_context_set_sink_input_volume(pulse->context, pa_stream_get_index(pulse->stream), + &cv, NULL, NULL); + + if (operation) + pa_operation_unref(operation); + + pa_threaded_mainloop_unlock(pulse->mainloop); + return TRUE; +} + +static UINT rdpsnd_pulse_play(rdpsndDevicePlugin* device, const BYTE* data, size_t size) +{ + size_t length; + int status; + pa_usec_t latency; + int negative; + rdpsndPulsePlugin* pulse = (rdpsndPulsePlugin*)device; + + if (!pulse->stream || !data) + return 0; + + pa_threaded_mainloop_lock(pulse->mainloop); + + while (size > 0) + { + while ((length = pa_stream_writable_size(pulse->stream)) == 0) + pa_threaded_mainloop_wait(pulse->mainloop); + + if (length == (size_t)-1) + break; + + if (length > size) + length = size; + + status = pa_stream_write(pulse->stream, data, length, NULL, 0LL, PA_SEEK_RELATIVE); + + if (status < 0) + { + break; + } + + data += length; + size -= length; + } + + if (pa_stream_get_latency(pulse->stream, &latency, &negative) != 0) + latency = 0; + + pa_threaded_mainloop_unlock(pulse->mainloop); + return latency / 1000; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpsnd_pulse_parse_addin_args(rdpsndDevicePlugin* device, ADDIN_ARGV* args) +{ + int status; + DWORD flags; + COMMAND_LINE_ARGUMENT_A* arg; + rdpsndPulsePlugin* pulse = (rdpsndPulsePlugin*)device; + COMMAND_LINE_ARGUMENT_A rdpsnd_pulse_args[] = { { "dev", COMMAND_LINE_VALUE_REQUIRED, + "", NULL, NULL, -1, NULL, "device" }, + { NULL, 0, NULL, NULL, NULL, -1, NULL, NULL } }; + flags = + COMMAND_LINE_SIGIL_NONE | COMMAND_LINE_SEPARATOR_COLON | COMMAND_LINE_IGN_UNKNOWN_KEYWORD; + status = CommandLineParseArgumentsA(args->argc, args->argv, rdpsnd_pulse_args, flags, pulse, + NULL, NULL); + + if (status < 0) + return ERROR_INVALID_DATA; + + arg = rdpsnd_pulse_args; + + do + { + if (!(arg->Flags & COMMAND_LINE_VALUE_PRESENT)) + continue; + + CommandLineSwitchStart(arg) CommandLineSwitchCase(arg, "dev") + { + pulse->device_name = _strdup(arg->Value); + + if (!pulse->device_name) + return ERROR_OUTOFMEMORY; + } + CommandLineSwitchEnd(arg) + } while ((arg = CommandLineFindNextArgumentA(arg)) != NULL); + + return CHANNEL_RC_OK; +} + +#ifdef BUILTIN_CHANNELS +#define freerdp_rdpsnd_client_subsystem_entry pulse_freerdp_rdpsnd_client_subsystem_entry +#else +#define freerdp_rdpsnd_client_subsystem_entry FREERDP_API freerdp_rdpsnd_client_subsystem_entry +#endif + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT freerdp_rdpsnd_client_subsystem_entry(PFREERDP_RDPSND_DEVICE_ENTRY_POINTS pEntryPoints) +{ + ADDIN_ARGV* args; + rdpsndPulsePlugin* pulse; + UINT ret; + pulse = (rdpsndPulsePlugin*)calloc(1, sizeof(rdpsndPulsePlugin)); + + if (!pulse) + return CHANNEL_RC_NO_MEMORY; + + pulse->device.Open = rdpsnd_pulse_open; + pulse->device.FormatSupported = rdpsnd_pulse_format_supported; + pulse->device.GetVolume = rdpsnd_pulse_get_volume; + pulse->device.SetVolume = rdpsnd_pulse_set_volume; + pulse->device.Play = rdpsnd_pulse_play; + pulse->device.Close = rdpsnd_pulse_close; + pulse->device.Free = rdpsnd_pulse_free; + pulse->device.DefaultFormat = rdpsnd_pulse_default_format; + args = pEntryPoints->args; + + if (args->argc > 1) + { + ret = rdpsnd_pulse_parse_addin_args((rdpsndDevicePlugin*)pulse, args); + + if (ret != CHANNEL_RC_OK) + { + WLog_ERR(TAG, "error parsing arguments"); + goto error; + } + } + + ret = CHANNEL_RC_NO_MEMORY; + pulse->mainloop = pa_threaded_mainloop_new(); + + if (!pulse->mainloop) + goto error; + + pulse->context = pa_context_new(pa_threaded_mainloop_get_api(pulse->mainloop), "freerdp"); + + if (!pulse->context) + goto error; + + pa_context_set_state_callback(pulse->context, rdpsnd_pulse_context_state_callback, pulse); + ret = ERROR_INVALID_OPERATION; + + if (!rdpsnd_pulse_connect((rdpsndDevicePlugin*)pulse)) + goto error; + + pEntryPoints->pRegisterRdpsndDevice(pEntryPoints->rdpsnd, (rdpsndDevicePlugin*)pulse); + return CHANNEL_RC_OK; +error: + rdpsnd_pulse_free((rdpsndDevicePlugin*)pulse); + return ret; +} diff --git a/channels/rdpsnd/client/rdpsnd_main.c b/channels/rdpsnd/client/rdpsnd_main.c new file mode 100644 index 0000000..f624058 --- /dev/null +++ b/channels/rdpsnd/client/rdpsnd_main.c @@ -0,0 +1,1706 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Audio Output Virtual Channel + * + * Copyright 2009-2011 Jay Sorg + * Copyright 2010-2011 Vic Lee + * Copyright 2012-2013 Marc-Andre Moreau + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger + * Copyright 2016 David PHAM-VAN + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#ifndef _WIN32 +#include +#include +#endif + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "rdpsnd_common.h" +#include "rdpsnd_main.h" + +struct _RDPSND_CHANNEL_CALLBACK +{ + IWTSVirtualChannelCallback iface; + + IWTSPlugin* plugin; + IWTSVirtualChannelManager* channel_mgr; + IWTSVirtualChannel* channel; +}; +typedef struct _RDPSND_CHANNEL_CALLBACK RDPSND_CHANNEL_CALLBACK; + +struct _RDPSND_LISTENER_CALLBACK +{ + IWTSListenerCallback iface; + + IWTSPlugin* plugin; + IWTSVirtualChannelManager* channel_mgr; + RDPSND_CHANNEL_CALLBACK* channel_callback; +}; +typedef struct _RDPSND_LISTENER_CALLBACK RDPSND_LISTENER_CALLBACK; + +struct rdpsnd_plugin +{ + IWTSPlugin iface; + IWTSListener* listener; + RDPSND_LISTENER_CALLBACK* listener_callback; + + CHANNEL_DEF channelDef; + CHANNEL_ENTRY_POINTS_FREERDP_EX channelEntryPoints; + + wStreamPool* pool; + wStream* data_in; + + void* InitHandle; + DWORD OpenHandle; + + wLog* log; + + BYTE cBlockNo; + UINT16 wQualityMode; + UINT16 wCurrentFormatNo; + + AUDIO_FORMAT* ServerFormats; + UINT16 NumberOfServerFormats; + + AUDIO_FORMAT* ClientFormats; + UINT16 NumberOfClientFormats; + + BOOL attached; + BOOL connected; + BOOL dynamic; + + BOOL expectingWave; + BYTE waveData[4]; + UINT16 waveDataSize; + UINT16 wTimeStamp; + UINT64 wArrivalTime; + + UINT32 latency; + BOOL isOpen; + AUDIO_FORMAT* fixed_format; + + UINT32 startPlayTime; + size_t totalPlaySize; + + char* subsystem; + char* device_name; + + /* Device plugin */ + rdpsndDevicePlugin* device; + rdpContext* rdpcontext; + + FREERDP_DSP_CONTEXT* dsp_context; + + HANDLE thread; + wMessageQueue* queue; + BOOL initialized; +}; + +static const char* rdpsnd_is_dyn_str(BOOL dynamic) +{ + if (dynamic) + return "[dynamic]"; + return "[static]"; +} + +static void rdpsnd_virtual_channel_event_terminated(rdpsndPlugin* rdpsnd); + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpsnd_virtual_channel_write(rdpsndPlugin* rdpsnd, wStream* s); + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpsnd_send_quality_mode_pdu(rdpsndPlugin* rdpsnd) +{ + wStream* pdu; + pdu = Stream_New(NULL, 8); + + if (!pdu) + { + WLog_ERR(TAG, "%s Stream_New failed!", rdpsnd_is_dyn_str(rdpsnd->dynamic)); + return CHANNEL_RC_NO_MEMORY; + } + + Stream_Write_UINT8(pdu, SNDC_QUALITYMODE); /* msgType */ + Stream_Write_UINT8(pdu, 0); /* bPad */ + Stream_Write_UINT16(pdu, 4); /* BodySize */ + Stream_Write_UINT16(pdu, rdpsnd->wQualityMode); /* wQualityMode */ + Stream_Write_UINT16(pdu, 0); /* Reserved */ + WLog_Print(rdpsnd->log, WLOG_DEBUG, "%s QualityMode: %" PRIu16 "", + rdpsnd_is_dyn_str(rdpsnd->dynamic), rdpsnd->wQualityMode); + return rdpsnd_virtual_channel_write(rdpsnd, pdu); +} + +static void rdpsnd_select_supported_audio_formats(rdpsndPlugin* rdpsnd) +{ + UINT16 index; + audio_formats_free(rdpsnd->ClientFormats, rdpsnd->NumberOfClientFormats); + rdpsnd->NumberOfClientFormats = 0; + rdpsnd->ClientFormats = NULL; + + if (!rdpsnd->NumberOfServerFormats) + return; + + rdpsnd->ClientFormats = audio_formats_new(rdpsnd->NumberOfServerFormats); + + if (!rdpsnd->ClientFormats || !rdpsnd->device) + return; + + for (index = 0; index < rdpsnd->NumberOfServerFormats; index++) + { + const AUDIO_FORMAT* serverFormat = &rdpsnd->ServerFormats[index]; + + if (!audio_format_compatible(rdpsnd->fixed_format, serverFormat)) + continue; + + if (freerdp_dsp_supports_format(serverFormat, FALSE) || + rdpsnd->device->FormatSupported(rdpsnd->device, serverFormat)) + { + AUDIO_FORMAT* clientFormat = &rdpsnd->ClientFormats[rdpsnd->NumberOfClientFormats++]; + audio_format_copy(serverFormat, clientFormat); + } + } +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpsnd_send_client_audio_formats(rdpsndPlugin* rdpsnd) +{ + UINT16 index; + wStream* pdu; + UINT16 length; + UINT32 dwVolume; + UINT16 wNumberOfFormats; + + if (!rdpsnd->device || (!rdpsnd->dynamic && (rdpsnd->OpenHandle == 0))) + return CHANNEL_RC_INITIALIZATION_ERROR; + + dwVolume = IFCALLRESULT(0, rdpsnd->device->GetVolume, rdpsnd->device); + wNumberOfFormats = rdpsnd->NumberOfClientFormats; + length = 4 + 20; + + for (index = 0; index < wNumberOfFormats; index++) + length += (18 + rdpsnd->ClientFormats[index].cbSize); + + pdu = Stream_New(NULL, length); + + if (!pdu) + { + WLog_ERR(TAG, "%s Stream_New failed!", rdpsnd_is_dyn_str(rdpsnd->dynamic)); + return CHANNEL_RC_NO_MEMORY; + } + + Stream_Write_UINT8(pdu, SNDC_FORMATS); /* msgType */ + Stream_Write_UINT8(pdu, 0); /* bPad */ + Stream_Write_UINT16(pdu, length - 4); /* BodySize */ + Stream_Write_UINT32(pdu, TSSNDCAPS_ALIVE | TSSNDCAPS_VOLUME); /* dwFlags */ + Stream_Write_UINT32(pdu, dwVolume); /* dwVolume */ + Stream_Write_UINT32(pdu, 0); /* dwPitch */ + Stream_Write_UINT16(pdu, 0); /* wDGramPort */ + Stream_Write_UINT16(pdu, wNumberOfFormats); /* wNumberOfFormats */ + Stream_Write_UINT8(pdu, 0); /* cLastBlockConfirmed */ + Stream_Write_UINT16(pdu, CHANNEL_VERSION_WIN_MAX); /* wVersion */ + Stream_Write_UINT8(pdu, 0); /* bPad */ + + for (index = 0; index < wNumberOfFormats; index++) + { + const AUDIO_FORMAT* clientFormat = &rdpsnd->ClientFormats[index]; + + if (!audio_format_write(pdu, clientFormat)) + { + Stream_Free(pdu, TRUE); + return ERROR_INTERNAL_ERROR; + } + } + + WLog_Print(rdpsnd->log, WLOG_DEBUG, "%s Client Audio Formats", + rdpsnd_is_dyn_str(rdpsnd->dynamic)); + return rdpsnd_virtual_channel_write(rdpsnd, pdu); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpsnd_recv_server_audio_formats_pdu(rdpsndPlugin* rdpsnd, wStream* s) +{ + UINT16 index; + UINT16 wVersion; + UINT16 wNumberOfFormats; + UINT ret = ERROR_BAD_LENGTH; + audio_formats_free(rdpsnd->ServerFormats, rdpsnd->NumberOfServerFormats); + rdpsnd->NumberOfServerFormats = 0; + rdpsnd->ServerFormats = NULL; + + if (Stream_GetRemainingLength(s) < 30) + return ERROR_BAD_LENGTH; + + /* http://msdn.microsoft.com/en-us/library/cc240956.aspx */ + Stream_Seek_UINT32(s); /* dwFlags */ + Stream_Seek_UINT32(s); /* dwVolume */ + Stream_Seek_UINT32(s); /* dwPitch */ + Stream_Seek_UINT16(s); /* wDGramPort */ + Stream_Read_UINT16(s, wNumberOfFormats); + Stream_Read_UINT8(s, rdpsnd->cBlockNo); /* cLastBlockConfirmed */ + Stream_Read_UINT16(s, wVersion); /* wVersion */ + Stream_Seek_UINT8(s); /* bPad */ + rdpsnd->NumberOfServerFormats = wNumberOfFormats; + + if (Stream_GetRemainingLength(s) / 14 < wNumberOfFormats) + return ERROR_BAD_LENGTH; + + rdpsnd->ServerFormats = audio_formats_new(wNumberOfFormats); + + if (!rdpsnd->ServerFormats) + return CHANNEL_RC_NO_MEMORY; + + for (index = 0; index < wNumberOfFormats; index++) + { + AUDIO_FORMAT* format = &rdpsnd->ServerFormats[index]; + + if (!audio_format_read(s, format)) + goto out_fail; + } + + rdpsnd_select_supported_audio_formats(rdpsnd); + WLog_Print(rdpsnd->log, WLOG_DEBUG, "%s Server Audio Formats", + rdpsnd_is_dyn_str(rdpsnd->dynamic)); + ret = rdpsnd_send_client_audio_formats(rdpsnd); + + if (ret == CHANNEL_RC_OK) + { + if (wVersion >= CHANNEL_VERSION_WIN_7) + ret = rdpsnd_send_quality_mode_pdu(rdpsnd); + } + + return ret; +out_fail: + audio_formats_free(rdpsnd->ServerFormats, rdpsnd->NumberOfServerFormats); + rdpsnd->ServerFormats = NULL; + rdpsnd->NumberOfServerFormats = 0; + return ret; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpsnd_send_training_confirm_pdu(rdpsndPlugin* rdpsnd, UINT16 wTimeStamp, + UINT16 wPackSize) +{ + wStream* pdu; + pdu = Stream_New(NULL, 8); + + if (!pdu) + { + WLog_ERR(TAG, "%s Stream_New failed!", rdpsnd_is_dyn_str(rdpsnd->dynamic)); + return CHANNEL_RC_NO_MEMORY; + } + + Stream_Write_UINT8(pdu, SNDC_TRAINING); /* msgType */ + Stream_Write_UINT8(pdu, 0); /* bPad */ + Stream_Write_UINT16(pdu, 4); /* BodySize */ + Stream_Write_UINT16(pdu, wTimeStamp); + Stream_Write_UINT16(pdu, wPackSize); + WLog_Print(rdpsnd->log, WLOG_DEBUG, + "%s Training Response: wTimeStamp: %" PRIu16 " wPackSize: %" PRIu16 "", + rdpsnd_is_dyn_str(rdpsnd->dynamic), wTimeStamp, wPackSize); + return rdpsnd_virtual_channel_write(rdpsnd, pdu); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpsnd_recv_training_pdu(rdpsndPlugin* rdpsnd, wStream* s) +{ + UINT16 wTimeStamp; + UINT16 wPackSize; + + if (Stream_GetRemainingLength(s) < 4) + return ERROR_BAD_LENGTH; + + Stream_Read_UINT16(s, wTimeStamp); + Stream_Read_UINT16(s, wPackSize); + WLog_Print(rdpsnd->log, WLOG_DEBUG, + "%s Training Request: wTimeStamp: %" PRIu16 " wPackSize: %" PRIu16 "", + rdpsnd_is_dyn_str(rdpsnd->dynamic), wTimeStamp, wPackSize); + return rdpsnd_send_training_confirm_pdu(rdpsnd, wTimeStamp, wPackSize); +} + +static BOOL rdpsnd_ensure_device_is_open(rdpsndPlugin* rdpsnd, UINT32 wFormatNo, + const AUDIO_FORMAT* format) +{ + if (!rdpsnd) + return FALSE; + + if (!rdpsnd->isOpen || (wFormatNo != rdpsnd->wCurrentFormatNo)) + { + BOOL rc; + BOOL supported; + AUDIO_FORMAT deviceFormat = *format; + + IFCALL(rdpsnd->device->Close, rdpsnd->device); + supported = IFCALLRESULT(FALSE, rdpsnd->device->FormatSupported, rdpsnd->device, format); + + if (!supported) + { + if (!IFCALLRESULT(FALSE, rdpsnd->device->DefaultFormat, rdpsnd->device, format, + &deviceFormat)) + { + deviceFormat.wFormatTag = WAVE_FORMAT_PCM; + deviceFormat.wBitsPerSample = 16; + deviceFormat.cbSize = 0; + } + } + + WLog_Print(rdpsnd->log, WLOG_DEBUG, "%s Opening device with format %s [backend %s]", + rdpsnd_is_dyn_str(rdpsnd->dynamic), + audio_format_get_tag_string(format->wFormatTag), + audio_format_get_tag_string(deviceFormat.wFormatTag)); + rc = IFCALLRESULT(FALSE, rdpsnd->device->Open, rdpsnd->device, &deviceFormat, + rdpsnd->latency); + + if (!rc) + return FALSE; + + if (!supported) + { + if (!freerdp_dsp_context_reset(rdpsnd->dsp_context, format)) + return FALSE; + } + + rdpsnd->isOpen = TRUE; + rdpsnd->wCurrentFormatNo = wFormatNo; + rdpsnd->startPlayTime = 0; + rdpsnd->totalPlaySize = 0; + } + + return TRUE; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpsnd_recv_wave_info_pdu(rdpsndPlugin* rdpsnd, wStream* s, UINT16 BodySize) +{ + UINT16 wFormatNo; + const AUDIO_FORMAT* format; + + if (Stream_GetRemainingLength(s) < 12) + return ERROR_BAD_LENGTH; + + rdpsnd->wArrivalTime = GetTickCount64(); + Stream_Read_UINT16(s, rdpsnd->wTimeStamp); + Stream_Read_UINT16(s, wFormatNo); + + if (wFormatNo >= rdpsnd->NumberOfClientFormats) + return ERROR_INVALID_DATA; + + Stream_Read_UINT8(s, rdpsnd->cBlockNo); + Stream_Seek(s, 3); /* bPad */ + Stream_Read(s, rdpsnd->waveData, 4); + rdpsnd->waveDataSize = BodySize - 8; + format = &rdpsnd->ClientFormats[wFormatNo]; + WLog_Print(rdpsnd->log, WLOG_DEBUG, + "%s WaveInfo: cBlockNo: %" PRIu8 " wFormatNo: %" PRIu16 " [%s]", + rdpsnd_is_dyn_str(rdpsnd->dynamic), rdpsnd->cBlockNo, wFormatNo, + audio_format_get_tag_string(format->wFormatTag)); + + if (!rdpsnd_ensure_device_is_open(rdpsnd, wFormatNo, format)) + return ERROR_INTERNAL_ERROR; + + rdpsnd->expectingWave = TRUE; + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpsnd_send_wave_confirm_pdu(rdpsndPlugin* rdpsnd, UINT16 wTimeStamp, + BYTE cConfirmedBlockNo) +{ + wStream* pdu; + pdu = Stream_New(NULL, 8); + + if (!pdu) + { + WLog_ERR(TAG, "%s Stream_New failed!", rdpsnd_is_dyn_str(rdpsnd->dynamic)); + return CHANNEL_RC_NO_MEMORY; + } + + Stream_Write_UINT8(pdu, SNDC_WAVECONFIRM); + Stream_Write_UINT8(pdu, 0); + Stream_Write_UINT16(pdu, 4); + Stream_Write_UINT16(pdu, wTimeStamp); + Stream_Write_UINT8(pdu, cConfirmedBlockNo); /* cConfirmedBlockNo */ + Stream_Write_UINT8(pdu, 0); /* bPad */ + return rdpsnd_virtual_channel_write(rdpsnd, pdu); +} + +static BOOL rdpsnd_detect_overrun(rdpsndPlugin* rdpsnd, const AUDIO_FORMAT* format, size_t size) +{ + UINT32 bpf; + UINT32 now; + UINT32 duration; + UINT32 totalDuration; + UINT32 remainingDuration; + UINT32 maxDuration; + + if (!rdpsnd || !format) + return FALSE; + + /* Older windows RDP servers do not limit the send buffer, which can + * cause quite a large amount of sound data buffered client side. + * If e.g. sound is paused server side the client will keep playing + * for a long time instead of pausing playback. + * + * To avoid this we check: + * + * 1. Is the sound sample received from a known format these servers + * support + * 2. If it is calculate the size of the client side sound buffer + * 3. If the buffer is too large silently drop the sample which will + * trigger a retransmit later on. + * + * This check must only be applied to these known formats, because + * with newer and other formats the sample size can not be calculated + * without decompressing the sample first. + */ + switch (format->wFormatTag) + { + case WAVE_FORMAT_PCM: + case WAVE_FORMAT_DVI_ADPCM: + case WAVE_FORMAT_ADPCM: + case WAVE_FORMAT_ALAW: + case WAVE_FORMAT_MULAW: + break; + case WAVE_FORMAT_MSG723: + case WAVE_FORMAT_GSM610: + case WAVE_FORMAT_AAC_MS: + default: + return FALSE; + } + + audio_format_print(WLog_Get(TAG), WLOG_DEBUG, format); + bpf = format->nChannels * format->wBitsPerSample * format->nSamplesPerSec / 8; + if (bpf == 0) + return FALSE; + + duration = (UINT32)(1000 * size / bpf); + totalDuration = (UINT32)(1000 * rdpsnd->totalPlaySize / bpf); + now = GetTickCountPrecise(); + if (rdpsnd->startPlayTime == 0) + { + rdpsnd->startPlayTime = now; + rdpsnd->totalPlaySize = size; + return FALSE; + } + else if (now - rdpsnd->startPlayTime > totalDuration + 10) + { + /* Buffer underrun */ + WLog_Print(rdpsnd->log, WLOG_DEBUG, "%s Buffer underrun by %u ms", + rdpsnd_is_dyn_str(rdpsnd->dynamic), + (UINT)(now - rdpsnd->startPlayTime - totalDuration)); + rdpsnd->startPlayTime = now; + rdpsnd->totalPlaySize = size; + return FALSE; + } + else + { + /* Calculate remaining duration to be played */ + remainingDuration = totalDuration - (now - rdpsnd->startPlayTime); + + /* Maximum allow duration calculation */ + maxDuration = duration * 2 + rdpsnd->latency; + + if (remainingDuration + duration > maxDuration) + { + WLog_Print(rdpsnd->log, WLOG_DEBUG, "%s Buffer overrun pending %u ms dropping %u ms", + rdpsnd_is_dyn_str(rdpsnd->dynamic), remainingDuration, duration); + return TRUE; + } + + rdpsnd->totalPlaySize += size; + return FALSE; + } +} + +static UINT rdpsnd_treat_wave(rdpsndPlugin* rdpsnd, wStream* s, size_t size) +{ + BYTE* data; + AUDIO_FORMAT* format; + UINT64 end; + UINT64 diffMS, ts; + UINT latency = 0; + UINT error; + + if (Stream_GetRemainingLength(s) < size) + return ERROR_BAD_LENGTH; + + if (rdpsnd->wCurrentFormatNo >= rdpsnd->NumberOfClientFormats) + return ERROR_INTERNAL_ERROR; + + /* + * Send the first WaveConfirm PDU. The server side uses this to determine the + * network latency. + * See also [MS-RDPEA] 2.2.3.8 Wave Confirm PDU + */ + error = rdpsnd_send_wave_confirm_pdu(rdpsnd, rdpsnd->wTimeStamp, rdpsnd->cBlockNo); + if (error) + return error; + + data = Stream_Pointer(s); + format = &rdpsnd->ClientFormats[rdpsnd->wCurrentFormatNo]; + WLog_Print(rdpsnd->log, WLOG_DEBUG, + "%s Wave: cBlockNo: %" PRIu8 " wTimeStamp: %" PRIu16 ", size: %" PRIdz, + rdpsnd_is_dyn_str(rdpsnd->dynamic), rdpsnd->cBlockNo, rdpsnd->wTimeStamp, size); + + if (rdpsnd->device && rdpsnd->attached && !rdpsnd_detect_overrun(rdpsnd, format, size)) + { + UINT status = CHANNEL_RC_OK; + wStream* pcmData = StreamPool_Take(rdpsnd->pool, 4096); + + if (rdpsnd->device->FormatSupported(rdpsnd->device, format)) + latency = IFCALLRESULT(0, rdpsnd->device->Play, rdpsnd->device, data, size); + else if (freerdp_dsp_decode(rdpsnd->dsp_context, format, data, size, pcmData)) + { + Stream_SealLength(pcmData); + latency = IFCALLRESULT(0, rdpsnd->device->Play, rdpsnd->device, Stream_Buffer(pcmData), + Stream_Length(pcmData)); + } + else + status = ERROR_INTERNAL_ERROR; + + Stream_Release(pcmData); + + if (status != CHANNEL_RC_OK) + return status; + } + + end = GetTickCount64(); + diffMS = end - rdpsnd->wArrivalTime + latency; + ts = (rdpsnd->wTimeStamp + diffMS) % UINT16_MAX; + + /* + * Send the second WaveConfirm PDU. With the first WaveConfirm PDU, + * the server side uses this second WaveConfirm PDU to determine the actual + * render latency. + */ + return rdpsnd_send_wave_confirm_pdu(rdpsnd, (UINT16)ts, rdpsnd->cBlockNo); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpsnd_recv_wave_pdu(rdpsndPlugin* rdpsnd, wStream* s) +{ + rdpsnd->expectingWave = FALSE; + + /** + * The Wave PDU is a special case: it is always sent after a Wave Info PDU, + * and we do not process its header. Instead, the header is pad that needs + * to be filled with the first four bytes of the audio sample data sent as + * part of the preceding Wave Info PDU. + */ + if (Stream_GetRemainingLength(s) < 4) + return ERROR_INVALID_DATA; + + CopyMemory(Stream_Buffer(s), rdpsnd->waveData, 4); + return rdpsnd_treat_wave(rdpsnd, s, rdpsnd->waveDataSize); +} + +static UINT rdpsnd_recv_wave2_pdu(rdpsndPlugin* rdpsnd, wStream* s, UINT16 BodySize) +{ + UINT16 wFormatNo; + AUDIO_FORMAT* format; + UINT32 dwAudioTimeStamp; + + if (Stream_GetRemainingLength(s) < 12) + return ERROR_BAD_LENGTH; + + Stream_Read_UINT16(s, rdpsnd->wTimeStamp); + Stream_Read_UINT16(s, wFormatNo); + Stream_Read_UINT8(s, rdpsnd->cBlockNo); + Stream_Seek(s, 3); /* bPad */ + Stream_Read_UINT32(s, dwAudioTimeStamp); + if (wFormatNo >= rdpsnd->NumberOfClientFormats) + return ERROR_INVALID_DATA; + format = &rdpsnd->ClientFormats[wFormatNo]; + rdpsnd->waveDataSize = BodySize - 12; + rdpsnd->wArrivalTime = GetTickCount64(); + WLog_Print(rdpsnd->log, WLOG_DEBUG, + "%s Wave2PDU: cBlockNo: %" PRIu8 " wFormatNo: %" PRIu16 " [%s] , align=%hu", + rdpsnd_is_dyn_str(rdpsnd->dynamic), rdpsnd->cBlockNo, wFormatNo, + audio_format_get_tag_string(format->wFormatTag), format->nBlockAlign); + + if (!rdpsnd_ensure_device_is_open(rdpsnd, wFormatNo, format)) + return ERROR_INTERNAL_ERROR; + + return rdpsnd_treat_wave(rdpsnd, s, rdpsnd->waveDataSize); +} + +static void rdpsnd_recv_close_pdu(rdpsndPlugin* rdpsnd) +{ + if (rdpsnd->isOpen) + { + WLog_Print(rdpsnd->log, WLOG_DEBUG, "%s Closing device", + rdpsnd_is_dyn_str(rdpsnd->dynamic)); + } + else + WLog_Print(rdpsnd->log, WLOG_DEBUG, "%s Device already closed", + rdpsnd_is_dyn_str(rdpsnd->dynamic)); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpsnd_recv_volume_pdu(rdpsndPlugin* rdpsnd, wStream* s) +{ + BOOL rc = FALSE; + UINT32 dwVolume; + + if (Stream_GetRemainingLength(s) < 4) + return ERROR_BAD_LENGTH; + + Stream_Read_UINT32(s, dwVolume); + WLog_Print(rdpsnd->log, WLOG_DEBUG, "%s Volume: 0x%08" PRIX32 "", + rdpsnd_is_dyn_str(rdpsnd->dynamic), dwVolume); + if (rdpsnd->device) + rc = IFCALLRESULT(FALSE, rdpsnd->device->SetVolume, rdpsnd->device, dwVolume); + + if (!rc) + { + WLog_ERR(TAG, "%s error setting volume", rdpsnd_is_dyn_str(rdpsnd->dynamic)); + return CHANNEL_RC_INITIALIZATION_ERROR; + } + + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpsnd_recv_pdu(rdpsndPlugin* rdpsnd, wStream* s) +{ + BYTE msgType; + UINT16 BodySize; + UINT status = CHANNEL_RC_OK; + + if (rdpsnd->expectingWave) + { + status = rdpsnd_recv_wave_pdu(rdpsnd, s); + goto out; + } + + if (Stream_GetRemainingLength(s) < 4) + { + status = ERROR_BAD_LENGTH; + goto out; + } + + Stream_Read_UINT8(s, msgType); /* msgType */ + Stream_Seek_UINT8(s); /* bPad */ + Stream_Read_UINT16(s, BodySize); + + switch (msgType) + { + case SNDC_FORMATS: + status = rdpsnd_recv_server_audio_formats_pdu(rdpsnd, s); + break; + + case SNDC_TRAINING: + status = rdpsnd_recv_training_pdu(rdpsnd, s); + break; + + case SNDC_WAVE: + status = rdpsnd_recv_wave_info_pdu(rdpsnd, s, BodySize); + break; + + case SNDC_CLOSE: + rdpsnd_recv_close_pdu(rdpsnd); + break; + + case SNDC_SETVOLUME: + status = rdpsnd_recv_volume_pdu(rdpsnd, s); + break; + + case SNDC_WAVE2: + status = rdpsnd_recv_wave2_pdu(rdpsnd, s, BodySize); + break; + + default: + WLog_ERR(TAG, "%s unknown msgType %" PRIu8 "", rdpsnd_is_dyn_str(rdpsnd->dynamic), + msgType); + break; + } + +out: + Stream_Release(s); + return status; +} + +static void rdpsnd_register_device_plugin(rdpsndPlugin* rdpsnd, rdpsndDevicePlugin* device) +{ + if (rdpsnd->device) + { + WLog_ERR(TAG, "%s existing device, abort.", rdpsnd_is_dyn_str(FALSE)); + return; + } + + rdpsnd->device = device; + device->rdpsnd = rdpsnd; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpsnd_load_device_plugin(rdpsndPlugin* rdpsnd, const char* name, ADDIN_ARGV* args) +{ + PFREERDP_RDPSND_DEVICE_ENTRY entry; + FREERDP_RDPSND_DEVICE_ENTRY_POINTS entryPoints; + UINT error; + DWORD flags = FREERDP_ADDIN_CHANNEL_STATIC | FREERDP_ADDIN_CHANNEL_ENTRYEX; + if (rdpsnd->dynamic) + flags = FREERDP_ADDIN_CHANNEL_DYNAMIC; + entry = + (PFREERDP_RDPSND_DEVICE_ENTRY)freerdp_load_channel_addin_entry("rdpsnd", name, NULL, flags); + + if (!entry) + return ERROR_INTERNAL_ERROR; + + entryPoints.rdpsnd = rdpsnd; + entryPoints.pRegisterRdpsndDevice = rdpsnd_register_device_plugin; + entryPoints.args = args; + + if ((error = entry(&entryPoints))) + WLog_ERR(TAG, "%s %s entry returns error %" PRIu32 "", rdpsnd_is_dyn_str(rdpsnd->dynamic), + name, error); + + WLog_INFO(TAG, "%s Loaded %s backend for rdpsnd", rdpsnd_is_dyn_str(rdpsnd->dynamic), name); + return error; +} + +static BOOL rdpsnd_set_subsystem(rdpsndPlugin* rdpsnd, const char* subsystem) +{ + free(rdpsnd->subsystem); + rdpsnd->subsystem = _strdup(subsystem); + return (rdpsnd->subsystem != NULL); +} + +static BOOL rdpsnd_set_device_name(rdpsndPlugin* rdpsnd, const char* device_name) +{ + free(rdpsnd->device_name); + rdpsnd->device_name = _strdup(device_name); + return (rdpsnd->device_name != NULL); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpsnd_process_addin_args(rdpsndPlugin* rdpsnd, ADDIN_ARGV* args) +{ + int status; + DWORD flags; + COMMAND_LINE_ARGUMENT_A* arg; + COMMAND_LINE_ARGUMENT_A rdpsnd_args[] = { + { "sys", COMMAND_LINE_VALUE_REQUIRED, "", NULL, NULL, -1, NULL, "subsystem" }, + { "dev", COMMAND_LINE_VALUE_REQUIRED, "", NULL, NULL, -1, NULL, "device" }, + { "format", COMMAND_LINE_VALUE_REQUIRED, "", NULL, NULL, -1, NULL, "format" }, + { "rate", COMMAND_LINE_VALUE_REQUIRED, "", NULL, NULL, -1, NULL, "rate" }, + { "channel", COMMAND_LINE_VALUE_REQUIRED, "", NULL, NULL, -1, NULL, "channel" }, + { "latency", COMMAND_LINE_VALUE_REQUIRED, "", NULL, NULL, -1, NULL, "latency" }, + { "quality", COMMAND_LINE_VALUE_REQUIRED, "", NULL, NULL, -1, NULL, + "quality mode" }, + { NULL, 0, NULL, NULL, NULL, -1, NULL, NULL } + }; + rdpsnd->wQualityMode = HIGH_QUALITY; /* default quality mode */ + + if (args->argc > 1) + { + flags = COMMAND_LINE_SIGIL_NONE | COMMAND_LINE_SEPARATOR_COLON; + status = CommandLineParseArgumentsA(args->argc, args->argv, rdpsnd_args, flags, rdpsnd, + NULL, NULL); + + if (status < 0) + return CHANNEL_RC_INITIALIZATION_ERROR; + + arg = rdpsnd_args; + errno = 0; + + do + { + if (!(arg->Flags & COMMAND_LINE_VALUE_PRESENT)) + continue; + + CommandLineSwitchStart(arg) CommandLineSwitchCase(arg, "sys") + { + if (!rdpsnd_set_subsystem(rdpsnd, arg->Value)) + return CHANNEL_RC_NO_MEMORY; + } + CommandLineSwitchCase(arg, "dev") + { + if (!rdpsnd_set_device_name(rdpsnd, arg->Value)) + return CHANNEL_RC_NO_MEMORY; + } + CommandLineSwitchCase(arg, "format") + { + unsigned long val = strtoul(arg->Value, NULL, 0); + + if ((errno != 0) || (val > UINT16_MAX)) + return CHANNEL_RC_INITIALIZATION_ERROR; + + rdpsnd->fixed_format->wFormatTag = (UINT16)val; + } + CommandLineSwitchCase(arg, "rate") + { + unsigned long val = strtoul(arg->Value, NULL, 0); + + if ((errno != 0) || (val > UINT32_MAX)) + return CHANNEL_RC_INITIALIZATION_ERROR; + + rdpsnd->fixed_format->nSamplesPerSec = val; + } + CommandLineSwitchCase(arg, "channel") + { + unsigned long val = strtoul(arg->Value, NULL, 0); + + if ((errno != 0) || (val > UINT16_MAX)) + return CHANNEL_RC_INITIALIZATION_ERROR; + + rdpsnd->fixed_format->nChannels = (UINT16)val; + } + CommandLineSwitchCase(arg, "latency") + { + unsigned long val = strtoul(arg->Value, NULL, 0); + + if ((errno != 0) || (val > INT32_MAX)) + return CHANNEL_RC_INITIALIZATION_ERROR; + + rdpsnd->latency = val; + } + CommandLineSwitchCase(arg, "quality") + { + long wQualityMode = DYNAMIC_QUALITY; + + if (_stricmp(arg->Value, "dynamic") == 0) + wQualityMode = DYNAMIC_QUALITY; + else if (_stricmp(arg->Value, "medium") == 0) + wQualityMode = MEDIUM_QUALITY; + else if (_stricmp(arg->Value, "high") == 0) + wQualityMode = HIGH_QUALITY; + else + { + wQualityMode = strtol(arg->Value, NULL, 0); + + if (errno != 0) + return CHANNEL_RC_INITIALIZATION_ERROR; + } + + if ((wQualityMode < 0) || (wQualityMode > 2)) + wQualityMode = DYNAMIC_QUALITY; + + rdpsnd->wQualityMode = (UINT16)wQualityMode; + } + CommandLineSwitchDefault(arg) + { + } + CommandLineSwitchEnd(arg) + } while ((arg = CommandLineFindNextArgumentA(arg)) != NULL); + } + + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpsnd_process_connect(rdpsndPlugin* rdpsnd) +{ + const struct + { + const char* subsystem; + const char* device; + } backends[] = { +#if defined(WITH_IOSAUDIO) + { "ios", "" }, +#endif +#if defined(WITH_OPENSLES) + { "opensles", "" }, +#endif +#if defined(WITH_PULSE) + { "pulse", "" }, +#endif +#if defined(WITH_ALSA) + { "alsa", "default" }, +#endif +#if defined(WITH_OSS) + { "oss", "" }, +#endif +#if defined(WITH_MACAUDIO) + { "mac", "default" }, +#endif +#if defined(WITH_WINMM) + { "winmm", "" }, +#endif + { "fake", "" } + }; + ADDIN_ARGV* args; + UINT status = ERROR_INTERNAL_ERROR; + rdpsnd->latency = 0; + args = (ADDIN_ARGV*)rdpsnd->channelEntryPoints.pExtendedData; + + if (args) + { + status = rdpsnd_process_addin_args(rdpsnd, args); + + if (status != CHANNEL_RC_OK) + return status; + } + + if (rdpsnd->subsystem) + { + if ((status = rdpsnd_load_device_plugin(rdpsnd, rdpsnd->subsystem, args))) + { + WLog_ERR(TAG, + "%s Unable to load sound playback subsystem %s because of error %" PRIu32 "", + rdpsnd_is_dyn_str(rdpsnd->dynamic), rdpsnd->subsystem, status); + return status; + } + } + else + { + size_t x; + + for (x = 0; x < ARRAYSIZE(backends); x++) + { + const char* subsystem_name = backends[x].subsystem; + const char* device_name = backends[x].device; + + if ((status = rdpsnd_load_device_plugin(rdpsnd, subsystem_name, args))) + WLog_ERR(TAG, + "%s Unable to load sound playback subsystem %s because of error %" PRIu32 + "", + rdpsnd_is_dyn_str(rdpsnd->dynamic), subsystem_name, status); + + if (!rdpsnd->device) + continue; + + if (!rdpsnd_set_subsystem(rdpsnd, subsystem_name) || + !rdpsnd_set_device_name(rdpsnd, device_name)) + return CHANNEL_RC_NO_MEMORY; + + break; + } + + if (!rdpsnd->device || status) + return CHANNEL_RC_INITIALIZATION_ERROR; + } + + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT rdpsnd_virtual_channel_write(rdpsndPlugin* rdpsnd, wStream* s) +{ + UINT status = CHANNEL_RC_BAD_INIT_HANDLE; + + if (rdpsnd) + { + if (rdpsnd->dynamic) + { + IWTSVirtualChannel* channel; + if (rdpsnd->listener_callback) + { + channel = rdpsnd->listener_callback->channel_callback->channel; + status = channel->Write(channel, (UINT32)Stream_Length(s), Stream_Buffer(s), NULL); + } + Stream_Free(s, TRUE); + } + else + { + status = rdpsnd->channelEntryPoints.pVirtualChannelWriteEx( + rdpsnd->InitHandle, rdpsnd->OpenHandle, Stream_Buffer(s), + (UINT32)Stream_GetPosition(s), s); + + if (status != CHANNEL_RC_OK) + { + Stream_Free(s, TRUE); + WLog_ERR(TAG, "%s pVirtualChannelWriteEx failed with %s [%08" PRIX32 "]", + rdpsnd_is_dyn_str(FALSE), WTSErrorToString(status), status); + } + } + } + + return status; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpsnd_virtual_channel_event_data_received(rdpsndPlugin* plugin, void* pData, + UINT32 dataLength, UINT32 totalLength, + UINT32 dataFlags) +{ + if ((dataFlags & CHANNEL_FLAG_SUSPEND) || (dataFlags & CHANNEL_FLAG_RESUME)) + return CHANNEL_RC_OK; + + if (dataFlags & CHANNEL_FLAG_FIRST) + { + if (!plugin->data_in) + plugin->data_in = StreamPool_Take(plugin->pool, totalLength); + + Stream_SetPosition(plugin->data_in, 0); + } + + if (!Stream_EnsureRemainingCapacity(plugin->data_in, dataLength)) + return CHANNEL_RC_NO_MEMORY; + + Stream_Write(plugin->data_in, pData, dataLength); + + if (dataFlags & CHANNEL_FLAG_LAST) + { + Stream_SealLength(plugin->data_in); + Stream_SetPosition(plugin->data_in, 0); + + if (!MessageQueue_Post(plugin->queue, NULL, 0, plugin->data_in, NULL)) + return ERROR_INTERNAL_ERROR; + + plugin->data_in = NULL; + } + + return CHANNEL_RC_OK; +} + +static VOID VCAPITYPE rdpsnd_virtual_channel_open_event_ex(LPVOID lpUserParam, DWORD openHandle, + UINT event, LPVOID pData, + UINT32 dataLength, UINT32 totalLength, + UINT32 dataFlags) +{ + UINT error = CHANNEL_RC_OK; + rdpsndPlugin* rdpsnd = (rdpsndPlugin*)lpUserParam; + + switch (event) + { + case CHANNEL_EVENT_DATA_RECEIVED: + if (!rdpsnd) + return; + + if (rdpsnd->OpenHandle != openHandle) + { + WLog_ERR(TAG, "%s error no match", rdpsnd_is_dyn_str(rdpsnd->dynamic)); + return; + } + if ((error = rdpsnd_virtual_channel_event_data_received(rdpsnd, pData, dataLength, + totalLength, dataFlags))) + WLog_ERR(TAG, + "%s rdpsnd_virtual_channel_event_data_received failed with error %" PRIu32 + "", + rdpsnd_is_dyn_str(rdpsnd->dynamic), error); + + break; + + case CHANNEL_EVENT_WRITE_CANCELLED: + case CHANNEL_EVENT_WRITE_COMPLETE: + { + wStream* s = (wStream*)pData; + Stream_Free(s, TRUE); + } + break; + + case CHANNEL_EVENT_USER: + break; + } + + if (error && rdpsnd && rdpsnd->rdpcontext) + { + char buffer[8192]; + _snprintf(buffer, sizeof(buffer), + "%s rdpsnd_virtual_channel_open_event_ex reported an error", + rdpsnd_is_dyn_str(rdpsnd->dynamic)); + setChannelError(rdpsnd->rdpcontext, error, buffer); + } +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpsnd_virtual_channel_event_connected(rdpsndPlugin* rdpsnd, LPVOID pData, + UINT32 dataLength) +{ + UINT32 status; + DWORD opened = 0; + WINPR_UNUSED(pData); + WINPR_UNUSED(dataLength); + + status = rdpsnd->channelEntryPoints.pVirtualChannelOpenEx( + rdpsnd->InitHandle, &opened, rdpsnd->channelDef.name, rdpsnd_virtual_channel_open_event_ex); + + if (status != CHANNEL_RC_OK) + { + WLog_ERR(TAG, "%s pVirtualChannelOpenEx failed with %s [%08" PRIX32 "]", + rdpsnd_is_dyn_str(rdpsnd->dynamic), WTSErrorToString(status), status); + goto fail; + } + + if (rdpsnd_process_connect(rdpsnd) != CHANNEL_RC_OK) + goto fail; + + rdpsnd->OpenHandle = opened; + return CHANNEL_RC_OK; +fail: + if (opened != 0) + rdpsnd->channelEntryPoints.pVirtualChannelCloseEx(rdpsnd->InitHandle, opened); + + return CHANNEL_RC_NO_MEMORY; +} + +static void cleanup_internals(rdpsndPlugin* rdpsnd) +{ + if (!rdpsnd) + return; + + if (rdpsnd->pool) + StreamPool_Return(rdpsnd->pool, rdpsnd->data_in); + + audio_formats_free(rdpsnd->ClientFormats, rdpsnd->NumberOfClientFormats); + audio_formats_free(rdpsnd->ServerFormats, rdpsnd->NumberOfServerFormats); + + rdpsnd->NumberOfClientFormats = 0; + rdpsnd->ClientFormats = NULL; + rdpsnd->NumberOfServerFormats = 0; + rdpsnd->ServerFormats = NULL; + + rdpsnd->data_in = NULL; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpsnd_virtual_channel_event_disconnected(rdpsndPlugin* rdpsnd) +{ + UINT error; + + if (rdpsnd->OpenHandle != 0) + { + DWORD opened = rdpsnd->OpenHandle; + rdpsnd->OpenHandle = 0; + + if (rdpsnd->device) + IFCALL(rdpsnd->device->Close, rdpsnd->device); + + error = rdpsnd->channelEntryPoints.pVirtualChannelCloseEx(rdpsnd->InitHandle, opened); + + if (CHANNEL_RC_OK != error) + { + WLog_ERR(TAG, "%s pVirtualChannelCloseEx failed with %s [%08" PRIX32 "]", + rdpsnd_is_dyn_str(rdpsnd->dynamic), WTSErrorToString(error), error); + return error; + } + } + + cleanup_internals(rdpsnd); + + if (rdpsnd->device) + { + IFCALL(rdpsnd->device->Free, rdpsnd->device); + rdpsnd->device = NULL; + } + + return CHANNEL_RC_OK; +} + +static void _queue_free(void* obj) +{ + wStream* s = obj; + Stream_Release(s); +} + +static void free_internals(rdpsndPlugin* rdpsnd) +{ + if (!rdpsnd) + return; + + freerdp_dsp_context_free(rdpsnd->dsp_context); + StreamPool_Free(rdpsnd->pool); + rdpsnd->pool = NULL; + rdpsnd->dsp_context = NULL; +} + +static BOOL allocate_internals(rdpsndPlugin* rdpsnd) +{ + if (!rdpsnd->pool) + { + rdpsnd->pool = StreamPool_New(TRUE, 4096); + if (!rdpsnd->pool) + return FALSE; + } + + if (!rdpsnd->dsp_context) + { + rdpsnd->dsp_context = freerdp_dsp_context_new(FALSE); + if (!rdpsnd->dsp_context) + return FALSE; + } + + return TRUE; +} + +static DWORD WINAPI play_thread(LPVOID arg) +{ + UINT error = CHANNEL_RC_OK; + rdpsndPlugin* rdpsnd = arg; + + if (!rdpsnd || !rdpsnd->queue) + return ERROR_INVALID_PARAMETER; + + while (TRUE) + { + int rc; + wMessage message; + wStream* s; + HANDLE handle = MessageQueue_Event(rdpsnd->queue); + WaitForSingleObject(handle, INFINITE); + + rc = MessageQueue_Peek(rdpsnd->queue, &message, TRUE); + if (rc < 1) + continue; + + if (message.id == WMQ_QUIT) + break; + + s = message.wParam; + error = rdpsnd_recv_pdu(rdpsnd, s); + + if (error) + return error; + } + + return CHANNEL_RC_OK; +} + +static UINT rdpsnd_virtual_channel_event_initialized(rdpsndPlugin* rdpsnd) +{ + wObject obj = { 0 }; + + if (!rdpsnd) + return ERROR_INVALID_PARAMETER; + + obj.fnObjectFree = _queue_free; + rdpsnd->queue = MessageQueue_New(&obj); + if (!rdpsnd->queue) + return CHANNEL_RC_NO_MEMORY; + + if (!allocate_internals(rdpsnd)) + return CHANNEL_RC_NO_MEMORY; + + rdpsnd->thread = CreateThread(NULL, 0, play_thread, rdpsnd, 0, NULL); + if (!rdpsnd->thread) + return CHANNEL_RC_INITIALIZATION_ERROR; + + return CHANNEL_RC_OK; +} + +void rdpsnd_virtual_channel_event_terminated(rdpsndPlugin* rdpsnd) +{ + if (rdpsnd) + { + if (rdpsnd->queue) + MessageQueue_PostQuit(rdpsnd->queue, 0); + if (rdpsnd->thread) + { + WaitForSingleObject(rdpsnd->thread, INFINITE); + CloseHandle(rdpsnd->thread); + } + MessageQueue_Free(rdpsnd->queue); + + free_internals(rdpsnd); + audio_formats_free(rdpsnd->fixed_format, 1); + free(rdpsnd->subsystem); + free(rdpsnd->device_name); + rdpsnd->InitHandle = 0; + } + + free(rdpsnd); +} + +static VOID VCAPITYPE rdpsnd_virtual_channel_init_event_ex(LPVOID lpUserParam, LPVOID pInitHandle, + UINT event, LPVOID pData, + UINT dataLength) +{ + UINT error = CHANNEL_RC_OK; + rdpsndPlugin* plugin = (rdpsndPlugin*)lpUserParam; + + if (!plugin) + return; + + if (plugin->InitHandle != pInitHandle) + { + WLog_ERR(TAG, "%s error no match", rdpsnd_is_dyn_str(plugin->dynamic)); + return; + } + + switch (event) + { + case CHANNEL_EVENT_INITIALIZED: + error = rdpsnd_virtual_channel_event_initialized(plugin); + break; + + case CHANNEL_EVENT_CONNECTED: + error = rdpsnd_virtual_channel_event_connected(plugin, pData, dataLength); + break; + + case CHANNEL_EVENT_DISCONNECTED: + error = rdpsnd_virtual_channel_event_disconnected(plugin); + break; + + case CHANNEL_EVENT_TERMINATED: + rdpsnd_virtual_channel_event_terminated(plugin); + plugin = NULL; + break; + + case CHANNEL_EVENT_ATTACHED: + plugin->attached = TRUE; + break; + + case CHANNEL_EVENT_DETACHED: + plugin->attached = FALSE; + break; + + default: + break; + } + + if (error && plugin && plugin->rdpcontext) + { + char buffer[8192]; + _snprintf(buffer, sizeof(buffer), "%s %s reported an error", + rdpsnd_is_dyn_str(plugin->dynamic), __FUNCTION__); + setChannelError(plugin->rdpcontext, error, buffer); + } +} + +rdpContext* freerdp_rdpsnd_get_context(rdpsndPlugin* plugin) +{ + if (!plugin) + return NULL; + + return plugin->rdpcontext; +} + +static rdpsndPlugin* allocatePlugin(void) +{ + rdpsndPlugin* rdpsnd = (rdpsndPlugin*)calloc(1, sizeof(rdpsndPlugin)); + if (!rdpsnd) + goto fail; + + rdpsnd->fixed_format = audio_format_new(); + if (!rdpsnd->fixed_format) + goto fail; + rdpsnd->log = WLog_Get("com.freerdp.channels.rdpsnd.client"); + if (!rdpsnd->log) + goto fail; + + rdpsnd->attached = TRUE; + return rdpsnd; + +fail: + if (rdpsnd) + audio_format_free(rdpsnd->fixed_format); + return NULL; +} +/* rdpsnd is always built-in */ +BOOL VCAPITYPE rdpsnd_VirtualChannelEntryEx(PCHANNEL_ENTRY_POINTS pEntryPoints, PVOID pInitHandle) +{ + UINT rc; + rdpsndPlugin* rdpsnd; + CHANNEL_ENTRY_POINTS_FREERDP_EX* pEntryPointsEx; + + if (!pEntryPoints) + return FALSE; + + rdpsnd = allocatePlugin(); + + if (!rdpsnd) + return FALSE; + + rdpsnd->channelDef.options = CHANNEL_OPTION_INITIALIZED | CHANNEL_OPTION_ENCRYPT_RDP; + sprintf_s(rdpsnd->channelDef.name, ARRAYSIZE(rdpsnd->channelDef.name), "rdpsnd"); + pEntryPointsEx = (CHANNEL_ENTRY_POINTS_FREERDP_EX*)pEntryPoints; + + if ((pEntryPointsEx->cbSize >= sizeof(CHANNEL_ENTRY_POINTS_FREERDP_EX)) && + (pEntryPointsEx->MagicNumber == FREERDP_CHANNEL_MAGIC_NUMBER)) + { + rdpsnd->rdpcontext = pEntryPointsEx->context; + } + + CopyMemory(&(rdpsnd->channelEntryPoints), pEntryPoints, + sizeof(CHANNEL_ENTRY_POINTS_FREERDP_EX)); + rdpsnd->InitHandle = pInitHandle; + rc = rdpsnd->channelEntryPoints.pVirtualChannelInitEx( + rdpsnd, NULL, pInitHandle, &rdpsnd->channelDef, 1, VIRTUAL_CHANNEL_VERSION_WIN2000, + rdpsnd_virtual_channel_init_event_ex); + + if (CHANNEL_RC_OK != rc) + { + WLog_ERR(TAG, "%s pVirtualChannelInitEx failed with %s [%08" PRIX32 "]", + rdpsnd_is_dyn_str(FALSE), WTSErrorToString(rc), rc); + rdpsnd_virtual_channel_event_terminated(rdpsnd); + return FALSE; + } + + return TRUE; +} + +static UINT rdpsnd_on_open(IWTSVirtualChannelCallback* pChannelCallback) +{ + RDPSND_CHANNEL_CALLBACK* callback = (RDPSND_CHANNEL_CALLBACK*)pChannelCallback; + rdpsndPlugin* rdpsnd = (rdpsndPlugin*)callback->plugin; + + if (!allocate_internals(rdpsnd)) + return ERROR_OUTOFMEMORY; + + return rdpsnd_process_connect(rdpsnd); +} + +static UINT rdpsnd_on_data_received(IWTSVirtualChannelCallback* pChannelCallback, wStream* data) +{ + RDPSND_CHANNEL_CALLBACK* callback = (RDPSND_CHANNEL_CALLBACK*)pChannelCallback; + rdpsndPlugin* plugin; + wStream* copy; + size_t len; + + len = Stream_GetRemainingLength(data); + + if (!callback || !callback->plugin) + return ERROR_INVALID_PARAMETER; + plugin = (rdpsndPlugin*)callback->plugin; + + copy = StreamPool_Take(plugin->pool, len); + if (!copy) + return ERROR_OUTOFMEMORY; + Stream_Copy(data, copy, len); + Stream_SealLength(copy); + Stream_SetPosition(copy, 0); + + if (!MessageQueue_Post(plugin->queue, NULL, 0, copy, NULL)) + { + Stream_Release(copy); + return ERROR_INTERNAL_ERROR; + } + + return CHANNEL_RC_OK; +} + +static UINT rdpsnd_on_close(IWTSVirtualChannelCallback* pChannelCallback) +{ + RDPSND_CHANNEL_CALLBACK* callback = (RDPSND_CHANNEL_CALLBACK*)pChannelCallback; + rdpsndPlugin* rdpsnd = (rdpsndPlugin*)callback->plugin; + + if (rdpsnd->device) + IFCALL(rdpsnd->device->Close, rdpsnd->device); + + cleanup_internals(rdpsnd); + + if (rdpsnd->device) + { + IFCALL(rdpsnd->device->Free, rdpsnd->device); + rdpsnd->device = NULL; + } + + free_internals(rdpsnd); + free(pChannelCallback); + return CHANNEL_RC_OK; +} + +static UINT rdpsnd_on_new_channel_connection(IWTSListenerCallback* pListenerCallback, + IWTSVirtualChannel* pChannel, BYTE* Data, + BOOL* pbAccept, + IWTSVirtualChannelCallback** ppCallback) +{ + RDPSND_CHANNEL_CALLBACK* callback; + RDPSND_LISTENER_CALLBACK* listener_callback = (RDPSND_LISTENER_CALLBACK*)pListenerCallback; + callback = (RDPSND_CHANNEL_CALLBACK*)calloc(1, sizeof(RDPSND_CHANNEL_CALLBACK)); + + WINPR_UNUSED(Data); + WINPR_UNUSED(pbAccept); + + if (!callback) + { + WLog_ERR(TAG, "%s calloc failed!", rdpsnd_is_dyn_str(TRUE)); + return CHANNEL_RC_NO_MEMORY; + } + + callback->iface.OnOpen = rdpsnd_on_open; + callback->iface.OnDataReceived = rdpsnd_on_data_received; + callback->iface.OnClose = rdpsnd_on_close; + callback->plugin = listener_callback->plugin; + callback->channel_mgr = listener_callback->channel_mgr; + callback->channel = pChannel; + listener_callback->channel_callback = callback; + *ppCallback = (IWTSVirtualChannelCallback*)callback; + return CHANNEL_RC_OK; +} + +static UINT rdpsnd_plugin_initialize(IWTSPlugin* pPlugin, IWTSVirtualChannelManager* pChannelMgr) +{ + UINT status; + rdpsndPlugin* rdpsnd = (rdpsndPlugin*)pPlugin; + if (rdpsnd->initialized) + { + WLog_ERR(TAG, "[%s] channel initialized twice, aborting", RDPSND_DVC_CHANNEL_NAME); + return ERROR_INVALID_DATA; + } + rdpsnd->listener_callback = + (RDPSND_LISTENER_CALLBACK*)calloc(1, sizeof(RDPSND_LISTENER_CALLBACK)); + + if (!rdpsnd->listener_callback) + { + WLog_ERR(TAG, "%s calloc failed!", rdpsnd_is_dyn_str(TRUE)); + return CHANNEL_RC_NO_MEMORY; + } + + rdpsnd->listener_callback->iface.OnNewChannelConnection = rdpsnd_on_new_channel_connection; + rdpsnd->listener_callback->plugin = pPlugin; + rdpsnd->listener_callback->channel_mgr = pChannelMgr; + status = pChannelMgr->CreateListener(pChannelMgr, RDPSND_DVC_CHANNEL_NAME, 0, + &rdpsnd->listener_callback->iface, &(rdpsnd->listener)); + rdpsnd->listener->pInterface = rdpsnd->iface.pInterface; + status = rdpsnd_virtual_channel_event_initialized(rdpsnd); + + rdpsnd->initialized = status == CHANNEL_RC_OK; + return status; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpsnd_plugin_terminated(IWTSPlugin* pPlugin) +{ + rdpsndPlugin* rdpsnd = (rdpsndPlugin*)pPlugin; + if (rdpsnd) + { + if (rdpsnd->listener_callback) + { + IWTSVirtualChannelManager* mgr = rdpsnd->listener_callback->channel_mgr; + if (mgr) + IFCALL(mgr->DestroyListener, mgr, rdpsnd->listener); + } + free(rdpsnd->listener_callback); + free(rdpsnd->iface.pInterface); + } + rdpsnd_virtual_channel_event_terminated(rdpsnd); + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT rdpsnd_DVCPluginEntry(IDRDYNVC_ENTRY_POINTS* pEntryPoints) +{ + UINT error = CHANNEL_RC_OK; + rdpsndPlugin* rdpsnd = (rdpsndPlugin*)pEntryPoints->GetPlugin(pEntryPoints, "rdpsnd"); + + if (!rdpsnd) + { + rdpsnd = allocatePlugin(); + if (!rdpsnd) + { + WLog_ERR(TAG, "%s calloc failed!", rdpsnd_is_dyn_str(TRUE)); + return CHANNEL_RC_NO_MEMORY; + } + + rdpsnd->iface.Initialize = rdpsnd_plugin_initialize; + rdpsnd->iface.Connected = NULL; + rdpsnd->iface.Disconnected = NULL; + rdpsnd->iface.Terminated = rdpsnd_plugin_terminated; + rdpsnd->dynamic = TRUE; + + /* user data pointer is not const, cast to avoid warning. */ + rdpsnd->channelEntryPoints.pExtendedData = (void*)pEntryPoints->GetPluginData(pEntryPoints); + + error = pEntryPoints->RegisterPlugin(pEntryPoints, "rdpsnd", &rdpsnd->iface); + } + else + { + WLog_ERR(TAG, "%s could not get rdpsnd Plugin.", rdpsnd_is_dyn_str(TRUE)); + return CHANNEL_RC_BAD_CHANNEL; + } + + return error; +} diff --git a/channels/rdpsnd/client/rdpsnd_main.h b/channels/rdpsnd/client/rdpsnd_main.h new file mode 100644 index 0000000..b3b478f --- /dev/null +++ b/channels/rdpsnd/client/rdpsnd_main.h @@ -0,0 +1,43 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Audio Output Virtual Channel + * + * Copyright 2010-2011 Vic Lee + * Copyright 2012-2013 Marc-Andre Moreau + * + * Licensed 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 FREERDP_CHANNEL_RDPSND_CLIENT_MAIN_H +#define FREERDP_CHANNEL_RDPSND_CLIENT_MAIN_H + +#include +#include +#include +#include +#include + +#define TAG CHANNELS_TAG("rdpsnd.client") + +FREERDP_API rdpContext* freerdp_rdpsnd_get_context(rdpsndPlugin* rdpsnd); + +#if defined(WITH_DEBUG_SND) +#define DEBUG_SND(...) WLog_DBG(TAG, __VA_ARGS__) +#else +#define DEBUG_SND(...) \ + do \ + { \ + } while (0) +#endif + +#endif /* FREERDP_CHANNEL_RDPSND_CLIENT_MAIN_H */ diff --git a/channels/rdpsnd/client/winmm/CMakeLists.txt b/channels/rdpsnd/client/winmm/CMakeLists.txt new file mode 100644 index 0000000..b4a337d --- /dev/null +++ b/channels/rdpsnd/client/winmm/CMakeLists.txt @@ -0,0 +1,39 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2012 Marc-Andre Moreau +# +# Licensed 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. + +define_channel_client_subsystem("rdpsnd" "winmm" "") + +set(${MODULE_PREFIX}_SRCS + rdpsnd_winmm.c) + +include_directories(..) + +add_channel_client_subsystem_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} "" TRUE "") + + + +set(${MODULE_PREFIX}_LIBS ${${MODULE_PREFIX}_LIBS} winpr) +set(${MODULE_PREFIX}_LIBS ${${MODULE_PREFIX}_LIBS} winmm.lib) +set(${MODULE_PREFIX}_LIBS ${${MODULE_PREFIX}_LIBS} freerdp) + +target_link_libraries(${MODULE_NAME} ${${MODULE_PREFIX}_LIBS}) + +if (WITH_DEBUG_SYMBOLS AND MSVC AND NOT BUILTIN_CHANNELS AND BUILD_SHARED_LIBS) + install(FILES ${CMAKE_PDB_BINARY_DIR}/${MODULE_NAME}.pdb DESTINATION ${FREERDP_ADDIN_PATH} COMPONENT symbols) +endif() + +set_property(TARGET ${MODULE_NAME} PROPERTY FOLDER "Channels/${CHANNEL_NAME}/Client/WinMM") diff --git a/channels/rdpsnd/client/winmm/rdpsnd_winmm.c b/channels/rdpsnd/client/winmm/rdpsnd_winmm.c new file mode 100644 index 0000000..0c51f77 --- /dev/null +++ b/channels/rdpsnd/client/winmm/rdpsnd_winmm.c @@ -0,0 +1,356 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Audio Output Virtual Channel + * + * Copyright 2009-2012 Jay Sorg + * Copyright 2010-2012 Vic Lee + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger + * Copyright 2016 David PHAM-VAN + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include + +#include +#include + +#include +#include +#include + +#include +#include + +#include "rdpsnd_main.h" + +typedef struct rdpsnd_winmm_plugin rdpsndWinmmPlugin; + +struct rdpsnd_winmm_plugin +{ + rdpsndDevicePlugin device; + + HWAVEOUT hWaveOut; + WAVEFORMATEX format; + UINT32 volume; + wLog* log; + UINT32 latency; + HANDLE hThread; + DWORD threadId; + CRITICAL_SECTION cs; +}; + +static BOOL rdpsnd_winmm_convert_format(const AUDIO_FORMAT* in, WAVEFORMATEX* out) +{ + if (!in || !out) + return FALSE; + + ZeroMemory(out, sizeof(WAVEFORMATEX)); + out->wFormatTag = WAVE_FORMAT_PCM; + out->nChannels = in->nChannels; + out->nSamplesPerSec = in->nSamplesPerSec; + + switch (in->wFormatTag) + { + case WAVE_FORMAT_PCM: + out->wBitsPerSample = in->wBitsPerSample; + break; + + default: + return FALSE; + } + + out->nBlockAlign = out->nChannels * out->wBitsPerSample / 8; + out->nAvgBytesPerSec = out->nSamplesPerSec * out->nBlockAlign; + return TRUE; +} + +static BOOL rdpsnd_winmm_set_format(rdpsndDevicePlugin* device, const AUDIO_FORMAT* format, + UINT32 latency) +{ + rdpsndWinmmPlugin* winmm = (rdpsndWinmmPlugin*)device; + + winmm->latency = latency; + if (!rdpsnd_winmm_convert_format(format, &winmm->format)) + return FALSE; + + return TRUE; +} + +static DWORD WINAPI waveOutProc(LPVOID lpParameter) +{ + MSG msg; + rdpsndWinmmPlugin* winmm = (rdpsndWinmmPlugin*)lpParameter; + while (GetMessage(&msg, NULL, 0, 0)) + { + if (msg.message == MM_WOM_CLOSE) + { + /* device was closed - exit thread */ + break; + } + else if (msg.message == MM_WOM_DONE) + { + /* free buffer */ + LPWAVEHDR waveHdr = (LPWAVEHDR)msg.lParam; + EnterCriticalSection(&winmm->cs); + waveOutUnprepareHeader((HWAVEOUT)msg.wParam, waveHdr, sizeof(WAVEHDR)); + LeaveCriticalSection(&winmm->cs); + free(waveHdr->lpData); + free(waveHdr); + } + } + + return 0; +} + +static BOOL rdpsnd_winmm_open(rdpsndDevicePlugin* device, const AUDIO_FORMAT* format, + UINT32 latency) +{ + MMRESULT mmResult; + rdpsndWinmmPlugin* winmm = (rdpsndWinmmPlugin*)device; + + if (winmm->hWaveOut) + return TRUE; + + if (!rdpsnd_winmm_set_format(device, format, latency)) + return FALSE; + + winmm->hThread = CreateThread(NULL, 0, waveOutProc, winmm, 0, &winmm->threadId); + if (!winmm->hThread) + { + WLog_Print(winmm->log, WLOG_ERROR, "CreateThread failed: %" PRIu32 "", GetLastError()); + return FALSE; + } + + mmResult = waveOutOpen(&winmm->hWaveOut, WAVE_MAPPER, &winmm->format, + (DWORD_PTR)winmm->threadId, 0, CALLBACK_THREAD); + + if (mmResult != MMSYSERR_NOERROR) + { + WLog_Print(winmm->log, WLOG_ERROR, "waveOutOpen failed: %" PRIu32 "", mmResult); + return FALSE; + } + + mmResult = waveOutSetVolume(winmm->hWaveOut, winmm->volume); + + if (mmResult != MMSYSERR_NOERROR) + { + WLog_Print(winmm->log, WLOG_ERROR, "waveOutSetVolume failed: %" PRIu32 "", mmResult); + return FALSE; + } + + return TRUE; +} + +static void rdpsnd_winmm_close(rdpsndDevicePlugin* device) +{ + MMRESULT mmResult; + rdpsndWinmmPlugin* winmm = (rdpsndWinmmPlugin*)device; + + if (winmm->hWaveOut) + { + EnterCriticalSection(&winmm->cs); + + mmResult = waveOutReset(winmm->hWaveOut); + if (mmResult != MMSYSERR_NOERROR) + WLog_Print(winmm->log, WLOG_ERROR, "waveOutReset failure: %" PRIu32 "", mmResult); + + mmResult = waveOutClose(winmm->hWaveOut); + if (mmResult != MMSYSERR_NOERROR) + WLog_Print(winmm->log, WLOG_ERROR, "waveOutClose failure: %" PRIu32 "", mmResult); + + LeaveCriticalSection(&winmm->cs); + + winmm->hWaveOut = NULL; + } + + if (winmm->hThread) + { + WaitForSingleObject(winmm->hThread, INFINITE); + CloseHandle(winmm->hThread); + winmm->hThread = NULL; + } +} + +static void rdpsnd_winmm_free(rdpsndDevicePlugin* device) +{ + rdpsndWinmmPlugin* winmm = (rdpsndWinmmPlugin*)device; + + if (winmm) + { + rdpsnd_winmm_close(device); + DeleteCriticalSection(&winmm->cs); + free(winmm); + } +} + +static BOOL rdpsnd_winmm_format_supported(rdpsndDevicePlugin* device, const AUDIO_FORMAT* format) +{ + MMRESULT result; + WAVEFORMATEX out; + + WINPR_UNUSED(device); + if (rdpsnd_winmm_convert_format(format, &out)) + { + result = waveOutOpen(NULL, WAVE_MAPPER, &out, 0, 0, WAVE_FORMAT_QUERY); + + if (result == MMSYSERR_NOERROR) + return TRUE; + } + + return FALSE; +} + +static UINT32 rdpsnd_winmm_get_volume(rdpsndDevicePlugin* device) +{ + MMRESULT mmResult; + DWORD dwVolume = UINT32_MAX; + rdpsndWinmmPlugin* winmm = (rdpsndWinmmPlugin*)device; + + if (!winmm->hWaveOut) + return dwVolume; + + mmResult = waveOutGetVolume(winmm->hWaveOut, &dwVolume); + if (mmResult != MMSYSERR_NOERROR) + { + WLog_Print(winmm->log, WLOG_ERROR, "waveOutGetVolume failure: %" PRIu32 "", mmResult); + dwVolume = UINT32_MAX; + } + return dwVolume; +} + +static BOOL rdpsnd_winmm_set_volume(rdpsndDevicePlugin* device, UINT32 value) +{ + MMRESULT mmResult; + rdpsndWinmmPlugin* winmm = (rdpsndWinmmPlugin*)device; + winmm->volume = value; + + if (!winmm->hWaveOut) + return TRUE; + + mmResult = waveOutSetVolume(winmm->hWaveOut, value); + if (mmResult != MMSYSERR_NOERROR) + { + WLog_Print(winmm->log, WLOG_ERROR, "waveOutGetVolume failure: %" PRIu32 "", mmResult); + return FALSE; + } + return TRUE; +} + +static UINT rdpsnd_winmm_play(rdpsndDevicePlugin* device, const BYTE* data, size_t size) +{ + MMRESULT mmResult; + LPWAVEHDR lpWaveHdr; + rdpsndWinmmPlugin* winmm = (rdpsndWinmmPlugin*)device; + + if (!winmm->hWaveOut) + return 0; + + if (size > UINT32_MAX) + return 0; + + lpWaveHdr = (LPWAVEHDR)calloc(1, sizeof(WAVEHDR)); + if (!lpWaveHdr) + return 0; + + lpWaveHdr->dwFlags = 0; + lpWaveHdr->dwLoops = 0; + lpWaveHdr->lpData = malloc(size); + if (!lpWaveHdr->lpData) + goto fail; + memcpy(lpWaveHdr->lpData, data, size); + lpWaveHdr->dwBufferLength = (DWORD)size; + + EnterCriticalSection(&winmm->cs); + + mmResult = waveOutPrepareHeader(winmm->hWaveOut, lpWaveHdr, sizeof(WAVEHDR)); + if (mmResult != MMSYSERR_NOERROR) + { + WLog_Print(winmm->log, WLOG_ERROR, "waveOutPrepareHeader failure: %" PRIu32 "", mmResult); + goto failCS; + } + + mmResult = waveOutWrite(winmm->hWaveOut, lpWaveHdr, sizeof(WAVEHDR)); + if (mmResult != MMSYSERR_NOERROR) + { + WLog_Print(winmm->log, WLOG_ERROR, "waveOutWrite failure: %" PRIu32 "", mmResult); + waveOutUnprepareHeader(winmm->hWaveOut, lpWaveHdr, sizeof(WAVEHDR)); + goto failCS; + } + + LeaveCriticalSection(&winmm->cs); + return winmm->latency; +failCS: + LeaveCriticalSection(&winmm->cs); +fail: + if (lpWaveHdr) + free(lpWaveHdr->lpData); + free(lpWaveHdr); + return 0; +} + +static void rdpsnd_winmm_parse_addin_args(rdpsndDevicePlugin* device, ADDIN_ARGV* args) +{ + WINPR_UNUSED(device); + WINPR_UNUSED(args); +} + +#ifdef BUILTIN_CHANNELS +#define freerdp_rdpsnd_client_subsystem_entry winmm_freerdp_rdpsnd_client_subsystem_entry +#else +#define freerdp_rdpsnd_client_subsystem_entry FREERDP_API freerdp_rdpsnd_client_subsystem_entry +#endif + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT freerdp_rdpsnd_client_subsystem_entry(PFREERDP_RDPSND_DEVICE_ENTRY_POINTS pEntryPoints) +{ + ADDIN_ARGV* args; + rdpsndWinmmPlugin* winmm; + + if (waveOutGetNumDevs() == 0) + { + WLog_Print(WLog_Get(TAG), WLOG_ERROR, "No sound playback device available!"); + return ERROR_DEVICE_NOT_AVAILABLE; + } + + winmm = (rdpsndWinmmPlugin*)calloc(1, sizeof(rdpsndWinmmPlugin)); + if (!winmm) + return CHANNEL_RC_NO_MEMORY; + + winmm->device.Open = rdpsnd_winmm_open; + winmm->device.FormatSupported = rdpsnd_winmm_format_supported; + winmm->device.GetVolume = rdpsnd_winmm_get_volume; + winmm->device.SetVolume = rdpsnd_winmm_set_volume; + winmm->device.Play = rdpsnd_winmm_play; + winmm->device.Close = rdpsnd_winmm_close; + winmm->device.Free = rdpsnd_winmm_free; + winmm->log = WLog_Get(TAG); + InitializeCriticalSection(&winmm->cs); + + args = pEntryPoints->args; + rdpsnd_winmm_parse_addin_args((rdpsndDevicePlugin*)winmm, args); + winmm->volume = 0xFFFFFFFF; + pEntryPoints->pRegisterRdpsndDevice(pEntryPoints->rdpsnd, (rdpsndDevicePlugin*)winmm); + return CHANNEL_RC_OK; +} diff --git a/channels/rdpsnd/common/CMakeLists.txt b/channels/rdpsnd/common/CMakeLists.txt new file mode 100644 index 0000000..32fa918 --- /dev/null +++ b/channels/rdpsnd/common/CMakeLists.txt @@ -0,0 +1,24 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2018 Armin Novak +# Copyright 2018 Thincast Technologies GmbH +# +# Licensed 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. + +set(SRCS + rdpsnd_common.h + rdpsnd_common.c) + +# Library currently header only +#add_library(rdpsnd-common STATIC ${SRCS}) diff --git a/channels/rdpsnd/common/rdpsnd_common.h b/channels/rdpsnd/common/rdpsnd_common.h new file mode 100644 index 0000000..6afcbc7 --- /dev/null +++ b/channels/rdpsnd/common/rdpsnd_common.h @@ -0,0 +1,43 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Server Audio Virtual Channel + * + * Copyright 2018 Armin Novak + * Copyright 2018 Thincast Technologies GmbH + * + * Licensed 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 FREERDP_CHANNEL_RDPSND_COMMON_MAIN_H +#define FREERDP_CHANNEL_RDPSND_COMMON_MAIN_H + +#include +#include +#include + +#include +#include +#include +#include + +typedef enum +{ + CHANNEL_VERSION_WIN_XP = 0x02, + CHANNEL_VERSION_WIN_XP_SP1 = 0x05, + CHANNEL_VERSION_WIN_VISTA = 0x05, + CHANNEL_VERSION_WIN_7 = 0x06, + CHANNEL_VERSION_WIN_8 = 0x08, + CHANNEL_VERSION_WIN_MAX = CHANNEL_VERSION_WIN_8 +} RdpSndChannelVersion; + +#endif /* FREERDP_CHANNEL_RDPSND_COMMON_MAIN_H */ diff --git a/channels/rdpsnd/server/CMakeLists.txt b/channels/rdpsnd/server/CMakeLists.txt new file mode 100644 index 0000000..9df47f2 --- /dev/null +++ b/channels/rdpsnd/server/CMakeLists.txt @@ -0,0 +1,26 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2012 Marc-Andre Moreau +# +# Licensed 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. + +define_channel_server("rdpsnd") + +set(${MODULE_PREFIX}_SRCS + rdpsnd_main.c + rdpsnd_main.h) + +add_channel_server_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} FALSE "VirtualChannelEntry") + +set_property(TARGET ${MODULE_NAME} PROPERTY FOLDER "Channels/${CHANNEL_NAME}/Server") diff --git a/channels/rdpsnd/server/rdpsnd_main.c b/channels/rdpsnd/server/rdpsnd_main.c new file mode 100644 index 0000000..363fac7 --- /dev/null +++ b/channels/rdpsnd/server/rdpsnd_main.c @@ -0,0 +1,1228 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Server Audio Virtual Channel + * + * Copyright 2012 Vic Lee + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include + +#include +#include +#include +#include + +#include + +#include "rdpsnd_common.h" +#include "rdpsnd_main.h" + +static wStream* rdpsnd_server_get_buffer(RdpsndServerContext* context) +{ + wStream* s; + WINPR_ASSERT(context); + WINPR_ASSERT(context->priv); + + s = context->priv->rdpsnd_pdu; + Stream_SetPosition(s, 0); + return s; +} + +/** + * Send Server Audio Formats and Version PDU (2.2.2.1) + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpsnd_server_send_formats(RdpsndServerContext* context) +{ + wStream* s = rdpsnd_server_get_buffer(context); + size_t pos; + UINT16 i; + BOOL status = FALSE; + ULONG written; + + if (!Stream_EnsureRemainingCapacity(s, 24)) + return ERROR_OUTOFMEMORY; + + Stream_Write_UINT8(s, SNDC_FORMATS); + Stream_Write_UINT8(s, 0); + Stream_Seek_UINT16(s); + Stream_Write_UINT32(s, 0); /* dwFlags */ + Stream_Write_UINT32(s, 0); /* dwVolume */ + Stream_Write_UINT32(s, 0); /* dwPitch */ + Stream_Write_UINT16(s, 0); /* wDGramPort */ + Stream_Write_UINT16(s, context->num_server_formats); /* wNumberOfFormats */ + Stream_Write_UINT8(s, context->block_no); /* cLastBlockConfirmed */ + Stream_Write_UINT16(s, CHANNEL_VERSION_WIN_MAX); /* wVersion */ + Stream_Write_UINT8(s, 0); /* bPad */ + + for (i = 0; i < context->num_server_formats; i++) + { + const AUDIO_FORMAT* format = &context->server_formats[i]; + + if (!audio_format_write(s, format)) + goto fail; + } + + pos = Stream_GetPosition(s); + Stream_SetPosition(s, 2); + Stream_Write_UINT16(s, pos - 4); + Stream_SetPosition(s, pos); + + WINPR_ASSERT(context->priv); + status = WTSVirtualChannelWrite(context->priv->ChannelHandle, (PCHAR)Stream_Buffer(s), + Stream_GetPosition(s), &written); + Stream_SetPosition(s, 0); +fail: + return status ? CHANNEL_RC_OK : ERROR_INTERNAL_ERROR; +} + +/** + * Read Wave Confirm PDU (2.2.3.8) and handle callback + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpsnd_server_recv_waveconfirm(RdpsndServerContext* context, wStream* s) +{ + UINT16 timestamp; + BYTE confirmBlockNum; + UINT error = CHANNEL_RC_OK; + + WINPR_ASSERT(context); + + if (!Stream_CheckAndLogRequiredLength(TAG, s, 4)) + return ERROR_INVALID_DATA; + + Stream_Read_UINT16(s, timestamp); + Stream_Read_UINT8(s, confirmBlockNum); + Stream_Seek_UINT8(s); + IFCALLRET(context->ConfirmBlock, error, context, confirmBlockNum, timestamp); + + if (error) + WLog_ERR(TAG, "context->ConfirmBlock failed with error %" PRIu32 "", error); + + return error; +} + +/** + * Read Training Confirm PDU (2.2.3.2) and handle callback + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpsnd_server_recv_trainingconfirm(RdpsndServerContext* context, wStream* s) +{ + UINT16 timestamp; + UINT16 packsize; + UINT error = CHANNEL_RC_OK; + + WINPR_ASSERT(context); + + if (!Stream_CheckAndLogRequiredLength(TAG, s, 4)) + return ERROR_INVALID_DATA; + + Stream_Read_UINT16(s, timestamp); + Stream_Read_UINT16(s, packsize); + + IFCALLRET(context->TrainingConfirm, error, context, timestamp, packsize); + if (error) + WLog_ERR(TAG, "context->TrainingConfirm failed with error %" PRIu32 "", error); + + return error; +} + +/** + * Read Quality Mode PDU (2.2.2.3) + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpsnd_server_recv_quality_mode(RdpsndServerContext* context, wStream* s) +{ + WINPR_ASSERT(context); + + if (Stream_GetRemainingLength(s) < 4) + { + WLog_ERR(TAG, "not enough data in stream!"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT16(s, context->qualityMode); /* wQualityMode */ + Stream_Seek_UINT16(s); /* Reserved */ + + WLog_DBG(TAG, "Client requested sound quality: 0x%04" PRIX16 "", context->qualityMode); + + return CHANNEL_RC_OK; +} + +/** + * Read Client Audio Formats and Version PDU (2.2.2.2) + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpsnd_server_recv_formats(RdpsndServerContext* context, wStream* s) +{ + UINT16 i, num_known_format = 0; + UINT16 udpPort; + BYTE lastblock; + UINT error = CHANNEL_RC_OK; + + WINPR_ASSERT(context); + + if (!Stream_CheckAndLogRequiredLength(TAG, s, 20)) + return ERROR_INVALID_DATA; + + Stream_Read_UINT32(s, context->capsFlags); /* dwFlags */ + Stream_Read_UINT32(s, context->initialVolume); /* dwVolume */ + Stream_Read_UINT32(s, context->initialPitch); /* dwPitch */ + Stream_Read_UINT16(s, udpPort); /* wDGramPort */ + Stream_Read_UINT16(s, context->num_client_formats); /* wNumberOfFormats */ + Stream_Read_UINT8(s, lastblock); /* cLastBlockConfirmed */ + Stream_Read_UINT16(s, context->clientVersion); /* wVersion */ + Stream_Seek_UINT8(s); /* bPad */ + + /* this check is only a guess as cbSize can influence the size of a format record */ + if (!Stream_CheckAndLogRequiredLength(TAG, s, 18ull * context->num_client_formats)) + return ERROR_INVALID_DATA; + + if (!context->num_client_formats) + { + WLog_ERR(TAG, "client doesn't support any format!"); + return ERROR_INTERNAL_ERROR; + } + + context->client_formats = audio_formats_new(context->num_client_formats); + + if (!context->client_formats) + { + WLog_ERR(TAG, "calloc failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + for (i = 0; i < context->num_client_formats; i++) + { + AUDIO_FORMAT* format = &context->client_formats[i]; + + if (!Stream_CheckAndLogRequiredLength(TAG, s, 18)) + { + WLog_ERR(TAG, "not enough data in stream!"); + error = ERROR_INVALID_DATA; + goto out_free; + } + + Stream_Read_UINT16(s, format->wFormatTag); + Stream_Read_UINT16(s, format->nChannels); + Stream_Read_UINT32(s, format->nSamplesPerSec); + Stream_Read_UINT32(s, format->nAvgBytesPerSec); + Stream_Read_UINT16(s, format->nBlockAlign); + Stream_Read_UINT16(s, format->wBitsPerSample); + Stream_Read_UINT16(s, format->cbSize); + + if (format->cbSize > 0) + { + if (!Stream_SafeSeek(s, format->cbSize)) + { + WLog_ERR(TAG, "Stream_SafeSeek failed!"); + error = ERROR_INTERNAL_ERROR; + goto out_free; + } + } + + if (format->wFormatTag != 0) + { + // lets call this a known format + // TODO: actually look through our own list of known formats + num_known_format++; + } + } + + if (!context->num_client_formats) + { + WLog_ERR(TAG, "client doesn't support any known format!"); + goto out_free; + } + + return CHANNEL_RC_OK; +out_free: + free(context->client_formats); + return error; +} + +static DWORD WINAPI rdpsnd_server_thread(LPVOID arg) +{ + DWORD nCount = 0, status; + HANDLE events[2] = { 0 }; + RdpsndServerContext* context = (RdpsndServerContext*)arg; + UINT error = CHANNEL_RC_OK; + + WINPR_ASSERT(context); + WINPR_ASSERT(context->priv); + + events[nCount++] = context->priv->channelEvent; + events[nCount++] = context->priv->StopEvent; + + WINPR_ASSERT(nCount <= ARRAYSIZE(events)); + + while (TRUE) + { + status = WaitForMultipleObjects(nCount, events, FALSE, INFINITE); + + if (status == WAIT_FAILED) + { + error = GetLastError(); + WLog_ERR(TAG, "WaitForMultipleObjects failed with error %" PRIu32 "!", error); + break; + } + + status = WaitForSingleObject(context->priv->StopEvent, 0); + + if (status == WAIT_FAILED) + { + error = GetLastError(); + WLog_ERR(TAG, "WaitForSingleObject failed with error %" PRIu32 "!", error); + break; + } + + if (status == WAIT_OBJECT_0) + break; + + if ((error = rdpsnd_server_handle_messages(context))) + { + WLog_ERR(TAG, "rdpsnd_server_handle_messages failed with error %" PRIu32 "", error); + break; + } + } + + if (error && context->rdpcontext) + setChannelError(context->rdpcontext, error, "rdpsnd_server_thread reported an error"); + + ExitThread(error); + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpsnd_server_initialize(RdpsndServerContext* context, BOOL ownThread) +{ + WINPR_ASSERT(context); + WINPR_ASSERT(context->priv); + + context->priv->ownThread = ownThread; + return context->Start(context); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpsnd_server_select_format(RdpsndServerContext* context, UINT16 client_format_index) +{ + int bs; + int out_buffer_size; + AUDIO_FORMAT* format; + UINT error = CHANNEL_RC_OK; + + WINPR_ASSERT(context); + WINPR_ASSERT(context->priv); + + if ((client_format_index >= context->num_client_formats) || (!context->src_format)) + { + WLog_ERR(TAG, "index %d is not correct.", client_format_index); + return ERROR_INVALID_DATA; + } + + EnterCriticalSection(&context->priv->lock); + context->priv->src_bytes_per_sample = context->src_format->wBitsPerSample / 8; + context->priv->src_bytes_per_frame = + context->priv->src_bytes_per_sample * context->src_format->nChannels; + context->selected_client_format = client_format_index; + format = &context->client_formats[client_format_index]; + + if (format->nSamplesPerSec == 0) + { + WLog_ERR(TAG, "invalid Client Sound Format!!"); + error = ERROR_INVALID_DATA; + goto out; + } + + if (context->latency <= 0) + context->latency = 50; + + context->priv->out_frames = context->src_format->nSamplesPerSec * context->latency / 1000; + + if (context->priv->out_frames < 1) + context->priv->out_frames = 1; + + switch (format->wFormatTag) + { + case WAVE_FORMAT_DVI_ADPCM: + bs = (format->nBlockAlign - 4 * format->nChannels) * 4; + context->priv->out_frames -= context->priv->out_frames % bs; + + if (context->priv->out_frames < bs) + context->priv->out_frames = bs; + + break; + + case WAVE_FORMAT_ADPCM: + bs = (format->nBlockAlign - 7 * format->nChannels) * 2 / format->nChannels + 2; + context->priv->out_frames -= context->priv->out_frames % bs; + + if (context->priv->out_frames < bs) + context->priv->out_frames = bs; + + break; + } + + context->priv->out_pending_frames = 0; + out_buffer_size = context->priv->out_frames * context->priv->src_bytes_per_frame; + + if (context->priv->out_buffer_size < out_buffer_size) + { + BYTE* newBuffer; + newBuffer = (BYTE*)realloc(context->priv->out_buffer, out_buffer_size); + + if (!newBuffer) + { + WLog_ERR(TAG, "realloc failed!"); + error = CHANNEL_RC_NO_MEMORY; + goto out; + } + + context->priv->out_buffer = newBuffer; + context->priv->out_buffer_size = out_buffer_size; + } + + freerdp_dsp_context_reset(context->priv->dsp_context, format); +out: + LeaveCriticalSection(&context->priv->lock); + return error; +} + +/** + * Send Training PDU (2.2.3.1) + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpsnd_server_training(RdpsndServerContext* context, UINT16 timestamp, UINT16 packsize, + BYTE* data) +{ + size_t end = 0; + ULONG written; + BOOL status; + wStream* s = rdpsnd_server_get_buffer(context); + + if (!Stream_EnsureRemainingCapacity(s, 8)) + return ERROR_INTERNAL_ERROR; + + Stream_Write_UINT8(s, SNDC_TRAINING); + Stream_Write_UINT8(s, 0); + Stream_Seek_UINT16(s); + Stream_Write_UINT16(s, timestamp); + Stream_Write_UINT16(s, packsize); + + if (packsize > 0) + { + if (!Stream_EnsureRemainingCapacity(s, packsize)) + { + Stream_SetPosition(s, 0); + return ERROR_INTERNAL_ERROR; + } + + Stream_Write(s, data, packsize); + } + + end = Stream_GetPosition(s); + Stream_SetPosition(s, 2); + Stream_Write_UINT16(s, end - 4); + + status = WTSVirtualChannelWrite(context->priv->ChannelHandle, (PCHAR)Stream_Buffer(s), end, + &written); + + Stream_SetPosition(s, 0); + + return status ? CHANNEL_RC_OK : ERROR_INTERNAL_ERROR; +} + +static BOOL rdpsnd_server_align_wave_pdu(wStream* s, UINT32 alignment) +{ + size_t size; + Stream_SealLength(s); + size = Stream_Length(s); + + if ((size % alignment) != 0) + { + size_t offset = alignment - size % alignment; + + if (!Stream_EnsureRemainingCapacity(s, offset)) + return FALSE; + + Stream_Zero(s, offset); + } + + Stream_SealLength(s); + return TRUE; +} + +/** + * Function description + * context->priv->lock should be obtained before calling this function + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpsnd_server_send_wave_pdu(RdpsndServerContext* context, UINT16 wTimestamp) +{ + size_t length; + size_t start, end = 0; + const BYTE* src; + AUDIO_FORMAT* format; + ULONG written; + UINT error = CHANNEL_RC_OK; + wStream* s = rdpsnd_server_get_buffer(context); + + if (context->selected_client_format > context->num_client_formats) + return ERROR_INTERNAL_ERROR; + + WINPR_ASSERT(context->client_formats); + + format = &context->client_formats[context->selected_client_format]; + /* WaveInfo PDU */ + Stream_SetPosition(s, 0); + + if (!Stream_EnsureRemainingCapacity(s, 16)) + return ERROR_OUTOFMEMORY; + + Stream_Write_UINT8(s, SNDC_WAVE); /* msgType */ + Stream_Write_UINT8(s, 0); /* bPad */ + Stream_Write_UINT16(s, 0); /* BodySize */ + Stream_Write_UINT16(s, wTimestamp); /* wTimeStamp */ + Stream_Write_UINT16(s, context->selected_client_format); /* wFormatNo */ + Stream_Write_UINT8(s, context->block_no); /* cBlockNo */ + Stream_Seek(s, 3); /* bPad */ + start = Stream_GetPosition(s); + src = context->priv->out_buffer; + length = context->priv->out_pending_frames * context->priv->src_bytes_per_frame * 1ULL; + + if (!freerdp_dsp_encode(context->priv->dsp_context, context->src_format, src, length, s)) + return ERROR_INTERNAL_ERROR; + else + { + /* Set stream size */ + if (!rdpsnd_server_align_wave_pdu(s, format->nBlockAlign)) + return ERROR_INTERNAL_ERROR; + + end = Stream_GetPosition(s); + Stream_SetPosition(s, 2); + Stream_Write_UINT16(s, end - start + 8); + Stream_SetPosition(s, end); + + if (!WTSVirtualChannelWrite(context->priv->ChannelHandle, (PCHAR)Stream_Buffer(s), + start + 4, &written)) + { + WLog_ERR(TAG, "WTSVirtualChannelWrite failed!"); + error = ERROR_INTERNAL_ERROR; + } + } + + if (error != CHANNEL_RC_OK) + { + WLog_ERR(TAG, "WTSVirtualChannelWrite failed!"); + error = ERROR_INTERNAL_ERROR; + goto out; + } + + Stream_SetPosition(s, start); + Stream_Write_UINT32(s, 0); /* bPad */ + Stream_SetPosition(s, start); + + if (!WTSVirtualChannelWrite(context->priv->ChannelHandle, (PCHAR)Stream_Pointer(s), end - start, + &written)) + { + WLog_ERR(TAG, "WTSVirtualChannelWrite failed!"); + error = ERROR_INTERNAL_ERROR; + } + + context->block_no = (context->block_no + 1) % 256; + +out: + Stream_SetPosition(s, 0); + context->priv->out_pending_frames = 0; + return error; +} + +/** + * Function description + * context->priv->lock should be obtained before calling this function + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpsnd_server_send_wave2_pdu(RdpsndServerContext* context, UINT16 formatNo, + const BYTE* data, size_t size, BOOL encoded, + UINT16 timestamp, UINT32 audioTimeStamp) +{ + size_t end = 0; + ULONG written; + UINT error = CHANNEL_RC_OK; + BOOL status; + wStream* s = rdpsnd_server_get_buffer(context); + + if (!Stream_EnsureRemainingCapacity(s, 16)) + { + error = ERROR_INTERNAL_ERROR; + goto out; + } + + /* Wave2 PDU */ + Stream_Write_UINT8(s, SNDC_WAVE2); /* msgType */ + Stream_Write_UINT8(s, 0); /* bPad */ + Stream_Write_UINT16(s, 0); /* BodySize */ + Stream_Write_UINT16(s, timestamp); /* wTimeStamp */ + Stream_Write_UINT16(s, formatNo); /* wFormatNo */ + Stream_Write_UINT8(s, context->block_no); /* cBlockNo */ + Stream_Write_UINT8(s, 0); /* bPad */ + Stream_Write_UINT8(s, 0); /* bPad */ + Stream_Write_UINT8(s, 0); /* bPad */ + Stream_Write_UINT32(s, audioTimeStamp); /* dwAudioTimeStamp */ + + if (encoded) + { + if (!Stream_EnsureRemainingCapacity(s, size)) + { + error = ERROR_INTERNAL_ERROR; + goto out; + } + + Stream_Write(s, data, size); + } + else + { + AUDIO_FORMAT* format; + + if (!freerdp_dsp_encode(context->priv->dsp_context, context->src_format, data, size, s)) + { + error = ERROR_INTERNAL_ERROR; + goto out; + } + + format = &context->client_formats[formatNo]; + if (!rdpsnd_server_align_wave_pdu(s, format->nBlockAlign)) + { + error = ERROR_INTERNAL_ERROR; + goto out; + } + } + + end = Stream_GetPosition(s); + Stream_SetPosition(s, 2); + Stream_Write_UINT16(s, end - 4); + + status = WTSVirtualChannelWrite(context->priv->ChannelHandle, (PCHAR)Stream_Buffer(s), end, + &written); + + if (!status || (end != written)) + { + WLog_ERR(TAG, "WTSVirtualChannelWrite failed! [stream length=%" PRIdz " - written=%" PRIu32, + end, written); + error = ERROR_INTERNAL_ERROR; + } + + context->block_no = (context->block_no + 1) % 256; + +out: + Stream_SetPosition(s, 0); + context->priv->out_pending_frames = 0; + return error; +} + +/* Wrapper function to send WAVE or WAVE2 PDU depending on client connected */ +static UINT rdpsnd_server_send_audio_pdu(RdpsndServerContext* context, UINT16 wTimestamp) +{ + const BYTE* src; + size_t length; + + WINPR_ASSERT(context); + WINPR_ASSERT(context->priv); + + if (context->selected_client_format >= context->num_client_formats) + return ERROR_INTERNAL_ERROR; + + src = context->priv->out_buffer; + length = context->priv->out_pending_frames * context->priv->src_bytes_per_frame; + + if (context->clientVersion >= CHANNEL_VERSION_WIN_8) + return rdpsnd_server_send_wave2_pdu(context, context->selected_client_format, src, length, + FALSE, wTimestamp, wTimestamp); + else + return rdpsnd_server_send_wave_pdu(context, wTimestamp); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpsnd_server_send_samples(RdpsndServerContext* context, const void* buf, int nframes, + UINT16 wTimestamp) +{ + UINT error = CHANNEL_RC_OK; + + WINPR_ASSERT(context); + WINPR_ASSERT(context->priv); + + EnterCriticalSection(&context->priv->lock); + + if (context->selected_client_format >= context->num_client_formats) + { + /* It's possible while format negotiation has not been done */ + WLog_WARN(TAG, "Drop samples because client format has not been negotiated."); + error = ERROR_NOT_READY; + goto out; + } + + while (nframes > 0) + { + const size_t cframes = + MIN(nframes, context->priv->out_frames - context->priv->out_pending_frames); + size_t cframesize = cframes * context->priv->src_bytes_per_frame; + CopyMemory(context->priv->out_buffer + + (context->priv->out_pending_frames * context->priv->src_bytes_per_frame), + buf, cframesize); + buf = (const BYTE*)buf + cframesize; + nframes -= cframes; + context->priv->out_pending_frames += cframes; + + if (context->priv->out_pending_frames >= context->priv->out_frames) + { + if ((error = rdpsnd_server_send_audio_pdu(context, wTimestamp))) + { + WLog_ERR(TAG, "rdpsnd_server_send_audio_pdu failed with error %" PRIu32 "", error); + break; + } + } + } + +out: + LeaveCriticalSection(&context->priv->lock); + return error; +} + +/** + * Send encoded audio samples using a Wave2 PDU. + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpsnd_server_send_samples2(RdpsndServerContext* context, UINT16 formatNo, + const void* buf, size_t size, UINT16 timestamp, + UINT32 audioTimeStamp) +{ + UINT error = CHANNEL_RC_OK; + + WINPR_ASSERT(context); + WINPR_ASSERT(context->priv); + + if (context->clientVersion < CHANNEL_VERSION_WIN_8) + return ERROR_INTERNAL_ERROR; + + EnterCriticalSection(&context->priv->lock); + + error = + rdpsnd_server_send_wave2_pdu(context, formatNo, buf, size, TRUE, timestamp, audioTimeStamp); + + LeaveCriticalSection(&context->priv->lock); + + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpsnd_server_set_volume(RdpsndServerContext* context, int left, int right) +{ + size_t len; + BOOL status; + ULONG written; + wStream* s = rdpsnd_server_get_buffer(context); + + if (!Stream_EnsureRemainingCapacity(s, 8)) + return ERROR_NOT_ENOUGH_MEMORY; + + Stream_Write_UINT8(s, SNDC_SETVOLUME); + Stream_Write_UINT8(s, 0); + Stream_Write_UINT16(s, 4); /* Payload length */ + Stream_Write_UINT16(s, left); + Stream_Write_UINT16(s, right); + len = Stream_GetPosition(s); + + status = WTSVirtualChannelWrite(context->priv->ChannelHandle, (PCHAR)Stream_Buffer(s), + (ULONG)len, &written); + Stream_SetPosition(s, 0); + return status ? CHANNEL_RC_OK : ERROR_INTERNAL_ERROR; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpsnd_server_close(RdpsndServerContext* context) +{ + size_t pos; + BOOL status; + ULONG written; + UINT error = CHANNEL_RC_OK; + wStream* s = rdpsnd_server_get_buffer(context); + + EnterCriticalSection(&context->priv->lock); + + if (context->priv->out_pending_frames > 0) + { + if (context->selected_client_format >= context->num_client_formats) + { + WLog_ERR(TAG, "Pending audio frame exists while no format selected."); + error = ERROR_INVALID_DATA; + } + else if ((error = rdpsnd_server_send_audio_pdu(context, 0))) + { + WLog_ERR(TAG, "rdpsnd_server_send_audio_pdu failed with error %" PRIu32 "", error); + } + } + + LeaveCriticalSection(&context->priv->lock); + + if (error) + return error; + + context->selected_client_format = 0xFFFF; + + if (!Stream_EnsureRemainingCapacity(s, 4)) + return ERROR_OUTOFMEMORY; + + Stream_Write_UINT8(s, SNDC_CLOSE); + Stream_Write_UINT8(s, 0); + Stream_Seek_UINT16(s); + pos = Stream_GetPosition(s); + Stream_SetPosition(s, 2); + Stream_Write_UINT16(s, pos - 4); + Stream_SetPosition(s, pos); + status = WTSVirtualChannelWrite(context->priv->ChannelHandle, (PCHAR)Stream_Buffer(s), + Stream_GetPosition(s), &written); + Stream_SetPosition(s, 0); + return status ? CHANNEL_RC_OK : ERROR_INTERNAL_ERROR; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpsnd_server_start(RdpsndServerContext* context) +{ + void* buffer = NULL; + DWORD bytesReturned; + RdpsndServerPrivate* priv; + UINT error = ERROR_INTERNAL_ERROR; + PULONG pSessionId = NULL; + + WINPR_ASSERT(context); + WINPR_ASSERT(context->priv); + + priv = context->priv; + priv->SessionId = WTS_CURRENT_SESSION; + + if (context->use_dynamic_virtual_channel) + { + UINT32 channelId; + BOOL status = TRUE; + + if (WTSQuerySessionInformationA(context->vcm, WTS_CURRENT_SESSION, WTSSessionId, + (LPSTR*)&pSessionId, &bytesReturned)) + { + priv->SessionId = (DWORD)*pSessionId; + WTSFreeMemory(pSessionId); + priv->ChannelHandle = (HANDLE)WTSVirtualChannelOpenEx( + priv->SessionId, "AUDIO_PLAYBACK_DVC", WTS_CHANNEL_OPTION_DYNAMIC); + if (!priv->ChannelHandle) + { + WLog_ERR(TAG, "Open audio dynamic virtual channel (AUDIO_PLAYBACK_DVC) failed!"); + return ERROR_INTERNAL_ERROR; + } + + channelId = WTSChannelGetIdByHandle(priv->ChannelHandle); + + IFCALLRET(context->ChannelIdAssigned, status, context, channelId); + if (!status) + { + WLog_ERR(TAG, "context->ChannelIdAssigned failed!"); + goto out_close; + } + } + else + { + WLog_ERR(TAG, "WTSQuerySessionInformationA failed!"); + return ERROR_INTERNAL_ERROR; + } + } + else + { + priv->ChannelHandle = + WTSVirtualChannelOpen(context->vcm, WTS_CURRENT_SESSION, "rdpsnd"); + if (!priv->ChannelHandle) + { + WLog_ERR(TAG, "Open audio static virtual channel (rdpsnd) failed!"); + return ERROR_INTERNAL_ERROR; + } + } + + if (!WTSVirtualChannelQuery(priv->ChannelHandle, WTSVirtualEventHandle, &buffer, + &bytesReturned) || + (bytesReturned != sizeof(HANDLE))) + { + WLog_ERR(TAG, + "error during WTSVirtualChannelQuery(WTSVirtualEventHandle) or invalid returned " + "size(%" PRIu32 ")", + bytesReturned); + + if (buffer) + WTSFreeMemory(buffer); + + goto out_close; + } + + CopyMemory(&priv->channelEvent, buffer, sizeof(HANDLE)); + WTSFreeMemory(buffer); + priv->rdpsnd_pdu = Stream_New(NULL, 4096); + + if (!priv->rdpsnd_pdu) + { + WLog_ERR(TAG, "Stream_New failed!"); + error = CHANNEL_RC_NO_MEMORY; + goto out_close; + } + + if (!InitializeCriticalSectionEx(&context->priv->lock, 0, 0)) + { + WLog_ERR(TAG, "InitializeCriticalSectionEx failed!"); + goto out_pdu; + } + + if ((error = rdpsnd_server_send_formats(context))) + { + WLog_ERR(TAG, "rdpsnd_server_send_formats failed with error %" PRIu32 "", error); + goto out_lock; + } + + if (priv->ownThread) + { + context->priv->StopEvent = CreateEvent(NULL, TRUE, FALSE, NULL); + + if (!context->priv->StopEvent) + { + WLog_ERR(TAG, "CreateEvent failed!"); + goto out_lock; + } + + context->priv->Thread = + CreateThread(NULL, 0, rdpsnd_server_thread, (void*)context, 0, NULL); + + if (!context->priv->Thread) + { + WLog_ERR(TAG, "CreateThread failed!"); + goto out_stopEvent; + } + } + + return CHANNEL_RC_OK; +out_stopEvent: + CloseHandle(context->priv->StopEvent); + context->priv->StopEvent = NULL; +out_lock: + DeleteCriticalSection(&context->priv->lock); +out_pdu: + Stream_Free(context->priv->rdpsnd_pdu, TRUE); + context->priv->rdpsnd_pdu = NULL; +out_close: + WTSVirtualChannelClose(context->priv->ChannelHandle); + context->priv->ChannelHandle = NULL; + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT rdpsnd_server_stop(RdpsndServerContext* context) +{ + UINT error = CHANNEL_RC_OK; + + WINPR_ASSERT(context); + WINPR_ASSERT(context->priv); + + if (!context->priv->StopEvent) + return error; + + if (context->priv->ownThread) + { + if (context->priv->StopEvent) + { + SetEvent(context->priv->StopEvent); + + if (WaitForSingleObject(context->priv->Thread, INFINITE) == WAIT_FAILED) + { + error = GetLastError(); + WLog_ERR(TAG, "WaitForSingleObject failed with error %" PRIu32 "!", error); + return error; + } + + CloseHandle(context->priv->Thread); + CloseHandle(context->priv->StopEvent); + context->priv->Thread = NULL; + context->priv->StopEvent = NULL; + } + } + + DeleteCriticalSection(&context->priv->lock); + + if (context->priv->rdpsnd_pdu) + { + Stream_Free(context->priv->rdpsnd_pdu, TRUE); + context->priv->rdpsnd_pdu = NULL; + } + + if (context->priv->ChannelHandle) + { + WTSVirtualChannelClose(context->priv->ChannelHandle); + context->priv->ChannelHandle = NULL; + } + + return error; +} + +RdpsndServerContext* rdpsnd_server_context_new(HANDLE vcm) +{ + RdpsndServerPrivate* priv; + RdpsndServerContext* context = (RdpsndServerContext*)calloc(1, sizeof(RdpsndServerContext)); + + if (!context) + goto fail; + + context->vcm = vcm; + context->Start = rdpsnd_server_start; + context->Stop = rdpsnd_server_stop; + context->selected_client_format = 0xFFFF; + context->Initialize = rdpsnd_server_initialize; + context->SendFormats = rdpsnd_server_send_formats; + context->SelectFormat = rdpsnd_server_select_format; + context->Training = rdpsnd_server_training; + context->SendSamples = rdpsnd_server_send_samples; + context->SendSamples2 = rdpsnd_server_send_samples2; + context->SetVolume = rdpsnd_server_set_volume; + context->Close = rdpsnd_server_close; + context->priv = priv = (RdpsndServerPrivate*)calloc(1, sizeof(RdpsndServerPrivate)); + + if (!priv) + { + WLog_ERR(TAG, "calloc failed!"); + goto fail; + } + + priv->dsp_context = freerdp_dsp_context_new(TRUE); + + if (!priv->dsp_context) + { + WLog_ERR(TAG, "freerdp_dsp_context_new failed!"); + goto fail; + } + + priv->input_stream = Stream_New(NULL, 4); + + if (!priv->input_stream) + { + WLog_ERR(TAG, "Stream_New failed!"); + goto fail; + } + + priv->expectedBytes = 4; + priv->waitingHeader = TRUE; + priv->ownThread = TRUE; + return context; +fail: + rdpsnd_server_context_free(context); + return NULL; +} + +void rdpsnd_server_context_reset(RdpsndServerContext* context) +{ + WINPR_ASSERT(context); + WINPR_ASSERT(context->priv); + + context->priv->expectedBytes = 4; + context->priv->waitingHeader = TRUE; + Stream_SetPosition(context->priv->input_stream, 0); +} + +void rdpsnd_server_context_free(RdpsndServerContext* context) +{ + if (!context) + return; + + if (context->priv) + { + rdpsnd_server_stop(context); + + free(context->priv->out_buffer); + + if (context->priv->dsp_context) + freerdp_dsp_context_free(context->priv->dsp_context); + + if (context->priv->input_stream) + Stream_Free(context->priv->input_stream, TRUE); + } + + free(context->server_formats); + free(context->client_formats); + free(context->priv); + free(context); +} + +HANDLE rdpsnd_server_get_event_handle(RdpsndServerContext* context) +{ + WINPR_ASSERT(context); + WINPR_ASSERT(context->priv); + + return context->priv->channelEvent; +} + +/* + * Handle rpdsnd messages - server side + * + * @param Server side context + * + * @return 0 on success + * ERROR_NO_DATA if no data could be read this time + * otherwise error + */ +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT rdpsnd_server_handle_messages(RdpsndServerContext* context) +{ + DWORD bytesReturned; + UINT ret = CHANNEL_RC_OK; + RdpsndServerPrivate* priv; + wStream* s; + + WINPR_ASSERT(context); + WINPR_ASSERT(context->priv); + + priv = context->priv; + s = priv->input_stream; + + if (!WTSVirtualChannelRead(priv->ChannelHandle, 0, (PCHAR)Stream_Pointer(s), + priv->expectedBytes, &bytesReturned)) + { + if (GetLastError() == ERROR_NO_DATA) + return ERROR_NO_DATA; + + WLog_ERR(TAG, "channel connection closed"); + return ERROR_INTERNAL_ERROR; + } + + priv->expectedBytes -= bytesReturned; + Stream_Seek(s, bytesReturned); + + if (priv->expectedBytes) + return CHANNEL_RC_OK; + + Stream_SealLength(s); + Stream_SetPosition(s, 0); + + if (priv->waitingHeader) + { + /* header case */ + Stream_Read_UINT8(s, priv->msgType); + Stream_Seek_UINT8(s); /* bPad */ + Stream_Read_UINT16(s, priv->expectedBytes); + priv->waitingHeader = FALSE; + Stream_SetPosition(s, 0); + + if (priv->expectedBytes) + { + if (!Stream_EnsureCapacity(s, priv->expectedBytes)) + { + WLog_ERR(TAG, "Stream_EnsureCapacity failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + return CHANNEL_RC_OK; + } + } + + /* when here we have the header + the body */ +#ifdef WITH_DEBUG_SND + WLog_DBG(TAG, "message type %" PRIu8 "", priv->msgType); +#endif + priv->expectedBytes = 4; + priv->waitingHeader = TRUE; + + switch (priv->msgType) + { + case SNDC_WAVECONFIRM: + ret = rdpsnd_server_recv_waveconfirm(context, s); + break; + + case SNDC_TRAINING: + ret = rdpsnd_server_recv_trainingconfirm(context, s); + break; + + case SNDC_FORMATS: + ret = rdpsnd_server_recv_formats(context, s); + + if ((ret == CHANNEL_RC_OK) && (context->clientVersion < CHANNEL_VERSION_WIN_7)) + IFCALL(context->Activated, context); + + break; + + case SNDC_QUALITYMODE: + ret = rdpsnd_server_recv_quality_mode(context, s); + + if ((ret == CHANNEL_RC_OK) && (context->clientVersion >= CHANNEL_VERSION_WIN_7)) + IFCALL(context->Activated, context); + + break; + + default: + WLog_ERR(TAG, "UNKNOWN MESSAGE TYPE!! (0x%02" PRIX8 ")", priv->msgType); + ret = ERROR_INVALID_DATA; + break; + } + + Stream_SetPosition(s, 0); + return ret; +} diff --git a/channels/rdpsnd/server/rdpsnd_main.h b/channels/rdpsnd/server/rdpsnd_main.h new file mode 100644 index 0000000..9849273 --- /dev/null +++ b/channels/rdpsnd/server/rdpsnd_main.h @@ -0,0 +1,59 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Server Audio Virtual Channel + * + * Copyright 2012 Vic Lee + * Copyright 2013 Marc-Andre Moreau + * + * Licensed 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 FREERDP_CHANNEL_RDPSND_SERVER_MAIN_H +#define FREERDP_CHANNEL_RDPSND_SERVER_MAIN_H + +#include +#include +#include + +#include +#include +#include +#include + +#define TAG CHANNELS_TAG("rdpsnd.server") + +struct _rdpsnd_server_private +{ + BOOL ownThread; + HANDLE Thread; + HANDLE StopEvent; + HANDLE channelEvent; + void* ChannelHandle; + DWORD SessionId; + + BOOL waitingHeader; + DWORD expectedBytes; + BYTE msgType; + wStream* input_stream; + wStream* rdpsnd_pdu; + BYTE* out_buffer; + int out_buffer_size; + int out_frames; + int out_pending_frames; + UINT32 src_bytes_per_sample; + UINT32 src_bytes_per_frame; + FREERDP_DSP_CONTEXT* dsp_context; + CRITICAL_SECTION lock; /* Protect out_buffer and related parameters */ +}; + +#endif /* FREERDP_CHANNEL_RDPSND_SERVER_MAIN_H */ diff --git a/channels/remdesk/CMakeLists.txt b/channels/remdesk/CMakeLists.txt new file mode 100644 index 0000000..23f1cf7 --- /dev/null +++ b/channels/remdesk/CMakeLists.txt @@ -0,0 +1,26 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2012 Marc-Andre Moreau +# +# Licensed 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. + +define_channel("remdesk") + +if(WITH_CLIENT_CHANNELS) + add_channel_client(${MODULE_PREFIX} ${CHANNEL_NAME}) +endif() + +if(WITH_SERVER_CHANNELS) + add_channel_server(${MODULE_PREFIX} ${CHANNEL_NAME}) +endif() diff --git a/channels/remdesk/ChannelOptions.cmake b/channels/remdesk/ChannelOptions.cmake new file mode 100644 index 0000000..17518e6 --- /dev/null +++ b/channels/remdesk/ChannelOptions.cmake @@ -0,0 +1,12 @@ + +set(OPTION_DEFAULT OFF) +set(OPTION_CLIENT_DEFAULT ON) +set(OPTION_SERVER_DEFAULT ON) + +define_channel_options(NAME "remdesk" TYPE "static" + DESCRIPTION "Remote Assistance Virtual Channel Extension" + SPECIFICATIONS "[MS-RA]" + DEFAULT ${OPTION_DEFAULT}) + +define_channel_client_options(${OPTION_CLIENT_DEFAULT}) +define_channel_server_options(${OPTION_SERVER_DEFAULT}) diff --git a/channels/remdesk/client/CMakeLists.txt b/channels/remdesk/client/CMakeLists.txt new file mode 100644 index 0000000..bb66f6e --- /dev/null +++ b/channels/remdesk/client/CMakeLists.txt @@ -0,0 +1,31 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2012 Marc-Andre Moreau +# +# Licensed 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. + +define_channel_client("remdesk") + +set(${MODULE_PREFIX}_SRCS + remdesk_main.c + remdesk_main.h) + +add_channel_client_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} FALSE "VirtualChannelEntryEx") + + + +set(${MODULE_PREFIX}_LIBS ${${MODULE_PREFIX}_LIBS} winpr freerdp) + + +set_property(TARGET ${MODULE_NAME} PROPERTY FOLDER "Channels/${CHANNEL_NAME}/Client") diff --git a/channels/remdesk/client/remdesk_main.c b/channels/remdesk/client/remdesk_main.c new file mode 100644 index 0000000..54d9c60 --- /dev/null +++ b/channels/remdesk/client/remdesk_main.c @@ -0,0 +1,1071 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Remote Assistance Virtual Channel + * + * Copyright 2014 Marc-Andre Moreau + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include + +#include + +#include +#include + +#include "remdesk_main.h" + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT remdesk_virtual_channel_write(remdeskPlugin* remdesk, wStream* s) +{ + UINT32 status; + + if (!remdesk) + { + WLog_ERR(TAG, "remdesk was null!"); + Stream_Free(s, TRUE); + return CHANNEL_RC_INVALID_INSTANCE; + } + + status = remdesk->channelEntryPoints.pVirtualChannelWriteEx( + remdesk->InitHandle, remdesk->OpenHandle, Stream_Buffer(s), (UINT32)Stream_Length(s), s); + + if (status != CHANNEL_RC_OK) + { + Stream_Free(s, TRUE); + WLog_ERR(TAG, "pVirtualChannelWriteEx failed with %s [%08" PRIX32 "]", + WTSErrorToString(status), status); + } + return status; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT remdesk_generate_expert_blob(remdeskPlugin* remdesk) +{ + char* name; + char* pass; + char* password; + rdpSettings* settings = remdesk->settings; + + if (remdesk->ExpertBlob) + return CHANNEL_RC_OK; + + if (settings->RemoteAssistancePassword) + password = settings->RemoteAssistancePassword; + else + password = settings->Password; + + if (!password) + { + WLog_ERR(TAG, "password was not set!"); + return ERROR_INTERNAL_ERROR; + } + + name = settings->Username; + + if (!name) + name = "Expert"; + + remdesk->EncryptedPassStub = freerdp_assistance_encrypt_pass_stub( + password, settings->RemoteAssistancePassStub, &(remdesk->EncryptedPassStubSize)); + + if (!remdesk->EncryptedPassStub) + { + WLog_ERR(TAG, "freerdp_assistance_encrypt_pass_stub failed!"); + return ERROR_INTERNAL_ERROR; + } + + pass = freerdp_assistance_bin_to_hex_string(remdesk->EncryptedPassStub, + remdesk->EncryptedPassStubSize); + + if (!pass) + { + WLog_ERR(TAG, "freerdp_assistance_bin_to_hex_string failed!"); + return ERROR_INTERNAL_ERROR; + } + + remdesk->ExpertBlob = freerdp_assistance_construct_expert_blob(name, pass); + free(pass); + + if (!remdesk->ExpertBlob) + { + WLog_ERR(TAG, "freerdp_assistance_construct_expert_blob failed!"); + return ERROR_INTERNAL_ERROR; + } + + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT remdesk_read_channel_header(wStream* s, REMDESK_CHANNEL_HEADER* header) +{ + int status; + UINT32 ChannelNameLen; + char* pChannelName = NULL; + + if (Stream_GetRemainingLength(s) < 8) + { + WLog_ERR(TAG, "Not enough data!"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT32(s, ChannelNameLen); /* ChannelNameLen (4 bytes) */ + Stream_Read_UINT32(s, header->DataLength); /* DataLen (4 bytes) */ + + if (ChannelNameLen > 64) + { + WLog_ERR(TAG, "ChannelNameLen > 64!"); + return ERROR_INVALID_DATA; + } + + if ((ChannelNameLen % 2) != 0) + { + WLog_ERR(TAG, "ChannelNameLen %% 2) != 0 "); + return ERROR_INVALID_DATA; + } + + if (Stream_GetRemainingLength(s) < ChannelNameLen) + { + WLog_ERR(TAG, "Not enough data!"); + return ERROR_INVALID_DATA; + } + + ZeroMemory(header->ChannelName, sizeof(header->ChannelName)); + pChannelName = (char*)header->ChannelName; + status = ConvertFromUnicode(CP_UTF8, 0, (WCHAR*)Stream_Pointer(s), ChannelNameLen / 2, + &pChannelName, 32, NULL, NULL); + Stream_Seek(s, ChannelNameLen); + + if (status <= 0) + { + WLog_ERR(TAG, "ConvertFromUnicode failed!"); + return ERROR_INTERNAL_ERROR; + } + + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT remdesk_write_channel_header(wStream* s, REMDESK_CHANNEL_HEADER* header) +{ + int index; + UINT32 ChannelNameLen; + WCHAR ChannelNameW[32]; + ZeroMemory(ChannelNameW, sizeof(ChannelNameW)); + + for (index = 0; index < 32; index++) + { + ChannelNameW[index] = (WCHAR)header->ChannelName[index]; + } + + ChannelNameLen = (strnlen(header->ChannelName, sizeof(header->ChannelName)) + 1) * 2; + Stream_Write_UINT32(s, ChannelNameLen); /* ChannelNameLen (4 bytes) */ + Stream_Write_UINT32(s, header->DataLength); /* DataLen (4 bytes) */ + Stream_Write(s, ChannelNameW, ChannelNameLen); /* ChannelName (variable) */ + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT remdesk_write_ctl_header(wStream* s, REMDESK_CTL_HEADER* ctlHeader) +{ + remdesk_write_channel_header(s, (REMDESK_CHANNEL_HEADER*)ctlHeader); + Stream_Write_UINT32(s, ctlHeader->msgType); /* msgType (4 bytes) */ + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT remdesk_prepare_ctl_header(REMDESK_CTL_HEADER* ctlHeader, UINT32 msgType, + UINT32 msgSize) +{ + ctlHeader->msgType = msgType; + sprintf_s(ctlHeader->ChannelName, ARRAYSIZE(ctlHeader->ChannelName), REMDESK_CHANNEL_CTL_NAME); + ctlHeader->DataLength = 4 + msgSize; + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT remdesk_recv_ctl_server_announce_pdu(remdeskPlugin* remdesk, wStream* s, + REMDESK_CHANNEL_HEADER* header) +{ + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT remdesk_recv_ctl_version_info_pdu(remdeskPlugin* remdesk, wStream* s, + REMDESK_CHANNEL_HEADER* header) +{ + UINT32 versionMajor; + UINT32 versionMinor; + + if (Stream_GetRemainingLength(s) < 8) + { + WLog_ERR(TAG, "Not enough data!"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT32(s, versionMajor); /* versionMajor (4 bytes) */ + Stream_Read_UINT32(s, versionMinor); /* versionMinor (4 bytes) */ + + if ((versionMajor != 1) || (versionMinor > 2) || (versionMinor == 0)) + { + WLog_ERR(TAG, "Unsupported protocol version %" PRId32 ".%" PRId32, versionMajor, + versionMinor); + } + + remdesk->Version = versionMinor; + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT remdesk_send_ctl_version_info_pdu(remdeskPlugin* remdesk) +{ + wStream* s; + REMDESK_CTL_VERSION_INFO_PDU pdu; + UINT error; + remdesk_prepare_ctl_header(&(pdu.ctlHeader), REMDESK_CTL_VERSIONINFO, 8); + pdu.versionMajor = 1; + pdu.versionMinor = 2; + s = Stream_New(NULL, REMDESK_CHANNEL_CTL_SIZE + pdu.ctlHeader.DataLength); + + if (!s) + { + WLog_ERR(TAG, "Stream_New failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + remdesk_write_ctl_header(s, &(pdu.ctlHeader)); + Stream_Write_UINT32(s, pdu.versionMajor); /* versionMajor (4 bytes) */ + Stream_Write_UINT32(s, pdu.versionMinor); /* versionMinor (4 bytes) */ + Stream_SealLength(s); + + if ((error = remdesk_virtual_channel_write(remdesk, s))) + WLog_ERR(TAG, "remdesk_virtual_channel_write failed with error %" PRIu32 "!", error); + + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT remdesk_recv_ctl_result_pdu(remdeskPlugin* remdesk, wStream* s, + REMDESK_CHANNEL_HEADER* header, UINT32* pResult) +{ + UINT32 result; + + if (Stream_GetRemainingLength(s) < 4) + { + WLog_ERR(TAG, "Not enough data!"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT32(s, result); /* result (4 bytes) */ + *pResult = result; + // WLog_DBG(TAG, "RemdeskRecvResult: 0x%08"PRIX32"", result); + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT remdesk_send_ctl_authenticate_pdu(remdeskPlugin* remdesk) +{ + int status; + UINT error; + wStream* s = NULL; + int cbExpertBlobW = 0; + WCHAR* expertBlobW = NULL; + int cbRaConnectionStringW = 0; + WCHAR* raConnectionStringW = NULL; + REMDESK_CTL_AUTHENTICATE_PDU pdu; + + if ((error = remdesk_generate_expert_blob(remdesk))) + { + WLog_ERR(TAG, "remdesk_generate_expert_blob failed with error %" PRIu32 "", error); + return error; + } + + pdu.expertBlob = remdesk->ExpertBlob; + pdu.raConnectionString = remdesk->settings->RemoteAssistanceRCTicket; + status = ConvertToUnicode(CP_UTF8, 0, pdu.raConnectionString, -1, &raConnectionStringW, 0); + + if (status <= 0) + { + WLog_ERR(TAG, "ConvertToUnicode failed!"); + return ERROR_INTERNAL_ERROR; + } + + cbRaConnectionStringW = status * 2; + status = ConvertToUnicode(CP_UTF8, 0, pdu.expertBlob, -1, &expertBlobW, 0); + + if (status <= 0) + { + WLog_ERR(TAG, "ConvertToUnicode failed!"); + error = ERROR_INTERNAL_ERROR; + goto out; + } + + cbExpertBlobW = status * 2; + remdesk_prepare_ctl_header(&(pdu.ctlHeader), REMDESK_CTL_AUTHENTICATE, + cbRaConnectionStringW + cbExpertBlobW); + s = Stream_New(NULL, REMDESK_CHANNEL_CTL_SIZE + pdu.ctlHeader.DataLength); + + if (!s) + { + WLog_ERR(TAG, "Stream_New failed!"); + error = CHANNEL_RC_NO_MEMORY; + goto out; + } + + remdesk_write_ctl_header(s, &(pdu.ctlHeader)); + Stream_Write(s, (BYTE*)raConnectionStringW, cbRaConnectionStringW); + Stream_Write(s, (BYTE*)expertBlobW, cbExpertBlobW); + Stream_SealLength(s); + + if ((error = remdesk_virtual_channel_write(remdesk, s))) + WLog_ERR(TAG, "remdesk_virtual_channel_write failed with error %" PRIu32 "!", error); + +out: + free(raConnectionStringW); + free(expertBlobW); + + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT remdesk_send_ctl_remote_control_desktop_pdu(remdeskPlugin* remdesk) +{ + int status; + UINT error; + wStream* s = NULL; + int cbRaConnectionStringW = 0; + WCHAR* raConnectionStringW = NULL; + REMDESK_CTL_REMOTE_CONTROL_DESKTOP_PDU pdu; + pdu.raConnectionString = remdesk->settings->RemoteAssistanceRCTicket; + status = ConvertToUnicode(CP_UTF8, 0, pdu.raConnectionString, -1, &raConnectionStringW, 0); + + if (status <= 0) + { + WLog_ERR(TAG, "ConvertToUnicode failed!"); + return ERROR_INTERNAL_ERROR; + } + + cbRaConnectionStringW = status * 2; + remdesk_prepare_ctl_header(&(pdu.ctlHeader), REMDESK_CTL_REMOTE_CONTROL_DESKTOP, + cbRaConnectionStringW); + s = Stream_New(NULL, REMDESK_CHANNEL_CTL_SIZE + pdu.ctlHeader.DataLength); + + if (!s) + { + WLog_ERR(TAG, "Stream_New failed!"); + error = CHANNEL_RC_NO_MEMORY; + goto out; + } + + remdesk_write_ctl_header(s, &(pdu.ctlHeader)); + Stream_Write(s, (BYTE*)raConnectionStringW, cbRaConnectionStringW); + Stream_SealLength(s); + + if ((error = remdesk_virtual_channel_write(remdesk, s))) + WLog_ERR(TAG, "remdesk_virtual_channel_write failed with error %" PRIu32 "!", error); + +out: + free(raConnectionStringW); + + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT remdesk_send_ctl_verify_password_pdu(remdeskPlugin* remdesk) +{ + int status; + UINT error; + wStream* s; + int cbExpertBlobW = 0; + WCHAR* expertBlobW = NULL; + REMDESK_CTL_VERIFY_PASSWORD_PDU pdu; + + if ((error = remdesk_generate_expert_blob(remdesk))) + { + WLog_ERR(TAG, "remdesk_generate_expert_blob failed with error %" PRIu32 "!", error); + return error; + } + + pdu.expertBlob = remdesk->ExpertBlob; + status = ConvertToUnicode(CP_UTF8, 0, pdu.expertBlob, -1, &expertBlobW, 0); + + if (status <= 0) + { + WLog_ERR(TAG, "ConvertToUnicode failed!"); + return ERROR_INTERNAL_ERROR; + } + + cbExpertBlobW = status * 2; + remdesk_prepare_ctl_header(&(pdu.ctlHeader), REMDESK_CTL_VERIFY_PASSWORD, cbExpertBlobW); + s = Stream_New(NULL, REMDESK_CHANNEL_CTL_SIZE + pdu.ctlHeader.DataLength); + + if (!s) + { + WLog_ERR(TAG, "Stream_New failed!"); + error = CHANNEL_RC_NO_MEMORY; + goto out; + } + + remdesk_write_ctl_header(s, &(pdu.ctlHeader)); + Stream_Write(s, (BYTE*)expertBlobW, cbExpertBlobW); + Stream_SealLength(s); + + if ((error = remdesk_virtual_channel_write(remdesk, s))) + WLog_ERR(TAG, "remdesk_virtual_channel_write failed with error %" PRIu32 "!", error); + +out: + free(expertBlobW); + + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT remdesk_send_ctl_expert_on_vista_pdu(remdeskPlugin* remdesk) +{ + UINT error; + wStream* s; + REMDESK_CTL_EXPERT_ON_VISTA_PDU pdu; + + if ((error = remdesk_generate_expert_blob(remdesk))) + { + WLog_ERR(TAG, "remdesk_generate_expert_blob failed with error %" PRIu32 "!", error); + return error; + } + + pdu.EncryptedPasswordLength = remdesk->EncryptedPassStubSize; + pdu.EncryptedPassword = remdesk->EncryptedPassStub; + remdesk_prepare_ctl_header(&(pdu.ctlHeader), REMDESK_CTL_EXPERT_ON_VISTA, + pdu.EncryptedPasswordLength); + s = Stream_New(NULL, REMDESK_CHANNEL_CTL_SIZE + pdu.ctlHeader.DataLength); + + if (!s) + { + WLog_ERR(TAG, "Stream_New failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + remdesk_write_ctl_header(s, &(pdu.ctlHeader)); + Stream_Write(s, pdu.EncryptedPassword, pdu.EncryptedPasswordLength); + Stream_SealLength(s); + return remdesk_virtual_channel_write(remdesk, s); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT remdesk_recv_ctl_pdu(remdeskPlugin* remdesk, wStream* s, REMDESK_CHANNEL_HEADER* header) +{ + UINT error = CHANNEL_RC_OK; + UINT32 msgType = 0; + UINT32 result = 0; + + if (Stream_GetRemainingLength(s) < 4) + { + WLog_ERR(TAG, "Not enough data!"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT32(s, msgType); /* msgType (4 bytes) */ + + // WLog_DBG(TAG, "msgType: %"PRIu32"", msgType); + + switch (msgType) + { + case REMDESK_CTL_REMOTE_CONTROL_DESKTOP: + break; + + case REMDESK_CTL_RESULT: + if ((error = remdesk_recv_ctl_result_pdu(remdesk, s, header, &result))) + WLog_ERR(TAG, "remdesk_recv_ctl_result_pdu failed with error %" PRIu32 "", error); + + break; + + case REMDESK_CTL_AUTHENTICATE: + break; + + case REMDESK_CTL_SERVER_ANNOUNCE: + if ((error = remdesk_recv_ctl_server_announce_pdu(remdesk, s, header))) + WLog_ERR(TAG, "remdesk_recv_ctl_server_announce_pdu failed with error %" PRIu32 "", + error); + + break; + + case REMDESK_CTL_DISCONNECT: + break; + + case REMDESK_CTL_VERSIONINFO: + if ((error = remdesk_recv_ctl_version_info_pdu(remdesk, s, header))) + { + WLog_ERR(TAG, "remdesk_recv_ctl_version_info_pdu failed with error %" PRIu32 "", + error); + break; + } + + if (remdesk->Version == 1) + { + if ((error = remdesk_send_ctl_version_info_pdu(remdesk))) + { + WLog_ERR(TAG, "remdesk_send_ctl_version_info_pdu failed with error %" PRIu32 "", + error); + break; + } + + if ((error = remdesk_send_ctl_authenticate_pdu(remdesk))) + { + WLog_ERR(TAG, "remdesk_send_ctl_authenticate_pdu failed with error %" PRIu32 "", + error); + break; + } + + if ((error = remdesk_send_ctl_remote_control_desktop_pdu(remdesk))) + { + WLog_ERR( + TAG, + "remdesk_send_ctl_remote_control_desktop_pdu failed with error %" PRIu32 "", + error); + break; + } + } + else if (remdesk->Version == 2) + { + if ((error = remdesk_send_ctl_expert_on_vista_pdu(remdesk))) + { + WLog_ERR(TAG, + "remdesk_send_ctl_expert_on_vista_pdu failed with error %" PRIu32 "", + error); + break; + } + + if ((error = remdesk_send_ctl_verify_password_pdu(remdesk))) + { + WLog_ERR(TAG, + "remdesk_send_ctl_verify_password_pdu failed with error %" PRIu32 "", + error); + break; + } + } + + break; + + case REMDESK_CTL_ISCONNECTED: + break; + + case REMDESK_CTL_VERIFY_PASSWORD: + break; + + case REMDESK_CTL_EXPERT_ON_VISTA: + break; + + case REMDESK_CTL_RANOVICE_NAME: + break; + + case REMDESK_CTL_RAEXPERT_NAME: + break; + + case REMDESK_CTL_TOKEN: + break; + + default: + WLog_ERR(TAG, "unknown msgType: %" PRIu32 "", msgType); + error = ERROR_INVALID_DATA; + break; + } + + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT remdesk_process_receive(remdeskPlugin* remdesk, wStream* s) +{ + UINT status; + REMDESK_CHANNEL_HEADER header; +#if 0 + WLog_DBG(TAG, "RemdeskReceive: %"PRIuz"", Stream_GetRemainingLength(s)); + winpr_HexDump(Stream_Pointer(s), Stream_GetRemainingLength(s)); +#endif + + if ((status = remdesk_read_channel_header(s, &header))) + { + WLog_ERR(TAG, "remdesk_read_channel_header failed with error %" PRIu32 "", status); + return status; + } + + if (strcmp(header.ChannelName, "RC_CTL") == 0) + { + status = remdesk_recv_ctl_pdu(remdesk, s, &header); + } + else if (strcmp(header.ChannelName, "70") == 0) + { + } + else if (strcmp(header.ChannelName, "71") == 0) + { + } + else if (strcmp(header.ChannelName, ".") == 0) + { + } + else if (strcmp(header.ChannelName, "1000.") == 0) + { + } + else if (strcmp(header.ChannelName, "RA_FX") == 0) + { + } + else + { + } + + return status; +} + +static void remdesk_process_connect(remdeskPlugin* remdesk) +{ + remdesk->settings = (rdpSettings*)remdesk->channelEntryPoints.pExtendedData; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT remdesk_virtual_channel_event_data_received(remdeskPlugin* remdesk, const void* pData, + UINT32 dataLength, UINT32 totalLength, + UINT32 dataFlags) +{ + wStream* data_in; + + if ((dataFlags & CHANNEL_FLAG_SUSPEND) || (dataFlags & CHANNEL_FLAG_RESUME)) + { + return CHANNEL_RC_OK; + } + + if (dataFlags & CHANNEL_FLAG_FIRST) + { + if (remdesk->data_in) + Stream_Free(remdesk->data_in, TRUE); + + remdesk->data_in = Stream_New(NULL, totalLength); + + if (!remdesk->data_in) + { + WLog_ERR(TAG, "Stream_New failed!"); + return CHANNEL_RC_NO_MEMORY; + } + } + + data_in = remdesk->data_in; + + if (!Stream_EnsureRemainingCapacity(data_in, dataLength)) + { + WLog_ERR(TAG, "Stream_EnsureRemainingCapacity failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + Stream_Write(data_in, pData, dataLength); + + if (dataFlags & CHANNEL_FLAG_LAST) + { + if (Stream_Capacity(data_in) != Stream_GetPosition(data_in)) + { + WLog_ERR(TAG, "read error"); + return ERROR_INTERNAL_ERROR; + } + + remdesk->data_in = NULL; + Stream_SealLength(data_in); + Stream_SetPosition(data_in, 0); + + if (!MessageQueue_Post(remdesk->queue, NULL, 0, (void*)data_in, NULL)) + { + WLog_ERR(TAG, "MessageQueue_Post failed!"); + return ERROR_INTERNAL_ERROR; + } + } + + return CHANNEL_RC_OK; +} + +static VOID VCAPITYPE remdesk_virtual_channel_open_event_ex(LPVOID lpUserParam, DWORD openHandle, + UINT event, LPVOID pData, + UINT32 dataLength, UINT32 totalLength, + UINT32 dataFlags) +{ + UINT error = CHANNEL_RC_OK; + remdeskPlugin* remdesk = (remdeskPlugin*)lpUserParam; + + switch (event) + { + case CHANNEL_EVENT_DATA_RECEIVED: + if (!remdesk || (remdesk->OpenHandle != openHandle)) + { + WLog_ERR(TAG, "error no match"); + return; + } + if ((error = remdesk_virtual_channel_event_data_received(remdesk, pData, dataLength, + totalLength, dataFlags))) + WLog_ERR(TAG, + "remdesk_virtual_channel_event_data_received failed with error %" PRIu32 + "!", + error); + + break; + + case CHANNEL_EVENT_WRITE_CANCELLED: + case CHANNEL_EVENT_WRITE_COMPLETE: + { + wStream* s = (wStream*)pData; + Stream_Free(s, TRUE); + } + break; + + case CHANNEL_EVENT_USER: + break; + + default: + WLog_ERR(TAG, "unhandled event %" PRIu32 "!", event); + error = ERROR_INTERNAL_ERROR; + } + + if (error && remdesk && remdesk->rdpcontext) + setChannelError(remdesk->rdpcontext, error, + "remdesk_virtual_channel_open_event_ex reported an error"); +} + +static DWORD WINAPI remdesk_virtual_channel_client_thread(LPVOID arg) +{ + wStream* data; + wMessage message; + remdeskPlugin* remdesk = (remdeskPlugin*)arg; + UINT error = CHANNEL_RC_OK; + remdesk_process_connect(remdesk); + + while (1) + { + if (!MessageQueue_Wait(remdesk->queue)) + { + WLog_ERR(TAG, "MessageQueue_Wait failed!"); + error = ERROR_INTERNAL_ERROR; + break; + } + + if (!MessageQueue_Peek(remdesk->queue, &message, TRUE)) + { + WLog_ERR(TAG, "MessageQueue_Peek failed!"); + error = ERROR_INTERNAL_ERROR; + break; + } + + if (message.id == WMQ_QUIT) + break; + + if (message.id == 0) + { + data = (wStream*)message.wParam; + + if ((error = remdesk_process_receive(remdesk, data))) + { + WLog_ERR(TAG, "remdesk_process_receive failed with error %" PRIu32 "!", error); + Stream_Free(data, TRUE); + break; + } + + Stream_Free(data, TRUE); + } + } + + if (error && remdesk->rdpcontext) + setChannelError(remdesk->rdpcontext, error, + "remdesk_virtual_channel_client_thread reported an error"); + + ExitThread(error); + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT remdesk_virtual_channel_event_connected(remdeskPlugin* remdesk, LPVOID pData, + UINT32 dataLength) +{ + UINT32 status; + UINT error; + status = remdesk->channelEntryPoints.pVirtualChannelOpenEx( + remdesk->InitHandle, &remdesk->OpenHandle, remdesk->channelDef.name, + remdesk_virtual_channel_open_event_ex); + + if (status != CHANNEL_RC_OK) + { + WLog_ERR(TAG, "pVirtualChannelOpenEx failed with %s [%08" PRIX32 "]", + WTSErrorToString(status), status); + return status; + } + + remdesk->queue = MessageQueue_New(NULL); + + if (!remdesk->queue) + { + WLog_ERR(TAG, "MessageQueue_New failed!"); + error = CHANNEL_RC_NO_MEMORY; + goto error_out; + } + + remdesk->thread = + CreateThread(NULL, 0, remdesk_virtual_channel_client_thread, (void*)remdesk, 0, NULL); + + if (!remdesk->thread) + { + WLog_ERR(TAG, "CreateThread failed"); + error = ERROR_INTERNAL_ERROR; + goto error_out; + } + + return CHANNEL_RC_OK; +error_out: + MessageQueue_Free(remdesk->queue); + remdesk->queue = NULL; + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT remdesk_virtual_channel_event_disconnected(remdeskPlugin* remdesk) +{ + UINT rc; + + if (remdesk->OpenHandle == 0) + return CHANNEL_RC_OK; + + if (MessageQueue_PostQuit(remdesk->queue, 0) && + (WaitForSingleObject(remdesk->thread, INFINITE) == WAIT_FAILED)) + { + rc = GetLastError(); + WLog_ERR(TAG, "WaitForSingleObject failed with error %" PRIu32 "", rc); + return rc; + } + + MessageQueue_Free(remdesk->queue); + CloseHandle(remdesk->thread); + remdesk->queue = NULL; + remdesk->thread = NULL; + rc = remdesk->channelEntryPoints.pVirtualChannelCloseEx(remdesk->InitHandle, + remdesk->OpenHandle); + + if (CHANNEL_RC_OK != rc) + { + WLog_ERR(TAG, "pVirtualChannelCloseEx failed with %s [%08" PRIX32 "]", WTSErrorToString(rc), + rc); + } + + remdesk->OpenHandle = 0; + + if (remdesk->data_in) + { + Stream_Free(remdesk->data_in, TRUE); + remdesk->data_in = NULL; + } + + return rc; +} + +static void remdesk_virtual_channel_event_terminated(remdeskPlugin* remdesk) +{ + remdesk->InitHandle = 0; + free(remdesk->context); + free(remdesk); +} + +static VOID VCAPITYPE remdesk_virtual_channel_init_event_ex(LPVOID lpUserParam, LPVOID pInitHandle, + UINT event, LPVOID pData, + UINT dataLength) +{ + UINT error = CHANNEL_RC_OK; + remdeskPlugin* remdesk = (remdeskPlugin*)lpUserParam; + + if (!remdesk || (remdesk->InitHandle != pInitHandle)) + { + WLog_ERR(TAG, "error no match"); + return; + } + + switch (event) + { + case CHANNEL_EVENT_CONNECTED: + if ((error = remdesk_virtual_channel_event_connected(remdesk, pData, dataLength))) + WLog_ERR(TAG, + "remdesk_virtual_channel_event_connected failed with error %" PRIu32 "", + error); + + break; + + case CHANNEL_EVENT_DISCONNECTED: + if ((error = remdesk_virtual_channel_event_disconnected(remdesk))) + WLog_ERR(TAG, + "remdesk_virtual_channel_event_disconnected failed with error %" PRIu32 "", + error); + + break; + + case CHANNEL_EVENT_TERMINATED: + remdesk_virtual_channel_event_terminated(remdesk); + break; + + case CHANNEL_EVENT_ATTACHED: + case CHANNEL_EVENT_DETACHED: + default: + break; + } + + if (error && remdesk->rdpcontext) + setChannelError(remdesk->rdpcontext, error, + "remdesk_virtual_channel_init_event reported an error"); +} + +/* remdesk is always built-in */ +#define VirtualChannelEntryEx remdesk_VirtualChannelEntryEx + +BOOL VCAPITYPE VirtualChannelEntryEx(PCHANNEL_ENTRY_POINTS pEntryPoints, PVOID pInitHandle) +{ + UINT rc; + remdeskPlugin* remdesk; + RemdeskClientContext* context = NULL; + CHANNEL_ENTRY_POINTS_FREERDP_EX* pEntryPointsEx; + + if (!pEntryPoints) + { + return FALSE; + } + + remdesk = (remdeskPlugin*)calloc(1, sizeof(remdeskPlugin)); + + if (!remdesk) + { + WLog_ERR(TAG, "calloc failed!"); + return FALSE; + } + + remdesk->channelDef.options = CHANNEL_OPTION_INITIALIZED | CHANNEL_OPTION_ENCRYPT_RDP | + CHANNEL_OPTION_COMPRESS_RDP | CHANNEL_OPTION_SHOW_PROTOCOL; + sprintf_s(remdesk->channelDef.name, ARRAYSIZE(remdesk->channelDef.name), "remdesk"); + remdesk->Version = 2; + pEntryPointsEx = (CHANNEL_ENTRY_POINTS_FREERDP_EX*)pEntryPoints; + + if ((pEntryPointsEx->cbSize >= sizeof(CHANNEL_ENTRY_POINTS_FREERDP_EX)) && + (pEntryPointsEx->MagicNumber == FREERDP_CHANNEL_MAGIC_NUMBER)) + { + context = (RemdeskClientContext*)calloc(1, sizeof(RemdeskClientContext)); + + if (!context) + { + WLog_ERR(TAG, "calloc failed!"); + goto error_out; + } + + context->handle = (void*)remdesk; + remdesk->context = context; + remdesk->rdpcontext = pEntryPointsEx->context; + } + + CopyMemory(&(remdesk->channelEntryPoints), pEntryPoints, + sizeof(CHANNEL_ENTRY_POINTS_FREERDP_EX)); + remdesk->InitHandle = pInitHandle; + rc = remdesk->channelEntryPoints.pVirtualChannelInitEx( + remdesk, context, pInitHandle, &remdesk->channelDef, 1, VIRTUAL_CHANNEL_VERSION_WIN2000, + remdesk_virtual_channel_init_event_ex); + + if (CHANNEL_RC_OK != rc) + { + WLog_ERR(TAG, "pVirtualChannelInitEx failed with %s [%08" PRIX32 "]", WTSErrorToString(rc), + rc); + goto error_out; + } + + remdesk->channelEntryPoints.pInterface = context; + return TRUE; +error_out: + free(remdesk); + free(context); + return FALSE; +} diff --git a/channels/remdesk/client/remdesk_main.h b/channels/remdesk/client/remdesk_main.h new file mode 100644 index 0000000..852e18e --- /dev/null +++ b/channels/remdesk/client/remdesk_main.h @@ -0,0 +1,63 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Remote Assistance Virtual Channel + * + * Copyright 2014 Marc-Andre Moreau + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger + * + * Licensed 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 FREERDP_CHANNEL_REMDESK_CLIENT_MAIN_H +#define FREERDP_CHANNEL_REMDESK_CLIENT_MAIN_H + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include + +#include +#define TAG CHANNELS_TAG("remdesk.client") + +struct remdesk_plugin +{ + CHANNEL_DEF channelDef; + CHANNEL_ENTRY_POINTS_FREERDP_EX channelEntryPoints; + + RemdeskClientContext* context; + + HANDLE thread; + wStream* data_in; + void* InitHandle; + DWORD OpenHandle; + rdpSettings* settings; + wMessageQueue* queue; + + UINT32 Version; + char* ExpertBlob; + BYTE* EncryptedPassStub; + size_t EncryptedPassStubSize; + rdpContext* rdpcontext; +}; +typedef struct remdesk_plugin remdeskPlugin; + +#endif /* FREERDP_CHANNEL_REMDESK_CLIENT_MAIN_H */ diff --git a/channels/remdesk/server/CMakeLists.txt b/channels/remdesk/server/CMakeLists.txt new file mode 100644 index 0000000..dc59a11 --- /dev/null +++ b/channels/remdesk/server/CMakeLists.txt @@ -0,0 +1,31 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2012 Marc-Andre Moreau +# +# Licensed 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. + +define_channel_server("remdesk") + +set(${MODULE_PREFIX}_SRCS + remdesk_main.c + remdesk_main.h) + +add_channel_server_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} FALSE "VirtualChannelEntry") + +set(${MODULE_PREFIX}_LIBS ${${MODULE_PREFIX}_LIBS} winpr) + +target_link_libraries(${MODULE_NAME} ${${MODULE_PREFIX}_LIBS}) + + +set_property(TARGET ${MODULE_NAME} PROPERTY FOLDER "Channels/${CHANNEL_NAME}/Server") diff --git a/channels/remdesk/server/remdesk_main.c b/channels/remdesk/server/remdesk_main.c new file mode 100644 index 0000000..aeaa332 --- /dev/null +++ b/channels/remdesk/server/remdesk_main.c @@ -0,0 +1,787 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Remote Assistance Virtual Channel + * + * Copyright 2014 Marc-Andre Moreau + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include + +#include "remdesk_main.h" + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT remdesk_virtual_channel_write(RemdeskServerContext* context, wStream* s) +{ + BOOL status; + ULONG BytesWritten = 0; + status = WTSVirtualChannelWrite(context->priv->ChannelHandle, (PCHAR)Stream_Buffer(s), + Stream_Length(s), &BytesWritten); + return (status) ? CHANNEL_RC_OK : ERROR_INTERNAL_ERROR; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT remdesk_read_channel_header(wStream* s, REMDESK_CHANNEL_HEADER* header) +{ + int status; + UINT32 ChannelNameLen; + char* pChannelName = NULL; + + if (Stream_GetRemainingLength(s) < 8) + { + WLog_ERR(TAG, "Stream_GetRemainingLength failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + Stream_Read_UINT32(s, ChannelNameLen); /* ChannelNameLen (4 bytes) */ + Stream_Read_UINT32(s, header->DataLength); /* DataLen (4 bytes) */ + + if (ChannelNameLen > 64) + { + WLog_ERR(TAG, "ChannelNameLen > 64!"); + return ERROR_INVALID_DATA; + } + + if ((ChannelNameLen % 2) != 0) + { + WLog_ERR(TAG, "(ChannelNameLen %% 2) != 0!"); + return ERROR_INVALID_DATA; + } + + if (Stream_GetRemainingLength(s) < ChannelNameLen) + { + WLog_ERR(TAG, "Stream_GetRemainingLength failed!"); + return ERROR_INVALID_DATA; + } + + ZeroMemory(header->ChannelName, sizeof(header->ChannelName)); + pChannelName = (char*)header->ChannelName; + status = ConvertFromUnicode(CP_UTF8, 0, (WCHAR*)Stream_Pointer(s), ChannelNameLen / 2, + &pChannelName, 32, NULL, NULL); + Stream_Seek(s, ChannelNameLen); + + if (status <= 0) + { + WLog_ERR(TAG, "ConvertFromUnicode failed!"); + return ERROR_INVALID_DATA; + } + + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT remdesk_write_channel_header(wStream* s, REMDESK_CHANNEL_HEADER* header) +{ + int index; + UINT32 ChannelNameLen; + WCHAR ChannelNameW[32]; + ZeroMemory(ChannelNameW, sizeof(ChannelNameW)); + + for (index = 0; index < 32; index++) + { + ChannelNameW[index] = (WCHAR)header->ChannelName[index]; + } + + ChannelNameLen = (strnlen(header->ChannelName, sizeof(header->ChannelName)) + 1) * 2; + Stream_Write_UINT32(s, ChannelNameLen); /* ChannelNameLen (4 bytes) */ + Stream_Write_UINT32(s, header->DataLength); /* DataLen (4 bytes) */ + Stream_Write(s, ChannelNameW, ChannelNameLen); /* ChannelName (variable) */ + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT remdesk_write_ctl_header(wStream* s, REMDESK_CTL_HEADER* ctlHeader) +{ + UINT error; + + if ((error = remdesk_write_channel_header(s, (REMDESK_CHANNEL_HEADER*)ctlHeader))) + { + WLog_ERR(TAG, "remdesk_write_channel_header failed with error %" PRIu32 "!", error); + return error; + } + + Stream_Write_UINT32(s, ctlHeader->msgType); /* msgType (4 bytes) */ + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT remdesk_prepare_ctl_header(REMDESK_CTL_HEADER* ctlHeader, UINT32 msgType, + UINT32 msgSize) +{ + ctlHeader->msgType = msgType; + sprintf_s(ctlHeader->ChannelName, ARRAYSIZE(ctlHeader->ChannelName), REMDESK_CHANNEL_CTL_NAME); + ctlHeader->DataLength = 4 + msgSize; + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT remdesk_send_ctl_result_pdu(RemdeskServerContext* context, UINT32 result) +{ + wStream* s; + REMDESK_CTL_RESULT_PDU pdu; + UINT error; + pdu.result = result; + + if ((error = remdesk_prepare_ctl_header(&(pdu.ctlHeader), REMDESK_CTL_RESULT, 4))) + { + WLog_ERR(TAG, "remdesk_prepare_ctl_header failed with error %" PRIu32 "!", error); + return error; + } + + s = Stream_New(NULL, REMDESK_CHANNEL_CTL_SIZE + pdu.ctlHeader.DataLength); + + if (!s) + { + WLog_ERR(TAG, "Stream_New failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + if ((error = remdesk_write_ctl_header(s, &(pdu.ctlHeader)))) + { + WLog_ERR(TAG, "remdesk_write_ctl_header failed with error %" PRIu32 "!", error); + goto out; + } + + Stream_Write_UINT32(s, pdu.result); /* result (4 bytes) */ + Stream_SealLength(s); + + if ((error = remdesk_virtual_channel_write(context, s))) + WLog_ERR(TAG, "remdesk_virtual_channel_write failed with error %" PRIu32 "!", error); + +out: + Stream_Free(s, TRUE); + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT remdesk_send_ctl_version_info_pdu(RemdeskServerContext* context) +{ + wStream* s; + REMDESK_CTL_VERSION_INFO_PDU pdu; + UINT error; + + if ((error = remdesk_prepare_ctl_header(&(pdu.ctlHeader), REMDESK_CTL_VERSIONINFO, 8))) + { + WLog_ERR(TAG, "remdesk_prepare_ctl_header failed with error %" PRIu32 "!", error); + return error; + } + + pdu.versionMajor = 1; + pdu.versionMinor = 2; + s = Stream_New(NULL, REMDESK_CHANNEL_CTL_SIZE + pdu.ctlHeader.DataLength); + + if (!s) + { + WLog_ERR(TAG, "Stream_New failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + if ((error = remdesk_write_ctl_header(s, &(pdu.ctlHeader)))) + { + WLog_ERR(TAG, "remdesk_write_ctl_header failed with error %" PRIu32 "!", error); + goto out; + } + + Stream_Write_UINT32(s, pdu.versionMajor); /* versionMajor (4 bytes) */ + Stream_Write_UINT32(s, pdu.versionMinor); /* versionMinor (4 bytes) */ + Stream_SealLength(s); + + if ((error = remdesk_virtual_channel_write(context, s))) + WLog_ERR(TAG, "remdesk_virtual_channel_write failed with error %" PRIu32 "!", error); + +out: + Stream_Free(s, TRUE); + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT remdesk_recv_ctl_version_info_pdu(RemdeskServerContext* context, wStream* s, + REMDESK_CHANNEL_HEADER* header) +{ + UINT32 versionMajor; + UINT32 versionMinor; + + if (Stream_GetRemainingLength(s) < 8) + { + WLog_ERR(TAG, "Stream_GetRemainingLength failed!"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT32(s, versionMajor); /* versionMajor (4 bytes) */ + Stream_Read_UINT32(s, versionMinor); /* versionMinor (4 bytes) */ + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT remdesk_recv_ctl_remote_control_desktop_pdu(RemdeskServerContext* context, wStream* s, + REMDESK_CHANNEL_HEADER* header) +{ + int status; + int cchStringW; + WCHAR* pStringW; + UINT32 msgLength; + int cbRaConnectionStringW = 0; + WCHAR* raConnectionStringW = NULL; + REMDESK_CTL_REMOTE_CONTROL_DESKTOP_PDU pdu; + UINT error; + msgLength = header->DataLength - 4; + pStringW = (WCHAR*)Stream_Pointer(s); + raConnectionStringW = pStringW; + cchStringW = 0; + + while ((msgLength > 0) && pStringW[cchStringW]) + { + msgLength -= 2; + cchStringW++; + } + + if (pStringW[cchStringW] || !cchStringW) + return ERROR_INVALID_DATA; + + cchStringW++; + cbRaConnectionStringW = cchStringW * 2; + pdu.raConnectionString = NULL; + status = ConvertFromUnicode(CP_UTF8, 0, raConnectionStringW, cbRaConnectionStringW / 2, + &pdu.raConnectionString, 0, NULL, NULL); + + if (status <= 0) + { + WLog_ERR(TAG, "ConvertFromUnicode failed!"); + return ERROR_INTERNAL_ERROR; + } + + WLog_INFO(TAG, "RaConnectionString: %s", pdu.raConnectionString); + free(pdu.raConnectionString); + + if ((error = remdesk_send_ctl_result_pdu(context, 0))) + WLog_ERR(TAG, "remdesk_send_ctl_result_pdu failed with error %" PRIu32 "!", error); + + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT remdesk_recv_ctl_authenticate_pdu(RemdeskServerContext* context, wStream* s, + REMDESK_CHANNEL_HEADER* header) +{ + int status; + int cchStringW; + WCHAR* pStringW; + UINT32 msgLength; + int cbExpertBlobW = 0; + WCHAR* expertBlobW = NULL; + int cbRaConnectionStringW = 0; + WCHAR* raConnectionStringW = NULL; + REMDESK_CTL_AUTHENTICATE_PDU pdu; + msgLength = header->DataLength - 4; + pStringW = (WCHAR*)Stream_Pointer(s); + raConnectionStringW = pStringW; + cchStringW = 0; + + while ((msgLength > 0) && pStringW[cchStringW]) + { + msgLength -= 2; + cchStringW++; + } + + if (pStringW[cchStringW] || !cchStringW) + return ERROR_INVALID_DATA; + + cchStringW++; + cbRaConnectionStringW = cchStringW * 2; + pStringW += cchStringW; + expertBlobW = pStringW; + cchStringW = 0; + + while ((msgLength > 0) && pStringW[cchStringW]) + { + msgLength -= 2; + cchStringW++; + } + + if (pStringW[cchStringW] || !cchStringW) + return ERROR_INVALID_DATA; + + cchStringW++; + cbExpertBlobW = cchStringW * 2; + pdu.raConnectionString = NULL; + status = ConvertFromUnicode(CP_UTF8, 0, raConnectionStringW, cbRaConnectionStringW / 2, + &pdu.raConnectionString, 0, NULL, NULL); + + if (status <= 0) + { + WLog_ERR(TAG, "ConvertFromUnicode failed!"); + return ERROR_INTERNAL_ERROR; + } + + pdu.expertBlob = NULL; + status = ConvertFromUnicode(CP_UTF8, 0, expertBlobW, cbExpertBlobW / 2, &pdu.expertBlob, 0, + NULL, NULL); + + if (status <= 0) + { + WLog_ERR(TAG, "ConvertFromUnicode failed!"); + free(pdu.raConnectionString); + return ERROR_INTERNAL_ERROR; + } + + WLog_INFO(TAG, "RaConnectionString: %s ExpertBlob: %s", pdu.raConnectionString, pdu.expertBlob); + free(pdu.raConnectionString); + free(pdu.expertBlob); + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT remdesk_recv_ctl_verify_password_pdu(RemdeskServerContext* context, wStream* s, + REMDESK_CHANNEL_HEADER* header) +{ + int status; + int cbExpertBlobW = 0; + WCHAR* expertBlobW = NULL; + REMDESK_CTL_VERIFY_PASSWORD_PDU pdu; + UINT error; + + if (Stream_GetRemainingLength(s) < 8) + { + WLog_ERR(TAG, "Stream_GetRemainingLength failed!"); + return ERROR_INVALID_DATA; + } + + pdu.expertBlob = NULL; + expertBlobW = (WCHAR*)Stream_Pointer(s); + cbExpertBlobW = header->DataLength - 4; + status = ConvertFromUnicode(CP_UTF8, 0, expertBlobW, cbExpertBlobW / 2, &pdu.expertBlob, 0, + NULL, NULL); + + if (status <= 0) + { + WLog_ERR(TAG, "ConvertFromUnicode failed!"); + return ERROR_INTERNAL_ERROR; + } + + WLog_INFO(TAG, "ExpertBlob: %s", pdu.expertBlob); + + if ((error = remdesk_send_ctl_result_pdu(context, 0))) + WLog_ERR(TAG, "remdesk_send_ctl_result_pdu failed with error %" PRIu32 "!", error); + + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT remdesk_recv_ctl_pdu(RemdeskServerContext* context, wStream* s, + REMDESK_CHANNEL_HEADER* header) +{ + UINT error = CHANNEL_RC_OK; + UINT32 msgType = 0; + + if (Stream_GetRemainingLength(s) < 4) + { + WLog_ERR(TAG, "Stream_GetRemainingLength failed!"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT32(s, msgType); /* msgType (4 bytes) */ + WLog_INFO(TAG, "msgType: %" PRIu32 "", msgType); + + switch (msgType) + { + case REMDESK_CTL_REMOTE_CONTROL_DESKTOP: + if ((error = remdesk_recv_ctl_remote_control_desktop_pdu(context, s, header))) + { + WLog_ERR(TAG, + "remdesk_recv_ctl_remote_control_desktop_pdu failed with error %" PRIu32 + "!", + error); + return error; + } + + break; + + case REMDESK_CTL_AUTHENTICATE: + if ((error = remdesk_recv_ctl_authenticate_pdu(context, s, header))) + { + WLog_ERR(TAG, "remdesk_recv_ctl_authenticate_pdu failed with error %" PRIu32 "!", + error); + return error; + } + + break; + + case REMDESK_CTL_DISCONNECT: + break; + + case REMDESK_CTL_VERSIONINFO: + if ((error = remdesk_recv_ctl_version_info_pdu(context, s, header))) + { + WLog_ERR(TAG, "remdesk_recv_ctl_version_info_pdu failed with error %" PRIu32 "!", + error); + return error; + } + + break; + + case REMDESK_CTL_ISCONNECTED: + break; + + case REMDESK_CTL_VERIFY_PASSWORD: + if ((error = remdesk_recv_ctl_verify_password_pdu(context, s, header))) + { + WLog_ERR(TAG, "remdesk_recv_ctl_verify_password_pdu failed with error %" PRIu32 "!", + error); + return error; + } + + break; + + case REMDESK_CTL_EXPERT_ON_VISTA: + break; + + case REMDESK_CTL_RANOVICE_NAME: + break; + + case REMDESK_CTL_RAEXPERT_NAME: + break; + + case REMDESK_CTL_TOKEN: + break; + + default: + WLog_ERR(TAG, "remdesk_recv_control_pdu: unknown msgType: %" PRIu32 "", msgType); + error = ERROR_INVALID_DATA; + break; + } + + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT remdesk_server_receive_pdu(RemdeskServerContext* context, wStream* s) +{ + UINT error = CHANNEL_RC_OK; + REMDESK_CHANNEL_HEADER header; +#if 0 + WLog_INFO(TAG, "RemdeskReceive: %"PRIuz"", Stream_GetRemainingLength(s)); + winpr_HexDump(Stream_Pointer(s), Stream_GetRemainingLength(s)); +#endif + + if ((error = remdesk_read_channel_header(s, &header))) + { + WLog_ERR(TAG, "remdesk_read_channel_header failed with error %" PRIu32 "!", error); + return error; + } + + if (strcmp(header.ChannelName, "RC_CTL") == 0) + { + if ((error = remdesk_recv_ctl_pdu(context, s, &header))) + { + WLog_ERR(TAG, "remdesk_recv_ctl_pdu failed with error %" PRIu32 "!", error); + return error; + } + } + else if (strcmp(header.ChannelName, "70") == 0) + { + } + else if (strcmp(header.ChannelName, "71") == 0) + { + } + else if (strcmp(header.ChannelName, ".") == 0) + { + } + else if (strcmp(header.ChannelName, "1000.") == 0) + { + } + else if (strcmp(header.ChannelName, "RA_FX") == 0) + { + } + else + { + } + + return error; +} + +static DWORD WINAPI remdesk_server_thread(LPVOID arg) +{ + wStream* s; + DWORD status; + DWORD nCount; + void* buffer; + UINT32* pHeader; + UINT32 PduLength; + HANDLE events[8]; + HANDLE ChannelEvent; + DWORD BytesReturned; + RemdeskServerContext* context; + UINT error; + context = (RemdeskServerContext*)arg; + buffer = NULL; + BytesReturned = 0; + ChannelEvent = NULL; + s = Stream_New(NULL, 4096); + + if (!s) + { + WLog_ERR(TAG, "Stream_New failed!"); + error = CHANNEL_RC_NO_MEMORY; + goto out; + } + + if (WTSVirtualChannelQuery(context->priv->ChannelHandle, WTSVirtualEventHandle, &buffer, + &BytesReturned) == TRUE) + { + if (BytesReturned == sizeof(HANDLE)) + CopyMemory(&ChannelEvent, buffer, sizeof(HANDLE)); + + WTSFreeMemory(buffer); + } + else + { + WLog_ERR(TAG, "WTSVirtualChannelQuery failed!"); + error = ERROR_INTERNAL_ERROR; + goto out; + } + + nCount = 0; + events[nCount++] = ChannelEvent; + events[nCount++] = context->priv->StopEvent; + + if ((error = remdesk_send_ctl_version_info_pdu(context))) + { + WLog_ERR(TAG, "remdesk_send_ctl_version_info_pdu failed with error %" PRIu32 "!", error); + goto out; + } + + while (1) + { + status = WaitForMultipleObjects(nCount, events, FALSE, INFINITE); + + if (status == WAIT_FAILED) + { + error = GetLastError(); + WLog_ERR(TAG, "WaitForMultipleObjects failed with error %" PRIu32 "", error); + break; + } + + status = WaitForSingleObject(context->priv->StopEvent, 0); + + if (status == WAIT_FAILED) + { + error = GetLastError(); + WLog_ERR(TAG, "WaitForSingleObject failed with error %" PRIu32 "", error); + break; + } + + if (status == WAIT_OBJECT_0) + { + break; + } + + if (WTSVirtualChannelRead(context->priv->ChannelHandle, 0, (PCHAR)Stream_Buffer(s), + Stream_Capacity(s), &BytesReturned)) + { + if (BytesReturned) + Stream_Seek(s, BytesReturned); + } + else + { + if (!Stream_EnsureRemainingCapacity(s, BytesReturned)) + { + WLog_ERR(TAG, "Stream_EnsureRemainingCapacity failed!"); + error = CHANNEL_RC_NO_MEMORY; + break; + } + } + + if (Stream_GetPosition(s) >= 8) + { + pHeader = (UINT32*)Stream_Buffer(s); + PduLength = pHeader[0] + pHeader[1] + 8; + + if (PduLength >= Stream_GetPosition(s)) + { + Stream_SealLength(s); + Stream_SetPosition(s, 0); + + if ((error = remdesk_server_receive_pdu(context, s))) + { + WLog_ERR(TAG, "remdesk_server_receive_pdu failed with error %" PRIu32 "!", + error); + break; + } + + Stream_SetPosition(s, 0); + } + } + } + + Stream_Free(s, TRUE); +out: + + if (error && context->rdpcontext) + setChannelError(context->rdpcontext, error, "remdesk_server_thread reported an error"); + + ExitThread(error); + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT remdesk_server_start(RemdeskServerContext* context) +{ + context->priv->ChannelHandle = + WTSVirtualChannelOpen(context->vcm, WTS_CURRENT_SESSION, "remdesk"); + + if (!context->priv->ChannelHandle) + { + WLog_ERR(TAG, "WTSVirtualChannelOpen failed!"); + return ERROR_INTERNAL_ERROR; + } + + if (!(context->priv->StopEvent = CreateEvent(NULL, TRUE, FALSE, NULL))) + { + WLog_ERR(TAG, "CreateEvent failed!"); + return ERROR_INTERNAL_ERROR; + } + + if (!(context->priv->Thread = + CreateThread(NULL, 0, remdesk_server_thread, (void*)context, 0, NULL))) + { + WLog_ERR(TAG, "CreateThread failed!"); + CloseHandle(context->priv->StopEvent); + context->priv->StopEvent = NULL; + return ERROR_INTERNAL_ERROR; + } + + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT remdesk_server_stop(RemdeskServerContext* context) +{ + UINT error; + SetEvent(context->priv->StopEvent); + + if (WaitForSingleObject(context->priv->Thread, INFINITE) == WAIT_FAILED) + { + error = GetLastError(); + WLog_ERR(TAG, "WaitForSingleObject failed with error %" PRIu32 "!", error); + return error; + } + + CloseHandle(context->priv->Thread); + CloseHandle(context->priv->StopEvent); + return CHANNEL_RC_OK; +} + +RemdeskServerContext* remdesk_server_context_new(HANDLE vcm) +{ + RemdeskServerContext* context; + context = (RemdeskServerContext*)calloc(1, sizeof(RemdeskServerContext)); + + if (context) + { + context->vcm = vcm; + context->Start = remdesk_server_start; + context->Stop = remdesk_server_stop; + context->priv = (RemdeskServerPrivate*)calloc(1, sizeof(RemdeskServerPrivate)); + + if (!context->priv) + { + free(context); + return NULL; + } + + context->priv->Version = 1; + } + + return context; +} + +void remdesk_server_context_free(RemdeskServerContext* context) +{ + if (context) + { + if (context->priv->ChannelHandle != INVALID_HANDLE_VALUE) + WTSVirtualChannelClose(context->priv->ChannelHandle); + + free(context->priv); + free(context); + } +} diff --git a/channels/remdesk/server/remdesk_main.h b/channels/remdesk/server/remdesk_main.h new file mode 100644 index 0000000..f47d037 --- /dev/null +++ b/channels/remdesk/server/remdesk_main.h @@ -0,0 +1,41 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Remote Assistance Virtual Channel + * + * Copyright 2014 Marc-Andre Moreau + * + * Licensed 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 FREERDP_CHANNEL_REMDESK_SERVER_MAIN_H +#define FREERDP_CHANNEL_REMDESK_SERVER_MAIN_H + +#include +#include +#include + +#include +#include + +#define TAG CHANNELS_TAG("remdesk.server") + +struct _remdesk_server_private +{ + HANDLE Thread; + HANDLE StopEvent; + void* ChannelHandle; + + UINT32 Version; +}; + +#endif /* FREERDP_CHANNEL_REMDESK_SERVER_MAIN_H */ diff --git a/channels/serial/CMakeLists.txt b/channels/serial/CMakeLists.txt new file mode 100644 index 0000000..aa5cd85 --- /dev/null +++ b/channels/serial/CMakeLists.txt @@ -0,0 +1,23 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2012 Marc-Andre Moreau +# +# Licensed 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. + +define_channel("serial") + +if(WITH_CLIENT_CHANNELS) + add_channel_client(${MODULE_PREFIX} ${CHANNEL_NAME}) +endif() + diff --git a/channels/serial/ChannelOptions.cmake b/channels/serial/ChannelOptions.cmake new file mode 100644 index 0000000..add3443 --- /dev/null +++ b/channels/serial/ChannelOptions.cmake @@ -0,0 +1,23 @@ + +set(OPTION_DEFAULT OFF) +set(OPTION_CLIENT_DEFAULT ON) +set(OPTION_SERVER_DEFAULT OFF) + +if(WIN32) + set(OPTION_CLIENT_DEFAULT OFF) + set(OPTION_SERVER_DEFAULT OFF) +endif() + +if(ANDROID) + set(OPTION_CLIENT_DEFAULT OFF) + set(OPTION_SERVER_DEFAULT OFF) +endif() + +define_channel_options(NAME "serial" TYPE "device" + DESCRIPTION "Serial Port Virtual Channel Extension" + SPECIFICATIONS "[MS-RDPESP]" + DEFAULT ${OPTION_DEFAULT}) + +define_channel_client_options(${OPTION_CLIENT_DEFAULT}) +define_channel_server_options(${OPTION_SERVER_DEFAULT}) + diff --git a/channels/serial/client/CMakeLists.txt b/channels/serial/client/CMakeLists.txt new file mode 100644 index 0000000..f16995b --- /dev/null +++ b/channels/serial/client/CMakeLists.txt @@ -0,0 +1,34 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2012 Marc-Andre Moreau +# +# Licensed 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. + +define_channel_client("serial") + +set(${MODULE_PREFIX}_SRCS + serial_main.c) + +add_channel_client_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} TRUE "DeviceServiceEntry") + + + +target_link_libraries(${MODULE_NAME} winpr freerdp) + + +if (WITH_DEBUG_SYMBOLS AND MSVC AND NOT BUILTIN_CHANNELS AND BUILD_SHARED_LIBS) + install(FILES ${CMAKE_BINARY_DIR}/${MODULE_NAME}.pdb DESTINATION ${FREERDP_ADDIN_PATH} COMPONENT symbols) +endif() + +set_property(TARGET ${MODULE_NAME} PROPERTY FOLDER "Channels/${CHANNEL_NAME}/Client") diff --git a/channels/serial/client/serial_main.c b/channels/serial/client/serial_main.c new file mode 100644 index 0000000..afe67b4 --- /dev/null +++ b/channels/serial/client/serial_main.c @@ -0,0 +1,968 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Serial Port Device Service Virtual Channel + * + * Copyright 2011 O.S. Systems Software Ltda. + * Copyright 2011 Eduardo Fiss Beloni + * Copyright 2014 Hewlett-Packard Development Company, L.P. + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#define TAG CHANNELS_TAG("serial.client") + +/* TODO: all #ifdef __linux__ could be removed once only some generic + * functions will be used. Replace CommReadFile by ReadFile, + * CommWriteFile by WriteFile etc.. */ +#if defined __linux__ && !defined ANDROID + +#define MAX_IRP_THREADS 5 + +typedef struct _SERIAL_DEVICE SERIAL_DEVICE; + +struct _SERIAL_DEVICE +{ + DEVICE device; + BOOL permissive; + SERIAL_DRIVER_ID ServerSerialDriverId; + HANDLE* hComm; + + wLog* log; + HANDLE MainThread; + wMessageQueue* MainIrpQueue; + + /* one thread per pending IRP and indexed according their CompletionId */ + wListDictionary* IrpThreads; + UINT32 IrpThreadToBeTerminatedCount; + CRITICAL_SECTION TerminatingIrpThreadsLock; + rdpContext* rdpcontext; +}; + +typedef struct _IRP_THREAD_DATA IRP_THREAD_DATA; + +struct _IRP_THREAD_DATA +{ + SERIAL_DEVICE* serial; + IRP* irp; +}; + +static UINT32 _GetLastErrorToIoStatus(SERIAL_DEVICE* serial) +{ + /* http://msdn.microsoft.com/en-us/library/ff547466%28v=vs.85%29.aspx#generic_status_values_for_serial_device_control_requests + */ + switch (GetLastError()) + { + case ERROR_BAD_DEVICE: + return STATUS_INVALID_DEVICE_REQUEST; + + case ERROR_CALL_NOT_IMPLEMENTED: + return STATUS_NOT_IMPLEMENTED; + + case ERROR_CANCELLED: + return STATUS_CANCELLED; + + case ERROR_INSUFFICIENT_BUFFER: + return STATUS_BUFFER_TOO_SMALL; /* NB: STATUS_BUFFER_SIZE_TOO_SMALL not defined */ + + case ERROR_INVALID_DEVICE_OBJECT_PARAMETER: /* eg: SerCx2.sys' _purge() */ + return STATUS_INVALID_DEVICE_STATE; + + case ERROR_INVALID_HANDLE: + return STATUS_INVALID_DEVICE_REQUEST; + + case ERROR_INVALID_PARAMETER: + return STATUS_INVALID_PARAMETER; + + case ERROR_IO_DEVICE: + return STATUS_IO_DEVICE_ERROR; + + case ERROR_IO_PENDING: + return STATUS_PENDING; + + case ERROR_NOT_SUPPORTED: + return STATUS_NOT_SUPPORTED; + + case ERROR_TIMEOUT: + return STATUS_TIMEOUT; + /* no default */ + } + + WLog_Print(serial->log, WLOG_DEBUG, "unexpected last-error: 0x%08" PRIX32 "", GetLastError()); + return STATUS_UNSUCCESSFUL; +} + +static UINT serial_process_irp_create(SERIAL_DEVICE* serial, IRP* irp) +{ + DWORD DesiredAccess; + DWORD SharedAccess; + DWORD CreateDisposition; + UINT32 PathLength; + + if (Stream_GetRemainingLength(irp->input) < 32) + return ERROR_INVALID_DATA; + + Stream_Read_UINT32(irp->input, DesiredAccess); /* DesiredAccess (4 bytes) */ + Stream_Seek_UINT64(irp->input); /* AllocationSize (8 bytes) */ + Stream_Seek_UINT32(irp->input); /* FileAttributes (4 bytes) */ + Stream_Read_UINT32(irp->input, SharedAccess); /* SharedAccess (4 bytes) */ + Stream_Read_UINT32(irp->input, CreateDisposition); /* CreateDisposition (4 bytes) */ + Stream_Seek_UINT32(irp->input); /* CreateOptions (4 bytes) */ + Stream_Read_UINT32(irp->input, PathLength); /* PathLength (4 bytes) */ + + if (!Stream_SafeSeek(irp->input, PathLength)) /* Path (variable) */ + return ERROR_INVALID_DATA; + + assert(PathLength == 0); /* MS-RDPESP 2.2.2.2 */ +#ifndef _WIN32 + /* Windows 2012 server sends on a first call : + * DesiredAccess = 0x00100080: SYNCHRONIZE | FILE_READ_ATTRIBUTES + * SharedAccess = 0x00000007: FILE_SHARE_DELETE | FILE_SHARE_WRITE | FILE_SHARE_READ + * CreateDisposition = 0x00000001: CREATE_NEW + * + * then Windows 2012 sends : + * DesiredAccess = 0x00120089: SYNCHRONIZE | READ_CONTROL | FILE_READ_ATTRIBUTES | + * FILE_READ_EA | FILE_READ_DATA SharedAccess = 0x00000007: FILE_SHARE_DELETE | + * FILE_SHARE_WRITE | FILE_SHARE_READ CreateDisposition = 0x00000001: CREATE_NEW + * + * assert(DesiredAccess == (GENERIC_READ | GENERIC_WRITE)); + * assert(SharedAccess == 0); + * assert(CreateDisposition == OPEN_EXISTING); + * + */ + WLog_Print(serial->log, WLOG_DEBUG, + "DesiredAccess: 0x%" PRIX32 ", SharedAccess: 0x%" PRIX32 + ", CreateDisposition: 0x%" PRIX32 "", + DesiredAccess, SharedAccess, CreateDisposition); + /* FIXME: As of today only the flags below are supported by CommCreateFileA: */ + DesiredAccess = GENERIC_READ | GENERIC_WRITE; + SharedAccess = 0; + CreateDisposition = OPEN_EXISTING; +#endif + serial->hComm = + CreateFile(serial->device.name, DesiredAccess, SharedAccess, NULL, /* SecurityAttributes */ + CreateDisposition, 0, /* FlagsAndAttributes */ + NULL); /* TemplateFile */ + + if (!serial->hComm || (serial->hComm == INVALID_HANDLE_VALUE)) + { + WLog_Print(serial->log, WLOG_WARN, "CreateFile failure: %s last-error: 0x%08" PRIX32 "", + serial->device.name, GetLastError()); + irp->IoStatus = STATUS_UNSUCCESSFUL; + goto error_handle; + } + + _comm_setServerSerialDriver(serial->hComm, serial->ServerSerialDriverId); + _comm_set_permissive(serial->hComm, serial->permissive); + /* NOTE: binary mode/raw mode required for the redirection. On + * Linux, CommCreateFileA forces this setting. + */ + /* ZeroMemory(&dcb, sizeof(DCB)); */ + /* dcb.DCBlength = sizeof(DCB); */ + /* GetCommState(serial->hComm, &dcb); */ + /* dcb.fBinary = TRUE; */ + /* SetCommState(serial->hComm, &dcb); */ + assert(irp->FileId == 0); + irp->FileId = irp->devman->id_sequence++; /* FIXME: why not ((WINPR_COMM*)hComm)->fd? */ + irp->IoStatus = STATUS_SUCCESS; + WLog_Print(serial->log, WLOG_DEBUG, "%s (DeviceId: %" PRIu32 ", FileId: %" PRIu32 ") created.", + serial->device.name, irp->device->id, irp->FileId); +error_handle: + Stream_Write_UINT32(irp->output, irp->FileId); /* FileId (4 bytes) */ + Stream_Write_UINT8(irp->output, 0); /* Information (1 byte) */ + return CHANNEL_RC_OK; +} + +static UINT serial_process_irp_close(SERIAL_DEVICE* serial, IRP* irp) +{ + if (Stream_GetRemainingLength(irp->input) < 32) + return ERROR_INVALID_DATA; + + Stream_Seek(irp->input, 32); /* Padding (32 bytes) */ + + if (!CloseHandle(serial->hComm)) + { + WLog_Print(serial->log, WLOG_WARN, "CloseHandle failure: %s (%" PRIu32 ") closed.", + serial->device.name, irp->device->id); + irp->IoStatus = STATUS_UNSUCCESSFUL; + goto error_handle; + } + + WLog_Print(serial->log, WLOG_DEBUG, "%s (DeviceId: %" PRIu32 ", FileId: %" PRIu32 ") closed.", + serial->device.name, irp->device->id, irp->FileId); + serial->hComm = NULL; + irp->IoStatus = STATUS_SUCCESS; +error_handle: + Stream_Zero(irp->output, 5); /* Padding (5 bytes) */ + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT serial_process_irp_read(SERIAL_DEVICE* serial, IRP* irp) +{ + UINT32 Length; + UINT64 Offset; + BYTE* buffer = NULL; + DWORD nbRead = 0; + + if (Stream_GetRemainingLength(irp->input) < 32) + return ERROR_INVALID_DATA; + + Stream_Read_UINT32(irp->input, Length); /* Length (4 bytes) */ + Stream_Read_UINT64(irp->input, Offset); /* Offset (8 bytes) */ + Stream_Seek(irp->input, 20); /* Padding (20 bytes) */ + buffer = (BYTE*)calloc(Length, sizeof(BYTE)); + + if (buffer == NULL) + { + irp->IoStatus = STATUS_NO_MEMORY; + goto error_handle; + } + + /* MS-RDPESP 3.2.5.1.4: If the Offset field is not set to 0, the value MUST be ignored + * assert(Offset == 0); + */ + WLog_Print(serial->log, WLOG_DEBUG, "reading %" PRIu32 " bytes from %s", Length, + serial->device.name); + + /* FIXME: CommReadFile to be replaced by ReadFile */ + if (CommReadFile(serial->hComm, buffer, Length, &nbRead, NULL)) + { + irp->IoStatus = STATUS_SUCCESS; + } + else + { + WLog_Print(serial->log, WLOG_DEBUG, + "read failure to %s, nbRead=%" PRIu32 ", last-error: 0x%08" PRIX32 "", + serial->device.name, nbRead, GetLastError()); + irp->IoStatus = _GetLastErrorToIoStatus(serial); + } + + WLog_Print(serial->log, WLOG_DEBUG, "%" PRIu32 " bytes read from %s", nbRead, + serial->device.name); +error_handle: + Stream_Write_UINT32(irp->output, nbRead); /* Length (4 bytes) */ + + if (nbRead > 0) + { + if (!Stream_EnsureRemainingCapacity(irp->output, nbRead)) + { + WLog_ERR(TAG, "Stream_EnsureRemainingCapacity failed!"); + free(buffer); + return CHANNEL_RC_NO_MEMORY; + } + + Stream_Write(irp->output, buffer, nbRead); /* ReadData */ + } + + free(buffer); + return CHANNEL_RC_OK; +} + +static UINT serial_process_irp_write(SERIAL_DEVICE* serial, IRP* irp) +{ + UINT32 Length; + UINT64 Offset; + void* ptr; + DWORD nbWritten = 0; + + if (Stream_GetRemainingLength(irp->input) < 32) + return ERROR_INVALID_DATA; + + Stream_Read_UINT32(irp->input, Length); /* Length (4 bytes) */ + Stream_Read_UINT64(irp->input, Offset); /* Offset (8 bytes) */ + if (!Stream_SafeSeek(irp->input, 20)) /* Padding (20 bytes) */ + return ERROR_INVALID_DATA; + + /* MS-RDPESP 3.2.5.1.5: The Offset field is ignored + * assert(Offset == 0); + * + * Using a serial printer, noticed though this field could be + * set. + */ + WLog_Print(serial->log, WLOG_DEBUG, "writing %" PRIu32 " bytes to %s", Length, + serial->device.name); + + ptr = Stream_Pointer(irp->input); + if (!Stream_SafeSeek(irp->input, Length)) + return ERROR_INVALID_DATA; + /* FIXME: CommWriteFile to be replaced by WriteFile */ + if (CommWriteFile(serial->hComm, ptr, Length, &nbWritten, NULL)) + { + irp->IoStatus = STATUS_SUCCESS; + } + else + { + WLog_Print(serial->log, WLOG_DEBUG, + "write failure to %s, nbWritten=%" PRIu32 ", last-error: 0x%08" PRIX32 "", + serial->device.name, nbWritten, GetLastError()); + irp->IoStatus = _GetLastErrorToIoStatus(serial); + } + + WLog_Print(serial->log, WLOG_DEBUG, "%" PRIu32 " bytes written to %s", nbWritten, + serial->device.name); + Stream_Write_UINT32(irp->output, nbWritten); /* Length (4 bytes) */ + Stream_Write_UINT8(irp->output, 0); /* Padding (1 byte) */ + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT serial_process_irp_device_control(SERIAL_DEVICE* serial, IRP* irp) +{ + UINT32 IoControlCode; + UINT32 InputBufferLength; + BYTE* InputBuffer = NULL; + UINT32 OutputBufferLength; + BYTE* OutputBuffer = NULL; + DWORD BytesReturned = 0; + + if (Stream_GetRemainingLength(irp->input) < 32) + return ERROR_INVALID_DATA; + + Stream_Read_UINT32(irp->input, OutputBufferLength); /* OutputBufferLength (4 bytes) */ + Stream_Read_UINT32(irp->input, InputBufferLength); /* InputBufferLength (4 bytes) */ + Stream_Read_UINT32(irp->input, IoControlCode); /* IoControlCode (4 bytes) */ + Stream_Seek(irp->input, 20); /* Padding (20 bytes) */ + + if (Stream_GetRemainingLength(irp->input) < InputBufferLength) + return ERROR_INVALID_DATA; + + OutputBuffer = (BYTE*)calloc(OutputBufferLength, sizeof(BYTE)); + + if (OutputBuffer == NULL) + { + irp->IoStatus = STATUS_NO_MEMORY; + goto error_handle; + } + + InputBuffer = (BYTE*)calloc(InputBufferLength, sizeof(BYTE)); + + if (InputBuffer == NULL) + { + irp->IoStatus = STATUS_NO_MEMORY; + goto error_handle; + } + + Stream_Read(irp->input, InputBuffer, InputBufferLength); + WLog_Print(serial->log, WLOG_DEBUG, + "CommDeviceIoControl: CompletionId=%" PRIu32 ", IoControlCode=[0x%" PRIX32 "] %s", + irp->CompletionId, IoControlCode, _comm_serial_ioctl_name(IoControlCode)); + + /* FIXME: CommDeviceIoControl to be replaced by DeviceIoControl() */ + if (CommDeviceIoControl(serial->hComm, IoControlCode, InputBuffer, InputBufferLength, + OutputBuffer, OutputBufferLength, &BytesReturned, NULL)) + { + /* WLog_Print(serial->log, WLOG_DEBUG, "CommDeviceIoControl: CompletionId=%"PRIu32", + * IoControlCode=[0x%"PRIX32"] %s done", irp->CompletionId, IoControlCode, + * _comm_serial_ioctl_name(IoControlCode)); */ + irp->IoStatus = STATUS_SUCCESS; + } + else + { + WLog_Print(serial->log, WLOG_DEBUG, + "CommDeviceIoControl failure: IoControlCode=[0x%" PRIX32 + "] %s, last-error: 0x%08" PRIX32 "", + IoControlCode, _comm_serial_ioctl_name(IoControlCode), GetLastError()); + irp->IoStatus = _GetLastErrorToIoStatus(serial); + } + +error_handle: + /* FIXME: find out whether it's required or not to get + * BytesReturned == OutputBufferLength when + * CommDeviceIoControl returns FALSE */ + assert(OutputBufferLength == BytesReturned); + Stream_Write_UINT32(irp->output, BytesReturned); /* OutputBufferLength (4 bytes) */ + + if (BytesReturned > 0) + { + if (!Stream_EnsureRemainingCapacity(irp->output, BytesReturned)) + { + WLog_ERR(TAG, "Stream_EnsureRemainingCapacity failed!"); + free(InputBuffer); + free(OutputBuffer); + return CHANNEL_RC_NO_MEMORY; + } + + Stream_Write(irp->output, OutputBuffer, BytesReturned); /* OutputBuffer */ + } + + /* FIXME: Why at least Windows 2008R2 gets lost with this + * extra byte and likely on a IOCTL_SERIAL_SET_BAUD_RATE? The + * extra byte is well required according MS-RDPEFS + * 2.2.1.5.5 */ + /* else */ + /* { */ + /* Stream_Write_UINT8(irp->output, 0); /\* Padding (1 byte) *\/ */ + /* } */ + free(InputBuffer); + free(OutputBuffer); + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT serial_process_irp(SERIAL_DEVICE* serial, IRP* irp) +{ + UINT error = CHANNEL_RC_OK; + WLog_Print(serial->log, WLOG_DEBUG, + "IRP MajorFunction: 0x%08" PRIX32 " MinorFunction: 0x%08" PRIX32 "\n", + irp->MajorFunction, irp->MinorFunction); + + switch (irp->MajorFunction) + { + case IRP_MJ_CREATE: + error = serial_process_irp_create(serial, irp); + break; + + case IRP_MJ_CLOSE: + error = serial_process_irp_close(serial, irp); + break; + + case IRP_MJ_READ: + if ((error = serial_process_irp_read(serial, irp))) + WLog_ERR(TAG, "serial_process_irp_read failed with error %" PRIu32 "!", error); + + break; + + case IRP_MJ_WRITE: + error = serial_process_irp_write(serial, irp); + break; + + case IRP_MJ_DEVICE_CONTROL: + if ((error = serial_process_irp_device_control(serial, irp))) + WLog_ERR(TAG, "serial_process_irp_device_control failed with error %" PRIu32 "!", + error); + + break; + + default: + irp->IoStatus = STATUS_NOT_SUPPORTED; + break; + } + + return error; +} + +static DWORD WINAPI irp_thread_func(LPVOID arg) +{ + IRP_THREAD_DATA* data = (IRP_THREAD_DATA*)arg; + UINT error; + + /* blocks until the end of the request */ + if ((error = serial_process_irp(data->serial, data->irp))) + { + WLog_ERR(TAG, "serial_process_irp failed with error %" PRIu32 "", error); + goto error_out; + } + + EnterCriticalSection(&data->serial->TerminatingIrpThreadsLock); + data->serial->IrpThreadToBeTerminatedCount++; + error = data->irp->Complete(data->irp); + LeaveCriticalSection(&data->serial->TerminatingIrpThreadsLock); +error_out: + + if (error && data->serial->rdpcontext) + setChannelError(data->serial->rdpcontext, error, "irp_thread_func reported an error"); + + /* NB: At this point, the server might already being reusing + * the CompletionId whereas the thread is not yet + * terminated */ + free(data); + ExitThread(error); + return error; +} + +static void create_irp_thread(SERIAL_DEVICE* serial, IRP* irp) +{ + IRP_THREAD_DATA* data = NULL; + HANDLE irpThread; + HANDLE previousIrpThread; + uintptr_t key; + /* for a test/debug purpose, uncomment the code below to get a + * single thread for all IRPs. NB: two IRPs could not be + * processed at the same time, typically two concurent + * Read/Write operations could block each other. */ + /* serial_process_irp(serial, irp); */ + /* irp->Complete(irp); */ + /* return; */ + /* NOTE: for good or bad, this implementation relies on the + * server to avoid a flooding of requests. see also _purge(). + */ + EnterCriticalSection(&serial->TerminatingIrpThreadsLock); + + while (serial->IrpThreadToBeTerminatedCount > 0) + { + /* Cleaning up termitating and pending irp + * threads. See also: irp_thread_func() */ + HANDLE irpThread; + ULONG_PTR* ids; + int i, nbIds; + nbIds = ListDictionary_GetKeys(serial->IrpThreads, &ids); + + for (i = 0; i < nbIds; i++) + { + /* Checking if ids[i] is terminating or pending */ + DWORD waitResult; + ULONG_PTR id = ids[i]; + irpThread = ListDictionary_GetItemValue(serial->IrpThreads, (void*)id); + /* FIXME: not quite sure a zero timeout is a good thing to check whether a thread is + * stil alived or not */ + waitResult = WaitForSingleObject(irpThread, 0); + + if (waitResult == WAIT_OBJECT_0) + { + /* terminating thread */ + /* WLog_Print(serial->log, WLOG_DEBUG, "IRP thread with CompletionId=%"PRIuz" + * naturally died", id); */ + CloseHandle(irpThread); + ListDictionary_Remove(serial->IrpThreads, (void*)id); + serial->IrpThreadToBeTerminatedCount--; + } + else if (waitResult != WAIT_TIMEOUT) + { + /* unexpected thread state */ + WLog_Print(serial->log, WLOG_WARN, + "WaitForSingleObject, got an unexpected result=0x%" PRIX32 "\n", + waitResult); + assert(FALSE); + } + + /* pending thread (but not yet terminating thread) if waitResult == WAIT_TIMEOUT */ + } + + if (serial->IrpThreadToBeTerminatedCount > 0) + { + WLog_Print(serial->log, WLOG_DEBUG, "%" PRIu32 " IRP thread(s) not yet terminated", + serial->IrpThreadToBeTerminatedCount); + Sleep(1); /* 1 ms */ + } + + free(ids); + } + + LeaveCriticalSection(&serial->TerminatingIrpThreadsLock); + /* NB: At this point and thanks to the synchronization we're + * sure that the incoming IRP uses well a recycled + * CompletionId or the server sent again an IRP already posted + * which didn't get yet a response (this later server behavior + * at least observed with IOCTL_SERIAL_WAIT_ON_MASK and + * mstsc.exe). + * + * FIXME: behavior documented somewhere? behavior not yet + * observed with FreeRDP). + */ + key = irp->CompletionId; + previousIrpThread = ListDictionary_GetItemValue(serial->IrpThreads, (void*)key); + + if (previousIrpThread) + { + /* Thread still alived <=> Request still pending */ + WLog_Print(serial->log, WLOG_DEBUG, + "IRP recall: IRP with the CompletionId=%" PRIu32 " not yet completed!", + irp->CompletionId); + assert(FALSE); /* unimplemented */ + /* TODO: asserts that previousIrpThread handles well + * the same request by checking more details. Need an + * access to the IRP object used by previousIrpThread + */ + /* TODO: taking over the pending IRP or sending a kind + * of wake up signal to accelerate the pending + * request + * + * To be considered: + * if (IoControlCode == IOCTL_SERIAL_WAIT_ON_MASK) { + * pComm->PendingEvents |= SERIAL_EV_FREERDP_*; + * } + */ + irp->Discard(irp); + return; + } + + if (ListDictionary_Count(serial->IrpThreads) >= MAX_IRP_THREADS) + { + WLog_Print(serial->log, WLOG_WARN, + "Number of IRP threads threshold reached: %d, keep on anyway", + ListDictionary_Count(serial->IrpThreads)); + assert(FALSE); /* unimplemented */ + /* TODO: MAX_IRP_THREADS has been thought to avoid a + * flooding of pending requests. Use + * WaitForMultipleObjects() when available in winpr + * for threads. + */ + } + + /* error_handle to be used ... */ + data = (IRP_THREAD_DATA*)calloc(1, sizeof(IRP_THREAD_DATA)); + + if (data == NULL) + { + WLog_Print(serial->log, WLOG_WARN, "Could not allocate a new IRP_THREAD_DATA."); + goto error_handle; + } + + data->serial = serial; + data->irp = irp; + /* data freed by irp_thread_func */ + irpThread = CreateThread(NULL, 0, irp_thread_func, (void*)data, 0, NULL); + + if (irpThread == INVALID_HANDLE_VALUE) + { + WLog_Print(serial->log, WLOG_WARN, "Could not allocate a new IRP thread."); + goto error_handle; + } + + key = irp->CompletionId; + + if (!ListDictionary_Add(serial->IrpThreads, (void*)key, irpThread)) + { + WLog_ERR(TAG, "ListDictionary_Add failed!"); + goto error_handle; + } + + return; +error_handle: + irp->IoStatus = STATUS_NO_MEMORY; + irp->Complete(irp); + free(data); +} + +static void terminate_pending_irp_threads(SERIAL_DEVICE* serial) +{ + ULONG_PTR* ids; + int i, nbIds; + nbIds = ListDictionary_GetKeys(serial->IrpThreads, &ids); + WLog_Print(serial->log, WLOG_DEBUG, "Terminating %d IRP thread(s)", nbIds); + + for (i = 0; i < nbIds; i++) + { + HANDLE irpThread; + ULONG_PTR id = ids[i]; + irpThread = ListDictionary_GetItemValue(serial->IrpThreads, (void*)id); + TerminateThread(irpThread, 0); + + if (WaitForSingleObject(irpThread, INFINITE) == WAIT_FAILED) + { + WLog_ERR(TAG, "WaitForSingleObject failed!"); + continue; + } + + CloseHandle(irpThread); + WLog_Print(serial->log, WLOG_DEBUG, "IRP thread terminated, CompletionId %p", (void*)id); + } + + ListDictionary_Clear(serial->IrpThreads); + free(ids); +} + +static DWORD WINAPI serial_thread_func(LPVOID arg) +{ + IRP* irp; + wMessage message; + SERIAL_DEVICE* serial = (SERIAL_DEVICE*)arg; + UINT error = CHANNEL_RC_OK; + + while (1) + { + if (!MessageQueue_Wait(serial->MainIrpQueue)) + { + WLog_ERR(TAG, "MessageQueue_Wait failed!"); + error = ERROR_INTERNAL_ERROR; + break; + } + + if (!MessageQueue_Peek(serial->MainIrpQueue, &message, TRUE)) + { + WLog_ERR(TAG, "MessageQueue_Peek failed!"); + error = ERROR_INTERNAL_ERROR; + break; + } + + if (message.id == WMQ_QUIT) + { + terminate_pending_irp_threads(serial); + break; + } + + irp = (IRP*)message.wParam; + + if (irp) + create_irp_thread(serial, irp); + } + + if (error && serial->rdpcontext) + setChannelError(serial->rdpcontext, error, "serial_thread_func reported an error"); + + ExitThread(error); + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT serial_irp_request(DEVICE* device, IRP* irp) +{ + SERIAL_DEVICE* serial = (SERIAL_DEVICE*)device; + assert(irp != NULL); + + if (irp == NULL) + return CHANNEL_RC_OK; + + /* NB: ENABLE_ASYNCIO is set, (MS-RDPEFS 2.2.2.7.2) this + * allows the server to send multiple simultaneous read or + * write requests. + */ + + if (!MessageQueue_Post(serial->MainIrpQueue, NULL, 0, (void*)irp, NULL)) + { + WLog_ERR(TAG, "MessageQueue_Post failed!"); + return ERROR_INTERNAL_ERROR; + } + + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT serial_free(DEVICE* device) +{ + UINT error; + SERIAL_DEVICE* serial = (SERIAL_DEVICE*)device; + WLog_Print(serial->log, WLOG_DEBUG, "freeing"); + MessageQueue_PostQuit(serial->MainIrpQueue, 0); + + if (WaitForSingleObject(serial->MainThread, INFINITE) == WAIT_FAILED) + { + error = GetLastError(); + WLog_ERR(TAG, "WaitForSingleObject failed with error %" PRIu32 "!", error); + return error; + } + + CloseHandle(serial->MainThread); + + if (serial->hComm) + CloseHandle(serial->hComm); + + /* Clean up resources */ + Stream_Free(serial->device.data, TRUE); + MessageQueue_Free(serial->MainIrpQueue); + ListDictionary_Free(serial->IrpThreads); + DeleteCriticalSection(&serial->TerminatingIrpThreadsLock); + free(serial); + return CHANNEL_RC_OK; +} + +#endif /* __linux__ */ + +#ifdef BUILTIN_CHANNELS +#define DeviceServiceEntry serial_DeviceServiceEntry +#else +#define DeviceServiceEntry FREERDP_API DeviceServiceEntry +#endif + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT DeviceServiceEntry(PDEVICE_SERVICE_ENTRY_POINTS pEntryPoints) +{ + char* name; + char* path; + char* driver; + RDPDR_SERIAL* device; +#if defined __linux__ && !defined ANDROID + size_t i, len; + SERIAL_DEVICE* serial; +#endif /* __linux__ */ + UINT error = CHANNEL_RC_OK; + device = (RDPDR_SERIAL*)pEntryPoints->device; + name = device->Name; + path = device->Path; + driver = device->Driver; + + if (!name || (name[0] == '*')) + { + /* TODO: implement auto detection of serial ports */ + return CHANNEL_RC_OK; + } + + if ((name && name[0]) && (path && path[0])) + { + wLog* log; + log = WLog_Get("com.freerdp.channel.serial.client"); + WLog_Print(log, WLOG_DEBUG, "initializing"); +#ifndef __linux__ /* to be removed */ + WLog_Print(log, WLOG_WARN, "Serial ports redirection not supported on this platform."); + return CHANNEL_RC_INITIALIZATION_ERROR; +#else /* __linux __ */ + WLog_Print(log, WLOG_DEBUG, "Defining %s as %s", name, path); + + if (!DefineCommDevice(name /* eg: COM1 */, path /* eg: /dev/ttyS0 */)) + { + DWORD status = GetLastError(); + WLog_ERR(TAG, "DefineCommDevice failed with %08" PRIx32, status); + return ERROR_INTERNAL_ERROR; + } + + serial = (SERIAL_DEVICE*)calloc(1, sizeof(SERIAL_DEVICE)); + + if (!serial) + { + WLog_ERR(TAG, "calloc failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + serial->log = log; + serial->device.type = RDPDR_DTYP_SERIAL; + serial->device.name = name; + serial->device.IRPRequest = serial_irp_request; + serial->device.Free = serial_free; + serial->rdpcontext = pEntryPoints->rdpcontext; + len = strlen(name); + serial->device.data = Stream_New(NULL, len + 1); + + if (!serial->device.data) + { + WLog_ERR(TAG, "calloc failed!"); + error = CHANNEL_RC_NO_MEMORY; + goto error_out; + } + + for (i = 0; i <= len; i++) + Stream_Write_UINT8(serial->device.data, name[i] < 0 ? '_' : name[i]); + + if (driver != NULL) + { + if (_stricmp(driver, "Serial") == 0) + serial->ServerSerialDriverId = SerialDriverSerialSys; + else if (_stricmp(driver, "SerCx") == 0) + serial->ServerSerialDriverId = SerialDriverSerCxSys; + else if (_stricmp(driver, "SerCx2") == 0) + serial->ServerSerialDriverId = SerialDriverSerCx2Sys; + else + { + assert(FALSE); + WLog_Print(serial->log, WLOG_DEBUG, + "Unknown server's serial driver: %s. SerCx2 will be used", driver); + serial->ServerSerialDriverId = SerialDriverSerialSys; + } + } + else + { + /* default driver */ + serial->ServerSerialDriverId = SerialDriverSerialSys; + } + + if (device->Permissive != NULL) + { + if (_stricmp(device->Permissive, "permissive") == 0) + { + serial->permissive = TRUE; + } + else + { + WLog_Print(serial->log, WLOG_DEBUG, "Unknown flag: %s", device->Permissive); + assert(FALSE); + } + } + + WLog_Print(serial->log, WLOG_DEBUG, "Server's serial driver: %s (id: %d)", driver, + serial->ServerSerialDriverId); + /* TODO: implement auto detection of the server's serial driver */ + serial->MainIrpQueue = MessageQueue_New(NULL); + + if (!serial->MainIrpQueue) + { + WLog_ERR(TAG, "MessageQueue_New failed!"); + error = CHANNEL_RC_NO_MEMORY; + goto error_out; + } + + /* IrpThreads content only modified by create_irp_thread() */ + serial->IrpThreads = ListDictionary_New(FALSE); + + if (!serial->IrpThreads) + { + WLog_ERR(TAG, "ListDictionary_New failed!"); + error = CHANNEL_RC_NO_MEMORY; + goto error_out; + } + + serial->IrpThreadToBeTerminatedCount = 0; + InitializeCriticalSection(&serial->TerminatingIrpThreadsLock); + + if ((error = pEntryPoints->RegisterDevice(pEntryPoints->devman, (DEVICE*)serial))) + { + WLog_ERR(TAG, "EntryPoints->RegisterDevice failed with error %" PRIu32 "!", error); + goto error_out; + } + + if (!(serial->MainThread = + CreateThread(NULL, 0, serial_thread_func, (void*)serial, 0, NULL))) + { + WLog_ERR(TAG, "CreateThread failed!"); + error = ERROR_INTERNAL_ERROR; + goto error_out; + } + +#endif /* __linux __ */ + } + + return error; +error_out: +#ifdef __linux__ /* to be removed */ + ListDictionary_Free(serial->IrpThreads); + MessageQueue_Free(serial->MainIrpQueue); + Stream_Free(serial->device.data, TRUE); + free(serial); +#endif /* __linux __ */ + return error; +} diff --git a/channels/server/CMakeLists.txt b/channels/server/CMakeLists.txt new file mode 100644 index 0000000..1f49c3b --- /dev/null +++ b/channels/server/CMakeLists.txt @@ -0,0 +1,43 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2012 Marc-Andre Moreau +# +# Licensed 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. + +set(MODULE_NAME "freerdp-channels-server") +set(MODULE_PREFIX "FREERDP_CHANNELS_SERVER") + +set(${MODULE_PREFIX}_SRCS + ${CMAKE_CURRENT_SOURCE_DIR}/channels.c + ${CMAKE_CURRENT_SOURCE_DIR}/channels.h) + +foreach(STATIC_MODULE ${CHANNEL_STATIC_SERVER_MODULES}) + set(STATIC_MODULE_NAME ${${STATIC_MODULE}_SERVER_NAME}) + set(STATIC_MODULE_CHANNEL ${${STATIC_MODULE}_SERVER_CHANNEL}) + set(${MODULE_PREFIX}_LIBS ${${MODULE_PREFIX}_LIBS} ${STATIC_MODULE_NAME}) +endforeach() + +add_library(${MODULE_NAME} STATIC ${${MODULE_PREFIX}_SRCS}) + +if (WITH_LIBRARY_VERSIONING) + set_target_properties(${MODULE_NAME} PROPERTIES VERSION ${FREERDP_VERSION} SOVERSION ${FREERDP_API_VERSION}) +endif() + + +set(${MODULE_PREFIX}_LIBS ${${MODULE_PREFIX}_LIBS} winpr freerdp) + +set(${MODULE_PREFIX}_SRCS ${${MODULE_PREFIX}_SRCS} PARENT_SCOPE) +set(${MODULE_PREFIX}_LIBS ${${MODULE_PREFIX}_LIBS} PARENT_SCOPE) + +set_property(TARGET ${MODULE_NAME} PROPERTY FOLDER "Server/Common") diff --git a/channels/server/channels.c b/channels/server/channels.c new file mode 100644 index 0000000..2223ef8 --- /dev/null +++ b/channels/server/channels.c @@ -0,0 +1,131 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Server Channels + * + * Copyright 2011-2012 Vic Lee + * Copyright 2012 Marc-Andre Moreau + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include + +#include +#include + +#include +#include +#include + +#include "channels.h" + +/** + * this is a workaround to force importing symbols + * will need to fix that later on cleanly + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(CHANNEL_RDPECAM_SERVER) +#include +#include +#endif + +#if defined(CHANNEL_AINPUT_SERVER) +#include +#endif + +extern void freerdp_channels_dummy(void); + +void freerdp_channels_dummy(void) +{ + audin_server_context* audin; + RdpsndServerContext* rdpsnd; + CliprdrServerContext* cliprdr; + echo_server_context* echo; + RdpdrServerContext* rdpdr; + DrdynvcServerContext* drdynvc; + RdpeiServerContext* rdpei; + RemdeskServerContext* remdesk; + EncomspServerContext* encomsp; + RailServerContext* rail; + TelemetryServerContext* telemetry; + RdpgfxServerContext* rdpgfx; + DispServerContext* disp; +#if defined (CHANNEL_RDPECAM_SERVER) + CamDevEnumServerContext* camera_enumerator; + CameraDeviceServerContext* camera_device; +#endif + audin = audin_server_context_new(NULL); + audin_server_context_free(audin); + rdpsnd = rdpsnd_server_context_new(NULL); + rdpsnd_server_context_free(rdpsnd); + cliprdr = cliprdr_server_context_new(NULL); + cliprdr_server_context_free(cliprdr); + echo = echo_server_context_new(NULL); + echo_server_context_free(echo); + rdpdr = rdpdr_server_context_new(NULL); + rdpdr_server_context_free(rdpdr); + drdynvc = drdynvc_server_context_new(NULL); + drdynvc_server_context_free(drdynvc); + rdpei = rdpei_server_context_new(NULL); + rdpei_server_context_free(rdpei); + remdesk = remdesk_server_context_new(NULL); + remdesk_server_context_free(remdesk); + encomsp = encomsp_server_context_new(NULL); + encomsp_server_context_free(encomsp); + rail = rail_server_context_new(NULL); + rail_server_context_free(rail); + telemetry = telemetry_server_context_new(NULL); + telemetry_server_context_free(telemetry); + rdpgfx = rdpgfx_server_context_new(NULL); + rdpgfx_server_context_free(rdpgfx); + disp = disp_server_context_new(NULL); + disp_server_context_free(disp); + +#if defined (CHANNEL_RDPECAM_SERVER) + camera_enumerator = cam_dev_enum_server_context_new(NULL); + cam_dev_enum_server_context_free(camera_enumerator); + camera_device = camera_device_server_context_new(NULL); + camera_device_server_context_free(camera_device); +#endif + +#if defined(CHANNEL_AINPUT_SERVER) + { + ainput_server_context* ainput = ainput_server_context_new(NULL); + ainput_server_context_free(ainput); + } +#endif +} + +/** + * end of ugly symbols import workaround + */ diff --git a/channels/server/channels.h b/channels/server/channels.h new file mode 100644 index 0000000..a6c4791 --- /dev/null +++ b/channels/server/channels.h @@ -0,0 +1,24 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Server Channels + * + * Copyright 2011-2012 Vic Lee + * Copyright 2012 Marc-Andre Moreau + * + * Licensed 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 FREERDP_CHANNEL_SERVER_CHANNELS_H +#define FREERDP_CHANNEL_SERVER_CHANNELS_H + +#endif /* FREERDP_CHANNEL_SERVER_CHANNELS_H */ diff --git a/channels/smartcard/CMakeLists.txt b/channels/smartcard/CMakeLists.txt new file mode 100644 index 0000000..98c6c72 --- /dev/null +++ b/channels/smartcard/CMakeLists.txt @@ -0,0 +1,22 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2012 Marc-Andre Moreau +# +# Licensed 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. + +define_channel("smartcard") + +if(WITH_CLIENT_CHANNELS) + add_channel_client(${MODULE_PREFIX} ${CHANNEL_NAME}) +endif() diff --git a/channels/smartcard/ChannelOptions.cmake b/channels/smartcard/ChannelOptions.cmake new file mode 100644 index 0000000..7af6e31 --- /dev/null +++ b/channels/smartcard/ChannelOptions.cmake @@ -0,0 +1,12 @@ + +set(OPTION_DEFAULT OFF) +set(OPTION_CLIENT_DEFAULT ON) +set(OPTION_SERVER_DEFAULT OFF) + +define_channel_options(NAME "smartcard" TYPE "device" + DESCRIPTION "Smart Card Virtual Channel Extension" + SPECIFICATIONS "[MS-RDPESC]" + DEFAULT ${OPTION_DEFAULT}) + +define_channel_client_options(${OPTION_CLIENT_DEFAULT}) +define_channel_server_options(${OPTION_SERVER_DEFAULT}) diff --git a/channels/smartcard/client/CMakeLists.txt b/channels/smartcard/client/CMakeLists.txt new file mode 100644 index 0000000..ef89087 --- /dev/null +++ b/channels/smartcard/client/CMakeLists.txt @@ -0,0 +1,35 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2012 Marc-Andre Moreau +# +# Licensed 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. + +define_channel_client("smartcard") + +set(${MODULE_PREFIX}_SRCS + smartcard_main.c + smartcard_main.h + smartcard_pack.c + smartcard_pack.h + smartcard_operations.h + smartcard_operations.c) + +add_channel_client_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} FALSE "DeviceServiceEntry") + + + +target_link_libraries(${MODULE_NAME} winpr freerdp) + + +set_property(TARGET ${MODULE_NAME} PROPERTY FOLDER "Channels/${CHANNEL_NAME}/Client") diff --git a/channels/smartcard/client/smartcard_main.c b/channels/smartcard/client/smartcard_main.c new file mode 100644 index 0000000..2df4c14 --- /dev/null +++ b/channels/smartcard/client/smartcard_main.c @@ -0,0 +1,810 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Smartcard Device Service Virtual Channel + * + * Copyright 2011 O.S. Systems Software Ltda. + * Copyright 2011 Eduardo Fiss Beloni + * Copyright 2011 Anthony Tong + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger + * Copyright 2016 David PHAM-VAN + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include + +#include + +#include "smartcard_main.h" + +#define CAST_FROM_DEVICE(device) cast_device_from(device, __FUNCTION__, __FILE__, __LINE__) + +static SMARTCARD_DEVICE* sSmartcard = NULL; + +static SMARTCARD_DEVICE* cast_device_from(DEVICE* device, const char* fkt, const char* file, + int line) +{ + if (!device) + { + WLog_ERR(TAG, "%s [%s:%d] Called smartcard channel with NULL device", fkt, file, line); + return NULL; + } + + if (device->type != RDPDR_DTYP_SMARTCARD) + { + WLog_ERR(TAG, "%s [%s:%d] Called smartcard channel with invalid device of type %" PRIx32, + fkt, file, line, device->type); + return NULL; + } + + return (SMARTCARD_DEVICE*)device; +} + +static DWORD WINAPI smartcard_context_thread(LPVOID arg) +{ + SMARTCARD_CONTEXT* pContext = (SMARTCARD_CONTEXT*)arg; + DWORD nCount; + LONG status = 0; + DWORD waitStatus; + HANDLE hEvents[2]; + wMessage message; + SMARTCARD_DEVICE* smartcard; + SMARTCARD_OPERATION* operation; + UINT error = CHANNEL_RC_OK; + smartcard = pContext->smartcard; + nCount = 0; + hEvents[nCount++] = MessageQueue_Event(pContext->IrpQueue); + + while (1) + { + waitStatus = WaitForMultipleObjects(nCount, hEvents, FALSE, INFINITE); + + if (waitStatus == WAIT_FAILED) + { + error = GetLastError(); + WLog_ERR(TAG, "WaitForMultipleObjects failed with error %" PRIu32 "!", error); + break; + } + + waitStatus = WaitForSingleObject(MessageQueue_Event(pContext->IrpQueue), 0); + + if (waitStatus == WAIT_FAILED) + { + error = GetLastError(); + WLog_ERR(TAG, "WaitForSingleObject failed with error %" PRIu32 "!", error); + break; + } + + if (waitStatus == WAIT_OBJECT_0) + { + if (!MessageQueue_Peek(pContext->IrpQueue, &message, TRUE)) + { + WLog_ERR(TAG, "MessageQueue_Peek failed!"); + status = ERROR_INTERNAL_ERROR; + break; + } + + if (message.id == WMQ_QUIT) + break; + + operation = (SMARTCARD_OPERATION*)message.wParam; + + if (operation) + { + if ((status = smartcard_irp_device_control_call(smartcard, operation))) + { + WLog_ERR(TAG, "smartcard_irp_device_control_call failed with error %" PRIu32 "", + status); + break; + } + + if (!Queue_Enqueue(smartcard->CompletedIrpQueue, (void*)operation->irp)) + { + WLog_ERR(TAG, "Queue_Enqueue failed!"); + status = ERROR_INTERNAL_ERROR; + break; + } + + free(operation); + } + } + } + + if (status && smartcard->rdpcontext) + setChannelError(smartcard->rdpcontext, error, "smartcard_context_thread reported an error"); + + ExitThread(status); + return error; +} + +SMARTCARD_CONTEXT* smartcard_context_new(SMARTCARD_DEVICE* smartcard, SCARDCONTEXT hContext) +{ + SMARTCARD_CONTEXT* pContext; + pContext = (SMARTCARD_CONTEXT*)calloc(1, sizeof(SMARTCARD_CONTEXT)); + + if (!pContext) + { + WLog_ERR(TAG, "calloc failed!"); + return pContext; + } + + pContext->smartcard = smartcard; + pContext->hContext = hContext; + pContext->IrpQueue = MessageQueue_New(NULL); + + if (!pContext->IrpQueue) + { + WLog_ERR(TAG, "MessageQueue_New failed!"); + goto error_irpqueue; + } + + pContext->thread = CreateThread(NULL, 0, smartcard_context_thread, pContext, 0, NULL); + + if (!pContext->thread) + { + WLog_ERR(TAG, "CreateThread failed!"); + goto error_thread; + } + + return pContext; +error_thread: + MessageQueue_Free(pContext->IrpQueue); +error_irpqueue: + free(pContext); + return NULL; +} + +void smartcard_context_free(void* pCtx) +{ + SMARTCARD_CONTEXT* pContext = pCtx; + + if (!pContext) + return; + + /* cancel blocking calls like SCardGetStatusChange */ + SCardCancel(pContext->hContext); + SCardReleaseContext(pContext->hContext); + + if (MessageQueue_PostQuit(pContext->IrpQueue, 0) && + (WaitForSingleObject(pContext->thread, INFINITE) == WAIT_FAILED)) + WLog_ERR(TAG, "WaitForSingleObject failed with error %" PRIu32 "!", GetLastError()); + + CloseHandle(pContext->thread); + MessageQueue_Free(pContext->IrpQueue); + free(pContext); +} + +static void smartcard_release_all_contexts(SMARTCARD_DEVICE* smartcard) +{ + int index; + int keyCount; + ULONG_PTR* pKeys; + SCARDCONTEXT hContext; + SMARTCARD_CONTEXT* pContext; + + /** + * On protocol termination, the following actions are performed: + * For each context in rgSCardContextList, SCardCancel is called causing all + * SCardGetStatusChange calls to be processed. After that, SCardReleaseContext is called on each + * context and the context MUST be removed from rgSCardContextList. + */ + + /** + * Call SCardCancel on existing contexts, unblocking all outstanding SCardGetStatusChange calls. + */ + + ListDictionary_Lock(smartcard->rgSCardContextList); + if (ListDictionary_Count(smartcard->rgSCardContextList) > 0) + { + pKeys = NULL; + keyCount = ListDictionary_GetKeys(smartcard->rgSCardContextList, &pKeys); + + for (index = 0; index < keyCount; index++) + { + pContext = (SMARTCARD_CONTEXT*)ListDictionary_GetItemValue( + smartcard->rgSCardContextList, (void*)pKeys[index]); + + if (!pContext) + continue; + + hContext = pContext->hContext; + + if (SCardIsValidContext(hContext) == SCARD_S_SUCCESS) + { + SCardCancel(hContext); + } + } + + free(pKeys); + } + ListDictionary_Unlock(smartcard->rgSCardContextList); + + /* Put thread to sleep so that PC/SC can process the cancel requests. This fixes a race + * condition that sometimes caused the pc/sc daemon to crash on MacOS (_xpc_api_misuse) */ + SleepEx(100, FALSE); + + /** + * Call SCardReleaseContext on remaining contexts and remove them from rgSCardContextList. + */ + + ListDictionary_Lock(smartcard->rgSCardContextList); + if (ListDictionary_Count(smartcard->rgSCardContextList) > 0) + { + pKeys = NULL; + keyCount = ListDictionary_GetKeys(smartcard->rgSCardContextList, &pKeys); + + for (index = 0; index < keyCount; index++) + { + ListDictionary_SetItemValue(smartcard->rgSCardContextList, (void*)pKeys[index], NULL); + } + + free(pKeys); + } + ListDictionary_Unlock(smartcard->rgSCardContextList); +} + +static UINT smartcard_free_(SMARTCARD_DEVICE* smartcard) +{ + if (!smartcard) + return CHANNEL_RC_OK; + + if (smartcard->IrpQueue) + { + MessageQueue_Free(smartcard->IrpQueue); + CloseHandle(smartcard->thread); + } + + Stream_Free(smartcard->device.data, TRUE); + LinkedList_Free(smartcard->names); + ListDictionary_Free(smartcard->rgSCardContextList); + ListDictionary_Free(smartcard->rgOutstandingMessages); + Queue_Free(smartcard->CompletedIrpQueue); + + if (smartcard->StartedEvent) + SCardReleaseStartedEvent(); + + free(smartcard); + return CHANNEL_RC_OK; +} +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT smartcard_free(DEVICE* device) +{ + UINT error; + SMARTCARD_DEVICE* smartcard = CAST_FROM_DEVICE(device); + + if (!smartcard) + return ERROR_INVALID_PARAMETER; + + /** + * Calling smartcard_release_all_contexts to unblock all operations waiting for transactions + * to unlock. + */ + smartcard_release_all_contexts(smartcard); + + /* Stopping all threads and cancelling all IRPs */ + + if (smartcard->IrpQueue) + { + if (MessageQueue_PostQuit(smartcard->IrpQueue, 0) && + (WaitForSingleObject(smartcard->thread, INFINITE) == WAIT_FAILED)) + { + error = GetLastError(); + WLog_ERR(TAG, "WaitForSingleObject failed with error %" PRIu32 "!", error); + return error; + } + } + + if (sSmartcard == smartcard) + sSmartcard = NULL; + + return smartcard_free_(smartcard); +} + +/** + * Initialization occurs when the protocol server sends a device announce message. + * At that time, we need to cancel all outstanding IRPs. + */ + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT smartcard_init(DEVICE* device) +{ + SMARTCARD_DEVICE* smartcard = CAST_FROM_DEVICE(device); + + if (!smartcard) + return ERROR_INVALID_PARAMETER; + + smartcard_release_all_contexts(smartcard); + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT smartcard_complete_irp(SMARTCARD_DEVICE* smartcard, IRP* irp) +{ + void* key; + key = (void*)(size_t)irp->CompletionId; + ListDictionary_Remove(smartcard->rgOutstandingMessages, key); + return irp->Complete(irp); +} + +/** + * Multiple threads and SCardGetStatusChange: + * http://musclecard.996296.n3.nabble.com/Multiple-threads-and-SCardGetStatusChange-td4430.html + */ + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT smartcard_process_irp(SMARTCARD_DEVICE* smartcard, IRP* irp) +{ + void* key; + LONG status; + BOOL asyncIrp = FALSE; + SMARTCARD_CONTEXT* pContext = NULL; + SMARTCARD_OPERATION* operation = NULL; + key = (void*)(size_t)irp->CompletionId; + + if (!ListDictionary_Add(smartcard->rgOutstandingMessages, key, irp)) + { + WLog_ERR(TAG, "ListDictionary_Add failed!"); + return ERROR_INTERNAL_ERROR; + } + + if (irp->MajorFunction == IRP_MJ_DEVICE_CONTROL) + { + operation = (SMARTCARD_OPERATION*)calloc(1, sizeof(SMARTCARD_OPERATION)); + + if (!operation) + { + WLog_ERR(TAG, "calloc failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + operation->irp = irp; + status = smartcard_irp_device_control_decode(smartcard, operation); + + if (status != SCARD_S_SUCCESS) + { + irp->IoStatus = (UINT32)STATUS_UNSUCCESSFUL; + + if (!Queue_Enqueue(smartcard->CompletedIrpQueue, (void*)irp)) + { + free(operation); + WLog_ERR(TAG, "Queue_Enqueue failed!"); + return ERROR_INTERNAL_ERROR; + } + + free(operation); + return CHANNEL_RC_OK; + } + + asyncIrp = TRUE; + + switch (operation->ioControlCode) + { + case SCARD_IOCTL_ESTABLISHCONTEXT: + case SCARD_IOCTL_RELEASECONTEXT: + case SCARD_IOCTL_ISVALIDCONTEXT: + case SCARD_IOCTL_CANCEL: + case SCARD_IOCTL_ACCESSSTARTEDEVENT: + case SCARD_IOCTL_RELEASETARTEDEVENT: + asyncIrp = FALSE; + break; + + case SCARD_IOCTL_LISTREADERGROUPSA: + case SCARD_IOCTL_LISTREADERGROUPSW: + case SCARD_IOCTL_LISTREADERSA: + case SCARD_IOCTL_LISTREADERSW: + case SCARD_IOCTL_INTRODUCEREADERGROUPA: + case SCARD_IOCTL_INTRODUCEREADERGROUPW: + case SCARD_IOCTL_FORGETREADERGROUPA: + case SCARD_IOCTL_FORGETREADERGROUPW: + case SCARD_IOCTL_INTRODUCEREADERA: + case SCARD_IOCTL_INTRODUCEREADERW: + case SCARD_IOCTL_FORGETREADERA: + case SCARD_IOCTL_FORGETREADERW: + case SCARD_IOCTL_ADDREADERTOGROUPA: + case SCARD_IOCTL_ADDREADERTOGROUPW: + case SCARD_IOCTL_REMOVEREADERFROMGROUPA: + case SCARD_IOCTL_REMOVEREADERFROMGROUPW: + case SCARD_IOCTL_LOCATECARDSA: + case SCARD_IOCTL_LOCATECARDSW: + case SCARD_IOCTL_LOCATECARDSBYATRA: + case SCARD_IOCTL_LOCATECARDSBYATRW: + case SCARD_IOCTL_READCACHEA: + case SCARD_IOCTL_READCACHEW: + case SCARD_IOCTL_WRITECACHEA: + case SCARD_IOCTL_WRITECACHEW: + case SCARD_IOCTL_GETREADERICON: + case SCARD_IOCTL_GETDEVICETYPEID: + case SCARD_IOCTL_GETSTATUSCHANGEA: + case SCARD_IOCTL_GETSTATUSCHANGEW: + case SCARD_IOCTL_CONNECTA: + case SCARD_IOCTL_CONNECTW: + case SCARD_IOCTL_RECONNECT: + case SCARD_IOCTL_DISCONNECT: + case SCARD_IOCTL_BEGINTRANSACTION: + case SCARD_IOCTL_ENDTRANSACTION: + case SCARD_IOCTL_STATE: + case SCARD_IOCTL_STATUSA: + case SCARD_IOCTL_STATUSW: + case SCARD_IOCTL_TRANSMIT: + case SCARD_IOCTL_CONTROL: + case SCARD_IOCTL_GETATTRIB: + case SCARD_IOCTL_SETATTRIB: + case SCARD_IOCTL_GETTRANSMITCOUNT: + asyncIrp = TRUE; + break; + } + + pContext = + ListDictionary_GetItemValue(smartcard->rgSCardContextList, (void*)operation->hContext); + + if (!pContext) + asyncIrp = FALSE; + + if (!asyncIrp) + { + if ((status = smartcard_irp_device_control_call(smartcard, operation))) + { + WLog_ERR(TAG, "smartcard_irp_device_control_call failed with error %" PRId32 "!", + status); + return (UINT32)status; + } + + if (!Queue_Enqueue(smartcard->CompletedIrpQueue, (void*)irp)) + { + free(operation); + WLog_ERR(TAG, "Queue_Enqueue failed!"); + return ERROR_INTERNAL_ERROR; + } + + free(operation); + } + else + { + if (pContext) + { + if (!MessageQueue_Post(pContext->IrpQueue, NULL, 0, (void*)operation, NULL)) + { + WLog_ERR(TAG, "MessageQueue_Post failed!"); + return ERROR_INTERNAL_ERROR; + } + } + } + } + else + { + WLog_ERR(TAG, + "Unexpected SmartCard IRP: MajorFunction 0x%08" PRIX32 + " MinorFunction: 0x%08" PRIX32 "", + irp->MajorFunction, irp->MinorFunction); + irp->IoStatus = (UINT32)STATUS_NOT_SUPPORTED; + + if (!Queue_Enqueue(smartcard->CompletedIrpQueue, (void*)irp)) + { + WLog_ERR(TAG, "Queue_Enqueue failed!"); + return ERROR_INTERNAL_ERROR; + } + } + + return CHANNEL_RC_OK; +} + +static DWORD WINAPI smartcard_thread_func(LPVOID arg) +{ + IRP* irp; + DWORD nCount; + DWORD status; + HANDLE hEvents[2]; + wMessage message; + UINT error = CHANNEL_RC_OK; + SMARTCARD_DEVICE* smartcard = CAST_FROM_DEVICE(arg); + + if (!smartcard) + return ERROR_INVALID_PARAMETER; + + nCount = 0; + hEvents[nCount++] = MessageQueue_Event(smartcard->IrpQueue); + hEvents[nCount++] = Queue_Event(smartcard->CompletedIrpQueue); + + while (1) + { + status = WaitForMultipleObjects(nCount, hEvents, FALSE, INFINITE); + + if (status == WAIT_FAILED) + { + error = GetLastError(); + WLog_ERR(TAG, "WaitForMultipleObjects failed with error %" PRIu32 "!", error); + break; + } + + status = WaitForSingleObject(MessageQueue_Event(smartcard->IrpQueue), 0); + + if (status == WAIT_FAILED) + { + error = GetLastError(); + WLog_ERR(TAG, "WaitForSingleObject failed with error %" PRIu32 "!", error); + break; + } + + if (status == WAIT_OBJECT_0) + { + if (!MessageQueue_Peek(smartcard->IrpQueue, &message, TRUE)) + { + WLog_ERR(TAG, "MessageQueue_Peek failed!"); + error = ERROR_INTERNAL_ERROR; + break; + } + + if (message.id == WMQ_QUIT) + { + while (1) + { + status = WaitForSingleObject(Queue_Event(smartcard->CompletedIrpQueue), 0); + + if (status == WAIT_FAILED) + { + error = GetLastError(); + WLog_ERR(TAG, "WaitForSingleObject failed with error %" PRIu32 "!", error); + goto out; + } + + if (status == WAIT_TIMEOUT) + break; + + irp = (IRP*)Queue_Dequeue(smartcard->CompletedIrpQueue); + + if (irp) + { + if (irp->thread) + { + status = WaitForSingleObject(irp->thread, INFINITE); + + if (status == WAIT_FAILED) + { + error = GetLastError(); + WLog_ERR(TAG, "WaitForSingleObject failed with error %" PRIu32 "!", + error); + goto out; + } + + CloseHandle(irp->thread); + irp->thread = NULL; + } + + if ((error = smartcard_complete_irp(smartcard, irp))) + { + WLog_ERR(TAG, "smartcard_complete_irp failed with error %" PRIu32 "!", + error); + goto out; + } + } + } + + break; + } + + irp = (IRP*)message.wParam; + + if (irp) + { + if ((error = smartcard_process_irp(smartcard, irp))) + { + WLog_ERR(TAG, "smartcard_process_irp failed with error %" PRIu32 "!", error); + goto out; + } + } + } + + status = WaitForSingleObject(Queue_Event(smartcard->CompletedIrpQueue), 0); + + if (status == WAIT_FAILED) + { + error = GetLastError(); + WLog_ERR(TAG, "WaitForSingleObject failed with error %" PRIu32 "!", error); + break; + } + + if (status == WAIT_OBJECT_0) + { + irp = (IRP*)Queue_Dequeue(smartcard->CompletedIrpQueue); + + if (irp) + { + if (irp->thread) + { + status = WaitForSingleObject(irp->thread, INFINITE); + + if (status == WAIT_FAILED) + { + error = GetLastError(); + WLog_ERR(TAG, "WaitForSingleObject failed with error %" PRIu32 "!", error); + break; + } + + CloseHandle(irp->thread); + irp->thread = NULL; + } + + if ((error = smartcard_complete_irp(smartcard, irp))) + { + if (error == CHANNEL_RC_NOT_CONNECTED) + { + error = CHANNEL_RC_OK; + goto out; + } + + WLog_ERR(TAG, "smartcard_complete_irp failed with error %" PRIu32 "!", error); + goto out; + } + } + } + } + +out: + + if (error && smartcard->rdpcontext) + setChannelError(smartcard->rdpcontext, error, "smartcard_thread_func reported an error"); + + ExitThread(error); + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT smartcard_irp_request(DEVICE* device, IRP* irp) +{ + SMARTCARD_DEVICE* smartcard = CAST_FROM_DEVICE(device); + + if (!smartcard) + return ERROR_INVALID_PARAMETER; + + if (!MessageQueue_Post(smartcard->IrpQueue, NULL, 0, (void*)irp, NULL)) + { + WLog_ERR(TAG, "MessageQueue_Post failed!"); + return ERROR_INTERNAL_ERROR; + } + + return CHANNEL_RC_OK; +} + +/* smartcard is always built-in */ +#define DeviceServiceEntry smartcard_DeviceServiceEntry + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT DeviceServiceEntry(PDEVICE_SERVICE_ENTRY_POINTS pEntryPoints) +{ + SMARTCARD_DEVICE* smartcard = NULL; + size_t length; + UINT error = CHANNEL_RC_NO_MEMORY; + + if (!sSmartcard) + { + wObject* obj; + smartcard = (SMARTCARD_DEVICE*)calloc(1, sizeof(SMARTCARD_DEVICE)); + + if (!smartcard) + { + WLog_ERR(TAG, "calloc failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + smartcard->device.type = RDPDR_DTYP_SMARTCARD; + smartcard->device.name = "SCARD"; + smartcard->device.IRPRequest = smartcard_irp_request; + smartcard->device.Init = smartcard_init; + smartcard->device.Free = smartcard_free; + smartcard->names = LinkedList_New(); + smartcard->rdpcontext = pEntryPoints->rdpcontext; + length = strlen(smartcard->device.name); + smartcard->device.data = Stream_New(NULL, length + 1); + + if (!smartcard->device.data || !smartcard->names) + { + WLog_ERR(TAG, "Stream_New failed!"); + goto fail; + } + + Stream_Write(smartcard->device.data, "SCARD", 6); + smartcard->IrpQueue = MessageQueue_New(NULL); + + if (!smartcard->IrpQueue) + { + WLog_ERR(TAG, "MessageQueue_New failed!"); + goto fail; + } + + smartcard->CompletedIrpQueue = Queue_New(TRUE, -1, -1); + + if (!smartcard->CompletedIrpQueue) + { + WLog_ERR(TAG, "Queue_New failed!"); + goto fail; + } + + smartcard->rgSCardContextList = ListDictionary_New(TRUE); + + if (!smartcard->rgSCardContextList) + { + WLog_ERR(TAG, "ListDictionary_New failed!"); + goto fail; + } + + obj = ListDictionary_ValueObject(smartcard->rgSCardContextList); + obj->fnObjectFree = smartcard_context_free; + smartcard->rgOutstandingMessages = ListDictionary_New(TRUE); + + if (!smartcard->rgOutstandingMessages) + { + WLog_ERR(TAG, "ListDictionary_New failed!"); + goto fail; + } + + if ((error = pEntryPoints->RegisterDevice(pEntryPoints->devman, &smartcard->device))) + { + WLog_ERR(TAG, "RegisterDevice failed!"); + goto fail; + } + + smartcard->thread = + CreateThread(NULL, 0, smartcard_thread_func, smartcard, CREATE_SUSPENDED, NULL); + + if (!smartcard->thread) + { + WLog_ERR(TAG, "ListDictionary_New failed!"); + error = ERROR_INTERNAL_ERROR; + goto fail; + } + + ResumeThread(smartcard->thread); + } + else + smartcard = sSmartcard; + + if (pEntryPoints->device->Name) + LinkedList_AddLast(smartcard->names, pEntryPoints->device->Name); + + sSmartcard = smartcard; + return CHANNEL_RC_OK; +fail: + smartcard_free_(smartcard); + return error; +} diff --git a/channels/smartcard/client/smartcard_main.h b/channels/smartcard/client/smartcard_main.h new file mode 100644 index 0000000..4d543d1 --- /dev/null +++ b/channels/smartcard/client/smartcard_main.h @@ -0,0 +1,73 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Smartcard Device Service Virtual Channel + * + * Copyright 2011 O.S. Systems Software Ltda. + * Copyright 2011 Eduardo Fiss Beloni + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger + * + * Licensed 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 FREERDP_CHANNEL_SMARTCARD_CLIENT_MAIN_H +#define FREERDP_CHANNEL_SMARTCARD_CLIENT_MAIN_H + +#include +#include + +#include +#include +#include +#include +#include + +#include "smartcard_operations.h" + +#define TAG CHANNELS_TAG("smartcard.client") + +typedef struct _SMARTCARD_DEVICE SMARTCARD_DEVICE; + +struct _SMARTCARD_CONTEXT +{ + HANDLE thread; + SCARDCONTEXT hContext; + wMessageQueue* IrpQueue; + SMARTCARD_DEVICE* smartcard; +}; +typedef struct _SMARTCARD_CONTEXT SMARTCARD_CONTEXT; + +struct _SMARTCARD_DEVICE +{ + DEVICE device; + + HANDLE thread; + HANDLE StartedEvent; + wMessageQueue* IrpQueue; + wQueue* CompletedIrpQueue; + wListDictionary* rgSCardContextList; + wListDictionary* rgOutstandingMessages; + rdpContext* rdpcontext; + wLinkedList* names; +}; + +SMARTCARD_CONTEXT* smartcard_context_new(SMARTCARD_DEVICE* smartcard, SCARDCONTEXT hContext); +void smartcard_context_free(void* pContext); + +LONG smartcard_irp_device_control_decode(SMARTCARD_DEVICE* smartcard, + SMARTCARD_OPERATION* operation); +LONG smartcard_irp_device_control_call(SMARTCARD_DEVICE* smartcard, SMARTCARD_OPERATION* operation); + +#include "smartcard_pack.h" + +#endif /* FREERDP_CHANNEL_SMARTCARD_CLIENT_MAIN_H */ diff --git a/channels/smartcard/client/smartcard_operations.c b/channels/smartcard/client/smartcard_operations.c new file mode 100644 index 0000000..4639b17 --- /dev/null +++ b/channels/smartcard/client/smartcard_operations.c @@ -0,0 +1,2695 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Smartcard Device Service Virtual Channel + * + * Copyright (C) Alexi Volkov 2006 + * Copyright 2011 O.S. Systems Software Ltda. + * Copyright 2011 Anthony Tong + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger + * Copyright 2017 Armin Novak + * Copyright 2017 Thincast Technologies GmbH + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include + +#include +#include +#include +#include + +#include +#include + +#include "smartcard_operations.h" +#include "smartcard_main.h" + +static LONG smartcard_call_to_operation_handle(SMARTCARD_DEVICE* smartcard, + SMARTCARD_OPERATION* operation) +{ + if (!smartcard || !operation) + return SCARD_E_INVALID_HANDLE; + operation->hContext = + smartcard_scard_context_native_from_redir(smartcard, &(operation->call.handles.hContext)); + operation->hCard = + smartcard_scard_handle_native_from_redir(smartcard, &(operation->call.handles.hCard)); + + return SCARD_S_SUCCESS; +} + +static LONG log_status_error(const char* tag, const char* what, LONG status) +{ + if (status != SCARD_S_SUCCESS) + { + DWORD level = WLOG_ERROR; + switch (status) + { + case SCARD_E_TIMEOUT: + level = WLOG_DEBUG; + break; + case SCARD_E_NO_READERS_AVAILABLE: + level = WLOG_INFO; + break; + default: + break; + } + WLog_Print(WLog_Get(tag), level, "%s failed with error %s [%" PRId32 "]", what, + SCardGetErrorString(status), status); + } + return status; +} + +static const char* smartcard_get_ioctl_string(UINT32 ioControlCode, BOOL funcName) +{ + switch (ioControlCode) + { + case SCARD_IOCTL_ESTABLISHCONTEXT: + return funcName ? "SCardEstablishContext" : "SCARD_IOCTL_ESTABLISHCONTEXT"; + + case SCARD_IOCTL_RELEASECONTEXT: + return funcName ? "SCardReleaseContext" : "SCARD_IOCTL_RELEASECONTEXT"; + + case SCARD_IOCTL_ISVALIDCONTEXT: + return funcName ? "SCardIsValidContext" : "SCARD_IOCTL_ISVALIDCONTEXT"; + + case SCARD_IOCTL_LISTREADERGROUPSA: + return funcName ? "SCardListReaderGroupsA" : "SCARD_IOCTL_LISTREADERGROUPSA"; + + case SCARD_IOCTL_LISTREADERGROUPSW: + return funcName ? "SCardListReaderGroupsW" : "SCARD_IOCTL_LISTREADERGROUPSW"; + + case SCARD_IOCTL_LISTREADERSA: + return funcName ? "SCardListReadersA" : "SCARD_IOCTL_LISTREADERSA"; + + case SCARD_IOCTL_LISTREADERSW: + return funcName ? "SCardListReadersW" : "SCARD_IOCTL_LISTREADERSW"; + + case SCARD_IOCTL_INTRODUCEREADERGROUPA: + return funcName ? "SCardIntroduceReaderGroupA" : "SCARD_IOCTL_INTRODUCEREADERGROUPA"; + + case SCARD_IOCTL_INTRODUCEREADERGROUPW: + return funcName ? "SCardIntroduceReaderGroupW" : "SCARD_IOCTL_INTRODUCEREADERGROUPW"; + + case SCARD_IOCTL_FORGETREADERGROUPA: + return funcName ? "SCardForgetReaderGroupA" : "SCARD_IOCTL_FORGETREADERGROUPA"; + + case SCARD_IOCTL_FORGETREADERGROUPW: + return funcName ? "SCardForgetReaderGroupW" : "SCARD_IOCTL_FORGETREADERGROUPW"; + + case SCARD_IOCTL_INTRODUCEREADERA: + return funcName ? "SCardIntroduceReaderA" : "SCARD_IOCTL_INTRODUCEREADERA"; + + case SCARD_IOCTL_INTRODUCEREADERW: + return funcName ? "SCardIntroduceReaderW" : "SCARD_IOCTL_INTRODUCEREADERW"; + + case SCARD_IOCTL_FORGETREADERA: + return funcName ? "SCardForgetReaderA" : "SCARD_IOCTL_FORGETREADERA"; + + case SCARD_IOCTL_FORGETREADERW: + return funcName ? "SCardForgetReaderW" : "SCARD_IOCTL_FORGETREADERW"; + + case SCARD_IOCTL_ADDREADERTOGROUPA: + return funcName ? "SCardAddReaderToGroupA" : "SCARD_IOCTL_ADDREADERTOGROUPA"; + + case SCARD_IOCTL_ADDREADERTOGROUPW: + return funcName ? "SCardAddReaderToGroupW" : "SCARD_IOCTL_ADDREADERTOGROUPW"; + + case SCARD_IOCTL_REMOVEREADERFROMGROUPA: + return funcName ? "SCardRemoveReaderFromGroupA" : "SCARD_IOCTL_REMOVEREADERFROMGROUPA"; + + case SCARD_IOCTL_REMOVEREADERFROMGROUPW: + return funcName ? "SCardRemoveReaderFromGroupW" : "SCARD_IOCTL_REMOVEREADERFROMGROUPW"; + + case SCARD_IOCTL_LOCATECARDSA: + return funcName ? "SCardLocateCardsA" : "SCARD_IOCTL_LOCATECARDSA"; + + case SCARD_IOCTL_LOCATECARDSW: + return funcName ? "SCardLocateCardsW" : "SCARD_IOCTL_LOCATECARDSW"; + + case SCARD_IOCTL_GETSTATUSCHANGEA: + return funcName ? "SCardGetStatusChangeA" : "SCARD_IOCTL_GETSTATUSCHANGEA"; + + case SCARD_IOCTL_GETSTATUSCHANGEW: + return funcName ? "SCardGetStatusChangeW" : "SCARD_IOCTL_GETSTATUSCHANGEW"; + + case SCARD_IOCTL_CANCEL: + return funcName ? "SCardCancel" : "SCARD_IOCTL_CANCEL"; + + case SCARD_IOCTL_CONNECTA: + return funcName ? "SCardConnectA" : "SCARD_IOCTL_CONNECTA"; + + case SCARD_IOCTL_CONNECTW: + return funcName ? "SCardConnectW" : "SCARD_IOCTL_CONNECTW"; + + case SCARD_IOCTL_RECONNECT: + return funcName ? "SCardReconnect" : "SCARD_IOCTL_RECONNECT"; + + case SCARD_IOCTL_DISCONNECT: + return funcName ? "SCardDisconnect" : "SCARD_IOCTL_DISCONNECT"; + + case SCARD_IOCTL_BEGINTRANSACTION: + return funcName ? "SCardBeginTransaction" : "SCARD_IOCTL_BEGINTRANSACTION"; + + case SCARD_IOCTL_ENDTRANSACTION: + return funcName ? "SCardEndTransaction" : "SCARD_IOCTL_ENDTRANSACTION"; + + case SCARD_IOCTL_STATE: + return funcName ? "SCardState" : "SCARD_IOCTL_STATE"; + + case SCARD_IOCTL_STATUSA: + return funcName ? "SCardStatusA" : "SCARD_IOCTL_STATUSA"; + + case SCARD_IOCTL_STATUSW: + return funcName ? "SCardStatusW" : "SCARD_IOCTL_STATUSW"; + + case SCARD_IOCTL_TRANSMIT: + return funcName ? "SCardTransmit" : "SCARD_IOCTL_TRANSMIT"; + + case SCARD_IOCTL_CONTROL: + return funcName ? "SCardControl" : "SCARD_IOCTL_CONTROL"; + + case SCARD_IOCTL_GETATTRIB: + return funcName ? "SCardGetAttrib" : "SCARD_IOCTL_GETATTRIB"; + + case SCARD_IOCTL_SETATTRIB: + return funcName ? "SCardSetAttrib" : "SCARD_IOCTL_SETATTRIB"; + + case SCARD_IOCTL_ACCESSSTARTEDEVENT: + return funcName ? "SCardAccessStartedEvent" : "SCARD_IOCTL_ACCESSSTARTEDEVENT"; + + case SCARD_IOCTL_LOCATECARDSBYATRA: + return funcName ? "SCardLocateCardsByATRA" : "SCARD_IOCTL_LOCATECARDSBYATRA"; + + case SCARD_IOCTL_LOCATECARDSBYATRW: + return funcName ? "SCardLocateCardsByATRB" : "SCARD_IOCTL_LOCATECARDSBYATRW"; + + case SCARD_IOCTL_READCACHEA: + return funcName ? "SCardReadCacheA" : "SCARD_IOCTL_READCACHEA"; + + case SCARD_IOCTL_READCACHEW: + return funcName ? "SCardReadCacheW" : "SCARD_IOCTL_READCACHEW"; + + case SCARD_IOCTL_WRITECACHEA: + return funcName ? "SCardWriteCacheA" : "SCARD_IOCTL_WRITECACHEA"; + + case SCARD_IOCTL_WRITECACHEW: + return funcName ? "SCardWriteCacheW" : "SCARD_IOCTL_WRITECACHEW"; + + case SCARD_IOCTL_GETTRANSMITCOUNT: + return funcName ? "SCardGetTransmitCount" : "SCARD_IOCTL_GETTRANSMITCOUNT"; + + case SCARD_IOCTL_RELEASETARTEDEVENT: + return funcName ? "SCardReleaseStartedEvent" : "SCARD_IOCTL_RELEASETARTEDEVENT"; + + case SCARD_IOCTL_GETREADERICON: + return funcName ? "SCardGetReaderIcon" : "SCARD_IOCTL_GETREADERICON"; + + case SCARD_IOCTL_GETDEVICETYPEID: + return funcName ? "SCardGetDeviceTypeId" : "SCARD_IOCTL_GETDEVICETYPEID"; + + default: + return funcName ? "SCardUnknown" : "SCARD_IOCTL_UNKNOWN"; + } +} + +static LONG smartcard_EstablishContext_Decode(SMARTCARD_DEVICE* smartcard, + SMARTCARD_OPERATION* operation) +{ + LONG status; + IRP* irp; + + if (!operation || !operation->irp) + return STATUS_NO_MEMORY; + + irp = operation->irp; + status = smartcard_unpack_establish_context_call(smartcard, irp->input, + &operation->call.establishContext); + if (status != SCARD_S_SUCCESS) + { + return log_status_error(TAG, "smartcard_unpack_establish_context_call", status); + } + + return SCARD_S_SUCCESS; +} + +static LONG smartcard_EstablishContext_Call(SMARTCARD_DEVICE* smartcard, + SMARTCARD_OPERATION* operation) +{ + LONG status; + SCARDCONTEXT hContext = { 0 }; + EstablishContext_Return ret = { 0 }; + IRP* irp = operation->irp; + EstablishContext_Call* call = &operation->call.establishContext; + status = ret.ReturnCode = SCardEstablishContext(call->dwScope, NULL, NULL, &hContext); + + if (ret.ReturnCode == SCARD_S_SUCCESS) + { + SMARTCARD_CONTEXT* pContext; + void* key = (void*)(size_t)hContext; + // TODO: handle return values + pContext = smartcard_context_new(smartcard, hContext); + + if (!pContext) + { + WLog_ERR(TAG, "smartcard_context_new failed!"); + return STATUS_NO_MEMORY; + } + + if (!ListDictionary_Add(smartcard->rgSCardContextList, key, (void*)pContext)) + { + WLog_ERR(TAG, "ListDictionary_Add failed!"); + return STATUS_INTERNAL_ERROR; + } + } + else + { + return log_status_error(TAG, "SCardEstablishContext", status); + } + + smartcard_scard_context_native_to_redir(smartcard, &(ret.hContext), hContext); + + status = smartcard_pack_establish_context_return(smartcard, irp->output, &ret); + if (status != SCARD_S_SUCCESS) + { + return log_status_error(TAG, "smartcard_pack_establish_context_return", status); + } + + return ret.ReturnCode; +} + +static LONG smartcard_ReleaseContext_Decode(SMARTCARD_DEVICE* smartcard, + SMARTCARD_OPERATION* operation) +{ + LONG status; + IRP* irp; + + if (!operation || !operation->irp) + return STATUS_NO_MEMORY; + + irp = operation->irp; + status = smartcard_unpack_context_call(smartcard, irp->input, &operation->call.context, + "ReleaseContext"); + if (status != SCARD_S_SUCCESS) + log_status_error(TAG, "smartcard_unpack_context_call", status); + + return status; +} + +static LONG smartcard_ReleaseContext_Call(SMARTCARD_DEVICE* smartcard, + SMARTCARD_OPERATION* operation) +{ + Long_Return ret = { 0 }; + ret.ReturnCode = SCardReleaseContext(operation->hContext); + + if (ret.ReturnCode == SCARD_S_SUCCESS) + { + SMARTCARD_CONTEXT* pContext; + void* key = (void*)(size_t)operation->hContext; + pContext = (SMARTCARD_CONTEXT*)ListDictionary_Remove(smartcard->rgSCardContextList, key); + smartcard_context_free(pContext); + } + else + { + return log_status_error(TAG, "SCardReleaseContext", ret.ReturnCode); + } + + smartcard_trace_long_return(smartcard, &ret, "ReleaseContext"); + return ret.ReturnCode; +} + +static LONG smartcard_IsValidContext_Decode(SMARTCARD_DEVICE* smartcard, + SMARTCARD_OPERATION* operation) +{ + LONG status; + IRP* irp; + + if (!operation || !operation->irp) + return STATUS_NO_MEMORY; + + irp = operation->irp; + status = smartcard_unpack_context_call(smartcard, irp->input, &operation->call.context, + "IsValidContext"); + + return status; +} + +static LONG smartcard_IsValidContext_Call(SMARTCARD_DEVICE* smartcard, + SMARTCARD_OPERATION* operation) +{ + Long_Return ret = { 0 }; + + ret.ReturnCode = SCardIsValidContext(operation->hContext); + smartcard_trace_long_return(smartcard, &ret, "IsValidContext"); + return ret.ReturnCode; +} + +static LONG smartcard_ListReaderGroupsA_Decode(SMARTCARD_DEVICE* smartcard, + SMARTCARD_OPERATION* operation) +{ + LONG status; + IRP* irp; + + if (!operation || !operation->irp) + return STATUS_NO_MEMORY; + irp = operation->irp; + status = smartcard_unpack_list_reader_groups_call(smartcard, irp->input, + &operation->call.listReaderGroups, FALSE); + + return status; +} + +static LONG smartcard_ListReaderGroupsA_Call(SMARTCARD_DEVICE* smartcard, + SMARTCARD_OPERATION* operation) +{ + LONG status; + ListReaderGroups_Return ret = { 0 }; + LPSTR mszGroups = NULL; + DWORD cchGroups = 0; + IRP* irp = operation->irp; + cchGroups = SCARD_AUTOALLOCATE; + ret.ReturnCode = SCardListReaderGroupsA(operation->hContext, (LPSTR)&mszGroups, &cchGroups); + ret.msz = (BYTE*)mszGroups; + ret.cBytes = cchGroups; + + status = smartcard_pack_list_reader_groups_return(smartcard, irp->output, &ret, FALSE); + + if (status != SCARD_S_SUCCESS) + return status; + + if (mszGroups) + SCardFreeMemory(operation->hContext, mszGroups); + + return ret.ReturnCode; +} + +static LONG smartcard_ListReaderGroupsW_Decode(SMARTCARD_DEVICE* smartcard, + SMARTCARD_OPERATION* operation) +{ + LONG status; + IRP* irp; + + if (!operation || !operation->irp) + return STATUS_NO_MEMORY; + irp = operation->irp; + status = smartcard_unpack_list_reader_groups_call(smartcard, irp->input, + &operation->call.listReaderGroups, TRUE); + + return status; +} + +static LONG smartcard_ListReaderGroupsW_Call(SMARTCARD_DEVICE* smartcard, + SMARTCARD_OPERATION* operation) +{ + LONG status; + ListReaderGroups_Return ret = { 0 }; + LPWSTR mszGroups = NULL; + DWORD cchGroups = 0; + IRP* irp; + + if (!operation || !operation->irp) + return STATUS_NO_MEMORY; + + irp = operation->irp; + cchGroups = SCARD_AUTOALLOCATE; + status = ret.ReturnCode = + SCardListReaderGroupsW(operation->hContext, (LPWSTR)&mszGroups, &cchGroups); + ret.msz = (BYTE*)mszGroups; + ret.cBytes = cchGroups * sizeof(WCHAR); + + if (status != SCARD_S_SUCCESS) + return status; + + status = smartcard_pack_list_reader_groups_return(smartcard, irp->output, &ret, TRUE); + + if (status != SCARD_S_SUCCESS) + return status; + + if (mszGroups) + SCardFreeMemory(operation->hContext, mszGroups); + + return ret.ReturnCode; +} + +static BOOL filter_match(wLinkedList* list, LPCSTR reader, size_t readerLen) +{ + if (readerLen < 1) + return FALSE; + + LinkedList_Enumerator_Reset(list); + + while (LinkedList_Enumerator_MoveNext(list)) + { + const char* filter = LinkedList_Enumerator_Current(list); + + if (filter) + { + if (strstr(reader, filter) != NULL) + return TRUE; + } + } + + return FALSE; +} + +static DWORD filter_device_by_name_a(wLinkedList* list, LPSTR* mszReaders, DWORD cchReaders) +{ + size_t rpos = 0, wpos = 0; + + if (*mszReaders == NULL || LinkedList_Count(list) < 1) + return cchReaders; + + do + { + LPCSTR rreader = &(*mszReaders)[rpos]; + LPSTR wreader = &(*mszReaders)[wpos]; + size_t readerLen = strnlen(rreader, cchReaders - rpos); + + rpos += readerLen + 1; + + if (filter_match(list, rreader, readerLen)) + { + if (rreader != wreader) + memmove(wreader, rreader, readerLen + 1); + + wpos += readerLen + 1; + } + } while (rpos < cchReaders); + + /* this string must be double 0 terminated */ + if (rpos != wpos) + { + if (wpos >= cchReaders) + return 0; + + (*mszReaders)[wpos++] = '\0'; + } + + return (DWORD)wpos; +} + +static DWORD filter_device_by_name_w(wLinkedList* list, LPWSTR* mszReaders, DWORD cchReaders) +{ + int res; + DWORD rc; + LPSTR readers = NULL; + + if (LinkedList_Count(list) < 1) + return cchReaders; + + res = ConvertFromUnicode(CP_UTF8, 0, *mszReaders, (int)cchReaders, &readers, 0, NULL, NULL); + + /* When res==0, readers may have been set to NULL by ConvertFromUnicode */ + if ((res < 0) || ((DWORD)res != cchReaders) || (readers == 0)) + return 0; + + free(*mszReaders); + *mszReaders = NULL; + rc = filter_device_by_name_a(list, &readers, cchReaders); + res = ConvertToUnicode(CP_UTF8, 0, readers, (int)rc, mszReaders, 0); + + if ((res < 0) || ((DWORD)res != rc)) + rc = 0; + + free(readers); + return rc; +} + +static LONG smartcard_ListReadersA_Decode(SMARTCARD_DEVICE* smartcard, + SMARTCARD_OPERATION* operation) +{ + LONG status; + IRP* irp; + + if (!operation || !operation->irp) + return STATUS_NO_MEMORY; + irp = operation->irp; + status = smartcard_unpack_list_readers_call(smartcard, irp->input, &operation->call.listReaders, + FALSE); + + return status; +} + +static LONG smartcard_ListReadersA_Call(SMARTCARD_DEVICE* smartcard, SMARTCARD_OPERATION* operation) +{ + LONG status; + ListReaders_Return ret = { 0 }; + LPSTR mszReaders = NULL; + DWORD cchReaders = 0; + IRP* irp = operation->irp; + ListReaders_Call* call = &operation->call.listReaders; + cchReaders = SCARD_AUTOALLOCATE; + status = ret.ReturnCode = SCardListReadersA(operation->hContext, (LPCSTR)call->mszGroups, + (LPSTR)&mszReaders, &cchReaders); + + if (call->mszGroups) + { + free(call->mszGroups); + call->mszGroups = NULL; + } + + if (status != SCARD_S_SUCCESS) + { + return log_status_error(TAG, "SCardListReadersA", status); + } + + cchReaders = filter_device_by_name_a(smartcard->names, &mszReaders, cchReaders); + ret.msz = (BYTE*)mszReaders; + ret.cBytes = cchReaders; + + status = smartcard_pack_list_readers_return(smartcard, irp->output, &ret, FALSE); + if (status != SCARD_S_SUCCESS) + { + return log_status_error(TAG, "smartcard_pack_list_readers_return", status); + } + + if (mszReaders) + SCardFreeMemory(operation->hContext, mszReaders); + + if (status != SCARD_S_SUCCESS) + return status; + + return ret.ReturnCode; +} + +static LONG smartcard_ListReadersW_Decode(SMARTCARD_DEVICE* smartcard, + SMARTCARD_OPERATION* operation) +{ + LONG status; + IRP* irp; + + if (!operation || !operation->irp) + return STATUS_NO_MEMORY; + irp = operation->irp; + status = smartcard_unpack_list_readers_call(smartcard, irp->input, &operation->call.listReaders, + TRUE); + + return status; +} + +static LONG smartcard_context_and_two_strings_a_Decode(SMARTCARD_DEVICE* smartcard, + SMARTCARD_OPERATION* operation) +{ + LONG status; + IRP* irp; + + if (!operation || !operation->irp) + return STATUS_NO_MEMORY; + irp = operation->irp; + status = smartcard_unpack_context_and_two_strings_a_call(smartcard, irp->input, + &operation->call.contextAndTwoStringA); + + return status; +} + +static LONG smartcard_context_and_two_strings_w_Decode(SMARTCARD_DEVICE* smartcard, + SMARTCARD_OPERATION* operation) +{ + LONG status; + IRP* irp; + + if (!operation || !operation->irp) + return STATUS_NO_MEMORY; + irp = operation->irp; + status = smartcard_unpack_context_and_two_strings_w_call(smartcard, irp->input, + &operation->call.contextAndTwoStringW); + + return status; +} + +static LONG smartcard_context_and_string_a_Decode(SMARTCARD_DEVICE* smartcard, + SMARTCARD_OPERATION* operation) +{ + LONG status; + IRP* irp; + + if (!operation || !operation->irp) + return STATUS_NO_MEMORY; + irp = operation->irp; + status = smartcard_unpack_context_and_string_a_call(smartcard, irp->input, + &operation->call.contextAndStringA); + + return status; +} + +static LONG smartcard_context_and_string_w_Decode(SMARTCARD_DEVICE* smartcard, + SMARTCARD_OPERATION* operation) +{ + LONG status; + IRP* irp; + + if (!operation || !operation->irp) + return STATUS_NO_MEMORY; + irp = operation->irp; + status = smartcard_unpack_context_and_string_w_call(smartcard, irp->input, + &operation->call.contextAndStringW); + + return status; +} + +static LONG smartcard_LocateCardsA_Decode(SMARTCARD_DEVICE* smartcard, + SMARTCARD_OPERATION* operation) +{ + LONG status; + IRP* irp; + + if (!operation || !operation->irp) + return STATUS_NO_MEMORY; + irp = operation->irp; + status = + smartcard_unpack_locate_cards_a_call(smartcard, irp->input, &operation->call.locateCardsA); + + return status; +} + +static LONG smartcard_LocateCardsW_Decode(SMARTCARD_DEVICE* smartcard, + SMARTCARD_OPERATION* operation) +{ + LONG status; + IRP* irp; + + if (!operation || !operation->irp) + return STATUS_NO_MEMORY; + irp = operation->irp; + status = + smartcard_unpack_locate_cards_w_call(smartcard, irp->input, &operation->call.locateCardsW); + + return status; +} + +static LONG smartcard_ListReadersW_Call(SMARTCARD_DEVICE* smartcard, SMARTCARD_OPERATION* operation) +{ + LONG status; + ListReaders_Return ret = { 0 }; + DWORD cchReaders = 0; + IRP* irp = operation->irp; + ListReaders_Call* call = &operation->call.listReaders; + union { + const BYTE* bp; + const char* sz; + const WCHAR* wz; + } string; + union { + WCHAR** ppw; + WCHAR* pw; + CHAR* pc; + BYTE* pb; + } mszReaders; + + string.bp = call->mszGroups; + cchReaders = SCARD_AUTOALLOCATE; + status = ret.ReturnCode = + SCardListReadersW(operation->hContext, string.wz, (LPWSTR)&mszReaders.pw, &cchReaders); + + if (call->mszGroups) + { + free(call->mszGroups); + call->mszGroups = NULL; + } + + if (status != SCARD_S_SUCCESS) + return log_status_error(TAG, "SCardListReadersW", status); + + cchReaders = filter_device_by_name_w(smartcard->names, &mszReaders.pw, cchReaders); + ret.msz = mszReaders.pb; + ret.cBytes = cchReaders; + status = smartcard_pack_list_readers_return(smartcard, irp->output, &ret, TRUE); + + if (mszReaders.pb) + SCardFreeMemory(operation->hContext, mszReaders.pb); + + if (status != SCARD_S_SUCCESS) + return status; + + return ret.ReturnCode; +} + +static LONG smartcard_IntroduceReaderGroupA_Call(SMARTCARD_DEVICE* smartcard, + SMARTCARD_OPERATION* operation) +{ + Long_Return ret = { 0 }; + ContextAndStringA_Call* call = &operation->call.contextAndStringA; + ret.ReturnCode = SCardIntroduceReaderGroupA(operation->hContext, call->sz); + log_status_error(TAG, "SCardIntroduceReaderGroupA", ret.ReturnCode); + if (call->sz) + { + free(call->sz); + call->sz = NULL; + } + + smartcard_trace_long_return(smartcard, &ret, "IntroduceReaderGroupA"); + return ret.ReturnCode; +} + +static LONG smartcard_IntroduceReaderGroupW_Call(SMARTCARD_DEVICE* smartcard, + SMARTCARD_OPERATION* operation) +{ + Long_Return ret = { 0 }; + ContextAndStringW_Call* call = &operation->call.contextAndStringW; + ret.ReturnCode = SCardIntroduceReaderGroupW(operation->hContext, call->sz); + log_status_error(TAG, "SCardIntroduceReaderGroupW", ret.ReturnCode); + if (call->sz) + { + free(call->sz); + call->sz = NULL; + } + + smartcard_trace_long_return(smartcard, &ret, "IntroduceReaderGroupW"); + return ret.ReturnCode; +} + +static LONG smartcard_IntroduceReaderA_Call(SMARTCARD_DEVICE* smartcard, + SMARTCARD_OPERATION* operation) +{ + Long_Return ret = { 0 }; + ContextAndTwoStringA_Call* call = &operation->call.contextAndTwoStringA; + ret.ReturnCode = SCardIntroduceReaderA(operation->hContext, call->sz1, call->sz2); + log_status_error(TAG, "SCardIntroduceReaderA", ret.ReturnCode); + free(call->sz1); + call->sz1 = NULL; + free(call->sz2); + call->sz2 = NULL; + + smartcard_trace_long_return(smartcard, &ret, "IntroduceReaderA"); + return ret.ReturnCode; +} + +static LONG smartcard_IntroduceReaderW_Call(SMARTCARD_DEVICE* smartcard, + SMARTCARD_OPERATION* operation) +{ + Long_Return ret = { 0 }; + ContextAndTwoStringW_Call* call = &operation->call.contextAndTwoStringW; + ret.ReturnCode = SCardIntroduceReaderW(operation->hContext, call->sz1, call->sz2); + log_status_error(TAG, "SCardIntroduceReaderW", ret.ReturnCode); + free(call->sz1); + call->sz1 = NULL; + free(call->sz2); + call->sz2 = NULL; + + smartcard_trace_long_return(smartcard, &ret, "IntroduceReaderW"); + return ret.ReturnCode; +} + +static LONG smartcard_ForgetReaderA_Call(SMARTCARD_DEVICE* smartcard, + SMARTCARD_OPERATION* operation) +{ + Long_Return ret = { 0 }; + ContextAndStringA_Call* call = &operation->call.contextAndStringA; + ret.ReturnCode = SCardForgetReaderA(operation->hContext, call->sz); + log_status_error(TAG, "SCardForgetReaderA", ret.ReturnCode); + if (call->sz) + { + free(call->sz); + call->sz = NULL; + } + + smartcard_trace_long_return(smartcard, &ret, "SCardForgetReaderA"); + return ret.ReturnCode; +} + +static LONG smartcard_ForgetReaderW_Call(SMARTCARD_DEVICE* smartcard, + SMARTCARD_OPERATION* operation) +{ + Long_Return ret = { 0 }; + ContextAndStringW_Call* call = &operation->call.contextAndStringW; + ret.ReturnCode = SCardForgetReaderW(operation->hContext, call->sz); + log_status_error(TAG, "SCardForgetReaderW", ret.ReturnCode); + if (call->sz) + { + free(call->sz); + call->sz = NULL; + } + + smartcard_trace_long_return(smartcard, &ret, "SCardForgetReaderW"); + return ret.ReturnCode; +} + +static LONG smartcard_AddReaderToGroupA_Call(SMARTCARD_DEVICE* smartcard, + SMARTCARD_OPERATION* operation) +{ + Long_Return ret = { 0 }; + ContextAndTwoStringA_Call* call = &operation->call.contextAndTwoStringA; + ret.ReturnCode = SCardAddReaderToGroupA(operation->hContext, call->sz1, call->sz2); + log_status_error(TAG, "SCardAddReaderToGroupA", ret.ReturnCode); + free(call->sz1); + call->sz1 = NULL; + free(call->sz2); + call->sz2 = NULL; + + smartcard_trace_long_return(smartcard, &ret, "SCardAddReaderToGroupA"); + return ret.ReturnCode; +} + +static LONG smartcard_AddReaderToGroupW_Call(SMARTCARD_DEVICE* smartcard, + SMARTCARD_OPERATION* operation) +{ + Long_Return ret = { 0 }; + ContextAndTwoStringW_Call* call = &operation->call.contextAndTwoStringW; + ret.ReturnCode = SCardAddReaderToGroupW(operation->hContext, call->sz1, call->sz2); + log_status_error(TAG, "SCardAddReaderToGroupW", ret.ReturnCode); + free(call->sz1); + call->sz1 = NULL; + free(call->sz2); + call->sz2 = NULL; + + smartcard_trace_long_return(smartcard, &ret, "SCardAddReaderToGroupA"); + return ret.ReturnCode; +} + +static LONG smartcard_RemoveReaderFromGroupA_Call(SMARTCARD_DEVICE* smartcard, + SMARTCARD_OPERATION* operation) +{ + Long_Return ret = { 0 }; + ContextAndTwoStringA_Call* call = &operation->call.contextAndTwoStringA; + ret.ReturnCode = SCardRemoveReaderFromGroupA(operation->hContext, call->sz1, call->sz2); + log_status_error(TAG, "SCardRemoveReaderFromGroupA", ret.ReturnCode); + free(call->sz1); + call->sz1 = NULL; + free(call->sz2); + call->sz2 = NULL; + + smartcard_trace_long_return(smartcard, &ret, "SCardRemoveReaderFromGroupA"); + return ret.ReturnCode; +} + +static LONG smartcard_RemoveReaderFromGroupW_Call(SMARTCARD_DEVICE* smartcard, + SMARTCARD_OPERATION* operation) +{ + Long_Return ret = { 0 }; + ContextAndTwoStringW_Call* call = &operation->call.contextAndTwoStringW; + ret.ReturnCode = SCardRemoveReaderFromGroupW(operation->hContext, call->sz1, call->sz2); + log_status_error(TAG, "SCardRemoveReaderFromGroupW", ret.ReturnCode); + free(call->sz1); + call->sz1 = NULL; + free(call->sz2); + call->sz2 = NULL; + + smartcard_trace_long_return(smartcard, &ret, "SCardRemoveReaderFromGroupW"); + return ret.ReturnCode; +} + +static LONG smartcard_LocateCardsA_Call(SMARTCARD_DEVICE* smartcard, SMARTCARD_OPERATION* operation) +{ + UINT32 x; + LONG status; + LocateCards_Return ret = { 0 }; + LocateCardsA_Call* call = &operation->call.locateCardsA; + IRP* irp = operation->irp; + + ret.ReturnCode = SCardLocateCardsA(operation->hContext, call->mszCards, call->rgReaderStates, + call->cReaders); + log_status_error(TAG, "SCardLocateCardsA", ret.ReturnCode); + ret.cReaders = call->cReaders; + ret.rgReaderStates = NULL; + + free(call->mszCards); + + if (ret.cReaders > 0) + { + ret.rgReaderStates = (ReaderState_Return*)calloc(ret.cReaders, sizeof(ReaderState_Return)); + + if (!ret.rgReaderStates) + return STATUS_NO_MEMORY; + } + + for (x = 0; x < ret.cReaders; x++) + { + ret.rgReaderStates[x].dwCurrentState = call->rgReaderStates[x].dwCurrentState; + ret.rgReaderStates[x].dwEventState = call->rgReaderStates[x].dwEventState; + ret.rgReaderStates[x].cbAtr = call->rgReaderStates[x].cbAtr; + CopyMemory(&(ret.rgReaderStates[x].rgbAtr), &(call->rgReaderStates[x].rgbAtr), + sizeof(ret.rgReaderStates[x].rgbAtr)); + } + + status = smartcard_pack_locate_cards_return(smartcard, irp->output, &ret); + + for (x = 0; x < call->cReaders; x++) + { + SCARD_READERSTATEA* state = &call->rgReaderStates[x]; + free(state->szReader); + } + + free(call->rgReaderStates); + + if (status != SCARD_S_SUCCESS) + return status; + + return ret.ReturnCode; +} + +static LONG smartcard_LocateCardsW_Call(SMARTCARD_DEVICE* smartcard, SMARTCARD_OPERATION* operation) +{ + UINT32 x; + LONG status; + LocateCards_Return ret = { 0 }; + LocateCardsW_Call* call = &operation->call.locateCardsW; + IRP* irp = operation->irp; + + ret.ReturnCode = SCardLocateCardsW(operation->hContext, call->mszCards, call->rgReaderStates, + call->cReaders); + log_status_error(TAG, "SCardLocateCardsW", ret.ReturnCode); + ret.cReaders = call->cReaders; + ret.rgReaderStates = NULL; + + free(call->mszCards); + + if (ret.cReaders > 0) + { + ret.rgReaderStates = (ReaderState_Return*)calloc(ret.cReaders, sizeof(ReaderState_Return)); + + if (!ret.rgReaderStates) + return STATUS_NO_MEMORY; + } + + for (x = 0; x < ret.cReaders; x++) + { + ret.rgReaderStates[x].dwCurrentState = call->rgReaderStates[x].dwCurrentState; + ret.rgReaderStates[x].dwEventState = call->rgReaderStates[x].dwEventState; + ret.rgReaderStates[x].cbAtr = call->rgReaderStates[x].cbAtr; + CopyMemory(&(ret.rgReaderStates[x].rgbAtr), &(call->rgReaderStates[x].rgbAtr), + sizeof(ret.rgReaderStates[x].rgbAtr)); + } + + status = smartcard_pack_locate_cards_return(smartcard, irp->output, &ret); + + for (x = 0; x < call->cReaders; x++) + { + SCARD_READERSTATEW* state = &call->rgReaderStates[x]; + free(state->szReader); + } + + free(call->rgReaderStates); + + if (status != SCARD_S_SUCCESS) + return status; + + return ret.ReturnCode; +} + +static LONG smartcard_ReadCacheA_Call(SMARTCARD_DEVICE* smartcard, SMARTCARD_OPERATION* operation) +{ + LONG status; + ReadCache_Return ret = { 0 }; + ReadCacheA_Call* call = &operation->call.readCacheA; + IRP* irp = operation->irp; + BOOL autoalloc = (call->Common.cbDataLen == SCARD_AUTOALLOCATE); + + if (!call->Common.fPbDataIsNULL) + { + ret.cbDataLen = call->Common.cbDataLen; + if (!autoalloc) + { + ret.pbData = malloc(ret.cbDataLen); + if (!ret.pbData) + return SCARD_F_INTERNAL_ERROR; + } + } + + if (autoalloc) + ret.ReturnCode = SCardReadCacheA(operation->hContext, call->Common.CardIdentifier, + call->Common.FreshnessCounter, call->szLookupName, + (BYTE*)&ret.pbData, &ret.cbDataLen); + else + ret.ReturnCode = SCardReadCacheA(operation->hContext, call->Common.CardIdentifier, + call->Common.FreshnessCounter, call->szLookupName, + ret.pbData, &ret.cbDataLen); + if ((ret.ReturnCode != SCARD_W_CACHE_ITEM_NOT_FOUND) && + (ret.ReturnCode != SCARD_W_CACHE_ITEM_STALE)) + { + log_status_error(TAG, "SCardReadCacheA", ret.ReturnCode); + } + free(call->szLookupName); + free(call->Common.CardIdentifier); + + status = smartcard_pack_read_cache_return(smartcard, irp->output, &ret); + if (autoalloc) + SCardFreeMemory(operation->hContext, ret.pbData); + else + free(ret.pbData); + if (status != SCARD_S_SUCCESS) + return status; + + return ret.ReturnCode; +} + +static LONG smartcard_ReadCacheW_Call(SMARTCARD_DEVICE* smartcard, SMARTCARD_OPERATION* operation) +{ + LONG status; + ReadCache_Return ret = { 0 }; + ReadCacheW_Call* call = &operation->call.readCacheW; + IRP* irp = operation->irp; + BOOL autoalloc = (call->Common.cbDataLen == SCARD_AUTOALLOCATE); + if (!call->Common.fPbDataIsNULL) + { + ret.cbDataLen = call->Common.cbDataLen; + if (!autoalloc) + { + ret.pbData = malloc(ret.cbDataLen); + if (!ret.pbData) + return SCARD_F_INTERNAL_ERROR; + } + } + + if (autoalloc) + ret.ReturnCode = SCardReadCacheW(operation->hContext, call->Common.CardIdentifier, + call->Common.FreshnessCounter, call->szLookupName, + (BYTE*)&ret.pbData, &ret.cbDataLen); + else + ret.ReturnCode = SCardReadCacheW(operation->hContext, call->Common.CardIdentifier, + call->Common.FreshnessCounter, call->szLookupName, + ret.pbData, &ret.cbDataLen); + if ((ret.ReturnCode != SCARD_W_CACHE_ITEM_NOT_FOUND) && + (ret.ReturnCode != SCARD_W_CACHE_ITEM_STALE)) + { + log_status_error(TAG, "SCardReadCacheA", ret.ReturnCode); + } + free(call->szLookupName); + free(call->Common.CardIdentifier); + + status = smartcard_pack_read_cache_return(smartcard, irp->output, &ret); + if (autoalloc) + SCardFreeMemory(operation->hContext, ret.pbData); + else + free(ret.pbData); + if (status != SCARD_S_SUCCESS) + return status; + + return ret.ReturnCode; +} + +static LONG smartcard_WriteCacheA_Call(SMARTCARD_DEVICE* smartcard, SMARTCARD_OPERATION* operation) +{ + Long_Return ret = { 0 }; + WriteCacheA_Call* call = &operation->call.writeCacheA; + + ret.ReturnCode = SCardWriteCacheA(operation->hContext, call->Common.CardIdentifier, + call->Common.FreshnessCounter, call->szLookupName, + call->Common.pbData, call->Common.cbDataLen); + log_status_error(TAG, "SCardWriteCacheA", ret.ReturnCode); + free(call->szLookupName); + free(call->Common.CardIdentifier); + free(call->Common.pbData); + + smartcard_trace_long_return(smartcard, &ret, "SCardWriteCacheA"); + return ret.ReturnCode; +} + +static LONG smartcard_WriteCacheW_Call(SMARTCARD_DEVICE* smartcard, SMARTCARD_OPERATION* operation) +{ + Long_Return ret = { 0 }; + WriteCacheW_Call* call = &operation->call.writeCacheW; + + ret.ReturnCode = SCardWriteCacheW(operation->hContext, call->Common.CardIdentifier, + call->Common.FreshnessCounter, call->szLookupName, + call->Common.pbData, call->Common.cbDataLen); + log_status_error(TAG, "SCardWriteCacheW", ret.ReturnCode); + free(call->szLookupName); + free(call->Common.CardIdentifier); + free(call->Common.pbData); + + smartcard_trace_long_return(smartcard, &ret, "SCardWriteCacheW"); + return ret.ReturnCode; +} + +static LONG smartcard_GetTransmitCount_Call(SMARTCARD_DEVICE* smartcard, + SMARTCARD_OPERATION* operation) +{ + LONG status; + GetTransmitCount_Return ret = { 0 }; + IRP* irp = operation->irp; + + ret.ReturnCode = SCardGetTransmitCount(operation->hCard, &ret.cTransmitCount); + log_status_error(TAG, "SCardGetTransmitCount", ret.ReturnCode); + status = smartcard_pack_get_transmit_count_return(smartcard, irp->output, &ret); + if (status != SCARD_S_SUCCESS) + return status; + + return ret.ReturnCode; +} + +static LONG smartcard_ReleaseStartedEvent_Call(SMARTCARD_DEVICE* smartcard, + SMARTCARD_OPERATION* operation) +{ + WINPR_UNUSED(smartcard); + WINPR_UNUSED(operation); + + WLog_WARN(TAG, "According to [MS-RDPESC] 3.1.4 Message Processing Events and Sequencing Rules " + "this is not supported?!?"); + return SCARD_E_UNSUPPORTED_FEATURE; +} + +static LONG smartcard_GetReaderIcon_Call(SMARTCARD_DEVICE* smartcard, + SMARTCARD_OPERATION* operation) +{ + LONG status; + GetReaderIcon_Return ret = { 0 }; + GetReaderIcon_Call* call = &operation->call.getReaderIcon; + IRP* irp = operation->irp; + + ret.cbDataLen = SCARD_AUTOALLOCATE; + ret.ReturnCode = SCardGetReaderIconW(operation->hContext, call->szReaderName, + (LPBYTE)&ret.pbData, &ret.cbDataLen); + log_status_error(TAG, "SCardGetReaderIconW", ret.ReturnCode); + free(call->szReaderName); + status = smartcard_pack_get_reader_icon_return(smartcard, irp->output, &ret); + SCardFreeMemory(operation->hContext, ret.pbData); + if (status != SCARD_S_SUCCESS) + return status; + + return ret.ReturnCode; +} + +static LONG smartcard_GetDeviceTypeId_Call(SMARTCARD_DEVICE* smartcard, + SMARTCARD_OPERATION* operation) +{ + LONG status; + GetDeviceTypeId_Return ret = { 0 }; + GetDeviceTypeId_Call* call = &operation->call.getDeviceTypeId; + IRP* irp = operation->irp; + + ret.ReturnCode = + SCardGetDeviceTypeIdW(operation->hContext, call->szReaderName, &ret.dwDeviceId); + log_status_error(TAG, "SCardGetDeviceTypeIdW", ret.ReturnCode); + free(call->szReaderName); + + status = smartcard_pack_device_type_id_return(smartcard, irp->output, &ret); + if (status != SCARD_S_SUCCESS) + return status; + + return ret.ReturnCode; +} + +static LONG smartcard_GetStatusChangeA_Decode(SMARTCARD_DEVICE* smartcard, + SMARTCARD_OPERATION* operation) +{ + LONG status; + IRP* irp; + + if (!operation || !operation->irp) + return STATUS_NO_MEMORY; + irp = operation->irp; + status = smartcard_unpack_get_status_change_a_call(smartcard, irp->input, + &operation->call.getStatusChangeA); + + return status; +} + +static LONG smartcard_GetStatusChangeA_Call(SMARTCARD_DEVICE* smartcard, + SMARTCARD_OPERATION* operation) +{ + UINT32 index; + GetStatusChange_Return ret = { 0 }; + LPSCARD_READERSTATEA rgReaderState = NULL; + IRP* irp = operation->irp; + GetStatusChangeA_Call* call = &operation->call.getStatusChangeA; + ret.ReturnCode = SCardGetStatusChangeA(operation->hContext, call->dwTimeOut, + call->rgReaderStates, call->cReaders); + log_status_error(TAG, "SCardGetStatusChangeA", ret.ReturnCode); + ret.cReaders = call->cReaders; + ret.rgReaderStates = NULL; + + if (ret.cReaders > 0) + { + ret.rgReaderStates = (ReaderState_Return*)calloc(ret.cReaders, sizeof(ReaderState_Return)); + + if (!ret.rgReaderStates) + return STATUS_NO_MEMORY; + } + + for (index = 0; index < ret.cReaders; index++) + { + ret.rgReaderStates[index].dwCurrentState = call->rgReaderStates[index].dwCurrentState; + ret.rgReaderStates[index].dwEventState = call->rgReaderStates[index].dwEventState; + ret.rgReaderStates[index].cbAtr = call->rgReaderStates[index].cbAtr; + CopyMemory(&(ret.rgReaderStates[index].rgbAtr), &(call->rgReaderStates[index].rgbAtr), + sizeof(ret.rgReaderStates[index].rgbAtr)); + } + + smartcard_pack_get_status_change_return(smartcard, irp->output, &ret, FALSE); + + if (call->rgReaderStates) + { + for (index = 0; index < call->cReaders; index++) + { + rgReaderState = &call->rgReaderStates[index]; + free((void*)rgReaderState->szReader); + } + + free(call->rgReaderStates); + } + + free(ret.rgReaderStates); + return ret.ReturnCode; +} + +static LONG smartcard_GetStatusChangeW_Decode(SMARTCARD_DEVICE* smartcard, + SMARTCARD_OPERATION* operation) +{ + LONG status; + IRP* irp; + + if (!operation || !operation->irp) + return STATUS_NO_MEMORY; + irp = operation->irp; + status = smartcard_unpack_get_status_change_w_call(smartcard, irp->input, + &operation->call.getStatusChangeW); + + return status; +} + +static LONG smartcard_GetStatusChangeW_Call(SMARTCARD_DEVICE* smartcard, + SMARTCARD_OPERATION* operation) +{ + LONG status; + UINT32 index; + GetStatusChange_Return ret = { 0 }; + LPSCARD_READERSTATEW rgReaderState = NULL; + IRP* irp = operation->irp; + GetStatusChangeW_Call* call = &operation->call.getStatusChangeW; + ret.ReturnCode = SCardGetStatusChangeW(operation->hContext, call->dwTimeOut, + call->rgReaderStates, call->cReaders); + log_status_error(TAG, "SCardGetStatusChangeW", ret.ReturnCode); + ret.cReaders = call->cReaders; + ret.rgReaderStates = NULL; + + if (ret.cReaders > 0) + { + ret.rgReaderStates = (ReaderState_Return*)calloc(ret.cReaders, sizeof(ReaderState_Return)); + + if (!ret.rgReaderStates) + return STATUS_NO_MEMORY; + } + + for (index = 0; index < ret.cReaders; index++) + { + ret.rgReaderStates[index].dwCurrentState = call->rgReaderStates[index].dwCurrentState; + ret.rgReaderStates[index].dwEventState = call->rgReaderStates[index].dwEventState; + ret.rgReaderStates[index].cbAtr = call->rgReaderStates[index].cbAtr; + CopyMemory(&(ret.rgReaderStates[index].rgbAtr), &(call->rgReaderStates[index].rgbAtr), + sizeof(ret.rgReaderStates[index].rgbAtr)); + } + + status = smartcard_pack_get_status_change_return(smartcard, irp->output, &ret, TRUE); + + if (call->rgReaderStates) + { + for (index = 0; index < call->cReaders; index++) + { + rgReaderState = &call->rgReaderStates[index]; + free((void*)rgReaderState->szReader); + } + + free(call->rgReaderStates); + } + + free(ret.rgReaderStates); + if (status != SCARD_S_SUCCESS) + return status; + return ret.ReturnCode; +} + +static LONG smartcard_Cancel_Decode(SMARTCARD_DEVICE* smartcard, SMARTCARD_OPERATION* operation) +{ + LONG status; + IRP* irp; + + if (!operation || !operation->irp) + return STATUS_NO_MEMORY; + irp = operation->irp; + status = + smartcard_unpack_context_call(smartcard, irp->input, &operation->call.context, "Cancel"); + + return status; +} + +static LONG smartcard_Cancel_Call(SMARTCARD_DEVICE* smartcard, SMARTCARD_OPERATION* operation) +{ + Long_Return ret = { 0 }; + + ret.ReturnCode = SCardCancel(operation->hContext); + log_status_error(TAG, "SCardCancel", ret.ReturnCode); + smartcard_trace_long_return(smartcard, &ret, "Cancel"); + return ret.ReturnCode; +} + +static LONG smartcard_ConnectA_Decode(SMARTCARD_DEVICE* smartcard, SMARTCARD_OPERATION* operation) +{ + LONG status; + IRP* irp; + + if (!operation || !operation->irp) + return STATUS_NO_MEMORY; + irp = operation->irp; + status = smartcard_unpack_connect_a_call(smartcard, irp->input, &operation->call.connectA); + + return status; +} + +static LONG smartcard_ConnectA_Call(SMARTCARD_DEVICE* smartcard, SMARTCARD_OPERATION* operation) +{ + LONG status; + SCARDHANDLE hCard = 0; + Connect_Return ret = { 0 }; + IRP* irp = operation->irp; + ConnectA_Call* call = &operation->call.connectA; + + if ((call->Common.dwPreferredProtocols == SCARD_PROTOCOL_UNDEFINED) && + (call->Common.dwShareMode != SCARD_SHARE_DIRECT)) + { + call->Common.dwPreferredProtocols = SCARD_PROTOCOL_Tx; + } + + ret.ReturnCode = + SCardConnectA(operation->hContext, (char*)call->szReader, call->Common.dwShareMode, + call->Common.dwPreferredProtocols, &hCard, &ret.dwActiveProtocol); + smartcard_scard_context_native_to_redir(smartcard, &(ret.hContext), operation->hContext); + smartcard_scard_handle_native_to_redir(smartcard, &(ret.hCard), hCard); + + status = smartcard_pack_connect_return(smartcard, irp->output, &ret); + if (status != SCARD_S_SUCCESS) + goto out_fail; + + status = ret.ReturnCode; +out_fail: + free(call->szReader); + return status; +} + +static LONG smartcard_ConnectW_Decode(SMARTCARD_DEVICE* smartcard, SMARTCARD_OPERATION* operation) +{ + LONG status; + IRP* irp; + + if (!operation || !operation->irp) + return STATUS_NO_MEMORY; + irp = operation->irp; + status = smartcard_unpack_connect_w_call(smartcard, irp->input, &operation->call.connectW); + + return status; +} + +static LONG smartcard_ConnectW_Call(SMARTCARD_DEVICE* smartcard, SMARTCARD_OPERATION* operation) +{ + LONG status; + SCARDHANDLE hCard = 0; + Connect_Return ret = { 0 }; + IRP* irp = operation->irp; + ConnectW_Call* call = &operation->call.connectW; + + if ((call->Common.dwPreferredProtocols == SCARD_PROTOCOL_UNDEFINED) && + (call->Common.dwShareMode != SCARD_SHARE_DIRECT)) + { + call->Common.dwPreferredProtocols = SCARD_PROTOCOL_Tx; + } + + ret.ReturnCode = + SCardConnectW(operation->hContext, (WCHAR*)call->szReader, call->Common.dwShareMode, + call->Common.dwPreferredProtocols, &hCard, &ret.dwActiveProtocol); + smartcard_scard_context_native_to_redir(smartcard, &(ret.hContext), operation->hContext); + smartcard_scard_handle_native_to_redir(smartcard, &(ret.hCard), hCard); + + status = smartcard_pack_connect_return(smartcard, irp->output, &ret); + if (status != SCARD_S_SUCCESS) + goto out_fail; + + status = ret.ReturnCode; +out_fail: + free(call->szReader); + return status; +} + +static LONG smartcard_Reconnect_Decode(SMARTCARD_DEVICE* smartcard, SMARTCARD_OPERATION* operation) +{ + LONG status; + IRP* irp; + + if (!operation || !operation->irp) + return STATUS_NO_MEMORY; + irp = operation->irp; + status = smartcard_unpack_reconnect_call(smartcard, irp->input, &operation->call.reconnect); + + return status; +} + +static LONG smartcard_Reconnect_Call(SMARTCARD_DEVICE* smartcard, SMARTCARD_OPERATION* operation) +{ + LONG status; + Reconnect_Return ret = { 0 }; + IRP* irp = operation->irp; + Reconnect_Call* call = &operation->call.reconnect; + ret.ReturnCode = SCardReconnect(operation->hCard, call->dwShareMode, call->dwPreferredProtocols, + call->dwInitialization, &ret.dwActiveProtocol); + log_status_error(TAG, "SCardReconnect", ret.ReturnCode); + status = smartcard_pack_reconnect_return(smartcard, irp->output, &ret); + if (status != SCARD_S_SUCCESS) + return status; + + return ret.ReturnCode; +} + +static LONG smartcard_Disconnect_Decode(SMARTCARD_DEVICE* smartcard, SMARTCARD_OPERATION* operation) +{ + LONG status; + IRP* irp; + + if (!operation || !operation->irp) + return STATUS_NO_MEMORY; + irp = operation->irp; + status = smartcard_unpack_hcard_and_disposition_call( + smartcard, irp->input, &operation->call.hCardAndDisposition, "Disconnect"); + + return status; +} + +static LONG smartcard_Disconnect_Call(SMARTCARD_DEVICE* smartcard, SMARTCARD_OPERATION* operation) +{ + Long_Return ret = { 0 }; + HCardAndDisposition_Call* call = &operation->call.hCardAndDisposition; + + ret.ReturnCode = SCardDisconnect(operation->hCard, call->dwDisposition); + log_status_error(TAG, "SCardDisconnect", ret.ReturnCode); + smartcard_trace_long_return(smartcard, &ret, "Disconnect"); + + return ret.ReturnCode; +} + +static LONG smartcard_BeginTransaction_Decode(SMARTCARD_DEVICE* smartcard, + SMARTCARD_OPERATION* operation) +{ + LONG status; + IRP* irp; + + if (!operation || !operation->irp) + return STATUS_NO_MEMORY; + irp = operation->irp; + status = smartcard_unpack_hcard_and_disposition_call( + smartcard, irp->input, &operation->call.hCardAndDisposition, "BeginTransaction"); + + return status; +} + +static LONG smartcard_BeginTransaction_Call(SMARTCARD_DEVICE* smartcard, + SMARTCARD_OPERATION* operation) +{ + Long_Return ret = { 0 }; + + ret.ReturnCode = SCardBeginTransaction(operation->hCard); + log_status_error(TAG, "SCardBeginTransaction", ret.ReturnCode); + smartcard_trace_long_return(smartcard, &ret, "BeginTransaction"); + return ret.ReturnCode; +} + +static LONG smartcard_EndTransaction_Decode(SMARTCARD_DEVICE* smartcard, + SMARTCARD_OPERATION* operation) +{ + LONG status; + IRP* irp; + + if (!operation || !operation->irp) + return STATUS_NO_MEMORY; + irp = operation->irp; + status = smartcard_unpack_hcard_and_disposition_call( + smartcard, irp->input, &operation->call.hCardAndDisposition, "EndTransaction"); + + return status; +} + +static LONG smartcard_EndTransaction_Call(SMARTCARD_DEVICE* smartcard, + SMARTCARD_OPERATION* operation) +{ + Long_Return ret = { 0 }; + HCardAndDisposition_Call* call = &operation->call.hCardAndDisposition; + + ret.ReturnCode = SCardEndTransaction(operation->hCard, call->dwDisposition); + log_status_error(TAG, "SCardEndTransaction", ret.ReturnCode); + smartcard_trace_long_return(smartcard, &ret, "EndTransaction"); + return ret.ReturnCode; +} + +static LONG smartcard_State_Decode(SMARTCARD_DEVICE* smartcard, SMARTCARD_OPERATION* operation) +{ + LONG status; + IRP* irp; + + if (!operation || !operation->irp) + return STATUS_NO_MEMORY; + irp = operation->irp; + status = smartcard_unpack_state_call(smartcard, irp->input, &operation->call.state); + + return status; +} + +static LONG smartcard_State_Call(SMARTCARD_DEVICE* smartcard, SMARTCARD_OPERATION* operation) +{ + LONG status; + State_Return ret = { 0 }; + IRP* irp = operation->irp; + ret.cbAtrLen = SCARD_ATR_LENGTH; + ret.ReturnCode = SCardState(operation->hCard, &ret.dwState, &ret.dwProtocol, (BYTE*)&ret.rgAtr, + &ret.cbAtrLen); + + log_status_error(TAG, "SCardState", ret.ReturnCode); + status = smartcard_pack_state_return(smartcard, irp->output, &ret); + if (status != SCARD_S_SUCCESS) + return status; + + return ret.ReturnCode; +} + +static LONG smartcard_StatusA_Decode(SMARTCARD_DEVICE* smartcard, SMARTCARD_OPERATION* operation) +{ + LONG status; + IRP* irp; + + if (!operation || !operation->irp) + return STATUS_NO_MEMORY; + irp = operation->irp; + status = smartcard_unpack_status_call(smartcard, irp->input, &operation->call.status, FALSE); + + return status; +} + +static LONG smartcard_StatusA_Call(SMARTCARD_DEVICE* smartcard, SMARTCARD_OPERATION* operation) +{ + LONG status; + Status_Return ret = { 0 }; + DWORD cchReaderLen = 0; + DWORD cbAtrLen = 0; + LPSTR mszReaderNames = NULL; + IRP* irp = operation->irp; + Status_Call* call = &operation->call.status; + + call->cbAtrLen = 32; + cbAtrLen = call->cbAtrLen; + + if (call->fmszReaderNamesIsNULL) + cchReaderLen = 0; + else + cchReaderLen = SCARD_AUTOALLOCATE; + + status = ret.ReturnCode = + SCardStatusA(operation->hCard, call->fmszReaderNamesIsNULL ? NULL : (LPSTR)&mszReaderNames, + &cchReaderLen, &ret.dwState, &ret.dwProtocol, + cbAtrLen ? (BYTE*)&ret.pbAtr : NULL, &cbAtrLen); + + log_status_error(TAG, "SCardStatusA", status); + if (status == SCARD_S_SUCCESS) + { + if (!call->fmszReaderNamesIsNULL) + ret.mszReaderNames = (BYTE*)mszReaderNames; + + ret.cBytes = cchReaderLen; + + if (call->cbAtrLen) + ret.cbAtrLen = cbAtrLen; + } + + status = smartcard_pack_status_return(smartcard, irp->output, &ret, FALSE); + + if (mszReaderNames) + SCardFreeMemory(operation->hContext, mszReaderNames); + + if (status != SCARD_S_SUCCESS) + return status; + return ret.ReturnCode; +} + +static LONG smartcard_StatusW_Decode(SMARTCARD_DEVICE* smartcard, SMARTCARD_OPERATION* operation) +{ + LONG status; + IRP* irp; + + if (!operation || !operation->irp) + return STATUS_NO_MEMORY; + irp = operation->irp; + status = smartcard_unpack_status_call(smartcard, irp->input, &operation->call.status, TRUE); + + return status; +} + +static LONG smartcard_StatusW_Call(SMARTCARD_DEVICE* smartcard, SMARTCARD_OPERATION* operation) +{ + LONG status; + Status_Return ret = { 0 }; + LPWSTR mszReaderNames = NULL; + IRP* irp = operation->irp; + Status_Call* call = &operation->call.status; + DWORD cbAtrLen; + + /** + * [MS-RDPESC] + * According to 2.2.2.18 Status_Call cbAtrLen is unused an must be ignored upon receipt. + */ + cbAtrLen = call->cbAtrLen = 32; + + if (call->fmszReaderNamesIsNULL) + ret.cBytes = 0; + else + ret.cBytes = SCARD_AUTOALLOCATE; + + status = ret.ReturnCode = + SCardStatusW(operation->hCard, call->fmszReaderNamesIsNULL ? NULL : (LPWSTR)&mszReaderNames, + &ret.cBytes, &ret.dwState, &ret.dwProtocol, (BYTE*)&ret.pbAtr, &cbAtrLen); + log_status_error(TAG, "SCardStatusW", status); + if (status == SCARD_S_SUCCESS) + { + if (!call->fmszReaderNamesIsNULL) + ret.mszReaderNames = (BYTE*)mszReaderNames; + + ret.cbAtrLen = cbAtrLen; + } + + /* SCardStatusW returns number of characters, we need number of bytes */ + ret.cBytes *= sizeof(WCHAR); + + status = smartcard_pack_status_return(smartcard, irp->output, &ret, TRUE); + if (status != SCARD_S_SUCCESS) + return status; + + if (mszReaderNames) + SCardFreeMemory(operation->hContext, mszReaderNames); + + return ret.ReturnCode; +} + +static LONG smartcard_Transmit_Decode(SMARTCARD_DEVICE* smartcard, SMARTCARD_OPERATION* operation) +{ + LONG status; + IRP* irp; + + if (!operation || !operation->irp) + return STATUS_NO_MEMORY; + irp = operation->irp; + status = smartcard_unpack_transmit_call(smartcard, irp->input, &operation->call.transmit); + + return status; +} + +static LONG smartcard_Transmit_Call(SMARTCARD_DEVICE* smartcard, SMARTCARD_OPERATION* operation) +{ + LONG status; + Transmit_Return ret = { 0 }; + IRP* irp = operation->irp; + Transmit_Call* call = &operation->call.transmit; + ret.cbRecvLength = 0; + ret.pbRecvBuffer = NULL; + + if (call->cbRecvLength && !call->fpbRecvBufferIsNULL) + { + if (call->cbRecvLength >= 66560) + call->cbRecvLength = 66560; + + ret.cbRecvLength = call->cbRecvLength; + ret.pbRecvBuffer = (BYTE*)malloc(ret.cbRecvLength); + + if (!ret.pbRecvBuffer) + return STATUS_NO_MEMORY; + } + + ret.pioRecvPci = call->pioRecvPci; + ret.ReturnCode = + SCardTransmit(operation->hCard, call->pioSendPci, call->pbSendBuffer, call->cbSendLength, + ret.pioRecvPci, ret.pbRecvBuffer, &(ret.cbRecvLength)); + + log_status_error(TAG, "SCardTransmit", ret.ReturnCode); + + status = smartcard_pack_transmit_return(smartcard, irp->output, &ret); + free(call->pbSendBuffer); + free(ret.pbRecvBuffer); + free(call->pioSendPci); + free(call->pioRecvPci); + + if (status != SCARD_S_SUCCESS) + return status; + return ret.ReturnCode; +} + +static LONG smartcard_Control_Decode(SMARTCARD_DEVICE* smartcard, SMARTCARD_OPERATION* operation) +{ + LONG status; + IRP* irp; + + if (!operation || !operation->irp) + return STATUS_NO_MEMORY; + irp = operation->irp; + status = smartcard_unpack_control_call(smartcard, irp->input, &operation->call.control); + + return status; +} + +static LONG smartcard_Control_Call(SMARTCARD_DEVICE* smartcard, SMARTCARD_OPERATION* operation) +{ + LONG status; + Control_Return ret = { 0 }; + IRP* irp = operation->irp; + Control_Call* call = &operation->call.control; + ret.cbOutBufferSize = call->cbOutBufferSize; + ret.pvOutBuffer = (BYTE*)malloc(call->cbOutBufferSize); + + if (!ret.pvOutBuffer) + return SCARD_E_NO_MEMORY; + + ret.ReturnCode = + SCardControl(operation->hCard, call->dwControlCode, call->pvInBuffer, call->cbInBufferSize, + ret.pvOutBuffer, call->cbOutBufferSize, &ret.cbOutBufferSize); + log_status_error(TAG, "SCardControl", ret.ReturnCode); + status = smartcard_pack_control_return(smartcard, irp->output, &ret); + + free(call->pvInBuffer); + free(ret.pvOutBuffer); + if (status != SCARD_S_SUCCESS) + return status; + return ret.ReturnCode; +} + +static LONG smartcard_GetAttrib_Decode(SMARTCARD_DEVICE* smartcard, SMARTCARD_OPERATION* operation) +{ + LONG status; + IRP* irp; + + if (!operation || !operation->irp) + return STATUS_NO_MEMORY; + irp = operation->irp; + status = smartcard_unpack_get_attrib_call(smartcard, irp->input, &operation->call.getAttrib); + + return status; +} + +static LONG smartcard_SetAttrib_Decode(SMARTCARD_DEVICE* smartcard, SMARTCARD_OPERATION* operation) +{ + LONG status; + IRP* irp; + + if (!operation || !operation->irp) + return STATUS_NO_MEMORY; + irp = operation->irp; + status = smartcard_unpack_set_attrib_call(smartcard, irp->input, &operation->call.setAttrib); + + return status; +} + +static LONG smartcard_GetAttrib_Call(SMARTCARD_DEVICE* smartcard, SMARTCARD_OPERATION* operation) +{ + BOOL autoAllocate = FALSE; + LONG status; + DWORD cbAttrLen = 0; + LPBYTE pbAttr = NULL; + GetAttrib_Return ret = { 0 }; + IRP* irp = operation->irp; + const GetAttrib_Call* call = &operation->call.getAttrib; + + if (!call->fpbAttrIsNULL) + { + autoAllocate = (call->cbAttrLen == SCARD_AUTOALLOCATE) ? TRUE : FALSE; + cbAttrLen = call->cbAttrLen; + if (cbAttrLen && !autoAllocate) + { + ret.pbAttr = (BYTE*)malloc(cbAttrLen); + + if (!ret.pbAttr) + return SCARD_E_NO_MEMORY; + } + + pbAttr = autoAllocate ? (LPBYTE) & (ret.pbAttr) : ret.pbAttr; + } + + ret.ReturnCode = SCardGetAttrib(operation->hCard, call->dwAttrId, pbAttr, &cbAttrLen); + log_status_error(TAG, "SCardGetAttrib", ret.ReturnCode); + ret.cbAttrLen = cbAttrLen; + + status = smartcard_pack_get_attrib_return(smartcard, irp->output, &ret, call->dwAttrId, + call->cbAttrLen); + + if (autoAllocate) + SCardFreeMemory(operation->hContext, ret.pbAttr); + else + free(ret.pbAttr); + return status; +} + +static LONG smartcard_SetAttrib_Call(SMARTCARD_DEVICE* smartcard, SMARTCARD_OPERATION* operation) +{ + Long_Return ret = { 0 }; + SetAttrib_Call* call = &operation->call.setAttrib; + + ret.ReturnCode = + SCardSetAttrib(operation->hCard, call->dwAttrId, call->pbAttr, call->cbAttrLen); + log_status_error(TAG, "SCardSetAttrib", ret.ReturnCode); + free(call->pbAttr); + smartcard_trace_long_return(smartcard, &ret, "SetAttrib"); + + return ret.ReturnCode; +} + +static LONG smartcard_AccessStartedEvent_Decode(SMARTCARD_DEVICE* smartcard, + SMARTCARD_OPERATION* operation) +{ + IRP* irp; + WINPR_UNUSED(smartcard); + irp = operation->irp; + + if (!operation || !operation->irp) + return STATUS_NO_MEMORY; + + if (Stream_GetRemainingLength(irp->input) < 4) + { + WLog_WARN(TAG, "AccessStartedEvent is too short: %" PRIuz "", + Stream_GetRemainingLength(irp->input)); + return SCARD_F_INTERNAL_ERROR; + } + + Stream_Read_INT32(irp->input, operation->call.lng.LongValue); /* Unused (4 bytes) */ + + return SCARD_S_SUCCESS; +} + +static LONG smartcard_AccessStartedEvent_Call(SMARTCARD_DEVICE* smartcard, + SMARTCARD_OPERATION* operation) +{ + LONG status = SCARD_S_SUCCESS; + WINPR_UNUSED(operation); + + if (!smartcard->StartedEvent) + smartcard->StartedEvent = SCardAccessStartedEvent(); + + if (!smartcard->StartedEvent) + status = SCARD_E_NO_SERVICE; + + return status; +} + +static LONG smartcard_LocateCardsByATRA_Decode(SMARTCARD_DEVICE* smartcard, + SMARTCARD_OPERATION* operation) +{ + LONG status; + IRP* irp; + + if (!operation || !operation->irp) + return STATUS_NO_MEMORY; + irp = operation->irp; + status = smartcard_unpack_locate_cards_by_atr_a_call(smartcard, irp->input, + &operation->call.locateCardsByATRA); + + return status; +} + +static LONG smartcard_LocateCardsByATRW_Decode(SMARTCARD_DEVICE* smartcard, + SMARTCARD_OPERATION* operation) +{ + LONG status; + IRP* irp; + + if (!operation || !operation->irp) + return STATUS_NO_MEMORY; + irp = operation->irp; + status = smartcard_unpack_locate_cards_by_atr_w_call(smartcard, irp->input, + &operation->call.locateCardsByATRW); + + return status; +} + +static LONG smartcard_ReadCacheA_Decode(SMARTCARD_DEVICE* smartcard, SMARTCARD_OPERATION* operation) +{ + LONG status; + IRP* irp; + + if (!operation || !operation->irp) + return STATUS_NO_MEMORY; + irp = operation->irp; + status = smartcard_unpack_read_cache_a_call(smartcard, irp->input, &operation->call.readCacheA); + + return status; +} + +static LONG smartcard_ReadCacheW_Decode(SMARTCARD_DEVICE* smartcard, SMARTCARD_OPERATION* operation) +{ + LONG status; + IRP* irp; + + if (!operation || !operation->irp) + return STATUS_NO_MEMORY; + irp = operation->irp; + + status = smartcard_unpack_read_cache_w_call(smartcard, irp->input, &operation->call.readCacheW); + + return status; +} + +static LONG smartcard_WriteCacheA_Decode(SMARTCARD_DEVICE* smartcard, + SMARTCARD_OPERATION* operation) +{ + LONG status; + IRP* irp; + + if (!operation || !operation->irp) + return STATUS_NO_MEMORY; + irp = operation->irp; + status = + smartcard_unpack_write_cache_a_call(smartcard, irp->input, &operation->call.writeCacheA); + + return status; +} + +static LONG smartcard_WriteCacheW_Decode(SMARTCARD_DEVICE* smartcard, + SMARTCARD_OPERATION* operation) +{ + LONG status; + IRP* irp; + + if (!operation || !operation->irp) + return STATUS_NO_MEMORY; + irp = operation->irp; + status = + smartcard_unpack_write_cache_w_call(smartcard, irp->input, &operation->call.writeCacheW); + + return status; +} + +static LONG smartcard_GetTransmitCount_Decode(SMARTCARD_DEVICE* smartcard, + SMARTCARD_OPERATION* operation) +{ + LONG status; + IRP* irp; + + if (!operation || !operation->irp) + return STATUS_NO_MEMORY; + irp = operation->irp; + status = smartcard_unpack_get_transmit_count_call(smartcard, irp->input, + &operation->call.getTransmitCount); + + return status; +} + +static LONG smartcard_ReleaseStartedEvent_Decode(SMARTCARD_DEVICE* smartcard, + SMARTCARD_OPERATION* operation) +{ + WINPR_UNUSED(smartcard); + WINPR_UNUSED(operation); + WLog_WARN(TAG, "According to [MS-RDPESC] 3.1.4 Message Processing Events and Sequencing Rules " + "SCARD_IOCTL_RELEASETARTEDEVENT is not supported"); + return SCARD_E_UNSUPPORTED_FEATURE; +} + +static LONG smartcard_GetReaderIcon_Decode(SMARTCARD_DEVICE* smartcard, + SMARTCARD_OPERATION* operation) +{ + LONG status; + IRP* irp; + + if (!operation || !operation->irp) + return STATUS_NO_MEMORY; + irp = operation->irp; + + status = smartcard_unpack_get_reader_icon_call(smartcard, irp->input, + &operation->call.getReaderIcon); + + return status; +} + +static LONG smartcard_GetDeviceTypeId_Decode(SMARTCARD_DEVICE* smartcard, + SMARTCARD_OPERATION* operation) +{ + LONG status; + IRP* irp; + + if (!operation || !operation->irp) + return STATUS_NO_MEMORY; + irp = operation->irp; + status = smartcard_unpack_get_device_type_id_call(smartcard, irp->input, + &operation->call.getDeviceTypeId); + + return status; +} + +static LONG smartcard_LocateCardsByATRA_Call(SMARTCARD_DEVICE* smartcard, + SMARTCARD_OPERATION* operation) +{ + LONG status; + DWORD i, j, k; + GetStatusChange_Return ret = { 0 }; + LPSCARD_READERSTATEA state = NULL; + LPSCARD_READERSTATEA states = NULL; + IRP* irp = operation->irp; + LocateCardsByATRA_Call* call = &operation->call.locateCardsByATRA; + states = (LPSCARD_READERSTATEA)calloc(call->cReaders, sizeof(SCARD_READERSTATEA)); + + if (!states) + return STATUS_NO_MEMORY; + + for (i = 0; i < call->cReaders; i++) + { + states[i].szReader = (LPSTR)call->rgReaderStates[i].szReader; + states[i].dwCurrentState = call->rgReaderStates[i].dwCurrentState; + states[i].dwEventState = call->rgReaderStates[i].dwEventState; + states[i].cbAtr = call->rgReaderStates[i].cbAtr; + CopyMemory(&(states[i].rgbAtr), &(call->rgReaderStates[i].rgbAtr), 36); + } + + status = ret.ReturnCode = + SCardGetStatusChangeA(operation->hContext, 0x000001F4, states, call->cReaders); + + log_status_error(TAG, "SCardGetStatusChangeA", status); + for (i = 0; i < call->cAtrs; i++) + { + for (j = 0; j < call->cReaders; j++) + { + for (k = 0; k < call->rgAtrMasks[i].cbAtr; k++) + { + if ((call->rgAtrMasks[i].rgbAtr[k] & call->rgAtrMasks[i].rgbMask[k]) != + (states[j].rgbAtr[k] & call->rgAtrMasks[i].rgbMask[k])) + { + break; + } + + states[j].dwEventState |= SCARD_STATE_ATRMATCH; + } + } + } + + ret.cReaders = call->cReaders; + ret.rgReaderStates = NULL; + + if (ret.cReaders > 0) + ret.rgReaderStates = (ReaderState_Return*)calloc(ret.cReaders, sizeof(ReaderState_Return)); + + if (!ret.rgReaderStates) + { + free(states); + return STATUS_NO_MEMORY; + } + + for (i = 0; i < ret.cReaders; i++) + { + state = &states[i]; + ret.rgReaderStates[i].dwCurrentState = state->dwCurrentState; + ret.rgReaderStates[i].dwEventState = state->dwEventState; + ret.rgReaderStates[i].cbAtr = state->cbAtr; + CopyMemory(&(ret.rgReaderStates[i].rgbAtr), &(state->rgbAtr), + sizeof(ret.rgReaderStates[i].rgbAtr)); + } + + free(states); + + status = smartcard_pack_get_status_change_return(smartcard, irp->output, &ret, FALSE); + + if (call->rgReaderStates) + { + for (i = 0; i < call->cReaders; i++) + { + state = (LPSCARD_READERSTATEA)&call->rgReaderStates[i]; + + if (state->szReader) + { + free((void*)state->szReader); + state->szReader = NULL; + } + } + + free(call->rgReaderStates); + call->rgReaderStates = NULL; + } + + free(ret.rgReaderStates); + if (status != SCARD_S_SUCCESS) + return status; + return ret.ReturnCode; +} + +LONG smartcard_irp_device_control_decode(SMARTCARD_DEVICE* smartcard, + SMARTCARD_OPERATION* operation) +{ + LONG status; + UINT32 offset; + UINT32 ioControlCode; + UINT32 outputBufferLength; + UINT32 inputBufferLength; + IRP* irp; + + if (!operation || !operation->irp) + return SCARD_E_NO_MEMORY; + irp = operation->irp; + + /* Device Control Request */ + + if (Stream_GetRemainingLength(irp->input) < 32) + { + WLog_WARN(TAG, "Device Control Request is too short: %" PRIuz "", + Stream_GetRemainingLength(irp->input)); + return SCARD_F_INTERNAL_ERROR; + } + + Stream_Read_UINT32(irp->input, outputBufferLength); /* OutputBufferLength (4 bytes) */ + Stream_Read_UINT32(irp->input, inputBufferLength); /* InputBufferLength (4 bytes) */ + Stream_Read_UINT32(irp->input, ioControlCode); /* IoControlCode (4 bytes) */ + Stream_Seek(irp->input, 20); /* Padding (20 bytes) */ + operation->ioControlCode = ioControlCode; + + if (Stream_Length(irp->input) != (Stream_GetPosition(irp->input) + inputBufferLength)) + { + WLog_WARN(TAG, "InputBufferLength mismatch: Actual: %" PRIuz " Expected: %" PRIuz "", + Stream_Length(irp->input), Stream_GetPosition(irp->input) + inputBufferLength); + return SCARD_F_INTERNAL_ERROR; + } + + WLog_DBG(TAG, "%s (0x%08" PRIX32 ") FileId: %" PRIu32 " CompletionId: %" PRIu32 "", + smartcard_get_ioctl_string(ioControlCode, TRUE), ioControlCode, irp->FileId, + irp->CompletionId); + + if ((ioControlCode != SCARD_IOCTL_ACCESSSTARTEDEVENT) && + (ioControlCode != SCARD_IOCTL_RELEASETARTEDEVENT)) + { + status = smartcard_unpack_common_type_header(smartcard, irp->input); + if (status != SCARD_S_SUCCESS) + return status; + + status = smartcard_unpack_private_type_header(smartcard, irp->input); + if (status != SCARD_S_SUCCESS) + return status; + } + + /* Decode */ + switch (ioControlCode) + { + case SCARD_IOCTL_ESTABLISHCONTEXT: + status = smartcard_EstablishContext_Decode(smartcard, operation); + break; + + case SCARD_IOCTL_RELEASECONTEXT: + status = smartcard_ReleaseContext_Decode(smartcard, operation); + break; + + case SCARD_IOCTL_ISVALIDCONTEXT: + status = smartcard_IsValidContext_Decode(smartcard, operation); + break; + + case SCARD_IOCTL_LISTREADERGROUPSA: + status = smartcard_ListReaderGroupsA_Decode(smartcard, operation); + break; + + case SCARD_IOCTL_LISTREADERGROUPSW: + status = smartcard_ListReaderGroupsW_Decode(smartcard, operation); + break; + + case SCARD_IOCTL_LISTREADERSA: + status = smartcard_ListReadersA_Decode(smartcard, operation); + break; + + case SCARD_IOCTL_LISTREADERSW: + status = smartcard_ListReadersW_Decode(smartcard, operation); + break; + + case SCARD_IOCTL_INTRODUCEREADERGROUPA: + status = smartcard_context_and_string_a_Decode(smartcard, operation); + break; + + case SCARD_IOCTL_INTRODUCEREADERGROUPW: + status = smartcard_context_and_string_w_Decode(smartcard, operation); + break; + + case SCARD_IOCTL_FORGETREADERGROUPA: + status = smartcard_context_and_string_a_Decode(smartcard, operation); + break; + + case SCARD_IOCTL_FORGETREADERGROUPW: + status = smartcard_context_and_string_w_Decode(smartcard, operation); + break; + + case SCARD_IOCTL_INTRODUCEREADERA: + status = smartcard_context_and_two_strings_a_Decode(smartcard, operation); + break; + + case SCARD_IOCTL_INTRODUCEREADERW: + status = smartcard_context_and_two_strings_w_Decode(smartcard, operation); + break; + + case SCARD_IOCTL_FORGETREADERA: + status = smartcard_context_and_string_a_Decode(smartcard, operation); + break; + + case SCARD_IOCTL_FORGETREADERW: + status = smartcard_context_and_string_w_Decode(smartcard, operation); + break; + + case SCARD_IOCTL_ADDREADERTOGROUPA: + status = smartcard_context_and_two_strings_a_Decode(smartcard, operation); + break; + + case SCARD_IOCTL_ADDREADERTOGROUPW: + status = smartcard_context_and_two_strings_w_Decode(smartcard, operation); + break; + + case SCARD_IOCTL_REMOVEREADERFROMGROUPA: + status = smartcard_context_and_two_strings_a_Decode(smartcard, operation); + break; + + case SCARD_IOCTL_REMOVEREADERFROMGROUPW: + status = smartcard_context_and_two_strings_w_Decode(smartcard, operation); + break; + + case SCARD_IOCTL_LOCATECARDSA: + status = smartcard_LocateCardsA_Decode(smartcard, operation); + break; + + case SCARD_IOCTL_LOCATECARDSW: + status = smartcard_LocateCardsW_Decode(smartcard, operation); + break; + + case SCARD_IOCTL_GETSTATUSCHANGEA: + status = smartcard_GetStatusChangeA_Decode(smartcard, operation); + break; + + case SCARD_IOCTL_GETSTATUSCHANGEW: + status = smartcard_GetStatusChangeW_Decode(smartcard, operation); + break; + + case SCARD_IOCTL_CANCEL: + status = smartcard_Cancel_Decode(smartcard, operation); + break; + + case SCARD_IOCTL_CONNECTA: + status = smartcard_ConnectA_Decode(smartcard, operation); + break; + + case SCARD_IOCTL_CONNECTW: + status = smartcard_ConnectW_Decode(smartcard, operation); + break; + + case SCARD_IOCTL_RECONNECT: + status = smartcard_Reconnect_Decode(smartcard, operation); + break; + + case SCARD_IOCTL_DISCONNECT: + status = smartcard_Disconnect_Decode(smartcard, operation); + break; + + case SCARD_IOCTL_BEGINTRANSACTION: + status = smartcard_BeginTransaction_Decode(smartcard, operation); + break; + + case SCARD_IOCTL_ENDTRANSACTION: + status = smartcard_EndTransaction_Decode(smartcard, operation); + break; + + case SCARD_IOCTL_STATE: + status = smartcard_State_Decode(smartcard, operation); + break; + + case SCARD_IOCTL_STATUSA: + status = smartcard_StatusA_Decode(smartcard, operation); + break; + + case SCARD_IOCTL_STATUSW: + status = smartcard_StatusW_Decode(smartcard, operation); + break; + + case SCARD_IOCTL_TRANSMIT: + status = smartcard_Transmit_Decode(smartcard, operation); + break; + + case SCARD_IOCTL_CONTROL: + status = smartcard_Control_Decode(smartcard, operation); + break; + + case SCARD_IOCTL_GETATTRIB: + status = smartcard_GetAttrib_Decode(smartcard, operation); + break; + + case SCARD_IOCTL_SETATTRIB: + status = smartcard_SetAttrib_Decode(smartcard, operation); + break; + + case SCARD_IOCTL_ACCESSSTARTEDEVENT: + status = smartcard_AccessStartedEvent_Decode(smartcard, operation); + break; + + case SCARD_IOCTL_LOCATECARDSBYATRA: + status = smartcard_LocateCardsByATRA_Decode(smartcard, operation); + break; + + case SCARD_IOCTL_LOCATECARDSBYATRW: + status = smartcard_LocateCardsByATRW_Decode(smartcard, operation); + break; + + case SCARD_IOCTL_READCACHEA: + status = smartcard_ReadCacheA_Decode(smartcard, operation); + break; + + case SCARD_IOCTL_READCACHEW: + status = smartcard_ReadCacheW_Decode(smartcard, operation); + break; + + case SCARD_IOCTL_WRITECACHEA: + status = smartcard_WriteCacheA_Decode(smartcard, operation); + break; + + case SCARD_IOCTL_WRITECACHEW: + status = smartcard_WriteCacheW_Decode(smartcard, operation); + break; + + case SCARD_IOCTL_GETTRANSMITCOUNT: + status = smartcard_GetTransmitCount_Decode(smartcard, operation); + break; + + case SCARD_IOCTL_RELEASETARTEDEVENT: + status = smartcard_ReleaseStartedEvent_Decode(smartcard, operation); + break; + + case SCARD_IOCTL_GETREADERICON: + status = smartcard_GetReaderIcon_Decode(smartcard, operation); + break; + + case SCARD_IOCTL_GETDEVICETYPEID: + status = smartcard_GetDeviceTypeId_Decode(smartcard, operation); + break; + + default: + status = SCARD_F_INTERNAL_ERROR; + break; + } + + smartcard_call_to_operation_handle(smartcard, operation); + + if ((ioControlCode != SCARD_IOCTL_ACCESSSTARTEDEVENT) && + (ioControlCode != SCARD_IOCTL_RELEASETARTEDEVENT)) + { + offset = (RDPDR_DEVICE_IO_REQUEST_LENGTH + RDPDR_DEVICE_IO_CONTROL_REQ_HDR_LENGTH); + smartcard_unpack_read_size_align(smartcard, irp->input, + Stream_GetPosition(irp->input) - offset, 8); + } + + if (Stream_GetPosition(irp->input) < Stream_Length(irp->input)) + { + SIZE_T difference; + difference = Stream_Length(irp->input) - Stream_GetPosition(irp->input); + WLog_WARN(TAG, + "IRP was not fully parsed %s (%s [0x%08" PRIX32 "]): Actual: %" PRIuz + ", Expected: %" PRIuz ", Difference: %" PRIuz "", + smartcard_get_ioctl_string(ioControlCode, TRUE), + smartcard_get_ioctl_string(ioControlCode, FALSE), ioControlCode, + Stream_GetPosition(irp->input), Stream_Length(irp->input), difference); + winpr_HexDump(TAG, WLOG_WARN, Stream_Pointer(irp->input), difference); + } + + if (Stream_GetPosition(irp->input) > Stream_Length(irp->input)) + { + SIZE_T difference; + difference = Stream_GetPosition(irp->input) - Stream_Length(irp->input); + WLog_WARN(TAG, + "IRP was parsed beyond its end %s (0x%08" PRIX32 "): Actual: %" PRIuz + ", Expected: %" PRIuz ", Difference: %" PRIuz "", + smartcard_get_ioctl_string(ioControlCode, TRUE), ioControlCode, + Stream_GetPosition(irp->input), Stream_Length(irp->input), difference); + } + + return status; +} + +LONG smartcard_irp_device_control_call(SMARTCARD_DEVICE* smartcard, SMARTCARD_OPERATION* operation) +{ + IRP* irp; + LONG result; + UINT32 offset; + UINT32 ioControlCode; + UINT32 outputBufferLength; + UINT32 objectBufferLength; + irp = operation->irp; + ioControlCode = operation->ioControlCode; + /** + * [MS-RDPESC] 3.2.5.1: Sending Outgoing Messages: + * the output buffer length SHOULD be set to 2048 + * + * Since it's a SHOULD and not a MUST, we don't care + * about it, but we still reserve at least 2048 bytes. + */ + if (!Stream_EnsureRemainingCapacity(irp->output, 2048)) + return SCARD_E_NO_MEMORY; + + /* Device Control Response */ + Stream_Seek_UINT32(irp->output); /* OutputBufferLength (4 bytes) */ + Stream_Seek(irp->output, SMARTCARD_COMMON_TYPE_HEADER_LENGTH); /* CommonTypeHeader (8 bytes) */ + Stream_Seek(irp->output, + SMARTCARD_PRIVATE_TYPE_HEADER_LENGTH); /* PrivateTypeHeader (8 bytes) */ + Stream_Seek_UINT32(irp->output); /* Result (4 bytes) */ + + /* Call */ + + switch (ioControlCode) + { + case SCARD_IOCTL_ESTABLISHCONTEXT: + result = smartcard_EstablishContext_Call(smartcard, operation); + break; + + case SCARD_IOCTL_RELEASECONTEXT: + result = smartcard_ReleaseContext_Call(smartcard, operation); + break; + + case SCARD_IOCTL_ISVALIDCONTEXT: + result = smartcard_IsValidContext_Call(smartcard, operation); + break; + + case SCARD_IOCTL_LISTREADERGROUPSA: + result = smartcard_ListReaderGroupsA_Call(smartcard, operation); + break; + + case SCARD_IOCTL_LISTREADERGROUPSW: + result = smartcard_ListReaderGroupsW_Call(smartcard, operation); + break; + + case SCARD_IOCTL_LISTREADERSA: + result = smartcard_ListReadersA_Call(smartcard, operation); + break; + + case SCARD_IOCTL_LISTREADERSW: + result = smartcard_ListReadersW_Call(smartcard, operation); + break; + + case SCARD_IOCTL_INTRODUCEREADERGROUPA: + result = smartcard_IntroduceReaderGroupA_Call(smartcard, operation); + break; + + case SCARD_IOCTL_INTRODUCEREADERGROUPW: + result = smartcard_IntroduceReaderGroupW_Call(smartcard, operation); + break; + + case SCARD_IOCTL_FORGETREADERGROUPA: + result = smartcard_ForgetReaderA_Call(smartcard, operation); + break; + + case SCARD_IOCTL_FORGETREADERGROUPW: + result = smartcard_ForgetReaderW_Call(smartcard, operation); + break; + + case SCARD_IOCTL_INTRODUCEREADERA: + result = smartcard_IntroduceReaderA_Call(smartcard, operation); + break; + + case SCARD_IOCTL_INTRODUCEREADERW: + result = smartcard_IntroduceReaderW_Call(smartcard, operation); + break; + + case SCARD_IOCTL_FORGETREADERA: + result = smartcard_ForgetReaderA_Call(smartcard, operation); + break; + + case SCARD_IOCTL_FORGETREADERW: + result = smartcard_ForgetReaderW_Call(smartcard, operation); + break; + + case SCARD_IOCTL_ADDREADERTOGROUPA: + result = smartcard_AddReaderToGroupA_Call(smartcard, operation); + break; + + case SCARD_IOCTL_ADDREADERTOGROUPW: + result = smartcard_AddReaderToGroupW_Call(smartcard, operation); + break; + + case SCARD_IOCTL_REMOVEREADERFROMGROUPA: + result = smartcard_RemoveReaderFromGroupA_Call(smartcard, operation); + break; + + case SCARD_IOCTL_REMOVEREADERFROMGROUPW: + result = smartcard_RemoveReaderFromGroupW_Call(smartcard, operation); + break; + + case SCARD_IOCTL_LOCATECARDSA: + result = smartcard_LocateCardsA_Call(smartcard, operation); + break; + + case SCARD_IOCTL_LOCATECARDSW: + result = smartcard_LocateCardsW_Call(smartcard, operation); + break; + + case SCARD_IOCTL_GETSTATUSCHANGEA: + result = smartcard_GetStatusChangeA_Call(smartcard, operation); + break; + + case SCARD_IOCTL_GETSTATUSCHANGEW: + result = smartcard_GetStatusChangeW_Call(smartcard, operation); + break; + + case SCARD_IOCTL_CANCEL: + result = smartcard_Cancel_Call(smartcard, operation); + break; + + case SCARD_IOCTL_CONNECTA: + result = smartcard_ConnectA_Call(smartcard, operation); + break; + + case SCARD_IOCTL_CONNECTW: + result = smartcard_ConnectW_Call(smartcard, operation); + break; + + case SCARD_IOCTL_RECONNECT: + result = smartcard_Reconnect_Call(smartcard, operation); + break; + + case SCARD_IOCTL_DISCONNECT: + result = smartcard_Disconnect_Call(smartcard, operation); + break; + + case SCARD_IOCTL_BEGINTRANSACTION: + result = smartcard_BeginTransaction_Call(smartcard, operation); + break; + + case SCARD_IOCTL_ENDTRANSACTION: + result = smartcard_EndTransaction_Call(smartcard, operation); + break; + + case SCARD_IOCTL_STATE: + result = smartcard_State_Call(smartcard, operation); + break; + + case SCARD_IOCTL_STATUSA: + result = smartcard_StatusA_Call(smartcard, operation); + break; + + case SCARD_IOCTL_STATUSW: + result = smartcard_StatusW_Call(smartcard, operation); + break; + + case SCARD_IOCTL_TRANSMIT: + result = smartcard_Transmit_Call(smartcard, operation); + break; + + case SCARD_IOCTL_CONTROL: + result = smartcard_Control_Call(smartcard, operation); + break; + + case SCARD_IOCTL_GETATTRIB: + result = smartcard_GetAttrib_Call(smartcard, operation); + break; + + case SCARD_IOCTL_SETATTRIB: + result = smartcard_SetAttrib_Call(smartcard, operation); + break; + + case SCARD_IOCTL_ACCESSSTARTEDEVENT: + result = smartcard_AccessStartedEvent_Call(smartcard, operation); + break; + + case SCARD_IOCTL_LOCATECARDSBYATRA: + result = smartcard_LocateCardsByATRA_Call(smartcard, operation); + break; + + case SCARD_IOCTL_LOCATECARDSBYATRW: + result = smartcard_LocateCardsW_Call(smartcard, operation); + break; + + case SCARD_IOCTL_READCACHEA: + result = smartcard_ReadCacheA_Call(smartcard, operation); + break; + + case SCARD_IOCTL_READCACHEW: + result = smartcard_ReadCacheW_Call(smartcard, operation); + break; + + case SCARD_IOCTL_WRITECACHEA: + result = smartcard_WriteCacheA_Call(smartcard, operation); + break; + + case SCARD_IOCTL_WRITECACHEW: + result = smartcard_WriteCacheW_Call(smartcard, operation); + break; + + case SCARD_IOCTL_GETTRANSMITCOUNT: + result = smartcard_GetTransmitCount_Call(smartcard, operation); + break; + + case SCARD_IOCTL_RELEASETARTEDEVENT: + result = smartcard_ReleaseStartedEvent_Call(smartcard, operation); + break; + + case SCARD_IOCTL_GETREADERICON: + result = smartcard_GetReaderIcon_Call(smartcard, operation); + break; + + case SCARD_IOCTL_GETDEVICETYPEID: + result = smartcard_GetDeviceTypeId_Call(smartcard, operation); + break; + + default: + result = STATUS_UNSUCCESSFUL; + break; + } + + /** + * [MS-RPCE] 2.2.6.3 Primitive Type Serialization + * The type MUST be aligned on an 8-byte boundary. If the size of the + * primitive type is not a multiple of 8 bytes, the data MUST be padded. + */ + + if ((ioControlCode != SCARD_IOCTL_ACCESSSTARTEDEVENT) && + (ioControlCode != SCARD_IOCTL_RELEASETARTEDEVENT)) + { + offset = (RDPDR_DEVICE_IO_RESPONSE_LENGTH + RDPDR_DEVICE_IO_CONTROL_RSP_HDR_LENGTH); + smartcard_pack_write_size_align(smartcard, irp->output, + Stream_GetPosition(irp->output) - offset, 8); + } + + if ((result != SCARD_S_SUCCESS) && (result != SCARD_E_TIMEOUT) && + (result != SCARD_E_NO_READERS_AVAILABLE) && (result != SCARD_E_NO_SERVICE) && + (result != SCARD_W_CACHE_ITEM_NOT_FOUND) && (result != SCARD_W_CACHE_ITEM_STALE)) + { + WLog_WARN(TAG, "IRP failure: %s (0x%08" PRIX32 "), status: %s (0x%08" PRIX32 ")", + smartcard_get_ioctl_string(ioControlCode, TRUE), ioControlCode, + SCardGetErrorString(result), result); + } + + irp->IoStatus = STATUS_SUCCESS; + + if ((result & 0xC0000000L) == 0xC0000000L) + { + /* NTSTATUS error */ + irp->IoStatus = (UINT32)result; + WLog_WARN(TAG, "IRP failure: %s (0x%08" PRIX32 "), ntstatus: 0x%08" PRIX32 "", + smartcard_get_ioctl_string(ioControlCode, TRUE), ioControlCode, result); + } + + Stream_SealLength(irp->output); + outputBufferLength = Stream_Length(irp->output) - RDPDR_DEVICE_IO_RESPONSE_LENGTH - 4; + objectBufferLength = outputBufferLength - RDPDR_DEVICE_IO_RESPONSE_LENGTH; + Stream_SetPosition(irp->output, RDPDR_DEVICE_IO_RESPONSE_LENGTH); + /* Device Control Response */ + Stream_Write_UINT32(irp->output, outputBufferLength); /* OutputBufferLength (4 bytes) */ + smartcard_pack_common_type_header(smartcard, irp->output); /* CommonTypeHeader (8 bytes) */ + smartcard_pack_private_type_header(smartcard, irp->output, + objectBufferLength); /* PrivateTypeHeader (8 bytes) */ + Stream_Write_INT32(irp->output, result); /* Result (4 bytes) */ + Stream_SetPosition(irp->output, Stream_Length(irp->output)); + return SCARD_S_SUCCESS; +} diff --git a/channels/smartcard/client/smartcard_operations.h b/channels/smartcard/client/smartcard_operations.h new file mode 100644 index 0000000..39add18 --- /dev/null +++ b/channels/smartcard/client/smartcard_operations.h @@ -0,0 +1,546 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Smartcard Device Service Virtual Channel + * + * Copyright 2011 O.S. Systems Software Ltda. + * Copyright 2011 Eduardo Fiss Beloni + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger + * + * Licensed 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 FREERDP_CHANNEL_SMARTCARD_OPERATIONS_MAIN_H +#define FREERDP_CHANNEL_SMARTCARD_OPERATIONS_MAIN_H + +#include + +#define RDP_SCARD_CTL_CODE(code) \ + CTL_CODE(FILE_DEVICE_FILE_SYSTEM, (code), METHOD_BUFFERED, FILE_ANY_ACCESS) + +#define SCARD_IOCTL_ESTABLISHCONTEXT RDP_SCARD_CTL_CODE(5) /* SCardEstablishContext */ +#define SCARD_IOCTL_RELEASECONTEXT RDP_SCARD_CTL_CODE(6) /* SCardReleaseContext */ +#define SCARD_IOCTL_ISVALIDCONTEXT RDP_SCARD_CTL_CODE(7) /* SCardIsValidContext */ +#define SCARD_IOCTL_LISTREADERGROUPSA RDP_SCARD_CTL_CODE(8) /* SCardListReaderGroupsA */ +#define SCARD_IOCTL_LISTREADERGROUPSW RDP_SCARD_CTL_CODE(9) /* SCardListReaderGroupsW */ +#define SCARD_IOCTL_LISTREADERSA RDP_SCARD_CTL_CODE(10) /* SCardListReadersA */ +#define SCARD_IOCTL_LISTREADERSW RDP_SCARD_CTL_CODE(11) /* SCardListReadersW */ +#define SCARD_IOCTL_INTRODUCEREADERGROUPA RDP_SCARD_CTL_CODE(20) /* SCardIntroduceReaderGroupA */ +#define SCARD_IOCTL_INTRODUCEREADERGROUPW RDP_SCARD_CTL_CODE(21) /* SCardIntroduceReaderGroupW */ +#define SCARD_IOCTL_FORGETREADERGROUPA RDP_SCARD_CTL_CODE(22) /* SCardForgetReaderGroupA */ +#define SCARD_IOCTL_FORGETREADERGROUPW RDP_SCARD_CTL_CODE(23) /* SCardForgetReaderGroupW */ +#define SCARD_IOCTL_INTRODUCEREADERA RDP_SCARD_CTL_CODE(24) /* SCardIntroduceReaderA */ +#define SCARD_IOCTL_INTRODUCEREADERW RDP_SCARD_CTL_CODE(25) /* SCardIntroduceReaderW */ +#define SCARD_IOCTL_FORGETREADERA RDP_SCARD_CTL_CODE(26) /* SCardForgetReaderA */ +#define SCARD_IOCTL_FORGETREADERW RDP_SCARD_CTL_CODE(27) /* SCardForgetReaderW */ +#define SCARD_IOCTL_ADDREADERTOGROUPA RDP_SCARD_CTL_CODE(28) /* SCardAddReaderToGroupA */ +#define SCARD_IOCTL_ADDREADERTOGROUPW RDP_SCARD_CTL_CODE(29) /* SCardAddReaderToGroupW */ +#define SCARD_IOCTL_REMOVEREADERFROMGROUPA \ + RDP_SCARD_CTL_CODE(30) /* SCardRemoveReaderFromGroupA \ + */ +#define SCARD_IOCTL_REMOVEREADERFROMGROUPW \ + RDP_SCARD_CTL_CODE(31) /* SCardRemoveReaderFromGroupW \ + */ +#define SCARD_IOCTL_LOCATECARDSA RDP_SCARD_CTL_CODE(38) /* SCardLocateCardsA */ +#define SCARD_IOCTL_LOCATECARDSW RDP_SCARD_CTL_CODE(39) /* SCardLocateCardsW */ +#define SCARD_IOCTL_GETSTATUSCHANGEA RDP_SCARD_CTL_CODE(40) /* SCardGetStatusChangeA */ +#define SCARD_IOCTL_GETSTATUSCHANGEW RDP_SCARD_CTL_CODE(41) /* SCardGetStatusChangeW */ +#define SCARD_IOCTL_CANCEL RDP_SCARD_CTL_CODE(42) /* SCardCancel */ +#define SCARD_IOCTL_CONNECTA RDP_SCARD_CTL_CODE(43) /* SCardConnectA */ +#define SCARD_IOCTL_CONNECTW RDP_SCARD_CTL_CODE(44) /* SCardConnectW */ +#define SCARD_IOCTL_RECONNECT RDP_SCARD_CTL_CODE(45) /* SCardReconnect */ +#define SCARD_IOCTL_DISCONNECT RDP_SCARD_CTL_CODE(46) /* SCardDisconnect */ +#define SCARD_IOCTL_BEGINTRANSACTION RDP_SCARD_CTL_CODE(47) /* SCardBeginTransaction */ +#define SCARD_IOCTL_ENDTRANSACTION RDP_SCARD_CTL_CODE(48) /* SCardEndTransaction */ +#define SCARD_IOCTL_STATE RDP_SCARD_CTL_CODE(49) /* SCardState */ +#define SCARD_IOCTL_STATUSA RDP_SCARD_CTL_CODE(50) /* SCardStatusA */ +#define SCARD_IOCTL_STATUSW RDP_SCARD_CTL_CODE(51) /* SCardStatusW */ +#define SCARD_IOCTL_TRANSMIT RDP_SCARD_CTL_CODE(52) /* SCardTransmit */ +#define SCARD_IOCTL_CONTROL RDP_SCARD_CTL_CODE(53) /* SCardControl */ +#define SCARD_IOCTL_GETATTRIB RDP_SCARD_CTL_CODE(54) /* SCardGetAttrib */ +#define SCARD_IOCTL_SETATTRIB RDP_SCARD_CTL_CODE(55) /* SCardSetAttrib */ +#define SCARD_IOCTL_ACCESSSTARTEDEVENT RDP_SCARD_CTL_CODE(56) /* SCardAccessStartedEvent */ +#define SCARD_IOCTL_RELEASETARTEDEVENT RDP_SCARD_CTL_CODE(57) /* SCardReleaseStartedEvent */ +#define SCARD_IOCTL_LOCATECARDSBYATRA RDP_SCARD_CTL_CODE(58) /* SCardLocateCardsByATRA */ +#define SCARD_IOCTL_LOCATECARDSBYATRW RDP_SCARD_CTL_CODE(59) /* SCardLocateCardsByATRW */ +#define SCARD_IOCTL_READCACHEA RDP_SCARD_CTL_CODE(60) /* SCardReadCacheA */ +#define SCARD_IOCTL_READCACHEW RDP_SCARD_CTL_CODE(61) /* SCardReadCacheW */ +#define SCARD_IOCTL_WRITECACHEA RDP_SCARD_CTL_CODE(62) /* SCardWriteCacheA */ +#define SCARD_IOCTL_WRITECACHEW RDP_SCARD_CTL_CODE(63) /* SCardWriteCacheW */ +#define SCARD_IOCTL_GETTRANSMITCOUNT RDP_SCARD_CTL_CODE(64) /* SCardGetTransmitCount */ +#define SCARD_IOCTL_GETREADERICON RDP_SCARD_CTL_CODE(65) /* SCardGetReaderIconA */ +#define SCARD_IOCTL_GETDEVICETYPEID RDP_SCARD_CTL_CODE(66) /* SCardGetDeviceTypeIdA */ + +#pragma pack(push, 1) + +/* interface type_scard_pack */ +/* [unique][version][uuid] */ + +typedef struct _REDIR_SCARDCONTEXT +{ + /* [range] */ DWORD cbContext; + /* [size_is][unique] */ BYTE pbContext[8]; +} REDIR_SCARDCONTEXT; + +typedef struct _REDIR_SCARDHANDLE +{ + /* [range] */ DWORD cbHandle; + /* [size_is] */ BYTE pbHandle[8]; +} REDIR_SCARDHANDLE; + +typedef struct _Long_Return +{ + LONG ReturnCode; +} Long_Return; + +typedef struct _longAndMultiString_Return +{ + LONG ReturnCode; + /* [range] */ DWORD cBytes; + /* [size_is][unique] */ BYTE* msz; +} ListReaderGroups_Return; + +typedef struct _longAndMultiString_Return ListReaders_Return; + +typedef struct _EstablishContext_Return +{ + LONG ReturnCode; + REDIR_SCARDCONTEXT hContext; +} EstablishContext_Return; + +typedef struct _ReaderState_Return +{ + DWORD dwCurrentState; + DWORD dwEventState; + /* [range] */ DWORD cbAtr; + BYTE rgbAtr[36]; +} ReaderState_Return; + +typedef struct _LocateCards_ATRMask +{ + /* [range] */ DWORD cbAtr; + BYTE rgbAtr[36]; + BYTE rgbMask[36]; +} LocateCards_ATRMask; + +typedef struct _GetStatusChange_Return +{ + LONG ReturnCode; + /* [range] */ DWORD cReaders; + /* [size_is] */ ReaderState_Return* rgReaderStates; +} LocateCards_Return; + +typedef struct _GetStatusChange_Return GetStatusChange_Return; + +typedef struct _GetReaderIcon_Return +{ + LONG ReturnCode; + ULONG cbDataLen; + BYTE* pbData; +} GetReaderIcon_Return; + +typedef struct _GetDeviceTypeId_Return +{ + LONG ReturnCode; + ULONG dwDeviceId; +} GetDeviceTypeId_Return; + +typedef struct _Connect_Return +{ + LONG ReturnCode; + REDIR_SCARDCONTEXT hContext; + REDIR_SCARDHANDLE hCard; + DWORD dwActiveProtocol; +} Connect_Return; + +typedef struct Reconnect_Return +{ + LONG ReturnCode; + DWORD dwActiveProtocol; +} Reconnect_Return; + +typedef struct _State_Return +{ + LONG ReturnCode; + DWORD dwState; + DWORD dwProtocol; + /* [range] */ DWORD cbAtrLen; + /* [size_is][unique] */ BYTE rgAtr[36]; +} State_Return; + +typedef struct _Status_Return +{ + LONG ReturnCode; + /* [range] */ DWORD cBytes; + /* [size_is][unique] */ BYTE* mszReaderNames; + DWORD dwState; + DWORD dwProtocol; + BYTE pbAtr[32]; + /* [range] */ DWORD cbAtrLen; +} Status_Return; + +typedef struct _SCardIO_Request +{ + DWORD dwProtocol; + /* [range] */ DWORD cbExtraBytes; + /* [size_is][unique] */ BYTE* pbExtraBytes; +} SCardIO_Request; + +typedef struct _Transmit_Return +{ + LONG ReturnCode; + /* [unique] */ LPSCARD_IO_REQUEST pioRecvPci; + /* [range] */ DWORD cbRecvLength; + /* [size_is][unique] */ BYTE* pbRecvBuffer; +} Transmit_Return; + +typedef struct _GetTransmitCount_Return +{ + LONG ReturnCode; + DWORD cTransmitCount; +} GetTransmitCount_Return; + +typedef struct _Control_Return +{ + LONG ReturnCode; + /* [range] */ DWORD cbOutBufferSize; + /* [size_is][unique] */ BYTE* pvOutBuffer; +} Control_Return; + +typedef struct _GetAttrib_Return +{ + LONG ReturnCode; + /* [range] */ DWORD cbAttrLen; + /* [size_is][unique] */ BYTE* pbAttr; +} GetAttrib_Return; + +typedef struct _ReadCache_Return +{ + LONG ReturnCode; + /* [range] */ DWORD cbDataLen; + /* [size_is][unique] */ BYTE* pbData; +} ReadCache_Return; +#pragma pack(pop) + +typedef struct _Handles_Call +{ + REDIR_SCARDCONTEXT hContext; + REDIR_SCARDHANDLE hCard; +} Handles_Call; + +typedef struct _ListReaderGroups_Call +{ + Handles_Call handles; + LONG fmszGroupsIsNULL; + DWORD cchGroups; +} ListReaderGroups_Call; + +typedef struct _ListReaders_Call +{ + Handles_Call handles; + /* [range] */ DWORD cBytes; + /* [size_is][unique] */ BYTE* mszGroups; + LONG fmszReadersIsNULL; + DWORD cchReaders; +} ListReaders_Call; + +typedef struct _GetStatusChangeA_Call +{ + Handles_Call handles; + DWORD dwTimeOut; + /* [range] */ DWORD cReaders; + /* [size_is] */ LPSCARD_READERSTATEA rgReaderStates; +} GetStatusChangeA_Call; + +typedef struct _LocateCardsA_Call +{ + Handles_Call handles; + /* [range] */ DWORD cBytes; + /* [size_is] */ CHAR* mszCards; + /* [range] */ DWORD cReaders; + /* [size_is] */ LPSCARD_READERSTATEA rgReaderStates; +} LocateCardsA_Call; + +typedef struct _LocateCardsW_Call +{ + Handles_Call handles; + /* [range] */ DWORD cBytes; + /* [size_is] */ WCHAR* mszCards; + /* [range] */ DWORD cReaders; + /* [size_is] */ LPSCARD_READERSTATEW rgReaderStates; +} LocateCardsW_Call; + +typedef struct _LocateCardsByATRA_Call +{ + Handles_Call handles; + /* [range] */ DWORD cAtrs; + /* [size_is] */ LocateCards_ATRMask* rgAtrMasks; + /* [range] */ DWORD cReaders; + /* [size_is] */ LPSCARD_READERSTATEA rgReaderStates; +} LocateCardsByATRA_Call; + +typedef struct _LocateCardsByATRW_Call +{ + Handles_Call handles; + /* [range] */ DWORD cAtrs; + /* [size_is] */ LocateCards_ATRMask* rgAtrMasks; + /* [range] */ DWORD cReaders; + /* [size_is] */ LPSCARD_READERSTATEW rgReaderStates; +} LocateCardsByATRW_Call; + +typedef struct _GetStatusChangeW_Call +{ + Handles_Call handles; + DWORD dwTimeOut; + /* [range] */ DWORD cReaders; + /* [size_is] */ LPSCARD_READERSTATEW rgReaderStates; +} GetStatusChangeW_Call; + +typedef struct _GetReaderIcon_Call +{ + Handles_Call handles; + WCHAR* szReaderName; +} GetReaderIcon_Call; + +typedef struct _GetDeviceTypeId_Call +{ + Handles_Call handles; + WCHAR* szReaderName; +} GetDeviceTypeId_Call; + +typedef struct _Connect_Common_Call +{ + Handles_Call handles; + DWORD dwShareMode; + DWORD dwPreferredProtocols; +} Connect_Common_Call; + +typedef struct _ConnectA_Call +{ + Connect_Common_Call Common; + /* [string] */ CHAR* szReader; +} ConnectA_Call; + +typedef struct _ConnectW_Call +{ + Connect_Common_Call Common; + /* [string] */ WCHAR* szReader; +} ConnectW_Call; + +typedef struct _Reconnect_Call +{ + Handles_Call handles; + DWORD dwShareMode; + DWORD dwPreferredProtocols; + DWORD dwInitialization; +} Reconnect_Call; + +typedef struct _HCardAndDisposition_Call +{ + Handles_Call handles; + DWORD dwDisposition; +} HCardAndDisposition_Call; + +typedef struct _State_Call +{ + Handles_Call handles; + LONG fpbAtrIsNULL; + DWORD cbAtrLen; +} State_Call; + +typedef struct _Status_Call +{ + Handles_Call handles; + LONG fmszReaderNamesIsNULL; + DWORD cchReaderLen; + DWORD cbAtrLen; +} Status_Call; + +typedef struct _Transmit_Call +{ + Handles_Call handles; + LPSCARD_IO_REQUEST pioSendPci; + /* [range] */ DWORD cbSendLength; + /* [size_is] */ BYTE* pbSendBuffer; + /* [unique] */ LPSCARD_IO_REQUEST pioRecvPci; + LONG fpbRecvBufferIsNULL; + DWORD cbRecvLength; +} Transmit_Call; + +typedef struct _Long_Call +{ + Handles_Call handles; + LONG LongValue; +} Long_Call; + +typedef struct _Context_Call +{ + Handles_Call handles; +} Context_Call; + +typedef struct _ContextAndStringA_Call +{ + Handles_Call handles; + /* [string] */ char* sz; +} ContextAndStringA_Call; + +typedef struct _ContextAndStringW_Call +{ + Handles_Call handles; + /* [string] */ WCHAR* sz; +} ContextAndStringW_Call; + +typedef struct _ContextAndTwoStringA_Call +{ + Handles_Call handles; + /* [string] */ char* sz1; + /* [string] */ char* sz2; +} ContextAndTwoStringA_Call; + +typedef struct _ContextAndTwoStringW_Call +{ + Handles_Call handles; + /* [string] */ WCHAR* sz1; + /* [string] */ WCHAR* sz2; +} ContextAndTwoStringW_Call; + +typedef struct _EstablishContext_Call +{ + Handles_Call handles; + DWORD dwScope; +} EstablishContext_Call; + +typedef struct _GetTranmitCount_Call +{ + Handles_Call handles; +} GetTransmitCount_Call; + +typedef struct _Control_Call +{ + Handles_Call handles; + DWORD dwControlCode; + /* [range] */ DWORD cbInBufferSize; + /* [size_is][unique] */ BYTE* pvInBuffer; + LONG fpvOutBufferIsNULL; + DWORD cbOutBufferSize; +} Control_Call; + +typedef struct _GetAttrib_Call +{ + Handles_Call handles; + DWORD dwAttrId; + LONG fpbAttrIsNULL; + DWORD cbAttrLen; +} GetAttrib_Call; + +typedef struct _SetAttrib_Call +{ + Handles_Call handles; + DWORD dwAttrId; + /* [range] */ DWORD cbAttrLen; + /* [size_is] */ BYTE* pbAttr; +} SetAttrib_Call; + +typedef struct _ReadCache_Common +{ + Handles_Call handles; + UUID* CardIdentifier; + DWORD FreshnessCounter; + LONG fPbDataIsNULL; + DWORD cbDataLen; +} ReadCache_Common; + +typedef struct _ReadCacheA_Call +{ + ReadCache_Common Common; + /* [string] */ char* szLookupName; +} ReadCacheA_Call; + +typedef struct _ReadCacheW_Call +{ + ReadCache_Common Common; + /* [string] */ WCHAR* szLookupName; +} ReadCacheW_Call; + +typedef struct _WriteCache_Common +{ + Handles_Call handles; + UUID* CardIdentifier; + DWORD FreshnessCounter; + /* [range] */ DWORD cbDataLen; + /* [size_is][unique] */ BYTE* pbData; +} WriteCache_Common; + +typedef struct _WriteCacheA_Call +{ + WriteCache_Common Common; + /* [string] */ char* szLookupName; +} WriteCacheA_Call; + +typedef struct _WriteCacheW_Call +{ + WriteCache_Common Common; + /* [string] */ WCHAR* szLookupName; +} WriteCacheW_Call; + +struct _SMARTCARD_OPERATION +{ + IRP* irp; + union + { + Handles_Call handles; + Long_Call lng; + Context_Call context; + ContextAndStringA_Call contextAndStringA; + ContextAndStringW_Call contextAndStringW; + ContextAndTwoStringA_Call contextAndTwoStringA; + ContextAndTwoStringW_Call contextAndTwoStringW; + EstablishContext_Call establishContext; + ListReaderGroups_Call listReaderGroups; + ListReaders_Call listReaders; + GetStatusChangeA_Call getStatusChangeA; + LocateCardsA_Call locateCardsA; + LocateCardsW_Call locateCardsW; + LocateCards_ATRMask locateCardsATRMask; + LocateCardsByATRA_Call locateCardsByATRA; + LocateCardsByATRW_Call locateCardsByATRW; + GetStatusChangeW_Call getStatusChangeW; + GetReaderIcon_Call getReaderIcon; + GetDeviceTypeId_Call getDeviceTypeId; + Connect_Common_Call connect; + ConnectA_Call connectA; + ConnectW_Call connectW; + Reconnect_Call reconnect; + HCardAndDisposition_Call hCardAndDisposition; + State_Call state; + Status_Call status; + SCardIO_Request scardIO; + Transmit_Call transmit; + GetTransmitCount_Call getTransmitCount; + Control_Call control; + GetAttrib_Call getAttrib; + SetAttrib_Call setAttrib; + ReadCache_Common readCache; + ReadCacheA_Call readCacheA; + ReadCacheW_Call readCacheW; + WriteCache_Common writeCache; + WriteCacheA_Call writeCacheA; + WriteCacheW_Call writeCacheW; + } call; + UINT32 ioControlCode; + SCARDCONTEXT hContext; + SCARDHANDLE hCard; +}; +typedef struct _SMARTCARD_OPERATION SMARTCARD_OPERATION; + +#endif /* FREERDP_CHANNEL_SMARTCARD_CLIENT_OPERATIONS_H */ diff --git a/channels/smartcard/client/smartcard_pack.c b/channels/smartcard/client/smartcard_pack.c new file mode 100644 index 0000000..f70eb4e --- /dev/null +++ b/channels/smartcard/client/smartcard_pack.c @@ -0,0 +1,3888 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Smart Card Structure Packing + * + * Copyright 2014 Marc-Andre Moreau + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger + * Copyright 2020 Armin Novak + * Copyright 2020 Thincast Technologies GmbH + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include + +#include "smartcard_pack.h" + +static const DWORD g_LogLevel = WLOG_DEBUG; + +#define smartcard_unpack_redir_scard_context(smartcard, s, context, index) \ + smartcard_unpack_redir_scard_context_((smartcard), (s), (context), (index), __FILE__, \ + __FUNCTION__, __LINE__) +#define smartcard_unpack_redir_scard_handle(smartcard, s, context, index) \ + smartcard_unpack_redir_scard_handle_((smartcard), (s), (context), (index), __FILE__, \ + __FUNCTION__, __LINE__) + +static LONG smartcard_unpack_redir_scard_context_(SMARTCARD_DEVICE* smartcard, wStream* s, + REDIR_SCARDCONTEXT* context, UINT32* index, + const char* file, const char* function, int line); +static LONG smartcard_pack_redir_scard_context(SMARTCARD_DEVICE* smartcard, wStream* s, + const REDIR_SCARDCONTEXT* context, DWORD* index); +static LONG smartcard_unpack_redir_scard_handle_(SMARTCARD_DEVICE* smartcard, wStream* s, + REDIR_SCARDHANDLE* handle, UINT32* index, + const char* file, const char* function, int line); +static LONG smartcard_pack_redir_scard_handle(SMARTCARD_DEVICE* smartcard, wStream* s, + const REDIR_SCARDHANDLE* handle, DWORD* index); +static LONG smartcard_unpack_redir_scard_context_ref(SMARTCARD_DEVICE* smartcard, wStream* s, + REDIR_SCARDCONTEXT* context); +static LONG smartcard_pack_redir_scard_context_ref(SMARTCARD_DEVICE* smartcard, wStream* s, + const REDIR_SCARDCONTEXT* context); + +static LONG smartcard_unpack_redir_scard_handle_ref(SMARTCARD_DEVICE* smartcard, wStream* s, + REDIR_SCARDHANDLE* handle); +static LONG smartcard_pack_redir_scard_handle_ref(SMARTCARD_DEVICE* smartcard, wStream* s, + const REDIR_SCARDHANDLE* handle); + +typedef enum +{ + NDR_PTR_FULL, + NDR_PTR_SIMPLE, + NDR_PTR_FIXED +} ndr_ptr_t; + +/* Reads a NDR pointer and checks if the value read has the expected relative + * addressing */ +#define smartcard_ndr_pointer_read(s, index, ptr) \ + smartcard_ndr_pointer_read_((s), (index), (ptr), __FILE__, __FUNCTION__, __LINE__) +static BOOL smartcard_ndr_pointer_read_(wStream* s, UINT32* index, UINT32* ptr, const char* file, + const char* fkt, int line) +{ + const UINT32 expect = 0x20000 + (*index) * 4; + UINT32 ndrPtr; + WINPR_UNUSED(file); + if (!s) + return FALSE; + if (Stream_GetRemainingLength(s) < 4) + return FALSE; + + Stream_Read_UINT32(s, ndrPtr); /* mszGroupsNdrPtr (4 bytes) */ + if (ptr) + *ptr = ndrPtr; + if (expect != ndrPtr) + { + /* Allow NULL pointer if we read the result */ + if (ptr && (ndrPtr == 0)) + return TRUE; + WLog_WARN(TAG, "[%s:%d] Read context pointer 0x%08" PRIx32 ", expected 0x%08" PRIx32, fkt, + line, ndrPtr, expect); + return FALSE; + } + + (*index) = (*index) + 1; + return TRUE; +} + +static LONG smartcard_ndr_read(wStream* s, BYTE** data, size_t min, size_t elementSize, + ndr_ptr_t type) +{ + size_t len, offset, len2; + void* r; + size_t required; + + switch (type) + { + case NDR_PTR_FULL: + required = 12; + break; + case NDR_PTR_SIMPLE: + required = 4; + break; + case NDR_PTR_FIXED: + required = min; + break; + } + + if (Stream_GetRemainingLength(s) < required) + { + WLog_ERR(TAG, "Short data while trying to read NDR pointer, expected 4, got %" PRIu32, + Stream_GetRemainingLength(s)); + return STATUS_BUFFER_TOO_SMALL; + } + + switch (type) + { + case NDR_PTR_FULL: + Stream_Read_UINT32(s, len); + Stream_Read_UINT32(s, offset); + Stream_Read_UINT32(s, len2); + if (len != offset + len2) + { + WLog_ERR(TAG, + "Invalid data when reading full NDR pointer: total=%" PRIu32 + ", offset=%" PRIu32 ", remaining=%" PRIu32, + len, offset, len2); + return STATUS_BUFFER_TOO_SMALL; + } + break; + case NDR_PTR_SIMPLE: + Stream_Read_UINT32(s, len); + + if ((len != min) && (min > 0)) + { + WLog_ERR(TAG, + "Invalid data when reading simple NDR pointer: total=%" PRIu32 + ", expected=%" PRIu32, + len, min); + return STATUS_BUFFER_TOO_SMALL; + } + break; + case NDR_PTR_FIXED: + len = (UINT32)min; + break; + } + + if (min > len) + { + WLog_ERR(TAG, "Invalid length read from NDR pointer, minimum %" PRIu32 ", got %" PRIu32, + min, len); + return STATUS_DATA_ERROR; + } + + if (len > SIZE_MAX / 2) + return STATUS_BUFFER_TOO_SMALL; + + if (Stream_GetRemainingLength(s) / elementSize < len) + { + WLog_ERR(TAG, + "Short data while trying to read data from NDR pointer, expected %" PRIu32 + ", got %" PRIu32, + len, Stream_GetRemainingLength(s)); + return STATUS_BUFFER_TOO_SMALL; + } + len *= elementSize; + + r = calloc(len + 1, sizeof(CHAR)); + if (!r) + return SCARD_E_NO_MEMORY; + Stream_Read(s, r, len); + smartcard_unpack_read_size_align(NULL, s, len, 4); + *data = r; + return STATUS_SUCCESS; +} + +static BOOL smartcard_ndr_pointer_write(wStream* s, UINT32* index, DWORD length) +{ + const UINT32 ndrPtr = 0x20000 + (*index) * 4; + + if (!s) + return FALSE; + if (!Stream_EnsureRemainingCapacity(s, 4)) + return FALSE; + + if (length > 0) + { + Stream_Write_UINT32(s, ndrPtr); /* mszGroupsNdrPtr (4 bytes) */ + (*index) = (*index) + 1; + } + else + Stream_Write_UINT32(s, 0); + return TRUE; +} + +static LONG smartcard_ndr_write(wStream* s, const BYTE* data, UINT32 size, UINT32 elementSize, + ndr_ptr_t type) +{ + const UINT32 offset = 0; + const UINT32 len = size; + const UINT32 dataLen = size * elementSize; + size_t required; + + if (size == 0) + return SCARD_S_SUCCESS; + + switch (type) + { + case NDR_PTR_FULL: + required = 12; + break; + case NDR_PTR_SIMPLE: + required = 4; + break; + case NDR_PTR_FIXED: + required = 0; + break; + } + + if (!Stream_EnsureRemainingCapacity(s, required + dataLen + 4)) + return STATUS_BUFFER_TOO_SMALL; + + switch (type) + { + case NDR_PTR_FULL: + Stream_Write_UINT32(s, len); + Stream_Write_UINT32(s, offset); + Stream_Write_UINT32(s, len); + break; + case NDR_PTR_SIMPLE: + Stream_Write_UINT32(s, len); + break; + case NDR_PTR_FIXED: + break; + } + + if (data) + Stream_Write(s, data, dataLen); + else + Stream_Zero(s, dataLen); + return smartcard_pack_write_size_align(NULL, s, len, 4); +} + +static LONG smartcard_ndr_write_state(wStream* s, const ReaderState_Return* data, UINT32 size, + ndr_ptr_t type) +{ + union + { + const ReaderState_Return* reader; + const BYTE* data; + } cnv; + + cnv.reader = data; + return smartcard_ndr_write(s, cnv.data, size, sizeof(ReaderState_Return), type); +} + +static LONG smartcard_ndr_read_atrmask(wStream* s, LocateCards_ATRMask** data, size_t min, + ndr_ptr_t type) +{ + union + { + LocateCards_ATRMask** ppc; + BYTE** ppv; + } u; + u.ppc = data; + return smartcard_ndr_read(s, u.ppv, min, sizeof(LocateCards_ATRMask), type); +} + +static LONG smartcard_ndr_read_fixed_string_a(wStream* s, CHAR** data, size_t min, ndr_ptr_t type) +{ + union + { + CHAR** ppc; + BYTE** ppv; + } u; + u.ppc = data; + return smartcard_ndr_read(s, u.ppv, min, sizeof(CHAR), type); +} + +static LONG smartcard_ndr_read_fixed_string_w(wStream* s, WCHAR** data, size_t min, ndr_ptr_t type) +{ + union + { + WCHAR** ppc; + BYTE** ppv; + } u; + u.ppc = data; + return smartcard_ndr_read(s, u.ppv, min, sizeof(WCHAR), type); +} + +static LONG smartcard_ndr_read_a(wStream* s, CHAR** data, ndr_ptr_t type) +{ + union + { + CHAR** ppc; + BYTE** ppv; + } u; + u.ppc = data; + return smartcard_ndr_read(s, u.ppv, 0, sizeof(CHAR), type); +} + +static LONG smartcard_ndr_read_w(wStream* s, WCHAR** data, ndr_ptr_t type) +{ + union + { + WCHAR** ppc; + BYTE** ppv; + } u; + u.ppc = data; + return smartcard_ndr_read(s, u.ppv, 0, sizeof(WCHAR), type); +} + +static LONG smartcard_ndr_read_u(wStream* s, UUID** data) +{ + union + { + UUID** ppc; + BYTE** ppv; + } u; + u.ppc = data; + return smartcard_ndr_read(s, u.ppv, 1, sizeof(UUID), NDR_PTR_FIXED); +} + +static char* smartcard_convert_string_list(const void* in, size_t bytes, BOOL unicode) +{ + size_t index, length; + union + { + const void* pv; + const char* sz; + const WCHAR* wz; + } string; + char* mszA = NULL; + + string.pv = in; + + if (bytes < 1) + return NULL; + + if (in == NULL) + return NULL; + + if (unicode) + { + length = (bytes / sizeof(WCHAR)) - 1; + mszA = (char*)calloc(length + 1, sizeof(WCHAR)); + if (!mszA) + return NULL; + if (ConvertFromUnicode(CP_UTF8, 0, string.wz, (int)length, &mszA, length + 1, NULL, NULL) != + (int)length) + { + free(mszA); + return NULL; + } + } + else + { + length = bytes; + mszA = (char*)calloc(length, sizeof(char)); + if (!mszA) + return NULL; + CopyMemory(mszA, string.sz, length - 1); + mszA[length - 1] = '\0'; + } + + for (index = 0; index < length - 1; index++) + { + if (mszA[index] == '\0') + mszA[index] = ','; + } + + return mszA; +} + +static char* smartcard_msz_dump_a(const char* msz, size_t len, char* buffer, size_t bufferLen) +{ + char* buf = buffer; + const char* cur = msz; + + while ((len > 0) && cur && cur[0] != '\0' && (bufferLen > 0)) + { + size_t clen = strnlen(cur, len); + int rc = _snprintf(buf, bufferLen, "%s", cur); + bufferLen -= (size_t)rc; + buf += rc; + + cur += clen; + } + + return buffer; +} + +static char* smartcard_msz_dump_w(const WCHAR* msz, size_t len, char* buffer, size_t bufferLen) +{ + char* sz = NULL; + ConvertFromUnicode(CP_UTF8, 0, msz, (int)len, &sz, 0, NULL, NULL); + return smartcard_msz_dump_a(sz, len, buffer, bufferLen); +} + +static char* smartcard_array_dump(const void* pd, size_t len, char* buffer, size_t bufferLen) +{ + const BYTE* data = pd; + size_t x; + int rc; + char* start = buffer; + + /* Ensure '\0' termination */ + if (bufferLen > 0) + { + buffer[bufferLen - 1] = '\0'; + bufferLen--; + } + + rc = _snprintf(buffer, bufferLen, "{ "); + if ((rc < 0) || ((size_t)rc > bufferLen)) + goto fail; + buffer += rc; + bufferLen -= (size_t)rc; + + for (x = 0; x < len; x++) + { + rc = _snprintf(buffer, bufferLen, "%02X", data[x]); + if ((rc < 0) || ((size_t)rc > bufferLen)) + goto fail; + buffer += rc; + bufferLen -= (size_t)rc; + } + + rc = _snprintf(buffer, bufferLen, " }"); + if ((rc < 0) || ((size_t)rc > bufferLen)) + goto fail; + buffer += rc; + bufferLen -= (size_t)rc; + +fail: + return start; +} +static void smartcard_log_redir_handle(const char* tag, const REDIR_SCARDHANDLE* pHandle) +{ + char buffer[128]; + + WLog_LVL(tag, g_LogLevel, " hContext: %s", + smartcard_array_dump(pHandle->pbHandle, pHandle->cbHandle, buffer, sizeof(buffer))); +} + +static void smartcard_log_context(const char* tag, const REDIR_SCARDCONTEXT* phContext) +{ + char buffer[128]; + WLog_DBG( + tag, "hContext: %s", + smartcard_array_dump(phContext->pbContext, phContext->cbContext, buffer, sizeof(buffer))); +} + +static void smartcard_trace_context_and_string_call_a(const char* name, + const REDIR_SCARDCONTEXT* phContext, + const CHAR* sz) +{ + if (!WLog_IsLevelActive(WLog_Get(TAG), g_LogLevel)) + return; + + WLog_LVL(TAG, g_LogLevel, "%s {", name); + smartcard_log_context(TAG, phContext); + WLog_LVL(TAG, g_LogLevel, " sz=%s", sz); + + WLog_LVL(TAG, g_LogLevel, "}"); +} + +static void smartcard_trace_context_and_string_call_w(const char* name, + const REDIR_SCARDCONTEXT* phContext, + const WCHAR* sz) +{ + char* tmp = NULL; + if (!WLog_IsLevelActive(WLog_Get(TAG), g_LogLevel)) + return; + + WLog_LVL(TAG, g_LogLevel, "%s {", name); + smartcard_log_context(TAG, phContext); + ConvertFromUnicode(CP_UTF8, 0, sz, -1, &tmp, 0, NULL, NULL); + WLog_LVL(TAG, g_LogLevel, " sz=%s", tmp); + free(tmp); + + WLog_LVL(TAG, g_LogLevel, "}"); +} + +static void smartcard_trace_context_call(SMARTCARD_DEVICE* smartcard, const Context_Call* call, + const char* name) +{ + WINPR_UNUSED(smartcard); + + if (!WLog_IsLevelActive(WLog_Get(TAG), g_LogLevel)) + return; + + WLog_LVL(TAG, g_LogLevel, "%s_Call {", name); + smartcard_log_context(TAG, &call->handles.hContext); + + WLog_LVL(TAG, g_LogLevel, "}"); +} + +static void smartcard_trace_list_reader_groups_call(SMARTCARD_DEVICE* smartcard, + const ListReaderGroups_Call* call, BOOL unicode) +{ + WINPR_UNUSED(smartcard); + + if (!WLog_IsLevelActive(WLog_Get(TAG), g_LogLevel)) + return; + + WLog_LVL(TAG, g_LogLevel, "ListReaderGroups%S_Call {", unicode ? "W" : "A"); + smartcard_log_context(TAG, &call->handles.hContext); + + WLog_LVL(TAG, g_LogLevel, "fmszGroupsIsNULL: %" PRId32 " cchGroups: 0x%08" PRIx32, + call->fmszGroupsIsNULL, call->cchGroups); + WLog_LVL(TAG, g_LogLevel, "}"); +} + +static void smartcard_trace_get_status_change_w_call(SMARTCARD_DEVICE* smartcard, + const GetStatusChangeW_Call* call) +{ + UINT32 index; + char* szEventState; + char* szCurrentState; + LPSCARD_READERSTATEW readerState; + WINPR_UNUSED(smartcard); + + if (!WLog_IsLevelActive(WLog_Get(TAG), g_LogLevel)) + return; + + WLog_LVL(TAG, g_LogLevel, "GetStatusChangeW_Call {"); + smartcard_log_context(TAG, &call->handles.hContext); + + WLog_LVL(TAG, g_LogLevel, "dwTimeOut: 0x%08" PRIX32 " cReaders: %" PRIu32 "", call->dwTimeOut, + call->cReaders); + + for (index = 0; index < call->cReaders; index++) + { + char* szReaderA = NULL; + readerState = &call->rgReaderStates[index]; + ConvertFromUnicode(CP_UTF8, 0, readerState->szReader, -1, &szReaderA, 0, NULL, NULL); + WLog_LVL(TAG, g_LogLevel, "\t[%" PRIu32 "]: szReader: %s cbAtr: %" PRIu32 "", index, + szReaderA, readerState->cbAtr); + szCurrentState = SCardGetReaderStateString(readerState->dwCurrentState); + szEventState = SCardGetReaderStateString(readerState->dwEventState); + WLog_LVL(TAG, g_LogLevel, "\t[%" PRIu32 "]: dwCurrentState: %s (0x%08" PRIX32 ")", index, + szCurrentState, readerState->dwCurrentState); + WLog_LVL(TAG, g_LogLevel, "\t[%" PRIu32 "]: dwEventState: %s (0x%08" PRIX32 ")", index, + szEventState, readerState->dwEventState); + free(szCurrentState); + free(szEventState); + free(szReaderA); + } + + WLog_LVL(TAG, g_LogLevel, "}"); +} + +static void smartcard_trace_list_reader_groups_return(SMARTCARD_DEVICE* smartcard, + const ListReaderGroups_Return* ret, + BOOL unicode) +{ + char* mszA = NULL; + WINPR_UNUSED(smartcard); + + if (!WLog_IsLevelActive(WLog_Get(TAG), g_LogLevel)) + return; + + mszA = smartcard_convert_string_list(ret->msz, ret->cBytes, unicode); + + WLog_LVL(TAG, g_LogLevel, "ListReaderGroups%s_Return {", unicode ? "W" : "A"); + WLog_LVL(TAG, g_LogLevel, " ReturnCode: %s (0x%08" PRIx32 ")", + SCardGetErrorString(ret->ReturnCode), ret->ReturnCode); + WLog_LVL(TAG, g_LogLevel, " cBytes: %" PRIu32 " msz: %s", ret->cBytes, mszA); + WLog_LVL(TAG, g_LogLevel, "}"); + free(mszA); +} + +static void smartcard_trace_list_readers_call(SMARTCARD_DEVICE* smartcard, + const ListReaders_Call* call, BOOL unicode) +{ + char* mszGroupsA = NULL; + WINPR_UNUSED(smartcard); + + if (!WLog_IsLevelActive(WLog_Get(TAG), g_LogLevel)) + return; + + mszGroupsA = smartcard_convert_string_list(call->mszGroups, call->cBytes, unicode); + + WLog_LVL(TAG, g_LogLevel, "ListReaders%s_Call {", unicode ? "W" : "A"); + smartcard_log_context(TAG, &call->handles.hContext); + + WLog_LVL(TAG, g_LogLevel, + "cBytes: %" PRIu32 " mszGroups: %s fmszReadersIsNULL: %" PRId32 + " cchReaders: 0x%08" PRIX32 "", + call->cBytes, mszGroupsA, call->fmszReadersIsNULL, call->cchReaders); + WLog_LVL(TAG, g_LogLevel, "}"); + + free(mszGroupsA); +} + +static void smartcard_trace_locate_cards_by_atr_a_call(SMARTCARD_DEVICE* smartcard, + const LocateCardsByATRA_Call* call) +{ + UINT32 index; + char* szEventState; + char* szCurrentState; + + WINPR_UNUSED(smartcard); + + if (!WLog_IsLevelActive(WLog_Get(TAG), g_LogLevel)) + return; + + WLog_LVL(TAG, g_LogLevel, "LocateCardsByATRA_Call {"); + smartcard_log_context(TAG, &call->handles.hContext); + + for (index = 0; index < call->cReaders; index++) + { + char buffer[1024]; + const LPSCARD_READERSTATEA readerState = &call->rgReaderStates[index]; + + WLog_LVL(TAG, g_LogLevel, "\t[%" PRIu32 "]: szReader: %s cbAtr: %" PRIu32 "", index, + readerState->szReader, readerState->cbAtr); + szCurrentState = SCardGetReaderStateString(readerState->dwCurrentState); + szEventState = SCardGetReaderStateString(readerState->dwEventState); + WLog_LVL(TAG, g_LogLevel, "\t[%" PRIu32 "]: dwCurrentState: %s (0x%08" PRIX32 ")", index, + szCurrentState, readerState->dwCurrentState); + WLog_LVL(TAG, g_LogLevel, "\t[%" PRIu32 "]: dwEventState: %s (0x%08" PRIX32 ")", index, + szEventState, readerState->dwEventState); + + WLog_DBG( + TAG, "\t[%" PRIu32 "]: cbAtr: %" PRIu32 " rgbAtr: %s", index, readerState->cbAtr, + smartcard_array_dump(readerState->rgbAtr, readerState->cbAtr, buffer, sizeof(buffer))); + + free(szCurrentState); + free(szEventState); + } + + WLog_LVL(TAG, g_LogLevel, "}"); +} + +static void smartcard_trace_locate_cards_a_call(SMARTCARD_DEVICE* smartcard, + const LocateCardsA_Call* call) +{ + char buffer[8192]; + WINPR_UNUSED(smartcard); + + if (!WLog_IsLevelActive(WLog_Get(TAG), g_LogLevel)) + return; + + WLog_LVL(TAG, g_LogLevel, "LocateCardsA_Call {"); + smartcard_log_context(TAG, &call->handles.hContext); + WLog_LVL(TAG, g_LogLevel, " cBytes=%" PRId32, call->cBytes); + WLog_LVL(TAG, g_LogLevel, " mszCards=%s", + smartcard_msz_dump_a(call->mszCards, call->cBytes, buffer, sizeof(buffer))); + WLog_LVL(TAG, g_LogLevel, " cReaders=%" PRId32, call->cReaders); + // WLog_LVL(TAG, g_LogLevel, " cReaders=%" PRId32, call->rgReaderStates); + + WLog_LVL(TAG, g_LogLevel, "}"); +} + +static void smartcard_trace_locate_cards_return(SMARTCARD_DEVICE* smartcard, + const LocateCards_Return* ret) +{ + WINPR_UNUSED(smartcard); + if (!WLog_IsLevelActive(WLog_Get(TAG), g_LogLevel)) + return; + + WLog_LVL(TAG, g_LogLevel, "LocateCards_Return {"); + WLog_LVL(TAG, g_LogLevel, " ReturnCode: %s (0x%08" PRIX32 ")", + SCardGetErrorString(ret->ReturnCode), ret->ReturnCode); + + if (ret->ReturnCode == SCARD_S_SUCCESS) + { + WLog_LVL(TAG, g_LogLevel, " cReaders=%" PRId32, ret->cReaders); + } + WLog_LVL(TAG, g_LogLevel, "}"); +} + +static void smartcard_trace_get_reader_icon_return(SMARTCARD_DEVICE* smartcard, + const GetReaderIcon_Return* ret) +{ + WINPR_UNUSED(smartcard); + if (!WLog_IsLevelActive(WLog_Get(TAG), g_LogLevel)) + return; + + WLog_LVL(TAG, g_LogLevel, "GetReaderIcon_Return {"); + WLog_LVL(TAG, g_LogLevel, " ReturnCode: %s (0x%08" PRIX32 ")", + SCardGetErrorString(ret->ReturnCode), ret->ReturnCode); + + if (ret->ReturnCode == SCARD_S_SUCCESS) + { + WLog_LVL(TAG, g_LogLevel, " cbDataLen=%" PRId32, ret->cbDataLen); + } + WLog_LVL(TAG, g_LogLevel, "}"); +} + +static void smartcard_trace_get_transmit_count_return(SMARTCARD_DEVICE* smartcard, + const GetTransmitCount_Return* ret) +{ + WINPR_UNUSED(smartcard); + if (!WLog_IsLevelActive(WLog_Get(TAG), g_LogLevel)) + return; + + WLog_LVL(TAG, g_LogLevel, "GetTransmitCount_Return {"); + WLog_LVL(TAG, g_LogLevel, " ReturnCode: %s (0x%08" PRIX32 ")", + SCardGetErrorString(ret->ReturnCode), ret->ReturnCode); + + WLog_LVL(TAG, g_LogLevel, " cTransmitCount=%" PRIu32, ret->cTransmitCount); + WLog_LVL(TAG, g_LogLevel, "}"); +} + +static void smartcard_trace_read_cache_return(SMARTCARD_DEVICE* smartcard, + const ReadCache_Return* ret) +{ + WINPR_UNUSED(smartcard); + if (!WLog_IsLevelActive(WLog_Get(TAG), g_LogLevel)) + return; + + WLog_LVL(TAG, g_LogLevel, "ReadCache_Return {"); + WLog_LVL(TAG, g_LogLevel, " ReturnCode: %s (0x%08" PRIX32 ")", + SCardGetErrorString(ret->ReturnCode), ret->ReturnCode); + + if (ret->ReturnCode == SCARD_S_SUCCESS) + { + char buffer[1024]; + WLog_LVL(TAG, g_LogLevel, " cbDataLen=%" PRId32, ret->cbDataLen); + WLog_LVL(TAG, g_LogLevel, " cbData: %s", + smartcard_array_dump(ret->pbData, ret->cbDataLen, buffer, sizeof(buffer))); + } + WLog_LVL(TAG, g_LogLevel, "}"); +} + +static void smartcard_trace_locate_cards_w_call(SMARTCARD_DEVICE* smartcard, + const LocateCardsW_Call* call) +{ + char buffer[8192]; + WINPR_UNUSED(smartcard); + if (!WLog_IsLevelActive(WLog_Get(TAG), g_LogLevel)) + return; + + WLog_LVL(TAG, g_LogLevel, "LocateCardsW_Call {"); + smartcard_log_context(TAG, &call->handles.hContext); + WLog_LVL(TAG, g_LogLevel, " cBytes=%" PRId32, call->cBytes); + WLog_LVL(TAG, g_LogLevel, " sz2=%s", + smartcard_msz_dump_w(call->mszCards, call->cBytes, buffer, sizeof(buffer))); + WLog_LVL(TAG, g_LogLevel, " cReaders=%" PRId32, call->cReaders); + // WLog_LVL(TAG, g_LogLevel, " sz2=%s", call->rgReaderStates); + WLog_LVL(TAG, g_LogLevel, "}"); +} + +static void smartcard_trace_list_readers_return(SMARTCARD_DEVICE* smartcard, + const ListReaders_Return* ret, BOOL unicode) +{ + char* mszA = NULL; + WINPR_UNUSED(smartcard); + + if (!WLog_IsLevelActive(WLog_Get(TAG), g_LogLevel)) + return; + + WLog_LVL(TAG, g_LogLevel, "ListReaders%s_Return {", unicode ? "W" : "A"); + WLog_LVL(TAG, g_LogLevel, " ReturnCode: %s (0x%08" PRIX32 ")", + SCardGetErrorString(ret->ReturnCode), ret->ReturnCode); + + if (ret->ReturnCode != SCARD_S_SUCCESS) + { + WLog_LVL(TAG, g_LogLevel, "}"); + return; + } + + mszA = smartcard_convert_string_list(ret->msz, ret->cBytes, unicode); + + WLog_LVL(TAG, g_LogLevel, " cBytes: %" PRIu32 " msz: %s", ret->cBytes, mszA); + WLog_LVL(TAG, g_LogLevel, "}"); + free(mszA); +} + +static void smartcard_trace_get_status_change_return(SMARTCARD_DEVICE* smartcard, + const GetStatusChange_Return* ret, + BOOL unicode) +{ + UINT32 index; + char* szEventState; + char* szCurrentState; + + WINPR_UNUSED(smartcard); + + if (!WLog_IsLevelActive(WLog_Get(TAG), g_LogLevel)) + return; + + WLog_LVL(TAG, g_LogLevel, "GetStatusChange%s_Return {", unicode ? "W" : "A"); + WLog_LVL(TAG, g_LogLevel, " ReturnCode: %s (0x%08" PRIX32 ")", + SCardGetErrorString(ret->ReturnCode), ret->ReturnCode); + WLog_LVL(TAG, g_LogLevel, " cReaders: %" PRIu32 "", ret->cReaders); + + for (index = 0; index < ret->cReaders; index++) + { + char buffer[1024]; + const ReaderState_Return* rgReaderState = &(ret->rgReaderStates[index]); + szCurrentState = SCardGetReaderStateString(rgReaderState->dwCurrentState); + szEventState = SCardGetReaderStateString(rgReaderState->dwEventState); + WLog_LVL(TAG, g_LogLevel, " [%" PRIu32 "]: dwCurrentState: %s (0x%08" PRIX32 ")", index, + szCurrentState, rgReaderState->dwCurrentState); + WLog_LVL(TAG, g_LogLevel, " [%" PRIu32 "]: dwEventState: %s (0x%08" PRIX32 ")", index, + szEventState, rgReaderState->dwEventState); + WLog_LVL(TAG, g_LogLevel, " [%" PRIu32 "]: cbAtr: %" PRIu32 " rgbAtr: %s", index, + rgReaderState->cbAtr, + smartcard_array_dump(rgReaderState->rgbAtr, rgReaderState->cbAtr, buffer, + sizeof(buffer))); + free(szCurrentState); + free(szEventState); + } + + WLog_LVL(TAG, g_LogLevel, "}"); +} + +static void smartcard_trace_context_and_two_strings_a_call(SMARTCARD_DEVICE* smartcard, + const ContextAndTwoStringA_Call* call) +{ + WINPR_UNUSED(smartcard); + if (!WLog_IsLevelActive(WLog_Get(TAG), g_LogLevel)) + return; + + WLog_LVL(TAG, g_LogLevel, "ContextAndTwoStringW_Call {"); + smartcard_log_context(TAG, &call->handles.hContext); + WLog_LVL(TAG, g_LogLevel, " sz1=%s", call->sz1); + WLog_LVL(TAG, g_LogLevel, " sz2=%s", call->sz2); + WLog_LVL(TAG, g_LogLevel, "}"); +} + +static void smartcard_trace_context_and_two_strings_w_call(SMARTCARD_DEVICE* smartcard, + const ContextAndTwoStringW_Call* call) +{ + CHAR* sz1 = NULL; + CHAR* sz2 = NULL; + + WINPR_UNUSED(smartcard); + if (!WLog_IsLevelActive(WLog_Get(TAG), g_LogLevel)) + return; + + WLog_LVL(TAG, g_LogLevel, "ContextAndTwoStringW_Call {"); + smartcard_log_context(TAG, &call->handles.hContext); + ConvertFromUnicode(CP_UTF8, 0, call->sz1, -1, &sz1, 0, NULL, NULL); + ConvertFromUnicode(CP_UTF8, 0, call->sz2, -1, &sz2, 0, NULL, NULL); + WLog_LVL(TAG, g_LogLevel, " sz1=%s", sz1); + WLog_LVL(TAG, g_LogLevel, " sz2=%s", sz2); + free(sz1); + free(sz2); + + WLog_LVL(TAG, g_LogLevel, "}"); +} + +static void smartcard_trace_get_transmit_count_call(SMARTCARD_DEVICE* smartcard, + const GetTransmitCount_Call* call) +{ + WINPR_UNUSED(smartcard); + if (!WLog_IsLevelActive(WLog_Get(TAG), g_LogLevel)) + return; + + WLog_LVL(TAG, g_LogLevel, "GetTransmitCount_Call {"); + smartcard_log_context(TAG, &call->handles.hContext); + smartcard_log_redir_handle(TAG, &call->handles.hCard); + + WLog_LVL(TAG, g_LogLevel, "}"); +} + +static void smartcard_trace_write_cache_a_call(SMARTCARD_DEVICE* smartcard, + const WriteCacheA_Call* call) +{ + char buffer[1024]; + WINPR_UNUSED(smartcard); + if (!WLog_IsLevelActive(WLog_Get(TAG), g_LogLevel)) + return; + + WLog_LVL(TAG, g_LogLevel, "GetTransmitCount_Call {"); + + WLog_LVL(TAG, g_LogLevel, " szLookupName=%s", call->szLookupName); + + smartcard_log_context(TAG, &call->Common.handles.hContext); + WLog_DBG( + TAG, "..CardIdentifier=%s", + smartcard_array_dump(call->Common.CardIdentifier, sizeof(UUID), buffer, sizeof(buffer))); + WLog_LVL(TAG, g_LogLevel, " FreshnessCounter=%" PRIu32, call->Common.FreshnessCounter); + WLog_LVL(TAG, g_LogLevel, " cbDataLen=%" PRIu32, call->Common.cbDataLen); + WLog_DBG( + TAG, " pbData=%s", + smartcard_array_dump(call->Common.pbData, call->Common.cbDataLen, buffer, sizeof(buffer))); + WLog_LVL(TAG, g_LogLevel, "}"); +} + +static void smartcard_trace_write_cache_w_call(SMARTCARD_DEVICE* smartcard, + const WriteCacheW_Call* call) +{ + char* tmp = NULL; + char buffer[1024]; + WINPR_UNUSED(smartcard); + if (!WLog_IsLevelActive(WLog_Get(TAG), g_LogLevel)) + return; + + WLog_LVL(TAG, g_LogLevel, "GetTransmitCount_Call {"); + + ConvertFromUnicode(CP_UTF8, 0, call->szLookupName, -1, &tmp, 0, NULL, NULL); + WLog_LVL(TAG, g_LogLevel, " szLookupName=%s", tmp); + free(tmp); + smartcard_log_context(TAG, &call->Common.handles.hContext); + WLog_DBG( + TAG, "..CardIdentifier=%s", + smartcard_array_dump(call->Common.CardIdentifier, sizeof(UUID), buffer, sizeof(buffer))); + WLog_LVL(TAG, g_LogLevel, " FreshnessCounter=%" PRIu32, call->Common.FreshnessCounter); + WLog_LVL(TAG, g_LogLevel, " cbDataLen=%" PRIu32, call->Common.cbDataLen); + WLog_DBG( + TAG, " pbData=%s", + smartcard_array_dump(call->Common.pbData, call->Common.cbDataLen, buffer, sizeof(buffer))); + WLog_LVL(TAG, g_LogLevel, "}"); +} + +static void smartcard_trace_read_cache_a_call(SMARTCARD_DEVICE* smartcard, + const ReadCacheA_Call* call) +{ + char buffer[1024]; + WINPR_UNUSED(smartcard); + if (!WLog_IsLevelActive(WLog_Get(TAG), g_LogLevel)) + return; + + WLog_LVL(TAG, g_LogLevel, "GetTransmitCount_Call {"); + + WLog_LVL(TAG, g_LogLevel, " szLookupName=%s", call->szLookupName); + smartcard_log_context(TAG, &call->Common.handles.hContext); + WLog_DBG( + TAG, "..CardIdentifier=%s", + smartcard_array_dump(call->Common.CardIdentifier, sizeof(UUID), buffer, sizeof(buffer))); + WLog_LVL(TAG, g_LogLevel, " FreshnessCounter=%" PRIu32, call->Common.FreshnessCounter); + WLog_LVL(TAG, g_LogLevel, " fPbDataIsNULL=%" PRId32, call->Common.fPbDataIsNULL); + WLog_LVL(TAG, g_LogLevel, " cbDataLen=%" PRIu32, call->Common.cbDataLen); + + WLog_LVL(TAG, g_LogLevel, "}"); +} + +static void smartcard_trace_read_cache_w_call(SMARTCARD_DEVICE* smartcard, + const ReadCacheW_Call* call) +{ + char* tmp = NULL; + char buffer[1024]; + WINPR_UNUSED(smartcard); + if (!WLog_IsLevelActive(WLog_Get(TAG), g_LogLevel)) + return; + + WLog_LVL(TAG, g_LogLevel, "GetTransmitCount_Call {"); + + ConvertFromUnicode(CP_UTF8, 0, call->szLookupName, -1, &tmp, 0, NULL, NULL); + WLog_LVL(TAG, g_LogLevel, " szLookupName=%s", tmp); + free(tmp); + smartcard_log_context(TAG, &call->Common.handles.hContext); + WLog_DBG( + TAG, "..CardIdentifier=%s", + smartcard_array_dump(call->Common.CardIdentifier, sizeof(UUID), buffer, sizeof(buffer))); + WLog_LVL(TAG, g_LogLevel, " FreshnessCounter=%" PRIu32, call->Common.FreshnessCounter); + WLog_LVL(TAG, g_LogLevel, " fPbDataIsNULL=%" PRId32, call->Common.fPbDataIsNULL); + WLog_LVL(TAG, g_LogLevel, " cbDataLen=%" PRIu32, call->Common.cbDataLen); + + WLog_LVL(TAG, g_LogLevel, "}"); +} + +static void smartcard_trace_transmit_call(SMARTCARD_DEVICE* smartcard, const Transmit_Call* call) +{ + UINT32 cbExtraBytes; + BYTE* pbExtraBytes; + WINPR_UNUSED(smartcard); + + if (!WLog_IsLevelActive(WLog_Get(TAG), g_LogLevel)) + return; + + WLog_LVL(TAG, g_LogLevel, "Transmit_Call {"); + smartcard_log_context(TAG, &call->handles.hContext); + smartcard_log_redir_handle(TAG, &call->handles.hCard); + + if (call->pioSendPci) + { + cbExtraBytes = (UINT32)(call->pioSendPci->cbPciLength - sizeof(SCARD_IO_REQUEST)); + pbExtraBytes = &((BYTE*)call->pioSendPci)[sizeof(SCARD_IO_REQUEST)]; + WLog_LVL(TAG, g_LogLevel, "pioSendPci: dwProtocol: %" PRIu32 " cbExtraBytes: %" PRIu32 "", + call->pioSendPci->dwProtocol, cbExtraBytes); + + if (cbExtraBytes) + { + char buffer[1024]; + WLog_LVL(TAG, g_LogLevel, "pbExtraBytes: %s", + smartcard_array_dump(pbExtraBytes, cbExtraBytes, buffer, sizeof(buffer))); + } + } + else + { + WLog_LVL(TAG, g_LogLevel, "pioSendPci: null"); + } + + WLog_LVL(TAG, g_LogLevel, "cbSendLength: %" PRIu32 "", call->cbSendLength); + + if (call->pbSendBuffer) + { + char buffer[1024]; + WLog_DBG( + TAG, "pbSendBuffer: %s", + smartcard_array_dump(call->pbSendBuffer, call->cbSendLength, buffer, sizeof(buffer))); + } + else + { + WLog_LVL(TAG, g_LogLevel, "pbSendBuffer: null"); + } + + if (call->pioRecvPci) + { + cbExtraBytes = (UINT32)(call->pioRecvPci->cbPciLength - sizeof(SCARD_IO_REQUEST)); + pbExtraBytes = &((BYTE*)call->pioRecvPci)[sizeof(SCARD_IO_REQUEST)]; + WLog_LVL(TAG, g_LogLevel, "pioRecvPci: dwProtocol: %" PRIu32 " cbExtraBytes: %" PRIu32 "", + call->pioRecvPci->dwProtocol, cbExtraBytes); + + if (cbExtraBytes) + { + char buffer[1024]; + WLog_LVL(TAG, g_LogLevel, "pbExtraBytes: %s", + smartcard_array_dump(pbExtraBytes, cbExtraBytes, buffer, sizeof(buffer))); + } + } + else + { + WLog_LVL(TAG, g_LogLevel, "pioRecvPci: null"); + } + + WLog_LVL(TAG, g_LogLevel, "fpbRecvBufferIsNULL: %" PRId32 " cbRecvLength: %" PRIu32 "", + call->fpbRecvBufferIsNULL, call->cbRecvLength); + WLog_LVL(TAG, g_LogLevel, "}"); +} + +static void smartcard_trace_locate_cards_by_atr_w_call(SMARTCARD_DEVICE* smartcard, + const LocateCardsByATRW_Call* call) +{ + UINT32 index; + char* szEventState; + char* szCurrentState; + + WINPR_UNUSED(smartcard); + + if (!WLog_IsLevelActive(WLog_Get(TAG), g_LogLevel)) + return; + + WLog_LVL(TAG, g_LogLevel, "LocateCardsByATRW_Call {"); + smartcard_log_context(TAG, &call->handles.hContext); + + for (index = 0; index < call->cReaders; index++) + { + char buffer[1024]; + char* tmp = NULL; + const LPSCARD_READERSTATEW readerState = + (const LPSCARD_READERSTATEW)&call->rgReaderStates[index]; + + ConvertFromUnicode(CP_UTF8, 0, readerState->szReader, -1, &tmp, 0, NULL, NULL); + WLog_LVL(TAG, g_LogLevel, "\t[%" PRIu32 "]: szReader: %s cbAtr: %" PRIu32 "", index, tmp, + readerState->cbAtr); + szCurrentState = SCardGetReaderStateString(readerState->dwCurrentState); + szEventState = SCardGetReaderStateString(readerState->dwEventState); + WLog_LVL(TAG, g_LogLevel, "\t[%" PRIu32 "]: dwCurrentState: %s (0x%08" PRIX32 ")", index, + szCurrentState, readerState->dwCurrentState); + WLog_LVL(TAG, g_LogLevel, "\t[%" PRIu32 "]: dwEventState: %s (0x%08" PRIX32 ")", index, + szEventState, readerState->dwEventState); + + WLog_DBG( + TAG, "\t[%" PRIu32 "]: cbAtr: %" PRIu32 " rgbAtr: %s", index, readerState->cbAtr, + smartcard_array_dump(readerState->rgbAtr, readerState->cbAtr, buffer, sizeof(buffer))); + + free(szCurrentState); + free(szEventState); + free(tmp); + } + + WLog_LVL(TAG, g_LogLevel, "}"); +} + +static void smartcard_trace_transmit_return(SMARTCARD_DEVICE* smartcard, const Transmit_Return* ret) +{ + UINT32 cbExtraBytes; + BYTE* pbExtraBytes; + WINPR_UNUSED(smartcard); + + if (!WLog_IsLevelActive(WLog_Get(TAG), g_LogLevel)) + return; + + WLog_LVL(TAG, g_LogLevel, "Transmit_Return {"); + WLog_LVL(TAG, g_LogLevel, " ReturnCode: %s (0x%08" PRIX32 ")", + SCardGetErrorString(ret->ReturnCode), ret->ReturnCode); + + if (ret->pioRecvPci) + { + cbExtraBytes = (UINT32)(ret->pioRecvPci->cbPciLength - sizeof(SCARD_IO_REQUEST)); + pbExtraBytes = &((BYTE*)ret->pioRecvPci)[sizeof(SCARD_IO_REQUEST)]; + WLog_LVL(TAG, g_LogLevel, " pioRecvPci: dwProtocol: %" PRIu32 " cbExtraBytes: %" PRIu32 "", + ret->pioRecvPci->dwProtocol, cbExtraBytes); + + if (cbExtraBytes) + { + char buffer[1024]; + WLog_LVL(TAG, g_LogLevel, " pbExtraBytes: %s", + smartcard_array_dump(pbExtraBytes, cbExtraBytes, buffer, sizeof(buffer))); + } + } + else + { + WLog_LVL(TAG, g_LogLevel, " pioRecvPci: null"); + } + + WLog_LVL(TAG, g_LogLevel, " cbRecvLength: %" PRIu32 "", ret->cbRecvLength); + + if (ret->pbRecvBuffer) + { + char buffer[1024]; + WLog_DBG( + TAG, " pbRecvBuffer: %s", + smartcard_array_dump(ret->pbRecvBuffer, ret->cbRecvLength, buffer, sizeof(buffer))); + } + else + { + WLog_LVL(TAG, g_LogLevel, " pbRecvBuffer: null"); + } + + WLog_LVL(TAG, g_LogLevel, "}"); +} + +static void smartcard_trace_control_return(SMARTCARD_DEVICE* smartcard, const Control_Return* ret) +{ + WINPR_UNUSED(smartcard); + + if (!WLog_IsLevelActive(WLog_Get(TAG), g_LogLevel)) + return; + + WLog_LVL(TAG, g_LogLevel, "Control_Return {"); + WLog_LVL(TAG, g_LogLevel, " ReturnCode: %s (0x%08" PRIX32 ")", + SCardGetErrorString(ret->ReturnCode), ret->ReturnCode); + WLog_LVL(TAG, g_LogLevel, " cbOutBufferSize: %" PRIu32 "", ret->cbOutBufferSize); + + if (ret->pvOutBuffer) + { + char buffer[1024]; + WLog_DBG( + TAG, "pvOutBuffer: %s", + smartcard_array_dump(ret->pvOutBuffer, ret->cbOutBufferSize, buffer, sizeof(buffer))); + } + else + { + WLog_LVL(TAG, g_LogLevel, "pvOutBuffer: null"); + } + + WLog_LVL(TAG, g_LogLevel, "}"); +} + +static void smartcard_trace_control_call(SMARTCARD_DEVICE* smartcard, const Control_Call* call) +{ + WINPR_UNUSED(smartcard); + + if (!WLog_IsLevelActive(WLog_Get(TAG), g_LogLevel)) + return; + + WLog_LVL(TAG, g_LogLevel, "Control_Call {"); + smartcard_log_context(TAG, &call->handles.hContext); + smartcard_log_redir_handle(TAG, &call->handles.hCard); + + WLog_LVL(TAG, g_LogLevel, + "dwControlCode: 0x%08" PRIX32 " cbInBufferSize: %" PRIu32 + " fpvOutBufferIsNULL: %" PRId32 " cbOutBufferSize: %" PRIu32 "", + call->dwControlCode, call->cbInBufferSize, call->fpvOutBufferIsNULL, + call->cbOutBufferSize); + + if (call->pvInBuffer) + { + char buffer[1024]; + WLog_DBG( + TAG, "pbInBuffer: %s", + smartcard_array_dump(call->pvInBuffer, call->cbInBufferSize, buffer, sizeof(buffer))); + } + else + { + WLog_LVL(TAG, g_LogLevel, "pvInBuffer: null"); + } + + WLog_LVL(TAG, g_LogLevel, "}"); +} + +static void smartcard_trace_set_attrib_call(SMARTCARD_DEVICE* smartcard, const SetAttrib_Call* call) +{ + char buffer[8192]; + WINPR_UNUSED(smartcard); + + if (!WLog_IsLevelActive(WLog_Get(TAG), g_LogLevel)) + return; + + WLog_LVL(TAG, g_LogLevel, "GetAttrib_Call {"); + smartcard_log_context(TAG, &call->handles.hContext); + smartcard_log_redir_handle(TAG, &call->handles.hCard); + WLog_LVL(TAG, g_LogLevel, "dwAttrId: 0x%08" PRIX32, call->dwAttrId); + WLog_LVL(TAG, g_LogLevel, "cbAttrLen: 0x%08" PRId32, call->cbAttrLen); + WLog_LVL(TAG, g_LogLevel, "pbAttr: %s", + smartcard_array_dump(call->pbAttr, call->cbAttrLen, buffer, sizeof(buffer))); + WLog_LVL(TAG, g_LogLevel, "}"); +} + +static void smartcard_trace_get_attrib_return(SMARTCARD_DEVICE* smartcard, + const GetAttrib_Return* ret, DWORD dwAttrId) +{ + WINPR_UNUSED(smartcard); + + if (!WLog_IsLevelActive(WLog_Get(TAG), g_LogLevel)) + return; + + WLog_LVL(TAG, g_LogLevel, "GetAttrib_Return {"); + WLog_LVL(TAG, g_LogLevel, " ReturnCode: %s (0x%08" PRIX32 ")", + SCardGetErrorString(ret->ReturnCode), ret->ReturnCode); + WLog_LVL(TAG, g_LogLevel, " dwAttrId: %s (0x%08" PRIX32 ") cbAttrLen: 0x%08" PRIX32 "", + SCardGetAttributeString(dwAttrId), dwAttrId, ret->cbAttrLen); + + if (dwAttrId == SCARD_ATTR_VENDOR_NAME) + { + WLog_LVL(TAG, g_LogLevel, " pbAttr: %.*s", ret->cbAttrLen, (char*)ret->pbAttr); + } + else if (dwAttrId == SCARD_ATTR_CURRENT_PROTOCOL_TYPE) + { + union + { + BYTE* pb; + DWORD* pd; + } attr; + attr.pb = ret->pbAttr; + WLog_LVL(TAG, g_LogLevel, " dwProtocolType: %s (0x%08" PRIX32 ")", + SCardGetProtocolString(*attr.pd), *attr.pd); + } + + WLog_LVL(TAG, g_LogLevel, "}"); +} + +static void smartcard_trace_get_attrib_call(SMARTCARD_DEVICE* smartcard, const GetAttrib_Call* call) +{ + WINPR_UNUSED(smartcard); + + if (!WLog_IsLevelActive(WLog_Get(TAG), g_LogLevel)) + return; + + WLog_LVL(TAG, g_LogLevel, "GetAttrib_Call {"); + smartcard_log_context(TAG, &call->handles.hContext); + smartcard_log_redir_handle(TAG, &call->handles.hCard); + + WLog_LVL(TAG, g_LogLevel, + "dwAttrId: %s (0x%08" PRIX32 ") fpbAttrIsNULL: %" PRId32 " cbAttrLen: 0x%08" PRIX32 "", + SCardGetAttributeString(call->dwAttrId), call->dwAttrId, call->fpbAttrIsNULL, + call->cbAttrLen); + WLog_LVL(TAG, g_LogLevel, "}"); +} + +static void smartcard_trace_status_call(SMARTCARD_DEVICE* smartcard, const Status_Call* call, + BOOL unicode) +{ + WINPR_UNUSED(smartcard); + + if (!WLog_IsLevelActive(WLog_Get(TAG), g_LogLevel)) + return; + + WLog_LVL(TAG, g_LogLevel, "Status%s_Call {", unicode ? "W" : "A"); + smartcard_log_context(TAG, &call->handles.hContext); + smartcard_log_redir_handle(TAG, &call->handles.hCard); + + WLog_LVL(TAG, g_LogLevel, + "fmszReaderNamesIsNULL: %" PRId32 " cchReaderLen: %" PRIu32 " cbAtrLen: %" PRIu32 "", + call->fmszReaderNamesIsNULL, call->cchReaderLen, call->cbAtrLen); + WLog_LVL(TAG, g_LogLevel, "}"); +} + +static void smartcard_trace_status_return(SMARTCARD_DEVICE* smartcard, const Status_Return* ret, + BOOL unicode) +{ + char* mszReaderNamesA = NULL; + char buffer[1024]; + DWORD cBytes; + + WINPR_UNUSED(smartcard); + + if (!WLog_IsLevelActive(WLog_Get(TAG), g_LogLevel)) + return; + cBytes = ret->cBytes; + if (ret->ReturnCode != SCARD_S_SUCCESS) + cBytes = 0; + if (cBytes == SCARD_AUTOALLOCATE) + cBytes = 0; + mszReaderNamesA = smartcard_convert_string_list(ret->mszReaderNames, cBytes, unicode); + + WLog_LVL(TAG, g_LogLevel, "Status%s_Return {", unicode ? "W" : "A"); + WLog_LVL(TAG, g_LogLevel, " ReturnCode: %s (0x%08" PRIX32 ")", + SCardGetErrorString(ret->ReturnCode), ret->ReturnCode); + WLog_LVL(TAG, g_LogLevel, " dwState: %s (0x%08" PRIX32 ") dwProtocol: %s (0x%08" PRIX32 ")", + SCardGetCardStateString(ret->dwState), ret->dwState, + SCardGetProtocolString(ret->dwProtocol), ret->dwProtocol); + + WLog_LVL(TAG, g_LogLevel, " cBytes: %" PRIu32 " mszReaderNames: %s", ret->cBytes, + mszReaderNamesA); + + WLog_LVL(TAG, g_LogLevel, " cbAtrLen: %" PRIu32 " pbAtr: %s", ret->cbAtrLen, + smartcard_array_dump(ret->pbAtr, ret->cbAtrLen, buffer, sizeof(buffer))); + WLog_LVL(TAG, g_LogLevel, "}"); + free(mszReaderNamesA); +} + +static void smartcard_trace_state_return(SMARTCARD_DEVICE* smartcard, const State_Return* ret) +{ + char buffer[1024]; + char* state; + WINPR_UNUSED(smartcard); + + if (!WLog_IsLevelActive(WLog_Get(TAG), g_LogLevel)) + return; + + state = SCardGetReaderStateString(ret->dwState); + WLog_LVL(TAG, g_LogLevel, "Reconnect_Return {"); + WLog_LVL(TAG, g_LogLevel, " ReturnCode: %s (0x%08" PRIX32 ")", + SCardGetErrorString(ret->ReturnCode), ret->ReturnCode); + WLog_LVL(TAG, g_LogLevel, " dwState: %s (0x%08" PRIX32 ")", state, ret->dwState); + WLog_LVL(TAG, g_LogLevel, " dwProtocol: %s (0x%08" PRIX32 ")", + SCardGetProtocolString(ret->dwProtocol), ret->dwProtocol); + WLog_LVL(TAG, g_LogLevel, " cbAtrLen: (0x%08" PRIX32 ")", ret->cbAtrLen); + WLog_LVL(TAG, g_LogLevel, " rgAtr: %s", + smartcard_array_dump(ret->rgAtr, sizeof(ret->rgAtr), buffer, sizeof(buffer))); + WLog_LVL(TAG, g_LogLevel, "}"); + free(state); +} + +static void smartcard_trace_reconnect_return(SMARTCARD_DEVICE* smartcard, + const Reconnect_Return* ret) +{ + WINPR_UNUSED(smartcard); + + if (!WLog_IsLevelActive(WLog_Get(TAG), g_LogLevel)) + return; + + WLog_LVL(TAG, g_LogLevel, "Reconnect_Return {"); + WLog_LVL(TAG, g_LogLevel, " ReturnCode: %s (0x%08" PRIX32 ")", + SCardGetErrorString(ret->ReturnCode), ret->ReturnCode); + WLog_LVL(TAG, g_LogLevel, " dwActiveProtocol: %s (0x%08" PRIX32 ")", + SCardGetProtocolString(ret->dwActiveProtocol), ret->dwActiveProtocol); + WLog_LVL(TAG, g_LogLevel, "}"); +} + +static void smartcard_trace_connect_a_call(SMARTCARD_DEVICE* smartcard, const ConnectA_Call* call) +{ + WINPR_UNUSED(smartcard); + + if (!WLog_IsLevelActive(WLog_Get(TAG), g_LogLevel)) + return; + + WLog_LVL(TAG, g_LogLevel, "ConnectA_Call {"); + smartcard_log_context(TAG, &call->Common.handles.hContext); + + WLog_LVL(TAG, g_LogLevel, + "szReader: %s dwShareMode: %s (0x%08" PRIX32 ") dwPreferredProtocols: %s (0x%08" PRIX32 + ")", + call->szReader, SCardGetShareModeString(call->Common.dwShareMode), + call->Common.dwShareMode, SCardGetProtocolString(call->Common.dwPreferredProtocols), + call->Common.dwPreferredProtocols); + WLog_LVL(TAG, g_LogLevel, "}"); +} + +static void smartcard_trace_connect_w_call(SMARTCARD_DEVICE* smartcard, const ConnectW_Call* call) +{ + char* szReaderA = NULL; + WINPR_UNUSED(smartcard); + + if (!WLog_IsLevelActive(WLog_Get(TAG), g_LogLevel)) + return; + + ConvertFromUnicode(CP_UTF8, 0, call->szReader, -1, &szReaderA, 0, NULL, NULL); + WLog_LVL(TAG, g_LogLevel, "ConnectW_Call {"); + smartcard_log_context(TAG, &call->Common.handles.hContext); + + WLog_LVL(TAG, g_LogLevel, + "szReader: %s dwShareMode: %s (0x%08" PRIX32 ") dwPreferredProtocols: %s (0x%08" PRIX32 + ")", + szReaderA, SCardGetShareModeString(call->Common.dwShareMode), call->Common.dwShareMode, + SCardGetProtocolString(call->Common.dwPreferredProtocols), + call->Common.dwPreferredProtocols); + WLog_LVL(TAG, g_LogLevel, "}"); + free(szReaderA); +} + +static void smartcard_trace_hcard_and_disposition_call(SMARTCARD_DEVICE* smartcard, + const HCardAndDisposition_Call* call, + const char* name) +{ + WINPR_UNUSED(smartcard); + + if (!WLog_IsLevelActive(WLog_Get(TAG), g_LogLevel)) + return; + + WLog_LVL(TAG, g_LogLevel, "%s_Call {", name); + smartcard_log_context(TAG, &call->handles.hContext); + smartcard_log_redir_handle(TAG, &call->handles.hCard); + + WLog_LVL(TAG, g_LogLevel, "dwDisposition: %s (0x%08" PRIX32 ")", + SCardGetDispositionString(call->dwDisposition), call->dwDisposition); + WLog_LVL(TAG, g_LogLevel, "}"); +} + +static void smartcard_trace_establish_context_call(SMARTCARD_DEVICE* smartcard, + const EstablishContext_Call* call) +{ + WINPR_UNUSED(smartcard); + + if (!WLog_IsLevelActive(WLog_Get(TAG), g_LogLevel)) + return; + + WLog_LVL(TAG, g_LogLevel, "EstablishContext_Call {"); + WLog_LVL(TAG, g_LogLevel, "dwScope: %s (0x%08" PRIX32 ")", SCardGetScopeString(call->dwScope), + call->dwScope); + WLog_LVL(TAG, g_LogLevel, "}"); +} + +static void smartcard_trace_establish_context_return(SMARTCARD_DEVICE* smartcard, + const EstablishContext_Return* ret) +{ + WINPR_UNUSED(smartcard); + + if (!WLog_IsLevelActive(WLog_Get(TAG), g_LogLevel)) + return; + + WLog_LVL(TAG, g_LogLevel, "EstablishContext_Return {"); + WLog_LVL(TAG, g_LogLevel, " ReturnCode: %s (0x%08" PRIX32 ")", + SCardGetErrorString(ret->ReturnCode), ret->ReturnCode); + smartcard_log_context(TAG, &ret->hContext); + + WLog_LVL(TAG, g_LogLevel, "}"); +} + +void smartcard_trace_long_return(SMARTCARD_DEVICE* smartcard, const Long_Return* ret, + const char* name) +{ + WINPR_UNUSED(smartcard); + + if (!WLog_IsLevelActive(WLog_Get(TAG), g_LogLevel)) + return; + + WLog_LVL(TAG, g_LogLevel, "%s_Return {", name); + WLog_LVL(TAG, g_LogLevel, " ReturnCode: %s (0x%08" PRIX32 ")", + SCardGetErrorString(ret->ReturnCode), ret->ReturnCode); + WLog_LVL(TAG, g_LogLevel, "}"); +} + +static void smartcard_trace_connect_return(SMARTCARD_DEVICE* smartcard, const Connect_Return* ret) +{ + WINPR_UNUSED(smartcard); + + if (!WLog_IsLevelActive(WLog_Get(TAG), g_LogLevel)) + return; + + WLog_LVL(TAG, g_LogLevel, "Connect_Return {"); + WLog_LVL(TAG, g_LogLevel, " ReturnCode: %s (0x%08" PRIX32 ")", + SCardGetErrorString(ret->ReturnCode), ret->ReturnCode); + smartcard_log_context(TAG, &ret->hContext); + smartcard_log_redir_handle(TAG, &ret->hCard); + + WLog_LVL(TAG, g_LogLevel, " dwActiveProtocol: %s (0x%08" PRIX32 ")", + SCardGetProtocolString(ret->dwActiveProtocol), ret->dwActiveProtocol); + WLog_LVL(TAG, g_LogLevel, "}"); +} + +void smartcard_trace_reconnect_call(SMARTCARD_DEVICE* smartcard, const Reconnect_Call* call) +{ + WINPR_UNUSED(smartcard); + + if (!WLog_IsLevelActive(WLog_Get(TAG), g_LogLevel)) + return; + + WLog_LVL(TAG, g_LogLevel, "Reconnect_Call {"); + smartcard_log_context(TAG, &call->handles.hContext); + smartcard_log_redir_handle(TAG, &call->handles.hCard); + + WLog_LVL(TAG, g_LogLevel, + "dwShareMode: %s (0x%08" PRIX32 ") dwPreferredProtocols: %s (0x%08" PRIX32 + ") dwInitialization: %s (0x%08" PRIX32 ")", + SCardGetShareModeString(call->dwShareMode), call->dwShareMode, + SCardGetProtocolString(call->dwPreferredProtocols), call->dwPreferredProtocols, + SCardGetDispositionString(call->dwInitialization), call->dwInitialization); + WLog_LVL(TAG, g_LogLevel, "}"); +} + +static void smartcard_trace_device_type_id_return(SMARTCARD_DEVICE* smartcard, + const GetDeviceTypeId_Return* ret) +{ + WINPR_UNUSED(smartcard); + + if (!WLog_IsLevelActive(WLog_Get(TAG), g_LogLevel)) + return; + + WLog_LVL(TAG, g_LogLevel, "GetDeviceTypeId_Return {"); + WLog_LVL(TAG, g_LogLevel, " ReturnCode: %s (0x%08" PRIX32 ")", + SCardGetErrorString(ret->ReturnCode), ret->ReturnCode); + WLog_LVL(TAG, g_LogLevel, " dwDeviceId=%08" PRIx32, ret->dwDeviceId); + + WLog_LVL(TAG, g_LogLevel, "}"); +} + +static LONG smartcard_unpack_common_context_and_string_a(SMARTCARD_DEVICE* smartcard, wStream* s, + REDIR_SCARDCONTEXT* phContext, + CHAR** pszReaderName) +{ + LONG status; + UINT32 index = 0; + status = smartcard_unpack_redir_scard_context(smartcard, s, phContext, &index); + if (status != SCARD_S_SUCCESS) + return status; + + if (!smartcard_ndr_pointer_read(s, &index, NULL)) + return ERROR_INVALID_DATA; + + status = smartcard_unpack_redir_scard_context_ref(smartcard, s, phContext); + if (status != SCARD_S_SUCCESS) + return status; + + status = smartcard_ndr_read_a(s, pszReaderName, NDR_PTR_FULL); + if (status != SCARD_S_SUCCESS) + return status; + + smartcard_trace_context_and_string_call_a(__FUNCTION__, phContext, *pszReaderName); + return SCARD_S_SUCCESS; +} + +static LONG smartcard_unpack_common_context_and_string_w(SMARTCARD_DEVICE* smartcard, wStream* s, + REDIR_SCARDCONTEXT* phContext, + WCHAR** pszReaderName) +{ + LONG status; + UINT32 index = 0; + + status = smartcard_unpack_redir_scard_context(smartcard, s, phContext, &index); + if (status != SCARD_S_SUCCESS) + return status; + + if (!smartcard_ndr_pointer_read(s, &index, NULL)) + return ERROR_INVALID_DATA; + + status = smartcard_unpack_redir_scard_context_ref(smartcard, s, phContext); + if (status != SCARD_S_SUCCESS) + return status; + + status = smartcard_ndr_read_w(s, pszReaderName, NDR_PTR_FULL); + if (status != SCARD_S_SUCCESS) + return status; + + smartcard_trace_context_and_string_call_w(__FUNCTION__, phContext, *pszReaderName); + return SCARD_S_SUCCESS; +} + +LONG smartcard_unpack_common_type_header(SMARTCARD_DEVICE* smartcard, wStream* s) +{ + UINT8 version; + UINT32 filler; + UINT8 endianness; + UINT16 commonHeaderLength; + WINPR_UNUSED(smartcard); + + if (Stream_GetRemainingLength(s) < 8) + { + WLog_WARN(TAG, "CommonTypeHeader is too short: %" PRIuz "", Stream_GetRemainingLength(s)); + return STATUS_BUFFER_TOO_SMALL; + } + + /* Process CommonTypeHeader */ + Stream_Read_UINT8(s, version); /* Version (1 byte) */ + Stream_Read_UINT8(s, endianness); /* Endianness (1 byte) */ + Stream_Read_UINT16(s, commonHeaderLength); /* CommonHeaderLength (2 bytes) */ + Stream_Read_UINT32(s, filler); /* Filler (4 bytes), should be 0xCCCCCCCC */ + + if (version != 1) + { + WLog_WARN(TAG, "Unsupported CommonTypeHeader Version %" PRIu8 "", version); + return STATUS_INVALID_PARAMETER; + } + + if (endianness != 0x10) + { + WLog_WARN(TAG, "Unsupported CommonTypeHeader Endianness %" PRIu8 "", endianness); + return STATUS_INVALID_PARAMETER; + } + + if (commonHeaderLength != 8) + { + WLog_WARN(TAG, "Unsupported CommonTypeHeader CommonHeaderLength %" PRIu16 "", + commonHeaderLength); + return STATUS_INVALID_PARAMETER; + } + + if (filler != 0xCCCCCCCC) + { + WLog_WARN(TAG, "Unexpected CommonTypeHeader Filler 0x%08" PRIX32 "", filler); + return STATUS_INVALID_PARAMETER; + } + + return SCARD_S_SUCCESS; +} + +void smartcard_pack_common_type_header(SMARTCARD_DEVICE* smartcard, wStream* s) +{ + WINPR_UNUSED(smartcard); + Stream_Write_UINT8(s, 1); /* Version (1 byte) */ + Stream_Write_UINT8(s, 0x10); /* Endianness (1 byte) */ + Stream_Write_UINT16(s, 8); /* CommonHeaderLength (2 bytes) */ + Stream_Write_UINT32(s, 0xCCCCCCCC); /* Filler (4 bytes), should be 0xCCCCCCCC */ +} + +LONG smartcard_unpack_private_type_header(SMARTCARD_DEVICE* smartcard, wStream* s) +{ + UINT32 filler; + UINT32 objectBufferLength; + WINPR_UNUSED(smartcard); + + if (Stream_GetRemainingLength(s) < 8) + { + WLog_WARN(TAG, "PrivateTypeHeader is too short: %" PRIuz "", Stream_GetRemainingLength(s)); + return STATUS_BUFFER_TOO_SMALL; + } + + Stream_Read_UINT32(s, objectBufferLength); /* ObjectBufferLength (4 bytes) */ + Stream_Read_UINT32(s, filler); /* Filler (4 bytes), should be 0x00000000 */ + + if (filler != 0x00000000) + { + WLog_WARN(TAG, "Unexpected PrivateTypeHeader Filler 0x%08" PRIX32 "", filler); + return STATUS_INVALID_PARAMETER; + } + + if (objectBufferLength != Stream_GetRemainingLength(s)) + { + WLog_WARN(TAG, + "PrivateTypeHeader ObjectBufferLength mismatch: Actual: %" PRIu32 + ", Expected: %" PRIuz "", + objectBufferLength, Stream_GetRemainingLength(s)); + return STATUS_INVALID_PARAMETER; + } + + return SCARD_S_SUCCESS; +} + +void smartcard_pack_private_type_header(SMARTCARD_DEVICE* smartcard, wStream* s, + UINT32 objectBufferLength) +{ + WINPR_UNUSED(smartcard); + Stream_Write_UINT32(s, objectBufferLength); /* ObjectBufferLength (4 bytes) */ + Stream_Write_UINT32(s, 0x00000000); /* Filler (4 bytes), should be 0x00000000 */ +} + +LONG smartcard_unpack_read_size_align(SMARTCARD_DEVICE* smartcard, wStream* s, size_t size, + UINT32 alignment) +{ + size_t pad; + WINPR_UNUSED(smartcard); + pad = size; + size = (size + alignment - 1) & ~(alignment - 1); + pad = size - pad; + + if (pad) + Stream_Seek(s, pad); + + return (LONG)pad; +} + +LONG smartcard_pack_write_size_align(SMARTCARD_DEVICE* smartcard, wStream* s, size_t size, + UINT32 alignment) +{ + size_t pad; + WINPR_UNUSED(smartcard); + pad = size; + size = (size + alignment - 1) & ~(alignment - 1); + pad = size - pad; + + if (pad) + { + if (!Stream_EnsureRemainingCapacity(s, pad)) + { + WLog_ERR(TAG, "Stream_EnsureRemainingCapacity failed!"); + return SCARD_F_INTERNAL_ERROR; + } + + Stream_Zero(s, pad); + } + + return SCARD_S_SUCCESS; +} + +SCARDCONTEXT smartcard_scard_context_native_from_redir(SMARTCARD_DEVICE* smartcard, + REDIR_SCARDCONTEXT* context) +{ + SCARDCONTEXT hContext = { 0 }; + WINPR_UNUSED(smartcard); + + if ((context->cbContext != sizeof(ULONG_PTR)) && (context->cbContext != 0)) + { + WLog_WARN(TAG, + "REDIR_SCARDCONTEXT does not match native size: Actual: %" PRIu32 + ", Expected: %" PRIuz "", + context->cbContext, sizeof(ULONG_PTR)); + return 0; + } + + if (context->cbContext) + CopyMemory(&hContext, &(context->pbContext), context->cbContext); + + return hContext; +} + +void smartcard_scard_context_native_to_redir(SMARTCARD_DEVICE* smartcard, + REDIR_SCARDCONTEXT* context, SCARDCONTEXT hContext) +{ + WINPR_UNUSED(smartcard); + ZeroMemory(context, sizeof(REDIR_SCARDCONTEXT)); + context->cbContext = sizeof(ULONG_PTR); + CopyMemory(&(context->pbContext), &hContext, context->cbContext); +} + +SCARDHANDLE smartcard_scard_handle_native_from_redir(SMARTCARD_DEVICE* smartcard, + REDIR_SCARDHANDLE* handle) +{ + SCARDHANDLE hCard = 0; + WINPR_UNUSED(smartcard); + + if (handle->cbHandle == 0) + return hCard; + + if (handle->cbHandle != sizeof(ULONG_PTR)) + { + WLog_WARN(TAG, + "REDIR_SCARDHANDLE does not match native size: Actual: %" PRIu32 + ", Expected: %" PRIuz "", + handle->cbHandle, sizeof(ULONG_PTR)); + return 0; + } + + if (handle->cbHandle) + CopyMemory(&hCard, &(handle->pbHandle), handle->cbHandle); + + return hCard; +} + +void smartcard_scard_handle_native_to_redir(SMARTCARD_DEVICE* smartcard, REDIR_SCARDHANDLE* handle, + SCARDHANDLE hCard) +{ + WINPR_UNUSED(smartcard); + ZeroMemory(handle, sizeof(REDIR_SCARDHANDLE)); + handle->cbHandle = sizeof(ULONG_PTR); + CopyMemory(&(handle->pbHandle), &hCard, handle->cbHandle); +} + +LONG smartcard_unpack_redir_scard_context_(SMARTCARD_DEVICE* smartcard, wStream* s, + REDIR_SCARDCONTEXT* context, UINT32* index, + const char* file, const char* function, int line) +{ + UINT32 pbContextNdrPtr; + WINPR_UNUSED(smartcard); + WINPR_UNUSED(file); + + ZeroMemory(context, sizeof(REDIR_SCARDCONTEXT)); + + if (Stream_GetRemainingLength(s) < 4) + { + WLog_WARN(TAG, "REDIR_SCARDCONTEXT is too short: %" PRIuz "", Stream_GetRemainingLength(s)); + return STATUS_BUFFER_TOO_SMALL; + } + + Stream_Read_UINT32(s, context->cbContext); /* cbContext (4 bytes) */ + + if (Stream_GetRemainingLength(s) < context->cbContext) + { + WLog_WARN(TAG, "REDIR_SCARDCONTEXT is too short: Actual: %" PRIuz ", Expected: %" PRIu32 "", + Stream_GetRemainingLength(s), context->cbContext); + return STATUS_BUFFER_TOO_SMALL; + } + + if ((context->cbContext != 0) && (context->cbContext != 4) && (context->cbContext != 8)) + { + WLog_WARN(TAG, "REDIR_SCARDCONTEXT length is not 0, 4 or 8: %" PRIu32 "", + context->cbContext); + return STATUS_INVALID_PARAMETER; + } + + if (!smartcard_ndr_pointer_read_(s, index, &pbContextNdrPtr, file, function, line)) + return ERROR_INVALID_DATA; + + if (((context->cbContext == 0) && pbContextNdrPtr) || + ((context->cbContext != 0) && !pbContextNdrPtr)) + { + WLog_WARN(TAG, + "REDIR_SCARDCONTEXT cbContext (%" PRIu32 ") pbContextNdrPtr (%" PRIu32 + ") inconsistency", + context->cbContext, pbContextNdrPtr); + return STATUS_INVALID_PARAMETER; + } + + if (context->cbContext > Stream_GetRemainingLength(s)) + { + WLog_WARN(TAG, "REDIR_SCARDCONTEXT is too long: Actual: %" PRIuz ", Expected: %" PRIu32 "", + Stream_GetRemainingLength(s), context->cbContext); + return STATUS_INVALID_PARAMETER; + } + + return SCARD_S_SUCCESS; +} + +LONG smartcard_pack_redir_scard_context(SMARTCARD_DEVICE* smartcard, wStream* s, + const REDIR_SCARDCONTEXT* context, DWORD* index) +{ + const UINT32 pbContextNdrPtr = 0x00020000 + *index * 4; + WINPR_UNUSED(smartcard); + + if (context->cbContext != 0) + { + Stream_Write_UINT32(s, context->cbContext); /* cbContext (4 bytes) */ + Stream_Write_UINT32(s, pbContextNdrPtr); /* pbContextNdrPtr (4 bytes) */ + *index = *index + 1; + } + else + Stream_Zero(s, 8); + + return SCARD_S_SUCCESS; +} + +LONG smartcard_unpack_redir_scard_context_ref(SMARTCARD_DEVICE* smartcard, wStream* s, + REDIR_SCARDCONTEXT* context) +{ + UINT32 length; + WINPR_UNUSED(smartcard); + + if (context->cbContext == 0) + return SCARD_S_SUCCESS; + + if (Stream_GetRemainingLength(s) < 4) + { + WLog_WARN(TAG, "REDIR_SCARDCONTEXT is too short: Actual: %" PRIuz ", Expected: 4", + Stream_GetRemainingLength(s)); + return STATUS_BUFFER_TOO_SMALL; + } + + Stream_Read_UINT32(s, length); /* Length (4 bytes) */ + + if (length != context->cbContext) + { + WLog_WARN(TAG, "REDIR_SCARDCONTEXT length (%" PRIu32 ") cbContext (%" PRIu32 ") mismatch", + length, context->cbContext); + return STATUS_INVALID_PARAMETER; + } + + if ((context->cbContext != 0) && (context->cbContext != 4) && (context->cbContext != 8)) + { + WLog_WARN(TAG, "REDIR_SCARDCONTEXT length is not 4 or 8: %" PRIu32 "", context->cbContext); + return STATUS_INVALID_PARAMETER; + } + + if (Stream_GetRemainingLength(s) < context->cbContext) + { + WLog_WARN(TAG, "REDIR_SCARDCONTEXT is too short: Actual: %" PRIuz ", Expected: %" PRIu32 "", + Stream_GetRemainingLength(s), context->cbContext); + return STATUS_BUFFER_TOO_SMALL; + } + + if (context->cbContext) + Stream_Read(s, &(context->pbContext), context->cbContext); + else + ZeroMemory(&(context->pbContext), sizeof(context->pbContext)); + + return SCARD_S_SUCCESS; +} + +LONG smartcard_pack_redir_scard_context_ref(SMARTCARD_DEVICE* smartcard, wStream* s, + const REDIR_SCARDCONTEXT* context) +{ + WINPR_UNUSED(smartcard); + Stream_Write_UINT32(s, context->cbContext); /* Length (4 bytes) */ + + if (context->cbContext) + { + Stream_Write(s, &(context->pbContext), context->cbContext); + } + + return SCARD_S_SUCCESS; +} + +LONG smartcard_unpack_redir_scard_handle_(SMARTCARD_DEVICE* smartcard, wStream* s, + REDIR_SCARDHANDLE* handle, UINT32* index, + const char* file, const char* function, int line) +{ + WINPR_UNUSED(smartcard); + ZeroMemory(handle, sizeof(REDIR_SCARDHANDLE)); + + if (Stream_GetRemainingLength(s) < 4) + { + WLog_WARN(TAG, "SCARDHANDLE is too short: %" PRIuz "", Stream_GetRemainingLength(s)); + return STATUS_BUFFER_TOO_SMALL; + } + + Stream_Read_UINT32(s, handle->cbHandle); /* Length (4 bytes) */ + + if ((Stream_GetRemainingLength(s) < handle->cbHandle) || (!handle->cbHandle)) + { + WLog_WARN(TAG, "SCARDHANDLE is too short: Actual: %" PRIuz ", Expected: %" PRIu32 "", + Stream_GetRemainingLength(s), handle->cbHandle); + return STATUS_BUFFER_TOO_SMALL; + } + + if (!smartcard_ndr_pointer_read_(s, index, NULL, file, function, line)) + return ERROR_INVALID_DATA; + + return SCARD_S_SUCCESS; +} + +LONG smartcard_pack_redir_scard_handle(SMARTCARD_DEVICE* smartcard, wStream* s, + const REDIR_SCARDHANDLE* handle, DWORD* index) +{ + const UINT32 pbContextNdrPtr = 0x00020000 + *index * 4; + WINPR_UNUSED(smartcard); + + if (handle->cbHandle != 0) + { + Stream_Write_UINT32(s, handle->cbHandle); /* cbContext (4 bytes) */ + Stream_Write_UINT32(s, pbContextNdrPtr); /* pbContextNdrPtr (4 bytes) */ + *index = *index + 1; + } + else + Stream_Zero(s, 8); + return SCARD_S_SUCCESS; +} + +LONG smartcard_unpack_redir_scard_handle_ref(SMARTCARD_DEVICE* smartcard, wStream* s, + REDIR_SCARDHANDLE* handle) +{ + UINT32 length; + WINPR_UNUSED(smartcard); + + if (Stream_GetRemainingLength(s) < 4) + { + WLog_WARN(TAG, "REDIR_SCARDHANDLE is too short: Actual: %" PRIuz ", Expected: 4", + Stream_GetRemainingLength(s)); + return STATUS_BUFFER_TOO_SMALL; + } + + Stream_Read_UINT32(s, length); /* Length (4 bytes) */ + + if (length != handle->cbHandle) + { + WLog_WARN(TAG, "REDIR_SCARDHANDLE length (%" PRIu32 ") cbHandle (%" PRIu32 ") mismatch", + length, handle->cbHandle); + return STATUS_INVALID_PARAMETER; + } + + if ((handle->cbHandle != 4) && (handle->cbHandle != 8)) + { + WLog_WARN(TAG, "REDIR_SCARDHANDLE length is not 4 or 8: %" PRIu32 "", handle->cbHandle); + return STATUS_INVALID_PARAMETER; + } + + if ((Stream_GetRemainingLength(s) < handle->cbHandle) || (!handle->cbHandle)) + { + WLog_WARN(TAG, "REDIR_SCARDHANDLE is too short: Actual: %" PRIuz ", Expected: %" PRIu32 "", + Stream_GetRemainingLength(s), handle->cbHandle); + return STATUS_BUFFER_TOO_SMALL; + } + + if (handle->cbHandle) + Stream_Read(s, &(handle->pbHandle), handle->cbHandle); + + return SCARD_S_SUCCESS; +} + +LONG smartcard_pack_redir_scard_handle_ref(SMARTCARD_DEVICE* smartcard, wStream* s, + const REDIR_SCARDHANDLE* handle) +{ + WINPR_UNUSED(smartcard); + Stream_Write_UINT32(s, handle->cbHandle); /* Length (4 bytes) */ + + if (handle->cbHandle) + Stream_Write(s, &(handle->pbHandle), handle->cbHandle); + + return SCARD_S_SUCCESS; +} + +LONG smartcard_unpack_establish_context_call(SMARTCARD_DEVICE* smartcard, wStream* s, + EstablishContext_Call* call) +{ + WINPR_UNUSED(smartcard); + + if (Stream_GetRemainingLength(s) < 4) + { + WLog_WARN(TAG, "EstablishContext_Call is too short: Actual: %" PRIuz ", Expected: 4", + Stream_GetRemainingLength(s)); + return STATUS_BUFFER_TOO_SMALL; + } + + Stream_Read_UINT32(s, call->dwScope); /* dwScope (4 bytes) */ + smartcard_trace_establish_context_call(smartcard, call); + return SCARD_S_SUCCESS; +} + +LONG smartcard_pack_establish_context_return(SMARTCARD_DEVICE* smartcard, wStream* s, + const EstablishContext_Return* ret) +{ + LONG status; + DWORD index = 0; + + smartcard_trace_establish_context_return(smartcard, ret); + if (ret->ReturnCode != SCARD_S_SUCCESS) + return ret->ReturnCode; + + if ((status = smartcard_pack_redir_scard_context(smartcard, s, &(ret->hContext), &index))) + return status; + + return smartcard_pack_redir_scard_context_ref(smartcard, s, &(ret->hContext)); +} + +LONG smartcard_unpack_context_call(SMARTCARD_DEVICE* smartcard, wStream* s, Context_Call* call, + const char* name) +{ + LONG status; + UINT32 index = 0; + + status = smartcard_unpack_redir_scard_context(smartcard, s, &(call->handles.hContext), &index); + if (status != SCARD_S_SUCCESS) + return status; + + if ((status = + smartcard_unpack_redir_scard_context_ref(smartcard, s, &(call->handles.hContext)))) + WLog_ERR(TAG, "smartcard_unpack_redir_scard_context_ref failed with error %" PRId32 "", + status); + + smartcard_trace_context_call(smartcard, call, name); + return status; +} + +LONG smartcard_unpack_list_reader_groups_call(SMARTCARD_DEVICE* smartcard, wStream* s, + ListReaderGroups_Call* call, BOOL unicode) +{ + LONG status; + UINT32 index = 0; + status = smartcard_unpack_redir_scard_context(smartcard, s, &(call->handles.hContext), &index); + + if (status != SCARD_S_SUCCESS) + return status; + + if (Stream_GetRemainingLength(s) < 8) + { + WLog_WARN(TAG, "ListReaderGroups_Call is too short: %" PRIdz, Stream_GetRemainingLength(s)); + return STATUS_BUFFER_TOO_SMALL; + } + + Stream_Read_INT32(s, call->fmszGroupsIsNULL); /* fmszGroupsIsNULL (4 bytes) */ + Stream_Read_UINT32(s, call->cchGroups); /* cchGroups (4 bytes) */ + status = smartcard_unpack_redir_scard_context_ref(smartcard, s, &(call->handles.hContext)); + + if (status != SCARD_S_SUCCESS) + return status; + + smartcard_trace_list_reader_groups_call(smartcard, call, unicode); + return SCARD_S_SUCCESS; +} + +LONG smartcard_pack_list_reader_groups_return(SMARTCARD_DEVICE* smartcard, wStream* s, + const ListReaderGroups_Return* ret, BOOL unicode) +{ + LONG status; + DWORD cBytes = ret->cBytes; + DWORD index = 0; + + smartcard_trace_list_reader_groups_return(smartcard, ret, unicode); + if (ret->ReturnCode != SCARD_S_SUCCESS) + cBytes = 0; + if (cBytes == SCARD_AUTOALLOCATE) + cBytes = 0; + + if (!Stream_EnsureRemainingCapacity(s, 4)) + return SCARD_E_NO_MEMORY; + + Stream_Write_UINT32(s, cBytes); /* cBytes (4 bytes) */ + if (!smartcard_ndr_pointer_write(s, &index, cBytes)) + return SCARD_E_NO_MEMORY; + + status = smartcard_ndr_write(s, ret->msz, cBytes, 1, NDR_PTR_SIMPLE); + if (status != SCARD_S_SUCCESS) + return status; + return ret->ReturnCode; +} + +LONG smartcard_unpack_list_readers_call(SMARTCARD_DEVICE* smartcard, wStream* s, + ListReaders_Call* call, BOOL unicode) +{ + LONG status; + UINT32 index = 0; + UINT32 mszGroupsNdrPtr; + call->mszGroups = NULL; + + status = smartcard_unpack_redir_scard_context(smartcard, s, &(call->handles.hContext), &index); + if (status != SCARD_S_SUCCESS) + return status; + + if (Stream_GetRemainingLength(s) < 16) + { + WLog_WARN(TAG, "ListReaders_Call is too short: %" PRIuz "", Stream_GetRemainingLength(s)); + return STATUS_BUFFER_TOO_SMALL; + } + + Stream_Read_UINT32(s, call->cBytes); /* cBytes (4 bytes) */ + if (!smartcard_ndr_pointer_read(s, &index, &mszGroupsNdrPtr)) + return ERROR_INVALID_DATA; + Stream_Read_INT32(s, call->fmszReadersIsNULL); /* fmszReadersIsNULL (4 bytes) */ + Stream_Read_UINT32(s, call->cchReaders); /* cchReaders (4 bytes) */ + + if ((status = + smartcard_unpack_redir_scard_context_ref(smartcard, s, &(call->handles.hContext)))) + return status; + + if (mszGroupsNdrPtr) + { + status = smartcard_ndr_read(s, &call->mszGroups, call->cBytes, 1, NDR_PTR_SIMPLE); + if (status != SCARD_S_SUCCESS) + return status; + } + + smartcard_trace_list_readers_call(smartcard, call, unicode); + return SCARD_S_SUCCESS; +} + +LONG smartcard_pack_list_readers_return(SMARTCARD_DEVICE* smartcard, wStream* s, + const ListReaders_Return* ret, BOOL unicode) +{ + LONG status; + DWORD index = 0; + UINT32 size = unicode ? sizeof(WCHAR) : sizeof(CHAR); + + size *= ret->cBytes; + + smartcard_trace_list_readers_return(smartcard, ret, unicode); + if (ret->ReturnCode != SCARD_S_SUCCESS) + size = 0; + + if (!Stream_EnsureRemainingCapacity(s, 4)) + { + WLog_ERR(TAG, "Stream_EnsureRemainingCapacity failed!"); + return SCARD_F_INTERNAL_ERROR; + } + + Stream_Write_UINT32(s, size); /* cBytes (4 bytes) */ + if (!smartcard_ndr_pointer_write(s, &index, size)) + return SCARD_E_NO_MEMORY; + + status = smartcard_ndr_write(s, ret->msz, size, 1, NDR_PTR_SIMPLE); + if (status != SCARD_S_SUCCESS) + return status; + return ret->ReturnCode; +} + +static LONG smartcard_unpack_connect_common(SMARTCARD_DEVICE* smartcard, wStream* s, + Connect_Common_Call* common, UINT32* index) +{ + LONG status; + + status = smartcard_unpack_redir_scard_context(smartcard, s, &(common->handles.hContext), index); + if (status != SCARD_S_SUCCESS) + return status; + + if (Stream_GetRemainingLength(s) < 8) + { + WLog_WARN(TAG, "Connect_Common is too short: %" PRIuz "", Stream_GetRemainingLength(s)); + return STATUS_BUFFER_TOO_SMALL; + } + + Stream_Read_UINT32(s, common->dwShareMode); /* dwShareMode (4 bytes) */ + Stream_Read_UINT32(s, common->dwPreferredProtocols); /* dwPreferredProtocols (4 bytes) */ + return SCARD_S_SUCCESS; +} + +LONG smartcard_unpack_connect_a_call(SMARTCARD_DEVICE* smartcard, wStream* s, ConnectA_Call* call) +{ + LONG status; + UINT32 index = 0; + call->szReader = NULL; + + if (!smartcard_ndr_pointer_read(s, &index, NULL)) + return ERROR_INVALID_DATA; + + if ((status = smartcard_unpack_connect_common(smartcard, s, &(call->Common), &index))) + { + WLog_ERR(TAG, "smartcard_unpack_connect_common failed with error %" PRId32 "", status); + return status; + } + + status = smartcard_ndr_read_a(s, &call->szReader, NDR_PTR_FULL); + if (status != SCARD_S_SUCCESS) + return status; + + if ((status = smartcard_unpack_redir_scard_context_ref(smartcard, s, + &(call->Common.handles.hContext)))) + WLog_ERR(TAG, "smartcard_unpack_redir_scard_context_ref failed with error %" PRId32 "", + status); + + smartcard_trace_connect_a_call(smartcard, call); + return status; +} + +LONG smartcard_unpack_connect_w_call(SMARTCARD_DEVICE* smartcard, wStream* s, ConnectW_Call* call) +{ + LONG status; + UINT32 index = 0; + + call->szReader = NULL; + + if (!smartcard_ndr_pointer_read(s, &index, NULL)) + return ERROR_INVALID_DATA; + + if ((status = smartcard_unpack_connect_common(smartcard, s, &(call->Common), &index))) + { + WLog_ERR(TAG, "smartcard_unpack_connect_common failed with error %" PRId32 "", status); + return status; + } + + status = smartcard_ndr_read_w(s, &call->szReader, NDR_PTR_FULL); + if (status != SCARD_S_SUCCESS) + return status; + + if ((status = smartcard_unpack_redir_scard_context_ref(smartcard, s, + &(call->Common.handles.hContext)))) + WLog_ERR(TAG, "smartcard_unpack_redir_scard_context_ref failed with error %" PRId32 "", + status); + + smartcard_trace_connect_w_call(smartcard, call); + return status; +} + +LONG smartcard_pack_connect_return(SMARTCARD_DEVICE* smartcard, wStream* s, + const Connect_Return* ret) +{ + LONG status; + DWORD index = 0; + + smartcard_trace_connect_return(smartcard, ret); + + status = smartcard_pack_redir_scard_context(smartcard, s, &ret->hContext, &index); + if (status != SCARD_S_SUCCESS) + return status; + + status = smartcard_pack_redir_scard_handle(smartcard, s, &ret->hCard, &index); + if (status != SCARD_S_SUCCESS) + return status; + + if (!Stream_EnsureRemainingCapacity(s, 4)) + return SCARD_E_NO_MEMORY; + + Stream_Write_UINT32(s, ret->dwActiveProtocol); /* dwActiveProtocol (4 bytes) */ + status = smartcard_pack_redir_scard_context_ref(smartcard, s, &ret->hContext); + if (status != SCARD_S_SUCCESS) + return status; + return smartcard_pack_redir_scard_handle_ref(smartcard, s, &(ret->hCard)); +} + +LONG smartcard_unpack_reconnect_call(SMARTCARD_DEVICE* smartcard, wStream* s, Reconnect_Call* call) +{ + LONG status; + UINT32 index = 0; + + status = smartcard_unpack_redir_scard_context(smartcard, s, &(call->handles.hContext), &index); + if (status != SCARD_S_SUCCESS) + return status; + + status = smartcard_unpack_redir_scard_handle(smartcard, s, &(call->handles.hCard), &index); + if (status != SCARD_S_SUCCESS) + return status; + + if (Stream_GetRemainingLength(s) < 12) + { + WLog_WARN(TAG, "Reconnect_Call is too short: %" PRIuz "", Stream_GetRemainingLength(s)); + return STATUS_BUFFER_TOO_SMALL; + } + + Stream_Read_UINT32(s, call->dwShareMode); /* dwShareMode (4 bytes) */ + Stream_Read_UINT32(s, call->dwPreferredProtocols); /* dwPreferredProtocols (4 bytes) */ + Stream_Read_UINT32(s, call->dwInitialization); /* dwInitialization (4 bytes) */ + + if ((status = + smartcard_unpack_redir_scard_context_ref(smartcard, s, &(call->handles.hContext)))) + { + WLog_ERR(TAG, "smartcard_unpack_redir_scard_context_ref failed with error %" PRId32 "", + status); + return status; + } + + if ((status = smartcard_unpack_redir_scard_handle_ref(smartcard, s, &(call->handles.hCard)))) + WLog_ERR(TAG, "smartcard_unpack_redir_scard_handle_ref failed with error %" PRId32 "", + status); + + smartcard_trace_reconnect_call(smartcard, call); + return status; +} + +LONG smartcard_pack_reconnect_return(SMARTCARD_DEVICE* smartcard, wStream* s, + const Reconnect_Return* ret) +{ + smartcard_trace_reconnect_return(smartcard, ret); + + if (!Stream_EnsureRemainingCapacity(s, 4)) + return SCARD_E_NO_MEMORY; + Stream_Write_UINT32(s, ret->dwActiveProtocol); /* dwActiveProtocol (4 bytes) */ + return ret->ReturnCode; +} + +LONG smartcard_unpack_hcard_and_disposition_call(SMARTCARD_DEVICE* smartcard, wStream* s, + HCardAndDisposition_Call* call, const char* name) +{ + LONG status; + UINT32 index = 0; + + status = smartcard_unpack_redir_scard_context(smartcard, s, &(call->handles.hContext), &index); + if (status != SCARD_S_SUCCESS) + return status; + + status = smartcard_unpack_redir_scard_handle(smartcard, s, &(call->handles.hCard), &index); + if (status != SCARD_S_SUCCESS) + return status; + + if (Stream_GetRemainingLength(s) < 4) + { + WLog_WARN(TAG, "HCardAndDisposition_Call is too short: %" PRIuz "", + Stream_GetRemainingLength(s)); + return STATUS_BUFFER_TOO_SMALL; + } + + Stream_Read_UINT32(s, call->dwDisposition); /* dwDisposition (4 bytes) */ + + if ((status = + smartcard_unpack_redir_scard_context_ref(smartcard, s, &(call->handles.hContext)))) + return status; + + if ((status = smartcard_unpack_redir_scard_handle_ref(smartcard, s, &(call->handles.hCard)))) + return status; + + smartcard_trace_hcard_and_disposition_call(smartcard, call, name); + return status; +} + +static void smartcard_trace_get_status_change_a_call(SMARTCARD_DEVICE* smartcard, + const GetStatusChangeA_Call* call) +{ + UINT32 index; + char* szEventState; + char* szCurrentState; + LPSCARD_READERSTATEA readerState; + WINPR_UNUSED(smartcard); + + if (!WLog_IsLevelActive(WLog_Get(TAG), g_LogLevel)) + return; + + WLog_LVL(TAG, g_LogLevel, "GetStatusChangeA_Call {"); + smartcard_log_context(TAG, &call->handles.hContext); + + WLog_LVL(TAG, g_LogLevel, "dwTimeOut: 0x%08" PRIX32 " cReaders: %" PRIu32 "", call->dwTimeOut, + call->cReaders); + + for (index = 0; index < call->cReaders; index++) + { + readerState = &call->rgReaderStates[index]; + WLog_LVL(TAG, g_LogLevel, "\t[%" PRIu32 "]: szReader: %s cbAtr: %" PRIu32 "", index, + readerState->szReader, readerState->cbAtr); + szCurrentState = SCardGetReaderStateString(readerState->dwCurrentState); + szEventState = SCardGetReaderStateString(readerState->dwEventState); + WLog_LVL(TAG, g_LogLevel, "\t[%" PRIu32 "]: dwCurrentState: %s (0x%08" PRIX32 ")", index, + szCurrentState, readerState->dwCurrentState); + WLog_LVL(TAG, g_LogLevel, "\t[%" PRIu32 "]: dwEventState: %s (0x%08" PRIX32 ")", index, + szEventState, readerState->dwEventState); + free(szCurrentState); + free(szEventState); + } + + WLog_LVL(TAG, g_LogLevel, "}"); +} + +static LONG smartcard_unpack_reader_state_a(wStream* s, LPSCARD_READERSTATEA* ppcReaders, + UINT32 cReaders, UINT32* ptrIndex) +{ + UINT32 index, len; + LONG status = SCARD_E_NO_MEMORY; + LPSCARD_READERSTATEA rgReaderStates; + BOOL* states; + + if (Stream_GetRemainingLength(s) < 4) + return status; + + Stream_Read_UINT32(s, len); + if (len != cReaders) + { + WLog_ERR(TAG, "Count mismatch when reading LPSCARD_READERSTATEA"); + return status; + } + rgReaderStates = (LPSCARD_READERSTATEA)calloc(cReaders, sizeof(SCARD_READERSTATEA)); + states = calloc(cReaders, sizeof(BOOL)); + if (!rgReaderStates || !states) + goto fail; + status = ERROR_INVALID_DATA; + + for (index = 0; index < cReaders; index++) + { + UINT32 ptr = UINT32_MAX; + LPSCARD_READERSTATEA readerState = &rgReaderStates[index]; + + if (Stream_GetRemainingLength(s) < 52) + { + WLog_WARN(TAG, "GetStatusChangeA_Call is too short: %" PRIuz "", + Stream_GetRemainingLength(s)); + goto fail; + } + + if (!smartcard_ndr_pointer_read(s, ptrIndex, &ptr)) + { + if (ptr != 0) + goto fail; + } + /* Ignore NULL length strings */ + states[index] = ptr != 0; + Stream_Read_UINT32(s, readerState->dwCurrentState); /* dwCurrentState (4 bytes) */ + Stream_Read_UINT32(s, readerState->dwEventState); /* dwEventState (4 bytes) */ + Stream_Read_UINT32(s, readerState->cbAtr); /* cbAtr (4 bytes) */ + Stream_Read(s, readerState->rgbAtr, 36); /* rgbAtr [0..36] (36 bytes) */ + } + + for (index = 0; index < cReaders; index++) + { + LPSCARD_READERSTATEA readerState = &rgReaderStates[index]; + + /* Ignore empty strings */ + if (!states[index]) + continue; + status = smartcard_ndr_read_a(s, &readerState->szReader, NDR_PTR_FULL); + if (status != SCARD_S_SUCCESS) + goto fail; + } + + *ppcReaders = rgReaderStates; + free(states); + return SCARD_S_SUCCESS; +fail: + if (rgReaderStates) + { + for (index = 0; index < cReaders; index++) + { + LPSCARD_READERSTATEA readerState = &rgReaderStates[index]; + free(readerState->szReader); + } + } + free(rgReaderStates); + free(states); + return status; +} + +static LONG smartcard_unpack_reader_state_w(wStream* s, LPSCARD_READERSTATEW* ppcReaders, + UINT32 cReaders, UINT32* ptrIndex) +{ + UINT32 index, len; + LONG status = SCARD_E_NO_MEMORY; + LPSCARD_READERSTATEW rgReaderStates; + BOOL* states; + + if (Stream_GetRemainingLength(s) < 4) + return status; + + Stream_Read_UINT32(s, len); + if (len != cReaders) + { + WLog_ERR(TAG, "Count mismatch when reading LPSCARD_READERSTATEW"); + return status; + } + + rgReaderStates = (LPSCARD_READERSTATEW)calloc(cReaders, sizeof(SCARD_READERSTATEW)); + states = calloc(cReaders, sizeof(BOOL)); + + if (!rgReaderStates || !states) + goto fail; + + status = ERROR_INVALID_DATA; + for (index = 0; index < cReaders; index++) + { + UINT32 ptr = UINT32_MAX; + LPSCARD_READERSTATEW readerState = &rgReaderStates[index]; + + if (Stream_GetRemainingLength(s) < 52) + { + WLog_WARN(TAG, "GetStatusChangeA_Call is too short: %" PRIuz "", + Stream_GetRemainingLength(s)); + goto fail; + } + + if (!smartcard_ndr_pointer_read(s, ptrIndex, &ptr)) + { + if (ptr != 0) + goto fail; + } + /* Ignore NULL length strings */ + states[index] = ptr != 0; + Stream_Read_UINT32(s, readerState->dwCurrentState); /* dwCurrentState (4 bytes) */ + Stream_Read_UINT32(s, readerState->dwEventState); /* dwEventState (4 bytes) */ + Stream_Read_UINT32(s, readerState->cbAtr); /* cbAtr (4 bytes) */ + Stream_Read(s, readerState->rgbAtr, 36); /* rgbAtr [0..36] (36 bytes) */ + } + + for (index = 0; index < cReaders; index++) + { + LPSCARD_READERSTATEW readerState = &rgReaderStates[index]; + + /* Skip NULL pointers */ + if (!states[index]) + continue; + + status = smartcard_ndr_read_w(s, &readerState->szReader, NDR_PTR_FULL); + if (status != SCARD_S_SUCCESS) + goto fail; + } + + *ppcReaders = rgReaderStates; + free(states); + return SCARD_S_SUCCESS; +fail: + if (rgReaderStates) + { + for (index = 0; index < cReaders; index++) + { + LPSCARD_READERSTATEW readerState = &rgReaderStates[index]; + free(readerState->szReader); + } + } + free(rgReaderStates); + free(states); + return status; +} + +/******************************************************************************/ +/************************************* End Trace Functions ********************/ +/******************************************************************************/ + +LONG smartcard_unpack_get_status_change_a_call(SMARTCARD_DEVICE* smartcard, wStream* s, + GetStatusChangeA_Call* call) +{ + LONG status; + UINT32 ndrPtr; + UINT32 index = 0; + call->rgReaderStates = NULL; + + status = smartcard_unpack_redir_scard_context(smartcard, s, &(call->handles.hContext), &index); + if (status != SCARD_S_SUCCESS) + return status; + + if (Stream_GetRemainingLength(s) < 12) + { + WLog_WARN(TAG, "GetStatusChangeA_Call is too short: %" PRIuz "", + Stream_GetRemainingLength(s)); + return STATUS_BUFFER_TOO_SMALL; + } + + Stream_Read_UINT32(s, call->dwTimeOut); /* dwTimeOut (4 bytes) */ + Stream_Read_UINT32(s, call->cReaders); /* cReaders (4 bytes) */ + if (!smartcard_ndr_pointer_read(s, &index, &ndrPtr)) + return ERROR_INVALID_DATA; + + if ((status = + smartcard_unpack_redir_scard_context_ref(smartcard, s, &(call->handles.hContext)))) + return status; + + if (ndrPtr) + { + status = smartcard_unpack_reader_state_a(s, &call->rgReaderStates, call->cReaders, &index); + if (status != SCARD_S_SUCCESS) + return status; + } + + smartcard_trace_get_status_change_a_call(smartcard, call); + return SCARD_S_SUCCESS; +} + +LONG smartcard_unpack_get_status_change_w_call(SMARTCARD_DEVICE* smartcard, wStream* s, + GetStatusChangeW_Call* call) +{ + UINT32 ndrPtr; + LONG status; + UINT32 index = 0; + + call->rgReaderStates = NULL; + + status = smartcard_unpack_redir_scard_context(smartcard, s, &(call->handles.hContext), &index); + if (status != SCARD_S_SUCCESS) + return status; + + if (Stream_GetRemainingLength(s) < 12) + { + WLog_WARN(TAG, "GetStatusChangeW_Call is too short: %" PRIuz "", + Stream_GetRemainingLength(s)); + return STATUS_BUFFER_TOO_SMALL; + } + + Stream_Read_UINT32(s, call->dwTimeOut); /* dwTimeOut (4 bytes) */ + Stream_Read_UINT32(s, call->cReaders); /* cReaders (4 bytes) */ + if (!smartcard_ndr_pointer_read(s, &index, &ndrPtr)) + return ERROR_INVALID_DATA; + + if ((status = + smartcard_unpack_redir_scard_context_ref(smartcard, s, &(call->handles.hContext)))) + return status; + + if (ndrPtr) + { + status = smartcard_unpack_reader_state_w(s, &call->rgReaderStates, call->cReaders, &index); + if (status != SCARD_S_SUCCESS) + return status; + } + + smartcard_trace_get_status_change_w_call(smartcard, call); + return SCARD_S_SUCCESS; +} + +LONG smartcard_pack_get_status_change_return(SMARTCARD_DEVICE* smartcard, wStream* s, + const GetStatusChange_Return* ret, BOOL unicode) +{ + LONG status; + DWORD cReaders = ret->cReaders; + UINT32 index = 0; + + smartcard_trace_get_status_change_return(smartcard, ret, unicode); + if (ret->ReturnCode != SCARD_S_SUCCESS) + cReaders = 0; + if (cReaders == SCARD_AUTOALLOCATE) + cReaders = 0; + + if (!Stream_EnsureRemainingCapacity(s, 4)) + return SCARD_E_NO_MEMORY; + + Stream_Write_UINT32(s, cReaders); /* cReaders (4 bytes) */ + if (!smartcard_ndr_pointer_write(s, &index, cReaders)) + return SCARD_E_NO_MEMORY; + status = smartcard_ndr_write_state(s, ret->rgReaderStates, cReaders, NDR_PTR_SIMPLE); + if (status != SCARD_S_SUCCESS) + return status; + return ret->ReturnCode; +} + +LONG smartcard_unpack_state_call(SMARTCARD_DEVICE* smartcard, wStream* s, State_Call* call) +{ + LONG status; + UINT32 index = 0; + + status = smartcard_unpack_redir_scard_context(smartcard, s, &(call->handles.hContext), &index); + if (status != SCARD_S_SUCCESS) + return status; + + status = smartcard_unpack_redir_scard_handle(smartcard, s, &(call->handles.hCard), &index); + if (status != SCARD_S_SUCCESS) + return status; + + if (Stream_GetRemainingLength(s) < 8) + { + WLog_WARN(TAG, "State_Call is too short: %" PRIuz "", Stream_GetRemainingLength(s)); + return STATUS_BUFFER_TOO_SMALL; + } + + Stream_Read_INT32(s, call->fpbAtrIsNULL); /* fpbAtrIsNULL (4 bytes) */ + Stream_Read_UINT32(s, call->cbAtrLen); /* cbAtrLen (4 bytes) */ + + if ((status = + smartcard_unpack_redir_scard_context_ref(smartcard, s, &(call->handles.hContext)))) + return status; + + if ((status = smartcard_unpack_redir_scard_handle_ref(smartcard, s, &(call->handles.hCard)))) + return status; + + return status; +} + +LONG smartcard_pack_state_return(SMARTCARD_DEVICE* smartcard, wStream* s, const State_Return* ret) +{ + LONG status; + DWORD cbAtrLen = ret->cbAtrLen; + DWORD index = 0; + + smartcard_trace_state_return(smartcard, ret); + if (ret->ReturnCode != SCARD_S_SUCCESS) + cbAtrLen = 0; + if (cbAtrLen == SCARD_AUTOALLOCATE) + cbAtrLen = 0; + + Stream_Write_UINT32(s, ret->dwState); /* dwState (4 bytes) */ + Stream_Write_UINT32(s, ret->dwProtocol); /* dwProtocol (4 bytes) */ + Stream_Write_UINT32(s, cbAtrLen); /* cbAtrLen (4 bytes) */ + if (!smartcard_ndr_pointer_write(s, &index, cbAtrLen)) + return SCARD_E_NO_MEMORY; + status = smartcard_ndr_write(s, ret->rgAtr, cbAtrLen, 1, NDR_PTR_SIMPLE); + if (status != SCARD_S_SUCCESS) + return status; + return ret->ReturnCode; +} + +LONG smartcard_unpack_status_call(SMARTCARD_DEVICE* smartcard, wStream* s, Status_Call* call, + BOOL unicode) +{ + LONG status; + UINT32 index = 0; + status = smartcard_unpack_redir_scard_context(smartcard, s, &(call->handles.hContext), &index); + if (status != SCARD_S_SUCCESS) + return status; + + status = smartcard_unpack_redir_scard_handle(smartcard, s, &(call->handles.hCard), &index); + if (status != SCARD_S_SUCCESS) + return status; + + if (Stream_GetRemainingLength(s) < 12) + { + WLog_WARN(TAG, "Status_Call is too short: %" PRIuz "", Stream_GetRemainingLength(s)); + return STATUS_BUFFER_TOO_SMALL; + } + + Stream_Read_INT32(s, call->fmszReaderNamesIsNULL); /* fmszReaderNamesIsNULL (4 bytes) */ + Stream_Read_UINT32(s, call->cchReaderLen); /* cchReaderLen (4 bytes) */ + Stream_Read_UINT32(s, call->cbAtrLen); /* cbAtrLen (4 bytes) */ + + if ((status = + smartcard_unpack_redir_scard_context_ref(smartcard, s, &(call->handles.hContext)))) + return status; + + if ((status = smartcard_unpack_redir_scard_handle_ref(smartcard, s, &(call->handles.hCard)))) + return status; + + smartcard_trace_status_call(smartcard, call, unicode); + return status; +} + +LONG smartcard_pack_status_return(SMARTCARD_DEVICE* smartcard, wStream* s, const Status_Return* ret, + BOOL unicode) +{ + LONG status; + DWORD index = 0; + DWORD cBytes = ret->cBytes; + + smartcard_trace_status_return(smartcard, ret, unicode); + if (ret->ReturnCode != SCARD_S_SUCCESS) + cBytes = 0; + if (cBytes == SCARD_AUTOALLOCATE) + cBytes = 0; + + if (!Stream_EnsureRemainingCapacity(s, 4)) + return SCARD_F_INTERNAL_ERROR; + + Stream_Write_UINT32(s, cBytes); /* cBytes (4 bytes) */ + if (!smartcard_ndr_pointer_write(s, &index, cBytes)) + return SCARD_E_NO_MEMORY; + + if (!Stream_EnsureRemainingCapacity(s, 44)) + return SCARD_F_INTERNAL_ERROR; + + Stream_Write_UINT32(s, ret->dwState); /* dwState (4 bytes) */ + Stream_Write_UINT32(s, ret->dwProtocol); /* dwProtocol (4 bytes) */ + Stream_Write(s, ret->pbAtr, sizeof(ret->pbAtr)); /* pbAtr (32 bytes) */ + Stream_Write_UINT32(s, ret->cbAtrLen); /* cbAtrLen (4 bytes) */ + status = smartcard_ndr_write(s, ret->mszReaderNames, cBytes, 1, NDR_PTR_SIMPLE); + if (status != SCARD_S_SUCCESS) + return status; + return ret->ReturnCode; +} + +LONG smartcard_unpack_get_attrib_call(SMARTCARD_DEVICE* smartcard, wStream* s, GetAttrib_Call* call) +{ + LONG status; + UINT32 index = 0; + + status = smartcard_unpack_redir_scard_context(smartcard, s, &(call->handles.hContext), &index); + if (status != SCARD_S_SUCCESS) + return status; + + status = smartcard_unpack_redir_scard_handle(smartcard, s, &(call->handles.hCard), &index); + if (status != SCARD_S_SUCCESS) + return status; + + if (Stream_GetRemainingLength(s) < 12) + { + WLog_WARN(TAG, "GetAttrib_Call is too short: %" PRIuz "", Stream_GetRemainingLength(s)); + return STATUS_BUFFER_TOO_SMALL; + } + + Stream_Read_UINT32(s, call->dwAttrId); /* dwAttrId (4 bytes) */ + Stream_Read_INT32(s, call->fpbAttrIsNULL); /* fpbAttrIsNULL (4 bytes) */ + Stream_Read_UINT32(s, call->cbAttrLen); /* cbAttrLen (4 bytes) */ + + if ((status = + smartcard_unpack_redir_scard_context_ref(smartcard, s, &(call->handles.hContext)))) + return status; + + if ((status = smartcard_unpack_redir_scard_handle_ref(smartcard, s, &(call->handles.hCard)))) + return status; + + smartcard_trace_get_attrib_call(smartcard, call); + return status; +} + +LONG smartcard_pack_get_attrib_return(SMARTCARD_DEVICE* smartcard, wStream* s, + const GetAttrib_Return* ret, DWORD dwAttrId, + DWORD cbAttrCallLen) +{ + LONG status; + DWORD cbAttrLen; + DWORD index = 0; + smartcard_trace_get_attrib_return(smartcard, ret, dwAttrId); + + if (!Stream_EnsureRemainingCapacity(s, 4)) + return SCARD_F_INTERNAL_ERROR; + + cbAttrLen = ret->cbAttrLen; + if (ret->ReturnCode != SCARD_S_SUCCESS) + cbAttrLen = 0; + if (cbAttrLen == SCARD_AUTOALLOCATE) + cbAttrLen = 0; + if (cbAttrCallLen < cbAttrLen) + cbAttrLen = cbAttrCallLen; + Stream_Write_UINT32(s, cbAttrLen); /* cbAttrLen (4 bytes) */ + if (!smartcard_ndr_pointer_write(s, &index, cbAttrLen)) + return SCARD_E_NO_MEMORY; + + status = smartcard_ndr_write(s, ret->pbAttr, cbAttrLen, 1, NDR_PTR_SIMPLE); + if (status != SCARD_S_SUCCESS) + return status; + return ret->ReturnCode; +} + +LONG smartcard_unpack_control_call(SMARTCARD_DEVICE* smartcard, wStream* s, Control_Call* call) +{ + LONG status; + UINT32 index = 0; + UINT32 pvInBufferNdrPtr; + + call->pvInBuffer = NULL; + + status = smartcard_unpack_redir_scard_context(smartcard, s, &(call->handles.hContext), &index); + if (status != SCARD_S_SUCCESS) + return status; + + status = smartcard_unpack_redir_scard_handle(smartcard, s, &(call->handles.hCard), &index); + if (status != SCARD_S_SUCCESS) + return status; + + if (Stream_GetRemainingLength(s) < 20) + { + WLog_WARN(TAG, "Control_Call is too short: %" PRIuz "", Stream_GetRemainingLength(s)); + return STATUS_BUFFER_TOO_SMALL; + } + + Stream_Read_UINT32(s, call->dwControlCode); /* dwControlCode (4 bytes) */ + Stream_Read_UINT32(s, call->cbInBufferSize); /* cbInBufferSize (4 bytes) */ + if (!smartcard_ndr_pointer_read(s, &index, &pvInBufferNdrPtr)) /* pvInBufferNdrPtr (4 bytes) */ + return ERROR_INVALID_DATA; + Stream_Read_INT32(s, call->fpvOutBufferIsNULL); /* fpvOutBufferIsNULL (4 bytes) */ + Stream_Read_UINT32(s, call->cbOutBufferSize); /* cbOutBufferSize (4 bytes) */ + + if ((status = + smartcard_unpack_redir_scard_context_ref(smartcard, s, &(call->handles.hContext)))) + return status; + + if ((status = smartcard_unpack_redir_scard_handle_ref(smartcard, s, &(call->handles.hCard)))) + return status; + + if (pvInBufferNdrPtr) + { + status = smartcard_ndr_read(s, &call->pvInBuffer, call->cbInBufferSize, 1, NDR_PTR_SIMPLE); + if (status != SCARD_S_SUCCESS) + return status; + } + + smartcard_trace_control_call(smartcard, call); + return SCARD_S_SUCCESS; +} + +LONG smartcard_pack_control_return(SMARTCARD_DEVICE* smartcard, wStream* s, + const Control_Return* ret) +{ + LONG status; + DWORD cbDataLen = ret->cbOutBufferSize; + DWORD index = 0; + + smartcard_trace_control_return(smartcard, ret); + if (ret->ReturnCode != SCARD_S_SUCCESS) + cbDataLen = 0; + if (cbDataLen == SCARD_AUTOALLOCATE) + cbDataLen = 0; + + if (!Stream_EnsureRemainingCapacity(s, 4)) + return SCARD_F_INTERNAL_ERROR; + + Stream_Write_UINT32(s, cbDataLen); /* cbOutBufferSize (4 bytes) */ + if (!smartcard_ndr_pointer_write(s, &index, cbDataLen)) + return SCARD_E_NO_MEMORY; + + status = smartcard_ndr_write(s, ret->pvOutBuffer, cbDataLen, 1, NDR_PTR_SIMPLE); + if (status != SCARD_S_SUCCESS) + return status; + return ret->ReturnCode; +} + +LONG smartcard_unpack_transmit_call(SMARTCARD_DEVICE* smartcard, wStream* s, Transmit_Call* call) +{ + LONG status; + UINT32 length; + BYTE* pbExtraBytes; + UINT32 pbExtraBytesNdrPtr; + UINT32 pbSendBufferNdrPtr; + UINT32 pioRecvPciNdrPtr; + SCardIO_Request ioSendPci; + SCardIO_Request ioRecvPci; + UINT32 index = 0; + call->pioSendPci = NULL; + call->pioRecvPci = NULL; + call->pbSendBuffer = NULL; + + status = smartcard_unpack_redir_scard_context(smartcard, s, &(call->handles.hContext), &index); + if (status != SCARD_S_SUCCESS) + return status; + + status = smartcard_unpack_redir_scard_handle(smartcard, s, &(call->handles.hCard), &index); + if (status != SCARD_S_SUCCESS) + return status; + + if (Stream_GetRemainingLength(s) < 32) + { + WLog_WARN(TAG, "Transmit_Call is too short: Actual: %" PRIuz ", Expected: 32", + Stream_GetRemainingLength(s)); + return STATUS_BUFFER_TOO_SMALL; + } + + Stream_Read_UINT32(s, ioSendPci.dwProtocol); /* dwProtocol (4 bytes) */ + Stream_Read_UINT32(s, ioSendPci.cbExtraBytes); /* cbExtraBytes (4 bytes) */ + if (!smartcard_ndr_pointer_read(s, &index, + &pbExtraBytesNdrPtr)) /* pbExtraBytesNdrPtr (4 bytes) */ + return ERROR_INVALID_DATA; + + Stream_Read_UINT32(s, call->cbSendLength); /* cbSendLength (4 bytes) */ + if (!smartcard_ndr_pointer_read(s, &index, + &pbSendBufferNdrPtr)) /* pbSendBufferNdrPtr (4 bytes) */ + return ERROR_INVALID_DATA; + + if (!smartcard_ndr_pointer_read(s, &index, &pioRecvPciNdrPtr)) /* pioRecvPciNdrPtr (4 bytes) */ + return ERROR_INVALID_DATA; + + Stream_Read_INT32(s, call->fpbRecvBufferIsNULL); /* fpbRecvBufferIsNULL (4 bytes) */ + Stream_Read_UINT32(s, call->cbRecvLength); /* cbRecvLength (4 bytes) */ + + if (ioSendPci.cbExtraBytes > 1024) + { + WLog_WARN(TAG, + "Transmit_Call ioSendPci.cbExtraBytes is out of bounds: %" PRIu32 " (max: 1024)", + ioSendPci.cbExtraBytes); + return STATUS_INVALID_PARAMETER; + } + + if (call->cbSendLength > 66560) + { + WLog_WARN(TAG, "Transmit_Call cbSendLength is out of bounds: %" PRIu32 " (max: 66560)", + ioSendPci.cbExtraBytes); + return STATUS_INVALID_PARAMETER; + } + + if ((status = + smartcard_unpack_redir_scard_context_ref(smartcard, s, &(call->handles.hContext)))) + return status; + + if ((status = smartcard_unpack_redir_scard_handle_ref(smartcard, s, &(call->handles.hCard)))) + return status; + + if (ioSendPci.cbExtraBytes && !pbExtraBytesNdrPtr) + { + WLog_WARN( + TAG, "Transmit_Call ioSendPci.cbExtraBytes is non-zero but pbExtraBytesNdrPtr is null"); + return STATUS_INVALID_PARAMETER; + } + + if (pbExtraBytesNdrPtr) + { + // TODO: Use unified pointer reading + if (Stream_GetRemainingLength(s) < 4) + { + WLog_WARN(TAG, "Transmit_Call is too short: %" PRIuz " (ioSendPci.pbExtraBytes)", + Stream_GetRemainingLength(s)); + return STATUS_BUFFER_TOO_SMALL; + } + + Stream_Read_UINT32(s, length); /* Length (4 bytes) */ + + if (Stream_GetRemainingLength(s) < ioSendPci.cbExtraBytes) + { + WLog_WARN(TAG, + "Transmit_Call is too short: Actual: %" PRIuz ", Expected: %" PRIu32 + " (ioSendPci.cbExtraBytes)", + Stream_GetRemainingLength(s), ioSendPci.cbExtraBytes); + return STATUS_BUFFER_TOO_SMALL; + } + + ioSendPci.pbExtraBytes = Stream_Pointer(s); + call->pioSendPci = + (LPSCARD_IO_REQUEST)malloc(sizeof(SCARD_IO_REQUEST) + ioSendPci.cbExtraBytes); + + if (!call->pioSendPci) + { + WLog_WARN(TAG, "Transmit_Call out of memory error (pioSendPci)"); + return STATUS_NO_MEMORY; + } + + call->pioSendPci->dwProtocol = ioSendPci.dwProtocol; + call->pioSendPci->cbPciLength = (DWORD)(ioSendPci.cbExtraBytes + sizeof(SCARD_IO_REQUEST)); + pbExtraBytes = &((BYTE*)call->pioSendPci)[sizeof(SCARD_IO_REQUEST)]; + Stream_Read(s, pbExtraBytes, ioSendPci.cbExtraBytes); + smartcard_unpack_read_size_align(smartcard, s, ioSendPci.cbExtraBytes, 4); + } + else + { + call->pioSendPci = (LPSCARD_IO_REQUEST)calloc(1, sizeof(SCARD_IO_REQUEST)); + + if (!call->pioSendPci) + { + WLog_WARN(TAG, "Transmit_Call out of memory error (pioSendPci)"); + return STATUS_NO_MEMORY; + } + + call->pioSendPci->dwProtocol = ioSendPci.dwProtocol; + call->pioSendPci->cbPciLength = sizeof(SCARD_IO_REQUEST); + } + + if (pbSendBufferNdrPtr) + { + status = smartcard_ndr_read(s, &call->pbSendBuffer, call->cbSendLength, 1, NDR_PTR_SIMPLE); + if (status != SCARD_S_SUCCESS) + return status; + } + + if (pioRecvPciNdrPtr) + { + if (Stream_GetRemainingLength(s) < 12) + { + WLog_WARN(TAG, "Transmit_Call is too short: Actual: %" PRIuz ", Expected: 12", + Stream_GetRemainingLength(s)); + return STATUS_BUFFER_TOO_SMALL; + } + + Stream_Read_UINT32(s, ioRecvPci.dwProtocol); /* dwProtocol (4 bytes) */ + Stream_Read_UINT32(s, ioRecvPci.cbExtraBytes); /* cbExtraBytes (4 bytes) */ + if (!smartcard_ndr_pointer_read(s, &index, + &pbExtraBytesNdrPtr)) /* pbExtraBytesNdrPtr (4 bytes) */ + return ERROR_INVALID_DATA; + + if (ioRecvPci.cbExtraBytes && !pbExtraBytesNdrPtr) + { + WLog_WARN( + TAG, + "Transmit_Call ioRecvPci.cbExtraBytes is non-zero but pbExtraBytesNdrPtr is null"); + return STATUS_INVALID_PARAMETER; + } + + if (pbExtraBytesNdrPtr) + { + // TODO: Unify ndr pointer reading + if (Stream_GetRemainingLength(s) < 4) + { + WLog_WARN(TAG, "Transmit_Call is too short: %" PRIuz " (ioRecvPci.pbExtraBytes)", + Stream_GetRemainingLength(s)); + return STATUS_BUFFER_TOO_SMALL; + } + + Stream_Read_UINT32(s, length); /* Length (4 bytes) */ + + if (ioRecvPci.cbExtraBytes > 1024) + { + WLog_WARN(TAG, + "Transmit_Call ioRecvPci.cbExtraBytes is out of bounds: %" PRIu32 + " (max: 1024)", + ioRecvPci.cbExtraBytes); + return STATUS_INVALID_PARAMETER; + } + + if (length != ioRecvPci.cbExtraBytes) + { + WLog_WARN(TAG, + "Transmit_Call unexpected length: Actual: %" PRIu32 ", Expected: %" PRIu32 + " (ioRecvPci.cbExtraBytes)", + length, ioRecvPci.cbExtraBytes); + return STATUS_INVALID_PARAMETER; + } + + if (Stream_GetRemainingLength(s) < ioRecvPci.cbExtraBytes) + { + WLog_WARN(TAG, + "Transmit_Call is too short: Actual: %" PRIuz ", Expected: %" PRIu32 + " (ioRecvPci.cbExtraBytes)", + Stream_GetRemainingLength(s), ioRecvPci.cbExtraBytes); + return STATUS_BUFFER_TOO_SMALL; + } + + ioRecvPci.pbExtraBytes = Stream_Pointer(s); + call->pioRecvPci = + (LPSCARD_IO_REQUEST)malloc(sizeof(SCARD_IO_REQUEST) + ioRecvPci.cbExtraBytes); + + if (!call->pioRecvPci) + { + WLog_WARN(TAG, "Transmit_Call out of memory error (pioRecvPci)"); + return STATUS_NO_MEMORY; + } + + call->pioRecvPci->dwProtocol = ioRecvPci.dwProtocol; + call->pioRecvPci->cbPciLength = + (DWORD)(ioRecvPci.cbExtraBytes + sizeof(SCARD_IO_REQUEST)); + pbExtraBytes = &((BYTE*)call->pioRecvPci)[sizeof(SCARD_IO_REQUEST)]; + Stream_Read(s, pbExtraBytes, ioRecvPci.cbExtraBytes); + smartcard_unpack_read_size_align(smartcard, s, ioRecvPci.cbExtraBytes, 4); + } + else + { + call->pioRecvPci = (LPSCARD_IO_REQUEST)calloc(1, sizeof(SCARD_IO_REQUEST)); + + if (!call->pioRecvPci) + { + WLog_WARN(TAG, "Transmit_Call out of memory error (pioRecvPci)"); + return STATUS_NO_MEMORY; + } + + call->pioRecvPci->dwProtocol = ioRecvPci.dwProtocol; + call->pioRecvPci->cbPciLength = sizeof(SCARD_IO_REQUEST); + } + } + + smartcard_trace_transmit_call(smartcard, call); + return SCARD_S_SUCCESS; +} + +LONG smartcard_pack_transmit_return(SMARTCARD_DEVICE* smartcard, wStream* s, + const Transmit_Return* ret) +{ + LONG status; + DWORD index = 0; + LONG error; + UINT32 cbRecvLength = ret->cbRecvLength; + UINT32 cbRecvPci = ret->pioRecvPci ? ret->pioRecvPci->cbPciLength : 0; + + smartcard_trace_transmit_return(smartcard, ret); + + if (!ret->pbRecvBuffer) + cbRecvLength = 0; + + if (!smartcard_ndr_pointer_write(s, &index, cbRecvPci)) + return SCARD_E_NO_MEMORY; + if (!Stream_EnsureRemainingCapacity(s, 4)) + return SCARD_E_NO_MEMORY; + Stream_Write_UINT32(s, cbRecvLength); /* cbRecvLength (4 bytes) */ + if (!smartcard_ndr_pointer_write(s, &index, cbRecvLength)) + return SCARD_E_NO_MEMORY; + + if (ret->pioRecvPci) + { + UINT32 cbExtraBytes = (UINT32)(ret->pioRecvPci->cbPciLength - sizeof(SCARD_IO_REQUEST)); + BYTE* pbExtraBytes = &((BYTE*)ret->pioRecvPci)[sizeof(SCARD_IO_REQUEST)]; + + if (!Stream_EnsureRemainingCapacity(s, cbExtraBytes + 16)) + { + WLog_ERR(TAG, "Stream_EnsureRemainingCapacity failed!"); + return SCARD_F_INTERNAL_ERROR; + } + + Stream_Write_UINT32(s, ret->pioRecvPci->dwProtocol); /* dwProtocol (4 bytes) */ + Stream_Write_UINT32(s, cbExtraBytes); /* cbExtraBytes (4 bytes) */ + if (!smartcard_ndr_pointer_write(s, &index, cbExtraBytes)) + return SCARD_E_NO_MEMORY; + error = smartcard_ndr_write(s, pbExtraBytes, cbExtraBytes, 1, NDR_PTR_SIMPLE); + if (error) + return error; + } + + status = smartcard_ndr_write(s, ret->pbRecvBuffer, ret->cbRecvLength, 1, NDR_PTR_SIMPLE); + if (status != SCARD_S_SUCCESS) + return status; + return ret->ReturnCode; +} + +LONG smartcard_unpack_locate_cards_by_atr_a_call(SMARTCARD_DEVICE* smartcard, wStream* s, + LocateCardsByATRA_Call* call) +{ + LONG status; + UINT32 rgReaderStatesNdrPtr; + UINT32 rgAtrMasksNdrPtr; + UINT32 index = 0; + call->rgReaderStates = NULL; + + status = smartcard_unpack_redir_scard_context(smartcard, s, &(call->handles.hContext), &index); + if (status != SCARD_S_SUCCESS) + return status; + + if (Stream_GetRemainingLength(s) < 16) + { + WLog_WARN(TAG, "LocateCardsByATRA_Call is too short: %" PRIuz "", + Stream_GetRemainingLength(s)); + return STATUS_BUFFER_TOO_SMALL; + } + + Stream_Read_UINT32(s, call->cAtrs); + if (!smartcard_ndr_pointer_read(s, &index, &rgAtrMasksNdrPtr)) + return ERROR_INVALID_DATA; + Stream_Read_UINT32(s, call->cReaders); /* cReaders (4 bytes) */ + if (!smartcard_ndr_pointer_read(s, &index, &rgReaderStatesNdrPtr)) + return ERROR_INVALID_DATA; + + if ((status = + smartcard_unpack_redir_scard_context_ref(smartcard, s, &(call->handles.hContext)))) + return status; + + if ((rgAtrMasksNdrPtr && !call->cAtrs) || (!rgAtrMasksNdrPtr && call->cAtrs)) + { + WLog_WARN(TAG, + "LocateCardsByATRA_Call rgAtrMasksNdrPtr (0x%08" PRIX32 + ") and cAtrs (0x%08" PRIX32 ") inconsistency", + rgAtrMasksNdrPtr, call->cAtrs); + return STATUS_INVALID_PARAMETER; + } + + if (rgAtrMasksNdrPtr) + { + status = smartcard_ndr_read_atrmask(s, &call->rgAtrMasks, call->cAtrs, NDR_PTR_SIMPLE); + if (status != SCARD_S_SUCCESS) + return status; + } + + if (rgReaderStatesNdrPtr) + { + status = smartcard_unpack_reader_state_a(s, &call->rgReaderStates, call->cReaders, &index); + if (status != SCARD_S_SUCCESS) + return status; + } + + smartcard_trace_locate_cards_by_atr_a_call(smartcard, call); + return SCARD_S_SUCCESS; +} + +LONG smartcard_unpack_context_and_two_strings_a_call(SMARTCARD_DEVICE* smartcard, wStream* s, + ContextAndTwoStringA_Call* call) +{ + LONG status; + UINT32 sz1NdrPtr, sz2NdrPtr; + UINT32 index = 0; + + status = smartcard_unpack_redir_scard_context(smartcard, s, &(call->handles.hContext), &index); + if (status != SCARD_S_SUCCESS) + return status; + + if (!smartcard_ndr_pointer_read(s, &index, &sz1NdrPtr)) + return ERROR_INVALID_DATA; + if (!smartcard_ndr_pointer_read(s, &index, &sz2NdrPtr)) + return ERROR_INVALID_DATA; + + status = smartcard_unpack_redir_scard_context_ref(smartcard, s, &call->handles.hContext); + if (status != SCARD_S_SUCCESS) + return status; + + if (sz1NdrPtr) + { + status = smartcard_ndr_read_a(s, &call->sz1, NDR_PTR_FULL); + if (status != SCARD_S_SUCCESS) + return status; + } + if (sz2NdrPtr) + { + status = smartcard_ndr_read_a(s, &call->sz2, NDR_PTR_FULL); + if (status != SCARD_S_SUCCESS) + return status; + } + smartcard_trace_context_and_two_strings_a_call(smartcard, call); + return SCARD_S_SUCCESS; +} + +LONG smartcard_unpack_context_and_two_strings_w_call(SMARTCARD_DEVICE* smartcard, wStream* s, + ContextAndTwoStringW_Call* call) +{ + LONG status; + UINT32 sz1NdrPtr, sz2NdrPtr; + UINT32 index = 0; + status = smartcard_unpack_redir_scard_context(smartcard, s, &(call->handles.hContext), &index); + if (status != SCARD_S_SUCCESS) + return status; + + if (!smartcard_ndr_pointer_read(s, &index, &sz1NdrPtr)) + return ERROR_INVALID_DATA; + if (!smartcard_ndr_pointer_read(s, &index, &sz2NdrPtr)) + return ERROR_INVALID_DATA; + + status = smartcard_unpack_redir_scard_context_ref(smartcard, s, &call->handles.hContext); + if (status != SCARD_S_SUCCESS) + return status; + + if (sz1NdrPtr) + { + status = smartcard_ndr_read_w(s, &call->sz1, NDR_PTR_FULL); + if (status != SCARD_S_SUCCESS) + return status; + } + if (sz2NdrPtr) + { + status = smartcard_ndr_read_w(s, &call->sz2, NDR_PTR_FULL); + if (status != SCARD_S_SUCCESS) + return status; + } + smartcard_trace_context_and_two_strings_w_call(smartcard, call); + return SCARD_S_SUCCESS; +} + +LONG smartcard_unpack_locate_cards_a_call(SMARTCARD_DEVICE* smartcard, wStream* s, + LocateCardsA_Call* call) +{ + LONG status; + UINT32 sz1NdrPtr, sz2NdrPtr; + UINT32 index = 0; + status = smartcard_unpack_redir_scard_context(smartcard, s, &(call->handles.hContext), &index); + if (status != SCARD_S_SUCCESS) + return status; + + if (Stream_GetRemainingLength(s) < 16) + { + WLog_WARN(TAG, "%s is too short: %" PRIuz "", __FUNCTION__, Stream_GetRemainingLength(s)); + return STATUS_BUFFER_TOO_SMALL; + } + Stream_Read_UINT32(s, call->cBytes); + if (!smartcard_ndr_pointer_read(s, &index, &sz1NdrPtr)) + return ERROR_INVALID_DATA; + + Stream_Read_UINT32(s, call->cReaders); + if (!smartcard_ndr_pointer_read(s, &index, &sz2NdrPtr)) + return ERROR_INVALID_DATA; + + if (sz1NdrPtr) + { + status = + smartcard_ndr_read_fixed_string_a(s, &call->mszCards, call->cBytes, NDR_PTR_SIMPLE); + if (status != SCARD_S_SUCCESS) + return status; + } + if (sz2NdrPtr) + { + status = smartcard_unpack_reader_state_a(s, &call->rgReaderStates, call->cReaders, &index); + if (status != SCARD_S_SUCCESS) + return status; + } + smartcard_trace_locate_cards_a_call(smartcard, call); + return SCARD_S_SUCCESS; +} + +LONG smartcard_unpack_locate_cards_w_call(SMARTCARD_DEVICE* smartcard, wStream* s, + LocateCardsW_Call* call) +{ + LONG status; + UINT32 sz1NdrPtr, sz2NdrPtr; + UINT32 index = 0; + + status = smartcard_unpack_redir_scard_context(smartcard, s, &(call->handles.hContext), &index); + if (status != SCARD_S_SUCCESS) + return status; + + if (Stream_GetRemainingLength(s) < 16) + { + WLog_WARN(TAG, "%s is too short: %" PRIuz "", __FUNCTION__, Stream_GetRemainingLength(s)); + return STATUS_BUFFER_TOO_SMALL; + } + Stream_Read_UINT32(s, call->cBytes); + if (!smartcard_ndr_pointer_read(s, &index, &sz1NdrPtr)) + return ERROR_INVALID_DATA; + + Stream_Read_UINT32(s, call->cReaders); + if (!smartcard_ndr_pointer_read(s, &index, &sz2NdrPtr)) + return ERROR_INVALID_DATA; + + if (sz1NdrPtr) + { + status = + smartcard_ndr_read_fixed_string_w(s, &call->mszCards, call->cBytes, NDR_PTR_SIMPLE); + if (status != SCARD_S_SUCCESS) + return status; + } + if (sz2NdrPtr) + { + status = smartcard_unpack_reader_state_w(s, &call->rgReaderStates, call->cReaders, &index); + if (status != SCARD_S_SUCCESS) + return status; + } + smartcard_trace_locate_cards_w_call(smartcard, call); + return SCARD_S_SUCCESS; +} + +LONG smartcard_unpack_set_attrib_call(SMARTCARD_DEVICE* smartcard, wStream* s, SetAttrib_Call* call) +{ + LONG status; + UINT32 index = 0; + UINT32 ndrPtr; + + status = smartcard_unpack_redir_scard_context(smartcard, s, &(call->handles.hContext), &index); + if (status != SCARD_S_SUCCESS) + return status; + status = smartcard_unpack_redir_scard_handle(smartcard, s, &(call->handles.hCard), &index); + if (status != SCARD_S_SUCCESS) + return status; + + if (Stream_GetRemainingLength(s) < 12) + return STATUS_BUFFER_TOO_SMALL; + Stream_Read_UINT32(s, call->dwAttrId); + Stream_Read_UINT32(s, call->cbAttrLen); + + if (!smartcard_ndr_pointer_read(s, &index, &ndrPtr)) + return ERROR_INVALID_DATA; + + if ((status = + smartcard_unpack_redir_scard_context_ref(smartcard, s, &(call->handles.hContext)))) + return status; + + if ((status = smartcard_unpack_redir_scard_handle_ref(smartcard, s, &(call->handles.hCard)))) + return status; + + if (ndrPtr) + { + // TODO: call->cbAttrLen was larger than the pointer value. + // TODO: Maybe need to refine the checks? + status = smartcard_ndr_read(s, &call->pbAttr, 0, 1, NDR_PTR_SIMPLE); + if (status != SCARD_S_SUCCESS) + return status; + } + smartcard_trace_set_attrib_call(smartcard, call); + return SCARD_S_SUCCESS; +} + +LONG smartcard_unpack_locate_cards_by_atr_w_call(SMARTCARD_DEVICE* smartcard, wStream* s, + LocateCardsByATRW_Call* call) +{ + LONG status; + UINT32 rgReaderStatesNdrPtr; + UINT32 rgAtrMasksNdrPtr; + UINT32 index = 0; + call->rgReaderStates = NULL; + + status = smartcard_unpack_redir_scard_context(smartcard, s, &(call->handles.hContext), &index); + if (status != SCARD_S_SUCCESS) + return status; + + if (Stream_GetRemainingLength(s) < 16) + { + WLog_WARN(TAG, "LocateCardsByATRW_Call is too short: %" PRIuz "", + Stream_GetRemainingLength(s)); + return STATUS_BUFFER_TOO_SMALL; + } + + Stream_Read_UINT32(s, call->cAtrs); + if (!smartcard_ndr_pointer_read(s, &index, &rgAtrMasksNdrPtr)) + return ERROR_INVALID_DATA; + + Stream_Read_UINT32(s, call->cReaders); /* cReaders (4 bytes) */ + if (!smartcard_ndr_pointer_read(s, &index, &rgReaderStatesNdrPtr)) + return ERROR_INVALID_DATA; + + if ((status = + smartcard_unpack_redir_scard_context_ref(smartcard, s, &(call->handles.hContext)))) + return status; + + if ((rgAtrMasksNdrPtr && !call->cAtrs) || (!rgAtrMasksNdrPtr && call->cAtrs)) + { + WLog_WARN(TAG, + "LocateCardsByATRW_Call rgAtrMasksNdrPtr (0x%08" PRIX32 + ") and cAtrs (0x%08" PRIX32 ") inconsistency", + rgAtrMasksNdrPtr, call->cAtrs); + return STATUS_INVALID_PARAMETER; + } + + if (rgAtrMasksNdrPtr) + { + status = smartcard_ndr_read_atrmask(s, &call->rgAtrMasks, call->cAtrs, NDR_PTR_SIMPLE); + if (status != SCARD_S_SUCCESS) + return status; + } + + if (rgReaderStatesNdrPtr) + { + status = smartcard_unpack_reader_state_w(s, &call->rgReaderStates, call->cReaders, &index); + if (status != SCARD_S_SUCCESS) + return status; + } + + smartcard_trace_locate_cards_by_atr_w_call(smartcard, call); + return SCARD_S_SUCCESS; +} + +LONG smartcard_unpack_read_cache_a_call(SMARTCARD_DEVICE* smartcard, wStream* s, + ReadCacheA_Call* call) +{ + LONG status; + UINT32 mszNdrPtr; + UINT32 contextNdrPtr; + UINT32 index = 0; + + if (!smartcard_ndr_pointer_read(s, &index, &mszNdrPtr)) + return ERROR_INVALID_DATA; + + status = smartcard_unpack_redir_scard_context(smartcard, s, &(call->Common.handles.hContext), + &index); + if (status != SCARD_S_SUCCESS) + return status; + + if (!smartcard_ndr_pointer_read(s, &index, &contextNdrPtr)) + return ERROR_INVALID_DATA; + + if (Stream_GetRemainingLength(s) < 12) + return STATUS_BUFFER_TOO_SMALL; + Stream_Read_UINT32(s, call->Common.FreshnessCounter); + Stream_Read_INT32(s, call->Common.fPbDataIsNULL); + Stream_Read_UINT32(s, call->Common.cbDataLen); + + call->szLookupName = NULL; + if (mszNdrPtr) + { + status = smartcard_ndr_read_a(s, &call->szLookupName, NDR_PTR_FULL); + if (status != SCARD_S_SUCCESS) + return status; + } + + status = smartcard_unpack_redir_scard_context_ref(smartcard, s, &call->Common.handles.hContext); + if (status != SCARD_S_SUCCESS) + return status; + + if (contextNdrPtr) + { + status = smartcard_ndr_read_u(s, &call->Common.CardIdentifier); + if (status != SCARD_S_SUCCESS) + return status; + } + smartcard_trace_read_cache_a_call(smartcard, call); + return SCARD_S_SUCCESS; +} + +LONG smartcard_unpack_read_cache_w_call(SMARTCARD_DEVICE* smartcard, wStream* s, + ReadCacheW_Call* call) +{ + LONG status; + UINT32 mszNdrPtr; + UINT32 contextNdrPtr; + UINT32 index = 0; + + if (!smartcard_ndr_pointer_read(s, &index, &mszNdrPtr)) + return ERROR_INVALID_DATA; + + status = smartcard_unpack_redir_scard_context(smartcard, s, &(call->Common.handles.hContext), + &index); + if (status != SCARD_S_SUCCESS) + return status; + + if (!smartcard_ndr_pointer_read(s, &index, &contextNdrPtr)) + return ERROR_INVALID_DATA; + + if (Stream_GetRemainingLength(s) < 12) + return STATUS_BUFFER_TOO_SMALL; + Stream_Read_UINT32(s, call->Common.FreshnessCounter); + Stream_Read_INT32(s, call->Common.fPbDataIsNULL); + Stream_Read_UINT32(s, call->Common.cbDataLen); + + call->szLookupName = NULL; + if (mszNdrPtr) + { + status = smartcard_ndr_read_w(s, &call->szLookupName, NDR_PTR_FULL); + if (status != SCARD_S_SUCCESS) + return status; + } + + status = smartcard_unpack_redir_scard_context_ref(smartcard, s, &call->Common.handles.hContext); + if (status != SCARD_S_SUCCESS) + return status; + + if (contextNdrPtr) + { + status = smartcard_ndr_read_u(s, &call->Common.CardIdentifier); + if (status != SCARD_S_SUCCESS) + return status; + } + smartcard_trace_read_cache_w_call(smartcard, call); + return SCARD_S_SUCCESS; +} + +LONG smartcard_unpack_write_cache_a_call(SMARTCARD_DEVICE* smartcard, wStream* s, + WriteCacheA_Call* call) +{ + LONG status; + UINT32 mszNdrPtr; + UINT32 contextNdrPtr; + UINT32 pbDataNdrPtr; + UINT32 index = 0; + + if (!smartcard_ndr_pointer_read(s, &index, &mszNdrPtr)) + return ERROR_INVALID_DATA; + + status = smartcard_unpack_redir_scard_context(smartcard, s, &(call->Common.handles.hContext), + &index); + if (status != SCARD_S_SUCCESS) + return status; + + if (!smartcard_ndr_pointer_read(s, &index, &contextNdrPtr)) + return ERROR_INVALID_DATA; + + if (Stream_GetRemainingLength(s) < 8) + return STATUS_BUFFER_TOO_SMALL; + + Stream_Read_UINT32(s, call->Common.FreshnessCounter); + Stream_Read_UINT32(s, call->Common.cbDataLen); + + if (!smartcard_ndr_pointer_read(s, &index, &pbDataNdrPtr)) + return ERROR_INVALID_DATA; + + call->szLookupName = NULL; + if (mszNdrPtr) + { + status = smartcard_ndr_read_a(s, &call->szLookupName, NDR_PTR_FULL); + if (status != SCARD_S_SUCCESS) + return status; + } + + status = smartcard_unpack_redir_scard_context_ref(smartcard, s, &call->Common.handles.hContext); + if (status != SCARD_S_SUCCESS) + return status; + + call->Common.CardIdentifier = NULL; + if (contextNdrPtr) + { + status = smartcard_ndr_read_u(s, &call->Common.CardIdentifier); + if (status != SCARD_S_SUCCESS) + return status; + } + + call->Common.pbData = NULL; + if (pbDataNdrPtr) + { + status = + smartcard_ndr_read(s, &call->Common.pbData, call->Common.cbDataLen, 1, NDR_PTR_SIMPLE); + if (status != SCARD_S_SUCCESS) + return status; + } + smartcard_trace_write_cache_a_call(smartcard, call); + return SCARD_S_SUCCESS; +} + +LONG smartcard_unpack_write_cache_w_call(SMARTCARD_DEVICE* smartcard, wStream* s, + WriteCacheW_Call* call) +{ + LONG status; + UINT32 mszNdrPtr; + UINT32 contextNdrPtr; + UINT32 pbDataNdrPtr; + UINT32 index = 0; + + if (!smartcard_ndr_pointer_read(s, &index, &mszNdrPtr)) + return ERROR_INVALID_DATA; + + status = smartcard_unpack_redir_scard_context(smartcard, s, &(call->Common.handles.hContext), + &index); + if (status != SCARD_S_SUCCESS) + return status; + + if (!smartcard_ndr_pointer_read(s, &index, &contextNdrPtr)) + return ERROR_INVALID_DATA; + + if (Stream_GetRemainingLength(s) < 8) + return STATUS_BUFFER_TOO_SMALL; + Stream_Read_UINT32(s, call->Common.FreshnessCounter); + Stream_Read_UINT32(s, call->Common.cbDataLen); + + if (!smartcard_ndr_pointer_read(s, &index, &pbDataNdrPtr)) + return ERROR_INVALID_DATA; + + call->szLookupName = NULL; + if (mszNdrPtr) + { + status = smartcard_ndr_read_w(s, &call->szLookupName, NDR_PTR_FULL); + if (status != SCARD_S_SUCCESS) + return status; + } + + status = smartcard_unpack_redir_scard_context_ref(smartcard, s, &call->Common.handles.hContext); + if (status != SCARD_S_SUCCESS) + return status; + + call->Common.CardIdentifier = NULL; + if (contextNdrPtr) + { + status = smartcard_ndr_read_u(s, &call->Common.CardIdentifier); + if (status != SCARD_S_SUCCESS) + return status; + } + + call->Common.pbData = NULL; + if (pbDataNdrPtr) + { + status = + smartcard_ndr_read(s, &call->Common.pbData, call->Common.cbDataLen, 1, NDR_PTR_SIMPLE); + if (status != SCARD_S_SUCCESS) + return status; + } + smartcard_trace_write_cache_w_call(smartcard, call); + return status; +} + +LONG smartcard_unpack_get_transmit_count_call(SMARTCARD_DEVICE* smartcard, wStream* s, + GetTransmitCount_Call* call) +{ + LONG status; + UINT32 index = 0; + + status = smartcard_unpack_redir_scard_context(smartcard, s, &(call->handles.hContext), &index); + if (status != SCARD_S_SUCCESS) + return status; + + status = smartcard_unpack_redir_scard_handle(smartcard, s, &(call->handles.hCard), &index); + if (status != SCARD_S_SUCCESS) + return status; + + if ((status = + smartcard_unpack_redir_scard_context_ref(smartcard, s, &(call->handles.hContext)))) + { + WLog_ERR(TAG, "smartcard_unpack_redir_scard_context_ref failed with error %" PRId32 "", + status); + return status; + } + + if ((status = smartcard_unpack_redir_scard_handle_ref(smartcard, s, &(call->handles.hCard)))) + WLog_ERR(TAG, "smartcard_unpack_redir_scard_handle_ref failed with error %" PRId32 "", + status); + + smartcard_trace_get_transmit_count_call(smartcard, call); + return status; +} + +LONG smartcard_unpack_get_reader_icon_call(SMARTCARD_DEVICE* smartcard, wStream* s, + GetReaderIcon_Call* call) +{ + return smartcard_unpack_common_context_and_string_w(smartcard, s, &call->handles.hContext, + &call->szReaderName); +} + +LONG smartcard_unpack_context_and_string_a_call(SMARTCARD_DEVICE* smartcard, wStream* s, + ContextAndStringA_Call* call) +{ + return smartcard_unpack_common_context_and_string_a(smartcard, s, &call->handles.hContext, + &call->sz); +} + +LONG smartcard_unpack_context_and_string_w_call(SMARTCARD_DEVICE* smartcard, wStream* s, + ContextAndStringW_Call* call) +{ + return smartcard_unpack_common_context_and_string_w(smartcard, s, &call->handles.hContext, + &call->sz); +} + +LONG smartcard_unpack_get_device_type_id_call(SMARTCARD_DEVICE* smartcard, wStream* s, + GetDeviceTypeId_Call* call) +{ + return smartcard_unpack_common_context_and_string_w(smartcard, s, &call->handles.hContext, + &call->szReaderName); +} + +LONG smartcard_pack_device_type_id_return(SMARTCARD_DEVICE* smartcard, wStream* s, + const GetDeviceTypeId_Return* ret) +{ + smartcard_trace_device_type_id_return(smartcard, ret); + + if (!Stream_EnsureRemainingCapacity(s, 4)) + { + WLog_ERR(TAG, "Stream_EnsureRemainingCapacity failed!"); + return SCARD_F_INTERNAL_ERROR; + } + + Stream_Write_UINT32(s, ret->dwDeviceId); /* cBytes (4 bytes) */ + + return ret->ReturnCode; +} + +LONG smartcard_pack_locate_cards_return(SMARTCARD_DEVICE* smartcard, wStream* s, + const LocateCards_Return* ret) +{ + LONG status; + DWORD cbDataLen = ret->cReaders; + DWORD index = 0; + + smartcard_trace_locate_cards_return(smartcard, ret); + if (ret->ReturnCode != SCARD_S_SUCCESS) + cbDataLen = 0; + if (cbDataLen == SCARD_AUTOALLOCATE) + cbDataLen = 0; + + if (!Stream_EnsureRemainingCapacity(s, 4)) + { + WLog_ERR(TAG, "Stream_EnsureRemainingCapacity failed!"); + return SCARD_F_INTERNAL_ERROR; + } + + Stream_Write_UINT32(s, cbDataLen); /* cBytes (4 cbDataLen) */ + if (!smartcard_ndr_pointer_write(s, &index, cbDataLen)) + return SCARD_E_NO_MEMORY; + + status = smartcard_ndr_write_state(s, ret->rgReaderStates, cbDataLen, NDR_PTR_SIMPLE); + if (status != SCARD_S_SUCCESS) + return status; + return ret->ReturnCode; +} + +LONG smartcard_pack_get_reader_icon_return(SMARTCARD_DEVICE* smartcard, wStream* s, + const GetReaderIcon_Return* ret) +{ + LONG status; + DWORD index = 0; + DWORD cbDataLen = ret->cbDataLen; + smartcard_trace_get_reader_icon_return(smartcard, ret); + if (ret->ReturnCode != SCARD_S_SUCCESS) + cbDataLen = 0; + if (cbDataLen == SCARD_AUTOALLOCATE) + cbDataLen = 0; + + if (!Stream_EnsureRemainingCapacity(s, 4)) + { + WLog_ERR(TAG, "Stream_EnsureRemainingCapacity failed!"); + return SCARD_F_INTERNAL_ERROR; + } + + Stream_Write_UINT32(s, cbDataLen); /* cBytes (4 cbDataLen) */ + if (!smartcard_ndr_pointer_write(s, &index, cbDataLen)) + return SCARD_E_NO_MEMORY; + + status = smartcard_ndr_write(s, ret->pbData, cbDataLen, 1, NDR_PTR_SIMPLE); + if (status != SCARD_S_SUCCESS) + return status; + return ret->ReturnCode; +} + +LONG smartcard_pack_get_transmit_count_return(SMARTCARD_DEVICE* smartcard, wStream* s, + const GetTransmitCount_Return* ret) +{ + smartcard_trace_get_transmit_count_return(smartcard, ret); + + if (!Stream_EnsureRemainingCapacity(s, 4)) + { + WLog_ERR(TAG, "Stream_EnsureRemainingCapacity failed!"); + return SCARD_F_INTERNAL_ERROR; + } + + Stream_Write_UINT32(s, ret->cTransmitCount); /* cBytes (4 cbDataLen) */ + + return ret->ReturnCode; +} + +LONG smartcard_pack_read_cache_return(SMARTCARD_DEVICE* smartcard, wStream* s, + const ReadCache_Return* ret) +{ + LONG status; + DWORD index = 0; + DWORD cbDataLen = ret->cbDataLen; + smartcard_trace_read_cache_return(smartcard, ret); + if (ret->ReturnCode != SCARD_S_SUCCESS) + cbDataLen = 0; + + if (cbDataLen == SCARD_AUTOALLOCATE) + cbDataLen = 0; + + if (!Stream_EnsureRemainingCapacity(s, 4)) + { + WLog_ERR(TAG, "Stream_EnsureRemainingCapacity failed!"); + return SCARD_F_INTERNAL_ERROR; + } + + Stream_Write_UINT32(s, cbDataLen); /* cBytes (4 cbDataLen) */ + if (!smartcard_ndr_pointer_write(s, &index, cbDataLen)) + return SCARD_E_NO_MEMORY; + + status = smartcard_ndr_write(s, ret->pbData, cbDataLen, 1, NDR_PTR_SIMPLE); + if (status != SCARD_S_SUCCESS) + return status; + return ret->ReturnCode; +} diff --git a/channels/smartcard/client/smartcard_pack.h b/channels/smartcard/client/smartcard_pack.h new file mode 100644 index 0000000..82b1f3b --- /dev/null +++ b/channels/smartcard/client/smartcard_pack.h @@ -0,0 +1,196 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Smart Card Structure Packing + * + * Copyright 2014 Marc-Andre Moreau + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger + * Copyright 2020 Armin Novak + * Copyright 2020 Thincast Technologies GmbH + * + * Licensed 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 FREERDP_CHANNEL_SMARTCARD_CLIENT_PACK_H +#define FREERDP_CHANNEL_SMARTCARD_CLIENT_PACK_H + +#include +#include +#include + +#define SMARTCARD_COMMON_TYPE_HEADER_LENGTH 8 +#define SMARTCARD_PRIVATE_TYPE_HEADER_LENGTH 8 + +#include "smartcard_main.h" + +LONG smartcard_pack_write_size_align(SMARTCARD_DEVICE* smartcard, wStream* s, size_t size, + UINT32 alignment); +LONG smartcard_unpack_read_size_align(SMARTCARD_DEVICE* smartcard, wStream* s, size_t size, + UINT32 alignment); + +SCARDCONTEXT smartcard_scard_context_native_from_redir(SMARTCARD_DEVICE* smartcard, + REDIR_SCARDCONTEXT* context); +void smartcard_scard_context_native_to_redir(SMARTCARD_DEVICE* smartcard, + REDIR_SCARDCONTEXT* context, SCARDCONTEXT hContext); + +SCARDHANDLE smartcard_scard_handle_native_from_redir(SMARTCARD_DEVICE* smartcard, + REDIR_SCARDHANDLE* handle); +void smartcard_scard_handle_native_to_redir(SMARTCARD_DEVICE* smartcard, REDIR_SCARDHANDLE* handle, + SCARDHANDLE hCard); + +LONG smartcard_unpack_common_type_header(SMARTCARD_DEVICE* smartcard, wStream* s); +void smartcard_pack_common_type_header(SMARTCARD_DEVICE* smartcard, wStream* s); + +LONG smartcard_unpack_private_type_header(SMARTCARD_DEVICE* smartcard, wStream* s); +void smartcard_pack_private_type_header(SMARTCARD_DEVICE* smartcard, wStream* s, + UINT32 objectBufferLength); + +LONG smartcard_unpack_establish_context_call(SMARTCARD_DEVICE* smartcard, wStream* s, + EstablishContext_Call* call); + +LONG smartcard_pack_establish_context_return(SMARTCARD_DEVICE* smartcard, wStream* s, + const EstablishContext_Return* ret); + +LONG smartcard_unpack_context_call(SMARTCARD_DEVICE* smartcard, wStream* s, Context_Call* call, + const char* name); + +void smartcard_trace_long_return(SMARTCARD_DEVICE* smartcard, const Long_Return* ret, + const char* name); + +LONG smartcard_unpack_list_reader_groups_call(SMARTCARD_DEVICE* smartcard, wStream* s, + ListReaderGroups_Call* call, BOOL unicode); + +LONG smartcard_pack_list_reader_groups_return(SMARTCARD_DEVICE* smartcard, wStream* s, + const ListReaderGroups_Return* ret, BOOL unicode); + +LONG smartcard_unpack_list_readers_call(SMARTCARD_DEVICE* smartcard, wStream* s, + ListReaders_Call* call, BOOL unicode); + +LONG smartcard_pack_list_readers_return(SMARTCARD_DEVICE* smartcard, wStream* s, + const ListReaders_Return* ret, BOOL unicode); + +LONG smartcard_unpack_context_and_two_strings_a_call(SMARTCARD_DEVICE* smartcard, wStream* s, + ContextAndTwoStringA_Call* call); + +LONG smartcard_unpack_context_and_two_strings_w_call(SMARTCARD_DEVICE* smartcard, wStream* s, + ContextAndTwoStringW_Call* call); + +LONG smartcard_unpack_context_and_string_a_call(SMARTCARD_DEVICE* smartcard, wStream* s, + ContextAndStringA_Call* call); + +LONG smartcard_unpack_context_and_string_w_call(SMARTCARD_DEVICE* smartcard, wStream* s, + ContextAndStringW_Call* call); + +LONG smartcard_unpack_locate_cards_a_call(SMARTCARD_DEVICE* smartcard, wStream* s, + LocateCardsA_Call* call); + +LONG smartcard_pack_locate_cards_return(SMARTCARD_DEVICE* smartcard, wStream* s, + const LocateCards_Return* ret); + +LONG smartcard_unpack_locate_cards_w_call(SMARTCARD_DEVICE* smartcard, wStream* s, + LocateCardsW_Call* call); + +LONG smartcard_pack_locate_cards_w_return(SMARTCARD_DEVICE* smartcard, wStream* s, + const LocateCardsW_Call* ret); + +LONG smartcard_unpack_connect_a_call(SMARTCARD_DEVICE* smartcard, wStream* s, ConnectA_Call* call); + +LONG smartcard_unpack_connect_w_call(SMARTCARD_DEVICE* smartcard, wStream* s, ConnectW_Call* call); + +LONG smartcard_pack_connect_return(SMARTCARD_DEVICE* smartcard, wStream* s, + const Connect_Return* ret); + +LONG smartcard_unpack_reconnect_call(SMARTCARD_DEVICE* smartcard, wStream* s, Reconnect_Call* call); + +LONG smartcard_pack_reconnect_return(SMARTCARD_DEVICE* smartcard, wStream* s, + const Reconnect_Return* ret); + +LONG smartcard_unpack_hcard_and_disposition_call(SMARTCARD_DEVICE* smartcard, wStream* s, + HCardAndDisposition_Call* call, const char* name); + +LONG smartcard_unpack_get_status_change_a_call(SMARTCARD_DEVICE* smartcard, wStream* s, + GetStatusChangeA_Call* call); + +LONG smartcard_unpack_get_status_change_w_call(SMARTCARD_DEVICE* smartcard, wStream* s, + GetStatusChangeW_Call* call); + +LONG smartcard_pack_get_status_change_return(SMARTCARD_DEVICE* smartcard, wStream* s, + const GetStatusChange_Return* ret, BOOL unicode); + +LONG smartcard_unpack_state_call(SMARTCARD_DEVICE* smartcard, wStream* s, State_Call* call); +LONG smartcard_pack_state_return(SMARTCARD_DEVICE* smartcard, wStream* s, const State_Return* ret); + +LONG smartcard_unpack_status_call(SMARTCARD_DEVICE* smartcard, wStream* s, Status_Call* call, + BOOL unicode); + +LONG smartcard_pack_status_return(SMARTCARD_DEVICE* smartcard, wStream* s, const Status_Return* ret, + BOOL unicode); + +LONG smartcard_unpack_get_attrib_call(SMARTCARD_DEVICE* smartcard, wStream* s, + GetAttrib_Call* call); + +LONG smartcard_pack_get_attrib_return(SMARTCARD_DEVICE* smartcard, wStream* s, + const GetAttrib_Return* ret, DWORD dwAttrId, + DWORD cbAttrCallLen); + +LONG smartcard_unpack_set_attrib_call(SMARTCARD_DEVICE* smartcard, wStream* s, + SetAttrib_Call* call); + +LONG smartcard_unpack_control_call(SMARTCARD_DEVICE* smartcard, wStream* s, Control_Call* call); + +LONG smartcard_pack_control_return(SMARTCARD_DEVICE* smartcard, wStream* s, + const Control_Return* ret); + +LONG smartcard_unpack_transmit_call(SMARTCARD_DEVICE* smartcard, wStream* s, Transmit_Call* call); + +LONG smartcard_pack_transmit_return(SMARTCARD_DEVICE* smartcard, wStream* s, + const Transmit_Return* ret); + +LONG smartcard_unpack_locate_cards_by_atr_a_call(SMARTCARD_DEVICE* smartcard, wStream* s, + LocateCardsByATRA_Call* call); + +LONG smartcard_unpack_locate_cards_by_atr_w_call(SMARTCARD_DEVICE* smartcard, wStream* s, + LocateCardsByATRW_Call* call); + +LONG smartcard_unpack_read_cache_a_call(SMARTCARD_DEVICE* smartcard, wStream* s, + ReadCacheA_Call* call); + +LONG smartcard_unpack_read_cache_w_call(SMARTCARD_DEVICE* smartcard, wStream* s, + ReadCacheW_Call* call); + +LONG smartcard_pack_read_cache_return(SMARTCARD_DEVICE* smartcard, wStream* s, + const ReadCache_Return* ret); + +LONG smartcard_unpack_write_cache_a_call(SMARTCARD_DEVICE* smartcard, wStream* s, + WriteCacheA_Call* call); + +LONG smartcard_unpack_write_cache_w_call(SMARTCARD_DEVICE* smartcard, wStream* s, + WriteCacheW_Call* call); + +LONG smartcard_unpack_get_transmit_count_call(SMARTCARD_DEVICE* smartcard, wStream* s, + GetTransmitCount_Call* call); +LONG smartcard_pack_get_transmit_count_return(SMARTCARD_DEVICE* smartcard, wStream* s, + const GetTransmitCount_Return* call); + +LONG smartcard_unpack_get_reader_icon_call(SMARTCARD_DEVICE* smartcard, wStream* s, + GetReaderIcon_Call* call); +LONG smartcard_pack_get_reader_icon_return(SMARTCARD_DEVICE* smartcard, wStream* s, + const GetReaderIcon_Return* ret); + +LONG smartcard_unpack_get_device_type_id_call(SMARTCARD_DEVICE* smartcard, wStream* s, + GetDeviceTypeId_Call* call); + +LONG smartcard_pack_device_type_id_return(SMARTCARD_DEVICE* smartcard, wStream* s, + const GetDeviceTypeId_Return* ret); + +#endif /* FREERDP_CHANNEL_SMARTCARD_CLIENT_PACK_H */ diff --git a/channels/sshagent/CMakeLists.txt b/channels/sshagent/CMakeLists.txt new file mode 100644 index 0000000..71aab99 --- /dev/null +++ b/channels/sshagent/CMakeLists.txt @@ -0,0 +1,23 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2012 Marc-Andre Moreau +# Copyright 2017 Ben Cohen +# +# Licensed 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. + +define_channel("sshagent") + +if(WITH_CLIENT_CHANNELS) + add_channel_client(${MODULE_PREFIX} ${CHANNEL_NAME}) +endif() diff --git a/channels/sshagent/ChannelOptions.cmake b/channels/sshagent/ChannelOptions.cmake new file mode 100644 index 0000000..41b5a21 --- /dev/null +++ b/channels/sshagent/ChannelOptions.cmake @@ -0,0 +1,12 @@ + +set(OPTION_DEFAULT OFF) +set(OPTION_CLIENT_DEFAULT OFF) +set(OPTION_SERVER_DEFAULT OFF) + +define_channel_options(NAME "sshagent" TYPE "dynamic" + DESCRIPTION "SSH Agent Forwarding (experimental)" + SPECIFICATIONS "" + DEFAULT ${OPTION_DEFAULT}) + +define_channel_client_options(${OPTION_CLIENT_DEFAULT}) + diff --git a/channels/sshagent/client/CMakeLists.txt b/channels/sshagent/client/CMakeLists.txt new file mode 100644 index 0000000..7feea96 --- /dev/null +++ b/channels/sshagent/client/CMakeLists.txt @@ -0,0 +1,34 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2012 Marc-Andre Moreau +# Copyright 2017 Ben Cohen +# +# Licensed 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. + +define_channel_client("sshagent") + +set(${MODULE_PREFIX}_SRCS + sshagent_main.c + sshagent_main.h) + +include_directories(..) + +add_channel_client_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} TRUE "DVCPluginEntry") + +if (WITH_DEBUG_SYMBOLS AND MSVC AND NOT BUILTIN_CHANNELS AND BUILD_SHARED_LIBS) + install(FILES ${CMAKE_BINARY_DIR}/${MODULE_NAME}.pdb DESTINATION ${FREERDP_ADDIN_PATH} COMPONENT symbols) +endif() + +target_link_libraries(${MODULE_NAME} winpr) +set_property(TARGET ${MODULE_NAME} PROPERTY FOLDER "Channels/${CHANNEL_NAME}/Client") diff --git a/channels/sshagent/client/sshagent_main.c b/channels/sshagent/client/sshagent_main.c new file mode 100644 index 0000000..aa7e632 --- /dev/null +++ b/channels/sshagent/client/sshagent_main.c @@ -0,0 +1,388 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * SSH Agent Virtual Channel Extension + * + * Copyright 2013 Christian Hofstaedtler + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger + * Copyright 2017 Ben Cohen + * + * Licensed 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. + */ + +/* + * sshagent_main.c: DVC plugin to forward queries from RDP to the ssh-agent + * + * This relays data to and from an ssh-agent program equivalent running on the + * RDP server to an ssh-agent running locally. Unlike the normal ssh-agent, + * which sends data over an SSH channel, the data is send over an RDP dynamic + * virtual channel. + * + * protocol specification: + * Forward data verbatim over RDP dynamic virtual channel named "sshagent" + * between a ssh client on the xrdp server and the real ssh-agent where + * the RDP client is running. Each connection by a separate client to + * xrdp-ssh-agent gets a separate DVC invocation. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "sshagent_main.h" +#include + +#define TAG CHANNELS_TAG("sshagent.client") + +typedef struct _SSHAGENT_LISTENER_CALLBACK SSHAGENT_LISTENER_CALLBACK; +struct _SSHAGENT_LISTENER_CALLBACK +{ + IWTSListenerCallback iface; + + IWTSPlugin* plugin; + IWTSVirtualChannelManager* channel_mgr; + + rdpContext* rdpcontext; + const char* agent_uds_path; +}; + +typedef struct _SSHAGENT_CHANNEL_CALLBACK SSHAGENT_CHANNEL_CALLBACK; +struct _SSHAGENT_CHANNEL_CALLBACK +{ + IWTSVirtualChannelCallback iface; + + IWTSPlugin* plugin; + IWTSVirtualChannelManager* channel_mgr; + IWTSVirtualChannel* channel; + + rdpContext* rdpcontext; + int agent_fd; + HANDLE thread; + CRITICAL_SECTION lock; +}; + +typedef struct _SSHAGENT_PLUGIN SSHAGENT_PLUGIN; +struct _SSHAGENT_PLUGIN +{ + IWTSPlugin iface; + + SSHAGENT_LISTENER_CALLBACK* listener_callback; + + rdpContext* rdpcontext; +}; + +/** + * Function to open the connection to the sshagent + * + * @return The fd on success, otherwise -1 + */ +static int connect_to_sshagent(const char* udspath) +{ + int agent_fd = socket(AF_UNIX, SOCK_STREAM, 0); + + if (agent_fd == -1) + { + WLog_ERR(TAG, "Can't open Unix domain socket!"); + return -1; + } + + struct sockaddr_un addr; + + memset(&addr, 0, sizeof(addr)); + + addr.sun_family = AF_UNIX; + + strncpy(addr.sun_path, udspath, sizeof(addr.sun_path) - 1); + + int rc = connect(agent_fd, (struct sockaddr*)&addr, sizeof(addr)); + + if (rc != 0) + { + WLog_ERR(TAG, "Can't connect to Unix domain socket \"%s\"!", udspath); + close(agent_fd); + return -1; + } + + return agent_fd; +} + +/** + * Entry point for thread to read from the ssh-agent socket and forward + * the data to RDP + * + * @return NULL + */ +static DWORD WINAPI sshagent_read_thread(LPVOID data) +{ + SSHAGENT_CHANNEL_CALLBACK* callback = (SSHAGENT_CHANNEL_CALLBACK*)data; + BYTE buffer[4096]; + int going = 1; + UINT status = CHANNEL_RC_OK; + + while (going) + { + int bytes_read = read(callback->agent_fd, buffer, sizeof(buffer)); + + if (bytes_read == 0) + { + /* Socket closed cleanly at other end */ + going = 0; + } + else if (bytes_read < 0) + { + if (errno != EINTR) + { + WLog_ERR(TAG, "Error reading from sshagent, errno=%d", errno); + status = ERROR_READ_FAULT; + going = 0; + } + } + else + { + /* Something read: forward to virtual channel */ + status = callback->channel->Write(callback->channel, bytes_read, buffer, NULL); + + if (status != CHANNEL_RC_OK) + { + going = 0; + } + } + } + + close(callback->agent_fd); + + if (status != CHANNEL_RC_OK) + setChannelError(callback->rdpcontext, status, "sshagent_read_thread reported an error"); + + ExitThread(status); + return status; +} + +/** + * Callback for data received from the RDP server; forward this to ssh-agent + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT sshagent_on_data_received(IWTSVirtualChannelCallback* pChannelCallback, wStream* data) +{ + SSHAGENT_CHANNEL_CALLBACK* callback = (SSHAGENT_CHANNEL_CALLBACK*)pChannelCallback; + BYTE* pBuffer = Stream_Pointer(data); + UINT32 cbSize = Stream_GetRemainingLength(data); + BYTE* pos = pBuffer; + /* Forward what we have received to the ssh agent */ + UINT32 bytes_to_write = cbSize; + errno = 0; + + while (bytes_to_write > 0) + { + int bytes_written = write(callback->agent_fd, pos, bytes_to_write); + + if (bytes_written < 0) + { + if (errno != EINTR) + { + WLog_ERR(TAG, "Error writing to sshagent, errno=%d", errno); + return ERROR_WRITE_FAULT; + } + } + else + { + bytes_to_write -= bytes_written; + pos += bytes_written; + } + } + + /* Consume stream */ + Stream_Seek(data, cbSize); + return CHANNEL_RC_OK; +} + +/** + * Callback for when the virtual channel is closed + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT sshagent_on_close(IWTSVirtualChannelCallback* pChannelCallback) +{ + SSHAGENT_CHANNEL_CALLBACK* callback = (SSHAGENT_CHANNEL_CALLBACK*)pChannelCallback; + /* Call shutdown() to wake up the read() in sshagent_read_thread(). */ + shutdown(callback->agent_fd, SHUT_RDWR); + EnterCriticalSection(&callback->lock); + + if (WaitForSingleObject(callback->thread, INFINITE) == WAIT_FAILED) + { + UINT error = GetLastError(); + WLog_ERR(TAG, "WaitForSingleObject failed with error %" PRIu32 "!", error); + return error; + } + + CloseHandle(callback->thread); + LeaveCriticalSection(&callback->lock); + DeleteCriticalSection(&callback->lock); + free(callback); + return CHANNEL_RC_OK; +} + +/** + * Callback for when a new virtual channel is opened + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT sshagent_on_new_channel_connection(IWTSListenerCallback* pListenerCallback, + IWTSVirtualChannel* pChannel, BYTE* Data, + BOOL* pbAccept, + IWTSVirtualChannelCallback** ppCallback) +{ + SSHAGENT_CHANNEL_CALLBACK* callback; + SSHAGENT_LISTENER_CALLBACK* listener_callback = (SSHAGENT_LISTENER_CALLBACK*)pListenerCallback; + callback = (SSHAGENT_CHANNEL_CALLBACK*)calloc(1, sizeof(SSHAGENT_CHANNEL_CALLBACK)); + + if (!callback) + { + WLog_ERR(TAG, "calloc failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + /* Now open a connection to the local ssh-agent. Do this for each + * connection to the plugin in case we mess up the agent session. */ + callback->agent_fd = connect_to_sshagent(listener_callback->agent_uds_path); + + if (callback->agent_fd == -1) + { + free(callback); + return CHANNEL_RC_INITIALIZATION_ERROR; + } + + InitializeCriticalSection(&callback->lock); + callback->iface.OnDataReceived = sshagent_on_data_received; + callback->iface.OnClose = sshagent_on_close; + callback->plugin = listener_callback->plugin; + callback->channel_mgr = listener_callback->channel_mgr; + callback->channel = pChannel; + callback->rdpcontext = listener_callback->rdpcontext; + callback->thread = CreateThread(NULL, 0, sshagent_read_thread, (void*)callback, 0, NULL); + + if (!callback->thread) + { + WLog_ERR(TAG, "CreateThread failed!"); + DeleteCriticalSection(&callback->lock); + free(callback); + return CHANNEL_RC_INITIALIZATION_ERROR; + } + + *ppCallback = (IWTSVirtualChannelCallback*)callback; + return CHANNEL_RC_OK; +} + +/** + * Callback for when the plugin is initialised + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT sshagent_plugin_initialize(IWTSPlugin* pPlugin, IWTSVirtualChannelManager* pChannelMgr) +{ + SSHAGENT_PLUGIN* sshagent = (SSHAGENT_PLUGIN*)pPlugin; + sshagent->listener_callback = + (SSHAGENT_LISTENER_CALLBACK*)calloc(1, sizeof(SSHAGENT_LISTENER_CALLBACK)); + + if (!sshagent->listener_callback) + { + WLog_ERR(TAG, "calloc failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + sshagent->listener_callback->rdpcontext = sshagent->rdpcontext; + sshagent->listener_callback->iface.OnNewChannelConnection = sshagent_on_new_channel_connection; + sshagent->listener_callback->plugin = pPlugin; + sshagent->listener_callback->channel_mgr = pChannelMgr; + sshagent->listener_callback->agent_uds_path = getenv("SSH_AUTH_SOCK"); + + if (sshagent->listener_callback->agent_uds_path == NULL) + { + WLog_ERR(TAG, "Environment variable $SSH_AUTH_SOCK undefined!"); + free(sshagent->listener_callback); + sshagent->listener_callback = NULL; + return CHANNEL_RC_INITIALIZATION_ERROR; + } + + return pChannelMgr->CreateListener(pChannelMgr, "SSHAGENT", 0, + (IWTSListenerCallback*)sshagent->listener_callback, NULL); +} + +/** + * Callback for when the plugin is terminated + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT sshagent_plugin_terminated(IWTSPlugin* pPlugin) +{ + SSHAGENT_PLUGIN* sshagent = (SSHAGENT_PLUGIN*)pPlugin; + free(sshagent); + return CHANNEL_RC_OK; +} + +#ifdef BUILTIN_CHANNELS +#define DVCPluginEntry sshagent_DVCPluginEntry +#else +#define DVCPluginEntry FREERDP_API DVCPluginEntry +#endif + +/** + * Main entry point for sshagent DVC plugin + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT DVCPluginEntry(IDRDYNVC_ENTRY_POINTS* pEntryPoints) +{ + UINT status = CHANNEL_RC_OK; + SSHAGENT_PLUGIN* sshagent; + sshagent = (SSHAGENT_PLUGIN*)pEntryPoints->GetPlugin(pEntryPoints, "sshagent"); + + if (!sshagent) + { + sshagent = (SSHAGENT_PLUGIN*)calloc(1, sizeof(SSHAGENT_PLUGIN)); + + if (!sshagent) + { + WLog_ERR(TAG, "calloc failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + sshagent->iface.Initialize = sshagent_plugin_initialize; + sshagent->iface.Connected = NULL; + sshagent->iface.Disconnected = NULL; + sshagent->iface.Terminated = sshagent_plugin_terminated; + sshagent->rdpcontext = + ((freerdp*)((rdpSettings*)pEntryPoints->GetRdpSettings(pEntryPoints))->instance) + ->context; + status = pEntryPoints->RegisterPlugin(pEntryPoints, "sshagent", (IWTSPlugin*)sshagent); + } + + return status; +} + +/* vim: set sw=8:ts=8:noet: */ diff --git a/channels/sshagent/client/sshagent_main.h b/channels/sshagent/client/sshagent_main.h new file mode 100644 index 0000000..550b2b7 --- /dev/null +++ b/channels/sshagent/client/sshagent_main.h @@ -0,0 +1,44 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * SSH Agent Virtual Channel Extension + * + * Copyright 2013 Christian Hofstaedtler + * Copyright 2017 Ben Cohen + * + * Licensed 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 SSHAGENT_MAIN_H +#define SSHAGENT_MAIN_H + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include + +#include +#include +#include + +#define DVC_TAG CHANNELS_TAG("sshagent.client") +#ifdef WITH_DEBUG_SSHAGENT +#define DEBUG_SSHAGENT(...) WLog_DBG(DVC_TAG, __VA_ARGS__) +#else +#define DEBUG_SSHAGENT(...) \ + do \ + { \ + } while (0) +#endif + +#endif /* SSHAGENT_MAIN_H */ diff --git a/channels/telemetry/CMakeLists.txt b/channels/telemetry/CMakeLists.txt new file mode 100644 index 0000000..d2b4f24 --- /dev/null +++ b/channels/telemetry/CMakeLists.txt @@ -0,0 +1,22 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2022 Pascal Nowack +# +# Licensed 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. + +define_channel("telemetry") + +if(WITH_SERVER_CHANNELS) + add_channel_server(${MODULE_PREFIX} ${CHANNEL_NAME}) +endif() diff --git a/channels/telemetry/ChannelOptions.cmake b/channels/telemetry/ChannelOptions.cmake new file mode 100644 index 0000000..1b9e391 --- /dev/null +++ b/channels/telemetry/ChannelOptions.cmake @@ -0,0 +1,12 @@ + +set(OPTION_DEFAULT OFF) +set(OPTION_CLIENT_DEFAULT OFF) +set(OPTION_SERVER_DEFAULT ON) + +define_channel_options(NAME "telemetry" TYPE "dynamic" + DESCRIPTION "Telemetry Virtual Channel Extension" + SPECIFICATIONS "[MS-RDPET]" + DEFAULT ${OPTION_DEFAULT}) + +define_channel_server_options(${OPTION_SERVER_DEFAULT}) + diff --git a/channels/telemetry/server/CMakeLists.txt b/channels/telemetry/server/CMakeLists.txt new file mode 100644 index 0000000..75be8ac --- /dev/null +++ b/channels/telemetry/server/CMakeLists.txt @@ -0,0 +1,26 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2022 Pascal Nowack +# +# Licensed 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. + +define_channel_server("telemetry") + +set(${MODULE_PREFIX}_SRCS + telemetry_main.c) + +add_channel_server_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} FALSE "DVCPluginEntry") + +target_link_libraries(${MODULE_NAME} freerdp) +set_property(TARGET ${MODULE_NAME} PROPERTY FOLDER "Channels/${CHANNEL_NAME}/Server") diff --git a/channels/telemetry/server/telemetry_main.c b/channels/telemetry/server/telemetry_main.c new file mode 100644 index 0000000..7173603 --- /dev/null +++ b/channels/telemetry/server/telemetry_main.c @@ -0,0 +1,443 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Telemetry Virtual Channel Extension + * + * Copyright 2022 Pascal Nowack + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include + +#define TAG CHANNELS_TAG("telemetry.server") + +typedef enum +{ + TELEMETRY_INITIAL, + TELEMETRY_OPENED, +} eTelemetryChannelState; + +typedef struct +{ + TelemetryServerContext context; + + HANDLE stopEvent; + + HANDLE thread; + void* telemetry_channel; + + DWORD SessionId; + + BOOL isOpened; + BOOL externalThread; + + /* Channel state */ + eTelemetryChannelState state; + + wStream* buffer; +} telemetry_server; + +static UINT telemetry_server_initialize(TelemetryServerContext* context, BOOL externalThread) +{ + UINT error = CHANNEL_RC_OK; + telemetry_server* telemetry = (telemetry_server*)context; + + WINPR_ASSERT(telemetry); + + if (telemetry->isOpened) + { + WLog_WARN(TAG, "Application error: TELEMETRY channel already initialized, " + "calling in this state is not possible!"); + return ERROR_INVALID_STATE; + } + + telemetry->externalThread = externalThread; + + return error; +} + +static UINT telemetry_server_open_channel(telemetry_server* telemetry) +{ + TelemetryServerContext* context = &telemetry->context; + DWORD Error = ERROR_SUCCESS; + HANDLE hEvent; + DWORD BytesReturned = 0; + PULONG pSessionId = NULL; + UINT32 channelId; + BOOL status = TRUE; + + WINPR_ASSERT(telemetry); + + if (WTSQuerySessionInformationA(telemetry->context.vcm, WTS_CURRENT_SESSION, WTSSessionId, + (LPSTR*)&pSessionId, &BytesReturned) == FALSE) + { + WLog_ERR(TAG, "WTSQuerySessionInformationA failed!"); + return ERROR_INTERNAL_ERROR; + } + + telemetry->SessionId = (DWORD)*pSessionId; + WTSFreeMemory(pSessionId); + hEvent = WTSVirtualChannelManagerGetEventHandle(telemetry->context.vcm); + + if (WaitForSingleObject(hEvent, 1000) == WAIT_FAILED) + { + Error = GetLastError(); + WLog_ERR(TAG, "WaitForSingleObject failed with error %" PRIu32 "!", Error); + return Error; + } + + telemetry->telemetry_channel = WTSVirtualChannelOpenEx( + telemetry->SessionId, TELEMETRY_DVC_CHANNEL_NAME, WTS_CHANNEL_OPTION_DYNAMIC); + if (!telemetry->telemetry_channel) + { + Error = GetLastError(); + WLog_ERR(TAG, "WTSVirtualChannelOpenEx failed with error %" PRIu32 "!", Error); + return Error; + } + + channelId = WTSChannelGetIdByHandle(telemetry->telemetry_channel); + + IFCALLRET(context->ChannelIdAssigned, status, context, channelId); + if (!status) + { + WLog_ERR(TAG, "context->ChannelIdAssigned failed!"); + return ERROR_INTERNAL_ERROR; + } + + return Error; +} + +static UINT telemetry_server_recv_rdp_telemetry_pdu(TelemetryServerContext* context, wStream* s) +{ + TELEMETRY_RDP_TELEMETRY_PDU pdu; + UINT error = CHANNEL_RC_OK; + + if (Stream_GetRemainingLength(s) < 16) + { + WLog_ERR(TAG, "telemetry_server_recv_rdp_telemetry_pdu: Not enough data!"); + return ERROR_NO_DATA; + } + + Stream_Read_UINT32(s, pdu.PromptForCredentialsMillis); + Stream_Read_UINT32(s, pdu.PromptForCredentialsDoneMillis); + Stream_Read_UINT32(s, pdu.GraphicsChannelOpenedMillis); + Stream_Read_UINT32(s, pdu.FirstGraphicsReceivedMillis); + + IFCALLRET(context->RdpTelemetry, error, context, &pdu); + if (error) + WLog_ERR(TAG, "context->RdpTelemetry failed with error %" PRIu32 "", error); + + return error; +} + +static UINT telemetry_process_message(telemetry_server* telemetry) +{ + BOOL rc; + UINT error = ERROR_INTERNAL_ERROR; + ULONG BytesReturned; + BYTE MessageId; + BYTE Length; + wStream* s; + + WINPR_ASSERT(telemetry); + WINPR_ASSERT(telemetry->telemetry_channel); + + s = telemetry->buffer; + WINPR_ASSERT(s); + + Stream_SetPosition(s, 0); + rc = WTSVirtualChannelRead(telemetry->telemetry_channel, 0, NULL, 0, &BytesReturned); + if (!rc) + goto out; + + if (BytesReturned < 1) + { + error = CHANNEL_RC_OK; + goto out; + } + + if (!Stream_EnsureRemainingCapacity(s, BytesReturned)) + { + WLog_ERR(TAG, "Stream_EnsureRemainingCapacity failed!"); + error = CHANNEL_RC_NO_MEMORY; + goto out; + } + + if (WTSVirtualChannelRead(telemetry->telemetry_channel, 0, (PCHAR)Stream_Buffer(s), + (ULONG)Stream_Capacity(s), &BytesReturned) == FALSE) + { + WLog_ERR(TAG, "WTSVirtualChannelRead failed!"); + goto out; + } + + Stream_SetLength(s, BytesReturned); + if (!Stream_CheckAndLogRequiredLength(TAG, s, 2)) + return ERROR_NO_DATA; + + Stream_Read_UINT8(s, MessageId); + Stream_Read_UINT8(s, Length); + + switch (MessageId) + { + case 0x01: + error = telemetry_server_recv_rdp_telemetry_pdu(&telemetry->context, s); + break; + default: + WLog_ERR(TAG, "telemetry_process_message: unknown MessageId %" PRIu8 "", MessageId); + break; + } + +out: + if (error) + WLog_ERR(TAG, "Response failed with error %" PRIu32 "!", error); + + return error; +} + +static UINT telemetry_server_context_poll_int(TelemetryServerContext* context) +{ + telemetry_server* telemetry = (telemetry_server*)context; + UINT error = ERROR_INTERNAL_ERROR; + + WINPR_ASSERT(telemetry); + + switch (telemetry->state) + { + case TELEMETRY_INITIAL: + error = telemetry_server_open_channel(telemetry); + if (error) + WLog_ERR(TAG, "telemetry_server_open_channel failed with error %" PRIu32 "!", + error); + else + telemetry->state = TELEMETRY_OPENED; + break; + case TELEMETRY_OPENED: + error = telemetry_process_message(telemetry); + break; + } + + return error; +} + +static HANDLE telemetry_server_get_channel_handle(telemetry_server* telemetry) +{ + void* buffer = NULL; + DWORD BytesReturned = 0; + HANDLE ChannelEvent = NULL; + + WINPR_ASSERT(telemetry); + + if (WTSVirtualChannelQuery(telemetry->telemetry_channel, WTSVirtualEventHandle, &buffer, + &BytesReturned) == TRUE) + { + if (BytesReturned == sizeof(HANDLE)) + CopyMemory(&ChannelEvent, buffer, sizeof(HANDLE)); + + WTSFreeMemory(buffer); + } + + return ChannelEvent; +} + +static DWORD WINAPI telemetry_server_thread_func(LPVOID arg) +{ + DWORD nCount; + HANDLE events[2] = { 0 }; + telemetry_server* telemetry = (telemetry_server*)arg; + UINT error = CHANNEL_RC_OK; + DWORD status; + + WINPR_ASSERT(telemetry); + + nCount = 0; + events[nCount++] = telemetry->stopEvent; + + while ((error == CHANNEL_RC_OK) && (WaitForSingleObject(events[0], 0) != WAIT_OBJECT_0)) + { + switch (telemetry->state) + { + case TELEMETRY_INITIAL: + error = telemetry_server_context_poll_int(&telemetry->context); + if (error == CHANNEL_RC_OK) + { + events[1] = telemetry_server_get_channel_handle(telemetry); + nCount = 2; + } + break; + case TELEMETRY_OPENED: + status = WaitForMultipleObjects(nCount, events, FALSE, INFINITE); + switch (status) + { + case WAIT_OBJECT_0: + break; + case WAIT_OBJECT_0 + 1: + case WAIT_TIMEOUT: + error = telemetry_server_context_poll_int(&telemetry->context); + break; + + case WAIT_FAILED: + default: + error = ERROR_INTERNAL_ERROR; + break; + } + break; + } + } + + WTSVirtualChannelClose(telemetry->telemetry_channel); + telemetry->telemetry_channel = NULL; + + if (error && telemetry->context.rdpcontext) + setChannelError(telemetry->context.rdpcontext, error, + "telemetry_server_thread_func reported an error"); + + ExitThread(error); + return error; +} + +static UINT telemetry_server_open(TelemetryServerContext* context) +{ + telemetry_server* telemetry = (telemetry_server*)context; + + WINPR_ASSERT(telemetry); + + if (!telemetry->externalThread && (telemetry->thread == NULL)) + { + telemetry->stopEvent = CreateEvent(NULL, TRUE, FALSE, NULL); + if (!telemetry->stopEvent) + { + WLog_ERR(TAG, "CreateEvent failed!"); + return ERROR_INTERNAL_ERROR; + } + + telemetry->thread = CreateThread(NULL, 0, telemetry_server_thread_func, telemetry, 0, NULL); + if (!telemetry->thread) + { + WLog_ERR(TAG, "CreateThread failed!"); + CloseHandle(telemetry->stopEvent); + telemetry->stopEvent = NULL; + return ERROR_INTERNAL_ERROR; + } + } + telemetry->isOpened = TRUE; + + return CHANNEL_RC_OK; +} + +static UINT telemetry_server_close(TelemetryServerContext* context) +{ + UINT error = CHANNEL_RC_OK; + telemetry_server* telemetry = (telemetry_server*)context; + + WINPR_ASSERT(telemetry); + + if (!telemetry->externalThread && telemetry->thread) + { + SetEvent(telemetry->stopEvent); + + if (WaitForSingleObject(telemetry->thread, INFINITE) == WAIT_FAILED) + { + error = GetLastError(); + WLog_ERR(TAG, "WaitForSingleObject failed with error %" PRIu32 "", error); + return error; + } + + CloseHandle(telemetry->thread); + CloseHandle(telemetry->stopEvent); + telemetry->thread = NULL; + telemetry->stopEvent = NULL; + } + if (telemetry->externalThread) + { + if (telemetry->state != TELEMETRY_INITIAL) + { + WTSVirtualChannelClose(telemetry->telemetry_channel); + telemetry->telemetry_channel = NULL; + telemetry->state = TELEMETRY_INITIAL; + } + } + telemetry->isOpened = FALSE; + + return error; +} + +static UINT telemetry_server_context_poll(TelemetryServerContext* context) +{ + telemetry_server* telemetry = (telemetry_server*)context; + + WINPR_ASSERT(telemetry); + + if (!telemetry->externalThread) + return ERROR_INTERNAL_ERROR; + + return telemetry_server_context_poll_int(context); +} + +static BOOL telemetry_server_context_handle(TelemetryServerContext* context, HANDLE* handle) +{ + telemetry_server* telemetry = (telemetry_server*)context; + + WINPR_ASSERT(telemetry); + WINPR_ASSERT(handle); + + if (!telemetry->externalThread) + return FALSE; + if (telemetry->state == TELEMETRY_INITIAL) + return FALSE; + + *handle = telemetry_server_get_channel_handle(telemetry); + + return TRUE; +} + +TelemetryServerContext* telemetry_server_context_new(HANDLE vcm) +{ + telemetry_server* telemetry = (telemetry_server*)calloc(1, sizeof(telemetry_server)); + + if (!telemetry) + return NULL; + + telemetry->context.vcm = vcm; + telemetry->context.Initialize = telemetry_server_initialize; + telemetry->context.Open = telemetry_server_open; + telemetry->context.Close = telemetry_server_close; + telemetry->context.Poll = telemetry_server_context_poll; + telemetry->context.ChannelHandle = telemetry_server_context_handle; + + telemetry->buffer = Stream_New(NULL, 4096); + if (!telemetry->buffer) + goto fail; + + return &telemetry->context; +fail: + telemetry_server_context_free(&telemetry->context); + return NULL; +} + +void telemetry_server_context_free(TelemetryServerContext* context) +{ + telemetry_server* telemetry = (telemetry_server*)context; + + if (telemetry) + { + telemetry_server_close(context); + Stream_Free(telemetry->buffer, TRUE); + } + + free(telemetry); +} diff --git a/channels/tsmf/CMakeLists.txt b/channels/tsmf/CMakeLists.txt new file mode 100644 index 0000000..8b4073e --- /dev/null +++ b/channels/tsmf/CMakeLists.txt @@ -0,0 +1,22 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2012 Marc-Andre Moreau +# +# Licensed 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. + +define_channel("tsmf") + +if(WITH_CLIENT_CHANNELS) + add_channel_client(${MODULE_PREFIX} ${CHANNEL_NAME}) +endif() diff --git a/channels/tsmf/ChannelOptions.cmake b/channels/tsmf/ChannelOptions.cmake new file mode 100644 index 0000000..b5252ea --- /dev/null +++ b/channels/tsmf/ChannelOptions.cmake @@ -0,0 +1,23 @@ + +set(OPTION_DEFAULT OFF) +set(OPTION_CLIENT_DEFAULT OFF) +set(OPTION_SERVER_DEFAULT OFF) + +if(WIN32) + set(OPTION_CLIENT_DEFAULT OFF) + set(OPTION_SERVER_DEFAULT OFF) +endif() + +if(ANDROID) + set(OPTION_CLIENT_DEFAULT OFF) + set(OPTION_SERVER_DEFAULT OFF) +endif() + +define_channel_options(NAME "tsmf" TYPE "dynamic" + DESCRIPTION "[DEPRECATED] Video Redirection Virtual Channel Extension" + SPECIFICATIONS "[MS-RDPEV]" + DEFAULT ${OPTION_DEFAULT}) + +define_channel_client_options(${OPTION_CLIENT_DEFAULT}) +define_channel_server_options(${OPTION_SERVER_DEFAULT}) + diff --git a/channels/tsmf/client/CMakeLists.txt b/channels/tsmf/client/CMakeLists.txt new file mode 100644 index 0000000..d7438de --- /dev/null +++ b/channels/tsmf/client/CMakeLists.txt @@ -0,0 +1,114 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2012 Marc-Andre Moreau +# Copyright 2012 Hewlett-Packard Development Company, L.P. +# +# Licensed 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. + +define_channel_client("tsmf") + +message(DEPRECATION "TSMF channel is no longer maintained. Use [MS-RDPEVOR] (/video) instead.") + +set(GSTREAMER_0_10_FEATURE_TYPE "OPTIONAL") +set(GSTREAMER_0_10_FEATURE_PURPOSE "multimedia") +set(GSTREAMER_0_10_FEATURE_DESCRIPTION "multimedia redirection, audio and video playback, gstreamer 0.10 version") + +set(GSTREAMER_1_0_FEATURE_TYPE "RECOMMENDED") +set(GSTREAMER_1_0_FEATURE_PURPOSE "multimedia") +set(GSTREAMER_1_0_FEATURE_DESCRIPTION "multimedia redirection, audio and video playback") + +if (WIN32) + set(GSTREAMER_1_0_FEATURE_TYPE "DISABLED") + set(GSTREAMER_0_10_FEATURE_TYPE "OPTIONAL") +endif() +if (APPLE) + set(GSTREAMER_1_0_FEATURE_TYPE "OPTIONAL") + + if (IOS) + set(GSTREAMER_1_0_FEATURE_TYPE "DISABLED") + set(GSTREAMER_0_10_FEATURE_TYPE "DISABLED") + endif() +endif() +if (ANDROID) + set(GSTREAMER_1_0_FEATURE_TYPE "DISABLED") + set(GSTREAMER_0_10_FEATURE_TYPE "DISABLED") +endif() + +find_feature(GStreamer_0_10 ${GSTREAMER_0_10_FEATURE_TYPE} ${GSTREAMER_0_10_FEATURE_PURPOSE} ${GSTREAMER_0_10_FEATURE_DESCRIPTION}) +find_feature(GStreamer_1_0 ${GSTREAMER_1_0_FEATURE_TYPE} ${GSTREAMER_1_0_FEATURE_PURPOSE} ${GSTREAMER_1_0_FEATURE_DESCRIPTION}) + +if (WITH_GSTREAMER_0_10 AND GSTREAMER_0_10_FOUND) + add_definitions(-DWITH_GSTREAMER_0_10) +endif() +if (WITH_GSTREAMER_1_0 AND GSTREAMER_1_0_FOUND) + add_definitions(-DWITH_GSTREAMER_1_0) +endif() + +set(${MODULE_PREFIX}_SRCS + tsmf_audio.c + tsmf_audio.h + tsmf_codec.c + tsmf_codec.h + tsmf_constants.h + tsmf_decoder.c + tsmf_decoder.h + tsmf_ifman.c + tsmf_ifman.h + tsmf_main.c + tsmf_main.h + tsmf_media.c + tsmf_media.h + tsmf_types.h) + +include_directories(..) + +add_channel_client_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} TRUE "DVCPluginEntry") + + + +target_link_libraries(${MODULE_NAME} freerdp winpr) + +if (WITH_DEBUG_SYMBOLS AND MSVC AND NOT BUILTIN_CHANNELS AND BUILD_SHARED_LIBS) + install(FILES ${CMAKE_BINARY_DIR}/${MODULE_NAME}.pdb DESTINATION ${FREERDP_ADDIN_PATH} COMPONENT symbols) +endif() + +set_property(TARGET ${MODULE_NAME} PROPERTY FOLDER "Channels/${CHANNEL_NAME}/Client") + +if(WITH_FFMPEG) + add_channel_client_subsystem(${MODULE_PREFIX} ${CHANNEL_NAME} "ffmpeg" "decoder") +endif() + +if(WITH_GSTREAMER_0_10 OR WITH_GSTREAMER_1_0) + set(XRANDR_FEATURE_TYPE "REQUIRED") + set(XRANDR_FEATURE_PURPOSE "X11 randr") + set(XRANDR_FEATURE_DESCRIPTION "X11 randr extension") + find_feature(XRandR ${XRANDR_FEATURE_TYPE} ${XRANDR_FEATURE_PURPOSE} ${XRANDR_FEATURE_DESCRIPTION}) + if (WITH_XRANDR) + add_channel_client_subsystem(${MODULE_PREFIX} ${CHANNEL_NAME} "gstreamer" "decoder") + else() + message(WARNING "Disabling tsmf gstreamer because XRandR wasn't found") + endif() +endif() + +if(WITH_OSS) + add_channel_client_subsystem(${MODULE_PREFIX} ${CHANNEL_NAME} "oss" "audio") +endif() + +if(WITH_ALSA) + add_channel_client_subsystem(${MODULE_PREFIX} ${CHANNEL_NAME} "alsa" "audio") +endif() + +if(WITH_PULSE) + add_channel_client_subsystem(${MODULE_PREFIX} ${CHANNEL_NAME} "pulse" "audio") +endif() diff --git a/channels/tsmf/client/alsa/CMakeLists.txt b/channels/tsmf/client/alsa/CMakeLists.txt new file mode 100644 index 0000000..a3938ea --- /dev/null +++ b/channels/tsmf/client/alsa/CMakeLists.txt @@ -0,0 +1,29 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2012 Marc-Andre Moreau +# +# Licensed 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. + +define_channel_client_subsystem("tsmf" "alsa" "audio") + +set(${MODULE_PREFIX}_SRCS + tsmf_alsa.c) + +include_directories(..) +include_directories(${ALSA_INCLUDE_DIRS}) + +add_channel_client_subsystem_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} "" TRUE "") + + +target_link_libraries(${MODULE_NAME} ${ALSA_LIBRARIES} winpr freerdp) diff --git a/channels/tsmf/client/alsa/tsmf_alsa.c b/channels/tsmf/client/alsa/tsmf_alsa.c new file mode 100644 index 0000000..6e1f003 --- /dev/null +++ b/channels/tsmf/client/alsa/tsmf_alsa.c @@ -0,0 +1,248 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Video Redirection Virtual Channel - ALSA Audio Device + * + * Copyright 2010-2011 Vic Lee + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include +#include + +#include + +#include + +#include +#include + +#include "tsmf_audio.h" + +typedef struct _TSMFALSAAudioDevice +{ + ITSMFAudioDevice iface; + + char device[32]; + snd_pcm_t* out_handle; + UINT32 source_rate; + UINT32 actual_rate; + UINT32 source_channels; + UINT32 actual_channels; + UINT32 bytes_per_sample; +} TSMFAlsaAudioDevice; + +static BOOL tsmf_alsa_open_device(TSMFAlsaAudioDevice* alsa) +{ + int error; + error = snd_pcm_open(&alsa->out_handle, alsa->device, SND_PCM_STREAM_PLAYBACK, 0); + + if (error < 0) + { + WLog_ERR(TAG, "failed to open device %s", alsa->device); + return FALSE; + } + + DEBUG_TSMF("open device %s", alsa->device); + return TRUE; +} + +static BOOL tsmf_alsa_open(ITSMFAudioDevice* audio, const char* device) +{ + TSMFAlsaAudioDevice* alsa = (TSMFAlsaAudioDevice*)audio; + + if (!device) + { + strncpy(alsa->device, "default", sizeof(alsa->device)); + } + else + { + strncpy(alsa->device, device, sizeof(alsa->device) - 1); + } + + return tsmf_alsa_open_device(alsa); +} + +static BOOL tsmf_alsa_set_format(ITSMFAudioDevice* audio, UINT32 sample_rate, UINT32 channels, + UINT32 bits_per_sample) +{ + int error; + snd_pcm_uframes_t frames; + snd_pcm_hw_params_t* hw_params; + snd_pcm_sw_params_t* sw_params; + TSMFAlsaAudioDevice* alsa = (TSMFAlsaAudioDevice*)audio; + + if (!alsa->out_handle) + return FALSE; + + snd_pcm_drop(alsa->out_handle); + alsa->actual_rate = alsa->source_rate = sample_rate; + alsa->actual_channels = alsa->source_channels = channels; + alsa->bytes_per_sample = bits_per_sample / 8; + error = snd_pcm_hw_params_malloc(&hw_params); + + if (error < 0) + { + WLog_ERR(TAG, "snd_pcm_hw_params_malloc failed"); + return FALSE; + } + + snd_pcm_hw_params_any(alsa->out_handle, hw_params); + snd_pcm_hw_params_set_access(alsa->out_handle, hw_params, SND_PCM_ACCESS_RW_INTERLEAVED); + snd_pcm_hw_params_set_format(alsa->out_handle, hw_params, SND_PCM_FORMAT_S16_LE); + snd_pcm_hw_params_set_rate_near(alsa->out_handle, hw_params, &alsa->actual_rate, NULL); + snd_pcm_hw_params_set_channels_near(alsa->out_handle, hw_params, &alsa->actual_channels); + frames = sample_rate; + snd_pcm_hw_params_set_buffer_size_near(alsa->out_handle, hw_params, &frames); + snd_pcm_hw_params(alsa->out_handle, hw_params); + snd_pcm_hw_params_free(hw_params); + error = snd_pcm_sw_params_malloc(&sw_params); + + if (error < 0) + { + WLog_ERR(TAG, "snd_pcm_sw_params_malloc"); + return FALSE; + } + + snd_pcm_sw_params_current(alsa->out_handle, sw_params); + snd_pcm_sw_params_set_start_threshold(alsa->out_handle, sw_params, frames / 2); + snd_pcm_sw_params(alsa->out_handle, sw_params); + snd_pcm_sw_params_free(sw_params); + snd_pcm_prepare(alsa->out_handle); + DEBUG_TSMF("sample_rate %" PRIu32 " channels %" PRIu32 " bits_per_sample %" PRIu32 "", + sample_rate, channels, bits_per_sample); + DEBUG_TSMF("hardware buffer %lu frames", frames); + + if ((alsa->actual_rate != alsa->source_rate) || + (alsa->actual_channels != alsa->source_channels)) + { + DEBUG_TSMF("actual rate %" PRIu32 " / channel %" PRIu32 " is different " + "from source rate %" PRIu32 " / channel %" PRIu32 ", resampling required.", + alsa->actual_rate, alsa->actual_channels, alsa->source_rate, + alsa->source_channels); + } + + return TRUE; +} + +static BOOL tsmf_alsa_play(ITSMFAudioDevice* audio, const BYTE* src, UINT32 data_size) +{ + int len; + int error; + int frames; + const BYTE* end; + const BYTE* pindex; + int rbytes_per_frame; + int sbytes_per_frame; + TSMFAlsaAudioDevice* alsa = (TSMFAlsaAudioDevice*)audio; + DEBUG_TSMF("data_size %" PRIu32 "", data_size); + + if (alsa->out_handle) + { + sbytes_per_frame = alsa->source_channels * alsa->bytes_per_sample; + rbytes_per_frame = alsa->actual_channels * alsa->bytes_per_sample; + pindex = src; + end = pindex + data_size; + + while (pindex < end) + { + len = end - pindex; + frames = len / rbytes_per_frame; + error = snd_pcm_writei(alsa->out_handle, pindex, frames); + + if (error == -EPIPE) + { + snd_pcm_recover(alsa->out_handle, error, 0); + error = 0; + } + else if (error < 0) + { + DEBUG_TSMF("error len %d", error); + snd_pcm_close(alsa->out_handle); + alsa->out_handle = 0; + tsmf_alsa_open_device(alsa); + break; + } + + DEBUG_TSMF("%d frames played.", error); + + if (error == 0) + break; + + pindex += error * rbytes_per_frame; + } + } + + return TRUE; +} + +static UINT64 tsmf_alsa_get_latency(ITSMFAudioDevice* audio) +{ + UINT64 latency = 0; + snd_pcm_sframes_t frames = 0; + TSMFAlsaAudioDevice* alsa = (TSMFAlsaAudioDevice*)audio; + + if (alsa->out_handle && alsa->actual_rate > 0 && + snd_pcm_delay(alsa->out_handle, &frames) == 0 && frames > 0) + { + latency = ((UINT64)frames) * 10000000LL / (UINT64)alsa->actual_rate; + } + + return latency; +} + +static BOOL tsmf_alsa_flush(ITSMFAudioDevice* audio) +{ + return TRUE; +} + +static void tsmf_alsa_free(ITSMFAudioDevice* audio) +{ + TSMFAlsaAudioDevice* alsa = (TSMFAlsaAudioDevice*)audio; + DEBUG_TSMF(""); + + if (alsa->out_handle) + { + snd_pcm_drain(alsa->out_handle); + snd_pcm_close(alsa->out_handle); + } + + free(alsa); +} + +#ifdef BUILTIN_CHANNELS +#define freerdp_tsmf_client_audio_subsystem_entry alsa_freerdp_tsmf_client_audio_subsystem_entry +#else +#define freerdp_tsmf_client_audio_subsystem_entry \ + FREERDP_API freerdp_tsmf_client_audio_subsystem_entry +#endif + +ITSMFAudioDevice* freerdp_tsmf_client_audio_subsystem_entry(void) +{ + TSMFAlsaAudioDevice* alsa; + alsa = (TSMFAlsaAudioDevice*)malloc(sizeof(TSMFAlsaAudioDevice)); + ZeroMemory(alsa, sizeof(TSMFAlsaAudioDevice)); + alsa->iface.Open = tsmf_alsa_open; + alsa->iface.SetFormat = tsmf_alsa_set_format; + alsa->iface.Play = tsmf_alsa_play; + alsa->iface.GetLatency = tsmf_alsa_get_latency; + alsa->iface.Flush = tsmf_alsa_flush; + alsa->iface.Free = tsmf_alsa_free; + return (ITSMFAudioDevice*)alsa; +} diff --git a/channels/tsmf/client/ffmpeg/CMakeLists.txt b/channels/tsmf/client/ffmpeg/CMakeLists.txt new file mode 100644 index 0000000..cda0bdf --- /dev/null +++ b/channels/tsmf/client/ffmpeg/CMakeLists.txt @@ -0,0 +1,45 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2012 Marc-Andre Moreau +# +# Licensed 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. + +define_channel_client_subsystem("tsmf" "ffmpeg" "decoder") + +set(${MODULE_PREFIX}_SRCS + tsmf_ffmpeg.c) + +include_directories(..) +include_directories(${FFMPEG_INCLUDE_DIRS}) + +add_channel_client_subsystem_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} "" TRUE "") + + + +if(APPLE) + # For this to work on apple, we need to add some frameworks + FIND_LIBRARY(COREFOUNDATION_LIBRARY CoreFoundation) + FIND_LIBRARY(COREVIDEO_LIBRARY CoreVideo) + FIND_LIBRARY(COREVIDEODECODE_LIBRARY VideoDecodeAcceleration) + set(${MODULE_PREFIX}_LIBS ${${MODULE_PREFIX}_LIBS} ${FFMPEG_LIBRARIES} ${COREFOUNDATION_LIBRARY} ${COREVIDEO_LIBRARY} ${COREVIDEODECODE_LIBRARY}) + target_link_libraries(${MODULE_NAME} ${${MODULE_PREFIX}_LIBS} freerdp) +else() + set(${MODULE_PREFIX}_LIBS ${${MODULE_PREFIX}_LIBS} ${FFMPEG_LIBRARIES}) + target_link_libraries(${MODULE_NAME} ${${MODULE_PREFIX}_LIBS}) +endif() + +if (WITH_DEBUG_SYMBOLS AND MSVC AND NOT BUILTIN_CHANNELS AND BUILD_SHARED_LIBS) + install(FILES ${CMAKE_BINARY_DIR}/${MODULE_NAME}.pdb DESTINATION ${FREERDP_ADDIN_PATH} COMPONENT symbols) +endif() + diff --git a/channels/tsmf/client/ffmpeg/tsmf_ffmpeg.c b/channels/tsmf/client/ffmpeg/tsmf_ffmpeg.c new file mode 100644 index 0000000..c14877b --- /dev/null +++ b/channels/tsmf/client/ffmpeg/tsmf_ffmpeg.c @@ -0,0 +1,667 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Video Redirection Virtual Channel - FFmpeg Decoder + * + * Copyright 2010-2011 Vic Lee + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include + +#include + +#include +#include + +#include +#include + +#include "tsmf_constants.h" +#include "tsmf_decoder.h" + +/* Compatibility with older FFmpeg */ +#if LIBAVUTIL_VERSION_MAJOR < 50 +#define AVMEDIA_TYPE_VIDEO 0 +#define AVMEDIA_TYPE_AUDIO 1 +#endif + +#if LIBAVCODEC_VERSION_MAJOR < 54 +#define MAX_AUDIO_FRAME_SIZE AVCODEC_MAX_AUDIO_FRAME_SIZE +#else +#define MAX_AUDIO_FRAME_SIZE 192000 +#endif + +#if LIBAVCODEC_VERSION_MAJOR < 55 +#define AV_CODEC_ID_VC1 CODEC_ID_VC1 +#define AV_CODEC_ID_WMAV2 CODEC_ID_WMAV2 +#define AV_CODEC_ID_WMAPRO CODEC_ID_WMAPRO +#define AV_CODEC_ID_MP3 CODEC_ID_MP3 +#define AV_CODEC_ID_MP2 CODEC_ID_MP2 +#define AV_CODEC_ID_MPEG2VIDEO CODEC_ID_MPEG2VIDEO +#define AV_CODEC_ID_WMV3 CODEC_ID_WMV3 +#define AV_CODEC_ID_AAC CODEC_ID_AAC +#define AV_CODEC_ID_H264 CODEC_ID_H264 +#define AV_CODEC_ID_AC3 CODEC_ID_AC3 +#endif + +#if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(56, 34, 2) +#define AV_CODEC_CAP_TRUNCATED CODEC_CAP_TRUNCATED +#define AV_CODEC_FLAG_TRUNCATED CODEC_FLAG_TRUNCATED +#endif + +#if LIBAVUTIL_VERSION_MAJOR < 52 +#define AV_PIX_FMT_YUV420P PIX_FMT_YUV420P +#endif + +typedef struct _TSMFFFmpegDecoder +{ + ITSMFDecoder iface; + + int media_type; +#if LIBAVCODEC_VERSION_MAJOR < 55 + enum CodecID codec_id; +#else + enum AVCodecID codec_id; +#endif + AVCodecContext* codec_context; + AVCodec* codec; + AVFrame* frame; + int prepared; + + BYTE* decoded_data; + UINT32 decoded_size; + UINT32 decoded_size_max; +} TSMFFFmpegDecoder; + +static BOOL tsmf_ffmpeg_init_context(ITSMFDecoder* decoder) +{ + TSMFFFmpegDecoder* mdecoder = (TSMFFFmpegDecoder*)decoder; + mdecoder->codec_context = avcodec_alloc_context3(NULL); + + if (!mdecoder->codec_context) + { + WLog_ERR(TAG, "avcodec_alloc_context failed."); + return FALSE; + } + + return TRUE; +} + +static BOOL tsmf_ffmpeg_init_video_stream(ITSMFDecoder* decoder, const TS_AM_MEDIA_TYPE* media_type) +{ + TSMFFFmpegDecoder* mdecoder = (TSMFFFmpegDecoder*)decoder; + mdecoder->codec_context->width = media_type->Width; + mdecoder->codec_context->height = media_type->Height; + mdecoder->codec_context->bit_rate = media_type->BitRate; + mdecoder->codec_context->time_base.den = media_type->SamplesPerSecond.Numerator; + mdecoder->codec_context->time_base.num = media_type->SamplesPerSecond.Denominator; +#if LIBAVCODEC_VERSION_MAJOR < 55 + mdecoder->frame = avcodec_alloc_frame(); +#else + mdecoder->frame = av_frame_alloc(); +#endif + return TRUE; +} + +static BOOL tsmf_ffmpeg_init_audio_stream(ITSMFDecoder* decoder, const TS_AM_MEDIA_TYPE* media_type) +{ + TSMFFFmpegDecoder* mdecoder = (TSMFFFmpegDecoder*)decoder; + mdecoder->codec_context->sample_rate = media_type->SamplesPerSecond.Numerator; + mdecoder->codec_context->bit_rate = media_type->BitRate; + mdecoder->codec_context->channels = media_type->Channels; + mdecoder->codec_context->block_align = media_type->BlockAlign; +#if LIBAVCODEC_VERSION_MAJOR < 55 +#ifdef AV_CPU_FLAG_SSE2 + mdecoder->codec_context->dsp_mask = AV_CPU_FLAG_SSE2 | AV_CPU_FLAG_MMX2; +#else +#if LIBAVCODEC_VERSION_MAJOR < 53 + mdecoder->codec_context->dsp_mask = FF_MM_SSE2 | FF_MM_MMXEXT; +#else + mdecoder->codec_context->dsp_mask = FF_MM_SSE2 | FF_MM_MMX2; +#endif +#endif +#else /* LIBAVCODEC_VERSION_MAJOR < 55 */ +#ifdef AV_CPU_FLAG_SSE2 + av_set_cpu_flags_mask(AV_CPU_FLAG_SSE2 | AV_CPU_FLAG_MMXEXT); +#else + av_set_cpu_flags_mask(FF_MM_SSE2 | FF_MM_MMX2); +#endif +#endif /* LIBAVCODEC_VERSION_MAJOR < 55 */ + return TRUE; +} + +static BOOL tsmf_ffmpeg_init_stream(ITSMFDecoder* decoder, const TS_AM_MEDIA_TYPE* media_type) +{ + BYTE* p; + UINT32 size; + const BYTE* s; + TSMFFFmpegDecoder* mdecoder = (TSMFFFmpegDecoder*)decoder; + mdecoder->codec = avcodec_find_decoder(mdecoder->codec_id); + + if (!mdecoder->codec) + { + WLog_ERR(TAG, "avcodec_find_decoder failed."); + return FALSE; + } + + mdecoder->codec_context->codec_id = mdecoder->codec_id; + mdecoder->codec_context->codec_type = mdecoder->media_type; + + switch (mdecoder->media_type) + { + case AVMEDIA_TYPE_VIDEO: + if (!tsmf_ffmpeg_init_video_stream(decoder, media_type)) + return FALSE; + + break; + + case AVMEDIA_TYPE_AUDIO: + if (!tsmf_ffmpeg_init_audio_stream(decoder, media_type)) + return FALSE; + + break; + + default: + WLog_ERR(TAG, "unknown media_type %d", mdecoder->media_type); + break; + } + + if (media_type->ExtraData) + { + /* Add a padding to avoid invalid memory read in some codec */ + mdecoder->codec_context->extradata_size = media_type->ExtraDataSize + 8; + mdecoder->codec_context->extradata = calloc(1, mdecoder->codec_context->extradata_size); + + if (!mdecoder->codec_context->extradata) + return FALSE; + + if (media_type->SubType == TSMF_SUB_TYPE_AVC1 && + media_type->FormatType == TSMF_FORMAT_TYPE_MPEG2VIDEOINFO) + { + size_t required = 6; + /* The extradata format that FFmpeg uses is following CodecPrivate in Matroska. + See http://haali.su/mkv/codecs.pdf */ + p = mdecoder->codec_context->extradata; + if (mdecoder->codec_context->extradata_size < required) + return FALSE; + *p++ = 1; /* Reserved? */ + *p++ = media_type->ExtraData[8]; /* Profile */ + *p++ = 0; /* Profile */ + *p++ = media_type->ExtraData[12]; /* Level */ + *p++ = 0xff; /* Flag? */ + *p++ = 0xe0 | 0x01; /* Reserved | #sps */ + s = media_type->ExtraData + 20; + size = ((UINT32)(*s)) * 256 + ((UINT32)(*(s + 1))); + required += size + 2; + if (mdecoder->codec_context->extradata_size < required) + return FALSE; + memcpy(p, s, size + 2); + s += size + 2; + p += size + 2; + required++; + if (mdecoder->codec_context->extradata_size < required) + return FALSE; + *p++ = 1; /* #pps */ + size = ((UINT32)(*s)) * 256 + ((UINT32)(*(s + 1))); + required += size + 2; + if (mdecoder->codec_context->extradata_size < required) + return FALSE; + memcpy(p, s, size + 2); + } + else + { + memcpy(mdecoder->codec_context->extradata, media_type->ExtraData, + media_type->ExtraDataSize); + if (mdecoder->codec_context->extradata_size < media_type->ExtraDataSize + 8) + return FALSE; + memset(mdecoder->codec_context->extradata + media_type->ExtraDataSize, 0, 8); + } + } + + if (mdecoder->codec->capabilities & AV_CODEC_CAP_TRUNCATED) + mdecoder->codec_context->flags |= AV_CODEC_FLAG_TRUNCATED; + + return TRUE; +} + +static BOOL tsmf_ffmpeg_prepare(ITSMFDecoder* decoder) +{ + TSMFFFmpegDecoder* mdecoder = (TSMFFFmpegDecoder*)decoder; + + if (avcodec_open2(mdecoder->codec_context, mdecoder->codec, NULL) < 0) + { + WLog_ERR(TAG, "avcodec_open2 failed."); + return FALSE; + } + + mdecoder->prepared = 1; + return TRUE; +} + +static BOOL tsmf_ffmpeg_set_format(ITSMFDecoder* decoder, TS_AM_MEDIA_TYPE* media_type) +{ + TSMFFFmpegDecoder* mdecoder = (TSMFFFmpegDecoder*)decoder; + + WINPR_ASSERT(mdecoder); + WINPR_ASSERT(media_type); + + switch (media_type->MajorType) + { + case TSMF_MAJOR_TYPE_VIDEO: + mdecoder->media_type = AVMEDIA_TYPE_VIDEO; + break; + + case TSMF_MAJOR_TYPE_AUDIO: + mdecoder->media_type = AVMEDIA_TYPE_AUDIO; + break; + + default: + return FALSE; + } + + switch (media_type->SubType) + { + case TSMF_SUB_TYPE_WVC1: + mdecoder->codec_id = AV_CODEC_ID_VC1; + break; + + case TSMF_SUB_TYPE_WMA2: + mdecoder->codec_id = AV_CODEC_ID_WMAV2; + break; + + case TSMF_SUB_TYPE_WMA9: + mdecoder->codec_id = AV_CODEC_ID_WMAPRO; + break; + + case TSMF_SUB_TYPE_MP3: + mdecoder->codec_id = AV_CODEC_ID_MP3; + break; + + case TSMF_SUB_TYPE_MP2A: + mdecoder->codec_id = AV_CODEC_ID_MP2; + break; + + case TSMF_SUB_TYPE_MP2V: + mdecoder->codec_id = AV_CODEC_ID_MPEG2VIDEO; + break; + + case TSMF_SUB_TYPE_WMV3: + mdecoder->codec_id = AV_CODEC_ID_WMV3; + break; + + case TSMF_SUB_TYPE_AAC: + mdecoder->codec_id = AV_CODEC_ID_AAC; + + /* For AAC the pFormat is a HEAACWAVEINFO struct, and the codec data + is at the end of it. See + http://msdn.microsoft.com/en-us/library/dd757806.aspx */ + if (media_type->ExtraData) + { + if (media_type->ExtraDataSize < 12) + return FALSE; + + media_type->ExtraData += 12; + media_type->ExtraDataSize -= 12; + } + + break; + + case TSMF_SUB_TYPE_H264: + case TSMF_SUB_TYPE_AVC1: + mdecoder->codec_id = AV_CODEC_ID_H264; + break; + + case TSMF_SUB_TYPE_AC3: + mdecoder->codec_id = AV_CODEC_ID_AC3; + break; + + default: + return FALSE; + } + + if (!tsmf_ffmpeg_init_context(decoder)) + return FALSE; + + if (!tsmf_ffmpeg_init_stream(decoder, media_type)) + return FALSE; + + if (!tsmf_ffmpeg_prepare(decoder)) + return FALSE; + + return TRUE; +} + +static BOOL tsmf_ffmpeg_decode_video(ITSMFDecoder* decoder, const BYTE* data, UINT32 data_size, + UINT32 extensions) +{ + TSMFFFmpegDecoder* mdecoder = (TSMFFFmpegDecoder*)decoder; + int decoded; + int len; + AVFrame* frame; + BOOL ret = TRUE; +#if LIBAVCODEC_VERSION_MAJOR < 52 || \ + (LIBAVCODEC_VERSION_MAJOR == 52 && LIBAVCODEC_VERSION_MINOR <= 20) + len = avcodec_decode_video(mdecoder->codec_context, mdecoder->frame, &decoded, data, data_size); +#else + { + AVPacket pkt; + av_init_packet(&pkt); + pkt.data = (BYTE*)data; + pkt.size = data_size; + + if (extensions & TSMM_SAMPLE_EXT_CLEANPOINT) + pkt.flags |= AV_PKT_FLAG_KEY; + + len = avcodec_decode_video2(mdecoder->codec_context, mdecoder->frame, &decoded, &pkt); + } +#endif + + if (len < 0) + { + WLog_ERR(TAG, "data_size %" PRIu32 ", avcodec_decode_video failed (%d)", data_size, len); + ret = FALSE; + } + else if (!decoded) + { + WLog_ERR(TAG, "data_size %" PRIu32 ", no frame is decoded.", data_size); + ret = FALSE; + } + else + { + DEBUG_TSMF("linesize[0] %d linesize[1] %d linesize[2] %d linesize[3] %d " + "pix_fmt %d width %d height %d", + mdecoder->frame->linesize[0], mdecoder->frame->linesize[1], + mdecoder->frame->linesize[2], mdecoder->frame->linesize[3], + mdecoder->codec_context->pix_fmt, mdecoder->codec_context->width, + mdecoder->codec_context->height); + mdecoder->decoded_size = + avpicture_get_size(mdecoder->codec_context->pix_fmt, mdecoder->codec_context->width, + mdecoder->codec_context->height); + mdecoder->decoded_data = calloc(1, mdecoder->decoded_size); + + if (!mdecoder->decoded_data) + return FALSE; + +#if LIBAVCODEC_VERSION_MAJOR < 55 + frame = avcodec_alloc_frame(); +#else + frame = av_frame_alloc(); +#endif + avpicture_fill((AVPicture*)frame, mdecoder->decoded_data, mdecoder->codec_context->pix_fmt, + mdecoder->codec_context->width, mdecoder->codec_context->height); + av_picture_copy((AVPicture*)frame, (AVPicture*)mdecoder->frame, + mdecoder->codec_context->pix_fmt, mdecoder->codec_context->width, + mdecoder->codec_context->height); + av_free(frame); + } + + return ret; +} + +static BOOL tsmf_ffmpeg_decode_audio(ITSMFDecoder* decoder, const BYTE* data, UINT32 data_size, + UINT32 extensions) +{ + TSMFFFmpegDecoder* mdecoder = (TSMFFFmpegDecoder*)decoder; + int len; + int frame_size; + UINT32 src_size; + const BYTE* src; + BYTE* dst; + int dst_offset; +#if 0 + WLog_DBG(TAG, ("tsmf_ffmpeg_decode_audio: data_size %"PRIu32"", data_size)); + int i; + + for (i = 0; i < data_size; i++) + { + WLog_DBG(TAG, ("%02"PRIX8"", data[i])); + + if (i % 16 == 15) + WLog_DBG(TAG, ("\n")); + } + +#endif + + if (mdecoder->decoded_size_max == 0) + mdecoder->decoded_size_max = MAX_AUDIO_FRAME_SIZE + 16; + + mdecoder->decoded_data = calloc(1, mdecoder->decoded_size_max); + + if (!mdecoder->decoded_data) + return FALSE; + + /* align the memory for SSE2 needs */ + dst = (BYTE*)(((uintptr_t)mdecoder->decoded_data + 15) & ~0x0F); + dst_offset = dst - mdecoder->decoded_data; + src = data; + src_size = data_size; + + while (src_size > 0) + { + /* Ensure enough space for decoding */ + if (mdecoder->decoded_size_max - mdecoder->decoded_size < MAX_AUDIO_FRAME_SIZE) + { + BYTE* tmp_data; + tmp_data = realloc(mdecoder->decoded_data, mdecoder->decoded_size_max * 2 + 16); + + if (!tmp_data) + return FALSE; + + mdecoder->decoded_size_max = mdecoder->decoded_size_max * 2 + 16; + mdecoder->decoded_data = tmp_data; + dst = (BYTE*)(((uintptr_t)mdecoder->decoded_data + 15) & ~0x0F); + + if (dst - mdecoder->decoded_data != dst_offset) + { + /* re-align the memory if the alignment has changed after realloc */ + memmove(dst, mdecoder->decoded_data + dst_offset, mdecoder->decoded_size); + dst_offset = dst - mdecoder->decoded_data; + } + + dst += mdecoder->decoded_size; + } + + frame_size = mdecoder->decoded_size_max - mdecoder->decoded_size; +#if LIBAVCODEC_VERSION_MAJOR < 52 || \ + (LIBAVCODEC_VERSION_MAJOR == 52 && LIBAVCODEC_VERSION_MINOR <= 20) + len = avcodec_decode_audio2(mdecoder->codec_context, (int16_t*)dst, &frame_size, src, + src_size); +#else + { +#if LIBAVCODEC_VERSION_MAJOR < 55 + AVFrame* decoded_frame = avcodec_alloc_frame(); +#else + AVFrame* decoded_frame = av_frame_alloc(); +#endif + int got_frame = 0; + AVPacket pkt; + av_init_packet(&pkt); + pkt.data = (BYTE*)src; + pkt.size = src_size; + len = avcodec_decode_audio4(mdecoder->codec_context, decoded_frame, &got_frame, &pkt); + + if (len >= 0 && got_frame) + { + frame_size = av_samples_get_buffer_size(NULL, mdecoder->codec_context->channels, + decoded_frame->nb_samples, + mdecoder->codec_context->sample_fmt, 1); + memcpy(dst, decoded_frame->data[0], frame_size); + } + else + { + frame_size = 0; + } + + av_free(decoded_frame); + } +#endif + + if (len > 0) + { + src += len; + src_size -= len; + } + + if (frame_size > 0) + { + mdecoder->decoded_size += frame_size; + dst += frame_size; + } + } + + if (mdecoder->decoded_size == 0) + { + free(mdecoder->decoded_data); + mdecoder->decoded_data = NULL; + } + else if (dst_offset) + { + /* move the aligned decoded data to original place */ + memmove(mdecoder->decoded_data, mdecoder->decoded_data + dst_offset, + mdecoder->decoded_size); + } + + DEBUG_TSMF("data_size %" PRIu32 " decoded_size %" PRIu32 "", data_size, mdecoder->decoded_size); + return TRUE; +} + +static BOOL tsmf_ffmpeg_decode(ITSMFDecoder* decoder, const BYTE* data, UINT32 data_size, + UINT32 extensions) +{ + TSMFFFmpegDecoder* mdecoder = (TSMFFFmpegDecoder*)decoder; + + if (mdecoder->decoded_data) + { + free(mdecoder->decoded_data); + mdecoder->decoded_data = NULL; + } + + mdecoder->decoded_size = 0; + + switch (mdecoder->media_type) + { + case AVMEDIA_TYPE_VIDEO: + return tsmf_ffmpeg_decode_video(decoder, data, data_size, extensions); + + case AVMEDIA_TYPE_AUDIO: + return tsmf_ffmpeg_decode_audio(decoder, data, data_size, extensions); + + default: + WLog_ERR(TAG, "unknown media type."); + return FALSE; + } +} + +static BYTE* tsmf_ffmpeg_get_decoded_data(ITSMFDecoder* decoder, UINT32* size) +{ + BYTE* buf; + TSMFFFmpegDecoder* mdecoder = (TSMFFFmpegDecoder*)decoder; + *size = mdecoder->decoded_size; + buf = mdecoder->decoded_data; + mdecoder->decoded_data = NULL; + mdecoder->decoded_size = 0; + return buf; +} + +static UINT32 tsmf_ffmpeg_get_decoded_format(ITSMFDecoder* decoder) +{ + TSMFFFmpegDecoder* mdecoder = (TSMFFFmpegDecoder*)decoder; + + switch (mdecoder->codec_context->pix_fmt) + { + case AV_PIX_FMT_YUV420P: + return RDP_PIXFMT_I420; + + default: + WLog_ERR(TAG, "unsupported pixel format %u", mdecoder->codec_context->pix_fmt); + return (UINT32)-1; + } +} + +static BOOL tsmf_ffmpeg_get_decoded_dimension(ITSMFDecoder* decoder, UINT32* width, UINT32* height) +{ + TSMFFFmpegDecoder* mdecoder = (TSMFFFmpegDecoder*)decoder; + + if (mdecoder->codec_context->width > 0 && mdecoder->codec_context->height > 0) + { + *width = mdecoder->codec_context->width; + *height = mdecoder->codec_context->height; + return TRUE; + } + else + { + return FALSE; + } +} + +static void tsmf_ffmpeg_free(ITSMFDecoder* decoder) +{ + TSMFFFmpegDecoder* mdecoder = (TSMFFFmpegDecoder*)decoder; + + if (mdecoder->frame) + av_free(mdecoder->frame); + + free(mdecoder->decoded_data); + + if (mdecoder->codec_context) + { + if (mdecoder->prepared) + avcodec_close(mdecoder->codec_context); + + free(mdecoder->codec_context->extradata); + av_free(mdecoder->codec_context); + } + + free(decoder); +} + +static INIT_ONCE g_Initialized = INIT_ONCE_STATIC_INIT; +static BOOL CALLBACK InitializeAvCodecs(PINIT_ONCE once, PVOID param, PVOID* context) +{ +#if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(58, 10, 100) + avcodec_register_all(); +#endif + return TRUE; +} + +#ifdef BUILTIN_CHANNELS +#define freerdp_tsmf_client_subsystem_entry ffmpeg_freerdp_tsmf_client_decoder_subsystem_entry +#else +#define freerdp_tsmf_client_subsystem_entry FREERDP_API freerdp_tsmf_client_decoder_subsystem_entry +#endif + +ITSMFDecoder* freerdp_tsmf_client_subsystem_entry(void) +{ + TSMFFFmpegDecoder* decoder; + InitOnceExecuteOnce(&g_Initialized, InitializeAvCodecs, NULL, NULL); + WLog_DBG(TAG, "TSMFDecoderEntry FFMPEG"); + decoder = (TSMFFFmpegDecoder*)calloc(1, sizeof(TSMFFFmpegDecoder)); + + if (!decoder) + return NULL; + + decoder->iface.SetFormat = tsmf_ffmpeg_set_format; + decoder->iface.Decode = tsmf_ffmpeg_decode; + decoder->iface.GetDecodedData = tsmf_ffmpeg_get_decoded_data; + decoder->iface.GetDecodedFormat = tsmf_ffmpeg_get_decoded_format; + decoder->iface.GetDecodedDimension = tsmf_ffmpeg_get_decoded_dimension; + decoder->iface.Free = tsmf_ffmpeg_free; + return (ITSMFDecoder*)decoder; +} diff --git a/channels/tsmf/client/gstreamer/CMakeLists.txt b/channels/tsmf/client/gstreamer/CMakeLists.txt new file mode 100644 index 0000000..fff688c --- /dev/null +++ b/channels/tsmf/client/gstreamer/CMakeLists.txt @@ -0,0 +1,65 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script for gstreamer subsystem +# +# (C) Copyright 2012 Hewlett-Packard Development Company, L.P. +# +# Licensed 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. + +define_channel_client_subsystem("tsmf" "gstreamer" "decoder") + +if(NOT GSTREAMER_0_10_FOUND AND NOT GSTREAMER_1_0_FOUND) + message(FATAL_ERROR "GStreamer library not found, but required for TSMF module.") +elseif (GSTREAMER_0_10_FOUND AND GSTREAMER_1_0_FOUND) + message(FATAL_ERROR "GStreamer 0.10 and GStreamer 1.0 support are mutually exclusive!") +endif() + +set(SRC "tsmf_gstreamer.c") + +if (GSTREAMER_1_0_FOUND) + set(LIBS ${GSTREAMER_1_0_LIBRARIES}) + include_directories(${GSTREAMER_1_0_INCLUDE_DIRS}) +elseif (GSTREAMER_0_10_FOUND) + set(LIBS ${GSTREAMER_0_10_LIBRARIES}) + include_directories(${GSTREAMER_0_10_INCLUDE_DIRS}) +endif() + +if(ANDROID) + set(SRC ${SRC} + tsmf_android.c) + set(LIBS ${LIBS}) +else() + set(XEXT_FEATURE_TYPE "RECOMMENDED") + set(XEXT_FEATURE_PURPOSE "X11 extension") + set(XEXT_FEATURE_DESCRIPTION "X11 core extensions") + + find_feature(Xext ${XEXT_FEATURE_TYPE} ${XEXT_FEATURE_PURPOSE} ${XEXT_FEATURE_DESCRIPTION}) + + set(SRC ${SRC} + tsmf_X11.c) + set(LIBS ${LIBS} ${X11_LIBRARIES} ${XEXT_LIBRARIES}) + if (NOT APPLE) + list(APPEND LIBS rt) + endif() + + if(XEXT_FOUND) + add_definitions(-DWITH_XEXT=1) + endif() + +endif() + +set(${MODULE_PREFIX}_SRCS "${SRC}") + +include_directories(..) + +add_channel_client_subsystem_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} "" TRUE "") +target_link_libraries(${MODULE_NAME} ${LIBS} winpr) diff --git a/channels/tsmf/client/gstreamer/tsmf_X11.c b/channels/tsmf/client/gstreamer/tsmf_X11.c new file mode 100644 index 0000000..ae383df --- /dev/null +++ b/channels/tsmf/client/gstreamer/tsmf_X11.c @@ -0,0 +1,506 @@ +/* + * FreeRDP: A Remote Desktop Protocol Implementation + * Video Redirection Virtual Channel - GStreamer Decoder X11 specifics + * + * (C) Copyright 2014 Thincast Technologies GmbH + * (C) Copyright 2014 Armin Novak + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#ifndef __CYGWIN__ +#include +#endif + +#include +#include +#include +#include +#include +#include +#include + +#if __clang__ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wparentheses-equality" +#endif /* __clang__ */ +#include +#if __clang__ +#pragma clang diagnostic pop +#endif /* __clang__ */ + +#if GST_VERSION_MAJOR > 0 +#include +#else +#include +#endif + +#include +#include +#include + +#include + +#include "tsmf_platform.h" +#include "tsmf_constants.h" +#include "tsmf_decoder.h" + +#if !defined(WITH_XEXT) +#warning "Building TSMF without shape extension support" +#endif + +struct X11Handle +{ + int shmid; + int* xfwin; +#if defined(WITH_XEXT) + BOOL has_shape; +#endif + Display* disp; + Window subwin; + BOOL subwinMapped; +#if GST_VERSION_MAJOR > 0 + GstVideoOverlay* overlay; +#else + GstXOverlay* overlay; +#endif + int subwinWidth; + int subwinHeight; + int subwinX; + int subwinY; +}; + +static const char* get_shm_id() +{ + static char shm_id[128]; + sprintf_s(shm_id, sizeof(shm_id), "/com.freerdp.xfreerdp.tsmf_%016X", GetCurrentProcessId()); + return shm_id; +} + +static GstBusSyncReply tsmf_platform_bus_sync_handler(GstBus* bus, GstMessage* message, + gpointer user_data) +{ + struct X11Handle* hdl; + + TSMFGstreamerDecoder* decoder = user_data; + + if (GST_MESSAGE_TYPE(message) != GST_MESSAGE_ELEMENT) + return GST_BUS_PASS; + +#if GST_VERSION_MAJOR > 0 + if (!gst_is_video_overlay_prepare_window_handle_message(message)) + return GST_BUS_PASS; +#else + if (!gst_structure_has_name(message->structure, "prepare-xwindow-id")) + return GST_BUS_PASS; +#endif + + hdl = (struct X11Handle*)decoder->platform; + + if (hdl->subwin) + { +#if GST_VERSION_MAJOR > 0 + hdl->overlay = GST_VIDEO_OVERLAY(GST_MESSAGE_SRC(message)); + gst_video_overlay_set_window_handle(hdl->overlay, hdl->subwin); + gst_video_overlay_handle_events(hdl->overlay, FALSE); +#else + hdl->overlay = GST_X_OVERLAY(GST_MESSAGE_SRC(message)); +#if GST_CHECK_VERSION(0, 10, 31) + gst_x_overlay_set_window_handle(hdl->overlay, hdl->subwin); +#else + gst_x_overlay_set_xwindow_id(hdl->overlay, hdl->subwin); +#endif + gst_x_overlay_handle_events(hdl->overlay, TRUE); +#endif + + if (hdl->subwinWidth != -1 && hdl->subwinHeight != -1 && hdl->subwinX != -1 && + hdl->subwinY != -1) + { +#if GST_VERSION_MAJOR > 0 + if (!gst_video_overlay_set_render_rectangle(hdl->overlay, 0, 0, hdl->subwinWidth, + hdl->subwinHeight)) + { + WLog_ERR(TAG, "Could not resize overlay!"); + } + + gst_video_overlay_expose(hdl->overlay); +#else + if (!gst_x_overlay_set_render_rectangle(hdl->overlay, 0, 0, hdl->subwinWidth, + hdl->subwinHeight)) + { + WLog_ERR(TAG, "Could not resize overlay!"); + } + + gst_x_overlay_expose(hdl->overlay); +#endif + XLockDisplay(hdl->disp); + XMoveResizeWindow(hdl->disp, hdl->subwin, hdl->subwinX, hdl->subwinY, hdl->subwinWidth, + hdl->subwinHeight); + XSync(hdl->disp, FALSE); + XUnlockDisplay(hdl->disp); + } + } + else + { + g_warning("Window was not available before retrieving the overlay!"); + } + + gst_message_unref(message); + + return GST_BUS_DROP; +} + +const char* tsmf_platform_get_video_sink(void) +{ + return "autovideosink"; +} + +const char* tsmf_platform_get_audio_sink(void) +{ + return "autoaudiosink"; +} + +int tsmf_platform_create(TSMFGstreamerDecoder* decoder) +{ + struct X11Handle* hdl; + + if (!decoder) + return -1; + + if (decoder->platform) + return -1; + + hdl = calloc(1, sizeof(struct X11Handle)); + if (!hdl) + { + WLog_ERR(TAG, "Could not allocate handle."); + return -1; + } + + decoder->platform = hdl; + hdl->shmid = shm_open(get_shm_id(), (O_RDWR | O_CREAT), (PROT_READ | PROT_WRITE)); + if (hdl->shmid == -1) + { + WLog_ERR(TAG, "failed to get access to shared memory - shmget(%s): %i - %s", get_shm_id(), + errno, strerror(errno)); + return -2; + } + + hdl->xfwin = mmap(0, sizeof(void*), PROT_READ | PROT_WRITE, MAP_SHARED, hdl->shmid, 0); + if (hdl->xfwin == MAP_FAILED) + { + WLog_ERR(TAG, "shmat failed!"); + return -3; + } + + hdl->disp = XOpenDisplay(NULL); + if (!hdl->disp) + { + WLog_ERR(TAG, "Failed to open display"); + return -4; + } + + hdl->subwinMapped = FALSE; + hdl->subwinX = -1; + hdl->subwinY = -1; + hdl->subwinWidth = -1; + hdl->subwinHeight = -1; + + return 0; +} + +int tsmf_platform_set_format(TSMFGstreamerDecoder* decoder) +{ + if (!decoder) + return -1; + + if (decoder->media_type == TSMF_MAJOR_TYPE_VIDEO) + { + } + + return 0; +} + +int tsmf_platform_register_handler(TSMFGstreamerDecoder* decoder) +{ + GstBus* bus; + + if (!decoder) + return -1; + + if (!decoder->pipe) + return -1; + + bus = gst_pipeline_get_bus(GST_PIPELINE(decoder->pipe)); + +#if GST_VERSION_MAJOR > 0 + gst_bus_set_sync_handler(bus, (GstBusSyncHandler)tsmf_platform_bus_sync_handler, decoder, NULL); +#else + gst_bus_set_sync_handler(bus, (GstBusSyncHandler)tsmf_platform_bus_sync_handler, decoder); +#endif + + if (!bus) + { + WLog_ERR(TAG, "gst_pipeline_get_bus failed!"); + return 1; + } + + gst_object_unref(bus); + + return 0; +} + +int tsmf_platform_free(TSMFGstreamerDecoder* decoder) +{ + struct X11Handle* hdl = decoder->platform; + + if (!hdl) + return -1; + + if (hdl->disp) + XCloseDisplay(hdl->disp); + + if (hdl->xfwin) + munmap(0, sizeof(void*)); + + if (hdl->shmid >= 0) + close(hdl->shmid); + + free(hdl); + decoder->platform = NULL; + + return 0; +} + +int tsmf_window_create(TSMFGstreamerDecoder* decoder) +{ + struct X11Handle* hdl; + + if (decoder->media_type != TSMF_MAJOR_TYPE_VIDEO) + { + decoder->ready = TRUE; + return -3; + } + else + { + if (!decoder) + return -1; + + if (!decoder->platform) + return -1; + + hdl = (struct X11Handle*)decoder->platform; + + if (!hdl->subwin) + { + XLockDisplay(hdl->disp); + hdl->subwin = XCreateSimpleWindow(hdl->disp, *(int*)hdl->xfwin, 0, 0, 1, 1, 0, 0, 0); + XUnlockDisplay(hdl->disp); + + if (!hdl->subwin) + { + WLog_ERR(TAG, "Could not create subwindow!"); + } + } + + tsmf_window_map(decoder); + + decoder->ready = TRUE; +#if defined(WITH_XEXT) + int event, error; + XLockDisplay(hdl->disp); + hdl->has_shape = XShapeQueryExtension(hdl->disp, &event, &error); + XUnlockDisplay(hdl->disp); +#endif + } + + return 0; +} + +int tsmf_window_resize(TSMFGstreamerDecoder* decoder, int x, int y, int width, int height, + int nr_rects, RDP_RECT* rects) +{ + struct X11Handle* hdl; + + if (!decoder) + return -1; + + if (decoder->media_type != TSMF_MAJOR_TYPE_VIDEO) + { + return -3; + } + + if (!decoder->platform) + return -1; + + hdl = (struct X11Handle*)decoder->platform; + DEBUG_TSMF("resize: x=%d, y=%d, w=%d, h=%d", x, y, width, height); + + if (hdl->overlay) + { +#if GST_VERSION_MAJOR > 0 + + if (!gst_video_overlay_set_render_rectangle(hdl->overlay, 0, 0, width, height)) + { + WLog_ERR(TAG, "Could not resize overlay!"); + } + + gst_video_overlay_expose(hdl->overlay); +#else + if (!gst_x_overlay_set_render_rectangle(hdl->overlay, 0, 0, width, height)) + { + WLog_ERR(TAG, "Could not resize overlay!"); + } + + gst_x_overlay_expose(hdl->overlay); +#endif + } + + if (hdl->subwin) + { + hdl->subwinX = x; + hdl->subwinY = y; + hdl->subwinWidth = width; + hdl->subwinHeight = height; + + XLockDisplay(hdl->disp); + XMoveResizeWindow(hdl->disp, hdl->subwin, hdl->subwinX, hdl->subwinY, hdl->subwinWidth, + hdl->subwinHeight); + + /* Unmap the window if there are no visibility rects */ + if (nr_rects == 0) + tsmf_window_unmap(decoder); + else + tsmf_window_map(decoder); + +#if defined(WITH_XEXT) + if (hdl->has_shape) + { + int i; + XRectangle* xrects = NULL; + + if (nr_rects == 0) + { + xrects = calloc(1, sizeof(XRectangle)); + xrects->x = x; + xrects->y = y; + xrects->width = width; + xrects->height = height; + } + else + { + xrects = calloc(nr_rects, sizeof(XRectangle)); + } + + if (xrects) + { + for (i = 0; i < nr_rects; i++) + { + xrects[i].x = rects[i].x - x; + xrects[i].y = rects[i].y - y; + xrects[i].width = rects[i].width; + xrects[i].height = rects[i].height; + } + + XShapeCombineRectangles(hdl->disp, hdl->subwin, ShapeBounding, x, y, xrects, + nr_rects, ShapeSet, 0); + free(xrects); + } + } +#endif + XSync(hdl->disp, FALSE); + XUnlockDisplay(hdl->disp); + } + + return 0; +} + +int tsmf_window_map(TSMFGstreamerDecoder* decoder) +{ + struct X11Handle* hdl; + if (!decoder) + return -1; + + hdl = (struct X11Handle*)decoder->platform; + + /* Only need to map the window if it is not currently mapped */ + if ((hdl->subwin) && (!hdl->subwinMapped)) + { + XLockDisplay(hdl->disp); + XMapWindow(hdl->disp, hdl->subwin); + hdl->subwinMapped = TRUE; + XSync(hdl->disp, FALSE); + XUnlockDisplay(hdl->disp); + } + + return 0; +} + +int tsmf_window_unmap(TSMFGstreamerDecoder* decoder) +{ + struct X11Handle* hdl; + if (!decoder) + return -1; + + hdl = (struct X11Handle*)decoder->platform; + + /* only need to unmap window if it is currently mapped */ + if ((hdl->subwin) && (hdl->subwinMapped)) + { + XLockDisplay(hdl->disp); + XUnmapWindow(hdl->disp, hdl->subwin); + hdl->subwinMapped = FALSE; + XSync(hdl->disp, FALSE); + XUnlockDisplay(hdl->disp); + } + + return 0; +} + +int tsmf_window_destroy(TSMFGstreamerDecoder* decoder) +{ + struct X11Handle* hdl; + + if (!decoder) + return -1; + + decoder->ready = FALSE; + + if (decoder->media_type != TSMF_MAJOR_TYPE_VIDEO) + return -3; + + if (!decoder->platform) + return -1; + + hdl = (struct X11Handle*)decoder->platform; + + if (hdl->subwin) + { + XLockDisplay(hdl->disp); + XDestroyWindow(hdl->disp, hdl->subwin); + XSync(hdl->disp, FALSE); + XUnlockDisplay(hdl->disp); + } + + hdl->overlay = NULL; + hdl->subwin = 0; + hdl->subwinMapped = FALSE; + hdl->subwinX = -1; + hdl->subwinY = -1; + hdl->subwinWidth = -1; + hdl->subwinHeight = -1; + return 0; +} diff --git a/channels/tsmf/client/gstreamer/tsmf_gstreamer.c b/channels/tsmf/client/gstreamer/tsmf_gstreamer.c new file mode 100644 index 0000000..51cbc86 --- /dev/null +++ b/channels/tsmf/client/gstreamer/tsmf_gstreamer.c @@ -0,0 +1,1072 @@ +/* + * FreeRDP: A Remote Desktop Protocol Implementation + * Video Redirection Virtual Channel - GStreamer Decoder + * + * (C) Copyright 2012 HP Development Company, LLC + * (C) Copyright 2014 Thincast Technologies GmbH + * (C) Copyright 2014 Armin Novak + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include + +#include +#include +#include +#include +#include + +#include + +#if __clang__ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wparentheses-equality" +#endif /* __clang__ */ +#include +#if __clang__ +#pragma clang diagnostic pop +#endif /* __clang__ */ + +#include +#include + +#include "tsmf_constants.h" +#include "tsmf_decoder.h" +#include "tsmf_platform.h" + +#ifdef HAVE_INTTYPES_H +#include +#endif + +/* 1 second = 10,000,000 100ns units*/ +#define SEEK_TOLERANCE 10 * 1000 * 1000 + +static BOOL tsmf_gstreamer_pipeline_build(TSMFGstreamerDecoder* mdecoder); +static void tsmf_gstreamer_clean_up(TSMFGstreamerDecoder* mdecoder); +static int tsmf_gstreamer_pipeline_set_state(TSMFGstreamerDecoder* mdecoder, + GstState desired_state); +static BOOL tsmf_gstreamer_buffer_level(ITSMFDecoder* decoder); + +static const char* get_type(TSMFGstreamerDecoder* mdecoder) +{ + if (!mdecoder) + return NULL; + + switch (mdecoder->media_type) + { + case TSMF_MAJOR_TYPE_VIDEO: + return "VIDEO"; + case TSMF_MAJOR_TYPE_AUDIO: + return "AUDIO"; + default: + return "UNKNOWN"; + } +} + +static void cb_child_added(GstChildProxy* child_proxy, GObject* object, + TSMFGstreamerDecoder* mdecoder) +{ + DEBUG_TSMF("NAME: %s", G_OBJECT_TYPE_NAME(object)); + + if (!g_strcmp0(G_OBJECT_TYPE_NAME(object), "GstXvImageSink") || + !g_strcmp0(G_OBJECT_TYPE_NAME(object), "GstXImageSink") || + !g_strcmp0(G_OBJECT_TYPE_NAME(object), "GstFluVAAutoSink")) + { + gst_base_sink_set_max_lateness((GstBaseSink*)object, 10000000); /* nanoseconds */ + g_object_set(G_OBJECT(object), "sync", TRUE, NULL); /* synchronize on the clock */ + g_object_set(G_OBJECT(object), "async", TRUE, NULL); /* no async state changes */ + } + + else if (!g_strcmp0(G_OBJECT_TYPE_NAME(object), "GstAlsaSink") || + !g_strcmp0(G_OBJECT_TYPE_NAME(object), "GstPulseSink")) + { + gst_base_sink_set_max_lateness((GstBaseSink*)object, 10000000); /* nanoseconds */ + g_object_set(G_OBJECT(object), "slave-method", 1, NULL); + g_object_set(G_OBJECT(object), "buffer-time", (gint64)20000, NULL); /* microseconds */ + g_object_set(G_OBJECT(object), "drift-tolerance", (gint64)20000, NULL); /* microseconds */ + g_object_set(G_OBJECT(object), "latency-time", (gint64)10000, NULL); /* microseconds */ + g_object_set(G_OBJECT(object), "sync", TRUE, NULL); /* synchronize on the clock */ + g_object_set(G_OBJECT(object), "async", TRUE, NULL); /* no async state changes */ + } +} + +static void tsmf_gstreamer_enough_data(GstAppSrc* src, gpointer user_data) +{ + TSMFGstreamerDecoder* mdecoder = user_data; + (void)mdecoder; + DEBUG_TSMF("%s", get_type(mdecoder)); +} + +static void tsmf_gstreamer_need_data(GstAppSrc* src, guint length, gpointer user_data) +{ + TSMFGstreamerDecoder* mdecoder = user_data; + (void)mdecoder; + DEBUG_TSMF("%s length=%u", get_type(mdecoder), length); +} + +static gboolean tsmf_gstreamer_seek_data(GstAppSrc* src, guint64 offset, gpointer user_data) +{ + TSMFGstreamerDecoder* mdecoder = user_data; + (void)mdecoder; + DEBUG_TSMF("%s offset=%" PRIu64 "", get_type(mdecoder), offset); + + return TRUE; +} + +static BOOL tsmf_gstreamer_change_volume(ITSMFDecoder* decoder, UINT32 newVolume, UINT32 muted) +{ + TSMFGstreamerDecoder* mdecoder = (TSMFGstreamerDecoder*)decoder; + + if (!mdecoder || !mdecoder->pipe) + return TRUE; + + if (mdecoder->media_type == TSMF_MAJOR_TYPE_VIDEO) + return TRUE; + + mdecoder->gstMuted = (BOOL)muted; + DEBUG_TSMF("mute=[%" PRId32 "]", mdecoder->gstMuted); + mdecoder->gstVolume = (double)newVolume / (double)10000; + DEBUG_TSMF("gst_new_vol=[%f]", mdecoder->gstVolume); + + if (!mdecoder->volume) + return TRUE; + + if (!G_IS_OBJECT(mdecoder->volume)) + return TRUE; + + g_object_set(mdecoder->volume, "mute", mdecoder->gstMuted, NULL); + g_object_set(mdecoder->volume, "volume", mdecoder->gstVolume, NULL); + + return TRUE; +} + +static inline GstClockTime tsmf_gstreamer_timestamp_ms_to_gst(UINT64 ms_timestamp) +{ + /* + * Convert Microsoft 100ns timestamps to Gstreamer 1ns units. + */ + return (GstClockTime)(ms_timestamp * 100); +} + +int tsmf_gstreamer_pipeline_set_state(TSMFGstreamerDecoder* mdecoder, GstState desired_state) +{ + GstStateChangeReturn state_change; + const char* name; + const char* sname = get_type(mdecoder); + + if (!mdecoder) + return 0; + + if (!mdecoder->pipe) + return 0; /* Just in case this is called during startup or shutdown when we don't expect it + */ + + if (desired_state == mdecoder->state) + return 0; /* Redundant request - Nothing to do */ + + name = gst_element_state_get_name(desired_state); /* For debug */ + DEBUG_TSMF("%s to %s", sname, name); + state_change = gst_element_set_state(mdecoder->pipe, desired_state); + + if (state_change == GST_STATE_CHANGE_FAILURE) + { + WLog_ERR(TAG, "%s: (%s) GST_STATE_CHANGE_FAILURE.", sname, name); + } + else if (state_change == GST_STATE_CHANGE_ASYNC) + { + WLog_ERR(TAG, "%s: (%s) GST_STATE_CHANGE_ASYNC.", sname, name); + mdecoder->state = desired_state; + } + else + { + mdecoder->state = desired_state; + } + + return 0; +} + +static GstBuffer* tsmf_get_buffer_from_data(const void* raw_data, gsize size) +{ + GstBuffer* buffer; + gpointer data; + + if (!raw_data) + return NULL; + + if (size < 1) + return NULL; + + data = g_malloc(size); + + if (!data) + { + WLog_ERR(TAG, "Could not allocate %" G_GSIZE_FORMAT " bytes of data.", size); + return NULL; + } + + CopyMemory(data, raw_data, size); + +#if GST_VERSION_MAJOR > 0 + buffer = gst_buffer_new_wrapped(data, size); +#else + buffer = gst_buffer_new(); + + if (!buffer) + { + WLog_ERR(TAG, "Could not create GstBuffer"); + free(data); + return NULL; + } + + GST_BUFFER_MALLOCDATA(buffer) = data; + GST_BUFFER_SIZE(buffer) = size; + GST_BUFFER_DATA(buffer) = GST_BUFFER_MALLOCDATA(buffer); +#endif + + return buffer; +} + +static BOOL tsmf_gstreamer_set_format(ITSMFDecoder* decoder, TS_AM_MEDIA_TYPE* media_type) +{ + TSMFGstreamerDecoder* mdecoder = (TSMFGstreamerDecoder*)decoder; + + if (!mdecoder) + return FALSE; + + DEBUG_TSMF(""); + + switch (media_type->MajorType) + { + case TSMF_MAJOR_TYPE_VIDEO: + mdecoder->media_type = TSMF_MAJOR_TYPE_VIDEO; + break; + case TSMF_MAJOR_TYPE_AUDIO: + mdecoder->media_type = TSMF_MAJOR_TYPE_AUDIO; + break; + default: + return FALSE; + } + + switch (media_type->SubType) + { + case TSMF_SUB_TYPE_WVC1: + mdecoder->gst_caps = gst_caps_new_simple( + "video/x-wmv", "bitrate", G_TYPE_UINT, media_type->BitRate, "width", G_TYPE_INT, + media_type->Width, "height", G_TYPE_INT, media_type->Height, "wmvversion", + G_TYPE_INT, 3, +#if GST_VERSION_MAJOR > 0 + "format", G_TYPE_STRING, "WVC1", +#else + "format", GST_TYPE_FOURCC, GST_MAKE_FOURCC('W', 'V', 'C', '1'), +#endif + "framerate", GST_TYPE_FRACTION, media_type->SamplesPerSecond.Numerator, + media_type->SamplesPerSecond.Denominator, "pixel-aspect-ratio", GST_TYPE_FRACTION, + 1, 1, NULL); + break; + case TSMF_SUB_TYPE_MP4S: + mdecoder->gst_caps = gst_caps_new_simple( + "video/x-divx", "divxversion", G_TYPE_INT, 5, "bitrate", G_TYPE_UINT, + media_type->BitRate, "width", G_TYPE_INT, media_type->Width, "height", G_TYPE_INT, + media_type->Height, +#if GST_VERSION_MAJOR > 0 + "format", G_TYPE_STRING, "MP42", +#else + "format", GST_TYPE_FOURCC, GST_MAKE_FOURCC('M', 'P', '4', '2'), +#endif + "framerate", GST_TYPE_FRACTION, media_type->SamplesPerSecond.Numerator, + media_type->SamplesPerSecond.Denominator, NULL); + break; + case TSMF_SUB_TYPE_MP42: + mdecoder->gst_caps = gst_caps_new_simple( + "video/x-msmpeg", "msmpegversion", G_TYPE_INT, 42, "bitrate", G_TYPE_UINT, + media_type->BitRate, "width", G_TYPE_INT, media_type->Width, "height", G_TYPE_INT, + media_type->Height, +#if GST_VERSION_MAJOR > 0 + "format", G_TYPE_STRING, "MP42", +#else + "format", GST_TYPE_FOURCC, GST_MAKE_FOURCC('M', 'P', '4', '2'), +#endif + "framerate", GST_TYPE_FRACTION, media_type->SamplesPerSecond.Numerator, + media_type->SamplesPerSecond.Denominator, NULL); + break; + case TSMF_SUB_TYPE_MP43: + mdecoder->gst_caps = gst_caps_new_simple( + "video/x-msmpeg", "msmpegversion", G_TYPE_INT, 43, "bitrate", G_TYPE_UINT, + media_type->BitRate, "width", G_TYPE_INT, media_type->Width, "height", G_TYPE_INT, + media_type->Height, +#if GST_VERSION_MAJOR > 0 + "format", G_TYPE_STRING, "MP43", +#else + "format", GST_TYPE_FOURCC, GST_MAKE_FOURCC('M', 'P', '4', '3'), +#endif + "framerate", GST_TYPE_FRACTION, media_type->SamplesPerSecond.Numerator, + media_type->SamplesPerSecond.Denominator, NULL); + break; + case TSMF_SUB_TYPE_M4S2: + mdecoder->gst_caps = gst_caps_new_simple( + "video/mpeg", "mpegversion", G_TYPE_INT, 4, "width", G_TYPE_INT, media_type->Width, + "height", G_TYPE_INT, media_type->Height, +#if GST_VERSION_MAJOR > 0 + "format", G_TYPE_STRING, "M4S2", +#else + "format", GST_TYPE_FOURCC, GST_MAKE_FOURCC('M', '4', 'S', '2'), +#endif + "framerate", GST_TYPE_FRACTION, media_type->SamplesPerSecond.Numerator, + media_type->SamplesPerSecond.Denominator, NULL); + break; + case TSMF_SUB_TYPE_WMA9: + mdecoder->gst_caps = gst_caps_new_simple( + "audio/x-wma", "wmaversion", G_TYPE_INT, 3, "rate", G_TYPE_INT, + media_type->SamplesPerSecond.Numerator, "channels", G_TYPE_INT, + media_type->Channels, "bitrate", G_TYPE_INT, media_type->BitRate, "depth", + G_TYPE_INT, media_type->BitsPerSample, "width", G_TYPE_INT, + media_type->BitsPerSample, "block_align", G_TYPE_INT, media_type->BlockAlign, NULL); + break; + case TSMF_SUB_TYPE_WMA1: + mdecoder->gst_caps = gst_caps_new_simple( + "audio/x-wma", "wmaversion", G_TYPE_INT, 1, "rate", G_TYPE_INT, + media_type->SamplesPerSecond.Numerator, "channels", G_TYPE_INT, + media_type->Channels, "bitrate", G_TYPE_INT, media_type->BitRate, "depth", + G_TYPE_INT, media_type->BitsPerSample, "width", G_TYPE_INT, + media_type->BitsPerSample, "block_align", G_TYPE_INT, media_type->BlockAlign, NULL); + break; + case TSMF_SUB_TYPE_WMA2: + mdecoder->gst_caps = gst_caps_new_simple( + "audio/x-wma", "wmaversion", G_TYPE_INT, 2, "rate", G_TYPE_INT, + media_type->SamplesPerSecond.Numerator, "channels", G_TYPE_INT, + media_type->Channels, "bitrate", G_TYPE_INT, media_type->BitRate, "depth", + G_TYPE_INT, media_type->BitsPerSample, "width", G_TYPE_INT, + media_type->BitsPerSample, "block_align", G_TYPE_INT, media_type->BlockAlign, NULL); + break; + case TSMF_SUB_TYPE_MP3: + mdecoder->gst_caps = + gst_caps_new_simple("audio/mpeg", "mpegversion", G_TYPE_INT, 1, "layer", G_TYPE_INT, + 3, "rate", G_TYPE_INT, media_type->SamplesPerSecond.Numerator, + "channels", G_TYPE_INT, media_type->Channels, NULL); + break; + case TSMF_SUB_TYPE_WMV1: + mdecoder->gst_caps = gst_caps_new_simple( + "video/x-wmv", "bitrate", G_TYPE_UINT, media_type->BitRate, "width", G_TYPE_INT, + media_type->Width, "height", G_TYPE_INT, media_type->Height, "wmvversion", + G_TYPE_INT, 1, +#if GST_VERSION_MAJOR > 0 + "format", G_TYPE_STRING, "WMV1", +#else + "format", GST_TYPE_FOURCC, GST_MAKE_FOURCC('W', 'M', 'V', '1'), +#endif + "framerate", GST_TYPE_FRACTION, media_type->SamplesPerSecond.Numerator, + media_type->SamplesPerSecond.Denominator, NULL); + break; + case TSMF_SUB_TYPE_WMV2: + mdecoder->gst_caps = gst_caps_new_simple( + "video/x-wmv", "width", G_TYPE_INT, media_type->Width, "height", G_TYPE_INT, + media_type->Height, "wmvversion", G_TYPE_INT, 2, +#if GST_VERSION_MAJOR > 0 + "format", G_TYPE_STRING, "WMV2", +#else + "format", GST_TYPE_FOURCC, GST_MAKE_FOURCC('W', 'M', 'V', '2'), +#endif + "framerate", GST_TYPE_FRACTION, media_type->SamplesPerSecond.Numerator, + media_type->SamplesPerSecond.Denominator, "pixel-aspect-ratio", GST_TYPE_FRACTION, + 1, 1, NULL); + break; + case TSMF_SUB_TYPE_WMV3: + mdecoder->gst_caps = gst_caps_new_simple( + "video/x-wmv", "bitrate", G_TYPE_UINT, media_type->BitRate, "width", G_TYPE_INT, + media_type->Width, "height", G_TYPE_INT, media_type->Height, "wmvversion", + G_TYPE_INT, 3, +#if GST_VERSION_MAJOR > 0 + "format", G_TYPE_STRING, "WMV3", +#else + "format", GST_TYPE_FOURCC, GST_MAKE_FOURCC('W', 'M', 'V', '3'), +#endif + "framerate", GST_TYPE_FRACTION, media_type->SamplesPerSecond.Numerator, + media_type->SamplesPerSecond.Denominator, "pixel-aspect-ratio", GST_TYPE_FRACTION, + 1, 1, NULL); + break; + case TSMF_SUB_TYPE_AVC1: + case TSMF_SUB_TYPE_H264: + mdecoder->gst_caps = gst_caps_new_simple( + "video/x-h264", "width", G_TYPE_INT, media_type->Width, "height", G_TYPE_INT, + media_type->Height, "framerate", GST_TYPE_FRACTION, + media_type->SamplesPerSecond.Numerator, media_type->SamplesPerSecond.Denominator, + "pixel-aspect-ratio", GST_TYPE_FRACTION, 1, 1, "stream-format", G_TYPE_STRING, + "byte-stream", "alignment", G_TYPE_STRING, "nal", NULL); + break; + case TSMF_SUB_TYPE_AC3: + mdecoder->gst_caps = gst_caps_new_simple( + "audio/x-ac3", "rate", G_TYPE_INT, media_type->SamplesPerSecond.Numerator, + "channels", G_TYPE_INT, media_type->Channels, NULL); + break; + case TSMF_SUB_TYPE_AAC: + + /* For AAC the pFormat is a HEAACWAVEINFO struct, and the codec data + is at the end of it. See + http://msdn.microsoft.com/en-us/library/dd757806.aspx */ + if (media_type->ExtraData) + { + if (media_type->ExtraDataSize < 12) + return FALSE; + media_type->ExtraData += 12; + media_type->ExtraDataSize -= 12; + } + + mdecoder->gst_caps = gst_caps_new_simple( + "audio/mpeg", "rate", G_TYPE_INT, media_type->SamplesPerSecond.Numerator, + "channels", G_TYPE_INT, media_type->Channels, "mpegversion", G_TYPE_INT, 4, + "framed", G_TYPE_BOOLEAN, TRUE, "stream-format", G_TYPE_STRING, "raw", NULL); + break; + case TSMF_SUB_TYPE_MP1A: + mdecoder->gst_caps = + gst_caps_new_simple("audio/mpeg", "mpegversion", G_TYPE_INT, 1, "channels", + G_TYPE_INT, media_type->Channels, NULL); + break; + case TSMF_SUB_TYPE_MP1V: + mdecoder->gst_caps = + gst_caps_new_simple("video/mpeg", "mpegversion", G_TYPE_INT, 1, "width", G_TYPE_INT, + media_type->Width, "height", G_TYPE_INT, media_type->Height, + "systemstream", G_TYPE_BOOLEAN, FALSE, NULL); + break; + case TSMF_SUB_TYPE_YUY2: +#if GST_VERSION_MAJOR > 0 + mdecoder->gst_caps = gst_caps_new_simple( + "video/x-raw", "format", G_TYPE_STRING, "YUY2", "width", G_TYPE_INT, + media_type->Width, "height", G_TYPE_INT, media_type->Height, NULL); +#else + mdecoder->gst_caps = gst_caps_new_simple( + "video/x-raw-yuv", "format", G_TYPE_STRING, "YUY2", "width", G_TYPE_INT, + media_type->Width, "height", G_TYPE_INT, media_type->Height, "framerate", + GST_TYPE_FRACTION, media_type->SamplesPerSecond.Numerator, + media_type->SamplesPerSecond.Denominator, NULL); +#endif + break; + case TSMF_SUB_TYPE_MP2V: + mdecoder->gst_caps = gst_caps_new_simple("video/mpeg", "mpegversion", G_TYPE_INT, 2, + "systemstream", G_TYPE_BOOLEAN, FALSE, NULL); + break; + case TSMF_SUB_TYPE_MP2A: + mdecoder->gst_caps = + gst_caps_new_simple("audio/mpeg", "mpegversion", G_TYPE_INT, 1, "rate", G_TYPE_INT, + media_type->SamplesPerSecond.Numerator, "channels", G_TYPE_INT, + media_type->Channels, NULL); + break; + case TSMF_SUB_TYPE_FLAC: + mdecoder->gst_caps = gst_caps_new_simple("audio/x-flac", "", NULL); + break; + default: + WLog_ERR(TAG, "unknown format:(%d).", media_type->SubType); + return FALSE; + } + + if (media_type->ExtraDataSize > 0) + { + GstBuffer* buffer; + DEBUG_TSMF("Extra data available (%" PRIu32 ")", media_type->ExtraDataSize); + buffer = tsmf_get_buffer_from_data(media_type->ExtraData, media_type->ExtraDataSize); + + if (!buffer) + { + WLog_ERR(TAG, "could not allocate GstBuffer!"); + return FALSE; + } + + gst_caps_set_simple(mdecoder->gst_caps, "codec_data", GST_TYPE_BUFFER, buffer, NULL); + } + + DEBUG_TSMF("%p format '%s'", (void*)mdecoder, gst_caps_to_string(mdecoder->gst_caps)); + tsmf_platform_set_format(mdecoder); + + /* Create the pipeline... */ + if (!tsmf_gstreamer_pipeline_build(mdecoder)) + return FALSE; + + return TRUE; +} + +void tsmf_gstreamer_clean_up(TSMFGstreamerDecoder* mdecoder) +{ + if (!mdecoder || !mdecoder->pipe) + return; + + if (mdecoder->pipe && GST_OBJECT_REFCOUNT_VALUE(mdecoder->pipe) > 0) + { + tsmf_gstreamer_pipeline_set_state(mdecoder, GST_STATE_NULL); + gst_object_unref(mdecoder->pipe); + } + + mdecoder->ready = FALSE; + mdecoder->paused = FALSE; + + mdecoder->pipe = NULL; + mdecoder->src = NULL; + mdecoder->queue = NULL; +} + +BOOL tsmf_gstreamer_pipeline_build(TSMFGstreamerDecoder* mdecoder) +{ +#if GST_VERSION_MAJOR > 0 + const char* video = + "appsrc name=videosource ! queue2 name=videoqueue ! decodebin name=videodecoder !"; + const char* audio = + "appsrc name=audiosource ! queue2 name=audioqueue ! decodebin name=audiodecoder ! " + "audioconvert ! audiorate ! audioresample ! volume name=audiovolume !"; +#else + const char* video = + "appsrc name=videosource ! queue2 name=videoqueue ! decodebin2 name=videodecoder !"; + const char* audio = + "appsrc name=audiosource ! queue2 name=audioqueue ! decodebin2 name=audiodecoder ! " + "audioconvert ! audiorate ! audioresample ! volume name=audiovolume !"; +#endif + char pipeline[1024]; + + if (!mdecoder) + return FALSE; + + /* TODO: Construction of the pipeline from a string allows easy overwrite with arguments. + * The only fixed elements necessary are appsrc and the volume element for audio streams. + * The rest could easily be provided in gstreamer pipeline notation from command line. */ + if (mdecoder->media_type == TSMF_MAJOR_TYPE_VIDEO) + sprintf_s(pipeline, sizeof(pipeline), "%s %s name=videosink", video, + tsmf_platform_get_video_sink()); + else + sprintf_s(pipeline, sizeof(pipeline), "%s %s name=audiosink", audio, + tsmf_platform_get_audio_sink()); + + DEBUG_TSMF("pipeline=%s", pipeline); + mdecoder->pipe = gst_parse_launch(pipeline, NULL); + + if (!mdecoder->pipe) + { + WLog_ERR(TAG, "Failed to create new pipe"); + return FALSE; + } + + if (mdecoder->media_type == TSMF_MAJOR_TYPE_VIDEO) + mdecoder->src = gst_bin_get_by_name(GST_BIN(mdecoder->pipe), "videosource"); + else + mdecoder->src = gst_bin_get_by_name(GST_BIN(mdecoder->pipe), "audiosource"); + + if (!mdecoder->src) + { + WLog_ERR(TAG, "Failed to get appsrc"); + return FALSE; + } + + if (mdecoder->media_type == TSMF_MAJOR_TYPE_VIDEO) + mdecoder->queue = gst_bin_get_by_name(GST_BIN(mdecoder->pipe), "videoqueue"); + else + mdecoder->queue = gst_bin_get_by_name(GST_BIN(mdecoder->pipe), "audioqueue"); + + if (!mdecoder->queue) + { + WLog_ERR(TAG, "Failed to get queue"); + return FALSE; + } + + if (mdecoder->media_type == TSMF_MAJOR_TYPE_VIDEO) + mdecoder->outsink = gst_bin_get_by_name(GST_BIN(mdecoder->pipe), "videosink"); + else + mdecoder->outsink = gst_bin_get_by_name(GST_BIN(mdecoder->pipe), "audiosink"); + + if (!mdecoder->outsink) + { + WLog_ERR(TAG, "Failed to get sink"); + return FALSE; + } + + g_signal_connect(mdecoder->outsink, "child-added", G_CALLBACK(cb_child_added), mdecoder); + + if (mdecoder->media_type == TSMF_MAJOR_TYPE_AUDIO) + { + mdecoder->volume = gst_bin_get_by_name(GST_BIN(mdecoder->pipe), "audiovolume"); + + if (!mdecoder->volume) + { + WLog_ERR(TAG, "Failed to get volume"); + return FALSE; + } + + tsmf_gstreamer_change_volume((ITSMFDecoder*)mdecoder, mdecoder->gstVolume * ((double)10000), + mdecoder->gstMuted); + } + + tsmf_platform_register_handler(mdecoder); + /* AppSrc settings */ + GstAppSrcCallbacks callbacks = { + tsmf_gstreamer_need_data, tsmf_gstreamer_enough_data, tsmf_gstreamer_seek_data, { NULL } + }; + g_object_set(mdecoder->src, "format", GST_FORMAT_TIME, NULL); + g_object_set(mdecoder->src, "is-live", FALSE, NULL); + g_object_set(mdecoder->src, "block", FALSE, NULL); + g_object_set(mdecoder->src, "blocksize", 1024, NULL); + gst_app_src_set_caps((GstAppSrc*)mdecoder->src, mdecoder->gst_caps); + gst_app_src_set_callbacks((GstAppSrc*)mdecoder->src, &callbacks, mdecoder, NULL); + gst_app_src_set_stream_type((GstAppSrc*)mdecoder->src, GST_APP_STREAM_TYPE_SEEKABLE); + gst_app_src_set_latency((GstAppSrc*)mdecoder->src, 0, -1); + gst_app_src_set_max_bytes((GstAppSrc*)mdecoder->src, (guint64)0); // unlimited + g_object_set(G_OBJECT(mdecoder->queue), "use-buffering", FALSE, NULL); + g_object_set(G_OBJECT(mdecoder->queue), "use-rate-estimate", FALSE, NULL); + g_object_set(G_OBJECT(mdecoder->queue), "max-size-buffers", 0, NULL); + g_object_set(G_OBJECT(mdecoder->queue), "max-size-bytes", 0, NULL); + g_object_set(G_OBJECT(mdecoder->queue), "max-size-time", (guint64)0, NULL); + + /* Only set these properties if not an autosink, otherwise we will set properties when real + * sinks are added */ + if (!g_strcmp0(G_OBJECT_TYPE_NAME(mdecoder->outsink), "GstAutoVideoSink") && + !g_strcmp0(G_OBJECT_TYPE_NAME(mdecoder->outsink), "GstAutoAudioSink")) + { + if (mdecoder->media_type == TSMF_MAJOR_TYPE_VIDEO) + { + gst_base_sink_set_max_lateness((GstBaseSink*)mdecoder->outsink, + 10000000); /* nanoseconds */ + } + else + { + gst_base_sink_set_max_lateness((GstBaseSink*)mdecoder->outsink, + 10000000); /* nanoseconds */ + g_object_set(G_OBJECT(mdecoder->outsink), "buffer-time", (gint64)20000, + NULL); /* microseconds */ + g_object_set(G_OBJECT(mdecoder->outsink), "drift-tolerance", (gint64)20000, + NULL); /* microseconds */ + g_object_set(G_OBJECT(mdecoder->outsink), "latency-time", (gint64)10000, + NULL); /* microseconds */ + g_object_set(G_OBJECT(mdecoder->outsink), "slave-method", 1, NULL); + } + g_object_set(G_OBJECT(mdecoder->outsink), "sync", TRUE, + NULL); /* synchronize on the clock */ + g_object_set(G_OBJECT(mdecoder->outsink), "async", TRUE, NULL); /* no async state changes */ + } + + tsmf_window_create(mdecoder); + tsmf_gstreamer_pipeline_set_state(mdecoder, GST_STATE_READY); + tsmf_gstreamer_pipeline_set_state(mdecoder, GST_STATE_PLAYING); + mdecoder->pipeline_start_time_valid = 0; + mdecoder->shutdown = 0; + mdecoder->paused = FALSE; + + GST_DEBUG_BIN_TO_DOT_FILE(GST_BIN(mdecoder->pipe), GST_DEBUG_GRAPH_SHOW_ALL, + get_type(mdecoder)); + + return TRUE; +} + +static BOOL tsmf_gstreamer_decodeEx(ITSMFDecoder* decoder, const BYTE* data, UINT32 data_size, + UINT32 extensions, UINT64 start_time, UINT64 end_time, + UINT64 duration) +{ + GstBuffer* gst_buf; + TSMFGstreamerDecoder* mdecoder = (TSMFGstreamerDecoder*)decoder; + UINT64 sample_time = tsmf_gstreamer_timestamp_ms_to_gst(start_time); + BOOL useTimestamps = TRUE; + + if (!mdecoder) + { + WLog_ERR(TAG, "Decoder not initialized!"); + return FALSE; + } + + /* + * This function is always called from a stream-specific thread. + * It should be alright to block here if necessary. + * We don't expect to block here often, since the pipeline should + * have more than enough buffering. + */ + DEBUG_TSMF( + "%s. Start:(%" PRIu64 ") End:(%" PRIu64 ") Duration:(%" PRIu64 ") Last Start:(%" PRIu64 ")", + get_type(mdecoder), start_time, end_time, duration, mdecoder->last_sample_start_time); + + if (mdecoder->shutdown) + { + WLog_ERR(TAG, "decodeEx called on shutdown decoder"); + return TRUE; + } + + if (mdecoder->gst_caps == NULL) + { + WLog_ERR(TAG, "tsmf_gstreamer_set_format not called or invalid format."); + return FALSE; + } + + if (!mdecoder->pipe) + tsmf_gstreamer_pipeline_build(mdecoder); + + if (!mdecoder->src) + { + WLog_ERR( + TAG, + "failed to construct pipeline correctly. Unable to push buffer to source element."); + return FALSE; + } + + gst_buf = tsmf_get_buffer_from_data(data, data_size); + + if (gst_buf == NULL) + { + WLog_ERR(TAG, "tsmf_get_buffer_from_data(%p, %" PRIu32 ") failed.", (void*)data, data_size); + return FALSE; + } + + /* Relative timestamping will sometimes be set to 0 + * so we ignore these timestamps just to be safe(bit 8) + */ + if (extensions & 0x00000080) + { + DEBUG_TSMF("Ignoring the timestamps - relative - bit 8"); + useTimestamps = FALSE; + } + + /* If no timestamps exist then we dont want to look at the timestamp values (bit 7) */ + if (extensions & 0x00000040) + { + DEBUG_TSMF("Ignoring the timestamps - none - bit 7"); + useTimestamps = FALSE; + } + + /* If performing a seek */ + if (mdecoder->seeking) + { + mdecoder->seeking = FALSE; + tsmf_gstreamer_pipeline_set_state(mdecoder, GST_STATE_PAUSED); + mdecoder->pipeline_start_time_valid = 0; + } + + if (mdecoder->pipeline_start_time_valid) + { + DEBUG_TSMF("%s start time %" PRIu64 "", get_type(mdecoder), start_time); + + /* Adjusted the condition for a seek to be based on start time only + * WMV1 and WMV2 files in particular have bad end time and duration values + * there seems to be no real side effects of just using the start time instead + */ + UINT64 minTime = mdecoder->last_sample_start_time - (UINT64)SEEK_TOLERANCE; + UINT64 maxTime = mdecoder->last_sample_start_time + (UINT64)SEEK_TOLERANCE; + + /* Make sure the minTime stops at 0 , should we be at the beginning of the stream */ + if (mdecoder->last_sample_start_time < (UINT64)SEEK_TOLERANCE) + minTime = 0; + + /* If the start_time is valid and different from the previous start time by more than the + * seek tolerance, then we have a seek condition */ + if (((start_time > maxTime) || (start_time < minTime)) && useTimestamps) + { + DEBUG_TSMF("tsmf_gstreamer_decodeEx: start_time=[%" PRIu64 + "] > last_sample_start_time=[%" PRIu64 "] OR ", + start_time, mdecoder->last_sample_start_time); + DEBUG_TSMF("tsmf_gstreamer_decodeEx: start_time=[%" PRIu64 + "] < last_sample_start_time=[%" PRIu64 "] with", + start_time, mdecoder->last_sample_start_time); + DEBUG_TSMF( + "tsmf_gstreamer_decodeEX: a tolerance of more than [%lu] from the last sample", + SEEK_TOLERANCE); + DEBUG_TSMF("tsmf_gstreamer_decodeEX: minTime=[%" PRIu64 "] maxTime=[%" PRIu64 "]", + minTime, maxTime); + + mdecoder->seeking = TRUE; + + /* since we cant make the gstreamer pipeline jump to the new start time after a seek - + * we just maintain a offset between realtime and gstreamer time + */ + mdecoder->seek_offset = start_time; + } + } + else + { + DEBUG_TSMF("%s start time %" PRIu64 "", get_type(mdecoder), start_time); + /* Always set base/start time to 0. Will use seek offset to translate real buffer times + * back to 0. This allows the video to be started from anywhere and the ability to handle + * seeks without rebuilding the pipeline, etc. since that is costly + */ + gst_element_set_base_time(mdecoder->pipe, tsmf_gstreamer_timestamp_ms_to_gst(0)); + gst_element_set_start_time(mdecoder->pipe, tsmf_gstreamer_timestamp_ms_to_gst(0)); + mdecoder->pipeline_start_time_valid = 1; + + /* Set the seek offset if buffer has valid timestamps. */ + if (useTimestamps) + mdecoder->seek_offset = start_time; + + if (!gst_element_seek(mdecoder->pipe, 1.0, GST_FORMAT_TIME, GST_SEEK_FLAG_FLUSH, + GST_SEEK_TYPE_SET, 0, GST_SEEK_TYPE_NONE, GST_CLOCK_TIME_NONE)) + { + WLog_ERR(TAG, "seek failed"); + } + } + +#if GST_VERSION_MAJOR > 0 + if (useTimestamps) + GST_BUFFER_PTS(gst_buf) = + sample_time - tsmf_gstreamer_timestamp_ms_to_gst(mdecoder->seek_offset); + else + GST_BUFFER_PTS(gst_buf) = GST_CLOCK_TIME_NONE; +#else + if (useTimestamps) + GST_BUFFER_TIMESTAMP(gst_buf) = + sample_time - tsmf_gstreamer_timestamp_ms_to_gst(mdecoder->seek_offset); + else + GST_BUFFER_TIMESTAMP(gst_buf) = GST_CLOCK_TIME_NONE; +#endif + GST_BUFFER_DURATION(gst_buf) = GST_CLOCK_TIME_NONE; + GST_BUFFER_OFFSET(gst_buf) = GST_BUFFER_OFFSET_NONE; +#if GST_VERSION_MAJOR > 0 +#else + gst_buffer_set_caps(gst_buf, mdecoder->gst_caps); +#endif + gst_app_src_push_buffer(GST_APP_SRC(mdecoder->src), gst_buf); + + /* Should only update the last timestamps if the current ones are valid */ + if (useTimestamps) + { + mdecoder->last_sample_start_time = start_time; + mdecoder->last_sample_end_time = end_time; + } + + if (mdecoder->pipe && (GST_STATE(mdecoder->pipe) != GST_STATE_PLAYING)) + { + DEBUG_TSMF("%s: state=%s", get_type(mdecoder), + gst_element_state_get_name(GST_STATE(mdecoder->pipe))); + + DEBUG_TSMF("%s Paused: %" PRIi32 " Shutdown: %i Ready: %" PRIi32 "", get_type(mdecoder), + mdecoder->paused, mdecoder->shutdown, mdecoder->ready); + if (!mdecoder->paused && !mdecoder->shutdown && mdecoder->ready) + tsmf_gstreamer_pipeline_set_state(mdecoder, GST_STATE_PLAYING); + } + + return TRUE; +} + +static BOOL tsmf_gstreamer_control(ITSMFDecoder* decoder, ITSMFControlMsg control_msg, UINT32* arg) +{ + TSMFGstreamerDecoder* mdecoder = (TSMFGstreamerDecoder*)decoder; + + if (!mdecoder) + { + WLog_ERR(TAG, "Control called with no decoder!"); + return TRUE; + } + + if (control_msg == Control_Pause) + { + DEBUG_TSMF("Control_Pause %s", get_type(mdecoder)); + + if (mdecoder->paused) + { + WLog_ERR(TAG, "%s: Ignoring Control_Pause, already received!", get_type(mdecoder)); + return TRUE; + } + + tsmf_gstreamer_pipeline_set_state(mdecoder, GST_STATE_PAUSED); + mdecoder->shutdown = 0; + mdecoder->paused = TRUE; + } + else if (control_msg == Control_Resume) + { + DEBUG_TSMF("Control_Resume %s", get_type(mdecoder)); + + if (!mdecoder->paused && !mdecoder->shutdown) + { + WLog_ERR(TAG, "%s: Ignoring Control_Resume, already received!", get_type(mdecoder)); + return TRUE; + } + + mdecoder->shutdown = 0; + mdecoder->paused = FALSE; + } + else if (control_msg == Control_Stop) + { + DEBUG_TSMF("Control_Stop %s", get_type(mdecoder)); + + if (mdecoder->shutdown) + { + WLog_ERR(TAG, "%s: Ignoring Control_Stop, already received!", get_type(mdecoder)); + return TRUE; + } + + /* Reset stamps, flush buffers, etc */ + if (mdecoder->pipe) + { + tsmf_gstreamer_pipeline_set_state(mdecoder, GST_STATE_NULL); + tsmf_window_destroy(mdecoder); + tsmf_gstreamer_clean_up(mdecoder); + } + mdecoder->seek_offset = 0; + mdecoder->pipeline_start_time_valid = 0; + mdecoder->shutdown = 1; + } + else if (control_msg == Control_Restart) + { + DEBUG_TSMF("Control_Restart %s", get_type(mdecoder)); + mdecoder->shutdown = 0; + mdecoder->paused = FALSE; + + if (mdecoder->pipeline_start_time_valid) + tsmf_gstreamer_pipeline_set_state(mdecoder, GST_STATE_PLAYING); + } + else + WLog_ERR(TAG, "Unknown control message %08x", control_msg); + + return TRUE; +} + +static BOOL tsmf_gstreamer_buffer_level(ITSMFDecoder* decoder) +{ + TSMFGstreamerDecoder* mdecoder = (TSMFGstreamerDecoder*)decoder; + DEBUG_TSMF(""); + + if (!mdecoder) + return FALSE; + + guint clbuff = 0; + + if (G_IS_OBJECT(mdecoder->queue)) + g_object_get(mdecoder->queue, "current-level-buffers", &clbuff, NULL); + + DEBUG_TSMF("%s buffer level %u", get_type(mdecoder), clbuff); + return clbuff; +} + +static void tsmf_gstreamer_free(ITSMFDecoder* decoder) +{ + TSMFGstreamerDecoder* mdecoder = (TSMFGstreamerDecoder*)decoder; + DEBUG_TSMF("%s", get_type(mdecoder)); + + if (mdecoder) + { + tsmf_window_destroy(mdecoder); + tsmf_gstreamer_clean_up(mdecoder); + + if (mdecoder->gst_caps) + gst_caps_unref(mdecoder->gst_caps); + + tsmf_platform_free(mdecoder); + ZeroMemory(mdecoder, sizeof(TSMFGstreamerDecoder)); + free(mdecoder); + mdecoder = NULL; + } +} + +static UINT64 tsmf_gstreamer_get_running_time(ITSMFDecoder* decoder) +{ + TSMFGstreamerDecoder* mdecoder = (TSMFGstreamerDecoder*)decoder; + + if (!mdecoder) + return 0; + + if (!mdecoder->outsink) + return mdecoder->last_sample_start_time; + + if (!mdecoder->pipe) + return 0; + + GstFormat fmt = GST_FORMAT_TIME; + gint64 pos = 0; +#if GST_VERSION_MAJOR > 0 + gst_element_query_position(mdecoder->pipe, fmt, &pos); +#else + gst_element_query_position(mdecoder->pipe, &fmt, &pos); +#endif + return (UINT64)(pos / 100 + mdecoder->seek_offset); +} + +static BOOL tsmf_gstreamer_update_rendering_area(ITSMFDecoder* decoder, int newX, int newY, + int newWidth, int newHeight, int numRectangles, + RDP_RECT* rectangles) +{ + TSMFGstreamerDecoder* mdecoder = (TSMFGstreamerDecoder*)decoder; + DEBUG_TSMF("x=%d, y=%d, w=%d, h=%d, rect=%d", newX, newY, newWidth, newHeight, numRectangles); + + if (mdecoder->media_type == TSMF_MAJOR_TYPE_VIDEO) + { + return tsmf_window_resize(mdecoder, newX, newY, newWidth, newHeight, numRectangles, + rectangles) == 0; + } + + return TRUE; +} + +static BOOL tsmf_gstreamer_ack(ITSMFDecoder* decoder, BOOL (*cb)(void*, BOOL), void* stream) +{ + TSMFGstreamerDecoder* mdecoder = (TSMFGstreamerDecoder*)decoder; + DEBUG_TSMF(""); + mdecoder->ack_cb = NULL; + mdecoder->stream = stream; + return TRUE; +} + +static BOOL tsmf_gstreamer_sync(ITSMFDecoder* decoder, void (*cb)(void*), void* stream) +{ + TSMFGstreamerDecoder* mdecoder = (TSMFGstreamerDecoder*)decoder; + DEBUG_TSMF(""); + mdecoder->sync_cb = NULL; + mdecoder->stream = stream; + return TRUE; +} + +#ifdef BUILTIN_CHANNELS +#define freerdp_tsmf_client_subsystem_entry gstreamer_freerdp_tsmf_client_decoder_subsystem_entry +#else +#define freerdp_tsmf_client_subsystem_entry FREERDP_API freerdp_tsmf_client_decoder_subsystem_entry +#endif + +ITSMFDecoder* freerdp_tsmf_client_subsystem_entry(void) +{ + TSMFGstreamerDecoder* decoder; + +#if GST_CHECK_VERSION(0, 10, 31) + if (!gst_is_initialized()) + { + gst_init(NULL, NULL); + } +#else + gst_init(NULL, NULL); +#endif + + decoder = calloc(1, sizeof(TSMFGstreamerDecoder)); + + if (!decoder) + return NULL; + + decoder->iface.SetFormat = tsmf_gstreamer_set_format; + decoder->iface.Decode = NULL; + decoder->iface.GetDecodedData = NULL; + decoder->iface.GetDecodedFormat = NULL; + decoder->iface.GetDecodedDimension = NULL; + decoder->iface.GetRunningTime = tsmf_gstreamer_get_running_time; + decoder->iface.UpdateRenderingArea = tsmf_gstreamer_update_rendering_area; + decoder->iface.Free = tsmf_gstreamer_free; + decoder->iface.Control = tsmf_gstreamer_control; + decoder->iface.DecodeEx = tsmf_gstreamer_decodeEx; + decoder->iface.ChangeVolume = tsmf_gstreamer_change_volume; + decoder->iface.BufferLevel = tsmf_gstreamer_buffer_level; + decoder->iface.SetAckFunc = tsmf_gstreamer_ack; + decoder->iface.SetSyncFunc = tsmf_gstreamer_sync; + decoder->paused = FALSE; + decoder->gstVolume = 0.5; + decoder->gstMuted = FALSE; + decoder->state = GST_STATE_VOID_PENDING; /* No real state yet */ + decoder->last_sample_start_time = 0; + decoder->last_sample_end_time = 0; + decoder->seek_offset = 0; + decoder->seeking = FALSE; + + if (tsmf_platform_create(decoder) < 0) + { + free(decoder); + return NULL; + } + + return (ITSMFDecoder*)decoder; +} diff --git a/channels/tsmf/client/gstreamer/tsmf_platform.h b/channels/tsmf/client/gstreamer/tsmf_platform.h new file mode 100644 index 0000000..b6f0b33 --- /dev/null +++ b/channels/tsmf/client/gstreamer/tsmf_platform.h @@ -0,0 +1,85 @@ +/* + * FreeRDP: A Remote Desktop Protocol Implementation + * Video Redirection Virtual Channel - GStreamer Decoder + * platform specific functions + * + * (C) Copyright 2014 Thincast Technologies GmbH + * (C) Copyright 2014 Armin Novak + * + * Licensed 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 FREERDP_CHANNEL_TSMF_CLIENT_GST_PLATFORM_H +#define FREERDP_CHANNEL_TSMF_CLIENT_GST_PLATFORM_H + +#include +#include + +typedef struct _TSMFGstreamerDecoder +{ + ITSMFDecoder iface; + + int media_type; /* TSMF_MAJOR_TYPE_AUDIO or TSMF_MAJOR_TYPE_VIDEO */ + + gint64 duration; + + GstState state; + GstCaps* gst_caps; + + GstElement* pipe; + GstElement* src; + GstElement* queue; + GstElement* outsink; + GstElement* volume; + + BOOL ready; + BOOL paused; + UINT64 last_sample_start_time; + UINT64 last_sample_end_time; + BOOL seeking; + UINT64 seek_offset; + + double gstVolume; + BOOL gstMuted; + + int pipeline_start_time_valid; /* We've set the start time and have not reset the pipeline */ + int shutdown; /* The decoder stream is shutting down */ + + void* platform; + + BOOL (*ack_cb)(void*, BOOL); + void (*sync_cb)(void*); + void* stream; + +} TSMFGstreamerDecoder; + +const char* tsmf_platform_get_video_sink(void); +const char* tsmf_platform_get_audio_sink(void); + +int tsmf_platform_create(TSMFGstreamerDecoder* decoder); +int tsmf_platform_set_format(TSMFGstreamerDecoder* decoder); +int tsmf_platform_register_handler(TSMFGstreamerDecoder* decoder); +int tsmf_platform_free(TSMFGstreamerDecoder* decoder); + +int tsmf_window_create(TSMFGstreamerDecoder* decoder); +int tsmf_window_resize(TSMFGstreamerDecoder* decoder, int x, int y, int width, int height, + int nr_rect, RDP_RECT* visible); +int tsmf_window_destroy(TSMFGstreamerDecoder* decoder); + +int tsmf_window_map(TSMFGstreamerDecoder* decoder); +int tsmf_window_unmap(TSMFGstreamerDecoder* decoder); + +BOOL tsmf_gstreamer_add_pad(TSMFGstreamerDecoder* mdecoder); +void tsmf_gstreamer_remove_pad(TSMFGstreamerDecoder* mdecoder); + +#endif /* FREERDP_CHANNEL_TSMF_CLIENT_GST_PLATFORM_H */ diff --git a/channels/tsmf/client/oss/CMakeLists.txt b/channels/tsmf/client/oss/CMakeLists.txt new file mode 100644 index 0000000..8f9e627 --- /dev/null +++ b/channels/tsmf/client/oss/CMakeLists.txt @@ -0,0 +1,28 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright (c) 2015 Rozhuk Ivan +# +# Licensed 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. + +define_channel_client_subsystem("tsmf" "oss" "audio") + +set(${MODULE_PREFIX}_SRCS + tsmf_oss.c) + +include_directories(..) +include_directories(${OSS_INCLUDE_DIRS}) + +add_channel_client_subsystem_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} "" TRUE "") +target_link_libraries(${MODULE_NAME} winpr) + diff --git a/channels/tsmf/client/oss/tsmf_oss.c b/channels/tsmf/client/oss/tsmf_oss.c new file mode 100644 index 0000000..774affb --- /dev/null +++ b/channels/tsmf/client/oss/tsmf_oss.c @@ -0,0 +1,252 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Video Redirection Virtual Channel - OSS Audio Device + * + * Copyright (c) 2015 Rozhuk Ivan + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#if defined(__OpenBSD__) +#include +#else +#include +#endif +#include + +#include +#include + +#include "tsmf_audio.h" + +typedef struct _TSMFOSSAudioDevice +{ + ITSMFAudioDevice iface; + + char dev_name[PATH_MAX]; + int pcm_handle; + + UINT32 sample_rate; + UINT32 channels; + UINT32 bits_per_sample; + + UINT32 data_size_last; +} TSMFOssAudioDevice; + +#define OSS_LOG_ERR(_text, _error) \ + if (_error != 0) \ + WLog_ERR(TAG, "%s: %i - %s", _text, _error, strerror(_error)); + +static BOOL tsmf_oss_open(ITSMFAudioDevice* audio, const char* device) +{ + int tmp; + TSMFOssAudioDevice* oss = (TSMFOssAudioDevice*)audio; + + if (oss == NULL || oss->pcm_handle != -1) + return FALSE; + + if (device == NULL) /* Default device. */ + { + strncpy(oss->dev_name, "/dev/dsp", sizeof(oss->dev_name)); + } + else + { + strncpy(oss->dev_name, device, sizeof(oss->dev_name) - 1); + } + + if ((oss->pcm_handle = open(oss->dev_name, O_WRONLY)) < 0) + { + OSS_LOG_ERR("sound dev open failed", errno); + oss->pcm_handle = -1; + return FALSE; + } + +#if 0 /* FreeBSD OSS implementation at this moment (2015.03) does not set PCM_CAP_OUTPUT flag. */ + tmp = 0; + + if (ioctl(oss->pcm_handle, SNDCTL_DSP_GETCAPS, &mask) == -1) + { + OSS_LOG_ERR("SNDCTL_DSP_GETCAPS failed, try ignory", errno); + } + else if ((mask & PCM_CAP_OUTPUT) == 0) + { + OSS_LOG_ERR("Device does not supports playback", EOPNOTSUPP); + close(oss->pcm_handle); + oss->pcm_handle = -1; + return FALSE; + } + +#endif + tmp = 0; + + if (ioctl(oss->pcm_handle, SNDCTL_DSP_GETFMTS, &tmp) == -1) + { + OSS_LOG_ERR("SNDCTL_DSP_GETFMTS failed", errno); + close(oss->pcm_handle); + oss->pcm_handle = -1; + return FALSE; + } + + if ((AFMT_S16_LE & tmp) == 0) + { + OSS_LOG_ERR("SNDCTL_DSP_GETFMTS - AFMT_S16_LE", EOPNOTSUPP); + close(oss->pcm_handle); + oss->pcm_handle = -1; + return FALSE; + } + + WLog_INFO(TAG, "open: %s", oss->dev_name); + return TRUE; +} + +static BOOL tsmf_oss_set_format(ITSMFAudioDevice* audio, UINT32 sample_rate, UINT32 channels, + UINT32 bits_per_sample) +{ + int tmp; + TSMFOssAudioDevice* oss = (TSMFOssAudioDevice*)audio; + + if (oss == NULL || oss->pcm_handle == -1) + return FALSE; + + oss->sample_rate = sample_rate; + oss->channels = channels; + oss->bits_per_sample = bits_per_sample; + tmp = AFMT_S16_LE; + + if (ioctl(oss->pcm_handle, SNDCTL_DSP_SETFMT, &tmp) == -1) + OSS_LOG_ERR("SNDCTL_DSP_SETFMT failed", errno); + + tmp = channels; + + if (ioctl(oss->pcm_handle, SNDCTL_DSP_CHANNELS, &tmp) == -1) + OSS_LOG_ERR("SNDCTL_DSP_CHANNELS failed", errno); + + tmp = sample_rate; + + if (ioctl(oss->pcm_handle, SNDCTL_DSP_SPEED, &tmp) == -1) + OSS_LOG_ERR("SNDCTL_DSP_SPEED failed", errno); + + tmp = ((bits_per_sample / 8) * channels * sample_rate); + + if (ioctl(oss->pcm_handle, SNDCTL_DSP_SETFRAGMENT, &tmp) == -1) + OSS_LOG_ERR("SNDCTL_DSP_SETFRAGMENT failed", errno); + + DEBUG_TSMF("sample_rate %" PRIu32 " channels %" PRIu32 " bits_per_sample %" PRIu32 "", + sample_rate, channels, bits_per_sample); + return TRUE; +} + +static BOOL tsmf_oss_play(ITSMFAudioDevice* audio, const BYTE* data, UINT32 data_size) +{ + int status; + UINT32 offset; + TSMFOssAudioDevice* oss = (TSMFOssAudioDevice*)audio; + DEBUG_TSMF("tsmf_oss_play: data_size %" PRIu32 "", data_size); + + if (oss == NULL || oss->pcm_handle == -1) + return FALSE; + + if (data == NULL || data_size == 0) + return TRUE; + + offset = 0; + oss->data_size_last = data_size; + + while (offset < data_size) + { + status = write(oss->pcm_handle, &data[offset], (data_size - offset)); + + if (status < 0) + { + OSS_LOG_ERR("write fail", errno); + return FALSE; + } + + offset += status; + } + + return TRUE; +} + +static UINT64 tsmf_oss_get_latency(ITSMFAudioDevice* audio) +{ + UINT64 latency = 0; + TSMFOssAudioDevice* oss = (TSMFOssAudioDevice*)audio; + + if (oss == NULL) + return 0; + + // latency = ((oss->data_size_last / (oss->bits_per_sample / 8)) * oss->sample_rate); + // WLog_INFO(TAG, "latency: %zu", latency); + return latency; +} + +static BOOL tsmf_oss_flush(ITSMFAudioDevice* audio) +{ + return TRUE; +} + +static void tsmf_oss_free(ITSMFAudioDevice* audio) +{ + TSMFOssAudioDevice* oss = (TSMFOssAudioDevice*)audio; + + if (oss == NULL) + return; + + if (oss->pcm_handle != -1) + { + WLog_INFO(TAG, "close: %s", oss->dev_name); + close(oss->pcm_handle); + } + + free(oss); +} + +#ifdef BUILTIN_CHANNELS +#define freerdp_tsmf_client_audio_subsystem_entry oss_freerdp_tsmf_client_audio_subsystem_entry +#else +#define freerdp_tsmf_client_audio_subsystem_entry \ + FREERDP_API freerdp_tsmf_client_audio_subsystem_entry +#endif + +ITSMFAudioDevice* freerdp_tsmf_client_audio_subsystem_entry(void) +{ + TSMFOssAudioDevice* oss; + oss = (TSMFOssAudioDevice*)malloc(sizeof(TSMFOssAudioDevice)); + ZeroMemory(oss, sizeof(TSMFOssAudioDevice)); + oss->iface.Open = tsmf_oss_open; + oss->iface.SetFormat = tsmf_oss_set_format; + oss->iface.Play = tsmf_oss_play; + oss->iface.GetLatency = tsmf_oss_get_latency; + oss->iface.Flush = tsmf_oss_flush; + oss->iface.Free = tsmf_oss_free; + oss->pcm_handle = -1; + return (ITSMFAudioDevice*)oss; +} diff --git a/channels/tsmf/client/pulse/CMakeLists.txt b/channels/tsmf/client/pulse/CMakeLists.txt new file mode 100644 index 0000000..9f78ca2 --- /dev/null +++ b/channels/tsmf/client/pulse/CMakeLists.txt @@ -0,0 +1,27 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2012 Marc-Andre Moreau +# +# Licensed 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. + +define_channel_client_subsystem("tsmf" "pulse" "audio") + +set(${MODULE_PREFIX}_SRCS + tsmf_pulse.c) + +include_directories(..) +include_directories(${PULSE_INCLUDE_DIR}) + +add_channel_client_subsystem_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} "" TRUE "") +target_link_libraries(${MODULE_NAME} winpr ${PULSE_LIBRARY}) diff --git a/channels/tsmf/client/pulse/tsmf_pulse.c b/channels/tsmf/client/pulse/tsmf_pulse.c new file mode 100644 index 0000000..b2f567e --- /dev/null +++ b/channels/tsmf/client/pulse/tsmf_pulse.c @@ -0,0 +1,422 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Video Redirection Virtual Channel - PulseAudio Device + * + * Copyright 2010-2011 Vic Lee + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include +#include + +#include + +#include + +#include "tsmf_audio.h" + +typedef struct _TSMFPulseAudioDevice +{ + ITSMFAudioDevice iface; + + char device[32]; + pa_threaded_mainloop* mainloop; + pa_context* context; + pa_sample_spec sample_spec; + pa_stream* stream; +} TSMFPulseAudioDevice; + +static void tsmf_pulse_context_state_callback(pa_context* context, void* userdata) +{ + TSMFPulseAudioDevice* pulse = (TSMFPulseAudioDevice*)userdata; + pa_context_state_t state; + state = pa_context_get_state(context); + + switch (state) + { + case PA_CONTEXT_READY: + DEBUG_TSMF("PA_CONTEXT_READY"); + pa_threaded_mainloop_signal(pulse->mainloop, 0); + break; + + case PA_CONTEXT_FAILED: + case PA_CONTEXT_TERMINATED: + DEBUG_TSMF("state %d", state); + pa_threaded_mainloop_signal(pulse->mainloop, 0); + break; + + default: + DEBUG_TSMF("state %d", state); + break; + } +} + +static BOOL tsmf_pulse_connect(TSMFPulseAudioDevice* pulse) +{ + pa_context_state_t state; + + if (!pulse->context) + return FALSE; + + if (pa_context_connect(pulse->context, NULL, 0, NULL)) + { + WLog_ERR(TAG, "pa_context_connect failed (%d)", pa_context_errno(pulse->context)); + return FALSE; + } + + pa_threaded_mainloop_lock(pulse->mainloop); + + if (pa_threaded_mainloop_start(pulse->mainloop) < 0) + { + pa_threaded_mainloop_unlock(pulse->mainloop); + WLog_ERR(TAG, "pa_threaded_mainloop_start failed (%d)", pa_context_errno(pulse->context)); + return FALSE; + } + + for (;;) + { + state = pa_context_get_state(pulse->context); + + if (state == PA_CONTEXT_READY) + break; + + if (!PA_CONTEXT_IS_GOOD(state)) + { + DEBUG_TSMF("bad context state (%d)", pa_context_errno(pulse->context)); + break; + } + + pa_threaded_mainloop_wait(pulse->mainloop); + } + + pa_threaded_mainloop_unlock(pulse->mainloop); + + if (state == PA_CONTEXT_READY) + { + DEBUG_TSMF("connected"); + return TRUE; + } + else + { + pa_context_disconnect(pulse->context); + return FALSE; + } +} + +static BOOL tsmf_pulse_open(ITSMFAudioDevice* audio, const char* device) +{ + TSMFPulseAudioDevice* pulse = (TSMFPulseAudioDevice*)audio; + + if (device) + { + strncpy(pulse->device, device, sizeof(pulse->device) - 1); + } + + pulse->mainloop = pa_threaded_mainloop_new(); + + if (!pulse->mainloop) + { + WLog_ERR(TAG, "pa_threaded_mainloop_new failed"); + return FALSE; + } + + pulse->context = pa_context_new(pa_threaded_mainloop_get_api(pulse->mainloop), "freerdp"); + + if (!pulse->context) + { + WLog_ERR(TAG, "pa_context_new failed"); + return FALSE; + } + + pa_context_set_state_callback(pulse->context, tsmf_pulse_context_state_callback, pulse); + + if (!tsmf_pulse_connect(pulse)) + { + WLog_ERR(TAG, "tsmf_pulse_connect failed"); + return FALSE; + } + + DEBUG_TSMF("open device %s", pulse->device); + return TRUE; +} + +static void tsmf_pulse_stream_success_callback(pa_stream* stream, int success, void* userdata) +{ + TSMFPulseAudioDevice* pulse = (TSMFPulseAudioDevice*)userdata; + pa_threaded_mainloop_signal(pulse->mainloop, 0); +} + +static void tsmf_pulse_wait_for_operation(TSMFPulseAudioDevice* pulse, pa_operation* operation) +{ + if (operation == NULL) + return; + + while (pa_operation_get_state(operation) == PA_OPERATION_RUNNING) + { + pa_threaded_mainloop_wait(pulse->mainloop); + } + + pa_operation_unref(operation); +} + +static void tsmf_pulse_stream_state_callback(pa_stream* stream, void* userdata) +{ + TSMFPulseAudioDevice* pulse = (TSMFPulseAudioDevice*)userdata; + pa_stream_state_t state; + state = pa_stream_get_state(stream); + + switch (state) + { + case PA_STREAM_READY: + DEBUG_TSMF("PA_STREAM_READY"); + pa_threaded_mainloop_signal(pulse->mainloop, 0); + break; + + case PA_STREAM_FAILED: + case PA_STREAM_TERMINATED: + DEBUG_TSMF("state %d", state); + pa_threaded_mainloop_signal(pulse->mainloop, 0); + break; + + default: + DEBUG_TSMF("state %d", state); + break; + } +} + +static void tsmf_pulse_stream_request_callback(pa_stream* stream, size_t length, void* userdata) +{ + TSMFPulseAudioDevice* pulse = (TSMFPulseAudioDevice*)userdata; + DEBUG_TSMF("%" PRIdz "", length); + pa_threaded_mainloop_signal(pulse->mainloop, 0); +} + +static BOOL tsmf_pulse_close_stream(TSMFPulseAudioDevice* pulse) +{ + if (!pulse->context || !pulse->stream) + return FALSE; + + DEBUG_TSMF(""); + pa_threaded_mainloop_lock(pulse->mainloop); + pa_stream_set_write_callback(pulse->stream, NULL, NULL); + tsmf_pulse_wait_for_operation( + pulse, pa_stream_drain(pulse->stream, tsmf_pulse_stream_success_callback, pulse)); + pa_stream_disconnect(pulse->stream); + pa_stream_unref(pulse->stream); + pulse->stream = NULL; + pa_threaded_mainloop_unlock(pulse->mainloop); + return TRUE; +} + +static BOOL tsmf_pulse_open_stream(TSMFPulseAudioDevice* pulse) +{ + pa_stream_state_t state; + pa_buffer_attr buffer_attr = { 0 }; + + if (!pulse->context) + return FALSE; + + DEBUG_TSMF(""); + pa_threaded_mainloop_lock(pulse->mainloop); + pulse->stream = pa_stream_new(pulse->context, "freerdp", &pulse->sample_spec, NULL); + + if (!pulse->stream) + { + pa_threaded_mainloop_unlock(pulse->mainloop); + WLog_ERR(TAG, "pa_stream_new failed (%d)", pa_context_errno(pulse->context)); + return FALSE; + } + + pa_stream_set_state_callback(pulse->stream, tsmf_pulse_stream_state_callback, pulse); + pa_stream_set_write_callback(pulse->stream, tsmf_pulse_stream_request_callback, pulse); + buffer_attr.maxlength = pa_usec_to_bytes(500000, &pulse->sample_spec); + buffer_attr.tlength = pa_usec_to_bytes(250000, &pulse->sample_spec); + buffer_attr.prebuf = (UINT32)-1; + buffer_attr.minreq = (UINT32)-1; + buffer_attr.fragsize = (UINT32)-1; + + if (pa_stream_connect_playback( + pulse->stream, pulse->device[0] ? pulse->device : NULL, &buffer_attr, + PA_STREAM_ADJUST_LATENCY | PA_STREAM_INTERPOLATE_TIMING | PA_STREAM_AUTO_TIMING_UPDATE, + NULL, NULL) < 0) + { + pa_threaded_mainloop_unlock(pulse->mainloop); + WLog_ERR(TAG, "pa_stream_connect_playback failed (%d)", pa_context_errno(pulse->context)); + return FALSE; + } + + for (;;) + { + state = pa_stream_get_state(pulse->stream); + + if (state == PA_STREAM_READY) + break; + + if (!PA_STREAM_IS_GOOD(state)) + { + WLog_ERR(TAG, "bad stream state (%d)", pa_context_errno(pulse->context)); + break; + } + + pa_threaded_mainloop_wait(pulse->mainloop); + } + + pa_threaded_mainloop_unlock(pulse->mainloop); + + if (state == PA_STREAM_READY) + { + DEBUG_TSMF("connected"); + return TRUE; + } + else + { + tsmf_pulse_close_stream(pulse); + return FALSE; + } +} + +static BOOL tsmf_pulse_set_format(ITSMFAudioDevice* audio, UINT32 sample_rate, UINT32 channels, + UINT32 bits_per_sample) +{ + TSMFPulseAudioDevice* pulse = (TSMFPulseAudioDevice*)audio; + DEBUG_TSMF("sample_rate %" PRIu32 " channels %" PRIu32 " bits_per_sample %" PRIu32 "", + sample_rate, channels, bits_per_sample); + pulse->sample_spec.rate = sample_rate; + pulse->sample_spec.channels = channels; + pulse->sample_spec.format = PA_SAMPLE_S16LE; + return tsmf_pulse_open_stream(pulse); +} + +static BOOL tsmf_pulse_play(ITSMFAudioDevice* audio, const BYTE* data, UINT32 data_size) +{ + TSMFPulseAudioDevice* pulse = (TSMFPulseAudioDevice*)audio; + const BYTE* src; + size_t len; + int ret; + DEBUG_TSMF("data_size %" PRIu32 "", data_size); + + if (pulse->stream) + { + pa_threaded_mainloop_lock(pulse->mainloop); + src = data; + + while (data_size > 0) + { + while ((len = pa_stream_writable_size(pulse->stream)) == 0) + { + DEBUG_TSMF("waiting"); + pa_threaded_mainloop_wait(pulse->mainloop); + } + + if (len == (size_t)-1) + break; + + if (len > data_size) + len = data_size; + + ret = pa_stream_write(pulse->stream, src, len, NULL, 0LL, PA_SEEK_RELATIVE); + + if (ret < 0) + { + DEBUG_TSMF("pa_stream_write failed (%d)", pa_context_errno(pulse->context)); + break; + } + + src += len; + data_size -= len; + } + + pa_threaded_mainloop_unlock(pulse->mainloop); + } + + return TRUE; +} + +static UINT64 tsmf_pulse_get_latency(ITSMFAudioDevice* audio) +{ + pa_usec_t usec; + UINT64 latency = 0; + TSMFPulseAudioDevice* pulse = (TSMFPulseAudioDevice*)audio; + + if (pulse->stream && pa_stream_get_latency(pulse->stream, &usec, NULL) == 0) + { + latency = ((UINT64)usec) * 10LL; + } + + return latency; +} + +static BOOL tsmf_pulse_flush(ITSMFAudioDevice* audio) +{ + TSMFPulseAudioDevice* pulse = (TSMFPulseAudioDevice*)audio; + pa_threaded_mainloop_lock(pulse->mainloop); + tsmf_pulse_wait_for_operation( + pulse, pa_stream_flush(pulse->stream, tsmf_pulse_stream_success_callback, pulse)); + pa_threaded_mainloop_unlock(pulse->mainloop); + return TRUE; +} + +static void tsmf_pulse_free(ITSMFAudioDevice* audio) +{ + TSMFPulseAudioDevice* pulse = (TSMFPulseAudioDevice*)audio; + DEBUG_TSMF(""); + tsmf_pulse_close_stream(pulse); + + if (pulse->mainloop) + { + pa_threaded_mainloop_stop(pulse->mainloop); + } + + if (pulse->context) + { + pa_context_disconnect(pulse->context); + pa_context_unref(pulse->context); + pulse->context = NULL; + } + + if (pulse->mainloop) + { + pa_threaded_mainloop_free(pulse->mainloop); + pulse->mainloop = NULL; + } + + free(pulse); +} + +#ifdef BUILTIN_CHANNELS +ITSMFAudioDevice* pulse_freerdp_tsmf_client_audio_subsystem_entry(void) +#else +FREERDP_API ITSMFAudioDevice* freerdp_tsmf_client_audio_subsystem_entry(void) +#endif +{ + TSMFPulseAudioDevice* pulse; + pulse = (TSMFPulseAudioDevice*)calloc(1, sizeof(TSMFPulseAudioDevice)); + + if (!pulse) + return NULL; + + pulse->iface.Open = tsmf_pulse_open; + pulse->iface.SetFormat = tsmf_pulse_set_format; + pulse->iface.Play = tsmf_pulse_play; + pulse->iface.GetLatency = tsmf_pulse_get_latency; + pulse->iface.Flush = tsmf_pulse_flush; + pulse->iface.Free = tsmf_pulse_free; + return (ITSMFAudioDevice*)pulse; +} diff --git a/channels/tsmf/client/tsmf_audio.c b/channels/tsmf/client/tsmf_audio.c new file mode 100644 index 0000000..9f0bb32 --- /dev/null +++ b/channels/tsmf/client/tsmf_audio.c @@ -0,0 +1,99 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Video Redirection Virtual Channel - Audio Device Manager + * + * Copyright 2010-2011 Vic Lee + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include + +#include "tsmf_audio.h" + +static ITSMFAudioDevice* tsmf_load_audio_device_by_name(const char* name, const char* device) +{ + ITSMFAudioDevice* audio; + TSMF_AUDIO_DEVICE_ENTRY entry; + + entry = + (TSMF_AUDIO_DEVICE_ENTRY)(void*)freerdp_load_channel_addin_entry("tsmf", name, "audio", 0); + + if (!entry) + return NULL; + + audio = entry(); + + if (!audio) + { + WLog_ERR(TAG, "failed to call export function in %s", name); + return NULL; + } + + if (!audio->Open(audio, device)) + { + audio->Free(audio); + audio = NULL; + WLog_ERR(TAG, "failed to open, name: %s, device: %s", name, device); + } + else + { + WLog_DBG(TAG, "name: %s, device: %s", name, device); + } + + return audio; +} + +ITSMFAudioDevice* tsmf_load_audio_device(const char* name, const char* device) +{ + ITSMFAudioDevice* audio = NULL; + + if (name) + { + audio = tsmf_load_audio_device_by_name(name, device); + } + else + { +#if defined(WITH_PULSE) + if (!audio) + audio = tsmf_load_audio_device_by_name("pulse", device); +#endif + +#if defined(WITH_OSS) + if (!audio) + audio = tsmf_load_audio_device_by_name("oss", device); +#endif + +#if defined(WITH_ALSA) + if (!audio) + audio = tsmf_load_audio_device_by_name("alsa", device); +#endif + } + + if (audio == NULL) + { + WLog_ERR(TAG, "no sound device."); + } + else + { + WLog_DBG(TAG, "name: %s, device: %s", name, device); + } + + return audio; +} diff --git a/channels/tsmf/client/tsmf_audio.h b/channels/tsmf/client/tsmf_audio.h new file mode 100644 index 0000000..e7ae68c --- /dev/null +++ b/channels/tsmf/client/tsmf_audio.h @@ -0,0 +1,51 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Video Redirection Virtual Channel - Audio Device Manager + * + * Copyright 2010-2011 Vic Lee + * + * Licensed 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 FREERDP_CHANNEL_TSMF_CLIENT_AUDIO_H +#define FREERDP_CHANNEL_TSMF_CLIENT_AUDIO_H + +#include "tsmf_types.h" + +typedef struct _ITSMFAudioDevice ITSMFAudioDevice; + +struct _ITSMFAudioDevice +{ + /* Open the audio device. */ + BOOL (*Open)(ITSMFAudioDevice* audio, const char* device); + /* Set the audio data format. */ + BOOL(*SetFormat) + (ITSMFAudioDevice* audio, UINT32 sample_rate, UINT32 channels, UINT32 bits_per_sample); + /* Play audio data. */ + BOOL (*Play)(ITSMFAudioDevice* audio, const BYTE* data, UINT32 data_size); + /* Get the latency of the last written sample, in 100ns */ + UINT64 (*GetLatency)(ITSMFAudioDevice* audio); + /* Change the playback volume level */ + BOOL (*ChangeVolume)(ITSMFAudioDevice* audio, UINT32 newVolume, UINT32 muted); + /* Flush queued audio data */ + BOOL (*Flush)(ITSMFAudioDevice* audio); + /* Free the audio device */ + void (*Free)(ITSMFAudioDevice* audio); +}; + +#define TSMF_AUDIO_DEVICE_EXPORT_FUNC_NAME "TSMFAudioDeviceEntry" +typedef ITSMFAudioDevice* (*TSMF_AUDIO_DEVICE_ENTRY)(void); + +ITSMFAudioDevice* tsmf_load_audio_device(const char* name, const char* device); + +#endif /* FREERDP_CHANNEL_TSMF_CLIENT_AUDIO_H */ diff --git a/channels/tsmf/client/tsmf_codec.c b/channels/tsmf/client/tsmf_codec.c new file mode 100644 index 0000000..a212aff --- /dev/null +++ b/channels/tsmf/client/tsmf_codec.c @@ -0,0 +1,612 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Video Redirection Virtual Channel - Codec + * + * Copyright 2010-2011 Vic Lee + * Copyright 2012 Hewlett-Packard Development Company, L.P. + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include + +#include "tsmf_decoder.h" +#include "tsmf_constants.h" +#include "tsmf_types.h" + +#include "tsmf_codec.h" + +#include + +#define TAG CHANNELS_TAG("tsmf.client") + +typedef struct _TSMFMediaTypeMap +{ + BYTE guid[16]; + const char* name; + int type; +} TSMFMediaTypeMap; + +static const TSMFMediaTypeMap tsmf_major_type_map[] = { + /* 73646976-0000-0010-8000-00AA00389B71 */ + { { 0x76, 0x69, 0x64, 0x73, 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0xAA, 0x00, 0x38, 0x9B, + 0x71 }, + "MEDIATYPE_Video", + TSMF_MAJOR_TYPE_VIDEO }, + + /* 73647561-0000-0010-8000-00AA00389B71 */ + { { 0x61, 0x75, 0x64, 0x73, 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0xAA, 0x00, 0x38, 0x9B, + 0x71 }, + "MEDIATYPE_Audio", + TSMF_MAJOR_TYPE_AUDIO }, + + { { 0 }, "Unknown", TSMF_MAJOR_TYPE_UNKNOWN } +}; + +static const TSMFMediaTypeMap tsmf_sub_type_map[] = { + /* 31435657-0000-0010-8000-00AA00389B71 */ + { { 0x57, 0x56, 0x43, 0x31, 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0xAA, 0x00, 0x38, 0x9B, + 0x71 }, + "MEDIASUBTYPE_WVC1", + TSMF_SUB_TYPE_WVC1 }, + + /* 00000160-0000-0010-8000-00AA00389B71 */ + { { 0x60, 0x01, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0xAA, 0x00, 0x38, 0x9B, + 0x71 }, + "MEDIASUBTYPE_WMAudioV1", /* V7, V8 has the same GUID */ + TSMF_SUB_TYPE_WMA1 }, + + /* 00000161-0000-0010-8000-00AA00389B71 */ + { { 0x61, 0x01, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0xAA, 0x00, 0x38, 0x9B, + 0x71 }, + "MEDIASUBTYPE_WMAudioV2", /* V7, V8 has the same GUID */ + TSMF_SUB_TYPE_WMA2 }, + + /* 00000162-0000-0010-8000-00AA00389B71 */ + { { 0x62, 0x01, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0xAA, 0x00, 0x38, 0x9B, + 0x71 }, + "MEDIASUBTYPE_WMAudioV9", + TSMF_SUB_TYPE_WMA9 }, + + /* 00000055-0000-0010-8000-00AA00389B71 */ + { { 0x55, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0xAA, 0x00, 0x38, 0x9B, + 0x71 }, + "MEDIASUBTYPE_MP3", + TSMF_SUB_TYPE_MP3 }, + + /* E06D802B-DB46-11CF-B4D1-00805F6CBBEA */ + { { 0x2B, 0x80, 0x6D, 0xE0, 0x46, 0xDB, 0xCF, 0x11, 0xB4, 0xD1, 0x00, 0x80, 0x5F, 0x6C, 0xBB, + 0xEA }, + "MEDIASUBTYPE_MPEG2_AUDIO", + TSMF_SUB_TYPE_MP2A }, + + /* E06D8026-DB46-11CF-B4D1-00805F6CBBEA */ + { { 0x26, 0x80, 0x6D, 0xE0, 0x46, 0xDB, 0xCF, 0x11, 0xB4, 0xD1, 0x00, 0x80, 0x5F, 0x6C, 0xBB, + 0xEA }, + "MEDIASUBTYPE_MPEG2_VIDEO", + TSMF_SUB_TYPE_MP2V }, + + /* 31564D57-0000-0010-8000-00AA00389B71 */ + { { 0x57, 0x4D, 0x56, 0x31, 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0xAA, 0x00, 0x38, 0x9B, + 0x71 }, + "MEDIASUBTYPE_WMV1", + TSMF_SUB_TYPE_WMV1 }, + + /* 32564D57-0000-0010-8000-00AA00389B71 */ + { { 0x57, 0x4D, 0x56, 0x32, 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0xAA, 0x00, 0x38, 0x9B, + 0x71 }, + "MEDIASUBTYPE_WMV2", + TSMF_SUB_TYPE_WMV2 }, + + /* 33564D57-0000-0010-8000-00AA00389B71 */ + { { 0x57, 0x4D, 0x56, 0x33, 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0xAA, 0x00, 0x38, 0x9B, + 0x71 }, + "MEDIASUBTYPE_WMV3", + TSMF_SUB_TYPE_WMV3 }, + + /* 00001610-0000-0010-8000-00AA00389B71 */ + { { 0x10, 0x16, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0xAA, 0x00, 0x38, 0x9B, + 0x71 }, + "MEDIASUBTYPE_MPEG_HEAAC", + TSMF_SUB_TYPE_AAC }, + + /* 34363248-0000-0010-8000-00AA00389B71 */ + { { 0x48, 0x32, 0x36, 0x34, 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0xAA, 0x00, 0x38, 0x9B, + 0x71 }, + "MEDIASUBTYPE_H264", + TSMF_SUB_TYPE_H264 }, + + /* 31435641-0000-0010-8000-00AA00389B71 */ + { { 0x41, 0x56, 0x43, 0x31, 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0xAA, 0x00, 0x38, 0x9B, + 0x71 }, + "MEDIASUBTYPE_AVC1", + TSMF_SUB_TYPE_AVC1 }, + + /* 3334504D-0000-0010-8000-00AA00389B71 */ + { { 0x4D, 0x50, 0x34, 0x33, 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0xAA, 0x00, 0x38, 0x9B, + 0x71 }, + "MEDIASUBTYPE_MP43", + TSMF_SUB_TYPE_MP43 }, + + /* 5634504D-0000-0010-8000-00AA00389B71 */ + { { 0x4D, 0x50, 0x34, 0x56, 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0xAA, 0x00, 0x38, 0x9B, + 0x71 }, + "MEDIASUBTYPE_MP4S", + TSMF_SUB_TYPE_MP4S }, + + /* 3234504D-0000-0010-8000-00AA00389B71 */ + { { 0x4D, 0x50, 0x34, 0x32, 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0xAA, 0x00, 0x38, 0x9B, + 0x71 }, + "MEDIASUBTYPE_MP42", + TSMF_SUB_TYPE_MP42 }, + + /* 3253344D-0000-0010-8000-00AA00389B71 */ + { { 0x4D, 0x34, 0x53, 0x32, 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0xAA, 0x00, 0x38, 0x9B, + 0x71 }, + "MEDIASUBTYPE_MP42", + TSMF_SUB_TYPE_M4S2 }, + + /* E436EB81-524F-11CE-9F53-0020AF0BA770 */ + { { 0x81, 0xEB, 0x36, 0xE4, 0x4F, 0x52, 0xCE, 0x11, 0x9F, 0x53, 0x00, 0x20, 0xAF, 0x0B, 0xA7, + 0x70 }, + "MEDIASUBTYPE_MP1V", + TSMF_SUB_TYPE_MP1V }, + + /* 00000050-0000-0010-8000-00AA00389B71 */ + { { 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0xAA, 0x00, 0x38, 0x9B, + 0x71 }, + "MEDIASUBTYPE_MP1A", + TSMF_SUB_TYPE_MP1A }, + + /* E06D802C-DB46-11CF-B4D1-00805F6CBBEA */ + { { 0x2C, 0x80, 0x6D, 0xE0, 0x46, 0xDB, 0xCF, 0x11, 0xB4, 0xD1, 0x00, 0x80, 0x5F, 0x6C, 0xBB, + 0xEA }, + "MEDIASUBTYPE_DOLBY_AC3", + TSMF_SUB_TYPE_AC3 }, + + /* 32595559-0000-0010-8000-00AA00389B71 */ + { { 0x59, 0x55, 0x59, 0x32, 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0xAA, 0x00, 0x38, 0x9B, + 0x71 }, + "MEDIASUBTYPE_YUY2", + TSMF_SUB_TYPE_YUY2 }, + + /* Opencodec IDS */ + { { 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0xAA, 0x00, 0x38, 0x9B, + 0x71 }, + "MEDIASUBTYPE_FLAC", + TSMF_SUB_TYPE_FLAC }, + + { { 0x61, 0x34, 0x70, 0x6D, 0x7A, 0x76, 0x4D, 0x49, 0xB4, 0x78, 0xF2, 0x9D, 0x25, 0xDC, 0x90, + 0x37 }, + "MEDIASUBTYPE_OGG", + TSMF_SUB_TYPE_OGG }, + + { { 0x4D, 0x34, 0x53, 0x32, 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0xAA, 0x00, 0x38, 0x9B, + 0x71 }, + "MEDIASUBTYPE_H263", + TSMF_SUB_TYPE_H263 }, + + /* WebMMF codec IDS */ + { { 0x56, 0x50, 0x38, 0x30, 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0xAA, 0x00, 0x38, 0x9B, + 0x71 }, + "MEDIASUBTYPE_VP8", + TSMF_SUB_TYPE_VP8 }, + + { { 0x0B, 0xD1, 0x2F, 0x8D, 0x41, 0x58, 0x6B, 0x4A, 0x89, 0x05, 0x58, 0x8F, 0xEC, 0x1A, 0xDE, + 0xD9 }, + "MEDIASUBTYPE_OGG", + TSMF_SUB_TYPE_OGG }, + + { { 0 }, "Unknown", TSMF_SUB_TYPE_UNKNOWN } + +}; + +static const TSMFMediaTypeMap tsmf_format_type_map[] = { + /* AED4AB2D-7326-43CB-9464-C879CAB9C43D */ + { { 0x2D, 0xAB, 0xD4, 0xAE, 0x26, 0x73, 0xCB, 0x43, 0x94, 0x64, 0xC8, 0x79, 0xCA, 0xB9, 0xC4, + 0x3D }, + "FORMAT_MFVideoFormat", + TSMF_FORMAT_TYPE_MFVIDEOFORMAT }, + + /* 05589F81-C356-11CE-BF01-00AA0055595A */ + { { 0x81, 0x9F, 0x58, 0x05, 0x56, 0xC3, 0xCE, 0x11, 0xBF, 0x01, 0x00, 0xAA, 0x00, 0x55, 0x59, + 0x5A }, + "FORMAT_WaveFormatEx", + TSMF_FORMAT_TYPE_WAVEFORMATEX }, + + /* E06D80E3-DB46-11CF-B4D1-00805F6CBBEA */ + { { 0xE3, 0x80, 0x6D, 0xE0, 0x46, 0xDB, 0xCF, 0x11, 0xB4, 0xD1, 0x00, 0x80, 0x5F, 0x6C, 0xBB, + 0xEA }, + "FORMAT_MPEG2_VIDEO", + TSMF_FORMAT_TYPE_MPEG2VIDEOINFO }, + + /* F72A76A0-EB0A-11D0-ACE4-0000C0CC16BA */ + { { 0xA0, 0x76, 0x2A, 0xF7, 0x0A, 0xEB, 0xD0, 0x11, 0xAC, 0xE4, 0x00, 0x00, 0xC0, 0xCC, 0x16, + 0xBA }, + "FORMAT_VideoInfo2", + TSMF_FORMAT_TYPE_VIDEOINFO2 }, + + /* 05589F82-C356-11CE-BF01-00AA0055595A */ + { { 0x82, 0x9F, 0x58, 0x05, 0x56, 0xC3, 0xCE, 0x11, 0xBF, 0x01, 0x00, 0xAA, 0x00, 0x55, 0x59, + 0x5A }, + "FORMAT_MPEG1_VIDEO", + TSMF_FORMAT_TYPE_MPEG1VIDEOINFO }, + + { { 0 }, "Unknown", TSMF_FORMAT_TYPE_UNKNOWN } +}; + +static void tsmf_print_guid(const BYTE* guid) +{ +#ifdef WITH_DEBUG_TSMF + char guidString[37]; + + snprintf(guidString, sizeof(guidString), + "%02" PRIX8 "%02" PRIX8 "%02" PRIX8 "%02" PRIX8 "-%02" PRIX8 "%02" PRIX8 "-%02" PRIX8 + "%02" PRIX8 "-%02" PRIX8 "%02" PRIX8 "-%02" PRIX8 "%02" PRIX8 "%02" PRIX8 "%02" PRIX8 + "%02" PRIX8 "%02" PRIX8 "", + guid[3], guid[2], guid[1], guid[0], guid[5], guid[4], guid[7], guid[6], guid[8], + guid[9], guid[10], guid[11], guid[12], guid[13], guid[14], guid[15]); + + WLog_INFO(TAG, "%s", guidString); +#endif +} + +/* http://msdn.microsoft.com/en-us/library/dd318229.aspx */ +static UINT32 tsmf_codec_parse_BITMAPINFOHEADER(TS_AM_MEDIA_TYPE* mediatype, wStream* s, + BOOL bypass) +{ + UINT32 biSize; + UINT32 biWidth; + UINT32 biHeight; + + if (Stream_GetRemainingLength(s) < 40) + return 0; + Stream_Read_UINT32(s, biSize); + Stream_Read_UINT32(s, biWidth); + Stream_Read_UINT32(s, biHeight); + Stream_Seek(s, 28); + + if (mediatype->Width == 0) + mediatype->Width = biWidth; + + if (mediatype->Height == 0) + mediatype->Height = biHeight; + + /* Assume there will be no color table for video? */ + if ((biSize < 40) || (Stream_GetRemainingLength(s) < (biSize - 40))) + return 0; + + if (bypass && biSize > 40) + Stream_Seek(s, biSize - 40); + + return (bypass ? biSize : 40); +} + +/* http://msdn.microsoft.com/en-us/library/dd407326.aspx */ +static UINT32 tsmf_codec_parse_VIDEOINFOHEADER2(TS_AM_MEDIA_TYPE* mediatype, wStream* s) +{ + UINT64 AvgTimePerFrame; + + /* VIDEOINFOHEADER2.rcSource, RECT(LONG left, LONG top, LONG right, LONG bottom) */ + if (Stream_GetRemainingLength(s) < 72) + return 0; + + Stream_Seek_UINT32(s); + Stream_Seek_UINT32(s); + Stream_Read_UINT32(s, mediatype->Width); + Stream_Read_UINT32(s, mediatype->Height); + /* VIDEOINFOHEADER2.rcTarget */ + Stream_Seek(s, 16); + /* VIDEOINFOHEADER2.dwBitRate */ + Stream_Read_UINT32(s, mediatype->BitRate); + /* VIDEOINFOHEADER2.dwBitErrorRate */ + Stream_Seek_UINT32(s); + /* VIDEOINFOHEADER2.AvgTimePerFrame */ + Stream_Read_UINT64(s, AvgTimePerFrame); + mediatype->SamplesPerSecond.Numerator = 1000000; + mediatype->SamplesPerSecond.Denominator = (int)(AvgTimePerFrame / 10LL); + /* Remaining fields before bmiHeader */ + Stream_Seek(s, 24); + return 72; +} + +/* http://msdn.microsoft.com/en-us/library/dd390700.aspx */ +static UINT32 tsmf_codec_parse_VIDEOINFOHEADER(TS_AM_MEDIA_TYPE* mediatype, wStream* s) +{ + /* + typedef struct tagVIDEOINFOHEADER { + RECT rcSource; //16 + RECT rcTarget; //16 32 + DWORD dwBitRate; //4 36 + DWORD dwBitErrorRate; //4 40 + REFERENCE_TIME AvgTimePerFrame; //8 48 + BITMAPINFOHEADER bmiHeader; + } VIDEOINFOHEADER; + */ + UINT64 AvgTimePerFrame; + + if (Stream_GetRemainingLength(s) < 48) + return 0; + + /* VIDEOINFOHEADER.rcSource, RECT(LONG left, LONG top, LONG right, LONG bottom) */ + Stream_Seek_UINT32(s); + Stream_Seek_UINT32(s); + Stream_Read_UINT32(s, mediatype->Width); + Stream_Read_UINT32(s, mediatype->Height); + /* VIDEOINFOHEADER.rcTarget */ + Stream_Seek(s, 16); + /* VIDEOINFOHEADER.dwBitRate */ + Stream_Read_UINT32(s, mediatype->BitRate); + /* VIDEOINFOHEADER.dwBitErrorRate */ + Stream_Seek_UINT32(s); + /* VIDEOINFOHEADER.AvgTimePerFrame */ + Stream_Read_UINT64(s, AvgTimePerFrame); + mediatype->SamplesPerSecond.Numerator = 1000000; + mediatype->SamplesPerSecond.Denominator = (int)(AvgTimePerFrame / 10LL); + return 48; +} + +static BOOL tsmf_read_format_type(TS_AM_MEDIA_TYPE* mediatype, wStream* s, UINT32 cbFormat) +{ + UINT32 i, j; + + switch (mediatype->FormatType) + { + case TSMF_FORMAT_TYPE_MFVIDEOFORMAT: + /* http://msdn.microsoft.com/en-us/library/aa473808.aspx */ + if (Stream_GetRemainingLength(s) < 176) + return FALSE; + + Stream_Seek(s, 8); /* dwSize and ? */ + Stream_Read_UINT32(s, mediatype->Width); /* videoInfo.dwWidth */ + Stream_Read_UINT32(s, mediatype->Height); /* videoInfo.dwHeight */ + Stream_Seek(s, 32); + /* videoInfo.FramesPerSecond */ + Stream_Read_UINT32(s, mediatype->SamplesPerSecond.Numerator); + Stream_Read_UINT32(s, mediatype->SamplesPerSecond.Denominator); + Stream_Seek(s, 80); + Stream_Read_UINT32(s, mediatype->BitRate); /* compressedInfo.AvgBitrate */ + Stream_Seek(s, 36); + + if (cbFormat > 176) + { + const size_t nsize = cbFormat - 176; + if (mediatype->ExtraDataSize < nsize) + return FALSE; + if (!Stream_CheckAndLogRequiredLength(TAG, s, nsize)) + return FALSE; + mediatype->ExtraDataSize = nsize; + mediatype->ExtraData = Stream_Pointer(s); + } + break; + + case TSMF_FORMAT_TYPE_WAVEFORMATEX: + /* http://msdn.microsoft.com/en-us/library/dd757720.aspx */ + if (Stream_GetRemainingLength(s) < 18) + return FALSE; + + Stream_Seek_UINT16(s); + Stream_Read_UINT16(s, mediatype->Channels); + Stream_Read_UINT32(s, mediatype->SamplesPerSecond.Numerator); + mediatype->SamplesPerSecond.Denominator = 1; + Stream_Read_UINT32(s, mediatype->BitRate); + mediatype->BitRate *= 8; + Stream_Read_UINT16(s, mediatype->BlockAlign); + Stream_Read_UINT16(s, mediatype->BitsPerSample); + Stream_Read_UINT16(s, mediatype->ExtraDataSize); + + if (mediatype->ExtraDataSize > 0) + { + if (Stream_GetRemainingLength(s) < mediatype->ExtraDataSize) + return FALSE; + mediatype->ExtraData = Stream_Pointer(s); + } + break; + + case TSMF_FORMAT_TYPE_MPEG1VIDEOINFO: + /* http://msdn.microsoft.com/en-us/library/dd390700.aspx */ + i = tsmf_codec_parse_VIDEOINFOHEADER(mediatype, s); + if (!i) + return FALSE; + j = tsmf_codec_parse_BITMAPINFOHEADER(mediatype, s, TRUE); + if (!j) + return FALSE; + i += j; + + if (cbFormat > i) + { + mediatype->ExtraDataSize = cbFormat - i; + if (Stream_GetRemainingLength(s) < mediatype->ExtraDataSize) + return FALSE; + mediatype->ExtraData = Stream_Pointer(s); + } + break; + + case TSMF_FORMAT_TYPE_MPEG2VIDEOINFO: + /* http://msdn.microsoft.com/en-us/library/dd390707.aspx */ + i = tsmf_codec_parse_VIDEOINFOHEADER2(mediatype, s); + if (!i) + return FALSE; + j = tsmf_codec_parse_BITMAPINFOHEADER(mediatype, s, TRUE); + if (!j) + return FALSE; + i += j; + + if (cbFormat > i) + { + mediatype->ExtraDataSize = cbFormat - i; + if (Stream_GetRemainingLength(s) < mediatype->ExtraDataSize) + return FALSE; + mediatype->ExtraData = Stream_Pointer(s); + } + break; + + case TSMF_FORMAT_TYPE_VIDEOINFO2: + i = tsmf_codec_parse_VIDEOINFOHEADER2(mediatype, s); + if (!i) + return FALSE; + j = tsmf_codec_parse_BITMAPINFOHEADER(mediatype, s, FALSE); + if (!j) + return FALSE; + i += j; + + if (cbFormat > i) + { + mediatype->ExtraDataSize = cbFormat - i; + if (Stream_GetRemainingLength(s) < mediatype->ExtraDataSize) + return FALSE; + mediatype->ExtraData = Stream_Pointer(s); + } + break; + + default: + WLog_INFO(TAG, "unhandled format type 0x%x", mediatype->FormatType); + break; + } + return TRUE; +} + +BOOL tsmf_codec_parse_media_type(TS_AM_MEDIA_TYPE* mediatype, wStream* s) +{ + UINT32 cbFormat; + BOOL ret = TRUE; + int i; + + ZeroMemory(mediatype, sizeof(TS_AM_MEDIA_TYPE)); + + /* MajorType */ + DEBUG_TSMF("MediaMajorType:"); + if (Stream_GetRemainingLength(s) < 16) + return FALSE; + tsmf_print_guid(Stream_Pointer(s)); + + for (i = 0; tsmf_major_type_map[i].type != TSMF_MAJOR_TYPE_UNKNOWN; i++) + { + if (memcmp(tsmf_major_type_map[i].guid, Stream_Pointer(s), 16) == 0) + break; + } + + mediatype->MajorType = tsmf_major_type_map[i].type; + if (mediatype->MajorType == TSMF_MAJOR_TYPE_UNKNOWN) + ret = FALSE; + + DEBUG_TSMF("MediaMajorType %s", tsmf_major_type_map[i].name); + Stream_Seek(s, 16); + + /* SubType */ + DEBUG_TSMF("MediaSubType:"); + if (Stream_GetRemainingLength(s) < 16) + return FALSE; + tsmf_print_guid(Stream_Pointer(s)); + + for (i = 0; tsmf_sub_type_map[i].type != TSMF_SUB_TYPE_UNKNOWN; i++) + { + if (memcmp(tsmf_sub_type_map[i].guid, Stream_Pointer(s), 16) == 0) + break; + } + + mediatype->SubType = tsmf_sub_type_map[i].type; + if (mediatype->SubType == TSMF_SUB_TYPE_UNKNOWN) + ret = FALSE; + + DEBUG_TSMF("MediaSubType %s", tsmf_sub_type_map[i].name); + Stream_Seek(s, 16); + + /* bFixedSizeSamples, bTemporalCompression, SampleSize */ + if (Stream_GetRemainingLength(s) < 12) + return FALSE; + Stream_Seek(s, 12); + + /* FormatType */ + DEBUG_TSMF("FormatType:"); + if (Stream_GetRemainingLength(s) < 16) + return FALSE; + tsmf_print_guid(Stream_Pointer(s)); + + for (i = 0; tsmf_format_type_map[i].type != TSMF_FORMAT_TYPE_UNKNOWN; i++) + { + if (memcmp(tsmf_format_type_map[i].guid, Stream_Pointer(s), 16) == 0) + break; + } + + mediatype->FormatType = tsmf_format_type_map[i].type; + if (mediatype->FormatType == TSMF_FORMAT_TYPE_UNKNOWN) + ret = FALSE; + + DEBUG_TSMF("FormatType %s", tsmf_format_type_map[i].name); + Stream_Seek(s, 16); + + /* cbFormat */ + if (Stream_GetRemainingLength(s) < 4) + return FALSE; + Stream_Read_UINT32(s, cbFormat); + DEBUG_TSMF("cbFormat %" PRIu32 "", cbFormat); +#ifdef WITH_DEBUG_TSMF + winpr_HexDump(TAG, WLOG_DEBUG, Stream_Pointer(s), cbFormat); +#endif + + ret = tsmf_read_format_type(mediatype, s, cbFormat); + + if (mediatype->SamplesPerSecond.Numerator == 0) + mediatype->SamplesPerSecond.Numerator = 1; + + if (mediatype->SamplesPerSecond.Denominator == 0) + mediatype->SamplesPerSecond.Denominator = 1; + + return ret; +} + +BOOL tsmf_codec_check_media_type(const char* decoder_name, wStream* s) +{ + BYTE* m; + BOOL ret = FALSE; + TS_AM_MEDIA_TYPE mediatype; + + static BOOL decoderAvailable = FALSE; + static BOOL firstRun = TRUE; + + if (firstRun) + { + firstRun = FALSE; + if (tsmf_check_decoder_available(decoder_name)) + decoderAvailable = TRUE; + } + + Stream_GetPointer(s, m); + if (decoderAvailable) + ret = tsmf_codec_parse_media_type(&mediatype, s); + Stream_SetPointer(s, m); + + if (ret) + { + ITSMFDecoder* decoder = tsmf_load_decoder(decoder_name, &mediatype); + + if (!decoder) + { + WLog_WARN(TAG, "Format not supported by decoder %s", decoder_name); + ret = FALSE; + } + else + { + decoder->Free(decoder); + } + } + + return ret; +} diff --git a/channels/tsmf/client/tsmf_codec.h b/channels/tsmf/client/tsmf_codec.h new file mode 100644 index 0000000..ab98899 --- /dev/null +++ b/channels/tsmf/client/tsmf_codec.h @@ -0,0 +1,28 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Video Redirection Virtual Channel - Codec + * + * Copyright 2010-2011 Vic Lee + * + * Licensed 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 FREERDP_CHANNEL_TSMF_CLIENT_CODEC_H +#define FREERDP_CHANNEL_TSMF_CLIENT_CODEC_H + +#include "tsmf_types.h" + +BOOL tsmf_codec_parse_media_type(TS_AM_MEDIA_TYPE* mediatype, wStream* s); +BOOL tsmf_codec_check_media_type(const char* decoder_name, wStream* s); + +#endif /* FREERDP_CHANNEL_TSMF_CLIENT_CODEC_H */ diff --git a/channels/tsmf/client/tsmf_constants.h b/channels/tsmf/client/tsmf_constants.h new file mode 100644 index 0000000..43d37f2 --- /dev/null +++ b/channels/tsmf/client/tsmf_constants.h @@ -0,0 +1,139 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Video Redirection Virtual Channel - Constants + * + * Copyright 2010-2011 Vic Lee + * Copyright 2012 Hewlett-Packard Development Company, L.P. + * + * Licensed 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 FREERDP_CHANNEL_TSMF_CLIENT_CONSTANTS_H +#define FREERDP_CHANNEL_TSMF_CLIENT_CONSTANTS_H + +#define GUID_SIZE 16 +#define TSMF_BUFFER_PADDING_SIZE 8 + +/* Interface IDs defined in [MS-RDPEV]. There's no constant names in the MS + documentation, so we create them on our own. */ +#define TSMF_INTERFACE_DEFAULT 0x00000000 +#define TSMF_INTERFACE_CLIENT_NOTIFICATIONS 0x00000001 +#define TSMF_INTERFACE_CAPABILITIES 0x00000002 + +/* Interface ID Mask */ +#define STREAM_ID_STUB 0x80000000 +#define STREAM_ID_PROXY 0x40000000 +#define STREAM_ID_NONE 0x00000000 + +/* Functon ID */ +/* Common IDs for all interfaces are as follows. */ +#define RIMCALL_RELEASE 0x00000001 +#define RIMCALL_QUERYINTERFACE 0x00000002 +/* Capabilities Negotiator Interface IDs are as follows. */ +#define RIM_EXCHANGE_CAPABILITY_REQUEST 0x00000100 +/* The Client Notifications Interface ID is as follows. */ +#define PLAYBACK_ACK 0x00000100 +#define CLIENT_EVENT_NOTIFICATION 0x00000101 +/* Server Data Interface IDs are as follows. */ +#define EXCHANGE_CAPABILITIES_REQ 0x00000100 +#define SET_CHANNEL_PARAMS 0x00000101 +#define ADD_STREAM 0x00000102 +#define ON_SAMPLE 0x00000103 +#define SET_VIDEO_WINDOW 0x00000104 +#define ON_NEW_PRESENTATION 0x00000105 +#define SHUTDOWN_PRESENTATION_REQ 0x00000106 +#define SET_TOPOLOGY_REQ 0x00000107 +#define CHECK_FORMAT_SUPPORT_REQ 0x00000108 +#define ON_PLAYBACK_STARTED 0x00000109 +#define ON_PLAYBACK_PAUSED 0x0000010a +#define ON_PLAYBACK_STOPPED 0x0000010b +#define ON_PLAYBACK_RESTARTED 0x0000010c +#define ON_PLAYBACK_RATE_CHANGED 0x0000010d +#define ON_FLUSH 0x0000010e +#define ON_STREAM_VOLUME 0x0000010f +#define ON_CHANNEL_VOLUME 0x00000110 +#define ON_END_OF_STREAM 0x00000111 +#define SET_ALLOCATOR 0x00000112 +#define NOTIFY_PREROLL 0x00000113 +#define UPDATE_GEOMETRY_INFO 0x00000114 +#define REMOVE_STREAM 0x00000115 +#define SET_SOURCE_VIDEO_RECT 0x00000116 + +/* Supported platform */ +#define MMREDIR_CAPABILITY_PLATFORM_MF 0x00000001 +#define MMREDIR_CAPABILITY_PLATFORM_DSHOW 0x00000002 +#define MMREDIR_CAPABILITY_PLATFORM_OTHER 0x00000004 + +/* TSMM_CLIENT_EVENT Constants */ +#define TSMM_CLIENT_EVENT_ENDOFSTREAM 0x0064 +#define TSMM_CLIENT_EVENT_STOP_COMPLETED 0x00C8 +#define TSMM_CLIENT_EVENT_START_COMPLETED 0x00C9 +#define TSMM_CLIENT_EVENT_MONITORCHANGED 0x012C + +/* TS_MM_DATA_SAMPLE.SampleExtensions */ +#define TSMM_SAMPLE_EXT_CLEANPOINT 0x00000001 +#define TSMM_SAMPLE_EXT_DISCONTINUITY 0x00000002 +#define TSMM_SAMPLE_EXT_INTERLACED 0x00000004 +#define TSMM_SAMPLE_EXT_BOTTOMFIELDFIRST 0x00000008 +#define TSMM_SAMPLE_EXT_REPEATFIELDFIRST 0x00000010 +#define TSMM_SAMPLE_EXT_SINGLEFIELD 0x00000020 +#define TSMM_SAMPLE_EXT_DERIVEDFROMTOPFIELD 0x00000040 +#define TSMM_SAMPLE_EXT_HAS_NO_TIMESTAMPS 0x00000080 +#define TSMM_SAMPLE_EXT_RELATIVE_TIMESTAMPS 0x00000100 +#define TSMM_SAMPLE_EXT_ABSOLUTE_TIMESTAMPS 0x00000200 + +/* MajorType */ +#define TSMF_MAJOR_TYPE_UNKNOWN 0 +#define TSMF_MAJOR_TYPE_VIDEO 1 +#define TSMF_MAJOR_TYPE_AUDIO 2 + +/* SubType */ +#define TSMF_SUB_TYPE_UNKNOWN 0 +#define TSMF_SUB_TYPE_WVC1 1 +#define TSMF_SUB_TYPE_WMA2 2 +#define TSMF_SUB_TYPE_WMA9 3 +#define TSMF_SUB_TYPE_MP3 4 +#define TSMF_SUB_TYPE_MP2A 5 +#define TSMF_SUB_TYPE_MP2V 6 +#define TSMF_SUB_TYPE_WMV3 7 +#define TSMF_SUB_TYPE_AAC 8 +#define TSMF_SUB_TYPE_H264 9 +#define TSMF_SUB_TYPE_AVC1 10 +#define TSMF_SUB_TYPE_AC3 11 +#define TSMF_SUB_TYPE_WMV2 12 +#define TSMF_SUB_TYPE_WMV1 13 +#define TSMF_SUB_TYPE_MP1V 14 +#define TSMF_SUB_TYPE_MP1A 15 +#define TSMF_SUB_TYPE_YUY2 16 +#define TSMF_SUB_TYPE_MP43 17 +#define TSMF_SUB_TYPE_MP4S 18 +#define TSMF_SUB_TYPE_MP42 19 +#define TSMF_SUB_TYPE_OGG 20 +#define TSMF_SUB_TYPE_SPEEX 21 +#define TSMF_SUB_TYPE_THEORA 22 +#define TSMF_SUB_TYPE_FLAC 23 +#define TSMF_SUB_TYPE_VP8 24 +#define TSMF_SUB_TYPE_VP9 25 +#define TSMF_SUB_TYPE_H263 26 +#define TSMF_SUB_TYPE_M4S2 27 +#define TSMF_SUB_TYPE_WMA1 28 + +/* FormatType */ +#define TSMF_FORMAT_TYPE_UNKNOWN 0 +#define TSMF_FORMAT_TYPE_MFVIDEOFORMAT 1 +#define TSMF_FORMAT_TYPE_WAVEFORMATEX 2 +#define TSMF_FORMAT_TYPE_MPEG2VIDEOINFO 3 +#define TSMF_FORMAT_TYPE_VIDEOINFO2 4 +#define TSMF_FORMAT_TYPE_MPEG1VIDEOINFO 5 + +#endif /* FREERDP_CHANNEL_TSMF_CLIENT_CONSTANTS_H */ diff --git a/channels/tsmf/client/tsmf_decoder.c b/channels/tsmf/client/tsmf_decoder.c new file mode 100644 index 0000000..c59b0f6 --- /dev/null +++ b/channels/tsmf/client/tsmf_decoder.c @@ -0,0 +1,122 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Video Redirection Virtual Channel - Decoder + * + * Copyright 2010-2011 Vic Lee + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include + +#include +#include + +#include "tsmf_types.h" +#include "tsmf_constants.h" +#include "tsmf_decoder.h" + +static ITSMFDecoder* tsmf_load_decoder_by_name(const char* name) +{ + ITSMFDecoder* decoder; + TSMF_DECODER_ENTRY entry; + + entry = (TSMF_DECODER_ENTRY)(void*)freerdp_load_channel_addin_entry("tsmf", name, "decoder", 0); + + if (!entry) + return NULL; + + decoder = entry(); + + if (!decoder) + { + WLog_ERR(TAG, "failed to call export function in %s", name); + return NULL; + } + + return decoder; +} + +static BOOL tsmf_decoder_set_format(ITSMFDecoder* decoder, TS_AM_MEDIA_TYPE* media_type) +{ + if (decoder->SetFormat(decoder, media_type)) + return TRUE; + else + return FALSE; +} + +ITSMFDecoder* tsmf_load_decoder(const char* name, TS_AM_MEDIA_TYPE* media_type) +{ + ITSMFDecoder* decoder = NULL; + + if (name) + { + decoder = tsmf_load_decoder_by_name(name); + } + +#if defined(WITH_GSTREAMER_1_0) || defined(WITH_GSTREAMER_0_10) + if (!decoder) + decoder = tsmf_load_decoder_by_name("gstreamer"); +#endif + +#if defined(WITH_FFMPEG) + if (!decoder) + decoder = tsmf_load_decoder_by_name("ffmpeg"); +#endif + + if (decoder) + { + if (!tsmf_decoder_set_format(decoder, media_type)) + { + decoder->Free(decoder); + decoder = NULL; + } + } + + return decoder; +} + +BOOL tsmf_check_decoder_available(const char* name) +{ + ITSMFDecoder* decoder = NULL; + BOOL retValue = FALSE; + + if (name) + { + decoder = tsmf_load_decoder_by_name(name); + } +#if defined(WITH_GSTREAMER_1_0) || defined(WITH_GSTREAMER_0_10) + if (!decoder) + decoder = tsmf_load_decoder_by_name("gstreamer"); +#endif + +#if defined(WITH_FFMPEG) + if (!decoder) + decoder = tsmf_load_decoder_by_name("ffmpeg"); +#endif + + if (decoder) + { + decoder->Free(decoder); + decoder = NULL; + retValue = TRUE; + } + + return retValue; +} diff --git a/channels/tsmf/client/tsmf_decoder.h b/channels/tsmf/client/tsmf_decoder.h new file mode 100644 index 0000000..9a16faf --- /dev/null +++ b/channels/tsmf/client/tsmf_decoder.h @@ -0,0 +1,78 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Video Redirection Virtual Channel - Decoder + * + * Copyright 2010-2011 Vic Lee + * Copyright 2012 Hewlett-Packard Development Company, L.P. + * + * Licensed 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 FREERDP_CHANNEL_TSMF_CLIENT_DECODER_H +#define FREERDP_CHANNEL_TSMF_CLIENT_DECODER_H + +#include "tsmf_types.h" + +typedef enum _ITSMFControlMsg +{ + Control_Pause, + Control_Resume, + Control_Restart, + Control_Stop +} ITSMFControlMsg; + +typedef struct _ITSMFDecoder ITSMFDecoder; + +struct _ITSMFDecoder +{ + /* Set the decoder format. Return true if supported. */ + BOOL (*SetFormat)(ITSMFDecoder* decoder, TS_AM_MEDIA_TYPE* media_type); + /* Decode a sample. */ + BOOL (*Decode)(ITSMFDecoder* decoder, const BYTE* data, UINT32 data_size, UINT32 extensions); + /* Get the decoded data */ + BYTE* (*GetDecodedData)(ITSMFDecoder* decoder, UINT32* size); + /* Get the pixel format of decoded video frame */ + UINT32 (*GetDecodedFormat)(ITSMFDecoder* decoder); + /* Get the width and height of decoded video frame */ + BOOL (*GetDecodedDimension)(ITSMFDecoder* decoder, UINT32* width, UINT32* height); + /* Free the decoder */ + void (*Free)(ITSMFDecoder* decoder); + /* Optional Contol function */ + BOOL (*Control)(ITSMFDecoder* decoder, ITSMFControlMsg control_msg, UINT32* arg); + /* Decode a sample with extended interface. */ + BOOL(*DecodeEx) + (ITSMFDecoder* decoder, const BYTE* data, UINT32 data_size, UINT32 extensions, + UINT64 start_time, UINT64 end_time, UINT64 duration); + /* Get current play time */ + UINT64 (*GetRunningTime)(ITSMFDecoder* decoder); + /* Update Gstreamer Rendering Area */ + BOOL(*UpdateRenderingArea) + (ITSMFDecoder* decoder, int newX, int newY, int newWidth, int newHeight, int numRectangles, + RDP_RECT* rectangles); + /* Change Gstreamer Audio Volume */ + BOOL (*ChangeVolume)(ITSMFDecoder* decoder, UINT32 newVolume, UINT32 muted); + /* Check buffer level */ + BOOL (*BufferLevel)(ITSMFDecoder* decoder); + /* Register a callback for frame ack. */ + BOOL (*SetAckFunc)(ITSMFDecoder* decoder, BOOL (*cb)(void*, BOOL), void* stream); + /* Register a callback for stream seek detection. */ + BOOL (*SetSyncFunc)(ITSMFDecoder* decoder, void (*cb)(void*), void* stream); +}; + +#define TSMF_DECODER_EXPORT_FUNC_NAME "TSMFDecoderEntry" +typedef ITSMFDecoder* (*TSMF_DECODER_ENTRY)(void); + +ITSMFDecoder* tsmf_load_decoder(const char* name, TS_AM_MEDIA_TYPE* media_type); +BOOL tsmf_check_decoder_available(const char* name); + +#endif /* FREERDP_CHANNEL_TSMF_CLIENT_DECODER_H */ diff --git a/channels/tsmf/client/tsmf_ifman.c b/channels/tsmf/client/tsmf_ifman.c new file mode 100644 index 0000000..cfee41c --- /dev/null +++ b/channels/tsmf/client/tsmf_ifman.c @@ -0,0 +1,841 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Video Redirection Virtual Channel - Interface Manipulation + * + * Copyright 2010-2011 Vic Lee + * Copyright 2012 Hewlett-Packard Development Company, L.P. + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include + +#include + +#include + +#include "tsmf_types.h" +#include "tsmf_constants.h" +#include "tsmf_media.h" +#include "tsmf_codec.h" + +#include "tsmf_ifman.h" + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT tsmf_ifman_rim_exchange_capability_request(TSMF_IFMAN* ifman) +{ + UINT32 CapabilityValue; + + if (Stream_GetRemainingLength(ifman->input) < 4) + return ERROR_INVALID_DATA; + + Stream_Read_UINT32(ifman->input, CapabilityValue); + DEBUG_TSMF("server CapabilityValue %" PRIu32 "", CapabilityValue); + + if (!Stream_EnsureRemainingCapacity(ifman->output, 8)) + return ERROR_INVALID_DATA; + + Stream_Write_UINT32(ifman->output, 1); /* CapabilityValue */ + Stream_Write_UINT32(ifman->output, 0); /* Result */ + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT tsmf_ifman_exchange_capability_request(TSMF_IFMAN* ifman) +{ + UINT32 i = 0; + UINT32 v = 0; + UINT32 pos = 0; + UINT32 CapabilityType = 0; + UINT32 cbCapabilityLength = 0; + UINT32 numHostCapabilities = 0; + + WINPR_ASSERT(ifman); + if (!Stream_EnsureRemainingCapacity(ifman->output, ifman->input_size + 4)) + return ERROR_OUTOFMEMORY; + + if (Stream_GetRemainingLength(ifman->input) < ifman->input_size) + return ERROR_INVALID_DATA; + + pos = Stream_GetPosition(ifman->output); + Stream_Copy(ifman->input, ifman->output, ifman->input_size); + Stream_SetPosition(ifman->output, pos); + + if (Stream_GetRemainingLength(ifman->output) < 4) + return ERROR_INVALID_DATA; + + Stream_Read_UINT32(ifman->output, numHostCapabilities); + + for (i = 0; i < numHostCapabilities; i++) + { + if (Stream_GetRemainingLength(ifman->output) < 8) + return ERROR_INVALID_DATA; + + Stream_Read_UINT32(ifman->output, CapabilityType); + Stream_Read_UINT32(ifman->output, cbCapabilityLength); + + if (Stream_GetRemainingLength(ifman->output) < cbCapabilityLength) + return ERROR_INVALID_DATA; + + pos = Stream_GetPosition(ifman->output); + + switch (CapabilityType) + { + case 1: /* Protocol version request */ + if (Stream_GetRemainingLength(ifman->output) < 4) + return ERROR_INVALID_DATA; + + Stream_Read_UINT32(ifman->output, v); + DEBUG_TSMF("server protocol version %" PRIu32 "", v); + break; + + case 2: /* Supported platform */ + if (Stream_GetRemainingLength(ifman->output) < 4) + return ERROR_INVALID_DATA; + + Stream_Peek_UINT32(ifman->output, v); + DEBUG_TSMF("server supported platform %" PRIu32 "", v); + /* Claim that we support both MF and DShow platforms. */ + Stream_Write_UINT32(ifman->output, MMREDIR_CAPABILITY_PLATFORM_MF | + MMREDIR_CAPABILITY_PLATFORM_DSHOW); + break; + + default: + WLog_ERR(TAG, "skipping unknown capability type %" PRIu32 "", CapabilityType); + break; + } + + Stream_SetPosition(ifman->output, pos + cbCapabilityLength); + } + + Stream_Write_UINT32(ifman->output, 0); /* Result */ + ifman->output_interface_id = TSMF_INTERFACE_DEFAULT | STREAM_ID_STUB; + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT tsmf_ifman_check_format_support_request(TSMF_IFMAN* ifman) +{ + UINT32 numMediaType; + UINT32 PlatformCookie; + UINT32 FormatSupported = 1; + + if (Stream_GetRemainingLength(ifman->input) < 12) + return ERROR_INVALID_DATA; + + Stream_Read_UINT32(ifman->input, PlatformCookie); + Stream_Seek_UINT32(ifman->input); /* NoRolloverFlags (4 bytes) */ + Stream_Read_UINT32(ifman->input, numMediaType); + DEBUG_TSMF("PlatformCookie %" PRIu32 " numMediaType %" PRIu32 "", PlatformCookie, numMediaType); + + if (!tsmf_codec_check_media_type(ifman->decoder_name, ifman->input)) + FormatSupported = 0; + + if (FormatSupported) + DEBUG_TSMF("format ok."); + + if (!Stream_EnsureRemainingCapacity(ifman->output, 12)) + return -1; + + Stream_Write_UINT32(ifman->output, FormatSupported); + Stream_Write_UINT32(ifman->output, PlatformCookie); + Stream_Write_UINT32(ifman->output, 0); /* Result */ + ifman->output_interface_id = TSMF_INTERFACE_DEFAULT | STREAM_ID_STUB; + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT tsmf_ifman_on_new_presentation(TSMF_IFMAN* ifman) +{ + UINT status = CHANNEL_RC_OK; + TSMF_PRESENTATION* presentation; + DEBUG_TSMF(""); + + if (Stream_GetRemainingLength(ifman->input) < GUID_SIZE) + return ERROR_INVALID_DATA; + + presentation = tsmf_presentation_find_by_id(Stream_Pointer(ifman->input)); + + if (presentation) + { + DEBUG_TSMF("Presentation already exists"); + ifman->output_pending = FALSE; + return CHANNEL_RC_OK; + } + + presentation = tsmf_presentation_new(Stream_Pointer(ifman->input), ifman->channel_callback); + + if (!presentation) + status = ERROR_OUTOFMEMORY; + else + tsmf_presentation_set_audio_device(presentation, ifman->audio_name, ifman->audio_device); + + ifman->output_pending = TRUE; + return status; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT tsmf_ifman_add_stream(TSMF_IFMAN* ifman, rdpContext* rdpcontext) +{ + UINT32 StreamId; + UINT status = CHANNEL_RC_OK; + TSMF_STREAM* stream; + TSMF_PRESENTATION* presentation; + DEBUG_TSMF(""); + + if (Stream_GetRemainingLength(ifman->input) < GUID_SIZE + 8) + return ERROR_INVALID_DATA; + + presentation = tsmf_presentation_find_by_id(Stream_Pointer(ifman->input)); + Stream_Seek(ifman->input, GUID_SIZE); + + if (!presentation) + { + WLog_ERR(TAG, "unknown presentation id"); + status = ERROR_NOT_FOUND; + } + else + { + Stream_Read_UINT32(ifman->input, StreamId); + Stream_Seek_UINT32(ifman->input); /* numMediaType */ + stream = tsmf_stream_new(presentation, StreamId, rdpcontext); + + if (!stream) + { + WLog_ERR(TAG, "failed to create stream"); + return ERROR_OUTOFMEMORY; + } + + if (!tsmf_stream_set_format(stream, ifman->decoder_name, ifman->input)) + { + WLog_ERR(TAG, "failed to set stream format"); + return ERROR_OUTOFMEMORY; + } + + tsmf_stream_start_threads(stream); + } + + ifman->output_pending = TRUE; + return status; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT tsmf_ifman_set_topology_request(TSMF_IFMAN* ifman) +{ + DEBUG_TSMF(""); + + if (!Stream_EnsureRemainingCapacity(ifman->output, 8)) + return ERROR_OUTOFMEMORY; + + Stream_Write_UINT32(ifman->output, 1); /* TopologyReady */ + Stream_Write_UINT32(ifman->output, 0); /* Result */ + ifman->output_interface_id = TSMF_INTERFACE_DEFAULT | STREAM_ID_STUB; + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT tsmf_ifman_remove_stream(TSMF_IFMAN* ifman) +{ + int status = CHANNEL_RC_OK; + UINT32 StreamId; + TSMF_STREAM* stream; + TSMF_PRESENTATION* presentation; + DEBUG_TSMF(""); + + if (Stream_GetRemainingLength(ifman->input) < 20) + return ERROR_INVALID_DATA; + + presentation = tsmf_presentation_find_by_id(Stream_Pointer(ifman->input)); + Stream_Seek(ifman->input, GUID_SIZE); + + if (!presentation) + { + status = ERROR_NOT_FOUND; + } + else + { + Stream_Read_UINT32(ifman->input, StreamId); + stream = tsmf_stream_find_by_id(presentation, StreamId); + + if (stream) + tsmf_stream_free(stream); + else + status = ERROR_NOT_FOUND; + } + + ifman->output_pending = TRUE; + return status; +} + +static float tsmf_stream_read_float(wStream* s) +{ + float fValue; + UINT32 iValue; + Stream_Read_UINT32(s, iValue); + CopyMemory(&fValue, &iValue, 4); + return fValue; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT tsmf_ifman_set_source_video_rect(TSMF_IFMAN* ifman) +{ + UINT status = CHANNEL_RC_OK; + float Left, Top; + float Right, Bottom; + TSMF_PRESENTATION* presentation; + DEBUG_TSMF(""); + + if (Stream_GetRemainingLength(ifman->input) < 32) + return ERROR_INVALID_DATA; + + presentation = tsmf_presentation_find_by_id(Stream_Pointer(ifman->input)); + Stream_Seek(ifman->input, GUID_SIZE); + + if (!presentation) + { + status = ERROR_NOT_FOUND; + } + else + { + Left = tsmf_stream_read_float(ifman->input); /* Left (4 bytes) */ + Top = tsmf_stream_read_float(ifman->input); /* Top (4 bytes) */ + Right = tsmf_stream_read_float(ifman->input); /* Right (4 bytes) */ + Bottom = tsmf_stream_read_float(ifman->input); /* Bottom (4 bytes) */ + DEBUG_TSMF("SetSourceVideoRect: Left: %f Top: %f Right: %f Bottom: %f", Left, Top, Right, + Bottom); + } + + ifman->output_pending = TRUE; + return status; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT tsmf_ifman_shutdown_presentation(TSMF_IFMAN* ifman) +{ + TSMF_PRESENTATION* presentation; + DEBUG_TSMF(""); + + if (Stream_GetRemainingLength(ifman->input) < GUID_SIZE) + return ERROR_INVALID_DATA; + + presentation = tsmf_presentation_find_by_id(Stream_Pointer(ifman->input)); + + if (presentation) + tsmf_presentation_free(presentation); + else + { + WLog_ERR(TAG, "unknown presentation id"); + return ERROR_NOT_FOUND; + } + + if (!Stream_EnsureRemainingCapacity(ifman->output, 4)) + return ERROR_OUTOFMEMORY; + + Stream_Write_UINT32(ifman->output, 0); /* Result */ + ifman->output_interface_id = TSMF_INTERFACE_DEFAULT | STREAM_ID_STUB; + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT tsmf_ifman_on_stream_volume(TSMF_IFMAN* ifman) +{ + TSMF_PRESENTATION* presentation; + UINT32 newVolume; + UINT32 muted; + DEBUG_TSMF("on stream volume"); + + if (Stream_GetRemainingLength(ifman->input) < GUID_SIZE + 8) + return ERROR_INVALID_DATA; + + presentation = tsmf_presentation_find_by_id(Stream_Pointer(ifman->input)); + + if (!presentation) + { + WLog_ERR(TAG, "unknown presentation id"); + return ERROR_NOT_FOUND; + } + + Stream_Seek(ifman->input, 16); + Stream_Read_UINT32(ifman->input, newVolume); + DEBUG_TSMF("on stream volume: new volume=[%" PRIu32 "]", newVolume); + Stream_Read_UINT32(ifman->input, muted); + DEBUG_TSMF("on stream volume: muted=[%" PRIu32 "]", muted); + + if (!tsmf_presentation_volume_changed(presentation, newVolume, muted)) + return ERROR_INVALID_OPERATION; + + ifman->output_pending = TRUE; + return 0; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT tsmf_ifman_on_channel_volume(TSMF_IFMAN* ifman) +{ + TSMF_PRESENTATION* presentation; + DEBUG_TSMF("on channel volume"); + + if (Stream_GetRemainingLength(ifman->input) < GUID_SIZE + 8) + return ERROR_INVALID_DATA; + + presentation = tsmf_presentation_find_by_id(Stream_Pointer(ifman->input)); + + if (presentation) + { + UINT32 channelVolume; + UINT32 changedChannel; + Stream_Seek(ifman->input, 16); + Stream_Read_UINT32(ifman->input, channelVolume); + DEBUG_TSMF("on channel volume: channel volume=[%" PRIu32 "]", channelVolume); + Stream_Read_UINT32(ifman->input, changedChannel); + DEBUG_TSMF("on stream volume: changed channel=[%" PRIu32 "]", changedChannel); + } + + ifman->output_pending = TRUE; + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT tsmf_ifman_set_video_window(TSMF_IFMAN* ifman) +{ + DEBUG_TSMF(""); + ifman->output_pending = TRUE; + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT tsmf_ifman_update_geometry_info(TSMF_IFMAN* ifman) +{ + TSMF_PRESENTATION* presentation; + UINT32 numGeometryInfo; + UINT32 Left; + UINT32 Top; + UINT32 Width; + UINT32 Height; + UINT32 cbVisibleRect; + RDP_RECT* rects = NULL; + int num_rects = 0; + UINT error = CHANNEL_RC_OK; + int i; + size_t pos; + + if (Stream_GetRemainingLength(ifman->input) < GUID_SIZE + 32) + return ERROR_INVALID_DATA; + + presentation = tsmf_presentation_find_by_id(Stream_Pointer(ifman->input)); + + if (!presentation) + return ERROR_NOT_FOUND; + + Stream_Seek(ifman->input, 16); + Stream_Read_UINT32(ifman->input, numGeometryInfo); + pos = Stream_GetPosition(ifman->input); + Stream_Seek(ifman->input, 12); /* VideoWindowId (8 bytes), VideoWindowState (4 bytes) */ + Stream_Read_UINT32(ifman->input, Width); + Stream_Read_UINT32(ifman->input, Height); + Stream_Read_UINT32(ifman->input, Left); + Stream_Read_UINT32(ifman->input, Top); + Stream_SetPosition(ifman->input, pos + numGeometryInfo); + Stream_Read_UINT32(ifman->input, cbVisibleRect); + num_rects = cbVisibleRect / 16; + DEBUG_TSMF("numGeometryInfo %" PRIu32 " Width %" PRIu32 " Height %" PRIu32 " Left %" PRIu32 + " Top %" PRIu32 " cbVisibleRect %" PRIu32 " num_rects %d", + numGeometryInfo, Width, Height, Left, Top, cbVisibleRect, num_rects); + + if (num_rects > 0) + { + rects = (RDP_RECT*)calloc(num_rects, sizeof(RDP_RECT)); + + for (i = 0; i < num_rects; i++) + { + Stream_Read_UINT16(ifman->input, rects[i].y); /* Top */ + Stream_Seek_UINT16(ifman->input); + Stream_Read_UINT16(ifman->input, rects[i].x); /* Left */ + Stream_Seek_UINT16(ifman->input); + Stream_Read_UINT16(ifman->input, rects[i].height); /* Bottom */ + Stream_Seek_UINT16(ifman->input); + Stream_Read_UINT16(ifman->input, rects[i].width); /* Right */ + Stream_Seek_UINT16(ifman->input); + rects[i].width -= rects[i].x; + rects[i].height -= rects[i].y; + DEBUG_TSMF("rect %d: %" PRId16 " %" PRId16 " %" PRId16 " %" PRId16 "", i, rects[i].x, + rects[i].y, rects[i].width, rects[i].height); + } + } + + if (!tsmf_presentation_set_geometry_info(presentation, Left, Top, Width, Height, num_rects, + rects)) + return ERROR_INVALID_OPERATION; + + ifman->output_pending = TRUE; + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT tsmf_ifman_set_allocator(TSMF_IFMAN* ifman) +{ + DEBUG_TSMF(""); + ifman->output_pending = TRUE; + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT tsmf_ifman_notify_preroll(TSMF_IFMAN* ifman) +{ + DEBUG_TSMF(""); + tsmf_ifman_on_playback_paused(ifman); + ifman->output_pending = TRUE; + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT tsmf_ifman_on_sample(TSMF_IFMAN* ifman) +{ + TSMF_PRESENTATION* presentation; + TSMF_STREAM* stream; + UINT32 StreamId; + UINT64 SampleStartTime; + UINT64 SampleEndTime; + UINT64 ThrottleDuration; + UINT32 SampleExtensions; + UINT32 cbData; + UINT error; + + if (Stream_GetRemainingLength(ifman->input) < 60) + return ERROR_INVALID_DATA; + + Stream_Seek(ifman->input, 16); + Stream_Read_UINT32(ifman->input, StreamId); + Stream_Seek_UINT32(ifman->input); /* numSample */ + Stream_Read_UINT64(ifman->input, SampleStartTime); + Stream_Read_UINT64(ifman->input, SampleEndTime); + Stream_Read_UINT64(ifman->input, ThrottleDuration); + Stream_Seek_UINT32(ifman->input); /* SampleFlags */ + Stream_Read_UINT32(ifman->input, SampleExtensions); + Stream_Read_UINT32(ifman->input, cbData); + + if (Stream_GetRemainingLength(ifman->input) < cbData) + return ERROR_INVALID_DATA; + + DEBUG_TSMF("MessageId %" PRIu32 " StreamId %" PRIu32 " SampleStartTime %" PRIu64 + " SampleEndTime %" PRIu64 " " + "ThrottleDuration %" PRIu64 " SampleExtensions %" PRIu32 " cbData %" PRIu32 "", + ifman->message_id, StreamId, SampleStartTime, SampleEndTime, ThrottleDuration, + SampleExtensions, cbData); + presentation = tsmf_presentation_find_by_id(ifman->presentation_id); + + if (!presentation) + { + WLog_ERR(TAG, "unknown presentation id"); + return ERROR_NOT_FOUND; + } + + stream = tsmf_stream_find_by_id(presentation, StreamId); + + if (!stream) + { + WLog_ERR(TAG, "unknown stream id"); + return ERROR_NOT_FOUND; + } + + if (!tsmf_stream_push_sample(stream, ifman->channel_callback, ifman->message_id, + SampleStartTime, SampleEndTime, ThrottleDuration, SampleExtensions, + cbData, Stream_Pointer(ifman->input))) + { + WLog_ERR(TAG, "unable to push sample"); + return ERROR_OUTOFMEMORY; + } + + if ((error = tsmf_presentation_sync(presentation))) + { + WLog_ERR(TAG, "tsmf_presentation_sync failed with error %" PRIu32 "", error); + return error; + } + + ifman->output_pending = TRUE; + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT tsmf_ifman_on_flush(TSMF_IFMAN* ifman) +{ + UINT32 StreamId; + TSMF_PRESENTATION* presentation; + TSMF_STREAM* stream; + + if (Stream_GetRemainingLength(ifman->input) < 20) + return ERROR_INVALID_DATA; + + Stream_Seek(ifman->input, 16); + Stream_Read_UINT32(ifman->input, StreamId); + DEBUG_TSMF("StreamId %" PRIu32 "", StreamId); + presentation = tsmf_presentation_find_by_id(ifman->presentation_id); + + if (!presentation) + { + WLog_ERR(TAG, "unknown presentation id"); + return ERROR_NOT_FOUND; + } + + /* Flush message is for a stream, not the entire presentation + * therefore we only flush the stream as intended per the MS-RDPEV spec + */ + stream = tsmf_stream_find_by_id(presentation, StreamId); + + if (stream) + { + if (!tsmf_stream_flush(stream)) + return ERROR_INVALID_OPERATION; + } + else + WLog_ERR(TAG, "unknown stream id"); + + ifman->output_pending = TRUE; + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT tsmf_ifman_on_end_of_stream(TSMF_IFMAN* ifman) +{ + UINT32 StreamId; + TSMF_STREAM* stream = NULL; + TSMF_PRESENTATION* presentation; + + if (Stream_GetRemainingLength(ifman->input) < 20) + return ERROR_INVALID_DATA; + + presentation = tsmf_presentation_find_by_id(Stream_Pointer(ifman->input)); + Stream_Seek(ifman->input, 16); + Stream_Read_UINT32(ifman->input, StreamId); + + if (presentation) + { + stream = tsmf_stream_find_by_id(presentation, StreamId); + + if (stream) + tsmf_stream_end(stream, ifman->message_id, ifman->channel_callback); + } + + DEBUG_TSMF("StreamId %" PRIu32 "", StreamId); + ifman->output_pending = TRUE; + ifman->output_interface_id = TSMF_INTERFACE_CLIENT_NOTIFICATIONS | STREAM_ID_PROXY; + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT tsmf_ifman_on_playback_started(TSMF_IFMAN* ifman) +{ + TSMF_PRESENTATION* presentation; + DEBUG_TSMF(""); + + if (Stream_GetRemainingLength(ifman->input) < 16) + return ERROR_INVALID_DATA; + + presentation = tsmf_presentation_find_by_id(Stream_Pointer(ifman->input)); + + if (presentation) + tsmf_presentation_start(presentation); + else + WLog_ERR(TAG, "unknown presentation id"); + + if (!Stream_EnsureRemainingCapacity(ifman->output, 16)) + return ERROR_OUTOFMEMORY; + + Stream_Write_UINT32(ifman->output, CLIENT_EVENT_NOTIFICATION); /* FunctionId */ + Stream_Write_UINT32(ifman->output, 0); /* StreamId */ + Stream_Write_UINT32(ifman->output, TSMM_CLIENT_EVENT_START_COMPLETED); /* EventId */ + Stream_Write_UINT32(ifman->output, 0); /* cbData */ + ifman->output_interface_id = TSMF_INTERFACE_CLIENT_NOTIFICATIONS | STREAM_ID_PROXY; + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT tsmf_ifman_on_playback_paused(TSMF_IFMAN* ifman) +{ + TSMF_PRESENTATION* presentation; + DEBUG_TSMF(""); + ifman->output_pending = TRUE; + /* Added pause control so gstreamer pipeline can be paused accordingly */ + presentation = tsmf_presentation_find_by_id(Stream_Pointer(ifman->input)); + + if (presentation) + { + if (!tsmf_presentation_paused(presentation)) + return ERROR_INVALID_OPERATION; + } + else + WLog_ERR(TAG, "unknown presentation id"); + + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT tsmf_ifman_on_playback_restarted(TSMF_IFMAN* ifman) +{ + TSMF_PRESENTATION* presentation; + DEBUG_TSMF(""); + ifman->output_pending = TRUE; + /* Added restart control so gstreamer pipeline can be resumed accordingly */ + presentation = tsmf_presentation_find_by_id(Stream_Pointer(ifman->input)); + + if (presentation) + { + if (!tsmf_presentation_restarted(presentation)) + return ERROR_INVALID_OPERATION; + } + else + WLog_ERR(TAG, "unknown presentation id"); + + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT tsmf_ifman_on_playback_stopped(TSMF_IFMAN* ifman) +{ + TSMF_PRESENTATION* presentation; + DEBUG_TSMF(""); + presentation = tsmf_presentation_find_by_id(Stream_Pointer(ifman->input)); + + if (presentation) + { + if (!tsmf_presentation_stop(presentation)) + return ERROR_INVALID_OPERATION; + } + else + WLog_ERR(TAG, "unknown presentation id"); + + if (!Stream_EnsureRemainingCapacity(ifman->output, 16)) + return ERROR_OUTOFMEMORY; + + Stream_Write_UINT32(ifman->output, CLIENT_EVENT_NOTIFICATION); /* FunctionId */ + Stream_Write_UINT32(ifman->output, 0); /* StreamId */ + Stream_Write_UINT32(ifman->output, TSMM_CLIENT_EVENT_STOP_COMPLETED); /* EventId */ + Stream_Write_UINT32(ifman->output, 0); /* cbData */ + ifman->output_interface_id = TSMF_INTERFACE_CLIENT_NOTIFICATIONS | STREAM_ID_PROXY; + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT tsmf_ifman_on_playback_rate_changed(TSMF_IFMAN* ifman) +{ + DEBUG_TSMF(""); + + if (!Stream_EnsureRemainingCapacity(ifman->output, 16)) + return ERROR_OUTOFMEMORY; + + Stream_Write_UINT32(ifman->output, CLIENT_EVENT_NOTIFICATION); /* FunctionId */ + Stream_Write_UINT32(ifman->output, 0); /* StreamId */ + Stream_Write_UINT32(ifman->output, TSMM_CLIENT_EVENT_MONITORCHANGED); /* EventId */ + Stream_Write_UINT32(ifman->output, 0); /* cbData */ + ifman->output_interface_id = TSMF_INTERFACE_CLIENT_NOTIFICATIONS | STREAM_ID_PROXY; + return CHANNEL_RC_OK; +} diff --git a/channels/tsmf/client/tsmf_ifman.h b/channels/tsmf/client/tsmf_ifman.h new file mode 100644 index 0000000..3830f5b --- /dev/null +++ b/channels/tsmf/client/tsmf_ifman.h @@ -0,0 +1,69 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Video Redirection Virtual Channel - Interface Manipulation + * + * Copyright 2010-2011 Vic Lee + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger + * + * Licensed 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 FREERDP_CHANNEL_TSMF_CLIENT_IFMAN_H +#define FREERDP_CHANNEL_TSMF_CLIENT_IFMAN_H + +#include + +typedef struct _TSMF_IFMAN TSMF_IFMAN; +struct _TSMF_IFMAN +{ + IWTSVirtualChannelCallback* channel_callback; + const char* decoder_name; + const char* audio_name; + const char* audio_device; + BYTE presentation_id[16]; + UINT32 stream_id; + UINT32 message_id; + + wStream* input; + UINT32 input_size; + wStream* output; + BOOL output_pending; + UINT32 output_interface_id; +}; + +UINT tsmf_ifman_rim_exchange_capability_request(TSMF_IFMAN* ifman); +UINT tsmf_ifman_exchange_capability_request(TSMF_IFMAN* ifman); +UINT tsmf_ifman_check_format_support_request(TSMF_IFMAN* ifman); +UINT tsmf_ifman_on_new_presentation(TSMF_IFMAN* ifman); +UINT tsmf_ifman_add_stream(TSMF_IFMAN* ifman, rdpContext* rdpcontext); +UINT tsmf_ifman_set_topology_request(TSMF_IFMAN* ifman); +UINT tsmf_ifman_remove_stream(TSMF_IFMAN* ifman); +UINT tsmf_ifman_set_source_video_rect(TSMF_IFMAN* ifman); +UINT tsmf_ifman_shutdown_presentation(TSMF_IFMAN* ifman); +UINT tsmf_ifman_on_stream_volume(TSMF_IFMAN* ifman); +UINT tsmf_ifman_on_channel_volume(TSMF_IFMAN* ifman); +UINT tsmf_ifman_set_video_window(TSMF_IFMAN* ifman); +UINT tsmf_ifman_update_geometry_info(TSMF_IFMAN* ifman); +UINT tsmf_ifman_set_allocator(TSMF_IFMAN* ifman); +UINT tsmf_ifman_notify_preroll(TSMF_IFMAN* ifman); +UINT tsmf_ifman_on_sample(TSMF_IFMAN* ifman); +UINT tsmf_ifman_on_flush(TSMF_IFMAN* ifman); +UINT tsmf_ifman_on_end_of_stream(TSMF_IFMAN* ifman); +UINT tsmf_ifman_on_playback_started(TSMF_IFMAN* ifman); +UINT tsmf_ifman_on_playback_paused(TSMF_IFMAN* ifman); +UINT tsmf_ifman_on_playback_restarted(TSMF_IFMAN* ifman); +UINT tsmf_ifman_on_playback_stopped(TSMF_IFMAN* ifman); +UINT tsmf_ifman_on_playback_rate_changed(TSMF_IFMAN* ifman); + +#endif /* FREERDP_CHANNEL_TSMF_CLIENT_IFMAN_H */ diff --git a/channels/tsmf/client/tsmf_main.c b/channels/tsmf/client/tsmf_main.c new file mode 100644 index 0000000..ec281ea --- /dev/null +++ b/channels/tsmf/client/tsmf_main.c @@ -0,0 +1,624 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Video Redirection Virtual Channel + * + * Copyright 2010-2011 Vic Lee + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include + +#include + +#include "tsmf_types.h" +#include "tsmf_constants.h" +#include "tsmf_ifman.h" +#include "tsmf_media.h" + +#include "tsmf_main.h" + +BOOL tsmf_send_eos_response(IWTSVirtualChannelCallback* pChannelCallback, UINT32 message_id) +{ + wStream* s = NULL; + int status = -1; + TSMF_CHANNEL_CALLBACK* callback = (TSMF_CHANNEL_CALLBACK*)pChannelCallback; + + if (!callback) + { + DEBUG_TSMF("No callback reference - unable to send eos response!"); + return FALSE; + } + + if (callback && callback->stream_id && callback->channel && callback->channel->Write) + { + s = Stream_New(NULL, 24); + + if (!s) + return FALSE; + + Stream_Write_UINT32(s, TSMF_INTERFACE_CLIENT_NOTIFICATIONS | STREAM_ID_PROXY); + Stream_Write_UINT32(s, message_id); + Stream_Write_UINT32(s, CLIENT_EVENT_NOTIFICATION); /* FunctionId */ + Stream_Write_UINT32(s, callback->stream_id); /* StreamId */ + Stream_Write_UINT32(s, TSMM_CLIENT_EVENT_ENDOFSTREAM); /* EventId */ + Stream_Write_UINT32(s, 0); /* cbData */ + DEBUG_TSMF("EOS response size %" PRIuz "", Stream_GetPosition(s)); + status = callback->channel->Write(callback->channel, Stream_GetPosition(s), + Stream_Buffer(s), NULL); + + if (status) + { + WLog_ERR(TAG, "response error %d", status); + } + + Stream_Free(s, TRUE); + } + + return (status == 0); +} + +BOOL tsmf_playback_ack(IWTSVirtualChannelCallback* pChannelCallback, UINT32 message_id, + UINT64 duration, UINT32 data_size) +{ + wStream* s = NULL; + int status = -1; + TSMF_CHANNEL_CALLBACK* callback = (TSMF_CHANNEL_CALLBACK*)pChannelCallback; + + if (!callback) + return FALSE; + + s = Stream_New(NULL, 32); + + if (!s) + return FALSE; + + Stream_Write_UINT32(s, TSMF_INTERFACE_CLIENT_NOTIFICATIONS | STREAM_ID_PROXY); + Stream_Write_UINT32(s, message_id); + Stream_Write_UINT32(s, PLAYBACK_ACK); /* FunctionId */ + Stream_Write_UINT32(s, callback->stream_id); /* StreamId */ + Stream_Write_UINT64(s, duration); /* DataDuration */ + Stream_Write_UINT64(s, data_size); /* cbData */ + DEBUG_TSMF("ACK response size %" PRIuz "", Stream_GetPosition(s)); + + if (!callback->channel || !callback->channel->Write) + { + WLog_ERR(TAG, "callback=%p, channel=%p, write=%p", callback, + (callback ? callback->channel : NULL), + (callback && callback->channel ? callback->channel->Write : NULL)); + } + else + { + status = callback->channel->Write(callback->channel, Stream_GetPosition(s), + Stream_Buffer(s), NULL); + } + + if (status) + { + WLog_ERR(TAG, "response error %d", status); + } + + Stream_Free(s, TRUE); + return (status == 0); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT tsmf_on_data_received(IWTSVirtualChannelCallback* pChannelCallback, wStream* data) +{ + size_t length; + wStream* input; + wStream* output; + UINT error = CHANNEL_RC_OK; + BOOL processed = FALSE; + TSMF_IFMAN ifman; + UINT32 MessageId; + UINT32 FunctionId; + UINT32 InterfaceId; + TSMF_CHANNEL_CALLBACK* callback = (TSMF_CHANNEL_CALLBACK*)pChannelCallback; + UINT32 cbSize = Stream_GetRemainingLength(data); + + /* 2.2.1 Shared Message Header (SHARED_MSG_HEADER) */ + if (cbSize < 12) + { + WLog_ERR(TAG, "invalid size. cbSize=%" PRIu32 "", cbSize); + return ERROR_INVALID_DATA; + } + + input = data; + output = Stream_New(NULL, 256); + + if (!output) + return ERROR_OUTOFMEMORY; + + Stream_Seek(output, 8); + Stream_Read_UINT32(input, InterfaceId); /* InterfaceId (4 bytes) */ + Stream_Read_UINT32(input, MessageId); /* MessageId (4 bytes) */ + Stream_Read_UINT32(input, FunctionId); /* FunctionId (4 bytes) */ + DEBUG_TSMF("cbSize=%" PRIu32 " InterfaceId=0x%" PRIX32 " MessageId=0x%" PRIX32 + " FunctionId=0x%" PRIX32 "", + cbSize, InterfaceId, MessageId, FunctionId); + ZeroMemory(&ifman, sizeof(TSMF_IFMAN)); + ifman.channel_callback = pChannelCallback; + ifman.decoder_name = ((TSMF_PLUGIN*)callback->plugin)->decoder_name; + ifman.audio_name = ((TSMF_PLUGIN*)callback->plugin)->audio_name; + ifman.audio_device = ((TSMF_PLUGIN*)callback->plugin)->audio_device; + CopyMemory(ifman.presentation_id, callback->presentation_id, GUID_SIZE); + ifman.stream_id = callback->stream_id; + ifman.message_id = MessageId; + ifman.input = input; + ifman.input_size = cbSize - 12; + ifman.output = output; + ifman.output_pending = FALSE; + ifman.output_interface_id = InterfaceId; + + // fprintf(stderr, "InterfaceId: 0x%08"PRIX32" MessageId: 0x%08"PRIX32" FunctionId: + // 0x%08"PRIX32"\n", InterfaceId, MessageId, FunctionId); + + switch (InterfaceId) + { + case TSMF_INTERFACE_CAPABILITIES | STREAM_ID_NONE: + switch (FunctionId) + { + case RIM_EXCHANGE_CAPABILITY_REQUEST: + error = tsmf_ifman_rim_exchange_capability_request(&ifman); + processed = TRUE; + break; + + case RIMCALL_RELEASE: + case RIMCALL_QUERYINTERFACE: + break; + + default: + break; + } + + break; + + case TSMF_INTERFACE_DEFAULT | STREAM_ID_PROXY: + switch (FunctionId) + { + case SET_CHANNEL_PARAMS: + if (Stream_GetRemainingLength(input) < GUID_SIZE + 4) + { + error = ERROR_INVALID_DATA; + goto out; + } + + CopyMemory(callback->presentation_id, Stream_Pointer(input), GUID_SIZE); + Stream_Seek(input, GUID_SIZE); + Stream_Read_UINT32(input, callback->stream_id); + DEBUG_TSMF("SET_CHANNEL_PARAMS StreamId=%" PRIu32 "", callback->stream_id); + ifman.output_pending = TRUE; + processed = TRUE; + break; + + case EXCHANGE_CAPABILITIES_REQ: + error = tsmf_ifman_exchange_capability_request(&ifman); + processed = TRUE; + break; + + case CHECK_FORMAT_SUPPORT_REQ: + error = tsmf_ifman_check_format_support_request(&ifman); + processed = TRUE; + break; + + case ON_NEW_PRESENTATION: + error = tsmf_ifman_on_new_presentation(&ifman); + processed = TRUE; + break; + + case ADD_STREAM: + error = + tsmf_ifman_add_stream(&ifman, ((TSMF_PLUGIN*)callback->plugin)->rdpcontext); + processed = TRUE; + break; + + case SET_TOPOLOGY_REQ: + error = tsmf_ifman_set_topology_request(&ifman); + processed = TRUE; + break; + + case REMOVE_STREAM: + error = tsmf_ifman_remove_stream(&ifman); + processed = TRUE; + break; + + case SET_SOURCE_VIDEO_RECT: + error = tsmf_ifman_set_source_video_rect(&ifman); + processed = TRUE; + break; + + case SHUTDOWN_PRESENTATION_REQ: + error = tsmf_ifman_shutdown_presentation(&ifman); + processed = TRUE; + break; + + case ON_STREAM_VOLUME: + error = tsmf_ifman_on_stream_volume(&ifman); + processed = TRUE; + break; + + case ON_CHANNEL_VOLUME: + error = tsmf_ifman_on_channel_volume(&ifman); + processed = TRUE; + break; + + case SET_VIDEO_WINDOW: + error = tsmf_ifman_set_video_window(&ifman); + processed = TRUE; + break; + + case UPDATE_GEOMETRY_INFO: + error = tsmf_ifman_update_geometry_info(&ifman); + processed = TRUE; + break; + + case SET_ALLOCATOR: + error = tsmf_ifman_set_allocator(&ifman); + processed = TRUE; + break; + + case NOTIFY_PREROLL: + error = tsmf_ifman_notify_preroll(&ifman); + processed = TRUE; + break; + + case ON_SAMPLE: + error = tsmf_ifman_on_sample(&ifman); + processed = TRUE; + break; + + case ON_FLUSH: + error = tsmf_ifman_on_flush(&ifman); + processed = TRUE; + break; + + case ON_END_OF_STREAM: + error = tsmf_ifman_on_end_of_stream(&ifman); + processed = TRUE; + break; + + case ON_PLAYBACK_STARTED: + error = tsmf_ifman_on_playback_started(&ifman); + processed = TRUE; + break; + + case ON_PLAYBACK_PAUSED: + error = tsmf_ifman_on_playback_paused(&ifman); + processed = TRUE; + break; + + case ON_PLAYBACK_RESTARTED: + error = tsmf_ifman_on_playback_restarted(&ifman); + processed = TRUE; + break; + + case ON_PLAYBACK_STOPPED: + error = tsmf_ifman_on_playback_stopped(&ifman); + processed = TRUE; + break; + + case ON_PLAYBACK_RATE_CHANGED: + error = tsmf_ifman_on_playback_rate_changed(&ifman); + processed = TRUE; + break; + + case RIMCALL_RELEASE: + case RIMCALL_QUERYINTERFACE: + break; + + default: + break; + } + + break; + + default: + break; + } + + input = NULL; + ifman.input = NULL; + + if (error) + { + WLog_ERR(TAG, "ifman data received processing error %" PRIu32 "", error); + } + + if (!processed) + { + switch (FunctionId) + { + case RIMCALL_RELEASE: + /* [MS-RDPEXPS] 2.2.2.2 Interface Release (IFACE_RELEASE) + This message does not require a reply. */ + processed = TRUE; + ifman.output_pending = 1; + break; + + case RIMCALL_QUERYINTERFACE: + /* [MS-RDPEXPS] 2.2.2.1.2 Query Interface Response (QI_RSP) + This message is not supported in this channel. */ + processed = TRUE; + break; + } + + if (!processed) + { + WLog_ERR(TAG, + "Unknown InterfaceId: 0x%08" PRIX32 " MessageId: 0x%08" PRIX32 + " FunctionId: 0x%08" PRIX32 "\n", + InterfaceId, MessageId, FunctionId); + /* When a request is not implemented we return empty response indicating error */ + } + + processed = TRUE; + } + + if (processed && !ifman.output_pending) + { + /* Response packet does not have FunctionId */ + length = Stream_GetPosition(output); + Stream_SetPosition(output, 0); + Stream_Write_UINT32(output, ifman.output_interface_id); + Stream_Write_UINT32(output, MessageId); + DEBUG_TSMF("response size %d", length); + error = callback->channel->Write(callback->channel, length, Stream_Buffer(output), NULL); + + if (error) + { + WLog_ERR(TAG, "response error %" PRIu32 "", error); + } + } + +out: + Stream_Free(output, TRUE); + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT tsmf_on_close(IWTSVirtualChannelCallback* pChannelCallback) +{ + TSMF_STREAM* stream; + TSMF_PRESENTATION* presentation; + TSMF_CHANNEL_CALLBACK* callback = (TSMF_CHANNEL_CALLBACK*)pChannelCallback; + DEBUG_TSMF(""); + + if (callback->stream_id) + { + presentation = tsmf_presentation_find_by_id(callback->presentation_id); + + if (presentation) + { + stream = tsmf_stream_find_by_id(presentation, callback->stream_id); + + if (stream) + tsmf_stream_free(stream); + } + } + + free(pChannelCallback); + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT tsmf_on_new_channel_connection(IWTSListenerCallback* pListenerCallback, + IWTSVirtualChannel* pChannel, BYTE* Data, BOOL* pbAccept, + IWTSVirtualChannelCallback** ppCallback) +{ + TSMF_CHANNEL_CALLBACK* callback; + TSMF_LISTENER_CALLBACK* listener_callback = (TSMF_LISTENER_CALLBACK*)pListenerCallback; + DEBUG_TSMF(""); + callback = (TSMF_CHANNEL_CALLBACK*)calloc(1, sizeof(TSMF_CHANNEL_CALLBACK)); + + if (!callback) + return CHANNEL_RC_NO_MEMORY; + + callback->iface.OnDataReceived = tsmf_on_data_received; + callback->iface.OnClose = tsmf_on_close; + callback->iface.OnOpen = NULL; + callback->plugin = listener_callback->plugin; + callback->channel_mgr = listener_callback->channel_mgr; + callback->channel = pChannel; + *ppCallback = (IWTSVirtualChannelCallback*)callback; + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT tsmf_plugin_initialize(IWTSPlugin* pPlugin, IWTSVirtualChannelManager* pChannelMgr) +{ + UINT status; + TSMF_PLUGIN* tsmf = (TSMF_PLUGIN*)pPlugin; + DEBUG_TSMF(""); + tsmf->listener_callback = (TSMF_LISTENER_CALLBACK*)calloc(1, sizeof(TSMF_LISTENER_CALLBACK)); + + if (!tsmf->listener_callback) + return CHANNEL_RC_NO_MEMORY; + + tsmf->listener_callback->iface.OnNewChannelConnection = tsmf_on_new_channel_connection; + tsmf->listener_callback->plugin = pPlugin; + tsmf->listener_callback->channel_mgr = pChannelMgr; + status = pChannelMgr->CreateListener( + pChannelMgr, "TSMF", 0, (IWTSListenerCallback*)tsmf->listener_callback, &(tsmf->listener)); + tsmf->listener->pInterface = tsmf->iface.pInterface; + return status; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT tsmf_plugin_terminated(IWTSPlugin* pPlugin) +{ + TSMF_PLUGIN* tsmf = (TSMF_PLUGIN*)pPlugin; + DEBUG_TSMF(""); + free(tsmf->listener_callback); + free(tsmf); + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT tsmf_process_addin_args(IWTSPlugin* pPlugin, ADDIN_ARGV* args) +{ + int status; + DWORD flags; + COMMAND_LINE_ARGUMENT_A* arg; + TSMF_PLUGIN* tsmf = (TSMF_PLUGIN*)pPlugin; + COMMAND_LINE_ARGUMENT_A tsmf_args[] = { { "sys", COMMAND_LINE_VALUE_REQUIRED, "", + NULL, NULL, -1, NULL, "audio subsystem" }, + { "dev", COMMAND_LINE_VALUE_REQUIRED, "", NULL, + NULL, -1, NULL, "audio device name" }, + { "decoder", COMMAND_LINE_VALUE_REQUIRED, "", + NULL, NULL, -1, NULL, "decoder subsystem" }, + { NULL, 0, NULL, NULL, NULL, -1, NULL, NULL } }; + flags = COMMAND_LINE_SIGIL_NONE | COMMAND_LINE_SEPARATOR_COLON; + status = CommandLineParseArgumentsA(args->argc, args->argv, tsmf_args, flags, tsmf, NULL, NULL); + + if (status != 0) + return ERROR_INVALID_DATA; + + arg = tsmf_args; + + do + { + if (!(arg->Flags & COMMAND_LINE_VALUE_PRESENT)) + continue; + + CommandLineSwitchStart(arg) CommandLineSwitchCase(arg, "sys") + { + tsmf->audio_name = _strdup(arg->Value); + + if (!tsmf->audio_name) + return ERROR_OUTOFMEMORY; + } + CommandLineSwitchCase(arg, "dev") + { + tsmf->audio_device = _strdup(arg->Value); + + if (!tsmf->audio_device) + return ERROR_OUTOFMEMORY; + } + CommandLineSwitchCase(arg, "decoder") + { + tsmf->decoder_name = _strdup(arg->Value); + + if (!tsmf->decoder_name) + return ERROR_OUTOFMEMORY; + } + CommandLineSwitchDefault(arg) + { + } + CommandLineSwitchEnd(arg) + } while ((arg = CommandLineFindNextArgumentA(arg)) != NULL); + + return CHANNEL_RC_OK; +} + +#ifdef BUILTIN_CHANNELS +#define DVCPluginEntry tsmf_DVCPluginEntry +#else +#define DVCPluginEntry FREERDP_API DVCPluginEntry +#endif + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT DVCPluginEntry(IDRDYNVC_ENTRY_POINTS* pEntryPoints) +{ + UINT status = 0; + TSMF_PLUGIN* tsmf; + TsmfClientContext* context; + UINT error = CHANNEL_RC_NO_MEMORY; + tsmf = (TSMF_PLUGIN*)pEntryPoints->GetPlugin(pEntryPoints, "tsmf"); + + if (!tsmf) + { + tsmf = (TSMF_PLUGIN*)calloc(1, sizeof(TSMF_PLUGIN)); + + if (!tsmf) + { + WLog_ERR(TAG, "calloc failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + tsmf->iface.Initialize = tsmf_plugin_initialize; + tsmf->iface.Connected = NULL; + tsmf->iface.Disconnected = NULL; + tsmf->iface.Terminated = tsmf_plugin_terminated; + tsmf->rdpcontext = + ((freerdp*)((rdpSettings*)pEntryPoints->GetRdpSettings(pEntryPoints))->instance) + ->context; + context = (TsmfClientContext*)calloc(1, sizeof(TsmfClientContext)); + + if (!context) + { + WLog_ERR(TAG, "calloc failed!"); + goto error_context; + } + + context->handle = (void*)tsmf; + tsmf->iface.pInterface = (void*)context; + + if (!tsmf_media_init()) + { + error = ERROR_INVALID_OPERATION; + goto error_init; + } + + status = pEntryPoints->RegisterPlugin(pEntryPoints, "tsmf", (IWTSPlugin*)tsmf); + } + + if (status == CHANNEL_RC_OK) + { + status = + tsmf_process_addin_args((IWTSPlugin*)tsmf, pEntryPoints->GetPluginData(pEntryPoints)); + } + + return status; +error_init: + free(context); +error_context: + free(tsmf); + return error; +} diff --git a/channels/tsmf/client/tsmf_main.h b/channels/tsmf/client/tsmf_main.h new file mode 100644 index 0000000..366215c --- /dev/null +++ b/channels/tsmf/client/tsmf_main.h @@ -0,0 +1,71 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Video Redirection Virtual Channel + * + * Copyright 2010-2011 Vic Lee + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger + * + * Licensed 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 FREERDP_CHANNEL_TSMF_CLIENT_MAIN_H +#define FREERDP_CHANNEL_TSMF_CLIENT_MAIN_H + +#include + +typedef struct _TSMF_LISTENER_CALLBACK TSMF_LISTENER_CALLBACK; + +typedef struct _TSMF_CHANNEL_CALLBACK TSMF_CHANNEL_CALLBACK; + +typedef struct _TSMF_PLUGIN TSMF_PLUGIN; + +struct _TSMF_LISTENER_CALLBACK +{ + IWTSListenerCallback iface; + + IWTSPlugin* plugin; + IWTSVirtualChannelManager* channel_mgr; +}; + +struct _TSMF_CHANNEL_CALLBACK +{ + IWTSVirtualChannelCallback iface; + + IWTSPlugin* plugin; + IWTSVirtualChannelManager* channel_mgr; + IWTSVirtualChannel* channel; + + BYTE presentation_id[GUID_SIZE]; + UINT32 stream_id; +}; + +struct _TSMF_PLUGIN +{ + IWTSPlugin iface; + + IWTSListener* listener; + TSMF_LISTENER_CALLBACK* listener_callback; + + const char* decoder_name; + const char* audio_name; + const char* audio_device; + + rdpContext* rdpcontext; +}; + +BOOL tsmf_send_eos_response(IWTSVirtualChannelCallback* pChannelCallback, UINT32 message_id); +BOOL tsmf_playback_ack(IWTSVirtualChannelCallback* pChannelCallback, UINT32 message_id, + UINT64 duration, UINT32 data_size); + +#endif /* FREERDP_CHANNEL_TSMF_CLIENT_MAIN_H */ diff --git a/channels/tsmf/client/tsmf_media.c b/channels/tsmf/client/tsmf_media.c new file mode 100644 index 0000000..b77a3c6 --- /dev/null +++ b/channels/tsmf/client/tsmf_media.c @@ -0,0 +1,1552 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Video Redirection Virtual Channel - Media Container + * + * Copyright 2010-2011 Vic Lee + * Copyright 2012 Hewlett-Packard Development Company, L.P. + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include +#include + +#ifdef HAVE_UNISTD_H +#include +#endif + +#ifndef _WIN32 +#include +#endif + +#include +#include +#include +#include +#include +#include + +#include + +#include "tsmf_constants.h" +#include "tsmf_types.h" +#include "tsmf_decoder.h" +#include "tsmf_audio.h" +#include "tsmf_main.h" +#include "tsmf_codec.h" +#include "tsmf_media.h" + +#define AUDIO_TOLERANCE 10000000LL + +/* 1 second = 10,000,000 100ns units*/ +#define VIDEO_ADJUST_MAX 10 * 1000 * 1000 + +#define MAX_ACK_TIME 666667 + +#define AUDIO_MIN_BUFFER_LEVEL 3 +#define AUDIO_MAX_BUFFER_LEVEL 6 + +#define VIDEO_MIN_BUFFER_LEVEL 10 +#define VIDEO_MAX_BUFFER_LEVEL 30 + +struct _TSMF_PRESENTATION +{ + BYTE presentation_id[GUID_SIZE]; + + const char* audio_name; + const char* audio_device; + + IWTSVirtualChannelCallback* channel_callback; + + UINT64 audio_start_time; + UINT64 audio_end_time; + + UINT32 volume; + UINT32 muted; + + wArrayList* stream_list; + + int x; + int y; + int width; + int height; + + int nr_rects; + void* rects; +}; + +struct _TSMF_STREAM +{ + UINT32 stream_id; + + TSMF_PRESENTATION* presentation; + + ITSMFDecoder* decoder; + + int major_type; + int eos; + UINT32 eos_message_id; + IWTSVirtualChannelCallback* eos_channel_callback; + int delayed_stop; + UINT32 width; + UINT32 height; + + ITSMFAudioDevice* audio; + UINT32 sample_rate; + UINT32 channels; + UINT32 bits_per_sample; + + /* The start time of last played sample */ + UINT64 last_start_time; + /* The end_time of last played sample */ + UINT64 last_end_time; + /* Next sample should not start before this system time. */ + UINT64 next_start_time; + + UINT32 minBufferLevel; + UINT32 maxBufferLevel; + UINT32 currentBufferLevel; + + HANDLE play_thread; + HANDLE ack_thread; + HANDLE stopEvent; + HANDLE ready; + + wQueue* sample_list; + wQueue* sample_ack_list; + rdpContext* rdpcontext; + + BOOL seeking; +}; + +struct _TSMF_SAMPLE +{ + UINT32 sample_id; + UINT64 start_time; + UINT64 end_time; + UINT64 duration; + UINT32 extensions; + UINT32 data_size; + BYTE* data; + UINT32 decoded_size; + UINT32 pixfmt; + + BOOL invalidTimestamps; + + TSMF_STREAM* stream; + IWTSVirtualChannelCallback* channel_callback; + UINT64 ack_time; +}; + +static wArrayList* presentation_list = NULL; +static int TERMINATING = 0; + +static void _tsmf_presentation_free(void* obj); +static void _tsmf_stream_free(void* obj); + +static UINT64 get_current_time(void) +{ + struct timeval tp; + gettimeofday(&tp, 0); + return ((UINT64)tp.tv_sec) * 10000000LL + ((UINT64)tp.tv_usec) * 10LL; +} + +static TSMF_SAMPLE* tsmf_stream_pop_sample(TSMF_STREAM* stream, int sync) +{ + UINT32 index; + UINT32 count; + TSMF_STREAM* s; + TSMF_SAMPLE* sample; + BOOL pending = FALSE; + TSMF_PRESENTATION* presentation = NULL; + + if (!stream) + return NULL; + + presentation = stream->presentation; + + if (Queue_Count(stream->sample_list) < 1) + return NULL; + + if (sync) + { + if (stream->decoder) + { + if (stream->decoder->GetDecodedData) + { + if (stream->major_type == TSMF_MAJOR_TYPE_AUDIO) + { + /* Check if some other stream has earlier sample that needs to be played first + */ + /* Start time is more reliable than end time as some stream types seem to have + * incorrect end times from the server + */ + if (stream->last_start_time > AUDIO_TOLERANCE) + { + ArrayList_Lock(presentation->stream_list); + count = ArrayList_Count(presentation->stream_list); + + for (index = 0; index < count; index++) + { + s = (TSMF_STREAM*)ArrayList_GetItem(presentation->stream_list, index); + + /* Start time is more reliable than end time as some stream types seem + * to have incorrect end times from the server + */ + if (s != stream && !s->eos && s->last_start_time && + s->last_start_time < stream->last_start_time - AUDIO_TOLERANCE) + { + DEBUG_TSMF("Pending due to audio tolerance"); + pending = TRUE; + break; + } + } + + ArrayList_Unlock(presentation->stream_list); + } + } + else + { + /* Start time is more reliable than end time as some stream types seem to have + * incorrect end times from the server + */ + if (stream->last_start_time > presentation->audio_start_time) + { + DEBUG_TSMF("Pending due to stream start time > audio start time"); + pending = TRUE; + } + } + } + } + } + + if (pending) + return NULL; + + sample = (TSMF_SAMPLE*)Queue_Dequeue(stream->sample_list); + + /* Only update stream last end time if the sample end time is valid and greater than the current + * stream end time */ + if (sample && (sample->end_time > stream->last_end_time) && (!sample->invalidTimestamps)) + stream->last_end_time = sample->end_time; + + /* Only update stream last start time if the sample start time is valid and greater than the + * current stream start time */ + if (sample && (sample->start_time > stream->last_start_time) && (!sample->invalidTimestamps)) + stream->last_start_time = sample->start_time; + + return sample; +} + +static void tsmf_sample_free(void* arg) +{ + TSMF_SAMPLE* sample = arg; + + if (!sample) + return; + + free(sample->data); + free(sample); +} + +static BOOL tsmf_sample_ack(TSMF_SAMPLE* sample) +{ + if (!sample) + return FALSE; + + return tsmf_playback_ack(sample->channel_callback, sample->sample_id, sample->duration, + sample->data_size); +} + +static BOOL tsmf_sample_queue_ack(TSMF_SAMPLE* sample) +{ + if (!sample) + return FALSE; + + if (!sample->stream) + return FALSE; + + return Queue_Enqueue(sample->stream->sample_ack_list, sample); +} + +/* Returns TRUE if no more samples are currently available + * Returns FALSE otherwise + */ +static BOOL tsmf_stream_process_ack(void* arg, BOOL force) +{ + TSMF_STREAM* stream = arg; + TSMF_SAMPLE* sample; + UINT64 ack_time; + BOOL rc = FALSE; + + if (!stream) + return TRUE; + + Queue_Lock(stream->sample_ack_list); + sample = (TSMF_SAMPLE*)Queue_Peek(stream->sample_ack_list); + + if (!sample) + { + rc = TRUE; + goto finally; + } + + if (!force) + { + /* Do some min/max ack limiting if we have access to Buffer level information */ + if (stream->decoder && stream->decoder->BufferLevel) + { + /* Try to keep buffer level below max by withholding acks */ + if (stream->currentBufferLevel > stream->maxBufferLevel) + goto finally; + /* Try to keep buffer level above min by pushing acks through quickly */ + else if (stream->currentBufferLevel < stream->minBufferLevel) + goto dequeue; + } + + /* Time based acks only */ + ack_time = get_current_time(); + + if (sample->ack_time > ack_time) + goto finally; + } + +dequeue: + sample = Queue_Dequeue(stream->sample_ack_list); + + if (sample) + { + tsmf_sample_ack(sample); + tsmf_sample_free(sample); + } + +finally: + Queue_Unlock(stream->sample_ack_list); + return rc; +} + +TSMF_PRESENTATION* tsmf_presentation_new(const BYTE* guid, + IWTSVirtualChannelCallback* pChannelCallback) +{ + TSMF_PRESENTATION* presentation; + + if (!guid || !pChannelCallback) + return NULL; + + presentation = (TSMF_PRESENTATION*)calloc(1, sizeof(TSMF_PRESENTATION)); + + if (!presentation) + { + WLog_ERR(TAG, "calloc failed"); + return NULL; + } + + CopyMemory(presentation->presentation_id, guid, GUID_SIZE); + presentation->channel_callback = pChannelCallback; + presentation->volume = 5000; /* 50% */ + presentation->muted = 0; + + if (!(presentation->stream_list = ArrayList_New(TRUE))) + goto error_stream_list; + + ArrayList_Object(presentation->stream_list)->fnObjectFree = _tsmf_stream_free; + + if (ArrayList_Add(presentation_list, presentation) < 0) + goto error_add; + + return presentation; +error_add: + ArrayList_Free(presentation->stream_list); +error_stream_list: + free(presentation); + return NULL; +} + +static char* guid_to_string(const BYTE* guid, char* str, size_t len) +{ + size_t i; + + if (!guid || !str) + return NULL; + + for (i = 0; i < GUID_SIZE && (len > 2 * i); i++) + sprintf_s(str + (2 * i), len - 2 * i, "%02" PRIX8 "", guid[i]); + + return str; +} + +TSMF_PRESENTATION* tsmf_presentation_find_by_id(const BYTE* guid) +{ + UINT32 index; + UINT32 count; + BOOL found = FALSE; + char guid_str[GUID_SIZE * 2 + 1]; + TSMF_PRESENTATION* presentation; + ArrayList_Lock(presentation_list); + count = ArrayList_Count(presentation_list); + + for (index = 0; index < count; index++) + { + presentation = (TSMF_PRESENTATION*)ArrayList_GetItem(presentation_list, index); + + if (memcmp(presentation->presentation_id, guid, GUID_SIZE) == 0) + { + found = TRUE; + break; + } + } + + ArrayList_Unlock(presentation_list); + + if (!found) + WLog_WARN(TAG, "presentation id %s not found", + guid_to_string(guid, guid_str, sizeof(guid_str))); + + return (found) ? presentation : NULL; +} + +static BOOL tsmf_sample_playback_video(TSMF_SAMPLE* sample) +{ + UINT64 t; + TSMF_VIDEO_FRAME_EVENT event; + TSMF_STREAM* stream = sample->stream; + TSMF_PRESENTATION* presentation = stream->presentation; + TSMF_CHANNEL_CALLBACK* callback = (TSMF_CHANNEL_CALLBACK*)sample->channel_callback; + TsmfClientContext* tsmf = (TsmfClientContext*)callback->plugin->pInterface; + DEBUG_TSMF("MessageId %" PRIu32 " EndTime %" PRIu64 " data_size %" PRIu32 " consumed.", + sample->sample_id, sample->end_time, sample->data_size); + + if (sample->data) + { + t = get_current_time(); + + /* Start time is more reliable than end time as some stream types seem to have incorrect + * end times from the server + */ + if (stream->next_start_time > t && + ((sample->start_time >= presentation->audio_start_time) || + ((sample->start_time < stream->last_start_time) && (!sample->invalidTimestamps)))) + { + USleep((stream->next_start_time - t) / 10); + } + + stream->next_start_time = t + sample->duration - 50000; + ZeroMemory(&event, sizeof(TSMF_VIDEO_FRAME_EVENT)); + event.frameData = sample->data; + event.frameSize = sample->decoded_size; + event.framePixFmt = sample->pixfmt; + event.frameWidth = sample->stream->width; + event.frameHeight = sample->stream->height; + event.x = presentation->x; + event.y = presentation->y; + event.width = presentation->width; + event.height = presentation->height; + + if (presentation->nr_rects > 0) + { + event.numVisibleRects = presentation->nr_rects; + event.visibleRects = (RECTANGLE_16*)calloc(event.numVisibleRects, sizeof(RECTANGLE_16)); + + if (!event.visibleRects) + { + WLog_ERR(TAG, "can't allocate memory for copy rectangles"); + return FALSE; + } + + memcpy(event.visibleRects, presentation->rects, + presentation->nr_rects * sizeof(RDP_RECT)); + presentation->nr_rects = 0; + } + +#if 0 + /* Dump a .ppm image for every 30 frames. Assuming the frame is in YUV format, we + extract the Y values to create a grayscale image. */ + static int frame_id = 0; + char buf[100]; + FILE* fp; + + if ((frame_id % 30) == 0) + { + sprintf_s(buf, sizeof(buf), "/tmp/FreeRDP_Frame_%d.ppm", frame_id); + fp = fopen(buf, "wb"); + fwrite("P5\n", 1, 3, fp); + sprintf_s(buf, sizeof(buf), "%"PRIu32" %"PRIu32"\n", sample->stream->width, + sample->stream->height); + fwrite(buf, 1, strnlen(buf, sizeof(buf)), fp); + fwrite("255\n", 1, 4, fp); + fwrite(sample->data, 1, sample->stream->width * sample->stream->height, fp); + fflush(fp); + fclose(fp); + } + + frame_id++; +#endif + /* The frame data ownership is passed to the event object, and is freed after the event is + * processed. */ + sample->data = NULL; + sample->decoded_size = 0; + + if (tsmf->FrameEvent) + tsmf->FrameEvent(tsmf, &event); + + free(event.frameData); + + if (event.visibleRects != NULL) + free(event.visibleRects); + } + + return TRUE; +} + +static BOOL tsmf_sample_playback_audio(TSMF_SAMPLE* sample) +{ + UINT64 latency = 0; + TSMF_STREAM* stream = sample->stream; + BOOL ret; + DEBUG_TSMF("MessageId %" PRIu32 " EndTime %" PRIu64 " consumed.", sample->sample_id, + sample->end_time); + + if (stream->audio && sample->data) + { + ret = + sample->stream->audio->Play(sample->stream->audio, sample->data, sample->decoded_size); + free(sample->data); + sample->data = NULL; + sample->decoded_size = 0; + + if (stream->audio->GetLatency) + latency = stream->audio->GetLatency(stream->audio); + } + else + { + ret = TRUE; + latency = 0; + } + + sample->ack_time = latency + get_current_time(); + + /* Only update stream times if the sample timestamps are valid */ + if (!sample->invalidTimestamps) + { + stream->last_start_time = sample->start_time + latency; + stream->last_end_time = sample->end_time + latency; + stream->presentation->audio_start_time = sample->start_time + latency; + stream->presentation->audio_end_time = sample->end_time + latency; + } + + return ret; +} + +static BOOL tsmf_sample_playback(TSMF_SAMPLE* sample) +{ + BOOL ret = FALSE; + UINT32 width; + UINT32 height; + UINT32 pixfmt = 0; + TSMF_STREAM* stream = sample->stream; + + if (stream->decoder) + { + if (stream->decoder->DecodeEx) + { + /* Try to "sync" video buffers to audio buffers by looking at the running time for each + * stream The difference between the two running times causes an offset between audio + * and video actual render times. So, we try to adjust timestamps on the video buffer to + * match those on the audio buffer. + */ + if (stream->major_type == TSMF_MAJOR_TYPE_VIDEO) + { + TSMF_STREAM* temp_stream = NULL; + TSMF_PRESENTATION* presentation = stream->presentation; + ArrayList_Lock(presentation->stream_list); + int count = ArrayList_Count(presentation->stream_list); + int index = 0; + + for (index = 0; index < count; index++) + { + UINT64 time_diff; + temp_stream = (TSMF_STREAM*)ArrayList_GetItem(presentation->stream_list, index); + + if (temp_stream->major_type == TSMF_MAJOR_TYPE_AUDIO) + { + UINT64 video_time = + (UINT64)stream->decoder->GetRunningTime(stream->decoder); + UINT64 audio_time = + (UINT64)temp_stream->decoder->GetRunningTime(temp_stream->decoder); + UINT64 max_adjust = VIDEO_ADJUST_MAX; + + if (video_time < audio_time) + max_adjust = -VIDEO_ADJUST_MAX; + + if (video_time > audio_time) + time_diff = video_time - audio_time; + else + time_diff = audio_time - video_time; + + time_diff = time_diff < VIDEO_ADJUST_MAX ? time_diff : max_adjust; + sample->start_time += time_diff; + sample->end_time += time_diff; + break; + } + } + + ArrayList_Unlock(presentation->stream_list); + } + + ret = stream->decoder->DecodeEx(stream->decoder, sample->data, sample->data_size, + sample->extensions, sample->start_time, + sample->end_time, sample->duration); + } + else + { + ret = stream->decoder->Decode(stream->decoder, sample->data, sample->data_size, + sample->extensions); + } + } + + if (!ret) + { + WLog_ERR(TAG, "decode error, queue ack anyways"); + + if (!tsmf_sample_queue_ack(sample)) + { + WLog_ERR(TAG, "error queuing sample for ack"); + return FALSE; + } + + return TRUE; + } + + free(sample->data); + sample->data = NULL; + + if (stream->major_type == TSMF_MAJOR_TYPE_VIDEO) + { + if (stream->decoder->GetDecodedFormat) + { + pixfmt = stream->decoder->GetDecodedFormat(stream->decoder); + + if (pixfmt == ((UINT32)-1)) + { + WLog_ERR(TAG, "unable to decode video format"); + + if (!tsmf_sample_queue_ack(sample)) + { + WLog_ERR(TAG, "error queuing sample for ack"); + } + + return FALSE; + } + + sample->pixfmt = pixfmt; + } + + if (stream->decoder->GetDecodedDimension) + { + ret = stream->decoder->GetDecodedDimension(stream->decoder, &width, &height); + + if (ret && (width != stream->width || height != stream->height)) + { + DEBUG_TSMF("video dimension changed to %" PRIu32 " x %" PRIu32 "", width, height); + stream->width = width; + stream->height = height; + } + } + } + + if (stream->decoder->GetDecodedData) + { + sample->data = stream->decoder->GetDecodedData(stream->decoder, &sample->decoded_size); + + switch (sample->stream->major_type) + { + case TSMF_MAJOR_TYPE_VIDEO: + ret = tsmf_sample_playback_video(sample) && tsmf_sample_queue_ack(sample); + break; + + case TSMF_MAJOR_TYPE_AUDIO: + ret = tsmf_sample_playback_audio(sample) && tsmf_sample_queue_ack(sample); + break; + } + } + else + { + TSMF_STREAM* stream = sample->stream; + UINT64 ack_anticipation_time = get_current_time(); + BOOL buffer_filled = TRUE; + + /* Classify the buffer as filled once it reaches minimum level */ + if (stream->decoder->BufferLevel) + { + if (stream->currentBufferLevel < stream->minBufferLevel) + buffer_filled = FALSE; + } + + ack_anticipation_time += + (sample->duration / 2 < MAX_ACK_TIME) ? sample->duration / 2 : MAX_ACK_TIME; + + switch (sample->stream->major_type) + { + case TSMF_MAJOR_TYPE_VIDEO: + { + break; + } + + case TSMF_MAJOR_TYPE_AUDIO: + { + break; + } + } + + sample->ack_time = ack_anticipation_time; + + if (!tsmf_sample_queue_ack(sample)) + { + WLog_ERR(TAG, "error queuing sample for ack"); + ret = FALSE; + } + } + + return ret; +} + +static DWORD WINAPI tsmf_stream_ack_func(LPVOID arg) +{ + HANDLE hdl[2]; + TSMF_STREAM* stream = (TSMF_STREAM*)arg; + UINT error = CHANNEL_RC_OK; + DEBUG_TSMF("in %" PRIu32 "", stream->stream_id); + hdl[0] = stream->stopEvent; + hdl[1] = Queue_Event(stream->sample_ack_list); + + while (1) + { + DWORD ev = WaitForMultipleObjects(2, hdl, FALSE, 1000); + + if (ev == WAIT_FAILED) + { + error = GetLastError(); + WLog_ERR(TAG, "WaitForMultipleObjects failed with error %" PRIu32 "!", error); + break; + } + + if (stream->decoder) + if (stream->decoder->BufferLevel) + stream->currentBufferLevel = stream->decoder->BufferLevel(stream->decoder); + + if (stream->eos) + { + while ((stream->currentBufferLevel > 0) && !(tsmf_stream_process_ack(stream, TRUE))) + { + DEBUG_TSMF("END OF STREAM PROCESSING!"); + + if (stream->decoder && stream->decoder->BufferLevel) + stream->currentBufferLevel = stream->decoder->BufferLevel(stream->decoder); + else + stream->currentBufferLevel = 1; + + USleep(1000); + } + + tsmf_send_eos_response(stream->eos_channel_callback, stream->eos_message_id); + stream->eos = 0; + + if (stream->delayed_stop) + { + DEBUG_TSMF("Finishing delayed stream stop, now that eos has processed."); + tsmf_stream_flush(stream); + + if (stream->decoder && stream->decoder->Control) + stream->decoder->Control(stream->decoder, Control_Stop, NULL); + } + } + + /* Stream stopped force all of the acks to happen */ + if (ev == WAIT_OBJECT_0) + { + DEBUG_TSMF("ack: Stream stopped!"); + + while (1) + { + if (tsmf_stream_process_ack(stream, TRUE)) + break; + + USleep(1000); + } + + break; + } + + if (tsmf_stream_process_ack(stream, FALSE)) + continue; + + if (stream->currentBufferLevel > stream->minBufferLevel) + USleep(1000); + } + + if (error && stream->rdpcontext) + setChannelError(stream->rdpcontext, error, "tsmf_stream_ack_func reported an error"); + + DEBUG_TSMF("out %" PRIu32 "", stream->stream_id); + ExitThread(error); + return error; +} + +static DWORD WINAPI tsmf_stream_playback_func(LPVOID arg) +{ + HANDLE hdl[2]; + TSMF_SAMPLE* sample = NULL; + TSMF_STREAM* stream = (TSMF_STREAM*)arg; + TSMF_PRESENTATION* presentation = stream->presentation; + UINT error = CHANNEL_RC_OK; + DWORD status; + DEBUG_TSMF("in %" PRIu32 "", stream->stream_id); + + if (stream->major_type == TSMF_MAJOR_TYPE_AUDIO && stream->sample_rate && stream->channels && + stream->bits_per_sample) + { + if (stream->decoder) + { + if (stream->decoder->GetDecodedData) + { + stream->audio = tsmf_load_audio_device( + presentation->audio_name && presentation->audio_name[0] + ? presentation->audio_name + : NULL, + presentation->audio_device && presentation->audio_device[0] + ? presentation->audio_device + : NULL); + + if (stream->audio) + { + stream->audio->SetFormat(stream->audio, stream->sample_rate, stream->channels, + stream->bits_per_sample); + } + } + } + } + + hdl[0] = stream->stopEvent; + hdl[1] = Queue_Event(stream->sample_list); + + while (1) + { + status = WaitForMultipleObjects(2, hdl, FALSE, 1000); + + if (status == WAIT_FAILED) + { + error = GetLastError(); + WLog_ERR(TAG, "WaitForMultipleObjects failed with error %" PRIu32 "!", error); + break; + } + + status = WaitForSingleObject(stream->stopEvent, 0); + + if (status == WAIT_FAILED) + { + error = GetLastError(); + WLog_ERR(TAG, "WaitForSingleObject failed with error %" PRIu32 "!", error); + break; + } + + if (status == WAIT_OBJECT_0) + break; + + if (stream->decoder) + if (stream->decoder->BufferLevel) + stream->currentBufferLevel = stream->decoder->BufferLevel(stream->decoder); + + sample = tsmf_stream_pop_sample(stream, 0); + + if (sample && !tsmf_sample_playback(sample)) + { + WLog_ERR(TAG, "error playing sample"); + error = ERROR_INTERNAL_ERROR; + break; + } + + if (stream->currentBufferLevel > stream->minBufferLevel) + USleep(1000); + } + + if (stream->audio) + { + stream->audio->Free(stream->audio); + stream->audio = NULL; + } + + if (error && stream->rdpcontext) + setChannelError(stream->rdpcontext, error, "tsmf_stream_playback_func reported an error"); + + DEBUG_TSMF("out %" PRIu32 "", stream->stream_id); + ExitThread(error); + return error; +} + +static BOOL tsmf_stream_start(TSMF_STREAM* stream) +{ + if (!stream || !stream->presentation || !stream->decoder || !stream->decoder->Control) + return TRUE; + + stream->eos = 0; + return stream->decoder->Control(stream->decoder, Control_Restart, NULL); +} + +static BOOL tsmf_stream_stop(TSMF_STREAM* stream) +{ + if (!stream || !stream->decoder || !stream->decoder->Control) + return TRUE; + + /* If stopping after eos - we delay until the eos has been processed + * this allows us to process any buffers that have been acked even though + * they have not actually been completely processes by the decoder + */ + if (stream->eos) + { + DEBUG_TSMF("Setting up a delayed stop for once the eos has been processed."); + stream->delayed_stop = 1; + return TRUE; + } + /* Otherwise force stop immediately */ + else + { + DEBUG_TSMF("Stop with no pending eos response, so do it immediately."); + tsmf_stream_flush(stream); + return stream->decoder->Control(stream->decoder, Control_Stop, NULL); + } +} + +static BOOL tsmf_stream_pause(TSMF_STREAM* stream) +{ + if (!stream || !stream->decoder || !stream->decoder->Control) + return TRUE; + + return stream->decoder->Control(stream->decoder, Control_Pause, NULL); +} + +static BOOL tsmf_stream_restart(TSMF_STREAM* stream) +{ + if (!stream || !stream->decoder || !stream->decoder->Control) + return TRUE; + + stream->eos = 0; + return stream->decoder->Control(stream->decoder, Control_Restart, NULL); +} + +static BOOL tsmf_stream_change_volume(TSMF_STREAM* stream, UINT32 newVolume, UINT32 muted) +{ + if (!stream || !stream->decoder) + return TRUE; + + if (stream->decoder != NULL && stream->decoder->ChangeVolume) + { + return stream->decoder->ChangeVolume(stream->decoder, newVolume, muted); + } + else if (stream->audio != NULL && stream->audio->ChangeVolume) + { + return stream->audio->ChangeVolume(stream->audio, newVolume, muted); + } + + return TRUE; +} + +BOOL tsmf_presentation_volume_changed(TSMF_PRESENTATION* presentation, UINT32 newVolume, + UINT32 muted) +{ + UINT32 index; + UINT32 count; + TSMF_STREAM* stream; + BOOL ret = TRUE; + presentation->volume = newVolume; + presentation->muted = muted; + ArrayList_Lock(presentation->stream_list); + count = ArrayList_Count(presentation->stream_list); + + for (index = 0; index < count; index++) + { + stream = (TSMF_STREAM*)ArrayList_GetItem(presentation->stream_list, index); + ret &= tsmf_stream_change_volume(stream, newVolume, muted); + } + + ArrayList_Unlock(presentation->stream_list); + return ret; +} + +BOOL tsmf_presentation_paused(TSMF_PRESENTATION* presentation) +{ + UINT32 index; + UINT32 count; + TSMF_STREAM* stream; + BOOL ret = TRUE; + ArrayList_Lock(presentation->stream_list); + count = ArrayList_Count(presentation->stream_list); + + for (index = 0; index < count; index++) + { + stream = (TSMF_STREAM*)ArrayList_GetItem(presentation->stream_list, index); + ret &= tsmf_stream_pause(stream); + } + + ArrayList_Unlock(presentation->stream_list); + return ret; +} + +BOOL tsmf_presentation_restarted(TSMF_PRESENTATION* presentation) +{ + UINT32 index; + UINT32 count; + TSMF_STREAM* stream; + BOOL ret = TRUE; + ArrayList_Lock(presentation->stream_list); + count = ArrayList_Count(presentation->stream_list); + + for (index = 0; index < count; index++) + { + stream = (TSMF_STREAM*)ArrayList_GetItem(presentation->stream_list, index); + ret &= tsmf_stream_restart(stream); + } + + ArrayList_Unlock(presentation->stream_list); + return ret; +} + +BOOL tsmf_presentation_start(TSMF_PRESENTATION* presentation) +{ + UINT32 index; + UINT32 count; + TSMF_STREAM* stream; + BOOL ret = TRUE; + ArrayList_Lock(presentation->stream_list); + count = ArrayList_Count(presentation->stream_list); + + for (index = 0; index < count; index++) + { + stream = (TSMF_STREAM*)ArrayList_GetItem(presentation->stream_list, index); + ret &= tsmf_stream_start(stream); + } + + ArrayList_Unlock(presentation->stream_list); + return ret; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT tsmf_presentation_sync(TSMF_PRESENTATION* presentation) +{ + UINT32 index; + UINT32 count; + UINT error; + ArrayList_Lock(presentation->stream_list); + count = ArrayList_Count(presentation->stream_list); + + for (index = 0; index < count; index++) + { + TSMF_STREAM* stream = (TSMF_STREAM*)ArrayList_GetItem(presentation->stream_list, index); + + if (WaitForSingleObject(stream->ready, 500) == WAIT_FAILED) + { + error = GetLastError(); + WLog_ERR(TAG, "WaitForSingleObject failed with error %" PRIu32 "!", error); + return error; + } + } + + ArrayList_Unlock(presentation->stream_list); + return CHANNEL_RC_OK; +} + +BOOL tsmf_presentation_stop(TSMF_PRESENTATION* presentation) +{ + UINT32 index; + UINT32 count; + TSMF_STREAM* stream; + BOOL ret = TRUE; + ArrayList_Lock(presentation->stream_list); + count = ArrayList_Count(presentation->stream_list); + + for (index = 0; index < count; index++) + { + stream = (TSMF_STREAM*)ArrayList_GetItem(presentation->stream_list, index); + ret &= tsmf_stream_stop(stream); + } + + ArrayList_Unlock(presentation->stream_list); + presentation->audio_start_time = 0; + presentation->audio_end_time = 0; + return ret; +} + +BOOL tsmf_presentation_set_geometry_info(TSMF_PRESENTATION* presentation, UINT32 x, UINT32 y, + UINT32 width, UINT32 height, int num_rects, + RDP_RECT* rects) +{ + UINT32 index; + UINT32 count; + TSMF_STREAM* stream; + void* tmp_rects = NULL; + BOOL ret = TRUE; + + /* The server may send messages with invalid width / height. + * Ignore those messages. */ + if (!width || !height) + return TRUE; + + /* Streams can be added/removed from the presentation and the server will resend geometry info + * when a new stream is added to the presentation. Also, num_rects is used to indicate whether + * or not the window is visible. So, always process a valid message with unchanged position/size + * and/or no visibility rects. + */ + presentation->x = x; + presentation->y = y; + presentation->width = width; + presentation->height = height; + tmp_rects = realloc(presentation->rects, sizeof(RDP_RECT) * num_rects); + + if (!tmp_rects && num_rects) + return FALSE; + + presentation->nr_rects = num_rects; + presentation->rects = tmp_rects; + if (presentation->rects) + CopyMemory(presentation->rects, rects, sizeof(RDP_RECT) * num_rects); + ArrayList_Lock(presentation->stream_list); + count = ArrayList_Count(presentation->stream_list); + + for (index = 0; index < count; index++) + { + stream = (TSMF_STREAM*)ArrayList_GetItem(presentation->stream_list, index); + + if (!stream->decoder) + continue; + + if (stream->decoder->UpdateRenderingArea) + { + ret = stream->decoder->UpdateRenderingArea(stream->decoder, x, y, width, height, + num_rects, rects); + } + } + + ArrayList_Unlock(presentation->stream_list); + return ret; +} + +void tsmf_presentation_set_audio_device(TSMF_PRESENTATION* presentation, const char* name, + const char* device) +{ + presentation->audio_name = name; + presentation->audio_device = device; +} + +BOOL tsmf_stream_flush(TSMF_STREAM* stream) +{ + BOOL ret = TRUE; + + // TSMF_SAMPLE* sample; + /* TODO: free lists */ + if (stream->audio) + ret = stream->audio->Flush(stream->audio); + + stream->eos = 0; + stream->eos_message_id = 0; + stream->eos_channel_callback = NULL; + stream->delayed_stop = 0; + stream->last_end_time = 0; + stream->next_start_time = 0; + + if (stream->major_type == TSMF_MAJOR_TYPE_AUDIO) + { + stream->presentation->audio_start_time = 0; + stream->presentation->audio_end_time = 0; + } + + return TRUE; +} + +void _tsmf_presentation_free(void* obj) +{ + TSMF_PRESENTATION* presentation = (TSMF_PRESENTATION*)obj; + + if (presentation) + { + tsmf_presentation_stop(presentation); + ArrayList_Clear(presentation->stream_list); + ArrayList_Free(presentation->stream_list); + free(presentation->rects); + ZeroMemory(presentation, sizeof(TSMF_PRESENTATION)); + free(presentation); + } +} + +void tsmf_presentation_free(TSMF_PRESENTATION* presentation) +{ + ArrayList_Remove(presentation_list, presentation); +} + +TSMF_STREAM* tsmf_stream_new(TSMF_PRESENTATION* presentation, UINT32 stream_id, + rdpContext* rdpcontext) +{ + TSMF_STREAM* stream; + stream = tsmf_stream_find_by_id(presentation, stream_id); + + if (stream) + { + WLog_ERR(TAG, "duplicated stream id %" PRIu32 "!", stream_id); + return NULL; + } + + stream = (TSMF_STREAM*)calloc(1, sizeof(TSMF_STREAM)); + + if (!stream) + { + WLog_ERR(TAG, "Calloc failed"); + return NULL; + } + + stream->minBufferLevel = VIDEO_MIN_BUFFER_LEVEL; + stream->maxBufferLevel = VIDEO_MAX_BUFFER_LEVEL; + stream->currentBufferLevel = 1; + stream->seeking = FALSE; + stream->eos = 0; + stream->eos_message_id = 0; + stream->eos_channel_callback = NULL; + stream->stream_id = stream_id; + stream->presentation = presentation; + stream->stopEvent = CreateEvent(NULL, TRUE, FALSE, NULL); + + if (!stream->stopEvent) + goto error_stopEvent; + + stream->ready = CreateEvent(NULL, TRUE, TRUE, NULL); + + if (!stream->ready) + goto error_ready; + + stream->sample_list = Queue_New(TRUE, -1, -1); + + if (!stream->sample_list) + goto error_sample_list; + + stream->sample_list->object.fnObjectFree = tsmf_sample_free; + stream->sample_ack_list = Queue_New(TRUE, -1, -1); + + if (!stream->sample_ack_list) + goto error_sample_ack_list; + + stream->sample_ack_list->object.fnObjectFree = tsmf_sample_free; + stream->play_thread = + CreateThread(NULL, 0, tsmf_stream_playback_func, stream, CREATE_SUSPENDED, NULL); + + if (!stream->play_thread) + goto error_play_thread; + + stream->ack_thread = + CreateThread(NULL, 0, tsmf_stream_ack_func, stream, CREATE_SUSPENDED, NULL); + + if (!stream->ack_thread) + goto error_ack_thread; + + if (ArrayList_Add(presentation->stream_list, stream) < 0) + goto error_add; + + stream->rdpcontext = rdpcontext; + return stream; +error_add: + SetEvent(stream->stopEvent); + + if (WaitForSingleObject(stream->ack_thread, INFINITE) == WAIT_FAILED) + WLog_ERR(TAG, "WaitForSingleObject failed with error %" PRIu32 "!", GetLastError()); + +error_ack_thread: + SetEvent(stream->stopEvent); + + if (WaitForSingleObject(stream->play_thread, INFINITE) == WAIT_FAILED) + WLog_ERR(TAG, "WaitForSingleObject failed with error %" PRIu32 "!", GetLastError()); + +error_play_thread: + Queue_Free(stream->sample_ack_list); +error_sample_ack_list: + Queue_Free(stream->sample_list); +error_sample_list: + CloseHandle(stream->ready); +error_ready: + CloseHandle(stream->stopEvent); +error_stopEvent: + free(stream); + return NULL; +} + +void tsmf_stream_start_threads(TSMF_STREAM* stream) +{ + ResumeThread(stream->play_thread); + ResumeThread(stream->ack_thread); +} + +TSMF_STREAM* tsmf_stream_find_by_id(TSMF_PRESENTATION* presentation, UINT32 stream_id) +{ + UINT32 index; + UINT32 count; + BOOL found = FALSE; + TSMF_STREAM* stream; + ArrayList_Lock(presentation->stream_list); + count = ArrayList_Count(presentation->stream_list); + + for (index = 0; index < count; index++) + { + stream = (TSMF_STREAM*)ArrayList_GetItem(presentation->stream_list, index); + + if (stream->stream_id == stream_id) + { + found = TRUE; + break; + } + } + + ArrayList_Unlock(presentation->stream_list); + return (found) ? stream : NULL; +} + +static void tsmf_stream_resync(void* arg) +{ + TSMF_STREAM* stream = arg; + ResetEvent(stream->ready); +} + +BOOL tsmf_stream_set_format(TSMF_STREAM* stream, const char* name, wStream* s) +{ + TS_AM_MEDIA_TYPE mediatype; + BOOL ret = TRUE; + + if (stream->decoder) + { + WLog_ERR(TAG, "duplicated call"); + return FALSE; + } + + if (!tsmf_codec_parse_media_type(&mediatype, s)) + { + WLog_ERR(TAG, "unable to parse media type"); + return FALSE; + } + + if (mediatype.MajorType == TSMF_MAJOR_TYPE_VIDEO) + { + DEBUG_TSMF("video width %" PRIu32 " height %" PRIu32 " bit_rate %" PRIu32 + " frame_rate %f codec_data %" PRIu32 "", + mediatype.Width, mediatype.Height, mediatype.BitRate, + (double)mediatype.SamplesPerSecond.Numerator / + (double)mediatype.SamplesPerSecond.Denominator, + mediatype.ExtraDataSize); + stream->minBufferLevel = VIDEO_MIN_BUFFER_LEVEL; + stream->maxBufferLevel = VIDEO_MAX_BUFFER_LEVEL; + } + else if (mediatype.MajorType == TSMF_MAJOR_TYPE_AUDIO) + { + DEBUG_TSMF("audio channel %" PRIu32 " sample_rate %" PRIu32 " bits_per_sample %" PRIu32 + " codec_data %" PRIu32 "", + mediatype.Channels, mediatype.SamplesPerSecond.Numerator, + mediatype.BitsPerSample, mediatype.ExtraDataSize); + stream->sample_rate = mediatype.SamplesPerSecond.Numerator; + stream->channels = mediatype.Channels; + stream->bits_per_sample = mediatype.BitsPerSample; + + if (stream->bits_per_sample == 0) + stream->bits_per_sample = 16; + + stream->minBufferLevel = AUDIO_MIN_BUFFER_LEVEL; + stream->maxBufferLevel = AUDIO_MAX_BUFFER_LEVEL; + } + + stream->major_type = mediatype.MajorType; + stream->width = mediatype.Width; + stream->height = mediatype.Height; + stream->decoder = tsmf_load_decoder(name, &mediatype); + ret &= tsmf_stream_change_volume(stream, stream->presentation->volume, + stream->presentation->muted); + + if (!stream->decoder) + return FALSE; + + if (stream->decoder->SetAckFunc) + ret &= stream->decoder->SetAckFunc(stream->decoder, tsmf_stream_process_ack, stream); + + if (stream->decoder->SetSyncFunc) + ret &= stream->decoder->SetSyncFunc(stream->decoder, tsmf_stream_resync, stream); + + return ret; +} + +void tsmf_stream_end(TSMF_STREAM* stream, UINT32 message_id, + IWTSVirtualChannelCallback* pChannelCallback) +{ + if (!stream) + return; + + stream->eos = 1; + stream->eos_message_id = message_id; + stream->eos_channel_callback = pChannelCallback; +} + +void _tsmf_stream_free(void* obj) +{ + TSMF_STREAM* stream = (TSMF_STREAM*)obj; + + if (!stream) + return; + + tsmf_stream_stop(stream); + SetEvent(stream->stopEvent); + + if (stream->play_thread) + { + if (WaitForSingleObject(stream->play_thread, INFINITE) == WAIT_FAILED) + { + WLog_ERR(TAG, "WaitForSingleObject failed with error %" PRIu32 "!", GetLastError()); + return; + } + + CloseHandle(stream->play_thread); + stream->play_thread = NULL; + } + + if (stream->ack_thread) + { + if (WaitForSingleObject(stream->ack_thread, INFINITE) == WAIT_FAILED) + { + WLog_ERR(TAG, "WaitForSingleObject failed with error %" PRIu32 "!", GetLastError()); + return; + } + + CloseHandle(stream->ack_thread); + stream->ack_thread = NULL; + } + + Queue_Free(stream->sample_list); + Queue_Free(stream->sample_ack_list); + + if (stream->decoder && stream->decoder->Free) + { + stream->decoder->Free(stream->decoder); + stream->decoder = NULL; + } + + CloseHandle(stream->stopEvent); + CloseHandle(stream->ready); + ZeroMemory(stream, sizeof(TSMF_STREAM)); + free(stream); +} + +void tsmf_stream_free(TSMF_STREAM* stream) +{ + TSMF_PRESENTATION* presentation = stream->presentation; + ArrayList_Remove(presentation->stream_list, stream); +} + +BOOL tsmf_stream_push_sample(TSMF_STREAM* stream, IWTSVirtualChannelCallback* pChannelCallback, + UINT32 sample_id, UINT64 start_time, UINT64 end_time, UINT64 duration, + UINT32 extensions, UINT32 data_size, BYTE* data) +{ + TSMF_SAMPLE* sample; + SetEvent(stream->ready); + + if (TERMINATING) + return TRUE; + + sample = (TSMF_SAMPLE*)calloc(1, sizeof(TSMF_SAMPLE)); + + if (!sample) + { + WLog_ERR(TAG, "calloc sample failed!"); + return FALSE; + } + + sample->sample_id = sample_id; + sample->start_time = start_time; + sample->end_time = end_time; + sample->duration = duration; + sample->extensions = extensions; + + if ((sample->extensions & 0x00000080) || (sample->extensions & 0x00000040)) + sample->invalidTimestamps = TRUE; + else + sample->invalidTimestamps = FALSE; + + sample->stream = stream; + sample->channel_callback = pChannelCallback; + sample->data_size = data_size; + sample->data = calloc(1, data_size + TSMF_BUFFER_PADDING_SIZE); + + if (!sample->data) + { + WLog_ERR(TAG, "calloc sample->data failed!"); + free(sample); + return FALSE; + } + + CopyMemory(sample->data, data, data_size); + return Queue_Enqueue(stream->sample_list, sample); +} + +#ifndef _WIN32 + +static void tsmf_signal_handler(int s) +{ + TERMINATING = 1; + ArrayList_Free(presentation_list); + + if (s == SIGINT) + { + signal(s, SIG_DFL); + kill(getpid(), s); + } + else if (s == SIGUSR1) + { + signal(s, SIG_DFL); + } +} + +#endif + +BOOL tsmf_media_init(void) +{ +#ifndef _WIN32 + struct sigaction sigtrap; + sigtrap.sa_handler = tsmf_signal_handler; + sigemptyset(&sigtrap.sa_mask); + sigtrap.sa_flags = 0; + sigaction(SIGINT, &sigtrap, 0); + sigaction(SIGUSR1, &sigtrap, 0); +#endif + + if (!presentation_list) + { + presentation_list = ArrayList_New(TRUE); + + if (!presentation_list) + return FALSE; + + ArrayList_Object(presentation_list)->fnObjectFree = _tsmf_presentation_free; + } + + return TRUE; +} diff --git a/channels/tsmf/client/tsmf_media.h b/channels/tsmf/client/tsmf_media.h new file mode 100644 index 0000000..ade06da --- /dev/null +++ b/channels/tsmf/client/tsmf_media.h @@ -0,0 +1,72 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Video Redirection Virtual Channel - Media Container + * + * Copyright 2010-2011 Vic Lee + * Copyright 2012 Hewlett-Packard Development Company, L.P. + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger + * + * Licensed 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. + */ + +/** + * The media container maintains a global list of presentations, and a list of + * streams in each presentation. + */ + +#ifndef FREERDP_CHANNEL_TSMF_CLIENT_MEDIA_H +#define FREERDP_CHANNEL_TSMF_CLIENT_MEDIA_H + +#include + +typedef struct _TSMF_PRESENTATION TSMF_PRESENTATION; + +typedef struct _TSMF_STREAM TSMF_STREAM; + +typedef struct _TSMF_SAMPLE TSMF_SAMPLE; + +TSMF_PRESENTATION* tsmf_presentation_new(const BYTE* guid, + IWTSVirtualChannelCallback* pChannelCallback); +TSMF_PRESENTATION* tsmf_presentation_find_by_id(const BYTE* guid); +BOOL tsmf_presentation_start(TSMF_PRESENTATION* presentation); +BOOL tsmf_presentation_stop(TSMF_PRESENTATION* presentation); +UINT tsmf_presentation_sync(TSMF_PRESENTATION* presentation); +BOOL tsmf_presentation_paused(TSMF_PRESENTATION* presentation); +BOOL tsmf_presentation_restarted(TSMF_PRESENTATION* presentation); +BOOL tsmf_presentation_volume_changed(TSMF_PRESENTATION* presentation, UINT32 newVolume, + UINT32 muted); +BOOL tsmf_presentation_set_geometry_info(TSMF_PRESENTATION* presentation, UINT32 x, UINT32 y, + UINT32 width, UINT32 height, int num_rects, + RDP_RECT* rects); +void tsmf_presentation_set_audio_device(TSMF_PRESENTATION* presentation, const char* name, + const char* device); +void tsmf_presentation_free(TSMF_PRESENTATION* presentation); + +TSMF_STREAM* tsmf_stream_new(TSMF_PRESENTATION* presentation, UINT32 stream_id, + rdpContext* rdpcontext); +TSMF_STREAM* tsmf_stream_find_by_id(TSMF_PRESENTATION* presentation, UINT32 stream_id); +BOOL tsmf_stream_set_format(TSMF_STREAM* stream, const char* name, wStream* s); +void tsmf_stream_end(TSMF_STREAM* stream, UINT32 message_id, + IWTSVirtualChannelCallback* pChannelCallback); +void tsmf_stream_free(TSMF_STREAM* stream); +BOOL tsmf_stream_flush(TSMF_STREAM* stream); + +BOOL tsmf_stream_push_sample(TSMF_STREAM* stream, IWTSVirtualChannelCallback* pChannelCallback, + UINT32 sample_id, UINT64 start_time, UINT64 end_time, UINT64 duration, + UINT32 extensions, UINT32 data_size, BYTE* data); + +BOOL tsmf_media_init(void); +void tsmf_stream_start_threads(TSMF_STREAM* stream); + +#endif /* FREERDP_CHANNEL_TSMF_CLIENT_MEDIA_H */ diff --git a/channels/tsmf/client/tsmf_types.h b/channels/tsmf/client/tsmf_types.h new file mode 100644 index 0000000..7e3823d --- /dev/null +++ b/channels/tsmf/client/tsmf_types.h @@ -0,0 +1,63 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Video Redirection Virtual Channel - Types + * + * Copyright 2010-2011 Vic Lee + * + * Licensed 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 FREERDP_CHANNEL_TSMF_CLIENT_TYPES_H +#define FREERDP_CHANNEL_TSMF_CLIENT_TYPES_H + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include + +#define TAG CHANNELS_TAG("tsmf.client") + +#ifdef WITH_DEBUG_TSMF +#define DEBUG_TSMF(...) WLog_DBG(TAG, __VA_ARGS__) +#else +#define DEBUG_TSMF(...) \ + do \ + { \ + } while (0) +#endif + +typedef struct _TS_AM_MEDIA_TYPE +{ + int MajorType; + int SubType; + int FormatType; + + UINT32 Width; + UINT32 Height; + UINT32 BitRate; + struct + { + UINT32 Numerator; + UINT32 Denominator; + } SamplesPerSecond; + UINT32 Channels; + UINT32 BitsPerSample; + UINT32 BlockAlign; + const BYTE* ExtraData; + UINT32 ExtraDataSize; +} TS_AM_MEDIA_TYPE; + +#endif /* FREERDP_CHANNEL_TSMF_CLIENT_TYPES_H */ diff --git a/channels/urbdrc/CMakeLists.txt b/channels/urbdrc/CMakeLists.txt new file mode 100644 index 0000000..bc570e0 --- /dev/null +++ b/channels/urbdrc/CMakeLists.txt @@ -0,0 +1,30 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2012 Marc-Andre Moreau +# +# Licensed 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. + +define_channel("urbdrc") + +include_directories(common) +add_subdirectory(common) + +if(WITH_CLIENT_CHANNELS) + option(WITH_DEBUG_URBDRC "Dump data send/received in URBDRC channel" OFF) + + find_package(libusb-1.0 REQUIRED) + include_directories(${LIBUSB_1_INCLUDE_DIRS}) + + add_channel_client(${MODULE_PREFIX} ${CHANNEL_NAME}) +endif() diff --git a/channels/urbdrc/ChannelOptions.cmake b/channels/urbdrc/ChannelOptions.cmake new file mode 100644 index 0000000..770ba5e --- /dev/null +++ b/channels/urbdrc/ChannelOptions.cmake @@ -0,0 +1,18 @@ + +if (IOS OR ANDROID) + set(OPTION_DEFAULT OFF) + set(OPTION_CLIENT_DEFAULT OFF) + set(OPTION_SERVER_DEFAULT OFF) +else() + set(OPTION_DEFAULT ON) + set(OPTION_CLIENT_DEFAULT ON) + set(OPTION_SERVER_DEFAULT OFF) +endif() + +define_channel_options(NAME "urbdrc" TYPE "dynamic" + DESCRIPTION "USB Devices Virtual Channel Extension" + SPECIFICATIONS "[MS-RDPEUSB]" + DEFAULT ${OPTION_DEFAULT}) + +define_channel_client_options(${OPTION_CLIENT_DEFAULT}) +define_channel_server_options(${OPTION_SERVER_DEFAULT}) diff --git a/channels/urbdrc/client/CMakeLists.txt b/channels/urbdrc/client/CMakeLists.txt new file mode 100644 index 0000000..2d69618 --- /dev/null +++ b/channels/urbdrc/client/CMakeLists.txt @@ -0,0 +1,47 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2012 Atrust corp. +# Copyright 2012 Alfred Liu +# +# Licensed 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. + +define_channel_client("urbdrc") + +set(${MODULE_PREFIX}_SRCS + data_transfer.c + data_transfer.h + urbdrc_main.c + urbdrc_main.h + $ + ) + +add_channel_client_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} TRUE "DVCPluginEntry") + +set(${MODULE_PREFIX}_LIBS) +if (UDEV_FOUND AND UDEV_LIBRARIES) + set(${MODULE_PREFIX}_LIBS ${${MODULE_PREFIX}_LIBS} ${UDEV_LIBRARIES}) +endif() + +set(${MODULE_PREFIX}_LIBS ${${MODULE_PREFIX}_LIBS} winpr freerdp) + +target_link_libraries(${MODULE_NAME} ${PRIVATE_KEYWOARD} ${${MODULE_PREFIX}_LIBS}) + +if (WITH_DEBUG_SYMBOLS AND MSVC AND NOT BUILTIN_CHANNELS AND BUILD_SHARED_LIBS) + install(FILES ${CMAKE_BINARY_DIR}/${MODULE_NAME}.pdb DESTINATION ${FREERDP_ADDIN_PATH} COMPONENT symbols) +endif() + +set_property(TARGET ${MODULE_NAME} PROPERTY FOLDER "Channels/${CHANNEL_NAME}/Client") + +# libusb subsystem +add_channel_client_subsystem(${MODULE_PREFIX} ${CHANNEL_NAME} "libusb" "") diff --git a/channels/urbdrc/client/data_transfer.c b/channels/urbdrc/client/data_transfer.c new file mode 100644 index 0000000..6987961 --- /dev/null +++ b/channels/urbdrc/client/data_transfer.c @@ -0,0 +1,1856 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * RemoteFX USB Redirection + * + * Copyright 2012 Atrust corp. + * Copyright 2012 Alfred Liu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include + +#include + +#include + +#include "urbdrc_types.h" +#include "data_transfer.h" + +static void usb_process_get_port_status(IUDEVICE* pdev, wStream* out) +{ + int bcdUSB = pdev->query_device_descriptor(pdev, BCD_USB); + + switch (bcdUSB) + { + case USB_v1_0: + Stream_Write_UINT32(out, 0x303); + break; + + case USB_v1_1: + Stream_Write_UINT32(out, 0x103); + break; + + case USB_v2_0: + Stream_Write_UINT32(out, 0x503); + break; + + default: + Stream_Write_UINT32(out, 0x503); + break; + } +} + +static UINT urb_write_completion(IUDEVICE* pdev, URBDRC_CHANNEL_CALLBACK* callback, BOOL noAck, + wStream* out, UINT32 InterfaceId, UINT32 MessageId, + UINT32 RequestId, UINT32 usbd_status, UINT32 OutputBufferSize) +{ + if (!out) + return ERROR_INVALID_PARAMETER; + + if (Stream_Capacity(out) < OutputBufferSize + 36) + { + Stream_Free(out, TRUE); + return ERROR_INVALID_PARAMETER; + } + + Stream_SetPosition(out, 0); + Stream_Write_UINT32(out, InterfaceId); /** interface */ + Stream_Write_UINT32(out, MessageId); /** message id */ + + if (OutputBufferSize != 0) + Stream_Write_UINT32(out, URB_COMPLETION); + else + Stream_Write_UINT32(out, URB_COMPLETION_NO_DATA); + + Stream_Write_UINT32(out, RequestId); /** RequestId */ + Stream_Write_UINT32(out, 8); /** CbTsUrbResult */ + /** TsUrbResult TS_URB_RESULT_HEADER */ + Stream_Write_UINT16(out, 8); /** Size */ + Stream_Write_UINT16(out, 0); /* Padding */ + Stream_Write_UINT32(out, usbd_status); /** UsbdStatus */ + Stream_Write_UINT32(out, 0); /** HResult */ + Stream_Write_UINT32(out, OutputBufferSize); /** OutputBufferSize */ + Stream_Seek(out, OutputBufferSize); + + if (!noAck) + return stream_write_and_free(callback->plugin, callback->channel, out); + else + Stream_Free(out, TRUE); + + return ERROR_SUCCESS; +} + +static wStream* urb_create_iocompletion(UINT32 InterfaceField, UINT32 MessageId, UINT32 RequestId, + UINT32 OutputBufferSize) +{ + const UINT32 InterfaceId = (STREAM_ID_PROXY << 30) | (InterfaceField & 0x3FFFFFFF); + wStream* out = Stream_New(NULL, OutputBufferSize + 28); + + if (!out) + return NULL; + + Stream_Write_UINT32(out, InterfaceId); /** interface */ + Stream_Write_UINT32(out, MessageId); /** message id */ + Stream_Write_UINT32(out, IOCONTROL_COMPLETION); /** function id */ + Stream_Write_UINT32(out, RequestId); /** RequestId */ + Stream_Write_UINT32(out, USBD_STATUS_SUCCESS); /** HResult */ + Stream_Write_UINT32(out, OutputBufferSize); /** Information */ + Stream_Write_UINT32(out, OutputBufferSize); /** OutputBufferSize */ + return out; +} + +static UINT urbdrc_process_register_request_callback(IUDEVICE* pdev, + URBDRC_CHANNEL_CALLBACK* callback, wStream* s, + IUDEVMAN* udevman) +{ + UINT32 NumRequestCompletion = 0; + UINT32 RequestCompletion = 0; + URBDRC_PLUGIN* urbdrc; + + if (!callback || !s || !udevman || !pdev) + return ERROR_INVALID_PARAMETER; + + urbdrc = (URBDRC_PLUGIN*)callback->plugin; + + if (!urbdrc) + return ERROR_INVALID_PARAMETER; + + WLog_Print(urbdrc->log, WLOG_DEBUG, "urbdrc_process_register_request_callback"); + + if (Stream_GetRemainingLength(s) >= 8) + { + Stream_Read_UINT32(s, NumRequestCompletion); /** must be 1 */ + /** RequestCompletion: + * unique Request Completion interface for the client to use */ + Stream_Read_UINT32(s, RequestCompletion); + pdev->set_ReqCompletion(pdev, RequestCompletion); + } + else if (Stream_GetRemainingLength(s) >= 4) /** Unregister the device */ + { + Stream_Read_UINT32(s, RequestCompletion); + + if (pdev->get_ReqCompletion(pdev) == RequestCompletion) + pdev->setChannelClosed(pdev); + } + else + return ERROR_INVALID_DATA; + + return ERROR_SUCCESS; +} + +static UINT urbdrc_process_cancel_request(IUDEVICE* pdev, wStream* s, IUDEVMAN* udevman) +{ + UINT32 CancelId; + URBDRC_PLUGIN* urbdrc; + + if (!s || !udevman || !pdev) + return ERROR_INVALID_PARAMETER; + + urbdrc = (URBDRC_PLUGIN*)udevman->plugin; + + if (Stream_GetRemainingLength(s) < 4) + return ERROR_INVALID_DATA; + + Stream_Read_UINT32(s, CancelId); + WLog_Print(urbdrc->log, WLOG_DEBUG, "CANCEL_REQUEST: CancelId=%08" PRIx32 "", CancelId); + + if (pdev->cancel_transfer_request(pdev, CancelId) < 0) + return ERROR_INTERNAL_ERROR; + + return ERROR_SUCCESS; +} + +static UINT urbdrc_process_retract_device_request(IUDEVICE* pdev, wStream* s, IUDEVMAN* udevman) +{ + UINT32 Reason; + URBDRC_PLUGIN* urbdrc; + + if (!s || !udevman) + return ERROR_INVALID_PARAMETER; + + urbdrc = (URBDRC_PLUGIN*)udevman->plugin; + + if (!urbdrc) + return ERROR_INVALID_PARAMETER; + + if (Stream_GetRemainingLength(s) < 4) + return ERROR_INVALID_DATA; + + Stream_Read_UINT32(s, Reason); /** Reason */ + + switch (Reason) + { + case UsbRetractReason_BlockedByPolicy: + WLog_Print(urbdrc->log, WLOG_DEBUG, + "UsbRetractReason_BlockedByPolicy: now it is not support"); + return ERROR_ACCESS_DENIED; + + default: + WLog_Print(urbdrc->log, WLOG_DEBUG, + "urbdrc_process_retract_device_request: Unknown Reason %" PRIu32 "", Reason); + return ERROR_ACCESS_DENIED; + } + + return ERROR_SUCCESS; +} + +static UINT urbdrc_process_io_control(IUDEVICE* pdev, URBDRC_CHANNEL_CALLBACK* callback, wStream* s, + UINT32 MessageId, IUDEVMAN* udevman) +{ + UINT32 InterfaceId; + UINT32 IoControlCode; + UINT32 InputBufferSize; + UINT32 OutputBufferSize; + UINT32 RequestId; + UINT32 usbd_status = USBD_STATUS_SUCCESS; + wStream* out; + int success = 0; + URBDRC_PLUGIN* urbdrc; + + if (!callback || !s || !udevman || !pdev) + return ERROR_INVALID_PARAMETER; + + urbdrc = (URBDRC_PLUGIN*)callback->plugin; + + if (!urbdrc) + return ERROR_INVALID_PARAMETER; + + if (Stream_GetRemainingLength(s) < 8) + return ERROR_INVALID_DATA; + + Stream_Read_UINT32(s, IoControlCode); + Stream_Read_UINT32(s, InputBufferSize); + + if (!Stream_SafeSeek(s, InputBufferSize)) + return ERROR_INVALID_DATA; + if (Stream_GetRemainingLength(s) < 8ULL) + return ERROR_INVALID_DATA; + + Stream_Read_UINT32(s, OutputBufferSize); + Stream_Read_UINT32(s, RequestId); + InterfaceId = ((STREAM_ID_PROXY << 30) | pdev->get_ReqCompletion(pdev)); + out = urb_create_iocompletion(InterfaceId, MessageId, RequestId, OutputBufferSize + 4); + + if (!out) + return ERROR_OUTOFMEMORY; + + switch (IoControlCode) + { + case IOCTL_INTERNAL_USB_SUBMIT_URB: /** 0x00220003 */ + WLog_Print(urbdrc->log, WLOG_DEBUG, "ioctl: IOCTL_INTERNAL_USB_SUBMIT_URB"); + WLog_Print(urbdrc->log, WLOG_ERROR, + " Function IOCTL_INTERNAL_USB_SUBMIT_URB: Unchecked"); + break; + + case IOCTL_INTERNAL_USB_RESET_PORT: /** 0x00220007 */ + WLog_Print(urbdrc->log, WLOG_DEBUG, "ioctl: IOCTL_INTERNAL_USB_RESET_PORT"); + break; + + case IOCTL_INTERNAL_USB_GET_PORT_STATUS: /** 0x00220013 */ + WLog_Print(urbdrc->log, WLOG_DEBUG, "ioctl: IOCTL_INTERNAL_USB_GET_PORT_STATUS"); + success = pdev->query_device_port_status(pdev, &usbd_status, &OutputBufferSize, + Stream_Pointer(out)); + + if (success) + { + if (!Stream_SafeSeek(out, OutputBufferSize)) + { + Stream_Free(out, TRUE); + return ERROR_INVALID_DATA; + } + + if (pdev->isExist(pdev) == 0) + Stream_Write_UINT32(out, 0); + else + usb_process_get_port_status(pdev, out); + } + + break; + + case IOCTL_INTERNAL_USB_CYCLE_PORT: /** 0x0022001F */ + WLog_Print(urbdrc->log, WLOG_DEBUG, "ioctl: IOCTL_INTERNAL_USB_CYCLE_PORT"); + WLog_Print(urbdrc->log, WLOG_ERROR, + " Function IOCTL_INTERNAL_USB_CYCLE_PORT: Unchecked"); + break; + + case IOCTL_INTERNAL_USB_SUBMIT_IDLE_NOTIFICATION: /** 0x00220027 */ + WLog_Print(urbdrc->log, WLOG_DEBUG, + "ioctl: IOCTL_INTERNAL_USB_SUBMIT_IDLE_NOTIFICATION"); + WLog_Print(urbdrc->log, WLOG_ERROR, + " Function IOCTL_INTERNAL_USB_SUBMIT_IDLE_NOTIFICATION: Unchecked"); + break; + + default: + WLog_Print(urbdrc->log, WLOG_DEBUG, + "urbdrc_process_io_control: unknown IoControlCode 0x%" PRIX32 "", + IoControlCode); + Stream_Free(out, TRUE); + return ERROR_INVALID_OPERATION; + } + + return stream_write_and_free(callback->plugin, callback->channel, out); +} + +static UINT urbdrc_process_internal_io_control(IUDEVICE* pdev, URBDRC_CHANNEL_CALLBACK* callback, + wStream* s, UINT32 MessageId, IUDEVMAN* udevman) +{ + wStream* out; + UINT32 IoControlCode, InterfaceId, InputBufferSize; + UINT32 OutputBufferSize, RequestId, frames; + + if (!pdev || !callback || !s || !udevman) + return ERROR_INVALID_PARAMETER; + + if (Stream_GetRemainingLength(s) < 8) + return ERROR_INVALID_DATA; + + Stream_Read_UINT32(s, IoControlCode); + Stream_Read_UINT32(s, InputBufferSize); + + if (!Stream_SafeSeek(s, InputBufferSize)) + return ERROR_INVALID_DATA; + if (Stream_GetRemainingLength(s) < 8ULL) + return ERROR_INVALID_DATA; + Stream_Read_UINT32(s, OutputBufferSize); + Stream_Read_UINT32(s, RequestId); + InterfaceId = ((STREAM_ID_PROXY << 30) | pdev->get_ReqCompletion(pdev)); + // TODO: Implement control code. + /** Fixme: Currently this is a FALSE bustime... */ + frames = GetTickCount(); + out = urb_create_iocompletion(InterfaceId, MessageId, RequestId, 4); + + if (!out) + return ERROR_OUTOFMEMORY; + + Stream_Write_UINT32(out, frames); /** OutputBuffer */ + return stream_write_and_free(callback->plugin, callback->channel, out); +} + +static UINT urbdrc_process_query_device_text(IUDEVICE* pdev, URBDRC_CHANNEL_CALLBACK* callback, + wStream* s, UINT32 MessageId, IUDEVMAN* udevman) +{ + UINT32 out_size; + UINT32 TextType; + UINT32 LocaleId; + UINT32 InterfaceId; + UINT8 bufferSize = 0xFF; + UINT32 hr; + wStream* out; + BYTE DeviceDescription[0x100] = { 0 }; + + if (!pdev || !callback || !s || !udevman) + return ERROR_INVALID_PARAMETER; + if (Stream_GetRemainingLength(s) < 8) + return ERROR_INVALID_DATA; + + Stream_Read_UINT32(s, TextType); + Stream_Read_UINT32(s, LocaleId); + if (LocaleId > UINT16_MAX) + return ERROR_INVALID_DATA; + + hr = pdev->control_query_device_text(pdev, TextType, (UINT16)LocaleId, &bufferSize, + DeviceDescription); + InterfaceId = ((STREAM_ID_STUB << 30) | pdev->get_UsbDevice(pdev)); + out_size = 16 + bufferSize; + + if (bufferSize != 0) + out_size += 2; + + out = Stream_New(NULL, out_size); + + if (!out) + return ERROR_OUTOFMEMORY; + + Stream_Write_UINT32(out, InterfaceId); /** interface */ + Stream_Write_UINT32(out, MessageId); /** message id */ + Stream_Write_UINT32(out, bufferSize / 2); /** cchDeviceDescription in WCHAR */ + Stream_Write(out, DeviceDescription, bufferSize); /* '\0' terminated unicode */ + Stream_Write_UINT32(out, hr); /** HResult */ + return stream_write_and_free(callback->plugin, callback->channel, out); +} + +static void func_select_all_interface_for_msconfig(IUDEVICE* pdev, + MSUSB_CONFIG_DESCRIPTOR* MsConfig) +{ + UINT32 inum; + MSUSB_INTERFACE_DESCRIPTOR** MsInterfaces = MsConfig->MsInterfaces; + BYTE InterfaceNumber, AlternateSetting; + UINT32 NumInterfaces = MsConfig->NumInterfaces; + + for (inum = 0; inum < NumInterfaces; inum++) + { + InterfaceNumber = MsInterfaces[inum]->InterfaceNumber; + AlternateSetting = MsInterfaces[inum]->AlternateSetting; + pdev->select_interface(pdev, InterfaceNumber, AlternateSetting); + } +} + +static UINT urb_select_configuration(IUDEVICE* pdev, URBDRC_CHANNEL_CALLBACK* callback, wStream* s, + UINT32 RequestField, UINT32 MessageId, IUDEVMAN* udevman, + int transferDir) +{ + MSUSB_CONFIG_DESCRIPTOR* MsConfig = NULL; + size_t out_size; + UINT32 InterfaceId, NumInterfaces, usbd_status = 0; + BYTE ConfigurationDescriptorIsValid; + wStream* out; + int MsOutSize = 0; + URBDRC_PLUGIN* urbdrc; + const BOOL noAck = (RequestField & 0x80000000U) != 0; + const UINT32 RequestId = RequestField & 0x7FFFFFFF; + + if (!callback || !s || !udevman || !pdev) + return ERROR_INVALID_PARAMETER; + + urbdrc = (URBDRC_PLUGIN*)callback->plugin; + + if (!urbdrc) + return ERROR_INVALID_PARAMETER; + + if (transferDir == 0) + { + WLog_Print(urbdrc->log, WLOG_ERROR, "urb_select_configuration: unsupported transfer out"); + return ERROR_INVALID_PARAMETER; + } + + if (Stream_GetRemainingLength(s) < 8) + return ERROR_INVALID_DATA; + + InterfaceId = ((STREAM_ID_PROXY << 30) | pdev->get_ReqCompletion(pdev)); + Stream_Read_UINT8(s, ConfigurationDescriptorIsValid); + Stream_Seek(s, 3); /* Padding */ + Stream_Read_UINT32(s, NumInterfaces); + + /** if ConfigurationDescriptorIsValid is zero, then just do nothing.*/ + if (ConfigurationDescriptorIsValid) + { + /* parser data for struct config */ + MsConfig = msusb_msconfig_read(s, NumInterfaces); + + if (!MsConfig) + return ERROR_INVALID_DATA; + + /* select config */ + pdev->select_configuration(pdev, MsConfig->bConfigurationValue); + /* select all interface */ + func_select_all_interface_for_msconfig(pdev, MsConfig); + /* complete configuration setup */ + if (!pdev->complete_msconfig_setup(pdev, MsConfig)) + { + msusb_msconfig_free(MsConfig); + MsConfig = NULL; + } + } + + if (MsConfig) + MsOutSize = MsConfig->MsOutSize; + + if (MsOutSize > 0) + { + if ((size_t)MsOutSize > SIZE_MAX - 36) + return ERROR_INVALID_DATA; + + out_size = 36 + MsOutSize; + } + else + out_size = 44; + + out = Stream_New(NULL, out_size); + + if (!out) + return ERROR_OUTOFMEMORY; + + Stream_Write_UINT32(out, InterfaceId); /** interface */ + Stream_Write_UINT32(out, MessageId); /** message id */ + Stream_Write_UINT32(out, URB_COMPLETION_NO_DATA); /** function id */ + Stream_Write_UINT32(out, RequestId); /** RequestId */ + + if (MsOutSize > 0) + { + /** CbTsUrbResult */ + Stream_Write_UINT32(out, 8 + MsOutSize); + /** TS_URB_RESULT_HEADER Size*/ + Stream_Write_UINT16(out, 8 + MsOutSize); + } + else + { + Stream_Write_UINT32(out, 16); + Stream_Write_UINT16(out, 16); + } + + /** Padding, MUST be ignored upon receipt */ + Stream_Write_UINT16(out, TS_URB_SELECT_CONFIGURATION); + Stream_Write_UINT32(out, usbd_status); /** UsbdStatus */ + + /** TS_URB_SELECT_CONFIGURATION_RESULT */ + if (MsOutSize > 0) + msusb_msconfig_write(MsConfig, out); + else + { + Stream_Write_UINT32(out, 0); /** ConfigurationHandle */ + Stream_Write_UINT32(out, NumInterfaces); /** NumInterfaces */ + } + + Stream_Write_UINT32(out, 0); /** HResult */ + Stream_Write_UINT32(out, 0); /** OutputBufferSize */ + + if (!noAck) + return stream_write_and_free(callback->plugin, callback->channel, out); + else + Stream_Free(out, TRUE); + + return ERROR_SUCCESS; +} + +static UINT urb_select_interface(IUDEVICE* pdev, URBDRC_CHANNEL_CALLBACK* callback, wStream* s, + UINT32 RequestField, UINT32 MessageId, IUDEVMAN* udevman, + int transferDir) +{ + MSUSB_CONFIG_DESCRIPTOR* MsConfig; + MSUSB_INTERFACE_DESCRIPTOR* MsInterface; + UINT32 out_size, InterfaceId, ConfigurationHandle; + UINT32 OutputBufferSize; + BYTE InterfaceNumber; + wStream* out; + UINT32 interface_size; + URBDRC_PLUGIN* urbdrc; + const BOOL noAck = (RequestField & 0x80000000U) != 0; + const UINT32 RequestId = RequestField & 0x7FFFFFFF; + + if (!callback || !s || !udevman || !pdev) + return ERROR_INVALID_PARAMETER; + + urbdrc = (URBDRC_PLUGIN*)callback->plugin; + + if (!urbdrc) + return ERROR_INVALID_PARAMETER; + + if (transferDir == 0) + { + WLog_Print(urbdrc->log, WLOG_ERROR, "urb_select_interface: not support transfer out"); + return ERROR_INVALID_PARAMETER; + } + + if (Stream_GetRemainingLength(s) < 4) + return ERROR_INVALID_DATA; + + InterfaceId = ((STREAM_ID_PROXY << 30) | pdev->get_ReqCompletion(pdev)); + Stream_Read_UINT32(s, ConfigurationHandle); + MsInterface = msusb_msinterface_read(s); + + if ((Stream_GetRemainingLength(s) < 4) || !MsInterface) + return ERROR_INVALID_DATA; + + Stream_Read_UINT32(s, OutputBufferSize); + pdev->select_interface(pdev, MsInterface->InterfaceNumber, MsInterface->AlternateSetting); + /* replace device's MsInterface */ + MsConfig = pdev->get_MsConfig(pdev); + InterfaceNumber = MsInterface->InterfaceNumber; + if (!msusb_msinterface_replace(MsConfig, InterfaceNumber, MsInterface)) + { + msusb_msconfig_free(MsConfig); + return ERROR_BAD_CONFIGURATION; + } + /* complete configuration setup */ + if (!pdev->complete_msconfig_setup(pdev, MsConfig)) + { + msusb_msconfig_free(MsConfig); + return ERROR_BAD_CONFIGURATION; + } + MsInterface = MsConfig->MsInterfaces[InterfaceNumber]; + interface_size = 16 + (MsInterface->NumberOfPipes * 20); + out_size = 36 + interface_size; + out = Stream_New(NULL, out_size); + + if (!out) + return ERROR_OUTOFMEMORY; + + Stream_Write_UINT32(out, InterfaceId); /** interface */ + Stream_Write_UINT32(out, MessageId); /** message id */ + Stream_Write_UINT32(out, URB_COMPLETION_NO_DATA); /** function id */ + Stream_Write_UINT32(out, RequestId); /** RequestId */ + Stream_Write_UINT32(out, 8 + interface_size); /** CbTsUrbResult */ + /** TS_URB_RESULT_HEADER */ + Stream_Write_UINT16(out, 8 + interface_size); /** Size */ + /** Padding, MUST be ignored upon receipt */ + Stream_Write_UINT16(out, TS_URB_SELECT_INTERFACE); + Stream_Write_UINT32(out, USBD_STATUS_SUCCESS); /** UsbdStatus */ + /** TS_URB_SELECT_INTERFACE_RESULT */ + msusb_msinterface_write(MsInterface, out); + Stream_Write_UINT32(out, 0); /** HResult */ + Stream_Write_UINT32(out, 0); /** OutputBufferSize */ + + if (!noAck) + return stream_write_and_free(callback->plugin, callback->channel, out); + else + Stream_Free(out, TRUE); + + return ERROR_SUCCESS; +} + +static UINT urb_control_transfer(IUDEVICE* pdev, URBDRC_CHANNEL_CALLBACK* callback, wStream* s, + UINT32 RequestField, UINT32 MessageId, IUDEVMAN* udevman, + int transferDir, int External) +{ + UINT32 out_size, InterfaceId, EndpointAddress, PipeHandle; + UINT32 TransferFlags, OutputBufferSize, usbd_status, Timeout; + BYTE bmRequestType, Request; + UINT16 Value, Index, length; + BYTE* buffer; + wStream* out; + URBDRC_PLUGIN* urbdrc; + const BOOL noAck = (RequestField & 0x80000000U) != 0; + const UINT32 RequestId = RequestField & 0x7FFFFFFF; + + if (!callback || !s || !udevman || !pdev) + return ERROR_INVALID_PARAMETER; + + urbdrc = (URBDRC_PLUGIN*)callback->plugin; + + if (!urbdrc) + return ERROR_INVALID_PARAMETER; + + if (Stream_GetRemainingLength(s) < 8) + return ERROR_INVALID_DATA; + + InterfaceId = ((STREAM_ID_PROXY << 30) | pdev->get_ReqCompletion(pdev)); + Stream_Read_UINT32(s, PipeHandle); + Stream_Read_UINT32(s, TransferFlags); /** TransferFlags */ + EndpointAddress = (PipeHandle & 0x000000ff); + Timeout = 2000; + + switch (External) + { + case URB_CONTROL_TRANSFER_EXTERNAL: + if (Stream_GetRemainingLength(s) < 4) + return ERROR_INVALID_DATA; + + Stream_Read_UINT32(s, Timeout); /** TransferFlags */ + break; + + case URB_CONTROL_TRANSFER_NONEXTERNAL: + break; + } + + /** SetupPacket 8 bytes */ + if (Stream_GetRemainingLength(s) < 12) + return ERROR_INVALID_DATA; + + Stream_Read_UINT8(s, bmRequestType); + Stream_Read_UINT8(s, Request); + Stream_Read_UINT16(s, Value); + Stream_Read_UINT16(s, Index); + Stream_Read_UINT16(s, length); + Stream_Read_UINT32(s, OutputBufferSize); + + if (length != OutputBufferSize) + { + WLog_Print(urbdrc->log, WLOG_ERROR, "urb_control_transfer ERROR: buf != length"); + return ERROR_INVALID_DATA; + } + + out_size = 36 + OutputBufferSize; + out = Stream_New(NULL, out_size); + + if (!out) + return ERROR_OUTOFMEMORY; + + Stream_Seek(out, 36); + /** Get Buffer Data */ + buffer = Stream_Pointer(out); + + if (transferDir == USBD_TRANSFER_DIRECTION_OUT) + Stream_Copy(s, out, OutputBufferSize); + + /** process TS_URB_CONTROL_TRANSFER */ + if (!pdev->control_transfer(pdev, RequestId, EndpointAddress, TransferFlags, bmRequestType, + Request, Value, Index, &usbd_status, &OutputBufferSize, buffer, + Timeout)) + { + WLog_Print(urbdrc->log, WLOG_ERROR, "control_transfer failed"); + Stream_Free(out, TRUE); + return ERROR_INTERNAL_ERROR; + } + + return urb_write_completion(pdev, callback, noAck, out, InterfaceId, MessageId, RequestId, + usbd_status, OutputBufferSize); +} + +static void urb_bulk_transfer_cb(IUDEVICE* pdev, URBDRC_CHANNEL_CALLBACK* callback, wStream* out, + UINT32 InterfaceId, BOOL noAck, UINT32 MessageId, UINT32 RequestId, + UINT32 NumberOfPackets, UINT32 status, UINT32 StartFrame, + UINT32 ErrorCount, UINT32 OutputBufferSize) +{ + if (!pdev->isChannelClosed(pdev)) + urb_write_completion(pdev, callback, noAck, out, InterfaceId, MessageId, RequestId, status, + OutputBufferSize); + else + Stream_Free(out, TRUE); +} + +static UINT urb_bulk_or_interrupt_transfer(IUDEVICE* pdev, URBDRC_CHANNEL_CALLBACK* callback, + wStream* s, UINT32 RequestField, UINT32 MessageId, + IUDEVMAN* udevman, int transferDir) +{ + UINT32 EndpointAddress, PipeHandle; + UINT32 TransferFlags, OutputBufferSize; + const BOOL noAck = (RequestField & 0x80000000U) != 0; + const UINT32 RequestId = RequestField & 0x7FFFFFFF; + + if (!pdev || !callback || !s || !udevman) + return ERROR_INVALID_PARAMETER; + + if (Stream_GetRemainingLength(s) < 12) + return ERROR_INVALID_DATA; + + Stream_Read_UINT32(s, PipeHandle); + Stream_Read_UINT32(s, TransferFlags); /** TransferFlags */ + Stream_Read_UINT32(s, OutputBufferSize); + EndpointAddress = (PipeHandle & 0x000000ff); + /** process TS_URB_BULK_OR_INTERRUPT_TRANSFER */ + return pdev->bulk_or_interrupt_transfer( + pdev, callback, MessageId, RequestId, EndpointAddress, TransferFlags, noAck, + OutputBufferSize, (transferDir == USBD_TRANSFER_DIRECTION_OUT) ? Stream_Pointer(s) : NULL, + urb_bulk_transfer_cb, 10000); +} + +static void urb_isoch_transfer_cb(IUDEVICE* pdev, URBDRC_CHANNEL_CALLBACK* callback, wStream* out, + UINT32 InterfaceId, BOOL noAck, UINT32 MessageId, + UINT32 RequestId, UINT32 NumberOfPackets, UINT32 status, + UINT32 StartFrame, UINT32 ErrorCount, UINT32 OutputBufferSize) +{ + if (!noAck) + { + UINT32 packetSize = (status == 0) ? NumberOfPackets * 12 : 0; + Stream_SetPosition(out, 0); + /* fill the send data */ + Stream_Write_UINT32(out, InterfaceId); /** interface */ + Stream_Write_UINT32(out, MessageId); /** message id */ + + if (OutputBufferSize == 0) + Stream_Write_UINT32(out, URB_COMPLETION_NO_DATA); /** function id */ + else + Stream_Write_UINT32(out, URB_COMPLETION); /** function id */ + + Stream_Write_UINT32(out, RequestId); /** RequestId */ + Stream_Write_UINT32(out, 20 + packetSize); /** CbTsUrbResult */ + /** TsUrbResult TS_URB_RESULT_HEADER */ + Stream_Write_UINT16(out, 20 + packetSize); /** Size */ + Stream_Write_UINT16(out, 0); /* Padding */ + Stream_Write_UINT32(out, status); /** UsbdStatus */ + Stream_Write_UINT32(out, StartFrame); /** StartFrame */ + + if (status == 0) + { + /** NumberOfPackets */ + Stream_Write_UINT32(out, NumberOfPackets); + Stream_Write_UINT32(out, ErrorCount); /** ErrorCount */ + Stream_Seek(out, packetSize); + } + else + { + Stream_Write_UINT32(out, 0); /** NumberOfPackets */ + Stream_Write_UINT32(out, ErrorCount); /** ErrorCount */ + } + + Stream_Write_UINT32(out, 0); /** HResult */ + Stream_Write_UINT32(out, OutputBufferSize); /** OutputBufferSize */ + Stream_Seek(out, OutputBufferSize); + + stream_write_and_free(callback->plugin, callback->channel, out); + } +} + +static UINT urb_isoch_transfer(IUDEVICE* pdev, URBDRC_CHANNEL_CALLBACK* callback, wStream* s, + UINT32 RequestField, UINT32 MessageId, IUDEVMAN* udevman, + int transferDir) +{ + UINT32 EndpointAddress; + UINT32 PipeHandle, TransferFlags, StartFrame, NumberOfPackets; + UINT32 ErrorCount, OutputBufferSize; + BYTE* packetDescriptorData; + const BOOL noAck = (RequestField & 0x80000000U) != 0; + const UINT32 RequestId = RequestField & 0x7FFFFFFF; + + if (!pdev || !callback || !udevman) + return ERROR_INVALID_PARAMETER; + + if (Stream_GetRemainingLength(s) < 20) + return ERROR_INVALID_DATA; + + Stream_Read_UINT32(s, PipeHandle); + EndpointAddress = (PipeHandle & 0x000000ff); + Stream_Read_UINT32(s, TransferFlags); /** TransferFlags */ + Stream_Read_UINT32(s, StartFrame); /** StartFrame */ + Stream_Read_UINT32(s, NumberOfPackets); /** NumberOfPackets */ + Stream_Read_UINT32(s, ErrorCount); /** ErrorCount */ + + if (Stream_GetRemainingLength(s) < NumberOfPackets * 12 + 4) + return ERROR_INVALID_DATA; + + packetDescriptorData = Stream_Pointer(s); + Stream_Seek(s, NumberOfPackets * 12); + Stream_Read_UINT32(s, OutputBufferSize); + return pdev->isoch_transfer( + pdev, callback, MessageId, RequestId, EndpointAddress, TransferFlags, StartFrame, + ErrorCount, noAck, packetDescriptorData, NumberOfPackets, OutputBufferSize, + (transferDir == USBD_TRANSFER_DIRECTION_OUT) ? Stream_Pointer(s) : NULL, + urb_isoch_transfer_cb, 2000); +} + +static UINT urb_control_descriptor_request(IUDEVICE* pdev, URBDRC_CHANNEL_CALLBACK* callback, + wStream* s, UINT32 RequestField, UINT32 MessageId, + IUDEVMAN* udevman, BYTE func_recipient, int transferDir) +{ + size_t out_size; + UINT32 InterfaceId, OutputBufferSize, usbd_status; + BYTE bmRequestType, desc_index, desc_type; + UINT16 langId; + wStream* out; + URBDRC_PLUGIN* urbdrc; + const BOOL noAck = (RequestField & 0x80000000U) != 0; + const UINT32 RequestId = RequestField & 0x7FFFFFFF; + + if (!callback || !s || !udevman || !pdev) + return ERROR_INVALID_PARAMETER; + + urbdrc = (URBDRC_PLUGIN*)callback->plugin; + + if (!urbdrc) + return ERROR_INVALID_PARAMETER; + + if (Stream_GetRemainingLength(s) < 8) + return ERROR_INVALID_DATA; + + InterfaceId = ((STREAM_ID_PROXY << 30) | pdev->get_ReqCompletion(pdev)); + Stream_Read_UINT8(s, desc_index); + Stream_Read_UINT8(s, desc_type); + Stream_Read_UINT16(s, langId); + Stream_Read_UINT32(s, OutputBufferSize); + if (OutputBufferSize > UINT32_MAX - 36) + return ERROR_INVALID_DATA; + if (transferDir == USBD_TRANSFER_DIRECTION_OUT) + { + if (Stream_GetRemainingLength(s) < OutputBufferSize) + return ERROR_INVALID_DATA; + } + + out_size = 36ULL + OutputBufferSize; + out = Stream_New(NULL, out_size); + + if (!out) + return ERROR_OUTOFMEMORY; + + Stream_Seek(out, 36); + bmRequestType = func_recipient; + + switch (transferDir) + { + case USBD_TRANSFER_DIRECTION_IN: + bmRequestType |= 0x80; + break; + + case USBD_TRANSFER_DIRECTION_OUT: + bmRequestType |= 0x00; + Stream_Copy(s, out, OutputBufferSize); + Stream_Rewind(out, OutputBufferSize); + break; + + default: + WLog_Print(urbdrc->log, WLOG_DEBUG, "get error transferDir"); + OutputBufferSize = 0; + usbd_status = USBD_STATUS_STALL_PID; + break; + } + + /** process get usb device descriptor */ + if (!pdev->control_transfer(pdev, RequestId, 0, 0, bmRequestType, + 0x06, /* REQUEST_GET_DESCRIPTOR */ + (desc_type << 8) | desc_index, langId, &usbd_status, + &OutputBufferSize, Stream_Pointer(out), 1000)) + { + WLog_Print(urbdrc->log, WLOG_ERROR, "get_descriptor failed"); + Stream_Free(out, TRUE); + return ERROR_INTERNAL_ERROR; + } + + return urb_write_completion(pdev, callback, noAck, out, InterfaceId, MessageId, RequestId, + usbd_status, OutputBufferSize); +} + +static UINT urb_control_get_status_request(IUDEVICE* pdev, URBDRC_CHANNEL_CALLBACK* callback, + wStream* s, UINT32 RequestField, UINT32 MessageId, + IUDEVMAN* udevman, BYTE func_recipient, int transferDir) +{ + size_t out_size; + UINT32 InterfaceId, OutputBufferSize, usbd_status; + UINT16 Index; + BYTE bmRequestType; + wStream* out; + URBDRC_PLUGIN* urbdrc; + const BOOL noAck = (RequestField & 0x80000000U) != 0; + const UINT32 RequestId = RequestField & 0x7FFFFFFF; + + if (!callback || !s || !udevman || !pdev) + return ERROR_INVALID_PARAMETER; + + urbdrc = (URBDRC_PLUGIN*)callback->plugin; + + if (!urbdrc) + return ERROR_INVALID_PARAMETER; + + if (transferDir == 0) + { + WLog_Print(urbdrc->log, WLOG_DEBUG, + "urb_control_get_status_request: transfer out not supported"); + return ERROR_INVALID_PARAMETER; + } + + if (Stream_GetRemainingLength(s) < 8) + return ERROR_INVALID_DATA; + + InterfaceId = ((STREAM_ID_PROXY << 30) | pdev->get_ReqCompletion(pdev)); + Stream_Read_UINT16(s, Index); /** Index */ + Stream_Seek(s, 2); + Stream_Read_UINT32(s, OutputBufferSize); + if (OutputBufferSize > UINT32_MAX - 36) + return ERROR_INVALID_DATA; + out_size = 36ULL + OutputBufferSize; + out = Stream_New(NULL, out_size); + + if (!out) + return ERROR_OUTOFMEMORY; + + Stream_Seek(out, 36); + bmRequestType = func_recipient | 0x80; + + if (!pdev->control_transfer(pdev, RequestId, 0, 0, bmRequestType, 0x00, /* REQUEST_GET_STATUS */ + 0, Index, &usbd_status, &OutputBufferSize, Stream_Pointer(out), + 1000)) + { + WLog_Print(urbdrc->log, WLOG_ERROR, "control_transfer failed"); + Stream_Free(out, TRUE); + return ERROR_INTERNAL_ERROR; + } + + return urb_write_completion(pdev, callback, noAck, out, InterfaceId, MessageId, RequestId, + usbd_status, OutputBufferSize); +} + +static UINT urb_control_vendor_or_class_request(IUDEVICE* pdev, URBDRC_CHANNEL_CALLBACK* callback, + wStream* s, UINT32 RequestField, UINT32 MessageId, + IUDEVMAN* udevman, BYTE func_type, + BYTE func_recipient, int transferDir) +{ + UINT32 out_size, InterfaceId, TransferFlags, usbd_status; + UINT32 OutputBufferSize; + BYTE ReqTypeReservedBits, Request, bmRequestType; + UINT16 Value, Index, Padding; + wStream* out; + URBDRC_PLUGIN* urbdrc; + const BOOL noAck = (RequestField & 0x80000000U) != 0; + const UINT32 RequestId = RequestField & 0x7FFFFFFF; + + if (!callback || !s || !udevman || !pdev) + return ERROR_INVALID_PARAMETER; + + urbdrc = (URBDRC_PLUGIN*)callback->plugin; + + if (!urbdrc) + return ERROR_INVALID_PARAMETER; + + if (Stream_GetRemainingLength(s) < 16) + return ERROR_INVALID_DATA; + + InterfaceId = ((STREAM_ID_PROXY << 30) | pdev->get_ReqCompletion(pdev)); + Stream_Read_UINT32(s, TransferFlags); /** TransferFlags */ + Stream_Read_UINT8(s, ReqTypeReservedBits); /** ReqTypeReservedBids */ + Stream_Read_UINT8(s, Request); /** Request */ + Stream_Read_UINT16(s, Value); /** value */ + Stream_Read_UINT16(s, Index); /** index */ + Stream_Read_UINT16(s, Padding); /** Padding */ + Stream_Read_UINT32(s, OutputBufferSize); + if (OutputBufferSize > UINT32_MAX - 36) + return ERROR_INVALID_DATA; + + if (transferDir == USBD_TRANSFER_DIRECTION_OUT) + { + if (Stream_GetRemainingLength(s) < OutputBufferSize) + return ERROR_INVALID_DATA; + } + + out_size = 36ULL + OutputBufferSize; + out = Stream_New(NULL, out_size); + + if (!out) + return ERROR_OUTOFMEMORY; + + Stream_Seek(out, 36); + + /** Get Buffer */ + if (transferDir == USBD_TRANSFER_DIRECTION_OUT) + { + Stream_Copy(s, out, OutputBufferSize); + Stream_Rewind(out, OutputBufferSize); + } + + /** vendor or class command */ + bmRequestType = func_type | func_recipient; + + if (TransferFlags & USBD_TRANSFER_DIRECTION) + bmRequestType |= 0x80; + + WLog_Print(urbdrc->log, WLOG_DEBUG, + "RequestId 0x%" PRIx32 " TransferFlags: 0x%" PRIx32 " ReqTypeReservedBits: 0x%" PRIx8 + " " + "Request:0x%" PRIx8 " Value: 0x%" PRIx16 " Index: 0x%" PRIx16 + " OutputBufferSize: 0x%" PRIx32 " bmRequestType: 0x%" PRIx8, + RequestId, TransferFlags, ReqTypeReservedBits, Request, Value, Index, + OutputBufferSize, bmRequestType); + + if (!pdev->control_transfer(pdev, RequestId, 0, 0, bmRequestType, Request, Value, Index, + &usbd_status, &OutputBufferSize, Stream_Pointer(out), 2000)) + { + WLog_Print(urbdrc->log, WLOG_ERROR, "control_transfer failed"); + Stream_Free(out, TRUE); + return ERROR_INTERNAL_ERROR; + } + + return urb_write_completion(pdev, callback, noAck, out, InterfaceId, MessageId, RequestId, + usbd_status, OutputBufferSize); +} + +static UINT urb_os_feature_descriptor_request(IUDEVICE* pdev, URBDRC_CHANNEL_CALLBACK* callback, + wStream* s, UINT32 RequestField, UINT32 MessageId, + IUDEVMAN* udevman, int transferDir) +{ + size_t out_size; + UINT32 InterfaceId, OutputBufferSize, usbd_status; + BYTE Recipient, InterfaceNumber, Ms_PageIndex; + UINT16 Ms_featureDescIndex; + wStream* out; + int ret; + URBDRC_PLUGIN* urbdrc; + const BOOL noAck = (RequestField & 0x80000000U) != 0; + const UINT32 RequestId = RequestField & 0x7FFFFFFF; + + if (!callback || !s || !udevman || !pdev) + return ERROR_INVALID_PARAMETER; + + urbdrc = (URBDRC_PLUGIN*)callback->plugin; + + if (!urbdrc) + return ERROR_INVALID_PARAMETER; + + if (Stream_GetRemainingLength(s) < 12) + return ERROR_INVALID_DATA; + + /* 2.2.9.15 TS_URB_OS_FEATURE_DESCRIPTOR_REQUEST */ + Stream_Read_UINT8(s, Recipient); /** Recipient */ + Recipient = (Recipient & 0x1f); /* Mask out Padding1 */ + Stream_Read_UINT8(s, InterfaceNumber); /** InterfaceNumber */ + Stream_Read_UINT8(s, Ms_PageIndex); /** Ms_PageIndex */ + Stream_Read_UINT16(s, Ms_featureDescIndex); /** Ms_featureDescIndex */ + Stream_Seek(s, 3); /* Padding 2 */ + Stream_Read_UINT32(s, OutputBufferSize); + if (OutputBufferSize > UINT32_MAX - 36) + return ERROR_INVALID_DATA; + + switch (transferDir) + { + case USBD_TRANSFER_DIRECTION_OUT: + if (Stream_GetRemainingLength(s) < OutputBufferSize) + return ERROR_INVALID_DATA; + + break; + + default: + break; + } + + InterfaceId = ((STREAM_ID_PROXY << 30) | pdev->get_ReqCompletion(pdev)); + out_size = 36ULL + OutputBufferSize; + out = Stream_New(NULL, out_size); + + if (!out) + return ERROR_OUTOFMEMORY; + + Stream_Seek(out, 36); + + switch (transferDir) + { + case USBD_TRANSFER_DIRECTION_OUT: + Stream_Copy(s, out, OutputBufferSize); + Stream_Rewind(out, OutputBufferSize); + break; + + case USBD_TRANSFER_DIRECTION_IN: + break; + } + + WLog_Print(urbdrc->log, WLOG_DEBUG, + "Ms descriptor arg: Recipient:0x%" PRIx8 ", " + "InterfaceNumber:0x%" PRIx8 ", Ms_PageIndex:0x%" PRIx8 ", " + "Ms_featureDescIndex:0x%" PRIx16 ", OutputBufferSize:0x%" PRIx32 "", + Recipient, InterfaceNumber, Ms_PageIndex, Ms_featureDescIndex, OutputBufferSize); + /** get ms string */ + ret = pdev->os_feature_descriptor_request(pdev, RequestId, Recipient, InterfaceNumber, + Ms_PageIndex, Ms_featureDescIndex, &usbd_status, + &OutputBufferSize, Stream_Pointer(out), 1000); + + if (ret < 0) + WLog_Print(urbdrc->log, WLOG_DEBUG, "os_feature_descriptor_request: error num %d", ret); + + return urb_write_completion(pdev, callback, noAck, out, InterfaceId, MessageId, RequestId, + usbd_status, OutputBufferSize); +} + +static UINT urb_pipe_request(IUDEVICE* pdev, URBDRC_CHANNEL_CALLBACK* callback, wStream* s, + UINT32 RequestField, UINT32 MessageId, IUDEVMAN* udevman, + int transferDir, int action) +{ + UINT32 out_size, InterfaceId, PipeHandle, EndpointAddress; + UINT32 OutputBufferSize, usbd_status = 0; + wStream* out; + UINT32 ret = USBD_STATUS_REQUEST_FAILED; + int rc; + URBDRC_PLUGIN* urbdrc; + const BOOL noAck = (RequestField & 0x80000000U) != 0; + const UINT32 RequestId = RequestField & 0x7FFFFFFF; + + if (!callback || !s || !udevman || !pdev) + return ERROR_INVALID_PARAMETER; + + urbdrc = (URBDRC_PLUGIN*)callback->plugin; + + if (!urbdrc) + return ERROR_INVALID_PARAMETER; + + if (Stream_GetRemainingLength(s) < 8) + return ERROR_INVALID_DATA; + + if (transferDir == 0) + { + WLog_Print(urbdrc->log, WLOG_DEBUG, "urb_pipe_request: not support transfer out"); + return ERROR_INVALID_PARAMETER; + } + + InterfaceId = ((STREAM_ID_PROXY << 30) | pdev->get_ReqCompletion(pdev)); + Stream_Read_UINT32(s, PipeHandle); /** PipeHandle */ + Stream_Read_UINT32(s, OutputBufferSize); + EndpointAddress = (PipeHandle & 0x000000ff); + + switch (action) + { + case PIPE_CANCEL: + rc = pdev->control_pipe_request(pdev, RequestId, EndpointAddress, &usbd_status, + PIPE_CANCEL); + + if (rc < 0) + WLog_Print(urbdrc->log, WLOG_DEBUG, "PIPE SET HALT: error %d", ret); + else + ret = USBD_STATUS_SUCCESS; + + break; + + case PIPE_RESET: + WLog_Print(urbdrc->log, WLOG_DEBUG, "urb_pipe_request: PIPE_RESET ep 0x%" PRIx32 "", + EndpointAddress); + rc = pdev->control_pipe_request(pdev, RequestId, EndpointAddress, &usbd_status, + PIPE_RESET); + + if (rc < 0) + WLog_Print(urbdrc->log, WLOG_DEBUG, "PIPE RESET: error %d", ret); + else + ret = USBD_STATUS_SUCCESS; + + break; + + default: + WLog_Print(urbdrc->log, WLOG_DEBUG, "urb_pipe_request action: %d not supported", + action); + ret = USBD_STATUS_INVALID_URB_FUNCTION; + break; + } + + /** send data */ + out_size = 36; + out = Stream_New(NULL, out_size); + + if (!out) + return ERROR_OUTOFMEMORY; + + return urb_write_completion(pdev, callback, noAck, out, InterfaceId, MessageId, RequestId, ret, + 0); +} + +static UINT urb_get_current_frame_number(IUDEVICE* pdev, URBDRC_CHANNEL_CALLBACK* callback, + wStream* s, UINT32 RequestField, UINT32 MessageId, + IUDEVMAN* udevman, int transferDir) +{ + UINT32 out_size, InterfaceId, OutputBufferSize; + UINT32 dummy_frames; + wStream* out; + URBDRC_PLUGIN* urbdrc; + const BOOL noAck = (RequestField & 0x80000000U) != 0; + const UINT32 RequestId = RequestField & 0x7FFFFFFF; + + if (!callback || !s || !udevman || !pdev) + return ERROR_INVALID_PARAMETER; + + urbdrc = (URBDRC_PLUGIN*)callback->plugin; + + if (!urbdrc) + return ERROR_INVALID_PARAMETER; + + if (Stream_GetRemainingLength(s) < 4) + return ERROR_INVALID_DATA; + + if (transferDir == 0) + { + WLog_Print(urbdrc->log, WLOG_DEBUG, + "urb_get_current_frame_number: not support transfer out"); + return ERROR_INVALID_PARAMETER; + } + + InterfaceId = ((STREAM_ID_PROXY << 30) | pdev->get_ReqCompletion(pdev)); + Stream_Read_UINT32(s, OutputBufferSize); + /** Fixme: Need to fill actual frame number!!*/ + dummy_frames = GetTickCount(); + out_size = 40; + out = Stream_New(NULL, out_size); + + if (!out) + return ERROR_OUTOFMEMORY; + + Stream_Write_UINT32(out, InterfaceId); /** interface */ + Stream_Write_UINT32(out, MessageId); /** message id */ + Stream_Write_UINT32(out, URB_COMPLETION_NO_DATA); + Stream_Write_UINT32(out, RequestId); /** RequestId */ + Stream_Write_UINT32(out, 12); /** CbTsUrbResult */ + /** TsUrbResult TS_URB_RESULT_HEADER */ + Stream_Write_UINT16(out, 12); /** Size */ + /** Padding, MUST be ignored upon receipt */ + Stream_Write_UINT16(out, TS_URB_GET_CURRENT_FRAME_NUMBER); + Stream_Write_UINT32(out, USBD_STATUS_SUCCESS); /** UsbdStatus */ + Stream_Write_UINT32(out, dummy_frames); /** FrameNumber */ + Stream_Write_UINT32(out, 0); /** HResult */ + Stream_Write_UINT32(out, 0); /** OutputBufferSize */ + + if (!noAck) + return stream_write_and_free(callback->plugin, callback->channel, out); + else + Stream_Free(out, TRUE); + + return ERROR_SUCCESS; +} + +/* Unused function for current server */ +static UINT urb_control_get_configuration_request(IUDEVICE* pdev, URBDRC_CHANNEL_CALLBACK* callback, + wStream* s, UINT32 RequestField, UINT32 MessageId, + IUDEVMAN* udevman, int transferDir) +{ + size_t out_size; + UINT32 InterfaceId, OutputBufferSize, usbd_status; + wStream* out; + URBDRC_PLUGIN* urbdrc; + const BOOL noAck = (RequestField & 0x80000000U) != 0; + const UINT32 RequestId = RequestField & 0x7FFFFFFF; + + if (!callback || !s || !udevman || !pdev) + return ERROR_INVALID_PARAMETER; + + urbdrc = (URBDRC_PLUGIN*)callback->plugin; + + if (!urbdrc) + return ERROR_INVALID_PARAMETER; + + if (transferDir == 0) + { + WLog_Print(urbdrc->log, WLOG_DEBUG, + "urb_control_get_configuration_request:" + " not support transfer out"); + return ERROR_INVALID_PARAMETER; + } + + if (Stream_GetRemainingLength(s) < 4) + return ERROR_INVALID_DATA; + + Stream_Read_UINT32(s, OutputBufferSize); + if (OutputBufferSize > UINT32_MAX - 36) + return ERROR_INVALID_DATA; + out_size = 36ULL + OutputBufferSize; + out = Stream_New(NULL, out_size); + + if (!out) + return ERROR_OUTOFMEMORY; + + Stream_Seek(out, 36); + InterfaceId = ((STREAM_ID_PROXY << 30) | pdev->get_ReqCompletion(pdev)); + + if (!pdev->control_transfer(pdev, RequestId, 0, 0, 0x80 | 0x00, + 0x08, /* REQUEST_GET_CONFIGURATION */ + 0, 0, &usbd_status, &OutputBufferSize, Stream_Pointer(out), 1000)) + { + WLog_Print(urbdrc->log, WLOG_DEBUG, "control_transfer failed"); + Stream_Free(out, TRUE); + return ERROR_INTERNAL_ERROR; + } + + return urb_write_completion(pdev, callback, noAck, out, InterfaceId, MessageId, RequestId, + usbd_status, OutputBufferSize); +} + +/* Unused function for current server */ +static UINT urb_control_get_interface_request(IUDEVICE* pdev, URBDRC_CHANNEL_CALLBACK* callback, + wStream* s, UINT32 RequestField, UINT32 MessageId, + IUDEVMAN* udevman, int transferDir) +{ + size_t out_size; + UINT32 InterfaceId, OutputBufferSize, usbd_status; + UINT16 interface; + wStream* out; + URBDRC_PLUGIN* urbdrc; + const BOOL noAck = (RequestField & 0x80000000U) != 0; + const UINT32 RequestId = RequestField & 0x7FFFFFFF; + + if (!callback || !s || !udevman || !pdev) + return ERROR_INVALID_PARAMETER; + + urbdrc = (URBDRC_PLUGIN*)callback->plugin; + + if (!urbdrc) + return ERROR_INVALID_PARAMETER; + + if (Stream_GetRemainingLength(s) < 8) + return ERROR_INVALID_DATA; + + if (transferDir == 0) + { + WLog_Print(urbdrc->log, WLOG_DEBUG, + "urb_control_get_interface_request: not support transfer out"); + return ERROR_INVALID_PARAMETER; + } + + InterfaceId = ((STREAM_ID_PROXY << 30) | pdev->get_ReqCompletion(pdev)); + Stream_Read_UINT16(s, interface); + Stream_Seek(s, 2); + Stream_Read_UINT32(s, OutputBufferSize); + if (OutputBufferSize > UINT32_MAX - 36) + return ERROR_INVALID_DATA; + out_size = 36ULL + OutputBufferSize; + out = Stream_New(NULL, out_size); + + if (!out) + return ERROR_OUTOFMEMORY; + + Stream_Seek(out, 36); + + if (!pdev->control_transfer( + pdev, RequestId, 0, 0, 0x80 | 0x01, 0x0A, /* REQUEST_GET_INTERFACE */ + 0, interface, &usbd_status, &OutputBufferSize, Stream_Pointer(out), 1000)) + { + WLog_Print(urbdrc->log, WLOG_DEBUG, "control_transfer failed"); + Stream_Free(out, TRUE); + return ERROR_INTERNAL_ERROR; + } + + return urb_write_completion(pdev, callback, noAck, out, InterfaceId, MessageId, RequestId, + usbd_status, OutputBufferSize); +} + +static UINT urb_control_feature_request(IUDEVICE* pdev, URBDRC_CHANNEL_CALLBACK* callback, + wStream* s, UINT32 RequestField, UINT32 MessageId, + IUDEVMAN* udevman, BYTE func_recipient, BYTE command, + int transferDir) +{ + UINT32 InterfaceId, OutputBufferSize, usbd_status; + UINT16 FeatureSelector, Index; + BYTE bmRequestType, bmRequest; + wStream* out; + URBDRC_PLUGIN* urbdrc; + const BOOL noAck = (RequestField & 0x80000000U) != 0; + const UINT32 RequestId = RequestField & 0x7FFFFFFF; + + if (!callback || !s || !udevman || !pdev) + return ERROR_INVALID_PARAMETER; + + urbdrc = (URBDRC_PLUGIN*)callback->plugin; + + if (!urbdrc) + return ERROR_INVALID_PARAMETER; + + if (Stream_GetRemainingLength(s) < 8) + return ERROR_INVALID_DATA; + + InterfaceId = ((STREAM_ID_PROXY << 30) | pdev->get_ReqCompletion(pdev)); + Stream_Read_UINT16(s, FeatureSelector); + Stream_Read_UINT16(s, Index); + Stream_Read_UINT32(s, OutputBufferSize); + if (OutputBufferSize > UINT32_MAX - 36) + return ERROR_INVALID_DATA; + switch (transferDir) + { + case USBD_TRANSFER_DIRECTION_OUT: + if (Stream_GetRemainingLength(s) < OutputBufferSize) + return ERROR_INVALID_DATA; + + break; + + default: + break; + } + + out = Stream_New(NULL, 36ULL + OutputBufferSize); + + if (!out) + return ERROR_OUTOFMEMORY; + + Stream_Seek(out, 36); + bmRequestType = func_recipient; + + switch (transferDir) + { + case USBD_TRANSFER_DIRECTION_OUT: + WLog_Print(urbdrc->log, WLOG_ERROR, + "Function urb_control_feature_request: OUT Unchecked"); + Stream_Copy(s, out, OutputBufferSize); + Stream_Rewind(out, OutputBufferSize); + bmRequestType |= 0x00; + break; + + case USBD_TRANSFER_DIRECTION_IN: + bmRequestType |= 0x80; + break; + } + + switch (command) + { + case URB_SET_FEATURE: + bmRequest = 0x03; /* REQUEST_SET_FEATURE */ + break; + + case URB_CLEAR_FEATURE: + bmRequest = 0x01; /* REQUEST_CLEAR_FEATURE */ + break; + + default: + WLog_Print(urbdrc->log, WLOG_ERROR, + "urb_control_feature_request: Error Command 0x%02" PRIx8 "", command); + Stream_Free(out, TRUE); + return ERROR_INTERNAL_ERROR; + } + + if (!pdev->control_transfer(pdev, RequestId, 0, 0, bmRequestType, bmRequest, FeatureSelector, + Index, &usbd_status, &OutputBufferSize, Stream_Pointer(out), 1000)) + { + WLog_Print(urbdrc->log, WLOG_DEBUG, "feature control transfer failed"); + Stream_Free(out, TRUE); + return ERROR_INTERNAL_ERROR; + } + + return urb_write_completion(pdev, callback, noAck, out, InterfaceId, MessageId, RequestId, + usbd_status, OutputBufferSize); +} + +static UINT urbdrc_process_transfer_request(IUDEVICE* pdev, URBDRC_CHANNEL_CALLBACK* callback, + wStream* s, UINT32 MessageId, IUDEVMAN* udevman, + int transferDir) +{ + UINT32 CbTsUrb; + UINT16 Size; + UINT16 URB_Function; + UINT32 RequestId; + UINT error = ERROR_INTERNAL_ERROR; + URBDRC_PLUGIN* urbdrc; + + if (!callback || !s || !udevman || !pdev) + return ERROR_INVALID_PARAMETER; + + urbdrc = (URBDRC_PLUGIN*)callback->plugin; + + if (!urbdrc) + return ERROR_INVALID_PARAMETER; + + if (Stream_GetRemainingLength(s) < 12) + return ERROR_INVALID_DATA; + + Stream_Read_UINT32(s, CbTsUrb); /** CbTsUrb */ + Stream_Read_UINT16(s, Size); /** size */ + Stream_Read_UINT16(s, URB_Function); + Stream_Read_UINT32(s, RequestId); + WLog_Print(urbdrc->log, WLOG_DEBUG, "URB %s[" PRIu16 "]", urb_function_string(URB_Function), + URB_Function); + + switch (URB_Function) + { + case TS_URB_SELECT_CONFIGURATION: /** 0x0000 */ + error = urb_select_configuration(pdev, callback, s, RequestId, MessageId, udevman, + transferDir); + break; + + case TS_URB_SELECT_INTERFACE: /** 0x0001 */ + error = + urb_select_interface(pdev, callback, s, RequestId, MessageId, udevman, transferDir); + break; + + case TS_URB_PIPE_REQUEST: /** 0x0002 */ + error = urb_pipe_request(pdev, callback, s, RequestId, MessageId, udevman, transferDir, + PIPE_CANCEL); + break; + + case TS_URB_TAKE_FRAME_LENGTH_CONTROL: /** 0x0003 */ + /** This URB function is obsolete in Windows 2000 + * and later operating systems + * and is not supported by Microsoft. */ + break; + + case TS_URB_RELEASE_FRAME_LENGTH_CONTROL: /** 0x0004 */ + /** This URB function is obsolete in Windows 2000 + * and later operating systems + * and is not supported by Microsoft. */ + break; + + case TS_URB_GET_FRAME_LENGTH: /** 0x0005 */ + /** This URB function is obsolete in Windows 2000 + * and later operating systems + * and is not supported by Microsoft. */ + break; + + case TS_URB_SET_FRAME_LENGTH: /** 0x0006 */ + /** This URB function is obsolete in Windows 2000 + * and later operating systems + * and is not supported by Microsoft. */ + break; + + case TS_URB_GET_CURRENT_FRAME_NUMBER: /** 0x0007 */ + error = urb_get_current_frame_number(pdev, callback, s, RequestId, MessageId, udevman, + transferDir); + break; + + case TS_URB_CONTROL_TRANSFER: /** 0x0008 */ + error = urb_control_transfer(pdev, callback, s, RequestId, MessageId, udevman, + transferDir, URB_CONTROL_TRANSFER_NONEXTERNAL); + break; + + case TS_URB_BULK_OR_INTERRUPT_TRANSFER: /** 0x0009 */ + error = urb_bulk_or_interrupt_transfer(pdev, callback, s, RequestId, MessageId, udevman, + transferDir); + break; + + case TS_URB_ISOCH_TRANSFER: /** 0x000A */ + error = + urb_isoch_transfer(pdev, callback, s, RequestId, MessageId, udevman, transferDir); + break; + + case TS_URB_GET_DESCRIPTOR_FROM_DEVICE: /** 0x000B */ + error = urb_control_descriptor_request(pdev, callback, s, RequestId, MessageId, udevman, + 0x00, transferDir); + break; + + case TS_URB_SET_DESCRIPTOR_TO_DEVICE: /** 0x000C */ + error = urb_control_descriptor_request(pdev, callback, s, RequestId, MessageId, udevman, + 0x00, transferDir); + break; + + case TS_URB_SET_FEATURE_TO_DEVICE: /** 0x000D */ + error = urb_control_feature_request(pdev, callback, s, RequestId, MessageId, udevman, + 0x00, URB_SET_FEATURE, transferDir); + break; + + case TS_URB_SET_FEATURE_TO_INTERFACE: /** 0x000E */ + error = urb_control_feature_request(pdev, callback, s, RequestId, MessageId, udevman, + 0x01, URB_SET_FEATURE, transferDir); + break; + + case TS_URB_SET_FEATURE_TO_ENDPOINT: /** 0x000F */ + error = urb_control_feature_request(pdev, callback, s, RequestId, MessageId, udevman, + 0x02, URB_SET_FEATURE, transferDir); + break; + + case TS_URB_CLEAR_FEATURE_TO_DEVICE: /** 0x0010 */ + error = urb_control_feature_request(pdev, callback, s, RequestId, MessageId, udevman, + 0x00, URB_CLEAR_FEATURE, transferDir); + break; + + case TS_URB_CLEAR_FEATURE_TO_INTERFACE: /** 0x0011 */ + error = urb_control_feature_request(pdev, callback, s, RequestId, MessageId, udevman, + 0x01, URB_CLEAR_FEATURE, transferDir); + break; + + case TS_URB_CLEAR_FEATURE_TO_ENDPOINT: /** 0x0012 */ + error = urb_control_feature_request(pdev, callback, s, RequestId, MessageId, udevman, + 0x02, URB_CLEAR_FEATURE, transferDir); + break; + + case TS_URB_GET_STATUS_FROM_DEVICE: /** 0x0013 */ + error = urb_control_get_status_request(pdev, callback, s, RequestId, MessageId, udevman, + 0x00, transferDir); + break; + + case TS_URB_GET_STATUS_FROM_INTERFACE: /** 0x0014 */ + error = urb_control_get_status_request(pdev, callback, s, RequestId, MessageId, udevman, + 0x01, transferDir); + break; + + case TS_URB_GET_STATUS_FROM_ENDPOINT: /** 0x0015 */ + error = urb_control_get_status_request(pdev, callback, s, RequestId, MessageId, udevman, + 0x02, transferDir); + break; + + case TS_URB_RESERVED_0X0016: /** 0x0016 */ + break; + + case TS_URB_VENDOR_DEVICE: /** 0x0017 */ + error = urb_control_vendor_or_class_request(pdev, callback, s, RequestId, MessageId, + udevman, (0x02 << 5), /* vendor type */ + 0x00, transferDir); + break; + + case TS_URB_VENDOR_INTERFACE: /** 0x0018 */ + error = urb_control_vendor_or_class_request(pdev, callback, s, RequestId, MessageId, + udevman, (0x02 << 5), /* vendor type */ + 0x01, transferDir); + break; + + case TS_URB_VENDOR_ENDPOINT: /** 0x0019 */ + error = urb_control_vendor_or_class_request(pdev, callback, s, RequestId, MessageId, + udevman, (0x02 << 5), /* vendor type */ + 0x02, transferDir); + break; + + case TS_URB_CLASS_DEVICE: /** 0x001A */ + error = urb_control_vendor_or_class_request(pdev, callback, s, RequestId, MessageId, + udevman, (0x01 << 5), /* class type */ + 0x00, transferDir); + break; + + case TS_URB_CLASS_INTERFACE: /** 0x001B */ + error = urb_control_vendor_or_class_request(pdev, callback, s, RequestId, MessageId, + udevman, (0x01 << 5), /* class type */ + 0x01, transferDir); + break; + + case TS_URB_CLASS_ENDPOINT: /** 0x001C */ + error = urb_control_vendor_or_class_request(pdev, callback, s, RequestId, MessageId, + udevman, (0x01 << 5), /* class type */ + 0x02, transferDir); + break; + + case TS_URB_RESERVE_0X001D: /** 0x001D */ + break; + + case TS_URB_SYNC_RESET_PIPE_AND_CLEAR_STALL: /** 0x001E */ + error = urb_pipe_request(pdev, callback, s, RequestId, MessageId, udevman, transferDir, + PIPE_RESET); + break; + + case TS_URB_CLASS_OTHER: /** 0x001F */ + error = urb_control_vendor_or_class_request(pdev, callback, s, RequestId, MessageId, + udevman, (0x01 << 5), /* class type */ + 0x03, transferDir); + break; + + case TS_URB_VENDOR_OTHER: /** 0x0020 */ + error = urb_control_vendor_or_class_request(pdev, callback, s, RequestId, MessageId, + udevman, (0x02 << 5), /* vendor type */ + 0x03, transferDir); + break; + + case TS_URB_GET_STATUS_FROM_OTHER: /** 0x0021 */ + error = urb_control_get_status_request(pdev, callback, s, RequestId, MessageId, udevman, + 0x03, transferDir); + break; + + case TS_URB_CLEAR_FEATURE_TO_OTHER: /** 0x0022 */ + error = urb_control_feature_request(pdev, callback, s, RequestId, MessageId, udevman, + 0x03, URB_CLEAR_FEATURE, transferDir); + break; + + case TS_URB_SET_FEATURE_TO_OTHER: /** 0x0023 */ + error = urb_control_feature_request(pdev, callback, s, RequestId, MessageId, udevman, + 0x03, URB_SET_FEATURE, transferDir); + break; + + case TS_URB_GET_DESCRIPTOR_FROM_ENDPOINT: /** 0x0024 */ + error = urb_control_descriptor_request(pdev, callback, s, RequestId, MessageId, udevman, + 0x02, transferDir); + break; + + case TS_URB_SET_DESCRIPTOR_TO_ENDPOINT: /** 0x0025 */ + error = urb_control_descriptor_request(pdev, callback, s, RequestId, MessageId, udevman, + 0x02, transferDir); + break; + + case TS_URB_CONTROL_GET_CONFIGURATION_REQUEST: /** 0x0026 */ + error = urb_control_get_configuration_request(pdev, callback, s, RequestId, MessageId, + udevman, transferDir); + break; + + case TS_URB_CONTROL_GET_INTERFACE_REQUEST: /** 0x0027 */ + error = urb_control_get_interface_request(pdev, callback, s, RequestId, MessageId, + udevman, transferDir); + break; + + case TS_URB_GET_DESCRIPTOR_FROM_INTERFACE: /** 0x0028 */ + error = urb_control_descriptor_request(pdev, callback, s, RequestId, MessageId, udevman, + 0x01, transferDir); + break; + + case TS_URB_SET_DESCRIPTOR_TO_INTERFACE: /** 0x0029 */ + error = urb_control_descriptor_request(pdev, callback, s, RequestId, MessageId, udevman, + 0x01, transferDir); + break; + + case TS_URB_GET_OS_FEATURE_DESCRIPTOR_REQUEST: /** 0x002A */ + error = urb_os_feature_descriptor_request(pdev, callback, s, RequestId, MessageId, + udevman, transferDir); + break; + + case TS_URB_RESERVE_0X002B: /** 0x002B */ + case TS_URB_RESERVE_0X002C: /** 0x002C */ + case TS_URB_RESERVE_0X002D: /** 0x002D */ + case TS_URB_RESERVE_0X002E: /** 0x002E */ + case TS_URB_RESERVE_0X002F: /** 0x002F */ + break; + + /** USB 2.0 calls start at 0x0030 */ + case TS_URB_SYNC_RESET_PIPE: /** 0x0030 */ + error = urb_pipe_request(pdev, callback, s, RequestId, MessageId, udevman, transferDir, + PIPE_RESET); + break; + + case TS_URB_SYNC_CLEAR_STALL: /** 0x0031 */ + urb_pipe_request(pdev, callback, s, RequestId, MessageId, udevman, transferDir, + PIPE_RESET); + break; + + case TS_URB_CONTROL_TRANSFER_EX: /** 0x0032 */ + error = urb_control_transfer(pdev, callback, s, RequestId, MessageId, udevman, + transferDir, URB_CONTROL_TRANSFER_EXTERNAL); + break; + + default: + WLog_Print(urbdrc->log, WLOG_DEBUG, "URB_Func: %" PRIx16 " is not found!", + URB_Function); + break; + } + + if (error) + { + WLog_Print(urbdrc->log, WLOG_WARN, + "USB transfer request URB Function %08" PRIx32 " failed with %08" PRIx32, + URB_Function, error); + } + + return error; +} + +UINT urbdrc_process_udev_data_transfer(URBDRC_CHANNEL_CALLBACK* callback, URBDRC_PLUGIN* urbdrc, + IUDEVMAN* udevman, wStream* data) +{ + UINT32 InterfaceId; + UINT32 MessageId; + UINT32 FunctionId; + IUDEVICE* pdev; + UINT error = ERROR_INTERNAL_ERROR; + size_t len; + + if (!urbdrc || !data || !callback || !udevman) + goto fail; + + len = Stream_GetRemainingLength(data); + + if (len < 8) + goto fail; + + Stream_Rewind_UINT32(data); + + Stream_Read_UINT32(data, InterfaceId); + Stream_Read_UINT32(data, MessageId); + Stream_Read_UINT32(data, FunctionId); + + pdev = udevman->get_udevice_by_UsbDevice(udevman, InterfaceId); + + /* Device does not exist, ignore this request. */ + if (pdev == NULL) + { + error = ERROR_SUCCESS; + goto fail; + } + + /* Device has been removed, ignore this request. */ + if (pdev->isChannelClosed(pdev)) + { + error = ERROR_SUCCESS; + goto fail; + } + + /* USB kernel driver detach!! */ + pdev->detach_kernel_driver(pdev); + + switch (FunctionId) + { + case CANCEL_REQUEST: + error = urbdrc_process_cancel_request(pdev, data, udevman); + break; + + case REGISTER_REQUEST_CALLBACK: + error = urbdrc_process_register_request_callback(pdev, callback, data, udevman); + break; + + case IO_CONTROL: + error = urbdrc_process_io_control(pdev, callback, data, MessageId, udevman); + break; + + case INTERNAL_IO_CONTROL: + error = urbdrc_process_internal_io_control(pdev, callback, data, MessageId, udevman); + break; + + case QUERY_DEVICE_TEXT: + error = urbdrc_process_query_device_text(pdev, callback, data, MessageId, udevman); + break; + + case TRANSFER_IN_REQUEST: + error = urbdrc_process_transfer_request(pdev, callback, data, MessageId, udevman, + USBD_TRANSFER_DIRECTION_IN); + break; + + case TRANSFER_OUT_REQUEST: + error = urbdrc_process_transfer_request(pdev, callback, data, MessageId, udevman, + USBD_TRANSFER_DIRECTION_OUT); + break; + + case RETRACT_DEVICE: + error = urbdrc_process_retract_device_request(pdev, data, udevman); + break; + + default: + WLog_Print(urbdrc->log, WLOG_WARN, + "urbdrc_process_udev_data_transfer:" + " unknown FunctionId 0x%" PRIX32 "", + FunctionId); + break; + } + +fail: + if (error) + { + WLog_WARN(TAG, "USB request failed with %08" PRIx32, error); + } + + return error; +} diff --git a/channels/urbdrc/client/data_transfer.h b/channels/urbdrc/client/data_transfer.h new file mode 100644 index 0000000..d63f82e --- /dev/null +++ b/channels/urbdrc/client/data_transfer.h @@ -0,0 +1,36 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * RemoteFX USB Redirection + * + * Copyright 2012 Atrust corp. + * Copyright 2012 Alfred Liu + * + * Licensed 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 FREERDP_CHANNEL_URBDRC_CLIENT_DATA_TRANSFER_H +#define FREERDP_CHANNEL_URBDRC_CLIENT_DATA_TRANSFER_H + +#include + +#include "urbdrc_main.h" + +#define DEVICE_CTX(dev) ((dev)->ctx) +#define HANDLE_CTX(handle) (DEVICE_CTX((handle)->dev)) +#define TRANSFER_CTX(transfer) (HANDLE_CTX((transfer)->dev_handle)) +#define ITRANSFER_CTX(transfer) (TRANSFER_CTX(__USBI_TRANSFER_TO_LIBUSB_TRANSFER(transfer))) + +UINT urbdrc_process_udev_data_transfer(URBDRC_CHANNEL_CALLBACK* callback, URBDRC_PLUGIN* urbdrc, + IUDEVMAN* udevman, wStream* data); + +#endif /* FREERDP_CHANNEL_URBDRC_CLIENT_DATA_TRANSFER_H */ diff --git a/channels/urbdrc/client/libusb/CMakeLists.txt b/channels/urbdrc/client/libusb/CMakeLists.txt new file mode 100644 index 0000000..c5e9b70 --- /dev/null +++ b/channels/urbdrc/client/libusb/CMakeLists.txt @@ -0,0 +1,46 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2012 Atrust corp. +# Copyright 2012 Alfred Liu +# +# Licensed 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. + +define_channel_client_subsystem("urbdrc" "libusb" "") + +set(${MODULE_PREFIX}_SRCS + libusb_udevman.c + libusb_udevice.c + libusb_udevice.h) + +include_directories(..) + +add_channel_client_subsystem_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} "" TRUE "") + + + +set(${MODULE_PREFIX}_LIBS + ${CMAKE_THREAD_LIBS_INIT}) + +set(${MODULE_PREFIX}_LIBS ${${MODULE_PREFIX}_LIBS} + ${LIBUSB_1_LIBRARIES}) + + +set(${MODULE_PREFIX}_LIBS ${${MODULE_PREFIX}_LIBS} winpr urbdrc-client) + +target_link_libraries(${MODULE_NAME} ${${MODULE_PREFIX}_LIBS}) + +if (WITH_DEBUG_SYMBOLS AND MSVC AND NOT BUILTIN_CHANNELS AND BUILD_SHARED_LIBS) + install(FILES ${CMAKE_BINARY_DIR}/${MODULE_NAME}.pdb DESTINATION ${FREERDP_ADDIN_PATH} COMPONENT symbols) +endif() + diff --git a/channels/urbdrc/client/libusb/libusb_udevice.c b/channels/urbdrc/client/libusb/libusb_udevice.c new file mode 100644 index 0000000..505c31d --- /dev/null +++ b/channels/urbdrc/client/libusb/libusb_udevice.c @@ -0,0 +1,1796 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * RemoteFX USB Redirection + * + * Copyright 2012 Atrust corp. + * Copyright 2012 Alfred Liu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include + +#include +#include + +#include + +#include "libusb_udevice.h" +#include "../common/urbdrc_types.h" + +#define BASIC_STATE_FUNC_DEFINED(_arg, _type) \ + static _type udev_get_##_arg(IUDEVICE* idev) \ + { \ + UDEVICE* pdev = (UDEVICE*)idev; \ + return pdev->_arg; \ + } \ + static void udev_set_##_arg(IUDEVICE* idev, _type _t) \ + { \ + UDEVICE* pdev = (UDEVICE*)idev; \ + pdev->_arg = _t; \ + } + +#define BASIC_POINT_FUNC_DEFINED(_arg, _type) \ + static _type udev_get_p_##_arg(IUDEVICE* idev) \ + { \ + UDEVICE* pdev = (UDEVICE*)idev; \ + return pdev->_arg; \ + } \ + static void udev_set_p_##_arg(IUDEVICE* idev, _type _t) \ + { \ + UDEVICE* pdev = (UDEVICE*)idev; \ + pdev->_arg = _t; \ + } + +#define BASIC_STATE_FUNC_REGISTER(_arg, _dev) \ + _dev->iface.get_##_arg = udev_get_##_arg; \ + _dev->iface.set_##_arg = udev_set_##_arg + +#if LIBUSB_API_VERSION >= 0x01000103 +#define HAVE_STREAM_ID_API 1 +#endif + +typedef struct _ASYNC_TRANSFER_USER_DATA ASYNC_TRANSFER_USER_DATA; + +struct _ASYNC_TRANSFER_USER_DATA +{ + wStream* data; + BOOL noack; + UINT32 MessageId; + UINT32 StartFrame; + UINT32 ErrorCount; + IUDEVICE* idev; + UINT32 OutputBufferSize; + URBDRC_CHANNEL_CALLBACK* callback; + t_isoch_transfer_cb cb; + wArrayList* queue; +#if !defined(HAVE_STREAM_ID_API) + UINT32 streamID; +#endif +}; + +static void request_free(void* value); + +static struct libusb_transfer* list_contains(wArrayList* list, UINT32 streamID) +{ + int x, count; + if (!list) + return NULL; + count = ArrayList_Count(list); + for (x = 0; x < count; x++) + { + struct libusb_transfer* transfer = ArrayList_GetItem(list, x); + +#if defined(HAVE_STREAM_ID_API) + const UINT32 currentID = libusb_transfer_get_stream_id(transfer); +#else + const ASYNC_TRANSFER_USER_DATA* user_data = (ASYNC_TRANSFER_USER_DATA*)transfer->user_data; + const UINT32 currentID = user_data->streamID; +#endif + if (currentID == streamID) + return transfer; + } + return NULL; +} + +static UINT32 stream_id_from_buffer(struct libusb_transfer* transfer) +{ + if (!transfer) + return 0; +#if defined(HAVE_STREAM_ID_API) + return libusb_transfer_get_stream_id(transfer); +#else + ASYNC_TRANSFER_USER_DATA* user_data = (ASYNC_TRANSFER_USER_DATA*)transfer->user_data; + if (!user_data) + return 0; + return user_data->streamID; +#endif +} + +static void set_stream_id_for_buffer(struct libusb_transfer* transfer, UINT32 streamID) +{ +#if defined(HAVE_STREAM_ID_API) + libusb_transfer_set_stream_id(transfer, streamID); +#else + ASYNC_TRANSFER_USER_DATA* user_data = (ASYNC_TRANSFER_USER_DATA*)transfer->user_data; + if (!user_data) + return; + user_data->streamID = streamID; +#endif +} +static BOOL log_libusb_result(wLog* log, DWORD lvl, const char* fmt, int error, ...) +{ + if (error < 0) + { + char buffer[8192] = { 0 }; + va_list ap; + va_start(ap, error); + vsnprintf(buffer, sizeof(buffer), fmt, ap); + va_end(ap); + + WLog_Print(log, lvl, "%s: error %s[%d]", buffer, libusb_error_name(error), error); + return TRUE; + } + return FALSE; +} + +const char* usb_interface_class_to_string(uint8_t class) +{ + switch (class) + { + case LIBUSB_CLASS_PER_INTERFACE: + return "LIBUSB_CLASS_PER_INTERFACE"; + case LIBUSB_CLASS_AUDIO: + return "LIBUSB_CLASS_AUDIO"; + case LIBUSB_CLASS_COMM: + return "LIBUSB_CLASS_COMM"; + case LIBUSB_CLASS_HID: + return "LIBUSB_CLASS_HID"; + case LIBUSB_CLASS_PHYSICAL: + return "LIBUSB_CLASS_PHYSICAL"; + case LIBUSB_CLASS_PRINTER: + return "LIBUSB_CLASS_PRINTER"; + case LIBUSB_CLASS_IMAGE: + return "LIBUSB_CLASS_IMAGE"; + case LIBUSB_CLASS_MASS_STORAGE: + return "LIBUSB_CLASS_MASS_STORAGE"; + case LIBUSB_CLASS_HUB: + return "LIBUSB_CLASS_HUB"; + case LIBUSB_CLASS_DATA: + return "LIBUSB_CLASS_DATA"; + case LIBUSB_CLASS_SMART_CARD: + return "LIBUSB_CLASS_SMART_CARD"; + case LIBUSB_CLASS_CONTENT_SECURITY: + return "LIBUSB_CLASS_CONTENT_SECURITY"; + case LIBUSB_CLASS_VIDEO: + return "LIBUSB_CLASS_VIDEO"; + case LIBUSB_CLASS_PERSONAL_HEALTHCARE: + return "LIBUSB_CLASS_PERSONAL_HEALTHCARE"; + case LIBUSB_CLASS_DIAGNOSTIC_DEVICE: + return "LIBUSB_CLASS_DIAGNOSTIC_DEVICE"; + case LIBUSB_CLASS_WIRELESS: + return "LIBUSB_CLASS_WIRELESS"; + case LIBUSB_CLASS_APPLICATION: + return "LIBUSB_CLASS_APPLICATION"; + case LIBUSB_CLASS_VENDOR_SPEC: + return "LIBUSB_CLASS_VENDOR_SPEC"; + default: + return "UNKNOWN_DEVICE_CLASS"; + } +} + +static ASYNC_TRANSFER_USER_DATA* async_transfer_user_data_new(IUDEVICE* idev, UINT32 MessageId, + size_t offset, size_t BufferSize, + const BYTE* data, size_t packetSize, + BOOL NoAck, t_isoch_transfer_cb cb, + URBDRC_CHANNEL_CALLBACK* callback) +{ + ASYNC_TRANSFER_USER_DATA* user_data = calloc(1, sizeof(ASYNC_TRANSFER_USER_DATA)); + UDEVICE* pdev = (UDEVICE*)idev; + + if (!user_data) + return NULL; + + user_data->data = Stream_New(NULL, offset + BufferSize + packetSize); + + if (!user_data->data) + { + free(user_data); + return NULL; + } + + Stream_Seek(user_data->data, offset); /* Skip header offset */ + if (data) + memcpy(Stream_Pointer(user_data->data), data, BufferSize); + else + user_data->OutputBufferSize = BufferSize; + + user_data->noack = NoAck; + user_data->cb = cb; + user_data->callback = callback; + user_data->idev = idev; + user_data->MessageId = MessageId; + + user_data->queue = pdev->request_queue; + + return user_data; +} + +static void async_transfer_user_data_free(ASYNC_TRANSFER_USER_DATA* user_data) +{ + if (user_data) + { + Stream_Free(user_data->data, TRUE); + free(user_data); + } +} + +static void LIBUSB_CALL func_iso_callback(struct libusb_transfer* transfer) +{ + ASYNC_TRANSFER_USER_DATA* user_data = (ASYNC_TRANSFER_USER_DATA*)transfer->user_data; + const UINT32 streamID = stream_id_from_buffer(transfer); + wArrayList* list = user_data->queue; + + ArrayList_Lock(list); + switch (transfer->status) + { + case LIBUSB_TRANSFER_COMPLETED: + { + int i; + UINT32 index = 0; + BYTE* dataStart = Stream_Pointer(user_data->data); + Stream_SetPosition(user_data->data, + 40); /* TS_URB_ISOCH_TRANSFER_RESULT IsoPacket offset */ + + for (i = 0; i < transfer->num_iso_packets; i++) + { + const UINT32 act_len = transfer->iso_packet_desc[i].actual_length; + Stream_Write_UINT32(user_data->data, index); + Stream_Write_UINT32(user_data->data, act_len); + Stream_Write_UINT32(user_data->data, transfer->iso_packet_desc[i].status); + + if (transfer->iso_packet_desc[i].status != USBD_STATUS_SUCCESS) + user_data->ErrorCount++; + else + { + const unsigned char* packetBuffer = + libusb_get_iso_packet_buffer_simple(transfer, i); + BYTE* data = dataStart + index; + + if (data != packetBuffer) + memmove(data, packetBuffer, act_len); + + index += act_len; + } + } + } + /* fallthrough */ + + case LIBUSB_TRANSFER_CANCELLED: + case LIBUSB_TRANSFER_TIMED_OUT: + case LIBUSB_TRANSFER_ERROR: + { + const UINT32 InterfaceId = + ((STREAM_ID_PROXY << 30) | user_data->idev->get_ReqCompletion(user_data->idev)); + + if (list_contains(list, streamID)) + { + if (!user_data->noack) + { + const UINT32 RequestID = streamID & INTERFACE_ID_MASK; + user_data->cb(user_data->idev, user_data->callback, user_data->data, + InterfaceId, user_data->noack, user_data->MessageId, RequestID, + transfer->num_iso_packets, transfer->status, + user_data->StartFrame, user_data->ErrorCount, + user_data->OutputBufferSize); + user_data->data = NULL; + } + ArrayList_Remove(list, transfer); + } + } + break; + default: + break; + } + ArrayList_Unlock(list); +} + +static const LIBUSB_ENDPOINT_DESCEIPTOR* func_get_ep_desc(LIBUSB_CONFIG_DESCRIPTOR* LibusbConfig, + MSUSB_CONFIG_DESCRIPTOR* MsConfig, + UINT32 EndpointAddress) +{ + BYTE alt; + UINT32 inum, pnum; + MSUSB_INTERFACE_DESCRIPTOR** MsInterfaces; + const LIBUSB_INTERFACE* interface; + const LIBUSB_ENDPOINT_DESCEIPTOR* endpoint; + MsInterfaces = MsConfig->MsInterfaces; + interface = LibusbConfig->interface; + + for (inum = 0; inum < MsConfig->NumInterfaces; inum++) + { + alt = MsInterfaces[inum]->AlternateSetting; + endpoint = interface[inum].altsetting[alt].endpoint; + + for (pnum = 0; pnum < MsInterfaces[inum]->NumberOfPipes; pnum++) + { + if (endpoint[pnum].bEndpointAddress == EndpointAddress) + { + return &endpoint[pnum]; + } + } + } + + return NULL; +} + +static void LIBUSB_CALL func_bulk_transfer_cb(struct libusb_transfer* transfer) +{ + ASYNC_TRANSFER_USER_DATA* user_data; + uint32_t streamID; + wArrayList* list; + + user_data = (ASYNC_TRANSFER_USER_DATA*)transfer->user_data; + if (!user_data) + { + WLog_ERR(TAG, "[%s]: Invalid transfer->user_data!"); + return; + } + list = user_data->queue; + ArrayList_Lock(list); + streamID = stream_id_from_buffer(transfer); + + if (list_contains(list, streamID)) + { + const UINT32 InterfaceId = + ((STREAM_ID_PROXY << 30) | user_data->idev->get_ReqCompletion(user_data->idev)); + const UINT32 RequestID = streamID & INTERFACE_ID_MASK; + + user_data->cb(user_data->idev, user_data->callback, user_data->data, InterfaceId, + user_data->noack, user_data->MessageId, RequestID, transfer->num_iso_packets, + transfer->status, user_data->StartFrame, user_data->ErrorCount, + transfer->actual_length); + user_data->data = NULL; + ArrayList_Remove(list, transfer); + } + ArrayList_Unlock(list); +} + +static BOOL func_set_usbd_status(URBDRC_PLUGIN* urbdrc, UDEVICE* pdev, UINT32* status, + int err_result) +{ + if (!urbdrc || !status) + return FALSE; + + switch (err_result) + { + case LIBUSB_SUCCESS: + *status = USBD_STATUS_SUCCESS; + break; + + case LIBUSB_ERROR_IO: + *status = USBD_STATUS_STALL_PID; + break; + + case LIBUSB_ERROR_INVALID_PARAM: + *status = USBD_STATUS_INVALID_PARAMETER; + break; + + case LIBUSB_ERROR_ACCESS: + *status = USBD_STATUS_NOT_ACCESSED; + break; + + case LIBUSB_ERROR_NO_DEVICE: + *status = USBD_STATUS_DEVICE_GONE; + + if (pdev) + { + if (!(pdev->status & URBDRC_DEVICE_NOT_FOUND)) + pdev->status |= URBDRC_DEVICE_NOT_FOUND; + } + + break; + + case LIBUSB_ERROR_NOT_FOUND: + *status = USBD_STATUS_STALL_PID; + break; + + case LIBUSB_ERROR_BUSY: + *status = USBD_STATUS_STALL_PID; + break; + + case LIBUSB_ERROR_TIMEOUT: + *status = USBD_STATUS_TIMEOUT; + break; + + case LIBUSB_ERROR_OVERFLOW: + *status = USBD_STATUS_STALL_PID; + break; + + case LIBUSB_ERROR_PIPE: + *status = USBD_STATUS_STALL_PID; + break; + + case LIBUSB_ERROR_INTERRUPTED: + *status = USBD_STATUS_STALL_PID; + break; + + case LIBUSB_ERROR_NO_MEM: + *status = USBD_STATUS_NO_MEMORY; + break; + + case LIBUSB_ERROR_NOT_SUPPORTED: + *status = USBD_STATUS_NOT_SUPPORTED; + break; + + case LIBUSB_ERROR_OTHER: + *status = USBD_STATUS_STALL_PID; + break; + + default: + *status = USBD_STATUS_SUCCESS; + break; + } + + return TRUE; +} + +static int func_config_release_all_interface(URBDRC_PLUGIN* urbdrc, + LIBUSB_DEVICE_HANDLE* libusb_handle, + UINT32 NumInterfaces) +{ + UINT32 i; + + for (i = 0; i < NumInterfaces; i++) + { + int ret = libusb_release_interface(libusb_handle, i); + + if (log_libusb_result(urbdrc->log, WLOG_WARN, "libusb_release_interface", ret)) + return -1; + } + + return 0; +} + +static int func_claim_all_interface(URBDRC_PLUGIN* urbdrc, LIBUSB_DEVICE_HANDLE* libusb_handle, + int NumInterfaces) +{ + int i, ret; + + for (i = 0; i < NumInterfaces; i++) + { + ret = libusb_claim_interface(libusb_handle, i); + + if (log_libusb_result(urbdrc->log, WLOG_ERROR, "libusb_claim_interface", ret)) + return -1; + } + + return 0; +} + +static LIBUSB_DEVICE* udev_get_libusb_dev(libusb_context* context, uint8_t bus_number, + uint8_t dev_number) +{ + ssize_t i, total_device; + LIBUSB_DEVICE** libusb_list; + LIBUSB_DEVICE* device = NULL; + total_device = libusb_get_device_list(context, &libusb_list); + + for (i = 0; i < total_device; i++) + { + LIBUSB_DEVICE* dev = libusb_list[i]; + if ((bus_number == libusb_get_bus_number(dev)) && + (dev_number == libusb_get_device_address(dev))) + { + device = dev; + } + else + { + libusb_unref_device(dev); + } + } + + libusb_free_device_list(libusb_list, 0); + return device; +} + +static LIBUSB_DEVICE_DESCRIPTOR* udev_new_descript(URBDRC_PLUGIN* urbdrc, LIBUSB_DEVICE* libusb_dev) +{ + int ret; + LIBUSB_DEVICE_DESCRIPTOR* descriptor = + (LIBUSB_DEVICE_DESCRIPTOR*)calloc(1, sizeof(LIBUSB_DEVICE_DESCRIPTOR)); + if (!descriptor) + return NULL; + ret = libusb_get_device_descriptor(libusb_dev, descriptor); + + if (log_libusb_result(urbdrc->log, WLOG_ERROR, "libusb_get_device_descriptor", ret)) + { + free(descriptor); + return NULL; + } + + return descriptor; +} + +static int libusb_udev_select_interface(IUDEVICE* idev, BYTE InterfaceNumber, BYTE AlternateSetting) +{ + int error = 0, diff = 0; + UDEVICE* pdev = (UDEVICE*)idev; + URBDRC_PLUGIN* urbdrc; + MSUSB_CONFIG_DESCRIPTOR* MsConfig; + MSUSB_INTERFACE_DESCRIPTOR** MsInterfaces; + + if (!pdev || !pdev->urbdrc) + return -1; + + urbdrc = pdev->urbdrc; + MsConfig = pdev->MsConfig; + + if (MsConfig) + { + MsInterfaces = MsConfig->MsInterfaces; + if (MsInterfaces) + { + WLog_Print(urbdrc->log, WLOG_INFO, + "select Interface(%" PRIu8 ") curr AlternateSetting(%" PRIu8 + ") new AlternateSetting(" PRIu8 ")", + InterfaceNumber, MsInterfaces[InterfaceNumber]->AlternateSetting, + AlternateSetting); + + if (MsInterfaces[InterfaceNumber]->AlternateSetting != AlternateSetting) + { + diff = 1; + } + } + + if (diff) + { + error = libusb_set_interface_alt_setting(pdev->libusb_handle, InterfaceNumber, + AlternateSetting); + + log_libusb_result(urbdrc->log, WLOG_ERROR, "libusb_set_interface_alt_setting", error); + } + } + + return error; +} + +static MSUSB_CONFIG_DESCRIPTOR* +libusb_udev_complete_msconfig_setup(IUDEVICE* idev, MSUSB_CONFIG_DESCRIPTOR* MsConfig) +{ + UDEVICE* pdev = (UDEVICE*)idev; + MSUSB_INTERFACE_DESCRIPTOR** MsInterfaces; + MSUSB_INTERFACE_DESCRIPTOR* MsInterface; + MSUSB_PIPE_DESCRIPTOR** MsPipes; + MSUSB_PIPE_DESCRIPTOR* MsPipe; + MSUSB_PIPE_DESCRIPTOR** t_MsPipes; + MSUSB_PIPE_DESCRIPTOR* t_MsPipe; + LIBUSB_CONFIG_DESCRIPTOR* LibusbConfig; + const LIBUSB_INTERFACE* LibusbInterface; + const LIBUSB_INTERFACE_DESCRIPTOR* LibusbAltsetting; + const LIBUSB_ENDPOINT_DESCEIPTOR* LibusbEndpoint; + BYTE LibusbNumEndpoint; + URBDRC_PLUGIN* urbdrc; + UINT32 inum = 0, pnum = 0, MsOutSize = 0; + + if (!pdev || !pdev->LibusbConfig || !pdev->urbdrc || !MsConfig) + return NULL; + + urbdrc = pdev->urbdrc; + LibusbConfig = pdev->LibusbConfig; + + if (LibusbConfig->bNumInterfaces != MsConfig->NumInterfaces) + { + WLog_Print(urbdrc->log, WLOG_ERROR, + "Select Configuration: Libusb NumberInterfaces(%" PRIu8 ") is different " + "with MsConfig NumberInterfaces(%" PRIu32 ")", + LibusbConfig->bNumInterfaces, MsConfig->NumInterfaces); + } + + /* replace MsPipes for libusb */ + MsInterfaces = MsConfig->MsInterfaces; + + for (inum = 0; inum < MsConfig->NumInterfaces; inum++) + { + MsInterface = MsInterfaces[inum]; + /* get libusb's number of endpoints */ + LibusbInterface = &LibusbConfig->interface[MsInterface->InterfaceNumber]; + LibusbAltsetting = &LibusbInterface->altsetting[MsInterface->AlternateSetting]; + LibusbNumEndpoint = LibusbAltsetting->bNumEndpoints; + t_MsPipes = + (MSUSB_PIPE_DESCRIPTOR**)calloc(LibusbNumEndpoint, sizeof(MSUSB_PIPE_DESCRIPTOR*)); + + for (pnum = 0; pnum < LibusbNumEndpoint; pnum++) + { + t_MsPipe = (MSUSB_PIPE_DESCRIPTOR*)calloc(1, sizeof(MSUSB_PIPE_DESCRIPTOR)); + + if (pnum < MsInterface->NumberOfPipes && MsInterface->MsPipes) + { + MsPipe = MsInterface->MsPipes[pnum]; + t_MsPipe->MaximumPacketSize = MsPipe->MaximumPacketSize; + t_MsPipe->MaximumTransferSize = MsPipe->MaximumTransferSize; + t_MsPipe->PipeFlags = MsPipe->PipeFlags; + } + else + { + t_MsPipe->MaximumPacketSize = 0; + t_MsPipe->MaximumTransferSize = 0xffffffff; + t_MsPipe->PipeFlags = 0; + } + + t_MsPipe->PipeHandle = 0; + t_MsPipe->bEndpointAddress = 0; + t_MsPipe->bInterval = 0; + t_MsPipe->PipeType = 0; + t_MsPipe->InitCompleted = 0; + t_MsPipes[pnum] = t_MsPipe; + } + + msusb_mspipes_replace(MsInterface, t_MsPipes, LibusbNumEndpoint); + } + + /* setup configuration */ + MsOutSize = 8; + /* ConfigurationHandle: 4 bytes + * --------------------------------------------------------------- + * ||<<< 1 byte >>>|<<< 1 byte >>>|<<<<<<<<<< 2 byte >>>>>>>>>>>|| + * || bus_number | dev_number | bConfigurationValue || + * --------------------------------------------------------------- + * ***********************/ + MsConfig->ConfigurationHandle = + MsConfig->bConfigurationValue | (pdev->bus_number << 24) | (pdev->dev_number << 16); + MsInterfaces = MsConfig->MsInterfaces; + + for (inum = 0; inum < MsConfig->NumInterfaces; inum++) + { + MsOutSize += 16; + MsInterface = MsInterfaces[inum]; + /* get libusb's interface */ + LibusbInterface = &LibusbConfig->interface[MsInterface->InterfaceNumber]; + LibusbAltsetting = &LibusbInterface->altsetting[MsInterface->AlternateSetting]; + /* InterfaceHandle: 4 bytes + * --------------------------------------------------------------- + * ||<<< 1 byte >>>|<<< 1 byte >>>|<<< 1 byte >>>|<<< 1 byte >>>|| + * || bus_number | dev_number | altsetting | interfaceNum || + * --------------------------------------------------------------- + * ***********************/ + MsInterface->InterfaceHandle = LibusbAltsetting->bInterfaceNumber | + (LibusbAltsetting->bAlternateSetting << 8) | + (pdev->dev_number << 16) | (pdev->bus_number << 24); + MsInterface->Length = 16 + (MsInterface->NumberOfPipes * 20); + MsInterface->bInterfaceClass = LibusbAltsetting->bInterfaceClass; + MsInterface->bInterfaceSubClass = LibusbAltsetting->bInterfaceSubClass; + MsInterface->bInterfaceProtocol = LibusbAltsetting->bInterfaceProtocol; + MsInterface->InitCompleted = 1; + MsPipes = MsInterface->MsPipes; + LibusbNumEndpoint = LibusbAltsetting->bNumEndpoints; + + for (pnum = 0; pnum < LibusbNumEndpoint; pnum++) + { + MsOutSize += 20; + MsPipe = MsPipes[pnum]; + /* get libusb's endpoint */ + LibusbEndpoint = &LibusbAltsetting->endpoint[pnum]; + /* PipeHandle: 4 bytes + * --------------------------------------------------------------- + * ||<<< 1 byte >>>|<<< 1 byte >>>|<<<<<<<<<< 2 byte >>>>>>>>>>>|| + * || bus_number | dev_number | bEndpointAddress || + * --------------------------------------------------------------- + * ***********************/ + MsPipe->PipeHandle = LibusbEndpoint->bEndpointAddress | (pdev->dev_number << 16) | + (pdev->bus_number << 24); + /* count endpoint max packet size */ + int max = LibusbEndpoint->wMaxPacketSize & 0x07ff; + BYTE attr = LibusbEndpoint->bmAttributes; + + if ((attr & 0x3) == 1 || (attr & 0x3) == 3) + { + max *= (1 + ((LibusbEndpoint->wMaxPacketSize >> 11) & 3)); + } + + MsPipe->MaximumPacketSize = max; + MsPipe->bEndpointAddress = LibusbEndpoint->bEndpointAddress; + MsPipe->bInterval = LibusbEndpoint->bInterval; + MsPipe->PipeType = attr & 0x3; + MsPipe->InitCompleted = 1; + } + } + + MsConfig->MsOutSize = MsOutSize; + MsConfig->InitCompleted = 1; + + /* replace device's MsConfig */ + if (MsConfig != pdev->MsConfig) + { + msusb_msconfig_free(pdev->MsConfig); + pdev->MsConfig = MsConfig; + } + + return MsConfig; +} + +static int libusb_udev_select_configuration(IUDEVICE* idev, UINT32 bConfigurationValue) +{ + UDEVICE* pdev = (UDEVICE*)idev; + MSUSB_CONFIG_DESCRIPTOR* MsConfig; + LIBUSB_DEVICE_HANDLE* libusb_handle; + LIBUSB_DEVICE* libusb_dev; + URBDRC_PLUGIN* urbdrc; + LIBUSB_CONFIG_DESCRIPTOR** LibusbConfig; + int ret = 0; + + if (!pdev || !pdev->MsConfig || !pdev->LibusbConfig || !pdev->urbdrc) + return -1; + + urbdrc = pdev->urbdrc; + MsConfig = pdev->MsConfig; + libusb_handle = pdev->libusb_handle; + libusb_dev = pdev->libusb_dev; + LibusbConfig = &pdev->LibusbConfig; + + if (MsConfig->InitCompleted) + { + func_config_release_all_interface(pdev->urbdrc, libusb_handle, + (*LibusbConfig)->bNumInterfaces); + } + + /* The configuration value -1 is mean to put the device in unconfigured state. */ + if (bConfigurationValue == 0) + ret = libusb_set_configuration(libusb_handle, -1); + else + ret = libusb_set_configuration(libusb_handle, bConfigurationValue); + + if (log_libusb_result(urbdrc->log, WLOG_ERROR, "libusb_set_configuration", ret)) + { + func_claim_all_interface(urbdrc, libusb_handle, (*LibusbConfig)->bNumInterfaces); + return -1; + } + else + { + ret = libusb_get_active_config_descriptor(libusb_dev, LibusbConfig); + + if (log_libusb_result(urbdrc->log, WLOG_ERROR, "libusb_set_configuration", ret)) + { + func_claim_all_interface(urbdrc, libusb_handle, (*LibusbConfig)->bNumInterfaces); + return -1; + } + } + + func_claim_all_interface(urbdrc, libusb_handle, (*LibusbConfig)->bNumInterfaces); + return 0; +} + +static int libusb_udev_control_pipe_request(IUDEVICE* idev, UINT32 RequestId, + UINT32 EndpointAddress, UINT32* UsbdStatus, int command) +{ + int error = 0; + UDEVICE* pdev = (UDEVICE*)idev; + + /* + pdev->request_queue->register_request(pdev->request_queue, RequestId, NULL, 0); + */ + switch (command) + { + case PIPE_CANCEL: + /** cancel bulk or int transfer */ + idev->cancel_all_transfer_request(idev); + // dummy_wait_s_obj(1); + /** set feature to ep (set halt)*/ + error = libusb_control_transfer( + pdev->libusb_handle, LIBUSB_ENDPOINT_OUT | LIBUSB_RECIPIENT_ENDPOINT, + LIBUSB_REQUEST_SET_FEATURE, ENDPOINT_HALT, EndpointAddress, NULL, 0, 1000); + break; + + case PIPE_RESET: + idev->cancel_all_transfer_request(idev); + error = libusb_clear_halt(pdev->libusb_handle, EndpointAddress); + // func_set_usbd_status(pdev, UsbdStatus, error); + break; + + default: + error = -0xff; + break; + } + + *UsbdStatus = 0; + return error; +} + +static UINT32 libusb_udev_control_query_device_text(IUDEVICE* idev, UINT32 TextType, + UINT16 LocaleId, UINT8* BufferSize, + BYTE* Buffer) +{ + UDEVICE* pdev = (UDEVICE*)idev; + LIBUSB_DEVICE_DESCRIPTOR* devDescriptor; + const char strDesc[] = "Generic Usb String"; + char deviceLocation[25] = { 0 }; + BYTE bus_number; + BYTE device_address; + int ret = 0; + size_t i, len; + URBDRC_PLUGIN* urbdrc; + WCHAR* text = (WCHAR*)Buffer; + BYTE slen, locale; + const UINT8 inSize = *BufferSize; + + *BufferSize = 0; + if (!pdev || !pdev->devDescriptor || !pdev->urbdrc) + return ERROR_INVALID_DATA; + + urbdrc = pdev->urbdrc; + devDescriptor = pdev->devDescriptor; + + switch (TextType) + { + case DeviceTextDescription: + { + BYTE data[0x100] = { 0 }; + ret = libusb_get_string_descriptor(pdev->libusb_handle, devDescriptor->iProduct, + LocaleId, data, 0xFF); + /* The returned data in the buffer is: + * 1 byte length of following data + * 1 byte descriptor type, must be 0x03 for strings + * n WCHAR unicode string (of length / 2 characters) including '\0' + */ + slen = data[0]; + locale = data[1]; + + if ((ret <= 0) || (ret <= 4) || (slen <= 4) || (locale != LIBUSB_DT_STRING) || + (ret > UINT8_MAX)) + { + const char* msg = "SHORT_DESCRIPTOR"; + if (ret < 0) + msg = libusb_error_name(ret); + WLog_Print(urbdrc->log, WLOG_DEBUG, + "libusb_get_string_descriptor: " + "%s [%d], iProduct: %" PRIu8 "!", + msg, ret, devDescriptor->iProduct); + + len = MIN(sizeof(strDesc), inSize); + for (i = 0; i < len; i++) + text[i] = (WCHAR)strDesc[i]; + + *BufferSize = (BYTE)(len * 2); + } + else + { + /* ret and slen should be equals, but you never know creativity + * of device manufacturers... + * So also check the string length returned as server side does + * not honor strings with multi '\0' characters well. + */ + const size_t rchar = _wcsnlen((WCHAR*)&data[2], sizeof(data) / 2); + len = MIN((BYTE)ret, slen); + len = MIN(len, inSize); + len = MIN(len, rchar * 2 + sizeof(WCHAR)); + memcpy(Buffer, &data[2], len); + + /* Just as above, the returned WCHAR string should be '\0' + * terminated, but never trust hardware to conform to specs... */ + Buffer[len - 2] = '\0'; + Buffer[len - 1] = '\0'; + *BufferSize = (BYTE)len; + } + } + break; + + case DeviceTextLocationInformation: + bus_number = libusb_get_bus_number(pdev->libusb_dev); + device_address = libusb_get_device_address(pdev->libusb_dev); + sprintf_s(deviceLocation, sizeof(deviceLocation), + "Port_#%04" PRIu8 ".Hub_#%04" PRIu8 "", device_address, bus_number); + + len = strnlen(deviceLocation, + MIN(sizeof(deviceLocation), (inSize > 0) ? inSize - 1U : 0)); + for (i = 0; i < len; i++) + text[i] = (WCHAR)deviceLocation[i]; + text[len++] = '\0'; + *BufferSize = (UINT8)(len * sizeof(WCHAR)); + break; + + default: + WLog_Print(urbdrc->log, WLOG_DEBUG, "Query Text: unknown TextType %" PRIu32 "", + TextType); + return ERROR_INVALID_DATA; + } + + return S_OK; +} + +static int libusb_udev_os_feature_descriptor_request(IUDEVICE* idev, UINT32 RequestId, + BYTE Recipient, BYTE InterfaceNumber, + BYTE Ms_PageIndex, UINT16 Ms_featureDescIndex, + UINT32* UsbdStatus, UINT32* BufferSize, + BYTE* Buffer, int Timeout) +{ + UDEVICE* pdev = (UDEVICE*)idev; + BYTE ms_string_desc[0x13] = { 0 }; + int error = 0; + /* + pdev->request_queue->register_request(pdev->request_queue, RequestId, NULL, 0); + */ + error = libusb_control_transfer(pdev->libusb_handle, LIBUSB_ENDPOINT_IN | Recipient, + LIBUSB_REQUEST_GET_DESCRIPTOR, 0x03ee, 0, ms_string_desc, 0x12, + Timeout); + + log_libusb_result(pdev->urbdrc->log, WLOG_DEBUG, "libusb_control_transfer", error); + + if (error > 0) + { + const BYTE bMS_Vendorcode = ms_string_desc[16]; + /** get os descriptor */ + error = libusb_control_transfer(pdev->libusb_handle, + LIBUSB_ENDPOINT_IN | LIBUSB_REQUEST_TYPE_VENDOR | Recipient, + bMS_Vendorcode, (InterfaceNumber << 8) | Ms_PageIndex, + Ms_featureDescIndex, Buffer, *BufferSize, Timeout); + log_libusb_result(pdev->urbdrc->log, WLOG_DEBUG, "libusb_control_transfer", error); + + if (error >= 0) + *BufferSize = error; + } + + if (error < 0) + *UsbdStatus = USBD_STATUS_STALL_PID; + else + *UsbdStatus = USBD_STATUS_SUCCESS; + + return ERROR_SUCCESS; +} + +static int libusb_udev_query_device_descriptor(IUDEVICE* idev, int offset) +{ + UDEVICE* pdev = (UDEVICE*)idev; + + switch (offset) + { + case B_LENGTH: + return pdev->devDescriptor->bLength; + + case B_DESCRIPTOR_TYPE: + return pdev->devDescriptor->bDescriptorType; + + case BCD_USB: + return pdev->devDescriptor->bcdUSB; + + case B_DEVICE_CLASS: + return pdev->devDescriptor->bDeviceClass; + + case B_DEVICE_SUBCLASS: + return pdev->devDescriptor->bDeviceSubClass; + + case B_DEVICE_PROTOCOL: + return pdev->devDescriptor->bDeviceProtocol; + + case B_MAX_PACKET_SIZE0: + return pdev->devDescriptor->bMaxPacketSize0; + + case ID_VENDOR: + return pdev->devDescriptor->idVendor; + + case ID_PRODUCT: + return pdev->devDescriptor->idProduct; + + case BCD_DEVICE: + return pdev->devDescriptor->bcdDevice; + + case I_MANUFACTURER: + return pdev->devDescriptor->iManufacturer; + + case I_PRODUCT: + return pdev->devDescriptor->iProduct; + + case I_SERIAL_NUMBER: + return pdev->devDescriptor->iSerialNumber; + + case B_NUM_CONFIGURATIONS: + return pdev->devDescriptor->bNumConfigurations; + + default: + return 0; + } + + return 0; +} + +static BOOL libusb_udev_detach_kernel_driver(IUDEVICE* idev) +{ + int i, err = 0; + UDEVICE* pdev = (UDEVICE*)idev; + URBDRC_PLUGIN* urbdrc; + + if (!pdev || !pdev->LibusbConfig || !pdev->libusb_handle || !pdev->urbdrc) + return FALSE; + +#ifdef _WIN32 + return TRUE; +#else + urbdrc = pdev->urbdrc; + + if ((pdev->status & URBDRC_DEVICE_DETACH_KERNEL) == 0) + { + for (i = 0; i < pdev->LibusbConfig->bNumInterfaces; i++) + { + err = libusb_kernel_driver_active(pdev->libusb_handle, i); + log_libusb_result(urbdrc->log, WLOG_DEBUG, "libusb_kernel_driver_active", err); + + if (err) + { + err = libusb_detach_kernel_driver(pdev->libusb_handle, i); + log_libusb_result(urbdrc->log, WLOG_DEBUG, "libusb_detach_kernel_driver", err); + } + } + + pdev->status |= URBDRC_DEVICE_DETACH_KERNEL; + } + + return TRUE; +#endif +} + +static BOOL libusb_udev_attach_kernel_driver(IUDEVICE* idev) +{ + int i, err = 0; + UDEVICE* pdev = (UDEVICE*)idev; + + if (!pdev || !pdev->LibusbConfig || !pdev->libusb_handle || !pdev->urbdrc) + return FALSE; + + for (i = 0; i < pdev->LibusbConfig->bNumInterfaces && err != LIBUSB_ERROR_NO_DEVICE; i++) + { + err = libusb_release_interface(pdev->libusb_handle, i); + + log_libusb_result(pdev->urbdrc->log, WLOG_DEBUG, "libusb_release_interface", err); + +#ifndef _WIN32 + if (err != LIBUSB_ERROR_NO_DEVICE) + { + err = libusb_attach_kernel_driver(pdev->libusb_handle, i); + log_libusb_result(pdev->urbdrc->log, WLOG_DEBUG, "libusb_attach_kernel_driver if=%d", + err, i); + } +#endif + } + + return TRUE; +} + +static int libusb_udev_is_composite_device(IUDEVICE* idev) +{ + UDEVICE* pdev = (UDEVICE*)idev; + return pdev->isCompositeDevice; +} + +static int libusb_udev_is_exist(IUDEVICE* idev) +{ + UDEVICE* pdev = (UDEVICE*)idev; + return (pdev->status & URBDRC_DEVICE_NOT_FOUND) ? 0 : 1; +} + +static int libusb_udev_is_channel_closed(IUDEVICE* idev) +{ + UDEVICE* pdev = (UDEVICE*)idev; + IUDEVMAN* udevman; + if (!pdev || !pdev->urbdrc) + return 1; + + udevman = pdev->urbdrc->udevman; + if (udevman) + { + if (udevman->status & URBDRC_DEVICE_CHANNEL_CLOSED) + return 1; + } + + if (pdev->status & URBDRC_DEVICE_CHANNEL_CLOSED) + return 1; + + return 0; +} + +static int libusb_udev_is_already_send(IUDEVICE* idev) +{ + UDEVICE* pdev = (UDEVICE*)idev; + return (pdev->status & URBDRC_DEVICE_ALREADY_SEND) ? 1 : 0; +} + +/* This is called from channel cleanup code. + * Avoid double free, just remove the device and mark the channel closed. */ +static void libusb_udev_mark_channel_closed(IUDEVICE* idev) +{ + UDEVICE* pdev = (UDEVICE*)idev; + if (pdev && ((pdev->status & URBDRC_DEVICE_CHANNEL_CLOSED) == 0)) + { + URBDRC_PLUGIN* urbdrc = pdev->urbdrc; + const uint8_t busNr = idev->get_bus_number(idev); + const uint8_t devNr = idev->get_dev_number(idev); + + pdev->status |= URBDRC_DEVICE_CHANNEL_CLOSED; + urbdrc->udevman->unregister_udevice(urbdrc->udevman, busNr, devNr); + } +} + +/* This is called by local events where the device is removed or in an error + * state. Remove the device from redirection and close the channel. */ +static void libusb_udev_channel_closed(IUDEVICE* idev) +{ + UDEVICE* pdev = (UDEVICE*)idev; + if (pdev && ((pdev->status & URBDRC_DEVICE_CHANNEL_CLOSED) == 0)) + { + URBDRC_PLUGIN* urbdrc = pdev->urbdrc; + const uint8_t busNr = idev->get_bus_number(idev); + const uint8_t devNr = idev->get_dev_number(idev); + IWTSVirtualChannel* channel = NULL; + + if (pdev->channelManager) + channel = IFCALLRESULT(NULL, pdev->channelManager->FindChannelById, + pdev->channelManager, pdev->channelID); + + pdev->status |= URBDRC_DEVICE_CHANNEL_CLOSED; + + if (channel) + channel->Write(channel, 0, NULL, NULL); + + urbdrc->udevman->unregister_udevice(urbdrc->udevman, busNr, devNr); + } +} + +static void libusb_udev_set_already_send(IUDEVICE* idev) +{ + UDEVICE* pdev = (UDEVICE*)idev; + pdev->status |= URBDRC_DEVICE_ALREADY_SEND; +} + +static char* libusb_udev_get_path(IUDEVICE* idev) +{ + UDEVICE* pdev = (UDEVICE*)idev; + return pdev->path; +} + +static int libusb_udev_query_device_port_status(IUDEVICE* idev, UINT32* UsbdStatus, + UINT32* BufferSize, BYTE* Buffer) +{ + UDEVICE* pdev = (UDEVICE*)idev; + int success = 0, ret; + URBDRC_PLUGIN* urbdrc; + + if (!pdev || !pdev->urbdrc) + return -1; + + urbdrc = pdev->urbdrc; + + if (pdev->hub_handle != NULL) + { + ret = idev->control_transfer( + idev, 0xffff, 0, 0, + LIBUSB_ENDPOINT_IN | LIBUSB_REQUEST_TYPE_CLASS | LIBUSB_RECIPIENT_OTHER, + LIBUSB_REQUEST_GET_STATUS, 0, pdev->port_number, UsbdStatus, BufferSize, Buffer, 1000); + + if (log_libusb_result(urbdrc->log, WLOG_DEBUG, "libusb_control_transfer", ret)) + *BufferSize = 0; + else + { + WLog_Print(urbdrc->log, WLOG_DEBUG, + "PORT STATUS:0x%02" PRIx8 "%02" PRIx8 "%02" PRIx8 "%02" PRIx8 "", Buffer[3], + Buffer[2], Buffer[1], Buffer[0]); + success = 1; + } + } + + return success; +} + +static int libusb_udev_isoch_transfer(IUDEVICE* idev, URBDRC_CHANNEL_CALLBACK* callback, + UINT32 MessageId, UINT32 RequestId, UINT32 EndpointAddress, + UINT32 TransferFlags, UINT32 StartFrame, UINT32 ErrorCount, + BOOL NoAck, const BYTE* packetDescriptorData, + UINT32 NumberOfPackets, UINT32 BufferSize, const BYTE* Buffer, + t_isoch_transfer_cb cb, UINT32 Timeout) +{ + UINT32 iso_packet_size; + UDEVICE* pdev = (UDEVICE*)idev; + ASYNC_TRANSFER_USER_DATA* user_data; + struct libusb_transfer* iso_transfer = NULL; + URBDRC_PLUGIN* urbdrc; + size_t outSize = (NumberOfPackets * 12); + uint32_t streamID = 0x40000000 | RequestId; + + if (!pdev || !pdev->urbdrc) + return -1; + + urbdrc = pdev->urbdrc; + user_data = async_transfer_user_data_new(idev, MessageId, 48, BufferSize, Buffer, + outSize + 1024, NoAck, cb, callback); + + if (!user_data) + return -1; + + user_data->ErrorCount = ErrorCount; + user_data->StartFrame = StartFrame; + + if (!Buffer) + Stream_Seek(user_data->data, (NumberOfPackets * 12)); + + iso_packet_size = BufferSize / NumberOfPackets; + iso_transfer = libusb_alloc_transfer(NumberOfPackets); + + if (iso_transfer == NULL) + { + WLog_Print(urbdrc->log, WLOG_ERROR, "Error: libusb_alloc_transfer."); + async_transfer_user_data_free(user_data); + return -1; + } + + /** process URB_FUNCTION_IOSCH_TRANSFER */ + libusb_fill_iso_transfer(iso_transfer, pdev->libusb_handle, EndpointAddress, + Stream_Pointer(user_data->data), BufferSize, NumberOfPackets, + func_iso_callback, user_data, Timeout); + set_stream_id_for_buffer(iso_transfer, streamID); + libusb_set_iso_packet_lengths(iso_transfer, iso_packet_size); + + if (ArrayList_Add(pdev->request_queue, iso_transfer) < 0) + { + WLog_Print(urbdrc->log, WLOG_WARN, + "Failed to queue iso transfer, streamID %08" PRIx32 " already in use!", + streamID); + request_free(iso_transfer); + return -1; + } + return libusb_submit_transfer(iso_transfer); +} + +static BOOL libusb_udev_control_transfer(IUDEVICE* idev, UINT32 RequestId, UINT32 EndpointAddress, + UINT32 TransferFlags, BYTE bmRequestType, BYTE Request, + UINT16 Value, UINT16 Index, UINT32* UrbdStatus, + UINT32* BufferSize, BYTE* Buffer, UINT32 Timeout) +{ + int status = 0; + UDEVICE* pdev = (UDEVICE*)idev; + + if (!pdev || !pdev->urbdrc) + return FALSE; + + status = libusb_control_transfer(pdev->libusb_handle, bmRequestType, Request, Value, Index, + Buffer, *BufferSize, Timeout); + + if (status >= 0) + *BufferSize = (UINT32)status; + else + log_libusb_result(pdev->urbdrc->log, WLOG_ERROR, "libusb_control_transfer", status); + + if (!func_set_usbd_status(pdev->urbdrc, pdev, UrbdStatus, status)) + return FALSE; + + return TRUE; +} + +static int libusb_udev_bulk_or_interrupt_transfer(IUDEVICE* idev, URBDRC_CHANNEL_CALLBACK* callback, + UINT32 MessageId, UINT32 RequestId, + UINT32 EndpointAddress, UINT32 TransferFlags, + BOOL NoAck, UINT32 BufferSize, const BYTE* data, + t_isoch_transfer_cb cb, UINT32 Timeout) +{ + UINT32 transfer_type; + UDEVICE* pdev = (UDEVICE*)idev; + const LIBUSB_ENDPOINT_DESCEIPTOR* ep_desc; + struct libusb_transfer* transfer = NULL; + URBDRC_PLUGIN* urbdrc; + ASYNC_TRANSFER_USER_DATA* user_data; + uint32_t streamID = 0x80000000 | RequestId; + + if (!pdev || !pdev->LibusbConfig || !pdev->urbdrc) + return -1; + + urbdrc = pdev->urbdrc; + user_data = + async_transfer_user_data_new(idev, MessageId, 36, BufferSize, data, 0, NoAck, cb, callback); + + if (!user_data) + return -1; + + /* alloc memory for urb transfer */ + transfer = libusb_alloc_transfer(0); + if (!transfer) + { + async_transfer_user_data_free(user_data); + return -1; + } + + ep_desc = func_get_ep_desc(pdev->LibusbConfig, pdev->MsConfig, EndpointAddress); + + if (!ep_desc) + { + WLog_Print(urbdrc->log, WLOG_ERROR, "func_get_ep_desc: endpoint 0x%" PRIx32 " not found", + EndpointAddress); + request_free(transfer); + return -1; + } + + transfer_type = (ep_desc->bmAttributes) & 0x3; + WLog_Print(urbdrc->log, WLOG_DEBUG, + "urb_bulk_or_interrupt_transfer: ep:0x%" PRIx32 " " + "transfer_type %" PRIu32 " flag:%" PRIu32 " OutputBufferSize:0x%" PRIx32 "", + EndpointAddress, transfer_type, TransferFlags, BufferSize); + + switch (transfer_type) + { + case BULK_TRANSFER: + /** Bulk Transfer */ + libusb_fill_bulk_transfer(transfer, pdev->libusb_handle, EndpointAddress, + Stream_Pointer(user_data->data), BufferSize, + func_bulk_transfer_cb, user_data, Timeout); + break; + + case INTERRUPT_TRANSFER: + /** Interrupt Transfer */ + libusb_fill_interrupt_transfer(transfer, pdev->libusb_handle, EndpointAddress, + Stream_Pointer(user_data->data), BufferSize, + func_bulk_transfer_cb, user_data, Timeout); + break; + + default: + WLog_Print(urbdrc->log, WLOG_DEBUG, + "urb_bulk_or_interrupt_transfer:" + " other transfer type 0x%" PRIX32 "", + transfer_type); + request_free(transfer); + return -1; + } + + set_stream_id_for_buffer(transfer, streamID); + + if (ArrayList_Add(pdev->request_queue, transfer) < 0) + { + WLog_Print(urbdrc->log, WLOG_WARN, + "Failed to queue transfer, streamID %08" PRIx32 " already in use!", streamID); + request_free(transfer); + return -1; + } + return libusb_submit_transfer(transfer); +} + +static int func_cancel_xact_request(URBDRC_PLUGIN* urbdrc, struct libusb_transfer* transfer) +{ + int status; + + if (!urbdrc || !transfer) + return -1; + + status = libusb_cancel_transfer(transfer); + + if (log_libusb_result(urbdrc->log, WLOG_WARN, "libusb_cancel_transfer", status)) + { + if (status == LIBUSB_ERROR_NOT_FOUND) + return -1; + } + else + return 1; + + return 0; +} + +static void libusb_udev_cancel_all_transfer_request(IUDEVICE* idev) +{ + UDEVICE* pdev = (UDEVICE*)idev; + int count, x; + + if (!pdev || !pdev->request_queue || !pdev->urbdrc) + return; + + ArrayList_Lock(pdev->request_queue); + count = ArrayList_Count(pdev->request_queue); + + for (x = 0; x < count; x++) + { + struct libusb_transfer* transfer = ArrayList_GetItem(pdev->request_queue, x); + func_cancel_xact_request(pdev->urbdrc, transfer); + } + + ArrayList_Unlock(pdev->request_queue); +} + +static int libusb_udev_cancel_transfer_request(IUDEVICE* idev, UINT32 RequestId) +{ + int rc = -1; + UDEVICE* pdev = (UDEVICE*)idev; + struct libusb_transfer* transfer; + uint32_t cancelID1 = 0x40000000 | RequestId; + uint32_t cancelID2 = 0x80000000 | RequestId; + + if (!idev || !pdev->urbdrc || !pdev->request_queue) + return -1; + + ArrayList_Lock(pdev->request_queue); + transfer = list_contains(pdev->request_queue, cancelID1); + if (!transfer) + transfer = list_contains(pdev->request_queue, cancelID2); + + if (transfer) + { + URBDRC_PLUGIN* urbdrc = (URBDRC_PLUGIN*)pdev->urbdrc; + + rc = func_cancel_xact_request(urbdrc, transfer); + } + ArrayList_Unlock(pdev->request_queue); + return rc; +} + +BASIC_STATE_FUNC_DEFINED(channelManager, IWTSVirtualChannelManager*) +BASIC_STATE_FUNC_DEFINED(channelID, UINT32) +BASIC_STATE_FUNC_DEFINED(ReqCompletion, UINT32) +BASIC_STATE_FUNC_DEFINED(bus_number, BYTE) +BASIC_STATE_FUNC_DEFINED(dev_number, BYTE) +BASIC_STATE_FUNC_DEFINED(port_number, int) +BASIC_STATE_FUNC_DEFINED(MsConfig, MSUSB_CONFIG_DESCRIPTOR*) + +BASIC_POINT_FUNC_DEFINED(udev, void*) +BASIC_POINT_FUNC_DEFINED(prev, void*) +BASIC_POINT_FUNC_DEFINED(next, void*) + +static UINT32 udev_get_UsbDevice(IUDEVICE* idev) +{ + UDEVICE* pdev = (UDEVICE*)idev; + + if (!pdev) + return 0; + + return pdev->UsbDevice; +} + +static void udev_set_UsbDevice(IUDEVICE* idev, UINT32 val) +{ + UDEVICE* pdev = (UDEVICE*)idev; + + if (!pdev) + return; + + pdev->UsbDevice = val; +} + +static void udev_free(IUDEVICE* idev) +{ + int rc; + UDEVICE* udev = (UDEVICE*)idev; + URBDRC_PLUGIN* urbdrc; + + if (!idev || !udev->urbdrc) + return; + + urbdrc = udev->urbdrc; + + libusb_udev_cancel_all_transfer_request(&udev->iface); + if (udev->libusb_handle) + { + rc = libusb_reset_device(udev->libusb_handle); + + log_libusb_result(urbdrc->log, WLOG_ERROR, "libusb_reset_device", rc); + } + + /* release all interface and attach kernel driver */ + udev->iface.attach_kernel_driver(idev); + ArrayList_Free(udev->request_queue); + /* free the config descriptor that send from windows */ + msusb_msconfig_free(udev->MsConfig); + libusb_unref_device(udev->libusb_dev); + libusb_close(udev->libusb_handle); + libusb_close(udev->hub_handle); + free(udev->devDescriptor); + free(idev); +} + +static void udev_load_interface(UDEVICE* pdev) +{ + /* load interface */ + /* Basic */ + BASIC_STATE_FUNC_REGISTER(channelManager, pdev); + BASIC_STATE_FUNC_REGISTER(channelID, pdev); + BASIC_STATE_FUNC_REGISTER(UsbDevice, pdev); + BASIC_STATE_FUNC_REGISTER(ReqCompletion, pdev); + BASIC_STATE_FUNC_REGISTER(bus_number, pdev); + BASIC_STATE_FUNC_REGISTER(dev_number, pdev); + BASIC_STATE_FUNC_REGISTER(port_number, pdev); + BASIC_STATE_FUNC_REGISTER(MsConfig, pdev); + BASIC_STATE_FUNC_REGISTER(p_udev, pdev); + BASIC_STATE_FUNC_REGISTER(p_prev, pdev); + BASIC_STATE_FUNC_REGISTER(p_next, pdev); + pdev->iface.isCompositeDevice = libusb_udev_is_composite_device; + pdev->iface.isExist = libusb_udev_is_exist; + pdev->iface.isAlreadySend = libusb_udev_is_already_send; + pdev->iface.isChannelClosed = libusb_udev_is_channel_closed; + pdev->iface.setAlreadySend = libusb_udev_set_already_send; + pdev->iface.setChannelClosed = libusb_udev_channel_closed; + pdev->iface.markChannelClosed = libusb_udev_mark_channel_closed; + pdev->iface.getPath = libusb_udev_get_path; + /* Transfer */ + pdev->iface.isoch_transfer = libusb_udev_isoch_transfer; + pdev->iface.control_transfer = libusb_udev_control_transfer; + pdev->iface.bulk_or_interrupt_transfer = libusb_udev_bulk_or_interrupt_transfer; + pdev->iface.select_interface = libusb_udev_select_interface; + pdev->iface.select_configuration = libusb_udev_select_configuration; + pdev->iface.complete_msconfig_setup = libusb_udev_complete_msconfig_setup; + pdev->iface.control_pipe_request = libusb_udev_control_pipe_request; + pdev->iface.control_query_device_text = libusb_udev_control_query_device_text; + pdev->iface.os_feature_descriptor_request = libusb_udev_os_feature_descriptor_request; + pdev->iface.cancel_all_transfer_request = libusb_udev_cancel_all_transfer_request; + pdev->iface.cancel_transfer_request = libusb_udev_cancel_transfer_request; + pdev->iface.query_device_descriptor = libusb_udev_query_device_descriptor; + pdev->iface.detach_kernel_driver = libusb_udev_detach_kernel_driver; + pdev->iface.attach_kernel_driver = libusb_udev_attach_kernel_driver; + pdev->iface.query_device_port_status = libusb_udev_query_device_port_status; + pdev->iface.free = udev_free; +} + +static int udev_get_device_handle(URBDRC_PLUGIN* urbdrc, libusb_context* ctx, UDEVICE* pdev, + UINT16 bus_number, UINT16 dev_number) +{ + int error; + ssize_t i, total_device; + uint8_t port_numbers[16]; + LIBUSB_DEVICE** libusb_list; + total_device = libusb_get_device_list(ctx, &libusb_list); + /* Look for device. */ + error = -1; + + for (i = 0; i < total_device; i++) + { + LIBUSB_DEVICE* dev = libusb_list[i]; + + if ((bus_number != libusb_get_bus_number(dev)) || + (dev_number != libusb_get_device_address(dev))) + continue; + + error = libusb_open(dev, &pdev->libusb_handle); + + if (log_libusb_result(urbdrc->log, WLOG_ERROR, "libusb_open", error)) + break; + + /* get port number */ + error = libusb_get_port_numbers(dev, port_numbers, sizeof(port_numbers)); + + if (error < 1) + { + /* Prevent open hub, treat as error. */ + log_libusb_result(urbdrc->log, WLOG_ERROR, "libusb_get_port_numbers", error); + break; + } + + pdev->port_number = port_numbers[(error - 1)]; + error = 0; + WLog_Print(urbdrc->log, WLOG_DEBUG, " Port: %d", pdev->port_number); + /* gen device path */ + sprintf(pdev->path, "%" PRIu16 "-%" PRIu16 "", bus_number, pdev->port_number); + + WLog_Print(urbdrc->log, WLOG_DEBUG, " DevPath: %s", pdev->path); + break; + } + libusb_free_device_list(libusb_list, 1); + + if (error < 0) + return -1; + return 0; +} + +static int udev_get_hub_handle(URBDRC_PLUGIN* urbdrc, libusb_context* ctx, UDEVICE* pdev, + UINT16 bus_number, UINT16 dev_number) +{ + int error; + ssize_t i, total_device; + LIBUSB_DEVICE** libusb_list; + LIBUSB_DEVICE_HANDLE* handle; + total_device = libusb_get_device_list(ctx, &libusb_list); + + /* Look for device hub. */ + error = -1; + + for (i = 0; i < total_device; i++) + { + LIBUSB_DEVICE* dev = libusb_list[i]; + + if ((bus_number != libusb_get_bus_number(dev)) || + (1 != libusb_get_device_address(dev))) /* Root hub allways first on bus. */ + continue; + + WLog_Print(urbdrc->log, WLOG_DEBUG, " Open hub: %" PRIu16 "", bus_number); + error = libusb_open(dev, &handle); + + if (!log_libusb_result(urbdrc->log, WLOG_ERROR, "libusb_open", error)) + pdev->hub_handle = handle; + + break; + } + + libusb_free_device_list(libusb_list, 1); + + if (error < 0) + return -1; + + return 0; +} + +static void request_free(void* value) +{ + ASYNC_TRANSFER_USER_DATA* user_data; + struct libusb_transfer* transfer = (struct libusb_transfer*)value; + if (!transfer) + return; + + user_data = (ASYNC_TRANSFER_USER_DATA*)transfer->user_data; + async_transfer_user_data_free(user_data); + transfer->user_data = NULL; + libusb_free_transfer(transfer); +} + +static IUDEVICE* udev_init(URBDRC_PLUGIN* urbdrc, libusb_context* context, LIBUSB_DEVICE* device, + BYTE bus_number, BYTE dev_number) +{ + UDEVICE* pdev; + int status = LIBUSB_ERROR_OTHER; + LIBUSB_DEVICE_DESCRIPTOR* devDescriptor; + LIBUSB_CONFIG_DESCRIPTOR* config_temp; + LIBUSB_INTERFACE_DESCRIPTOR interface_temp; + pdev = (PUDEVICE)calloc(1, sizeof(UDEVICE)); + + if (!pdev) + return NULL; + + pdev->urbdrc = urbdrc; + udev_load_interface(pdev); + + if (device) + pdev->libusb_dev = device; + else + pdev->libusb_dev = udev_get_libusb_dev(context, bus_number, dev_number); + + if (pdev->libusb_dev == NULL) + goto fail; + + if (urbdrc->listener_callback) + udev_set_channelManager(&pdev->iface, urbdrc->listener_callback->channel_mgr); + + /* Get DEVICE handle */ + status = udev_get_device_handle(urbdrc, context, pdev, bus_number, dev_number); + if (status != LIBUSB_SUCCESS) + { + struct libusb_device_descriptor desc; + const uint8_t port = libusb_get_port_number(pdev->libusb_dev); + libusb_get_device_descriptor(pdev->libusb_dev, &desc); + + log_libusb_result(urbdrc->log, WLOG_ERROR, + "libusb_open [b=0x%02X,p=0x%02X,a=0x%02X,VID=0x%04X,PID=0x%04X]", status, + bus_number, port, dev_number, desc.idVendor, desc.idProduct); + goto fail; + } + + /* Get HUB handle */ + status = udev_get_hub_handle(urbdrc, context, pdev, bus_number, dev_number); + + if (status < 0) + pdev->hub_handle = NULL; + + pdev->devDescriptor = udev_new_descript(urbdrc, pdev->libusb_dev); + + if (!pdev->devDescriptor) + goto fail; + + status = libusb_get_active_config_descriptor(pdev->libusb_dev, &pdev->LibusbConfig); + + if (status == LIBUSB_ERROR_NOT_FOUND) + status = libusb_get_config_descriptor(pdev->libusb_dev, 0, &pdev->LibusbConfig); + + if (status < 0) + goto fail; + + config_temp = pdev->LibusbConfig; + /* get the first interface and first altsetting */ + interface_temp = config_temp->interface[0].altsetting[0]; + WLog_Print(urbdrc->log, WLOG_DEBUG, + "Registered Device: Vid: 0x%04" PRIX16 " Pid: 0x%04" PRIX16 "" + " InterfaceClass = %s", + pdev->devDescriptor->idVendor, pdev->devDescriptor->idProduct, + usb_interface_class_to_string(interface_temp.bInterfaceClass)); + /* Check composite device */ + devDescriptor = pdev->devDescriptor; + + if ((devDescriptor->bNumConfigurations == 1) && (config_temp->bNumInterfaces > 1) && + (devDescriptor->bDeviceClass == LIBUSB_CLASS_PER_INTERFACE)) + { + pdev->isCompositeDevice = 1; + } + else if ((devDescriptor->bDeviceClass == 0xef) && + (devDescriptor->bDeviceSubClass == LIBUSB_CLASS_COMM) && + (devDescriptor->bDeviceProtocol == 0x01)) + { + pdev->isCompositeDevice = 1; + } + else + pdev->isCompositeDevice = 0; + + /* set device class to first interface class */ + devDescriptor->bDeviceClass = interface_temp.bInterfaceClass; + devDescriptor->bDeviceSubClass = interface_temp.bInterfaceSubClass; + devDescriptor->bDeviceProtocol = interface_temp.bInterfaceProtocol; + /* initialize pdev */ + pdev->bus_number = bus_number; + pdev->dev_number = dev_number; + pdev->request_queue = ArrayList_New(TRUE); + + if (!pdev->request_queue) + goto fail; + + ArrayList_Object(pdev->request_queue)->fnObjectFree = request_free; + + /* set config of windows */ + pdev->MsConfig = msusb_msconfig_new(); + + if (!pdev->MsConfig) + goto fail; + + // deb_config_msg(pdev->libusb_dev, config_temp, devDescriptor->bNumConfigurations); + return &pdev->iface; +fail: + pdev->iface.free(&pdev->iface); + return NULL; +} + +size_t udev_new_by_id(URBDRC_PLUGIN* urbdrc, libusb_context* ctx, UINT16 idVendor, UINT16 idProduct, + IUDEVICE*** devArray) +{ + LIBUSB_DEVICE** libusb_list; + UDEVICE** array; + ssize_t i, total_device; + size_t num = 0; + + if (!urbdrc || !devArray) + return 0; + + WLog_Print(urbdrc->log, WLOG_INFO, "VID: 0x%04" PRIX16 ", PID: 0x%04" PRIX16 "", idVendor, + idProduct); + total_device = libusb_get_device_list(ctx, &libusb_list); + array = (UDEVICE**)calloc(total_device, sizeof(UDEVICE*)); + + if (!array) + goto fail; + + for (i = 0; i < total_device; i++) + { + LIBUSB_DEVICE* dev = libusb_list[i]; + LIBUSB_DEVICE_DESCRIPTOR* descriptor = udev_new_descript(urbdrc, dev); + + if ((descriptor->idVendor == idVendor) && (descriptor->idProduct == idProduct)) + { + array[num] = (PUDEVICE)udev_init(urbdrc, ctx, dev, libusb_get_bus_number(dev), + libusb_get_device_address(dev)); + + if (array[num] != NULL) + num++; + } + else + { + libusb_unref_device(dev); + } + + free(descriptor); + } + +fail: + libusb_free_device_list(libusb_list, 0); + *devArray = (IUDEVICE**)array; + return num; +} + +IUDEVICE* udev_new_by_addr(URBDRC_PLUGIN* urbdrc, libusb_context* context, BYTE bus_number, + BYTE dev_number) +{ + WLog_Print(urbdrc->log, WLOG_DEBUG, "bus:%d dev:%d", bus_number, dev_number); + return udev_init(urbdrc, context, NULL, bus_number, dev_number); +} diff --git a/channels/urbdrc/client/libusb/libusb_udevice.h b/channels/urbdrc/client/libusb/libusb_udevice.h new file mode 100644 index 0000000..a5f836b --- /dev/null +++ b/channels/urbdrc/client/libusb/libusb_udevice.h @@ -0,0 +1,78 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * RemoteFX USB Redirection + * + * Copyright 2012 Atrust corp. + * Copyright 2012 Alfred Liu + * + * Licensed 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 FREERDP_CHANNEL_URBDRC_CLIENT_LIBUSB_UDEVICE_H +#define FREERDP_CHANNEL_URBDRC_CLIENT_LIBUSB_UDEVICE_H + +#include +#include + +#include "urbdrc_types.h" +#include "urbdrc_main.h" + +typedef struct libusb_device LIBUSB_DEVICE; +typedef struct libusb_device_handle LIBUSB_DEVICE_HANDLE; +typedef struct libusb_device_descriptor LIBUSB_DEVICE_DESCRIPTOR; +typedef struct libusb_config_descriptor LIBUSB_CONFIG_DESCRIPTOR; +typedef struct libusb_interface LIBUSB_INTERFACE; +typedef struct libusb_interface_descriptor LIBUSB_INTERFACE_DESCRIPTOR; +typedef struct libusb_endpoint_descriptor LIBUSB_ENDPOINT_DESCEIPTOR; + +typedef struct _UDEVICE UDEVICE; + +struct _UDEVICE +{ + IUDEVICE iface; + + void* udev; + void* prev; + void* next; + + UINT32 UsbDevice; /* An unique interface ID */ + UINT32 ReqCompletion; /* An unique interface ID */ + IWTSVirtualChannelManager* channelManager; + UINT32 channelID; + UINT16 status; + BYTE bus_number; + BYTE dev_number; + char path[17]; + int port_number; + int isCompositeDevice; + + LIBUSB_DEVICE_HANDLE* libusb_handle; + LIBUSB_DEVICE_HANDLE* hub_handle; + LIBUSB_DEVICE* libusb_dev; + LIBUSB_DEVICE_DESCRIPTOR* devDescriptor; + MSUSB_CONFIG_DESCRIPTOR* MsConfig; + LIBUSB_CONFIG_DESCRIPTOR* LibusbConfig; + + wArrayList* request_queue; + + URBDRC_PLUGIN* urbdrc; +}; +typedef UDEVICE* PUDEVICE; + +size_t udev_new_by_id(URBDRC_PLUGIN* urbdrc, libusb_context* ctx, UINT16 idVendor, UINT16 idProduct, + IUDEVICE*** devArray); +IUDEVICE* udev_new_by_addr(URBDRC_PLUGIN* urbdrc, libusb_context* ctx, BYTE bus_number, + BYTE dev_number); +const char* usb_interface_class_to_string(uint8_t class); + +#endif /* FREERDP_CHANNEL_URBDRC_CLIENT_LIBUSB_UDEVICE_H */ diff --git a/channels/urbdrc/client/libusb/libusb_udevman.c b/channels/urbdrc/client/libusb/libusb_udevman.c new file mode 100644 index 0000000..5f1e9e0 --- /dev/null +++ b/channels/urbdrc/client/libusb/libusb_udevman.c @@ -0,0 +1,975 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * RemoteFX USB Redirection + * + * Copyright 2012 Atrust corp. + * Copyright 2012 Alfred Liu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include + +#include +#include +#include + +#include + +#include "urbdrc_types.h" +#include "urbdrc_main.h" + +#include "libusb_udevice.h" + +#include + +#if !defined(LIBUSB_HOTPLUG_NO_FLAGS) +#define LIBUSB_HOTPLUG_NO_FLAGS 0 +#endif + +#define BASIC_STATE_FUNC_DEFINED(_arg, _type) \ + static _type udevman_get_##_arg(IUDEVMAN* idevman) \ + { \ + UDEVMAN* udevman = (UDEVMAN*)idevman; \ + return udevman->_arg; \ + } \ + static void udevman_set_##_arg(IUDEVMAN* idevman, _type _t) \ + { \ + UDEVMAN* udevman = (UDEVMAN*)idevman; \ + udevman->_arg = _t; \ + } + +#define BASIC_STATE_FUNC_REGISTER(_arg, _man) \ + _man->iface.get_##_arg = udevman_get_##_arg; \ + _man->iface.set_##_arg = udevman_set_##_arg + +typedef struct _VID_PID_PAIR VID_PID_PAIR; + +struct _VID_PID_PAIR +{ + UINT16 vid; + UINT16 pid; +}; + +typedef struct _UDEVMAN UDEVMAN; + +struct _UDEVMAN +{ + IUDEVMAN iface; + + IUDEVICE* idev; /* iterator device */ + IUDEVICE* head; /* head device in linked list */ + IUDEVICE* tail; /* tail device in linked list */ + + LPSTR devices_vid_pid; + LPSTR devices_addr; + wArrayList* hotplug_vid_pids; + UINT16 flags; + UINT32 device_num; + UINT32 next_device_id; + UINT32 channel_id; + + HANDLE devman_loading; + libusb_context* context; + HANDLE thread; + BOOL running; +}; +typedef UDEVMAN* PUDEVMAN; + +static BOOL poll_libusb_events(UDEVMAN* udevman); + +static void udevman_rewind(IUDEVMAN* idevman) +{ + UDEVMAN* udevman = (UDEVMAN*)idevman; + udevman->idev = udevman->head; +} + +static BOOL udevman_has_next(IUDEVMAN* idevman) +{ + UDEVMAN* udevman = (UDEVMAN*)idevman; + + if (!udevman || !udevman->idev) + return FALSE; + else + return TRUE; +} + +static IUDEVICE* udevman_get_next(IUDEVMAN* idevman) +{ + UDEVMAN* udevman = (UDEVMAN*)idevman; + IUDEVICE* pdev; + pdev = udevman->idev; + udevman->idev = (IUDEVICE*)((UDEVICE*)udevman->idev)->next; + return pdev; +} + +static IUDEVICE* udevman_get_udevice_by_addr(IUDEVMAN* idevman, BYTE bus_number, BYTE dev_number) +{ + IUDEVICE* dev = NULL; + + if (!idevman) + return NULL; + + idevman->loading_lock(idevman); + idevman->rewind(idevman); + + while (idevman->has_next(idevman)) + { + IUDEVICE* pdev = idevman->get_next(idevman); + + if ((pdev->get_bus_number(pdev) == bus_number) && + (pdev->get_dev_number(pdev) == dev_number)) + { + dev = pdev; + break; + } + } + + idevman->loading_unlock(idevman); + return dev; +} + +static size_t udevman_register_udevice(IUDEVMAN* idevman, BYTE bus_number, BYTE dev_number, + UINT16 idVendor, UINT16 idProduct, UINT32 flag) +{ + UDEVMAN* udevman = (UDEVMAN*)idevman; + IUDEVICE* pdev = NULL; + IUDEVICE** devArray; + URBDRC_PLUGIN* urbdrc; + size_t i, num, addnum = 0; + + if (!idevman || !idevman->plugin) + return 0; + + urbdrc = (URBDRC_PLUGIN*)idevman->plugin; + pdev = (IUDEVICE*)udevman_get_udevice_by_addr(idevman, bus_number, dev_number); + + if (pdev != NULL) + return 0; + + if (flag & UDEVMAN_FLAG_ADD_BY_ADDR) + { + UINT32 id; + IUDEVICE* tdev = udev_new_by_addr(urbdrc, udevman->context, bus_number, dev_number); + + if (tdev == NULL) + return 0; + + id = idevman->get_next_device_id(idevman); + tdev->set_UsbDevice(tdev, id); + idevman->loading_lock(idevman); + + if (udevman->head == NULL) + { + /* linked list is empty */ + udevman->head = tdev; + udevman->tail = tdev; + } + else + { + /* append device to the end of the linked list */ + udevman->tail->set_p_next(udevman->tail, tdev); + tdev->set_p_prev(tdev, udevman->tail); + udevman->tail = tdev; + } + + udevman->device_num += 1; + idevman->loading_unlock(idevman); + } + else if (flag & UDEVMAN_FLAG_ADD_BY_VID_PID) + { + addnum = 0; + /* register all device that match pid vid */ + num = udev_new_by_id(urbdrc, udevman->context, idVendor, idProduct, &devArray); + + if (num == 0) + { + WLog_Print(urbdrc->log, WLOG_WARN, + "Could not find or redirect any usb devices by id %04x:%04x", idVendor, idProduct); + } + + for (i = 0; i < num; i++) + { + UINT32 id; + IUDEVICE* tdev = devArray[i]; + + if (udevman_get_udevice_by_addr(idevman, tdev->get_bus_number(tdev), + tdev->get_dev_number(tdev)) != NULL) + { + tdev->free(tdev); + devArray[i] = NULL; + continue; + } + + id = idevman->get_next_device_id(idevman); + tdev->set_UsbDevice(tdev, id); + idevman->loading_lock(idevman); + + if (udevman->head == NULL) + { + /* linked list is empty */ + udevman->head = tdev; + udevman->tail = tdev; + } + else + { + /* append device to the end of the linked list */ + udevman->tail->set_p_next(udevman->tail, tdev); + tdev->set_p_prev(tdev, udevman->tail); + udevman->tail = tdev; + } + + udevman->device_num += 1; + idevman->loading_unlock(idevman); + addnum++; + } + + free(devArray); + return addnum; + } + else + { + WLog_Print(urbdrc->log, WLOG_ERROR, "udevman_register_udevice: Invalid flag=%08 " PRIx32, + flag); + return 0; + } + + return 1; +} + +static BOOL udevman_unregister_udevice(IUDEVMAN* idevman, BYTE bus_number, BYTE dev_number) +{ + UDEVMAN* udevman = (UDEVMAN*)idevman; + UDEVICE* pdev; + UDEVICE* dev = (UDEVICE*)udevman_get_udevice_by_addr(idevman, bus_number, dev_number); + + if (!dev || !idevman) + return FALSE; + + idevman->loading_lock(idevman); + idevman->rewind(idevman); + + while (idevman->has_next(idevman)) + { + pdev = (UDEVICE*)idevman->get_next(idevman); + + if (pdev == dev) /* device exists */ + { + /* set previous device to point to next device */ + if (dev->prev != NULL) + { + /* unregistered device is not the head */ + pdev = dev->prev; + pdev->next = dev->next; + } + else + { + /* unregistered device is the head, update head */ + udevman->head = (IUDEVICE*)dev->next; + } + + /* set next device to point to previous device */ + + if (dev->next != NULL) + { + /* unregistered device is not the tail */ + pdev = (UDEVICE*)dev->next; + pdev->prev = dev->prev; + } + else + { + /* unregistered device is the tail, update tail */ + udevman->tail = (IUDEVICE*)dev->prev; + } + + udevman->device_num--; + break; + } + } + + idevman->loading_unlock(idevman); + + if (dev) + { + dev->iface.free(&dev->iface); + return TRUE; /* unregistration successful */ + } + + /* if we reach this point, the device wasn't found */ + return FALSE; +} + +static BOOL udevman_unregister_all_udevices(IUDEVMAN* idevman) +{ + UDEVMAN* udevman = (UDEVMAN*)idevman; + + if (!idevman) + return FALSE; + + if (!udevman->head) + return TRUE; + + idevman->loading_lock(idevman); + idevman->rewind(idevman); + + while (idevman->has_next(idevman)) + { + UDEVICE* dev = (UDEVICE*)idevman->get_next(idevman); + + if (!dev) + continue; + + /* set previous device to point to next device */ + if (dev->prev != NULL) + { + /* unregistered device is not the head */ + UDEVICE* pdev = dev->prev; + pdev->next = dev->next; + } + else + { + /* unregistered device is the head, update head */ + udevman->head = (IUDEVICE*)dev->next; + } + + /* set next device to point to previous device */ + + if (dev->next != NULL) + { + /* unregistered device is not the tail */ + UDEVICE* pdev = (UDEVICE*)dev->next; + pdev->prev = dev->prev; + } + else + { + /* unregistered device is the tail, update tail */ + udevman->tail = (IUDEVICE*)dev->prev; + } + + dev->iface.free(&dev->iface); + udevman->device_num--; + } + + idevman->loading_unlock(idevman); + + return TRUE; +} + +static int udevman_is_auto_add(IUDEVMAN* idevman) +{ + UDEVMAN* udevman = (UDEVMAN*)idevman; + return (udevman->flags & UDEVMAN_FLAG_ADD_BY_AUTO) ? 1 : 0; +} + +static IUDEVICE* udevman_get_udevice_by_UsbDevice(IUDEVMAN* idevman, UINT32 UsbDevice) +{ + UDEVICE* pdev; + URBDRC_PLUGIN* urbdrc; + + if (!idevman || !idevman->plugin) + return NULL; + + /* Mask highest 2 bits, must be ignored */ + UsbDevice = UsbDevice & INTERFACE_ID_MASK; + urbdrc = (URBDRC_PLUGIN*)idevman->plugin; + idevman->loading_lock(idevman); + idevman->rewind(idevman); + + while (idevman->has_next(idevman)) + { + pdev = (UDEVICE*)idevman->get_next(idevman); + + if (pdev->UsbDevice == UsbDevice) + { + idevman->loading_unlock(idevman); + return (IUDEVICE*)pdev; + } + } + + idevman->loading_unlock(idevman); + WLog_Print(urbdrc->log, WLOG_WARN, "Failed to find a USB device mapped to deviceId=%08" PRIx32, + UsbDevice); + return NULL; +} + +static IUDEVICE* udevman_get_udevice_by_ChannelID(IUDEVMAN* idevman, UINT32 channelID) +{ + UDEVICE* pdev; + URBDRC_PLUGIN* urbdrc; + + if (!idevman || !idevman->plugin) + return NULL; + + /* Mask highest 2 bits, must be ignored */ + urbdrc = (URBDRC_PLUGIN*)idevman->plugin; + idevman->loading_lock(idevman); + idevman->rewind(idevman); + + while (idevman->has_next(idevman)) + { + pdev = (UDEVICE*)idevman->get_next(idevman); + + if (pdev->channelID == channelID) + { + idevman->loading_unlock(idevman); + return (IUDEVICE*)pdev; + } + } + + idevman->loading_unlock(idevman); + WLog_Print(urbdrc->log, WLOG_WARN, "Failed to find a USB device mapped to channelID=%08" PRIx32, + channelID); + return NULL; +} + +static void udevman_loading_lock(IUDEVMAN* idevman) +{ + UDEVMAN* udevman = (UDEVMAN*)idevman; + WaitForSingleObject(udevman->devman_loading, INFINITE); +} + +static void udevman_loading_unlock(IUDEVMAN* idevman) +{ + UDEVMAN* udevman = (UDEVMAN*)idevman; + ReleaseMutex(udevman->devman_loading); +} + +BASIC_STATE_FUNC_DEFINED(device_num, UINT32) + +static UINT32 udevman_get_next_device_id(IUDEVMAN* idevman) +{ + UDEVMAN* udevman = (UDEVMAN*)idevman; + return udevman->next_device_id++; +} + +static void udevman_set_next_device_id(IUDEVMAN* idevman, UINT32 _t) +{ + UDEVMAN* udevman = (UDEVMAN*)idevman; + udevman->next_device_id = _t; +} + +static void udevman_free(IUDEVMAN* idevman) +{ + UDEVMAN* udevman = (UDEVMAN*)idevman; + + if (!udevman) + return; + + udevman->running = FALSE; + if (udevman->thread) + { + WaitForSingleObject(udevman->thread, INFINITE); + CloseHandle(udevman->thread); + } + + udevman_unregister_all_udevices(idevman); + + if (udevman->devman_loading) + CloseHandle(udevman->devman_loading); + + libusb_exit(udevman->context); + + ArrayList_Free(udevman->hotplug_vid_pids); + free(udevman); +} + +static BOOL filter_by_class(uint8_t bDeviceClass, uint8_t bDeviceSubClass) +{ + switch (bDeviceClass) + { + case LIBUSB_CLASS_AUDIO: + case LIBUSB_CLASS_HID: + case LIBUSB_CLASS_MASS_STORAGE: + case LIBUSB_CLASS_HUB: + case LIBUSB_CLASS_SMART_CARD: + return TRUE; + default: + break; + } + + switch (bDeviceSubClass) + { + default: + break; + } + + return FALSE; +} + +static BOOL append(char* dst, size_t length, const char* src) +{ + return winpr_str_append(src, dst, length, NULL); +} + +static BOOL device_is_filtered(struct libusb_device* dev, + const struct libusb_device_descriptor* desc, + libusb_hotplug_event event) +{ + char buffer[8192] = { 0 }; + char* what; + BOOL filtered = FALSE; + append(buffer, sizeof(buffer), usb_interface_class_to_string(desc->bDeviceClass)); + if (filter_by_class(desc->bDeviceClass, desc->bDeviceSubClass)) + filtered = TRUE; + + switch (desc->bDeviceClass) + { + case LIBUSB_CLASS_PER_INTERFACE: + { + struct libusb_config_descriptor* config = NULL; + int rc = libusb_get_active_config_descriptor(dev, &config); + if (rc == LIBUSB_SUCCESS) + { + uint8_t x; + + for (x = 0; x < config->bNumInterfaces; x++) + { + int y; + const struct libusb_interface* ifc = &config->interface[x]; + for (y = 0; y < ifc->num_altsetting; y++) + { + const struct libusb_interface_descriptor* const alt = &ifc->altsetting[y]; + if (filter_by_class(alt->bInterfaceClass, alt->bInterfaceSubClass)) + filtered = TRUE; + + append(buffer, sizeof(buffer), "|"); + append(buffer, sizeof(buffer), + usb_interface_class_to_string(alt->bInterfaceClass)); + } + } + } + libusb_free_config_descriptor(config); + } + break; + default: + break; + } + + if (filtered) + what = "Filtered"; + else + { + switch (event) + { + case LIBUSB_HOTPLUG_EVENT_DEVICE_LEFT: + what = "Hotplug remove"; + break; + case LIBUSB_HOTPLUG_EVENT_DEVICE_ARRIVED: + what = "Hotplug add"; + break; + default: + what = "Hotplug unknown"; + break; + } + } + + WLog_DBG(TAG, "%s device VID=0x%04X,PID=0x%04X class %s", what, desc->idVendor, desc->idProduct, + buffer); + return filtered; +} + +static int hotplug_callback(struct libusb_context* ctx, struct libusb_device* dev, + libusb_hotplug_event event, void* user_data) +{ + VID_PID_PAIR pair; + struct libusb_device_descriptor desc; + UDEVMAN* udevman = (UDEVMAN*)user_data; + const uint8_t bus = libusb_get_bus_number(dev); + const uint8_t addr = libusb_get_device_address(dev); + int rc = libusb_get_device_descriptor(dev, &desc); + + WINPR_UNUSED(ctx); + + if (rc != LIBUSB_SUCCESS) + return rc; + + switch (event) + { + case LIBUSB_HOTPLUG_EVENT_DEVICE_ARRIVED: + pair.vid = desc.idVendor; + pair.pid = desc.idProduct; + if ((ArrayList_Contains(udevman->hotplug_vid_pids, &pair)) || + (udevman->iface.isAutoAdd(&udevman->iface) && + !device_is_filtered(dev, &desc, event))) + { + add_device(&udevman->iface, DEVICE_ADD_FLAG_ALL, bus, addr, desc.idVendor, + desc.idProduct); + } + break; + + case LIBUSB_HOTPLUG_EVENT_DEVICE_LEFT: + del_device(&udevman->iface, DEVICE_ADD_FLAG_ALL, bus, addr, desc.idVendor, + desc.idProduct); + break; + + default: + break; + } + + return 0; +} + +static BOOL udevman_initialize(IUDEVMAN* idevman, UINT32 channelId) +{ + UDEVMAN* udevman = (UDEVMAN*)idevman; + + if (!udevman) + return FALSE; + + idevman->status &= ~URBDRC_DEVICE_CHANNEL_CLOSED; + idevman->controlChannelId = channelId; + return TRUE; +} + +static BOOL udevman_vid_pid_pair_equals(const void* objA, const void* objB) +{ + const VID_PID_PAIR* a = objA; + const VID_PID_PAIR* b = objB; + + return (a->vid == b->vid) && (a->pid == b->pid); +} + +static BOOL udevman_parse_device_id_addr(const char** str, UINT16* id1, UINT16* id2, UINT16 max, + char split_sign, char delimiter) +{ + char* mid; + char* end; + unsigned long rc; + + rc = strtoul(*str, &mid, 16); + + if ((mid == *str) || (*mid != split_sign) || (rc > max)) + return FALSE; + + *id1 = (UINT16)rc; + rc = strtoul(++mid, &end, 16); + + if ((end == mid) || (rc > max)) + return FALSE; + + *id2 = (UINT16)rc; + + *str += end - *str; + if (*end == '\0') + return TRUE; + if (*end == delimiter) + { + (*str)++; + return TRUE; + } + + return FALSE; +} + +static BOOL urbdrc_udevman_register_devices(UDEVMAN* udevman, const char* devices, BOOL add_by_addr) +{ + const char* pos = devices; + VID_PID_PAIR* idpair; + UINT16 id1, id2; + + while (*pos != '\0') + { + if (!udevman_parse_device_id_addr(&pos, &id1, &id2, (add_by_addr) ? UINT8_MAX : UINT16_MAX, + ':', '#')) + { + WLog_ERR(TAG, "Invalid device argument: \"%s\"", devices); + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + } + + if (add_by_addr) + { + add_device(&udevman->iface, DEVICE_ADD_FLAG_BUS | DEVICE_ADD_FLAG_DEV, (UINT8)id1, + (UINT8)id2, 0, 0); + } + else + { + idpair = calloc(1, sizeof(VID_PID_PAIR)); + if (!idpair) + return CHANNEL_RC_NO_MEMORY; + idpair->vid = id1; + idpair->pid = id2; + if (ArrayList_Add(udevman->hotplug_vid_pids, idpair) == -1) + { + free(idpair); + return CHANNEL_RC_NO_MEMORY; + } + + add_device(&udevman->iface, DEVICE_ADD_FLAG_VENDOR | DEVICE_ADD_FLAG_PRODUCT, 0, 0, id1, + id2); + } + } + + return CHANNEL_RC_OK; +} + +static UINT urbdrc_udevman_parse_addin_args(UDEVMAN* udevman, ADDIN_ARGV* args) +{ + int status; + LPSTR devices = NULL; + COMMAND_LINE_ARGUMENT_A* arg; + COMMAND_LINE_ARGUMENT_A urbdrc_udevman_args[] = { + { "dbg", COMMAND_LINE_VALUE_FLAG, "", NULL, BoolValueFalse, -1, NULL, "debug" }, + { "dev", COMMAND_LINE_VALUE_REQUIRED, "", NULL, NULL, -1, NULL, "device list" }, + { "id", COMMAND_LINE_VALUE_OPTIONAL, "", NULL, BoolValueFalse, -1, NULL, + "FLAG_ADD_BY_VID_PID" }, + { "addr", COMMAND_LINE_VALUE_OPTIONAL, "", NULL, BoolValueFalse, -1, NULL, + "FLAG_ADD_BY_ADDR" }, + { "auto", COMMAND_LINE_VALUE_FLAG, "", NULL, BoolValueFalse, -1, NULL, "FLAG_ADD_BY_AUTO" }, + { NULL, 0, NULL, NULL, NULL, -1, NULL, NULL } + }; + + status = CommandLineParseArgumentsA(args->argc, args->argv, urbdrc_udevman_args, + COMMAND_LINE_SIGIL_NONE | COMMAND_LINE_SEPARATOR_COLON, + udevman, NULL, NULL); + + if (status != CHANNEL_RC_OK) + return status; + + arg = urbdrc_udevman_args; + + do + { + if (!(arg->Flags & COMMAND_LINE_ARGUMENT_PRESENT)) + continue; + + CommandLineSwitchStart(arg) CommandLineSwitchCase(arg, "dbg") + { + WLog_SetLogLevel(WLog_Get(TAG), WLOG_TRACE); + } + CommandLineSwitchCase(arg, "dev") + { + devices = arg->Value; + } + CommandLineSwitchCase(arg, "id") + { + if (arg->Value) + udevman->devices_vid_pid = arg->Value; + else + udevman->flags = UDEVMAN_FLAG_ADD_BY_VID_PID; + } + CommandLineSwitchCase(arg, "addr") + { + if (arg->Value) + udevman->devices_addr = arg->Value; + else + udevman->flags = UDEVMAN_FLAG_ADD_BY_ADDR; + } + CommandLineSwitchCase(arg, "auto") + { + udevman->flags |= UDEVMAN_FLAG_ADD_BY_AUTO; + } + CommandLineSwitchDefault(arg) + { + } + CommandLineSwitchEnd(arg) + } while ((arg = CommandLineFindNextArgumentA(arg)) != NULL); + + if (devices) + { + if (udevman->flags & UDEVMAN_FLAG_ADD_BY_VID_PID) + udevman->devices_vid_pid = devices; + else if (udevman->flags & UDEVMAN_FLAG_ADD_BY_ADDR) + udevman->devices_addr = devices; + } + + return CHANNEL_RC_OK; +} + +static UINT udevman_listener_created_callback(IUDEVMAN* iudevman) +{ + UINT status; + UDEVMAN* udevman = (UDEVMAN*)iudevman; + + if (udevman->devices_vid_pid) + { + status = urbdrc_udevman_register_devices(udevman, udevman->devices_vid_pid, FALSE); + if (status != CHANNEL_RC_OK) + return status; + } + + if (udevman->devices_addr) + return urbdrc_udevman_register_devices(udevman, udevman->devices_addr, TRUE); + + return CHANNEL_RC_OK; +} + +static void udevman_load_interface(UDEVMAN* udevman) +{ + /* standard */ + udevman->iface.free = udevman_free; + /* manage devices */ + udevman->iface.rewind = udevman_rewind; + udevman->iface.get_next = udevman_get_next; + udevman->iface.has_next = udevman_has_next; + udevman->iface.register_udevice = udevman_register_udevice; + udevman->iface.unregister_udevice = udevman_unregister_udevice; + udevman->iface.get_udevice_by_UsbDevice = udevman_get_udevice_by_UsbDevice; + udevman->iface.get_udevice_by_ChannelID = udevman_get_udevice_by_ChannelID; + /* Extension */ + udevman->iface.isAutoAdd = udevman_is_auto_add; + /* Basic state */ + BASIC_STATE_FUNC_REGISTER(device_num, udevman); + BASIC_STATE_FUNC_REGISTER(next_device_id, udevman); + + /* control semaphore or mutex lock */ + udevman->iface.loading_lock = udevman_loading_lock; + udevman->iface.loading_unlock = udevman_loading_unlock; + udevman->iface.initialize = udevman_initialize; + udevman->iface.listener_created_callback = udevman_listener_created_callback; +} + +static BOOL poll_libusb_events(UDEVMAN* udevman) +{ + int rc = LIBUSB_SUCCESS; + struct timeval tv = { 0, 500 }; + if (libusb_try_lock_events(udevman->context) == 0) + { + if (libusb_event_handling_ok(udevman->context)) + { + rc = libusb_handle_events_locked(udevman->context, &tv); + if (rc != LIBUSB_SUCCESS) + WLog_WARN(TAG, "libusb_handle_events_locked %d", rc); + } + libusb_unlock_events(udevman->context); + } + else + { + libusb_lock_event_waiters(udevman->context); + if (libusb_event_handler_active(udevman->context)) + { + rc = libusb_wait_for_event(udevman->context, &tv); + if (rc < LIBUSB_SUCCESS) + WLog_WARN(TAG, "libusb_wait_for_event %d", rc); + } + libusb_unlock_event_waiters(udevman->context); + } + + return rc > 0; +} + +static DWORD WINAPI poll_thread(LPVOID lpThreadParameter) +{ + libusb_hotplug_callback_handle handle; + UDEVMAN* udevman = (UDEVMAN*)lpThreadParameter; + BOOL hasHotplug = libusb_has_capability(LIBUSB_CAP_HAS_HOTPLUG); + + if (hasHotplug) + { + int rc = libusb_hotplug_register_callback( + udevman->context, + LIBUSB_HOTPLUG_EVENT_DEVICE_ARRIVED | LIBUSB_HOTPLUG_EVENT_DEVICE_LEFT, + LIBUSB_HOTPLUG_NO_FLAGS, LIBUSB_HOTPLUG_MATCH_ANY, LIBUSB_HOTPLUG_MATCH_ANY, + LIBUSB_HOTPLUG_MATCH_ANY, hotplug_callback, udevman, &handle); + + if (rc != LIBUSB_SUCCESS) + udevman->running = FALSE; + } + else + WLog_WARN(TAG, "Platform does not support libusb hotplug. USB devices plugged in later " + "will not be detected."); + + while (udevman->running) + { + poll_libusb_events(udevman); + } + + if (hasHotplug) + libusb_hotplug_deregister_callback(udevman->context, handle); + + /* Process remaining usb events */ + while (poll_libusb_events(udevman)) + ; + + ExitThread(0); + return 0; +} + +#ifdef BUILTIN_CHANNELS +#define freerdp_urbdrc_client_subsystem_entry libusb_freerdp_urbdrc_client_subsystem_entry +#else +#define freerdp_urbdrc_client_subsystem_entry FREERDP_API freerdp_urbdrc_client_subsystem_entry +#endif +UINT freerdp_urbdrc_client_subsystem_entry(PFREERDP_URBDRC_SERVICE_ENTRY_POINTS pEntryPoints) +{ + UINT rc; + UINT status; + UDEVMAN* udevman; + ADDIN_ARGV* args = pEntryPoints->args; + udevman = (PUDEVMAN)calloc(1, sizeof(UDEVMAN)); + + if (!udevman) + goto fail; + + udevman->hotplug_vid_pids = ArrayList_New(TRUE); + if (!udevman->hotplug_vid_pids) + goto fail; + ArrayList_Object(udevman->hotplug_vid_pids)->fnObjectFree = free; + ArrayList_Object(udevman->hotplug_vid_pids)->fnObjectEquals = udevman_vid_pid_pair_equals; + + udevman->next_device_id = BASE_USBDEVICE_NUM; + udevman->iface.plugin = pEntryPoints->plugin; + rc = libusb_init(&udevman->context); + + if (rc != LIBUSB_SUCCESS) + goto fail; + +#ifdef _WIN32 +#if LIBUSB_API_VERSION >= 0x01000106 + /* Prefer usbDK backend on windows. Not supported on other platforms. */ + rc = libusb_set_option(udevman->context, LIBUSB_OPTION_USE_USBDK); + switch (rc) + { + case LIBUSB_SUCCESS: + break; + case LIBUSB_ERROR_NOT_FOUND: + case LIBUSB_ERROR_NOT_SUPPORTED: + WLog_WARN(TAG, "LIBUSB_OPTION_USE_USBDK %s [%d]", libusb_strerror(rc), rc); + break; + default: + WLog_ERR(TAG, "LIBUSB_OPTION_USE_USBDK %s [%d]", libusb_strerror(rc), rc); + goto fail; + } +#endif +#endif + + udevman->flags = UDEVMAN_FLAG_ADD_BY_VID_PID; + udevman->devman_loading = CreateMutexA(NULL, FALSE, "devman_loading"); + + if (!udevman->devman_loading) + goto fail; + + /* load usb device service management */ + udevman_load_interface(udevman); + status = urbdrc_udevman_parse_addin_args(udevman, args); + + if (status != CHANNEL_RC_OK) + goto fail; + + udevman->running = TRUE; + udevman->thread = CreateThread(NULL, 0, poll_thread, udevman, 0, NULL); + + if (!udevman->thread) + goto fail; + + if (!pEntryPoints->pRegisterUDEVMAN(pEntryPoints->plugin, (IUDEVMAN*)udevman)) + goto fail; + + WLog_DBG(TAG, "UDEVMAN device registered."); + return 0; +fail: + udevman_free(&udevman->iface); + return ERROR_INTERNAL_ERROR; +} diff --git a/channels/urbdrc/client/urbdrc_main.c b/channels/urbdrc/client/urbdrc_main.c new file mode 100644 index 0000000..1683634 --- /dev/null +++ b/channels/urbdrc/client/urbdrc_main.c @@ -0,0 +1,1017 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * RemoteFX USB Redirection + * + * Copyright 2012 Atrust corp. + * Copyright 2012 Alfred Liu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "urbdrc_types.h" +#include "urbdrc_main.h" +#include "data_transfer.h" + +#include + +static BOOL Stream_Write_UTF16_String_From_Utf8(wStream* s, const char* utf8, size_t len) +{ + BOOL ret; + WCHAR* utf16; + int rc; + + if (len > INT_MAX) + return FALSE; + + rc = ConvertToUnicode(CP_UTF8, 0, utf8, (int)len, &utf16, 0); + + if (rc < 0) + return FALSE; + + ret = Stream_Write_UTF16_String(s, utf16, (size_t)rc); + free(utf16); + return ret; +} + +static IWTSVirtualChannel* get_channel(IUDEVMAN* idevman) +{ + IWTSVirtualChannelManager* channel_mgr; + URBDRC_PLUGIN* urbdrc; + + if (!idevman) + return NULL; + + urbdrc = (URBDRC_PLUGIN*)idevman->plugin; + + if (!urbdrc || !urbdrc->listener_callback) + return NULL; + + channel_mgr = urbdrc->listener_callback->channel_mgr; + + if (!channel_mgr) + return NULL; + + return channel_mgr->FindChannelById(channel_mgr, idevman->controlChannelId); +} + +static int func_container_id_generate(IUDEVICE* pdev, char* strContainerId) +{ + char *p, *path; + UINT8 containerId[17] = { 0 }; + UINT16 idVendor, idProduct; + idVendor = (UINT16)pdev->query_device_descriptor(pdev, ID_VENDOR); + idProduct = (UINT16)pdev->query_device_descriptor(pdev, ID_PRODUCT); + path = pdev->getPath(pdev); + + if (strlen(path) > 8) + p = (path + strlen(path)) - 8; + else + p = path; + + sprintf_s((char*)containerId, sizeof(containerId), "%04" PRIX16 "%04" PRIX16 "%s", idVendor, + idProduct, p); + /* format */ + sprintf_s(strContainerId, DEVICE_CONTAINER_STR_SIZE, + "{%02" PRIx8 "%02" PRIx8 "%02" PRIx8 "%02" PRIx8 "-%02" PRIx8 "%02" PRIx8 "-%02" PRIx8 + "%02" PRIx8 "-%02" PRIx8 "%02" PRIx8 "-%02" PRIx8 "%02" PRIx8 "%02" PRIx8 "%02" PRIx8 + "%02" PRIx8 "%02" PRIx8 "}", + containerId[0], containerId[1], containerId[2], containerId[3], containerId[4], + containerId[5], containerId[6], containerId[7], containerId[8], containerId[9], + containerId[10], containerId[11], containerId[12], containerId[13], containerId[14], + containerId[15]); + return 0; +} + +static int func_instance_id_generate(IUDEVICE* pdev, char* strInstanceId, size_t len) +{ + char instanceId[17] = { 0 }; + sprintf_s(instanceId, sizeof(instanceId), "\\%s", pdev->getPath(pdev)); + /* format */ + sprintf_s(strInstanceId, len, + "%02" PRIx8 "%02" PRIx8 "%02" PRIx8 "%02" PRIx8 "-%02" PRIx8 "%02" PRIx8 "-%02" PRIx8 + "%02" PRIx8 "-%02" PRIx8 "%02" PRIx8 "-%02" PRIx8 "%02" PRIx8 "%02" PRIx8 "%02" PRIx8 + "%02" PRIx8 "%02" PRIx8 "", + instanceId[0], instanceId[1], instanceId[2], instanceId[3], instanceId[4], + instanceId[5], instanceId[6], instanceId[7], instanceId[8], instanceId[9], + instanceId[10], instanceId[11], instanceId[12], instanceId[13], instanceId[14], + instanceId[15]); + return 0; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT urbdrc_process_capability_request(URBDRC_CHANNEL_CALLBACK* callback, wStream* s, + UINT32 MessageId) +{ + UINT32 InterfaceId; + UINT32 Version; + UINT32 out_size; + wStream* out; + + if (!callback || !s) + return ERROR_INVALID_PARAMETER; + + if (Stream_GetRemainingLength(s) < 4) + return ERROR_INVALID_DATA; + + Stream_Read_UINT32(s, Version); + + if (Version > RIM_CAPABILITY_VERSION_01) + Version = RIM_CAPABILITY_VERSION_01; + + InterfaceId = ((STREAM_ID_NONE << 30) | CAPABILITIES_NEGOTIATOR); + out_size = 16; + out = Stream_New(NULL, out_size); + + if (!out) + return ERROR_OUTOFMEMORY; + + Stream_Write_UINT32(out, InterfaceId); /* interface id */ + Stream_Write_UINT32(out, MessageId); /* message id */ + Stream_Write_UINT32(out, Version); /* usb protocol version */ + Stream_Write_UINT32(out, 0x00000000); /* HRESULT */ + return stream_write_and_free(callback->plugin, callback->channel, out); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT urbdrc_process_channel_create(URBDRC_CHANNEL_CALLBACK* callback, wStream* s, + UINT32 MessageId) +{ + UINT32 InterfaceId; + UINT32 out_size; + UINT32 MajorVersion; + UINT32 MinorVersion; + UINT32 Capabilities; + wStream* out; + URBDRC_PLUGIN* urbdrc; + + if (!callback || !s || !callback->plugin) + return ERROR_INVALID_PARAMETER; + + urbdrc = (URBDRC_PLUGIN*)callback->plugin; + + if (Stream_GetRemainingLength(s) < 12) + return ERROR_INVALID_DATA; + + Stream_Read_UINT32(s, MajorVersion); + Stream_Read_UINT32(s, MinorVersion); + Stream_Read_UINT32(s, Capabilities); + + /* Version check, we only support version 1.0 */ + if ((MajorVersion != 1) || (MinorVersion != 0)) + { + WLog_Print(urbdrc->log, WLOG_WARN, + "server supports USB channel version %" PRIu32 ".%" PRIu32); + WLog_Print(urbdrc->log, WLOG_WARN, "we only support channel version 1.0"); + MajorVersion = 1; + MinorVersion = 0; + } + + InterfaceId = ((STREAM_ID_PROXY << 30) | CLIENT_CHANNEL_NOTIFICATION); + out_size = 24; + out = Stream_New(NULL, out_size); + + if (!out) + return ERROR_OUTOFMEMORY; + + Stream_Write_UINT32(out, InterfaceId); /* interface id */ + Stream_Write_UINT32(out, MessageId); /* message id */ + Stream_Write_UINT32(out, CHANNEL_CREATED); /* function id */ + Stream_Write_UINT32(out, MajorVersion); + Stream_Write_UINT32(out, MinorVersion); + Stream_Write_UINT32(out, Capabilities); /* capabilities version */ + return stream_write_and_free(callback->plugin, callback->channel, out); +} + +static UINT urdbrc_send_virtual_channel_add(IWTSPlugin* plugin, IWTSVirtualChannel* channel, + UINT32 MessageId) +{ + const UINT32 InterfaceId = ((STREAM_ID_PROXY << 30) | CLIENT_DEVICE_SINK); + wStream* out = Stream_New(NULL, 12); + + if (!out) + return ERROR_OUTOFMEMORY; + + Stream_Write_UINT32(out, InterfaceId); /* interface */ + Stream_Write_UINT32(out, MessageId); /* message id */ + Stream_Write_UINT32(out, ADD_VIRTUAL_CHANNEL); /* function id */ + return stream_write_and_free(plugin, channel, out); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT urdbrc_send_usb_device_add(URBDRC_CHANNEL_CALLBACK* callback, IUDEVICE* pdev) +{ + wStream* out; + UINT32 InterfaceId; + char HardwareIds[2][DEVICE_HARDWARE_ID_SIZE] = { { 0 } }; + char CompatibilityIds[3][DEVICE_COMPATIBILITY_ID_SIZE] = { { 0 } }; + char strContainerId[DEVICE_CONTAINER_STR_SIZE] = { 0 }; + char strInstanceId[DEVICE_INSTANCE_STR_SIZE] = { 0 }; + const char* composite_str = "USB\\COMPOSITE"; + const size_t composite_len = 13; + size_t size; + size_t CompatibilityIdLen[3]; + size_t HardwareIdsLen[2]; + size_t ContainerIdLen, InstanceIdLen; + size_t cchCompatIds; + UINT32 bcdUSB; + InterfaceId = ((STREAM_ID_PROXY << 30) | CLIENT_DEVICE_SINK); + /* USB kernel driver detach!! */ + pdev->detach_kernel_driver(pdev); + { + const UINT16 idVendor = (UINT16)pdev->query_device_descriptor(pdev, ID_VENDOR); + const UINT16 idProduct = (UINT16)pdev->query_device_descriptor(pdev, ID_PRODUCT); + const UINT16 bcdDevice = (UINT16)pdev->query_device_descriptor(pdev, BCD_DEVICE); + sprintf_s(HardwareIds[1], DEVICE_HARDWARE_ID_SIZE, + "USB\\VID_%04" PRIX16 "&PID_%04" PRIX16 "", idVendor, idProduct); + sprintf_s(HardwareIds[0], DEVICE_HARDWARE_ID_SIZE, + "USB\\VID_%04" PRIX16 "&PID_%04" PRIX16 "&REV_%04" PRIX16 "", idVendor, idProduct, + bcdDevice); + } + { + const UINT8 bDeviceClass = (UINT8)pdev->query_device_descriptor(pdev, B_DEVICE_CLASS); + const UINT8 bDeviceSubClass = (UINT8)pdev->query_device_descriptor(pdev, B_DEVICE_SUBCLASS); + const UINT8 bDeviceProtocol = (UINT8)pdev->query_device_descriptor(pdev, B_DEVICE_PROTOCOL); + + if (!(pdev->isCompositeDevice(pdev))) + { + sprintf_s(CompatibilityIds[2], DEVICE_COMPATIBILITY_ID_SIZE, "USB\\Class_%02" PRIX8 "", + bDeviceClass); + sprintf_s(CompatibilityIds[1], DEVICE_COMPATIBILITY_ID_SIZE, + "USB\\Class_%02" PRIX8 "&SubClass_%02" PRIX8 "", bDeviceClass, + bDeviceSubClass); + sprintf_s(CompatibilityIds[0], DEVICE_COMPATIBILITY_ID_SIZE, + "USB\\Class_%02" PRIX8 "&SubClass_%02" PRIX8 "&Prot_%02" PRIX8 "", + bDeviceClass, bDeviceSubClass, bDeviceProtocol); + } + else + { + sprintf_s(CompatibilityIds[2], DEVICE_COMPATIBILITY_ID_SIZE, "USB\\DevClass_00"); + sprintf_s(CompatibilityIds[1], DEVICE_COMPATIBILITY_ID_SIZE, + "USB\\DevClass_00&SubClass_00"); + sprintf_s(CompatibilityIds[0], DEVICE_COMPATIBILITY_ID_SIZE, + "USB\\DevClass_00&SubClass_00&Prot_00"); + } + } + func_instance_id_generate(pdev, strInstanceId, DEVICE_INSTANCE_STR_SIZE); + func_container_id_generate(pdev, strContainerId); + CompatibilityIdLen[0] = strnlen(CompatibilityIds[0], sizeof(CompatibilityIds[0])); + CompatibilityIdLen[1] = strnlen(CompatibilityIds[1], sizeof(CompatibilityIds[1])); + CompatibilityIdLen[2] = strnlen(CompatibilityIds[2], sizeof(CompatibilityIds[2])); + HardwareIdsLen[0] = strnlen(HardwareIds[0], sizeof(HardwareIds[0])); + HardwareIdsLen[1] = strnlen(HardwareIds[1], sizeof(HardwareIds[1])); + cchCompatIds = + CompatibilityIdLen[0] + 1 + CompatibilityIdLen[1] + 1 + CompatibilityIdLen[2] + 2; + InstanceIdLen = strnlen(strInstanceId, sizeof(strInstanceId)); + ContainerIdLen = strnlen(strContainerId, sizeof(strContainerId)); + + if (pdev->isCompositeDevice(pdev)) + cchCompatIds += composite_len + 1; + + size = 24; + size += (InstanceIdLen + 1) * 2 + (HardwareIdsLen[0] + 1) * 2 + 4 + + (HardwareIdsLen[1] + 1) * 2 + 2 + 4 + (cchCompatIds)*2 + (ContainerIdLen + 1) * 2 + 4 + + 28; + out = Stream_New(NULL, size); + + if (!out) + return ERROR_OUTOFMEMORY; + + Stream_Write_UINT32(out, InterfaceId); /* interface */ + Stream_Write_UINT32(out, 0); + Stream_Write_UINT32(out, ADD_DEVICE); /* function id */ + Stream_Write_UINT32(out, 0x00000001); /* NumUsbDevice */ + Stream_Write_UINT32(out, pdev->get_UsbDevice(pdev)); /* UsbDevice */ + Stream_Write_UINT32(out, (UINT32)InstanceIdLen + 1); /* cchDeviceInstanceId */ + Stream_Write_UTF16_String_From_Utf8(out, strInstanceId, InstanceIdLen); + Stream_Write_UINT16(out, 0); + Stream_Write_UINT32(out, HardwareIdsLen[0] + HardwareIdsLen[1] + 3); /* cchHwIds */ + /* HardwareIds 1 */ + Stream_Write_UTF16_String_From_Utf8(out, HardwareIds[0], HardwareIdsLen[0]); + Stream_Write_UINT16(out, 0); + Stream_Write_UTF16_String_From_Utf8(out, HardwareIds[1], HardwareIdsLen[1]); + Stream_Write_UINT16(out, 0); + Stream_Write_UINT16(out, 0); /* add "\0" */ + Stream_Write_UINT32(out, (UINT32)cchCompatIds); /* cchCompatIds */ + /* CompatibilityIds */ + Stream_Write_UTF16_String_From_Utf8(out, CompatibilityIds[0], CompatibilityIdLen[0]); + Stream_Write_UINT16(out, 0); + Stream_Write_UTF16_String_From_Utf8(out, CompatibilityIds[1], CompatibilityIdLen[1]); + Stream_Write_UINT16(out, 0); + Stream_Write_UTF16_String_From_Utf8(out, CompatibilityIds[2], CompatibilityIdLen[2]); + Stream_Write_UINT16(out, 0); + + if (pdev->isCompositeDevice(pdev)) + { + Stream_Write_UTF16_String_From_Utf8(out, composite_str, composite_len); + Stream_Write_UINT16(out, 0); + } + + Stream_Write_UINT16(out, 0x0000); /* add "\0" */ + Stream_Write_UINT32(out, (UINT32)ContainerIdLen + 1); /* cchContainerId */ + /* ContainerId */ + Stream_Write_UTF16_String_From_Utf8(out, strContainerId, ContainerIdLen); + Stream_Write_UINT16(out, 0); + /* USB_DEVICE_CAPABILITIES 28 bytes */ + Stream_Write_UINT32(out, 0x0000001c); /* CbSize */ + Stream_Write_UINT32(out, 2); /* UsbBusInterfaceVersion, 0 ,1 or 2 */ // TODO: Get from libusb + Stream_Write_UINT32(out, 0x600); /* USBDI_Version, 0x500 or 0x600 */ // TODO: Get from libusb + /* Supported_USB_Version, 0x110,0x110 or 0x200(usb2.0) */ + bcdUSB = pdev->query_device_descriptor(pdev, BCD_USB); + Stream_Write_UINT32(out, bcdUSB); + Stream_Write_UINT32(out, 0x00000000); /* HcdCapabilities, MUST always be zero */ + + if (bcdUSB < 0x200) + Stream_Write_UINT32(out, 0x00000000); /* DeviceIsHighSpeed */ + else + Stream_Write_UINT32(out, 0x00000001); /* DeviceIsHighSpeed */ + + Stream_Write_UINT32(out, 0x50); /* NoAckIsochWriteJitterBufferSizeInMs, >=10 or <=512 */ + return stream_write_and_free(callback->plugin, callback->channel, out); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT urbdrc_exchange_capabilities(URBDRC_CHANNEL_CALLBACK* callback, wStream* data) +{ + UINT32 MessageId; + UINT32 FunctionId; + UINT32 InterfaceId; + UINT error = CHANNEL_RC_OK; + + if (!data) + return ERROR_INVALID_PARAMETER; + + if (Stream_GetRemainingLength(data) < 8) + return ERROR_INVALID_DATA; + + Stream_Rewind_UINT32(data); + Stream_Read_UINT32(data, InterfaceId); + Stream_Read_UINT32(data, MessageId); + Stream_Read_UINT32(data, FunctionId); + + switch (FunctionId) + { + case RIM_EXCHANGE_CAPABILITY_REQUEST: + error = urbdrc_process_capability_request(callback, data, MessageId); + break; + + case RIMCALL_RELEASE: + break; + + default: + error = ERROR_NOT_FOUND; + break; + } + + return error; +} + +static BOOL urbdrc_announce_devices(IUDEVMAN* udevman) +{ + UINT error = ERROR_SUCCESS; + + udevman->loading_lock(udevman); + udevman->rewind(udevman); + + while (udevman->has_next(udevman)) + { + IUDEVICE* pdev = udevman->get_next(udevman); + + if (!pdev->isAlreadySend(pdev)) + { + const UINT32 deviceId = pdev->get_UsbDevice(pdev); + UINT error = + urdbrc_send_virtual_channel_add(udevman->plugin, get_channel(udevman), deviceId); + + if (error != ERROR_SUCCESS) + break; + } + } + + udevman->loading_unlock(udevman); + + return error == ERROR_SUCCESS; +} + +static UINT urbdrc_device_control_channel(URBDRC_CHANNEL_CALLBACK* callback, wStream* s) +{ + URBDRC_PLUGIN* urbdrc = (URBDRC_PLUGIN*)callback->plugin; + IUDEVMAN* udevman = urbdrc->udevman; + IWTSVirtualChannel* channel = callback->channel; + IUDEVICE* pdev = NULL; + BOOL found = FALSE; + UINT error = ERROR_INTERNAL_ERROR; + UINT32 channelId = callback->channel_mgr->GetChannelId(channel); + + switch (urbdrc->vchannel_status) + { + case INIT_CHANNEL_IN: + /* Control channel was established */ + error = ERROR_SUCCESS; + udevman->initialize(udevman, channelId); + + if (!urbdrc_announce_devices(udevman)) + goto fail; + + urbdrc->vchannel_status = INIT_CHANNEL_OUT; + break; + + case INIT_CHANNEL_OUT: + /* A new device channel was created, add the channel + * to the device */ + udevman->loading_lock(udevman); + udevman->rewind(udevman); + + while (udevman->has_next(udevman)) + { + pdev = udevman->get_next(udevman); + + if (!pdev->isAlreadySend(pdev)) + { + const UINT32 channelID = callback->channel_mgr->GetChannelId(channel); + found = TRUE; + pdev->setAlreadySend(pdev); + pdev->set_channelManager(pdev, callback->channel_mgr); + pdev->set_channelID(pdev, channelID); + break; + } + } + + udevman->loading_unlock(udevman); + error = ERROR_SUCCESS; + + if (found && pdev->isAlreadySend(pdev)) + error = urdbrc_send_usb_device_add(callback, pdev); + + break; + + default: + WLog_Print(urbdrc->log, WLOG_ERROR, "vchannel_status unknown value %" PRIu32 "", + urbdrc->vchannel_status); + break; + } + +fail: + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT urbdrc_process_channel_notification(URBDRC_CHANNEL_CALLBACK* callback, wStream* data) +{ + UINT32 MessageId; + UINT32 FunctionId; + UINT32 InterfaceId; + UINT error = CHANNEL_RC_OK; + URBDRC_PLUGIN* urbdrc; + + if (!callback || !data) + return ERROR_INVALID_PARAMETER; + + urbdrc = (URBDRC_PLUGIN*)callback->plugin; + + if (!urbdrc) + return ERROR_INVALID_PARAMETER; + + if (Stream_GetRemainingLength(data) < 8) + return ERROR_INVALID_DATA; + + Stream_Rewind(data, 4); + Stream_Read_UINT32(data, InterfaceId); + Stream_Read_UINT32(data, MessageId); + Stream_Read_UINT32(data, FunctionId); + WLog_Print(urbdrc->log, WLOG_TRACE, "%s [%" PRIu32 "]", + call_to_string(FALSE, InterfaceId, FunctionId), FunctionId); + + switch (FunctionId) + { + case CHANNEL_CREATED: + error = urbdrc_process_channel_create(callback, data, MessageId); + break; + + case RIMCALL_RELEASE: + error = urbdrc_device_control_channel(callback, data); + break; + + default: + WLog_Print(urbdrc->log, WLOG_TRACE, "%s: unknown FunctionId 0x%" PRIX32 "", + __FUNCTION__, FunctionId); + error = 1; + break; + } + + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT urbdrc_on_data_received(IWTSVirtualChannelCallback* pChannelCallback, wStream* data) +{ + URBDRC_CHANNEL_CALLBACK* callback = (URBDRC_CHANNEL_CALLBACK*)pChannelCallback; + URBDRC_PLUGIN* urbdrc; + IUDEVMAN* udevman; + UINT32 InterfaceId; + UINT error = ERROR_INTERNAL_ERROR; + + if (callback == NULL) + return ERROR_INVALID_PARAMETER; + + if (callback->plugin == NULL) + return error; + + urbdrc = (URBDRC_PLUGIN*)callback->plugin; + + if (urbdrc->udevman == NULL) + return error; + + udevman = (IUDEVMAN*)urbdrc->udevman; + + if (Stream_GetRemainingLength(data) < 12) + return ERROR_INVALID_DATA; + + urbdrc_dump_message(urbdrc->log, FALSE, FALSE, data); + Stream_Read_UINT32(data, InterfaceId); + + /* Need to check InterfaceId and mask values */ + switch (InterfaceId) + { + case CAPABILITIES_NEGOTIATOR | (STREAM_ID_NONE << 30): + error = urbdrc_exchange_capabilities(callback, data); + break; + + case SERVER_CHANNEL_NOTIFICATION | (STREAM_ID_PROXY << 30): + error = urbdrc_process_channel_notification(callback, data); + break; + + default: + error = urbdrc_process_udev_data_transfer(callback, urbdrc, udevman, data); + error = ERROR_SUCCESS; /* Ignore errors, the device may have been unplugged. */ + break; + } + + return error; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT urbdrc_on_close(IWTSVirtualChannelCallback* pChannelCallback) +{ + URBDRC_CHANNEL_CALLBACK* callback = (URBDRC_CHANNEL_CALLBACK*)pChannelCallback; + if (callback) + { + URBDRC_PLUGIN* urbdrc = (URBDRC_PLUGIN*)callback->plugin; + if (urbdrc) + { + IUDEVMAN* udevman = urbdrc->udevman; + if (udevman && callback->channel_mgr) + { + UINT32 control = callback->channel_mgr->GetChannelId(callback->channel); + if (udevman->controlChannelId == control) + udevman->status |= URBDRC_DEVICE_CHANNEL_CLOSED; + else + { /* Need to notify the local backend the device is gone */ + IUDEVICE* pdev = udevman->get_udevice_by_ChannelID(udevman, control); + if (pdev) + pdev->markChannelClosed(pdev); + } + } + } + } + free(callback); + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT urbdrc_on_new_channel_connection(IWTSListenerCallback* pListenerCallback, + IWTSVirtualChannel* pChannel, BYTE* pData, + BOOL* pbAccept, + IWTSVirtualChannelCallback** ppCallback) +{ + URBDRC_LISTENER_CALLBACK* listener_callback = (URBDRC_LISTENER_CALLBACK*)pListenerCallback; + URBDRC_CHANNEL_CALLBACK* callback; + + if (!ppCallback) + return ERROR_INVALID_PARAMETER; + + callback = (URBDRC_CHANNEL_CALLBACK*)calloc(1, sizeof(URBDRC_CHANNEL_CALLBACK)); + + if (!callback) + return ERROR_OUTOFMEMORY; + + callback->iface.OnDataReceived = urbdrc_on_data_received; + callback->iface.OnClose = urbdrc_on_close; + callback->plugin = listener_callback->plugin; + callback->channel_mgr = listener_callback->channel_mgr; + callback->channel = pChannel; + *ppCallback = (IWTSVirtualChannelCallback*)callback; + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT urbdrc_plugin_initialize(IWTSPlugin* pPlugin, IWTSVirtualChannelManager* pChannelMgr) +{ + UINT status; + URBDRC_PLUGIN* urbdrc = (URBDRC_PLUGIN*)pPlugin; + IUDEVMAN* udevman; + char channelName[sizeof(URBDRC_CHANNEL_NAME)] = { URBDRC_CHANNEL_NAME }; + + if (!urbdrc || !urbdrc->udevman) + return ERROR_INVALID_PARAMETER; + + if (urbdrc->initialized) + { + WLog_ERR(TAG, "[%s] channel initialized twice, aborting", URBDRC_CHANNEL_NAME); + return ERROR_INVALID_DATA; + } + udevman = urbdrc->udevman; + urbdrc->listener_callback = + (URBDRC_LISTENER_CALLBACK*)calloc(1, sizeof(URBDRC_LISTENER_CALLBACK)); + + if (!urbdrc->listener_callback) + return CHANNEL_RC_NO_MEMORY; + + urbdrc->listener_callback->iface.OnNewChannelConnection = urbdrc_on_new_channel_connection; + urbdrc->listener_callback->plugin = pPlugin; + urbdrc->listener_callback->channel_mgr = pChannelMgr; + + /* [MS-RDPEUSB] 2.1 Transport defines the channel name in uppercase letters */ + CharUpperA(channelName); + status = pChannelMgr->CreateListener(pChannelMgr, channelName, 0, + &urbdrc->listener_callback->iface, &urbdrc->listener); + if (status != CHANNEL_RC_OK) + return status; + + status = CHANNEL_RC_OK; + if (udevman->listener_created_callback) + status = udevman->listener_created_callback(udevman); + + urbdrc->initialized = status == CHANNEL_RC_OK; + return status; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT urbdrc_plugin_terminated(IWTSPlugin* pPlugin) +{ + URBDRC_PLUGIN* urbdrc = (URBDRC_PLUGIN*)pPlugin; + IUDEVMAN* udevman; + + if (!urbdrc) + return ERROR_INVALID_DATA; + if (urbdrc->listener_callback) + { + IWTSVirtualChannelManager* mgr = urbdrc->listener_callback->channel_mgr; + if (mgr) + IFCALL(mgr->DestroyListener, mgr, urbdrc->listener); + } + udevman = urbdrc->udevman; + + if (udevman) + { + udevman->free(udevman); + udevman = NULL; + } + + free(urbdrc->subsystem); + free(urbdrc->listener_callback); + free(urbdrc); + return CHANNEL_RC_OK; +} + +static BOOL urbdrc_register_udevman_addin(IWTSPlugin* pPlugin, IUDEVMAN* udevman) +{ + URBDRC_PLUGIN* urbdrc = (URBDRC_PLUGIN*)pPlugin; + + if (urbdrc->udevman) + { + WLog_Print(urbdrc->log, WLOG_ERROR, "existing device, abort."); + return FALSE; + } + + DEBUG_DVC("device registered."); + urbdrc->udevman = udevman; + return TRUE; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT urbdrc_load_udevman_addin(IWTSPlugin* pPlugin, LPCSTR name, ADDIN_ARGV* args) +{ + URBDRC_PLUGIN* urbdrc = (URBDRC_PLUGIN*)pPlugin; + PFREERDP_URBDRC_DEVICE_ENTRY entry; + FREERDP_URBDRC_SERVICE_ENTRY_POINTS entryPoints; + entry = (PFREERDP_URBDRC_DEVICE_ENTRY)freerdp_load_channel_addin_entry(URBDRC_CHANNEL_NAME, + name, NULL, 0); + + if (!entry) + return ERROR_INVALID_OPERATION; + + entryPoints.plugin = pPlugin; + entryPoints.pRegisterUDEVMAN = urbdrc_register_udevman_addin; + entryPoints.args = args; + + if (entry(&entryPoints) != 0) + { + WLog_Print(urbdrc->log, WLOG_ERROR, "%s entry returns error.", name); + return ERROR_INVALID_OPERATION; + } + + return CHANNEL_RC_OK; +} + +static BOOL urbdrc_set_subsystem(URBDRC_PLUGIN* urbdrc, const char* subsystem) +{ + free(urbdrc->subsystem); + urbdrc->subsystem = _strdup(subsystem); + return (urbdrc->subsystem != NULL); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT urbdrc_process_addin_args(URBDRC_PLUGIN* urbdrc, const ADDIN_ARGV* args) +{ + int status; + COMMAND_LINE_ARGUMENT_A urbdrc_args[] = { + { "dbg", COMMAND_LINE_VALUE_FLAG, "", NULL, BoolValueFalse, -1, NULL, "debug" }, + { "sys", COMMAND_LINE_VALUE_REQUIRED, "", NULL, NULL, -1, NULL, "subsystem" }, + { NULL, 0, NULL, NULL, NULL, -1, NULL, NULL } + }; + + const DWORD flags = + COMMAND_LINE_SIGIL_NONE | COMMAND_LINE_SEPARATOR_COLON | COMMAND_LINE_IGN_UNKNOWN_KEYWORD; + COMMAND_LINE_ARGUMENT_A* arg; + status = + CommandLineParseArgumentsA(args->argc, args->argv, urbdrc_args, flags, urbdrc, NULL, NULL); + + if (status < 0) + return ERROR_INVALID_DATA; + + arg = urbdrc_args; + + do + { + if (!(arg->Flags & COMMAND_LINE_VALUE_PRESENT)) + continue; + + CommandLineSwitchStart(arg) CommandLineSwitchCase(arg, "dbg") + { + WLog_SetLogLevel(urbdrc->log, WLOG_TRACE); + } + CommandLineSwitchCase(arg, "sys") + { + if (!urbdrc_set_subsystem(urbdrc, arg->Value)) + return ERROR_OUTOFMEMORY; + } + CommandLineSwitchDefault(arg) + { + } + CommandLineSwitchEnd(arg) + } while ((arg = CommandLineFindNextArgumentA(arg)) != NULL); + + return CHANNEL_RC_OK; +} + +BOOL add_device(IUDEVMAN* idevman, UINT32 flags, BYTE busnum, BYTE devnum, UINT16 idVendor, + UINT16 idProduct) +{ + size_t success = 0; + URBDRC_PLUGIN* urbdrc; + UINT32 mask, regflags = 0; + + if (!idevman) + return FALSE; + + urbdrc = (URBDRC_PLUGIN*)idevman->plugin; + + if (!urbdrc || !urbdrc->listener_callback) + return FALSE; + + mask = (DEVICE_ADD_FLAG_VENDOR | DEVICE_ADD_FLAG_PRODUCT); + if ((flags & mask) == mask) + regflags |= UDEVMAN_FLAG_ADD_BY_VID_PID; + mask = (DEVICE_ADD_FLAG_BUS | DEVICE_ADD_FLAG_DEV); + if ((flags & mask) == mask) + regflags |= UDEVMAN_FLAG_ADD_BY_ADDR; + + success = idevman->register_udevice(idevman, busnum, devnum, idVendor, idProduct, regflags); + + if ((success > 0) && (flags & DEVICE_ADD_FLAG_REGISTER)) + { + if (!urbdrc_announce_devices(idevman)) + return FALSE; + } + + return TRUE; +} + +BOOL del_device(IUDEVMAN* idevman, UINT32 flags, BYTE busnum, BYTE devnum, UINT16 idVendor, + UINT16 idProduct) +{ + IUDEVICE* pdev = NULL; + URBDRC_PLUGIN* urbdrc; + + if (!idevman) + return FALSE; + + urbdrc = (URBDRC_PLUGIN*)idevman->plugin; + + if (!urbdrc || !urbdrc->listener_callback) + return FALSE; + + idevman->loading_lock(idevman); + idevman->rewind(idevman); + + while (idevman->has_next(idevman)) + { + BOOL match = TRUE; + IUDEVICE* dev = idevman->get_next(idevman); + + if ((flags & (DEVICE_ADD_FLAG_BUS | DEVICE_ADD_FLAG_DEV | DEVICE_ADD_FLAG_VENDOR | + DEVICE_ADD_FLAG_PRODUCT)) == 0) + match = FALSE; + if (flags & DEVICE_ADD_FLAG_BUS) + { + if (dev->get_bus_number(dev) != busnum) + match = FALSE; + } + if (flags & DEVICE_ADD_FLAG_DEV) + { + if (dev->get_dev_number(dev) != devnum) + match = FALSE; + } + if (flags & DEVICE_ADD_FLAG_VENDOR) + { + int vid = dev->query_device_descriptor(dev, ID_VENDOR); + if (vid != idVendor) + match = FALSE; + } + if (flags & DEVICE_ADD_FLAG_PRODUCT) + { + int pid = dev->query_device_descriptor(dev, ID_PRODUCT); + if (pid != idProduct) + match = FALSE; + } + + if (match) + { + pdev = dev; + break; + } + } + + if (pdev) + pdev->setChannelClosed(pdev); + + idevman->loading_unlock(idevman); + return TRUE; +} +#ifdef BUILTIN_CHANNELS +#define DVCPluginEntry urbdrc_DVCPluginEntry +#else +#define DVCPluginEntry FREERDP_API DVCPluginEntry +#endif + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT DVCPluginEntry(IDRDYNVC_ENTRY_POINTS* pEntryPoints) +{ + UINT status = 0; + ADDIN_ARGV* args; + URBDRC_PLUGIN* urbdrc; + urbdrc = (URBDRC_PLUGIN*)pEntryPoints->GetPlugin(pEntryPoints, URBDRC_CHANNEL_NAME); + args = pEntryPoints->GetPluginData(pEntryPoints); + + if (urbdrc == NULL) + { + urbdrc = (URBDRC_PLUGIN*)calloc(1, sizeof(URBDRC_PLUGIN)); + + if (!urbdrc) + return CHANNEL_RC_NO_MEMORY; + + urbdrc->iface.Initialize = urbdrc_plugin_initialize; + urbdrc->iface.Terminated = urbdrc_plugin_terminated; + urbdrc->vchannel_status = INIT_CHANNEL_IN; + status = + pEntryPoints->RegisterPlugin(pEntryPoints, URBDRC_CHANNEL_NAME, (IWTSPlugin*)urbdrc); + + if (status != CHANNEL_RC_OK) + goto fail; + + urbdrc->log = WLog_Get(TAG); + + if (!urbdrc->log) + goto fail; + } + + status = urbdrc_process_addin_args(urbdrc, args); + + if (status != CHANNEL_RC_OK) + goto fail; + + if (!urbdrc->subsystem && !urbdrc_set_subsystem(urbdrc, "libusb")) + goto fail; + + return urbdrc_load_udevman_addin((IWTSPlugin*)urbdrc, urbdrc->subsystem, args); +fail: + urbdrc_plugin_terminated(&urbdrc->iface); + return status; +} + +UINT stream_write_and_free(IWTSPlugin* plugin, IWTSVirtualChannel* channel, wStream* out) +{ + UINT rc; + URBDRC_PLUGIN* urbdrc = (URBDRC_PLUGIN*)plugin; + + if (!out) + return ERROR_INVALID_PARAMETER; + + if (!channel || !out || !urbdrc) + { + Stream_Free(out, TRUE); + return ERROR_INVALID_PARAMETER; + } + + if (!channel->Write) + { + Stream_Free(out, TRUE); + return ERROR_INTERNAL_ERROR; + } + + urbdrc_dump_message(urbdrc->log, TRUE, TRUE, out); + rc = channel->Write(channel, Stream_GetPosition(out), Stream_Buffer(out), NULL); + Stream_Free(out, TRUE); + return rc; +} diff --git a/channels/urbdrc/client/urbdrc_main.h b/channels/urbdrc/client/urbdrc_main.h new file mode 100644 index 0000000..8335714 --- /dev/null +++ b/channels/urbdrc/client/urbdrc_main.h @@ -0,0 +1,247 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * RemoteFX USB Redirection + * + * Copyright 2012 Atrust corp. + * Copyright 2012 Alfred Liu + * + * Licensed 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 FREERDP_CHANNEL_URBDRC_CLIENT_MAIN_H +#define FREERDP_CHANNEL_URBDRC_CLIENT_MAIN_H + +#include +#include + +#define DEVICE_HARDWARE_ID_SIZE 32 +#define DEVICE_COMPATIBILITY_ID_SIZE 36 +#define DEVICE_INSTANCE_STR_SIZE 37 +#define DEVICE_CONTAINER_STR_SIZE 39 + +#define TAG CHANNELS_TAG("urbdrc.client") +#ifdef WITH_DEBUG_DVC +#define DEBUG_DVC(...) WLog_DBG(TAG, __VA_ARGS__) +#else +#define DEBUG_DVC(...) \ + do \ + { \ + } while (0) +#endif + +typedef struct _IUDEVICE IUDEVICE; +typedef struct _IUDEVMAN IUDEVMAN; + +#define BASIC_DEV_STATE_DEFINED(_arg, _type) \ + _type (*get_##_arg)(IUDEVICE * pdev); \ + void (*set_##_arg)(IUDEVICE * pdev, _type _arg) + +#define BASIC_DEVMAN_STATE_DEFINED(_arg, _type) \ + _type (*get_##_arg)(IUDEVMAN * udevman); \ + void (*set_##_arg)(IUDEVMAN * udevman, _type _arg) + +typedef struct _URBDRC_LISTENER_CALLBACK URBDRC_LISTENER_CALLBACK; + +struct _URBDRC_LISTENER_CALLBACK +{ + IWTSListenerCallback iface; + + IWTSPlugin* plugin; + IWTSVirtualChannelManager* channel_mgr; +}; + +typedef struct _URBDRC_CHANNEL_CALLBACK URBDRC_CHANNEL_CALLBACK; + +struct _URBDRC_CHANNEL_CALLBACK +{ + IWTSVirtualChannelCallback iface; + + IWTSPlugin* plugin; + IWTSVirtualChannelManager* channel_mgr; + IWTSVirtualChannel* channel; +}; + +typedef struct _URBDRC_PLUGIN URBDRC_PLUGIN; + +struct _URBDRC_PLUGIN +{ + IWTSPlugin iface; + + URBDRC_LISTENER_CALLBACK* listener_callback; + + IUDEVMAN* udevman; + UINT32 vchannel_status; + char* subsystem; + + wLog* log; + IWTSListener* listener; + BOOL initialized; +}; + +typedef BOOL (*PREGISTERURBDRCSERVICE)(IWTSPlugin* plugin, IUDEVMAN* udevman); +struct _FREERDP_URBDRC_SERVICE_ENTRY_POINTS +{ + IWTSPlugin* plugin; + PREGISTERURBDRCSERVICE pRegisterUDEVMAN; + ADDIN_ARGV* args; +}; +typedef struct _FREERDP_URBDRC_SERVICE_ENTRY_POINTS FREERDP_URBDRC_SERVICE_ENTRY_POINTS; +typedef FREERDP_URBDRC_SERVICE_ENTRY_POINTS* PFREERDP_URBDRC_SERVICE_ENTRY_POINTS; + +typedef int (*PFREERDP_URBDRC_DEVICE_ENTRY)(PFREERDP_URBDRC_SERVICE_ENTRY_POINTS pEntryPoints); + +typedef struct _TRANSFER_DATA TRANSFER_DATA; + +struct _TRANSFER_DATA +{ + URBDRC_CHANNEL_CALLBACK* callback; + URBDRC_PLUGIN* urbdrc; + IUDEVMAN* udevman; + IWTSVirtualChannel* channel; + wStream* s; +}; + +typedef void (*t_isoch_transfer_cb)(IUDEVICE* idev, URBDRC_CHANNEL_CALLBACK* callback, wStream* out, + UINT32 InterfaceId, BOOL noAck, UINT32 MessageId, + UINT32 RequestId, UINT32 NumberOfPackets, UINT32 status, + UINT32 StartFrame, UINT32 ErrorCount, UINT32 OutputBufferSize); + +struct _IUDEVICE +{ + /* Transfer */ + int (*isoch_transfer)(IUDEVICE* idev, URBDRC_CHANNEL_CALLBACK* callback, UINT32 MessageId, + UINT32 RequestId, UINT32 EndpointAddress, UINT32 TransferFlags, + UINT32 StartFrame, UINT32 ErrorCount, BOOL NoAck, + const BYTE* packetDescriptorData, UINT32 NumberOfPackets, + UINT32 BufferSize, const BYTE* Buffer, t_isoch_transfer_cb cb, + UINT32 Timeout); + + BOOL(*control_transfer) + (IUDEVICE* idev, UINT32 RequestId, UINT32 EndpointAddress, UINT32 TransferFlags, + BYTE bmRequestType, BYTE Request, UINT16 Value, UINT16 Index, UINT32* UrbdStatus, + UINT32* BufferSize, BYTE* Buffer, UINT32 Timeout); + + int (*bulk_or_interrupt_transfer)(IUDEVICE* idev, URBDRC_CHANNEL_CALLBACK* callback, + UINT32 MessageId, UINT32 RequestId, UINT32 EndpointAddress, + UINT32 TransferFlags, BOOL NoAck, UINT32 BufferSize, + const BYTE* data, t_isoch_transfer_cb cb, UINT32 Timeout); + + int (*select_configuration)(IUDEVICE* idev, UINT32 bConfigurationValue); + + int (*select_interface)(IUDEVICE* idev, BYTE InterfaceNumber, BYTE AlternateSetting); + + int (*control_pipe_request)(IUDEVICE* idev, UINT32 RequestId, UINT32 EndpointAddress, + UINT32* UsbdStatus, int command); + + UINT32(*control_query_device_text) + (IUDEVICE* idev, UINT32 TextType, UINT16 LocaleId, UINT8* BufferSize, BYTE* Buffer); + + int (*os_feature_descriptor_request)(IUDEVICE* idev, UINT32 RequestId, BYTE Recipient, + BYTE InterfaceNumber, BYTE Ms_PageIndex, + UINT16 Ms_featureDescIndex, UINT32* UsbdStatus, + UINT32* BufferSize, BYTE* Buffer, int Timeout); + + void (*cancel_all_transfer_request)(IUDEVICE* idev); + + int (*cancel_transfer_request)(IUDEVICE* idev, UINT32 RequestId); + + int (*query_device_descriptor)(IUDEVICE* idev, int offset); + + BOOL (*detach_kernel_driver)(IUDEVICE* idev); + + BOOL (*attach_kernel_driver)(IUDEVICE* idev); + + int (*query_device_port_status)(IUDEVICE* idev, UINT32* UsbdStatus, UINT32* BufferSize, + BYTE* Buffer); + + MSUSB_CONFIG_DESCRIPTOR* (*complete_msconfig_setup)(IUDEVICE* idev, + MSUSB_CONFIG_DESCRIPTOR* MsConfig); + /* Basic state */ + int (*isCompositeDevice)(IUDEVICE* idev); + + int (*isExist)(IUDEVICE* idev); + int (*isAlreadySend)(IUDEVICE* idev); + int (*isChannelClosed)(IUDEVICE* idev); + + void (*setAlreadySend)(IUDEVICE* idev); + void (*setChannelClosed)(IUDEVICE* idev); + void (*markChannelClosed)(IUDEVICE* idev); + char* (*getPath)(IUDEVICE* idev); + + void (*free)(IUDEVICE* idev); + + BASIC_DEV_STATE_DEFINED(channelManager, IWTSVirtualChannelManager*); + BASIC_DEV_STATE_DEFINED(channelID, UINT32); + BASIC_DEV_STATE_DEFINED(UsbDevice, UINT32); + BASIC_DEV_STATE_DEFINED(ReqCompletion, UINT32); + BASIC_DEV_STATE_DEFINED(bus_number, BYTE); + BASIC_DEV_STATE_DEFINED(dev_number, BYTE); + BASIC_DEV_STATE_DEFINED(port_number, int); + BASIC_DEV_STATE_DEFINED(MsConfig, MSUSB_CONFIG_DESCRIPTOR*); + + BASIC_DEV_STATE_DEFINED(p_udev, void*); + BASIC_DEV_STATE_DEFINED(p_prev, void*); + BASIC_DEV_STATE_DEFINED(p_next, void*); +}; + +struct _IUDEVMAN +{ + /* Standard */ + void (*free)(IUDEVMAN* idevman); + + /* Manage devices */ + void (*rewind)(IUDEVMAN* idevman); + BOOL (*has_next)(IUDEVMAN* idevman); + BOOL (*unregister_udevice)(IUDEVMAN* idevman, BYTE bus_number, BYTE dev_number); + size_t (*register_udevice)(IUDEVMAN* idevman, BYTE bus_number, BYTE dev_number, UINT16 idVendor, + UINT16 idProduct, UINT32 flag); + IUDEVICE* (*get_next)(IUDEVMAN* idevman); + IUDEVICE* (*get_udevice_by_UsbDevice)(IUDEVMAN* idevman, UINT32 UsbDevice); + IUDEVICE* (*get_udevice_by_ChannelID)(IUDEVMAN* idevman, UINT32 channelID); + + /* Extension */ + int (*isAutoAdd)(IUDEVMAN* idevman); + + /* Basic state */ + BASIC_DEVMAN_STATE_DEFINED(device_num, UINT32); + BASIC_DEVMAN_STATE_DEFINED(next_device_id, UINT32); + + /* control semaphore or mutex lock */ + void (*loading_lock)(IUDEVMAN* idevman); + void (*loading_unlock)(IUDEVMAN* idevman); + BOOL (*initialize)(IUDEVMAN* idevman, UINT32 channelId); + UINT (*listener_created_callback)(IUDEVMAN* idevman); + + IWTSPlugin* plugin; + UINT32 controlChannelId; + UINT32 status; +}; + +#define DEVICE_ADD_FLAG_BUS 0x01 +#define DEVICE_ADD_FLAG_DEV 0x02 +#define DEVICE_ADD_FLAG_VENDOR 0x04 +#define DEVICE_ADD_FLAG_PRODUCT 0x08 +#define DEVICE_ADD_FLAG_REGISTER 0x10 + +#define DEVICE_ADD_FLAG_ALL \ + (DEVICE_ADD_FLAG_BUS | DEVICE_ADD_FLAG_DEV | DEVICE_ADD_FLAG_VENDOR | \ + DEVICE_ADD_FLAG_PRODUCT | DEVICE_ADD_FLAG_REGISTER) + +FREERDP_API BOOL add_device(IUDEVMAN* idevman, UINT32 flags, BYTE busnum, BYTE devnum, + UINT16 idVendor, UINT16 idProduct); +FREERDP_API BOOL del_device(IUDEVMAN* idevman, UINT32 flags, BYTE busnum, BYTE devnum, + UINT16 idVendor, UINT16 idProduct); + +UINT stream_write_and_free(IWTSPlugin* plugin, IWTSVirtualChannel* channel, wStream* s); + +#endif /* FREERDP_CHANNEL_URBDRC_CLIENT_MAIN_H */ diff --git a/channels/urbdrc/common/CMakeLists.txt b/channels/urbdrc/common/CMakeLists.txt new file mode 100644 index 0000000..0e7b448 --- /dev/null +++ b/channels/urbdrc/common/CMakeLists.txt @@ -0,0 +1,26 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2019 Armin Novak +# Copyright 2019 Thincast Technologies GmbH +# +# Licensed 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. + +set(SRCS + urbdrc_types.h + urbdrc_helpers.h + urbdrc_helpers.c + msusb.h + msusb.c) + +add_library(urbdrc-common OBJECT ${SRCS}) diff --git a/channels/urbdrc/common/msusb.c b/channels/urbdrc/common/msusb.c new file mode 100644 index 0000000..bb517ce --- /dev/null +++ b/channels/urbdrc/common/msusb.c @@ -0,0 +1,402 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * RemoteFX USB Redirection + * + * Copyright 2012 Atrust corp. + * Copyright 2012 Alfred Liu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include + +#include +#include + +#define TAG FREERDP_TAG("utils") + +static MSUSB_PIPE_DESCRIPTOR* msusb_mspipe_new() +{ + return (MSUSB_PIPE_DESCRIPTOR*)calloc(1, sizeof(MSUSB_PIPE_DESCRIPTOR)); +} + +static void msusb_mspipes_free(MSUSB_PIPE_DESCRIPTOR** MsPipes, UINT32 NumberOfPipes) +{ + UINT32 pnum = 0; + + if (MsPipes) + { + for (pnum = 0; pnum < NumberOfPipes && MsPipes[pnum]; pnum++) + free(MsPipes[pnum]); + + free(MsPipes); + } +} + +BOOL msusb_mspipes_replace(MSUSB_INTERFACE_DESCRIPTOR* MsInterface, + MSUSB_PIPE_DESCRIPTOR** NewMsPipes, UINT32 NewNumberOfPipes) +{ + if (!MsInterface || !NewMsPipes) + return FALSE; + + /* free orignal MsPipes */ + msusb_mspipes_free(MsInterface->MsPipes, MsInterface->NumberOfPipes); + /* And replace it */ + MsInterface->MsPipes = NewMsPipes; + MsInterface->NumberOfPipes = NewNumberOfPipes; + return TRUE; +} + +static MSUSB_PIPE_DESCRIPTOR** msusb_mspipes_read(wStream* s, UINT32 NumberOfPipes) +{ + UINT32 pnum; + MSUSB_PIPE_DESCRIPTOR** MsPipes; + + if (Stream_GetRemainingCapacity(s) / 12 < NumberOfPipes) + return NULL; + + MsPipes = (MSUSB_PIPE_DESCRIPTOR**)calloc(NumberOfPipes, sizeof(MSUSB_PIPE_DESCRIPTOR*)); + + if (!MsPipes) + return NULL; + + for (pnum = 0; pnum < NumberOfPipes; pnum++) + { + MSUSB_PIPE_DESCRIPTOR* MsPipe = msusb_mspipe_new(); + + if (!MsPipe) + goto out_error; + + Stream_Read_UINT16(s, MsPipe->MaximumPacketSize); + Stream_Seek(s, 2); + Stream_Read_UINT32(s, MsPipe->MaximumTransferSize); + Stream_Read_UINT32(s, MsPipe->PipeFlags); + /* Already set to zero by memset + MsPipe->PipeHandle = 0; + MsPipe->bEndpointAddress = 0; + MsPipe->bInterval = 0; + MsPipe->PipeType = 0; + MsPipe->InitCompleted = 0; + */ + MsPipes[pnum] = MsPipe; + } + + return MsPipes; +out_error: + + for (pnum = 0; pnum < NumberOfPipes; pnum++) + free(MsPipes[pnum]); + + free(MsPipes); + return NULL; +} + +static MSUSB_INTERFACE_DESCRIPTOR* msusb_msinterface_new() +{ + return (MSUSB_INTERFACE_DESCRIPTOR*)calloc(1, sizeof(MSUSB_INTERFACE_DESCRIPTOR)); +} + +static void msusb_msinterface_free(MSUSB_INTERFACE_DESCRIPTOR* MsInterface) +{ + if (MsInterface) + { + msusb_mspipes_free(MsInterface->MsPipes, MsInterface->NumberOfPipes); + MsInterface->MsPipes = NULL; + free(MsInterface); + } +} + +static void msusb_msinterface_free_list(MSUSB_INTERFACE_DESCRIPTOR** MsInterfaces, + UINT32 NumInterfaces) +{ + UINT32 inum = 0; + + if (MsInterfaces) + { + for (inum = 0; inum < NumInterfaces; inum++) + { + msusb_msinterface_free(MsInterfaces[inum]); + } + + free(MsInterfaces); + } +} + +BOOL msusb_msinterface_replace(MSUSB_CONFIG_DESCRIPTOR* MsConfig, BYTE InterfaceNumber, + MSUSB_INTERFACE_DESCRIPTOR* NewMsInterface) +{ + if (!MsConfig || !MsConfig->MsInterfaces) + return FALSE; + + msusb_msinterface_free(MsConfig->MsInterfaces[InterfaceNumber]); + MsConfig->MsInterfaces[InterfaceNumber] = NewMsInterface; + return TRUE; +} + +MSUSB_INTERFACE_DESCRIPTOR* msusb_msinterface_read(wStream* s) +{ + MSUSB_INTERFACE_DESCRIPTOR* MsInterface; + + if (Stream_GetRemainingCapacity(s) < 12) + return NULL; + + MsInterface = msusb_msinterface_new(); + + if (!MsInterface) + return NULL; + + Stream_Read_UINT16(s, MsInterface->Length); + Stream_Read_UINT16(s, MsInterface->NumberOfPipesExpected); + Stream_Read_UINT8(s, MsInterface->InterfaceNumber); + Stream_Read_UINT8(s, MsInterface->AlternateSetting); + Stream_Seek(s, 2); + Stream_Read_UINT32(s, MsInterface->NumberOfPipes); + MsInterface->InterfaceHandle = 0; + MsInterface->bInterfaceClass = 0; + MsInterface->bInterfaceSubClass = 0; + MsInterface->bInterfaceProtocol = 0; + MsInterface->InitCompleted = 0; + MsInterface->MsPipes = NULL; + + if (MsInterface->NumberOfPipes > 0) + { + MsInterface->MsPipes = msusb_mspipes_read(s, MsInterface->NumberOfPipes); + + if (!MsInterface->MsPipes) + goto out_error; + } + + return MsInterface; +out_error: + msusb_msinterface_free(MsInterface); + return NULL; +} + +BOOL msusb_msinterface_write(MSUSB_INTERFACE_DESCRIPTOR* MsInterface, wStream* out) +{ + MSUSB_PIPE_DESCRIPTOR** MsPipes; + MSUSB_PIPE_DESCRIPTOR* MsPipe; + UINT32 pnum = 0; + + if (!MsInterface) + return FALSE; + + if (!Stream_EnsureRemainingCapacity(out, 16 + MsInterface->NumberOfPipes * 20)) + return FALSE; + + /* Length */ + Stream_Write_UINT16(out, MsInterface->Length); + /* InterfaceNumber */ + Stream_Write_UINT8(out, MsInterface->InterfaceNumber); + /* AlternateSetting */ + Stream_Write_UINT8(out, MsInterface->AlternateSetting); + /* bInterfaceClass */ + Stream_Write_UINT8(out, MsInterface->bInterfaceClass); + /* bInterfaceSubClass */ + Stream_Write_UINT8(out, MsInterface->bInterfaceSubClass); + /* bInterfaceProtocol */ + Stream_Write_UINT8(out, MsInterface->bInterfaceProtocol); + /* Padding */ + Stream_Write_UINT8(out, 0); + /* InterfaceHandle */ + Stream_Write_UINT32(out, MsInterface->InterfaceHandle); + /* NumberOfPipes */ + Stream_Write_UINT32(out, MsInterface->NumberOfPipes); + /* Pipes */ + MsPipes = MsInterface->MsPipes; + + for (pnum = 0; pnum < MsInterface->NumberOfPipes; pnum++) + { + MsPipe = MsPipes[pnum]; + /* MaximumPacketSize */ + Stream_Write_UINT16(out, MsPipe->MaximumPacketSize); + /* EndpointAddress */ + Stream_Write_UINT8(out, MsPipe->bEndpointAddress); + /* Interval */ + Stream_Write_UINT8(out, MsPipe->bInterval); + /* PipeType */ + Stream_Write_UINT32(out, MsPipe->PipeType); + /* PipeHandle */ + Stream_Write_UINT32(out, MsPipe->PipeHandle); + /* MaximumTransferSize */ + Stream_Write_UINT32(out, MsPipe->MaximumTransferSize); + /* PipeFlags */ + Stream_Write_UINT32(out, MsPipe->PipeFlags); + } + + return TRUE; +} + +static MSUSB_INTERFACE_DESCRIPTOR** msusb_msinterface_read_list(wStream* s, UINT32 NumInterfaces) +{ + UINT32 inum; + MSUSB_INTERFACE_DESCRIPTOR** MsInterfaces; + MsInterfaces = + (MSUSB_INTERFACE_DESCRIPTOR**)calloc(NumInterfaces, sizeof(MSUSB_INTERFACE_DESCRIPTOR*)); + + if (!MsInterfaces) + return NULL; + + for (inum = 0; inum < NumInterfaces; inum++) + { + MsInterfaces[inum] = msusb_msinterface_read(s); + + if (!MsInterfaces[inum]) + goto fail; + } + + return MsInterfaces; +fail: + + for (inum = 0; inum < NumInterfaces; inum++) + msusb_msinterface_free(MsInterfaces[inum]); + + free(MsInterfaces); + return NULL; +} + +BOOL msusb_msconfig_write(MSUSB_CONFIG_DESCRIPTOR* MsConfg, wStream* out) +{ + UINT32 inum = 0; + MSUSB_INTERFACE_DESCRIPTOR** MsInterfaces; + MSUSB_INTERFACE_DESCRIPTOR* MsInterface; + + if (!MsConfg) + return FALSE; + + if (!Stream_EnsureRemainingCapacity(out, 8)) + return FALSE; + + /* ConfigurationHandle*/ + Stream_Write_UINT32(out, MsConfg->ConfigurationHandle); + /* NumInterfaces*/ + Stream_Write_UINT32(out, MsConfg->NumInterfaces); + /* Interfaces */ + MsInterfaces = MsConfg->MsInterfaces; + + for (inum = 0; inum < MsConfg->NumInterfaces; inum++) + { + MsInterface = MsInterfaces[inum]; + + if (!msusb_msinterface_write(MsInterface, out)) + return FALSE; + } + + return TRUE; +} + +MSUSB_CONFIG_DESCRIPTOR* msusb_msconfig_new(void) +{ + return (MSUSB_CONFIG_DESCRIPTOR*)calloc(1, sizeof(MSUSB_CONFIG_DESCRIPTOR)); +} + +void msusb_msconfig_free(MSUSB_CONFIG_DESCRIPTOR* MsConfig) +{ + if (MsConfig) + { + msusb_msinterface_free_list(MsConfig->MsInterfaces, MsConfig->NumInterfaces); + MsConfig->MsInterfaces = NULL; + free(MsConfig); + } +} + +MSUSB_CONFIG_DESCRIPTOR* msusb_msconfig_read(wStream* s, UINT32 NumInterfaces) +{ + MSUSB_CONFIG_DESCRIPTOR* MsConfig; + BYTE lenConfiguration, typeConfiguration; + + if (Stream_GetRemainingCapacity(s) < 6ULL + NumInterfaces * 2ULL) + return NULL; + + MsConfig = msusb_msconfig_new(); + + if (!MsConfig) + goto fail; + + MsConfig->MsInterfaces = msusb_msinterface_read_list(s, NumInterfaces); + + if (!MsConfig->MsInterfaces) + goto fail; + + Stream_Read_UINT8(s, lenConfiguration); + Stream_Read_UINT8(s, typeConfiguration); + + if (lenConfiguration != 0x9 || typeConfiguration != 0x2) + { + WLog_ERR(TAG, "len and type must be 0x9 and 0x2 , but it is 0x%" PRIx8 " and 0x%" PRIx8 "", + lenConfiguration, typeConfiguration); + goto fail; + } + + Stream_Read_UINT16(s, MsConfig->wTotalLength); + Stream_Seek(s, 1); + Stream_Read_UINT8(s, MsConfig->bConfigurationValue); + MsConfig->NumInterfaces = NumInterfaces; + return MsConfig; +fail: + msusb_msconfig_free(MsConfig); + return NULL; +} + +void msusb_msconfig_dump(MSUSB_CONFIG_DESCRIPTOR* MsConfig) +{ + MSUSB_INTERFACE_DESCRIPTOR** MsInterfaces; + MSUSB_INTERFACE_DESCRIPTOR* MsInterface; + MSUSB_PIPE_DESCRIPTOR** MsPipes; + MSUSB_PIPE_DESCRIPTOR* MsPipe; + UINT32 inum = 0, pnum = 0; + WLog_INFO(TAG, "=================MsConfig:========================"); + WLog_INFO(TAG, "wTotalLength:%" PRIu16 "", MsConfig->wTotalLength); + WLog_INFO(TAG, "bConfigurationValue:%" PRIu8 "", MsConfig->bConfigurationValue); + WLog_INFO(TAG, "ConfigurationHandle:0x%08" PRIx32 "", MsConfig->ConfigurationHandle); + WLog_INFO(TAG, "InitCompleted:%d", MsConfig->InitCompleted); + WLog_INFO(TAG, "MsOutSize:%d", MsConfig->MsOutSize); + WLog_INFO(TAG, "NumInterfaces:%" PRIu32 "", MsConfig->NumInterfaces); + MsInterfaces = MsConfig->MsInterfaces; + + for (inum = 0; inum < MsConfig->NumInterfaces; inum++) + { + MsInterface = MsInterfaces[inum]; + WLog_INFO(TAG, " Interface: %" PRIu8 "", MsInterface->InterfaceNumber); + WLog_INFO(TAG, " Length: %" PRIu16 "", MsInterface->Length); + WLog_INFO(TAG, " NumberOfPipesExpected: %" PRIu16 "", + MsInterface->NumberOfPipesExpected); + WLog_INFO(TAG, " AlternateSetting: %" PRIu8 "", MsInterface->AlternateSetting); + WLog_INFO(TAG, " NumberOfPipes: %" PRIu32 "", MsInterface->NumberOfPipes); + WLog_INFO(TAG, " InterfaceHandle: 0x%08" PRIx32 "", MsInterface->InterfaceHandle); + WLog_INFO(TAG, " bInterfaceClass: 0x%02" PRIx8 "", MsInterface->bInterfaceClass); + WLog_INFO(TAG, " bInterfaceSubClass: 0x%02" PRIx8 "", MsInterface->bInterfaceSubClass); + WLog_INFO(TAG, " bInterfaceProtocol: 0x%02" PRIx8 "", MsInterface->bInterfaceProtocol); + WLog_INFO(TAG, " InitCompleted: %d", MsInterface->InitCompleted); + MsPipes = MsInterface->MsPipes; + + for (pnum = 0; pnum < MsInterface->NumberOfPipes; pnum++) + { + MsPipe = MsPipes[pnum]; + WLog_INFO(TAG, " Pipe: %d", pnum); + WLog_INFO(TAG, " MaximumPacketSize: 0x%04" PRIx16 "", MsPipe->MaximumPacketSize); + WLog_INFO(TAG, " MaximumTransferSize: 0x%08" PRIx32 "", + MsPipe->MaximumTransferSize); + WLog_INFO(TAG, " PipeFlags: 0x%08" PRIx32 "", MsPipe->PipeFlags); + WLog_INFO(TAG, " PipeHandle: 0x%08" PRIx32 "", MsPipe->PipeHandle); + WLog_INFO(TAG, " bEndpointAddress: 0x%02" PRIx8 "", MsPipe->bEndpointAddress); + WLog_INFO(TAG, " bInterval: %" PRIu8 "", MsPipe->bInterval); + WLog_INFO(TAG, " PipeType: 0x%02" PRIx8 "", MsPipe->PipeType); + WLog_INFO(TAG, " InitCompleted: %d", MsPipe->InitCompleted); + } + } + + WLog_INFO(TAG, "=================================================="); +} diff --git a/channels/urbdrc/common/msusb.h b/channels/urbdrc/common/msusb.h new file mode 100644 index 0000000..89f1a2b --- /dev/null +++ b/channels/urbdrc/common/msusb.h @@ -0,0 +1,97 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * RemoteFX USB Redirection + * + * Copyright 2012 Atrust corp. + * Copyright 2012 Alfred Liu + * + * Licensed 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 FREERDP_UTILS_MSCONFIG_H +#define FREERDP_UTILS_MSCONFIG_H + +#include +#include + +typedef struct _MSUSB_INTERFACE_DESCRIPTOR MSUSB_INTERFACE_DESCRIPTOR; +typedef struct _MSUSB_PIPE_DESCRIPTOR MSUSB_PIPE_DESCRIPTOR; +typedef struct _MSUSB_CONFIG_DESCRIPTOR MSUSB_CONFIG_DESCRIPTOR; + +struct _MSUSB_PIPE_DESCRIPTOR +{ + UINT16 MaximumPacketSize; + UINT32 MaximumTransferSize; + UINT32 PipeFlags; + UINT32 PipeHandle; + BYTE bEndpointAddress; + BYTE bInterval; + BYTE PipeType; + int InitCompleted; +}; + +struct _MSUSB_INTERFACE_DESCRIPTOR +{ + UINT16 Length; + UINT16 NumberOfPipesExpected; + BYTE InterfaceNumber; + BYTE AlternateSetting; + UINT32 NumberOfPipes; + UINT32 InterfaceHandle; + BYTE bInterfaceClass; + BYTE bInterfaceSubClass; + BYTE bInterfaceProtocol; + MSUSB_PIPE_DESCRIPTOR** MsPipes; + int InitCompleted; +}; + +struct _MSUSB_CONFIG_DESCRIPTOR +{ + UINT16 wTotalLength; + BYTE bConfigurationValue; + UINT32 ConfigurationHandle; + UINT32 NumInterfaces; + MSUSB_INTERFACE_DESCRIPTOR** MsInterfaces; + int InitCompleted; + int MsOutSize; +}; + +#ifdef __cplusplus +extern "C" +{ +#endif + + /* MSUSB_PIPE exported functions */ + FREERDP_API BOOL msusb_mspipes_replace(MSUSB_INTERFACE_DESCRIPTOR* MsInterface, + MSUSB_PIPE_DESCRIPTOR** NewMsPipes, + UINT32 NewNumberOfPipes); + + /* MSUSB_INTERFACE exported functions */ + FREERDP_API BOOL msusb_msinterface_replace(MSUSB_CONFIG_DESCRIPTOR* MsConfig, + BYTE InterfaceNumber, + MSUSB_INTERFACE_DESCRIPTOR* NewMsInterface); + FREERDP_API MSUSB_INTERFACE_DESCRIPTOR* msusb_msinterface_read(wStream* out); + FREERDP_API BOOL msusb_msinterface_write(MSUSB_INTERFACE_DESCRIPTOR* MsInterface, wStream* out); + + /* MSUSB_CONFIG exported functions */ + FREERDP_API MSUSB_CONFIG_DESCRIPTOR* msusb_msconfig_new(void); + FREERDP_API void msusb_msconfig_free(MSUSB_CONFIG_DESCRIPTOR* MsConfig); + FREERDP_API MSUSB_CONFIG_DESCRIPTOR* msusb_msconfig_read(wStream* s, UINT32 NumInterfaces); + FREERDP_API BOOL msusb_msconfig_write(MSUSB_CONFIG_DESCRIPTOR* MsConfg, wStream* out); + FREERDP_API void msusb_msconfig_dump(MSUSB_CONFIG_DESCRIPTOR* MsConfg); + +#ifdef __cplusplus +} +#endif + +#endif /* FREERDP_UTILS_MSCONFIG_H */ diff --git a/channels/urbdrc/common/urbdrc_helpers.c b/channels/urbdrc/common/urbdrc_helpers.c new file mode 100644 index 0000000..53c3b89 --- /dev/null +++ b/channels/urbdrc/common/urbdrc_helpers.c @@ -0,0 +1,421 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Server USB redirection channel - helper functions + * + * Copyright 2019 Armin Novak + * Copyright 2019 Thincast Technologies GmbH + * + * Licensed 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. + */ +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "urbdrc_helpers.h" +#include "urbdrc_types.h" +#include + +const char* mask_to_string(UINT32 mask) +{ + switch (mask) + { + case STREAM_ID_NONE: + return "STREAM_ID_NONE"; + + case STREAM_ID_PROXY: + return "STREAM_ID_PROXY"; + + case STREAM_ID_STUB: + return "STREAM_ID_STUB"; + + default: + return "UNKNOWN"; + } +} +const char* interface_to_string(UINT32 id) +{ + switch (id) + { + case CAPABILITIES_NEGOTIATOR: + return "CAPABILITIES_NEGOTIATOR"; + + case SERVER_CHANNEL_NOTIFICATION: + return "SERVER_CHANNEL_NOTIFICATION"; + + case CLIENT_CHANNEL_NOTIFICATION: + return "CLIENT_CHANNEL_NOTIFICATION"; + + default: + return "DEVICE_MESSAGE"; + } +} + +static const char* call_to_string_none(BOOL client, UINT32 interfaceId, UINT32 functionId) +{ + WINPR_UNUSED(interfaceId); + + if (client) + return "RIM_EXCHANGE_CAPABILITY_RESPONSE [none |client]"; + else + { + switch (functionId) + { + case RIM_EXCHANGE_CAPABILITY_REQUEST: + return "RIM_EXCHANGE_CAPABILITY_REQUEST [none |server]"; + + case RIMCALL_RELEASE: + return "RIMCALL_RELEASE [none |server]"; + + case RIMCALL_QUERYINTERFACE: + return "RIMCALL_QUERYINTERFACE [none |server]"; + + default: + return "UNKNOWN [none |server]"; + } + } +} + +static const char* call_to_string_proxy_server(UINT32 functionId) +{ + switch (functionId) + { + case QUERY_DEVICE_TEXT: + return "QUERY_DEVICE_TEXT [proxy|server]"; + + case INTERNAL_IO_CONTROL: + return "INTERNAL_IO_CONTROL [proxy|server]"; + + case IO_CONTROL: + return "IO_CONTROL [proxy|server]"; + + case REGISTER_REQUEST_CALLBACK: + return "REGISTER_REQUEST_CALLBACK [proxy|server]"; + + case CANCEL_REQUEST: + return "CANCEL_REQUEST [proxy|server]"; + + case RETRACT_DEVICE: + return "RETRACT_DEVICE [proxy|server]"; + + case TRANSFER_IN_REQUEST: + return "TRANSFER_IN_REQUEST [proxy|server]"; + + default: + return "UNKNOWN [proxy|server]"; + } +} + +static const char* call_to_string_proxy_client(UINT32 functionId) +{ + switch (functionId) + { + case URB_COMPLETION_NO_DATA: + return "URB_COMPLETION_NO_DATA [proxy|client]"; + + case URB_COMPLETION: + return "URB_COMPLETION [proxy|client]"; + + case IOCONTROL_COMPLETION: + return "IOCONTROL_COMPLETION [proxy|client]"; + + case TRANSFER_OUT_REQUEST: + return "TRANSFER_OUT_REQUEST [proxy|client]"; + + default: + return "UNKNOWN [proxy|client]"; + } +} + +static const char* call_to_string_proxy(BOOL client, UINT32 interfaceId, UINT32 functionId) +{ + switch (interfaceId & INTERFACE_ID_MASK) + { + case CLIENT_DEVICE_SINK: + switch (functionId) + { + case ADD_VIRTUAL_CHANNEL: + return "ADD_VIRTUAL_CHANNEL [proxy|sink ]"; + + case ADD_DEVICE: + return "ADD_DEVICE [proxy|sink ]"; + case RIMCALL_RELEASE: + return "RIMCALL_RELEASE [proxy|sink ]"; + + case RIMCALL_QUERYINTERFACE: + return "RIMCALL_QUERYINTERFACE [proxy|sink ]"; + default: + return "UNKNOWN [proxy|sink ]"; + } + + case SERVER_CHANNEL_NOTIFICATION: + switch (functionId) + { + case CHANNEL_CREATED: + return "CHANNEL_CREATED [proxy|server]"; + + case RIMCALL_RELEASE: + return "RIMCALL_RELEASE [proxy|server]"; + + case RIMCALL_QUERYINTERFACE: + return "RIMCALL_QUERYINTERFACE [proxy|server]"; + + default: + return "UNKNOWN [proxy|server]"; + } + + case CLIENT_CHANNEL_NOTIFICATION: + switch (functionId) + { + case CHANNEL_CREATED: + return "CHANNEL_CREATED [proxy|client]"; + case RIMCALL_RELEASE: + return "RIMCALL_RELEASE [proxy|client]"; + case RIMCALL_QUERYINTERFACE: + return "RIMCALL_QUERYINTERFACE [proxy|client]"; + default: + return "UNKNOWN [proxy|client]"; + } + + default: + if (client) + return call_to_string_proxy_client(functionId); + else + return call_to_string_proxy_server(functionId); + } +} + +static const char* call_to_string_stub(BOOL client, UINT32 interfaceId, UINT32 functionId) +{ + return "QUERY_DEVICE_TEXT_RSP [stub |client]"; +} + +const char* call_to_string(BOOL client, UINT32 interface, UINT32 functionId) +{ + const UINT32 mask = (interface & STREAM_ID_MASK) >> 30; + const UINT32 interfaceId = interface & INTERFACE_ID_MASK; + + switch (mask) + { + case STREAM_ID_NONE: + return call_to_string_none(client, interfaceId, functionId); + + case STREAM_ID_PROXY: + return call_to_string_proxy(client, interfaceId, functionId); + + case STREAM_ID_STUB: + return call_to_string_stub(client, interfaceId, functionId); + + default: + return "UNKNOWN[mask]"; + } +} + +const char* urb_function_string(UINT16 urb) +{ + switch (urb) + { + case TS_URB_SELECT_CONFIGURATION: + return "TS_URB_SELECT_CONFIGURATION"; + + case TS_URB_SELECT_INTERFACE: + return "TS_URB_SELECT_INTERFACE"; + + case TS_URB_PIPE_REQUEST: + return "TS_URB_PIPE_REQUEST"; + + case TS_URB_TAKE_FRAME_LENGTH_CONTROL: + return "TS_URB_TAKE_FRAME_LENGTH_CONTROL"; + + case TS_URB_RELEASE_FRAME_LENGTH_CONTROL: + return "TS_URB_RELEASE_FRAME_LENGTH_CONTROL"; + + case TS_URB_GET_FRAME_LENGTH: + return "TS_URB_GET_FRAME_LENGTH"; + + case TS_URB_SET_FRAME_LENGTH: + return "TS_URB_SET_FRAME_LENGTH"; + + case TS_URB_GET_CURRENT_FRAME_NUMBER: + return "TS_URB_GET_CURRENT_FRAME_NUMBER"; + + case TS_URB_CONTROL_TRANSFER: + return "TS_URB_CONTROL_TRANSFER"; + + case TS_URB_BULK_OR_INTERRUPT_TRANSFER: + return "TS_URB_BULK_OR_INTERRUPT_TRANSFER"; + + case TS_URB_ISOCH_TRANSFER: + return "TS_URB_ISOCH_TRANSFER"; + + case TS_URB_GET_DESCRIPTOR_FROM_DEVICE: + return "TS_URB_GET_DESCRIPTOR_FROM_DEVICE"; + + case TS_URB_SET_DESCRIPTOR_TO_DEVICE: + return "TS_URB_SET_DESCRIPTOR_TO_DEVICE"; + + case TS_URB_SET_FEATURE_TO_DEVICE: + return "TS_URB_SET_FEATURE_TO_DEVICE"; + + case TS_URB_SET_FEATURE_TO_INTERFACE: + return "TS_URB_SET_FEATURE_TO_INTERFACE"; + + case TS_URB_SET_FEATURE_TO_ENDPOINT: + return "TS_URB_SET_FEATURE_TO_ENDPOINT"; + + case TS_URB_CLEAR_FEATURE_TO_DEVICE: + return "TS_URB_CLEAR_FEATURE_TO_DEVICE"; + + case TS_URB_CLEAR_FEATURE_TO_INTERFACE: + return "TS_URB_CLEAR_FEATURE_TO_INTERFACE"; + + case TS_URB_CLEAR_FEATURE_TO_ENDPOINT: + return "TS_URB_CLEAR_FEATURE_TO_ENDPOINT"; + + case TS_URB_GET_STATUS_FROM_DEVICE: + return "TS_URB_GET_STATUS_FROM_DEVICE"; + + case TS_URB_GET_STATUS_FROM_INTERFACE: + return "TS_URB_GET_STATUS_FROM_INTERFACE"; + + case TS_URB_GET_STATUS_FROM_ENDPOINT: + return "TS_URB_GET_STATUS_FROM_ENDPOINT"; + + case TS_URB_RESERVED_0X0016: + return "TS_URB_RESERVED_0X0016"; + + case TS_URB_VENDOR_DEVICE: + return "TS_URB_VENDOR_DEVICE"; + + case TS_URB_VENDOR_INTERFACE: + return "TS_URB_VENDOR_INTERFACE"; + + case TS_URB_VENDOR_ENDPOINT: + return "TS_URB_VENDOR_ENDPOINT"; + + case TS_URB_CLASS_DEVICE: + return "TS_URB_CLASS_DEVICE"; + + case TS_URB_CLASS_INTERFACE: + return "TS_URB_CLASS_INTERFACE"; + + case TS_URB_CLASS_ENDPOINT: + return "TS_URB_CLASS_ENDPOINT"; + + case TS_URB_RESERVE_0X001D: + return "TS_URB_RESERVE_0X001D"; + + case TS_URB_SYNC_RESET_PIPE_AND_CLEAR_STALL: + return "TS_URB_SYNC_RESET_PIPE_AND_CLEAR_STALL"; + + case TS_URB_CLASS_OTHER: + return "TS_URB_CLASS_OTHER"; + + case TS_URB_VENDOR_OTHER: + return "TS_URB_VENDOR_OTHER"; + + case TS_URB_GET_STATUS_FROM_OTHER: + return "TS_URB_GET_STATUS_FROM_OTHER"; + + case TS_URB_CLEAR_FEATURE_TO_OTHER: + return "TS_URB_CLEAR_FEATURE_TO_OTHER"; + + case TS_URB_SET_FEATURE_TO_OTHER: + return "TS_URB_SET_FEATURE_TO_OTHER"; + + case TS_URB_GET_DESCRIPTOR_FROM_ENDPOINT: + return "TS_URB_GET_DESCRIPTOR_FROM_ENDPOINT"; + + case TS_URB_SET_DESCRIPTOR_TO_ENDPOINT: + return "TS_URB_SET_DESCRIPTOR_TO_ENDPOINT"; + + case TS_URB_CONTROL_GET_CONFIGURATION_REQUEST: + return "TS_URB_CONTROL_GET_CONFIGURATION_REQUEST"; + + case TS_URB_CONTROL_GET_INTERFACE_REQUEST: + return "TS_URB_CONTROL_GET_INTERFACE_REQUEST"; + + case TS_URB_GET_DESCRIPTOR_FROM_INTERFACE: + return "TS_URB_GET_DESCRIPTOR_FROM_INTERFACE"; + + case TS_URB_SET_DESCRIPTOR_TO_INTERFACE: + return "TS_URB_SET_DESCRIPTOR_TO_INTERFACE"; + + case TS_URB_GET_OS_FEATURE_DESCRIPTOR_REQUEST: + return "TS_URB_GET_OS_FEATURE_DESCRIPTOR_REQUEST"; + + case TS_URB_RESERVE_0X002B: + return "TS_URB_RESERVE_0X002B"; + + case TS_URB_RESERVE_0X002C: + return "TS_URB_RESERVE_0X002C"; + + case TS_URB_RESERVE_0X002D: + return "TS_URB_RESERVE_0X002D"; + + case TS_URB_RESERVE_0X002E: + return "TS_URB_RESERVE_0X002E"; + + case TS_URB_RESERVE_0X002F: + return "TS_URB_RESERVE_0X002F"; + + case TS_URB_SYNC_RESET_PIPE: + return "TS_URB_SYNC_RESET_PIPE"; + + case TS_URB_SYNC_CLEAR_STALL: + return "TS_URB_SYNC_CLEAR_STALL"; + + case TS_URB_CONTROL_TRANSFER_EX: + return "TS_URB_CONTROL_TRANSFER_EX"; + + default: + return "UNKNOWN"; + } +} + +void urbdrc_dump_message(wLog* log, BOOL client, BOOL write, wStream* s) +{ + const char* type = write ? "WRITE" : "READ"; + UINT32 InterfaceId, MessageId, FunctionId; + size_t length, pos; + + pos = Stream_GetPosition(s); + if (write) + { + length = pos; + Stream_SetPosition(s, 0); + } + else + length = Stream_GetRemainingLength(s); + + if (length < 12) + return; + + Stream_Read_UINT32(s, InterfaceId); + Stream_Read_UINT32(s, MessageId); + Stream_Read_UINT32(s, FunctionId); + Stream_SetPosition(s, pos); + + WLog_Print(log, WLOG_DEBUG, + "[%-5s] %s [%08" PRIx32 "] InterfaceId=%08" PRIx32 ", MessageId=%08" PRIx32 + ", FunctionId=%08" PRIx32 ", length=%" PRIuz, + type, call_to_string(client, InterfaceId, FunctionId), FunctionId, InterfaceId, + MessageId, FunctionId, length); +#if defined(WITH_DEBUG_URBDRC) + if (write) + WLog_Print(log, WLOG_TRACE, "-------------------------- URBDRC sent: ---"); + else + WLog_Print(log, WLOG_TRACE, "-------------------------- URBDRC received:"); + winpr_HexLogDump(log, WLOG_TRACE, Stream_Buffer(s), length); + WLog_Print(log, WLOG_TRACE, "-------------------------- URBDRC end -----"); +#endif +} diff --git a/channels/urbdrc/common/urbdrc_helpers.h b/channels/urbdrc/common/urbdrc_helpers.h new file mode 100644 index 0000000..e9e25af --- /dev/null +++ b/channels/urbdrc/common/urbdrc_helpers.h @@ -0,0 +1,45 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Server USB redirection channel - helper functions + * + * Copyright 2019 Armin Novak + * Copyright 2019 Thincast Technologies GmbH + * + * Licensed 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 FREERDP_CHANNEL_URBDRC_HELPERS_H +#define FREERDP_CHANNEL_URBDRC_HELPERS_H + +#include + +#ifdef __cplusplus +extern "C" +{ +#endif + +#include +#include + + const char* urb_function_string(UINT16 urb); + const char* mask_to_string(UINT32 mask); + const char* interface_to_string(UINT32 id); + const char* call_to_string(BOOL client, UINT32 interface, UINT32 functionId); + + void urbdrc_dump_message(wLog* log, BOOL client, BOOL write, wStream* s); + +#ifdef __cplusplus +} +#endif + +#endif /* FREERDP_CHANNEL_URBDRC_HELPERS_H */ diff --git a/channels/urbdrc/common/urbdrc_types.h b/channels/urbdrc/common/urbdrc_types.h new file mode 100644 index 0000000..c120715 --- /dev/null +++ b/channels/urbdrc/common/urbdrc_types.h @@ -0,0 +1,308 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * RemoteFX USB Redirection + * + * Copyright 2012 Atrust corp. + * Copyright 2012 Alfred Liu + * + * Licensed 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 FREERDP_CHANNEL_URBDRC_CLIENT_TYPES_H +#define FREERDP_CHANNEL_URBDRC_CLIENT_TYPES_H + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include + +#include + +#include + +#define RIM_CAPABILITY_VERSION_01 0x00000001 + +#define CAPABILITIES_NEGOTIATOR 0x00000000 +#define CLIENT_DEVICE_SINK 0x00000001 +#define SERVER_CHANNEL_NOTIFICATION 0x00000002 +#define CLIENT_CHANNEL_NOTIFICATION 0x00000003 +#define BASE_USBDEVICE_NUM 0x00000005 + +#define RIMCALL_RELEASE 0x00000001 +#define RIMCALL_QUERYINTERFACE 0x00000002 +#define RIM_EXCHANGE_CAPABILITY_REQUEST 0x00000100 +#define CHANNEL_CREATED 0x00000100 +#define ADD_VIRTUAL_CHANNEL 0x00000100 +#define ADD_DEVICE 0x00000101 + +#define INIT_CHANNEL_IN 1 +#define INIT_CHANNEL_OUT 0 + +/* InterfaceClass */ +#define CLASS_RESERVE 0x00 +#define CLASS_AUDIO 0x01 +#define CLASS_COMMUNICATION_IF 0x02 +#define CLASS_HID 0x03 +#define CLASS_PHYSICAL 0x05 +#define CLASS_IMAGE 0x06 +#define CLASS_PRINTER 0x07 +#define CLASS_MASS_STORAGE 0x08 +#define CLASS_HUB 0x09 +#define CLASS_COMMUNICATION_DATA_IF 0x0a +#define CLASS_SMART_CARD 0x0b +#define CLASS_CONTENT_SECURITY 0x0d +#define CLASS_VIDEO 0x0e +#define CLASS_PERSONAL_HEALTHCARE 0x0f +#define CLASS_DIAGNOSTIC 0xdc +#define CLASS_WIRELESS_CONTROLLER 0xe0 +#define CLASS_ELSE_DEVICE 0xef +#define CLASS_DEPENDENCE 0xfe +#define CLASS_VENDOR_DEPENDENCE 0xff + +/* usb version */ +#define USB_v1_0 0x100 +#define USB_v1_1 0x110 +#define USB_v2_0 0x200 +#define USB_v3_0 0x300 + +#define STREAM_ID_NONE 0x0UL +#define STREAM_ID_PROXY 0x1UL +#define STREAM_ID_STUB 0x2UL +#define STREAM_ID_MASK 0xC0000000 +#define INTERFACE_ID_MASK 0x3FFFFFFF + +#define CANCEL_REQUEST 0x00000100 +#define REGISTER_REQUEST_CALLBACK 0x00000101 +#define IO_CONTROL 0x00000102 +#define INTERNAL_IO_CONTROL 0x00000103 +#define QUERY_DEVICE_TEXT 0x00000104 + +#define TRANSFER_IN_REQUEST 0x00000105 +#define TRANSFER_OUT_REQUEST 0x00000106 +#define RETRACT_DEVICE 0x00000107 + +#define IOCONTROL_COMPLETION 0x00000100 +#define URB_COMPLETION 0x00000101 +#define URB_COMPLETION_NO_DATA 0x00000102 + +/* The USB device is to be stopped from being redirected because the + * device is blocked by the server's policy. */ +#define UsbRetractReason_BlockedByPolicy 0x00000001 + +enum device_text_type +{ + DeviceTextDescription = 0, + DeviceTextLocationInformation = 1, +}; + +enum device_descriptor_table +{ + B_LENGTH = 0, + B_DESCRIPTOR_TYPE = 1, + BCD_USB = 2, + B_DEVICE_CLASS = 4, + B_DEVICE_SUBCLASS = 5, + B_DEVICE_PROTOCOL = 6, + B_MAX_PACKET_SIZE0 = 7, + ID_VENDOR = 8, + ID_PRODUCT = 10, + BCD_DEVICE = 12, + I_MANUFACTURER = 14, + I_PRODUCT = 15, + I_SERIAL_NUMBER = 16, + B_NUM_CONFIGURATIONS = 17 +}; + +#define PIPE_CANCEL 0 +#define PIPE_RESET 1 + +#define IOCTL_INTERNAL_USB_SUBMIT_URB 0x00220003 +#define IOCTL_INTERNAL_USB_RESET_PORT 0x00220007 +#define IOCTL_INTERNAL_USB_GET_PORT_STATUS 0x00220013 +#define IOCTL_INTERNAL_USB_CYCLE_PORT 0x0022001F +#define IOCTL_INTERNAL_USB_SUBMIT_IDLE_NOTIFICATION 0x00220027 + +#define TS_URB_SELECT_CONFIGURATION 0x0000 +#define TS_URB_SELECT_INTERFACE 0x0001 +#define TS_URB_PIPE_REQUEST 0x0002 +#define TS_URB_TAKE_FRAME_LENGTH_CONTROL 0x0003 +#define TS_URB_RELEASE_FRAME_LENGTH_CONTROL 0x0004 +#define TS_URB_GET_FRAME_LENGTH 0x0005 +#define TS_URB_SET_FRAME_LENGTH 0x0006 +#define TS_URB_GET_CURRENT_FRAME_NUMBER 0x0007 +#define TS_URB_CONTROL_TRANSFER 0x0008 +#define TS_URB_BULK_OR_INTERRUPT_TRANSFER 0x0009 +#define TS_URB_ISOCH_TRANSFER 0x000A +#define TS_URB_GET_DESCRIPTOR_FROM_DEVICE 0x000B +#define TS_URB_SET_DESCRIPTOR_TO_DEVICE 0x000C +#define TS_URB_SET_FEATURE_TO_DEVICE 0x000D +#define TS_URB_SET_FEATURE_TO_INTERFACE 0x000E +#define TS_URB_SET_FEATURE_TO_ENDPOINT 0x000F +#define TS_URB_CLEAR_FEATURE_TO_DEVICE 0x0010 +#define TS_URB_CLEAR_FEATURE_TO_INTERFACE 0x0011 +#define TS_URB_CLEAR_FEATURE_TO_ENDPOINT 0x0012 +#define TS_URB_GET_STATUS_FROM_DEVICE 0x0013 +#define TS_URB_GET_STATUS_FROM_INTERFACE 0x0014 +#define TS_URB_GET_STATUS_FROM_ENDPOINT 0x0015 +#define TS_URB_RESERVED_0X0016 0x0016 +#define TS_URB_VENDOR_DEVICE 0x0017 +#define TS_URB_VENDOR_INTERFACE 0x0018 +#define TS_URB_VENDOR_ENDPOINT 0x0019 +#define TS_URB_CLASS_DEVICE 0x001A +#define TS_URB_CLASS_INTERFACE 0x001B +#define TS_URB_CLASS_ENDPOINT 0x001C +#define TS_URB_RESERVE_0X001D 0x001D +#define TS_URB_SYNC_RESET_PIPE_AND_CLEAR_STALL 0x001E +#define TS_URB_CLASS_OTHER 0x001F +#define TS_URB_VENDOR_OTHER 0x0020 +#define TS_URB_GET_STATUS_FROM_OTHER 0x0021 +#define TS_URB_CLEAR_FEATURE_TO_OTHER 0x0022 +#define TS_URB_SET_FEATURE_TO_OTHER 0x0023 +#define TS_URB_GET_DESCRIPTOR_FROM_ENDPOINT 0x0024 +#define TS_URB_SET_DESCRIPTOR_TO_ENDPOINT 0x0025 +#define TS_URB_CONTROL_GET_CONFIGURATION_REQUEST 0x0026 +#define TS_URB_CONTROL_GET_INTERFACE_REQUEST 0x0027 +#define TS_URB_GET_DESCRIPTOR_FROM_INTERFACE 0x0028 +#define TS_URB_SET_DESCRIPTOR_TO_INTERFACE 0x0029 +#define TS_URB_GET_OS_FEATURE_DESCRIPTOR_REQUEST 0x002A +#define TS_URB_RESERVE_0X002B 0x002B +#define TS_URB_RESERVE_0X002C 0x002C +#define TS_URB_RESERVE_0X002D 0x002D +#define TS_URB_RESERVE_0X002E 0x002E +#define TS_URB_RESERVE_0X002F 0x002F +// USB 2.0 calls start at 0x0030 +#define TS_URB_SYNC_RESET_PIPE 0x0030 +#define TS_URB_SYNC_CLEAR_STALL 0x0031 +#define TS_URB_CONTROL_TRANSFER_EX 0x0032 + +#define USBD_STATUS_SUCCESS 0x0 +#define USBD_STATUS_PENDING 0x40000000 +#define USBD_STATUS_CANCELED 0xC0010000 + +#define USBD_STATUS_INVALID_URB_FUNCTION 0x80000200 +#define USBD_STATUS_CRC 0xC0000001 +#define USBD_STATUS_BTSTUFF 0xC0000002 +#define USBD_STATUS_DATA_TOGGLE_MISMATCH 0xC0000003 +#define USBD_STATUS_STALL_PID 0xC0000004 +#define USBD_STATUS_DEV_NOT_RESPONDING 0xC0000005 +#define USBD_STATUS_PID_CHECK_FAILURE 0xC0000006 +#define USBD_STATUS_UNEXPECTED_PID 0xC0000007 +#define USBD_STATUS_DATA_OVERRUN 0xC0000008 +#define USBD_STATUS_DATA_UNDERRUN 0xC0000009 +#define USBD_STATUS_RESERVED1 0xC000000A +#define USBD_STATUS_RESERVED2 0xC000000B +#define USBD_STATUS_BUFFER_OVERRUN 0xC000000C +#define USBD_STATUS_BUFFER_UNDERRUN 0xC000000D + +/* unknown */ +#define USBD_STATUS_NO_DATA 0xC000000E + +#define USBD_STATUS_NOT_ACCESSED 0xC000000F +#define USBD_STATUS_FIFO 0xC0000010 +#define USBD_STATUS_XACT_ERROR 0xC0000011 +#define USBD_STATUS_BABBLE_DETECTED 0xC0000012 +#define USBD_STATUS_DATA_BUFFER_ERROR 0xC0000013 + +#define USBD_STATUS_NOT_SUPPORTED 0xC0000E00 +#define USBD_STATUS_BUFFER_TOO_SMALL 0xC0003000 +#define USBD_STATUS_TIMEOUT 0xC0006000 +#define USBD_STATUS_DEVICE_GONE 0xC0007000 + +#define USBD_STATUS_NO_MEMORY 0x80000100 +#define USBD_STATUS_INVALID_URB_FUNCTION 0x80000200 +#define USBD_STATUS_INVALID_PARAMETER 0x80000300 +#define USBD_STATUS_REQUEST_FAILED 0x80000500 +#define USBD_STATUS_INVALID_PIPE_HANDLE 0x80000600 +#define USBD_STATUS_ERROR_SHORT_TRANSFER 0x80000900 + +// Values for URB TransferFlags Field +// + +/* + Set if data moves device->host +*/ +#define USBD_TRANSFER_DIRECTION 0x00000001 +/* + This bit if not set indicates that a short packet, and hence, + a short transfer is an error condition +*/ +#define USBD_SHORT_TRANSFER_OK 0x00000002 +/* + Subit the iso transfer on the next frame +*/ +#define USBD_START_ISO_TRANSFER_ASAP 0x00000004 +#define USBD_DEFAULT_PIPE_TRANSFER 0x00000008 + +#define USBD_TRANSFER_DIRECTION_FLAG(flags) ((flags)&USBD_TRANSFER_DIRECTION) + +#define USBD_TRANSFER_DIRECTION_OUT 0 +#define USBD_TRANSFER_DIRECTION_IN 1 + +#define VALID_TRANSFER_FLAGS_MASK USBD_SHORT_TRANSFER_OK | \ + USBD_TRANSFER_DIRECTION | \ + USBD_START_ISO_TRANSFER_ASAP | \ + USBD_DEFAULT_PIPE_TRANSFER) + +#define ENDPOINT_HALT 0x00 +#define DEVICE_REMOTE_WAKEUP 0x01 + +/* transfer type */ +#define CONTROL_TRANSFER 0x00 +#define ISOCHRONOUS_TRANSFER 0x01 +#define BULK_TRANSFER 0x02 +#define INTERRUPT_TRANSFER 0x03 + +#define ClearHubFeature (0x2000 | LIBUSB_REQUEST_CLEAR_FEATURE) +#define ClearPortFeature (0x2300 | LIBUSB_REQUEST_CLEAR_FEATURE) +#define GetHubDescriptor (0xa000 | LIBUSB_REQUEST_GET_DESCRIPTOR) +#define GetHubStatus (0xa000 | LIBUSB_REQUEST_GET_STATUS) +#define GetPortStatus (0xa300 | LIBUSB_REQUEST_GET_STATUS) +#define SetHubFeature (0x2000 | LIBUSB_REQUEST_SET_FEATURE) +#define SetPortFeature (0x2300 | LIBUSB_REQUEST_SET_FEATURE) + +#define USBD_PF_CHANGE_MAX_PACKET 0x00000001 +#define USBD_PF_SHORT_PACKET_OPT 0x00000002 +#define USBD_PF_ENABLE_RT_THREAD_ACCESS 0x00000004 +#define USBD_PF_MAP_ADD_TRANSFERS 0x00000008 + +/* feature request */ +#define URB_SET_FEATURE 0x00 +#define URB_CLEAR_FEATURE 0x01 + +#define USBD_PF_CHANGE_MAX_PACKET 0x00000001 +#define USBD_PF_SHORT_PACKET_OPT 0x00000002 +#define USBD_PF_ENABLE_RT_THREAD_ACCESS 0x00000004 +#define USBD_PF_MAP_ADD_TRANSFERS 0x00000008 + +#define URB_CONTROL_TRANSFER_EXTERNAL 0x1 +#define URB_CONTROL_TRANSFER_NONEXTERNAL 0x0 + +#define USBFS_URB_SHORT_NOT_OK 0x01 +#define USBFS_URB_ISO_ASAP 0x02 +#define USBFS_URB_BULK_CONTINUATION 0x04 +#define USBFS_URB_QUEUE_BULK 0x10 + +#define URBDRC_DEVICE_INITIALIZED 0x01 +#define URBDRC_DEVICE_NOT_FOUND 0x02 +#define URBDRC_DEVICE_CHANNEL_CLOSED 0x08 +#define URBDRC_DEVICE_ALREADY_SEND 0x10 +#define URBDRC_DEVICE_DETACH_KERNEL 0x20 + +#define UDEVMAN_FLAG_ADD_BY_VID_PID 0x01 +#define UDEVMAN_FLAG_ADD_BY_ADDR 0x02 +#define UDEVMAN_FLAG_ADD_BY_AUTO 0x04 +#define UDEVMAN_FLAG_DEBUG 0x08 + +#endif /* FREERDP_CHANNEL_URBDRC_CLIENT_TYPES_H */ diff --git a/channels/video/CMakeLists.txt b/channels/video/CMakeLists.txt new file mode 100644 index 0000000..f03c851 --- /dev/null +++ b/channels/video/CMakeLists.txt @@ -0,0 +1,22 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2017 David Fort +# +# Licensed 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. + +define_channel("video") + +if(WITH_CLIENT_CHANNELS) + add_channel_client(${MODULE_PREFIX} ${CHANNEL_NAME}) +endif() diff --git a/channels/video/ChannelOptions.cmake b/channels/video/ChannelOptions.cmake new file mode 100644 index 0000000..e7f9ce8 --- /dev/null +++ b/channels/video/ChannelOptions.cmake @@ -0,0 +1,12 @@ + +set(OPTION_DEFAULT OFF) +set(OPTION_CLIENT_DEFAULT ON) +set(OPTION_SERVER_DEFAULT OFF) + +define_channel_options(NAME "video" TYPE "dynamic" + DESCRIPTION "Video optimized remoting Virtual Channel Extension" + SPECIFICATIONS "[MS-RDPEVOR]" + DEFAULT ${OPTION_DEFAULT}) + +define_channel_client_options(${OPTION_CLIENT_DEFAULT}) + diff --git a/channels/video/client/CMakeLists.txt b/channels/video/client/CMakeLists.txt new file mode 100644 index 0000000..cbbe483 --- /dev/null +++ b/channels/video/client/CMakeLists.txt @@ -0,0 +1,39 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2018 David Fort +# +# Licensed 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. + +define_channel_client("video") + +set(${MODULE_PREFIX}_SRCS + video_main.c + video_main.h) + +include_directories(..) + +add_channel_client_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} TRUE "DVCPluginEntry") + + + +set(${MODULE_PREFIX}_LIBS ${${MODULE_PREFIX}_LIBS} winpr) + +target_link_libraries(${MODULE_NAME} ${${MODULE_PREFIX}_LIBS}) + + +if (WITH_DEBUG_SYMBOLS AND MSVC AND NOT BUILTIN_CHANNELS AND BUILD_SHARED_LIBS) + install(FILES ${CMAKE_BINARY_DIR}/${MODULE_NAME}.pdb DESTINATION ${FREERDP_ADDIN_PATH} COMPONENT symbols) +endif() + +set_property(TARGET ${MODULE_NAME} PROPERTY FOLDER "Channels/${CHANNEL_NAME}/Client") diff --git a/channels/video/client/video_main.c b/channels/video/client/video_main.c new file mode 100644 index 0000000..a8031fc --- /dev/null +++ b/channels/video/client/video_main.c @@ -0,0 +1,1190 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Video Optimized Remoting Virtual Channel Extension + * + * Copyright 2017 David Fort + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#define TAG CHANNELS_TAG("video") + +#include "video_main.h" + +struct _VIDEO_CHANNEL_CALLBACK +{ + IWTSVirtualChannelCallback iface; + + IWTSPlugin* plugin; + IWTSVirtualChannelManager* channel_mgr; + IWTSVirtualChannel* channel; +}; +typedef struct _VIDEO_CHANNEL_CALLBACK VIDEO_CHANNEL_CALLBACK; + +struct _VIDEO_LISTENER_CALLBACK +{ + IWTSListenerCallback iface; + + IWTSPlugin* plugin; + IWTSVirtualChannelManager* channel_mgr; + VIDEO_CHANNEL_CALLBACK* channel_callback; +}; +typedef struct _VIDEO_LISTENER_CALLBACK VIDEO_LISTENER_CALLBACK; + +struct _VIDEO_PLUGIN +{ + IWTSPlugin wtsPlugin; + + IWTSListener* controlListener; + IWTSListener* dataListener; + VIDEO_LISTENER_CALLBACK* control_callback; + VIDEO_LISTENER_CALLBACK* data_callback; + + VideoClientContext* context; + BOOL initialized; +}; +typedef struct _VIDEO_PLUGIN VIDEO_PLUGIN; + +#define XF_VIDEO_UNLIMITED_RATE 31 + +static const BYTE MFVideoFormat_H264[] = { 'H', '2', '6', '4', 0x00, 0x00, 0x10, 0x00, + 0x80, 0x00, 0x00, 0xAA, 0x00, 0x38, 0x9B, 0x71 }; + +typedef struct _PresentationContext PresentationContext; +typedef struct _VideoFrame VideoFrame; + +/** @brief private data for the channel */ +struct _VideoClientContextPriv +{ + VideoClientContext* video; + GeometryClientContext* geometry; + wQueue* frames; + CRITICAL_SECTION framesLock; + wBufferPool* surfacePool; + UINT32 publishedFrames; + UINT32 droppedFrames; + UINT32 lastSentRate; + UINT64 nextFeedbackTime; + PresentationContext* currentPresentation; +}; + +/** @brief */ +struct _VideoFrame +{ + UINT64 publishTime; + UINT64 hnsDuration; + MAPPED_GEOMETRY* geometry; + UINT32 w, h; + BYTE* surfaceData; + PresentationContext* presentation; +}; + +/** @brief */ +struct _PresentationContext +{ + VideoClientContext* video; + BYTE PresentationId; + UINT32 SourceWidth, SourceHeight; + UINT32 ScaledWidth, ScaledHeight; + MAPPED_GEOMETRY* geometry; + + UINT64 startTimeStamp; + UINT64 publishOffset; + H264_CONTEXT* h264; + YUV_CONTEXT* yuv; + wStream* currentSample; + UINT64 lastPublishTime, nextPublishTime; + volatile LONG refCounter; + BYTE* surfaceData; + VideoSurface* surface; +}; + +static const char* video_command_name(BYTE cmd) +{ + switch (cmd) + { + case TSMM_START_PRESENTATION: + return "start"; + case TSMM_STOP_PRESENTATION: + return "stop"; + default: + return ""; + } +} + +static BOOL yuv_to_rgb(PresentationContext* presentation, BYTE* dest) +{ + const BYTE* pYUVPoint[3]; + H264_CONTEXT* h264 = presentation->h264; + + BYTE** ppYUVData; + ppYUVData = h264->pYUVData; + + pYUVPoint[0] = ppYUVData[0]; + pYUVPoint[1] = ppYUVData[1]; + pYUVPoint[2] = ppYUVData[2]; + + if (!yuv_context_decode(presentation->yuv, pYUVPoint, h264->iStride, PIXEL_FORMAT_BGRX32, dest, + h264->width * 4)) + { + WLog_ERR(TAG, "error in yuv_to_rgb conversion"); + return FALSE; + } + + return TRUE; +} + +static void video_client_context_set_geometry(VideoClientContext* video, + GeometryClientContext* geometry) +{ + video->priv->geometry = geometry; +} + +static VideoClientContextPriv* VideoClientContextPriv_new(VideoClientContext* video) +{ + VideoClientContextPriv* ret = calloc(1, sizeof(*ret)); + if (!ret) + return NULL; + + ret->frames = Queue_New(TRUE, 10, 2); + if (!ret->frames) + { + WLog_ERR(TAG, "unable to allocate frames queue"); + goto error_frames; + } + + ret->surfacePool = BufferPool_New(FALSE, 0, 16); + if (!ret->surfacePool) + { + WLog_ERR(TAG, "unable to create surface pool"); + goto error_surfacePool; + } + + if (!InitializeCriticalSectionAndSpinCount(&ret->framesLock, 4 * 1000)) + { + WLog_ERR(TAG, "unable to initialize frames lock"); + goto error_spinlock; + } + + ret->video = video; + + /* don't set to unlimited so that we have the chance to send a feedback in + * the first second (for servers that want feedback directly) + */ + ret->lastSentRate = 30; + return ret; + +error_spinlock: + BufferPool_Free(ret->surfacePool); +error_surfacePool: + Queue_Free(ret->frames); +error_frames: + free(ret); + return NULL; +} + +static PresentationContext* PresentationContext_new(VideoClientContext* video, BYTE PresentationId, + UINT32 x, UINT32 y, UINT32 width, UINT32 height) +{ + size_t s; + VideoClientContextPriv* priv = video->priv; + PresentationContext* ret; + s = width * height * 4ULL; + if (s > INT32_MAX) + return NULL; + + ret = calloc(1, sizeof(*ret)); + if (!ret) + return NULL; + + ret->video = video; + ret->PresentationId = PresentationId; + + ret->h264 = h264_context_new(FALSE); + if (!ret->h264) + { + WLog_ERR(TAG, "unable to create a h264 context"); + goto error_h264; + } + h264_context_reset(ret->h264, width, height); + + ret->currentSample = Stream_New(NULL, 4096); + if (!ret->currentSample) + { + WLog_ERR(TAG, "unable to create current packet stream"); + goto error_currentSample; + } + + ret->surfaceData = BufferPool_Take(priv->surfacePool, s); + if (!ret->surfaceData) + { + WLog_ERR(TAG, "unable to allocate surfaceData"); + goto error_surfaceData; + } + + ret->surface = video->createSurface(video, ret->surfaceData, x, y, width, height); + if (!ret->surface) + { + WLog_ERR(TAG, "unable to create surface"); + goto error_surface; + } + + ret->yuv = yuv_context_new(FALSE); + if (!ret->yuv) + { + WLog_ERR(TAG, "unable to create YUV decoder"); + goto error_yuv; + } + + yuv_context_reset(ret->yuv, width, height); + ret->refCounter = 1; + return ret; + +error_yuv: + video->deleteSurface(video, ret->surface); +error_surface: + BufferPool_Return(priv->surfacePool, ret->surfaceData); +error_surfaceData: + Stream_Free(ret->currentSample, TRUE); +error_currentSample: + h264_context_free(ret->h264); +error_h264: + free(ret); + return NULL; +} + +static void PresentationContext_unref(PresentationContext* presentation) +{ + VideoClientContextPriv* priv; + MAPPED_GEOMETRY* geometry; + + if (!presentation) + return; + + if (InterlockedDecrement(&presentation->refCounter) != 0) + return; + + geometry = presentation->geometry; + if (geometry) + { + geometry->MappedGeometryUpdate = NULL; + geometry->MappedGeometryClear = NULL; + geometry->custom = NULL; + mappedGeometryUnref(geometry); + } + + priv = presentation->video->priv; + + h264_context_free(presentation->h264); + Stream_Free(presentation->currentSample, TRUE); + presentation->video->deleteSurface(presentation->video, presentation->surface); + BufferPool_Return(priv->surfacePool, presentation->surfaceData); + yuv_context_free(presentation->yuv); + free(presentation); +} + +static void VideoFrame_free(VideoFrame** pframe) +{ + VideoFrame* frame = *pframe; + + mappedGeometryUnref(frame->geometry); + BufferPool_Return(frame->presentation->video->priv->surfacePool, frame->surfaceData); + PresentationContext_unref(frame->presentation); + free(frame); + *pframe = NULL; +} + +static void VideoClientContextPriv_free(VideoClientContextPriv* priv) +{ + EnterCriticalSection(&priv->framesLock); + while (Queue_Count(priv->frames)) + { + VideoFrame* frame = Queue_Dequeue(priv->frames); + if (frame) + VideoFrame_free(&frame); + } + + Queue_Free(priv->frames); + LeaveCriticalSection(&priv->framesLock); + + DeleteCriticalSection(&priv->framesLock); + + if (priv->currentPresentation) + PresentationContext_unref(priv->currentPresentation); + + BufferPool_Free(priv->surfacePool); + free(priv); +} + +static UINT video_control_send_presentation_response(VideoClientContext* context, + TSMM_PRESENTATION_RESPONSE* resp) +{ + BYTE buf[12]; + wStream* s; + VIDEO_PLUGIN* video = (VIDEO_PLUGIN*)context->handle; + IWTSVirtualChannel* channel; + UINT ret; + + s = Stream_New(buf, 12); + if (!s) + return CHANNEL_RC_NO_MEMORY; + + Stream_Write_UINT32(s, 12); /* cbSize */ + Stream_Write_UINT32(s, TSMM_PACKET_TYPE_PRESENTATION_RESPONSE); /* PacketType */ + Stream_Write_UINT8(s, resp->PresentationId); + Stream_Zero(s, 3); + Stream_SealLength(s); + + channel = video->control_callback->channel_callback->channel; + ret = channel->Write(channel, 12, buf, NULL); + Stream_Free(s, FALSE); + + return ret; +} + +static BOOL video_onMappedGeometryUpdate(MAPPED_GEOMETRY* geometry) +{ + PresentationContext* presentation = (PresentationContext*)geometry->custom; + RDP_RECT* r = &geometry->geometry.boundingRect; + WLog_DBG(TAG, "geometry updated topGeom=(%d,%d-%dx%d) geom=(%d,%d-%dx%d) rects=(%d,%d-%dx%d)", + geometry->topLevelLeft, geometry->topLevelTop, + geometry->topLevelRight - geometry->topLevelLeft, + geometry->topLevelBottom - geometry->topLevelTop, + + geometry->left, geometry->top, geometry->right - geometry->left, + geometry->bottom - geometry->top, + + r->x, r->y, r->width, r->height); + + presentation->surface->x = geometry->topLevelLeft + geometry->left; + presentation->surface->y = geometry->topLevelTop + geometry->top; + + return TRUE; +} + +static BOOL video_onMappedGeometryClear(MAPPED_GEOMETRY* geometry) +{ + PresentationContext* presentation = (PresentationContext*)geometry->custom; + + mappedGeometryUnref(presentation->geometry); + presentation->geometry = NULL; + return TRUE; +} + +static UINT video_PresentationRequest(VideoClientContext* video, TSMM_PRESENTATION_REQUEST* req) +{ + VideoClientContextPriv* priv = video->priv; + PresentationContext* presentation; + UINT ret = CHANNEL_RC_OK; + + presentation = priv->currentPresentation; + + if (req->Command == TSMM_START_PRESENTATION) + { + MAPPED_GEOMETRY* geom; + TSMM_PRESENTATION_RESPONSE resp; + + if (memcmp(req->VideoSubtypeId, MFVideoFormat_H264, 16) != 0) + { + WLog_ERR(TAG, "not a H264 video, ignoring request"); + return CHANNEL_RC_OK; + } + + if (presentation) + { + if (presentation->PresentationId == req->PresentationId) + { + WLog_ERR(TAG, "ignoring start request for existing presentation %d", + req->PresentationId); + return CHANNEL_RC_OK; + } + + WLog_ERR(TAG, "releasing current presentation %d", req->PresentationId); + PresentationContext_unref(presentation); + presentation = priv->currentPresentation = NULL; + } + + if (!priv->geometry) + { + WLog_ERR(TAG, "geometry channel not ready, ignoring request"); + return CHANNEL_RC_OK; + } + + geom = HashTable_GetItemValue(priv->geometry->geometries, &(req->GeometryMappingId)); + if (!geom) + { + WLog_ERR(TAG, "geometry mapping 0x%" PRIx64 " not registered", req->GeometryMappingId); + return CHANNEL_RC_OK; + } + + WLog_DBG(TAG, "creating presentation 0x%x", req->PresentationId); + presentation = PresentationContext_new( + video, req->PresentationId, geom->topLevelLeft + geom->left, + geom->topLevelTop + geom->top, req->SourceWidth, req->SourceHeight); + if (!presentation) + { + WLog_ERR(TAG, "unable to create presentation video"); + return CHANNEL_RC_NO_MEMORY; + } + + mappedGeometryRef(geom); + presentation->geometry = geom; + + priv->currentPresentation = presentation; + presentation->video = video; + presentation->SourceWidth = req->SourceWidth; + presentation->SourceHeight = req->SourceHeight; + presentation->ScaledWidth = req->ScaledWidth; + presentation->ScaledHeight = req->ScaledHeight; + + geom->custom = presentation; + geom->MappedGeometryUpdate = video_onMappedGeometryUpdate; + geom->MappedGeometryClear = video_onMappedGeometryClear; + + /* send back response */ + resp.PresentationId = req->PresentationId; + ret = video_control_send_presentation_response(video, &resp); + } + else if (req->Command == TSMM_STOP_PRESENTATION) + { + WLog_DBG(TAG, "stopping presentation 0x%x", req->PresentationId); + if (!presentation) + { + WLog_ERR(TAG, "unknown presentation to stop %d", req->PresentationId); + return CHANNEL_RC_OK; + } + + priv->currentPresentation = NULL; + priv->droppedFrames = 0; + priv->publishedFrames = 0; + PresentationContext_unref(presentation); + } + + return ret; +} + +static UINT video_read_tsmm_presentation_req(VideoClientContext* context, wStream* s) +{ + TSMM_PRESENTATION_REQUEST req; + + if (Stream_GetRemainingLength(s) < 60) + { + WLog_ERR(TAG, "not enough bytes for a TSMM_PRESENTATION_REQUEST"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT8(s, req.PresentationId); + Stream_Read_UINT8(s, req.Version); + Stream_Read_UINT8(s, req.Command); + Stream_Read_UINT8(s, req.FrameRate); /* FrameRate - reserved and ignored */ + + Stream_Seek_UINT16(s); /* AverageBitrateKbps reserved and ignored */ + Stream_Seek_UINT16(s); /* reserved */ + + Stream_Read_UINT32(s, req.SourceWidth); + Stream_Read_UINT32(s, req.SourceHeight); + Stream_Read_UINT32(s, req.ScaledWidth); + Stream_Read_UINT32(s, req.ScaledHeight); + Stream_Read_UINT64(s, req.hnsTimestampOffset); + Stream_Read_UINT64(s, req.GeometryMappingId); + Stream_Read(s, req.VideoSubtypeId, 16); + + Stream_Read_UINT32(s, req.cbExtra); + + if (Stream_GetRemainingLength(s) < req.cbExtra) + { + WLog_ERR(TAG, "not enough bytes for cbExtra of TSMM_PRESENTATION_REQUEST"); + return ERROR_INVALID_DATA; + } + + req.pExtraData = Stream_Pointer(s); + + WLog_DBG(TAG, + "presentationReq: id:%" PRIu8 " version:%" PRIu8 + " command:%s srcWidth/srcHeight=%" PRIu32 "x%" PRIu32 " scaled Width/Height=%" PRIu32 + "x%" PRIu32 " timestamp=%" PRIu64 " mappingId=%" PRIx64 "", + req.PresentationId, req.Version, video_command_name(req.Command), req.SourceWidth, + req.SourceHeight, req.ScaledWidth, req.ScaledHeight, req.hnsTimestampOffset, + req.GeometryMappingId); + + return video_PresentationRequest(context, &req); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT video_control_on_data_received(IWTSVirtualChannelCallback* pChannelCallback, wStream* s) +{ + VIDEO_CHANNEL_CALLBACK* callback = (VIDEO_CHANNEL_CALLBACK*)pChannelCallback; + VIDEO_PLUGIN* video; + VideoClientContext* context; + UINT ret = CHANNEL_RC_OK; + UINT32 cbSize, packetType; + + video = (VIDEO_PLUGIN*)callback->plugin; + context = (VideoClientContext*)video->wtsPlugin.pInterface; + + if (Stream_GetRemainingLength(s) < 4) + return ERROR_INVALID_DATA; + + Stream_Read_UINT32(s, cbSize); + if (cbSize < 8 || Stream_GetRemainingLength(s) < (cbSize - 4)) + { + WLog_ERR(TAG, "invalid cbSize"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT32(s, packetType); + switch (packetType) + { + case TSMM_PACKET_TYPE_PRESENTATION_REQUEST: + ret = video_read_tsmm_presentation_req(context, s); + break; + default: + WLog_ERR(TAG, "not expecting packet type %" PRIu32 "", packetType); + ret = ERROR_UNSUPPORTED_TYPE; + break; + } + + return ret; +} + +static UINT video_control_send_client_notification(VideoClientContext* context, + TSMM_CLIENT_NOTIFICATION* notif) +{ + BYTE buf[100]; + wStream* s; + VIDEO_PLUGIN* video = (VIDEO_PLUGIN*)context->handle; + IWTSVirtualChannel* channel; + UINT ret; + UINT32 cbSize; + + s = Stream_New(buf, 30); + if (!s) + return CHANNEL_RC_NO_MEMORY; + + cbSize = 16; + Stream_Seek_UINT32(s); /* cbSize */ + Stream_Write_UINT32(s, TSMM_PACKET_TYPE_CLIENT_NOTIFICATION); /* PacketType */ + Stream_Write_UINT8(s, notif->PresentationId); + Stream_Write_UINT8(s, notif->NotificationType); + Stream_Zero(s, 2); + if (notif->NotificationType == TSMM_CLIENT_NOTIFICATION_TYPE_FRAMERATE_OVERRIDE) + { + Stream_Write_UINT32(s, 16); /* cbData */ + + /* TSMM_CLIENT_NOTIFICATION_FRAMERATE_OVERRIDE */ + Stream_Write_UINT32(s, notif->FramerateOverride.Flags); + Stream_Write_UINT32(s, notif->FramerateOverride.DesiredFrameRate); + Stream_Zero(s, 4 * 2); + + cbSize += 4 * 4; + } + else + { + Stream_Write_UINT32(s, 0); /* cbData */ + } + + Stream_SealLength(s); + Stream_SetPosition(s, 0); + Stream_Write_UINT32(s, cbSize); + Stream_Free(s, FALSE); + + channel = video->control_callback->channel_callback->channel; + ret = channel->Write(channel, cbSize, buf, NULL); + + return ret; +} + +static void video_timer(VideoClientContext* video, UINT64 now) +{ + PresentationContext* presentation; + VideoClientContextPriv* priv = video->priv; + VideoFrame *peekFrame, *frame = NULL; + + EnterCriticalSection(&priv->framesLock); + do + { + peekFrame = (VideoFrame*)Queue_Peek(priv->frames); + if (!peekFrame) + break; + + if (peekFrame->publishTime > now) + break; + + if (frame) + { + WLog_DBG(TAG, "dropping frame @%" PRIu64, frame->publishTime); + priv->droppedFrames++; + VideoFrame_free(&frame); + } + frame = peekFrame; + Queue_Dequeue(priv->frames); + } while (1); + LeaveCriticalSection(&priv->framesLock); + + if (!frame) + goto treat_feedback; + + presentation = frame->presentation; + + priv->publishedFrames++; + memcpy(presentation->surfaceData, frame->surfaceData, frame->w * frame->h * 4ULL); + + video->showSurface(video, presentation->surface); + + VideoFrame_free(&frame); + +treat_feedback: + if (priv->nextFeedbackTime < now) + { + /* we can compute some feedback only if we have some published frames and + * a current presentation + */ + if (priv->publishedFrames && priv->currentPresentation) + { + UINT32 computedRate; + + InterlockedIncrement(&priv->currentPresentation->refCounter); + + if (priv->droppedFrames) + { + /** + * some dropped frames, looks like we're asking too many frames per seconds, + * try lowering rate. We go directly from unlimited rate to 24 frames/seconds + * otherwise we lower rate by 2 frames by seconds + */ + if (priv->lastSentRate == XF_VIDEO_UNLIMITED_RATE) + computedRate = 24; + else + { + computedRate = priv->lastSentRate - 2; + if (!computedRate) + computedRate = 2; + } + } + else + { + /** + * we treat all frames ok, so either ask the server to send more, + * or stay unlimited + */ + if (priv->lastSentRate == XF_VIDEO_UNLIMITED_RATE) + computedRate = XF_VIDEO_UNLIMITED_RATE; /* stay unlimited */ + else + { + computedRate = priv->lastSentRate + 2; + if (computedRate > XF_VIDEO_UNLIMITED_RATE) + computedRate = XF_VIDEO_UNLIMITED_RATE; + } + } + + if (computedRate != priv->lastSentRate) + { + TSMM_CLIENT_NOTIFICATION notif; + notif.PresentationId = priv->currentPresentation->PresentationId; + notif.NotificationType = TSMM_CLIENT_NOTIFICATION_TYPE_FRAMERATE_OVERRIDE; + if (computedRate == XF_VIDEO_UNLIMITED_RATE) + { + notif.FramerateOverride.Flags = 0x01; + notif.FramerateOverride.DesiredFrameRate = 0x00; + } + else + { + notif.FramerateOverride.Flags = 0x02; + notif.FramerateOverride.DesiredFrameRate = computedRate; + } + + video_control_send_client_notification(video, ¬if); + priv->lastSentRate = computedRate; + + WLog_DBG(TAG, "server notified with rate %d published=%d dropped=%d", + priv->lastSentRate, priv->publishedFrames, priv->droppedFrames); + } + + PresentationContext_unref(priv->currentPresentation); + } + + WLog_DBG(TAG, "currentRate=%d published=%d dropped=%d", priv->lastSentRate, + priv->publishedFrames, priv->droppedFrames); + + priv->droppedFrames = 0; + priv->publishedFrames = 0; + priv->nextFeedbackTime = now + 1000; + } +} + +static UINT video_VideoData(VideoClientContext* context, TSMM_VIDEO_DATA* data) +{ + VideoClientContextPriv* priv = context->priv; + PresentationContext* presentation; + int status; + + presentation = priv->currentPresentation; + if (!presentation) + { + WLog_ERR(TAG, "no current presentation"); + return CHANNEL_RC_OK; + } + + if (presentation->PresentationId != data->PresentationId) + { + WLog_ERR(TAG, "current presentation id=%d doesn't match data id=%d", + presentation->PresentationId, data->PresentationId); + return CHANNEL_RC_OK; + } + + if (!Stream_EnsureRemainingCapacity(presentation->currentSample, data->cbSample)) + { + WLog_ERR(TAG, "unable to expand the current packet"); + return CHANNEL_RC_NO_MEMORY; + } + + Stream_Write(presentation->currentSample, data->pSample, data->cbSample); + + if (data->CurrentPacketIndex == data->PacketsInSample) + { + H264_CONTEXT* h264 = presentation->h264; + UINT64 startTime = GetTickCount64(), timeAfterH264; + MAPPED_GEOMETRY* geom = presentation->geometry; + + Stream_SealLength(presentation->currentSample); + Stream_SetPosition(presentation->currentSample, 0); + + status = h264->subsystem->Decompress(h264, Stream_Pointer(presentation->currentSample), + Stream_Length(presentation->currentSample)); + if (status == 0) + return CHANNEL_RC_OK; + + if (status < 0) + return CHANNEL_RC_OK; + + timeAfterH264 = GetTickCount64(); + if (data->SampleNumber == 1) + { + presentation->lastPublishTime = startTime; + } + + presentation->lastPublishTime += (data->hnsDuration / 10000); + if (presentation->lastPublishTime <= timeAfterH264 + 10) + { + int dropped = 0; + + /* if the frame is to be published in less than 10 ms, let's consider it's now */ + yuv_to_rgb(presentation, presentation->surfaceData); + + context->showSurface(context, presentation->surface); + + priv->publishedFrames++; + + /* cleanup previously scheduled frames */ + EnterCriticalSection(&priv->framesLock); + while (Queue_Count(priv->frames) > 0) + { + VideoFrame* frame = Queue_Dequeue(priv->frames); + if (frame) + { + priv->droppedFrames++; + VideoFrame_free(&frame); + dropped++; + } + } + LeaveCriticalSection(&priv->framesLock); + + if (dropped) + WLog_DBG(TAG, "showing frame (%d dropped)", dropped); + } + else + { + BOOL enqueueResult; + VideoFrame* frame = calloc(1, sizeof(*frame)); + if (!frame) + { + WLog_ERR(TAG, "unable to create frame"); + return CHANNEL_RC_NO_MEMORY; + } + mappedGeometryRef(geom); + + frame->presentation = presentation; + frame->publishTime = presentation->lastPublishTime; + frame->geometry = geom; + frame->w = presentation->SourceWidth; + frame->h = presentation->SourceHeight; + + frame->surfaceData = BufferPool_Take(priv->surfacePool, frame->w * frame->h * 4ULL); + if (!frame->surfaceData) + { + WLog_ERR(TAG, "unable to allocate frame data"); + mappedGeometryUnref(geom); + free(frame); + return CHANNEL_RC_NO_MEMORY; + } + + if (!yuv_to_rgb(presentation, frame->surfaceData)) + { + WLog_ERR(TAG, "error during YUV->RGB conversion"); + BufferPool_Return(priv->surfacePool, frame->surfaceData); + mappedGeometryUnref(geom); + free(frame); + return CHANNEL_RC_NO_MEMORY; + } + + InterlockedIncrement(&presentation->refCounter); + + EnterCriticalSection(&priv->framesLock); + enqueueResult = Queue_Enqueue(priv->frames, frame); + LeaveCriticalSection(&priv->framesLock); + + if (!enqueueResult) + { + WLog_ERR(TAG, "unable to enqueue frame"); + VideoFrame_free(&frame); + return CHANNEL_RC_NO_MEMORY; + } + + WLog_DBG(TAG, "scheduling frame in %" PRIu32 " ms", (frame->publishTime - startTime)); + } + } + + return CHANNEL_RC_OK; +} + +static UINT video_data_on_data_received(IWTSVirtualChannelCallback* pChannelCallback, wStream* s) +{ + VIDEO_CHANNEL_CALLBACK* callback = (VIDEO_CHANNEL_CALLBACK*)pChannelCallback; + VIDEO_PLUGIN* video; + VideoClientContext* context; + UINT32 cbSize, packetType; + TSMM_VIDEO_DATA data; + + video = (VIDEO_PLUGIN*)callback->plugin; + context = (VideoClientContext*)video->wtsPlugin.pInterface; + + if (Stream_GetRemainingLength(s) < 4) + return ERROR_INVALID_DATA; + + Stream_Read_UINT32(s, cbSize); + if (cbSize < 8 || Stream_GetRemainingLength(s) < (cbSize - 4)) + { + WLog_ERR(TAG, "invalid cbSize"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT32(s, packetType); + if (packetType != TSMM_PACKET_TYPE_VIDEO_DATA) + { + WLog_ERR(TAG, "only expecting VIDEO_DATA on the data channel"); + return ERROR_INVALID_DATA; + } + + if (Stream_GetRemainingLength(s) < 32) + { + WLog_ERR(TAG, "not enough bytes for a TSMM_VIDEO_DATA"); + return ERROR_INVALID_DATA; + } + + Stream_Read_UINT8(s, data.PresentationId); + Stream_Read_UINT8(s, data.Version); + Stream_Read_UINT8(s, data.Flags); + Stream_Seek_UINT8(s); /* reserved */ + Stream_Read_UINT64(s, data.hnsTimestamp); + Stream_Read_UINT64(s, data.hnsDuration); + Stream_Read_UINT16(s, data.CurrentPacketIndex); + Stream_Read_UINT16(s, data.PacketsInSample); + Stream_Read_UINT32(s, data.SampleNumber); + Stream_Read_UINT32(s, data.cbSample); + if (!Stream_CheckAndLogRequiredLength(TAG, s, data.cbSample)) + return ERROR_INVALID_DATA; + data.pSample = Stream_Pointer(s); + + /* + WLog_DBG(TAG, "videoData: id:%"PRIu8" version:%"PRIu8" flags:0x%"PRIx8" timestamp=%"PRIu64" + duration=%"PRIu64 " curPacketIndex:%"PRIu16" packetInSample:%"PRIu16" sampleNumber:%"PRIu32" + cbSample:%"PRIu32"", data.PresentationId, data.Version, data.Flags, data.hnsTimestamp, + data.hnsDuration, data.CurrentPacketIndex, data.PacketsInSample, data.SampleNumber, + data.cbSample); + */ + + return video_VideoData(context, &data); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT video_control_on_close(IWTSVirtualChannelCallback* pChannelCallback) +{ + free(pChannelCallback); + return CHANNEL_RC_OK; +} + +static UINT video_data_on_close(IWTSVirtualChannelCallback* pChannelCallback) +{ + free(pChannelCallback); + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT video_control_on_new_channel_connection(IWTSListenerCallback* listenerCallback, + IWTSVirtualChannel* channel, BYTE* Data, + BOOL* pbAccept, + IWTSVirtualChannelCallback** ppCallback) +{ + VIDEO_CHANNEL_CALLBACK* callback; + VIDEO_LISTENER_CALLBACK* listener_callback = (VIDEO_LISTENER_CALLBACK*)listenerCallback; + + WINPR_UNUSED(Data); + WINPR_UNUSED(pbAccept); + + callback = (VIDEO_CHANNEL_CALLBACK*)calloc(1, sizeof(VIDEO_CHANNEL_CALLBACK)); + if (!callback) + { + WLog_ERR(TAG, "calloc failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + callback->iface.OnDataReceived = video_control_on_data_received; + callback->iface.OnClose = video_control_on_close; + callback->plugin = listener_callback->plugin; + callback->channel_mgr = listener_callback->channel_mgr; + callback->channel = channel; + listener_callback->channel_callback = callback; + + *ppCallback = (IWTSVirtualChannelCallback*)callback; + + return CHANNEL_RC_OK; +} + +static UINT video_data_on_new_channel_connection(IWTSListenerCallback* pListenerCallback, + IWTSVirtualChannel* pChannel, BYTE* Data, + BOOL* pbAccept, + IWTSVirtualChannelCallback** ppCallback) +{ + VIDEO_CHANNEL_CALLBACK* callback; + VIDEO_LISTENER_CALLBACK* listener_callback = (VIDEO_LISTENER_CALLBACK*)pListenerCallback; + + WINPR_UNUSED(Data); + WINPR_UNUSED(pbAccept); + + callback = (VIDEO_CHANNEL_CALLBACK*)calloc(1, sizeof(VIDEO_CHANNEL_CALLBACK)); + if (!callback) + { + WLog_ERR(TAG, "calloc failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + callback->iface.OnDataReceived = video_data_on_data_received; + callback->iface.OnClose = video_data_on_close; + callback->plugin = listener_callback->plugin; + callback->channel_mgr = listener_callback->channel_mgr; + callback->channel = pChannel; + listener_callback->channel_callback = callback; + + *ppCallback = (IWTSVirtualChannelCallback*)callback; + + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT video_plugin_initialize(IWTSPlugin* plugin, IWTSVirtualChannelManager* channelMgr) +{ + UINT status; + VIDEO_PLUGIN* video = (VIDEO_PLUGIN*)plugin; + VIDEO_LISTENER_CALLBACK* callback; + + if (video->initialized) + { + WLog_ERR(TAG, "[%s] channel initialized twice, aborting", VIDEO_CONTROL_DVC_CHANNEL_NAME); + return ERROR_INVALID_DATA; + } + video->control_callback = callback = + (VIDEO_LISTENER_CALLBACK*)calloc(1, sizeof(VIDEO_LISTENER_CALLBACK)); + if (!callback) + { + WLog_ERR(TAG, "calloc for control callback failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + callback->iface.OnNewChannelConnection = video_control_on_new_channel_connection; + callback->plugin = plugin; + callback->channel_mgr = channelMgr; + + status = channelMgr->CreateListener(channelMgr, VIDEO_CONTROL_DVC_CHANNEL_NAME, 0, + &callback->iface, &(video->controlListener)); + + if (status != CHANNEL_RC_OK) + return status; + video->controlListener->pInterface = video->wtsPlugin.pInterface; + + video->data_callback = callback = + (VIDEO_LISTENER_CALLBACK*)calloc(1, sizeof(VIDEO_LISTENER_CALLBACK)); + if (!callback) + { + WLog_ERR(TAG, "calloc for data callback failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + callback->iface.OnNewChannelConnection = video_data_on_new_channel_connection; + callback->plugin = plugin; + callback->channel_mgr = channelMgr; + + status = channelMgr->CreateListener(channelMgr, VIDEO_DATA_DVC_CHANNEL_NAME, 0, + &callback->iface, &(video->dataListener)); + + if (status == CHANNEL_RC_OK) + video->dataListener->pInterface = video->wtsPlugin.pInterface; + + video->initialized = status == CHANNEL_RC_OK; + return status; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT video_plugin_terminated(IWTSPlugin* pPlugin) +{ + VIDEO_PLUGIN* video = (VIDEO_PLUGIN*)pPlugin; + + if (video->control_callback) + { + IWTSVirtualChannelManager* mgr = video->control_callback->channel_mgr; + if (mgr) + IFCALL(mgr->DestroyListener, mgr, video->controlListener); + } + if (video->data_callback) + { + IWTSVirtualChannelManager* mgr = video->data_callback->channel_mgr; + if (mgr) + IFCALL(mgr->DestroyListener, mgr, video->dataListener); + } + + if (video->context) + VideoClientContextPriv_free(video->context->priv); + + free(video->control_callback); + free(video->data_callback); + free(video->wtsPlugin.pInterface); + free(pPlugin); + return CHANNEL_RC_OK; +} + +/** + * Channel Client Interface + */ + +#ifdef BUILTIN_CHANNELS +#define DVCPluginEntry video_DVCPluginEntry +#else +#define DVCPluginEntry FREERDP_API DVCPluginEntry +#endif + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT DVCPluginEntry(IDRDYNVC_ENTRY_POINTS* pEntryPoints) +{ + UINT error = CHANNEL_RC_OK; + VIDEO_PLUGIN* videoPlugin; + VideoClientContext* videoContext; + VideoClientContextPriv* priv; + + videoPlugin = (VIDEO_PLUGIN*)pEntryPoints->GetPlugin(pEntryPoints, "video"); + if (!videoPlugin) + { + videoPlugin = (VIDEO_PLUGIN*)calloc(1, sizeof(VIDEO_PLUGIN)); + if (!videoPlugin) + { + WLog_ERR(TAG, "calloc failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + videoPlugin->wtsPlugin.Initialize = video_plugin_initialize; + videoPlugin->wtsPlugin.Connected = NULL; + videoPlugin->wtsPlugin.Disconnected = NULL; + videoPlugin->wtsPlugin.Terminated = video_plugin_terminated; + + videoContext = (VideoClientContext*)calloc(1, sizeof(VideoClientContext)); + if (!videoContext) + { + WLog_ERR(TAG, "calloc failed!"); + free(videoPlugin); + return CHANNEL_RC_NO_MEMORY; + } + + priv = VideoClientContextPriv_new(videoContext); + if (!priv) + { + WLog_ERR(TAG, "VideoClientContextPriv_new failed!"); + free(videoContext); + free(videoPlugin); + return CHANNEL_RC_NO_MEMORY; + } + + videoContext->handle = (void*)videoPlugin; + videoContext->priv = priv; + videoContext->timer = video_timer; + videoContext->setGeometry = video_client_context_set_geometry; + + videoPlugin->wtsPlugin.pInterface = (void*)videoContext; + videoPlugin->context = videoContext; + + error = pEntryPoints->RegisterPlugin(pEntryPoints, "video", (IWTSPlugin*)videoPlugin); + } + else + { + WLog_ERR(TAG, "could not get video Plugin."); + return CHANNEL_RC_BAD_CHANNEL; + } + + return error; +} diff --git a/channels/video/client/video_main.h b/channels/video/client/video_main.h new file mode 100644 index 0000000..1814566 --- /dev/null +++ b/channels/video/client/video_main.h @@ -0,0 +1,33 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Video Optimized Remoting Virtual Channel Extension + * + * Copyright 2017 David Fort + * + * Licensed 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 FREERDP_CHANNEL_VIDEO_CLIENT_MAIN_H +#define FREERDP_CHANNEL_VIDEO_CLIENT_MAIN_H + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include + +#include + +#endif /* FREERDP_CHANNEL_GEOMETRY_CLIENT_MAIN_H */ diff --git a/ci/cmake-preloads/config-android.txt b/ci/cmake-preloads/config-android.txt new file mode 100644 index 0000000..bcb3446 --- /dev/null +++ b/ci/cmake-preloads/config-android.txt @@ -0,0 +1,7 @@ +message("PRELOADING android cache") +set(CMAKE_TOOLCHAIN_FILE "$ANDROID_NDK/build/cmake/android.toolchain.cmake" CACHE PATH "ToolChain file") +set(WITH_SANITIZE_ADDRESS ON) +set(FREERDP_EXTERNAL_SSL_PATH $ENV{ANDROID_SSL_PATH} CACHE PATH "android ssl") +# ANDROID_NDK and ANDROID_SDK must be set as environment variable +#set(ANDROID_NDK $ENV{ANDROID_SDK} CACHE PATH "Android NDK") +#set(ANDROID_SDK "${ANDROID_NDK}" CACHE PATH "android SDK") diff --git a/ci/cmake-preloads/config-debian-squeeze.txt b/ci/cmake-preloads/config-debian-squeeze.txt new file mode 100644 index 0000000..c7319cf --- /dev/null +++ b/ci/cmake-preloads/config-debian-squeeze.txt @@ -0,0 +1,11 @@ +message("PRELOADING cache") +set (WITH_MANPAGES OFF CACHE BOOL "man pages") +set (CMAKE_BUILD_TYPE "Debug" CACHE STRING "build type") +set (WITH_CUPS OFF CACHE BOOL "CUPS printing") +set (WITH_GSSAPI ON CACHE BOOL "Kerberos support") +set (WITH_ALSA OFF CACHE BOOL "alsa audio") +set (WITH_FFMPEG OFF CACHE BOOL "ffmepg support") +set (WITH_XV OFF CACHE BOOL "xvideo support") +set (BUILD_TESTING ON CACHE BOOL "build testing") +set (WITH_XSHM OFF CACHE BOOL "build with xshm support") +set (WITH_SANITIZE_ADDRESS ON) diff --git a/ci/cmake-preloads/config-ios.txt b/ci/cmake-preloads/config-ios.txt new file mode 100644 index 0000000..08145bb --- /dev/null +++ b/ci/cmake-preloads/config-ios.txt @@ -0,0 +1,8 @@ +message("PRELOADING iOS cache") +set (CMAKE_TOOLCHAIN_FILE "${CMAKE_SOURCE_DIR}/cmake/ios.toolchain.cmake" CACHE PATH "cmake toolchain file") +set (FREERDP_IOS_EXTERNAL_SSL_PATH $ENV{FREERDP_IOS_EXTERNAL_SSL_PATH} CACHE PATH "android ssl") +set (CMAKE_BUILD_TYPE "Debug" CACHE STRING "build type") +set (CMAKE_OSX_ARCHITECTURES "arm64" CACHE STRING "iOS platform to build") +set (CMAKE_OSX_DEPLOYMENT_TARGET "10.0" CACHE STRING "iOS minimum target") +set (WITH_SANITIZE_ADDRESS ON CACHE BOOL "build with address sanitizer") +set (WITH_CLIENT OFF CACHE BOOL "disable iOS client") diff --git a/ci/cmake-preloads/config-linux-all.txt b/ci/cmake-preloads/config-linux-all.txt new file mode 100644 index 0000000..4ba743e --- /dev/null +++ b/ci/cmake-preloads/config-linux-all.txt @@ -0,0 +1,53 @@ +message("PRELOADING cache") +set (BUILD_TESTING ON CACHE BOOL "testing") +set (WITH_MANPAGES OFF CACHE BOOL "man pages") +set (CMAKE_BUILD_TYPE "Debug" CACHE STRING "build type") +set (BUILD_TESTING ON CACHE BOOL "build testing") +set (WITH_PULSE ON CACHE BOOL "pulse") +set (WITH_CHANNELS ON CACHE BOOL "channels") +set (BUILTIN_CHANNELS ON CACHE BOOL "static channels") +set (WITH_CUPS ON CACHE BOOL "cups") +set (WITH_WAYLAND ON CACHE BOOL "wayland") +set (WITH_GSSAPI ON CACHE BOOL "Kerberos support") +set (WITH_PCSC ON CACHE BOOL "PCSC") +set (WITH_JPEG ON CACHE BOOL "jpeg") +set (WITH_GSTREAMER_0_10 ON CACHE BOOL "gstreamer") +set (WITH_GSM ON CACHE BOOL "gsm") +set (CHANNEL_URBDRC ON CACHE BOOL "urbdrc") +set (CHANNEL_URBDRC_CLIENT ON CACHE BOOL "urbdrc client") +set (WITH_SERVER ON CACHE BOOL "server side") +set (WITH_DEBUG_ALL OFF CACHE BOOL "enable debug") +set (WITH_DEBUG_CAPABILITIES OFF CACHE BOOL "enable debug") +set (WITH_DEBUG_CERTIFICATE OFF CACHE BOOL "enable debug") +set (WITH_DEBUG_CHANNELS OFF CACHE BOOL "enable debug") +set (WITH_DEBUG_CLIPRDR OFF CACHE BOOL "enable debug") +set (WITH_DEBUG_RDPGFX OFF CACHE BOOL "enable debug") +set (WITH_DEBUG_DVC OFF CACHE BOOL "enable debug") +set (WITH_DEBUG_KBD OFF CACHE BOOL "enable debug") +set (WITH_DEBUG_LICENSE OFF CACHE BOOL "enable debug") +set (WITH_DEBUG_NEGO OFF CACHE BOOL "enable debug") +set (WITH_DEBUG_NLA OFF CACHE BOOL "enable debug") +set (WITH_DEBUG_NTLM OFF CACHE BOOL "enable debug") +set (WITH_DEBUG_RAIL OFF CACHE BOOL "enable debug") +set (WITH_DEBUG_RDP OFF CACHE BOOL "enable debug") +set (WITH_DEBUG_RDPEI OFF CACHE BOOL "enable debug") +set (WITH_DEBUG_REDIR OFF CACHE BOOL "enable debug") +set (WITH_DEBUG_RDPDR OFF CACHE BOOL "enable debug") +set (WITH_DEBUG_RFX OFF CACHE BOOL "enable debug") +set (WITH_DEBUG_SCARD OFF CACHE BOOL "enable debug") +set (WITH_DEBUG_SND OFF CACHE BOOL "enable debug") +set (WITH_DEBUG_SVC OFF CACHE BOOL "enable debug") +set (WITH_DEBUG_THREADS OFF CACHE BOOL "enable debug") +set (WITH_DEBUG_TIMEZONE OFF CACHE BOOL "enable debug") +set (WITH_DEBUG_TRANSPORT OFF CACHE BOOL "enable debug") +set (WITH_DEBUG_TSG OFF CACHE BOOL "enable debug") +set (WITH_DEBUG_TSMF OFF CACHE BOOL "enable debug") +set (WITH_DEBUG_WND OFF CACHE BOOL "enable debug") +set (WITH_DEBUG_X11 OFF CACHE BOOL "enable debug") +set (WITH_DEBUG_X11_CLIPRDR OFF CACHE BOOL "enable debug") +set (WITH_DEBUG_X11_LOCAL_MOVESIZE OFF CACHE BOOL "enable debug") +set (WITH_DEBUG_XV OFF CACHE BOOL "enable debug") +set (WITH_SAMPLE ON CACHE BOOL "samples") +set (WITH_NO_UNDEFINED ON CACHE BOOL "don't link with undefined symbols") +set (WITH_SANITIZE_ADDRESS ON) +set (WITH_PROXY_MODULES OFF CACHE BOOL "compile proxy modules") diff --git a/ci/cmake-preloads/config-macosx.txt b/ci/cmake-preloads/config-macosx.txt new file mode 100644 index 0000000..a004dd9 --- /dev/null +++ b/ci/cmake-preloads/config-macosx.txt @@ -0,0 +1,8 @@ +message("PRELOADING mac cache") +set (WITH_MANPAGES OFF CACHE BOOL "man pages") +set (CMAKE_BUILD_TYPE "Debug" CACHE STRING "build type") +set (WITH_CUPS ON CACHE BOOL "CUPS printing") +set (CHANNEL_URBDRC OFF CACHE BOOL "USB redirection") +set (WITH_X11 ON CACHE BOOL "Enable X11") +set (BUILD_TESTING ON CACHE BOOL "build testing") +set (WITH_SANITIZE_ADDRESS ON) diff --git a/ci/cmake-preloads/config-ubuntu-1204.txt b/ci/cmake-preloads/config-ubuntu-1204.txt new file mode 100644 index 0000000..c7319cf --- /dev/null +++ b/ci/cmake-preloads/config-ubuntu-1204.txt @@ -0,0 +1,11 @@ +message("PRELOADING cache") +set (WITH_MANPAGES OFF CACHE BOOL "man pages") +set (CMAKE_BUILD_TYPE "Debug" CACHE STRING "build type") +set (WITH_CUPS OFF CACHE BOOL "CUPS printing") +set (WITH_GSSAPI ON CACHE BOOL "Kerberos support") +set (WITH_ALSA OFF CACHE BOOL "alsa audio") +set (WITH_FFMPEG OFF CACHE BOOL "ffmepg support") +set (WITH_XV OFF CACHE BOOL "xvideo support") +set (BUILD_TESTING ON CACHE BOOL "build testing") +set (WITH_XSHM OFF CACHE BOOL "build with xshm support") +set (WITH_SANITIZE_ADDRESS ON) diff --git a/ci/cmake-preloads/config-windows.txt b/ci/cmake-preloads/config-windows.txt new file mode 100644 index 0000000..fcc78ae --- /dev/null +++ b/ci/cmake-preloads/config-windows.txt @@ -0,0 +1,6 @@ +message("PRELOADING windows cache") +set (CMAKE_BUILD_TYPE "Debug" CACHE STRING "build type") +set (WITH_SERVER "ON" CACHE BOOL "Build server binaries") +set (CHANNEL_URBDRC OFF CACHE BOOL "USB redirection") +set (BUILD_TESTING ON CACHE BOOL "build testing") +set (WITH_SANITIZE_ADDRESS ON) diff --git a/client/.gitignore b/client/.gitignore new file mode 100644 index 0000000..7c1ea95 --- /dev/null +++ b/client/.gitignore @@ -0,0 +1,12 @@ +/* +!/Android +!/common +!/iOS +!/Mac +!/Sample +!/Windows +!/X11 +!/Wayland +!/CMakeLists.txt +!*.in +Wayland/wlfreerdp.1 diff --git a/client/Android/BuildFlags.java.in b/client/Android/BuildFlags.java.in new file mode 100644 index 0000000..9b15e47 --- /dev/null +++ b/client/Android/BuildFlags.java.in @@ -0,0 +1,6 @@ +package com.freerdp.freerdpcore.utils; + +public class BuildFlags +{ + private final static boolean USE_OPENSSL_DEFAULT_NAMES = @USE_OPENSSL_DEFAULT_NAMES@; +} diff --git a/client/Android/CMakeLists.txt b/client/Android/CMakeLists.txt new file mode 100644 index 0000000..b3473a9 --- /dev/null +++ b/client/Android/CMakeLists.txt @@ -0,0 +1,54 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# Android Client +# +# Copyright 2012 Marc-Andre Moreau +# Copyright 2013 Bernhard Miklautz +# +# Licensed 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. + +set(MODULE_NAME "freerdp-android") +set(MODULE_PREFIX "FREERDP_CLIENT_ANDROID") + +include_directories(.) + +if(CMAKE_COMPILER_IS_GNUCC) + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wno-pointer-sign") +endif() + +set(${MODULE_PREFIX}_SRCS + android_event.c + android_event.h + android_freerdp.c + android_freerdp.h + android_jni_utils.c + android_jni_utils.h + android_jni_callback.c + android_jni_callback.h) + +if(WITH_CLIENT_CHANNELS) + set(${MODULE_PREFIX}_SRCS ${${MODULE_PREFIX}_SRCS} + android_cliprdr.c + android_cliprdr.h) +endif() + +add_library(${MODULE_NAME} SHARED ${${MODULE_PREFIX}_SRCS}) + +set(${MODULE_PREFIX}_LIBS ${${MODULE_PREFIX}_LIBS} winpr freerdp freerdp-client) + +set(${MODULE_PREFIX}_LIBS ${${MODULE_PREFIX}_LIBS} dl) +set(${MODULE_PREFIX}_LIBS ${${MODULE_PREFIX}_LIBS} jnigraphics) + +#set_target_properties(${MODULE_NAME} PROPERTIES OUTPUT_NAME ${MODULE_NAME}${FREERDP_API_VERSION}) + +target_link_libraries(${MODULE_NAME} ${${MODULE_PREFIX}_LIBS}) +install(TARGETS ${MODULE_NAME} DESTINATION ${CMAKE_INSTALL_LIBDIR} COMPONENT libraries EXPORT AndroidTargets) diff --git a/client/Android/Studio/.gitignore b/client/Android/Studio/.gitignore new file mode 100644 index 0000000..a2dc231 --- /dev/null +++ b/client/Android/Studio/.gitignore @@ -0,0 +1,37 @@ +#built application files +*.apk +*.ap_ + +# files for the dex VM +*.dex + +# Java class files +*.class + +# generated files +bin/ +gen/ + +# Local configuration file (sdk path, etc) +local.properties + +# Windows thumbnail db +Thumbs.db + +# OSX files +.DS_Store + +# Eclipse project files +.classpath +.project + +# Android Studio +*.iml +.idea +#.idea/workspace.xml - remove # and delete .idea if it better suit your needs. +.gradle +build/ + +#NDK +obj/ +jniLibs/ diff --git a/client/Android/Studio/aFreeRDP/build.gradle b/client/Android/Studio/aFreeRDP/build.gradle new file mode 100644 index 0000000..96f7184 --- /dev/null +++ b/client/Android/Studio/aFreeRDP/build.gradle @@ -0,0 +1,52 @@ +plugins { + id 'com.gladed.androidgitversion' version '0.4.4' +} + +androidGitVersion { + abis = ['armeabi':1, 'armeabi-v7a':2, 'arm64-v8a':3, 'mips':5, 'mips64':6, 'x86':8, 'x86_64':9 ] + prefix '' +} + +println 'Version Name: ' + androidGitVersion.name() +println 'Version Code: ' + androidGitVersion.code() + +apply plugin: 'com.android.application' +android { + compileSdkVersion = rootProject.ext.compileApi + buildToolsVersion = rootProject.ext.toolsVersion + + defaultConfig { + applicationId "com.freerdp.afreerdp" + minSdkVersion rootProject.ext.minApi + targetSdkVersion rootProject.ext.targetApi + vectorDrawables.useSupportLibrary = true + versionName = androidGitVersion.name() + versionCode = androidGitVersion.code() + } + + signingConfigs { + release { + storeFile file(RELEASE_STORE_FILE) + storePassword RELEASE_STORE_PASSWORD + keyAlias RELEASE_KEY_ALIAS + keyPassword RELEASE_KEY_PASSWORD + storeType "jks" + } + } + + buildTypes { + release { + minifyEnabled false + signingConfig signingConfigs.release + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' + } + debug { + jniDebuggable true + renderscriptDebuggable true + } + } +} + +dependencies { + implementation project(':freeRDPCore') +} diff --git a/client/Android/Studio/aFreeRDP/lint.xml b/client/Android/Studio/aFreeRDP/lint.xml new file mode 100644 index 0000000..c70207f --- /dev/null +++ b/client/Android/Studio/aFreeRDP/lint.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/client/Android/Studio/aFreeRDP/src/main/AndroidManifest.xml b/client/Android/Studio/aFreeRDP/src/main/AndroidManifest.xml new file mode 100644 index 0000000..8c46bc9 --- /dev/null +++ b/client/Android/Studio/aFreeRDP/src/main/AndroidManifest.xml @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/client/Android/Studio/aFreeRDP/src/main/assets/FreeRDP_Logo.png b/client/Android/Studio/aFreeRDP/src/main/assets/FreeRDP_Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..1e272627b473c8527f109af88cdcc982b6082a0f GIT binary patch literal 30329 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 literal 0 HcmV?d00001 diff --git a/client/Android/Studio/aFreeRDP/src/main/assets/about.css b/client/Android/Studio/aFreeRDP/src/main/assets/about.css new file mode 100644 index 0000000..604e505 --- /dev/null +++ b/client/Android/Studio/aFreeRDP/src/main/assets/about.css @@ -0,0 +1,147 @@ +p { + border: none; + padding: 0in; + font-variant: normal; + font-family: "Helvetica"; + font-style: normal; + font-weight: normal; + line-height: 100%; + text-align: center; +} + +td p { + border: none; + padding: 0in; + font-variant: normal; + font-family: "Helvetica"; + font-style: normal; + font-weight: normal; + line-height: 100%; + text-align: center; +} + +h2 { + border: none; + padding: 0in; + direction: inherit; + font-variant: normal; + color: #ffffff; + line-height: 100%; + text-align: center; +} + +h2.western { + font-style: normal; + } + +h2.cjk { + font-family: "AR PL SungtiL GB"; + font-style: normal; +} + +h2.ctl { + font-family: "Lohit Devanagari"; + font-style: normal; +} + +h3 { + border: none; + padding: 0in; + direction: inherit; + font-variant: normal; + color: #ffffff; + line-height: 100%; + text-align: center; + page-break-before: auto; + page-break-after: auto; +} + +h3.western { + font-style: normal; +} + +h3.cjk { + font-family: "AR PL SungtiL GB"; + font-style: normal; +} + +h3.ctl { + font-family: "Lohit Devanagari"; + font-style: normal; +} + +h4 { + border: none; + padding: 0in; + direction: inherit; + font-variant: normal; + color: #ffffff; + line-height: 100%; + text-align: center; + page-break-before: auto; + page-break-after: auto; +} + +h4.western { + font-style: normal; +} + +h4.cjk { + font-family: "AR PL SungtiL GB"; + font-style: normal; +} + +h4.ctl { + font-family: "Lohit Devanagari"; + font-style: normal; +} + +pre { + direction: inherit; + font-variant: normal; + line-height: 100%; + text-align: center; + page-break-before: auto; + white-space: pre-wrap; /* css-3 */ + white-space: -moz-pre-wrap; /* Mozilla, since 1999 */ + white-space: -pre-wrap; /* Opera 4-6 */ + white-space: -o-pre-wrap; /* Opera 7 */ + word-wrap: break-word; /* Internet Explorer 5.5+ */ +} + +pre.western { + font-size: 8pt; + font-style: normal; + font-weight: normal; + white-space: pre-wrap; /* css-3 */ + white-space: -moz-pre-wrap; /* Mozilla, since 1999 */ + white-space: -pre-wrap; /* Opera 4-6 */ + white-space: -o-pre-wrap; /* Opera 7 */ + word-wrap: break-word; /* Internet Explorer 5.5+ */ +} + +pre.cjk { + font-family: "AR PL SungtiL GB", monospace; + font-size: 8pt; + font-style: normal; + font-weight: normal; + white-space: pre-wrap; /* css-3 */ + white-space: -moz-pre-wrap; /* Mozilla, since 1999 */ + white-space: -pre-wrap; /* Opera 4-6 */ + white-space: -o-pre-wrap; /* Opera 7 */ + word-wrap: break-word; /* Internet Explorer 5.5+ */ +} + +pre.ctl { + font-style: normal; + font-weight: normal; + white-space: pre-wrap; /* css-3 */ + white-space: -moz-pre-wrap; /* Mozilla, since 1999 */ + white-space: -pre-wrap; /* Opera 4-6 */ + white-space: -o-pre-wrap; /* Opera 7 */ + word-wrap: break-word; /* Internet Explorer 5.5+ */ +} + +a:link { + color: #0000ff +} \ No newline at end of file diff --git a/client/Android/Studio/aFreeRDP/src/main/assets/about_page/about.html b/client/Android/Studio/aFreeRDP/src/main/assets/about_page/about.html new file mode 100644 index 0000000..fa73e42 --- /dev/null +++ b/client/Android/Studio/aFreeRDP/src/main/assets/about_page/about.html @@ -0,0 +1,397 @@ + + + + + + + + + + + +
+


+
+ +

+
+

aFreeRDP
+ Remote + Desktop Client

+
+

+ +

+
+

aFreeRDP is an open source client + capable of natively using
+ Remote Desktop Protocol (RDP) in + order to remotely access your Windows desktop.
+

+
+
+

+
+
+ +

+
+

+ Version Information

+
+ + + + + + + + + + + + + +
+

aFreeRDP Version

+
+

%AFREERDP_VERSION%

+
+

System Version

+
+

%SYSTEM_VERSION%

+
+

Model

+
+

%DEVICE_MODEL%

+
+
+

+ Credits

+
+

aFreeRDP + is a part of FreeRDP +

+
+

+ + Data protection

+
+

Details + about data collection and usage by aFreeRDP are available at

+

http://www.freerdp.com/privacy +

+
+

+ + Licenses

+
+
+

+ + aFreeRDP

+
+
This program is free software;
+
+you can redistribute it and/or modify it under the terms
+
+of the Mozilla Public License, v. 2.0.
+
+You can obtain an online version of the License from
+
+http://mozilla.org/MPL/2.0/. 
+This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+
+without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 
+A copy of the product's source code can be obtained from the FreeRDP GitHub repository at
+
+https://github.com/FreeRDP/FreeRDP.
+

+
+
+ +

+
+

+ + FreeRDP

+
+
Licensed 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. 
+A copy of the product's source code can be obtained from the FreeRDP GitHub repository at
+
+https://github.com/FreeRDP/FreeRDP.
+
+

+ + OpenSSL

+
+
LICENSE ISSUES
+
+==============
+
+
+The OpenSSL toolkit stays under a dual license, i.e. both the conditions of
+
+the OpenSSL License and the original SSLeay license apply to the toolkit.
+
+See below for the actual license texts.
+
+
+OpenSSL License
+
+---------------
+
+
+/* ====================================================================
+
+* Copyright (c) 1998-2016 The OpenSSL Project. All rights reserved.
+
+*
+
+* Redistribution and use in source and binary forms, with or without
+
+* modification, are permitted provided that the following conditions
+
+* are met:
+
+*
+
+* 1. Redistributions of source code must retain the above copyright
+
+* notice, this list of conditions and the following disclaimer.
+
+*
+
+* 2. Redistributions in binary form must reproduce the above copyright
+
+* notice, this list of conditions and the following disclaimer in
+
+* the documentation and/or other materials provided with the
+
+* distribution.
+
+*
+
+* 3. All advertising materials mentioning features or use of this
+
+* software must display the following acknowledgment:
+
+* "This product includes software developed by the OpenSSL Project
+
+* for use in the OpenSSL Toolkit. (http://www.openssl.org/)"
+
+*
+
+* 4. The names "OpenSSL Toolkit" and "OpenSSL Project" must not be used to
+
+* endorse or promote products derived from this software without
+
+* prior written permission. For written permission, please contact
+
+* openssl-core@openssl.org.
+
+*
+
+* 5. Products derived from this software may not be called "OpenSSL"
+
+* nor may "OpenSSL" appear in their names without prior written
+
+* permission of the OpenSSL Project.
+
+*
+
+* 6. Redistributions of any form whatsoever must retain the following
+
+* acknowledgment:
+
+* "This product includes software developed by the OpenSSL Project
+
+* for use in the OpenSSL Toolkit (http://www.openssl.org/)"
+
+*
+
+* THIS SOFTWARE IS PROVIDED BY THE OpenSSL PROJECT ``AS IS'' AND ANY
+
+* EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+
+* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+
+* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE OpenSSL PROJECT OR
+
+* ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+
+* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+
+* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+
+* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+
+* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+
+* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+
+* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+
+* OF THE POSSIBILITY OF SUCH DAMAGE.
+
+* ====================================================================
+
+*
+
+* This product includes cryptographic software written by Eric Young
+
+* (eay@cryptsoft.com). This product includes software written by Tim
+
+* Hudson (tjh@cryptsoft.com).
+
+*
+
+*/
+
+
+Original SSLeay License
+
+-----------------------
+
+
+/* Copyright (C) 1995-1998 Eric Young (eay@cryptsoft.com)
+
+* All rights reserved.
+
+*
+
+* This package is an SSL implementation written
+
+* by Eric Young (eay@cryptsoft.com).
+
+* The implementation was written so as to conform with Netscapes SSL.
+
+*
+
+* This library is free for commercial and non-commercial use as long as
+
+* the following conditions are aheared to. The following conditions
+
+* apply to all code found in this distribution, be it the RC4, RSA,
+
+* lhash, DES, etc., code; not just the SSL code. The SSL documentation
+
+* included with this distribution is covered by the same copyright terms
+
+* except that the holder is Tim Hudson (tjh@cryptsoft.com).
+
+*
+
+* Copyright remains Eric Young's, and as such any Copyright notices in
+
+* the code are not to be removed.
+
+* If this package is used in a product, Eric Young should be given attribution
+
+* as the author of the parts of the library used.
+
+* This can be in the form of a textual message at program startup or
+
+* in documentation (online or textual) provided with the package.
+
+*
+
+* Redistribution and use in source and binary forms, with or without
+
+* modification, are permitted provided that the following conditions
+
+* are met:
+
+* 1. Redistributions of source code must retain the copyright
+
+* notice, this list of conditions and the following disclaimer.
+
+* 2. Redistributions in binary form must reproduce the above copyright
+
+* notice, this list of conditions and the following disclaimer in the
+
+* documentation and/or other materials provided with the distribution.
+
+* 3. All advertising materials mentioning features or use of this software
+
+* must display the following acknowledgement:
+
+* "This product includes cryptographic software written by
+
+* Eric Young (eay@cryptsoft.com)"
+
+* The word 'cryptographic' can be left out if the rouines from the library
+
+* being used are not cryptographic related :-).
+
+* 4. If you include any Windows specific code (or a derivative thereof) from
+
+* the apps directory (application code) you must include an acknowledgement:
+
+* "This product includes software written by Tim Hudson (tjh@cryptsoft.com)"
+
+*
+
+* THIS SOFTWARE IS PROVIDED BY ERIC YOUNG ``AS IS'' AND
+
+* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+
+* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+
+* ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+
+* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+
+* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+
+* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+
+* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+
+* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+
+* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+
+* SUCH DAMAGE.
+
+*
+
+* The licence and distribution terms for any publically available version or
+
+* derivative of this code cannot be changed. i.e. this code cannot simply be
+
+* copied and put under another distribution licence
+
+* [including the GNU Public Licence.]
+
+*/
+A copy of the product's source code can be obtained from the project page at
+
+https://www.openssl.org/.
+
+
+ + diff --git a/client/Android/Studio/aFreeRDP/src/main/assets/about_page/about_phone.html b/client/Android/Studio/aFreeRDP/src/main/assets/about_page/about_phone.html new file mode 100644 index 0000000..fa73e42 --- /dev/null +++ b/client/Android/Studio/aFreeRDP/src/main/assets/about_page/about_phone.html @@ -0,0 +1,397 @@ + + + + + + + + + + + +
+


+
+ +

+
+

aFreeRDP
+ Remote + Desktop Client

+
+

+ +

+
+

aFreeRDP is an open source client + capable of natively using
+ Remote Desktop Protocol (RDP) in + order to remotely access your Windows desktop.
+

+
+
+

+
+
+ +

+
+

+ Version Information

+
+ + + + + + + + + + + + + +
+

aFreeRDP Version

+
+

%AFREERDP_VERSION%

+
+

System Version

+
+

%SYSTEM_VERSION%

+
+

Model

+
+

%DEVICE_MODEL%

+
+
+

+ Credits

+
+

aFreeRDP + is a part of FreeRDP +

+
+

+ + Data protection

+
+

Details + about data collection and usage by aFreeRDP are available at

+

http://www.freerdp.com/privacy +

+
+

+ + Licenses

+
+
+

+ + aFreeRDP

+
+
This program is free software;
+
+you can redistribute it and/or modify it under the terms
+
+of the Mozilla Public License, v. 2.0.
+
+You can obtain an online version of the License from
+
+http://mozilla.org/MPL/2.0/. 
+This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+
+without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 
+A copy of the product's source code can be obtained from the FreeRDP GitHub repository at
+
+https://github.com/FreeRDP/FreeRDP.
+

+
+
+ +

+
+

+ + FreeRDP

+
+
Licensed 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. 
+A copy of the product's source code can be obtained from the FreeRDP GitHub repository at
+
+https://github.com/FreeRDP/FreeRDP.
+
+

+ + OpenSSL

+
+
LICENSE ISSUES
+
+==============
+
+
+The OpenSSL toolkit stays under a dual license, i.e. both the conditions of
+
+the OpenSSL License and the original SSLeay license apply to the toolkit.
+
+See below for the actual license texts.
+
+
+OpenSSL License
+
+---------------
+
+
+/* ====================================================================
+
+* Copyright (c) 1998-2016 The OpenSSL Project. All rights reserved.
+
+*
+
+* Redistribution and use in source and binary forms, with or without
+
+* modification, are permitted provided that the following conditions
+
+* are met:
+
+*
+
+* 1. Redistributions of source code must retain the above copyright
+
+* notice, this list of conditions and the following disclaimer.
+
+*
+
+* 2. Redistributions in binary form must reproduce the above copyright
+
+* notice, this list of conditions and the following disclaimer in
+
+* the documentation and/or other materials provided with the
+
+* distribution.
+
+*
+
+* 3. All advertising materials mentioning features or use of this
+
+* software must display the following acknowledgment:
+
+* "This product includes software developed by the OpenSSL Project
+
+* for use in the OpenSSL Toolkit. (http://www.openssl.org/)"
+
+*
+
+* 4. The names "OpenSSL Toolkit" and "OpenSSL Project" must not be used to
+
+* endorse or promote products derived from this software without
+
+* prior written permission. For written permission, please contact
+
+* openssl-core@openssl.org.
+
+*
+
+* 5. Products derived from this software may not be called "OpenSSL"
+
+* nor may "OpenSSL" appear in their names without prior written
+
+* permission of the OpenSSL Project.
+
+*
+
+* 6. Redistributions of any form whatsoever must retain the following
+
+* acknowledgment:
+
+* "This product includes software developed by the OpenSSL Project
+
+* for use in the OpenSSL Toolkit (http://www.openssl.org/)"
+
+*
+
+* THIS SOFTWARE IS PROVIDED BY THE OpenSSL PROJECT ``AS IS'' AND ANY
+
+* EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+
+* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+
+* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE OpenSSL PROJECT OR
+
+* ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+
+* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+
+* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+
+* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+
+* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+
+* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+
+* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+
+* OF THE POSSIBILITY OF SUCH DAMAGE.
+
+* ====================================================================
+
+*
+
+* This product includes cryptographic software written by Eric Young
+
+* (eay@cryptsoft.com). This product includes software written by Tim
+
+* Hudson (tjh@cryptsoft.com).
+
+*
+
+*/
+
+
+Original SSLeay License
+
+-----------------------
+
+
+/* Copyright (C) 1995-1998 Eric Young (eay@cryptsoft.com)
+
+* All rights reserved.
+
+*
+
+* This package is an SSL implementation written
+
+* by Eric Young (eay@cryptsoft.com).
+
+* The implementation was written so as to conform with Netscapes SSL.
+
+*
+
+* This library is free for commercial and non-commercial use as long as
+
+* the following conditions are aheared to. The following conditions
+
+* apply to all code found in this distribution, be it the RC4, RSA,
+
+* lhash, DES, etc., code; not just the SSL code. The SSL documentation
+
+* included with this distribution is covered by the same copyright terms
+
+* except that the holder is Tim Hudson (tjh@cryptsoft.com).
+
+*
+
+* Copyright remains Eric Young's, and as such any Copyright notices in
+
+* the code are not to be removed.
+
+* If this package is used in a product, Eric Young should be given attribution
+
+* as the author of the parts of the library used.
+
+* This can be in the form of a textual message at program startup or
+
+* in documentation (online or textual) provided with the package.
+
+*
+
+* Redistribution and use in source and binary forms, with or without
+
+* modification, are permitted provided that the following conditions
+
+* are met:
+
+* 1. Redistributions of source code must retain the copyright
+
+* notice, this list of conditions and the following disclaimer.
+
+* 2. Redistributions in binary form must reproduce the above copyright
+
+* notice, this list of conditions and the following disclaimer in the
+
+* documentation and/or other materials provided with the distribution.
+
+* 3. All advertising materials mentioning features or use of this software
+
+* must display the following acknowledgement:
+
+* "This product includes cryptographic software written by
+
+* Eric Young (eay@cryptsoft.com)"
+
+* The word 'cryptographic' can be left out if the rouines from the library
+
+* being used are not cryptographic related :-).
+
+* 4. If you include any Windows specific code (or a derivative thereof) from
+
+* the apps directory (application code) you must include an acknowledgement:
+
+* "This product includes software written by Tim Hudson (tjh@cryptsoft.com)"
+
+*
+
+* THIS SOFTWARE IS PROVIDED BY ERIC YOUNG ``AS IS'' AND
+
+* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+
+* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+
+* ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+
+* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+
+* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+
+* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+
+* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+
+* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+
+* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+
+* SUCH DAMAGE.
+
+*
+
+* The licence and distribution terms for any publically available version or
+
+* derivative of this code cannot be changed. i.e. this code cannot simply be
+
+* copied and put under another distribution licence
+
+* [including the GNU Public Licence.]
+
+*/
+A copy of the product's source code can be obtained from the project page at
+
+https://www.openssl.org/.
+
+
+ + diff --git a/client/Android/Studio/aFreeRDP/src/main/assets/background.jpg b/client/Android/Studio/aFreeRDP/src/main/assets/background.jpg new file mode 100644 index 0000000000000000000000000000000000000000..fd4e1d364121e24b94a027bb5d71b0eb4025bf69 GIT binary patch literal 118109 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 OF8}ivPfY#APk#ZXhjdT? literal 0 HcmV?d00001 diff --git a/client/Android/Studio/aFreeRDP/src/main/assets/de_about_page/about.html b/client/Android/Studio/aFreeRDP/src/main/assets/de_about_page/about.html new file mode 100644 index 0000000..90760c1 --- /dev/null +++ b/client/Android/Studio/aFreeRDP/src/main/assets/de_about_page/about.html @@ -0,0 +1,410 @@ + + + + + + + + + + + +

+


+
+ +

+
+

aFreeRDP
+ Remote + Desktop Client
+

+
+
+

+ + +

+
+
+

+ aFreeRDP ist ein Open Source Programm + mit nativer Unterstützung des Remote Desktop Protocol (RDP)
+ um + einen entfernten Zugriff auf Windows Desktops zu ermöglichen.
+

+
+

+ Versions Information

+
+ + + + + + + + + + + + + +
+

aFreeRDP Version

+
+

%AFREERDP_VERSION%

+
+

System Version

+
+

%SYSTEM_VERSION%

+
+

Model

+
+

%DEVICE_MODEL%

+
+
+
+

+ +
+
+ +

+
+

+ + Credits

+
+

aFreeRDP + ist ein Teil von FreeRDP +

+
+
+

+ +
+
+ +

+
+

+ + Datenschutz

+
+

Details + zu den Daten die aFreeRDP sammelt und verarbeitet sind unter

+

http://www.freerdp.com/privacy + zu finden.

+
+

+ + Lizenzen

+
+
+

+ + aFreeRDP

+
+
This program is free software;
+
+you can redistribute it and/or modify it under the terms
+
+of the Mozilla Public License, v. 2.0.
+
+You can obtain an online version of the License from
+
+http://mozilla.org/MPL/2.0/. 
+This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+
+without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 
+A copy of the product's source code can be obtained from the FreeRDP GitHub repository at
+
+https://github.com/FreeRDP/FreeRDP.
+
+

+ + FreeRDP

+
+
Licensed 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. 
+A copy of the product's source code can be obtained from the FreeRDP GitHub repository at
+
+https://github.com/FreeRDP/FreeRDP.
+
+

+ + OpenSSL

+
+
LICENSE ISSUES
+
+==============
+
+
+The OpenSSL toolkit stays under a dual license, i.e. both the conditions of
+
+the OpenSSL License and the original SSLeay license apply to the toolkit.
+
+See below for the actual license texts.
+
+
+OpenSSL License
+
+---------------
+
+
+/* ====================================================================
+
+* Copyright (c) 1998-2016 The OpenSSL Project. All rights reserved.
+
+*
+
+* Redistribution and use in source and binary forms, with or without
+
+* modification, are permitted provided that the following conditions
+
+* are met:
+
+*
+
+* 1. Redistributions of source code must retain the above copyright
+
+* notice, this list of conditions and the following disclaimer.
+
+*
+
+* 2. Redistributions in binary form must reproduce the above copyright
+
+* notice, this list of conditions and the following disclaimer in
+
+* the documentation and/or other materials provided with the
+
+* distribution.
+
+*
+
+* 3. All advertising materials mentioning features or use of this
+
+* software must display the following acknowledgment:
+
+* "This product includes software developed by the OpenSSL Project
+
+* for use in the OpenSSL Toolkit. (http://www.openssl.org/)"
+
+*
+
+* 4. The names "OpenSSL Toolkit" and "OpenSSL Project" must not be used to
+
+* endorse or promote products derived from this software without
+
+* prior written permission. For written permission, please contact
+
+* openssl-core@openssl.org.
+
+*
+
+* 5. Products derived from this software may not be called "OpenSSL"
+
+* nor may "OpenSSL" appear in their names without prior written
+
+* permission of the OpenSSL Project.
+
+*
+
+* 6. Redistributions of any form whatsoever must retain the following
+
+* acknowledgment:
+
+* "This product includes software developed by the OpenSSL Project
+
+* for use in the OpenSSL Toolkit (http://www.openssl.org/)"
+
+*
+
+* THIS SOFTWARE IS PROVIDED BY THE OpenSSL PROJECT ``AS IS'' AND ANY
+
+* EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+
+* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+
+* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE OpenSSL PROJECT OR
+
+* ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+
+* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+
+* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+
+* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+
+* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+
+* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+
+* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+
+* OF THE POSSIBILITY OF SUCH DAMAGE.
+
+* ====================================================================
+
+*
+
+* This product includes cryptographic software written by Eric Young
+
+* (eay@cryptsoft.com). This product includes software written by Tim
+
+* Hudson (tjh@cryptsoft.com).
+
+*
+
+*/
+
+
+Original SSLeay License
+
+-----------------------
+
+
+/* Copyright (C) 1995-1998 Eric Young (eay@cryptsoft.com)
+
+* All rights reserved.
+
+*
+
+* This package is an SSL implementation written
+
+* by Eric Young (eay@cryptsoft.com).
+
+* The implementation was written so as to conform with Netscapes SSL.
+
+*
+
+* This library is free for commercial and non-commercial use as long as
+
+* the following conditions are aheared to. The following conditions
+
+* apply to all code found in this distribution, be it the RC4, RSA,
+
+* lhash, DES, etc., code; not just the SSL code. The SSL documentation
+
+* included with this distribution is covered by the same copyright terms
+
+* except that the holder is Tim Hudson (tjh@cryptsoft.com).
+
+*
+
+* Copyright remains Eric Young's, and as such any Copyright notices in
+
+* the code are not to be removed.
+
+* If this package is used in a product, Eric Young should be given attribution
+
+* as the author of the parts of the library used.
+
+* This can be in the form of a textual message at program startup or
+
+* in documentation (online or textual) provided with the package.
+
+*
+
+* Redistribution and use in source and binary forms, with or without
+
+* modification, are permitted provided that the following conditions
+
+* are met:
+
+* 1. Redistributions of source code must retain the copyright
+
+* notice, this list of conditions and the following disclaimer.
+
+* 2. Redistributions in binary form must reproduce the above copyright
+
+* notice, this list of conditions and the following disclaimer in the
+
+* documentation and/or other materials provided with the distribution.
+
+* 3. All advertising materials mentioning features or use of this software
+
+* must display the following acknowledgement:
+
+* "This product includes cryptographic software written by
+
+* Eric Young (eay@cryptsoft.com)"
+
+* The word 'cryptographic' can be left out if the rouines from the library
+
+* being used are not cryptographic related :-).
+
+* 4. If you include any Windows specific code (or a derivative thereof) from
+
+* the apps directory (application code) you must include an acknowledgement:
+
+* "This product includes software written by Tim Hudson (tjh@cryptsoft.com)"
+
+*
+
+* THIS SOFTWARE IS PROVIDED BY ERIC YOUNG ``AS IS'' AND
+
+* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+
+* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+
+* ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+
+* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+
+* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+
+* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+
+* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+
+* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+
+* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+
+* SUCH DAMAGE.
+
+*
+
+* The licence and distribution terms for any publically available version or
+
+* derivative of this code cannot be changed. i.e. this code cannot simply be
+
+* copied and put under another distribution licence
+
+* [including the GNU Public Licence.]
+
+*/
+A copy of the product's source code can be obtained from the project page at
+
+https://www.openssl.org/.
+
+
+ + diff --git a/client/Android/Studio/aFreeRDP/src/main/assets/de_about_page/about_phone.html b/client/Android/Studio/aFreeRDP/src/main/assets/de_about_page/about_phone.html new file mode 100644 index 0000000..a5d9664 --- /dev/null +++ b/client/Android/Studio/aFreeRDP/src/main/assets/de_about_page/about_phone.html @@ -0,0 +1,412 @@ + + + + + + + + + + + + +
+


+
+ +

+
+

aFreeRDP
+ Remote + Desktop Client
+

+
+
+

+ + +

+
+
+

+ aFreeRDP ist ein Open Source Programm + mit nativer Unterstützung des Remote Desktop Protocol (RDP)
+ um + einen entfernten Zugriff auf Windows Desktops zu ermöglichen.
+

+
+

+ Versions Information

+
+ + + + + + + + + + + + + +
+

aFreeRDP Version

+
+

%AFREERDP_VERSION%

+
+

System Version

+
+

%SYSTEM_VERSION%

+
+

Model

+
+

%DEVICE_MODEL%

+
+
+
+

+ +
+
+ +

+
+

+ + Credits

+
+

aFreeRDP + ist ein Teil von FreeRDP +

+
+
+

+ +
+
+ +

+
+

+ + Datenschutz

+
+

Details + zu den Daten die aFreeRDP sammelt und verarbeitet sind unter

+

http://www.freerdp.com/privacy + zu finden.

+
+

+ + Lizenzen

+
+
+

aFreeRDP

+
+
This program is free software;
+
+you can redistribute it and/or modify it under the terms
+
+of the Mozilla Public License, v. 2.0.
+
+You can obtain an online version of the License from
+
+http://mozilla.org/MPL/2.0/. 
+This program is distributed in the hope
+that it will be useful, but WITHOUT ANY WARRANTY;
+
+without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 
+A copy of the product's source code can be
+obtained from the FreeRDP GitHub repository at
+
+https://github.com/FreeRDP/FreeRDP.
+
+

+ + FreeRDP

+
+
Licensed 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. 
+A copy of the product's source code can be obtained
+ from the FreeRDP GitHub repository at
+
+https://github.com/FreeRDP/FreeRDP.
+
+

+ + OpenSSL

+
+
LICENSE ISSUES
+
+==============
+
+
+The OpenSSL toolkit stays under a dual license, i.e. both the conditions of
+
+the OpenSSL License and the original SSLeay license apply to the toolkit.
+
+See below for the actual license texts.
+
+
+OpenSSL License
+
+---------------
+
+
+/* ====================================================================
+
+* Copyright (c) 1998-2016 The OpenSSL Project. All rights reserved.
+
+*
+
+* Redistribution and use in source and binary forms, with or without
+
+* modification, are permitted provided that the following conditions
+
+* are met:
+
+*
+
+* 1. Redistributions of source code must retain the above copyright
+
+* notice, this list of conditions and the following disclaimer.
+
+*
+
+* 2. Redistributions in binary form must reproduce the above copyright
+
+* notice, this list of conditions and the following disclaimer in
+
+* the documentation and/or other materials provided with the
+
+* distribution.
+
+*
+
+* 3. All advertising materials mentioning features or use of this
+
+* software must display the following acknowledgment:
+
+* "This product includes software developed by the OpenSSL Project
+
+* for use in the OpenSSL Toolkit. (http://www.openssl.org/)"
+
+*
+
+* 4. The names "OpenSSL Toolkit" and "OpenSSL Project" must not be used to
+
+* endorse or promote products derived from this software without
+
+* prior written permission. For written permission, please contact
+
+* openssl-core@openssl.org.
+
+*
+
+* 5. Products derived from this software may not be called "OpenSSL"
+
+* nor may "OpenSSL" appear in their names without prior written
+
+* permission of the OpenSSL Project.
+
+*
+
+* 6. Redistributions of any form whatsoever must retain the following
+
+* acknowledgment:
+
+* "This product includes software developed by the OpenSSL Project
+
+* for use in the OpenSSL Toolkit (http://www.openssl.org/)"
+
+*
+
+* THIS SOFTWARE IS PROVIDED BY THE OpenSSL PROJECT ``AS IS'' AND ANY
+
+* EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+
+* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+
+* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE OpenSSL PROJECT OR
+
+* ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+
+* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+
+* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+
+* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+
+* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+
+* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+
+* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+
+* OF THE POSSIBILITY OF SUCH DAMAGE.
+
+* ====================================================================
+
+*
+
+* This product includes cryptographic software written by Eric Young
+
+* (eay@cryptsoft.com). This product includes software written by Tim
+
+* Hudson (tjh@cryptsoft.com).
+
+*
+
+*/
+
+
+Original SSLeay License
+
+-----------------------
+
+
+/* Copyright (C) 1995-1998 Eric Young (eay@cryptsoft.com)
+
+* All rights reserved.
+
+*
+
+* This package is an SSL implementation written
+
+* by Eric Young (eay@cryptsoft.com).
+
+* The implementation was written so as to conform with Netscapes SSL.
+
+*
+
+* This library is free for commercial and non-commercial use as long as
+
+* the following conditions are aheared to. The following conditions
+
+* apply to all code found in this distribution, be it the RC4, RSA,
+
+* lhash, DES, etc., code; not just the SSL code. The SSL documentation
+
+* included with this distribution is covered by the same copyright terms
+
+* except that the holder is Tim Hudson (tjh@cryptsoft.com).
+
+*
+
+* Copyright remains Eric Young's, and as such any Copyright notices in
+
+* the code are not to be removed.
+
+* If this package is used in a product, Eric Young should be given attribution
+
+* as the author of the parts of the library used.
+
+* This can be in the form of a textual message at program startup or
+
+* in documentation (online or textual) provided with the package.
+
+*
+
+* Redistribution and use in source and binary forms, with or without
+
+* modification, are permitted provided that the following conditions
+
+* are met:
+
+* 1. Redistributions of source code must retain the copyright
+
+* notice, this list of conditions and the following disclaimer.
+
+* 2. Redistributions in binary form must reproduce the above copyright
+
+* notice, this list of conditions and the following disclaimer in the
+
+* documentation and/or other materials provided with the distribution.
+
+* 3. All advertising materials mentioning features or use of this software
+
+* must display the following acknowledgement:
+
+* "This product includes cryptographic software written by
+
+* Eric Young (eay@cryptsoft.com)"
+
+* The word 'cryptographic' can be left out if the rouines from the library
+
+* being used are not cryptographic related :-).
+
+* 4. If you include any Windows specific code (or a derivative thereof) from
+
+* the apps directory (application code) you must include an acknowledgement:
+
+* "This product includes software written by Tim Hudson (tjh@cryptsoft.com)"
+
+*
+
+* THIS SOFTWARE IS PROVIDED BY ERIC YOUNG ``AS IS'' AND
+
+* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+
+* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+
+* ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+
+* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+
+* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+
+* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+
+* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+
+* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+
+* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+
+* SUCH DAMAGE.
+
+*
+
+* The licence and distribution terms for any publically available version or
+
+* derivative of this code cannot be changed. i.e. this code cannot simply be
+
+* copied and put under another distribution licence
+
+* [including the GNU Public Licence.]
+
+*/
+A copy of the product's source code can be obtained from the project page at
+
+https://www.openssl.org/.
+
+
+ + diff --git a/client/Android/Studio/aFreeRDP/src/main/assets/de_help_page/gestures.html b/client/Android/Studio/aFreeRDP/src/main/assets/de_help_page/gestures.html new file mode 100644 index 0000000..879f2ee --- /dev/null +++ b/client/Android/Studio/aFreeRDP/src/main/assets/de_help_page/gestures.html @@ -0,0 +1,33 @@ + + + + + + + Help + + + + + +
+
+ + +
+

Gesten

+

+ aFreeRDP ist für Touch Geräte entwickelt worden. + Diese Gesten lassen sie die häufigsten Operationen mit ihren Fingern + durchführen.

+

+
+
+
+ + diff --git a/client/Android/Studio/aFreeRDP/src/main/assets/de_help_page/gestures.png b/client/Android/Studio/aFreeRDP/src/main/assets/de_help_page/gestures.png new file mode 100644 index 0000000000000000000000000000000000000000..78b3e7b341002dec895e8f4ea28a780cc8b47ffa GIT binary patch literal 43781 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 literal 0 HcmV?d00001 diff --git a/client/Android/Studio/aFreeRDP/src/main/assets/de_help_page/gestures_phone.html b/client/Android/Studio/aFreeRDP/src/main/assets/de_help_page/gestures_phone.html new file mode 100644 index 0000000..b72dabf --- /dev/null +++ b/client/Android/Studio/aFreeRDP/src/main/assets/de_help_page/gestures_phone.html @@ -0,0 +1,38 @@ + + + + + + + + Help + + + + + +

+
+ + +
+

Gesten

+

+ aFreeRDP ist für Touch Geräte entwickelt worden. + Diese Gesten lassen sie die häufigsten Operationen mit ihren Fingern + durchführen.

+

+ + +
+
+
+ + + diff --git a/client/Android/Studio/aFreeRDP/src/main/assets/de_help_page/gestures_phone.png b/client/Android/Studio/aFreeRDP/src/main/assets/de_help_page/gestures_phone.png new file mode 100644 index 0000000000000000000000000000000000000000..4eea33ef47968b4b7fed3626229caea2d871db34 GIT binary patch literal 27770 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 M07*qoM6N<$g5l!8i~s-t literal 0 HcmV?d00001 diff --git a/client/Android/Studio/aFreeRDP/src/main/assets/de_help_page/nav_toolbar.png b/client/Android/Studio/aFreeRDP/src/main/assets/de_help_page/nav_toolbar.png new file mode 100644 index 0000000000000000000000000000000000000000..f66b24d215a0736731b836a2948a4e0ccd54da32 GIT binary patch literal 2022 zcmV_Io$3FMIw|mE9 zQW230GlJQ|WHyq-#51u>4D*@F17$t4j?ppUOfaKm1~5L1gEUuZ$X%Go#&C}@skd+6 zKKj@7>jra8?S;xJqp_m0vhqqrWrfOvq@v>T?^mu|yma|2lg!yHAfjPR55|w_#Axmn zasZ=eGMK}E-@27oTYK%AvHB_*jn!0DWu%JAD&hfD9x&yXf2ZFrUZP*lou}fGGn8Lo zpseg1)3K}*?{bv(j3W_wFwWADy_rv#Lrg;5AAeq|=77!S8Zy_^5WkD~q-A6{RZO{g zC+S3PzRCk8hsirxNCk!>OD1VYGU)pQ3AFi(*zY*fo=kTlYR_ndknNet%t0pps>xK& z%QKlwX3M)KJtLF8{`NcC8^2%WAqDvyOiZ$DH*V6?oH=t#Ioe?6B_eWS>=1M}W+$`N zXuO(L_Ui?z;2B(KI7RXMzoi{JcdICXoxAo@{Jw8Uzj+J2HzS+|40x%KgN|UH$E-%s zp-e2_bVU^!if)}hUq-y8opF2Ui!Iw!6vURT+iAy_yJ!XnJ#f%qa&mG$+!(eKf?mNa zK2=z(!)!K>+Qn;p>Y+4V0%B?9swfo&vpQ-m z_36`>0=jjhcJ11oLD&ddn~;ug*DhVA zqfPqFG<(k6`w1^Ct0_6oL3?#qKo%zn&sVV5ytE04d z7p&i~5zk<<0-Y}_lXbikCvsYjy^5FBi4)(E73l2Qvv;+xT^mgW1qF2T=1qcX zYHBHkm-Vo)_i)^1KlIa2$D}oU@S~r#KicQb`%pR-yg1gP7h6d13O=1K0)Ho7pDfq4 zbLTGP?d>fNdj9-{+9~`lh~T@mapT6z3RGJ9v#4#~zWoF>G&E2~Muw%%%gb-Ps5KNG zKFel51dnul_&{P}lBhquW4lkI(_<#wT_I4Bq%2*_bxAH3@#Wy_uK4;jvPrB z)uOgm`ePq$I41Su9D<~6Km2eIwQOM7_Vj#Q9Q4c?;o3LfoInfaFQkzpM#&0PQc_~A zv0q=`a8Li5s5Jy(Yl|OBJCr6G-()hi+F!5VEIn6?;9QVUV=QQL=oHkl(Xpd9d3bnG z+qP|+f)1NDT|4%TaWwaXdGzwI;j#i1pDGq=4y~TDv$9+5llo(-4SvzSq;9=Gfd0cP z@>h>LVkbo}T3D>P8L@&4LaI3DE|P4x*VeXHx%u{bdC*7!1}L`}H^K@9Ed+ zbOd2*iyul#N)nA>FdXg9Z+w&`F`x@9|E~Tt{N>@qTNUMlA2Y~F<3w%RwD~t^mIEi>)7#s}^0k?- zZzpV36b!x26$OB-n$f^n<~D|$ z#JDgPX_z1;q0vyskcuK{9x~{~h=kOqH8cFm+<8pBa7qDLT|suu7{b^yBH1x+%v&%? z0yi_)RUS&r$Vf7a!Hd$6rE1GO%WOag6~*x}S`#U-VsMa9L%cZ!OO44RUnqQcvE?%cdpcvaXh+ROzoMCd65 z3!Ma$E0MbiRv}9`@}EbK&Q(?2y<1vVj?&UHl$4aBsJH~OKoet7P)Hm;nl-`05fB>PTDsqspQaB`JRR8l|x5`9do2?SI%1X#pBuVp; zd*LF^o;#0k&VJj(7@QH#ozFpb?ss-g4j;|J7k|sZx(%E5h@`!QX8^PoOq$5v!bD-O zkXBw%Q6R@NDk^ODtL9kN*Vwgt4^q>18a$y;pS}As?d_V+te81-))kRCUwPKqo z^vA+S7cPE(@XXnBxP0XbwkN0H)3sI)#9*y;Jz`>);N^b((4k{Tn}~c=7)GHN3ZwV! zKTvh*)M*?!atw(pS7AlspF9@>9^L5Ht^1E6@@XNFLPyzbw%3y7j6Znjup9xK@JU>} zfd{iJJ^_9EzJzYwyQ5XBR+lL@g|=j5?w`PKYiT=oA$G|pSQN9^z=Qdt9No`9-wPoj z-O#dSz0fah+Pq~{+|qbRi@EbYGVpxx?6W^dmo8mJ=$3Ut_mVT+vT#uhKC`aFjG41k zu>ZhUaAnoi)r@Hpl6Rzl=FZ%Pthl&@Io-*{c|YO)8|RrGJrLZvGt6dlz0k{+#ann4 ztXjRs{tl+HJRuS3a;%fQ{Cv&|1tOD)f!-vSu&c^K7A3DX* ztk(6K-{_c{lWR|z@_ya;_U*}_#l*y-u<&+6aW0`TuAw?;?$*VY@!ZqZt5@xFXxv|d zoQ#a&eAPIop*7jkcdUnN(3p>TxsF;#=gwUa6cpqv^qe{KER*Cah>cx>QKLrdl9iPO znq%LUD}Tp>2M@4sUnVwh-iq|}ohT?Mu=mZMzo0IT#&IR5GUhU;6kTcWi;kYo7<)g> zF^(;L&eyAh##+>V&g0(Qt&1(k3G>q@%tecfTgo`L%&YZj`54DKxfc65m%d{?YTa5+ zCwOcJ1_r{%$ERNCnKM4Hyz~1=EL^YR0Eh>v4aK)u2yXnYhxOx^(wMxNpu?_o;rC ztW&L<{Tz3k$9=l0kH>bqc27GCJuNEQ^7h*im^)`4hK0SR%i%+ZLDT$-iVE!6^F@QP zGp5e~ZQ+7N^lOi}b!wb4rk``-gh_RAY-w+eddn$>K5cpYa+h^9WH)Zy{uMUEy z`Ls1_)~a#pxL&M&oDPhkHImQeIBU@vccU)-J*i`=kH$@%=aRsu1L5!I?=1ABiIXk7 z-KWcy8aiO0E(i7>08R7D%gV89=dK20BO=Ct_TlU~^lP7GTcE~;4<7;As#U8S&B<6T zALD5a#X012InG+temCplT8-{W9aH_9ni}qjd0nE;b49y$?a;QZADTCJ5IQn4(h@#A z9Mh&vL%)~%>5`e537Y0_+_(`n_iOCOh#H%ln~SotGAI9f{&h9Z{;Z*~)yt{o(B`Qz z#37%{an_>tyHS_6R-=1T$5cN(E!|#QSXda;xhGGY+35*H?J339`=^s z{5lL%r%c6*efsK>k+Ban$G)7L9NfQuAF1+Qn6Nwn8`f_?eqO%4FEVm`UECj{r-R0L z#u4UY9HFn5lmENMzBhjN24mQ-?4=%>V?Grc*H-)8s7qU`(LJeSs?Q^rH4={T^J2?2 z3CGoWuJDs%yG@%m&O*xA~Jr@Ih z5#tXfcI+Y(F@IsW+|`G8uJhf?-{0RuG4S*A<0}>6^SRnl=pbRd+~O literal 0 HcmV?d00001 diff --git a/client/Android/Studio/aFreeRDP/src/main/assets/de_help_page/toolbar.html b/client/Android/Studio/aFreeRDP/src/main/assets/de_help_page/toolbar.html new file mode 100644 index 0000000..c40694a --- /dev/null +++ b/client/Android/Studio/aFreeRDP/src/main/assets/de_help_page/toolbar.html @@ -0,0 +1,49 @@ + + + + + + + + Help + + + + + +
+
+ + +
+

Toolbar

+

+ With the toolbar you'll be able to display and hide the main tools in your session. + This allows together with the touch pointer and the gestures an intuitiv workflow + for remote computing on touch sensitive screens. +

+

+ +
+
+

Tastatur

+ Zeige/verstecke die standard und die erweiterte Tastatur mit Funktionstasten +
+
+

Touch Zeiger

+ Zeige/verstecke den gesten gesteuerten Zeiger +
+
+

Beenden

+ Beende die aktuelle Sitzung. Seihen sie sich bewusst, dass das Beenden kein Logout + ist. +
+
+
+
+ + diff --git a/client/Android/Studio/aFreeRDP/src/main/assets/de_help_page/toolbar.png b/client/Android/Studio/aFreeRDP/src/main/assets/de_help_page/toolbar.png new file mode 100644 index 0000000000000000000000000000000000000000..42f055b9f36708ac204d45bce45ed8b301a1978b GIT binary patch literal 7010 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 literal 0 HcmV?d00001 diff --git a/client/Android/Studio/aFreeRDP/src/main/assets/de_help_page/toolbar_phone.html b/client/Android/Studio/aFreeRDP/src/main/assets/de_help_page/toolbar_phone.html new file mode 100644 index 0000000..65d0f94 --- /dev/null +++ b/client/Android/Studio/aFreeRDP/src/main/assets/de_help_page/toolbar_phone.html @@ -0,0 +1,49 @@ + + + + + + + + Help + + + + + +
+
+ + +
+

Toolbar

+

+ With the toolbar you'll be able to display and hide the main tools in your session. + This allows together with the touch pointer and the gestures an intuitiv workflow + for remote computing on touch sensitive screens. +

+

+ +
+
+

Tastatur

+ Zeige/verstecke die standard und die erweiterte Tastatur mit Funktionstasten +
+
+

Touch Zeiger

+ Zeige/verstecke den gesten gesteuerten Zeiger +
+
+

Beenden

+ Beende die aktuelle Sitzung. Seihen sie sich bewusst, dass das Beenden kein Logout + ist. +
+
+
+
+ + diff --git a/client/Android/Studio/aFreeRDP/src/main/assets/de_help_page/toolbar_phone.png b/client/Android/Studio/aFreeRDP/src/main/assets/de_help_page/toolbar_phone.png new file mode 100644 index 0000000000000000000000000000000000000000..278cd3a9d869689630656f13a6579ca02b9646b6 GIT binary patch literal 5555 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 Zwvi4DVMW`7x1X^D>Z&>_<;t(Z{|6Cv0iOT> literal 0 HcmV?d00001 diff --git a/client/Android/Studio/aFreeRDP/src/main/assets/de_help_page/touch_pointer.html b/client/Android/Studio/aFreeRDP/src/main/assets/de_help_page/touch_pointer.html new file mode 100644 index 0000000..3da3ef5 --- /dev/null +++ b/client/Android/Studio/aFreeRDP/src/main/assets/de_help_page/touch_pointer.html @@ -0,0 +1,29 @@ + + + + + + + + Help + + + + + +
+
+ + +
+

Touch Pointer

+

+

+
+
+ + diff --git a/client/Android/Studio/aFreeRDP/src/main/assets/de_help_page/touch_pointer.png b/client/Android/Studio/aFreeRDP/src/main/assets/de_help_page/touch_pointer.png new file mode 100644 index 0000000000000000000000000000000000000000..af3ebcab0071a96ecdc452b48e7ab92fb335da41 GIT binary patch literal 110449 zcmc$l^c3w#unRFGzAr9qGuSQ-JPq+2QJkglamLZze|1Z0<7 znrHdGp673Pe%gKAy?5`KnRDjMnR(9#ZB11&VkTk$0LY#_{qH3JfM5WCmqLh-y&|z* zE(m}srJnt#r0Wg(mqq0LYNHO*gbB#1Xmgsiuj6%+S(=+oUL39aC!eKiBojqUg}nFQ z*DpV)UQ<-X=yWl#_f2uu_tjDO#jre{ho=em4plmET{!ke7V&<9mF zW3}1qnY9>&OO_vF;%&Q5Z$#77ITHu1MbpTOPXEp6Lz5}_?C|+CO<#X=-muJ?t^16vdML}+NwXuV|VJGX}zPNzYf#%`YeQyuSe$lqcruxZqg1a zyr0oR?PvIL=?eb7hvCf528)>zey4lS7o#!nU0ei-Lj>xp6rfpNVfQFh?Xxr*MaSG2DDTkC6hMoRgEHlk_YfBO9V^0=-E zW;a*wbO|hVQz){``=Q!gYQ-I^Q$VM^61D$+kl$=*LN+230M`{ zd8es)U-VFGO{{Flqmej(kON?uCF@1rrcx2F%3v~G@eTV1Qbdz)U}zmuP#>_Q=2Rc} zLo#vcIQEr6GXUuhzIQxCq-~3e%mlMzx(`a ztJ@AZ5<9#AuqxB}?T7eds0JFx>iX_StmtBCq~I`o7*6isJ25RKXBs|gKACic?cH_6 z2h$eMYB@fW(q=EzqU)a|gKq{!9G=734T-;2_f4zt zF;Mk!WeHtmB|? zcrM32vnJM1TohKm&9utA3}A~AB=x6kKOEu%z5bu*kH2@GWwXKA@u(J3?yH{lDxt2) zTM@H?AN-&0bz94JYFQq)g1ymQgE;0-dSw>gx#`-JOi00 zYKD$lXF5>lX%tQob;|exZJB0oS#TdA#|UzKeesaQuAnNKaZMUo!?L!SqL+Hyr+*3g z-hEo}9+f46fuBH$DM6(;-B#}@B_t&9Vd&=F3ghhIY^Y;ozF+XVWoCDiNjG3CTG!=t z|3LdHm;iYMC6tPC!(EDC(W2M}@gYwJX~q?fzOr4TnYXh2G*fP_FSS@@-r1fH#B-#4 zgvk5;Nfoh5{>TDL)4c;574@2<8cGxe89(+F(_#{;j|zeiBX*9Quh^DJ&^H7` zLT~+RSc1%Z&N=O^)6Ks>8u51NoYXgCHSw5yC5xASH=%4tcaemi zu>(4kJ#Mq~scnc_kp=oxcVl+!w4ok9T1mkV-YXyhKu;;gb}s?fx5}ouIA@yAK}5p- zd5veQL92sKsd7GUgmUIyTUlQCM_XeQkoV|L%9>ck8*lV_q87fE;`KZ9k4IynrxQ9_ z_muHt-kQ^{{E2l?cN>zsAQY z)w5(g!ZHc2CTbR;ri&di%+0P8(Yk6YU*Wb^hv zke{{BRGM|zt@VAJ;~MXbCb1yy0n#185Pq|E7A+owjsX9vw`+aoN1G!n@aE7B15;De zD!bo>xrK$C1`W=pRrZr5=6`<`urr81zzdgLc&7uH^zyZ0Fde8fcf7o{dyt4X__&o# zo9RhzihQ)e&QIq*PlaPg#dtOGz-uZC{dP1{y0a^c1P5VYZ0uLDcEXwwtnPG%yO zgY7nklG+X~GF>r8FxA$o{vgW94Zr;+F7M-?st2>m=AgWgylv+S&=1__#vA7{x#105 zdL-r|0V!LOVcEH0bvf;@`yCPe)%9s{j4&MgF2@78UBBaP&4HUG1mpJ52~nEEIw1^=J9sSep@0Z$PTy2Sm^bMn=ZUoXsv$QTtz5P$7%>w-i*_Ai3 z3ri4}ZTTg|CkNmWnUkcW!I-16w6jx{=RYO{kH7;A$#{0M6BnP)Cx@r0CJ(dM@X3-6 zpm+$`<7PS4NEY8v%z_tN_8SA?c;PGGrVgQDy5vJRGqAREu-4zNgto=a9khOq(}5|B zVr8R=G=U0t@A_`^F<0KcDq-DF^S?N5a$D{B5Skm>LL-Q{my4tA?FXZL3X(VH^g*g_ zoMk+8@v<3j@v2-dt8K=A{CFXFe<2!tFn`9Vus>U;CTtG7H9y{dVe#1ePmK+d%5})! zu}79h%dMrIVtOl8c%9Sh*~ZxrU!_Tv^zwN;?&5@!dBRNjC$sEa{V6^yWY@ zFB(64HaL5a<4M^8`}R6+oy_~s;Bo7k@#(N75zPV&bvlY#_VMy7<#vj(m@DRN`n>YZ zEd&*WaXE3ZV|*CL5wkOME{_!|n@y6hfPC(7SM*rY=?4R>PR=pTzOKUpNe; z5V^+Yd}~`mC89rkU!jCR^;c@_9@~kMpWR8o4=@7-cn2V3-?PyglKBiCft0&+}>E+ z3|k5;SoQ)B&(H(}UhkTXLbp_BqPJ9EgnkJP&spCjlnk2r^9G}fJ|D+sM z$E*@g>Jz0$-^w#VcEJyd5ji^R-VyyNhdavK z1yC;^*IwEg+F!L8%{6iLpaGwXH^q6?BuY^nt>Iix-{x01Kay604LqlDOZG<4ye1zg ziRZOTG~A+p{8DYxvb3v4;=+R{e#!h_B{n>=pIgjfyMW{jUHU8Sl`h?!N(OQwXL{4& zfO5tMWjvv(FRO2(SiJ-OwkqTnEaTYo44foZ(e7$>V+ko{$1wIot7E)>R_C7mS6faZl_9T`GojY3U{(o#3r`-35}?-hphM%1dK~ z%1bWKwhcLj&3%}GXCNDTT74eQ2SoBgpC~%sE#qkN43H7xle2I`yYL^+z~Ea_^x@v0 zLi@cFLwjLU5zwSJh8Ggq0ZPQ|#}8^qDJtW%Qw#ic!)|$|jVtr2rkSY8HPOuZ4?`<@ zj&uM^xi{D-VzwQFdEw;qa~zYgbk!%+`)GFTP&B#Zh4f@An|Yo{8aOnV3lc!k$%Uh- z(m2RnYCaIxX{lmRz4C`Ld&@&w(}E#5ZntsK+=%dHSUYC%ybRK*LtE*=6?U&xVoJQ$ znPGh2O=ShUdxpcIA=D;9+8}OVVHZj9HDUfPWS&J>wkkHDl_oKKmP<|8_@Zd_`ksX7 z3?h?lMF=lEjD*eF;|U#Fr&6m^GF3#++X{HVw^Q(Kw zkWK-{R^9fmPT2kRn^RcFEXJ!frHs%=HngsGY{37nS(li zR+kiR9i9qq$pjT=Lo40o!*VyVPG4O?Tg76D+@|@O%+jL)u_9A=|4*iCf}zu2!6PR+lNMtn$8JHop?~$Q!y-@d zsHmv%qo?un&1eXnT`$2N9+4~^X6>aaec@Zgase1Jp>bdaEcz#)1JKr!*=}$DXo<(r zal}#yf)V|_kXb?Xi(gHyMwIO^JUK~G2H{X3a>6@8Q_IetCSI!f1X^sXsgby!fq4Y0 zF1~yN22L6)Ow3d)PQYK?gN}J+EQFUpsO|xN34QQrCGkM%#&wqEK%Xsl)J5n^j-?Ts13R_OIPxwS0(jUV((!($% z@JJv$8)oF!#ZL6{@C+X=l0W)A+tzN(7!fL^{2pNA_sSO>lEqbLTfAWAG zMUZ*5^uaP`a;I`etHAz}WsCCuPP17MMO&cJ^(3WE_mb|0zI!0x8o7JT0}QskARU|r zD5hTu#bH*i30vBP4M35mJ=Z^}2jp!bab(%bLMf(u`CD(lhIa;N<^HB|0vvHf4VCia zcLbzc4QpSs5`XAjlTMywgI`8(-nKuv(c0SrBgB74tzB02e%X*{E>7tx{5kGGUHQMy5`d5##D6 zsE!r?EbKMlxFE`itA=(Z^b_HUqP!qKn!b^FblaZ$Psp-^rg0*knL{;vOZ7H~4ra^u z%IG~l$+8=MtL->+5P{@afyf$((<3D^eX1XxIqD2WjIl1*Y?*Ogel9K>U3aCU4NZioL z)%ixk8xvJo3Lu|d=J?Z}NphfFodHP>}HE)!cPtHfn z0*$Kx7Gr3#@R#pxEaorW5#B-RI1e(Drt9YeZ-du3m~wAb_cAPR34o2*>_34qiCT5Y zJ?nil319+D_;D=#)`1r2aeoD@PTKR44qsUhiT6(p6DKj*ZTx)2Zb!ndT0=0v)@Ja- z|GN~Rc7LCqknYX-pD4Fm$ZQlTVng>Ok>B~CRW(=UkZ>M4kr?*o8kO=AP>;OcB*e}2 zMJNCV00?^C zqUz%Oj!?25CLb_N7%ErNSPlhMg4@fV4)wWPwmypebEVxJpV22Anru)AyMuj^$Q2E6 zAs}qok+gnWdLD+1kR%v5siO^N$pvde>4;!|K%-DV|D{Oi0J7AIWE0y-!@@-V5cc<|5c7VDx*I$rv{3r0%X5~ zZ;4C|oappVHRUz9J7)Q&MQGu7Xj&p-OA7aer<77UKTKn+ctX+XdZBiN`vvk?4fI|b z_BV2yhc!Gf-#B~sm=fVW6Ajf3I^qu|oS>jnn$b*ztNKD7-s&&4c%eR)kt9deR1p5u zOZ`fR;E637J<1;fDJ28^Yu>jP$q=$Y9zV z-(a_Q(=jKI;WnS%ib1?0t`bv>P`ma}2P z1m6)(H8Xh{SUW(=#B1S%$S0>~;@fX3okDdD6ZetNp){6M1qF%JZbC%QUOecF&55Jb zl4noa>A|L13|q3|E;V8pL`2zd*Pab{$!&Z^Kjw4;D64Yi($+pU9Ni+a!D)Bk-ps7XJ@@5?AaB*TB#AG!%dloEQN)+%ymaMfg0xaBH;bH^PQ+ZrsZNRXO1ZixO~; zjM~s>Jg_b_loBWO;9I0-hE{*)kC4mt4bs3zZlfg&fv(vNYzm@==94A4%8MZAoS!IF zwBMyH5JMV7hOlAi@UwU;ivQr*7!H3F`ieN64sn2){SRWU^sc%en3=-QgWv$aUj^MB z7b%kW@_h-66K@7W>S~!Kr*MBU_ZOdw`M?xO71r@q>%$;;n1t^sEm6?oy-N`xDR zf0Q+k2Dx&q#S}0XL(*HBwS!U6cwqhQS7Y`QU|PxDV*eaYK3+afhts=>2G&sGfxvx% ziL)hYMKXjFIpV+{VHr53$H3cJmT4|dIBp&mxe00$mKq|NFo9A!@&e#jCpX?Xsc6i& z7(;UBy-^2HC~;Li()HGF{gG3RMCx~Q?q8JJe0le>^}p(wic}JpHMQh}Oa6iw0xNxh zDq)X+U*Evf1!Y|VxeUM@1hCQ%j=X->`eozC(C;9^m;{1Q(nkOyv@@*@siI?nfIcRW z8){u4sIsoldxZBS0-BGxAq-^f9rIi3TphH(LA77*Tg94h;yEW(1ty9szkxH}`}1Y> zWTv+@+m;No!`G{$IRsp!KLz=yt21j4NC{!@h^7t(9;9EABj4VV=jcesqlgbNu zA?=1vmSxQ_a#gKC-5qRzL(sq2vnXcEg~5zk6oVxSjU3KOYk>nt6$*VQ#uu}{AgPvP z?lkdIO4?IV<;6m0u>@t^zaeYjhZNb9Jqoky9Ul3I=VP9qk;?9Gz_BChAEv6$7*Fxu zvP&_Hk)4ejsRTWwKU?vM!eH(*XUn49RhG6j;&ClsUYkBXBda1OQyTAc4Z^_FXle}) zL+f62Q|4>eFwfXn?J>{G$YV1!KqL>a;Dg}$;4dLge%oTHNPF2fV+T$tdZ^mhL&F-A zo=(c@8|19>&YGJvvsFHbCmbM_Qwnffi0Qjr4bn_=)4;u2WV~n|8H?$f9{K4}BOapt z*Y5LTvt&W}n^IkbxwhZ8SMR-md zMp^x+Ajt+N?>_Vkmo|_(gy&2nWDlRXLCSD0g_zGg_$XNn#f5=Y90{My#oUY@>H6p_ zle8(cKC4LVJMuH$9u^UE#*sse_~R(GD$e?G2e_hSdvGSM%=VtXKY*Cke*770l2%5k z45o9Cds!*HG!VXE^y$EMQIxVt19EeM7)HRNI&YhZ>q(@?=ucq@cI4G zZ&@o+$OCq+pUt$#X~Z>}A;vj*okllN|(E4{HQ)QHcwG)crq@E4Uz z0QC>CucK-c8sPcnuN3ej-Mq`Zx)xU^IxV%U*>F~M;H#Z@k{l%NS4kB;`6Xq(WyT>P zZMgmQ_V=z8ualkyMX*UL>lw(&w_Kb&h^79(;Pch zDMRs9`u|chP3-{33{gYKxKpI3r0OlaglRtBUKqZNJ+@EVtrD#$Ri}rb>Bpkb3D?R9 zXm+k%xIZDb>cAi~r9T3Kczz2Wqwt{(Fgkd3_Or3vFAcDpH6O5ed6vs|P$y_QI)9;e zu(o_*R(>-iV?`9$U${VRpj(Ni7OGttunm<+<0qXO6mAwgQ*GwO71Az?@O~aU$i_#l zTTHA&_)N)FmNI`Cra4|tehEFcD25%6_-C#C4vtNNcxPi4W!a!B=O9HRxbR_snDW{; zyw%C;Qio~5IabsdPXMV=QK{mWE;Br6xgd@F_kX>>jHgYF@Ju;=i)@eo$PxH9oSxgW z9%d<Z8;6Z@tyWD>3@_-=7Lt8yaT#W&6puYD97B`tf)R?>wIt zz)@882=A#8`HbAt2wJQVG;MZ#>mIms-PB95KsdNWX=?Fs+8VXtq}umVDsix+lg+Ia zIVJ^Prtq-KocU|Hpe4PbmO{u~{~M~INk&-0^|bK=1}`$TJw}pyJdbxbM40~f(FDLZ zMl!RXKSnW@D5AeIAULAIdqwyM4-OtCInDIhOf$|Ja%nsanK#SCY=w8UGXln%oO9O# z?0Woq3FXgb=+a4P7{Uh^q1||N9C$`wAHHMZ6J`|39(lm?&V<8a-$V3#gY)M*9{hPh zwV}=DVaM4@A^7ur@M)`pf&%rIoz&Jx4@x?{O#wp66~@C%U3kuGX{JS{ypr%fY396& zP5QwTk@?TGkoEh9g%JeAp*-GkM;v>-M>qj)x86}g!?=G_RAV8N++@Fmi00v+hfd*t zfPh%oA)cZ0G<5C2YxvEQ!1DYW@1Eg&`tszLlXxbu|I))#mLER@t*{>kc$*+qkzQ)9*C*2iWwdmWRb?@2 z1D$WlPVk9T4}LUemwonzzebH%!g!&-DH*FkkC((6`rpDRToNvkYidC*IGCZ!wZRo@SJ$;EfS;eRq#dx@yI+Qoc-%=B4wtBNhU z4Ud33cMV1kn1rpHn3nIk;8&8mXuF3X!tN7|)$$pau%BBPT6(3x?2Q`#B|+#cTz#0so_t|Mu#v z6ix+g=N|i)G%4WsdaT_)mjB4lqBHoC$-3>x+3=8V?YizW0k#4(O&=)&TPA}_tkwTX z(kV3UxLTN<)TuC4I8V3ZdtRB{Y-#ydoUdOiGn2({vS+%PH&(@UZ?xNLE4C8N=s?4{2jK&XBrUvZq&8k26+{0|`O9Dv? zj%y18(Ml_u4;TovO0}xwa>H5c>DIgseQ9(NoYGHn37*yw852BmRT#GkBXCGQc$&s= zP0>`zT9gxTp|^jpGASOjeqDSadK6pu#_ogk#Odnk^?8z6*@oL)o?Hv2{o8wO#O4{j zxdv`0?M(iljxr%!JWove^?c|njwc@t<=@Q=3@L>~RJ47$MV6=uwY>MwiH<5A>sW^3 z+VO#cf9ciW5n{_??kULiobQ8ZrnQP~`g}#oNDyCc*k4B_#6W_Kgb6sN0wfDxlk2hz zgDCotJq0Tq%o2_TxkxQM?%L?XR%+%v`xuK{HWm>}Sze<=-#5Z&*_O&wBgaaw zuvO(ky189?jtM7w^f?^LNogW&@oSO~o03Rnkv}b=4AxJMF?i&PPI9Q+maN)IP`h+) ztQzZO$1Ru)lVf%V?KXSahaw7uugp0VRb#Il%5f44qM2Ze+a;u5d~L(Q$2NCh4uHp^;9XO&}F z7jv)X1)X?5{?fq+$tgp{v9-zNP69UEa3IDKFO1I33n5_A7E}~@Aj>X{#PwEp?aaXS z{l2KZU}MsOjM~`;**R7y{`>yBJ}ba?%c<-z_h%Z~@Slwy?eA7#yGxUYn`Ua$to+*pbZAh0PH+riGZ8ZbJ)#hR9&L4x@R5h8!IB zePX@T3QwVemo^X_qt6;8vbiAKs1bSLrC9NHF&UjWi$4$x7t*V%*8kb_TbF9ZA{%V3 zvAX`=fQ=R4wxU8Y{TfFp>OM?cUdb&uB%IV#4P<$+`lf4~YP_mUy-kTH(i^&WD8ax> zxzWVx_CDnJLOinnlpG7A^b5ka(v@Q~IUsDy9JpKZ2N%W1H7(3*{|q;j_Tz=qGubR$ zaIcK`QFqC7IlHg5$D2;W_yJy{S^7z%YrmsP%FZHj*%vK!6@=Tk7&dYBDQF23auPW$ zIgz>`rl|?YF}@!g*kXudUOn$W3&QURcECNTSo>JRY|UU4r*QqO5fX>@zLg$D6dEL= z{koB7&iL0S6^D;>a+b>x0+QIAs{jd7hAi+Xhz;rY^w3^XjdrR|_CZ9wZY#T9U}Ha* ze!24iC^({|8(V%vnupTl#t*ra>jpF|c(0Mo;XRlu(#U95kr0{XnVj?mX}0Ma?cym| zQhPpAITQ@~x!>S8zyEfvs+!DRZqr9T+}&EW(bVmuY0c+yI8)YclLU&La<0?wxm25} z@>aQxO@lN0QopNC6pwE9qt4i-fG-!DqJtiK%)p}o>tBkYj;)GrzJfotKO7aaKOmqohXOY1KH<0Km0em~Ja8^; z6{IZ$>H6+(c-;^9hxm;@Ubsu&Lr%i;Am z#gh_Ff~)j4y^W=nt6(=>V8?ujP?Obg%*g?3qsBVe;KL`<`y<#=u($}YfcE;AF7DH* z)drJ{87(w4YE$Za45E|cgK@O<{1zcOR_+zbkk=0Yx@!ZrmJ7NRqxj zA)Fq=56$dvm6PL(BL3z4>kF_hC!XJ=uumNwoWq}33VxsJT{U+n12YbkI&Fyzu*>-A z@He5!5n7D%3Ph3{@6mQb3>wLcr#L#YylBqg>OQU}5F7pIU*;o$8``_mL{H|`q{UAx z^NX#Cg5pJKKlq!3F0Kfcjha15X&}ON!-p2?6o8<)@&CAK5dxM2!$%e?{+54%?TB$7 zIwjy>g*!M^9UqpSa#Rjn{u2%$7dCe91l!Ueh(QfPvHhfCu59j^D4(XSw{pq*O!k7n z*OHbd9N`Yi)LjEYM^RRM;8)F;@ve|zdc-<~Dd4v79*8_3*xv$7&8aN<9aJdEq~1-gZaJ8Y{I$dPHzLV6cjOuB7}rBe zx&8^xcM3>!nOyEqm4AL^x0^-O24DJS3}5&nc;YuP3J_;dN4Ec;^MfQS8A;6qGX& zg#LFKA}kOdM1+ia#qr~^do6w zV)<-O7v8*R`D((6V;*FXYvF1Pf&weCt#t;kf%;aT6(PNGEI~=Q&*!iKH!menGX~|Z zT_@E&N+Ta^MLwxEADQ*=BSWZi0@LNj4U!&#A2+Yl)LkOXxvBxcWrn4FOA1D=70hsN5+nja&;Og%^n%ZylSXlQINyfHfyP9qI=L|@}CnyZD31F0>8 zaS=l{!^x9JF1F}LX>K9r%FOwb+;L?lX^udofQ2vH)r$tW!@;F>>?*#%JE>%vUF=wg z3^&O*dxItHUej)#q^WD@Jk*R#`cCvn?Ryk&#cWYdo9EM83kcS6jYfAMhP7#Q8&{btc|3mn*c7Y4uLNB$_Y3YwTGo2k>4S@80b@Nyta2aZ%CP(ohc({~>mQ*| z!m^p!IbD@`c`F_#r$$|9;sZIdU@XpA^|X7DRo0V5yRz~j86Zo^?=Y>7)fIln{Si!E z{uccPk-gMBf7m_j^S{Qu?(kSZ%c15~sh+s4N296x09LUW)kPtFse8PNmwUBy|6SII zeA>t~S?WFj%l=OqPF!BY&e z*FZQ08$|AWJrT6!>MV%b&<$6c!x~OUC!(Wi{pOCzdk#CjIvOLuEc&5j9Y@l|J7X9h z1%G0dZSUq(Po-3jbFjV)IEETAIEtpFn=rvQSL4ggIs$~?kg8!?1j8cx^gw~4ut(0v ze^{-5`>?E^N&$HNoAp3wOPT>xRPG;57Ijdycj^?s2T-2_Xk-u$LeK@U{2%0)X7m*6 zs|KxymkvSDIJGxroJ8DlKd^(*_~rmN4lUE+xBTzU*>xEP^^mG(X?`kAZTEml8=wiOjotXuAWz7c0#v&Y{eb> zDnS1TdrP60I1O+HQmU(|saXypfDr0c!bg6Uy9$&TG&!%#Bp`>a@5cA}w*hyb45jq|J8bj)V2s^RH;C{Cg0u9TSvyhsrilswW%`lXh@MEc(d}w3+SQ zL9nES{cTGjYkoCBD}0r$cLjZQc0lbLpbBW0cG3EDp_WA0d?9EW+x|#@?Y)j3g$JiA zpJ}}d$J|N#E&HXWeF8~IZx`^OuL1SSv4E1Qr)3ZNs78+4i56S3{w@T>;#n40rriWM z$O2p!Nx=XmqWhMzMBD8$D@*L0MDg&+bOFd3^8c zQ4$X6$7GX^N5#jHZ7MAg*2nsg3Ar-PsUiQOSbO#PXkWwvlDI_qd6A*71SA=;-~ zgjFW0tB-y@Gy~kQFlq>Um9NeDQUqxH+N2zPjjh}v#zxKyh#x#-3pVujS2z>SP?enztym`o;p#TsWTwnNH=|KvF#g1+|}3D z2ej-r@9IU`v~jSoJ1W4Tckru}GBDuI+QaJ&=l>9~*0ITIw!``r@*G*6xpuQs7$8wt zCZHCYBV=uP?lN&FOI~wS4Bs4~WQ6caD zVA{(4DMkIEElfMSXA^UmsRZ(Pi(AUYS&!Uz2K_gMWX}`AE;XC?R+f9(V{y6QH@4^x zd=U4&pXjEl}%ZW$b5n z+&j?#dY#a&me>z#aH$jr77#F@nEj;Ma?1na^_)OGEFCA|$#wUXB7^|J* z)R2GE`PNL%lRdRrkF~M@lHwi{n8m+89Qd(62tIwDqtpemTb`$%etzhiekr(BeR(aN z%QLVFkJgDLxyB4%wf*%fLr#sY_$B_tBnKNvcNE1;4~Z@m1-KBK?|&@-W;!FxUK2j? z^=BK`p2k`F@NN9$H%#bwQ?Sup0g2pbuqdyhOLi()QV3WB@(7~^CcIQ3l#8Hah=rhmyzITXI#iH+`q zvGuO<#?x7c>N56$spQZaN7o0$t1(tmdJWxjFM;CQPE~JBT0?N$Qnb8KMbEh*y}z>< z?tQu6NF4JQo309L{;wLRU%-5GzWwl9!23cG-%J?GKR#{RScsGAS4lpvCK`(Ly?%1r z(&Gl0=Uuj^Vhm-heR3|^>dIF2y!YmtjyPV5Jsor5Ye-$H8J^!;PKfxZqp66D#tr(f3?>7r?^J?=!+vuiA+q`_;L zcT=nKIogEDeM|c^t=f2bujD%tf1CRZ{R3cQ=g)>sIslnQ3+<(V}dfjQ5iGZU}`soc?MnUmQUN1#6o}dJ&pX_=atb zrcd4&cU(CWhvsO8^>Hot?$0-IG+ykKCK4;PDhWLpT_8n#EGI;;rh^c@t^0-%?}6eK zyHMIzH6WSI8Sq_l`gRk`e&k=z4p^>&0`{;Ns8O9|Z`5)hvkPfSX=#YlT>Xe!Yx|l` z91m&U(eLaIsq?PGt%EawZ4m-z{+}GnpvK!q3wJb1auV;>b9gFup?ogb)5d;I1$(gD zItgs?QS;urZ3)mpLxS$ZaC4EALR5_q@Jay*rVjP~RrUPXZNBoFR`nHykms-bXM=-@ zTwGY(n+e(|>VQ40MqkUNhxgij5&}NKXLNQV%2`sxfwrSB-K!=b26wZc2)@okAa_%p zq_wI#I0tK|Ouvuh(T^gK7fn~Y1?Qu@qD7KlI@TI59Z!IFD@Et+QBnP|iC2Gi3e-|w z{zo`fZp`dBQ?>Hx^E75q*Wi_ZCs>p>P3U~oD6OG*~ zrV_+ac8jn`_5VVnt-0@^|M=47X7$p|#jLEPgxKERKDe}^+5V*G1GWAJsS*wp_B1N# zD4qJ32O6P2w5ypkB@|2}oizE&L>{*O;Ic_G9xkZ`^ev<)m-ZaIV{$(R>b{irTV-WN z*&FW3`=Y&2s?oB8;uJ$+NuRs^e~}?G4^ihA`h*#Z1Hcfxa4wocIktk%AlQbXlI{6!+hzIG~0k zA`RlveyEEdnxJMvOA}+n$&Rr+z~>82jAAQxxt&wfi?Kqqmr3nF^D!!5f7WKL-xkFbLY-W5_pPV4 z?u398wt63OG^QxDJ-Hj4>xF{b%aP|`^_2E-JP_lYz(-nCZ*U|q z=lh4lyF_YpNW)1PYn*4QEF7MEWJ)3rHeyv^!iK16UotYSmQ~-b1O{Gk*r{uFx`>=h zy7^o5>a{849%oA%2Uo^q-+Q<3qv^Wnv(=8{+uKY3#wuP|4jbAB{}ycIKAYVu=p=y5 zs~LPO;#ppJQvDyyEt0+ zB8n*RY3^Q*2vN;8VJS1nqbwue+1Uxkp0`5hfL1Ke3yZ7kCWhUSx9>vz0+}uXxUuDv z54v@jXiGDP+v#}yaaZ-H&p1Ragvza&rw`y_^;I(!FxcCe=ZlZ$)gm*ECZkycK25C2`>LE`FWfy7@N<@LT~ELJb2vi{b0 z^$i!jV&m-@NGtSXzxOLtMzfL|wh;l`F(*mT(Y!2L(o9_N6wNbEm5}3ZqP(VD9(aF23Wc40+j$R4w=)B6tx0fb}+8=CpgLtD-9bPF~#gx7)um%o27hibUn!pSeoX zc$Rjd(BJ~w8ZFQOYJ>oWWA5Ij>R^_*&6X^LlG#b_P%Dl@(Q#A&!GuH#I9Vv&BSyz=&YU;YW$Cel%0 zS?BkkKve+nuuIkC`4(-!WxPU0B!FqYQp;S)eJFzXxtr9|e`)SUQ2P23(u}Kt63U|Q z^;pO&xj3(3EgQltw`qw?5Bj9?qW%uD$uU2QlkjBez%a`CESbJ2WUrM&`F*)DS}sPl zl=}ucuY@G02!-XLgho-HF#XRdk{jdItl~hhyltgYlF7wmEILnd7Pd@XOhyXgLJ-jj z7}2C7dL{ie_VUd8Vi?`BgM~?BgY5Zn?J{q!#cR96?RdLGyTqRBwC0J2zu%+~YWn-I zY1B7J;5vx04R(5t2A-iktK(pK(zEkB#gubNx#DJ@Yh{|)e_h_Z%!oIaCTARNK4=WB#W=grV!C#PKbiquYsM7=e zRtjDJiG2JeX1|8_lCqQJN_Q#D1@3Lv@zME}R2Drqv+5+@vE0{&s`igK<3eEXxI=s^ zUlO#bLCGpm95@b%BVGeB+(K@y^BV6RE!I*0A=ksBOZ5|&&KUMcwSP~T=>shn!h10< z#?Tgkwby%cl#z{r!je}sU;82R%C-N$hz{w$2?S;Z|H85rPi~4YAdkNCZqL?5hquRD zR8lCmDg>!ZN!(xoJ5m|1eU^87Ge5+g<_X-q7a18NDCxk+SnNuH zWBS`&^PV*DwlN!&H$%dudh;w#SAx_Xb>yLGV=E5=C# z+~fq7Q6SIQYhk~$8j7=>{wGx-N~^6SQ5-|>8(8$QDXQ=dZ^!Y^pQp_>%|0=$;|C2P-V0;EBd#(X2gYCRn5z9#zeN+U#1!n(@n&i7TkgUFDwKk&frx#aV= zm;}ShA%Fehx=fSh85LTK3$RW8)S}dxg(&}*YA5h`T3C_r&nvA{!0`ti0zR`a_=msv z0S>@i3z*;UBF>>|P9uP~ZvG#$ zrQ&Ce$m{~6=j}1A*xrcr=(I^p1LRH7(GB87$?}2c*|Qd+uisPo2=@-b%dE9;YIQ&E z-qS4VS_$yZ2%V45!xBe1T#HoiBTaIV%#j`X=HwHKoc=+uo8ainDdO)sv@0P^uzw_( zih%(doM%l=B2H9@pYG>7HF&v-gumIM(qBh3fp3klqYb2BGa8l7N@qYI1sLhUsvxRi zR>T309k$Sr7V$&oh(z2e{_mx8yu3osbCV+w_i$6x!x(1H;nr|9ZfVP-O^uFsT}#wD z{((i>=d_mxgArXz8|xzjIblBE6AR_2d|$O=ftz+1kraq0EN6Y%z!!fV+x(7i+n8>ewH|fVv1?C3r}EOmwJ7?@^zcw3nHbi~=7x8!R_~9| zA!we>as$-q7`Kv6k93w;oZKxXItM&4l{3RF(rx}B6WqZoNc!ckP215)i%bntBp7w0 zCdTr^E44(ZpIIn|`RS!ID0L*x@T7f#y@as?)y=;!QXYRTl$IYes*j;c1rG zU+U80#3|iD;n$08E?G)BINkcq*)>G;M9uDOn6w>5&Tn(=B!kjYF)OK$$)z$NBg-EX zq%0O=1e&tTR&#Bhs93aWPLq80^CJlk-BoN0G)1E@5dLsm26ZzIj#|yOv+yUw|}9+Flp@@bFliPaAIKg^wHR1u(e+)c1yN3n#6hix@7MQwl^Uy;#4K4 zTDP^2u$j?CKW8)Qc_fQZx3rZGZ4>?m;ca@r`MVf=d*-f^nP_oyQj+X9dD^Ksez7!| z+%#z&sJU3&ZWB9vHD$mbTxUxb=UrrBqr|I*(zbnezna-c^_DL?!}PI!Mdz>neu&^> zyX~=@K2m-MDnWSyB^3+#c^kj0FyC+Ne}E7du>Hx~jOZCCeXi+cjw@*ZZ))U*dDue0 zF7_dRN;!nLY%MTUSCR>^+$57G_no>W7UrK4tz!hh>Pv>;;MlZNa}*x-59};#0guWlnZb`|ORk z9~~0nU?0ouYgqFH#ng=tGw$F5rLoZ|1@k#%dsm$t8o*)tp?q6s?H(D$Q>6Ta&`v~q zQS!z5OS4O1mhCCoA)Hd|?(F6482Ka5ec#~?i>3!^v4-Ju$L)U^p#)7ifOqadYj%=K zJ=~gt&FG}F<2puRZi6&7!Pld_BV{E>68RVVA#l*5lS#iG=t%!-a978nJf0e`Tm_`X zLzswFFsgY|FMz3n#qy?2MP1izqi_fon;jS_a|*A1T6;BO1SXnMs?JNu$0ps(&$~GQ zl~qFxLY;5Y3sgWV7<%n2`SAU6*j*jFeE zn5|eUqm=>bAZhd=-7LG#)C14Y_WYVOOsxr&j)lNME2~CVNbo^^tSIASpudq* zb_oe-+kca3kjpDiPFk7Uiqjy9*C#IWpcuo~PRD-aIB4ojGYuBAQ}Ca~BQ|e*Y0=A3 zyTPKlRh(E8$pp3D`q+m~$sM`X-gz}_&p4L9wCJqF2r@~z?jCbyMXf=}FA5YY0^A=M z0IIr+fIrC_uv6?9UZVjpAkX~-Fy{)oU0?=nQ@51I_z&UIG>JeibmwS``rGoz_ zHvrFj^ZR!n4Nc#L#U$Z#ZXl7{UbExqoEx}skR`Ny^TB`M07_ePS6#;2@qy+OavGYA z3kN(`mc6cY8Aw*7c@Ihve+>(YDtDCF*bR~U&x{QtN|OnOHnhkb-P*PwF^V=+cF=;VkNXNx!ngwH zz3GEE!_W(Z5XOE9D%|*Py(e<2Rh4RbgK+D(IS@PF+i3lQJceqkj9SSVvR+F5=7`(N zg0x{JM*ORMMcD6^r7}#OD0hbs^%1*a)lX$DiQNePnMn%vu6;wwe)RZ?)aw%kCr=>x z4Mfd?;yrl`tp-h?`sHl&HUCEoucL8*D)Jq$?X#wAct66xI`i5d$IrPmZMM{FJ2pjR zuuC5sXa^3OF%i$KL-~QG-JNIX)@;9S-~E>go7NGZsgbm;z#;o_%7)Md#O;sOuEm;& z7skGZL$$M4TxF(q6P1NGG7XrjYx=EZ3)j|9cnCIMPg`VcjK}>(QZd2GQx5}jOsovV z`n+>!VZjoe=4~gcF+vdg5ZK|%USjB2!g@M$sq&R zBuP2V{Ev(TFq~C8umiQ*xaePKT;lQjKh5mMMfQMN8{w5g)C9=Wz%qRNGDQ2Tx zl^I-R4X>RI|8DiYLl@Wn`>OOP;EGz=Y=|}tTNk=mpn=ja6uTwue&xQM!0k@7Fom@@ zL@4A+uZfWbSLWW=dY0RhO4aixaw+rpawcR5wJWgw@ddv|k>*c6D4*BPZ&&thyq03K z`g)i+B=v1rx_Y+SoWq+_#-POIfSgyTLRgy)wy5G4Yt@fz=W%a7siSY&a=o?Jd68-A zIjv70=eYO-jdz$hkUUbYF%!(nG?(1p*nHaAon<4`4YO@GFsD$BOQ-HVog!1X3S`x` z!81(0Q4kN^U{s_Nw3$ojhWnRZt;#iR4ipg!68#PzxE<~!Zbyev>L;K4-3JC#zQ_pz zJ_)gHsFKTbs`r|$Rs~8M<_ZztzE3S-fI^Su)X_hsjkIU@?Un)#I3N!uDcrwL9dvjuZIs z>#M#u z1Tlv*(aND5h``&{VK^KoP!*;$2|e=6pP%jgblvG~6*pkqRoj&L!a|#D#DeDFf%}!- z@-E_d%S4H^}CgCCTG*=*D9z)Y+|KXap2_5rqBBg?kt3pH`ZTF196%CouTFxc2nk-riewh$bG40}|`{pH#^HuqsQ`4?vfAej3)s5_uoTQ#KaHWPV zXG7!=9b(SZ&bGW?x9VZ=vC+BgF0}AcE$umMJpSc#H5U=mB_$&E_bqiTZ9sbNdTMz= zK|8^G8}>YlCOnk5Mqiev_J0bK82{JI1KaMSYpnP@i8Woe343xJrTd zP<4qQcoM2U5P?X(^_9=EYrd^$6H^nuBjtLei<{Zs4*5|-A?(bZ?7CbdV+L#rZ@2+S zVY94P5n>@)UtqVlsSc9HL-^I%wnTkR@vZTVXBWC}>oDaVvGso!wG4{Gk!ibzw2 z5ewGCJ36}E>gU+wTZY-iBbs5VM@(yLHBuhf#r`nH;il4gJ}(ANOgC*x#VI~bdzDlY zi2c;)HLU7qaHKK5jiq=;{1yC78jNcL>Cb&5@L&Itooip6bYgXYUGff8m&-ZAk3TM@fimz z;LKKM1za@WtP%0Jvp41yjoBI1Iz(*TTO7_*@^TXf9%fhk0^oLi3|l&G#+6Cv^>@at zUzjfYzQB)(%=SN#X=)=iKJ*GT>xkJ4T>Hh_sycjybqHv0G3EIgXu$OQ8kAl}V{Mv0 zw`U6;#9eO5H}!x{y&hBUkHp z9lg+7yV^U#)Qb#@%}Tri(ogw|Au9Clu&p&@LZT5J@8}-%rv;wTj-Z++l}kB|Gy_^< zR(GYb*x2lF%h!yE=)3Wq=Rzo>KBhMcLAZVHNEps=qPj-mU+C3vE}NFCliSaji(7iS>sNSp+y{f|U#<_{<~$C1VyCe_bV-FHe_|Ljs5>FevSK|R1x z_OV+L3>3|%SnFuj#Q&V~y;v#N)tD1Bbo6X_P%S`|)sj#5&1evMt?U8atz$~I2)GBC z9R0;&i(O+YSU_}JwEx6>y^1;-yAxy4PBN>Y!mvsu`3x*ec8;VZN$lVygARx1hEP{s z(zlGy^0pA&GxVKSCUqhUnCaeT4IpX2PzCKC5h4UUHoH&?c+~WDSl9EcdgE=lrCzSxMZ;KDsnCTa~N^Yz18lpJ>xx5O%#vUtV zO%*D&O0xV5IjdwP*g^f$>-L`%orc^`&sH})aJ_gc9FV--jrWS>8`^}`pKf6&Imkxl zE?PtlPyW?-YB2K=vZp#vY-^O9mWV=A__EvtoeyVvfjV}uxg|>VL1okA52Ke3*~u}b zFg1BOPQt<>@og@a|0`wJIA9jAK&ZxrACPA#?9Q)G^|>S~P1nj^x_94a&; z9$sVky*!EJUyxS1btxX@H#LdoG8()r^z$b4sTzIou=H=ZDi6-lbJus@zCFSI^XCLL z0!z3q=idP7Z6EZ#emW>j4KbQUQdxRCR-f%NX@coJdr*7A(~KZ%OA5i2`1bJlJ$-(U z3GhiX;%U7J=xm~)me{GVyW){zZyQ>8%~pBoteaO}gmxIFxNijZh^udX91L#guOLjt zF&fg3W@{*Lz>BM8m#*6SA2p5RMupazDVa9spogwbV#Z+dUQ?+;D zEJv1A-=&&;bK4G4t!}-(AzS>0mkkAyS?Q{-C+8N8wzSKq2_1w-MMYVIsDrF7Li@3S z!38imc|my zLf>Y+*V{cBb5QxiIgIaZ%hmO3C$}^Wt-p4^*ZKL1d-#5d9G;Oj#CPixdW@_EJ1503 zmFW!$ndNNj-{V{187~`Unj8R2OG{pH z^Y7A$$;lpWuRyD$LOiO*=|cY>950TKjAVNsrVh;wc)`}vO>ZhXL36oLFWOSBM=4DC zN_RcuE9`_X#1Snv@+e{aoZ7;U_FUWiwT3cMwf}qEe^^^mX#RsttT|va=1u-oX_K?2 zljp#9nv%P3FZ~EuuXvYqjy?q5X@xvXG50-*nh%yNQ^OMImE6!I<2ARpV;;Hn_oy_1<_a;wbR z>$pz@@gPT$r7h0qOaTETL&hUt=#xts`?x3Q8P+DfYoi(|`-V>o_wSpu65WLb2Y?KkgV`*OX+z-k?Tc&*hGH?oIyWC*S1^Waps5nAvMUP%o=>VGg_RB7|p zo?P55*&!Q27uu9Xoqqt=am)oR%xgP0@;MYk7MGW& zON52*#WgrjR4{)Qi#AtseMAZkpE_=CvYP1bPI)$n%R(vP*bh;9l2GrcPM zID79lCZx{rAIy( zJXxWwPOE-lr1lSY!Kd$830EEHmW7=IP_H(sR*wIJksx89yao0)+?ws#Q_~035)cO4 z{M0hd={E}ZiI19ODu-r5rj9!-P%4?$UEJ<-wD6se4&98z zp+*bj1GskR3OQ%r1hI+=rE@=5nIUxO*0?f7BdjcU=O=>DnR`Hd; z!ksa4!zzN!A=*&B5an%+!w?D9QxG!Brbp(3DnbcDcF5`z5@|+?hP*9d9lS>u_k3x!ZLDQX2x!1fTG%d;o z0|5&(9msNJaP>WoWTHiMuLvz_W!Lhe&QO%!|(8yTl>QJkXQ}?tEpEw-^J1(@V z%#E>-Rf)KdOY3zky7J#wERAF??tJ!a3!odzn1WmqE=Du3_>yUMJkO0Vx@#O`bl--| zoq?ZBTIDuAU>@LqJ%FZX!Ep@<3%+81ob4U0?T^2GCv&t;*a+ zmJ0hb%ryBQx>R+L;eY5ne(>jKtNUzf&MV2O=^C0D5J{4c#$3VRFJTh;V&b8f4yuYH zpMko_dQHRVr&NO?hLhu5z$24iK)hT1d* zWpYiL;QMs&w5nI|Sgub!35hBB!Q!uC!}$O*Ts0;F67 z#eKog{AwhcHWCypiI;u?u2+@Sg{pfK(Pn>{7>3}%vm(}?hxJODL#ADT>$L(P(SyE#q1Gxw{MBcsj?FSnYZbQ&5p-%wiL?j+fAoFo%2vXl&40?No20zNy`Rn#qMV7FfEp*s^!J7oWCGc0^DEmj^`*zkt{E z1?W$IGo%2T3=2Kk6nn3MUr_Ogq*O;E7qZ;d(~H<{V=+YI`BUDt2kd;JHDe7^f*|YB z?dR-hmIQey?OkIUN4ql1ShA6n)l=^RD^{Gm>+}de@1mMc{LQx7W~MCEjkm+cN*!Kc zyy9FrqChIVn{=H6D2A}Y$+-b$AP;)^HTG4) zD`4CO*Y-`KPoiajFEKoIZP?i`wMQElCPy6?Kk~ z5V5#bvJ4g^ZQ6@!IKcY`*iXM5d1f=5L`iG>$jvgr}WAivtZjSq=t^&Dg7=;HtD<=e7 z6;2@G1^9VzQNV?LX>Tsm>PyOfgW6SKh8mEdg4gteUJfk9gEeN^a;XC2u=;7%XV%|7 zHBLHvA%jGSUpW8gZM7a4xqKjSLXY2^0k8PeTOqq_cCVS_`Q7%U4RcReMVz>w4kf8Oa0Bh!4e^GC@OV z&=KpV%(*(fsM{VGR}F<#gZ2l0D$%JD5D!uLQ&`@3 z&jviv&;RzN*0>DFl(D_bLM@*%m?l>!yBU0*{#r&U`{cX^7=S>wq>x z*Kb5MQ08Em)7aDKo2LVkWA6e&y;kQf>;V;7h$6^&Uu#yc{NyVtZu8Eibzh9KjPn@jQ;Uz2JzP0V#nwQQ9lHAm->G+pI;|&PaQ!W1%oR?sPg}x5`zDae(cQDmxmjFN-M26=Z$KG z^U+U*Cq>z>SKla@FVOyBql?;E#<##1VCU?dUtA8!6G1yk$e$B? zT3Vf3SE-Y0Tqwcgc}OG^M?LRR#m*Y|kj}71J)AwuUOO={G0GA#sT|Ye=bZrErboSO zx}Q!{tU^xWj4b^rF#2(uuPQ*g4}G$hqizQlHqa<0dwB-)HED>wPSq(8PWgvla9SbD zKi-0yB{?OU zFXTJB6b7WE0JsU1X=}E@Yn{h4&OCtJ0Gs9two;^^d1esnDRD9n**yP44&ZHGg6wl4FMwg!bVll5l9?WW|{ z-!TzhQdyZ(iiJaGTE6iswa?4=UZDu$F*L8V*_bjhaD=(SYr4?moBx_TQA@wKlDg)& z)U$>yKJKf1ZmmDKK5vy3n2?!!^RY{Q!S@!e%B`ax75WbUkdgCF2ufo`1Yt%+42|S%`jBX0hn6dMn+94UbuZe7oH7Lp-4YBQ$(3 zvW->`)50}2S1-~mMGDmM%=r`1cG`Sm={*T}TN>#NU^it^g{0>0=r$SB-TbCu@Tm&H zt>UPOJ`i->WE!HaT>Ng6d48K-qF@)Bz6=;MBjRvtH@+M#Y-V48CcRIcx(2s+q}r8= zFMCbKpJ?zJZpaagY9UMo^E(hSd44x1LxBstDp1vgEeAwSn%Cz53z8p(cnNls8f>({ zeWpiL{kA#YoMf9?@t-7Gu?Gz%P@3x&V4c{8QFkDj+xmjMVIbz*NKsoi2lM zn(lmfv;_3WHb~@d1LKLkFp~=#9Tw^Y9gf}81^Xf3Nb{-``D(2!DM3vS*bL@4&5;WP zoG)_jw1dC>3+(46hnzjBH}uZW!?J~tWz%2ORj1RZReAfTOf^uYEA!58gsa&h6M zFb>iegifK@CJNgBpK1?jbqKB02}V;-EILF|qUf2TZZ^L-Q<}r}1eMR?6|OCs5}Qv*1@o=9 zY?84>D6{Rg@vTPE;VUtM#4{$x%o{F-2JKS30i$h!3DbcF|5~_phW+)*G+ax7nkbtO zMH>)f-LmpD^jbiG+8(PNh69hBMkxF0ylv=tHIF|w-9WB~C%*cZ3OT=}0PF_qdkfJ< zq7*FODE)dlf_TAd5=af{;h*R(HE~1q^gV8|y|=cgn1#l=CGnm~B5 zD(W5J%4J&)ChgWd8P0tSvjh;7-a42C8iw@~{ zL_<2l7ox#hmZRU=p&Cp;8B>2ir)N+rL#_Gyj=W(@sK`_Ck_~YAmp~eo4G)F=g|!+y{W0c+f9bT zxoe$LJmef;a#{1S{4Oyd-2>aIvX}MRk1`b)2L=4whEJXxOoQZ6I0y(vR{S{oHxvE? z({l4W7~%_yq~oAWP&v52hg(Xhc9$yAuh_%(@CJ$pA)1O$4%ecwP;U3ejRJJKOBoHA zh5AwLj%u+*?1dY&MjFw$@{}_%g^){E*JJ3UntfO0)g0 zLq>Mw#-iqv#mklhU_(bAG+X(ksuzvkPj5HL3pc6urLiq2&H1lowQIptS+B>)yU!mz z>e<3u|LQ;e8W04wk9h2NbHG^K-PX>#SHS7~U^|cBEOXRrc#@8`Wuv!KBG~9u>oP^j zCGSct|8n+Xz~c;gw~h{!i?ee_zCTD0w8yZCb##zflc$4AB|01UxmAp8DtM925+BYN zM4Hi>-j`L#te0o|URB;`es<4@_0RB$o(w9~Chd@e}F4 zH7T)xe=wc(5lOT)*ztsirUXWti9+^@tUzOkUfC$88N9pcFN*Q^5uJB19MR%!v$ zP=L8^9zi-ouG>TYCS-bW^fHAIhr#Q};ay?p5;Q2H9PS3tmA?e{QLg6h&>}#Wf!X)f z?_k-AXpEbuYqd&iT3j@FkQs&bjK4d@r_3L!=S3{4ogz6w?*o^3`^4Uhl&YN_4I$bu zPz=D|AMnCsN?60-;cDawFMxxd)o)Yd$6xLc19aVs4Dvdnre=G%3m@-Kbt+#7`!IiL zOxayiwp=9BCCT6AeZMRiDlx|peOP>Q6NhS&P0D#V$naY(Yy_gxX_^^?Kp$ed&2&Et z>LIG5ayTH@=9IB?9Gqv{gCc4c*G(+rkW5N9miX0#P<;3y&Go#m}I=ZazuPt+=&7 zcJWndRCU`04@k9YAgi|$m-TXh=g!{|p_(L@T_9qicBK^kVyMn4!Ss7< z&zd=Qnb#8GIXH>HNt1ukU;}R{#rJx>Sd9P~8^t>;rmnr}59x-|!yq=*{yVNOrs5v&@R__|Cks4F!^4f0{H&u_pQ>&B58Q|7+O)2EZ4vSbtsPpS z_~NdGeiYL*s`c~ZwZm@k7|yC_p>>HZLM9Py`9zXO6KNDKM+vJJKk~s7*7xy`SqVeR zC0qVrmkZ|Hf&rQ_?ph`UWv`xm*@t6h+Wh_;IbaJS=P3MM?n^MfE@W&hpQjSY+xr=J z18Jl(6!4ZkeMBq?Y<~)SUl?T4)GPru);tJ57H%LtV!1Z;uw?k|KD!Hf@{RXerSGyk zFYP;)B9=jRAESU3FeoA`RPzk)=S`GcySblCQAk%!GD7-sg- z{SFHk5zWr385nqBD@uF)H{v_*u*|ckMe0uzNAtauZ{JcMUZ`Tv-h5JKCh|Nx<>~qk zgE>081o`ILS5fh!S6#<3S%yh0e!U{>@8rbSu-h@pX~WOpfr(?oyjTmHtSS?lMa)(1 zYLME+B7`Re;czW0H&4mji7U@`&cyfdLw+q;Oj5;B@*IY*nEoxEKrVIY<$gwVi7D&` zSpc;Lc`Fe2Lo?s00>!bVqghd=We9?7 zMu#CiFhbC2??8uSX=8z*n3KQX-2S}YCK@>S>VX3+kj(0 zDfBj9gsGxF^f0PWK%O5&*eV5JSqQHy)WsRt)i2+qF_go5)$?k6#b)`qs^=vum#!XURaAJ6AQ4wROO_Rp%hkx9j$))#r zWuABSLn)8S8^I>Lsb3fua1C*R{Z8h@Rw11Zu4z|Eqqn5B(P!ozSc&0Fh*NPcG zv_AUT7s3dZ%FyugWwYgh>blEqYXGL>;CM$W(~8Z7`*Y7t>7!d;=lPkuJ8drIUi}5Q zsAw7%pxb}fNQfELF+esOZhxf(kw_8-6@$JPFw0Q&`Olp*QEaK)Oj6$ zrP4hU>|*@vbd8c5xE;%jjC>A3)WF1Nj?!W3eBd&BGIDZlzP7TUM+tnk^}z6#iaJu4 z^OS9ru{t@IRm13?B#vNj&}>bO@yZ3^q}5p&&1{3D5@n27p10-!%YNB%KwWU!03Lt8 zcoW|@uKX9IGcHJP7pz29KGSg@jgdN0ck>bet$|+6Eq-Bod48z$5lPd>oDKopYq!Is z8OZZ@R_{>6lu7)wU1=#I8~gs~nXzZ+jNNZa)zPQ$Icp0!hdaMM5P5R=g;1 zBQoV*kpi0wto$$WVYykd+c9`(bECQ>$@d>NsGFJ^=SE`#>(Ez}~+IiAshf#vbwR)s$*XWn?RJL+esjN6%Du ziU^%I_vVndal2eoJ$jyF^-zi4YhZT_0spP7 ziP?eT)ZzqHTV@RQfL#y3>T~!FpTlQY+>g$Jm7krQ6-*}xaoaVEkK|?Dj^VXgnK~ra zOhks=qbEp)@V`+tY7T6@&bP+UmgcG67UgovwA_5wLCw^JPrJ06a@8=wYg*{&UI&~&M7oTE|4jcsIs!-|EA7h7vz<5Y53G;+iX0cS9tc-%W zn$y2!2x{lFt;(lcSzd0OyNcNo6F^V-5Bc+?X05WR8)NX#PKiV;qLoOskDR^`G0731 z9}|Xb=s)C4T&dd1O*&KIx=sjs5_BR8^@m+17gYqmWKo|7d zo-CK~;-1Zp73^fpjo>sv+Vz;sVK`O#IfW+L1g#82mcy-?$RK^41FbzU|gH>HO=~iKI5%Gzr zbuusdbD^$?0_=y^No>eGxOMn$uQP_veAEss&rl!giK+S?vN;N)N+9Gpsy`9IU|ymLsw-@3-AaXful;@t{P ziWI;`ba5)}n&}bpxc8>JHTles12MecE!;}|X|0>>fwv=h!Gvx0Sf2(F50_-EpHj$D~TIc=3JTj%(3&9 zHnFHFdnNly}I^>W;? zd4I!SgEP-cOGfD0dDRb}6L8y-nseoW&p{It>9~WP+*+o5O_el~X13nCON3pHHvIJb zOl`@spBV{=rriK0GGZyMqc0{HVoIj7;Gfajs#cSKfv-&BtS(>NDBSgWe)nVGk$~%j zfu5>Aw}DlYPwP{Jo&bRj-v=rdvTZlG1TF`A{Uv@ve1RMOiE5!$cYScY8{F6jZ=r;L zPyXL8T!K1k!18A>)aW2BoysoQW12OtUQEkUfrYZ4MG1^|ij26$s6B^#ZQvp$XkM7z zxYj|qC922>1+<&-v?{Q=5rkFnP-~gG(6f7?`Oq)0>k63lASyMT81N5aN6MgrA4G9m z`;U*iQ+%1%8uwcVTM0de+G9+w0L%^QGS@Pl7q->fL@kFEytk7+kZ%6zBL(YiJAlPQ zu3=qhD3SuGJzFWdF+(hCXoX|+77FV;lmMY^a^;hBGN%5YbcA^{KHRGuac!i)aTNfG zFkTrH!^Ez(#f|J~_W_BZ(Ei9M~>x4P4**g%Pdqg$1vIq;T%s?S{FfhXNQ z3QjKAH~x@HQs*g~GlhUBW+5S~f}?Zh-Q_lE_v4=E?)XEH#bdKgsULhBPiCmBe}|5r zRzn@kcM*|R)c{lMKAF1TtCL9#EQFt*-mvX4b)Jmr6lL}P%?xE4g73q1zEuck5?czD z$`i=?u4h5Rr&QaApdbT9w~9UTUvrME2}fl93ZA=J|ACj!%Zf)fvQ5{r`t??RpcUq% z7jw6yka#THT7WY8i5Vijy_06$d9s9FXKK0GF1knjZsO`B7#*N}YNX$@MR``W!^ zMKIO08s)$N>N!d5pH`*S=+}XDu~niI#YO8qD$@W|@z35pg`apAu#KsCz!J?NnbvxS z*CI!T&y!g{n@}zypW5hJ-}_5l<}Tf!0% zj@?FLo#Ld+RB+N6U3?+ruNjdNph-Xo{d4(8NCoFAgB^`9_v=74Eh6;UAsJIFCAxV= zkLSR8ulW}C2hdeA;nNNza)w5<9amj1zZ(FGL+}69d=1V^r&&BNrD!)Dg+DN}L1r5U zt-PYpAP-TYqIU|_2b9^+Qce`c+Y0-D`Pq}ky?P4ZA{S*e(DVdQRFnhq84BhlA{6@< z!R_SKS*H4LSCv2B1x1LUjy}Znh)FDQO>Gc!z+LX7hM_gJRslk^#RdM&d}qR~-kWUZ zhG|%PD*~^(p?liFn3;TFfR#*}s0U03ewM%Oa^<@cAS!WJR+aL{LgL0*ALh+g*$*`V z%!gEEAJgZ*J{G-kDNB2wD6OJzs;nfS)E7a3U6TnRL`1MUvAi$T0oU#F0@(D#jz`bC zUcaNEL0ur*>l(?sLUVOpNT@I^u|iG2TCdlkT0LFxq3}0C6+a(tBVbOqkzSXUKMNi| zr5I)Vp)ZLi>xze!FGLifvPHQ3+Kj>wXgpB|)&md{G#bc(=HTNPJeG+571=&~Pdr9E zOnTvNEdP5_31;pG_U1VPvWy;nl^oaVGhE?EfE{aPWQHJJ>3qM-IU7#YGv-^tq{pGqhB-P#4z zAj)rAlb_Fi>)6148KcnKfk;qlvH=Ydthq^@_YK(Ng7U&Sf%u%(7(AoaN%|$Xh_XWF z^py}B!J|J8In5i6l(}UQj-iW4hKFvIm`MPx$qLxBc&D|p4$VQAkNWhs9R~#}R)JPk z#{~$08;Mco0dGI7x(ANs(6-M??itE{7ui#X&Gxo$xC?7>&^u6Gylp8#pm@LY{@HNmjKkgv z%&yxz#4If9@AK}jEo2?_CU;+ z75`bora+wqHOF|kQ9EM=MK&x$dS;Dk;~pzp0q+yYKbf#NWKO5BO~azZ zg4mv}EC+k&gAc)tXd|v#R^5X2?PT)>NntIzrY#vivKn{#+ir)zk#GnOflikikfryg zq7ga(tYx2<8eA=yIsgiJbqUf>_>f8=lvbOOC*=;O^z(``>Q9GMzF!Y{?c6}Zba|bR z(OSdnS~%a|WLQ0XZgm0QGCkZ)+?y!R{yp}G^#LA|bzaNzVaAIlyj6K`^vm|fE97Ll z`9ky>wYRdasrgEWlrkZo%AVgz>%lXyy_WJ!tMom9|A-%WXM||{SBVKAF9wVz5Mc`lL{b*h)P}R&%!TZ z8Pn9XX8qpzdpHrUv#Uy?F5f30sN7)4RI-sQD){?p2ZN=G7O{T@m8jS(;f4w}=9%YY ztSCBj4kac5vM_+2x~f(<`-)P#A~Xo@x-C2vcp76`mNdv8QC*IB(W)8wU}U#0wLe+n zW{t#)1m3y&hZJx*32FVM3Zdn1ALsi@|4F^Tfa-W;oRdm&l{mzBGhsA<18{vC9v=WF zHvFN#HZlBe%b~X_pBk8K$Pt)vn%#uF;5EMl(>R_y0Oyq8i@j?CcG1t+gqWGxbP=za z*~TZ&PTiEh>sP3i@crf=CRIlDmMbsNSxG)C{dK$f(ku=UO%X%z1pU3vPd^t$T>Q=q z0!iMs-c9O$w%+?kmOJy!Uv=sDLnw^Rtf>)RD5j!E(8q^HMAwG5==*X_*WXD2bGPzC zn1u(`F0%!^T($Xfs^m$cZ(%JBc_lpNus3{snEEPQdkMJsp{KN;7r75!P{)>S>u;oW z1#WK-Njh6hAQ)UOl7Cc6LzM1Hk-_?#mNul_(Db-)SFt(cr8fpUq*EO zf|*)=x7y{xBqXLUMn|;#pSq;Fp93M<19c1<-&8ie8YyfGMr2+%IzK62IAcBxu%QGt z?VdNMd`U?yKisW0SJ>%$e$lBRz@PzqtIk(Ed5ek#-vXeiWFLH?koAzeXCaI7jaZU} z?3tg39h;`NXz>TIe~Exc`EiQ?x#tB;(xWlb2wYKJsw#=_iw!$vS1tA0R1Mwvdxz@N zJZhz0VkTc2)aoqBp~oO!6Vy%}J!ahM-(VGR6EEk6iUkQfpg}DzP5w^fxjaJFJ!ZZD zGA(eK+3E}Q{!)BgLw0rvBDI_N^aHM?c0F+6)xgLw8PDtV!Q3a!FVu1bLQ+fT5d?=l zt)tYel~3gvb4(Mu%vz4BcSt{Q*tvg!kflRpqf~E#$pxK@;=aidF)jp0biq?uA%g~HY(lewJ2xX*+F%~lQoq2c zCK=}|QUN^VCM>FTtwoS9!NyNi465rsTIexMtz(}~0h@B=? zhzBMPK68st5j2P?hri6a!plzsfG>}}f+0=W+Gdqp6w|jI=|&}%33U9^@b1Q}VBcXS zGKh-kDmjbk?+euC9s5rt$q_eq_Xj1(Lz6HJ#r9#@Fg3oALUUrv8H}PgTXkW` zovbz7U%j-d>OJ=hV_f2yE^#!!Kg|qvEri>gR5D@ZZ_$CObi$G+yW9{Z*xl%}%%HTD z<@sOCy>~p-@&Et*KF6_lX0~Kympu;>WoPfzl0+28K6asq>@u>-CbBuUQizNqdlSbz zw(E8Je6H{HyTMSd=^RwZ?&ZKlF>Q}k=9TE z#Hx-y`Gq%Pc9(@Ne_s3P{PBZ4%|zd3ah(z(w_x7qqcN?VEaJ4_Nn_F0)N#Y@e(y;|Ky#4;;j}3E2vpD~fPgswZk7Z^0t0(HuQ8jT?a%{fMveyGy~3^8KRWTIuU+osbv%pTZ8-!HpD7YP z9xLk=PMZCRWGhOcM3lAO9nWWwm!ObhhQN)wr8ZRpGR`&!MG4mgLo1VpwauKx5?@ud zQKn{PY<9!|kwbJ_=9qV+^2sg7`5ZS$iDG##v*}XG-kq0gTR-NA-JKs1VNh9Q4kHwF z`|)LnK;r-ciE5MZm%}qGEzW8T z8@tlBx3^giIUeS^m0J?=OBPkLKu4!{G=r;>OJ6j93TZ!e%lRvE=JLy~z*V(Or`S-q zr@Lp!0Sbc^Afp_Ku#Ty$;;nv;sVqLqU9&2?18rH2CmdgLzQ5+EPpyppEdnE?9U&Xy z0RHeHeHGmtNywuS2p5=br&_4(_a6o#(_%I_XkM5LCRsaXV1@69L~A?m=|9sQYY=Q+J+#yvt}93^JO_2==D-%Qg% z#;(E;+x<^zu=2lfgR&tw^fY~p{P0xkNQTHi+;PD%IITD@z-ec(KA`S*{`YkCqLM#h zf6IQ=J~=-EvV}4mvueBjp0O6k&abEaN;UJtpnJMH9Lec3kH*Ym0KSZq#tmC-^|)z4 zDwSr(JNWCATiwEv`wygRK2i5RmB1d!@*^ z=G*R`tVZlkXgPn3z0M?R&D_%jZNJ#huqB4L^S_LE#@_b(Y1q1vmQYi_zL?HwxJ=M=uKN@glX z*Au)httwz!6YKBYE{>V{vuP28OG_o6gLVIcpnw!_>BGVM-|?W}vcG%!3%c7Nhal60+4)@hBw%(^+2 zbP!FvN#+(eLie}5J;#xQb21Hm9jbe6?Xdg=E@9gqov?5ZLos*wv!WVxA1T(zvE(j1 zJ;w}wk)vX$W=iyy?2YER(GBar@~68)H6F33l9)#RP0iut)F4SEYIgZugYoEb+>%*f z1&z85Bu9mY*6tpj9Vrt|TG!WFc-^%1?2ok=$;Yk2 z#EYE3icQCH5X+g9SVZ>+ zvK-*|tk7A;CajkW_Qm$ccu(#$+)V0wVHS=fE{3=>*jnZ&;ny4G?Mc={lSH#Ym=*q|0@uSGrQq)WBF`*$x(ec^%hHTrrn{^ENoe< zTvPt{7u@5Mo7`ud55DbvMTw@*q|EF?crx*ssry*L5=7@&E58YOv>!EXzK6L~#^m`j zrS%>3zON-ijQaY*#KKl?)Ucf`ThAjc4>!ksctYa0Zd>Y2Ih9WYG=Lmg^HS_CPsU*3GNe}wumez?H zmDOxFX7Jdrwm0`peX-3*SwfYhhHw$FmnQ!3Ob`C3gD@Y5CAuaoQkg5Xh zQOh2onrsg8UL^=EZzRM!qY61Z$C^|`eBbo9 zl*uzqe}600aJBB?GxdPeQi8Mh0ZETtC(OrdfnUzWNo}SF7{3gZT7`}%KU^x#OALD% zCLlWP3d?x^{_!(mDJS5Hq(QzgGZlN;{W9LqPuJt|Is7>$W(E0ik+6e&>&CC1 z%TI3jhEF~}k75+`pFUlf1^LRzt-pvf*~?+BtUZTjBi zuOhpO6vozMed9)m;x<|)n!MIWQUe)ek6oot;p(?r#}Uxe7V1)qjnLkfg4!$PHP!bV zzCD}3{k(NDV^sJ<4z;>+|MT6oy*iFex7|N^et#>vGe1OsC2npK@OVZ75N?sNh!CEU zV$15NQ~9=m_|Ng$qIJ0}SSV|!&yPF|USRydpC)2{`R6rbfh){8f2W_kZLhNqb$Ozz zsEp~Zw=HZe{@eM_dv@1Po24GGbyy#?@(|(QAqMnT(Zxz?rBl!0Pq(RgdUbo|ti-kz z?Qp+(&ir~@Lp{$APZIq|SoZ2p1674HH?i&n;;F6&(MypsO9ZiRf9sI)xFvlNdAM_V z^D}S^p-6lWkyCVt5KH!alqzkg*9GYd#Ik5&2zH4Oc=O`k3#g(fYOxf-%p7mkB;Gxt z=$>7fF@YOn$m~0T9E@dyWBZ7RLN;a7sa7zYK1w)sy88~3VSr~Uy<=f)d^N`Q)qtCe z#IJ`L%qyH~(vwq3C04jPajPpc9Ocp1kXktvejGFIL?j$nVq#cT1W6GeM=F2OG(F|z zRUf^zvqP>bbm90Yelw?DM}@mY^hpLMde*OCcA_Ky8tGar5!4lC#GuQl2m0Np$5b8I zBgon`!h2bbM(k?-O_m)lS&1)E6lsa1BCwbAEG!331^i`sl+Hc90hNQ-k54}A-w1Dl zXbhy1PtGS^7E!rKD%25hcs+R~UqhNgxF=5ZxFRt%6) zk%H+88GS*2|K{EfOl#_r?#SaDsS<@cWli3OkFW>8%CqvplFg7xU5grv~UH{0ZDTMsFkA3er`M1v&EX-F$qe95#q>wy8^9`1CP@Pz#a-BZk z5+bn21&S#3D$VOvCaUkSjVP_h6#Q~LiR0t+zhvTLpOW12F)IH znn=huL`#VzyI2RO8TvQ~?1DtgRd;9+K0eCAjrzju5Rck}tTgY|oDxRWr{?+B7(ZTe zjJYkhsARWHVJI^9tmH&KRzaiqNWil+u?a|YpP8)Te4B!N(lZIZjaZjMEUCZY(D0m5 z8QGkK{r`eN+s9+jcnTX1AsYLIiyNO$X=KUJeaIobzMc>9&lgPJRh{=+L|d(gk9PJ| zm&@-+>&n5fEB7=0hZ^lfyvM#n2RN} z{njp#2=k=h*`Xt-^gDsLZ>_UxGxT%k5>eSbu>{zF+jCQWaZxs+IC|X)&j)v(wBGc* z=Kh@tt&IG6vU+9G+W1-G*N@6wjZXfbXlf53^HoYF5#7^QiEH!oicrtyLSOY$eD$F(P0&~%@@#}$^{%HIBQP= zKq7)1QLdKEFRnEzw@p6{r!DsaAv`Y+bHI4<2w@>c@lE`<750E6 ze2$0*(nS6?jpmvNO`}d)Pi{*EsY=zVP?uRuuzh_i={#vLC#%pCe@`fGblXJQelL={ zgFrlxxsCSCKcF0$YQ%@z3wcwq;>F_ zm(q0@$()4D_*f~>pzSwQps)Xx=r-j zH9+$Vc_+y)&=oQ7W^6VpDQZz)oWbUBcye4wU9-}pm1*@%=rd!J{?T#I&hXEllG|>J z=rY4%qlV(U;H=3zZavL=ZjoM9|FgwGwV@IGj6xwd>M}3ba5LO18T{IL>-M>6q%-as zaJ2nzz=8W;Pa;3yc%LDP;YH#L6Ul_c{K=fGe_lX1mbc?;XGr^_yT}?Fa^NnzctF;1 z%SNDNa>CvIt2*mygLOl0{l^lsU)z?zG>_sEb_R|$oyd!UH^T=Sd7t?O$!@>+y5C8j z%t=M?JbH9=)3ZsHJX)7;$RT{HROzJ~#FO>&g0%11Z&xZr??1$vC)t3&iM*bFJ{e7Q z?i1>X=-c#^eC&>V+Jbe1P+o!_9LbRu>jJ{a;if1tJZ?;WY7dG25=x+hZ zTGMMF`}f;y8kON(bZSqUB%Of&2E(AoeO#Y>c}X=s#dY??aCI z%7=F_QXU^k3N}7f*wB5wbJb$gt`D;QbhkgaAj9_xu=%ZxicPcBH5N?XZ{P#2$jZHD zIRZQcu9bjVF-`{9n>X0p@kW(952GQ(Qy&5vpz^z!s>28;5L26eYHDT0s(3IR8dkqj zyZ{yisXB-7IMZZr2N7z(`ndCOkVe+eMFNZu0P_fjF0g*#qpRF2CncEvK>|Ji<^ntt z#mC<`#tvk>>G0XtfDV;C$X{t~r&eJrwr!H1e)90$qf~Q4ZJkTbQ)jtB{~^BpysQpe z?0d&L;oeRe8Plz=xP=ICm478DXqrZrN}k5#b(oGn-hv_{*%;u$nSzv^S3bY1U6QZm z)&2^`ZLSmxqVb?t9ds6nz8DdDmEj@E`e(<0XX`%atJ4-{&sV)vF?h5bJt0P|;$E;F z+pZ%Lm+_`arfWAv(msax{ePKo7Z#-%PCU>VAsYd&{sV>6pW2mr1y;Xh)Sa@-%|HkG z?`z=YZ`kY*P&Li%*VG5}$`S*9Ed=#W9m)dxcCP|@WHz6@T2k!8WgkpxG0Q{rbj(Vq zDlpbpMl1spUUT~nK@_ma!^#YWeVC?qvX=-p+Z_oVIcbkSjCJ4kdi3?x(9}QM5xK10 z%reZ-;edlPOcVkv-D6+Hvx7OG_E^ul2yn&^1Lm^P?>P6R2U(X@#Tf&Y4;1w`nY%v; zx!V&@D~o!CjPZBq(JRPxq2A4v&&e^#dMFXTCvL|b+s*)tA)7rJ<`MEhYvI+6duI#e z$i4*@aBUy=RLeu0yC3)I7pqGmJmH+sZGHW0;$k!-q;RviX{?|_S!Dko>Fq2wX#I^W z5bQV9cI`CwffOm9x75t+TwsxH2|e-&q7K`1e)LF@1QX3^o*{y0u&t?LDU;w7i^DU{ z@Wts7V$6l@fWTHBc*u6-$@Wvuz*P(94@J|SjZLv~z%Z4m16%ieCx1HA-PzemKt#;_28t>la61H0`Kw(JiyMxQZf$&Jp@jxGt4ve zC8RO4e8!CCPa$KPZ8| z&`lA+)2J-=^I5Cr*l*ixT%^j$S;19qpWi^>EBz)ZiD=Z;&((eByA1KQ`+XF6xZ?ob zJ1_^1g((G{oeUhDZZvFCZ8YxmP4ADpQRnMta!jD+UUIYX>!g&sEe~Yx_(fwcurp_b znSJ0gjxEp5<_AOLMJdEUZKlR=VF zM>MKZH>wD;Kl}>#Lj^Q3@R=VJ;C-Ji;LDyHWz|643Com{l7gHW<)pzWjbFJ;ueA4b zGw6O#95?R5B3!w#33$oi#WcQn7nb@%P*{)HQgDZRp$DWg=A`zf#W@JBLs6sVhK97h zZ^FKe+IX`cw!MQ-Z+NKKA$rh0L;1Qh#~Dx0CUTMDA8BGpU&Km-0WklAWB20s^k%`s zPszcQ86#lGA`Bb$9_X4y1)KwSxFI#ECJx4}X52P>Kf{NQt$`3t@4HP`OB(n2xmlfs zZBZv3qSTs6pL)*Qr};+OCm9cAjt>~`BFH@wPK%7_z95gsc7^?-Hz1;9A^FfkkpdIb z$V}*a*R)E5@S~%OKK!?uBT}gu<91l`CbSk4vu$L~qK%qNC6iDgz5_~V+iP1#0%;I8 zUmuM;fhdz{Sc4+*#7LQ)nC--G@fvR+aJu}hSuRD|wJrIZ> z^dUyDi(v%_!5oA=rVh_!RNx3i{h+RS^wH14{yp|MWK1xWnA#TItw9nsT65>i1^ZkM zPWLuH0&ArHHaeDUI-l_L7)K3UCocGu-Dx0Mq3TmPZ=4Px?^C(6^v?V7K_e=u6<7x; zBB+|K*8csR8rFn1Qf#3_#fE(j>uh9dAy~ra47kYo^CM;whg$EAQX1rR$U|sd_S%m`JdDwPQW!wSXu5phPB`nunHr= zoQcC_z>csS2vRpSw}PLQS(flpfC5hxhTLH0Z>Q>Jymr)(g)jx|3dh3Fk4BewZP+8m z(E6$rG=gygi0)UJyNe>;B8u+a1p4eLxaD}W%>c)RB5LIZuk-hJS@%`y{pZfQ%^v?g z?^qQ%&a`a^I3HU3ycv}La7nqkb8RjC4;2r%1xEGnBgZ84ihB8P%YpPylVAhENqPsd zI;m;?L*-z*0OsurfqfYP3Wo}cEs_JC+}=0cSMr=+jyrXXt#>sLSw%tfyE&>~sZusE5apccB@ z*keWPaj)IkM0j;w%^xo-bJx3h&wT>{%#oZ9@Zh?HJe!oKhCCr}XZK%sA0`t995>%p z92u3>>)%pRA7#aR+HzHovdgtXmGj{Yyeh6B+@c0XbBkqc1-Z}2R-7qxEgvQw^A#pXc@nh9BvwQJp4G;rWa@(LK>j>n!@0aq8Qfno zf7+qoD92hS?W&Xw(UPl#GXf1i@p?xo$kpsQ-!52^FGS9cf?b=d``%S8oDuk^A zbh#!GrfeID!#rVs>F+&=JuIl2{i(&;Uq90xS#t5(J)Jzonp-0x{DdJwgz*4{r;P}4 zBS@6=HM@m6A!?^S_yZj8RXJSGk##V*m#!`Vs^ z2IhQU*Av&wZff}bc}~kSGEE44YF9{OotlsqChvzFupu}AP0s(kdCE>ldlaa#t^m&s zFD|yvf@OIwAN2 z%2CJb^?R~?@vcq90;1yOxjDtGu4iaxd6I{EaR%n4K~Cl$`8Y{xzgP@@(sH3V)M-Qp zZ~I=HMS~-|TZ$R9pl*XHQ-re5 z^2K+khZMS3HI>*V7dGdi&$=!XN#bR%8KzJU1wo<3tRR1GfwZHp>K*ml}rl$?V65-_d!a;(w&HXG98cPFk2xJaDY zhsn$_>bq^dMe}F#u__CvQU-g8j479~JiFgXR2rolWz@!n%GfTclPAS#zIldG!ehAW z86$eotM|4jY|;hYt%Wk!uov{dhi9#k)(<<_NVN^hvE-$S=GbzHC>npdxYOD6w~buU zoaQ2jZ*ZWf!3aayV4LQfEl|MJ1Nn0Kp9zpP+kS5sK`yn>l~u1i2>n&8_v=$-&P0<& z6*5Z0?6Sp^pF5t@-glY3-~(XQL(o_Z`*0&iN+}?0fDBaQ1!J!J1XEa}W?zssB99oh z;DVFl)e&t}*$07gGQ03o>nzi-PNNdyrxFp7^`rQO>+L+jzz0)PTY3w)pslkGPd{^( zaA(2nGAeh2z00`B`-vmRLm|X<@?3dXx?1QqQ8K)t5t7~9X!tcvqCXf6l2c2lTCUMA z+FFR#+|%XFZlJQi>(A)U-$J4&Q(uRy5i>~oQ9|f$!8}?d{|&$4`dA{O%_g)z!;j|` z8ME>oR?SpSxP%!$30h%LQBGjFbmUrrm=RO*oL{TjjOET>SilYJByX2nZ*JNzlgB_8 z38_5-F=PifQ7?$QUBG^nKr+6J9ARp0H`)E2_=ayVChfI3#>(uuxO}uDNjURljV)9k zy!as1&GqU0o^-eL!od&?t+LGVtqtaNYd8sFTJcjl$L506zMV9&I+nB+$|#LXp(pqOCY;6V}R1NL_O_ zFW-bDiu|hD^bCISC7%o);(b?;{v6qI^_%^rP=XI^GihVyg1Nt+4-<^kd(=&D=zt7Y8u42g~0{LgCNkF#b(9lJnJwu?8z~ZbNNnzc8 z=g*hhok%YZ+wE`J$4Aj?o?w~EwA-hQH>fw=#Ip;1gUFDWto^3*59nmQfDA;~I12v=+ce=ay<++!CNt7c z^xMiQBncI{}$D0I1G=IHk#Y%wq;Ep81|-4nbolG{OrY9#Ei#0JGwJ zbpE?QTJETR?4x5#7F^M9|Ix_z!5Ymz9kU<<=tb|j^I9~cN*X!Znh689M5jwoYjKCY zC)OSF=L41Ou*0xpkCQ%QjsfRLU_&zD4#Q}2gP!%ZFFPMIH8ODKX_Z}X5<5_7&)DWr zn(PNk?bX_{{NJT~nq(L1Oi{L0^Lsw)u+lN58~uCzz0*I4j+&Lf?Rui7Dgra#j(<_< zmMc^2ruk#(*t0j&x^iTxW}lI;pUgpq1T>+WHPEGfAq!H~C+JJC8AqUw|>ema-G|KRY||xYGKY=A{gCpo-)J0^~N@;W! zwB{>ixegw~1hlbtLMXi2xs9N&|KGL<8WR3n+Wh<-Ji0wC`i>_Dq@<)jR!0A0ikP~o z)|}7*77=vYO_%4-=+q;VE?<7D|6DUYZc~8!T$*(24Y>zl)>0yafc;yQjd}rnvFHhN zLHB6vhb!+(*$FULRUQo=>v_5t%L-C13Wd=hGA z>vc^Ix22k0%hi+0lh(`>_%x|w_{Y|QDf(lTiYdu(;$?U_B??XdbQQmIFQ>Vb<<9y( z0OEMbT^`}ca^VOneqMKj4H81J1w3ASn-WCHX?b?Ig~mhd%2xdDpMC4uL52S8hx1tN zL&aHRs(rB&jKlUegqK=Wvdg~cGj5h8Eva09VZz4T7O2k&Z`eyn@iwq!!OKmSAee7G z8&|g=^vtFlh85Y&r6rLc=p{nG1j_H1x(LjyG#Rq7HP<)Fq9=;O5te8NMe;w_`~cCT z!91BV%3HkftV{1Z5orH$L2vdGpBb7=S5{h$KVmRXYnUso6zIsV-TD*d4Hs_!0+v3MjT&4awFgt58F9ZP#ia)!P+DhtrMy}V~VO7IX4UCFBR8mv3-QiROovq^5hIZ`5} z`6L{IwN~2{(`BgrsW!8CG2}>{`;yzzq669zi7pkKeL5pc}h7<)#Zuq`9b=>npz3XmHCPGwk(UP_tyubC) z`|*enkECR*)t{M(pj2P9ME6mqor+mOO21J);cH<)BJz79Kjp0Y%{%AivaT3`{ZlFf zX}Pqnu`ALvZQ5;C9yc6dZ-o6|005**g1j$M-l(tV68zU zrVCZMtBiDa`vi4^uI47!Pp8>JjLON(N2YRRugONAZ{Lk#Uof&r(`}P;JGa^I!QVNH z>mrb)$v!MP*XgQ;;O+Mv2Blb=dQYYC$-(IlLTTf)S--tDAe4qK)x;X8+xHe-gq)56 ziAOl-#Lt`(UJk$>17n@~urE%+LcKIZMNG0z|>loVQsbq>#Aw7N2i`lMNrP4 zPhLTbuO}MVDYn>v+JWs~tCs_zV`ApVkaL(c^!YpCNnowTy_sB?_}nd;3Ki0xjQ1*f z{B02BH0-)0#jAAE4|PblOOhsIjWfiGq;_?-UUb=ju$H42Mzt9p@g#UW{i<{j-l9W&-^@Rh z6@xsep;Ry#2RY?_Pzni_Dp8^Llss9uHZa@X;Zj$23ZhabgdGA(N(sGWw3EHEM}uvo z^V50o`wvrc%(o%?jx(;2RB>jl^7^p1sivV@v)JPIO-Rac9&QqpdBR&G0SfWp%nh=o zHb7W(4cKaG0WnLJLTtl!7gy_Ta76iekxvf%BO*Y2mR|zeBS$Yh=~z*c!E(~iB?&@Q zjei5}L`#0C;=Jb`N0zx}#SnsQ;Su{6#`r1>O(6V|+RaAfpNpa_AptyGvd`N{t{sd` zkkEKEnG@7w2uYUe#l+WwGX3Vtls7veO$%IyTu1acWSM9M zUj$9uBw>=G{p}Kd_;SpyWjj$UQ-3)5Su5&M*sE1S00Yl}hc!scQib#_E@#V-kZfTD z)a);CIMsli@7%Kr)~zGiSRBt16S<1cjQ7$z&z{qnrb1lzJuP|_W;C+*n>H~3#*lLv zqj@O}5@6^m3KRTx=Cdj3t1-Ik>&f(L9ewm!iWJY6Nv-QVczuvDc~35Ei|{cmN3=Q1 z+#D^rxygHD70q2wXs5M_ zRn+WYdvlK#U)|^P=>Ty6R}ivX$=Llp5C!=7$3BzH$;)w?7p@Wk2NewY)ve(21gV*) zXml06QCG3wSf{AifJglOMt>*BMp2NA-qIK&6cd`YMtOyVA%co#m>xw26!MXc%~}UH z1%wWp!r?7a+1`ceS#+gF2lju;&^jACZ*A~;$AnCPK;pGM?yaq|a=g8c9k&GyT_Zm7 z&{MCJe!1c4%;%?ifRP@s*>IXGfMs!&0DcbvAwd$63VOsSapMhHiWo=aGKPj6w#3@M zCc&1;=%2Pj@RsU?*(Z;XH7(bV1^Z zyh|A1kMlv*EIQ05hBi!Kd5DE8ym87IOxcXeVE*sh{Lj zXsrXG$<)tHgHHd7Gnjh(3D~Zq9=jFySq#_^{vW)SJ$#LHgP#+H`{O5X zb~k5e;lh{&W`o{GY-^#^g19r|AzQZ;WbOHVI4HRv>wWR>d76LzXm0Bv=Dh|rd)z9vXmIGD5V>RG{AN^UUk{1Q2Iy%j)G^)Bc+) z-WiYQ#XNfYGy2?cF0YmXlU9%LLe|2&{d@0%cG* znHq-d4q8YYafd=MPDyTK=jEDK;1(%5vOx0*fg5$);Go)hcG!eMtuh8)zdL96j!*bL zDSMHYZ6Zet(KJ5?=kS!Tb9rgqz+tWr736aHX5pCwvz=ipsmU6aEM20MM9)GvLjOthg>!vB*T?o5r zR>me1=;*OKR%A-aDOd0D;0soYfQZs!6c=@7!er;M5;4hSYwWq^Lez4*|dw3!Eivz9Bc;$y`+?Cxa++$=Na|mv#Ql-5jRyZ+aJo#(hFpdlkANlQP#mihjd3E@3TD3w#GH1MrGej3>>;G{@qRhT3J#lMP z>4vMM>d}?rh8Z~P=f6Aq{V?GqYhi@&Z7uERgv>t8=Fg=91lZKDg`|1h2+bH4JWNE3 zDFh6LMWRqNy^!=xl17Wqy~R%9gjiZP8fch6Z95c6rcY5u6HAGn`G*l{P3L)&7ay~w zAg6>T#iIEgq&795>l%H6q7KpLC~|*dbz7uP&NW=eEowPU#UC1~iYJ%Ur;-rhEV4J# zs@L`x391fpq91JWB21T9rL*gQzFB+{x`w0?GuR1!LR0Km4%ugCQVock7J|X`+5LCV zMi|u`)eMkY=kIi^d<-!=EHe@sq>AcCJLrpRo);UaKlIEwfMz?rB#GOvx|eq!70=~m zTWq6VLP-Wp*ezvX82PRwK9kB#tmDH?!LKPGPBp`?@D--wiW+IO(++P2OANvHe|UkE zDeh&6ZJZGUvEJJSK&!Ct31_Asz`o2!Gb_*HObCeFnfJh+T!_pJ7KtM+DY5Nuke zj#NJVazUrt?2zV-T6-zMFExX92mErtVg_cOlFdJqHu@<)iIGm4shfZxjDii)3(Lb8 zp!1MVi=&;8gE9dnF3T%VC`)-qCv?Ur4@kW6{EwXP;r^TiR08z_-plhRgRQZRnXq!a zd{6q}LAuZhG&OYFy4E6!%FrvaI4q0%SERjkqJs>-R&-41Mx0!*i-oq3Pm{=CW22@= zs2``_dPURiWt3skV3R;+bn+b*?FW^LC0iiy@4*|j=l5|1QaD*4AF+;JyG6ClYmv8o zX_L2iYyVcoowBe6zsu_Z!Ewp{oJBW2Tsj7^w1E^daSRg2X}!5AM)#p_TnRVNX|A@$ zhD5OL&(6SPk;z}MWHu!63SjYP$DFMnM|3|e)%h}!W)oBZE^bTob4_5CvI|R_-wT0$3XMUM*+VgfVy;DA4nNtx-37NPl`r zlYVz>LNbK}W@eSP^A-?208xkaR!g~C$4Jpl>5CDe!My0WU4l``v%PCC4J}DWdC&QHD`blzfIz!1% zt(?Xb&T`13f3$6*SLray(Pb zmN_^1pRW`LmW&0QuMgxwZcNBaPtLufq0ix#pG0p8o(M z!hHZBva^UEZlF8A#u{K;t`c>S0p$mKOM|!!jBhwo&;g|CX@6ooAXnxj9NoTKp{5S* z%!E^)3D#N(T%#hEa zi{C2lxCC2P6S+2jHPn!^VKJhKWl?^nbn?C>bR%p4E#4BMxx<9tUUM;6g){yYK5UB} zxu`;%QgUejnt^FAkfeC_$)dQ7=YwxfcA^llzSpKJKs&uKk9X~H|3tCHr_X#L!Fvbp zy+zzBFFy){apw_U}q5 zp}1kL!I+bJf{J2GM~FR9pDAO*=b!GGHkvpq90OG6gvgk}l8Cm)hL^vx8dC#!%~K8f zk%K|1p)hXo?V=)xyR!b)f-$Glq(n1&z`v(Yl zESRMYQRi-m{Q`g2M2TkaU`f9iLWD_*w97Gf0MgYr93f%1sD-VKK}`KPWDVddC3O4J zQO^LGB-N4IoS*T7jMkGoU9O*mDABG2==q~NBM$H8_8h*fPv8HK0{JJaaeKm8W8Gsr z?Ucz-;g92^z9kErMomiHEJo)^gnxDTjQY5Q8H;Ghzx{q=@|;12{Q$gaj@jmI#U|+t zvi}=lNq|CkBn!xrdDB|utH7eE?>gJ2@6A@ za-1a$kpOOJ!pBs|yikK`@5GWEVduZqDRY}f_%U7w{eAkBFlF(?GQp7The5545fJ*O zVvDPOrwhI12t1hiZPVbzPDA*Mx!XgpbaRegL%!NY4O|j|J)wK+&{x7+Kq*IA*0_BO zd$Q9{Os$ATx^Fh&I4Y%FMiAHTeIt?gT1WNB10}Fdia7TeAas`dA8sF6Pr3{jnotTt zN!2b_@VJ?XmQVNlOTRH{;iiIuq_BP`Tip!6EBIjqAa-a%ta-+9Jd$po9iIa|IJ7wX zdU#|+8ZL(ikx1CivDDG4?<$W=0g-b;Yni$-1l+r7+W%pZKo&x8M{<4S#wzcUGt5@q1H+b%DR;`sE4<(5Q8?`tKcT&JD^h<$JDt9Xn1@l%SdmJhktq|O<3 zbpikuD(h%n)qqm`zpCWBaQ9u9uY;Qp8^RMYC6N(XfFZbp`Fp)0!D#_B(ex7Dc<ut+fVbtxG&NMxg%c#41W8^2BU!4U_g(=u!YQ6t z-~yz{wfnzaLRsA!C2ALvtlDtK4S>iU3Ni}hc5fHxrZehfD9Y8Y78ZF?vCBVI9ypny z4#MizE#8McB#YpOkJCR@nGK2S{|XsF0-MQFh|RjCj4N&CeRttcrMoKd*2{MAbXO}w zj7QjBgzB%_<2BnSbEgmZ!RY@`{`yV(p`R=XqZ0-{8R0y#qpsF`pkNDwk6akOxIr6-&I@-Y29zG!Yr*1BtAd!;~3pFcL7KM6P; zZv4ROU{_E9C^JqzAgL~xwC&GOYDj$ZMp4;$Fju`u8~mPlpsmS^NzC@!U;_>C?3Ypk z9V#bY`U|AHGyRmT(qW;jcRaW47rrL84(4gM3-$s*aD4k48oCdJb6qdTEkJMwm(P@# zJeQ)O`rJDO@7Y8`JP|?e+3HUQGz}zA^T@)*2S4hxjB$TUw>2}}8ZFtI-Ey^*4XGH9`%~ZpLvuXm4y~0^W zNwce5$3H_z*I&1Zr#qx~=yKx81myG`1ks8l*KOO?tv^4qz>Yb^67DSZ^H+8Cl1G3{ zj)RW{ftwAGMX@`$K{H#kFH7UC9dD~`Js<%JB|d^IyLFoBZ0n#J(?Y7R54X@DbYk}*M!X_={z^J5 z>W=0l8U%G&-2!iZ)_RKu9Xjy0g_2XdxbnI|As$un{13TlbKsrcE}^9m_t1Aa|Qb=3*;T zIZ5OdF4%TNn6)|Ap0#|M|M+^ZZhpNW>+wIr;)Z)-)XB~yb0HNB6$j^FC8%njma%3Z zliAzfYDQ2ei+D^Cv$XSTeC2&yTXw`46wGT3qa!SfCilhHNm}+7BIqD6m08i7ZI{4p zA&c<<1rfJ5Bc|#xWb_cFPD4}#COqj5$Np7EviMik;rCv&>0Yyq99!o{jk@sh-uk^W zi&hc**v;8iz6MZU+}+&FPh7lD8N|OJyIb#wR-S}DixfmAUvjBYM)C+63h_!>+;te^ z3z}~jU*!0%yvKJNrrq<69i6`9D45)Tbl`y61qY-9>!?@(aSl;60``c94-k=FxY3B6 z+q{8D!@OTM-mpr8x!=B;EE9ZLPEBp5JB^7ZdLnbJn9@~ z!6*Gc?EvZ7x65t0!vGO5ZX6&vZiH4M45-PyMOhKv8^;uPG?Gn(g+Nmynd2(q$XBm}GaQOfHKB@FvK;Z1K2F7uig)2esP4l37gnM28)`=pW;KtNN!b zLqB@qJ9=w}eB@$X8oI-T^I2*nUym#E^#^Kwhg?jBiPB$dsoRLbfj4aX5G-APWZ=T~8-{f&%p1|61R^%trQK{9ZPj zN5uyBCUsvMO>9gIuW(vWQt6BxQk^;#R0$@`B|r;pvX{e~J`Qsg6x&Xm%h80knH;H??wm@FTU3@? zdQQEl2gnZ`s5$#u3s>8L^nvBPK_W{US;91A2nXy-xf`{-`kf@QkRDq7(7tT^5~ zR(${4?3_0t$p3OPuP70?6ZrQ(ds@5WcGi%jE#vPt4l?rfC*5C6C(;oogX6pzzP&~9 zq7CxiZo9tNNDKims_J^{2nOxpvR1(B3yEiI7pw*U@=ya88Js7j)?JT3AwyW3X#@@D zk0G=zLX4S^EIe56rrw_SFxb{dSQ#*~8*$wG^eWI87LD!!z66u&k%QgUbqtI-9#brP zqqHd%I^<-DG+dT~jFw)V40=UMZzhf%b+#-Fy%(pd4YS_PrW@e3q7D*xEfFru&Bk_1#J0>JC~(LfD8qJ@0|jK17r9h5SmP*m#2_6)!|WG zyUL)BY-Jbe%&zBmtOz5Oz*9rpOwNtRzpxWsA*SMCZ2El9?3p+x&HVhxr~UGbVeiFr zs$c1;gkc5EIeU*)tmlX>H&72hc{DRx8Uby4BW==cw|2(cSLMIp?e=sypuIfO-^fsR z%Z8XnR!T1{Xj)M<0-E)&h;q!<%}sqW}K!AL0H^qa$VrCL!4VA$V4J&)9;k z`wjDM2)Tdi z5$*LTHph|J8jviJUXdeIKE@tZiTkm4dG=2zgGkluk4-~h3)~0Vm?WAh{_>C@Ljw(p z{#T&2oMvsW2P|+hU8vP=55RQuVw8rBVs=g^@H~Fh&JI^@`P$eye|D}jjI4_LCz*dn zdKOu`a%j!M9@hNBk6*yJXd9%qqQb3ukn+eW6OkjyH}`h*gAz6u5f>xjl2n;V!rNAq z>mTCHFoNwH6NQzYdheiAq86*2oJiLs$E;$=+cytPHBE8mU%?EFxeY08-+PwoP5W|S zl6$=gl)UbthiwmA+M(cUbL7M?{Ht_f*9=<0fQAH1^PLl^78n6Xc@NiD4{S2cSEr!y zg({jwKD?-$?wnNsVK=#na#3gJs&1g&=KG)2D(skA(H}JyG*3fqjIcb2X`@p&Fm8q8 zEqcHeLofC!sct+jvwHQjhP@9u%&ah`=un!4!(FpH|DYkK$yyi{z#qfR0SHkM{UHaa zY?N776M($eWjVbo-hn+f9;L_f%kJ+z`YznR`&l}1T#mQF1lq$2f9y*41aKEC-4nl$ zY=a%R5p(h2sVnW2R5vv>!eB=@k5L5b5W)FgN!l{iNw692)vsj8>t!D!O6_>P+zJ&B zSqZjpf@;YssxIt%xQnQCPu5PyNDfOG+vgdGAZ@kel{NWJ8}*wh!MeT7smbJvqPkjQ zz?7BGi2*sq_V>hkK5F+@gnJrR_T|E7AIHZ`Gybq;vTH7d)fR0&gm=;Do07>#h2|4& zoF^GBeoLF}1^X!|n1!op-AM=-<`C59`=bk3L#475OGv43B!7g%~%w3$@cBSRYXg z$?u{&IM&U|^`--glJ!k2x&IsqiuR3!rKv^`o?#C14BCNXsvKApA#GCLC8g) zmdSq}{zDYT)e4W4qTKoQTg(_}AWGJIN6_n$F&enlwsa#|^hY$@j(*E9#6IH5{0m&u zG`wsj=jDQnCgYsz-ZGBrmC;L>5^VV?d0t6+Z|1|Nt7ci~ctS~>l0&>XdMwma9HIPO zu$yah_wp@@LZ;XApe%U(iB?N=jN&VNgZXU!)6p2iVwU5jI5-h>*xsCfw+tXyZbSc7PdqK{|Wq2UB`O@ z=Az^X2N5v=;_qKeOU$%vCE*2(%-YpC9lm>O?{7{|*&dGR62Hd}sbwrt40^}~M18ql zOTU@i{GyOmv+xls3V&B%-tQ)0#5`%U2dH2^M2>NQ)U+7epXAM)2Z)zEDV@qVZues< zIA^@pvBt4J1hDy(1jXM7w@~z30UA9LFx%DL=3e2Td<% zfNVB*2?zW{a>Nslr7-uUl0QB}=^P?8>pK2kAquRSy2MVS9l43yltIBiqe52x+E+@X zy6RJs6p5?e9m}p8F2l7vh0p9zB*BeIV_VWLNB(TNaS1yX2(Jp`+Atm%$Qbqx4FX5y z8E1!Qi>)l>C1&NTvk|tBZvN~8(d}rz>ohw8d|HU&%Xx@Or*S~iDuHeL0x|f0o>g}7 z;$JVOJK)Z|iRxFezt1*WU@E=xJK!Dzto^u$EKmwlFq;L9dn7TqI{Hnl9_HNa+Cp=aPi%_rgB8@1vNbfuE<0a8C?&LIMVoP4qZp)Fv4 zntK!UiU9Tf+h((Ma=5uHP$5GH`1F1jV3z#?_n22Kvp4{gJ77g&<=#@JB3c`_ zS6T>39svK#a5|on_=+R>u#_Uai(yJap6SNE1!rgam6qz0O@?l^C;=%$fx(o@qYr+m zWN`CMLExZkZUN!=6AFhB=l{DSLWbWk#T5D>p@zKkkJZ%5-RYA<(^n)jXK(tks+5wS zJQMdufx0ak3yD@SJ^Yd!i^SO%&q#r-IGWEF6s{t+ogfANFhWxN+S-Icc<|IWeo8=8xp z$#OgWmq8-~PWy%w*5|_o- zSN@vgAOl$Uwo*z6-$Uj@z#M=^4fNY=#GPJqpwOrw)<3pS(B`vS3D}F38kPdA(W)J_ zf(>CmY96OmJ*QP8->KAYy7#r`VW*VjV15F_uX7X z91qYn3p;R6y>WaAkm4Xy2swbba690g5fMNTQA>jhOaxxWb~(n}Y$D8Q7HL%IKl@Mv z!=^MHY>DBSHhC?-Z0D-U1xLh$Scv1W+78BK!!CCWREU-geGr&Y4iTd~Opz=~Q&9PG zoGJY)$n|!#M!YssYd}xz3ZQAI*Ol&Obv1hyXreu@XXMU$5$L5?%J*)x);vt00X&G; z;E)E12sU9@k@fzNXQxMv)W$j0C?wpwd4IJ6_E6;bVgvj~xqwXW|I`i8e}2q!`VFu$ zeSIXAg0$on4p3iEP0NwT-)j?rd0Pl}J;j;v%tS-6fOeil@{8?e;vh&i9kgBn_^M(h zFpWUIKA?u~k?}xLmD;Lr4}rbIBt_%xKdar;$i-6LvgV!h&=TE8ni+D!;xB%TbxWZ& zAN8GNydN3qCwP{X5qG1^^F1~zE{;H!U~2KM_{+h`P(NORGi^$)2ho~+m``z+b~bCn zpF)?_sh)n+>GS#=cO%QgZJ_IVA6yKNoY0xilP!P4-IS0a@Q>nuzQ^eLxmT3vUBEIi z?5YY2HP_EJBsV1?aa#}Q;X-n9Wv8V^UY6!X5t<+@M;-J?S{iJtt8XHv1xRNQmzLiR z2O2K*1vOLV2yI!d98FK1CGu2eP?ZucEMj0qb&IwEq8Jb5A_$f>yqs~WpLwv)J&s!~ z_&NjgYHEW0hTE zTWJ^iohx57&QXGHuVL=!?^|kc&>;bwb5sbiqzSgZIqF+y*KfJg!h5IV{vf1fOf>Y= z8+8YVeU?pQ%Py$@&Q0$~>!Wy5kuclO?PX3c#RNvnBBE1IM`*Z z*Xe=}wqZNe7Y8w6oHS>PB&-61|oH+!T+m7r$Hc14By91%5F-CIG=JCdxlli+2zAyjIQ6IDwjb z$z4WwYTb^@XYoK3TpPX)xP;W0QJo}in% za4%}t#;O6AJypwhA>v;v9z>^|mfHq;Osy;hNPoNMz(4&NPrysZyuleg7E$D>Wm=QN zss~hQ<-0g}uoqS57ONx`s|{jn%27RhYXF~5H(eYW9{u+W7p}JnwHg$|u7qX!Y+yt) z6}%>%nZsBt^Wiu$-85OQ1J*)k*U)(V4PBI zG9ue27}P4@1TQZFg0daaxD~+QbfcHgUKFYS*B010L*cc}K$0k4(E@$R(A7fKwh1JL zG2%FcgRBJX?qT5-E(3Rkg$(1)mKLs-SWq~#siP7?4xu1%F=le_ek9Ag$AZz5bI&EO zmqFyc(kC>Lf`vPhPW?%cnN{k~j|%uczF4u0c}!m~(!M-&iTSm>vcFXRr2>jDKlN_z zP;+??c5n5N9i&Ji?(S@M27R@VKS@d&XTki1q%Gop(Ka`(6{NcSk>oVC&E5}FC7xRL2}Z|)%_LAIkYyD|7h=2W$#U9HJ+HY zi_hureAT(RRH+0cqm|=MteMT)mAU7?C1_Of354Z zfQ{MOCtuMS9JyiFj;w&cP8BM|CI$RTQV)!!M9 zwjyDe3k}cnh4Oacd>CmK$;guQeV1ymkNrWIN`F}L-3EP2@a01ZSc;VfQvZDazQ%}q z^Jr`Ec5bDb$2k}cKX}!ol$_4)4(%!SiMAJPQn^1El)Xd}iOy#~vN|PQk_yYjYpXZ% zy{br}WdUn;d_6qUXeO+QNHi{tm;6@LR52_Uhf#IRCD)`MH+I!K=Z@DsLq~aB&CAW7 zZevw!dT12?l>=DtJs&F<@_h*`55btcp34pthWVWYxKO0Lel#tvO+Rh1^tBzX zdBu82+qd(!Z-?MwV3Qedeo=?z#EUoD;>((qtdxXC)zmTJD=!8T?Gc6K&u3XHzFc)j zWRt0oh5yd)J(~xuc^u&MxZM9_jtG2unT;9Z572y* z)E+i&DSc@GT-z;EC76&ecwt%nP3-TSC-dnccZ&3EMUC!3b!7i(&<#*8qV3r4r00Gq z`q=}Ib4`9fke3&49H^nrjHkl(&cS5THJgq^sf_IPZmXA4yakfQov7zf9v^m2!>}2P zemstOHFMs2TA6h`g2K^lLx;|D4ruRP|pC`-PhX&?q zEPca|^0=+fk$C!GrTsffKavx!s|aF%2uuBleM$oB!aj%cY&_MW$Z<|fuc1|+WoF3s zC!V3oRvwL`&L5Jp;NVJ9p?dSLr3n4ekKTYkzMCCC%F8b4&+hH&_}=gZ-~&eB)yXI< zntVH#J9~2a{&i_(<$muZNPquSGxSQC#W6r z`1sg?jKP0g7>~{&2@>laAOWb1f3Bmjzf%UdQ9hWZ!e_=x{Q|1uU)uAe(CRYWVWxG7?ZoHcL%(V1 z_=&KQ#^aJM3bZ18Go@Wv znKB0y6Mu9kZORxS74;_W4UvGVCZ$A7>mxY2M5d3yo{uj!%khyqfLOZCx+L z!CeUI_9^eeo8Bs4uddBBY4r9$&poGEW)r9Fe19hCiJkv{5B_{*U{7SiMV7B~e=&F* zW5r~!gFZ;m-ypEOepAYxHDEi@)WutCV(8kurM*2WiC)DOY`DI$F_JoXJ;JHVp;Djm z90ApZh9W}YQ67!ZtOy9D>*H-U3GSnFpQANxqUe~WFP3Ch5JfjXB zz7uWyAHEBvadmTt;@Tj|j0@`@L`9k$4zG^o`iy9(S_!i76AP@|%Fs-lNpbkFpZ7rGePq?}dkg;=f=P_z% zKRJAz%Bw%R7{-`W4UH!dxg?bg;(_NKM1x8s`oB8(BHOYq^T!@L)80&&ky=Nz@Jut^ zI0V+|9+oB4VJgbHDaTOU{s31||A%)mPC&8Q>B5;cvM>yMy_vFT|49OqYC*mGKQ4F< zBU015uETL2BG?4dJkotuV?S>bUsy7rKWoKMq1^qKY=N>u2^s*CcyV8JI~^#j@KH6v z#W})O+%5ljGubRSP}m5BPxR84oD^{reXps(cMIPZTdy`diA4RL6pjAH_il`+{sj$~ z*<4n-_oOA~-BO23>6X3`|JlGeXqe+Z!Uqny2gh= zU%yA2Dr&OJOoq&Nk%>Ybp8i%WPrPKDop|;tqZW*Q-K}2?f~Y`jTT6~-c;md|U%b_i z-fcP5R*-nGy~I*=pTs4mj;8Zj091}p0Ekbwk!g!85*fDTi9~=7^T2Ho=$iXFzofW- ze_uT>x$2ofZ(6STE3Q3Tp$Yi~Y3+pqZ*aB^Rw;*S-XZbl_gscnrC}%DV*eTXUQBr( zmOQ+EU*D>%d5lV(8{BtVijs9@B-mu!B*~eN@8Ls*5BwJWow(a^U4dAi5D|O`wFQUR z=ZeFuMW>JDM5;#FGL92M^ZB$f#b0{k2qcg8pICk5>}h|lIZ=%81;2ssq$=h(Y`Zvq zL1JRKKfsOnSw8Q62NN5+-}?)^=+PFoy+c(?EGr^#CwntjE!J!vb98seIX5|{LKS6iTH^F{w(+!o;$ zWdeD}yx-nb?A=_SE6=R$O2oQNvB302&Nr**x{zLL|Y!%nFPuizQGg7_;O@#H!$Kb4%1VK4wftW3keK4+LsXpOY;HpD+};>w7dVc@HV{ z2?@%2#~mJTD;wbsy6~VCgi4*}ExIjFT9p2n;d^UXlDVpaIaa#K!W?4klwN<%}E71&a(NaZjH3<8P?ZWDqCOMH`$lFNBNG+(3`(zVYo_yoW=KvJWCq@eIapw9KHCs{nVfQi+eQ(?cg z1rxFWKtwQ&w|d_UpHjvBr!l_V_yX8r!Q=6s=|eZy!x%HJuyEWzrhLvdH5@xpabBAf z!q#LtCueFMWB>$iHO`Fb!iexpvruQak?X8IVhdy^04aK*&@2f_aKSXt>4@znM;IT=Q&DCzvc5xv9nDt z5$A;k$Y--FKA-dxs?Eh!NGbjc2t0{`#$`FEyZa9wEm`=N58cvWPXIapKrlyE|C^C2 z11}mchg83(7%C~TmEFdpYQ?><6QJ37c*VHkmSs%+wmW2oPDbvB+iegbj?lgg=zd#N z6P^gVUVzsWsz~FGRcM^A79LzQ^cNixwc~nw&-*V|VU$z*w*ONXE%XA=uOK)1A-PVD zWQ;jzPF3mhFR?=HonIcxuxfabNYNpIAyGHiSXpS?<_aAs$bKy$1p}U8iibHve}d~{ zwPqlPNYY|}a$&sNAIaSr^oZGiE_?fRtN&I}>=>jagK7S?CPv=wagSpSv&806+1Lob zbY@r8+viJb!VkWrVSiG5A7O43_RYKZbYcv69ko;e^1InYD)(St9&0Lh*}HQPYo~Ex zzYFcteCJ%OM(zc5uLGVN2a~<{FBVRU+}Gt^<`JdGfo0>f*W%ZB8>^W!;aCxwUIV%8!RU$Z@04A}0O9=R)BhW^=e!drDfIya?KxXA66;z3 zb>+|^SNEyKo~TVw_q}C0o!m6E==q-+T%{67d&AV&SO> zsmSOL6(V+?v9NN)(_n&R`vlU0y#b_v{5zbVDMXS$U5jiZtV9(={;F2JPokl42 zgazN6FkHGN^>MXDX*9QVI!G=5f6D1CC-kS@ z#Zsl@eu;j`m-inWX`}uN5onfT`-Jh!NJZjA^lv=P7PdoEf~fT!1qnJo5)$x~fUM(g z)r)}*Iw51-8T~rD-(O71`fLA#+L_4@W;6bPKRmiXnVDpq68x>K%$Y5_X(5~`k&U$Y z+qZ96b)@(M|NVqD)GDm=)&647U*uB_dW!|?5RfX8$UE?d4=zlceVQs~Rr4E2XX@8%p3EyjplvL0_ca#BUK_raeGwwr* z2H?!FeCWH(J@n=+fjZIx{^O37G{Q`txIw5Yrt6&kwi#J?^L#5UB>~0N?tPwHlN1U! zmxmo7r$#yzLZ(Cys&;wjnScG9A$omoc6)%p9rvGCkcwL@AMpGC@%Bx)*s5zDTa)`v z6OHbx zhyC}WXVAUEg3QN`)DQVQk3cPWMy&#PZ;a=+1fF7h(@h-Qvh?|PNP?>CX>Wf8#LWK} zp=ZFBV=mp)1aAjaRTO(c#uByn>PDYbiiUwVU5N$AkMpr?kao2S>zXJv#WMF5Fyu@W z18#kHTnxbN)kF?E3}Oio#U`58iusjJ&ZVB^$F#al-bI*8la1 zm?Ti7dB>DC{1tk6FA6Vg(6?*gHD}3XkbyN7NEq2AS}m3`NOwhEb@0=uxZ+!H?iJ82JyZS zsm|TSM*erqfG6}VFjS5^lp${v#iq7P>9>1~EXckn|Lm{61?s!`FW%X*^`!UW#JnM0 zb^Aajr|TEUOneBL2Tj|TxHvg}3%&n~Yqt^f!-vRx&%S;Lvm!qAa{=dc%2xm3?6Ug8g_{1B@|umwu~dk||J)DTJZ4pJN6txbE8J6Z75knd#uvp=poE1g_#txkq7 zrxkW2AdWKuJtp=izjyCmIMA}wv{@DY(T}Va@j;G8j&6zhf2H6w&Qy;Mt1b85A=&5o ze#{@A%?r9_y<l=*jvc3MxOTM{ z0Xu#9=FL75boDCUv+fL?2jBGxx^{2P*|(T>u>4K~SHs~i`H<$FCc%b_oo3Y~uS{?{ z_P6WzA*jB%3lKqMUk)adBv}TWC2MC~?@=LB52%BLDimk27Q{hAmddGYzE&FUrCV?| zb#z^SWBIGQ>Q`z~0TL8@5P z%NCff=pO>gvw`pAsnW9uTi4@pBfLcxrMY6Ga;1X+OP~%Bh8#Y|f3|`i9wzToEYZhb zBw3-P9Pl@q4ZOJ;fAd}3r{#9q__gzIyx7hroP2LEQAHlE1h||h2xAks3phz z#nrC-wHeAw)LJ-`)q=S-!i*?e*t9PCxHt5@_2$n+VbfqxVR*qx^Y<~XhlfYU_YX`V z9-9+5;Ee?P>ub(m0n8l4FAm?J9(-#7Q$(4FB1?U82fM-Af3uL4AK*L+=paW1s81i= zMtc4*UygM zq20-SuXc6s&@9}+7G5zLcF-UDh@G#ujGWx(e?8d+lN|G>;fRnAl#5|Gyt97}{tbv? z2WU53gpKO#UeXafEDj=9r2(gfZDK9vl{PrT*bYeIcMv%wwFQxk(;t1Jv2F+EzkIU` z54@e?5kYBrR_?%IggJsbr#+Qa}x<*b6Lj~MigbLza-wMd@@JVSIT3SxM_e@B_@O$zQF?3 zgTH&&;JVN`Y?^1~_AkLmn$3|n*in&KG{6x#*S6FeXvOG`Ighla|dEn z_ZuwA%E~PMf@fUsDaURS9x=VEe8MvoUDWa5-64Pr$eSlpR7BNUci}3i^b`D|r2jzb zgV~{ZyXk58e_Y*)nDlutghCRS3AO=e`(nP#gHRsshy<6gIMwnwhJai!h~bvhhXz19 zec<@?{Z!#iVbnVt$kxG?DD3KqVW-%c+%j9;^4#U@Y?LOmF+Yy zlBG8>QTx0O*c7Ce>(*vaKzMbJ(?OQv})Uk`jV57Jx+Djk#50}m;>i?>fYT`*8pC%u;)Wxjdy{V>XXXjtZuKU{!iL*H5ey5| zHRXm_-dtbpqCj{aVHldU5k&E6@fdPnK?xpKpG1HF@%=5+J78r)=L-T47jDTr-MB3# zt~p33J56wj(Ag@O1M;1(>}8eTI~vt#6*wC-`lp03GBn&ts_<`LX21~1Us=i)RoR z@~go!$h)I8Me^GWfeg6H^4kMba3dO`k6zgat%0L&@=3z2MD@zt$RalZyeR;?tCQrc z-_*G=jD{!Y6@KlBm%c)SmnJJiWx#SR@viBX z#osYM?{+c(RxniVUo`*bhvm(A(p~P?(?cVYZUON7#g#RMy2;6k7oLPn%Nu@svSPkO zu1V&eg|DS|lq*U~myfRX+Q|QL ze`BxJR&BiaDrto7dz_GZcDRaGC#5K+MX=I3<^lSrQgVSMmzq8nS}a3(0isK%d~tc# zsLdlE(zl2~@4U-jZx}j{<#t;MGtdJW=y>DA@&wuPgiQ|?yMuL6wTA>%B3}?j-5&?&D%KO@6;VYrNxAim)Z7F-^MQ9JO ztGP&kpZ}R47K1?V2PP(3HA%L$GA8Z%2gWA;8?gXN{|!lzkrNUtnFj10qzKM?9d z$<1~hcV2F)wHVGvd}Mu2sDR*g^VWi4tstwrFSH*oSq{iymmOZeqWbo*wEcoyDp;x& zQ;@S8k=|m-;U8dQNr%#;6|1<6CKPP-Ll`Yz^1K_lEp>rkYHP#!G09kk&Sy)y0KqihB6JZ)=HKMH-RHa*zUu@L39skUtIERt~Du-2u zzWY@>jjvd(U8paaV~21o@fC}@Y_58kx^V9s2U_4ok`iCYky9VGfUvoD#xxtr*byES zZUr{Y8x~gR>&(|{lv%31NIs!&fu|U%I%<~C@v%Eu9T0t=EaqZH5*Es7-dLrPBWK_I zw%W5YSO{ItW32n0ku>gjex`7!AfHJjsTu2AN&tMbD%~sfE?Qf;@}DH|houqvxATaZ zsP9C#$*EYvp}OB1lt4=VPgW9b05E{Z<{Hls2u2pLdTm@YXNAwv`A@<`de#T~BW;8uJ6ovUCRbOnA%JI$rUiFZ4x6 z+hZHhsQ@x8;J4~RDldoz-o`MpH!f%@ch!45n=bdM?p3uJ@zxGIaQY*w?Y*AN=LTi@ z_;2i!P_7XtZq5vReo4q5n&(imn2i%wVW1W9A-szw_kGHq3==-(r|KGQm4#J&gIYT* zVuk@4pCD*+;EyvV>>s;(x1MkzyprSji1pTSmJVR~jV+;rhfUsPjJJW6X4mLJ@+MXq z(pJ&Rfk(C~c#GMzpjSY_dkbh7BVfTId06eOwL8!l5Ol+0Wunl3WODf`7B3q*)9yJM z(QbHVu)B+0u(#iC1N*#)a?VGoABrXDm!3biYZQo6&ILD0aAIrrxJr7KII(daSUcYq zU6p||;MTN#LY8LRlh@`a`Lo&K-Gtz-dH|oMFR{j|-RGB0#9}=}xnU6w_ngE5j{@5J zi!JbV3O_{_fd8E?v#d>0r&@#$%NKW_6sn{2sI#H7`99Zx9A6hDzL|@kXHu(wre?pk zC2qeOI&OCH*<%1^e(YAZVR(yW!iOa~tssPiN8=DO@Lm_Ve9ft1N#e_LN`f6wMeBwC zvrm#~V5ET+5OoBK_EQlyW@PwHnO1*vlfz%TC2Zh4ZN<$e)&h1G4cZocgG?wAhuB0u zQtj5T4)-uPi8~P?>#Dk*FO;8LvFxr~cL%Va)kXBji`zufQkrkzD(sc)U4K!X3&A0q z$Ihh=fYL?sMGrzf&ww~xX8ff`l-lwOd;iu|taqep&74VcwTa* zHB>`P=d{85mCL?&VY#J5*U@9mFC_HmrZMoC9WL;K-6aDOvpC5y9F1An9R`SsgneYKvYqf52r>*?WoHdp9Vkn$3q@Bbh7dev7m!eA`^ z=!P8ET)Mu)VnZJ9Kz5O=7j!f>4>t9lFJ#t7IguNeGu_)NZG(4O*%t+fGo4y*;Y_Q5 z>-g6N(tq^S4=ygwf0#^gddHw;!qFDAv1`p1n**!s7gQIsIsu<#^sZ0s{X9G8G8WDx zXrq3(`5+jrYV(FnO~OgwQ7hMeNSEBKESZ1Lz$1-v9bN#Zb(Klm@0IBO;yK z8-C9w@bF8NND&x)Na{TspQi>#xKqVFt9VT`OD|mnoX;M-a7@JYS*}O2a|l{zU5n)G zUFu&9;H6*Ay|>94-zcBd!4Y!g{>*hB+a&cSKDaN$;uzz!AZkHBo1Au`x2pqDCu1#s zd?1DDsvRR5Gh?@wUZ@Z6AOs~ylFE0#8MI7=6hA{mCkeX7fB6;0L;xlXgm!brt+1@3 z-;S#|3RiWD290JmUydK4U!VEfZ305%#a|IY=!oCXg@#H$`At-oT)Y0YY`L7n^aN;g zAZaNYaXD-q*8kpVoG6u~d$@cfq6dJ048oBs4F|bQ-Key?6wzTqf;$YJnpU@egb$83 zj~t5rQmX(At6Om@+8z%66=`^5p7ir{sSq(zU0gVp)qrXRwp~9P?RPeXpG{pOC091 zf{i&%=cG`lUt$jgl_AkN;26PBYRs6S>?~r2TGQByVXf+Ub*FOU^|#MHNHp!LKbR;TLlP;KVUfYk{1tY0-WWV0u&cC zn}N>+>Ja-je;xqr9Fjn8HaP*j0HbplSn`M5Sc(m}QeX#gy8~EtnZB$JiN4ZIGz_+n zS&{#uoOUlWqflW!$AIHH9mU0flq)s9L3t>aORLwQQob5(0r?^}iXt%i^;8owIgdiL zEUZvtHT}4phww=tq6p(tSl?Dp72E144L-3kXGCJHqG4(B^22PN*_$K<;q!j(&I)Yuqz`WG@`h{mI?wZTHD-f7J zc(@CRqfP$s3Lg_#%I*%(wCCPysusHV z@7L=3a_d9RgG1Tgi$gMVJNcVKz7IifRoE_ex6O632}rpEXIml~qtl&%AYC3}D#Edw1y=1jEN&E+Kk8HD0hOIv zpg~3)3=Pst?-X@|3#4Zq@$|B8`YP9}@#OD#!%2l7scYZmn0ulUsYY;?Nyte=@*C(T ztj5b`rSi_Wp9#DzXF~w`f{|f5FM;!%w_%y9rlDxVUuRkX3LAZ_WXov;G@<2F=Z%DL zPsN;qMcyWL?57{f`McWc0hcw+*rdP@wqJ58c6aK!Kvp{vr@=C=XhT(l{ z55BR-O{WEuH#26Fq>5#)6$iqfil< z)=gY{D-&`&xIe627i=!5=&lZhVJj1|!#+Oq?c(8;QMo>SSOA>9=K~@!bum0Pnwq~o zP{_DMio^tOAbJC9>>fRQTnKn;6tb3T z6E!NpbS&lK@v`)0X)YPi%3PV}4u?gl$C&zZeh)-YOp0wV?8`v(s+kG5Zr(zSWRMqv%8iWUsNG zXB1b-t3E*kw>zE8J-}PVk8ULH8KJNtqtk89+073Y9rugtgW4m)Zj8%!Ay01J4P2Q| zq`NBaDXP$Y+A&PH78l2SVg1&rzx(xz=QC1aUAjqk%9*{ z2A>9UzjdwV@ZFCh$DCk7fR>SU`TMW|eu#-kHPmhVj-+ zpD~@adZiZ8ZY*$vIf3HSSgH7Xw<;HPr;*lm9L=z!agZOeD8OmIli$7Fa_}#D^1PeO z>i+5ziR-{ih7Vemo5YOS>7__5RTo@2C9&`i!r9aq3hl5A%F@P-{dN-kl?4QimO3`L z=??-&=wvJ{(gffz-zdcUS{EsmYU^{?6FgZ?qyO5blWS2nfm_pOHIEke0amLV>xz5# zsSNXvp*WltpwN`uCF;Rz!@OlIIH7pWO71)zjO34f;GX2#*@)#6R7l6? z9KxG6sOiJmf#)%ET&(ji`yHJ>PcYd(@tPvFzzV~!%(cgbygZz(&qb5>W=CQ2=6K$SUOo!vL%>Iw6 z@GR|M!(APwCW0k!HYa=U)u)rB5$PA0W;v&z{och*1yr)I__(qA708E~;$^4(~n@at;DzN5gX3aSZ+);9xZ_Vjij?3 zN*+^Kf|>R0i_pr#5}=beo|b*p8cZ=7?^}?CWOHp2rr$~9F3y|Ybg{l-%s)2G&@LMK zB3|SlqAA zuK7cdmkp1mb^9L4(QMDRR`>*&lOp>Jigq>Mf_t=wOc}cZNJE zNu0zQ<*U$g9`2jZK4UH1r7XRPK$(UVj$ebGvpR|)zZ)=17F`qanlzuBp*7qyy#MU` z&cr(|y3zF+?m7RcyIBjRWIbdUR=ju7eF^j41h>t;`Y-UshHE&5!8x(Zs#V&&^9YB1 z;?wF z{SQR;!f&Rlcv^I{>$K{*bLoz6ZL40smd01JD^(~iy~Oqd>Tdvni1T2rxZDKv3YM4p zI`*Y_f*Lq7-uf8K6O&)=in>jyQlaIC%u}_cVm&0mGWtYd7JdBB%^2MrktM1MZ(4KJ zp61p%213qbl3$ahI)%-tm4~a<&G^|e8B`xf$A`zF*znfqX+ghAe@H#THY&nk)+B-h zGJe}OpJ>zQQO66R`1`$?HcV$Zz%^Sg!`wVnBN1DgkEA*Nh1MLM6T(*1aBfUENaZEa zhA1aGMjW^RFhahfd6xKe?U6F47>Z1>;MFUNR;>mUDk<2CNmL-TEkI`o!OPsL-8N^x z11NzeHEVHq3Prs0>?zsExsV$)@k5C*KHO88%*w_mv!!QyZI*$$R^Rj}V-JwLUL3(lwLn2-&zl{agk_=C;$ zuNi#Y-K>J9wjht^80Q2Eel@lxj75Vz7ea<2tcwp8m#S*S`o{o&W!L7RaQA7z$8sFc z@shb8*m!DV;(i=31?=1FkJ9%HH#TR+E%#2R43Ej$EE|-m1I?k{w`q<+j_Y(Jn7WIQ z)y-;Xcaf})rw78=oGB3NGC)K92P0R*XqvBCEcGjhv`wf+S;!ym93gaTt3OsTswu5| z2Z_8{z3|hdxPHt#4){T;q| z{>3Ypoe$$h#I&szrdEsr#khBWUFz$eEuYVJrF&<&by{%Tq=?J!pq)Nyd!O~3XoJEi zg%7IwUMQb7@)tq{lF9|*D$%3I=U+72rZT+uuvdjv8s9BTCVkm4O;%>3)cP{amDlk$ z>G-{1ANdex`>pm{9YUX^P3~-krf5e5`~kvOziki(^t#*dCI7?wqutIo(vi77hsE>N z37RjCjzRx_Nz9wjICyL?*oU?&DLyTzJ6!2ue?Z*fu~opzuS-cbVqjhcLql}Dl2bfy zy3jFcTwJL#I*Sib(VY*sBkyKqvE2@Fm;0cyk?eSdt3appqAW{zM6_moq?-mH}A}&=c#Emb#@F)B)1FGaD~wGX0Q08j?a_gv%3Myo)hA3 zlLDjEw;LuNRwR&QodzAE``%`G;&6LiokxpD!q<)!0~E!Jx`UzuErQ%FOs+fKzOyjG zwN(IHEXro@a=IM{{d2XZ9bP?NDU_}CN0qz1aqOB=L zv(T#ffe(M}1l=f&>?R1!VxT(yo!Pi^5EN>_9Qy8_fr()7mm3u-Mv*gguLP=@24AZb z&Wb2iXEp4PJ6u7{#om$5^MWzkwS zwR5p<37JcFAcM$%m8H6GNO-t6|k}S&9Jy5Tzbbf7Y8P&Z-s(Idlo12gUU>&IGfcB5-8o|K)V#t4{?Q zwLFiHed;k(EZQty>$JnI(LcSL_=M8wOlCIrO}Z9$*|OhWymY>m)iQUP-N@hc13{{VMIvWnohKW^v)fu68o_^vT-PB=#^T+Th3SWRH{Wyz-)avwFX6 z>0!gS7K+b1+P^y)>Lgw9{#&yZANNlofd9V~0YW&#g04J`XCERr^L~q`K5Q zKZJ+|<>p?O(2vV#Zmiu-_*(Im>OUrgm23wCW^u|v>X++t44xCv(TO@IYdzBaZO$-}ireHWzBf+#gp|tE z(_~usr{%LP{t=elCW+9j`5`!urtW0&ql{Vu3aa0^DThM50&%mgFNI zevcp;c^VyBUsQclb+hBiZo)#c%T~X{Y#1^`9JPqg&tr{?qqGaAJeL zFM>n-=Hh_G^X~`l@qbQUF3MU$OKpU_Gp7rlX@$F8Usw5a$;WIK1`0&Wy-Ny`S0Ao< z1U&1KBG*7Q{M!S=TNps5q;Ko(O z4SPR>C#RfQ%uKV&gHOH?#*tTqq%Q*Qst$&t#1ZBgJ=VVHu=p4h9uaH>S-bu zGXM$6hzPuXIC3-Tw=JiXv#gh|ROO5T6xo&kj|^Gfv2DHlWqzgU% zcA3I*+Ha%GXQ$~YIH%bEyiTvc%Py(r$&`4=-2bE$@g5@!ShP~d6AS1Z4AV{EAQZs9 zjJD~+`11=(Fg7&b%pKPu#O!^Ew5$PD3}pO7D(72y1Z7pDbv!l<|W^ zw#i0WU0t5!eeC2_Pm6a^&VxU(RKqGUZb0c367jYeeRKd_wEIfQP{>(9=ji-|Lt6hL93Gi;uZqRWD z&l3s0*5kZrBry8urYTbvOwKFhvDiSUU8@a>ER9TyDI86Swnp7)VYO?>>w@1I$gcq% znwUZY2lW_Zrre;E_0>(Egd@iCK}YxftIhKM_^NwTgV0jngYV&sSKT*Ddx?~{eUyGc zQ=Fgv%sv#P$M46?j~egIay37(E%8aIK>lcbQQfWgF869O-CeZ$#e&CgkEdkFt-ok# z)|;#T=KRFHsdG+qoLCDxbmwR0{`OT0+V}3ugZJM@4b> zEvH7!JpMKmUi%+tggn5@Wy4KJp~3sE1oZ5QjQ%Tfr`+BZRqXPkjL8I1qN*F=KN%0V zlCt3T52U_QPRO44i#lsYun%3oZOMyur{oelCiAbK#QRD2OdP!d4VEWKUDl68{3ZX`J$?6|3(jzwRs+pN-x&C}7 z0781nbe&)uuh4# z&pe4yK16$_b!KAh2qD(FKd?8&lO?V#99ga9>IoxZdAMBMavSZ^P=+c{0d}I3m{dUa z7=iLRnxck!co*hjh0FIGWGLY>-`$D{5J{bX?kOYXCc}=J1#uRO>f$;KzQ8vIgbO01 zO9-T`8aHYVKrE$RX;;7YbFl4;Qy$gab9;B202NU`2a#zKca9o|R7qcbO zBe#$Pq%ZEh?GbOf55buNjE4V&A0^FA{0Z^o>aukx%l^3wRi1y*^Q^s;*bsk!VEiFG zzg16U6=|#hi4w^qM_q#IgjE%iV4pOx-=x~MrEy;4Rui^f=@K=9YF6{UUMqWBmk0WG zqPN!U32m6rF6DWpF*L7wS*kJ#CBj2VWb1-MgaZw~Y4ReCOylDgri=w0PbA z6<4gYWax<5jy*}%>c^E;uk0s-vw&6|o%nZ(r7%2ruSJ54J3~Bka1UWLn&O8hg$h%? zz%Tf`BP**fQk6Jk9h{PeGX3{W{%c~8h7RjnW4V8r&nE4v*039~X2W0Ox@!CNi0_uoaj_4%3kVgWVKQRvSA}c#=!hhp^~s3KS?!2RxBPz-4}EabOs! zM$yd+Be-u9GU6eS_ABDQJDbB64a}cpVi1pgplVIJ4}Dcyh5ESU$B(2q{}P)162MY> zS{*j_DM1+uLY6x|MHD}Q(oWku|Br})`DIT4J9XI?X^5mmy%vEzQ&%=1|B%uLJ+8x* zPS)b~e)tnq3P9PwytJ@HAeToCp)u2bVk@W0Q+LCW+>MGm#aRF(Ku#FFx0rczw^176 zN`^k*XT1XdK_a~3Bj_2#EG(PFp9el)I01Hs2rJT)IrGmRlT&%SVAEA} z^PvC!-7ll${_+H-;~O#E$^}WzoHB7UwZ>67_v?l*+7%eELPeM99YiuE9 z7ApkrjVQD)bl+mt%YWs7f7~vi=fPE#b=qgn1ihH3oe%A&G#5)JnMU1hXHmQNn9ovu z2g%yCA?13-+LRX_I`^)CXHW_NKIB~tvTiNDJ!T!80TQ_Xk6|UlP5ws|?XTKpc zAAY)4AvIM*0641P*msw9X+DosF_M85XiHg$@!g;wz{qe2aG)%K9QyObQLnaJ80a(w2Yi2JLjS*C{#{|)6|_8@ zF8~89$<#$$7V@!+F1V(e42b_?o5}A~ltUI11R2`>L>PK%-}@W^83F~fZl4*|0b=1D z$ad&BP$vD0K($m|n*eA~a>`vVL(}zsCLKald*pC$75By+PVr`*Ey~3e|~ryi=T@o#ZCE^nc^n^N)jB!DbMvu$^S5P5C8)U z+ZL7GA0XrLe?R9I+i9F@U&jvOff(KfwiFcO?!HCD0(2C==R1(#*x@3^VqE94MPd*h zwQwY;`;66MEyfY#Ajv6Y0bw9e;>R^*34{ui^K7*ZkN(|B&Or>|?~%+ypkDvAQzg;= z&>qR!-f(1h?q$PxqCw>|1*EnUfw}up4NGeN4>0m^S#74c7;s?Vog#b*+?0T4Ajk%E zgNDMe1q7S|0s9~+I|eXp`KU&`eUkd)f0ry`K{6scJG+)PT4j!6;64}w6C<4qVx)04 zVG(ly<6o4Qr=JqaogetfBDE6|6C;q%oHr(FwZ+K+4r1K&*JXB$+^w2u?bE-1)XR*{ zy|M{!ltaXY_SRYJURK?l{#?}n3=4o`A&aIdw}W_QpIBh~=%1a%emYC6U#?AV`S-;; zpf`8y12!^TPXs%~w?Fl^QJiwvcA9~<-5yCOK?fU!@NT z)$f}=H(fTWN!or1*|gm&OgMN(czNiGD(NFhw?!Oi%;7zoIja^x@i z4TZdK>hk>_goJDe-v*5Z{bU*3$yrjdPooQdGP9%5h?CDqZ>7odzrJQGD6g|NCn!H9 zyI3`s<}TNa3Hl>R0mzek$OQ!IAc{s|?}xs3TeN$vx-g-f+V{cN6zM)%-#$LxBzgRg z-|pF@HBq-))`%)}U!-5~_%tTaq4e&Ps`TPji$%F8<=CBCn$fws6@DSA929=LPn^_P zOro&3_%R@y`q>E#i75g7VJOep92`9OcEmz3EsP?7*&uD_8{>y4QZJe{^zTRQO1L)e z)0x!8G`^d< zzIpQo<^j?bCCEMsb8o8()>hbWWVcd&uV*l8s5;qeLYqxtXZ6Q3_woie&ko)EC z;rkYxq{0$Jv_>0k%NPSgkA63nQ^Bw>c(#%9WGb{ zc1hIgw>sHFKtfw7Nygdq9~}XpeJb@gYzGQ%1jEPRT~LU9T;mf~s-fxrWv#vlYX3BK zjwWUqtKT{`2FNC%c*<(qb zq&*ksK|qAbkUOw{0VV+P0l9Dj?@fS&g~P25g~ZOoPwR{+jIO{KQ!Hs2jPm&{`6r$Wt~{lxq8cZX&&ls@2(83?ysmp}QOVHwA!Cw|ZB5 zfB}SrtaO)`a2cXP2UN^8!i$neMn>j0(LADDPWD=N%5>NI5F}JJ=^Wh1^Vj{?D!N*+ zh64zzmHwX1-)c3^Qw=7ipCthaLnHHe3~hADkob}$=p!BNJ7AwrN`YRCj5sqJyL6=L zv%+sSsG>Bzw39fQ?-8|+6QlLuVMrPGx=blD2&mj_lrFMKA4jkoTj@Qz*)9>7YF|L`zJN2DJMN$F$yXYaw1F1;ikQSVOtzQ2t<8%MF&j*+=-K zMQ{^qz}9>oSYe~$^s#gM?R=TspSXM#%e8d@mfbKdHYL6*mndQaf6;cFgNovKkmSpu z8+kCQKmfjV`S)VzjsA_RILw2L;&#eub zXj<&IfnW{N-2wT{38WiXh};<s9j)NjEC~pTS4F2pDY!x#zW+$0a(@oCIz81UFD=2uy2Pl(>P4;ucv{BWcAU3b3hzY6ygQG6 zGr?m~po-K5>jZKYD<4%1&jvX}Le*{NfSMJ=Yi; za$ZRbZYzBtZ-{63K&abq#RY1XS&h6ivZ!@we?jL|K%+IcFLX$hYjUl~k0*r2c++=2 z6mv1&z+ZW0{zONUnhhU3W)yhPXZb9SIqt;Eq%QX&5Yw{W++&frQ?)m{7(5s@1-wMm z>#u~xqNTj4SB1N5Fi1pbl*W>G2*$HDbPGi|P*zQZnh&c(EU;ku%~&j(7sq8f@qe&X zQ*vt!C7(w;uhM*MU_44faPIJ7j)nu2E8qf~8sFv^D_ntE=K}v`*9wiHM~rM+J$?C) ziUe7j04}`Qk~!V(YQ$c=%}dk3lXQYD{x0e@Nj9m<(`b5{!7R_1G#(rU(MM+elFksj zj_%ZLiz))n5jR+!@SaRD_z!XEcYgQf2F%fBb90KrVAAnm)=#iJtTKlV4J>7MPk<90 zbit@|!$@Wzo-60|C9DicEE2A;Mn{qPMw;VcV#M5f?rfQ)L5G*Sz9o9c6n%X8QFtT{c~JOel_eA^#-#;l)L#igdu)Q zd>vutaWpke%DGwQ@A&e!QAd;fW8)qnlJhZYZNCv07^#gj3=KVmiKD5oz5;kOdpWqz z#*A4le`^bulSx|iHjmP0<%09hJ`o}6A&39v*UHGb5w5DyYdhI`i%I?QH?|N@P`ADs z+B#?d=H@D~)CL9is=7h#6ycDaK^P=M_rD5*C2dj#%aT~;ZuqoW6f*e&Ub%}CjO zD(XS6z|d$@1O6}SOIlOn<^zKf+!-7*Id>^hAI}fpZCiCz=&ZjmTARj}?^tyVy1z~iD#3y=5W;|RXtKlXe;m{U|!#v5xpRE(Mfg&C6 zponps%JbTmSzd3BF2iD~lfXy|)Sg+22KZeV!5JkL9tWtcMSSP_lyVR--&eVuY!Xns zzcgN@1>)=8%30S-X*30o+v_RZ{l2>b3V$;u_C7`(m5ZGBzw{8>302?;CB*Z!LS`Q0Zl^us+TL1ReJivQh% zaUJ7q*!(sL4~ad|fFK#odjs{IpcUf7T2zC9VWFNCFKIys($t15B*;Dlr)MV5M7XPeFYvT(dou$YchJ*h2U|xsB8>k3?BM(kuq((%z3$`2MMsL&6@=Sp-eXnHoIs`VuPvIa=Tca{cinu;*gjKluFw&2?grt5oq>A$*5sybGqFh@9#G_Yy z-*M$a#Yka?_0h+XxEZzb*Jj-Dx)K42tz8o9XYPi997XHm zF2JJak*s4crGNW>K$}`aj|ZICp!oR>EF8uYM&<4D?sX{KYcui^+V9TVs*N)8h`&VW z9y-%|;oQv48qEzVNNP%{?|6D>fhdcCBtlfug$v1Z1U;}X#B8Wc=CDHBzQ*Jat0KK# z%S+Q1Am}@zIOj?!uA0IMhHh>9tvrg|{FYlRX~!=)Wy#r9gPb>8Ja}3WoOHC>Ui;Da zuG!0*x76M2%NC(GiH3Gz61d3k1l(t!dsnyBx<=il77T&ZFzFW2v z@AUR4Q@9Pc8o1GUrOf^0JZ|csr{Yj?*{P)eXpWWxzafWL0Ipot)qh4DJcb*t+ch`o2 zMn>OFgAdV-qmjE7?tUd9o|AWpLlEho@S}eKvO5fO_#R}zJNk@^RP4vDynE?6{F&0P zfM%cOfs@2~+$O5(((^a;T&vB^sZtKqSm8J`?h;PZtr7;O577H}*2_+->~~;!u#`XD zXPh6Q3(t%o`k%s(!)_DMA12%AgVN&zD#)!wEb=l0hm)!fa3`IrT=({Q!dvcHLNg^v zF>wi<;5EpvIQU`0w=rq;(UxiQ*}1eMk*uY6n$oFE?TDi5=vbk$Oe`=7n%PlehPboqCUrYK^>bD<>U5jAf) zB?|PPAZE}%R%oFDUbodvFn}?`t?p)B)eT`umk9&%b6c56JpnLxNRMf-x4~oS z$b&ppH#mHnc+nT0c?0j_GQ8&-j~J!d1h zjY(g=)!<~hOI*eE=jZMBp~w!lNy7*c*ce_6X1u0c7I+NY_A#GdLS*+Cnr`@&-VL*> zF8jz)E_5Zbk;AD~mh{;hw4T~V&0y}7i_En6=!oq|+W>^={7HtPsK8WE1aeAf&7_U~ zIw+3g!VrS<dq3l6Z>mR;)ilY@EDQ}FgmE| zo3($Pr%WG13~yflGwJf9g^UDK`SPgsD{uK}Y$syqq-_CLQ!YnR{oH~qZQk?j(4=9( z@=JUG$zI;^=A~h$rtggO6zK|1K{!PtuUeU}!c*77F*l*a<)hnk>xp34ST6FWyDy_L)+aG84(A%VzI< ztwfmh9(|EBOe7ndcTaB=q(_rr=udLAyn}kkHu>EeUc^0vc1rUh(WeSubxf&V)XORw zw#k%v4J~=S_)F1PGrAiM{7Iv$dlby+$G)$9MI(jW;uh0JB^q_oeL45oDqc8|OfCMI z2(H;2J(|c(D?*RFe@TlPq!j8&En`hD%`y(C-Rb~gAW6nuT*%($+LOx!fYc33Mvr^QKDSz+#nMLz5j-AGmXW6dzl)h7%jOcH>o90pkb65&T+VczUZ%XbsZ3$mY zgKKBw6caVf1#bV24lWrxGyCP&OoQ)DzvX&$5r2bzv#D=a?!?TQ2JJ%EPCS!&y4ywH zaK3@B;X*o5zm${}ANqA4!pFKC_ykBW-3C08w}%*A#sqb&nl3VZAB`vdFc=%4VdXPJ z=*52POV|DrE*oh3xrfr6Nj=XG2m2<&N{b#R*-0()Ul*SI@P~Ec`KUP+6%_E%Fv-+Z z{dk1Og|&D!NKstkE3pg{L|kZkjBRXQvU4kjd+-11*uCLMoaAfWd&PFYc9SJn^Ajkk zGX+oAr4c4SD~)8J!+5KJ>)csmnH_!Os>-|wq%rN<>$cG#GA<(Vy++id3x=!|2u|3y z&%jY1OgsZCOgVCIY_6OYhb*dY0b~n7^QHR-oD4NB*pYtwCs}&^v671Za3A>-%9M@5 zYKF9Wy107!P_7o`o5as~y;1v{S*C@26=<@dSU1Us%1BR>iR@sqDQ=4gP0#}aN+dJh zVX0`!a@=3V{RLQsF41TvxYOOvJA0|J>BCtHcl#|Kbyuxc#Y{6{5k3P%cR3syoBdBZj0 zV|F>Eir_^e@y14)45A<;o$A5jyBK1y~erfuD z_6n-H(}pCfspHmEw;kSM2IcMn~~_LW`= z3T0Vxeo<LIpz!Z;l4@4O?Dx06EDDCtJ zWPUi*vR>eh8!VOwn?;lGZh4aTy{}?fE>G}f``ZeC1qXPM8-1rl;uJ+x(fs0htSZLh z6|a|*S3nBo2mZXuXY@{wPxQt@vclUK)mv92EI2^z=!GlH^>Kw_otvzK`ZpHj_6{eg zzcB?=q`tqr8m*-MG;q!7{S>#FUXN?<(%B^ZN&i_c=Sy*{xu{d&^a~gds^kh7GdWix z-(QAJDrAkFdz3!1_pkva2q%)#MdD*u-?IjJ0w_^WjwIL%bib0yN%+-oadfUyC=JXV zd*L}L{E6LUKQUd*TF9u{<2RcErX#PK=k%v833IZb`qq-mRPfsj2W6eiu zx8i0*qa3#J@+Rr)Tpz99T0*-ea{)m-!pdZeJtyA|zt2Q8k3;D$97^{ebh?K9G6WBZ zCR{_)tjojW$n|~1tMmgf<8HWx!pMo5nzR=cw&c9ekIJ>Xd%^pyUQ$CUt;#g}J>)C% z4a|@%I)BK$1?N#Vh&x7em(3-A-~9@aQu{9-fi;0y>x5Cg$9i4I$5Z;#(!ol;G)A4m zIaxK}3ea|bbTduny2ctcaEo=kBBGDJ`@+hbBU^&E+%ODw9{J$zYoFjdfRY#ru{IsH zl~Wp6)yLC`iJNS5Tt36p}HvNu#RX6f-v2Zo8>abo%)s{T4 zD{qp)SfR(CLOHa)+hX0~*|l?ira7G933SB(Wtdz2i~=S9emleZInUzFCgaPMpR({*5a zOlA~V(>HyLH3AuvHdkMVdN8yTp*qc~7tx92ty4=|{THi0$D2LtbT5ot07kPK3JNpY zs5tE)_!JLs1*z&D87IOm<7$Ql2fjurW7_#k|68OdQ=5*%H(0=T>;%e*LRblF`g|^@ zlMrLSwL!b7J7BO0>i=Jlyos5r;H_)DW!QP;g;DKjLMza_H?ys=sF{iD-M2Zt;E`t> z9mN?aduImYc5i|vGo2WdMMgAcFu0&>wCR^i}xe-EJ7Tts*cNVsPmO!yd zLf$7EzcXi1L+eBB;PnuVF!*cqFI8%KFfx(3dQ#gMxP9(l_5Kao;x~j&(;jZ4Z0(>^ z$%`Xjx%-|aW>S;~TrA;jM!q;P?VR>}yRe`7mnsuweqe!D4M_?zdzS47( zlH^uP)(5B2%Nm{=e&b$SNf|kp;QxlBRo{{t*m>UnC33}%JLTWuff=vRJtTThs=(rB z7HN3ymwE zIc{ll8M?Zw5E>okmNY8(8afYMxHvj~ODt-^;c!0qw`*otS0|fIgQ{pkI~!(BZl>pI zHC0pjld&6J4A8I^}j?Z2Hdu=0G&m`ebKmUbJUpp=yVKe)6 zSPClb?^CnL$sOZQUAWL?TqkbKjL_|6gE12MiFY}}C_vj91Kjd~yT@Aym=(kbC8GW~ zsL#b?1&XN~o0f(p|`_5#WO24k^9RYtqq#?O1~{6{Uv z7D3DXz-CoN*RMsFm7gU?55s021$urPIOVrJ?fEW+33+s*O;Ych4C9{I)=$=3kQMA# z-E*fPwz=wd!?|BToo5kEXNZaqf0#w83A+-;Ld=c+%XZJF?I1v^fd^!&dsCHHJvt(} zk8YNgOz<@Zrr>f`et!|aJxChCg*=qnzjbt4xm{EXNDM-j$JDCKa_`W>Z>vK#!{;#B zSquY8NXB0EjB4NK9Wwr(oNo%pWsl>+Sw4-jrcV#rQ-VUu%zoouox)rCWOtQ_THIxB zmP$au+(#yvqTELgn ziA@er)T|8`>)@Nn?bYIvN?X_H{*CLEJj1WK9{oTlZ@f85zPuO(;cLB>)h#4< z2AzFh79A7jFU(`BMj~TPAjEO*OMmG-w|}+uK0yo@Z(Ce-IsP!eRAE%YreyQ%?>qjs zQ7YCe4>*wx_&6=u-6rh&6!*bqE50c%qymug{2*9Zh=N&MPM+p#d6-Li{ZKY`3>Q#0 z)~1O)=CNSYYZJb=dE1&~49am*SOi%x)C-*E-*aGtN3tviX*P4R;5xW5jj!O&Y+koj zUT%Endky@rKViz-i4@8|k3Rhgc{zA@Z-n2~<#*(7+#_(-n?5bGocSk8$cwP_yQHvR z__9d-&ABf=Nhr7OcWpm+x#hgfr~8gLgL&Ne1iU6(OUJ0}p(ro2`^x*n7WV_68a6lb z=X9P>B0n`t-0ohJ=yIg8(ww?o^krpsmcNirY=>it7t=P(*Q{|ToP+y7GvV)@*>q!{ zz0|;3t07^5ljC4c?xQW@-&l1u{K?9vy19U1SAB19Z@H0_0Asgh=I)B2Dw3sA-iOE)jiu(#(EOIO~_+TG`=G z&Z9`1JrXr!z+73P%W-|77j)KdKdcMoIn6o+{6uj;w@>-v6@U?mX8y-r2wEWT1s}WKhenc`j};FSV+|_DkW+W9tWyV?)Vhx62LG)4AJDR_!+j`}rZm*1<^ue~ zFs?Is(P!36ozZ7cu2aX@X~=O0Fe;{nFD6jd1+e13N?Ak$CdgAOc6=Ym0!Z`E9Jo8Y0aH*a&>3OZ! zQj=E1>r>K>Nbpj^J(7&MPIE_Ri~@dt4xE!c^|^^C_oeiqUwJA@JS{Hvv27L6 ziowsN-vTw9x;cf>XRCN>uXsk=ZqTPb*mq>zN7z5;gO%UYW{Zud{?+PgK62QI7G*&s zcn+i+{e?;JfvpglcdcaJ3ghNFOJ;Sa_-2c$SuYsP6pyt47ppah`G@AF@DZ8LFPrg% zqx6p|*rG?|E;EK2RW@U`p;^f0F9 zLm=6&djaHj=2>^{G^!aoPWTPqc35{1yoN`nU5#W;^&-^Qm){6aU}k%(Oz@_oA7XX| zJWC6HJ-70{eG>F{MBdW3$*0R{&~+?e=%*{g??c>TQT3_6$wc!>rWT(s`)x9@dvY4zsXxL9)J@jxw{LtY%??W0>;P2l|1|4Q=KZ%>-xXY8J`q{T zbkIz%BHVtBDLAk_Ois+uiVbyoh@PPcM^_cf9mzy*qt50Pa?tOXL@_2#^dy}{;m^@u^WmfF-Z=B z=)VNYh3JZ`!rewK-Yf`4qQvNju^(Frx!wVC-Aq!Y+3~*pS{W0*r?)#AM4s&qwruW6@Ptg_@p*>{BEdYU;u-znpgbiv zMsyRY!wqEo$9IUB=ZzGihleRbL_Y&CkEsf**2M#?u6oSPrP9KX1=#u&AI%M0Vnq1p zVtUt>@wih#E%dB~Wy)wPWa1I%H>^7dtMBI~Jf>j?Xo_&NPZT;QyR9~b_&*~mWzs8Xp2qyecX~A?*ZWU3VVrGZH=kMQHLT>$c}F46V4X;`Kmhi4LYx&nt)_ zwrubdNKktu=iZ`Rh9Y=FC|PMYLk6ki1;fjq(2qJsUd6b`ij^4x^WeRqlkms?TKcKc zk|aOKyys>}$(~6#UcMLf{KVBF&+64>oYz}w$WDIX8YZ;IZL~3skiLHw3Ip4e?`(ux zP|=x{gEO3fL&_5bi4r>@J$>t4&G^gc6@;Y{N7_haDaR*#4q|F1Ri zgSaioy$Lgd64US6omG_MRlw77kpV?7Os_?#;g4&0Lfnp<%Wkb@*Rw-LNne|O$C3b9q2qrt)6Fy`xg9W(e@$M3V^8GaUO_P5wPi)f_Y%RR2g|9@(Zxl3~ogNi^mW=A( zJEUY5DfoHh#4JvTWRE1eT?VpuLZ+Ha&W#5tzV3YS-Go(Nf@sSQNOZ}}jrI245-pvd zBWC^r683p{aWdD$O)CpzST3!PSFJ+jzWCXQ5Z}#4oOAWV{uHau*XL9~WNZ4NqR%dw zN_TB-!zdwBo(maI{WoE4vGC`e;#NDubI?ai!{s#tLnlf@6$9z=qy`2CplzRzs?>>a zptq?pW(uD`+;Y7w{$JcWRl-^oBmLA{w{ej0=Ki>j)dE#0PrXB%4&X6*NlvUFWCf%x zJXw7#XyZOYWa3v+Q4TY>-1NK^hf*=7|Ni)6m&?lVrxdv45WF8CR87d>4u4bdAA<^{ zk{>f$)EaMnBf3ub5~RI0Sugu|!@SYwIcD>tqGT_~(o;|q^dife;Z}3SCZF*+RD|es zu@SYSlhfoIpIfQ!lwJGlx`$sp|HZlAekKdtf|$@v??B8tUK>ztg{WeldoB($cD96k z@>_CrM`$xfY@oNdmoeC`pQte~y8H|)x>A$&pMbR~`9<6)2MAYtP*k7c#BP@{P~ITC zwryTxjg7L8Gl?)CrM0Q}Pq6rg$nWHd~f0BfjCNnEqS0&kVaZy<@vAnZe5|)zzSdZR~=* zh3oR-EN!^ykIIczdX?yHeW<$JxfX4^@ThyNhdf_N(V%L7oBbNC1oxa->9<30L;vMf zP*9$zJZ?<(5T>-ZKedUXH5Ob`655xnu3>6t#|Pj^9?TFN!=iii~J`G zFeC$?LG{ZIoSouc*HK6}#tzJ;q7t4%chi`MxENwVYN@P$4m`HaZwH0>8;VvK^vAYv zYTsu+3hwlQz3lo7|0@@D(pa(X$M~%y^=Pif#~_>A7fy9fY7R`PxFnrZg6 z$PBMzTYxvsL2(F0tGxq4NcoEWP*yU$E*&_TTPhufgV|jhBzIL^>(^yuL}(xAZ&?kb zT{XrZt$&&3BE*~tonozS{=IMiKjzLVE~=<;*LxVcW|VG_Zctic00oqkmIg%$k&>@~zYh{Xdc@{rV(nXJ<#4ul1ox7tMepg4b0lsn-6VYuX>*niy=$-u(9b z5k&EqhAfKQVidfd_~9rBPvhP0Edk@Xn-dkYbw=hMWAd_c#{<~jflvY#e-Yd%u$P3U z)VS)FakHmWC0JTu1&TKm&CQwEq;uG*uLj&f;GJ~5!jc7Et!E5GV*zQG*?<#Q%N#F4 zGu3F?J^$X=_VcioYZ@SD@dk5*m!YL4i<$cmasF;_;{zB9vl_v6YB*%<>LpG;_KG9` zsdP9SA3Vos*K7{t!?QHPK3)g>ydQuc2fh*uGyM!m8XWBHBP|H4($ndI-a0}$z0drU zbtUrt1LCrh7Df~R^gfahpy{MA^LSqAYx<`VZk+6@fAM=JFDjkl4eebat9!0OWVl;k3(qcC8%ry z@hd7o5j`{5A;s56cT>WhaSraQKEv{)5hDysQg zXBqcIndvtV!X!?{@*8VTH(g%-{0K!XrF*C3s5V%2;2@Z&HSoz9y_=_JdCdqaT(>4g zOYR_c?XV$^htT$u{75DiO7vkTNy&eu`rAB~J0q)|{PyNK>FN#VAeX<8H2!rC8jIW|rf_Mj>!rv?1zN zDlg%?c|bD>e4lZKSGseEq2=mq6T0^>i}k{y&VkCOQydJWYACuGkmAPaf=lg{%f?+l zgp{)E(#Q$i9VmEp7Cj7NR3 z?mqqXoQ6O-g4{fV^FOaoeb%&lmuqbHq)NYPQIGdD;C{BgQd^vb+`hBEU4MULH?gcd z`p&cOb}fx?n+4G}s2d1DtERPZtuTH2yZXWFrOSZyb9K{il)---_6SS*7#2ME{P%7a z(NcWY<0WU4^4`sMpb4>%Diyr#@DGR%o%O37<0KFC4n)WK&wfqf503M-%9%*f;KGv` z2?0U!$of?u-vo45vtE?{pA%AuHD;zYmvhB}C}v_V0LKFO-a5xbjLgcjn+n^S8>=oV^q3SGUD@=NJtgeZb*PK5;(>RqI5x zjXW2)cB>Bt;wovt2b>!l8~eihE{A6j(H1cpe|xJ4bEWb@Negw7`xEROrI}K$;NeRc z)qV{9O?L%^$an3HuZdt$2w=xsSxRE*6~!kO&<+yY!R0LD4{jri8q1ckNc0Q_s+Gl3 zCbX@G7pK>D@WJ`gxVZJ)!=Ku^!Z+|D_o3gQ+(D<33doj!2_A&}$?LF9$&&={jEi8f z;D;mnKm0Y>U_9|2SjI#c7Cfi$Y_KgLPV9L*>x^}1soF2W*2rdlK0YO3o&6+lq=RZY z0hk&aMzy7%@T-YSXT5ju_**?G`S9UG!{Ln($@?A3?$}}1&Q>ZH7Qh~%%E^Ug2qdaM zo}JtOHft|i(c2)R4?lx*Fs>YdSgs!7Ln(iFh%7ZatVH#x;C{yK*EycMa~+$x4_j2i zh4jA`CyfQliXty-NO7el{AXQ9wZxeRe&&8N_L}>9CX3~a zIST~Q6}L)e&&DN#RMtqOczlPRDdkZW&efOiGAh${fvl<}hKd<^&&WzgBbyNN z%KtPEWU`?yU@H%tf$;Sg{Ga(Bd!A&j9#K+Kg|iV7#qHsX3mmhlElV5F7Uj~HhS4L{ zc%pm14mSivdNo?iXe(RSi{lHs&IP9 zXLkhL5C8*vi zKEa51pCJ34xU{f3@D5YtWEePhez$|y|Ds|NhDX?(w#w~vOJ7%SX4Wvd{^tgc73dsi z33T=e@b>WfBc2l2>~a_YKC75_HIsCB*9cg<$2@yBWy15)g$dh(T2AxYp69z2F8(bM z>z*T=#(ZSV=8Ke*fC!p3BNSdI!7wEL<7@0ZGX-ApxA&gSWNDbltFod+^rkFy8{2*9 zV>mJ58Pk^Z7|gb{@Gg5a=P1=6v6Wg!E)4yR9yzH25rsb;g20!W%q+X+U*(AKtGw+8 zfoun%_;`87Mq&12%MOxvj)o6}L=JH-BP*B4>YwiG^2XK*f)k7S%3*eP@8yp676DF@ zUtgkVf10Zvr95G9|0RU3rG0Ja#Hyn!!tz1CPR}${-zwz}P!&Nf1fKpU>T83_{d5=y zWxgEvsqkLiF&0u?8~D4ljr5h$NNPjunUsjrsNJU()`{TkLxhc)oW8yjTa|DMZd-5d z{Ztf~Hzr^Stp2jOd3*Y>cL8##FI$xLK#J)9a-J^`iR)ZsJ3Qnx7=UW+pYCNB7Mi|0 zNg)pCauCKNA@d7n$)vsv1^6ui8+7=BI;cI@3%emqdF&R9O!!PYNA@LHIxbC-XSc)j zPb`g^`ysey?F5gQe|Fitt{vF=dfVthf*oca8}uLkGkw~{HIa9I#&zhirxhqLxJTc+ zOHZL6xL()hpV_8Rg}Gefesib$&aLS$dc&f}?}eF1IQZJNb_P$b#4wWad_t8rLYAzV z23X&Cf4CZ@z8IxvD?$M~04^hpnwHt&9ZM4S(fIko#Ilk@G(43cA??9z!gQ+_ld^jq z|LDkrAKf;Ohdnjxf_2>7e^okW%PsCR&13j_uNj24(FwQiS#n=oO3hc8Uc~1RbaDJ0 zl;Xu_2EaqYU?$DMb+;g1jIe<3(7>%qK5JUu65(`Gq?vZ{XViF0oNutbd+;G|lXc~> zXtheJpwwf!TdWnHQEXx=j%WZW=gjjJf*q1%|B`*48* z6LsU>N*tf}U;5_v7?G6DF5!2!^!={o1&=waKppzir(@fs6tUBd`dai;TI^8b{41&o1R4Rb3l%@RK z(zO=ld8uJgcOW0!o7_18{wO;Sn%SP@&;1r1i9egxY-AdRcg#Mgx@;?s4DuGaR~?lQUM=1a2-ae9L(E@t_D2J3RCD*=*3 zCwHU%YEfKWZk%ki4Y>QZGG1R!2W2P)zyuZ=?oIoY(IZXZKjXc?+Cm8Hr#++05{kH0 zW5b|^ZDETm%(;z1N~H%G9(-UVKH)f}y4uv9|4s3g%zTAmV8!Tk772nC|JA-Y9xIsL zZP9KLnJmMHzJ$yWPg%c;rcqy}-R{0(^G|jva&Sp98~FisxT`^k*y6#-zRHnUASzPD zrIZyeie1%hPjU69?9*gc^@j{*hIpjK zi%EeUBTipT6}#6K3@y1GU>RV}M(aB0!HCJ@I6TsAC0GUHfIs$UE7an=ksB zeOhIAS`1-`9|SmpvI-jD^}p?ZYTaqY9hROMK2(d1{;241q*^9MZmAA^rEz3rYT2?| zzYgj1SUxi${h6G4I5r2aLL+WmzGOq?6qbruW8kr~yfe3a5cyxeEJ5OA0BU|6Swc=m z03XqPt^Ad%RO9jyO|#X{&2FGlcBhpxwrG~2(Fk+;Q4C{Vi20ySSc*89d$j5kx=bv6 zohd!gyh==?1vXPMI!nGjKRJ-sCV;*6cvQi=v@qa!hncmDq;R3Q-+igIZ5{v>=UJ~v zSZ&&ElJIK|CXJxZ5BSkQe25Yy@MlJ0KPZ9#+4bk2!aczU!8dWRno>hm0929#;f@)5 z@95HCfSwQfXbG}Bw|T_^hjJ}$gUWh#V~yE+7Q#n6jT|dM^ltJBz@<&(nHL?{3 zs{q}&PvOF*QMm2wpT!X|1hkjs&85lJ8icjEG0()xb66sg3aA5=Fj*_tCQ!iswIT3! zaMY0Y^UoF#{kG4Z;pNwN;D{(JQ5vRFisb_MayLXSyIv>(D$E6#L{0!oMkB}X(Ndzz zeXmR})lq8OR0ufi+E_b3K`#a1pR}gaB005Aw@e8pO2Ai7Uad0$4DG0B<20F5-pF;h zYq348gCfkYOQ`0qDb9Z+5F(BMIiQ$8t*vrK5LGFlMUW>U4%mii9+62}NiyEXn>(7A zjfV^05p|lXdjl5naV`Z=7+D5r+UzftPz-0JrAoQ#DrLWqKYtj_;ALR5HCaW}IQXUT z(I<+xG@7I=^0(MdGQr{jCATypqE#%_y>y3@LsKXI4)96&yD|962{(`yed|6RAF6S) zgGx=-v z2t2E&PZE27TilvE8!ogHC zHw~;}36Q9__UYM|xB8NZ(a$Py9B*)x)1{BG@*4^i5Pnp?vF4gPk?;#y`)%|eWS(SI z#IqR*cM6kEhZF{Ve^B;z|L%%Gu=u@~8b*LC#VO-vry_s9kQ8_(8r)WKb%-=6#jLid zTEFtf;QVNxD9z|qXN)JflKY!ebKT|x6>+DMS`TO!WsOs)5XY%BEA6bhZZqJz4hoA%!OuqJ8!1G>y)vgj|j#)ca%MygX-(usqGjdnA~y_c~qjfru+)B z7T5DoGwfG3#m5IkmCUJC#`}!}Wl-c4v3o6!2zj&xgKEUuduEq6G^;j`?k9USIw)gA z&#nS0d)n)RDx|;dB@^G7bw7Bh5HylCAyI(#kB3V!0eCnt9a>g}e7$TU2GBJ9D_b>g zXND9367c{BY)wo_QV{ra!SYvSKX!c_>Rv&6a*w^1{2puRbCiM9mcbm?Z=cLo&^nQ9 zq@JqL2;D{T01w+rqy5?DZ(F>L?EuXbWP>$>&|;d&r$97$&=X?TQu9n158{|XN_W~w zVqCgI#6L;8JNPU;^794`m@kzeJfdwU{J4oM`i~?%%IKaj+qVq>-|_jcYa`#qRJ>GM z(2;JHO7emaSh(!>7>W7PBvyN~ita0cpi_ccx+y`_wELj!59pb-%R8FFMugHlm>ttGzzEyGJMT_F4w~CZV~IuB zM*!f{Z>v}YxDJ&dR`8`XBt{0^B&;Nois`gwuFS~^-r#L7APdD~&ZIg0oQ6b= zkSg~v1On=j(!8hkJA?dzU{DMU>T76*6m=w$9RzKWbc*BIy}jOSH0Zq2RMlp$1^3=B zT!@j|-ED=RcDn_9 zb+##A4Qm4p*wf-@Qe|7vwOVnP1P(LmT!;Q?_L>!l8LpGL`OTb|8*IiQz3)NrX$qmA zSQLXJ@{Iobgzkt{AlvwQiKSd#0mpSPZY97BO3@ueV!FM9(~j|S5c7!Lhs3=4@7%*! zGe3C;30LPdb`H+MINZpy5gM|C2B=v;{ruVv$#sD@<7cb;{Tam9X;cK4_uUr0=VRLY z$@mArrN!s*pCg6s^-Cw&PNIUlOmtuoY_=q9m!oPhug5yTXsf`con}*DW@to!BJmCH zh3j1g_OUsvGw(zs$Hr2OPvpP;y`R?ONVpYBQt_ zQn2z;&UG%}>u9Gq-vkBVt?GC;w=#S7Wdy@_6Z36~JC-JIE9!JvKM$JgsLh)TOPEl9 z-V!T1F!PJ}4yoTrr*^Btj?+|a(Gt`8CDh;K0)(MVZE#issvG@2h$%WpGQ8$9wR<*2 zorrmE$}j*qgUUvIA-)W^aNaGP?K7JrXF7tUF2)ZlS&S^dxDl#U^-*ks?>jT{7tG^` zAoba~&tcQR35DH3O|*WYzydGP*6=k2kE{`!&As zGT5BZL5j3?l;KMh)K1?uIHx7T**mHwc9rlH;8}GX5T*uKFK8OInnTSFO*~pm+%Yq+ zoeSpf7wG7P<(8ViTafC0&l4?9LL5`Y5^QK2H3>ot6}fZ$Vka8U!9a zq&r*j6u@FcQx8n!$A!<~dvdd`zsz^n9Hq$~91*mqr&bDxoZv&a=(?m%O4^ zXxvr{4#1s7cO&l2!cjg23W)dT?-K&Sif=MNZg?u1;Ql zB}WD_51HRA_-9Rz#9Gs}rI)`2?`x%y# z-l?S-H}4Je|GC>m(YpOP?eJp$L-^RUBf}IyQ|ix$z`jMRZ|e}&{;qXo#I4x7L&#l( z{A(Ptrmg=oUML9Yp;EjJaCczM9O7%Xw=MW)ON-5lWfH^Z8JB(+n8gwH1n^EgeV7~IF zGijixNYp*QGtMwW=7-U+lCo&nP>SLEiKbW5?v7wnsf_v6%FA$$&1lXGrVLMQt4FP% z(EYbB`>XHF)KtG9BEG1ArZnVZa^52nmTvI2d#-=aC!rA>Hb}l}7PlYJ(;flY6+wYS z`?yDk46|h`qAC+7qjyp<<`&M#M%xc=XSOEu>{!85XDk@HMH!UFfW{6`Ef1BNmvyZ z5O-ejQIH0PSau)36Cy};RV(t%oN;8vyQ*HqN5!qgLC8Kk zt42cA*5z->lh#OJ%k=S?os}W{m;rYR5CCxPN^ifLjML53# z`N|c3*+^Y`+sSHpds^8K8pVsY+I!6R-;ryIzDQin*f80PWZsw>RW`bGT#}p|%caOB z(R;G1?j}m~TPZOZr>hbaqFXmm-d1@i?p3?=qvz|}sy&?A2g5x6F{H$;TAe)^_eSz) zU7D9pRvv~OW(GsgXSv6QdyTmWK!`q#RvD~!y6r#=zj5;Jlyfbf{6?Nvr9~vq4~H~}U3FSGe%r08T;uqP zDqJ!OIR0(=EG`SKR=5OTgPju*q~_K7&11R*oK9s8t^8Kp(?0qs)2AoZn>e0vpNeIi zXJlIh2Ox;r4?1i;3}5a*Q8z!9Cs-4D;ptipc~Z$rn`+za=K~07Li6(Rkyo7>el)9o~R`T*q44EGDlulMg8EiW}%;0E3wxRrL zgJ66-MX0beODbr&yktf2x47p{O|$BE-;X&@&)CUAotv)10`*q68($4?wbH~k9(5l? zSe^|33axl4u%VE%mu&hn*;Qwv=Ix?*ivV3Z;EkKE-Ven=AUd^s>>!3M;-J2qN6tqZ zY$>1SW}Xrorz+Tz3EXF_d<#Ff=$WJ#SlEW>s+c4=;`)2P-#ALZd>V|ad=GBQ#YuiK z^~XDPtpM#zc8m&F#VH*1Cm&_&jXVc`-|;)%_Is-IgT#%6ta{kC-aAHc{xS_bUg$5VC|JbMC>~E zA+=gnClo^-_s%D{qCa3;3ah-H@juYanfQu^?g#1zr2SPLsfk`QQJ)X}OjCdnuHN@c zbq9G7-lS%dN+0!&(*|0Dnp$E4F9*sf~Jqf%XBCe#3 zRucj@!dg@YAnCl$ZYk6zQsI?UyJ4YZcHbav)vl)*Sbx*Zh}K;v2CL-c6hU;{kh!Ua z@!q^)z@N?t?+El@$;D?%tXb#;Z*uLgV?CriUwncDH-uNX@PcE`X8XSm=k z9-4$5ck=a_K>NCP2pcY4jP|6uIuSg#@ibd<^1K@qHeN-wdV-M;UOXZq?*1EKRkg@O zVwCt1;~xFqF~FB2G%vwkhO7jlY~9cI&SboH7(3(q7G{u^K7ZwcNfcHt*CSyn77&Gk z?e3#16Rbtpz=xq5XWw7GCH))8E2a4)Yaaid7r}Gi$GEBg+=91UIl*%$V3lx_ulJT^ zrqtJ}x)??)Y2n0bNXn6Hx{%0TFkMP@+g>JVmVL=Qt^llhmz6a!ei z@!3OXmN^P-XBvyy^#ZRMl@b!HeEV4*0h@G2R>2&Nkk_Z>FjpgVT&4o|p}`=`bOww4 zkRokuI(W7WK6z+a;*MJ&Ww{PMc-tWoWR~AYGCW$cjULg~H@%B93^>Rz7A2O`Q|Zujb0A20ts=Aer`U{HI$w-TI+#0vGRgG5+%U5^A^ zfw-n^GHcG2p;?18sq%S=K)WV&8U_RG*$SlCA9 zDm2q=^<#?GL+315^5s1JX`@}@e^TDCbMdk*Yp5HZJ zLzRefB%~EOX9!RK`e3Q`(GWmjFmG3}kYq@<@8^<6l_Cjw0;_=KXAkFcI93*w+IIJN z$`;kKG(VpBq-)XUbEdu-6)J;J6+)R)M4~LIah$WpP~j>7;Q506>)IWvgady2GtZdzMSL@Bd+Q{8vdv&yn7>~QhiG6 zOZgNhVk%&$XPa44%jpF)KhDQI=0GZI(?zb)v$gbalGiV2YfjF`xiO;5*0%oTd?xM7 zhl*ha%xQDIW(to2+|!oCO309OPj9cYC~=)8^s*PM`uf8J&DKJNZ=%{%C;iSMrv%tQ z&-x_y>*qwqeMJeTiAQK*m)Y7fP}qHZv^--RshY)BCghsQf?T*Nl98-))kajl0g){_ zfcIGg{L@F`xoUBm5bp$lYB!H0qKMovi}lCwd+nOr(&C-!_6GKIaUVXas51=5r>Hol zm)8x(IXLT2k^m5cJJT@ui-+K@06v2IkhAs&QQ!tq(|n~Vg=r>ldql5pC-+RuLY<+`#ob6lBN-6c5 z?E`m6pWNw5m!q_NZ>zXCc*TTfGhCIM*Wjlv+j_l@SP1|5i&RU;52PpP=%{=ROLLMq z9hyxjfc_&_=V|tnjffWP@gfKrt^a-UK~Ut|?`e_LVmC%ybGRGx!TxS351(4|gDCLP za5R49tU+6VII#L_F7dAQ@I-@Vg(mGc3~l>2cR&Dy&95e+|6PJiF8Y2Ku58p9KX|)M zD~KOvgAA+G#b+AjZxM^Yr@UxMJ#Sj+7YW#c#JE3Uam*xxfWeQQ`(5U(gFI_$wCyn1 z&p(L%NLDVr-;nIjZA(r#!CSzZqE5j>e!$ix7^m_+sgIAmK#R5j6VM8m!Q3Kb{frfC z(*jZlMVe&9g2_J1L<*7Id(voHc#{=Gj2NMS`r@r2wH6UozPkKhay`0uXN4Sn;f6Hp zbpxH#)&Q#@Ke3=}gFi&=D&Fz5OypN8jQR?F$(T(N{oSYSd!r)rIanfYFgFw+`%hAh zgGf(ia)DHPF+S-=VyJXrk z9J+!eZou>w?n^ho{*XGpCPb(~ordL2M=rteBhpZ_N2}V~c zq2djHWC4pE6%QbN+Ll)bTL04!UI-s`kuvR~IGis~Uu<}m0c3;k7o0za;k?mL0XBYx z;c{lzC{`yfT4*jQJ$At`$C5T_EHju!1eML+x z8M1%#0ZdpjA0K^q8S3qe>UpwLGPM#0&QON%A^fM+=JO`QZUos%oWhIKUAR~ty>GQd z>(6uZyGi{|&vEJ>0G}(DVd&&3s+W5U-M@8D9)n=6%vXpp2X*Ppv`cmwM8r<~D-+Uv z<-2`VKyaQ?5UWSl{cVl>PmCZR4*mhLxcQjLd4SBk^Cx3P5H)6Ps3>i01@Z82zYo_( ze%0+9jmjl|24Ue1yl`ek5$6YgETD4ut_o z3nBc>(^$60Km7d#@ZvY{ZU+7Eaa^l5<&w^+NE&HK^RZZ)=yDF(k;1!Y&p-|xhjlJ- zTZZ9j;<2RImNfU|x^tp^C0elBgJGL4#jj%CsH)^tkXn?nOn zziKAajH=kzNZ2piya6%db@MI}HbVhJoAKhZ(_N*(|L+{$iT1N?0YsqlnYc=&D*H{D zsK@rrDsm9ZPb{KBR;ZPdt1&%;hGRcU|0vEJKcGaM3mP%^Vz8raUTgPgd+S#XzjMQU z__C%w$ncdEgEHAgIazqJtME2?1o&&-!1YTXb=1H3Reko)**!KdGUGT_Y(Pein~1Ln zLhQz}3SqOLG@E=b4U(5!2|iuuL+841z%M<;5hGSOIx`stDxn?w|K5VeF8_F?B;Vk~ z+e}cQ@I#kY6y&VQ0%(LSTF7>5x!m1C`u}|&>9&xfvU1mEW9K>+Pp1@r@%yurr9=-_ z=WTsYc|L!K0;Epy(Qb1}-I6pSau>T@b^jpay1KfKnn8I6teZI^dX?FKwF)fz`};1CI8Pb5rS~77x6DP}T0FQJ(3cr>^>=Q@4-*hk_PB-%1XNXqz%{SeB#MYa ziEw3sXgIV`bNJt&7UkX#2f6d3%?V8Rw}0Gu7sV3&uQrx2y8m+L{2=iR8VY0(g&AOO z=uM-F5a!nsmXeiNV`TwAjvg3VCi{QMzA4p^Rx;TC;QY^I=!<`R7zn$w;5@e##A1^F zEZ^9K%u^)^GCY1UfY}55%`aHdGp&A5fjXE^Y?6{73WM{1Fe-jcXFOORaSS7&#k^$5w#7hGh_pny8zW_MhpSq=Q4cJs)YwcxZzx zu<~o4B=~Z5hh$M|>ynvvHj#|f(r1|p3#k%AjFmdraggaJCExPJxj_0{HF&vWC)DOT zsT%esz_=7Gnb|AVBYj1?Duam&o^NjkzWk^2fGt!eU@(qPB&SMAgb3Fi$bz+=_zvCe z31+f7e?8f&>-m1N_eu0boF>!x&(TIaa>}>|1j=~U^1!>~oCyzB2l<{xvo35BO`cG9 z`>1QCX{^LZRas{^B;8%+P;_R2KT1=8NW$`GVcaz z#Gy6es{r`dD@I82rka0M3#6~n>=PAs`D{K6WMv#cJrTmhA#^SBEdDI7WaW-_V+kyf z0(f;wRfDesc2pJ4<|PNDSGUTaW6LBSjf2{Dg+AA;RpWtqv+|C*hJ2Xv1iwC9LP7$6 z613*jD+z3BCJ2bw7sjGV=F5N!MqA{IcMshBd+#AJU*4r0+lS47YFBBNInA`&oDHBsJS{ak zf^r%(bwf(@cA7D5b64jWm{Fb;7+N80E%MuGpAz?#D+ zt8LYH!T#xa;`!HeAph0|xQd)?4(_up?KybgXy5>y{OA#4sKp}_~c z!i;whN3;pllLfF8H8TgeP#vtPuD&fc!Y`XKJ(Y5Wj3(PPPIs*WQmx zoSU6ih_o8cACY-b`P*1FoGwHW#DFQI0yMsM99e%bNQ#5NcG%)V_b1YhuQT*VM@Nt( zd)8%eKcr&&cm2T?B^B_5@JlaCknG{j)$eIgvpr&EcqXX-Zp*-#FVM@`BUfs63 z;-&)qGYM=}*h>pwgOd?A|F;Z8<4H|RN=hKQ%Y6y#@?jGMX2|I9CafwWi?hBS0%rAZ z{1bO)*V~V94R`@YL4+DM$@gKB4;%|Asd$D)^uONtB3TTy@ zHya`hoAE1P4FH`L`w$)XE>UY$^G*|gsR+ePxoJIlHt!+wF=1s>Qxp6(h}2U$SIU6q z`p`+=30yaZUCO-5%15$jO$4d&f~cEdpc|RebA;MXyqUEzf=<-t%RVOPx_LIm@7x9p zz<($yZ*r)cUM!COC-BBsaB@w6pHg@SBH&hUo=ZuN#qNiJKj1fu703s82Y{7`rm3I& zR??pDo&s6EzBoeJ^=SvnlLiK5iL!H_-VQdaM^fl@=f8=N5M7(7V2K;!_zpIVAin_> zC^WK^9$+EP8sq=+txJzntmojJAHMfd!H47e5sqjYctze@vot*1f68k7_8>1%R3FQx zj2~peTBFjbHK$q*I)P7GF`0a9&f?58Y=z}K5EaCOE`t_Hbe-EW@!rrgJmm-ry7!KB-T|gtdOBzQj+$8gEqrl9!K|7r_xv`lPuzWUl0U0RcOt1` z;SA@@3>f8th=$#?uSu)Tnk7n!fU$gjcDVL43G~9pgv~v|Dh=Kd1!`3-E>T`wtv1hI z4#4mN#r189z8dO->K`0gE8KV3p%KFuCdZXUz(}k;!R$jwlyDo5=fW*xa2caBcpXR4-wzT7!9)=Th7YBoW1c$krTnwPy`u)M#VIzF- zM*PFLxeuUy{iflfJ+XH^RB;DV0jqd-%)m+r)T=iz6DZc-U*W+f2YeLHF66W20_88h zv7TcZLL_J|JPh%U`+?6WmtsBkJ)B0ZWrLbGHYs@B2kl3GNY~P<^OT(0$KV zLF*f|5w@Rr!IESrt2pIGn6rMDBRaoN$&akp6(gom<8Y5}7R!w*691PQUsF#!5am5( z;`aQw0;0W?yem4D*~99VS)Fkg3~a)gv_JZnw)4<2|(72%P$wAIe(RX*6%_$owlb{tT! z*TTHmAu`O6TZpq%`_qK2Pune|w!gCae&_7v<@rg)+n#lgg&*ZPGO~derv`^a4-4NL z$h&}H$Op+Sj|pGguDdL*U^xrq5bsz{EHa?7ygaUN|62n};kQ#1{3A(F$=(5|SZ^IP zjP=Go-#h=^#vygZa%Y#n%ah9;R7Ndb-eh{L8;$D-!dFDicp-NvL&S`rif4Ff!#Amw z*Pg#Kn=6Mv@%8(WVRdf@&6MSJ#`zLqqhq(K46Mg59ya{Ju3@GE=aoy^Uat97Pd@K% z6FB}63?-0Pn{mFn_$wwg5M4aR18-^t_)yV0U>WVmO>0xpXyANfvJzD-$b+AJc4 zvB*!8h5aesz1xDwZ{qI_M&&WVk04`5+^bKCVi!Wd{(&WdnP^!K%U*6^~R~kBzFgCAp8% zXYL0xPE2u3cqq-g*7oEc;pCO&8_pN>Z2jBC``oHfzndc{Jo^qpKr???H5_{M8KJFg z3G##)`hFu_gGXmGI4npCw9bb-@%vlWv)+QXu)VAI5k`F;4a_<*VXR~$rOY|j>0c|u zJC_I=g%0rYxOb@<2YiDCFN3QrSi@k)e|I5$fWE~)DMHEq=IYhx`FY6X#FsX1^kWV^ z!_jm0rBAk~F&b2x){HQD;KmW8O;I47}?>xDOAiBCoewe9ae=JQdRc=aL~LG+pr zw=s|dSDcYx==M;sPO1dcPS!~ts}eu#N$t!#hXa)YzW3uK?;g6fg1&Cn-$RjL<|FO4 zK1idV*ZKy{UaVYSsqlTOZ5=vLO?3TwUP7X3)*r}3f2I=NDs(ZrNztbxPOgZpOwE4M z8(Z$`e|q)<$9y(3`ASaZcT@zvM&rx(+=V$JBcC4UtG4O3B9K(EKX292^kI@6U%WoR z$xHAh%d`MhrOd2ll-T2oe9WNvUQS~><^#B3GWvO_v24@=y5VV)3|bV3$!_Fea^ADJ z?9WH^MiAtqzu_TiHGk;mG%~~gvEh$$MDWtz(HM8vd9;e?z~+UzOt0Ba%s=eo!0Ni~ z?x(gPq`idWqwYkqI}v~{7S?WZajNFfuwI~>H0%8OQYZ4p;INcNTXbF9l)eTPZDH=- zmF}3ukH1VIECv%p6kR3XX^>uomBz(AM@vmbE~h%y9A8N|RM(eqByTg($3s2Na2|tv zwWTiqkd^@o<{Lak!*dNS?8jAT2j$qK>I>$p z?z;}E-t(9@MQ!bRg%^>R#$A@lv5(}EF6b0EM>EKP0L&m+K=4hMTdOpk5Bd5@d2QF} z;=#BuZT7sc+aEUlH$>Lb+RC8g{>VKT+*B9pqC?h8sMfp|pI_>^Low#J5b-Bf*Ul31sncdh9Y^c0#~-5SEb~^tAw*dN*A%#%A195cM(XAGQTkv|9Rd$Q-5npk~l;I~I#FQiMTbh@zx~v*4$wH}+ zXlzk!of~0tV9g63mx22Mp$cR|-JP@AX@WTzrVODNyCCT|7na3m=jNcz+jx8^rI(oj z)ryE3N;sxc(Y7TIBCwxVZdJfzmAnbK*+j z`B86`*7>)IzUw!a?*khi*F5{l_Yn_n7_9c%SCI7$FlS2+bS)6f3!l5pm5CDokP;gF zk(-I1=}8-YNxk8EQT(fk4;6^`e2JTSH!tVzcj#^TSu7zQjU2{)a0rkb0D8I7?+0LnW&jhZiUtwON4-m0USofi%Yt z90WJ1=K=C^co|ZJ8>Mw&K<4&)2~Q{1bZT&S8DtHjq;=gFyWz>5K74bN)x5 z4SJ--uy0dzmFu*0+obC7PVL3XFVA_S{WSZi&>)2s<&LVLR*qAb1>t#2n2{i9n^eJ$ zLpx-O7kMF!2d}cM>cxQ1Otg&I?2GnDe55`SECy&XhO06 zL_HCYitV%cBRXO&^|?(uOJ4k@lCzZtwS`NQQr``7^lfmX5h%nMoCyuU8nS57cyw2j z{3dg4cfMVdMm#tRc;@pW%s674v|TGp*p#BBVpqL`p#3OH`F{NM#lBbQ38R-A+R^fT z-Tc;T;@#`l0eV}iHtmL>RYMF9{XrUVv2w&^{S9)L9`_0VB92L|b;X?S9GMU%3Fps2#B3 zH$WV(WPZ(`41YrAf+8@^h$0|NEd)qPRqBXfI{QI5=ow5YE(35Br_g^;ly$%D`Mc_# zcS&EoA9*k?cB0%rtbXezzI}0n+P!Ti&X@Dzu>5y_<4ZcsV*5@&xt6BKY+L=3PyCtO zrr*T6-{uKs+7Ev4VNH(GJvyk25u+C*>{g!i%JSg)(&vZY?9JZ~GazujTyIT;vW;cc_OEN{koC zTf3#cQprG`hNBaxZRYjoh{z}F%V^QaO}%^#O)y*i4zZKx9z_@!1 z4~|iuY`4{~DsCLnLsGn)!v+FU?oMdsleAN5T(xVLpR5PepE;ZE1l3R9sqo8Ev^HKv zSABuJ{MZOF7N90vq6~cCMfd*jD&SruYl(Nv6RG2vW%fPC*HsTYp#_t$Qykn!#A#|(T^4(%|?J@8HF;{Qi@;gR`wgWo%XcKZ42EBUS zqL6;Gs);9#SC6M?V*lVFaXu}uad}TkyW{oWO7x-EiOhgBr>4tn8qkMXo@wkh8@y(T z_IpVRe7hJ7UiQq|n|1n9#d!l`x)jjU)YV8Sl(SL+?P(L)UXOclg(#kt}o}tKXD9615Sp1m4 z6hU9K^8e9pgT1Q~xZ{;-tz_4*oLejOKUv>M$4)#!i-yT7C>vrn<{+4pE-`AqI>yj{;U;=-bTa_2cRf@*g8*$W&Sp ztdsfxZt=#Y&nA++%TncJ)6U;1n637C5zD7j>GnslzbJpgVK7AFB#j$Te;*QQI6?6;cjy_grXBKA9#N$bbyB0M(owTXCn(ayEQR|3lmxIJ z<1~(*fCU||>+x|jNRc-)ag1iqqh17cAVo8z?60(Y7xaLH$Wq`kpZMQ)~!8CE&>lkCc z)-o`NbA9J&O-Q$N`bis^}NEvoad zNfvNXkOi~ad>Dy~#<8osg<_Ma$4c z(M8kDtfu6j!t>xYM#KyQCI+6|*7IQsheDiOeLtI_29K4YfJGB{@mHsJ$#AtM#pyL= z9ZR2)?s@Qqe(9%4`-SB{ftN=UdSuVuy{oxnR<<D_nSuf)Wia!p) zv&)lERagRBgrbN8#wH)co(wNLi+R>`hHX}{PjCKdrWxxe_s&?r|41xr`SMdUxR}gO zWtx{SZbe@CT2-|NF7sQ;Te7+!<5O%1EwXpWRfXbhP|adO%pum+xX$qS8`KnmH|f>y z(=cpICjBR0-b7Ow-by#$J1jX4vH;EaRwXCBaQ#S@X!OSfKj%2+^lNp6n?Wnz(;V*r z@%oAe;7L2U2ZVlNqhfRUR_XO*TD3x7D=JbKutNFs(HIYA>$mXxZV`svK~z?}d>1Q8 zNT|o0(+OwTnqYV+7 zvlnxfuXZJ(|99)iel8-Su|CEySd)t%b(--p>QV!&Y{5Ie>1-#A7plq?GL|S%)lT?Awei-Df`E@BMh(zuxl?oa>y&nd`dF`~AG0 zFOYSH-r%4}Y!#4;VF|8uh$Fe{{UwBO<%+8)ZDX$c$bwraWL#D!s!?0CW4gPOVE%7w zO>G|&|GAuTq*)5sqQUy(Uet;C;F=s<4wk(hzmhEQz;RpSl1F*9b?u#hB319j$3mf`*~~9)5h``i}`>{E%?Xp=Di< z+fNXPPSbN#$Cr`PhM`1$l+A0GcK(Moit8w_#cEG=^fg1 z9D*OW19%))B?xt+ue&pDPpur-&hV(WFY4*9j&RxD)$P{?Z&T^t+w`{{_u)oU5^j>n zpzEH8`U7k!A!nK16b+y_n1_y^k;V@oqKWkZr$pr2SnTJ5acW$l;B06`Q&!HjQ>oqc z<$UY*)!9{wrq!ZkexGu!n~jOHxh~g;^dsoIwM)Gy?cC(gt3iL6u~G+Xmc{{J-nPbb zft^hY_Wm3V*^`*k4;N+?-+4MLz?oenncqMMZx?H`tTh1K{72#cH0#2#cV)yly6?*FUu0 z7Kv-vgzg}6)as2gqrDJ#Bhp#m$%cR-*;T%Y3f0&;a_m@AOd*f12K^x_CpnTpHjNQO z#{IumufzeU?bc61IY`tQ9hi-8Srp_L z|Eb?-L2}&85f)Tn>~QD%D=PYKwj^Uvl-!}pjlSs(38`_rx(cgX(`n(6nwy(j+B!>9 z%J88PSm$%XFP6(sN8W`KWbo-NdIkuy2G+wU@?XA!X@*bvs1zO1`gxmE2fN*#Sw-DZ ztMI=C6XJ?EPu*jY)-rv+2`Z>b8t^4dJjotNZ2o z)=4oS&7{G36yZKFD>H4SNt)$bS4KMN`qPZ9W$T33UImQ0jVaE; zS{0T3Ovd@AFN0I&CGWiE-mbmC{PGWrg%My;1_@9V7t*A@IDgHw-Q7~?^}KCa6}FmM zcANG_#;eOV++5|z2z%l#_y3WGfEN-#oj@6 ze9XBLq35D>C1|Vv8c>FLX5I2%<=JwId8$Q@+gbUw1C^#@bfTD)lL7bsQkE7G*oLy}PNvWs8 z2eP1_A9tEQc?_>WCqY=jI8nP~>X6i$9I?$GB1}?KF`di`wy#rttHS3KagyZz!z}XS zpS8g`Pn&IzXMMxqBMStidDvY2yLiJej}V`hI=-wbV?U-#2*a6eO%k&yDQ1GseBghu z0O2}f3ismbrH=dAas{wyJ05vv%O}Yt$aKFS-{E~UP>;xTCtRA6WRcxNSe~mVv99PM zPV>9^ixlD2O|hdk;EZ=G!{`l<5F|A02RXrXIEr64;`L1ai^7)9k7X&%OmX#xawaMr zv0M+Er*Am6mkFI8x;wuN;T<&?(T^73Kk&z(E3-bK&M30nf)Y-!ud{Q4+pr4Kd;^Fz zwU7|1#ICA2RWk~egRWI>;`@|}^!$A665{-c8&>o9yIxXHb0MZqAlTa+JGS=KcXJ;Q zCfDxbvrc>92{CPco7yv3@Tx+QUkl7`FRXQA*h7N|G$*WhZAsVON^f%J@OKCPzvma`ow5^rWQbkc@{=*NZ?v?}8xW2_f06*xbw|oaEri05 z=H@eV>1~kLkT3+ClQuUgtQ5djuSN7>Z(Gc4W(KMRv;nr8{Bu7BaaSzw#r8+pzR~7W z}Ga=;^(+>sB?)8O+!?@k3D+zLeK@y}m|1 zDjL=&+Dvc_i{%BN|8>w}ok%U+$_bR|SfES)moDayIsRuJrmlYC?8$WF50ybrmGxpO zUOL^|xYl6!1*Q*Y5=N-mE4puM7R-!)>Odfexkw<#U+1{O^J_OWjKQ~XK)Jk(n{=yA zrq4ZyKZo4QX}Hu$P$55Y(RG!$Zw`FBBmA?WBy4Zc7NnGJ^21OZy@64UIn$bpRr6D| zdCFt2{gh3(7^fg+(|BQdvOU|sjT*(*40viaRa!&S1s-7^1dAv)KH$sU^usOu3)Dap zRqVltRV)IZld{G9{o_4u=IQl7-FP15K1GBwHleB$aW{3=ngPvDfMz1{Ex#;j=I{S_ zD}ion6)9mXMh(!7>i~hR6~}nLrk}b0YpI@9+c!-Mj%CV2w=h|RO?z%ekr^188`e4E z+T>c+If@UYgrlDU{30#QSEI=1b&Bta0i=I@TKvTGb(=**(G6wU8V{#U#u~w#$0a!zoAH_)v=WuQKu;!GjqF?!E8GLMYkA{ zjuD)Q+`t-_hCL9zK!4M_t}K+S*1qo;a@7WG;UNKk$=KB+HTh+O3u)Y8>D4WYtiX0) zxq-4$?}D%+=mYNp=t`pZhUR>%`Rrre^X9MgK!ke!0~kjFzam?)rc$j9^5wEbt0x~z z%qfLpteuZ$Ofng2Fps0y{(~ocOeS3%lk&jw#mU)PW)$zB&C6ORCR4i~fNA8X z?>QH_V9-Vn4w0)H&M0=~6Hs zxRsFT-|9KXx7L5Nq=OF_V+Owl#!kHX-2;pqyP`otzh>laRkyik0q#ECf>@c_bDagc zG52rMtoyG!UHH-d%YzMDcF*!t9fbaeT7JA)iL`#`10<`a((o%EDqnRyOlFc3;q4Ts zUGgjv@7DwLbyKQees>^2z1w4v2B?ucN-kfsKbO#6sf7l;d`$fodKRR7S(-xq9hw*! zFJlPc_S>qRz2u3l#hWy%VbBa50Z0$1k6-ayC0+o8GJxz$Ug(s`T&rRRqPyIdI8n=w z_F6O@0Y*0FBO2yFQgd(&utKHYf-Ai~#{h|Yd&BL_2zjr%?gg@^AKdEi~AMo?h|IbsXG{>2LG ztm>M5Hledg;T1_%8IkSI9?dTuUV9u30U6u>A0LeW%Q>N;=Xtv%g${r}1KsO7L@oO# F{{se3+A075 literal 0 HcmV?d00001 diff --git a/client/Android/Studio/aFreeRDP/src/main/assets/de_help_page/touch_pointer_phone.html b/client/Android/Studio/aFreeRDP/src/main/assets/de_help_page/touch_pointer_phone.html new file mode 100644 index 0000000..58e68df --- /dev/null +++ b/client/Android/Studio/aFreeRDP/src/main/assets/de_help_page/touch_pointer_phone.html @@ -0,0 +1,30 @@ + + + + + + + + Help + + + + + +
+
+ + +
+

Touch Pointer

+

+

+
+
+
+ + diff --git a/client/Android/Studio/aFreeRDP/src/main/assets/de_help_page/touch_pointer_phone.png b/client/Android/Studio/aFreeRDP/src/main/assets/de_help_page/touch_pointer_phone.png new file mode 100644 index 0000000000000000000000000000000000000000..ab7c598961d3f2e6953a2069531553fb6acb2a08 GIT binary patch literal 87793 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

RAt~66 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μT{Ss{C?f8V>0 ztEp(s4+RzEs?5R==_ zB4UvcMDnREw%h%Cff-MRCry+D?aH#hu~+7Qzw4PPwZdA+*DpTD!{g1Y8VII_3r*fX z1R}3j_xASL#Z_1649VdcD}#^TeBU5`81cnj-H3nkNMdB(6%@S4P{{=44cWbj0mDZnCsyH z2nJ$7KT7e?%3(Tv0*Loc#~Fl!WST@QUmM9zd67ozAg=m|vYb**4eY5T8LE%b?0op>54Rb@ev5DXXjJ0em-DQ{=)}Pj zA5OBahvX!r84=3e$s*-DvPME)P0N3sU13e}Qqbs3GPdBNL=uqHbg7r=1=dZv*G90`S-qi_*@VEc-(z@Sp~qvLeK*`sXMk%Ov~Z1$|G~Rm zXcBU-{OXcQK(KGEI3=y+wpEz}EM@=EY{CGOt_^OiJx&^y^tQNAHB4F;vjuKrHGJgDeeCW?v+C<#M3{c*OPh1jWXSegVYI2ADvK&I7aTY@dRr?Ty2rG({hTy;ohgf$;QW@xQTBWKUyZ!uyr9|r6}+Hy27Hn(YEDs<87+&2?C~F z7qD%$A<0yHShtyx>p7tJD1@$iLcu+K2WWGuh??^phbJiqJayio)J43s9jZRiIl*C` zNd}M-x2#bLFCxk@1+b~SjeI7|79B6HL}1lAD0Dg49A|EyK?e_`^JPYxwRQG(wlPPsG+7qX-0IG^AOswiWD&o1OD`h?TEtv zWld0$wc^G#mjfMK#?PAmU5sU;^G-VoxIqg+nx2hPjm-KZYiyy>2ju7gnl)Ljka+cK zdS)!yg=ZO`$4bwEWe)Af@&h$w-tZB86pC)k9N4l%y-V~vEk_7BPgXljGb#HPMb2bj0y43U6}R!nF?MJk;zls7m!>PqskOiSs`BW z*BS9uZ-kd%5CEs9!O1*0i-Wlxt!=!ZAGlfQ>WW-IVuZgm;5LnT!bkkdPz#VSCWVv} zrN2obyFSltJZLb>GB2~?>(HFqSgMa31;LTbHn_8+#XpG7mF7eSCRbutsUA54P!|tO zUQ}n@+(OQdIbj|lvMkZ@4Om-2LH7qt^SzMptpO3o^zG96A2wHzVFdEI_RLR(npqsB z_0;Sv$&8emzZHlNO03Bik3R)nnrpS~>$-mLXk7^@f3|Vaf%hv#zQeg@)v-%)=%-S~ a4YOay&izsj;T8z^(NMXMtWmOx_ literal 0 HcmV?d00001 diff --git a/client/Android/Studio/aFreeRDP/src/main/assets/help.css b/client/Android/Studio/aFreeRDP/src/main/assets/help.css new file mode 100644 index 0000000..e845acd --- /dev/null +++ b/client/Android/Studio/aFreeRDP/src/main/assets/help.css @@ -0,0 +1,100 @@ + \ No newline at end of file diff --git a/client/Android/Studio/aFreeRDP/src/main/assets/help_page/gestures.html b/client/Android/Studio/aFreeRDP/src/main/assets/help_page/gestures.html new file mode 100644 index 0000000..a9ae66d --- /dev/null +++ b/client/Android/Studio/aFreeRDP/src/main/assets/help_page/gestures.html @@ -0,0 +1,32 @@ + + + + + + + + Help + + + + + +

+
+ + +
+

Gestures

+

+ aFreeRDP is designed for touch sensitive devices. + These gestures let you do the most usual operations with your fingers.

+

+
+
+
+ + diff --git a/client/Android/Studio/aFreeRDP/src/main/assets/help_page/gestures.png b/client/Android/Studio/aFreeRDP/src/main/assets/help_page/gestures.png new file mode 100644 index 0000000000000000000000000000000000000000..78b3e7b341002dec895e8f4ea28a780cc8b47ffa GIT binary patch literal 43781 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 literal 0 HcmV?d00001 diff --git a/client/Android/Studio/aFreeRDP/src/main/assets/help_page/gestures_phone.html b/client/Android/Studio/aFreeRDP/src/main/assets/help_page/gestures_phone.html new file mode 100644 index 0000000..8c81048 --- /dev/null +++ b/client/Android/Studio/aFreeRDP/src/main/assets/help_page/gestures_phone.html @@ -0,0 +1,33 @@ + + + + + + + + Help + + + + + +

+
+ + +
+

Gestures

+

+ aFreeRDP is designed for touch sensitive devices. + These gestures let you do the most usual operations with your fingers.

+

+
+
+
+ + + diff --git a/client/Android/Studio/aFreeRDP/src/main/assets/help_page/gestures_phone.png b/client/Android/Studio/aFreeRDP/src/main/assets/help_page/gestures_phone.png new file mode 100644 index 0000000000000000000000000000000000000000..4eea33ef47968b4b7fed3626229caea2d871db34 GIT binary patch literal 27770 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 M07*qoM6N<$g5l!8i~s-t literal 0 HcmV?d00001 diff --git a/client/Android/Studio/aFreeRDP/src/main/assets/help_page/nav_toolbar.png b/client/Android/Studio/aFreeRDP/src/main/assets/help_page/nav_toolbar.png new file mode 100644 index 0000000000000000000000000000000000000000..f66b24d215a0736731b836a2948a4e0ccd54da32 GIT binary patch literal 2022 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 literal 0 HcmV?d00001 diff --git a/client/Android/Studio/aFreeRDP/src/main/assets/help_page/toolbar.html b/client/Android/Studio/aFreeRDP/src/main/assets/help_page/toolbar.html new file mode 100644 index 0000000..639fba9 --- /dev/null +++ b/client/Android/Studio/aFreeRDP/src/main/assets/help_page/toolbar.html @@ -0,0 +1,49 @@ + + + + + + + + Help + + + + + +
+
+ + +
+

Toolbar

+

+ With the toolbar you'll be able to display and hide the main tools in your session. + This allows together with the touch pointer and the gestures an intuitiv workflow + for remote computing on touch sensitive screens. +

+

+ +
+
+

Keyboards

+ Display/hide the default keyboard as well as an extended keyboard with function keys +
+
+

Touch Pointer

+ Display/hide the gesture controlled cursor +
+
+

Disconnect

+ Disconnect your current session. Please be aware that a disconnect is not the same + as a log out. +
+
+
+
+ + diff --git a/client/Android/Studio/aFreeRDP/src/main/assets/help_page/toolbar.png b/client/Android/Studio/aFreeRDP/src/main/assets/help_page/toolbar.png new file mode 100644 index 0000000000000000000000000000000000000000..42f055b9f36708ac204d45bce45ed8b301a1978b GIT binary patch literal 7010 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 literal 0 HcmV?d00001 diff --git a/client/Android/Studio/aFreeRDP/src/main/assets/help_page/toolbar_phone.html b/client/Android/Studio/aFreeRDP/src/main/assets/help_page/toolbar_phone.html new file mode 100644 index 0000000..78f7357 --- /dev/null +++ b/client/Android/Studio/aFreeRDP/src/main/assets/help_page/toolbar_phone.html @@ -0,0 +1,50 @@ + + + + + + + + Help + + + + + +
+
+ + +
+

Toolbar

+

+ With the toolbar you'll be able to display and hide the main tools in your session. + This allows together with the touch pointer and the gestures an intuitiv workflow + for remote computing on touch sensitive screens. +

+

+ +
+
+

Keyboards

+ Display/hide the default keyboard as well as an extended keyboard with function keys +
+
+

Touch Pointer

+ Display/hide the gesture controlled cursor +
+ +
+

Disconnect

+ Disconnect your current session. Please be aware that a disconnect is not the same + as a log out. +
+
+
+
+ + diff --git a/client/Android/Studio/aFreeRDP/src/main/assets/help_page/toolbar_phone.png b/client/Android/Studio/aFreeRDP/src/main/assets/help_page/toolbar_phone.png new file mode 100644 index 0000000000000000000000000000000000000000..278cd3a9d869689630656f13a6579ca02b9646b6 GIT binary patch literal 5555 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 Zwvi4DVMW`7x1X^D>Z&>_<;t(Z{|6Cv0iOT> literal 0 HcmV?d00001 diff --git a/client/Android/Studio/aFreeRDP/src/main/assets/help_page/touch_pointer.html b/client/Android/Studio/aFreeRDP/src/main/assets/help_page/touch_pointer.html new file mode 100644 index 0000000..3da3ef5 --- /dev/null +++ b/client/Android/Studio/aFreeRDP/src/main/assets/help_page/touch_pointer.html @@ -0,0 +1,29 @@ + + + + + + + + Help + + + + + +
+
+ + +
+

Touch Pointer

+

+

+
+
+ + diff --git a/client/Android/Studio/aFreeRDP/src/main/assets/help_page/touch_pointer.png b/client/Android/Studio/aFreeRDP/src/main/assets/help_page/touch_pointer.png new file mode 100644 index 0000000000000000000000000000000000000000..af3ebcab0071a96ecdc452b48e7ab92fb335da41 GIT binary patch literal 110449 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 F{{se3+A075 literal 0 HcmV?d00001 diff --git a/client/Android/Studio/aFreeRDP/src/main/assets/help_page/touch_pointer_phone.html b/client/Android/Studio/aFreeRDP/src/main/assets/help_page/touch_pointer_phone.html new file mode 100644 index 0000000..58e68df --- /dev/null +++ b/client/Android/Studio/aFreeRDP/src/main/assets/help_page/touch_pointer_phone.html @@ -0,0 +1,30 @@ + + + + + + + + Help + + + + + +
+
+ + +
+

Touch Pointer

+

+

+
+
+
+ + diff --git a/client/Android/Studio/aFreeRDP/src/main/assets/help_page/touch_pointer_phone.png b/client/Android/Studio/aFreeRDP/src/main/assets/help_page/touch_pointer_phone.png new file mode 100644 index 0000000000000000000000000000000000000000..ab7c598961d3f2e6953a2069531553fb6acb2a08 GIT binary patch literal 87793 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

RAt~66 z5#~RaUV@{PMoWNzmWNOo6gdN{{=H#3Ktn)-Ov`R{q<<594(l%0jGDR8&ZLwvP3?9q z=yp}I2jrM`A6--W5-8Lh29p#JXRNT`X9o0QSr8l&4c95G^rEd7TJE=8ROwBRVt9ms zpsXzOM&oPGHf~Ikc!Cm;Aum#zWw_zxNaqax%e2wZt!yH(QD^*j(oN_JxG68i+g(^w z<#%aH6S>Ad<`gl3ml+S4S3JRz^!4jHk8OnkLgx9O6Vyk%f|t)Rgi2tk>w!jk|-X$PbB+9$U?AAjkL zhD2qCA+PB5o6wSTE+A8mhg!l$E_-k}{zvx*tZ~sts}2i}jzr}UemPG-qq+9Mn#K+E zdgeAff$G)T$)tvsnT&0j3)GIW++*Mzuo_yzH1krN2Xnid&<`772;`p>0#@ex*=;U} zq1umgkLd!`5`eOMG0`*6z$(&U{YqfkDd)3mI9d(-abQI;KmoLY=7Pqy4+C@jR=->N zuDrt4FiPd$zrg8xCZ;OL&siMB2Ja8Jds=;%{mt)7=;7L9^w}nrf%P*DJ51z^k|mFB zLtj2kd6YIgTpv373W7oBF)~Zk3(!4T3~#BRfd5gO;!y<^Fx)B?31d~w7e>0aOl+X5 z+(u_b+{hx2t_%Y@vbvfc4zhhNOw#z7P50xJQlEd=&a;Wnpp=1VfcC*C;0N_$7$~

|2;MV$NtBWsY&a~ivU2d$@fl&AO%kL*eC>EdxE=wrS+kav}pn>yx zc-u`8U_H`*!%Qu7MJow1n?2_GER_Ue^ed_t#`gVT5Ky9dKOILdT^fLLjnxZ zW3ksp?yx&R$N2Ix+ESWmTYWu<|;=lvmE6Fzo|%E6#PjX2(-KZ&U<;!(&rgLApH zcIQ3kOX&BtbACpJ*ISi`>M%<%I?ydjp00+?%B+@MHji&usM;sz2w{DH>BFkvV9zFfcQ>m*%hlO0lj`2*)vvkULirf$235Zwv)sdDORXVqeS4R~j>#lyhtWTz>l0W*H^pT@I9aUeG+j z0L7UJ9wFH>&urHbfb^i|O2ij474vxTs0Tjd-Dck(Z+{EaE)+^*0q&x+O24?L3@rf! zR3ZDH8F4Kh52^Io&R_!zJhd-n5mjs$q^1)!*6qpr;;Q7y+Rpa&nW=4V73LZFAIVZU8-sTrV^lL5l~qI{8In#|K;SW z@HTDDq&g?OA3jCHHt(13e=nmT+EYv~I9RXoLPIp}7+e%|z=B%_*zK>`Kqt{8&G`BG ztHXZ0G`l?nakDHgcHsb=$TR(7NZH%BFe$*4&L2>Ywg5g}ZasdBQKS_xk7i7iswtI* z9ZxX3K@4Zb2~_+%(qVtRn>Q+&nKB)reoEVh`w+F*MiZItKCvG62rMotzxdy4b;15D$Q!U=Yc~{G&o||Vh;-e zz@c*S&*ms@^w&p+u3J1|9zX}smP`PI0Def1^78w4S4Ec1ZF~9dh1dlI>N-^q<&aHq zAXHnq_9^`X9Wtjp%iQDyIAwk|TlueKt^|8ID80yPbB}CJIP6<0${E6eVIWFMPL5!s z9F*-V4Or`mH71YaCmwvkJTFWg+#K_QBrh`g)J9|`(o`3x6u4#|p9FgBhOkSujP8qF?p_A1N z`_Fc*L*!_6&bve!df_t+eSKS(ve!Qw1!9yufXN95Wm=F%b$*b{FN0s8Y!YMy*Oa!3 zV3L9SwRh7h4y8UmXKWfo^ZSWheLTe0|6(O*9+$PV6nzEti7tB*9Q+kQ9=vz>46-J<%4(^%Vrs7VXJOY7P=lvZ+qb3Ofi~iL>2C$R(9Ib^YFJ^|tWUqYzZ6 zF|@yD=l1$Y(Efzg2|pW-#I`th`G($oD6+H7(8n$3DI0UNvmQ<+(0N`Zw8p!(j`#D z%m5tCKR&=&f@)J3s~=#GOzYPB{rI#aZIYy&kx?p{$5`C z1|RK#s{-oLL*wHlj?f6)DuA#cmy(jI+|0e>-M)>%C>mi7_qTqyTndP&ZBI6bmoV*F z53v5T0SM#K&$tnV=sQwZLbKw{Nf5>3!_n0Ps0Vsz0p(&en71&}>Da#mMK+N+XWI+wqB9 zPnNVz^Ws~mAeA1>bgIJU@A~#?|I~VpNa>Y?$VYkEd@E65z~6@{los9sVP{rrHUnPE zaBy78z%xv;7Em&90;q4BN_hTr!7p-?wK;ydL~q4s@F1@qQD=_{Wl;gZ419A0@c~;| z4;CIiId_lINvpx(`|Gs2IT;boyV@AeLyz|2q z`l06kjjnLRcNib(Uv=1zEKsq>4YP%7EEYr?{8{lIaV>Y+`>z-oq=4J%Uew z=ivc3utNcSW{P$nW^~J_&`4*z>4eSO)19MW`tv@nf0~zS{ zKncvCm04YoD64sAn1w4|c{3Xo?KY-sl?~NEHW2(_KIR;Lg9QLmODx_z@wUcXQOCxa z|KOQ=tqN%tW+emJoeChaY{eU?aC250dGMM|jZl!~TJHy9n`fOBaLM`GjJ;iU%N>kzCgeJ(&_3Z)`6#Rv5) z`mCkrtu%AKyiqBA@|qe{V0v%)b;LP!d+p4~NkjL_t3bRg4{ z<}{s7NQ;%>nZ`=idVW|2Jg2>hyuF-m11JP(BlI7elop`P@|;Xy#SNGIoY;)5RFDt2 zC4ZUjMGPTH3&Dh~daX~$Wbjw`VCxtfd(jLXqC0c^#*g*`7xcsyZC)IGJtiKD(|#VH z(w*yJ&p|pgAX3n;{76BWrJyf85X^6-9JR4zO5ucS8C!khRsk;}o`D<3+1t+n{g*qc z;+uGm8<}6$WKM(L0z-khbDp~S&du8pMK9X(|7LdQmgZapZ5#% z-e+d)a$lNU#%W>BYYK7cFH#RQ(RpNKCG2!?sBb2lFDvhK@FpJEg)>-*2eP+&X>L|K z4P2^snv>(Vz~{dU4-nf8w&d{cM=pU_x)mP}V(2@b{l$=lG-!lk%7s4v?D2kh+ZC5{ z(DHL&H5n}Hec^38$&6j9-K%T;wI`E# zuja7TeQ7q2>FZ*}NUxX2Iorcd@t?y5o0Ff7@{g{c7MPsp@Vfx_{Wd^TtnZQelT}dQ z)MxUJ;>K%O=cS?5kR;JE)4Ed8Nqvkxi~_9jY1)WN;3w_r;R^56Hz1ANh^ylQ8E#2N z%E|rtj?wByC(8Pos^ol7FZaZBUqP3v13;+*t$libGMaspt=*|OPr$Ejd*ZgfCu7Q9 zbI2~V_jh6mw$~P-eY=G*XknY$_3sdbfK@?ztIaW=>ZS*nF%!-RBoeVRXPdcTPru$F z@6|eH*|da%a3^BGKY>dmCVeQ6vDOe*c!Y6)`ih6mHm^4Rnx<( z#PEX?1s%T5b-|#cj}0qw0M_R8>AZ4r)ds-(QcY$qKS5O@-`rYpLk(|6R$glns%-1& z@ZH}8^kwmn7@G~lOiv@(`6Du-Rfi>2`Dt-_=OvbZnXOBd!(sy1Op_sHUa;MODk_mD zWtjN}k1A_kcK=?7N3StFi;dx+{8aY3M`QXgLOc40Dp}LF1QQh?JV@B+ntwa;^PaHbZtE|5%rIB){s?jQ!mlS1kbEZggH)ENefU4CL3LYx$EcqHurHLfM81xKCi)D;Is9 zC1MQ_MEJ5`av8$QB84LU{`tj%!}A|)+PTzJ{uZ#$-;GP~IhK_6KC_y+gR1J%@S8P( zDIT)(J|bqB-8<{^cAwC(ruf692URoMNi>Z%IiJE&RpVgV~#p9@pFaY+2|nLNI!n7RpeM7WM3dySz)u!3>?&GQlE<%D(B zdq(Freb0*;J_z2Bzcn!$ymL@Bw}i{?2d$_vQ&+kMWsP;&nPw?oJ9ptEd~lWy zM~@ZUt6s%~7m)<_)Ti(;zKXlR(|b5zL3(&Qrktme=ht#pQ<-yw(JiB{cq$>0q}sPH z>v9)%)V?LN+AM;dqgaGBc6my3a~l#qW>$V-XLzTrZI3TY{!b{w;GDlZgn zCAU-j&IWtd8Mv!id1?s=$ib_re<+C>G5bS_^L`CDbuo6_`||cCK+W9j)Mi0D=+b@~u4Blb;WN}V zPyhB|I%@NQeZKf^0>xp)VqMDhz`VUUT^+PgIlKwEsG8T~$pX+B9{Um=mxwUX?2P-%p?y`S#2Y>e4fd;p?Ng53F=F2Oi%8 z_*D&Od(nbT-h=0iZ9iKa&aG|-foP`Vc+-?6ys1G7F@zIMQt)^je|W6BM{5p~wfW6Q zzd=QTk)Z3TR_EI@$lW*bpM#~3@-?W`uk8>6B^x?5B19K;qPa3(nY&w z^r3uLYnnO@66kv~NpMs`b2yOonHI7tPR3XxLhV7XQ0#qU}85y?q%oQlB1E>=zazGutFXg6~ftM&hd62rU_w7sr z^*`IjCOiM9ZLGsPlq19<;Q{OkS%^r9Xxn>Q086INX{#LC_O2Gn&k0A#=0&w58u>1h zEyxH)$VO@(R1}A|jBb8RC_aiJK4w{0nSpBlIMIc`7FqmNljx|IeuTZ6{nRJ3Gg&v+i-L{ns7q3GW|x$pS4F_gHxTyK&07? z16aU@|Gseh*J_p%MRozx$&()=*%yW6q+ke1*Qx_HyGDJw`~2yRR21^G!;0BV(EV%& zuy2*y*$;?|>&}p-E?_8`tJ(>(r?Uf%W<&$7N+^0ctxKT-$sEi+?)_YeK+=3c1Er%? zrl}7n%D^50<8|?4R4Uz=Bls;}P$Xx_<%VgjB;#OhogItOpRtiBj~|I|p5jBZRcHMB z`q~Z!e>u*4KqPpVfp8JdwS01ouu%wYpf&&|p`OQ>loJsLko$H6_(PF69Gz z5SgFk_`E;3PL#HTX~~4JT|6G2ov^+O>7u%Jch0+@jot|bwmMoo?N@>g|G4|qIn&O5 zAx0VkzlNQ|PQh@RjIrrFwo0poain_vy9qk{@q;!ePZRNDmgqywis3BMn7i9*DM9P2 z1VtzAXS0r@0&d-Kh&ph(CF1ZsliXytP@xXp$(cYdui2G(-dr)%SR?Hku_GOo@I>l8#J|F^^b~+qjq|-jcy`p zf8P=qC~n-v->Pu3j8VSBHGIhB9ci94+Y-N{{M#3Ctp}8aB0zXqP?`0VSWDEz=c8#!up-O6j4S>tTsYE zde*kMq?)5I^kG9d7wj$t440eYZZmv*Aieav!>=V7gLLA%`FTLd++wh9rNv)W#xnN2 z&@Zz$r=39VY5-}DH5r#ToS092Q{kGq^P=ll?D!ojgf(ep=lkF5BO8^slCcR}y=7iB z+jaut-5MC+fz&!%`%T~o@}xvGjUU5Y8qTaiHd65FkT>$s#%DO-W<>DjM+(}3rWdDR zm~+Tf^SZ9Sl{C7n8Us1EShF@xGlgvwtGX;Ca`frJ(`%>7{!LFa^J4n3t!9HJHA;P^ zy5;laz=)ua3Tl0Df4k9&&Z*tbyM~<>)kv-@0z4&ZeVE3Cg@Z6~8mP0uR2-F>oRox! ziJf77rRxui@9(=WpHTHy-avGluxyDdZ;@N7d^Uy+x^$ge!nGuy7dW1^2Mf zk1VcdCM|A^SI@~el3uFDSwWM+yyrg+ET2}86B;N47Ll|L_yZ7SBykd!6vBPuC-V%b zV9t|axOrJDa1$iH8W(?5A+_4_BNmI5z`ULJY1ji7GSn^Zh0bYdM;v4|i$#=ijt% zQuZ@1d4qA0J+V~c9s_%f`0v#43jWmB@E1ObQixm#@}uC)(ftiF?RI&TSem!t{G;IT zZG+fCht@s)Z&R`HT`Sdi@m*+zCZa#Mc_x;nRVy!;1L**IQMLI|zq zam!jOLtEhmbadUVwn{h6hv6VM9o!(oomMETnhG?xV8p{~C+KHd{^@e*g2=K6BRSx{ zlg>*G_!ZD>wq=|j37njp=kD0ozS&8SIM=BzQ24ucbSb!I5gqbwn|+t8uUK;2*sTd!0v3;-@3;fX^n0 zQ*?~5(aG%Fde!+}RaEUXEs;uqAm>~_K4s}tz#Zc$P)WagXyCKiPWJ1jb7uXo_wOxT zh2X!(gApuj$`Sk~qsqlKE;%6ElB#uKv#Sv*Tw5xXN0Z~9Uv?va@p;@5ST2s>O6X#I zEt+;S=b$;jZZKfhkq@&1*;)7yeGuF-h||_iuf_% z%Aa)Q`I1E5jqS!oEPEVA<)3wV-HrQY9$f$Ug>!!Ee42BzniN^}^9qGk9GUe|(e(H$ z*VSWSEummmaJ9WVK5r?9ro2F60HX#=GHgy1fhDb7+JV zk}bk)gJmDHW62s@G&T$Zo8JgfxHX@I%C9UxTs~qWl0GTNrXf*t4aD;tw(slD@SGm* zM1Tdr*50e(1mF~NxpsC@(}AZiel45Z7(bo%WknDUMh-1MnG;UOKXm#s?<^O-hvF(= zf1}clTT|HPR|V|}+G_ueR+xGD5xWDsouFnaes5-A6pzI0Xz7(~s@BEfQjq1y$c5?4 zm#@qYxVg(0O+L0)l)0+UzJG$M(0zSBf4OcgJKcUEHzTf7I&!wcom(ty<69_or?7>Vo^DW>U>l+l-!D3rQe_>brmGscPcOY~uTd$LF{L^MMKE3wZ_Pd!r%Hb7sFEWsq?wezKx&P1Q=AzywK^edV>IJg($*F$_x9Jx}i}P(=$N)S#k*+4)5(bX}Oe^cy z5Y8$NbUinScm(G{`+Fnv2wy`@GL*VUgy7i}T>W|T&B6O6PZ`#9*7O9b74^T!MC%WP zk9l7vK15|;{>;=mv=&ivEIl2j#H)KhA{=YT_$Eul>GS>i_4n~3EheUQD#`0CIQr1; z5!fOZo4~20sIY(JlzOo&!e;w|Xh>Jc!aCUzu7p<^8LhcVZXce8rEa;*npi5=xtBfX zbjHXw0eM+ZeOHp7{}Mq%X$PRgV!(_@TYO8+^`jKGC=R8^DXT6=ymJik~W*1 zjvE|Bp?sLib71dAzrgL65}3#!)u4erM;$Mo0rhZwC}*w=>?{)KRLW$cIR14UiN<9^ z>#C|g`l2(l`Y1d`8+2560V2H0LkBQ;5b7s=Tt1V}lep+`0a@1KLSqo19fK6Ev8gdx zwFeUUMSkn%cVF$46IvvI)fwy8>px0!Dy_tGoeoiiXOBINC&3HD;HqN0)zZOfAs&fy zLexbd!Fg$Pg|F%npcG3*xZd)o4w2_Ye^b>>ang1P5Gki+;CI#nF=4L=wQ;L4KI+q;0dgo1m8}HMeRx{2`rY$GZ=l`ErAOtEz{{7CAQDqV#E~>3|%_QQB<1IQnD- zisdTp;W|7;*$>D5>R6!8!&Bk%CC7;7gDagNL@l;gJ^5P=uvz_iH1Ffxf{O%Me)#}D z1Rpql=IfRAMZ_E$36)=^7!u%PN!B#zf@3KAE{ieW>e!m_G`=|3zYou03gV-T!Ssih z$ymL>=)M*S2uaH#ar5;_2u!$6 z&mdf63I0kl{4Db6=Zs->PQ5q&D(}sJ`FCMsuyQPB&?mtDn^-I-0h8zkTm6! zU(9KK_naP0dNL|k9%hlS_+V%k7rj-oR91RPGiy)yxe=T~>{D zdWL+adqC%Me%l8tjV?j(Pq1>|a`FmJT-}rpohCKPQr(&s;6R)J4ae_dLI>>*=8A|- zhFUz3$3G&BG+P*OccHLSTxch+-VVwEv1L4D7?62=avL4{qv%_?Wr?dkK2(nf`P5Z& zdfy{s{_#eDq9VBBI+i=)zua9or1QP_1(*oP5SMCgl-p0(C|nNbqQO+R!p@=CRP4I)&PT1TKH26R3`QKZwFibVWiaUtu<2wR ztwE6bg&)frQ*WT$z`vhBrcKC&MI7^rC{)ghyAc9+Y=$EGoM+mh zb|j`z5tEBlND{43B-b;DR6Y#N)Sn`jXfQm%`9pqw*Td%VE_?2Fo*+2R=yafC-ev{e*0 z2kiC3%M1<%=3A4lX>0SwPn#=x5L$YGuhb%t)Wdp@#UePvFwQ-zkz0M%Dgt=xO*W1-zena~`R2j>NXK1f~>6H^3| zT70C7uGxw?tAHLhx^)H=nWp3`0-h(VRA#pIsG44BR=&Co{~nB3f0%T@9>ukX6rvw^ z_kbN5D-MpXy{I^?xvKCchJ?6BVvq&TWn5ghtIkZA*1}s$dLMpTo2Y2#8H51?((zM2 z<^&x4s`+VFmY4|iQh!R4ERz&Jx9$yvQWCC81)P;irL!R)%}$U$8)`|@4+<597s2S@?{Q7Glw7m`C*?8fWXa%P1vr z&;(w`el9I?Pmjafdfi;fHS_nloxS}r>nx*85aXZ{gzBsqJwyPTUw^l`CJRt#A3AH< z_DfBkf-0I?_1%T*7YiVh-A&)b^_rX#%z-W`tQo~u6xMC*;*KA-Nc;c80hM#Q*J9yP z+ibJ(4T!>aPg??X6ARtUJKxTqO(PG0nWQWbj2VVz(O&9s>g85&v+Y)U9pyb|s{mM!ST)f75r zOOpsdR)%d3a91F(-rrr&N%>Vu$lU`Spry(po{-co1M?L+0=g&i#^*>pC&-KIqpmJj z;6^|x&VB9VE1yJb@oTYf6GEjsNuE`MN2e!$>DU0UXkR-*gX#d+&GhH@m%q33=g^Le z<$>Ggy7&_SbIh{JD)6-6%!GOeucPio1F6~XgihB1z~S3-G?2FZDBVd#N6Y96jRgv5 z;5s?sv!>$nv2+9SYkfYpiF{oC`f)Wp=Cw0$!!I`yhRWTX zPNS_x>AwjIsBOZ8C4u3sS{CG^r)#m@Xzvp}@O9cV{crEn$jHb)M2+ZsPCsvXIEFzZ z)j#tS3h&0CKyy-dGVn~?q4b~p$!Z2Z^S8fmz!|fs2^A~(tjjmbxHtNqpEy)pS-Acj z3D9T1p+Kv!K@~7H=?hUJw~+6p)c<^BQ@f*0x|6*LCQ=zy(W$bd_yuCI`F(P-(|qE5 z=tu}-7Q6GkUokisRSpO&Woo-_+oHdl(He7ORMIbkftK;v<2zG%p zD<^irUv=j2nOJ=q-t|vFPzvD3)_!~9a8f693Qk=lygxm~DC^1#&)X0P9J}z0C7Z7* zlRR0Oe-z3ON54b5^ePKWEnW(}Wb1O?_D!*gHaGUqK_M@2n6XB2R2A4mP&>t`kEjZ- zE;$%_>+yL=w2sztLJAy>mi2;z-c=Y}W0&AT%eJ!C)<1E}?aMZ} z1;D||V9yR3Yx)%1yEp<;x>~_VMjODy2orf;Fv-y=X?P@VvgiQdq{t<7zOv0-0~XiUp8 z$UXUBuJYrj7w{c&>wYO@h*gXw%S>@g;kopl-@MV8D09%mom1+4L1dvc0^HGppgE2J* z5fKsIz~sCA0Vdj{6}Dg9avSU6&0ljl#8w%}{T(-%K`PPYCDOKuUoKJPKS)@LvEnZJ zr}UD=arapV@4SnH)WaWP5O@@Jcl7ne2gUV64$KHV zpR@JKCMB0uO2wMxLmvz zbve(kFRCjWNM?M79^W8+gz(oXpH$@#%{Ao%($lqOT){X>b z1Ql&VffOo89p+2%O7qb@u-;A@ikDI+U!Fh34-4>t5nr7jJUhubSpPhV2lQW$-?mEn zuOO&0DPR~n4IPqS>+FkuP2hxPKy!L<^LHzq()OGhfr4XV6Xpb=#<>x zL61nc?;XV~<@m!mki3IKu%C;YQEOf=8s!2`uy%@E0oeR~Fd!-2GXc@4Vt4PBwvN{V(3m`m5=Ax?!}mAl)Gy&u8BE_dMU{A9(n|b36xOo7*Se@9TA4 z92_|Q-reWQzKzD;?~D<-f%5l#?ugEPU!bu&%~pt2_bHvRV!>Bpj&qqj^9NQ~EvLKN zv82rr)CkHSWB_GZ@oG3*T1>`|^{#>;wMF5C z2r9JAFSHG>gSXj?XxF}I;hOBjAAB=MXVJ~J4LcXpjnCKSDS?MxYXbtze_~zL(alZI zEGpiNv44jtp@Za)iGk*>N%Tsv7{V-VQE+!^se?J}E(nW^P8>J`5cvMxWU%AcUN~a7 zM8F$}y?cCauU z3aI3;>FxVSPP$r1y3Po>3`}2Sefr*{v2>*`yXR_D)t9??d|yuLG>_$af9+6!imvtg zCnc-E0Jh2_9@4NItSrE3_S+;Pp;qBC_qRhW;W|QAfH2Otj+S!GgO}~0>rygJ{8ASe zv5>>oWcio0k1d{Zw>Q^bqyYQB27sn*i#2+3;o7F(b>l7PD3?B8GeHhsfEOY-fRH*` zR%hdCAkzK!Fe9_xZ3+)RH1njP^76|9^VuexOUgHE%*l1efr@S9bffH5;*;hHh<+8h z7@)C=4187+t?)-0SG?V%+vsIHh_EFNEaN3d8wW0UK3s0PkR5azxFq?vE1c3SjIoK< z1;>rFr5(WIXf>7S<2OHq$lIfY^2XFpGaB18}~cr>H=8?HUp{dHnMYdC->till(P45E^lsyFpEnl?c zeF?USLRe+-ELdG*^yn$5gEkBA6RKd#BXKwj`N7T#xc>ibKf zk|2_Dos70x-3Q#5h&fV&SK{&(HNMG7$v&(v>omW6wlZlh22pfzx(}Tji4d%bnhAUa zjr`seT!58NJ24y-aTD8O_JEJP(IPDZh;qazui|`u{@sEvtilZmUZk`YXD>Ya7y`_5 zr~#IOHO1Lye{bZXrao}!@o)l#-0pXD8a9aDOp7tDgzaBM=5ntTuk0T4G5(cn(z*Am zsYn}|d(=dTENJI8O{s!W*`k5iOxQ%K5-hl#1_8Gtuww)*~(K``<7 z+0Ma2UperfT?wE&F5k8@(jK^*%j(HywK7s(r&;#KXiBJF;*ozhkj)=6b^rzdT^f`5 zO*^Am!k-&Ja)?)l$W44V*zH4+6v-5P%z}Xay-Wv(WSk4;jH^iSc@(M)^?$X{q$d|T z#zkZyN4dtOY?CBNQu2D!D(5Z$7o>aS>=TDn1BvyOtf|}{ zd36tk(=|Ccp+(R~2qP4z29Rw<3Y1|#0TCp})ydbnBUvpVIUn2KVRNR-n#uGlVox>c zAS&4^VfJY*=z>?d(`j|18YQr&T^o8748jaHZd|0Og2$p((1(Q=HB(Ph^iJC?RgYUg<82xfz}Hc~Fe6_pNEqb0jjLg&mJFwETw>W*xZ`2q$?da6|JaYx@2AiV(hg?r5^>VSx1zvpmf@P@ z?~*}^C*`Jo$7oNSPg3;tRxDZ=fXfcX`y~ULI}|KmLdKJ+?aV++oJ>qh!pD&M@F}yv zx?@gs(l&#qG&r`KYg1vCaO&xv0(Z<8IW!SOZ5a;0a#V(ISg6uFhzb6xjK2wP({w{YjR zjjNHGaR&Fz6)C%kj?Yd<*cqfORb0G5vm;P-=1U}bgmnb5EG=Qm;z^i<$>aGcJ{puu zhD**EFSg7Y3C!&CE<+slYooqcD%JE9Zo4Zc1IHeezI@nXFVp{{&@w=Fhm0FA8PlES ztcl((jDDsOE3+ zs`_<-0#0sVoo1c3CmzOvC9IZ6gz)baY_9-)#>szn>u&nC!-26X7H`hW@)RJYtUM?Y=85c^clA-?ZLGh2?MXt(| zBR<(0;y<~7+^^QqA;!;G9S<87NONJ9-=XV6FK4_pEsx-E5Y2KY_wjA=fF4G&&E#Ka zs=fGeIluL^?`Svt^xT)9$Y5||HmGd!ooR~=df}1(?zLpkS5SNeUQtg%&sIJO-DS3A zFEdEVob`8ZBZuygS6=il8(|BM3&2VIP+d*F_baMOOAmpKi;&%%h4l`LEK5d&zNDa- zlH`$g!qONT#d2V{DyOPfPMW#(o5xSzj*yJ?^2FX?@u2B&&;W1qb=N0>8gv{Zy9;N7NU=e`2Ne7yDK zQOU$1z1viX9J_Mu99=orG z7F5PYs_su}um~q*Mp2yU#ugbs1b20VFI|8u6|pS)H-fpnibq0cnM8%+_)1PAstoF? zGz+d>5$hsXO~R*QhS3FN=CVcb?j1pH9u+ z-b5t*y5V8>noqEhzSuu7{bolG(f_(7m&;p|g2s>6W0Q-b|1%Rk zFI^f7Z;PPHxPwYRQ|E-B*D}TawzG zqo?5_VI7qLemVHC5?Ds()M#a7s~!au=51sM=@DVkap*2DlEg+N;IgTg=E0FlP~E#_ zv#!C~1TZPRIiPTui?wP%4Yl?W=qqv(%ru3?^_(A1dtOP<5S+^$SU!bGqL-f<#CwLOnv zt+4&he(2ckByV10Xrw*QqpDDCoxUoCx1z7N^;TC%6p}@s23xGqb{>g=%VHcTuT~V4EJ){;vg`PS`Xxnd(m>@ z`8Zx~=Y6%GRzlQwD2j_;`#-TiIP?0}A$#1_49whI^hDcCkhcwtY%}P_OO-Q5vExQx z^k_u++U3OXt96A}& z7qY@348HuL3bS9-eFh#$V#C4wOcZyBSs9Ic=IgnAD%vS@7E4~8BAO&)9IB-WXghUUd?}m_!%iwp{Bi_@77_#P-=ZEa-n1&17tm zcuY}=tPvynvIrmcNphCJMvbz=rWq$cHK_iIYY*L{v=|6Mqs)+$s24 zklXA(Z1;hLKu{sVqp>za-M#&x47DEfcfjs-(%_t`XHGJFs(`G>A65%`kRv}IurOGD zYqwrW2O=!((s8H}{h{`0gNMQrYwPs9?MUKu+a|X;Ss&rm*VbhVak_+>-~a)~w6d-A z`aY5eanwqDDe5L)d3PF4L5fDa;c;c($)ne~+t}GcPa0J4rtZ^ftS8%jE#6DUf*A*< zU9ft&c65@$jIEynGuIZgL3`rc^@4pD>!>KWAuBc6uHP7U70C~C>65-ADF6v)kr_O9 zzqSgk4q#JF@@c8}2)dN~+Tm5=sC{mJBh68KDkdR_efsbivpRpe=_#a7DcAsdr z7f9O^hP~%KntNE1D0S}@D?m7Q2C08<3waZ7A?hfoMIF_>)n#Oc>7e(TkVNVzv89w*xkcg^UIe$d9d`n1wtG}pD2^C zO>YdWnAO)J1^BZ!{dy8Q5-GbO-`)muq)J?LwO()Zu9<)s7Bq|m7iWMSoYlMVu`+px z53Sm&g=}9?FhEiax7+tYptnOM4j5*WNQ)yQ-Gf;OW2n$H}7Wxk@ynP?O79H+lx2oXKP? z$ZRlXKHy?!b3Si!x9r#4$t=3k-R^S6nIEXT?oluYx;o7#I!?M~x2)?I8gz=baz!F> zrpV0DAa>1NWCVxKY^kEx;ygMNOL#pO|1MHtNV*b6OV(ns$ROdUQx$WARr0wbYLEOs zKy;8`!S;wLDS!R8*Ydx)k`7?*vJ;rQO#i;zYq&cXsB?KqT;62iv$PpgTx&eH5e)P< z#II_trK(m~8de>vSuGGfD$LGaozL?l(%^qU_g@SRILt9qv6(N3dwv{y_!M}hGIo(1 z3asn?u@ev8{H!^i)d_#uddCG%PZf|@4858*q9D#V7MLcA=1@teY_;vLn{Dq#FB1X9 z(VBqa#g}r|7v%rNl`!4N+5gpz{7u81+_ztu`YNH2a&oJ}@{?M;OqWD!2v`wB5N8&# zNXsPYk75r3CD~H0h*ZE92bx+GzPh#$Mv-_a&;4wEW^khij6dId_2Zy3K-yt~>JROI#t7?Pp2Ps@0#gs2OS$m>rX09&WWc$D>|X{chSWJDkeFjz*Li?YwWK{ron8?#&aiv3wd4f9`AjbQa=I|HO1(4NoO7vc0e*8208uc4TTYQ41(q-R{Wy*+ZRIlqwu= zWM4;3vn`2{JZl?(eGUV2nTtkyvG0@WRd~>^uYuN6i zhlCHW1f?}>MK%vof8Z$xWTD96ojObI4oY8j5cMlk5jRM^uEs~=yKn>MxGW#mf^o=r zv2yJz(?f(R2rO!4wUXq@#t+nMB_&_|Y=xENYV%E3P-y&6G#h&yf-XQ?kSU4?NiRQE z{IKwP#%YC`5=>ANQ(<5^BaPML%AlxAs?GVPJ%DYLJ!ybbfNhuRQRBmT&p+K1uat_r zxZMWXTJVVpK(3O5q{ns4lb>4RNm6CH-w}uN{o2V?Ue%K&1?5`v1G}tBXk@mBC^13z z*?t5zWpYx3Ett&}&GGagH=ZbO_#rR~D@F!=_~o`(R`Y*S;%O7;*P`J=tWQu3`yiW!WP<8yVZ5ukZ6Ef@D1R5`gU@ z5*a7NGwEB240de|*Xn6}vcEEx`RXH+H*4U$>!n=4t#-f{E%VrEB(bmu*!O=%vX*0i zQJ)f`_=--KC~1H!t|D$J8^_jYeCKx)Cmb72&E;k?IOLg+1HFrijZzRN#9(esB7Z6~ z9`{da`?jfgd|fAIf84J*r^$Wo-~5*lJN`aLt-pupHy(|;>l9-87Flttm^!I4%C85( z)o$aVpIia_jm=uvVzne7fhKPtFYOUgkFO3sDW_2^gDen|AsxMCq;g|LxcE*``Ej3> z5}>l8GDSVPK=ME=8`$W_0Ff{{9x<OZYofXJ zT;lK8GvE4q%Q~v>$OW@6bj#ZQrqpp#(n@{~f&8?Chjn-W^+@^uxYW8Mh_UF+wDQXI znr6R6DgmHbKgx0X2kvQd6eR2EGuc9FmP{ZL=~bWp{68W!5AmW)ih|cCqm%zSj0cTs zy_#}joP^oz;oJ1VcK9-u-8hI#5XG9t)*GOIdMvRP?f2a3{)36`z@ixo5xY1L*DSZ*Mx3 zm2vT?xpW1WzvSl5ONS-3x_bQndVP;gV%;u}=%Xm>Kukc>P=gGqid5D0x+?}=!VX0x zUjGU~nbnfR=;RG-aN=nBjZL_Wx3#5T2pY(msRPADRy@WFMo)Nd*@xY?A6NgUx48b) zPbW>^G%>Rln#kC4og!0e1TwAzTVJ zspBVHmDfiN6X-*l^G`f!oIjVi2~Dm~ZNWjMki0)%uvrvgb!|7n2l%0?eDl*Qcn=6dQb zH9C3Yqg!rb-22y~znJWi7C{cE4L=^>x&}i5AWe{&PX1$NkR&^D_k%Zy&g5>>oqVy( zA@2+_1Q=xRttRkIAMVi_OYyWaTT}yRd>Pi2&4%LViXgjr03mZH2v%jo$nD0C} z>Tq+G5q-R_EPIF*e_WBFxRPd<-0wByKRKYhb(t$*22U$%-wam#X@=S87lz8DKF)tv z&QL(iYA!EgfS3){(^{iJ5Mi+(l7Sb8QE>mRB%BFftQ zsJr~m;bxS7sR&AOm~P&2<-9= zL;%oYhH>0Kl^bvs&BRcJsT!NJzI=dnfA*=QgBrAo9(eOg1fxXykusqUtI!dTz)qrj zn^=gyv}_H|S3(_ViK(Rk4!X>B>?gPLa&qr*5t?M6dz*&AGNo|pvG{y28l@vfE`Mqk zb((4v2koUJeZ4*F`9lz?I8px`{PvAzm5n57@NF{7(?G5Ue8V3@e_B0<)ERm-{F|gb znvVTLYFXbQ+_`H4j?mbiQ-?-!Z5|>usdqzJA5cU6HG!=~~OJEX9 zPDFYgpmD}mR#q_GyikB(gNxt{VF)$@0N%P-j8|YLoNDHPlmalGc*9!+G1Yrw-4bg~ zxC6Ko==}LXwiJH9CqooQ)j-fxG}rsE>1Id8Yun=Q+HRm4-cxVf7xp!Rs?<{4Wpo%r#aKx2?m?24OAQgGPE zCVwyT*wjMj6m6l2?7>BEl{YhI{|9R;PI?+Pg58Z9i=XZl)ueFTm}wGrG4tz#h4_A*vu*nf>&tYyZptKlS56KlXw%Hz|gzp`;txrb?9Jhe6YFV_IomT=HEO(r^e|a$$7zwCXX_K z`PwnAxSy<~kh_SxxMd$R*CgWbEl{2E;GhAb&Ddg;dtnqlrr6QG8G(#hi+K>;zT@L$ zf=3H+sp}3MdV6;r(5Zd;ADtQiu`#Vk%$%oRmq8k%m!E#X{&ZR>_Cizh5WC7BFr(OG zI)0^KHsMe~XzGJYA-sQ%yj!S!;UYRE&Q@%Rk2%_(ySoNCzosY25(9J)%VIznsTz}L z^{v)7XRnjZnQwcEf!7zR3_z)H8Dn@7c3EsWcij5*b#4D-XXf_&UzpJ9HdYR4kK2w+ zm3bCi4XkjnMEKEy@&FfZI5ZNO=;j=^?G?G`uL^1vv65nn#(U14#(GEZbkJnaL!BJ*|=}$ds zwZ6A&cPs+>Hu7Dd3b;7$w?_&H#qac|2(qOJ+i42;9v94E;~i(+0?69R8k|RTZ5*T) zd<;NiSJ5FVX2O7t&8dX|(xI>5Rthth^XD(gc(UToMB@OphYMB;HxaYynB8L(KWL}= z2{?DW7wMijPc_T!j=W_988iJ{q5@P|eWrNi%vWda5mceSzS_L4_&E|+S(KAh17t_d zfGrzQ>rkK3pYB5vbdFOMtlq%vpKO%n`QZv{1#+@LBk%QskIieAr>FPXlnZI#2H#(+ z&H!^PxB$S1Wio?Aj}>ZtEAsr)+KsQ-@caD#s?|cTkCgcvJ*Wgzx|scV;!;S;re*Hq zc@$b;1uP8(-Du4K1fer66o;6Whu}AMJ7@qvu0jDg#oP`==IY0Vh|Qb;1a8BOKQER< zRkWz1?gKcUY6T$F0<;{~{V8nV`a3XajY+NJuUA<2c9+8lsVKpD2%J2pjhBgl8xhzs z*Yn4y@pQX*>!jc>C(AN^W=i<&#u@>#otfhefErP4yKaq)A6Kt*?iR(Og=%_U^MF-ivSioTpr2)6el)YM4c{)F%)BDj zsj~aY`)&4awS$-J@V8f4i5XX z3BCSJc+Gy>0a(n_m-$$ZmV&S0nmg?_pf(_e2aL|Dm2gyB0}>&YXFuO^$-QgpeT`msXHfz&szk>1 zz-%vA!bp;b-(0Z;HAU@gKyNc$>ttvvP3Vo(q0d{oxp%3+tDOzBXNd~I@uI_DF`DdW zj32gh3)BzVG+vj8qza;kPJ#H|OZ!j^V%zCJEn^-4qCn=*sw$n$dcJs!{vNVBH>6&h zN0tIgD6n9C$uy8E^s%2Q^`RM#V{1b`V9&u{3TYIv8)?4=f6AX-Zt^)gJW={>SwDuG z^sJ~^8>!P^KVGEXk-;6mZBjrvQ}n+ohLh}<>e>Zo8dVnXb)Y!1!zV~*{t&3WfnB@E z-ax?2K=Sg^4)h?5T9PQ8S)u_SV9)k-YV+pUICJKx70Wzv)^VO_VL%z^J!@*bicgej zV>f-2UDHVbp{&L84OTe(dRZ=zmY2ub(pc!mAbR~O3J|zWH~em}d=Yg6V3YxWi%oB0 zw4bni;+5rzD-bvkD12@9TN)7g^?jaU*xwj0{$g964SY^#cP>QXag;naK6L3{HSs`C_Lf#( z_JysH;&Bu>-0x3Lo@yvemWQHJ@dS_R{wf$RJnj|l%MDOAfn)&K&Y3j-x5)Ye%T6p{ z==SfFghyCB(VYWiTltrHe!pbn#G`Sa#WD7AK$O=-f(wE}JC8$KcAk@i&*R+q3H~LAWkN9g7c~ z*7Or#YB;^`wJ;U?^VK}ldln!5r;PyO$xc7`8vKWnp9?=4MEL~$g!rr%*STXeH-6;H z6uKtTvUT|Z9l~P`GdVEGg7S9&xD%b*tlV2(xwmb0rHiazvg4OzMvmccqmvy-&mpb7 zKQ~|2Kz#P?tZ@a3Ku{hf#AKPi>_k+5GBpohvPESKHDI3Si*8VdA*1Afvzg_`Jm}AT zNz>yU4=hi7ALnY7b1#Kq+2c;0BuU$1E>_;)qNV&&tCAm-NtHH9@(UfCtyU-!$~e51Y!-=U0ZC}Ds%sbgV7F-(}1~al^yTnM(=vO4AT@ZJPNHS zYSoL^)@f!hn`5qQ_b*1FnP=F;Z%?(M=3>iIBnVP|4%+;{SY36ojf>kB#*3`m^AYk- zsYxmfOvmEebux8^F(UbV41%oT>RH2-lqlT($WBVIcq)=$uHNFUa6{ShT_f&D2G3Zd zYB4Q6x$hA);nK%Iwmb5dJaQl9KtsU|3*}f?rYXCVm%df^FZgLqx`LuD=v9p6=R*Ct zy&*?QvX>2yQnL6wDZ0~Oa7>k(#r|jvI8(;A8*!TCdxmE31Biu#W4Bii2yYPz;)O6O zie^laI_s6ftDk{~ssR{-P`mX|yNfN%8rLc8SZOD5rswhyT`bV^_&=ZC&)KxQE_ zzo{U_VIn{O2+#p=b37udO)y}&`1;3FGh~1z?x6grqho}kvj3+3kz1c6?BW5M9~EfK ze6w==KECS3Hx(jWJ6%sWHD19uBTRr7!~h~dji-yCkXdy)OY@{H4TM8Ey79y?qxRNT z99M2Q!XPV!>L_X+!tL}tQF`XVX;o+gjMQKhqAY0P?+7#$C(X^bW?zha=#b?qPhGf+ z7q`MM7r_J=$qu`DFG-wJ*G0_Hj$I9L^QD|t&CfyaLk*L;9)+z?>dS_%qsZHdun?dx z6G|)}jad4Kicu;lcevf=gj|_k#P{MSYa3JPefdnFm`u>}TI5>7HjX^a2z_Kh-zfR~16cxo?3*E(`%bqnza(6^TlOKow!4`v!7}#p zckDQTt4ojP6|+f@gEUX;L$2B7@>^rR6BFwAe_Anxvk z5KMgnYF34|@~DcoK?SISYI9u>B6%osaaJ}c%4>$P< zO|W4`!p|1yB4$F_k2Q99qu|F9HK>AuP8b-_g$pu#ZNQ_T)!+c+TW-?%v=j&VZ6lO0 z=q3YR$%>J4%LNH!+}zjVpg;BNx-zkj^gSyw{n0NA86uvW)jSbWR;p>jPq`m^nNOS@ zP*wf$)nB5t24*)zm6x>j4;z6>VlSgO5SVLksrqwi)pKq2?6rXMY zlb<={z`JtWqfEiW-(koctiokiUTo%mI$$YQ&NSd&iYN7k=Gz<{uHfaxLf3bbkRxwo z-)p21NI2cS+NyayCEsg6ypdnw>(m04SB#C0&gY7Dx3sy05gR_%YhN-E#6!2g-@)uf zah}hzI+#&SgeVk+;39L%KJr6yvLz!MCBe9UwBlf@WcZL-b%t03#ZH%7-Yl{bx=k0N z2(O>n>5j$Mc>fhOR>K7uIFL?!wjFG-9HTLbSeW#fJG)-~w&f%4$$Nj&$&Tr}1T!M( z;yT^$Z}}H9(!0tPrMH)L3v#!gFWxcr$`-FwnQI(3}88mo;xzNBby z&`GkXPjf!lTlik+hu-VM_S3|R*nkN0uvKXb>I*q-5jdcbipEw0wlrunk=^8vB6(Q4 znt!!Y`xQH*i5!ZG?RbI3W^k_NmkJGqP+>aA`R!xX{7TdzVrBxA8%DR*ePS+PSH5LP zh`ziMo-w>gPFSw8%*ph7PxVdWokMcz2LQ_qum=pS_;VEyWPeSu@_ASD)WN% zu29XW5`9BNSw7=+4%4Y9&Hz3|S+vSyONhbPd?npu6u!6|Q@pOuDV=wL-|G`-ZVbwO zagz}7XEu;Y;}P6aK*|>N9xjsP$4+FdrG-+II*ndL-HtH8X^8GDtkp&wc9O={NVB zby>;?I3&<(7~*p|1-#(R+bW`2#PJoH+wE{1TXEQ`Q`dK$8NF7skLMJxi*euc;lr^A zx$%{_{!s&#H)QI>kOO0PNY{! z{#>AO$K=^^7B6aD#Jz-dl2;5HvD8Fr#YYi3oic&?EB~Iis|gP!ERTa?gvyVFZP}{K z);-5YdCuDqg}{P4o6Y>WLMm>dBzN-nnz=882PT{Yh*}BSjBG+|xa;A!7{!wBRU{X)dOs@bUSjkH?->^M$PlB0mjno@#*NP(KQCrt z5rBW@cRgFScvqm4UkU4ybAK5ohHnJa(MHW_K15%GOc$Dna>l-*>Lh6u%CW$?RSMw5Tb5(H_s;I%tZ3Z}5p)&K?|(C-8KH8- zz16RvtC1kGukXOqv3T{J8kjK1hOFffCN296yB^)W?&`Nk0&Q89jQ(|*?krjAChIHw z35bj3Lr_IKm|i@ZI9U4OUX&pavJgks0hB=H|V&mXWN3^IRT_ZM@Z$nN%} zS8j5%W54QUbo85xPF)D#PnuM?13V-E1(#QRvFFm&9>~K;b;*u)kPCud9P#`AcHgu0RNk>5WvsEf+}>a^L;(}i?#gi8r|EOq)A9x;)E=|^h1bW|Gu_tny6Ami8`a-+hM*DBj zEh0b7&E*P+*^DhU%$CXzi4q6Zjr9ca;5IUfW-<+#Jy>;?01pfXcBA`FE`Xc*s^{R1 zFLMvEjCu**X$sUYU{N>;^c-bjXgTCx_Cjk)iu;sK+!tb%JSI>PwY{{m^mvusHeN8S z%<7MULRmPqotMJi;fFM}`c`hfY#CJD@b9*_APxHL&!p^+y;>J=WG>mA!wq#;8>~wH z{Oq@%eV8HFE)%shq4P_r{2gF$R?FZ06||IT7hbdOncIARwI<~^3M?<1auj#$3s%_@ zvGK4?OL?5zkt>oVYJbn!t#x5xffo_e&2#J5z|6I%b(feQ4u&z_pME-@P=}^?&@#Jb z-6!m{7aK7yN6X%(Te5L+L=J%ZQB=m;<5U4k`1$9$XkeZ%RI;jFUBu4K2nLkR87*WE z4_Co#C2W{qBd&AWhYu-BL56(_xBX%DfDLG^$LYz-)kRqLxL}($9dzC~m77qXGUb?< zU?vQ_e9!Qle9-3FPn$zM8AJ_e#G@ie`-W%-o_$KlCDU1@BuxhGKzs_`Mn*j}GUsFa z6k`*ueJ3{PuqWi%!yAifpj%?21oGOQ^LlOy-J0c3pzou!E7arT8>IC~!8QxC3QLml z!xhR~5nn2D>QMTb0p@^rXyC@+Gb=oenWE5zv|zb_$U#`J&3O7LyfBnukAzMPqezik zYuG_~WX(}z{(9_CmelPEa4+9{&kDb*-CmRfH;G@#XuUw&mnCU)i|7FjL6U3bS}tbg z+=ki_CoSJLPVE6b0$2X`;ow8Ouq3@o`)a=od=}8~1vL8?9J`1_(U7-oTo(`gML?kc z2aC@-w$IeFEsILXSM=(Xz))bk}%^5X2` zeoUF+>3S4-dElRpr1Rn!UX!ZhPkwZK{yu{e?y=9C{W@J!MbCQc4wg-m%=QS)62+V* z7B0w`+?7hmd8k`DWQVYa5DIun6k~XJ+Hh7=A3lGFe7A44PU0r@DQFc16i_^j@K#Yx<)~KKMR1DC3X)U_kHjk`4a!yPx_A6U z^GMGC9P6TauVpJy5CYs$=&nw;Fo% zFJJ{m2CKnht!x>?iBSvGgrRk*>E8`y{BZhTBvieGO?O4^4Yu3slikr*lK@t;yPG!W zG{nRt_Thv@V1K_ii8};6dN}{%&wYv0fHQ@;)9WFYOKp!KhHDPjJ47t5Yc6@9h?4@W z589D49j3gAIO@|M@Q^7H!fsc8ngeb>2l5A<;d^mNUA28rFrp$r`yRed-X5&zyV>!$ z&Mt7a6%IWV(UJ#-A;JytF}|#n0fvX92;+#~<|2F5n0I;X&wk4QqdbVc^@%zMrn3j+0ur#-F`96mWT0WXl z>xtzQDmS>rwk115!taz9PrgkdTD;DAZrW^Qw&RtN+Z^&xIdEFRY;21J8gTNhk^J_2 zkhPqgUhp?{D(@^8ct%e-`Vyqx{>J;APMd*)o}`|(p47J!MvcEKQYTNj_BDtJ=I&2Y z5d59<+Ze)6?jr(L-@Cxjc37mx)Fs5ts7mzTgT~7Zv)w;_V?*{CdpO|}cVx{Mu8iGj z9e@ATZ9k#_`gZ^x80U?KM9=}RY*pT?+hEq{&S)ktq}u7mX_^1>NNs2zA4YVm95_*a zmm62T4lWbH7H}RLz#TZ_qqt(a$9N!0t2M;BEVCo%z5VgA2g||5UIR(tkCQhy{5O~U zeoPnpv7p%2wX*%Y_PEAw#bG@43thcx@rzchENe;o91gP#r`D zUrNaOs{@T75+7#yUKCfP|CF}ERT7TBFSCDb@7;q@p}*z1`|FWsmpCTR`22tJjEsOU zu1=IprkL5gpdEieEuw9aj2A*@7v65#?M#7qefm^WLqo!1B3))6_gp~pvD0St#ab#&qR5K~rK^Fh)B?kpdx-pDVe8C6#sFYfdEDIGt1!F`{fyFfY% z(Pp9X^uzj_{U2IKSpim?MQgFAX-8-1zVa+Rm3mN(j4LF;?DPf@mgp2Jk)k8M>TK9> zKZ5uG39IRE{5~+6FzcwKKMfYtyI7e|zuJ5mBTYzkdzq+6&8$VoOJpL3QS#5=>iQ81 zEHqziY+`B&-pY1zBC?0US^?aXzYJbiUqm*mEXbkRe>%S-JWq~;7lm9$$Yn$+;fi7% zQ)2c7-ax1K`@iBZvnDb`ukz~EwS~u12@zpWo%Uxmf7sljiG3yN(_w0}J!^1dE<(;D zy#c^YUu&+Wdbno>UT!<+eaH{CTVQ%mVvM_QPIxbzsJyU!;xt`oS0YtxUP~84dlcfL zxVU0;xz!&@+=^%SD0W<{L!LqWm!;u<+7!Rq9ql!i-~F~5eE(a#(wT0L&^xZGFZ!U=Q z%T`aoU`Y?c2C0DSJQ74ZAXCLkQAvW=GAq2JSkt1n`mOb``vFGIo&M8!Sy~SxRPW00f=M&X|u?J=0#T}*rr2;Zfz54yqe1;zX z`|*K)YYY^WKa_Ifg0{XTzc3Eyifpg_!%dXyt7W$T;lcAHQ_qNhreBmhCkLgj)DV2rP zAXn8yKgi0F2Y}F^&mmOYwjR0oVXP5&u$@}`du3K(A6ID~mnCW3(Pp~5v@?omfTdZFh7_E2 zN9g{Fb$oW$dnb&AZNxj49ILjTys>H38yES73}wRk+qZ}e)h{BJ1-yHTia)<-Ci>{4 z_revr#Ox2rDdRNLthZ^}`aG=hY$I(z6dOSl+(`sHL+p41`P67ER*o>S)z61dW8Cwx z(VcTN;9LJ?{#}w$k$o7 zClUApN^T%;>1%lz)n!9>kJ~w==F>eUlzm7Jws_1tKXPF}gX0Os%hh`lzuH9!MmW!+ zrsAyo2Wb$F#o?R;nnj(-Q~2~9!QDDo5jEbzx&UGJAR4foY5s0-ZXw)?7`nrNQV&NQ z0rwvf!JLf~G^jE5UTxgFf3b4iJ&2d3@3mc1;lbV$(;!OcG%DXQAUw-!@Z$-((xcqo zB|;5=sF}}y8<1na&v^=_^x)LO*}!LS5i5FA#tJt9c6&u>Ax1GL>x>~zAc$WL4cxlu zb3F99CLMyb)Ep~${VztORS)mbJyhJ}DhHff;gCvJ25C30hCmbI_r%6LwsRE%4!zYp(hI;DWzcrtRJg#R6eGi6@^b~$SB9qx z_?X>THTL&CRBJ`$KMs2rBsf<-H~gpkC?j2o0g*6K$26e-nB4F$*NDUUR6xmP4ucUnI&el~>|21msoE(25W^`gB@VFwn!M<2c>(bdjqjM>L&t+k$i;H~n3i4X zce=X_fJnakt;GB|&!w)9?Mlna$N;B>r@>y+#o$OfTm_q9RRXx%a+eIu|=@EZ*zGl7f_$%pru&sAwaAHLjO=+ z!&>bz4`9`$`mqB5Z$V)vp?7reK!Bme=G6bdOTbyd914B{kVlwJT5LVUL4!pX8Rjn$ z!RufMRk5P;nf;<}Y1zdW4~!|ez5+fru>l>T7_hfpw=5dCgE_|JRkXw^E$1o3;u)~= zr14IFX8w5;LqD*7wu-3?04Yr*>g|)UaALYQX46dpelAyMt~Rij7m`IJ!AC*b2eM)@@OQ$e zkYT_#I?mPG2Lbwy)K6|V*T2gxP;K~0jPLBdj@5vltGh{uVUUZoN6}oaJr9G`-%P|3 zv)~TOswBTO*#s4_ul<>g`E=~eV0Lo- z!ZbhxLidhA#!yaZuW1Oras?b2D5Zx z0h~!risRzoRnX=1t@A?Db(Zn?W-#z3iec%M5GoeM zK_737e(S{gKgGTGKh^&q|9_4h$+0)b-ZHZ}I5rtYc4k)gUO85d8M4ck?2+spLduR; zMnd+=h!DPy)BE%JUcUdq*N>&nc%JikJ|6e`?S8$+wkm~Wa^(fEVzdOEPtKOY!u~OU z!b=%0gbQaMhq*Ed@p5xRkce&0#)#KF$H9z{Py%D*%x6OsP8lTkey;SQuodFw9C$Pq zpj9&{tBUU>7q9DKbiCCc#p@JUbYaCRohJ zJlyT5*EF#GNqlIs?G+?Z7mRBNgwe_d{8~jhd`zGPeh{L%HYJx;ObBpQ54eH?%3jEw z8rS*l%^^y;@k9=VlG7#sNhYJFKnL>P62^PE_WDL}&h&jcmd5_hMhppaDb35!W}RGV zUwVYW4)>_Y2OTYm20DLWGso7)j*D#tJBSzfw@~X`FEvoTn-5nw_vH9CMeq?f-}RCF z`D|FxF!@TOYC=V}hTKS?Y83t{&&|h21QKA>5UI4|gT;1<{~8>0;mHK|+`lulMFT18 zoeX{xvd?*PGBgry3n^}k`@3wjf7mmqW+}?9E_%YR!g~l@cv&)L?x9L%QcTr*R(8K) z-k<_9>NKiZ2k5UJ=kl^L0+4(;K%1BFu~P4@cK;a|>y`n8tuK&Gu<8)89DytsgsLD? zJbI0+0%Uq)Hd)eUCpIWUm@Jj~$$Kc8@*#@+Z@?mo14#Q)UL&A52RWeG>T;ff<}%6v@1H0TOMg?X#{qD4KFI5&XWV=TAcku!3|wJFS%%gfQ6ZAGbkCA z0nOuW;3Yl5SRiLtRxDoY+GrIwjmS~N@ew4D@mNj&hp7^$-ciyw+Pwn8$Fo({*!V^; zg&g*z2L-MCS%?qoPc7@zM-MGUVx@GVSpPl+aOnt;tWjL*3XK4tU%arZoySUhv$c1v zZgFbU=rWS?c>iE`vvS#+EefB#IgVeATA8EE4$X@*EG>X!g=;MJF7Bg9EYBVVBpt6mLQ=wr>dF(RJyucw z27Ir8b^N101e794ROxz*0PqkhES&i{X1tpx3J_&s7rNo&k#>DdA-{Tsl9aXa^{jd! zgpld3tLh%Es01u75H2$H?F9||U^pk$cqCMi#_iRUVeSw8pC(T!pkvR%wGH^+bRKVv z&GXHVa2y+QX15ubP?bHjGU4sr(=*9w6a0#J*lc5ip>&;mCs1Oh==>a=+@gPGeGAI6 z!2JVt8Aadp{9fSb5LCJw9>O1xC5DU6D8wz)#`ZZf$Mj*=vmwN|^ zSFHyHUh@7_Q3OL~ul>41{-|HbO}FoFuU9^$PLw|@>~BT$_#QmVOv94Zw4xt^&=e>K zLVAx85o_+lJI)$c`5~v@61mo{?`uA0}wHGKCt9Z{vZ-=GPP&P#>|l#!iDbBTje z%7Dx`qg+R8xuZ~`<^s4{va?UhWpWOAD|6N*CKKU!#O zqPV^G9b@MRbPA-vrY&Cn8JDhNsZY2Qw(*?a~v%7D;`LZbgrC2b&xrnJ4y3f%nXm3uu zH&LP~K78B>0;LPT?>>2w+NE|0C|6)G{k*Dqw^(mGJe`7w4Pp53eJ(}!44r;rVzBDR zy)_Vnr5stx&Cw|SF)>!8v235xAAfxZ{YT48Z56WtH&HIxR@iN^yWzQ50&mGTRlNWZ zn6<9Uf{4R0Ce^;IWD=If2nqyB^#@$e`Xxu5td>vHL(Levt|#*#MpWVzBzB1f5ay%M z@emqP(V~5B?SlvNI5g!eoG8JbK&=`RUbi_X>Mt5wF>+4EDye}occLD0B>8mI|Fae{ z_B%1YZqIexp-NP1dCL&GIJD2F^}c{<)C|5|Rbo3cj<0g}h50+(*k{YL-Pd0q*FEQE zPwTm>Xu}WNI{3sP$BfNVRbXi>Wh^K5=BVIh=Z8cCTnGx8UX`)?h=JgbBvtaj2Qbjo z=IhcIq0vJC-zsqU5)@0$IG{rA@3m29-RGyb3p9=;hpkO-#lB~V`Nb3b5j%@P9XP_B z*A0kVHoK8GYUXd%hBU!c)q@mqA2h3yh@J3$mJ;{-h_awN4>lkx|2JlYRw-#ioPOi! zbwCW(3nonKOhN7m(#=dIIffZrv?Nj($Kjsb49o4FREe&2sz;ae*3{Sw;? zJ#KY07~3-MW*n5vU)wn-+)&47y=mWS{q{U)b}W8=33noTH=}cEIL+zlCYv*H?6X8| z+VCGp)^H;BjFB!xL{Bf!ckV|J=#|OfMX^VIy_;MhoIx=|@*@^ErPf$2fmokrxfJxn z>f;E^wVswiD!Ff)VzUQL;=mRLErF8QI3?s6YUd-5H#!G~_JW3EzbQGk!UI0PKD{jf z7sa+pWn|1>W#|i*AJJTsR@rbFk+Mme8cQ9;&Kzre9aEd@AI31U17B|o$%t0n&@5?p zKakM>LlLl4*x2y6D_Fq^QBq4j>JN-YiCzAaR}xCAr`>Qwzb0dmNd?(|SEx4omC+E5 zlVAh%ALVlM>)4k(Q~!-;UZ1>G@GBio8x0n*_mWkKZc!;W7m4`XAB6))I3nQ6qjd%} ztNMH}*P!dbIZ2OtkG-NSQs~n`mG~-feJ1d*5;oOdBE(=JTg*li$>Y3a%}-V!tfl${gG}vx}6=e#e5pC zLCEM54d7a&(Q!6k^yvGgzN#EovtASK67~ggt~|o2=4qk$^)U8TiyFdDI+7E5xQKCh z>xX~Ucgm{Pqt?MMDo<5(VTDiPze9)(;-`?1yvKv#X7?lidY4x&mtQY+x!QF;4C<*k zc`=NGB$S|j>Up;J{q0TY?*?xO=_S|SgCC|9MXsU?k82k@s10x=Pwb)0POetfmzr@9 z4sRst&sG`gP~@%KCI-w^xkAgy`AM0+)m|fF+OC=ctGtQ@I)uO)?T>+wKA?>yJtst? zl4{Lkf;e(a>YoBa$2I6VqopAn8_x^@UxKhqbcbamqsAh=76q{{d z(R{GfK<#H_*_pQR0{qjvc&V?T=;^gz7G*O%12Xv^4aoMTzNQ0YgZ$a5 z#MZtU%h0_W37JQ*t@F-++#P4mY2Fd5uui|@$M*;}VMFP&vH?DyA1E|{5d|k>MdpNs zcQ4A#ID@JaoCO@XY4<66t*Y)6zy(t_GOpM#c}%-XEGZ_~1eTJXlr0ztL*p$gg%fyeX?p(|%b$0b&=? zw6$LohapI*AOX3UkCqe+NtfxXxKj;(7H}Wr(CD*HGN_roZFlFw%UJtpzYRfd6E=U% zO2=QcD-l>d>;8oR6IW0aj%u5q$(#P?U372uVo5A$2V&&;F6<}V!GMB6KYESg3l6XS zb3MA|Dt8KcItEirz|HtU!gC|3L7^4x{&ijC<+A??L2DxMuJ(Yme`c z@7T|03%e!{aP^ZM0)MK_&3NNth1MSCWF2qGyLOzNJ0%S(U9}`KZ9H3-^}+q#S!q~w zHamxJ#sCS0y=istyl)w|<51}N`=PK~mHTq*(wFARcRpr+udf^LLP#^8vI^3b2UM{H zKIYEAF&e*oFc1ThanXe@h_aC3+ho-xr;1TKQH@c2(Pns2;I;RynMa1x1Na!vDzYX1 zT?cFi26gFTu3@fOU|A5YFBJ=3Kg|~xLswA^_EQ5)Ft<%RZNXh{ zd?#)wE5iebJgJ#Hq?fk%yb(SQIII%#Vg#8LcNE3IvFZ&TX};%e+b2)}8KZsuF_e*N zryRNO+Dy)cg|vRHj_{eroD4j8{%ZbUqaw{1f7w=MBi7*X5U1!xOStpIL7oF__%J)7 zET4pRonrYTV@B}pRiw`vHf!_QQ!s|%fL(+c`O*((tcXx=yJ;xZ?Ur%bo^~=J3nO#! z@-lMU@2@&s<+9T-=hfn`uhK78bhjk-bSICl?vBin?^hC#0$!bcp@OMz^TLQ5jEq&Va$h*T$Tn7;4<}XJ zoK*p*sK2%($R#dg9X1|X9%7>rvI%0EDyGra1uI`on8x~DUgz79U#-0bf9t**Q#7$J zK_a%Af|I5G(W=v048M|*ToldQp8o11vEc08U>>IE%XeM)^ZD*T1wnXOrG@+peMfQ(rbQGKujNC z;p_MLH@ZBjC=^i+>ilmn(WvU%Z`S62s4LbnjdWp>iqq^s+(d^af4vtl(P4q((`N)$8;yP2UXu>Kn-+Il z)S&(Oc*fjZbnk^yr*t;|l{OK1Ctc3dMJ9aKgFiFZeT?2;GM?(zKQ?kj^amF2IJEmJ zZjHuEs=VIoNP70rC1!l4LU`%X3*X>VFFKNz+sLKME#0t5mFMXC4}5&KR-SrVKO@0y zysT!XbT+)-zwFbxVyyt0yDiPzamwZ3ztTtR_M%*|2yv+IoW7%0ifs~h(K@WkukAld zWV$6^j1m1tu_cF>%)%z|n&D@+#t18`IM7~yFUyxZ z7^Y_ARBxXAl3tRz_s9%E@!|TwVG<6HyX#1CRo-vMR3xd#WEIg{giqVp8&|beD!OtX z%4W`FM&?i1q*a@-c0&CEYQT|aaqK4`Ss;vqmQO?XIt}|M(sY%fGN!S zpao%$4Mya>O6jgG!xOcl?&O|h!RLvwTG&DXo7qt0_G`&R61G7R&=ka}tM`DUib{sv zslYzbXvQvZ47hf}uoPMZ8RtUl>iWd%QOHLtPZCbW_FaK6Gz#_+bvw^NmlTCuoa zf27$-g360Hp^~w*ur{afqH8%ES!__wlq7Zl_hFsOF5!>EH07 z|HJ0`oXuQ!UA|fSGyGX<)2%s`IwhaT=z;%tR~FN>m{}Y-eo-tSD`3a?pjXO zzuy!3biOGuO-}h(cM(*DE~f>wBxrQ_%fK;*)J1v*XY#CzXi8*tb+n77PUrbx^XN&Q z&5FG1xc1mcuw2dagK6ng(Ii)G8|co7C+kAPzd+hgmKrcJX99O}Hy5i{y#pfnZRXZL z@#`$$vD+e3at{~n--bfoM8LEN^i_29=tmNGu+pf1d?#&?W@xpy?=o9|0ZF+cTEwNw zez4dB7daH*0Qssnba}OuGbM*AVOJxMn;4ZWNUt1Xkyy>>wP=vx>q(?Nclign8?8mW5r9piAZH0WJDjbY{&QvJi6t^jD;k-l>YCR zEkaww?MkQ~gV7>4QE^oX>Tfr2zps#3WxncRrFs;ehjPdHMfdCE?h`S-F3k`P?CC^I zO}VM3$gb|0z%fc1?J~6nIsvH2|Lc%41zQ6y4aTfCK7$C zUIDh!u|RSb9sN9*TR1=Q-2oLpPZd`ZM_rQmz{O?4&|3RHXNi~Zolu&4`!$ji!JgJf z7*4?ZPnHcFmdt@>?KR&gD@OXvYWM$+&s54(&-wjkz^! z9z|-Km_H@R)P80DMO?I;O_+c&wDst~|EDXDzct3@f;hRCz_bwLt7i3H*3&R%Ao|gF zuY*%Q_s@K7E48Bjme+5~+kv+_YBBT5Z#qvaOzQnfm`svFhs}VO=a(m)fyZAY4lsg& zQ?2Uz>9Sd3mG3n9+!#IdGYL47kQs@tMfl^t#OprTOkAO0`xL8kXxwEl{XX{kJPBNB zH54Gigo7ZP3M7i#lHKC&K#Dagr;SB=Z`DQ_G-2CFvGr?|Hro!zC>#h2Z&ww$K3Wts z(!W%}D#o3!ed<4!)NC#G=Fid2;6c}}M6qVqy!{c&4>)v3!mac7OlJ>)5CM$0Bym*- zg=C1m7UQR_Vkeed<(!bLyt0=z)W=@z_jTleJ~2iZERmyCjwOccIu~jdiHGUkJA`?@ zE?l+}?IviA^xxtmkJ+dNA|}2Z$1BlXt?lP#-soGMTpqS}hi#lF_uH}? z9ph#5POjTkLvRqO3A5Jx?9d)FyKxj*`WS=qk5}7I`57tnNJBVfJ`N?Xjbu($^j6T^ zHmb~koa#eLj9|0NL8<9{_)7gXC>Z5*!bLKzy38g|ae)5Doj^>?rz8pB7`W)F0^|cb zI3d3+4AQijsuLJP4?~9IxvsH3E78dLl*aqbr+xreKq$wXPE}!BYNO}k_X{Gc{V-*O zJ|4y9fx(V?c$6kpmC|+~FYX<8P5v!+$n&T4TVZJ*AMtaC511t!z_L07%m3OoFL)h8 z!%3)MpQOUaEeWY`eXNP_`=(HIFjhm7sMjqpm_g=KdJuOq1s9l{{7dp1?_v47&>hCh z$2C;9L?&=`Gnr-jRCZbnR>4({cIesU3Z>o+ThAu$cc}1;M^B6!9X9#mBu^`O75wBe zdWop|u!!gJ(3gG#f_P2+W8$-BFRSDml?H_9hkePCNY6!57t%BcX+9H!d50py# z_NNyN3WjC`DUEaNTLBL@Zd!7>oo)_qu#4w!?k3dGw@7LTfj(_arDyz*5j2KCeGPJp z=oYnRVF|C5=LzRyN?FGM*$pTnA#{KcD@3fppiaFS#(}5&yySUuOMuKkC||mO_40Hw`lq zbnsicy=`)kP$tMFkUAlB0{d(R)D& zB&;&VPs>At1nI*Rf6w^4^euPeHLW` zZqR^&L`Xr&*<#uVC7pRm!pf{VO{;$ra8>eF(x>t6)fFgIi#jnDpgw_L4n|NC3jYf#QIS`&eV8oQ zVI&o%=I6PXT2YPp>vZAGahv=l#q}**ePs~o30n|mTEI3XM=wZH)V5Mf2vQz1Ipzw0 zyL#2ToRjvYx$v(qc@e|`{|)+Y1n5+v!`$$Z3Ht>2V?^lto64h)AND?c6rDiG`qt?y zeoE_;L^Y5{JM4VY0;1qn9On?Fb8VJ^jf6WIzRy%&w7^+)@;-?_AR7F?xFs?oY{tVm z0!*V2Za_ooGy48A&j_aQq(5*4!F~q$Z=M=NB5j0E_@?UooJaxAITgB~%4G)1e`@fi-PdjmvFes0_TI*ex zLq~q_{NjddQfS$1pW{Z6QGW=U{BOK90`1xe3;i|4lZ*zn@ZDMT|6Ye7-z{J0^lOBj zF!=rl<%|z)Ga?F?#l2xpPdOh9q*vm@zM9o$FL-PCkPL&c$i0S(zme#!yG;Lqz@iyF z4A+;9N0I$FDM{rldG^CkB}%gaTZ&<&#}NI_qU~aR!4>n5?eo)=bJFrRDaY83%EXrPZQcXPMt0y zoia~ABTT=^F8-?y+ygj?Qwv-1$dQA}dmqsAG|__=m9Oo>@2QZu*==^fM#e7> z+-^MKY963@J!NWP!Po&3NpF3Db_RM`A0}Y_zT;nTsnqdxY3UEK{O;2^moQy+vR-Tw z*&ogXMZjsYf4dX-E3i*%w>n7VPV{1D=)gr5ZiTZ89~Ws`I!I-08QzrH}~K<3SrOc07`-oleEo6hyO!8wk*K^D+h zr5CWJLU56Y!AItz|Kdr?C~nK)vVR~A#AosNNB{0iVD-+qN?ie zC?F)cVOGj*?+|sTV7P%Hj00)*1gn7N7bd!-a#6eW@N8<;Abc2~6ceFxnrjHDLy0`s zd%d>WHyAkz@=9zMmTGs8rmuG8%J8|EH4g&L*D_yWgC$WLMeLSER0vXL4=vu!&|(e6 zjI}eCne$(Cso&_FTonmSap&(`Q_E}wHj})Gi(^|#zjF@3G^@o9!;f*wEwl(=tyC!_ zEX;_cwIe2cc|?q@0{lX8f+QATl*)@U3%}nxwSJstYo@w^rA^(cN7fReyU>70t7kMD zmo(n}-gzm>hM?oYEY5itzE+ov5s#3~39p;(%1{a$vpP0F4b{wCGW-b{3jab`^eEO+ zU{3Rdt%&(|8U`S-J0hH^w(-XUry--}%|i@SiPnk=W3cVTM2-m2<7ssPN5fn%G0C5g zun(GcD7wM_UEcS3>H_eIdsqdcR_w>w3T47{gSf}g5+f2wD;Xx}^gl7+V%y@kO2AHe z8AD9pf9lKPk^|`5r z&GKJk)qcDl$GP`TENF-ULjo|Xd}dz>eiI4HpK^FEGw6xgPd$g~cPXi_D%uc-kkSkX zSwaO9jTxhHadpjCOcVuCg1A(7Lgaxc>g_yG@4ffh{jZ*@R6F}$Y{wWl$2@MX&hWut z4KeWTFb(Bc_r9|qiQv%6z#*a*ybWMW@vkU`F?V=yvJz$?juOQIRdM0s_R6@EJ&4R2b7gPK$ZxwKvz;uZcBm z;Mxtme30G&eLY&fEi!yW&&tw4hiLl?V`;V0`?;Sa6JhsUW~!`fd=QId!aP5o^Kp}e zIQoKvulRrOogN?%=kZ$CKs&wfNS*70JmgK@g~(+EuBmhN+Elq0vh;Nj&`^f>&BQQ6b{6C z0s>-Kf$Rb6*$ztMXcua3u{s+ACm^BRB-@*BexX9vw^RMNTRxfPG>yiJBNu$?2Gv_< zxIEALQ!n-a8mSx)-{=;;>U_vQD_;(}-)`VLhtbHR{;R+}4C&{d+XBt*qFGn5?VyZ2 zBW{+$voGRHf4%PC9}QZ#YhuII6M@AFM1)ZO!W|qtULtYD zG)A`jQ^%*jmu>+WQ1%h=z1yLGmSg%a0?i}V+app_VS)&)G4ihOO|JBA^mTodGM;CP zQ;(LvlR9Ri{3o)6ZJS;&E6B?MKcJ4P_Gb=nmXq&gNgI(sk#Svv6HKC$C%g6b=z0ox z=*(sTFfO;{?0YcwmpUjDE=(x!`h4R9#3x@qG{SGsjR2gGGZ0Bc{Y8Wrecv`{!883s zEoGRw-I;6dHY^ZP!umGSFE`Hv;k%VOT!MA0V0qt~5e%R`GtcZfP163i7(4O8Ch1u4 zmF~-;tqvX9e6|+yKU3Ai7dW951wE8~dsql#p74@AvBDQ+(|k6gjNV888j;J@-70 z#maO_b($0?bZ%#v354%Cx8}dZbe=Vak8&xr%7Gk%Be^OSnAuv#c`sWsipS^)&@$5@AkZsl7_pYVT$cSGv;>1mR z^G>(?Hj2-p(b=%P#hZhlI#ILkQZj7J=pTLM3301uA-G$RekBOiJUO;0*X}tA0H4u5 zB_u!b$~{2$Wv3z%Ov7fuN05dDooouuv2A`M-`jE`ktaIS=jKT^-y!hV1+O{km8uXu zb;_rwPLzQCJXb#iq)^GWrOzH&XqJ_(*J7}xbjFi*!-9V1 z?~EvTXLOq%<{4P0ilaV{d&j>8rg;mFn5uj<28cuQPE7o>XDKd3K?!Es=>GFEfG0I=QWQuhsX!Xgr%Bd3ZUQ*bXRSHlEp-6kfADVHDD%R-cIM^PZ$`_$&dB0!jlkoy7} zZ`FhPm$$NIh8nozrn)ooB%qZwo^C=tN&I6Jz4+No)i$l^<$`<~Ul$vTIJJwtq@t8{ zrV_wOnO5DySE}%t%MSy0>@0C4>H=H8;+kzcHFS;d=2*_ty#n2*?jh+ld@Aa}oC$gq ztbE+>QLK>PzOEv%G4w8IIu@pj zckc|artID`z|LXY#IU-clA6tPp)vtPpAv{PfzZX409!+}FB#KL-%w+m2fpV-*+;wt zXUb)H!Vt^M@4a7K@Y9E$;=UImAX!F%3dD}l^$sK=u3Y%87oUvE2Ebmf^A5xx-F$Ux+T zMOXZDGj8F^jXn`9e(`eZqN)6IBu=;1TEHyEP-pXdccsmfcd2m*G5yQN#w~k>*r_zZ z=PJyGIP~Un=_;{Bl;Y0h)IeVn5spvYb1`K+I0M0N11&TS8KiF!OO`gA;0+VHiF8G+ z#m`vR*t8}dU?4ir1PS69V<|}yOF%#fuUlqH`rF4#=QYQh3h_6Th6%&IfF`>^%+b~x z{5dq<95C#hG_ZC4MD{Z5je5LqzVf0F0AFwD-7By8S|<;ok!>J_pG zo6?bJ!+7AVle7Qafus~VhC?8HxXIImNB|| zCVjs?mu^G37bs(tMsBKdWZ{R+dM;>a5TwT;nsMKg#9%jo5K?YK3DoTO!AV&_g}l4- zCc0p-BL;PU?JaeBm^S?6=iP(Qdz($a^uIOTH0maqdAm}ST$gRWRhdUWQ?82h6>HR- zk7}`3Lyd`Zd8>IXO0lZY|3APN0&Q`HJ*2zTfj5 zBM5r1!`{1fqr2~dvD78Z0ZU!|3~vj5tbn1|`pFfS{9GIS{?3mDSESj@QI>g9eEY1= zQgOztQ{v35yXlJ?%f0d2rz362R?~ElMM--NkHtk7EXy$a>!yO$Kfw`W0)iy+zJTRi z`1Z5s$Tc<)+|2*}eMtjJ>4|X**fGM8)CC8>El8l?Aznb-X)X1RtP(kZHZ9VUoJg3*_ zDtbjP8xxILxtEuo4GzR4MQvfI6KmYt=S8lad#)gr*DossMCgsOe{CmGWsH}<=DKSf zT-S;T^_$vf50lQS3$dt!jNK+WsRCw)m8Lpom$)BQE8qrQ9MIj!Jn>OD9IX6G5iq#c zZ_X8S<~$(oMC#AK5&wXRI&+h{SK{JJ56671O2Yk+&6oO3yOl*tq$5xXe3!@s+Ak%a zDK&9f2L zzw>m1#T^VLK{#MK$b5UX{6=kj*3o5#z8&^?TSS^2^TV;d@5~3qdwZ|(-M8d_y?hnnZ|UXxYq*24_wD+DGYR^2#vQ~7?nG_;vqx2UPjtz{SX6Nyis|ir zz2g*8mcNtn6&H=7^T77<^kkJMhwWJ{@b|En4#cr6%v#sSX|mscKc7hMG!lc_bR@^u zm!YP(=-I^hW6M{lc$iM^F>QL;COOf2-=V%2nCD9&IqS4#qSoqUF1pCIbY)%$!!O!w zC~@d*K{;lB*NMAf6zyQX2{@CMe^1V{;^BJbE#?d35bU-M`L(?Co%YFa&9<;iz(H83 zK!{GkTsbqV-jxd0S3=fpulMu0!ZkkMg2T5l20sQQLq5P1v3Mp#ps_R~c2_*TfrbKV~L4Jj@6HASVv;Fzvrme|G}&ae3ausgU1YZrTpvlb2&d z1H>Y}(G%JST#LRZjcwIkZ5xhvV4qy%o+L)RJ(@bp42zlWMBLgzr(K7?xGLOvTLd(H ztgz4rC&4-ctf|{uL#%TLrbwJ7k`WqGpyf-0x=t`ANxy3v7>vVCup>cjsxrUz- zBdG^j9A{Y^f{sQ4fG7Rs6O6YN(9!ewN{zmlM4)ZGo5BLVIs(ejhfoCh<-3ei9 z(w^y2?xeXy=o8E*MFdCRg*oe~_u^vdox-M~-Zy)AxXk%Y!Z3*a1O2P=bD3`T`Ny_0 zf^%S5bk&9y9Y`F~kX}NI46tTw#9Q~64&lz4R4L?Uo}$i4UsHCB?`uqCh@-gG#KwH7 z|Li_Rl(?mMhqYd0Q|mgFd)Ck)e&&HWL9xUA;1-AU>;{MoalxpL@j0Bia%;^j0)tk~c4ejUO}S*c&YR4*02$;x;IzJNBN zvOgXfO+n1!KW#VQ>AUh)lE=C}vlsV{{f>i7oM(N9KZLL&t|ctWAOy;}D?k(u%vy3$ zZ4MR)`Z7v~%s$Q0`0Za|DZ0N5CSqxOouMf*WKmYW-dHJ(Bx0|YCJ;V6aPTQ~oeNn$ zD{nhk3m6tBO1Q zy=OYkeeu9E@!oRMuyW-wx}<3{C1Hb9;}AY_YGr7T!;(?`?DyA4CSzS61Cbqq{MtCz zvaI17OLNWc-OZr3W^by-e)dl5V}FL-=Q{Vr3rWZA zqzgU2y?Z13!&th-qW;qcs00!|pc2g6zmU~Wj9$=7+%s-=W2CL7 zXiFMXidPTn70EFn{?X)qS6iy#0+4YP0_I(tqiIEhzWfY%%O$RvvVK3zvQeQK10$cT zv$eU}N|+11Q7D>?10KH(8%iCW#<-Y%!_g65CSx02d6Q&kN|NoQihnKB`yaI*oXLpW za{8*qgc1PF9A@%vTi<5G@UjtkzsK|hc+Kd{~%1NrekZBKB@9#pxg?v0zR zq%x-cF{EKWy8OI=XzbY`#!YT-`gV?~t$ihvg1UrJ5O}gqG<9Emz3XjIb@D0a3{l)` z6lBrz>$~Hd+0JXtT1fQ~IDdYNF`EHq`XK?LRpqv$ovqQ}j66TZe&<@CMd0M5s`Fbx z3h!rcy5qoJlo(&;eWt82?5qtXiCUTI%9Nbp@nkvE2w%0-nU{%9xYm2`QK%xN%K=+C zIN#}#0L8=3DHHR}^X3C68E`a?MW%lt8$%2SQkZDyJV@{2` zET+YD_Ss^1wP9lDKlfIni4BFDZ?D)Jev34!v>#U0)5AFlXq;u_>zGB%_V9>>}vbX>0v>|KT61S^bq!h!TVgt zBu7Qw^py`>`X>FqG2YQ;WpdifM z@0=xZ*Zr+e+d(S+FAm5bp6dB4>p}i7gXzC_vf;zvfC;=$2(hWPHuV8Vht%{zpc6&=d>o) z@ElCj@tA24TGmY)Q$OBLXo}%^#?P~&@p|=5rX^uKCvJa8*p@Sx+5^zI8>Cbf_H z4TFie^~NGqu#@YQ>fWPwe$f>`I2Rn!!-Ljxxh(Q;DDVZ4+#i=}PX{D$o54a&rb9ut+_#Py&o5WC2&( z)fxxw#ig?-E$@HSgkjRCCer59$a`ro|0raO!@JGUQS85T-QIS#X$X{OZo$%o3E&TX zBo&y2K0iBNf93uiKY?8@L|x>eVT-k~*5|g5iCFB$NMkkUgz=ZTsNir6H?S7&5R zPSCskaWPo8=L%)7Rp_OhpAr-+M#fP7rWbN_s3rIZ z_fapdm|pH+>D}-ufP3iC#5QSZD(^y}{UajSWpvGnx2gGDtOO>=IS=XJ95r7>q}Qb$ z3Hk4vW6ghVugN&MIQ&iT{IF;j7d7e&YUXy053&xZ+n>2WDvJiUμT{Ss{C?f8V>0 ztEp(s4+RzEs?5R==_ zB4UvcMDnREw%h%Cff-MRCry+D?aH#hu~+7Qzw4PPwZdA+*DpTD!{g1Y8VII_3r*fX z1R}3j_xASL#Z_1649VdcD}#^TeBU5`81cnj-H3nkNMdB(6%@S4P{{=44cWbj0mDZnCsyH z2nJ$7KT7e?%3(Tv0*Loc#~Fl!WST@QUmM9zd67ozAg=m|vYb**4eY5T8LE%b?0op>54Rb@ev5DXXjJ0em-DQ{=)}Pj zA5OBahvX!r84=3e$s*-DvPME)P0N3sU13e}Qqbs3GPdBNL=uqHbg7r=1=dZv*G90`S-qi_*@VEc-(z@Sp~qvLeK*`sXMk%Ov~Z1$|G~Rm zXcBU-{OXcQK(KGEI3=y+wpEz}EM@=EY{CGOt_^OiJx&^y^tQNAHB4F;vjuKrHGJgDeeCW?v+C<#M3{c*OPh1jWXSegVYI2ADvK&I7aTY@dRr?Ty2rG({hTy;ohgf$;QW@xQTBWKUyZ!uyr9|r6}+Hy27Hn(YEDs<87+&2?C~F z7qD%$A<0yHShtyx>p7tJD1@$iLcu+K2WWGuh??^phbJiqJayio)J43s9jZRiIl*C` zNd}M-x2#bLFCxk@1+b~SjeI7|79B6HL}1lAD0Dg49A|EyK?e_`^JPYxwRQG(wlPPsG+7qX-0IG^AOswiWD&o1OD`h?TEtv zWld0$wc^G#mjfMK#?PAmU5sU;^G-VoxIqg+nx2hPjm-KZYiyy>2ju7gnl)Ljka+cK zdS)!yg=ZO`$4bwEWe)Af@&h$w-tZB86pC)k9N4l%y-V~vEk_7BPgXljGb#HPMb2bj0y43U6}R!nF?MJk;zls7m!>PqskOiSs`BW z*BS9uZ-kd%5CEs9!O1*0i-Wlxt!=!ZAGlfQ>WW-IVuZgm;5LnT!bkkdPz#VSCWVv} zrN2obyFSltJZLb>GB2~?>(HFqSgMa31;LTbHn_8+#XpG7mF7eSCRbutsUA54P!|tO zUQ}n@+(OQdIbj|lvMkZ@4Om-2LH7qt^SzMptpO3o^zG96A2wHzVFdEI_RLR(npqsB z_0;Sv$&8emzZHlNO03Bik3R)nnrpS~>$-mLXk7^@f3|Vaf%hv#zQeg@)v-%)=%-S~ a4YOay&izsj;T8z^(NMXMtWmOx_ literal 0 HcmV?d00001 diff --git a/client/Android/Studio/aFreeRDP/src/main/java/com/freerdp/afreerdp/application/GlobalApp.java b/client/Android/Studio/aFreeRDP/src/main/java/com/freerdp/afreerdp/application/GlobalApp.java new file mode 100644 index 0000000..7d44959 --- /dev/null +++ b/client/Android/Studio/aFreeRDP/src/main/java/com/freerdp/afreerdp/application/GlobalApp.java @@ -0,0 +1,5 @@ +package com.freerdp.afreerdp.application; + +public class GlobalApp extends com.freerdp.freerdpcore.application.GlobalApp +{ +} diff --git a/client/Android/Studio/aFreeRDP/src/main/res/drawable-hdpi/icon_launcher_freerdp.png b/client/Android/Studio/aFreeRDP/src/main/res/drawable-hdpi/icon_launcher_freerdp.png new file mode 100644 index 0000000000000000000000000000000000000000..ff31f25761bfc55a7cf6922797999b9653a3f7f2 GIT binary patch literal 2858 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 ax%~}j$`eV1;f#X-0000 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/client/Android/Studio/aFreeRDP/src/main/res/drawable/icon_launcher_freerdp.png b/client/Android/Studio/aFreeRDP/src/main/res/drawable/icon_launcher_freerdp.png new file mode 100644 index 0000000000000000000000000000000000000000..53c5b36e112f18ebbf740995d3d865a67cd4fb72 GIT binary patch literal 4196 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 + + + + + + + + \ No newline at end of file diff --git a/client/Android/Studio/aFreeRDP/src/main/res/values-de/strings.xml b/client/Android/Studio/aFreeRDP/src/main/res/values-de/strings.xml new file mode 100644 index 0000000..16fc2ba --- /dev/null +++ b/client/Android/Studio/aFreeRDP/src/main/res/values-de/strings.xml @@ -0,0 +1,4 @@ + + + Entfernte Rechner + \ No newline at end of file diff --git a/client/Android/Studio/aFreeRDP/src/main/res/values-es/strings.xml b/client/Android/Studio/aFreeRDP/src/main/res/values-es/strings.xml new file mode 100644 index 0000000..401d0f2 --- /dev/null +++ b/client/Android/Studio/aFreeRDP/src/main/res/values-es/strings.xml @@ -0,0 +1,4 @@ + + + Remote Computers + diff --git a/client/Android/Studio/aFreeRDP/src/main/res/values-fr/strings.xml b/client/Android/Studio/aFreeRDP/src/main/res/values-fr/strings.xml new file mode 100644 index 0000000..054f6c2 --- /dev/null +++ b/client/Android/Studio/aFreeRDP/src/main/res/values-fr/strings.xml @@ -0,0 +1,4 @@ + + + L\'ordinateur distant + diff --git a/client/Android/Studio/aFreeRDP/src/main/res/values-nl/strings.xml b/client/Android/Studio/aFreeRDP/src/main/res/values-nl/strings.xml new file mode 100644 index 0000000..401d0f2 --- /dev/null +++ b/client/Android/Studio/aFreeRDP/src/main/res/values-nl/strings.xml @@ -0,0 +1,4 @@ + + + Remote Computers + diff --git a/client/Android/Studio/aFreeRDP/src/main/res/values-zh/strings.xml b/client/Android/Studio/aFreeRDP/src/main/res/values-zh/strings.xml new file mode 100644 index 0000000..86d2230 --- /dev/null +++ b/client/Android/Studio/aFreeRDP/src/main/res/values-zh/strings.xml @@ -0,0 +1,4 @@ + + + Remote Computer + \ No newline at end of file diff --git a/client/Android/Studio/aFreeRDP/src/main/res/values/strings.xml b/client/Android/Studio/aFreeRDP/src/main/res/values/strings.xml new file mode 100644 index 0000000..1c00d49 --- /dev/null +++ b/client/Android/Studio/aFreeRDP/src/main/res/values/strings.xml @@ -0,0 +1,7 @@ + + + aFreeRDP + + aFreeRDP + Remote Computers + diff --git a/client/Android/Studio/aFreeRDP/src/main/res/xml/searchable.xml b/client/Android/Studio/aFreeRDP/src/main/res/xml/searchable.xml new file mode 100644 index 0000000..d8b5f0b --- /dev/null +++ b/client/Android/Studio/aFreeRDP/src/main/res/xml/searchable.xml @@ -0,0 +1,22 @@ + + diff --git a/client/Android/Studio/build.gradle b/client/Android/Studio/build.gradle new file mode 100644 index 0000000..81ae37c --- /dev/null +++ b/client/Android/Studio/build.gradle @@ -0,0 +1,52 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. +Properties properties = new Properties() +File file = new File('release.properties') +if (file.canRead()) { + properties.load(new FileInputStream(file)) +} + +if (!hasProperty('RELEASE_STORE_FILE')) { + ext.RELEASE_STORE_FILE='nokeyfile' +} +if (!hasProperty('RELEASE_KEY_ALIAS')) { + ext.RELEASE_KEY_ALIAS='' +} +if (!hasProperty('RELEASE_KEY_ALIAS')) { + ext.RELEASE_KEY_ALIAS='' +} +if (!hasProperty('RELEASE_KEY_PASSWORD')) { + ext.RELEASE_KEY_PASSWORD='' +} + +ext { + compileApi = properties.get('COMPILE_API', 31) + targetApi = properties.get('TARGET_API', 31) + minApi = properties.get('MIN_API', 23) + toolsVersion = properties.get('TOOLS_VERSION', '31.0.0') + + println '----------------- Project configuration -------------------' + println 'RELEASE_STORE_FILE: ' + RELEASE_STORE_FILE + println 'RELEASE_KEY_ALIAS: ' + RELEASE_KEY_ALIAS + println 'compile API: ' + compileApi + println 'target API: ' + targetApi + println 'min API: ' + minApi + println 'tools version: ' + toolsVersion + println '-----------------------------------------------------------' +} + +buildscript { + repositories { + mavenCentral() + google() + } + dependencies { + classpath 'com.android.tools.build:gradle:7.1.2' + } +} + +allprojects { + repositories { + mavenCentral() + google() + } +} diff --git a/client/Android/Studio/freeRDPCore/build.gradle b/client/Android/Studio/freeRDPCore/build.gradle new file mode 100644 index 0000000..e2224c2 --- /dev/null +++ b/client/Android/Studio/freeRDPCore/build.gradle @@ -0,0 +1,29 @@ +apply plugin: 'com.android.library' + +android { + compileSdkVersion = rootProject.ext.compileApi + buildToolsVersion = rootProject.ext.toolsVersion + + defaultConfig { + minSdkVersion rootProject.ext.minApi + targetSdkVersion rootProject.ext.targetApi + vectorDrawables.useSupportLibrary = true + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' + } + debug { + jniDebuggable true + renderscriptDebuggable true + } + } +} + +dependencies { + implementation 'com.android.support:appcompat-v7:28.0.0' + implementation 'com.android.support:support-v4:28.0.0' + implementation 'com.android.support:support-vector-drawable:28.0.0' +} diff --git a/client/Android/Studio/freeRDPCore/lint.xml b/client/Android/Studio/freeRDPCore/lint.xml new file mode 100644 index 0000000..c70207f --- /dev/null +++ b/client/Android/Studio/freeRDPCore/lint.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/client/Android/Studio/freeRDPCore/src/main/AndroidManifest.xml b/client/Android/Studio/freeRDPCore/src/main/AndroidManifest.xml new file mode 100644 index 0000000..3461333 --- /dev/null +++ b/client/Android/Studio/freeRDPCore/src/main/AndroidManifest.xml @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/application/GlobalApp.java b/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/application/GlobalApp.java new file mode 100644 index 0000000..e71a86e --- /dev/null +++ b/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/application/GlobalApp.java @@ -0,0 +1,211 @@ +/* + Android Main Application + + Copyright 2013 Thincast Technologies GmbH, Author: Martin Fleisz + + This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. + If a copy of the MPL was not distributed with this file, You can obtain one at + http://mozilla.org/MPL/2.0/. +*/ + +package com.freerdp.freerdpcore.application; + +import android.app.Application; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.net.Uri; +import android.util.Log; + +import com.freerdp.freerdpcore.domain.BookmarkBase; +import com.freerdp.freerdpcore.presentation.ApplicationSettingsActivity; +import com.freerdp.freerdpcore.services.BookmarkDB; +import com.freerdp.freerdpcore.services.HistoryDB; +import com.freerdp.freerdpcore.services.LibFreeRDP; +import com.freerdp.freerdpcore.services.ManualBookmarkGateway; +import com.freerdp.freerdpcore.services.QuickConnectHistoryGateway; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Timer; +import java.util.TimerTask; + +public class GlobalApp extends Application implements LibFreeRDP.EventListener +{ + // event notification defines + public static final String EVENT_TYPE = "EVENT_TYPE"; + public static final String EVENT_PARAM = "EVENT_PARAM"; + public static final String EVENT_STATUS = "EVENT_STATUS"; + public static final String EVENT_ERROR = "EVENT_ERROR"; + public static final String ACTION_EVENT_FREERDP = "com.freerdp.freerdp.event.freerdp"; + public static final int FREERDP_EVENT_CONNECTION_SUCCESS = 1; + public static final int FREERDP_EVENT_CONNECTION_FAILURE = 2; + public static final int FREERDP_EVENT_DISCONNECTED = 3; + private static final String TAG = "GlobalApp"; + public static boolean ConnectedTo3G = false; + private static Map sessionMap; + private static BookmarkDB bookmarkDB; + private static ManualBookmarkGateway manualBookmarkGateway; + + private static HistoryDB historyDB; + private static QuickConnectHistoryGateway quickConnectHistoryGateway; + + // timer for disconnecting sessions after the screen was turned off + private static Timer disconnectTimer = null; + + public static ManualBookmarkGateway getManualBookmarkGateway() + { + return manualBookmarkGateway; + } + + public static QuickConnectHistoryGateway getQuickConnectHistoryGateway() + { + return quickConnectHistoryGateway; + } + + // Disconnect handling for Screen on/off events + public void startDisconnectTimer() + { + final int timeoutMinutes = ApplicationSettingsActivity.getDisconnectTimeout(this); + if (timeoutMinutes > 0) + { + // start disconnect timeout... + disconnectTimer = new Timer(); + disconnectTimer.schedule(new DisconnectTask(), timeoutMinutes * 60 * 1000); + } + } + + static public void cancelDisconnectTimer() + { + // cancel any pending timer events + if (disconnectTimer != null) + { + disconnectTimer.cancel(); + disconnectTimer.purge(); + disconnectTimer = null; + } + } + + // RDP session handling + static public SessionState createSession(BookmarkBase bookmark, Context context) + { + SessionState session = new SessionState(LibFreeRDP.newInstance(context), bookmark); + sessionMap.put(Long.valueOf(session.getInstance()), session); + return session; + } + + static public SessionState createSession(Uri openUri, Context context) + { + SessionState session = new SessionState(LibFreeRDP.newInstance(context), openUri); + sessionMap.put(Long.valueOf(session.getInstance()), session); + return session; + } + + static public SessionState getSession(long instance) + { + return sessionMap.get(instance); + } + + static public Collection getSessions() + { + // return a copy of the session items + return new ArrayList(sessionMap.values()); + } + + static public void freeSession(long instance) + { + if (GlobalApp.sessionMap.containsKey(instance)) + { + GlobalApp.sessionMap.remove(instance); + LibFreeRDP.freeInstance(instance); + } + } + + @Override public void onCreate() + { + super.onCreate(); + + /* Initialize preferences. */ + ApplicationSettingsActivity.get(this); + + sessionMap = Collections.synchronizedMap(new HashMap()); + + LibFreeRDP.setEventListener(this); + + bookmarkDB = new BookmarkDB(this); + + manualBookmarkGateway = new ManualBookmarkGateway(bookmarkDB); + + historyDB = new HistoryDB(this); + quickConnectHistoryGateway = new QuickConnectHistoryGateway(historyDB); + + ConnectedTo3G = NetworkStateReceiver.isConnectedTo3G(this); + + // init screen receiver here (this can't be declared in AndroidManifest - refer to: + // http://thinkandroid.wordpress.com/2010/01/24/handling-screen-off-and-screen-on-intents/ + IntentFilter filter = new IntentFilter(Intent.ACTION_SCREEN_ON); + filter.addAction(Intent.ACTION_SCREEN_OFF); + registerReceiver(new ScreenReceiver(), filter); + } + + // helper to send FreeRDP notifications + private void sendRDPNotification(int type, long param) + { + // send broadcast + Intent intent = new Intent(ACTION_EVENT_FREERDP); + intent.putExtra(EVENT_TYPE, type); + intent.putExtra(EVENT_PARAM, param); + sendBroadcast(intent); + } + + @Override public void OnPreConnect(long instance) + { + Log.v(TAG, "OnPreConnect"); + } + + // ////////////////////////////////////////////////////////////////////// + // Implementation of LibFreeRDP.EventListener + public void OnConnectionSuccess(long instance) + { + Log.v(TAG, "OnConnectionSuccess"); + sendRDPNotification(FREERDP_EVENT_CONNECTION_SUCCESS, instance); + } + + public void OnConnectionFailure(long instance) + { + Log.v(TAG, "OnConnectionFailure"); + + // send notification to session activity + sendRDPNotification(FREERDP_EVENT_CONNECTION_FAILURE, instance); + } + + public void OnDisconnecting(long instance) + { + Log.v(TAG, "OnDisconnecting"); + } + + public void OnDisconnected(long instance) + { + Log.v(TAG, "OnDisconnected"); + sendRDPNotification(FREERDP_EVENT_DISCONNECTED, instance); + } + + // TimerTask for disconnecting sessions after screen was turned off + private static class DisconnectTask extends TimerTask + { + @Override public void run() + { + Log.v("DisconnectTask", "Doing action"); + + // disconnect any running rdp session + Collection sessions = GlobalApp.getSessions(); + for (SessionState session : sessions) + { + LibFreeRDP.disconnect(session.getInstance()); + } + } + } +} diff --git a/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/application/NetworkStateReceiver.java b/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/application/NetworkStateReceiver.java new file mode 100644 index 0000000..ea3d663 --- /dev/null +++ b/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/application/NetworkStateReceiver.java @@ -0,0 +1,58 @@ +/* + Network State Receiver + + Copyright 2013 Thincast Technologies GmbH, Author: Martin Fleisz + + This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. + If a copy of the MPL was not distributed with this file, You can obtain one at + http://mozilla.org/MPL/2.0/. +*/ + +package com.freerdp.freerdpcore.application; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.util.Log; + +public class NetworkStateReceiver extends BroadcastReceiver +{ + + public static boolean isConnectedTo3G(Context context) + { + ConnectivityManager connectivity = + (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo info = connectivity.getActiveNetworkInfo(); + + // no connection or background data disabled + if (info == null || !info.isConnected()) + return false; + + return (info.getType() != ConnectivityManager.TYPE_WIFI && + info.getType() != ConnectivityManager.TYPE_WIMAX); + } + + @Override public void onReceive(Context context, Intent intent) + { + + // check if we are connected via 3g or wlan + if (intent.getExtras() != null) + { + NetworkInfo info = + (NetworkInfo)intent.getExtras().get(ConnectivityManager.EXTRA_NETWORK_INFO); + + // are we connected at all? + if (info != null && info.isConnected()) + { + // see if we are connected through 3G or WiFi + Log.d("app", "Connected via type " + info.getTypeName()); + GlobalApp.ConnectedTo3G = (info.getType() != ConnectivityManager.TYPE_WIFI && + info.getType() != ConnectivityManager.TYPE_WIMAX); + } + + Log.v("NetworkState", info.toString()); + } + } +} diff --git a/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/application/ScreenReceiver.java b/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/application/ScreenReceiver.java new file mode 100644 index 0000000..d1330ca --- /dev/null +++ b/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/application/ScreenReceiver.java @@ -0,0 +1,30 @@ +/* + Helper class to receive notifications when the screen is turned on/off + + Copyright 2013 Thincast Technologies GmbH, Author: Martin Fleisz + + This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. + If a copy of the MPL was not distributed with this file, You can obtain one at + http://mozilla.org/MPL/2.0/. +*/ + +package com.freerdp.freerdpcore.application; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.util.Log; + +public class ScreenReceiver extends BroadcastReceiver +{ + + @Override public void onReceive(Context context, Intent intent) + { + GlobalApp app = (GlobalApp)context.getApplicationContext(); + Log.v("ScreenReceiver", "Received action: " + intent.getAction()); + if (intent.getAction().equals(Intent.ACTION_SCREEN_OFF)) + app.startDisconnectTimer(); + else if (intent.getAction().equals(Intent.ACTION_SCREEN_ON)) + app.cancelDisconnectTimer(); + } +} diff --git a/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/application/SessionState.java b/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/application/SessionState.java new file mode 100644 index 0000000..1e1431c --- /dev/null +++ b/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/application/SessionState.java @@ -0,0 +1,129 @@ +/* + Session State class + + Copyright 2013 Thincast Technologies GmbH, Author: Martin Fleisz + + This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. + If a copy of the MPL was not distributed with this file, You can obtain one at + http://mozilla.org/MPL/2.0/. +*/ + +package com.freerdp.freerdpcore.application; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.drawable.BitmapDrawable; +import android.net.Uri; +import android.os.Parcel; +import android.os.Parcelable; + +import com.freerdp.freerdpcore.domain.BookmarkBase; +import com.freerdp.freerdpcore.services.LibFreeRDP; + +public class SessionState implements Parcelable +{ + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + public SessionState createFromParcel(Parcel in) + { + return new SessionState(in); + } + + @Override public SessionState[] newArray(int size) + { + return new SessionState[size]; + } + }; + private long instance; + private BookmarkBase bookmark; + private Uri openUri; + private BitmapDrawable surface; + private LibFreeRDP.UIEventListener uiEventListener; + + public SessionState(Parcel parcel) + { + instance = parcel.readLong(); + bookmark = parcel.readParcelable(null); + openUri = parcel.readParcelable(null); + + Bitmap bitmap = parcel.readParcelable(null); + surface = new BitmapDrawable(bitmap); + } + + public SessionState(long instance, BookmarkBase bookmark) + { + this.instance = instance; + this.bookmark = bookmark; + this.openUri = null; + this.uiEventListener = null; + } + + public SessionState(long instance, Uri openUri) + { + this.instance = instance; + this.bookmark = null; + this.openUri = openUri; + this.uiEventListener = null; + } + + public void connect(Context context) + { + if (bookmark != null) + { + LibFreeRDP.setConnectionInfo(context, instance, bookmark); + } + else + { + LibFreeRDP.setConnectionInfo(context, instance, openUri); + } + LibFreeRDP.connect(instance); + } + + public long getInstance() + { + return instance; + } + + public BookmarkBase getBookmark() + { + return bookmark; + } + + public Uri getOpenUri() + { + return openUri; + } + + public LibFreeRDP.UIEventListener getUIEventListener() + { + return uiEventListener; + } + + public void setUIEventListener(LibFreeRDP.UIEventListener uiEventListener) + { + this.uiEventListener = uiEventListener; + } + + public BitmapDrawable getSurface() + { + return surface; + } + + public void setSurface(BitmapDrawable surface) + { + this.surface = surface; + } + + @Override public int describeContents() + { + return 0; + } + + @Override public void writeToParcel(Parcel out, int flags) + { + out.writeLong(instance); + out.writeParcelable(bookmark, flags); + out.writeParcelable(openUri, flags); + out.writeParcelable(surface.getBitmap(), flags); + } +} diff --git a/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/domain/BookmarkBase.java b/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/domain/BookmarkBase.java new file mode 100644 index 0000000..171f279 --- /dev/null +++ b/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/domain/BookmarkBase.java @@ -0,0 +1,1063 @@ +/* + Defines base attributes of a bookmark object + + Copyright 2013 Thincast Technologies GmbH, Author: Martin Fleisz + + This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. + If a copy of the MPL was not distributed with this file, You can obtain one at + http://mozilla.org/MPL/2.0/. +*/ + +package com.freerdp.freerdpcore.domain; + +import android.content.SharedPreferences; +import android.os.Parcel; +import android.os.Parcelable; + +import com.freerdp.freerdpcore.application.GlobalApp; + +import java.util.Locale; + +public class BookmarkBase implements Parcelable, Cloneable +{ + public static final int TYPE_INVALID = -1; + public static final int TYPE_MANUAL = 1; + public static final int TYPE_QUICKCONNECT = 2; + public static final int TYPE_PLACEHOLDER = 3; + public static final int TYPE_CUSTOM_BASE = 1000; + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + public BookmarkBase createFromParcel(Parcel in) + { + return new BookmarkBase(in); + } + + @Override public BookmarkBase[] newArray(int size) + { + return new BookmarkBase[size]; + } + }; + protected int type; + private long id; + private String label; + private String username; + private String password; + private String domain; + private ScreenSettings screenSettings; + private PerformanceFlags performanceFlags; + private AdvancedSettings advancedSettings; + private DebugSettings debugSettings; + + public BookmarkBase(Parcel parcel) + { + type = parcel.readInt(); + id = parcel.readLong(); + label = parcel.readString(); + username = parcel.readString(); + password = parcel.readString(); + domain = parcel.readString(); + + screenSettings = parcel.readParcelable(ScreenSettings.class.getClassLoader()); + performanceFlags = parcel.readParcelable(PerformanceFlags.class.getClassLoader()); + advancedSettings = parcel.readParcelable(AdvancedSettings.class.getClassLoader()); + debugSettings = parcel.readParcelable(DebugSettings.class.getClassLoader()); + } + + public BookmarkBase() + { + init(); + } + + private void init() + { + type = TYPE_INVALID; + id = -1; + label = ""; + username = ""; + password = ""; + domain = ""; + + screenSettings = new ScreenSettings(); + performanceFlags = new PerformanceFlags(); + advancedSettings = new AdvancedSettings(); + debugSettings = new DebugSettings(); + } + + @SuppressWarnings("unchecked") public T get() + { + return (T)this; + } + + public int getType() + { + return type; + } + + public long getId() + { + return id; + } + + public void setId(long id) + { + this.id = id; + } + + public String getLabel() + { + return label; + } + + public void setLabel(String label) + { + this.label = label; + } + + public String getUsername() + { + return username; + } + + public void setUsername(String username) + { + this.username = username; + } + + public String getPassword() + { + return password; + } + + public void setPassword(String password) + { + this.password = password; + } + + public String getDomain() + { + return domain; + } + + public void setDomain(String domain) + { + this.domain = domain; + } + + public ScreenSettings getScreenSettings() + { + return screenSettings; + } + + public void setScreenSettings(ScreenSettings screenSettings) + { + this.screenSettings = screenSettings; + } + + public PerformanceFlags getPerformanceFlags() + { + return performanceFlags; + } + + public void setPerformanceFlags(PerformanceFlags performanceFlags) + { + this.performanceFlags = performanceFlags; + } + + public AdvancedSettings getAdvancedSettings() + { + return advancedSettings; + } + + public void setAdvancedSettings(AdvancedSettings advancedSettings) + { + this.advancedSettings = advancedSettings; + } + + public DebugSettings getDebugSettings() + { + return debugSettings; + } + + public void setDebugSettings(DebugSettings debugSettings) + { + this.debugSettings = debugSettings; + } + + public ScreenSettings getActiveScreenSettings() + { + return (GlobalApp.ConnectedTo3G && advancedSettings.getEnable3GSettings()) + ? advancedSettings.getScreen3G() + : screenSettings; + } + + public PerformanceFlags getActivePerformanceFlags() + { + return (GlobalApp.ConnectedTo3G && advancedSettings.getEnable3GSettings()) + ? advancedSettings.getPerformance3G() + : performanceFlags; + } + + @Override public int describeContents() + { + return 0; + } + + @Override public void writeToParcel(Parcel out, int flags) + { + out.writeInt(type); + out.writeLong(id); + out.writeString(label); + out.writeString(username); + out.writeString(password); + out.writeString(domain); + + out.writeParcelable(screenSettings, flags); + out.writeParcelable(performanceFlags, flags); + out.writeParcelable(advancedSettings, flags); + out.writeParcelable(debugSettings, flags); + } + + // write to shared preferences + public void writeToSharedPreferences(SharedPreferences sharedPrefs) + { + + Locale locale = Locale.ENGLISH; + + SharedPreferences.Editor editor = sharedPrefs.edit(); + editor.clear(); + editor.putString("bookmark.label", label); + editor.putString("bookmark.username", username); + editor.putString("bookmark.password", password); + editor.putString("bookmark.domain", domain); + + editor.putInt("bookmark.colors", screenSettings.getColors()); + editor.putString("bookmark.resolution", + screenSettings.getResolutionString().toLowerCase(locale)); + editor.putInt("bookmark.width", screenSettings.getWidth()); + editor.putInt("bookmark.height", screenSettings.getHeight()); + + editor.putBoolean("bookmark.perf_remotefx", performanceFlags.getRemoteFX()); + editor.putBoolean("bookmark.perf_gfx", performanceFlags.getGfx()); + editor.putBoolean("bookmark.perf_gfx_h264", performanceFlags.getH264()); + editor.putBoolean("bookmark.perf_wallpaper", performanceFlags.getWallpaper()); + editor.putBoolean("bookmark.perf_font_smoothing", performanceFlags.getFontSmoothing()); + editor.putBoolean("bookmark.perf_desktop_composition", + performanceFlags.getDesktopComposition()); + editor.putBoolean("bookmark.perf_window_dragging", performanceFlags.getFullWindowDrag()); + editor.putBoolean("bookmark.perf_menu_animation", performanceFlags.getMenuAnimations()); + editor.putBoolean("bookmark.perf_themes", performanceFlags.getTheming()); + + editor.putBoolean("bookmark.enable_3g_settings", advancedSettings.getEnable3GSettings()); + + editor.putInt("bookmark.colors_3g", advancedSettings.getScreen3G().getColors()); + editor.putString("bookmark.resolution_3g", + advancedSettings.getScreen3G().getResolutionString().toLowerCase(locale)); + editor.putInt("bookmark.width_3g", advancedSettings.getScreen3G().getWidth()); + editor.putInt("bookmark.height_3g", advancedSettings.getScreen3G().getHeight()); + + editor.putBoolean("bookmark.perf_remotefx_3g", + advancedSettings.getPerformance3G().getRemoteFX()); + editor.putBoolean("bookmark.perf_gfx_3g", advancedSettings.getPerformance3G().getGfx()); + editor.putBoolean("bookmark.perf_gfx_h264_3g", + advancedSettings.getPerformance3G().getH264()); + editor.putBoolean("bookmark.perf_wallpaper_3g", + advancedSettings.getPerformance3G().getWallpaper()); + editor.putBoolean("bookmark.perf_font_smoothing_3g", + advancedSettings.getPerformance3G().getFontSmoothing()); + editor.putBoolean("bookmark.perf_desktop_composition_3g", + advancedSettings.getPerformance3G().getDesktopComposition()); + editor.putBoolean("bookmark.perf_window_dragging_3g", + advancedSettings.getPerformance3G().getFullWindowDrag()); + editor.putBoolean("bookmark.perf_menu_animation_3g", + advancedSettings.getPerformance3G().getMenuAnimations()); + editor.putBoolean("bookmark.perf_themes_3g", + advancedSettings.getPerformance3G().getTheming()); + + editor.putBoolean("bookmark.redirect_sdcard", advancedSettings.getRedirectSDCard()); + editor.putInt("bookmark.redirect_sound", advancedSettings.getRedirectSound()); + editor.putBoolean("bookmark.redirect_microphone", advancedSettings.getRedirectMicrophone()); + editor.putInt("bookmark.security", advancedSettings.getSecurity()); + editor.putString("bookmark.remote_program", advancedSettings.getRemoteProgram()); + editor.putString("bookmark.work_dir", advancedSettings.getWorkDir()); + editor.putBoolean("bookmark.console_mode", advancedSettings.getConsoleMode()); + + editor.putBoolean("bookmark.async_channel", debugSettings.getAsyncChannel()); + editor.putBoolean("bookmark.async_input", debugSettings.getAsyncInput()); + editor.putBoolean("bookmark.async_update", debugSettings.getAsyncUpdate()); + editor.putString("bookmark.debug_level", debugSettings.getDebugLevel()); + + editor.apply(); + } + + // read from shared preferences + public void readFromSharedPreferences(SharedPreferences sharedPrefs) + { + label = sharedPrefs.getString("bookmark.label", ""); + username = sharedPrefs.getString("bookmark.username", ""); + password = sharedPrefs.getString("bookmark.password", ""); + domain = sharedPrefs.getString("bookmark.domain", ""); + + screenSettings.setColors(sharedPrefs.getInt("bookmark.colors", 16)); + screenSettings.setResolution(sharedPrefs.getString("bookmark.resolution", "automatic"), + sharedPrefs.getInt("bookmark.width", 800), + sharedPrefs.getInt("bookmark.height", 600)); + + performanceFlags.setRemoteFX(sharedPrefs.getBoolean("bookmark.perf_remotefx", false)); + performanceFlags.setGfx(sharedPrefs.getBoolean("bookmark.perf_gfx", false)); + performanceFlags.setH264(sharedPrefs.getBoolean("bookmark.perf_gfx_h264", false)); + performanceFlags.setWallpaper(sharedPrefs.getBoolean("bookmark.perf_wallpaper", false)); + performanceFlags.setFontSmoothing( + sharedPrefs.getBoolean("bookmark.perf_font_smoothing", false)); + performanceFlags.setDesktopComposition( + sharedPrefs.getBoolean("bookmark.perf_desktop_composition", false)); + performanceFlags.setFullWindowDrag( + sharedPrefs.getBoolean("bookmark.perf_window_dragging", false)); + performanceFlags.setMenuAnimations( + sharedPrefs.getBoolean("bookmark.perf_menu_animation", false)); + performanceFlags.setTheming(sharedPrefs.getBoolean("bookmark.perf_themes", false)); + + advancedSettings.setEnable3GSettings( + sharedPrefs.getBoolean("bookmark.enable_3g_settings", false)); + + advancedSettings.getScreen3G().setColors(sharedPrefs.getInt("bookmark.colors_3g", 16)); + advancedSettings.getScreen3G().setResolution( + sharedPrefs.getString("bookmark.resolution_3g", "automatic"), + sharedPrefs.getInt("bookmark.width_3g", 800), + sharedPrefs.getInt("bookmark.height_3g", 600)); + + advancedSettings.getPerformance3G().setRemoteFX( + sharedPrefs.getBoolean("bookmark.perf_remotefx_3g", false)); + advancedSettings.getPerformance3G().setGfx( + sharedPrefs.getBoolean("bookmark.perf_gfx_3g", false)); + advancedSettings.getPerformance3G().setH264( + sharedPrefs.getBoolean("bookmark.perf_gfx_h264_3g", false)); + advancedSettings.getPerformance3G().setWallpaper( + sharedPrefs.getBoolean("bookmark.perf_wallpaper_3g", false)); + advancedSettings.getPerformance3G().setFontSmoothing( + sharedPrefs.getBoolean("bookmark.perf_font_smoothing_3g", false)); + advancedSettings.getPerformance3G().setDesktopComposition( + sharedPrefs.getBoolean("bookmark.perf_desktop_composition_3g", false)); + advancedSettings.getPerformance3G().setFullWindowDrag( + sharedPrefs.getBoolean("bookmark.perf_window_dragging_3g", false)); + advancedSettings.getPerformance3G().setMenuAnimations( + sharedPrefs.getBoolean("bookmark.perf_menu_animation_3g", false)); + advancedSettings.getPerformance3G().setTheming( + sharedPrefs.getBoolean("bookmark.perf_themes_3g", false)); + + advancedSettings.setRedirectSDCard( + sharedPrefs.getBoolean("bookmark.redirect_sdcard", false)); + advancedSettings.setRedirectSound(sharedPrefs.getInt("bookmark.redirect_sound", 0)); + advancedSettings.setRedirectMicrophone( + sharedPrefs.getBoolean("bookmark.redirect_microphone", false)); + advancedSettings.setSecurity(sharedPrefs.getInt("bookmark.security", 0)); + advancedSettings.setRemoteProgram(sharedPrefs.getString("bookmark.remote_program", "")); + advancedSettings.setWorkDir(sharedPrefs.getString("bookmark.work_dir", "")); + advancedSettings.setConsoleMode(sharedPrefs.getBoolean("bookmark.console_mode", false)); + + debugSettings.setAsyncChannel(sharedPrefs.getBoolean("bookmark.async_channel", true)); + debugSettings.setAsyncInput(sharedPrefs.getBoolean("bookmark.async_input", true)); + debugSettings.setAsyncUpdate(sharedPrefs.getBoolean("bookmark.async_update", true)); + debugSettings.setDebugLevel(sharedPrefs.getString("bookmark.debug_level", "INFO")); + } + + // Cloneable + public Object clone() + { + try + { + return super.clone(); + } + catch (CloneNotSupportedException e) + { + return null; + } + } + + // performance flags + public static class PerformanceFlags implements Parcelable + { + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + public PerformanceFlags createFromParcel(Parcel in) + { + return new PerformanceFlags(in); + } + + @Override public PerformanceFlags[] newArray(int size) + { + return new PerformanceFlags[size]; + } + }; + private boolean remotefx; + private boolean gfx; + private boolean h264; + private boolean wallpaper; + private boolean theming; + private boolean fullWindowDrag; + private boolean menuAnimations; + private boolean fontSmoothing; + private boolean desktopComposition; + + public PerformanceFlags() + { + remotefx = false; + gfx = false; + h264 = false; + wallpaper = false; + theming = false; + fullWindowDrag = false; + menuAnimations = false; + fontSmoothing = false; + desktopComposition = false; + } + + public PerformanceFlags(Parcel parcel) + { + remotefx = parcel.readInt() == 1; + gfx = parcel.readInt() == 1; + h264 = parcel.readInt() == 1; + wallpaper = parcel.readInt() == 1; + theming = parcel.readInt() == 1; + fullWindowDrag = (parcel.readInt() == 1); + menuAnimations = parcel.readInt() == 1; + fontSmoothing = parcel.readInt() == 1; + desktopComposition = parcel.readInt() == 1; + } + + public boolean getRemoteFX() + { + return remotefx; + } + + public void setRemoteFX(boolean remotefx) + { + this.remotefx = remotefx; + } + + public boolean getGfx() + { + return gfx; + } + + public void setGfx(boolean gfx) + { + this.gfx = gfx; + } + + public boolean getH264() + { + return h264; + } + + public void setH264(boolean h264) + { + this.h264 = h264; + } + + public boolean getWallpaper() + { + return wallpaper; + } + + public void setWallpaper(boolean wallpaper) + { + this.wallpaper = wallpaper; + } + + public boolean getTheming() + { + return theming; + } + + public void setTheming(boolean theming) + { + this.theming = theming; + } + + public boolean getFullWindowDrag() + { + return fullWindowDrag; + } + + public void setFullWindowDrag(boolean fullWindowDrag) + { + this.fullWindowDrag = fullWindowDrag; + } + + public boolean getMenuAnimations() + { + return menuAnimations; + } + + public void setMenuAnimations(boolean menuAnimations) + { + this.menuAnimations = menuAnimations; + } + + public boolean getFontSmoothing() + { + return fontSmoothing; + } + + public void setFontSmoothing(boolean fontSmoothing) + { + this.fontSmoothing = fontSmoothing; + } + + public boolean getDesktopComposition() + { + return desktopComposition; + } + + public void setDesktopComposition(boolean desktopComposition) + { + this.desktopComposition = desktopComposition; + } + + @Override public int describeContents() + { + return 0; + } + + @Override public void writeToParcel(Parcel out, int flags) + { + out.writeInt(remotefx ? 1 : 0); + out.writeInt(gfx ? 1 : 0); + out.writeInt(h264 ? 1 : 0); + out.writeInt(wallpaper ? 1 : 0); + out.writeInt(theming ? 1 : 0); + out.writeInt(fullWindowDrag ? 1 : 0); + out.writeInt(menuAnimations ? 1 : 0); + out.writeInt(fontSmoothing ? 1 : 0); + out.writeInt(desktopComposition ? 1 : 0); + } + } + + // Screen Settings class + public static class ScreenSettings implements Parcelable + { + public static final int FITSCREEN = -2; + public static final int AUTOMATIC = -1; + public static final int CUSTOM = 0; + public static final int PREDEFINED = 1; + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + public ScreenSettings createFromParcel(Parcel in) + { + return new ScreenSettings(in); + } + + @Override public ScreenSettings[] newArray(int size) + { + return new ScreenSettings[size]; + } + }; + private int resolution; + private int colors; + private int width; + private int height; + + public ScreenSettings() + { + init(); + } + + public ScreenSettings(Parcel parcel) + { + resolution = parcel.readInt(); + colors = parcel.readInt(); + width = parcel.readInt(); + height = parcel.readInt(); + } + + private void validate() + { + switch (colors) + { + case 32: + case 24: + case 16: + case 15: + case 8: + break; + default: + colors = 32; + break; + } + + if ((width <= 0) || (width > 65536)) + { + width = 1024; + } + + if ((height <= 0) || (height > 65536)) + { + height = 768; + } + + switch (resolution) + { + case FITSCREEN: + case AUTOMATIC: + case CUSTOM: + case PREDEFINED: + break; + default: + resolution = AUTOMATIC; + break; + } + } + + private void init() + { + resolution = AUTOMATIC; + colors = 16; + width = 0; + height = 0; + } + + public void setResolution(String resolution, int width, int height) + { + if (resolution.contains("x")) + { + String[] dimensions = resolution.split("x"); + this.width = Integer.valueOf(dimensions[0]); + this.height = Integer.valueOf(dimensions[1]); + this.resolution = PREDEFINED; + } + else if (resolution.equalsIgnoreCase("custom")) + { + this.width = width; + this.height = height; + this.resolution = CUSTOM; + } + else if (resolution.equalsIgnoreCase("fitscreen")) + { + this.width = this.height = 0; + this.resolution = FITSCREEN; + } + else + { + this.width = this.height = 0; + this.resolution = AUTOMATIC; + } + } + + public int getResolution() + { + return resolution; + } + + public void setResolution(int resolution) + { + this.resolution = resolution; + + if (resolution == AUTOMATIC || resolution == FITSCREEN) + { + width = 0; + height = 0; + } + } + + public String getResolutionString() + { + if (isPredefined()) + return (width + "x" + height); + + return (isFitScreen() ? "fitscreen" : isAutomatic() ? "automatic" : "custom"); + } + + public boolean isPredefined() + { + validate(); + return (resolution == PREDEFINED); + } + + public boolean isAutomatic() + { + validate(); + return (resolution == AUTOMATIC); + } + + public boolean isFitScreen() + { + validate(); + return (resolution == FITSCREEN); + } + + public boolean isCustom() + { + validate(); + return (resolution == CUSTOM); + } + + public int getWidth() + { + validate(); + return width; + } + + public void setWidth(int width) + { + this.width = width; + } + + public int getHeight() + { + validate(); + return height; + } + + public void setHeight(int height) + { + this.height = height; + } + + public int getColors() + { + validate(); + return colors; + } + + public void setColors(int colors) + { + this.colors = colors; + } + + @Override public int describeContents() + { + return 0; + } + + @Override public void writeToParcel(Parcel out, int flags) + { + out.writeInt(resolution); + out.writeInt(colors); + out.writeInt(width); + out.writeInt(height); + } + } + + public static class DebugSettings implements Parcelable + { + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + public DebugSettings createFromParcel(Parcel in) + { + return new DebugSettings(in); + } + + @Override public DebugSettings[] newArray(int size) + { + return new DebugSettings[size]; + } + }; + private String debug; + private boolean asyncChannel; + private boolean asyncTransport; + private boolean asyncInput; + private boolean asyncUpdate; + + public DebugSettings() + { + init(); + } + + // Session Settings + public DebugSettings(Parcel parcel) + { + asyncChannel = parcel.readInt() == 1; + asyncTransport = parcel.readInt() == 1; + asyncInput = parcel.readInt() == 1; + asyncUpdate = parcel.readInt() == 1; + debug = parcel.readString(); + } + + private void init() + { + debug = "INFO"; + asyncChannel = true; + asyncTransport = false; + asyncInput = true; + asyncUpdate = true; + } + + private void validate() + { + final String[] levels = { "OFF", "FATAL", "ERROR", "WARN", "INFO", "DEBUG", "TRACE" }; + + for (String level : levels) + { + if (level.equalsIgnoreCase(this.debug)) + { + return; + } + } + + this.debug = "INFO"; + } + + public String getDebugLevel() + { + validate(); + return debug; + } + + public void setDebugLevel(String debug) + { + this.debug = debug; + } + + public boolean getAsyncUpdate() + { + return asyncUpdate; + } + + public void setAsyncUpdate(boolean enabled) + { + asyncUpdate = enabled; + } + + public boolean getAsyncInput() + { + return asyncInput; + } + + public void setAsyncInput(boolean enabled) + { + asyncInput = enabled; + } + + public boolean getAsyncChannel() + { + return asyncChannel; + } + + public void setAsyncChannel(boolean enabled) + { + asyncChannel = enabled; + } + + @Override public int describeContents() + { + return 0; + } + + @Override public void writeToParcel(Parcel out, int flags) + { + out.writeInt(asyncChannel ? 1 : 0); + out.writeInt(asyncTransport ? 1 : 0); + out.writeInt(asyncInput ? 1 : 0); + out.writeInt(asyncUpdate ? 1 : 0); + out.writeString(debug); + } + } + + // Session Settings + public static class AdvancedSettings implements Parcelable + { + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + public AdvancedSettings createFromParcel(Parcel in) + { + return new AdvancedSettings(in); + } + + @Override public AdvancedSettings[] newArray(int size) + { + return new AdvancedSettings[size]; + } + }; + private boolean enable3GSettings; + private ScreenSettings screen3G; + private PerformanceFlags performance3G; + private boolean redirectSDCard; + private int redirectSound; + private boolean redirectMicrophone; + private int security; + private boolean consoleMode; + private String remoteProgram; + private String workDir; + + public AdvancedSettings() + { + init(); + } + + public AdvancedSettings(Parcel parcel) + { + enable3GSettings = parcel.readInt() == 1; + screen3G = parcel.readParcelable(ScreenSettings.class.getClassLoader()); + performance3G = parcel.readParcelable(PerformanceFlags.class.getClassLoader()); + redirectSDCard = parcel.readInt() == 1; + redirectSound = parcel.readInt(); + redirectMicrophone = parcel.readInt() == 1; + security = parcel.readInt(); + consoleMode = parcel.readInt() == 1; + remoteProgram = parcel.readString(); + workDir = parcel.readString(); + } + + private void init() + { + enable3GSettings = false; + screen3G = new ScreenSettings(); + performance3G = new PerformanceFlags(); + redirectSDCard = false; + redirectSound = 0; + redirectMicrophone = false; + security = 0; + consoleMode = false; + remoteProgram = ""; + workDir = ""; + } + + private void validate() + { + switch (redirectSound) + { + case 0: + case 1: + case 2: + break; + default: + redirectSound = 0; + break; + } + + switch (security) + { + case 0: + case 1: + case 2: + case 3: + break; + default: + security = 0; + break; + } + } + + public boolean getEnable3GSettings() + { + return enable3GSettings; + } + + public void setEnable3GSettings(boolean enable3GSettings) + { + this.enable3GSettings = enable3GSettings; + } + + public ScreenSettings getScreen3G() + { + return screen3G; + } + + public void setScreen3G(ScreenSettings screen3G) + { + this.screen3G = screen3G; + } + + public PerformanceFlags getPerformance3G() + { + return performance3G; + } + + public void setPerformance3G(PerformanceFlags performance3G) + { + this.performance3G = performance3G; + } + + public boolean getRedirectSDCard() + { + return redirectSDCard; + } + + public void setRedirectSDCard(boolean redirectSDCard) + { + this.redirectSDCard = redirectSDCard; + } + + public int getRedirectSound() + { + validate(); + return redirectSound; + } + + public void setRedirectSound(int redirect) + { + this.redirectSound = redirect; + } + + public boolean getRedirectMicrophone() + { + return redirectMicrophone; + } + + public void setRedirectMicrophone(boolean redirect) + { + this.redirectMicrophone = redirect; + } + + public int getSecurity() + { + validate(); + return security; + } + + public void setSecurity(int security) + { + this.security = security; + } + + public boolean getConsoleMode() + { + return consoleMode; + } + + public void setConsoleMode(boolean consoleMode) + { + this.consoleMode = consoleMode; + } + + public String getRemoteProgram() + { + return remoteProgram; + } + + public void setRemoteProgram(String remoteProgram) + { + this.remoteProgram = remoteProgram; + } + + public String getWorkDir() + { + return workDir; + } + + public void setWorkDir(String workDir) + { + this.workDir = workDir; + } + + @Override public int describeContents() + { + return 0; + } + + @Override public void writeToParcel(Parcel out, int flags) + { + out.writeInt(enable3GSettings ? 1 : 0); + out.writeParcelable(screen3G, flags); + out.writeParcelable(performance3G, flags); + out.writeInt(redirectSDCard ? 1 : 0); + out.writeInt(redirectSound); + out.writeInt(redirectMicrophone ? 1 : 0); + out.writeInt(security); + out.writeInt(consoleMode ? 1 : 0); + out.writeString(remoteProgram); + out.writeString(workDir); + } + } +} diff --git a/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/domain/ConnectionReference.java b/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/domain/ConnectionReference.java new file mode 100644 index 0000000..3e68776 --- /dev/null +++ b/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/domain/ConnectionReference.java @@ -0,0 +1,85 @@ +/* + A RDP connection reference. References can use bookmark ids or hostnames to connect to a RDP + server. + + Copyright 2013 Thincast Technologies GmbH, Author: Martin Fleisz + + This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. + If a copy of the MPL was not distributed with this file, You can obtain one at + http://mozilla.org/MPL/2.0/. +*/ + +package com.freerdp.freerdpcore.domain; + +public class ConnectionReference +{ + public static final String PATH_MANUAL_BOOKMARK_ID = "MBMID/"; + public static final String PATH_HOSTNAME = "HOST/"; + public static final String PATH_PLACEHOLDER = "PLCHLD/"; + public static final String PATH_FILE = "FILE/"; + + public static String getManualBookmarkReference(long bookmarkId) + { + return (PATH_MANUAL_BOOKMARK_ID + bookmarkId); + } + + public static String getHostnameReference(String hostname) + { + return (PATH_HOSTNAME + hostname); + } + + public static String getPlaceholderReference(String name) + { + return (PATH_PLACEHOLDER + name); + } + + public static String getFileReference(String uri) + { + return (PATH_FILE + uri); + } + + public static boolean isBookmarkReference(String refStr) + { + return refStr.startsWith(PATH_MANUAL_BOOKMARK_ID); + } + + public static boolean isManualBookmarkReference(String refStr) + { + return refStr.startsWith(PATH_MANUAL_BOOKMARK_ID); + } + + public static boolean isHostnameReference(String refStr) + { + return refStr.startsWith(PATH_HOSTNAME); + } + + public static boolean isPlaceholderReference(String refStr) + { + return refStr.startsWith(PATH_PLACEHOLDER); + } + + public static boolean isFileReference(String refStr) + { + return refStr.startsWith(PATH_FILE); + } + + public static long getManualBookmarkId(String refStr) + { + return Integer.parseInt(refStr.substring(PATH_MANUAL_BOOKMARK_ID.length())); + } + + public static String getHostname(String refStr) + { + return refStr.substring(PATH_HOSTNAME.length()); + } + + public static String getPlaceholder(String refStr) + { + return refStr.substring(PATH_PLACEHOLDER.length()); + } + + public static String getFile(String refStr) + { + return refStr.substring(PATH_FILE.length()); + } +} diff --git a/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/domain/ManualBookmark.java b/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/domain/ManualBookmark.java new file mode 100644 index 0000000..874d4e9 --- /dev/null +++ b/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/domain/ManualBookmark.java @@ -0,0 +1,255 @@ +/* + Manual Bookmark implementation + + Copyright 2013 Thincast Technologies GmbH, Author: Martin Fleisz + + This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. + If a copy of the MPL was not distributed with this file, You can obtain one at + http://mozilla.org/MPL/2.0/. +*/ + +package com.freerdp.freerdpcore.domain; + +import android.content.SharedPreferences; +import android.os.Parcel; +import android.os.Parcelable; + +public class ManualBookmark extends BookmarkBase +{ + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + public ManualBookmark createFromParcel(Parcel in) + { + return new ManualBookmark(in); + } + + @Override public ManualBookmark[] newArray(int size) + { + return new ManualBookmark[size]; + } + }; + private String hostname; + private int port; + private boolean enableGatewaySettings; + private GatewaySettings gatewaySettings; + + public ManualBookmark(Parcel parcel) + { + super(parcel); + type = TYPE_MANUAL; + hostname = parcel.readString(); + port = parcel.readInt(); + + enableGatewaySettings = (parcel.readInt() == 1 ? true : false); + gatewaySettings = parcel.readParcelable(GatewaySettings.class.getClassLoader()); + } + + public ManualBookmark() + { + super(); + init(); + } + + private void init() + { + type = TYPE_MANUAL; + hostname = ""; + port = 3389; + enableGatewaySettings = false; + gatewaySettings = new GatewaySettings(); + } + + public String getHostname() + { + return hostname; + } + + public void setHostname(String hostname) + { + this.hostname = hostname; + } + + public int getPort() + { + return port; + } + + public void setPort(int port) + { + this.port = port; + } + + public boolean getEnableGatewaySettings() + { + return enableGatewaySettings; + } + + public void setEnableGatewaySettings(boolean enableGatewaySettings) + { + this.enableGatewaySettings = enableGatewaySettings; + } + + public GatewaySettings getGatewaySettings() + { + return gatewaySettings; + } + + public void setGatewaySettings(GatewaySettings gatewaySettings) + { + this.gatewaySettings = gatewaySettings; + } + + @Override public int describeContents() + { + return 0; + } + + @Override public void writeToParcel(Parcel out, int flags) + { + super.writeToParcel(out, flags); + out.writeString(hostname); + out.writeInt(port); + out.writeInt(enableGatewaySettings ? 1 : 0); + out.writeParcelable(gatewaySettings, flags); + } + + @Override public void writeToSharedPreferences(SharedPreferences sharedPrefs) + { + super.writeToSharedPreferences(sharedPrefs); + + SharedPreferences.Editor editor = sharedPrefs.edit(); + editor.putString("bookmark.hostname", hostname); + editor.putInt("bookmark.port", port); + editor.putBoolean("bookmark.enable_gateway_settings", enableGatewaySettings); + editor.putString("bookmark.gateway_hostname", gatewaySettings.getHostname()); + editor.putInt("bookmark.gateway_port", gatewaySettings.getPort()); + editor.putString("bookmark.gateway_username", gatewaySettings.getUsername()); + editor.putString("bookmark.gateway_password", gatewaySettings.getPassword()); + editor.putString("bookmark.gateway_domain", gatewaySettings.getDomain()); + editor.commit(); + } + + @Override public void readFromSharedPreferences(SharedPreferences sharedPrefs) + { + super.readFromSharedPreferences(sharedPrefs); + + hostname = sharedPrefs.getString("bookmark.hostname", ""); + port = sharedPrefs.getInt("bookmark.port", 3389); + enableGatewaySettings = sharedPrefs.getBoolean("bookmark.enable_gateway_settings", false); + gatewaySettings.setHostname(sharedPrefs.getString("bookmark.gateway_hostname", "")); + gatewaySettings.setPort(sharedPrefs.getInt("bookmark.gateway_port", 443)); + gatewaySettings.setUsername(sharedPrefs.getString("bookmark.gateway_username", "")); + gatewaySettings.setPassword(sharedPrefs.getString("bookmark.gateway_password", "")); + gatewaySettings.setDomain(sharedPrefs.getString("bookmark.gateway_domain", "")); + } + + // Cloneable + public Object clone() + { + return super.clone(); + } + + // Gateway Settings class + public static class GatewaySettings implements Parcelable + { + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + public GatewaySettings createFromParcel(Parcel in) + { + return new GatewaySettings(in); + } + + @Override public GatewaySettings[] newArray(int size) + { + return new GatewaySettings[size]; + } + }; + private String hostname; + private int port; + private String username; + private String password; + private String domain; + + public GatewaySettings() + { + hostname = ""; + port = 443; + username = ""; + password = ""; + domain = ""; + } + + public GatewaySettings(Parcel parcel) + { + hostname = parcel.readString(); + port = parcel.readInt(); + username = parcel.readString(); + password = parcel.readString(); + domain = parcel.readString(); + } + + public String getHostname() + { + return hostname; + } + + public void setHostname(String hostname) + { + this.hostname = hostname; + } + + public int getPort() + { + return port; + } + + public void setPort(int port) + { + this.port = port; + } + + public String getUsername() + { + return username; + } + + public void setUsername(String username) + { + this.username = username; + } + + public String getPassword() + { + return password; + } + + public void setPassword(String password) + { + this.password = password; + } + + public String getDomain() + { + return domain; + } + + public void setDomain(String domain) + { + this.domain = domain; + } + + @Override public int describeContents() + { + return 0; + } + + @Override public void writeToParcel(Parcel out, int flags) + { + out.writeString(hostname); + out.writeInt(port); + out.writeString(username); + out.writeString(password); + out.writeString(domain); + } + } +} diff --git a/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/domain/PlaceholderBookmark.java b/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/domain/PlaceholderBookmark.java new file mode 100644 index 0000000..d15aaf7 --- /dev/null +++ b/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/domain/PlaceholderBookmark.java @@ -0,0 +1,84 @@ +/* + Placeholder for bookmark items with a special purpose (i.e. just displaying some text) + + Copyright 2013 Thincast Technologies GmbH, Author: Martin Fleisz + + This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. + If a copy of the MPL was not distributed with this file, You can obtain one at + http://mozilla.org/MPL/2.0/. +*/ + +package com.freerdp.freerdpcore.domain; + +import android.content.SharedPreferences; +import android.os.Parcel; +import android.os.Parcelable; + +public class PlaceholderBookmark extends BookmarkBase +{ + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + public PlaceholderBookmark createFromParcel(Parcel in) + { + return new PlaceholderBookmark(in); + } + + @Override public PlaceholderBookmark[] newArray(int size) + { + return new PlaceholderBookmark[size]; + } + }; + private String name; + + public PlaceholderBookmark(Parcel parcel) + { + super(parcel); + type = TYPE_PLACEHOLDER; + name = parcel.readString(); + } + + public PlaceholderBookmark() + { + super(); + type = TYPE_PLACEHOLDER; + name = ""; + } + + public String getName() + { + return name; + } + + public void setName(String name) + { + this.name = name; + } + + @Override public int describeContents() + { + return 0; + } + + @Override public void writeToParcel(Parcel out, int flags) + { + super.writeToParcel(out, flags); + out.writeString(name); + } + + @Override public void writeToSharedPreferences(SharedPreferences sharedPrefs) + { + super.writeToSharedPreferences(sharedPrefs); + } + + @Override public void readFromSharedPreferences(SharedPreferences sharedPrefs) + { + super.readFromSharedPreferences(sharedPrefs); + } + + // Cloneable + public Object clone() + { + return super.clone(); + } +} diff --git a/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/domain/QuickConnectBookmark.java b/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/domain/QuickConnectBookmark.java new file mode 100644 index 0000000..3367b54 --- /dev/null +++ b/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/domain/QuickConnectBookmark.java @@ -0,0 +1,70 @@ +/* + Quick Connect bookmark (used for quick connects using just a hostname) + + Copyright 2013 Thincast Technologies GmbH, Author: Martin Fleisz + + This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. + If a copy of the MPL was not distributed with this file, You can obtain one at + http://mozilla.org/MPL/2.0/. +*/ + +package com.freerdp.freerdpcore.domain; + +import android.content.SharedPreferences; +import android.os.Parcel; +import android.os.Parcelable; + +public class QuickConnectBookmark extends ManualBookmark +{ + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + public QuickConnectBookmark createFromParcel(Parcel in) + { + return new QuickConnectBookmark(in); + } + + @Override public QuickConnectBookmark[] newArray(int size) + { + return new QuickConnectBookmark[size]; + } + }; + + public QuickConnectBookmark(Parcel parcel) + { + super(parcel); + type = TYPE_QUICKCONNECT; + } + + public QuickConnectBookmark() + { + super(); + type = TYPE_QUICKCONNECT; + } + + @Override public int describeContents() + { + return 0; + } + + @Override public void writeToParcel(Parcel out, int flags) + { + super.writeToParcel(out, flags); + } + + @Override public void writeToSharedPreferences(SharedPreferences sharedPrefs) + { + super.writeToSharedPreferences(sharedPrefs); + } + + @Override public void readFromSharedPreferences(SharedPreferences sharedPrefs) + { + super.readFromSharedPreferences(sharedPrefs); + } + + // Cloneable + public Object clone() + { + return super.clone(); + } +} diff --git a/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/presentation/AboutActivity.java b/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/presentation/AboutActivity.java new file mode 100644 index 0000000..8fdfa7a --- /dev/null +++ b/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/presentation/AboutActivity.java @@ -0,0 +1,121 @@ +package com.freerdp.freerdpcore.presentation; + +import android.content.pm.PackageManager; +import android.content.res.Configuration; +import android.nfc.FormatException; +import android.os.Build; +import android.os.Bundle; +import androidx.core.text.TextUtilsCompat; +import androidx.appcompat.app.AppCompatActivity; +import android.util.Log; +import android.webkit.WebSettings; +import android.webkit.WebView; + +import com.freerdp.freerdpcore.R; +import com.freerdp.freerdpcore.services.LibFreeRDP; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.Formatter; +import java.util.IllegalFormatException; +import java.util.Locale; + +public class AboutActivity extends AppCompatActivity +{ + private static final String TAG = AboutActivity.class.toString(); + private WebView mWebView; + + @Override protected void onCreate(Bundle savedInstanceState) + { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_about); + mWebView = (WebView)findViewById(R.id.activity_about_webview); + } + + @Override protected void onResume() + { + populate(); + super.onResume(); + } + + private void populate() + { + StringBuilder total = new StringBuilder(); + + String filename = "about_phone.html"; + if ((getResources().getConfiguration().screenLayout & + Configuration.SCREENLAYOUT_SIZE_MASK) >= Configuration.SCREENLAYOUT_SIZE_LARGE) + { + filename = "about.html"; + } + Locale def = Locale.getDefault(); + String prefix = def.getLanguage().toLowerCase(def); + + String dir = prefix + "_about_page/"; + String file = dir + filename; + InputStream is; + try + { + is = getAssets().open(file); + is.close(); + } + catch (IOException e) + { + Log.e(TAG, "Missing localized asset " + file, e); + dir = "about_page/"; + file = dir + filename; + } + + try + { + BufferedReader r = new BufferedReader(new InputStreamReader(getAssets().open(file))); + try + { + String line; + while ((line = r.readLine()) != null) + { + total.append(line); + total.append("\n"); + } + } + finally + { + r.close(); + } + } + catch (IOException e) + { + Log.e(TAG, "Could not read about page " + file, e); + } + + // append FreeRDP core version to app version + // get app version + String version; + try + { + version = getPackageManager().getPackageInfo(getPackageName(), 0).versionName; + } + catch (PackageManager.NameNotFoundException e) + { + version = "unknown"; + } + version = version + " (" + LibFreeRDP.getVersion() + ")"; + + WebSettings settings = mWebView.getSettings(); + settings.setDomStorageEnabled(true); + settings.setUseWideViewPort(true); + settings.setLoadWithOverviewMode(true); + settings.setSupportZoom(true); + + final String base = "file:///android_asset/" + dir; + + final String rawHtml = total.toString(); + final String html = rawHtml.replaceAll("%AFREERDP_VERSION%", version) + .replaceAll("%SYSTEM_VERSION%", Build.VERSION.RELEASE) + .replaceAll("%DEVICE_MODEL%", Build.MODEL); + + mWebView.loadDataWithBaseURL(base, html, "text/html", null, "about:blank"); + } +} diff --git a/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/presentation/ApplicationSettingsActivity.java b/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/presentation/ApplicationSettingsActivity.java new file mode 100644 index 0000000..2506e71 --- /dev/null +++ b/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/presentation/ApplicationSettingsActivity.java @@ -0,0 +1,300 @@ +/* + Application Settings Activity + + Copyright 2013 Thincast Technologies GmbH, Author: Martin Fleisz + + This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. + If a copy of the MPL was not distributed with this file, You can obtain one at + http://mozilla.org/MPL/2.0/. +*/ + +package com.freerdp.freerdpcore.presentation; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.DialogInterface; +import android.content.SharedPreferences; +import android.content.res.Configuration; +import android.os.Build; +import android.os.Bundle; +import android.preference.EditTextPreference; +import android.preference.Preference; +import android.preference.PreferenceFragment; +import android.preference.PreferenceManager; +import android.preference.PreferenceScreen; +import androidx.appcompat.app.AlertDialog; +import android.widget.Toast; + +import com.freerdp.freerdpcore.R; +import com.freerdp.freerdpcore.utils.AppCompatPreferenceActivity; + +import java.io.File; +import java.util.List; +import java.util.UUID; + +public class ApplicationSettingsActivity extends AppCompatPreferenceActivity +{ + private static boolean isXLargeTablet(Context context) + { + return (context.getResources().getConfiguration().screenLayout & + Configuration.SCREENLAYOUT_SIZE_MASK) >= Configuration.SCREENLAYOUT_SIZE_XLARGE; + } + + @Override protected void onCreate(Bundle savedInstanceState) + { + super.onCreate(savedInstanceState); + setupActionBar(); + } + + private void setupActionBar() + { + android.app.ActionBar actionBar = getActionBar(); + if (actionBar != null) + { + actionBar.setDisplayHomeAsUpEnabled(true); + } + } + + @Override public boolean onIsMultiPane() + { + return isXLargeTablet(this); + } + + @Override + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + public void onBuildHeaders(List

target) + { + loadHeadersFromResource(R.xml.settings_app_headers, target); + } + + protected boolean isValidFragment(String fragmentName) + { + return PreferenceFragment.class.getName().equals(fragmentName) || + ClientPreferenceFragment.class.getName().equals(fragmentName) || + UiPreferenceFragment.class.getName().equals(fragmentName) || + PowerPreferenceFragment.class.getName().equals(fragmentName) || + SecurityPreferenceFragment.class.getName().equals(fragmentName); + } + + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + public static class ClientPreferenceFragment + extends PreferenceFragment implements SharedPreferences.OnSharedPreferenceChangeListener + { + @Override public void onCreate(Bundle savedInstanceState) + { + super.onCreate(savedInstanceState); + addPreferencesFromResource(R.xml.settings_app_client); + SharedPreferences preferences = get(getActivity()); + preferences.registerOnSharedPreferenceChangeListener(this); + } + + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) + { + if (isAdded()) + { + final String clientNameKey = getString(R.string.preference_key_client_name); + + get(getActivity()); + if (key.equals(clientNameKey)) + { + final String clientNameValue = sharedPreferences.getString(clientNameKey, ""); + EditTextPreference pref = (EditTextPreference)findPreference(clientNameKey); + pref.setText(clientNameValue); + } + } + } + } + + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + public static class UiPreferenceFragment extends PreferenceFragment + { + @Override public void onCreate(Bundle savedInstanceState) + { + super.onCreate(savedInstanceState); + addPreferencesFromResource(R.xml.settings_app_ui); + } + } + + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + public static class PowerPreferenceFragment extends PreferenceFragment + { + @Override public void onCreate(Bundle savedInstanceState) + { + super.onCreate(savedInstanceState); + addPreferencesFromResource(R.xml.settings_app_power); + } + } + + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + public static class SecurityPreferenceFragment extends PreferenceFragment + { + @Override public void onCreate(Bundle savedInstanceState) + { + super.onCreate(savedInstanceState); + addPreferencesFromResource(R.xml.settings_app_security); + } + + @Override + public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, + Preference preference) + { + final String clear = + getString(R.string.preference_key_security_clear_certificate_cache); + if (preference.getKey().equals(clear)) + { + showDialog(); + return true; + } + else + { + return super.onPreferenceTreeClick(preferenceScreen, preference); + } + } + + private void showDialog() + { + new AlertDialog.Builder(getActivity()) + .setTitle(R.string.dlg_title_clear_cert_cache) + .setMessage(R.string.dlg_msg_clear_cert_cache) + .setPositiveButton(android.R.string.ok, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) + { + clearCertificateCache(); + dialog.dismiss(); + } + }) + .setNegativeButton(android.R.string.cancel, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) + { + dialog.dismiss(); + } + }) + .setIcon(android.R.drawable.ic_delete) + .show(); + } + + private boolean deleteDirectory(File dir) + { + if (dir.isDirectory()) + { + String[] children = dir.list(); + for (String file : children) + { + if (!deleteDirectory(new File(dir, file))) + return false; + } + } + return dir.delete(); + } + + private void clearCertificateCache() + { + Context context = getActivity(); + if ((new File(context.getFilesDir() + "/.freerdp")).exists()) + { + if (deleteDirectory(new File(context.getFilesDir() + "/.freerdp"))) + Toast.makeText(context, R.string.info_reset_success, Toast.LENGTH_LONG).show(); + else + Toast.makeText(context, R.string.info_reset_failed, Toast.LENGTH_LONG).show(); + } + else + Toast.makeText(context, R.string.info_reset_success, Toast.LENGTH_LONG).show(); + } + } + + public static SharedPreferences get(Context context) + { + Context appContext = context.getApplicationContext(); + PreferenceManager.setDefaultValues(appContext, R.xml.settings_app_client, false); + PreferenceManager.setDefaultValues(appContext, R.xml.settings_app_power, false); + PreferenceManager.setDefaultValues(appContext, R.xml.settings_app_security, false); + PreferenceManager.setDefaultValues(appContext, R.xml.settings_app_ui, false); + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(appContext); + + final String key = context.getString(R.string.preference_key_client_name); + final String value = preferences.getString(key, ""); + if (value.isEmpty()) + { + final String android_id = UUID.randomUUID().toString(); + final String defaultValue = context.getString(R.string.preference_default_client_name); + final String name = defaultValue + "-" + android_id; + preferences.edit().putString(key, name.substring(0, 31)).apply(); + } + + return preferences; + } + + public static int getDisconnectTimeout(Context context) + { + SharedPreferences preferences = get(context); + return preferences.getInt( + context.getString(R.string.preference_key_power_disconnect_timeout), 0); + } + + public static boolean getHideStatusBar(Context context) + { + SharedPreferences preferences = get(context); + return preferences.getBoolean(context.getString(R.string.preference_key_ui_hide_status_bar), + false); + } + + public static boolean getHideActionBar(Context context) + { + SharedPreferences preferences = get(context); + return preferences.getBoolean(context.getString(R.string.preference_key_ui_hide_action_bar), + false); + } + + public static boolean getAcceptAllCertificates(Context context) + { + SharedPreferences preferences = get(context); + return preferences.getBoolean( + context.getString(R.string.preference_key_accept_certificates), false); + } + + public static boolean getHideZoomControls(Context context) + { + SharedPreferences preferences = get(context); + return preferences.getBoolean( + context.getString(R.string.preference_key_ui_hide_zoom_controls), false); + } + + public static boolean getSwapMouseButtons(Context context) + { + SharedPreferences preferences = get(context); + return preferences.getBoolean( + context.getString(R.string.preference_key_ui_swap_mouse_buttons), false); + } + + public static boolean getInvertScrolling(Context context) + { + SharedPreferences preferences = get(context); + return preferences.getBoolean( + context.getString(R.string.preference_key_ui_invert_scrolling), false); + } + + public static boolean getAskOnExit(Context context) + { + SharedPreferences preferences = get(context); + return preferences.getBoolean(context.getString(R.string.preference_key_ui_ask_on_exit), + false); + } + + public static boolean getAutoScrollTouchPointer(Context context) + { + SharedPreferences preferences = get(context); + return preferences.getBoolean( + context.getString(R.string.preference_key_ui_auto_scroll_touchpointer), false); + } + + public static String getClientName(Context context) + { + SharedPreferences preferences = get(context); + return preferences.getString(context.getString(R.string.preference_key_client_name), ""); + } +} diff --git a/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/presentation/BookmarkActivity.java b/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/presentation/BookmarkActivity.java new file mode 100644 index 0000000..89ac4d4 --- /dev/null +++ b/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/presentation/BookmarkActivity.java @@ -0,0 +1,743 @@ +/* + Bookmark editing activity + + Copyright 2013 Thincast Technologies GmbH, Author: Martin Fleisz + + This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. + If a copy of the MPL was not distributed with this file, You can obtain one at + http://mozilla.org/MPL/2.0/. + */ + +package com.freerdp.freerdpcore.presentation; + +import android.app.AlertDialog; +import android.content.ComponentName; +import android.content.DialogInterface; +import android.content.SharedPreferences; +import android.content.SharedPreferences.OnSharedPreferenceChangeListener; +import android.os.Bundle; +import android.preference.ListPreference; +import android.preference.Preference; +import android.preference.PreferenceActivity; +import android.preference.PreferenceManager; +import android.preference.PreferenceScreen; +import android.util.Log; +import android.view.View; + +import com.freerdp.freerdpcore.R; +import com.freerdp.freerdpcore.application.GlobalApp; +import com.freerdp.freerdpcore.domain.BookmarkBase; +import com.freerdp.freerdpcore.domain.ConnectionReference; +import com.freerdp.freerdpcore.domain.ManualBookmark; +import com.freerdp.freerdpcore.services.BookmarkBaseGateway; +import com.freerdp.freerdpcore.services.LibFreeRDP; +import com.freerdp.freerdpcore.utils.RDPFileParser; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; + +public class BookmarkActivity extends PreferenceActivity implements OnSharedPreferenceChangeListener +{ + public static final String PARAM_CONNECTION_REFERENCE = "conRef"; + + private static final String TAG = "BookmarkActivity"; + private static final int PREFERENCES_BOOKMARK = 1; + private static final int PREFERENCES_CREDENTIALS = 2; + private static final int PREFERENCES_SCREEN = 3; + private static final int PREFERENCES_PERFORMANCE = 4; + private static final int PREFERENCES_ADVANCED = 5; + private static final int PREFERENCES_SCREEN3G = 6; + private static final int PREFERENCES_PERFORMANCE3G = 7; + private static final int PREFERENCES_GATEWAY = 8; + private static final int PREFERENCES_DEBUG = 9; + // bookmark needs to be static because the activity is started for each + // subview + // (we have to do this because Android has a bug where the style for + // Preferences + // is only applied to the first PreferenceScreen but not to subsequent ones) + private static BookmarkBase bookmark = null; + private static boolean settings_changed = false; + private static boolean new_bookmark = false; + private int current_preferences; + + @Override public void onCreate(Bundle savedInstanceState) + { + super.onCreate(savedInstanceState); + + PreferenceManager mgr = getPreferenceManager(); + // init shared preferences for activity + mgr.setSharedPreferencesName("TEMP"); + mgr.setSharedPreferencesMode(MODE_PRIVATE); + + if (bookmark == null) + { + // if we have a bookmark id set in the extras we are in edit mode + Bundle bundle = getIntent().getExtras(); + if (bundle != null) + { + // See if we got a connection reference to a bookmark + if (bundle.containsKey(PARAM_CONNECTION_REFERENCE)) + { + String refStr = bundle.getString(PARAM_CONNECTION_REFERENCE); + if (ConnectionReference.isManualBookmarkReference(refStr)) + { + bookmark = GlobalApp.getManualBookmarkGateway().findById( + ConnectionReference.getManualBookmarkId(refStr)); + new_bookmark = false; + } + else if (ConnectionReference.isHostnameReference(refStr)) + { + bookmark = new ManualBookmark(); + bookmark.get().setLabel( + ConnectionReference.getHostname(refStr)); + bookmark.get().setHostname( + ConnectionReference.getHostname(refStr)); + new_bookmark = true; + } + else if (ConnectionReference.isFileReference(refStr)) + { + String file = ConnectionReference.getFile(refStr); + + bookmark = new ManualBookmark(); + bookmark.setLabel(file); + + try + { + RDPFileParser rdpFile = new RDPFileParser(file); + updateBookmarkFromFile((ManualBookmark)bookmark, rdpFile); + + bookmark.setLabel(new File(file).getName()); + new_bookmark = true; + } + catch (IOException e) + { + Log.e(TAG, "Failed reading RDP file", e); + } + } + } + } + + // last chance - ensure we really have a valid bookmark + if (bookmark == null) + bookmark = new ManualBookmark(); + + // hide gateway settings if we edit a non-manual bookmark + if (current_preferences == PREFERENCES_ADVANCED && + bookmark.getType() != ManualBookmark.TYPE_MANUAL) + { + PreferenceScreen screen = getPreferenceScreen(); + screen.removePreference(findPreference("bookmark.enable_gateway")); + screen.removePreference(findPreference("bookmark.gateway")); + } + + updateH264Preferences(); + + // update preferences from bookmark + bookmark.writeToSharedPreferences(mgr.getSharedPreferences()); + + // no settings changed yet + settings_changed = false; + } + + // load the requested settings resource + if (getIntent() == null || getIntent().getData() == null) + { + addPreferencesFromResource(R.xml.bookmark_settings); + current_preferences = PREFERENCES_BOOKMARK; + } + else if (getIntent().getData().toString().equals("preferences://screen_settings")) + { + addPreferencesFromResource(R.xml.screen_settings); + current_preferences = PREFERENCES_SCREEN; + } + else if (getIntent().getData().toString().equals("preferences://performance_flags")) + { + addPreferencesFromResource(R.xml.performance_flags); + current_preferences = PREFERENCES_PERFORMANCE; + } + else if (getIntent().getData().toString().equals("preferences://screen_settings_3g")) + { + addPreferencesFromResource(R.xml.screen_settings_3g); + current_preferences = PREFERENCES_SCREEN3G; + } + else if (getIntent().getData().toString().equals("preferences://performance_flags_3g")) + { + addPreferencesFromResource(R.xml.performance_flags_3g); + current_preferences = PREFERENCES_PERFORMANCE3G; + } + else if (getIntent().getData().toString().equals("preferences://advanced_settings")) + { + addPreferencesFromResource(R.xml.advanced_settings); + current_preferences = PREFERENCES_ADVANCED; + } + else if (getIntent().getData().toString().equals("preferences://credentials_settings")) + { + addPreferencesFromResource(R.xml.credentials_settings); + current_preferences = PREFERENCES_CREDENTIALS; + } + else if (getIntent().getData().toString().equals("preferences://gateway_settings")) + { + addPreferencesFromResource(R.xml.gateway_settings); + current_preferences = PREFERENCES_GATEWAY; + } + else if (getIntent().getData().toString().equals("preferences://debug_settings")) + { + addPreferencesFromResource(R.xml.debug_settings); + current_preferences = PREFERENCES_DEBUG; + } + else + { + addPreferencesFromResource(R.xml.bookmark_settings); + current_preferences = PREFERENCES_BOOKMARK; + } + + // update UI with bookmark data + SharedPreferences spref = mgr.getSharedPreferences(); + initSettings(spref); + + // register for preferences changed notification + mgr.getSharedPreferences().registerOnSharedPreferenceChangeListener(this); + + // set the correct component names in our preferencescreen settings + setIntentComponentNames(); + + updateH264Preferences(); + } + + private void updateH264Preferences() + { + if (!LibFreeRDP.hasH264Support()) + { + final int preferenceIdList[] = { R.string.preference_key_h264, + R.string.preference_key_h264_3g }; + + PreferenceManager mgr = getPreferenceManager(); + for (int id : preferenceIdList) + { + final String key = getString(id); + Preference preference = mgr.findPreference(key); + if (preference != null) + { + preference.setEnabled(false); + } + } + } + } + + private void updateBookmarkFromFile(ManualBookmark bookmark, RDPFileParser rdpFile) + { + String s; + Integer i; + + s = rdpFile.getString("full address"); + if (s != null) + { + // this gets complicated as it can include port + if (s.lastIndexOf(":") > s.lastIndexOf("]")) + { + try + { + String port = s.substring(s.lastIndexOf(":") + 1); + bookmark.setPort(Integer.parseInt(port)); + } + catch (NumberFormatException e) + { + Log.e(TAG, "Malformed address"); + } + + s = s.substring(0, s.lastIndexOf(":")); + } + + // or even be an ipv6 address + if (s.startsWith("[") && s.endsWith("]")) + s = s.substring(1, s.length() - 1); + + bookmark.setHostname(s); + } + + i = rdpFile.getInteger("server port"); + if (i != null) + bookmark.setPort(i); + + s = rdpFile.getString("username"); + if (s != null) + bookmark.setUsername(s); + + s = rdpFile.getString("domain"); + if (s != null) + bookmark.setDomain(s); + + i = rdpFile.getInteger("connect to console"); + if (i != null) + bookmark.getAdvancedSettings().setConsoleMode(i == 1); + } + + private void setIntentComponentNames() + { + // we set the component name for our sub-activity calls here because we + // don't know the package + // name of the main app in our library project. + ComponentName compName = + new ComponentName(getPackageName(), BookmarkActivity.class.getName()); + ArrayList prefKeys = new ArrayList(); + + prefKeys.add("bookmark.credentials"); + prefKeys.add("bookmark.screen"); + prefKeys.add("bookmark.performance"); + prefKeys.add("bookmark.advanced"); + prefKeys.add("bookmark.screen_3g"); + prefKeys.add("bookmark.performance_3g"); + prefKeys.add("bookmark.gateway_settings"); + prefKeys.add("bookmark.debug"); + + for (String p : prefKeys) + { + Preference pref = findPreference(p); + if (pref != null) + pref.getIntent().setComponent(compName); + } + } + + @Override public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) + { + settings_changed = true; + switch (current_preferences) + { + case PREFERENCES_DEBUG: + debugSettingsChanged(sharedPreferences, key); + break; + + case PREFERENCES_BOOKMARK: + bookmarkSettingsChanged(sharedPreferences, key); + break; + + case PREFERENCES_ADVANCED: + advancedSettingsChanged(sharedPreferences, key); + break; + + case PREFERENCES_CREDENTIALS: + credentialsSettingsChanged(sharedPreferences, key); + break; + + case PREFERENCES_SCREEN: + case PREFERENCES_SCREEN3G: + screenSettingsChanged(sharedPreferences, key); + break; + + case PREFERENCES_GATEWAY: + gatewaySettingsChanged(sharedPreferences, key); + break; + + default: + break; + } + } + + private void initSettings(SharedPreferences sharedPreferences) + { + switch (current_preferences) + { + case PREFERENCES_BOOKMARK: + initBookmarkSettings(sharedPreferences); + break; + + case PREFERENCES_ADVANCED: + initAdvancedSettings(sharedPreferences); + break; + + case PREFERENCES_CREDENTIALS: + initCredentialsSettings(sharedPreferences); + break; + + case PREFERENCES_SCREEN: + initScreenSettings(sharedPreferences); + break; + + case PREFERENCES_SCREEN3G: + initScreenSettings3G(sharedPreferences); + break; + + case PREFERENCES_GATEWAY: + initGatewaySettings(sharedPreferences); + break; + + case PREFERENCES_DEBUG: + initDebugSettings(sharedPreferences); + break; + + default: + break; + } + } + + private void initBookmarkSettings(SharedPreferences sharedPreferences) + { + bookmarkSettingsChanged(sharedPreferences, "bookmark.label"); + bookmarkSettingsChanged(sharedPreferences, "bookmark.hostname"); + bookmarkSettingsChanged(sharedPreferences, "bookmark.port"); + bookmarkSettingsChanged(sharedPreferences, "bookmark.username"); + bookmarkSettingsChanged(sharedPreferences, "bookmark.resolution"); + } + + private void bookmarkSettingsChanged(SharedPreferences sharedPreferences, String key) + { + if (key.equals("bookmark.label") && findPreference(key) != null) + findPreference(key).setSummary(sharedPreferences.getString(key, "")); + else if (key.equals("bookmark.hostname") && findPreference(key) != null) + findPreference(key).setSummary(sharedPreferences.getString(key, "")); + else if (key.equals("bookmark.port") && findPreference(key) != null) + findPreference(key).setSummary(String.valueOf(sharedPreferences.getInt(key, -1))); + else if (key.equals("bookmark.username")) + { + String username = sharedPreferences.getString(key, ""); + if (username.length() == 0) + username = ""; + findPreference("bookmark.credentials").setSummary(username); + } + else if (key.equals("bookmark.resolution") || key.equals("bookmark.colors") || + key.equals("bookmark.width") || key.equals("bookmark.height")) + { + String resolution = sharedPreferences.getString("bookmark.resolution", "800x600"); + // compare english string from resolutions_values_array array, + // decode to localized + // text for display + if (resolution.equals("automatic")) + { + resolution = getResources().getString(R.string.resolution_automatic); + } + if (resolution.equals("custom")) + { + resolution = getResources().getString(R.string.resolution_custom); + } + if (resolution.equals("fitscreen")) + { + resolution = getResources().getString(R.string.resolution_fit); + } + resolution += "@" + sharedPreferences.getInt("bookmark.colors", 16); + findPreference("bookmark.screen").setSummary(resolution); + } + } + + private void initAdvancedSettings(SharedPreferences sharedPreferences) + { + advancedSettingsChanged(sharedPreferences, "bookmark.enable_gateway_settings"); + advancedSettingsChanged(sharedPreferences, "bookmark.enable_3g_settings"); + advancedSettingsChanged(sharedPreferences, "bookmark.security"); + advancedSettingsChanged(sharedPreferences, "bookmark.resolution_3g"); + advancedSettingsChanged(sharedPreferences, "bookmark.remote_program"); + advancedSettingsChanged(sharedPreferences, "bookmark.work_dir"); + } + + private void advancedSettingsChanged(SharedPreferences sharedPreferences, String key) + { + if (key.equals("bookmark.enable_gateway_settings")) + { + boolean enabled = sharedPreferences.getBoolean(key, false); + findPreference("bookmark.gateway_settings").setEnabled(enabled); + } + else if (key.equals("bookmark.enable_3g_settings")) + { + boolean enabled = sharedPreferences.getBoolean(key, false); + findPreference("bookmark.screen_3g").setEnabled(enabled); + findPreference("bookmark.performance_3g").setEnabled(enabled); + } + else if (key.equals("bookmark.security")) + { + ListPreference listPreference = (ListPreference)findPreference(key); + CharSequence security = listPreference.getEntries()[sharedPreferences.getInt(key, 0)]; + listPreference.setSummary(security); + } + else if (key.equals("bookmark.resolution_3g") || key.equals("bookmark.colors_3g") || + key.equals("bookmark.width_3g") || key.equals("bookmark.height_3g")) + { + String resolution = sharedPreferences.getString("bookmark.resolution_3g", "800x600"); + if (resolution.equals("automatic")) + resolution = getResources().getString(R.string.resolution_automatic); + else if (resolution.equals("custom")) + resolution = getResources().getString(R.string.resolution_custom); + resolution += "@" + sharedPreferences.getInt("bookmark.colors_3g", 16); + findPreference("bookmark.screen_3g").setSummary(resolution); + } + else if (key.equals("bookmark.remote_program")) + findPreference(key).setSummary(sharedPreferences.getString(key, "")); + else if (key.equals("bookmark.work_dir")) + findPreference(key).setSummary(sharedPreferences.getString(key, "")); + } + + private void initCredentialsSettings(SharedPreferences sharedPreferences) + { + credentialsSettingsChanged(sharedPreferences, "bookmark.username"); + credentialsSettingsChanged(sharedPreferences, "bookmark.password"); + credentialsSettingsChanged(sharedPreferences, "bookmark.domain"); + } + + private void credentialsSettingsChanged(SharedPreferences sharedPreferences, String key) + { + if (key.equals("bookmark.username")) + findPreference(key).setSummary(sharedPreferences.getString(key, "")); + else if (key.equals("bookmark.password")) + { + if (sharedPreferences.getString(key, "").length() == 0) + findPreference(key).setSummary( + getResources().getString(R.string.settings_password_empty)); + else + findPreference(key).setSummary( + getResources().getString(R.string.settings_password_present)); + } + else if (key.equals("bookmark.domain")) + findPreference(key).setSummary(sharedPreferences.getString(key, "")); + } + + private void initScreenSettings(SharedPreferences sharedPreferences) + { + screenSettingsChanged(sharedPreferences, "bookmark.colors"); + screenSettingsChanged(sharedPreferences, "bookmark.resolution"); + screenSettingsChanged(sharedPreferences, "bookmark.width"); + screenSettingsChanged(sharedPreferences, "bookmark.height"); + } + + private void initScreenSettings3G(SharedPreferences sharedPreferences) + { + screenSettingsChanged(sharedPreferences, "bookmark.colors_3g"); + screenSettingsChanged(sharedPreferences, "bookmark.resolution_3g"); + screenSettingsChanged(sharedPreferences, "bookmark.width_3g"); + screenSettingsChanged(sharedPreferences, "bookmark.height_3g"); + } + + private void screenSettingsChanged(SharedPreferences sharedPreferences, String key) + { + // could happen during initialization because 3g and non-3g settings + // share this routine - just skip + if (findPreference(key) == null) + return; + + if (key.equals("bookmark.colors") || key.equals("bookmark.colors_3g")) + { + ListPreference listPreference = (ListPreference)findPreference(key); + listPreference.setSummary(listPreference.getEntry()); + } + else if (key.equals("bookmark.resolution") || key.equals("bookmark.resolution_3g")) + { + ListPreference listPreference = (ListPreference)findPreference(key); + listPreference.setSummary(listPreference.getEntry()); + + String value = listPreference.getValue(); + boolean enabled = value.equalsIgnoreCase("custom"); + if (key.equals("bookmark.resolution")) + { + findPreference("bookmark.width").setEnabled(enabled); + findPreference("bookmark.height").setEnabled(enabled); + } + else + { + findPreference("bookmark.width_3g").setEnabled(enabled); + findPreference("bookmark.height_3g").setEnabled(enabled); + } + } + else if (key.equals("bookmark.width") || key.equals("bookmark.width_3g")) + findPreference(key).setSummary(String.valueOf(sharedPreferences.getInt(key, 800))); + else if (key.equals("bookmark.height") || key.equals("bookmark.height_3g")) + findPreference(key).setSummary(String.valueOf(sharedPreferences.getInt(key, 600))); + } + + private void initDebugSettings(SharedPreferences sharedPreferences) + { + debugSettingsChanged(sharedPreferences, "bookmark.debug_level"); + debugSettingsChanged(sharedPreferences, "bookmark.async_channel"); + debugSettingsChanged(sharedPreferences, "bookmark.async_update"); + debugSettingsChanged(sharedPreferences, "bookmark.async_input"); + } + + private void initGatewaySettings(SharedPreferences sharedPreferences) + { + gatewaySettingsChanged(sharedPreferences, "bookmark.gateway_hostname"); + gatewaySettingsChanged(sharedPreferences, "bookmark.gateway_port"); + gatewaySettingsChanged(sharedPreferences, "bookmark.gateway_username"); + gatewaySettingsChanged(sharedPreferences, "bookmark.gateway_password"); + gatewaySettingsChanged(sharedPreferences, "bookmark.gateway_domain"); + } + + private void debugSettingsChanged(SharedPreferences sharedPreferences, String key) + { + if (key.equals("bookmark.debug_level")) + { + String level = sharedPreferences.getString(key, "INFO"); + Preference pref = findPreference("bookmark.debug_level"); + pref.setDefaultValue(level); + } + else if (key.equals("bookmark.async_channel")) + { + boolean enabled = sharedPreferences.getBoolean(key, false); + Preference pref = findPreference("bookmark.async_channel"); + pref.setDefaultValue(enabled); + } + else if (key.equals("bookmark.async_update")) + { + boolean enabled = sharedPreferences.getBoolean(key, false); + Preference pref = findPreference("bookmark.async_update"); + pref.setDefaultValue(enabled); + } + else if (key.equals("bookmark.async_input")) + { + boolean enabled = sharedPreferences.getBoolean(key, false); + Preference pref = findPreference("bookmark.async_input"); + pref.setDefaultValue(enabled); + } + } + + private void gatewaySettingsChanged(SharedPreferences sharedPreferences, String key) + { + if (key.equals("bookmark.gateway_hostname")) + { + findPreference(key).setSummary(sharedPreferences.getString(key, "")); + } + else if (key.equals("bookmark.gateway_port")) + { + findPreference(key).setSummary(String.valueOf(sharedPreferences.getInt(key, 443))); + } + else if (key.equals("bookmark.gateway_username")) + { + findPreference(key).setSummary(sharedPreferences.getString(key, "")); + } + else if (key.equals("bookmark.gateway_password")) + { + if (sharedPreferences.getString(key, "").length() == 0) + findPreference(key).setSummary( + getResources().getString(R.string.settings_password_empty)); + else + findPreference(key).setSummary( + getResources().getString(R.string.settings_password_present)); + } + else if (key.equals("bookmark.gateway_domain")) + findPreference(key).setSummary(sharedPreferences.getString(key, "")); + } + + private boolean verifySettings(SharedPreferences sharedPreferences) + { + + boolean verifyFailed = false; + // perform sanity checks on settings + // Label set + if (sharedPreferences.getString("bookmark.label", "").length() == 0) + verifyFailed = true; + + // Server and port specified + if (!verifyFailed && sharedPreferences.getString("bookmark.hostname", "").length() == 0) + verifyFailed = true; + + // Server and port specified + if (!verifyFailed && sharedPreferences.getInt("bookmark.port", -1) <= 0) + verifyFailed = true; + + // if an error occurred - display toast and return false + return (!verifyFailed); + } + + private void finishAndResetBookmark() + { + bookmark = null; + getPreferenceManager().getSharedPreferences().unregisterOnSharedPreferenceChangeListener( + this); + finish(); + } + + @Override public void onBackPressed() + { + // only proceed if we are in the main preferences screen + if (current_preferences != PREFERENCES_BOOKMARK) + { + super.onBackPressed(); + getPreferenceManager() + .getSharedPreferences() + .unregisterOnSharedPreferenceChangeListener(this); + return; + } + + SharedPreferences sharedPreferences = getPreferenceManager().getSharedPreferences(); + if (!verifySettings(sharedPreferences)) + { + // ask the user if he wants to cancel or continue editing + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(R.string.error_bookmark_incomplete_title) + .setMessage(R.string.error_bookmark_incomplete) + .setPositiveButton(R.string.cancel, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) + { + finishAndResetBookmark(); + } + }) + .setNegativeButton(R.string.cont, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) + { + dialog.cancel(); + } + }) + .show(); + + return; + } + else + { + // ask the user if he wants to save or cancel editing if a setting + // has changed + if (new_bookmark || settings_changed) + { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(R.string.dlg_title_save_bookmark) + .setMessage(R.string.dlg_save_bookmark) + .setPositiveButton( + R.string.yes, + new DialogInterface.OnClickListener() { + @Override public void onClick(DialogInterface dialog, int which) + { + // read shared prefs back to bookmark + bookmark.readFromSharedPreferences( + getPreferenceManager().getSharedPreferences()); + + BookmarkBaseGateway bookmarkGateway; + if (bookmark.getType() == BookmarkBase.TYPE_MANUAL) + { + bookmarkGateway = GlobalApp.getManualBookmarkGateway(); + // remove any history entry for this + // bookmark + GlobalApp.getQuickConnectHistoryGateway().removeHistoryItem( + bookmark.get().getHostname()); + } + else + { + assert false; + return; + } + + // insert or update bookmark and leave + // activity + if (bookmark.getId() > 0) + bookmarkGateway.update(bookmark); + else + bookmarkGateway.insert(bookmark); + + finishAndResetBookmark(); + } + }) + .setNegativeButton(R.string.no, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) + { + finishAndResetBookmark(); + } + }) + .show(); + } + else + { + finishAndResetBookmark(); + } + } + } +} diff --git a/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/presentation/HelpActivity.java b/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/presentation/HelpActivity.java new file mode 100644 index 0000000..8772c9e --- /dev/null +++ b/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/presentation/HelpActivity.java @@ -0,0 +1,77 @@ +/* + Activity that displays the help pages + + Copyright 2013 Thincast Technologies GmbH, Author: Martin Fleisz + + This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. + If a copy of the MPL was not distributed with this file, You can obtain one at + http://mozilla.org/MPL/2.0/. +*/ + +package com.freerdp.freerdpcore.presentation; + +import android.content.res.Configuration; +import android.os.Bundle; +import androidx.appcompat.app.AppCompatActivity; +import android.util.Log; +import android.webkit.WebSettings; +import android.webkit.WebView; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.Locale; + +public class HelpActivity extends AppCompatActivity +{ + + private static final String TAG = HelpActivity.class.toString(); + + @Override public void onCreate(Bundle savedInstanceState) + { + super.onCreate(savedInstanceState); + + WebView webview = new WebView(this); + setContentView(webview); + + String filename; + if ((getResources().getConfiguration().screenLayout & + Configuration.SCREENLAYOUT_SIZE_MASK) >= Configuration.SCREENLAYOUT_SIZE_LARGE) + filename = "gestures.html"; + else + filename = "gestures_phone.html"; + + WebSettings settings = webview.getSettings(); + settings.setDomStorageEnabled(true); + settings.setUseWideViewPort(true); + settings.setLoadWithOverviewMode(true); + settings.setSupportZoom(true); + settings.setJavaScriptEnabled(true); + + settings.setAllowContentAccess(true); + settings.setAllowFileAccess(true); + + final Locale def = Locale.getDefault(); + final String prefix = def.getLanguage().toLowerCase(def); + + final String base = "file:///android_asset/"; + final String baseName = "help_page"; + String dir = prefix + "_" + baseName + "/"; + String file = dir + filename; + InputStream is; + try + { + is = getAssets().open(file); + is.close(); + } + catch (IOException e) + { + Log.e(TAG, "Missing localized asset " + file, e); + dir = baseName + "/"; + file = dir + filename; + } + + webview.loadUrl(base + file); + } +} diff --git a/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/presentation/HomeActivity.java b/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/presentation/HomeActivity.java new file mode 100644 index 0000000..f8cd21c --- /dev/null +++ b/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/presentation/HomeActivity.java @@ -0,0 +1,399 @@ +/* + Main/Home Activity + + Copyright 2013 Thincast Technologies GmbH, Author: Martin Fleisz + + This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. + If a copy of the MPL was not distributed with this file, You can obtain one at + http://mozilla.org/MPL/2.0/. +*/ + +package com.freerdp.freerdpcore.presentation; + +import android.app.AlertDialog; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.res.Configuration; +import android.net.Uri; +import android.os.Bundle; +import androidx.appcompat.app.AppCompatActivity; +import android.text.Editable; +import android.text.TextWatcher; +import android.util.Log; +import android.view.ContextMenu; +import android.view.ContextMenu.ContextMenuInfo; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.View.OnCreateContextMenuListener; +import android.widget.AdapterView; +import android.widget.AdapterView.AdapterContextMenuInfo; +import android.widget.Button; +import android.widget.CheckBox; +import android.widget.EditText; +import android.widget.ListView; + +import com.freerdp.freerdpcore.R; +import com.freerdp.freerdpcore.application.GlobalApp; +import com.freerdp.freerdpcore.domain.BookmarkBase; +import com.freerdp.freerdpcore.domain.ConnectionReference; +import com.freerdp.freerdpcore.domain.PlaceholderBookmark; +import com.freerdp.freerdpcore.domain.QuickConnectBookmark; +import com.freerdp.freerdpcore.utils.BookmarkArrayAdapter; +import com.freerdp.freerdpcore.utils.SeparatedListAdapter; + +import java.util.ArrayList; + +public class HomeActivity extends AppCompatActivity +{ + private final static String ADD_BOOKMARK_PLACEHOLDER = "add_bookmark"; + private static final String TAG = "HomeActivity"; + private static final String PARAM_SUPERBAR_TEXT = "superbar_text"; + private ListView listViewBookmarks; + private Button clearTextButton; + private EditText superBarEditText; + private BookmarkArrayAdapter manualBookmarkAdapter; + private SeparatedListAdapter separatedListAdapter; + private PlaceholderBookmark addBookmarkPlaceholder; + private String sectionLabelBookmarks; + + View mDecor; + + @Override public void onCreate(Bundle savedInstanceState) + { + setTitle(R.string.title_home); + super.onCreate(savedInstanceState); + setContentView(R.layout.home); + + mDecor = getWindow().getDecorView(); + mDecor.setSystemUiVisibility(View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | + View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY); + + long heapSize = Runtime.getRuntime().maxMemory(); + Log.i(TAG, "Max HeapSize: " + heapSize); + Log.i(TAG, "App data folder: " + getFilesDir().toString()); + + // load strings + sectionLabelBookmarks = getResources().getString(R.string.section_bookmarks); + + // create add bookmark/quick connect bookmark placeholder + addBookmarkPlaceholder = new PlaceholderBookmark(); + addBookmarkPlaceholder.setName(ADD_BOOKMARK_PLACEHOLDER); + addBookmarkPlaceholder.setLabel( + getResources().getString(R.string.list_placeholder_add_bookmark)); + + // check for passed .rdp file and open it in a new bookmark + Intent caller = getIntent(); + Uri callParameter = caller.getData(); + + if (Intent.ACTION_VIEW.equals(caller.getAction()) && callParameter != null) + { + String refStr = ConnectionReference.getFileReference(callParameter.getPath()); + Bundle bundle = new Bundle(); + bundle.putString(BookmarkActivity.PARAM_CONNECTION_REFERENCE, refStr); + + Intent bookmarkIntent = + new Intent(this.getApplicationContext(), BookmarkActivity.class); + bookmarkIntent.putExtras(bundle); + startActivity(bookmarkIntent); + } + + // load views + clearTextButton = (Button)findViewById(R.id.clear_search_btn); + superBarEditText = (EditText)findViewById(R.id.superBarEditText); + + listViewBookmarks = (ListView)findViewById(R.id.listViewBookmarks); + + // set listeners for the list view + listViewBookmarks.setOnItemClickListener(new AdapterView.OnItemClickListener() { + public void onItemClick(AdapterView parent, View view, int position, long id) + { + String curSection = separatedListAdapter.getSectionForPosition(position); + Log.v(TAG, "Clicked on item id " + separatedListAdapter.getItemId(position) + + " in section " + curSection); + if (curSection.equals(sectionLabelBookmarks)) + { + String refStr = view.getTag().toString(); + if (ConnectionReference.isManualBookmarkReference(refStr) || + ConnectionReference.isHostnameReference(refStr)) + { + Bundle bundle = new Bundle(); + bundle.putString(SessionActivity.PARAM_CONNECTION_REFERENCE, refStr); + + Intent sessionIntent = new Intent(view.getContext(), SessionActivity.class); + sessionIntent.putExtras(bundle); + startActivity(sessionIntent); + + // clear any search text + superBarEditText.setText(""); + superBarEditText.clearFocus(); + } + else if (ConnectionReference.isPlaceholderReference(refStr)) + { + // is this the add bookmark placeholder? + if (ConnectionReference.getPlaceholder(refStr).equals( + ADD_BOOKMARK_PLACEHOLDER)) + { + Intent bookmarkIntent = + new Intent(view.getContext(), BookmarkActivity.class); + startActivity(bookmarkIntent); + } + } + } + } + }); + + listViewBookmarks.setOnCreateContextMenuListener(new OnCreateContextMenuListener() { + @Override + public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) + { + // if the selected item is not a session item (tag == null) and not a quick connect + // entry (not a hostname connection reference) inflate the context menu + View itemView = ((AdapterContextMenuInfo)menuInfo).targetView; + String refStr = itemView.getTag() != null ? itemView.getTag().toString() : null; + if (refStr != null && !ConnectionReference.isHostnameReference(refStr) && + !ConnectionReference.isPlaceholderReference(refStr)) + { + getMenuInflater().inflate(R.menu.bookmark_context_menu, menu); + menu.setHeaderTitle(getResources().getString(R.string.menu_title_bookmark)); + } + } + }); + + superBarEditText.addTextChangedListener(new SuperBarTextWatcher()); + + clearTextButton.setOnClickListener(new OnClickListener() { + @Override public void onClick(View v) + { + superBarEditText.setText(""); + } + }); + } + + @Override public void onConfigurationChanged(Configuration newConfig) + { + // ignore orientation/keyboard change + super.onConfigurationChanged(newConfig); + mDecor.setSystemUiVisibility(View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | + View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY); + } + + @Override public boolean onSearchRequested() + { + superBarEditText.requestFocus(); + return true; + } + + @Override public boolean onContextItemSelected(MenuItem aItem) + { + + // get connection reference + AdapterContextMenuInfo menuInfo = (AdapterContextMenuInfo)aItem.getMenuInfo(); + String refStr = menuInfo.targetView.getTag().toString(); + + // refer to http://tools.android.com/tips/non-constant-fields why we can't use switch/case + // here .. + int itemId = aItem.getItemId(); + if (itemId == R.id.bookmark_connect) + { + Bundle bundle = new Bundle(); + bundle.putString(SessionActivity.PARAM_CONNECTION_REFERENCE, refStr); + Intent sessionIntent = new Intent(this, SessionActivity.class); + sessionIntent.putExtras(bundle); + + startActivity(sessionIntent); + return true; + } + else if (itemId == R.id.bookmark_edit) + { + Bundle bundle = new Bundle(); + bundle.putString(BookmarkActivity.PARAM_CONNECTION_REFERENCE, refStr); + + Intent bookmarkIntent = + new Intent(this.getApplicationContext(), BookmarkActivity.class); + bookmarkIntent.putExtras(bundle); + startActivity(bookmarkIntent); + return true; + } + else if (itemId == R.id.bookmark_delete) + { + if (ConnectionReference.isManualBookmarkReference(refStr)) + { + long id = ConnectionReference.getManualBookmarkId(refStr); + GlobalApp.getManualBookmarkGateway().delete(id); + manualBookmarkAdapter.remove(id); + separatedListAdapter.notifyDataSetChanged(); + } + else + { + assert false; + } + + // clear super bar text + superBarEditText.setText(""); + return true; + } + + return false; + } + + @Override protected void onResume() + { + super.onResume(); + Log.v(TAG, "HomeActivity.onResume"); + + // create bookmark cursor adapter + manualBookmarkAdapter = new BookmarkArrayAdapter( + this, R.layout.bookmark_list_item, GlobalApp.getManualBookmarkGateway().findAll()); + + // add add bookmark item to manual adapter + manualBookmarkAdapter.insert(addBookmarkPlaceholder, 0); + + // attach all adapters to the separatedListView adapter and assign it to the list view + separatedListAdapter = new SeparatedListAdapter(this); + separatedListAdapter.addSection(sectionLabelBookmarks, manualBookmarkAdapter); + listViewBookmarks.setAdapter(separatedListAdapter); + + // if we have a filter text entered cause an update to be caused here + String filter = superBarEditText.getText().toString(); + if (filter.length() > 0) + superBarEditText.setText(filter); + } + + @Override protected void onPause() + { + super.onPause(); + Log.v(TAG, "HomeActivity.onPause"); + + // reset adapters + listViewBookmarks.setAdapter(null); + separatedListAdapter = null; + manualBookmarkAdapter = null; + } + + @Override public void onBackPressed() + { + // if back was pressed - ask the user if he really wants to exit + if (ApplicationSettingsActivity.getAskOnExit(this)) + { + final CheckBox cb = new CheckBox(this); + cb.setChecked(!ApplicationSettingsActivity.getAskOnExit(this)); + cb.setText(R.string.dlg_dont_show_again); + + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(R.string.dlg_title_exit) + .setMessage(R.string.dlg_msg_exit) + .setView(cb) + .setPositiveButton(R.string.yes, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) + { + finish(); + } + }) + .setNegativeButton(R.string.no, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) + { + dialog.dismiss(); + } + }) + .create() + .show(); + } + else + { + super.onBackPressed(); + } + } + + @Override protected void onSaveInstanceState(Bundle outState) + { + super.onSaveInstanceState(outState); + outState.putString(PARAM_SUPERBAR_TEXT, superBarEditText.getText().toString()); + } + + @Override protected void onRestoreInstanceState(Bundle inState) + { + super.onRestoreInstanceState(inState); + superBarEditText.setText(inState.getString(PARAM_SUPERBAR_TEXT)); + } + + @Override public boolean onCreateOptionsMenu(Menu menu) + { + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.home_menu, menu); + return true; + } + + @Override public boolean onOptionsItemSelected(MenuItem item) + { + + // refer to http://tools.android.com/tips/non-constant-fields why we can't use switch/case + // here .. + int itemId = item.getItemId(); + if (itemId == R.id.newBookmark) + { + Intent bookmarkIntent = new Intent(this, BookmarkActivity.class); + startActivity(bookmarkIntent); + } + else if (itemId == R.id.appSettings) + { + Intent settingsIntent = new Intent(this, ApplicationSettingsActivity.class); + startActivity(settingsIntent); + } + else if (itemId == R.id.help) + { + Intent helpIntent = new Intent(this, HelpActivity.class); + startActivity(helpIntent); + } + else if (itemId == R.id.about) + { + Intent aboutIntent = new Intent(this, AboutActivity.class); + startActivity(aboutIntent); + } + + return true; + } + + private class SuperBarTextWatcher implements TextWatcher + { + @Override public void afterTextChanged(Editable s) + { + if (separatedListAdapter != null) + { + String text = s.toString(); + if (text.length() > 0) + { + ArrayList computers_list = + GlobalApp.getQuickConnectHistoryGateway().findHistory(text); + computers_list.addAll( + GlobalApp.getManualBookmarkGateway().findByLabelOrHostnameLike(text)); + manualBookmarkAdapter.replaceItems(computers_list); + QuickConnectBookmark qcBm = new QuickConnectBookmark(); + qcBm.setLabel(text); + qcBm.setHostname(text); + manualBookmarkAdapter.insert(qcBm, 0); + } + else + { + manualBookmarkAdapter.replaceItems( + GlobalApp.getManualBookmarkGateway().findAll()); + manualBookmarkAdapter.insert(addBookmarkPlaceholder, 0); + } + + separatedListAdapter.notifyDataSetChanged(); + } + } + + @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) + { + } + + @Override public void onTextChanged(CharSequence s, int start, int before, int count) + { + } + } +} diff --git a/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/presentation/ScrollView2D.java b/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/presentation/ScrollView2D.java new file mode 100644 index 0000000..ad1d572 --- /dev/null +++ b/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/presentation/ScrollView2D.java @@ -0,0 +1,1349 @@ +/* + * Copyright (C) 2006 The Android Open Source Project + * + * Licensed 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. + */ +/* + * Revised 5/19/2010 by GORGES + * Now supports two-dimensional view scrolling + * http://GORGES.us + */ + +package com.freerdp.freerdpcore.presentation; + +import android.content.Context; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.view.FocusFinder; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.VelocityTracker; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewGroup; +import android.view.ViewParent; +import android.view.animation.AnimationUtils; +import android.widget.FrameLayout; +import android.widget.LinearLayout; +import android.widget.Scroller; +import android.widget.TextView; + +import java.util.List; + +/** + * Layout container for a view hierarchy that can be scrolled by the user, + * allowing it to be larger than the physical display. A TwoDScrollView + * is a {@link FrameLayout}, meaning you should place one child in it + * containing the entire contents to scroll; this child may itself be a layout + * manager with a complex hierarchy of objects. A child that is often used + * is a {@link LinearLayout} in a vertical orientation, presenting a vertical + * array of top-level items that the user can scroll through. + *

+ *

The {@link TextView} class also + * takes care of its own scrolling, so does not require a TwoDScrollView, but + * using the two together is possible to achieve the effect of a text view + * within a larger container. + */ +public class ScrollView2D extends FrameLayout +{ + + static final int ANIMATED_SCROLL_GAP = 250; + static final float MAX_SCROLL_FACTOR = 0.5f; + private final Rect mTempRect = new Rect(); + private ScrollView2DListener scrollView2DListener = null; + private long mLastScroll; + private Scroller mScroller; + private boolean scrollEnabled = true; + /** + * Flag to indicate that we are moving focus ourselves. This is so the + * code that watches for focus changes initiated outside this TwoDScrollView + * knows that it does not have to do anything. + */ + private boolean mTwoDScrollViewMovedFocus; + /** + * Position of the last motion event. + */ + private float mLastMotionY; + private float mLastMotionX; + /** + * True when the layout has changed but the traversal has not come through yet. + * Ideally the view hierarchy would keep track of this for us. + */ + private boolean mIsLayoutDirty = true; + /** + * The child to give focus to in the event that a child has requested focus while the + * layout is dirty. This prevents the scroll from being wrong if the child has not been + * laid out before requesting focus. + */ + private View mChildToScrollTo = null; + /** + * True if the user is currently dragging this TwoDScrollView around. This is + * not the same as 'is being flinged', which can be checked by + * mScroller.isFinished() (flinging begins when the user lifts his finger). + */ + private boolean mIsBeingDragged = false; + /** + * Determines speed during touch scrolling + */ + private VelocityTracker mVelocityTracker; + /** + * Whether arrow scrolling is animated. + */ + private int mTouchSlop; + private int mMinimumVelocity; + private int mMaximumVelocity; + public ScrollView2D(Context context) + { + super(context); + initTwoDScrollView(); + } + + public ScrollView2D(Context context, AttributeSet attrs) + { + super(context, attrs); + initTwoDScrollView(); + } + + public ScrollView2D(Context context, AttributeSet attrs, int defStyle) + { + super(context, attrs, defStyle); + initTwoDScrollView(); + } + + @Override protected float getTopFadingEdgeStrength() + { + if (getChildCount() == 0) + { + return 0.0f; + } + final int length = getVerticalFadingEdgeLength(); + if (getScrollY() < length) + { + return getScrollY() / (float)length; + } + return 1.0f; + } + + @Override protected float getBottomFadingEdgeStrength() + { + if (getChildCount() == 0) + { + return 0.0f; + } + final int length = getVerticalFadingEdgeLength(); + final int bottomEdge = getHeight() - getPaddingBottom(); + final int span = getChildAt(0).getBottom() - getScrollY() - bottomEdge; + if (span < length) + { + return span / (float)length; + } + return 1.0f; + } + + @Override protected float getLeftFadingEdgeStrength() + { + if (getChildCount() == 0) + { + return 0.0f; + } + final int length = getHorizontalFadingEdgeLength(); + if (getScrollX() < length) + { + return getScrollX() / (float)length; + } + return 1.0f; + } + + @Override protected float getRightFadingEdgeStrength() + { + if (getChildCount() == 0) + { + return 0.0f; + } + final int length = getHorizontalFadingEdgeLength(); + final int rightEdge = getWidth() - getPaddingRight(); + final int span = getChildAt(0).getRight() - getScrollX() - rightEdge; + if (span < length) + { + return span / (float)length; + } + return 1.0f; + } + + /** + * Disable/Enable scrolling + */ + public void setScrollEnabled(boolean enable) + { + scrollEnabled = enable; + } + + /** + * @return The maximum amount this scroll view will scroll in response to + * an arrow event. + */ + public int getMaxScrollAmountVertical() + { + return (int)(MAX_SCROLL_FACTOR * getHeight()); + } + + public int getMaxScrollAmountHorizontal() + { + return (int)(MAX_SCROLL_FACTOR * getWidth()); + } + + private void initTwoDScrollView() + { + mScroller = new Scroller(getContext()); + setFocusable(true); + setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); + setWillNotDraw(false); + final ViewConfiguration configuration = ViewConfiguration.get(getContext()); + mTouchSlop = configuration.getScaledTouchSlop(); + mMinimumVelocity = configuration.getScaledMinimumFlingVelocity(); + mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); + } + + @Override public void addView(View child) + { + if (getChildCount() > 0) + { + throw new IllegalStateException("TwoDScrollView can host only one direct child"); + } + super.addView(child); + } + + @Override public void addView(View child, int index) + { + if (getChildCount() > 0) + { + throw new IllegalStateException("TwoDScrollView can host only one direct child"); + } + super.addView(child, index); + } + + @Override public void addView(View child, ViewGroup.LayoutParams params) + { + if (getChildCount() > 0) + { + throw new IllegalStateException("TwoDScrollView can host only one direct child"); + } + super.addView(child, params); + } + + @Override public void addView(View child, int index, ViewGroup.LayoutParams params) + { + if (getChildCount() > 0) + { + throw new IllegalStateException("TwoDScrollView can host only one direct child"); + } + super.addView(child, index, params); + } + + /** + * @return Returns true this TwoDScrollView can be scrolled + */ + private boolean canScroll() + { + if (!scrollEnabled) + return false; + View child = getChildAt(0); + if (child != null) + { + int childHeight = child.getHeight(); + int childWidth = child.getWidth(); + return (getHeight() < childHeight + getPaddingTop() + getPaddingBottom()) || + (getWidth() < childWidth + getPaddingLeft() + getPaddingRight()); + } + return false; + } + + @Override public boolean dispatchKeyEvent(KeyEvent event) + { + // Let the focused view and/or our descendants get the key first + boolean handled = super.dispatchKeyEvent(event); + if (handled) + { + return true; + } + return executeKeyEvent(event); + } + + /** + * You can call this function yourself to have the scroll view perform + * scrolling from a key event, just as if the event had been dispatched to + * it by the view hierarchy. + * + * @param event The key event to execute. + * @return Return true if the event was handled, else false. + */ + public boolean executeKeyEvent(KeyEvent event) + { + mTempRect.setEmpty(); + if (!canScroll()) + { + if (isFocused()) + { + View currentFocused = findFocus(); + if (currentFocused == this) + currentFocused = null; + View nextFocused = + FocusFinder.getInstance().findNextFocus(this, currentFocused, View.FOCUS_DOWN); + return nextFocused != null && nextFocused != this && + nextFocused.requestFocus(View.FOCUS_DOWN); + } + return false; + } + boolean handled = false; + if (event.getAction() == KeyEvent.ACTION_DOWN) + { + switch (event.getKeyCode()) + { + case KeyEvent.KEYCODE_DPAD_UP: + if (!event.isAltPressed()) + { + handled = arrowScroll(View.FOCUS_UP, false); + } + else + { + handled = fullScroll(View.FOCUS_UP, false); + } + break; + case KeyEvent.KEYCODE_DPAD_DOWN: + if (!event.isAltPressed()) + { + handled = arrowScroll(View.FOCUS_DOWN, false); + } + else + { + handled = fullScroll(View.FOCUS_DOWN, false); + } + break; + case KeyEvent.KEYCODE_DPAD_LEFT: + if (!event.isAltPressed()) + { + handled = arrowScroll(View.FOCUS_LEFT, true); + } + else + { + handled = fullScroll(View.FOCUS_LEFT, true); + } + break; + case KeyEvent.KEYCODE_DPAD_RIGHT: + if (!event.isAltPressed()) + { + handled = arrowScroll(View.FOCUS_RIGHT, true); + } + else + { + handled = fullScroll(View.FOCUS_RIGHT, true); + } + break; + } + } + return handled; + } + + @Override public boolean onInterceptTouchEvent(MotionEvent ev) + { + /* + * This method JUST determines whether we want to intercept the motion. + * If we return true, onMotionEvent will be called and we do the actual + * scrolling there. + * + * Shortcut the most recurring case: the user is in the dragging + * state and he is moving his finger. We want to intercept this + * motion. + */ + final int action = ev.getAction(); + if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) + { + return true; + } + if (!canScroll()) + { + mIsBeingDragged = false; + return false; + } + final float y = ev.getY(); + final float x = ev.getX(); + switch (action) + { + case MotionEvent.ACTION_MOVE: + /* + * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check + * whether the user has moved far enough from his original down touch. + */ + /* + * Locally do absolute value. mLastMotionY is set to the y value + * of the down event. + */ + final int yDiff = (int)Math.abs(y - mLastMotionY); + final int xDiff = (int)Math.abs(x - mLastMotionX); + if (yDiff > mTouchSlop || xDiff > mTouchSlop) + { + mIsBeingDragged = true; + } + break; + + case MotionEvent.ACTION_DOWN: + /* Remember location of down touch */ + mLastMotionY = y; + mLastMotionX = x; + + /* + * If being flinged and user touches the screen, initiate drag; + * otherwise don't. mScroller.isFinished should be false when + * being flinged. + */ + mIsBeingDragged = !mScroller.isFinished(); + break; + + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_UP: + /* Release the drag */ + mIsBeingDragged = false; + break; + } + + /* + * The only time we want to intercept motion events is if we are in the + * drag mode. + */ + return mIsBeingDragged; + } + + @Override public boolean onTouchEvent(MotionEvent ev) + { + + if (ev.getAction() == MotionEvent.ACTION_DOWN && ev.getEdgeFlags() != 0) + { + // Don't handle edge touches immediately -- they may actually belong to one of our + // descendants. + return false; + } + + if (!canScroll()) + { + return false; + } + + if (mVelocityTracker == null) + { + mVelocityTracker = VelocityTracker.obtain(); + } + mVelocityTracker.addMovement(ev); + + final int action = ev.getAction(); + final float y = ev.getY(); + final float x = ev.getX(); + + switch (action) + { + case MotionEvent.ACTION_DOWN: + /* + * If being flinged and user touches, stop the fling. isFinished + * will be false if being flinged. + */ + if (!mScroller.isFinished()) + { + mScroller.abortAnimation(); + } + + // Remember where the motion event started + mLastMotionY = y; + mLastMotionX = x; + break; + case MotionEvent.ACTION_MOVE: + // Scroll to follow the motion event + int deltaX = (int)(mLastMotionX - x); + int deltaY = (int)(mLastMotionY - y); + mLastMotionX = x; + mLastMotionY = y; + + if (deltaX < 0) + { + if (getScrollX() < 0) + { + deltaX = 0; + } + } + else if (deltaX > 0) + { + final int rightEdge = getWidth() - getPaddingRight(); + final int availableToScroll = + getChildAt(0).getRight() - getScrollX() - rightEdge; + if (availableToScroll > 0) + { + deltaX = Math.min(availableToScroll, deltaX); + } + else + { + deltaX = 0; + } + } + if (deltaY < 0) + { + if (getScrollY() < 0) + { + deltaY = 0; + } + } + else if (deltaY > 0) + { + final int bottomEdge = getHeight() - getPaddingBottom(); + final int availableToScroll = + getChildAt(0).getBottom() - getScrollY() - bottomEdge; + if (availableToScroll > 0) + { + deltaY = Math.min(availableToScroll, deltaY); + } + else + { + deltaY = 0; + } + } + if (deltaY != 0 || deltaX != 0) + scrollBy(deltaX, deltaY); + break; + case MotionEvent.ACTION_UP: + final VelocityTracker velocityTracker = mVelocityTracker; + velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); + int initialXVelocity = (int)velocityTracker.getXVelocity(); + int initialYVelocity = (int)velocityTracker.getYVelocity(); + if ((Math.abs(initialXVelocity) + Math.abs(initialYVelocity) > mMinimumVelocity) && + getChildCount() > 0) + { + fling(-initialXVelocity, -initialYVelocity); + } + if (mVelocityTracker != null) + { + mVelocityTracker.recycle(); + mVelocityTracker = null; + } + } + return true; + } + + /** + * Finds the next focusable component that fits in this View's bounds + * (excluding fading edges) pretending that this View's top is located at + * the parameter top. + * + * @param topFocus look for a candidate is the one at the top of the bounds + * if topFocus is true, or at the bottom of the bounds if topFocus is + * false + * @param top the top offset of the bounds in which a focusable must be + * found (the fading edge is assumed to start at this position) + * @param preferredFocusable the View that has highest priority and will be + * returned if it is within my bounds (null is valid) + * @return the next focusable component in the bounds or null if none can be + * found + */ + private View findFocusableViewInMyBounds(final boolean topFocus, final int top, + final boolean leftFocus, final int left, + View preferredFocusable) + { + /* + * The fading edge's transparent side should be considered for focus + * since it's mostly visible, so we divide the actual fading edge length + * by 2. + */ + final int verticalFadingEdgeLength = getVerticalFadingEdgeLength() / 2; + final int topWithoutFadingEdge = top + verticalFadingEdgeLength; + final int bottomWithoutFadingEdge = top + getHeight() - verticalFadingEdgeLength; + final int horizontalFadingEdgeLength = getHorizontalFadingEdgeLength() / 2; + final int leftWithoutFadingEdge = left + horizontalFadingEdgeLength; + final int rightWithoutFadingEdge = left + getWidth() - horizontalFadingEdgeLength; + + if ((preferredFocusable != null) && + (preferredFocusable.getTop() < bottomWithoutFadingEdge) && + (preferredFocusable.getBottom() > topWithoutFadingEdge) && + (preferredFocusable.getLeft() < rightWithoutFadingEdge) && + (preferredFocusable.getRight() > leftWithoutFadingEdge)) + { + return preferredFocusable; + } + return findFocusableViewInBounds(topFocus, topWithoutFadingEdge, bottomWithoutFadingEdge, + leftFocus, leftWithoutFadingEdge, rightWithoutFadingEdge); + } + + /** + * Finds the next focusable component that fits in the specified bounds. + *

+ * + * @param topFocus look for a candidate is the one at the top of the bounds + * if topFocus is true, or at the bottom of the bounds if topFocus is + * false + * @param top the top offset of the bounds in which a focusable must be + * found + * @param bottom the bottom offset of the bounds in which a focusable must + * be found + * @return the next focusable component in the bounds or null if none can + * be found + */ + private View findFocusableViewInBounds(boolean topFocus, int top, int bottom, boolean leftFocus, + int left, int right) + { + List focusables = getFocusables(View.FOCUS_FORWARD); + View focusCandidate = null; + + /* + * A fully contained focusable is one where its top is below the bound's + * top, and its bottom is above the bound's bottom. A partially + * contained focusable is one where some part of it is within the + * bounds, but it also has some part that is not within bounds. A fully contained + * focusable is preferred to a partially contained focusable. + */ + boolean foundFullyContainedFocusable = false; + + int count = focusables.size(); + for (int i = 0; i < count; i++) + { + View view = focusables.get(i); + int viewTop = view.getTop(); + int viewBottom = view.getBottom(); + int viewLeft = view.getLeft(); + int viewRight = view.getRight(); + + if (top < viewBottom && viewTop < bottom && left < viewRight && viewLeft < right) + { + /* + * the focusable is in the target area, it is a candidate for + * focusing + */ + final boolean viewIsFullyContained = (top < viewTop) && (viewBottom < bottom) && + (left < viewLeft) && (viewRight < right); + if (focusCandidate == null) + { + /* No candidate, take this one */ + focusCandidate = view; + foundFullyContainedFocusable = viewIsFullyContained; + } + else + { + final boolean viewIsCloserToVerticalBoundary = + (topFocus && viewTop < focusCandidate.getTop()) || + (!topFocus && viewBottom > focusCandidate.getBottom()); + final boolean viewIsCloserToHorizontalBoundary = + (leftFocus && viewLeft < focusCandidate.getLeft()) || + (!leftFocus && viewRight > focusCandidate.getRight()); + if (foundFullyContainedFocusable) + { + if (viewIsFullyContained && viewIsCloserToVerticalBoundary && + viewIsCloserToHorizontalBoundary) + { + /* + * We're dealing with only fully contained views, so + * it has to be closer to the boundary to beat our + * candidate + */ + focusCandidate = view; + } + } + else + { + if (viewIsFullyContained) + { + /* Any fully contained view beats a partially contained view */ + focusCandidate = view; + foundFullyContainedFocusable = true; + } + else if (viewIsCloserToVerticalBoundary && viewIsCloserToHorizontalBoundary) + { + /* + * Partially contained view beats another partially + * contained view if it's closer + */ + focusCandidate = view; + } + } + } + } + } + return focusCandidate; + } + + /** + *

Handles scrolling in response to a "home/end" shortcut press. This + * method will scroll the view to the top or bottom and give the focus + * to the topmost/bottommost component in the new visible area. If no + * component is a good candidate for focus, this scrollview reclaims the + * focus.

+ * + * @param direction the scroll direction: {@link android.view.View#FOCUS_UP} + * to go the top of the view or + * {@link android.view.View#FOCUS_DOWN} to go the bottom + * @return true if the key event is consumed by this method, false otherwise + */ + public boolean fullScroll(int direction, boolean horizontal) + { + if (!horizontal) + { + boolean down = direction == View.FOCUS_DOWN; + int height = getHeight(); + mTempRect.top = 0; + mTempRect.bottom = height; + if (down) + { + int count = getChildCount(); + if (count > 0) + { + View view = getChildAt(count - 1); + mTempRect.bottom = view.getBottom(); + mTempRect.top = mTempRect.bottom - height; + } + } + return scrollAndFocus(direction, mTempRect.top, mTempRect.bottom, 0, 0, 0); + } + else + { + boolean right = direction == View.FOCUS_DOWN; + int width = getWidth(); + mTempRect.left = 0; + mTempRect.right = width; + if (right) + { + int count = getChildCount(); + if (count > 0) + { + View view = getChildAt(count - 1); + mTempRect.right = view.getBottom(); + mTempRect.left = mTempRect.right - width; + } + } + return scrollAndFocus(0, 0, 0, direction, mTempRect.top, mTempRect.bottom); + } + } + + /** + *

Scrolls the view to make the area defined by top and + * bottom visible. This method attempts to give the focus + * to a component visible in this area. If no component can be focused in + * the new visible area, the focus is reclaimed by this scrollview.

+ * + * @param direction the scroll direction: {@link android.view.View#FOCUS_UP} + * to go upward + * {@link android.view.View#FOCUS_DOWN} to downward + * @param top the top offset of the new area to be made visible + * @param bottom the bottom offset of the new area to be made visible + * @return true if the key event is consumed by this method, false otherwise + */ + private boolean scrollAndFocus(int directionY, int top, int bottom, int directionX, int left, + int right) + { + boolean handled = true; + int height = getHeight(); + int containerTop = getScrollY(); + int containerBottom = containerTop + height; + boolean up = directionY == View.FOCUS_UP; + int width = getWidth(); + int containerLeft = getScrollX(); + int containerRight = containerLeft + width; + boolean leftwards = directionX == View.FOCUS_UP; + View newFocused = findFocusableViewInBounds(up, top, bottom, leftwards, left, right); + if (newFocused == null) + { + newFocused = this; + } + if ((top >= containerTop && bottom <= containerBottom) || + (left >= containerLeft && right <= containerRight)) + { + handled = false; + } + else + { + int deltaY = up ? (top - containerTop) : (bottom - containerBottom); + int deltaX = leftwards ? (left - containerLeft) : (right - containerRight); + doScroll(deltaX, deltaY); + } + if (newFocused != findFocus() && newFocused.requestFocus(directionY)) + { + mTwoDScrollViewMovedFocus = true; + mTwoDScrollViewMovedFocus = false; + } + return handled; + } + + /** + * Handle scrolling in response to an up or down arrow click. + * + * @param direction The direction corresponding to the arrow key that was + * pressed + * @return True if we consumed the event, false otherwise + */ + public boolean arrowScroll(int direction, boolean horizontal) + { + View currentFocused = findFocus(); + if (currentFocused == this) + currentFocused = null; + View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused, direction); + final int maxJump = + horizontal ? getMaxScrollAmountHorizontal() : getMaxScrollAmountVertical(); + + if (!horizontal) + { + if (nextFocused != null) + { + nextFocused.getDrawingRect(mTempRect); + offsetDescendantRectToMyCoords(nextFocused, mTempRect); + int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect); + doScroll(0, scrollDelta); + nextFocused.requestFocus(direction); + } + else + { + // no new focus + int scrollDelta = maxJump; + if (direction == View.FOCUS_UP && getScrollY() < scrollDelta) + { + scrollDelta = getScrollY(); + } + else if (direction == View.FOCUS_DOWN) + { + if (getChildCount() > 0) + { + int daBottom = getChildAt(0).getBottom(); + int screenBottom = getScrollY() + getHeight(); + if (daBottom - screenBottom < maxJump) + { + scrollDelta = daBottom - screenBottom; + } + } + } + if (scrollDelta == 0) + { + return false; + } + doScroll(0, direction == View.FOCUS_DOWN ? scrollDelta : -scrollDelta); + } + } + else + { + if (nextFocused != null) + { + nextFocused.getDrawingRect(mTempRect); + offsetDescendantRectToMyCoords(nextFocused, mTempRect); + int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect); + doScroll(scrollDelta, 0); + nextFocused.requestFocus(direction); + } + else + { + // no new focus + int scrollDelta = maxJump; + if (direction == View.FOCUS_UP && getScrollY() < scrollDelta) + { + scrollDelta = getScrollY(); + } + else if (direction == View.FOCUS_DOWN) + { + if (getChildCount() > 0) + { + int daBottom = getChildAt(0).getBottom(); + int screenBottom = getScrollY() + getHeight(); + if (daBottom - screenBottom < maxJump) + { + scrollDelta = daBottom - screenBottom; + } + } + } + if (scrollDelta == 0) + { + return false; + } + doScroll(direction == View.FOCUS_DOWN ? scrollDelta : -scrollDelta, 0); + } + } + return true; + } + + /** + * Smooth scroll by a Y delta + * + * @param delta the number of pixels to scroll by on the Y axis + */ + private void doScroll(int deltaX, int deltaY) + { + if (deltaX != 0 || deltaY != 0) + { + smoothScrollBy(deltaX, deltaY); + } + } + + /** + * Like {@link View#scrollBy}, but scroll smoothly instead of immediately. + * + * @param dx the number of pixels to scroll by on the X axis + * @param dy the number of pixels to scroll by on the Y axis + */ + public final void smoothScrollBy(int dx, int dy) + { + long duration = AnimationUtils.currentAnimationTimeMillis() - mLastScroll; + if (duration > ANIMATED_SCROLL_GAP) + { + mScroller.startScroll(getScrollX(), getScrollY(), dx, dy); + awakenScrollBars(mScroller.getDuration()); + invalidate(); + } + else + { + if (!mScroller.isFinished()) + { + mScroller.abortAnimation(); + } + scrollBy(dx, dy); + } + mLastScroll = AnimationUtils.currentAnimationTimeMillis(); + } + + /** + * Like {@link #scrollTo}, but scroll smoothly instead of immediately. + * + * @param x the position where to scroll on the X axis + * @param y the position where to scroll on the Y axis + */ + public final void smoothScrollTo(int x, int y) + { + smoothScrollBy(x - getScrollX(), y - getScrollY()); + } + + /** + *

The scroll range of a scroll view is the overall height of all of its + * children.

+ */ + @Override protected int computeVerticalScrollRange() + { + int count = getChildCount(); + return count == 0 ? getHeight() : (getChildAt(0)).getBottom(); + } + + @Override protected int computeHorizontalScrollRange() + { + int count = getChildCount(); + return count == 0 ? getWidth() : (getChildAt(0)).getRight(); + } + + @Override + protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) + { + ViewGroup.LayoutParams lp = child.getLayoutParams(); + int childWidthMeasureSpec; + int childHeightMeasureSpec; + + childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, + getPaddingLeft() + getPaddingRight(), lp.width); + childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + + child.measure(childWidthMeasureSpec, childHeightMeasureSpec); + } + + @Override + protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, + int parentHeightMeasureSpec, int heightUsed) + { + final MarginLayoutParams lp = (MarginLayoutParams)child.getLayoutParams(); + final int childWidthMeasureSpec = + MeasureSpec.makeMeasureSpec(lp.leftMargin + lp.rightMargin, MeasureSpec.UNSPECIFIED); + final int childHeightMeasureSpec = + MeasureSpec.makeMeasureSpec(lp.topMargin + lp.bottomMargin, MeasureSpec.UNSPECIFIED); + + child.measure(childWidthMeasureSpec, childHeightMeasureSpec); + } + + @Override public void computeScroll() + { + if (mScroller.computeScrollOffset()) + { + // This is called at drawing time by ViewGroup. We don't want to + // re-show the scrollbars at this point, which scrollTo will do, + // so we replicate most of scrollTo here. + // + // It's a little odd to call onScrollChanged from inside the drawing. + // + // It is, except when you remember that computeScroll() is used to + // animate scrolling. So unless we want to defer the onScrollChanged() + // until the end of the animated scrolling, we don't really have a + // choice here. + // + // I agree. The alternative, which I think would be worse, is to post + // something and tell the subclasses later. This is bad because there + // will be a window where mScrollX/Y is different from what the app + // thinks it is. + // + int oldX = getScrollX(); + int oldY = getScrollY(); + int x = mScroller.getCurrX(); + int y = mScroller.getCurrY(); + if (getChildCount() > 0) + { + View child = getChildAt(0); + scrollTo( + clamp(x, getWidth() - getPaddingRight() - getPaddingLeft(), child.getWidth()), + clamp(y, getHeight() - getPaddingBottom() - getPaddingTop(), + child.getHeight())); + } + else + { + scrollTo(x, y); + } + if (oldX != getScrollX() || oldY != getScrollY()) + { + onScrollChanged(getScrollX(), getScrollY(), oldX, oldY); + } + + // Keep on drawing until the animation has finished. + postInvalidate(); + } + } + + /** + * Scrolls the view to the given child. + * + * @param child the View to scroll to + */ + private void scrollToChild(View child) + { + child.getDrawingRect(mTempRect); + /* Offset from child's local coordinates to TwoDScrollView coordinates */ + offsetDescendantRectToMyCoords(child, mTempRect); + int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect); + if (scrollDelta != 0) + { + scrollBy(0, scrollDelta); + } + } + + /** + * If rect is off screen, scroll just enough to get it (or at least the + * first screen size chunk of it) on screen. + * + * @param rect The rectangle. + * @param immediate True to scroll immediately without animation + * @return true if scrolling was performed + */ + private boolean scrollToChildRect(Rect rect, boolean immediate) + { + final int delta = computeScrollDeltaToGetChildRectOnScreen(rect); + final boolean scroll = delta != 0; + if (scroll) + { + if (immediate) + { + scrollBy(0, delta); + } + else + { + smoothScrollBy(0, delta); + } + } + return scroll; + } + + /** + * Compute the amount to scroll in the Y direction in order to get + * a rectangle completely on the screen (or, if taller than the screen, + * at least the first screen size chunk of it). + * + * @param rect The rect. + * @return The scroll delta. + */ + protected int computeScrollDeltaToGetChildRectOnScreen(Rect rect) + { + if (getChildCount() == 0) + return 0; + int height = getHeight(); + int screenTop = getScrollY(); + int screenBottom = screenTop + height; + int fadingEdge = getVerticalFadingEdgeLength(); + // leave room for top fading edge as long as rect isn't at very top + if (rect.top > 0) + { + screenTop += fadingEdge; + } + + // leave room for bottom fading edge as long as rect isn't at very bottom + if (rect.bottom < getChildAt(0).getHeight()) + { + screenBottom -= fadingEdge; + } + int scrollYDelta = 0; + if (rect.bottom > screenBottom && rect.top > screenTop) + { + // need to move down to get it in view: move down just enough so + // that the entire rectangle is in view (or at least the first + // screen size chunk). + if (rect.height() > height) + { + // just enough to get screen size chunk on + scrollYDelta += (rect.top - screenTop); + } + else + { + // get entire rect at bottom of screen + scrollYDelta += (rect.bottom - screenBottom); + } + + // make sure we aren't scrolling beyond the end of our content + int bottom = getChildAt(0).getBottom(); + int distanceToBottom = bottom - screenBottom; + scrollYDelta = Math.min(scrollYDelta, distanceToBottom); + } + else if (rect.top < screenTop && rect.bottom < screenBottom) + { + // need to move up to get it in view: move up just enough so that + // entire rectangle is in view (or at least the first screen + // size chunk of it). + + if (rect.height() > height) + { + // screen size chunk + scrollYDelta -= (screenBottom - rect.bottom); + } + else + { + // entire rect at top + scrollYDelta -= (screenTop - rect.top); + } + + // make sure we aren't scrolling any further than the top our content + scrollYDelta = Math.max(scrollYDelta, -getScrollY()); + } + return scrollYDelta; + } + + @Override public void requestChildFocus(View child, View focused) + { + if (!mTwoDScrollViewMovedFocus) + { + if (!mIsLayoutDirty) + { + scrollToChild(focused); + } + else + { + // The child may not be laid out yet, we can't compute the scroll yet + mChildToScrollTo = focused; + } + } + super.requestChildFocus(child, focused); + } + + /** + * When looking for focus in children of a scroll view, need to be a little + * more careful not to give focus to something that is scrolled off screen. + *

+ * This is more expensive than the default {@link android.view.ViewGroup} + * implementation, otherwise this behavior might have been made the default. + */ + @Override + protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) + { + // convert from forward / backward notation to up / down / left / right + // (ugh). + if (direction == View.FOCUS_FORWARD) + { + direction = View.FOCUS_DOWN; + } + else if (direction == View.FOCUS_BACKWARD) + { + direction = View.FOCUS_UP; + } + + final View nextFocus = previouslyFocusedRect == null + ? FocusFinder.getInstance().findNextFocus(this, null, direction) + : FocusFinder.getInstance().findNextFocusFromRect( + this, previouslyFocusedRect, direction); + + if (nextFocus == null) + { + return false; + } + + return nextFocus.requestFocus(direction, previouslyFocusedRect); + } + + @Override + public boolean requestChildRectangleOnScreen(View child, Rect rectangle, boolean immediate) + { + // offset into coordinate space of this scroll view + rectangle.offset(child.getLeft() - child.getScrollX(), child.getTop() - child.getScrollY()); + return scrollToChildRect(rectangle, immediate); + } + + @Override public void requestLayout() + { + mIsLayoutDirty = true; + super.requestLayout(); + } + + @Override protected void onLayout(boolean changed, int l, int t, int r, int b) + { + super.onLayout(changed, l, t, r, b); + mIsLayoutDirty = false; + // Give a child focus if it needs it + if (mChildToScrollTo != null && isViewDescendantOf(mChildToScrollTo, this)) + { + scrollToChild(mChildToScrollTo); + } + mChildToScrollTo = null; + + // Calling this with the present values causes it to re-clam them + scrollTo(getScrollX(), getScrollY()); + } + + @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) + { + super.onSizeChanged(w, h, oldw, oldh); + + View currentFocused = findFocus(); + if (null == currentFocused || this == currentFocused) + return; + + // If the currently-focused view was visible on the screen when the + // screen was at the old height, then scroll the screen to make that + // view visible with the new screen height. + currentFocused.getDrawingRect(mTempRect); + offsetDescendantRectToMyCoords(currentFocused, mTempRect); + int scrollDeltaX = computeScrollDeltaToGetChildRectOnScreen(mTempRect); + int scrollDeltaY = computeScrollDeltaToGetChildRectOnScreen(mTempRect); + doScroll(scrollDeltaX, scrollDeltaY); + } + + /** + * Return true if child is an descendant of parent, (or equal to the parent). + */ + private boolean isViewDescendantOf(View child, View parent) + { + if (child == parent) + { + return true; + } + + final ViewParent theParent = child.getParent(); + return (theParent instanceof ViewGroup) && isViewDescendantOf((View)theParent, parent); + } + + /** + * Fling the scroll view + * + * @param velocityY The initial velocity in the Y direction. Positive + * numbers mean that the finger/curor is moving down the screen, + * which means we want to scroll towards the top. + */ + public void fling(int velocityX, int velocityY) + { + if (getChildCount() > 0) + { + int height = getHeight() - getPaddingBottom() - getPaddingTop(); + int bottom = getChildAt(0).getHeight(); + int width = getWidth() - getPaddingRight() - getPaddingLeft(); + int right = getChildAt(0).getWidth(); + + mScroller.fling(getScrollX(), getScrollY(), velocityX, velocityY, 0, right - width, 0, + bottom - height); + + final boolean movingDown = velocityY > 0; + final boolean movingRight = velocityX > 0; + + View newFocused = findFocusableViewInMyBounds( + movingRight, mScroller.getFinalX(), movingDown, mScroller.getFinalY(), findFocus()); + if (newFocused == null) + { + newFocused = this; + } + + if (newFocused != findFocus() && + newFocused.requestFocus(movingDown ? View.FOCUS_DOWN : View.FOCUS_UP)) + { + mTwoDScrollViewMovedFocus = true; + mTwoDScrollViewMovedFocus = false; + } + + awakenScrollBars(mScroller.getDuration()); + invalidate(); + } + } + + /** + * {@inheritDoc} + *

+ *

This version also clamps the scrolling to the bounds of our child. + */ + public void scrollTo(int x, int y) + { + // we rely on the fact the View.scrollBy calls scrollTo. + if (getChildCount() > 0) + { + View child = getChildAt(0); + x = clamp(x, getWidth() - getPaddingRight() - getPaddingLeft(), child.getWidth()); + y = clamp(y, getHeight() - getPaddingBottom() - getPaddingTop(), child.getHeight()); + if (x != getScrollX() || y != getScrollY()) + { + super.scrollTo(x, y); + } + } + } + + private int clamp(int n, int my, int child) + { + if (my >= child || n < 0) + { + /* my >= child is this case: + * |--------------- me ---------------| + * |------ child ------| + * or + * |--------------- me ---------------| + * |------ child ------| + * or + * |--------------- me ---------------| + * |------ child ------| + * + * n < 0 is this case: + * |------ me ------| + * |-------- child --------| + * |-- mScrollX --| + */ + return 0; + } + if ((my + n) > child) + { + /* this case: + * |------ me ------| + * |------ child ------| + * |-- mScrollX --| + */ + return child - my; + } + return n; + } + + public void setScrollViewListener(ScrollView2DListener scrollViewListener) + { + this.scrollView2DListener = scrollViewListener; + } + + @Override protected void onScrollChanged(int x, int y, int oldx, int oldy) + { + super.onScrollChanged(x, y, oldx, oldy); + if (scrollView2DListener != null) + { + scrollView2DListener.onScrollChanged(this, x, y, oldx, oldy); + } + } + + // interface to receive notifications when the view is scrolled + public interface ScrollView2DListener { + abstract void onScrollChanged(ScrollView2D scrollView, int x, int y, int oldx, int oldy); + } +} diff --git a/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/presentation/SessionActivity.java b/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/presentation/SessionActivity.java new file mode 100644 index 0000000..a2d25f1 --- /dev/null +++ b/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/presentation/SessionActivity.java @@ -0,0 +1,1433 @@ +/* + Android Session Activity + + Copyright 2013 Thincast Technologies GmbH, Author: Martin Fleisz + + This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. + If a copy of the MPL was not distributed with this file, You can obtain one at + http://mozilla.org/MPL/2.0/. + */ + +package com.freerdp.freerdpcore.presentation; + +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.ProgressDialog; +import android.app.UiModeManager; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.SharedPreferences; +import android.content.res.Configuration; +import android.graphics.Bitmap; +import android.graphics.Bitmap.Config; +import android.graphics.Point; +import android.graphics.Rect; +import android.graphics.drawable.BitmapDrawable; +import android.inputmethodservice.Keyboard; +import android.inputmethodservice.KeyboardView; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import androidx.appcompat.app.AppCompatActivity; +import android.util.Log; +import android.view.KeyEvent; +import android.view.Menu; +import android.view.MenuItem; +import android.view.MotionEvent; +import android.view.ScaleGestureDetector; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewTreeObserver.OnGlobalLayoutListener; +import android.view.WindowManager; +import android.view.inputmethod.InputMethodManager; +import android.widget.EditText; +import android.widget.Toast; +import android.widget.ZoomControls; + +import com.freerdp.freerdpcore.R; +import com.freerdp.freerdpcore.application.GlobalApp; +import com.freerdp.freerdpcore.application.SessionState; +import com.freerdp.freerdpcore.domain.BookmarkBase; +import com.freerdp.freerdpcore.domain.ConnectionReference; +import com.freerdp.freerdpcore.domain.ManualBookmark; +import com.freerdp.freerdpcore.services.LibFreeRDP; +import com.freerdp.freerdpcore.utils.ClipboardManagerProxy; +import com.freerdp.freerdpcore.utils.KeyboardMapper; +import com.freerdp.freerdpcore.utils.Mouse; + +import java.util.Collection; +import java.util.Iterator; +import java.util.List; + +public class SessionActivity extends AppCompatActivity + implements LibFreeRDP.UIEventListener, KeyboardView.OnKeyboardActionListener, + ScrollView2D.ScrollView2DListener, KeyboardMapper.KeyProcessingListener, + SessionView.SessionViewListener, TouchPointerView.TouchPointerListener, + ClipboardManagerProxy.OnClipboardChangedListener +{ + public static final String PARAM_CONNECTION_REFERENCE = "conRef"; + public static final String PARAM_INSTANCE = "instance"; + private static final float ZOOMING_STEP = 0.5f; + private static final int ZOOMCONTROLS_AUTOHIDE_TIMEOUT = 4000; + // timeout between subsequent scrolling requests when the touch-pointer is + // at the edge of the session view + private static final int SCROLLING_TIMEOUT = 50; + private static final int SCROLLING_DISTANCE = 20; + private static final String TAG = "FreeRDP.SessionActivity"; + // variables for delayed move event sending + private static final int MAX_DISCARDED_MOVE_EVENTS = 3; + private static final int SEND_MOVE_EVENT_TIMEOUT = 150; + private Bitmap bitmap; + private SessionState session; + private SessionView sessionView; + private TouchPointerView touchPointerView; + private ProgressDialog progressDialog; + private KeyboardView keyboardView; + private KeyboardView modifiersKeyboardView; + private ZoomControls zoomControls; + private KeyboardMapper keyboardMapper; + + private Keyboard specialkeysKeyboard; + private Keyboard numpadKeyboard; + private Keyboard cursorKeyboard; + private Keyboard modifiersKeyboard; + + private AlertDialog dlgVerifyCertificate; + private AlertDialog dlgUserCredentials; + private View userCredView; + + private UIHandler uiHandler; + + private int screen_width; + private int screen_height; + + private boolean connectCancelledByUser = false; + private boolean sessionRunning = false; + private boolean toggleMouseButtons = false; + + private LibFreeRDPBroadcastReceiver libFreeRDPBroadcastReceiver; + private ScrollView2D scrollView; + // keyboard visibility flags + private boolean sysKeyboardVisible = false; + private boolean extKeyboardVisible = false; + private int discardedMoveEvents = 0; + private ClipboardManagerProxy mClipboardManager; + private boolean callbackDialogResult; + View mDecor; + + private void createDialogs() + { + // build verify certificate dialog + dlgVerifyCertificate = + new AlertDialog.Builder(this) + .setTitle(R.string.dlg_title_verify_certificate) + .setPositiveButton(android.R.string.yes, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) + { + callbackDialogResult = true; + synchronized (dialog) + { + dialog.notify(); + } + } + }) + .setNegativeButton(android.R.string.no, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) + { + callbackDialogResult = false; + connectCancelledByUser = true; + synchronized (dialog) + { + dialog.notify(); + } + } + }) + .setCancelable(false) + .create(); + + // build the dialog + userCredView = getLayoutInflater().inflate(R.layout.credentials, null, true); + dlgUserCredentials = + new AlertDialog.Builder(this) + .setView(userCredView) + .setTitle(R.string.dlg_title_credentials) + .setPositiveButton(android.R.string.ok, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) + { + callbackDialogResult = true; + synchronized (dialog) + { + dialog.notify(); + } + } + }) + .setNegativeButton(android.R.string.cancel, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) + { + callbackDialogResult = false; + connectCancelledByUser = true; + synchronized (dialog) + { + dialog.notify(); + } + } + }) + .setCancelable(false) + .create(); + } + + private boolean hasHardwareMenuButton() + { + if (Build.VERSION.SDK_INT <= 10) + return true; + + if (Build.VERSION.SDK_INT >= 14) + { + boolean rc = false; + final ViewConfiguration cfg = ViewConfiguration.get(this); + + return cfg.hasPermanentMenuKey(); + } + + return false; + } + + @Override public void onCreate(Bundle savedInstanceState) + { + super.onCreate(savedInstanceState); + + // show status bar or make fullscreen? + if (ApplicationSettingsActivity.getHideStatusBar(this)) + { + getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, + WindowManager.LayoutParams.FLAG_FULLSCREEN); + } + + this.setContentView(R.layout.session); + if (hasHardwareMenuButton() || ApplicationSettingsActivity.getHideActionBar(this)) + { + this.getSupportActionBar().hide(); + } + else + this.getSupportActionBar().show(); + + Log.v(TAG, "Session.onCreate"); + + // ATTENTION: We use the onGlobalLayout notification to start our + // session. + // This is because only then we can know the exact size of our session + // when using fit screen + // accounting for any status bars etc. that Android might throws on us. + // A bit weird looking + // but this is the only way ... + final View activityRootView = findViewById(R.id.session_root_view); + activityRootView.getViewTreeObserver().addOnGlobalLayoutListener( + new OnGlobalLayoutListener() { + @Override public void onGlobalLayout() + { + screen_width = activityRootView.getWidth(); + screen_height = activityRootView.getHeight(); + + // start session + if (!sessionRunning && getIntent() != null) + { + processIntent(getIntent()); + sessionRunning = true; + } + } + }); + + sessionView = (SessionView)findViewById(R.id.sessionView); + sessionView.setScaleGestureDetector( + new ScaleGestureDetector(this, new PinchZoomListener())); + sessionView.setSessionViewListener(this); + sessionView.requestFocus(); + + touchPointerView = (TouchPointerView)findViewById(R.id.touchPointerView); + touchPointerView.setTouchPointerListener(this); + + keyboardMapper = new KeyboardMapper(); + keyboardMapper.init(this); + keyboardMapper.reset(this); + + modifiersKeyboard = new Keyboard(getApplicationContext(), R.xml.modifiers_keyboard); + specialkeysKeyboard = new Keyboard(getApplicationContext(), R.xml.specialkeys_keyboard); + numpadKeyboard = new Keyboard(getApplicationContext(), R.xml.numpad_keyboard); + cursorKeyboard = new Keyboard(getApplicationContext(), R.xml.cursor_keyboard); + + // hide keyboard below the sessionView + keyboardView = (KeyboardView)findViewById(R.id.extended_keyboard); + keyboardView.setKeyboard(specialkeysKeyboard); + keyboardView.setOnKeyboardActionListener(this); + + modifiersKeyboardView = (KeyboardView)findViewById(R.id.extended_keyboard_header); + modifiersKeyboardView.setKeyboard(modifiersKeyboard); + modifiersKeyboardView.setOnKeyboardActionListener(this); + + scrollView = (ScrollView2D)findViewById(R.id.sessionScrollView); + scrollView.setScrollViewListener(this); + uiHandler = new UIHandler(); + libFreeRDPBroadcastReceiver = new LibFreeRDPBroadcastReceiver(); + + zoomControls = (ZoomControls)findViewById(R.id.zoomControls); + zoomControls.hide(); + zoomControls.setOnZoomInClickListener(new View.OnClickListener() { + @Override public void onClick(View v) + { + resetZoomControlsAutoHideTimeout(); + zoomControls.setIsZoomInEnabled(sessionView.zoomIn(ZOOMING_STEP)); + zoomControls.setIsZoomOutEnabled(true); + } + }); + zoomControls.setOnZoomOutClickListener(new View.OnClickListener() { + @Override public void onClick(View v) + { + resetZoomControlsAutoHideTimeout(); + zoomControls.setIsZoomOutEnabled(sessionView.zoomOut(ZOOMING_STEP)); + zoomControls.setIsZoomInEnabled(true); + } + }); + + toggleMouseButtons = false; + + createDialogs(); + + // register freerdp events broadcast receiver + IntentFilter filter = new IntentFilter(); + filter.addAction(GlobalApp.ACTION_EVENT_FREERDP); + registerReceiver(libFreeRDPBroadcastReceiver, filter); + + mClipboardManager = ClipboardManagerProxy.getClipboardManager(this); + mClipboardManager.addClipboardChangedListener(this); + + mDecor = getWindow().getDecorView(); + mDecor.setSystemUiVisibility(View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | + View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY); + } + + @Override protected void onStart() + { + super.onStart(); + Log.v(TAG, "Session.onStart"); + } + + @Override protected void onRestart() + { + super.onRestart(); + Log.v(TAG, "Session.onRestart"); + } + + @Override protected void onResume() + { + super.onResume(); + Log.v(TAG, "Session.onResume"); + } + + @Override protected void onPause() + { + super.onPause(); + Log.v(TAG, "Session.onPause"); + + // hide any visible keyboards + showKeyboard(false, false); + } + + @Override protected void onStop() + { + super.onStop(); + Log.v(TAG, "Session.onStop"); + } + + @Override protected void onDestroy() + { + super.onDestroy(); + Log.v(TAG, "Session.onDestroy"); + + // Cancel running disconnect timers. + GlobalApp.cancelDisconnectTimer(); + + // Disconnect all remaining sessions. + Collection sessions = GlobalApp.getSessions(); + for (SessionState session : sessions) + LibFreeRDP.disconnect(session.getInstance()); + + // unregister freerdp events broadcast receiver + unregisterReceiver(libFreeRDPBroadcastReceiver); + + // remove clipboard listener + mClipboardManager.removeClipboardboardChangedListener(this); + + // free session + GlobalApp.freeSession(session.getInstance()); + + session = null; + } + + @Override public void onConfigurationChanged(Configuration newConfig) + { + super.onConfigurationChanged(newConfig); + + // reload keyboard resources (changed from landscape) + modifiersKeyboard = new Keyboard(getApplicationContext(), R.xml.modifiers_keyboard); + specialkeysKeyboard = new Keyboard(getApplicationContext(), R.xml.specialkeys_keyboard); + numpadKeyboard = new Keyboard(getApplicationContext(), R.xml.numpad_keyboard); + cursorKeyboard = new Keyboard(getApplicationContext(), R.xml.cursor_keyboard); + + // apply loaded keyboards + keyboardView.setKeyboard(specialkeysKeyboard); + modifiersKeyboardView.setKeyboard(modifiersKeyboard); + + mDecor.setSystemUiVisibility(View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | + View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY); + } + + private void processIntent(Intent intent) + { + // get either session instance or create one from a bookmark/uri + Bundle bundle = intent.getExtras(); + Uri openUri = intent.getData(); + if (openUri != null) + { + // Launched from URI, e.g: + // freerdp://user@ip:port/connect?sound=&rfx=&p=password&clipboard=%2b&themes=- + connect(openUri); + } + else if (bundle.containsKey(PARAM_INSTANCE)) + { + int inst = bundle.getInt(PARAM_INSTANCE); + session = GlobalApp.getSession(inst); + bitmap = session.getSurface().getBitmap(); + bindSession(); + } + else if (bundle.containsKey(PARAM_CONNECTION_REFERENCE)) + { + BookmarkBase bookmark = null; + String refStr = bundle.getString(PARAM_CONNECTION_REFERENCE); + if (ConnectionReference.isHostnameReference(refStr)) + { + bookmark = new ManualBookmark(); + bookmark.get().setHostname(ConnectionReference.getHostname(refStr)); + } + else if (ConnectionReference.isBookmarkReference(refStr)) + { + if (ConnectionReference.isManualBookmarkReference(refStr)) + bookmark = GlobalApp.getManualBookmarkGateway().findById( + ConnectionReference.getManualBookmarkId(refStr)); + else + assert false; + } + + if (bookmark != null) + connect(bookmark); + else + closeSessionActivity(RESULT_CANCELED); + } + else + { + // no session found - exit + closeSessionActivity(RESULT_CANCELED); + } + } + + private void connect(BookmarkBase bookmark) + { + session = GlobalApp.createSession(bookmark, getApplicationContext()); + + BookmarkBase.ScreenSettings screenSettings = + session.getBookmark().getActiveScreenSettings(); + Log.v(TAG, "Screen Resolution: " + screenSettings.getResolutionString()); + if (screenSettings.isAutomatic()) + { + if ((getResources().getConfiguration().screenLayout & + Configuration.SCREENLAYOUT_SIZE_MASK) >= Configuration.SCREENLAYOUT_SIZE_LARGE) + { + // large screen device i.e. tablet: simply use screen info + screenSettings.setHeight(screen_height); + screenSettings.setWidth(screen_width); + } + else + { + // small screen device i.e. phone: + // Automatic uses the largest side length of the screen and + // makes a 16:10 resolution setting out of it + int screenMax = (screen_width > screen_height) ? screen_width : screen_height; + screenSettings.setHeight(screenMax); + screenSettings.setWidth((int)((float)screenMax * 1.6f)); + } + } + if (screenSettings.isFitScreen()) + { + screenSettings.setHeight(screen_height); + screenSettings.setWidth(screen_width); + } + + connectWithTitle(bookmark.getLabel()); + } + + private void connect(Uri openUri) + { + session = GlobalApp.createSession(openUri, getApplicationContext()); + + connectWithTitle(openUri.getAuthority()); + } + + private void connectWithTitle(String title) + { + session.setUIEventListener(this); + + progressDialog = new ProgressDialog(this); + progressDialog.setTitle(title); + progressDialog.setMessage(getResources().getText(R.string.dlg_msg_connecting)); + progressDialog.setButton( + ProgressDialog.BUTTON_NEGATIVE, "Cancel", new DialogInterface.OnClickListener() { + @Override public void onClick(DialogInterface dialog, int which) + { + connectCancelledByUser = true; + LibFreeRDP.cancelConnection(session.getInstance()); + } + }); + progressDialog.setCancelable(false); + progressDialog.show(); + + Thread thread = new Thread(new Runnable() { + public void run() + { + session.connect(getApplicationContext()); + } + }); + thread.start(); + } + + // binds the current session to the activity by wiring it up with the + // sessionView and updating all internal objects accordingly + private void bindSession() + { + Log.v(TAG, "bindSession called"); + session.setUIEventListener(this); + sessionView.onSurfaceChange(session); + scrollView.requestLayout(); + keyboardMapper.reset(this); + mDecor.setSystemUiVisibility(View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | + View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY); + } + + private void hideSoftInput() + { + InputMethodManager mgr = (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE); + + if (mgr.isActive()) + { + mgr.toggleSoftInput(InputMethodManager.HIDE_NOT_ALWAYS, 0); + } + else + { + mgr.toggleSoftInput(0, InputMethodManager.HIDE_NOT_ALWAYS); + } + } + + // displays either the system or the extended keyboard or non of them + private void showKeyboard(final boolean showSystemKeyboard, final boolean showExtendedKeyboard) + { + // no matter what we are doing ... hide the zoom controls + // TODO: this is not working correctly as hiding the keyboard issues a + // onScrollChange notification showing the control again ... + uiHandler.removeMessages(UIHandler.HIDE_ZOOMCONTROLS); + if (zoomControls.getVisibility() == View.VISIBLE) + zoomControls.hide(); + + InputMethodManager mgr = (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE); + + if (showSystemKeyboard) + { + // hide extended keyboard + keyboardView.setVisibility(View.GONE); + // show system keyboard + mgr.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0); + + // show modifiers keyboard + modifiersKeyboardView.setVisibility(View.VISIBLE); + } + else if (showExtendedKeyboard) + { + // hide system keyboard + hideSoftInput(); + + // show extended keyboard + keyboardView.setKeyboard(specialkeysKeyboard); + keyboardView.setVisibility(View.VISIBLE); + modifiersKeyboardView.setVisibility(View.VISIBLE); + } + else + { + // hide both + hideSoftInput(); + keyboardView.setVisibility(View.GONE); + modifiersKeyboardView.setVisibility(View.GONE); + + // clear any active key modifiers) + keyboardMapper.clearlAllModifiers(); + } + + sysKeyboardVisible = showSystemKeyboard; + extKeyboardVisible = showExtendedKeyboard; + } + + private void closeSessionActivity(int resultCode) + { + // Go back to home activity (and send intent data back to home) + setResult(resultCode, getIntent()); + finish(); + } + + // update the state of our modifier keys + private void updateModifierKeyStates() + { + // check if any key is in the keycodes list + + List keys = modifiersKeyboard.getKeys(); + for (Iterator it = keys.iterator(); it.hasNext();) + { + // if the key is a sticky key - just set it to off + Keyboard.Key curKey = it.next(); + if (curKey.sticky) + { + switch (keyboardMapper.getModifierState(curKey.codes[0])) + { + case KeyboardMapper.KEYSTATE_ON: + curKey.on = true; + curKey.pressed = false; + break; + + case KeyboardMapper.KEYSTATE_OFF: + curKey.on = false; + curKey.pressed = false; + break; + + case KeyboardMapper.KEYSTATE_LOCKED: + curKey.on = true; + curKey.pressed = true; + break; + } + } + } + + // refresh image + modifiersKeyboardView.invalidateAllKeys(); + } + + private void sendDelayedMoveEvent(int x, int y) + { + if (uiHandler.hasMessages(UIHandler.SEND_MOVE_EVENT)) + { + uiHandler.removeMessages(UIHandler.SEND_MOVE_EVENT); + discardedMoveEvents++; + } + else + discardedMoveEvents = 0; + + if (discardedMoveEvents > MAX_DISCARDED_MOVE_EVENTS) + LibFreeRDP.sendCursorEvent(session.getInstance(), x, y, Mouse.getMoveEvent()); + else + uiHandler.sendMessageDelayed(Message.obtain(null, UIHandler.SEND_MOVE_EVENT, x, y), + SEND_MOVE_EVENT_TIMEOUT); + } + + private void cancelDelayedMoveEvent() + { + uiHandler.removeMessages(UIHandler.SEND_MOVE_EVENT); + } + + @Override public boolean onCreateOptionsMenu(Menu menu) + { + getMenuInflater().inflate(R.menu.session_menu, menu); + return true; + } + + @Override public boolean onOptionsItemSelected(MenuItem item) + { + // refer to http://tools.android.com/tips/non-constant-fields why we + // can't use switch/case here .. + int itemId = item.getItemId(); + + if (itemId == R.id.session_touch_pointer) + { + // toggle touch pointer + if (touchPointerView.getVisibility() == View.VISIBLE) + { + touchPointerView.setVisibility(View.INVISIBLE); + sessionView.setTouchPointerPadding(0, 0); + } + else + { + touchPointerView.setVisibility(View.VISIBLE); + sessionView.setTouchPointerPadding(touchPointerView.getPointerWidth(), + touchPointerView.getPointerHeight()); + } + } + else if (itemId == R.id.session_sys_keyboard) + { + showKeyboard(!sysKeyboardVisible, false); + } + else if (itemId == R.id.session_ext_keyboard) + { + showKeyboard(false, !extKeyboardVisible); + } + else if (itemId == R.id.session_disconnect) + { + showKeyboard(false, false); + LibFreeRDP.disconnect(session.getInstance()); + } + + return true; + } + + @Override public void onBackPressed() + { + // hide keyboards (if any visible) or send alt+f4 to the session + if (sysKeyboardVisible || extKeyboardVisible) + showKeyboard(false, false); + else + keyboardMapper.sendAltF4(); + } + + @Override public boolean onKeyLongPress(int keyCode, KeyEvent event) + { + if (keyCode == KeyEvent.KEYCODE_BACK) + { + LibFreeRDP.disconnect(session.getInstance()); + return true; + } + return super.onKeyLongPress(keyCode, event); + } + + // android keyboard input handling + // We always use the unicode value to process input from the android + // keyboard except if key modifiers + // (like Win, Alt, Ctrl) are activated. In this case we will send the + // virtual key code to allow key + // combinations (like Win + E to open the explorer). + @Override public boolean onKeyDown(int keycode, KeyEvent event) + { + return keyboardMapper.processAndroidKeyEvent(event); + } + + @Override public boolean onKeyUp(int keycode, KeyEvent event) + { + return keyboardMapper.processAndroidKeyEvent(event); + } + + // onKeyMultiple is called for input of some special characters like umlauts + // and some symbol characters + @Override public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) + { + return keyboardMapper.processAndroidKeyEvent(event); + } + + // **************************************************************************** + // KeyboardView.KeyboardActionEventListener + @Override public void onKey(int primaryCode, int[] keyCodes) + { + keyboardMapper.processCustomKeyEvent(primaryCode); + } + + @Override public void onText(CharSequence text) + { + } + + @Override public void swipeRight() + { + } + + @Override public void swipeLeft() + { + } + + @Override public void swipeDown() + { + } + + @Override public void swipeUp() + { + } + + @Override public void onPress(int primaryCode) + { + } + + @Override public void onRelease(int primaryCode) + { + } + + // **************************************************************************** + // KeyboardMapper.KeyProcessingListener implementation + @Override public void processVirtualKey(int virtualKeyCode, boolean down) + { + LibFreeRDP.sendKeyEvent(session.getInstance(), virtualKeyCode, down); + } + + @Override public void processUnicodeKey(int unicodeKey) + { + LibFreeRDP.sendUnicodeKeyEvent(session.getInstance(), unicodeKey, true); + LibFreeRDP.sendUnicodeKeyEvent(session.getInstance(), unicodeKey, false); + } + + @Override public void switchKeyboard(int keyboardType) + { + switch (keyboardType) + { + case KeyboardMapper.KEYBOARD_TYPE_FUNCTIONKEYS: + keyboardView.setKeyboard(specialkeysKeyboard); + break; + + case KeyboardMapper.KEYBOARD_TYPE_NUMPAD: + keyboardView.setKeyboard(numpadKeyboard); + break; + + case KeyboardMapper.KEYBOARD_TYPE_CURSOR: + keyboardView.setKeyboard(cursorKeyboard); + break; + + default: + break; + } + } + + @Override public void modifiersChanged() + { + updateModifierKeyStates(); + } + + // **************************************************************************** + // LibFreeRDP UI event listener implementation + @Override public void OnSettingsChanged(int width, int height, int bpp) + { + + if (bpp > 16) + bitmap = Bitmap.createBitmap(width, height, Config.ARGB_8888); + else + bitmap = Bitmap.createBitmap(width, height, Config.RGB_565); + + session.setSurface(new BitmapDrawable(bitmap)); + + if (session.getBookmark() == null) + { + // Return immediately if we launch from URI + return; + } + + // check this settings and initial settings - if they are not equal the + // server doesn't support our settings + // FIXME: the additional check (settings.getWidth() != width + 1) is for + // the RDVH bug fix to avoid accidental notifications + // (refer to android_freerdp.c for more info on this problem) + BookmarkBase.ScreenSettings settings = session.getBookmark().getActiveScreenSettings(); + if ((settings.getWidth() != width && settings.getWidth() != width + 1) || + settings.getHeight() != height || settings.getColors() != bpp) + uiHandler.sendMessage( + Message.obtain(null, UIHandler.DISPLAY_TOAST, + getResources().getText(R.string.info_capabilities_changed))); + } + + @Override public void OnGraphicsUpdate(int x, int y, int width, int height) + { + LibFreeRDP.updateGraphics(session.getInstance(), bitmap, x, y, width, height); + + sessionView.addInvalidRegion(new Rect(x, y, x + width, y + height)); + + /* + * since sessionView can only be modified from the UI thread any + * modifications to it need to be scheduled + */ + + uiHandler.sendEmptyMessage(UIHandler.REFRESH_SESSIONVIEW); + } + + @Override public void OnGraphicsResize(int width, int height, int bpp) + { + // replace bitmap + if (bpp > 16) + bitmap = Bitmap.createBitmap(width, height, Config.ARGB_8888); + else + bitmap = Bitmap.createBitmap(width, height, Config.RGB_565); + session.setSurface(new BitmapDrawable(bitmap)); + + /* + * since sessionView can only be modified from the UI thread any + * modifications to it need to be scheduled + */ + uiHandler.sendEmptyMessage(UIHandler.GRAPHICS_CHANGED); + } + + @Override + public boolean OnAuthenticate(StringBuilder username, StringBuilder domain, + StringBuilder password) + { + // this is where the return code of our dialog will be stored + callbackDialogResult = false; + + // set text fields + ((EditText)userCredView.findViewById(R.id.editTextUsername)).setText(username); + ((EditText)userCredView.findViewById(R.id.editTextDomain)).setText(domain); + ((EditText)userCredView.findViewById(R.id.editTextPassword)).setText(password); + + // start dialog in UI thread + uiHandler.sendMessage(Message.obtain(null, UIHandler.SHOW_DIALOG, dlgUserCredentials)); + + // wait for result + try + { + synchronized (dlgUserCredentials) + { + dlgUserCredentials.wait(); + } + } + catch (InterruptedException e) + { + } + + // clear buffers + username.setLength(0); + domain.setLength(0); + password.setLength(0); + + // read back user credentials + username.append( + ((EditText)userCredView.findViewById(R.id.editTextUsername)).getText().toString()); + domain.append( + ((EditText)userCredView.findViewById(R.id.editTextDomain)).getText().toString()); + password.append( + ((EditText)userCredView.findViewById(R.id.editTextPassword)).getText().toString()); + + return callbackDialogResult; + } + + @Override + public boolean OnGatewayAuthenticate(StringBuilder username, StringBuilder domain, + StringBuilder password) + { + // this is where the return code of our dialog will be stored + callbackDialogResult = false; + + // set text fields + ((EditText)userCredView.findViewById(R.id.editTextUsername)).setText(username); + ((EditText)userCredView.findViewById(R.id.editTextDomain)).setText(domain); + ((EditText)userCredView.findViewById(R.id.editTextPassword)).setText(password); + + // start dialog in UI thread + uiHandler.sendMessage(Message.obtain(null, UIHandler.SHOW_DIALOG, dlgUserCredentials)); + + // wait for result + try + { + synchronized (dlgUserCredentials) + { + dlgUserCredentials.wait(); + } + } + catch (InterruptedException e) + { + } + + // clear buffers + username.setLength(0); + domain.setLength(0); + password.setLength(0); + + // read back user credentials + username.append( + ((EditText)userCredView.findViewById(R.id.editTextUsername)).getText().toString()); + domain.append( + ((EditText)userCredView.findViewById(R.id.editTextDomain)).getText().toString()); + password.append( + ((EditText)userCredView.findViewById(R.id.editTextPassword)).getText().toString()); + + return callbackDialogResult; + } + + @Override + public int OnVerifiyCertificate(String commonName, String subject, String issuer, + String fingerprint, boolean mismatch) + { + // see if global settings says accept all + if (ApplicationSettingsActivity.getAcceptAllCertificates(this)) + return 0; + + // this is where the return code of our dialog will be stored + callbackDialogResult = false; + + // set message + String msg = getResources().getString(R.string.dlg_msg_verify_certificate); + msg = msg + "\n\nSubject: " + subject + "\nIssuer: " + issuer + + "\nFingerprint: " + fingerprint; + dlgVerifyCertificate.setMessage(msg); + + // start dialog in UI thread + uiHandler.sendMessage(Message.obtain(null, UIHandler.SHOW_DIALOG, dlgVerifyCertificate)); + + // wait for result + try + { + synchronized (dlgVerifyCertificate) + { + dlgVerifyCertificate.wait(); + } + } + catch (InterruptedException e) + { + } + + return callbackDialogResult ? 1 : 0; + } + + @Override + public int OnVerifyChangedCertificate(String commonName, String subject, String issuer, + String fingerprint, String oldSubject, String oldIssuer, + String oldFingerprint) + { + // see if global settings says accept all + if (ApplicationSettingsActivity.getAcceptAllCertificates(this)) + return 0; + + // this is where the return code of our dialog will be stored + callbackDialogResult = false; + + // set message + String msg = getResources().getString(R.string.dlg_msg_verify_certificate); + msg = msg + "\n\nSubject: " + subject + "\nIssuer: " + issuer + + "\nFingerprint: " + fingerprint; + dlgVerifyCertificate.setMessage(msg); + + // start dialog in UI thread + uiHandler.sendMessage(Message.obtain(null, UIHandler.SHOW_DIALOG, dlgVerifyCertificate)); + + // wait for result + try + { + synchronized (dlgVerifyCertificate) + { + dlgVerifyCertificate.wait(); + } + } + catch (InterruptedException e) + { + } + + return callbackDialogResult ? 1 : 0; + } + + @Override public void OnRemoteClipboardChanged(String data) + { + Log.v(TAG, "OnRemoteClipboardChanged: " + data); + mClipboardManager.setClipboardData(data); + } + + // **************************************************************************** + // ScrollView2DListener implementation + private void resetZoomControlsAutoHideTimeout() + { + uiHandler.removeMessages(UIHandler.HIDE_ZOOMCONTROLS); + uiHandler.sendEmptyMessageDelayed(UIHandler.HIDE_ZOOMCONTROLS, + ZOOMCONTROLS_AUTOHIDE_TIMEOUT); + } + + @Override public void onScrollChanged(ScrollView2D scrollView, int x, int y, int oldx, int oldy) + { + zoomControls.setIsZoomInEnabled(!sessionView.isAtMaxZoom()); + zoomControls.setIsZoomOutEnabled(!sessionView.isAtMinZoom()); + if (!ApplicationSettingsActivity.getHideZoomControls(this) && + zoomControls.getVisibility() != View.VISIBLE) + zoomControls.show(); + resetZoomControlsAutoHideTimeout(); + } + + // **************************************************************************** + // SessionView.SessionViewListener + @Override public void onSessionViewBeginTouch() + { + scrollView.setScrollEnabled(false); + } + + @Override public void onSessionViewEndTouch() + { + scrollView.setScrollEnabled(true); + } + + @Override public void onSessionViewLeftTouch(int x, int y, boolean down) + { + if (!down) + cancelDelayedMoveEvent(); + + LibFreeRDP.sendCursorEvent(session.getInstance(), x, y, + toggleMouseButtons ? Mouse.getRightButtonEvent(this, down) + : Mouse.getLeftButtonEvent(this, down)); + + if (!down) + toggleMouseButtons = false; + } + + public void onSessionViewRightTouch(int x, int y, boolean down) + { + if (!down) + toggleMouseButtons = !toggleMouseButtons; + } + + @Override public void onSessionViewMove(int x, int y) + { + sendDelayedMoveEvent(x, y); + } + + @Override public void onSessionViewScroll(boolean down) + { + LibFreeRDP.sendCursorEvent(session.getInstance(), 0, 0, Mouse.getScrollEvent(this, down)); + } + + // **************************************************************************** + // TouchPointerView.TouchPointerListener + @Override public void onTouchPointerClose() + { + touchPointerView.setVisibility(View.INVISIBLE); + sessionView.setTouchPointerPadding(0, 0); + } + + private Point mapScreenCoordToSessionCoord(int x, int y) + { + int mappedX = (int)((float)(x + scrollView.getScrollX()) / sessionView.getZoom()); + int mappedY = (int)((float)(y + scrollView.getScrollY()) / sessionView.getZoom()); + if (mappedX > bitmap.getWidth()) + mappedX = bitmap.getWidth(); + if (mappedY > bitmap.getHeight()) + mappedY = bitmap.getHeight(); + return new Point(mappedX, mappedY); + } + + @Override public void onTouchPointerLeftClick(int x, int y, boolean down) + { + Point p = mapScreenCoordToSessionCoord(x, y); + LibFreeRDP.sendCursorEvent(session.getInstance(), p.x, p.y, + Mouse.getLeftButtonEvent(this, down)); + } + + @Override public void onTouchPointerRightClick(int x, int y, boolean down) + { + Point p = mapScreenCoordToSessionCoord(x, y); + LibFreeRDP.sendCursorEvent(session.getInstance(), p.x, p.y, + Mouse.getRightButtonEvent(this, down)); + } + + @Override public void onTouchPointerMove(int x, int y) + { + Point p = mapScreenCoordToSessionCoord(x, y); + LibFreeRDP.sendCursorEvent(session.getInstance(), p.x, p.y, Mouse.getMoveEvent()); + + if (ApplicationSettingsActivity.getAutoScrollTouchPointer(this) && + !uiHandler.hasMessages(UIHandler.SCROLLING_REQUESTED)) + { + Log.v(TAG, "Starting auto-scroll"); + uiHandler.sendEmptyMessageDelayed(UIHandler.SCROLLING_REQUESTED, SCROLLING_TIMEOUT); + } + } + + @Override public void onTouchPointerScroll(boolean down) + { + LibFreeRDP.sendCursorEvent(session.getInstance(), 0, 0, Mouse.getScrollEvent(this, down)); + } + + @Override public void onTouchPointerToggleKeyboard() + { + showKeyboard(!sysKeyboardVisible, false); + } + + @Override public void onTouchPointerToggleExtKeyboard() + { + showKeyboard(false, !extKeyboardVisible); + } + + @Override public void onTouchPointerResetScrollZoom() + { + sessionView.setZoom(1.0f); + scrollView.scrollTo(0, 0); + } + + @Override public boolean onGenericMotionEvent(MotionEvent e) + { + super.onGenericMotionEvent(e); + switch (e.getAction()) + { + case MotionEvent.ACTION_SCROLL: + final float vScroll = e.getAxisValue(MotionEvent.AXIS_VSCROLL); + if (vScroll < 0) + { + LibFreeRDP.sendCursorEvent(session.getInstance(), 0, 0, + Mouse.getScrollEvent(this, false)); + } + if (vScroll > 0) + { + LibFreeRDP.sendCursorEvent(session.getInstance(), 0, 0, + Mouse.getScrollEvent(this, true)); + } + break; + } + return true; + } + + // **************************************************************************** + // ClipboardManagerProxy.OnClipboardChangedListener + @Override public void onClipboardChanged(String data) + { + Log.v(TAG, "onClipboardChanged: " + data); + LibFreeRDP.sendClipboardData(session.getInstance(), data); + } + + private class UIHandler extends Handler + { + + public static final int REFRESH_SESSIONVIEW = 1; + public static final int DISPLAY_TOAST = 2; + public static final int HIDE_ZOOMCONTROLS = 3; + public static final int SEND_MOVE_EVENT = 4; + public static final int SHOW_DIALOG = 5; + public static final int GRAPHICS_CHANGED = 6; + public static final int SCROLLING_REQUESTED = 7; + + UIHandler() + { + super(); + } + + @Override public void handleMessage(Message msg) + { + switch (msg.what) + { + case GRAPHICS_CHANGED: + { + sessionView.onSurfaceChange(session); + scrollView.requestLayout(); + break; + } + case REFRESH_SESSIONVIEW: + { + sessionView.invalidateRegion(); + break; + } + case DISPLAY_TOAST: + { + Toast errorToast = Toast.makeText(getApplicationContext(), msg.obj.toString(), + Toast.LENGTH_LONG); + errorToast.show(); + break; + } + case HIDE_ZOOMCONTROLS: + { + zoomControls.hide(); + break; + } + case SEND_MOVE_EVENT: + { + LibFreeRDP.sendCursorEvent(session.getInstance(), msg.arg1, msg.arg2, + Mouse.getMoveEvent()); + break; + } + case SHOW_DIALOG: + { + // create and show the dialog + ((Dialog)msg.obj).show(); + break; + } + case SCROLLING_REQUESTED: + { + int scrollX = 0; + int scrollY = 0; + float[] pointerPos = touchPointerView.getPointerPosition(); + + if (pointerPos[0] > (screen_width - touchPointerView.getPointerWidth())) + scrollX = SCROLLING_DISTANCE; + else if (pointerPos[0] < 0) + scrollX = -SCROLLING_DISTANCE; + + if (pointerPos[1] > (screen_height - touchPointerView.getPointerHeight())) + scrollY = SCROLLING_DISTANCE; + else if (pointerPos[1] < 0) + scrollY = -SCROLLING_DISTANCE; + + scrollView.scrollBy(scrollX, scrollY); + + // see if we reached the min/max scroll positions + if (scrollView.getScrollX() == 0 || + scrollView.getScrollX() == (sessionView.getWidth() - scrollView.getWidth())) + scrollX = 0; + if (scrollView.getScrollY() == 0 || + scrollView.getScrollY() == + (sessionView.getHeight() - scrollView.getHeight())) + scrollY = 0; + + if (scrollX != 0 || scrollY != 0) + uiHandler.sendEmptyMessageDelayed(SCROLLING_REQUESTED, SCROLLING_TIMEOUT); + else + Log.v(TAG, "Stopping auto-scroll"); + break; + } + } + } + } + + private class PinchZoomListener extends ScaleGestureDetector.SimpleOnScaleGestureListener + { + private float scaleFactor = 1.0f; + + @Override public boolean onScaleBegin(ScaleGestureDetector detector) + { + scrollView.setScrollEnabled(false); + return true; + } + + @Override public boolean onScale(ScaleGestureDetector detector) + { + + // calc scale factor + scaleFactor *= detector.getScaleFactor(); + scaleFactor = Math.max(SessionView.MIN_SCALE_FACTOR, + Math.min(scaleFactor, SessionView.MAX_SCALE_FACTOR)); + sessionView.setZoom(scaleFactor); + + if (!sessionView.isAtMinZoom() && !sessionView.isAtMaxZoom()) + { + // transform scroll origin to the new zoom space + float transOriginX = scrollView.getScrollX() * detector.getScaleFactor(); + float transOriginY = scrollView.getScrollY() * detector.getScaleFactor(); + + // transform center point to the zoomed space + float transCenterX = + (scrollView.getScrollX() + detector.getFocusX()) * detector.getScaleFactor(); + float transCenterY = + (scrollView.getScrollY() + detector.getFocusY()) * detector.getScaleFactor(); + + // scroll by the difference between the distance of the + // transformed center/origin point and their old distance + // (focusX/Y) + scrollView.scrollBy((int)((transCenterX - transOriginX) - detector.getFocusX()), + (int)((transCenterY - transOriginY) - detector.getFocusY())); + } + + return true; + } + + @Override public void onScaleEnd(ScaleGestureDetector de) + { + scrollView.setScrollEnabled(true); + } + } + + private class LibFreeRDPBroadcastReceiver extends BroadcastReceiver + { + @Override public void onReceive(Context context, Intent intent) + { + // still got a valid session? + if (session == null) + return; + + // is this event for the current session? + if (session.getInstance() != intent.getExtras().getLong(GlobalApp.EVENT_PARAM, -1)) + return; + + switch (intent.getExtras().getInt(GlobalApp.EVENT_TYPE, -1)) + { + case GlobalApp.FREERDP_EVENT_CONNECTION_SUCCESS: + OnConnectionSuccess(context); + break; + + case GlobalApp.FREERDP_EVENT_CONNECTION_FAILURE: + OnConnectionFailure(context); + break; + case GlobalApp.FREERDP_EVENT_DISCONNECTED: + OnDisconnected(context); + break; + } + } + + private void OnConnectionSuccess(Context context) + { + Log.v(TAG, "OnConnectionSuccess"); + + // bind session + bindSession(); + + if (progressDialog != null) + { + progressDialog.dismiss(); + progressDialog = null; + } + + if (session.getBookmark() == null) + { + // Return immediately if we launch from URI + return; + } + + // add hostname to history if quick connect was used + Bundle bundle = getIntent().getExtras(); + if (bundle != null && bundle.containsKey(PARAM_CONNECTION_REFERENCE)) + { + if (ConnectionReference.isHostnameReference( + bundle.getString(PARAM_CONNECTION_REFERENCE))) + { + assert session.getBookmark().getType() == BookmarkBase.TYPE_MANUAL; + String item = session.getBookmark().get().getHostname(); + if (!GlobalApp.getQuickConnectHistoryGateway().historyItemExists(item)) + GlobalApp.getQuickConnectHistoryGateway().addHistoryItem(item); + } + } + } + + private void OnConnectionFailure(Context context) + { + Log.v(TAG, "OnConnectionFailure"); + + // remove pending move events + uiHandler.removeMessages(UIHandler.SEND_MOVE_EVENT); + + if (progressDialog != null) + { + progressDialog.dismiss(); + progressDialog = null; + } + + // post error message on UI thread + if (!connectCancelledByUser) + uiHandler.sendMessage( + Message.obtain(null, UIHandler.DISPLAY_TOAST, + getResources().getText(R.string.error_connection_failure))); + + closeSessionActivity(RESULT_CANCELED); + } + + private void OnDisconnected(Context context) + { + Log.v(TAG, "OnDisconnected"); + + // remove pending move events + uiHandler.removeMessages(UIHandler.SEND_MOVE_EVENT); + + if (progressDialog != null) + { + progressDialog.dismiss(); + progressDialog = null; + } + + session.setUIEventListener(null); + closeSessionActivity(RESULT_OK); + } + } +} diff --git a/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/presentation/SessionView.java b/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/presentation/SessionView.java new file mode 100644 index 0000000..fd4e02b --- /dev/null +++ b/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/presentation/SessionView.java @@ -0,0 +1,411 @@ +/* + Android Session view + + Copyright 2013 Thincast Technologies GmbH, Author: Martin Fleisz + + This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. + If a copy of the MPL was not distributed with this file, You can obtain one at + http://mozilla.org/MPL/2.0/. +*/ + +package com.freerdp.freerdpcore.presentation; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Matrix; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.drawable.BitmapDrawable; +import android.util.AttributeSet; +import android.util.Log; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.ScaleGestureDetector; +import android.view.View; + +import com.freerdp.freerdpcore.application.SessionState; +import com.freerdp.freerdpcore.utils.DoubleGestureDetector; +import com.freerdp.freerdpcore.utils.GestureDetector; + +import java.util.Stack; + +public class SessionView extends View +{ + public static final float MAX_SCALE_FACTOR = 3.0f; + public static final float MIN_SCALE_FACTOR = 1.0f; + private static final String TAG = "SessionView"; + private static final float SCALE_FACTOR_DELTA = 0.0001f; + private static final float TOUCH_SCROLL_DELTA = 10.0f; + private int width; + private int height; + private BitmapDrawable surface; + private Stack invalidRegions; + private int touchPointerPaddingWidth = 0; + private int touchPointerPaddingHeight = 0; + private SessionViewListener sessionViewListener = null; + // helpers for scaling gesture handling + private float scaleFactor = 1.0f; + private Matrix scaleMatrix; + private Matrix invScaleMatrix; + private RectF invalidRegionF; + private GestureDetector gestureDetector; + private SessionState currentSession; + + // private static final String TAG = "FreeRDP.SessionView"; + private DoubleGestureDetector doubleGestureDetector; + public SessionView(Context context) + { + super(context); + initSessionView(context); + } + + public SessionView(Context context, AttributeSet attrs) + { + super(context, attrs); + initSessionView(context); + } + + public SessionView(Context context, AttributeSet attrs, int defStyle) + { + super(context, attrs, defStyle); + initSessionView(context); + } + + private void initSessionView(Context context) + { + invalidRegions = new Stack(); + gestureDetector = new GestureDetector(context, new SessionGestureListener(), null, true); + doubleGestureDetector = + new DoubleGestureDetector(context, null, new SessionDoubleGestureListener()); + + scaleFactor = 1.0f; + scaleMatrix = new Matrix(); + invScaleMatrix = new Matrix(); + invalidRegionF = new RectF(); + + setSystemUiVisibility(View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | + View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY); + } + + public void setScaleGestureDetector(ScaleGestureDetector scaleGestureDetector) + { + doubleGestureDetector.setScaleGestureDetector(scaleGestureDetector); + } + + public void setSessionViewListener(SessionViewListener sessionViewListener) + { + this.sessionViewListener = sessionViewListener; + } + + public void addInvalidRegion(Rect invalidRegion) + { + // correctly transform invalid region depending on current scaling + invalidRegionF.set(invalidRegion); + scaleMatrix.mapRect(invalidRegionF); + invalidRegionF.roundOut(invalidRegion); + + invalidRegions.add(invalidRegion); + } + + public void invalidateRegion() + { + invalidate(invalidRegions.pop()); + } + + public void onSurfaceChange(SessionState session) + { + surface = session.getSurface(); + Bitmap bitmap = surface.getBitmap(); + width = bitmap.getWidth(); + height = bitmap.getHeight(); + surface.setBounds(0, 0, width, height); + + setMinimumWidth(width); + setMinimumHeight(height); + + requestLayout(); + currentSession = session; + } + + public float getZoom() + { + return scaleFactor; + } + + public void setZoom(float factor) + { + // calc scale matrix and inverse scale matrix (to correctly transform the view and moues + // coordinates) + scaleFactor = factor; + scaleMatrix.setScale(scaleFactor, scaleFactor); + invScaleMatrix.setScale(1.0f / scaleFactor, 1.0f / scaleFactor); + + // update layout + requestLayout(); + } + + public boolean isAtMaxZoom() + { + return (scaleFactor > (MAX_SCALE_FACTOR - SCALE_FACTOR_DELTA)); + } + + public boolean isAtMinZoom() + { + return (scaleFactor < (MIN_SCALE_FACTOR + SCALE_FACTOR_DELTA)); + } + + public boolean zoomIn(float factor) + { + boolean res = true; + scaleFactor += factor; + if (scaleFactor > (MAX_SCALE_FACTOR - SCALE_FACTOR_DELTA)) + { + scaleFactor = MAX_SCALE_FACTOR; + res = false; + } + setZoom(scaleFactor); + return res; + } + + public boolean zoomOut(float factor) + { + boolean res = true; + scaleFactor -= factor; + if (scaleFactor < (MIN_SCALE_FACTOR + SCALE_FACTOR_DELTA)) + { + scaleFactor = MIN_SCALE_FACTOR; + res = false; + } + setZoom(scaleFactor); + return res; + } + + public void setTouchPointerPadding(int widht, int height) + { + touchPointerPaddingWidth = widht; + touchPointerPaddingHeight = height; + requestLayout(); + } + + public int getTouchPointerPaddingWidth() + { + return touchPointerPaddingWidth; + } + + public int getTouchPointerPaddingHeight() + { + return touchPointerPaddingHeight; + } + + @Override public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) + { + Log.v(TAG, width + "x" + height); + this.setMeasuredDimension((int)(width * scaleFactor) + touchPointerPaddingWidth, + (int)(height * scaleFactor) + touchPointerPaddingHeight); + } + + @Override public void onDraw(Canvas canvas) + { + super.onDraw(canvas); + + canvas.save(); + canvas.concat(scaleMatrix); + canvas.drawColor(Color.BLACK); + surface.draw(canvas); + canvas.restore(); + } + + // dirty hack: we call back to our activity and call onBackPressed as this doesn't reach us when + // the soft keyboard is shown ... + @Override public boolean dispatchKeyEventPreIme(KeyEvent event) + { + if (event.getKeyCode() == KeyEvent.KEYCODE_BACK && + event.getAction() == KeyEvent.ACTION_DOWN) + ((SessionActivity)this.getContext()).onBackPressed(); + return super.dispatchKeyEventPreIme(event); + } + + // perform mapping on the touch event's coordinates according to the current scaling + private MotionEvent mapTouchEvent(MotionEvent event) + { + MotionEvent mappedEvent = MotionEvent.obtain(event); + float[] coordinates = { mappedEvent.getX(), mappedEvent.getY() }; + invScaleMatrix.mapPoints(coordinates); + mappedEvent.setLocation(coordinates[0], coordinates[1]); + return mappedEvent; + } + + // perform mapping on the double touch event's coordinates according to the current scaling + private MotionEvent mapDoubleTouchEvent(MotionEvent event) + { + MotionEvent mappedEvent = MotionEvent.obtain(event); + float[] coordinates = { (mappedEvent.getX(0) + mappedEvent.getX(1)) / 2, + (mappedEvent.getY(0) + mappedEvent.getY(1)) / 2 }; + invScaleMatrix.mapPoints(coordinates); + mappedEvent.setLocation(coordinates[0], coordinates[1]); + return mappedEvent; + } + + @Override public boolean onTouchEvent(MotionEvent event) + { + boolean res = gestureDetector.onTouchEvent(event); + res |= doubleGestureDetector.onTouchEvent(event); + return res; + } + + public interface SessionViewListener { + abstract void onSessionViewBeginTouch(); + + abstract void onSessionViewEndTouch(); + + abstract void onSessionViewLeftTouch(int x, int y, boolean down); + + abstract void onSessionViewRightTouch(int x, int y, boolean down); + + abstract void onSessionViewMove(int x, int y); + + abstract void onSessionViewScroll(boolean down); + } + + private class SessionGestureListener extends GestureDetector.SimpleOnGestureListener + { + boolean longPressInProgress = false; + + public boolean onDown(MotionEvent e) + { + return true; + } + + public boolean onUp(MotionEvent e) + { + sessionViewListener.onSessionViewEndTouch(); + return true; + } + + public void onLongPress(MotionEvent e) + { + MotionEvent mappedEvent = mapTouchEvent(e); + sessionViewListener.onSessionViewBeginTouch(); + sessionViewListener.onSessionViewLeftTouch((int)mappedEvent.getX(), + (int)mappedEvent.getY(), true); + longPressInProgress = true; + } + + public void onLongPressUp(MotionEvent e) + { + MotionEvent mappedEvent = mapTouchEvent(e); + sessionViewListener.onSessionViewLeftTouch((int)mappedEvent.getX(), + (int)mappedEvent.getY(), false); + longPressInProgress = false; + sessionViewListener.onSessionViewEndTouch(); + } + + public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) + { + if (longPressInProgress) + { + MotionEvent mappedEvent = mapTouchEvent(e2); + sessionViewListener.onSessionViewMove((int)mappedEvent.getX(), + (int)mappedEvent.getY()); + return true; + } + + return false; + } + + public boolean onDoubleTap(MotionEvent e) + { + // send 2nd click for double click + MotionEvent mappedEvent = mapTouchEvent(e); + sessionViewListener.onSessionViewLeftTouch((int)mappedEvent.getX(), + (int)mappedEvent.getY(), true); + sessionViewListener.onSessionViewLeftTouch((int)mappedEvent.getX(), + (int)mappedEvent.getY(), false); + return true; + } + + public boolean onSingleTapUp(MotionEvent e) + { + // send single click + MotionEvent mappedEvent = mapTouchEvent(e); + sessionViewListener.onSessionViewBeginTouch(); + switch (e.getButtonState()) + { + case MotionEvent.BUTTON_PRIMARY: + sessionViewListener.onSessionViewLeftTouch((int)mappedEvent.getX(), + (int)mappedEvent.getY(), true); + sessionViewListener.onSessionViewLeftTouch((int)mappedEvent.getX(), + (int)mappedEvent.getY(), false); + break; + case MotionEvent.BUTTON_SECONDARY: + sessionViewListener.onSessionViewRightTouch((int)mappedEvent.getX(), + (int)mappedEvent.getY(), true); + sessionViewListener.onSessionViewRightTouch((int)mappedEvent.getX(), + (int)mappedEvent.getY(), false); + sessionViewListener.onSessionViewLeftTouch((int)mappedEvent.getX(), + (int)mappedEvent.getY(), true); + sessionViewListener.onSessionViewLeftTouch((int)mappedEvent.getX(), + (int)mappedEvent.getY(), false); + break; + } + sessionViewListener.onSessionViewEndTouch(); + return true; + } + } + + private class SessionDoubleGestureListener + implements DoubleGestureDetector.OnDoubleGestureListener + { + private MotionEvent prevEvent = null; + + public boolean onDoubleTouchDown(MotionEvent e) + { + sessionViewListener.onSessionViewBeginTouch(); + prevEvent = MotionEvent.obtain(e); + return true; + } + + public boolean onDoubleTouchUp(MotionEvent e) + { + if (prevEvent != null) + { + prevEvent.recycle(); + prevEvent = null; + } + sessionViewListener.onSessionViewEndTouch(); + return true; + } + + public boolean onDoubleTouchScroll(MotionEvent e1, MotionEvent e2) + { + // calc if user scrolled up or down (or if any scrolling happened at all) + float deltaY = e2.getY() - prevEvent.getY(); + if (deltaY > TOUCH_SCROLL_DELTA) + { + sessionViewListener.onSessionViewScroll(true); + prevEvent.recycle(); + prevEvent = MotionEvent.obtain(e2); + } + else if (deltaY < -TOUCH_SCROLL_DELTA) + { + sessionViewListener.onSessionViewScroll(false); + prevEvent.recycle(); + prevEvent = MotionEvent.obtain(e2); + } + return true; + } + + public boolean onDoubleTouchSingleTap(MotionEvent e) + { + // send single click + MotionEvent mappedEvent = mapDoubleTouchEvent(e); + sessionViewListener.onSessionViewRightTouch((int)mappedEvent.getX(), + (int)mappedEvent.getY(), true); + sessionViewListener.onSessionViewRightTouch((int)mappedEvent.getX(), + (int)mappedEvent.getY(), false); + return true; + } + } +} diff --git a/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/presentation/ShortcutsActivity.java b/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/presentation/ShortcutsActivity.java new file mode 100644 index 0000000..2121c6e --- /dev/null +++ b/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/presentation/ShortcutsActivity.java @@ -0,0 +1,160 @@ +/* + Android Shortcut activity + + Copyright 2013 Thincast Technologies GmbH, Author: Martin Fleisz + + This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. + If a copy of the MPL was not distributed with this file, You can obtain one at + http://mozilla.org/MPL/2.0/. +*/ + +package com.freerdp.freerdpcore.presentation; + +import android.app.AlertDialog; +import android.app.ListActivity; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.os.Parcelable; +import android.view.View; +import android.widget.AdapterView; +import android.widget.EditText; +import android.widget.TextView; + +import com.freerdp.freerdpcore.R; +import com.freerdp.freerdpcore.application.GlobalApp; +import com.freerdp.freerdpcore.domain.BookmarkBase; +import com.freerdp.freerdpcore.services.SessionRequestHandlerActivity; +import com.freerdp.freerdpcore.utils.BookmarkArrayAdapter; + +import java.util.ArrayList; + +public class ShortcutsActivity extends ListActivity +{ + + public static final String TAG = "ShortcutsActivity"; + + @Override public void onCreate(Bundle savedInstanceState) + { + + super.onCreate(savedInstanceState); + + Intent intent = getIntent(); + if (Intent.ACTION_CREATE_SHORTCUT.equals(intent.getAction())) + { + // set listeners for the list view + getListView().setOnItemClickListener(new AdapterView.OnItemClickListener() { + public void onItemClick(AdapterView parent, View view, int position, long id) + { + String refStr = view.getTag().toString(); + String defLabel = + ((TextView)(view.findViewById(R.id.bookmark_text1))).getText().toString(); + setupShortcut(refStr, defLabel); + } + }); + } + else + { + // just exit + finish(); + } + } + + @Override public void onResume() + { + super.onResume(); + // create bookmark cursor adapter + ArrayList bookmarks = GlobalApp.getManualBookmarkGateway().findAll(); + BookmarkArrayAdapter bookmarkAdapter = + new BookmarkArrayAdapter(this, android.R.layout.simple_list_item_2, bookmarks); + getListView().setAdapter(bookmarkAdapter); + } + + public void onPause() + { + super.onPause(); + getListView().setAdapter(null); + } + + /** + * This function creates a shortcut and returns it to the caller. There are actually two + * intents that you will send back. + *

+ * The first intent serves as a container for the shortcut and is returned to the launcher by + * setResult(). This intent must contain three fields: + *

+ *

    + *
  • {@link android.content.Intent#EXTRA_SHORTCUT_INTENT} The shortcut intent.
  • + *
  • {@link android.content.Intent#EXTRA_SHORTCUT_NAME} The text that will be displayed with + * the shortcut.
  • + *
  • {@link android.content.Intent#EXTRA_SHORTCUT_ICON} The shortcut's icon, if provided as a + * bitmap, or {@link android.content.Intent#EXTRA_SHORTCUT_ICON_RESOURCE} if provided as + * a drawable resource.
  • + *
+ *

+ * If you use a simple drawable resource, note that you must wrapper it using + * {@link android.content.Intent.ShortcutIconResource}, as shown below. This is required so + * that the launcher can access resources that are stored in your application's .apk file. If + * you return a bitmap, such as a thumbnail, you can simply put the bitmap into the extras + * bundle using {@link android.content.Intent#EXTRA_SHORTCUT_ICON}. + *

+ * The shortcut intent can be any intent that you wish the launcher to send, when the user + * clicks on the shortcut. Typically this will be {@link android.content.Intent#ACTION_VIEW} + * with an appropriate Uri for your content, but any Intent will work here as long as it + * triggers the desired action within your Activity. + */ + + private void setupShortcut(String strRef, String defaultLabel) + { + final String paramStrRef = strRef; + final String paramDefaultLabel = defaultLabel; + final Context paramContext = this; + + // display edit dialog to the user so he can specify the shortcut name + final EditText input = new EditText(this); + input.setText(defaultLabel); + + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(R.string.dlg_title_create_shortcut) + .setMessage(R.string.dlg_msg_create_shortcut) + .setView(input) + .setPositiveButton( + android.R.string.ok, + new DialogInterface.OnClickListener() { + @Override public void onClick(DialogInterface dialog, int which) + { + String label = input.getText().toString(); + if (label.length() == 0) + label = paramDefaultLabel; + + Intent shortcutIntent = new Intent(Intent.ACTION_VIEW); + shortcutIntent.setClassName(paramContext, + SessionRequestHandlerActivity.class.getName()); + shortcutIntent.setData(Uri.parse(paramStrRef)); + + // Then, set up the container intent (the response to the caller) + Intent intent = new Intent(); + intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent); + intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, label); + Parcelable iconResource = Intent.ShortcutIconResource.fromContext( + paramContext, R.drawable.icon_launcher_freerdp); + intent.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, iconResource); + + // Now, return the result to the launcher + setResult(RESULT_OK, intent); + finish(); + } + }) + .setNegativeButton(android.R.string.cancel, + new DialogInterface.OnClickListener() { + @Override public void onClick(DialogInterface dialog, int which) + { + dialog.dismiss(); + } + }) + .create() + .show(); + } +} diff --git a/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/presentation/TouchPointerView.java b/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/presentation/TouchPointerView.java new file mode 100644 index 0000000..6b8b96c --- /dev/null +++ b/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/presentation/TouchPointerView.java @@ -0,0 +1,385 @@ +/* + Android Touch Pointer view + + Copyright 2013 Thincast Technologies GmbH, Author: Martin Fleisz + + This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. + If a copy of the MPL was not distributed with this file, You can obtain one at + http://mozilla.org/MPL/2.0/. +*/ + +package com.freerdp.freerdpcore.presentation; + +import android.content.Context; +import android.graphics.Matrix; +import android.graphics.RectF; +import android.os.Handler; +import android.os.Message; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.widget.ImageView; + +import com.freerdp.freerdpcore.R; +import com.freerdp.freerdpcore.utils.GestureDetector; + +public class TouchPointerView extends ImageView +{ + + private static final int POINTER_ACTION_CURSOR = 0; + private static final int POINTER_ACTION_CLOSE = 3; + + // the touch pointer consists of 9 quadrants with the following functionality: + // + // ------------- + // | 0 | 1 | 2 | + // ------------- + // | 3 | 4 | 5 | + // ------------- + // | 6 | 7 | 8 | + // ------------- + // + // 0 ... contains the actual pointer (the tip must be centered in the quadrant) + // 1 ... is left empty + // 2, 3, 5, 6, 7, 8 ... function quadrants that issue a callback + // 4 ... pointer center used for left clicks and to drag the pointer + private static final int POINTER_ACTION_RCLICK = 2; + private static final int POINTER_ACTION_LCLICK = 4; + private static final int POINTER_ACTION_MOVE = 4; + private static final int POINTER_ACTION_SCROLL = 5; + private static final int POINTER_ACTION_RESET = 6; + private static final int POINTER_ACTION_KEYBOARD = 7; + private static final int POINTER_ACTION_EXTKEYBOARD = 8; + private static final float SCROLL_DELTA = 10.0f; + private static final int DEFAULT_TOUCH_POINTER_RESTORE_DELAY = 150; + private RectF pointerRect; + private RectF pointerAreaRects[] = new RectF[9]; + private Matrix translationMatrix; + private boolean pointerMoving = false; + private boolean pointerScrolling = false; + private TouchPointerListener listener = null; + private UIHandler uiHandler = new UIHandler(); + // gesture detection + private GestureDetector gestureDetector; + public TouchPointerView(Context context) + { + super(context); + initTouchPointer(context); + } + + public TouchPointerView(Context context, AttributeSet attrs) + { + super(context, attrs); + initTouchPointer(context); + } + + public TouchPointerView(Context context, AttributeSet attrs, int defStyle) + { + super(context, attrs, defStyle); + initTouchPointer(context); + } + + private void initTouchPointer(Context context) + { + gestureDetector = + new GestureDetector(context, new TouchPointerGestureListener(), null, true); + gestureDetector.setLongPressTimeout(500); + translationMatrix = new Matrix(); + setScaleType(ScaleType.MATRIX); + setImageMatrix(translationMatrix); + + // init rects + final float rectSizeWidth = (float)getDrawable().getIntrinsicWidth() / 3.0f; + final float rectSizeHeight = (float)getDrawable().getIntrinsicWidth() / 3.0f; + for (int i = 0; i < 3; i++) + { + for (int j = 0; j < 3; j++) + { + int left = (int)(j * rectSizeWidth); + int top = (int)(i * rectSizeHeight); + int right = left + (int)rectSizeWidth; + int bottom = top + (int)rectSizeHeight; + pointerAreaRects[i * 3 + j] = new RectF(left, top, right, bottom); + } + } + pointerRect = + new RectF(0, 0, getDrawable().getIntrinsicWidth(), getDrawable().getIntrinsicHeight()); + } + + public void setTouchPointerListener(TouchPointerListener listener) + { + this.listener = listener; + } + + public int getPointerWidth() + { + return getDrawable().getIntrinsicWidth(); + } + + public int getPointerHeight() + { + return getDrawable().getIntrinsicHeight(); + } + + public float[] getPointerPosition() + { + float[] curPos = new float[2]; + translationMatrix.mapPoints(curPos); + return curPos; + } + + private void movePointer(float deltaX, float deltaY) + { + translationMatrix.postTranslate(deltaX, deltaY); + setImageMatrix(translationMatrix); + } + + private void ensureVisibility(int screen_width, int screen_height) + { + float[] curPos = new float[2]; + translationMatrix.mapPoints(curPos); + + if (curPos[0] > (screen_width - pointerRect.width())) + curPos[0] = screen_width - pointerRect.width(); + if (curPos[0] < 0) + curPos[0] = 0; + if (curPos[1] > (screen_height - pointerRect.height())) + curPos[1] = screen_height - pointerRect.height(); + if (curPos[1] < 0) + curPos[1] = 0; + + translationMatrix.setTranslate(curPos[0], curPos[1]); + setImageMatrix(translationMatrix); + } + + private void displayPointerImageAction(int resId) + { + setPointerImage(resId); + uiHandler.sendEmptyMessageDelayed(0, DEFAULT_TOUCH_POINTER_RESTORE_DELAY); + } + + private void setPointerImage(int resId) + { + setImageResource(resId); + } + + // returns the pointer area with the current translation matrix applied + private RectF getCurrentPointerArea(int area) + { + RectF transRect = new RectF(pointerAreaRects[area]); + translationMatrix.mapRect(transRect); + return transRect; + } + + private boolean pointerAreaTouched(MotionEvent event, int area) + { + RectF transRect = new RectF(pointerAreaRects[area]); + translationMatrix.mapRect(transRect); + if (transRect.contains(event.getX(), event.getY())) + return true; + return false; + } + + private boolean pointerTouched(MotionEvent event) + { + RectF transRect = new RectF(pointerRect); + translationMatrix.mapRect(transRect); + if (transRect.contains(event.getX(), event.getY())) + return true; + return false; + } + + @Override public boolean onTouchEvent(MotionEvent event) + { + // check if pointer is being moved or if we are in scroll mode or if the pointer is touched + if (!pointerMoving && !pointerScrolling && !pointerTouched(event)) + return false; + return gestureDetector.onTouchEvent(event); + } + + @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) + { + // ensure touch pointer is visible + if (changed) + ensureVisibility(right - left, bottom - top); + } + + // touch pointer listener - is triggered if an action field is + public interface TouchPointerListener { + abstract void onTouchPointerClose(); + + abstract void onTouchPointerLeftClick(int x, int y, boolean down); + + abstract void onTouchPointerRightClick(int x, int y, boolean down); + + abstract void onTouchPointerMove(int x, int y); + + abstract void onTouchPointerScroll(boolean down); + + abstract void onTouchPointerToggleKeyboard(); + + abstract void onTouchPointerToggleExtKeyboard(); + + abstract void onTouchPointerResetScrollZoom(); + } + + private class UIHandler extends Handler + { + + UIHandler() + { + super(); + } + + @Override public void handleMessage(Message msg) + { + setPointerImage(R.drawable.touch_pointer_default); + } + } + + private class TouchPointerGestureListener extends GestureDetector.SimpleOnGestureListener + { + + private MotionEvent prevEvent = null; + + public boolean onDown(MotionEvent e) + { + if (pointerAreaTouched(e, POINTER_ACTION_MOVE)) + { + prevEvent = MotionEvent.obtain(e); + pointerMoving = true; + } + else if (pointerAreaTouched(e, POINTER_ACTION_SCROLL)) + { + prevEvent = MotionEvent.obtain(e); + pointerScrolling = true; + setPointerImage(R.drawable.touch_pointer_scroll); + } + + return true; + } + + public boolean onUp(MotionEvent e) + { + if (prevEvent != null) + { + prevEvent.recycle(); + prevEvent = null; + } + + if (pointerScrolling) + setPointerImage(R.drawable.touch_pointer_default); + + pointerMoving = false; + pointerScrolling = false; + return true; + } + + public void onLongPress(MotionEvent e) + { + if (pointerAreaTouched(e, POINTER_ACTION_LCLICK)) + { + setPointerImage(R.drawable.touch_pointer_active); + pointerMoving = true; + RectF rect = getCurrentPointerArea(POINTER_ACTION_CURSOR); + listener.onTouchPointerLeftClick((int)rect.centerX(), (int)rect.centerY(), true); + } + } + + public void onLongPressUp(MotionEvent e) + { + if (pointerMoving) + { + setPointerImage(R.drawable.touch_pointer_default); + pointerMoving = false; + RectF rect = getCurrentPointerArea(POINTER_ACTION_CURSOR); + listener.onTouchPointerLeftClick((int)rect.centerX(), (int)rect.centerY(), false); + } + } + + public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) + { + if (pointerMoving) + { + // move pointer graphics + movePointer((int)(e2.getX() - prevEvent.getX()), + (int)(e2.getY() - prevEvent.getY())); + prevEvent.recycle(); + prevEvent = MotionEvent.obtain(e2); + + // send move notification + RectF rect = getCurrentPointerArea(POINTER_ACTION_CURSOR); + listener.onTouchPointerMove((int)rect.centerX(), (int)rect.centerY()); + return true; + } + else if (pointerScrolling) + { + // calc if user scrolled up or down (or if any scrolling happened at all) + float deltaY = e2.getY() - prevEvent.getY(); + if (deltaY > SCROLL_DELTA) + { + listener.onTouchPointerScroll(true); + prevEvent.recycle(); + prevEvent = MotionEvent.obtain(e2); + } + else if (deltaY < -SCROLL_DELTA) + { + listener.onTouchPointerScroll(false); + prevEvent.recycle(); + prevEvent = MotionEvent.obtain(e2); + } + return true; + } + return false; + } + + public boolean onSingleTapUp(MotionEvent e) + { + // look what area got touched and fire actions accordingly + if (pointerAreaTouched(e, POINTER_ACTION_CLOSE)) + listener.onTouchPointerClose(); + else if (pointerAreaTouched(e, POINTER_ACTION_LCLICK)) + { + displayPointerImageAction(R.drawable.touch_pointer_lclick); + RectF rect = getCurrentPointerArea(POINTER_ACTION_CURSOR); + listener.onTouchPointerLeftClick((int)rect.centerX(), (int)rect.centerY(), true); + listener.onTouchPointerLeftClick((int)rect.centerX(), (int)rect.centerY(), false); + } + else if (pointerAreaTouched(e, POINTER_ACTION_RCLICK)) + { + displayPointerImageAction(R.drawable.touch_pointer_rclick); + RectF rect = getCurrentPointerArea(POINTER_ACTION_CURSOR); + listener.onTouchPointerRightClick((int)rect.centerX(), (int)rect.centerY(), true); + listener.onTouchPointerRightClick((int)rect.centerX(), (int)rect.centerY(), false); + } + else if (pointerAreaTouched(e, POINTER_ACTION_KEYBOARD)) + { + displayPointerImageAction(R.drawable.touch_pointer_keyboard); + listener.onTouchPointerToggleKeyboard(); + } + else if (pointerAreaTouched(e, POINTER_ACTION_EXTKEYBOARD)) + { + displayPointerImageAction(R.drawable.touch_pointer_extkeyboard); + listener.onTouchPointerToggleExtKeyboard(); + } + else if (pointerAreaTouched(e, POINTER_ACTION_RESET)) + { + displayPointerImageAction(R.drawable.touch_pointer_reset); + listener.onTouchPointerResetScrollZoom(); + } + + return true; + } + + public boolean onDoubleTap(MotionEvent e) + { + // issue a double click notification if performed in center quadrant + if (pointerAreaTouched(e, POINTER_ACTION_LCLICK)) + { + RectF rect = getCurrentPointerArea(POINTER_ACTION_CURSOR); + listener.onTouchPointerLeftClick((int)rect.centerX(), (int)rect.centerY(), true); + listener.onTouchPointerLeftClick((int)rect.centerX(), (int)rect.centerY(), false); + } + return true; + } + } +} diff --git a/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/services/BookmarkBaseGateway.java b/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/services/BookmarkBaseGateway.java new file mode 100644 index 0000000..d3ed7fe --- /dev/null +++ b/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/services/BookmarkBaseGateway.java @@ -0,0 +1,617 @@ +/* + Helper class to access bookmark database + + Copyright 2013 Thincast Technologies GmbH, Author: Martin Fleisz + + This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. + If a copy of the MPL was not distributed with this file, You can obtain one at + http://mozilla.org/MPL/2.0/. +*/ + +package com.freerdp.freerdpcore.services; + +import android.content.ContentValues; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteException; +import android.database.sqlite.SQLiteOpenHelper; +import android.database.sqlite.SQLiteQueryBuilder; +import android.util.Log; + +import com.freerdp.freerdpcore.domain.BookmarkBase; + +import java.util.ArrayList; + +public abstract class BookmarkBaseGateway +{ + private final static String TAG = "BookmarkBaseGateway"; + private SQLiteOpenHelper bookmarkDB; + + private static final String JOIN_PREFIX = "join_"; + private static final String KEY_BOOKMARK_ID = "bookmarkId"; + private static final String KEY_SCREEN_COLORS = "screenColors"; + private static final String KEY_SCREEN_COLORS_3G = "screenColors3G"; + private static final String KEY_SCREEN_RESOLUTION = "screenResolution"; + private static final String KEY_SCREEN_RESOLUTION_3G = "screenResolution3G"; + private static final String KEY_SCREEN_WIDTH = "screenWidth"; + private static final String KEY_SCREEN_WIDTH_3G = "screenWidth3G"; + private static final String KEY_SCREEN_HEIGHT = "screenHeight"; + private static final String KEY_SCREEN_HEIGHT_3G = "screenHeight3G"; + + private static final String KEY_PERFORMANCE_RFX = "performanceRemoteFX"; + private static final String KEY_PERFORMANCE_RFX_3G = "performanceRemoteFX3G"; + private static final String KEY_PERFORMANCE_GFX = "performanceGfx"; + private static final String KEY_PERFORMANCE_GFX_3G = "performanceGfx3G"; + private static final String KEY_PERFORMANCE_H264 = "performanceGfxH264"; + private static final String KEY_PERFORMANCE_H264_3G = "performanceGfxH2643G"; + private static final String KEY_PERFORMANCE_WALLPAPER = "performanceWallpaper"; + private static final String KEY_PERFORMANCE_WALLPAPER_3G = "performanceWallpaper3G"; + private static final String KEY_PERFORMANCE_THEME = "performanceTheming"; + private static final String KEY_PERFORMANCE_THEME_3G = "performanceTheming3G"; + + private static final String KEY_PERFORMANCE_DRAG = "performanceFullWindowDrag"; + private static final String KEY_PERFORMANCE_DRAG_3G = "performanceFullWindowDrag3G"; + private static final String KEY_PERFORMANCE_MENU_ANIMATIONS = "performanceMenuAnimations"; + private static final String KEY_PERFORMANCE_MENU_ANIMATIONS_3G = "performanceMenuAnimations3G"; + private static final String KEY_PERFORMANCE_FONTS = "performanceFontSmoothing"; + private static final String KEY_PERFORMANCE_FONTS_3G = "performanceFontSmoothing3G"; + private static final String KEY_PERFORMANCE_COMPOSITION = "performanceDesktopComposition"; + private static final String KEY_PERFORMANCE_COMPOSITION_3G = "performanceDesktopComposition3G"; + + public BookmarkBaseGateway(SQLiteOpenHelper bookmarkDB) + { + this.bookmarkDB = bookmarkDB; + } + + protected abstract BookmarkBase createBookmark(); + + protected abstract String getBookmarkTableName(); + + protected abstract void addBookmarkSpecificColumns(ArrayList columns); + + protected abstract void addBookmarkSpecificColumns(BookmarkBase bookmark, + ContentValues columns); + + protected abstract void readBookmarkSpecificColumns(BookmarkBase bookmark, Cursor cursor); + + public void insert(BookmarkBase bookmark) + { + // begin transaction + SQLiteDatabase db = getWritableDatabase(); + db.beginTransaction(); + + long rowid; + ContentValues values = new ContentValues(); + values.put(BookmarkDB.DB_KEY_BOOKMARK_LABEL, bookmark.getLabel()); + values.put(BookmarkDB.DB_KEY_BOOKMARK_USERNAME, bookmark.getUsername()); + values.put(BookmarkDB.DB_KEY_BOOKMARK_PASSWORD, bookmark.getPassword()); + values.put(BookmarkDB.DB_KEY_BOOKMARK_DOMAIN, bookmark.getDomain()); + // insert screen and performance settings + rowid = insertScreenSettings(db, bookmark.getScreenSettings()); + values.put(BookmarkDB.DB_KEY_SCREEN_SETTINGS, rowid); + rowid = insertPerformanceFlags(db, bookmark.getPerformanceFlags()); + values.put(BookmarkDB.DB_KEY_PERFORMANCE_FLAGS, rowid); + + // advanced settings + values.put(BookmarkDB.DB_KEY_BOOKMARK_3G_ENABLE, + bookmark.getAdvancedSettings().getEnable3GSettings()); + // insert 3G screen and 3G performance settings + rowid = insertScreenSettings(db, bookmark.getAdvancedSettings().getScreen3G()); + values.put(BookmarkDB.DB_KEY_SCREEN_SETTINGS_3G, rowid); + rowid = insertPerformanceFlags(db, bookmark.getAdvancedSettings().getPerformance3G()); + values.put(BookmarkDB.DB_KEY_PERFORMANCE_FLAGS_3G, rowid); + values.put(BookmarkDB.DB_KEY_BOOKMARK_REDIRECT_SDCARD, + bookmark.getAdvancedSettings().getRedirectSDCard()); + values.put(BookmarkDB.DB_KEY_BOOKMARK_REDIRECT_SOUND, + bookmark.getAdvancedSettings().getRedirectSound()); + values.put(BookmarkDB.DB_KEY_BOOKMARK_REDIRECT_MICROPHONE, + bookmark.getAdvancedSettings().getRedirectMicrophone()); + values.put(BookmarkDB.DB_KEY_BOOKMARK_SECURITY, + bookmark.getAdvancedSettings().getSecurity()); + values.put(BookmarkDB.DB_KEY_BOOKMARK_CONSOLE_MODE, + bookmark.getAdvancedSettings().getConsoleMode()); + values.put(BookmarkDB.DB_KEY_BOOKMARK_REMOTE_PROGRAM, + bookmark.getAdvancedSettings().getRemoteProgram()); + values.put(BookmarkDB.DB_KEY_BOOKMARK_WORK_DIR, + bookmark.getAdvancedSettings().getWorkDir()); + + values.put(BookmarkDB.DB_KEY_BOOKMARK_ASYNC_CHANNEL, + bookmark.getDebugSettings().getAsyncChannel()); + values.put(BookmarkDB.DB_KEY_BOOKMARK_ASYNC_INPUT, + bookmark.getDebugSettings().getAsyncInput()); + values.put(BookmarkDB.DB_KEY_BOOKMARK_ASYNC_UPDATE, + bookmark.getDebugSettings().getAsyncUpdate()); + values.put(BookmarkDB.DB_KEY_BOOKMARK_DEBUG_LEVEL, + bookmark.getDebugSettings().getDebugLevel()); + + // add any special columns + addBookmarkSpecificColumns(bookmark, values); + + // insert bookmark and end transaction + db.insertOrThrow(getBookmarkTableName(), null, values); + db.setTransactionSuccessful(); + db.endTransaction(); + } + + public boolean update(BookmarkBase bookmark) + { + // start a transaction + SQLiteDatabase db = getWritableDatabase(); + db.beginTransaction(); + + // bookmark settings + ContentValues values = new ContentValues(); + values.put(BookmarkDB.DB_KEY_BOOKMARK_LABEL, bookmark.getLabel()); + values.put(BookmarkDB.DB_KEY_BOOKMARK_USERNAME, bookmark.getUsername()); + values.put(BookmarkDB.DB_KEY_BOOKMARK_PASSWORD, bookmark.getPassword()); + values.put(BookmarkDB.DB_KEY_BOOKMARK_DOMAIN, bookmark.getDomain()); + // update screen and performance settings settings + updateScreenSettings(db, bookmark); + updatePerformanceFlags(db, bookmark); + + // advanced settings + values.put(BookmarkDB.DB_KEY_BOOKMARK_3G_ENABLE, + bookmark.getAdvancedSettings().getEnable3GSettings()); + // update 3G screen and 3G performance settings settings + updateScreenSettings3G(db, bookmark); + updatePerformanceFlags3G(db, bookmark); + values.put(BookmarkDB.DB_KEY_BOOKMARK_REDIRECT_SDCARD, + bookmark.getAdvancedSettings().getRedirectSDCard()); + values.put(BookmarkDB.DB_KEY_BOOKMARK_REDIRECT_SOUND, + bookmark.getAdvancedSettings().getRedirectSound()); + values.put(BookmarkDB.DB_KEY_BOOKMARK_REDIRECT_MICROPHONE, + bookmark.getAdvancedSettings().getRedirectMicrophone()); + values.put(BookmarkDB.DB_KEY_BOOKMARK_SECURITY, + bookmark.getAdvancedSettings().getSecurity()); + values.put(BookmarkDB.DB_KEY_BOOKMARK_CONSOLE_MODE, + bookmark.getAdvancedSettings().getConsoleMode()); + values.put(BookmarkDB.DB_KEY_BOOKMARK_REMOTE_PROGRAM, + bookmark.getAdvancedSettings().getRemoteProgram()); + values.put(BookmarkDB.DB_KEY_BOOKMARK_WORK_DIR, + bookmark.getAdvancedSettings().getWorkDir()); + + values.put(BookmarkDB.DB_KEY_BOOKMARK_ASYNC_CHANNEL, + bookmark.getDebugSettings().getAsyncChannel()); + values.put(BookmarkDB.DB_KEY_BOOKMARK_ASYNC_INPUT, + bookmark.getDebugSettings().getAsyncInput()); + values.put(BookmarkDB.DB_KEY_BOOKMARK_ASYNC_UPDATE, + bookmark.getDebugSettings().getAsyncUpdate()); + values.put(BookmarkDB.DB_KEY_BOOKMARK_DEBUG_LEVEL, + bookmark.getDebugSettings().getDebugLevel()); + + addBookmarkSpecificColumns(bookmark, values); + + // update bookmark + boolean res = (db.update(getBookmarkTableName(), values, + BookmarkDB.ID + " = " + bookmark.getId(), null) == 1); + + // commit + db.setTransactionSuccessful(); + db.endTransaction(); + + return res; + } + + public void delete(long id) + { + SQLiteDatabase db = getWritableDatabase(); + db.delete(getBookmarkTableName(), BookmarkDB.ID + " = " + id, null); + } + + public BookmarkBase findById(long id) + { + Cursor cursor = + queryBookmarks(getBookmarkTableName() + "." + BookmarkDB.ID + " = " + id, null); + if (cursor.getCount() == 0) + { + cursor.close(); + return null; + } + + cursor.moveToFirst(); + BookmarkBase bookmark = getBookmarkFromCursor(cursor); + cursor.close(); + return bookmark; + } + + public BookmarkBase findByLabel(String label) + { + Cursor cursor = queryBookmarks(BookmarkDB.DB_KEY_BOOKMARK_LABEL + " = '" + label + "'", + BookmarkDB.DB_KEY_BOOKMARK_LABEL); + if (cursor.getCount() > 1) + Log.e(TAG, "More than one bookmark with the same label found!"); + + BookmarkBase bookmark = null; + if (cursor.moveToFirst() && (cursor.getCount() > 0)) + bookmark = getBookmarkFromCursor(cursor); + + cursor.close(); + return bookmark; + } + + public ArrayList findByLabelLike(String pattern) + { + Cursor cursor = + queryBookmarks(BookmarkDB.DB_KEY_BOOKMARK_LABEL + " LIKE '%" + pattern + "%'", + BookmarkDB.DB_KEY_BOOKMARK_LABEL); + ArrayList bookmarks = new ArrayList(cursor.getCount()); + + if (cursor.moveToFirst() && (cursor.getCount() > 0)) + { + do + { + bookmarks.add(getBookmarkFromCursor(cursor)); + } while (cursor.moveToNext()); + } + + cursor.close(); + return bookmarks; + } + + public ArrayList findAll() + { + Cursor cursor = queryBookmarks(null, BookmarkDB.DB_KEY_BOOKMARK_LABEL); + final int count = cursor.getCount(); + ArrayList bookmarks = new ArrayList<>(count); + + if (cursor.moveToFirst() && (count > 0)) + { + do + { + bookmarks.add(getBookmarkFromCursor(cursor)); + } while (cursor.moveToNext()); + } + + cursor.close(); + return bookmarks; + } + + protected Cursor queryBookmarks(String whereClause, String orderBy) + { + // create tables string + final String ID = BookmarkDB.ID; + final String tables = + BookmarkDB.DB_TABLE_BOOKMARK + " INNER JOIN " + BookmarkDB.DB_TABLE_SCREEN + " AS " + + JOIN_PREFIX + BookmarkDB.DB_KEY_SCREEN_SETTINGS + " ON " + JOIN_PREFIX + + BookmarkDB.DB_KEY_SCREEN_SETTINGS + "." + ID + " = " + BookmarkDB.DB_TABLE_BOOKMARK + + "." + BookmarkDB.DB_KEY_SCREEN_SETTINGS + " INNER JOIN " + + BookmarkDB.DB_TABLE_PERFORMANCE + " AS " + JOIN_PREFIX + + BookmarkDB.DB_KEY_PERFORMANCE_FLAGS + " ON " + JOIN_PREFIX + + BookmarkDB.DB_KEY_PERFORMANCE_FLAGS + "." + ID + " = " + BookmarkDB.DB_TABLE_BOOKMARK + + "." + BookmarkDB.DB_KEY_PERFORMANCE_FLAGS + " INNER JOIN " + + BookmarkDB.DB_TABLE_SCREEN + " AS " + JOIN_PREFIX + + BookmarkDB.DB_KEY_SCREEN_SETTINGS_3G + " ON " + JOIN_PREFIX + + BookmarkDB.DB_KEY_SCREEN_SETTINGS_3G + "." + ID + " = " + BookmarkDB.DB_TABLE_BOOKMARK + + "." + BookmarkDB.DB_KEY_SCREEN_SETTINGS_3G + " INNER JOIN " + + BookmarkDB.DB_TABLE_PERFORMANCE + " AS " + JOIN_PREFIX + + BookmarkDB.DB_KEY_PERFORMANCE_FLAGS_3G + " ON " + JOIN_PREFIX + + BookmarkDB.DB_KEY_PERFORMANCE_FLAGS_3G + "." + ID + " = " + + BookmarkDB.DB_TABLE_BOOKMARK + "." + BookmarkDB.DB_KEY_PERFORMANCE_FLAGS_3G; + + // create columns list + ArrayList columns = new ArrayList<>(); + addBookmarkColumns(columns); + addScreenSettingsColumns(columns); + addPerformanceFlagsColumns(columns); + addScreenSettings3GColumns(columns); + addPerformanceFlags3GColumns(columns); + + String[] cols = new String[columns.size()]; + columns.toArray(cols); + + SQLiteDatabase db = getReadableDatabase(); + final String query = SQLiteQueryBuilder.buildQueryString(false, tables, cols, whereClause, + null, null, orderBy, null); + return db.rawQuery(query, null); + } + + private void addBookmarkColumns(ArrayList columns) + { + columns.add(getBookmarkTableName() + "." + BookmarkDB.ID + " " + KEY_BOOKMARK_ID); + columns.add(BookmarkDB.DB_KEY_BOOKMARK_LABEL); + columns.add(BookmarkDB.DB_KEY_BOOKMARK_USERNAME); + columns.add(BookmarkDB.DB_KEY_BOOKMARK_PASSWORD); + columns.add(BookmarkDB.DB_KEY_BOOKMARK_DOMAIN); + + // advanced settings + columns.add(BookmarkDB.DB_KEY_BOOKMARK_3G_ENABLE); + columns.add(BookmarkDB.DB_KEY_BOOKMARK_REDIRECT_SDCARD); + columns.add(BookmarkDB.DB_KEY_BOOKMARK_REDIRECT_SOUND); + columns.add(BookmarkDB.DB_KEY_BOOKMARK_REDIRECT_MICROPHONE); + columns.add(BookmarkDB.DB_KEY_BOOKMARK_SECURITY); + columns.add(BookmarkDB.DB_KEY_BOOKMARK_CONSOLE_MODE); + columns.add(BookmarkDB.DB_KEY_BOOKMARK_REMOTE_PROGRAM); + columns.add(BookmarkDB.DB_KEY_BOOKMARK_WORK_DIR); + + // debug settings + columns.add(BookmarkDB.DB_KEY_BOOKMARK_DEBUG_LEVEL); + columns.add(BookmarkDB.DB_KEY_BOOKMARK_ASYNC_CHANNEL); + columns.add(BookmarkDB.DB_KEY_BOOKMARK_ASYNC_UPDATE); + columns.add(BookmarkDB.DB_KEY_BOOKMARK_ASYNC_INPUT); + + addBookmarkSpecificColumns(columns); + } + + private void addScreenSettingsColumns(ArrayList columns) + { + columns.add(JOIN_PREFIX + BookmarkDB.DB_KEY_SCREEN_SETTINGS + "." + + BookmarkDB.DB_KEY_SCREEN_COLORS + " as " + KEY_SCREEN_COLORS); + columns.add(JOIN_PREFIX + BookmarkDB.DB_KEY_SCREEN_SETTINGS + "." + + BookmarkDB.DB_KEY_SCREEN_RESOLUTION + " as " + KEY_SCREEN_RESOLUTION); + columns.add(JOIN_PREFIX + BookmarkDB.DB_KEY_SCREEN_SETTINGS + "." + + BookmarkDB.DB_KEY_SCREEN_WIDTH + " as " + KEY_SCREEN_WIDTH); + columns.add(JOIN_PREFIX + BookmarkDB.DB_KEY_SCREEN_SETTINGS + "." + + BookmarkDB.DB_KEY_SCREEN_HEIGHT + " as " + KEY_SCREEN_HEIGHT); + } + + private void addPerformanceFlagsColumns(ArrayList columns) + { + columns.add(JOIN_PREFIX + BookmarkDB.DB_KEY_PERFORMANCE_FLAGS + "." + + BookmarkDB.DB_KEY_PERFORMANCE_RFX + " as " + KEY_PERFORMANCE_RFX); + columns.add(JOIN_PREFIX + BookmarkDB.DB_KEY_PERFORMANCE_FLAGS + "." + + BookmarkDB.DB_KEY_PERFORMANCE_GFX + " as " + KEY_PERFORMANCE_GFX); + columns.add(JOIN_PREFIX + BookmarkDB.DB_KEY_PERFORMANCE_FLAGS + "." + + BookmarkDB.DB_KEY_PERFORMANCE_H264 + " as " + KEY_PERFORMANCE_H264); + columns.add(JOIN_PREFIX + BookmarkDB.DB_KEY_PERFORMANCE_FLAGS + "." + + BookmarkDB.DB_KEY_PERFORMANCE_WALLPAPER + " as " + KEY_PERFORMANCE_WALLPAPER); + columns.add(JOIN_PREFIX + BookmarkDB.DB_KEY_PERFORMANCE_FLAGS + "." + + BookmarkDB.DB_KEY_PERFORMANCE_THEME + " as " + KEY_PERFORMANCE_THEME); + columns.add(JOIN_PREFIX + BookmarkDB.DB_KEY_PERFORMANCE_FLAGS + "." + + BookmarkDB.DB_KEY_PERFORMANCE_DRAG + " as " + KEY_PERFORMANCE_DRAG); + columns.add(JOIN_PREFIX + BookmarkDB.DB_KEY_PERFORMANCE_FLAGS + "." + + BookmarkDB.DB_KEY_PERFORMANCE_MENU_ANIMATIONS + " as " + + KEY_PERFORMANCE_MENU_ANIMATIONS); + columns.add(JOIN_PREFIX + BookmarkDB.DB_KEY_PERFORMANCE_FLAGS + "." + + BookmarkDB.DB_KEY_PERFORMANCE_FONTS + " as " + KEY_PERFORMANCE_FONTS); + columns.add(JOIN_PREFIX + BookmarkDB.DB_KEY_PERFORMANCE_FLAGS + "." + + BookmarkDB.DB_KEY_PERFORMANCE_COMPOSITION + " " + KEY_PERFORMANCE_COMPOSITION); + } + + private void addScreenSettings3GColumns(ArrayList columns) + { + columns.add(JOIN_PREFIX + BookmarkDB.DB_KEY_SCREEN_SETTINGS_3G + "." + + BookmarkDB.DB_KEY_SCREEN_COLORS + " as " + KEY_SCREEN_COLORS_3G); + columns.add(JOIN_PREFIX + BookmarkDB.DB_KEY_SCREEN_SETTINGS_3G + "." + + BookmarkDB.DB_KEY_SCREEN_RESOLUTION + " as " + KEY_SCREEN_RESOLUTION_3G); + columns.add(JOIN_PREFIX + BookmarkDB.DB_KEY_SCREEN_SETTINGS_3G + "." + + BookmarkDB.DB_KEY_SCREEN_WIDTH + " as " + KEY_SCREEN_WIDTH_3G); + columns.add(JOIN_PREFIX + BookmarkDB.DB_KEY_SCREEN_SETTINGS_3G + "." + + BookmarkDB.DB_KEY_SCREEN_HEIGHT + " as " + KEY_SCREEN_HEIGHT_3G); + } + + private void addPerformanceFlags3GColumns(ArrayList columns) + { + columns.add(JOIN_PREFIX + BookmarkDB.DB_KEY_PERFORMANCE_FLAGS_3G + "." + + BookmarkDB.DB_KEY_PERFORMANCE_RFX + " as " + KEY_PERFORMANCE_RFX_3G); + columns.add(JOIN_PREFIX + BookmarkDB.DB_KEY_PERFORMANCE_FLAGS_3G + "." + + BookmarkDB.DB_KEY_PERFORMANCE_GFX + " as " + KEY_PERFORMANCE_GFX_3G); + columns.add(JOIN_PREFIX + BookmarkDB.DB_KEY_PERFORMANCE_FLAGS_3G + "." + + BookmarkDB.DB_KEY_PERFORMANCE_H264 + " as " + KEY_PERFORMANCE_H264_3G); + columns.add(JOIN_PREFIX + BookmarkDB.DB_KEY_PERFORMANCE_FLAGS_3G + "." + + BookmarkDB.DB_KEY_PERFORMANCE_WALLPAPER + " as " + + KEY_PERFORMANCE_WALLPAPER_3G); + columns.add(JOIN_PREFIX + BookmarkDB.DB_KEY_PERFORMANCE_FLAGS_3G + "." + + BookmarkDB.DB_KEY_PERFORMANCE_THEME + " as " + KEY_PERFORMANCE_THEME_3G); + columns.add(JOIN_PREFIX + BookmarkDB.DB_KEY_PERFORMANCE_FLAGS_3G + "." + + BookmarkDB.DB_KEY_PERFORMANCE_DRAG + " as " + KEY_PERFORMANCE_DRAG_3G); + columns.add(JOIN_PREFIX + BookmarkDB.DB_KEY_PERFORMANCE_FLAGS_3G + "." + + BookmarkDB.DB_KEY_PERFORMANCE_MENU_ANIMATIONS + " as " + + KEY_PERFORMANCE_MENU_ANIMATIONS_3G); + columns.add(JOIN_PREFIX + BookmarkDB.DB_KEY_PERFORMANCE_FLAGS_3G + "." + + BookmarkDB.DB_KEY_PERFORMANCE_FONTS + " as " + KEY_PERFORMANCE_FONTS_3G); + columns.add(JOIN_PREFIX + BookmarkDB.DB_KEY_PERFORMANCE_FLAGS_3G + "." + + BookmarkDB.DB_KEY_PERFORMANCE_COMPOSITION + " " + + KEY_PERFORMANCE_COMPOSITION_3G); + } + + protected BookmarkBase getBookmarkFromCursor(Cursor cursor) + { + BookmarkBase bookmark = createBookmark(); + bookmark.setId(cursor.getLong(cursor.getColumnIndex(KEY_BOOKMARK_ID))); + bookmark.setLabel( + cursor.getString(cursor.getColumnIndex(BookmarkDB.DB_KEY_BOOKMARK_LABEL))); + bookmark.setUsername( + cursor.getString(cursor.getColumnIndex(BookmarkDB.DB_KEY_BOOKMARK_USERNAME))); + bookmark.setPassword( + cursor.getString(cursor.getColumnIndex(BookmarkDB.DB_KEY_BOOKMARK_PASSWORD))); + bookmark.setDomain( + cursor.getString(cursor.getColumnIndex(BookmarkDB.DB_KEY_BOOKMARK_DOMAIN))); + readScreenSettings(bookmark, cursor); + readPerformanceFlags(bookmark, cursor); + + // advanced settings + bookmark.getAdvancedSettings().setEnable3GSettings( + cursor.getInt(cursor.getColumnIndex(BookmarkDB.DB_KEY_BOOKMARK_3G_ENABLE)) != 0); + readScreenSettings3G(bookmark, cursor); + readPerformanceFlags3G(bookmark, cursor); + bookmark.getAdvancedSettings().setRedirectSDCard( + cursor.getInt(cursor.getColumnIndex(BookmarkDB.DB_KEY_BOOKMARK_REDIRECT_SDCARD)) != 0); + bookmark.getAdvancedSettings().setRedirectSound( + cursor.getInt(cursor.getColumnIndex(BookmarkDB.DB_KEY_BOOKMARK_REDIRECT_SOUND))); + bookmark.getAdvancedSettings().setRedirectMicrophone( + cursor.getInt(cursor.getColumnIndex(BookmarkDB.DB_KEY_BOOKMARK_REDIRECT_MICROPHONE)) != + 0); + bookmark.getAdvancedSettings().setSecurity( + cursor.getInt(cursor.getColumnIndex(BookmarkDB.DB_KEY_BOOKMARK_SECURITY))); + bookmark.getAdvancedSettings().setConsoleMode( + cursor.getInt(cursor.getColumnIndex(BookmarkDB.DB_KEY_BOOKMARK_CONSOLE_MODE)) != 0); + bookmark.getAdvancedSettings().setRemoteProgram( + cursor.getString(cursor.getColumnIndex(BookmarkDB.DB_KEY_BOOKMARK_REMOTE_PROGRAM))); + bookmark.getAdvancedSettings().setWorkDir( + cursor.getString(cursor.getColumnIndex(BookmarkDB.DB_KEY_BOOKMARK_WORK_DIR))); + + bookmark.getDebugSettings().setAsyncChannel( + cursor.getInt(cursor.getColumnIndex(BookmarkDB.DB_KEY_BOOKMARK_ASYNC_CHANNEL)) == 1); + bookmark.getDebugSettings().setAsyncInput( + cursor.getInt(cursor.getColumnIndex(BookmarkDB.DB_KEY_BOOKMARK_ASYNC_INPUT)) == 1); + bookmark.getDebugSettings().setAsyncUpdate( + cursor.getInt(cursor.getColumnIndex(BookmarkDB.DB_KEY_BOOKMARK_ASYNC_UPDATE)) == 1); + bookmark.getDebugSettings().setDebugLevel( + cursor.getString(cursor.getColumnIndex(BookmarkDB.DB_KEY_BOOKMARK_DEBUG_LEVEL))); + + readBookmarkSpecificColumns(bookmark, cursor); + + return bookmark; + } + + private void readScreenSettings(BookmarkBase bookmark, Cursor cursor) + { + BookmarkBase.ScreenSettings screenSettings = bookmark.getScreenSettings(); + screenSettings.setColors(cursor.getInt(cursor.getColumnIndex(KEY_SCREEN_COLORS))); + screenSettings.setResolution(cursor.getInt(cursor.getColumnIndex(KEY_SCREEN_RESOLUTION))); + screenSettings.setWidth(cursor.getInt(cursor.getColumnIndex(KEY_SCREEN_WIDTH))); + screenSettings.setHeight(cursor.getInt(cursor.getColumnIndex(KEY_SCREEN_HEIGHT))); + } + + private void readPerformanceFlags(BookmarkBase bookmark, Cursor cursor) + { + BookmarkBase.PerformanceFlags perfFlags = bookmark.getPerformanceFlags(); + perfFlags.setRemoteFX(cursor.getInt(cursor.getColumnIndex(KEY_PERFORMANCE_RFX)) != 0); + perfFlags.setGfx(cursor.getInt(cursor.getColumnIndex(KEY_PERFORMANCE_GFX)) != 0); + perfFlags.setH264(cursor.getInt(cursor.getColumnIndex(KEY_PERFORMANCE_H264)) != 0); + perfFlags.setWallpaper(cursor.getInt(cursor.getColumnIndex(KEY_PERFORMANCE_WALLPAPER)) != + 0); + perfFlags.setTheming(cursor.getInt(cursor.getColumnIndex(KEY_PERFORMANCE_THEME)) != 0); + perfFlags.setFullWindowDrag(cursor.getInt(cursor.getColumnIndex(KEY_PERFORMANCE_DRAG)) != + 0); + perfFlags.setMenuAnimations( + cursor.getInt(cursor.getColumnIndex(KEY_PERFORMANCE_MENU_ANIMATIONS)) != 0); + perfFlags.setFontSmoothing(cursor.getInt(cursor.getColumnIndex(KEY_PERFORMANCE_FONTS)) != + 0); + perfFlags.setDesktopComposition( + cursor.getInt(cursor.getColumnIndex(KEY_PERFORMANCE_COMPOSITION)) != 0); + } + + private void readScreenSettings3G(BookmarkBase bookmark, Cursor cursor) + { + BookmarkBase.ScreenSettings screenSettings = bookmark.getAdvancedSettings().getScreen3G(); + screenSettings.setColors(cursor.getInt(cursor.getColumnIndex(KEY_SCREEN_COLORS_3G))); + screenSettings.setResolution( + cursor.getInt(cursor.getColumnIndex(KEY_SCREEN_RESOLUTION_3G))); + screenSettings.setWidth(cursor.getInt(cursor.getColumnIndex(KEY_SCREEN_WIDTH_3G))); + screenSettings.setHeight(cursor.getInt(cursor.getColumnIndex(KEY_SCREEN_HEIGHT_3G))); + } + + private void readPerformanceFlags3G(BookmarkBase bookmark, Cursor cursor) + { + BookmarkBase.PerformanceFlags perfFlags = bookmark.getAdvancedSettings().getPerformance3G(); + perfFlags.setRemoteFX(cursor.getInt(cursor.getColumnIndex(KEY_PERFORMANCE_RFX_3G)) != 0); + perfFlags.setGfx(cursor.getInt(cursor.getColumnIndex(KEY_PERFORMANCE_GFX_3G)) != 0); + perfFlags.setH264(cursor.getInt(cursor.getColumnIndex(KEY_PERFORMANCE_H264_3G)) != 0); + perfFlags.setWallpaper(cursor.getInt(cursor.getColumnIndex(KEY_PERFORMANCE_WALLPAPER_3G)) != + 0); + perfFlags.setTheming(cursor.getInt(cursor.getColumnIndex(KEY_PERFORMANCE_THEME_3G)) != 0); + perfFlags.setFullWindowDrag(cursor.getInt(cursor.getColumnIndex(KEY_PERFORMANCE_DRAG_3G)) != + 0); + perfFlags.setMenuAnimations( + cursor.getInt(cursor.getColumnIndex(KEY_PERFORMANCE_MENU_ANIMATIONS_3G)) != 0); + perfFlags.setFontSmoothing(cursor.getInt(cursor.getColumnIndex(KEY_PERFORMANCE_FONTS_3G)) != + 0); + perfFlags.setDesktopComposition( + cursor.getInt(cursor.getColumnIndex(KEY_PERFORMANCE_COMPOSITION_3G)) != 0); + } + + private void fillScreenSettingsContentValues(BookmarkBase.ScreenSettings settings, + ContentValues values) + { + values.put(BookmarkDB.DB_KEY_SCREEN_COLORS, settings.getColors()); + values.put(BookmarkDB.DB_KEY_SCREEN_RESOLUTION, settings.getResolution()); + values.put(BookmarkDB.DB_KEY_SCREEN_WIDTH, settings.getWidth()); + values.put(BookmarkDB.DB_KEY_SCREEN_HEIGHT, settings.getHeight()); + } + + private void fillPerformanceFlagsContentValues(BookmarkBase.PerformanceFlags perfFlags, + ContentValues values) + { + values.put(BookmarkDB.DB_KEY_PERFORMANCE_RFX, perfFlags.getRemoteFX()); + values.put(BookmarkDB.DB_KEY_PERFORMANCE_GFX, perfFlags.getGfx()); + values.put(BookmarkDB.DB_KEY_PERFORMANCE_H264, perfFlags.getH264()); + values.put(BookmarkDB.DB_KEY_PERFORMANCE_WALLPAPER, perfFlags.getWallpaper()); + values.put(BookmarkDB.DB_KEY_PERFORMANCE_THEME, perfFlags.getTheming()); + values.put(BookmarkDB.DB_KEY_PERFORMANCE_DRAG, perfFlags.getFullWindowDrag()); + values.put(BookmarkDB.DB_KEY_PERFORMANCE_MENU_ANIMATIONS, perfFlags.getMenuAnimations()); + values.put(BookmarkDB.DB_KEY_PERFORMANCE_FONTS, perfFlags.getFontSmoothing()); + values.put(BookmarkDB.DB_KEY_PERFORMANCE_COMPOSITION, perfFlags.getDesktopComposition()); + } + + private long insertScreenSettings(SQLiteDatabase db, BookmarkBase.ScreenSettings settings) + { + ContentValues values = new ContentValues(); + fillScreenSettingsContentValues(settings, values); + return db.insertOrThrow(BookmarkDB.DB_TABLE_SCREEN, null, values); + } + + private boolean updateScreenSettings(SQLiteDatabase db, BookmarkBase bookmark) + { + ContentValues values = new ContentValues(); + fillScreenSettingsContentValues(bookmark.getScreenSettings(), values); + String whereClause = BookmarkDB.ID + " IN " + + "(SELECT " + BookmarkDB.DB_KEY_SCREEN_SETTINGS + " FROM " + + getBookmarkTableName() + " WHERE " + BookmarkDB.ID + " = " + + bookmark.getId() + ");"; + return (db.update(BookmarkDB.DB_TABLE_SCREEN, values, whereClause, null) == 1); + } + + private boolean updateScreenSettings3G(SQLiteDatabase db, BookmarkBase bookmark) + { + ContentValues values = new ContentValues(); + fillScreenSettingsContentValues(bookmark.getAdvancedSettings().getScreen3G(), values); + String whereClause = BookmarkDB.ID + " IN " + + "(SELECT " + BookmarkDB.DB_KEY_SCREEN_SETTINGS_3G + " FROM " + + getBookmarkTableName() + " WHERE " + BookmarkDB.ID + " = " + + bookmark.getId() + ");"; + return (db.update(BookmarkDB.DB_TABLE_SCREEN, values, whereClause, null) == 1); + } + + private long insertPerformanceFlags(SQLiteDatabase db, BookmarkBase.PerformanceFlags perfFlags) + { + ContentValues values = new ContentValues(); + fillPerformanceFlagsContentValues(perfFlags, values); + return db.insertOrThrow(BookmarkDB.DB_TABLE_PERFORMANCE, null, values); + } + + private boolean updatePerformanceFlags(SQLiteDatabase db, BookmarkBase bookmark) + { + ContentValues values = new ContentValues(); + fillPerformanceFlagsContentValues(bookmark.getPerformanceFlags(), values); + String whereClause = BookmarkDB.ID + " IN " + + "(SELECT " + BookmarkDB.DB_KEY_PERFORMANCE_FLAGS + " FROM " + + getBookmarkTableName() + " WHERE " + BookmarkDB.ID + " = " + + bookmark.getId() + ");"; + return (db.update(BookmarkDB.DB_TABLE_PERFORMANCE, values, whereClause, null) == 1); + } + + private boolean updatePerformanceFlags3G(SQLiteDatabase db, BookmarkBase bookmark) + { + ContentValues values = new ContentValues(); + fillPerformanceFlagsContentValues(bookmark.getAdvancedSettings().getPerformance3G(), + values); + String whereClause = BookmarkDB.ID + " IN " + + "(SELECT " + BookmarkDB.DB_KEY_PERFORMANCE_FLAGS_3G + " FROM " + + getBookmarkTableName() + " WHERE " + BookmarkDB.ID + " = " + + bookmark.getId() + ");"; + return (db.update(BookmarkDB.DB_TABLE_PERFORMANCE, values, whereClause, null) == 1); + } + + // safety wrappers + // in case of getReadableDatabase it could happen that upgradeDB gets called which is + // a problem if the DB is only readable + private SQLiteDatabase getWritableDatabase() + { + return bookmarkDB.getWritableDatabase(); + } + + private SQLiteDatabase getReadableDatabase() + { + SQLiteDatabase db; + try + { + db = bookmarkDB.getReadableDatabase(); + } + catch (SQLiteException e) + { + db = bookmarkDB.getWritableDatabase(); + } + return db; + } +} diff --git a/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/services/BookmarkDB.java b/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/services/BookmarkDB.java new file mode 100644 index 0000000..420e540 --- /dev/null +++ b/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/services/BookmarkDB.java @@ -0,0 +1,422 @@ +/* + Android Bookmark Database + + Copyright 2013 Thincast Technologies GmbH, Author: Martin Fleisz + + This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. + If a copy of the MPL was not distributed with this file, You can obtain one at + http://mozilla.org/MPL/2.0/. +*/ + +package com.freerdp.freerdpcore.services; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.provider.BaseColumns; +import android.util.Log; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class BookmarkDB extends SQLiteOpenHelper +{ + public static final String ID = BaseColumns._ID; + private static final int DB_VERSION = 9; + private static final String DB_BACKUP_PREFIX = "temp_"; + private static final String DB_NAME = "bookmarks.db"; + static final String DB_TABLE_BOOKMARK = "tbl_manual_bookmarks"; + static final String DB_TABLE_SCREEN = "tbl_screen_settings"; + static final String DB_TABLE_PERFORMANCE = "tbl_performance_flags"; + private static final String[] DB_TABLES = { DB_TABLE_BOOKMARK, DB_TABLE_SCREEN, + DB_TABLE_PERFORMANCE }; + + static final String DB_KEY_SCREEN_COLORS = "colors"; + static final String DB_KEY_SCREEN_RESOLUTION = "resolution"; + static final String DB_KEY_SCREEN_WIDTH = "width"; + static final String DB_KEY_SCREEN_HEIGHT = "height"; + + static final String DB_KEY_SCREEN_SETTINGS = "screen_settings"; + static final String DB_KEY_SCREEN_SETTINGS_3G = "screen_3g"; + static final String DB_KEY_PERFORMANCE_FLAGS = "performance_flags"; + static final String DB_KEY_PERFORMANCE_FLAGS_3G = "performance_3g"; + + static final String DB_KEY_PERFORMANCE_RFX = "perf_remotefx"; + static final String DB_KEY_PERFORMANCE_GFX = "perf_gfx"; + static final String DB_KEY_PERFORMANCE_H264 = "perf_gfx_h264"; + static final String DB_KEY_PERFORMANCE_WALLPAPER = "perf_wallpaper"; + static final String DB_KEY_PERFORMANCE_THEME = "perf_theming"; + static final String DB_KEY_PERFORMANCE_DRAG = "perf_full_window_drag"; + static final String DB_KEY_PERFORMANCE_MENU_ANIMATIONS = "perf_menu_animations"; + static final String DB_KEY_PERFORMANCE_FONTS = "perf_font_smoothing"; + static final String DB_KEY_PERFORMANCE_COMPOSITION = "perf_desktop_composition"; + + static final String DB_KEY_BOOKMARK_LABEL = "label"; + static final String DB_KEY_BOOKMARK_HOSTNAME = "hostname"; + static final String DB_KEY_BOOKMARK_USERNAME = "username"; + static final String DB_KEY_BOOKMARK_PASSWORD = "password"; + static final String DB_KEY_BOOKMARK_DOMAIN = "domain"; + static final String DB_KEY_BOOKMARK_PORT = "port"; + + static final String DB_KEY_BOOKMARK_REDIRECT_SDCARD = "redirect_sdcard"; + static final String DB_KEY_BOOKMARK_REDIRECT_SOUND = "redirect_sound"; + static final String DB_KEY_BOOKMARK_REDIRECT_MICROPHONE = "redirect_microphone"; + static final String DB_KEY_BOOKMARK_SECURITY = "security"; + static final String DB_KEY_BOOKMARK_REMOTE_PROGRAM = "remote_program"; + static final String DB_KEY_BOOKMARK_WORK_DIR = "work_dir"; + static final String DB_KEY_BOOKMARK_ASYNC_CHANNEL = "async_channel"; + static final String DB_KEY_BOOKMARK_ASYNC_INPUT = "async_input"; + static final String DB_KEY_BOOKMARK_ASYNC_UPDATE = "async_update"; + static final String DB_KEY_BOOKMARK_CONSOLE_MODE = "console_mode"; + static final String DB_KEY_BOOKMARK_DEBUG_LEVEL = "debug_level"; + + static final String DB_KEY_BOOKMARK_GW_ENABLE = "enable_gateway_settings"; + static final String DB_KEY_BOOKMARK_GW_HOSTNAME = "gateway_hostname"; + static final String DB_KEY_BOOKMARK_GW_PORT = "gateway_port"; + static final String DB_KEY_BOOKMARK_GW_USERNAME = "gateway_username"; + static final String DB_KEY_BOOKMARK_GW_PASSWORD = "gateway_password"; + static final String DB_KEY_BOOKMARK_GW_DOMAIN = "gateway_domain"; + static final String DB_KEY_BOOKMARK_3G_ENABLE = "enable_3g_settings"; + + public BookmarkDB(Context context) + { + super(context, DB_NAME, null, DB_VERSION); + } + + private static List GetColumns(SQLiteDatabase db, String tableName) + { + List ar = null; + Cursor c = null; + try + { + c = db.rawQuery("SELECT * FROM " + tableName + " LIMIT 1", null); + if (c != null) + { + ar = new ArrayList<>(Arrays.asList(c.getColumnNames())); + } + } + catch (Exception e) + { + Log.v(tableName, e.getMessage(), e); + e.printStackTrace(); + } + finally + { + if (c != null) + c.close(); + } + return ar; + } + + private static String joinStrings(List list, String delim) + { + StringBuilder buf = new StringBuilder(); + int num = list.size(); + for (int i = 0; i < num; i++) + { + if (i != 0) + buf.append(delim); + buf.append((String)list.get(i)); + } + return buf.toString(); + } + + private void backupTables(SQLiteDatabase db) + { + for (String table : DB_TABLES) + { + final String tmpTable = DB_BACKUP_PREFIX + table; + final String query = "ALTER TABLE '" + table + "' RENAME TO '" + tmpTable + "'"; + try + { + db.execSQL(query); + } + catch (Exception e) + { + /* Ignore errors if table does not exist. */ + } + } + } + + private void dropOldTables(SQLiteDatabase db) + { + for (String table : DB_TABLES) + { + final String tmpTable = DB_BACKUP_PREFIX + table; + final String query = "DROP TABLE IF EXISTS '" + tmpTable + "'"; + db.execSQL(query); + } + } + + private void createDB(SQLiteDatabase db) + { + final String sqlScreenSettings = + "CREATE TABLE IF NOT EXISTS " + DB_TABLE_SCREEN + " (" + ID + " INTEGER PRIMARY KEY, " + + DB_KEY_SCREEN_COLORS + " INTEGER DEFAULT 16, " + DB_KEY_SCREEN_RESOLUTION + + " INTEGER DEFAULT 0, " + DB_KEY_SCREEN_WIDTH + ", " + DB_KEY_SCREEN_HEIGHT + ");"; + + db.execSQL(sqlScreenSettings); + + final String sqlPerformanceFlags = + "CREATE TABLE IF NOT EXISTS " + DB_TABLE_PERFORMANCE + " (" + ID + + " INTEGER PRIMARY KEY, " + DB_KEY_PERFORMANCE_RFX + " INTEGER, " + + DB_KEY_PERFORMANCE_GFX + " INTEGER, " + DB_KEY_PERFORMANCE_H264 + " INTEGER, " + + DB_KEY_PERFORMANCE_WALLPAPER + " INTEGER, " + DB_KEY_PERFORMANCE_THEME + " INTEGER, " + + DB_KEY_PERFORMANCE_DRAG + " INTEGER, " + DB_KEY_PERFORMANCE_MENU_ANIMATIONS + + " INTEGER, " + DB_KEY_PERFORMANCE_FONTS + " INTEGER, " + + DB_KEY_PERFORMANCE_COMPOSITION + " INTEGER);"; + + db.execSQL(sqlPerformanceFlags); + + final String sqlManualBookmarks = getManualBookmarksCreationString(); + db.execSQL(sqlManualBookmarks); + } + + private void upgradeTables(SQLiteDatabase db) + { + for (String table : DB_TABLES) + { + final String tmpTable = DB_BACKUP_PREFIX + table; + + final List newColumns = GetColumns(db, table); + List columns = GetColumns(db, tmpTable); + + if (columns != null) + { + columns.retainAll(newColumns); + + // restore data + final String cols = joinStrings(columns, ","); + final String query = String.format("INSERT INTO %s (%s) SELECT %s from '%s'", table, + cols, cols, tmpTable); + db.execSQL(query); + } + } + } + + private void downgradeTables(SQLiteDatabase db) + { + for (String table : DB_TABLES) + { + final String tmpTable = DB_BACKUP_PREFIX + table; + + List oldColumns = GetColumns(db, table); + final List columns = GetColumns(db, tmpTable); + + if (oldColumns != null) + { + oldColumns.retainAll(columns); + + // restore data + final String cols = joinStrings(oldColumns, ","); + final String query = String.format("INSERT INTO %s (%s) SELECT %s from '%s'", table, + cols, cols, tmpTable); + db.execSQL(query); + } + } + } + + private List getTableNames(SQLiteDatabase db) + { + final String query = "SELECT name FROM sqlite_master WHERE type='table'"; + Cursor cursor = db.rawQuery(query, null); + List list = new ArrayList<>(); + try + { + if (cursor.moveToFirst() && (cursor.getCount() > 0)) + { + while (!cursor.isAfterLast()) + { + final String name = cursor.getString(cursor.getColumnIndex("name")); + list.add(name); + cursor.moveToNext(); + } + } + } + finally + { + cursor.close(); + } + + return list; + } + + private void insertDefault(SQLiteDatabase db) + { + ContentValues screenValues = new ContentValues(); + screenValues.put(DB_KEY_SCREEN_COLORS, 32); + screenValues.put(DB_KEY_SCREEN_RESOLUTION, 1); + screenValues.put(DB_KEY_SCREEN_WIDTH, 1024); + screenValues.put(DB_KEY_SCREEN_HEIGHT, 768); + + final long idScreen = db.insert(DB_TABLE_SCREEN, null, screenValues); + final long idScreen3g = db.insert(DB_TABLE_SCREEN, null, screenValues); + + ContentValues performanceValues = new ContentValues(); + performanceValues.put(DB_KEY_PERFORMANCE_RFX, 1); + performanceValues.put(DB_KEY_PERFORMANCE_GFX, 1); + performanceValues.put(DB_KEY_PERFORMANCE_H264, 0); + performanceValues.put(DB_KEY_PERFORMANCE_WALLPAPER, 0); + performanceValues.put(DB_KEY_PERFORMANCE_THEME, 0); + performanceValues.put(DB_KEY_PERFORMANCE_DRAG, 0); + performanceValues.put(DB_KEY_PERFORMANCE_MENU_ANIMATIONS, 0); + performanceValues.put(DB_KEY_PERFORMANCE_FONTS, 0); + performanceValues.put(DB_KEY_PERFORMANCE_COMPOSITION, 0); + + final long idPerformance = db.insert(DB_TABLE_PERFORMANCE, null, performanceValues); + final long idPerformance3g = db.insert(DB_TABLE_PERFORMANCE, null, performanceValues); + + ContentValues bookmarkValues = new ContentValues(); + bookmarkValues.put(DB_KEY_BOOKMARK_LABEL, "Test Server"); + bookmarkValues.put(DB_KEY_BOOKMARK_HOSTNAME, "testservice.afreerdp.com"); + bookmarkValues.put(DB_KEY_BOOKMARK_USERNAME, ""); + bookmarkValues.put(DB_KEY_BOOKMARK_PASSWORD, ""); + bookmarkValues.put(DB_KEY_BOOKMARK_DOMAIN, ""); + bookmarkValues.put(DB_KEY_BOOKMARK_PORT, "3389"); + + bookmarkValues.put(DB_KEY_SCREEN_SETTINGS, idScreen); + bookmarkValues.put(DB_KEY_SCREEN_SETTINGS_3G, idScreen3g); + bookmarkValues.put(DB_KEY_PERFORMANCE_FLAGS, idPerformance); + bookmarkValues.put(DB_KEY_PERFORMANCE_FLAGS_3G, idPerformance3g); + + bookmarkValues.put(DB_KEY_BOOKMARK_REDIRECT_SDCARD, 0); + bookmarkValues.put(DB_KEY_BOOKMARK_REDIRECT_SOUND, 0); + bookmarkValues.put(DB_KEY_BOOKMARK_REDIRECT_MICROPHONE, 0); + bookmarkValues.put(DB_KEY_BOOKMARK_SECURITY, 0); + bookmarkValues.put(DB_KEY_BOOKMARK_REMOTE_PROGRAM, ""); + bookmarkValues.put(DB_KEY_BOOKMARK_WORK_DIR, ""); + bookmarkValues.put(DB_KEY_BOOKMARK_ASYNC_CHANNEL, 1); + bookmarkValues.put(DB_KEY_BOOKMARK_ASYNC_INPUT, 1); + bookmarkValues.put(DB_KEY_BOOKMARK_ASYNC_UPDATE, 1); + bookmarkValues.put(DB_KEY_BOOKMARK_CONSOLE_MODE, 0); + bookmarkValues.put(DB_KEY_BOOKMARK_DEBUG_LEVEL, "INFO"); + + db.insert(DB_TABLE_BOOKMARK, null, bookmarkValues); + } + + @Override public void onCreate(SQLiteDatabase db) + { + createDB(db); + insertDefault(db); + } + + private String getManualBookmarksCreationString() + { + return ("CREATE TABLE IF NOT EXISTS " + DB_TABLE_BOOKMARK + " (" + ID + + " INTEGER PRIMARY KEY, " + DB_KEY_BOOKMARK_LABEL + " TEXT NOT NULL, " + + DB_KEY_BOOKMARK_HOSTNAME + " TEXT NOT NULL, " + DB_KEY_BOOKMARK_USERNAME + + " TEXT NOT NULL, " + DB_KEY_BOOKMARK_PASSWORD + " TEXT, " + DB_KEY_BOOKMARK_DOMAIN + + " TEXT, " + DB_KEY_BOOKMARK_PORT + " TEXT, " + DB_KEY_SCREEN_SETTINGS + + " INTEGER NOT NULL, " + DB_KEY_PERFORMANCE_FLAGS + " INTEGER NOT NULL, " + + + DB_KEY_BOOKMARK_GW_ENABLE + " INTEGER DEFAULT 0, " + DB_KEY_BOOKMARK_GW_HOSTNAME + + " TEXT, " + DB_KEY_BOOKMARK_GW_PORT + " INTEGER DEFAULT 443, " + + DB_KEY_BOOKMARK_GW_USERNAME + " TEXT, " + DB_KEY_BOOKMARK_GW_PASSWORD + " TEXT, " + + DB_KEY_BOOKMARK_GW_DOMAIN + " TEXT, " + + + DB_KEY_BOOKMARK_3G_ENABLE + " INTEGER DEFAULT 0, " + DB_KEY_SCREEN_SETTINGS_3G + + " INTEGER NOT NULL, " + DB_KEY_PERFORMANCE_FLAGS_3G + " INTEGER NOT NULL, " + + DB_KEY_BOOKMARK_REDIRECT_SDCARD + " INTEGER DEFAULT 0, " + + DB_KEY_BOOKMARK_REDIRECT_SOUND + " INTEGER DEFAULT 0, " + + DB_KEY_BOOKMARK_REDIRECT_MICROPHONE + " INTEGER DEFAULT 0, " + + DB_KEY_BOOKMARK_SECURITY + " INTEGER, " + DB_KEY_BOOKMARK_REMOTE_PROGRAM + + " TEXT, " + DB_KEY_BOOKMARK_WORK_DIR + " TEXT, " + DB_KEY_BOOKMARK_ASYNC_CHANNEL + + " INTEGER DEFAULT 0, " + DB_KEY_BOOKMARK_ASYNC_INPUT + " INTEGER DEFAULT 0, " + + DB_KEY_BOOKMARK_ASYNC_UPDATE + " INTEGER DEFAULT 0, " + + DB_KEY_BOOKMARK_CONSOLE_MODE + " INTEGER, " + DB_KEY_BOOKMARK_DEBUG_LEVEL + + " TEXT DEFAULT 'INFO', " + + + "FOREIGN KEY(" + DB_KEY_SCREEN_SETTINGS + ") REFERENCES " + DB_TABLE_SCREEN + + "(" + ID + "), " + + "FOREIGN KEY(" + DB_KEY_PERFORMANCE_FLAGS + ") REFERENCES " + + DB_TABLE_PERFORMANCE + "(" + ID + "), " + + "FOREIGN KEY(" + DB_KEY_SCREEN_SETTINGS_3G + ") REFERENCES " + DB_TABLE_SCREEN + + "(" + ID + "), " + + "FOREIGN KEY(" + DB_KEY_PERFORMANCE_FLAGS_3G + ") REFERENCES " + + DB_TABLE_PERFORMANCE + "(" + ID + ") " + + + ");"); + } + + private void recreateDB(SQLiteDatabase db) + { + for (String table : DB_TABLES) + { + final String query = "DROP TABLE IF EXISTS '" + table + "'"; + db.execSQL(query); + } + onCreate(db); + } + + private void upgradeDB(SQLiteDatabase db) + { + db.beginTransaction(); + try + { + /* Back up old tables. */ + dropOldTables(db); + backupTables(db); + createDB(db); + upgradeTables(db); + + db.setTransactionSuccessful(); + } + finally + { + db.endTransaction(); + dropOldTables(db); + } + } + + private void downgradeDB(SQLiteDatabase db) + { + db.beginTransaction(); + try + { + /* Back up old tables. */ + dropOldTables(db); + backupTables(db); + createDB(db); + downgradeTables(db); + + db.setTransactionSuccessful(); + } + finally + { + db.endTransaction(); + dropOldTables(db); + } + } + + // from + // http://stackoverflow.com/questions/3424156/upgrade-sqlite-database-from-one-version-to-another + @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) + { + switch (oldVersion) + { + case 0: + case 1: + case 2: + case 3: + case 4: + case 5: + case 6: + case 7: + case 8: + case 9: + upgradeDB(db); + break; + default: + recreateDB(db); + break; + } + } + + @Override public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) + { + downgradeDB(db); + } +} diff --git a/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/services/FreeRDPSuggestionProvider.java b/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/services/FreeRDPSuggestionProvider.java new file mode 100644 index 0000000..d5f657c --- /dev/null +++ b/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/services/FreeRDPSuggestionProvider.java @@ -0,0 +1,134 @@ +/* + Suggestion Provider for RDP bookmarks + + Copyright 2013 Thincast Technologies GmbH, Author: Martin Fleisz + + This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. + If a copy of the MPL was not distributed with this file, You can obtain one at + http://mozilla.org/MPL/2.0/. +*/ + +package com.freerdp.freerdpcore.services; + +import android.app.SearchManager; +import android.content.ContentProvider; +import android.content.ContentValues; +import android.database.Cursor; +import android.database.MatrixCursor; +import android.net.Uri; + +import com.freerdp.freerdpcore.R; +import com.freerdp.freerdpcore.application.GlobalApp; +import com.freerdp.freerdpcore.domain.BookmarkBase; +import com.freerdp.freerdpcore.domain.ConnectionReference; +import com.freerdp.freerdpcore.domain.ManualBookmark; + +import java.util.ArrayList; + +public class FreeRDPSuggestionProvider extends ContentProvider +{ + + public static final Uri CONTENT_URI = + Uri.parse("content://com.freerdp.afreerdp.services.freerdpsuggestionprovider"); + + @Override public int delete(Uri uri, String selection, String[] selectionArgs) + { + // TODO Auto-generated method stub + return 0; + } + + @Override public String getType(Uri uri) + { + return "vnd.android.cursor.item/vnd.freerdp.remote"; + } + + @Override public Uri insert(Uri uri, ContentValues values) + { + // TODO Auto-generated method stub + return null; + } + + @Override public boolean onCreate() + { + return true; + } + + @Override + public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, + String sortOrder) + { + + String query = (selectionArgs != null && selectionArgs.length > 0) ? selectionArgs[0] : ""; + + // search history + ArrayList history = + GlobalApp.getQuickConnectHistoryGateway().findHistory(query); + + // search bookmarks + ArrayList manualBookmarks; + if (query.length() > 0) + manualBookmarks = GlobalApp.getManualBookmarkGateway().findByLabelOrHostnameLike(query); + else + manualBookmarks = GlobalApp.getManualBookmarkGateway().findAll(); + + return createResultCursor(history, manualBookmarks); + } + + @Override + public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) + { + // TODO Auto-generated method stub + return 0; + } + + private void addBookmarksToCursor(ArrayList bookmarks, MatrixCursor resultCursor) + { + Object[] row = new Object[5]; + for (BookmarkBase bookmark : bookmarks) + { + row[0] = new Long(bookmark.getId()); + row[1] = bookmark.getLabel(); + row[2] = bookmark.get().getHostname(); + row[3] = ConnectionReference.getManualBookmarkReference(bookmark.getId()); + row[4] = "android.resource://" + getContext().getPackageName() + "/" + + R.drawable.icon_star_on; + resultCursor.addRow(row); + } + } + + private void addHistoryToCursor(ArrayList history, MatrixCursor resultCursor) + { + Object[] row = new Object[5]; + for (BookmarkBase bookmark : history) + { + row[0] = new Integer(1); + row[1] = bookmark.getLabel(); + row[2] = bookmark.getLabel(); + row[3] = ConnectionReference.getHostnameReference(bookmark.getLabel()); + row[4] = "android.resource://" + getContext().getPackageName() + "/" + + R.drawable.icon_star_off; + resultCursor.addRow(row); + } + } + + private Cursor createResultCursor(ArrayList history, + ArrayList manualBookmarks) + { + + // create result matrix cursor + int totalCount = history.size() + manualBookmarks.size(); + String[] columns = { android.provider.BaseColumns._ID, SearchManager.SUGGEST_COLUMN_TEXT_1, + SearchManager.SUGGEST_COLUMN_TEXT_2, + SearchManager.SUGGEST_COLUMN_INTENT_DATA, + SearchManager.SUGGEST_COLUMN_ICON_2 }; + MatrixCursor matrixCursor = new MatrixCursor(columns, totalCount); + + // populate result matrix + if (totalCount > 0) + { + addHistoryToCursor(history, matrixCursor); + addBookmarksToCursor(manualBookmarks, matrixCursor); + } + return matrixCursor; + } +} diff --git a/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/services/HistoryDB.java b/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/services/HistoryDB.java new file mode 100644 index 0000000..b483aac --- /dev/null +++ b/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/services/HistoryDB.java @@ -0,0 +1,46 @@ +/* + Quick Connect History Database + + Copyright 2013 Thincast Technologies GmbH, Author: Martin Fleisz + + This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. + If a copy of the MPL was not distributed with this file, You can obtain one at + http://mozilla.org/MPL/2.0/. +*/ + +package com.freerdp.freerdpcore.services; + +import android.content.Context; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; + +public class HistoryDB extends SQLiteOpenHelper +{ + + public static final String QUICK_CONNECT_TABLE_NAME = "quick_connect_history"; + public static final String QUICK_CONNECT_TABLE_COL_ITEM = "item"; + public static final String QUICK_CONNECT_TABLE_COL_TIMESTAMP = "timestamp"; + private static final int DB_VERSION = 1; + private static final String DB_NAME = "history.db"; + + public HistoryDB(Context context) + { + super(context, DB_NAME, null, DB_VERSION); + } + + @Override public void onCreate(SQLiteDatabase db) + { + + String sqlQuickConnectHistory = "CREATE TABLE " + QUICK_CONNECT_TABLE_NAME + " (" + + QUICK_CONNECT_TABLE_COL_ITEM + " TEXT PRIMARY KEY, " + + QUICK_CONNECT_TABLE_COL_TIMESTAMP + " INTEGER" + + ");"; + + db.execSQL(sqlQuickConnectHistory); + } + + @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) + { + // TODO Auto-generated method stub + } +} diff --git a/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/services/LibFreeRDP.java b/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/services/LibFreeRDP.java new file mode 100644 index 0000000..0f4a921 --- /dev/null +++ b/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/services/LibFreeRDP.java @@ -0,0 +1,656 @@ +/* + Android FreeRDP JNI Wrapper + + Copyright 2013 Thincast Technologies GmbH, Author: Martin Fleisz + + This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. + If a copy of the MPL was not distributed with this file, You can obtain one at + http://mozilla.org/MPL/2.0/. +*/ + +package com.freerdp.freerdpcore.services; + +import android.content.Context; +import android.graphics.Bitmap; +import android.net.Uri; +import androidx.collection.LongSparseArray; +import android.util.Log; + +import com.freerdp.freerdpcore.application.GlobalApp; +import com.freerdp.freerdpcore.application.SessionState; +import com.freerdp.freerdpcore.domain.BookmarkBase; +import com.freerdp.freerdpcore.domain.ManualBookmark; +import com.freerdp.freerdpcore.presentation.ApplicationSettingsActivity; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.regex.MatchResult; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class LibFreeRDP +{ + private static final String TAG = "LibFreeRDP"; + private static EventListener listener; + private static boolean mHasH264 = false; + + private static final LongSparseArray mInstanceState = new LongSparseArray<>(); + + private static boolean tryLoad(String[] libraries) + { + boolean success = false; + final String LD_PATH = System.getProperty("java.library.path"); + + for (String lib : libraries) + { + try + { + Log.v(TAG, "Trying to load library " + lib + " from LD_PATH: " + LD_PATH); + System.loadLibrary(lib); + success = true; + } + catch (UnsatisfiedLinkError e) + { + Log.e(TAG, "Failed to load library " + lib + ": " + e.toString()); + success = false; + break; + } + } + + return success; + } + + private static boolean tryLoad(String library) + { + return tryLoad(new String[] { library }); + } + static + { + try + { + System.loadLibrary("freerdp-android"); + String version = freerdp_get_jni_version(); + Pattern pattern = Pattern.compile("^(\\d+)\\.(\\d+)\\.(\\d+).*"); + Matcher matcher = pattern.matcher(version); + if (!matcher.matches() || (matcher.groupCount() < 3)) + throw new RuntimeException("APK broken: native library version " + version + + " does not meet requirements!"); + int major = Integer.parseInt(Objects.requireNonNull(matcher.group(1))); + int minor = Integer.parseInt(Objects.requireNonNull(matcher.group(2))); + int patch = Integer.parseInt(Objects.requireNonNull(matcher.group(3))); + + if (major > 2) + mHasH264 = freerdp_has_h264(); + else if (minor > 5) + mHasH264 = freerdp_has_h264(); + else if ((minor == 5) && (patch >= 1)) + mHasH264 = freerdp_has_h264(); + else + throw new RuntimeException("APK broken: native library version " + version + + " does not meet requirements!"); + Log.i(TAG, "Successfully loaded native library. H264 is " + + (mHasH264 ? "supported" : "not available")); + } + catch (UnsatisfiedLinkError e) + { + Log.e(TAG, "Failed to load library: " + e.toString()); + throw e; + } + } + + public static boolean hasH264Support() + { + return mHasH264; + } + + private static native boolean freerdp_has_h264(); + + private static native String freerdp_get_jni_version(); + + private static native String freerdp_get_version(); + + private static native String freerdp_get_build_date(); + + private static native String freerdp_get_build_revision(); + + private static native String freerdp_get_build_config(); + + private static native long freerdp_new(Context context); + + private static native void freerdp_free(long inst); + + private static native boolean freerdp_parse_arguments(long inst, String[] args); + + private static native boolean freerdp_connect(long inst); + + private static native boolean freerdp_disconnect(long inst); + + private static native boolean freerdp_update_graphics(long inst, Bitmap bitmap, int x, int y, + int width, int height); + + private static native boolean freerdp_send_cursor_event(long inst, int x, int y, int flags); + + private static native boolean freerdp_send_key_event(long inst, int keycode, boolean down); + + private static native boolean freerdp_send_unicodekey_event(long inst, int keycode, + boolean down); + + private static native boolean freerdp_send_clipboard_data(long inst, String data); + + private static native String freerdp_get_last_error_string(long inst); + + public static void setEventListener(EventListener l) + { + listener = l; + } + + public static long newInstance(Context context) + { + return freerdp_new(context); + } + + public static void freeInstance(long inst) + { + synchronized (mInstanceState) + { + if (mInstanceState.get(inst, false)) + { + freerdp_disconnect(inst); + } + while (mInstanceState.get(inst, false)) + { + try + { + mInstanceState.wait(); + } + catch (InterruptedException e) + { + throw new RuntimeException(); + } + } + } + freerdp_free(inst); + } + + public static boolean connect(long inst) + { + synchronized (mInstanceState) + { + if (mInstanceState.get(inst, false)) + { + throw new RuntimeException("instance already connected"); + } + } + return freerdp_connect(inst); + } + + public static boolean disconnect(long inst) + { + synchronized (mInstanceState) + { + if (mInstanceState.get(inst, false)) + { + return freerdp_disconnect(inst); + } + return true; + } + } + + public static boolean cancelConnection(long inst) + { + synchronized (mInstanceState) + { + if (mInstanceState.get(inst, false)) + { + return freerdp_disconnect(inst); + } + return true; + } + } + + private static String addFlag(String name, boolean enabled) + { + if (enabled) + { + return "+" + name; + } + return "-" + name; + } + + public static boolean setConnectionInfo(Context context, long inst, BookmarkBase bookmark) + { + BookmarkBase.ScreenSettings screenSettings = bookmark.getActiveScreenSettings(); + BookmarkBase.AdvancedSettings advanced = bookmark.getAdvancedSettings(); + BookmarkBase.DebugSettings debug = bookmark.getDebugSettings(); + + String arg; + ArrayList args = new ArrayList(); + + args.add(TAG); + args.add("/gdi:sw"); + + final String clientName = ApplicationSettingsActivity.getClientName(context); + if (!clientName.isEmpty()) + { + args.add("/client-hostname:" + clientName); + } + String certName = ""; + if (bookmark.getType() != BookmarkBase.TYPE_MANUAL) + { + return false; + } + + int port = bookmark.get().getPort(); + String hostname = bookmark.get().getHostname(); + + args.add("/v:" + hostname); + args.add("/port:" + String.valueOf(port)); + + arg = bookmark.getUsername(); + if (!arg.isEmpty()) + { + args.add("/u:" + arg); + } + arg = bookmark.getDomain(); + if (!arg.isEmpty()) + { + args.add("/d:" + arg); + } + arg = bookmark.getPassword(); + if (!arg.isEmpty()) + { + args.add("/p:" + arg); + } + + args.add( + String.format("/size:%dx%d", screenSettings.getWidth(), screenSettings.getHeight())); + args.add("/bpp:" + String.valueOf(screenSettings.getColors())); + + if (advanced.getConsoleMode()) + { + args.add("/admin"); + } + + switch (advanced.getSecurity()) + { + case 3: // NLA + args.add("/sec-nla"); + break; + case 2: // TLS + args.add("/sec-tls"); + break; + case 1: // RDP + args.add("/sec-rdp"); + break; + default: + break; + } + + if (!certName.isEmpty()) + { + args.add("/cert-name:" + certName); + } + + BookmarkBase.PerformanceFlags flags = bookmark.getActivePerformanceFlags(); + if (flags.getRemoteFX()) + { + args.add("/rfx"); + } + + if (flags.getGfx()) + { + args.add("/gfx"); + } + + if (flags.getH264() && mHasH264) + { + args.add("/gfx:AVC444"); + } + + args.add(addFlag("wallpaper", flags.getWallpaper())); + args.add(addFlag("window-drag", flags.getFullWindowDrag())); + args.add(addFlag("menu-anims", flags.getMenuAnimations())); + args.add(addFlag("themes", flags.getTheming())); + args.add(addFlag("fonts", flags.getFontSmoothing())); + args.add(addFlag("aero", flags.getDesktopComposition())); + args.add(addFlag("glyph-cache", false)); + + if (!advanced.getRemoteProgram().isEmpty()) + { + args.add("/shell:" + advanced.getRemoteProgram()); + } + + if (!advanced.getWorkDir().isEmpty()) + { + args.add("/shell-dir:" + advanced.getWorkDir()); + } + + args.add(addFlag("async-channels", debug.getAsyncChannel())); + args.add(addFlag("async-input", debug.getAsyncInput())); + args.add(addFlag("async-update", debug.getAsyncUpdate())); + + if (advanced.getRedirectSDCard()) + { + String path = android.os.Environment.getExternalStorageDirectory().getPath(); + args.add("/drive:sdcard," + path); + } + + args.add("/clipboard"); + + // Gateway enabled? + if (bookmark.getType() == BookmarkBase.TYPE_MANUAL && + bookmark.get().getEnableGatewaySettings()) + { + ManualBookmark.GatewaySettings gateway = + bookmark.get().getGatewaySettings(); + + args.add(String.format("/g:%s:%d", gateway.getHostname(), gateway.getPort())); + + arg = gateway.getUsername(); + if (!arg.isEmpty()) + { + args.add("/gu:" + arg); + } + arg = gateway.getDomain(); + if (!arg.isEmpty()) + { + args.add("/gd:" + arg); + } + arg = gateway.getPassword(); + if (!arg.isEmpty()) + { + args.add("/gp:" + arg); + } + } + + /* 0 ... local + 1 ... remote + 2 ... disable */ + args.add("/audio-mode:" + String.valueOf(advanced.getRedirectSound())); + if (advanced.getRedirectSound() == 0) + { + args.add("/sound"); + } + + if (advanced.getRedirectMicrophone()) + { + args.add("/microphone"); + } + + args.add("/cert-ignore"); + args.add("/log-level:" + debug.getDebugLevel()); + String[] arrayArgs = args.toArray(new String[args.size()]); + return freerdp_parse_arguments(inst, arrayArgs); + } + + public static boolean setConnectionInfo(Context context, long inst, Uri openUri) + { + ArrayList args = new ArrayList<>(); + + // Parse URI from query string. Same key overwrite previous one + // freerdp://user@ip:port/connect?sound=&rfx=&p=password&clipboard=%2b&themes=- + + // Now we only support Software GDI + args.add(TAG); + args.add("/gdi:sw"); + + final String clientName = ApplicationSettingsActivity.getClientName(context); + if (!clientName.isEmpty()) + { + args.add("/client-hostname:" + clientName); + } + + // Parse hostname and port. Set to 'v' argument + String hostname = openUri.getHost(); + int port = openUri.getPort(); + if (hostname != null) + { + hostname = hostname + ((port == -1) ? "" : (":" + String.valueOf(port))); + args.add("/v:" + hostname); + } + + String user = openUri.getUserInfo(); + if (user != null) + { + args.add("/u:" + user); + } + + for (String key : openUri.getQueryParameterNames()) + { + String value = openUri.getQueryParameter(key); + + if (value.isEmpty()) + { + // Query: key= + // To freerdp argument: /key + args.add("/" + key); + } + else if (value.equals("-") || value.equals("+")) + { + // Query: key=- or key=+ + // To freerdp argument: -key or +key + args.add(value + key); + } + else + { + // Query: key=value + // To freerdp argument: /key:value + if (key.equals("drive") && value.equals("sdcard")) + { + // Special for sdcard redirect + String path = android.os.Environment.getExternalStorageDirectory().getPath(); + value = "sdcard," + path; + } + + args.add("/" + key + ":" + value); + } + } + + String[] arrayArgs = args.toArray(new String[args.size()]); + return freerdp_parse_arguments(inst, arrayArgs); + } + + public static boolean updateGraphics(long inst, Bitmap bitmap, int x, int y, int width, + int height) + { + return freerdp_update_graphics(inst, bitmap, x, y, width, height); + } + + public static boolean sendCursorEvent(long inst, int x, int y, int flags) + { + return freerdp_send_cursor_event(inst, x, y, flags); + } + + public static boolean sendKeyEvent(long inst, int keycode, boolean down) + { + return freerdp_send_key_event(inst, keycode, down); + } + + public static boolean sendUnicodeKeyEvent(long inst, int keycode, boolean down) + { + return freerdp_send_unicodekey_event(inst, keycode, down); + } + + public static boolean sendClipboardData(long inst, String data) + { + return freerdp_send_clipboard_data(inst, data); + } + + private static void OnConnectionSuccess(long inst) + { + if (listener != null) + listener.OnConnectionSuccess(inst); + synchronized (mInstanceState) + { + mInstanceState.append(inst, true); + mInstanceState.notifyAll(); + } + } + + private static void OnConnectionFailure(long inst) + { + if (listener != null) + listener.OnConnectionFailure(inst); + synchronized (mInstanceState) + { + mInstanceState.remove(inst); + mInstanceState.notifyAll(); + } + } + + private static void OnPreConnect(long inst) + { + if (listener != null) + listener.OnPreConnect(inst); + } + + private static void OnDisconnecting(long inst) + { + if (listener != null) + listener.OnDisconnecting(inst); + } + + private static void OnDisconnected(long inst) + { + if (listener != null) + listener.OnDisconnected(inst); + synchronized (mInstanceState) + { + mInstanceState.remove(inst); + mInstanceState.notifyAll(); + } + } + + private static void OnSettingsChanged(long inst, int width, int height, int bpp) + { + SessionState s = GlobalApp.getSession(inst); + if (s == null) + return; + UIEventListener uiEventListener = s.getUIEventListener(); + if (uiEventListener != null) + uiEventListener.OnSettingsChanged(width, height, bpp); + } + + private static boolean OnAuthenticate(long inst, StringBuilder username, StringBuilder domain, + StringBuilder password) + { + SessionState s = GlobalApp.getSession(inst); + if (s == null) + return false; + UIEventListener uiEventListener = s.getUIEventListener(); + if (uiEventListener != null) + return uiEventListener.OnAuthenticate(username, domain, password); + return false; + } + + private static boolean OnGatewayAuthenticate(long inst, StringBuilder username, + StringBuilder domain, StringBuilder password) + { + SessionState s = GlobalApp.getSession(inst); + if (s == null) + return false; + UIEventListener uiEventListener = s.getUIEventListener(); + if (uiEventListener != null) + return uiEventListener.OnGatewayAuthenticate(username, domain, password); + return false; + } + + private static int OnVerifyCertificate(long inst, String commonName, String subject, + String issuer, String fingerprint, boolean hostMismatch) + { + SessionState s = GlobalApp.getSession(inst); + if (s == null) + return 0; + UIEventListener uiEventListener = s.getUIEventListener(); + if (uiEventListener != null) + return uiEventListener.OnVerifiyCertificate(commonName, subject, issuer, fingerprint, + hostMismatch); + return 0; + } + + private static int OnVerifyChangedCertificate(long inst, String commonName, String subject, + String issuer, String fingerprint, + String oldSubject, String oldIssuer, + String oldFingerprint) + { + SessionState s = GlobalApp.getSession(inst); + if (s == null) + return 0; + UIEventListener uiEventListener = s.getUIEventListener(); + if (uiEventListener != null) + return uiEventListener.OnVerifyChangedCertificate( + commonName, subject, issuer, fingerprint, oldSubject, oldIssuer, oldFingerprint); + return 0; + } + + private static void OnGraphicsUpdate(long inst, int x, int y, int width, int height) + { + SessionState s = GlobalApp.getSession(inst); + if (s == null) + return; + UIEventListener uiEventListener = s.getUIEventListener(); + if (uiEventListener != null) + uiEventListener.OnGraphicsUpdate(x, y, width, height); + } + + private static void OnGraphicsResize(long inst, int width, int height, int bpp) + { + SessionState s = GlobalApp.getSession(inst); + if (s == null) + return; + UIEventListener uiEventListener = s.getUIEventListener(); + if (uiEventListener != null) + uiEventListener.OnGraphicsResize(width, height, bpp); + } + + private static void OnRemoteClipboardChanged(long inst, String data) + { + SessionState s = GlobalApp.getSession(inst); + if (s == null) + return; + UIEventListener uiEventListener = s.getUIEventListener(); + if (uiEventListener != null) + uiEventListener.OnRemoteClipboardChanged(data); + } + + public static String getVersion() + { + return freerdp_get_version(); + } + + public static interface EventListener { + void OnPreConnect(long instance); + + void OnConnectionSuccess(long instance); + + void OnConnectionFailure(long instance); + + void OnDisconnecting(long instance); + + void OnDisconnected(long instance); + } + + public static interface UIEventListener { + void OnSettingsChanged(int width, int height, int bpp); + + boolean OnAuthenticate(StringBuilder username, StringBuilder domain, + StringBuilder password); + + boolean OnGatewayAuthenticate(StringBuilder username, StringBuilder domain, + StringBuilder password); + + int OnVerifiyCertificate(String commonName, String subject, String issuer, + String fingerprint, boolean mismatch); + + int OnVerifyChangedCertificate(String commonName, String subject, String issuer, + String fingerprint, String oldSubject, String oldIssuer, + String oldFingerprint); + + void OnGraphicsUpdate(int x, int y, int width, int height); + + void OnGraphicsResize(int width, int height, int bpp); + + void OnRemoteClipboardChanged(String data); + } +} diff --git a/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/services/ManualBookmarkGateway.java b/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/services/ManualBookmarkGateway.java new file mode 100644 index 0000000..2cd2751 --- /dev/null +++ b/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/services/ManualBookmarkGateway.java @@ -0,0 +1,131 @@ +/* + Manual bookmarks database gateway + + Copyright 2013 Thincast Technologies GmbH, Author: Martin Fleisz + + This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. + If a copy of the MPL was not distributed with this file, You can obtain one at + http://mozilla.org/MPL/2.0/. +*/ + +package com.freerdp.freerdpcore.services; + +import android.content.ContentValues; +import android.database.Cursor; +import android.database.sqlite.SQLiteOpenHelper; + +import com.freerdp.freerdpcore.domain.BookmarkBase; +import com.freerdp.freerdpcore.domain.ManualBookmark; + +import java.util.ArrayList; + +public class ManualBookmarkGateway extends BookmarkBaseGateway +{ + + public ManualBookmarkGateway(SQLiteOpenHelper bookmarkDB) + { + super(bookmarkDB); + } + + @Override protected BookmarkBase createBookmark() + { + return new ManualBookmark(); + } + + @Override protected String getBookmarkTableName() + { + return BookmarkDB.DB_TABLE_BOOKMARK; + } + + @Override + protected void addBookmarkSpecificColumns(BookmarkBase bookmark, ContentValues columns) + { + ManualBookmark bm = (ManualBookmark)bookmark; + columns.put(BookmarkDB.DB_KEY_BOOKMARK_HOSTNAME, bm.getHostname()); + columns.put(BookmarkDB.DB_KEY_BOOKMARK_PORT, bm.getPort()); + + // gateway settings + columns.put(BookmarkDB.DB_KEY_BOOKMARK_GW_ENABLE, bm.getEnableGatewaySettings()); + columns.put(BookmarkDB.DB_KEY_BOOKMARK_GW_HOSTNAME, bm.getGatewaySettings().getHostname()); + columns.put(BookmarkDB.DB_KEY_BOOKMARK_GW_PORT, bm.getGatewaySettings().getPort()); + columns.put(BookmarkDB.DB_KEY_BOOKMARK_GW_USERNAME, bm.getGatewaySettings().getUsername()); + columns.put(BookmarkDB.DB_KEY_BOOKMARK_GW_PASSWORD, bm.getGatewaySettings().getPassword()); + columns.put(BookmarkDB.DB_KEY_BOOKMARK_GW_DOMAIN, bm.getGatewaySettings().getDomain()); + } + + @Override protected void addBookmarkSpecificColumns(ArrayList columns) + { + columns.add(BookmarkDB.DB_KEY_BOOKMARK_HOSTNAME); + columns.add(BookmarkDB.DB_KEY_BOOKMARK_PORT); + columns.add(BookmarkDB.DB_KEY_BOOKMARK_GW_ENABLE); + columns.add(BookmarkDB.DB_KEY_BOOKMARK_GW_HOSTNAME); + columns.add(BookmarkDB.DB_KEY_BOOKMARK_GW_PORT); + columns.add(BookmarkDB.DB_KEY_BOOKMARK_GW_USERNAME); + columns.add(BookmarkDB.DB_KEY_BOOKMARK_GW_PASSWORD); + columns.add(BookmarkDB.DB_KEY_BOOKMARK_GW_DOMAIN); + } + + @Override protected void readBookmarkSpecificColumns(BookmarkBase bookmark, Cursor cursor) + { + ManualBookmark bm = (ManualBookmark)bookmark; + bm.setHostname( + cursor.getString(cursor.getColumnIndex(BookmarkDB.DB_KEY_BOOKMARK_HOSTNAME))); + bm.setPort(cursor.getInt(cursor.getColumnIndex(BookmarkDB.DB_KEY_BOOKMARK_PORT))); + + bm.setEnableGatewaySettings( + cursor.getInt(cursor.getColumnIndex(BookmarkDB.DB_KEY_BOOKMARK_GW_ENABLE)) != 0); + readGatewaySettings(bm, cursor); + } + + public BookmarkBase findByLabelOrHostname(String pattern) + { + if (pattern.length() == 0) + return null; + + Cursor cursor = + queryBookmarks(BookmarkDB.DB_KEY_BOOKMARK_LABEL + " = '" + pattern + "' OR " + + BookmarkDB.DB_KEY_BOOKMARK_HOSTNAME + " = '" + pattern + "'", + BookmarkDB.DB_KEY_BOOKMARK_LABEL); + BookmarkBase bookmark = null; + if (cursor.moveToFirst() && (cursor.getCount() > 0)) + bookmark = getBookmarkFromCursor(cursor); + + cursor.close(); + return bookmark; + } + + public ArrayList findByLabelOrHostnameLike(String pattern) + { + Cursor cursor = + queryBookmarks(BookmarkDB.DB_KEY_BOOKMARK_LABEL + " LIKE '%" + pattern + "%' OR " + + BookmarkDB.DB_KEY_BOOKMARK_HOSTNAME + " LIKE '%" + pattern + "%'", + BookmarkDB.DB_KEY_BOOKMARK_LABEL); + ArrayList bookmarks = new ArrayList(cursor.getCount()); + + if (cursor.moveToFirst() && (cursor.getCount() > 0)) + { + do + { + bookmarks.add(getBookmarkFromCursor(cursor)); + } while (cursor.moveToNext()); + } + + cursor.close(); + return bookmarks; + } + + private void readGatewaySettings(ManualBookmark bookmark, Cursor cursor) + { + ManualBookmark.GatewaySettings gatewaySettings = bookmark.getGatewaySettings(); + gatewaySettings.setHostname( + cursor.getString(cursor.getColumnIndex(BookmarkDB.DB_KEY_BOOKMARK_GW_HOSTNAME))); + gatewaySettings.setPort( + cursor.getInt(cursor.getColumnIndex(BookmarkDB.DB_KEY_BOOKMARK_GW_PORT))); + gatewaySettings.setUsername( + cursor.getString(cursor.getColumnIndex(BookmarkDB.DB_KEY_BOOKMARK_GW_USERNAME))); + gatewaySettings.setPassword( + cursor.getString(cursor.getColumnIndex(BookmarkDB.DB_KEY_BOOKMARK_GW_PASSWORD))); + gatewaySettings.setDomain( + cursor.getString(cursor.getColumnIndex(BookmarkDB.DB_KEY_BOOKMARK_GW_DOMAIN))); + } +} diff --git a/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/services/QuickConnectHistoryGateway.java b/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/services/QuickConnectHistoryGateway.java new file mode 100644 index 0000000..4dd5139 --- /dev/null +++ b/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/services/QuickConnectHistoryGateway.java @@ -0,0 +1,121 @@ +/* + Quick connect history gateway + + Copyright 2013 Thincast Technologies GmbH, Author: Martin Fleisz + + This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. + If a copy of the MPL was not distributed with this file, You can obtain one at + http://mozilla.org/MPL/2.0/. +*/ + +package com.freerdp.freerdpcore.services; + +import android.database.Cursor; +import android.database.SQLException; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteException; +import android.database.sqlite.SQLiteOpenHelper; +import android.util.Log; + +import com.freerdp.freerdpcore.domain.BookmarkBase; +import com.freerdp.freerdpcore.domain.QuickConnectBookmark; + +import java.util.ArrayList; + +public class QuickConnectHistoryGateway +{ + private final static String TAG = "QuickConnectHistoryGateway"; + private SQLiteOpenHelper historyDB; + + public QuickConnectHistoryGateway(SQLiteOpenHelper historyDB) + { + this.historyDB = historyDB; + } + + public ArrayList findHistory(String filter) + { + String[] column = { HistoryDB.QUICK_CONNECT_TABLE_COL_ITEM }; + + SQLiteDatabase db = getReadableDatabase(); + String selection = + (filter.length() > 0) + ? (HistoryDB.QUICK_CONNECT_TABLE_COL_ITEM + " LIKE '%" + filter + "%'") + : null; + Cursor cursor = db.query(HistoryDB.QUICK_CONNECT_TABLE_NAME, column, selection, null, null, + null, HistoryDB.QUICK_CONNECT_TABLE_COL_TIMESTAMP); + + ArrayList result = new ArrayList(cursor.getCount()); + if (cursor.moveToFirst()) + { + do + { + String hostname = + cursor.getString(cursor.getColumnIndex(HistoryDB.QUICK_CONNECT_TABLE_COL_ITEM)); + QuickConnectBookmark bookmark = new QuickConnectBookmark(); + bookmark.setLabel(hostname); + bookmark.setHostname(hostname); + result.add(bookmark); + } while (cursor.moveToNext()); + } + cursor.close(); + return result; + } + + public void addHistoryItem(String item) + { + String insertHistoryItem = "INSERT OR REPLACE INTO " + HistoryDB.QUICK_CONNECT_TABLE_NAME + + " (" + HistoryDB.QUICK_CONNECT_TABLE_COL_ITEM + ", " + + HistoryDB.QUICK_CONNECT_TABLE_COL_TIMESTAMP + ") VALUES('" + + item + "', datetime('now'))"; + SQLiteDatabase db = getWritableDatabase(); + try + { + db.execSQL(insertHistoryItem); + } + catch (SQLException e) + { + Log.v(TAG, e.toString()); + } + } + + public boolean historyItemExists(String item) + { + String[] column = { HistoryDB.QUICK_CONNECT_TABLE_COL_ITEM }; + SQLiteDatabase db = getReadableDatabase(); + Cursor cursor = db.query(HistoryDB.QUICK_CONNECT_TABLE_NAME, column, + HistoryDB.QUICK_CONNECT_TABLE_COL_ITEM + " = '" + item + "'", null, + null, null, null); + boolean exists = (cursor.getCount() == 1); + cursor.close(); + return exists; + } + + public void removeHistoryItem(String hostname) + { + SQLiteDatabase db = getWritableDatabase(); + db.delete(HistoryDB.QUICK_CONNECT_TABLE_NAME, + HistoryDB.QUICK_CONNECT_TABLE_COL_ITEM + " = '" + hostname + "'", null); + } + + // safety wrappers + // in case of getReadableDatabase it could happen that upgradeDB gets called which is + // a problem if the DB is only readable + private SQLiteDatabase getWritableDatabase() + { + return historyDB.getWritableDatabase(); + } + + private SQLiteDatabase getReadableDatabase() + { + SQLiteDatabase db; + try + { + db = historyDB.getReadableDatabase(); + } + catch (SQLiteException e) + { + db = historyDB.getWritableDatabase(); + } + return db; + } +} diff --git a/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/services/SessionRequestHandlerActivity.java b/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/services/SessionRequestHandlerActivity.java new file mode 100644 index 0000000..9436210 --- /dev/null +++ b/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/services/SessionRequestHandlerActivity.java @@ -0,0 +1,77 @@ +/* + Activity for handling connection requests + + Copyright 2013 Thincast Technologies GmbH, Author: Martin Fleisz + + This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. + If a copy of the MPL was not distributed with this file, You can obtain one at + http://mozilla.org/MPL/2.0/. +*/ + +package com.freerdp.freerdpcore.services; + +import android.app.Activity; +import android.app.SearchManager; +import android.content.Intent; +import android.os.Bundle; +import androidx.appcompat.app.AppCompatActivity; + +import com.freerdp.freerdpcore.domain.ConnectionReference; +import com.freerdp.freerdpcore.presentation.BookmarkActivity; +import com.freerdp.freerdpcore.presentation.SessionActivity; + +public class SessionRequestHandlerActivity extends AppCompatActivity +{ + + @Override public void onCreate(Bundle savedInstanceState) + { + super.onCreate(savedInstanceState); + handleIntent(getIntent()); + } + + @Override protected void onNewIntent(Intent intent) + { + setIntent(intent); + handleIntent(intent); + } + + private void startSessionWithConnectionReference(String refStr) + { + + Bundle bundle = new Bundle(); + bundle.putString(SessionActivity.PARAM_CONNECTION_REFERENCE, refStr); + Intent sessionIntent = new Intent(this, SessionActivity.class); + sessionIntent.putExtras(bundle); + + startActivityForResult(sessionIntent, 0); + } + + private void editBookmarkWithConnectionReference(String refStr) + { + Bundle bundle = new Bundle(); + bundle.putString(BookmarkActivity.PARAM_CONNECTION_REFERENCE, refStr); + Intent bookmarkIntent = new Intent(this.getApplicationContext(), BookmarkActivity.class); + bookmarkIntent.putExtras(bundle); + startActivityForResult(bookmarkIntent, 0); + } + + private void handleIntent(Intent intent) + { + + String action = intent.getAction(); + if (Intent.ACTION_SEARCH.equals(action)) + startSessionWithConnectionReference(ConnectionReference.getHostnameReference( + intent.getStringExtra(SearchManager.QUERY))); + else if (Intent.ACTION_VIEW.equals(action)) + startSessionWithConnectionReference(intent.getDataString()); + else if (Intent.ACTION_EDIT.equals(action)) + editBookmarkWithConnectionReference(intent.getDataString()); + } + + @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) + { + super.onActivityResult(requestCode, resultCode, data); + this.setResult(resultCode); + this.finish(); + } +} diff --git a/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/utils/AppCompatPreferenceActivity.java b/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/utils/AppCompatPreferenceActivity.java new file mode 100644 index 0000000..d321d7b --- /dev/null +++ b/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/utils/AppCompatPreferenceActivity.java @@ -0,0 +1,112 @@ +package com.freerdp.freerdpcore.utils; + +import android.content.res.Configuration; +import android.os.Bundle; +import android.preference.PreferenceActivity; +import androidx.annotation.Nullable; +import androidx.annotation.NonNull; +import androidx.annotation.LayoutRes; +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.app.AppCompatDelegate; +import androidx.appcompat.widget.Toolbar; +import android.view.MenuInflater; +import android.view.View; +import android.view.ViewGroup; + +public abstract class AppCompatPreferenceActivity extends PreferenceActivity +{ + + private AppCompatDelegate mDelegate; + + @Override protected void onCreate(Bundle savedInstanceState) + { + getDelegate().installViewFactory(); + getDelegate().onCreate(savedInstanceState); + super.onCreate(savedInstanceState); + } + + @Override protected void onPostCreate(Bundle savedInstanceState) + { + super.onPostCreate(savedInstanceState); + getDelegate().onPostCreate(savedInstanceState); + } + + public ActionBar getSupportActionBar() + { + return getDelegate().getSupportActionBar(); + } + + public void setSupportActionBar(@Nullable Toolbar toolbar) + { + getDelegate().setSupportActionBar(toolbar); + } + + @Override @NonNull public MenuInflater getMenuInflater() + { + return getDelegate().getMenuInflater(); + } + + @Override public void setContentView(@LayoutRes int layoutResID) + { + getDelegate().setContentView(layoutResID); + } + + @Override public void setContentView(View view) + { + getDelegate().setContentView(view); + } + + @Override public void setContentView(View view, ViewGroup.LayoutParams params) + { + getDelegate().setContentView(view, params); + } + + @Override public void addContentView(View view, ViewGroup.LayoutParams params) + { + getDelegate().addContentView(view, params); + } + + @Override protected void onPostResume() + { + super.onPostResume(); + getDelegate().onPostResume(); + } + + @Override protected void onTitleChanged(CharSequence title, int color) + { + super.onTitleChanged(title, color); + getDelegate().setTitle(title); + } + + @Override public void onConfigurationChanged(Configuration newConfig) + { + super.onConfigurationChanged(newConfig); + getDelegate().onConfigurationChanged(newConfig); + } + + @Override protected void onStop() + { + super.onStop(); + getDelegate().onStop(); + } + + @Override protected void onDestroy() + { + super.onDestroy(); + getDelegate().onDestroy(); + } + + public void invalidateOptionsMenu() + { + getDelegate().invalidateOptionsMenu(); + } + + private AppCompatDelegate getDelegate() + { + if (mDelegate == null) + { + mDelegate = AppCompatDelegate.create(this, null); + } + return mDelegate; + } +} \ No newline at end of file diff --git a/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/utils/BookmarkArrayAdapter.java b/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/utils/BookmarkArrayAdapter.java new file mode 100644 index 0000000..6c6de6a --- /dev/null +++ b/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/utils/BookmarkArrayAdapter.java @@ -0,0 +1,135 @@ +/* + ArrayAdapter for bookmark lists + + Copyright 2013 Thincast Technologies GmbH, Author: Martin Fleisz + + This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. + If a copy of the MPL was not distributed with this file, You can obtain one at + http://mozilla.org/MPL/2.0/. +*/ + +package com.freerdp.freerdpcore.utils; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.ImageView; +import android.widget.TextView; + +import com.freerdp.freerdpcore.R; +import com.freerdp.freerdpcore.domain.BookmarkBase; +import com.freerdp.freerdpcore.domain.ConnectionReference; +import com.freerdp.freerdpcore.domain.ManualBookmark; +import com.freerdp.freerdpcore.domain.PlaceholderBookmark; +import com.freerdp.freerdpcore.presentation.BookmarkActivity; + +import java.util.List; + +public class BookmarkArrayAdapter extends ArrayAdapter +{ + + public BookmarkArrayAdapter(Context context, int textViewResourceId, List objects) + { + super(context, textViewResourceId, objects); + } + + @Override public View getView(int position, View convertView, ViewGroup parent) + { + View curView = convertView; + if (curView == null) + { + LayoutInflater vi = + (LayoutInflater)getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE); + curView = vi.inflate(R.layout.bookmark_list_item, null); + } + + BookmarkBase bookmark = getItem(position); + TextView label = (TextView)curView.findViewById(R.id.bookmark_text1); + TextView hostname = (TextView)curView.findViewById(R.id.bookmark_text2); + ImageView star_icon = (ImageView)curView.findViewById(R.id.bookmark_icon2); + assert label != null; + assert hostname != null; + + label.setText(bookmark.getLabel()); + star_icon.setVisibility(View.VISIBLE); + + String refStr; + if (bookmark.getType() == BookmarkBase.TYPE_MANUAL) + { + hostname.setText(bookmark.get().getHostname()); + refStr = ConnectionReference.getManualBookmarkReference(bookmark.getId()); + star_icon.setImageResource(R.drawable.icon_star_on); + } + else if (bookmark.getType() == BookmarkBase.TYPE_QUICKCONNECT) + { + // just set an empty hostname (with a blank) - the hostname is already displayed in the + // label and in case we just set it to "" the textview will shrunk + hostname.setText(" "); + refStr = ConnectionReference.getHostnameReference(bookmark.getLabel()); + star_icon.setImageResource(R.drawable.icon_star_off); + } + else if (bookmark.getType() == BookmarkBase.TYPE_PLACEHOLDER) + { + hostname.setText(" "); + refStr = ConnectionReference.getPlaceholderReference( + bookmark.get().getName()); + star_icon.setVisibility(View.GONE); + } + else + { + // unknown bookmark type... + refStr = ""; + assert false; + } + + star_icon.setOnClickListener(new OnClickListener() { + @Override public void onClick(View v) + { + // start bookmark editor + Bundle bundle = new Bundle(); + String refStr = v.getTag().toString(); + bundle.putString(BookmarkActivity.PARAM_CONNECTION_REFERENCE, refStr); + + Intent bookmarkIntent = new Intent(getContext(), BookmarkActivity.class); + bookmarkIntent.putExtras(bundle); + getContext().startActivity(bookmarkIntent); + } + }); + + curView.setTag(refStr); + star_icon.setTag(refStr); + + return curView; + } + + public void addItems(List newItems) + { + for (BookmarkBase item : newItems) + add(item); + } + + public void replaceItems(List newItems) + { + clear(); + for (BookmarkBase item : newItems) + add(item); + } + + public void remove(long bookmarkId) + { + for (int i = 0; i < getCount(); i++) + { + BookmarkBase bm = getItem(i); + if (bm.getId() == bookmarkId) + { + remove(bm); + return; + } + } + } +} diff --git a/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/utils/ButtonPreference.java b/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/utils/ButtonPreference.java new file mode 100644 index 0000000..72c8cf0 --- /dev/null +++ b/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/utils/ButtonPreference.java @@ -0,0 +1,96 @@ +/* + Custom preference item showing a button on the right side + + Copyright 2013 Thincast Technologies GmbH, Author: Martin Fleisz + + This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. + If a copy of the MPL was not distributed with this file, You can obtain one at + http://mozilla.org/MPL/2.0/. +*/ + +package com.freerdp.freerdpcore.utils; + +import android.content.Context; +import android.preference.Preference; +import android.util.AttributeSet; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.LinearLayout; + +import com.freerdp.freerdpcore.R; + +public class ButtonPreference extends Preference +{ + + private OnClickListener buttonOnClickListener; + private String buttonText; + private Button button; + + public ButtonPreference(Context context) + { + super(context); + init(); + } + + public ButtonPreference(Context context, AttributeSet attrs) + { + super(context, attrs); + init(); + } + + public ButtonPreference(Context context, AttributeSet attrs, int defStyle) + { + super(context, attrs, defStyle); + init(); + } + + private void init() + { + setLayoutResource(R.layout.button_preference); + button = null; + buttonText = null; + buttonOnClickListener = null; + } + + @Override public View getView(View convertView, ViewGroup parent) + { + View v = super.getView(convertView, parent); + button = (Button)v.findViewById(R.id.preference_button); + if (buttonText != null) + button.setText(buttonText); + if (buttonOnClickListener != null) + button.setOnClickListener(buttonOnClickListener); + + // additional init for ICS - make widget frame visible + // refer to + // http://stackoverflow.com/questions/8762984/custom-preference-broken-in-honeycomb-ics + LinearLayout widgetFrameView = ((LinearLayout)v.findViewById(android.R.id.widget_frame)); + widgetFrameView.setVisibility(View.VISIBLE); + + return v; + } + + public void setButtonText(int resId) + { + buttonText = getContext().getResources().getString(resId); + if (button != null) + button.setText(buttonText); + } + + public void setButtonText(String text) + { + buttonText = text; + if (button != null) + button.setText(text); + } + + public void setButtonOnClickListener(OnClickListener listener) + { + if (button != null) + button.setOnClickListener(listener); + else + buttonOnClickListener = listener; + } +} diff --git a/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/utils/ClipboardManagerProxy.java b/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/utils/ClipboardManagerProxy.java new file mode 100644 index 0000000..cb26ddb --- /dev/null +++ b/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/utils/ClipboardManagerProxy.java @@ -0,0 +1,100 @@ +package com.freerdp.freerdpcore.utils; + +import android.annotation.TargetApi; +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; + +public abstract class ClipboardManagerProxy +{ + + public static ClipboardManagerProxy getClipboardManager(Context ctx) + { + if (VERSION.SDK_INT < VERSION_CODES.HONEYCOMB) + return new PreHCClipboardManager(ctx); + else + return new HCClipboardManager(ctx); + } + + public abstract void setClipboardData(String data); + + public abstract void addClipboardChangedListener(OnClipboardChangedListener listener); + + public abstract void removeClipboardboardChangedListener(OnClipboardChangedListener listener); + + public static interface OnClipboardChangedListener { + void onClipboardChanged(String data); + } + + private static class PreHCClipboardManager extends ClipboardManagerProxy + { + + public PreHCClipboardManager(Context ctx) + { + } + + @Override public void setClipboardData(String data) + { + } + + @Override public void addClipboardChangedListener(OnClipboardChangedListener listener) + { + } + + @Override + public void removeClipboardboardChangedListener(OnClipboardChangedListener listener) + { + } + } + + @TargetApi(11) + private static class HCClipboardManager + extends ClipboardManagerProxy implements ClipboardManager.OnPrimaryClipChangedListener + { + private ClipboardManager mClipboardManager; + private OnClipboardChangedListener mListener; + + public HCClipboardManager(Context ctx) + { + mClipboardManager = (ClipboardManager)ctx.getSystemService(Context.CLIPBOARD_SERVICE); + } + + @Override public void setClipboardData(String data) + { + mClipboardManager.setPrimaryClip( + ClipData.newPlainText("rdp-clipboard", data == null ? "" : data)); + } + + @Override public void onPrimaryClipChanged() + { + ClipData clip = mClipboardManager.getPrimaryClip(); + String data = null; + + if (clip != null && clip.getItemCount() > 0) + { + CharSequence cs = clip.getItemAt(0).getText(); + if (cs != null) + data = cs.toString(); + } + if (mListener != null) + { + mListener.onClipboardChanged(data); + } + } + + @Override public void addClipboardChangedListener(OnClipboardChangedListener listener) + { + mListener = listener; + mClipboardManager.addPrimaryClipChangedListener(this); + } + + @Override + public void removeClipboardboardChangedListener(OnClipboardChangedListener listener) + { + mListener = null; + mClipboardManager.removePrimaryClipChangedListener(this); + } + } +} diff --git a/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/utils/DoubleGestureDetector.java b/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/utils/DoubleGestureDetector.java new file mode 100644 index 0000000..2e8dfc8 --- /dev/null +++ b/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/utils/DoubleGestureDetector.java @@ -0,0 +1,349 @@ +/* + 2 finger gesture detector + + Copyright 2013 Thincast Technologies GmbH, Author: Martin Fleisz + + This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. + If a copy of the MPL was not distributed with this file, You can obtain one at + http://mozilla.org/MPL/2.0/. +*/ + +package com.freerdp.freerdpcore.utils; + +import android.content.Context; +import android.os.Handler; +import android.view.MotionEvent; +import android.view.ScaleGestureDetector; + +import com.freerdp.freerdpcore.utils.GestureDetector.OnGestureListener; + +public class DoubleGestureDetector +{ + // timeout during that the second finger has to touch the screen before the double finger + // detection is cancelled + private static final long DOUBLE_TOUCH_TIMEOUT = 100; + // timeout during that an UP event will trigger a single double touch event + private static final long SINGLE_DOUBLE_TOUCH_TIMEOUT = 1000; + // constants for Message.what used by GestureHandler below + private static final int TAP = 1; + // different detection modes + private static final int MODE_UNKNOWN = 0; + private static final int MODE_PINCH_ZOOM = 1; + private static final int MODE_SCROLL = 2; + private static final int SCROLL_SCORE_TO_REACH = 20; + private final OnDoubleGestureListener mListener; + private int mPointerDistanceSquare; + private int mCurrentMode; + private int mScrollDetectionScore; + private ScaleGestureDetector scaleGestureDetector; + private boolean mCancelDetection; + private boolean mDoubleInProgress; + private GestureHandler mHandler; + private MotionEvent mCurrentDownEvent; + private MotionEvent mCurrentDoubleDownEvent; + private MotionEvent mPreviousUpEvent; + private MotionEvent mPreviousPointerUpEvent; + /** + * Creates a GestureDetector with the supplied listener. + * You may only use this constructor from a UI thread (this is the usual situation). + * + * @param context the application's context + * @param listener the listener invoked for all the callbacks, this must + * not be null. + * @throws NullPointerException if {@code listener} is null. + * @see android.os.Handler#Handler() + */ + public DoubleGestureDetector(Context context, Handler handler, OnDoubleGestureListener listener) + { + mListener = listener; + init(context, handler); + } + + private void init(Context context, Handler handler) + { + if (mListener == null) + { + throw new NullPointerException("OnGestureListener must not be null"); + } + + if (handler != null) + mHandler = new GestureHandler(handler); + else + mHandler = new GestureHandler(); + + // we use 1cm distance to decide between scroll and pinch zoom + // - first convert cm to inches + // - then multiply inches by dots per inch + float distInches = 0.5f / 2.54f; + float distPixelsX = distInches * context.getResources().getDisplayMetrics().xdpi; + float distPixelsY = distInches * context.getResources().getDisplayMetrics().ydpi; + + mPointerDistanceSquare = (int)(distPixelsX * distPixelsX + distPixelsY * distPixelsY); + } + + /** + * Set scale gesture detector + * + * @param scaleGestureDetector + */ + public void setScaleGestureDetector(ScaleGestureDetector scaleGestureDetector) + { + this.scaleGestureDetector = scaleGestureDetector; + } + + /** + * Analyzes the given motion event and if applicable triggers the + * appropriate callbacks on the {@link OnGestureListener} supplied. + * + * @param ev The current motion event. + * @return true if the {@link OnGestureListener} consumed the event, + * else false. + */ + public boolean onTouchEvent(MotionEvent ev) + { + boolean handled = false; + final int action = ev.getAction(); + // dumpEvent(ev); + + switch (action & MotionEvent.ACTION_MASK) + { + case MotionEvent.ACTION_DOWN: + if (mCurrentDownEvent != null) + mCurrentDownEvent.recycle(); + + mCurrentMode = MODE_UNKNOWN; + mCurrentDownEvent = MotionEvent.obtain(ev); + mCancelDetection = false; + mDoubleInProgress = false; + mScrollDetectionScore = 0; + handled = true; + break; + + case MotionEvent.ACTION_POINTER_UP: + if (mPreviousPointerUpEvent != null) + mPreviousPointerUpEvent.recycle(); + mPreviousPointerUpEvent = MotionEvent.obtain(ev); + break; + + case MotionEvent.ACTION_POINTER_DOWN: + // more than 2 fingers down? cancel + // 2nd finger touched too late? cancel + if (ev.getPointerCount() > 2 || + (ev.getEventTime() - mCurrentDownEvent.getEventTime()) > DOUBLE_TOUCH_TIMEOUT) + { + cancel(); + break; + } + + // detection cancelled? + if (mCancelDetection) + break; + + // double touch gesture in progress + mDoubleInProgress = true; + if (mCurrentDoubleDownEvent != null) + mCurrentDoubleDownEvent.recycle(); + mCurrentDoubleDownEvent = MotionEvent.obtain(ev); + + // set detection mode to unkown and send a TOUCH timeout event to detect single taps + mCurrentMode = MODE_UNKNOWN; + mHandler.sendEmptyMessageDelayed(TAP, SINGLE_DOUBLE_TOUCH_TIMEOUT); + + handled |= mListener.onDoubleTouchDown(ev); + break; + + case MotionEvent.ACTION_MOVE: + + // detection cancelled or not active? + if (mCancelDetection || !mDoubleInProgress || ev.getPointerCount() != 2) + break; + + // determine mode + if (mCurrentMode == MODE_UNKNOWN) + { + // did the pointer distance change? + if (pointerDistanceChanged(mCurrentDoubleDownEvent, ev)) + { + handled |= scaleGestureDetector.onTouchEvent(mCurrentDownEvent); + MotionEvent e = MotionEvent.obtain(ev); + e.setAction(mCurrentDoubleDownEvent.getAction()); + handled |= scaleGestureDetector.onTouchEvent(e); + mCurrentMode = MODE_PINCH_ZOOM; + break; + } + else + { + mScrollDetectionScore++; + if (mScrollDetectionScore >= SCROLL_SCORE_TO_REACH) + mCurrentMode = MODE_SCROLL; + } + } + + switch (mCurrentMode) + { + case MODE_PINCH_ZOOM: + if (scaleGestureDetector != null) + handled |= scaleGestureDetector.onTouchEvent(ev); + break; + + case MODE_SCROLL: + handled = mListener.onDoubleTouchScroll(mCurrentDownEvent, ev); + break; + + default: + handled = true; + break; + } + + break; + + case MotionEvent.ACTION_UP: + // fingers were not removed equally? cancel + if (mPreviousPointerUpEvent != null && + (ev.getEventTime() - mPreviousPointerUpEvent.getEventTime()) > + DOUBLE_TOUCH_TIMEOUT) + { + mPreviousPointerUpEvent.recycle(); + mPreviousPointerUpEvent = null; + cancel(); + break; + } + + // detection cancelled or not active? + if (mCancelDetection || !mDoubleInProgress) + break; + + boolean hasTapEvent = mHandler.hasMessages(TAP); + MotionEvent currentUpEvent = MotionEvent.obtain(ev); + if (mCurrentMode == MODE_UNKNOWN && hasTapEvent) + handled = mListener.onDoubleTouchSingleTap(mCurrentDoubleDownEvent); + else if (mCurrentMode == MODE_PINCH_ZOOM) + handled = scaleGestureDetector.onTouchEvent(ev); + + if (mPreviousUpEvent != null) + mPreviousUpEvent.recycle(); + + // Hold the event we obtained above - listeners may have changed the original. + mPreviousUpEvent = currentUpEvent; + handled |= mListener.onDoubleTouchUp(ev); + break; + + case MotionEvent.ACTION_CANCEL: + cancel(); + break; + } + + if ((action == MotionEvent.ACTION_MOVE) && handled == false) + handled = true; + + return handled; + } + + private void cancel() + { + mHandler.removeMessages(TAP); + mCurrentMode = MODE_UNKNOWN; + mCancelDetection = true; + mDoubleInProgress = false; + } + + // returns true of the distance between the two pointers changed + private boolean pointerDistanceChanged(MotionEvent oldEvent, MotionEvent newEvent) + { + int deltaX1 = Math.abs((int)oldEvent.getX(0) - (int)oldEvent.getX(1)); + int deltaX2 = Math.abs((int)newEvent.getX(0) - (int)newEvent.getX(1)); + int distXSquare = (deltaX2 - deltaX1) * (deltaX2 - deltaX1); + + int deltaY1 = Math.abs((int)oldEvent.getY(0) - (int)oldEvent.getY(1)); + int deltaY2 = Math.abs((int)newEvent.getY(0) - (int)newEvent.getY(1)); + int distYSquare = (deltaY2 - deltaY1) * (deltaY2 - deltaY1); + + return (distXSquare + distYSquare) > mPointerDistanceSquare; + } + + /** + * The listener that is used to notify when gestures occur. + * If you want to listen for all the different gestures then implement + * this interface. If you only want to listen for a subset it might + * be easier to extend {@link SimpleOnGestureListener}. + */ + public interface OnDoubleGestureListener { + + /** + * Notified when a multi tap event starts + */ + boolean onDoubleTouchDown(MotionEvent e); + + /** + * Notified when a multi tap event ends + */ + boolean onDoubleTouchUp(MotionEvent e); + + /** + * Notified when a tap occurs with the up {@link MotionEvent} + * that triggered it. + * + * @param e The up motion event that completed the first tap + * @return true if the event is consumed, else false + */ + boolean onDoubleTouchSingleTap(MotionEvent e); + + /** + * Notified when a scroll occurs with the initial on down {@link MotionEvent} and the + * current move {@link MotionEvent}. The distance in x and y is also supplied for + * convenience. + * + * @param e1 The first down motion event that started the scrolling. + * @param e2 The move motion event that triggered the current onScroll. + * @param distanceX The distance along the X axis that has been scrolled since the last + * call to onScroll. This is NOT the distance between {@code e1} + * and {@code e2}. + * @param distanceY The distance along the Y axis that has been scrolled since the last + * call to onScroll. This is NOT the distance between {@code e1} + * and {@code e2}. + * @return true if the event is consumed, else false + */ + boolean onDoubleTouchScroll(MotionEvent e1, MotionEvent e2); + } + + /* + private void dumpEvent(MotionEvent event) { + String names[] = { "DOWN" , "UP" , "MOVE" , "CANCEL" , "OUTSIDE" , + "POINTER_DOWN" , "POINTER_UP" , "7?" , "8?" , "9?" }; + StringBuilder sb = new StringBuilder(); + int action = event.getAction(); + int actionCode = action & MotionEvent.ACTION_MASK; + sb.append("event ACTION_" ).append(names[actionCode]); + if (actionCode == MotionEvent.ACTION_POINTER_DOWN + || actionCode == MotionEvent.ACTION_POINTER_UP) { + sb.append("(pid " ).append( + action >> MotionEvent.ACTION_POINTER_ID_SHIFT); + sb.append(")" ); + } + sb.append("[" ); + for (int i = 0; i < event.getPointerCount(); i++) { + sb.append("#" ).append(i); + sb.append("(pid " ).append(event.getPointerId(i)); + sb.append(")=" ).append((int) event.getX(i)); + sb.append("," ).append((int) event.getY(i)); + if (i + 1 < event.getPointerCount()) + sb.append(";" ); + } + sb.append("]" ); + Log.d("DoubleDetector", sb.toString()); + } + */ + + private class GestureHandler extends Handler + { + GestureHandler() + { + super(); + } + + GestureHandler(Handler handler) + { + super(handler.getLooper()); + } + } +} diff --git a/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/utils/GestureDetector.java b/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/utils/GestureDetector.java new file mode 100644 index 0000000..2e971b5 --- /dev/null +++ b/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/utils/GestureDetector.java @@ -0,0 +1,620 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed 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. + * + * Modified for aFreeRDP by Martin Fleisz (martin.fleisz@thincast.com) + */ + +package com.freerdp.freerdpcore.utils; + +import android.content.Context; +import android.os.Build; +import android.os.Handler; +import android.os.Message; +import android.util.DisplayMetrics; +import android.view.MotionEvent; +import android.view.ViewConfiguration; + +public class GestureDetector +{ + + private static final int TAP_TIMEOUT = 100; + private static final int DOUBLE_TAP_TIMEOUT = 200; + // Distance a touch can wander before we think the user is the first touch in a sequence of + // double tap + private static final int LARGE_TOUCH_SLOP = 18; + // Distance between the first touch and second touch to still be considered a double tap + private static final int DOUBLE_TAP_SLOP = 100; + // constants for Message.what used by GestureHandler below + private static final int SHOW_PRESS = 1; + private static final int LONG_PRESS = 2; + private static final int TAP = 3; + private final Handler mHandler; + private final OnGestureListener mListener; + private int mTouchSlopSquare; + private int mLargeTouchSlopSquare; + private int mDoubleTapSlopSquare; + private int mLongpressTimeout = 100; + private OnDoubleTapListener mDoubleTapListener; + private boolean mStillDown; + private boolean mInLongPress; + private boolean mAlwaysInTapRegion; + private boolean mAlwaysInBiggerTapRegion; + private MotionEvent mCurrentDownEvent; + private MotionEvent mPreviousUpEvent; + /** + * True when the user is still touching for the second tap (down, move, and + * up events). Can only be true if there is a double tap listener attached. + */ + private boolean mIsDoubleTapping; + private float mLastMotionY; + private float mLastMotionX; + private boolean mIsLongpressEnabled; + /** + * True if we are at a target API level of >= Froyo or the developer can + * explicitly set it. If true, input events with > 1 pointer will be ignored + * so we can work side by side with multitouch gesture detectors. + */ + private boolean mIgnoreMultitouch; + /** + * Creates a GestureDetector with the supplied listener. + * You may only use this constructor from a UI thread (this is the usual situation). + * + * @param context the application's context + * @param listener the listener invoked for all the callbacks, this must + * not be null. + * @throws NullPointerException if {@code listener} is null. + * @see android.os.Handler#Handler() + */ + public GestureDetector(Context context, OnGestureListener listener) + { + this(context, listener, null); + } + + /** + * Creates a GestureDetector with the supplied listener. + * You may only use this constructor from a UI thread (this is the usual situation). + * + * @param context the application's context + * @param listener the listener invoked for all the callbacks, this must + * not be null. + * @param handler the handler to use + * @throws NullPointerException if {@code listener} is null. + * @see android.os.Handler#Handler() + */ + public GestureDetector(Context context, OnGestureListener listener, Handler handler) + { + this(context, listener, handler, + context != null && + context.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.FROYO); + } + + /** + * Creates a GestureDetector with the supplied listener. + * You may only use this constructor from a UI thread (this is the usual situation). + * + * @param context the application's context + * @param listener the listener invoked for all the callbacks, this must + * not be null. + * @param handler the handler to use + * @param ignoreMultitouch whether events involving more than one pointer should + * be ignored. + * @throws NullPointerException if {@code listener} is null. + * @see android.os.Handler#Handler() + */ + public GestureDetector(Context context, OnGestureListener listener, Handler handler, + boolean ignoreMultitouch) + { + if (handler != null) + { + mHandler = new GestureHandler(handler); + } + else + { + mHandler = new GestureHandler(); + } + mListener = listener; + if (listener instanceof OnDoubleTapListener) + { + setOnDoubleTapListener((OnDoubleTapListener)listener); + } + init(context, ignoreMultitouch); + } + + private void init(Context context, boolean ignoreMultitouch) + { + if (mListener == null) + { + throw new NullPointerException("OnGestureListener must not be null"); + } + mIsLongpressEnabled = true; + mIgnoreMultitouch = ignoreMultitouch; + + // Fallback to support pre-donuts releases + int touchSlop, largeTouchSlop, doubleTapSlop; + if (context == null) + { + // noinspection deprecation + touchSlop = ViewConfiguration.getTouchSlop(); + largeTouchSlop = touchSlop + 2; + doubleTapSlop = DOUBLE_TAP_SLOP; + } + else + { + final DisplayMetrics metrics = context.getResources().getDisplayMetrics(); + final float density = metrics.density; + final ViewConfiguration configuration = ViewConfiguration.get(context); + touchSlop = configuration.getScaledTouchSlop(); + largeTouchSlop = (int)(density * LARGE_TOUCH_SLOP + 0.5f); + doubleTapSlop = configuration.getScaledDoubleTapSlop(); + } + mTouchSlopSquare = touchSlop * touchSlop; + mLargeTouchSlopSquare = largeTouchSlop * largeTouchSlop; + mDoubleTapSlopSquare = doubleTapSlop * doubleTapSlop; + } + + /** + * Sets the listener which will be called for double-tap and related + * gestures. + * + * @param onDoubleTapListener the listener invoked for all the callbacks, or + * null to stop listening for double-tap gestures. + */ + public void setOnDoubleTapListener(OnDoubleTapListener onDoubleTapListener) + { + mDoubleTapListener = onDoubleTapListener; + } + + /** + * Set whether longpress is enabled, if this is enabled when a user + * presses and holds down you get a longpress event and nothing further. + * If it's disabled the user can press and hold down and then later + * moved their finger and you will get scroll events. By default + * longpress is enabled. + * + * @param isLongpressEnabled whether longpress should be enabled. + */ + public void setIsLongpressEnabled(boolean isLongpressEnabled) + { + mIsLongpressEnabled = isLongpressEnabled; + } + + /** + * @return true if longpress is enabled, else false. + */ + public boolean isLongpressEnabled() + { + return mIsLongpressEnabled; + } + + public void setLongPressTimeout(int timeout) + { + mLongpressTimeout = timeout; + } + + /** + * Analyzes the given motion event and if applicable triggers the + * appropriate callbacks on the {@link OnGestureListener} supplied. + * + * @param ev The current motion event. + * @return true if the {@link OnGestureListener} consumed the event, + * else false. + */ + public boolean onTouchEvent(MotionEvent ev) + { + final int action = ev.getAction(); + final float y = ev.getY(); + final float x = ev.getX(); + + boolean handled = false; + + switch (action & MotionEvent.ACTION_MASK) + { + case MotionEvent.ACTION_POINTER_DOWN: + if (mIgnoreMultitouch) + { + // Multitouch event - abort. + cancel(); + } + break; + + case MotionEvent.ACTION_POINTER_UP: + // Ending a multitouch gesture and going back to 1 finger + if (mIgnoreMultitouch && ev.getPointerCount() == 2) + { + int index = (((action & MotionEvent.ACTION_POINTER_INDEX_MASK) >> + MotionEvent.ACTION_POINTER_INDEX_SHIFT) == 0) + ? 1 + : 0; + mLastMotionX = ev.getX(index); + mLastMotionY = ev.getY(index); + } + break; + + case MotionEvent.ACTION_DOWN: + if (mDoubleTapListener != null) + { + boolean hadTapMessage = mHandler.hasMessages(TAP); + if (hadTapMessage) + mHandler.removeMessages(TAP); + if ((mCurrentDownEvent != null) && (mPreviousUpEvent != null) && + hadTapMessage && + isConsideredDoubleTap(mCurrentDownEvent, mPreviousUpEvent, ev)) + { + // This is a second tap + mIsDoubleTapping = true; + // Give a callback with the first tap of the double-tap + handled |= mDoubleTapListener.onDoubleTap(mCurrentDownEvent); + // Give a callback with down event of the double-tap + handled |= mDoubleTapListener.onDoubleTapEvent(ev); + } + else + { + // This is a first tap + mHandler.sendEmptyMessageDelayed(TAP, DOUBLE_TAP_TIMEOUT); + } + } + + mLastMotionX = x; + mLastMotionY = y; + if (mCurrentDownEvent != null) + { + mCurrentDownEvent.recycle(); + } + mCurrentDownEvent = MotionEvent.obtain(ev); + mAlwaysInTapRegion = true; + mAlwaysInBiggerTapRegion = true; + mStillDown = true; + mInLongPress = false; + + if (mIsLongpressEnabled) + { + mHandler.removeMessages(LONG_PRESS); + mHandler.sendEmptyMessageAtTime(LONG_PRESS, mCurrentDownEvent.getDownTime() + + TAP_TIMEOUT + + mLongpressTimeout); + } + mHandler.sendEmptyMessageAtTime(SHOW_PRESS, + mCurrentDownEvent.getDownTime() + TAP_TIMEOUT); + handled |= mListener.onDown(ev); + break; + + case MotionEvent.ACTION_MOVE: + if (mIgnoreMultitouch && ev.getPointerCount() > 1) + { + break; + } + final float scrollX = mLastMotionX - x; + final float scrollY = mLastMotionY - y; + if (mIsDoubleTapping) + { + // Give the move events of the double-tap + handled |= mDoubleTapListener.onDoubleTapEvent(ev); + } + else if (mAlwaysInTapRegion) + { + final int deltaX = (int)(x - mCurrentDownEvent.getX()); + final int deltaY = (int)(y - mCurrentDownEvent.getY()); + int distance = (deltaX * deltaX) + (deltaY * deltaY); + if (distance > mTouchSlopSquare) + { + mLastMotionX = x; + mLastMotionY = y; + mAlwaysInTapRegion = false; + mHandler.removeMessages(TAP); + mHandler.removeMessages(SHOW_PRESS); + mHandler.removeMessages(LONG_PRESS); + } + if (distance > mLargeTouchSlopSquare) + { + mAlwaysInBiggerTapRegion = false; + } + handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY); + } + else if ((Math.abs(scrollX) >= 1) || (Math.abs(scrollY) >= 1)) + { + handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY); + mLastMotionX = x; + mLastMotionY = y; + } + break; + + case MotionEvent.ACTION_UP: + mStillDown = false; + MotionEvent currentUpEvent = MotionEvent.obtain(ev); + if (mIsDoubleTapping) + { + // Finally, give the up event of the double-tap + handled |= mDoubleTapListener.onDoubleTapEvent(ev); + } + else if (mInLongPress) + { + mHandler.removeMessages(TAP); + mListener.onLongPressUp(ev); + mInLongPress = false; + } + else if (mAlwaysInTapRegion) + { + handled = mListener.onSingleTapUp(mCurrentDownEvent); + } + else + { + // A fling must travel the minimum tap distance + } + if (mPreviousUpEvent != null) + { + mPreviousUpEvent.recycle(); + } + // Hold the event we obtained above - listeners may have changed the original. + mPreviousUpEvent = currentUpEvent; + mIsDoubleTapping = false; + mHandler.removeMessages(SHOW_PRESS); + mHandler.removeMessages(LONG_PRESS); + handled |= mListener.onUp(ev); + break; + case MotionEvent.ACTION_CANCEL: + cancel(); + break; + } + return handled; + } + + private void cancel() + { + mHandler.removeMessages(SHOW_PRESS); + mHandler.removeMessages(LONG_PRESS); + mHandler.removeMessages(TAP); + mAlwaysInTapRegion = false; // ensures that we won't receive an OnSingleTap notification + // when a 2-Finger tap is performed + mIsDoubleTapping = false; + mStillDown = false; + if (mInLongPress) + { + mInLongPress = false; + } + } + + private boolean isConsideredDoubleTap(MotionEvent firstDown, MotionEvent firstUp, + MotionEvent secondDown) + { + if (!mAlwaysInBiggerTapRegion) + { + return false; + } + + if (secondDown.getEventTime() - firstUp.getEventTime() > DOUBLE_TAP_TIMEOUT) + { + return false; + } + + int deltaX = (int)firstDown.getX() - (int)secondDown.getX(); + int deltaY = (int)firstDown.getY() - (int)secondDown.getY(); + return (deltaX * deltaX + deltaY * deltaY < mDoubleTapSlopSquare); + } + + private void dispatchLongPress() + { + mHandler.removeMessages(TAP); + mInLongPress = true; + mListener.onLongPress(mCurrentDownEvent); + } + + /** + * The listener that is used to notify when gestures occur. + * If you want to listen for all the different gestures then implement + * this interface. If you only want to listen for a subset it might + * be easier to extend {@link SimpleOnGestureListener}. + */ + public interface OnGestureListener { + + /** + * Notified when a tap occurs with the down {@link MotionEvent} + * that triggered it. This will be triggered immediately for + * every down event. All other events should be preceded by this. + * + * @param e The down motion event. + */ + boolean onDown(MotionEvent e); + + /** + * Notified when a tap finishes with the up {@link MotionEvent} + * that triggered it. This will be triggered immediately for + * every up event. All other events should be preceded by this. + * + * @param e The up motion event. + */ + boolean onUp(MotionEvent e); + + /** + * The user has performed a down {@link MotionEvent} and not performed + * a move or up yet. This event is commonly used to provide visual + * feedback to the user to let them know that their action has been + * recognized i.e. highlight an element. + * + * @param e The down motion event + */ + void onShowPress(MotionEvent e); + + /** + * Notified when a tap occurs with the up {@link MotionEvent} + * that triggered it. + * + * @param e The up motion event that completed the first tap + * @return true if the event is consumed, else false + */ + boolean onSingleTapUp(MotionEvent e); + + /** + * Notified when a scroll occurs with the initial on down {@link MotionEvent} and the + * current move {@link MotionEvent}. The distance in x and y is also supplied for + * convenience. + * + * @param e1 The first down motion event that started the scrolling. + * @param e2 The move motion event that triggered the current onScroll. + * @param distanceX The distance along the X axis that has been scrolled since the last + * call to onScroll. This is NOT the distance between {@code e1} + * and {@code e2}. + * @param distanceY The distance along the Y axis that has been scrolled since the last + * call to onScroll. This is NOT the distance between {@code e1} + * and {@code e2}. + * @return true if the event is consumed, else false + */ + boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY); + + /** + * Notified when a long press occurs with the initial on down {@link MotionEvent} + * that trigged it. + * + * @param e The initial on down motion event that started the longpress. + */ + void onLongPress(MotionEvent e); + + /** + * Notified when a long press ends with the final {@link MotionEvent}. + * + * @param e The up motion event that ended the longpress. + */ + void onLongPressUp(MotionEvent e); + } + + /** + * The listener that is used to notify when a double-tap or a confirmed + * single-tap occur. + */ + public interface OnDoubleTapListener { + /** + * Notified when a single-tap occurs. + *

+ * Unlike {@link OnGestureListener#onSingleTapUp(MotionEvent)}, this + * will only be called after the detector is confident that the user's + * first tap is not followed by a second tap leading to a double-tap + * gesture. + * + * @param e The down motion event of the single-tap. + * @return true if the event is consumed, else false + */ + boolean onSingleTapConfirmed(MotionEvent e); + + /** + * Notified when a double-tap occurs. + * + * @param e The down motion event of the first tap of the double-tap. + * @return true if the event is consumed, else false + */ + boolean onDoubleTap(MotionEvent e); + + /** + * Notified when an event within a double-tap gesture occurs, including + * the down, move, and up events. + * + * @param e The motion event that occurred during the double-tap gesture. + * @return true if the event is consumed, else false + */ + boolean onDoubleTapEvent(MotionEvent e); + } + + /** + * A convenience class to extend when you only want to listen for a subset + * of all the gestures. This implements all methods in the + * {@link OnGestureListener} and {@link OnDoubleTapListener} but does + * nothing and return {@code false} for all applicable methods. + */ + public static class SimpleOnGestureListener implements OnGestureListener, OnDoubleTapListener + { + public boolean onSingleTapUp(MotionEvent e) + { + return false; + } + + public void onLongPress(MotionEvent e) + { + } + + public void onLongPressUp(MotionEvent e) + { + } + + public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) + { + return false; + } + + public void onShowPress(MotionEvent e) + { + } + + public boolean onDown(MotionEvent e) + { + return false; + } + + public boolean onUp(MotionEvent e) + { + return false; + } + + public boolean onDoubleTap(MotionEvent e) + { + return false; + } + + public boolean onDoubleTapEvent(MotionEvent e) + { + return false; + } + + public boolean onSingleTapConfirmed(MotionEvent e) + { + return false; + } + } + + private class GestureHandler extends Handler + { + GestureHandler() + { + super(); + } + + GestureHandler(Handler handler) + { + super(handler.getLooper()); + } + + @Override public void handleMessage(Message msg) + { + switch (msg.what) + { + case SHOW_PRESS: + mListener.onShowPress(mCurrentDownEvent); + break; + + case LONG_PRESS: + dispatchLongPress(); + break; + + case TAP: + // If the user's finger is still down, do not count it as a tap + if (mDoubleTapListener != null && !mStillDown) + { + mDoubleTapListener.onSingleTapConfirmed(mCurrentDownEvent); + } + break; + + default: + throw new RuntimeException("Unknown message " + msg); // never + } + } + } +} diff --git a/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/utils/IntEditTextPreference.java b/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/utils/IntEditTextPreference.java new file mode 100644 index 0000000..a383d91 --- /dev/null +++ b/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/utils/IntEditTextPreference.java @@ -0,0 +1,101 @@ +/* + EditTextPreference to store/load integer values + + Copyright 2013 Thincast Technologies GmbH, Author: Martin Fleisz + + This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. + If a copy of the MPL was not distributed with this file, You can obtain one at + http://mozilla.org/MPL/2.0/. +*/ + +package com.freerdp.freerdpcore.utils; + +import android.content.Context; +import android.content.res.TypedArray; +import android.preference.EditTextPreference; +import android.util.AttributeSet; + +import com.freerdp.freerdpcore.R; + +public class IntEditTextPreference extends EditTextPreference +{ + + private int bounds_min, bounds_max, bounds_default; + + public IntEditTextPreference(Context context) + { + super(context); + init(context, null); + } + + public IntEditTextPreference(Context context, AttributeSet attrs) + { + super(context, attrs); + init(context, attrs); + } + + public IntEditTextPreference(Context context, AttributeSet attrs, int defStyle) + { + super(context, attrs, defStyle); + init(context, attrs); + } + + private void init(Context context, AttributeSet attrs) + { + if (attrs != null) + { + TypedArray array = + context.obtainStyledAttributes(attrs, R.styleable.IntEditTextPreference, 0, 0); + bounds_min = + array.getInt(R.styleable.IntEditTextPreference_bounds_min, Integer.MIN_VALUE); + bounds_max = + array.getInt(R.styleable.IntEditTextPreference_bounds_max, Integer.MAX_VALUE); + bounds_default = array.getInt(R.styleable.IntEditTextPreference_bounds_default, 0); + array.recycle(); + } + else + { + bounds_min = Integer.MIN_VALUE; + bounds_max = Integer.MAX_VALUE; + bounds_default = 0; + } + } + + public void setBounds(int min, int max, int defaultValue) + { + bounds_min = min; + bounds_max = max; + bounds_default = defaultValue; + } + + @Override protected String getPersistedString(String defaultReturnValue) + { + int value = getPersistedInt(-1); + if (value > bounds_max || value < bounds_min) + value = bounds_default; + return String.valueOf(value); + } + + @Override protected boolean persistString(String value) + { + return persistInt(Integer.valueOf(value)); + } + + @Override protected void onDialogClosed(boolean positiveResult) + { + if (positiveResult) + { + // prevent exception when an empty value is persisted + if (getEditText().getText().length() == 0) + getEditText().setText("0"); + + // check bounds + int value = Integer.valueOf(getEditText().getText().toString()); + if (value > bounds_max || value < bounds_min) + value = bounds_default; + getEditText().setText(String.valueOf(value)); + } + + super.onDialogClosed(positiveResult); + } +} diff --git a/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/utils/IntListPreference.java b/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/utils/IntListPreference.java new file mode 100644 index 0000000..0b4f643 --- /dev/null +++ b/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/utils/IntListPreference.java @@ -0,0 +1,39 @@ +/* + ListPreference to store/load integer values + + Copyright 2013 Thincast Technologies GmbH, Author: Martin Fleisz + + This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. + If a copy of the MPL was not distributed with this file, You can obtain one at + http://mozilla.org/MPL/2.0/. +*/ + +package com.freerdp.freerdpcore.utils; + +import android.content.Context; +import android.preference.ListPreference; +import android.util.AttributeSet; + +public class IntListPreference extends ListPreference +{ + + public IntListPreference(Context context) + { + super(context); + } + + public IntListPreference(Context context, AttributeSet attrs) + { + super(context, attrs); + } + + @Override protected String getPersistedString(String defaultReturnValue) + { + return String.valueOf(getPersistedInt(-1)); + } + + @Override protected boolean persistString(String value) + { + return persistInt(Integer.valueOf(value)); + } +} diff --git a/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/utils/KeyboardMapper.java b/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/utils/KeyboardMapper.java new file mode 100644 index 0000000..f1456b2 --- /dev/null +++ b/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/utils/KeyboardMapper.java @@ -0,0 +1,725 @@ +/* + Android Keyboard Mapping + + Copyright 2013 Thincast Technologies GmbH, Author: Martin Fleisz + + This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. + If a copy of the MPL was not distributed with this file, You can obtain one at + http://mozilla.org/MPL/2.0/. +*/ + +package com.freerdp.freerdpcore.utils; + +import android.content.Context; +import android.view.KeyEvent; + +import com.freerdp.freerdpcore.R; + +public class KeyboardMapper +{ + public static final int KEYBOARD_TYPE_FUNCTIONKEYS = 1; + public static final int KEYBOARD_TYPE_NUMPAD = 2; + public static final int KEYBOARD_TYPE_CURSOR = 3; + + // defines key states for modifier keys - locked means on and no auto-release if an other key is + // pressed + public static final int KEYSTATE_ON = 1; + public static final int KEYSTATE_LOCKED = 2; + public static final int KEYSTATE_OFF = 3; + final static int VK_LBUTTON = 0x01; + final static int VK_RBUTTON = 0x02; + final static int VK_CANCEL = 0x03; + final static int VK_MBUTTON = 0x04; + final static int VK_XBUTTON1 = 0x05; + final static int VK_XBUTTON2 = 0x06; + final static int VK_BACK = 0x08; + final static int VK_TAB = 0x09; + final static int VK_CLEAR = 0x0C; + final static int VK_RETURN = 0x0D; + final static int VK_SHIFT = 0x10; + final static int VK_CONTROL = 0x11; + final static int VK_MENU = 0x12; + final static int VK_PAUSE = 0x13; + final static int VK_CAPITAL = 0x14; + final static int VK_KANA = 0x15; + final static int VK_HANGUEL = 0x15; + final static int VK_HANGUL = 0x15; + final static int VK_JUNJA = 0x17; + final static int VK_FINAL = 0x18; + final static int VK_HANJA = 0x19; + final static int VK_KANJI = 0x19; + final static int VK_ESCAPE = 0x1B; + final static int VK_CONVERT = 0x1C; + final static int VK_NONCONVERT = 0x1D; + final static int VK_ACCEPT = 0x1E; + final static int VK_MODECHANGE = 0x1F; + final static int VK_SPACE = 0x20; + final static int VK_PRIOR = 0x21; + final static int VK_NEXT = 0x22; + final static int VK_END = 0x23; + final static int VK_HOME = 0x24; + final static int VK_LEFT = 0x25; + final static int VK_UP = 0x26; + final static int VK_RIGHT = 0x27; + final static int VK_DOWN = 0x28; + final static int VK_SELECT = 0x29; + final static int VK_PRINT = 0x2A; + final static int VK_EXECUTE = 0x2B; + final static int VK_SNAPSHOT = 0x2C; + final static int VK_INSERT = 0x2D; + final static int VK_DELETE = 0x2E; + final static int VK_HELP = 0x2F; + final static int VK_KEY_0 = 0x30; + final static int VK_KEY_1 = 0x31; + final static int VK_KEY_2 = 0x32; + final static int VK_KEY_3 = 0x33; + final static int VK_KEY_4 = 0x34; + final static int VK_KEY_5 = 0x35; + final static int VK_KEY_6 = 0x36; + final static int VK_KEY_7 = 0x37; + final static int VK_KEY_8 = 0x38; + final static int VK_KEY_9 = 0x39; + final static int VK_KEY_A = 0x41; + final static int VK_KEY_B = 0x42; + final static int VK_KEY_C = 0x43; + final static int VK_KEY_D = 0x44; + final static int VK_KEY_E = 0x45; + final static int VK_KEY_F = 0x46; + final static int VK_KEY_G = 0x47; + final static int VK_KEY_H = 0x48; + final static int VK_KEY_I = 0x49; + final static int VK_KEY_J = 0x4A; + final static int VK_KEY_K = 0x4B; + final static int VK_KEY_L = 0x4C; + final static int VK_KEY_M = 0x4D; + final static int VK_KEY_N = 0x4E; + final static int VK_KEY_O = 0x4F; + final static int VK_KEY_P = 0x50; + final static int VK_KEY_Q = 0x51; + final static int VK_KEY_R = 0x52; + final static int VK_KEY_S = 0x53; + final static int VK_KEY_T = 0x54; + final static int VK_KEY_U = 0x55; + final static int VK_KEY_V = 0x56; + final static int VK_KEY_W = 0x57; + final static int VK_KEY_X = 0x58; + final static int VK_KEY_Y = 0x59; + final static int VK_KEY_Z = 0x5A; + final static int VK_LWIN = 0x5B; + final static int VK_RWIN = 0x5C; + final static int VK_APPS = 0x5D; + final static int VK_SLEEP = 0x5F; + final static int VK_NUMPAD0 = 0x60; + final static int VK_NUMPAD1 = 0x61; + final static int VK_NUMPAD2 = 0x62; + final static int VK_NUMPAD3 = 0x63; + final static int VK_NUMPAD4 = 0x64; + final static int VK_NUMPAD5 = 0x65; + final static int VK_NUMPAD6 = 0x66; + final static int VK_NUMPAD7 = 0x67; + final static int VK_NUMPAD8 = 0x68; + final static int VK_NUMPAD9 = 0x69; + final static int VK_MULTIPLY = 0x6A; + final static int VK_ADD = 0x6B; + final static int VK_SEPARATOR = 0x6C; + final static int VK_SUBTRACT = 0x6D; + final static int VK_DECIMAL = 0x6E; + final static int VK_DIVIDE = 0x6F; + final static int VK_F1 = 0x70; + final static int VK_F2 = 0x71; + final static int VK_F3 = 0x72; + final static int VK_F4 = 0x73; + final static int VK_F5 = 0x74; + final static int VK_F6 = 0x75; + final static int VK_F7 = 0x76; + final static int VK_F8 = 0x77; + final static int VK_F9 = 0x78; + final static int VK_F10 = 0x79; + final static int VK_F11 = 0x7A; + final static int VK_F12 = 0x7B; + final static int VK_F13 = 0x7C; + final static int VK_F14 = 0x7D; + final static int VK_F15 = 0x7E; + final static int VK_F16 = 0x7F; + final static int VK_F17 = 0x80; + final static int VK_F18 = 0x81; + final static int VK_F19 = 0x82; + final static int VK_F20 = 0x83; + final static int VK_F21 = 0x84; + final static int VK_F22 = 0x85; + final static int VK_F23 = 0x86; + final static int VK_F24 = 0x87; + final static int VK_NUMLOCK = 0x90; + final static int VK_SCROLL = 0x91; + final static int VK_LSHIFT = 0xA0; + final static int VK_RSHIFT = 0xA1; + final static int VK_LCONTROL = 0xA2; + final static int VK_RCONTROL = 0xA3; + final static int VK_LMENU = 0xA4; + final static int VK_RMENU = 0xA5; + final static int VK_BROWSER_BACK = 0xA6; + final static int VK_BROWSER_FORWARD = 0xA7; + final static int VK_BROWSER_REFRESH = 0xA8; + final static int VK_BROWSER_STOP = 0xA9; + final static int VK_BROWSER_SEARCH = 0xAA; + final static int VK_BROWSER_FAVORITES = 0xAB; + final static int VK_BROWSER_HOME = 0xAC; + final static int VK_VOLUME_MUTE = 0xAD; + final static int VK_VOLUME_DOWN = 0xAE; + final static int VK_VOLUME_UP = 0xAF; + final static int VK_MEDIA_NEXT_TRACK = 0xB0; + final static int VK_MEDIA_PREV_TRACK = 0xB1; + final static int VK_MEDIA_STOP = 0xB2; + final static int VK_MEDIA_PLAY_PAUSE = 0xB3; + final static int VK_LAUNCH_MAIL = 0xB4; + final static int VK_LAUNCH_MEDIA_SELECT = 0xB5; + final static int VK_LAUNCH_APP1 = 0xB6; + final static int VK_LAUNCH_APP2 = 0xB7; + final static int VK_OEM_1 = 0xBA; + final static int VK_OEM_PLUS = 0xBB; + final static int VK_OEM_COMMA = 0xBC; + final static int VK_OEM_MINUS = 0xBD; + final static int VK_OEM_PERIOD = 0xBE; + final static int VK_OEM_2 = 0xBF; + final static int VK_OEM_3 = 0xC0; + final static int VK_ABNT_C1 = 0xC1; + final static int VK_ABNT_C2 = 0xC2; + final static int VK_OEM_4 = 0xDB; + final static int VK_OEM_5 = 0xDC; + final static int VK_OEM_6 = 0xDD; + final static int VK_OEM_7 = 0xDE; + final static int VK_OEM_8 = 0xDF; + final static int VK_OEM_102 = 0xE2; + final static int VK_PROCESSKEY = 0xE5; + final static int VK_PACKET = 0xE7; + final static int VK_ATTN = 0xF6; + final static int VK_CRSEL = 0xF7; + final static int VK_EXSEL = 0xF8; + final static int VK_EREOF = 0xF9; + final static int VK_PLAY = 0xFA; + final static int VK_ZOOM = 0xFB; + final static int VK_NONAME = 0xFC; + final static int VK_PA1 = 0xFD; + final static int VK_OEM_CLEAR = 0xFE; + final static int VK_UNICODE = 0x80000000; + final static int VK_EXT_KEY = 0x00000100; + // key codes to switch between custom keyboard + private final static int EXTKEY_KBFUNCTIONKEYS = 0x1100; + private final static int EXTKEY_KBNUMPAD = 0x1101; + private final static int EXTKEY_KBCURSOR = 0x1102; + // this flag indicates if we got a VK or a unicode character in our translation map + private static final int KEY_FLAG_UNICODE = 0x80000000; + // this flag indicates if the key is a toggle key (remains down when pressed and goes up if + // pressed again) + private static final int KEY_FLAG_TOGGLE = 0x40000000; + private static int[] keymapAndroid; + private static int[] keymapExt; + private static boolean initialized = false; + private KeyProcessingListener listener = null; + private boolean shiftPressed = false; + private boolean ctrlPressed = false; + private boolean altPressed = false; + private boolean winPressed = false; + private long lastModifierTime; + private int lastModifierKeyCode = -1; + private boolean isShiftLocked = false; + private boolean isCtrlLocked = false; + private boolean isAltLocked = false; + private boolean isWinLocked = false; + + public void init(Context context) + { + if (initialized == true) + return; + + keymapAndroid = new int[256]; + + keymapAndroid[KeyEvent.KEYCODE_0] = VK_KEY_0; + keymapAndroid[KeyEvent.KEYCODE_1] = VK_KEY_1; + keymapAndroid[KeyEvent.KEYCODE_2] = VK_KEY_2; + keymapAndroid[KeyEvent.KEYCODE_3] = VK_KEY_3; + keymapAndroid[KeyEvent.KEYCODE_4] = VK_KEY_4; + keymapAndroid[KeyEvent.KEYCODE_5] = VK_KEY_5; + keymapAndroid[KeyEvent.KEYCODE_6] = VK_KEY_6; + keymapAndroid[KeyEvent.KEYCODE_7] = VK_KEY_7; + keymapAndroid[KeyEvent.KEYCODE_8] = VK_KEY_8; + keymapAndroid[KeyEvent.KEYCODE_9] = VK_KEY_9; + + keymapAndroid[KeyEvent.KEYCODE_A] = VK_KEY_A; + keymapAndroid[KeyEvent.KEYCODE_B] = VK_KEY_B; + keymapAndroid[KeyEvent.KEYCODE_C] = VK_KEY_C; + keymapAndroid[KeyEvent.KEYCODE_D] = VK_KEY_D; + keymapAndroid[KeyEvent.KEYCODE_E] = VK_KEY_E; + keymapAndroid[KeyEvent.KEYCODE_F] = VK_KEY_F; + keymapAndroid[KeyEvent.KEYCODE_G] = VK_KEY_G; + keymapAndroid[KeyEvent.KEYCODE_H] = VK_KEY_H; + keymapAndroid[KeyEvent.KEYCODE_I] = VK_KEY_I; + keymapAndroid[KeyEvent.KEYCODE_J] = VK_KEY_J; + keymapAndroid[KeyEvent.KEYCODE_K] = VK_KEY_K; + keymapAndroid[KeyEvent.KEYCODE_L] = VK_KEY_L; + keymapAndroid[KeyEvent.KEYCODE_M] = VK_KEY_M; + keymapAndroid[KeyEvent.KEYCODE_N] = VK_KEY_N; + keymapAndroid[KeyEvent.KEYCODE_O] = VK_KEY_O; + keymapAndroid[KeyEvent.KEYCODE_P] = VK_KEY_P; + keymapAndroid[KeyEvent.KEYCODE_Q] = VK_KEY_Q; + keymapAndroid[KeyEvent.KEYCODE_R] = VK_KEY_R; + keymapAndroid[KeyEvent.KEYCODE_S] = VK_KEY_S; + keymapAndroid[KeyEvent.KEYCODE_T] = VK_KEY_T; + keymapAndroid[KeyEvent.KEYCODE_U] = VK_KEY_U; + keymapAndroid[KeyEvent.KEYCODE_V] = VK_KEY_V; + keymapAndroid[KeyEvent.KEYCODE_W] = VK_KEY_W; + keymapAndroid[KeyEvent.KEYCODE_X] = VK_KEY_X; + keymapAndroid[KeyEvent.KEYCODE_Y] = VK_KEY_Y; + keymapAndroid[KeyEvent.KEYCODE_Z] = VK_KEY_Z; + + keymapAndroid[KeyEvent.KEYCODE_DEL] = VK_BACK; + keymapAndroid[KeyEvent.KEYCODE_ENTER] = VK_RETURN; + keymapAndroid[KeyEvent.KEYCODE_SPACE] = VK_SPACE; + keymapAndroid[KeyEvent.KEYCODE_TAB] = VK_TAB; + // keymapAndroid[KeyEvent.KEYCODE_SHIFT_LEFT] = VK_LSHIFT; + // keymapAndroid[KeyEvent.KEYCODE_SHIFT_RIGHT] = VK_RSHIFT; + + // keymapAndroid[KeyEvent.KEYCODE_DPAD_DOWN] = VK_DOWN; + // keymapAndroid[KeyEvent.KEYCODE_DPAD_LEFT] = VK_LEFT; + // keymapAndroid[KeyEvent.KEYCODE_DPAD_RIGHT] = VK_RIGHT; + // keymapAndroid[KeyEvent.KEYCODE_DPAD_UP] = VK_UP; + + // keymapAndroid[KeyEvent.KEYCODE_COMMA] = VK_OEM_COMMA; + // keymapAndroid[KeyEvent.KEYCODE_PERIOD] = VK_OEM_PERIOD; + // keymapAndroid[KeyEvent.KEYCODE_MINUS] = VK_OEM_MINUS; + // keymapAndroid[KeyEvent.KEYCODE_PLUS] = VK_OEM_PLUS; + + // keymapAndroid[KeyEvent.KEYCODE_ALT_LEFT] = VK_LMENU; + // keymapAndroid[KeyEvent.KEYCODE_ALT_RIGHT] = VK_RMENU; + + // keymapAndroid[KeyEvent.KEYCODE_AT] = (KEY_FLAG_UNICODE | 64); + // keymapAndroid[KeyEvent.KEYCODE_APOSTROPHE] = (KEY_FLAG_UNICODE | 39); + // keymapAndroid[KeyEvent.KEYCODE_BACKSLASH] = (KEY_FLAG_UNICODE | 92); + // keymapAndroid[KeyEvent.KEYCODE_COMMA] = (KEY_FLAG_UNICODE | 44); + // keymapAndroid[KeyEvent.KEYCODE_EQUALS] = (KEY_FLAG_UNICODE | 61); + // keymapAndroid[KeyEvent.KEYCODE_GRAVE] = (KEY_FLAG_UNICODE | 96); + // keymapAndroid[KeyEvent.KEYCODE_LEFT_BRACKET] = (KEY_FLAG_UNICODE | 91); + // keymapAndroid[KeyEvent.KEYCODE_RIGHT_BRACKET] = (KEY_FLAG_UNICODE | 93); + // keymapAndroid[KeyEvent.KEYCODE_MINUS] = (KEY_FLAG_UNICODE | 45); + // keymapAndroid[KeyEvent.KEYCODE_PERIOD] = (KEY_FLAG_UNICODE | 46); + // keymapAndroid[KeyEvent.KEYCODE_PLUS] = (KEY_FLAG_UNICODE | 43); + // keymapAndroid[KeyEvent.KEYCODE_POUND] = (KEY_FLAG_UNICODE | 35); + // keymapAndroid[KeyEvent.KEYCODE_SEMICOLON] = (KEY_FLAG_UNICODE | 59); + // keymapAndroid[KeyEvent.KEYCODE_SLASH] = (KEY_FLAG_UNICODE | 47); + // keymapAndroid[KeyEvent.KEYCODE_STAR] = (KEY_FLAG_UNICODE | 42); + + // special keys mapping + keymapExt = new int[256]; + keymapExt[context.getResources().getInteger(R.integer.keycode_F1)] = VK_F1; + keymapExt[context.getResources().getInteger(R.integer.keycode_F2)] = VK_F2; + keymapExt[context.getResources().getInteger(R.integer.keycode_F3)] = VK_F3; + keymapExt[context.getResources().getInteger(R.integer.keycode_F4)] = VK_F4; + keymapExt[context.getResources().getInteger(R.integer.keycode_F5)] = VK_F5; + keymapExt[context.getResources().getInteger(R.integer.keycode_F6)] = VK_F6; + keymapExt[context.getResources().getInteger(R.integer.keycode_F7)] = VK_F7; + keymapExt[context.getResources().getInteger(R.integer.keycode_F8)] = VK_F8; + keymapExt[context.getResources().getInteger(R.integer.keycode_F9)] = VK_F9; + keymapExt[context.getResources().getInteger(R.integer.keycode_F10)] = VK_F10; + keymapExt[context.getResources().getInteger(R.integer.keycode_F11)] = VK_F11; + keymapExt[context.getResources().getInteger(R.integer.keycode_F12)] = VK_F12; + keymapExt[context.getResources().getInteger(R.integer.keycode_tab)] = VK_TAB; + keymapExt[context.getResources().getInteger(R.integer.keycode_print)] = VK_PRINT; + keymapExt[context.getResources().getInteger(R.integer.keycode_insert)] = + VK_INSERT | VK_EXT_KEY; + keymapExt[context.getResources().getInteger(R.integer.keycode_delete)] = + VK_DELETE | VK_EXT_KEY; + keymapExt[context.getResources().getInteger(R.integer.keycode_home)] = VK_HOME | VK_EXT_KEY; + keymapExt[context.getResources().getInteger(R.integer.keycode_end)] = VK_END | VK_EXT_KEY; + keymapExt[context.getResources().getInteger(R.integer.keycode_pgup)] = + VK_PRIOR | VK_EXT_KEY; + keymapExt[context.getResources().getInteger(R.integer.keycode_pgdn)] = VK_NEXT | VK_EXT_KEY; + + // numpad mapping + keymapExt[context.getResources().getInteger(R.integer.keycode_numpad_0)] = VK_NUMPAD0; + keymapExt[context.getResources().getInteger(R.integer.keycode_numpad_1)] = VK_NUMPAD1; + keymapExt[context.getResources().getInteger(R.integer.keycode_numpad_2)] = VK_NUMPAD2; + keymapExt[context.getResources().getInteger(R.integer.keycode_numpad_3)] = VK_NUMPAD3; + keymapExt[context.getResources().getInteger(R.integer.keycode_numpad_4)] = VK_NUMPAD4; + keymapExt[context.getResources().getInteger(R.integer.keycode_numpad_5)] = VK_NUMPAD5; + keymapExt[context.getResources().getInteger(R.integer.keycode_numpad_6)] = VK_NUMPAD6; + keymapExt[context.getResources().getInteger(R.integer.keycode_numpad_7)] = VK_NUMPAD7; + keymapExt[context.getResources().getInteger(R.integer.keycode_numpad_8)] = VK_NUMPAD8; + keymapExt[context.getResources().getInteger(R.integer.keycode_numpad_9)] = VK_NUMPAD9; + keymapExt[context.getResources().getInteger(R.integer.keycode_numpad_numlock)] = VK_NUMLOCK; + keymapExt[context.getResources().getInteger(R.integer.keycode_numpad_add)] = VK_ADD; + keymapExt[context.getResources().getInteger(R.integer.keycode_numpad_comma)] = VK_DECIMAL; + keymapExt[context.getResources().getInteger(R.integer.keycode_numpad_divide)] = + VK_DIVIDE | VK_EXT_KEY; + keymapExt[context.getResources().getInteger(R.integer.keycode_numpad_enter)] = + VK_RETURN | VK_EXT_KEY; + keymapExt[context.getResources().getInteger(R.integer.keycode_numpad_multiply)] = + VK_MULTIPLY; + keymapExt[context.getResources().getInteger(R.integer.keycode_numpad_subtract)] = + VK_SUBTRACT; + keymapExt[context.getResources().getInteger(R.integer.keycode_numpad_equals)] = + (KEY_FLAG_UNICODE | 61); + keymapExt[context.getResources().getInteger(R.integer.keycode_numpad_left_paren)] = + (KEY_FLAG_UNICODE | 40); + keymapExt[context.getResources().getInteger(R.integer.keycode_numpad_right_paren)] = + (KEY_FLAG_UNICODE | 41); + + // cursor key codes + keymapExt[context.getResources().getInteger(R.integer.keycode_up)] = VK_UP | VK_EXT_KEY; + keymapExt[context.getResources().getInteger(R.integer.keycode_down)] = VK_DOWN | VK_EXT_KEY; + keymapExt[context.getResources().getInteger(R.integer.keycode_left)] = VK_LEFT | VK_EXT_KEY; + keymapExt[context.getResources().getInteger(R.integer.keycode_right)] = + VK_RIGHT | VK_EXT_KEY; + keymapExt[context.getResources().getInteger(R.integer.keycode_enter)] = + VK_RETURN | VK_EXT_KEY; + keymapExt[context.getResources().getInteger(R.integer.keycode_backspace)] = VK_BACK; + + // shared keys + keymapExt[context.getResources().getInteger(R.integer.keycode_win)] = VK_LWIN | VK_EXT_KEY; + keymapExt[context.getResources().getInteger(R.integer.keycode_menu)] = VK_APPS | VK_EXT_KEY; + keymapExt[context.getResources().getInteger(R.integer.keycode_esc)] = VK_ESCAPE; + + /* keymapExt[context.getResources().getInteger(R.integer.keycode_modifier_ctrl)] = + VK_LCONTROL; keymapExt[context.getResources().getInteger(R.integer.keycode_modifier_alt)] + = VK_LMENU; + keymapExt[context.getResources().getInteger(R.integer.keycode_modifier_shift)] = + VK_LSHIFT; + */ + // get custom keyboard key codes + keymapExt[context.getResources().getInteger(R.integer.keycode_specialkeys_keyboard)] = + EXTKEY_KBFUNCTIONKEYS; + keymapExt[context.getResources().getInteger(R.integer.keycode_numpad_keyboard)] = + EXTKEY_KBNUMPAD; + keymapExt[context.getResources().getInteger(R.integer.keycode_cursor_keyboard)] = + EXTKEY_KBCURSOR; + + keymapExt[context.getResources().getInteger(R.integer.keycode_toggle_shift)] = + (KEY_FLAG_TOGGLE | VK_LSHIFT); + keymapExt[context.getResources().getInteger(R.integer.keycode_toggle_ctrl)] = + (KEY_FLAG_TOGGLE | VK_LCONTROL); + keymapExt[context.getResources().getInteger(R.integer.keycode_toggle_alt)] = + (KEY_FLAG_TOGGLE | VK_LMENU); + keymapExt[context.getResources().getInteger(R.integer.keycode_toggle_win)] = + (KEY_FLAG_TOGGLE | VK_LWIN); + + initialized = true; + } + + public void reset(KeyProcessingListener listener) + { + shiftPressed = false; + ctrlPressed = false; + altPressed = false; + winPressed = false; + setKeyProcessingListener(listener); + } + + public void setKeyProcessingListener(KeyProcessingListener listener) + { + this.listener = listener; + } + + public boolean processAndroidKeyEvent(KeyEvent event) + { + switch (event.getAction()) + { + // we only process down events + case KeyEvent.ACTION_UP: + { + return false; + } + + case KeyEvent.ACTION_DOWN: + { + boolean modifierActive = isModifierPressed(); + // if a modifier is pressed we will send a VK event (if possible) so that key + // combinations will be recognized correctly. Otherwise we will send the unicode + // key. At the end we will reset all modifiers and notifiy our listener. + int vkcode = getVirtualKeyCode(event.getKeyCode()); + if ((vkcode & KEY_FLAG_UNICODE) != 0) + listener.processUnicodeKey(vkcode & (~KEY_FLAG_UNICODE)); + // if we got a valid vkcode send it - except for letters/numbers if a modifier is + // active + else if (vkcode > 0 && + (event.getMetaState() & (KeyEvent.META_ALT_ON | KeyEvent.META_SHIFT_ON | + KeyEvent.META_SYM_ON)) == 0) + { + listener.processVirtualKey(vkcode, true); + listener.processVirtualKey(vkcode, false); + } + else if (event.isShiftPressed() && vkcode != 0) + { + listener.processVirtualKey(VK_LSHIFT, true); + listener.processVirtualKey(vkcode, true); + listener.processVirtualKey(vkcode, false); + listener.processVirtualKey(VK_LSHIFT, false); + } + else if (event.getUnicodeChar() != 0) + listener.processUnicodeKey(event.getUnicodeChar()); + else + return false; + + // reset any pending toggle states if a modifier was pressed + if (modifierActive) + resetModifierKeysAfterInput(false); + return true; + } + + case KeyEvent.ACTION_MULTIPLE: + { + String str = event.getCharacters(); + for (int i = 0; i < str.length(); i++) + listener.processUnicodeKey(str.charAt(i)); + return true; + } + + default: + break; + } + return false; + } + + public void processCustomKeyEvent(int keycode) + { + int extCode = getExtendedKeyCode(keycode); + if (extCode == 0) + return; + + // toggle button pressed? + if ((extCode & KEY_FLAG_TOGGLE) != 0) + { + processToggleButton(extCode & (~KEY_FLAG_TOGGLE)); + return; + } + + // keyboard switch button pressed? + if (extCode == EXTKEY_KBFUNCTIONKEYS || extCode == EXTKEY_KBNUMPAD || + extCode == EXTKEY_KBCURSOR) + { + switchKeyboard(extCode); + return; + } + + // nope - see if we got a unicode or vk + if ((extCode & KEY_FLAG_UNICODE) != 0) + listener.processUnicodeKey(extCode & (~KEY_FLAG_UNICODE)); + else + { + listener.processVirtualKey(extCode, true); + listener.processVirtualKey(extCode, false); + } + + resetModifierKeysAfterInput(false); + } + + public void sendAltF4() + { + listener.processVirtualKey(VK_LMENU, true); + listener.processVirtualKey(VK_F4, true); + listener.processVirtualKey(VK_F4, false); + listener.processVirtualKey(VK_LMENU, false); + } + + private boolean isModifierPressed() + { + return (shiftPressed || ctrlPressed || altPressed || winPressed); + } + + public int getModifierState(int keycode) + { + int modifierCode = getExtendedKeyCode(keycode); + + // check and get real modifier keycode + if ((modifierCode & KEY_FLAG_TOGGLE) == 0) + return -1; + modifierCode = modifierCode & (~KEY_FLAG_TOGGLE); + + switch (modifierCode) + { + case VK_LSHIFT: + { + return (shiftPressed ? (isShiftLocked ? KEYSTATE_LOCKED : KEYSTATE_ON) + : KEYSTATE_OFF); + } + case VK_LCONTROL: + { + return (ctrlPressed ? (isCtrlLocked ? KEYSTATE_LOCKED : KEYSTATE_ON) + : KEYSTATE_OFF); + } + case VK_LMENU: + { + return (altPressed ? (isAltLocked ? KEYSTATE_LOCKED : KEYSTATE_ON) : KEYSTATE_OFF); + } + case VK_LWIN: + { + return (winPressed ? (isWinLocked ? KEYSTATE_LOCKED : KEYSTATE_ON) : KEYSTATE_OFF); + } + } + + return -1; + } + + private int getVirtualKeyCode(int keycode) + { + if (keycode >= 0 && keycode <= 0xFF) + return keymapAndroid[keycode]; + return 0; + } + + private int getExtendedKeyCode(int keycode) + { + if (keycode >= 0 && keycode <= 0xFF) + return keymapExt[keycode]; + return 0; + } + + private void processToggleButton(int keycode) + { + switch (keycode) + { + case VK_LSHIFT: + { + if (!checkToggleModifierLock(VK_LSHIFT)) + { + isShiftLocked = false; + shiftPressed = !shiftPressed; + listener.processVirtualKey(VK_LSHIFT, shiftPressed); + } + else + isShiftLocked = true; + break; + } + case VK_LCONTROL: + { + if (!checkToggleModifierLock(VK_LCONTROL)) + { + isCtrlLocked = false; + ctrlPressed = !ctrlPressed; + listener.processVirtualKey(VK_LCONTROL, ctrlPressed); + } + else + isCtrlLocked = true; + break; + } + case VK_LMENU: + { + if (!checkToggleModifierLock(VK_LMENU)) + { + isAltLocked = false; + altPressed = !altPressed; + listener.processVirtualKey(VK_LMENU, altPressed); + } + else + isAltLocked = true; + break; + } + case VK_LWIN: + { + if (!checkToggleModifierLock(VK_LWIN)) + { + isWinLocked = false; + winPressed = !winPressed; + listener.processVirtualKey(VK_LWIN | VK_EXT_KEY, winPressed); + } + else + isWinLocked = true; + break; + } + } + listener.modifiersChanged(); + } + + public void clearlAllModifiers() + { + resetModifierKeysAfterInput(true); + } + + private void resetModifierKeysAfterInput(boolean force) + { + if (shiftPressed && (!isShiftLocked || force)) + { + listener.processVirtualKey(VK_LSHIFT, false); + shiftPressed = false; + } + if (ctrlPressed && (!isCtrlLocked || force)) + { + listener.processVirtualKey(VK_LCONTROL, false); + ctrlPressed = false; + } + if (altPressed && (!isAltLocked || force)) + { + listener.processVirtualKey(VK_LMENU, false); + altPressed = false; + } + if (winPressed && (!isWinLocked || force)) + { + listener.processVirtualKey(VK_LWIN | VK_EXT_KEY, false); + winPressed = false; + } + + if (listener != null) + listener.modifiersChanged(); + } + + private void switchKeyboard(int keycode) + { + switch (keycode) + { + case EXTKEY_KBFUNCTIONKEYS: + { + listener.switchKeyboard(KEYBOARD_TYPE_FUNCTIONKEYS); + break; + } + + case EXTKEY_KBNUMPAD: + { + listener.switchKeyboard(KEYBOARD_TYPE_NUMPAD); + break; + } + + case EXTKEY_KBCURSOR: + { + listener.switchKeyboard(KEYBOARD_TYPE_CURSOR); + break; + } + + default: + break; + } + } + + private boolean checkToggleModifierLock(int keycode) + { + long now = System.currentTimeMillis(); + + // was the same modifier hit? + if (lastModifierKeyCode != keycode) + { + lastModifierKeyCode = keycode; + lastModifierTime = now; + return false; + } + + // within a certain time interval? + if (lastModifierTime + 800 > now) + { + lastModifierTime = 0; + return true; + } + else + { + lastModifierTime = now; + return false; + } + } + + // interface that gets called for input handling + public interface KeyProcessingListener { + abstract void processVirtualKey(int virtualKeyCode, boolean down); + + abstract void processUnicodeKey(int unicodeKey); + + abstract void switchKeyboard(int keyboardType); + + abstract void modifiersChanged(); + } +} diff --git a/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/utils/Mouse.java b/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/utils/Mouse.java new file mode 100644 index 0000000..11f1d3e --- /dev/null +++ b/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/utils/Mouse.java @@ -0,0 +1,64 @@ +/* + Android Mouse Input Mapping + + Copyright 2013 Thincast Technologies GmbH, Author: Martin Fleisz + + This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. + If a copy of the MPL was not distributed with this file, You can obtain one at + http://mozilla.org/MPL/2.0/. +*/ + +package com.freerdp.freerdpcore.utils; + +import android.content.Context; + +import com.freerdp.freerdpcore.presentation.ApplicationSettingsActivity; + +public class Mouse +{ + + private final static int PTRFLAGS_LBUTTON = 0x1000; + private final static int PTRFLAGS_RBUTTON = 0x2000; + + private final static int PTRFLAGS_DOWN = 0x8000; + private final static int PTRFLAGS_MOVE = 0x0800; + + private final static int PTRFLAGS_WHEEL = 0x0200; + private final static int PTRFLAGS_WHEEL_NEGATIVE = 0x0100; + + public static int getLeftButtonEvent(Context context, boolean down) + { + if (ApplicationSettingsActivity.getSwapMouseButtons(context)) + return (PTRFLAGS_RBUTTON | (down ? PTRFLAGS_DOWN : 0)); + else + return (PTRFLAGS_LBUTTON | (down ? PTRFLAGS_DOWN : 0)); + } + + public static int getRightButtonEvent(Context context, boolean down) + { + if (ApplicationSettingsActivity.getSwapMouseButtons(context)) + return (PTRFLAGS_LBUTTON | (down ? PTRFLAGS_DOWN : 0)); + else + return (PTRFLAGS_RBUTTON | (down ? PTRFLAGS_DOWN : 0)); + } + + public static int getMoveEvent() + { + return PTRFLAGS_MOVE; + } + + public static int getScrollEvent(Context context, boolean down) + { + int flags = PTRFLAGS_WHEEL; + + // invert scrolling? + if (ApplicationSettingsActivity.getInvertScrolling(context)) + down = !down; + + if (down) + flags |= (PTRFLAGS_WHEEL_NEGATIVE | 0x0088); + else + flags |= 0x0078; + return flags; + } +} diff --git a/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/utils/RDPFileParser.java b/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/utils/RDPFileParser.java new file mode 100644 index 0000000..413050a --- /dev/null +++ b/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/utils/RDPFileParser.java @@ -0,0 +1,111 @@ +/* + Simple .RDP file parser + + Copyright 2013 Blaz Bacnik + + This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. + If a copy of the MPL was not distributed with this file, You can obtain one at + http://mozilla.org/MPL/2.0/. +*/ + +package com.freerdp.freerdpcore.utils; + +import java.io.BufferedReader; +import java.io.FileReader; +import java.io.IOException; +import java.util.HashMap; +import java.util.Locale; + +public class RDPFileParser +{ + + private static final int MAX_ERRORS = 20; + private static final int MAX_LINES = 500; + + private HashMap options; + + public RDPFileParser() + { + init(); + } + + public RDPFileParser(String filename) throws IOException + { + init(); + parse(filename); + } + + private void init() + { + options = new HashMap(); + } + + public void parse(String filename) throws IOException + { + BufferedReader br = new BufferedReader(new FileReader(filename)); + String line = null; + + int errors = 0; + int lines = 0; + boolean ok; + + while ((line = br.readLine()) != null) + { + lines++; + ok = false; + + if (errors > MAX_ERRORS || lines > MAX_LINES) + { + br.close(); + throw new IOException("Parsing limits exceeded"); + } + + String[] fields = line.split(":", 3); + + if (fields.length == 3) + { + if (fields[1].equals("s")) + { + options.put(fields[0].toLowerCase(Locale.ENGLISH), fields[2]); + ok = true; + } + else if (fields[1].equals("i")) + { + try + { + Integer i = Integer.parseInt(fields[2]); + options.put(fields[0].toLowerCase(Locale.ENGLISH), i); + ok = true; + } + catch (NumberFormatException e) + { + } + } + else if (fields[1].equals("b")) + { + ok = true; + } + } + + if (!ok) + errors++; + } + br.close(); + } + + public String getString(String optionName) + { + if (options.get(optionName) instanceof String) + return (String)options.get(optionName); + else + return null; + } + + public Integer getInteger(String optionName) + { + if (options.get(optionName) instanceof Integer) + return (Integer)options.get(optionName); + else + return null; + } +} diff --git a/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/utils/SeparatedListAdapter.java b/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/utils/SeparatedListAdapter.java new file mode 100644 index 0000000..659732d --- /dev/null +++ b/client/Android/Studio/freeRDPCore/src/main/java/com/freerdp/freerdpcore/utils/SeparatedListAdapter.java @@ -0,0 +1,208 @@ +/* + Separated List Adapter + Taken from http://jsharkey.org/blog/2008/08/18/separating-lists-with-headers-in-android-09/ + + Copyright Jeff Sharkey + + This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. + If a copy of the MPL was not distributed with this file, You can obtain one at + http://mozilla.org/MPL/2.0/. +*/ + +package com.freerdp.freerdpcore.utils; + +import android.content.Context; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Adapter; +import android.widget.ArrayAdapter; +import android.widget.BaseAdapter; + +import com.freerdp.freerdpcore.R; + +import java.util.LinkedHashMap; +import java.util.Map; + +public class SeparatedListAdapter extends BaseAdapter +{ + + public final static int TYPE_SECTION_HEADER = 0; + public final Map sections = new LinkedHashMap(); + public final ArrayAdapter headers; + + public SeparatedListAdapter(Context context) + { + headers = new ArrayAdapter(context, R.layout.list_header); + } + + public void addSection(String section, Adapter adapter) + { + this.headers.add(section); + this.sections.put(section, adapter); + } + + public void setSectionTitle(int section, String title) + { + String oldTitle = this.headers.getItem(section); + + // remove/add to headers array + this.headers.remove(oldTitle); + this.headers.insert(title, section); + + // remove/add to section map + Adapter adapter = this.sections.get(oldTitle); + this.sections.remove(oldTitle); + this.sections.put(title, adapter); + } + + public Object getItem(int position) + { + for (int i = 0; i < headers.getCount(); i++) + { + String section = headers.getItem(i); + Adapter adapter = sections.get(section); + + // ignore empty sections + if (adapter.getCount() > 0) + { + int size = adapter.getCount() + 1; + + // check if position inside this section + if (position == 0) + return section; + if (position < size) + return adapter.getItem(position - 1); + + // otherwise jump into next section + position -= size; + } + } + return null; + } + + public int getCount() + { + // total together all sections, plus one for each section header (except if the section is + // empty) + int total = 0; + for (Adapter adapter : this.sections.values()) + total += ((adapter.getCount() > 0) ? adapter.getCount() + 1 : 0); + return total; + } + + public int getViewTypeCount() + { + // assume that headers count as one, then total all sections + int total = 1; + for (Adapter adapter : this.sections.values()) + total += adapter.getViewTypeCount(); + return total; + } + + public int getItemViewType(int position) + { + int type = 1; + for (int i = 0; i < headers.getCount(); i++) + { + String section = headers.getItem(i); + Adapter adapter = sections.get(section); + + // skip empty sections + if (adapter.getCount() > 0) + { + int size = adapter.getCount() + 1; + + // check if position inside this section + if (position == 0) + return TYPE_SECTION_HEADER; + if (position < size) + return type + adapter.getItemViewType(position - 1); + + // otherwise jump into next section + position -= size; + type += adapter.getViewTypeCount(); + } + } + return -1; + } + + public boolean areAllItemsSelectable() + { + return false; + } + + public boolean isEnabled(int position) + { + return (getItemViewType(position) != TYPE_SECTION_HEADER); + } + + @Override public View getView(int position, View convertView, ViewGroup parent) + { + int sectionnum = 0; + for (int i = 0; i < headers.getCount(); i++) + { + String section = headers.getItem(i); + Adapter adapter = sections.get(section); + + // skip empty sections + if (adapter.getCount() > 0) + { + int size = adapter.getCount() + 1; + + // check if position inside this section + if (position == 0) + return headers.getView(sectionnum, convertView, parent); + if (position < size) + return adapter.getView(position - 1, null, parent); + + // otherwise jump into next section + position -= size; + } + sectionnum++; + } + return null; + } + + @Override public long getItemId(int position) + { + for (int i = 0; i < headers.getCount(); i++) + { + String section = headers.getItem(i); + Adapter adapter = sections.get(section); + if (adapter.getCount() > 0) + { + int size = adapter.getCount() + 1; + + // check if position inside this section + if (position < size) + return adapter.getItemId(position - 1); + + // otherwise jump into next section + position -= size; + } + } + return -1; + } + + public String getSectionForPosition(int position) + { + int curPos = 0; + for (int i = 0; i < headers.getCount(); i++) + { + String section = headers.getItem(i); + Adapter adapter = sections.get(section); + if (adapter.getCount() > 0) + { + int size = adapter.getCount() + 1; + + // check if position inside this section + if (position >= curPos && position < (curPos + size)) + return section.toString(); + + // otherwise jump into next section + curPos += size; + } + } + return null; + } +} \ No newline at end of file diff --git a/client/Android/Studio/freeRDPCore/src/main/res/drawable-hdpi/icon_button_add.png b/client/Android/Studio/freeRDPCore/src/main/res/drawable-hdpi/icon_button_add.png new file mode 100644 index 0000000000000000000000000000000000000000..19104c8e9c6dba7ce51078b1c705067dcf5e2dcb GIT binary patch literal 1086 zcmV-E1i|}>P)gpte5uE(*6miG>~H1 zn+CnD1WQ7rm7w5Npb`lNw%ulv&7O}L$uI{F2)jw3US(fccIKV)8|Im3&b%uvuPP+3 z>hnLM&(M0%fbRf9ip2sT5GbVot@!fvXhAw6=E8Z^kfWl(w%JLyP1CN(U4|?aA4uZiRseeCEdIB+UKnk zZ{xiQx%S$^n~d=W!;}e}!V*nLg*u~*F|rV@-JduidTF}wYkgvr5z1%;mVAIDq{av% z6O|_qrcWTq{kQd6t@b(C`=Zl zMC>i^cK%|!^Uv}=NiHYc#ujhBHFJ;`zH1&~;5Gx^GN0Agal za8{G4Lt+eZHCasOjYGTKcz<+FIxtuw;^tWA7vFL!BZ%qxrP_{d>UrsP2UY8b_QQ5#n$lbp^s^lOs`*dAipr~HdlzdAd+Wpa6+43v` zv9S1&*b#uHa4lF>804(HZ5>RG>P7(A487GCZP#sT6<90*!-S2fVE~D)SrRmrTIGF( zL^x{}0fPWUYS;UsKW_c>m|5!=8B*}(f`~2&5KtsU<8VmtCIOG9S_4j_T{5|I27=53 ziL;==`aS^|-xk|P$@(<~g3a{SF)y&6$HO%{ z(#zmpl86{Vfnu)2ahtl!%)491M8o=uJx+;+-zYZJr_6~u=S7Jwf6x*u=KEy3M}%Se^p$Evx~=boceE45 zdY308SS%@=8tq_)7(;^exP-)gM;!Kjv|pV4xjRGu3t4bF#u4_^k^lez07*qoM6N<$ Eg8ixmyZ`_I literal 0 HcmV?d00001 diff --git a/client/Android/Studio/freeRDPCore/src/main/res/drawable-hdpi/icon_edittext_clear.png b/client/Android/Studio/freeRDPCore/src/main/res/drawable-hdpi/icon_edittext_clear.png new file mode 100644 index 0000000000000000000000000000000000000000..ae185579e235866f7aa3274ea3a11959437fde24 GIT binary patch literal 374 zcmV-+0g3*JP)&0Jy|I@E-_-nQ+74Of%E zPX@3$TzKaz!1;ApuR|Y>Q^0iy61s3)1tLi3!p$>OkPzV3Q$sW)06%iU&9^ENW>Xh@ z3yEE?6DZ5_S$OB7L{StkQ%>N3a&3Mz7u=SxawxZCiBrl2)-8H7$`Y#!9wtnfUprI62><{907*qoM6N<$g6fW(qyPW_ literal 0 HcmV?d00001 diff --git a/client/Android/Studio/freeRDPCore/src/main/res/drawable-hdpi/icon_edittext_search.png b/client/Android/Studio/freeRDPCore/src/main/res/drawable-hdpi/icon_edittext_search.png new file mode 100644 index 0000000000000000000000000000000000000000..2283a919441230f87e7446b1f51676ba85fcc43c GIT binary patch literal 1299 zcmV+u1?>8XP)0YGbiQ0;FhU|T?;b^@$oV1^lR#sL#&&=tpkb)`N96huXtz&2Di zyDe_U1v4UxGn%@1H!s~1v&$`++ivE}?)(eR&ikHrygdpVxQpd;_dL(}wl{4 zc)$);rT1*ZZ0E=UT;A?+_oDS>1P z<0Xssi#DZ`zS_+2xl-_e|Fzn4dq?&m6T*e?AAO{Yv{GG<`WC_=Ozn2jV98-SeO1Nq zxiav$1Lt2J??5Jsza1vkL|q9CQIqP2A&jQ>wI4XH+ifoJrS@rPe`oLZ$ybrV*q@z5 z?SnKl`M5nAP2H4V@r*g4EV<)zh1+IYM)#m--?2w~h{6L;B8tcD`;lGMPdXQzx)QeG zb8U?cr`u4}G7=voicRoTGBZbx=mG1m}t zu_>SH#KzGUY{8vFJ*P`yCEH;1ofZ`Nr)p;$Y|7^X6Tzd+*o=XRP}l{R#najwg&qwQ(5Ge@Bc0@dvDEsZcZ^;7W{54h<#P_0?XN_}^qKa$#;F1|eNMk8 zRS3DDqVgOYE*4L+E8ozO<0eSrb0&BgcmchVO~d*xa_^`PDM66YfU0}_72A|I#;yVr zKi2BN!0PjJr;x_9de`W{E-xYRCs2==q$E zBDVL!ZcK5Gn$8u>=iXF*qezhj!# z+z>8h$Ay;!Q1Q9^Boe$j;6>XCyqMaX$BpmiUl#5NO*won_ix1gAT$u!h)*{#_#kw@ zr(8BEhzp6FrtI{bMuwnM*Dd-mTI+Mh!EA|1$L&z_V1&M6ub{l9-i{RR5g(?3|B%9sEE002ov JPDHLkV1j20f1CgS literal 0 HcmV?d00001 diff --git a/client/Android/Studio/freeRDPCore/src/main/res/drawable-hdpi/icon_launcher_freerdp.png b/client/Android/Studio/freeRDPCore/src/main/res/drawable-hdpi/icon_launcher_freerdp.png new file mode 100644 index 0000000000000000000000000000000000000000..ff31f25761bfc55a7cf6922797999b9653a3f7f2 GIT binary patch literal 2858 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 IM6N<$f`Za3c>n+a literal 0 HcmV?d00001 diff --git a/client/Android/Studio/freeRDPCore/src/main/res/drawable-hdpi/icon_menu_add.png b/client/Android/Studio/freeRDPCore/src/main/res/drawable-hdpi/icon_menu_add.png new file mode 100644 index 0000000000000000000000000000000000000000..46b50d6ed484480ee2cf357479a6521a5724184e GIT binary patch literal 2550 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 M07*qoM6N<$f`0G6SpWb4 literal 0 HcmV?d00001 diff --git a/client/Android/Studio/freeRDPCore/src/main/res/drawable-hdpi/icon_menu_close.png b/client/Android/Studio/freeRDPCore/src/main/res/drawable-hdpi/icon_menu_close.png new file mode 100644 index 0000000000000000000000000000000000000000..82d41702e39d4bae93182c62bc295bbe911db9c7 GIT binary patch literal 3105 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 literal 0 HcmV?d00001 diff --git a/client/Android/Studio/freeRDPCore/src/main/res/drawable-hdpi/icon_menu_disconnect.png b/client/Android/Studio/freeRDPCore/src/main/res/drawable-hdpi/icon_menu_disconnect.png new file mode 100644 index 0000000000000000000000000000000000000000..192c57bb380bb7cc6acd7db17d2997f36a05b640 GIT binary patch literal 4424 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 literal 0 HcmV?d00001 diff --git a/client/Android/Studio/freeRDPCore/src/main/res/drawable-hdpi/icon_menu_help.png b/client/Android/Studio/freeRDPCore/src/main/res/drawable-hdpi/icon_menu_help.png new file mode 100644 index 0000000000000000000000000000000000000000..8ba27516dc4d459796c2ea691b8e18e1977a51ba GIT binary patch literal 2590 zcmV+(3gPvMP);(Gw*~O6G3)f==BCcB>*{K5=B}E#>*}t%rmm^!!pam`P{cPvP2P7w7WR#W zWm)z`R^F6M5!Bx5^)KY3M2}lor*rV|1q7RQ*>SRAt4wA0z^ne+kTd*U641&8`}{3)~wCi+POu^ znU*Ip4gw?y#N{Q>0lH4w!~Iv$BoZO=)w`E0({7LZJg#d#mLVNkoKz$ufyJGTJ(5t9 z8@+o9B+z|gI1z28Yc2$buak)o=Kj?Jd)$Qt9>f4MY$#&IszY`7?P?*j!i;RZjCiJP zPlxhi@RbKHR>JJCA_a4x z-IX~5WA=*05(N(qbdA);?U5lCJ)mu!3q9-sC)_AQIVw=asqTfZD*2udUX-GQ75JAT z9&=s)o&?D!5q){>CghE< zG-72F$VvtYVp3{cXDW*Qd@{i>)qSukwaQ-^q)M$0^oLprFI>#3E`YYJ>_NyqQ);0R z>Rs-63Zx~AwB0(Cs2;3DC8w$v#zSB=UgfOeuHjKC)k?MZrsKCSlgQrBj}ko)1FXyJEh$69NV0pM3BS zJ9Lfv*P@E`)s@2s4+_m}1qULa-3S)VojP-YU;bvKwxA)tL5cCAXPy<~gy#z{Hor=<6ATMVHhksSwa5h2s zaKhFw2)2i3b|k~lo#$WXkM-wFBf3Y_i|3{Fk9*JXS{CdH+6^0$Q2IwE(Qqb2bXY!k z%c3Lsa4`=qfd{f9r-A6>Nf>i>e0q+fswEQ+xUQgtrD9f>_Vt*ZF)R&5aKgo0 z7gu6+^RB4B3q;l7L}?l6s*_#bSIP>Ja&q1M0g(tpJ+Mp-(Xx%p;Z#d85C;m7iA8uB zdcJaiKiFT1bvs+`h47l_k&44Ae>D4m9;E|%oy?BRPRn$nC=rCA1X9SQK$E`1!~>O} z2WOCypdBcL3HdCanNid;LspGVtN!y-2uGDI(mx`g#JLku5a{m=e&@C$JCKuco}#58 z1`>sn;-Sg6oer%pY`d5ONV-7x6&#F8q*F7 z3RN3yD7*|CcW{>#vRoMEldx;ju1H_hOMB*Gd3PQxu=Nt6%N(#~T%dSqh=GXAVMKYG zEU>8s7@VT+6eN~I!Z@O&_!z&=pAR#vgVi%D%$=pviGD5)G0+^!P)?A^YDEF8SD}Dq z2WEA}R4$nooBRA1$uI^?unb<$EHIsHxStLE7bAkN!S`Ijw-#6!_aK;IL@H+CMa)1F za+nd8tHCTVwn5XZ3^7n|`7ybDlI19WC^KU)Ovn!yVGpBN6Z2Ok`VT;CqAA!l+tNt?iPD&JbuAIz|)eTXFr9wr-<8ml<#+d8!^u{&ix? zuJNyr-N*yIl9hryzX74hRV{F|Zi^y%SWszL0^LfBh4u;FDjN*`kb#(<#dRhm6G21N zeEZDSr7T~~4HbtOyY1h-HjQYhXr~cqvG^Em{)O|98O!;DaC)4EDPw7 z6AIM|_xp`envOu-Z&!{sEp3@ zNDa~6V`k_s>X;svqC*xkxpf@TS6PYZl2W0h!=z;I`%qtHi zsD7tuCdM8Q9@b|>1iEGdT_mE7G7-Y|+*ey#cHWLW=#YsFWCYUv>1vNNSO(w8h6zqq z^>O3uw%dsIN{u4XA-WFwgxf8y5s46a;GV{mH9IRlcb>PwjC|z603$50GtW0wM{C!< z_55c)hD4MnrUY*Qu9Hp>=vjJ5@PB``aCB<(oCec|vh|I%Ewx+f>NYwWbUU8Hcu?=v z!fc_QKpk{l6`%{Wn1~(|-nr!R1!8|io6;2A|C+wz+z^4&O+88z%jI>NhOo6tt)@Qg zHN_=AF7(C*;fK7y(+=Dnz2Y3zs75uaQQav07Z0IfoqhS*?EnA(07*qoM6N<$g3l<| A%>V!Z literal 0 HcmV?d00001 diff --git a/client/Android/Studio/freeRDPCore/src/main/res/drawable-hdpi/icon_menu_preferences.png b/client/Android/Studio/freeRDPCore/src/main/res/drawable-hdpi/icon_menu_preferences.png new file mode 100644 index 0000000000000000000000000000000000000000..d6ea16c3772b95d2748612225f12e3c83307eb6f GIT binary patch literal 2668 zcmV-y3X}DTP))5Hqjxu&SW34hOm_S0_Ap}5(5(1Dw9-HRUS5Gf)J@< zu|OE}5R2gyi!1;u@g0zlG=#WU&Y6*rmD-znFgbUF%vO>gN_{tNcT!_~(j!;F%FV?U zWkG0&1_BY zrX+`wBoL=2{IpWdRH`1U)awTuxS1W?$<0*C8@SL6Y2{At;?97Bnu@IRW+kNoD@2k2 zGR8PZ0iK9S>QDY)K`fAzwT_#0C%M^Y+RIz0H_&0Um@$6S9;21lyt(kUq?^+YrBG5e zOwvN@I)?X-0wmv%xOX|P~$9_v6d#IDz*b%y?^g>?xdiG6=)oh8&B@atgAp32v>E$lNoNH-a208Uj#yVQ!2bL=sIT=V1bCGb*61H;%)fx5N$Zcj&2X{~x^%`3W zfGQ1$x20Aj{c*vY@dxI=K7UtSF2EA#nX>s6FSi>VeBO3$)`(Mw^++yZ9xILud;~vip(Gmq@Je2mq5?;f3maBOoCG3s64JMtFJs)HlJ1m_csgr&0RYzS5AZ6Y{BbSS*Bv`Ij88v!M zu#$5*^>FM=m^3AJS{LCA*Qn(DmStFP8~K(AVBVDF9~W~iHCrES$!*+4jy#}JO}sVz z%UrhWrCiOS4c%nf7+lZk2j)*(B!NwP+qjh+Jft(&$VEGWGA$REM&fIbVksuxsYXjk z2rvj_9Rb}q>vNz)*Q%mpqDfIA5eXYKsFsR4&6s+}gylW-h&)+82!jX=YlIXMN=#cjAn%+pdoyYfWlL0%C9r0{)qxTN@NfVW7G$l^ zB-_=ippIsR#Br$u3=C}?=osWf!+8Vu#-lLsC{U@}KbV!;@eeKhdV&pk-l++?c*9so4Ya#?a6k09U@?3S6Zk*xfASfaN4i4%SGoC!ILROm z|40E+CaXcSgP2$BH8f&hHJBhamhkp{Ui zC;)w*013vB21i3nh`X~rI8%{{QLj(738@EyK|L4<>H$^xK}6Iu{M1CHBxu{9UK7a| z{7Mzc`rlg-Km|s~3ak-a9Q}dR7Z9DlSHj7$0*3RmbIXvoJ);L z9wwCLi?l#e!*%{_>yC+Z(L+?mt6N8NLqj&1)1b z>O_8(R;cSz#V{DmC&W>l#+et4{rC`PM2xsD;)#g6Y?uU%6%hd&WcU{t6BHw>pr=us zW&7xYfHGN1Wz%f0((M-~5g0_L#D7HqY#Jg06AdYa(4axJas*((dX%dEMSW~a;0W>= zqYGM(ji^-$jf#1;{=rDP#0DE+kU?ZHfdRn^qyp8737^bHgorv~Jet3+MXn}bltI9& z*d>0T9W?H$L*5x;aq;}{f87`o!b%xFkx3Pkgjn&8=WgY6@OC6%)D#1#MYjYt_0@B! zVW$$lly)vQwOiw&_lz=X;_!QXM7vSJt--D`z$459 z8gSXvq6%%IRRYf9=0G_Y>DF3&o7SA7U?sPG?#cul5@x2ib7&9CIoT6C*-P7Fzt$LLiRY(QFLZj${up%nd&DXhv zYuFL+)%Q;YI#f4+PK+r|E6FKg()|nHPUoRIE;q9Zczqy$aJ^~1>3S=rf;V!<$Nt{F zn_nLJ zV*p-s3BLl1XK&e_v~9yPHJj`E>ig@Tsd{$p_PFiW!osJxg&#inafNYXH?rWDy_!qK zCGl##TDJhU!-ww-KLDTTk$q~Ptw-~yZq2LqXnnRmwMY5^bmM!X#~SB_HO3idoN>na af98LNCIM`a-IA670000 literal 0 HcmV?d00001 diff --git a/client/Android/Studio/freeRDPCore/src/main/res/drawable-hdpi/icon_menu_settings.png b/client/Android/Studio/freeRDPCore/src/main/res/drawable-hdpi/icon_menu_settings.png new file mode 100644 index 0000000000000000000000000000000000000000..8dcef3e76b45c09111fbd69ef107ee90d8d85d33 GIT binary patch literal 2311 zcmV+i3HbJjP)0Rl)DHCx4rhTy_+P{jedNP~nRN)P}g8WoNb z79hmP!CAWoe*N#ss*{2LF%Rq>KH7E>YY`QQ)p*RHF>FsZ^O$=od3BP@iKp^RBomoa zci*{hkL9q#Unc_hgVE`s+wgmVkstw@8tR;ngJ=$9(x2q9P+ z9ib0l3gD$*TKpUb z=3}mz@UsbbPHev{1qM8o&Mk`)@Aj3r6!iI{5Pcy=q9HV$z{oonRG8DOtHaGt?z^>W z{n2;7SzfmJgLj8y&ui7Tx+L+Ik9`li%9YY2B8j#T&j~@B;+ofW(NRxgD4Y41tMZDw z6m+sLx6zcq=Gb}ixq`~M5X%Jc~VM+Q6Utv&f4XQDMAzmN4 zX;MOAf-7hwJFTr?$G^Rr|C5Ao4DA9me~#4aT{mqZ}r`i zywg2lgFf99a{I=3m!q^=X-9-~eF<5x%dRdLRpmW4e$}FR8U5uI<0@DfiBc0ahyGaR zC@~}kf#eCKd?dmuD9g=7(K*4h= z27oZE`kQ40V52tB$pa+S_)2|7hV8#`LD7oT>ndN%T)8aJKH0(9IpsattW3L!6Unxt zS+5tynie~gAel$w)|ldDEBZPw`CY$$?-$2*Y^hxaK!6rP`}%L)?iYB2u?CbTAppij zyFwbm&~@*(I%&193a)U4+~Pqxo7@;v+_Ym$x|IO|L9zAT%rQMW9WOC&c(cWvIJiXH z4uDC~u8;$dt(zRU(a(c{v{ivOJ*G>0k*YJoEwo3>GdggTOX4%i77u+mqxRZ^y~i}a zKcO@>Xc8P2?OTTiR17PJE>44W4R3flc6F@b7K6%AP@0ZD+b@(P-7LQR`G<4if$H*3 zhi*x49n*q)qx(D*ky3Fy+RNPxQXap_v3=M^qm_B2h4;iB9!|#@@UB(Lrlt z;;a}40*SMdLx}0|Ui_0jQKEr}lFLkSl2Q&V7&Hb3U4zvXI1;T>ra@@njFJk{BNdtD zFVCu;^Jm|uo^sKSr}C!2?%dqo3zoLgPgFTH0D%b@&9uvZ@Y(y)9~n2VN>o$38UYaU zLxh}v`FE~hiWf~7FExP^-9z0&IE{gCF}dXBxj)L=mFC=2?ch5ll|Tb!L*}IH8$P!z z-VE4#At2=9g^m*-94g|x5(J6{0@OqV#R@q*e&)6j8>@$vq&e4~bYKVs1}L$o8@$(w z0$e4b8TL}`qb#hw)p4yLieoh87IAGDMw3^M)#65J3=bC;J!@t_y1 zNjkUAI>|UbX+Ojs&VamdV1?&t1+2$Kk#b&5BIEl43eK|Mp!sU<4CYxm%#9?6&v2|Y z%6iD^&c5?25{likg&;1Hy;z3};Z1A;Q4Qd?Ms;BRlINJ?eAi+5dKzbO8nfApeU>tQ zwF_8=UG=>Ri?d^~TJ752^yO8BoM@k3(8}dZ7SCAKZAIUgHjLH1*n0`%EE6Ca00(OU zylc?4z0FHoCv$>5-pc2TOBhQ5-o9bdo}o?;_M1+*)yxLbAd~_qHmTn0JgaOHCxo8j z^nnA-ttp?R&^>n5Xg;C`?3r=Z&A40<4FmyvXb?qxW>gh~ALWp#0OM$Ul=0x_qja`2 zqV&aUfuAa(!8i`!bCYV}$mclPxwoSKsJAbF{Q92nJiw=%0v0Z%Ctu~DK`XQuK#`l? z1=9*SguR^ZpY`~A9}aUSbM~6|(if^xLa?FVkF1b2o?8F>>61B>4={_5hx1P9Y-vsP znF2PWf%ybLv3%vBcYfhi`zelL0cU@_z76FK1Nab^fCB*LOO>1M*GDhhy72R*i?_C> zddmRHaEUmA!vMCqfW0k0%><~{39t{Bkj*#=V1@|*g=QvzEx6-zUwbRr hNF$9j(#Zcm{{ZjE7dbmjeCq%J002ovPDHLkV1hLtUJU>M literal 0 HcmV?d00001 diff --git a/client/Android/Studio/freeRDPCore/src/main/res/drawable-hdpi/icon_menu_sys_keyboard.png b/client/Android/Studio/freeRDPCore/src/main/res/drawable-hdpi/icon_menu_sys_keyboard.png new file mode 100644 index 0000000000000000000000000000000000000000..3d824d30a7f7437f5e5706a165ba3f1f8b8a5d35 GIT binary patch literal 1397 zcmV-*1&aEKP)33ssAGrWc5z94Zk+q^g&SMyjdQa7d*R+zaWI8&Wy+$c=N6I5Z+x z4vnBHQgWdHDLsHHLy3AIfX%C+07)CaclzJaj^$nX6>Om(&lmr+_C9Rpm+{Q{KVXbW zGN2-3WO5fI1<6Q3QjioR1xZ0tkQ5{Z$w)y`kQ5{ZNkLMO6eI=7D2%aOBnlMH)ea-# zghntydwU-Mc$tqNJ5lQo z_#MxWkC7~h|I^H(5`r{ByiN$xx8??DMBe2X_IoggB+AdSc(2uMiGDo7L8<|s(~9X6W{cDvmHpp;PZ5yV4p#u0)( zyLAh7b#>6Qwzh`V)m8rCLqkJI&cwt7Z$MX97rJiTfXK?q3VtW__ZuD_Msmi-$9ZG! ze02v+O-&G4T3W)w!UCFU-`4{J5ZT$;!PL|Pw6rwC>-9q9$NTrO{cJm>pm;nE8cLB% zN>H33Nm|qxpjpuiqFD>EXpC<)rtuyW48A%C5F)gq6(r;k;Sa7|TQ4oOM`H0+EPhI&^$zPx=KB7r~v)6>(u z7xdh`iJqPwh%7HJV`*`T_lA*?5hQ19Yz%rvM@Nw=+uPfG76Sw5D+T&GEqezCzXR|G zaSm&WSrC22xS(&m-U|s*A(KszOwrF`m;M#2sj1;(kjv!~TTX3lEv{+`I?Z=?cc&br z4-O76J39kC^uLDI)>b6_`o!kbr`Y^+Q@<~P`}%d${JOER!RhGe;3UET-?|SzpAVg# zot$*FB2n5nJUqlNb903hRQOa@R-(SX9!cRdnVzbuDl{}UA~}J8A9|{*tHqDhM0WkV ziDS*p%_wl>k5z>~1aF9-{r!DYVN3KNasOkjKKlIuW9zrpg|NH33ku&ZE-t33D4NQy zr>AG8J83A~i;FlVoJ2UO9vv&{Slo@4?7KcJe~oXd4B_Nl%*zJ__%0r zep3<*26Z6_hr-~ML{-ttAC88Gh8_~MZuFvfG5?z9<>C@s2{cUu10z`>eVS4*tHA-S1_$vmaTP!p!`#2nqGmq!WGT literal 0 HcmV?d00001 diff --git a/client/Android/Studio/freeRDPCore/src/main/res/drawable-hdpi/icon_menu_touch_pointer.png b/client/Android/Studio/freeRDPCore/src/main/res/drawable-hdpi/icon_menu_touch_pointer.png new file mode 100644 index 0000000000000000000000000000000000000000..121b545a11d2f2b396ca4ca644d74434a59b3360 GIT binary patch literal 2083 zcmV+;2;BFHP)Px_OR(> znJ;@;Vgi7=@Sqx8L;wUStqMp1=R_KSmcRWA`~K?T=4LsS8O}~L_9ov{dVz#b&-dJO zZY!IZn26*hF|d~%kz*>L$Pq=u=*>MMBPaqCsaOOkQn4r`8i_^B{4tt89|7Xu6OSY# zX~?_CV&okpPQ!Z3fo9ylfB%!}>grR^o;}-%uRlbxkmVvQ986VS7oddp_V$k-K76>X zp`qc}ty{M)0_@E5=g&VAVPRg2L!yzWw_ya0BNJI!SxwKoyV;{hj~Jk4+HAJm%mq?{8Z7lAXc%!WUAnZ{Xf(3;_;{9(kbuN9I{Nzh*yG2KncZ#& zRui!99=?A4`p>B2nI~OcU*cOnLe?SgBdJJ|0xYE~X%Et$k&$tGe0-cy2+CwKX;}0~ zM{jQ*Yiny`cAIS`s<*zbzW$4fii%%qYiiB`@sN5fH9&sEosyDbi;IhM1_A-?1nQ6~1q}41nC49|Z8H-oZfahJn0t&NYWYyKbz$z;%E2^rh z3X6p#AVwrg9ndRW&G<2GEd9Ji&kytugs_^LnoNMQU}E1@T3Q;yYH4Yi#9E8Ihs;Cb z5QAbMam_GM+wkf;{eHjxoOCQiTa>n71y5Lj3SpI$loZ{$bLU*M&9)yup27EnF^Yl0 z*DNh9ZR(oI#nKj~*y=DW8$wu>m6dxhT)6N(PR&M)3V7=a=8y+6+=ife| z9w^+^jA|AaC#abkEPA97j2Fs&;IeJdYFCXl{nE7a7CfN0JE4o%25`8cwH^fgm(HKR>$c6PR%ZaEn~DTFuha|NpDmtXZ>IS67#X@4sdvW^7gsYJlE=nt41P*3;8txq9_#A->E+79sJ7 z;dKV$HA}N$lPR}m0wCIxb#``IaPR1AJe4IMNg9y)MI+riqTfF_I5>%PR6de?AXPQf zwi)#M38|x_LjsZtAYL*(y1jwA(0e>GdPX3edsYDVYiRmUen@K$wiXcLN6u>>R+b9BfZNiu7h{^s@iykj1ZJS2xaBxyjrW+R9b_iK2~#5juWtq5bpSyN~0k zI>d@JW@cty{&d^6^FE14ZZ{#dTku|*07*5Fpl0~6l~yya*UN^6hS>7u%d7V6*>jZN z?0Sm0kv^nt_3G8d+aw|dgM{P`i=-4tP_v%Jix*qzl_VUh)rO55ujS?CeTQ>RlQ+BY z{Uqo|1}2fVZ{IEj$!H{`poUZ;KvE5)uUYS^RjaPhN|&xuA7p2jY}&NxCtfMS8RM7? zj}sy!@AQyx5Ry?tasa8gM1-UsD2NQqn>VikbJ3~n4I9pE-n^OK`LrX>f8PldA>rqW zb8@CXlIS3$Kp@CmgyaBHaftv)9gx0ePq%K}dU4B^E$8v?M>-tgUn@q+Iq&!RB_RcH zbt5DPkctT@6G=j1l>+H^_((UeR6pVs;7pB_3nafxBwXEq^y{Tdbkl7iVpa}Be<%_} z{5(ND&Z{EjVkG$_BGC#SpJt<)B_c)zK++B|QdU-RZf>rWkwkYUX-$SX@(I771m7?q zF;W&sn$Da=%MN-^KQS@UdEmf-le8QmBn3d~kapzlc=eH_!=d-^4iy%jT)1FCEx&O1 z6acA4$`c|@jbnBe78aaDz5c}0UC6M4im69h3nZ+{|I@@x3&oL<5oR`-ocO5B369f= z3?Ttk3q|!vYu6U%11TUz(s1Zr6W$)T7Zem615Tv?M=6kcr2PEz{y;#F#Bl;49NY%B zA39Wc0{5S8Q>_Fzs)5ua?F3RFpp7J2>u5LyFFt|Ri2Q{Nhr>}1q<$p*==FHLv@xYk za;R1Z4<6^YR{lmt!cNHQfz%_dTet2qwk0Q$lauT5!OjYdoUa5QvU)53-+UAjgUmqY zAc?$213Y6I&f5v3AsIMSG%`85f*Ouopvdv&14V!$KoOt_Py{G)`~%>B(zlotG8+H@ N002ovPDHLkV1jaO+Ts8J literal 0 HcmV?d00001 diff --git a/client/Android/Studio/freeRDPCore/src/main/res/drawable-hdpi/icon_star_off.png b/client/Android/Studio/freeRDPCore/src/main/res/drawable-hdpi/icon_star_off.png new file mode 100644 index 0000000000000000000000000000000000000000..eeebc098a3064cff25375476b7a72baca94ceda5 GIT binary patch literal 1406 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 literal 0 HcmV?d00001 diff --git a/client/Android/Studio/freeRDPCore/src/main/res/drawable-hdpi/search_plate.9.png b/client/Android/Studio/freeRDPCore/src/main/res/drawable-hdpi/search_plate.9.png new file mode 100644 index 0000000000000000000000000000000000000000..8ab6ad770323c4de21acb76c4dc36444db6bc883 GIT binary patch literal 238 zcmeAS@N?(olHy`uVBq!ia0vp^3P2pp!3HG%onf5~q_%pxIEGZ*dOM4k?XZJ{OZv^* zZ(FaGH(ZULw%7Upq8e95mX!vn_Zft)8GlytSXJl!E=J#-f7K+DV;*xSJ3n`ESvhIb zcRhEdN4Fb{Lnj$s=1$0O;Osg1KciuJ1KWfNoHH6Y3&4oM!HEIL2T=tK3^EB!Y6(nI z2~5e%t@_J7B2=fEgaxRs+*-Dg>5S$2Ja#kl1E+aEM94jO#FVdQ&MBb@0I$7T3;+NC literal 0 HcmV?d00001 diff --git a/client/Android/Studio/freeRDPCore/src/main/res/drawable-hdpi/sym_keyboard_delete.png b/client/Android/Studio/freeRDPCore/src/main/res/drawable-hdpi/sym_keyboard_delete.png new file mode 100644 index 0000000000000000000000000000000000000000..104c1b92207ed778a811f9a0e68cc83f7bba2b3d GIT binary patch literal 1520 zcmVCz`=QmARHCLIOtg)*(ufBh z9EM;E9z?4K6>D3Mwzk!n)T)i9m&T?|)D%RgGwdttYU8pi@r(WcKfLokJmJSX^UO07 z5YN!h9jpW763hb~dVKN27jL}qz$O*-kx_qXU-WpQ!wYX|JvuzxgX!?2D{&-C#}h%2 zMChb_2_Z(t(nuwNFaq!+h)B$Il_Q;dFg;z1$JLfpY^~V3Vx{#}GKs{1p5O(;>(*D6 zm2a!wSig4rYZ4J0pR)gh`nAR5%$K;@QlY7(<CmRx3mbcsg#~%zPCr=Sei_MVEg8Mr`)p(57w4Mq3UTI3`eS~u&ptV`d8amw zF}8hn?8gl?H5=-Rh`{%$I!IH~@dH?i!l-o+dx1n_&dRYz{<=ae1~+?SWm)ORi{Hv9 zAcJuHZM{)-%pKdSw(R~$epwhH@hN-PRc+a^y|S#fq#a-Oi0oZImL%y|Oc?S0kFIah zy1s=Z8RL==SYl|2$Xt`eMbZajRba-PV z6f?2J5$}>?h@i7N0ow=D;ZHaj44t-ke$m`zu6h1D3+9nYsKhykp~Ha1FyutT?OVc4 zojp!Zg79zx(@;65Pm8D0*vW*UdooN-phBGyQis0ESj<@R7_BUy`4lK?k-UfB+Hz*( zZ5C3wb1>uiB$y-WC?iP4OgJX#aLjZggI*+KQ5OOdNT)Yx#1n~0b0odv77D4Hd03tV z(@-Z*spP{)7)-Q`=(HS9I+3*Q&4U~0BSC=#(S5`#U)Nf5vEYxkI_QW>p6nFnh-x?{ zQD*!k%HCB>tLEQr7UHvm3?-G!{)N|X2yy-D@Iex+dk8kk3Ct1Ic!D8BYbZM`V$TPn zReO&zJ#*>rH`VxV^V<{Y(tenRVptn$s3S_7#x7*l?Xlr@o&V);^=!VobOlKS;L(1V zo=~hd)aR$kX!k6y`riC}Qm0nw(WmW6A`A_ToFFdt&i9ce{fW;W>fZe+6^z1+k3zWuvs9O;l$*B)7n4P2ydHLp@WD|jpE7RzK58=FARin~={iDw`x+|Q( zu^NX>P{ppwV(N~vMjP(X0Ww`FeLK#&^HY_0^;*hA5(&i9@v3m`J0bN?^P<&^q@cRq zhO1mgD4y+y*{f~9p4MM+>2l;);t6@Y&KYpoMlhwAx&{r~^Q4)MRI WqnyW)%L&*30000HCQK~Fp)o&O zSPe_j$7U$)9r6?TIB&^z0-LMTyp$<(5P(oN? zCFGGqjZIhzK31X+{ZKLS6`PRLVOCL}pA1QlZ)QNvQmVj13xgIvQF zbRn#OF6_c6lomz}%VG)Mh^XTTkVQ>|4+pT^#xQEI7z5>o)uS6PMbt^mMJB@JX*8mU z8So%SEoPyJMx+sSl*$dOm#@>b(`bPg@e^&rE)3&53h+c!8*X6`J23-kI1j5s2Of#2 zGnfYtQARD+VhBp>gO2+bbq7it!Zu`Ght;7S4@6WS@)4rsu{L4|@>oTbM3lk^wz~?G z%i^Ax>=FVQRmM`>0~vP}_r<@B;t*!LT#B`rkDFq$D;81mD`p}t#TJ~v2o8;jl9z(r7{)1id}m=IF2oZNcMQ#C#5JJ}(+&HiF%3mD zIX`(AMm@Ul%OhRxp))Y8k0A{9r8_Z^7o4FC1FWe5|)G|VM$mLmV{N4 YznaC*JptuW1^@s607*qoM6N<$g8G!ZsQ>@~ literal 0 HcmV?d00001 diff --git a/client/Android/Studio/freeRDPCore/src/main/res/drawable-hdpi/sym_keyboard_feedback_return.png b/client/Android/Studio/freeRDPCore/src/main/res/drawable-hdpi/sym_keyboard_feedback_return.png new file mode 100644 index 0000000000000000000000000000000000000000..19b33da5fee3612f3d929c66e680e032e22ee56a GIT binary patch literal 636 zcmV-?0)zdDP)ICUu~#i6)ZtQ4V$5L%*--{pk^FINx4d5^q5_^dB{$LAdok^hE^aWO8&#kiRK za4{~%#kd$3lkp4&QNT3N>UsG^Pv3R+`)H1G(YFprAH7!L>W00O~h zG_}Qe7{xvOghaG7#S~D(Ep+1^))G@d71yx?(Hdhgj4SvK(Hdh=!5p?A8O0b(W4m85 z1|iO36Ov(!hf|P##RNEwkC2RGLR^C+=%Do{F+(_pRY-<021A&_Ye<5xnA|O5$+_pK^@{Ov2#XK}<`Klj6fuU!5CuM99EG@;5O*L7 z{J;V#7>J80<0iTaVxHmv3cX@H)Nvn@V-L}U4;B~0STTC1(L z-~`sv#E3|CQp6aPF^hHWF*axq&S6t~j1Ah0OZcWe#s=-fRdlq+M8BMFU(iCIo zVLxu83&A=X8e?ow9e1&e^9VJ@*cCC18p<%ah|$l*xEL4XV)Sz{F2=>c#VFunbo&M6 W#SW1m#tfGL00000x$iEP)nkluRhDaf7-wa}7`h%_@q zXFdr?C`3Nx&!>C|pF#xT^c7A}Hg&??PS*LrhYROi&=1@n=bl6VJsb{)!{Kl^GAKYL zR8f_5Og7C?N~nV-xK1@eJ)DO#aO#lLfb*qNDVERYQ+S9*i$TflZhMQR#I04H+O53O&Hw38A8DFgj+1aev3+_Qm@Gn0)c>c!Y~=s!|hh9^>s+tC8*!eC~GpAd__j!_xm5=yfwH0 zg)z#Z3Yze~y3qw>fF7Wa`^Ei;T7uR@BJq-p!Rz$~aPCF83XhL*1+Kv>@p$|l*@bqy z{SEUMpeSXOf<{^J!!O7lh(sbg4u|8O%jLQQT%)`VoKEN6OeXVz453n~eD1Fyp$1CA zEY!ktu~_UGVFZ5rX@4CF=bqUT)umIOl zk9)VvLX8BQI80yA$KWztf?BAOAc*~viaHBr)Cpj@2xb6-&lUzS z_|RkkgRKDxV6d6P00!G{3}CQfI>m4}91e%W;rsv`Y}*2d1@7Pg0000$N=wl@)w5GiQ|o#U zgYclBM)TXlHJxU2^!X0Y3m<;)@S{J?Qm=nx)3-gfHSq*w{1@B8%Y#VMhR(% zMsQ-w)Ci*t5-xGY zxkY1`=y_N-r%R|f9q-V^l!hv5W-2Zs*;O$6D$0;kZF0CT-qKLPQ`4xw)O9D3;(VwZ z^~YufCFEL(KI_{|s(J3Qt}8STiFaZBLD={*Cu&}Fac+3nk9F2Wub32l9) zVzp;1%7bQ_!U|T=$;#lYtQ3oUS%q0JMM8UBq$D-d(k9y4vBO(@rY)xpEk94U*A$56vzmS|ARa&_GeWo5e8K7} z^(@4!;W?=g_Bgd%%yy)WZ5onhkW!G5CjPKtB+r#QD3+_hgd`}xFYNmM`~iJe+Ij7` zdRRS_>@yChyS6xtBZ*x|P7vZqP726D9FX3qylr{;CAdMMw?Bv!c>;eG2qdj({Ik;O z4fd6T<1GNrUT?e5dPpqkE$*%Ix;yXN-dMN0lAU^I{G_5O22o5(xQAyrPb&z>xc*(< zvNdSrF&x0YhS*cuZmlC`Qr@gY5@7`#9G3|IBmm|xj{)`oFS5!-Nsro{;80DqVp@D|uT!Vl67)~c>2v0Ju)h=n~@n0Xp=^bju3}*3&z5yp`=+`~+p$q^3 N002ovPDHLkV1gMoZIb{1 literal 0 HcmV?d00001 diff --git a/client/Android/Studio/freeRDPCore/src/main/res/drawable-ldpi/icon_launcher_freerdp.png b/client/Android/Studio/freeRDPCore/src/main/res/drawable-ldpi/icon_launcher_freerdp.png new file mode 100644 index 0000000000000000000000000000000000000000..49726f4f0f76307d4a458376818e0c62d8f70f55 GIT binary patch literal 1200 zcmV;h1W)^kP)6Hp|Vardy?ufuYB1a5xO1eC<%)?gb9tcqNR|C`xhG(Dhn}duJ5&`(=IZ<_v*M8{raHx1MNSJ=x()$zRq2^0dQH zkw3D{g(4{bwNUUao#%47&EBiC#R1mu=sscJ8t z57Qy8rKew|H+f8kKrVr8fn#_%J`=Kea`e^eUU$0f?}l8Fhl_# zM+u~j1bd|wS_tIizg1iVF)(HpN*z{6oDz&m63sRKVM z!`ojpK~-zwpmjlA*U8TvxBu=$Y~wj-8_$B_L*xQcOjv+rKIolZh&PL>hl8Y0y-5nO zhkg<>Apx(n!?~ME>ZHC}1>4QdsFUph|65>7S!1!9Wxi`u_B?u+dxcZlh@I`jn8N5ne*YSt-rtG!!#zS0lw=@G|hD+koaC+@KggL=#&A7t3!8FqF&r=)OhO^K% zbt53J46aEz@ZxYr@jG2}536BYfaS-tLD87yxv}jf&HnqxBk(G@F;a4A1|s-0E^D<| z;2(}q&HGrsR)wd0!x2_ojo7Ahwp+l#l|4WCW)aKy=gVM&z|syN!93{ugO1wmFsmFFqF1WO|J@V=hAO3!~vh-CS<)tD^v!|R!3u{{EEa_LV0S%IMZ<4`6Zz{tAZ zZ$`7B8~p=jB&FrUH6ce5Wr)Gjb5&gj%I^i2kgUOgAS!wYX{q5P_q0hX7Q$UZ8y@)vcj zDb?N~j3<8*QG%O_%F5H?!Z4lqX?eAHbMTf}*<=vsD_Xuo zw{=2HLzlo8mgPRSOrU_E{IXei!Z%#tj}#%KphDQ*+INV8sl3O}I_qyNe!`AQQx>QI O00009T zwYp?1^FMfz25xeIb$p?&)95A*ntf)r0Dq9e2-PObPPRiv5JD!|&a$gDp3UzF0iHsb zgXFPZ2bbG3NJTMIyi>dwSi}sL+dIr=3aMugPhcZQSjY!_X)>%*Dr2+La)WD*=by_z zx8`_mFgvX>wu}|lDwE15>}LayA&*OZ$h)kyFg-Cj8UD+~Qs-`ykh)EyV)0+j$=L0| z_^tPtuibe}N-Am0CCh4|Fy6N>AVD|m?i$@SGUjO54eGM5yZs4OWLk6iSSKc>kUG|z zugFhWeY!@%kq)OrRQXQri0XtRwUT#cZ9=wo*6G9~{7Ndd%r=nQRVVEuVNu-<={$S5 z^KfV9*>=6@(aL7Y?SfgRmQf0T4BKH35X<*0voi=ZZ7O}RtYp`@VRx6IL6_p1mR_?v~RmKI6LHl4H8rvWL(bKE~S0b zEuj=YQ7jaA6*0+t>h;74j_RV~h%l`%CF%X7X@#1Xv8*b@#HD$YaZMsoV($HTbXhz$ zq7pq~%s&jB9lQYMB`-0?)eK|R7~*xpqmElN%v8yudE~9T4i7VmG^G%p49`ue+Yh4z8;Qhv;+EjU$fHaXIb4Gw_Og#G1uV;};Du!c3EhbqpNK zz5kWufzeEKl?7IBqE5ht@rxGn2GiZQnb&Jy)kUtuJx7ffuRb4Sv})N`=k>nBYi=T` zOkjkQRN6H(`)Z{!QYETeC1p~Yl1(@yeXO$eQ?l#)o9Q|<;Nb9mX% zvZ2i3fRwvsPD!Lf8qOR`IIo@Ev@t1_>LxlYp+CNPe~lEo0k840}0G zEmbs|uyu{W*sjIx4LeT+`-A;EPqa65Egp=$MvK*CD*1+A*uxVz$#3kYhdxtpF9HNH z@`7pvSip9>&-_d$y}IZr90mTS7U8IAx3g^w3MSibw~rbjz#W7GJQJS*nf%EO2I-+w w?a}ClW-_DI|Avor(rDwL+NM#uW}Zv_1-EF20WBH+DgXcg07*qoM6N<$g4{|uAOHXW literal 0 HcmV?d00001 diff --git a/client/Android/Studio/freeRDPCore/src/main/res/drawable-ldpi/icon_menu_add.png b/client/Android/Studio/freeRDPCore/src/main/res/drawable-ldpi/icon_menu_add.png new file mode 100644 index 0000000000000000000000000000000000000000..2eaa36966335c477faa0a98670c70aca16d29dac GIT binary patch literal 1299 zcmV+u1?>8XP)fP>tvj3AJHX^*lF(PWYG2J%VNT`^atargCF~el)aaIwB&?*FGF!DNGifHQptvn(-Lb(?p>lbS zyztyBH%>6R(oTN(Qip6}VYq zE5!T`>*jzzY4ESs@bn$48eCfaP9 z?5;mQ=94iUi7Qc?9@UgxBTJ_(pupxNML#BT1dm$JMSJPVsBAV<(kj|)QtIb>@e*6O zN%ULkTuN6XQu@9a1*}!4m`p#mP-a`qiWe%S#Y)O1NoHHjWM5orUoXW)ts^eUzNDGV z>WVZ=rIbuA#BH=NfD_EMGx^Yt(&D)U#3ZF^Zis4C~T58vsGtxO?L#PygHW! zwvQRfHMpr_ohrw3uuhuvk8YNZ`tH2Li@TPzKa^%G=}}z;haBB`yBeA$VKtd`QrWqh z5*49{YY0&ahAOGEqGm)w`>Uu>3Cg(!2WN2ETtuTG31`9*5VyF*WiFScg#|2O zX`xd`E}acXSa*aZ8Z4#6c7|u@M>i2$n1*Q~30gtjUX}HkzP<`sKQoaWC}g$S+#`4M_Bq*o8LL z!+iR2mQKR9dQx+PQf1cYG1cPFdL+N$l_zJ6$#3xJs9JPhS3}Y^t5M(JZN6dvy=>$F z4)q32Z8iED8$>IW;u1&3k#XtW-xgZ+wJ#1qN2bJ*s@a6;6;x+Xf z>EjjFYFVwBgR9S30t9stge}8FA%eHD7Rso6xIAwQ!tEgpJYa z2|h#+yOvWt&Vy>BahY?}>8`Wff}?EbOVadkRJGg6Y!4^}+^^iWwC%V$O+A0Im&5!o zdijG3e86XXLpL4TBV5v%ee6U?ahoKGQqNAhv^!Zz31RL?{sKg(swrV0SXux8002ov JPDHLkV1l?*aS{Lk literal 0 HcmV?d00001 diff --git a/client/Android/Studio/freeRDPCore/src/main/res/drawable-ldpi/icon_menu_disconnect.png b/client/Android/Studio/freeRDPCore/src/main/res/drawable-ldpi/icon_menu_disconnect.png new file mode 100644 index 0000000000000000000000000000000000000000..99c1ad45cbbf024d1b530402d9d33a9256f1d93e GIT binary patch literal 1994 zcmV;*2Q~PKP)V(&Sg%wY&T|Ewq2M76xu>dp&%`k7AVJp zl(q=fC7q&3Q8PtFLA{WO3Zhz%RWEABMWYx0-Mi=g^0JtfQ!l z>ADq(IE3IQz$@~M$&X6rNh!hmz<2%q{kskvI1t;pb0;nWEgTM)vcH7X)bSFUB4t_%xdYA3%~V%cOXcO|eQ(q&i^6?UIcu^kmD7=`eVnA`JrHR6VziB3yX^IoI35&rAwuvRI+$#Om&ig53XFf zQry(sq?DAxH$C2VMgf0zWl2HlE;e>hyY)(2kN< zYBz4&$QDl}j#@gFQM&1SC=>PheEe2aU%!Qz*dH+J?%lhzTeoi25z#VW&X6DCJ5xo( zd|(aAx^E-!3yqQnO!J znaIV97n|+&A`LAKf~8dJc2iSRlXh)vQVBw8K9>)%5H%PwslaL_tJM}2mF5lWC28|_ z2L=Y5C=(Mw&QwreUtf?tbK$~;I<&FobUIYlxNB-?>(;H~AvH;qUwHiZalc+~plq{Q zDTM%aNTN@VBP2I1LF#t1I^vy}bkk?-nf|^$SE0?OIf{!_$JKxouGOwyy*d`DDN?%i z2+>%U(MT3c9>Eq}5(7zbgrwD1;i3r2q0$z)(j%=sR}8$Q52S21tHzmB3}WcOF-h^U zbl(cE&)36*%;p@*%g>KUOgtY)NETfOWnwU(DjBjQB-xTAi_BJ}&Yg2v3ko#eHtcpg zZz1C$mB`QHAO#_8Hx?==D2TA(fjPqwdKvh!3Py%EltKtbp9J0vAmKq$=RgWuh-O2_ zz+0*dq3(#Jdmp^_!aWpSj=o;~Zz%d=>_ z(=)Mhmy5i^k&>nf2LC){$;%^?DVy7$OZLr3B}-ug2LgczlYy~0m6fUGT*7Fi&^;Q| z;z>Y-DK_2*NzIW8tgdmp`)%+ZE97uG!$kBYkQ_%yjJtU6-o0Jy9~c`(^>f?*47@%R z@gtFf-&~l9v^MQEM?5qAB^zzxNWu6Hg3*C|;Wtz& zb#)zUW}54dbtaZNY2&xAbF`E(KTw z>77VnwQm)}2RtJMpjN1@O+)I2PoF-00@oZuY9Hd`qeqX1TUuH)7+K{JLXf{gc-W5# znMFX#IEMUqlgf32~kWypUI`}zEw)O$4stlG+oim>|b!h$BXCm9un2FM3 zgqu>c(26>#g5NZ=@y4<`c=@B_lEb1BdVGUY6n#+iK`C!~E;!6(y8o=#unW!D0iZU| zAXQ^q!TvqEP{TyyoyI#26S~m;J=+SZVUTL`z*aNcui#raKp4XTp+VV7t>yTfgMzsT z#dspTIq3MD)>5`o!^Rk!4o?K&{x1y;~X{DwN2jvdPTp^l*Ck@6+jICY4R( z6l!?5cgudZz;>Kd|HB9!2s%-Ts!H?IC?(ZX*|<7HaZ2#Si8sC^)hL;tR#jHA$u_56 zz<_FK@SP~dzGCwtht*gvmOVvh<}dty@yx~13%5rXPEjly%O%t}nHTpJm$I$llmW}w z#NCIa%_v*d^dLe*YBU$6)WSvQZ=X`$9sp@E_1VSl)IyY^xgj+|O%KXeZRUiLlh4t@ z%_h_f9gfI88p?#T5lTJ2QuHGxq++Q_oDg42m!&aWE=oO)P&hl3@zD`k#|aZBTbBO} zk4jrnzP4>HOrdOuhMrw0_CvR(!cq0fF6=_pClwCe8b2-+4?PR95vI1e^0iwzwd|+a ziE^9}2Gk!=D6ICYgOpHYl&(HB*-rYIo%uWS^)u~sXtH#*jD#{s{c2cMsQ!R~Q{|jq zqLv>zgpAgB5A|n)G%%kiwBfkjNs%XR(%;kHbMxk;<8~Vog#+_JwtA=~E_13AwafqM zvv`WGs{sn6d#QKICVs*A$HTJ$3T6~i_&Y$uv*RCMhfVZO^|BS9uB(6H4|Jm$OW0wT z4&&Gg=d_>vX&-r$Rt$(oxSt=L@lsEwC*!5)%>DdFe6_-x^s(h9$Mmrkhd61+5;B~U z8I~3Dw3obTg}RgNFo<>7kl(}?PsYPVP176lKjLdUx|0f99+IamE6kj9Vu=p;u@&1P zUv-l^?V^)Y5QQjNS2NQ=O2$Pl{_dcfnUZydfH*njV#`hPwGw>ENeh-R!v|UT;EcIQ z&bY}muPrp<`>sxMJ#kTZI(*N?TPHQzjnEdF=G|N9*B~fz!{SK#_vzS?ij!lRfJo z{bH%62Jtm1=ji(DxQ5a7NoQisL6mCriw-vI}jh~1x+b=NFds3T`nt>k$yvTBSI0UW?G{BT1Lp9mpd>{@K2cGa4* zW{sp>sJvYYR#GH-{Gy3l)0GY*2-Bn2w=MhufT)z zFaQ|L3ri!oQE;YWa4bA&zr6E4c)iCm_&= zJiLK%iL=r-ZH>m@v*`LX@Ab^Y#7VAI?Pf_Q+T*m~5$xYif zHVLqZG4v?`UlR~a2!viVGPe8eyTcTsShqHq(0=go{?0C*^W8aDt{MPmicWL3suU(Ill>+IhoGf&KYE-&N)l@+S(&k(RfLiCMmTu5JdZ^L^YNmRsqOx4;Xfr=lRyEay z!(rKKwUz_WD2j4A(XU@lLyGwP@bD;QsxZB}-`Xm1_=&#dyF*&KM34IW`>?PeLzZPs zIvkMPZg{<3IM~}pY;beza=Fmk+lPsX2~1B~?TlDdGT!djSbaZs$!`7DQM;O%Qb|(PbAxl1i3}mIN^mu=Ne_odT zNTpK3N-&7s-CfZq5()0y!s)%9p6B?uvm<(^a|*K_PZB^&zJW~CxW2wFhe9C)SAycW zqR|M#9J_fmiwv>6yey7sd~95V^85Yb$h>oN@OV7v?(Tud>n%v2N@@xO0&fQg2Vozz z^O=~!NxfcO#opeY=#$ALmX?-~EfFYN{GFEt3uK|Yk!U2zhY~Zc8Mvs}fm&j*SW=QC zPU?Xqxr=&aVuxb!p92+hhbrk-G#Y)!Kj||O$V68H!NB6czyNG^8zv_mMLjYD)lk#M z#>Uv@=4NPg)Lzsb(os2CshOT}hyE#$G&}pA?QJsW`*PKij%w-J@bHU>qClgh)0#9X zPLI9@$vVg`ue(XTU*;2=lD#=g$QIMGg-4&j#X8Pj_6C)S4tP+A2b~q y$V6tcsAQ(|WF$Rl)jvbUuTr2=pi-d!FVGL7SS8msU%a*e0000X9JP)WGT9*4iXGhG2m)OAK75olK0`mrv#aekA>S4klT5%7m z(2W&H!FrJ!?aqYOM3ANUdPbgOVz-N<=i)0Itr^U z=Y#x<7IJBg6l8hHsg=>j;ha;zXn24ckj`OmQ8a1{t zX^o0E5u|O^z>COA$-e9)zdDxjsa~=VJRyC> zJvJCcCstq_S?EHcK zIT~|o$*DN$w(4*Aw+8y$ldq#gD*|nR#R>2=_b6XbHNduGra0xyH5SR91 z=Y0QJ8(9(+Y6VqLIpt9yS^12mT9hgZ?f-rT;eY^+Vi6{x0IjG;mvkPV%?%x_rgF{9 z8!d`;)S4Ju&6-7XQZ#aX8lgu75J46ur3am`p$IXF@QdyvlkZYBSv512vCLG#MS{aS zQ%NilouU*~I13-{LO+h924!dwLFqWo&FWbdYYBetKjj~3{xn{{ciHdLuOTW$L^(J7 z4u^3M1~|P1ALFp7l~Vx_NYG=H#4BcEp{SRC#07NXB42qQcEgP;sKeOpjLGjw0D%z& z@)r4sAOvv(A#U|RWMVe#h~fmk9*z4s7c-JEsHVl9-?rGz^_0nxV|8vKe*7IWZ=_Mws7mUfH{HmsxX-FbcP>yjjzthETg>*o*noWbYw~27cR_v-#Jzn}=Yakqp0f`v z92@&%adE3tnGZ^iS=MmR%ZaWfra;WR%cANwxNo&xnvSRLn4Rc55LtDyxhEWsT!Ubk zp=7ydWsxz&Yxs+s(}gRnEBnHx-**#-x?8lY7Tq~_^5e&5vTxXT)8%Yq53qaXMDOQm z=GpFAI(v2H!Gdp{U3EdXIiLNpPE^0(5~8WaIkUp#=r;!~T)`XdBtQ92(N2Uurd(Z9 z1NmQ8vc-TmOKIf&W%Ey`pQ6VpuBARKrv5P;4|#tsRB3P!0C4<^7DEo{+0!fMT)w|fL>E%Cr4>eXv#@JQ5ENeJ(rk(G5bbZfH zd!tg)PDyi{zSqlb?kZV>niEmJegBcxW=Zqg^An}WQL)#N>hq`sQ<5hRKhwLnD7lu8 z%qPB`%Fag$v%hYj8YJMl`g`7j&{#S(k0VbvQ7kBGf%@q|+SX-oJ`hkz$Q5_=&P>-) zQ55NDFLw}2dinU3DaPSNLs3y`8$H=4gb)-(HNl;}{@L2<7HUdB=KV~BGrv@1cxK&M z-0?uLAYiY(b!*2rQqxH)(ZpJKGWV@-**k^cf+;%LJX6OJEfw8IfnW*xXC-)*B%~?d z`o-sZ-;;kf?NYR9VdPK4J3gDa#A|SB3V+?uxX1+q0RAl;WPvW7qV`x}EZO4Jyw~zO zc76AOy|(#x7guxZChj^tv#<2E0wRiu$2~Mp5Km+It_@M6=n3HlxmOa245y3kg^9BV zPyMD*U9+WoZ+=^cwn-!eeEf&aZ(g+LC9;?smx?|^o}85Jq%Rz84UiClXyWE*poV10 zYAjwbwX9{Y;%MSOA4X_!)AB@~IwhCZu&H&VcA+KWO+>}isi2ti0;-nr`LEbW)0K6a z7GGUH{#y`=wA5VgSjYClxI~FsPzlZub1z}xC0tHQUtSogx{B7Q5U+In2T<}v*7BW` z=?C3~c1hwAQ86o$NraZkvnm;Fq~7K(hUrBoVmDJ48*Pc_`6SF2c;%!bq5Ct z?Emitc^7kZ@+I!1?oZeUw3iC-K^*h)R@bFsGi>8AKK05?djKU*i$Euff&?+ViAC1N z^Squ@Bn-aV?!TXiCLSXqAt4PU#6aMq{EzWZa92%qW2$pd00000NkvXXu0mjfGXkF1 literal 0 HcmV?d00001 diff --git a/client/Android/Studio/freeRDPCore/src/main/res/drawable-ldpi/icon_menu_settings.png b/client/Android/Studio/freeRDPCore/src/main/res/drawable-ldpi/icon_menu_settings.png new file mode 100644 index 0000000000000000000000000000000000000000..61a2fdd3a0d69838fc2d38c12c4f2dfda3f01ca8 GIT binary patch literal 1088 zcmV-G1i$-62L6o#LBznSSWQ(!0+f~*n)HZ&Nh8u~*_00}fCwip8$VTPk#!hL%-< zhD8z)0w^WmHX4fxD2ozpi@`>bB0(r?O-0)2^!x5{Koo&86#nY-o3A zkMI{W*u-0GxW%_AXB1V`a|hCx&P=AODXtvX#M_u_2`4EfCUV_-y5Zs)y~$Kcq)&4e zG7*I_22m)Ch1No}>#=@WUx5@dpBPjy-SskaXDpClYq1nbv6RZ9sHGCTdMY->l7C@! zmr|Cgm=mRP1e;{MxNReDcjDK{Tko$ioJqYnu4!0Al!NE8qX93(3KA>?1&HNw`wPYt z&+^niAYuRhH&%Tz?YBiOaSE+9S8R)abxl;kpxp6=rxq-+JEuHY(c`FdC~rc|%J^56 za&RzLDt3_IT+tZxxBVB5#&Qolk&!i^u3wsGQ~fj9 zzzea1Y;3JbOg}bn{l4Cr-9p`e?R3odld%$<9s}K9<_^rwIodE+<%niHvu+<`q6#*h zT9aN&${$iSE;I1(r1U9+eFu%@i}boL;e)3q*S4nXu_}pqggM-Hkiy^~;(&BF$%U_c zl{YhQ@YJD!;XMN0=L>#r_A*Kzpo+tk(FXgN=6aENa|*70+CrHnxiUqj zI+G-O(r1H~UkXZzU1aCV_>K8VLB?u;&1~cPAlH~pilrQrov=bRp#Xz87GhlI!roE+ z9ywXjBpNa2t+Tts0q1ns+U3Bfl(758AP@kB@5Y{zA6*B`sm88z&j%xpwv65Asz2e$ zKKNa--f!~Q!x3(v5dc3o^E~hIK4Z0q@-;=0ZwF=Vw&0rz{$<#ji$-#I4wVA=48&mRa5*zqY<9Ej_OBoC-+|w;*k5vyfue>VtgLb;-|D z>jDENjLd7Ee|%w=m7CuSX{OE_?8h%DbZ(GO9pqv53C}Wr&yzfChx_L!s|MV$*g8Jcge@p@GLM#XQ)E* zwE*kb!#z;VE_N`Ckor&w8#zEF_sG(y%c50ADP>XrBm4;{x$dkaf z3Tg^!3TiM6)Q_eh57A5Xt9>6D2SId*RpPC(g zGMW5F0F!ru!B!~<0$lfR7r$^EC{ORh<*QZO(p6VEj<_wAuCMQeLM!8AV?P79sU?W* zXeN`H%;)o8wY0R*g$}eJvgM7{7qIWV_2v9oWot4VC0;o4uNG%nC3MDOf&~2r;DT6j z3-ZY{LA2%z1$;6zgn@wpY;0_xySp1h2 zk7IXt7mU%-QKZvp$?@@V?Ck8|S0an6-d52Xi5M;o@Zz8F8HV2%3Wd<#-j2@BPPDbP zX*(K?A`*$PEg24nwN3l{e|-&8U88Nj55ux7TwPvr^m3|}oPTY5uh#>^Fpx^6kjv#@ znnjdKC9=;@E>~a{i&$G*lPUB0d>k2>hKq{}Y;SKP;18fsEXY(*`uWR=Wf>(Z1-ic% zPoK_x-_g;*VSG(ICsyA(hx+>ZFg!dA#>vSE@fdgSeum@YW0{h#A3g+QetsU))6?kb z>5(ZRZFn^C2#1G<*xK5{?CdNQu(-I0?AaMe%wGWZi78^4XuDGO|3MZ18)DAm@f1}t zJMWFq!NCDXL#CrrDa&+a(ivLID4HgITwd0lw7I#7K!8$JDDWOdCk2br%+0a1w4}T4 z^z>8-f`A)!r%d68XU{M=I4FygYioahA79+NhtaVy?CtHLtE)>A0(i0U0$)CO;GRWT zUth-?sx?A@NqdG$m-tU-8H?ez=9}}JN>JS}j*gDxXy{RXFc?H{Z!g^7eU2m{PD~I@ zrcPU(DP!`?z{_M3zbhZa<>$}gD4#^BfhoY4o14=E(^32E^FvXwPsU=gZW;$aB@&50 zRZZQzQ_$7b6-?8F5Dt%e>ab5`m(>dvR11|#1Tm_I1ht z0h@i>=9Qymy{ql3oXd&NC%*eAp7Ns>L@8e?zcuwPh;O0HAVARNAq`CQ&3aThl0NW_0@*c TT`B`G00000NkvXXu0mjfSSbGB literal 0 HcmV?d00001 diff --git a/client/Android/Studio/freeRDPCore/src/main/res/drawable-ldpi/icon_menu_touch_pointer.png b/client/Android/Studio/freeRDPCore/src/main/res/drawable-ldpi/icon_menu_touch_pointer.png new file mode 100644 index 0000000000000000000000000000000000000000..066ce3f63988aa8aa3e068efbae1116051c683e3 GIT binary patch literal 1246 zcmV<41R?v0P)&3>O(d0!Bk&NOnfgiP-?@qJIl`AU>EkjJG%>| z1Q!rOL?jRb8&a$b1*qZ9i!{9OFaWW>H1l>mJNZWkjS(5Zm~@hpd4S3M_B-eQecwFt zzrG$`0CK2DseJ0NxaaLm3lb((sCdP)H19yCxMbz*92>Wj(A$(D_cjZy%g zQs<~Q4;)Saof;Sza5XhGp|7tGw?=QNOG``Xt*tHTz~MxGUZOs8yIfjbT^*FTg7)?{ z^!9#-k&zK~ad9!dxw&~Ta3a*tQ6F}8b}j@00fZxA)Yp6A^?DEvhw*i5D|&jqMLL~U z=jZ3sn;RSZ3nxPT-uRt6sc1Bc#->Ij5^+Qb=JWYb>vqHKb|FaEmYX-x-QA6$p&@l{ zZZ5sCA8;Z^uTWpcVlge*n8b}6O-LpaP!t6bR>|-8!Q=73ZnwiO%dB9%{y$xvT^Jl3 zRA*;r)9dT&S-4NAcg3kKpmI#v#$Ue>Q)2qd8VQz2BoHT5G#WuT6oTJJFy_qZaKdVp zAXy~T*484KNTQ>o1O5H|>hSPz`^vq0XGNuQ&Dl0Sp2}vZiMAPmVzDTq5giHvzaI_t z4Xl_BhMJlhR8>`>qM`!D#l=}Y_~G{LivW)8iEWWcL}P~j7KmA5rTolS1H2xuULl9w zp#znalxUf;F&V%a>TxQ^^l9tr>RO;hI|8cr*6Z1-vDTonvQnFwnF)%3AE)y7rJ$|xmlZ-nCJm;fqL0Al+m_e zFrcw>?-q7BSPOz+fQzSv>6Yc8krSG#Q22e1*y?RLZAaOhADLIqjPjCrW- zwsD4Ov`K$CU#4+SYQ2B|moNveD$6#G5zjUtR!5(A*DV(Po~>{ulQ9P+YOVut zvaD9qBId>jq=Q&263RJv3k!=-R#x^ifVZim`}2WdfMOp~vlbC<8;~Sf@zu3!C@Q+F zjgF35M70Y{1N;*dH^yS_Mk=Si4x~Klrp2^odH?_b07*qo IM6N<$g8fKUIsgCw literal 0 HcmV?d00001 diff --git a/client/Android/Studio/freeRDPCore/src/main/res/drawable-ldpi/icon_star_off.png b/client/Android/Studio/freeRDPCore/src/main/res/drawable-ldpi/icon_star_off.png new file mode 100644 index 0000000000000000000000000000000000000000..b0dce0e66fa089cf89ea4ab6d7f2953c25d4eb70 GIT binary patch literal 551 zcmV+?0@(eDP)%JuqFm3y%bC2(Pp@@gd(O((OHG{gi|CYh2g&-sRi z#A}Yt4^EzGf-~1pTpcpvd`Uk$!~jPa6Gy2^)+wrtGoGz)f&)3+MTZr0lp1BK7!@Oo z<}zJkWTCDVTks17p1Pi?7gRa;hhb{eGd0cFnw$T#W$m$cGr}i)%DzP7SJ(A2^7izM5%yKaLlqYs<4UJ4RB1|EZe$L-7K;X z$r@%lNn1?S7#nx1Qy0@MuIhxOG>b_Sn~ydoF-D_xG>v`s6aq{r+YZ72_QDUy!TY@T z=6`p>|E_k^v-BjTykrK0;mc4cbT=y;PrZgSy@>nDN8RXC@A;&eCu}|gZN2v2P0nVDHCHS8HGCPi&+?XixIj!$}fd$kOUE!!_6$neCk0vv`zSXo)Y=;-K7 zXJ_XZO-)TF%F4_n!plBbFO=fp zH37IT;Lc?MuHUqXMx)@tBM=C{Zb#sj z8B^+`5PJ?lJiQk)X9aAGGW=)a28js)&OQNGPswn#y9i?MVb~_lU}qUBO?aIl? zY0&HScf#HqzzBy7B}0sKEyD0B0w zPtpk3JXZv#(}~8$#_!ql!oLeoRz^m~k-53K?bX31Jp77*-;s)xbe_Hy5cyDm-|aw4 zOUtyR@JCfqQE`%w9U>ne06t=1JH5XLIJF;mPvA9f@Q#4M^>K7}ci&=5rlbM&_4PWp z+l}b!41Gh+J3fJ+dl7s`5I1RoclYDLQEoA!1kY=gEiX&@xV5*pI~NxhdBC3`Y`+1s z*?hmbxp}U#vU0e;zkkKUL5Z>R_`@v1Xf!@xPmPoT&#P3b<=NR;Oixe8+uGW0=H}*} z3CgT-IPwJ zTU96&pGp~!Mz4~B_WaFfSx)=t2<4EZN|W%tmnFCR@9R%^)DX|gow*)+k{ z<g6Jz}FR&}FG|B<;zlDp^YD%HEC6rEdry*OP^WwXLemXjJDY>t<> hC;5F4TV0jNeCSP+m+QjsZ-EYB@O1TaS?83{1OWHlLZ$!! literal 0 HcmV?d00001 diff --git a/client/Android/Studio/freeRDPCore/src/main/res/drawable-ldpi/sym_keyboard_delete.png b/client/Android/Studio/freeRDPCore/src/main/res/drawable-ldpi/sym_keyboard_delete.png new file mode 100644 index 0000000000000000000000000000000000000000..07f95d5dc171d4620f1455dfb10658982bfbb8d9 GIT binary patch literal 670 zcmV;P0%84$P)aCJoW!p7HU;#r9f+24+&L-svtpi-;wEw5h{LELU$q zhLFHaV$^*9(O8ej-$Ri+&3hA3_fsVCu@J-?S{4=>Dk1$fj&uxq7O!bK^+f&6SR0sm zVM1@4+g)`E65oReeYLJ0GALeLZ!Yn_TSE3q1_+@rb$oTzRIkbKEg*|kny+X07m!*` zv}^Bj=7mXvZ5|Kt#`QUQ%Sz^j(bx(lPi@CWgyH|mPg@%%=++r!G5`Po07*qoM6N<$ Ef{ltbCIA2c literal 0 HcmV?d00001 diff --git a/client/Android/Studio/freeRDPCore/src/main/res/drawable-ldpi/sym_keyboard_feedback_delete.png b/client/Android/Studio/freeRDPCore/src/main/res/drawable-ldpi/sym_keyboard_feedback_delete.png new file mode 100644 index 0000000000000000000000000000000000000000..064a2c298c91c8bd929fdb3819ad341ae754d2ba GIT binary patch literal 445 zcmV;u0Yd(XP)+s$q}Vd!l@S_uRuAX77cAtLJFyNoj7vnB~aLs zX_F*Mu-jyA^)%fzJ?6>Qp7H%1{i<(Q*LC0i8R}A-)FrW{+RDn>YNm2cuxF^2bGj7|nX{lhYSnHTIa$)US$IyJ%LYCIwX*-NuT6cvb!Wm^*%aUC(%UsdB)&BTyX`HrRRJ5hVvC#$-q!#@UieuGT5!#*!p1ZDJkOzXk zNXt{f)wpz`sRAmR5&)^8s5LDJZm0s-;EN0qcb@E!`pie$KEFn^NPq6H*knNb$NF4FS zNBc$5X~DogyDSNMaYUNxshvFDqCV>U|*()DYsMe$VpTE)3U nLZs|~o~mM2T(O+^&-(Bizo)YvIz=?B00000NkvXXu0mjf9$eVV literal 0 HcmV?d00001 diff --git a/client/Android/Studio/freeRDPCore/src/main/res/drawable-ldpi/sym_keyboard_feedback_return.png b/client/Android/Studio/freeRDPCore/src/main/res/drawable-ldpi/sym_keyboard_feedback_return.png new file mode 100644 index 0000000000000000000000000000000000000000..7e7b3de27c40a15c6027de3e68911d8717edee27 GIT binary patch literal 267 zcmV+m0rdWfP)uLdo0002eNklM9LMp`FcaKi5wU1n)+Crsg25nOz~l+U6L!A zs1g}y4P@E!7H_2rkLl}%S7AZ49#4({`dT4wqWQN6^4&SQRsm~n zI|W~^)S}w+!hdSqI5$-Jqwgg~jm|cW^CPys=SK{El93-{By4Q_? z4M|M-XhCOF;_moL~?|5?A~55uy!;^A0ci4+b|* zGpI5MLZN6}^uNFV|Nr`h&2NkhVuu#|hl027&hNdt9Re@z`t%+o_TmnMG#0xOEkC~a zj|W`e!61aGjdf=oK22ZWWww4!Y(O8to2&@h-ri z5Qf-br`I5Dw@)&NGcds+LBN^q3(|P^I57Q@1i12jKR-V+oq>ncY{J1H35;YW clJtxM0D`2}ql9wEP5=M^07*qoM6N<$f?8IOp#T5? literal 0 HcmV?d00001 diff --git a/client/Android/Studio/freeRDPCore/src/main/res/drawable-mdpi/icon_button_add.png b/client/Android/Studio/freeRDPCore/src/main/res/drawable-mdpi/icon_button_add.png new file mode 100644 index 0000000000000000000000000000000000000000..33c42e0ebff72203c9d59b1727f5ff0421ee25ef GIT binary patch literal 717 zcmV;;0y6!HP)1OSKuvF- zg9a@gCk2PYVOJ~`lSZS#3HD)~Shu*;^bUDw@xaS*a3m7(%RI-Dq z>AlCr124zH$z*a@tJS2}>zNphMlzjFWwBUrT1@R=YI@Azz_oOE;N>V-Js4UhlQB=m zYk~ona=BdUo6SaFuh+T?a9ez8dd%R!MS~U(yzo34oKC0R`FvhltroBh;>?@?E*eXZ z7oJCh716EkoB42na=9$QV9;?eTr`#*FFcP17YYTfSS(7tUN`R%La)~=_xJbu#l?kw zb#()byCafr|z$9(dvD=;2c2U9;H~!!Uq(pF*J!OF=LW1OmFnrKZOW4qQuz z2VPE}YF-f!qDrN52#i>Zb}%))_qcfAQ^J&hqa6CU}}1YJhXV=x6eU?7LSv6wR$2o1%qGBE0N7+EwG2#NPzl}8C*1I@i;jc zC<9IXazkA%JMbgGfr|$1XFdeD&0Jy|I@E-_-nQ+74Of%E zPX@3$TzKaz!1;ApuR|Y>Q^0iy61s3)1tLi3!p$>OkPzV3Q$sW)06%iU&9^ENW>Xh@ z3yEE?6DZ5_S$OB7L{StkQ%>N3a&3Mz7u=SxawxZCiBrl2)-8H7$`Y#!9wtnfUprI62><{907*qoM6N<$g6fW(qyPW_ literal 0 HcmV?d00001 diff --git a/client/Android/Studio/freeRDPCore/src/main/res/drawable-mdpi/icon_edittext_search.png b/client/Android/Studio/freeRDPCore/src/main/res/drawable-mdpi/icon_edittext_search.png new file mode 100644 index 0000000000000000000000000000000000000000..60112c1096f2e99b16c30540cf7718fcf26e9b6a GIT binary patch literal 926 zcmV;P17ZA$P)p1;(FqzWv_k1r7&% zik=QRL{>tJN`~E&C9&PFU9);d)Ptk!^&IAH?}~{FzhwY$UM(+PCWcNJ?Xq^ zyDCrKy38a!tYyV_%GdvejA;6zWAz+InZA)O8-6XdBegxfYog$a^!Y-HU6iP7tJ;}^ zEC-zTdpN|w8@=xa;YJ5s@FI>8j49KzeY8{aTjhH>s5L1s^)fc+FPGtfMRW{y0Z$`| zq$_pbM-%I;IY^%Mtwt!1yI!)xypbKp5kX>i|D6z9Xb2keHlc0?DAVe+?J&c%kr}P< zAdK)=p_LZuFX{3(p@2YbG}=Df-h}L|aN{IAXZ=fVnr=20Z9=D({WMc6Yj!oDF)It& zL><#7c!IX^W3O&P!MnlLW>)_2X+bTFS((@NUbg-!(@jgvvU4*^Mi<*yL4|ZUPm6~2 zjN(z}>Eh-G9=aY`YKiO+E{gx@r-5Y)3Y7v?FrXfKsG)+U+%Vt4Zn|$f_vfJKww|HH zQYR%InJw)seW4Tz9LCXCs^j{3JH-wyk~uF2dC|Lj@W%u@Y2eYn%I+MQEKjPYHTU!n zT=dh;Iw}HNeC6Hf-*f)@AcJ(#EU018#IlF3Le$4W4sqo6MJee!J=d~!b(TyW+_!-w7+{7Qk!Mryr>$dwiL*29E$coBLgp5Idx$@e|i4jUYh;T;b1B-?2eYX_%3 z%B(@XmY|vp*25^OM{VXXBaS2VPU2dO9=5T8pRoA)hlK>HvZ`LMDw7(L*e7E9dZ(hR zU2GLh!Q$^fC_v?76Nd;!QM54eO=LwDtbUEnpyEjh$V`#9;@^grJL#Z}`h_PW5!Vav zjZU9h@v!T6=OL*6BW@HaI6E`2;5|hAfB*7-02$=Q{xW&#tN;K207*qoM6N<$f@06a A&j0`b literal 0 HcmV?d00001 diff --git a/client/Android/Studio/freeRDPCore/src/main/res/drawable-mdpi/icon_launcher_freerdp.png b/client/Android/Studio/freeRDPCore/src/main/res/drawable-mdpi/icon_launcher_freerdp.png new file mode 100644 index 0000000000000000000000000000000000000000..6b18c0ae3a0f951b1bd97f70fa28b1fffad31c38 GIT binary patch literal 1784 zcmVXqz&t}0-;-RfNBn(ZXT_L0$;xifyBA&T!r(h2IqZu225h)H=Etho|qXa>CMojO~N zz&;`xz9+Jw$|$Y~Ou014P{em7#q}9*+!_nJO$Xt+HxcTr5(JzpI7JgihG1YRL47zy z2~rgLIxc~ikc!}!CcNIX_->?-pgx#+;7B@Kxni)p8lgGmSdmtEgHh=BCJiV^b;BYM zC-vQ?ctSR0I^`mfvCnBaM>A; z{;o=C^xJ*t2AR17K3*NAokm5(Sf2(G8npXHMze%CEFotE26==)HpqtJ9U(KDf>GY# zP``}8b<#thC$7|2iMES43|rPc;`7X1@tCv!BtDHki$%vYaN|yFy=prqs5W7Mi$AQC z8xfRwMKn-AI_R@}xL8{}YaMbJvvw!IGd2}|$+-wjy#!VIWl|&&oOu;t+{B0|oV#(p zC|)FubkgS~RZ-0{SsZo6o<#T@&%%n-LP@ML?XvVYh5eIr1yaw?YJUEVoFQZMb>mR3 z9d$89E(Glg|`Gx$&%M*fD5%3vr3uk z-X!gG5ntNUR1*8wV!$@ykO@b=m@km#Is4ZF%k2`@OQtPuVA<*Xq5fy`e|l}=&k^>) z%u+#`;_wzXz-bdl>JU&s#9kUjkwzKdZ%e=7ouGkF()rs1=O|n5%)l2Y>Jz|RFV_0x z8baCgxe=o;6?a&YbOD_wxS{nZXBe3~ZfGg-9ZANkUQD4{3n#apP)O1e6&?_$YWYOnH7Vn;Vol)_-05mM~yLs&t7D;ThNW>ui|0t9jy@ zhAtM4c-KmS_QUMaYPdZ{E?p~4C7KYw-yjpxxtV`6MgPdFm;o(eAX1ljE;RTi=fGs* zT$qk^!RCXBxO(#z?%w+yd8OB}{EO{qKim#|(Lftm1#My-bPWNG&rVC5YhVbu zqiex>#-*X7xf5cOHPE&)8tKTof86ZlaPnP^iS9x0P_2XUI0gJp=QU;Xiy*C=D&B(l zlP3f<3C%8t?fU%~Ve5s8$5qg_tb$S)jhF%12dw)CPQ!asJ#ple1|}mN;I{7s)aOeY zn_dOdwW$W|NF@2|Uj~>at=H^=^*j}xS64&Z{`m75=Bd_0_Wl$eV5VZt_Gn!D;SPE- zDl>N-(`kUu;(}$yDuZ82A-owWWo8MKFQb)1hBVc{bh42=t8IYupJ!bGW1Rg^S@je( zn;4OVbJ;kRnGdn$TrOvJl4F68CT64B2BZ`mK;dqCk3$`wibzcXJohJI$%zZ%V!qAq z%xbEE_lXCotZhKWjN(rreVMP5Q?pT5Q-*IvZ+jmEQ)sQ7oGJ4v6ko-|#KsdnCn%7R zl7WjwS1?BI1NqLQP-k6z#Eci*oJHK5e5WgoiidLBjR!E=(FeDF{tdd?@sw=5W9iII z?v!)}){efm=9z!^4BGInNH)wCZFys1%#7BKsn^mcT-?;OhBr6IKr50)Afjt6Oymv2 zXhi@L({k~!q7u)ZKgZo)A7I1&c$iN1kdB+Y6Q$`|QRE!=O*)T}i&vw|BzLqOKN~VG z_F)TGLM0Y-8O7S~Mm9H<29abU)FdFKKwyXk73nfwAy~L974k}N2o;r8!hw?+g2^Z+ zp`Y_IAuRv8zzbc0QLFj?(zE(}slc6FF!N9gyf+eL7LJ0;4nbIX@f(5BiWl#V$V}OP ax%~}j$`eV1;f#X-0000%$ zb?dsW;#4u9Vd8~cC>oqdH0o3&OGFZ*W*US{5yh?Nynmm-3AV+;0OJ+EUwYE#<9(j? z-1HO~T;w9m@IU1b90$>%9#y4!p;)RPRUL|w{R0;WF^KQr!(4TX3pw24nyb9{9)ssU z9QYc1XBG6RBh&P^yUuVgL~LXdp6g)$V6x zc#oBov77lKUo?&qrI?gTnCrOe^VVYEPEF_ZSs@lFpuK4Ifd+P`drgWjaI?Ie&ri{I zGVC?+bby|N?M0ANoY?sJ3hLFu)k6Ws)4fccvY%V+ZO6AA-)i?~WP8~oD^?G^S_n1& z>?;QGEf#1hPrpwNCfG+uJH>4~KU(<3;x88OUAXn_lw_RBi4{J2`l%M^Z)L(6Ig0Bw zuS5v^ z?M{Z;Hn~?8tSsnshudP3`KKzKRx2ys|9jsIUxtcKR4#np|rsQ4lfW8vz7x@`65#}hF#GkM1 z-!i;q_?7*MKO_Iz3@r!B zvKDA$FbP0`qzD6MtHyEU@U_ZkeC9fJVFVSLs_UgzHl~~C$e>-vh5#HI0(Fu(Rh7iiL|H#*n8rs_%$4syy`^hznA=VV=c#v?5Jq=+H19 zZG-+LHH?4a2yTD_GJdF5#aq8~`+@Y@wRLNU($C+HS+qwK(1(u+QHrfPgswYt^PMBk zQ`wVwQ+ZR_e|P@DIg*>(yBfkT^jZ*0<{=r4>iY_)xo1Z>=Y!q!u!dgxL|)d&Y&V_n zXSda@1vY673RsUNkSb%l0=UdswqvSHnl@R@M$imdAzNex&1R#?_OX=N|H&<+4m_cN zbS!})Mf_>iFNX_Q$7Z9|Zf6&}*&9=jymhjjLA%+gWdXMjzPs`e$`uw1@t}fwVdeH0 zix@D1R#;m0v7ZARu;P+gpKKFm&bGME7cJ98-?7_LG7FR8jRv27?N?n2J9@&XOj`KyV%3txQ49A+2}-cV)Wdg9rvUv zTi5|(CkqOIZ)m_KEC}%h1=M{l`(nLZW~VfW%k1IM38pZaNlYF5eRs^6RVPE1pV?D& zcLQHs_?WKYAZ}CCau2u}GCN`y8<)|+;6dQTidRyB;Ap2f@rtCv3>hxwc2QfYJq;5H zxq^EY(0TW+%K^689qhcWE;Gcy$QiX+&(+^hfw4Ar8gtbSN$=yX|Msr|?o*eL5MsR! z?5?O_tJTi%tjw?>nr<=tPZK-!dxWOYT4Z(XV zY*Ju?4P*ihItV+q$QGG6 zhe2EwX#kD_LR9<|B;iv?i)ExD5Dv90)*%7JR&6yp*Lkn*W55nDAg5Y7NymamlvH-M zQIe9j_p$cF-n+hw#}nT{jslLQ`z9OMD9sz$z(Ql&iOrF$XjWuPbgPS4r>)b8$J|@Q z$r{@{y6rc+O-WN^HPAQl@@IkK#2fUgJJrl>4KMoGVAe}}KdV{HUCd_z^O!phJm7f1 z0pyP7iIXpt5=r9hQlCu`_2UB(`xe7eLaj1Z4H z9(53fkrHwC7%szO)<{aSu@%iOAX6t2GJ`FOh}>I0UB^0GM(ppgnpLbe+$@cT)kb}z z-la;SZgIqws#j98>H5pZ?gAcDSCNow7)GX|BY0R^n_kY7QOU9ha3nR73Y3jLDb8HA zR?>c|1A$w;plks*<$#U3n%0 z*rpK+G67pAK#SOi3|0#4r%KJBI3sI)67l9uHx zwhBeXR=Lb7XJxyMT82;l@zX$&A{NDcAo>*W*E_mj@Uen!(_?$2rJF96i(GWeOeD?S zl*5VsL)t&3fFEFyWXNdOfD0=Q_?s_ROG}qoVY)51?WV|fTNS3ubg_ml7YFw;8R(OA z7M4JjjOSI20$%#oz^|I8z4XMszsxSPB8D{8s<7Bh|01htRAaR6r&^7Jrk`q-{_T==fLKn_oGVaXRD8 zHSeWShmfvXG~jjI0Po-f3}cU~(xEUPaXjicrf?27j3FXkRy}IJs@LkZ{i;u$R1|jx hHxd4yu5*i9{0~NXV+H!3kqH0*002ovPDHLkV1lLAJmmlY literal 0 HcmV?d00001 diff --git a/client/Android/Studio/freeRDPCore/src/main/res/drawable-mdpi/icon_menu_disconnect.png b/client/Android/Studio/freeRDPCore/src/main/res/drawable-mdpi/icon_menu_disconnect.png new file mode 100644 index 0000000000000000000000000000000000000000..9a78d64ce2eab5ae10027f3d37db2373fc0989ec GIT binary patch literal 2534 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 P00000NkvXXu0mjfzMR~V literal 0 HcmV?d00001 diff --git a/client/Android/Studio/freeRDPCore/src/main/res/drawable-mdpi/icon_menu_help.png b/client/Android/Studio/freeRDPCore/src/main/res/drawable-mdpi/icon_menu_help.png new file mode 100644 index 0000000000000000000000000000000000000000..36041a5294e22bd26a47a5a314fd0c9ab5a270f9 GIT binary patch literal 1620 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 literal 0 HcmV?d00001 diff --git a/client/Android/Studio/freeRDPCore/src/main/res/drawable-mdpi/icon_menu_sys_keyboard.png b/client/Android/Studio/freeRDPCore/src/main/res/drawable-mdpi/icon_menu_sys_keyboard.png new file mode 100644 index 0000000000000000000000000000000000000000..a7d5585ae029f7faf23761993e9480c4f4a06ca6 GIT binary patch literal 1154 zcmV-|1bzF7P)7>3{ZVL+h3Oe{kng9`}%)P+b(LPBatO;d?lt#-L1V z>VG_s$De+5?b-x`6iFV|fQy`64Xs`lS@9N4=!@{5*iWAzc?ZyYauhF5lSzrlTi1tI zF@J7|Mx#C&XaKNYN(P)n*1tWKO2KBg(nkst5fQ)B=6^N?Cz zURLqIz<`QZS62~>#V|NHsMcj1i9|3oG^FAO4}Jz{r>m=*>tWov^W8tjgn*IFf~q3U zQl0}g;&8cKsI071w6(PqTp8D=I1~Ag}9mI$^WfV7IFy{zZ(EA7uRE9P9(dD~{En znRU4qp^+zbb#?avgh{HDmpL!#_fPq68yUmL5>hoT_Jp|ygsNXt4+uT#b-kwd=j}Cp zu15v;CK3tUy!qu<09HtdJR@ax$*&|LZJ{stnCj+M^zk+ zMi2;mFH1_5k<`r0Osm~;X% zpi5{Bz#{qcNDO%E>rMCaSS*TEGWDu@;OGr}JT``W&CSiJ3yzG8U|+Xy-NNYAtGQB= z&jc#>Ojui6!}RntCMPFxkOF}K?%oZY8Uvr{CjOlyNculDaWEJ(z5i@)Z?lAwn4O(9 zz1u7-%d?I?iTy66$&68k3&lLMHA^EQcrb5r;hA7;PJR| z;lhCFh3v5Ng@pxdY-|+k0rkDBtE*8}RgK-<#QpK{@lOE!Mz)G)phUS`<@z>Sx+==c z@e>}>DQrp;_XKMNlI`mpdeO7N1~C{^?}3Ze!bbB3&8WC83DqmCqTF6mIM*@uV~SZwl$DkJ!Z1qGrIe79R1uG^zc+{j zuUCR3$qTZnvyeG#2KLjI((WBN9tmRTgJ-S3jh-?CmI2FvWxz6E8L$jk1}p=A12XIu U7%NAKs{jB107*qoM6N<$f=@Ui_W%F@ literal 0 HcmV?d00001 diff --git a/client/Android/Studio/freeRDPCore/src/main/res/drawable-mdpi/icon_menu_touch_pointer.png b/client/Android/Studio/freeRDPCore/src/main/res/drawable-mdpi/icon_menu_touch_pointer.png new file mode 100644 index 0000000000000000000000000000000000000000..b76585af81ed9e5af7132ca45bc4b9fd0b4d10d4 GIT binary patch literal 1458 zcmV;j1x@;iP)1D+0fQhK6Qa?$5Lsf38uWsYaBC!*2njJ>;gwR*NcY*!Y~7g7)^ zX|Z&fX-Qjp1g@2Yu-Xlgm~19o#CkclQ*4ArgOe$zV}=H4eMw9 zte^kj0Z>M5rFK%!QroG`)W&}cfCT>{b$~id9i;ZF_jcGGM*Z>!hK7cIxpL*ogFAQb z%q}l4YXFW?hing{Y;!mkjXsD(A{ZVX#^mHA2xAaNw>^yV{K4tz>DkuSR>Wd4L}O7T zlSvE?4kDYKz~xJqjN7+w>q|>Z_JdIm-+%Y+U9F>|13f)GNOh&q-rf!ZQ6VJw{=mQh z#>dC;gRUDlZ{D;Ij66&|{AfCpF}l0Ekn?X9AMqghaGt!N&REi)8#A&bAPzVjd z1~fD@Aj+D_WHLBEGJ=a2FP03(+C~ctngrUH?pp<5CVJ4rtVlMJotYx%rBIGs+ooGvsqH6h9x@-a)Zv$H1vJV$LTYBy@{XqO`X+W;(b1}P9> zghC-^qy>!x3HSphNJT{j8XFt00(hT#Mlw*;Zq(b?Yp|FL0+7}lIvc`SW+NB~qPDhH zTJkLb$Eeq+tpZTgZp4nFb0+*F0Lg&U;Y4R=rv!hTdV|_VZ7j)9W51W7Rxp6(@yP#3 z@Z$S@7Qq+KMhxEIuU9ZYQ)44|!qgFJ7gb)W$*gQNtJAv$14#C`6k2AW1U8!2;_*1T zySoYki12kPnNkp{1|ElvxC<3(A*u~}s z`T2P*7K_R`~C0}#Dtib zC=rCZsa?K&DJKEu1IRWizp7W$WE+(g+Y&%_A+eE{yO56{u~>;906a*Bg>ks z*#aQHx_FxfEQUNO#Gk zGpqgmi2P-d-_kTqwoyxfAhRGg@_008nir8slvmW>A@-;>yKJo?z<;DmPEDizR?$!RPbC?e>7c%vc0th|=?{rmTi zyWMVC%zl*lkeBvy_4jq^;|muqOc6pXW(nZ;2TU8eTyDvR*p72z@)oW71%NjdMA=%} zXgl>Pb%1(Tfbu=7KJ%1%|HG-Nshp$U!Bsk73BW86!V9O<1vi(z5VI>cLEfiNsAp|n z%Pf?u0Cy;uEsqwQ?DuNVf0_DVYH})9Usq>dXsiKzW-Zh^9H_0Uhl81G4TmMeZ&TYg zl;&7Na0klO)z-;X@=pOI0|IceqC$q=Px%10fT*dd5xZCdI9)D$bEXn?_4UTRd-p!i zw~#F$ay-@5)$nNAqX1?%I#XFG_Az+A0`OLT5!(u)s;bJgivln^P*qhmoWBMCHGp@h zo%wClRuI{2HdjdqX`f=9>gpPKZ5g~RA;!kW$4;F(4Uxm)kl;^S1aDi2!y_Z-;5g&lruyZ+CZh57*b%1!0_(mGwRPUN|*ll}crtoSdBB+1U|2Jv|b} zb8~YJwOVb2FL5$Tv3!VlBa232C78dp; zX1lhru_1bUd&TndGLbRc>~{NhL`1~b(0;%iCHGmN1ODEOX%?^5YH!5E#C(^Ml48os z%e#+1>&VE+>eABEuFK^TxI$udbX06@Z4t(@4SgKQANb$d+S+Q(&(FV~k&$sXHa7N_ zPN%z}9Keg7jIUHwR1D3|&bl2A$HC_2rYyX$uplr~F*GzJXNQ4^>FH_VbUF!Rwy}>I zYMCQ*QG=QT)~v0q9fil+o{X=RmzP`C*4B7hv{WbVq!ieRmVh6Z~;K)_AzxhLZb;0^F;eSQ5eJUCXXRnBI7e4O#Ipq4p|S)-|` zX(>23_!jEE2{fLJ{lKH(sz#%^jip&+qS`GZ}-@f`DZw#%NC1;r&6@Hw|j*eYD-E=oXGwB z$n<1SCe6RW=-vJO{Q}F*nRta7YVm;l?&s%Mj{Nh!yww-*irvG$?RR!|imIwA(b(9? zt0C&@>Nq>Xm~HHn8fdxC2YS7}fxlFIF@7REJp3n|)IWJrIh*S0YJo>a7FVthW45tR zYN*8%``pYB-U`oiPEFIh){T66KimTxbTLj)h(x zs8p&I0BA`hlhWR`#I-W zvd~bOLN~kI@ESQj>&3{(NG-H)aF6sp0z!d5@E&6JVn|3xUR+#UVOm<6UZ>M_^z`&x zGn>tGHk-|{wzfty#xpckuMiEo;Wg*D28|Q9!q(f{dyRWDGBPUT>n#DE97#y=$7qAx^DTNMvYB$wX2Z6)9t5)OE5LL;Wg*D#+n&u zxrcjshSh3y@yy1?#;fpn*_-jv>gwtt2x-%4Bl}Q3b$<2)olR4y_=G~WrxfZttB|!$ zA(sKBMmN0X9M@RGTJGsUFVEmvJhQ&Oegqz`c{4uX@9&>qRaG^@LUP!My26P%6#NR8 zK{x0DeL|1qP!pZ>px5CxZE0z-sMYFxx#xe2J>Ug!3YTSaVPS#DQbg2(Azy-TfB{?q zy&lHBk~4?~TJGUqp3&OcIvo@gbPjXlfySHhZg4L+qS0v1H#9U%udlBYjcAGLFsK={ zgKx#CPq-qvcF{n~J?P~#oe2&OF2>Ac5GEd8W48#7!n~xpxp|g%CK~*Rs1gGkJkQ?q zETskwYth4JdIM*AQSR$Lpt|dOEW+e~fB=1ad%Jye-A?p5hE{+^F}fu1x>0J-u(q?a zb1^V5upBd92lsh-kMC-Jx~Zw@hHKtTiyvZe38)pSg<1hM*08p%t!)myXM8pFH2>6S zG_KvAFwm`B3@-v-$Zw(Gkz);OO(xTC*w^dOuLeHH9_(VRSKbZ{X$I(neAv~+%Qa&nV<3JMB-Mm~Eh z#;>HNrkcyj%I0HZWBH%uB6t@(4;}+y!lU3>Ie?Own3y5%!3Uv5b}XMm1KtMbK^hMi zlTdIE*d?gJUU`rXfmG>vXRA*&4}Sn0o&}-oEEnxx_B|OO!6ADdW#D!0&n=j z-tYanL~4)av5p5ysUj@@+19M;wGHG;5ELreQtM Op$wj`elF{r5}E*dEK(}~ literal 0 HcmV?d00001 diff --git a/client/Android/Studio/freeRDPCore/src/main/res/drawable-mdpi/sym_keyboard_delete.png b/client/Android/Studio/freeRDPCore/src/main/res/drawable-mdpi/sym_keyboard_delete.png new file mode 100644 index 0000000000000000000000000000000000000000..98a2bc2ca1f70d8c20bf2f79637f064c4d74c035 GIT binary patch literal 615 zcmV-t0+{`YP)!3f>AIE7=f4>h}nUd8;E&C@*>2l)W8 zG|7%RcI?<$pjapnTLG~y5NiXmDG+Tb)*^wYV5C;Nr(u^5176R!AXej7I z_1FM$>aAP1?n52q55zJgIp+BB<7YrVfhIx$Am#vKP9PRfO-&8_`Sa)3A3uJ4i;0P8 zg*qzX1)Iol@9K-@m@upZD;|;{h&}`2H^8nNm z{qyI~9|5Ta=?M%BoB@hj5Fg}7U=(FREo1{}f_m8f&Ye4V2|0)ZWNCVOdO32M0g8nH zu`1j#n0gd|H~``Z1OS;46&2M13T^tt zLRAc^rXHjR=AiEG?hT-*17&Nd*^EOqga$qhQ8SlGq@<(-!E#x2baWeKxs391$P5&E zAa#_MLxa7f9{9R!sMYrLZx0Q=7UM_-qhJ(_0syi>773%^b`t;q002ovPDHLkV1gU| B1Q!4R literal 0 HcmV?d00001 diff --git a/client/Android/Studio/freeRDPCore/src/main/res/drawable-mdpi/sym_keyboard_feedback_delete.png b/client/Android/Studio/freeRDPCore/src/main/res/drawable-mdpi/sym_keyboard_feedback_delete.png new file mode 100644 index 0000000000000000000000000000000000000000..617d4e96e870e5f6916df4143da57e6a23c62868 GIT binary patch literal 267 zcmeAS@N?(olHy`uVBq!ia0vp^DnP8u!3HFc@t+I;QkOhk978H@y}j1Rd00V&M|O^=gY20Ufuu`DX=@oK8ASY7DEDN3Iiq0Ngj+Rp(-~Q2G$;Pz z&ybL9nQ>!j!4q*t_FvcBplm2)H`g<6WI&ucl4j)cZpj(C1}+iy|WtygKw4T$!Rzp zZau)n{D|=Y%TMmi%ZGk0SkKRxwA0?Fk-@R~!G3k8h6JaE4T`PvCA6Ir^pb0D{rNO6 zLt#~}QJ&-0oyRlP6xkBCn*4L6%?S=!EzaqVKO|g^DHyQ@8Wsbs d^s3EgFPNdyQnPWvU!cPoJYD@<);T3K0RZfqR4V`g literal 0 HcmV?d00001 diff --git a/client/Android/Studio/freeRDPCore/src/main/res/drawable-mdpi/sym_keyboard_return.png b/client/Android/Studio/freeRDPCore/src/main/res/drawable-mdpi/sym_keyboard_return.png new file mode 100644 index 0000000000000000000000000000000000000000..27e26fcbb0a96e97a629ad5462d90866bb306d65 GIT binary patch literal 631 zcmV--0*L*IP)GT&(N;aFV^VuED!zpozV%4b&l8V_-6y-yoXi`$C)Hi+8;w}P`AfBM8 zNy+E)pTul(jx*3MVVw>9{=Z^Gn$6}|3xO zwL5MEI7x3rF@`1>48G*~S8*02V}&mki@i9GVmh79A2F%DupG5R-f%elR7_aIqCb!B zsA5XH#@WzwSWP4nuZ`$ZpS0WU-=R=Q5fgfT-1nFbKCG##x--e9<|OUuuZUza`AVF` z4P1jq+B@SPdvVgnJ@%`j8#l0o1vur-7+2JY!6-Oj8+|jFtf;U&Aj{?QJL%Oi(`)D= z?pLeThDHX1!2w-U#0|i95_e;V=u{A!SQner4eLo?*uA)j^RU5;88fFQ{{Y4B=Y$D< R8?OKW002ovPDHLkV1f=fD|P?? literal 0 HcmV?d00001 diff --git a/client/Android/Studio/freeRDPCore/src/main/res/drawable/button_background.xml b/client/Android/Studio/freeRDPCore/src/main/res/drawable/button_background.xml new file mode 100644 index 0000000..a6aeb24 --- /dev/null +++ b/client/Android/Studio/freeRDPCore/src/main/res/drawable/button_background.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/client/Android/Studio/freeRDPCore/src/main/res/drawable/icon_button_cancel.png b/client/Android/Studio/freeRDPCore/src/main/res/drawable/icon_button_cancel.png new file mode 100644 index 0000000000000000000000000000000000000000..99c1ad45cbbf024d1b530402d9d33a9256f1d93e GIT binary patch literal 1994 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 + + + + + + + + \ No newline at end of file diff --git a/client/Android/Studio/freeRDPCore/src/main/res/drawable/sym_keyboard_arrows.png b/client/Android/Studio/freeRDPCore/src/main/res/drawable/sym_keyboard_arrows.png new file mode 100644 index 0000000000000000000000000000000000000000..ec6959cbee1da328e8191aa6bcd5f0b5cdac379a GIT binary patch literal 605 zcmV-j0;2tiP)3IS~!Yf;duZ5$L)QUzn>=l9?Z;c@qRxum&+4_wIag&+4P@4b#@k4mMo8U`lqP@PBE7Dt)Gs6syOVnaL2sO*o2G5`fuAs9OCDQ0N=UZ?C-=L6|FpZ1oMlbe8Et5wX*8;xp!YE`J#hQRI zV|a@bQOf*-Fz3jfhb$Ky`8f!Gkq=wuBz{3g&5pT zh8_7OEBg~iT({{rug+nMqF`2|Z`z^>~GGM?U(#T*Z z`jA5jvXqcBnI2@2#vtV5CHjM2{jA@WYT^HoEK|!ac?s;nYsmQH$QB?=!I7^*#x#1u zP9Oc4f$%26@pF?be67AfZ9mJQQGOgZeL|R=M53woE r{ry1Q?R#MD>tic5gb{3S_P7260qB>OIo3P!00000NkvXXu0mjf7T69v literal 0 HcmV?d00001 diff --git a/client/Android/Studio/freeRDPCore/src/main/res/drawable/sym_keyboard_arrows_black.png b/client/Android/Studio/freeRDPCore/src/main/res/drawable/sym_keyboard_arrows_black.png new file mode 100644 index 0000000000000000000000000000000000000000..53dc89293c64817c58dc06bbbed2ed675e1d16ae GIT binary patch literal 567 zcmV-70?7S|P) zgE(~1LLne&V=W3=ZHt28&^I?Yh17Qiau0S4F zVQUG(Z{!iE3-|%K>abOWJc_VYhg@qo2j9+M&2SEEjY1xyu(b|t@ExaNERUa%K@z4) zkVgrol8`|SXYsEdjGzWttY8{5m_-?jki{a(n8ggHu>x6q!w}+#UB)UDvA4)0HVFZd zyBT+vw1S9px0klyB&twEDmaG!%Ao=o3?qd!4k8N&Wyqrp2U+wZjTD9h%%O;VxQ>U&B2-CiN23FH4uvp|1a<+f>p0#)O}K|0Xj_B$00l6K1lpd1Wyr@A z66jbb@X2SGZge`&?-F=|-H5CqEJJvVK1ALK#;_lo>MuT8qWp3yQq=$e002ovPDHLk FV1oCw=ac{d literal 0 HcmV?d00001 diff --git a/client/Android/Studio/freeRDPCore/src/main/res/drawable/sym_keyboard_down_arrow.png b/client/Android/Studio/freeRDPCore/src/main/res/drawable/sym_keyboard_down_arrow.png new file mode 100644 index 0000000000000000000000000000000000000000..331fea5dc4972813d2e3e5013d57cbe9f2a72b1a GIT binary patch literal 321 zcmV-H0lxl;P)98qltK!nP@%g+Pf*a&K?DVt3LLFcG2S+1{RNSE2At7Zg7bf zIh0ROi3?0{0leV~JJ|LK(m2FJpr8poD&Gq!R5-!2FlNR%8n5dFj(WxLJw_gN>Jc?# z@c3?omy*vUkBTspd_MV1@=HC*W(ps5gnN9vjXXvkRbn4!=updJWV%7;cD58sl7<0i8mFMZ0k2+)g9HU3|GDKZ47XFPeDQ!)9 zlz}zr>(keyPin8N>HAS_tZ~IgkI|z7G%!F7B|S!u@=<~ZIpz*N4kNBFG7R(p0000< KMNUMnLSTaL=Y0nN literal 0 HcmV?d00001 diff --git a/client/Android/Studio/freeRDPCore/src/main/res/drawable/sym_keyboard_left_arrow.png b/client/Android/Studio/freeRDPCore/src/main/res/drawable/sym_keyboard_left_arrow.png new file mode 100644 index 0000000000000000000000000000000000000000..0e67f890ed6964b20caac7b86b38af08d645fc58 GIT binary patch literal 317 zcmV-D0mA-?P)l&Y!(3W-s!m|z!0jPgyCc)$_FvwRDeu%!#AXL$*0 zSU|nXuV5{AP>=FCEZ`mm@)fK>ekQ^f*D=MH&k=+N*}!bzA#R{ue`AERfrfa+F{ICN%Xku$V^B6<7?2ppBr7tsAhh6?7LCHg3X7P;d#? zQ#g%`isIa9=D=6~7v6iz%X=V!gTOq85`N)lG5(1NQ&b?9<6r1N&)RU7;}JB55uDX{ z7kXg~XEEM~CYZp7_k?%Iz47mO{DvhK51M0yf5@%z9yCQ!(h~}bh@dG;{6h{Etg}3z zid9wz2m*vSW^q8=2O23KF@VF>3TUQu#0E78t|kc3zyW$;hClo~p#%?i6BH0)pZtb4 z#YgO*XG1uc6(GbGik9^Of&g{&QHH=YfsZ0QPqVsRH{k#pK$zes^kmu)^XVnS>`0I22P5sR4Vj0UMEpEyzX= zwjme0Q6S~Z&$-wp<;ZOAX8c{ZN<}}3UULP7>}_S zC&gnT5|D_=NJp)DzQf@VPN&n@S~4{byu(|m3C(zq4`{(Bc=QlnuUB+*bVRmoZEe;6 zq(_XA2nK_~sQ&(b(b?H4JRVPOtU>I!f@4w%ig8#fMu|Lj94AqRN}R!2oJTz)5DB1G zPfw5V`F!QEENH837M088x;`*4AoS|(?KP&W*6nsD4i?0XnI^}^U=~u4iiKE&#YjU2 zmSKT<9$&Oq4Wd>!92S8<;0wL8&Mb&m55nj5xC^`XqbPtb1o0gq*s)Iuvc9jcPyDKk z9wJPI9;DqQ$b4MFeLTifJjV+(;uYRV_pn(BGM8xx2`X9`{&Ox- zgYcN1rD3B5$uSid$zE zBtr|b9k1bo1K(hmTQ>p-p%Nh8Ru{s zWpaBC7jPBVa0YX<^XJMJpy2{Wu*8??3te}2x1Yd!)Dg+D_%~|`S|HTLj_GJrgBVjr z5U(1f9&3?|8JLbFOvPA?(7tFSMq>h|Nz;O~zrU4WMNC_=3^L^OMowfDs<6o> Z>jx}0Ykg)dhr<8>002ovPDHLkV1l{2u&4k4 literal 0 HcmV?d00001 diff --git a/client/Android/Studio/freeRDPCore/src/main/res/drawable/sym_keyboard_menu_black.png b/client/Android/Studio/freeRDPCore/src/main/res/drawable/sym_keyboard_menu_black.png new file mode 100644 index 0000000000000000000000000000000000000000..fe5c072d8a26d7537eac7ba2890784d364202d45 GIT binary patch literal 1039 zcmV+q1n~QbP)7i_J3Wn((5>QXtAg zgoz^IHi9gYY#wZAO~5G?!cB>G%Fa#A+%j@XEq*lU#&a$rlzK(si{c{3JM(S9Wss2(Wo@Q ziT4yI8qkOja1_lrhT~|#Cpcp^n{N-$>+kOuyWQ?u&xKZiu#}mZ`PKaVycioD9Tgr| zpVewjy3-IpEHD;}G^8T~S$GmwWFrSpV=vaq%F3#mot>3nEiNvK%jNpx1qwFgT96?J z@ExWNKZ=&{H*Vl2RxpjXwIK&4Cnx1@_4M>eadENRIAlJ^kUeO{5BLclT);3!@C!zj z@A0xWWEYQNMMBis*(tfXxu$rc!L3wsf^T3cJYj-v_>_gb9si|q1n3xDP#2##j z4SFWn@uu=R%JBxOQHwg%qY~S&7H{Wax7#g#zhCC&=45biP*zu0CD8KnvNSd}o(?u7 z2YRpJBCcZwS1_&q`mux+{EI16Vy)!l#+%hR z#Ed$8h7kmA)0Ee zRa#ow>~uP%s;bJDpPyeJ8ylOtS-)A02RVtK3LPmaDKBkN{Rc~zMYUu7sqg>*002ov JPDHLkV1o4>*Zcqg literal 0 HcmV?d00001 diff --git a/client/Android/Studio/freeRDPCore/src/main/res/drawable/sym_keyboard_right_arrow.png b/client/Android/Studio/freeRDPCore/src/main/res/drawable/sym_keyboard_right_arrow.png new file mode 100644 index 0000000000000000000000000000000000000000..d44ae11ab56c97526a36e6b6494ee6fe9fc0883a GIT binary patch literal 327 zcmV-N0l5B&P)HiEW-l})-_WBiK| zWQyg<;!cVk_;}sR&&~^Pi#&PqEHKkFl~~3Ks=|9TrbUNM_b83H#uElKs62!A@CDJ& z!S$qg@FC=v5f^Sz`r8p>bXy+}>}^nBeZeoPu!RHk5iTLd-`A%x_mF?I`1*SqDkv4d*t-2JPY_QOp@OP=;Rn8mG9$Lxktp z7<;i=jZot#xF(Hp{V%g8<8b}(kFhVuI|Q$GO&&oFuJHJCGTwLUC2V6K>&0BgNklIaw$y0a*7R9Pe7LRg#hg;MMa&-Q8 zIKTRI*Kg;)+=QE(+jsMjLK;5AmqHBzO5#yb#0*=EQIw8q;0hIe$I>s=hI(?tNHQvb z(xL><7ivL2p3g@aErbRp&mkt~D`N%qPbTMUS%~>`KK=Fr1)Ckrgb95H+u6RnQ3rI-N<@bGe3^s&I&z|Jg2|FOy#$fEQ9Oi_%*g&$wW^4hX7 z`sz4A;h>#KU`0%^dAyA5lBl4GJYq9H;pS#}cVCtywJ0XMNdN!<07*qoM6N<$f+g94 AlK=n! literal 0 HcmV?d00001 diff --git a/client/Android/Studio/freeRDPCore/src/main/res/drawable/sym_keyboard_up_arrow.png b/client/Android/Studio/freeRDPCore/src/main/res/drawable/sym_keyboard_up_arrow.png new file mode 100644 index 0000000000000000000000000000000000000000..ca62134a1d7869480eb4d7aac1fb19131cee94e0 GIT binary patch literal 331 zcmV-R0kr;!P);)^qTClRQOYaHv z7p4f)IK=SM%!9B0nKumMy)#8o_=xd=2+ETlBc_EP4AIeHs;QVZ{;|PxwiqFSY9gkG z1MVePu_z;^kE0P&j+)>TO6p|H7}G^ZB~T5<2&~U;O|V#>{K;6XPbfxUefs77!w&Zv zoH0NfewCmW#yHm$bi=IDebPYzzA(T78FK6`1J=lqVTmbHgz(?FM*$)&#;;{ef~X|0 z2&okLZ4s0s@+8IuxgZx*|F|F*>wSV^a81LFO;g d6pF=2%mXMCs^+=M03`qb002ovPDHLkV1j?1hgARo literal 0 HcmV?d00001 diff --git a/client/Android/Studio/freeRDPCore/src/main/res/drawable/sym_keyboard_up_arrow_black.png b/client/Android/Studio/freeRDPCore/src/main/res/drawable/sym_keyboard_up_arrow_black.png new file mode 100644 index 0000000000000000000000000000000000000000..96577f4875fac086612bc406199e96077355a3ca GIT binary patch literal 322 zcmV-I0lof-P)98aD1{Wt2MXOCdV+$E4k9SHR1m?z#nm%7h!+TC^#+2w;3PN;om|}9Jwbhj z2p<_V((nc(fB5YWc{_At;S?Hd!ayV26k6=!1cMLLxnfp0z&$?jfKznPIbx3RjCsww zbkujuFUM|IHIVj|8bbS6lhFY09k&KDmec|L=EJYr5@!iW_V z{=61vm=Xnj;&RFNRACz UuBSri_5c6?07*qoM6N<$f?OVatN;K2 literal 0 HcmV?d00001 diff --git a/client/Android/Studio/freeRDPCore/src/main/res/drawable/sym_keyboard_winkey.png b/client/Android/Studio/freeRDPCore/src/main/res/drawable/sym_keyboard_winkey.png new file mode 100644 index 0000000000000000000000000000000000000000..8abbd6c32465418619550df3986461d9ce616051 GIT binary patch literal 701 zcmV;u0z&BcujD1tB&R;3&?bD28n$ zA_rT|KGZIVn8a>mq7w=uXp4m9U=HSD1xnC>7wCgq7m1ZE4&1;Q>_cJX^=zz1Ij+Et zD&(SB7!k%Avydd5L?=EX03Y1Ag9wuf+euv zV&wTkBq0r1*nnajK^=}G#Vq6klz<)Ea0=g`Fo-5pq7(;Ei$=8IDV|`pS%^mvF@`d1 zgbPYw2${Nw0^u+pGLVlU;aV?Jbs^QlVIJh741a}dt-6T$a0&+fMk|UTJ=^|3hAzZ{ z*MB5Y7+-{r`>@kzD#`8{aCvGF42^d8?s;1-q jI2|`Nmx&K!z5DwI+$(UMsF{Aj00000NkvXXu0mjfq81-8 literal 0 HcmV?d00001 diff --git a/client/Android/Studio/freeRDPCore/src/main/res/drawable/sym_keyboard_winkey_black.png b/client/Android/Studio/freeRDPCore/src/main/res/drawable/sym_keyboard_winkey_black.png new file mode 100644 index 0000000000000000000000000000000000000000..778ad4fa664cd5dd83a7e8a1ca4557ca6e8aa671 GIT binary patch literal 668 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 literal 0 HcmV?d00001 diff --git a/client/Android/Studio/freeRDPCore/src/main/res/drawable/touch_pointer_default.png b/client/Android/Studio/freeRDPCore/src/main/res/drawable/touch_pointer_default.png new file mode 100644 index 0000000000000000000000000000000000000000..98f9f5a33eb5bf235c34f7830de1ac052f7d1044 GIT binary patch literal 9305 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 BthxXI literal 0 HcmV?d00001 diff --git a/client/Android/Studio/freeRDPCore/src/main/res/drawable/touch_pointer_extkeyboard.png b/client/Android/Studio/freeRDPCore/src/main/res/drawable/touch_pointer_extkeyboard.png new file mode 100644 index 0000000000000000000000000000000000000000..f1fff6d4c1184f02ff1d321e8d7447ec59e2af9a GIT binary patch literal 9555 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 literal 0 HcmV?d00001 diff --git a/client/Android/Studio/freeRDPCore/src/main/res/drawable/touch_pointer_keyboard.png b/client/Android/Studio/freeRDPCore/src/main/res/drawable/touch_pointer_keyboard.png new file mode 100644 index 0000000000000000000000000000000000000000..08880bb5f8f81b31999d7227acc0f3ba3b5a4de5 GIT binary patch literal 9596 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Ⓢ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 literal 0 HcmV?d00001 diff --git a/client/Android/Studio/freeRDPCore/src/main/res/drawable/touch_pointer_scroll.png b/client/Android/Studio/freeRDPCore/src/main/res/drawable/touch_pointer_scroll.png new file mode 100644 index 0000000000000000000000000000000000000000..c5275a4ce5f1c2b925b676d3ec01baf1bbae4e7e GIT binary patch literal 11773 zcmWlfc|6o#7stOd1~ax9V_#=5cG*%QF_vuE`h`?>LLp08vWzd;_cbZYD56r<$Uc^A z2_e}{mPAChvCrfA)vzD=f302O!Sx;co_fyU^dX#`R{B7|3Bb#XSPG# z;R^tq>om~OG!KNV=h26JzuT}J;2Pli>%%~N>d^J&*_?^SV7=~{%h{FIUji+-*%^~} zgqvZhEQJ)ocqN3*=V-i}BYoC&l$O*=9!xTgrO)d0D1^GKcDQw@Auc`<$%YG29~4)) z5JTOd9OrMTZv>xaEL^%dbboDaqS`d}J;&6!U*rD2`gxXjRz^PLsrvP)Jl-nW{>)`( ze>@O+cxiCyf^Ao@9ICOoZ14t6Ulz?NPbHLAl6xPtzI}Uj^4~&t-YdP72LH8jh2_pT zHg0D#;mt{VOJLN=H^CqpW(&ndH9T-^@RdNo^qxc)IxH&hv?v|9I%c$6DHHUF5o{5z#SVD;}} z>4kqPFZO4jL$2pxShZBTmgfPwJeY-K{=Yv$DT z6EJRhVFj2W{#ji-%yYUyn0k13GQ2;Xv3339s@W4+>2w?6ogh(GG@e`3lt}U7sTOC3 zKF5^`;lHzCueaTNQ`djv@ajD6F;@oAK!_22vpd+fhH-Eo}M(dxlnLn`?G!_LA4b3>WYhaac)qYYtsd4!wP z*)g9NB{nYSJv-N%6c2%13`HdJ(TtaL^Pb0CI8G7jvT|Vop*5u*865f z1%b1r8-Eai{>>~2Hk%^1lLx1(#CQ8occ9t8aR>#!SKnhfavlqjf;gB#;O0*m>~&4d z%=}=bl9b%s^xMzu^2g9@+2DHkOHV=4d3p^JzR;%#If+c%GAN zi08HlH`9Xr?19tP{Qxa{f63MHE1nKDg-uc7F!s>tFI>|1m3gRMNURfQW@Ulrl*0X&b{=*xb4+=xPiv8dvo#oH%bYH-0eiZ>Gjc@+fPt z=CxAH+Gt5E&cP!La>s`&y(*E6vD$-xa8x=f{n_er8RWfhTlrlycC<6aqlM6YS28dx zkMrAvTx;`2e|Vx6$l|7Uy46T;qNlE|!Zwe`GSe2~A}zE*Ug)$J8e7xp)hvB7dc>My z+UGIp7xc|_rLC>IMkd62Xjm(jCLD{7tWu5qDUN4`G+icidd>%&Bws6Ddyyshujg4= zfciC!lDl@dy0!xeXQy=D4fRb$$sEfME3Hh0+g}XLzm;WOKGUv^d*m{~_4eN}gi#qI z#6|QpV!Y)Gsa9e4hmN91m$yv;nbp%E&tJtt)XV_zw(XHw-&J;)=(l(h3(W->{4%5| z__XHHzgbbA0=J2q4;K8s)DkTFCd12kdEH@C_u*zUaPa0NY`^5gHAQvfbE8$|T6-&n z?TnOUs{+3)6fqj$g_Z&rHw-kgGJ_W#9sT126wv^?d%5z3Jh>L6DoP!7(H|~^u%S((L4DQT`AT_u z=3~X!N$8flhSDYF`ENeAI>Uk>Wx`rgqoQRh0BjSfKoEK8S(3u9vc*Y>oAG@w9)6qF zjR$TI=p9GHl>D)p>+}U*-}%{XM5vEg=E);A?F)GH9vnY3C#<`4_zDXOWQa;XS;+)( z89R{Fu%2QI{3p0KqgPAFf$#F}54!;WjoJ00;^KewJiamq$J>KHj!K-|ZEnCS+5tS| znF;mN4C$hRI=Y5CFDZud;$U7O13)65-DKLY;$Dm}Mcgw*{5+AOI(&iXKujLYHu0dX zU8d{8sDzTz(pXW0*EZ)nIfr!KtPKto8N8xe^H@&6NI*|6)kz^1#)7WsE5XG+-|oAf z0eq#CQ!gX`F#>vv1`w+rz$|7=7iD|eD_#Hu;qO$tBvm_gv-FSxx3d?Se$uEtdUpxZ zWB|C~q8>Q)v`Xk$yNY3@TsoG;AkUF^Z1tB@#(YjpTK41&3i?`<_Z_eRIwhOtk$f;^ zCLrfYD{S8ab~wm%BquaJXvCNYU$SGai6ee#?|j&Fx$1jOV(9^2$}^aof}>TvAZJRJ_ z$ms^5(PS;C^`x(3r}EFv1;bP@auW-kYLu7teQZF4;U{^Rq5vAsE*lf@x8H#}w38P$ zZhHlRcab}qXsNw|+0+)*M3Ie6Z-VUJcyoJdjMcf;841fNFF&S{qd@C|;3rpEL_f<3;2hg1h2>HucNDtfFD-ij#gL2!TmEteXn4x9^Ga)hrWXT z$E45CV`rutc1Hpz>r-`}1?r)b%mS*9_J4o-#;EY%VOT5SWO7q2e!z*Gol(Av9^OfF zcA>i#t|y1P1B`M_U)jYu<9cN3}~E}JZ$Q04;Ek2kel2lKBCloSwFd*;p@qVCnTs$fEQ(ze^^ zF_$))wv($vkNjO1x{_oiv7IlkCRi&b@+m*4M*URp`6UjGM?qMzuS5Lx*M`z=paVP_Y;E&zSXuq%R}XpC`t!DrPvundz~Ep&Rdw~PPrh-Vyyn!i<-MABpH|-+ z5nDbh=^~3Fkr{C11CCPJ&c$Zjh7Yga=WiL=V`buq`*3^LBj2Cy3kP6|^Tr&N2QQF+ zIl!N-HQ?%D`0}2H|LW+!f4v#vuH=6}N)ruwHbI#N3)094#wk7QWFYr!)kMfyuAOF9 zh@=Skj6O;8`8)Xat6wh-4|a6y{hhpQI|7d|5xyLa#9XvdAJkyw#cFA4hF|;8_2@UC z*ANUO@LJ(M)gs7QOJ^~WMh{Zgst0qI^MeO|x>{OV3h^`tHH~lRCiBR4CSUrF)H=Oy73jK(PqNl!F((5vM@Oc1#VGFb(x5E)YsKq*`wjvg!iNy({eXE=^c zJol`=)|0&GO$Oy&n-*Bc8;qw?CKrlIN>l)ZHzuYT7E3OMUH)(n>XuV->$F_VH2+b8_2Ns)To`gQZES8flwuR0 zuaa$0mXeg@C#Pm0AVgDswF*LC@hLAiS4Qx#T@-jQ^7`Wwzu!Z%E5pUrBhA6edZd7$ z-PM)bDS|V3A8ZPIR0IAl*0}!|TN$tHi2B(P6ISUtb#Zy9P&*YOjA&!9w!rG(<@G85 zx-pFo4l@mY=KRb1d9BQ`GGqJto3|Yo2wpHD2yC3djKab;<0!iK0uqqNmp5P`s{uF- zS=nSpB%rVM9})}5<<}4LTnXB_2Y5}@CNX-ni$#7GYA!F|;8U-bDt4gw^2)0glTf5PhabDB^shP- zlt|PWn}8XgY_NcK-+4b!H0O#U6MzB+b%MGReZsq+7eG{R-J z>mm`BY>Tv|5EU9j9YL*>@Z+nxb%sp_#Gm#FX-Emk{0AQBasTRHn`L^92qU{Tu11c{ zH?2$aHzu#wWpuLs=_Nz7+S{V93D1OObFZECPg8UX0QpI7+Y_4wN-F%@`#U#F>{?rv z)enDb*O@OqMEUBoU&l%zx1jp+uCwi4UK^G2b(+gdvS<5oG4XyZG5!EW{wR;a9uJ$k zma~xE`{|DroVq+je%`6~74>;4Q_zVjXWO{EcB)P|{SX>5jDC_6)xbA*39P;yv7mNPOGFxhVYF1%< z&Ikr?-Lcz9Iods$P$yGc&fd2Updjdl=6{Nz^Y3%O-3^Q%%jUlzFR~{o=8|>{-32+V zK-TbSt*x}IZ1@#(o=71JD6i=vY)6bPr69DB5c4_43<|NNzZ7}^PKcl`?1uyI!m3~u z#h8uOHVlRDh);^@+bv+c@v2czYj@JUT)|Sd_*TQ%LV8X~`sV`i-VPBCb~!S~jzdTE zq35&nA)o429y%8f3De;ASy@X3k8%2Y8ALPm@T&(V_yzd)mpUFQ-UF3%0@*#Vawea% zK5Dvj`I-k=hxXW_%@R)XcH@SlPRm+r?GEp49aoQ2(?ll)IzxjFdL-|16dJW@9t9}h&qV%}duquCy`UtkIcr23_A@j62DLuxv*>s+`-RjS~Yo^izs8WLjC9 z-w#oJJo4K3O2XPyzE@B)qzQ7U?>Z{sk;x{IboI`~`0l{ubjUCsrp>BmfY0*2w>+S1 zb5~eFr`SwdU-LhBT4T!d&<4WM@^DBY7y@t%MW())eXkDPIaonst>!5y`mN&_aU<>m zd_uMn?@GyH`>XhPhP!@$ldgesqWSduPnb8qFX#cKP?Iq5zv~`}=l$1~xpA&n|@2A zml}FN;4K6=uw46vnw`H;8W3(qBn_l}>77hCI?K4O?pbxk1ekv%S9s7qj<69{y z;_9{5=K7QAvO@t*pPCmJV|8dRq8P>n7De#cSvNHxYo7(b9DSUbimXCzP!}-NkM&PO z1K$K`*jFfp)bC7@k(DIM#^JSJW+F{?TTn<{5F5C? zS=?`+kpurfUak zqND%Ate#(YJGrQ^q7*aTf=8;16)%5@dj?9uV&CtqBsLtx)e8 zDgh9`mG_rg(^~&ff-A!^UJuv{0q<`(o^{m+)$J*nwmb{=mKH@+WmxyeRG%50n zVYECxfqT<{u2<3xnUdeK>Db&l&{H;-EKinY7gb(6bwn3}xp@7wEhp&W%~pP(LWMro z`I@YfJvC2x>0;ML`zd|zXorTD8)4dIr0IMyp?9kJ3 zuOKRpOP)Q7^3Cw&OFBrP9D9@*QHod(AS9fNxE!LxTWILRv{S+_FukXfK<3DcAxo$3 z-xm80$}xTFC4$t5@kwW)s9YowHf|k?AYKO3g4OaHc*~aqVui?*Iy&`Ls|hM5j_P|o z<8D{VZ5}xxXB0K(xbT3@==7hC$R3q^0!BBDn36-K?;8RZ5PiA{tWAadP@Uaq06oiE zfL`A|f20z7+%LAmzpqP57DwH$D9`-hvHmNIjuZV?ty!`p35Hmp0Fj?g_y`=!6O>k> zC!|lqX1CE|&D4>?7%2H1K-UQYw&&f)`q>ii-hvGc4{D#zj3e!{OaOh_dTdTKWQ%c~ zaU5o!4yS#^?+i&8lNUn&PeWI=54rAuP(Btd{cOrbKW&Sb*$XtG!#Qk8Xioi_KAb6boz83aX92A z4iLM)&k~2V(QId>if=Il*lCW)W<&w!&rgqeFW*(XfP(=cWefjNj>!$CU&)cFtHyxA zzRP_$amj$Y!ko-y3GEr`rtWl88#@IT=ld$eueEj>y$2$J^Ct2st|*YvlZ})}oquCt z2z;ecgUfeTqxDTp9Gm!|-p12yoO?p2Z@v`G2bmfO|MsU9MgFc*jC$q3tA9%Zrr#_D zCuy2JhY8Q+(&EQVwe*|6IBgXOy48gJnLrT7I}Cv>`pMT?07(SK@7g@m2;DRR(pIJM zfH6#46nq+i0zZpg2jfI{^qSfIX7-*X!o|dqkD!L4Yavwh*>V1w>;r`R(Ug znsFbMp1>iv+8vc?g5HP;n>Ex7!41TNBoF_W;#G~ zv2q)_c#q0jNv{U?F1LEv`}W4fdIwhYqaw_{Cq3_q#v@31&VYG_6OZ2BnQYYz$VaVOE zSYD+azNeM9`W-hSKG0VT_r|*}zH;*Ya2uNWki)DI^n*&`si!A)PWutL;WKqDe`g&0 z>F8Hst5U>D7V;ffdT5lzH~4=CdiUTe`F|jb5{96|w}loL&Dd1kOHNLbtr^Sxcbbm> zxTgUhX_kPhF^voZ>|4YZ!`y$~h{}vxjpv!bN!aE9O`?n05-Z6K`j`Uj%hVogVEB$~ zwAwZ6-hLBxI26Hs)fWHSNQA+!v7qRn)|fJe{SM57^&WZncbI_)=@nFedAk0gNlq4+ zLvQ5(Z~!=CI_addB>KXW4E(cGJMjbp^gi#7q7HGT)>E1{Ot04RzhG-(h%IE;H$aiO z?(RZ5^HB$~MD`*ymUYyBbGEraHM}G|dQ!^%1AI8*Jrk6>a&C{X;Rl#U>BOgp*BATkR0I(~;llLlvu2J)9z2tLhhJUx@yE z*T7~qa0?&IzJ_ez#O_&DyC73C|DDT&kM-y91oyR-OVB@g1FCRDmJD-|Yvk3Q^*82{ z+1|xX$y`5bqY7;!FA6ZJ)SSjNsXFur@@`Cd^{+ilJE!~WduWJO?FZT(u(XkL2>8>$ z04+LZN7(;8Q;F(%CH6|K+|wr6A|aenr3HR6M$uCGbf!q|Aq}!MF&ZiwE(HM$=OQRH zqp2i4mHjCRR^JcgcRTLCFg-^(G^7PT{zP@4$bX5ta$PhnMQ3>|)X{Md`7Znm^aGvk z2@LKah%bv8AMp+O;@hW*cM4%Rz!6^3f#NCjW@n!n%|gyhKvgq8-t37P(}XJ*+Av~` zkDRhuc9aFEW&c91+ofyF##l!?r;7d9d3;UwsuJco*Fn$k=>}7iy=+hwS<#h8?>6N5 z@e0!W`&qFp>BbV74!_9d?lt0}e+1(q>N4Q>MX_DsUtj3cS7-6K zq$CYGNRD$+)2J4biBB9Ho*|fm1&7SnAeB!e!|!;Vgqrr!8s5E-W{pRt?O`ULb&N^t zZ(79ShVt4Pq`4I(gk;VKAX1nb6h@(#@B1dj1tC`{v@dUW5&{nDW=dc6wDM6g;-`b@ z`>j37%($vK{jRnSB@X_lYNR#zFsfm&fW8LpF1|h&rHJc`a`L?y0B2)}eeX+sXE*sm zr#S%e!j*;yH>5LN@L}9nY>T-x=i`FPH<{?hCJ5f_2BmM1I>frIV*>-d)t3sUfFR#^ zRw@tN=8IL}r#)KhZ)*4s{FQc4ftOIH!K7w#0Xudbyj1E6t9T!8Mq1&+MF-|ABJ3*7 z`43mH9Y%`dT_mw!WLW<_?xlm6q6#r!Pzd@~HVWvg{ zFHcL37jl8u-~?Y!02Kj2WAPob8lup^nDEJ8r@)M$*jT@Lc#L3>casNqea~S%;Rj0a zH4cw(EAP`xb!|#cXp1Odd5wQJL8NjACT@q{+wDtNW-}sehL%L&jnvr;JbwHf8PHWz zsvzwVIKiINuL}#~!n38ybJzxTwZD^1B?DENo>w+Ulon#bIG&07$0$T-n>cZJNA_2a)GYBEolWM$&vYbTB zzx7-9Yn*a;yKVp`-9`5@suDQM(>d4XaQ*~*|FTw}Zr_i)(X*5bve&X@{jd;2{3Wb7 zQ5jsD4|kpe+k?L)*wy@2`SI#ws5NT_Tj$b7vr#B=b96uON;(o^u(uyJR3VHZZA0v2 z#^M44&06%s@%SM(KqK?DsyuHoPHbY*@u)KvKMGE_3I0tIC;@*F*bAf9T!K za*?g$@_X!s=p6(c0T$|YX~Q0zPfk+OrgOY#KWq%MV(*s!`zq;6uqHoVybLduH#_GV&%ec5SCHQB86?A+?JX0<-KXQ-?E-C1h1~qYB0}#PDF~96)vutQ2tZ5E;mDI{wv6MYxGk}E{t?y$FUIc zVVPZ%T`O~d6R&%_yJYDh|KIUSCrE*w`kj^#U7;@Bm?hi8lgtoaik#fhtbB6P9Pep$ zgvB1juDK;14%SN%ad=v1?oMXfRJ`Ty|AfM8JV&x%HReK)pZMn*kE;Yz<-T+roN*vh z+?TxV-$bI@p_SaX#FPN;hd|FKo<;@Ig`^{M6}S+t1T;x3sS){yTLje3M=aB>p_}qs z2baNTA+=v$DAzBn?<%dh*A3ZheiLy_5?R=t(rzc*{v;N{!r8z5PJ+FYjX{TY+6>F5 zmElZ0|5*&8{nXm(-yy$>ywpuPoh|1pL@2 z@7m}L-J*%c$F8GdEoD11E+FWxv}A2Qs=ZV9K=kZKOu~FqNH}uwyn(h-WV7!3@T<8z0Qb#?W?6)%O)nt(qQ84`qbYU>%@8{vA%aV^jXQzfMrdx;q%}oy0WU}N_An0AVRDc?; z@J}gWn-i5&kt<&G0lGs-M>q_$Z>}$+Mf*elITs|iJT`#_D}i9%+>P`m{pcJ#oP4(a z^Qa58dK|Cc8t5d3&sMO}M(igAeoxz4q(s8Rv!5fu$GAPkU3h&t_RrSont@EE=5ljjm@g_TaZ#dJfw)awZY2HC<5W!WGuEVCDTZdpLX;V$r+C2a$?WKzVWl$qyJHOrk$ys z!y(#iKH)%Ye~-(+GQ*2`gazYAkRV^HH^2Yn-T95qYis8&J~rICy4`I_BZqd=(!b{x zL-U>mmm@+rUzyPr^(*2XFSfNb93B45$W6YNxN62Jx#N@X4w~e4FW8WU1Q;TJXzS`r z8oyS`j6aTOiik{khCzjtsfKtFPqk#^%9pd*xo84O=l5)_>XJLMJ<;QZx>z=~;sVm8PlRjF`dXM720D9=0df-IJIVrRZrosSUm)feQQqm0V z9X0)yDdfXg_byV;uvh=PNBy9P+=9#Y0v8_#KXNChxv{7s8Q;0g()ce$knVo*oJ!*2ja6DI??K*8dI<0ZFumJ49nD4mf6OPF z*G(CDm3R*)`VI}qjmr7?du?Y-jV*A81{-;8a}R!-YPvY?Y5T_jFlTr!2`NkZzg@RdN9;hehQO6AC|64DyzsbMBnd5DOM!*M_px>&!OLcsmclt z;_iQ5E-IOiWNI-6)eS-NHn4lS+g1gNqDsq9pB7>dZIv>#Y%beB1x6~cq7GIFd+NeN zO-pr=rE(}*YTuadZ|qH|5(umpJVXqIwY~m=kiTyeo&tPfhI%vx!SRA-g3wZ*>k7<_ z!ye&)gYoh2I})3`Rr`rdq)hq{COI)L@*VAQF&2n$($_*mP*3;$xl-vEz0r+N{dHCP zIg*{>c*6)oaUhgHo8-|_rGhmH?bGugT3HP<`y+48KoWHM( z8bP^9+iXXt51%5MAnJllu_5;|6Qjh@a}xsm6ZdcHjvgmvOOs;1)A|+UXIQp&paW`w zc6k$*bbOrIJ(xTn{?Wb^!>MllrWnKxo|O#*-4P2gTu0gW1WhPnwA{$GCxbXJB6*@9 zBY{8u2Ms?k-}{$KmuCy}f8MI}8FRr4n47xCV~79aLDTX>y(Q2{W-Z7DZ4T3gQhunF zG0~R-jABz-y5knQA@l#0!pP?>wV{Co6B#T-W)m{-_4w|?SPxcHt;s+Cr??I}GZR(M z>ALEDeY{j^0J=Qk!X&$?s3&Y0=MG7)&WdBA?zLfr*E?A3nKqpJt~(r1S@_49O8h&oZJx~I`$w; zuu-LYsVGS$B{BFy3aE4wJ(`>T=3Q;B5@N{fSHl95k|L>#c#gEYKJK`~ zc#vW;ojT>a^>mHC%d6KuAt#;GM=xapH1bNAngWCL$M`&*ue4Gb0E0P))e;KdT##sI zLVK%KdN(1m%n{hDIa#4afd!C)jz2#(RLu<~gmTO|O3k4F`@6@WOVIiIVD=Qry&w6K zKQi1)f?oPOY<#zMMg%UlJ@!ya8D_O*I;_`Ac_CPNFL)$D7Vy2A^n?0=GO2CEpK&9V z(Oebs$Oin$!e;&gbr@{6d5-qe7@R1I`S-&4!LKikDep$Je+15y{ZLoeWhu0P2)#Ch z6AelI1}TjK~ONPcemH$wX)+VSyY((=K&c)5nr%5Qfh8}y=EK!p^7 zYz&{T2Octc_tHe{z{=F|pAq&sA7R!6#wh$=YrHuG7e0C_bF5SQJl}R%?p4)-x}n-T zga1Qovoi&cS$2o=0Ry59ZKN%o<;z}=X~arGm|iha2YVfd#Nq(;!lJiuk`u>CxBHiJ zx}^;o@|!>zfh%gy=uAgsZ?J426}ZL=eD>!Bt}L?;BSD>1gF79+px3S7U?E*q<1Z$i zp&{&zD|UTx*V32A>&h$e|aZmEJ0{*p*2t{(q?psY(~#ym)V+nZG7WxY1W zR`cW*`^%)nbfSdBwTkhStMB6mf1ZzM{;06HDhw>~;^1{>m;PA<%--b&9N$ZOUqeZP zvL3p!+yt+o@vn7MUTKO3 zG$%U%hKj^c&4|lKGX3H|M>;n?5E8bZalT%A6$C2X^*b58-L+;gGD!c+e&aD-=X=H( zA*lI6Ta^!sHce_(df+7~&}!lJm-_O-AC-HmG~|>MAyES8r5~0%Pbf}_eIta2BZOwu z^zl#N6$k_d_bczjxNdo*m(>mPirpL;*qXK<1tq*ZxLR;?GgYE28YuLKzhRJI?nP|+ zAHt4{rec==km-vMFm*&hSBTtZrF4*yH8VsDNt@{xNizzN6fBWKbnimJjja7|i-iLv zqvx9h6RO*~wKOEEy55fN$QvgLm<8EqmDN<;k6*dI6I9AE(7XNEvw|W!6gl`sids5u zSjme9WU?8gM)qCe& literal 0 HcmV?d00001 diff --git a/client/Android/Studio/freeRDPCore/src/main/res/layout/activity_about.xml b/client/Android/Studio/freeRDPCore/src/main/res/layout/activity_about.xml new file mode 100644 index 0000000..1e22849 --- /dev/null +++ b/client/Android/Studio/freeRDPCore/src/main/res/layout/activity_about.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/client/Android/Studio/freeRDPCore/src/main/res/layout/bookmark_list_item.xml b/client/Android/Studio/freeRDPCore/src/main/res/layout/bookmark_list_item.xml new file mode 100644 index 0000000..913b3ef --- /dev/null +++ b/client/Android/Studio/freeRDPCore/src/main/res/layout/bookmark_list_item.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + diff --git a/client/Android/Studio/freeRDPCore/src/main/res/layout/button_preference.xml b/client/Android/Studio/freeRDPCore/src/main/res/layout/button_preference.xml new file mode 100644 index 0000000..407f3ee --- /dev/null +++ b/client/Android/Studio/freeRDPCore/src/main/res/layout/button_preference.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/client/Mac/Clipboard.h b/client/Mac/Clipboard.h new file mode 100644 index 0000000..5883d3b --- /dev/null +++ b/client/Mac/Clipboard.h @@ -0,0 +1,31 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * + * Copyright 2014 Marc-Andre Moreau + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger + * + * Licensed 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. + */ + +#import "mfreerdp.h" +#import "mf_client.h" + +#import "freerdp/freerdp.h" +#import "freerdp/channels/channels.h" +#import "freerdp/client/cliprdr.h" + +int mac_cliprdr_send_client_format_list(CliprdrClientContext* cliprdr); + +void mac_cliprdr_init(mfContext* mfc, CliprdrClientContext* cliprdr); +void mac_cliprdr_uninit(mfContext* mfc, CliprdrClientContext* cliprdr); diff --git a/client/Mac/Clipboard.m b/client/Mac/Clipboard.m new file mode 100644 index 0000000..a57725f --- /dev/null +++ b/client/Mac/Clipboard.m @@ -0,0 +1,433 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * + * Copyright 2014 Marc-Andre Moreau + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger + * + * Licensed 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. + */ + +#import "Clipboard.h" + +int mac_cliprdr_send_client_format_list(CliprdrClientContext *cliprdr) +{ + UINT32 index; + UINT32 formatId; + UINT32 numFormats; + UINT32 *pFormatIds; + const char *formatName; + CLIPRDR_FORMAT *formats; + CLIPRDR_FORMAT_LIST formatList = { 0 }; + mfContext *mfc = (mfContext *)cliprdr->custom; + + ZeroMemory(&formatList, sizeof(CLIPRDR_FORMAT_LIST)); + + pFormatIds = NULL; + numFormats = ClipboardGetFormatIds(mfc->clipboard, &pFormatIds); + + formats = (CLIPRDR_FORMAT *)calloc(numFormats, sizeof(CLIPRDR_FORMAT)); + + if (!formats) + return -1; + + for (index = 0; index < numFormats; index++) + { + formatId = pFormatIds[index]; + formatName = ClipboardGetFormatName(mfc->clipboard, formatId); + + formats[index].formatId = formatId; + formats[index].formatName = NULL; + + if ((formatId > CF_MAX) && formatName) + formats[index].formatName = _strdup(formatName); + } + + formatList.msgFlags = CB_RESPONSE_OK; + formatList.numFormats = numFormats; + formatList.formats = formats; + formatList.msgType = CB_FORMAT_LIST; + + mfc->cliprdr->ClientFormatList(mfc->cliprdr, &formatList); + + for (index = 0; index < numFormats; index++) + { + free(formats[index].formatName); + } + + free(pFormatIds); + free(formats); + + return 1; +} + +static int mac_cliprdr_send_client_format_list_response(CliprdrClientContext *cliprdr, BOOL status) +{ + CLIPRDR_FORMAT_LIST_RESPONSE formatListResponse; + + formatListResponse.msgType = CB_FORMAT_LIST_RESPONSE; + formatListResponse.msgFlags = status ? CB_RESPONSE_OK : CB_RESPONSE_FAIL; + formatListResponse.dataLen = 0; + + cliprdr->ClientFormatListResponse(cliprdr, &formatListResponse); + + return 1; +} + +static int mac_cliprdr_send_client_format_data_request(CliprdrClientContext *cliprdr, + UINT32 formatId) +{ + CLIPRDR_FORMAT_DATA_REQUEST formatDataRequest; + mfContext *mfc = (mfContext *)cliprdr->custom; + + ZeroMemory(&formatDataRequest, sizeof(CLIPRDR_FORMAT_DATA_REQUEST)); + + formatDataRequest.msgType = CB_FORMAT_DATA_REQUEST; + formatDataRequest.msgFlags = 0; + + formatDataRequest.requestedFormatId = formatId; + mfc->requestedFormatId = formatId; + ResetEvent(mfc->clipboardRequestEvent); + + cliprdr->ClientFormatDataRequest(cliprdr, &formatDataRequest); + + return 1; +} + +static int mac_cliprdr_send_client_capabilities(CliprdrClientContext *cliprdr) +{ + CLIPRDR_CAPABILITIES capabilities; + CLIPRDR_GENERAL_CAPABILITY_SET generalCapabilitySet; + + capabilities.cCapabilitiesSets = 1; + capabilities.capabilitySets = (CLIPRDR_CAPABILITY_SET *)&(generalCapabilitySet); + + generalCapabilitySet.capabilitySetType = CB_CAPSTYPE_GENERAL; + generalCapabilitySet.capabilitySetLength = 12; + + generalCapabilitySet.version = CB_CAPS_VERSION_2; + generalCapabilitySet.generalFlags = CB_USE_LONG_FORMAT_NAMES; + + cliprdr->ClientCapabilities(cliprdr, &capabilities); + + return 1; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT mac_cliprdr_monitor_ready(CliprdrClientContext *cliprdr, + const CLIPRDR_MONITOR_READY *monitorReady) +{ + mfContext *mfc = (mfContext *)cliprdr->custom; + + mfc->clipboardSync = TRUE; + mac_cliprdr_send_client_capabilities(cliprdr); + mac_cliprdr_send_client_format_list(cliprdr); + + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT mac_cliprdr_server_capabilities(CliprdrClientContext *cliprdr, + const CLIPRDR_CAPABILITIES *capabilities) +{ + UINT32 index; + CLIPRDR_CAPABILITY_SET *capabilitySet; + mfContext *mfc = (mfContext *)cliprdr->custom; + + for (index = 0; index < capabilities->cCapabilitiesSets; index++) + { + capabilitySet = &(capabilities->capabilitySets[index]); + + if ((capabilitySet->capabilitySetType == CB_CAPSTYPE_GENERAL) && + (capabilitySet->capabilitySetLength >= CB_CAPSTYPE_GENERAL_LEN)) + { + CLIPRDR_GENERAL_CAPABILITY_SET *generalCapabilitySet = + (CLIPRDR_GENERAL_CAPABILITY_SET *)capabilitySet; + + mfc->clipboardCapabilities = generalCapabilitySet->generalFlags; + break; + } + } + + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT mac_cliprdr_server_format_list(CliprdrClientContext *cliprdr, + const CLIPRDR_FORMAT_LIST *formatList) +{ + UINT32 index; + CLIPRDR_FORMAT *format; + mfContext *mfc = (mfContext *)cliprdr->custom; + + if (mfc->serverFormats) + { + for (index = 0; index < mfc->numServerFormats; index++) + { + free(mfc->serverFormats[index].formatName); + } + + free(mfc->serverFormats); + mfc->serverFormats = NULL; + mfc->numServerFormats = 0; + } + + if (formatList->numFormats < 1) + return CHANNEL_RC_OK; + + mfc->numServerFormats = formatList->numFormats; + mfc->serverFormats = (CLIPRDR_FORMAT *)calloc(mfc->numServerFormats, sizeof(CLIPRDR_FORMAT)); + + if (!mfc->serverFormats) + return CHANNEL_RC_NO_MEMORY; + + for (index = 0; index < mfc->numServerFormats; index++) + { + mfc->serverFormats[index].formatId = formatList->formats[index].formatId; + mfc->serverFormats[index].formatName = NULL; + + if (formatList->formats[index].formatName) + mfc->serverFormats[index].formatName = _strdup(formatList->formats[index].formatName); + } + + mac_cliprdr_send_client_format_list_response(cliprdr, TRUE); + + for (index = 0; index < mfc->numServerFormats; index++) + { + format = &(mfc->serverFormats[index]); + + if (format->formatId == CF_UNICODETEXT) + { + mac_cliprdr_send_client_format_data_request(cliprdr, CF_UNICODETEXT); + break; + } + else if (format->formatId == CF_OEMTEXT) + { + mac_cliprdr_send_client_format_data_request(cliprdr, CF_OEMTEXT); + break; + } + else if (format->formatId == CF_TEXT) + { + mac_cliprdr_send_client_format_data_request(cliprdr, CF_TEXT); + break; + } + } + + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT +mac_cliprdr_server_format_list_response(CliprdrClientContext *cliprdr, + const CLIPRDR_FORMAT_LIST_RESPONSE *formatListResponse) +{ + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT +mac_cliprdr_server_lock_clipboard_data(CliprdrClientContext *cliprdr, + const CLIPRDR_LOCK_CLIPBOARD_DATA *lockClipboardData) +{ + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT +mac_cliprdr_server_unlock_clipboard_data(CliprdrClientContext *cliprdr, + const CLIPRDR_UNLOCK_CLIPBOARD_DATA *unlockClipboardData) +{ + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT +mac_cliprdr_server_format_data_request(CliprdrClientContext *cliprdr, + const CLIPRDR_FORMAT_DATA_REQUEST *formatDataRequest) +{ + BYTE *data; + UINT32 size; + UINT32 formatId; + CLIPRDR_FORMAT_DATA_RESPONSE response; + mfContext *mfc = (mfContext *)cliprdr->custom; + + ZeroMemory(&response, sizeof(CLIPRDR_FORMAT_DATA_RESPONSE)); + + formatId = formatDataRequest->requestedFormatId; + data = (BYTE *)ClipboardGetData(mfc->clipboard, formatId, &size); + + response.msgFlags = CB_RESPONSE_OK; + response.dataLen = size; + response.requestedFormatData = data; + + if (!data) + { + response.msgFlags = CB_RESPONSE_FAIL; + response.dataLen = 0; + response.requestedFormatData = NULL; + } + + cliprdr->ClientFormatDataResponse(cliprdr, &response); + + free(data); + + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT +mac_cliprdr_server_format_data_response(CliprdrClientContext *cliprdr, + const CLIPRDR_FORMAT_DATA_RESPONSE *formatDataResponse) +{ + BYTE *data; + UINT32 size; + UINT32 index; + UINT32 formatId; + CLIPRDR_FORMAT *format = NULL; + mfContext *mfc = (mfContext *)cliprdr->custom; + MRDPView *view = (MRDPView *)mfc->view; + + if (formatDataResponse->msgFlags & CB_RESPONSE_FAIL) + { + SetEvent(mfc->clipboardRequestEvent); + return ERROR_INTERNAL_ERROR; + } + + for (index = 0; index < mfc->numServerFormats; index++) + { + if (mfc->requestedFormatId == mfc->serverFormats[index].formatId) + format = &(mfc->serverFormats[index]); + } + + if (!format) + { + SetEvent(mfc->clipboardRequestEvent); + return ERROR_INTERNAL_ERROR; + } + + if (format->formatName) + formatId = ClipboardRegisterFormat(mfc->clipboard, format->formatName); + else + formatId = format->formatId; + + size = formatDataResponse->dataLen; + + ClipboardSetData(mfc->clipboard, formatId, formatDataResponse->requestedFormatData, size); + + SetEvent(mfc->clipboardRequestEvent); + + if ((formatId == CF_TEXT) || (formatId == CF_OEMTEXT) || (formatId == CF_UNICODETEXT)) + { + formatId = ClipboardRegisterFormat(mfc->clipboard, "UTF8_STRING"); + + data = (void *)ClipboardGetData(mfc->clipboard, formatId, &size); + + if (size > 1) + size--; /* we need the size without the null terminator */ + + NSString *str = [[NSString alloc] initWithBytes:(void *)data + length:size + encoding:NSUTF8StringEncoding]; + free(data); + + NSArray *types = [[NSArray alloc] initWithObjects:NSStringPboardType, nil]; + [view->pasteboard_wr declareTypes:types owner:view]; + [view->pasteboard_wr setString:str forType:NSStringPboardType]; + } + + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT +mac_cliprdr_server_file_contents_request(CliprdrClientContext *cliprdr, + const CLIPRDR_FILE_CONTENTS_REQUEST *fileContentsRequest) +{ + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT mac_cliprdr_server_file_contents_response( + CliprdrClientContext *cliprdr, const CLIPRDR_FILE_CONTENTS_RESPONSE *fileContentsResponse) +{ + return CHANNEL_RC_OK; +} + +void mac_cliprdr_init(mfContext *mfc, CliprdrClientContext *cliprdr) +{ + cliprdr->custom = (void *)mfc; + mfc->cliprdr = cliprdr; + + mfc->clipboard = ClipboardCreate(); + mfc->clipboardRequestEvent = CreateEvent(NULL, TRUE, FALSE, NULL); + + cliprdr->MonitorReady = mac_cliprdr_monitor_ready; + cliprdr->ServerCapabilities = mac_cliprdr_server_capabilities; + cliprdr->ServerFormatList = mac_cliprdr_server_format_list; + cliprdr->ServerFormatListResponse = mac_cliprdr_server_format_list_response; + cliprdr->ServerLockClipboardData = mac_cliprdr_server_lock_clipboard_data; + cliprdr->ServerUnlockClipboardData = mac_cliprdr_server_unlock_clipboard_data; + cliprdr->ServerFormatDataRequest = mac_cliprdr_server_format_data_request; + cliprdr->ServerFormatDataResponse = mac_cliprdr_server_format_data_response; + cliprdr->ServerFileContentsRequest = mac_cliprdr_server_file_contents_request; + cliprdr->ServerFileContentsResponse = mac_cliprdr_server_file_contents_response; +} + +void mac_cliprdr_uninit(mfContext *mfc, CliprdrClientContext *cliprdr) +{ + cliprdr->custom = NULL; + mfc->cliprdr = NULL; + + ClipboardDestroy(mfc->clipboard); + CloseHandle(mfc->clipboardRequestEvent); +} diff --git a/client/Mac/Credits.rtf b/client/Mac/Credits.rtf new file mode 100644 index 0000000..41b9f40 --- /dev/null +++ b/client/Mac/Credits.rtf @@ -0,0 +1,21 @@ +{\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf320 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\vieww9600\viewh8400\viewkind0 +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720 + +\f0\b\fs24 \cf0 Engineering: +\b0 \ + Jay sorg\ + Marc-Andre Moreau\ + Vic Lee\ + Otvaio Salvador \ + Laxmikant Rashinkar\ + and others\ +\ + +\b Human Interface Design: +\b0 \ + Laxmikant Rashinkar\ + Jay Sorg\ +} \ No newline at end of file diff --git a/client/Mac/Info.plist b/client/Mac/Info.plist new file mode 100644 index 0000000..fd111db --- /dev/null +++ b/client/Mac/Info.plist @@ -0,0 +1,30 @@ + + + + + NSCameraUsageDescription + This application requires camera access to redirect it to the remote host + NSMicrophoneUsageDescription + This application requires microphone access to redirect it to the remote host + CFBundleDevelopmentRegion + English + CFBundleIconFile + + CFBundleIdentifier + FreeRDP.Mac + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + NSPrincipalClass + + + diff --git a/client/Mac/Keyboard.h b/client/Mac/Keyboard.h new file mode 100644 index 0000000..27efcdf --- /dev/null +++ b/client/Mac/Keyboard.h @@ -0,0 +1,27 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * MacFreeRDP + * + * Copyright 2014 Marc-Andre Moreau + * + * Licensed 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. + */ + +enum APPLE_KEYBOARD_TYPE +{ + APPLE_KEYBOARD_TYPE_ANSI, + APPLE_KEYBOARD_TYPE_ISO, + APPLE_KEYBOARD_TYPE_JIS +}; + +enum APPLE_KEYBOARD_TYPE mac_detect_keyboard_type(void); diff --git a/client/Mac/Keyboard.m b/client/Mac/Keyboard.m new file mode 100644 index 0000000..9bb9cd4 --- /dev/null +++ b/client/Mac/Keyboard.m @@ -0,0 +1,241 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * MacFreeRDP + * + * Copyright 2014 Marc-Andre Moreau + * + * Licensed 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. + */ + +#import "Keyboard.h" + +#include + +#include +#include + +struct _APPLE_KEYBOARD_DESC +{ + uint32_t ProductId; + enum APPLE_KEYBOARD_TYPE Type; +}; +typedef struct _APPLE_KEYBOARD_DESC APPLE_KEYBOARD_DESC; + +/* VendorID: 0x05AC (Apple, Inc.) */ + +static const APPLE_KEYBOARD_DESC APPLE_KEYBOARDS[] = { + { 0x200, APPLE_KEYBOARD_TYPE_ANSI }, + { 0x201, APPLE_KEYBOARD_TYPE_ANSI }, /* USB Keyboard [Alps or Logitech, M2452] */ + { 0x202, APPLE_KEYBOARD_TYPE_ANSI }, /* Keyboard [ALPS] */ + { 0x203, APPLE_KEYBOARD_TYPE_ANSI }, + { 0x204, APPLE_KEYBOARD_TYPE_ANSI }, + { 0x205, APPLE_KEYBOARD_TYPE_ANSI }, /* Extended Keyboard [Mitsumi] */ + { 0x206, APPLE_KEYBOARD_TYPE_ANSI }, /* Extended Keyboard [Mitsumi] */ + { 0x207, APPLE_KEYBOARD_TYPE_ANSI }, + { 0x208, APPLE_KEYBOARD_TYPE_ANSI }, + { 0x209, APPLE_KEYBOARD_TYPE_ANSI }, + { 0x20A, APPLE_KEYBOARD_TYPE_ANSI }, + { 0x20B, APPLE_KEYBOARD_TYPE_ANSI }, /* Pro Keyboard [Mitsumi, A1048/US layout] */ + { 0x20C, APPLE_KEYBOARD_TYPE_ANSI }, /* Extended Keyboard [Mitsumi] */ + { 0x20D, APPLE_KEYBOARD_TYPE_ANSI }, /* Pro Keyboard [Mitsumi, A1048/JIS layout] */ + { 0x20E, APPLE_KEYBOARD_TYPE_ANSI }, /* Internal Keyboard/Trackpad (ANSI) */ + { 0x20F, APPLE_KEYBOARD_TYPE_ISO }, /* Internal Keyboard/Trackpad (ISO) */ + { 0x210, APPLE_KEYBOARD_TYPE_ANSI }, + { 0x211, APPLE_KEYBOARD_TYPE_ANSI }, + { 0x212, APPLE_KEYBOARD_TYPE_ANSI }, + { 0x213, APPLE_KEYBOARD_TYPE_ANSI }, + { 0x214, APPLE_KEYBOARD_TYPE_ANSI }, /* Internal Keyboard/Trackpad (ANSI) */ + { 0x215, APPLE_KEYBOARD_TYPE_ISO }, /* Internal Keyboard/Trackpad (ISO) */ + { 0x216, APPLE_KEYBOARD_TYPE_JIS }, /* Internal Keyboard/Trackpad (JIS) */ + { 0x217, APPLE_KEYBOARD_TYPE_ANSI }, /* Internal Keyboard/Trackpad (ANSI) */ + { 0x218, APPLE_KEYBOARD_TYPE_ISO }, /* Internal Keyboard/Trackpad (ISO) */ + { 0x219, APPLE_KEYBOARD_TYPE_JIS }, /* Internal Keyboard/Trackpad (JIS) */ + { 0x21A, APPLE_KEYBOARD_TYPE_ANSI }, /* Internal Keyboard/Trackpad (ANSI) */ + { 0x21B, APPLE_KEYBOARD_TYPE_ISO }, /* Internal Keyboard/Trackpad (ISO) */ + { 0x21C, APPLE_KEYBOARD_TYPE_JIS }, /* Internal Keyboard/Trackpad (JIS) */ + { 0x21D, APPLE_KEYBOARD_TYPE_ANSI }, /* Aluminum Mini Keyboard (ANSI) */ + { 0x21E, APPLE_KEYBOARD_TYPE_ISO }, /* Aluminum Mini Keyboard (ISO) */ + { 0x21F, APPLE_KEYBOARD_TYPE_JIS }, /* Aluminum Mini Keyboard (JIS) */ + { 0x220, APPLE_KEYBOARD_TYPE_ANSI }, /* Aluminum Keyboard (ANSI) */ + { 0x221, APPLE_KEYBOARD_TYPE_JIS }, /* Aluminum Keyboard (JIS) */ + { 0x222, APPLE_KEYBOARD_TYPE_JIS }, /* Aluminum Keyboard (JIS) */ + { 0x223, APPLE_KEYBOARD_TYPE_ANSI }, /* Internal Keyboard/Trackpad (ANSI) */ + { 0x224, APPLE_KEYBOARD_TYPE_ISO }, /* Internal Keyboard/Trackpad (ISO) */ + { 0x225, APPLE_KEYBOARD_TYPE_JIS }, /* Internal Keyboard/Trackpad (JIS) */ + { 0x226, APPLE_KEYBOARD_TYPE_ANSI }, + { 0x227, APPLE_KEYBOARD_TYPE_ANSI }, + { 0x228, APPLE_KEYBOARD_TYPE_ANSI }, + { 0x229, APPLE_KEYBOARD_TYPE_ANSI }, /* Internal Keyboard/Trackpad (MacBook Pro) (ANSI) */ + { 0x22A, APPLE_KEYBOARD_TYPE_ISO }, /* Internal Keyboard/Trackpad (MacBook Pro) (ISO) */ + { 0x22B, APPLE_KEYBOARD_TYPE_JIS }, /* Internal Keyboard/Trackpad (MacBook Pro) (JIS) */ + { 0x22C, APPLE_KEYBOARD_TYPE_ANSI }, + { 0x22D, APPLE_KEYBOARD_TYPE_ANSI }, + { 0x22E, APPLE_KEYBOARD_TYPE_ANSI }, + { 0x22F, APPLE_KEYBOARD_TYPE_ANSI }, + { 0x230, APPLE_KEYBOARD_TYPE_ANSI }, /* Internal Keyboard/Trackpad (MacBook Pro 4,1) (ANSI) */ + { 0x231, APPLE_KEYBOARD_TYPE_ISO }, /* Internal Keyboard/Trackpad (MacBook Pro 4,1) (ISO) */ + { 0x232, APPLE_KEYBOARD_TYPE_JIS }, /* Internal Keyboard/Trackpad (MacBook Pro 4,1) (JIS) */ + { 0x233, APPLE_KEYBOARD_TYPE_ANSI }, + { 0x234, APPLE_KEYBOARD_TYPE_ANSI }, + { 0x235, APPLE_KEYBOARD_TYPE_ANSI }, + { 0x236, APPLE_KEYBOARD_TYPE_ANSI }, /* Internal Keyboard/Trackpad (ANSI) */ + { 0x237, APPLE_KEYBOARD_TYPE_ISO }, /* Internal Keyboard/Trackpad (ISO) */ + { 0x238, APPLE_KEYBOARD_TYPE_JIS }, /* Internal Keyboard/Trackpad (JIS) */ + { 0x239, APPLE_KEYBOARD_TYPE_ANSI }, + { 0x23A, APPLE_KEYBOARD_TYPE_ANSI }, + { 0x23B, APPLE_KEYBOARD_TYPE_ANSI }, + { 0x23C, APPLE_KEYBOARD_TYPE_ANSI }, + { 0x23D, APPLE_KEYBOARD_TYPE_ANSI }, + { 0x23E, APPLE_KEYBOARD_TYPE_ANSI }, + { 0x23F, APPLE_KEYBOARD_TYPE_ANSI }, /* Internal Keyboard/Trackpad (ANSI) */ + { 0x240, APPLE_KEYBOARD_TYPE_ISO }, /* Internal Keyboard/Trackpad (ISO) */ + { 0x241, APPLE_KEYBOARD_TYPE_JIS }, /* Internal Keyboard/Trackpad (JIS) */ + { 0x242, APPLE_KEYBOARD_TYPE_ANSI }, /* Internal Keyboard/Trackpad (ANSI) */ + { 0x243, APPLE_KEYBOARD_TYPE_ISO }, /* Internal Keyboard/Trackpad (ISO) */ + { 0x244, APPLE_KEYBOARD_TYPE_JIS }, /* Internal Keyboard/Trackpad (JIS) */ + { 0x245, APPLE_KEYBOARD_TYPE_ANSI }, /* Internal Keyboard/Trackpad (ANSI) */ + { 0x246, APPLE_KEYBOARD_TYPE_ISO }, /* Internal Keyboard/Trackpad (ISO) */ + { 0x247, APPLE_KEYBOARD_TYPE_JIS }, /* Internal Keyboard/Trackpad (JIS) */ + { 0x248, APPLE_KEYBOARD_TYPE_ANSI }, + { 0x249, APPLE_KEYBOARD_TYPE_ANSI }, + { 0x24A, APPLE_KEYBOARD_TYPE_ISO }, /* Internal Keyboard/Trackpad (MacBook Air) (ISO) */ + { 0x24B, APPLE_KEYBOARD_TYPE_ANSI }, + { 0x24C, APPLE_KEYBOARD_TYPE_ANSI }, + { 0x24D, APPLE_KEYBOARD_TYPE_ISO }, /* Internal Keyboard/Trackpad (MacBook Air) (ISO) */ + { 0x24E, APPLE_KEYBOARD_TYPE_ANSI }, + { 0x24F, APPLE_KEYBOARD_TYPE_ANSI }, + { 0x250, APPLE_KEYBOARD_TYPE_ISO }, /* Aluminium Keyboard (ISO) */ + { 0x251, APPLE_KEYBOARD_TYPE_ANSI }, + { 0x252, APPLE_KEYBOARD_TYPE_ANSI }, /* Internal Keyboard/Trackpad (ANSI) */ + { 0x253, APPLE_KEYBOARD_TYPE_ISO }, /* Internal Keyboard/Trackpad (ISO) */ + { 0x254, APPLE_KEYBOARD_TYPE_JIS }, /* Internal Keyboard/Trackpad (JIS) */ + { 0x255, APPLE_KEYBOARD_TYPE_ANSI }, + { 0x256, APPLE_KEYBOARD_TYPE_ANSI }, + { 0x257, APPLE_KEYBOARD_TYPE_ANSI }, + { 0x258, APPLE_KEYBOARD_TYPE_ANSI }, + { 0x259, APPLE_KEYBOARD_TYPE_ANSI }, + { 0x25A, APPLE_KEYBOARD_TYPE_ANSI }, + { 0x25B, APPLE_KEYBOARD_TYPE_ANSI }, + { 0x25C, APPLE_KEYBOARD_TYPE_ANSI }, + { 0x25D, APPLE_KEYBOARD_TYPE_ANSI }, + { 0x25E, APPLE_KEYBOARD_TYPE_ANSI }, + { 0x25F, APPLE_KEYBOARD_TYPE_ANSI }, + { 0x260, APPLE_KEYBOARD_TYPE_ANSI }, + { 0x261, APPLE_KEYBOARD_TYPE_ANSI }, + { 0x262, APPLE_KEYBOARD_TYPE_ANSI }, + { 0x263, APPLE_KEYBOARD_TYPE_ANSI }, /* Apple Internal Keyboard / Trackpad (MacBook Retina) */ + { 0x264, APPLE_KEYBOARD_TYPE_ANSI }, + { 0x265, APPLE_KEYBOARD_TYPE_ANSI }, + { 0x266, APPLE_KEYBOARD_TYPE_ANSI }, + { 0x267, APPLE_KEYBOARD_TYPE_ANSI }, + { 0x268, APPLE_KEYBOARD_TYPE_ANSI }, + { 0x269, APPLE_KEYBOARD_TYPE_ANSI }, + { 0x26A, APPLE_KEYBOARD_TYPE_ANSI } +}; + +static enum APPLE_KEYBOARD_TYPE mac_identify_keyboard_type(uint32_t vendorID, uint32_t productID) +{ + enum APPLE_KEYBOARD_TYPE type = APPLE_KEYBOARD_TYPE_ANSI; + + if (vendorID != 0x05AC) /* Apple, Inc. */ + return type; + + if ((productID < 0x200) || (productID > 0x26A)) + return type; + + type = APPLE_KEYBOARDS[productID - 0x200].Type; + return type; +} + +enum APPLE_KEYBOARD_TYPE mac_detect_keyboard_type(void) +{ + CFSetRef deviceCFSetRef = NULL; + IOHIDDeviceRef inIOHIDDeviceRef = NULL; + IOHIDManagerRef tIOHIDManagerRef = NULL; + IOHIDDeviceRef *tIOHIDDeviceRefs = nil; + enum APPLE_KEYBOARD_TYPE type = APPLE_KEYBOARD_TYPE_ANSI; + tIOHIDManagerRef = IOHIDManagerCreate(kCFAllocatorDefault, kIOHIDOptionsTypeNone); + + if (!tIOHIDManagerRef) + return type; + + IOHIDManagerSetDeviceMatching(tIOHIDManagerRef, NULL); + IOReturn tIOReturn = IOHIDManagerOpen(tIOHIDManagerRef, kIOHIDOptionsTypeNone); + + if (noErr != tIOReturn) + return type; + + deviceCFSetRef = IOHIDManagerCopyDevices(tIOHIDManagerRef); + + if (!deviceCFSetRef) + return type; + + CFIndex deviceIndex, deviceCount = CFSetGetCount(deviceCFSetRef); + tIOHIDDeviceRefs = malloc(sizeof(IOHIDDeviceRef) * deviceCount); + + if (!tIOHIDDeviceRefs) + return type; + + CFSetGetValues(deviceCFSetRef, (const void **)tIOHIDDeviceRefs); + CFRelease(deviceCFSetRef); + deviceCFSetRef = NULL; + + for (deviceIndex = 0; deviceIndex < deviceCount; deviceIndex++) + { + CFTypeRef tCFTypeRef; + uint32_t vendorID = 0; + uint32_t productID = 0; + uint32_t countryCode = 0; + enum APPLE_KEYBOARD_TYPE ltype; + + if (!tIOHIDDeviceRefs[deviceIndex]) + continue; + + inIOHIDDeviceRef = tIOHIDDeviceRefs[deviceIndex]; + tCFTypeRef = IOHIDDeviceGetProperty(inIOHIDDeviceRef, CFSTR(kIOHIDVendorIDKey)); + + if (tCFTypeRef) + CFNumberGetValue((CFNumberRef)tCFTypeRef, kCFNumberSInt32Type, &vendorID); + + tCFTypeRef = IOHIDDeviceGetProperty(inIOHIDDeviceRef, CFSTR(kIOHIDProductIDKey)); + + if (tCFTypeRef) + CFNumberGetValue((CFNumberRef)tCFTypeRef, kCFNumberSInt32Type, &productID); + + tCFTypeRef = IOHIDDeviceGetProperty(inIOHIDDeviceRef, CFSTR(kIOHIDCountryCodeKey)); + + if (tCFTypeRef) + CFNumberGetValue((CFNumberRef)tCFTypeRef, kCFNumberSInt32Type, &countryCode); + + ltype = mac_identify_keyboard_type(vendorID, productID); + + if (ltype != APPLE_KEYBOARD_TYPE_ANSI) + { + type = ltype; + break; + } + } + + free(tIOHIDDeviceRefs); + + if (deviceCFSetRef) + { + CFRelease(deviceCFSetRef); + deviceCFSetRef = NULL; + } + + if (tIOHIDManagerRef) + CFRelease(tIOHIDManagerRef); + + return type; +} diff --git a/client/Mac/MRDPCursor.h b/client/Mac/MRDPCursor.h new file mode 100644 index 0000000..6b16d79 --- /dev/null +++ b/client/Mac/MRDPCursor.h @@ -0,0 +1,34 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * MacFreeRDP + * + * Copyright 2012 Thomas Goddard + * + * Licensed 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. + */ + +#import + +#include "freerdp/graphics.h" + +@interface MRDPCursor : NSObject +{ + @public + rdpPointer *pointer; + BYTE *cursor_data; + NSBitmapImageRep *bmiRep; + NSCursor *nsCursor; + NSImage *nsImage; +} + +@end diff --git a/client/Mac/MRDPCursor.m b/client/Mac/MRDPCursor.m new file mode 100644 index 0000000..6df6267 --- /dev/null +++ b/client/Mac/MRDPCursor.m @@ -0,0 +1,24 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * MacFreeRDP + * + * Copyright 2012 Thomas Goddard + * + * Licensed 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. + */ + +#import "MRDPCursor.h" + +@implementation MRDPCursor + +@end diff --git a/client/Mac/MRDPView.h b/client/Mac/MRDPView.h new file mode 100644 index 0000000..ea531c8 --- /dev/null +++ b/client/Mac/MRDPView.h @@ -0,0 +1,91 @@ +#ifndef FREERDP_CLIENT_MAC_MRDPVIEW_H +#define FREERDP_CLIENT_MAC_MRDPVIEW_H + +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * MacFreeRDP + * + * Copyright 2012 Thomas Goddard + * + * Licensed 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. + */ + +#import + +#import "mfreerdp.h" +#import "mf_client.h" +#import "Keyboard.h" + +#import + +@interface MRDPView : NSView +{ + mfContext *mfc; + NSBitmapImageRep *bmiRep; + NSMutableArray *cursors; + NSMutableArray *windows; + NSTimer *pasteboard_timer; + NSCursor *currentCursor; + NSRect prevWinPosition; + freerdp *instance; + rdpContext *context; + CGContextRef bitmap_context; + char *pixel_data; + int argc; + char **argv; + DWORD kbdModFlags; + BOOL initialized; + NSPoint savedDragLocation; + BOOL firstCreateWindow; + BOOL isMoveSizeInProgress; + BOOL skipResizeOnce; + BOOL saveInitialDragLoc; + BOOL skipMoveWindowOnce; + @public + NSPasteboard *pasteboard_rd; + NSPasteboard *pasteboard_wr; + int pasteboard_changecount; + int pasteboard_format; + int is_connected; +} + +- (int)rdpStart:(rdpContext *)rdp_context; +- (void)setCursor:(NSCursor *)cursor; +- (void)setScrollOffset:(int)xOffset y:(int)yOffset w:(int)width h:(int)height; + +- (void)onPasteboardTimerFired:(NSTimer *)timer; +- (void)pause; +- (void)resume; +- (void)releaseResources; + +@property(assign) int is_connected; + +@end + +BOOL mac_pre_connect(freerdp *instance); +BOOL mac_post_connect(freerdp *instance); +void mac_post_disconnect(freerdp *instance); +BOOL mac_authenticate(freerdp *instance, char **username, char **password, char **domain); +BOOL mac_gw_authenticate(freerdp *instance, char **username, char **password, char **domain); + +DWORD mac_verify_certificate_ex(freerdp *instance, const char *host, UINT16 port, + const char *common_name, const char *subject, const char *issuer, + const char *fingerprint, DWORD flags); +DWORD mac_verify_changed_certificate_ex(freerdp *instance, const char *host, UINT16 port, + const char *common_name, const char *subject, + const char *issuer, const char *fingerprint, + const char *old_subject, const char *old_issuer, + const char *old_fingerprint, DWORD flags); + +int mac_logon_error_info(freerdp *instance, UINT32 data, UINT32 type); +#endif /* FREERDP_CLIENT_MAC_MRDPVIEW_H */ diff --git a/client/Mac/MRDPView.m b/client/Mac/MRDPView.m new file mode 100644 index 0000000..bb3aaeb --- /dev/null +++ b/client/Mac/MRDPView.m @@ -0,0 +1,1423 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * MacFreeRDP + * + * Copyright 2012 Thomas Goddard + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include "mf_client.h" +#import "mfreerdp.h" +#import "MRDPView.h" +#import "MRDPCursor.h" +#import "Clipboard.h" +#import "PasswordDialog.h" +#import "CertificateDialog.h" + +#include +#include +#include +#include + +#include + +#import "freerdp/freerdp.h" +#import "freerdp/types.h" +#import "freerdp/channels/channels.h" +#import "freerdp/gdi/gdi.h" +#import "freerdp/gdi/dc.h" +#import "freerdp/gdi/region.h" +#import "freerdp/graphics.h" +#import "freerdp/client/file.h" +#import "freerdp/client/cmdline.h" +#import "freerdp/log.h" + +#import + +#define TAG CLIENT_TAG("mac") + +static BOOL mf_Pointer_New(rdpContext *context, rdpPointer *pointer); +static void mf_Pointer_Free(rdpContext *context, rdpPointer *pointer); +static BOOL mf_Pointer_Set(rdpContext *context, const rdpPointer *pointer); +static BOOL mf_Pointer_SetNull(rdpContext *context); +static BOOL mf_Pointer_SetDefault(rdpContext *context); +static BOOL mf_Pointer_SetPosition(rdpContext *context, UINT32 x, UINT32 y); + +static BOOL mac_begin_paint(rdpContext *context); +static BOOL mac_end_paint(rdpContext *context); +static BOOL mac_desktop_resize(rdpContext *context); + +static void input_activity_cb(freerdp *instance); + +static DWORD WINAPI mac_client_thread(void *param); + +@implementation MRDPView + +@synthesize is_connected; + +- (int)rdpStart:(rdpContext *)rdp_context +{ + rdpSettings *settings; + EmbedWindowEventArgs e; + [self initializeView]; + context = rdp_context; + mfc = (mfContext *)rdp_context; + instance = context->instance; + settings = context->settings; + EventArgsInit(&e, "mfreerdp"); + e.embed = TRUE; + e.handle = (void *)self; + PubSub_OnEmbedWindow(context->pubSub, context, &e); + NSScreen *screen = [[NSScreen screens] objectAtIndex:0]; + NSRect screenFrame = [screen frame]; + + if (instance->settings->Fullscreen) + { + instance->settings->DesktopWidth = screenFrame.size.width; + instance->settings->DesktopHeight = screenFrame.size.height; + [self enterFullScreenMode:[NSScreen mainScreen] withOptions:nil]; + } + else + { + [self exitFullScreenModeWithOptions:nil]; + } + + mfc->client_height = instance->settings->DesktopHeight; + mfc->client_width = instance->settings->DesktopWidth; + + if (!(mfc->thread = + CreateThread(NULL, 0, mac_client_thread, (void *)context, 0, &mfc->mainThreadId))) + { + WLog_ERR(TAG, "failed to create client thread"); + return -1; + } + + return 0; +} + +static DWORD WINAPI mac_client_input_thread(LPVOID param) +{ + int status; + wMessage message; + wMessageQueue *queue; + rdpContext *context = (rdpContext *)param; + status = 1; + queue = freerdp_get_message_queue(context->instance, FREERDP_INPUT_MESSAGE_QUEUE); + + while (MessageQueue_Wait(queue)) + { + while (MessageQueue_Peek(queue, &message, TRUE)) + { + status = freerdp_message_queue_process_message(context->instance, + FREERDP_INPUT_MESSAGE_QUEUE, &message); + + if (!status) + break; + } + + if (!status) + break; + } + + ExitThread(0); + return 0; +} + +DWORD WINAPI mac_client_thread(void *param) +{ + @autoreleasepool + { + int status; + DWORD rc; + HANDLE events[16]; + HANDLE inputEvent; + HANDLE inputThread = NULL; + DWORD nCount; + DWORD nCountTmp; + DWORD nCountBase; + rdpContext *context = (rdpContext *)param; + mfContext *mfc = (mfContext *)context; + freerdp *instance = context->instance; + MRDPView *view = mfc->view; + rdpSettings *settings = context->settings; + status = freerdp_connect(context->instance); + + if (!status) + { + [view setIs_connected:0]; + return 0; + } + + [view setIs_connected:1]; + nCount = 0; + events[nCount++] = mfc->stopEvent; + + if (settings->AsyncInput) + { + if (!(inputThread = CreateThread(NULL, 0, mac_client_input_thread, context, 0, NULL))) + { + WLog_ERR(TAG, "failed to create async input thread"); + goto disconnect; + } + } + else + { + if (!(inputEvent = freerdp_get_message_queue_event_handle(instance, + FREERDP_INPUT_MESSAGE_QUEUE))) + { + WLog_ERR(TAG, "failed to get input event handle"); + goto disconnect; + } + + events[nCount++] = inputEvent; + } + + nCountBase = nCount; + + while (!freerdp_shall_disconnect(instance)) + { + nCount = nCountBase; + { + if (!(nCountTmp = freerdp_get_event_handles(context, &events[nCount], 16 - nCount))) + { + WLog_ERR(TAG, "freerdp_get_event_handles failed"); + break; + } + + nCount += nCountTmp; + } + rc = WaitForMultipleObjects(nCount, events, FALSE, INFINITE); + + if (rc >= (WAIT_OBJECT_0 + nCount)) + { + WLog_ERR(TAG, "WaitForMultipleObjects failed (0x%08X)", rc); + break; + } + + if (rc == WAIT_OBJECT_0) + { + /* stop event triggered */ + break; + } + + if (!settings->AsyncInput) + { + if (WaitForSingleObject(inputEvent, 0) == WAIT_OBJECT_0) + { + input_activity_cb(instance); + } + } + + { + if (!freerdp_check_event_handles(context)) + { + WLog_ERR(TAG, "freerdp_check_event_handles failed"); + break; + } + } + } + + disconnect: + [view setIs_connected:0]; + freerdp_disconnect(instance); + + if (settings->AsyncInput && inputThread) + { + wMessageQueue *inputQueue = + freerdp_get_message_queue(instance, FREERDP_INPUT_MESSAGE_QUEUE); + + if (inputQueue) + { + MessageQueue_PostQuit(inputQueue, 0); + WaitForSingleObject(inputThread, INFINITE); + } + + CloseHandle(inputThread); + } + + ExitThread(0); + return 0; + } +} + +- (id)initWithFrame:(NSRect)frame +{ + self = [super initWithFrame:frame]; + + if (self) + { + // Initialization code here. + } + + return self; +} + +- (void)viewDidLoad +{ + [self initializeView]; +} + +- (void)initializeView +{ + if (!initialized) + { + cursors = [[NSMutableArray alloc] initWithCapacity:10]; + // setup a mouse tracking area + NSTrackingArea *trackingArea = [[NSTrackingArea alloc] + initWithRect:[self visibleRect] + options:NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved | + NSTrackingCursorUpdate | NSTrackingEnabledDuringMouseDrag | + NSTrackingActiveWhenFirstResponder + owner:self + userInfo:nil]; + [self addTrackingArea:trackingArea]; + // Set the default cursor + currentCursor = [NSCursor arrowCursor]; + initialized = YES; + } +} + +- (void)setCursor:(NSCursor *)cursor +{ + self->currentCursor = cursor; + dispatch_async(dispatch_get_main_queue(), ^{ + [[self window] invalidateCursorRectsForView:self]; + }); +} + +- (void)resetCursorRects +{ + [self addCursorRect:[self visibleRect] cursor:currentCursor]; +} + +- (BOOL)acceptsFirstResponder +{ + return YES; +} + +- (void)mouseMoved:(NSEvent *)event +{ + [super mouseMoved:event]; + + if (!self.is_connected) + return; + + NSPoint loc = [event locationInWindow]; + int x = (int)loc.x; + int y = (int)loc.y; + mf_scale_mouse_event(context, instance->input, PTR_FLAGS_MOVE, x, y); +} + +- (void)mouseDown:(NSEvent *)event +{ + [super mouseDown:event]; + + if (!self.is_connected) + return; + + NSPoint loc = [event locationInWindow]; + int x = (int)loc.x; + int y = (int)loc.y; + mf_press_mouse_button(context, instance->input, 0, x, y, TRUE); +} + +- (void)mouseUp:(NSEvent *)event +{ + [super mouseUp:event]; + + if (!self.is_connected) + return; + + NSPoint loc = [event locationInWindow]; + int x = (int)loc.x; + int y = (int)loc.y; + mf_press_mouse_button(context, instance->input, 0, x, y, FALSE); +} + +- (void)rightMouseDown:(NSEvent *)event +{ + [super rightMouseDown:event]; + + if (!self.is_connected) + return; + + NSPoint loc = [event locationInWindow]; + int x = (int)loc.x; + int y = (int)loc.y; + mf_press_mouse_button(context, instance->input, 1, x, y, TRUE); +} + +- (void)rightMouseUp:(NSEvent *)event +{ + [super rightMouseUp:event]; + + if (!self.is_connected) + return; + + NSPoint loc = [event locationInWindow]; + int x = (int)loc.x; + int y = (int)loc.y; + mf_press_mouse_button(context, instance->input, 1, x, y, FALSE); +} + +- (void)otherMouseDown:(NSEvent *)event +{ + [super otherMouseDown:event]; + + if (!self.is_connected) + return; + + NSPoint loc = [event locationInWindow]; + int x = (int)loc.x; + int y = (int)loc.y; + int pressed = [event buttonNumber]; + mf_press_mouse_button(context, instance->input, pressed, x, y, TRUE); +} + +- (void)otherMouseUp:(NSEvent *)event +{ + [super otherMouseUp:event]; + + if (!self.is_connected) + return; + + NSPoint loc = [event locationInWindow]; + int x = (int)loc.x; + int y = (int)loc.y; + int pressed = [event buttonNumber]; + mf_press_mouse_button(context, instance->input, pressed, x, y, FALSE); +} + +- (void)scrollWheel:(NSEvent *)event +{ + UINT16 flags; + [super scrollWheel:event]; + + if (!self.is_connected) + return; + + float dx = [event deltaX]; + float dy = [event deltaY]; + /* 1 event = 120 units */ + UINT16 units = 0; + + if (fabsf(dy) > FLT_EPSILON) + { + flags = PTR_FLAGS_WHEEL; + units = fabsf(dy) * 120; + + if (dy < 0) + flags |= PTR_FLAGS_WHEEL_NEGATIVE; + } + else if (fabsf(dx) > FLT_EPSILON) + { + flags = PTR_FLAGS_HWHEEL; + units = fabsf(dx) * 120; + + if (dx > 0) + flags |= PTR_FLAGS_WHEEL_NEGATIVE; + } + else + return; + + /* Wheel rotation steps: + * + * positive: 0 ... 0xFF -> slow ... fast + * negative: 0 ... 0xFF -> fast ... slow + */ + UINT16 step = units; + if (step > 0xFF) + step = 0xFF; + + /* Negative rotation, so count down steps from top + * 9bit twos complement */ + if (flags & PTR_FLAGS_WHEEL_NEGATIVE) + step = 0x100 - step; + + mf_scale_mouse_event(context, instance->input, flags | step, 0, 0); +} + +- (void)mouseDragged:(NSEvent *)event +{ + [super mouseDragged:event]; + + if (!self.is_connected) + return; + + NSPoint loc = [event locationInWindow]; + int x = (int)loc.x; + int y = (int)loc.y; + // send mouse motion event to RDP server + mf_scale_mouse_event(context, instance->input, PTR_FLAGS_MOVE, x, y); +} + +DWORD fixKeyCode(DWORD keyCode, unichar keyChar, enum APPLE_KEYBOARD_TYPE type) +{ + /** + * In 99% of cases, the given key code is truly keyboard independent. + * This function handles the remaining 1% of edge cases. + * + * Hungarian Keyboard: This is 'QWERTZ' and not 'QWERTY'. + * The '0' key is on the left of the '1' key, where '~' is on a US keyboard. + * A special 'i' letter key with acute is found on the right of the left shift key. + * On the hungarian keyboard, the 'i' key is at the left of the 'Y' key + * Some international keyboards have a corresponding key which would be at + * the left of the 'Z' key when using a QWERTY layout. + * + * The Apple Hungarian keyboard sends inverted key codes for the '0' and 'i' keys. + * When using the US keyboard layout, key codes are left as-is (inverted). + * When using the Hungarian keyboard layout, key codes are swapped (non-inverted). + * This means that when using the Hungarian keyboard layout with a US keyboard, + * the keys corresponding to '0' and 'i' will effectively be inverted. + * + * To fix the '0' and 'i' key inversion, we use the corresponding output character + * provided by OS X and check for a character to key code mismatch: for instance, + * when the output character is '0' for the key code corresponding to the 'i' key. + */ +#if 0 + switch (keyChar) + { + case '0': + case 0x00A7: /* section sign */ + if (keyCode == APPLE_VK_ISO_Section) + keyCode = APPLE_VK_ANSI_Grave; + + break; + + case 0x00ED: /* latin small letter i with acute */ + case 0x00CD: /* latin capital letter i with acute */ + if (keyCode == APPLE_VK_ANSI_Grave) + keyCode = APPLE_VK_ISO_Section; + + break; + } + +#endif + + /* Perform keycode correction for all ISO keyboards */ + + if (type == APPLE_KEYBOARD_TYPE_ISO) + { + if (keyCode == APPLE_VK_ANSI_Grave) + keyCode = APPLE_VK_ISO_Section; + else if (keyCode == APPLE_VK_ISO_Section) + keyCode = APPLE_VK_ANSI_Grave; + } + + return keyCode; +} + +- (void)keyDown:(NSEvent *)event +{ + DWORD keyCode; + DWORD keyFlags; + DWORD vkcode; + DWORD scancode; + unichar keyChar; + NSString *characters; + + if (!is_connected) + return; + + keyFlags = KBD_FLAGS_DOWN; + keyCode = [event keyCode]; + characters = [event charactersIgnoringModifiers]; + + if ([characters length] > 0) + { + keyChar = [characters characterAtIndex:0]; + keyCode = fixKeyCode(keyCode, keyChar, mfc->appleKeyboardType); + } + + vkcode = GetVirtualKeyCodeFromKeycode(keyCode + 8, KEYCODE_TYPE_APPLE); + scancode = GetVirtualScanCodeFromVirtualKeyCode(vkcode, 4); + keyFlags |= (scancode & KBDEXT) ? KBDEXT : 0; + scancode &= 0xFF; + vkcode &= 0xFF; +#if 0 + WLog_ERR(TAG, + "keyDown: keyCode: 0x%04X scancode: 0x%04X vkcode: 0x%04X keyFlags: %d name: %s", + keyCode, scancode, vkcode, keyFlags, GetVirtualKeyName(vkcode)); +#endif + sync_keyboard_state(instance); + freerdp_input_send_keyboard_event(instance->input, keyFlags, scancode); +} + +- (void)keyUp:(NSEvent *)event +{ + DWORD keyCode; + DWORD keyFlags; + DWORD vkcode; + DWORD scancode; + unichar keyChar; + NSString *characters; + + if (!is_connected) + return; + + keyFlags = KBD_FLAGS_RELEASE; + keyCode = [event keyCode]; + characters = [event charactersIgnoringModifiers]; + + if ([characters length] > 0) + { + keyChar = [characters characterAtIndex:0]; + keyCode = fixKeyCode(keyCode, keyChar, mfc->appleKeyboardType); + } + + vkcode = GetVirtualKeyCodeFromKeycode(keyCode + 8, KEYCODE_TYPE_APPLE); + scancode = GetVirtualScanCodeFromVirtualKeyCode(vkcode, 4); + keyFlags |= (scancode & KBDEXT) ? KBDEXT : 0; + scancode &= 0xFF; + vkcode &= 0xFF; +#if 0 + WLog_DBG(TAG, + "keyUp: key: 0x%04X scancode: 0x%04X vkcode: 0x%04X keyFlags: %d name: %s", + keyCode, scancode, vkcode, keyFlags, GetVirtualKeyName(vkcode)); +#endif + freerdp_input_send_keyboard_event(instance->input, keyFlags, scancode); +} + +- (void)flagsChanged:(NSEvent *)event +{ + int key; + DWORD keyFlags; + DWORD vkcode; + DWORD scancode; + DWORD modFlags; + + if (!is_connected) + return; + + keyFlags = 0; + key = [event keyCode] + 8; + modFlags = [event modifierFlags] & NSDeviceIndependentModifierFlagsMask; + vkcode = GetVirtualKeyCodeFromKeycode(key, KEYCODE_TYPE_APPLE); + scancode = GetVirtualScanCodeFromVirtualKeyCode(vkcode, 4); + keyFlags |= (scancode & KBDEXT) ? KBDEXT : 0; + scancode &= 0xFF; + vkcode &= 0xFF; +#if 0 + WLog_DBG(TAG, + "flagsChanged: key: 0x%04X scancode: 0x%04X vkcode: 0x%04X extended: %d name: %s modFlags: 0x%04X", + key - 8, scancode, vkcode, keyFlags, GetVirtualKeyName(vkcode), modFlags); + + if (modFlags & NSAlphaShiftKeyMask) + WLog_DBG(TAG, "NSAlphaShiftKeyMask"); + + if (modFlags & NSShiftKeyMask) + WLog_DBG(TAG, "NSShiftKeyMask"); + + if (modFlags & NSControlKeyMask) + WLog_DBG(TAG, "NSControlKeyMask"); + + if (modFlags & NSAlternateKeyMask) + WLog_DBG(TAG, "NSAlternateKeyMask"); + + if (modFlags & NSCommandKeyMask) + WLog_DBG(TAG, "NSCommandKeyMask"); + + if (modFlags & NSNumericPadKeyMask) + WLog_DBG(TAG, "NSNumericPadKeyMask"); + + if (modFlags & NSHelpKeyMask) + WLog_DBG(TAG, "NSHelpKeyMask"); + +#endif + + if ((modFlags & NSAlphaShiftKeyMask) && !(kbdModFlags & NSAlphaShiftKeyMask)) + freerdp_input_send_keyboard_event(instance->input, keyFlags | KBD_FLAGS_DOWN, scancode); + else if (!(modFlags & NSAlphaShiftKeyMask) && (kbdModFlags & NSAlphaShiftKeyMask)) + freerdp_input_send_keyboard_event(instance->input, keyFlags | KBD_FLAGS_RELEASE, scancode); + + if ((modFlags & NSShiftKeyMask) && !(kbdModFlags & NSShiftKeyMask)) + freerdp_input_send_keyboard_event(instance->input, keyFlags | KBD_FLAGS_DOWN, scancode); + else if (!(modFlags & NSShiftKeyMask) && (kbdModFlags & NSShiftKeyMask)) + freerdp_input_send_keyboard_event(instance->input, keyFlags | KBD_FLAGS_RELEASE, scancode); + + if ((modFlags & NSControlKeyMask) && !(kbdModFlags & NSControlKeyMask)) + freerdp_input_send_keyboard_event(instance->input, keyFlags | KBD_FLAGS_DOWN, scancode); + else if (!(modFlags & NSControlKeyMask) && (kbdModFlags & NSControlKeyMask)) + freerdp_input_send_keyboard_event(instance->input, keyFlags | KBD_FLAGS_RELEASE, scancode); + + if ((modFlags & NSAlternateKeyMask) && !(kbdModFlags & NSAlternateKeyMask)) + freerdp_input_send_keyboard_event(instance->input, keyFlags | KBD_FLAGS_DOWN, scancode); + else if (!(modFlags & NSAlternateKeyMask) && (kbdModFlags & NSAlternateKeyMask)) + freerdp_input_send_keyboard_event(instance->input, keyFlags | KBD_FLAGS_RELEASE, scancode); + + if ((modFlags & NSCommandKeyMask) && !(kbdModFlags & NSCommandKeyMask)) + freerdp_input_send_keyboard_event(instance->input, keyFlags | KBD_FLAGS_DOWN, scancode); + else if (!(modFlags & NSCommandKeyMask) && (kbdModFlags & NSCommandKeyMask)) + freerdp_input_send_keyboard_event(instance->input, keyFlags | KBD_FLAGS_RELEASE, scancode); + + if ((modFlags & NSNumericPadKeyMask) && !(kbdModFlags & NSNumericPadKeyMask)) + freerdp_input_send_keyboard_event(instance->input, keyFlags | KBD_FLAGS_DOWN, scancode); + else if (!(modFlags & NSNumericPadKeyMask) && (kbdModFlags & NSNumericPadKeyMask)) + freerdp_input_send_keyboard_event(instance->input, keyFlags | KBD_FLAGS_RELEASE, scancode); + + if ((modFlags & NSHelpKeyMask) && !(kbdModFlags & NSHelpKeyMask)) + freerdp_input_send_keyboard_event(instance->input, keyFlags | KBD_FLAGS_DOWN, scancode); + else if (!(modFlags & NSHelpKeyMask) && (kbdModFlags & NSHelpKeyMask)) + freerdp_input_send_keyboard_event(instance->input, keyFlags | KBD_FLAGS_RELEASE, scancode); + + kbdModFlags = modFlags; +} + +- (void)releaseResources +{ + int i; + + for (i = 0; i < argc; i++) + free(argv[i]); + + if (!is_connected) + return; + + free(pixel_data); +} + +- (void)drawRect:(NSRect)rect +{ + if (!context) + return; + + if (self->bitmap_context) + { + CGContextRef cgContext = [[NSGraphicsContext currentContext] graphicsPort]; + CGImageRef cgImage = CGBitmapContextCreateImage(self->bitmap_context); + CGContextSaveGState(cgContext); + CGContextClipToRect( + cgContext, CGRectMake(rect.origin.x, rect.origin.y, rect.size.width, rect.size.height)); + CGContextDrawImage(cgContext, + CGRectMake(0, 0, [self bounds].size.width, [self bounds].size.height), + cgImage); + CGContextRestoreGState(cgContext); + CGImageRelease(cgImage); + } + else + { + /* Fill the screen with black */ + [[NSColor blackColor] set]; + NSRectFill([self bounds]); + } +} + +- (void)onPasteboardTimerFired:(NSTimer *)timer +{ + const BYTE *data; + UINT32 size; + UINT32 formatId; + BOOL formatMatch; + int changeCount; + NSData *formatData; + const char *formatType; + NSPasteboardItem *item; + changeCount = (int)[pasteboard_rd changeCount]; + + if (changeCount == pasteboard_changecount) + return; + + pasteboard_changecount = changeCount; + NSArray *items = [pasteboard_rd pasteboardItems]; + + if ([items count] < 1) + return; + + item = [items objectAtIndex:0]; + /** + * System-Declared Uniform Type Identifiers: + * https://developer.apple.com/library/ios/documentation/Miscellaneous/Reference/UTIRef/Articles/System-DeclaredUniformTypeIdentifiers.html + */ + formatMatch = FALSE; + + for (NSString *type in [item types]) + { + formatType = [type UTF8String]; + + if (strcmp(formatType, "public.utf8-plain-text") == 0) + { + formatData = [item dataForType:type]; + formatId = ClipboardRegisterFormat(mfc->clipboard, "UTF8_STRING"); + size = (UINT32)[formatData length]; + data = [formatData bytes]; + /* size is the string length without the terminating NULL terminator */ + ClipboardSetData(mfc->clipboard, formatId, data, size + 1); + formatMatch = TRUE; + break; + } + } + + if (!formatMatch) + ClipboardEmpty(mfc->clipboard); + + if (mfc->clipboardSync) + mac_cliprdr_send_client_format_list(mfc->cliprdr); +} + +- (void)pause +{ + dispatch_async(dispatch_get_main_queue(), ^{ + [self->pasteboard_timer invalidate]; + }); + NSArray *trackingAreas = self.trackingAreas; + + for (NSTrackingArea *ta in trackingAreas) + { + [self removeTrackingArea:ta]; + } +} + +- (void)resume +{ + if (!self.is_connected) + return; + + dispatch_async(dispatch_get_main_queue(), ^{ + self->pasteboard_timer = + [NSTimer scheduledTimerWithTimeInterval:0.5 + target:self + selector:@selector(onPasteboardTimerFired:) + userInfo:nil + repeats:YES]; + + NSTrackingArea *trackingArea = [[NSTrackingArea alloc] + initWithRect:[self visibleRect] + options:NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved | + NSTrackingCursorUpdate | NSTrackingEnabledDuringMouseDrag | + NSTrackingActiveWhenFirstResponder + owner:self + userInfo:nil]; + [self addTrackingArea:trackingArea]; + [trackingArea release]; + }); +} + +- (void)setScrollOffset:(int)xOffset y:(int)yOffset w:(int)width h:(int)height +{ + mfc->yCurrentScroll = yOffset; + mfc->xCurrentScroll = xOffset; + mfc->client_height = height; + mfc->client_width = width; +} + +void mac_OnChannelConnectedEventHandler(void *context, ChannelConnectedEventArgs *e) +{ + mfContext *mfc = (mfContext *)context; + rdpSettings *settings = mfc->context.settings; + + if (strcmp(e->name, RDPEI_DVC_CHANNEL_NAME) == 0) + { + } + else if (strcmp(e->name, RDPGFX_DVC_CHANNEL_NAME) == 0) + { + if (settings->SoftwareGdi) + gdi_graphics_pipeline_init(mfc->context.gdi, (RdpgfxClientContext *)e->pInterface); + } + else if (strcmp(e->name, CLIPRDR_SVC_CHANNEL_NAME) == 0) + { + mac_cliprdr_init(mfc, (CliprdrClientContext *)e->pInterface); + } + else if (strcmp(e->name, ENCOMSP_SVC_CHANNEL_NAME) == 0) + { + } +} + +void mac_OnChannelDisconnectedEventHandler(void *context, ChannelDisconnectedEventArgs *e) +{ + mfContext *mfc = (mfContext *)context; + rdpSettings *settings = mfc->context.settings; + + if (strcmp(e->name, RDPEI_DVC_CHANNEL_NAME) == 0) + { + } + else if (strcmp(e->name, RDPGFX_DVC_CHANNEL_NAME) == 0) + { + if (settings->SoftwareGdi) + gdi_graphics_pipeline_uninit(mfc->context.gdi, (RdpgfxClientContext *)e->pInterface); + } + else if (strcmp(e->name, CLIPRDR_SVC_CHANNEL_NAME) == 0) + { + mac_cliprdr_uninit(mfc, (CliprdrClientContext *)e->pInterface); + } + else if (strcmp(e->name, ENCOMSP_SVC_CHANNEL_NAME) == 0) + { + } +} + +BOOL mac_pre_connect(freerdp *instance) +{ + rdpSettings *settings; + instance->update->BeginPaint = mac_begin_paint; + instance->update->EndPaint = mac_end_paint; + instance->update->DesktopResize = mac_desktop_resize; + settings = instance->settings; + + if (!settings->ServerHostname) + { + WLog_ERR(TAG, "error: server hostname was not specified with /v:[:port]"); + return FALSE; + } + + settings->OsMajorType = OSMAJORTYPE_MACINTOSH; + settings->OsMinorType = OSMINORTYPE_MACINTOSH; + PubSub_SubscribeChannelConnected(instance->context->pubSub, mac_OnChannelConnectedEventHandler); + PubSub_SubscribeChannelDisconnected(instance->context->pubSub, + mac_OnChannelDisconnectedEventHandler); + + if (!freerdp_client_load_addins(instance->context->channels, instance->settings)) + return FALSE; + + return TRUE; +} + +BOOL mac_post_connect(freerdp *instance) +{ + rdpGdi *gdi; + rdpSettings *settings; + rdpPointer rdp_pointer; + mfContext *mfc = (mfContext *)instance->context; + MRDPView *view = (MRDPView *)mfc->view; + ZeroMemory(&rdp_pointer, sizeof(rdpPointer)); + rdp_pointer.size = sizeof(rdpPointer); + rdp_pointer.New = mf_Pointer_New; + rdp_pointer.Free = mf_Pointer_Free; + rdp_pointer.Set = mf_Pointer_Set; + rdp_pointer.SetNull = mf_Pointer_SetNull; + rdp_pointer.SetDefault = mf_Pointer_SetDefault; + rdp_pointer.SetPosition = mf_Pointer_SetPosition; + settings = instance->settings; + + if (!gdi_init(instance, PIXEL_FORMAT_BGRX32)) + return FALSE; + + gdi = instance->context->gdi; + view->bitmap_context = mac_create_bitmap_context(instance->context); + graphics_register_pointer(instance->context->graphics, &rdp_pointer); + /* setup pasteboard (aka clipboard) for copy operations (write only) */ + view->pasteboard_wr = [NSPasteboard generalPasteboard]; + /* setup pasteboard for read operations */ + dispatch_async(dispatch_get_main_queue(), ^{ + view->pasteboard_rd = [NSPasteboard generalPasteboard]; + view->pasteboard_changecount = -1; + }); + [view resume]; + mfc->appleKeyboardType = mac_detect_keyboard_type(); + return TRUE; +} + +void mac_post_disconnect(freerdp *instance) +{ + mfContext *mfc; + MRDPView *view; + if (!instance || !instance->context) + return; + + mfc = (mfContext *)instance->context; + view = (MRDPView *)mfc->view; + + [view pause]; + + PubSub_UnsubscribeChannelConnected(instance->context->pubSub, + mac_OnChannelConnectedEventHandler); + PubSub_UnsubscribeChannelDisconnected(instance->context->pubSub, + mac_OnChannelDisconnectedEventHandler); + gdi_free(instance); +} + +static BOOL mac_authenticate_int(NSString *title, freerdp *instance, char **username, + char **password, char **domain) +{ + mfContext *mfc = (mfContext *)instance->context; + MRDPView *view = (MRDPView *)mfc->view; + PasswordDialog *dialog = [PasswordDialog new]; + dialog.serverHostname = title; + + if (*username) + dialog.username = [NSString stringWithCString:*username encoding:NSUTF8StringEncoding]; + + if (*password) + dialog.password = [NSString stringWithCString:*password encoding:NSUTF8StringEncoding]; + + if (*domain) + dialog.domain = [NSString stringWithCString:*domain encoding:NSUTF8StringEncoding]; + + dispatch_sync(dispatch_get_main_queue(), ^{ + [dialog performSelectorOnMainThread:@selector(runModal:) + withObject:[view window] + waitUntilDone:TRUE]; + }); + BOOL ok = dialog.modalCode; + + if (ok) + { + size_t ulen, plen, dlen; + const char *submittedUsername = [dialog.username cStringUsingEncoding:NSUTF8StringEncoding]; + ulen = (strlen(submittedUsername) + 1) * sizeof(char); + *username = malloc(ulen); + + if (!(*username)) + return FALSE; + + sprintf_s(*username, ulen, "%s", submittedUsername); + const char *submittedPassword = [dialog.password cStringUsingEncoding:NSUTF8StringEncoding]; + plen = (strlen(submittedPassword) + 1) * sizeof(char); + *password = malloc(plen); + + if (!(*password)) + return FALSE; + + sprintf_s(*password, plen, "%s", submittedPassword); + const char *submittedDomain = [dialog.domain cStringUsingEncoding:NSUTF8StringEncoding]; + dlen = (strlen(submittedDomain) + 1) * sizeof(char); + *domain = malloc(dlen); + + if (!(*domain)) + return FALSE; + + sprintf_s(*domain, dlen, "%s", submittedDomain); + } + + return ok; +} + +BOOL mac_authenticate(freerdp *instance, char **username, char **password, char **domain) +{ + NSString *title = + [NSString stringWithFormat:@"%@:%u", + [NSString stringWithCString:instance->settings->ServerHostname + encoding:NSUTF8StringEncoding], + instance -> settings -> ServerPort]; + return mac_authenticate_int(title, instance, username, password, domain); +} + +BOOL mac_gw_authenticate(freerdp *instance, char **username, char **password, char **domain) +{ + NSString *title = + [NSString stringWithFormat:@"%@:%u", + [NSString stringWithCString:instance->settings->GatewayHostname + encoding:NSUTF8StringEncoding], + instance -> settings -> GatewayPort]; + return mac_authenticate_int(title, instance, username, password, domain); +} + +DWORD mac_verify_certificate_ex(freerdp *instance, const char *host, UINT16 port, + const char *common_name, const char *subject, const char *issuer, + const char *fingerprint, DWORD flags) +{ + mfContext *mfc = (mfContext *)instance->context; + MRDPView *view = (MRDPView *)mfc->view; + CertificateDialog *dialog = [CertificateDialog new]; + const char *type = "RDP-Server"; + char hostname[8192]; + + if (flags & VERIFY_CERT_FLAG_GATEWAY) + type = "RDP-Gateway"; + + if (flags & VERIFY_CERT_FLAG_REDIRECT) + type = "RDP-Redirect"; + + sprintf_s(hostname, sizeof(hostname), "%s %s:%" PRIu16, type, host, port); + dialog.serverHostname = [NSString stringWithCString:hostname]; + dialog.commonName = [NSString stringWithCString:common_name encoding:NSUTF8StringEncoding]; + dialog.subject = [NSString stringWithCString:subject encoding:NSUTF8StringEncoding]; + dialog.issuer = [NSString stringWithCString:issuer encoding:NSUTF8StringEncoding]; + dialog.fingerprint = [NSString stringWithCString:fingerprint encoding:NSUTF8StringEncoding]; + + if (flags & VERIFY_CERT_FLAG_MISMATCH) + dialog.hostMismatch = TRUE; + + if (flags & VERIFY_CERT_FLAG_CHANGED) + dialog.changed = TRUE; + + [dialog performSelectorOnMainThread:@selector(runModal:) + withObject:[view window] + waitUntilDone:TRUE]; + return dialog.result; +} + +DWORD mac_verify_changed_certificate_ex(freerdp *instance, const char *host, UINT16 port, + const char *common_name, const char *subject, + const char *issuer, const char *fingerprint, + const char *old_subject, const char *old_issuer, + const char *old_fingerprint, DWORD flags) +{ + mfContext *mfc = (mfContext *)instance->context; + MRDPView *view = (MRDPView *)mfc->view; + CertificateDialog *dialog = [CertificateDialog new]; + const char *type = "RDP-Server"; + char hostname[8192]; + + if (flags & VERIFY_CERT_FLAG_GATEWAY) + type = "RDP-Gateway"; + + if (flags & VERIFY_CERT_FLAG_REDIRECT) + type = "RDP-Redirect"; + + sprintf_s(hostname, sizeof(hostname), "%s %s:%" PRIu16, type, host, port); + dialog.serverHostname = [NSString stringWithCString:hostname]; + dialog.commonName = [NSString stringWithCString:common_name encoding:NSUTF8StringEncoding]; + dialog.subject = [NSString stringWithCString:subject encoding:NSUTF8StringEncoding]; + dialog.issuer = [NSString stringWithCString:issuer encoding:NSUTF8StringEncoding]; + dialog.fingerprint = [NSString stringWithCString:fingerprint encoding:NSUTF8StringEncoding]; + + if (flags & VERIFY_CERT_FLAG_MISMATCH) + dialog.hostMismatch = TRUE; + + if (flags & VERIFY_CERT_FLAG_CHANGED) + dialog.changed = TRUE; + + [dialog performSelectorOnMainThread:@selector(runModal:) + withObject:[view window] + waitUntilDone:TRUE]; + return dialog.result; +} + +int mac_logon_error_info(freerdp *instance, UINT32 data, UINT32 type) +{ + const char *str_data = freerdp_get_logon_error_info_data(data); + const char *str_type = freerdp_get_logon_error_info_type(type); + // TODO: Error message dialog + WLog_INFO(TAG, "Logon Error Info %s [%s]", str_data, str_type); + return 1; +} + +BOOL mf_Pointer_New(rdpContext *context, rdpPointer *pointer) +{ + rdpGdi *gdi; + NSRect rect; + NSImage *image; + NSPoint hotSpot; + NSCursor *cursor; + BYTE *cursor_data; + NSMutableArray *ma; + NSBitmapImageRep *bmiRep; + MRDPCursor *mrdpCursor = [[MRDPCursor alloc] init]; + mfContext *mfc = (mfContext *)context; + MRDPView *view; + UINT32 format; + + if (!mfc || !context || !pointer) + return FALSE; + + view = (MRDPView *)mfc->view; + gdi = context->gdi; + + if (!gdi || !view) + return FALSE; + + rect.size.width = pointer->width; + rect.size.height = pointer->height; + rect.origin.x = pointer->xPos; + rect.origin.y = pointer->yPos; + cursor_data = (BYTE *)malloc(rect.size.width * rect.size.height * 4); + + if (!cursor_data) + return FALSE; + + mrdpCursor->cursor_data = cursor_data; + format = PIXEL_FORMAT_RGBA32; + + if (!freerdp_image_copy_from_pointer_data(cursor_data, format, 0, 0, 0, pointer->width, + pointer->height, pointer->xorMaskData, + pointer->lengthXorMask, pointer->andMaskData, + pointer->lengthAndMask, pointer->xorBpp, NULL)) + { + free(cursor_data); + mrdpCursor->cursor_data = NULL; + return FALSE; + } + + /* store cursor bitmap image in representation - required by NSImage */ + bmiRep = [[NSBitmapImageRep alloc] + initWithBitmapDataPlanes:(unsigned char **)&cursor_data + pixelsWide:rect.size.width + pixelsHigh:rect.size.height + bitsPerSample:8 + samplesPerPixel:4 + hasAlpha:YES + isPlanar:NO + colorSpaceName:NSDeviceRGBColorSpace + bitmapFormat:0 + bytesPerRow:rect.size.width * GetBytesPerPixel(format) + bitsPerPixel:0]; + mrdpCursor->bmiRep = bmiRep; + /* create an image using above representation */ + image = [[NSImage alloc] initWithSize:[bmiRep size]]; + [image addRepresentation:bmiRep]; + [image setFlipped:NO]; + mrdpCursor->nsImage = image; + /* need hotspot to create cursor */ + hotSpot.x = pointer->xPos; + hotSpot.y = pointer->yPos; + cursor = [[NSCursor alloc] initWithImage:image hotSpot:hotSpot]; + mrdpCursor->nsCursor = cursor; + mrdpCursor->pointer = pointer; + /* save cursor for later use in mf_Pointer_Set() */ + ma = view->cursors; + [ma addObject:mrdpCursor]; + return TRUE; +} + +void mf_Pointer_Free(rdpContext *context, rdpPointer *pointer) +{ + mfContext *mfc = (mfContext *)context; + MRDPView *view = (MRDPView *)mfc->view; + NSMutableArray *ma = view->cursors; + + for (MRDPCursor *cursor in ma) + { + if (cursor->pointer == pointer) + { + cursor->nsImage = nil; + cursor->nsCursor = nil; + cursor->bmiRep = nil; + free(cursor->cursor_data); + [ma removeObject:cursor]; + return; + } + } +} + +BOOL mf_Pointer_Set(rdpContext *context, const rdpPointer *pointer) +{ + mfContext *mfc = (mfContext *)context; + MRDPView *view = (MRDPView *)mfc->view; + NSMutableArray *ma = view->cursors; + + for (MRDPCursor *cursor in ma) + { + if (cursor->pointer == pointer) + { + [view setCursor:cursor->nsCursor]; + return TRUE; + } + } + + NSLog(@"Cursor not found"); + return TRUE; +} + +BOOL mf_Pointer_SetNull(rdpContext *context) +{ + return TRUE; +} + +BOOL mf_Pointer_SetDefault(rdpContext *context) +{ + mfContext *mfc = (mfContext *)context; + MRDPView *view = (MRDPView *)mfc->view; + [view setCursor:[NSCursor arrowCursor]]; + return TRUE; +} + +static BOOL mf_Pointer_SetPosition(rdpContext *context, UINT32 x, UINT32 y) +{ + mfContext *mfc = (mfContext *)context; + + if (!mfc) + return FALSE; + + /* TODO: Set pointer position */ + return TRUE; +} + +CGContextRef mac_create_bitmap_context(rdpContext *context) +{ + CGContextRef bitmap_context; + rdpGdi *gdi = context->gdi; + UINT32 bpp = GetBytesPerPixel(gdi->dstFormat); + CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); + + if (bpp == 2) + { + bitmap_context = CGBitmapContextCreate( + gdi->primary_buffer, gdi->width, gdi->height, 5, gdi->stride, colorSpace, + kCGBitmapByteOrder16Little | kCGImageAlphaNoneSkipFirst); + } + else + { + bitmap_context = CGBitmapContextCreate( + gdi->primary_buffer, gdi->width, gdi->height, 8, gdi->stride, colorSpace, + kCGBitmapByteOrder32Little | kCGImageAlphaNoneSkipFirst); + } + + CGColorSpaceRelease(colorSpace); + return bitmap_context; +} + +BOOL mac_begin_paint(rdpContext *context) +{ + rdpGdi *gdi = context->gdi; + + if (!gdi) + return FALSE; + + gdi->primary->hdc->hwnd->invalid->null = TRUE; + return TRUE; +} + +BOOL mac_end_paint(rdpContext *context) +{ + rdpGdi *gdi; + HGDI_RGN invalid; + NSRect newDrawRect; + int ww, wh, dw, dh; + mfContext *mfc = (mfContext *)context; + MRDPView *view = (MRDPView *)mfc->view; + gdi = context->gdi; + + if (!gdi) + return FALSE; + + ww = mfc->client_width; + wh = mfc->client_height; + dw = mfc->context.settings->DesktopWidth; + dh = mfc->context.settings->DesktopHeight; + + if ((!context) || (!context->gdi)) + return FALSE; + + if (context->gdi->primary->hdc->hwnd->invalid->null) + return TRUE; + + invalid = gdi->primary->hdc->hwnd->invalid; + newDrawRect.origin.x = invalid->x; + newDrawRect.origin.y = invalid->y; + newDrawRect.size.width = invalid->w; + newDrawRect.size.height = invalid->h; + + if (mfc->context.settings->SmartSizing && (ww != dw || wh != dh)) + { + newDrawRect.origin.y = newDrawRect.origin.y * wh / dh - 1; + newDrawRect.size.height = newDrawRect.size.height * wh / dh + 1; + newDrawRect.origin.x = newDrawRect.origin.x * ww / dw - 1; + newDrawRect.size.width = newDrawRect.size.width * ww / dw + 1; + } + else + { + newDrawRect.origin.y = newDrawRect.origin.y - 1; + newDrawRect.size.height = newDrawRect.size.height + 1; + newDrawRect.origin.x = newDrawRect.origin.x - 1; + newDrawRect.size.width = newDrawRect.size.width + 1; + } + + windows_to_apple_cords(mfc->view, &newDrawRect); + dispatch_sync(dispatch_get_main_queue(), ^{ + [view setNeedsDisplayInRect:newDrawRect]; + }); + gdi->primary->hdc->hwnd->ninvalid = 0; + return TRUE; +} + +BOOL mac_desktop_resize(rdpContext *context) +{ + ResizeWindowEventArgs e; + mfContext *mfc = (mfContext *)context; + MRDPView *view = (MRDPView *)mfc->view; + rdpSettings *settings = context->settings; + + if (!context->gdi) + return TRUE; + + /** + * TODO: Fix resizing race condition. We should probably implement a message to be + * put on the update message queue to be able to properly flush pending updates, + * resize, and then continue with post-resizing graphical updates. + */ + CGContextRef old_context = view->bitmap_context; + view->bitmap_context = NULL; + CGContextRelease(old_context); + mfc->width = settings->DesktopWidth; + mfc->height = settings->DesktopHeight; + + if (!gdi_resize(context->gdi, mfc->width, mfc->height)) + return FALSE; + + view->bitmap_context = mac_create_bitmap_context(context); + + if (!view->bitmap_context) + return FALSE; + + mfc->client_width = mfc->width; + mfc->client_height = mfc->height; + [view setFrameSize:NSMakeSize(mfc->width, mfc->height)]; + EventArgsInit(&e, "mfreerdp"); + e.width = settings->DesktopWidth; + e.height = settings->DesktopHeight; + PubSub_OnResizeWindow(context->pubSub, context, &e); + return TRUE; +} + +void input_activity_cb(freerdp *instance) +{ + int status; + wMessage message; + wMessageQueue *queue; + status = 1; + queue = freerdp_get_message_queue(instance, FREERDP_INPUT_MESSAGE_QUEUE); + + if (queue) + { + while (MessageQueue_Peek(queue, &message, TRUE)) + { + status = freerdp_message_queue_process_message(instance, FREERDP_INPUT_MESSAGE_QUEUE, + &message); + + if (!status) + break; + } + } + else + { + WLog_ERR(TAG, "input_activity_cb: No queue!"); + } +} + +/** + * given a rect with 0,0 at the top left (windows cords) + * convert it to a rect with 0,0 at the bottom left (apple cords) + * + * Note: the formula works for conversions in both directions. + * + */ + +void windows_to_apple_cords(MRDPView *view, NSRect *r) +{ + dispatch_sync(dispatch_get_main_queue(), ^{ + r->origin.y = [view frame].size.height - (r->origin.y + r->size.height); + }); +} + +void sync_keyboard_state(freerdp *instance) +{ + mfContext *context = (mfContext *)instance->context; + UINT32 flags = 0; + CGEventFlags currentFlags = CGEventSourceFlagsState(kCGEventSourceStateHIDSystemState); + + if (context->kbdFlags != currentFlags) + { + if (currentFlags & kCGEventFlagMaskAlphaShift) + flags |= KBD_SYNC_CAPS_LOCK; + + if (currentFlags & kCGEventFlagMaskNumericPad) + flags |= KBD_SYNC_NUM_LOCK; + + freerdp_input_send_synchronize_event(instance->input, flags); + context->kbdFlags = currentFlags; + } +} + +@end diff --git a/client/Mac/ModuleOptions.cmake b/client/Mac/ModuleOptions.cmake new file mode 100644 index 0000000..3902d2b --- /dev/null +++ b/client/Mac/ModuleOptions.cmake @@ -0,0 +1,4 @@ + +set(FREERDP_CLIENT_NAME "mfreerdp") +set(FREERDP_CLIENT_PLATFORM "MacOSX") +set(FREERDP_CLIENT_VENDOR "FreeRDP") diff --git a/client/Mac/PasswordDialog.h b/client/Mac/PasswordDialog.h new file mode 100644 index 0000000..eb24c5c --- /dev/null +++ b/client/Mac/PasswordDialog.h @@ -0,0 +1,49 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * MacFreeRDP + * + * Copyright 2013 Christian Hofstaedtler + * + * Licensed 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. + */ + +#import + +@interface PasswordDialog : NSWindowController +{ + @public + NSTextField *usernameText; + NSTextField *passwordText; + NSTextField *messageLabel; + NSString *serverHostname; + NSString *username; + NSString *password; + NSString *domain; + BOOL modalCode; +} +@property(retain) IBOutlet NSTextField *usernameText; +@property(retain) IBOutlet NSTextField *passwordText; +@property(retain) IBOutlet NSTextField *messageLabel; + +- (IBAction)onOK:(NSObject *)sender; +- (IBAction)onCancel:(NSObject *)sender; + +@property(retain) NSString *serverHostname; +@property(retain) NSString *username; +@property(retain) NSString *password; +@property(retain) NSString *domain; +@property(readonly) BOOL modalCode; + +- (BOOL)runModal:(NSWindow *)mainWindow; + +@end diff --git a/client/Mac/PasswordDialog.m b/client/Mac/PasswordDialog.m new file mode 100644 index 0000000..f4c520b --- /dev/null +++ b/client/Mac/PasswordDialog.m @@ -0,0 +1,134 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * MacFreeRDP + * + * Copyright 2013 Christian Hofstaedtler + * + * Licensed 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. + */ + +#import "PasswordDialog.h" +#import + +#import + +@interface PasswordDialog () + +@property BOOL modalCode; + +@end + +@implementation PasswordDialog + +@synthesize usernameText; +@synthesize passwordText; +@synthesize messageLabel; +@synthesize serverHostname; +@synthesize username; +@synthesize password; +@synthesize domain; +@synthesize modalCode; + +- (id)init +{ + return [self initWithWindowNibName:@"PasswordDialog"]; +} + +- (void)windowDidLoad +{ + [super windowDidLoad]; + // Implement this method to handle any initialization after your window controller's window has + // been loaded from its nib file. + [self.window setTitle:self.serverHostname]; + [self.messageLabel + setStringValue:[NSString stringWithFormat:@"Authenticate to %@", self.serverHostname]]; + NSMutableString *domainUser = [[NSMutableString alloc] initWithString:@""]; + + if (self.domain != nil && + [[self.domain stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]] + length] > 0) + { + [domainUser appendFormat:@"%@\\", self.domain]; + } + + if (self.username != nil) + { + [domainUser appendString:self.username]; + [self.window makeFirstResponder:self.passwordText]; + } + + [self.usernameText setStringValue:domainUser]; +} + +- (IBAction)onOK:(NSObject *)sender +{ + char *submittedUser = NULL; + char *submittedDomain = NULL; + + if (freerdp_parse_username( + [self.usernameText.stringValue cStringUsingEncoding:NSUTF8StringEncoding], + &submittedUser, &submittedDomain)) + { + self.username = [NSString stringWithCString:submittedUser encoding:NSUTF8StringEncoding]; + self.domain = [NSString stringWithCString:submittedDomain encoding:NSUTF8StringEncoding]; + } + else + { + self.username = self.usernameText.stringValue; + } + + self.password = self.passwordText.stringValue; + [NSApp stopModalWithCode:TRUE]; +} + +- (IBAction)onCancel:(NSObject *)sender +{ + [NSApp stopModalWithCode:FALSE]; +} + +- (BOOL)runModal:(NSWindow *)mainWindow +{ + if ([mainWindow respondsToSelector:@selector(beginSheet:completionHandler:)]) + { + [mainWindow beginSheet:self.window completionHandler:nil]; + self.modalCode = [NSApp runModalForWindow:self.window]; + [mainWindow endSheet:self.window]; + } + else + { + [NSApp beginSheet:self.window + modalForWindow:mainWindow + modalDelegate:nil + didEndSelector:nil + contextInfo:nil]; + self.modalCode = [NSApp runModalForWindow:self.window]; + [NSApp endSheet:self.window]; + } + + [self.window orderOut:nil]; + return self.modalCode; +} + +- (void)dealloc +{ + [usernameText release]; + [passwordText release]; + [messageLabel release]; + [serverHostname release]; + [username release]; + [password release]; + [domain release]; + [super dealloc]; +} + +@end diff --git a/client/Mac/PasswordDialog.xib b/client/Mac/PasswordDialog.xib new file mode 100644 index 0000000..3911c14 --- /dev/null +++ b/client/Mac/PasswordDialog.xib @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + NSAllRomanInputSourcesLocaleIdentifier + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/client/Mac/cli/AppDelegate.h b/client/Mac/cli/AppDelegate.h new file mode 100644 index 0000000..64b2611 --- /dev/null +++ b/client/Mac/cli/AppDelegate.h @@ -0,0 +1,26 @@ +// +// AppDelegate.h +// MacClient2 +// +// Created by Benoît et Kathy on 2013-05-08. +// +// + +#import +#import +#import + +@interface AppDelegate : NSObject +{ + @public + NSWindow *window; + rdpContext *context; + MRDPView *mrdpView; +} + +- (void)rdpConnectError:(NSString *)customMessage; + +@property(assign) IBOutlet NSWindow *window; +@property(assign) rdpContext *context; + +@end diff --git a/client/Mac/cli/AppDelegate.m b/client/Mac/cli/AppDelegate.m new file mode 100644 index 0000000..e6e625b --- /dev/null +++ b/client/Mac/cli/AppDelegate.m @@ -0,0 +1,307 @@ +// +// AppDelegate.m +// MacClient2 +// +// Created by Benoît et Kathy on 2013-05-08. +// +// + +#import "AppDelegate.h" +#import "MacFreeRDP/mfreerdp.h" +#import "MacFreeRDP/mf_client.h" +#import "MacFreeRDP/MRDPView.h" +#import + +static AppDelegate *_singleDelegate = nil; +void AppDelegate_ConnectionResultEventHandler(void *context, ConnectionResultEventArgs *e); +void AppDelegate_ErrorInfoEventHandler(void *ctx, ErrorInfoEventArgs *e); +void AppDelegate_EmbedWindowEventHandler(void *context, EmbedWindowEventArgs *e); +void AppDelegate_ResizeWindowEventHandler(void *context, ResizeWindowEventArgs *e); +void mac_set_view_size(rdpContext *context, MRDPView *view); + +@implementation AppDelegate + +- (void)dealloc +{ + [super dealloc]; +} + +@synthesize window = window; + +@synthesize context = context; + +- (void)applicationDidFinishLaunching:(NSNotification *)aNotification +{ + int status; + mfContext *mfc; + _singleDelegate = self; + [self CreateContext]; + status = [self ParseCommandLineArguments]; + mfc = (mfContext *)context; + mfc->view = (void *)mrdpView; + + if (status == 0) + { + NSScreen *screen = [[NSScreen screens] objectAtIndex:0]; + NSRect screenFrame = [screen frame]; + + if (context->instance->settings->Fullscreen) + { + context->instance->settings->DesktopWidth = screenFrame.size.width; + context->instance->settings->DesktopHeight = screenFrame.size.height; + } + + PubSub_SubscribeConnectionResult(context->pubSub, AppDelegate_ConnectionResultEventHandler); + PubSub_SubscribeErrorInfo(context->pubSub, AppDelegate_ErrorInfoEventHandler); + PubSub_SubscribeEmbedWindow(context->pubSub, AppDelegate_EmbedWindowEventHandler); + PubSub_SubscribeResizeWindow(context->pubSub, AppDelegate_ResizeWindowEventHandler); + freerdp_client_start(context); + NSString *winTitle; + + if (mfc->context.settings->WindowTitle && mfc->context.settings->WindowTitle[0]) + { + winTitle = [[NSString alloc] initWithCString:mfc->context.settings->WindowTitle]; + } + else + { + winTitle = [[NSString alloc] + initWithFormat:@"%@:%u", + [NSString stringWithCString:mfc->context.settings->ServerHostname + encoding:NSUTF8StringEncoding], + mfc -> context.settings->ServerPort]; + } + + [window setTitle:winTitle]; + } + else + { + [NSApp terminate:self]; + } +} + +- (void)applicationWillBecomeActive:(NSNotification *)notification +{ + [mrdpView resume]; +} + +- (void)applicationWillResignActive:(NSNotification *)notification +{ + [mrdpView pause]; +} + +- (void)applicationWillTerminate:(NSNotification *)notification +{ + NSLog(@"Stopping...\n"); + freerdp_client_stop(context); + [mrdpView releaseResources]; + _singleDelegate = nil; + NSLog(@"Stopped.\n"); + [NSApp terminate:self]; +} + +- (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender +{ + return YES; +} + +- (int)ParseCommandLineArguments +{ + int i; + int length; + int status; + char *cptr; + NSArray *args = [[NSProcessInfo processInfo] arguments]; + context->argc = (int)[args count]; + context->argv = malloc(sizeof(char *) * context->argc); + i = 0; + + for (NSString *str in args) + { + /* filter out some arguments added by XCode */ + if ([str isEqualToString:@"YES"]) + continue; + + if ([str isEqualToString:@"-NSDocumentRevisionsDebugMode"]) + continue; + + length = (int)([str length] + 1); + cptr = (char *)malloc(length); + sprintf_s(cptr, length, "%s", [str UTF8String]); + context->argv[i++] = cptr; + } + + context->argc = i; + status = freerdp_client_settings_parse_command_line(context->settings, context->argc, + context->argv, FALSE); + freerdp_client_settings_command_line_status_print(context->settings, status, context->argc, + context->argv); + return status; +} + +- (void)CreateContext +{ + RDP_CLIENT_ENTRY_POINTS clientEntryPoints; + ZeroMemory(&clientEntryPoints, sizeof(RDP_CLIENT_ENTRY_POINTS)); + clientEntryPoints.Size = sizeof(RDP_CLIENT_ENTRY_POINTS); + clientEntryPoints.Version = RDP_CLIENT_INTERFACE_VERSION; + RdpClientEntry(&clientEntryPoints); + context = freerdp_client_context_new(&clientEntryPoints); +} + +- (void)ReleaseContext +{ + mfContext *mfc; + MRDPView *view; + mfc = (mfContext *)context; + view = (MRDPView *)mfc->view; + [view exitFullScreenModeWithOptions:nil]; + [view releaseResources]; + [view release]; + mfc->view = nil; + freerdp_client_context_free(context); + context = nil; +} + +/** ********************************************************************* + * called when we fail to connect to a RDP server - Make sure this is called from the main thread. + ***********************************************************************/ + +- (void)rdpConnectError:(NSString *)withMessage +{ + mfContext *mfc; + MRDPView *view; + mfc = (mfContext *)context; + view = (MRDPView *)mfc->view; + [view exitFullScreenModeWithOptions:nil]; + NSString *message = withMessage ? withMessage : @"Error connecting to server"; + NSAlert *alert = [[NSAlert alloc] init]; + [alert setMessageText:message]; + [alert beginSheetModalForWindow:[self window] + modalDelegate:self + didEndSelector:@selector(alertDidEnd:returnCode:contextInfo:) + contextInfo:nil]; +} + +/** ********************************************************************* + * just a terminate selector for above call + ***********************************************************************/ + +- (void)alertDidEnd:(NSAlert *)a returnCode:(NSInteger)rc contextInfo:(void *)ci +{ + [NSApp terminate:nil]; +} + +@end + +/** ********************************************************************* + * On connection error, display message and quit application + ***********************************************************************/ + +void AppDelegate_ConnectionResultEventHandler(void *ctx, ConnectionResultEventArgs *e) +{ + rdpContext *context = (rdpContext *)ctx; + NSLog(@"ConnectionResult event result:%d\n", e->result); + + if (_singleDelegate) + { + if (e->result != 0) + { + NSString *message = nil; + DWORD code = freerdp_get_last_error(context); + switch (code) + { + case FREERDP_ERROR_AUTHENTICATION_FAILED: + message = [NSString + stringWithFormat:@"%@", @"Authentication failure, check credentials."]; + break; + default: + break; + } + + // Making sure this should be invoked on the main UI thread. + [_singleDelegate performSelectorOnMainThread:@selector(rdpConnectError:) + withObject:message + waitUntilDone:FALSE]; + } + } +} + +void AppDelegate_ErrorInfoEventHandler(void *ctx, ErrorInfoEventArgs *e) +{ + NSLog(@"ErrorInfo event code:%d\n", e->code); + + if (_singleDelegate) + { + // Retrieve error message associated with error code + NSString *message = nil; + + if (e->code != ERRINFO_NONE) + { + const char *errorMessage = freerdp_get_error_info_string(e->code); + message = [[NSString alloc] initWithUTF8String:errorMessage]; + } + + // Making sure this should be invoked on the main UI thread. + [_singleDelegate performSelectorOnMainThread:@selector(rdpConnectError:) + withObject:message + waitUntilDone:TRUE]; + [message release]; + } +} + +void AppDelegate_EmbedWindowEventHandler(void *ctx, EmbedWindowEventArgs *e) +{ + rdpContext *context = (rdpContext *)ctx; + + if (_singleDelegate) + { + mfContext *mfc = (mfContext *)context; + _singleDelegate->mrdpView = mfc->view; + + if (_singleDelegate->window) + { + [[_singleDelegate->window contentView] addSubview:mfc->view]; + } + + dispatch_async(dispatch_get_main_queue(), ^{ + mac_set_view_size(context, mfc->view); + }); + } +} + +void AppDelegate_ResizeWindowEventHandler(void *ctx, ResizeWindowEventArgs *e) +{ + rdpContext *context = (rdpContext *)ctx; + fprintf(stderr, "ResizeWindowEventHandler: %d %d\n", e->width, e->height); + + if (_singleDelegate) + { + mfContext *mfc = (mfContext *)context; + dispatch_async(dispatch_get_main_queue(), ^{ + mac_set_view_size(context, mfc->view); + }); + } +} + +void mac_set_view_size(rdpContext *context, MRDPView *view) +{ + // set client area to specified dimensions + NSRect innerRect; + innerRect.origin.x = 0; + innerRect.origin.y = 0; + innerRect.size.width = context->settings->DesktopWidth; + innerRect.size.height = context->settings->DesktopHeight; + [view setFrame:innerRect]; + // calculate window of same size, but keep position + NSRect outerRect = [[view window] frame]; + outerRect.size = [[view window] frameRectForContentRect:innerRect].size; + // we are not in RemoteApp mode, disable larger than resolution + [[view window] setContentMaxSize:innerRect.size]; + // set window to given area + [[view window] setFrame:outerRect display:YES]; + // set window to front + [NSApp activateIgnoringOtherApps:YES]; + + if (context->settings->Fullscreen) + [[view window] toggleFullScreen:nil]; +} diff --git a/client/Mac/cli/CMakeLists.txt b/client/Mac/cli/CMakeLists.txt new file mode 100644 index 0000000..b481ab5 --- /dev/null +++ b/client/Mac/cli/CMakeLists.txt @@ -0,0 +1,114 @@ + +project(MacFreeRDP) + +set(MODULE_NAME "MacFreeRDP") +set(MODULE_OUTPUT_NAME "MacFreeRDP") +set(MODULE_PREFIX "FREERDP_CLIENT_MAC_CLIENT") + +# Import libraries +find_library(FOUNDATION_LIBRARY Foundation) +find_library(COCOA_LIBRARY Cocoa) +find_library(APPKIT_LIBRARY AppKit) + +string(TIMESTAMP VERSION_YEAR "%Y") +set(MACOSX_BUNDLE_INFO_STRING "MacFreeRDP") +set(MACOSX_BUNDLE_ICON_FILE "FreeRDP.icns") +set(MACOSX_BUNDLE_GUI_IDENTIFIER "com.freerdp.mac") +set(MACOSX_BUNDLE_BUNDLE_IDENTIFIER "FreeRDP-client.Mac") +set(MACOSX_BUNDLE_LONG_VERSION_STRING "MacFreeRDP Client Version ${FREERDP_VERSION}") +set(MACOSX_BUNDLE_BUNDLE_NAME "MacFreeRDP") +set(MACOSX_BUNDLE_SHORT_VERSION_STRING ${FREERDP_VERSION}) +set(MACOSX_BUNDLE_BUNDLE_VERSION ${FREERDP_VERSION}) +set(MACOSX_BUNDLE_COPYRIGHT "Copyright 2013-${VERSION_YEAR}. All Rights Reserved.") + +set(MACOSX_BUNDLE_NSMAIN_NIB_FILE "MainMenu") +set(MACOSX_BUNDLE_NSPRINCIPAL_CLASS "NSApplication") + +mark_as_advanced(COCOA_LIBRARY FOUNDATION_LIBRARY APPKIT_LIBRARY) +set(APP_TYPE MACOSX_BUNDLE) + +set(${MODULE_PREFIX}_XIBS MainMenu.xib) + +set(${MODULE_PREFIX}_SOURCES "") + +set(${MODULE_PREFIX}_OBJECTIVE_SOURCES + main.m + AppDelegate.m) + +list(APPEND ${MODULE_PREFIX}_SOURCES ${${MODULE_PREFIX}_OBJECTIVE_SOURCES}) + +set(${MODULE_PREFIX}_HEADERS + AppDelegate.h) + +set(${MODULE_PREFIX}_RESOURCES ${MACOSX_BUNDLE_ICON_FILE}) + +# Include XIB file in Xcode resources. +if("${CMAKE_GENERATOR}" MATCHES "Xcode") + message(STATUS "Adding Xcode XIB resources for ${MODULE_NAME}") + set(${MODULE_PREFIX}_RESOURCES ${${MODULE_PREFIX}_RESOURCES} ${${MODULE_PREFIX}_XIBS}) +endif() + +add_executable(${MODULE_NAME} + ${APP_TYPE} + ${${MODULE_PREFIX}_HEADERS} + ${${MODULE_PREFIX}_SOURCES} + ${${MODULE_PREFIX}_RESOURCES}) + +set_target_properties(${MODULE_NAME} PROPERTIES OUTPUT_NAME "${MODULE_OUTPUT_NAME}") + +# This is necessary for the xib file part below +configure_file(${CMAKE_CURRENT_SOURCE_DIR}/Info.plist ${CMAKE_CURRENT_BINARY_DIR}/Info.plist) + +# This allows for automatic xib to nib ibitool +set_target_properties(${MODULE_NAME} PROPERTIES RESOURCE "${${MODULE_PREFIX}_RESOURCES}") + +# Tell the compiler where to look for the FreeRDP framework +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -F../") +set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -F../") + +# Tell XCode where to look for the MacFreeRDP framework +set_target_properties(${MODULE_NAME} PROPERTIES XCODE_ATTRIBUTE_FRAMEWORK_SEARCH_PATHS + "${XCODE_ATTRIBUTE_FRAMEWORK_SEARCH_PATHS} ${CMAKE_CURRENT_BINARY_DIR}/../$(CONFIGURATION)") + +# Set the info plist to the custom instance +set_target_properties(${MODULE_NAME} PROPERTIES MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_BINARY_DIR}/Info.plist) + +# Disable transitive linking +target_link_libraries(${MODULE_NAME} ${COCOA_LIBRARY} ${FOUNDATION_LIBRARY} ${APPKIT_LIBRARY} MacFreeRDP-library) + +set_property(TARGET ${MODULE_NAME} PROPERTY FOLDER "Client/Mac") + +# Embed the FreeRDP framework into the app bundle +add_custom_command(TARGET ${MODULE_NAME} POST_BUILD + COMMAND mkdir ARGS -p ${CMAKE_CURRENT_BINARY_DIR}/$(CONFIGURATION)/${MODULE_OUTPUT_NAME}.app/Contents/Frameworks + COMMAND ditto ${CMAKE_CURRENT_BINARY_DIR}/../$(CONFIGURATION)/MacFreeRDP.framework ${CMAKE_CURRENT_BINARY_DIR}/$(CONFIGURATION)/${MODULE_OUTPUT_NAME}.app/Contents/Frameworks/MacFreeRDP.framework + COMMAND install_name_tool -change "@executable_path/../Frameworks/MacFreeRDP.framework/Versions/${MAC_OS_X_BUNDLE_BUNDLE_VERSION}/MacFreeRDP" + "@executable_path/../Frameworks/MacFreeRDP.framework/Versions/Current/MacFreeRDP" + "${CMAKE_CURRENT_BINARY_DIR}/$(CONFIGURATION)/${MODULE_OUTPUT_NAME}.app/Contents/MacOS/${MODULE_NAME}" + COMMENT Setting install name for MacFreeRDP) + +# Add post-build NIB file generation in unix makefiles. XCode handles this implicitly. +if("${CMAKE_GENERATOR}" MATCHES "Unix Makefiles") + message(STATUS "Adding post-build NIB file generation event for ${MODULE_NAME}") + + # Make sure we can find the 'ibtool' program. If we can NOT find it we skip generation of this project + find_program(IBTOOL ibtool HINTS "/usr/bin" "${OSX_DEVELOPER_ROOT}/usr/bin") + if (${IBTOOL} STREQUAL "IBTOOL-NOTFOUND") + message(SEND_ERROR "ibtool can not be found and is needed to compile the .xib files. It should have been installed with + the Apple developer tools. The default system paths were searched in addition to ${OSX_DEVELOPER_ROOT}/usr/bin") + endif() + + # Make sure the 'Resources' Directory is correctly created before we build + add_custom_command(TARGET ${MODULE_NAME} PRE_BUILD COMMAND mkdir -p ${CMAKE_CURRENT_BINARY_DIR}/\${CONFIGURATION}/${MODULE_OUTPUT_NAME}.app/Contents/Resources) + + # Compile the .xib files using the 'ibtool' program with the destination being the app package + foreach(xib ${${MODULE_PREFIX}_XIBS}) + get_filename_component(XIB_WE ${xib} NAME_WE) + + add_custom_command (TARGET ${MODULE_NAME} POST_BUILD + COMMAND ${IBTOOL} --errors --warnings --notices --output-format human-readable-text + --compile ${CMAKE_CURRENT_BINARY_DIR}/\${CONFIGURATION}/${MODULE_OUTPUT_NAME}.app/Contents/Resources/${XIB_WE}.nib ${CMAKE_CURRENT_SOURCE_DIR}/${xib} + COMMENT "Compiling ${xib}") + endforeach() + +endif() diff --git a/client/Mac/cli/FreeRDP.icns b/client/Mac/cli/FreeRDP.icns new file mode 100644 index 0000000000000000000000000000000000000000..88bd44ca05621268824d56bc4e14371c27402c52 GIT binary patch literal 60681 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 + + + + NSCameraUsageDescription + This application requires camera access to redirect it to the remote host + NSMicrophoneUsageDescription + This application requires microphone access to redirect it to the remote host + CFBundleDevelopmentRegion + en + CFBundleExecutable + + CFBundleIconFile + FreeRDP + CFBundleIdentifier + FreeRDP.Mac + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSMinimumSystemVersion + + NSHumanReadableCopyright + Copyright © 2012 __MyCompanyName__. All rights reserved. + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/client/Mac/cli/MacClient2-Info.plist b/client/Mac/cli/MacClient2-Info.plist new file mode 100644 index 0000000..6efd7bd --- /dev/null +++ b/client/Mac/cli/MacClient2-Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIconFile + + CFBundleIdentifier + awakecoding.${PRODUCT_NAME:rfc1034identifier} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${PRODUCT_NAME} + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSMinimumSystemVersion + ${MACOSX_DEPLOYMENT_TARGET} + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/client/Mac/cli/MacClient2-Prefix.pch b/client/Mac/cli/MacClient2-Prefix.pch new file mode 100644 index 0000000..f81d505 --- /dev/null +++ b/client/Mac/cli/MacClient2-Prefix.pch @@ -0,0 +1,7 @@ +// +// Prefix header for all source files of the 'MacClient2' target in the 'MacClient2' project +// + +#ifdef __OBJC__ + #import +#endif diff --git a/client/Mac/cli/MainMenu.xib b/client/Mac/cli/MainMenu.xib new file mode 100644 index 0000000..f647699 --- /dev/null +++ b/client/Mac/cli/MainMenu.xib @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/client/Mac/cli/en.lproj/Credits.rtf b/client/Mac/cli/en.lproj/Credits.rtf new file mode 100644 index 0000000..46576ef --- /dev/null +++ b/client/Mac/cli/en.lproj/Credits.rtf @@ -0,0 +1,29 @@ +{\rtf0\ansi{\fonttbl\f0\fswiss Helvetica;} +{\colortbl;\red255\green255\blue255;} +\paperw9840\paperh8400 +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\ql\qnatural + +\f0\b\fs24 \cf0 Engineering: +\b0 \ + Some people\ +\ + +\b Human Interface Design: +\b0 \ + Some other people\ +\ + +\b Testing: +\b0 \ + Hopefully not nobody\ +\ + +\b Documentation: +\b0 \ + Whoever\ +\ + +\b With special thanks to: +\b0 \ + Mom\ +} diff --git a/client/Mac/cli/en.lproj/InfoPlist.strings b/client/Mac/cli/en.lproj/InfoPlist.strings new file mode 100644 index 0000000..477b28f --- /dev/null +++ b/client/Mac/cli/en.lproj/InfoPlist.strings @@ -0,0 +1,2 @@ +/* Localized versions of Info.plist keys */ + diff --git a/client/Mac/cli/en.lproj/MainMenu.xib b/client/Mac/cli/en.lproj/MainMenu.xib new file mode 100644 index 0000000..dd4e190 --- /dev/null +++ b/client/Mac/cli/en.lproj/MainMenu.xib @@ -0,0 +1,3299 @@ + + + + 1080 + 12D78 + 3084 + 1187.37 + 626.00 + + com.apple.InterfaceBuilder.CocoaPlugin + 3084 + + + IBNSLayoutConstraint + NSCustomObject + NSCustomView + NSMenu + NSMenuItem + NSView + NSWindowTemplate + + + com.apple.InterfaceBuilder.CocoaPlugin + + + PluginDependencyRecalculationVersion + + + + + NSApplication + + + FirstResponder + + + NSApplication + + + AMainMenu + + + + MacClient2 + + 1048576 + 2147483647 + + NSImage + NSMenuCheckmark + + + NSImage + NSMenuMixedState + + submenuAction: + + MacClient2 + + + + About MacClient2 + + 2147483647 + + + + + + YES + YES + + + 1048576 + 2147483647 + + + + + + Preferences… + , + 1048576 + 2147483647 + + + + + + YES + YES + + + 1048576 + 2147483647 + + + + + + Services + + 1048576 + 2147483647 + + + submenuAction: + + Services + + _NSServicesMenu + + + + + YES + YES + + + 1048576 + 2147483647 + + + + + + Hide MacClient2 + h + 1048576 + 2147483647 + + + + + + Hide Others + h + 1572864 + 2147483647 + + + + + + Show All + + 1048576 + 2147483647 + + + + + + YES + YES + + + 1048576 + 2147483647 + + + + + + Quit MacClient2 + q + 1048576 + 2147483647 + + + + + _NSAppleMenu + + + + + File + + 1048576 + 2147483647 + + + submenuAction: + + File + + + + New + n + 1048576 + 2147483647 + + + + + + Open… + o + 1048576 + 2147483647 + + + + + + Open Recent + + 1048576 + 2147483647 + + + submenuAction: + + Open Recent + + + + Clear Menu + + 1048576 + 2147483647 + + + + + _NSRecentDocumentsMenu + + + + + YES + YES + + + 1048576 + 2147483647 + + + + + + Close + w + 1048576 + 2147483647 + + + + + + Save… + s + 1048576 + 2147483647 + + + + + + Revert to Saved + + 2147483647 + + + + + + YES + YES + + + 1048576 + 2147483647 + + + + + + Page Setup... + P + 1179648 + 2147483647 + + + + + + + Print… + p + 1048576 + 2147483647 + + + + + + + + + Edit + + 1048576 + 2147483647 + + + submenuAction: + + Edit + + + + Undo + z + 1048576 + 2147483647 + + + + + + Redo + Z + 1179648 + 2147483647 + + + + + + YES + YES + + + 1048576 + 2147483647 + + + + + + Cut + x + 1048576 + 2147483647 + + + + + + Copy + c + 1048576 + 2147483647 + + + + + + Paste + v + 1048576 + 2147483647 + + + + + + Paste and Match Style + V + 1572864 + 2147483647 + + + + + + Delete + + 1048576 + 2147483647 + + + + + + Select All + a + 1048576 + 2147483647 + + + + + + YES + YES + + + 1048576 + 2147483647 + + + + + + Find + + 1048576 + 2147483647 + + + submenuAction: + + Find + + + + Find… + f + 1048576 + 2147483647 + + + 1 + + + + Find and Replace… + f + 1572864 + 2147483647 + + + 12 + + + + Find Next + g + 1048576 + 2147483647 + + + 2 + + + + Find Previous + G + 1179648 + 2147483647 + + + 3 + + + + Use Selection for Find + e + 1048576 + 2147483647 + + + 7 + + + + Jump to Selection + j + 1048576 + 2147483647 + + + + + + + + + Spelling and Grammar + + 1048576 + 2147483647 + + + submenuAction: + + Spelling and Grammar + + + + Show Spelling and Grammar + : + 1048576 + 2147483647 + + + + + + Check Document Now + ; + 1048576 + 2147483647 + + + + + + YES + YES + + + 2147483647 + + + + + + Check Spelling While Typing + + 1048576 + 2147483647 + + + + + + Check Grammar With Spelling + + 1048576 + 2147483647 + + + + + + Correct Spelling Automatically + + 2147483647 + + + + + + + + + Substitutions + + 1048576 + 2147483647 + + + submenuAction: + + Substitutions + + + + Show Substitutions + + 2147483647 + + + + + + YES + YES + + + 2147483647 + + + + + + Smart Copy/Paste + f + 1048576 + 2147483647 + + + 1 + + + + Smart Quotes + g + 1048576 + 2147483647 + + + 2 + + + + Smart Dashes + + 2147483647 + + + + + + Smart Links + G + 1179648 + 2147483647 + + + 3 + + + + Text Replacement + + 2147483647 + + + + + + + + + Transformations + + 2147483647 + + + submenuAction: + + Transformations + + + + Make Upper Case + + 2147483647 + + + + + + Make Lower Case + + 2147483647 + + + + + + Capitalize + + 2147483647 + + + + + + + + + Speech + + 1048576 + 2147483647 + + + submenuAction: + + Speech + + + + Start Speaking + + 1048576 + 2147483647 + + + + + + Stop Speaking + + 1048576 + 2147483647 + + + + + + + + + + + + Format + + 2147483647 + + + submenuAction: + + Format + + + + Font + + 2147483647 + + + submenuAction: + + Font + + + + Show Fonts + t + 1048576 + 2147483647 + + + + + + Bold + b + 1048576 + 2147483647 + + + 2 + + + + Italic + i + 1048576 + 2147483647 + + + 1 + + + + Underline + u + 1048576 + 2147483647 + + + + + + YES + YES + + + 2147483647 + + + + + + Bigger + + + 1048576 + 2147483647 + + + 3 + + + + Smaller + - + 1048576 + 2147483647 + + + 4 + + + + YES + YES + + + 2147483647 + + + + + + Kern + + 2147483647 + + + submenuAction: + + Kern + + + + Use Default + + 2147483647 + + + + + + Use None + + 2147483647 + + + + + + Tighten + + 2147483647 + + + + + + Loosen + + 2147483647 + + + + + + + + + Ligatures + + 2147483647 + + + submenuAction: + + Ligatures + + + + Use Default + + 2147483647 + + + + + + Use None + + 2147483647 + + + + + + Use All + + 2147483647 + + + + + + + + + Baseline + + 2147483647 + + + submenuAction: + + Baseline + + + + Use Default + + 2147483647 + + + + + + Superscript + + 2147483647 + + + + + + Subscript + + 2147483647 + + + + + + Raise + + 2147483647 + + + + + + Lower + + 2147483647 + + + + + + + + + YES + YES + + + 2147483647 + + + + + + Show Colors + C + 1048576 + 2147483647 + + + + + + YES + YES + + + 2147483647 + + + + + + Copy Style + c + 1572864 + 2147483647 + + + + + + Paste Style + v + 1572864 + 2147483647 + + + + + _NSFontMenu + + + + + Text + + 2147483647 + + + submenuAction: + + Text + + + + Align Left + { + 1048576 + 2147483647 + + + + + + Center + | + 1048576 + 2147483647 + + + + + + Justify + + 2147483647 + + + + + + Align Right + } + 1048576 + 2147483647 + + + + + + YES + YES + + + 2147483647 + + + + + + Writing Direction + + 2147483647 + + + submenuAction: + + Writing Direction + + + + YES + Paragraph + + 2147483647 + + + + + + CURlZmF1bHQ + + 2147483647 + + + + + + CUxlZnQgdG8gUmlnaHQ + + 2147483647 + + + + + + CVJpZ2h0IHRvIExlZnQ + + 2147483647 + + + + + + YES + YES + + + 2147483647 + + + + + + YES + Selection + + 2147483647 + + + + + + CURlZmF1bHQ + + 2147483647 + + + + + + CUxlZnQgdG8gUmlnaHQ + + 2147483647 + + + + + + CVJpZ2h0IHRvIExlZnQ + + 2147483647 + + + + + + + + + YES + YES + + + 2147483647 + + + + + + Show Ruler + + 2147483647 + + + + + + Copy Ruler + c + 1310720 + 2147483647 + + + + + + Paste Ruler + v + 1310720 + 2147483647 + + + + + + + + + + + + View + + 1048576 + 2147483647 + + + submenuAction: + + View + + + + Show Toolbar + t + 1572864 + 2147483647 + + + + + + Customize Toolbar… + + 1048576 + 2147483647 + + + + + + + + + Window + + 1048576 + 2147483647 + + + submenuAction: + + Window + + + + Minimize + m + 1048576 + 2147483647 + + + + + + Zoom + + 1048576 + 2147483647 + + + + + + YES + YES + + + 1048576 + 2147483647 + + + + + + Bring All to Front + + 1048576 + 2147483647 + + + + + _NSWindowsMenu + + + + + Help + + 2147483647 + + + submenuAction: + + Help + + + + MacClient2 Help + ? + 1048576 + 2147483647 + + + + + _NSHelpMenu + + + + _NSMainMenu + + + 15 + 2 + {{335, 390}, {480, 360}} + 1954021376 + MacClient2 + NSWindow + + + + + 256 + + + + 268 + {480, 360} + + _NS:9 + MRDPView + + + {480, 360} + + + + {{0, 0}, {1440, 878}} + {10000000000000, 10000000000000} + YES + + + AppDelegate + + + NSFontManager + + + + + + + terminate: + + + + 449 + + + + orderFrontStandardAboutPanel: + + + + 142 + + + + delegate + + + + 495 + + + + performMiniaturize: + + + + 37 + + + + arrangeInFront: + + + + 39 + + + + print: + + + + 86 + + + + runPageLayout: + + + + 87 + + + + clearRecentDocuments: + + + + 127 + + + + performClose: + + + + 193 + + + + toggleContinuousSpellChecking: + + + + 222 + + + + undo: + + + + 223 + + + + copy: + + + + 224 + + + + checkSpelling: + + + + 225 + + + + paste: + + + + 226 + + + + stopSpeaking: + + + + 227 + + + + cut: + + + + 228 + + + + showGuessPanel: + + + + 230 + + + + redo: + + + + 231 + + + + selectAll: + + + + 232 + + + + startSpeaking: + + + + 233 + + + + delete: + + + + 235 + + + + performZoom: + + + + 240 + + + + performFindPanelAction: + + + + 241 + + + + centerSelectionInVisibleArea: + + + + 245 + + + + toggleGrammarChecking: + + + + 347 + + + + toggleSmartInsertDelete: + + + + 355 + + + + toggleAutomaticQuoteSubstitution: + + + + 356 + + + + toggleAutomaticLinkDetection: + + + + 357 + + + + saveDocument: + + + + 362 + + + + revertDocumentToSaved: + + + + 364 + + + + runToolbarCustomizationPalette: + + + + 365 + + + + toggleToolbarShown: + + + + 366 + + + + hide: + + + + 367 + + + + hideOtherApplications: + + + + 368 + + + + unhideAllApplications: + + + + 370 + + + + newDocument: + + + + 373 + + + + openDocument: + + + + 374 + + + + raiseBaseline: + + + + 426 + + + + lowerBaseline: + + + + 427 + + + + copyFont: + + + + 428 + + + + subscript: + + + + 429 + + + + superscript: + + + + 430 + + + + tightenKerning: + + + + 431 + + + + underline: + + + + 432 + + + + orderFrontColorPanel: + + + + 433 + + + + useAllLigatures: + + + + 434 + + + + loosenKerning: + + + + 435 + + + + pasteFont: + + + + 436 + + + + unscript: + + + + 437 + + + + useStandardKerning: + + + + 438 + + + + useStandardLigatures: + + + + 439 + + + + turnOffLigatures: + + + + 440 + + + + turnOffKerning: + + + + 441 + + + + toggleAutomaticSpellingCorrection: + + + + 456 + + + + orderFrontSubstitutionsPanel: + + + + 458 + + + + toggleAutomaticDashSubstitution: + + + + 461 + + + + toggleAutomaticTextReplacement: + + + + 463 + + + + uppercaseWord: + + + + 464 + + + + capitalizeWord: + + + + 467 + + + + lowercaseWord: + + + + 468 + + + + pasteAsPlainText: + + + + 486 + + + + performFindPanelAction: + + + + 487 + + + + performFindPanelAction: + + + + 488 + + + + performFindPanelAction: + + + + 489 + + + + showHelp: + + + + 493 + + + + alignCenter: + + + + 518 + + + + pasteRuler: + + + + 519 + + + + toggleRuler: + + + + 520 + + + + alignRight: + + + + 521 + + + + copyRuler: + + + + 522 + + + + alignJustified: + + + + 523 + + + + alignLeft: + + + + 524 + + + + makeBaseWritingDirectionNatural: + + + + 525 + + + + makeBaseWritingDirectionLeftToRight: + + + + 526 + + + + makeBaseWritingDirectionRightToLeft: + + + + 527 + + + + makeTextWritingDirectionNatural: + + + + 528 + + + + makeTextWritingDirectionLeftToRight: + + + + 529 + + + + makeTextWritingDirectionRightToLeft: + + + + 530 + + + + performFindPanelAction: + + + + 535 + + + + addFontTrait: + + + + 421 + + + + addFontTrait: + + + + 422 + + + + modifyFont: + + + + 423 + + + + orderFrontFontPanel: + + + + 424 + + + + modifyFont: + + + + 425 + + + + mrdpView + + + + 549 + + + + window + + + + 550 + + + + + + 0 + + + + + + -2 + + + File's Owner + + + -1 + + + First Responder + + + -3 + + + Application + + + 29 + + + + + + + + + + + + + + 19 + + + + + + + + 56 + + + + + + + + 217 + + + + + + + + 83 + + + + + + + + 81 + + + + + + + + + + + + + + + + + 75 + + + + + 78 + + + + + 72 + + + + + 82 + + + + + 124 + + + + + + + + 77 + + + + + 73 + + + + + 79 + + + + + 112 + + + + + 74 + + + + + 125 + + + + + + + + 126 + + + + + 205 + + + + + + + + + + + + + + + + + + + + + + 202 + + + + + 198 + + + + + 207 + + + + + 214 + + + + + 199 + + + + + 203 + + + + + 197 + + + + + 206 + + + + + 215 + + + + + 218 + + + + + + + + 216 + + + + + + + + 200 + + + + + + + + + + + + + 219 + + + + + 201 + + + + + 204 + + + + + 220 + + + + + + + + + + + + + 213 + + + + + 210 + + + + + 221 + + + + + 208 + + + + + 209 + + + + + 57 + + + + + + + + + + + + + + + + + + 58 + + + + + 134 + + + + + 150 + + + + + 136 + + + + + 144 + + + + + 129 + + + + + 143 + + + + + 236 + + + + + 131 + + + + + + + + 149 + + + + + 145 + + + + + 130 + + + + + 24 + + + + + + + + + + + 92 + + + + + 5 + + + + + 239 + + + + + 23 + + + + + 295 + + + + + + + + 296 + + + + + + + + + 297 + + + + + 298 + + + + + 211 + + + + + + + + 212 + + + + + + + + + 195 + + + + + 196 + + + + + 346 + + + + + 348 + + + + + + + + 349 + + + + + + + + + + + + + + 350 + + + + + 351 + + + + + 354 + + + + + 371 + + + + + + + + 372 + + + + + 6 + 0 + + 6 + 1 + + 0.0 + + 1000 + + 8 + 29 + 3 + + + + 4 + 0 + + 4 + 1 + + 0.0 + + 1000 + + 8 + 29 + 3 + + + + 5 + 0 + + 5 + 1 + + 0.0 + + 1000 + + 8 + 29 + 3 + + + + 3 + 0 + + 3 + 1 + + 0.0 + + 1000 + + 8 + 29 + 3 + + + + + + + 375 + + + + + + + + 376 + + + + + + + + + 377 + + + + + + + + 388 + + + + + + + + + + + + + + + + + + + + + + + 389 + + + + + 390 + + + + + 391 + + + + + 392 + + + + + 393 + + + + + 394 + + + + + 395 + + + + + 396 + + + + + 397 + + + + + + + + 398 + + + + + + + + 399 + + + + + + + + 400 + + + + + 401 + + + + + 402 + + + + + 403 + + + + + 404 + + + + + 405 + + + + + + + + + + + + 406 + + + + + 407 + + + + + 408 + + + + + 409 + + + + + 410 + + + + + 411 + + + + + + + + + + 412 + + + + + 413 + + + + + 414 + + + + + 415 + + + + + + + + + + + 416 + + + + + 417 + + + + + 418 + + + + + 419 + + + + + 420 + + + + + 450 + + + + + + + + 451 + + + + + + + + + + 452 + + + + + 453 + + + + + 454 + + + + + 457 + + + + + 459 + + + + + 460 + + + + + 462 + + + + + 465 + + + + + 466 + + + + + 485 + + + + + 490 + + + + + + + + 491 + + + + + + + + 492 + + + + + 494 + + + + + 496 + + + + + + + + 497 + + + + + + + + + + + + + + + + + 498 + + + + + 499 + + + + + 500 + + + + + 501 + + + + + 502 + + + + + 503 + + + + + + + + 504 + + + + + 505 + + + + + 506 + + + + + 507 + + + + + 508 + + + + + + + + + + + + + + + + 509 + + + + + 510 + + + + + 511 + + + + + 512 + + + + + 513 + + + + + 514 + + + + + 515 + + + + + 516 + + + + + 517 + + + + + 534 + + + + + 536 + + + + + 542 + + + + + 544 + + + + + 545 + + + + + 546 + + + + + + + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + {{380, 496}, {480, 360}} + + + + + + + + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + + + + + + 550 + + + 0 + IBCocoaFramework + YES + 3 + + {11, 11} + {10, 3} + + YES + + diff --git a/client/Mac/cli/main.m b/client/Mac/cli/main.m new file mode 100644 index 0000000..7e5e478 --- /dev/null +++ b/client/Mac/cli/main.m @@ -0,0 +1,14 @@ +// +// main.m +// MacClient2 +// +// Created by Benoît et Kathy on 2013-05-08. +// +// + +#import + +int main(int argc, const char *argv[]) +{ + return NSApplicationMain(argc, argv); +} diff --git a/client/Mac/en.lproj/InfoPlist.strings b/client/Mac/en.lproj/InfoPlist.strings new file mode 100644 index 0000000..477b28f --- /dev/null +++ b/client/Mac/en.lproj/InfoPlist.strings @@ -0,0 +1,2 @@ +/* Localized versions of Info.plist keys */ + diff --git a/client/Mac/main.m b/client/Mac/main.m new file mode 100644 index 0000000..0f1916e --- /dev/null +++ b/client/Mac/main.m @@ -0,0 +1,25 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * MacFreeRDP + * + * Copyright 2012 Thomas Goddard + * + * Licensed 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. + */ + +#import + +int main(int argc, const char *argv[]) +{ + return NSApplicationMain(argc, argv); +} diff --git a/client/Mac/mf_client.h b/client/Mac/mf_client.h new file mode 100644 index 0000000..c3b3c85 --- /dev/null +++ b/client/Mac/mf_client.h @@ -0,0 +1,49 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Windows Client + * + * Copyright 2009-2011 Jay Sorg + * Copyright 2010-2011 Vic Lee + * Copyright 2010-2011 Marc-Andre Moreau + * + * Licensed 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 FREERDP_CLIENT_MAC_CLIENT_H +#define FREERDP_CLIENT_MAC_CLIENT_H + +#include + +#ifdef __cplusplus +extern "C" +{ +#endif + + FREERDP_API void mf_press_mouse_button(void* context, rdpInput* intput, int button, int x, + int y, BOOL down); + FREERDP_API void mf_scale_mouse_event(void* context, rdpInput* input, UINT16 flags, UINT16 x, + UINT16 y); + FREERDP_API void mf_scale_mouse_event_ex(void* context, rdpInput* input, UINT16 flags, UINT16 x, + UINT16 y); + + /** + * Client Interface + */ + + FREERDP_API int RdpClientEntry(RDP_CLIENT_ENTRY_POINTS* pEntryPoints); + +#ifdef __cplusplus +} +#endif + +#endif /* FREERDP_CLIENT_MAC_CLIENT_H */ diff --git a/client/Mac/mf_client.m b/client/Mac/mf_client.m new file mode 100644 index 0000000..a46d3d1 --- /dev/null +++ b/client/Mac/mf_client.m @@ -0,0 +1,216 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * X11 Client Interface + * + * Copyright 2013 Marc-Andre Moreau + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "mfreerdp.h" +#include +#include +#include + +/** + * Client Interface + */ + +static BOOL mfreerdp_client_global_init(void) +{ + freerdp_handle_signals(); + return TRUE; +} + +static void mfreerdp_client_global_uninit(void) +{ +} + +static int mfreerdp_client_start(rdpContext *context) +{ + MRDPView *view; + mfContext *mfc = (mfContext *)context; + + if (mfc->view == NULL) + { + // view not specified beforehand. Create view dynamically + mfc->view = + [[MRDPView alloc] initWithFrame:NSMakeRect(0, 0, context->settings->DesktopWidth, + context->settings->DesktopHeight)]; + mfc->view_ownership = TRUE; + } + + view = (MRDPView *)mfc->view; + return [view rdpStart:context]; +} + +static int mfreerdp_client_stop(rdpContext *context) +{ + mfContext *mfc = (mfContext *)context; + + freerdp_abort_connect(context->instance); + if (mfc->thread) + { + SetEvent(mfc->stopEvent); + WaitForSingleObject(mfc->thread, INFINITE); + CloseHandle(mfc->thread); + mfc->thread = NULL; + } + + if (mfc->view_ownership) + { + MRDPView *view = (MRDPView *)mfc->view; + [view releaseResources]; + [view release]; + mfc->view = nil; + } + + return 0; +} + +static BOOL mfreerdp_client_new(freerdp *instance, rdpContext *context) +{ + mfContext *mfc; + rdpSettings *settings; + mfc = (mfContext *)instance->context; + mfc->stopEvent = CreateEvent(NULL, TRUE, FALSE, NULL); + context->instance->PreConnect = mac_pre_connect; + context->instance->PostConnect = mac_post_connect; + context->instance->PostDisconnect = mac_post_disconnect; + context->instance->Authenticate = mac_authenticate; + context->instance->GatewayAuthenticate = mac_gw_authenticate; + context->instance->VerifyCertificateEx = mac_verify_certificate_ex; + context->instance->VerifyChangedCertificateEx = mac_verify_changed_certificate_ex; + context->instance->LogonErrorInfo = mac_logon_error_info; + context->instance->settings = instance->settings; + settings = context->settings; + settings->AsyncInput = TRUE; + return TRUE; +} + +static void mfreerdp_client_free(freerdp *instance, rdpContext *context) +{ + mfContext *mfc; + + if (!instance || !context) + return; + + mfc = (mfContext *)instance->context; + CloseHandle(mfc->stopEvent); +} + +static void mf_scale_mouse_coordinates(mfContext *mfc, UINT16 *px, UINT16 *py) +{ + UINT16 x = *px; + UINT16 y = *py; + UINT32 ww = mfc->client_width; + UINT32 wh = mfc->client_height; + UINT32 dw = mfc->context.settings->DesktopWidth; + UINT32 dh = mfc->context.settings->DesktopHeight; + + if (!mfc->context.settings->SmartSizing || ((ww == dw) && (wh == dh))) + { + y = y + mfc->yCurrentScroll; + x = x + mfc->xCurrentScroll; + + y -= (dh - wh); + x -= (dw - ww); + } + else + { + y = y * dh / wh + mfc->yCurrentScroll; + x = x * dw / ww + mfc->xCurrentScroll; + } + + *px = x; + *py = y; +} + +void mf_scale_mouse_event(void *context, rdpInput *input, UINT16 flags, UINT16 x, UINT16 y) +{ + mfContext *mfc = (mfContext *)context; + MRDPView *view = (MRDPView *)mfc->view; + // Convert to windows coordinates + y = [view frame].size.height - y; + + if ((flags & (PTR_FLAGS_WHEEL | PTR_FLAGS_HWHEEL)) == 0) + mf_scale_mouse_coordinates(mfc, &x, &y); + freerdp_input_send_mouse_event(input, flags, x, y); +} + +void mf_scale_mouse_event_ex(void *context, rdpInput *input, UINT16 flags, UINT16 x, UINT16 y) +{ + mfContext *mfc = (mfContext *)context; + MRDPView *view = (MRDPView *)mfc->view; + // Convert to windows coordinates + y = [view frame].size.height - y; + + mf_scale_mouse_coordinates(mfc, &x, &y); + freerdp_input_send_extended_mouse_event(input, flags, x, y); +} + +void mf_press_mouse_button(void *context, rdpInput *input, int button, int x, int y, BOOL down) +{ + UINT16 flags = 0; + UINT16 xflags = 0; + + if (down) + { + flags |= PTR_FLAGS_DOWN; + xflags |= PTR_XFLAGS_DOWN; + } + + switch (button) + { + case 0: + mf_scale_mouse_event(context, input, flags | PTR_FLAGS_BUTTON1, x, y); + break; + + case 1: + mf_scale_mouse_event(context, input, flags | PTR_FLAGS_BUTTON2, x, y); + break; + + case 2: + mf_scale_mouse_event(context, input, flags | PTR_FLAGS_BUTTON3, x, y); + break; + + case 3: + mf_scale_mouse_event_ex(context, input, xflags | PTR_XFLAGS_BUTTON1, x, y); + break; + + case 4: + mf_scale_mouse_event_ex(context, input, xflags | PTR_XFLAGS_BUTTON2, x, y); + break; + + default: + break; + } +} + +int RdpClientEntry(RDP_CLIENT_ENTRY_POINTS *pEntryPoints) +{ + pEntryPoints->Version = 1; + pEntryPoints->Size = sizeof(RDP_CLIENT_ENTRY_POINTS_V1); + pEntryPoints->GlobalInit = mfreerdp_client_global_init; + pEntryPoints->GlobalUninit = mfreerdp_client_global_uninit; + pEntryPoints->ContextSize = sizeof(mfContext); + pEntryPoints->ClientNew = mfreerdp_client_new; + pEntryPoints->ClientFree = mfreerdp_client_free; + pEntryPoints->ClientStart = mfreerdp_client_start; + pEntryPoints->ClientStop = mfreerdp_client_stop; + return 0; +} diff --git a/client/Mac/mfreerdp.h b/client/Mac/mfreerdp.h new file mode 100644 index 0000000..be39031 --- /dev/null +++ b/client/Mac/mfreerdp.h @@ -0,0 +1,91 @@ +#ifndef FREERDP_CLIENT_MAC_FREERDP_H +#define FREERDP_CLIENT_MAC_FREERDP_H + +typedef struct mf_context mfContext; + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "MRDPView.h" +#include "Keyboard.h" +#include + +struct mf_context +{ + rdpContext context; + DEFINE_RDP_CLIENT_COMMON(); + + void* view; + BOOL view_ownership; + + int width; + int height; + int offset_x; + int offset_y; + int fs_toggle; + int fullscreen; + int percentscreen; + char window_title[64]; + int client_x; + int client_y; + int client_width; + int client_height; + + HANDLE stopEvent; + HANDLE keyboardThread; + enum APPLE_KEYBOARD_TYPE appleKeyboardType; + + DWORD mainThreadId; + DWORD keyboardThreadId; + + BOOL clipboardSync; + wClipboard* clipboard; + UINT32 numServerFormats; + UINT32 requestedFormatId; + HANDLE clipboardRequestEvent; + CLIPRDR_FORMAT* serverFormats; + CliprdrClientContext* cliprdr; + UINT32 clipboardCapabilities; + + rdpFile* connectionRdpFile; + + // Keep track of window size and position, disable when in fullscreen mode. + BOOL disablewindowtracking; + + // These variables are required for horizontal scrolling. + BOOL updating_scrollbars; + BOOL xScrollVisible; + int xMinScroll; // minimum horizontal scroll value + int xCurrentScroll; // current horizontal scroll value + int xMaxScroll; // maximum horizontal scroll value + + // These variables are required for vertical scrolling. + BOOL yScrollVisible; + int yMinScroll; // minimum vertical scroll value + int yCurrentScroll; // current vertical scroll value + int yMaxScroll; // maximum vertical scroll value + + CGEventFlags kbdFlags; +}; + +#endif /* FREERDP_CLIENT_MAC_FREERDP_H */ diff --git a/client/Sample/CMakeLists.txt b/client/Sample/CMakeLists.txt new file mode 100644 index 0000000..a8e9a6e --- /dev/null +++ b/client/Sample/CMakeLists.txt @@ -0,0 +1,51 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP Sample UI cmake build script +# +# Copyright 2012 Marc-Andre Moreau +# +# Licensed 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. + +set(MODULE_NAME "sfreerdp") +set(MODULE_PREFIX "FREERDP_CLIENT_SAMPLE") + +set(${MODULE_PREFIX}_SRCS + tf_channels.c + tf_channels.h + tf_freerdp.h + tf_freerdp.c) + +# On windows create dll version information. +# Vendor, product and year are already set in top level CMakeLists.txt +if (WIN32) + set (RC_VERSION_MAJOR ${FREERDP_VERSION_MAJOR}) + set (RC_VERSION_MINOR ${FREERDP_VERSION_MINOR}) + set (RC_VERSION_BUILD ${FREERDP_VERSION_REVISION}) + set (RC_VERSION_FILE "${MODULE_NAME}${CMAKE_EXECUTABLE_SUFFIX}" ) + + configure_file( + ${CMAKE_SOURCE_DIR}/cmake/WindowsDLLVersion.rc.in + ${CMAKE_CURRENT_BINARY_DIR}/version.rc + @ONLY) + + set (WINPR_SRCS ${WINPR_SRCS} ${CMAKE_CURRENT_BINARY_DIR}/version.rc) +endif() + + +add_executable(${MODULE_NAME} ${${MODULE_PREFIX}_SRCS}) + +set(${MODULE_PREFIX}_LIBS ${${MODULE_PREFIX}_LIBS} ${CMAKE_DL_LIBS}) +set(${MODULE_PREFIX}_LIBS ${${MODULE_PREFIX}_LIBS} freerdp-client freerdp) +target_link_libraries(${MODULE_NAME} ${${MODULE_PREFIX}_LIBS}) + +set_property(TARGET ${MODULE_NAME} PROPERTY FOLDER "Client/Sample") +install(TARGETS ${MODULE_NAME} DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT client) diff --git a/client/Sample/ModuleOptions.cmake b/client/Sample/ModuleOptions.cmake new file mode 100644 index 0000000..d4d5a9e --- /dev/null +++ b/client/Sample/ModuleOptions.cmake @@ -0,0 +1,4 @@ + +set(FREERDP_CLIENT_NAME "sfreerdp") +set(FREERDP_CLIENT_PLATFORM "Sample") +set(FREERDP_CLIENT_VENDOR "FreeRDP") diff --git a/client/Sample/tf_channels.c b/client/Sample/tf_channels.c new file mode 100644 index 0000000..7119a1c --- /dev/null +++ b/client/Sample/tf_channels.c @@ -0,0 +1,115 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Sample Client Channels + * + * Copyright 2018 Armin Novak + * Copyright 2018 Thincast Technologies GmbH + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include + +#include +#include +#include +#include +#include + +#include "tf_channels.h" +#include "tf_freerdp.h" + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT +tf_encomsp_participant_created(EncomspClientContext* context, + const ENCOMSP_PARTICIPANT_CREATED_PDU* participantCreated) +{ + WINPR_UNUSED(context); + WINPR_UNUSED(participantCreated); + return CHANNEL_RC_OK; +} + +static void tf_encomsp_init(tfContext* tf, EncomspClientContext* encomsp) +{ + tf->encomsp = encomsp; + encomsp->custom = (void*)tf; + encomsp->ParticipantCreated = tf_encomsp_participant_created; +} + +static void tf_encomsp_uninit(tfContext* tf, EncomspClientContext* encomsp) +{ + if (encomsp) + { + encomsp->custom = NULL; + encomsp->ParticipantCreated = NULL; + } + + if (tf) + tf->encomsp = NULL; +} + +void tf_OnChannelConnectedEventHandler(void* context, ChannelConnectedEventArgs* e) +{ + tfContext* tf = (tfContext*)context; + + if (strcmp(e->name, RDPEI_DVC_CHANNEL_NAME) == 0) + { + tf->rdpei = (RdpeiClientContext*)e->pInterface; + } + else if (strcmp(e->name, RDPGFX_DVC_CHANNEL_NAME) == 0) + { + gdi_graphics_pipeline_init(tf->context.gdi, (RdpgfxClientContext*)e->pInterface); + } + else if (strcmp(e->name, RAIL_SVC_CHANNEL_NAME) == 0) + { + } + else if (strcmp(e->name, CLIPRDR_SVC_CHANNEL_NAME) == 0) + { + } + else if (strcmp(e->name, ENCOMSP_SVC_CHANNEL_NAME) == 0) + { + tf_encomsp_init(tf, (EncomspClientContext*)e->pInterface); + } +} + +void tf_OnChannelDisconnectedEventHandler(void* context, ChannelDisconnectedEventArgs* e) +{ + tfContext* tf = (tfContext*)context; + + if (strcmp(e->name, RDPEI_DVC_CHANNEL_NAME) == 0) + { + tf->rdpei = NULL; + } + else if (strcmp(e->name, RDPGFX_DVC_CHANNEL_NAME) == 0) + { + gdi_graphics_pipeline_uninit(tf->context.gdi, (RdpgfxClientContext*)e->pInterface); + } + else if (strcmp(e->name, RAIL_SVC_CHANNEL_NAME) == 0) + { + } + else if (strcmp(e->name, CLIPRDR_SVC_CHANNEL_NAME) == 0) + { + } + else if (strcmp(e->name, ENCOMSP_SVC_CHANNEL_NAME) == 0) + { + tf_encomsp_uninit(tf, (EncomspClientContext*)e->pInterface); + } +} diff --git a/client/Sample/tf_channels.h b/client/Sample/tf_channels.h new file mode 100644 index 0000000..b1c0b86 --- /dev/null +++ b/client/Sample/tf_channels.h @@ -0,0 +1,33 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Sample Client Channels + * + * Copyright 2018 Armin Novak + * Copyright 2018 Thincast Technologies GmbH + * + * Licensed 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 FREERDP_CLIENT_SAMPLE_CHANNELS_H +#define FREERDP_CLIENT_SAMPLE_CHANNELS_H + +#include +#include + +int tf_on_channel_connected(freerdp* instance, const char* name, void* pInterface); +int tf_on_channel_disconnected(freerdp* instance, const char* name, void* pInterface); + +void tf_OnChannelConnectedEventHandler(void* context, ChannelConnectedEventArgs* e); +void tf_OnChannelDisconnectedEventHandler(void* context, ChannelDisconnectedEventArgs* e); + +#endif /* FREERDP_CLIENT_SAMPLE_CHANNELS_H */ diff --git a/client/Sample/tf_freerdp.c b/client/Sample/tf_freerdp.c new file mode 100644 index 0000000..e9b9fe8 --- /dev/null +++ b/client/Sample/tf_freerdp.c @@ -0,0 +1,359 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * FreeRDP Test UI + * + * Copyright 2011 Marc-Andre Moreau + * Copyright 2016,2018 Armin Novak + * Copyright 2016,2018 Thincast Technologies GmbH + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "tf_channels.h" +#include "tf_freerdp.h" + +#define TAG CLIENT_TAG("sample") + +/* This function is called whenever a new frame starts. + * It can be used to reset invalidated areas. */ +static BOOL tf_begin_paint(rdpContext* context) +{ + rdpGdi* gdi = context->gdi; + gdi->primary->hdc->hwnd->invalid->null = TRUE; + return TRUE; +} + +/* This function is called when the library completed composing a new + * frame. Read out the changed areas and blit them to your output device. + * The image buffer will have the format specified by gdi_init + */ +static BOOL tf_end_paint(rdpContext* context) +{ + rdpGdi* gdi = context->gdi; + + if (gdi->primary->hdc->hwnd->invalid->null) + return TRUE; + + return TRUE; +} + +/* This function is called to output a System BEEP */ +static BOOL tf_play_sound(rdpContext* context, const PLAY_SOUND_UPDATE* play_sound) +{ + /* TODO: Implement */ + WINPR_UNUSED(context); + WINPR_UNUSED(play_sound); + return TRUE; +} + +/* This function is called to update the keyboard indocator LED */ +static BOOL tf_keyboard_set_indicators(rdpContext* context, UINT16 led_flags) +{ + /* TODO: Set local keyboard indicator LED status */ + WINPR_UNUSED(context); + WINPR_UNUSED(led_flags); + return TRUE; +} + +/* This function is called to set the IME state */ +static BOOL tf_keyboard_set_ime_status(rdpContext* context, UINT16 imeId, UINT32 imeState, + UINT32 imeConvMode) +{ + if (!context) + return FALSE; + + WLog_WARN(TAG, + "KeyboardSetImeStatus(unitId=%04" PRIx16 ", imeState=%08" PRIx32 + ", imeConvMode=%08" PRIx32 ") ignored", + imeId, imeState, imeConvMode); + return TRUE; +} + +/* Called before a connection is established. + * Set all configuration options to support and load channels here. */ +static BOOL tf_pre_connect(freerdp* instance) +{ + rdpSettings* settings; + settings = instance->settings; + /* Optional OS identifier sent to server */ + settings->OsMajorType = OSMAJORTYPE_UNIX; + settings->OsMinorType = OSMINORTYPE_NATIVE_XSERVER; + /* settings->OrderSupport is initialized at this point. + * Only override it if you plan to implement custom order + * callbacks or deactiveate certain features. */ + /* Register the channel listeners. + * They are required to set up / tear down channels if they are loaded. */ + PubSub_SubscribeChannelConnected(instance->context->pubSub, tf_OnChannelConnectedEventHandler); + PubSub_SubscribeChannelDisconnected(instance->context->pubSub, + tf_OnChannelDisconnectedEventHandler); + + /* Load all required plugins / channels / libraries specified by current + * settings. */ + if (!freerdp_client_load_addins(instance->context->channels, instance->settings)) + return FALSE; + + /* TODO: Any code your client requires */ + return TRUE; +} + +/* Called after a RDP connection was successfully established. + * Settings might have changed during negociation of client / server feature + * support. + * + * Set up local framebuffers and paing callbacks. + * If required, register pointer callbacks to change the local mouse cursor + * when hovering over the RDP window + */ +static BOOL tf_post_connect(freerdp* instance) +{ + if (!gdi_init(instance, PIXEL_FORMAT_XRGB32)) + return FALSE; + + instance->update->BeginPaint = tf_begin_paint; + instance->update->EndPaint = tf_end_paint; + instance->update->PlaySound = tf_play_sound; + instance->update->SetKeyboardIndicators = tf_keyboard_set_indicators; + instance->update->SetKeyboardImeStatus = tf_keyboard_set_ime_status; + return TRUE; +} + +/* This function is called whether a session ends by failure or success. + * Clean up everything allocated by pre_connect and post_connect. + */ +static void tf_post_disconnect(freerdp* instance) +{ + tfContext* context; + + if (!instance) + return; + + if (!instance->context) + return; + + context = (tfContext*)instance->context; + PubSub_UnsubscribeChannelConnected(instance->context->pubSub, + tf_OnChannelConnectedEventHandler); + PubSub_UnsubscribeChannelDisconnected(instance->context->pubSub, + tf_OnChannelDisconnectedEventHandler); + gdi_free(instance); + /* TODO : Clean up custom stuff */ + WINPR_UNUSED(context); +} + +/* RDP main loop. + * Connects RDP, loops while running and handles event and dispatch, cleans up + * after the connection ends. */ +static DWORD WINAPI tf_client_thread_proc(LPVOID arg) +{ + freerdp* instance = (freerdp*)arg; + DWORD nCount; + DWORD status; + DWORD result = 0; + HANDLE handles[64]; + BOOL rc = freerdp_connect(instance); + + if (instance->settings->AuthenticationOnly) + { + result = freerdp_get_last_error(instance->context); + freerdp_abort_connect(instance); + WLog_ERR(TAG, "Authentication only, exit status 0x%08" PRIx32 "", result); + goto disconnect; + } + + if (!rc) + { + result = freerdp_get_last_error(instance->context); + WLog_ERR(TAG, "connection failure 0x%08" PRIx32, result); + return result; + } + + while (!freerdp_shall_disconnect(instance)) + { + nCount = freerdp_get_event_handles(instance->context, &handles[0], 64); + + if (nCount == 0) + { + WLog_ERR(TAG, "%s: freerdp_get_event_handles failed", __FUNCTION__); + break; + } + + status = WaitForMultipleObjects(nCount, handles, FALSE, 100); + + if (status == WAIT_FAILED) + { + WLog_ERR(TAG, "%s: WaitForMultipleObjects failed with %" PRIu32 "", __FUNCTION__, + status); + break; + } + + if (!freerdp_check_event_handles(instance->context)) + { + if (freerdp_get_last_error(instance->context) == FREERDP_ERROR_SUCCESS) + WLog_ERR(TAG, "Failed to check FreeRDP event handles"); + + break; + } + } + +disconnect: + freerdp_disconnect(instance); + return result; +} + +/* Optional global initializer. + * Here we just register a signal handler to print out stack traces + * if available. */ +static BOOL tf_client_global_init(void) +{ + if (freerdp_handle_signals() != 0) + return FALSE; + + return TRUE; +} + +/* Optional global tear down */ +static void tf_client_global_uninit(void) +{ +} + +static int tf_logon_error_info(freerdp* instance, UINT32 data, UINT32 type) +{ + tfContext* tf; + const char* str_data = freerdp_get_logon_error_info_data(data); + const char* str_type = freerdp_get_logon_error_info_type(type); + + if (!instance || !instance->context) + return -1; + + tf = (tfContext*)instance->context; + WLog_INFO(TAG, "Logon Error Info %s [%s]", str_data, str_type); + WINPR_UNUSED(tf); + + return 1; +} + +static BOOL tf_client_new(freerdp* instance, rdpContext* context) +{ + tfContext* tf = (tfContext*)context; + + if (!instance || !context) + return FALSE; + + instance->PreConnect = tf_pre_connect; + instance->PostConnect = tf_post_connect; + instance->PostDisconnect = tf_post_disconnect; + instance->Authenticate = client_cli_authenticate; + instance->GatewayAuthenticate = client_cli_gw_authenticate; + instance->VerifyCertificateEx = client_cli_verify_certificate_ex; + instance->VerifyChangedCertificateEx = client_cli_verify_changed_certificate_ex; + instance->LogonErrorInfo = tf_logon_error_info; + /* TODO: Client display set up */ + WINPR_UNUSED(tf); + return TRUE; +} + +static void tf_client_free(freerdp* instance, rdpContext* context) +{ + tfContext* tf = (tfContext*)instance->context; + + if (!context) + return; + + /* TODO: Client display tear down */ + WINPR_UNUSED(tf); +} + +static int tf_client_start(rdpContext* context) +{ + /* TODO: Start client related stuff */ + WINPR_UNUSED(context); + return 0; +} + +static int tf_client_stop(rdpContext* context) +{ + /* TODO: Stop client related stuff */ + WINPR_UNUSED(context); + return 0; +} + +static int RdpClientEntry(RDP_CLIENT_ENTRY_POINTS* pEntryPoints) +{ + ZeroMemory(pEntryPoints, sizeof(RDP_CLIENT_ENTRY_POINTS)); + pEntryPoints->Version = RDP_CLIENT_INTERFACE_VERSION; + pEntryPoints->Size = sizeof(RDP_CLIENT_ENTRY_POINTS_V1); + pEntryPoints->GlobalInit = tf_client_global_init; + pEntryPoints->GlobalUninit = tf_client_global_uninit; + pEntryPoints->ContextSize = sizeof(tfContext); + pEntryPoints->ClientNew = tf_client_new; + pEntryPoints->ClientFree = tf_client_free; + pEntryPoints->ClientStart = tf_client_start; + pEntryPoints->ClientStop = tf_client_stop; + return 0; +} + +int main(int argc, char* argv[]) +{ + int rc = -1; + DWORD status; + RDP_CLIENT_ENTRY_POINTS clientEntryPoints; + rdpContext* context; + RdpClientEntry(&clientEntryPoints); + context = freerdp_client_context_new(&clientEntryPoints); + + if (!context) + goto fail; + + status = freerdp_client_settings_parse_command_line(context->settings, argc, argv, FALSE); + if (status) + { + rc = freerdp_client_settings_command_line_status_print(context->settings, status, argc, + argv); + goto fail; + } + + if (freerdp_client_start(context) != 0) + goto fail; + + rc = tf_client_thread_proc(context->instance); + + if (freerdp_client_stop(context) != 0) + rc = -1; + +fail: + freerdp_client_context_free(context); + return rc; +} diff --git a/client/Sample/tf_freerdp.h b/client/Sample/tf_freerdp.h new file mode 100644 index 0000000..19e0cee --- /dev/null +++ b/client/Sample/tf_freerdp.h @@ -0,0 +1,42 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Sample Client + * + * Copyright 2018 Armin Novak + * Copyright 2018 Thincast Technologies GmbH + * + * Licensed 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 FREERDP_CLIENT_SAMPLE_H +#define FREERDP_CLIENT_SAMPLE_H + +#include +#include +#include +#include +#include +#include + +struct tf_context +{ + rdpContext context; + + /* Channels */ + RdpeiClientContext* rdpei; + RdpgfxClientContext* gfx; + EncomspClientContext* encomsp; +}; +typedef struct tf_context tfContext; + +#endif /* FREERDP_CLIENT_SAMPLE_H */ diff --git a/client/Wayland/CMakeLists.txt b/client/Wayland/CMakeLists.txt new file mode 100644 index 0000000..a9e19e3 --- /dev/null +++ b/client/Wayland/CMakeLists.txt @@ -0,0 +1,50 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP Wayland Client cmake build script +# +# Copyright 2014 Manuel Bachmann +# Copyright 2015 David Fort +# +# Licensed 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. + +set(MODULE_NAME "wlfreerdp") +set(MODULE_PREFIX "FREERDP_CLIENT_WAYLAND") + +include_directories(${WAYLAND_INCLUDE_DIR}) +include_directories(${CMAKE_SOURCE_DIR}/uwac/include) + +set(${MODULE_PREFIX}_SRCS + wlfreerdp.c + wlfreerdp.h + wlf_disp.c + wlf_disp.h + wlf_pointer.c + wlf_pointer.h + wlf_input.c + wlf_input.h + wlf_cliprdr.c + wlf_cliprdr.h + wlf_channels.c + wlf_channels.h + ) + +list (APPEND ${MODULE_PREFIX}_LIBS freerdp-client freerdp uwac) + +add_executable(${MODULE_NAME} ${${MODULE_PREFIX}_SRCS}) + +target_link_libraries(${MODULE_NAME} ${${MODULE_PREFIX}_LIBS}) + +install(TARGETS ${MODULE_NAME} DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT client) + +set_property(TARGET ${MODULE_NAME} PROPERTY FOLDER "Client/Wayland") +configure_file(wlfreerdp.1.in ${CMAKE_CURRENT_BINARY_DIR}/wlfreerdp.1) +install_freerdp_man(${CMAKE_CURRENT_BINARY_DIR}/wlfreerdp.1 1) diff --git a/client/Wayland/wlf_channels.c b/client/Wayland/wlf_channels.c new file mode 100644 index 0000000..9c49584 --- /dev/null +++ b/client/Wayland/wlf_channels.c @@ -0,0 +1,156 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * X11 Client Channels + * + * Copyright 2013 Marc-Andre Moreau + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include + +#include "wlf_channels.h" +#include "wlf_cliprdr.h" +#include "wlf_disp.h" +#include "wlfreerdp.h" + +BOOL encomsp_toggle_control(EncomspClientContext* encomsp, BOOL control) +{ + ENCOMSP_CHANGE_PARTICIPANT_CONTROL_LEVEL_PDU pdu; + + if (!encomsp) + return FALSE; + + pdu.ParticipantId = 0; + pdu.Flags = ENCOMSP_REQUEST_VIEW; + + if (control) + pdu.Flags |= ENCOMSP_REQUEST_INTERACT; + + encomsp->ChangeParticipantControlLevel(encomsp, &pdu); + return TRUE; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT +wlf_encomsp_participant_created(EncomspClientContext* context, + const ENCOMSP_PARTICIPANT_CREATED_PDU* participantCreated) +{ + wlfContext* wlf; + rdpSettings* settings; + BOOL request; + + if (!context || !context->custom || !participantCreated) + return ERROR_INVALID_PARAMETER; + + wlf = (wlfContext*)context->custom; + settings = wlf->context.settings; + + if (!settings) + return ERROR_INVALID_PARAMETER; + + request = freerdp_settings_get_bool(settings, FreeRDP_RemoteAssistanceRequestControl); + if (request && (participantCreated->Flags & ENCOMSP_MAY_VIEW) && + !(participantCreated->Flags & ENCOMSP_MAY_INTERACT)) + { + if (!encomsp_toggle_control(context, TRUE)) + return ERROR_INTERNAL_ERROR; + } + + return CHANNEL_RC_OK; +} + +static void wlf_encomsp_init(wlfContext* wlf, EncomspClientContext* encomsp) +{ + wlf->encomsp = encomsp; + encomsp->custom = (void*)wlf; + encomsp->ParticipantCreated = wlf_encomsp_participant_created; +} + +static void wlf_encomsp_uninit(wlfContext* wlf, EncomspClientContext* encomsp) +{ + if (encomsp) + { + encomsp->custom = NULL; + encomsp->ParticipantCreated = NULL; + } + + if (wlf) + wlf->encomsp = NULL; +} + +void wlf_OnChannelConnectedEventHandler(void* context, ChannelConnectedEventArgs* e) +{ + wlfContext* wlf = (wlfContext*)context; + + if (strcmp(e->name, RDPEI_DVC_CHANNEL_NAME) == 0) + { + wlf->rdpei = (RdpeiClientContext*)e->pInterface; + } + else if (strcmp(e->name, RDPGFX_DVC_CHANNEL_NAME) == 0) + { + gdi_graphics_pipeline_init(wlf->context.gdi, (RdpgfxClientContext*)e->pInterface); + } + else if (strcmp(e->name, RAIL_SVC_CHANNEL_NAME) == 0) + { + } + else if (strcmp(e->name, CLIPRDR_SVC_CHANNEL_NAME) == 0) + { + wlf_cliprdr_init(wlf->clipboard, (CliprdrClientContext*)e->pInterface); + } + else if (strcmp(e->name, ENCOMSP_SVC_CHANNEL_NAME) == 0) + { + wlf_encomsp_init(wlf, (EncomspClientContext*)e->pInterface); + } + else if (strcmp(e->name, DISP_DVC_CHANNEL_NAME) == 0) + { + wlf_disp_init(wlf->disp, (DispClientContext*)e->pInterface); + } +} + +void wlf_OnChannelDisconnectedEventHandler(void* context, ChannelDisconnectedEventArgs* e) +{ + wlfContext* wlf = (wlfContext*)context; + + if (strcmp(e->name, RDPEI_DVC_CHANNEL_NAME) == 0) + { + wlf->rdpei = NULL; + } + else if (strcmp(e->name, RDPGFX_DVC_CHANNEL_NAME) == 0) + { + gdi_graphics_pipeline_uninit(wlf->context.gdi, (RdpgfxClientContext*)e->pInterface); + } + else if (strcmp(e->name, RAIL_SVC_CHANNEL_NAME) == 0) + { + } + else if (strcmp(e->name, CLIPRDR_SVC_CHANNEL_NAME) == 0) + { + wlf_cliprdr_uninit(wlf->clipboard, (CliprdrClientContext*)e->pInterface); + } + else if (strcmp(e->name, ENCOMSP_SVC_CHANNEL_NAME) == 0) + { + wlf_encomsp_uninit(wlf, (EncomspClientContext*)e->pInterface); + } + else if (strcmp(e->name, DISP_DVC_CHANNEL_NAME) == 0) + { + wlf_disp_uninit(wlf->disp, (DispClientContext*)e->pInterface); + } +} diff --git a/client/Wayland/wlf_channels.h b/client/Wayland/wlf_channels.h new file mode 100644 index 0000000..c56be6a --- /dev/null +++ b/client/Wayland/wlf_channels.h @@ -0,0 +1,37 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * X11 Client Channels + * + * Copyright 2013 Marc-Andre Moreau + * + * Licensed 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 FREERDP_CLIENT_WAYLAND_CHANNELS_H +#define FREERDP_CLIENT_WAYLAND_CHANNELS_H + +#include +#include +#include +#include +#include +#include +#include + +int wlf_on_channel_connected(freerdp* instance, const char* name, void* pInterface); +int wlf_on_channel_disconnected(freerdp* instance, const char* name, void* pInterface); + +void wlf_OnChannelConnectedEventHandler(void* context, ChannelConnectedEventArgs* e); +void wlf_OnChannelDisconnectedEventHandler(void* context, ChannelDisconnectedEventArgs* e); + +#endif /* FREERDP_CLIENT_WAYLAND_CHANNELS_H */ diff --git a/client/Wayland/wlf_cliprdr.c b/client/Wayland/wlf_cliprdr.c new file mode 100644 index 0000000..0f0195d --- /dev/null +++ b/client/Wayland/wlf_cliprdr.c @@ -0,0 +1,921 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Wayland Clipboard Redirection + * + * Copyright 2018 Armin Novak + * Copyright 2018 Thincast Technologies GmbH + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "wlf_cliprdr.h" + +#define TAG CLIENT_TAG("wayland.cliprdr") + +#define MAX_CLIPBOARD_FORMATS 255 + +static const char* mime_text[] = { "text/plain", "text/plain;charset=utf-8", + "UTF8_STRING", "COMPOUND_TEXT", + "TEXT", "STRING" }; + +static const char* mime_image[] = { + "image/png", "image/bmp", "image/x-bmp", "image/x-MS-bmp", + "image/x-icon", "image/x-ico", "image/x-win-bitmap", "image/vmd.microsoft.icon", + "application/ico", "image/ico", "image/icon", "image/jpeg", + "image/tiff" +}; + +static const char* mime_html[] = { "text/html" }; + +struct wlf_clipboard +{ + wlfContext* wfc; + rdpChannels* channels; + CliprdrClientContext* context; + wLog* log; + + UwacSeat* seat; + wClipboard* system; + wClipboardDelegate* delegate; + + size_t numClientFormats; + CLIPRDR_FORMAT* clientFormats; + + size_t numServerFormats; + CLIPRDR_FORMAT* serverFormats; + + BOOL sync; + + /* File clipping */ + BOOL streams_supported; + BOOL file_formats_registered; + + /* Server response stuff */ + FILE* responseFile; + UINT32 responseFormat; + const char* responseMime; + CRITICAL_SECTION lock; +}; + +static BOOL wlf_mime_is_text(const char* mime) +{ + size_t x; + + for (x = 0; x < ARRAYSIZE(mime_text); x++) + { + if (strcmp(mime, mime_text[x]) == 0) + return TRUE; + } + + return FALSE; +} + +static BOOL wlf_mime_is_image(const char* mime) +{ + size_t x; + + for (x = 0; x < ARRAYSIZE(mime_image); x++) + { + if (strcmp(mime, mime_image[x]) == 0) + return TRUE; + } + + return FALSE; +} + +static BOOL wlf_mime_is_html(const char* mime) +{ + size_t x; + + for (x = 0; x < ARRAYSIZE(mime_html); x++) + { + if (strcmp(mime, mime_html[x]) == 0) + return TRUE; + } + + return FALSE; +} + +static void wlf_cliprdr_free_server_formats(wfClipboard* clipboard) +{ + if (clipboard && clipboard->serverFormats) + { + size_t j; + + for (j = 0; j < clipboard->numServerFormats; j++) + { + CLIPRDR_FORMAT* format = &clipboard->serverFormats[j]; + free(format->formatName); + } + + free(clipboard->serverFormats); + clipboard->serverFormats = NULL; + clipboard->numServerFormats = 0; + } + + if (clipboard) + UwacClipboardOfferDestroy(clipboard->seat); +} + +static void wlf_cliprdr_free_client_formats(wfClipboard* clipboard) +{ + if (clipboard && clipboard->numClientFormats) + { + size_t j; + + for (j = 0; j < clipboard->numClientFormats; j++) + { + CLIPRDR_FORMAT* format = &clipboard->clientFormats[j]; + free(format->formatName); + } + + free(clipboard->clientFormats); + clipboard->clientFormats = NULL; + clipboard->numClientFormats = 0; + } + + if (clipboard) + UwacClipboardOfferDestroy(clipboard->seat); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT wlf_cliprdr_send_client_format_list(wfClipboard* clipboard) +{ + CLIPRDR_FORMAT_LIST formatList = { 0 }; + formatList.msgFlags = CB_RESPONSE_OK; + formatList.numFormats = (UINT32)clipboard->numClientFormats; + formatList.formats = clipboard->clientFormats; + formatList.msgType = CB_FORMAT_LIST; + return clipboard->context->ClientFormatList(clipboard->context, &formatList); +} + +static void wfl_cliprdr_add_client_format_id(wfClipboard* clipboard, UINT32 formatId) +{ + size_t x; + CLIPRDR_FORMAT* format; + const char* name = ClipboardGetFormatName(clipboard->system, formatId); + + for (x = 0; x < clipboard->numClientFormats; x++) + { + format = &clipboard->clientFormats[x]; + + if (format->formatId == formatId) + return; + } + + format = realloc(clipboard->clientFormats, + (clipboard->numClientFormats + 1) * sizeof(CLIPRDR_FORMAT)); + + if (!format) + return; + + clipboard->clientFormats = format; + format = &clipboard->clientFormats[clipboard->numClientFormats++]; + format->formatId = formatId; + format->formatName = NULL; + + if (name && (formatId >= CF_MAX)) + format->formatName = _strdup(name); +} + +static void wlf_cliprdr_add_client_format(wfClipboard* clipboard, const char* mime) +{ + if (wlf_mime_is_html(mime)) + { + UINT32 formatId = ClipboardGetFormatId(clipboard->system, "HTML Format"); + wfl_cliprdr_add_client_format_id(clipboard, formatId); + } + else if (wlf_mime_is_text(mime)) + { + wfl_cliprdr_add_client_format_id(clipboard, CF_TEXT); + wfl_cliprdr_add_client_format_id(clipboard, CF_OEMTEXT); + wfl_cliprdr_add_client_format_id(clipboard, CF_UNICODETEXT); + } + else if (wlf_mime_is_image(mime)) + { + UINT32 formatId = ClipboardGetFormatId(clipboard->system, "image/bmp"); + wfl_cliprdr_add_client_format_id(clipboard, formatId); + wfl_cliprdr_add_client_format_id(clipboard, CF_DIB); + } + + wlf_cliprdr_send_client_format_list(clipboard); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT wlf_cliprdr_send_data_request(wfClipboard* clipboard, UINT32 formatId) +{ + CLIPRDR_FORMAT_DATA_REQUEST request = { 0 }; + request.requestedFormatId = formatId; + return clipboard->context->ClientFormatDataRequest(clipboard->context, &request); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT wlf_cliprdr_send_data_response(wfClipboard* clipboard, const BYTE* data, size_t size) +{ + CLIPRDR_FORMAT_DATA_RESPONSE response = { 0 }; + + if (size > UINT32_MAX) + return ERROR_INVALID_PARAMETER; + + response.msgFlags = (data) ? CB_RESPONSE_OK : CB_RESPONSE_FAIL; + response.dataLen = (UINT32)size; + response.requestedFormatData = data; + return clipboard->context->ClientFormatDataResponse(clipboard->context, &response); +} + +BOOL wlf_cliprdr_handle_event(wfClipboard* clipboard, const UwacClipboardEvent* event) +{ + if (!clipboard || !event) + return FALSE; + + if (!clipboard->context) + return TRUE; + + switch (event->type) + { + case UWAC_EVENT_CLIPBOARD_AVAILABLE: + clipboard->seat = event->seat; + return TRUE; + + case UWAC_EVENT_CLIPBOARD_OFFER: + WLog_Print(clipboard->log, WLOG_DEBUG, "client announces mime %s", event->mime); + wlf_cliprdr_add_client_format(clipboard, event->mime); + return TRUE; + + case UWAC_EVENT_CLIPBOARD_SELECT: + WLog_Print(clipboard->log, WLOG_DEBUG, "client announces new data"); + wlf_cliprdr_free_client_formats(clipboard); + return TRUE; + + default: + return FALSE; + } +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT wlf_cliprdr_send_client_capabilities(wfClipboard* clipboard) +{ + CLIPRDR_CAPABILITIES capabilities; + CLIPRDR_GENERAL_CAPABILITY_SET generalCapabilitySet; + capabilities.cCapabilitiesSets = 1; + capabilities.capabilitySets = (CLIPRDR_CAPABILITY_SET*)&(generalCapabilitySet); + generalCapabilitySet.capabilitySetType = CB_CAPSTYPE_GENERAL; + generalCapabilitySet.capabilitySetLength = 12; + generalCapabilitySet.version = CB_CAPS_VERSION_2; + generalCapabilitySet.generalFlags = CB_USE_LONG_FORMAT_NAMES; + + if (clipboard->streams_supported && clipboard->file_formats_registered) + generalCapabilitySet.generalFlags |= + CB_STREAM_FILECLIP_ENABLED | CB_FILECLIP_NO_FILE_PATHS | CB_HUGE_FILE_SUPPORT_ENABLED; + + return clipboard->context->ClientCapabilities(clipboard->context, &capabilities); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT wlf_cliprdr_send_client_format_list_response(wfClipboard* clipboard, BOOL status) +{ + CLIPRDR_FORMAT_LIST_RESPONSE formatListResponse; + formatListResponse.msgType = CB_FORMAT_LIST_RESPONSE; + formatListResponse.msgFlags = status ? CB_RESPONSE_OK : CB_RESPONSE_FAIL; + formatListResponse.dataLen = 0; + return clipboard->context->ClientFormatListResponse(clipboard->context, &formatListResponse); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT wlf_cliprdr_monitor_ready(CliprdrClientContext* context, + const CLIPRDR_MONITOR_READY* monitorReady) +{ + wfClipboard* clipboard = (wfClipboard*)context->custom; + UINT ret; + WINPR_UNUSED(monitorReady); + + if ((ret = wlf_cliprdr_send_client_capabilities(clipboard)) != CHANNEL_RC_OK) + return ret; + + if ((ret = wlf_cliprdr_send_client_format_list(clipboard)) != CHANNEL_RC_OK) + return ret; + + clipboard->sync = TRUE; + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT wlf_cliprdr_server_capabilities(CliprdrClientContext* context, + const CLIPRDR_CAPABILITIES* capabilities) +{ + UINT32 i; + const BYTE* capsPtr = (const BYTE*)capabilities->capabilitySets; + wfClipboard* clipboard = (wfClipboard*)context->custom; + clipboard->streams_supported = FALSE; + + for (i = 0; i < capabilities->cCapabilitiesSets; i++) + { + const CLIPRDR_CAPABILITY_SET* caps = (const CLIPRDR_CAPABILITY_SET*)capsPtr; + + if (caps->capabilitySetType == CB_CAPSTYPE_GENERAL) + { + const CLIPRDR_GENERAL_CAPABILITY_SET* generalCaps = + (const CLIPRDR_GENERAL_CAPABILITY_SET*)caps; + + if (generalCaps->generalFlags & CB_STREAM_FILECLIP_ENABLED) + { + clipboard->streams_supported = TRUE; + } + } + + capsPtr += caps->capabilitySetLength; + } + + return CHANNEL_RC_OK; +} + +static void wlf_cliprdr_transfer_data(UwacSeat* seat, void* context, const char* mime, int fd) +{ + wfClipboard* clipboard = (wfClipboard*)context; + size_t x; + WINPR_UNUSED(seat); + + EnterCriticalSection(&clipboard->lock); + clipboard->responseMime = NULL; + + for (x = 0; x < ARRAYSIZE(mime_html); x++) + { + const char* mime_cur = mime_html[x]; + + if (strcmp(mime_cur, mime) == 0) + { + clipboard->responseMime = mime_cur; + clipboard->responseFormat = ClipboardGetFormatId(clipboard->system, "HTML Format"); + break; + } + } + + for (x = 0; x < ARRAYSIZE(mime_text); x++) + { + const char* mime_cur = mime_text[x]; + + if (strcmp(mime_cur, mime) == 0) + { + clipboard->responseMime = mime_cur; + clipboard->responseFormat = CF_UNICODETEXT; + break; + } + } + + for (x = 0; x < ARRAYSIZE(mime_image); x++) + { + const char* mime_cur = mime_image[x]; + + if (strcmp(mime_cur, mime) == 0) + { + clipboard->responseMime = mime_cur; + clipboard->responseFormat = CF_DIB; + break; + } + } + + if (clipboard->responseMime != NULL) + { + if (clipboard->responseFile != NULL) + fclose(clipboard->responseFile); + clipboard->responseFile = fdopen(fd, "w"); + + if (clipboard->responseFile) + wlf_cliprdr_send_data_request(clipboard, clipboard->responseFormat); + else + WLog_Print(clipboard->log, WLOG_ERROR, + "failed to open clipboard file descriptor for MIME %s", + clipboard->responseMime); + } + LeaveCriticalSection(&clipboard->lock); +} + +static void wlf_cliprdr_cancel_data(UwacSeat* seat, void* context) +{ + WINPR_UNUSED(seat); + WINPR_UNUSED(context); +} + +/** + * Called when the clipboard changes server side. + * + * Clear the local clipboard offer and replace it with a new one + * that announces the formats we get listed here. + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT wlf_cliprdr_server_format_list(CliprdrClientContext* context, + const CLIPRDR_FORMAT_LIST* formatList) +{ + UINT32 i; + wfClipboard* clipboard; + BOOL html = FALSE; + BOOL text = FALSE; + BOOL image = FALSE; + + if (!context || !context->custom) + return ERROR_INVALID_PARAMETER; + + clipboard = (wfClipboard*)context->custom; + wlf_cliprdr_free_server_formats(clipboard); + + if (!(clipboard->serverFormats = + (CLIPRDR_FORMAT*)calloc(formatList->numFormats, sizeof(CLIPRDR_FORMAT)))) + { + WLog_Print(clipboard->log, WLOG_ERROR, + "failed to allocate %" PRIuz " CLIPRDR_FORMAT structs", + clipboard->numServerFormats); + return CHANNEL_RC_NO_MEMORY; + } + + clipboard->numServerFormats = formatList->numFormats; + + if (!clipboard->seat) + { + WLog_Print(clipboard->log, WLOG_ERROR, + "clipboard->seat=NULL, check your client implementation"); + return ERROR_INTERNAL_ERROR; + } + + for (i = 0; i < formatList->numFormats; i++) + { + const CLIPRDR_FORMAT* format = &formatList->formats[i]; + CLIPRDR_FORMAT* srvFormat = &clipboard->serverFormats[i]; + srvFormat->formatId = format->formatId; + + if (format->formatName) + { + srvFormat->formatName = _strdup(format->formatName); + + if (!srvFormat->formatName) + { + wlf_cliprdr_free_server_formats(clipboard); + return CHANNEL_RC_NO_MEMORY; + } + } + + if (format->formatName) + { + if (strcmp(format->formatName, "HTML Format") == 0) + { + text = TRUE; + html = TRUE; + } + } + else + { + switch (format->formatId) + { + case CF_TEXT: + case CF_OEMTEXT: + case CF_UNICODETEXT: + text = TRUE; + break; + + case CF_DIB: + image = TRUE; + break; + + default: + break; + } + } + } + + if (html) + { + size_t x; + + for (x = 0; x < ARRAYSIZE(mime_html); x++) + UwacClipboardOfferCreate(clipboard->seat, mime_html[x]); + } + + if (text) + { + size_t x; + + for (x = 0; x < ARRAYSIZE(mime_text); x++) + UwacClipboardOfferCreate(clipboard->seat, mime_text[x]); + } + + if (image) + { + size_t x; + + for (x = 0; x < ARRAYSIZE(mime_image); x++) + UwacClipboardOfferCreate(clipboard->seat, mime_image[x]); + } + + UwacClipboardOfferAnnounce(clipboard->seat, clipboard, wlf_cliprdr_transfer_data, + wlf_cliprdr_cancel_data); + return wlf_cliprdr_send_client_format_list_response(clipboard, TRUE); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT +wlf_cliprdr_server_format_list_response(CliprdrClientContext* context, + const CLIPRDR_FORMAT_LIST_RESPONSE* formatListResponse) +{ + // wfClipboard* clipboard = (wfClipboard*) context->custom; + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT +wlf_cliprdr_server_format_data_request(CliprdrClientContext* context, + const CLIPRDR_FORMAT_DATA_REQUEST* formatDataRequest) +{ + int cnv; + UINT rc = CHANNEL_RC_OK; + BYTE* data; + LPWSTR cdata; + size_t size; + const char* mime; + UINT32 formatId = formatDataRequest->requestedFormatId; + wfClipboard* clipboard = (wfClipboard*)context->custom; + + switch (formatId) + { + case CF_TEXT: + case CF_OEMTEXT: + case CF_UNICODETEXT: + mime = "text/plain;charset=utf-8"; + break; + + case CF_DIB: + case CF_DIBV5: + mime = "image/bmp"; + break; + + default: + if (formatId == ClipboardGetFormatId(clipboard->system, "HTML Format")) + mime = "text/html"; + else if (formatId == ClipboardGetFormatId(clipboard->system, "image/bmp")) + mime = "image/bmp"; + else + mime = ClipboardGetFormatName(clipboard->system, formatId); + + break; + } + + data = UwacClipboardDataGet(clipboard->seat, mime, &size); + + if (!data) + return ERROR_INTERNAL_ERROR; + + switch (formatId) + { + case CF_UNICODETEXT: + if (size > INT_MAX) + rc = ERROR_INTERNAL_ERROR; + else + { + cdata = NULL; + cnv = ConvertToUnicode(CP_UTF8, 0, (LPCSTR)data, (int)size, &cdata, 0); + free(data); + data = NULL; + + if (cnv < 0) + rc = ERROR_INTERNAL_ERROR; + else + { + size = (size_t)cnv; + data = (BYTE*)cdata; + size *= sizeof(WCHAR); + } + } + + break; + + default: + // TODO: Image conversions + break; + } + + if (rc != CHANNEL_RC_OK) + return rc; + + rc = wlf_cliprdr_send_data_response(clipboard, data, size); + free(data); + return rc; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT +wlf_cliprdr_server_format_data_response(CliprdrClientContext* context, + const CLIPRDR_FORMAT_DATA_RESPONSE* formatDataResponse) +{ + int cnv; + UINT rc = ERROR_INTERNAL_ERROR; + UINT32 size = formatDataResponse->dataLen; + LPSTR cdata = NULL; + LPCSTR data = (LPCSTR)formatDataResponse->requestedFormatData; + const WCHAR* wdata = (const WCHAR*)formatDataResponse->requestedFormatData; + wfClipboard* clipboard = (wfClipboard*)context->custom; + + EnterCriticalSection(&clipboard->lock); + + if (size > INT_MAX * sizeof(WCHAR)) + return ERROR_INTERNAL_ERROR; + + switch (clipboard->responseFormat) + { + case CF_UNICODETEXT: + cnv = ConvertFromUnicode(CP_UTF8, 0, wdata, (int)(size / sizeof(WCHAR)), &cdata, 0, + NULL, NULL); + + if (cnv < 0) + return ERROR_INTERNAL_ERROR; + + size = (size_t)cnv; + data = cdata; + break; + + default: + // TODO: Image conversions + break; + } + + if (clipboard->responseFile) + { + fwrite(data, 1, size, clipboard->responseFile); + fclose(clipboard->responseFile); + clipboard->responseFile = NULL; + } + rc = CHANNEL_RC_OK; + free(cdata); + + LeaveCriticalSection(&clipboard->lock); + return rc; +} + +static UINT +wlf_cliprdr_server_file_size_request(wfClipboard* clipboard, + const CLIPRDR_FILE_CONTENTS_REQUEST* fileContentsRequest) +{ + wClipboardFileSizeRequest request = { 0 }; + request.streamId = fileContentsRequest->streamId; + request.listIndex = fileContentsRequest->listIndex; + + if (fileContentsRequest->cbRequested != sizeof(UINT64)) + { + WLog_Print(clipboard->log, WLOG_WARN, + "unexpected FILECONTENTS_SIZE request: %" PRIu32 " bytes", + fileContentsRequest->cbRequested); + } + + return clipboard->delegate->ClientRequestFileSize(clipboard->delegate, &request); +} + +static UINT +wlf_cliprdr_server_file_range_request(wfClipboard* clipboard, + const CLIPRDR_FILE_CONTENTS_REQUEST* fileContentsRequest) +{ + wClipboardFileRangeRequest request = { 0 }; + request.streamId = fileContentsRequest->streamId; + request.listIndex = fileContentsRequest->listIndex; + request.nPositionLow = fileContentsRequest->nPositionLow; + request.nPositionHigh = fileContentsRequest->nPositionHigh; + request.cbRequested = fileContentsRequest->cbRequested; + return clipboard->delegate->ClientRequestFileRange(clipboard->delegate, &request); +} + +static UINT +wlf_cliprdr_send_file_contents_failure(CliprdrClientContext* context, + const CLIPRDR_FILE_CONTENTS_REQUEST* fileContentsRequest) +{ + CLIPRDR_FILE_CONTENTS_RESPONSE response = { 0 }; + response.msgFlags = CB_RESPONSE_FAIL; + response.streamId = fileContentsRequest->streamId; + return context->ClientFileContentsResponse(context, &response); +} + +static UINT +wlf_cliprdr_server_file_contents_request(CliprdrClientContext* context, + const CLIPRDR_FILE_CONTENTS_REQUEST* fileContentsRequest) +{ + UINT error = NO_ERROR; + wfClipboard* clipboard = context->custom; + + /* + * MS-RDPECLIP 2.2.5.3 File Contents Request PDU (CLIPRDR_FILECONTENTS_REQUEST): + * The FILECONTENTS_SIZE and FILECONTENTS_RANGE flags MUST NOT be set at the same time. + */ + if ((fileContentsRequest->dwFlags & (FILECONTENTS_SIZE | FILECONTENTS_RANGE)) == + (FILECONTENTS_SIZE | FILECONTENTS_RANGE)) + { + WLog_Print(clipboard->log, WLOG_ERROR, "invalid CLIPRDR_FILECONTENTS_REQUEST.dwFlags"); + return wlf_cliprdr_send_file_contents_failure(context, fileContentsRequest); + } + + if (fileContentsRequest->dwFlags & FILECONTENTS_SIZE) + error = wlf_cliprdr_server_file_size_request(clipboard, fileContentsRequest); + + if (fileContentsRequest->dwFlags & FILECONTENTS_RANGE) + error = wlf_cliprdr_server_file_range_request(clipboard, fileContentsRequest); + + if (error) + { + WLog_Print(clipboard->log, WLOG_ERROR, + "failed to handle CLIPRDR_FILECONTENTS_REQUEST: 0x%08X", error); + return wlf_cliprdr_send_file_contents_failure(context, fileContentsRequest); + } + + return CHANNEL_RC_OK; +} + +static UINT wlf_cliprdr_clipboard_file_size_success(wClipboardDelegate* delegate, + const wClipboardFileSizeRequest* request, + UINT64 fileSize) +{ + CLIPRDR_FILE_CONTENTS_RESPONSE response = { 0 }; + wfClipboard* clipboard = delegate->custom; + response.msgFlags = CB_RESPONSE_OK; + response.streamId = request->streamId; + response.cbRequested = sizeof(UINT64); + response.requestedData = (BYTE*)&fileSize; + return clipboard->context->ClientFileContentsResponse(clipboard->context, &response); +} + +static UINT wlf_cliprdr_clipboard_file_size_failure(wClipboardDelegate* delegate, + const wClipboardFileSizeRequest* request, + UINT errorCode) +{ + CLIPRDR_FILE_CONTENTS_RESPONSE response = { 0 }; + wfClipboard* clipboard = delegate->custom; + WINPR_UNUSED(errorCode); + response.msgFlags = CB_RESPONSE_FAIL; + response.streamId = request->streamId; + return clipboard->context->ClientFileContentsResponse(clipboard->context, &response); +} + +static UINT wlf_cliprdr_clipboard_file_range_success(wClipboardDelegate* delegate, + const wClipboardFileRangeRequest* request, + const BYTE* data, UINT32 size) +{ + CLIPRDR_FILE_CONTENTS_RESPONSE response = { 0 }; + wfClipboard* clipboard = delegate->custom; + response.msgFlags = CB_RESPONSE_OK; + response.streamId = request->streamId; + response.cbRequested = size; + response.requestedData = (const BYTE*)data; + return clipboard->context->ClientFileContentsResponse(clipboard->context, &response); +} + +static UINT wlf_cliprdr_clipboard_file_range_failure(wClipboardDelegate* delegate, + const wClipboardFileRangeRequest* request, + UINT errorCode) +{ + CLIPRDR_FILE_CONTENTS_RESPONSE response = { 0 }; + wfClipboard* clipboard = delegate->custom; + WINPR_UNUSED(errorCode); + response.msgFlags = CB_RESPONSE_FAIL; + response.streamId = request->streamId; + return clipboard->context->ClientFileContentsResponse(clipboard->context, &response); +} + +wfClipboard* wlf_clipboard_new(wlfContext* wfc) +{ + rdpChannels* channels; + wfClipboard* clipboard = (wfClipboard*)calloc(1, sizeof(wfClipboard)); + + if (!clipboard) + goto fail; + + InitializeCriticalSection(&clipboard->lock); + clipboard->wfc = wfc; + channels = wfc->context.channels; + clipboard->log = WLog_Get(TAG); + clipboard->channels = channels; + clipboard->system = ClipboardCreate(); + if (!clipboard->system) + goto fail; + + clipboard->delegate = ClipboardGetDelegate(clipboard->system); + if (!clipboard->delegate) + goto fail; + + clipboard->delegate->custom = clipboard; + /* TODO: set up a filesystem base path for local URI */ + /* clipboard->delegate->basePath = "file:///tmp/foo/bar/gaga"; */ + clipboard->delegate->ClipboardFileSizeSuccess = wlf_cliprdr_clipboard_file_size_success; + clipboard->delegate->ClipboardFileSizeFailure = wlf_cliprdr_clipboard_file_size_failure; + clipboard->delegate->ClipboardFileRangeSuccess = wlf_cliprdr_clipboard_file_range_success; + clipboard->delegate->ClipboardFileRangeFailure = wlf_cliprdr_clipboard_file_range_failure; + return clipboard; + +fail: + wlf_clipboard_free(clipboard); + return NULL; +} + +void wlf_clipboard_free(wfClipboard* clipboard) +{ + if (!clipboard) + return; + + wlf_cliprdr_free_server_formats(clipboard); + wlf_cliprdr_free_client_formats(clipboard); + ClipboardDestroy(clipboard->system); + + EnterCriticalSection(&clipboard->lock); + if (clipboard->responseFile) + fclose(clipboard->responseFile); + LeaveCriticalSection(&clipboard->lock); + DeleteCriticalSection(&clipboard->lock); + free(clipboard); +} + +BOOL wlf_cliprdr_init(wfClipboard* clipboard, CliprdrClientContext* cliprdr) +{ + if (!cliprdr || !clipboard) + return FALSE; + + clipboard->context = cliprdr; + cliprdr->custom = (void*)clipboard; + cliprdr->MonitorReady = wlf_cliprdr_monitor_ready; + cliprdr->ServerCapabilities = wlf_cliprdr_server_capabilities; + cliprdr->ServerFormatList = wlf_cliprdr_server_format_list; + cliprdr->ServerFormatListResponse = wlf_cliprdr_server_format_list_response; + cliprdr->ServerFormatDataRequest = wlf_cliprdr_server_format_data_request; + cliprdr->ServerFormatDataResponse = wlf_cliprdr_server_format_data_response; + cliprdr->ServerFileContentsRequest = wlf_cliprdr_server_file_contents_request; + return TRUE; +} + +BOOL wlf_cliprdr_uninit(wfClipboard* clipboard, CliprdrClientContext* cliprdr) +{ + if (cliprdr) + cliprdr->custom = NULL; + + if (clipboard) + clipboard->context = NULL; + + return TRUE; +} diff --git a/client/Wayland/wlf_cliprdr.h b/client/Wayland/wlf_cliprdr.h new file mode 100644 index 0000000..a113140 --- /dev/null +++ b/client/Wayland/wlf_cliprdr.h @@ -0,0 +1,36 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Wayland Clipboard Redirection + * + * Copyright 2018 Armin Novak + * Copyright 2018 Thincast Technologies GmbH + * + * Licensed 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 FREERDP_CLIENT_WAYLAND_CLIPRDR_H +#define FREERDP_CLIENT_WAYLAND_CLIPRDR_H + +#include "wlfreerdp.h" + +#include + +wfClipboard* wlf_clipboard_new(wlfContext* wlc); +void wlf_clipboard_free(wfClipboard* clipboard); + +BOOL wlf_cliprdr_init(wfClipboard* clipboard, CliprdrClientContext* cliprdr); +BOOL wlf_cliprdr_uninit(wfClipboard* clipboard, CliprdrClientContext* cliprdr); + +BOOL wlf_cliprdr_handle_event(wfClipboard* clipboard, const UwacClipboardEvent* event); + +#endif /* FREERDP_CLIENT_WAYLAND_CLIPRDR_H */ diff --git a/client/Wayland/wlf_disp.c b/client/Wayland/wlf_disp.c new file mode 100644 index 0000000..51a5f9e --- /dev/null +++ b/client/Wayland/wlf_disp.c @@ -0,0 +1,401 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Wayland Display Control Channel + * + * Copyright 2018 Armin Novak + * Copyright 2018 Thincast Technologies GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include "wlf_disp.h" + +#define TAG CLIENT_TAG("wayland.disp") + +#define RESIZE_MIN_DELAY 200 /* minimum delay in ms between two resizes */ + +struct _wlfDispContext +{ + wlfContext* wlc; + DispClientContext* disp; + BOOL haveXRandr; + int eventBase, errorBase; + int lastSentWidth, lastSentHeight; + UINT64 lastSentDate; + int targetWidth, targetHeight; + BOOL activated; + BOOL waitingResize; + BOOL fullscreen; + UINT16 lastSentDesktopOrientation; + UINT32 lastSentDesktopScaleFactor; + UINT32 lastSentDeviceScaleFactor; +}; + +static UINT wlf_disp_sendLayout(DispClientContext* disp, rdpMonitor* monitors, size_t nmonitors); + +static BOOL wlf_disp_settings_changed(wlfDispContext* wlfDisp) +{ + rdpSettings* settings = wlfDisp->wlc->context.settings; + + if (wlfDisp->lastSentWidth != wlfDisp->targetWidth) + return TRUE; + + if (wlfDisp->lastSentHeight != wlfDisp->targetHeight) + return TRUE; + + if (wlfDisp->lastSentDesktopOrientation != settings->DesktopOrientation) + return TRUE; + + if (wlfDisp->lastSentDesktopScaleFactor != settings->DesktopScaleFactor) + return TRUE; + + if (wlfDisp->lastSentDeviceScaleFactor != settings->DeviceScaleFactor) + return TRUE; + + if (wlfDisp->fullscreen != wlfDisp->wlc->fullscreen) + return TRUE; + + return FALSE; +} + +static BOOL wlf_update_last_sent(wlfDispContext* wlfDisp) +{ + rdpSettings* settings = wlfDisp->wlc->context.settings; + wlfDisp->lastSentWidth = wlfDisp->targetWidth; + wlfDisp->lastSentHeight = wlfDisp->targetHeight; + wlfDisp->lastSentDesktopOrientation = settings->DesktopOrientation; + wlfDisp->lastSentDesktopScaleFactor = settings->DesktopScaleFactor; + wlfDisp->lastSentDeviceScaleFactor = settings->DeviceScaleFactor; + wlfDisp->fullscreen = wlfDisp->wlc->fullscreen; + return TRUE; +} + +static BOOL wlf_disp_sendResize(wlfDispContext* wlfDisp) +{ + DISPLAY_CONTROL_MONITOR_LAYOUT layout; + wlfContext* wlc; + rdpSettings* settings; + + if (!wlfDisp || !wlfDisp->wlc) + return FALSE; + + wlc = wlfDisp->wlc; + settings = wlc->context.settings; + + if (!settings) + return FALSE; + + if (!wlfDisp->activated || !wlfDisp->disp) + return TRUE; + + if (GetTickCount64() - wlfDisp->lastSentDate < RESIZE_MIN_DELAY) + return TRUE; + + wlfDisp->lastSentDate = GetTickCount64(); + + if (!wlf_disp_settings_changed(wlfDisp)) + return TRUE; + + /* TODO: Multimonitor support for wayland + if (wlc->fullscreen && (settings->MonitorCount > 0)) + { + if (wlf_disp_sendLayout(wlfDisp->disp, settings->MonitorDefArray, + settings->MonitorCount) != CHANNEL_RC_OK) + return FALSE; + } + else + */ + { + wlfDisp->waitingResize = TRUE; + layout.Flags = DISPLAY_CONTROL_MONITOR_PRIMARY; + layout.Top = layout.Left = 0; + layout.Width = wlfDisp->targetWidth; + layout.Height = wlfDisp->targetHeight; + layout.Orientation = settings->DesktopOrientation; + layout.DesktopScaleFactor = settings->DesktopScaleFactor; + layout.DeviceScaleFactor = settings->DeviceScaleFactor; + layout.PhysicalWidth = wlfDisp->targetWidth; + layout.PhysicalHeight = wlfDisp->targetHeight; + + if (IFCALLRESULT(CHANNEL_RC_OK, wlfDisp->disp->SendMonitorLayout, wlfDisp->disp, 1, + &layout) != CHANNEL_RC_OK) + return FALSE; + } + return wlf_update_last_sent(wlfDisp); +} + +static BOOL wlf_disp_set_window_resizable(wlfDispContext* wlfDisp) +{ +#if 0 // TODO +#endif + return TRUE; +} + +static BOOL wlf_disp_check_context(void* context, wlfContext** ppwlc, wlfDispContext** ppwlfDisp, + rdpSettings** ppSettings) +{ + wlfContext* wlc; + + if (!context) + return FALSE; + + wlc = (wlfContext*)context; + + if (!(wlc->disp)) + return FALSE; + + if (!wlc->context.settings) + return FALSE; + + *ppwlc = wlc; + *ppwlfDisp = wlc->disp; + *ppSettings = wlc->context.settings; + return TRUE; +} + +static void wlf_disp_OnActivated(void* context, ActivatedEventArgs* e) +{ + wlfContext* wlc; + wlfDispContext* wlfDisp; + rdpSettings* settings; + + if (!wlf_disp_check_context(context, &wlc, &wlfDisp, &settings)) + return; + + wlfDisp->waitingResize = FALSE; + + if (wlfDisp->activated && !settings->Fullscreen) + { + wlf_disp_set_window_resizable(wlfDisp); + + if (e->firstActivation) + return; + + wlf_disp_sendResize(wlfDisp); + } +} + +static void wlf_disp_OnGraphicsReset(void* context, GraphicsResetEventArgs* e) +{ + wlfContext* wlc; + wlfDispContext* wlfDisp; + rdpSettings* settings; + + WINPR_UNUSED(e); + if (!wlf_disp_check_context(context, &wlc, &wlfDisp, &settings)) + return; + + wlfDisp->waitingResize = FALSE; + + if (wlfDisp->activated && !settings->Fullscreen) + { + wlf_disp_set_window_resizable(wlfDisp); + wlf_disp_sendResize(wlfDisp); + } +} + +static void wlf_disp_OnTimer(void* context, TimerEventArgs* e) +{ + wlfContext* wlc; + wlfDispContext* wlfDisp; + rdpSettings* settings; + + WINPR_UNUSED(e); + if (!wlf_disp_check_context(context, &wlc, &wlfDisp, &settings)) + return; + + if (!wlfDisp->activated || settings->Fullscreen) + return; + + wlf_disp_sendResize(wlfDisp); +} + +wlfDispContext* wlf_disp_new(wlfContext* wlc) +{ + wlfDispContext* ret; + + if (!wlc || !wlc->context.settings || !wlc->context.pubSub) + return NULL; + + ret = calloc(1, sizeof(wlfDispContext)); + + if (!ret) + return NULL; + + ret->wlc = wlc; + ret->lastSentWidth = ret->targetWidth = wlc->context.settings->DesktopWidth; + ret->lastSentHeight = ret->targetHeight = wlc->context.settings->DesktopHeight; + PubSub_SubscribeActivated(wlc->context.pubSub, wlf_disp_OnActivated); + PubSub_SubscribeGraphicsReset(wlc->context.pubSub, wlf_disp_OnGraphicsReset); + PubSub_SubscribeTimer(wlc->context.pubSub, wlf_disp_OnTimer); + return ret; +} + +void wlf_disp_free(wlfDispContext* disp) +{ + if (!disp) + return; + + if (disp->wlc) + { + PubSub_UnsubscribeActivated(disp->wlc->context.pubSub, wlf_disp_OnActivated); + PubSub_UnsubscribeGraphicsReset(disp->wlc->context.pubSub, wlf_disp_OnGraphicsReset); + PubSub_UnsubscribeTimer(disp->wlc->context.pubSub, wlf_disp_OnTimer); + } + + free(disp); +} + +UINT wlf_disp_sendLayout(DispClientContext* disp, rdpMonitor* monitors, size_t nmonitors) +{ + UINT ret = CHANNEL_RC_OK; + DISPLAY_CONTROL_MONITOR_LAYOUT* layouts; + size_t i; + wlfDispContext* wlfDisp = (wlfDispContext*)disp->custom; + rdpSettings* settings = wlfDisp->wlc->context.settings; + layouts = calloc(nmonitors, sizeof(DISPLAY_CONTROL_MONITOR_LAYOUT)); + + if (!layouts) + return CHANNEL_RC_NO_MEMORY; + + for (i = 0; i < nmonitors; i++) + { + layouts[i].Flags = (monitors[i].is_primary ? DISPLAY_CONTROL_MONITOR_PRIMARY : 0); + layouts[i].Left = monitors[i].x; + layouts[i].Top = monitors[i].y; + layouts[i].Width = monitors[i].width; + layouts[i].Height = monitors[i].height; + layouts[i].Orientation = ORIENTATION_LANDSCAPE; + layouts[i].PhysicalWidth = monitors[i].attributes.physicalWidth; + layouts[i].PhysicalHeight = monitors[i].attributes.physicalHeight; + + switch (monitors[i].attributes.orientation) + { + case 90: + layouts[i].Orientation = ORIENTATION_PORTRAIT; + break; + + case 180: + layouts[i].Orientation = ORIENTATION_LANDSCAPE_FLIPPED; + break; + + case 270: + layouts[i].Orientation = ORIENTATION_PORTRAIT_FLIPPED; + break; + + case 0: + default: + /* MS-RDPEDISP - 2.2.2.2.1: + * Orientation (4 bytes): A 32-bit unsigned integer that specifies the + * orientation of the monitor in degrees. Valid values are 0, 90, 180 + * or 270 + * + * So we default to ORIENTATION_LANDSCAPE + */ + layouts[i].Orientation = ORIENTATION_LANDSCAPE; + break; + } + + layouts[i].DesktopScaleFactor = settings->DesktopScaleFactor; + layouts[i].DeviceScaleFactor = settings->DeviceScaleFactor; + } + + ret = IFCALLRESULT(CHANNEL_RC_OK, disp->SendMonitorLayout, disp, nmonitors, layouts); + free(layouts); + return ret; +} + +BOOL wlf_disp_handle_configure(wlfDispContext* disp, int32_t width, int32_t height) +{ + if (!disp) + return FALSE; + + disp->targetWidth = width; + disp->targetHeight = height; + return wlf_disp_sendResize(disp); +} + +static UINT wlf_DisplayControlCaps(DispClientContext* disp, UINT32 maxNumMonitors, + UINT32 maxMonitorAreaFactorA, UINT32 maxMonitorAreaFactorB) +{ + /* we're called only if dynamic resolution update is activated */ + wlfDispContext* wlfDisp = (wlfDispContext*)disp->custom; + rdpSettings* settings = wlfDisp->wlc->context.settings; + WLog_DBG(TAG, + "DisplayControlCapsPdu: MaxNumMonitors: %" PRIu32 " MaxMonitorAreaFactorA: %" PRIu32 + " MaxMonitorAreaFactorB: %" PRIu32 "", + maxNumMonitors, maxMonitorAreaFactorA, maxMonitorAreaFactorB); + wlfDisp->activated = TRUE; + + if (settings->Fullscreen) + return CHANNEL_RC_OK; + + WLog_DBG(TAG, "DisplayControlCapsPdu: setting the window as resizable"); + return wlf_disp_set_window_resizable(wlfDisp) ? CHANNEL_RC_OK : CHANNEL_RC_NO_MEMORY; +} + +BOOL wlf_disp_init(wlfDispContext* wlfDisp, DispClientContext* disp) +{ + rdpSettings* settings; + + if (!wlfDisp || !wlfDisp->wlc || !disp) + return FALSE; + + settings = wlfDisp->wlc->context.settings; + + if (!settings) + return FALSE; + + wlfDisp->disp = disp; + disp->custom = (void*)wlfDisp; + + if (settings->DynamicResolutionUpdate) + { + disp->DisplayControlCaps = wlf_DisplayControlCaps; + } + + return TRUE; +} + +BOOL wlf_disp_uninit(wlfDispContext* wlfDisp, DispClientContext* disp) +{ + if (!wlfDisp || !disp) + return FALSE; + + wlfDisp->disp = NULL; + return TRUE; +} + +int wlf_list_monitors(wlfContext* wlc) +{ + uint32_t i, nmonitors = UwacDisplayGetNbOutputs(wlc->display); + + for (i = 0; i < nmonitors; i++) + { + const UwacOutput* monitor = UwacDisplayGetOutput(wlc->display, i); + UwacSize resolution; + UwacPosition pos; + + if (!monitor) + continue; + UwacOutputGetPosition(monitor, &pos); + UwacOutputGetResolution(monitor, &resolution); + + printf(" %s [%d] %dx%d\t+%d+%d\n", (i == 0) ? "*" : " ", i, resolution.width, + resolution.height, pos.x, pos.y); + } + + return 0; +} diff --git a/client/Wayland/wlf_disp.h b/client/Wayland/wlf_disp.h new file mode 100644 index 0000000..36fa27c --- /dev/null +++ b/client/Wayland/wlf_disp.h @@ -0,0 +1,38 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Wayland Display Control Channel + * + * Copyright 2018 Armin Novak + * Copyright 2018 Thincast Technologies GmbH + * + * Licensed 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 FREERDP_CLIENT_WAYLAND_DISP_H +#define FREERDP_CLIENT_WAYLAND_DISP_H + +#include +#include + +#include "wlfreerdp.h" + +FREERDP_API BOOL wlf_disp_init(wlfDispContext* xfDisp, DispClientContext* disp); +FREERDP_API BOOL wlf_disp_uninit(wlfDispContext* xfDisp, DispClientContext* disp); + +wlfDispContext* wlf_disp_new(wlfContext* wlc); +void wlf_disp_free(wlfDispContext* disp); +BOOL wlf_disp_handle_configure(wlfDispContext* disp, int32_t width, int32_t height); +void wlf_disp_resized(wlfDispContext* disp); + +int wlf_list_monitors(wlfContext* wlc); + +#endif /* FREERDP_CLIENT_WAYLAND_DISP_H */ diff --git a/client/Wayland/wlf_input.c b/client/Wayland/wlf_input.c new file mode 100644 index 0000000..22041ea --- /dev/null +++ b/client/Wayland/wlf_input.c @@ -0,0 +1,401 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Wayland Input + * + * Copyright 2014 Manuel Bachmann + * Copyright 2015 David Fort + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +#include +#include +#include + +#include "wlfreerdp.h" +#include "wlf_input.h" + +#define TAG CLIENT_TAG("wayland.input") + +#define MAX_CONTACTS 20 + +typedef struct touch_contact +{ + int id; + double pos_x; + double pos_y; + BOOL emulate_mouse; +} touchContact; + +static touchContact contacts[MAX_CONTACTS]; + +BOOL wlf_handle_pointer_enter(freerdp* instance, const UwacPointerEnterLeaveEvent* ev) +{ + uint32_t x, y; + + if (!instance || !ev || !instance->input) + return FALSE; + + x = ev->x; + y = ev->y; + + if (!wlf_scale_coordinates(instance->context, &x, &y, TRUE)) + return FALSE; + + return freerdp_input_send_mouse_event(instance->input, PTR_FLAGS_MOVE, x, y); +} + +BOOL wlf_handle_pointer_motion(freerdp* instance, const UwacPointerMotionEvent* ev) +{ + uint32_t x, y; + + if (!instance || !ev || !instance->input) + return FALSE; + + x = ev->x; + y = ev->y; + + if (!wlf_scale_coordinates(instance->context, &x, &y, TRUE)) + return FALSE; + + return freerdp_input_send_mouse_event(instance->input, PTR_FLAGS_MOVE, x, y); +} + +BOOL wlf_handle_pointer_buttons(freerdp* instance, const UwacPointerButtonEvent* ev) +{ + rdpInput* input; + UINT16 flags = 0; + UINT16 xflags = 0; + uint32_t x, y; + + if (!instance || !ev || !instance->input) + return FALSE; + + x = ev->x; + y = ev->y; + + if (!wlf_scale_coordinates(instance->context, &x, &y, TRUE)) + return FALSE; + + input = instance->input; + + if (ev->state == WL_POINTER_BUTTON_STATE_PRESSED) + { + flags |= PTR_FLAGS_DOWN; + xflags |= PTR_XFLAGS_DOWN; + } + + switch (ev->button) + { + case BTN_LEFT: + flags |= PTR_FLAGS_BUTTON1; + break; + + case BTN_RIGHT: + flags |= PTR_FLAGS_BUTTON2; + break; + + case BTN_MIDDLE: + flags |= PTR_FLAGS_BUTTON3; + break; + + case BTN_SIDE: + xflags |= PTR_XFLAGS_BUTTON1; + break; + + case BTN_EXTRA: + xflags |= PTR_XFLAGS_BUTTON2; + break; + + default: + return TRUE; + } + + if ((flags & ~PTR_FLAGS_DOWN) != 0) + return freerdp_input_send_mouse_event(input, flags, x, y); + + if ((xflags & ~PTR_XFLAGS_DOWN) != 0) + return freerdp_input_send_extended_mouse_event(input, xflags, x, y); + + return FALSE; +} + +BOOL wlf_handle_pointer_axis(freerdp* instance, const UwacPointerAxisEvent* ev) +{ + rdpInput* input; + UINT16 flags = 0; + int32_t direction; + uint32_t x, y; + uint32_t i; + + if (!instance || !ev || !instance->input) + return FALSE; + + x = ev->x; + y = ev->y; + + if (!wlf_scale_coordinates(instance->context, &x, &y, TRUE)) + return FALSE; + + input = instance->input; + + direction = ev->value; + switch (ev->axis) + { + case WL_POINTER_AXIS_VERTICAL_SCROLL: + flags |= PTR_FLAGS_WHEEL; + if (direction > 0) + flags |= PTR_FLAGS_WHEEL_NEGATIVE; + break; + + case WL_POINTER_AXIS_HORIZONTAL_SCROLL: + flags |= PTR_FLAGS_HWHEEL; + if (direction < 0) + flags |= PTR_FLAGS_WHEEL_NEGATIVE; + break; + + default: + return FALSE; + } + + /* Wheel rotation steps: + * + * positive: 0 ... 0xFF -> slow ... fast + * negative: 0 ... 0xFF -> fast ... slow + */ + for (i = 0; i < abs(direction); i++) + { + uint32_t cflags = flags | 0x78; + /* Convert negative values to 9bit twos complement */ + if (flags & PTR_FLAGS_WHEEL_NEGATIVE) + cflags = (flags & 0xFF00) | (0x100 - (cflags & 0xFF)); + if (!freerdp_input_send_mouse_event(input, cflags, (UINT16)x, (UINT16)y)) + return FALSE; + } + + return TRUE; +} + +BOOL wlf_handle_key(freerdp* instance, const UwacKeyEvent* ev) +{ + rdpInput* input; + DWORD rdp_scancode; + + if (!instance || !ev || !instance->input) + return FALSE; + + input = instance->input; + rdp_scancode = freerdp_keyboard_get_rdp_scancode_from_x11_keycode(ev->raw_key + 8); + + if (rdp_scancode == RDP_SCANCODE_UNKNOWN) + return TRUE; + + return freerdp_input_send_keyboard_event_ex(input, ev->pressed, rdp_scancode); +} + +BOOL wlf_keyboard_enter(freerdp* instance, const UwacKeyboardEnterLeaveEvent* ev) +{ + if (!instance || !ev || !instance->input) + return FALSE; + + ((wlfContext*)instance->context)->focusing = TRUE; + return TRUE; +} + +BOOL wlf_keyboard_modifiers(freerdp* instance, const UwacKeyboardModifiersEvent* ev) +{ + rdpInput* input; + uint32_t syncFlags; + + if (!instance || !ev || !instance->input) + return FALSE; + + input = instance->input; + syncFlags = 0; + + if (ev->modifiers & UWAC_MOD_CAPS_MASK) + syncFlags |= KBD_SYNC_CAPS_LOCK; + if (ev->modifiers & UWAC_MOD_NUM_MASK) + syncFlags |= KBD_SYNC_NUM_LOCK; + + if (!((wlfContext*)instance->context)->focusing) + return TRUE; + + ((wlfContext*)instance->context)->focusing = FALSE; + return freerdp_input_send_focus_in_event(input, syncFlags) && + freerdp_input_send_mouse_event(input, PTR_FLAGS_MOVE, 0, 0); +} + +BOOL wlf_handle_touch_up(freerdp* instance, const UwacTouchUp* ev) +{ + uint32_t x, y; + int i; + int touchId; + int contactId; + + if (!instance || !ev || !instance->context) + return FALSE; + + touchId = ev->id; + + for (i = 0; i < MAX_CONTACTS; i++) + { + if (contacts[i].id == touchId) + { + contacts[i].id = 0; + x = contacts[i].pos_x; + y = contacts[i].pos_y; + break; + } + } + + if (i == MAX_CONTACTS) + return FALSE; + + WLog_DBG(TAG, "%s called | event_id: %u | x: %u / y: %u", __FUNCTION__, touchId, x, y); + + if (!wlf_scale_coordinates(instance->context, &x, &y, TRUE)) + return FALSE; + + RdpeiClientContext* rdpei = ((wlfContext*)instance->context)->rdpei; + + if (contacts[i].emulate_mouse == TRUE) + { + UINT16 flags = 0; + flags |= PTR_FLAGS_BUTTON1; + + if ((flags & ~PTR_FLAGS_DOWN) != 0) + return freerdp_input_send_mouse_event(instance->input, flags, x, y); + + return TRUE; + } + + if (!rdpei) + return FALSE; + + rdpei->TouchEnd(rdpei, touchId, x, y, &contactId); + + return TRUE; +} + +BOOL wlf_handle_touch_down(freerdp* instance, const UwacTouchDown* ev) +{ + uint32_t x, y; + int i; + int touchId; + int contactId; + wlfContext* context; + + if (!instance || !ev || !instance->context) + return FALSE; + + x = ev->x; + y = ev->y; + touchId = ev->id; + + for (i = 0; i < MAX_CONTACTS; i++) + { + if (contacts[i].id == 0) + { + contacts[i].id = touchId; + contacts[i].pos_x = x; + contacts[i].pos_y = y; + contacts[i].emulate_mouse = FALSE; + break; + } + } + + if (i == MAX_CONTACTS) + return FALSE; + + WLog_DBG(TAG, "%s called | event_id: %u | x: %u / y: %u", __FUNCTION__, touchId, x, y); + + if (!wlf_scale_coordinates(instance->context, &x, &y, TRUE)) + return FALSE; + + context = (wlfContext*)instance->context; + RdpeiClientContext* rdpei = ((wlfContext*)instance->context)->rdpei; + + // Emulate mouse click if touch is not possible, like in login screen + if (!rdpei) + { + contacts[i].emulate_mouse = TRUE; + + UINT16 flags = 0; + flags |= PTR_FLAGS_DOWN; + flags |= PTR_FLAGS_BUTTON1; + + if ((flags & ~PTR_FLAGS_DOWN) != 0) + return freerdp_input_send_mouse_event(instance->input, flags, x, y); + + return FALSE; + } + + rdpei->TouchBegin(rdpei, touchId, x, y, &contactId); + + return TRUE; +} + +BOOL wlf_handle_touch_motion(freerdp* instance, const UwacTouchMotion* ev) +{ + uint32_t x, y; + int i; + int touchId; + int contactId; + + if (!instance || !ev || !instance->context) + return FALSE; + + x = ev->x; + y = ev->y; + touchId = ev->id; + + for (i = 0; i < MAX_CONTACTS; i++) + { + if (contacts[i].id == touchId) + { + if (contacts[i].pos_x == x && contacts[i].pos_y == y) + { + return TRUE; + } + contacts[i].pos_x = x; + contacts[i].pos_y = y; + break; + } + } + + if (i == MAX_CONTACTS) + return FALSE; + + WLog_DBG(TAG, "%s called | event_id: %u | x: %u / y: %u", __FUNCTION__, touchId, x, y); + + if (!wlf_scale_coordinates(instance->context, &x, &y, TRUE)) + return FALSE; + + RdpeiClientContext* rdpei = ((wlfContext*)instance->context)->rdpei; + + if (contacts[i].emulate_mouse == TRUE) + { + return TRUE; + } + + if (!rdpei) + return FALSE; + + rdpei->TouchUpdate(rdpei, touchId, x, y, &contactId); + + return TRUE; +} diff --git a/client/Wayland/wlf_input.h b/client/Wayland/wlf_input.h new file mode 100644 index 0000000..2382dd8 --- /dev/null +++ b/client/Wayland/wlf_input.h @@ -0,0 +1,41 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Wayland Input + * + * Copyright 2014 Manuel Bachmann + * Copyright 2015 David Fort + * + * Licensed 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 FREERDP_CLIENT_WAYLAND_INPUT_H +#define FREERDP_CLIENT_WAYLAND_INPUT_H + +#include +#include +#include +#include + +BOOL wlf_handle_pointer_enter(freerdp* instance, const UwacPointerEnterLeaveEvent* ev); +BOOL wlf_handle_pointer_motion(freerdp* instance, const UwacPointerMotionEvent* ev); +BOOL wlf_handle_pointer_buttons(freerdp* instance, const UwacPointerButtonEvent* ev); +BOOL wlf_handle_pointer_axis(freerdp* instance, const UwacPointerAxisEvent* ev); +BOOL wlf_handle_touch_up(freerdp* instance, const UwacTouchUp* ev); +BOOL wlf_handle_touch_down(freerdp* instance, const UwacTouchDown* ev); +BOOL wlf_handle_touch_motion(freerdp* instance, const UwacTouchMotion* ev); + +BOOL wlf_handle_key(freerdp* instance, const UwacKeyEvent* ev); +BOOL wlf_keyboard_enter(freerdp* instance, const UwacKeyboardEnterLeaveEvent* ev); +BOOL wlf_keyboard_modifiers(freerdp* instance, const UwacKeyboardModifiersEvent* ev); + +#endif /* FREERDP_CLIENT_WAYLAND_INPUT_H */ diff --git a/client/Wayland/wlf_pointer.c b/client/Wayland/wlf_pointer.c new file mode 100644 index 0000000..decde7f --- /dev/null +++ b/client/Wayland/wlf_pointer.c @@ -0,0 +1,170 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Wayland Mouse Pointer + * + * Copyright 2019 Armin Novak + * Copyright 2019 Thincast Technologies GmbH + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "wlf_pointer.h" +#include "wlfreerdp.h" + +#define TAG CLIENT_TAG("wayland.pointer") + +struct wlf_pointer +{ + rdpPointer pointer; + size_t size; + void* data; +}; +typedef struct wlf_pointer wlfPointer; + +static BOOL wlf_Pointer_New(rdpContext* context, rdpPointer* pointer) +{ + wlfPointer* ptr = (wlfPointer*)pointer; + + if (!ptr) + return FALSE; + + ptr->size = pointer->width * pointer->height * 4ULL; + ptr->data = _aligned_malloc(ptr->size, 16); + + if (!ptr->data) + return FALSE; + + if (!freerdp_image_copy_from_pointer_data( + ptr->data, PIXEL_FORMAT_BGRA32, 0, 0, 0, pointer->width, pointer->height, + pointer->xorMaskData, pointer->lengthXorMask, pointer->andMaskData, + pointer->lengthAndMask, pointer->xorBpp, &context->gdi->palette)) + { + _aligned_free(ptr->data); + return FALSE; + } + + return TRUE; +} + +static void wlf_Pointer_Free(rdpContext* context, rdpPointer* pointer) +{ + wlfPointer* ptr = (wlfPointer*)pointer; + WINPR_UNUSED(context); + + if (ptr) + _aligned_free(ptr->data); +} + +static BOOL wlf_Pointer_Set(rdpContext* context, const rdpPointer* pointer) +{ + wlfContext* wlf = (wlfContext*)context; + wlfPointer* ptr = (wlfPointer*)pointer; + void* data; + UINT32 w, h, x, y; + size_t size; + UwacReturnCode rc; + BOOL res = FALSE; + RECTANGLE_16 area; + + if (!wlf || !wlf->seat) + return FALSE; + + x = pointer->xPos; + y = pointer->yPos; + w = pointer->width; + h = pointer->height; + + if (!wlf_scale_coordinates(context, &x, &y, FALSE) || + !wlf_scale_coordinates(context, &w, &h, FALSE)) + return FALSE; + + size = w * h * 4ULL; + data = malloc(size); + + if (!data) + return FALSE; + + area.top = 0; + area.left = 0; + area.right = (UINT16)pointer->width; + area.bottom = (UINT16)pointer->height; + + if (!wlf_copy_image(ptr->data, pointer->width * 4, pointer->width, pointer->height, data, w * 4, + w, h, &area, context->settings->SmartSizing)) + goto fail; + + rc = UwacSeatSetMouseCursor(wlf->seat, data, size, w, h, x, y); + + if (rc == UWAC_SUCCESS) + res = TRUE; + +fail: + free(data); + return res; +} + +static BOOL wlf_Pointer_SetNull(rdpContext* context) +{ + wlfContext* wlf = (wlfContext*)context; + + if (!wlf || !wlf->seat) + return FALSE; + + if (UwacSeatSetMouseCursor(wlf->seat, NULL, 0, 0, 0, 0, 0) != UWAC_SUCCESS) + return FALSE; + + return TRUE; +} + +static BOOL wlf_Pointer_SetDefault(rdpContext* context) +{ + wlfContext* wlf = (wlfContext*)context; + + if (!wlf || !wlf->seat) + return FALSE; + + if (UwacSeatSetMouseCursor(wlf->seat, NULL, 1, 0, 0, 0, 0) != UWAC_SUCCESS) + return FALSE; + + return TRUE; +} + +static BOOL wlf_Pointer_SetPosition(rdpContext* context, UINT32 x, UINT32 y) +{ + // TODO + WLog_WARN(TAG, "%s not implemented", __FUNCTION__); + return TRUE; +} + +BOOL wlf_register_pointer(rdpGraphics* graphics) +{ + rdpPointer* pointer = NULL; + + if (!(pointer = (rdpPointer*)calloc(1, sizeof(rdpPointer)))) + return FALSE; + + pointer->size = sizeof(wlfPointer); + pointer->New = wlf_Pointer_New; + pointer->Free = wlf_Pointer_Free; + pointer->Set = wlf_Pointer_Set; + pointer->SetNull = wlf_Pointer_SetNull; + pointer->SetDefault = wlf_Pointer_SetDefault; + pointer->SetPosition = wlf_Pointer_SetPosition; + graphics_register_pointer(graphics, pointer); + free(pointer); + return TRUE; +} diff --git a/client/Wayland/wlf_pointer.h b/client/Wayland/wlf_pointer.h new file mode 100644 index 0000000..8ae82e1 --- /dev/null +++ b/client/Wayland/wlf_pointer.h @@ -0,0 +1,28 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Wayland Mouse Pointer + * + * Copyright 2019 Armin Novak + * Copyright 2019 Thincast Technologies GmbH + * + * Licensed 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 FREERDP_CLIENT_WAYLAND_POINTER_H +#define FREERDP_CLIENT_WAYLAND_POINTER_H + +#include + +BOOL wlf_register_pointer(rdpGraphics* graphics); + +#endif /* FREERDP_CLIENT_WAYLAND_POINTER_H */ diff --git a/client/Wayland/wlfreerdp.1.in b/client/Wayland/wlfreerdp.1.in new file mode 100644 index 0000000..c268546 --- /dev/null +++ b/client/Wayland/wlfreerdp.1.in @@ -0,0 +1,38 @@ +.de URL +\\$2 \(laURL: \\$1 \(ra\\$3 +.. +.if \n[.g] .mso www.tmac +.TH wlfreerdp 1 2017-01-12 "@FREERDP_VERSION_FULL@" "FreeRDP" +.SH NAME +wlfreerdp \- FreeRDP wayland client +.SH SYNOPSIS +.B wlfreerdp +[file] +[\fIdefault_client_options\fP] +[\fB/v\fP:[:port]] +[\fB/version\fP] +[\fB/help\fP] +.SH DESCRIPTION +.B wlfreerdp +is a wayland Remote Desktop Protocol (RDP) client which is part of the FreeRDP project. A RDP server is built-in to many editions of Windows. Alternative servers included xrdp and VRDP (VirtualBox). +.SH OPTIONS +The wayland client also supports a lot of the \fIdefault client options\fP which are not described here. For details on those see the xfreerdp(1) man page. +.IP \fB/v:\fP\fI[:port]\fP +The server hostname or IP, and optionally the port, to connect to. +.IP /version +Print the version and exit. +.IP /help +Print the help and exit. +.SH EXIT STATUS +.TP +.B 0 +Successful program execution. +.TP +.B not 0 +On failure. + +.SH SEE ALSO +xfreerdp(1) wlog(7) + +.SH AUTHOR +FreeRDP diff --git a/client/Wayland/wlfreerdp.c b/client/Wayland/wlfreerdp.c new file mode 100644 index 0000000..c7a9fca --- /dev/null +++ b/client/Wayland/wlfreerdp.c @@ -0,0 +1,752 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Wayland Client + * + * Copyright 2014 Manuel Bachmann + * Copyright 2016 Thincast Technologies GmbH + * Copyright 2016 Armin Novak + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include + +#include + +#include "wlfreerdp.h" +#include "wlf_input.h" +#include "wlf_cliprdr.h" +#include "wlf_disp.h" +#include "wlf_channels.h" +#include "wlf_pointer.h" + +#define TAG CLIENT_TAG("wayland") + +static BOOL wl_begin_paint(rdpContext* context) +{ + rdpGdi* gdi; + + if (!context || !context->gdi) + return FALSE; + + gdi = context->gdi; + + if (!gdi->primary) + return FALSE; + + gdi->primary->hdc->hwnd->invalid->null = TRUE; + return TRUE; +} + +static BOOL wl_update_buffer(wlfContext* context_w, INT32 ix, INT32 iy, INT32 iw, INT32 ih) +{ + BOOL res = FALSE; + rdpGdi* gdi; + char* data; + UINT32 x, y, w, h; + UwacSize geometry; + size_t stride; + UwacReturnCode rc; + RECTANGLE_16 area; + + if (!context_w) + return FALSE; + + if ((ix < 0) || (iy < 0) || (iw < 0) || (ih < 0)) + return FALSE; + + EnterCriticalSection(&context_w->critical); + x = (UINT32)ix; + y = (UINT32)iy; + w = (UINT32)iw; + h = (UINT32)ih; + rc = UwacWindowGetDrawingBufferGeometry(context_w->window, &geometry, &stride); + data = UwacWindowGetDrawingBuffer(context_w->window); + + if (!data || (rc != UWAC_SUCCESS)) + goto fail; + + gdi = context_w->context.gdi; + + if (!gdi) + goto fail; + + /* Ignore output if the surface size does not match. */ + if (((INT64)x > geometry.width) || ((INT64)y > geometry.height)) + { + res = TRUE; + goto fail; + } + + area.left = x; + area.top = y; + area.right = x + w; + area.bottom = y + h; + + if (!wlf_copy_image(gdi->primary_buffer, gdi->stride, gdi->width, gdi->height, data, stride, + geometry.width, geometry.height, &area, + context_w->context.settings->SmartSizing)) + goto fail; + + if (!wlf_scale_coordinates(&context_w->context, &x, &y, FALSE)) + goto fail; + + if (!wlf_scale_coordinates(&context_w->context, &w, &h, FALSE)) + goto fail; + + if (UwacWindowAddDamage(context_w->window, x, y, w, h) != UWAC_SUCCESS) + goto fail; + + if (UwacWindowSubmitBuffer(context_w->window, false) != UWAC_SUCCESS) + goto fail; + + res = TRUE; +fail: + LeaveCriticalSection(&context_w->critical); + return res; +} + +static BOOL wl_end_paint(rdpContext* context) +{ + rdpGdi* gdi; + wlfContext* context_w; + INT32 x, y; + INT32 w, h; + + if (!context || !context->gdi || !context->gdi->primary) + return FALSE; + + gdi = context->gdi; + + if (gdi->primary->hdc->hwnd->invalid->null) + return TRUE; + + x = gdi->primary->hdc->hwnd->invalid->x; + y = gdi->primary->hdc->hwnd->invalid->y; + w = gdi->primary->hdc->hwnd->invalid->w; + h = gdi->primary->hdc->hwnd->invalid->h; + context_w = (wlfContext*)context; + return wl_update_buffer(context_w, x, y, w, h); +} + +static BOOL wl_refresh_display(wlfContext* context) +{ + rdpGdi* gdi; + + if (!context || !context->context.gdi) + return FALSE; + + gdi = context->context.gdi; + return wl_update_buffer(context, 0, 0, gdi->width, gdi->height); +} + +static BOOL wl_resize_display(rdpContext* context) +{ + wlfContext* wlc = (wlfContext*)context; + rdpGdi* gdi = context->gdi; + rdpSettings* settings = context->settings; + + if (!gdi_resize(gdi, settings->DesktopWidth, settings->DesktopHeight)) + return FALSE; + + return wl_refresh_display(wlc); +} + +static BOOL wl_pre_connect(freerdp* instance) +{ + rdpSettings* settings; + wlfContext* context; + const UwacOutput* output; + UwacSize resolution; + + if (!instance) + return FALSE; + + context = (wlfContext*)instance->context; + settings = instance->settings; + + if (!context || !settings) + return FALSE; + + settings->OsMajorType = OSMAJORTYPE_UNIX; + settings->OsMinorType = OSMINORTYPE_NATIVE_WAYLAND; + PubSub_SubscribeChannelConnected(instance->context->pubSub, wlf_OnChannelConnectedEventHandler); + PubSub_SubscribeChannelDisconnected(instance->context->pubSub, + wlf_OnChannelDisconnectedEventHandler); + + if (settings->Fullscreen) + { + // Use the resolution of the first display output + output = UwacDisplayGetOutput(context->display, 0); + + if ((output != NULL) && (UwacOutputGetResolution(output, &resolution) == UWAC_SUCCESS)) + { + settings->DesktopWidth = (UINT32)resolution.width; + settings->DesktopHeight = (UINT32)resolution.height; + } + else + { + WLog_WARN(TAG, "Failed to get output resolution! Check your display settings"); + } + } + + if (!freerdp_client_load_addins(instance->context->channels, instance->settings)) + return FALSE; + + return TRUE; +} + +static BOOL wl_post_connect(freerdp* instance) +{ + rdpGdi* gdi; + UwacWindow* window; + wlfContext* context; + rdpSettings* settings; + char* title = "FreeRDP"; + char* app_id = "wlfreerdp"; + UINT32 w, h; + + if (!instance || !instance->context) + return FALSE; + + context = (wlfContext*)instance->context; + settings = instance->context->settings; + + if (settings->WindowTitle) + title = settings->WindowTitle; + + if (!gdi_init(instance, PIXEL_FORMAT_BGRA32)) + return FALSE; + + gdi = instance->context->gdi; + + if (!gdi || (gdi->width < 0) || (gdi->height < 0)) + return FALSE; + + if (!wlf_register_pointer(instance->context->graphics)) + return FALSE; + + w = (UINT32)gdi->width; + h = (UINT32)gdi->height; + + if (settings->SmartSizing && !context->fullscreen) + { + if (settings->SmartSizingWidth > 0) + w = settings->SmartSizingWidth; + + if (settings->SmartSizingHeight > 0) + h = settings->SmartSizingHeight; + } + + context->window = window = UwacCreateWindowShm(context->display, w, h, WL_SHM_FORMAT_XRGB8888); + + if (!window) + return FALSE; + + UwacWindowSetFullscreenState(window, NULL, instance->context->settings->Fullscreen); + UwacWindowSetTitle(window, title); + UwacWindowSetAppId(window, app_id); + UwacWindowSetOpaqueRegion(context->window, 0, 0, w, h); + instance->update->BeginPaint = wl_begin_paint; + instance->update->EndPaint = wl_end_paint; + instance->update->DesktopResize = wl_resize_display; + freerdp_keyboard_init_ex(instance->context->settings->KeyboardLayout, + instance->context->settings->KeyboardRemappingList); + + if (!(context->disp = wlf_disp_new(context))) + return FALSE; + + context->clipboard = wlf_clipboard_new(context); + + if (!context->clipboard) + return FALSE; + + return wl_refresh_display(context); +} + +static void wl_post_disconnect(freerdp* instance) +{ + wlfContext* context; + + if (!instance) + return; + + if (!instance->context) + return; + + context = (wlfContext*)instance->context; + gdi_free(instance); + wlf_clipboard_free(context->clipboard); + wlf_disp_free(context->disp); + + if (context->window) + UwacDestroyWindow(&context->window); +} + +static BOOL handle_uwac_events(freerdp* instance, UwacDisplay* display) +{ + BOOL rc; + UwacEvent event; + wlfContext* context; + + if (UwacDisplayDispatch(display, 1) < 0) + return FALSE; + + context = (wlfContext*)instance->context; + + while (UwacHasEvent(display)) + { + if (UwacNextEvent(display, &event) != UWAC_SUCCESS) + return FALSE; + + /*printf("UWAC event type %d\n", event.type);*/ + switch (event.type) + { + case UWAC_EVENT_NEW_SEAT: + context->seat = event.seat_new.seat; + break; + + case UWAC_EVENT_REMOVED_SEAT: + context->seat = NULL; + break; + + case UWAC_EVENT_FRAME_DONE: + { + UwacReturnCode r; + EnterCriticalSection(&context->critical); + r = UwacWindowSubmitBuffer(context->window, false); + LeaveCriticalSection(&context->critical); + if (r != UWAC_SUCCESS) + return FALSE; + } + break; + + case UWAC_EVENT_POINTER_ENTER: + if (!wlf_handle_pointer_enter(instance, &event.mouse_enter_leave)) + return FALSE; + + break; + + case UWAC_EVENT_POINTER_MOTION: + if (!wlf_handle_pointer_motion(instance, &event.mouse_motion)) + return FALSE; + + break; + + case UWAC_EVENT_POINTER_BUTTONS: + if (!wlf_handle_pointer_buttons(instance, &event.mouse_button)) + return FALSE; + + break; + + case UWAC_EVENT_POINTER_AXIS: + break; + + case UWAC_EVENT_POINTER_AXIS_DISCRETE: + if (!wlf_handle_pointer_axis(instance, &event.mouse_axis)) + return FALSE; + + break; + + case UWAC_EVENT_KEY: + if (!wlf_handle_key(instance, &event.key)) + return FALSE; + + break; + + case UWAC_EVENT_TOUCH_UP: + if (!wlf_handle_touch_up(instance, &event.touchUp)) + return FALSE; + + break; + + case UWAC_EVENT_TOUCH_DOWN: + if (!wlf_handle_touch_down(instance, &event.touchDown)) + return FALSE; + + break; + + case UWAC_EVENT_TOUCH_MOTION: + if (!wlf_handle_touch_motion(instance, &event.touchMotion)) + return FALSE; + + break; + + case UWAC_EVENT_KEYBOARD_ENTER: + if (instance->context->settings->GrabKeyboard) + UwacSeatInhibitShortcuts(event.keyboard_enter_leave.seat, true); + + if (!wlf_keyboard_enter(instance, &event.keyboard_enter_leave)) + return FALSE; + + break; + + case UWAC_EVENT_KEYBOARD_MODIFIERS: + if (!wlf_keyboard_modifiers(instance, &event.keyboard_modifiers)) + return FALSE; + + break; + + case UWAC_EVENT_CONFIGURE: + if (!wlf_disp_handle_configure(context->disp, event.configure.width, + event.configure.height)) + return FALSE; + + if (!wl_refresh_display(context)) + return FALSE; + + break; + + case UWAC_EVENT_CLIPBOARD_AVAILABLE: + case UWAC_EVENT_CLIPBOARD_OFFER: + case UWAC_EVENT_CLIPBOARD_SELECT: + if (!wlf_cliprdr_handle_event(context->clipboard, &event.clipboard)) + return FALSE; + + break; + + case UWAC_EVENT_CLOSE: + context->closed = TRUE; + + break; + + default: + break; + } + } + + return TRUE; +} + +static BOOL handle_window_events(freerdp* instance) +{ + rdpSettings* settings; + + if (!instance || !instance->settings) + return FALSE; + + settings = instance->settings; + + if (!settings->AsyncInput) + { + } + + return TRUE; +} + +static int wlfreerdp_run(freerdp* instance) +{ + wlfContext* context; + DWORD count; + HANDLE handles[64]; + DWORD status = WAIT_ABANDONED; + + if (!instance) + return -1; + + context = (wlfContext*)instance->context; + + if (!context) + return -1; + + if (!freerdp_connect(instance)) + { + WLog_Print(context->log, WLOG_ERROR, "Failed to connect"); + return -1; + } + + while (!freerdp_shall_disconnect(instance)) + { + handles[0] = context->displayHandle; + count = freerdp_get_event_handles(instance->context, &handles[1], 63) + 1; + + if (count <= 1) + { + WLog_Print(context->log, WLOG_ERROR, "Failed to get FreeRDP file descriptor"); + break; + } + + status = WaitForMultipleObjects(count, handles, FALSE, INFINITE); + + if (WAIT_FAILED == status) + { + WLog_Print(context->log, WLOG_ERROR, "%s: WaitForMultipleObjects failed", __FUNCTION__); + break; + } + + if (!handle_uwac_events(instance, context->display)) + { + WLog_Print(context->log, WLOG_ERROR, "error handling UWAC events"); + break; + } + + if (context->closed) + { + WLog_Print(context->log, WLOG_INFO, "Closed from Wayland"); + break; + } + + if (freerdp_check_event_handles(instance->context) != TRUE) + { + if (client_auto_reconnect_ex(instance, handle_window_events)) + continue; + else + { + /* + * Indicate an unsuccessful connection attempt if reconnect + * did not succeed and no other error was specified. + */ + if (freerdp_error_info(instance) == 0) + status = 42; + } + + if (freerdp_get_last_error(instance->context) == FREERDP_ERROR_SUCCESS) + WLog_Print(context->log, WLOG_ERROR, "Failed to check FreeRDP file descriptor"); + + break; + } + } + + freerdp_disconnect(instance); + return status; +} + +static BOOL wlf_client_global_init(void) +{ + setlocale(LC_ALL, ""); + + if (freerdp_handle_signals() != 0) + return FALSE; + + return TRUE; +} + +static void wlf_client_global_uninit(void) +{ +} + +static int wlf_logon_error_info(freerdp* instance, UINT32 data, UINT32 type) +{ + wlfContext* wlf; + const char* str_data = freerdp_get_logon_error_info_data(data); + const char* str_type = freerdp_get_logon_error_info_type(type); + + if (!instance || !instance->context) + return -1; + + wlf = (wlfContext*)instance->context; + WLog_Print(wlf->log, WLOG_INFO, "Logon Error Info %s [%s]", str_data, str_type); + return 1; +} + +static BOOL wlf_client_new(freerdp* instance, rdpContext* context) +{ + UwacReturnCode status; + wlfContext* wfl = (wlfContext*)context; + + if (!instance || !context) + return FALSE; + + instance->PreConnect = wl_pre_connect; + instance->PostConnect = wl_post_connect; + instance->PostDisconnect = wl_post_disconnect; + instance->Authenticate = client_cli_authenticate; + instance->GatewayAuthenticate = client_cli_gw_authenticate; + instance->VerifyCertificateEx = client_cli_verify_certificate_ex; + instance->VerifyChangedCertificateEx = client_cli_verify_changed_certificate_ex; + instance->PresentGatewayMessage = client_cli_present_gateway_message; + instance->LogonErrorInfo = wlf_logon_error_info; + wfl->log = WLog_Get(TAG); + wfl->display = UwacOpenDisplay(NULL, &status); + + if (!wfl->display || (status != UWAC_SUCCESS) || !wfl->log) + return FALSE; + + wfl->displayHandle = CreateFileDescriptorEvent(NULL, FALSE, FALSE, + UwacDisplayGetFd(wfl->display), WINPR_FD_READ); + + if (!wfl->displayHandle) + return FALSE; + + InitializeCriticalSection(&wfl->critical); + + return TRUE; +} + +static void wlf_client_free(freerdp* instance, rdpContext* context) +{ + wlfContext* wlf = (wlfContext*)instance->context; + + if (!context) + return; + + if (wlf->display) + UwacCloseDisplay(&wlf->display); + + if (wlf->displayHandle) + CloseHandle(wlf->displayHandle); + DeleteCriticalSection(&wlf->critical); +} + +static int wfl_client_start(rdpContext* context) +{ + WINPR_UNUSED(context); + return 0; +} + +static int wfl_client_stop(rdpContext* context) +{ + WINPR_UNUSED(context); + return 0; +} + +static int RdpClientEntry(RDP_CLIENT_ENTRY_POINTS* pEntryPoints) +{ + ZeroMemory(pEntryPoints, sizeof(RDP_CLIENT_ENTRY_POINTS)); + pEntryPoints->Version = RDP_CLIENT_INTERFACE_VERSION; + pEntryPoints->Size = sizeof(RDP_CLIENT_ENTRY_POINTS_V1); + pEntryPoints->GlobalInit = wlf_client_global_init; + pEntryPoints->GlobalUninit = wlf_client_global_uninit; + pEntryPoints->ContextSize = sizeof(wlfContext); + pEntryPoints->ClientNew = wlf_client_new; + pEntryPoints->ClientFree = wlf_client_free; + pEntryPoints->ClientStart = wfl_client_start; + pEntryPoints->ClientStop = wfl_client_stop; + return 0; +} + +int main(int argc, char* argv[]) +{ + int rc = -1; + int status; + RDP_CLIENT_ENTRY_POINTS clientEntryPoints; + rdpContext* context; + rdpSettings* settings; + wlfContext* wlc; + + RdpClientEntry(&clientEntryPoints); + context = freerdp_client_context_new(&clientEntryPoints); + if (!context) + goto fail; + wlc = (wlfContext*)context; + settings = context->settings; + + status = freerdp_client_settings_parse_command_line(settings, argc, argv, FALSE); + if (status) + { + BOOL list = settings->ListMonitors; + + rc = freerdp_client_settings_command_line_status_print(settings, status, argc, argv); + + if (list) + wlf_list_monitors(wlc); + + goto fail; + } + + if (freerdp_client_start(context) != 0) + goto fail; + + rc = wlfreerdp_run(context->instance); + + if (freerdp_client_stop(context) != 0) + rc = -1; + +fail: + freerdp_client_context_free(context); + return rc; +} + +BOOL wlf_copy_image(const void* src, size_t srcStride, size_t srcWidth, size_t srcHeight, void* dst, + size_t dstStride, size_t dstWidth, size_t dstHeight, const RECTANGLE_16* area, + BOOL scale) +{ + BOOL rc = FALSE; + + if (!src || !dst || !area) + return FALSE; + + if (scale) + { + return freerdp_image_scale(dst, PIXEL_FORMAT_BGRA32, dstStride, 0, 0, dstWidth, dstHeight, + src, PIXEL_FORMAT_BGRA32, srcStride, 0, 0, srcWidth, srcHeight); + } + else + { + size_t i; + const size_t baseSrcOffset = area->top * srcStride + area->left * 4; + const size_t baseDstOffset = area->top * dstStride + area->left * 4; + const size_t width = MIN((size_t)area->right - area->left, dstWidth - area->left); + const size_t height = MIN((size_t)area->bottom - area->top, dstHeight - area->top); + const BYTE* psrc = (const BYTE*)src; + BYTE* pdst = (BYTE*)dst; + + for (i = 0; i < height; i++) + { + const size_t srcOffset = i * srcStride + baseSrcOffset; + const size_t dstOffset = i * dstStride + baseDstOffset; + memcpy(&pdst[dstOffset], &psrc[srcOffset], width * 4); + } + + rc = TRUE; + } + + return rc; +} + +BOOL wlf_scale_coordinates(rdpContext* context, UINT32* px, UINT32* py, BOOL fromLocalToRDP) +{ + wlfContext* wlf = (wlfContext*)context; + rdpGdi* gdi; + UwacSize geometry; + double sx, sy; + + if (!context || !px || !py || !context->gdi) + return FALSE; + + if (!context->settings->SmartSizing) + return TRUE; + + gdi = context->gdi; + + if (UwacWindowGetDrawingBufferGeometry(wlf->window, &geometry, NULL) != UWAC_SUCCESS) + return FALSE; + + sx = geometry.width / (double)gdi->width; + sy = geometry.height / (double)gdi->height; + + if (!fromLocalToRDP) + { + *px *= sx; + *py *= sy; + } + else + { + *px /= sx; + *py /= sy; + } + + return TRUE; +} diff --git a/client/Wayland/wlfreerdp.h b/client/Wayland/wlfreerdp.h new file mode 100644 index 0000000..d84f91c --- /dev/null +++ b/client/Wayland/wlfreerdp.h @@ -0,0 +1,63 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Wayland Client + * + * Copyright 2014 Manuel Bachmann + * + * Licensed 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 FREERDP_CLIENT_WAYLAND_FREERDP_H +#define FREERDP_CLIENT_WAYLAND_FREERDP_H + +#include +#include +#include +#include +#include +#include +#include + +typedef struct wlf_context wlfContext; +typedef struct wlf_clipboard wfClipboard; +typedef struct _wlfDispContext wlfDispContext; + +struct wlf_context +{ + rdpContext context; + + UwacDisplay* display; + HANDLE displayHandle; + UwacWindow* window; + UwacSeat* seat; + + BOOL fullscreen; + BOOL closed; + BOOL focusing; + + /* Channels */ + RdpeiClientContext* rdpei; + RdpgfxClientContext* gfx; + EncomspClientContext* encomsp; + wfClipboard* clipboard; + wlfDispContext* disp; + wLog* log; + CRITICAL_SECTION critical; +}; + +BOOL wlf_scale_coordinates(rdpContext* context, UINT32* px, UINT32* py, BOOL fromLocalToRDP); +BOOL wlf_copy_image(const void* src, size_t srcStride, size_t srcWidth, size_t srcHeight, void* dst, + size_t dstStride, size_t dstWidth, size_t dstHeight, const RECTANGLE_16* area, + BOOL scale); + +#endif /* FREERDP_CLIENT_WAYLAND_FREERDP_H */ diff --git a/client/Windows/CMakeLists.txt b/client/Windows/CMakeLists.txt new file mode 100644 index 0000000..6274571 --- /dev/null +++ b/client/Windows/CMakeLists.txt @@ -0,0 +1,98 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP Windows cmake build script +# +# Copyright 2012 Marc-Andre Moreau +# +# Licensed 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. + +set(MODULE_NAME "wfreerdp-client") +set(MODULE_PREFIX "FREERDP_CLIENT_WINDOWS_CONTROL") + +set(${MODULE_PREFIX}_SRCS + wf_gdi.c + wf_gdi.h + wf_event.c + wf_event.h + wf_channels.c + wf_channels.h + wf_graphics.c + wf_graphics.h + wf_cliprdr.c + wf_cliprdr.h + wf_rail.c + wf_rail.h + wf_client.c + wf_client.h + wf_floatbar.c + wf_floatbar.h + wfreerdp.rc + resource.h) + +# On windows create dll version information. +# Vendor, product and year are already set in top level CMakeLists.txt +if (WIN32 AND BUILD_SHARED_LIBS) + set (RC_VERSION_MAJOR ${FREERDP_VERSION_MAJOR}) + set (RC_VERSION_MINOR ${FREERDP_VERSION_MINOR}) + set (RC_VERSION_BUILD ${FREERDP_VERSION_REVISION}) + if(WITH_CLIENT_INTERFACE) + set (RC_VERSION_FILE "${CMAKE_SHARED_LIBRARY_PREFIX}${MODULE_NAME}${CMAKE_SHARED_LIBRARY_SUFFIX}" ) + else() + set (RC_VERSION_FILE "${MODULE_NAME}${CMAKE_EXECUTABLE_SUFFIX}" ) + endif() + + configure_file( + ${CMAKE_SOURCE_DIR}/cmake/WindowsDLLVersion.rc.in + ${CMAKE_CURRENT_BINARY_DIR}/version.rc + @ONLY) + + set ( ${MODULE_PREFIX}_SRCS ${${MODULE_PREFIX}_SRCS} ${CMAKE_CURRENT_BINARY_DIR}/version.rc) +endif() + + +if(WITH_CLIENT_INTERFACE) + if(CLIENT_INTERFACE_SHARED) + add_library(${MODULE_NAME} SHARED ${${MODULE_PREFIX}_SRCS}) + else() + add_library(${MODULE_NAME} ${${MODULE_PREFIX}_SRCS}) + endif() + if (WITH_LIBRARY_VERSIONING) + set_target_properties(${MODULE_NAME} PROPERTIES VERSION ${FREERDP_VERSION} SOVERSION ${FREERDP_API_VERSION}) + endif() + +else() + set(${MODULE_PREFIX}_SRCS ${${MODULE_PREFIX}_SRCS} cli/wfreerdp.c cli/wfreerdp.h) + add_executable(${MODULE_NAME} WIN32 ${${MODULE_PREFIX}_SRCS}) + include_directories(${CMAKE_CURRENT_SOURCE_DIR}) + set_target_properties(${MODULE_NAME} PROPERTIES OUTPUT_NAME "wfreerdp") +endif() + +set(${MODULE_PREFIX}_LIBS ${${MODULE_PREFIX}_LIBS} freerdp-client) +set(${MODULE_PREFIX}_LIBS ${${MODULE_PREFIX}_LIBS} winpr freerdp) +set(${MODULE_PREFIX}_LIBS ${${MODULE_PREFIX}_LIBS} msimg32) +target_link_libraries(${MODULE_NAME} ${${MODULE_PREFIX}_LIBS}) + +if(WITH_CLIENT_INTERFACE) + install(TARGETS ${MODULE_NAME} DESTINATION ${CMAKE_INSTALL_LIBDIR} COMPONENT libraries) + if (WITH_DEBUG_SYMBOLS AND MSVC AND BUILD_SHARED_LIBS) + install(FILES ${CMAKE_PDB_BINARY_DIR}/${MODULE_NAME}.pdb DESTINATION ${CMAKE_INSTALL_LIBDIR} COMPONENT symbols) + endif() + add_subdirectory(cli) +else() + install(TARGETS ${MODULE_NAME} DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT client) + if (WITH_DEBUG_SYMBOLS AND MSVC) + get_target_property(OUTPUT_FILENAME ${MODULE_NAME} OUTPUT_NAME) + install(FILES ${CMAKE_PDB_BINARY_DIR}/${OUTPUT_FILENAME}.pdb DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT symbols) + endif() +endif() + +set_property(TARGET ${MODULE_NAME} PROPERTY FOLDER "Client/Windows") diff --git a/client/Windows/FreeRDP.ico b/client/Windows/FreeRDP.ico new file mode 100644 index 0000000000000000000000000000000000000000..0864d1cdaa2396eb23c5667eead61b3d548999dc GIT binary patch literal 30160 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 M<8nS#V_JvwKd)_Y9smFU literal 0 HcmV?d00001 diff --git a/client/Windows/ModuleOptions.cmake b/client/Windows/ModuleOptions.cmake new file mode 100644 index 0000000..a0fcaec --- /dev/null +++ b/client/Windows/ModuleOptions.cmake @@ -0,0 +1,4 @@ + +set(FREERDP_CLIENT_NAME "wfreerdp") +set(FREERDP_CLIENT_PLATFORM "Windows") +set(FREERDP_CLIENT_VENDOR "FreeRDP") diff --git a/client/Windows/cli/CMakeLists.txt b/client/Windows/cli/CMakeLists.txt new file mode 100644 index 0000000..0272611 --- /dev/null +++ b/client/Windows/cli/CMakeLists.txt @@ -0,0 +1,54 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP Windows cmake build script +# +# Copyright 2012 Marc-Andre Moreau +# +# Licensed 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. + +set(MODULE_NAME "wfreerdp") +set(MODULE_PREFIX "FREERDP_CLIENT_WINDOWS") + +include_directories(..) + +set(${MODULE_PREFIX}_SRCS + wfreerdp.c + wfreerdp.h + ../wfreerdp.rc) + +# On windows create dll version information. +# Vendor, product and year are already set in top level CMakeLists.txt +if (WIN32) + set (RC_VERSION_MAJOR ${FREERDP_VERSION_MAJOR}) + set (RC_VERSION_MINOR ${FREERDP_VERSION_MINOR}) + set (RC_VERSION_BUILD ${FREERDP_VERSION_REVISION}) + set (RC_VERSION_FILE "${MODULE_NAME}${CMAKE_EXECUTABLE_SUFFIX}" ) + + configure_file( + ${CMAKE_SOURCE_DIR}/cmake/WindowsDLLVersion.rc.in + ${CMAKE_CURRENT_BINARY_DIR}/version.rc + @ONLY) + + set ( ${MODULE_PREFIX}_SRCS ${${MODULE_PREFIX}_SRCS} ${CMAKE_CURRENT_BINARY_DIR}/version.rc) +endif() +add_executable(${MODULE_NAME} WIN32 ${${MODULE_PREFIX}_SRCS}) + +set(${MODULE_PREFIX}_LIBS ${${MODULE_PREFIX}_LIBS} wfreerdp-client) + +target_link_libraries(${MODULE_NAME} ${${MODULE_PREFIX}_LIBS}) + +install(TARGETS ${MODULE_NAME} RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT client) +if (WITH_DEBUG_SYMBOLS AND MSVC) + install(FILES ${CMAKE_PDB_BINARY_DIR}/${MODULE_NAME}.pdb DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT symbols) +endif() + +set_property(TARGET ${MODULE_NAME} PROPERTY FOLDER "Client/Windows") diff --git a/client/Windows/cli/wfreerdp.c b/client/Windows/cli/wfreerdp.c new file mode 100644 index 0000000..e325f84 --- /dev/null +++ b/client/Windows/cli/wfreerdp.c @@ -0,0 +1,144 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Windows Client + * + * Copyright 2009-2011 Jay Sorg + * Copyright 2010-2011 Vic Lee + * Copyright 2010-2011 Marc-Andre Moreau + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include + +#include +#include + +#include +#include + +#include +#include +#include +#include + +#include "resource.h" + +#include "wf_client.h" + +#include + +INT WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) +{ + int status; + HANDLE thread; + wfContext* wfc; + DWORD dwExitCode; + rdpContext* context; + rdpSettings* settings; + LPWSTR cmd; + char** argv = NULL; + RDP_CLIENT_ENTRY_POINTS clientEntryPoints = { 0 }; + int ret = 1; + int argc = 0, i; + LPWSTR* args = NULL; + + WINPR_UNUSED(hInstance); + WINPR_UNUSED(hPrevInstance); + WINPR_UNUSED(lpCmdLine); + WINPR_UNUSED(nCmdShow); + + RdpClientEntry(&clientEntryPoints); + context = freerdp_client_context_new(&clientEntryPoints); + + if (!context) + return -1; + + cmd = GetCommandLineW(); + + if (!cmd) + goto out; + + args = CommandLineToArgvW(cmd, &argc); + + if (!args || (argc <= 0)) + goto out; + + argv = calloc((size_t)argc, sizeof(char*)); + + if (!argv) + goto out; + + for (i = 0; i < argc; i++) + { + int size = WideCharToMultiByte(CP_UTF8, 0, args[i], -1, NULL, 0, NULL, NULL); + if (size <= 0) + goto out; + argv[i] = calloc((size_t)size, sizeof(char)); + + if (!argv[i]) + goto out; + + if (WideCharToMultiByte(CP_UTF8, 0, args[i], -1, argv[i], size, NULL, NULL) != size) + goto out; + } + + settings = context->settings; + wfc = (wfContext*)context; + + if (!settings || !wfc) + goto out; + + status = freerdp_client_settings_parse_command_line(settings, argc, argv, FALSE); + + if (status) + { + ret = freerdp_client_settings_command_line_status_print(settings, status, argc, argv); + goto out; + } + + if (freerdp_client_start(context) != 0) + goto out; + + thread = freerdp_client_get_thread(context); + + if (thread) + { + if (WaitForSingleObject(thread, INFINITE) == WAIT_OBJECT_0) + { + GetExitCodeThread(thread, &dwExitCode); + ret = (int)dwExitCode; + } + } + + if (freerdp_client_stop(context) != 0) + goto out; + +out: + freerdp_client_context_free(context); + + if (argv) + { + for (i = 0; i < argc; i++) + free(argv[i]); + + free(argv); + } + + LocalFree(args); + return ret; +} diff --git a/client/Windows/cli/wfreerdp.h b/client/Windows/cli/wfreerdp.h new file mode 100644 index 0000000..2bb57bc --- /dev/null +++ b/client/Windows/cli/wfreerdp.h @@ -0,0 +1,27 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Windows Client + * + * Copyright 2009-2011 Jay Sorg + * Copyright 2010-2011 Vic Lee + * Copyright 2010-2011 Marc-Andre Moreau + * + * Licensed 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 FREERDP_CLIENT_WIN_FREERDP_H +#define FREERDP_CLIENT_WIN_FREERDP_H + +#include "wf_interface.h" + +#endif /* FREERDP_CLIENT_WIN_FREERDP_H */ diff --git a/client/Windows/resource.h b/client/Windows/resource.h new file mode 100644 index 0000000..35991fc --- /dev/null +++ b/client/Windows/resource.h @@ -0,0 +1,12 @@ + +#define IDI_ICON1 101 +#define IDB_MINIMIZE 103 +#define IDB_MINIMIZE_ACT 104 +#define IDB_LOCK 105 +#define IDB_LOCK_ACT 106 +#define IDB_UNLOCK 107 +#define IDB_UNLOCK_ACT 108 +#define IDB_CLOSE 109 +#define IDB_CLOSE_ACT 100 +#define IDB_RESTORE 111 +#define IDB_RESTORE_ACT 112 diff --git a/client/Windows/resource/close.bmp b/client/Windows/resource/close.bmp new file mode 100644 index 0000000000000000000000000000000000000000..bb17b287f1d255da9691d40e3df4c057cdc9ed15 GIT binary patch literal 1986 zcmZ?rJ;crc24+A~1Bk_eSQLmE85Dpd149o`{s{OT- zh(;DCl?~GW`{(!nKfZkdn?o&gVea_(?bH7sUq8bAK&g9S=ECUjpWeXDhKW&w{`T?J ze-Qrt<11Co|M}&^{~theK^WvtYK1q*oNpgr|Nr?3VlFHUe&a|#F!@n>Gz11q2mk<% CV3c|Q literal 0 HcmV?d00001 diff --git a/client/Windows/resource/close_active.bmp b/client/Windows/resource/close_active.bmp new file mode 100644 index 0000000000000000000000000000000000000000..a59b2e341328ee28a80e900bbacb8022f929f272 GIT binary patch literal 1986 zcmb`FZAep57{}MAzL=3DeeGi(Dv_C45yFTFVo}uC2P3s+f~AgZ=w`c_M)YELALAvW=^GZ*Kes%Qf$3Nz5o9Gq0Ihhc}bmpH^D zG{we4Tj8;v$4Gbc3TlC`s~JPiCQ09r!;IcrMp%#Jp;42IGr~T-x9i}w)v~=(@3Wfm z)2jw}@7AMw_eNF+I?HM}ayh>4%c$IyU=KPkDasyjSQ)!4KMOB9n-pdDx0@NeT%8SX z2d608E&NZhCeQ9&kYxY)9fhdW+KO`&Q|HC;4yyT@e+-ZS9odxdLHq7)n)3V9G9Ojd3`cBto z`ntgB@?h0%$HxIzT6QG#6Qp=_DI@;4_W{lwEdZy_$7laTS$1r30a1ZVqO)dF@2SBa znDj-sQo09UMxRKsi4zIUBNk3@#X7`g-jBE*c*sHbRgOiG~iAP+gNr!Lme3%bT zD8eTW@rdiWQHdT~9p(alEPS12;%6toe|m#oGNLtp_HEKH=20XL@rX-r;JLj9F0K?E iCOz7WhtXE6MY}P*$U|PranzT<(Qur}#g&10#Qh5hxGdQK literal 0 HcmV?d00001 diff --git a/client/Windows/resource/lock.bmp b/client/Windows/resource/lock.bmp new file mode 100644 index 0000000000000000000000000000000000000000..0442c027e25a19fb46ef4594c1eb736319e65511 GIT binary patch literal 1986 zcmZ?rJ;crc24+A~1Bk_eSQLmE85Dpd149o`{s2ZWoMCirFwd#L~mXY>+>Q^)o(o bAajtz1i5~I$x%YX%5ESH0`Rh&ifje|U%-b1 literal 0 HcmV?d00001 diff --git a/client/Windows/resource/lock_active.bmp b/client/Windows/resource/lock_active.bmp new file mode 100644 index 0000000000000000000000000000000000000000..2e37e36e86225c29072b0d2735ecbac8e0660311 GIT binary patch literal 1986 zcmb`|-%C?r7zgmsx$b$59dA4`#JC49hg$(yTI_D z3~s!_%(Nr7n zr^02egLk+!O3d&KwPL~Ed~V-@&48(19ZdOYXjG=)3HO|-m$h(PYM{@R|99SXXyJZc zkE*O>{v2p8Qn-TQ8E!+x&0~As-Xf_zXEX73v0R4P!DdPAnSKLrmnah89$+Kd3;>Go zougGJ_j?Lw^8QUz#C759aPt#A)u->X^OaFj`)$`#Xw&0)-#228$rko0uHrLV95h0! zkb!-06&us`Gq!kbv}>`wF+BuUod%Qo8h*?<&e-DKzqMD~qj*-34pt+_Pv2NfcDQGX z=hx=Pq1R-hz33KxElo;l2R=H{P}`IQ)D8xg@x1IFtW_$6f~%6+izbD76BQ(f=tm SdsP2u2#kinXb2335C8x}usj0* literal 0 HcmV?d00001 diff --git a/client/Windows/resource/minimize_active.bmp b/client/Windows/resource/minimize_active.bmp new file mode 100644 index 0000000000000000000000000000000000000000..e16b252a776c6bc1b901d6098a56a9fd2f4eda3c GIT binary patch literal 1986 zcmb`{%S#(U90%}t>P0OTrPm%k*owqwr3j&jP$EhjaTQ4keGFoOTV*ZqxEdT^u2EPQ8{-QRp>c6M2X()~!rj##k` z7xPq1n3w?BGqIJ)+Qj=y!B=4y{x$lf5ER8fL4NE#5*LD8`D18eZm%VWJff+uNsWNR zT#1Fj#yipk*FYoYto66n%~|xA=+MHL9*G)77(R&eOnj+;(^SUh`u}+za{le32F|W3 zl)MaO`#@`Xh$A(|HQ0>8ge!a2+Q8MGu^5@19TkY_{(7$VRIi@dIr3mQ`vgfFeh#TU zv5W8XdQZOh)N-}Q+DmV~XM3kX(w17Azw4u)i=l}QV!nH*iK{)|Z-7P~2;tpx>`Yp@ z+B=g25LA&cs$SsljE$?k=KPBCx6u&PQP_76bG0{TN1#*3qa`a52dm>;ZO@_|)funw zDK81f8xF4a_UaT2nF_QOyvB)Vo~!+5a{#=3-Sh4Jx~ab&M?E>@ tkxT!DALdfnge>&w)acfxp}S0pUakL74b{>ZOH~Fe)$dtsLN>@F_Y%GN)KdTe literal 0 HcmV?d00001 diff --git a/client/Windows/resource/restore.bmp b/client/Windows/resource/restore.bmp new file mode 100644 index 0000000000000000000000000000000000000000..26117b0b1d3595e966fd36ed6f6516834f574748 GIT binary patch literal 1986 zcmeH@F$#b%3`OJQ<|eL=p1=dRdoxcZ-b<58md_VL3Ai+nwE6vclx+LiONmwMqB&`f znw`vAMGk$xhBu4QO4~rf2JVkYK#QS~!!-0ndd6W|goPf)VH(awdd6W|goPgCurQzN lI1}xC9#5Y)-S63F7MpRHR`Y-3u&80-4C9)HzSjS@fd_1Xvx@)# literal 0 HcmV?d00001 diff --git a/client/Windows/resource/restore_active.bmp b/client/Windows/resource/restore_active.bmp new file mode 100644 index 0000000000000000000000000000000000000000..c2479baabd41b9a03005a82068c1d78e1ffacc84 GIT binary patch literal 1986 zcmb`F?@NN6iTJH(|ot$jzJZ*fHF0h4Wel>J6#5Bi9-3s6o(G&1(Jop2N)FwpbAC zszv3wyyP=0oYJ??#|DSOQ0WbBg6M11IL)RaBiV`uDAeKLZ*5|<5u ziF%>L{qpxzdh+ghgOIJ{E1t?JbCG}jtZMIn_IEnC`8*CUrmpeo$z52ok{CbyqFGb+ zWS;{TeIlgm=~x}{YRX<6>4#*>fYWpu-^P4GHuXSl)B`1^?gyqb1ohipU026H{2Zc8TDGkj!gSeS9k%fxAL&D6o|{_ad2Y vLpgcvDjEql3YXcq z#Nir=VBj(XmpBnd!j-_xz(vCq62ZV_1}-`^PKWMv$y{lqfGlckT3Aa>yf^`i6Tc za9e9JJKTImTHqUM#%z0^OP$l9izmBV`1|CZlTkH7$iRSmmmpXWQ z>QHebft>^GB^pO+j&HaXW!DevDf6u5)gJFRGP_tA3(o)-*2du_ zbq3;bC;vW$Gx^ll7?mxx=KGGyqwm?-ZIab~_xc%h8JC#vf7=p~E$ma=i_c_c&;*?_ z2HeA|SoPXZ*y6co-HYv2?+`dm8qAthY)m>%*y7sr+TT4RsD6+EPMeNj{?UkRsWrvp z%Tr@8XtU8)coV-DKFDeZ=A5X{zlB$ibFjVSj>;DASGZqU7>B7qh4v@6u@jgHYYTHC z?bnxQVJ^M{OZi>wu6>o&4hFvAd1)@J6>5ZnOS0PJk!v?-@b{+=|ALFM_~ejBuKP_H z#w<114*0S6XN4uSwTRH>JpN0Bo`tr49Muc+s3(U!a_RkiwpPNy6=J}kMX&xodaKmv Z*M|?)P%X`|)#bxh|A@uG6@ff*_W|PWTiO5s literal 0 HcmV?d00001 diff --git a/client/Windows/wf_channels.c b/client/Windows/wf_channels.c new file mode 100644 index 0000000..3afd52d --- /dev/null +++ b/client/Windows/wf_channels.c @@ -0,0 +1,85 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * + * Copyright 2014 Marc-Andre Moreau + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "wf_channels.h" + +#include "wf_rail.h" +#include "wf_cliprdr.h" + +#include + +#include +#define TAG CLIENT_TAG("windows") + +void wf_OnChannelConnectedEventHandler(void* context, ChannelConnectedEventArgs* e) +{ + wfContext* wfc = (wfContext*)context; + rdpSettings* settings = wfc->context.settings; + + if (strcmp(e->name, RDPEI_DVC_CHANNEL_NAME) == 0) + { + } + else if (strcmp(e->name, RDPGFX_DVC_CHANNEL_NAME) == 0) + { + if (!settings->SoftwareGdi) + WLog_WARN(TAG, "Channel " RDPGFX_DVC_CHANNEL_NAME + " does not support hardware acceleration, using fallback."); + + gdi_graphics_pipeline_init(wfc->context.gdi, (RdpgfxClientContext*)e->pInterface); + } + else if (strcmp(e->name, RAIL_SVC_CHANNEL_NAME) == 0) + { + wf_rail_init(wfc, (RailClientContext*)e->pInterface); + } + else if (strcmp(e->name, CLIPRDR_SVC_CHANNEL_NAME) == 0) + { + wf_cliprdr_init(wfc, (CliprdrClientContext*)e->pInterface); + } + else if (strcmp(e->name, ENCOMSP_SVC_CHANNEL_NAME) == 0) + { + } +} + +void wf_OnChannelDisconnectedEventHandler(void* context, ChannelDisconnectedEventArgs* e) +{ + wfContext* wfc = (wfContext*)context; + rdpSettings* settings = wfc->context.settings; + + if (strcmp(e->name, RDPEI_DVC_CHANNEL_NAME) == 0) + { + } + else if (strcmp(e->name, RDPGFX_DVC_CHANNEL_NAME) == 0) + { + gdi_graphics_pipeline_uninit(wfc->context.gdi, (RdpgfxClientContext*)e->pInterface); + } + else if (strcmp(e->name, RAIL_SVC_CHANNEL_NAME) == 0) + { + wf_rail_uninit(wfc, (RailClientContext*)e->pInterface); + } + else if (strcmp(e->name, CLIPRDR_SVC_CHANNEL_NAME) == 0) + { + wf_cliprdr_uninit(wfc, (CliprdrClientContext*)e->pInterface); + } + else if (strcmp(e->name, ENCOMSP_SVC_CHANNEL_NAME) == 0) + { + } +} diff --git a/client/Windows/wf_channels.h b/client/Windows/wf_channels.h new file mode 100644 index 0000000..5e4ca52 --- /dev/null +++ b/client/Windows/wf_channels.h @@ -0,0 +1,34 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * + * Copyright 2014 Marc-Andre Moreau + * + * Licensed 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 FREERDP_CLIENT_WIN_CHANNELS_H +#define FREERDP_CLIENT_WIN_CHANNELS_H + +#include +#include +#include +#include +#include +#include + +#include "wf_client.h" + +void wf_OnChannelConnectedEventHandler(void* context, ChannelConnectedEventArgs* e); +void wf_OnChannelDisconnectedEventHandler(void* context, ChannelDisconnectedEventArgs* e); + +#endif /* FREERDP_CLIENT_WIN_CHANNELS_H */ diff --git a/client/Windows/wf_client.c b/client/Windows/wf_client.c new file mode 100644 index 0000000..5ee0594 --- /dev/null +++ b/client/Windows/wf_client.c @@ -0,0 +1,1164 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Windows Client + * + * Copyright 2009-2011 Jay Sorg + * Copyright 2010-2011 Vic Lee + * Copyright 2010-2011 Marc-Andre Moreau + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "wf_gdi.h" +#include "wf_rail.h" +#include "wf_channels.h" +#include "wf_graphics.h" +#include "wf_cliprdr.h" + +#include "wf_client.h" + +#include "resource.h" + +#define TAG CLIENT_TAG("windows") + +static BOOL wf_create_console(void) +{ +#if defined(WITH_WIN_CONSOLE) + if (!AttachConsole(ATTACH_PARENT_PROCESS)) + return FALSE; + + freopen("CONOUT$", "w", stdout); + freopen("CONOUT$", "w", stderr); + clearerr(stdout); + clearerr(stderr); + fflush(stdout); + fflush(stderr); + + freopen("CONIN$", "r", stdin); + clearerr(stdin); + + WLog_INFO(TAG, "Debug console created."); + + return TRUE; +#else + return FALSE; +#endif +} + +static BOOL wf_end_paint(rdpContext* context) +{ + int i; + rdpGdi* gdi; + int ninvalid; + RECT updateRect; + HGDI_RGN cinvalid; + REGION16 invalidRegion; + RECTANGLE_16 invalidRect; + const RECTANGLE_16* extents; + wfContext* wfc = (wfContext*)context; + gdi = context->gdi; + ninvalid = gdi->primary->hdc->hwnd->ninvalid; + cinvalid = gdi->primary->hdc->hwnd->cinvalid; + + if (ninvalid < 1) + return TRUE; + + region16_init(&invalidRegion); + + for (i = 0; i < ninvalid; i++) + { + invalidRect.left = cinvalid[i].x; + invalidRect.top = cinvalid[i].y; + invalidRect.right = cinvalid[i].x + cinvalid[i].w; + invalidRect.bottom = cinvalid[i].y + cinvalid[i].h; + region16_union_rect(&invalidRegion, &invalidRegion, &invalidRect); + } + + if (!region16_is_empty(&invalidRegion)) + { + extents = region16_extents(&invalidRegion); + updateRect.left = extents->left; + updateRect.top = extents->top; + updateRect.right = extents->right; + updateRect.bottom = extents->bottom; + + if (wfc->xScrollVisible) + { + updateRect.left -= MIN(updateRect.left, wfc->xCurrentScroll); + updateRect.right -= MIN(updateRect.right, wfc->xCurrentScroll); + } + if (wfc->yScrollVisible) + { + updateRect.top -= MIN(updateRect.top, wfc->yCurrentScroll); + updateRect.bottom -= MIN(updateRect.bottom, wfc->yCurrentScroll); + } + + InvalidateRect(wfc->hwnd, &updateRect, FALSE); + + if (wfc->rail) + wf_rail_invalidate_region(wfc, &invalidRegion); + } + + region16_uninit(&invalidRegion); + return TRUE; +} + +static BOOL wf_begin_paint(rdpContext* context) +{ + HGDI_DC hdc; + + if (!context || !context->gdi || !context->gdi->primary || !context->gdi->primary->hdc) + return FALSE; + + hdc = context->gdi->primary->hdc; + + if (!hdc || !hdc->hwnd || !hdc->hwnd->invalid) + return FALSE; + + hdc->hwnd->invalid->null = TRUE; + hdc->hwnd->ninvalid = 0; + return TRUE; +} + +static BOOL wf_desktop_resize(rdpContext* context) +{ + BOOL same; + RECT rect; + rdpSettings* settings; + wfContext* wfc = (wfContext*)context; + + if (!context || !context->settings) + return FALSE; + + settings = context->settings; + + if (wfc->primary) + { + same = (wfc->primary == wfc->drawing) ? TRUE : FALSE; + wf_image_free(wfc->primary); + wfc->primary = wf_image_new(wfc, settings->DesktopWidth, settings->DesktopHeight, + context->gdi->dstFormat, NULL); + } + + if (!gdi_resize_ex(context->gdi, settings->DesktopWidth, settings->DesktopHeight, 0, + context->gdi->dstFormat, wfc->primary->pdata, NULL)) + return FALSE; + + if (same) + wfc->drawing = wfc->primary; + + if (wfc->fullscreen != TRUE) + { + if (wfc->hwnd) + SetWindowPos(wfc->hwnd, HWND_TOP, -1, -1, settings->DesktopWidth + wfc->diff.x, + settings->DesktopHeight + wfc->diff.y, SWP_NOMOVE); + } + else + { + wf_update_offset(wfc); + GetWindowRect(wfc->hwnd, &rect); + InvalidateRect(wfc->hwnd, &rect, TRUE); + } + + return TRUE; +} + +static BOOL wf_pre_connect(freerdp* instance) +{ + UINT32 rc; + wfContext* wfc; + int desktopWidth; + int desktopHeight; + rdpContext* context; + rdpSettings* settings; + + if (!instance || !instance->context || !instance->settings) + return FALSE; + + context = instance->context; + wfc = (wfContext*)instance->context; + settings = instance->settings; + settings->OsMajorType = OSMAJORTYPE_WINDOWS; + settings->OsMinorType = OSMINORTYPE_WINDOWS_NT; + wfc->fullscreen = settings->Fullscreen; + wfc->fullscreen_toggle = settings->ToggleFullscreen; + desktopWidth = settings->DesktopWidth; + desktopHeight = settings->DesktopHeight; + + if (wfc->percentscreen > 0) + { + desktopWidth = (GetSystemMetrics(SM_CXSCREEN) * wfc->percentscreen) / 100; + settings->DesktopWidth = desktopWidth; + desktopHeight = (GetSystemMetrics(SM_CYSCREEN) * wfc->percentscreen) / 100; + settings->DesktopHeight = desktopHeight; + } + + if (wfc->fullscreen) + { + if (settings->UseMultimon) + { + desktopWidth = GetSystemMetrics(SM_CXVIRTUALSCREEN); + desktopHeight = GetSystemMetrics(SM_CYVIRTUALSCREEN); + } + else + { + desktopWidth = GetSystemMetrics(SM_CXSCREEN); + desktopHeight = GetSystemMetrics(SM_CYSCREEN); + } + } + + /* FIXME: desktopWidth has a limitation that it should be divisible by 4, + * otherwise the screen will crash when connecting to an XP desktop.*/ + desktopWidth = (desktopWidth + 3) & (~3); + + if (desktopWidth != settings->DesktopWidth) + { + freerdp_set_param_uint32(settings, FreeRDP_DesktopWidth, desktopWidth); + } + + if (desktopHeight != settings->DesktopHeight) + { + freerdp_set_param_uint32(settings, FreeRDP_DesktopHeight, desktopHeight); + } + + if ((settings->DesktopWidth < 64) || (settings->DesktopHeight < 64) || + (settings->DesktopWidth > 4096) || (settings->DesktopHeight > 4096)) + { + WLog_ERR(TAG, "invalid dimensions %lu %lu", settings->DesktopWidth, + settings->DesktopHeight); + return FALSE; + } + + if (!freerdp_client_load_addins(context->channels, instance->settings)) + return -1; + + rc = freerdp_keyboard_init(freerdp_settings_get_uint32(settings, FreeRDP_KeyboardLayout)); + freerdp_set_param_uint32(settings, FreeRDP_KeyboardLayout, rc); + PubSub_SubscribeChannelConnected(instance->context->pubSub, wf_OnChannelConnectedEventHandler); + PubSub_SubscribeChannelDisconnected(instance->context->pubSub, + wf_OnChannelDisconnectedEventHandler); + return TRUE; +} + +static void wf_add_system_menu(wfContext* wfc) +{ + HMENU hMenu; + MENUITEMINFO item_info; + + if (wfc->fullscreen && !wfc->fullscreen_toggle) + { + return; + } + + hMenu = GetSystemMenu(wfc->hwnd, FALSE); + ZeroMemory(&item_info, sizeof(MENUITEMINFO)); + item_info.fMask = MIIM_CHECKMARKS | MIIM_FTYPE | MIIM_ID | MIIM_STRING | MIIM_DATA; + item_info.cbSize = sizeof(MENUITEMINFO); + item_info.wID = SYSCOMMAND_ID_SMARTSIZING; + item_info.fType = MFT_STRING; + item_info.dwTypeData = _wcsdup(_T("Smart sizing")); + item_info.cch = (UINT)_wcslen(_T("Smart sizing")); + item_info.dwItemData = (ULONG_PTR)wfc; + InsertMenuItem(hMenu, 6, TRUE, &item_info); + + if (wfc->context.settings->SmartSizing) + { + CheckMenuItem(hMenu, SYSCOMMAND_ID_SMARTSIZING, MF_CHECKED); + } +} + +static WCHAR* wf_window_get_title(rdpSettings* settings) +{ + BOOL port; + WCHAR* windowTitle = NULL; + size_t size; + char* name; + WCHAR prefix[] = L"FreeRDP:"; + + if (!settings) + return NULL; + + name = settings->ServerHostname; + + if (settings->WindowTitle) + { + ConvertToUnicode(CP_UTF8, 0, settings->WindowTitle, -1, &windowTitle, 0); + return windowTitle; + } + + port = (settings->ServerPort != 3389); + size = strlen(name) + 16 + wcslen(prefix); + windowTitle = calloc(size, sizeof(WCHAR)); + + if (!windowTitle) + return NULL; + + if (!port) + _snwprintf_s(windowTitle, size, _TRUNCATE, L"%s %S", prefix, name); + else + _snwprintf_s(windowTitle, size, _TRUNCATE, L"%s %S:%u", prefix, name, settings->ServerPort); + + return windowTitle; +} + +static BOOL wf_post_connect(freerdp* instance) +{ + rdpGdi* gdi; + DWORD dwStyle; + rdpCache* cache; + wfContext* wfc; + rdpContext* context; + rdpSettings* settings; + EmbedWindowEventArgs e; + const UINT32 format = PIXEL_FORMAT_BGRX32; + settings = instance->settings; + context = instance->context; + wfc = (wfContext*)instance->context; + cache = instance->context->cache; + wfc->primary = wf_image_new(wfc, settings->DesktopWidth, settings->DesktopHeight, format, NULL); + + if (!gdi_init_ex(instance, format, 0, wfc->primary->pdata, NULL)) + return FALSE; + + gdi = instance->context->gdi; + + if (!settings->SoftwareGdi) + { + wf_gdi_register_update_callbacks(instance->update); + } + + wfc->window_title = wf_window_get_title(settings); + + if (!wfc->window_title) + return FALSE; + + if (settings->EmbeddedWindow) + settings->Decorations = FALSE; + + if (wfc->fullscreen) + dwStyle = WS_POPUP; + else if (!settings->Decorations) + dwStyle = WS_CHILD | WS_BORDER; + else + dwStyle = + WS_CAPTION | WS_OVERLAPPED | WS_SYSMENU | WS_MINIMIZEBOX | WS_SIZEBOX | WS_MAXIMIZEBOX; + + if (!wfc->hwnd) + { + wfc->hwnd = CreateWindowEx((DWORD)NULL, wfc->wndClassName, wfc->window_title, dwStyle, 0, 0, + 0, 0, wfc->hWndParent, NULL, wfc->hInstance, NULL); + SetWindowLongPtr(wfc->hwnd, GWLP_USERDATA, (LONG_PTR)wfc); + } + + wf_resize_window(wfc); + wf_add_system_menu(wfc); + BitBlt(wfc->primary->hdc, 0, 0, settings->DesktopWidth, settings->DesktopHeight, NULL, 0, 0, + BLACKNESS); + wfc->drawing = wfc->primary; + EventArgsInit(&e, "wfreerdp"); + e.embed = FALSE; + e.handle = (void*)wfc->hwnd; + PubSub_OnEmbedWindow(context->pubSub, context, &e); + ShowWindow(wfc->hwnd, SW_SHOWNORMAL); + UpdateWindow(wfc->hwnd); + instance->update->BeginPaint = wf_begin_paint; + instance->update->DesktopResize = wf_desktop_resize; + instance->update->EndPaint = wf_end_paint; + wf_register_pointer(context->graphics); + + if (!settings->SoftwareGdi) + { + wf_register_graphics(context->graphics); + wf_gdi_register_update_callbacks(instance->update); + brush_cache_register_callbacks(instance->update); + glyph_cache_register_callbacks(instance->update); + bitmap_cache_register_callbacks(instance->update); + offscreen_cache_register_callbacks(instance->update); + palette_cache_register_callbacks(instance->update); + } + + wfc->floatbar = wf_floatbar_new(wfc, wfc->hInstance, settings->Floatbar); + return TRUE; +} + +static void wf_post_disconnect(freerdp* instance) +{ + wfContext* wfc; + + if (!instance || !instance->context || !instance->settings) + return; + + wfc = (wfContext*)instance->context; + free(wfc->window_title); +} + +static CREDUI_INFOA wfUiInfo = { sizeof(CREDUI_INFOA), NULL, "Enter your credentials", + "Remote Desktop Security", NULL }; + +static BOOL wf_authenticate_raw(freerdp* instance, const char* title, char** username, + char** password, char** domain) +{ + wfContext* wfc; + BOOL fSave; + DWORD status; + DWORD dwFlags; + char UserName[CREDUI_MAX_USERNAME_LENGTH + 1] = { 0 }; + char Password[CREDUI_MAX_PASSWORD_LENGTH + 1] = { 0 }; + char User[CREDUI_MAX_USERNAME_LENGTH + 1] = { 0 }; + char Domain[CREDUI_MAX_DOMAIN_TARGET_LENGTH + 1] = { 0 }; + + if (!instance || !instance->context) + return FALSE; + wfc = (wfContext*)instance->context; + + fSave = FALSE; + dwFlags = CREDUI_FLAGS_DO_NOT_PERSIST | CREDUI_FLAGS_EXCLUDE_CERTIFICATES; + + if (username && *username) + strncpy(UserName, *username, CREDUI_MAX_USERNAME_LENGTH); + if (wfc->isConsole) + status = CredUICmdLinePromptForCredentialsA( + title, NULL, 0, UserName, CREDUI_MAX_USERNAME_LENGTH + 1, Password, + CREDUI_MAX_PASSWORD_LENGTH + 1, &fSave, dwFlags); + else + status = CredUIPromptForCredentialsA(&wfUiInfo, title, NULL, 0, UserName, + CREDUI_MAX_USERNAME_LENGTH + 1, Password, + CREDUI_MAX_PASSWORD_LENGTH + 1, &fSave, dwFlags); + + if (status != NO_ERROR) + { + WLog_ERR(TAG, "CredUIPromptForCredentials unexpected status: 0x%08lX", status); + return FALSE; + } + + status = CredUIParseUserNameA(UserName, User, sizeof(User), Domain, sizeof(Domain)); + // WLog_ERR(TAG, "User: %s Domain: %s Password: %s", User, Domain, Password); + *username = _strdup(User); + + if (!(*username)) + { + WLog_ERR(TAG, "strdup failed", status); + return FALSE; + } + + if (strlen(Domain) > 0) + *domain = _strdup(Domain); + else + *domain = _strdup("\0"); + + if (!(*domain)) + { + free(*username); + WLog_ERR(TAG, "strdup failed", status); + return FALSE; + } + + *password = _strdup(Password); + + if (!(*password)) + { + free(*username); + free(*domain); + return FALSE; + } + + return TRUE; +} + +static BOOL wf_authenticate(freerdp* instance, char** username, char** password, char** domain) +{ + return wf_authenticate_raw(instance, instance->settings->ServerHostname, username, password, + domain); +} + +static BOOL wf_gw_authenticate(freerdp* instance, char** username, char** password, char** domain) +{ + char tmp[MAX_PATH]; + sprintf_s(tmp, sizeof(tmp), "Gateway %s", instance->settings->GatewayHostname); + return wf_authenticate_raw(instance, tmp, username, password, domain); +} + +static WCHAR* wf_format_text(const WCHAR* fmt, ...) +{ + int rc; + size_t size = 1024; + WCHAR* buffer = calloc(size, sizeof(WCHAR)); + if (!buffer) + return NULL; + + do + { + WCHAR* tmp; + va_list ap; + va_start(ap, fmt); + rc = vswprintf_s(buffer, size, fmt, ap); + va_end(ap); + if (rc <= 0) + goto fail; + + if ((size_t)rc < size) + return buffer; + + size = (size_t)rc + 1; + tmp = realloc(buffer, size * sizeof(WCHAR)); + if (!tmp) + goto fail; + + buffer = tmp; + } while (TRUE); + +fail: + free(buffer); + return NULL; +} + +static DWORD wf_verify_certificate_ex(freerdp* instance, const char* host, UINT16 port, + const char* common_name, const char* subject, + const char* issuer, const char* fingerprint, DWORD flags) +{ + WCHAR* buffer; + WCHAR* caption; + int what = IDCANCEL; + + buffer = wf_format_text( + L"Certificate details:\n" + L"\tCommonName: %S\n" + L"\tSubject: %S\n" + L"\tIssuer: %S\n" + L"\tThumbprint: %S\n" + L"\tHostMismatch: %S\n" + L"\n" + L"The above X.509 certificate could not be verified, possibly because you do not have " + L"the CA certificate in your certificate store, or the certificate has expired. " + L"Please look at the OpenSSL documentation on how to add a private CA to the store.\n" + L"\n" + L"YES\tAccept permanently\n" + L"NO\tAccept for this session only\n" + L"CANCEL\tAbort connection\n", + common_name, subject, issuer, fingerprint, + flags & VERIFY_CERT_FLAG_MISMATCH ? "Yes" : "No"); + caption = wf_format_text(L"Verify certificate for %S:%hu", host, port); + + if (!buffer || !caption) + goto fail; + + what = MessageBoxW(NULL, buffer, caption, MB_YESNOCANCEL); +fail: + free(buffer); + free(caption); + + /* return 1 to accept and store a certificate, 2 to accept + * a certificate only for this session, 0 otherwise */ + switch (what) + { + case IDYES: + return 1; + case IDNO: + return 2; + default: + return 0; + } +} + +static DWORD wf_verify_changed_certificate_ex(freerdp* instance, const char* host, UINT16 port, + const char* common_name, const char* subject, + const char* issuer, const char* new_fingerprint, + const char* old_subject, const char* old_issuer, + const char* old_fingerprint, DWORD flags) +{ + WCHAR* buffer; + WCHAR* caption; + int what = IDCANCEL; + + buffer = wf_format_text( + L"New Certificate details:\n" + L"\tCommonName: %S\n" + L"\tSubject: %S\n" + L"\tIssuer: %S\n" + L"\tThumbprint: %S\n" + L"\tHostMismatch: %S\n" + L"\n" + L"Old Certificate details:\n" + L"\tSubject: %S\n" + L"\tIssuer: %S\n" + L"\tThumbprint: %S" + L"The above X.509 certificate could not be verified, possibly because you do not have " + L"the CA certificate in your certificate store, or the certificate has expired. " + L"Please look at the OpenSSL documentation on how to add a private CA to the store.\n" + L"\n" + L"YES\tAccept permanently\n" + L"NO\tAccept for this session only\n" + L"CANCEL\tAbort connection\n", + common_name, subject, issuer, new_fingerprint, + flags & VERIFY_CERT_FLAG_MISMATCH ? "Yes" : "No", old_subject, old_issuer, old_fingerprint); + caption = wf_format_text(L"Verify certificate change for %S:%hu", host, port); + + if (!buffer || !caption) + goto fail; + + what = MessageBoxW(NULL, buffer, caption, MB_YESNOCANCEL); +fail: + free(buffer); + free(caption); + + /* return 1 to accept and store a certificate, 2 to accept + * a certificate only for this session, 0 otherwise */ + switch (what) + { + case IDYES: + return 1; + case IDNO: + return 2; + default: + return 0; + } +} + +static BOOL wf_present_gateway_message(freerdp* instance, UINT32 type, BOOL isDisplayMandatory, + BOOL isConsentMandatory, size_t length, const WCHAR* message) +{ + if (!isDisplayMandatory && !isConsentMandatory) + return TRUE; + + /* special handling for consent messages (show modal dialog) */ + if (type == GATEWAY_MESSAGE_CONSENT && isConsentMandatory) + { + int mbRes; + WCHAR* msg; + + msg = wf_format_text(L"%.*s\n\nI understand and agree to the terms of this policy", length, + message); + mbRes = MessageBoxW(NULL, msg, L"Consent Message", MB_YESNO); + free(msg); + + if (mbRes != IDYES) + return FALSE; + } + else + return client_cli_present_gateway_message(instance, type, isDisplayMandatory, + isConsentMandatory, length, message); + + return TRUE; +} + +static DWORD WINAPI wf_input_thread(LPVOID arg) +{ + int status; + wMessage message; + wMessageQueue* queue; + freerdp* instance = (freerdp*)arg; + assert(NULL != instance); + status = 1; + queue = freerdp_get_message_queue(instance, FREERDP_INPUT_MESSAGE_QUEUE); + + while (MessageQueue_Wait(queue)) + { + while (MessageQueue_Peek(queue, &message, TRUE)) + { + status = freerdp_message_queue_process_message(instance, FREERDP_INPUT_MESSAGE_QUEUE, + &message); + + if (!status) + break; + } + + if (!status) + break; + } + + ExitThread(0); + return 0; +} + +static DWORD WINAPI wf_client_thread(LPVOID lpParam) +{ + MSG msg; + int width; + int height; + BOOL msg_ret; + int quit_msg; + DWORD nCount; + DWORD error; + HANDLE handles[64]; + wfContext* wfc; + freerdp* instance; + rdpContext* context; + rdpChannels* channels; + rdpSettings* settings; + BOOL async_input; + HANDLE input_thread; + instance = (freerdp*)lpParam; + context = instance->context; + wfc = (wfContext*)instance->context; + + if (!freerdp_connect(instance)) + goto end; + + channels = instance->context->channels; + settings = instance->context->settings; + async_input = settings->AsyncInput; + + if (async_input) + { + if (!(input_thread = CreateThread(NULL, 0, wf_input_thread, instance, 0, NULL))) + { + WLog_ERR(TAG, "Failed to create async input thread."); + goto disconnect; + } + } + + while (1) + { + nCount = 0; + + if (freerdp_focus_required(instance)) + { + wf_event_focus_in(wfc); + wf_event_focus_in(wfc); + } + + { + DWORD tmp = freerdp_get_event_handles(context, &handles[nCount], 64 - nCount); + + if (tmp == 0) + { + WLog_ERR(TAG, "freerdp_get_event_handles failed"); + break; + } + + nCount += tmp; + } + + if (MsgWaitForMultipleObjects(nCount, handles, FALSE, 1000, QS_ALLINPUT) == WAIT_FAILED) + { + WLog_ERR(TAG, "wfreerdp_run: WaitForMultipleObjects failed: 0x%08lX", GetLastError()); + break; + } + + { + if (!freerdp_check_event_handles(context)) + { + if (client_auto_reconnect(instance)) + continue; + + WLog_ERR(TAG, "Failed to check FreeRDP file descriptor"); + break; + } + } + + if (freerdp_shall_disconnect(instance)) + break; + + quit_msg = FALSE; + + while (PeekMessage(&msg, NULL, 0, 0, PM_NOREMOVE)) + { + msg_ret = GetMessage(&msg, NULL, 0, 0); + + if (instance->settings->EmbeddedWindow) + { + if ((msg.message == WM_SETFOCUS) && (msg.lParam == 1)) + { + PostMessage(wfc->hwnd, WM_SETFOCUS, 0, 0); + } + else if ((msg.message == WM_KILLFOCUS) && (msg.lParam == 1)) + { + PostMessage(wfc->hwnd, WM_KILLFOCUS, 0, 0); + } + } + + if (msg.message == WM_SIZE) + { + width = LOWORD(msg.lParam); + height = HIWORD(msg.lParam); + SetWindowPos(wfc->hwnd, HWND_TOP, 0, 0, width, height, SWP_FRAMECHANGED); + } + + if ((msg_ret == 0) || (msg_ret == -1)) + { + quit_msg = TRUE; + break; + } + + TranslateMessage(&msg); + DispatchMessage(&msg); + } + + if (quit_msg) + break; + } + + /* cleanup */ + if (async_input) + { + wMessageQueue* input_queue; + input_queue = freerdp_get_message_queue(instance, FREERDP_INPUT_MESSAGE_QUEUE); + + if (MessageQueue_PostQuit(input_queue, 0)) + WaitForSingleObject(input_thread, INFINITE); + } + +disconnect: + freerdp_disconnect(instance); + + if (async_input) + CloseHandle(input_thread); + +end: + error = freerdp_get_last_error(instance->context); + WLog_DBG(TAG, "Main thread exited with %" PRIu32, error); + ExitThread(error); + return error; +} + +static DWORD WINAPI wf_keyboard_thread(LPVOID lpParam) +{ + MSG msg; + BOOL status; + wfContext* wfc; + HHOOK hook_handle; + wfc = (wfContext*)lpParam; + assert(NULL != wfc); + hook_handle = SetWindowsHookEx(WH_KEYBOARD_LL, wf_ll_kbd_proc, wfc->hInstance, 0); + + if (hook_handle) + { + while ((status = GetMessage(&msg, NULL, 0, 0)) != 0) + { + if (status == -1) + { + WLog_ERR(TAG, "keyboard thread error getting message"); + break; + } + else + { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + } + + UnhookWindowsHookEx(hook_handle); + } + else + { + WLog_ERR(TAG, "failed to install keyboard hook"); + } + + WLog_DBG(TAG, "Keyboard thread exited."); + ExitThread(0); + return (DWORD)NULL; +} + +static rdpSettings* freerdp_client_get_settings(wfContext* wfc) +{ + return wfc->context.settings; +} + +static int freerdp_client_focus_in(wfContext* wfc) +{ + PostThreadMessage(wfc->mainThreadId, WM_SETFOCUS, 0, 1); + return 0; +} + +static int freerdp_client_focus_out(wfContext* wfc) +{ + PostThreadMessage(wfc->mainThreadId, WM_KILLFOCUS, 0, 1); + return 0; +} + +int freerdp_client_set_window_size(wfContext* wfc, int width, int height) +{ + WLog_DBG(TAG, "freerdp_client_set_window_size %d, %d", width, height); + + if ((width != wfc->client_width) || (height != wfc->client_height)) + { + PostThreadMessage(wfc->mainThreadId, WM_SIZE, SIZE_RESTORED, + ((UINT)height << 16) | (UINT)width); + } + + return 0; +} + +void wf_size_scrollbars(wfContext* wfc, UINT32 client_width, UINT32 client_height) +{ + if (wfc->disablewindowtracking) + return; + + // prevent infinite message loop + wfc->disablewindowtracking = TRUE; + + if (wfc->context.settings->SmartSizing) + { + wfc->xCurrentScroll = 0; + wfc->yCurrentScroll = 0; + + if (wfc->xScrollVisible || wfc->yScrollVisible) + { + if (ShowScrollBar(wfc->hwnd, SB_BOTH, FALSE)) + { + wfc->xScrollVisible = FALSE; + wfc->yScrollVisible = FALSE; + } + } + } + else + { + SCROLLINFO si; + BOOL horiz = wfc->xScrollVisible; + BOOL vert = wfc->yScrollVisible; + + if (!horiz && client_width < wfc->context.settings->DesktopWidth) + { + horiz = TRUE; + } + else if (horiz && + client_width >= + wfc->context.settings->DesktopWidth /* - GetSystemMetrics(SM_CXVSCROLL)*/) + { + horiz = FALSE; + } + + if (!vert && client_height < wfc->context.settings->DesktopHeight) + { + vert = TRUE; + } + else if (vert && + client_height >= + wfc->context.settings->DesktopHeight /* - GetSystemMetrics(SM_CYHSCROLL)*/) + { + vert = FALSE; + } + + if (horiz == vert && (horiz != wfc->xScrollVisible && vert != wfc->yScrollVisible)) + { + if (ShowScrollBar(wfc->hwnd, SB_BOTH, horiz)) + { + wfc->xScrollVisible = horiz; + wfc->yScrollVisible = vert; + } + } + + if (horiz != wfc->xScrollVisible) + { + if (ShowScrollBar(wfc->hwnd, SB_HORZ, horiz)) + { + wfc->xScrollVisible = horiz; + } + } + + if (vert != wfc->yScrollVisible) + { + if (ShowScrollBar(wfc->hwnd, SB_VERT, vert)) + { + wfc->yScrollVisible = vert; + } + } + + if (horiz) + { + // The horizontal scrolling range is defined by + // (bitmap_width) - (client_width). The current horizontal + // scroll value remains within the horizontal scrolling range. + wfc->xMaxScroll = MAX(wfc->context.settings->DesktopWidth - client_width, 0); + wfc->xCurrentScroll = MIN(wfc->xCurrentScroll, wfc->xMaxScroll); + si.cbSize = sizeof(si); + si.fMask = SIF_RANGE | SIF_PAGE | SIF_POS; + si.nMin = wfc->xMinScroll; + si.nMax = wfc->context.settings->DesktopWidth; + si.nPage = client_width; + si.nPos = wfc->xCurrentScroll; + SetScrollInfo(wfc->hwnd, SB_HORZ, &si, TRUE); + } + + if (vert) + { + // The vertical scrolling range is defined by + // (bitmap_height) - (client_height). The current vertical + // scroll value remains within the vertical scrolling range. + wfc->yMaxScroll = MAX(wfc->context.settings->DesktopHeight - client_height, 0); + wfc->yCurrentScroll = MIN(wfc->yCurrentScroll, wfc->yMaxScroll); + si.cbSize = sizeof(si); + si.fMask = SIF_RANGE | SIF_PAGE | SIF_POS; + si.nMin = wfc->yMinScroll; + si.nMax = wfc->context.settings->DesktopHeight; + si.nPage = client_height; + si.nPos = wfc->yCurrentScroll; + SetScrollInfo(wfc->hwnd, SB_VERT, &si, TRUE); + } + } + + wfc->disablewindowtracking = FALSE; + wf_update_canvas_diff(wfc); +} + +static BOOL wfreerdp_client_global_init(void) +{ + WSADATA wsaData; + + WSAStartup(0x101, &wsaData); + + freerdp_register_addin_provider(freerdp_channels_load_static_addin_entry, 0); + return TRUE; +} + +static void wfreerdp_client_global_uninit(void) +{ + WSACleanup(); +} + +static BOOL wfreerdp_client_new(freerdp* instance, rdpContext* context) +{ + wfContext* wfc = (wfContext*)context; + if (!wfc) + return FALSE; + + // AttachConsole and stdin do not work well. + // Use GUI input dialogs instead of command line ones. + wfc->isConsole = wf_create_console(); + + if (!(wfreerdp_client_global_init())) + return FALSE; + + instance->PreConnect = wf_pre_connect; + instance->PostConnect = wf_post_connect; + instance->PostDisconnect = wf_post_disconnect; + instance->Authenticate = wf_authenticate; + instance->GatewayAuthenticate = wf_gw_authenticate; + if (wfc->isConsole) + { + instance->VerifyCertificateEx = client_cli_verify_certificate_ex; + instance->VerifyChangedCertificateEx = client_cli_verify_changed_certificate_ex; + instance->PresentGatewayMessage = client_cli_present_gateway_message; + } + else + { + instance->VerifyCertificateEx = wf_verify_certificate_ex; + instance->VerifyChangedCertificateEx = wf_verify_changed_certificate_ex; + instance->PresentGatewayMessage = wf_present_gateway_message; + } + + return TRUE; +} + +static void wfreerdp_client_free(freerdp* instance, rdpContext* context) +{ + if (!context) + return; +} + +static int wfreerdp_client_start(rdpContext* context) +{ + HWND hWndParent; + HINSTANCE hInstance; + wfContext* wfc = (wfContext*)context; + freerdp* instance = context->instance; + hInstance = GetModuleHandle(NULL); + hWndParent = (HWND)instance->settings->ParentWindowId; + instance->settings->EmbeddedWindow = (hWndParent) ? TRUE : FALSE; + wfc->hWndParent = hWndParent; + wfc->hInstance = hInstance; + wfc->cursor = LoadCursor(NULL, IDC_ARROW); + wfc->icon = LoadIcon(GetModuleHandle(NULL), MAKEINTRESOURCE(IDI_ICON1)); + wfc->wndClassName = _tcsdup(_T("FreeRDP")); + wfc->wndClass.cbSize = sizeof(WNDCLASSEX); + wfc->wndClass.style = CS_HREDRAW | CS_VREDRAW; + wfc->wndClass.lpfnWndProc = wf_event_proc; + wfc->wndClass.cbClsExtra = 0; + wfc->wndClass.cbWndExtra = 0; + wfc->wndClass.hCursor = wfc->cursor; + wfc->wndClass.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH); + wfc->wndClass.lpszMenuName = NULL; + wfc->wndClass.lpszClassName = wfc->wndClassName; + wfc->wndClass.hInstance = hInstance; + wfc->wndClass.hIcon = wfc->icon; + wfc->wndClass.hIconSm = wfc->icon; + RegisterClassEx(&(wfc->wndClass)); + wfc->keyboardThread = + CreateThread(NULL, 0, wf_keyboard_thread, (void*)wfc, 0, &wfc->keyboardThreadId); + + if (!wfc->keyboardThread) + return -1; + + wfc->thread = CreateThread(NULL, 0, wf_client_thread, (void*)instance, 0, &wfc->mainThreadId); + + if (!wfc->thread) + return -1; + + return 0; +} + +static int wfreerdp_client_stop(rdpContext* context) +{ + wfContext* wfc = (wfContext*)context; + + if (wfc->thread) + { + PostThreadMessage(wfc->mainThreadId, WM_QUIT, 0, 0); + WaitForSingleObject(wfc->thread, INFINITE); + CloseHandle(wfc->thread); + wfc->thread = NULL; + wfc->mainThreadId = 0; + } + + if (wfc->keyboardThread) + { + PostThreadMessage(wfc->keyboardThreadId, WM_QUIT, 0, 0); + WaitForSingleObject(wfc->keyboardThread, INFINITE); + CloseHandle(wfc->keyboardThread); + wfc->keyboardThread = NULL; + wfc->keyboardThreadId = 0; + } + + return 0; +} + +int RdpClientEntry(RDP_CLIENT_ENTRY_POINTS* pEntryPoints) +{ + pEntryPoints->Version = 1; + pEntryPoints->Size = sizeof(RDP_CLIENT_ENTRY_POINTS_V1); + pEntryPoints->GlobalInit = wfreerdp_client_global_init; + pEntryPoints->GlobalUninit = wfreerdp_client_global_uninit; + pEntryPoints->ContextSize = sizeof(wfContext); + pEntryPoints->ClientNew = wfreerdp_client_new; + pEntryPoints->ClientFree = wfreerdp_client_free; + pEntryPoints->ClientStart = wfreerdp_client_start; + pEntryPoints->ClientStop = wfreerdp_client_stop; + return 0; +} diff --git a/client/Windows/wf_client.h b/client/Windows/wf_client.h new file mode 100644 index 0000000..29f194a --- /dev/null +++ b/client/Windows/wf_client.h @@ -0,0 +1,151 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Windows Client + * + * Copyright 2009-2011 Jay Sorg + * Copyright 2010-2011 Vic Lee + * Copyright 2010-2011 Marc-Andre Moreau + * + * Licensed 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 FREERDP_CLIENT_WIN_INTERFACE_H +#define FREERDP_CLIENT_WIN_INTERFACE_H + +#include + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +typedef struct wf_context wfContext; + +#include "wf_channels.h" +#include "wf_floatbar.h" +#include "wf_event.h" +#include "wf_cliprdr.h" + +#ifdef __cplusplus +extern "C" +{ +#endif + +// System menu constants +#define SYSCOMMAND_ID_SMARTSIZING 1000 + + struct wf_bitmap + { + rdpBitmap _bitmap; + HDC hdc; + HBITMAP bitmap; + HBITMAP org_bitmap; + BYTE* pdata; + }; + typedef struct wf_bitmap wfBitmap; + + struct wf_pointer + { + rdpPointer pointer; + HCURSOR cursor; + }; + typedef struct wf_pointer wfPointer; + + struct wf_context + { + rdpContext context; + DEFINE_RDP_CLIENT_COMMON(); + + int offset_x; + int offset_y; + int fullscreen_toggle; + int fullscreen; + int percentscreen; + WCHAR* window_title; + int client_x; + int client_y; + int client_width; + int client_height; + + HANDLE keyboardThread; + + HICON icon; + HWND hWndParent; + HINSTANCE hInstance; + WNDCLASSEX wndClass; + LPCTSTR wndClassName; + HCURSOR hDefaultCursor; + + HWND hwnd; + POINT diff; + + wfBitmap* primary; + wfBitmap* drawing; + HCURSOR cursor; + HBRUSH brush; + HBRUSH org_brush; + RECT update_rect; + RECT scale_update_rect; + + DWORD mainThreadId; + DWORD keyboardThreadId; + + rdpFile* connectionRdpFile; + + BOOL disablewindowtracking; + + BOOL updating_scrollbars; + BOOL xScrollVisible; + int xMinScroll; + int xCurrentScroll; + int xMaxScroll; + + BOOL yScrollVisible; + int yMinScroll; + int yCurrentScroll; + int yMaxScroll; + + void* clipboard; + CliprdrClientContext* cliprdr; + + wfFloatBar* floatbar; + + RailClientContext* rail; + wHashTable* railWindows; + BOOL isConsole; + }; + + /** + * Client Interface + */ + + FREERDP_API int RdpClientEntry(RDP_CLIENT_ENTRY_POINTS* pEntryPoints); + FREERDP_API int freerdp_client_set_window_size(wfContext* wfc, int width, int height); + FREERDP_API void wf_size_scrollbars(wfContext* wfc, UINT32 client_width, UINT32 client_height); + +#ifdef __cplusplus +} +#endif + +#endif /* FREERDP_CLIENT_WIN_INTERFACE_H */ diff --git a/client/Windows/wf_cliprdr.c b/client/Windows/wf_cliprdr.c new file mode 100644 index 0000000..bc4bde6 --- /dev/null +++ b/client/Windows/wf_cliprdr.c @@ -0,0 +1,2552 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Windows Clipboard Redirection + * + * Copyright 2012 Jason Champion + * Copyright 2014 Marc-Andre Moreau + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#define CINTERFACE +#define COBJMACROS + +#include +#include +#include +#include + +#include + +#include +#include +#include + +#include +#include + +#include + +#include "wf_cliprdr.h" + +#define TAG CLIENT_TAG("windows") + +#ifdef WITH_DEBUG_CLIPRDR +#define DEBUG_CLIPRDR(...) WLog_DBG(TAG, __VA_ARGS__) +#else +#define DEBUG_CLIPRDR(...) \ + do \ + { \ + } while (0) +#endif + +typedef BOOL(WINAPI* fnAddClipboardFormatListener)(HWND hwnd); +typedef BOOL(WINAPI* fnRemoveClipboardFormatListener)(HWND hwnd); +typedef BOOL(WINAPI* fnGetUpdatedClipboardFormats)(PUINT lpuiFormats, UINT cFormats, + PUINT pcFormatsOut); + +struct format_mapping +{ + UINT32 remote_format_id; + UINT32 local_format_id; + WCHAR* name; +}; +typedef struct format_mapping formatMapping; + +struct _CliprdrEnumFORMATETC +{ + IEnumFORMATETC iEnumFORMATETC; + + LONG m_lRefCount; + LONG m_nIndex; + LONG m_nNumFormats; + FORMATETC* m_pFormatEtc; +}; +typedef struct _CliprdrEnumFORMATETC CliprdrEnumFORMATETC; + +struct _CliprdrStream +{ + IStream iStream; + + LONG m_lRefCount; + ULONG m_lIndex; + ULARGE_INTEGER m_lSize; + ULARGE_INTEGER m_lOffset; + FILEDESCRIPTORW m_Dsc; + void* m_pData; +}; +typedef struct _CliprdrStream CliprdrStream; + +struct _CliprdrDataObject +{ + IDataObject iDataObject; + + LONG m_lRefCount; + FORMATETC* m_pFormatEtc; + STGMEDIUM* m_pStgMedium; + ULONG m_nNumFormats; + ULONG m_nStreams; + IStream** m_pStream; + void* m_pData; +}; +typedef struct _CliprdrDataObject CliprdrDataObject; + +struct wf_clipboard +{ + wfContext* wfc; + rdpChannels* channels; + CliprdrClientContext* context; + + BOOL sync; + UINT32 capabilities; + + size_t map_size; + size_t map_capacity; + formatMapping* format_mappings; + + UINT32 requestedFormatId; + + HWND hwnd; + HANDLE hmem; + HANDLE thread; + HANDLE response_data_event; + + LPDATAOBJECT data_obj; + ULONG req_fsize; + char* req_fdata; + HANDLE req_fevent; + + size_t nFiles; + size_t file_array_size; + WCHAR** file_names; + FILEDESCRIPTORW** fileDescriptor; + + BOOL legacyApi; + HMODULE hUser32; + HWND hWndNextViewer; + fnAddClipboardFormatListener AddClipboardFormatListener; + fnRemoveClipboardFormatListener RemoveClipboardFormatListener; + fnGetUpdatedClipboardFormats GetUpdatedClipboardFormats; +}; +typedef struct wf_clipboard wfClipboard; + +#define WM_CLIPRDR_MESSAGE (WM_USER + 156) +#define OLE_SETCLIPBOARD 1 + +static BOOL wf_create_file_obj(wfClipboard* cliprdrrdr, IDataObject** ppDataObject); +static void wf_destroy_file_obj(IDataObject* instance); +static UINT32 get_remote_format_id(wfClipboard* clipboard, UINT32 local_format); +static UINT cliprdr_send_data_request(wfClipboard* clipboard, UINT32 format); +static UINT cliprdr_send_lock(wfClipboard* clipboard); +static UINT cliprdr_send_unlock(wfClipboard* clipboard); +static UINT cliprdr_send_request_filecontents(wfClipboard* clipboard, const void* streamid, + ULONG index, UINT32 flag, DWORD positionhigh, + DWORD positionlow, ULONG request); + +static void CliprdrDataObject_Delete(CliprdrDataObject* instance); + +static CliprdrEnumFORMATETC* CliprdrEnumFORMATETC_New(ULONG nFormats, FORMATETC* pFormatEtc); +static void CliprdrEnumFORMATETC_Delete(CliprdrEnumFORMATETC* instance); + +static void CliprdrStream_Delete(CliprdrStream* instance); + +static BOOL try_open_clipboard(HWND hwnd) +{ + size_t x; + for (x = 0; x < 10; x++) + { + if (OpenClipboard(hwnd)) + return TRUE; + Sleep(10); + } + return FALSE; +} + +/** + * IStream + */ + +static HRESULT STDMETHODCALLTYPE CliprdrStream_QueryInterface(IStream* This, REFIID riid, + void** ppvObject) +{ + if (IsEqualIID(riid, &IID_IStream) || IsEqualIID(riid, &IID_IUnknown)) + { + IStream_AddRef(This); + *ppvObject = This; + return S_OK; + } + else + { + *ppvObject = 0; + return E_NOINTERFACE; + } +} + +static ULONG STDMETHODCALLTYPE CliprdrStream_AddRef(IStream* This) +{ + CliprdrStream* instance = (CliprdrStream*)This; + + if (!instance) + return 0; + + return InterlockedIncrement(&instance->m_lRefCount); +} + +static ULONG STDMETHODCALLTYPE CliprdrStream_Release(IStream* This) +{ + LONG count; + CliprdrStream* instance = (CliprdrStream*)This; + + if (!instance) + return 0; + + count = InterlockedDecrement(&instance->m_lRefCount); + + if (count == 0) + { + CliprdrStream_Delete(instance); + return 0; + } + else + { + return count; + } +} + +static HRESULT STDMETHODCALLTYPE CliprdrStream_Read(IStream* This, void* pv, ULONG cb, + ULONG* pcbRead) +{ + int ret; + CliprdrStream* instance = (CliprdrStream*)This; + wfClipboard* clipboard; + + if (!pv || !pcbRead || !instance) + return E_INVALIDARG; + + clipboard = (wfClipboard*)instance->m_pData; + *pcbRead = 0; + + if (instance->m_lOffset.QuadPart >= instance->m_lSize.QuadPart) + return S_FALSE; + + ret = cliprdr_send_request_filecontents(clipboard, (void*)This, instance->m_lIndex, + FILECONTENTS_RANGE, instance->m_lOffset.HighPart, + instance->m_lOffset.LowPart, cb); + + if (ret < 0) + return E_FAIL; + + if (clipboard->req_fdata) + { + CopyMemory(pv, clipboard->req_fdata, clipboard->req_fsize); + free(clipboard->req_fdata); + } + + *pcbRead = clipboard->req_fsize; + instance->m_lOffset.QuadPart += clipboard->req_fsize; + + if (clipboard->req_fsize < cb) + return S_FALSE; + + return S_OK; +} + +static HRESULT STDMETHODCALLTYPE CliprdrStream_Write(IStream* This, const void* pv, ULONG cb, + ULONG* pcbWritten) +{ + (void)This; + (void)pv; + (void)cb; + (void)pcbWritten; + return STG_E_ACCESSDENIED; +} + +static HRESULT STDMETHODCALLTYPE CliprdrStream_Seek(IStream* This, LARGE_INTEGER dlibMove, + DWORD dwOrigin, ULARGE_INTEGER* plibNewPosition) +{ + ULONGLONG newoffset; + CliprdrStream* instance = (CliprdrStream*)This; + + if (!instance) + return E_INVALIDARG; + + newoffset = instance->m_lOffset.QuadPart; + + switch (dwOrigin) + { + case STREAM_SEEK_SET: + newoffset = dlibMove.QuadPart; + break; + + case STREAM_SEEK_CUR: + newoffset += dlibMove.QuadPart; + break; + + case STREAM_SEEK_END: + newoffset = instance->m_lSize.QuadPart + dlibMove.QuadPart; + break; + + default: + return E_INVALIDARG; + } + + if (newoffset < 0 || newoffset >= instance->m_lSize.QuadPart) + return E_FAIL; + + instance->m_lOffset.QuadPart = newoffset; + + if (plibNewPosition) + plibNewPosition->QuadPart = instance->m_lOffset.QuadPart; + + return S_OK; +} + +static HRESULT STDMETHODCALLTYPE CliprdrStream_SetSize(IStream* This, ULARGE_INTEGER libNewSize) +{ + (void)This; + (void)libNewSize; + return E_NOTIMPL; +} + +static HRESULT STDMETHODCALLTYPE CliprdrStream_CopyTo(IStream* This, IStream* pstm, + ULARGE_INTEGER cb, ULARGE_INTEGER* pcbRead, + ULARGE_INTEGER* pcbWritten) +{ + (void)This; + (void)pstm; + (void)cb; + (void)pcbRead; + (void)pcbWritten; + return E_NOTIMPL; +} + +static HRESULT STDMETHODCALLTYPE CliprdrStream_Commit(IStream* This, DWORD grfCommitFlags) +{ + (void)This; + (void)grfCommitFlags; + return E_NOTIMPL; +} + +static HRESULT STDMETHODCALLTYPE CliprdrStream_Revert(IStream* This) +{ + (void)This; + return E_NOTIMPL; +} + +static HRESULT STDMETHODCALLTYPE CliprdrStream_LockRegion(IStream* This, ULARGE_INTEGER libOffset, + ULARGE_INTEGER cb, DWORD dwLockType) +{ + (void)This; + (void)libOffset; + (void)cb; + (void)dwLockType; + return E_NOTIMPL; +} + +static HRESULT STDMETHODCALLTYPE CliprdrStream_UnlockRegion(IStream* This, ULARGE_INTEGER libOffset, + ULARGE_INTEGER cb, DWORD dwLockType) +{ + (void)This; + (void)libOffset; + (void)cb; + (void)dwLockType; + return E_NOTIMPL; +} + +static HRESULT STDMETHODCALLTYPE CliprdrStream_Stat(IStream* This, STATSTG* pstatstg, + DWORD grfStatFlag) +{ + CliprdrStream* instance = (CliprdrStream*)This; + + if (!instance) + return E_INVALIDARG; + + if (pstatstg == NULL) + return STG_E_INVALIDPOINTER; + + ZeroMemory(pstatstg, sizeof(STATSTG)); + + switch (grfStatFlag) + { + case STATFLAG_DEFAULT: + return STG_E_INSUFFICIENTMEMORY; + + case STATFLAG_NONAME: + pstatstg->cbSize.QuadPart = instance->m_lSize.QuadPart; + pstatstg->grfLocksSupported = LOCK_EXCLUSIVE; + pstatstg->grfMode = GENERIC_READ; + pstatstg->grfStateBits = 0; + pstatstg->type = STGTY_STREAM; + break; + + case STATFLAG_NOOPEN: + return STG_E_INVALIDFLAG; + + default: + return STG_E_INVALIDFLAG; + } + + return S_OK; +} + +static HRESULT STDMETHODCALLTYPE CliprdrStream_Clone(IStream* This, IStream** ppstm) +{ + (void)This; + (void)ppstm; + return E_NOTIMPL; +} + +static CliprdrStream* CliprdrStream_New(ULONG index, void* pData, const FILEDESCRIPTORW* dsc) +{ + IStream* iStream; + BOOL success = FALSE; + BOOL isDir = FALSE; + CliprdrStream* instance; + wfClipboard* clipboard = (wfClipboard*)pData; + instance = (CliprdrStream*)calloc(1, sizeof(CliprdrStream)); + + if (instance) + { + instance->m_Dsc = *dsc; + iStream = &instance->iStream; + iStream->lpVtbl = (IStreamVtbl*)calloc(1, sizeof(IStreamVtbl)); + + if (iStream->lpVtbl) + { + iStream->lpVtbl->QueryInterface = CliprdrStream_QueryInterface; + iStream->lpVtbl->AddRef = CliprdrStream_AddRef; + iStream->lpVtbl->Release = CliprdrStream_Release; + iStream->lpVtbl->Read = CliprdrStream_Read; + iStream->lpVtbl->Write = CliprdrStream_Write; + iStream->lpVtbl->Seek = CliprdrStream_Seek; + iStream->lpVtbl->SetSize = CliprdrStream_SetSize; + iStream->lpVtbl->CopyTo = CliprdrStream_CopyTo; + iStream->lpVtbl->Commit = CliprdrStream_Commit; + iStream->lpVtbl->Revert = CliprdrStream_Revert; + iStream->lpVtbl->LockRegion = CliprdrStream_LockRegion; + iStream->lpVtbl->UnlockRegion = CliprdrStream_UnlockRegion; + iStream->lpVtbl->Stat = CliprdrStream_Stat; + iStream->lpVtbl->Clone = CliprdrStream_Clone; + instance->m_lRefCount = 1; + instance->m_lIndex = index; + instance->m_pData = pData; + instance->m_lOffset.QuadPart = 0; + + if (instance->m_Dsc.dwFlags & FD_ATTRIBUTES) + { + if (instance->m_Dsc.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) + isDir = TRUE; + } + + if (((instance->m_Dsc.dwFlags & FD_FILESIZE) == 0) && !isDir) + { + /* get content size of this stream */ + if (cliprdr_send_request_filecontents(clipboard, (void*)instance, + instance->m_lIndex, FILECONTENTS_SIZE, 0, 0, + 8) == CHANNEL_RC_OK) + { + success = TRUE; + } + + instance->m_lSize.QuadPart = *((LONGLONG*)clipboard->req_fdata); + free(clipboard->req_fdata); + } + else + success = TRUE; + } + } + + if (!success) + { + CliprdrStream_Delete(instance); + instance = NULL; + } + + return instance; +} + +void CliprdrStream_Delete(CliprdrStream* instance) +{ + if (instance) + { + free(instance->iStream.lpVtbl); + free(instance); + } +} + +/** + * IDataObject + */ + +static LONG cliprdr_lookup_format(CliprdrDataObject* instance, FORMATETC* pFormatEtc) +{ + ULONG i; + + if (!instance || !pFormatEtc) + return -1; + + for (i = 0; i < instance->m_nNumFormats; i++) + { + if ((pFormatEtc->tymed & instance->m_pFormatEtc[i].tymed) && + pFormatEtc->cfFormat == instance->m_pFormatEtc[i].cfFormat && + pFormatEtc->dwAspect & instance->m_pFormatEtc[i].dwAspect) + { + return (LONG)i; + } + } + + return -1; +} + +static HRESULT STDMETHODCALLTYPE CliprdrDataObject_QueryInterface(IDataObject* This, REFIID riid, + void** ppvObject) +{ + (void)This; + + if (!ppvObject) + return E_INVALIDARG; + + if (IsEqualIID(riid, &IID_IDataObject) || IsEqualIID(riid, &IID_IUnknown)) + { + IDataObject_AddRef(This); + *ppvObject = This; + return S_OK; + } + else + { + *ppvObject = 0; + return E_NOINTERFACE; + } +} + +static ULONG STDMETHODCALLTYPE CliprdrDataObject_AddRef(IDataObject* This) +{ + CliprdrDataObject* instance = (CliprdrDataObject*)This; + + if (!instance) + return E_INVALIDARG; + + return InterlockedIncrement(&instance->m_lRefCount); +} + +static ULONG STDMETHODCALLTYPE CliprdrDataObject_Release(IDataObject* This) +{ + LONG count; + CliprdrDataObject* instance = (CliprdrDataObject*)This; + + if (!instance) + return E_INVALIDARG; + + count = InterlockedDecrement(&instance->m_lRefCount); + + if (count == 0) + { + CliprdrDataObject_Delete(instance); + return 0; + } + else + return count; +} + +static HRESULT STDMETHODCALLTYPE CliprdrDataObject_GetData(IDataObject* This, FORMATETC* pFormatEtc, + STGMEDIUM* pMedium) +{ + ULONG i; + LONG idx; + CliprdrDataObject* instance = (CliprdrDataObject*)This; + wfClipboard* clipboard; + + if (!pFormatEtc || !pMedium || !instance) + return E_INVALIDARG; + + clipboard = (wfClipboard*)instance->m_pData; + + if (!clipboard) + return E_INVALIDARG; + + if ((idx = cliprdr_lookup_format(instance, pFormatEtc)) == -1) + return DV_E_FORMATETC; + + pMedium->tymed = instance->m_pFormatEtc[idx].tymed; + pMedium->pUnkForRelease = 0; + + if (instance->m_pFormatEtc[idx].cfFormat == RegisterClipboardFormat(CFSTR_FILEDESCRIPTORW)) + { + FILEGROUPDESCRIPTOR* dsc; + DWORD remote = get_remote_format_id(clipboard, instance->m_pFormatEtc[idx].cfFormat); + + if (cliprdr_send_data_request(clipboard, remote) != 0) + return E_UNEXPECTED; + + pMedium->hGlobal = clipboard->hmem; /* points to a FILEGROUPDESCRIPTOR structure */ + /* GlobalLock returns a pointer to the first byte of the memory block, + * in which is a FILEGROUPDESCRIPTOR structure, whose first UINT member + * is the number of FILEDESCRIPTOR's */ + dsc = (FILEGROUPDESCRIPTOR*)GlobalLock(clipboard->hmem); + instance->m_nStreams = dsc->cItems; + GlobalUnlock(clipboard->hmem); + + if (instance->m_nStreams > 0) + { + if (!instance->m_pStream) + { + instance->m_pStream = (LPSTREAM*)calloc(instance->m_nStreams, sizeof(LPSTREAM)); + + if (instance->m_pStream) + { + for (i = 0; i < instance->m_nStreams; i++) + { + instance->m_pStream[i] = + (IStream*)CliprdrStream_New(i, clipboard, &dsc->fgd[i]); + + if (!instance->m_pStream[i]) + return E_OUTOFMEMORY; + } + } + } + } + + if (!instance->m_pStream) + { + if (clipboard->hmem) + { + GlobalFree(clipboard->hmem); + clipboard->hmem = NULL; + } + + pMedium->hGlobal = NULL; + return E_OUTOFMEMORY; + } + } + else if (instance->m_pFormatEtc[idx].cfFormat == RegisterClipboardFormat(CFSTR_FILECONTENTS)) + { + if (pFormatEtc->lindex < instance->m_nStreams) + { + pMedium->pstm = instance->m_pStream[pFormatEtc->lindex]; + IDataObject_AddRef(instance->m_pStream[pFormatEtc->lindex]); + } + else + return E_INVALIDARG; + } + else + return E_UNEXPECTED; + + return S_OK; +} + +static HRESULT STDMETHODCALLTYPE CliprdrDataObject_GetDataHere(IDataObject* This, + FORMATETC* pformatetc, + STGMEDIUM* pmedium) +{ + (void)This; + (void)pformatetc; + (void)pmedium; + return E_NOTIMPL; +} + +static HRESULT STDMETHODCALLTYPE CliprdrDataObject_QueryGetData(IDataObject* This, + FORMATETC* pformatetc) +{ + CliprdrDataObject* instance = (CliprdrDataObject*)This; + + if (!pformatetc) + return E_INVALIDARG; + + if (cliprdr_lookup_format(instance, pformatetc) == -1) + return DV_E_FORMATETC; + + return S_OK; +} + +static HRESULT STDMETHODCALLTYPE CliprdrDataObject_GetCanonicalFormatEtc(IDataObject* This, + FORMATETC* pformatectIn, + FORMATETC* pformatetcOut) +{ + (void)This; + (void)pformatectIn; + + if (!pformatetcOut) + return E_INVALIDARG; + + pformatetcOut->ptd = NULL; + return E_NOTIMPL; +} + +static HRESULT STDMETHODCALLTYPE CliprdrDataObject_SetData(IDataObject* This, FORMATETC* pformatetc, + STGMEDIUM* pmedium, BOOL fRelease) +{ + (void)This; + (void)pformatetc; + (void)pmedium; + (void)fRelease; + return E_NOTIMPL; +} + +static HRESULT STDMETHODCALLTYPE CliprdrDataObject_EnumFormatEtc(IDataObject* This, + DWORD dwDirection, + IEnumFORMATETC** ppenumFormatEtc) +{ + CliprdrDataObject* instance = (CliprdrDataObject*)This; + + if (!instance || !ppenumFormatEtc) + return E_INVALIDARG; + + if (dwDirection == DATADIR_GET) + { + *ppenumFormatEtc = (IEnumFORMATETC*)CliprdrEnumFORMATETC_New(instance->m_nNumFormats, + instance->m_pFormatEtc); + return (*ppenumFormatEtc) ? S_OK : E_OUTOFMEMORY; + } + else + { + return E_NOTIMPL; + } +} + +static HRESULT STDMETHODCALLTYPE CliprdrDataObject_DAdvise(IDataObject* This, FORMATETC* pformatetc, + DWORD advf, IAdviseSink* pAdvSink, + DWORD* pdwConnection) +{ + (void)This; + (void)pformatetc; + (void)advf; + (void)pAdvSink; + (void)pdwConnection; + return OLE_E_ADVISENOTSUPPORTED; +} + +static HRESULT STDMETHODCALLTYPE CliprdrDataObject_DUnadvise(IDataObject* This, DWORD dwConnection) +{ + (void)This; + (void)dwConnection; + return OLE_E_ADVISENOTSUPPORTED; +} + +static HRESULT STDMETHODCALLTYPE CliprdrDataObject_EnumDAdvise(IDataObject* This, + IEnumSTATDATA** ppenumAdvise) +{ + (void)This; + (void)ppenumAdvise; + return OLE_E_ADVISENOTSUPPORTED; +} + +static CliprdrDataObject* CliprdrDataObject_New(FORMATETC* fmtetc, STGMEDIUM* stgmed, ULONG count, + void* data) +{ + int i; + CliprdrDataObject* instance; + IDataObject* iDataObject; + instance = (CliprdrDataObject*)calloc(1, sizeof(CliprdrDataObject)); + + if (!instance) + goto error; + + iDataObject = &instance->iDataObject; + iDataObject->lpVtbl = (IDataObjectVtbl*)calloc(1, sizeof(IDataObjectVtbl)); + + if (!iDataObject->lpVtbl) + goto error; + + iDataObject->lpVtbl->QueryInterface = CliprdrDataObject_QueryInterface; + iDataObject->lpVtbl->AddRef = CliprdrDataObject_AddRef; + iDataObject->lpVtbl->Release = CliprdrDataObject_Release; + iDataObject->lpVtbl->GetData = CliprdrDataObject_GetData; + iDataObject->lpVtbl->GetDataHere = CliprdrDataObject_GetDataHere; + iDataObject->lpVtbl->QueryGetData = CliprdrDataObject_QueryGetData; + iDataObject->lpVtbl->GetCanonicalFormatEtc = CliprdrDataObject_GetCanonicalFormatEtc; + iDataObject->lpVtbl->SetData = CliprdrDataObject_SetData; + iDataObject->lpVtbl->EnumFormatEtc = CliprdrDataObject_EnumFormatEtc; + iDataObject->lpVtbl->DAdvise = CliprdrDataObject_DAdvise; + iDataObject->lpVtbl->DUnadvise = CliprdrDataObject_DUnadvise; + iDataObject->lpVtbl->EnumDAdvise = CliprdrDataObject_EnumDAdvise; + instance->m_lRefCount = 1; + instance->m_nNumFormats = count; + instance->m_pData = data; + instance->m_nStreams = 0; + instance->m_pStream = NULL; + + if (count > 0) + { + instance->m_pFormatEtc = (FORMATETC*)calloc(count, sizeof(FORMATETC)); + + if (!instance->m_pFormatEtc) + goto error; + + instance->m_pStgMedium = (STGMEDIUM*)calloc(count, sizeof(STGMEDIUM)); + + if (!instance->m_pStgMedium) + goto error; + + for (i = 0; i < count; i++) + { + instance->m_pFormatEtc[i] = fmtetc[i]; + instance->m_pStgMedium[i] = stgmed[i]; + } + } + + return instance; +error: + CliprdrDataObject_Delete(instance); + return NULL; +} + +void CliprdrDataObject_Delete(CliprdrDataObject* instance) +{ + if (instance) + { + free(instance->iDataObject.lpVtbl); + free(instance->m_pFormatEtc); + free(instance->m_pStgMedium); + + if (instance->m_pStream) + { + ULONG i; + + for (i = 0; i < instance->m_nStreams; i++) + CliprdrStream_Release(instance->m_pStream[i]); + + free(instance->m_pStream); + } + + free(instance); + } +} + +static BOOL wf_create_file_obj(wfClipboard* clipboard, IDataObject** ppDataObject) +{ + FORMATETC fmtetc[2]; + STGMEDIUM stgmeds[2]; + + if (!ppDataObject) + return FALSE; + + fmtetc[0].cfFormat = RegisterClipboardFormat(CFSTR_FILEDESCRIPTORW); + fmtetc[0].dwAspect = DVASPECT_CONTENT; + fmtetc[0].lindex = 0; + fmtetc[0].ptd = NULL; + fmtetc[0].tymed = TYMED_HGLOBAL; + stgmeds[0].tymed = TYMED_HGLOBAL; + stgmeds[0].hGlobal = NULL; + stgmeds[0].pUnkForRelease = NULL; + fmtetc[1].cfFormat = RegisterClipboardFormat(CFSTR_FILECONTENTS); + fmtetc[1].dwAspect = DVASPECT_CONTENT; + fmtetc[1].lindex = 0; + fmtetc[1].ptd = NULL; + fmtetc[1].tymed = TYMED_ISTREAM; + stgmeds[1].tymed = TYMED_ISTREAM; + stgmeds[1].pstm = NULL; + stgmeds[1].pUnkForRelease = NULL; + *ppDataObject = (IDataObject*)CliprdrDataObject_New(fmtetc, stgmeds, 2, clipboard); + return (*ppDataObject) ? TRUE : FALSE; +} + +static void wf_destroy_file_obj(IDataObject* instance) +{ + if (instance) + IDataObject_Release(instance); +} + +/** + * IEnumFORMATETC + */ + +static void cliprdr_format_deep_copy(FORMATETC* dest, FORMATETC* source) +{ + *dest = *source; + + if (source->ptd) + { + dest->ptd = (DVTARGETDEVICE*)CoTaskMemAlloc(sizeof(DVTARGETDEVICE)); + + if (dest->ptd) + *(dest->ptd) = *(source->ptd); + } +} + +static HRESULT STDMETHODCALLTYPE CliprdrEnumFORMATETC_QueryInterface(IEnumFORMATETC* This, + REFIID riid, void** ppvObject) +{ + (void)This; + + if (IsEqualIID(riid, &IID_IEnumFORMATETC) || IsEqualIID(riid, &IID_IUnknown)) + { + IEnumFORMATETC_AddRef(This); + *ppvObject = This; + return S_OK; + } + else + { + *ppvObject = 0; + return E_NOINTERFACE; + } +} + +static ULONG STDMETHODCALLTYPE CliprdrEnumFORMATETC_AddRef(IEnumFORMATETC* This) +{ + CliprdrEnumFORMATETC* instance = (CliprdrEnumFORMATETC*)This; + + if (!instance) + return 0; + + return InterlockedIncrement(&instance->m_lRefCount); +} + +static ULONG STDMETHODCALLTYPE CliprdrEnumFORMATETC_Release(IEnumFORMATETC* This) +{ + LONG count; + CliprdrEnumFORMATETC* instance = (CliprdrEnumFORMATETC*)This; + + if (!instance) + return 0; + + count = InterlockedDecrement(&instance->m_lRefCount); + + if (count == 0) + { + CliprdrEnumFORMATETC_Delete(instance); + return 0; + } + else + { + return count; + } +} + +static HRESULT STDMETHODCALLTYPE CliprdrEnumFORMATETC_Next(IEnumFORMATETC* This, ULONG celt, + FORMATETC* rgelt, ULONG* pceltFetched) +{ + ULONG copied = 0; + CliprdrEnumFORMATETC* instance = (CliprdrEnumFORMATETC*)This; + + if (!instance || !celt || !rgelt) + return E_INVALIDARG; + + while ((instance->m_nIndex < instance->m_nNumFormats) && (copied < celt)) + { + cliprdr_format_deep_copy(&rgelt[copied++], &instance->m_pFormatEtc[instance->m_nIndex++]); + } + + if (pceltFetched != 0) + *pceltFetched = copied; + + return (copied == celt) ? S_OK : E_FAIL; +} + +static HRESULT STDMETHODCALLTYPE CliprdrEnumFORMATETC_Skip(IEnumFORMATETC* This, ULONG celt) +{ + CliprdrEnumFORMATETC* instance = (CliprdrEnumFORMATETC*)This; + + if (!instance) + return E_INVALIDARG; + + if (instance->m_nIndex + (LONG)celt > instance->m_nNumFormats) + return E_FAIL; + + instance->m_nIndex += celt; + return S_OK; +} + +static HRESULT STDMETHODCALLTYPE CliprdrEnumFORMATETC_Reset(IEnumFORMATETC* This) +{ + CliprdrEnumFORMATETC* instance = (CliprdrEnumFORMATETC*)This; + + if (!instance) + return E_INVALIDARG; + + instance->m_nIndex = 0; + return S_OK; +} + +static HRESULT STDMETHODCALLTYPE CliprdrEnumFORMATETC_Clone(IEnumFORMATETC* This, + IEnumFORMATETC** ppEnum) +{ + CliprdrEnumFORMATETC* instance = (CliprdrEnumFORMATETC*)This; + + if (!instance || !ppEnum) + return E_INVALIDARG; + + *ppEnum = + (IEnumFORMATETC*)CliprdrEnumFORMATETC_New(instance->m_nNumFormats, instance->m_pFormatEtc); + + if (!*ppEnum) + return E_OUTOFMEMORY; + + ((CliprdrEnumFORMATETC*)*ppEnum)->m_nIndex = instance->m_nIndex; + return S_OK; +} + +CliprdrEnumFORMATETC* CliprdrEnumFORMATETC_New(ULONG nFormats, FORMATETC* pFormatEtc) +{ + ULONG i; + CliprdrEnumFORMATETC* instance; + IEnumFORMATETC* iEnumFORMATETC; + + if ((nFormats != 0) && !pFormatEtc) + return NULL; + + instance = (CliprdrEnumFORMATETC*)calloc(1, sizeof(CliprdrEnumFORMATETC)); + + if (!instance) + goto error; + + iEnumFORMATETC = &instance->iEnumFORMATETC; + iEnumFORMATETC->lpVtbl = (IEnumFORMATETCVtbl*)calloc(1, sizeof(IEnumFORMATETCVtbl)); + + if (!iEnumFORMATETC->lpVtbl) + goto error; + + iEnumFORMATETC->lpVtbl->QueryInterface = CliprdrEnumFORMATETC_QueryInterface; + iEnumFORMATETC->lpVtbl->AddRef = CliprdrEnumFORMATETC_AddRef; + iEnumFORMATETC->lpVtbl->Release = CliprdrEnumFORMATETC_Release; + iEnumFORMATETC->lpVtbl->Next = CliprdrEnumFORMATETC_Next; + iEnumFORMATETC->lpVtbl->Skip = CliprdrEnumFORMATETC_Skip; + iEnumFORMATETC->lpVtbl->Reset = CliprdrEnumFORMATETC_Reset; + iEnumFORMATETC->lpVtbl->Clone = CliprdrEnumFORMATETC_Clone; + instance->m_lRefCount = 1; + instance->m_nIndex = 0; + instance->m_nNumFormats = nFormats; + + if (nFormats > 0) + { + instance->m_pFormatEtc = (FORMATETC*)calloc(nFormats, sizeof(FORMATETC)); + + if (!instance->m_pFormatEtc) + goto error; + + for (i = 0; i < nFormats; i++) + cliprdr_format_deep_copy(&instance->m_pFormatEtc[i], &pFormatEtc[i]); + } + + return instance; +error: + CliprdrEnumFORMATETC_Delete(instance); + return NULL; +} + +void CliprdrEnumFORMATETC_Delete(CliprdrEnumFORMATETC* instance) +{ + LONG i; + + if (instance) + { + free(instance->iEnumFORMATETC.lpVtbl); + + if (instance->m_pFormatEtc) + { + for (i = 0; i < instance->m_nNumFormats; i++) + { + if (instance->m_pFormatEtc[i].ptd) + CoTaskMemFree(instance->m_pFormatEtc[i].ptd); + } + + free(instance->m_pFormatEtc); + } + + free(instance); + } +} + +/***********************************************************************************/ + +static UINT32 get_local_format_id_by_name(wfClipboard* clipboard, const TCHAR* format_name) +{ + size_t i; + formatMapping* map; + WCHAR* unicode_name; +#if !defined(UNICODE) + size_t size; +#endif + + if (!clipboard || !format_name) + return 0; + +#if defined(UNICODE) + unicode_name = _wcsdup(format_name); +#else + size = _tcslen(format_name); + unicode_name = calloc(size + 1, sizeof(WCHAR)); + + if (!unicode_name) + return 0; + + MultiByteToWideChar(CP_OEMCP, 0, format_name, strlen(format_name), unicode_name, size); +#endif + + if (!unicode_name) + return 0; + + for (i = 0; i < clipboard->map_size; i++) + { + map = &clipboard->format_mappings[i]; + + if (map->name) + { + if (wcscmp(map->name, unicode_name) == 0) + { + free(unicode_name); + return map->local_format_id; + } + } + } + + free(unicode_name); + return 0; +} + +static INLINE BOOL file_transferring(wfClipboard* clipboard) +{ + return get_local_format_id_by_name(clipboard, CFSTR_FILEDESCRIPTORW) ? TRUE : FALSE; +} + +static UINT32 get_remote_format_id(wfClipboard* clipboard, UINT32 local_format) +{ + UINT32 i; + formatMapping* map; + + if (!clipboard) + return 0; + + for (i = 0; i < clipboard->map_size; i++) + { + map = &clipboard->format_mappings[i]; + + if (map->local_format_id == local_format) + return map->remote_format_id; + } + + return local_format; +} + +static void map_ensure_capacity(wfClipboard* clipboard) +{ + if (!clipboard) + return; + + if (clipboard->map_size >= clipboard->map_capacity) + { + size_t new_size; + formatMapping* new_map; + new_size = clipboard->map_capacity * 2; + new_map = + (formatMapping*)realloc(clipboard->format_mappings, sizeof(formatMapping) * new_size); + + if (!new_map) + return; + + clipboard->format_mappings = new_map; + clipboard->map_capacity = new_size; + } +} + +static BOOL clear_format_map(wfClipboard* clipboard) +{ + size_t i; + formatMapping* map; + + if (!clipboard) + return FALSE; + + if (clipboard->format_mappings) + { + for (i = 0; i < clipboard->map_capacity; i++) + { + map = &clipboard->format_mappings[i]; + map->remote_format_id = 0; + map->local_format_id = 0; + free(map->name); + map->name = NULL; + } + } + + clipboard->map_size = 0; + return TRUE; +} + +static UINT cliprdr_send_tempdir(wfClipboard* clipboard) +{ + CLIPRDR_TEMP_DIRECTORY tempDirectory; + + if (!clipboard) + return -1; + + if (GetEnvironmentVariableA("TEMP", tempDirectory.szTempDir, sizeof(tempDirectory.szTempDir)) == + 0) + return -1; + + return clipboard->context->TempDirectory(clipboard->context, &tempDirectory); +} + +static BOOL cliprdr_GetUpdatedClipboardFormats(wfClipboard* clipboard, PUINT lpuiFormats, + UINT cFormats, PUINT pcFormatsOut) +{ + UINT index = 0; + UINT format = 0; + BOOL clipboardOpen = FALSE; + + if (!clipboard->legacyApi) + return clipboard->GetUpdatedClipboardFormats(lpuiFormats, cFormats, pcFormatsOut); + + clipboardOpen = try_open_clipboard(clipboard->hwnd); + + if (!clipboardOpen) + { + *pcFormatsOut = 0; + return TRUE; /* Other app holding clipboard */ + } + + while (index < cFormats) + { + format = EnumClipboardFormats(format); + + if (!format) + break; + + lpuiFormats[index] = format; + index++; + } + + *pcFormatsOut = index; + CloseClipboard(); + return TRUE; +} + +static UINT cliprdr_send_format_list(wfClipboard* clipboard) +{ + UINT rc; + int count = 0; + UINT32 index; + UINT32 numFormats = 0; + UINT32 formatId = 0; + char formatName[1024]; + CLIPRDR_FORMAT* formats = NULL; + CLIPRDR_FORMAT_LIST formatList = { 0 }; + + if (!clipboard) + return ERROR_INTERNAL_ERROR; + + ZeroMemory(&formatList, sizeof(CLIPRDR_FORMAT_LIST)); + + /* Ignore if other app is holding clipboard */ + if (try_open_clipboard(clipboard->hwnd)) + { + count = CountClipboardFormats(); + numFormats = (UINT32)count; + formats = (CLIPRDR_FORMAT*)calloc(numFormats, sizeof(CLIPRDR_FORMAT)); + + if (!formats) + { + CloseClipboard(); + return CHANNEL_RC_NO_MEMORY; + } + + index = 0; + + if (IsClipboardFormatAvailable(CF_HDROP)) + { + formats[index++].formatId = RegisterClipboardFormat(CFSTR_FILEDESCRIPTORW); + formats[index++].formatId = RegisterClipboardFormat(CFSTR_FILECONTENTS); + } + else + { + while (formatId = EnumClipboardFormats(formatId)) + formats[index++].formatId = formatId; + } + + numFormats = index; + + if (!CloseClipboard()) + { + free(formats); + return ERROR_INTERNAL_ERROR; + } + + for (index = 0; index < numFormats; index++) + { + if (GetClipboardFormatNameA(formats[index].formatId, formatName, sizeof(formatName))) + { + formats[index].formatName = _strdup(formatName); + } + } + } + + formatList.numFormats = numFormats; + formatList.formats = formats; + formatList.msgType = CB_FORMAT_LIST; + rc = clipboard->context->ClientFormatList(clipboard->context, &formatList); + + for (index = 0; index < numFormats; index++) + free(formats[index].formatName); + + free(formats); + return rc; +} + +static UINT cliprdr_send_data_request(wfClipboard* clipboard, UINT32 formatId) +{ + UINT rc; + CLIPRDR_FORMAT_DATA_REQUEST formatDataRequest; + + if (!clipboard || !clipboard->context || !clipboard->context->ClientFormatDataRequest) + return ERROR_INTERNAL_ERROR; + + formatDataRequest.requestedFormatId = formatId; + clipboard->requestedFormatId = formatId; + rc = clipboard->context->ClientFormatDataRequest(clipboard->context, &formatDataRequest); + + if (WaitForSingleObject(clipboard->response_data_event, INFINITE) != WAIT_OBJECT_0) + rc = ERROR_INTERNAL_ERROR; + else if (!ResetEvent(clipboard->response_data_event)) + rc = ERROR_INTERNAL_ERROR; + + return rc; +} + +UINT cliprdr_send_request_filecontents(wfClipboard* clipboard, const void* streamid, ULONG index, + UINT32 flag, DWORD positionhigh, DWORD positionlow, + ULONG nreq) +{ + UINT rc; + CLIPRDR_FILE_CONTENTS_REQUEST fileContentsRequest; + + if (!clipboard || !clipboard->context || !clipboard->context->ClientFileContentsRequest) + return ERROR_INTERNAL_ERROR; + + fileContentsRequest.streamId = (UINT32)streamid; + fileContentsRequest.listIndex = index; + fileContentsRequest.dwFlags = flag; + fileContentsRequest.nPositionLow = positionlow; + fileContentsRequest.nPositionHigh = positionhigh; + fileContentsRequest.cbRequested = nreq; + fileContentsRequest.clipDataId = 0; + fileContentsRequest.msgFlags = 0; + rc = clipboard->context->ClientFileContentsRequest(clipboard->context, &fileContentsRequest); + + if (WaitForSingleObject(clipboard->req_fevent, INFINITE) != WAIT_OBJECT_0) + rc = ERROR_INTERNAL_ERROR; + else if (!ResetEvent(clipboard->req_fevent)) + rc = ERROR_INTERNAL_ERROR; + + return rc; +} + +static UINT cliprdr_send_response_filecontents(wfClipboard* clipboard, UINT32 streamId, UINT32 size, + BYTE* data) +{ + CLIPRDR_FILE_CONTENTS_RESPONSE fileContentsResponse; + + if (!clipboard || !clipboard->context || !clipboard->context->ClientFileContentsResponse) + return ERROR_INTERNAL_ERROR; + + fileContentsResponse.streamId = streamId; + fileContentsResponse.cbRequested = size; + fileContentsResponse.requestedData = data; + fileContentsResponse.msgFlags = CB_RESPONSE_OK; + return clipboard->context->ClientFileContentsResponse(clipboard->context, + &fileContentsResponse); +} + +static LRESULT CALLBACK cliprdr_proc(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam) +{ + static wfClipboard* clipboard = NULL; + + switch (Msg) + { + case WM_CREATE: + DEBUG_CLIPRDR("info: WM_CREATE"); + clipboard = (wfClipboard*)((CREATESTRUCT*)lParam)->lpCreateParams; + clipboard->hwnd = hWnd; + + if (!clipboard->legacyApi) + clipboard->AddClipboardFormatListener(hWnd); + else + clipboard->hWndNextViewer = SetClipboardViewer(hWnd); + + break; + + case WM_CLOSE: + DEBUG_CLIPRDR("info: WM_CLOSE"); + + if (!clipboard->legacyApi) + clipboard->RemoveClipboardFormatListener(hWnd); + + break; + + case WM_DESTROY: + if (clipboard->legacyApi) + ChangeClipboardChain(hWnd, clipboard->hWndNextViewer); + + break; + + case WM_CLIPBOARDUPDATE: + DEBUG_CLIPRDR("info: WM_CLIPBOARDUPDATE"); + + if (clipboard->sync) + { + if ((GetClipboardOwner() != clipboard->hwnd) && + (S_FALSE == OleIsCurrentClipboard(clipboard->data_obj))) + { + if (clipboard->hmem) + { + GlobalFree(clipboard->hmem); + clipboard->hmem = NULL; + } + + cliprdr_send_format_list(clipboard); + } + } + + break; + + case WM_RENDERALLFORMATS: + DEBUG_CLIPRDR("info: WM_RENDERALLFORMATS"); + + /* discard all contexts in clipboard */ + if (!try_open_clipboard(clipboard->hwnd)) + { + DEBUG_CLIPRDR("OpenClipboard failed with 0x%x", GetLastError()); + break; + } + + EmptyClipboard(); + CloseClipboard(); + break; + + case WM_RENDERFORMAT: + DEBUG_CLIPRDR("info: WM_RENDERFORMAT"); + + if (cliprdr_send_data_request(clipboard, (UINT32)wParam) != 0) + { + DEBUG_CLIPRDR("error: cliprdr_send_data_request failed."); + break; + } + + if (!SetClipboardData((UINT)wParam, clipboard->hmem)) + { + DEBUG_CLIPRDR("SetClipboardData failed with 0x%x", GetLastError()); + + if (clipboard->hmem) + { + GlobalFree(clipboard->hmem); + clipboard->hmem = NULL; + } + } + + /* Note: GlobalFree() is not needed when success */ + break; + + case WM_DRAWCLIPBOARD: + if (clipboard->legacyApi) + { + if ((GetClipboardOwner() != clipboard->hwnd) && + (S_FALSE == OleIsCurrentClipboard(clipboard->data_obj))) + { + cliprdr_send_format_list(clipboard); + } + + SendMessage(clipboard->hWndNextViewer, Msg, wParam, lParam); + } + + break; + + case WM_CHANGECBCHAIN: + if (clipboard->legacyApi) + { + HWND hWndCurrViewer = (HWND)wParam; + HWND hWndNextViewer = (HWND)lParam; + + if (hWndCurrViewer == clipboard->hWndNextViewer) + clipboard->hWndNextViewer = hWndNextViewer; + else if (clipboard->hWndNextViewer) + SendMessage(clipboard->hWndNextViewer, Msg, wParam, lParam); + } + + break; + + case WM_CLIPRDR_MESSAGE: + DEBUG_CLIPRDR("info: WM_CLIPRDR_MESSAGE"); + + switch (wParam) + { + case OLE_SETCLIPBOARD: + DEBUG_CLIPRDR("info: OLE_SETCLIPBOARD"); + + if (S_FALSE == OleIsCurrentClipboard(clipboard->data_obj)) + { + if (wf_create_file_obj(clipboard, &clipboard->data_obj)) + { + if (OleSetClipboard(clipboard->data_obj) != S_OK) + { + wf_destroy_file_obj(clipboard->data_obj); + clipboard->data_obj = NULL; + } + } + } + + break; + + default: + break; + } + + break; + + case WM_DESTROYCLIPBOARD: + case WM_ASKCBFORMATNAME: + case WM_HSCROLLCLIPBOARD: + case WM_PAINTCLIPBOARD: + case WM_SIZECLIPBOARD: + case WM_VSCROLLCLIPBOARD: + default: + return DefWindowProc(hWnd, Msg, wParam, lParam); + } + + return 0; +} + +static int create_cliprdr_window(wfClipboard* clipboard) +{ + WNDCLASSEX wnd_cls; + ZeroMemory(&wnd_cls, sizeof(WNDCLASSEX)); + wnd_cls.cbSize = sizeof(WNDCLASSEX); + wnd_cls.style = CS_OWNDC; + wnd_cls.lpfnWndProc = cliprdr_proc; + wnd_cls.cbClsExtra = 0; + wnd_cls.cbWndExtra = 0; + wnd_cls.hIcon = NULL; + wnd_cls.hCursor = NULL; + wnd_cls.hbrBackground = NULL; + wnd_cls.lpszMenuName = NULL; + wnd_cls.lpszClassName = _T("ClipboardHiddenMessageProcessor"); + wnd_cls.hInstance = GetModuleHandle(NULL); + wnd_cls.hIconSm = NULL; + RegisterClassEx(&wnd_cls); + clipboard->hwnd = + CreateWindowEx(WS_EX_LEFT, _T("ClipboardHiddenMessageProcessor"), _T("rdpclip"), 0, 0, 0, 0, + 0, HWND_MESSAGE, NULL, GetModuleHandle(NULL), clipboard); + + if (!clipboard->hwnd) + { + DEBUG_CLIPRDR("error: CreateWindowEx failed with %x.", GetLastError()); + return -1; + } + + return 0; +} + +static DWORD WINAPI cliprdr_thread_func(LPVOID arg) +{ + int ret; + MSG msg; + BOOL mcode; + wfClipboard* clipboard = (wfClipboard*)arg; + OleInitialize(0); + + if ((ret = create_cliprdr_window(clipboard)) != 0) + { + OleUninitialize(); + DEBUG_CLIPRDR("error: create clipboard window failed."); + return 0; + } + + while ((mcode = GetMessage(&msg, 0, 0, 0)) != 0) + { + if (mcode == -1) + { + DEBUG_CLIPRDR("error: clipboard thread GetMessage failed."); + break; + } + else + { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + } + + OleUninitialize(); + return 0; +} + +static void clear_file_array(wfClipboard* clipboard) +{ + size_t i; + + if (!clipboard) + return; + + /* clear file_names array */ + if (clipboard->file_names) + { + for (i = 0; i < clipboard->nFiles; i++) + { + free(clipboard->file_names[i]); + clipboard->file_names[i] = NULL; + } + + free(clipboard->file_names); + clipboard->file_names = NULL; + } + + /* clear fileDescriptor array */ + if (clipboard->fileDescriptor) + { + for (i = 0; i < clipboard->nFiles; i++) + { + free(clipboard->fileDescriptor[i]); + clipboard->fileDescriptor[i] = NULL; + } + + free(clipboard->fileDescriptor); + clipboard->fileDescriptor = NULL; + } + + clipboard->file_array_size = 0; + clipboard->nFiles = 0; +} + +static BOOL wf_cliprdr_get_file_contents(WCHAR* file_name, BYTE* buffer, LONG positionLow, + LONG positionHigh, DWORD nRequested, DWORD* puSize) +{ + BOOL res = FALSE; + HANDLE hFile; + DWORD nGet, rc; + + if (!file_name || !buffer || !puSize) + { + WLog_ERR(TAG, "get file contents Invalid Arguments."); + return FALSE; + } + + hFile = CreateFileW(file_name, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL | FILE_FLAG_BACKUP_SEMANTICS, NULL); + + if (hFile == INVALID_HANDLE_VALUE) + return FALSE; + + rc = SetFilePointer(hFile, positionLow, &positionHigh, FILE_BEGIN); + + if (rc == INVALID_SET_FILE_POINTER) + goto error; + + if (!ReadFile(hFile, buffer, nRequested, &nGet, NULL)) + { + DEBUG_CLIPRDR("ReadFile failed with 0x%08lX.", GetLastError()); + goto error; + } + + res = TRUE; +error: + + if (!CloseHandle(hFile)) + res = FALSE; + + if (res) + *puSize = nGet; + + return res; +} + +/* path_name has a '\' at the end. e.g. c:\newfolder\, file_name is c:\newfolder\new.txt */ +static FILEDESCRIPTORW* wf_cliprdr_get_file_descriptor(WCHAR* file_name, size_t pathLen) +{ + HANDLE hFile; + FILEDESCRIPTORW* fd; + fd = (FILEDESCRIPTORW*)calloc(1, sizeof(FILEDESCRIPTORW)); + + if (!fd) + return NULL; + + hFile = CreateFileW(file_name, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL | FILE_FLAG_BACKUP_SEMANTICS, NULL); + + if (hFile == INVALID_HANDLE_VALUE) + { + free(fd); + return NULL; + } + + fd->dwFlags = FD_ATTRIBUTES | FD_FILESIZE | FD_WRITESTIME | FD_PROGRESSUI; + fd->dwFileAttributes = GetFileAttributes(file_name); + + if (!GetFileTime(hFile, NULL, NULL, &fd->ftLastWriteTime)) + { + fd->dwFlags &= ~FD_WRITESTIME; + } + + fd->nFileSizeLow = GetFileSize(hFile, &fd->nFileSizeHigh); + wcscpy_s(fd->cFileName, sizeof(fd->cFileName) / 2, file_name + pathLen); + CloseHandle(hFile); + return fd; +} + +static BOOL wf_cliprdr_array_ensure_capacity(wfClipboard* clipboard) +{ + if (!clipboard) + return FALSE; + + if (clipboard->nFiles == clipboard->file_array_size) + { + size_t new_size; + FILEDESCRIPTORW** new_fd; + WCHAR** new_name; + new_size = (clipboard->file_array_size + 1) * 2; + new_fd = (FILEDESCRIPTORW**)realloc(clipboard->fileDescriptor, + new_size * sizeof(FILEDESCRIPTORW*)); + + if (new_fd) + clipboard->fileDescriptor = new_fd; + + new_name = (WCHAR**)realloc(clipboard->file_names, new_size * sizeof(WCHAR*)); + + if (new_name) + clipboard->file_names = new_name; + + if (!new_fd || !new_name) + return FALSE; + + clipboard->file_array_size = new_size; + } + + return TRUE; +} + +static BOOL wf_cliprdr_add_to_file_arrays(wfClipboard* clipboard, WCHAR* full_file_name, + size_t pathLen) +{ + if (!wf_cliprdr_array_ensure_capacity(clipboard)) + return FALSE; + + /* add to name array */ + clipboard->file_names[clipboard->nFiles] = (LPWSTR)malloc(MAX_PATH * 2); + + if (!clipboard->file_names[clipboard->nFiles]) + return FALSE; + + wcscpy_s(clipboard->file_names[clipboard->nFiles], MAX_PATH, full_file_name); + /* add to descriptor array */ + clipboard->fileDescriptor[clipboard->nFiles] = + wf_cliprdr_get_file_descriptor(full_file_name, pathLen); + + if (!clipboard->fileDescriptor[clipboard->nFiles]) + { + free(clipboard->file_names[clipboard->nFiles]); + return FALSE; + } + + clipboard->nFiles++; + return TRUE; +} + +static BOOL wf_cliprdr_traverse_directory(wfClipboard* clipboard, WCHAR* Dir, size_t pathLen) +{ + HANDLE hFind; + WCHAR DirSpec[MAX_PATH]; + WIN32_FIND_DATA FindFileData; + + if (!clipboard || !Dir) + return FALSE; + + StringCchCopy(DirSpec, MAX_PATH, Dir); + StringCchCat(DirSpec, MAX_PATH, TEXT("\\*")); + hFind = FindFirstFile(DirSpec, &FindFileData); + + if (hFind == INVALID_HANDLE_VALUE) + { + DEBUG_CLIPRDR("FindFirstFile failed with 0x%x.", GetLastError()); + return FALSE; + } + + while (FindNextFile(hFind, &FindFileData)) + { + if ((FindFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0 && + wcscmp(FindFileData.cFileName, _T(".")) == 0 || + wcscmp(FindFileData.cFileName, _T("..")) == 0) + { + continue; + } + + if ((FindFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0) + { + WCHAR DirAdd[MAX_PATH]; + StringCchCopy(DirAdd, MAX_PATH, Dir); + StringCchCat(DirAdd, MAX_PATH, _T("\\")); + StringCchCat(DirAdd, MAX_PATH, FindFileData.cFileName); + + if (!wf_cliprdr_add_to_file_arrays(clipboard, DirAdd, pathLen)) + return FALSE; + + if (!wf_cliprdr_traverse_directory(clipboard, DirAdd, pathLen)) + return FALSE; + } + else + { + WCHAR fileName[MAX_PATH]; + StringCchCopy(fileName, MAX_PATH, Dir); + StringCchCat(fileName, MAX_PATH, _T("\\")); + StringCchCat(fileName, MAX_PATH, FindFileData.cFileName); + + if (!wf_cliprdr_add_to_file_arrays(clipboard, fileName, pathLen)) + return FALSE; + } + } + + FindClose(hFind); + return TRUE; +} + +static UINT wf_cliprdr_send_client_capabilities(wfClipboard* clipboard) +{ + CLIPRDR_CAPABILITIES capabilities; + CLIPRDR_GENERAL_CAPABILITY_SET generalCapabilitySet; + + if (!clipboard || !clipboard->context || !clipboard->context->ClientCapabilities) + return ERROR_INTERNAL_ERROR; + + capabilities.cCapabilitiesSets = 1; + capabilities.capabilitySets = (CLIPRDR_CAPABILITY_SET*)&(generalCapabilitySet); + generalCapabilitySet.capabilitySetType = CB_CAPSTYPE_GENERAL; + generalCapabilitySet.capabilitySetLength = 12; + generalCapabilitySet.version = CB_CAPS_VERSION_2; + generalCapabilitySet.generalFlags = + CB_USE_LONG_FORMAT_NAMES | CB_STREAM_FILECLIP_ENABLED | CB_FILECLIP_NO_FILE_PATHS; + return clipboard->context->ClientCapabilities(clipboard->context, &capabilities); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT wf_cliprdr_monitor_ready(CliprdrClientContext* context, + const CLIPRDR_MONITOR_READY* monitorReady) +{ + UINT rc; + wfClipboard* clipboard = (wfClipboard*)context->custom; + + if (!context || !monitorReady) + return ERROR_INTERNAL_ERROR; + + clipboard->sync = TRUE; + rc = wf_cliprdr_send_client_capabilities(clipboard); + + if (rc != CHANNEL_RC_OK) + return rc; + + return cliprdr_send_format_list(clipboard); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT wf_cliprdr_server_capabilities(CliprdrClientContext* context, + const CLIPRDR_CAPABILITIES* capabilities) +{ + UINT32 index; + CLIPRDR_CAPABILITY_SET* capabilitySet; + wfClipboard* clipboard = (wfClipboard*)context->custom; + + if (!context || !capabilities) + return ERROR_INTERNAL_ERROR; + + for (index = 0; index < capabilities->cCapabilitiesSets; index++) + { + capabilitySet = &(capabilities->capabilitySets[index]); + + if ((capabilitySet->capabilitySetType == CB_CAPSTYPE_GENERAL) && + (capabilitySet->capabilitySetLength >= CB_CAPSTYPE_GENERAL_LEN)) + { + CLIPRDR_GENERAL_CAPABILITY_SET* generalCapabilitySet = + (CLIPRDR_GENERAL_CAPABILITY_SET*)capabilitySet; + clipboard->capabilities = generalCapabilitySet->generalFlags; + break; + } + } + + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT wf_cliprdr_server_format_list(CliprdrClientContext* context, + const CLIPRDR_FORMAT_LIST* formatList) +{ + UINT rc = ERROR_INTERNAL_ERROR; + UINT32 i; + formatMapping* mapping; + CLIPRDR_FORMAT* format; + wfClipboard* clipboard = (wfClipboard*)context->custom; + + if (!clear_format_map(clipboard)) + return ERROR_INTERNAL_ERROR; + + for (i = 0; i < formatList->numFormats; i++) + { + format = &(formatList->formats[i]); + mapping = &(clipboard->format_mappings[i]); + mapping->remote_format_id = format->formatId; + + if (format->formatName) + { + int size = MultiByteToWideChar(CP_UTF8, 0, format->formatName, + strlen(format->formatName), NULL, 0); + mapping->name = calloc(size + 1, sizeof(WCHAR)); + + if (mapping->name) + { + MultiByteToWideChar(CP_UTF8, 0, format->formatName, strlen(format->formatName), + mapping->name, size); + mapping->local_format_id = RegisterClipboardFormatW((LPWSTR)mapping->name); + } + } + else + { + mapping->name = NULL; + mapping->local_format_id = mapping->remote_format_id; + } + + clipboard->map_size++; + map_ensure_capacity(clipboard); + } + + if (file_transferring(clipboard)) + { + if (PostMessage(clipboard->hwnd, WM_CLIPRDR_MESSAGE, OLE_SETCLIPBOARD, 0)) + rc = CHANNEL_RC_OK; + } + else + { + if (!try_open_clipboard(clipboard->hwnd)) + return CHANNEL_RC_OK; /* Ignore, other app holding clipboard */ + + if (EmptyClipboard()) + { + for (i = 0; i < (UINT32)clipboard->map_size; i++) + SetClipboardData(clipboard->format_mappings[i].local_format_id, NULL); + + rc = CHANNEL_RC_OK; + } + + if (!CloseClipboard() && GetLastError()) + return ERROR_INTERNAL_ERROR; + } + + return rc; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT +wf_cliprdr_server_format_list_response(CliprdrClientContext* context, + const CLIPRDR_FORMAT_LIST_RESPONSE* formatListResponse) +{ + (void)context; + (void)formatListResponse; + + if (formatListResponse->msgFlags != CB_RESPONSE_OK) + return E_FAIL; + + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT +wf_cliprdr_server_lock_clipboard_data(CliprdrClientContext* context, + const CLIPRDR_LOCK_CLIPBOARD_DATA* lockClipboardData) +{ + (void)context; + (void)lockClipboardData; + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT +wf_cliprdr_server_unlock_clipboard_data(CliprdrClientContext* context, + const CLIPRDR_UNLOCK_CLIPBOARD_DATA* unlockClipboardData) +{ + (void)context; + (void)unlockClipboardData; + return CHANNEL_RC_OK; +} + +static BOOL wf_cliprdr_process_filename(wfClipboard* clipboard, WCHAR* wFileName, size_t str_len) +{ + size_t pathLen; + size_t offset = str_len; + + if (!clipboard || !wFileName) + return FALSE; + + /* find the last '\' in full file name */ + while (offset > 0) + { + if (wFileName[offset] == L'\\') + break; + else + offset--; + } + + pathLen = offset + 1; + + if (!wf_cliprdr_add_to_file_arrays(clipboard, wFileName, pathLen)) + return FALSE; + + if ((clipboard->fileDescriptor[clipboard->nFiles - 1]->dwFileAttributes & + FILE_ATTRIBUTE_DIRECTORY) != 0) + { + /* this is a directory */ + if (!wf_cliprdr_traverse_directory(clipboard, wFileName, pathLen)) + return FALSE; + } + + return TRUE; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT +wf_cliprdr_server_format_data_request(CliprdrClientContext* context, + const CLIPRDR_FORMAT_DATA_REQUEST* formatDataRequest) +{ + UINT rc; + size_t size = 0; + void* buff = NULL; + char* globlemem = NULL; + HANDLE hClipdata = NULL; + UINT32 requestedFormatId; + CLIPRDR_FORMAT_DATA_RESPONSE response; + wfClipboard* clipboard; + + if (!context || !formatDataRequest) + return ERROR_INTERNAL_ERROR; + + clipboard = (wfClipboard*)context->custom; + + if (!clipboard) + return ERROR_INTERNAL_ERROR; + + requestedFormatId = formatDataRequest->requestedFormatId; + + if (requestedFormatId == RegisterClipboardFormat(CFSTR_FILEDESCRIPTORW)) + { + size_t len; + size_t i; + WCHAR* wFileName; + HRESULT result; + LPDATAOBJECT dataObj; + FORMATETC format_etc; + STGMEDIUM stg_medium; + DROPFILES* dropFiles; + FILEGROUPDESCRIPTORW* groupDsc; + result = OleGetClipboard(&dataObj); + + if (FAILED(result)) + return ERROR_INTERNAL_ERROR; + + ZeroMemory(&format_etc, sizeof(FORMATETC)); + ZeroMemory(&stg_medium, sizeof(STGMEDIUM)); + /* get DROPFILES struct from OLE */ + format_etc.cfFormat = CF_HDROP; + format_etc.tymed = TYMED_HGLOBAL; + format_etc.dwAspect = 1; + format_etc.lindex = -1; + result = IDataObject_GetData(dataObj, &format_etc, &stg_medium); + + if (FAILED(result)) + { + DEBUG_CLIPRDR("dataObj->GetData failed."); + goto exit; + } + + dropFiles = (DROPFILES*)GlobalLock(stg_medium.hGlobal); + + if (!dropFiles) + { + GlobalUnlock(stg_medium.hGlobal); + ReleaseStgMedium(&stg_medium); + clipboard->nFiles = 0; + goto exit; + } + + clear_file_array(clipboard); + + if (dropFiles->fWide) + { + /* dropFiles contains file names */ + for (wFileName = (WCHAR*)((char*)dropFiles + dropFiles->pFiles); + (len = wcslen(wFileName)) > 0; wFileName += len + 1) + { + wf_cliprdr_process_filename(clipboard, wFileName, wcslen(wFileName)); + } + } + else + { + char* p; + + for (p = (char*)((char*)dropFiles + dropFiles->pFiles); (len = strlen(p)) > 0; + p += len + 1, clipboard->nFiles++) + { + int cchWideChar; + WCHAR* wFileName; + cchWideChar = MultiByteToWideChar(CP_ACP, MB_COMPOSITE, p, len, NULL, 0); + wFileName = (LPWSTR)calloc(cchWideChar, sizeof(WCHAR)); + MultiByteToWideChar(CP_ACP, MB_COMPOSITE, p, len, wFileName, cchWideChar); + wf_cliprdr_process_filename(clipboard, wFileName, cchWideChar); + } + } + + GlobalUnlock(stg_medium.hGlobal); + ReleaseStgMedium(&stg_medium); + exit: + size = 4 + clipboard->nFiles * sizeof(FILEDESCRIPTORW); + groupDsc = (FILEGROUPDESCRIPTORW*)malloc(size); + + if (groupDsc) + { + groupDsc->cItems = clipboard->nFiles; + + for (i = 0; i < clipboard->nFiles; i++) + { + if (clipboard->fileDescriptor[i]) + groupDsc->fgd[i] = *clipboard->fileDescriptor[i]; + } + + buff = groupDsc; + } + + IDataObject_Release(dataObj); + } + else + { + /* Ignore if other app is holding the clipboard */ + if (try_open_clipboard(clipboard->hwnd)) + { + hClipdata = GetClipboardData(requestedFormatId); + + if (!hClipdata) + { + CloseClipboard(); + return ERROR_INTERNAL_ERROR; + } + + globlemem = (char*)GlobalLock(hClipdata); + size = (int)GlobalSize(hClipdata); + buff = malloc(size); + CopyMemory(buff, globlemem, size); + GlobalUnlock(hClipdata); + CloseClipboard(); + } + } + + response.msgFlags = CB_RESPONSE_OK; + response.dataLen = size; + response.requestedFormatData = (BYTE*)buff; + rc = clipboard->context->ClientFormatDataResponse(clipboard->context, &response); + free(buff); + return rc; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT +wf_cliprdr_server_format_data_response(CliprdrClientContext* context, + const CLIPRDR_FORMAT_DATA_RESPONSE* formatDataResponse) +{ + BYTE* data; + HANDLE hMem; + wfClipboard* clipboard; + + if (!context || !formatDataResponse) + return ERROR_INTERNAL_ERROR; + + if (formatDataResponse->msgFlags != CB_RESPONSE_OK) + return E_FAIL; + + clipboard = (wfClipboard*)context->custom; + + if (!clipboard) + return ERROR_INTERNAL_ERROR; + + hMem = GlobalAlloc(GMEM_MOVEABLE, formatDataResponse->dataLen); + + if (!hMem) + return ERROR_INTERNAL_ERROR; + + data = (BYTE*)GlobalLock(hMem); + + if (!data) + { + GlobalFree(hMem); + return ERROR_INTERNAL_ERROR; + } + + CopyMemory(data, formatDataResponse->requestedFormatData, formatDataResponse->dataLen); + + if (!GlobalUnlock(hMem) && GetLastError()) + { + GlobalFree(hMem); + return ERROR_INTERNAL_ERROR; + } + + clipboard->hmem = hMem; + + if (!SetEvent(clipboard->response_data_event)) + return ERROR_INTERNAL_ERROR; + + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT +wf_cliprdr_server_file_contents_request(CliprdrClientContext* context, + const CLIPRDR_FILE_CONTENTS_REQUEST* fileContentsRequest) +{ + DWORD uSize = 0; + BYTE* pData = NULL; + HRESULT hRet = S_OK; + FORMATETC vFormatEtc; + LPDATAOBJECT pDataObj = NULL; + STGMEDIUM vStgMedium; + BOOL bIsStreamFile = TRUE; + static LPSTREAM pStreamStc = NULL; + static UINT32 uStreamIdStc = 0; + wfClipboard* clipboard; + UINT rc = ERROR_INTERNAL_ERROR; + UINT sRc; + UINT32 cbRequested; + + if (!context || !fileContentsRequest) + return ERROR_INTERNAL_ERROR; + + clipboard = (wfClipboard*)context->custom; + + if (!clipboard) + return ERROR_INTERNAL_ERROR; + + cbRequested = fileContentsRequest->cbRequested; + if (fileContentsRequest->dwFlags == FILECONTENTS_SIZE) + cbRequested = sizeof(UINT64); + + pData = (BYTE*)calloc(1, cbRequested); + + if (!pData) + goto error; + + hRet = OleGetClipboard(&pDataObj); + + if (FAILED(hRet)) + { + WLog_ERR(TAG, "filecontents: get ole clipboard failed."); + goto error; + } + + ZeroMemory(&vFormatEtc, sizeof(FORMATETC)); + ZeroMemory(&vStgMedium, sizeof(STGMEDIUM)); + vFormatEtc.cfFormat = RegisterClipboardFormat(CFSTR_FILECONTENTS); + vFormatEtc.tymed = TYMED_ISTREAM; + vFormatEtc.dwAspect = 1; + vFormatEtc.lindex = fileContentsRequest->listIndex; + vFormatEtc.ptd = NULL; + + if ((uStreamIdStc != fileContentsRequest->streamId) || !pStreamStc) + { + LPENUMFORMATETC pEnumFormatEtc; + ULONG CeltFetched; + FORMATETC vFormatEtc2; + + if (pStreamStc) + { + IStream_Release(pStreamStc); + pStreamStc = NULL; + } + + bIsStreamFile = FALSE; + hRet = IDataObject_EnumFormatEtc(pDataObj, DATADIR_GET, &pEnumFormatEtc); + + if (hRet == S_OK) + { + do + { + hRet = IEnumFORMATETC_Next(pEnumFormatEtc, 1, &vFormatEtc2, &CeltFetched); + + if (hRet == S_OK) + { + if (vFormatEtc2.cfFormat == RegisterClipboardFormat(CFSTR_FILECONTENTS)) + { + hRet = IDataObject_GetData(pDataObj, &vFormatEtc, &vStgMedium); + + if (hRet == S_OK) + { + pStreamStc = vStgMedium.pstm; + uStreamIdStc = fileContentsRequest->streamId; + bIsStreamFile = TRUE; + } + + break; + } + } + } while (hRet == S_OK); + } + } + + if (bIsStreamFile == TRUE) + { + if (fileContentsRequest->dwFlags == FILECONTENTS_SIZE) + { + STATSTG vStatStg; + ZeroMemory(&vStatStg, sizeof(STATSTG)); + hRet = IStream_Stat(pStreamStc, &vStatStg, STATFLAG_NONAME); + + if (hRet == S_OK) + { + *((UINT32*)&pData[0]) = vStatStg.cbSize.LowPart; + *((UINT32*)&pData[4]) = vStatStg.cbSize.HighPart; + uSize = cbRequested; + } + } + else if (fileContentsRequest->dwFlags == FILECONTENTS_RANGE) + { + LARGE_INTEGER dlibMove; + ULARGE_INTEGER dlibNewPosition; + dlibMove.HighPart = fileContentsRequest->nPositionHigh; + dlibMove.LowPart = fileContentsRequest->nPositionLow; + hRet = IStream_Seek(pStreamStc, dlibMove, STREAM_SEEK_SET, &dlibNewPosition); + + if (SUCCEEDED(hRet)) + hRet = IStream_Read(pStreamStc, pData, cbRequested, (PULONG)&uSize); + } + } + else + { + if (fileContentsRequest->dwFlags == FILECONTENTS_SIZE) + { + if (clipboard->nFiles <= fileContentsRequest->listIndex) + goto error; + *((UINT32*)&pData[0]) = + clipboard->fileDescriptor[fileContentsRequest->listIndex]->nFileSizeLow; + *((UINT32*)&pData[4]) = + clipboard->fileDescriptor[fileContentsRequest->listIndex]->nFileSizeHigh; + uSize = cbRequested; + } + else if (fileContentsRequest->dwFlags == FILECONTENTS_RANGE) + { + BOOL bRet; + if (clipboard->nFiles <= fileContentsRequest->listIndex) + goto error; + bRet = wf_cliprdr_get_file_contents( + clipboard->file_names[fileContentsRequest->listIndex], pData, + fileContentsRequest->nPositionLow, fileContentsRequest->nPositionHigh, cbRequested, + &uSize); + + if (bRet == FALSE) + { + WLog_ERR(TAG, "get file contents failed."); + uSize = 0; + goto error; + } + } + } + + rc = CHANNEL_RC_OK; +error: + + if (pDataObj) + IDataObject_Release(pDataObj); + + if (uSize == 0) + { + free(pData); + pData = NULL; + } + + sRc = + cliprdr_send_response_filecontents(clipboard, fileContentsRequest->streamId, uSize, pData); + free(pData); + + if (sRc != CHANNEL_RC_OK) + return sRc; + + return rc; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT +wf_cliprdr_server_file_contents_response(CliprdrClientContext* context, + const CLIPRDR_FILE_CONTENTS_RESPONSE* fileContentsResponse) +{ + wfClipboard* clipboard; + + if (!context || !fileContentsResponse) + return ERROR_INTERNAL_ERROR; + + if (fileContentsResponse->msgFlags != CB_RESPONSE_OK) + return E_FAIL; + + clipboard = (wfClipboard*)context->custom; + + if (!clipboard) + return ERROR_INTERNAL_ERROR; + + clipboard->req_fsize = fileContentsResponse->cbRequested; + clipboard->req_fdata = (char*)malloc(fileContentsResponse->cbRequested); + + if (!clipboard->req_fdata) + return ERROR_INTERNAL_ERROR; + + CopyMemory(clipboard->req_fdata, fileContentsResponse->requestedData, + fileContentsResponse->cbRequested); + + if (!SetEvent(clipboard->req_fevent)) + { + free(clipboard->req_fdata); + return ERROR_INTERNAL_ERROR; + } + + return CHANNEL_RC_OK; +} + +BOOL wf_cliprdr_init(wfContext* wfc, CliprdrClientContext* cliprdr) +{ + wfClipboard* clipboard; + rdpContext* context = (rdpContext*)wfc; + + if (!context || !cliprdr) + return FALSE; + + wfc->clipboard = (wfClipboard*)calloc(1, sizeof(wfClipboard)); + + if (!wfc->clipboard) + return FALSE; + + clipboard = wfc->clipboard; + clipboard->wfc = wfc; + clipboard->context = cliprdr; + clipboard->channels = context->channels; + clipboard->sync = FALSE; + clipboard->map_capacity = 32; + clipboard->map_size = 0; + clipboard->hUser32 = LoadLibraryA("user32.dll"); + + if (clipboard->hUser32) + { + clipboard->AddClipboardFormatListener = (fnAddClipboardFormatListener)GetProcAddress( + clipboard->hUser32, "AddClipboardFormatListener"); + clipboard->RemoveClipboardFormatListener = (fnRemoveClipboardFormatListener)GetProcAddress( + clipboard->hUser32, "RemoveClipboardFormatListener"); + clipboard->GetUpdatedClipboardFormats = (fnGetUpdatedClipboardFormats)GetProcAddress( + clipboard->hUser32, "GetUpdatedClipboardFormats"); + } + + if (!(clipboard->hUser32 && clipboard->AddClipboardFormatListener && + clipboard->RemoveClipboardFormatListener && clipboard->GetUpdatedClipboardFormats)) + clipboard->legacyApi = TRUE; + + if (!(clipboard->format_mappings = + (formatMapping*)calloc(clipboard->map_capacity, sizeof(formatMapping)))) + goto error; + + if (!(clipboard->response_data_event = CreateEvent(NULL, TRUE, FALSE, NULL))) + goto error; + + if (!(clipboard->req_fevent = CreateEvent(NULL, TRUE, FALSE, NULL))) + goto error; + + if (!(clipboard->thread = CreateThread(NULL, 0, cliprdr_thread_func, clipboard, 0, NULL))) + goto error; + + cliprdr->MonitorReady = wf_cliprdr_monitor_ready; + cliprdr->ServerCapabilities = wf_cliprdr_server_capabilities; + cliprdr->ServerFormatList = wf_cliprdr_server_format_list; + cliprdr->ServerFormatListResponse = wf_cliprdr_server_format_list_response; + cliprdr->ServerLockClipboardData = wf_cliprdr_server_lock_clipboard_data; + cliprdr->ServerUnlockClipboardData = wf_cliprdr_server_unlock_clipboard_data; + cliprdr->ServerFormatDataRequest = wf_cliprdr_server_format_data_request; + cliprdr->ServerFormatDataResponse = wf_cliprdr_server_format_data_response; + cliprdr->ServerFileContentsRequest = wf_cliprdr_server_file_contents_request; + cliprdr->ServerFileContentsResponse = wf_cliprdr_server_file_contents_response; + cliprdr->custom = (void*)wfc->clipboard; + return TRUE; +error: + wf_cliprdr_uninit(wfc, cliprdr); + return FALSE; +} + +BOOL wf_cliprdr_uninit(wfContext* wfc, CliprdrClientContext* cliprdr) +{ + wfClipboard* clipboard; + + if (!wfc || !cliprdr) + return FALSE; + + clipboard = wfc->clipboard; + + if (!clipboard) + return FALSE; + + cliprdr->custom = NULL; + + if (clipboard->hwnd) + PostMessage(clipboard->hwnd, WM_QUIT, 0, 0); + + if (clipboard->thread) + { + WaitForSingleObject(clipboard->thread, INFINITE); + CloseHandle(clipboard->thread); + } + + if (clipboard->response_data_event) + CloseHandle(clipboard->response_data_event); + + if (clipboard->req_fevent) + CloseHandle(clipboard->req_fevent); + + clear_file_array(clipboard); + clear_format_map(clipboard); + free(clipboard->format_mappings); + free(clipboard); + return TRUE; +} diff --git a/client/Windows/wf_cliprdr.h b/client/Windows/wf_cliprdr.h new file mode 100644 index 0000000..3a6b4a1 --- /dev/null +++ b/client/Windows/wf_cliprdr.h @@ -0,0 +1,27 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Windows Clipboard Redirection + * + * Copyright 2012 Jason Champion + * + * Licensed 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 FREERDP_CLIENT_WIN_CLIPRDR_H +#define FREERDP_CLIENT_WIN_CLIPRDR_H + +#include "wf_client.h" + +BOOL wf_cliprdr_init(wfContext* wfc, CliprdrClientContext* cliprdr); +BOOL wf_cliprdr_uninit(wfContext* wfc, CliprdrClientContext* cliprdr); + +#endif /* FREERDP_CLIENT_WIN_CLIPRDR_H */ diff --git a/client/Windows/wf_event.c b/client/Windows/wf_event.c new file mode 100644 index 0000000..d1ded4c --- /dev/null +++ b/client/Windows/wf_event.c @@ -0,0 +1,778 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Event Handling + * + * Copyright 2009-2011 Jay Sorg + * Copyright 2010-2011 Vic Lee + * Copyright 2010-2011 Marc-Andre Moreau + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include + +#include + +#include "wf_client.h" + +#include "wf_gdi.h" +#include "wf_event.h" + +#include + +static HWND g_focus_hWnd; + +#define X_POS(lParam) ((UINT16)(lParam & 0xFFFF)) +#define Y_POS(lParam) ((UINT16)((lParam >> 16) & 0xFFFF)) + +static BOOL wf_scale_blt(wfContext* wfc, HDC hdc, int x, int y, int w, int h, HDC hdcSrc, int x1, + int y1, DWORD rop); +static BOOL wf_scale_mouse_event(wfContext* wfc, rdpInput* input, UINT16 flags, UINT16 x, UINT16 y); +#if (_WIN32_WINNT >= 0x0500) +static BOOL wf_scale_mouse_event_ex(wfContext* wfc, rdpInput* input, UINT16 flags, + UINT16 buttonMask, UINT16 x, UINT16 y); +#endif + +static BOOL g_flipping_in; +static BOOL g_flipping_out; + +static BOOL alt_ctrl_down() +{ + return ((GetAsyncKeyState(VK_CONTROL) & 0x8000) || (GetAsyncKeyState(VK_MENU) & 0x8000)); +} + +LRESULT CALLBACK wf_ll_kbd_proc(int nCode, WPARAM wParam, LPARAM lParam) +{ + wfContext* wfc; + DWORD rdp_scancode; + rdpInput* input; + PKBDLLHOOKSTRUCT p; + DEBUG_KBD("Low-level keyboard hook, hWnd %X nCode %X wParam %X", g_focus_hWnd, nCode, wParam); + + if (g_flipping_in) + { + if (!alt_ctrl_down()) + g_flipping_in = FALSE; + + return CallNextHookEx(NULL, nCode, wParam, lParam); + } + + if (g_focus_hWnd && (nCode == HC_ACTION)) + { + switch (wParam) + { + case WM_KEYDOWN: + case WM_SYSKEYDOWN: + case WM_KEYUP: + case WM_SYSKEYUP: + wfc = (wfContext*)GetWindowLongPtr(g_focus_hWnd, GWLP_USERDATA); + p = (PKBDLLHOOKSTRUCT)lParam; + + if (!wfc || !p) + return 1; + + input = wfc->context.input; + rdp_scancode = MAKE_RDP_SCANCODE((BYTE)p->scanCode, p->flags & LLKHF_EXTENDED); + DEBUG_KBD("keydown %d scanCode 0x%08lX flags 0x%08lX vkCode 0x%08lX", + (wParam == WM_KEYDOWN), p->scanCode, p->flags, p->vkCode); + + if (wfc->fullscreen_toggle && + ((p->vkCode == VK_RETURN) || (p->vkCode == VK_CANCEL)) && + (GetAsyncKeyState(VK_CONTROL) & 0x8000) && + (GetAsyncKeyState(VK_MENU) & 0x8000)) /* could also use flags & LLKHF_ALTDOWN */ + { + if (wParam == WM_KEYDOWN) + { + wf_toggle_fullscreen(wfc); + return 1; + } + } + + if (rdp_scancode == RDP_SCANCODE_NUMLOCK_EXTENDED) + { + /* Windows sends NumLock as extended - rdp doesn't */ + DEBUG_KBD("hack: NumLock (x45) should not be extended"); + rdp_scancode = RDP_SCANCODE_NUMLOCK; + } + else if (rdp_scancode == RDP_SCANCODE_NUMLOCK) + { + /* Windows sends Pause as if it was a RDP NumLock (handled above). + * It must however be sent as a one-shot Ctrl+NumLock */ + if (wParam == WM_KEYDOWN) + { + DEBUG_KBD("Pause, sent as Ctrl+NumLock"); + freerdp_input_send_keyboard_event_ex(input, TRUE, RDP_SCANCODE_LCONTROL); + freerdp_input_send_keyboard_event_ex(input, TRUE, RDP_SCANCODE_NUMLOCK); + freerdp_input_send_keyboard_event_ex(input, FALSE, RDP_SCANCODE_LCONTROL); + freerdp_input_send_keyboard_event_ex(input, FALSE, RDP_SCANCODE_NUMLOCK); + } + else + { + DEBUG_KBD("Pause up"); + } + + return 1; + } + else if (rdp_scancode == RDP_SCANCODE_RSHIFT_EXTENDED) + { + DEBUG_KBD("right shift (x36) should not be extended"); + rdp_scancode = RDP_SCANCODE_RSHIFT; + } + + freerdp_input_send_keyboard_event_ex(input, !(p->flags & LLKHF_UP), rdp_scancode); + + if (p->vkCode == VK_NUMLOCK || p->vkCode == VK_CAPITAL || p->vkCode == VK_SCROLL || + p->vkCode == VK_KANA) + DEBUG_KBD( + "lock keys are processed on client side too to toggle their indicators"); + else + return 1; + + break; + } + } + + if (g_flipping_out) + { + if (!alt_ctrl_down()) + { + g_flipping_out = FALSE; + g_focus_hWnd = NULL; + } + } + + return CallNextHookEx(NULL, nCode, wParam, lParam); +} + +void wf_event_focus_in(wfContext* wfc) +{ + UINT16 syncFlags; + rdpInput* input; + POINT pt; + RECT rc; + input = wfc->context.input; + syncFlags = 0; + + if (GetKeyState(VK_NUMLOCK)) + syncFlags |= KBD_SYNC_NUM_LOCK; + + if (GetKeyState(VK_CAPITAL)) + syncFlags |= KBD_SYNC_CAPS_LOCK; + + if (GetKeyState(VK_SCROLL)) + syncFlags |= KBD_SYNC_SCROLL_LOCK; + + if (GetKeyState(VK_KANA)) + syncFlags |= KBD_SYNC_KANA_LOCK; + + input->FocusInEvent(input, syncFlags); + /* send pointer position if the cursor is currently inside our client area */ + GetCursorPos(&pt); + ScreenToClient(wfc->hwnd, &pt); + GetClientRect(wfc->hwnd, &rc); + + if (pt.x >= rc.left && pt.x < rc.right && pt.y >= rc.top && pt.y < rc.bottom) + input->MouseEvent(input, PTR_FLAGS_MOVE, (UINT16)pt.x, (UINT16)pt.y); +} + +static BOOL wf_event_process_WM_MOUSEWHEEL(wfContext* wfc, HWND hWnd, UINT Msg, WPARAM wParam, + LPARAM lParam, BOOL horizontal, UINT16 x, UINT16 y) +{ + int delta; + UINT16 flags = 0; + rdpInput* input; + DefWindowProc(hWnd, Msg, wParam, lParam); + input = wfc->context.input; + delta = ((signed short)HIWORD(wParam)); /* GET_WHEEL_DELTA_WPARAM(wParam); */ + + if (horizontal) + flags |= PTR_FLAGS_HWHEEL; + else + flags |= PTR_FLAGS_WHEEL; + + if (delta < 0) + { + flags |= PTR_FLAGS_WHEEL_NEGATIVE; + /* 9bit twos complement, delta already negative */ + delta = 0x100 + delta; + } + + flags |= delta; + return wf_scale_mouse_event(wfc, input, flags, x, y); +} + +static void wf_sizing(wfContext* wfc, WPARAM wParam, LPARAM lParam) +{ + rdpSettings* settings = wfc->context.settings; + // Holding the CTRL key down while resizing the window will force the desktop aspect ratio. + LPRECT rect; + + if (settings->SmartSizing && (GetAsyncKeyState(VK_CONTROL) & 0x8000)) + { + rect = (LPRECT)wParam; + + switch (lParam) + { + case WMSZ_LEFT: + case WMSZ_RIGHT: + case WMSZ_BOTTOMRIGHT: + // Adjust height + rect->bottom = rect->top + settings->DesktopHeight * (rect->right - rect->left) / + settings->DesktopWidth; + break; + + case WMSZ_TOP: + case WMSZ_BOTTOM: + case WMSZ_TOPRIGHT: + // Adjust width + rect->right = rect->left + settings->DesktopWidth * (rect->bottom - rect->top) / + settings->DesktopHeight; + break; + + case WMSZ_BOTTOMLEFT: + case WMSZ_TOPLEFT: + // adjust width + rect->left = rect->right - (settings->DesktopWidth * (rect->bottom - rect->top) / + settings->DesktopHeight); + break; + } + } +} + +LRESULT CALLBACK wf_event_proc(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam) +{ + HDC hdc; + LONG_PTR ptr; + wfContext* wfc; + int x, y, w, h; + PAINTSTRUCT ps; + BOOL processed; + RECT windowRect; + MINMAXINFO* minmax; + SCROLLINFO si; + processed = TRUE; + ptr = GetWindowLongPtr(hWnd, GWLP_USERDATA); + wfc = (wfContext*)ptr; + + if (wfc != NULL) + { + rdpInput* input = wfc->context.input; + rdpSettings* settings = wfc->context.settings; + + switch (Msg) + { + case WM_MOVE: + if (!wfc->disablewindowtracking) + { + int x = (int)(short)LOWORD(lParam); + int y = (int)(short)HIWORD(lParam); + wfc->client_x = x; + wfc->client_y = y; + } + + break; + + case WM_GETMINMAXINFO: + if (wfc->context.settings->SmartSizing) + { + processed = FALSE; + } + else + { + // Set maximum window size for resizing + minmax = (MINMAXINFO*)lParam; + + // always use the last determined canvas diff, because it could be + // that the window is minimized when this gets called + // wf_update_canvas_diff(wfc); + + if (!wfc->fullscreen) + { + // add window decoration + minmax->ptMaxTrackSize.x = settings->DesktopWidth + wfc->diff.x; + minmax->ptMaxTrackSize.y = settings->DesktopHeight + wfc->diff.y; + } + } + + break; + + case WM_SIZING: + wf_sizing(wfc, lParam, wParam); + break; + + case WM_SIZE: + GetWindowRect(wfc->hwnd, &windowRect); + + if (!wfc->fullscreen) + { + wfc->client_width = LOWORD(lParam); + wfc->client_height = HIWORD(lParam); + wfc->client_x = windowRect.left; + wfc->client_y = windowRect.top; + } + + if (wfc->client_width && wfc->client_height) + { + wf_size_scrollbars(wfc, LOWORD(lParam), HIWORD(lParam)); + + // Workaround: when the window is maximized, the call to "ShowScrollBars" + // returns TRUE but has no effect. + if (wParam == SIZE_MAXIMIZED && !wfc->fullscreen) + SetWindowPos(wfc->hwnd, HWND_TOP, 0, 0, windowRect.right - windowRect.left, + windowRect.bottom - windowRect.top, + SWP_NOMOVE | SWP_FRAMECHANGED); + } + + break; + + case WM_EXITSIZEMOVE: + wf_size_scrollbars(wfc, wfc->client_width, wfc->client_height); + break; + + case WM_ERASEBKGND: + /* Say we handled it - prevents flickering */ + return (LRESULT)1; + + case WM_PAINT: + hdc = BeginPaint(hWnd, &ps); + x = ps.rcPaint.left; + y = ps.rcPaint.top; + w = ps.rcPaint.right - ps.rcPaint.left + 1; + h = ps.rcPaint.bottom - ps.rcPaint.top + 1; + wf_scale_blt(wfc, hdc, x, y, w, h, wfc->primary->hdc, + x - wfc->offset_x + wfc->xCurrentScroll, + y - wfc->offset_y + wfc->yCurrentScroll, SRCCOPY); + EndPaint(hWnd, &ps); + break; +#if (_WIN32_WINNT >= 0x0500) + + case WM_XBUTTONDOWN: + wf_scale_mouse_event_ex(wfc, input, PTR_XFLAGS_DOWN, GET_XBUTTON_WPARAM(wParam), + X_POS(lParam) - wfc->offset_x, + Y_POS(lParam) - wfc->offset_y); + break; + + case WM_XBUTTONUP: + wf_scale_mouse_event_ex(wfc, input, 0, GET_XBUTTON_WPARAM(wParam), + X_POS(lParam) - wfc->offset_x, + Y_POS(lParam) - wfc->offset_y); + break; +#endif + + case WM_MBUTTONDOWN: + wf_scale_mouse_event(wfc, input, PTR_FLAGS_DOWN | PTR_FLAGS_BUTTON3, + X_POS(lParam) - wfc->offset_x, Y_POS(lParam) - wfc->offset_y); + break; + + case WM_MBUTTONUP: + wf_scale_mouse_event(wfc, input, PTR_FLAGS_BUTTON3, X_POS(lParam) - wfc->offset_x, + Y_POS(lParam) - wfc->offset_y); + break; + + case WM_LBUTTONDOWN: + wf_scale_mouse_event(wfc, input, PTR_FLAGS_DOWN | PTR_FLAGS_BUTTON1, + X_POS(lParam) - wfc->offset_x, Y_POS(lParam) - wfc->offset_y); + break; + + case WM_LBUTTONUP: + wf_scale_mouse_event(wfc, input, PTR_FLAGS_BUTTON1, X_POS(lParam) - wfc->offset_x, + Y_POS(lParam) - wfc->offset_y); + break; + + case WM_RBUTTONDOWN: + wf_scale_mouse_event(wfc, input, PTR_FLAGS_DOWN | PTR_FLAGS_BUTTON2, + X_POS(lParam) - wfc->offset_x, Y_POS(lParam) - wfc->offset_y); + break; + + case WM_RBUTTONUP: + wf_scale_mouse_event(wfc, input, PTR_FLAGS_BUTTON2, X_POS(lParam) - wfc->offset_x, + Y_POS(lParam) - wfc->offset_y); + break; + + case WM_MOUSEMOVE: + wf_scale_mouse_event(wfc, input, PTR_FLAGS_MOVE, X_POS(lParam) - wfc->offset_x, + Y_POS(lParam) - wfc->offset_y); + break; +#if (_WIN32_WINNT >= 0x0400) || (_WIN32_WINDOWS > 0x0400) + + case WM_MOUSEWHEEL: + wf_event_process_WM_MOUSEWHEEL(wfc, hWnd, Msg, wParam, lParam, FALSE, + X_POS(lParam) - wfc->offset_x, + Y_POS(lParam) - wfc->offset_y); + break; +#endif +#if (_WIN32_WINNT >= 0x0600) + + case WM_MOUSEHWHEEL: + wf_event_process_WM_MOUSEWHEEL(wfc, hWnd, Msg, wParam, lParam, TRUE, + X_POS(lParam) - wfc->offset_x, + Y_POS(lParam) - wfc->offset_y); + break; +#endif + + case WM_SETCURSOR: + if (LOWORD(lParam) == HTCLIENT) + SetCursor(wfc->cursor); + else + DefWindowProc(hWnd, Msg, wParam, lParam); + + break; + + case WM_HSCROLL: + { + int xDelta; // xDelta = new_pos - current_pos + int xNewPos; // new position + int yDelta = 0; + + switch (LOWORD(wParam)) + { + // User clicked the scroll bar shaft left of the scroll box. + case SB_PAGEUP: + xNewPos = wfc->xCurrentScroll - 50; + break; + + // User clicked the scroll bar shaft right of the scroll box. + case SB_PAGEDOWN: + xNewPos = wfc->xCurrentScroll + 50; + break; + + // User clicked the left arrow. + case SB_LINEUP: + xNewPos = wfc->xCurrentScroll - 5; + break; + + // User clicked the right arrow. + case SB_LINEDOWN: + xNewPos = wfc->xCurrentScroll + 5; + break; + + // User dragged the scroll box. + case SB_THUMBPOSITION: + xNewPos = HIWORD(wParam); + break; + + // user is dragging the scrollbar + case SB_THUMBTRACK: + xNewPos = HIWORD(wParam); + break; + + default: + xNewPos = wfc->xCurrentScroll; + } + + // New position must be between 0 and the screen width. + xNewPos = MAX(0, xNewPos); + xNewPos = MIN(wfc->xMaxScroll, xNewPos); + + // If the current position does not change, do not scroll. + if (xNewPos == wfc->xCurrentScroll) + break; + + // Determine the amount scrolled (in pixels). + xDelta = xNewPos - wfc->xCurrentScroll; + // Reset the current scroll position. + wfc->xCurrentScroll = xNewPos; + // Scroll the window. (The system repaints most of the + // client area when ScrollWindowEx is called; however, it is + // necessary to call UpdateWindow in order to repaint the + // rectangle of pixels that were invalidated.) + ScrollWindowEx(wfc->hwnd, -xDelta, -yDelta, (CONST RECT*)NULL, (CONST RECT*)NULL, + (HRGN)NULL, (PRECT)NULL, SW_INVALIDATE); + UpdateWindow(wfc->hwnd); + // Reset the scroll bar. + si.cbSize = sizeof(si); + si.fMask = SIF_POS; + si.nPos = wfc->xCurrentScroll; + SetScrollInfo(wfc->hwnd, SB_HORZ, &si, TRUE); + } + break; + + case WM_VSCROLL: + { + int xDelta = 0; + int yDelta; // yDelta = new_pos - current_pos + int yNewPos; // new position + + switch (LOWORD(wParam)) + { + // User clicked the scroll bar shaft above the scroll box. + case SB_PAGEUP: + yNewPos = wfc->yCurrentScroll - 50; + break; + + // User clicked the scroll bar shaft below the scroll box. + case SB_PAGEDOWN: + yNewPos = wfc->yCurrentScroll + 50; + break; + + // User clicked the top arrow. + case SB_LINEUP: + yNewPos = wfc->yCurrentScroll - 5; + break; + + // User clicked the bottom arrow. + case SB_LINEDOWN: + yNewPos = wfc->yCurrentScroll + 5; + break; + + // User dragged the scroll box. + case SB_THUMBPOSITION: + yNewPos = HIWORD(wParam); + break; + + // user is dragging the scrollbar + case SB_THUMBTRACK: + yNewPos = HIWORD(wParam); + break; + + default: + yNewPos = wfc->yCurrentScroll; + } + + // New position must be between 0 and the screen height. + yNewPos = MAX(0, yNewPos); + yNewPos = MIN(wfc->yMaxScroll, yNewPos); + + // If the current position does not change, do not scroll. + if (yNewPos == wfc->yCurrentScroll) + break; + + // Determine the amount scrolled (in pixels). + yDelta = yNewPos - wfc->yCurrentScroll; + // Reset the current scroll position. + wfc->yCurrentScroll = yNewPos; + // Scroll the window. (The system repaints most of the + // client area when ScrollWindowEx is called; however, it is + // necessary to call UpdateWindow in order to repaint the + // rectangle of pixels that were invalidated.) + ScrollWindowEx(wfc->hwnd, -xDelta, -yDelta, (CONST RECT*)NULL, (CONST RECT*)NULL, + (HRGN)NULL, (PRECT)NULL, SW_INVALIDATE); + UpdateWindow(wfc->hwnd); + // Reset the scroll bar. + si.cbSize = sizeof(si); + si.fMask = SIF_POS; + si.nPos = wfc->yCurrentScroll; + SetScrollInfo(wfc->hwnd, SB_VERT, &si, TRUE); + } + break; + + case WM_SYSCOMMAND: + { + if (wParam == SYSCOMMAND_ID_SMARTSIZING) + { + HMENU hMenu = GetSystemMenu(wfc->hwnd, FALSE); + freerdp_set_param_bool(wfc->context.settings, FreeRDP_SmartSizing, + !wfc->context.settings->SmartSizing); + CheckMenuItem(hMenu, SYSCOMMAND_ID_SMARTSIZING, + wfc->context.settings->SmartSizing ? MF_CHECKED : MF_UNCHECKED); + } + else + { + processed = FALSE; + } + } + break; + + default: + processed = FALSE; + break; + } + } + else + { + processed = FALSE; + } + + if (processed) + return 0; + + switch (Msg) + { + case WM_DESTROY: + PostQuitMessage(WM_QUIT); + break; + + case WM_SETFOCUS: + DEBUG_KBD("getting focus %X", hWnd); + + if (alt_ctrl_down()) + g_flipping_in = TRUE; + + g_focus_hWnd = hWnd; + freerdp_set_focus(wfc->context.instance); + break; + + case WM_KILLFOCUS: + if (g_focus_hWnd == hWnd && wfc && !wfc->fullscreen) + { + DEBUG_KBD("loosing focus %X", hWnd); + + if (alt_ctrl_down()) + g_flipping_out = TRUE; + else + g_focus_hWnd = NULL; + } + + break; + + case WM_ACTIVATE: + { + int activate = (int)(short)LOWORD(wParam); + + if (activate != WA_INACTIVE) + { + if (alt_ctrl_down()) + g_flipping_in = TRUE; + + g_focus_hWnd = hWnd; + } + else + { + if (alt_ctrl_down()) + g_flipping_out = TRUE; + else + g_focus_hWnd = NULL; + } + } + + default: + return DefWindowProc(hWnd, Msg, wParam, lParam); + break; + } + + return 0; +} + +BOOL wf_scale_blt(wfContext* wfc, HDC hdc, int x, int y, int w, int h, HDC hdcSrc, int x1, int y1, + DWORD rop) +{ + rdpSettings* settings; + UINT32 ww, wh, dw, dh; + settings = wfc->context.settings; + + if (!wfc->client_width) + wfc->client_width = settings->DesktopWidth; + + if (!wfc->client_height) + wfc->client_height = settings->DesktopHeight; + + ww = wfc->client_width; + wh = wfc->client_height; + dw = settings->DesktopWidth; + dh = settings->DesktopHeight; + + if (!ww) + ww = dw; + + if (!wh) + wh = dh; + + if (wfc->fullscreen || !wfc->context.settings->SmartSizing || (ww == dw && wh == dh)) + { + return BitBlt(hdc, x, y, w, h, wfc->primary->hdc, x1, y1, SRCCOPY); + } + else + { + SetStretchBltMode(hdc, HALFTONE); + SetBrushOrgEx(hdc, 0, 0, NULL); + return StretchBlt(hdc, 0, 0, ww, wh, wfc->primary->hdc, 0, 0, dw, dh, SRCCOPY); + } + + return TRUE; +} + +static BOOL wf_scale_mouse_pos(wfContext* wfc, UINT16* x, UINT16* y) +{ + int ww, wh, dw, dh; + rdpContext* context; + rdpSettings* settings; + + if (!wfc || !x || !y) + return FALSE; + + settings = wfc->context.settings; + + if (!settings) + return FALSE; + + if (!wfc->client_width) + wfc->client_width = settings->DesktopWidth; + + if (!wfc->client_height) + wfc->client_height = settings->DesktopHeight; + + ww = wfc->client_width; + wh = wfc->client_height; + dw = settings->DesktopWidth; + dh = settings->DesktopHeight; + + if (!settings->SmartSizing || ((ww == dw) && (wh == dh))) + { + *x += wfc->xCurrentScroll; + *y += wfc->yCurrentScroll; + } + else + { + *x = *x * dw / ww + wfc->xCurrentScroll; + *y = *y * dh / wh + wfc->yCurrentScroll; + } + + return TRUE; +} + +static BOOL wf_scale_mouse_event(wfContext* wfc, rdpInput* input, UINT16 flags, UINT16 x, UINT16 y) +{ + MouseEventEventArgs eventArgs; + + if (!wf_scale_mouse_pos(wfc, &x, &y)) + return FALSE; + + if (freerdp_input_send_mouse_event(input, flags, x, y)) + return FALSE; + + eventArgs.flags = flags; + eventArgs.x = x; + eventArgs.y = y; + PubSub_OnMouseEvent(wfc->context.pubSub, &wfc->context, &eventArgs); + return TRUE; +} + +#if (_WIN32_WINNT >= 0x0500) +static BOOL wf_scale_mouse_event_ex(wfContext* wfc, rdpInput* input, UINT16 flags, + UINT16 buttonMask, UINT16 x, UINT16 y) +{ + MouseEventExEventArgs eventArgs; + + if (buttonMask & XBUTTON1) + flags |= PTR_XFLAGS_BUTTON1; + + if (buttonMask & XBUTTON2) + flags |= PTR_XFLAGS_BUTTON2; + + if (!wf_scale_mouse_pos(wfc, &x, &y)) + return FALSE; + + if (freerdp_input_send_extended_mouse_event(input, flags, x, y)) + return FALSE; + + eventArgs.flags = flags; + eventArgs.x = x; + eventArgs.y = y; + PubSub_OnMouseEventEx(wfc->context.pubSub, &wfc->context, &eventArgs); + return TRUE; +} +#endif diff --git a/client/Windows/wf_event.h b/client/Windows/wf_event.h new file mode 100644 index 0000000..f879f87 --- /dev/null +++ b/client/Windows/wf_event.h @@ -0,0 +1,43 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Event Handling + * + * Copyright 2009-2011 Jay Sorg + * Copyright 2010-2011 Vic Lee + * Copyright 2010-2011 Marc-Andre Moreau + * + * Licensed 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 FREERDP_CLIENT_WIN_EVENT_H +#define FREERDP_CLIENT_WIN_EVENT_H + +#include "wf_client.h" +#include + +LRESULT CALLBACK wf_ll_kbd_proc(int nCode, WPARAM wParam, LPARAM lParam); +LRESULT CALLBACK wf_event_proc(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam); + +void wf_event_focus_in(wfContext* wfc); + +#define KBD_TAG CLIENT_TAG("windows") +#ifdef WITH_DEBUG_KBD +#define DEBUG_KBD(...) WLog_DBG(KBD_TAG, __VA_ARGS__) +#else +#define DEBUG_KBD(...) \ + do \ + { \ + } while (0) +#endif + +#endif /* FREERDP_CLIENT_WIN_EVENT_H */ diff --git a/client/Windows/wf_floatbar.c b/client/Windows/wf_floatbar.c new file mode 100644 index 0000000..512c66b --- /dev/null +++ b/client/Windows/wf_floatbar.c @@ -0,0 +1,745 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Windows Float Bar + * + * Copyright 2013 Zhang Zhaolong + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +#include "resource.h" + +#include "wf_client.h" +#include "wf_floatbar.h" +#include "wf_gdi.h" +#pragma comment(lib, "Msimg32.lib") + +#define TAG CLIENT_TAG("windows.floatbar") + +typedef struct _Button Button; + +/* TIMERs */ +#define TIMER_HIDE 1 +#define TIMER_ANIMAT_SHOW 2 +#define TIMER_ANIMAT_HIDE 3 + +/* Button Type */ +#define BUTTON_LOCKPIN 0 +#define BUTTON_MINIMIZE 1 +#define BUTTON_RESTORE 2 +#define BUTTON_CLOSE 3 +#define BTN_MAX 4 + +/* bmp size */ +#define BACKGROUND_W 576 +#define BACKGROUND_H 27 +#define BUTTON_OFFSET 5 +#define BUTTON_Y 2 +#define BUTTON_WIDTH 23 +#define BUTTON_HEIGHT 21 +#define BUTTON_SPACING 1 + +#define LOCK_X (BACKGROUND_H + BUTTON_OFFSET) +#define CLOSE_X ((BACKGROUND_W - (BACKGROUND_H + BUTTON_OFFSET)) - BUTTON_WIDTH) +#define RESTORE_X (CLOSE_X - (BUTTON_WIDTH + BUTTON_SPACING)) +#define MINIMIZE_X (RESTORE_X - (BUTTON_WIDTH + BUTTON_SPACING)) +#define TEXT_X (BACKGROUND_H + ((BUTTON_WIDTH + BUTTON_SPACING) * 3) + 5) + +struct _Button +{ + wfFloatBar* floatbar; + int type; + int x, y, h, w; + int active; + HBITMAP bmp; + HBITMAP bmp_act; + + /* Lock Specified */ + HBITMAP locked_bmp; + HBITMAP locked_bmp_act; + HBITMAP unlocked_bmp; + HBITMAP unlocked_bmp_act; +}; + +struct _FloatBar +{ + HINSTANCE root_window; + DWORD flags; + HWND parent; + HWND hwnd; + RECT rect; + LONG width; + LONG height; + LONG offset; + wfContext* wfc; + Button* buttons[BTN_MAX]; + BOOL shown; + BOOL locked; + HDC hdcmem; + RECT textRect; + UINT_PTR animating; +}; + +static BOOL floatbar_kill_timers(wfFloatBar* floatbar) +{ + size_t x; + UINT_PTR timers[] = { TIMER_HIDE, TIMER_ANIMAT_HIDE, TIMER_ANIMAT_SHOW }; + + if (!floatbar) + return FALSE; + + for (x = 0; x < ARRAYSIZE(timers); x++) + KillTimer(floatbar->hwnd, timers[x]); + + floatbar->animating = 0; + return TRUE; +} + +static BOOL floatbar_animation(wfFloatBar* const floatbar, const BOOL show) +{ + UINT_PTR timer = show ? TIMER_ANIMAT_SHOW : TIMER_ANIMAT_HIDE; + + if (!floatbar) + return FALSE; + + if (floatbar->shown == show) + return TRUE; + + if (floatbar->animating == timer) + return TRUE; + + floatbar->animating = timer; + + if (SetTimer(floatbar->hwnd, timer, USER_TIMER_MINIMUM, NULL) == NULL) + { + DWORD err = GetLastError(); + WLog_ERR(TAG, "SetTimer failed with %08" PRIx32, err); + return FALSE; + } + + return TRUE; +} + +static BOOL floatbar_trigger_hide(wfFloatBar* floatbar) +{ + if (!floatbar_kill_timers(floatbar)) + return FALSE; + + if (!floatbar->locked && floatbar->shown) + { + if (SetTimer(floatbar->hwnd, TIMER_HIDE, 3000, NULL) == NULL) + { + DWORD err = GetLastError(); + WLog_ERR(TAG, "SetTimer failed with %08" PRIx32, err); + return FALSE; + } + } + + return TRUE; +} + +static BOOL floatbar_hide(wfFloatBar* floatbar) +{ + if (!floatbar_kill_timers(floatbar)) + return FALSE; + + floatbar->offset = floatbar->height - 2; + + if (!MoveWindow(floatbar->hwnd, floatbar->rect.left, -floatbar->offset, floatbar->width, + floatbar->height, TRUE)) + { + DWORD err = GetLastError(); + WLog_ERR(TAG, "MoveWindow failed with %08" PRIx32, err); + return FALSE; + } + + floatbar->shown = FALSE; + + if (!floatbar_trigger_hide(floatbar)) + return FALSE; + + return TRUE; +} + +static BOOL floatbar_show(wfFloatBar* floatbar) +{ + if (!floatbar_kill_timers(floatbar)) + return FALSE; + + floatbar->offset = 0; + + if (!MoveWindow(floatbar->hwnd, floatbar->rect.left, -floatbar->offset, floatbar->width, + floatbar->height, TRUE)) + { + DWORD err = GetLastError(); + WLog_ERR(TAG, "MoveWindow failed with %08" PRIx32, err); + return FALSE; + } + + floatbar->shown = TRUE; + + if (!floatbar_trigger_hide(floatbar)) + return FALSE; + + return TRUE; +} + +static BOOL button_set_locked(Button* button, BOOL locked) +{ + if (locked) + { + button->bmp = button->locked_bmp; + button->bmp_act = button->locked_bmp_act; + } + else + { + button->bmp = button->unlocked_bmp; + button->bmp_act = button->unlocked_bmp_act; + } + + InvalidateRect(button->floatbar->hwnd, NULL, FALSE); + UpdateWindow(button->floatbar->hwnd); + return TRUE; +} + +static BOOL update_locked_state(wfFloatBar* floatbar) +{ + Button* button; + + if (!floatbar) + return FALSE; + + button = floatbar->buttons[3]; + + if (!button_set_locked(button, floatbar->locked)) + return FALSE; + + return TRUE; +} + +static int button_hit(Button* const button) +{ + wfFloatBar* const floatbar = button->floatbar; + + switch (button->type) + { + case BUTTON_LOCKPIN: + floatbar->locked = !floatbar->locked; + update_locked_state(floatbar); + break; + + case BUTTON_MINIMIZE: + ShowWindow(floatbar->parent, SW_MINIMIZE); + break; + + case BUTTON_RESTORE: + wf_toggle_fullscreen(floatbar->wfc); + break; + + case BUTTON_CLOSE: + SendMessage(floatbar->parent, WM_DESTROY, 0, 0); + break; + + default: + return 0; + } + + return 0; +} + +static int button_paint(const Button* const button, const HDC hdc) +{ + if (button != NULL) + { + wfFloatBar* floatbar = button->floatbar; + BLENDFUNCTION bf; + SelectObject(floatbar->hdcmem, button->active ? button->bmp_act : button->bmp); + bf.BlendOp = AC_SRC_OVER; + bf.BlendFlags = 0; + bf.SourceConstantAlpha = 255; + bf.AlphaFormat = AC_SRC_ALPHA; + AlphaBlend(hdc, button->x, button->y, button->w, button->h, floatbar->hdcmem, 0, 0, + button->w, button->h, bf); + } + + return 0; +} + +static Button* floatbar_create_button(wfFloatBar* const floatbar, const int type, const int resid, + const int resid_act, const int x, const int y, const int h, + const int w) +{ + Button* button = (Button*)calloc(1, sizeof(Button)); + + if (!button) + return NULL; + + button->floatbar = floatbar; + button->type = type; + button->x = x; + button->y = y; + button->w = w; + button->h = h; + button->active = FALSE; + button->bmp = (HBITMAP)LoadImage(floatbar->root_window, MAKEINTRESOURCE(resid), IMAGE_BITMAP, 0, + 0, LR_DEFAULTCOLOR); + button->bmp_act = (HBITMAP)LoadImage(floatbar->root_window, MAKEINTRESOURCE(resid_act), + IMAGE_BITMAP, 0, 0, LR_DEFAULTCOLOR); + return button; +} + +static Button* floatbar_create_lock_button(wfFloatBar* const floatbar, const int unlock_resid, + const int unlock_resid_act, const int lock_resid, + const int lock_resid_act, const int x, const int y, + const int h, const int w) +{ + Button* button = floatbar_create_button(floatbar, BUTTON_LOCKPIN, unlock_resid, + unlock_resid_act, x, y, h, w); + + if (!button) + return NULL; + + button->unlocked_bmp = button->bmp; + button->unlocked_bmp_act = button->bmp_act; + button->locked_bmp = (HBITMAP)LoadImage(floatbar->wfc->hInstance, MAKEINTRESOURCE(lock_resid), + IMAGE_BITMAP, 0, 0, LR_DEFAULTCOLOR); + button->locked_bmp_act = + (HBITMAP)LoadImage(floatbar->wfc->hInstance, MAKEINTRESOURCE(lock_resid_act), IMAGE_BITMAP, + 0, 0, LR_DEFAULTCOLOR); + return button; +} + +static Button* floatbar_get_button(const wfFloatBar* const floatbar, const int x, const int y) +{ + int i; + + if ((y > BUTTON_Y) && (y < BUTTON_Y + BUTTON_HEIGHT)) + { + for (i = 0; i < BTN_MAX; i++) + { + if ((floatbar->buttons[i] != NULL) && (x > floatbar->buttons[i]->x) && + (x < floatbar->buttons[i]->x + floatbar->buttons[i]->w)) + { + return floatbar->buttons[i]; + } + } + } + + return NULL; +} + +static BOOL floatbar_paint(wfFloatBar* const floatbar, const HDC hdc) +{ + int i; + HPEN hpen; + HGDIOBJECT orig; + /* paint background */ + GRADIENT_RECT gradientRect = { 0, 1 }; + COLORREF rgbTop = RGB(117, 154, 198); + COLORREF rgbBottom = RGB(6, 55, 120); + const int top = 0; + int left = 0; + int bottom = BACKGROUND_H - 1; + int right = BACKGROUND_W - 1; + const int angleOffset = BACKGROUND_H - 1; + TRIVERTEX triVertext[2] = { left, + top, + GetRValue(rgbTop) << 8, + GetGValue(rgbTop) << 8, + GetBValue(rgbTop) << 8, + 0x0000, + right, + bottom, + GetRValue(rgbBottom) << 8, + GetGValue(rgbBottom) << 8, + GetBValue(rgbBottom) << 8, + 0x0000 }; + + if (!floatbar) + return FALSE; + + GradientFill(hdc, triVertext, 2, &gradientRect, 1, GRADIENT_FILL_RECT_V); + /* paint shadow */ + hpen = CreatePen(PS_SOLID, 1, RGB(71, 71, 71)); + orig = SelectObject(hdc, hpen); + MoveToEx(hdc, left, top, NULL); + LineTo(hdc, left + angleOffset, bottom); + LineTo(hdc, right - angleOffset, bottom); + LineTo(hdc, right + 1, top - 1); + DeleteObject(hpen); + hpen = CreatePen(PS_SOLID, 1, RGB(107, 141, 184)); + SelectObject(hdc, hpen); + left += 1; + bottom -= 1; + right -= 1; + MoveToEx(hdc, left, top, NULL); + LineTo(hdc, left + (angleOffset - 1), bottom); + LineTo(hdc, right - (angleOffset - 1), bottom); + LineTo(hdc, right + 1, top - 1); + DeleteObject(hpen); + SelectObject(hdc, orig); + DrawText(hdc, floatbar->wfc->window_title, wcslen(floatbar->wfc->window_title), + &floatbar->textRect, + DT_CENTER | DT_VCENTER | DT_END_ELLIPSIS | DT_NOPREFIX | DT_SINGLELINE); + + /* paint buttons */ + + for (i = 0; i < BTN_MAX; i++) + button_paint(floatbar->buttons[i], hdc); + + return TRUE; +} + +static LRESULT CALLBACK floatbar_proc(const HWND hWnd, const UINT Msg, const WPARAM wParam, + const LPARAM lParam) +{ + static int dragging = FALSE; + static int lbtn_dwn = FALSE; + static int btn_dwn_x = 0; + static wfFloatBar* floatbar; + static TRACKMOUSEEVENT tme; + PAINTSTRUCT ps; + Button* button; + HDC hdc; + int pos_x; + int pos_y; + NONCLIENTMETRICS ncm; + int xScreen = GetSystemMetrics(SM_CXSCREEN); + + switch (Msg) + { + case WM_CREATE: + floatbar = ((wfFloatBar*)((CREATESTRUCT*)lParam)->lpCreateParams); + floatbar->hwnd = hWnd; + GetWindowRect(floatbar->hwnd, &floatbar->rect); + floatbar->width = floatbar->rect.right - floatbar->rect.left; + floatbar->height = floatbar->rect.bottom - floatbar->rect.top; + hdc = GetDC(hWnd); + floatbar->hdcmem = CreateCompatibleDC(hdc); + ReleaseDC(hWnd, hdc); + tme.cbSize = sizeof(TRACKMOUSEEVENT); + tme.dwFlags = TME_LEAVE; + tme.hwndTrack = hWnd; + tme.dwHoverTime = HOVER_DEFAULT; + // Use caption font, white, draw transparent + GetClientRect(hWnd, &floatbar->textRect); + InflateRect(&floatbar->textRect, -TEXT_X, 0); + SetBkMode(hdc, TRANSPARENT); + SetTextColor(hdc, RGB(255, 255, 255)); + ncm.cbSize = sizeof(NONCLIENTMETRICS); + SystemParametersInfo(SPI_GETNONCLIENTMETRICS, sizeof(NONCLIENTMETRICS), &ncm, 0); + SelectObject(hdc, CreateFontIndirect(&ncm.lfCaptionFont)); + floatbar_trigger_hide(floatbar); + break; + + case WM_PAINT: + hdc = BeginPaint(hWnd, &ps); + floatbar_paint(floatbar, hdc); + EndPaint(hWnd, &ps); + break; + + case WM_LBUTTONDOWN: + pos_x = lParam & 0xffff; + pos_y = (lParam >> 16) & 0xffff; + button = floatbar_get_button(floatbar, pos_x, pos_y); + + if (!button) + { + SetCapture(hWnd); + dragging = TRUE; + btn_dwn_x = lParam & 0xffff; + } + else + lbtn_dwn = TRUE; + + break; + + case WM_LBUTTONUP: + pos_x = lParam & 0xffff; + pos_y = (lParam >> 16) & 0xffff; + ReleaseCapture(); + dragging = FALSE; + + if (lbtn_dwn) + { + button = floatbar_get_button(floatbar, pos_x, pos_y); + + if (button) + button_hit(button); + + lbtn_dwn = FALSE; + } + + break; + + case WM_MOUSEMOVE: + pos_x = lParam & 0xffff; + pos_y = (lParam >> 16) & 0xffff; + + if (!floatbar->locked) + floatbar_animation(floatbar, TRUE); + + if (dragging) + { + floatbar->rect.left = floatbar->rect.left + (lParam & 0xffff) - btn_dwn_x; + + if (floatbar->rect.left < 0) + floatbar->rect.left = 0; + else if (floatbar->rect.left > xScreen - floatbar->width) + floatbar->rect.left = xScreen - floatbar->width; + + MoveWindow(hWnd, floatbar->rect.left, 0, floatbar->width, floatbar->height, TRUE); + } + else + { + int i; + + for (i = 0; i < BTN_MAX; i++) + { + if (floatbar->buttons[i] != NULL) + { + floatbar->buttons[i]->active = FALSE; + } + } + + button = floatbar_get_button(floatbar, pos_x, pos_y); + + if (button) + button->active = TRUE; + + InvalidateRect(hWnd, NULL, FALSE); + UpdateWindow(hWnd); + } + + TrackMouseEvent(&tme); + break; + + case WM_CAPTURECHANGED: + dragging = FALSE; + break; + + case WM_MOUSELEAVE: + { + int i; + + for (i = 0; i < BTN_MAX; i++) + { + if (floatbar->buttons[i] != NULL) + { + floatbar->buttons[i]->active = FALSE; + } + } + + InvalidateRect(hWnd, NULL, FALSE); + UpdateWindow(hWnd); + floatbar_trigger_hide(floatbar); + break; + } + + case WM_TIMER: + switch (wParam) + { + case TIMER_HIDE: + floatbar_animation(floatbar, FALSE); + break; + + case TIMER_ANIMAT_SHOW: + { + floatbar->offset--; + MoveWindow(floatbar->hwnd, floatbar->rect.left, -floatbar->offset, + floatbar->width, floatbar->height, TRUE); + + if (floatbar->offset <= 0) + floatbar_show(floatbar); + + break; + } + + case TIMER_ANIMAT_HIDE: + { + floatbar->offset++; + MoveWindow(floatbar->hwnd, floatbar->rect.left, -floatbar->offset, + floatbar->width, floatbar->height, TRUE); + + if (floatbar->offset >= floatbar->height - 2) + floatbar_hide(floatbar); + + break; + } + + default: + break; + } + + break; + + case WM_DESTROY: + DeleteDC(floatbar->hdcmem); + PostQuitMessage(0); + break; + + default: + return DefWindowProc(hWnd, Msg, wParam, lParam); + } + + return 0; +} + +static BOOL floatbar_window_create(wfFloatBar* floatbar) +{ + WNDCLASSEX wnd_cls; + HWND barWnd; + HRGN hRgn; + POINT pt[4]; + RECT rect; + LONG x; + + if (!floatbar) + return FALSE; + + if (!GetWindowRect(floatbar->parent, &rect)) + return FALSE; + + x = (rect.right - rect.left - BACKGROUND_W) / 2; + wnd_cls.cbSize = sizeof(WNDCLASSEX); + wnd_cls.style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC; + wnd_cls.lpfnWndProc = floatbar_proc; + wnd_cls.cbClsExtra = 0; + wnd_cls.cbWndExtra = 0; + wnd_cls.hIcon = LoadIcon(NULL, IDI_APPLICATION); + wnd_cls.hCursor = LoadCursor(floatbar->root_window, IDC_ARROW); + wnd_cls.hbrBackground = NULL; + wnd_cls.lpszMenuName = NULL; + wnd_cls.lpszClassName = L"floatbar"; + wnd_cls.hInstance = floatbar->root_window; + wnd_cls.hIconSm = LoadIcon(NULL, IDI_APPLICATION); + RegisterClassEx(&wnd_cls); + barWnd = CreateWindowEx(WS_EX_TOPMOST, L"floatbar", L"floatbar", WS_CHILD, x, 0, BACKGROUND_W, + BACKGROUND_H, floatbar->parent, NULL, floatbar->root_window, floatbar); + + if (barWnd == NULL) + return FALSE; + + pt[0].x = 0; + pt[0].y = 0; + pt[1].x = BACKGROUND_W; + pt[1].y = 0; + pt[2].x = BACKGROUND_W - BACKGROUND_H; + pt[2].y = BACKGROUND_H; + pt[3].x = BACKGROUND_H; + pt[3].y = BACKGROUND_H; + hRgn = CreatePolygonRgn(pt, 4, ALTERNATE); + SetWindowRgn(barWnd, hRgn, TRUE); + return TRUE; +} + +void wf_floatbar_free(wfFloatBar* floatbar) +{ + if (!floatbar) + return; + + free(floatbar); +} + +wfFloatBar* wf_floatbar_new(wfContext* wfc, HINSTANCE window, DWORD flags) +{ + wfFloatBar* floatbar; + + /* Floatbar not enabled */ + if ((flags & 0x0001) == 0) + return NULL; + + if (!wfc) + return NULL; + + // TODO: Disable for remote app + floatbar = (wfFloatBar*)calloc(1, sizeof(wfFloatBar)); + + if (!floatbar) + return NULL; + + floatbar->root_window = window; + floatbar->flags = flags; + floatbar->wfc = wfc; + floatbar->locked = (flags & 0x0002) != 0; + floatbar->shown = (flags & 0x0006) != 0; /* If it is loked or shown show it */ + floatbar->hwnd = NULL; + floatbar->parent = wfc->hwnd; + floatbar->hdcmem = NULL; + + if (wfc->fullscreen_toggle) + { + floatbar->buttons[0] = + floatbar_create_button(floatbar, BUTTON_MINIMIZE, IDB_MINIMIZE, IDB_MINIMIZE_ACT, + MINIMIZE_X, BUTTON_Y, BUTTON_HEIGHT, BUTTON_WIDTH); + floatbar->buttons[1] = + floatbar_create_button(floatbar, BUTTON_RESTORE, IDB_RESTORE, IDB_RESTORE_ACT, + RESTORE_X, BUTTON_Y, BUTTON_HEIGHT, BUTTON_WIDTH); + } + else + { + floatbar->buttons[0] = NULL; + floatbar->buttons[1] = NULL; + } + + floatbar->buttons[2] = floatbar_create_button(floatbar, BUTTON_CLOSE, IDB_CLOSE, IDB_CLOSE_ACT, + CLOSE_X, BUTTON_Y, BUTTON_HEIGHT, BUTTON_WIDTH); + floatbar->buttons[3] = + floatbar_create_lock_button(floatbar, IDB_UNLOCK, IDB_UNLOCK_ACT, IDB_LOCK, IDB_LOCK_ACT, + LOCK_X, BUTTON_Y, BUTTON_HEIGHT, BUTTON_WIDTH); + + if (!floatbar_window_create(floatbar)) + goto fail; + + if (!update_locked_state(floatbar)) + goto fail; + + if (!wf_floatbar_toggle_fullscreen(floatbar, wfc->context.settings->Fullscreen)) + goto fail; + + return floatbar; +fail: + wf_floatbar_free(floatbar); + return NULL; +} + +BOOL wf_floatbar_toggle_fullscreen(wfFloatBar* floatbar, BOOL fullscreen) +{ + BOOL show_fs, show_wn; + + if (!floatbar) + return FALSE; + + show_fs = (floatbar->flags & 0x0010) != 0; + show_wn = (floatbar->flags & 0x0020) != 0; + + if ((show_fs && fullscreen) || (show_wn && !fullscreen)) + { + ShowWindow(floatbar->hwnd, SW_SHOWNORMAL); + Sleep(10); + + if (floatbar->shown) + floatbar_show(floatbar); + else + floatbar_hide(floatbar); + } + else + { + ShowWindow(floatbar->hwnd, SW_HIDE); + } + + return TRUE; +} diff --git a/client/Windows/wf_floatbar.h b/client/Windows/wf_floatbar.h new file mode 100644 index 0000000..2636aba --- /dev/null +++ b/client/Windows/wf_floatbar.h @@ -0,0 +1,33 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Windows Float Bar + * + * Copyright 2013 Zhang Zhaolong + * + * Licensed 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 FREERDP_CLIENT_WIN_FLOATBAR_H +#define FREERDP_CLIENT_WIN_FLOATBAR_H + +#include + +typedef struct _FloatBar wfFloatBar; +typedef struct wf_context wfContext; + +wfFloatBar* wf_floatbar_new(wfContext* wfc, HINSTANCE window, DWORD flags); +void wf_floatbar_free(wfFloatBar* floatbar); + +BOOL wf_floatbar_toggle_fullscreen(wfFloatBar* floatbar, BOOL fullscreen); + +#endif /* FREERDP_CLIENT_WIN_FLOATBAR_H */ diff --git a/client/Windows/wf_gdi.c b/client/Windows/wf_gdi.c new file mode 100644 index 0000000..329f43a --- /dev/null +++ b/client/Windows/wf_gdi.c @@ -0,0 +1,827 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Windows GDI + * + * Copyright 2009-2011 Jay Sorg + * Copyright 2010-2011 Vic Lee + * Copyright 2010-2011 Marc-Andre Moreau + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "wf_client.h" +#include "wf_graphics.h" +#include "wf_gdi.h" + +#define TAG CLIENT_TAG("windows.gdi") + +static const BYTE wf_rop2_table[] = { + R2_BLACK, /* 0 */ + R2_NOTMERGEPEN, /* DPon */ + R2_MASKNOTPEN, /* DPna */ + R2_NOTCOPYPEN, /* Pn */ + R2_MASKPENNOT, /* PDna */ + R2_NOT, /* Dn */ + R2_XORPEN, /* DPx */ + R2_NOTMASKPEN, /* DPan */ + R2_MASKPEN, /* DPa */ + R2_NOTXORPEN, /* DPxn */ + R2_NOP, /* D */ + R2_MERGENOTPEN, /* DPno */ + R2_COPYPEN, /* P */ + R2_MERGEPENNOT, /* PDno */ + R2_MERGEPEN, /* PDo */ + R2_WHITE, /* 1 */ +}; + +static BOOL wf_decode_color(wfContext* wfc, const UINT32 srcColor, COLORREF* color, UINT32* format) +{ + rdpGdi* gdi; + rdpSettings* settings; + UINT32 SrcFormat, DstFormat; + + if (!wfc) + return FALSE; + + gdi = wfc->context.gdi; + settings = wfc->context.settings; + + if (!gdi || !settings) + return FALSE; + + SrcFormat = gdi_get_pixel_format(gdi->context->settings->ColorDepth); + + if (format) + *format = SrcFormat; + + switch (GetBitsPerPixel(gdi->dstFormat)) + { + case 32: + DstFormat = PIXEL_FORMAT_ABGR32; + break; + + case 24: + DstFormat = PIXEL_FORMAT_BGR24; + break; + + case 16: + DstFormat = PIXEL_FORMAT_RGB16; + break; + + default: + return FALSE; + } + + *color = FreeRDPConvertColor(srcColor, SrcFormat, DstFormat, &gdi->palette); + return TRUE; +} + +static BOOL wf_set_rop2(HDC hdc, int rop2) +{ + if ((rop2 < 0x01) || (rop2 > 0x10)) + { + WLog_ERR(TAG, "Unsupported ROP2: %d", rop2); + return FALSE; + } + + SetROP2(hdc, wf_rop2_table[rop2 - 1]); + return TRUE; +} + +static wfBitmap* wf_glyph_new(wfContext* wfc, GLYPH_DATA* glyph) +{ + wfBitmap* glyph_bmp; + glyph_bmp = wf_image_new(wfc, glyph->cx, glyph->cy, PIXEL_FORMAT_MONO, glyph->aj); + return glyph_bmp; +} + +static void wf_glyph_free(wfBitmap* glyph) +{ + wf_image_free(glyph); +} + +static BYTE* wf_glyph_convert(wfContext* wfc, int width, int height, BYTE* data) +{ + int indexx; + int indexy; + BYTE* src; + BYTE* dst; + BYTE* cdata; + int src_bytes_per_row; + int dst_bytes_per_row; + src_bytes_per_row = (width + 7) / 8; + dst_bytes_per_row = src_bytes_per_row + (src_bytes_per_row % 2); + cdata = (BYTE*)malloc(dst_bytes_per_row * height); + src = data; + + for (indexy = 0; indexy < height; indexy++) + { + dst = cdata + indexy * dst_bytes_per_row; + + for (indexx = 0; indexx < dst_bytes_per_row; indexx++) + { + if (indexx < src_bytes_per_row) + *dst++ = *src++; + else + *dst++ = 0; + } + } + + return cdata; +} + +static HBRUSH wf_create_brush(wfContext* wfc, rdpBrush* brush, UINT32 color, UINT32 bpp) +{ + UINT32 i; + HBRUSH br; + LOGBRUSH lbr; + BYTE* cdata; + BYTE ipattern[8]; + HBITMAP pattern = NULL; + lbr.lbStyle = brush->style; + + if (lbr.lbStyle == BS_DIBPATTERN || lbr.lbStyle == BS_DIBPATTERN8X8 || + lbr.lbStyle == BS_DIBPATTERNPT) + lbr.lbColor = DIB_RGB_COLORS; + else + lbr.lbColor = color; + + if (lbr.lbStyle == BS_PATTERN || lbr.lbStyle == BS_PATTERN8X8) + { + if (brush->bpp > 1) + { + UINT32 format = gdi_get_pixel_format(bpp); + pattern = wf_create_dib(wfc, 8, 8, format, brush->data, NULL); + lbr.lbHatch = (ULONG_PTR)pattern; + } + else + { + for (i = 0; i != 8; i++) + ipattern[7 - i] = brush->data[i]; + + cdata = wf_glyph_convert(wfc, 8, 8, ipattern); + pattern = CreateBitmap(8, 8, 1, 1, cdata); + lbr.lbHatch = (ULONG_PTR)pattern; + free(cdata); + } + } + else if (lbr.lbStyle == BS_HATCHED) + { + lbr.lbHatch = brush->hatch; + } + else + { + lbr.lbHatch = 0; + } + + br = CreateBrushIndirect(&lbr); + SetBrushOrgEx(wfc->drawing->hdc, brush->x, brush->y, NULL); + + if (pattern != NULL) + DeleteObject(pattern); + + return br; +} + +static BOOL wf_scale_rect(wfContext* wfc, RECT* source) +{ + UINT32 ww, wh, dw, dh; + rdpSettings* settings; + + if (!wfc || !source || !wfc->context.settings) + return FALSE; + + settings = wfc->context.settings; + + if (!settings) + return FALSE; + + dw = settings->DesktopWidth; + dh = settings->DesktopHeight; + + if (!wfc->client_width) + wfc->client_width = dw; + + if (!wfc->client_height) + wfc->client_height = dh; + + ww = wfc->client_width; + wh = wfc->client_height; + + if (!ww) + ww = dw; + + if (!wh) + wh = dh; + + if (wfc->context.settings->SmartSizing && (ww != dw || wh != dh)) + { + source->bottom = source->bottom * wh / dh + 20; + source->top = source->top * wh / dh - 20; + source->left = source->left * ww / dw - 20; + source->right = source->right * ww / dw + 20; + } + + source->bottom -= wfc->yCurrentScroll; + source->top -= wfc->yCurrentScroll; + source->left -= wfc->xCurrentScroll; + source->right -= wfc->xCurrentScroll; + return TRUE; +} + +void wf_invalidate_region(wfContext* wfc, UINT32 x, UINT32 y, UINT32 width, UINT32 height) +{ + RECT rect; + rdpGdi* gdi = wfc->context.gdi; + wfc->update_rect.left = x + wfc->offset_x; + wfc->update_rect.top = y + wfc->offset_y; + wfc->update_rect.right = wfc->update_rect.left + width; + wfc->update_rect.bottom = wfc->update_rect.top + height; + wf_scale_rect(wfc, &(wfc->update_rect)); + InvalidateRect(wfc->hwnd, &(wfc->update_rect), FALSE); + rect.left = x; + rect.right = width; + rect.top = y; + rect.bottom = height; + wf_scale_rect(wfc, &rect); + gdi_InvalidateRegion(gdi->primary->hdc, rect.left, rect.top, rect.right, rect.bottom); +} + +void wf_update_offset(wfContext* wfc) +{ + rdpSettings* settings; + settings = wfc->context.settings; + + if (wfc->fullscreen) + { + if (wfc->context.settings->UseMultimon) + { + int x = GetSystemMetrics(SM_XVIRTUALSCREEN); + int y = GetSystemMetrics(SM_YVIRTUALSCREEN); + int w = GetSystemMetrics(SM_CXVIRTUALSCREEN); + int h = GetSystemMetrics(SM_CYVIRTUALSCREEN); + wfc->offset_x = (w - settings->DesktopWidth) / 2; + + if (wfc->offset_x < x) + wfc->offset_x = x; + + wfc->offset_y = (h - settings->DesktopHeight) / 2; + + if (wfc->offset_y < y) + wfc->offset_y = y; + } + else + { + wfc->offset_x = (GetSystemMetrics(SM_CXSCREEN) - settings->DesktopWidth) / 2; + + if (wfc->offset_x < 0) + wfc->offset_x = 0; + + wfc->offset_y = (GetSystemMetrics(SM_CYSCREEN) - settings->DesktopHeight) / 2; + + if (wfc->offset_y < 0) + wfc->offset_y = 0; + } + } + else + { + wfc->offset_x = 0; + wfc->offset_y = 0; + } +} + +void wf_resize_window(wfContext* wfc) +{ + rdpSettings* settings; + settings = wfc->context.settings; + + if (wfc->fullscreen) + { + if (wfc->context.settings->UseMultimon) + { + int x = GetSystemMetrics(SM_XVIRTUALSCREEN); + int y = GetSystemMetrics(SM_YVIRTUALSCREEN); + int w = GetSystemMetrics(SM_CXVIRTUALSCREEN); + int h = GetSystemMetrics(SM_CYVIRTUALSCREEN); + SetWindowLongPtr(wfc->hwnd, GWL_STYLE, WS_POPUP); + SetWindowPos(wfc->hwnd, HWND_TOP, x, y, w, h, SWP_FRAMECHANGED); + } + else + { + SetWindowLongPtr(wfc->hwnd, GWL_STYLE, WS_POPUP); + SetWindowPos(wfc->hwnd, HWND_TOP, 0, 0, GetSystemMetrics(SM_CXSCREEN), + GetSystemMetrics(SM_CYSCREEN), SWP_FRAMECHANGED); + } + } + else if (!wfc->context.settings->Decorations) + { + SetWindowLongPtr(wfc->hwnd, GWL_STYLE, WS_CHILD); + + if (settings->EmbeddedWindow) + { + if (!wfc->client_height) + wfc->client_height = settings->DesktopHeight; + + if (!wfc->client_width) + wfc->client_width = settings->DesktopWidth; + + wf_update_canvas_diff(wfc); + /* Now resize to get full canvas size and room for caption and borders */ + SetWindowPos(wfc->hwnd, HWND_TOP, wfc->client_x, wfc->client_y, + wfc->client_width + wfc->diff.x, wfc->client_height + wfc->diff.y, + 0 /*SWP_FRAMECHANGED*/); + } + else + { + /* Now resize to get full canvas size and room for caption and borders */ + SetWindowPos(wfc->hwnd, HWND_TOP, 0, 0, settings->DesktopWidth, settings->DesktopHeight, + SWP_FRAMECHANGED); + wf_update_canvas_diff(wfc); + SetWindowPos(wfc->hwnd, HWND_TOP, -1, -1, settings->DesktopWidth + wfc->diff.x, + settings->DesktopHeight + wfc->diff.y, SWP_NOMOVE | SWP_FRAMECHANGED); + } + } + else + { + SetWindowLongPtr(wfc->hwnd, GWL_STYLE, + WS_CAPTION | WS_OVERLAPPED | WS_SYSMENU | WS_MINIMIZEBOX | WS_SIZEBOX | + WS_MAXIMIZEBOX); + + if (!wfc->client_height) + wfc->client_height = settings->DesktopHeight; + + if (!wfc->client_width) + wfc->client_width = settings->DesktopWidth; + + if (!wfc->client_x) + wfc->client_x = 10; + + if (!wfc->client_y) + wfc->client_y = 10; + + wf_update_canvas_diff(wfc); + /* Now resize to get full canvas size and room for caption and borders */ + SetWindowPos(wfc->hwnd, HWND_TOP, wfc->client_x, wfc->client_y, + wfc->client_width + wfc->diff.x, wfc->client_height + wfc->diff.y, + 0 /*SWP_FRAMECHANGED*/); + // wf_size_scrollbars(wfc, wfc->client_width, wfc->client_height); + } + + wf_update_offset(wfc); +} + +void wf_toggle_fullscreen(wfContext* wfc) +{ + ShowWindow(wfc->hwnd, SW_HIDE); + wfc->fullscreen = !wfc->fullscreen; + + if (wfc->fullscreen) + { + wfc->disablewindowtracking = TRUE; + } + + wf_floatbar_toggle_fullscreen(wfc->floatbar, wfc->fullscreen); + SetParent(wfc->hwnd, wfc->fullscreen ? NULL : wfc->hWndParent); + wf_resize_window(wfc); + ShowWindow(wfc->hwnd, SW_SHOW); + SetForegroundWindow(wfc->hwnd); + + if (!wfc->fullscreen) + { + // Reenable window tracking AFTER resizing it back, otherwise it can lean to repositioning + // errors. + wfc->disablewindowtracking = FALSE; + } +} + +static BOOL wf_gdi_palette_update(rdpContext* context, const PALETTE_UPDATE* palette) +{ + return TRUE; +} + +void wf_set_null_clip_rgn(wfContext* wfc) +{ + SelectClipRgn(wfc->drawing->hdc, NULL); +} + +void wf_set_clip_rgn(wfContext* wfc, int x, int y, int width, int height) +{ + HRGN clip; + clip = CreateRectRgn(x, y, x + width, y + height); + SelectClipRgn(wfc->drawing->hdc, clip); + DeleteObject(clip); +} + +static BOOL wf_gdi_set_bounds(rdpContext* context, const rdpBounds* bounds) +{ + HRGN hrgn; + wfContext* wfc = (wfContext*)context; + + if (!context || !bounds) + return FALSE; + + if (bounds != NULL) + { + hrgn = CreateRectRgn(bounds->left, bounds->top, bounds->right + 1, bounds->bottom + 1); + SelectClipRgn(wfc->drawing->hdc, hrgn); + DeleteObject(hrgn); + } + else + SelectClipRgn(wfc->drawing->hdc, NULL); + + return TRUE; +} + +static BOOL wf_gdi_dstblt(rdpContext* context, const DSTBLT_ORDER* dstblt) +{ + wfContext* wfc = (wfContext*)context; + + if (!context || !dstblt) + return FALSE; + + if (!BitBlt(wfc->drawing->hdc, dstblt->nLeftRect, dstblt->nTopRect, dstblt->nWidth, + dstblt->nHeight, NULL, 0, 0, gdi_rop3_code(dstblt->bRop))) + return FALSE; + + wf_invalidate_region(wfc, dstblt->nLeftRect, dstblt->nTopRect, dstblt->nWidth, dstblt->nHeight); + return TRUE; +} + +static BOOL wf_gdi_patblt(rdpContext* context, PATBLT_ORDER* patblt) +{ + HBRUSH brush; + HBRUSH org_brush; + int org_bkmode; + UINT32 fgcolor; + UINT32 bgcolor; + COLORREF org_bkcolor; + COLORREF org_textcolor; + BOOL rc; + wfContext* wfc = (wfContext*)context; + + if (!context || !patblt) + return FALSE; + + if (!wf_decode_color(wfc, patblt->foreColor, &fgcolor, NULL)) + return FALSE; + + if (!wf_decode_color(wfc, patblt->backColor, &bgcolor, NULL)) + return FALSE; + + brush = wf_create_brush(wfc, &patblt->brush, fgcolor, context->settings->ColorDepth); + org_bkmode = SetBkMode(wfc->drawing->hdc, OPAQUE); + org_bkcolor = SetBkColor(wfc->drawing->hdc, bgcolor); + org_textcolor = SetTextColor(wfc->drawing->hdc, fgcolor); + org_brush = (HBRUSH)SelectObject(wfc->drawing->hdc, brush); + rc = PatBlt(wfc->drawing->hdc, patblt->nLeftRect, patblt->nTopRect, patblt->nWidth, + patblt->nHeight, gdi_rop3_code(patblt->bRop)); + SelectObject(wfc->drawing->hdc, org_brush); + DeleteObject(brush); + SetBkMode(wfc->drawing->hdc, org_bkmode); + SetBkColor(wfc->drawing->hdc, org_bkcolor); + SetTextColor(wfc->drawing->hdc, org_textcolor); + + if (wfc->drawing == wfc->primary) + wf_invalidate_region(wfc, patblt->nLeftRect, patblt->nTopRect, patblt->nWidth, + patblt->nHeight); + + return rc; +} + +static BOOL wf_gdi_scrblt(rdpContext* context, const SCRBLT_ORDER* scrblt) +{ + wfContext* wfc = (wfContext*)context; + + if (!context || !scrblt || !wfc->drawing) + return FALSE; + + if (!BitBlt(wfc->drawing->hdc, scrblt->nLeftRect, scrblt->nTopRect, scrblt->nWidth, + scrblt->nHeight, wfc->primary->hdc, scrblt->nXSrc, scrblt->nYSrc, + gdi_rop3_code(scrblt->bRop))) + return FALSE; + + wf_invalidate_region(wfc, scrblt->nLeftRect, scrblt->nTopRect, scrblt->nWidth, scrblt->nHeight); + return TRUE; +} + +static BOOL wf_gdi_opaque_rect(rdpContext* context, const OPAQUE_RECT_ORDER* opaque_rect) +{ + RECT rect; + HBRUSH brush; + UINT32 brush_color; + wfContext* wfc = (wfContext*)context; + + if (!context || !opaque_rect) + return FALSE; + + if (!wf_decode_color(wfc, opaque_rect->color, &brush_color, NULL)) + return FALSE; + + rect.left = opaque_rect->nLeftRect; + rect.top = opaque_rect->nTopRect; + rect.right = opaque_rect->nLeftRect + opaque_rect->nWidth; + rect.bottom = opaque_rect->nTopRect + opaque_rect->nHeight; + brush = CreateSolidBrush(brush_color); + FillRect(wfc->drawing->hdc, &rect, brush); + DeleteObject(brush); + + if (wfc->drawing == wfc->primary) + wf_invalidate_region(wfc, rect.left, rect.top, rect.right - rect.left + 1, + rect.bottom - rect.top + 1); + + return TRUE; +} + +static BOOL wf_gdi_multi_opaque_rect(rdpContext* context, + const MULTI_OPAQUE_RECT_ORDER* multi_opaque_rect) +{ + UINT32 i; + RECT rect; + HBRUSH brush; + UINT32 brush_color; + wfContext* wfc = (wfContext*)context; + + if (!context || !multi_opaque_rect) + return FALSE; + + if (!wf_decode_color(wfc, multi_opaque_rect->color, &brush_color, NULL)) + return FALSE; + + for (i = 0; i < multi_opaque_rect->numRectangles; i++) + { + const DELTA_RECT* rectangle = &multi_opaque_rect->rectangles[i]; + rect.left = rectangle->left; + rect.top = rectangle->top; + rect.right = rectangle->left + rectangle->width; + rect.bottom = rectangle->top + rectangle->height; + brush = CreateSolidBrush(brush_color); + FillRect(wfc->drawing->hdc, &rect, brush); + + if (wfc->drawing == wfc->primary) + wf_invalidate_region(wfc, rect.left, rect.top, rect.right - rect.left + 1, + rect.bottom - rect.top + 1); + + DeleteObject(brush); + } + + return TRUE; +} + +static BOOL wf_gdi_line_to(rdpContext* context, const LINE_TO_ORDER* line_to) +{ + HPEN pen; + HPEN org_pen; + int x, y, w, h; + UINT32 pen_color; + wfContext* wfc = (wfContext*)context; + + if (!context || !line_to) + return FALSE; + + if (!wf_decode_color(wfc, line_to->penColor, &pen_color, NULL)) + return FALSE; + + pen = CreatePen(line_to->penStyle, line_to->penWidth, pen_color); + wf_set_rop2(wfc->drawing->hdc, line_to->bRop2); + org_pen = (HPEN)SelectObject(wfc->drawing->hdc, pen); + MoveToEx(wfc->drawing->hdc, line_to->nXStart, line_to->nYStart, NULL); + LineTo(wfc->drawing->hdc, line_to->nXEnd, line_to->nYEnd); + x = (line_to->nXStart < line_to->nXEnd) ? line_to->nXStart : line_to->nXEnd; + y = (line_to->nYStart < line_to->nYEnd) ? line_to->nYStart : line_to->nYEnd; + w = (line_to->nXStart < line_to->nXEnd) ? (line_to->nXEnd - line_to->nXStart) + : (line_to->nXStart - line_to->nXEnd); + h = (line_to->nYStart < line_to->nYEnd) ? (line_to->nYEnd - line_to->nYStart) + : (line_to->nYStart - line_to->nYEnd); + + if (wfc->drawing == wfc->primary) + wf_invalidate_region(wfc, x, y, w, h); + + SelectObject(wfc->drawing->hdc, org_pen); + DeleteObject(pen); + return TRUE; +} + +static BOOL wf_gdi_polyline(rdpContext* context, const POLYLINE_ORDER* polyline) +{ + int org_rop2; + HPEN hpen; + HPEN org_hpen; + UINT32 pen_color; + wfContext* wfc = (wfContext*)context; + + if (!context || !polyline) + return FALSE; + + if (!wf_decode_color(wfc, polyline->penColor, &pen_color, NULL)) + return FALSE; + + hpen = CreatePen(0, 1, pen_color); + org_rop2 = wf_set_rop2(wfc->drawing->hdc, polyline->bRop2); + org_hpen = (HPEN)SelectObject(wfc->drawing->hdc, hpen); + + if (polyline->numDeltaEntries > 0) + { + POINT* pts; + POINT temp; + int numPoints; + int i; + numPoints = polyline->numDeltaEntries + 1; + pts = (POINT*)malloc(sizeof(POINT) * numPoints); + pts[0].x = temp.x = polyline->xStart; + pts[0].y = temp.y = polyline->yStart; + + for (i = 0; i < (int)polyline->numDeltaEntries; i++) + { + temp.x += polyline->points[i].x; + temp.y += polyline->points[i].y; + pts[i + 1].x = temp.x; + pts[i + 1].y = temp.y; + } + + if (wfc->drawing == wfc->primary) + wf_invalidate_region(wfc, wfc->client_x, wfc->client_y, wfc->client_width, + wfc->client_height); + + Polyline(wfc->drawing->hdc, pts, numPoints); + free(pts); + } + + SelectObject(wfc->drawing->hdc, org_hpen); + wf_set_rop2(wfc->drawing->hdc, org_rop2); + DeleteObject(hpen); + return TRUE; +} + +static BOOL wf_gdi_memblt(rdpContext* context, MEMBLT_ORDER* memblt) +{ + wfBitmap* bitmap; + wfContext* wfc = (wfContext*)context; + + if (!context || !memblt) + return FALSE; + + bitmap = (wfBitmap*)memblt->bitmap; + + if (!bitmap || !wfc->drawing || !wfc->drawing->hdc) + return FALSE; + + if (!BitBlt(wfc->drawing->hdc, memblt->nLeftRect, memblt->nTopRect, memblt->nWidth, + memblt->nHeight, bitmap->hdc, memblt->nXSrc, memblt->nYSrc, + gdi_rop3_code(memblt->bRop))) + return FALSE; + + if (wfc->drawing == wfc->primary) + wf_invalidate_region(wfc, memblt->nLeftRect, memblt->nTopRect, memblt->nWidth, + memblt->nHeight); + + return TRUE; +} + +static BOOL wf_gdi_mem3blt(rdpContext* context, MEM3BLT_ORDER* mem3blt) +{ + BOOL rc = FALSE; + HDC hdc; + wfBitmap* bitmap; + wfContext* wfc = (wfContext*)context; + COLORREF fgcolor, bgcolor, orgColor; + HBRUSH orgBrush = NULL, brush = NULL; + + if (!context || !mem3blt) + return FALSE; + + bitmap = (wfBitmap*)mem3blt->bitmap; + + if (!bitmap || !wfc->drawing || !wfc->drawing->hdc) + return FALSE; + + hdc = wfc->drawing->hdc; + + if (!wf_decode_color(wfc, mem3blt->foreColor, &fgcolor, NULL)) + return FALSE; + + if (!wf_decode_color(wfc, mem3blt->backColor, &bgcolor, NULL)) + return FALSE; + + orgColor = SetTextColor(hdc, fgcolor); + + switch (mem3blt->brush.style) + { + case GDI_BS_SOLID: + brush = CreateSolidBrush(fgcolor); + break; + + case GDI_BS_HATCHED: + case GDI_BS_PATTERN: + { + HBITMAP bmp = CreateBitmap(8, 8, 1, mem3blt->brush.bpp, mem3blt->brush.data); + brush = CreatePatternBrush(bmp); + } + break; + + default: + goto fail; + } + + orgBrush = SelectObject(hdc, brush); + + if (!BitBlt(hdc, mem3blt->nLeftRect, mem3blt->nTopRect, mem3blt->nWidth, mem3blt->nHeight, + bitmap->hdc, mem3blt->nXSrc, mem3blt->nYSrc, gdi_rop3_code(mem3blt->bRop))) + goto fail; + + if (wfc->drawing == wfc->primary) + wf_invalidate_region(wfc, mem3blt->nLeftRect, mem3blt->nTopRect, mem3blt->nWidth, + mem3blt->nHeight); + + rc = TRUE; +fail: + + if (brush) + SelectObject(hdc, orgBrush); + + SetTextColor(hdc, orgColor); + return rc; +} + +static BOOL wf_gdi_surface_frame_marker(rdpContext* context, + const SURFACE_FRAME_MARKER* surface_frame_marker) +{ + rdpSettings* settings; + + if (!context || !surface_frame_marker || !context->instance) + return FALSE; + + settings = context->instance->settings; + + if (!settings) + return FALSE; + + if (surface_frame_marker->frameAction == SURFACECMD_FRAMEACTION_END && + settings->FrameAcknowledge > 0) + { + IFCALL(context->instance->update->SurfaceFrameAcknowledge, context, + surface_frame_marker->frameId); + } + + return TRUE; +} + +void wf_gdi_register_update_callbacks(rdpUpdate* update) +{ + rdpPrimaryUpdate* primary = update->primary; + update->Palette = wf_gdi_palette_update; + update->SetBounds = wf_gdi_set_bounds; + primary->DstBlt = wf_gdi_dstblt; + primary->PatBlt = wf_gdi_patblt; + primary->ScrBlt = wf_gdi_scrblt; + primary->OpaqueRect = wf_gdi_opaque_rect; + primary->MultiOpaqueRect = wf_gdi_multi_opaque_rect; + primary->LineTo = wf_gdi_line_to; + primary->Polyline = wf_gdi_polyline; + primary->MemBlt = wf_gdi_memblt; + primary->Mem3Blt = wf_gdi_mem3blt; + update->SurfaceFrameMarker = wf_gdi_surface_frame_marker; +} + +void wf_update_canvas_diff(wfContext* wfc) +{ + RECT rc_client, rc_wnd; + int dx, dy; + GetClientRect(wfc->hwnd, &rc_client); + GetWindowRect(wfc->hwnd, &rc_wnd); + dx = (rc_wnd.right - rc_wnd.left) - rc_client.right; + dy = (rc_wnd.bottom - rc_wnd.top) - rc_client.bottom; + + if (!wfc->disablewindowtracking) + { + wfc->diff.x = dx; + wfc->diff.y = dy; + } +} diff --git a/client/Windows/wf_gdi.h b/client/Windows/wf_gdi.h new file mode 100644 index 0000000..a093e1a --- /dev/null +++ b/client/Windows/wf_gdi.h @@ -0,0 +1,38 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Windows GDI + * + * Copyright 2009-2011 Jay Sorg + * Copyright 2010-2011 Vic Lee + * Copyright 2010-2011 Marc-Andre Moreau + * + * Licensed 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 FREERDP_CLIENT_WIN_GDI_H +#define FREERDP_CLIENT_WIN_GDI_H + +#include "wf_client.h" + +void wf_invalidate_region(wfContext* wfc, UINT32 x, UINT32 y, UINT32 width, UINT32 height); +wfBitmap* wf_image_new(wfContext* wfc, UINT32 width, UINT32 height, UINT32 bpp, const BYTE* data); +void wf_image_free(wfBitmap* image); +void wf_update_offset(wfContext* wfc); +void wf_resize_window(wfContext* wfc); +void wf_toggle_fullscreen(wfContext* wfc); + +void wf_gdi_register_update_callbacks(rdpUpdate* update); + +void wf_update_canvas_diff(wfContext* wfc); + +#endif /* FREERDP_CLIENT_WIN_GDI_H */ diff --git a/client/Windows/wf_graphics.c b/client/Windows/wf_graphics.c new file mode 100644 index 0000000..8a146f3 --- /dev/null +++ b/client/Windows/wf_graphics.c @@ -0,0 +1,370 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Windows Graphical Objects + * + * Copyright 2010-2011 Marc-Andre Moreau + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include + +#include +#include + +#include "wf_gdi.h" +#include "wf_graphics.h" + +#define TAG CLIENT_TAG("windows") + +HBITMAP wf_create_dib(wfContext* wfc, UINT32 width, UINT32 height, UINT32 srcFormat, + const BYTE* data, BYTE** pdata) +{ + HDC hdc; + int negHeight; + HBITMAP bitmap; + BITMAPINFO bmi; + BYTE* cdata = NULL; + UINT32 dstFormat = srcFormat; + /** + * See: http://msdn.microsoft.com/en-us/library/dd183376 + * if biHeight is positive, the bitmap is bottom-up + * if biHeight is negative, the bitmap is top-down + * Since we get top-down bitmaps, let's keep it that way + */ + negHeight = (height < 0) ? height : height * (-1); + hdc = GetDC(NULL); + bmi.bmiHeader.biSize = sizeof(BITMAPINFO); + bmi.bmiHeader.biWidth = width; + bmi.bmiHeader.biHeight = negHeight; + bmi.bmiHeader.biPlanes = 1; + bmi.bmiHeader.biBitCount = GetBitsPerPixel(dstFormat); + bmi.bmiHeader.biCompression = BI_RGB; + bitmap = CreateDIBSection(hdc, &bmi, DIB_RGB_COLORS, (void**)&cdata, NULL, 0); + + if (data) + freerdp_image_copy(cdata, dstFormat, 0, 0, 0, width, height, data, srcFormat, 0, 0, 0, + &wfc->context.gdi->palette, FREERDP_FLIP_NONE); + + if (pdata) + *pdata = cdata; + + ReleaseDC(NULL, hdc); + GdiFlush(); + return bitmap; +} + +wfBitmap* wf_image_new(wfContext* wfc, UINT32 width, UINT32 height, UINT32 format, const BYTE* data) +{ + HDC hdc; + wfBitmap* image; + hdc = GetDC(NULL); + image = (wfBitmap*)malloc(sizeof(wfBitmap)); + image->hdc = CreateCompatibleDC(hdc); + image->bitmap = wf_create_dib(wfc, width, height, format, data, &(image->pdata)); + image->org_bitmap = (HBITMAP)SelectObject(image->hdc, image->bitmap); + ReleaseDC(NULL, hdc); + return image; +} + +void wf_image_free(wfBitmap* image) +{ + if (image != 0) + { + SelectObject(image->hdc, image->org_bitmap); + DeleteObject(image->bitmap); + DeleteDC(image->hdc); + free(image); + } +} + +/* Bitmap Class */ + +static BOOL wf_Bitmap_New(rdpContext* context, rdpBitmap* bitmap) +{ + HDC hdc; + wfContext* wfc = (wfContext*)context; + wfBitmap* wf_bitmap = (wfBitmap*)bitmap; + + if (!context || !bitmap) + return FALSE; + + wf_bitmap = (wfBitmap*)bitmap; + hdc = GetDC(NULL); + wf_bitmap->hdc = CreateCompatibleDC(hdc); + + if (!bitmap->data) + wf_bitmap->bitmap = CreateCompatibleBitmap(hdc, bitmap->width, bitmap->height); + else + wf_bitmap->bitmap = + wf_create_dib(wfc, bitmap->width, bitmap->height, bitmap->format, bitmap->data, NULL); + + wf_bitmap->org_bitmap = (HBITMAP)SelectObject(wf_bitmap->hdc, wf_bitmap->bitmap); + ReleaseDC(NULL, hdc); + return TRUE; +} + +static void wf_Bitmap_Free(rdpContext* context, rdpBitmap* bitmap) +{ + wfBitmap* wf_bitmap = (wfBitmap*)bitmap; + + if (wf_bitmap != 0) + { + SelectObject(wf_bitmap->hdc, wf_bitmap->org_bitmap); + DeleteObject(wf_bitmap->bitmap); + DeleteDC(wf_bitmap->hdc); + + _aligned_free(wf_bitmap->_bitmap.data); + wf_bitmap->_bitmap.data = NULL; + } +} + +static BOOL wf_Bitmap_Paint(rdpContext* context, rdpBitmap* bitmap) +{ + BOOL rc; + UINT32 width, height; + wfContext* wfc = (wfContext*)context; + wfBitmap* wf_bitmap = (wfBitmap*)bitmap; + + if (!context || !bitmap) + return FALSE; + + width = bitmap->right - bitmap->left + 1; + height = bitmap->bottom - bitmap->top + 1; + rc = BitBlt(wfc->primary->hdc, bitmap->left, bitmap->top, width, height, wf_bitmap->hdc, 0, 0, + SRCCOPY); + wf_invalidate_region(wfc, bitmap->left, bitmap->top, width, height); + return rc; +} + +static BOOL wf_Bitmap_SetSurface(rdpContext* context, rdpBitmap* bitmap, BOOL primary) +{ + wfContext* wfc = (wfContext*)context; + wfBitmap* bmp = (wfBitmap*)bitmap; + rdpGdi* gdi = context->gdi; + + if (!gdi || !wfc) + return FALSE; + + if (primary) + wfc->drawing = wfc->primary; + else if (!bmp) + return FALSE; + else + wfc->drawing = bmp; + + return TRUE; +} + +/* Pointer Class */ + +static BOOL flip_bitmap(const BYTE* src, BYTE* dst, UINT32 scanline, UINT32 nHeight) +{ + UINT32 x; + BYTE* bottomLine = dst + scanline * (nHeight - 1); + + for (x = 0; x < nHeight; x++) + { + memcpy(bottomLine, src, scanline); + src += scanline; + bottomLine -= scanline; + } + + return TRUE; +} + +static BOOL wf_Pointer_New(rdpContext* context, const rdpPointer* pointer) +{ + HCURSOR hCur; + ICONINFO info; + rdpGdi* gdi; + BOOL rc = FALSE; + + if (!context || !pointer) + return FALSE; + + gdi = context->gdi; + + if (!gdi) + return FALSE; + + info.fIcon = FALSE; + info.xHotspot = pointer->xPos; + info.yHotspot = pointer->yPos; + + if (pointer->xorBpp == 1) + { + BYTE* pdata = (BYTE*)_aligned_malloc(pointer->lengthAndMask + pointer->lengthXorMask, 16); + + if (!pdata) + goto fail; + + CopyMemory(pdata, pointer->andMaskData, pointer->lengthAndMask); + CopyMemory(pdata + pointer->lengthAndMask, pointer->xorMaskData, pointer->lengthXorMask); + info.hbmMask = CreateBitmap(pointer->width, pointer->height * 2, 1, 1, pdata); + _aligned_free(pdata); + info.hbmColor = NULL; + } + else + { + UINT32 srcFormat; + BYTE* pdata = (BYTE*)_aligned_malloc(pointer->lengthAndMask, 16); + + if (!pdata) + goto fail; + + flip_bitmap(pointer->andMaskData, pdata, (pointer->width + 7) / 8, pointer->height); + info.hbmMask = CreateBitmap(pointer->width, pointer->height, 1, 1, pdata); + _aligned_free(pdata); + + /* currently color xorBpp is only 24 per [T128] section 8.14.3 */ + srcFormat = gdi_get_pixel_format(pointer->xorBpp); + + if (!srcFormat) + goto fail; + + info.hbmColor = wf_create_dib((wfContext*)context, pointer->width, pointer->height, + gdi->dstFormat, NULL, &pdata); + + if (!info.hbmColor) + goto fail; + + if (!freerdp_image_copy_from_pointer_data( + pdata, gdi->dstFormat, 0, 0, 0, pointer->width, pointer->height, + pointer->xorMaskData, pointer->lengthXorMask, pointer->andMaskData, + pointer->lengthAndMask, pointer->xorBpp, &gdi->palette)) + { + goto fail; + } + } + + hCur = CreateIconIndirect(&info); + ((wfPointer*)pointer)->cursor = hCur; + rc = TRUE; +fail: + + if (info.hbmMask) + DeleteObject(info.hbmMask); + + if (info.hbmColor) + DeleteObject(info.hbmColor); + + return rc; +} + +static BOOL wf_Pointer_Free(rdpContext* context, rdpPointer* pointer) +{ + HCURSOR hCur; + + if (!context || !pointer) + return FALSE; + + hCur = ((wfPointer*)pointer)->cursor; + + if (hCur != 0) + DestroyIcon(hCur); + + return TRUE; +} + +static BOOL wf_Pointer_Set(rdpContext* context, const rdpPointer* pointer) +{ + HCURSOR hCur; + wfContext* wfc = (wfContext*)context; + + if (!context || !pointer) + return FALSE; + + hCur = ((wfPointer*)pointer)->cursor; + + if (hCur != NULL) + { + SetCursor(hCur); + wfc->cursor = hCur; + } + + return TRUE; +} + +static BOOL wf_Pointer_SetNull(rdpContext* context) +{ + if (!context) + return FALSE; + + return TRUE; +} + +static BOOL wf_Pointer_SetDefault(rdpContext* context) +{ + if (!context) + return FALSE; + + return TRUE; +} + +static BOOL wf_Pointer_SetPosition(rdpContext* context, UINT32 x, UINT32 y) +{ + if (!context) + return FALSE; + + return TRUE; +} + +BOOL wf_register_pointer(rdpGraphics* graphics) +{ + wfContext* wfc; + rdpPointer pointer; + + if (!graphics) + return FALSE; + + wfc = (wfContext*)graphics->context; + ZeroMemory(&pointer, sizeof(rdpPointer)); + pointer.size = sizeof(wfPointer); + pointer.New = wf_Pointer_New; + pointer.Free = wf_Pointer_Free; + pointer.Set = wf_Pointer_Set; + pointer.SetNull = wf_Pointer_SetNull; + pointer.SetDefault = wf_Pointer_SetDefault; + pointer.SetPosition = wf_Pointer_SetPosition; + graphics_register_pointer(graphics, &pointer); + return TRUE; +} + +/* Graphics Module */ + +BOOL wf_register_graphics(rdpGraphics* graphics) +{ + wfContext* wfc; + rdpGlyph glyph; + rdpBitmap bitmap; + + if (!graphics) + return FALSE; + + wfc = (wfContext*)graphics->context; + bitmap = *graphics->Bitmap_Prototype; + bitmap.size = sizeof(wfBitmap); + bitmap.New = wf_Bitmap_New; + bitmap.Free = wf_Bitmap_Free; + bitmap.Paint = wf_Bitmap_Paint; + bitmap.SetSurface = wf_Bitmap_SetSurface; + graphics_register_bitmap(graphics, &bitmap); + glyph = *graphics->Glyph_Prototype; + graphics_register_glyph(graphics, &glyph); + return TRUE; +} diff --git a/client/Windows/wf_graphics.h b/client/Windows/wf_graphics.h new file mode 100644 index 0000000..241575f --- /dev/null +++ b/client/Windows/wf_graphics.h @@ -0,0 +1,34 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Windows Graphical Objects + * + * Copyright 2010-2011 Marc-Andre Moreau + * + * Licensed 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 FREERDP_CLIENT_WIN_GRAPHICS_H +#define FREERDP_CLIENT_WIN_GRAPHICS_H + +#include "wf_client.h" + +HBITMAP wf_create_dib(wfContext* wfc, UINT32 width, UINT32 height, UINT32 format, const BYTE* data, + BYTE** pdata); +wfBitmap* wf_image_new(wfContext* wfc, UINT32 width, UINT32 height, UINT32 format, + const BYTE* data); +void wf_image_free(wfBitmap* image); + +BOOL wf_register_pointer(rdpGraphics* graphics); +BOOL wf_register_graphics(rdpGraphics* graphics); + +#endif /* FREERDP_CLIENT_WIN_GRAPHICS_H */ diff --git a/client/Windows/wf_rail.c b/client/Windows/wf_rail.c new file mode 100644 index 0000000..85fbc83 --- /dev/null +++ b/client/Windows/wf_rail.c @@ -0,0 +1,1073 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * + * Copyright 2013-2014 Marc-Andre Moreau + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include +#include + +#include "wf_rail.h" + +#define TAG CLIENT_TAG("windows") + +#define GET_X_LPARAM(lParam) ((UINT16)(lParam & 0xFFFF)) +#define GET_Y_LPARAM(lParam) ((UINT16)((lParam >> 16) & 0xFFFF)) + +struct wf_rail_window +{ + wfContext* wfc; + + HWND hWnd; + + DWORD dwStyle; + DWORD dwExStyle; + + int x; + int y; + int width; + int height; + char* title; +}; + +/* RemoteApp Core Protocol Extension */ + +struct _WINDOW_STYLE +{ + UINT32 style; + const char* name; + BOOL multi; +}; +typedef struct _WINDOW_STYLE WINDOW_STYLE; + +static const WINDOW_STYLE WINDOW_STYLES[] = { { WS_BORDER, "WS_BORDER", FALSE }, + { WS_CAPTION, "WS_CAPTION", FALSE }, + { WS_CHILD, "WS_CHILD", FALSE }, + { WS_CLIPCHILDREN, "WS_CLIPCHILDREN", FALSE }, + { WS_CLIPSIBLINGS, "WS_CLIPSIBLINGS", FALSE }, + { WS_DISABLED, "WS_DISABLED", FALSE }, + { WS_DLGFRAME, "WS_DLGFRAME", FALSE }, + { WS_GROUP, "WS_GROUP", FALSE }, + { WS_HSCROLL, "WS_HSCROLL", FALSE }, + { WS_ICONIC, "WS_ICONIC", FALSE }, + { WS_MAXIMIZE, "WS_MAXIMIZE", FALSE }, + { WS_MAXIMIZEBOX, "WS_MAXIMIZEBOX", FALSE }, + { WS_MINIMIZE, "WS_MINIMIZE", FALSE }, + { WS_MINIMIZEBOX, "WS_MINIMIZEBOX", FALSE }, + { WS_OVERLAPPED, "WS_OVERLAPPED", FALSE }, + { WS_OVERLAPPEDWINDOW, "WS_OVERLAPPEDWINDOW", TRUE }, + { WS_POPUP, "WS_POPUP", FALSE }, + { WS_POPUPWINDOW, "WS_POPUPWINDOW", TRUE }, + { WS_SIZEBOX, "WS_SIZEBOX", FALSE }, + { WS_SYSMENU, "WS_SYSMENU", FALSE }, + { WS_TABSTOP, "WS_TABSTOP", FALSE }, + { WS_THICKFRAME, "WS_THICKFRAME", FALSE }, + { WS_VISIBLE, "WS_VISIBLE", FALSE } }; + +static const WINDOW_STYLE EXTENDED_WINDOW_STYLES[] = { + { WS_EX_ACCEPTFILES, "WS_EX_ACCEPTFILES", FALSE }, + { WS_EX_APPWINDOW, "WS_EX_APPWINDOW", FALSE }, + { WS_EX_CLIENTEDGE, "WS_EX_CLIENTEDGE", FALSE }, + { WS_EX_COMPOSITED, "WS_EX_COMPOSITED", FALSE }, + { WS_EX_CONTEXTHELP, "WS_EX_CONTEXTHELP", FALSE }, + { WS_EX_CONTROLPARENT, "WS_EX_CONTROLPARENT", FALSE }, + { WS_EX_DLGMODALFRAME, "WS_EX_DLGMODALFRAME", FALSE }, + { WS_EX_LAYERED, "WS_EX_LAYERED", FALSE }, + { WS_EX_LAYOUTRTL, "WS_EX_LAYOUTRTL", FALSE }, + { WS_EX_LEFT, "WS_EX_LEFT", FALSE }, + { WS_EX_LEFTSCROLLBAR, "WS_EX_LEFTSCROLLBAR", FALSE }, + { WS_EX_LTRREADING, "WS_EX_LTRREADING", FALSE }, + { WS_EX_MDICHILD, "WS_EX_MDICHILD", FALSE }, + { WS_EX_NOACTIVATE, "WS_EX_NOACTIVATE", FALSE }, + { WS_EX_NOINHERITLAYOUT, "WS_EX_NOINHERITLAYOUT", FALSE }, + { WS_EX_NOPARENTNOTIFY, "WS_EX_NOPARENTNOTIFY", FALSE }, + { WS_EX_OVERLAPPEDWINDOW, "WS_EX_OVERLAPPEDWINDOW", TRUE }, + { WS_EX_PALETTEWINDOW, "WS_EX_PALETTEWINDOW", TRUE }, + { WS_EX_RIGHT, "WS_EX_RIGHT", FALSE }, + { WS_EX_RIGHTSCROLLBAR, "WS_EX_RIGHTSCROLLBAR", FALSE }, + { WS_EX_RTLREADING, "WS_EX_RTLREADING", FALSE }, + { WS_EX_STATICEDGE, "WS_EX_STATICEDGE", FALSE }, + { WS_EX_TOOLWINDOW, "WS_EX_TOOLWINDOW", FALSE }, + { WS_EX_TOPMOST, "WS_EX_TOPMOST", FALSE }, + { WS_EX_TRANSPARENT, "WS_EX_TRANSPARENT", FALSE }, + { WS_EX_WINDOWEDGE, "WS_EX_WINDOWEDGE", FALSE } +}; + +static void PrintWindowStyles(UINT32 style) +{ + int i; + WLog_INFO(TAG, "\tWindow Styles:\t{"); + + for (i = 0; i < ARRAYSIZE(WINDOW_STYLES); i++) + { + if (style & WINDOW_STYLES[i].style) + { + if (WINDOW_STYLES[i].multi) + { + if ((style & WINDOW_STYLES[i].style) != WINDOW_STYLES[i].style) + continue; + } + + WLog_INFO(TAG, "\t\t%s", WINDOW_STYLES[i].name); + } + } +} + +static void PrintExtendedWindowStyles(UINT32 style) +{ + int i; + WLog_INFO(TAG, "\tExtended Window Styles:\t{"); + + for (i = 0; i < ARRAYSIZE(EXTENDED_WINDOW_STYLES); i++) + { + if (style & EXTENDED_WINDOW_STYLES[i].style) + { + if (EXTENDED_WINDOW_STYLES[i].multi) + { + if ((style & EXTENDED_WINDOW_STYLES[i].style) != EXTENDED_WINDOW_STYLES[i].style) + continue; + } + + WLog_INFO(TAG, "\t\t%s", EXTENDED_WINDOW_STYLES[i].name); + } + } +} + +static void PrintRailWindowState(const WINDOW_ORDER_INFO* orderInfo, + const WINDOW_STATE_ORDER* windowState) +{ + if (orderInfo->fieldFlags & WINDOW_ORDER_STATE_NEW) + WLog_INFO(TAG, "WindowCreate: WindowId: 0x%08X", orderInfo->windowId); + else + WLog_INFO(TAG, "WindowUpdate: WindowId: 0x%08X", orderInfo->windowId); + + WLog_INFO(TAG, "{"); + + if (orderInfo->fieldFlags & WINDOW_ORDER_FIELD_OWNER) + { + WLog_INFO(TAG, "\tOwnerWindowId: 0x%08X", windowState->ownerWindowId); + } + + if (orderInfo->fieldFlags & WINDOW_ORDER_FIELD_STYLE) + { + WLog_INFO(TAG, "\tStyle: 0x%08X ExtendedStyle: 0x%08X", windowState->style, + windowState->extendedStyle); + PrintWindowStyles(windowState->style); + PrintExtendedWindowStyles(windowState->extendedStyle); + } + + if (orderInfo->fieldFlags & WINDOW_ORDER_FIELD_SHOW) + { + WLog_INFO(TAG, "\tShowState: %u", windowState->showState); + } + + if (orderInfo->fieldFlags & WINDOW_ORDER_FIELD_TITLE) + { + char* title = NULL; + ConvertFromUnicode(CP_UTF8, 0, (WCHAR*)windowState->titleInfo.string, + windowState->titleInfo.length / 2, &title, 0, NULL, NULL); + WLog_INFO(TAG, "\tTitleInfo: %s (length = %hu)", title, windowState->titleInfo.length); + free(title); + } + + if (orderInfo->fieldFlags & WINDOW_ORDER_FIELD_CLIENT_AREA_OFFSET) + { + WLog_INFO(TAG, "\tClientOffsetX: %d ClientOffsetY: %d", windowState->clientOffsetX, + windowState->clientOffsetY); + } + + if (orderInfo->fieldFlags & WINDOW_ORDER_FIELD_CLIENT_AREA_SIZE) + { + WLog_INFO(TAG, "\tClientAreaWidth: %u ClientAreaHeight: %u", windowState->clientAreaWidth, + windowState->clientAreaHeight); + } + + if (orderInfo->fieldFlags & WINDOW_ORDER_FIELD_RP_CONTENT) + { + WLog_INFO(TAG, "\tRPContent: %u", windowState->RPContent); + } + + if (orderInfo->fieldFlags & WINDOW_ORDER_FIELD_ROOT_PARENT) + { + WLog_INFO(TAG, "\tRootParentHandle: 0x%08X", windowState->rootParentHandle); + } + + if (orderInfo->fieldFlags & WINDOW_ORDER_FIELD_WND_OFFSET) + { + WLog_INFO(TAG, "\tWindowOffsetX: %d WindowOffsetY: %d", windowState->windowOffsetX, + windowState->windowOffsetY); + } + + if (orderInfo->fieldFlags & WINDOW_ORDER_FIELD_WND_CLIENT_DELTA) + { + WLog_INFO(TAG, "\tWindowClientDeltaX: %d WindowClientDeltaY: %d", + windowState->windowClientDeltaX, windowState->windowClientDeltaY); + } + + if (orderInfo->fieldFlags & WINDOW_ORDER_FIELD_WND_SIZE) + { + WLog_INFO(TAG, "\tWindowWidth: %u WindowHeight: %u", windowState->windowWidth, + windowState->windowHeight); + } + + if (orderInfo->fieldFlags & WINDOW_ORDER_FIELD_WND_RECTS) + { + UINT32 index; + RECTANGLE_16* rect; + WLog_INFO(TAG, "\tnumWindowRects: %u", windowState->numWindowRects); + + for (index = 0; index < windowState->numWindowRects; index++) + { + rect = &windowState->windowRects[index]; + WLog_INFO(TAG, "\twindowRect[%u]: left: %hu top: %hu right: %hu bottom: %hu", index, + rect->left, rect->top, rect->right, rect->bottom); + } + } + + if (orderInfo->fieldFlags & WINDOW_ORDER_FIELD_VIS_OFFSET) + { + WLog_INFO(TAG, "\tvisibileOffsetX: %d visibleOffsetY: %d", windowState->visibleOffsetX, + windowState->visibleOffsetY); + } + + if (orderInfo->fieldFlags & WINDOW_ORDER_FIELD_VISIBILITY) + { + UINT32 index; + RECTANGLE_16* rect; + WLog_INFO(TAG, "\tnumVisibilityRects: %u", windowState->numVisibilityRects); + + for (index = 0; index < windowState->numVisibilityRects; index++) + { + rect = &windowState->visibilityRects[index]; + WLog_INFO(TAG, "\tvisibilityRect[%u]: left: %hu top: %hu right: %hu bottom: %hu", index, + rect->left, rect->top, rect->right, rect->bottom); + } + } + + WLog_INFO(TAG, "}"); +} + +static void PrintRailIconInfo(const WINDOW_ORDER_INFO* orderInfo, const ICON_INFO* iconInfo) +{ + WLog_INFO(TAG, "ICON_INFO"); + WLog_INFO(TAG, "{"); + WLog_INFO(TAG, "\tbigIcon: %s", + (orderInfo->fieldFlags & WINDOW_ORDER_FIELD_ICON_BIG) ? "true" : "false"); + WLog_INFO(TAG, "\tcacheEntry; 0x%08X", iconInfo->cacheEntry); + WLog_INFO(TAG, "\tcacheId: 0x%08X", iconInfo->cacheId); + WLog_INFO(TAG, "\tbpp: %u", iconInfo->bpp); + WLog_INFO(TAG, "\twidth: %u", iconInfo->width); + WLog_INFO(TAG, "\theight: %u", iconInfo->height); + WLog_INFO(TAG, "\tcbColorTable: %u", iconInfo->cbColorTable); + WLog_INFO(TAG, "\tcbBitsMask: %u", iconInfo->cbBitsMask); + WLog_INFO(TAG, "\tcbBitsColor: %u", iconInfo->cbBitsColor); + WLog_INFO(TAG, "\tcolorTable: %p", (void*)iconInfo->colorTable); + WLog_INFO(TAG, "\tbitsMask: %p", (void*)iconInfo->bitsMask); + WLog_INFO(TAG, "\tbitsColor: %p", (void*)iconInfo->bitsColor); + WLog_INFO(TAG, "}"); +} + +LRESULT CALLBACK wf_RailWndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) +{ + HDC hDC; + int x, y; + int width; + int height; + UINT32 xPos; + UINT32 yPos; + PAINTSTRUCT ps; + UINT32 inputFlags; + wfContext* wfc = NULL; + rdpInput* input = NULL; + rdpContext* context = NULL; + wfRailWindow* railWindow; + railWindow = (wfRailWindow*)GetWindowLongPtr(hWnd, GWLP_USERDATA); + + if (railWindow) + wfc = railWindow->wfc; + + if (wfc) + context = (rdpContext*)wfc; + + if (context) + input = context->input; + + switch (msg) + { + case WM_PAINT: + { + if (!wfc) + return 0; + + hDC = BeginPaint(hWnd, &ps); + x = ps.rcPaint.left; + y = ps.rcPaint.top; + width = ps.rcPaint.right - ps.rcPaint.left + 1; + height = ps.rcPaint.bottom - ps.rcPaint.top + 1; + BitBlt(hDC, x, y, width, height, wfc->primary->hdc, railWindow->x + x, + railWindow->y + y, SRCCOPY); + EndPaint(hWnd, &ps); + } + break; + + case WM_LBUTTONDOWN: + { + if (!railWindow || !input) + return 0; + + xPos = GET_X_LPARAM(lParam) + railWindow->x; + yPos = GET_Y_LPARAM(lParam) + railWindow->y; + inputFlags = PTR_FLAGS_DOWN | PTR_FLAGS_BUTTON1; + + if (input) + input->MouseEvent(input, inputFlags, xPos, yPos); + } + break; + + case WM_LBUTTONUP: + { + if (!railWindow || !input) + return 0; + + xPos = GET_X_LPARAM(lParam) + railWindow->x; + yPos = GET_Y_LPARAM(lParam) + railWindow->y; + inputFlags = PTR_FLAGS_BUTTON1; + + if (input) + input->MouseEvent(input, inputFlags, xPos, yPos); + } + break; + + case WM_RBUTTONDOWN: + { + if (!railWindow || !input) + return 0; + + xPos = GET_X_LPARAM(lParam) + railWindow->x; + yPos = GET_Y_LPARAM(lParam) + railWindow->y; + inputFlags = PTR_FLAGS_DOWN | PTR_FLAGS_BUTTON2; + + if (input) + input->MouseEvent(input, inputFlags, xPos, yPos); + } + break; + + case WM_RBUTTONUP: + { + if (!railWindow || !input) + return 0; + + xPos = GET_X_LPARAM(lParam) + railWindow->x; + yPos = GET_Y_LPARAM(lParam) + railWindow->y; + inputFlags = PTR_FLAGS_BUTTON2; + + if (input) + input->MouseEvent(input, inputFlags, xPos, yPos); + } + break; + + case WM_MOUSEMOVE: + { + if (!railWindow || !input) + return 0; + + xPos = GET_X_LPARAM(lParam) + railWindow->x; + yPos = GET_Y_LPARAM(lParam) + railWindow->y; + inputFlags = PTR_FLAGS_MOVE; + + if (input) + input->MouseEvent(input, inputFlags, xPos, yPos); + } + break; + + case WM_MOUSEWHEEL: + break; + + case WM_CLOSE: + DestroyWindow(hWnd); + break; + + case WM_DESTROY: + PostQuitMessage(0); + break; + + default: + return DefWindowProc(hWnd, msg, wParam, lParam); + } + + return 0; +} + +#define RAIL_DISABLED_WINDOW_STYLES \ + (WS_BORDER | WS_THICKFRAME | WS_DLGFRAME | WS_CAPTION | WS_OVERLAPPED | WS_VSCROLL | \ + WS_HSCROLL | WS_SYSMENU | WS_MINIMIZEBOX | WS_MAXIMIZEBOX) +#define RAIL_DISABLED_EXTENDED_WINDOW_STYLES \ + (WS_EX_DLGMODALFRAME | WS_EX_CLIENTEDGE | WS_EX_STATICEDGE | WS_EX_WINDOWEDGE) + +static BOOL wf_rail_window_common(rdpContext* context, const WINDOW_ORDER_INFO* orderInfo, + const WINDOW_STATE_ORDER* windowState) +{ + wfRailWindow* railWindow = NULL; + wfContext* wfc = (wfContext*)context; + RailClientContext* rail = wfc->rail; + UINT32 fieldFlags = orderInfo->fieldFlags; + PrintRailWindowState(orderInfo, windowState); + + if (fieldFlags & WINDOW_ORDER_STATE_NEW) + { + HANDLE hInstance; + WCHAR* titleW = NULL; + WNDCLASSEX wndClassEx; + railWindow = (wfRailWindow*)calloc(1, sizeof(wfRailWindow)); + + if (!railWindow) + return FALSE; + + railWindow->wfc = wfc; + railWindow->dwStyle = windowState->style; + railWindow->dwStyle &= ~RAIL_DISABLED_WINDOW_STYLES; + railWindow->dwExStyle = windowState->extendedStyle; + railWindow->dwExStyle &= ~RAIL_DISABLED_EXTENDED_WINDOW_STYLES; + railWindow->x = windowState->windowOffsetX; + railWindow->y = windowState->windowOffsetY; + railWindow->width = windowState->windowWidth; + railWindow->height = windowState->windowHeight; + + if (fieldFlags & WINDOW_ORDER_FIELD_TITLE) + { + char* title = NULL; + + if (windowState->titleInfo.length == 0) + { + if (!(title = _strdup(""))) + { + WLog_ERR(TAG, "failed to duplicate empty window title string"); + /* error handled below */ + } + } + else if (ConvertFromUnicode(CP_UTF8, 0, (WCHAR*)windowState->titleInfo.string, + windowState->titleInfo.length / 2, &title, 0, NULL, + NULL) < 1) + { + WLog_ERR(TAG, "failed to convert window title"); + /* error handled below */ + } + + railWindow->title = title; + } + else + { + if (!(railWindow->title = _strdup("RdpRailWindow"))) + WLog_ERR(TAG, "failed to duplicate default window title string"); + } + + if (!railWindow->title) + { + free(railWindow); + return FALSE; + } + + ConvertToUnicode(CP_UTF8, 0, railWindow->title, -1, &titleW, 0); + hInstance = GetModuleHandle(NULL); + ZeroMemory(&wndClassEx, sizeof(WNDCLASSEX)); + wndClassEx.cbSize = sizeof(WNDCLASSEX); + wndClassEx.style = 0; + wndClassEx.lpfnWndProc = wf_RailWndProc; + wndClassEx.cbClsExtra = 0; + wndClassEx.cbWndExtra = 0; + wndClassEx.hIcon = NULL; + wndClassEx.hCursor = NULL; + wndClassEx.hbrBackground = NULL; + wndClassEx.lpszMenuName = NULL; + wndClassEx.lpszClassName = _T("RdpRailWindow"); + wndClassEx.hInstance = hInstance; + wndClassEx.hIconSm = NULL; + RegisterClassEx(&wndClassEx); + railWindow->hWnd = CreateWindowExW(railWindow->dwExStyle, /* dwExStyle */ + _T("RdpRailWindow"), /* lpClassName */ + titleW, /* lpWindowName */ + railWindow->dwStyle, /* dwStyle */ + railWindow->x, /* x */ + railWindow->y, /* y */ + railWindow->width, /* nWidth */ + railWindow->height, /* nHeight */ + NULL, /* hWndParent */ + NULL, /* hMenu */ + hInstance, /* hInstance */ + NULL /* lpParam */ + ); + + if (!railWindow->hWnd) + { + free(titleW); + free(railWindow->title); + free(railWindow); + WLog_ERR(TAG, "CreateWindowExW failed with error %" PRIu32 "", GetLastError()); + return FALSE; + } + + SetWindowLongPtr(railWindow->hWnd, GWLP_USERDATA, (LONG_PTR)railWindow); + HashTable_Add(wfc->railWindows, (void*)(UINT_PTR)orderInfo->windowId, (void*)railWindow); + free(titleW); + UpdateWindow(railWindow->hWnd); + return TRUE; + } + else + { + railWindow = (wfRailWindow*)HashTable_GetItemValue(wfc->railWindows, + (void*)(UINT_PTR)orderInfo->windowId); + } + + if (!railWindow) + return TRUE; + + if ((fieldFlags & WINDOW_ORDER_FIELD_WND_OFFSET) || (fieldFlags & WINDOW_ORDER_FIELD_WND_SIZE)) + { + if (fieldFlags & WINDOW_ORDER_FIELD_WND_OFFSET) + { + railWindow->x = windowState->windowOffsetX; + railWindow->y = windowState->windowOffsetY; + } + + if (fieldFlags & WINDOW_ORDER_FIELD_WND_SIZE) + { + railWindow->width = windowState->windowWidth; + railWindow->height = windowState->windowHeight; + } + + SetWindowPos(railWindow->hWnd, NULL, railWindow->x, railWindow->y, railWindow->width, + railWindow->height, 0); + } + + if (fieldFlags & WINDOW_ORDER_FIELD_OWNER) + { + } + + if (fieldFlags & WINDOW_ORDER_FIELD_STYLE) + { + railWindow->dwStyle = windowState->style; + railWindow->dwStyle &= ~RAIL_DISABLED_WINDOW_STYLES; + railWindow->dwExStyle = windowState->extendedStyle; + railWindow->dwExStyle &= ~RAIL_DISABLED_EXTENDED_WINDOW_STYLES; + SetWindowLongPtr(railWindow->hWnd, GWL_STYLE, (LONG)railWindow->dwStyle); + SetWindowLongPtr(railWindow->hWnd, GWL_EXSTYLE, (LONG)railWindow->dwExStyle); + } + + if (fieldFlags & WINDOW_ORDER_FIELD_SHOW) + { + ShowWindow(railWindow->hWnd, windowState->showState); + } + + if (fieldFlags & WINDOW_ORDER_FIELD_TITLE) + { + char* title = NULL; + WCHAR* titleW = NULL; + + if (windowState->titleInfo.length == 0) + { + if (!(title = _strdup(""))) + { + WLog_ERR(TAG, "failed to duplicate empty window title string"); + return FALSE; + } + } + else if (ConvertFromUnicode(CP_UTF8, 0, (WCHAR*)windowState->titleInfo.string, + windowState->titleInfo.length / 2, &title, 0, NULL, NULL) < 1) + { + WLog_ERR(TAG, "failed to convert window title"); + return FALSE; + } + + free(railWindow->title); + railWindow->title = title; + ConvertToUnicode(CP_UTF8, 0, railWindow->title, -1, &titleW, 0); + SetWindowTextW(railWindow->hWnd, titleW); + free(titleW); + } + + if (fieldFlags & WINDOW_ORDER_FIELD_CLIENT_AREA_OFFSET) + { + } + + if (fieldFlags & WINDOW_ORDER_FIELD_CLIENT_AREA_SIZE) + { + } + + if (fieldFlags & WINDOW_ORDER_FIELD_WND_CLIENT_DELTA) + { + } + + if (fieldFlags & WINDOW_ORDER_FIELD_RP_CONTENT) + { + } + + if (fieldFlags & WINDOW_ORDER_FIELD_ROOT_PARENT) + { + } + + if (fieldFlags & WINDOW_ORDER_FIELD_WND_RECTS) + { + UINT32 index; + HRGN hWndRect; + HRGN hWndRects; + RECTANGLE_16* rect; + + if (windowState->numWindowRects > 0) + { + rect = &(windowState->windowRects[0]); + hWndRects = CreateRectRgn(rect->left, rect->top, rect->right, rect->bottom); + + for (index = 1; index < windowState->numWindowRects; index++) + { + rect = &(windowState->windowRects[index]); + hWndRect = CreateRectRgn(rect->left, rect->top, rect->right, rect->bottom); + CombineRgn(hWndRects, hWndRects, hWndRect, RGN_OR); + DeleteObject(hWndRect); + } + + SetWindowRgn(railWindow->hWnd, hWndRects, TRUE); + DeleteObject(hWndRects); + } + } + + if (fieldFlags & WINDOW_ORDER_FIELD_VIS_OFFSET) + { + } + + if (fieldFlags & WINDOW_ORDER_FIELD_VISIBILITY) + { + } + + UpdateWindow(railWindow->hWnd); + return TRUE; +} + +static BOOL wf_rail_window_delete(rdpContext* context, const WINDOW_ORDER_INFO* orderInfo) +{ + wfRailWindow* railWindow = NULL; + wfContext* wfc = (wfContext*)context; + RailClientContext* rail = wfc->rail; + WLog_DBG(TAG, "RailWindowDelete"); + railWindow = (wfRailWindow*)HashTable_GetItemValue(wfc->railWindows, + (void*)(UINT_PTR)orderInfo->windowId); + + if (!railWindow) + return TRUE; + + HashTable_Remove(wfc->railWindows, (void*)(UINT_PTR)orderInfo->windowId); + DestroyWindow(railWindow->hWnd); + free(railWindow); + return TRUE; +} + +static BOOL wf_rail_window_icon(rdpContext* context, const WINDOW_ORDER_INFO* orderInfo, + const WINDOW_ICON_ORDER* windowIcon) +{ + HDC hDC; + int bpp; + int width; + int height; + HICON hIcon; + BOOL bigIcon; + ICONINFO iconInfo; + BITMAPINFO bitmapInfo; + wfRailWindow* railWindow; + BITMAPINFOHEADER* bitmapInfoHeader; + wfContext* wfc = (wfContext*)context; + RailClientContext* rail = wfc->rail; + WLog_DBG(TAG, "RailWindowIcon"); + PrintRailIconInfo(orderInfo, windowIcon->iconInfo); + railWindow = (wfRailWindow*)HashTable_GetItemValue(wfc->railWindows, + (void*)(UINT_PTR)orderInfo->windowId); + + if (!railWindow) + return TRUE; + + bigIcon = (orderInfo->fieldFlags & WINDOW_ORDER_FIELD_ICON_BIG) ? TRUE : FALSE; + hDC = GetDC(railWindow->hWnd); + iconInfo.fIcon = TRUE; + iconInfo.xHotspot = 0; + iconInfo.yHotspot = 0; + ZeroMemory(&bitmapInfo, sizeof(BITMAPINFO)); + bitmapInfoHeader = &(bitmapInfo.bmiHeader); + bpp = windowIcon->iconInfo->bpp; + width = windowIcon->iconInfo->width; + height = windowIcon->iconInfo->height; + bitmapInfoHeader->biSize = sizeof(BITMAPINFOHEADER); + bitmapInfoHeader->biWidth = width; + bitmapInfoHeader->biHeight = height; + bitmapInfoHeader->biPlanes = 1; + bitmapInfoHeader->biBitCount = bpp; + bitmapInfoHeader->biCompression = 0; + bitmapInfoHeader->biSizeImage = height * width * ((bpp + 7) / 8); + bitmapInfoHeader->biXPelsPerMeter = width; + bitmapInfoHeader->biYPelsPerMeter = height; + bitmapInfoHeader->biClrUsed = 0; + bitmapInfoHeader->biClrImportant = 0; + iconInfo.hbmMask = CreateDIBitmap(hDC, bitmapInfoHeader, CBM_INIT, + windowIcon->iconInfo->bitsMask, &bitmapInfo, DIB_RGB_COLORS); + iconInfo.hbmColor = + CreateDIBitmap(hDC, bitmapInfoHeader, CBM_INIT, windowIcon->iconInfo->bitsColor, + &bitmapInfo, DIB_RGB_COLORS); + hIcon = CreateIconIndirect(&iconInfo); + + if (hIcon) + { + WPARAM wParam; + LPARAM lParam; + wParam = (WPARAM)bigIcon ? ICON_BIG : ICON_SMALL; + lParam = (LPARAM)hIcon; + SendMessage(railWindow->hWnd, WM_SETICON, wParam, lParam); + } + + ReleaseDC(NULL, hDC); + + if (windowIcon->iconInfo->cacheEntry != 0xFFFF) + { + /* icon should be cached */ + } + + return TRUE; +} + +static BOOL wf_rail_window_cached_icon(rdpContext* context, const WINDOW_ORDER_INFO* orderInfo, + const WINDOW_CACHED_ICON_ORDER* windowCachedIcon) +{ + WLog_DBG(TAG, "RailWindowCachedIcon"); + return TRUE; +} + +static void wf_rail_notify_icon_common(rdpContext* context, const WINDOW_ORDER_INFO* orderInfo, + const NOTIFY_ICON_STATE_ORDER* notifyIconState) +{ + if (orderInfo->fieldFlags & WINDOW_ORDER_FIELD_NOTIFY_VERSION) + { + } + + if (orderInfo->fieldFlags & WINDOW_ORDER_FIELD_NOTIFY_TIP) + { + } + + if (orderInfo->fieldFlags & WINDOW_ORDER_FIELD_NOTIFY_INFO_TIP) + { + } + + if (orderInfo->fieldFlags & WINDOW_ORDER_FIELD_NOTIFY_STATE) + { + } + + if (orderInfo->fieldFlags & WINDOW_ORDER_ICON) + { + const ICON_INFO* iconInfo = &(notifyIconState->icon); + PrintRailIconInfo(orderInfo, iconInfo); + } + + if (orderInfo->fieldFlags & WINDOW_ORDER_CACHED_ICON) + { + } +} + +static BOOL wf_rail_notify_icon_create(rdpContext* context, const WINDOW_ORDER_INFO* orderInfo, + const NOTIFY_ICON_STATE_ORDER* notifyIconState) +{ + wfContext* wfc = (wfContext*)context; + RailClientContext* rail = wfc->rail; + WLog_DBG(TAG, "RailNotifyIconCreate"); + wf_rail_notify_icon_common(context, orderInfo, notifyIconState); + return TRUE; +} + +static BOOL wf_rail_notify_icon_update(rdpContext* context, const WINDOW_ORDER_INFO* orderInfo, + const NOTIFY_ICON_STATE_ORDER* notifyIconState) +{ + wfContext* wfc = (wfContext*)context; + RailClientContext* rail = wfc->rail; + WLog_DBG(TAG, "RailNotifyIconUpdate"); + wf_rail_notify_icon_common(context, orderInfo, notifyIconState); + return TRUE; +} + +static BOOL wf_rail_notify_icon_delete(rdpContext* context, const WINDOW_ORDER_INFO* orderInfo) +{ + wfContext* wfc = (wfContext*)context; + RailClientContext* rail = wfc->rail; + WLog_DBG(TAG, "RailNotifyIconDelete"); + return TRUE; +} + +static BOOL wf_rail_monitored_desktop(rdpContext* context, const WINDOW_ORDER_INFO* orderInfo, + const MONITORED_DESKTOP_ORDER* monitoredDesktop) +{ + wfContext* wfc = (wfContext*)context; + RailClientContext* rail = wfc->rail; + WLog_DBG(TAG, "RailMonitorDesktop"); + return TRUE; +} + +static BOOL wf_rail_non_monitored_desktop(rdpContext* context, const WINDOW_ORDER_INFO* orderInfo) +{ + wfContext* wfc = (wfContext*)context; + RailClientContext* rail = wfc->rail; + WLog_DBG(TAG, "RailNonMonitorDesktop"); + return TRUE; +} + +void wf_rail_register_update_callbacks(rdpUpdate* update) +{ + rdpWindowUpdate* window = update->window; + window->WindowCreate = wf_rail_window_common; + window->WindowUpdate = wf_rail_window_common; + window->WindowDelete = wf_rail_window_delete; + window->WindowIcon = wf_rail_window_icon; + window->WindowCachedIcon = wf_rail_window_cached_icon; + window->NotifyIconCreate = wf_rail_notify_icon_create; + window->NotifyIconUpdate = wf_rail_notify_icon_update; + window->NotifyIconDelete = wf_rail_notify_icon_delete; + window->MonitoredDesktop = wf_rail_monitored_desktop; + window->NonMonitoredDesktop = wf_rail_non_monitored_desktop; +} + +/* RemoteApp Virtual Channel Extension */ + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT wf_rail_server_execute_result(RailClientContext* context, + const RAIL_EXEC_RESULT_ORDER* execResult) +{ + WLog_DBG(TAG, "RailServerExecuteResult: 0x%08X", execResult->rawResult); + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT wf_rail_server_system_param(RailClientContext* context, + const RAIL_SYSPARAM_ORDER* sysparam) +{ + return CHANNEL_RC_OK; +} + +static UINT wf_rail_server_start_cmd(RailClientContext* context) +{ + UINT status; + RAIL_EXEC_ORDER exec = { 0 }; + RAIL_SYSPARAM_ORDER sysparam = { 0 }; + RAIL_CLIENT_STATUS_ORDER clientStatus = { 0 }; + wfContext* wfc = (wfContext*)context->custom; + rdpSettings* settings = wfc->context.settings; + clientStatus.flags = TS_RAIL_CLIENTSTATUS_ALLOWLOCALMOVESIZE; + + if (settings->AutoReconnectionEnabled) + clientStatus.flags |= TS_RAIL_CLIENTSTATUS_AUTORECONNECT; + + clientStatus.flags |= TS_RAIL_CLIENTSTATUS_ZORDER_SYNC; + clientStatus.flags |= TS_RAIL_CLIENTSTATUS_WINDOW_RESIZE_MARGIN_SUPPORTED; + clientStatus.flags |= TS_RAIL_CLIENTSTATUS_APPBAR_REMOTING_SUPPORTED; + clientStatus.flags |= TS_RAIL_CLIENTSTATUS_POWER_DISPLAY_REQUEST_SUPPORTED; + clientStatus.flags |= TS_RAIL_CLIENTSTATUS_BIDIRECTIONAL_CLOAK_SUPPORTED; + status = context->ClientInformation(context, &clientStatus); + + if (status != CHANNEL_RC_OK) + return status; + + if (settings->RemoteAppLanguageBarSupported) + { + RAIL_LANGBAR_INFO_ORDER langBarInfo; + langBarInfo.languageBarStatus = 0x00000008; /* TF_SFT_HIDDEN */ + status = context->ClientLanguageBarInfo(context, &langBarInfo); + + /* We want the language bar, but the server might not support it. */ + switch (status) + { + case CHANNEL_RC_OK: + case ERROR_BAD_CONFIGURATION: + break; + default: + return status; + } + } + + sysparam.params = 0; + sysparam.params |= SPI_MASK_SET_HIGH_CONTRAST; + sysparam.highContrast.colorScheme.string = NULL; + sysparam.highContrast.colorScheme.length = 0; + sysparam.highContrast.flags = 0x7E; + sysparam.params |= SPI_MASK_SET_MOUSE_BUTTON_SWAP; + sysparam.mouseButtonSwap = FALSE; + sysparam.params |= SPI_MASK_SET_KEYBOARD_PREF; + sysparam.keyboardPref = FALSE; + sysparam.params |= SPI_MASK_SET_DRAG_FULL_WINDOWS; + sysparam.dragFullWindows = FALSE; + sysparam.params |= SPI_MASK_SET_KEYBOARD_CUES; + sysparam.keyboardCues = FALSE; + sysparam.params |= SPI_MASK_SET_WORK_AREA; + sysparam.workArea.left = 0; + sysparam.workArea.top = 0; + sysparam.workArea.right = settings->DesktopWidth; + sysparam.workArea.bottom = settings->DesktopHeight; + sysparam.dragFullWindows = FALSE; + status = context->ClientSystemParam(context, &sysparam); + + if (status != CHANNEL_RC_OK) + return status; + + exec.RemoteApplicationProgram = settings->RemoteApplicationProgram; + exec.RemoteApplicationWorkingDir = settings->ShellWorkingDirectory; + exec.RemoteApplicationArguments = settings->RemoteApplicationCmdLine; + return context->ClientExecute(context, &exec); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT wf_rail_server_handshake(RailClientContext* context, + const RAIL_HANDSHAKE_ORDER* handshake) +{ + return wf_rail_server_start_cmd(context); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT wf_rail_server_handshake_ex(RailClientContext* context, + const RAIL_HANDSHAKE_EX_ORDER* handshakeEx) +{ + return wf_rail_server_start_cmd(context); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT wf_rail_server_local_move_size(RailClientContext* context, + const RAIL_LOCALMOVESIZE_ORDER* localMoveSize) +{ + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT wf_rail_server_min_max_info(RailClientContext* context, + const RAIL_MINMAXINFO_ORDER* minMaxInfo) +{ + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT wf_rail_server_language_bar_info(RailClientContext* context, + const RAIL_LANGBAR_INFO_ORDER* langBarInfo) +{ + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT wf_rail_server_get_appid_response(RailClientContext* context, + const RAIL_GET_APPID_RESP_ORDER* getAppIdResp) +{ + return CHANNEL_RC_OK; +} + +void wf_rail_invalidate_region(wfContext* wfc, REGION16* invalidRegion) +{ + int index; + int count; + RECT updateRect; + RECTANGLE_16 windowRect; + ULONG_PTR* pKeys = NULL; + wfRailWindow* railWindow; + const RECTANGLE_16* extents; + REGION16 windowInvalidRegion; + region16_init(&windowInvalidRegion); + count = HashTable_GetKeys(wfc->railWindows, &pKeys); + + for (index = 0; index < count; index++) + { + railWindow = (wfRailWindow*)HashTable_GetItemValue(wfc->railWindows, (void*)pKeys[index]); + + if (railWindow) + { + windowRect.left = railWindow->x; + windowRect.top = railWindow->y; + windowRect.right = railWindow->x + railWindow->width; + windowRect.bottom = railWindow->y + railWindow->height; + region16_clear(&windowInvalidRegion); + region16_intersect_rect(&windowInvalidRegion, invalidRegion, &windowRect); + + if (!region16_is_empty(&windowInvalidRegion)) + { + extents = region16_extents(&windowInvalidRegion); + updateRect.left = extents->left - railWindow->x; + updateRect.top = extents->top - railWindow->y; + updateRect.right = extents->right - railWindow->x; + updateRect.bottom = extents->bottom - railWindow->y; + InvalidateRect(railWindow->hWnd, &updateRect, FALSE); + } + } + } + + region16_uninit(&windowInvalidRegion); +} + +BOOL wf_rail_init(wfContext* wfc, RailClientContext* rail) +{ + rdpContext* context = (rdpContext*)wfc; + wfc->rail = rail; + rail->custom = (void*)wfc; + rail->ServerExecuteResult = wf_rail_server_execute_result; + rail->ServerSystemParam = wf_rail_server_system_param; + rail->ServerHandshake = wf_rail_server_handshake; + rail->ServerHandshakeEx = wf_rail_server_handshake_ex; + rail->ServerLocalMoveSize = wf_rail_server_local_move_size; + rail->ServerMinMaxInfo = wf_rail_server_min_max_info; + rail->ServerLanguageBarInfo = wf_rail_server_language_bar_info; + rail->ServerGetAppIdResponse = wf_rail_server_get_appid_response; + wf_rail_register_update_callbacks(context->update); + wfc->railWindows = HashTable_New(TRUE); + return (wfc->railWindows != NULL); +} + +void wf_rail_uninit(wfContext* wfc, RailClientContext* rail) +{ + wfc->rail = NULL; + rail->custom = NULL; + HashTable_Free(wfc->railWindows); +} diff --git a/client/Windows/wf_rail.h b/client/Windows/wf_rail.h new file mode 100644 index 0000000..2b73821 --- /dev/null +++ b/client/Windows/wf_rail.h @@ -0,0 +1,33 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * + * Copyright 2013-2014 Marc-Andre Moreau + * + * Licensed 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 FREERDP_CLIENT_WIN_RAIL_H +#define FREERDP_CLIENT_WIN_RAIL_H + +typedef struct wf_rail_window wfRailWindow; + +#include "wf_client.h" + +#include + +BOOL wf_rail_init(wfContext* wfc, RailClientContext* rail); +void wf_rail_uninit(wfContext* wfc, RailClientContext* rail); + +void wf_rail_invalidate_region(wfContext* wfc, REGION16* invalidRegion); + +#endif /* FREERDP_CLIENT_WIN_RAIL_H */ diff --git a/client/Windows/wfreerdp.rc b/client/Windows/wfreerdp.rc new file mode 100644 index 0000000000000000000000000000000000000000..e5bfc9c112914cbed8b6ec1ff5f3703e3e4d30c8 GIT binary patch literal 1670 zcmcJP(Q3jl7=_REVDBLHUhomt=s-5DSiKoSnXa&+uA{47?BUguxZtAsT`w|9NZX`6 zU%#aPVSkT1v{B##ORTZQ8*;P|p<@4ql6NYOV4oH4TtGL%30Iy+|ekm(~f-M^qOnVft@l zW5M&!KZr7>DyF+h|3j4Fm5yq1?nJ(4u9d>m1&>SDqT3M0)?BY8R$M!uDNEuvWQoF( zG%+;ej{PL9n$l-}Q!bBWE6l37JhyBe0oXRt>;M1& literal 0 HcmV?d00001 diff --git a/client/X11/.gitignore b/client/X11/.gitignore new file mode 100644 index 0000000..2f903d6 --- /dev/null +++ b/client/X11/.gitignore @@ -0,0 +1,2 @@ +xfreerdp-argument.1.xml +generate_argument_docbook diff --git a/client/X11/CMakeLists.txt b/client/X11/CMakeLists.txt new file mode 100644 index 0000000..869652c --- /dev/null +++ b/client/X11/CMakeLists.txt @@ -0,0 +1,249 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP X11 Client +# +# Copyright 2012 Marc-Andre Moreau +# Copyright 2013 Corey Clayton +# +# Licensed 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. + +set(MODULE_NAME "xfreerdp-client") +set(MODULE_PREFIX "FREERDP_CLIENT_X11_CONTROL") + +include(FindDocBookXSL) +include_directories(${X11_INCLUDE_DIRS}) +include_directories(${OPENSSL_INCLUDE_DIR}) + +set(${MODULE_PREFIX}_SRCS + xf_gdi.c + xf_gdi.h + xf_gfx.c + xf_gfx.h + xf_rail.c + xf_rail.h + xf_input.c + xf_input.h + xf_event.c + xf_event.h + xf_floatbar.c + xf_floatbar.h + xf_input.c + xf_input.h + xf_channels.c + xf_channels.h + xf_cliprdr.c + xf_cliprdr.h + xf_monitor.c + xf_monitor.h + xf_disp.c + xf_disp.h + xf_graphics.c + xf_graphics.h + xf_keyboard.c + xf_keyboard.h + xf_video.c + xf_video.h + xf_window.c + xf_window.h + xf_client.c + xf_client.h) + +if (CHANNEL_TSMF_CLIENT) + set(${MODULE_PREFIX}_SRCS ${${MODULE_PREFIX}_SRCS} + xf_tsmf.c + xf_tsmf.h) +endif() + +if(WITH_CLIENT_INTERFACE) + if(CLIENT_INTERFACE_SHARED) + add_library(${MODULE_NAME} SHARED ${${MODULE_PREFIX}_SRCS}) + if (WITH_LIBRARY_VERSIONING) + set_target_properties(${MODULE_NAME} PROPERTIES VERSION ${FREERDP_VERSION} SOVERSION ${FREERDP_API_VERSION}) + endif() + else() + add_library(${MODULE_NAME} ${${MODULE_PREFIX}_SRCS}) + endif() + +else() + set(${MODULE_PREFIX}_SRCS ${${MODULE_PREFIX}_SRCS} cli/xfreerdp.c xfreerdp.h) + add_executable(${MODULE_NAME} ${${MODULE_PREFIX}_SRCS}) + set_target_properties(${MODULE_NAME} PROPERTIES OUTPUT_NAME "xfreerdp") + include_directories(..) +endif() + +set(${MODULE_PREFIX}_LIBS + ${X11_LIBRARIES}) + +if(WITH_MANPAGES) + find_program(XSLTPROC_EXECUTABLE NAMES xsltproc) + + if(DOCBOOKXSL_FOUND AND XSLTPROC_EXECUTABLE) + + # We need the variable ${MAN_TODAY} to contain the current date in ISO + # format to replace it in the configure_file step. + include(today) + + TODAY(MAN_TODAY) + + configure_file(xfreerdp.1.xml.in xfreerdp.1.xml @ONLY IMMEDIATE) + + # Compile the helper tool with default compiler settings. + # We need the include paths though. + get_property(dirs DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTY INCLUDE_DIRECTORIES) + set(GENERATE_INCLUDES "") + foreach(dir ${dirs}) + set(GENERATE_INCLUDES ${GENERATE_INCLUDES} -I${dir}) + endforeach(dir) + + add_custom_command(OUTPUT xfreerdp.1 + COMMAND ${CMAKE_C_COMPILER} ${GENERATE_INCLUDES} + ${CMAKE_CURRENT_SOURCE_DIR}/generate_argument_docbook.c + -o ${CMAKE_CURRENT_BINARY_DIR}/generate_argument_docbook + COMMAND ${CMAKE_CURRENT_BINARY_DIR}/generate_argument_docbook + COMMAND ${CMAKE_COMMAND} -E copy + ${CMAKE_CURRENT_SOURCE_DIR}/xfreerdp-channels.1.xml ${CMAKE_CURRENT_BINARY_DIR} + COMMAND ${CMAKE_COMMAND} -E copy + ${CMAKE_CURRENT_SOURCE_DIR}/xfreerdp-examples.1.xml ${CMAKE_CURRENT_BINARY_DIR} + COMMAND ${CMAKE_COMMAND} -E copy + ${CMAKE_CURRENT_SOURCE_DIR}/xfreerdp-envvar.1.xml ${CMAKE_CURRENT_BINARY_DIR} + COMMAND ${XSLTPROC_EXECUTABLE} ${DOCBOOKXSL_DIR}/manpages/docbook.xsl xfreerdp.1.xml + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} + DEPENDS + ${CMAKE_CURRENT_BINARY_DIR}/xfreerdp.1.xml + ${CMAKE_CURRENT_SOURCE_DIR}/xfreerdp-examples.1.xml + ${CMAKE_CURRENT_SOURCE_DIR}/xfreerdp-channels.1.xml + ${CMAKE_CURRENT_SOURCE_DIR}/xfreerdp-envvar.1.xml) + + add_custom_target(xfreerdp.manpage ALL + DEPENDS xfreerdp.1) + + install_freerdp_man(${CMAKE_CURRENT_BINARY_DIR}/xfreerdp.1 1) + else() + message(WARNING "WITH_MANPAGES was set, but xsltproc was not found. man-pages will not be installed") + endif() +endif(WITH_MANPAGES) + +set(XSHM_FEATURE_TYPE "REQUIRED") +set(XSHM_FEATURE_PURPOSE "X11 shared memory") +set(XSHM_FEATURE_DESCRIPTION "X11 shared memory extension") + +set(XINERAMA_FEATURE_TYPE "RECOMMENDED") +set(XINERAMA_FEATURE_PURPOSE "multi-monitor") +set(XINERAMA_FEATURE_DESCRIPTION "X11 multi-monitor extension") + +set(XEXT_FEATURE_TYPE "RECOMMENDED") +set(XEXT_FEATURE_PURPOSE "X11 extension") +set(XEXT_FEATURE_DESCRIPTION "X11 core extensions") + +set(XCURSOR_FEATURE_TYPE "RECOMMENDED") +set(XCURSOR_FEATURE_PURPOSE "cursor") +set(XCURSOR_FEATURE_DESCRIPTION "X11 cursor extension") + +set(XV_FEATURE_TYPE "RECOMMENDED") +set(XV_FEATURE_PURPOSE "video") +set(XV_FEATURE_DESCRIPTION "X11 video extension") + +set(XI_FEATURE_TYPE "RECOMMENDED") +set(XI_FEATURE_PURPOSE "input") +set(XI_FEATURE_DESCRIPTION "X11 input extension") + +set(XRENDER_FEATURE_TYPE "RECOMMENDED") +set(XRENDER_FEATURE_PURPOSE "rendering") +set(XRENDER_FEATURE_DESCRIPTION "X11 render extension") + +set(XRANDR_FEATURE_TYPE "RECOMMENDED") +set(XRANDR_FEATURE_PURPOSE "tracking output configuration") +set(XRANDR_FEATURE_DESCRIPTION "X11 randr extension") + +set(XFIXES_FEATURE_TYPE "RECOMMENDED") +set(XFIXES_FEATURE_PURPOSE "X11 xfixes extension") +set(XFIXES_FEATURE_DESCRIPTION "Useful additions to the X11 core protocol") + +find_feature(XShm ${XSHM_FEATURE_TYPE} ${XSHM_FEATURE_PURPOSE} ${XSHM_FEATURE_DESCRIPTION}) +find_feature(Xinerama ${XINERAMA_FEATURE_TYPE} ${XINERAMA_FEATURE_PURPOSE} ${XINERAMA_FEATURE_DESCRIPTION}) +find_feature(Xext ${XEXT_FEATURE_TYPE} ${XEXT_FEATURE_PURPOSE} ${XEXT_FEATURE_DESCRIPTION}) +find_feature(Xcursor ${XCURSOR_FEATURE_TYPE} ${XCURSOR_FEATURE_PURPOSE} ${XCURSOR_FEATURE_DESCRIPTION}) +find_feature(Xv ${XV_FEATURE_TYPE} ${XV_FEATURE_PURPOSE} ${XV_FEATURE_DESCRIPTION}) +find_feature(Xi ${XI_FEATURE_TYPE} ${XI_FEATURE_PURPOSE} ${XI_FEATURE_DESCRIPTION}) +find_feature(Xrender ${XRENDER_FEATURE_TYPE} ${XRENDER_FEATURE_PURPOSE} ${XRENDER_FEATURE_DESCRIPTION}) +find_feature(XRandR ${XRANDR_FEATURE_TYPE} ${XRANDR_FEATURE_PURPOSE} ${XRANDR_FEATURE_DESCRIPTION}) +find_feature(Xfixes ${XFIXES_FEATURE_TYPE} ${XFIXES_FEATURE_PURPOSE} ${XFIXES_FEATURE_DESCRIPTION}) + +if(WITH_XINERAMA) + add_definitions(-DWITH_XINERAMA) + include_directories(${XINERAMA_INCLUDE_DIRS}) + set(${MODULE_PREFIX}_LIBS ${${MODULE_PREFIX}_LIBS} ${XINERAMA_LIBRARIES}) +endif() + +if(WITH_XEXT) + add_definitions(-DWITH_XEXT) + include_directories(${XEXT_INCLUDE_DIRS}) + set(${MODULE_PREFIX}_LIBS ${${MODULE_PREFIX}_LIBS} ${XEXT_LIBRARIES}) +endif() + +if(WITH_XCURSOR) + add_definitions(-DWITH_XCURSOR) + include_directories(${XCURSOR_INCLUDE_DIRS}) + set(${MODULE_PREFIX}_LIBS ${${MODULE_PREFIX}_LIBS} ${XCURSOR_LIBRARIES}) +endif() + +if(WITH_XV) + add_definitions(-DWITH_XV) + include_directories(${XV_INCLUDE_DIRS}) + set(${MODULE_PREFIX}_LIBS ${${MODULE_PREFIX}_LIBS} ${XV_LIBRARIES}) +endif() + +if(WITH_XI) + add_definitions(-DWITH_XI) + include_directories(${XI_INCLUDE_DIRS}) + set(${MODULE_PREFIX}_LIBS ${${MODULE_PREFIX}_LIBS} ${XI_LIBRARIES}) +endif() + +if(WITH_XRENDER) + add_definitions(-DWITH_XRENDER) + include_directories(${XRENDER_INCLUDE_DIRS}) + set(${MODULE_PREFIX}_LIBS ${${MODULE_PREFIX}_LIBS} ${XRENDER_LIBRARIES}) +endif() + +if(WITH_XRANDR) + add_definitions(-DWITH_XRANDR) + include_directories(${XRANDR_INCLUDE_DIRS}) + set(${MODULE_PREFIX}_LIBS ${${MODULE_PREFIX}_LIBS} ${XRANDR_LIBRARIES}) +endif() + +if(WITH_XFIXES) + add_definitions(-DWITH_XFIXES) + include_directories(${XFIXES_INCLUDE_DIRS}) + set(${MODULE_PREFIX}_LIBS ${${MODULE_PREFIX}_LIBS} ${XFIXES_LIBRARIES}) +endif() + +include_directories(${CMAKE_SOURCE_DIR}/resources) + +set(${MODULE_PREFIX}_LIBS ${${MODULE_PREFIX}_LIBS} freerdp-client freerdp m) +if (NOT APPLE) + list(APPEND ${MODULE_PREFIX}_LIBS rt) +endif() +target_link_libraries(${MODULE_NAME} ${PRIVATE_KEYWORD} ${${MODULE_PREFIX}_LIBS}) + +if(WITH_IPP) + target_link_libraries(${MODULE_NAME} ${PRIVATE_KEYWORD} ${IPP_LIBRARY_LIST}) +endif() + +if(WITH_CLIENT_INTERFACE) + install(TARGETS ${MODULE_NAME} DESTINATION ${CMAKE_INSTALL_LIBDIR} COMPONENT libraries) + add_subdirectory(cli) +else() + install(TARGETS ${MODULE_NAME} DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT client) +endif() + +set_property(TARGET ${MODULE_NAME} PROPERTY FOLDER "Client/X11") + diff --git a/client/X11/ModuleOptions.cmake b/client/X11/ModuleOptions.cmake new file mode 100644 index 0000000..4fef68a --- /dev/null +++ b/client/X11/ModuleOptions.cmake @@ -0,0 +1,4 @@ + +set(FREERDP_CLIENT_NAME "xfreerdp") +set(FREERDP_CLIENT_PLATFORM "X11") +set(FREERDP_CLIENT_VENDOR "FreeRDP") diff --git a/client/X11/cli/.gitignore b/client/X11/cli/.gitignore new file mode 100644 index 0000000..6ddebc6 --- /dev/null +++ b/client/X11/cli/.gitignore @@ -0,0 +1,2 @@ +xfreerdp + diff --git a/client/X11/cli/CMakeLists.txt b/client/X11/cli/CMakeLists.txt new file mode 100644 index 0000000..5f805c2 --- /dev/null +++ b/client/X11/cli/CMakeLists.txt @@ -0,0 +1,38 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP X11 cmake build script +# +# Copyright 2012 Marc-Andre Moreau +# +# Licensed 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. + +set(MODULE_NAME "xfreerdp-cli") +set(MODULE_PREFIX "FREERDP_CLIENT_X11") + +set(${MODULE_PREFIX}_SRCS + xfreerdp.c) + +add_executable(${MODULE_NAME} ${${MODULE_PREFIX}_SRCS}) +set_target_properties(${MODULE_NAME} PROPERTIES OUTPUT_NAME "xfreerdp" RUNTIME_OUTPUT_DIRECTORY "..") + +set(${MODULE_PREFIX}_LIBS ${${MODULE_PREFIX}_LIBS} xfreerdp-client freerdp-client) + +if(OPENBSD) + target_link_libraries(${MODULE_NAME} ${${MODULE_PREFIX}_LIBS} ossaudio) +else() + target_link_libraries(${MODULE_NAME} ${${MODULE_PREFIX}_LIBS}) +endif() + +install(TARGETS ${MODULE_NAME} RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT client) + +set_property(TARGET ${MODULE_NAME} PROPERTY FOLDER "Client/X11") + diff --git a/client/X11/cli/xfreerdp.c b/client/X11/cli/xfreerdp.c new file mode 100644 index 0000000..8db4d39 --- /dev/null +++ b/client/X11/cli/xfreerdp.c @@ -0,0 +1,85 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * X11 Client + * + * Copyright 2011 Marc-Andre Moreau + * Copyright 2012 HP Development Company, LLC + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include + +#include +#include + +#include "../xf_client.h" +#include "../xfreerdp.h" + +int main(int argc, char* argv[]) +{ + int rc = 1; + int status; + HANDLE thread; + xfContext* xfc; + DWORD dwExitCode; + rdpContext* context; + rdpSettings* settings; + RDP_CLIENT_ENTRY_POINTS clientEntryPoints; + + ZeroMemory(&clientEntryPoints, sizeof(RDP_CLIENT_ENTRY_POINTS)); + clientEntryPoints.Size = sizeof(RDP_CLIENT_ENTRY_POINTS); + clientEntryPoints.Version = RDP_CLIENT_INTERFACE_VERSION; + + RdpClientEntry(&clientEntryPoints); + + context = freerdp_client_context_new(&clientEntryPoints); + if (!context) + return 1; + + settings = context->settings; + xfc = (xfContext*)context; + + status = freerdp_client_settings_parse_command_line(context->settings, argc, argv, FALSE); + if (status) + { + rc = freerdp_client_settings_command_line_status_print(settings, status, argc, argv); + + if (settings->ListMonitors) + xf_list_monitors(xfc); + + goto out; + } + + if (freerdp_client_start(context) != 0) + goto out; + + thread = freerdp_client_get_thread(context); + + WaitForSingleObject(thread, INFINITE); + GetExitCodeThread(thread, &dwExitCode); + rc = xf_exit_code_from_disconnect_reason(dwExitCode); + + freerdp_client_stop(context); + +out: + freerdp_client_context_free(context); + + return rc; +} diff --git a/client/X11/generate_argument_docbook.c b/client/X11/generate_argument_docbook.c new file mode 100644 index 0000000..4fd07b8 --- /dev/null +++ b/client/X11/generate_argument_docbook.c @@ -0,0 +1,271 @@ +#include +#include +#include +#include + +#include "../common/cmdline.h" + +#define TAG FREERDP_TAG("generate_argument_docbook") +LPSTR tr_esc_str(LPCSTR arg, bool format) +{ + LPSTR tmp = NULL; + LPSTR tmp2 = NULL; + size_t cs = 0, x, ds, len; + size_t s; + + if (NULL == arg) + return NULL; + + s = strlen(arg); + + /* Find trailing whitespaces */ + while ((s > 0) && isspace(arg[s - 1])) + s--; + + /* Prepare a initial buffer with the size of the result string. */ + ds = s + 1; + + if (ds) + { + tmp2 = (LPSTR)realloc(tmp, ds * sizeof(CHAR)); + if (!tmp2) + free(tmp); + tmp = tmp2; + } + + if (NULL == tmp) + { + fprintf(stderr, "Could not allocate string buffer.\n"); + exit(-2); + } + + /* Copy character for character and check, if it is necessary to escape. */ + memset(tmp, 0, ds * sizeof(CHAR)); + + for (x = 0; x < s; x++) + { + switch (arg[x]) + { + case '<': + len = format ? 13 : 4; + ds += len - 1; + tmp2 = (LPSTR)realloc(tmp, ds * sizeof(CHAR)); + if (!tmp2) + free(tmp); + tmp = tmp2; + + if (NULL == tmp) + { + fprintf(stderr, "Could not reallocate string buffer.\n"); + exit(-3); + } + + if (format) + /* coverity[buffer_size] */ + strncpy(&tmp[cs], "", len); + else + /* coverity[buffer_size] */ + strncpy(&tmp[cs], "<", len); + + cs += len; + break; + + case '>': + len = format ? 14 : 4; + ds += len - 1; + tmp2 = (LPSTR)realloc(tmp, ds * sizeof(CHAR)); + if (!tmp2) + free(tmp); + tmp = tmp2; + + if (NULL == tmp) + { + fprintf(stderr, "Could not reallocate string buffer.\n"); + exit(-4); + } + + if (format) + /* coverity[buffer_size] */ + strncpy(&tmp[cs], "", len); + else + /* coverity[buffer_size] */ + strncpy(&tmp[cs], ">", len); + + cs += len; + break; + + case '\'': + ds += 5; + tmp2 = (LPSTR)realloc(tmp, ds * sizeof(CHAR)); + if (!tmp2) + free(tmp); + tmp = tmp2; + + if (NULL == tmp) + { + fprintf(stderr, "Could not reallocate string buffer.\n"); + exit(-5); + } + + tmp[cs++] = '&'; + tmp[cs++] = 'a'; + tmp[cs++] = 'p'; + tmp[cs++] = 'o'; + tmp[cs++] = 's'; + tmp[cs++] = ';'; + break; + + case '"': + ds += 5; + tmp2 = (LPSTR)realloc(tmp, ds * sizeof(CHAR)); + if (!tmp2) + free(tmp); + tmp = tmp2; + + if (NULL == tmp) + { + fprintf(stderr, "Could not reallocate string buffer.\n"); + exit(-6); + } + + tmp[cs++] = '&'; + tmp[cs++] = 'q'; + tmp[cs++] = 'u'; + tmp[cs++] = 'o'; + tmp[cs++] = 't'; + tmp[cs++] = ';'; + break; + + case '&': + ds += 4; + tmp2 = (LPSTR)realloc(tmp, ds * sizeof(CHAR)); + if (!tmp2) + free(tmp); + tmp = tmp2; + + if (NULL == tmp) + { + fprintf(stderr, "Could not reallocate string buffer.\n"); + exit(-7); + } + + tmp[cs++] = '&'; + tmp[cs++] = 'a'; + tmp[cs++] = 'm'; + tmp[cs++] = 'p'; + tmp[cs++] = ';'; + break; + + default: + tmp[cs++] = arg[x]; + break; + } + + /* Assure, the string is '\0' terminated. */ + tmp[ds - 1] = '\0'; + } + + return tmp; +} + +int main(int argc, char* argv[]) +{ + size_t elements = sizeof(args) / sizeof(args[0]); + size_t x; + const char* fname = "xfreerdp-argument.1.xml"; + FILE* fp = NULL; + /* Open output file for writing, truncate if existing. */ + fp = fopen(fname, "w"); + + if (NULL == fp) + { + fprintf(stderr, "Could not open '%s' for writing.\n", fname); + return -1; + } + + /* The tag used as header in the manpage */ + fprintf(fp, "\n"); + fprintf(fp, "\tOptions\n"); + fprintf(fp, "\t\t\n"); + + /* Iterate over argument struct and write data to docbook 4.5 + * compatible XML */ + if (elements < 2) + { + fprintf(stderr, "The argument array 'args' is empty, writing an empty file.\n"); + elements = 1; + } + + for (x = 0; x < elements - 1; x++) + { + const COMMAND_LINE_ARGUMENT_A* arg = &args[x]; + char* name = tr_esc_str((LPSTR)arg->Name, FALSE); + char* alias = tr_esc_str((LPSTR)arg->Alias, FALSE); + char* format = tr_esc_str(arg->Format, TRUE); + char* text = tr_esc_str((LPSTR)arg->Text, FALSE); + fprintf(fp, "\t\t\t\n"); + + do + { + fprintf(fp, "\t\t\t\t", name); + + if (format) + { + if (arg->Flags == COMMAND_LINE_VALUE_OPTIONAL) + fprintf(fp, "["); + + fprintf(fp, ":%s", format); + + if (arg->Flags == COMMAND_LINE_VALUE_OPTIONAL) + fprintf(fp, "]"); + } + + fprintf(fp, "\n"); + + if (alias == name) + break; + + free(name); + name = alias; + } while (alias); + + if (text) + { + fprintf(fp, "\t\t\t\t\n"); + fprintf(fp, "\t\t\t\t\t"); + + if (text) + fprintf(fp, "%s", text); + + if (arg->Flags & COMMAND_LINE_VALUE_BOOL && + (!arg->Default || arg->Default == BoolValueTrue)) + fprintf(fp, " (default:%s)", arg->Default ? "on" : "off"); + else if (arg->Default) + { + char* value = tr_esc_str((LPSTR)arg->Default, FALSE); + fprintf(fp, " (default:%s)", value); + free(value); + } + + fprintf(fp, "\n"); + fprintf(fp, "\t\t\t\t\n"); + } + + fprintf(fp, "\t\t\t\n"); + free(name); + free(format); + free(text); + } + + fprintf(fp, "\t\t\n"); + fprintf(fp, "\t\n"); + fclose(fp); + return 0; +} diff --git a/client/X11/resource/close.xbm b/client/X11/resource/close.xbm new file mode 100644 index 0000000..45c60e3 --- /dev/null +++ b/client/X11/resource/close.xbm @@ -0,0 +1,11 @@ +#define close_width 24 +#define close_height 24 +static unsigned char close_bits[] = +{ + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0x7c, 0xfe, 0xff, 0x38, 0xfe, 0xff, 0x11, 0xff, 0xff, 0x83, 0xff, + 0xff, 0xc7, 0xff, 0xff, 0x83, 0xff, 0xff, 0x11, 0xff, 0xff, 0x38, 0xfe, + 0xff, 0x7c, 0xfe, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff +}; diff --git a/client/X11/resource/lock.xbm b/client/X11/resource/lock.xbm new file mode 100644 index 0000000..12340f5 --- /dev/null +++ b/client/X11/resource/lock.xbm @@ -0,0 +1,11 @@ +#define lock_width 24 +#define lock_height 24 +static unsigned char lock_bits[] = +{ + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x83, 0xff, + 0xff, 0x83, 0xff, 0xff, 0xc7, 0xff, 0xff, 0xc7, 0xff, 0xff, 0xc7, 0xff, + 0xff, 0x00, 0xfe, 0xff, 0x00, 0xfe, 0xff, 0xef, 0xff, 0xff, 0xef, 0xff, + 0xff, 0xef, 0xff, 0xff, 0xef, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff +}; diff --git a/client/X11/resource/minimize.xbm b/client/X11/resource/minimize.xbm new file mode 100644 index 0000000..c69d861 --- /dev/null +++ b/client/X11/resource/minimize.xbm @@ -0,0 +1,11 @@ +#define minimize_width 24 +#define minimize_height 24 +static unsigned char minimize_bits[] = +{ + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, 0x00, 0xfc, + 0x3f, 0x00, 0xfc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff +}; diff --git a/client/X11/resource/restore.xbm b/client/X11/resource/restore.xbm new file mode 100644 index 0000000..e9909f5 --- /dev/null +++ b/client/X11/resource/restore.xbm @@ -0,0 +1,11 @@ +#define restore_width 24 +#define restore_height 24 +static unsigned char restore_bits[] = +{ + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0x03, 0xff, 0xff, 0x03, 0xff, 0xff, 0x3b, 0xff, 0x7f, 0x20, 0xff, + 0x7f, 0x20, 0xff, 0x7f, 0x07, 0xff, 0x7f, 0xe7, 0xff, 0x7f, 0xe7, 0xff, + 0x7f, 0xe0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff +}; diff --git a/client/X11/resource/unlock.xbm b/client/X11/resource/unlock.xbm new file mode 100644 index 0000000..a809126 --- /dev/null +++ b/client/X11/resource/unlock.xbm @@ -0,0 +1,11 @@ +#define unlock_width 24 +#define unlock_height 24 +static unsigned char unlock_bits[] = +{ + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xf3, 0xff, 0xff, 0xf3, 0xff, 0xff, 0x73, 0xfe, 0xff, 0x03, 0xfe, + 0x3f, 0x00, 0xfe, 0xff, 0x03, 0xfe, 0xff, 0x73, 0xfe, 0xff, 0xf3, 0xff, + 0xff, 0xf3, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff +}; diff --git a/client/X11/xf_channels.c b/client/X11/xf_channels.c new file mode 100644 index 0000000..8090b9b --- /dev/null +++ b/client/X11/xf_channels.c @@ -0,0 +1,137 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * X11 Client Channels + * + * Copyright 2013 Marc-Andre Moreau + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include "xf_channels.h" + +#include "xf_client.h" +#include "xfreerdp.h" + +#include "xf_gfx.h" +#if defined(CHANNEL_TSMF_CLIENT) +#include "xf_tsmf.h" +#endif +#include "xf_rail.h" +#include "xf_cliprdr.h" +#include "xf_disp.h" +#include "xf_video.h" + +void xf_OnChannelConnectedEventHandler(void* context, ChannelConnectedEventArgs* e) +{ + xfContext* xfc = (xfContext*)context; + + if (strcmp(e->name, RDPEI_DVC_CHANNEL_NAME) == 0) + { + xfc->rdpei = (RdpeiClientContext*)e->pInterface; + } +#if defined(CHANNEL_TSMF_CLIENT) + else if (strcmp(e->name, TSMF_DVC_CHANNEL_NAME) == 0) + { + xf_tsmf_init(xfc, (TsmfClientContext*)e->pInterface); + } +#endif + else if (strcmp(e->name, RDPGFX_DVC_CHANNEL_NAME) == 0) + { + xf_graphics_pipeline_init(xfc, (RdpgfxClientContext*)e->pInterface); + } + else if (strcmp(e->name, RAIL_SVC_CHANNEL_NAME) == 0) + { + xf_rail_init(xfc, (RailClientContext*)e->pInterface); + } + else if (strcmp(e->name, CLIPRDR_SVC_CHANNEL_NAME) == 0) + { + xf_cliprdr_init(xfc, (CliprdrClientContext*)e->pInterface); + } + else if (strcmp(e->name, ENCOMSP_SVC_CHANNEL_NAME) == 0) + { + xf_encomsp_init(xfc, (EncomspClientContext*)e->pInterface); + } + else if (strcmp(e->name, DISP_DVC_CHANNEL_NAME) == 0) + { + xf_disp_init(xfc->xfDisp, (DispClientContext*)e->pInterface); + } + else if (strcmp(e->name, GEOMETRY_DVC_CHANNEL_NAME) == 0) + { + gdi_video_geometry_init(xfc->context.gdi, (GeometryClientContext*)e->pInterface); + } + else if (strcmp(e->name, VIDEO_CONTROL_DVC_CHANNEL_NAME) == 0) + { + xf_video_control_init(xfc, (VideoClientContext*)e->pInterface); + } + else if (strcmp(e->name, VIDEO_DATA_DVC_CHANNEL_NAME) == 0) + { + gdi_video_data_init(xfc->context.gdi, (VideoClientContext*)e->pInterface); + } +} + +void xf_OnChannelDisconnectedEventHandler(void* context, ChannelDisconnectedEventArgs* e) +{ + xfContext* xfc = (xfContext*)context; + rdpSettings* settings = xfc->context.settings; + + if (strcmp(e->name, RDPEI_DVC_CHANNEL_NAME) == 0) + { + xfc->rdpei = NULL; + } + else if (strcmp(e->name, DISP_DVC_CHANNEL_NAME) == 0) + { + xf_disp_uninit(xfc->xfDisp, (DispClientContext*)e->pInterface); + } +#if defined(CHANNEL_TSMF_CLIENT) + else if (strcmp(e->name, TSMF_DVC_CHANNEL_NAME) == 0) + { + xf_tsmf_uninit(xfc, (TsmfClientContext*)e->pInterface); + } +#endif + else if (strcmp(e->name, RDPGFX_DVC_CHANNEL_NAME) == 0) + { + xf_graphics_pipeline_uninit(xfc, (RdpgfxClientContext*)e->pInterface); + } + else if (strcmp(e->name, RAIL_SVC_CHANNEL_NAME) == 0) + { + xf_rail_uninit(xfc, (RailClientContext*)e->pInterface); + } + else if (strcmp(e->name, CLIPRDR_SVC_CHANNEL_NAME) == 0) + { + xf_cliprdr_uninit(xfc, (CliprdrClientContext*)e->pInterface); + } + else if (strcmp(e->name, ENCOMSP_SVC_CHANNEL_NAME) == 0) + { + xf_encomsp_uninit(xfc, (EncomspClientContext*)e->pInterface); + } + else if (strcmp(e->name, GEOMETRY_DVC_CHANNEL_NAME) == 0) + { + gdi_video_geometry_uninit(xfc->context.gdi, (GeometryClientContext*)e->pInterface); + } + else if (strcmp(e->name, VIDEO_CONTROL_DVC_CHANNEL_NAME) == 0) + { + if (settings->SoftwareGdi) + gdi_video_control_uninit(xfc->context.gdi, (VideoClientContext*)e->pInterface); + else + xf_video_control_uninit(xfc, (VideoClientContext*)e->pInterface); + } + else if (strcmp(e->name, VIDEO_DATA_DVC_CHANNEL_NAME) == 0) + { + gdi_video_data_uninit(xfc->context.gdi, (VideoClientContext*)e->pInterface); + } +} diff --git a/client/X11/xf_channels.h b/client/X11/xf_channels.h new file mode 100644 index 0000000..c12d823 --- /dev/null +++ b/client/X11/xf_channels.h @@ -0,0 +1,37 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * X11 Client Channels + * + * Copyright 2013 Marc-Andre Moreau + * + * Licensed 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 FREERDP_CLIENT_X11_CHANNELS_H +#define FREERDP_CLIENT_X11_CHANNELS_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +void xf_OnChannelConnectedEventHandler(void* context, ChannelConnectedEventArgs* e); +void xf_OnChannelDisconnectedEventHandler(void* context, ChannelDisconnectedEventArgs* e); + +#endif /* FREERDP_CLIENT_X11_CHANNELS_H */ diff --git a/client/X11/xf_client.c b/client/X11/xf_client.c new file mode 100644 index 0000000..bd3eb0d --- /dev/null +++ b/client/X11/xf_client.c @@ -0,0 +1,2096 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * X11 Client Interface + * + * Copyright 2013 Marc-Andre Moreau + * Copyright 2013 Corey Clayton + * Copyright 2014 Thincast Technologies GmbH + * Copyright 2014 Norbert Federa + * Copyright 2016 Armin Novak + * Copyright 2016 Thincast Technologies GmbH + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include + +#include +#include +#include + +#ifdef WITH_XRENDER +#include +#include +#endif + +#ifdef WITH_XI +#include +#include +#endif + +#ifdef WITH_XCURSOR +#include +#endif + +#ifdef WITH_XINERAMA +#include +#endif + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "xf_gdi.h" +#include "xf_rail.h" +#if defined(CHANNEL_TSMF_CLIENT) +#include "xf_tsmf.h" +#endif +#include "xf_event.h" +#include "xf_input.h" +#include "xf_cliprdr.h" +#include "xf_disp.h" +#include "xf_video.h" +#include "xf_monitor.h" +#include "xf_graphics.h" +#include "xf_keyboard.h" +#include "xf_input.h" +#include "xf_channels.h" +#include "xfreerdp.h" + +#include +#define TAG CLIENT_TAG("x11") + +#define MIN_PIXEL_DIFF 0.001 + +static int (*_def_error_handler)(Display*, XErrorEvent*); +static int _xf_error_handler(Display* d, XErrorEvent* ev); +static void xf_check_extensions(xfContext* context); +static void xf_window_free(xfContext* xfc); +static BOOL xf_get_pixmap_info(xfContext* xfc); + +#ifdef WITH_XRENDER +static void xf_draw_screen_scaled(xfContext* xfc, int x, int y, int w, int h) +{ + XTransform transform; + Picture windowPicture; + Picture primaryPicture; + XRenderPictureAttributes pa; + XRenderPictFormat* picFormat; + double xScalingFactor; + double yScalingFactor; + int x2; + int y2; + const char* filter; + rdpSettings* settings = xfc->context.settings; + + if (xfc->scaledWidth <= 0 || xfc->scaledHeight <= 0) + { + WLog_ERR(TAG, "the current window dimensions are invalid"); + return; + } + + if (settings->DesktopWidth <= 0 || settings->DesktopHeight <= 0) + { + WLog_ERR(TAG, "the window dimensions are invalid"); + return; + } + + xScalingFactor = settings->DesktopWidth / (double)xfc->scaledWidth; + yScalingFactor = settings->DesktopHeight / (double)xfc->scaledHeight; + XSetFillStyle(xfc->display, xfc->gc, FillSolid); + XSetForeground(xfc->display, xfc->gc, 0); + /* Black out possible space between desktop and window borders */ + { + XRectangle box1 = { 0, 0, xfc->window->width, xfc->window->height }; + XRectangle box2 = { xfc->offset_x, xfc->offset_y, xfc->scaledWidth, xfc->scaledHeight }; + Region reg1 = XCreateRegion(); + Region reg2 = XCreateRegion(); + XUnionRectWithRegion(&box1, reg1, reg1); + XUnionRectWithRegion(&box2, reg2, reg2); + + if (XSubtractRegion(reg1, reg2, reg1) && !XEmptyRegion(reg1)) + { + XSetRegion(xfc->display, xfc->gc, reg1); + XFillRectangle(xfc->display, xfc->window->handle, xfc->gc, 0, 0, xfc->window->width, + xfc->window->height); + XSetClipMask(xfc->display, xfc->gc, None); + } + + XDestroyRegion(reg1); + XDestroyRegion(reg2); + } + picFormat = XRenderFindVisualFormat(xfc->display, xfc->visual); + pa.subwindow_mode = IncludeInferiors; + primaryPicture = + XRenderCreatePicture(xfc->display, xfc->primary, picFormat, CPSubwindowMode, &pa); + windowPicture = + XRenderCreatePicture(xfc->display, xfc->window->handle, picFormat, CPSubwindowMode, &pa); + /* avoid blurry filter when scaling factor is 2x, 3x, etc + * useful when the client has high-dpi monitor */ + filter = FilterBilinear; + if (fabs(xScalingFactor - yScalingFactor) < MIN_PIXEL_DIFF) + { + const double inverseX = 1.0 / xScalingFactor; + const double inverseRoundedX = round(inverseX); + const double absInverse = fabs(inverseX - inverseRoundedX); + + if (absInverse < MIN_PIXEL_DIFF) + filter = FilterNearest; + } + XRenderSetPictureFilter(xfc->display, primaryPicture, filter, 0, 0); + transform.matrix[0][0] = XDoubleToFixed(xScalingFactor); + transform.matrix[0][1] = XDoubleToFixed(0.0); + transform.matrix[0][2] = XDoubleToFixed(0.0); + transform.matrix[1][0] = XDoubleToFixed(0.0); + transform.matrix[1][1] = XDoubleToFixed(yScalingFactor); + transform.matrix[1][2] = XDoubleToFixed(0.0); + transform.matrix[2][0] = XDoubleToFixed(0.0); + transform.matrix[2][1] = XDoubleToFixed(0.0); + transform.matrix[2][2] = XDoubleToFixed(1.0); + /* calculate and fix up scaled coordinates */ + x2 = x + w; + y2 = y + h; + x = floor(x / xScalingFactor) - 1; + y = floor(y / yScalingFactor) - 1; + w = ceil(x2 / xScalingFactor) + 1 - x; + h = ceil(y2 / yScalingFactor) + 1 - y; + XRenderSetPictureTransform(xfc->display, primaryPicture, &transform); + XRenderComposite(xfc->display, PictOpSrc, primaryPicture, 0, windowPicture, x, y, 0, 0, + xfc->offset_x + x, xfc->offset_y + y, w, h); + XRenderFreePicture(xfc->display, primaryPicture); + XRenderFreePicture(xfc->display, windowPicture); +} + +BOOL xf_picture_transform_required(xfContext* xfc) +{ + rdpSettings* settings = xfc->context.settings; + + if ((xfc->offset_x != 0) || (xfc->offset_y != 0) || + (xfc->scaledWidth != (INT64)settings->DesktopWidth) || + (xfc->scaledHeight != (INT64)settings->DesktopHeight)) + { + return TRUE; + } + + return FALSE; +} +#endif /* WITH_XRENDER defined */ + +void xf_draw_screen_(xfContext* xfc, int x, int y, int w, int h, const char* fkt, const char* file, + int line) +{ + if (!xfc) + { + WLog_DBG(TAG, "[%s] called from [%s] xfc=%p", __FUNCTION__, fkt, xfc); + return; + } + + if (w == 0 || h == 0) + { + WLog_WARN(TAG, "invalid width and/or height specified: w=%d h=%d", w, h); + return; + } + +#ifdef WITH_XRENDER + + if (xf_picture_transform_required(xfc)) + { + xf_draw_screen_scaled(xfc, x, y, w, h); + return; + } + +#endif + XCopyArea(xfc->display, xfc->primary, xfc->window->handle, xfc->gc, x, y, w, h, x, y); +} + +static BOOL xf_desktop_resize(rdpContext* context) +{ + rdpSettings* settings; + xfContext* xfc = (xfContext*)context; + settings = context->settings; + + if (xfc->primary) + { + BOOL same = (xfc->primary == xfc->drawing) ? TRUE : FALSE; + XFreePixmap(xfc->display, xfc->primary); + + if (!(xfc->primary = XCreatePixmap(xfc->display, xfc->drawable, settings->DesktopWidth, + settings->DesktopHeight, xfc->depth))) + return FALSE; + + if (same) + xfc->drawing = xfc->primary; + } + +#ifdef WITH_XRENDER + + if (!xfc->context.settings->SmartSizing) + { + xfc->scaledWidth = settings->DesktopWidth; + xfc->scaledHeight = settings->DesktopHeight; + } + +#endif + + if (!xfc->fullscreen) + { + xf_ResizeDesktopWindow(xfc, xfc->window, settings->DesktopWidth, settings->DesktopHeight); + } + else + { +#ifdef WITH_XRENDER + + if (!xfc->context.settings->SmartSizing) +#endif + { + /* Update the saved width and height values the window will be + * resized to when toggling out of fullscreen */ + xfc->savedWidth = settings->DesktopWidth; + xfc->savedHeight = settings->DesktopHeight; + } + + XSetFunction(xfc->display, xfc->gc, GXcopy); + XSetFillStyle(xfc->display, xfc->gc, FillSolid); + XSetForeground(xfc->display, xfc->gc, 0); + XFillRectangle(xfc->display, xfc->drawable, xfc->gc, 0, 0, settings->DesktopWidth, + settings->DesktopHeight); + } + + return TRUE; +} + +static BOOL xf_sw_end_paint(rdpContext* context) +{ + int i; + INT32 x, y; + UINT32 w, h; + int ninvalid; + HGDI_RGN cinvalid; + xfContext* xfc = (xfContext*)context; + rdpGdi* gdi = context->gdi; + + if (gdi->suppressOutput) + return TRUE; + + x = gdi->primary->hdc->hwnd->invalid->x; + y = gdi->primary->hdc->hwnd->invalid->y; + w = gdi->primary->hdc->hwnd->invalid->w; + h = gdi->primary->hdc->hwnd->invalid->h; + ninvalid = gdi->primary->hdc->hwnd->ninvalid; + cinvalid = gdi->primary->hdc->hwnd->cinvalid; + + if (!xfc->remote_app) + { + if (!xfc->complex_regions) + { + if (gdi->primary->hdc->hwnd->invalid->null) + return TRUE; + + xf_lock_x11(xfc); + XPutImage(xfc->display, xfc->primary, xfc->gc, xfc->image, x, y, x, y, w, h); + xf_draw_screen(xfc, x, y, w, h); + xf_unlock_x11(xfc); + } + else + { + if (gdi->primary->hdc->hwnd->ninvalid < 1) + return TRUE; + + xf_lock_x11(xfc); + + for (i = 0; i < ninvalid; i++) + { + x = cinvalid[i].x; + y = cinvalid[i].y; + w = cinvalid[i].w; + h = cinvalid[i].h; + XPutImage(xfc->display, xfc->primary, xfc->gc, xfc->image, x, y, x, y, w, h); + xf_draw_screen(xfc, x, y, w, h); + } + + XFlush(xfc->display); + xf_unlock_x11(xfc); + } + } + else + { + if (gdi->primary->hdc->hwnd->invalid->null) + return TRUE; + + xf_lock_x11(xfc); + xf_rail_paint(xfc, x, y, x + w, y + h); + xf_unlock_x11(xfc); + } + + gdi->primary->hdc->hwnd->invalid->null = TRUE; + gdi->primary->hdc->hwnd->ninvalid = 0; + return TRUE; +} + +static BOOL xf_sw_desktop_resize(rdpContext* context) +{ + rdpGdi* gdi = context->gdi; + xfContext* xfc = (xfContext*)context; + rdpSettings* settings = context->settings; + BOOL ret = FALSE; + xf_lock_x11(xfc); + + if (!gdi_resize(gdi, settings->DesktopWidth, settings->DesktopHeight)) + goto out; + + if (xfc->image) + { + xfc->image->data = NULL; + XDestroyImage(xfc->image); + } + + if (!(xfc->image = XCreateImage(xfc->display, xfc->visual, xfc->depth, ZPixmap, 0, + (char*)gdi->primary_buffer, gdi->width, gdi->height, + xfc->scanline_pad, gdi->stride))) + { + goto out; + } + + xfc->image->byte_order = LSBFirst; + xfc->image->bitmap_bit_order = LSBFirst; + ret = xf_desktop_resize(context); +out: + xf_unlock_x11(xfc); + return ret; +} + +static BOOL xf_hw_end_paint(rdpContext* context) +{ + INT32 x, y; + UINT32 w, h; + xfContext* xfc = (xfContext*)context; + + if (xfc->context.gdi->suppressOutput) + return TRUE; + + if (!xfc->remote_app) + { + if (!xfc->complex_regions) + { + if (xfc->hdc->hwnd->invalid->null) + return TRUE; + + x = xfc->hdc->hwnd->invalid->x; + y = xfc->hdc->hwnd->invalid->y; + w = xfc->hdc->hwnd->invalid->w; + h = xfc->hdc->hwnd->invalid->h; + xf_lock_x11(xfc); + xf_draw_screen(xfc, x, y, w, h); + xf_unlock_x11(xfc); + } + else + { + int i; + int ninvalid; + HGDI_RGN cinvalid; + + if (xfc->hdc->hwnd->ninvalid < 1) + return TRUE; + + ninvalid = xfc->hdc->hwnd->ninvalid; + cinvalid = xfc->hdc->hwnd->cinvalid; + xf_lock_x11(xfc); + + for (i = 0; i < ninvalid; i++) + { + x = cinvalid[i].x; + y = cinvalid[i].y; + w = cinvalid[i].w; + h = cinvalid[i].h; + xf_draw_screen(xfc, x, y, w, h); + } + + XFlush(xfc->display); + xf_unlock_x11(xfc); + } + } + else + { + if (xfc->hdc->hwnd->invalid->null) + return TRUE; + + x = xfc->hdc->hwnd->invalid->x; + y = xfc->hdc->hwnd->invalid->y; + w = xfc->hdc->hwnd->invalid->w; + h = xfc->hdc->hwnd->invalid->h; + xf_lock_x11(xfc); + xf_rail_paint(xfc, x, y, x + w, y + h); + xf_unlock_x11(xfc); + } + + xfc->hdc->hwnd->invalid->null = TRUE; + xfc->hdc->hwnd->ninvalid = 0; + return TRUE; +} + +static BOOL xf_hw_desktop_resize(rdpContext* context) +{ + rdpGdi* gdi = context->gdi; + xfContext* xfc = (xfContext*)context; + rdpSettings* settings = context->settings; + BOOL ret = FALSE; + xf_lock_x11(xfc); + + if (!gdi_resize(gdi, settings->DesktopWidth, settings->DesktopHeight)) + goto out; + + ret = xf_desktop_resize(context); +out: + xf_unlock_x11(xfc); + return ret; +} + +static BOOL xf_process_x_events(freerdp* instance) +{ + BOOL status; + XEvent xevent; + int pending_status; + xfContext* xfc = (xfContext*)instance->context; + status = TRUE; + pending_status = TRUE; + + while (pending_status) + { + xf_lock_x11(xfc); + pending_status = XPending(xfc->display); + + if (pending_status) + { + ZeroMemory(&xevent, sizeof(xevent)); + XNextEvent(xfc->display, &xevent); + status = xf_event_process(instance, &xevent); + } + xf_unlock_x11(xfc); + if (!status) + break; + } + + return status; +} + +static char* xf_window_get_title(rdpSettings* settings) +{ + BOOL port; + char* windowTitle; + size_t size; + char* name; + const char* prefix = "FreeRDP:"; + + if (!settings) + return NULL; + + name = settings->ServerHostname; + + if (settings->WindowTitle) + return _strdup(settings->WindowTitle); + + port = (settings->ServerPort != 3389); + /* Just assume a window title is never longer than a filename... */ + size = strnlen(name, MAX_PATH) + 16; + windowTitle = calloc(size, sizeof(char)); + + if (!windowTitle) + return NULL; + + if (!port) + sprintf_s(windowTitle, size, "%s %s", prefix, name); + else + sprintf_s(windowTitle, size, "%s %s:%i", prefix, name, settings->ServerPort); + + return windowTitle; +} + +BOOL xf_create_window(xfContext* xfc) +{ + XGCValues gcv; + XEvent xevent; + int width, height; + char* windowTitle; + rdpGdi* gdi; + rdpSettings* settings; + settings = xfc->context.settings; + gdi = xfc->context.gdi; + ZeroMemory(&xevent, sizeof(xevent)); + width = settings->DesktopWidth; + height = settings->DesktopHeight; + + if (!xfc->hdc) + if (!(xfc->hdc = gdi_CreateDC(gdi->dstFormat))) + return FALSE; + + if (!xfc->remote_app) + { + xfc->attribs.background_pixel = BlackPixelOfScreen(xfc->screen); + xfc->attribs.border_pixel = WhitePixelOfScreen(xfc->screen); + xfc->attribs.backing_store = xfc->primary ? NotUseful : Always; + xfc->attribs.override_redirect = False; + xfc->attribs.colormap = xfc->colormap; + xfc->attribs.bit_gravity = NorthWestGravity; + xfc->attribs.win_gravity = NorthWestGravity; +#ifdef WITH_XRENDER + xfc->offset_x = 0; + xfc->offset_y = 0; +#endif + windowTitle = xf_window_get_title(settings); + + if (!windowTitle) + return FALSE; + +#ifdef WITH_XRENDER + + if (settings->SmartSizing && !xfc->fullscreen) + { + if (settings->SmartSizingWidth) + width = settings->SmartSizingWidth; + + if (settings->SmartSizingHeight) + height = settings->SmartSizingHeight; + + xfc->scaledWidth = width; + xfc->scaledHeight = height; + } + +#endif + xfc->window = xf_CreateDesktopWindow(xfc, windowTitle, width, height); + free(windowTitle); + + if (xfc->fullscreen) + xf_SetWindowFullscreen(xfc, xfc->window, xfc->fullscreen); + + xfc->unobscured = (xevent.xvisibility.state == VisibilityUnobscured); + XSetWMProtocols(xfc->display, xfc->window->handle, &(xfc->WM_DELETE_WINDOW), 1); + xfc->drawable = xfc->window->handle; + } + else + { + xfc->drawable = xf_CreateDummyWindow(xfc); + } + + ZeroMemory(&gcv, sizeof(gcv)); + + if (xfc->modifierMap) + XFreeModifiermap(xfc->modifierMap); + + xfc->modifierMap = XGetModifierMapping(xfc->display); + + if (!xfc->gc) + xfc->gc = XCreateGC(xfc->display, xfc->drawable, GCGraphicsExposures, &gcv); + + if (!xfc->primary) + xfc->primary = XCreatePixmap(xfc->display, xfc->drawable, settings->DesktopWidth, + settings->DesktopHeight, xfc->depth); + + xfc->drawing = xfc->primary; + + if (!xfc->bitmap_mono) + xfc->bitmap_mono = XCreatePixmap(xfc->display, xfc->drawable, 8, 8, 1); + + if (!xfc->gc_mono) + xfc->gc_mono = XCreateGC(xfc->display, xfc->bitmap_mono, GCGraphicsExposures, &gcv); + + XSetFunction(xfc->display, xfc->gc, GXcopy); + XSetFillStyle(xfc->display, xfc->gc, FillSolid); + XSetForeground(xfc->display, xfc->gc, BlackPixelOfScreen(xfc->screen)); + XFillRectangle(xfc->display, xfc->primary, xfc->gc, 0, 0, settings->DesktopWidth, + settings->DesktopHeight); + XFlush(xfc->display); + + if (!xfc->image) + { + rdpGdi* gdi = xfc->context.gdi; + xfc->image = XCreateImage(xfc->display, xfc->visual, xfc->depth, ZPixmap, 0, + (char*)gdi->primary_buffer, settings->DesktopWidth, + settings->DesktopHeight, xfc->scanline_pad, gdi->stride); + xfc->image->byte_order = LSBFirst; + xfc->image->bitmap_bit_order = LSBFirst; + } + + return TRUE; +} + +static void xf_window_free(xfContext* xfc) +{ + if (xfc->window) + { + xf_DestroyDesktopWindow(xfc, xfc->window); + xfc->window = NULL; + } + + if (xfc->hdc) + { + gdi_DeleteDC(xfc->hdc); + xfc->hdc = NULL; + } + +#if defined(CHANNEL_TSMF_CLIENT) + if (xfc->xv_context) + { + xf_tsmf_uninit(xfc, NULL); + xfc->xv_context = NULL; + } +#endif + + if (xfc->image) + { + xfc->image->data = NULL; + XDestroyImage(xfc->image); + xfc->image = NULL; + } + + if (xfc->bitmap_mono) + { + XFreePixmap(xfc->display, xfc->bitmap_mono); + xfc->bitmap_mono = 0; + } + + if (xfc->gc_mono) + { + XFreeGC(xfc->display, xfc->gc_mono); + xfc->gc_mono = 0; + } + + if (xfc->primary) + { + XFreePixmap(xfc->display, xfc->primary); + xfc->primary = 0; + } + + if (xfc->gc) + { + XFreeGC(xfc->display, xfc->gc); + xfc->gc = 0; + } + + if (xfc->modifierMap) + { + XFreeModifiermap(xfc->modifierMap); + xfc->modifierMap = NULL; + } +} + +void xf_toggle_fullscreen(xfContext* xfc) +{ + WindowStateChangeEventArgs e; + rdpContext* context = (rdpContext*)xfc; + rdpSettings* settings = context->settings; + + /* + when debugging, ungrab keyboard when toggling fullscreen + to allow keyboard usage on the debugger + */ + if (xfc->debug) + { + XUngrabKeyboard(xfc->display, CurrentTime); + } + + xfc->fullscreen = (xfc->fullscreen) ? FALSE : TRUE; + xfc->decorations = (xfc->fullscreen) ? FALSE : settings->Decorations; + xf_SetWindowFullscreen(xfc, xfc->window, xfc->fullscreen); + EventArgsInit(&e, "xfreerdp"); + e.state = xfc->fullscreen ? FREERDP_WINDOW_STATE_FULLSCREEN : 0; + PubSub_OnWindowStateChange(context->pubSub, context, &e); +} + +BOOL xf_toggle_control(xfContext* xfc) +{ + EncomspClientContext* encomsp; + ENCOMSP_CHANGE_PARTICIPANT_CONTROL_LEVEL_PDU pdu; + encomsp = xfc->encomsp; + + if (!encomsp) + return FALSE; + + pdu.ParticipantId = 0; + pdu.Flags = ENCOMSP_REQUEST_VIEW; + + if (!xfc->controlToggle) + pdu.Flags |= ENCOMSP_REQUEST_INTERACT; + + encomsp->ChangeParticipantControlLevel(encomsp, &pdu); + xfc->controlToggle = !xfc->controlToggle; + return TRUE; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT +xf_encomsp_participant_created(EncomspClientContext* context, + const ENCOMSP_PARTICIPANT_CREATED_PDU* participantCreated) +{ + xfContext* xfc; + rdpSettings* settings; + BOOL request; + + if (!context || !context->custom || !participantCreated) + return ERROR_INVALID_PARAMETER; + + xfc = context->custom; + settings = xfc->context.settings; + + if (!settings) + return ERROR_INVALID_PARAMETER; + + request = freerdp_settings_get_bool(settings, FreeRDP_RemoteAssistanceRequestControl); + if (request && (participantCreated->Flags & ENCOMSP_MAY_VIEW) && + !(participantCreated->Flags & ENCOMSP_MAY_INTERACT)) + xf_toggle_control(xfc); + + return CHANNEL_RC_OK; +} + +void xf_encomsp_init(xfContext* xfc, EncomspClientContext* encomsp) +{ + xfc->encomsp = encomsp; + encomsp->custom = (void*)xfc; + encomsp->ParticipantCreated = xf_encomsp_participant_created; +} + +void xf_encomsp_uninit(xfContext* xfc, EncomspClientContext* encomsp) +{ + WINPR_UNUSED(encomsp); + xfc->encomsp = NULL; +} + +void xf_lock_x11_(xfContext* xfc, const char* fkt) +{ + + if (!xfc->UseXThreads) + WaitForSingleObject(xfc->mutex, INFINITE); + else + XLockDisplay(xfc->display); + + xfc->locked++; + WLog_VRB(TAG, "%s:\t[%" PRIu32 "] from %s", __FUNCTION__, xfc->locked, fkt); +} + +void xf_unlock_x11_(xfContext* xfc, const char* fkt) +{ + if (xfc->locked == 0) + WLog_WARN(TAG, "X11: trying to unlock although not locked!"); + + WLog_VRB(TAG, "%s:\t[%" PRIu32 "] from %s", __FUNCTION__, xfc->locked - 1, fkt); + if (!xfc->UseXThreads) + ReleaseMutex(xfc->mutex); + else + XUnlockDisplay(xfc->display); + xfc->locked--; +} + +static BOOL xf_get_pixmap_info(xfContext* xfc) +{ + int i; + int vi_count; + int pf_count; + XVisualInfo* vi; + XVisualInfo* vis; + XVisualInfo tpl; + XPixmapFormatValues* pf; + XPixmapFormatValues* pfs; + XWindowAttributes window_attributes; + assert(xfc->display); + pfs = XListPixmapFormats(xfc->display, &pf_count); + + if (!pfs) + { + WLog_ERR(TAG, "XListPixmapFormats failed"); + return 1; + } + + for (i = 0; i < pf_count; i++) + { + pf = pfs + i; + + if (pf->depth == xfc->depth) + { + xfc->scanline_pad = pf->scanline_pad; + break; + } + } + + XFree(pfs); + ZeroMemory(&tpl, sizeof(tpl)); + tpl.class = TrueColor; + tpl.screen = xfc->screen_number; + + if (XGetWindowAttributes(xfc->display, RootWindowOfScreen(xfc->screen), &window_attributes) == + 0) + { + WLog_ERR(TAG, "XGetWindowAttributes failed"); + return FALSE; + } + + vis = XGetVisualInfo(xfc->display, VisualClassMask | VisualScreenMask, &tpl, &vi_count); + + if (!vis) + { + WLog_ERR(TAG, "XGetVisualInfo failed"); + return FALSE; + } + + vi = vis; + + for (i = 0; i < vi_count; i++) + { + vi = vis + i; + + if (vi->visual == window_attributes.visual) + { + xfc->visual = vi->visual; + break; + } + } + + if (xfc->visual) + { + /* + * Detect if the server visual has an inverted colormap + * (BGR vs RGB, or red being the least significant byte) + */ + if (vi->red_mask & 0xFF) + { + xfc->invert = FALSE; + } + } + + XFree(vis); + + if ((xfc->visual == NULL) || (xfc->scanline_pad == 0)) + { + return FALSE; + } + + return TRUE; +} + +static int xf_error_handler(Display* d, XErrorEvent* ev) +{ + char buf[256]; + int do_abort = TRUE; + XGetErrorText(d, ev->error_code, buf, sizeof(buf)); + WLog_ERR(TAG, "%s", buf); + + if (do_abort) + abort(); + + _def_error_handler(d, ev); + return FALSE; +} + +static int _xf_error_handler(Display* d, XErrorEvent* ev) +{ + /* + * ungrab the keyboard, in case a debugger is running in + * another window. This make xf_error_handler() a potential + * debugger breakpoint. + */ + + XUngrabKeyboard(d, CurrentTime); + return xf_error_handler(d, ev); +} + +static BOOL xf_play_sound(rdpContext* context, const PLAY_SOUND_UPDATE* play_sound) +{ + xfContext* xfc = (xfContext*)context; + WINPR_UNUSED(play_sound); + XkbBell(xfc->display, None, 100, 0); + return TRUE; +} + +static void xf_check_extensions(xfContext* context) +{ + int xkb_opcode, xkb_event, xkb_error; + int xkb_major = XkbMajorVersion; + int xkb_minor = XkbMinorVersion; + + if (XkbLibraryVersion(&xkb_major, &xkb_minor) && + XkbQueryExtension(context->display, &xkb_opcode, &xkb_event, &xkb_error, &xkb_major, + &xkb_minor)) + { + context->xkbAvailable = TRUE; + } + +#ifdef WITH_XRENDER + { + int xrender_event_base; + int xrender_error_base; + + if (XRenderQueryExtension(context->display, &xrender_event_base, &xrender_error_base)) + { + context->xrenderAvailable = TRUE; + } + } +#endif +} + +#ifdef WITH_XI +/* Input device which does NOT have the correct mapping. We must disregard */ +/* this device when trying to find the input device which is the pointer. */ +static const char TEST_PTR_STR[] = "Virtual core XTEST pointer"; +static const size_t TEST_PTR_LEN = sizeof(TEST_PTR_STR) / sizeof(char); +#endif /* WITH_XI */ + +static void xf_get_x11_button_map(xfContext* xfc, unsigned char* x11_map) +{ +#ifdef WITH_XI + int opcode, event, error; + XDevice* ptr_dev; + XExtensionVersion* version; + XDeviceInfo* devices1; + XIDeviceInfo* devices2; + int i, num_devices; + + if (XQueryExtension(xfc->display, "XInputExtension", &opcode, &event, &error)) + { + WLog_DBG(TAG, "Searching for XInput pointer device"); + ptr_dev = NULL; + /* loop through every device, looking for a pointer */ + version = XGetExtensionVersion(xfc->display, INAME); + + if (version->major_version >= 2) + { + /* XID of pointer device using XInput version 2 */ + devices2 = XIQueryDevice(xfc->display, XIAllDevices, &num_devices); + + if (devices2) + { + for (i = 0; i < num_devices; ++i) + { + if ((devices2[i].use == XISlavePointer) && + (strncmp(devices2[i].name, TEST_PTR_STR, TEST_PTR_LEN) != 0)) + { + ptr_dev = XOpenDevice(xfc->display, devices2[i].deviceid); + if (ptr_dev) + break; + } + } + + XIFreeDeviceInfo(devices2); + } + } + else + { + /* XID of pointer device using XInput version 1 */ + devices1 = XListInputDevices(xfc->display, &num_devices); + + if (devices1) + { + for (i = 0; i < num_devices; ++i) + { + if ((devices1[i].use == IsXExtensionPointer) && + (strncmp(devices1[i].name, TEST_PTR_STR, TEST_PTR_LEN) != 0)) + { + ptr_dev = XOpenDevice(xfc->display, devices1[i].id); + if (ptr_dev) + break; + } + } + + XFreeDeviceList(devices1); + } + } + + XFree(version); + + /* get button mapping from input extension if there is a pointer device; */ + /* otherwise leave unchanged. */ + if (ptr_dev) + { + WLog_DBG(TAG, "Pointer device: %d", ptr_dev->device_id); + XGetDeviceButtonMapping(xfc->display, ptr_dev, x11_map, NUM_BUTTONS_MAPPED); + XCloseDevice(xfc->display, ptr_dev); + } + else + { + WLog_DBG(TAG, "No pointer device found!"); + } + } + else +#endif /* WITH_XI */ + { + WLog_DBG(TAG, "Get global pointer mapping (no XInput)"); + XGetPointerMapping(xfc->display, x11_map, NUM_BUTTONS_MAPPED); + } +} + +/* Assignment of physical (not logical) mouse buttons to wire flags. */ +/* Notice that the middle button is 2 in X11, but 3 in RDP. */ +static const button_map xf_button_flags[NUM_BUTTONS_MAPPED] = { + { Button1, PTR_FLAGS_BUTTON1 }, + { Button2, PTR_FLAGS_BUTTON3 }, + { Button3, PTR_FLAGS_BUTTON2 }, + { Button4, PTR_FLAGS_WHEEL | 0x78 }, + /* Negative value is 9bit twos complement */ + { Button5, PTR_FLAGS_WHEEL | PTR_FLAGS_WHEEL_NEGATIVE | (0x100 - 0x78) }, + { 6, PTR_FLAGS_HWHEEL | PTR_FLAGS_WHEEL_NEGATIVE | (0x100 - 0x78) }, + { 7, PTR_FLAGS_HWHEEL | 0x78 }, + { 8, PTR_XFLAGS_BUTTON1 }, + { 9, PTR_XFLAGS_BUTTON2 }, + { 97, PTR_XFLAGS_BUTTON1 }, + { 112, PTR_XFLAGS_BUTTON2 } +}; + +static UINT16 get_flags_for_button(int button) +{ + size_t x; + + for (x = 0; x < ARRAYSIZE(xf_button_flags); x++) + { + const button_map* map = &xf_button_flags[x]; + + if (map->button == button) + return map->flags; + } + + return 0; +} + +static void xf_button_map_init(xfContext* xfc) +{ + size_t pos = 0; + /* loop counter for array initialization */ + size_t physical; + /* logical mouse button which is used for each physical mouse */ + /* button (indexed from zero). This is the default map. */ + unsigned char x11_map[112] = { 0 }; + x11_map[0] = Button1; + x11_map[1] = Button2; + x11_map[2] = Button3; + x11_map[3] = Button4; + x11_map[4] = Button5; + x11_map[5] = 6; + x11_map[6] = 7; + x11_map[7] = 8; + x11_map[8] = 9; + x11_map[96] = 97; + x11_map[111] = 112; + + /* query system for actual remapping */ + if (xfc->context.settings->UnmapButtons) + { + xf_get_x11_button_map(xfc, x11_map); + } + + /* iterate over all (mapped) physical buttons; for each of them */ + /* find the logical button in X11, and assign to this the */ + /* appropriate value to send over the RDP wire. */ + for (physical = 0; physical < ARRAYSIZE(x11_map); ++physical) + { + const unsigned char logical = x11_map[physical]; + const UINT16 flags = get_flags_for_button(logical); + + if ((logical != 0) && (flags != 0)) + { + if (pos >= NUM_BUTTONS_MAPPED) + { + WLog_ERR(TAG, "Failed to map mouse button to RDP button, no space"); + } + else + { + button_map* map = &xfc->button_map[pos++]; + map->button = logical; + map->flags = get_flags_for_button(physical + Button1); + } + } + } +} + +/** + * Callback given to freerdp_connect() to process the pre-connect operations. + * It will fill the rdp_freerdp structure (instance) with the appropriate options to use for the + * connection. + * + * @param instance - pointer to the rdp_freerdp structure that contains the connection's parameters, + * and will be filled with the appropriate informations. + * + * @return TRUE if successful. FALSE otherwise. + * Can exit with error code XF_EXIT_PARSE_ARGUMENTS if there is an error in the parameters. + */ +static BOOL xf_pre_connect(freerdp* instance) +{ + rdpChannels* channels; + rdpSettings* settings; + rdpContext* context = instance->context; + xfContext* xfc = (xfContext*)instance->context; + UINT32 maxWidth = 0; + UINT32 maxHeight = 0; + settings = instance->settings; + channels = context->channels; + settings->OsMajorType = OSMAJORTYPE_UNIX; + settings->OsMinorType = OSMINORTYPE_NATIVE_XSERVER; + PubSub_SubscribeChannelConnected(instance->context->pubSub, xf_OnChannelConnectedEventHandler); + PubSub_SubscribeChannelDisconnected(instance->context->pubSub, + xf_OnChannelDisconnectedEventHandler); + + if (!freerdp_client_load_addins(channels, instance->settings)) + return FALSE; + + if (!settings->Username && !settings->CredentialsFromStdin && !settings->SmartcardLogon) + { + char login_name[MAX_PATH] = { 0 }; + ULONG size = sizeof(login_name) - 1; + + if (GetUserNameExA(NameSamCompatible, login_name, &size)) + { + if (!freerdp_settings_set_string(settings, FreeRDP_Username, login_name)) + return FALSE; + + WLog_INFO(TAG, "No user name set. - Using login name: %s", settings->Username); + } + } + + if (settings->AuthenticationOnly) + { + /* Check +auth-only has a username and password. */ + if (!settings->Password) + { + WLog_INFO(TAG, "auth-only, but no password set. Please provide one."); + return FALSE; + } + + WLog_INFO(TAG, "Authentication only. Don't connect to X."); + } + + if (!xf_keyboard_init(xfc)) + return FALSE; + + xf_detect_monitors(xfc, &maxWidth, &maxHeight); + + if (maxWidth && maxHeight) + { + settings->DesktopWidth = maxWidth; + settings->DesktopHeight = maxHeight; + } + +#ifdef WITH_XRENDER + + /** + * If /f is specified in combination with /smart-sizing:widthxheight then + * we run the session in the /smart-sizing dimensions scaled to full screen + */ + if (settings->Fullscreen && settings->SmartSizing && settings->SmartSizingWidth && + settings->SmartSizingHeight) + { + settings->DesktopWidth = settings->SmartSizingWidth; + settings->DesktopHeight = settings->SmartSizingHeight; + } + +#endif + xfc->fullscreen = settings->Fullscreen; + xfc->decorations = settings->Decorations; + xfc->grab_keyboard = settings->GrabKeyboard; + xfc->fullscreen_toggle = settings->ToggleFullscreen; + xf_button_map_init(xfc); + return TRUE; +} + +/** + * Callback given to freerdp_connect() to perform post-connection operations. + * It will be called only if the connection was initialized properly, and will continue the + * initialization based on the newly created connection. + */ +static BOOL xf_post_connect(freerdp* instance) +{ + rdpUpdate* update; + rdpContext* context; + rdpSettings* settings; + ResizeWindowEventArgs e; + xfContext* xfc = (xfContext*)instance->context; + context = instance->context; + settings = instance->settings; + update = context->update; + BOOL serverIsWindowsPlatform; + + if (!gdi_init(instance, xf_get_local_color_format(xfc, TRUE))) + return FALSE; + + if (!xf_register_pointer(context->graphics)) + return FALSE; + + if (!settings->SoftwareGdi) + { + if (!xf_register_graphics(context->graphics)) + { + WLog_ERR(TAG, "failed to register graphics"); + return FALSE; + } + + xf_gdi_register_update_callbacks(update); + brush_cache_register_callbacks(instance->update); + glyph_cache_register_callbacks(instance->update); + bitmap_cache_register_callbacks(instance->update); + offscreen_cache_register_callbacks(instance->update); + palette_cache_register_callbacks(instance->update); + } + +#ifdef WITH_XRENDER + xfc->scaledWidth = settings->DesktopWidth; + xfc->scaledHeight = settings->DesktopHeight; + xfc->offset_x = 0; + xfc->offset_y = 0; +#endif + + if (!xfc->xrenderAvailable) + { + if (settings->SmartSizing) + { + WLog_ERR(TAG, "XRender not available: disabling smart-sizing"); + settings->SmartSizing = FALSE; + } + + if (settings->MultiTouchGestures) + { + WLog_ERR(TAG, "XRender not available: disabling local multi-touch gestures"); + settings->MultiTouchGestures = FALSE; + } + } + + if (settings->RemoteApplicationMode) + xfc->remote_app = TRUE; + + if (!xf_create_window(xfc)) + { + WLog_ERR(TAG, "xf_create_window failed"); + return FALSE; + } + + if (settings->SoftwareGdi) + { + update->EndPaint = xf_sw_end_paint; + update->DesktopResize = xf_sw_desktop_resize; + } + else + { + update->EndPaint = xf_hw_end_paint; + update->DesktopResize = xf_hw_desktop_resize; + } + + update->PlaySound = xf_play_sound; + update->SetKeyboardIndicators = xf_keyboard_set_indicators; + update->SetKeyboardImeStatus = xf_keyboard_set_ime_status; + + serverIsWindowsPlatform = (settings->OsMajorType == OSMAJORTYPE_WINDOWS); + if (!(xfc->clipboard = xf_clipboard_new(xfc, !serverIsWindowsPlatform))) + return FALSE; + + if (!(xfc->xfDisp = xf_disp_new(xfc))) + { + xf_clipboard_free(xfc->clipboard); + return FALSE; + } + + EventArgsInit(&e, "xfreerdp"); + e.width = settings->DesktopWidth; + e.height = settings->DesktopHeight; + PubSub_OnResizeWindow(context->pubSub, xfc, &e); + return TRUE; +} + +static void xf_post_disconnect(freerdp* instance) +{ + xfContext* xfc; + rdpContext* context; + + if (!instance || !instance->context) + return; + + context = instance->context; + xfc = (xfContext*)context; + PubSub_UnsubscribeChannelConnected(instance->context->pubSub, + xf_OnChannelConnectedEventHandler); + PubSub_UnsubscribeChannelDisconnected(instance->context->pubSub, + xf_OnChannelDisconnectedEventHandler); + gdi_free(instance); + + if (xfc->clipboard) + { + xf_clipboard_free(xfc->clipboard); + xfc->clipboard = NULL; + } + + if (xfc->xfDisp) + { + xf_disp_free(xfc->xfDisp); + xfc->xfDisp = NULL; + } + + if ((xfc->window != NULL) && (xfc->drawable == xfc->window->handle)) + xfc->drawable = 0; + else + xf_DestroyDummyWindow(xfc, xfc->drawable); + + xf_window_free(xfc); + xf_keyboard_free(xfc); +} + +static int xf_logon_error_info(freerdp* instance, UINT32 data, UINT32 type) +{ + xfContext* xfc = (xfContext*)instance->context; + const char* str_data = freerdp_get_logon_error_info_data(data); + const char* str_type = freerdp_get_logon_error_info_type(type); + WLog_INFO(TAG, "Logon Error Info %s [%s]", str_data, str_type); + if(type != LOGON_MSG_SESSION_CONTINUE) + { + xf_rail_disable_remoteapp_mode(xfc); + } + return 1; +} + +static DWORD WINAPI xf_input_thread(LPVOID arg) +{ + BOOL running = TRUE; + DWORD status; + DWORD nCount; + HANDLE events[3]; + wMessage msg; + wMessageQueue* queue; + freerdp* instance = (freerdp*)arg; + xfContext* xfc = (xfContext*)instance->context; + queue = freerdp_get_message_queue(instance, FREERDP_INPUT_MESSAGE_QUEUE); + nCount = 0; + events[nCount++] = MessageQueue_Event(queue); + events[nCount++] = xfc->x11event; + events[nCount++] = instance->context->abortEvent; + + while (running) + { + status = WaitForMultipleObjects(nCount, events, FALSE, INFINITE); + + switch (status) + { + case WAIT_OBJECT_0: + case WAIT_OBJECT_0 + 1: + case WAIT_OBJECT_0 + 2: + if (WaitForSingleObject(events[0], 0) == WAIT_OBJECT_0) + { + if (MessageQueue_Peek(queue, &msg, FALSE)) + { + if (msg.id == WMQ_QUIT) + running = FALSE; + } + } + + if (WaitForSingleObject(events[1], 0) == WAIT_OBJECT_0) + { + if (!xf_process_x_events(xfc->context.instance)) + { + running = FALSE; + break; + } + } + + if (WaitForSingleObject(events[2], 0) == WAIT_OBJECT_0) + running = FALSE; + + break; + + default: + running = FALSE; + break; + } + } + + MessageQueue_PostQuit(queue, 0); + freerdp_abort_connect(xfc->context.instance); + ExitThread(0); + return 0; +} + +static BOOL handle_window_events(freerdp* instance) +{ + rdpSettings* settings; + + if (!instance || !instance->settings) + return FALSE; + + settings = instance->settings; + + if (!settings->AsyncInput) + { + if (!xf_process_x_events(instance)) + { + WLog_DBG(TAG, "Closed from X11"); + return FALSE; + } + } + + return TRUE; +} + +/** Main loop for the rdp connection. + * It will be run from the thread's entry point (thread_func()). + * It initiates the connection, and will continue to run until the session ends, + * processing events as they are received. + * @param instance - pointer to the rdp_freerdp structure that contains the session's settings + * @return A code from the enum XF_EXIT_CODE (0 if successful) + */ +static DWORD WINAPI xf_client_thread(LPVOID param) +{ + BOOL status; + DWORD exit_code = 0; + DWORD nCount; + DWORD waitStatus; + HANDLE handles[64]; + xfContext* xfc; + freerdp* instance; + rdpContext* context; + HANDLE inputEvent = NULL; + HANDLE inputThread = NULL; + HANDLE timer = NULL; + LARGE_INTEGER due; + rdpSettings* settings; + TimerEventArgs timerEvent; + EventArgsInit(&timerEvent, "xfreerdp"); + instance = (freerdp*)param; + context = instance->context; + status = freerdp_connect(instance); + xfc = (xfContext*)instance->context; + + if (!status) + { + if (freerdp_get_last_error(instance->context) == FREERDP_ERROR_AUTHENTICATION_FAILED) + exit_code = XF_EXIT_AUTH_FAILURE; + else if (freerdp_get_last_error(instance->context) == + FREERDP_ERROR_SECURITY_NEGO_CONNECT_FAILED) + exit_code = XF_EXIT_NEGO_FAILURE; + else if (freerdp_get_last_error(instance->context) == + FREERDP_ERROR_CONNECT_LOGON_FAILURE) + exit_code = XF_EXIT_LOGON_FAILURE; + else if (freerdp_get_last_error(instance->context) == + FREERDP_ERROR_CONNECT_ACCOUNT_LOCKED_OUT) + exit_code = XF_EXIT_ACCOUNT_LOCKED_OUT; + else if (freerdp_get_last_error(instance->context) == + FREERDP_ERROR_PRE_CONNECT_FAILED) + exit_code = XF_EXIT_PRE_CONNECT_FAILED; + else if (freerdp_get_last_error(instance->context) == + FREERDP_ERROR_CONNECT_UNDEFINED) + exit_code = XF_EXIT_CONNECT_UNDEFINED; + else if (freerdp_get_last_error(instance->context) == + FREERDP_ERROR_POST_CONNECT_FAILED) + exit_code = XF_EXIT_POST_CONNECT_FAILED; + else if (freerdp_get_last_error(instance->context) == + FREERDP_ERROR_DNS_ERROR) + exit_code = XF_EXIT_DNS_ERROR; + else if (freerdp_get_last_error(instance->context) == + FREERDP_ERROR_DNS_NAME_NOT_FOUND) + exit_code = XF_EXIT_DNS_NAME_NOT_FOUND; + else if (freerdp_get_last_error(instance->context) == + FREERDP_ERROR_CONNECT_FAILED) + exit_code = XF_EXIT_CONNECT_FAILED; + else if (freerdp_get_last_error(instance->context) == + FREERDP_ERROR_MCS_CONNECT_INITIAL_ERROR) + exit_code = XF_EXIT_MCS_CONNECT_INITIAL_ERROR; + else if (freerdp_get_last_error(instance->context) == + FREERDP_ERROR_TLS_CONNECT_FAILED) + exit_code = XF_EXIT_TLS_CONNECT_FAILED; + else if (freerdp_get_last_error(instance->context) == + FREERDP_ERROR_INSUFFICIENT_PRIVILEGES) + exit_code = XF_EXIT_INSUFFICIENT_PRIVILEGES; + else if (freerdp_get_last_error(instance->context) == + FREERDP_ERROR_CONNECT_CANCELLED) + exit_code = XF_EXIT_CONNECT_CANCELLED; + else if (freerdp_get_last_error(instance->context) == + FREERDP_ERROR_SECURITY_NEGO_CONNECT_FAILED) + exit_code = XF_EXIT_SECURITY_NEGO_CONNECT_FAILED; + else if (freerdp_get_last_error(instance->context) == + FREERDP_ERROR_CONNECT_TRANSPORT_FAILED) + exit_code = XF_EXIT_CONNECT_TRANSPORT_FAILED; + else if (freerdp_get_last_error(instance->context) == + FREERDP_ERROR_CONNECT_PASSWORD_EXPIRED) + exit_code = XF_EXIT_CONNECT_PASSWORD_EXPIRED; + else if (freerdp_get_last_error(instance->context) == + FREERDP_ERROR_CONNECT_PASSWORD_MUST_CHANGE) + exit_code = XF_EXIT_CONNECT_PASSWORD_MUST_CHANGE; + else if (freerdp_get_last_error(instance->context) == + FREERDP_ERROR_CONNECT_KDC_UNREACHABLE) + exit_code = XF_EXIT_CONNECT_KDC_UNREACHABLE; + else if (freerdp_get_last_error(instance->context) == + FREERDP_ERROR_CONNECT_ACCOUNT_DISABLED) + exit_code = XF_EXIT_CONNECT_ACCOUNT_DISABLED; + else if (freerdp_get_last_error(instance->context) == + FREERDP_ERROR_CONNECT_PASSWORD_CERTAINLY_EXPIRED) + exit_code = XF_EXIT_CONNECT_PASSWORD_CERTAINLY_EXPIRED; + else if (freerdp_get_last_error(instance->context) == + FREERDP_ERROR_CONNECT_CLIENT_REVOKED) + exit_code = XF_EXIT_CONNECT_CLIENT_REVOKED; + else if (freerdp_get_last_error(instance->context) == + FREERDP_ERROR_CONNECT_WRONG_PASSWORD) + exit_code = XF_EXIT_CONNECT_WRONG_PASSWORD; + else if (freerdp_get_last_error(instance->context) == + FREERDP_ERROR_CONNECT_ACCESS_DENIED) + exit_code = XF_EXIT_CONNECT_ACCESS_DENIED; + else if (freerdp_get_last_error(instance->context) == + FREERDP_ERROR_CONNECT_ACCOUNT_RESTRICTION) + exit_code = XF_EXIT_CONNECT_ACCOUNT_RESTRICTION; + else if (freerdp_get_last_error(instance->context) == + FREERDP_ERROR_CONNECT_ACCOUNT_EXPIRED) + exit_code = XF_EXIT_CONNECT_ACCOUNT_EXPIRED; + else if (freerdp_get_last_error(instance->context) == + FREERDP_ERROR_CONNECT_LOGON_TYPE_NOT_GRANTED) + exit_code = XF_EXIT_CONNECT_LOGON_TYPE_NOT_GRANTED; + else if (freerdp_get_last_error(instance->context) == + FREERDP_ERROR_CONNECT_NO_OR_MISSING_CREDENTIALS) + exit_code = XF_EXIT_CONNECT_NO_OR_MISSING_CREDENTIALS; + else + exit_code = XF_EXIT_CONN_FAILED; + } + else + exit_code = XF_EXIT_SUCCESS; + + if (!status) + goto end; + + /* --authonly ? */ + if (instance->settings->AuthenticationOnly) + { + WLog_ERR(TAG, "Authentication only, exit status %" PRId32 "", !status); + goto disconnect; + } + + if (!status) + { + WLog_ERR(TAG, "Freerdp connect error exit status %" PRId32 "", !status); + exit_code = freerdp_error_info(instance); + + if (freerdp_get_last_error(instance->context) == FREERDP_ERROR_AUTHENTICATION_FAILED) + exit_code = XF_EXIT_AUTH_FAILURE; + else if (exit_code == ERRINFO_SUCCESS) + exit_code = XF_EXIT_CONN_FAILED; + + goto disconnect; + } + + settings = context->settings; + timer = CreateWaitableTimerA(NULL, FALSE, "mainloop-periodic-timer"); + + if (!timer) + { + WLog_ERR(TAG, "failed to create timer"); + goto disconnect; + } + + due.QuadPart = 0; + + if (!SetWaitableTimer(timer, &due, 20, NULL, NULL, FALSE)) + { + goto disconnect; + } + + if (!settings->AsyncInput) + { + inputEvent = xfc->x11event; + } + else + { + if (!(inputThread = CreateThread(NULL, 0, xf_input_thread, instance, 0, NULL))) + { + WLog_ERR(TAG, "async input: failed to create input thread"); + exit_code = XF_EXIT_UNKNOWN; + goto disconnect; + } + } + + while (!freerdp_shall_disconnect(instance)) + { + nCount = 0; + handles[nCount++] = timer; + + if (!settings->AsyncInput) + handles[nCount++] = inputEvent; + + /* + * win8 and server 2k12 seem to have some timing issue/race condition + * when a initial sync request is send to sync the keyboard indicators + * sending the sync event twice fixed this problem + */ + if (freerdp_focus_required(instance)) + { + xf_keyboard_focus_in(xfc); + xf_keyboard_focus_in(xfc); + } + + { + DWORD tmp = + freerdp_get_event_handles(context, &handles[nCount], ARRAYSIZE(handles) - nCount); + + if (tmp == 0) + { + WLog_ERR(TAG, "freerdp_get_event_handles failed"); + break; + } + + nCount += tmp; + } + + if (xfc->window) + xf_floatbar_hide_and_show(xfc->window->floatbar); + + waitStatus = WaitForMultipleObjects(nCount, handles, FALSE, INFINITE); + + if (waitStatus == WAIT_FAILED) + break; + + { + if (!freerdp_check_event_handles(context)) + { + if (client_auto_reconnect_ex(instance, handle_window_events)) + continue; + else + { + /* + * Indicate an unsuccessful connection attempt if reconnect + * did not succeed and no other error was specified. + */ + if (freerdp_error_info(instance) == 0) + exit_code = XF_EXIT_CONN_FAILED; + } + + if (freerdp_get_last_error(context) == FREERDP_ERROR_SUCCESS) + WLog_ERR(TAG, "Failed to check FreeRDP file descriptor"); + + break; + } + } + + if (!handle_window_events(instance)) + break; + + if ((status != WAIT_TIMEOUT) && (waitStatus == WAIT_OBJECT_0)) + { + timerEvent.now = GetTickCount64(); + PubSub_OnTimer(context->pubSub, context, &timerEvent); + } + } + + if (settings->AsyncInput) + { + WaitForSingleObject(inputThread, INFINITE); + CloseHandle(inputThread); + } + + if (!exit_code) + { + exit_code = freerdp_error_info(instance); + + if (exit_code == XF_EXIT_DISCONNECT && + freerdp_get_disconnect_ultimatum(context) == Disconnect_Ultimatum_user_requested) + { + /* This situation might be limited to Windows XP. */ + WLog_INFO(TAG, "Error info says user did not initiate but disconnect ultimatum says " + "they did; treat this as a user logoff"); + exit_code = XF_EXIT_LOGOFF; + } + } + +disconnect: + + if (timer) + CloseHandle(timer); + + freerdp_disconnect(instance); +end: + ExitThread(exit_code); + return exit_code; +} + +DWORD xf_exit_code_from_disconnect_reason(DWORD reason) +{ + if (reason == 0 || (reason >= XF_EXIT_PARSE_ARGUMENTS && reason <= XF_EXIT_CONNECT_NO_OR_MISSING_CREDENTIALS)) + return reason; + /* License error set */ + else if (reason >= 0x100 && reason <= 0x10A) + reason -= 0x100 + XF_EXIT_LICENSE_INTERNAL; + /* RDP protocol error set */ + else if (reason >= 0x10c9 && reason <= 0x1193) + reason = XF_EXIT_RDP; + /* There's no need to test protocol-independent codes: they match */ + else if (!(reason <= 0xC)) + reason = XF_EXIT_UNKNOWN; + + return reason; +} + +static void xf_TerminateEventHandler(void* context, TerminateEventArgs* e) +{ + rdpContext* ctx = (rdpContext*)context; + WINPR_UNUSED(e); + freerdp_abort_connect(ctx->instance); +} + +#ifdef WITH_XRENDER +static void xf_ZoomingChangeEventHandler(void* context, ZoomingChangeEventArgs* e) +{ + xfContext* xfc = (xfContext*)context; + rdpSettings* settings = xfc->context.settings; + int w = xfc->scaledWidth + e->dx; + int h = xfc->scaledHeight + e->dy; + + if (e->dx == 0 && e->dy == 0) + return; + + if (w < 10) + w = 10; + + if (h < 10) + h = 10; + + if (w == xfc->scaledWidth && h == xfc->scaledHeight) + return; + + xfc->scaledWidth = w; + xfc->scaledHeight = h; + xf_draw_screen(xfc, 0, 0, settings->DesktopWidth, settings->DesktopHeight); +} + +static void xf_PanningChangeEventHandler(void* context, PanningChangeEventArgs* e) +{ + xfContext* xfc = (xfContext*)context; + rdpSettings* settings = xfc->context.settings; + + if (e->dx == 0 && e->dy == 0) + return; + + xfc->offset_x += e->dx; + xfc->offset_y += e->dy; + xf_draw_screen(xfc, 0, 0, settings->DesktopWidth, settings->DesktopHeight); +} +#endif + +/** + * Client Interface + */ + +static BOOL xfreerdp_client_global_init() +{ + setlocale(LC_ALL, ""); + + if (freerdp_handle_signals() != 0) + return FALSE; + + return TRUE; +} + +static void xfreerdp_client_global_uninit() +{ +} + +static int xfreerdp_client_start(rdpContext* context) +{ + xfContext* xfc = (xfContext*)context; + rdpSettings* settings = context->settings; + + if (!settings->ServerHostname) + { + WLog_ERR(TAG, "error: server hostname was not specified with /v:[:port]"); + return -1; + } + + if (!(xfc->thread = CreateThread(NULL, 0, xf_client_thread, context->instance, 0, NULL))) + { + WLog_ERR(TAG, "failed to create client thread"); + return -1; + } + + return 0; +} + +static int xfreerdp_client_stop(rdpContext* context) +{ + xfContext* xfc = (xfContext*)context; + freerdp_abort_connect(context->instance); + + if (xfc->thread) + { + WaitForSingleObject(xfc->thread, INFINITE); + CloseHandle(xfc->thread); + xfc->thread = NULL; + } + + return 0; +} + +static Atom get_supported_atom(xfContext* xfc, const char* atomName) +{ + unsigned long i; + const Atom atom = XInternAtom(xfc->display, atomName, False); + + for (i = 0; i < xfc->supportedAtomCount; i++) + { + if (xfc->supportedAtoms[i] == atom) + return atom; + } + + return None; +} +static BOOL xfreerdp_client_new(freerdp* instance, rdpContext* context) +{ + xfContext* xfc = (xfContext*)instance->context; + assert(context); + assert(xfc); + assert(!xfc->display); + assert(!xfc->mutex); + assert(!xfc->x11event); + instance->PreConnect = xf_pre_connect; + instance->PostConnect = xf_post_connect; + instance->PostDisconnect = xf_post_disconnect; + instance->Authenticate = client_cli_authenticate; + instance->GatewayAuthenticate = client_cli_gw_authenticate; + instance->VerifyCertificateEx = client_cli_verify_certificate_ex; + instance->VerifyChangedCertificateEx = client_cli_verify_changed_certificate_ex; + instance->PresentGatewayMessage = client_cli_present_gateway_message; + instance->LogonErrorInfo = xf_logon_error_info; + PubSub_SubscribeTerminate(context->pubSub, xf_TerminateEventHandler); +#ifdef WITH_XRENDER + PubSub_SubscribeZoomingChange(context->pubSub, xf_ZoomingChangeEventHandler); + PubSub_SubscribePanningChange(context->pubSub, xf_PanningChangeEventHandler); +#endif + xfc->UseXThreads = TRUE; + /* uncomment below if debugging to prevent keyboard grap */ + /* xfc->debug = TRUE; */ + + if (xfc->UseXThreads) + { + if (!XInitThreads()) + { + WLog_WARN(TAG, "XInitThreads() failure"); + xfc->UseXThreads = FALSE; + } + } + + xfc->display = XOpenDisplay(NULL); + + if (!xfc->display) + { + WLog_ERR(TAG, "failed to open display: %s", XDisplayName(NULL)); + WLog_ERR(TAG, "Please check that the $DISPLAY environment variable is properly set."); + goto fail_open_display; + } + + xfc->mutex = CreateMutex(NULL, FALSE, NULL); + + if (!xfc->mutex) + { + WLog_ERR(TAG, "Could not create mutex!"); + goto fail_create_mutex; + } + + xfc->xfds = ConnectionNumber(xfc->display); + xfc->screen_number = DefaultScreen(xfc->display); + xfc->screen = ScreenOfDisplay(xfc->display, xfc->screen_number); + xfc->depth = DefaultDepthOfScreen(xfc->screen); + xfc->big_endian = (ImageByteOrder(xfc->display) == MSBFirst); + xfc->invert = TRUE; + xfc->complex_regions = TRUE; + xfc->_NET_SUPPORTED = XInternAtom(xfc->display, "_NET_SUPPORTED", True); + xfc->_NET_SUPPORTING_WM_CHECK = XInternAtom(xfc->display, "_NET_SUPPORTING_WM_CHECK", True); + + if ((xfc->_NET_SUPPORTED != None) && (xfc->_NET_SUPPORTING_WM_CHECK != None)) + { + Atom actual_type = 0; + int actual_format = 0; + unsigned long nitems = 0, after = 0; + unsigned char* data = NULL; + int status = XGetWindowProperty(xfc->display, RootWindowOfScreen(xfc->screen), + xfc->_NET_SUPPORTED, 0, 1024, False, XA_ATOM, &actual_type, + &actual_format, &nitems, &after, &data); + + if ((status == Success) && (actual_type == XA_ATOM) && (actual_format == 32)) + { + xfc->supportedAtomCount = nitems; + xfc->supportedAtoms = calloc(nitems, sizeof(Atom)); + memcpy(xfc->supportedAtoms, data, nitems * sizeof(Atom)); + } + + if (data) + XFree(data); + } + + xfc->_XWAYLAND_MAY_GRAB_KEYBOARD = + XInternAtom(xfc->display, "_XWAYLAND_MAY_GRAB_KEYBOARD", False); + xfc->_NET_WM_ICON = XInternAtom(xfc->display, "_NET_WM_ICON", False); + xfc->_MOTIF_WM_HINTS = XInternAtom(xfc->display, "_MOTIF_WM_HINTS", False); + xfc->_NET_CURRENT_DESKTOP = XInternAtom(xfc->display, "_NET_CURRENT_DESKTOP", False); + xfc->_NET_WORKAREA = XInternAtom(xfc->display, "_NET_WORKAREA", False); + xfc->_NET_WM_STATE = get_supported_atom(xfc, "_NET_WM_STATE"); + xfc->_NET_WM_STATE_FULLSCREEN = get_supported_atom(xfc, "_NET_WM_STATE_FULLSCREEN"); + xfc->_NET_WM_STATE_MAXIMIZED_HORZ = + XInternAtom(xfc->display, "_NET_WM_STATE_MAXIMIZED_HORZ", False); + xfc->_NET_WM_STATE_MAXIMIZED_VERT = + XInternAtom(xfc->display, "_NET_WM_STATE_MAXIMIZED_VERT", False); + xfc->_NET_WM_FULLSCREEN_MONITORS = get_supported_atom(xfc, "_NET_WM_FULLSCREEN_MONITORS"); + xfc->_NET_WM_NAME = XInternAtom(xfc->display, "_NET_WM_NAME", False); + xfc->_NET_WM_PID = XInternAtom(xfc->display, "_NET_WM_PID", False); + xfc->_NET_WM_WINDOW_TYPE = XInternAtom(xfc->display, "_NET_WM_WINDOW_TYPE", False); + xfc->_NET_WM_WINDOW_TYPE_NORMAL = + XInternAtom(xfc->display, "_NET_WM_WINDOW_TYPE_NORMAL", False); + xfc->_NET_WM_WINDOW_TYPE_DIALOG = + XInternAtom(xfc->display, "_NET_WM_WINDOW_TYPE_DIALOG", False); + xfc->_NET_WM_WINDOW_TYPE_POPUP = XInternAtom(xfc->display, "_NET_WM_WINDOW_TYPE_POPUP", False); + xfc->_NET_WM_WINDOW_TYPE_POPUP_MENU = + XInternAtom(xfc->display, "_NET_WM_WINDOW_TYPE_POPUP_MENU", False); + xfc->_NET_WM_WINDOW_TYPE_UTILITY = + XInternAtom(xfc->display, "_NET_WM_WINDOW_TYPE_UTILITY", False); + xfc->_NET_WM_WINDOW_TYPE_DROPDOWN_MENU = + XInternAtom(xfc->display, "_NET_WM_WINDOW_TYPE_DROPDOWN_MENU", False); + xfc->_NET_WM_STATE_SKIP_TASKBAR = + XInternAtom(xfc->display, "_NET_WM_STATE_SKIP_TASKBAR", False); + xfc->_NET_WM_STATE_SKIP_PAGER = XInternAtom(xfc->display, "_NET_WM_STATE_SKIP_PAGER", False); + xfc->_NET_WM_MOVERESIZE = XInternAtom(xfc->display, "_NET_WM_MOVERESIZE", False); + xfc->_NET_MOVERESIZE_WINDOW = XInternAtom(xfc->display, "_NET_MOVERESIZE_WINDOW", False); + xfc->UTF8_STRING = XInternAtom(xfc->display, "UTF8_STRING", FALSE); + xfc->WM_PROTOCOLS = XInternAtom(xfc->display, "WM_PROTOCOLS", False); + xfc->WM_DELETE_WINDOW = XInternAtom(xfc->display, "WM_DELETE_WINDOW", False); + xfc->WM_STATE = XInternAtom(xfc->display, "WM_STATE", False); + xfc->x11event = CreateFileDescriptorEvent(NULL, FALSE, FALSE, xfc->xfds, WINPR_FD_READ); + + if (!xfc->x11event) + { + WLog_ERR(TAG, "Could not create xfds event"); + goto fail_xfds_event; + } + + xfc->colormap = DefaultColormap(xfc->display, xfc->screen_number); + + if (xfc->debug) + { + WLog_INFO(TAG, "Enabling X11 debug mode."); + XSynchronize(xfc->display, TRUE); + _def_error_handler = XSetErrorHandler(_xf_error_handler); + } + + xf_check_extensions(xfc); + + if (!xf_get_pixmap_info(xfc)) + { + WLog_ERR(TAG, "Failed to get pixmap info"); + goto fail_pixmap_info; + } + + xfc->vscreen.monitors = calloc(16, sizeof(MONITOR_INFO)); + + if (!xfc->vscreen.monitors) + goto fail_vscreen_monitors; + + return TRUE; +fail_vscreen_monitors: +fail_pixmap_info: + CloseHandle(xfc->x11event); + xfc->x11event = NULL; +fail_xfds_event: + CloseHandle(xfc->mutex); + xfc->mutex = NULL; +fail_create_mutex: + XCloseDisplay(xfc->display); + xfc->display = NULL; +fail_open_display: + return FALSE; +} + +static void xfreerdp_client_free(freerdp* instance, rdpContext* context) +{ + xfContext* xfc = (xfContext*)instance->context; + + if (!context) + return; + + PubSub_UnsubscribeTerminate(context->pubSub, xf_TerminateEventHandler); +#ifdef WITH_XRENDER + PubSub_UnsubscribeZoomingChange(context->pubSub, xf_ZoomingChangeEventHandler); + PubSub_UnsubscribePanningChange(context->pubSub, xf_PanningChangeEventHandler); +#endif + + if (xfc->display) + { + XCloseDisplay(xfc->display); + xfc->display = NULL; + } + + if (xfc->x11event) + { + CloseHandle(xfc->x11event); + xfc->x11event = NULL; + } + + if (xfc->mutex) + { + CloseHandle(xfc->mutex); + xfc->mutex = NULL; + } + + if (xfc->vscreen.monitors) + { + free(xfc->vscreen.monitors); + xfc->vscreen.monitors = NULL; + } + + free(xfc->supportedAtoms); +} + +int RdpClientEntry(RDP_CLIENT_ENTRY_POINTS* pEntryPoints) +{ + pEntryPoints->Version = 1; + pEntryPoints->Size = sizeof(RDP_CLIENT_ENTRY_POINTS_V1); + pEntryPoints->GlobalInit = xfreerdp_client_global_init; + pEntryPoints->GlobalUninit = xfreerdp_client_global_uninit; + pEntryPoints->ContextSize = sizeof(xfContext); + pEntryPoints->ClientNew = xfreerdp_client_new; + pEntryPoints->ClientFree = xfreerdp_client_free; + pEntryPoints->ClientStart = xfreerdp_client_start; + pEntryPoints->ClientStop = xfreerdp_client_stop; + return 0; +} diff --git a/client/X11/xf_client.h b/client/X11/xf_client.h new file mode 100644 index 0000000..e3b00bf --- /dev/null +++ b/client/X11/xf_client.h @@ -0,0 +1,53 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * X11 Client Interface + * + * Copyright 2013 Marc-Andre Moreau + * + * Licensed 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 FREERDP_CLIENT_X11_CLIENT_H +#define FREERDP_CLIENT_X11_CLIENT_H + +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" +{ +#endif + + /** + * Client Interface + */ + + FREERDP_API int RdpClientEntry(RDP_CLIENT_ENTRY_POINTS* pEntryPoints); + +#ifdef __cplusplus +} +#endif + +#endif /* FREERDP_CLIENT_X11_CLIENT_H */ diff --git a/client/X11/xf_cliprdr.c b/client/X11/xf_cliprdr.c new file mode 100644 index 0000000..37ed538 --- /dev/null +++ b/client/X11/xf_cliprdr.c @@ -0,0 +1,1978 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * X11 Clipboard Redirection + * + * Copyright 2010-2011 Vic Lee + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include + +#ifdef WITH_XFIXES +#include +#endif + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "xf_cliprdr.h" + +#define TAG CLIENT_TAG("x11") + +#define MAX_CLIPBOARD_FORMATS 255 + +struct xf_cliprdr_format +{ + Atom atom; + UINT32 formatId; + char* formatName; +}; +typedef struct xf_cliprdr_format xfCliprdrFormat; + +struct xf_clipboard +{ + xfContext* xfc; + rdpChannels* channels; + CliprdrClientContext* context; + + wClipboard* system; + wClipboardDelegate* delegate; + + Window root_window; + Atom clipboard_atom; + Atom property_atom; + + Atom timestamp_property_atom; + Time selection_ownership_timestamp; + + Atom raw_transfer_atom; + Atom raw_format_list_atom; + + int numClientFormats; + xfCliprdrFormat clientFormats[20]; + + int numServerFormats; + CLIPRDR_FORMAT* serverFormats; + + int numTargets; + Atom targets[20]; + + int requestedFormatId; + + BYTE* data; + BYTE* data_raw; + BOOL data_raw_format; + UINT32 data_format_id; + const char* data_format_name; + int data_length; + int data_raw_length; + XSelectionEvent* respond; + + Window owner; + BOOL sync; + + /* INCR mechanism */ + Atom incr_atom; + BOOL incr_starts; + BYTE* incr_data; + int incr_data_length; + + /* XFixes extension */ + int xfixes_event_base; + int xfixes_error_base; + BOOL xfixes_supported; + + /* File clipping */ + BOOL streams_supported; + BOOL file_formats_registered; + UINT32 file_capability_flags; + /* last sent data */ + CLIPRDR_FORMAT* lastSentFormats; + UINT32 lastSentNumFormats; +}; + +static UINT xf_cliprdr_send_client_format_list(xfClipboard* clipboard); +static void xf_cliprdr_set_selection_owner(xfContext* xfc, xfClipboard* clipboard, Time timestamp); + +static void xf_cliprdr_check_owner(xfClipboard* clipboard) +{ + Window owner; + xfContext* xfc = clipboard->xfc; + + if (clipboard->sync) + { + owner = XGetSelectionOwner(xfc->display, clipboard->clipboard_atom); + + if (clipboard->owner != owner) + { + clipboard->owner = owner; + xf_cliprdr_send_client_format_list(clipboard); + } + } +} + +static BOOL xf_cliprdr_is_self_owned(xfClipboard* clipboard) +{ + xfContext* xfc = clipboard->xfc; + return XGetSelectionOwner(xfc->display, clipboard->clipboard_atom) == xfc->drawable; +} + +static void xf_cliprdr_set_raw_transfer_enabled(xfClipboard* clipboard, BOOL enabled) +{ + UINT32 data = enabled; + xfContext* xfc = clipboard->xfc; + XChangeProperty(xfc->display, xfc->drawable, clipboard->raw_transfer_atom, XA_INTEGER, 32, + PropModeReplace, (BYTE*)&data, 1); +} + +static BOOL xf_cliprdr_is_raw_transfer_available(xfClipboard* clipboard) +{ + Atom type; + int format; + int result = 0; + unsigned long length; + unsigned long bytes_left; + UINT32* data = NULL; + UINT32 is_enabled = 0; + Window owner = None; + xfContext* xfc = clipboard->xfc; + owner = XGetSelectionOwner(xfc->display, clipboard->clipboard_atom); + + if (owner != None) + { + result = + XGetWindowProperty(xfc->display, owner, clipboard->raw_transfer_atom, 0, 4, 0, + XA_INTEGER, &type, &format, &length, &bytes_left, (BYTE**)&data); + } + + if (data) + { + is_enabled = *data; + XFree(data); + } + + if ((owner == None) || (owner == xfc->drawable)) + return FALSE; + + if (result != Success) + return FALSE; + + return is_enabled ? TRUE : FALSE; +} + +static BOOL xf_cliprdr_formats_equal(const CLIPRDR_FORMAT* server, const xfCliprdrFormat* client) +{ + if (server->formatName && client->formatName) + { + /* The server may be using short format names while we store them in full form. */ + return (0 == strncmp(server->formatName, client->formatName, strlen(server->formatName))); + } + + if (!server->formatName && !client->formatName) + { + return (server->formatId == client->formatId); + } + + return FALSE; +} + +static xfCliprdrFormat* xf_cliprdr_get_client_format_by_id(xfClipboard* clipboard, UINT32 formatId) +{ + int index; + xfCliprdrFormat* format; + + for (index = 0; index < clipboard->numClientFormats; index++) + { + format = &(clipboard->clientFormats[index]); + + if (format->formatId == formatId) + return format; + } + + return NULL; +} + +static xfCliprdrFormat* xf_cliprdr_get_client_format_by_atom(xfClipboard* clipboard, Atom atom) +{ + int i; + xfCliprdrFormat* format; + + for (i = 0; i < clipboard->numClientFormats; i++) + { + format = &(clipboard->clientFormats[i]); + + if (format->atom == atom) + return format; + } + + return NULL; +} + +static CLIPRDR_FORMAT* xf_cliprdr_get_server_format_by_atom(xfClipboard* clipboard, Atom atom) +{ + int i, j; + xfCliprdrFormat* client_format; + CLIPRDR_FORMAT* server_format; + + for (i = 0; i < clipboard->numClientFormats; i++) + { + client_format = &(clipboard->clientFormats[i]); + + if (client_format->atom == atom) + { + for (j = 0; j < clipboard->numServerFormats; j++) + { + server_format = &(clipboard->serverFormats[j]); + + if (xf_cliprdr_formats_equal(server_format, client_format)) + return server_format; + } + } + } + + return NULL; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT xf_cliprdr_send_data_request(xfClipboard* clipboard, UINT32 formatId) +{ + CLIPRDR_FORMAT_DATA_REQUEST request = { 0 }; + request.requestedFormatId = formatId; + return clipboard->context->ClientFormatDataRequest(clipboard->context, &request); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT xf_cliprdr_send_data_response(xfClipboard* clipboard, const BYTE* data, size_t size) +{ + CLIPRDR_FORMAT_DATA_RESPONSE response = { 0 }; + + /* No request currently pending, do not send a response. */ + if (clipboard->requestedFormatId < 0) + return CHANNEL_RC_OK; + + /* Request handled, reset to invalid */ + clipboard->requestedFormatId = -1; + + response.msgFlags = (data) ? CB_RESPONSE_OK : CB_RESPONSE_FAIL; + response.dataLen = size; + response.requestedFormatData = data; + return clipboard->context->ClientFormatDataResponse(clipboard->context, &response); +} + +static wStream* xf_cliprdr_serialize_server_format_list(xfClipboard* clipboard) +{ + UINT32 i; + UINT32 formatCount; + wStream* s = NULL; + + /* Typical MS Word format list is about 80 bytes long. */ + if (!(s = Stream_New(NULL, 128))) + { + WLog_ERR(TAG, "failed to allocate serialized format list"); + goto error; + } + + /* If present, the last format is always synthetic CF_RAW. Do not include it. */ + formatCount = (clipboard->numServerFormats > 0) ? clipboard->numServerFormats - 1 : 0; + Stream_Write_UINT32(s, formatCount); + + for (i = 0; i < formatCount; i++) + { + CLIPRDR_FORMAT* format = &clipboard->serverFormats[i]; + size_t name_length = format->formatName ? strlen(format->formatName) : 0; + + if (!Stream_EnsureRemainingCapacity(s, sizeof(UINT32) + name_length + 1)) + { + WLog_ERR(TAG, "failed to expand serialized format list"); + goto error; + } + + Stream_Write_UINT32(s, format->formatId); + + if (format->formatName) + Stream_Write(s, format->formatName, name_length); + + Stream_Write_UINT8(s, '\0'); + } + + Stream_SealLength(s); + return s; +error: + Stream_Free(s, TRUE); + return NULL; +} + +static CLIPRDR_FORMAT* xf_cliprdr_parse_server_format_list(BYTE* data, size_t length, + UINT32* numFormats) +{ + UINT32 i; + wStream* s = NULL; + CLIPRDR_FORMAT* formats = NULL; + + if (!(s = Stream_New(data, length))) + { + WLog_ERR(TAG, "failed to allocate stream for parsing serialized format list"); + goto error; + } + + if (Stream_GetRemainingLength(s) < sizeof(UINT32)) + { + WLog_ERR(TAG, "too short serialized format list"); + goto error; + } + + Stream_Read_UINT32(s, *numFormats); + + if (*numFormats > MAX_CLIPBOARD_FORMATS) + { + WLog_ERR(TAG, "unexpectedly large number of formats: %" PRIu32 "", *numFormats); + goto error; + } + + if (!(formats = (CLIPRDR_FORMAT*)calloc(*numFormats, sizeof(CLIPRDR_FORMAT)))) + { + WLog_ERR(TAG, "failed to allocate format list"); + goto error; + } + + for (i = 0; i < *numFormats; i++) + { + const char* formatName = NULL; + size_t formatNameLength = 0; + + if (Stream_GetRemainingLength(s) < sizeof(UINT32)) + { + WLog_ERR(TAG, "unexpected end of serialized format list"); + goto error; + } + + Stream_Read_UINT32(s, formats[i].formatId); + formatName = (const char*)Stream_Pointer(s); + formatNameLength = strnlen(formatName, Stream_GetRemainingLength(s)); + + if (formatNameLength == Stream_GetRemainingLength(s)) + { + WLog_ERR(TAG, "missing terminating null byte, %" PRIuz " bytes left to read", + formatNameLength); + goto error; + } + + formats[i].formatName = strndup(formatName, formatNameLength); + Stream_Seek(s, formatNameLength + 1); + } + + Stream_Free(s, FALSE); + return formats; +error: + Stream_Free(s, FALSE); + free(formats); + *numFormats = 0; + return NULL; +} + +static void xf_cliprdr_free_formats(CLIPRDR_FORMAT* formats, UINT32 numFormats) +{ + UINT32 i; + + for (i = 0; i < numFormats; i++) + { + free(formats[i].formatName); + } + + free(formats); +} + +static CLIPRDR_FORMAT* xf_cliprdr_get_raw_server_formats(xfClipboard* clipboard, UINT32* numFormats) +{ + Atom type = None; + int format = 0; + unsigned long length = 0; + unsigned long remaining; + BYTE* data = NULL; + CLIPRDR_FORMAT* formats = NULL; + xfContext* xfc = clipboard->xfc; + *numFormats = 0; + XGetWindowProperty(xfc->display, clipboard->owner, clipboard->raw_format_list_atom, 0, 4096, + False, clipboard->raw_format_list_atom, &type, &format, &length, &remaining, + &data); + + if (data && length > 0 && format == 8 && type == clipboard->raw_format_list_atom) + { + formats = xf_cliprdr_parse_server_format_list(data, length, numFormats); + } + else + { + WLog_ERR(TAG, + "failed to retrieve raw format list: data=%p, length=%lu, format=%d, type=%lu " + "(expected=%lu)", + (void*)data, length, format, (unsigned long)type, + (unsigned long)clipboard->raw_format_list_atom); + } + + if (data) + XFree(data); + + return formats; +} + +static CLIPRDR_FORMAT* xf_cliprdr_get_formats_from_targets(xfClipboard* clipboard, + UINT32* numFormats) +{ + unsigned long i; + Atom atom; + BYTE* data = NULL; + int format_property; + unsigned long length; + unsigned long bytes_left; + xfCliprdrFormat* format = NULL; + CLIPRDR_FORMAT* formats = NULL; + xfContext* xfc = clipboard->xfc; + *numFormats = 0; + XGetWindowProperty(xfc->display, xfc->drawable, clipboard->property_atom, 0, 200, 0, XA_ATOM, + &atom, &format_property, &length, &bytes_left, &data); + + if (length > 0) + { + if (!data) + { + WLog_ERR(TAG, "XGetWindowProperty set length = %lu but data is NULL", length); + goto out; + } + + if (!(formats = (CLIPRDR_FORMAT*)calloc(length, sizeof(CLIPRDR_FORMAT)))) + { + WLog_ERR(TAG, "failed to allocate %lu CLIPRDR_FORMAT structs", length); + goto out; + } + } + + for (i = 0; i < length; i++) + { + atom = ((Atom*)data)[i]; + format = xf_cliprdr_get_client_format_by_atom(clipboard, atom); + + if (format) + { + formats[*numFormats].formatId = format->formatId; + formats[*numFormats].formatName = _strdup(format->formatName); + *numFormats += 1; + } + } + +out: + + if (data) + XFree(data); + + return formats; +} + +static CLIPRDR_FORMAT* xf_cliprdr_get_client_formats(xfClipboard* clipboard, UINT32* numFormats) +{ + CLIPRDR_FORMAT* formats = NULL; + *numFormats = 0; + + if (xf_cliprdr_is_raw_transfer_available(clipboard)) + { + formats = xf_cliprdr_get_raw_server_formats(clipboard, numFormats); + } + + if (*numFormats == 0) + { + xf_cliprdr_free_formats(formats, *numFormats); + formats = xf_cliprdr_get_formats_from_targets(clipboard, numFormats); + } + + return formats; +} + +static void xf_cliprdr_provide_server_format_list(xfClipboard* clipboard) +{ + wStream* formats = NULL; + xfContext* xfc = clipboard->xfc; + formats = xf_cliprdr_serialize_server_format_list(clipboard); + + if (formats) + { + XChangeProperty(xfc->display, xfc->drawable, clipboard->raw_format_list_atom, + clipboard->raw_format_list_atom, 8, PropModeReplace, Stream_Buffer(formats), + Stream_Length(formats)); + } + else + { + XDeleteProperty(xfc->display, xfc->drawable, clipboard->raw_format_list_atom); + } + + Stream_Free(formats, TRUE); +} + +static BOOL xf_clipboard_format_equal(const CLIPRDR_FORMAT* a, const CLIPRDR_FORMAT* b) +{ + if (a->formatId != b->formatId) + return FALSE; + if (!a->formatName && !b->formatName) + return TRUE; + + return strcmp(a->formatName, b->formatName) == 0; +} +static BOOL xf_clipboard_changed(xfClipboard* clipboard, const CLIPRDR_FORMAT* formats, + UINT32 numFormats) +{ + UINT32 x, y; + if (clipboard->lastSentNumFormats != numFormats) + return TRUE; + + for (x = 0; x < numFormats; x++) + { + const CLIPRDR_FORMAT* cur = &clipboard->lastSentFormats[x]; + BOOL contained = FALSE; + for (y = 0; y < numFormats; y++) + { + if (xf_clipboard_format_equal(cur, &formats[y])) + { + contained = TRUE; + break; + } + } + if (!contained) + return TRUE; + } + + return FALSE; +} + +static void xf_clipboard_formats_free(xfClipboard* clipboard) +{ + xf_cliprdr_free_formats(clipboard->lastSentFormats, clipboard->lastSentNumFormats); + clipboard->lastSentFormats = NULL; + clipboard->lastSentNumFormats = 0; +} +static BOOL xf_clipboard_copy_formats(xfClipboard* clipboard, const CLIPRDR_FORMAT* formats, + UINT32 numFormats) +{ + UINT32 x; + + xf_clipboard_formats_free(clipboard); + clipboard->lastSentFormats = calloc(numFormats, sizeof(CLIPRDR_FORMAT)); + if (!clipboard->lastSentFormats) + return FALSE; + clipboard->lastSentNumFormats = numFormats; + for (x = 0; x < numFormats; x++) + { + CLIPRDR_FORMAT* lcur = &clipboard->lastSentFormats[x]; + const CLIPRDR_FORMAT* cur = &formats[x]; + *lcur = *cur; + if (cur->formatName) + lcur->formatName = _strdup(cur->formatName); + } + return FALSE; +} + +static UINT xf_cliprdr_send_format_list(xfClipboard* clipboard, const CLIPRDR_FORMAT* formats, + UINT32 numFormats) +{ + CLIPRDR_FORMAT_LIST formatList = { 0 }; + formatList.msgFlags = CB_RESPONSE_OK; + formatList.numFormats = numFormats; + formatList.formats = (CLIPRDR_FORMAT*)formats; + formatList.msgType = CB_FORMAT_LIST; + + if (!xf_clipboard_changed(clipboard, formats, numFormats)) + return CHANNEL_RC_OK; + + xf_clipboard_copy_formats(clipboard, formats, numFormats); + /* Ensure all pending requests are answered. */ + xf_cliprdr_send_data_response(clipboard, NULL, 0); + return clipboard->context->ClientFormatList(clipboard->context, &formatList); +} + +static void xf_cliprdr_get_requested_targets(xfClipboard* clipboard) +{ + UINT32 numFormats = 0; + CLIPRDR_FORMAT* formats = NULL; + formats = xf_cliprdr_get_client_formats(clipboard, &numFormats); + xf_cliprdr_send_format_list(clipboard, formats, numFormats); + xf_cliprdr_free_formats(formats, numFormats); +} + +static void xf_cliprdr_process_requested_data(xfClipboard* clipboard, BOOL hasData, BYTE* data, + int size) +{ + BOOL bSuccess; + UINT32 SrcSize; + UINT32 DstSize; + UINT32 srcFormatId; + UINT32 dstFormatId; + BYTE* pDstData = NULL; + xfCliprdrFormat* format; + + if (clipboard->incr_starts && hasData) + return; + + format = xf_cliprdr_get_client_format_by_id(clipboard, clipboard->requestedFormatId); + + if (!hasData || !data || !format) + { + xf_cliprdr_send_data_response(clipboard, NULL, 0); + return; + } + + srcFormatId = 0; + + switch (format->formatId) + { + case CF_RAW: + srcFormatId = CF_RAW; + break; + + case CF_TEXT: + case CF_OEMTEXT: + case CF_UNICODETEXT: + size = strlen((char*)data) + 1; + srcFormatId = ClipboardGetFormatId(clipboard->system, "UTF8_STRING"); + break; + + case CF_DIB: + srcFormatId = ClipboardGetFormatId(clipboard->system, "image/bmp"); + break; + + case CB_FORMAT_HTML: + srcFormatId = ClipboardGetFormatId(clipboard->system, "text/html"); + break; + + case CB_FORMAT_TEXTURILIST: + srcFormatId = ClipboardGetFormatId(clipboard->system, "text/uri-list"); + break; + } + + SrcSize = (UINT32)size; + bSuccess = ClipboardSetData(clipboard->system, srcFormatId, data, SrcSize); + + if (format->formatName) + dstFormatId = ClipboardGetFormatId(clipboard->system, format->formatName); + else + dstFormatId = format->formatId; + + if (bSuccess) + { + DstSize = 0; + pDstData = (BYTE*)ClipboardGetData(clipboard->system, dstFormatId, &DstSize); + } + + if (!pDstData) + { + xf_cliprdr_send_data_response(clipboard, NULL, 0); + return; + } + + /* + * File lists require a bit of postprocessing to convert them from WinPR's FILDESCRIPTOR + * format to CLIPRDR_FILELIST expected by the server. + * + * We check for "FileGroupDescriptorW" format being registered (i.e., nonzero) in order + * to not process CF_RAW as a file list in case WinPR does not support file transfers. + */ + if (dstFormatId && + (dstFormatId == ClipboardGetFormatId(clipboard->system, "FileGroupDescriptorW"))) + { + UINT error = NO_ERROR; + FILEDESCRIPTORW* file_array = (FILEDESCRIPTORW*)pDstData; + UINT32 file_count = DstSize / sizeof(FILEDESCRIPTORW); + pDstData = NULL; + DstSize = 0; + error = cliprdr_serialize_file_list_ex(clipboard->file_capability_flags, file_array, + file_count, &pDstData, &DstSize); + + if (error) + WLog_ERR(TAG, "failed to serialize CLIPRDR_FILELIST: 0x%08X", error); + + free(file_array); + } + + xf_cliprdr_send_data_response(clipboard, pDstData, DstSize); + free(pDstData); +} + +static BOOL xf_cliprdr_get_requested_data(xfClipboard* clipboard, Atom target) +{ + Atom type; + BYTE* data = NULL; + BOOL has_data = FALSE; + int format_property; + unsigned long dummy; + unsigned long length; + unsigned long bytes_left; + xfCliprdrFormat* format; + xfContext* xfc = clipboard->xfc; + + format = xf_cliprdr_get_client_format_by_id(clipboard, clipboard->requestedFormatId); + + if (!format || (format->atom != target)) + { + xf_cliprdr_send_data_response(clipboard, NULL, 0); + return FALSE; + } + + XGetWindowProperty(xfc->display, xfc->drawable, clipboard->property_atom, 0, 0, 0, target, + &type, &format_property, &length, &bytes_left, &data); + + if (data) + { + XFree(data); + data = NULL; + } + + if (bytes_left <= 0 && !clipboard->incr_starts) + { + } + else if (type == clipboard->incr_atom) + { + clipboard->incr_starts = TRUE; + + if (clipboard->incr_data) + { + free(clipboard->incr_data); + clipboard->incr_data = NULL; + } + + clipboard->incr_data_length = 0; + has_data = TRUE; /* data will be followed in PropertyNotify event */ + } + else + { + if (bytes_left <= 0) + { + /* INCR finish */ + data = clipboard->incr_data; + clipboard->incr_data = NULL; + bytes_left = clipboard->incr_data_length; + clipboard->incr_data_length = 0; + clipboard->incr_starts = 0; + has_data = TRUE; + } + else if (XGetWindowProperty(xfc->display, xfc->drawable, clipboard->property_atom, 0, + bytes_left, 0, target, &type, &format_property, &length, &dummy, + &data) == Success) + { + if (clipboard->incr_starts) + { + BYTE* new_data; + bytes_left = length * format_property / 8; + new_data = + (BYTE*)realloc(clipboard->incr_data, clipboard->incr_data_length + bytes_left); + + if (new_data) + { + + clipboard->incr_data = new_data; + CopyMemory(clipboard->incr_data + clipboard->incr_data_length, data, + bytes_left); + clipboard->incr_data_length += bytes_left; + XFree(data); + data = NULL; + } + } + + has_data = TRUE; + } + else + { + } + } + + XDeleteProperty(xfc->display, xfc->drawable, clipboard->property_atom); + xf_cliprdr_process_requested_data(clipboard, has_data, data, bytes_left); + + if (data) + XFree(data); + + return TRUE; +} + +static void xf_cliprdr_append_target(xfClipboard* clipboard, Atom target) +{ + int i; + + if (clipboard->numTargets < 0) + return; + + if ((size_t)clipboard->numTargets >= ARRAYSIZE(clipboard->targets)) + return; + + for (i = 0; i < clipboard->numTargets; i++) + { + if (clipboard->targets[i] == target) + return; + } + + clipboard->targets[clipboard->numTargets++] = target; +} + +static void xf_cliprdr_provide_targets(xfClipboard* clipboard, const XSelectionEvent* respond) +{ + xfContext* xfc = clipboard->xfc; + + if (respond->property != None) + { + XChangeProperty(xfc->display, respond->requestor, respond->property, XA_ATOM, 32, + PropModeReplace, (BYTE*)clipboard->targets, clipboard->numTargets); + } +} + +static void xf_cliprdr_provide_timestamp(xfClipboard* clipboard, const XSelectionEvent* respond) +{ + xfContext* xfc = clipboard->xfc; + + if (respond->property != None) + { + XChangeProperty(xfc->display, respond->requestor, respond->property, XA_INTEGER, 32, + PropModeReplace, (BYTE*)&clipboard->selection_ownership_timestamp, 1); + } +} + +static void xf_cliprdr_provide_data(xfClipboard* clipboard, const XSelectionEvent* respond, + const BYTE* data, UINT32 size) +{ + xfContext* xfc = clipboard->xfc; + + if (respond->property != None) + { + XChangeProperty(xfc->display, respond->requestor, respond->property, respond->target, 8, + PropModeReplace, data, size); + } +} + +static BOOL xf_cliprdr_process_selection_notify(xfClipboard* clipboard, + const XSelectionEvent* xevent) +{ + if (xevent->target == clipboard->targets[1]) + { + if (xevent->property == None) + { + xf_cliprdr_send_client_format_list(clipboard); + } + else + { + xf_cliprdr_get_requested_targets(clipboard); + } + + return TRUE; + } + else + { + return xf_cliprdr_get_requested_data(clipboard, xevent->target); + } +} + +static void xf_cliprdr_clear_cached_data(xfClipboard* clipboard) +{ + if (clipboard->data) + { + free(clipboard->data); + clipboard->data = NULL; + } + + clipboard->data_length = 0; + + if (clipboard->data_raw) + { + free(clipboard->data_raw); + clipboard->data_raw = NULL; + } + + clipboard->data_raw_length = 0; +} + +static BOOL xf_cliprdr_process_selection_request(xfClipboard* clipboard, + const XSelectionRequestEvent* xevent) +{ + int fmt; + Atom type; + UINT32 formatId; + const char* formatName; + XSelectionEvent* respond; + BYTE* data = NULL; + BOOL delayRespond; + BOOL rawTransfer; + BOOL matchingFormat; + unsigned long length; + unsigned long bytes_left; + CLIPRDR_FORMAT* format; + xfContext* xfc = clipboard->xfc; + + if (xevent->owner != xfc->drawable) + return FALSE; + + delayRespond = FALSE; + + if (!(respond = (XSelectionEvent*)calloc(1, sizeof(XSelectionEvent)))) + { + WLog_ERR(TAG, "failed to allocate XEvent data"); + return FALSE; + } + + respond->property = None; + respond->type = SelectionNotify; + respond->display = xevent->display; + respond->requestor = xevent->requestor; + respond->selection = xevent->selection; + respond->target = xevent->target; + respond->time = xevent->time; + + if (xevent->target == clipboard->targets[0]) /* TIMESTAMP */ + { + /* Someone else requests the selection's timestamp */ + respond->property = xevent->property; + xf_cliprdr_provide_timestamp(clipboard, respond); + } + else if (xevent->target == clipboard->targets[1]) /* TARGETS */ + { + /* Someone else requests our available formats */ + respond->property = xevent->property; + xf_cliprdr_provide_targets(clipboard, respond); + } + else + { + format = xf_cliprdr_get_server_format_by_atom(clipboard, xevent->target); + + if (format && (xevent->requestor != xfc->drawable)) + { + formatId = format->formatId; + formatName = format->formatName; + rawTransfer = FALSE; + + if (formatId == CF_RAW) + { + if (XGetWindowProperty(xfc->display, xevent->requestor, clipboard->property_atom, 0, + 4, 0, XA_INTEGER, &type, &fmt, &length, &bytes_left, + &data) != Success) + { + } + + if (data) + { + rawTransfer = TRUE; + CopyMemory(&formatId, data, 4); + XFree(data); + } + } + + /* We can compare format names by pointer value here as they are both + * taken from the same clipboard->serverFormats array */ + matchingFormat = (formatId == clipboard->data_format_id) && + (formatName == clipboard->data_format_name); + + if (matchingFormat && (clipboard->data != 0) && !rawTransfer) + { + /* Cached converted clipboard data available. Send it now */ + respond->property = xevent->property; + xf_cliprdr_provide_data(clipboard, respond, clipboard->data, + clipboard->data_length); + } + else if (matchingFormat && (clipboard->data_raw != 0) && rawTransfer) + { + /* Cached raw clipboard data available. Send it now */ + respond->property = xevent->property; + xf_cliprdr_provide_data(clipboard, respond, clipboard->data_raw, + clipboard->data_raw_length); + } + else if (clipboard->respond) + { + /* duplicate request */ + } + else + { + /** + * Send clipboard data request to the server. + * Response will be postponed after receiving the data + */ + xf_cliprdr_clear_cached_data(clipboard); + respond->property = xevent->property; + clipboard->respond = respond; + clipboard->data_format_id = formatId; + clipboard->data_format_name = formatName; + clipboard->data_raw_format = rawTransfer; + delayRespond = TRUE; + xf_cliprdr_send_data_request(clipboard, formatId); + } + } + } + + if (!delayRespond) + { + union + { + XEvent* ev; + XSelectionEvent* sev; + } conv; + + conv.sev = respond; + XSendEvent(xfc->display, xevent->requestor, 0, 0, conv.ev); + XFlush(xfc->display); + free(respond); + } + + return TRUE; +} + +static BOOL xf_cliprdr_process_selection_clear(xfClipboard* clipboard, + const XSelectionClearEvent* xevent) +{ + xfContext* xfc = clipboard->xfc; + + WINPR_UNUSED(xevent); + + if (xf_cliprdr_is_self_owned(clipboard)) + return FALSE; + + XDeleteProperty(xfc->display, clipboard->root_window, clipboard->property_atom); + return TRUE; +} + +static BOOL xf_cliprdr_process_property_notify(xfClipboard* clipboard, const XPropertyEvent* xevent) +{ + xfCliprdrFormat* format; + xfContext* xfc = NULL; + + if (!clipboard) + return TRUE; + + xfc = clipboard->xfc; + + if (xevent->atom == clipboard->timestamp_property_atom) + { + /* This is the response to the property change we did + * in xf_cliprdr_prepare_to_set_selection_owner. Now + * we can set ourselves as the selection owner. (See + * comments in those functions below.) */ + xf_cliprdr_set_selection_owner(xfc, clipboard, xevent->time); + return TRUE; + } + + if (xevent->atom != clipboard->property_atom) + return FALSE; /* Not cliprdr-related */ + + if (xevent->window == clipboard->root_window) + { + xf_cliprdr_send_client_format_list(clipboard); + } + else if ((xevent->window == xfc->drawable) && (xevent->state == PropertyNewValue) && + clipboard->incr_starts) + { + format = xf_cliprdr_get_client_format_by_id(clipboard, clipboard->requestedFormatId); + + if (format) + xf_cliprdr_get_requested_data(clipboard, format->atom); + } + + return TRUE; +} + +void xf_cliprdr_handle_xevent(xfContext* xfc, const XEvent* event) +{ + xfClipboard* clipboard; + + if (!xfc || !event) + return; + + clipboard = xfc->clipboard; + + if (!clipboard) + return; + +#ifdef WITH_XFIXES + + if (clipboard->xfixes_supported && + event->type == XFixesSelectionNotify + clipboard->xfixes_event_base) + { + XFixesSelectionNotifyEvent* se = (XFixesSelectionNotifyEvent*)event; + + if (se->subtype == XFixesSetSelectionOwnerNotify) + { + if (se->selection != clipboard->clipboard_atom) + return; + + if (XGetSelectionOwner(xfc->display, se->selection) == xfc->drawable) + return; + + clipboard->owner = None; + xf_cliprdr_check_owner(clipboard); + } + + return; + } + +#endif + + switch (event->type) + { + case SelectionNotify: + xf_cliprdr_process_selection_notify(clipboard, &event->xselection); + break; + + case SelectionRequest: + xf_cliprdr_process_selection_request(clipboard, &event->xselectionrequest); + break; + + case SelectionClear: + xf_cliprdr_process_selection_clear(clipboard, &event->xselectionclear); + break; + + case PropertyNotify: + xf_cliprdr_process_property_notify(clipboard, &event->xproperty); + break; + + case FocusIn: + if (!clipboard->xfixes_supported) + { + xf_cliprdr_check_owner(clipboard); + } + + break; + } +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT xf_cliprdr_send_client_capabilities(xfClipboard* clipboard) +{ + CLIPRDR_CAPABILITIES capabilities; + CLIPRDR_GENERAL_CAPABILITY_SET generalCapabilitySet; + capabilities.cCapabilitiesSets = 1; + capabilities.capabilitySets = (CLIPRDR_CAPABILITY_SET*)&(generalCapabilitySet); + generalCapabilitySet.capabilitySetType = CB_CAPSTYPE_GENERAL; + generalCapabilitySet.capabilitySetLength = 12; + generalCapabilitySet.version = CB_CAPS_VERSION_2; + generalCapabilitySet.generalFlags = CB_USE_LONG_FORMAT_NAMES; + + if (clipboard->streams_supported && clipboard->file_formats_registered) + generalCapabilitySet.generalFlags |= + CB_STREAM_FILECLIP_ENABLED | CB_FILECLIP_NO_FILE_PATHS | CB_HUGE_FILE_SUPPORT_ENABLED; + + clipboard->file_capability_flags = generalCapabilitySet.generalFlags; + return clipboard->context->ClientCapabilities(clipboard->context, &capabilities); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT xf_cliprdr_send_client_format_list(xfClipboard* clipboard) +{ + UINT32 i, numFormats; + CLIPRDR_FORMAT* formats = NULL; + xfContext* xfc = clipboard->xfc; + UINT ret; + numFormats = clipboard->numClientFormats; + + if (numFormats) + { + if (!(formats = (CLIPRDR_FORMAT*)calloc(numFormats, sizeof(CLIPRDR_FORMAT)))) + { + WLog_ERR(TAG, "failed to allocate %" PRIu32 " CLIPRDR_FORMAT structs", numFormats); + return CHANNEL_RC_NO_MEMORY; + } + } + + for (i = 0; i < numFormats; i++) + { + formats[i].formatId = clipboard->clientFormats[i].formatId; + formats[i].formatName = clipboard->clientFormats[i].formatName; + } + + ret = xf_cliprdr_send_format_list(clipboard, formats, numFormats); + free(formats); + + if (clipboard->owner && clipboard->owner != xfc->drawable) + { + /* Request the owner for TARGETS, and wait for SelectionNotify event */ + XConvertSelection(xfc->display, clipboard->clipboard_atom, clipboard->targets[1], + clipboard->property_atom, xfc->drawable, CurrentTime); + } + + return ret; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT xf_cliprdr_send_client_format_list_response(xfClipboard* clipboard, BOOL status) +{ + CLIPRDR_FORMAT_LIST_RESPONSE formatListResponse; + formatListResponse.msgType = CB_FORMAT_LIST_RESPONSE; + formatListResponse.msgFlags = status ? CB_RESPONSE_OK : CB_RESPONSE_FAIL; + formatListResponse.dataLen = 0; + return clipboard->context->ClientFormatListResponse(clipboard->context, &formatListResponse); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT xf_cliprdr_monitor_ready(CliprdrClientContext* context, + const CLIPRDR_MONITOR_READY* monitorReady) +{ + xfClipboard* clipboard = (xfClipboard*)context->custom; + UINT ret; + + WINPR_UNUSED(monitorReady); + + if ((ret = xf_cliprdr_send_client_capabilities(clipboard)) != CHANNEL_RC_OK) + return ret; + + if ((ret = xf_cliprdr_send_client_format_list(clipboard)) != CHANNEL_RC_OK) + return ret; + + clipboard->sync = TRUE; + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT xf_cliprdr_server_capabilities(CliprdrClientContext* context, + const CLIPRDR_CAPABILITIES* capabilities) +{ + UINT32 i; + const CLIPRDR_CAPABILITY_SET* caps; + const CLIPRDR_GENERAL_CAPABILITY_SET* generalCaps; + const BYTE* capsPtr = (const BYTE*)capabilities->capabilitySets; + xfClipboard* clipboard = (xfClipboard*)context->custom; + clipboard->streams_supported = FALSE; + + for (i = 0; i < capabilities->cCapabilitiesSets; i++) + { + caps = (const CLIPRDR_CAPABILITY_SET*)capsPtr; + + if (caps->capabilitySetType == CB_CAPSTYPE_GENERAL) + { + generalCaps = (const CLIPRDR_GENERAL_CAPABILITY_SET*)caps; + + if (generalCaps->generalFlags & CB_STREAM_FILECLIP_ENABLED) + { + clipboard->streams_supported = TRUE; + } + } + + capsPtr += caps->capabilitySetLength; + } + + return CHANNEL_RC_OK; +} + +static void xf_cliprdr_prepare_to_set_selection_owner(xfContext* xfc, xfClipboard* clipboard) +{ + /* + * When you're writing to the selection in response to a + * normal X event like a mouse click or keyboard action, you + * get the selection timestamp by copying the time field out + * of that X event. Here, we're doing it on our own + * initiative, so we have to _request_ the X server time. + * + * There isn't a GetServerTime request in the X protocol, so I + * work around it by setting a property on our own window, and + * waiting for a PropertyNotify event to come back telling me + * it's been done - which will have a timestamp we can use. + */ + + /* We have to set the property to some value, but it doesn't + * matter what. Set it to its own name, which we have here + * anyway! */ + Atom value = clipboard->timestamp_property_atom; + + XChangeProperty(xfc->display, xfc->drawable, clipboard->timestamp_property_atom, XA_ATOM, 32, + PropModeReplace, (BYTE*)&value, 1); + XFlush(xfc->display); +} + +static void xf_cliprdr_set_selection_owner(xfContext* xfc, xfClipboard* clipboard, Time timestamp) +{ + /* + * Actually set ourselves up as the selection owner, now that + * we have a timestamp to use. + */ + + clipboard->selection_ownership_timestamp = timestamp; + XSetSelectionOwner(xfc->display, clipboard->clipboard_atom, xfc->drawable, timestamp); + XFlush(xfc->display); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT xf_cliprdr_server_format_list(CliprdrClientContext* context, + const CLIPRDR_FORMAT_LIST* formatList) +{ + UINT32 i; + int j; + xfClipboard* clipboard = (xfClipboard*)context->custom; + xfContext* xfc = clipboard->xfc; + UINT ret; + xf_clipboard_formats_free(clipboard); + xf_cliprdr_clear_cached_data(clipboard); + clipboard->data_format_id = -1; + clipboard->data_format_name = NULL; + + if (clipboard->serverFormats) + { + for (j = 0; j < clipboard->numServerFormats; j++) + free(clipboard->serverFormats[j].formatName); + + free(clipboard->serverFormats); + clipboard->serverFormats = NULL; + clipboard->numServerFormats = 0; + } + + clipboard->numServerFormats = formatList->numFormats + 1; /* +1 for CF_RAW */ + + if (!(clipboard->serverFormats = + (CLIPRDR_FORMAT*)calloc(clipboard->numServerFormats, sizeof(CLIPRDR_FORMAT)))) + { + WLog_ERR(TAG, "failed to allocate %d CLIPRDR_FORMAT structs", clipboard->numServerFormats); + return CHANNEL_RC_NO_MEMORY; + } + + for (i = 0; i < formatList->numFormats; i++) + { + CLIPRDR_FORMAT* format = &formatList->formats[i]; + clipboard->serverFormats[i].formatId = format->formatId; + + if (format->formatName) + { + clipboard->serverFormats[i].formatName = _strdup(format->formatName); + + if (!clipboard->serverFormats[i].formatName) + { + UINT32 k; + + for (k = 0; k < i; k++) + free(clipboard->serverFormats[k].formatName); + + clipboard->numServerFormats = 0; + free(clipboard->serverFormats); + clipboard->serverFormats = NULL; + return CHANNEL_RC_NO_MEMORY; + } + } + } + + /* CF_RAW is always implicitly supported by the server */ + { + CLIPRDR_FORMAT* format = &clipboard->serverFormats[formatList->numFormats]; + format->formatId = CF_RAW; + format->formatName = NULL; + } + xf_cliprdr_provide_server_format_list(clipboard); + clipboard->numTargets = 2; + + for (i = 0; i < formatList->numFormats; i++) + { + CLIPRDR_FORMAT* format = &formatList->formats[i]; + + for (j = 0; j < clipboard->numClientFormats; j++) + { + if (xf_cliprdr_formats_equal(format, &clipboard->clientFormats[j])) + { + xf_cliprdr_append_target(clipboard, clipboard->clientFormats[j].atom); + } + } + } + + ret = xf_cliprdr_send_client_format_list_response(clipboard, TRUE); + if (xfc->remote_app) + xf_cliprdr_set_selection_owner(xfc, clipboard, CurrentTime); + else + xf_cliprdr_prepare_to_set_selection_owner(xfc, clipboard); + return ret; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT +xf_cliprdr_server_format_list_response(CliprdrClientContext* context, + const CLIPRDR_FORMAT_LIST_RESPONSE* formatListResponse) +{ + // xfClipboard* clipboard = (xfClipboard*) context->custom; + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT +xf_cliprdr_server_format_data_request(CliprdrClientContext* context, + const CLIPRDR_FORMAT_DATA_REQUEST* formatDataRequest) +{ + BOOL rawTransfer; + xfCliprdrFormat* format = NULL; + UINT32 formatId = formatDataRequest->requestedFormatId; + xfClipboard* clipboard = (xfClipboard*)context->custom; + xfContext* xfc = clipboard->xfc; + rawTransfer = xf_cliprdr_is_raw_transfer_available(clipboard); + + if (rawTransfer) + { + format = xf_cliprdr_get_client_format_by_id(clipboard, CF_RAW); + XChangeProperty(xfc->display, xfc->drawable, clipboard->property_atom, XA_INTEGER, 32, + PropModeReplace, (BYTE*)&formatId, 1); + } + else + format = xf_cliprdr_get_client_format_by_id(clipboard, formatId); + + clipboard->requestedFormatId = rawTransfer ? CF_RAW : formatId; + if (!format) + return xf_cliprdr_send_data_response(clipboard, NULL, 0); + + XConvertSelection(xfc->display, clipboard->clipboard_atom, format->atom, + clipboard->property_atom, xfc->drawable, CurrentTime); + XFlush(xfc->display); + /* After this point, we expect a SelectionNotify event from the clipboard owner. */ + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT +xf_cliprdr_server_format_data_response(CliprdrClientContext* context, + const CLIPRDR_FORMAT_DATA_RESPONSE* formatDataResponse) +{ + BOOL bSuccess; + BYTE* pDstData; + UINT32 DstSize; + UINT32 SrcSize; + UINT32 srcFormatId; + UINT32 dstFormatId; + BOOL nullTerminated = FALSE; + UINT32 size = formatDataResponse->dataLen; + const BYTE* data = formatDataResponse->requestedFormatData; + xfClipboard* clipboard = (xfClipboard*)context->custom; + xfContext* xfc = clipboard->xfc; + + if (!clipboard->respond) + return CHANNEL_RC_OK; + + xf_cliprdr_clear_cached_data(clipboard); + pDstData = NULL; + DstSize = 0; + srcFormatId = 0; + dstFormatId = 0; + + if (clipboard->data_raw_format) + { + srcFormatId = CF_RAW; + dstFormatId = CF_RAW; + } + else if (clipboard->data_format_name) + { + if (strcmp(clipboard->data_format_name, "HTML Format") == 0) + { + srcFormatId = ClipboardGetFormatId(clipboard->system, "HTML Format"); + dstFormatId = ClipboardGetFormatId(clipboard->system, "text/html"); + nullTerminated = TRUE; + } + + if (strcmp(clipboard->data_format_name, "FileGroupDescriptorW") == 0) + { + srcFormatId = ClipboardGetFormatId(clipboard->system, "FileGroupDescriptorW"); + dstFormatId = ClipboardGetFormatId(clipboard->system, "text/uri-list"); + nullTerminated = FALSE; + } + } + else + { + switch (clipboard->data_format_id) + { + case CF_TEXT: + srcFormatId = CF_TEXT; + dstFormatId = ClipboardGetFormatId(clipboard->system, "UTF8_STRING"); + nullTerminated = TRUE; + break; + + case CF_OEMTEXT: + srcFormatId = CF_OEMTEXT; + dstFormatId = ClipboardGetFormatId(clipboard->system, "UTF8_STRING"); + nullTerminated = TRUE; + break; + + case CF_UNICODETEXT: + srcFormatId = CF_UNICODETEXT; + dstFormatId = ClipboardGetFormatId(clipboard->system, "UTF8_STRING"); + nullTerminated = TRUE; + break; + + case CF_DIB: + srcFormatId = CF_DIB; + dstFormatId = ClipboardGetFormatId(clipboard->system, "image/bmp"); + break; + + default: + break; + } + } + + SrcSize = (UINT32)size; + bSuccess = ClipboardSetData(clipboard->system, srcFormatId, data, SrcSize); + + if (bSuccess) + { + if (SrcSize == 0) + { + WLog_DBG(TAG, "skipping, empty data detected!"); + free(clipboard->respond); + clipboard->respond = NULL; + return CHANNEL_RC_OK; + } + + pDstData = (BYTE*)ClipboardGetData(clipboard->system, dstFormatId, &DstSize); + + if (!pDstData) + { + WLog_WARN(TAG, "failed to get clipboard data in format %s [source format %s]", + ClipboardGetFormatName(clipboard->system, dstFormatId), + ClipboardGetFormatName(clipboard->system, srcFormatId)); + } + + if (nullTerminated && pDstData) + { + BYTE* nullTerminator = memchr(pDstData, '\0', DstSize); + if (nullTerminator) + DstSize = nullTerminator - pDstData; + } + } + + /* Cache converted and original data to avoid doing a possibly costly + * conversion again on subsequent requests */ + clipboard->data = pDstData; + clipboard->data_length = DstSize; + /* We have to copy the original data again, as pSrcData is now owned + * by clipboard->system. Memory allocation failure is not fatal here + * as this is only a cached value. */ + clipboard->data_raw = (BYTE*)malloc(size); + + if (clipboard->data_raw) + { + CopyMemory(clipboard->data_raw, data, size); + clipboard->data_raw_length = size; + } + else + { + WLog_WARN(TAG, "failed to allocate %" PRIu32 " bytes for a copy of raw clipboard data", + size); + } + + xf_cliprdr_provide_data(clipboard, clipboard->respond, pDstData, DstSize); + { + union + { + XEvent* ev; + XSelectionEvent* sev; + } conv; + + conv.sev = clipboard->respond; + + XSendEvent(xfc->display, clipboard->respond->requestor, 0, 0, conv.ev); + XFlush(xfc->display); + } + free(clipboard->respond); + clipboard->respond = NULL; + return CHANNEL_RC_OK; +} + +static UINT +xf_cliprdr_server_file_size_request(xfClipboard* clipboard, + const CLIPRDR_FILE_CONTENTS_REQUEST* fileContentsRequest) +{ + wClipboardFileSizeRequest request = { 0 }; + request.streamId = fileContentsRequest->streamId; + request.listIndex = fileContentsRequest->listIndex; + + if (fileContentsRequest->cbRequested != sizeof(UINT64)) + { + WLog_WARN(TAG, "unexpected FILECONTENTS_SIZE request: %" PRIu32 " bytes", + fileContentsRequest->cbRequested); + } + + return clipboard->delegate->ClientRequestFileSize(clipboard->delegate, &request); +} + +static UINT +xf_cliprdr_server_file_range_request(xfClipboard* clipboard, + const CLIPRDR_FILE_CONTENTS_REQUEST* fileContentsRequest) +{ + wClipboardFileRangeRequest request = { 0 }; + request.streamId = fileContentsRequest->streamId; + request.listIndex = fileContentsRequest->listIndex; + request.nPositionLow = fileContentsRequest->nPositionLow; + request.nPositionHigh = fileContentsRequest->nPositionHigh; + request.cbRequested = fileContentsRequest->cbRequested; + return clipboard->delegate->ClientRequestFileRange(clipboard->delegate, &request); +} + +static UINT +xf_cliprdr_send_file_contents_failure(CliprdrClientContext* context, + const CLIPRDR_FILE_CONTENTS_REQUEST* fileContentsRequest) +{ + CLIPRDR_FILE_CONTENTS_RESPONSE response = { 0 }; + response.msgFlags = CB_RESPONSE_FAIL; + response.streamId = fileContentsRequest->streamId; + return context->ClientFileContentsResponse(context, &response); +} + +static UINT +xf_cliprdr_server_file_contents_request(CliprdrClientContext* context, + const CLIPRDR_FILE_CONTENTS_REQUEST* fileContentsRequest) +{ + UINT error = NO_ERROR; + xfClipboard* clipboard = context->custom; + + /* + * MS-RDPECLIP 2.2.5.3 File Contents Request PDU (CLIPRDR_FILECONTENTS_REQUEST): + * The FILECONTENTS_SIZE and FILECONTENTS_RANGE flags MUST NOT be set at the same time. + */ + if ((fileContentsRequest->dwFlags & (FILECONTENTS_SIZE | FILECONTENTS_RANGE)) == + (FILECONTENTS_SIZE | FILECONTENTS_RANGE)) + { + WLog_ERR(TAG, "invalid CLIPRDR_FILECONTENTS_REQUEST.dwFlags"); + return xf_cliprdr_send_file_contents_failure(context, fileContentsRequest); + } + + if (fileContentsRequest->dwFlags & FILECONTENTS_SIZE) + error = xf_cliprdr_server_file_size_request(clipboard, fileContentsRequest); + + if (fileContentsRequest->dwFlags & FILECONTENTS_RANGE) + error = xf_cliprdr_server_file_range_request(clipboard, fileContentsRequest); + + if (error) + { + WLog_ERR(TAG, "failed to handle CLIPRDR_FILECONTENTS_REQUEST: 0x%08X", error); + return xf_cliprdr_send_file_contents_failure(context, fileContentsRequest); + } + + return CHANNEL_RC_OK; +} + +static UINT xf_cliprdr_clipboard_file_size_success(wClipboardDelegate* delegate, + const wClipboardFileSizeRequest* request, + UINT64 fileSize) +{ + CLIPRDR_FILE_CONTENTS_RESPONSE response = { 0 }; + xfClipboard* clipboard = delegate->custom; + response.msgFlags = CB_RESPONSE_OK; + response.streamId = request->streamId; + response.cbRequested = sizeof(UINT64); + response.requestedData = (BYTE*)&fileSize; + return clipboard->context->ClientFileContentsResponse(clipboard->context, &response); +} + +static UINT xf_cliprdr_clipboard_file_size_failure(wClipboardDelegate* delegate, + const wClipboardFileSizeRequest* request, + UINT errorCode) +{ + CLIPRDR_FILE_CONTENTS_RESPONSE response = { 0 }; + xfClipboard* clipboard = delegate->custom; + WINPR_UNUSED(errorCode); + + response.msgFlags = CB_RESPONSE_FAIL; + response.streamId = request->streamId; + return clipboard->context->ClientFileContentsResponse(clipboard->context, &response); +} + +static UINT xf_cliprdr_clipboard_file_range_success(wClipboardDelegate* delegate, + const wClipboardFileRangeRequest* request, + const BYTE* data, UINT32 size) +{ + CLIPRDR_FILE_CONTENTS_RESPONSE response = { 0 }; + xfClipboard* clipboard = delegate->custom; + response.msgFlags = CB_RESPONSE_OK; + response.streamId = request->streamId; + response.cbRequested = size; + response.requestedData = (BYTE*)data; + return clipboard->context->ClientFileContentsResponse(clipboard->context, &response); +} + +static UINT xf_cliprdr_clipboard_file_range_failure(wClipboardDelegate* delegate, + const wClipboardFileRangeRequest* request, + UINT errorCode) +{ + CLIPRDR_FILE_CONTENTS_RESPONSE response = { 0 }; + xfClipboard* clipboard = delegate->custom; + WINPR_UNUSED(errorCode); + + response.msgFlags = CB_RESPONSE_FAIL; + response.streamId = request->streamId; + return clipboard->context->ClientFileContentsResponse(clipboard->context, &response); +} + +static BOOL xf_cliprdr_clipboard_is_valid_unix_filename(LPCWSTR filename) +{ + LPCWSTR c; + + if (!filename) + return FALSE; + + if (filename[0] == L'\0') + return FALSE; + + /* Reserved characters */ + for (c = filename; *c; ++c) + { + if (*c == L'/') + return FALSE; + } + + return TRUE; +} + +xfClipboard* xf_clipboard_new(xfContext* xfc, BOOL relieveFilenameRestriction) +{ + int i, n = 0; + rdpChannels* channels; + xfClipboard* clipboard; + const char* selectionAtom; + + if (!(clipboard = (xfClipboard*)calloc(1, sizeof(xfClipboard)))) + { + WLog_ERR(TAG, "failed to allocate xfClipboard data"); + return NULL; + } + + xfc->clipboard = clipboard; + clipboard->xfc = xfc; + channels = ((rdpContext*)xfc)->channels; + clipboard->channels = channels; + clipboard->system = ClipboardCreate(); + clipboard->requestedFormatId = -1; + clipboard->root_window = DefaultRootWindow(xfc->display); + selectionAtom = "CLIPBOARD"; + if (xfc->context.settings->XSelectionAtom) + selectionAtom = xfc->context.settings->XSelectionAtom; + clipboard->clipboard_atom = XInternAtom(xfc->display, selectionAtom, FALSE); + + if (clipboard->clipboard_atom == None) + { + WLog_ERR(TAG, "unable to get %s atom", selectionAtom); + goto error; + } + + clipboard->timestamp_property_atom = + XInternAtom(xfc->display, "_FREERDP_TIMESTAMP_PROPERTY", FALSE); + clipboard->property_atom = XInternAtom(xfc->display, "_FREERDP_CLIPRDR", FALSE); + clipboard->raw_transfer_atom = XInternAtom(xfc->display, "_FREERDP_CLIPRDR_RAW", FALSE); + clipboard->raw_format_list_atom = XInternAtom(xfc->display, "_FREERDP_CLIPRDR_FORMATS", FALSE); + xf_cliprdr_set_raw_transfer_enabled(clipboard, TRUE); + XSelectInput(xfc->display, clipboard->root_window, PropertyChangeMask); +#ifdef WITH_XFIXES + + if (XFixesQueryExtension(xfc->display, &clipboard->xfixes_event_base, + &clipboard->xfixes_error_base)) + { + int xfmajor, xfminor; + + if (XFixesQueryVersion(xfc->display, &xfmajor, &xfminor)) + { + XFixesSelectSelectionInput(xfc->display, clipboard->root_window, + clipboard->clipboard_atom, + XFixesSetSelectionOwnerNotifyMask); + clipboard->xfixes_supported = TRUE; + } + else + { + WLog_ERR(TAG, "Error querying X Fixes extension version"); + } + } + else + { + WLog_ERR(TAG, "Error loading X Fixes extension"); + } + +#else + WLog_ERR( + TAG, + "Warning: Using clipboard redirection without XFIXES extension is strongly discouraged!"); +#endif + clipboard->clientFormats[n].atom = XInternAtom(xfc->display, "_FREERDP_RAW", False); + clipboard->clientFormats[n].formatId = CF_RAW; + n++; + clipboard->clientFormats[n].atom = XInternAtom(xfc->display, "UTF8_STRING", False); + clipboard->clientFormats[n].formatId = CF_UNICODETEXT; + n++; + clipboard->clientFormats[n].atom = XA_STRING; + clipboard->clientFormats[n].formatId = CF_TEXT; + n++; + clipboard->clientFormats[n].atom = XInternAtom(xfc->display, "image/png", False); + clipboard->clientFormats[n].formatId = CB_FORMAT_PNG; + n++; + clipboard->clientFormats[n].atom = XInternAtom(xfc->display, "image/jpeg", False); + clipboard->clientFormats[n].formatId = CB_FORMAT_JPEG; + n++; + clipboard->clientFormats[n].atom = XInternAtom(xfc->display, "image/gif", False); + clipboard->clientFormats[n].formatId = CB_FORMAT_GIF; + n++; + clipboard->clientFormats[n].atom = XInternAtom(xfc->display, "image/bmp", False); + clipboard->clientFormats[n].formatId = CF_DIB; + n++; + clipboard->clientFormats[n].atom = XInternAtom(xfc->display, "text/html", False); + clipboard->clientFormats[n].formatId = CB_FORMAT_HTML; + clipboard->clientFormats[n].formatName = _strdup("HTML Format"); + + if (!clipboard->clientFormats[n].formatName) + goto error; + + n++; + + /* + * Existence of registered format IDs for file formats does not guarantee that they are + * in fact supported by wClipboard (as further initialization may have failed after format + * registration). However, they are definitely not supported if there are no registered + * formats. In this case we should not list file formats in TARGETS. + */ + if (ClipboardGetFormatId(clipboard->system, "text/uri-list")) + { + clipboard->file_formats_registered = TRUE; + clipboard->clientFormats[n].atom = XInternAtom(xfc->display, "text/uri-list", False); + clipboard->clientFormats[n].formatId = CB_FORMAT_TEXTURILIST; + clipboard->clientFormats[n].formatName = _strdup("FileGroupDescriptorW"); + + if (!clipboard->clientFormats[n].formatName) + goto error; + + n++; + } + + clipboard->numClientFormats = n; + clipboard->targets[0] = XInternAtom(xfc->display, "TIMESTAMP", FALSE); + clipboard->targets[1] = XInternAtom(xfc->display, "TARGETS", FALSE); + clipboard->numTargets = 2; + clipboard->incr_atom = XInternAtom(xfc->display, "INCR", FALSE); + clipboard->delegate = ClipboardGetDelegate(clipboard->system); + clipboard->delegate->custom = clipboard; + /* TODO: set up a filesystem base path for local URI */ + /* clipboard->delegate->basePath = "file:///tmp/foo/bar/gaga"; */ + clipboard->delegate->ClipboardFileSizeSuccess = xf_cliprdr_clipboard_file_size_success; + clipboard->delegate->ClipboardFileSizeFailure = xf_cliprdr_clipboard_file_size_failure; + clipboard->delegate->ClipboardFileRangeSuccess = xf_cliprdr_clipboard_file_range_success; + clipboard->delegate->ClipboardFileRangeFailure = xf_cliprdr_clipboard_file_range_failure; + + if (relieveFilenameRestriction) + { + WLog_DBG(TAG, "Relieving CLIPRDR filename restriction"); + clipboard->delegate->IsFileNameComponentValid = xf_cliprdr_clipboard_is_valid_unix_filename; + } + + return clipboard; +error: + + for (i = 0; i < n; i++) + free(clipboard->clientFormats[i].formatName); + + ClipboardDestroy(clipboard->system); + free(clipboard); + return NULL; +} + +void xf_clipboard_free(xfClipboard* clipboard) +{ + int i; + + if (!clipboard) + return; + + if (clipboard->serverFormats) + { + for (i = 0; i < clipboard->numServerFormats; i++) + free(clipboard->serverFormats[i].formatName); + + free(clipboard->serverFormats); + clipboard->serverFormats = NULL; + } + + if (clipboard->numClientFormats) + { + for (i = 0; i < clipboard->numClientFormats; i++) + free(clipboard->clientFormats[i].formatName); + } + + ClipboardDestroy(clipboard->system); + xf_clipboard_formats_free(clipboard); + free(clipboard->data); + free(clipboard->data_raw); + free(clipboard->respond); + free(clipboard->incr_data); + free(clipboard); +} + +void xf_cliprdr_init(xfContext* xfc, CliprdrClientContext* cliprdr) +{ + xfc->cliprdr = cliprdr; + xfc->clipboard->context = cliprdr; + cliprdr->custom = (void*)xfc->clipboard; + cliprdr->MonitorReady = xf_cliprdr_monitor_ready; + cliprdr->ServerCapabilities = xf_cliprdr_server_capabilities; + cliprdr->ServerFormatList = xf_cliprdr_server_format_list; + cliprdr->ServerFormatListResponse = xf_cliprdr_server_format_list_response; + cliprdr->ServerFormatDataRequest = xf_cliprdr_server_format_data_request; + cliprdr->ServerFormatDataResponse = xf_cliprdr_server_format_data_response; + cliprdr->ServerFileContentsRequest = xf_cliprdr_server_file_contents_request; +} + +void xf_cliprdr_uninit(xfContext* xfc, CliprdrClientContext* cliprdr) +{ + xfc->cliprdr = NULL; + cliprdr->custom = NULL; + + if (xfc->clipboard) + xfc->clipboard->context = NULL; +} diff --git a/client/X11/xf_cliprdr.h b/client/X11/xf_cliprdr.h new file mode 100644 index 0000000..7f571c4 --- /dev/null +++ b/client/X11/xf_cliprdr.h @@ -0,0 +1,36 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * X11 Clipboard Redirection + * + * Copyright 2010-2011 Vic Lee + * + * Licensed 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 FREERDP_CLIENT_X11_CLIPRDR_H +#define FREERDP_CLIENT_X11_CLIPRDR_H + +#include "xf_client.h" +#include "xfreerdp.h" + +#include + +xfClipboard* xf_clipboard_new(xfContext* xfc, BOOL relieveFilenameRestriction); +void xf_clipboard_free(xfClipboard* clipboard); + +void xf_cliprdr_init(xfContext* xfc, CliprdrClientContext* cliprdr); +void xf_cliprdr_uninit(xfContext* xfc, CliprdrClientContext* cliprdr); + +void xf_cliprdr_handle_xevent(xfContext* xfc, const XEvent* event); + +#endif /* FREERDP_CLIENT_X11_CLIPRDR_H */ diff --git a/client/X11/xf_disp.c b/client/X11/xf_disp.c new file mode 100644 index 0000000..32ddb62 --- /dev/null +++ b/client/X11/xf_disp.c @@ -0,0 +1,480 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * X11 Display Control channel + * + * Copyright 2017 David Fort + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +#ifdef WITH_XRANDR +#include +#include + +#if (RANDR_MAJOR * 100 + RANDR_MINOR) >= 105 +#define USABLE_XRANDR +#endif + +#endif + +#include "xf_disp.h" +#include "xf_monitor.h" + +#define TAG CLIENT_TAG("x11disp") +#define RESIZE_MIN_DELAY 200 /* minimum delay in ms between two resizes */ + +struct _xfDispContext +{ + xfContext* xfc; + DispClientContext* disp; + BOOL haveXRandr; + int eventBase, errorBase; + int lastSentWidth, lastSentHeight; + UINT64 lastSentDate; + int targetWidth, targetHeight; + BOOL activated; + BOOL fullscreen; + UINT16 lastSentDesktopOrientation; + UINT32 lastSentDesktopScaleFactor; + UINT32 lastSentDeviceScaleFactor; +}; + +static UINT xf_disp_sendLayout(DispClientContext* disp, rdpMonitor* monitors, int nmonitors); + +static BOOL xf_disp_settings_changed(xfDispContext* xfDisp) +{ + rdpSettings* settings = xfDisp->xfc->context.settings; + + if (xfDisp->lastSentWidth != xfDisp->targetWidth) + return TRUE; + + if (xfDisp->lastSentHeight != xfDisp->targetHeight) + return TRUE; + + if (xfDisp->lastSentDesktopOrientation != settings->DesktopOrientation) + return TRUE; + + if (xfDisp->lastSentDesktopScaleFactor != settings->DesktopScaleFactor) + return TRUE; + + if (xfDisp->lastSentDeviceScaleFactor != settings->DeviceScaleFactor) + return TRUE; + + if (xfDisp->fullscreen != xfDisp->xfc->fullscreen) + return TRUE; + + return FALSE; +} + +static BOOL xf_update_last_sent(xfDispContext* xfDisp) +{ + rdpSettings* settings = xfDisp->xfc->context.settings; + xfDisp->lastSentWidth = xfDisp->targetWidth; + xfDisp->lastSentHeight = xfDisp->targetHeight; + xfDisp->lastSentDesktopOrientation = settings->DesktopOrientation; + xfDisp->lastSentDesktopScaleFactor = settings->DesktopScaleFactor; + xfDisp->lastSentDeviceScaleFactor = settings->DeviceScaleFactor; + xfDisp->fullscreen = xfDisp->xfc->fullscreen; + return TRUE; +} + +static BOOL xf_disp_sendResize(xfDispContext* xfDisp) +{ + DISPLAY_CONTROL_MONITOR_LAYOUT layout = { 0 }; + xfContext* xfc; + rdpSettings* settings; + + if (!xfDisp || !xfDisp->xfc) + return FALSE; + + xfc = xfDisp->xfc; + settings = xfc->context.settings; + + if (!settings) + return FALSE; + + if (!xfDisp->activated || !xfDisp->disp) + return TRUE; + + if (GetTickCount64() - xfDisp->lastSentDate < RESIZE_MIN_DELAY) + return TRUE; + + if (!xf_disp_settings_changed(xfDisp)) + return TRUE; + + xfDisp->lastSentDate = GetTickCount64(); + if (xfc->fullscreen && (settings->MonitorCount > 0)) + { + if (xf_disp_sendLayout(xfDisp->disp, settings->MonitorDefArray, settings->MonitorCount) != + CHANNEL_RC_OK) + return FALSE; + } + else + { + layout.Flags = DISPLAY_CONTROL_MONITOR_PRIMARY; + layout.Top = layout.Left = 0; + layout.Width = xfDisp->targetWidth; + layout.Height = xfDisp->targetHeight; + layout.Orientation = settings->DesktopOrientation; + layout.DesktopScaleFactor = settings->DesktopScaleFactor; + layout.DeviceScaleFactor = settings->DeviceScaleFactor; + layout.PhysicalWidth = xfDisp->targetWidth / 75 * 25.4f; + layout.PhysicalHeight = xfDisp->targetHeight / 75 * 25.4f; + + if (IFCALLRESULT(CHANNEL_RC_OK, xfDisp->disp->SendMonitorLayout, xfDisp->disp, 1, + &layout) != CHANNEL_RC_OK) + return FALSE; + } + + return xf_update_last_sent(xfDisp); +} + +static BOOL xf_disp_queueResize(xfDispContext* xfDisp, UINT32 width, UINT32 height) +{ + if ((xfDisp->targetWidth == width) && (xfDisp->targetHeight == height)) + return TRUE; + xfDisp->targetWidth = width; + xfDisp->targetHeight = height; + xfDisp->lastSentDate = GetTickCount64(); + return xf_disp_sendResize(xfDisp); +} + +static BOOL xf_disp_set_window_resizable(xfDispContext* xfDisp) +{ + XSizeHints* size_hints; + + if (!(size_hints = XAllocSizeHints())) + return FALSE; + + size_hints->flags = PMinSize | PMaxSize | PWinGravity; + size_hints->win_gravity = NorthWestGravity; + size_hints->min_width = size_hints->min_height = 320; + size_hints->max_width = size_hints->max_height = 8192; + + if (xfDisp->xfc->window) + XSetWMNormalHints(xfDisp->xfc->display, xfDisp->xfc->window->handle, size_hints); + + XFree(size_hints); + return TRUE; +} + +static BOOL xf_disp_check_context(void* context, xfContext** ppXfc, xfDispContext** ppXfDisp, + rdpSettings** ppSettings) +{ + xfContext* xfc; + + if (!context) + return FALSE; + + xfc = (xfContext*)context; + + if (!(xfc->xfDisp)) + return FALSE; + + if (!xfc->context.settings) + return FALSE; + + *ppXfc = xfc; + *ppXfDisp = xfc->xfDisp; + *ppSettings = xfc->context.settings; + return TRUE; +} + +static void xf_disp_OnActivated(void* context, ActivatedEventArgs* e) +{ + xfContext* xfc; + xfDispContext* xfDisp; + rdpSettings* settings; + + if (!xf_disp_check_context(context, &xfc, &xfDisp, &settings)) + return; + + if (xfDisp->activated && !xfc->fullscreen) + { + xf_disp_set_window_resizable(xfDisp); + + if (e->firstActivation) + return; + + xf_disp_sendResize(xfDisp); + } +} + +static void xf_disp_OnGraphicsReset(void* context, GraphicsResetEventArgs* e) +{ + xfContext* xfc; + xfDispContext* xfDisp; + rdpSettings* settings; + + WINPR_UNUSED(e); + + if (!xf_disp_check_context(context, &xfc, &xfDisp, &settings)) + return; + + if (xfDisp->activated && !settings->Fullscreen) + { + xf_disp_set_window_resizable(xfDisp); + xf_disp_sendResize(xfDisp); + } +} + +static void xf_disp_OnTimer(void* context, TimerEventArgs* e) +{ + xfContext* xfc; + xfDispContext* xfDisp; + rdpSettings* settings; + + WINPR_UNUSED(e); + + if (!xf_disp_check_context(context, &xfc, &xfDisp, &settings)) + return; + + if (!xfDisp->activated || xfc->fullscreen) + return; + + xf_disp_sendResize(xfDisp); +} + +static void xf_disp_OnWindowStateChange(void* context, const WindowStateChangeEventArgs* e) +{ + xfContext* xfc; + xfDispContext* xfDisp; + rdpSettings* settings; + + WINPR_UNUSED(e); + + if (!xf_disp_check_context(context, &xfc, &xfDisp, &settings)) + return; + + if (!xfDisp->activated || !xfc->fullscreen) + return; + + xf_disp_sendResize(xfDisp); +} + +xfDispContext* xf_disp_new(xfContext* xfc) +{ + xfDispContext* ret; + + if (!xfc || !xfc->context.settings || !xfc->context.pubSub) + return NULL; + + ret = calloc(1, sizeof(xfDispContext)); + + if (!ret) + return NULL; + + ret->xfc = xfc; +#ifdef USABLE_XRANDR + + if (XRRQueryExtension(xfc->display, &ret->eventBase, &ret->errorBase)) + { + ret->haveXRandr = TRUE; + } + +#endif + ret->lastSentWidth = ret->targetWidth = xfc->context.settings->DesktopWidth; + ret->lastSentHeight = ret->targetHeight = xfc->context.settings->DesktopHeight; + PubSub_SubscribeActivated(xfc->context.pubSub, xf_disp_OnActivated); + PubSub_SubscribeGraphicsReset(xfc->context.pubSub, xf_disp_OnGraphicsReset); + PubSub_SubscribeTimer(xfc->context.pubSub, xf_disp_OnTimer); + PubSub_SubscribeWindowStateChange(xfc->context.pubSub, xf_disp_OnWindowStateChange); + return ret; +} + +void xf_disp_free(xfDispContext* disp) +{ + if (!disp) + return; + + if (disp->xfc) + { + PubSub_UnsubscribeActivated(disp->xfc->context.pubSub, xf_disp_OnActivated); + PubSub_UnsubscribeGraphicsReset(disp->xfc->context.pubSub, xf_disp_OnGraphicsReset); + PubSub_UnsubscribeTimer(disp->xfc->context.pubSub, xf_disp_OnTimer); + PubSub_UnsubscribeWindowStateChange(disp->xfc->context.pubSub, xf_disp_OnWindowStateChange); + } + + free(disp); +} + +UINT xf_disp_sendLayout(DispClientContext* disp, rdpMonitor* monitors, int nmonitors) +{ + UINT ret = CHANNEL_RC_OK; + DISPLAY_CONTROL_MONITOR_LAYOUT* layouts; + int i; + xfDispContext* xfDisp = (xfDispContext*)disp->custom; + rdpSettings* settings = xfDisp->xfc->context.settings; + layouts = calloc(nmonitors, sizeof(DISPLAY_CONTROL_MONITOR_LAYOUT)); + + if (!layouts) + return CHANNEL_RC_NO_MEMORY; + + for (i = 0; i < nmonitors; i++) + { + layouts[i].Flags = (monitors[i].is_primary ? DISPLAY_CONTROL_MONITOR_PRIMARY : 0); + layouts[i].Left = monitors[i].x; + layouts[i].Top = monitors[i].y; + layouts[i].Width = monitors[i].width; + layouts[i].Height = monitors[i].height; + layouts[i].Orientation = ORIENTATION_LANDSCAPE; + layouts[i].PhysicalWidth = monitors[i].attributes.physicalWidth; + layouts[i].PhysicalHeight = monitors[i].attributes.physicalHeight; + + switch (monitors[i].attributes.orientation) + { + case 90: + layouts[i].Orientation = ORIENTATION_PORTRAIT; + break; + + case 180: + layouts[i].Orientation = ORIENTATION_LANDSCAPE_FLIPPED; + break; + + case 270: + layouts[i].Orientation = ORIENTATION_PORTRAIT_FLIPPED; + break; + + case 0: + default: + /* MS-RDPEDISP - 2.2.2.2.1: + * Orientation (4 bytes): A 32-bit unsigned integer that specifies the + * orientation of the monitor in degrees. Valid values are 0, 90, 180 + * or 270 + * + * So we default to ORIENTATION_LANDSCAPE + */ + layouts[i].Orientation = ORIENTATION_LANDSCAPE; + break; + } + + layouts[i].DesktopScaleFactor = settings->DesktopScaleFactor; + layouts[i].DeviceScaleFactor = settings->DeviceScaleFactor; + } + + ret = IFCALLRESULT(CHANNEL_RC_OK, disp->SendMonitorLayout, disp, nmonitors, layouts); + free(layouts); + return ret; +} + +BOOL xf_disp_handle_xevent(xfContext* xfc, const XEvent* event) +{ + xfDispContext* xfDisp; + rdpSettings* settings; + UINT32 maxWidth, maxHeight; + + if (!xfc || !event) + return FALSE; + + xfDisp = xfc->xfDisp; + + if (!xfDisp) + return FALSE; + + settings = xfc->context.settings; + + if (!settings) + return FALSE; + + if (!xfDisp->haveXRandr || !xfDisp->disp) + return TRUE; + +#ifdef USABLE_XRANDR + + if (event->type != xfDisp->eventBase + RRScreenChangeNotify) + return TRUE; + +#endif + xf_detect_monitors(xfc, &maxWidth, &maxHeight); + return xf_disp_sendLayout(xfDisp->disp, settings->MonitorDefArray, settings->MonitorCount) == + CHANNEL_RC_OK; +} + +BOOL xf_disp_handle_configureNotify(xfContext* xfc, int width, int height) +{ + xfDispContext* xfDisp; + + if (!xfc) + return FALSE; + + xfDisp = xfc->xfDisp; + + if (!xfDisp) + return FALSE; + + return xf_disp_queueResize(xfDisp, width, height); +} + +static UINT xf_DisplayControlCaps(DispClientContext* disp, UINT32 maxNumMonitors, + UINT32 maxMonitorAreaFactorA, UINT32 maxMonitorAreaFactorB) +{ + /* we're called only if dynamic resolution update is activated */ + xfDispContext* xfDisp = (xfDispContext*)disp->custom; + rdpSettings* settings = xfDisp->xfc->context.settings; + WLog_DBG(TAG, + "DisplayControlCapsPdu: MaxNumMonitors: %" PRIu32 " MaxMonitorAreaFactorA: %" PRIu32 + " MaxMonitorAreaFactorB: %" PRIu32 "", + maxNumMonitors, maxMonitorAreaFactorA, maxMonitorAreaFactorB); + xfDisp->activated = TRUE; + + if (settings->Fullscreen) + return CHANNEL_RC_OK; + + WLog_DBG(TAG, "DisplayControlCapsPdu: setting the window as resizable"); + return xf_disp_set_window_resizable(xfDisp) ? CHANNEL_RC_OK : CHANNEL_RC_NO_MEMORY; +} + +BOOL xf_disp_init(xfDispContext* xfDisp, DispClientContext* disp) +{ + rdpSettings* settings; + + if (!xfDisp || !xfDisp->xfc || !disp) + return FALSE; + + settings = xfDisp->xfc->context.settings; + + if (!settings) + return FALSE; + + xfDisp->disp = disp; + disp->custom = (void*)xfDisp; + + if (settings->DynamicResolutionUpdate) + { + disp->DisplayControlCaps = xf_DisplayControlCaps; +#ifdef USABLE_XRANDR + + if (settings->Fullscreen) + { + /* ask X11 to notify us of screen changes */ + XRRSelectInput(xfDisp->xfc->display, DefaultRootWindow(xfDisp->xfc->display), + RRScreenChangeNotifyMask); + } + +#endif + } + + return TRUE; +} + +BOOL xf_disp_uninit(xfDispContext* xfDisp, DispClientContext* disp) +{ + if (!xfDisp || !disp) + return FALSE; + + xfDisp->disp = NULL; + return TRUE; +} diff --git a/client/X11/xf_disp.h b/client/X11/xf_disp.h new file mode 100644 index 0000000..9062501 --- /dev/null +++ b/client/X11/xf_disp.h @@ -0,0 +1,37 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * X11 Display Control channel + * + * Copyright 2017 David Fort + * + * Licensed 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 FREERDP_CLIENT_X11_DISP_H +#define FREERDP_CLIENT_X11_DISP_H + +#include +#include + +#include "xf_client.h" +#include "xfreerdp.h" + +FREERDP_API BOOL xf_disp_init(xfDispContext* xfDisp, DispClientContext* disp); +FREERDP_API BOOL xf_disp_uninit(xfDispContext* xfDisp, DispClientContext* disp); + +xfDispContext* xf_disp_new(xfContext* xfc); +void xf_disp_free(xfDispContext* disp); +BOOL xf_disp_handle_xevent(xfContext* xfc, const XEvent* event); +BOOL xf_disp_handle_configureNotify(xfContext* xfc, int width, int height); +void xf_disp_resized(xfDispContext* disp); + +#endif /* FREERDP_CLIENT_X11_DISP_H */ diff --git a/client/X11/xf_event.c b/client/X11/xf_event.c new file mode 100644 index 0000000..bdcdcb5 --- /dev/null +++ b/client/X11/xf_event.c @@ -0,0 +1,1152 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * X11 Event Handling + * + * Copyright 2011 Marc-Andre Moreau + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include + +#include + +#include +#include + +#include "xf_rail.h" +#include "xf_window.h" +#include "xf_cliprdr.h" +#include "xf_disp.h" +#include "xf_input.h" +#include "xf_gfx.h" +#include "xf_graphics.h" + +#include "xf_event.h" +#include "xf_input.h" + +#define TAG CLIENT_TAG("x11") + +#define CLAMP_COORDINATES(x, y) \ + if (x < 0) \ + x = 0; \ + if (y < 0) \ + y = 0 + +static const char* x11_event_string(int event) +{ + switch (event) + { + case KeyPress: + return "KeyPress"; + + case KeyRelease: + return "KeyRelease"; + + case ButtonPress: + return "ButtonPress"; + + case ButtonRelease: + return "ButtonRelease"; + + case MotionNotify: + return "MotionNotify"; + + case EnterNotify: + return "EnterNotify"; + + case LeaveNotify: + return "LeaveNotify"; + + case FocusIn: + return "FocusIn"; + + case FocusOut: + return "FocusOut"; + + case KeymapNotify: + return "KeymapNotify"; + + case Expose: + return "Expose"; + + case GraphicsExpose: + return "GraphicsExpose"; + + case NoExpose: + return "NoExpose"; + + case VisibilityNotify: + return "VisibilityNotify"; + + case CreateNotify: + return "CreateNotify"; + + case DestroyNotify: + return "DestroyNotify"; + + case UnmapNotify: + return "UnmapNotify"; + + case MapNotify: + return "MapNotify"; + + case MapRequest: + return "MapRequest"; + + case ReparentNotify: + return "ReparentNotify"; + + case ConfigureNotify: + return "ConfigureNotify"; + + case ConfigureRequest: + return "ConfigureRequest"; + + case GravityNotify: + return "GravityNotify"; + + case ResizeRequest: + return "ResizeRequest"; + + case CirculateNotify: + return "CirculateNotify"; + + case CirculateRequest: + return "CirculateRequest"; + + case PropertyNotify: + return "PropertyNotify"; + + case SelectionClear: + return "SelectionClear"; + + case SelectionRequest: + return "SelectionRequest"; + + case SelectionNotify: + return "SelectionNotify"; + + case ColormapNotify: + return "ColormapNotify"; + + case ClientMessage: + return "ClientMessage"; + + case MappingNotify: + return "MappingNotify"; + + case GenericEvent: + return "GenericEvent"; + + default: + return "UNKNOWN"; + }; +} + +#ifdef WITH_DEBUG_X11 +#define DEBUG_X11(...) WLog_DBG(TAG, __VA_ARGS__) +#else +#define DEBUG_X11(...) \ + do \ + { \ + } while (0) +#endif + +BOOL xf_event_action_script_init(xfContext* xfc) +{ + char* xevent; + FILE* actionScript; + char buffer[1024] = { 0 }; + char command[1024] = { 0 }; + xfc->xevents = ArrayList_New(TRUE); + + if (!xfc->xevents) + return FALSE; + + ArrayList_Object(xfc->xevents)->fnObjectFree = free; + sprintf_s(command, sizeof(command), "%s xevent", xfc->context.settings->ActionScript); + actionScript = popen(command, "r"); + + if (!actionScript) + return FALSE; + + while (fgets(buffer, sizeof(buffer), actionScript)) + { + char* context = NULL; + strtok_s(buffer, "\n", &context); + xevent = _strdup(buffer); + + if (!xevent || ArrayList_Add(xfc->xevents, xevent) < 0) + { + pclose(actionScript); + ArrayList_Free(xfc->xevents); + xfc->xevents = NULL; + return FALSE; + } + } + + pclose(actionScript); + return TRUE; +} + +void xf_event_action_script_free(xfContext* xfc) +{ + if (xfc->xevents) + { + ArrayList_Free(xfc->xevents); + xfc->xevents = NULL; + } +} + +static BOOL xf_event_execute_action_script(xfContext* xfc, const XEvent* event) +{ + int index; + int count; + char* name; + FILE* actionScript; + BOOL match = FALSE; + const char* xeventName; + char buffer[1024] = { 0 }; + char command[1024] = { 0 }; + + if (!xfc->actionScriptExists || !xfc->xevents || !xfc->window) + return FALSE; + + if (event->type > LASTEvent) + return FALSE; + + xeventName = x11_event_string(event->type); + count = ArrayList_Count(xfc->xevents); + + for (index = 0; index < count; index++) + { + name = (char*)ArrayList_GetItem(xfc->xevents, index); + + if (_stricmp(name, xeventName) == 0) + { + match = TRUE; + break; + } + } + + if (!match) + return FALSE; + + sprintf_s(command, sizeof(command), "%s xevent %s %lu", xfc->context.settings->ActionScript, + xeventName, (unsigned long)xfc->window->handle); + actionScript = popen(command, "r"); + + if (!actionScript) + return FALSE; + + while (fgets(buffer, sizeof(buffer), actionScript)) + { + char* context = NULL; + strtok_s(buffer, "\n", &context); + } + + pclose(actionScript); + return TRUE; +} + +void xf_adjust_coordinates_to_screen(xfContext* xfc, UINT32* x, UINT32* y) +{ + rdpSettings* settings; + INT64 tx, ty; + + if (!xfc || !xfc->context.settings || !y || !x) + return; + + settings = xfc->context.settings; + tx = *x; + ty = *y; + if (!xfc->remote_app) + { +#ifdef WITH_XRENDER + + if (xf_picture_transform_required(xfc)) + { + double xScalingFactor = xfc->scaledWidth / (double)settings->DesktopWidth; + double yScalingFactor = xfc->scaledHeight / (double)settings->DesktopHeight; + tx = ((tx + xfc->offset_x) * xScalingFactor); + ty = ((ty + xfc->offset_y) * yScalingFactor); + } + +#endif + } + + CLAMP_COORDINATES(tx, ty); + *x = tx; + *y = ty; +} + +void xf_event_adjust_coordinates(xfContext* xfc, int* x, int* y) +{ + rdpSettings* settings; + + if (!xfc || !xfc->context.settings || !y || !x) + return; + + settings = xfc->context.settings; + + if (!xfc->remote_app) + { +#ifdef WITH_XRENDER + + if (xf_picture_transform_required(xfc)) + { + double xScalingFactor = settings->DesktopWidth / (double)xfc->scaledWidth; + double yScalingFactor = settings->DesktopHeight / (double)xfc->scaledHeight; + *x = (int)((*x - xfc->offset_x) * xScalingFactor); + *y = (int)((*y - xfc->offset_y) * yScalingFactor); + } + +#endif + } + + CLAMP_COORDINATES(*x, *y); +} +static BOOL xf_event_Expose(xfContext* xfc, const XExposeEvent* event, BOOL app) +{ + int x, y; + int w, h; + rdpSettings* settings = xfc->context.settings; + + if (!app && (settings->SmartSizing || settings->MultiTouchGestures)) + { + x = 0; + y = 0; + w = settings->DesktopWidth; + h = settings->DesktopHeight; + } + else + { + x = event->x; + y = event->y; + w = event->width; + h = event->height; + } + + if (!app) + { + if (xfc->context.gdi->gfx) + { + xf_OutputExpose(xfc, x, y, w, h); + return TRUE; + } + xf_draw_screen(xfc, x, y, w, h); + } + else + { + xfAppWindow* appWindow; + appWindow = xf_AppWindowFromX11Window(xfc, event->window); + + if (appWindow) + { + xf_UpdateWindowArea(xfc, appWindow, x, y, w, h); + } + } + + return TRUE; +} + +static BOOL xf_event_VisibilityNotify(xfContext* xfc, const XVisibilityEvent* event, BOOL app) +{ + WINPR_UNUSED(app); + xfc->unobscured = event->state == VisibilityUnobscured; + return TRUE; +} + +BOOL xf_generic_MotionNotify(xfContext* xfc, int x, int y, int state, Window window, BOOL app) +{ + rdpInput* input; + Window childWindow; + input = xfc->context.input; + + if (!xfc->context.settings->MouseMotion) + { + if ((state & (Button1Mask | Button2Mask | Button3Mask)) == 0) + return TRUE; + } + + if (app) + { + /* make sure window exists */ + if (!xf_AppWindowFromX11Window(xfc, window)) + return TRUE; + + /* Translate to desktop coordinates */ + XTranslateCoordinates(xfc->display, window, RootWindowOfScreen(xfc->screen), x, y, &x, &y, + &childWindow); + } + + xf_event_adjust_coordinates(xfc, &x, &y); + freerdp_input_send_mouse_event(input, PTR_FLAGS_MOVE, x, y); + + if (xfc->fullscreen && !app) + { + XSetInputFocus(xfc->display, xfc->window->handle, RevertToPointerRoot, CurrentTime); + } + + return TRUE; +} +static BOOL xf_event_MotionNotify(xfContext* xfc, const XMotionEvent* event, BOOL app) +{ + if (xfc->window) + xf_floatbar_set_root_y(xfc->window->floatbar, event->y); + + if (xfc->use_xinput) + return TRUE; + + return xf_generic_MotionNotify(xfc, event->x, event->y, event->state, event->window, app); +} + +BOOL xf_generic_ButtonEvent(xfContext* xfc, int x, int y, int button, Window window, BOOL app, + BOOL down) +{ + UINT16 flags = 0; + rdpInput* input; + Window childWindow; + size_t i; + + for (i = 0; i < ARRAYSIZE(xfc->button_map); i++) + { + const button_map* cur = &xfc->button_map[i]; + + if (cur->button == button) + { + flags = cur->flags; + break; + } + } + + input = xfc->context.input; + + if (flags != 0) + { + if (flags & (PTR_FLAGS_WHEEL | PTR_FLAGS_HWHEEL)) + { + if (down) + freerdp_input_send_mouse_event(input, flags, 0, 0); + } + else + { + BOOL extended = FALSE; + + if (flags & (PTR_XFLAGS_BUTTON1 | PTR_XFLAGS_BUTTON2)) + { + extended = TRUE; + + if (down) + flags |= PTR_XFLAGS_DOWN; + } + else if (flags & (PTR_FLAGS_BUTTON1 | PTR_FLAGS_BUTTON2 | PTR_FLAGS_BUTTON3)) + { + if (down) + flags |= PTR_FLAGS_DOWN; + } + + if (app) + { + /* make sure window exists */ + if (!xf_AppWindowFromX11Window(xfc, window)) + return TRUE; + + /* Translate to desktop coordinates */ + XTranslateCoordinates(xfc->display, window, RootWindowOfScreen(xfc->screen), x, y, + &x, &y, &childWindow); + } + + xf_event_adjust_coordinates(xfc, &x, &y); + + if (extended) + freerdp_input_send_extended_mouse_event(input, flags, x, y); + else + freerdp_input_send_mouse_event(input, flags, x, y); + } + } + + return TRUE; +} +static BOOL xf_event_ButtonPress(xfContext* xfc, const XButtonEvent* event, BOOL app) +{ + if (xfc->use_xinput) + return TRUE; + + return xf_generic_ButtonEvent(xfc, event->x, event->y, event->button, event->window, app, TRUE); +} + +static BOOL xf_event_ButtonRelease(xfContext* xfc, const XButtonEvent* event, BOOL app) +{ + if (xfc->use_xinput) + return TRUE; + + return xf_generic_ButtonEvent(xfc, event->x, event->y, event->button, event->window, app, + FALSE); +} + +static BOOL xf_event_KeyPress(xfContext* xfc, const XKeyEvent* event, BOOL app) +{ + KeySym keysym; + char str[256]; + WINPR_UNUSED(app); + XLookupString((XKeyEvent*)event, str, sizeof(str), &keysym, NULL); + xf_keyboard_key_press(xfc, event->keycode, keysym); + return TRUE; +} + +static BOOL xf_event_KeyRelease(xfContext* xfc, const XKeyEvent* event, BOOL app) +{ + KeySym keysym; + char str[256]; + WINPR_UNUSED(app); + XLookupString((XKeyEvent*)event, str, sizeof(str), &keysym, NULL); + xf_keyboard_key_release(xfc, event->keycode, keysym); + return TRUE; +} + +static BOOL xf_event_FocusIn(xfContext* xfc, const XFocusInEvent* event, BOOL app) +{ + if (event->mode == NotifyGrab) + return TRUE; + + xfc->focused = TRUE; + + if (xfc->mouse_active && !app) + { + if (!xfc->window) + return FALSE; + + XGrabKeyboard(xfc->display, xfc->window->handle, TRUE, GrabModeAsync, GrabModeAsync, + CurrentTime); + } + + /* Release all keys, should already be done at FocusOut but might be missed + * if the WM decided to use an alternate event order */ + xf_keyboard_release_all_keypress(xfc); + xf_pointer_update_scale(xfc); + + if (app) + { + xfAppWindow* appWindow; + xf_rail_send_activate(xfc, event->window, TRUE); + appWindow = xf_AppWindowFromX11Window(xfc, event->window); + + /* Update the server with any window changes that occurred while the window was not focused. + */ + if (appWindow) + { + xf_rail_adjust_position(xfc, appWindow); + } + } + + xf_keyboard_focus_in(xfc); + return TRUE; +} + +static BOOL xf_event_FocusOut(xfContext* xfc, const XFocusOutEvent* event, BOOL app) +{ + if (event->mode == NotifyUngrab) + return TRUE; + + xfc->focused = FALSE; + + if (event->mode == NotifyWhileGrabbed) + XUngrabKeyboard(xfc->display, CurrentTime); + + xf_keyboard_release_all_keypress(xfc); + + if (app) + xf_rail_send_activate(xfc, event->window, FALSE); + + return TRUE; +} + +static BOOL xf_event_MappingNotify(xfContext* xfc, const XMappingEvent* event, BOOL app) +{ + WINPR_UNUSED(app); + + if (event->request == MappingModifier) + { + if (xfc->modifierMap) + XFreeModifiermap(xfc->modifierMap); + + xfc->modifierMap = XGetModifierMapping(xfc->display); + } + + return TRUE; +} + +static BOOL xf_event_ClientMessage(xfContext* xfc, const XClientMessageEvent* event, BOOL app) +{ + if ((event->message_type == xfc->WM_PROTOCOLS) && + ((Atom)event->data.l[0] == xfc->WM_DELETE_WINDOW)) + { + if (app) + { + xfAppWindow* appWindow; + appWindow = xf_AppWindowFromX11Window(xfc, event->window); + + if (appWindow) + { + xf_rail_send_client_system_command(xfc, appWindow->windowId, SC_CLOSE); + } + + return TRUE; + } + else + { + DEBUG_X11("Main window closed"); + return FALSE; + } + } + + return TRUE; +} + +static BOOL xf_event_EnterNotify(xfContext* xfc, const XEnterWindowEvent* event, BOOL app) +{ + if (!app) + { + if (!xfc->window) + return FALSE; + + xfc->mouse_active = TRUE; + + if (xfc->fullscreen) + XSetInputFocus(xfc->display, xfc->window->handle, RevertToPointerRoot, CurrentTime); + + if (xfc->focused) + XGrabKeyboard(xfc->display, xfc->window->handle, TRUE, GrabModeAsync, GrabModeAsync, + CurrentTime); + } + else + { + xfAppWindow* appWindow = xf_AppWindowFromX11Window(xfc, event->window); + + /* keep track of which window has focus so that we can apply pointer updates */ + xfc->appWindow = appWindow; + } + + return TRUE; +} + +static BOOL xf_event_LeaveNotify(xfContext* xfc, const XLeaveWindowEvent* event, BOOL app) +{ + if (!app) + { + xfc->mouse_active = FALSE; + XUngrabKeyboard(xfc->display, CurrentTime); + } + else + { + xfAppWindow* appWindow = xf_AppWindowFromX11Window(xfc, event->window); + + /* keep track of which window has focus so that we can apply pointer updates */ + if (xfc->appWindow == appWindow) + xfc->appWindow = NULL; + } + return TRUE; +} + +static BOOL xf_event_ConfigureNotify(xfContext* xfc, const XConfigureEvent* event, BOOL app) +{ + Window childWindow; + xfAppWindow* appWindow; + rdpSettings* settings; + settings = xfc->context.settings; + + WLog_DBG(TAG, "%s: x=%" PRId32 ", y=%" PRId32 ", w=%" PRId32 ", h=%" PRId32, __func__, event->x, + event->y, event->width, event->height); + + if (!app) + { + if (!xfc->window) + return FALSE; + + if (xfc->window->left != event->x) + xfc->window->left = event->x; + + if (xfc->window->top != event->y) + xfc->window->top = event->y; + + if (xfc->window->width != event->width || xfc->window->height != event->height) + { + xfc->window->width = event->width; + xfc->window->height = event->height; +#ifdef WITH_XRENDER + xfc->offset_x = 0; + xfc->offset_y = 0; + + if (xfc->context.settings->SmartSizing || xfc->context.settings->MultiTouchGestures) + { + xfc->scaledWidth = xfc->window->width; + xfc->scaledHeight = xfc->window->height; + xf_draw_screen(xfc, 0, 0, settings->DesktopWidth, settings->DesktopHeight); + } + else + { + xfc->scaledWidth = settings->DesktopWidth; + xfc->scaledHeight = settings->DesktopHeight; + } + +#endif + } + + if (settings->DynamicResolutionUpdate) + { + int alignedWidth, alignedHeight; + alignedWidth = (xfc->window->width / 2) * 2; + alignedHeight = (xfc->window->height / 2) * 2; + /* ask the server to resize using the display channel */ + xf_disp_handle_configureNotify(xfc, alignedWidth, alignedHeight); + } + } + else + { + appWindow = xf_AppWindowFromX11Window(xfc, event->window); + + if (appWindow) + { + /* + * ConfigureNotify coordinates are expressed relative to the window parent. + * Translate these to root window coordinates. + */ + XTranslateCoordinates(xfc->display, appWindow->handle, RootWindowOfScreen(xfc->screen), + 0, 0, &appWindow->x, &appWindow->y, &childWindow); + appWindow->width = event->width; + appWindow->height = event->height; + + /* + * Additional checks for not in a local move and not ignoring configure to send + * position update to server, also should the window not be focused then do not + * send to server yet (i.e. resizing using window decoration). + * The server will be updated when the window gets refocused. + */ + if (appWindow->decorations) + { + /* moving resizing using window decoration */ + xf_rail_adjust_position(xfc, appWindow); + } + else + { + if ((!event->send_event || appWindow->local_move.state == LMS_NOT_ACTIVE) && + !appWindow->rail_ignore_configure && xfc->focused) + xf_rail_adjust_position(xfc, appWindow); + } + } + } + return xf_pointer_update_scale(xfc); +} + +static BOOL xf_event_MapNotify(xfContext* xfc, const XMapEvent* event, BOOL app) +{ + xfAppWindow* appWindow; + + if (!app) + gdi_send_suppress_output(xfc->context.gdi, FALSE); + else + { + appWindow = xf_AppWindowFromX11Window(xfc, event->window); + + if (appWindow && (appWindow->rail_state == WINDOW_SHOW)) + { + /* local restore event */ + /* This is now handled as part of the PropertyNotify + * Doing this here would inhibit the ability to restore a maximized window + * that is minimized back to the maximized state + */ + // xf_rail_send_client_system_command(xfc, appWindow->windowId, SC_RESTORE); + appWindow->is_mapped = TRUE; + } + } + + return TRUE; +} + +static BOOL xf_event_UnmapNotify(xfContext* xfc, const XUnmapEvent* event, BOOL app) +{ + xfAppWindow* appWindow; + + WINPR_ASSERT(xfc); + WINPR_ASSERT(event); + + xf_keyboard_release_all_keypress(xfc); + + if (!app) + gdi_send_suppress_output(xfc->context.gdi, TRUE); + else + { + appWindow = xf_AppWindowFromX11Window(xfc, event->window); + + if (appWindow) + { + appWindow->is_mapped = FALSE; + } + } + + return TRUE; +} + +static BOOL xf_event_PropertyNotify(xfContext* xfc, const XPropertyEvent* event, BOOL app) +{ + WINPR_ASSERT(xfc); + WINPR_ASSERT(event); + + /* + * This section handles sending the appropriate commands to the rail server + * when the window has been minimized, maximized, restored locally + * ie. not using the buttons on the rail window itself + */ + if ((((Atom)event->atom == xfc->_NET_WM_STATE) && (event->state != PropertyDelete)) || + (((Atom)event->atom == xfc->WM_STATE) && (event->state != PropertyDelete))) + { + unsigned long i; + BOOL status; + BOOL maxVert = FALSE; + BOOL maxHorz = FALSE; + BOOL minimized = FALSE; + BOOL minimizedChanged = FALSE; + unsigned long nitems; + unsigned long bytes; + unsigned char* prop; + xfAppWindow* appWindow = NULL; + + if (app) + { + appWindow = xf_AppWindowFromX11Window(xfc, event->window); + + if (!appWindow) + return TRUE; + } + + if ((Atom)event->atom == xfc->_NET_WM_STATE) + { + status = xf_GetWindowProperty(xfc, event->window, xfc->_NET_WM_STATE, 12, &nitems, + &bytes, &prop); + + if (status) + { + if (appWindow) + { + appWindow->maxVert = FALSE; + appWindow->maxHorz = FALSE; + } + for (i = 0; i < nitems; i++) + { + if ((Atom)((UINT16**)prop)[i] == + XInternAtom(xfc->display, "_NET_WM_STATE_MAXIMIZED_VERT", False)) + { + maxVert = TRUE; + if (appWindow) + appWindow->maxVert = TRUE; + } + + if ((Atom)((UINT16**)prop)[i] == + XInternAtom(xfc->display, "_NET_WM_STATE_MAXIMIZED_HORZ", False)) + { + maxHorz = TRUE; + if (appWindow) + appWindow->maxHorz = TRUE; + } + } + + XFree(prop); + } + } + + if ((Atom)event->atom == xfc->WM_STATE) + { + status = + xf_GetWindowProperty(xfc, event->window, xfc->WM_STATE, 1, &nitems, &bytes, &prop); + + if (status) + { + /* If the window is in the iconic state */ + if (((UINT32)*prop == 3)) + { + minimized = TRUE; + if (appWindow) + appWindow->minimized = TRUE; + } + else + { + minimized = FALSE; + if (appWindow) + appWindow->minimized = FALSE; + } + + minimizedChanged = TRUE; + XFree(prop); + } + } + + if (app) + { + WINPR_ASSERT(appWindow); + if (appWindow->maxVert && appWindow->maxHorz && !appWindow->minimized) + { + if (appWindow->rail_state != WINDOW_SHOW_MAXIMIZED) + { + appWindow->rail_state = WINDOW_SHOW_MAXIMIZED; + xf_rail_send_client_system_command(xfc, appWindow->windowId, SC_MAXIMIZE); + } + } + else if (appWindow->minimized) + { + if (appWindow->rail_state != WINDOW_SHOW_MINIMIZED) + { + appWindow->rail_state = WINDOW_SHOW_MINIMIZED; + xf_rail_send_client_system_command(xfc, appWindow->windowId, SC_MINIMIZE); + } + } + else + { + if (appWindow->rail_state != WINDOW_SHOW && appWindow->rail_state != WINDOW_HIDE) + { + appWindow->rail_state = WINDOW_SHOW; + xf_rail_send_client_system_command(xfc, appWindow->windowId, SC_RESTORE); + } + } + } + else if (minimizedChanged) + gdi_send_suppress_output(xfc->context.gdi, minimized); + } + + return TRUE; +} + +static BOOL xf_event_suppress_events(xfContext* xfc, xfAppWindow* appWindow, const XEvent* event) +{ + if (!xfc->remote_app) + return FALSE; + + switch (appWindow->local_move.state) + { + case LMS_NOT_ACTIVE: + + /* No local move in progress, nothing to do */ + + /* Prevent Configure from happening during indeterminant state of Horz or Vert Max only + */ + if ((event->type == ConfigureNotify) && appWindow->rail_ignore_configure) + { + appWindow->rail_ignore_configure = FALSE; + return TRUE; + } + + break; + + case LMS_STARTING: + + /* Local move initiated by RDP server, but we have not yet seen any updates from the X + * server */ + switch (event->type) + { + case ConfigureNotify: + /* Starting to see move events from the X server. Local move is now in progress. + */ + appWindow->local_move.state = LMS_ACTIVE; + /* Allow these events to be processed during move to keep our state up to date. + */ + break; + + case ButtonPress: + case ButtonRelease: + case KeyPress: + case KeyRelease: + case UnmapNotify: + /* + * A button release event means the X window server did not grab the + * mouse before the user released it. In this case we must cancel the + * local move. The event will be processed below as normal, below. + */ + break; + + case VisibilityNotify: + case PropertyNotify: + case Expose: + /* Allow these events to pass */ + break; + + default: + /* Eat any other events */ + return TRUE; + } + + break; + + case LMS_ACTIVE: + + /* Local move is in progress */ + switch (event->type) + { + case ConfigureNotify: + case VisibilityNotify: + case PropertyNotify: + case Expose: + case GravityNotify: + /* Keep us up to date on position */ + break; + + default: + /* Any other event terminates move */ + xf_rail_end_local_move(xfc, appWindow); + break; + } + + break; + + case LMS_TERMINATING: + /* Already sent RDP end move to server. Allow events to pass. */ + break; + } + + return FALSE; +} + +BOOL xf_event_process(freerdp* instance, const XEvent* event) +{ + BOOL status = TRUE; + xfAppWindow* appWindow; + xfContext* xfc = (xfContext*)instance->context; + rdpSettings* settings = xfc->context.settings; + + if (xfc->remote_app) + { + appWindow = xf_AppWindowFromX11Window(xfc, event->xany.window); + + if (appWindow) + { + /* Update "current" window for cursor change orders */ + xfc->appWindow = appWindow; + + if (xf_event_suppress_events(xfc, appWindow, event)) + return TRUE; + } + } + + if (xfc->window) + { + if (xf_floatbar_check_event(xfc->window->floatbar, event)) + { + xf_floatbar_event_process(xfc->window->floatbar, event); + return TRUE; + } + } + + xf_event_execute_action_script(xfc, event); + + if (event->type != MotionNotify) + { + DEBUG_X11("%s Event(%d): wnd=0x%08lX", x11_event_string(event->type), event->type, + (unsigned long)event->xany.window); + } + + switch (event->type) + { + case Expose: + status = xf_event_Expose(xfc, &event->xexpose, xfc->remote_app); + break; + + case VisibilityNotify: + status = xf_event_VisibilityNotify(xfc, &event->xvisibility, xfc->remote_app); + break; + + case MotionNotify: + status = xf_event_MotionNotify(xfc, &event->xmotion, xfc->remote_app); + break; + + case ButtonPress: + status = xf_event_ButtonPress(xfc, &event->xbutton, xfc->remote_app); + break; + + case ButtonRelease: + status = xf_event_ButtonRelease(xfc, &event->xbutton, xfc->remote_app); + break; + + case KeyPress: + status = xf_event_KeyPress(xfc, &event->xkey, xfc->remote_app); + break; + + case KeyRelease: + status = xf_event_KeyRelease(xfc, &event->xkey, xfc->remote_app); + break; + + case FocusIn: + status = xf_event_FocusIn(xfc, &event->xfocus, xfc->remote_app); + break; + + case FocusOut: + status = xf_event_FocusOut(xfc, &event->xfocus, xfc->remote_app); + break; + + case EnterNotify: + status = xf_event_EnterNotify(xfc, &event->xcrossing, xfc->remote_app); + break; + + case LeaveNotify: + status = xf_event_LeaveNotify(xfc, &event->xcrossing, xfc->remote_app); + break; + + case NoExpose: + break; + + case GraphicsExpose: + break; + + case ConfigureNotify: + status = xf_event_ConfigureNotify(xfc, &event->xconfigure, xfc->remote_app); + break; + + case MapNotify: + status = xf_event_MapNotify(xfc, &event->xmap, xfc->remote_app); + break; + + case UnmapNotify: + status = xf_event_UnmapNotify(xfc, &event->xunmap, xfc->remote_app); + break; + + case ReparentNotify: + break; + + case MappingNotify: + status = xf_event_MappingNotify(xfc, &event->xmapping, xfc->remote_app); + break; + + case ClientMessage: + status = xf_event_ClientMessage(xfc, &event->xclient, xfc->remote_app); + break; + + case PropertyNotify: + status = xf_event_PropertyNotify(xfc, &event->xproperty, xfc->remote_app); + break; + + default: + if (settings->SupportDisplayControl) + xf_disp_handle_xevent(xfc, event); + + break; + } + + xf_cliprdr_handle_xevent(xfc, event); + xf_input_handle_event(xfc, event); + XSync(xfc->display, FALSE); + return status; +} diff --git a/client/X11/xf_event.h b/client/X11/xf_event.h new file mode 100644 index 0000000..2269d3e --- /dev/null +++ b/client/X11/xf_event.h @@ -0,0 +1,43 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * X11 Event Handling + * + * Copyright 2011 Marc-Andre Moreau + * + * Licensed 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 FREERDP_CLIENT_X11_EVENT_H +#define FREERDP_CLIENT_X11_EVENT_H + +#include "xf_keyboard.h" + +#include "xf_client.h" +#include "xfreerdp.h" + +BOOL xf_event_action_script_init(xfContext* xfc); +void xf_event_action_script_free(xfContext* xfc); + +BOOL xf_event_process(freerdp* instance, const XEvent* event); +void xf_event_SendClientEvent(xfContext* xfc, xfWindow* window, Atom atom, unsigned int numArgs, + ...); + +void xf_event_adjust_coordinates(xfContext* xfc, int* x, int* y); +void xf_adjust_coordinates_to_screen(xfContext* xfc, UINT32* x, UINT32* y); + +BOOL xf_generic_MotionNotify(xfContext* xfc, int x, int y, int state, Window window, BOOL app); +BOOL xf_generic_ButtonPress(xfContext* xfc, int x, int y, int button, Window window, BOOL app); +BOOL xf_generic_ButtonEvent(xfContext* xfc, int x, int y, int button, Window window, BOOL app, + BOOL down); + +#endif /* FREERDP_CLIENT_X11_EVENT_H */ diff --git a/client/X11/xf_floatbar.c b/client/X11/xf_floatbar.c new file mode 100644 index 0000000..0966ff5 --- /dev/null +++ b/client/X11/xf_floatbar.c @@ -0,0 +1,813 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * X11 Windows + * + * Licensed under the Apache License, Version 2.0 (the "License");n + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include +#include + +#include "xf_floatbar.h" +#include "resource/close.xbm" +#include "resource/lock.xbm" +#include "resource/unlock.xbm" +#include "resource/minimize.xbm" +#include "resource/restore.xbm" + +#define TAG CLIENT_TAG("x11") + +#define FLOATBAR_HEIGHT 26 +#define FLOATBAR_DEFAULT_WIDTH 576 +#define FLOATBAR_MIN_WIDTH 200 +#define FLOATBAR_BORDER 24 +#define FLOATBAR_BUTTON_WIDTH 24 +#define FLOATBAR_COLOR_BACKGROUND "RGB:31/6c/a9" +#define FLOATBAR_COLOR_BORDER "RGB:75/9a/c8" +#define FLOATBAR_COLOR_FOREGROUND "RGB:FF/FF/FF" + +#ifdef WITH_DEBUG_X11 +#define DEBUG_X11(...) WLog_DBG(TAG, __VA_ARGS__) +#else +#define DEBUG_X11(...) \ + do \ + { \ + } while (0) +#endif + +#define XF_FLOATBAR_MODE_NONE 0 +#define XF_FLOATBAR_MODE_DRAGGING 1 +#define XF_FLOATBAR_MODE_RESIZE_LEFT 2 +#define XF_FLOATBAR_MODE_RESIZE_RIGHT 3 + +#define XF_FLOATBAR_BUTTON_CLOSE 1 +#define XF_FLOATBAR_BUTTON_RESTORE 2 +#define XF_FLOATBAR_BUTTON_MINIMIZE 3 +#define XF_FLOATBAR_BUTTON_LOCKED 4 + +typedef BOOL (*OnClick)(xfFloatbar*); + +typedef struct xf_floatbar_button xfFloatbarButton; + +struct xf_floatbar +{ + int x; + int y; + int width; + int height; + int mode; + int last_motion_x_root; + int last_motion_y_root; + bool locked; + xfFloatbarButton* buttons[4]; + Window handle; + BOOL hasCursor; + xfContext* xfc; + DWORD flags; + BOOL created; + Window root_window; + char* title; +}; + +struct xf_floatbar_button +{ + int x; + int y; + int type; + bool focus; + bool clicked; + OnClick onclick; + Window handle; +}; + +static xfFloatbarButton* xf_floatbar_new_button(xfFloatbar* floatbar, int type); + +static BOOL xf_floatbar_button_onclick_close(xfFloatbar* floatbar) +{ + if (!floatbar) + return FALSE; + + return freerdp_abort_connect(floatbar->xfc->context.instance); +} + +static BOOL xf_floatbar_button_onclick_minimize(xfFloatbar* floatbar) +{ + xfContext* xfc; + + if (!floatbar || !floatbar->xfc) + return FALSE; + + xfc = floatbar->xfc; + xf_SetWindowMinimized(xfc, xfc->window); + return TRUE; +} + +static BOOL xf_floatbar_button_onclick_restore(xfFloatbar* floatbar) +{ + if (!floatbar) + return FALSE; + + xf_toggle_fullscreen(floatbar->xfc); + return TRUE; +} + +static BOOL xf_floatbar_button_onclick_locked(xfFloatbar* floatbar) +{ + if (!floatbar) + return FALSE; + + floatbar->locked = (floatbar->locked) ? FALSE : TRUE; + return xf_floatbar_hide_and_show(floatbar); +} + +BOOL xf_floatbar_set_root_y(xfFloatbar* floatbar, int y) +{ + if (!floatbar) + return FALSE; + + floatbar->last_motion_y_root = y; + return TRUE; +} + +BOOL xf_floatbar_hide_and_show(xfFloatbar* floatbar) +{ + xfContext* xfc; + + if (!floatbar || !floatbar->xfc) + return FALSE; + + if (!floatbar->created) + return TRUE; + + xfc = floatbar->xfc; + + if (!floatbar->locked) + { + if ((floatbar->mode == XF_FLOATBAR_MODE_NONE) && (floatbar->last_motion_y_root > 10) && + (floatbar->y > (FLOATBAR_HEIGHT * -1))) + { + floatbar->y = floatbar->y - 1; + XMoveWindow(xfc->display, floatbar->handle, floatbar->x, floatbar->y); + } + else if (floatbar->y < 0 && (floatbar->last_motion_y_root < 10)) + { + floatbar->y = floatbar->y + 1; + XMoveWindow(xfc->display, floatbar->handle, floatbar->x, floatbar->y); + } + } + + return TRUE; +} + +static BOOL create_floatbar(xfFloatbar* floatbar) +{ + xfContext* xfc; + Status status; + XWindowAttributes attr; + + if (floatbar->created) + return TRUE; + + xfc = floatbar->xfc; + status = XGetWindowAttributes(xfc->display, floatbar->root_window, &attr); + floatbar->x = attr.x + attr.width / 2 - FLOATBAR_DEFAULT_WIDTH / 2; + floatbar->y = 0; + + if (((floatbar->flags & 0x0004) == 0) && !floatbar->locked) + floatbar->y = -FLOATBAR_HEIGHT + 1; + + floatbar->handle = + XCreateWindow(xfc->display, floatbar->root_window, floatbar->x, 0, FLOATBAR_DEFAULT_WIDTH, + FLOATBAR_HEIGHT, 0, CopyFromParent, InputOutput, CopyFromParent, 0, NULL); + floatbar->width = FLOATBAR_DEFAULT_WIDTH; + floatbar->height = FLOATBAR_HEIGHT; + floatbar->mode = XF_FLOATBAR_MODE_NONE; + floatbar->buttons[0] = xf_floatbar_new_button(floatbar, XF_FLOATBAR_BUTTON_CLOSE); + floatbar->buttons[1] = xf_floatbar_new_button(floatbar, XF_FLOATBAR_BUTTON_RESTORE); + floatbar->buttons[2] = xf_floatbar_new_button(floatbar, XF_FLOATBAR_BUTTON_MINIMIZE); + floatbar->buttons[3] = xf_floatbar_new_button(floatbar, XF_FLOATBAR_BUTTON_LOCKED); + XSelectInput(xfc->display, floatbar->handle, + ExposureMask | ButtonPressMask | ButtonReleaseMask | PointerMotionMask | + FocusChangeMask | LeaveWindowMask | EnterWindowMask | StructureNotifyMask | + PropertyChangeMask); + floatbar->created = TRUE; + return TRUE; +} + +BOOL xf_floatbar_toggle_fullscreen(xfFloatbar* floatbar, bool fullscreen) +{ + int i, size; + bool visible = False; + xfContext* xfc; + + if (!floatbar || !floatbar->xfc) + return FALSE; + + xfc = floatbar->xfc; + + /* Only visible if enabled */ + if (floatbar->flags & 0x0001) + { + /* Visible if fullscreen and flag visible in fullscreen mode */ + visible |= ((floatbar->flags & 0x0010) != 0) && fullscreen; + /* Visible if window and flag visible in window mode */ + visible |= ((floatbar->flags & 0x0020) != 0) && !fullscreen; + } + + if (visible) + { + if (!create_floatbar(floatbar)) + return FALSE; + + XMapWindow(xfc->display, floatbar->handle); + size = ARRAYSIZE(floatbar->buttons); + + for (i = 0; i < size; i++) + { + XMapWindow(xfc->display, floatbar->buttons[i]->handle); + } + + /* If default is hidden (and not sticky) don't show on fullscreen state changes */ + if (((floatbar->flags & 0x0004) == 0) && !floatbar->locked) + floatbar->y = -FLOATBAR_HEIGHT + 1; + + xf_floatbar_hide_and_show(floatbar); + } + else if (floatbar->created) + { + XUnmapSubwindows(xfc->display, floatbar->handle); + XUnmapWindow(xfc->display, floatbar->handle); + } + + return TRUE; +} + +xfFloatbarButton* xf_floatbar_new_button(xfFloatbar* floatbar, int type) +{ + xfFloatbarButton* button; + button = (xfFloatbarButton*)calloc(1, sizeof(xfFloatbarButton)); + button->type = type; + + switch (type) + { + case XF_FLOATBAR_BUTTON_CLOSE: + button->x = floatbar->width - FLOATBAR_BORDER - FLOATBAR_BUTTON_WIDTH * type; + button->onclick = xf_floatbar_button_onclick_close; + break; + + case XF_FLOATBAR_BUTTON_RESTORE: + button->x = floatbar->width - FLOATBAR_BORDER - FLOATBAR_BUTTON_WIDTH * type; + button->onclick = xf_floatbar_button_onclick_restore; + break; + + case XF_FLOATBAR_BUTTON_MINIMIZE: + button->x = floatbar->width - FLOATBAR_BORDER - FLOATBAR_BUTTON_WIDTH * type; + button->onclick = xf_floatbar_button_onclick_minimize; + break; + + case XF_FLOATBAR_BUTTON_LOCKED: + button->x = FLOATBAR_BORDER; + button->onclick = xf_floatbar_button_onclick_locked; + break; + + default: + break; + } + + button->y = 0; + button->focus = FALSE; + button->handle = XCreateWindow(floatbar->xfc->display, floatbar->handle, button->x, 0, + FLOATBAR_BUTTON_WIDTH, FLOATBAR_BUTTON_WIDTH, 0, CopyFromParent, + InputOutput, CopyFromParent, 0, NULL); + XSelectInput(floatbar->xfc->display, button->handle, + ExposureMask | ButtonPressMask | ButtonReleaseMask | FocusChangeMask | + LeaveWindowMask | EnterWindowMask | StructureNotifyMask); + return button; +} + +xfFloatbar* xf_floatbar_new(xfContext* xfc, Window window, const char* name, DWORD flags) +{ + xfFloatbar* floatbar; + + /* Floatbar not enabled */ + if ((flags & 0x0001) == 0) + return NULL; + + if (!xfc) + return NULL; + + /* Force disable with remote app */ + if (xfc->remote_app) + return NULL; + + floatbar = (xfFloatbar*)calloc(1, sizeof(xfFloatbar)); + + if (!floatbar) + return NULL; + + floatbar->title = _strdup(name); + + if (!floatbar->title) + goto fail; + + floatbar->root_window = window; + floatbar->flags = flags; + floatbar->xfc = xfc; + floatbar->locked = flags & 0x0002; + xf_floatbar_toggle_fullscreen(floatbar, FALSE); + return floatbar; +fail: + xf_floatbar_free(floatbar); + return NULL; +} + +static unsigned long xf_floatbar_get_color(xfFloatbar* floatbar, char* rgb_value) +{ + Colormap cmap; + XColor color; + Display* display = floatbar->xfc->display; + cmap = DefaultColormap(display, XDefaultScreen(display)); + XParseColor(display, cmap, rgb_value, &color); + XAllocColor(display, cmap, &color); + return color.pixel; +} + +static void xf_floatbar_event_expose(xfFloatbar* floatbar) +{ + GC gc, shape_gc; + Pixmap pmap; + XPoint shape[5], border[5]; + int len; + Display* display = floatbar->xfc->display; + + /* create the pixmap that we'll use for shaping the window */ + pmap = XCreatePixmap(display, floatbar->handle, floatbar->width, floatbar->height, 1); + gc = XCreateGC(display, floatbar->handle, 0, 0); + shape_gc = XCreateGC(display, pmap, 0, 0); + /* points for drawing the floatbar */ + shape[0].x = 0; + shape[0].y = 0; + shape[1].x = floatbar->width; + shape[1].y = 0; + shape[2].x = shape[1].x - FLOATBAR_BORDER; + shape[2].y = FLOATBAR_HEIGHT; + shape[3].x = shape[0].x + FLOATBAR_BORDER; + shape[3].y = FLOATBAR_HEIGHT; + shape[4].x = shape[0].x; + shape[4].y = shape[0].y; + /* points for drawing the border of the floatbar */ + border[0].x = shape[0].x; + border[0].y = shape[0].y - 1; + border[1].x = shape[1].x - 1; + border[1].y = shape[1].y - 1; + border[2].x = shape[2].x; + border[2].y = shape[2].y - 1; + border[3].x = shape[3].x - 1; + border[3].y = shape[3].y - 1; + border[4].x = border[0].x; + border[4].y = border[0].y; + /* Fill all pixels with 0 */ + XSetForeground(display, shape_gc, 0); + XFillRectangle(display, pmap, shape_gc, 0, 0, floatbar->width, floatbar->height); + /* Fill all pixels which should be shown with 1 */ + XSetForeground(display, shape_gc, 1); + XFillPolygon(display, pmap, shape_gc, shape, 5, 0, CoordModeOrigin); + XShapeCombineMask(display, floatbar->handle, ShapeBounding, 0, 0, pmap, ShapeSet); + /* draw the float bar */ + XSetForeground(display, gc, xf_floatbar_get_color(floatbar, FLOATBAR_COLOR_BACKGROUND)); + XFillPolygon(display, floatbar->handle, gc, shape, 4, 0, CoordModeOrigin); + /* draw an border for the floatbar */ + XSetForeground(display, gc, xf_floatbar_get_color(floatbar, FLOATBAR_COLOR_BORDER)); + XDrawLines(display, floatbar->handle, gc, border, 5, CoordModeOrigin); + /* draw the host name connected to (limit to maximum file name) */ + len = strnlen(floatbar->title, MAX_PATH); + XSetForeground(display, gc, xf_floatbar_get_color(floatbar, FLOATBAR_COLOR_FOREGROUND)); + XDrawString(display, floatbar->handle, gc, floatbar->width / 2 - len * 2, 15, floatbar->title, + len); + XFreeGC(display, gc); + XFreeGC(display, shape_gc); +} + +static xfFloatbarButton* xf_floatbar_get_button(xfFloatbar* floatbar, Window window) +{ + int i, size; + size = ARRAYSIZE(floatbar->buttons); + + for (i = 0; i < size; i++) + { + if (floatbar->buttons[i]->handle == window) + { + return floatbar->buttons[i]; + } + } + + return NULL; +} + +static void xf_floatbar_button_update_positon(xfFloatbar* floatbar) +{ + xfFloatbarButton* button; + int i, size; + xfContext* xfc = floatbar->xfc; + size = ARRAYSIZE(floatbar->buttons); + + for (i = 0; i < size; i++) + { + button = floatbar->buttons[i]; + + switch (button->type) + { + case XF_FLOATBAR_BUTTON_CLOSE: + button->x = + floatbar->width - FLOATBAR_BORDER - FLOATBAR_BUTTON_WIDTH * button->type; + break; + + case XF_FLOATBAR_BUTTON_RESTORE: + button->x = + floatbar->width - FLOATBAR_BORDER - FLOATBAR_BUTTON_WIDTH * button->type; + break; + + case XF_FLOATBAR_BUTTON_MINIMIZE: + button->x = + floatbar->width - FLOATBAR_BORDER - FLOATBAR_BUTTON_WIDTH * button->type; + break; + + default: + break; + } + + XMoveWindow(xfc->display, button->handle, button->x, button->y); + xf_floatbar_event_expose(floatbar); + } +} + +static void xf_floatbar_button_event_expose(xfFloatbar* floatbar, Window window) +{ + xfFloatbarButton* button = xf_floatbar_get_button(floatbar, window); + static unsigned char* bits; + GC gc; + Pixmap pattern; + xfContext* xfc = floatbar->xfc; + + if (!button) + return; + + gc = XCreateGC(xfc->display, button->handle, 0, 0); + floatbar = xfc->window->floatbar; + + switch (button->type) + { + case XF_FLOATBAR_BUTTON_CLOSE: + bits = close_bits; + break; + + case XF_FLOATBAR_BUTTON_RESTORE: + bits = restore_bits; + break; + + case XF_FLOATBAR_BUTTON_MINIMIZE: + bits = minimize_bits; + break; + + case XF_FLOATBAR_BUTTON_LOCKED: + if (floatbar->locked) + bits = lock_bits; + else + bits = unlock_bits; + + break; + + default: + break; + } + + pattern = XCreateBitmapFromData(xfc->display, button->handle, (const char*)bits, + FLOATBAR_BUTTON_WIDTH, FLOATBAR_BUTTON_WIDTH); + + if (!(button->focus)) + XSetForeground(xfc->display, gc, + xf_floatbar_get_color(floatbar, FLOATBAR_COLOR_BACKGROUND)); + else + XSetForeground(xfc->display, gc, xf_floatbar_get_color(floatbar, FLOATBAR_COLOR_BORDER)); + + XSetBackground(xfc->display, gc, xf_floatbar_get_color(floatbar, FLOATBAR_COLOR_FOREGROUND)); + XCopyPlane(xfc->display, pattern, button->handle, gc, 0, 0, FLOATBAR_BUTTON_WIDTH, + FLOATBAR_BUTTON_WIDTH, 0, 0, 1); + XFreePixmap(xfc->display, pattern); + XFreeGC(xfc->display, gc); +} + +static void xf_floatbar_button_event_buttonpress(xfFloatbar* floatbar, const XButtonEvent* event) +{ + xfFloatbarButton* button = xf_floatbar_get_button(floatbar, event->window); + + if (button) + button->clicked = TRUE; +} + +static void xf_floatbar_button_event_buttonrelease(xfFloatbar* floatbar, const XButtonEvent* event) +{ + xfFloatbarButton* button; + button = xf_floatbar_get_button(floatbar, event->window); + + if (button) + { + if (button->clicked) + button->onclick(floatbar); + button->clicked = FALSE; + } +} + +static void xf_floatbar_event_buttonpress(xfFloatbar* floatbar, const XButtonEvent* event) +{ + switch (event->button) + { + case Button1: + if (event->x <= FLOATBAR_BORDER) + floatbar->mode = XF_FLOATBAR_MODE_RESIZE_LEFT; + else if (event->x >= (floatbar->width - FLOATBAR_BORDER)) + floatbar->mode = XF_FLOATBAR_MODE_RESIZE_RIGHT; + else + floatbar->mode = XF_FLOATBAR_MODE_DRAGGING; + + break; + + default: + break; + } +} + +static void xf_floatbar_event_buttonrelease(xfFloatbar* floatbar, const XButtonEvent* event) +{ + switch (event->button) + { + case Button1: + floatbar->mode = XF_FLOATBAR_MODE_NONE; + break; + + default: + break; + } +} + +static void xf_floatbar_resize(xfFloatbar* floatbar, const XMotionEvent* event) +{ + int x, width, movement; + xfContext* xfc = floatbar->xfc; + /* calculate movement which happened on the root window */ + movement = event->x_root - floatbar->last_motion_x_root; + + /* set x and width depending if movement happens on the left or right */ + if (floatbar->mode == XF_FLOATBAR_MODE_RESIZE_LEFT) + { + x = floatbar->x + movement; + width = floatbar->width + movement * -1; + } + else + { + x = floatbar->x; + width = floatbar->width + movement; + } + + /* only resize and move window if still above minimum width */ + if (FLOATBAR_MIN_WIDTH < width) + { + XMoveResizeWindow(xfc->display, floatbar->handle, x, 0, width, floatbar->height); + floatbar->x = x; + floatbar->width = width; + } +} + +static void xf_floatbar_dragging(xfFloatbar* floatbar, const XMotionEvent* event) +{ + int x, movement; + xfContext* xfc = floatbar->xfc; + /* calculate movement and new x position */ + movement = event->x_root - floatbar->last_motion_x_root; + x = floatbar->x + movement; + + /* do nothing if floatbar would be moved out of the window */ + if (x < 0 || (x + floatbar->width) > xfc->window->width) + return; + + /* move window to new x position */ + XMoveWindow(xfc->display, floatbar->handle, x, 0); + /* update struct values for the next event */ + floatbar->last_motion_x_root = floatbar->last_motion_x_root + movement; + floatbar->x = x; +} + +static void xf_floatbar_event_motionnotify(xfFloatbar* floatbar, const XMotionEvent* event) +{ + int mode; + Cursor cursor; + xfContext* xfc = floatbar->xfc; + mode = floatbar->mode; + cursor = XCreateFontCursor(xfc->display, XC_arrow); + + if ((event->state & Button1Mask) && (mode > XF_FLOATBAR_MODE_DRAGGING)) + { + xf_floatbar_resize(floatbar, event); + } + else if ((event->state & Button1Mask) && (mode == XF_FLOATBAR_MODE_DRAGGING)) + { + xf_floatbar_dragging(floatbar, event); + } + else + { + if (event->x <= FLOATBAR_BORDER || event->x >= floatbar->width - FLOATBAR_BORDER) + cursor = XCreateFontCursor(xfc->display, XC_sb_h_double_arrow); + } + + XDefineCursor(xfc->display, xfc->window->handle, cursor); + XFreeCursor(xfc->display, cursor); + floatbar->last_motion_x_root = event->x_root; +} + +static void xf_floatbar_button_event_focusin(xfFloatbar* floatbar, const XAnyEvent* event) +{ + xfFloatbarButton* button; + button = xf_floatbar_get_button(floatbar, event->window); + + if (button) + { + button->focus = TRUE; + xf_floatbar_button_event_expose(floatbar, event->window); + } +} + +static void xf_floatbar_button_event_focusout(xfFloatbar* floatbar, const XAnyEvent* event) +{ + xfFloatbarButton* button; + button = xf_floatbar_get_button(floatbar, event->window); + + if (button) + { + button->focus = FALSE; + xf_floatbar_button_event_expose(floatbar, event->window); + } +} + +static void xf_floatbar_event_focusout(xfFloatbar* floatbar) +{ + xfContext* xfc = floatbar->xfc; + + if (xfc->pointer) + { + XDefineCursor(xfc->display, xfc->window->handle, xfc->pointer->cursor); + } +} + +BOOL xf_floatbar_check_event(xfFloatbar* floatbar, const XEvent* event) +{ + xfFloatbarButton* button; + size_t i, size; + + if (!floatbar || !floatbar->xfc || !event) + return FALSE; + + if (!floatbar->created) + return FALSE; + + if (event->xany.window == floatbar->handle) + return TRUE; + + size = ARRAYSIZE(floatbar->buttons); + + for (i = 0; i < size; i++) + { + button = floatbar->buttons[i]; + + if (event->xany.window == button->handle) + return TRUE; + } + + return FALSE; +} + +BOOL xf_floatbar_event_process(xfFloatbar* floatbar, const XEvent* event) +{ + if (!floatbar || !floatbar->xfc || !event) + return FALSE; + + if (!floatbar->created) + return FALSE; + + switch (event->type) + { + case Expose: + if (event->xexpose.window == floatbar->handle) + xf_floatbar_event_expose(floatbar); + else + xf_floatbar_button_event_expose(floatbar, event->xexpose.window); + + break; + + case MotionNotify: + xf_floatbar_event_motionnotify(floatbar, &event->xmotion); + break; + + case ButtonPress: + if (event->xany.window == floatbar->handle) + xf_floatbar_event_buttonpress(floatbar, &event->xbutton); + else + xf_floatbar_button_event_buttonpress(floatbar, &event->xbutton); + + break; + + case ButtonRelease: + if (event->xany.window == floatbar->handle) + xf_floatbar_event_buttonrelease(floatbar, &event->xbutton); + else + xf_floatbar_button_event_buttonrelease(floatbar, &event->xbutton); + + break; + + case EnterNotify: + case FocusIn: + if (event->xany.window != floatbar->handle) + xf_floatbar_button_event_focusin(floatbar, &event->xany); + + break; + + case LeaveNotify: + case FocusOut: + if (event->xany.window == floatbar->handle) + xf_floatbar_event_focusout(floatbar); + else + xf_floatbar_button_event_focusout(floatbar, &event->xany); + + break; + + case ConfigureNotify: + if (event->xany.window == floatbar->handle) + xf_floatbar_button_update_positon(floatbar); + + break; + + case PropertyNotify: + if (event->xany.window == floatbar->handle) + xf_floatbar_button_update_positon(floatbar); + + break; + + default: + break; + } + + return floatbar->handle == event->xany.window; +} + +static void xf_floatbar_button_free(xfContext* xfc, xfFloatbarButton* button) +{ + if (!button) + return; + + if (button->handle) + { + XUnmapWindow(xfc->display, button->handle); + XDestroyWindow(xfc->display, button->handle); + } + + free(button); +} + +void xf_floatbar_free(xfFloatbar* floatbar) +{ + size_t i, size; + xfContext* xfc; + + if (!floatbar) + return; + + free(floatbar->title); + xfc = floatbar->xfc; + size = ARRAYSIZE(floatbar->buttons); + + for (i = 0; i < size; i++) + { + xf_floatbar_button_free(xfc, floatbar->buttons[i]); + floatbar->buttons[i] = NULL; + } + + if (floatbar->handle) + { + XUnmapWindow(xfc->display, floatbar->handle); + XDestroyWindow(xfc->display, floatbar->handle); + } + + free(floatbar); +} diff --git a/client/X11/xf_floatbar.h b/client/X11/xf_floatbar.h new file mode 100644 index 0000000..145514b --- /dev/null +++ b/client/X11/xf_floatbar.h @@ -0,0 +1,34 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * X11 Windows + * + * Licensed 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 FREERDP_CLIENT_X11_FLOATBAR_H +#define FREERDP_CLIENT_X11_FLOATBAR_H + +typedef struct xf_floatbar xfFloatbar; + +#include "xfreerdp.h" + +xfFloatbar* xf_floatbar_new(xfContext* xfc, Window window, const char* title, DWORD flags); +void xf_floatbar_free(xfFloatbar* floatbar); + +BOOL xf_floatbar_event_process(xfFloatbar* floatbar, const XEvent* event); +BOOL xf_floatbar_check_event(xfFloatbar* floatbar, const XEvent* event); +BOOL xf_floatbar_toggle_fullscreen(xfFloatbar* floatbar, bool visible); +BOOL xf_floatbar_hide_and_show(xfFloatbar* floatbar); +BOOL xf_floatbar_set_root_y(xfFloatbar* floatbar, int y); + +#endif /* FREERDP_CLIENT_X11_FLOATBAR_H */ diff --git a/client/X11/xf_gdi.c b/client/X11/xf_gdi.c new file mode 100644 index 0000000..4f52853 --- /dev/null +++ b/client/X11/xf_gdi.c @@ -0,0 +1,1114 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * X11 GDI + * + * Copyright 2011 Marc-Andre Moreau + * Copyright 2014 Thincast Technologies GmbH + * Copyright 2014 Norbert Federa + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "xf_gdi.h" +#include "xf_graphics.h" + +#include +#define TAG CLIENT_TAG("x11") + +static const UINT8 GDI_BS_HATCHED_PATTERNS[] = { + 0xFF, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, /* HS_HORIZONTAL */ + 0xF7, 0xF7, 0xF7, 0xF7, 0xF7, 0xF7, 0xF7, 0xF7, /* HS_VERTICAL */ + 0xFE, 0xFD, 0xFB, 0xF7, 0xEF, 0xDF, 0xBF, 0x7F, /* HS_FDIAGONAL */ + 0x7F, 0xBF, 0xDF, 0xEF, 0xF7, 0xFB, 0xFD, 0xFE, /* HS_BDIAGONAL */ + 0xF7, 0xF7, 0xF7, 0x00, 0xF7, 0xF7, 0xF7, 0xF7, /* HS_CROSS */ + 0x7E, 0xBD, 0xDB, 0xE7, 0xE7, 0xDB, 0xBD, 0x7E /* HS_DIACROSS */ +}; + +static const BYTE xf_rop2_table[] = { + 0, + GXclear, /* 0 */ + GXnor, /* DPon */ + GXandInverted, /* DPna */ + GXcopyInverted, /* Pn */ + GXandReverse, /* PDna */ + GXinvert, /* Dn */ + GXxor, /* DPx */ + GXnand, /* DPan */ + GXand, /* DPa */ + GXequiv, /* DPxn */ + GXnoop, /* D */ + GXorInverted, /* DPno */ + GXcopy, /* P */ + GXorReverse, /* PDno */ + GXor, /* DPo */ + GXset /* 1 */ +}; + +static BOOL xf_set_rop2(xfContext* xfc, int rop2) +{ + if ((rop2 < 0x01) || (rop2 > 0x10)) + { + WLog_ERR(TAG, "Unsupported ROP2: %d", rop2); + return FALSE; + } + + XSetFunction(xfc->display, xfc->gc, xf_rop2_table[rop2]); + return TRUE; +} + +static BOOL xf_set_rop3(xfContext* xfc, UINT32 rop3) +{ + int function = -1; + + switch (rop3) + { + case GDI_BLACKNESS: + function = GXclear; + break; + + case GDI_DPon: + function = GXnor; + break; + + case GDI_DPna: + function = GXandInverted; + break; + + case GDI_Pn: + function = GXcopyInverted; + break; + + case GDI_NOTSRCERASE: + function = GXnor; + break; + + case GDI_DSna: + function = GXandInverted; + break; + + case GDI_NOTSRCCOPY: + function = GXcopyInverted; + break; + + case GDI_SRCERASE: + function = GXandReverse; + break; + + case GDI_PDna: + function = GXandReverse; + break; + + case GDI_DSTINVERT: + function = GXinvert; + break; + + case GDI_PATINVERT: + function = GXxor; + break; + + case GDI_DPan: + function = GXnand; + break; + + case GDI_SRCINVERT: + function = GXxor; + break; + + case GDI_DSan: + function = GXnand; + break; + + case GDI_SRCAND: + function = GXand; + break; + + case GDI_DSxn: + function = GXequiv; + break; + + case GDI_DPa: + function = GXand; + break; + + case GDI_PDxn: + function = GXequiv; + break; + + case GDI_DSTCOPY: + function = GXnoop; + break; + + case GDI_DPno: + function = GXorInverted; + break; + + case GDI_MERGEPAINT: + function = GXorInverted; + break; + + case GDI_SRCCOPY: + function = GXcopy; + break; + + case GDI_SDno: + function = GXorReverse; + break; + + case GDI_SRCPAINT: + function = GXor; + break; + + case GDI_PATCOPY: + function = GXcopy; + break; + + case GDI_PDno: + function = GXorReverse; + break; + + case GDI_DPo: + function = GXor; + break; + + case GDI_WHITENESS: + function = GXset; + break; + + case GDI_PSDPxax: + function = GXand; + break; + + default: + break; + } + + if (function < 0) + { + WLog_ERR(TAG, "Unsupported ROP3: 0x%08" PRIX32 "", rop3); + XSetFunction(xfc->display, xfc->gc, GXclear); + return FALSE; + } + + XSetFunction(xfc->display, xfc->gc, function); + return TRUE; +} + +static Pixmap xf_brush_new(xfContext* xfc, UINT32 width, UINT32 height, UINT32 bpp, BYTE* data) +{ + GC gc; + Pixmap bitmap; + BYTE* cdata; + XImage* image; + rdpGdi* gdi; + UINT32 brushFormat; + gdi = xfc->context.gdi; + bitmap = XCreatePixmap(xfc->display, xfc->drawable, width, height, xfc->depth); + + if (data) + { + brushFormat = gdi_get_pixel_format(bpp); + cdata = (BYTE*)_aligned_malloc(width * height * 4ULL, 16); + freerdp_image_copy(cdata, gdi->dstFormat, 0, 0, 0, width, height, data, brushFormat, 0, 0, + 0, &xfc->context.gdi->palette, FREERDP_FLIP_NONE); + image = XCreateImage(xfc->display, xfc->visual, xfc->depth, ZPixmap, 0, (char*)cdata, width, + height, xfc->scanline_pad, 0); + image->byte_order = LSBFirst; + image->bitmap_bit_order = LSBFirst; + gc = XCreateGC(xfc->display, xfc->drawable, 0, NULL); + XPutImage(xfc->display, bitmap, gc, image, 0, 0, 0, 0, width, height); + image->data = NULL; + XDestroyImage(image); + + if (cdata != data) + _aligned_free(cdata); + + XFreeGC(xfc->display, gc); + } + + return bitmap; +} + +static Pixmap xf_mono_bitmap_new(xfContext* xfc, int width, int height, const BYTE* data) +{ + int scanline; + XImage* image; + Pixmap bitmap; + scanline = (width + 7) / 8; + bitmap = XCreatePixmap(xfc->display, xfc->drawable, width, height, 1); + image = XCreateImage(xfc->display, xfc->visual, 1, ZPixmap, 0, (char*)data, width, height, 8, + scanline); + image->byte_order = LSBFirst; + image->bitmap_bit_order = LSBFirst; + XPutImage(xfc->display, bitmap, xfc->gc_mono, image, 0, 0, 0, 0, width, height); + image->data = NULL; + XDestroyImage(image); + return bitmap; +} + +static BOOL xf_gdi_set_bounds(rdpContext* context, const rdpBounds* bounds) +{ + XRectangle clip; + xfContext* xfc = (xfContext*)context; + xf_lock_x11(xfc); + + if (bounds) + { + clip.x = bounds->left; + clip.y = bounds->top; + clip.width = bounds->right - bounds->left + 1; + clip.height = bounds->bottom - bounds->top + 1; + XSetClipRectangles(xfc->display, xfc->gc, 0, 0, &clip, 1, YXBanded); + } + else + { + XSetClipMask(xfc->display, xfc->gc, None); + } + + xf_unlock_x11(xfc); + return TRUE; +} + +static BOOL xf_gdi_dstblt(rdpContext* context, const DSTBLT_ORDER* dstblt) +{ + xfContext* xfc = (xfContext*)context; + BOOL ret = FALSE; + xf_lock_x11(xfc); + + if (!xf_set_rop3(xfc, gdi_rop3_code(dstblt->bRop))) + goto fail; + + XSetFillStyle(xfc->display, xfc->gc, FillSolid); + XFillRectangle(xfc->display, xfc->drawing, xfc->gc, dstblt->nLeftRect, dstblt->nTopRect, + dstblt->nWidth, dstblt->nHeight); + ret = TRUE; + + if (xfc->drawing == xfc->primary) + ret = gdi_InvalidateRegion(xfc->hdc, dstblt->nLeftRect, dstblt->nTopRect, dstblt->nWidth, + dstblt->nHeight); + +fail: + XSetFunction(xfc->display, xfc->gc, GXcopy); + xf_unlock_x11(xfc); + return ret; +} + +static BOOL xf_gdi_patblt(rdpContext* context, PATBLT_ORDER* patblt) +{ + const rdpBrush* brush; + xfContext* xfc = (xfContext*)context; + BOOL ret = FALSE; + XColor xfg, xbg; + + if (!xf_decode_color(xfc, patblt->foreColor, &xfg)) + return FALSE; + + if (!xf_decode_color(xfc, patblt->backColor, &xbg)) + return FALSE; + + xf_lock_x11(xfc); + brush = &patblt->brush; + + if (!xf_set_rop3(xfc, gdi_rop3_code(patblt->bRop))) + goto fail; + + switch (brush->style) + { + case GDI_BS_SOLID: + XSetFillStyle(xfc->display, xfc->gc, FillSolid); + XSetBackground(xfc->display, xfc->gc, xbg.pixel); + XSetForeground(xfc->display, xfc->gc, xfg.pixel); + XFillRectangle(xfc->display, xfc->drawing, xfc->gc, patblt->nLeftRect, patblt->nTopRect, + patblt->nWidth, patblt->nHeight); + break; + + case GDI_BS_HATCHED: + { + Pixmap pattern = + xf_mono_bitmap_new(xfc, 8, 8, &GDI_BS_HATCHED_PATTERNS[8 * brush->hatch]); + XSetBackground(xfc->display, xfc->gc, xbg.pixel); + XSetForeground(xfc->display, xfc->gc, xfg.pixel); + XSetFillStyle(xfc->display, xfc->gc, FillOpaqueStippled); + XSetStipple(xfc->display, xfc->gc, pattern); + XSetTSOrigin(xfc->display, xfc->gc, brush->x, brush->y); + XFillRectangle(xfc->display, xfc->drawing, xfc->gc, patblt->nLeftRect, patblt->nTopRect, + patblt->nWidth, patblt->nHeight); + XFreePixmap(xfc->display, pattern); + } + break; + + case GDI_BS_PATTERN: + if (brush->bpp > 1) + { + UINT32 bpp = brush->bpp; + + if ((bpp == 16) && (context->settings->ColorDepth == 15)) + bpp = 15; + + Pixmap pattern = xf_brush_new(xfc, 8, 8, bpp, brush->data); + XSetFillStyle(xfc->display, xfc->gc, FillTiled); + XSetTile(xfc->display, xfc->gc, pattern); + XSetTSOrigin(xfc->display, xfc->gc, brush->x, brush->y); + XFillRectangle(xfc->display, xfc->drawing, xfc->gc, patblt->nLeftRect, + patblt->nTopRect, patblt->nWidth, patblt->nHeight); + XSetTile(xfc->display, xfc->gc, xfc->primary); + XFreePixmap(xfc->display, pattern); + } + else + { + Pixmap pattern = xf_mono_bitmap_new(xfc, 8, 8, brush->data); + XSetBackground(xfc->display, xfc->gc, xfg.pixel); + XSetForeground(xfc->display, xfc->gc, xbg.pixel); + XSetFillStyle(xfc->display, xfc->gc, FillOpaqueStippled); + XSetStipple(xfc->display, xfc->gc, pattern); + XSetTSOrigin(xfc->display, xfc->gc, brush->x, brush->y); + XFillRectangle(xfc->display, xfc->drawing, xfc->gc, patblt->nLeftRect, + patblt->nTopRect, patblt->nWidth, patblt->nHeight); + XFreePixmap(xfc->display, pattern); + } + + break; + + default: + WLog_ERR(TAG, "unimplemented brush style:%" PRIu32 "", brush->style); + goto fail; + } + + ret = TRUE; + + if (xfc->drawing == xfc->primary) + ret = gdi_InvalidateRegion(xfc->hdc, patblt->nLeftRect, patblt->nTopRect, patblt->nWidth, + patblt->nHeight); + +fail: + XSetFunction(xfc->display, xfc->gc, GXcopy); + xf_unlock_x11(xfc); + return ret; +} + +static BOOL xf_gdi_scrblt(rdpContext* context, const SCRBLT_ORDER* scrblt) +{ + xfContext* xfc = (xfContext*)context; + BOOL ret = FALSE; + + if (!xfc->display || !xfc->drawing) + return FALSE; + + xf_lock_x11(xfc); + + if (!xf_set_rop3(xfc, gdi_rop3_code(scrblt->bRop))) + goto fail; + + XCopyArea(xfc->display, xfc->primary, xfc->drawing, xfc->gc, scrblt->nXSrc, scrblt->nYSrc, + scrblt->nWidth, scrblt->nHeight, scrblt->nLeftRect, scrblt->nTopRect); + ret = TRUE; + + if (xfc->drawing == xfc->primary) + ret = gdi_InvalidateRegion(xfc->hdc, scrblt->nLeftRect, scrblt->nTopRect, scrblt->nWidth, + scrblt->nHeight); + + XSetFunction(xfc->display, xfc->gc, GXcopy); +fail: + xf_unlock_x11(xfc); + return ret; +} + +static BOOL xf_gdi_opaque_rect(rdpContext* context, const OPAQUE_RECT_ORDER* opaque_rect) +{ + XColor color; + xfContext* xfc = (xfContext*)context; + BOOL ret = TRUE; + + if (!xf_decode_color(xfc, opaque_rect->color, &color)) + return FALSE; + + xf_lock_x11(xfc); + XSetFunction(xfc->display, xfc->gc, GXcopy); + XSetFillStyle(xfc->display, xfc->gc, FillSolid); + XSetForeground(xfc->display, xfc->gc, color.pixel); + XFillRectangle(xfc->display, xfc->drawing, xfc->gc, opaque_rect->nLeftRect, + opaque_rect->nTopRect, opaque_rect->nWidth, opaque_rect->nHeight); + + if (xfc->drawing == xfc->primary) + ret = gdi_InvalidateRegion(xfc->hdc, opaque_rect->nLeftRect, opaque_rect->nTopRect, + opaque_rect->nWidth, opaque_rect->nHeight); + + xf_unlock_x11(xfc); + return ret; +} + +static BOOL xf_gdi_multi_opaque_rect(rdpContext* context, + const MULTI_OPAQUE_RECT_ORDER* multi_opaque_rect) +{ + UINT32 i; + xfContext* xfc = (xfContext*)context; + BOOL ret = TRUE; + XColor color; + + if (!xf_decode_color(xfc, multi_opaque_rect->color, &color)) + return FALSE; + + xf_lock_x11(xfc); + XSetFunction(xfc->display, xfc->gc, GXcopy); + XSetFillStyle(xfc->display, xfc->gc, FillSolid); + XSetForeground(xfc->display, xfc->gc, color.pixel); + + for (i = 0; i < multi_opaque_rect->numRectangles; i++) + { + const DELTA_RECT* rectangle = &multi_opaque_rect->rectangles[i]; + XFillRectangle(xfc->display, xfc->drawing, xfc->gc, rectangle->left, rectangle->top, + rectangle->width, rectangle->height); + + if (xfc->drawing == xfc->primary) + { + if (!(ret = gdi_InvalidateRegion(xfc->hdc, rectangle->left, rectangle->top, + rectangle->width, rectangle->height))) + break; + } + } + + xf_unlock_x11(xfc); + return ret; +} + +static BOOL xf_gdi_line_to(rdpContext* context, const LINE_TO_ORDER* line_to) +{ + XColor color; + xfContext* xfc = (xfContext*)context; + BOOL ret = TRUE; + + if (!xf_decode_color(xfc, line_to->penColor, &color)) + return FALSE; + + xf_lock_x11(xfc); + xf_set_rop2(xfc, line_to->bRop2); + XSetFillStyle(xfc->display, xfc->gc, FillSolid); + XSetForeground(xfc->display, xfc->gc, color.pixel); + XDrawLine(xfc->display, xfc->drawing, xfc->gc, line_to->nXStart, line_to->nYStart, + line_to->nXEnd, line_to->nYEnd); + + if (xfc->drawing == xfc->primary) + { + int x, y, w, h; + x = MIN(line_to->nXStart, line_to->nXEnd); + y = MIN(line_to->nYStart, line_to->nYEnd); + w = abs(line_to->nXEnd - line_to->nXStart) + 1; + h = abs(line_to->nYEnd - line_to->nYStart) + 1; + ret = gdi_InvalidateRegion(xfc->hdc, x, y, w, h); + } + + XSetFunction(xfc->display, xfc->gc, GXcopy); + xf_unlock_x11(xfc); + return ret; +} + +static BOOL xf_gdi_invalidate_poly_region(xfContext* xfc, XPoint* points, int npoints) +{ + int x, y, x1, y1, x2, y2; + + if (npoints < 2) + return FALSE; + + x = x1 = x2 = points->x; + y = y1 = y2 = points->y; + + while (--npoints) + { + points++; + x += points->x; + y += points->y; + + if (x > x2) + x2 = x; + + if (x < x1) + x1 = x; + + if (y > y2) + y2 = y; + + if (y < y1) + y1 = y; + } + + x2++; + y2++; + return gdi_InvalidateRegion(xfc->hdc, x1, y1, x2 - x1, y2 - y1); +} + +static BOOL xf_gdi_polyline(rdpContext* context, const POLYLINE_ORDER* polyline) +{ + UINT32 i; + int npoints; + XColor color; + XPoint* points; + xfContext* xfc = (xfContext*)context; + BOOL ret = TRUE; + + if (!xf_decode_color(xfc, polyline->penColor, &color)) + return FALSE; + + xf_lock_x11(xfc); + xf_set_rop2(xfc, polyline->bRop2); + XSetFillStyle(xfc->display, xfc->gc, FillSolid); + XSetForeground(xfc->display, xfc->gc, color.pixel); + npoints = polyline->numDeltaEntries + 1; + points = calloc(npoints, sizeof(XPoint)); + + if (!points) + { + xf_unlock_x11(xfc); + return FALSE; + } + + points[0].x = polyline->xStart; + points[0].y = polyline->yStart; + + for (i = 0; i < polyline->numDeltaEntries; i++) + { + points[i + 1].x = polyline->points[i].x; + points[i + 1].y = polyline->points[i].y; + } + + XDrawLines(xfc->display, xfc->drawing, xfc->gc, points, npoints, CoordModePrevious); + + if (xfc->drawing == xfc->primary) + { + if (!xf_gdi_invalidate_poly_region(xfc, points, npoints)) + ret = FALSE; + } + + XSetFunction(xfc->display, xfc->gc, GXcopy); + free(points); + xf_unlock_x11(xfc); + return ret; +} + +static BOOL xf_gdi_memblt(rdpContext* context, MEMBLT_ORDER* memblt) +{ + xfBitmap* bitmap; + xfContext* xfc; + BOOL ret = TRUE; + + if (!context || !memblt) + return FALSE; + + bitmap = (xfBitmap*)memblt->bitmap; + xfc = (xfContext*)context; + + if (!bitmap || !xfc || !xfc->display || !xfc->drawing) + return FALSE; + + xf_lock_x11(xfc); + + if (xf_set_rop3(xfc, gdi_rop3_code(memblt->bRop))) + { + XCopyArea(xfc->display, bitmap->pixmap, xfc->drawing, xfc->gc, memblt->nXSrc, memblt->nYSrc, + memblt->nWidth, memblt->nHeight, memblt->nLeftRect, memblt->nTopRect); + + if (xfc->drawing == xfc->primary) + ret = gdi_InvalidateRegion(xfc->hdc, memblt->nLeftRect, memblt->nTopRect, + memblt->nWidth, memblt->nHeight); + } + + XSetFunction(xfc->display, xfc->gc, GXcopy); + xf_unlock_x11(xfc); + return ret; +} + +static BOOL xf_gdi_mem3blt(rdpContext* context, MEM3BLT_ORDER* mem3blt) +{ + const rdpBrush* brush; + xfBitmap* bitmap; + XColor foreColor; + XColor backColor; + Pixmap pattern = 0; + xfContext* xfc = (xfContext*)context; + BOOL ret = FALSE; + + if (!xfc->display || !xfc->drawing) + return FALSE; + + if (!xf_decode_color(xfc, mem3blt->foreColor, &foreColor)) + return FALSE; + + if (!xf_decode_color(xfc, mem3blt->backColor, &backColor)) + return FALSE; + + xf_lock_x11(xfc); + brush = &mem3blt->brush; + bitmap = (xfBitmap*)mem3blt->bitmap; + + if (!xf_set_rop3(xfc, gdi_rop3_code(mem3blt->bRop))) + goto fail; + + switch (brush->style) + { + case GDI_BS_PATTERN: + if (brush->bpp > 1) + { + UINT32 bpp = brush->bpp; + + if ((bpp == 16) && (context->settings->ColorDepth == 15)) + bpp = 15; + + pattern = xf_brush_new(xfc, 8, 8, bpp, brush->data); + XSetFillStyle(xfc->display, xfc->gc, FillTiled); + XSetTile(xfc->display, xfc->gc, pattern); + XSetTSOrigin(xfc->display, xfc->gc, brush->x, brush->y); + } + else + { + pattern = xf_mono_bitmap_new(xfc, 8, 8, brush->data); + XSetBackground(xfc->display, xfc->gc, backColor.pixel); + XSetForeground(xfc->display, xfc->gc, foreColor.pixel); + XSetFillStyle(xfc->display, xfc->gc, FillOpaqueStippled); + XSetStipple(xfc->display, xfc->gc, pattern); + XSetTSOrigin(xfc->display, xfc->gc, brush->x, brush->y); + } + + break; + + case GDI_BS_SOLID: + XSetFillStyle(xfc->display, xfc->gc, FillSolid); + XSetBackground(xfc->display, xfc->gc, backColor.pixel); + XSetForeground(xfc->display, xfc->gc, foreColor.pixel); + XSetTSOrigin(xfc->display, xfc->gc, brush->x, brush->y); + break; + + default: + WLog_ERR(TAG, "Mem3Blt unimplemented brush style:%" PRIu32 "", brush->style); + goto fail; + } + + XCopyArea(xfc->display, bitmap->pixmap, xfc->drawing, xfc->gc, mem3blt->nXSrc, mem3blt->nYSrc, + mem3blt->nWidth, mem3blt->nHeight, mem3blt->nLeftRect, mem3blt->nTopRect); + ret = TRUE; + + if (xfc->drawing == xfc->primary) + ret = gdi_InvalidateRegion(xfc->hdc, mem3blt->nLeftRect, mem3blt->nTopRect, mem3blt->nWidth, + mem3blt->nHeight); + + XSetFillStyle(xfc->display, xfc->gc, FillSolid); + XSetTSOrigin(xfc->display, xfc->gc, 0, 0); + + if (pattern != 0) + XFreePixmap(xfc->display, pattern); + +fail: + XSetFunction(xfc->display, xfc->gc, GXcopy); + xf_unlock_x11(xfc); + return ret; +} + +static BOOL xf_gdi_polygon_sc(rdpContext* context, const POLYGON_SC_ORDER* polygon_sc) +{ + UINT32 i; + int npoints; + XPoint* points; + XColor brush_color; + xfContext* xfc = (xfContext*)context; + BOOL ret = TRUE; + + if (!xf_decode_color(xfc, polygon_sc->brushColor, &brush_color)) + return FALSE; + + xf_lock_x11(xfc); + xf_set_rop2(xfc, polygon_sc->bRop2); + npoints = polygon_sc->numPoints + 1; + points = calloc(npoints, sizeof(XPoint)); + + if (!points) + { + xf_unlock_x11(xfc); + return FALSE; + } + + points[0].x = polygon_sc->xStart; + points[0].y = polygon_sc->yStart; + + for (i = 0; i < polygon_sc->numPoints; i++) + { + points[i + 1].x = polygon_sc->points[i].x; + points[i + 1].y = polygon_sc->points[i].y; + } + + switch (polygon_sc->fillMode) + { + case 1: /* alternate */ + XSetFillRule(xfc->display, xfc->gc, EvenOddRule); + break; + + case 2: /* winding */ + XSetFillRule(xfc->display, xfc->gc, WindingRule); + break; + + default: + WLog_ERR(TAG, "PolygonSC unknown fillMode: %" PRIu32 "", polygon_sc->fillMode); + break; + } + + XSetFillStyle(xfc->display, xfc->gc, FillSolid); + XSetForeground(xfc->display, xfc->gc, brush_color.pixel); + XFillPolygon(xfc->display, xfc->drawing, xfc->gc, points, npoints, Complex, CoordModePrevious); + + if (xfc->drawing == xfc->primary) + { + if (!xf_gdi_invalidate_poly_region(xfc, points, npoints)) + ret = FALSE; + } + + XSetFunction(xfc->display, xfc->gc, GXcopy); + free(points); + xf_unlock_x11(xfc); + return ret; +} + +static BOOL xf_gdi_polygon_cb(rdpContext* context, POLYGON_CB_ORDER* polygon_cb) +{ + UINT32 i; + int npoints; + XPoint* points; + Pixmap pattern; + const rdpBrush* brush; + XColor foreColor; + XColor backColor; + xfContext* xfc = (xfContext*)context; + BOOL ret = TRUE; + + if (!xf_decode_color(xfc, polygon_cb->foreColor, &foreColor)) + return FALSE; + + if (!xf_decode_color(xfc, polygon_cb->backColor, &backColor)) + return FALSE; + + xf_lock_x11(xfc); + brush = &(polygon_cb->brush); + xf_set_rop2(xfc, polygon_cb->bRop2); + npoints = polygon_cb->numPoints + 1; + points = calloc(npoints, sizeof(XPoint)); + + if (!points) + { + xf_unlock_x11(xfc); + return FALSE; + } + + points[0].x = polygon_cb->xStart; + points[0].y = polygon_cb->yStart; + + for (i = 0; i < polygon_cb->numPoints; i++) + { + points[i + 1].x = polygon_cb->points[i].x; + points[i + 1].y = polygon_cb->points[i].y; + } + + switch (polygon_cb->fillMode) + { + case GDI_FILL_ALTERNATE: /* alternate */ + XSetFillRule(xfc->display, xfc->gc, EvenOddRule); + break; + + case GDI_FILL_WINDING: /* winding */ + XSetFillRule(xfc->display, xfc->gc, WindingRule); + break; + + default: + WLog_ERR(TAG, "PolygonCB unknown fillMode: %" PRIu32 "", polygon_cb->fillMode); + break; + } + + if (brush->style == GDI_BS_PATTERN) + { + if (brush->bpp > 1) + { + UINT32 bpp = brush->bpp; + + if ((bpp == 16) && (context->settings->ColorDepth == 15)) + bpp = 15; + + pattern = xf_brush_new(xfc, 8, 8, bpp, brush->data); + XSetFillStyle(xfc->display, xfc->gc, FillTiled); + XSetTile(xfc->display, xfc->gc, pattern); + } + else + { + pattern = xf_mono_bitmap_new(xfc, 8, 8, brush->data); + XSetForeground(xfc->display, xfc->gc, backColor.pixel); + XSetBackground(xfc->display, xfc->gc, foreColor.pixel); + + if (polygon_cb->backMode == BACKMODE_TRANSPARENT) + XSetFillStyle(xfc->display, xfc->gc, FillStippled); + else if (polygon_cb->backMode == BACKMODE_OPAQUE) + XSetFillStyle(xfc->display, xfc->gc, FillOpaqueStippled); + + XSetStipple(xfc->display, xfc->gc, pattern); + } + + XSetTSOrigin(xfc->display, xfc->gc, brush->x, brush->y); + XFillPolygon(xfc->display, xfc->drawing, xfc->gc, points, npoints, Complex, + CoordModePrevious); + XSetFillStyle(xfc->display, xfc->gc, FillSolid); + XSetTSOrigin(xfc->display, xfc->gc, 0, 0); + XFreePixmap(xfc->display, pattern); + + if (xfc->drawing == xfc->primary) + { + if (!xf_gdi_invalidate_poly_region(xfc, points, npoints)) + ret = FALSE; + } + } + else + { + WLog_ERR(TAG, "PolygonCB unimplemented brush style:%" PRIu32 "", brush->style); + } + + XSetFunction(xfc->display, xfc->gc, GXcopy); + free(points); + xf_unlock_x11(xfc); + return ret; +} + +static BOOL xf_gdi_surface_frame_marker(rdpContext* context, + const SURFACE_FRAME_MARKER* surface_frame_marker) +{ + rdpSettings* settings; + xfContext* xfc = (xfContext*)context; + BOOL ret = TRUE; + settings = xfc->context.settings; + xf_lock_x11(xfc); + + switch (surface_frame_marker->frameAction) + { + case SURFACECMD_FRAMEACTION_BEGIN: + xfc->frame_begin = TRUE; + xfc->frame_x1 = 0; + xfc->frame_y1 = 0; + xfc->frame_x2 = 0; + xfc->frame_y2 = 0; + break; + + case SURFACECMD_FRAMEACTION_END: + xfc->frame_begin = FALSE; + + if ((xfc->frame_x2 > xfc->frame_x1) && (xfc->frame_y2 > xfc->frame_y1)) + ret = gdi_InvalidateRegion(xfc->hdc, xfc->frame_x1, xfc->frame_y1, + xfc->frame_x2 - xfc->frame_x1, + xfc->frame_y2 - xfc->frame_y1); + + if (settings->FrameAcknowledge > 0) + { + IFCALL(xfc->context.update->SurfaceFrameAcknowledge, context, + surface_frame_marker->frameId); + } + + break; + } + + xf_unlock_x11(xfc); + return ret; +} + +static BOOL xf_gdi_surface_update_frame(xfContext* xfc, UINT16 tx, UINT16 ty, UINT16 width, + UINT16 height) +{ + BOOL ret = TRUE; + + if (!xfc->remote_app) + { + if (xfc->frame_begin) + { + if (xfc->frame_x2 > xfc->frame_x1 && xfc->frame_y2 > xfc->frame_y1) + { + xfc->frame_x1 = MIN(xfc->frame_x1, tx); + xfc->frame_y1 = MIN(xfc->frame_y1, ty); + xfc->frame_x2 = MAX(xfc->frame_x2, tx + width); + xfc->frame_y2 = MAX(xfc->frame_y2, ty + height); + } + else + { + xfc->frame_x1 = tx; + xfc->frame_y1 = ty; + xfc->frame_x2 = tx + width; + xfc->frame_y2 = ty + height; + } + } + else + { + ret = gdi_InvalidateRegion(xfc->hdc, tx, ty, width, height); + } + } + else + { + ret = gdi_InvalidateRegion(xfc->hdc, tx, ty, width, height); + } + + return ret; +} + +static BOOL xf_gdi_update_screen(xfContext* xfc, const BYTE* pSrcData, UINT32 scanline, + const REGION16* pRegion) +{ + BOOL ret = FALSE; + XImage* image; + UINT32 i, nbRects; + const RECTANGLE_16* rects; + UINT32 bpp; + + if (!xfc || !pSrcData) + return FALSE; + + if (!(rects = region16_rects(pRegion, &nbRects))) + return TRUE; + + if (xfc->depth > 16) + bpp = 4; + else if (xfc->depth > 8) + bpp = 2; + else + bpp = 1; + + XSetFunction(xfc->display, xfc->gc, GXcopy); + XSetFillStyle(xfc->display, xfc->gc, FillSolid); + + for (i = 0; i < nbRects; i++) + { + UINT32 left = rects[i].left; + UINT32 top = rects[i].top; + UINT32 width = rects[i].right - rects[i].left; + UINT32 height = rects[i].bottom - rects[i].top; + const BYTE* src = pSrcData + top * scanline + bpp * left; + image = XCreateImage(xfc->display, xfc->visual, xfc->depth, ZPixmap, 0, (char*)src, width, + height, xfc->scanline_pad, scanline); + + if (!image) + break; + + image->byte_order = LSBFirst; + image->bitmap_bit_order = LSBFirst; + XPutImage(xfc->display, xfc->primary, xfc->gc, image, 0, 0, left, top, width, height); + image->data = NULL; + XDestroyImage(image); + ret = xf_gdi_surface_update_frame(xfc, left, top, width, height); + } + + XSetClipMask(xfc->display, xfc->gc, None); + return ret; +} + +static BOOL xf_gdi_surface_bits(rdpContext* context, const SURFACE_BITS_COMMAND* cmd) +{ + BYTE* pSrcData; + xfContext* xfc = (xfContext*)context; + BOOL ret = FALSE; + DWORD format; + rdpGdi* gdi; + size_t size; + REGION16 region; + RECTANGLE_16 cmdRect; + + if (!context || !cmd || !context->gdi) + return FALSE; + + region16_init(®ion); + cmdRect.left = cmd->destLeft; + cmdRect.top = cmd->destTop; + cmdRect.right = cmdRect.left + cmd->bmp.width; + cmdRect.bottom = cmdRect.top + cmd->bmp.height; + gdi = context->gdi; + xf_lock_x11(xfc); + + switch (cmd->bmp.codecID) + { + case RDP_CODEC_ID_REMOTEFX: + if (!rfx_process_message(context->codecs->rfx, cmd->bmp.bitmapData, + cmd->bmp.bitmapDataLength, cmd->destLeft, cmd->destTop, + gdi->primary_buffer, gdi->dstFormat, gdi->stride, gdi->height, + ®ion)) + goto fail; + + break; + + case RDP_CODEC_ID_NSCODEC: + if (!nsc_process_message(context->codecs->nsc, cmd->bmp.bpp, cmd->bmp.width, + cmd->bmp.height, cmd->bmp.bitmapData, + cmd->bmp.bitmapDataLength, gdi->primary_buffer, gdi->dstFormat, + gdi->stride, 0, 0, cmd->bmp.width, cmd->bmp.height, + FREERDP_FLIP_VERTICAL)) + goto fail; + + region16_union_rect(®ion, ®ion, &cmdRect); + break; + + case RDP_CODEC_ID_NONE: + pSrcData = cmd->bmp.bitmapData; + format = gdi_get_pixel_format(cmd->bmp.bpp); + size = cmd->bmp.width * cmd->bmp.height * GetBytesPerPixel(format) * 1ULL; + if (size > cmd->bmp.bitmapDataLength) + { + WLog_ERR(TAG, "Short nocodec message: got %" PRIu32 " bytes, require %" PRIuz, + cmd->bmp.bitmapDataLength, size); + goto fail; + } + + if (!freerdp_image_copy(gdi->primary_buffer, gdi->dstFormat, gdi->stride, cmd->destLeft, + cmd->destTop, cmd->bmp.width, cmd->bmp.height, pSrcData, format, + 0, 0, 0, &xfc->context.gdi->palette, FREERDP_FLIP_VERTICAL)) + goto fail; + + region16_union_rect(®ion, ®ion, &cmdRect); + break; + + default: + WLog_ERR(TAG, "Unsupported codecID %" PRIu16 "", cmd->bmp.codecID); + goto fail; + } + + ret = xf_gdi_update_screen(xfc, gdi->primary_buffer, gdi->stride, ®ion); +fail: + region16_uninit(®ion); + xf_unlock_x11(xfc); + return ret; +} + +void xf_gdi_register_update_callbacks(rdpUpdate* update) +{ + rdpPrimaryUpdate* primary = update->primary; + update->SetBounds = xf_gdi_set_bounds; + primary->DstBlt = xf_gdi_dstblt; + primary->PatBlt = xf_gdi_patblt; + primary->ScrBlt = xf_gdi_scrblt; + primary->OpaqueRect = xf_gdi_opaque_rect; + primary->MultiOpaqueRect = xf_gdi_multi_opaque_rect; + primary->LineTo = xf_gdi_line_to; + primary->Polyline = xf_gdi_polyline; + primary->MemBlt = xf_gdi_memblt; + primary->Mem3Blt = xf_gdi_mem3blt; + primary->PolygonSC = xf_gdi_polygon_sc; + primary->PolygonCB = xf_gdi_polygon_cb; + update->SurfaceBits = xf_gdi_surface_bits; + update->SurfaceFrameMarker = xf_gdi_surface_frame_marker; +} diff --git a/client/X11/xf_gdi.h b/client/X11/xf_gdi.h new file mode 100644 index 0000000..84dcfc4 --- /dev/null +++ b/client/X11/xf_gdi.h @@ -0,0 +1,32 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * X11 GDI + * + * Copyright 2011 Marc-Andre Moreau + * Copyright 2016 Thincast Technologies GmbH + * Copyright 2016 Armin Novak + * + * Licensed 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 FREERDP_CLIENT_X11_GDI_H +#define FREERDP_CLIENT_X11_GDI_H + +#include + +#include "xf_client.h" +#include "xfreerdp.h" + +void xf_gdi_register_update_callbacks(rdpUpdate* update); + +#endif /* FREERDP_CLIENT_X11_GDI_H */ diff --git a/client/X11/xf_gfx.c b/client/X11/xf_gfx.c new file mode 100644 index 0000000..97d3ad3 --- /dev/null +++ b/client/X11/xf_gfx.c @@ -0,0 +1,418 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * X11 Graphics Pipeline + * + * Copyright 2014 Marc-Andre Moreau + * Copyright 2016 Armin Novak + * Copyright 2016 Thincast Technologies GmbH + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include "xf_gfx.h" +#include "xf_rail.h" + +#include + +#define TAG CLIENT_TAG("x11") + +static UINT xf_OutputUpdate(xfContext* xfc, xfGfxSurface* surface) +{ + UINT rc = ERROR_INTERNAL_ERROR; + UINT32 surfaceX, surfaceY; + RECTANGLE_16 surfaceRect; + rdpGdi* gdi; + UINT32 nbRects, x; + double sx, sy; + const RECTANGLE_16* rects; + gdi = xfc->context.gdi; + surfaceX = surface->gdi.outputOriginX; + surfaceY = surface->gdi.outputOriginY; + surfaceRect.left = 0; + surfaceRect.top = 0; + surfaceRect.right = surface->gdi.mappedWidth; + surfaceRect.bottom = surface->gdi.mappedHeight; + XSetClipMask(xfc->display, xfc->gc, None); + XSetFunction(xfc->display, xfc->gc, GXcopy); + XSetFillStyle(xfc->display, xfc->gc, FillSolid); + region16_intersect_rect(&(surface->gdi.invalidRegion), &(surface->gdi.invalidRegion), + &surfaceRect); + sx = surface->gdi.outputTargetWidth / (double)surface->gdi.mappedWidth; + sy = surface->gdi.outputTargetHeight / (double)surface->gdi.mappedHeight; + + if (!(rects = region16_rects(&surface->gdi.invalidRegion, &nbRects))) + return CHANNEL_RC_OK; + + for (x = 0; x < nbRects; x++) + { + const UINT32 nXSrc = rects[x].left; + const UINT32 nYSrc = rects[x].top; + const UINT32 swidth = rects[x].right - nXSrc; + const UINT32 sheight = rects[x].bottom - nYSrc; + const UINT32 nXDst = surfaceX + nXSrc * sx; + const UINT32 nYDst = surfaceY + nYSrc * sy; + const UINT32 dwidth = swidth * sx; + const UINT32 dheight = sheight * sy; + + if (surface->stage) + { + if (!freerdp_image_scale(surface->stage, gdi->dstFormat, surface->stageScanline, nXSrc, + nYSrc, dwidth, dheight, surface->gdi.data, surface->gdi.format, + surface->gdi.scanline, nXSrc, nYSrc, swidth, sheight)) + goto fail; + } + + if (xfc->remote_app) + { + XPutImage(xfc->display, xfc->primary, xfc->gc, surface->image, nXSrc, nYSrc, nXDst, + nYDst, dwidth, dheight); + xf_lock_x11(xfc); + xf_rail_paint(xfc, nXDst, nYDst, nXDst + dwidth, nYDst + dheight); + xf_unlock_x11(xfc); + } + else +#ifdef WITH_XRENDER + if (xfc->context.settings->SmartSizing || xfc->context.settings->MultiTouchGestures) + { + XPutImage(xfc->display, xfc->primary, xfc->gc, surface->image, nXSrc, nYSrc, nXDst, + nYDst, dwidth, dheight); + xf_draw_screen(xfc, nXDst, nYDst, dwidth, dheight); + } + else +#endif + { + XPutImage(xfc->display, xfc->drawable, xfc->gc, surface->image, nXSrc, nYSrc, nXDst, + nYDst, dwidth, dheight); + } + } + + rc = CHANNEL_RC_OK; +fail: + region16_clear(&surface->gdi.invalidRegion); + XSetClipMask(xfc->display, xfc->gc, None); + XSync(xfc->display, False); + return rc; +} + +static UINT xf_UpdateSurfaces(RdpgfxClientContext* context) +{ + UINT16 count; + UINT32 index; + UINT status = CHANNEL_RC_OK; + UINT16* pSurfaceIds = NULL; + rdpGdi* gdi = (rdpGdi*)context->custom; + xfContext* xfc; + + if (!gdi) + return status; + + if (gdi->suppressOutput) + return CHANNEL_RC_OK; + + xfc = (xfContext*)gdi->context; + EnterCriticalSection(&context->mux); + context->GetSurfaceIds(context, &pSurfaceIds, &count); + + for (index = 0; index < count; index++) + { + xfGfxSurface* surface = (xfGfxSurface*)context->GetSurfaceData(context, pSurfaceIds[index]); + + if (!surface) + continue; + + /* If UpdateSurfaceArea callback is available, the output has already been updated. */ + if (context->UpdateSurfaceArea) + { + if (surface->gdi.windowId != 0) + continue; + } + + status = ERROR_INTERNAL_ERROR; + + if (surface->gdi.outputMapped) + status = xf_OutputUpdate(xfc, surface); + + if (status != 0) + break; + } + + free(pSurfaceIds); + LeaveCriticalSection(&context->mux); + return status; +} + +UINT xf_OutputExpose(xfContext* xfc, UINT32 x, UINT32 y, UINT32 width, UINT32 height) +{ + UINT16 count; + UINT32 index; + UINT status = ERROR_INTERNAL_ERROR; + xfGfxSurface* surface; + RECTANGLE_16 invalidRect; + RECTANGLE_16 surfaceRect; + RECTANGLE_16 intersection; + UINT16* pSurfaceIds = NULL; + RdpgfxClientContext* context = xfc->context.gdi->gfx; + invalidRect.left = x; + invalidRect.top = y; + invalidRect.right = x + width; + invalidRect.bottom = y + height; + status = context->GetSurfaceIds(context, &pSurfaceIds, &count); + + if (status != CHANNEL_RC_OK) + goto fail; + + if (!TryEnterCriticalSection(&context->mux)) + { + free(pSurfaceIds); + return CHANNEL_RC_OK; + } + for (index = 0; index < count; index++) + { + surface = (xfGfxSurface*)context->GetSurfaceData(context, pSurfaceIds[index]); + + if (!surface || !surface->gdi.outputMapped) + continue; + + surfaceRect.left = surface->gdi.outputOriginX; + surfaceRect.top = surface->gdi.outputOriginY; + surfaceRect.right = surface->gdi.outputOriginX + surface->gdi.outputTargetWidth; + surfaceRect.bottom = surface->gdi.outputOriginY + surface->gdi.outputTargetHeight; + + if (rectangles_intersection(&invalidRect, &surfaceRect, &intersection)) + { + /* Invalid rects are specified relative to surface origin */ + intersection.left -= surfaceRect.left; + intersection.top -= surfaceRect.top; + intersection.right -= surfaceRect.left; + intersection.bottom -= surfaceRect.top; + region16_union_rect(&surface->gdi.invalidRegion, &surface->gdi.invalidRegion, + &intersection); + } + } + + free(pSurfaceIds); + LeaveCriticalSection(&context->mux); + IFCALLRET(context->UpdateSurfaces, status, context); + + if (status != CHANNEL_RC_OK) + goto fail; + +fail: + return status; +} + +UINT32 x11_pad_scanline(UINT32 scanline, UINT32 inPad) +{ + /* Ensure X11 alignment is met */ + if (inPad > 0) + { + const UINT32 align = inPad / 8; + const UINT32 pad = align - scanline % align; + + if (align != pad) + scanline += pad; + } + + /* 16 byte alingment is required for ASM optimized code */ + if (scanline % 16) + scanline += 16 - scanline % 16; + + return scanline; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT xf_CreateSurface(RdpgfxClientContext* context, + const RDPGFX_CREATE_SURFACE_PDU* createSurface) +{ + UINT ret = CHANNEL_RC_NO_MEMORY; + size_t size; + xfGfxSurface* surface; + rdpGdi* gdi = (rdpGdi*)context->custom; + xfContext* xfc = (xfContext*)gdi->context; + surface = (xfGfxSurface*)calloc(1, sizeof(xfGfxSurface)); + + if (!surface) + return CHANNEL_RC_NO_MEMORY; + + surface->gdi.codecs = gdi->context->codecs; + + if (!surface->gdi.codecs) + { + WLog_ERR(TAG, "%s: global GDI codecs aren't set", __FUNCTION__); + goto out_free; + } + + surface->gdi.surfaceId = createSurface->surfaceId; + surface->gdi.width = x11_pad_scanline(createSurface->width, 0); + surface->gdi.height = x11_pad_scanline(createSurface->height, 0); + surface->gdi.mappedWidth = createSurface->width; + surface->gdi.mappedHeight = createSurface->height; + surface->gdi.outputTargetWidth = createSurface->width; + surface->gdi.outputTargetHeight = createSurface->height; + + switch (createSurface->pixelFormat) + { + case GFX_PIXEL_FORMAT_ARGB_8888: + surface->gdi.format = PIXEL_FORMAT_BGRA32; + break; + + case GFX_PIXEL_FORMAT_XRGB_8888: + surface->gdi.format = PIXEL_FORMAT_BGRX32; + break; + + default: + WLog_ERR(TAG, "%s: unknown pixelFormat 0x%" PRIx32 "", __FUNCTION__, + createSurface->pixelFormat); + ret = ERROR_INTERNAL_ERROR; + goto out_free; + } + + surface->gdi.scanline = surface->gdi.width * GetBytesPerPixel(surface->gdi.format); + surface->gdi.scanline = x11_pad_scanline(surface->gdi.scanline, xfc->scanline_pad); + size = surface->gdi.scanline * surface->gdi.height * 1ULL; + surface->gdi.data = (BYTE*)_aligned_malloc(size, 16); + + if (!surface->gdi.data) + { + WLog_ERR(TAG, "%s: unable to allocate GDI data", __FUNCTION__); + goto out_free; + } + + ZeroMemory(surface->gdi.data, size); + + if (AreColorFormatsEqualNoAlpha(gdi->dstFormat, surface->gdi.format)) + { + surface->image = + XCreateImage(xfc->display, xfc->visual, xfc->depth, ZPixmap, 0, + (char*)surface->gdi.data, surface->gdi.mappedWidth, + surface->gdi.mappedHeight, xfc->scanline_pad, surface->gdi.scanline); + } + else + { + UINT32 width = surface->gdi.width; + UINT32 bytes = GetBytesPerPixel(gdi->dstFormat); + surface->stageScanline = width * bytes; + surface->stageScanline = x11_pad_scanline(surface->stageScanline, xfc->scanline_pad); + size = surface->stageScanline * surface->gdi.height * 1ULL; + surface->stage = (BYTE*)_aligned_malloc(size, 16); + + if (!surface->stage) + { + WLog_ERR(TAG, "%s: unable to allocate stage buffer", __FUNCTION__); + goto out_free_gdidata; + } + + ZeroMemory(surface->stage, size); + surface->image = + XCreateImage(xfc->display, xfc->visual, xfc->depth, ZPixmap, 0, (char*)surface->stage, + surface->gdi.mappedWidth, surface->gdi.mappedHeight, xfc->scanline_pad, + surface->stageScanline); + } + + if (!surface->image) + { + WLog_ERR(TAG, "%s: an error occurred when creating the XImage", __FUNCTION__); + goto error_surface_image; + } + + surface->image->byte_order = LSBFirst; + surface->image->bitmap_bit_order = LSBFirst; + surface->gdi.outputMapped = FALSE; + region16_init(&surface->gdi.invalidRegion); + + if (context->SetSurfaceData(context, surface->gdi.surfaceId, (void*)surface) != CHANNEL_RC_OK) + { + WLog_ERR(TAG, "%s: an error occurred during SetSurfaceData", __FUNCTION__); + goto error_set_surface_data; + } + + return CHANNEL_RC_OK; +error_set_surface_data: + surface->image->data = NULL; + XDestroyImage(surface->image); +error_surface_image: + _aligned_free(surface->stage); +out_free_gdidata: + _aligned_free(surface->gdi.data); +out_free: + free(surface); + return ret; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT xf_DeleteSurface(RdpgfxClientContext* context, + const RDPGFX_DELETE_SURFACE_PDU* deleteSurface) +{ + rdpCodecs* codecs = NULL; + xfGfxSurface* surface = NULL; + UINT status; + EnterCriticalSection(&context->mux); + surface = (xfGfxSurface*)context->GetSurfaceData(context, deleteSurface->surfaceId); + + if (surface) + { + if (surface->gdi.windowId > 0) + IFCALL(context->UnmapWindowForSurface, context, surface->gdi.windowId); + +#ifdef WITH_GFX_H264 + h264_context_free(surface->gdi.h264); +#endif + surface->image->data = NULL; + XDestroyImage(surface->image); + _aligned_free(surface->gdi.data); + _aligned_free(surface->stage); + region16_uninit(&surface->gdi.invalidRegion); + codecs = surface->gdi.codecs; + free(surface); + } + + status = context->SetSurfaceData(context, deleteSurface->surfaceId, NULL); + + if (codecs && codecs->progressive) + progressive_delete_surface_context(codecs->progressive, deleteSurface->surfaceId); + + LeaveCriticalSection(&context->mux); + return status; +} + +void xf_graphics_pipeline_init(xfContext* xfc, RdpgfxClientContext* gfx) +{ + rdpGdi* gdi = xfc->context.gdi; + gdi_graphics_pipeline_init(gdi, gfx); + + if (!xfc->context.settings->SoftwareGdi) + { + gfx->UpdateSurfaces = xf_UpdateSurfaces; + gfx->CreateSurface = xf_CreateSurface; + gfx->DeleteSurface = xf_DeleteSurface; + } +} + +void xf_graphics_pipeline_uninit(xfContext* xfc, RdpgfxClientContext* gfx) +{ + rdpGdi* gdi = xfc->context.gdi; + gdi_graphics_pipeline_uninit(gdi, gfx); +} diff --git a/client/X11/xf_gfx.h b/client/X11/xf_gfx.h new file mode 100644 index 0000000..934e85a --- /dev/null +++ b/client/X11/xf_gfx.h @@ -0,0 +1,45 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * X11 Graphics Pipeline + * + * Copyright 2014 Marc-Andre Moreau + * Copyright 2016 Thincast Technologies GmbH + * Copyright 2016 Armin Novak + * + * Licensed 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 FREERDP_CLIENT_X11_GFX_H +#define FREERDP_CLIENT_X11_GFX_H + +#include "xf_client.h" +#include "xfreerdp.h" + +#include + +struct xf_gfx_surface +{ + gdiGfxSurface gdi; + BYTE* stage; + UINT32 stageScanline; + XImage* image; +}; +typedef struct xf_gfx_surface xfGfxSurface; + +UINT xf_OutputExpose(xfContext* xfc, UINT32 x, UINT32 y, UINT32 width, UINT32 height); + +void xf_graphics_pipeline_init(xfContext* xfc, RdpgfxClientContext* gfx); + +void xf_graphics_pipeline_uninit(xfContext* xfc, RdpgfxClientContext* gfx); + +#endif /* FREERDP_CLIENT_X11_GFX_H */ diff --git a/client/X11/xf_graphics.c b/client/X11/xf_graphics.c new file mode 100644 index 0000000..7050569 --- /dev/null +++ b/client/X11/xf_graphics.c @@ -0,0 +1,790 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * X11 Graphical Objects + * + * Copyright 2011 Marc-Andre Moreau + * Copyright 2016 Thincast Technologies GmbH + * Copyright 2016 Armin Novak + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include + +#ifdef WITH_XCURSOR +#include +#endif + +#include +#include + +#include + +#include +#include + +#include "xf_graphics.h" +#include "xf_gdi.h" +#include "xf_event.h" + +#include +#define TAG CLIENT_TAG("x11") + +static BOOL xf_Pointer_Set(rdpContext* context, const rdpPointer* pointer); + +BOOL xf_decode_color(xfContext* xfc, const UINT32 srcColor, XColor* color) +{ + rdpGdi* gdi; + rdpSettings* settings; + UINT32 SrcFormat; + BYTE r, g, b, a; + + if (!xfc || !color) + return FALSE; + + gdi = xfc->context.gdi; + + if (!gdi) + return FALSE; + + settings = xfc->context.settings; + + if (!settings) + return FALSE; + + switch (settings->ColorDepth) + { + case 32: + case 24: + SrcFormat = PIXEL_FORMAT_BGR24; + break; + + case 16: + SrcFormat = PIXEL_FORMAT_RGB16; + break; + + case 15: + SrcFormat = PIXEL_FORMAT_RGB15; + break; + + case 8: + SrcFormat = PIXEL_FORMAT_RGB8; + break; + + default: + return FALSE; + } + + SplitColor(srcColor, SrcFormat, &r, &g, &b, &a, &gdi->palette); + color->blue = (unsigned short)(b << 8); + color->green = (unsigned short)(g << 8); + color->red = (unsigned short)(r << 8); + color->flags = DoRed | DoGreen | DoBlue; + + if (XAllocColor(xfc->display, xfc->colormap, color) == 0) + return FALSE; + + return TRUE; +} + +/* Bitmap Class */ +static BOOL xf_Bitmap_New(rdpContext* context, rdpBitmap* bitmap) +{ + BOOL rc = FALSE; + UINT32 depth; + BYTE* data; + rdpGdi* gdi; + xfBitmap* xbitmap = (xfBitmap*)bitmap; + xfContext* xfc = (xfContext*)context; + + if (!context || !bitmap || !context->gdi) + return FALSE; + + gdi = context->gdi; + xf_lock_x11(xfc); + depth = GetBitsPerPixel(bitmap->format); + xbitmap->pixmap = + XCreatePixmap(xfc->display, xfc->drawable, bitmap->width, bitmap->height, xfc->depth); + + if (!xbitmap->pixmap) + goto unlock; + + if (bitmap->data) + { + XSetFunction(xfc->display, xfc->gc, GXcopy); + + if ((INT64)depth != xfc->depth) + { + if (!(data = _aligned_malloc(bitmap->width * bitmap->height * 4ULL, 16))) + goto unlock; + + if (!freerdp_image_copy(data, gdi->dstFormat, 0, 0, 0, bitmap->width, bitmap->height, + bitmap->data, bitmap->format, 0, 0, 0, &context->gdi->palette, + FREERDP_FLIP_NONE)) + { + _aligned_free(data); + goto unlock; + } + + _aligned_free(bitmap->data); + bitmap->data = data; + bitmap->format = gdi->dstFormat; + } + + xbitmap->image = + XCreateImage(xfc->display, xfc->visual, xfc->depth, ZPixmap, 0, (char*)bitmap->data, + bitmap->width, bitmap->height, xfc->scanline_pad, 0); + + if (!xbitmap->image) + goto unlock; + + xbitmap->image->byte_order = LSBFirst; + xbitmap->image->bitmap_bit_order = LSBFirst; + XPutImage(xfc->display, xbitmap->pixmap, xfc->gc, xbitmap->image, 0, 0, 0, 0, bitmap->width, + bitmap->height); + } + + rc = TRUE; +unlock: + xf_unlock_x11(xfc); + return rc; +} + +static void xf_Bitmap_Free(rdpContext* context, rdpBitmap* bitmap) +{ + xfContext* xfc = (xfContext*)context; + xfBitmap* xbitmap = (xfBitmap*)bitmap; + + if (!xfc || !xbitmap) + return; + + xf_lock_x11(xfc); + + if (xbitmap->pixmap != 0) + { + XFreePixmap(xfc->display, xbitmap->pixmap); + xbitmap->pixmap = 0; + } + + if (xbitmap->image) + { + xbitmap->image->data = NULL; + XDestroyImage(xbitmap->image); + xbitmap->image = NULL; + } + + xf_unlock_x11(xfc); + _aligned_free(bitmap->data); + free(xbitmap); +} + +static BOOL xf_Bitmap_Paint(rdpContext* context, rdpBitmap* bitmap) +{ + int width, height; + xfContext* xfc = (xfContext*)context; + xfBitmap* xbitmap = (xfBitmap*)bitmap; + BOOL ret; + + if (!context || !xbitmap) + return FALSE; + + width = bitmap->right - bitmap->left + 1; + height = bitmap->bottom - bitmap->top + 1; + xf_lock_x11(xfc); + XSetFunction(xfc->display, xfc->gc, GXcopy); + XPutImage(xfc->display, xfc->primary, xfc->gc, xbitmap->image, 0, 0, bitmap->left, bitmap->top, + width, height); + ret = gdi_InvalidateRegion(xfc->hdc, bitmap->left, bitmap->top, width, height); + xf_unlock_x11(xfc); + return ret; +} + +static BOOL xf_Bitmap_SetSurface(rdpContext* context, rdpBitmap* bitmap, BOOL primary) +{ + xfContext* xfc = (xfContext*)context; + + if (!context || (!bitmap && !primary)) + return FALSE; + + xf_lock_x11(xfc); + + if (primary) + xfc->drawing = xfc->primary; + else + xfc->drawing = ((xfBitmap*)bitmap)->pixmap; + + xf_unlock_x11(xfc); + return TRUE; +} + +static BOOL xf_Pointer_GetCursorForCurrentScale(rdpContext* context, const rdpPointer* pointer, + Cursor* cursor) +{ +#ifdef WITH_XCURSOR + UINT32 CursorFormat; + xfContext* xfc = (xfContext*)context; + xfPointer* xpointer = (xfPointer*)pointer; + XcursorImage ci = { 0 }; + rdpSettings* settings; + UINT32 xTargetSize; + UINT32 yTargetSize; + double xscale; + double yscale; + size_t size; + int cursorIndex = -1; + + if (!context || !pointer || !context->gdi) + return FALSE; + + settings = xfc->context.settings; + + if (!settings) + return FALSE; + + xscale = (settings->SmartSizing ? xfc->scaledWidth / (double)settings->DesktopWidth : 1); + yscale = (settings->SmartSizing ? xfc->scaledHeight / (double)settings->DesktopHeight : 1); + xTargetSize = pointer->width * xscale; + yTargetSize = pointer->height * yscale; + + WLog_DBG(TAG, "%s: scaled: %" PRIu32 "x%" PRIu32 ", desktop: %" PRIu32 "x%" PRIu32, __func__, + xfc->scaledWidth, xfc->savedHeight, settings->DesktopWidth, settings->DesktopHeight); + + for (int i = 0; i < xpointer->nCursors; i++) + { + if (xpointer->cursorWidths[i] == xTargetSize && xpointer->cursorHeights[i] == yTargetSize) + { + cursorIndex = i; + } + } + + if (cursorIndex == -1) + { + xf_lock_x11(xfc); + + if (!xfc->invert) + CursorFormat = (!xfc->big_endian) ? PIXEL_FORMAT_RGBA32 : PIXEL_FORMAT_ABGR32; + else + CursorFormat = (!xfc->big_endian) ? PIXEL_FORMAT_BGRA32 : PIXEL_FORMAT_ARGB32; + + if (xpointer->nCursors == xpointer->mCursors) + { + xpointer->mCursors = (xpointer->mCursors == 0 ? 1 : xpointer->mCursors * 2); + + if (!(xpointer->cursorWidths = (UINT32*)realloc(xpointer->cursorWidths, + sizeof(UINT32) * xpointer->mCursors))) + { + xf_unlock_x11(xfc); + return FALSE; + } + if (!(xpointer->cursorHeights = (UINT32*)realloc(xpointer->cursorHeights, + sizeof(UINT32) * xpointer->mCursors))) + { + xf_unlock_x11(xfc); + return FALSE; + } + if (!(xpointer->cursors = + (Cursor*)realloc(xpointer->cursors, sizeof(Cursor) * xpointer->mCursors))) + { + xf_unlock_x11(xfc); + return FALSE; + } + } + + ci.version = XCURSOR_IMAGE_VERSION; + ci.size = sizeof(ci); + ci.width = xTargetSize; + ci.height = yTargetSize; + ci.xhot = pointer->xPos * xscale; + ci.yhot = pointer->yPos * yscale; + size = ci.height * ci.width * GetBytesPerPixel(CursorFormat) * 1ULL; + + if (!(ci.pixels = (XcursorPixel*)_aligned_malloc(size, 16))) + { + xf_unlock_x11(xfc); + return FALSE; + } + + const double xs = fabs(fabs(xscale) - 1.0); + const double ys = fabs(fabs(yscale) - 1.0); + WLog_DBG(TAG, + "%s: cursorIndex %" PRId32 " scaling pointer %" PRIu32 "x%" PRIu32 " --> %" PRIu32 + "x%" PRIu32 " [%lfx%lf]", + __func__, cursorIndex, pointer->width, pointer->height, ci.width, ci.height, + xscale, yscale); + if ((xs > DBL_EPSILON) || (ys > DBL_EPSILON)) + { + if (!freerdp_image_scale((BYTE*)ci.pixels, CursorFormat, 0, 0, 0, ci.width, ci.height, + (BYTE*)xpointer->cursorPixels, CursorFormat, 0, 0, 0, + pointer->width, pointer->height)) + { + _aligned_free(ci.pixels); + xf_unlock_x11(xfc); + return FALSE; + } + } + else + { + memcpy(ci.pixels, xpointer->cursorPixels, size); + } + + cursorIndex = xpointer->nCursors; + xpointer->cursorWidths[cursorIndex] = ci.width; + xpointer->cursorHeights[cursorIndex] = ci.height; + xpointer->cursors[cursorIndex] = XcursorImageLoadCursor(xfc->display, &ci); + xpointer->nCursors += 1; + _aligned_free(ci.pixels); + + xf_unlock_x11(xfc); + } + else + { + WLog_DBG(TAG, "%s: using cached cursor %" PRId32, __func__, cursorIndex); + } + + cursor[0] = xpointer->cursors[cursorIndex]; +#endif + return TRUE; +} + +/* Pointer Class */ +static Window xf_Pointer_get_window(xfContext* xfc) +{ + if (!xfc) + { + WLog_WARN(TAG, "xf_Pointer: Invalid context"); + return 0; + } + if (xfc->remote_app) + { + if (!xfc->appWindow) + { + WLog_WARN(TAG, "xf_Pointer: Invalid appWindow"); + return 0; + } + return xfc->appWindow->handle; + } + else + { + if (!xfc->window) + { + WLog_WARN(TAG, "xf_Pointer: Invalid window"); + return 0; + } + return xfc->window->handle; + } +} + +BOOL xf_pointer_update_scale(xfContext* xfc) +{ + xfPointer* pointer; + WINPR_ASSERT(xfc); + + pointer = xfc->pointer; + if (!pointer) + return TRUE; + + return xf_Pointer_Set(&xfc->context, &xfc->pointer->pointer); +} + +static BOOL xf_Pointer_New(rdpContext* context, rdpPointer* pointer) +{ + BOOL rc = FALSE; +#ifdef WITH_XCURSOR + UINT32 CursorFormat; + size_t size; + xfContext* xfc = (xfContext*)context; + xfPointer* xpointer = (xfPointer*)pointer; + + if (!context || !pointer || !context->gdi) + goto fail; + + if (!xfc->invert) + CursorFormat = (!xfc->big_endian) ? PIXEL_FORMAT_RGBA32 : PIXEL_FORMAT_ABGR32; + else + CursorFormat = (!xfc->big_endian) ? PIXEL_FORMAT_BGRA32 : PIXEL_FORMAT_ARGB32; + + xpointer->nCursors = 0; + xpointer->mCursors = 0; + + size = pointer->height * pointer->width * GetBytesPerPixel(CursorFormat) * 1ULL; + + if (!(xpointer->cursorPixels = (XcursorPixel*)_aligned_malloc(size, 16))) + goto fail; + + if (!freerdp_image_copy_from_pointer_data( + (BYTE*)xpointer->cursorPixels, CursorFormat, 0, 0, 0, pointer->width, pointer->height, + pointer->xorMaskData, pointer->lengthXorMask, pointer->andMaskData, + pointer->lengthAndMask, pointer->xorBpp, &context->gdi->palette)) + { + _aligned_free(xpointer->cursorPixels); + return FALSE; + } + rc = TRUE; + +#endif +fail: + WLog_DBG(TAG, "%s: %ld", __func__, rc ? pointer : -1); + return rc; +} + +static void xf_Pointer_Free(rdpContext* context, rdpPointer* pointer) +{ + WLog_DBG(TAG, "%s: %p", __func__, pointer); + +#ifdef WITH_XCURSOR + xfContext* xfc = (xfContext*)context; + xfPointer* xpointer = (xfPointer*)pointer; + + xf_lock_x11(xfc); + + _aligned_free(xpointer->cursorPixels); + free(xpointer->cursorWidths); + free(xpointer->cursorHeights); + + for (int i = 0; i < xpointer->nCursors; i++) + { + XFreeCursor(xfc->display, xpointer->cursors[i]); + } + + free(xpointer->cursors); + xpointer->nCursors = 0; + xpointer->mCursors = 0; + + xf_unlock_x11(xfc); +#endif +} + +static BOOL xf_Pointer_Set(rdpContext* context, const rdpPointer* pointer) +{ + WLog_DBG(TAG, "%s: %p", __func__, pointer); + +#ifdef WITH_XCURSOR + xfContext* xfc = (xfContext*)context; + Window handle = xf_Pointer_get_window(xfc); + xfc->pointer = (xfPointer*)pointer; + + /* in RemoteApp mode, window can be null if none has had focus */ + + if (handle) + { + if (!xf_Pointer_GetCursorForCurrentScale(context, pointer, &(xfc->pointer->cursor))) + return FALSE; + xf_lock_x11(xfc); + XDefineCursor(xfc->display, handle, xfc->pointer->cursor); + xf_unlock_x11(xfc); + } + else + { + WLog_WARN(TAG, "%s: handle=%ld", __func__, handle); + } +#endif + return TRUE; +} + +static BOOL xf_Pointer_SetNull(rdpContext* context) +{ + WLog_DBG(TAG, "%s", __func__); +#ifdef WITH_XCURSOR + xfContext* xfc = (xfContext*)context; + static Cursor nullcursor = None; + Window handle = xf_Pointer_get_window(xfc); + xf_lock_x11(xfc); + + if (nullcursor == None) + { + XcursorImage ci; + XcursorPixel xp = 0; + ZeroMemory(&ci, sizeof(ci)); + ci.version = XCURSOR_IMAGE_VERSION; + ci.size = sizeof(ci); + ci.width = ci.height = 1; + ci.xhot = ci.yhot = 0; + ci.pixels = &xp; + nullcursor = XcursorImageLoadCursor(xfc->display, &ci); + } + + xfc->pointer = NULL; + + if ((handle) && (nullcursor != None)) + XDefineCursor(xfc->display, handle, nullcursor); + + xf_unlock_x11(xfc); +#endif + return TRUE; +} + +static BOOL xf_Pointer_SetDefault(rdpContext* context) +{ +#ifdef WITH_XCURSOR + xfContext* xfc = (xfContext*)context; + Window handle = xf_Pointer_get_window(xfc); + xf_lock_x11(xfc); + xfc->pointer = NULL; + + if (handle) + XUndefineCursor(xfc->display, handle); + + xf_unlock_x11(xfc); +#endif + return TRUE; +} + +static BOOL xf_Pointer_SetPosition(rdpContext* context, UINT32 x, UINT32 y) +{ + xfContext* xfc = (xfContext*)context; + XWindowAttributes current; + XSetWindowAttributes tmp; + BOOL ret = FALSE; + Status rc; + Window handle = xf_Pointer_get_window(xfc); + + if (!handle) + { + WLog_WARN(TAG, "%s: focus %d, handle%lu", __func__, xfc->focused, handle); + return TRUE; + } + + WLog_DBG(TAG, "%s: %" PRIu32 "x%" PRIu32, __func__, x, y); + if (xfc->remote_app && !xfc->focused) + return TRUE; + + xf_adjust_coordinates_to_screen(xfc, &x, &y); + + xf_lock_x11(xfc); + + rc = XGetWindowAttributes(xfc->display, handle, ¤t); + if (rc == 0) + { + WLog_WARN(TAG, "%s: XGetWindowAttributes==%d", __func__, rc); + goto out; + } + + tmp.event_mask = (current.your_event_mask & ~(PointerMotionMask)); + + rc = XChangeWindowAttributes(xfc->display, handle, CWEventMask, &tmp); + if (rc == 0) + { + WLog_WARN(TAG, "%s: XChangeWindowAttributes==%d", __func__, rc); + goto out; + } + + rc = XWarpPointer(xfc->display, None, handle, 0, 0, 0, 0, x, y); + if (rc == 0) + WLog_WARN(TAG, "%s: XWarpPointer==%d", __func__, rc); + tmp.event_mask = current.your_event_mask; + rc = XChangeWindowAttributes(xfc->display, handle, CWEventMask, &tmp); + if (rc == 0) + WLog_WARN(TAG, "%s: 2.try XChangeWindowAttributes==%d", __func__, rc); + ret = TRUE; +out: + xf_unlock_x11(xfc); + return ret; +} + +/* Glyph Class */ +static BOOL xf_Glyph_New(rdpContext* context, const rdpGlyph* glyph) +{ + int scanline; + XImage* image; + xfGlyph* xf_glyph; + xf_glyph = (xfGlyph*)glyph; + xfContext* xfc = (xfContext*)context; + xf_lock_x11(xfc); + scanline = (glyph->cx + 7) / 8; + xf_glyph->pixmap = XCreatePixmap(xfc->display, xfc->drawing, glyph->cx, glyph->cy, 1); + image = XCreateImage(xfc->display, xfc->visual, 1, ZPixmap, 0, (char*)glyph->aj, glyph->cx, + glyph->cy, 8, scanline); + image->byte_order = MSBFirst; + image->bitmap_bit_order = MSBFirst; + XInitImage(image); + XPutImage(xfc->display, xf_glyph->pixmap, xfc->gc_mono, image, 0, 0, 0, 0, glyph->cx, + glyph->cy); + image->data = NULL; + XDestroyImage(image); + xf_unlock_x11(xfc); + return TRUE; +} + +static void xf_Glyph_Free(rdpContext* context, rdpGlyph* glyph) +{ + xfContext* xfc = (xfContext*)context; + xf_lock_x11(xfc); + + if (((xfGlyph*)glyph)->pixmap != 0) + XFreePixmap(xfc->display, ((xfGlyph*)glyph)->pixmap); + + xf_unlock_x11(xfc); + free(glyph->aj); + free(glyph); +} + +static BOOL xf_Glyph_Draw(rdpContext* context, const rdpGlyph* glyph, INT32 x, INT32 y, INT32 w, + INT32 h, INT32 sx, INT32 sy, BOOL fOpRedundant) +{ + xfGlyph* xf_glyph; + xfContext* xfc = (xfContext*)context; + xf_glyph = (xfGlyph*)glyph; + xf_lock_x11(xfc); + + if (!fOpRedundant) + { + XSetFillStyle(xfc->display, xfc->gc, FillOpaqueStippled); + XFillRectangle(xfc->display, xfc->drawable, xfc->gc, x, y, w, h); + } + + XSetFillStyle(xfc->display, xfc->gc, FillStippled); + XSetStipple(xfc->display, xfc->gc, xf_glyph->pixmap); + + if (sx || sy) + WLog_ERR(TAG, ""); + + // XSetClipOrigin(xfc->display, xfc->gc, sx, sy); + XSetTSOrigin(xfc->display, xfc->gc, x, y); + XFillRectangle(xfc->display, xfc->drawing, xfc->gc, x, y, w, h); + xf_unlock_x11(xfc); + return TRUE; +} + +static BOOL xf_Glyph_BeginDraw(rdpContext* context, INT32 x, INT32 y, INT32 width, INT32 height, + UINT32 bgcolor, UINT32 fgcolor, BOOL fOpRedundant) +{ + xfContext* xfc = (xfContext*)context; + XRectangle rect; + XColor xbgcolor, xfgcolor; + + if (!xf_decode_color(xfc, bgcolor, &xbgcolor)) + return FALSE; + + if (!xf_decode_color(xfc, fgcolor, &xfgcolor)) + return FALSE; + + rect.x = x; + rect.y = y; + rect.width = width; + rect.height = height; + xf_lock_x11(xfc); + + if (!fOpRedundant) + { + XSetForeground(xfc->display, xfc->gc, xfgcolor.pixel); + XSetBackground(xfc->display, xfc->gc, xfgcolor.pixel); + XSetFillStyle(xfc->display, xfc->gc, FillOpaqueStippled); + XFillRectangle(xfc->display, xfc->drawable, xfc->gc, x, y, width, height); + } + + XSetForeground(xfc->display, xfc->gc, xbgcolor.pixel); + XSetBackground(xfc->display, xfc->gc, xfgcolor.pixel); + xf_unlock_x11(xfc); + return TRUE; +} + +static BOOL xf_Glyph_EndDraw(rdpContext* context, INT32 x, INT32 y, INT32 width, INT32 height, + UINT32 bgcolor, UINT32 fgcolor) +{ + xfContext* xfc = (xfContext*)context; + BOOL ret = TRUE; + XColor xfgcolor, xbgcolor; + + if (!xf_decode_color(xfc, bgcolor, &xbgcolor)) + return FALSE; + + if (!xf_decode_color(xfc, fgcolor, &xfgcolor)) + return FALSE; + + if (xfc->drawing == xfc->primary) + ret = gdi_InvalidateRegion(xfc->hdc, x, y, width, height); + + return ret; +} + +/* Graphics Module */ +BOOL xf_register_pointer(rdpGraphics* graphics) +{ + rdpPointer* pointer = NULL; + + if (!(pointer = (rdpPointer*)calloc(1, sizeof(rdpPointer)))) + return FALSE; + + pointer->size = sizeof(xfPointer); + pointer->New = xf_Pointer_New; + pointer->Free = xf_Pointer_Free; + pointer->Set = xf_Pointer_Set; + pointer->SetNull = xf_Pointer_SetNull; + pointer->SetDefault = xf_Pointer_SetDefault; + pointer->SetPosition = xf_Pointer_SetPosition; + graphics_register_pointer(graphics, pointer); + free(pointer); + return TRUE; +} + +BOOL xf_register_graphics(rdpGraphics* graphics) +{ + rdpBitmap bitmap; + rdpGlyph glyph; + + if (!graphics || !graphics->Bitmap_Prototype || !graphics->Glyph_Prototype) + return FALSE; + + bitmap = *graphics->Bitmap_Prototype; + glyph = *graphics->Glyph_Prototype; + bitmap.size = sizeof(xfBitmap); + bitmap.New = xf_Bitmap_New; + bitmap.Free = xf_Bitmap_Free; + bitmap.Paint = xf_Bitmap_Paint; + bitmap.SetSurface = xf_Bitmap_SetSurface; + graphics_register_bitmap(graphics, &bitmap); + glyph.size = sizeof(xfGlyph); + glyph.New = xf_Glyph_New; + glyph.Free = xf_Glyph_Free; + glyph.Draw = xf_Glyph_Draw; + glyph.BeginDraw = xf_Glyph_BeginDraw; + glyph.EndDraw = xf_Glyph_EndDraw; + graphics_register_glyph(graphics, &glyph); + return TRUE; +} + +UINT32 xf_get_local_color_format(xfContext* xfc, BOOL aligned) +{ + UINT32 DstFormat; + BOOL invert = FALSE; + + if (!xfc) + return 0; + + invert = xfc->invert; + + if (xfc->depth == 32) + DstFormat = (!invert) ? PIXEL_FORMAT_RGBA32 : PIXEL_FORMAT_BGRA32; + else if (xfc->depth == 30) + DstFormat = (!invert) ? PIXEL_FORMAT_RGBX32_DEPTH30 : PIXEL_FORMAT_BGRX32_DEPTH30; + else if (xfc->depth == 24) + { + if (aligned) + DstFormat = (!invert) ? PIXEL_FORMAT_RGBX32 : PIXEL_FORMAT_BGRX32; + else + DstFormat = (!invert) ? PIXEL_FORMAT_RGB24 : PIXEL_FORMAT_BGR24; + } + else if (xfc->depth == 16) + DstFormat = PIXEL_FORMAT_RGB16; + else if (xfc->depth == 15) + DstFormat = PIXEL_FORMAT_RGB15; + else + DstFormat = (!invert) ? PIXEL_FORMAT_RGBX32 : PIXEL_FORMAT_BGRX32; + + return DstFormat; +} diff --git a/client/X11/xf_graphics.h b/client/X11/xf_graphics.h new file mode 100644 index 0000000..96e1d98 --- /dev/null +++ b/client/X11/xf_graphics.h @@ -0,0 +1,34 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * X11 Graphical Objects + * + * Copyright 2011 Marc-Andre Moreau + * + * Licensed 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 FREERDP_CLIENT_X11_GRAPHICS_H +#define FREERDP_CLIENT_X11_GRAPHICS_H + +#include "xf_client.h" +#include "xfreerdp.h" + +BOOL xf_register_pointer(rdpGraphics* graphics); +BOOL xf_register_graphics(rdpGraphics* graphics); + +BOOL xf_decode_color(xfContext* xfc, const UINT32 srcColor, XColor* color); +UINT32 xf_get_local_color_format(xfContext* xfc, BOOL aligned); + +BOOL xf_pointer_update_scale(xfContext* xfc); + +#endif /* FREERDP_CLIENT_X11_GRAPHICS_H */ diff --git a/client/X11/xf_input.c b/client/X11/xf_input.c new file mode 100644 index 0000000..50a52e0 --- /dev/null +++ b/client/X11/xf_input.c @@ -0,0 +1,669 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * X11 Input + * + * Copyright 2013 Corey Clayton + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include + +#ifdef WITH_XCURSOR +#include +#endif + +#ifdef WITH_XI +#include +#endif + +#include + +#include "xf_event.h" +#include "xf_input.h" + +#include +#define TAG CLIENT_TAG("x11") + +#ifdef WITH_XI + +#define MAX_CONTACTS 2 + +#define PAN_THRESHOLD 50 +#define ZOOM_THRESHOLD 10 + +#define MIN_FINGER_DIST 5 + +typedef struct touch_contact +{ + int id; + int count; + double pos_x; + double pos_y; + double last_x; + double last_y; + +} touchContact; + +static touchContact contacts[MAX_CONTACTS]; + +static int active_contacts; +static int lastEvType; +static XIDeviceEvent lastEvent; +static double firstDist = -1.0; +static double lastDist; + +static double z_vector; +static double px_vector; +static double py_vector; + +const char* xf_input_get_class_string(int class) +{ + if (class == XIKeyClass) + return "XIKeyClass"; + else if (class == XIButtonClass) + return "XIButtonClass"; + else if (class == XIValuatorClass) + return "XIValuatorClass"; + else if (class == XIScrollClass) + return "XIScrollClass"; + else if (class == XITouchClass) + return "XITouchClass"; + + return "XIUnknownClass"; +} + +int xf_input_init(xfContext* xfc, Window window) +{ + int i, j; + int nmasks; + int ndevices; + int major = 2; + int minor = 2; + Status xstatus; + XIDeviceInfo* info; + XIEventMask evmasks[64]; + int opcode, event, error; + BYTE masks[8][XIMaskLen(XI_LASTEVENT)]; + z_vector = 0; + px_vector = 0; + py_vector = 0; + nmasks = 0; + ndevices = 0; + active_contacts = 0; + ZeroMemory(contacts, sizeof(touchContact) * MAX_CONTACTS); + + if (!XQueryExtension(xfc->display, "XInputExtension", &opcode, &event, &error)) + { + WLog_WARN(TAG, "XInput extension not available."); + return -1; + } + + xfc->XInputOpcode = opcode; + XIQueryVersion(xfc->display, &major, &minor); + + if (major * 1000 + minor < 2002) + { + WLog_WARN(TAG, "Server does not support XI 2.2"); + return -1; + } + + if (xfc->context.settings->MultiTouchInput) + xfc->use_xinput = TRUE; + + info = XIQueryDevice(xfc->display, XIAllDevices, &ndevices); + + for (i = 0; i < ndevices; i++) + { + BOOL touch = FALSE; + XIDeviceInfo* dev = &info[i]; + + for (j = 0; j < dev->num_classes; j++) + { + XIAnyClassInfo* class = dev->classes[j]; + XITouchClassInfo* t = (XITouchClassInfo*)class; + + if ((class->type == XITouchClass) && (t->mode == XIDirectTouch) && + (strcmp(dev->name, "Virtual core pointer") != 0)) + { + touch = TRUE; + } + } + + for (j = 0; j < dev->num_classes; j++) + { + XIAnyClassInfo* class = dev->classes[j]; + XITouchClassInfo* t = (XITouchClassInfo*)class; + + if (xfc->context.settings->MultiTouchInput) + { + WLog_DBG(TAG, "%s (%d) \"%s\" id: %d", xf_input_get_class_string(class->type), + class->type, dev->name, dev->deviceid); + } + + evmasks[nmasks].mask = masks[nmasks]; + evmasks[nmasks].mask_len = sizeof(masks[0]); + ZeroMemory(masks[nmasks], sizeof(masks[0])); + evmasks[nmasks].deviceid = dev->deviceid; + + if ((class->type == XITouchClass) && (t->mode == XIDirectTouch) && + (strcmp(dev->name, "Virtual core pointer") != 0)) + { + if (xfc->context.settings->MultiTouchInput) + { + WLog_DBG(TAG, "%s %s touch device (id: %d, mode: %d), supporting %d touches.", + dev->name, (t->mode == XIDirectTouch) ? "direct" : "dependent", + dev->deviceid, t->mode, t->num_touches); + } + + XISetMask(masks[nmasks], XI_TouchBegin); + XISetMask(masks[nmasks], XI_TouchUpdate); + XISetMask(masks[nmasks], XI_TouchEnd); + nmasks++; + } + + if (xfc->use_xinput) + { + if (!touch && (class->type == XIButtonClass) && + strcmp(dev->name, "Virtual core pointer")) + { + WLog_DBG(TAG, "%s button device (id: %d, mode: %d)", dev->name, dev->deviceid, + t->mode); + XISetMask(masks[nmasks], XI_ButtonPress); + XISetMask(masks[nmasks], XI_ButtonRelease); + XISetMask(masks[nmasks], XI_Motion); + nmasks++; + } + } + } + } + + XIFreeDeviceInfo(info); + + if (nmasks > 0) + xstatus = XISelectEvents(xfc->display, window, evmasks, nmasks); + + return 0; +} + +static BOOL xf_input_is_duplicate(const XGenericEventCookie* cookie) +{ + const XIDeviceEvent* event; + event = cookie->data; + + if ((lastEvent.time == event->time) && (lastEvType == cookie->evtype) && + (lastEvent.detail == event->detail) && (lastEvent.event_x == event->event_x) && + (lastEvent.event_y == event->event_y)) + { + return TRUE; + } + + return FALSE; +} + +static void xf_input_save_last_event(const XGenericEventCookie* cookie) +{ + const XIDeviceEvent* event; + event = cookie->data; + lastEvType = cookie->evtype; + lastEvent.time = event->time; + lastEvent.detail = event->detail; + lastEvent.event_x = event->event_x; + lastEvent.event_y = event->event_y; +} + +static void xf_input_detect_pan(xfContext* xfc) +{ + double dx[2]; + double dy[2]; + double px; + double py; + double dist_x; + double dist_y; + rdpContext* ctx = &xfc->context; + + if (active_contacts != 2) + { + return; + } + + dx[0] = contacts[0].pos_x - contacts[0].last_x; + dx[1] = contacts[1].pos_x - contacts[1].last_x; + dy[0] = contacts[0].pos_y - contacts[0].last_y; + dy[1] = contacts[1].pos_y - contacts[1].last_y; + px = fabs(dx[0]) < fabs(dx[1]) ? dx[0] : dx[1]; + py = fabs(dy[0]) < fabs(dy[1]) ? dy[0] : dy[1]; + px_vector += px; + py_vector += py; + dist_x = fabs(contacts[0].pos_x - contacts[1].pos_x); + dist_y = fabs(contacts[0].pos_y - contacts[1].pos_y); + + if (dist_y > MIN_FINGER_DIST) + { + if (px_vector > PAN_THRESHOLD) + { + { + PanningChangeEventArgs e; + EventArgsInit(&e, "xfreerdp"); + e.dx = 5; + e.dy = 0; + PubSub_OnPanningChange(ctx->pubSub, xfc, &e); + } + px_vector = 0; + py_vector = 0; + z_vector = 0; + } + else if (px_vector < -PAN_THRESHOLD) + { + { + PanningChangeEventArgs e; + EventArgsInit(&e, "xfreerdp"); + e.dx = -5; + e.dy = 0; + PubSub_OnPanningChange(ctx->pubSub, xfc, &e); + } + px_vector = 0; + py_vector = 0; + z_vector = 0; + } + } + + if (dist_x > MIN_FINGER_DIST) + { + if (py_vector > PAN_THRESHOLD) + { + { + PanningChangeEventArgs e; + EventArgsInit(&e, "xfreerdp"); + e.dx = 0; + e.dy = 5; + PubSub_OnPanningChange(ctx->pubSub, xfc, &e); + } + py_vector = 0; + px_vector = 0; + z_vector = 0; + } + else if (py_vector < -PAN_THRESHOLD) + { + { + PanningChangeEventArgs e; + EventArgsInit(&e, "xfreerdp"); + e.dx = 0; + e.dy = -5; + PubSub_OnPanningChange(ctx->pubSub, xfc, &e); + } + py_vector = 0; + px_vector = 0; + z_vector = 0; + } + } +} + +static void xf_input_detect_pinch(xfContext* xfc) +{ + double dist; + double delta; + ZoomingChangeEventArgs e; + rdpContext* ctx = &xfc->context; + + if (active_contacts != 2) + { + firstDist = -1.0; + return; + } + + /* first calculate the distance */ + dist = sqrt(pow(contacts[1].pos_x - contacts[0].last_x, 2.0) + + pow(contacts[1].pos_y - contacts[0].last_y, 2.0)); + + /* if this is the first 2pt touch */ + if (firstDist <= 0) + { + firstDist = dist; + lastDist = firstDist; + z_vector = 0; + px_vector = 0; + py_vector = 0; + } + else + { + delta = lastDist - dist; + + if (delta > 1.0) + delta = 1.0; + + if (delta < -1.0) + delta = -1.0; + + /* compare the current distance to the first one */ + z_vector += delta; + lastDist = dist; + + if (z_vector > ZOOM_THRESHOLD) + { + EventArgsInit(&e, "xfreerdp"); + e.dx = e.dy = -10; + PubSub_OnZoomingChange(ctx->pubSub, xfc, &e); + z_vector = 0; + px_vector = 0; + py_vector = 0; + } + + if (z_vector < -ZOOM_THRESHOLD) + { + EventArgsInit(&e, "xfreerdp"); + e.dx = e.dy = 10; + PubSub_OnZoomingChange(ctx->pubSub, xfc, &e); + z_vector = 0; + px_vector = 0; + py_vector = 0; + } + } +} + +static void xf_input_touch_begin(xfContext* xfc, XIDeviceEvent* event) +{ + int i; + + WINPR_UNUSED(xfc); + for (i = 0; i < MAX_CONTACTS; i++) + { + if (contacts[i].id == 0) + { + contacts[i].id = event->detail; + contacts[i].count = 1; + contacts[i].pos_x = event->event_x; + contacts[i].pos_y = event->event_y; + active_contacts++; + break; + } + } +} + +static void xf_input_touch_update(xfContext* xfc, XIDeviceEvent* event) +{ + int i; + + for (i = 0; i < MAX_CONTACTS; i++) + { + if (contacts[i].id == event->detail) + { + contacts[i].count++; + contacts[i].last_x = contacts[i].pos_x; + contacts[i].last_y = contacts[i].pos_y; + contacts[i].pos_x = event->event_x; + contacts[i].pos_y = event->event_y; + xf_input_detect_pinch(xfc); + xf_input_detect_pan(xfc); + break; + } + } +} + +static void xf_input_touch_end(xfContext* xfc, XIDeviceEvent* event) +{ + int i; + + WINPR_UNUSED(xfc); + for (i = 0; i < MAX_CONTACTS; i++) + { + if (contacts[i].id == event->detail) + { + contacts[i].id = 0; + contacts[i].count = 0; + active_contacts--; + break; + } + } +} + +static int xf_input_handle_event_local(xfContext* xfc, const XEvent* event) +{ + union { + const XGenericEventCookie* cc; + XGenericEventCookie* vc; + } cookie; + cookie.cc = &event->xcookie; + XGetEventData(xfc->display, cookie.vc); + + if ((cookie.cc->type == GenericEvent) && (cookie.cc->extension == xfc->XInputOpcode)) + { + switch (cookie.cc->evtype) + { + case XI_TouchBegin: + if (xf_input_is_duplicate(cookie.cc) == FALSE) + xf_input_touch_begin(xfc, cookie.cc->data); + + xf_input_save_last_event(cookie.cc); + break; + + case XI_TouchUpdate: + if (xf_input_is_duplicate(cookie.cc) == FALSE) + xf_input_touch_update(xfc, cookie.cc->data); + + xf_input_save_last_event(cookie.cc); + break; + + case XI_TouchEnd: + if (xf_input_is_duplicate(cookie.cc) == FALSE) + xf_input_touch_end(xfc, cookie.cc->data); + + xf_input_save_last_event(cookie.cc); + break; + + default: + WLog_ERR(TAG, "unhandled xi type= %d", cookie.cc->evtype); + break; + } + } + + XFreeEventData(xfc->display, cookie.vc); + return 0; +} + +#ifdef WITH_DEBUG_X11 +static char* xf_input_touch_state_string(DWORD flags) +{ + if (flags & CONTACT_FLAG_DOWN) + return "RDPINPUT::CONTACT_FLAG_DOWN"; + else if (flags & CONTACT_FLAG_UPDATE) + return "RDPINPUT::CONTACT_FLAG_UPDATE"; + else if (flags & CONTACT_FLAG_UP) + return "RDPINPUT::CONTACT_FLAG_UP"; + else if (flags & CONTACT_FLAG_INRANGE) + return "RDPINPUT::CONTACT_FLAG_INRANGE"; + else if (flags & CONTACT_FLAG_INCONTACT) + return "RDPINPUT::CONTACT_FLAG_INCONTACT"; + else if (flags & CONTACT_FLAG_CANCELED) + return "RDPINPUT::CONTACT_FLAG_CANCELED"; + else + return "RDPINPUT::CONTACT_FLAG_UNKNOWN"; +} +#endif + +static void xf_input_hide_cursor(xfContext* xfc) +{ +#ifdef WITH_XCURSOR + + if (!xfc->cursorHidden) + { + XcursorImage ci; + XcursorPixel xp = 0; + static Cursor nullcursor = None; + xf_lock_x11(xfc); + ZeroMemory(&ci, sizeof(ci)); + ci.version = XCURSOR_IMAGE_VERSION; + ci.size = sizeof(ci); + ci.width = ci.height = 1; + ci.xhot = ci.yhot = 0; + ci.pixels = &xp; + nullcursor = XcursorImageLoadCursor(xfc->display, &ci); + + if ((xfc->window) && (nullcursor != None)) + XDefineCursor(xfc->display, xfc->window->handle, nullcursor); + + xfc->cursorHidden = TRUE; + xf_unlock_x11(xfc); + } + +#endif +} + +static void xf_input_show_cursor(xfContext* xfc) +{ +#ifdef WITH_XCURSOR + xf_lock_x11(xfc); + + if (xfc->cursorHidden) + { + if (xfc->window) + { + if (!xfc->pointer) + XUndefineCursor(xfc->display, xfc->window->handle); + else + XDefineCursor(xfc->display, xfc->window->handle, xfc->pointer->cursor); + } + + xfc->cursorHidden = FALSE; + } + + xf_unlock_x11(xfc); +#endif +} + +static int xf_input_touch_remote(xfContext* xfc, XIDeviceEvent* event, int evtype) +{ + int x, y; + int touchId; + int contactId; + RdpeiClientContext* rdpei = xfc->rdpei; + + if (!rdpei) + return 0; + + xf_input_hide_cursor(xfc); + touchId = event->detail; + x = (int)event->event_x; + y = (int)event->event_y; + xf_event_adjust_coordinates(xfc, &x, &y); + + if (evtype == XI_TouchBegin) + { + WLog_DBG(TAG, "TouchBegin: %d", touchId); + rdpei->TouchBegin(rdpei, touchId, x, y, &contactId); + } + else if (evtype == XI_TouchUpdate) + { + WLog_DBG(TAG, "TouchUpdate: %d", touchId); + rdpei->TouchUpdate(rdpei, touchId, x, y, &contactId); + } + else if (evtype == XI_TouchEnd) + { + WLog_DBG(TAG, "TouchEnd: %d", touchId); + rdpei->TouchEnd(rdpei, touchId, x, y, &contactId); + } + + return 0; +} + +static int xf_input_event(xfContext* xfc, XIDeviceEvent* event, int evtype) +{ + xf_input_show_cursor(xfc); + + switch (evtype) + { + case XI_ButtonPress: + xf_generic_ButtonEvent(xfc, (int)event->event_x, (int)event->event_y, event->detail, + event->event, xfc->remote_app, TRUE); + break; + + case XI_ButtonRelease: + xf_generic_ButtonEvent(xfc, (int)event->event_x, (int)event->event_y, event->detail, + event->event, xfc->remote_app, FALSE); + break; + + case XI_Motion: + xf_generic_MotionNotify(xfc, (int)event->event_x, (int)event->event_y, event->detail, + event->event, xfc->remote_app); + break; + } + + return 0; +} + +static int xf_input_handle_event_remote(xfContext* xfc, const XEvent* event) +{ + union { + const XGenericEventCookie* cc; + XGenericEventCookie* vc; + } cookie; + cookie.cc = &event->xcookie; + XGetEventData(xfc->display, cookie.vc); + + if ((cookie.cc->type == GenericEvent) && (cookie.cc->extension == xfc->XInputOpcode)) + { + switch (cookie.cc->evtype) + { + case XI_TouchBegin: + xf_input_touch_remote(xfc, cookie.cc->data, XI_TouchBegin); + break; + + case XI_TouchUpdate: + xf_input_touch_remote(xfc, cookie.cc->data, XI_TouchUpdate); + break; + + case XI_TouchEnd: + xf_input_touch_remote(xfc, cookie.cc->data, XI_TouchEnd); + break; + + default: + xf_input_event(xfc, cookie.cc->data, cookie.cc->evtype); + break; + } + } + + XFreeEventData(xfc->display, cookie.vc); + return 0; +} + +#else + +int xf_input_init(xfContext* xfc, Window window) +{ + return 0; +} + +#endif + +int xf_input_handle_event(xfContext* xfc, const XEvent* event) +{ +#ifdef WITH_XI + + if (xfc->context.settings->MultiTouchInput) + { + return xf_input_handle_event_remote(xfc, event); + } + + if (xfc->context.settings->MultiTouchGestures) + { + return xf_input_handle_event_local(xfc, event); + } + +#endif + return 0; +} diff --git a/client/X11/xf_input.h b/client/X11/xf_input.h new file mode 100644 index 0000000..a961512 --- /dev/null +++ b/client/X11/xf_input.h @@ -0,0 +1,33 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * X11 Input + * + * Copyright 2013 Corey Clayton + * + * Licensed 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 FREERDP_CLIENT_X11_INPUT_H +#define FREERDP_CLIENT_X11_INPUT_H + +#include "xf_client.h" +#include "xfreerdp.h" + +#ifdef WITH_XI +#include +#endif + +int xf_input_init(xfContext* xfc, Window window); +int xf_input_handle_event(xfContext* xfc, const XEvent* event); + +#endif /* FREERDP_CLIENT_X11_INPUT_H */ diff --git a/client/X11/xf_keyboard.c b/client/X11/xf_keyboard.c new file mode 100644 index 0000000..377e9bd --- /dev/null +++ b/client/X11/xf_keyboard.c @@ -0,0 +1,655 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * X11 Keyboard Handling + * + * Copyright 2011 Marc-Andre Moreau + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include + +#include + +#include "xf_event.h" + +#include "xf_keyboard.h" + +#include +#define TAG CLIENT_TAG("x11") + +static BOOL xf_sync_kbd_state(xfContext* xfc) +{ + const UINT32 syncFlags = xf_keyboard_get_toggle_keys_state(xfc); + return freerdp_input_send_synchronize_event(xfc->context.input, syncFlags); +} + +static void xf_keyboard_clear(xfContext* xfc) +{ + ZeroMemory(xfc->KeyboardState, 256 * sizeof(BOOL)); +} + +static BOOL xf_keyboard_action_script_init(xfContext* xfc) +{ + FILE* keyScript; + char* keyCombination; + char buffer[1024] = { 0 }; + char command[1024] = { 0 }; + xfc->actionScriptExists = winpr_PathFileExists(xfc->context.settings->ActionScript); + + if (!xfc->actionScriptExists) + return FALSE; + + xfc->keyCombinations = ArrayList_New(TRUE); + + if (!xfc->keyCombinations) + return FALSE; + + ArrayList_Object(xfc->keyCombinations)->fnObjectFree = free; + sprintf_s(command, sizeof(command), "%s key", xfc->context.settings->ActionScript); + keyScript = popen(command, "r"); + + if (!keyScript) + { + xfc->actionScriptExists = FALSE; + return FALSE; + } + + while (fgets(buffer, sizeof(buffer), keyScript) != NULL) + { + char* context = NULL; + strtok_s(buffer, "\n", &context); + keyCombination = _strdup(buffer); + + if (!keyCombination || ArrayList_Add(xfc->keyCombinations, keyCombination) < 0) + { + ArrayList_Free(xfc->keyCombinations); + xfc->actionScriptExists = FALSE; + pclose(keyScript); + return FALSE; + } + } + + pclose(keyScript); + return xf_event_action_script_init(xfc); +} + +static void xf_keyboard_action_script_free(xfContext* xfc) +{ + xf_event_action_script_free(xfc); + + if (xfc->keyCombinations) + { + ArrayList_Free(xfc->keyCombinations); + xfc->keyCombinations = NULL; + xfc->actionScriptExists = FALSE; + } +} + +BOOL xf_keyboard_init(xfContext* xfc) +{ + xf_keyboard_clear(xfc); + xfc->KeyboardLayout = xfc->context.settings->KeyboardLayout; + xfc->KeyboardLayout = + freerdp_keyboard_init_ex(xfc->KeyboardLayout, xfc->context.settings->KeyboardRemappingList); + xfc->context.settings->KeyboardLayout = xfc->KeyboardLayout; + + if (xfc->modifierMap) + XFreeModifiermap(xfc->modifierMap); + + if (!(xfc->modifierMap = XGetModifierMapping(xfc->display))) + return FALSE; + + xf_keyboard_action_script_init(xfc); + return TRUE; +} + +void xf_keyboard_free(xfContext* xfc) +{ + if (xfc->modifierMap) + { + XFreeModifiermap(xfc->modifierMap); + xfc->modifierMap = NULL; + } + + xf_keyboard_action_script_free(xfc); +} + +void xf_keyboard_key_press(xfContext* xfc, BYTE keycode, KeySym keysym) +{ + if (keycode < 8) + return; + + xfc->KeyboardState[keycode] = TRUE; + + if (xf_keyboard_handle_special_keys(xfc, keysym)) + return; + + xf_keyboard_send_key(xfc, TRUE, keycode); +} + +void xf_keyboard_key_release(xfContext* xfc, BYTE keycode, KeySym keysym) +{ + if (keycode < 8) + return; + + xfc->KeyboardState[keycode] = FALSE; + xf_keyboard_handle_special_keys_release(xfc, keysym); + xf_keyboard_send_key(xfc, FALSE, keycode); +} + +void xf_keyboard_release_all_keypress(xfContext* xfc) +{ + size_t keycode; + DWORD rdp_scancode; + + for (keycode = 0; keycode < ARRAYSIZE(xfc->KeyboardState); keycode++) + { + if (xfc->KeyboardState[keycode]) + { + rdp_scancode = freerdp_keyboard_get_rdp_scancode_from_x11_keycode(keycode); + + // release tab before releasing the windows key. + // this stops the start menu from opening on unfocus event. + if (rdp_scancode == RDP_SCANCODE_LWIN) + freerdp_input_send_keyboard_event_ex(xfc->context.input, FALSE, RDP_SCANCODE_TAB); + + freerdp_input_send_keyboard_event_ex(xfc->context.input, FALSE, rdp_scancode); + xfc->KeyboardState[keycode] = FALSE; + } + } + xf_sync_kbd_state(xfc); +} + +BOOL xf_keyboard_key_pressed(xfContext* xfc, KeySym keysym) +{ + KeyCode keycode = XKeysymToKeycode(xfc->display, keysym); + return xfc->KeyboardState[keycode]; +} + +void xf_keyboard_send_key(xfContext* xfc, BOOL down, BYTE keycode) +{ + DWORD rdp_scancode; + rdpInput* input; + input = xfc->context.input; + rdp_scancode = freerdp_keyboard_get_rdp_scancode_from_x11_keycode(keycode); + + if (rdp_scancode == RDP_SCANCODE_UNKNOWN) + { + WLog_ERR(TAG, "Unknown key with X keycode 0x%02" PRIx8 "", keycode); + } + else if (rdp_scancode == RDP_SCANCODE_PAUSE && !xf_keyboard_key_pressed(xfc, XK_Control_L) && + !xf_keyboard_key_pressed(xfc, XK_Control_R)) + { + /* Pause without Ctrl has to be sent as a series of keycodes + * in a single input PDU. Pause only happens on "press"; + * no code is sent on "release". + */ + if (down) + { + freerdp_input_send_keyboard_pause_event(input); + } + } + else + { + freerdp_input_send_keyboard_event_ex(input, down, rdp_scancode); + + if ((rdp_scancode == RDP_SCANCODE_CAPSLOCK) && (down == FALSE)) + { + xf_sync_kbd_state(xfc); + } + } +} + +int xf_keyboard_read_keyboard_state(xfContext* xfc) +{ + int dummy; + Window wdummy; + UINT32 state = 0; + + if (!xfc->remote_app) + { + XQueryPointer(xfc->display, xfc->window->handle, &wdummy, &wdummy, &dummy, &dummy, &dummy, + &dummy, &state); + } + else + { + XQueryPointer(xfc->display, DefaultRootWindow(xfc->display), &wdummy, &wdummy, &dummy, + &dummy, &dummy, &dummy, &state); + } + + return state; +} + +static int xf_keyboard_get_keymask(xfContext* xfc, int keysym) +{ + int modifierpos, key, keysymMask = 0; + KeyCode keycode = XKeysymToKeycode(xfc->display, keysym); + + if (keycode == NoSymbol) + return 0; + + for (modifierpos = 0; modifierpos < 8; modifierpos++) + { + int offset = xfc->modifierMap->max_keypermod * modifierpos; + + for (key = 0; key < xfc->modifierMap->max_keypermod; key++) + { + if (xfc->modifierMap->modifiermap[offset + key] == keycode) + { + keysymMask |= 1 << modifierpos; + } + } + } + + return keysymMask; +} + +BOOL xf_keyboard_get_key_state(xfContext* xfc, int state, int keysym) +{ + int keysymMask = xf_keyboard_get_keymask(xfc, keysym); + + if (!keysymMask) + return FALSE; + + return (state & keysymMask) ? TRUE : FALSE; +} + +static BOOL xf_keyboard_set_key_state(xfContext* xfc, BOOL on, int keysym) +{ + int keysymMask; + + if (!xfc->xkbAvailable) + return FALSE; + + keysymMask = xf_keyboard_get_keymask(xfc, keysym); + + if (!keysymMask) + { + return FALSE; + } + + return XkbLockModifiers(xfc->display, XkbUseCoreKbd, keysymMask, on ? keysymMask : 0); +} + +UINT32 xf_keyboard_get_toggle_keys_state(xfContext* xfc) +{ + int state; + UINT32 toggleKeysState = 0; + state = xf_keyboard_read_keyboard_state(xfc); + + if (xf_keyboard_get_key_state(xfc, state, XK_Scroll_Lock)) + toggleKeysState |= KBD_SYNC_SCROLL_LOCK; + + if (xf_keyboard_get_key_state(xfc, state, XK_Num_Lock)) + toggleKeysState |= KBD_SYNC_NUM_LOCK; + + if (xf_keyboard_get_key_state(xfc, state, XK_Caps_Lock)) + toggleKeysState |= KBD_SYNC_CAPS_LOCK; + + if (xf_keyboard_get_key_state(xfc, state, XK_Kana_Lock)) + toggleKeysState |= KBD_SYNC_KANA_LOCK; + + return toggleKeysState; +} + +static void xk_keyboard_update_modifier_keys(xfContext* xfc) +{ + int state; + size_t i; + KeyCode keycode; + int keysyms[] = { XK_Shift_L, XK_Shift_R, XK_Alt_L, XK_Alt_R, + XK_Control_L, XK_Control_R, XK_Super_L, XK_Super_R }; + + xf_keyboard_clear(xfc); + + state = xf_keyboard_read_keyboard_state(xfc); + + for (i = 0; i < ARRAYSIZE(keysyms); i++) + { + if (xf_keyboard_get_key_state(xfc, state, keysyms[i])) + { + keycode = XKeysymToKeycode(xfc->display, keysyms[i]); + xfc->KeyboardState[keycode] = TRUE; + } + } +} + +void xf_keyboard_focus_in(xfContext* xfc) +{ + rdpInput* input; + UINT32 syncFlags, state; + Window w; + int d, x, y; + + if (!xfc->display || !xfc->window) + return; + + input = xfc->context.input; + syncFlags = xf_keyboard_get_toggle_keys_state(xfc); + freerdp_input_send_focus_in_event(input, syncFlags); + xk_keyboard_update_modifier_keys(xfc); + + /* finish with a mouse pointer position like mstsc.exe if required */ + + if (xfc->remote_app) + return; + + if (XQueryPointer(xfc->display, xfc->window->handle, &w, &w, &d, &d, &x, &y, &state)) + { + if (x >= 0 && x < xfc->window->width && y >= 0 && y < xfc->window->height) + { + xf_event_adjust_coordinates(xfc, &x, &y); + freerdp_input_send_mouse_event(input, PTR_FLAGS_MOVE, x, y); + } + } +} + +static int xf_keyboard_execute_action_script(xfContext* xfc, XF_MODIFIER_KEYS* mod, KeySym keysym) +{ + int index; + int count; + int status = 1; + FILE* keyScript; + const char* keyStr; + BOOL match = FALSE; + char* keyCombination; + char buffer[1024] = { 0 }; + char command[2048] = { 0 }; + char combination[1024] = { 0 }; + + if (!xfc->actionScriptExists) + return 1; + + if ((keysym == XK_Shift_L) || (keysym == XK_Shift_R) || (keysym == XK_Alt_L) || + (keysym == XK_Alt_R) || (keysym == XK_Control_L) || (keysym == XK_Control_R)) + { + return 1; + } + + keyStr = XKeysymToString(keysym); + + if (keyStr == 0) + { + return 1; + } + + if (mod->Shift) + winpr_str_append("Shift", combination, sizeof(combination), "+"); + + if (mod->Ctrl) + winpr_str_append("Ctrl", combination, sizeof(combination), "+"); + + if (mod->Alt) + winpr_str_append("Alt", combination, sizeof(combination), "+"); + + if (mod->Super) + winpr_str_append("Super", combination, sizeof(combination), "+"); + + winpr_str_append(keyStr, combination, sizeof(combination), NULL); + + count = ArrayList_Count(xfc->keyCombinations); + + for (index = 0; index < count; index++) + { + keyCombination = (char*)ArrayList_GetItem(xfc->keyCombinations, index); + + if (_stricmp(keyCombination, combination) == 0) + { + match = TRUE; + break; + } + } + + if (!match) + return 1; + + sprintf_s(command, sizeof(command), "%s key %s", xfc->context.settings->ActionScript, + combination); + keyScript = popen(command, "r"); + + if (!keyScript) + return -1; + + while (fgets(buffer, sizeof(buffer), keyScript) != NULL) + { + char* context = NULL; + strtok_s(buffer, "\n", &context); + + if (strcmp(buffer, "key-local") == 0) + status = 0; + } + + if (pclose(keyScript) == -1) + status = -1; + + return status; +} + +static int xk_keyboard_get_modifier_keys(xfContext* xfc, XF_MODIFIER_KEYS* mod) +{ + mod->LeftShift = xf_keyboard_key_pressed(xfc, XK_Shift_L); + mod->RightShift = xf_keyboard_key_pressed(xfc, XK_Shift_R); + mod->Shift = mod->LeftShift || mod->RightShift; + mod->LeftAlt = xf_keyboard_key_pressed(xfc, XK_Alt_L); + mod->RightAlt = xf_keyboard_key_pressed(xfc, XK_Alt_R); + mod->Alt = mod->LeftAlt || mod->RightAlt; + mod->LeftCtrl = xf_keyboard_key_pressed(xfc, XK_Control_L); + mod->RightCtrl = xf_keyboard_key_pressed(xfc, XK_Control_R); + mod->Ctrl = mod->LeftCtrl || mod->RightCtrl; + mod->LeftSuper = xf_keyboard_key_pressed(xfc, XK_Super_L); + mod->RightSuper = xf_keyboard_key_pressed(xfc, XK_Super_R); + mod->Super = mod->LeftSuper || mod->RightSuper; + return 0; +} + +BOOL xf_keyboard_handle_special_keys(xfContext* xfc, KeySym keysym) +{ + XF_MODIFIER_KEYS mod = { 0 }; + xk_keyboard_get_modifier_keys(xfc, &mod); + + // remember state of RightCtrl to ungrab keyboard if next action is release of RightCtrl + // do not return anything such that the key could be used by client if ungrab is not the goal + if (keysym == XK_Control_R) + { + if (mod.RightCtrl && xfc->firstPressRightCtrl) + { + // Right Ctrl is pressed, getting ready to ungrab + xfc->ungrabKeyboardWithRightCtrl = TRUE; + xfc->firstPressRightCtrl = FALSE; + } + } + else + { + // some other key has been pressed, abort ungrabbing + if (xfc->ungrabKeyboardWithRightCtrl) + xfc->ungrabKeyboardWithRightCtrl = FALSE; + } + + if (!xf_keyboard_execute_action_script(xfc, &mod, keysym)) + { + return TRUE; + } + + if (!xfc->remote_app && xfc->fullscreen_toggle) + { + if (keysym == XK_Return) + { + if (mod.Ctrl && mod.Alt) + { + /* Ctrl-Alt-Enter: toggle full screen */ + xf_toggle_fullscreen(xfc); + return TRUE; + } + } + } + + if ((keysym == XK_c) || (keysym == XK_C)) + { + if (mod.Ctrl && mod.Alt) + { + /* Ctrl-Alt-C: toggle control */ + if (xf_toggle_control(xfc)) + return TRUE; + } + } + +#if 0 /* set to 1 to enable multi touch gesture simulation via keyboard */ +#ifdef WITH_XRENDER + + if (!xfc->remote_app && xfc->settings->MultiTouchGestures) + { + rdpContext* ctx = &xfc->context; + + if (mod.Ctrl && mod.Alt) + { + int pdx = 0; + int pdy = 0; + int zdx = 0; + int zdy = 0; + + switch (keysym) + { + case XK_0: /* Ctrl-Alt-0: Reset scaling and panning */ + xfc->scaledWidth = xfc->sessionWidth; + xfc->scaledHeight = xfc->sessionHeight; + xfc->offset_x = 0; + xfc->offset_y = 0; + + if (!xfc->fullscreen && (xfc->sessionWidth != xfc->window->width || + xfc->sessionHeight != xfc->window->height)) + { + xf_ResizeDesktopWindow(xfc, xfc->window, xfc->sessionWidth, xfc->sessionHeight); + } + + xf_draw_screen(xfc, 0, 0, xfc->sessionWidth, xfc->sessionHeight); + return TRUE; + + case XK_1: /* Ctrl-Alt-1: Zoom in */ + zdx = zdy = 10; + break; + + case XK_2: /* Ctrl-Alt-2: Zoom out */ + zdx = zdy = -10; + break; + + case XK_3: /* Ctrl-Alt-3: Pan left */ + pdx = -10; + break; + + case XK_4: /* Ctrl-Alt-4: Pan right */ + pdx = 10; + break; + + case XK_5: /* Ctrl-Alt-5: Pan up */ + pdy = -10; + break; + + case XK_6: /* Ctrl-Alt-6: Pan up */ + pdy = 10; + break; + } + + if (pdx != 0 || pdy != 0) + { + PanningChangeEventArgs e; + EventArgsInit(&e, "xfreerdp"); + e.dx = pdx; + e.dy = pdy; + PubSub_OnPanningChange(ctx->pubSub, xfc, &e); + return TRUE; + } + + if (zdx != 0 || zdy != 0) + { + ZoomingChangeEventArgs e; + EventArgsInit(&e, "xfreerdp"); + e.dx = zdx; + e.dy = zdy; + PubSub_OnZoomingChange(ctx->pubSub, xfc, &e); + return TRUE; + } + } + } + +#endif /* WITH_XRENDER defined */ +#endif /* pinch/zoom/pan simulation */ + return FALSE; +} + +void xf_keyboard_handle_special_keys_release(xfContext* xfc, KeySym keysym) +{ + if (keysym != XK_Control_R) + return; + + xfc->firstPressRightCtrl = TRUE; + + if (!xfc->ungrabKeyboardWithRightCtrl) + return; + + // all requirements for ungrab are fulfilled, ungrabbing now + XF_MODIFIER_KEYS mod = { 0 }; + xk_keyboard_get_modifier_keys(xfc, &mod); + + if (!mod.RightCtrl) + { + if (!xfc->fullscreen) + { + xf_toggle_control(xfc); + } + + xfc->mouse_active = FALSE; + XUngrabKeyboard(xfc->display, CurrentTime); + } + + // ungrabbed + xfc->ungrabKeyboardWithRightCtrl = FALSE; +} + +BOOL xf_keyboard_set_indicators(rdpContext* context, UINT16 led_flags) +{ + xfContext* xfc = (xfContext*)context; + xf_keyboard_set_key_state(xfc, led_flags & KBD_SYNC_SCROLL_LOCK, XK_Scroll_Lock); + xf_keyboard_set_key_state(xfc, led_flags & KBD_SYNC_NUM_LOCK, XK_Num_Lock); + xf_keyboard_set_key_state(xfc, led_flags & KBD_SYNC_CAPS_LOCK, XK_Caps_Lock); + xf_keyboard_set_key_state(xfc, led_flags & KBD_SYNC_KANA_LOCK, XK_Kana_Lock); + return TRUE; +} + +BOOL xf_keyboard_set_ime_status(rdpContext* context, UINT16 imeId, UINT32 imeState, + UINT32 imeConvMode) +{ + if (!context) + return FALSE; + + WLog_WARN(TAG, + "KeyboardSetImeStatus(unitId=%04" PRIx16 ", imeState=%08" PRIx32 + ", imeConvMode=%08" PRIx32 ") ignored", + imeId, imeState, imeConvMode); + return TRUE; +} diff --git a/client/X11/xf_keyboard.h b/client/X11/xf_keyboard.h new file mode 100644 index 0000000..7492fa8 --- /dev/null +++ b/client/X11/xf_keyboard.h @@ -0,0 +1,63 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * X11 Keyboard Handling + * + * Copyright 2011 Marc-Andre Moreau + * + * Licensed 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 FREERDP_CLIENT_X11_XF_KEYBOARD_H +#define FREERDP_CLIENT_X11_XF_KEYBOARD_H + +#include + +#include "xf_client.h" +#include "xfreerdp.h" + +struct _XF_MODIFIER_KEYS +{ + BOOL Shift; + BOOL LeftShift; + BOOL RightShift; + BOOL Alt; + BOOL LeftAlt; + BOOL RightAlt; + BOOL Ctrl; + BOOL LeftCtrl; + BOOL RightCtrl; + BOOL Super; + BOOL LeftSuper; + BOOL RightSuper; +}; +typedef struct _XF_MODIFIER_KEYS XF_MODIFIER_KEYS; + +BOOL xf_keyboard_init(xfContext* xfc); +void xf_keyboard_free(xfContext* xfc); + +void xf_keyboard_key_press(xfContext* xfc, BYTE keycode, KeySym keysym); +void xf_keyboard_key_release(xfContext* xfc, BYTE keycode, KeySym keysym); +void xf_keyboard_release_all_keypress(xfContext* xfc); +BOOL xf_keyboard_key_pressed(xfContext* xfc, KeySym keysym); +void xf_keyboard_send_key(xfContext* xfc, BOOL down, BYTE keycode); +int xf_keyboard_read_keyboard_state(xfContext* xfc); +BOOL xf_keyboard_get_key_state(xfContext* xfc, int state, int keysym); +UINT32 xf_keyboard_get_toggle_keys_state(xfContext* xfc); +void xf_keyboard_focus_in(xfContext* xfc); +BOOL xf_keyboard_handle_special_keys(xfContext* xfc, KeySym keysym); +void xf_keyboard_handle_special_keys_release(xfContext* xfc, KeySym keysym); +BOOL xf_keyboard_set_indicators(rdpContext* context, UINT16 led_flags); +BOOL xf_keyboard_set_ime_status(rdpContext* context, UINT16 imeId, UINT32 imeState, + UINT32 imeConvMode); + +#endif /* FREERDP_CLIENT_X11_XF_KEYBOARD_H */ diff --git a/client/X11/xf_monitor.c b/client/X11/xf_monitor.c new file mode 100644 index 0000000..72a3dbe --- /dev/null +++ b/client/X11/xf_monitor.c @@ -0,0 +1,573 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * X11 Monitor Handling + * + * Copyright 2011 Marc-Andre Moreau + * Copyright 2017 David Fort + * Copyright 2018 Kai Harms + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include +#include +#include + +#include + +#include + +#define TAG CLIENT_TAG("x11") + +#ifdef WITH_XINERAMA +#include +#endif + +#ifdef WITH_XRANDR +#include +#include + +#if (RANDR_MAJOR * 100 + RANDR_MINOR) >= 105 +#define USABLE_XRANDR +#endif + +#endif + +#include "xf_monitor.h" + +/* See MSDN Section on Multiple Display Monitors: http://msdn.microsoft.com/en-us/library/dd145071 + */ + +int xf_list_monitors(xfContext* xfc) +{ + Display* display; + int major, minor; + int i, nmonitors = 0; + display = XOpenDisplay(NULL); + + if (!display) + { + WLog_ERR(TAG, "failed to open X display"); + return -1; + } + +#if defined(USABLE_XRANDR) + + if (XRRQueryExtension(xfc->display, &major, &minor) && + (XRRQueryVersion(xfc->display, &major, &minor) == True) && (major * 100 + minor >= 105)) + { + XRRMonitorInfo* monitors = + XRRGetMonitors(xfc->display, DefaultRootWindow(xfc->display), 1, &nmonitors); + + for (i = 0; i < nmonitors; i++) + { + printf(" %s [%d] %dx%d\t+%d+%d\n", monitors[i].primary ? "*" : " ", i, + monitors[i].width, monitors[i].height, monitors[i].x, monitors[i].y); + } + + XRRFreeMonitors(monitors); + } + else +#endif +#ifdef WITH_XINERAMA + if (XineramaQueryExtension(display, &major, &minor)) + { + if (XineramaIsActive(display)) + { + XineramaScreenInfo* screen = XineramaQueryScreens(display, &nmonitors); + + for (i = 0; i < nmonitors; i++) + { + printf(" %s [%d] %hdx%hd\t+%hd+%hd\n", (i == 0) ? "*" : " ", i, + screen[i].width, screen[i].height, screen[i].x_org, screen[i].y_org); + } + + XFree(screen); + } + } + else +#else + { + Screen* screen = ScreenOfDisplay(display, DefaultScreen(display)); + printf(" * [0] %dx%d\t+0+0\n", WidthOfScreen(screen), HeightOfScreen(screen)); + } + +#endif + XCloseDisplay(display); + return 0; +} + +static BOOL xf_is_monitor_id_active(xfContext* xfc, UINT32 id) +{ + UINT32 index; + rdpSettings* settings = xfc->context.settings; + + if (!settings->NumMonitorIds) + return TRUE; + + for (index = 0; index < settings->NumMonitorIds; index++) + { + if (settings->MonitorIds[index] == id) + return TRUE; + } + + return FALSE; +} + +BOOL xf_detect_monitors(xfContext* xfc, UINT32* pMaxWidth, UINT32* pMaxHeight) +{ + int nmonitors = 0; + int monitor_index = 0; + BOOL primaryMonitorFound = FALSE; + VIRTUAL_SCREEN* vscreen; + rdpSettings* settings; + int mouse_x, mouse_y, _dummy_i; + Window _dummy_w; + int current_monitor = 0; + Screen* screen; + MONITOR_INFO* monitor; +#if defined WITH_XINERAMA || defined WITH_XRANDR + int major, minor; +#endif +#if defined(USABLE_XRANDR) + XRRMonitorInfo* rrmonitors = NULL; + BOOL useXRandr = FALSE; +#endif + + if (!xfc || !pMaxWidth || !pMaxHeight || !xfc->context.settings) + return FALSE; + + settings = xfc->context.settings; + vscreen = &xfc->vscreen; + *pMaxWidth = settings->DesktopWidth; + *pMaxHeight = settings->DesktopHeight; + + /* get mouse location */ + if (!XQueryPointer(xfc->display, DefaultRootWindow(xfc->display), &_dummy_w, &_dummy_w, + &mouse_x, &mouse_y, &_dummy_i, &_dummy_i, (void*)&_dummy_i)) + mouse_x = mouse_y = 0; + +#if defined(USABLE_XRANDR) + + if (XRRQueryExtension(xfc->display, &major, &minor) && + (XRRQueryVersion(xfc->display, &major, &minor) == True) && (major * 100 + minor >= 105)) + { + XRRMonitorInfo* rrmonitors = + XRRGetMonitors(xfc->display, DefaultRootWindow(xfc->display), 1, &vscreen->nmonitors); + + if (vscreen->nmonitors > 16) + vscreen->nmonitors = 0; + + if (vscreen->nmonitors) + { + int i; + + for (i = 0; i < vscreen->nmonitors; i++) + { + vscreen->monitors[i].area.left = rrmonitors[i].x; + vscreen->monitors[i].area.top = rrmonitors[i].y; + vscreen->monitors[i].area.right = rrmonitors[i].x + rrmonitors[i].width - 1; + vscreen->monitors[i].area.bottom = rrmonitors[i].y + rrmonitors[i].height - 1; + vscreen->monitors[i].primary = rrmonitors[i].primary > 0; + } + } + + XRRFreeMonitors(rrmonitors); + useXRandr = TRUE; + } + else +#endif +#ifdef WITH_XINERAMA + if (XineramaQueryExtension(xfc->display, &major, &minor) && XineramaIsActive(xfc->display)) + { + XineramaScreenInfo* screenInfo = XineramaQueryScreens(xfc->display, &vscreen->nmonitors); + + if (vscreen->nmonitors > 16) + vscreen->nmonitors = 0; + + if (vscreen->nmonitors) + { + int i; + + for (i = 0; i < vscreen->nmonitors; i++) + { + vscreen->monitors[i].area.left = screenInfo[i].x_org; + vscreen->monitors[i].area.top = screenInfo[i].y_org; + vscreen->monitors[i].area.right = screenInfo[i].x_org + screenInfo[i].width - 1; + vscreen->monitors[i].area.bottom = screenInfo[i].y_org + screenInfo[i].height - 1; + } + } + + XFree(screenInfo); + } + +#endif + xfc->fullscreenMonitors.top = xfc->fullscreenMonitors.bottom = xfc->fullscreenMonitors.left = + xfc->fullscreenMonitors.right = 0; + + /* Determine which monitor that the mouse cursor is on */ + if (vscreen->monitors) + { + int i; + + for (i = 0; i < vscreen->nmonitors; i++) + { + if ((mouse_x >= vscreen->monitors[i].area.left) && + (mouse_x <= vscreen->monitors[i].area.right) && + (mouse_y >= vscreen->monitors[i].area.top) && + (mouse_y <= vscreen->monitors[i].area.bottom)) + { + current_monitor = i; + break; + } + } + } + + /* + Even for a single monitor, we need to calculate the virtual screen to support + window managers that do not implement all X window state hints. + + If the user did not request multiple monitor or is using workarea + without remote app, we force the number of monitors be 1 so later + the rest of the client don't end up using more monitors than the user desires. + */ + if ((!settings->UseMultimon && !settings->SpanMonitors) || + (settings->Workarea && !settings->RemoteApplicationMode)) + { + /* If no monitors were specified on the command-line then set the current monitor as active + */ + if (!settings->NumMonitorIds) + { + settings->MonitorIds[0] = current_monitor; + } + + /* Always sets number of monitors from command-line to just 1. + * If the monitor is invalid then we will default back to current monitor + * later as a fallback. So, there is no need to validate command-line entry here. + */ + settings->NumMonitorIds = 1; + } + + /* WORKAROUND: With Remote Application Mode - using NET_WM_WORKAREA + * causes issues with the ability to fully size the window vertically + * (the bottom of the window area is never updated). So, we just set + * the workArea to match the full Screen width/height. + */ + if (settings->RemoteApplicationMode || !xf_GetWorkArea(xfc)) + { + /* + if only 1 monitor is enabled, use monitor area + this is required in case of a screen composed of more than one monitor + but user did not enable multimonitor + */ + if ((settings->NumMonitorIds == 1) && (vscreen->nmonitors > current_monitor)) + { + monitor = vscreen->monitors + current_monitor; + + if (!monitor) + return FALSE; + + xfc->workArea.x = monitor->area.left; + xfc->workArea.y = monitor->area.top; + xfc->workArea.width = monitor->area.right - monitor->area.left + 1; + xfc->workArea.height = monitor->area.bottom - monitor->area.top + 1; + } + else + { + xfc->workArea.x = 0; + xfc->workArea.y = 0; + xfc->workArea.width = WidthOfScreen(xfc->screen); + xfc->workArea.height = HeightOfScreen(xfc->screen); + } + } + + if (settings->Fullscreen) + { + *pMaxWidth = WidthOfScreen(xfc->screen); + *pMaxHeight = HeightOfScreen(xfc->screen); + } + else if (settings->Workarea) + { + *pMaxWidth = xfc->workArea.width; + *pMaxHeight = xfc->workArea.height; + } + else if (settings->PercentScreen) + { + /* If we have specific monitor information then limit the PercentScreen value + * to only affect the current monitor vs. the entire desktop + */ + if (vscreen->nmonitors > 0) + { + if (!vscreen->monitors) + return FALSE; + + *pMaxWidth = vscreen->monitors[current_monitor].area.right - + vscreen->monitors[current_monitor].area.left + 1; + *pMaxHeight = vscreen->monitors[current_monitor].area.bottom - + vscreen->monitors[current_monitor].area.top + 1; + + if (settings->PercentScreenUseWidth) + *pMaxWidth = ((vscreen->monitors[current_monitor].area.right - + vscreen->monitors[current_monitor].area.left + 1) * + settings->PercentScreen) / + 100; + + if (settings->PercentScreenUseHeight) + *pMaxHeight = ((vscreen->monitors[current_monitor].area.bottom - + vscreen->monitors[current_monitor].area.top + 1) * + settings->PercentScreen) / + 100; + } + else + { + *pMaxWidth = xfc->workArea.width; + *pMaxHeight = xfc->workArea.height; + + if (settings->PercentScreenUseWidth) + *pMaxWidth = (xfc->workArea.width * settings->PercentScreen) / 100; + + if (settings->PercentScreenUseHeight) + *pMaxHeight = (xfc->workArea.height * settings->PercentScreen) / 100; + } + } + else if (settings->DesktopWidth && settings->DesktopHeight) + { + *pMaxWidth = settings->DesktopWidth; + *pMaxHeight = settings->DesktopHeight; + } + + /* Create array of all active monitors by taking into account monitors requested on the + * command-line */ + { + int i; + + for (i = 0; i < vscreen->nmonitors; i++) + { + MONITOR_ATTRIBUTES* attrs; + + if (!xf_is_monitor_id_active(xfc, (UINT32)i)) + continue; + + if (!vscreen->monitors) + return FALSE; + + settings->MonitorDefArray[nmonitors].x = + (vscreen->monitors[i].area.left * + (settings->PercentScreenUseWidth ? settings->PercentScreen : 100)) / + 100; + settings->MonitorDefArray[nmonitors].y = + (vscreen->monitors[i].area.top * + (settings->PercentScreenUseHeight ? settings->PercentScreen : 100)) / + 100; + settings->MonitorDefArray[nmonitors].width = + ((vscreen->monitors[i].area.right - vscreen->monitors[i].area.left + 1) * + (settings->PercentScreenUseWidth ? settings->PercentScreen : 100)) / + 100; + settings->MonitorDefArray[nmonitors].height = + ((vscreen->monitors[i].area.bottom - vscreen->monitors[i].area.top + 1) * + (settings->PercentScreenUseWidth ? settings->PercentScreen : 100)) / + 100; + settings->MonitorDefArray[nmonitors].orig_screen = i; +#ifdef USABLE_XRANDR + + if (useXRandr && rrmonitors) + { + Rotation rot, ret; + attrs = &settings->MonitorDefArray[nmonitors].attributes; + attrs->physicalWidth = rrmonitors[i].mwidth; + attrs->physicalHeight = rrmonitors[i].mheight; + ret = XRRRotations(xfc->display, i, &rot); + attrs->orientation = rot; + } + +#endif + + if ((UINT32)i == settings->MonitorIds[0]) + { + settings->MonitorDefArray[nmonitors].is_primary = TRUE; + settings->MonitorLocalShiftX = settings->MonitorDefArray[nmonitors].x; + settings->MonitorLocalShiftY = settings->MonitorDefArray[nmonitors].y; + primaryMonitorFound = TRUE; + } + + nmonitors++; + } + } + + /* If no monitor is active(bogus command-line monitor specification) - then lets try to fallback + * to go fullscreen on the current monitor only */ + if (nmonitors == 0 && vscreen->nmonitors > 0) + { + INT32 width, height; + if (!vscreen->monitors) + return FALSE; + + width = vscreen->monitors[current_monitor].area.right - + vscreen->monitors[current_monitor].area.left + 1L; + height = vscreen->monitors[current_monitor].area.bottom - + vscreen->monitors[current_monitor].area.top + 1L; + + settings->MonitorDefArray[0].x = vscreen->monitors[current_monitor].area.left; + settings->MonitorDefArray[0].y = vscreen->monitors[current_monitor].area.top; + settings->MonitorDefArray[0].width = MIN(width, (INT64)(*pMaxWidth)); + settings->MonitorDefArray[0].height = MIN(height, (INT64)(*pMaxHeight)); + settings->MonitorDefArray[0].orig_screen = current_monitor; + nmonitors = 1; + } + + settings->MonitorCount = nmonitors; + + /* If we have specific monitor information */ + if (settings->MonitorCount) + { + UINT32 i; + /* Initialize bounding rectangle for all monitors */ + int vX = settings->MonitorDefArray[0].x; + int vY = settings->MonitorDefArray[0].y; + int vR = vX + settings->MonitorDefArray[0].width; + int vB = vY + settings->MonitorDefArray[0].height; + xfc->fullscreenMonitors.top = xfc->fullscreenMonitors.bottom = + xfc->fullscreenMonitors.left = xfc->fullscreenMonitors.right = + settings->MonitorDefArray[0].orig_screen; + + /* Calculate bounding rectangle around all monitors to be used AND + * also set the Xinerama indices which define left/top/right/bottom monitors. + */ + for (i = 1; i < settings->MonitorCount; i++) + { + /* does the same as gdk_rectangle_union */ + int destX = MIN(vX, settings->MonitorDefArray[i].x); + int destY = MIN(vY, settings->MonitorDefArray[i].y); + int destR = + MAX(vR, settings->MonitorDefArray[i].x + settings->MonitorDefArray[i].width); + int destB = + MAX(vB, settings->MonitorDefArray[i].y + settings->MonitorDefArray[i].height); + + if (vX != destX) + xfc->fullscreenMonitors.left = settings->MonitorDefArray[i].orig_screen; + + if (vY != destY) + xfc->fullscreenMonitors.top = settings->MonitorDefArray[i].orig_screen; + + if (vR != destR) + xfc->fullscreenMonitors.right = settings->MonitorDefArray[i].orig_screen; + + if (vB != destB) + xfc->fullscreenMonitors.bottom = settings->MonitorDefArray[i].orig_screen; + + vX = destX / ((settings->PercentScreenUseWidth ? settings->PercentScreen : 100) / 100.); + vY = + destY / ((settings->PercentScreenUseHeight ? settings->PercentScreen : 100) / 100.); + vR = destR / ((settings->PercentScreenUseWidth ? settings->PercentScreen : 100) / 100.); + vB = + destB / ((settings->PercentScreenUseHeight ? settings->PercentScreen : 100) / 100.); + } + + vscreen->area.left = 0; + vscreen->area.right = vR - vX - 1; + vscreen->area.top = 0; + vscreen->area.bottom = vB - vY - 1; + + if (settings->Workarea) + { + vscreen->area.top = xfc->workArea.y; + vscreen->area.bottom = xfc->workArea.height + xfc->workArea.y - 1; + } + + if (!primaryMonitorFound) + { + /* If we have a command line setting we should use it */ + if (settings->NumMonitorIds) + { + /* The first monitor is the first in the setting which should be used */ + monitor_index = settings->MonitorIds[0]; + } + else + { + /* This is the same as when we would trust the Xinerama results.. + and set the monitor index to zero. + The monitor listed with /monitor-list on index zero is always the primary + */ + screen = DefaultScreenOfDisplay(xfc->display); + monitor_index = XScreenNumberOfScreen(screen); + } + + int j = monitor_index; + + /* If the "default" monitor is not 0,0 use it */ + if (settings->MonitorDefArray[j].x != 0 || settings->MonitorDefArray[j].y != 0) + { + settings->MonitorDefArray[j].is_primary = TRUE; + settings->MonitorLocalShiftX = settings->MonitorDefArray[j].x; + settings->MonitorLocalShiftY = settings->MonitorDefArray[j].y; + primaryMonitorFound = TRUE; + } + else + { + /* Lets try to see if there is a monitor with a 0,0 coordinate and use it as a + * fallback*/ + for (i = 0; i < settings->MonitorCount; i++) + { + if (!primaryMonitorFound && settings->MonitorDefArray[i].x == 0 && + settings->MonitorDefArray[i].y == 0) + { + settings->MonitorDefArray[i].is_primary = TRUE; + settings->MonitorLocalShiftX = settings->MonitorDefArray[i].x; + settings->MonitorLocalShiftY = settings->MonitorDefArray[i].y; + primaryMonitorFound = TRUE; + } + } + } + } + + /* Subtract monitor shift from monitor variables for server-side use. + * We maintain monitor shift value as Window requires the primary monitor to have a + * coordinate of 0,0 In some X configurations, no monitor may have a coordinate of 0,0. This + * can also be happen if the user requests specific monitors from the command-line as well. + * So, we make sure to translate our primary monitor's upper-left corner to 0,0 on the + * server. + */ + for (i = 0; i < settings->MonitorCount; i++) + { + settings->MonitorDefArray[i].x = + settings->MonitorDefArray[i].x - settings->MonitorLocalShiftX; + settings->MonitorDefArray[i].y = + settings->MonitorDefArray[i].y - settings->MonitorLocalShiftY; + } + + /* Set the desktop width and height according to the bounding rectangle around the active + * monitors */ + *pMaxWidth = MIN(*pMaxWidth, (UINT32)vscreen->area.right - vscreen->area.left + 1); + *pMaxHeight = MIN(*pMaxHeight, (UINT32)vscreen->area.bottom - vscreen->area.top + 1); + } + + /* some 2008 server freeze at logon if we announce support for monitor layout PDU with + * #monitors < 2. So let's announce it only if we have more than 1 monitor. + */ + if (settings->MonitorCount) + settings->SupportMonitorLayoutPdu = TRUE; + +#ifdef USABLE_XRANDR + + if (rrmonitors) + XRRFreeMonitors(rrmonitors); + +#endif + return TRUE; +} diff --git a/client/X11/xf_monitor.h b/client/X11/xf_monitor.h new file mode 100644 index 0000000..2e3cd2f --- /dev/null +++ b/client/X11/xf_monitor.h @@ -0,0 +1,50 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * X11 Monitor Handling + * + * Copyright 2011 Marc-Andre Moreau + * + * Licensed 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 FREERDP_CLIENT_X11_MONITOR_H +#define FREERDP_CLIENT_X11_MONITOR_H + +#include +#include + +struct _MONITOR_INFO +{ + RECTANGLE_16 area; + RECTANGLE_16 workarea; + BOOL primary; +}; +typedef struct _MONITOR_INFO MONITOR_INFO; + +struct _VIRTUAL_SCREEN +{ + int nmonitors; + RECTANGLE_16 area; + RECTANGLE_16 workarea; + MONITOR_INFO* monitors; +}; +typedef struct _VIRTUAL_SCREEN VIRTUAL_SCREEN; + +#include "xf_client.h" +#include "xfreerdp.h" + +FREERDP_API int xf_list_monitors(xfContext* xfc); +FREERDP_API BOOL xf_detect_monitors(xfContext* xfc, UINT32* pWidth, UINT32* pHeight); +FREERDP_API void xf_monitors_free(xfContext* xfc); + +#endif /* FREERDP_CLIENT_X11_MONITOR_H */ diff --git a/client/X11/xf_rail.c b/client/X11/xf_rail.c new file mode 100644 index 0000000..090f599 --- /dev/null +++ b/client/X11/xf_rail.c @@ -0,0 +1,1200 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * X11 RAIL + * + * Copyright 2011 Marc-Andre Moreau + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include + +#include +#include + +#include "xf_window.h" +#include "xf_rail.h" + +#define TAG CLIENT_TAG("x11") + +static const char* error_code_names[] = { "RAIL_EXEC_S_OK", + "RAIL_EXEC_E_HOOK_NOT_LOADED", + "RAIL_EXEC_E_DECODE_FAILED", + "RAIL_EXEC_E_NOT_IN_ALLOWLIST", + "RAIL_EXEC_E_FILE_NOT_FOUND", + "RAIL_EXEC_E_FAIL", + "RAIL_EXEC_E_SESSION_LOCKED" }; + +#ifdef WITH_DEBUG_RAIL +static const char* movetype_names[] = { + "(invalid)", "RAIL_WMSZ_LEFT", "RAIL_WMSZ_RIGHT", + "RAIL_WMSZ_TOP", "RAIL_WMSZ_TOPLEFT", "RAIL_WMSZ_TOPRIGHT", + "RAIL_WMSZ_BOTTOM", "RAIL_WMSZ_BOTTOMLEFT", "RAIL_WMSZ_BOTTOMRIGHT", + "RAIL_WMSZ_MOVE", "RAIL_WMSZ_KEYMOVE", "RAIL_WMSZ_KEYSIZE" +}; +#endif + +struct xf_rail_icon +{ + long* data; + int length; +}; +typedef struct xf_rail_icon xfRailIcon; + +struct xf_rail_icon_cache +{ + xfRailIcon* entries; + UINT32 numCaches; + UINT32 numCacheEntries; + xfRailIcon scratch; +}; + +void xf_rail_enable_remoteapp_mode(xfContext* xfc) +{ + if (!xfc->remote_app) + { + xfc->remote_app = TRUE; + xfc->drawable = xf_CreateDummyWindow(xfc); + xf_DestroyDesktopWindow(xfc, xfc->window); + xfc->window = NULL; + } +} + +void xf_rail_disable_remoteapp_mode(xfContext* xfc) +{ + if (xfc->remote_app) + { + xfc->remote_app = FALSE; + xf_DestroyDummyWindow(xfc, xfc->drawable); + xf_create_window(xfc); + } +} + +void xf_rail_send_activate(xfContext* xfc, Window xwindow, BOOL enabled) +{ + xfAppWindow* appWindow; + RAIL_ACTIVATE_ORDER activate; + appWindow = xf_AppWindowFromX11Window(xfc, xwindow); + + if (!appWindow) + return; + + if (enabled) + xf_SetWindowStyle(xfc, appWindow, appWindow->dwStyle, appWindow->dwExStyle); + else + xf_SetWindowStyle(xfc, appWindow, 0, 0); + + activate.windowId = appWindow->windowId; + activate.enabled = enabled; + xfc->rail->ClientActivate(xfc->rail, &activate); +} + +void xf_rail_send_client_system_command(xfContext* xfc, UINT32 windowId, UINT16 command) +{ + RAIL_SYSCOMMAND_ORDER syscommand; + syscommand.windowId = windowId; + syscommand.command = command; + xfc->rail->ClientSystemCommand(xfc->rail, &syscommand); +} + +/** + * The position of the X window can become out of sync with the RDP window + * if the X window is moved locally by the window manager. In this event + * send an update to the RDP server informing it of the new window position + * and size. + */ +void xf_rail_adjust_position(xfContext* xfc, xfAppWindow* appWindow) +{ + RAIL_WINDOW_MOVE_ORDER windowMove; + + if (!appWindow->is_mapped || appWindow->local_move.state != LMS_NOT_ACTIVE) + return; + + /* If current window position disagrees with RDP window position, send update to RDP server */ + if (appWindow->x != appWindow->windowOffsetX || appWindow->y != appWindow->windowOffsetY || + appWindow->width != (INT64)appWindow->windowWidth || + appWindow->height != (INT64)appWindow->windowHeight) + { + windowMove.windowId = appWindow->windowId; + /* + * Calculate new size/position for the rail window(new values for + * windowOffsetX/windowOffsetY/windowWidth/windowHeight) on the server + */ + windowMove.left = appWindow->x - appWindow->resizeMarginLeft; + windowMove.top = appWindow->y - appWindow->resizeMarginTop; + windowMove.right = appWindow->x + appWindow->width + appWindow->resizeMarginRight; + windowMove.bottom = appWindow->y + appWindow->height + appWindow->resizeMarginBottom; + xfc->rail->ClientWindowMove(xfc->rail, &windowMove); + } +} + +void xf_rail_end_local_move(xfContext* xfc, xfAppWindow* appWindow) +{ + int x, y; + int child_x; + int child_y; + unsigned int mask; + Window root_window; + Window child_window; + RAIL_WINDOW_MOVE_ORDER windowMove; + rdpInput* input = xfc->context.input; + /* + * For keyboard moves send and explicit update to RDP server + */ + windowMove.windowId = appWindow->windowId; + /* + * Calculate new size/position for the rail window(new values for + * windowOffsetX/windowOffsetY/windowWidth/windowHeight) on the server + * + */ + windowMove.left = appWindow->x - appWindow->resizeMarginLeft; + windowMove.top = appWindow->y - appWindow->resizeMarginTop; + windowMove.right = + appWindow->x + + appWindow->width + appWindow->resizeMarginRight; /* In the update to RDP the position is one past the window */ + windowMove.bottom = appWindow->y + appWindow->height + appWindow->resizeMarginBottom; + xfc->rail->ClientWindowMove(xfc->rail, &windowMove); + /* + * Simulate button up at new position to end the local move (per RDP spec) + */ + XQueryPointer(xfc->display, appWindow->handle, &root_window, &child_window, &x, &y, &child_x, + &child_y, &mask); + + /* only send the mouse coordinates if not a keyboard move or size */ + if ((appWindow->local_move.direction != _NET_WM_MOVERESIZE_MOVE_KEYBOARD) && + (appWindow->local_move.direction != _NET_WM_MOVERESIZE_SIZE_KEYBOARD)) + { + freerdp_input_send_mouse_event(input, PTR_FLAGS_BUTTON1, x, y); + } + + /* + * Proactively update the RAIL window dimensions. There is a race condition where + * we can start to receive GDI orders for the new window dimensions before we + * receive the RAIL ORDER for the new window size. This avoids that race condition. + */ + appWindow->windowOffsetX = appWindow->x; + appWindow->windowOffsetY = appWindow->y; + appWindow->windowWidth = appWindow->width; + appWindow->windowHeight = appWindow->height; + appWindow->local_move.state = LMS_TERMINATING; +} + +static void xf_rail_invalidate_region(xfContext* xfc, REGION16* invalidRegion) +{ + int index; + int count = 0; + RECTANGLE_16 updateRect; + RECTANGLE_16 windowRect; + ULONG_PTR* pKeys = NULL; + xfAppWindow* appWindow; + const RECTANGLE_16* extents; + REGION16 windowInvalidRegion; + region16_init(&windowInvalidRegion); + if (xfc->railWindows) + count = HashTable_GetKeys(xfc->railWindows, &pKeys); + + for (index = 0; index < count; index++) + { + appWindow = xf_rail_get_window(xfc, *(UINT64*)pKeys[index]); + + if (appWindow) + { + windowRect.left = MAX(appWindow->x, 0); + windowRect.top = MAX(appWindow->y, 0); + windowRect.right = MAX(appWindow->x + appWindow->width, 0); + windowRect.bottom = MAX(appWindow->y + appWindow->height, 0); + region16_clear(&windowInvalidRegion); + region16_intersect_rect(&windowInvalidRegion, invalidRegion, &windowRect); + + if (!region16_is_empty(&windowInvalidRegion)) + { + extents = region16_extents(&windowInvalidRegion); + updateRect.left = extents->left - appWindow->x; + updateRect.top = extents->top - appWindow->y; + updateRect.right = extents->right - appWindow->x; + updateRect.bottom = extents->bottom - appWindow->y; + xf_UpdateWindowArea(xfc, appWindow, updateRect.left, updateRect.top, + updateRect.right - updateRect.left, + updateRect.bottom - updateRect.top); + } + } + } + + free(pKeys); + region16_uninit(&windowInvalidRegion); +} + +void xf_rail_paint(xfContext* xfc, INT32 uleft, INT32 utop, UINT32 uright, UINT32 ubottom) +{ + REGION16 invalidRegion; + RECTANGLE_16 invalidRect; + invalidRect.left = uleft; + invalidRect.top = utop; + invalidRect.right = uright; + invalidRect.bottom = ubottom; + region16_init(&invalidRegion); + region16_union_rect(&invalidRegion, &invalidRegion, &invalidRect); + xf_rail_invalidate_region(xfc, &invalidRegion); + region16_uninit(&invalidRegion); +} + +/* RemoteApp Core Protocol Extension */ + +static BOOL xf_rail_window_common(rdpContext* context, const WINDOW_ORDER_INFO* orderInfo, + const WINDOW_STATE_ORDER* windowState) +{ + xfAppWindow* appWindow = NULL; + xfContext* xfc = (xfContext*)context; + UINT32 fieldFlags = orderInfo->fieldFlags; + BOOL position_or_size_updated = FALSE; + appWindow = xf_rail_get_window(xfc, orderInfo->windowId); + + if (fieldFlags & WINDOW_ORDER_STATE_NEW) + { + if (!appWindow) + appWindow = xf_rail_add_window(xfc, orderInfo->windowId, windowState->windowOffsetX, + windowState->windowOffsetY, windowState->windowWidth, + windowState->windowHeight, 0xFFFFFFFF); + + if (!appWindow) + return FALSE; + + appWindow->dwStyle = windowState->style; + appWindow->dwExStyle = windowState->extendedStyle; + + /* Ensure window always gets a window title */ + if (fieldFlags & WINDOW_ORDER_FIELD_TITLE) + { + char* title = NULL; + + if (windowState->titleInfo.length == 0) + { + if (!(title = _strdup(""))) + { + WLog_ERR(TAG, "failed to duplicate empty window title string"); + /* error handled below */ + } + } + else if (ConvertFromUnicode(CP_UTF8, 0, (WCHAR*)windowState->titleInfo.string, + windowState->titleInfo.length / 2, &title, 0, NULL, + NULL) < 1) + { + WLog_ERR(TAG, "failed to convert window title"); + /* error handled below */ + } + + appWindow->title = title; + } + else + { + if (!(appWindow->title = _strdup("RdpRailWindow"))) + WLog_ERR(TAG, "failed to duplicate default window title string"); + } + + if (!appWindow->title) + { + free(appWindow); + return FALSE; + } + + xf_AppWindowInit(xfc, appWindow); + } + + if (!appWindow) + return FALSE; + + /* Keep track of any position/size update so that we can force a refresh of the window */ + if ((fieldFlags & WINDOW_ORDER_FIELD_WND_OFFSET) || + (fieldFlags & WINDOW_ORDER_FIELD_WND_SIZE) || + (fieldFlags & WINDOW_ORDER_FIELD_CLIENT_AREA_OFFSET) || + (fieldFlags & WINDOW_ORDER_FIELD_CLIENT_AREA_SIZE) || + (fieldFlags & WINDOW_ORDER_FIELD_WND_CLIENT_DELTA) || + (fieldFlags & WINDOW_ORDER_FIELD_VIS_OFFSET) || + (fieldFlags & WINDOW_ORDER_FIELD_VISIBILITY)) + { + position_or_size_updated = TRUE; + } + + /* Update Parameters */ + + if (fieldFlags & WINDOW_ORDER_FIELD_WND_OFFSET) + { + appWindow->windowOffsetX = windowState->windowOffsetX; + appWindow->windowOffsetY = windowState->windowOffsetY; + } + + if (fieldFlags & WINDOW_ORDER_FIELD_WND_SIZE) + { + appWindow->windowWidth = windowState->windowWidth; + appWindow->windowHeight = windowState->windowHeight; + } + + if (fieldFlags & WINDOW_ORDER_FIELD_RESIZE_MARGIN_X) + { + appWindow->resizeMarginLeft = windowState->resizeMarginLeft; + appWindow->resizeMarginRight = windowState->resizeMarginRight; + } + + if (fieldFlags & WINDOW_ORDER_FIELD_RESIZE_MARGIN_Y) + { + appWindow->resizeMarginTop = windowState->resizeMarginTop; + appWindow->resizeMarginBottom = windowState->resizeMarginBottom; + } + + if (fieldFlags & WINDOW_ORDER_FIELD_OWNER) + { + appWindow->ownerWindowId = windowState->ownerWindowId; + } + + if (fieldFlags & WINDOW_ORDER_FIELD_STYLE) + { + appWindow->dwStyle = windowState->style; + appWindow->dwExStyle = windowState->extendedStyle; + } + + if (fieldFlags & WINDOW_ORDER_FIELD_SHOW) + { + appWindow->showState = windowState->showState; + } + + if (fieldFlags & WINDOW_ORDER_FIELD_TITLE) + { + char* title = NULL; + + if (windowState->titleInfo.length == 0) + { + if (!(title = _strdup(""))) + { + WLog_ERR(TAG, "failed to duplicate empty window title string"); + return FALSE; + } + } + else if (ConvertFromUnicode(CP_UTF8, 0, (WCHAR*)windowState->titleInfo.string, + windowState->titleInfo.length / 2, &title, 0, NULL, NULL) < 1) + { + WLog_ERR(TAG, "failed to convert window title"); + return FALSE; + } + + free(appWindow->title); + appWindow->title = title; + } + + if (fieldFlags & WINDOW_ORDER_FIELD_CLIENT_AREA_OFFSET) + { + appWindow->clientOffsetX = windowState->clientOffsetX; + appWindow->clientOffsetY = windowState->clientOffsetY; + } + + if (fieldFlags & WINDOW_ORDER_FIELD_CLIENT_AREA_SIZE) + { + appWindow->clientAreaWidth = windowState->clientAreaWidth; + appWindow->clientAreaHeight = windowState->clientAreaHeight; + } + + if (fieldFlags & WINDOW_ORDER_FIELD_WND_CLIENT_DELTA) + { + appWindow->windowClientDeltaX = windowState->windowClientDeltaX; + appWindow->windowClientDeltaY = windowState->windowClientDeltaY; + } + + if (fieldFlags & WINDOW_ORDER_FIELD_WND_RECTS) + { + if (appWindow->windowRects) + { + free(appWindow->windowRects); + appWindow->windowRects = NULL; + } + + appWindow->numWindowRects = windowState->numWindowRects; + + if (appWindow->numWindowRects) + { + appWindow->windowRects = + (RECTANGLE_16*)calloc(appWindow->numWindowRects, sizeof(RECTANGLE_16)); + + if (!appWindow->windowRects) + return FALSE; + + CopyMemory(appWindow->windowRects, windowState->windowRects, + appWindow->numWindowRects * sizeof(RECTANGLE_16)); + } + } + + if (fieldFlags & WINDOW_ORDER_FIELD_VIS_OFFSET) + { + appWindow->visibleOffsetX = windowState->visibleOffsetX; + appWindow->visibleOffsetY = windowState->visibleOffsetY; + } + + if (fieldFlags & WINDOW_ORDER_FIELD_VISIBILITY) + { + if (appWindow->visibilityRects) + { + free(appWindow->visibilityRects); + appWindow->visibilityRects = NULL; + } + + appWindow->numVisibilityRects = windowState->numVisibilityRects; + + if (appWindow->numVisibilityRects) + { + appWindow->visibilityRects = + (RECTANGLE_16*)calloc(appWindow->numVisibilityRects, sizeof(RECTANGLE_16)); + + if (!appWindow->visibilityRects) + return FALSE; + + CopyMemory(appWindow->visibilityRects, windowState->visibilityRects, + appWindow->numVisibilityRects * sizeof(RECTANGLE_16)); + } + } + + /* Update Window */ + + if (fieldFlags & WINDOW_ORDER_FIELD_STYLE) + { + } + + if (fieldFlags & WINDOW_ORDER_FIELD_SHOW) + { + xf_ShowWindow(xfc, appWindow, appWindow->showState); + } + + if (fieldFlags & WINDOW_ORDER_FIELD_TITLE) + { + if (appWindow->title) + xf_SetWindowText(xfc, appWindow, appWindow->title); + } + + if (position_or_size_updated) + { + UINT32 visibilityRectsOffsetX = + (appWindow->visibleOffsetX - + (appWindow->clientOffsetX - appWindow->windowClientDeltaX)); + UINT32 visibilityRectsOffsetY = + (appWindow->visibleOffsetY - + (appWindow->clientOffsetY - appWindow->windowClientDeltaY)); + + /* + * The rail server like to set the window to a small size when it is minimized even though + * it is hidden in some cases this can cause the window not to restore back to its original + * size. Therefore we don't update our local window when that rail window state is minimized + */ + if (appWindow->rail_state != WINDOW_SHOW_MINIMIZED) + { + /* Redraw window area if already in the correct position */ + if (appWindow->x == (INT64)appWindow->windowOffsetX && + appWindow->y == (INT64)appWindow->windowOffsetY && + appWindow->width == (INT64)appWindow->windowWidth && + appWindow->height == (INT64)appWindow->windowHeight) + { + xf_UpdateWindowArea(xfc, appWindow, 0, 0, appWindow->windowWidth, + appWindow->windowHeight); + } + else + { + xf_MoveWindow(xfc, appWindow, appWindow->windowOffsetX, appWindow->windowOffsetY, + appWindow->windowWidth, appWindow->windowHeight); + } + + xf_SetWindowVisibilityRects(xfc, appWindow, visibilityRectsOffsetX, + visibilityRectsOffsetY, appWindow->visibilityRects, + appWindow->numVisibilityRects); + } + } + + /* We should only be using the visibility rects for shaping the window */ + /*if (fieldFlags & WINDOW_ORDER_FIELD_WND_RECTS) + { + xf_SetWindowRects(xfc, appWindow, appWindow->windowRects, appWindow->numWindowRects); + }*/ + return TRUE; +} + +static BOOL xf_rail_window_delete(rdpContext* context, const WINDOW_ORDER_INFO* orderInfo) +{ + xfContext* xfc = (xfContext*)context; + return xf_rail_del_window(xfc, orderInfo->windowId); +} + +static xfRailIconCache* RailIconCache_New(rdpSettings* settings) +{ + xfRailIconCache* cache; + cache = calloc(1, sizeof(xfRailIconCache)); + + if (!cache) + return NULL; + + cache->numCaches = settings->RemoteAppNumIconCaches; + cache->numCacheEntries = settings->RemoteAppNumIconCacheEntries; + cache->entries = calloc(cache->numCaches * cache->numCacheEntries * 1ULL, sizeof(xfRailIcon)); + + if (!cache->entries) + { + WLog_ERR(TAG, "failed to allocate icon cache %d x %d entries", cache->numCaches, + cache->numCacheEntries); + free(cache); + return NULL; + } + + return cache; +} + +static void RailIconCache_Free(xfRailIconCache* cache) +{ + UINT32 i; + + if (cache) + { + for (i = 0; i < cache->numCaches * cache->numCacheEntries; i++) + { + free(cache->entries[i].data); + } + + free(cache->scratch.data); + free(cache->entries); + free(cache); + } +} + +static xfRailIcon* RailIconCache_Lookup(xfRailIconCache* cache, UINT8 cacheId, UINT16 cacheEntry) +{ + /* + * MS-RDPERP 2.2.1.2.3 Icon Info (TS_ICON_INFO) + * + * CacheId (1 byte): + * If the value is 0xFFFF, the icon SHOULD NOT be cached. + * + * Yes, the spec says "0xFFFF" in the 2018-03-16 revision, + * but the actual protocol field is 1-byte wide. + */ + if (cacheId == 0xFF) + return &cache->scratch; + + if (cacheId >= cache->numCaches) + return NULL; + + if (cacheEntry >= cache->numCacheEntries) + return NULL; + + return &cache->entries[cache->numCacheEntries * cacheId + cacheEntry]; +} + +/* + * _NET_WM_ICON format is defined as "array of CARDINAL" values which for + * Xlib must be represented with an array of C's "long" values. Note that + * "long" != "INT32" on 64-bit systems. Therefore we can't simply cast + * the bitmap data as (unsigned char*), we have to copy all the pixels. + * + * The first two values are width and height followed by actual color data + * in ARGB format (e.g., 0xFFFF0000L is opaque red), pixels are in normal, + * left-to-right top-down order. + */ +static BOOL convert_rail_icon(const ICON_INFO* iconInfo, xfRailIcon* railIcon) +{ + BYTE* argbPixels = NULL; + BYTE* nextPixel; + long* pixels; + int i; + int nelements; + argbPixels = calloc(iconInfo->width * iconInfo->height * 1ULL, 4); + + if (!argbPixels) + goto error; + + if (!freerdp_image_copy_from_icon_data( + argbPixels, PIXEL_FORMAT_ARGB32, 0, 0, 0, iconInfo->width, iconInfo->height, + iconInfo->bitsColor, iconInfo->cbBitsColor, iconInfo->bitsMask, iconInfo->cbBitsMask, + iconInfo->colorTable, iconInfo->cbColorTable, iconInfo->bpp)) + goto error; + + nelements = 2 + iconInfo->width * iconInfo->height; + pixels = realloc(railIcon->data, nelements * sizeof(long)); + + if (!pixels) + goto error; + + railIcon->data = pixels; + railIcon->length = nelements; + pixels[0] = iconInfo->width; + pixels[1] = iconInfo->height; + nextPixel = argbPixels; + + for (i = 2; i < nelements; i++) + { + pixels[i] = ReadColor(nextPixel, PIXEL_FORMAT_BGRA32); + nextPixel += 4; + } + + free(argbPixels); + return TRUE; +error: + free(argbPixels); + return FALSE; +} + +static void xf_rail_set_window_icon(xfContext* xfc, xfAppWindow* railWindow, xfRailIcon* icon, + BOOL replace) +{ + XChangeProperty(xfc->display, railWindow->handle, xfc->_NET_WM_ICON, XA_CARDINAL, 32, + replace ? PropModeReplace : PropModeAppend, (unsigned char*)icon->data, + icon->length); + XFlush(xfc->display); +} + +static BOOL xf_rail_window_icon(rdpContext* context, const WINDOW_ORDER_INFO* orderInfo, + const WINDOW_ICON_ORDER* windowIcon) +{ + xfContext* xfc = (xfContext*)context; + xfAppWindow* railWindow; + xfRailIcon* icon; + BOOL replaceIcon; + railWindow = xf_rail_get_window(xfc, orderInfo->windowId); + + if (!railWindow) + return TRUE; + + icon = RailIconCache_Lookup(xfc->railIconCache, windowIcon->iconInfo->cacheId, + windowIcon->iconInfo->cacheEntry); + + if (!icon) + { + WLog_WARN(TAG, "failed to get icon from cache %02X:%04X", windowIcon->iconInfo->cacheId, + windowIcon->iconInfo->cacheEntry); + return FALSE; + } + + if (!convert_rail_icon(windowIcon->iconInfo, icon)) + { + WLog_WARN(TAG, "failed to convert icon for window %08X", orderInfo->windowId); + return FALSE; + } + + replaceIcon = !!(orderInfo->fieldFlags & WINDOW_ORDER_STATE_NEW); + xf_rail_set_window_icon(xfc, railWindow, icon, replaceIcon); + return TRUE; +} + +static BOOL xf_rail_window_cached_icon(rdpContext* context, const WINDOW_ORDER_INFO* orderInfo, + const WINDOW_CACHED_ICON_ORDER* windowCachedIcon) +{ + xfContext* xfc = (xfContext*)context; + xfAppWindow* railWindow; + xfRailIcon* icon; + BOOL replaceIcon; + railWindow = xf_rail_get_window(xfc, orderInfo->windowId); + + if (!railWindow) + return TRUE; + + icon = RailIconCache_Lookup(xfc->railIconCache, windowCachedIcon->cachedIcon.cacheId, + windowCachedIcon->cachedIcon.cacheEntry); + + if (!icon) + { + WLog_WARN(TAG, "failed to get icon from cache %02X:%04X", + windowCachedIcon->cachedIcon.cacheId, windowCachedIcon->cachedIcon.cacheEntry); + return FALSE; + } + + replaceIcon = !!(orderInfo->fieldFlags & WINDOW_ORDER_STATE_NEW); + xf_rail_set_window_icon(xfc, railWindow, icon, replaceIcon); + return TRUE; +} + +static BOOL xf_rail_notify_icon_common(rdpContext* context, const WINDOW_ORDER_INFO* orderInfo, + const NOTIFY_ICON_STATE_ORDER* notifyIconState) +{ + if (orderInfo->fieldFlags & WINDOW_ORDER_FIELD_NOTIFY_VERSION) + { + } + + if (orderInfo->fieldFlags & WINDOW_ORDER_FIELD_NOTIFY_TIP) + { + } + + if (orderInfo->fieldFlags & WINDOW_ORDER_FIELD_NOTIFY_INFO_TIP) + { + } + + if (orderInfo->fieldFlags & WINDOW_ORDER_FIELD_NOTIFY_STATE) + { + } + + if (orderInfo->fieldFlags & WINDOW_ORDER_ICON) + { + } + + if (orderInfo->fieldFlags & WINDOW_ORDER_CACHED_ICON) + { + } + + return TRUE; +} + +static BOOL xf_rail_notify_icon_create(rdpContext* context, const WINDOW_ORDER_INFO* orderInfo, + const NOTIFY_ICON_STATE_ORDER* notifyIconState) +{ + return xf_rail_notify_icon_common(context, orderInfo, notifyIconState); +} + +static BOOL xf_rail_notify_icon_update(rdpContext* context, const WINDOW_ORDER_INFO* orderInfo, + const NOTIFY_ICON_STATE_ORDER* notifyIconState) +{ + return xf_rail_notify_icon_common(context, orderInfo, notifyIconState); +} + +static BOOL xf_rail_notify_icon_delete(rdpContext* context, const WINDOW_ORDER_INFO* orderInfo) +{ + return TRUE; +} + +static BOOL xf_rail_monitored_desktop(rdpContext* context, const WINDOW_ORDER_INFO* orderInfo, + const MONITORED_DESKTOP_ORDER* monitoredDesktop) +{ + return TRUE; +} + +static BOOL xf_rail_non_monitored_desktop(rdpContext* context, const WINDOW_ORDER_INFO* orderInfo) +{ + xfContext* xfc = (xfContext*)context; + xf_rail_disable_remoteapp_mode(xfc); + return TRUE; +} + +static void xf_rail_register_update_callbacks(rdpUpdate* update) +{ + rdpWindowUpdate* window = update->window; + window->WindowCreate = xf_rail_window_common; + window->WindowUpdate = xf_rail_window_common; + window->WindowDelete = xf_rail_window_delete; + window->WindowIcon = xf_rail_window_icon; + window->WindowCachedIcon = xf_rail_window_cached_icon; + window->NotifyIconCreate = xf_rail_notify_icon_create; + window->NotifyIconUpdate = xf_rail_notify_icon_update; + window->NotifyIconDelete = xf_rail_notify_icon_delete; + window->MonitoredDesktop = xf_rail_monitored_desktop; + window->NonMonitoredDesktop = xf_rail_non_monitored_desktop; +} + +/* RemoteApp Virtual Channel Extension */ + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT xf_rail_server_execute_result(RailClientContext* context, + const RAIL_EXEC_RESULT_ORDER* execResult) +{ + xfContext* xfc = (xfContext*)context->custom; + + if (execResult->execResult != RAIL_EXEC_S_OK) + { + WLog_ERR(TAG, "RAIL exec error: execResult=%s NtError=0x%X\n", + error_code_names[execResult->execResult], execResult->rawResult); + freerdp_abort_connect(xfc->context.instance); + } + else + { + xf_rail_enable_remoteapp_mode(xfc); + } + + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT xf_rail_server_system_param(RailClientContext* context, + const RAIL_SYSPARAM_ORDER* sysparam) +{ + // TODO: Actually apply param + return CHANNEL_RC_OK; +} + +static UINT xf_rail_server_start_cmd(RailClientContext* context) +{ + UINT status; + RAIL_EXEC_ORDER exec = { 0 }; + RAIL_SYSPARAM_ORDER sysparam = { 0 }; + RAIL_CLIENT_STATUS_ORDER clientStatus = { 0 }; + xfContext* xfc = (xfContext*)context->custom; + rdpSettings* settings = xfc->context.settings; + clientStatus.flags = TS_RAIL_CLIENTSTATUS_ALLOWLOCALMOVESIZE; + + if (settings->AutoReconnectionEnabled) + clientStatus.flags |= TS_RAIL_CLIENTSTATUS_AUTORECONNECT; + + clientStatus.flags |= TS_RAIL_CLIENTSTATUS_ZORDER_SYNC; + clientStatus.flags |= TS_RAIL_CLIENTSTATUS_WINDOW_RESIZE_MARGIN_SUPPORTED; + clientStatus.flags |= TS_RAIL_CLIENTSTATUS_APPBAR_REMOTING_SUPPORTED; + clientStatus.flags |= TS_RAIL_CLIENTSTATUS_POWER_DISPLAY_REQUEST_SUPPORTED; + clientStatus.flags |= TS_RAIL_CLIENTSTATUS_BIDIRECTIONAL_CLOAK_SUPPORTED; + status = context->ClientInformation(context, &clientStatus); + + if (status != CHANNEL_RC_OK) + return status; + + if (settings->RemoteAppLanguageBarSupported) + { + RAIL_LANGBAR_INFO_ORDER langBarInfo; + langBarInfo.languageBarStatus = 0x00000008; /* TF_SFT_HIDDEN */ + status = context->ClientLanguageBarInfo(context, &langBarInfo); + + /* We want the language bar, but the server might not support it. */ + switch (status) + { + case CHANNEL_RC_OK: + case ERROR_BAD_CONFIGURATION: + break; + default: + return status; + } + } + + sysparam.params = 0; + sysparam.params |= SPI_MASK_SET_HIGH_CONTRAST; + sysparam.highContrast.colorScheme.string = NULL; + sysparam.highContrast.colorScheme.length = 0; + sysparam.highContrast.flags = 0x7E; + sysparam.params |= SPI_MASK_SET_MOUSE_BUTTON_SWAP; + sysparam.mouseButtonSwap = FALSE; + sysparam.params |= SPI_MASK_SET_KEYBOARD_PREF; + sysparam.keyboardPref = FALSE; + sysparam.params |= SPI_MASK_SET_DRAG_FULL_WINDOWS; + sysparam.dragFullWindows = FALSE; + sysparam.params |= SPI_MASK_SET_KEYBOARD_CUES; + sysparam.keyboardCues = FALSE; + sysparam.params |= SPI_MASK_SET_WORK_AREA; + sysparam.workArea.left = 0; + sysparam.workArea.top = 0; + sysparam.workArea.right = settings->DesktopWidth; + sysparam.workArea.bottom = settings->DesktopHeight; + sysparam.dragFullWindows = FALSE; + status = context->ClientSystemParam(context, &sysparam); + + if (status != CHANNEL_RC_OK) + return status; + + exec.RemoteApplicationProgram = settings->RemoteApplicationProgram; + exec.RemoteApplicationWorkingDir = settings->ShellWorkingDirectory; + exec.RemoteApplicationArguments = settings->RemoteApplicationCmdLine; + return context->ClientExecute(context, &exec); +} +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT xf_rail_server_handshake(RailClientContext* context, + const RAIL_HANDSHAKE_ORDER* handshake) +{ + return xf_rail_server_start_cmd(context); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT xf_rail_server_handshake_ex(RailClientContext* context, + const RAIL_HANDSHAKE_EX_ORDER* handshakeEx) +{ + return xf_rail_server_start_cmd(context); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT xf_rail_server_local_move_size(RailClientContext* context, + const RAIL_LOCALMOVESIZE_ORDER* localMoveSize) +{ + int x = 0, y = 0; + int direction = 0; + Window child_window; + xfContext* xfc = (xfContext*)context->custom; + xfAppWindow* appWindow = xf_rail_get_window(xfc, localMoveSize->windowId); + + if (!appWindow) + return ERROR_INTERNAL_ERROR; + + switch (localMoveSize->moveSizeType) + { + case RAIL_WMSZ_LEFT: + direction = _NET_WM_MOVERESIZE_SIZE_LEFT; + x = localMoveSize->posX; + y = localMoveSize->posY; + break; + + case RAIL_WMSZ_RIGHT: + direction = _NET_WM_MOVERESIZE_SIZE_RIGHT; + x = localMoveSize->posX; + y = localMoveSize->posY; + break; + + case RAIL_WMSZ_TOP: + direction = _NET_WM_MOVERESIZE_SIZE_TOP; + x = localMoveSize->posX; + y = localMoveSize->posY; + break; + + case RAIL_WMSZ_TOPLEFT: + direction = _NET_WM_MOVERESIZE_SIZE_TOPLEFT; + x = localMoveSize->posX; + y = localMoveSize->posY; + break; + + case RAIL_WMSZ_TOPRIGHT: + direction = _NET_WM_MOVERESIZE_SIZE_TOPRIGHT; + x = localMoveSize->posX; + y = localMoveSize->posY; + break; + + case RAIL_WMSZ_BOTTOM: + direction = _NET_WM_MOVERESIZE_SIZE_BOTTOM; + x = localMoveSize->posX; + y = localMoveSize->posY; + break; + + case RAIL_WMSZ_BOTTOMLEFT: + direction = _NET_WM_MOVERESIZE_SIZE_BOTTOMLEFT; + x = localMoveSize->posX; + y = localMoveSize->posY; + break; + + case RAIL_WMSZ_BOTTOMRIGHT: + direction = _NET_WM_MOVERESIZE_SIZE_BOTTOMRIGHT; + x = localMoveSize->posX; + y = localMoveSize->posY; + break; + + case RAIL_WMSZ_MOVE: + direction = _NET_WM_MOVERESIZE_MOVE; + XTranslateCoordinates(xfc->display, appWindow->handle, RootWindowOfScreen(xfc->screen), + localMoveSize->posX, localMoveSize->posY, &x, &y, &child_window); + break; + + case RAIL_WMSZ_KEYMOVE: + direction = _NET_WM_MOVERESIZE_MOVE_KEYBOARD; + x = localMoveSize->posX; + y = localMoveSize->posY; + /* FIXME: local keyboard moves not working */ + return CHANNEL_RC_OK; + + case RAIL_WMSZ_KEYSIZE: + direction = _NET_WM_MOVERESIZE_SIZE_KEYBOARD; + x = localMoveSize->posX; + y = localMoveSize->posY; + /* FIXME: local keyboard moves not working */ + return CHANNEL_RC_OK; + } + + if (localMoveSize->isMoveSizeStart) + xf_StartLocalMoveSize(xfc, appWindow, direction, x, y); + else + xf_EndLocalMoveSize(xfc, appWindow); + + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT xf_rail_server_min_max_info(RailClientContext* context, + const RAIL_MINMAXINFO_ORDER* minMaxInfo) +{ + xfContext* xfc = (xfContext*)context->custom; + xfAppWindow* appWindow = xf_rail_get_window(xfc, minMaxInfo->windowId); + + if (appWindow) + { + xf_SetWindowMinMaxInfo(xfc, appWindow, minMaxInfo->maxWidth, minMaxInfo->maxHeight, + minMaxInfo->maxPosX, minMaxInfo->maxPosY, minMaxInfo->minTrackWidth, + minMaxInfo->minTrackHeight, minMaxInfo->maxTrackWidth, + minMaxInfo->maxTrackHeight); + } + + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT xf_rail_server_language_bar_info(RailClientContext* context, + const RAIL_LANGBAR_INFO_ORDER* langBarInfo) +{ + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT xf_rail_server_get_appid_response(RailClientContext* context, + const RAIL_GET_APPID_RESP_ORDER* getAppIdResp) +{ + return CHANNEL_RC_OK; +} + +static BOOL rail_window_key_equals(void* key1, void* key2) +{ + const UINT64* k1 = (const UINT64*)key1; + const UINT64* k2 = (const UINT64*)key2; + + if (!k1 || !k2) + return FALSE; + + return *k1 == *k2; +} + +static UINT32 rail_window_key_hash(void* key) +{ + const UINT64* k1 = (const UINT64*)key; + return (UINT32)*k1; +} + +static void rail_window_free(void* value) +{ + xfAppWindow* appWindow = (xfAppWindow*)value; + + if (!appWindow) + return; + + xf_DestroyWindow(appWindow->xfc, appWindow); +} + +int xf_rail_init(xfContext* xfc, RailClientContext* rail) +{ + rdpContext* context = (rdpContext*)xfc; + + if (!xfc || !rail) + return 0; + + xfc->rail = rail; + xf_rail_register_update_callbacks(context->update); + rail->custom = (void*)xfc; + rail->ServerExecuteResult = xf_rail_server_execute_result; + rail->ServerSystemParam = xf_rail_server_system_param; + rail->ServerHandshake = xf_rail_server_handshake; + rail->ServerHandshakeEx = xf_rail_server_handshake_ex; + rail->ServerLocalMoveSize = xf_rail_server_local_move_size; + rail->ServerMinMaxInfo = xf_rail_server_min_max_info; + rail->ServerLanguageBarInfo = xf_rail_server_language_bar_info; + rail->ServerGetAppIdResponse = xf_rail_server_get_appid_response; + xfc->railWindows = HashTable_New(TRUE); + + if (!xfc->railWindows) + return 0; + + xfc->railWindows->keyCompare = rail_window_key_equals; + xfc->railWindows->hash = rail_window_key_hash; + xfc->railWindows->valueFree = rail_window_free; + xfc->railIconCache = RailIconCache_New(xfc->context.settings); + + if (!xfc->railIconCache) + { + HashTable_Free(xfc->railWindows); + return 0; + } + + return 1; +} + +int xf_rail_uninit(xfContext* xfc, RailClientContext* rail) +{ + WINPR_UNUSED(rail); + + if (xfc->rail) + { + xfc->rail->custom = NULL; + xfc->rail = NULL; + } + + if (xfc->railWindows) + { + HashTable_Free(xfc->railWindows); + xfc->railWindows = NULL; + } + + if (xfc->railIconCache) + { + RailIconCache_Free(xfc->railIconCache); + xfc->railIconCache = NULL; + } + + return 1; +} + +xfAppWindow* xf_rail_add_window(xfContext* xfc, UINT64 id, UINT32 x, UINT32 y, UINT32 width, + UINT32 height, UINT32 surfaceId) +{ + xfAppWindow* appWindow; + + if (!xfc) + return NULL; + + appWindow = (xfAppWindow*)calloc(1, sizeof(xfAppWindow)); + + if (!appWindow) + return NULL; + + appWindow->xfc = xfc; + appWindow->windowId = id; + appWindow->surfaceId = surfaceId; + appWindow->x = x; + appWindow->y = y; + appWindow->width = width; + appWindow->height = height; + xf_AppWindowCreate(xfc, appWindow); + HashTable_Add(xfc->railWindows, &appWindow->windowId, (void*)appWindow); + return appWindow; +} + +BOOL xf_rail_del_window(xfContext* xfc, UINT64 id) +{ + if (!xfc) + return FALSE; + + if (!xfc->railWindows) + return FALSE; + + return HashTable_Remove(xfc->railWindows, &id); +} + +xfAppWindow* xf_rail_get_window(xfContext* xfc, UINT64 id) +{ + if (!xfc) + return NULL; + + if (!xfc->railWindows) + return FALSE; + + return HashTable_GetItemValue(xfc->railWindows, &id); +} diff --git a/client/X11/xf_rail.h b/client/X11/xf_rail.h new file mode 100644 index 0000000..c99ed70 --- /dev/null +++ b/client/X11/xf_rail.h @@ -0,0 +1,49 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * X11 RAIL + * + * Copyright 2011 Marc-Andre Moreau + * + * Licensed 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 FREERDP_CLIENT_X11_RAIL_H +#define FREERDP_CLIENT_X11_RAIL_H + +#include "xf_client.h" +#include "xfreerdp.h" + +#include + +void xf_rail_paint(xfContext* xfc, INT32 uleft, INT32 utop, UINT32 uright, UINT32 ubottom); +void xf_rail_send_client_system_command(xfContext* xfc, UINT32 windowId, UINT16 command); +void xf_rail_send_activate(xfContext* xfc, Window xwindow, BOOL enabled); +void xf_rail_adjust_position(xfContext* xfc, xfAppWindow* appWindow); +void xf_rail_end_local_move(xfContext* xfc, xfAppWindow* appWindow); +void xf_rail_enable_remoteapp_mode(xfContext* xfc); +void xf_rail_disable_remoteapp_mode(xfContext* xfc); + +xfAppWindow* xf_rail_add_window(xfContext* xfc, UINT64 id, UINT32 x, UINT32 y, UINT32 width, + UINT32 height, UINT32 surfaceId); +xfAppWindow* xf_rail_get_window(xfContext* xfc, UINT64 id); + +BOOL xf_rail_del_window(xfContext* xfc, UINT64 id); + +BOOL xf_rail_draw_window(xfContext* xfc, xfAppWindow* window, const char* data, UINT32 scanline, + UINT32 width, UINT32 height, const RECTANGLE_16* src, + const RECTANGLE_16* dst); + +int xf_rail_init(xfContext* xfc, RailClientContext* rail); +int xf_rail_uninit(xfContext* xfc, RailClientContext* rail); + +#endif /* FREERDP_CLIENT_X11_RAIL_H */ diff --git a/client/X11/xf_tsmf.c b/client/X11/xf_tsmf.c new file mode 100644 index 0000000..87f1047 --- /dev/null +++ b/client/X11/xf_tsmf.c @@ -0,0 +1,475 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * X11 Video Redirection + * + * Copyright 2010-2011 Vic Lee + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include + +#include +#include + +#include +#include +#include +#include + +#include +#include + +#include "xf_tsmf.h" + +#ifdef WITH_XV + +#include +#include + +static long xv_port = 0; + +struct xf_xv_context +{ + long xv_port; + Atom xv_colorkey_atom; + int xv_image_size; + int xv_shmid; + char* xv_shmaddr; + UINT32* xv_pixfmts; +}; +typedef struct xf_xv_context xfXvContext; + +#define TAG CLIENT_TAG("x11") + +static BOOL xf_tsmf_is_format_supported(xfXvContext* xv, UINT32 pixfmt) +{ + int i; + + if (!xv->xv_pixfmts) + return FALSE; + + for (i = 0; xv->xv_pixfmts[i]; i++) + { + if (xv->xv_pixfmts[i] == pixfmt) + return TRUE; + } + + return FALSE; +} + +static int xf_tsmf_xv_video_frame_event(TsmfClientContext* tsmf, TSMF_VIDEO_FRAME_EVENT* event) +{ + int i; + int x, y; + UINT32 width; + UINT32 height; + BYTE* data1; + BYTE* data2; + UINT32 pixfmt; + UINT32 xvpixfmt; + XvImage* image; + int colorkey = 0; + int numRects = 0; + xfContext* xfc; + xfXvContext* xv; + XRectangle* xrects = NULL; + XShmSegmentInfo shminfo; + BOOL converti420yv12 = FALSE; + + if (!tsmf) + return -1; + + xfc = (xfContext*)tsmf->custom; + + if (!xfc) + return -1; + + xv = (xfXvContext*)xfc->xv_context; + + if (!xv) + return -1; + + if (xv->xv_port == 0) + return -1001; + + /* In case the player is minimized */ + if (event->x < -2048 || event->y < -2048 || event->numVisibleRects == 0) + { + return -1002; + } + + xrects = NULL; + numRects = event->numVisibleRects; + + if (numRects > 0) + { + xrects = (XRectangle*)calloc(numRects, sizeof(XRectangle)); + + if (!xrects) + return -1; + + for (i = 0; i < numRects; i++) + { + x = event->x + event->visibleRects[i].left; + y = event->y + event->visibleRects[i].top; + width = event->visibleRects[i].right - event->visibleRects[i].left; + height = event->visibleRects[i].bottom - event->visibleRects[i].top; + + xrects[i].x = x; + xrects[i].y = y; + xrects[i].width = width; + xrects[i].height = height; + } + } + + if (xv->xv_colorkey_atom != None) + { + XvGetPortAttribute(xfc->display, xv->xv_port, xv->xv_colorkey_atom, &colorkey); + XSetFunction(xfc->display, xfc->gc, GXcopy); + XSetFillStyle(xfc->display, xfc->gc, FillSolid); + XSetForeground(xfc->display, xfc->gc, colorkey); + + if (event->numVisibleRects < 1) + { + XSetClipMask(xfc->display, xfc->gc, None); + } + else + { + XFillRectangles(xfc->display, xfc->window->handle, xfc->gc, xrects, numRects); + } + } + else + { + XSetFunction(xfc->display, xfc->gc, GXcopy); + XSetFillStyle(xfc->display, xfc->gc, FillSolid); + + if (event->numVisibleRects < 1) + { + XSetClipMask(xfc->display, xfc->gc, None); + } + else + { + XSetClipRectangles(xfc->display, xfc->gc, 0, 0, xrects, numRects, YXBanded); + } + } + + pixfmt = event->framePixFmt; + + if (xf_tsmf_is_format_supported(xv, pixfmt)) + { + xvpixfmt = pixfmt; + } + else if (pixfmt == RDP_PIXFMT_I420 && xf_tsmf_is_format_supported(xv, RDP_PIXFMT_YV12)) + { + xvpixfmt = RDP_PIXFMT_YV12; + converti420yv12 = TRUE; + } + else if (pixfmt == RDP_PIXFMT_YV12 && xf_tsmf_is_format_supported(xv, RDP_PIXFMT_I420)) + { + xvpixfmt = RDP_PIXFMT_I420; + converti420yv12 = TRUE; + } + else + { + WLog_DBG(TAG, "pixel format 0x%" PRIX32 " not supported by hardware.", pixfmt); + free(xrects); + return -1003; + } + + image = XvShmCreateImage(xfc->display, xv->xv_port, xvpixfmt, 0, event->frameWidth, + event->frameHeight, &shminfo); + + if (xv->xv_image_size != image->data_size) + { + if (xv->xv_image_size > 0) + { + shmdt(xv->xv_shmaddr); + shmctl(xv->xv_shmid, IPC_RMID, NULL); + } + + xv->xv_image_size = image->data_size; + xv->xv_shmid = shmget(IPC_PRIVATE, image->data_size, IPC_CREAT | 0777); + xv->xv_shmaddr = shmat(xv->xv_shmid, 0, 0); + } + + shminfo.shmid = xv->xv_shmid; + shminfo.shmaddr = image->data = xv->xv_shmaddr; + shminfo.readOnly = FALSE; + + if (!XShmAttach(xfc->display, &shminfo)) + { + XFree(image); + free(xrects); + WLog_DBG(TAG, "XShmAttach failed."); + return -1004; + } + + /* The video driver may align each line to a different size + and we need to convert our original image data. */ + switch (pixfmt) + { + case RDP_PIXFMT_I420: + case RDP_PIXFMT_YV12: + /* Y */ + if (image->pitches[0] == event->frameWidth) + { + CopyMemory(image->data + image->offsets[0], event->frameData, + event->frameWidth * event->frameHeight); + } + else + { + for (i = 0; i < event->frameHeight; i++) + { + CopyMemory(image->data + image->offsets[0] + i * image->pitches[0], + event->frameData + i * event->frameWidth, event->frameWidth); + } + } + /* UV */ + /* Conversion between I420 and YV12 is to simply swap U and V */ + if (!converti420yv12) + { + data1 = event->frameData + event->frameWidth * event->frameHeight; + data2 = event->frameData + event->frameWidth * event->frameHeight + + event->frameWidth * event->frameHeight / 4; + } + else + { + data2 = event->frameData + event->frameWidth * event->frameHeight; + data1 = event->frameData + event->frameWidth * event->frameHeight + + event->frameWidth * event->frameHeight / 4; + image->id = pixfmt == RDP_PIXFMT_I420 ? RDP_PIXFMT_YV12 : RDP_PIXFMT_I420; + } + + if (image->pitches[1] * 2 == event->frameWidth) + { + CopyMemory(image->data + image->offsets[1], data1, + event->frameWidth * event->frameHeight / 4); + CopyMemory(image->data + image->offsets[2], data2, + event->frameWidth * event->frameHeight / 4); + } + else + { + for (i = 0; i < event->frameHeight / 2; i++) + { + CopyMemory(image->data + image->offsets[1] + i * image->pitches[1], + data1 + i * event->frameWidth / 2, event->frameWidth / 2); + CopyMemory(image->data + image->offsets[2] + i * image->pitches[2], + data2 + i * event->frameWidth / 2, event->frameWidth / 2); + } + } + break; + + default: + if (image->data_size < 0) + { + free(xrects); + return -2000; + } + else + { + const size_t size = ((UINT32)image->data_size <= event->frameSize) + ? (UINT32)image->data_size + : event->frameSize; + CopyMemory(image->data, event->frameData, size); + } + break; + } + + XvShmPutImage(xfc->display, xv->xv_port, xfc->window->handle, xfc->gc, image, 0, 0, + image->width, image->height, event->x, event->y, event->width, event->height, + FALSE); + + if (xv->xv_colorkey_atom == None) + XSetClipMask(xfc->display, xfc->gc, None); + + XSync(xfc->display, FALSE); + + XShmDetach(xfc->display, &shminfo); + XFree(image); + + free(xrects); + + return 1; +} + +int xf_tsmf_xv_init(xfContext* xfc, TsmfClientContext* tsmf) +{ + int ret; + unsigned int i; + unsigned int version; + unsigned int release; + unsigned int event_base; + unsigned int error_base; + unsigned int request_base; + unsigned int num_adaptors; + xfXvContext* xv; + XvAdaptorInfo* ai; + XvAttribute* attr; + XvImageFormatValues* fo; + + if (xfc->xv_context) + return 1; /* context already created */ + + xv = (xfXvContext*)calloc(1, sizeof(xfXvContext)); + + if (!xv) + return -1; + + xfc->xv_context = xv; + + xv->xv_colorkey_atom = None; + xv->xv_image_size = 0; + xv->xv_port = xv_port; + + if (!XShmQueryExtension(xfc->display)) + { + WLog_DBG(TAG, "no xshm available."); + return -1; + } + + ret = + XvQueryExtension(xfc->display, &version, &release, &request_base, &event_base, &error_base); + + if (ret != Success) + { + WLog_DBG(TAG, "XvQueryExtension failed %d.", ret); + return -1; + } + + WLog_DBG(TAG, "version %u release %u", version, release); + + ret = XvQueryAdaptors(xfc->display, DefaultRootWindow(xfc->display), &num_adaptors, &ai); + + if (ret != Success) + { + WLog_DBG(TAG, "XvQueryAdaptors failed %d.", ret); + return -1; + } + + for (i = 0; i < num_adaptors; i++) + { + WLog_DBG(TAG, "adapter port %lu-%lu (%s)", ai[i].base_id, + ai[i].base_id + ai[i].num_ports - 1, ai[i].name); + + if (xv->xv_port == 0 && i == num_adaptors - 1) + xv->xv_port = ai[i].base_id; + } + + if (num_adaptors > 0) + XvFreeAdaptorInfo(ai); + + if (xv->xv_port == 0) + { + WLog_DBG(TAG, "no adapter selected, video frames will not be processed."); + return -1; + } + WLog_DBG(TAG, "selected %ld", xv->xv_port); + + attr = XvQueryPortAttributes(xfc->display, xv->xv_port, &ret); + + for (i = 0; i < (unsigned int)ret; i++) + { + if (strcmp(attr[i].name, "XV_COLORKEY") == 0) + { + xv->xv_colorkey_atom = XInternAtom(xfc->display, "XV_COLORKEY", FALSE); + XvSetPortAttribute(xfc->display, xv->xv_port, xv->xv_colorkey_atom, + attr[i].min_value + 1); + break; + } + } + XFree(attr); + + WLog_DBG(TAG, "xf_tsmf_init: pixel format "); + + fo = XvListImageFormats(xfc->display, xv->xv_port, &ret); + + if (ret > 0) + { + xv->xv_pixfmts = (UINT32*)calloc((ret + 1), sizeof(UINT32)); + + for (i = 0; i < (unsigned int)ret; i++) + { + xv->xv_pixfmts[i] = fo[i].id; + WLog_DBG(TAG, "%c%c%c%c ", ((char*)(xv->xv_pixfmts + i))[0], + ((char*)(xv->xv_pixfmts + i))[1], ((char*)(xv->xv_pixfmts + i))[2], + ((char*)(xv->xv_pixfmts + i))[3]); + } + xv->xv_pixfmts[i] = 0; + } + XFree(fo); + + if (tsmf) + { + xfc->tsmf = tsmf; + tsmf->custom = (void*)xfc; + + tsmf->FrameEvent = xf_tsmf_xv_video_frame_event; + } + + return 1; +} + +int xf_tsmf_xv_uninit(xfContext* xfc, TsmfClientContext* tsmf) +{ + xfXvContext* xv = (xfXvContext*)xfc->xv_context; + + WINPR_UNUSED(tsmf); + if (xv) + { + if (xv->xv_image_size > 0) + { + shmdt(xv->xv_shmaddr); + shmctl(xv->xv_shmid, IPC_RMID, NULL); + } + if (xv->xv_pixfmts) + { + free(xv->xv_pixfmts); + xv->xv_pixfmts = NULL; + } + free(xv); + xfc->xv_context = NULL; + } + + if (xfc->tsmf) + { + xfc->tsmf->custom = NULL; + xfc->tsmf = NULL; + } + + return 1; +} + +#endif + +int xf_tsmf_init(xfContext* xfc, TsmfClientContext* tsmf) +{ +#ifdef WITH_XV + return xf_tsmf_xv_init(xfc, tsmf); +#endif + + return 1; +} + +int xf_tsmf_uninit(xfContext* xfc, TsmfClientContext* tsmf) +{ +#ifdef WITH_XV + return xf_tsmf_xv_uninit(xfc, tsmf); +#endif + + return 1; +} diff --git a/client/X11/xf_tsmf.h b/client/X11/xf_tsmf.h new file mode 100644 index 0000000..63a973a --- /dev/null +++ b/client/X11/xf_tsmf.h @@ -0,0 +1,29 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * X11 Video Redirection + * + * Copyright 2010-2011 Vic Lee + * + * Licensed 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 FREERDP_CLIENT_X11_TSMF_H +#define FREERDP_CLIENT_X11_TSMF_H + +#include "xf_client.h" +#include "xfreerdp.h" + +int xf_tsmf_init(xfContext* xfc, TsmfClientContext* tsmf); +int xf_tsmf_uninit(xfContext* xfc, TsmfClientContext* tsmf); + +#endif /* FREERDP_CLIENT_X11_TSMF_H */ diff --git a/client/X11/xf_video.c b/client/X11/xf_video.c new file mode 100644 index 0000000..9520454 --- /dev/null +++ b/client/X11/xf_video.c @@ -0,0 +1,107 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Video Optimized Remoting Virtual Channel Extension for X11 + * + * Copyright 2017 David Fort + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include + +#include "xf_video.h" + +#define TAG CLIENT_TAG("video") + +typedef struct +{ + VideoSurface base; + XImage* image; +} xfVideoSurface; + +static VideoSurface* xfVideoCreateSurface(VideoClientContext* video, BYTE* data, UINT32 x, UINT32 y, + UINT32 width, UINT32 height) +{ + xfContext* xfc = (xfContext*)video->custom; + xfVideoSurface* ret = calloc(1, sizeof(*ret)); + + if (!ret) + return NULL; + + ret->base.data = data; + ret->base.x = x; + ret->base.y = y; + ret->base.w = width; + ret->base.h = height; + ret->image = XCreateImage(xfc->display, xfc->visual, xfc->depth, ZPixmap, 0, (char*)data, width, + height, 8, width * 4); + + if (!ret->image) + { + WLog_ERR(TAG, "unable to create surface image"); + free(ret); + return NULL; + } + + return &ret->base; +} + +static BOOL xfVideoShowSurface(VideoClientContext* video, VideoSurface* surface) +{ + xfVideoSurface* xfSurface = (xfVideoSurface*)surface; + xfContext* xfc = video->custom; +#ifdef WITH_XRENDER + + if (xfc->context.settings->SmartSizing || xfc->context.settings->MultiTouchGestures) + { + XPutImage(xfc->display, xfc->primary, xfc->gc, xfSurface->image, 0, 0, surface->x, + surface->y, surface->w, surface->h); + xf_draw_screen(xfc, surface->x, surface->y, surface->w, surface->h); + } + else +#endif + { + XPutImage(xfc->display, xfc->drawable, xfc->gc, xfSurface->image, 0, 0, surface->x, + surface->y, surface->w, surface->h); + } + + return TRUE; +} + +static BOOL xfVideoDeleteSurface(VideoClientContext* video, VideoSurface* surface) +{ + xfVideoSurface* xfSurface = (xfVideoSurface*)surface; + + WINPR_UNUSED(video); + + if (xfSurface) + XFree(xfSurface->image); + + free(surface); + return TRUE; +} +void xf_video_control_init(xfContext* xfc, VideoClientContext* video) +{ + gdi_video_control_init(xfc->context.gdi, video); + video->custom = xfc; + video->createSurface = xfVideoCreateSurface; + video->showSurface = xfVideoShowSurface; + video->deleteSurface = xfVideoDeleteSurface; +} + +void xf_video_control_uninit(xfContext* xfc, VideoClientContext* video) +{ + gdi_video_control_uninit(xfc->context.gdi, video); +} diff --git a/client/X11/xf_video.h b/client/X11/xf_video.h new file mode 100644 index 0000000..83708f0 --- /dev/null +++ b/client/X11/xf_video.h @@ -0,0 +1,33 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Video Optimized Remoting Virtual Channel Extension for X11 + * + * Copyright 2017 David Fort + * + * Licensed 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 CLIENT_X11_XF_VIDEO_H_ +#define CLIENT_X11_XF_VIDEO_H_ + +#include "xfreerdp.h" + +#include +#include + +void xf_video_control_init(xfContext* xfc, VideoClientContext* video); +void xf_video_control_uninit(xfContext* xfc, VideoClientContext* video); + +xfVideoContext* xf_video_new(xfContext* xfc); +void xf_video_free(xfVideoContext* context); + +#endif /* CLIENT_X11_XF_VIDEO_H_ */ diff --git a/client/X11/xf_window.c b/client/X11/xf_window.c new file mode 100644 index 0000000..9b5b1c4 --- /dev/null +++ b/client/X11/xf_window.c @@ -0,0 +1,1143 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * X11 Windows + * + * Copyright 2011 Marc-Andre Moreau + * Copyright 2012 HP Development Company, LLC + * Copyright 2016 Thincast Technologies GmbH + * Copyright 2016 Armin Novak + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include + +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include + +#include +#include + +#ifdef WITH_XEXT +#include +#endif + +#ifdef WITH_XI +#include +#include "xf_input.h" +#endif + +#include "xf_rail.h" +#include "xf_input.h" + +#define TAG CLIENT_TAG("x11") + +#ifdef WITH_DEBUG_X11 +#define DEBUG_X11(...) WLog_DBG(TAG, __VA_ARGS__) +#else +#define DEBUG_X11(...) \ + do \ + { \ + } while (0) +#endif + +#include "FreeRDP_Icon_256px.h" +#define xf_icon_prop FreeRDP_Icon_256px_prop + +#include "xf_window.h" + +/* Extended Window Manager Hints: http://standards.freedesktop.org/wm-spec/wm-spec-1.3.html */ + +/* bit definitions for MwmHints.flags */ +#define MWM_HINTS_FUNCTIONS (1L << 0) +#define MWM_HINTS_DECORATIONS (1L << 1) +#define MWM_HINTS_INPUT_MODE (1L << 2) +#define MWM_HINTS_STATUS (1L << 3) + +/* bit definitions for MwmHints.functions */ +#define MWM_FUNC_ALL (1L << 0) +#define MWM_FUNC_RESIZE (1L << 1) +#define MWM_FUNC_MOVE (1L << 2) +#define MWM_FUNC_MINIMIZE (1L << 3) +#define MWM_FUNC_MAXIMIZE (1L << 4) +#define MWM_FUNC_CLOSE (1L << 5) + +/* bit definitions for MwmHints.decorations */ +#define MWM_DECOR_ALL (1L << 0) +#define MWM_DECOR_BORDER (1L << 1) +#define MWM_DECOR_RESIZEH (1L << 2) +#define MWM_DECOR_TITLE (1L << 3) +#define MWM_DECOR_MENU (1L << 4) +#define MWM_DECOR_MINIMIZE (1L << 5) +#define MWM_DECOR_MAXIMIZE (1L << 6) + +#define PROP_MOTIF_WM_HINTS_ELEMENTS 5 + +struct _PropMotifWmHints +{ + unsigned long flags; + unsigned long functions; + unsigned long decorations; + long inputMode; + unsigned long status; +}; +typedef struct _PropMotifWmHints PropMotifWmHints; + +static void xf_SetWindowTitleText(xfContext* xfc, Window window, const char* name) +{ + const size_t i = strnlen(name, MAX_PATH); + XStoreName(xfc->display, window, name); + Atom wm_Name = xfc->_NET_WM_NAME; + Atom utf8Str = xfc->UTF8_STRING; + XChangeProperty(xfc->display, window, wm_Name, utf8Str, 8, PropModeReplace, + (const unsigned char*)name, (int)i); +} + +/** + * Post an event from the client to the X server + */ +void xf_SendClientEvent(xfContext* xfc, Window window, Atom atom, unsigned int numArgs, ...) +{ + XEvent xevent = { 0 }; + unsigned int i; + va_list argp; + va_start(argp, numArgs); + + xevent.xclient.type = ClientMessage; + xevent.xclient.serial = 0; + xevent.xclient.send_event = False; + xevent.xclient.display = xfc->display; + xevent.xclient.window = window; + xevent.xclient.message_type = atom; + xevent.xclient.format = 32; + + for (i = 0; i < numArgs; i++) + { + xevent.xclient.data.l[i] = va_arg(argp, int); + } + + DEBUG_X11("Send ClientMessage Event: wnd=0x%04lX", (unsigned long)xevent.xclient.window); + XSendEvent(xfc->display, RootWindowOfScreen(xfc->screen), False, + SubstructureRedirectMask | SubstructureNotifyMask, &xevent); + XSync(xfc->display, False); + va_end(argp); +} + +void xf_SetWindowMinimized(xfContext* xfc, xfWindow* window) +{ + XIconifyWindow(xfc->display, window->handle, xfc->screen_number); +} + +void xf_SetWindowFullscreen(xfContext* xfc, xfWindow* window, BOOL fullscreen) +{ + UINT32 i; + rdpSettings* settings = xfc->context.settings; + int startX, startY; + UINT32 width = window->width; + UINT32 height = window->height; + /* xfc->decorations is set by caller depending on settings and whether it is fullscreen or not + */ + window->decorations = xfc->decorations; + /* show/hide decorations (e.g. title bar) as guided by xfc->decorations */ + xf_SetWindowDecorations(xfc, window->handle, window->decorations); + DEBUG_X11(TAG, "X window decoration set to %d", (int)window->decorations); + xf_floatbar_toggle_fullscreen(xfc->window->floatbar, fullscreen); + + if (fullscreen) + { + xfc->savedWidth = xfc->window->width; + xfc->savedHeight = xfc->window->height; + xfc->savedPosX = xfc->window->left; + xfc->savedPosY = xfc->window->top; + startX = (settings->DesktopPosX != UINT32_MAX) ? settings->DesktopPosX : 0; + startY = (settings->DesktopPosY != UINT32_MAX) ? settings->DesktopPosY : 0; + } + else + { + width = xfc->savedWidth; + height = xfc->savedHeight; + startX = xfc->savedPosX; + startY = xfc->savedPosY; + } + + /* Determine the x,y starting location for the fullscreen window */ + if (fullscreen) + { + /* Initialize startX and startY with reasonable values */ + startX = xfc->context.settings->MonitorDefArray[0].x; + startY = xfc->context.settings->MonitorDefArray[0].y; + + /* Search all monitors to find the lowest startX and startY values */ + for (i = 0; i < xfc->context.settings->MonitorCount; i++) + { + startX = MIN(startX, xfc->context.settings->MonitorDefArray[i].x); + startY = MIN(startY, xfc->context.settings->MonitorDefArray[i].y); + } + + /* Lastly apply any monitor shift(translation from remote to local coordinate system) + * to startX and startY values + */ + startX += xfc->context.settings->MonitorLocalShiftX; + startY += xfc->context.settings->MonitorLocalShiftY; + } + + /* + It is safe to proceed with simply toogling _NET_WM_STATE_FULLSCREEN window state on the + following conditions: + - The window manager supports multiple monitor full screen + - The user requested to use a single monitor to render the remote desktop + */ + if (xfc->_NET_WM_FULLSCREEN_MONITORS != None || settings->MonitorCount == 1) + { + xf_ResizeDesktopWindow(xfc, window, width, height); + + if (fullscreen) + { + /* enter full screen: move the window before adding NET_WM_STATE_FULLSCREEN */ + XMoveWindow(xfc->display, window->handle, startX, startY); + } + + /* Set the fullscreen state */ + xf_SendClientEvent(xfc, window->handle, xfc->_NET_WM_STATE, 4, + fullscreen ? _NET_WM_STATE_ADD : _NET_WM_STATE_REMOVE, + xfc->_NET_WM_STATE_FULLSCREEN, 0, 0); + + if (!fullscreen) + { + /* leave full screen: move the window after removing NET_WM_STATE_FULLSCREEN */ + XMoveWindow(xfc->display, window->handle, startX, startY); + } + + /* Set monitor bounds */ + if (settings->MonitorCount > 1) + { + xf_SendClientEvent(xfc, window->handle, xfc->_NET_WM_FULLSCREEN_MONITORS, 5, + xfc->fullscreenMonitors.top, xfc->fullscreenMonitors.bottom, + xfc->fullscreenMonitors.left, xfc->fullscreenMonitors.right, 1); + } + } + else + { + if (fullscreen) + { + xf_SetWindowDecorations(xfc, window->handle, FALSE); + + if (xfc->fullscreenMonitors.top) + { + xf_SendClientEvent(xfc, window->handle, xfc->_NET_WM_STATE, 4, _NET_WM_STATE_ADD, + xfc->fullscreenMonitors.top, 0, 0); + } + else + { + XSetWindowAttributes xswa; + xswa.override_redirect = True; + XChangeWindowAttributes(xfc->display, window->handle, CWOverrideRedirect, &xswa); + XRaiseWindow(xfc->display, window->handle); + xswa.override_redirect = False; + XChangeWindowAttributes(xfc->display, window->handle, CWOverrideRedirect, &xswa); + } + + /* if window is in maximized state, save and remove */ + if (xfc->_NET_WM_STATE_MAXIMIZED_VERT != None) + { + BYTE state; + unsigned long nitems; + unsigned long bytes; + BYTE* prop; + + if (xf_GetWindowProperty(xfc, window->handle, xfc->_NET_WM_STATE, 255, &nitems, + &bytes, &prop)) + { + state = 0; + + while (nitems-- > 0) + { + if (((Atom*)prop)[nitems] == xfc->_NET_WM_STATE_MAXIMIZED_VERT) + state |= 0x01; + + if (((Atom*)prop)[nitems] == xfc->_NET_WM_STATE_MAXIMIZED_HORZ) + state |= 0x02; + } + + if (state) + { + xf_SendClientEvent(xfc, window->handle, xfc->_NET_WM_STATE, 4, + _NET_WM_STATE_REMOVE, xfc->_NET_WM_STATE_MAXIMIZED_VERT, + 0, 0); + xf_SendClientEvent(xfc, window->handle, xfc->_NET_WM_STATE, 4, + _NET_WM_STATE_REMOVE, xfc->_NET_WM_STATE_MAXIMIZED_HORZ, + 0, 0); + xfc->savedMaximizedState = state; + } + + XFree(prop); + } + } + + width = xfc->vscreen.area.right - xfc->vscreen.area.left + 1; + height = xfc->vscreen.area.bottom - xfc->vscreen.area.top + 1; + DEBUG_X11("X window move and resize %dx%d@%dx%d", startX, startY, width, height); + xf_ResizeDesktopWindow(xfc, window, width, height); + XMoveWindow(xfc->display, window->handle, startX, startY); + } + else + { + xf_SetWindowDecorations(xfc, window->handle, window->decorations); + xf_ResizeDesktopWindow(xfc, window, width, height); + XMoveWindow(xfc->display, window->handle, startX, startY); + + if (xfc->fullscreenMonitors.top) + { + xf_SendClientEvent(xfc, window->handle, xfc->_NET_WM_STATE, 4, _NET_WM_STATE_REMOVE, + xfc->fullscreenMonitors.top, 0, 0); + } + + /* restore maximized state, if the window was maximized before setting fullscreen */ + if (xfc->savedMaximizedState & 0x01) + { + xf_SendClientEvent(xfc, window->handle, xfc->_NET_WM_STATE, 4, _NET_WM_STATE_ADD, + xfc->_NET_WM_STATE_MAXIMIZED_VERT, 0, 0); + } + + if (xfc->savedMaximizedState & 0x02) + { + xf_SendClientEvent(xfc, window->handle, xfc->_NET_WM_STATE, 4, _NET_WM_STATE_ADD, + xfc->_NET_WM_STATE_MAXIMIZED_HORZ, 0, 0); + } + + xfc->savedMaximizedState = 0; + } + } +} + +/* http://tronche.com/gui/x/xlib/window-information/XGetWindowProperty.html */ + +BOOL xf_GetWindowProperty(xfContext* xfc, Window window, Atom property, int length, + unsigned long* nitems, unsigned long* bytes, BYTE** prop) +{ + int status; + Atom actual_type; + int actual_format; + + if (property == None) + return FALSE; + + status = XGetWindowProperty(xfc->display, window, property, 0, length, False, AnyPropertyType, + &actual_type, &actual_format, nitems, bytes, prop); + + if (status != Success) + return FALSE; + + if (actual_type == None) + { + WLog_DBG(TAG, "Property %lu does not exist", (unsigned long)property); + return FALSE; + } + + return TRUE; +} + +BOOL xf_GetCurrentDesktop(xfContext* xfc) +{ + BOOL status; + unsigned long nitems; + unsigned long bytes; + unsigned char* prop; + status = xf_GetWindowProperty(xfc, DefaultRootWindow(xfc->display), xfc->_NET_CURRENT_DESKTOP, + 1, &nitems, &bytes, &prop); + + if (!status) + return FALSE; + + xfc->current_desktop = (int)*prop; + free(prop); + return TRUE; +} + +BOOL xf_GetWorkArea(xfContext* xfc) +{ + long* plong; + BOOL status; + unsigned long nitems; + unsigned long bytes; + unsigned char* prop; + status = xf_GetCurrentDesktop(xfc); + + if (!status) + return FALSE; + + status = xf_GetWindowProperty(xfc, DefaultRootWindow(xfc->display), xfc->_NET_WORKAREA, 32 * 4, + &nitems, &bytes, &prop); + + if (!status) + return FALSE; + + if ((xfc->current_desktop * 4 + 3) >= (INT64)nitems) + { + free(prop); + return FALSE; + } + + plong = (long*)prop; + xfc->workArea.x = plong[xfc->current_desktop * 4 + 0]; + xfc->workArea.y = plong[xfc->current_desktop * 4 + 1]; + xfc->workArea.width = plong[xfc->current_desktop * 4 + 2]; + xfc->workArea.height = plong[xfc->current_desktop * 4 + 3]; + free(prop); + return TRUE; +} + +void xf_SetWindowDecorations(xfContext* xfc, Window window, BOOL show) +{ + PropMotifWmHints hints; + hints.decorations = (show) ? MWM_DECOR_ALL : 0; + hints.functions = MWM_FUNC_ALL; + hints.flags = MWM_HINTS_DECORATIONS | MWM_HINTS_FUNCTIONS; + hints.inputMode = 0; + hints.status = 0; + XChangeProperty(xfc->display, window, xfc->_MOTIF_WM_HINTS, xfc->_MOTIF_WM_HINTS, 32, + PropModeReplace, (BYTE*)&hints, PROP_MOTIF_WM_HINTS_ELEMENTS); +} + +void xf_SetWindowUnlisted(xfContext* xfc, Window window) +{ + Atom window_state[2]; + window_state[0] = xfc->_NET_WM_STATE_SKIP_PAGER; + window_state[1] = xfc->_NET_WM_STATE_SKIP_TASKBAR; + XChangeProperty(xfc->display, window, xfc->_NET_WM_STATE, XA_ATOM, 32, PropModeReplace, + (BYTE*)&window_state, 2); +} + +static void xf_SetWindowPID(xfContext* xfc, Window window, pid_t pid) +{ + Atom am_wm_pid; + + if (!pid) + pid = getpid(); + + am_wm_pid = xfc->_NET_WM_PID; + XChangeProperty(xfc->display, window, am_wm_pid, XA_CARDINAL, 32, PropModeReplace, (BYTE*)&pid, + 1); +} + +static const char* get_shm_id(void) +{ + static char shm_id[64]; + sprintf_s(shm_id, sizeof(shm_id), "/com.freerdp.xfreerdp.tsmf_%016X", GetCurrentProcessId()); + return shm_id; +} + +Window xf_CreateDummyWindow(xfContext* xfc) +{ + return XCreateSimpleWindow(xfc->display, DefaultRootWindow(xfc->display), 0, 0, 1, 1, 0, 0, 0); +} + +void xf_DestroyDummyWindow(xfContext* xfc, Window window) +{ + if (window) + XDestroyWindow(xfc->display, window); +} + +xfWindow* xf_CreateDesktopWindow(xfContext* xfc, char* name, int width, int height) +{ + XEvent xevent; + int input_mask; + xfWindow* window; + Window parentWindow; + XClassHint* classHints; + rdpSettings* settings; + window = (xfWindow*)calloc(1, sizeof(xfWindow)); + + if (!window) + return NULL; + + settings = xfc->context.settings; + parentWindow = (Window)xfc->context.settings->ParentWindowId; + window->width = width; + window->height = height; + window->decorations = xfc->decorations; + window->is_mapped = FALSE; + window->is_transient = FALSE; + window->handle = XCreateWindow(xfc->display, RootWindowOfScreen(xfc->screen), xfc->workArea.x, + xfc->workArea.y, xfc->workArea.width, xfc->workArea.height, 0, + xfc->depth, InputOutput, xfc->visual, + CWBackPixel | CWBackingStore | CWOverrideRedirect | CWColormap | + CWBorderPixel | CWWinGravity | CWBitGravity, + &xfc->attribs); + window->shmid = shm_open(get_shm_id(), (O_CREAT | O_RDWR), (S_IREAD | S_IWRITE)); + + if (window->shmid < 0) + { + DEBUG_X11("xf_CreateDesktopWindow: failed to get access to shared memory - shmget()\n"); + } + else + { + void* mem; + ftruncate(window->shmid, sizeof(window->handle)); + mem = mmap(0, sizeof(window->handle), PROT_READ | PROT_WRITE, MAP_SHARED, window->shmid, 0); + + if (mem == MAP_FAILED) + { + DEBUG_X11("xf_CreateDesktopWindow: failed to assign pointer to the memory address - " + "shmat()\n"); + } + else + { + window->xfwin = mem; + *window->xfwin = window->handle; + } + } + + classHints = XAllocClassHint(); + + if (classHints) + { + classHints->res_name = "xfreerdp"; + + if (xfc->context.settings->WmClass) + classHints->res_class = xfc->context.settings->WmClass; + else + classHints->res_class = "xfreerdp"; + + XSetClassHint(xfc->display, window->handle, classHints); + XFree(classHints); + } + + xf_ResizeDesktopWindow(xfc, window, width, height); + xf_SetWindowDecorations(xfc, window->handle, window->decorations); + xf_SetWindowPID(xfc, window->handle, 0); + input_mask = KeyPressMask | KeyReleaseMask | ButtonPressMask | ButtonReleaseMask | + VisibilityChangeMask | FocusChangeMask | StructureNotifyMask | PointerMotionMask | + ExposureMask | PropertyChangeMask; + + if (xfc->grab_keyboard) + input_mask |= EnterWindowMask | LeaveWindowMask; + + XChangeProperty(xfc->display, window->handle, xfc->_NET_WM_ICON, XA_CARDINAL, 32, + PropModeReplace, (BYTE*)xf_icon_prop, ARRAYSIZE(xf_icon_prop)); + + if (parentWindow) + XReparentWindow(xfc->display, window->handle, parentWindow, 0, 0); + + XSelectInput(xfc->display, window->handle, input_mask); + XClearWindow(xfc->display, window->handle); + xf_SetWindowTitleText(xfc, window->handle, name); + XMapWindow(xfc->display, window->handle); + xf_input_init(xfc, window->handle); + + /* + * NOTE: This must be done here to handle reparenting the window, + * so that we don't miss the event and hang waiting for the next one + */ + do + { + XMaskEvent(xfc->display, VisibilityChangeMask, &xevent); + } while (xevent.type != VisibilityNotify); + + /* + * The XCreateWindow call will start the window in the upper-left corner of our current + * monitor instead of the upper-left monitor for remote app mode (which uses all monitors). + * This extra call after the window is mapped will position the login window correctly + */ + if (xfc->context.settings->RemoteApplicationMode) + { + XMoveWindow(xfc->display, window->handle, 0, 0); + } + else if (settings->DesktopPosX != UINT32_MAX && settings->DesktopPosY != UINT32_MAX) + { + XMoveWindow(xfc->display, window->handle, settings->DesktopPosX, settings->DesktopPosY); + } + + window->floatbar = xf_floatbar_new(xfc, window->handle, name, settings->Floatbar); + + if (xfc->_XWAYLAND_MAY_GRAB_KEYBOARD) + xf_SendClientEvent(xfc, window->handle, xfc->_XWAYLAND_MAY_GRAB_KEYBOARD, 1, 1); + + return window; +} + +void xf_ResizeDesktopWindow(xfContext* xfc, xfWindow* window, int width, int height) +{ + XSizeHints* size_hints; + rdpSettings* settings = NULL; + + if (!xfc || !window) + return; + + settings = xfc->context.settings; + + if (!(size_hints = XAllocSizeHints())) + return; + + size_hints->flags = PMinSize | PMaxSize | PWinGravity; + size_hints->win_gravity = NorthWestGravity; + size_hints->min_width = size_hints->min_height = 1; + size_hints->max_width = size_hints->max_height = 16384; + XResizeWindow(xfc->display, window->handle, width, height); +#ifdef WITH_XRENDER + + if (!settings->SmartSizing && !settings->DynamicResolutionUpdate) +#endif + { + if (!xfc->fullscreen) + { + /* min == max is an hint for the WM to indicate that the window should + * not be resizable */ + size_hints->min_width = size_hints->max_width = width; + size_hints->min_height = size_hints->max_height = height; + } + } + + XSetWMNormalHints(xfc->display, window->handle, size_hints); + XFree(size_hints); +} + +void xf_DestroyDesktopWindow(xfContext* xfc, xfWindow* window) +{ + if (!window) + return; + + if (xfc->window == window) + xfc->window = NULL; + + xf_floatbar_free(window->floatbar); + + if (window->gc) + XFreeGC(xfc->display, window->gc); + + if (window->handle) + { + XUnmapWindow(xfc->display, window->handle); + XDestroyWindow(xfc->display, window->handle); + } + + if (window->xfwin) + munmap(0, sizeof(*window->xfwin)); + + if (window->shmid >= 0) + close(window->shmid); + + shm_unlink(get_shm_id()); + window->xfwin = (Window*)-1; + window->shmid = -1; + free(window); +} + +void xf_SetWindowStyle(xfContext* xfc, xfAppWindow* appWindow, UINT32 style, UINT32 ex_style) +{ + Atom window_type; + BOOL redirect = FALSE; + + if ((ex_style & WS_EX_NOACTIVATE) || (ex_style & WS_EX_TOOLWINDOW)) + { + redirect = TRUE; + appWindow->is_transient = TRUE; + xf_SetWindowUnlisted(xfc, appWindow->handle); + window_type = xfc->_NET_WM_WINDOW_TYPE_DROPDOWN_MENU; + } + /* + * TOPMOST window that is not a tool window is treated like a regular window (i.e. task + * manager). Want to do this here, since the window may have type WS_POPUP + */ + else if (ex_style & WS_EX_TOPMOST) + { + window_type = xfc->_NET_WM_WINDOW_TYPE_NORMAL; + } + else if (style & WS_POPUP) + { + /* this includes dialogs, popups, etc, that need to be full-fledged windows */ + appWindow->is_transient = TRUE; + window_type = xfc->_NET_WM_WINDOW_TYPE_DIALOG; + xf_SetWindowUnlisted(xfc, appWindow->handle); + } + else + { + window_type = xfc->_NET_WM_WINDOW_TYPE_NORMAL; + } + + { + /* + * Tooltips and menu items should be unmanaged windows + * (called "override redirect" in X windows parlance) + * If they are managed, there are issues with window focus that + * cause the windows to behave improperly. For example, a mouse + * press will dismiss a drop-down menu because the RDP server + * sees that as a focus out event from the window owning the + * dropdown. + */ + XSetWindowAttributes attrs; + attrs.override_redirect = redirect ? True : False; + XChangeWindowAttributes(xfc->display, appWindow->handle, CWOverrideRedirect, &attrs); + } + + XChangeProperty(xfc->display, appWindow->handle, xfc->_NET_WM_WINDOW_TYPE, XA_ATOM, 32, + PropModeReplace, (BYTE*)&window_type, 1); +} + +void xf_SetWindowText(xfContext* xfc, xfAppWindow* appWindow, const char* name) +{ + xf_SetWindowTitleText(xfc, appWindow->handle, name); +} + +static void xf_FixWindowCoordinates(xfContext* xfc, int* x, int* y, int* width, int* height) +{ + int vscreen_width; + int vscreen_height; + vscreen_width = xfc->vscreen.area.right - xfc->vscreen.area.left + 1; + vscreen_height = xfc->vscreen.area.bottom - xfc->vscreen.area.top + 1; + + if (*x < xfc->vscreen.area.left) + { + *width += *x; + *x = xfc->vscreen.area.left; + } + + if (*y < xfc->vscreen.area.top) + { + *height += *y; + *y = xfc->vscreen.area.top; + } + + if (*width > vscreen_width) + { + *width = vscreen_width; + } + + if (*height > vscreen_height) + { + *height = vscreen_height; + } + + if (*width < 1) + { + *width = 1; + } + + if (*height < 1) + { + *height = 1; + } +} + +int xf_AppWindowInit(xfContext* xfc, xfAppWindow* appWindow) +{ + if (!xfc || !appWindow) + return -1; + + xf_SetWindowDecorations(xfc, appWindow->handle, appWindow->decorations); + xf_SetWindowStyle(xfc, appWindow, appWindow->dwStyle, appWindow->dwExStyle); + xf_SetWindowPID(xfc, appWindow->handle, 0); + xf_ShowWindow(xfc, appWindow, WINDOW_SHOW); + XClearWindow(xfc->display, appWindow->handle); + XMapWindow(xfc->display, appWindow->handle); + /* Move doesn't seem to work until window is mapped. */ + xf_MoveWindow(xfc, appWindow, appWindow->x, appWindow->y, appWindow->width, appWindow->height); + xf_SetWindowText(xfc, appWindow, appWindow->title); + return 1; +} + +int xf_AppWindowCreate(xfContext* xfc, xfAppWindow* appWindow) +{ + XGCValues gcv; + int input_mask; + XWMHints* InputModeHint; + XClassHint* class_hints; + xf_FixWindowCoordinates(xfc, &appWindow->x, &appWindow->y, &appWindow->width, + &appWindow->height); + appWindow->decorations = FALSE; + appWindow->fullscreen = FALSE; + appWindow->local_move.state = LMS_NOT_ACTIVE; + appWindow->is_mapped = FALSE; + appWindow->is_transient = FALSE; + appWindow->rail_state = 0; + appWindow->maxVert = FALSE; + appWindow->maxHorz = FALSE; + appWindow->minimized = FALSE; + appWindow->rail_ignore_configure = FALSE; + appWindow->handle = XCreateWindow(xfc->display, RootWindowOfScreen(xfc->screen), appWindow->x, + appWindow->y, appWindow->width, appWindow->height, 0, + xfc->depth, InputOutput, xfc->visual, 0, &xfc->attribs); + + if (!appWindow->handle) + return -1; + + ZeroMemory(&gcv, sizeof(gcv)); + appWindow->gc = XCreateGC(xfc->display, appWindow->handle, GCGraphicsExposures, &gcv); + class_hints = XAllocClassHint(); + + if (class_hints) + { + char* class = NULL; + + if (xfc->context.settings->WmClass) + { + class_hints->res_class = xfc->context.settings->WmClass; + } + else + { + class = malloc(sizeof("RAIL:00000000")); + sprintf_s(class, sizeof("RAIL:00000000"), "RAIL:%08" PRIX64 "", appWindow->windowId); + class_hints->res_class = class; + } + + class_hints->res_name = "RAIL"; + XSetClassHint(xfc->display, appWindow->handle, class_hints); + XFree(class_hints); + free(class); + } + + /* Set the input mode hint for the WM */ + InputModeHint = XAllocWMHints(); + InputModeHint->flags = (1L << 0); + InputModeHint->input = True; + XSetWMHints(xfc->display, appWindow->handle, InputModeHint); + XFree(InputModeHint); + XSetWMProtocols(xfc->display, appWindow->handle, &(xfc->WM_DELETE_WINDOW), 1); + input_mask = KeyPressMask | KeyReleaseMask | ButtonPressMask | ButtonReleaseMask | + EnterWindowMask | LeaveWindowMask | PointerMotionMask | Button1MotionMask | + Button2MotionMask | Button3MotionMask | Button4MotionMask | Button5MotionMask | + ButtonMotionMask | KeymapStateMask | ExposureMask | VisibilityChangeMask | + StructureNotifyMask | SubstructureNotifyMask | SubstructureRedirectMask | + FocusChangeMask | PropertyChangeMask | ColormapChangeMask | OwnerGrabButtonMask; + XSelectInput(xfc->display, appWindow->handle, input_mask); + + if (xfc->_XWAYLAND_MAY_GRAB_KEYBOARD) + xf_SendClientEvent(xfc, appWindow->handle, xfc->_XWAYLAND_MAY_GRAB_KEYBOARD, 1, 1); + + return 1; +} + +void xf_SetWindowMinMaxInfo(xfContext* xfc, xfAppWindow* appWindow, int maxWidth, int maxHeight, + int maxPosX, int maxPosY, int minTrackWidth, int minTrackHeight, + int maxTrackWidth, int maxTrackHeight) +{ + XSizeHints* size_hints; + size_hints = XAllocSizeHints(); + + if (size_hints) + { + size_hints->flags = PMinSize | PMaxSize | PResizeInc; + size_hints->min_width = minTrackWidth; + size_hints->min_height = minTrackHeight; + size_hints->max_width = maxTrackWidth; + size_hints->max_height = maxTrackHeight; + /* to speedup window drawing we need to select optimal value for sizing step. */ + size_hints->width_inc = size_hints->height_inc = 1; + XSetWMNormalHints(xfc->display, appWindow->handle, size_hints); + XFree(size_hints); + } +} + +void xf_StartLocalMoveSize(xfContext* xfc, xfAppWindow* appWindow, int direction, int x, int y) +{ + if (appWindow->local_move.state != LMS_NOT_ACTIVE) + return; + + /* + * Save original mouse location relative to root. This will be needed + * to end local move to RDP server and/or X server + */ + appWindow->local_move.root_x = x; + appWindow->local_move.root_y = y; + appWindow->local_move.state = LMS_STARTING; + appWindow->local_move.direction = direction; + XUngrabPointer(xfc->display, CurrentTime); + xf_SendClientEvent( + xfc, appWindow->handle, + xfc->_NET_WM_MOVERESIZE, /* request X window manager to initiate a local move */ + 5, /* 5 arguments to follow */ + x, /* x relative to root window */ + y, /* y relative to root window */ + direction, /* extended ICCM direction flag */ + 1, /* simulated mouse button 1 */ + 1); /* 1 == application request per extended ICCM */ +} + +void xf_EndLocalMoveSize(xfContext* xfc, xfAppWindow* appWindow) +{ + if (appWindow->local_move.state == LMS_NOT_ACTIVE) + return; + + if (appWindow->local_move.state == LMS_STARTING) + { + /* + * The move never was property started. This can happen due to race + * conditions between the mouse button up and the communications to the + * RDP server for local moves. We must cancel the X window manager move. + * Per ICCM, the X client can ask to cancel an active move. + */ + xf_SendClientEvent( + xfc, appWindow->handle, + xfc->_NET_WM_MOVERESIZE, /* request X window manager to abort a local move */ + 5, /* 5 arguments to follow */ + appWindow->local_move.root_x, /* x relative to root window */ + appWindow->local_move.root_y, /* y relative to root window */ + _NET_WM_MOVERESIZE_CANCEL, /* extended ICCM direction flag */ + 1, /* simulated mouse button 1 */ + 1); /* 1 == application request per extended ICCM */ + } + + appWindow->local_move.state = LMS_NOT_ACTIVE; +} + +void xf_MoveWindow(xfContext* xfc, xfAppWindow* appWindow, int x, int y, int width, int height) +{ + BOOL resize = FALSE; + + if ((width * height) < 1) + return; + + if ((appWindow->width != width) || (appWindow->height != height)) + resize = TRUE; + + if (appWindow->local_move.state == LMS_STARTING || appWindow->local_move.state == LMS_ACTIVE) + return; + + appWindow->x = x; + appWindow->y = y; + appWindow->width = width; + appWindow->height = height; + + if (resize) + XMoveResizeWindow(xfc->display, appWindow->handle, x, y, width, height); + else + XMoveWindow(xfc->display, appWindow->handle, x, y); + + xf_UpdateWindowArea(xfc, appWindow, 0, 0, width, height); +} + +void xf_ShowWindow(xfContext* xfc, xfAppWindow* appWindow, BYTE state) +{ + WINPR_ASSERT(xfc); + WINPR_ASSERT(appWindow); + + switch (state) + { + case WINDOW_HIDE: + XWithdrawWindow(xfc->display, appWindow->handle, xfc->screen_number); + break; + + case WINDOW_SHOW_MINIMIZED: + appWindow->minimized = TRUE; + XIconifyWindow(xfc->display, appWindow->handle, xfc->screen_number); + break; + + case WINDOW_SHOW_MAXIMIZED: + /* Set the window as maximized */ + appWindow->maxHorz = TRUE; + appWindow->maxVert = TRUE; + xf_SendClientEvent(xfc, appWindow->handle, xfc->_NET_WM_STATE, 4, _NET_WM_STATE_ADD, + xfc->_NET_WM_STATE_MAXIMIZED_VERT, xfc->_NET_WM_STATE_MAXIMIZED_HORZ, + 0); + + /* + * This is a workaround for the case where the window is maximized locally before the + * rail server is told to maximize the window, this appears to be a race condition where + * the local window with incomplete data and once the window is actually maximized on + * the server + * - an update of the new areas may not happen. So, we simply to do a full update of the + * entire window once the rail server notifies us that the window is now maximized. + */ + if (appWindow->rail_state == WINDOW_SHOW_MAXIMIZED) + { + xf_UpdateWindowArea(xfc, appWindow, 0, 0, appWindow->windowWidth, + appWindow->windowHeight); + } + + break; + + case WINDOW_SHOW: + /* Ensure the window is not maximized */ + xf_SendClientEvent(xfc, appWindow->handle, xfc->_NET_WM_STATE, 4, _NET_WM_STATE_REMOVE, + xfc->_NET_WM_STATE_MAXIMIZED_VERT, xfc->_NET_WM_STATE_MAXIMIZED_HORZ, + 0); + + /* + * Ignore configure requests until both the Maximized properties have been processed + * to prevent condition where WM overrides size of request due to one or both of these + * properties still being set - which causes a position adjustment to be sent back to + * the server thus causing the window to not return to its original size + */ + if (appWindow->rail_state == WINDOW_SHOW_MAXIMIZED) + appWindow->rail_ignore_configure = TRUE; + + if (appWindow->is_transient) + xf_SetWindowUnlisted(xfc, appWindow->handle); + + XMapWindow(xfc->display, appWindow->handle); + break; + } + + /* Save the current rail state of this window */ + appWindow->rail_state = state; + XFlush(xfc->display); +} + +void xf_SetWindowRects(xfContext* xfc, xfAppWindow* appWindow, RECTANGLE_16* rects, int nrects) +{ + int i; + XRectangle* xrects; + + if (nrects < 1) + return; + +#ifdef WITH_XEXT + xrects = (XRectangle*)calloc(nrects, sizeof(XRectangle)); + + for (i = 0; i < nrects; i++) + { + xrects[i].x = rects[i].left; + xrects[i].y = rects[i].top; + xrects[i].width = rects[i].right - rects[i].left; + xrects[i].height = rects[i].bottom - rects[i].top; + } + + XShapeCombineRectangles(xfc->display, appWindow->handle, ShapeBounding, 0, 0, xrects, nrects, + ShapeSet, 0); + free(xrects); +#endif +} + +void xf_SetWindowVisibilityRects(xfContext* xfc, xfAppWindow* appWindow, UINT32 rectsOffsetX, + UINT32 rectsOffsetY, RECTANGLE_16* rects, int nrects) +{ + int i; + XRectangle* xrects; + + if (nrects < 1) + return; + +#ifdef WITH_XEXT + xrects = (XRectangle*)calloc(nrects, sizeof(XRectangle)); + + for (i = 0; i < nrects; i++) + { + xrects[i].x = rects[i].left; + xrects[i].y = rects[i].top; + xrects[i].width = rects[i].right - rects[i].left; + xrects[i].height = rects[i].bottom - rects[i].top; + } + + XShapeCombineRectangles(xfc->display, appWindow->handle, ShapeBounding, rectsOffsetX, + rectsOffsetY, xrects, nrects, ShapeSet, 0); + free(xrects); +#endif +} + +void xf_UpdateWindowArea(xfContext* xfc, xfAppWindow* appWindow, int x, int y, int width, + int height) +{ + int ax, ay; + + if (appWindow == NULL) + return; + + if (appWindow->surfaceId < UINT16_MAX) + return; + + ax = x + appWindow->windowOffsetX; + ay = y + appWindow->windowOffsetY; + + if (ax + width > appWindow->windowOffsetX + appWindow->width) + width = (appWindow->windowOffsetX + appWindow->width - 1) - ax; + + if (ay + height > appWindow->windowOffsetY + appWindow->height) + height = (appWindow->windowOffsetY + appWindow->height - 1) - ay; + + xf_lock_x11(xfc); + + if (xfc->context.settings->SoftwareGdi) + { + XPutImage(xfc->display, xfc->primary, appWindow->gc, xfc->image, ax, ay, ax, ay, width, + height); + } + + XCopyArea(xfc->display, xfc->primary, appWindow->handle, appWindow->gc, ax, ay, width, height, + x, y); + XFlush(xfc->display); + xf_unlock_x11(xfc); +} + +void xf_DestroyWindow(xfContext* xfc, xfAppWindow* appWindow) +{ + if (!appWindow) + return; + + if (xfc->appWindow == appWindow) + xfc->appWindow = NULL; + + if (appWindow->gc) + XFreeGC(xfc->display, appWindow->gc); + + if (appWindow->handle) + { + XUnmapWindow(xfc->display, appWindow->handle); + XDestroyWindow(xfc->display, appWindow->handle); + } + + if (appWindow->xfwin) + munmap(0, sizeof(*appWindow->xfwin)); + + if (appWindow->shmid >= 0) + close(appWindow->shmid); + + shm_unlink(get_shm_id()); + appWindow->xfwin = (Window*)-1; + appWindow->shmid = -1; + free(appWindow->title); + free(appWindow->windowRects); + free(appWindow->visibilityRects); + free(appWindow); +} + +xfAppWindow* xf_AppWindowFromX11Window(xfContext* xfc, Window wnd) +{ + int index; + int count; + ULONG_PTR* pKeys = NULL; + xfAppWindow* appWindow; + count = HashTable_GetKeys(xfc->railWindows, &pKeys); + + for (index = 0; index < count; index++) + { + appWindow = xf_rail_get_window(xfc, *(UINT64*)pKeys[index]); + + if (!appWindow) + return NULL; + + if (appWindow->handle == wnd) + { + free(pKeys); + return appWindow; + } + } + + free(pKeys); + return NULL; +} diff --git a/client/X11/xf_window.h b/client/X11/xf_window.h new file mode 100644 index 0000000..0f85af1 --- /dev/null +++ b/client/X11/xf_window.h @@ -0,0 +1,192 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * X11 Windows + * + * Copyright 2011 Marc-Andre Moreau + * + * Licensed 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 FREERDP_CLIENT_X11_WINDOW_H +#define FREERDP_CLIENT_X11_WINDOW_H + +#include + +#include + +typedef struct xf_app_window xfAppWindow; + +typedef struct xf_localmove xfLocalMove; +typedef struct xf_window xfWindow; + +#include "xf_client.h" +#include "xf_floatbar.h" +#include "xfreerdp.h" + +// Extended ICCM flags http://standards.freedesktop.org/wm-spec/wm-spec-latest.html +#define _NET_WM_MOVERESIZE_SIZE_TOPLEFT 0 +#define _NET_WM_MOVERESIZE_SIZE_TOP 1 +#define _NET_WM_MOVERESIZE_SIZE_TOPRIGHT 2 +#define _NET_WM_MOVERESIZE_SIZE_RIGHT 3 +#define _NET_WM_MOVERESIZE_SIZE_BOTTOMRIGHT 4 +#define _NET_WM_MOVERESIZE_SIZE_BOTTOM 5 +#define _NET_WM_MOVERESIZE_SIZE_BOTTOMLEFT 6 +#define _NET_WM_MOVERESIZE_SIZE_LEFT 7 +#define _NET_WM_MOVERESIZE_MOVE 8 /* movement only */ +#define _NET_WM_MOVERESIZE_SIZE_KEYBOARD 9 /* size via keyboard */ +#define _NET_WM_MOVERESIZE_MOVE_KEYBOARD 10 /* move via keyboard */ +#define _NET_WM_MOVERESIZE_CANCEL 11 /* cancel operation */ + +#define _NET_WM_STATE_REMOVE 0 /* remove/unset property */ +#define _NET_WM_STATE_ADD 1 /* add/set property */ +#define _NET_WM_STATE_TOGGLE 2 /* toggle property */ + +enum xf_localmove_state +{ + LMS_NOT_ACTIVE, + LMS_STARTING, + LMS_ACTIVE, + LMS_TERMINATING +}; + +struct xf_localmove +{ + int root_x; + int root_y; + int window_x; + int window_y; + enum xf_localmove_state state; + int direction; +}; + +struct xf_window +{ + GC gc; + int left; + int top; + int right; + int bottom; + int width; + int height; + int shmid; + Window handle; + Window* xfwin; + xfFloatbar* floatbar; + BOOL decorations; + BOOL is_mapped; + BOOL is_transient; +}; + +struct xf_app_window +{ + xfContext* xfc; + + int x; + int y; + int width; + int height; + char* title; + + UINT32 surfaceId; + UINT64 windowId; + UINT32 ownerWindowId; + + UINT32 dwStyle; + UINT32 dwExStyle; + UINT32 showState; + + INT32 clientOffsetX; + INT32 clientOffsetY; + UINT32 clientAreaWidth; + UINT32 clientAreaHeight; + + INT32 windowOffsetX; + INT32 windowOffsetY; + INT32 windowClientDeltaX; + INT32 windowClientDeltaY; + UINT32 windowWidth; + UINT32 windowHeight; + UINT32 numWindowRects; + RECTANGLE_16* windowRects; + + INT32 visibleOffsetX; + INT32 visibleOffsetY; + UINT32 numVisibilityRects; + RECTANGLE_16* visibilityRects; + + UINT32 localWindowOffsetCorrX; + UINT32 localWindowOffsetCorrY; + + UINT32 resizeMarginLeft; + UINT32 resizeMarginTop; + UINT32 resizeMarginRight; + UINT32 resizeMarginBottom; + + GC gc; + int shmid; + Window handle; + Window* xfwin; + BOOL fullscreen; + BOOL decorations; + BOOL is_mapped; + BOOL is_transient; + xfLocalMove local_move; + BYTE rail_state; + BOOL maxVert; + BOOL maxHorz; + BOOL minimized; + BOOL rail_ignore_configure; +}; + +void xf_ewmhints_init(xfContext* xfc); + +BOOL xf_GetCurrentDesktop(xfContext* xfc); +BOOL xf_GetWorkArea(xfContext* xfc); + +void xf_SetWindowFullscreen(xfContext* xfc, xfWindow* window, BOOL fullscreen); +void xf_SetWindowMinimized(xfContext* xfc, xfWindow* window); +void xf_SetWindowDecorations(xfContext* xfc, Window window, BOOL show); +void xf_SetWindowUnlisted(xfContext* xfc, Window window); + +xfWindow* xf_CreateDesktopWindow(xfContext* xfc, char* name, int width, int height); +void xf_ResizeDesktopWindow(xfContext* xfc, xfWindow* window, int width, int height); +void xf_DestroyDesktopWindow(xfContext* xfc, xfWindow* window); + +Window xf_CreateDummyWindow(xfContext* xfc); +void xf_DestroyDummyWindow(xfContext* xfc, Window window); + +BOOL xf_GetWindowProperty(xfContext* xfc, Window window, Atom property, int length, + unsigned long* nitems, unsigned long* bytes, BYTE** prop); +void xf_SendClientEvent(xfContext* xfc, Window window, Atom atom, unsigned int numArgs, ...); + +int xf_AppWindowCreate(xfContext* xfc, xfAppWindow* appWindow); +int xf_AppWindowInit(xfContext* xfc, xfAppWindow* appWindow); +void xf_SetWindowText(xfContext* xfc, xfAppWindow* appWindow, const char* name); +void xf_MoveWindow(xfContext* xfc, xfAppWindow* appWindow, int x, int y, int width, int height); +void xf_ShowWindow(xfContext* xfc, xfAppWindow* appWindow, BYTE state); +// void xf_SetWindowIcon(xfContext* xfc, xfAppWindow* appWindow, rdpIcon* icon); +void xf_SetWindowRects(xfContext* xfc, xfAppWindow* appWindow, RECTANGLE_16* rects, int nrects); +void xf_SetWindowVisibilityRects(xfContext* xfc, xfAppWindow* appWindow, UINT32 rectsOffsetX, + UINT32 rectsOffsetY, RECTANGLE_16* rects, int nrects); +void xf_SetWindowStyle(xfContext* xfc, xfAppWindow* appWindow, UINT32 style, UINT32 ex_style); +void xf_UpdateWindowArea(xfContext* xfc, xfAppWindow* appWindow, int x, int y, int width, + int height); +void xf_DestroyWindow(xfContext* xfc, xfAppWindow* appWindow); +void xf_SetWindowMinMaxInfo(xfContext* xfc, xfAppWindow* appWindow, int maxWidth, int maxHeight, + int maxPosX, int maxPosY, int minTrackWidth, int minTrackHeight, + int maxTrackWidth, int maxTrackHeight); +void xf_StartLocalMoveSize(xfContext* xfc, xfAppWindow* appWindow, int direction, int x, int y); +void xf_EndLocalMoveSize(xfContext* xfc, xfAppWindow* appWindow); +xfAppWindow* xf_AppWindowFromX11Window(xfContext* xfc, Window wnd); + +#endif /* FREERDP_CLIENT_X11_WINDOW_H */ diff --git a/client/X11/xfreerdp-channels.1.xml b/client/X11/xfreerdp-channels.1.xml new file mode 100644 index 0000000..e69de29 diff --git a/client/X11/xfreerdp-envvar.1.xml b/client/X11/xfreerdp-envvar.1.xml new file mode 100644 index 0000000..955adf5 --- /dev/null +++ b/client/X11/xfreerdp-envvar.1.xml @@ -0,0 +1,15 @@ + + Environment variables + + + + wlog environment variable + + xfreerdp uses wLog as its log facility, you can refer to the + corresponding man page (wlog(7)) for more informations. Arguments passed + via the /log-level or /log-filters + have precedence over the environment variables. + + + + diff --git a/client/X11/xfreerdp-examples.1.xml b/client/X11/xfreerdp-examples.1.xml new file mode 100644 index 0000000..3418143 --- /dev/null +++ b/client/X11/xfreerdp-examples.1.xml @@ -0,0 +1,95 @@ + + Examples + + + xfreerdp connection.rdp /p:Pwd123! /f + + Connect in fullscreen mode using a stored configuration connection.rdp and the password Pwd123! + + + + xfreerdp /u:USER /size:50%h /v:rdp.contoso.com + + Connect to host rdp.contoso.com with user USER and a size of 50 percent of the height. If width (w) is set instead of height (h) like /size:50%w. 50 percent of the width is used. + + + + xfreerdp /u:CONTOSO\\JohnDoe /p:Pwd123! /v:rdp.contoso.com + + Connect to host rdp.contoso.com with user CONTOSO\\JohnDoe and password Pwd123! + + + + xfreerdp /u:JohnDoe /p:Pwd123! /w:1366 /h:768 /v:192.168.1.100:4489 + + Connect to host 192.168.1.100 on port 4489 with user JohnDoe, password Pwd123!. The screen width is set to 1366 and the height to 768 + + + + xfreerdp /u:JohnDoe /p:Pwd123! /vmconnect:C824F53E-95D2-46C6-9A18-23A5BB403532 /v:192.168.1.100 + + Establish a connection to host 192.168.1.100 with user JohnDoe, password Pwd123! and connect to Hyper-V console (use port 2179, disable negotiation) with VMID C824F53E-95D2-46C6-9A18-23A5BB403532 + + + + +clipboard + + Activate clipboard redirection + + + + /drive:home,/home/user + + Activate drive redirection of /home/user as home drive + + + + /smartcard:<device> + + Activate smartcard redirection for device device + + + + /printer:<device>,<driver> + + Activate printer redirection for printer device using driver driver + + + + /serial:<device> + + Activate serial port redirection for port device + + + + /parallel:<device> + + Activate parallel port redirection for port device + + + + /sound:sys:alsa + + Activate audio output redirection using device sys:alsa + + + + /microphone:sys:alsa + + Activate audio input redirection using device sys:alsa + + + + /multimedia:sys:alsa + + Activate multimedia redirection using device sys:alsa + + + + /usb:id,dev:054c:0268 + + Activate USB device redirection for the device identified by 054c:0268 + + + + diff --git a/client/X11/xfreerdp.1.xml.in b/client/X11/xfreerdp.1.xml.in new file mode 100644 index 0000000..119f7f3 --- /dev/null +++ b/client/X11/xfreerdp.1.xml.in @@ -0,0 +1,63 @@ + + + + + + ] +> + + + + @MAN_TODAY@ + + The FreeRDP Team + + + + xfreerdp + 1 + freerdp + xfreerdp + + + xfreerdp + FreeRDP X11 client + + + + @MAN_TODAY@ + + + xfreerdp [file] [options] [/v:server[:port]] + + + + + @MAN_TODAY@ + + DESCRIPTION + + xfreerdp is an X11 Remote Desktop Protocol (RDP) + client which is part of the FreeRDP project. An RDP server is built-in + to many editions of Windows. Alternative servers included xrdp and VRDP (VirtualBox). + + + + &syntax; + + &channels; + + &envvar; + + &examples; + + + LINKS + + http://www.freerdp.com/ + + + diff --git a/client/X11/xfreerdp.h b/client/X11/xfreerdp.h new file mode 100644 index 0000000..636e60a --- /dev/null +++ b/client/X11/xfreerdp.h @@ -0,0 +1,365 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * X11 Client + * + * Copyright 2011 Marc-Andre Moreau + * Copyright 2016 Thincast Technologies GmbH + * Copyright 2016 Armin Novak + * + * Licensed 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 FREERDP_CLIENT_X11_FREERDP_H +#define FREERDP_CLIENT_X11_FREERDP_H + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +typedef struct xf_context xfContext; + +#ifdef WITH_XCURSOR +#include +#endif + +#include + +#include "xf_window.h" +#include "xf_monitor.h" +#include "xf_channels.h" + +#if defined(CHANNEL_TSMF_CLIENT) +#include +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if !defined(XcursorUInt) +typedef unsigned int XcursorUInt; +#endif + +#if !defined(XcursorPixel) +typedef XcursorUInt XcursorPixel; +#endif + +struct xf_FullscreenMonitors +{ + UINT32 top; + UINT32 bottom; + UINT32 left; + UINT32 right; +}; +typedef struct xf_FullscreenMonitors xfFullscreenMonitors; + +struct xf_WorkArea +{ + UINT32 x; + UINT32 y; + UINT32 width; + UINT32 height; +}; +typedef struct xf_WorkArea xfWorkArea; + +struct xf_pointer +{ + rdpPointer pointer; + XcursorPixel* cursorPixels; + UINT32 nCursors; + UINT32 mCursors; + UINT32* cursorWidths; + UINT32* cursorHeights; + Cursor* cursors; + Cursor cursor; +}; +typedef struct xf_pointer xfPointer; + +struct xf_bitmap +{ + rdpBitmap bitmap; + Pixmap pixmap; + XImage* image; +}; +typedef struct xf_bitmap xfBitmap; + +struct xf_glyph +{ + rdpGlyph glyph; + Pixmap pixmap; +}; +typedef struct xf_glyph xfGlyph; + +typedef struct xf_clipboard xfClipboard; +typedef struct _xfDispContext xfDispContext; +typedef struct _xfVideoContext xfVideoContext; +typedef struct xf_rail_icon_cache xfRailIconCache; + +/* Number of buttons that are mapped from X11 to RDP button events. */ +#define NUM_BUTTONS_MAPPED 11 + +typedef struct +{ + int button; + UINT16 flags; +} button_map; + +struct xf_context +{ + rdpContext context; + DEFINE_RDP_CLIENT_COMMON(); + + GC gc; + int xfds; + int depth; + + GC gc_mono; + BOOL invert; + Screen* screen; + XImage* image; + Pixmap primary; + Pixmap drawing; + Visual* visual; + Display* display; + Drawable drawable; + Pixmap bitmap_mono; + Colormap colormap; + int screen_number; + int scanline_pad; + BOOL big_endian; + BOOL fullscreen; + BOOL decorations; + BOOL grab_keyboard; + BOOL unobscured; + BOOL debug; + HANDLE x11event; + xfWindow* window; + xfAppWindow* appWindow; + xfPointer* pointer; + xfWorkArea workArea; + xfFullscreenMonitors fullscreenMonitors; + int current_desktop; + BOOL remote_app; + HANDLE mutex; + BOOL UseXThreads; + BOOL cursorHidden; + + HGDI_DC hdc; + UINT32 bitmap_size; + BYTE* bitmap_buffer; + + BOOL frame_begin; + UINT16 frame_x1; + UINT16 frame_y1; + UINT16 frame_x2; + UINT16 frame_y2; + + int XInputOpcode; + + int savedWidth; + int savedHeight; + int savedPosX; + int savedPosY; + +#ifdef WITH_XRENDER + int scaledWidth; + int scaledHeight; + int offset_x; + int offset_y; +#endif + + BOOL focused; + BOOL use_xinput; + BOOL mouse_active; + BOOL fullscreen_toggle; + BOOL controlToggle; + UINT32 KeyboardLayout; + BOOL KeyboardState[256]; + XModifierKeymap* modifierMap; + wArrayList* keyCombinations; + wArrayList* xevents; + BOOL actionScriptExists; + + XSetWindowAttributes attribs; + BOOL complex_regions; + VIRTUAL_SCREEN vscreen; +#if defined(CHANNEL_TSMF_CLIENT) + void* xv_context; +#endif + + Atom* supportedAtoms; + unsigned long supportedAtomCount; + + Atom UTF8_STRING; + + Atom _XWAYLAND_MAY_GRAB_KEYBOARD; + + Atom _NET_WM_ICON; + Atom _MOTIF_WM_HINTS; + Atom _NET_CURRENT_DESKTOP; + Atom _NET_WORKAREA; + + Atom _NET_SUPPORTED; + ATOM _NET_SUPPORTING_WM_CHECK; + + Atom _NET_WM_STATE; + Atom _NET_WM_STATE_FULLSCREEN; + Atom _NET_WM_STATE_MAXIMIZED_HORZ; + Atom _NET_WM_STATE_MAXIMIZED_VERT; + Atom _NET_WM_STATE_SKIP_TASKBAR; + Atom _NET_WM_STATE_SKIP_PAGER; + + Atom _NET_WM_FULLSCREEN_MONITORS; + + Atom _NET_WM_NAME; + Atom _NET_WM_PID; + + Atom _NET_WM_WINDOW_TYPE; + Atom _NET_WM_WINDOW_TYPE_NORMAL; + Atom _NET_WM_WINDOW_TYPE_DIALOG; + Atom _NET_WM_WINDOW_TYPE_UTILITY; + Atom _NET_WM_WINDOW_TYPE_POPUP; + Atom _NET_WM_WINDOW_TYPE_POPUP_MENU; + Atom _NET_WM_WINDOW_TYPE_DROPDOWN_MENU; + + Atom _NET_WM_MOVERESIZE; + Atom _NET_MOVERESIZE_WINDOW; + + Atom WM_STATE; + Atom WM_PROTOCOLS; + Atom WM_DELETE_WINDOW; + + /* Channels */ +#if defined(CHANNEL_TSMF_CLIENT) + TsmfClientContext* tsmf; +#endif + + xfClipboard* clipboard; + CliprdrClientContext* cliprdr; + xfVideoContext* xfVideo; + RdpeiClientContext* rdpei; + EncomspClientContext* encomsp; + xfDispContext* xfDisp; + + RailClientContext* rail; + wHashTable* railWindows; + xfRailIconCache* railIconCache; + + BOOL xkbAvailable; + BOOL xrenderAvailable; + + /* value to be sent over wire for each logical client mouse button */ + button_map button_map[NUM_BUTTONS_MAPPED]; + BYTE savedMaximizedState; + UINT32 locked; + BOOL firstPressRightCtrl; + BOOL ungrabKeyboardWithRightCtrl; +}; + +BOOL xf_create_window(xfContext* xfc); +void xf_toggle_fullscreen(xfContext* xfc); +BOOL xf_toggle_control(xfContext* xfc); + +void xf_encomsp_init(xfContext* xfc, EncomspClientContext* encomsp); +void xf_encomsp_uninit(xfContext* xfc, EncomspClientContext* encomsp); + +enum XF_EXIT_CODE +{ + /* section 0-15: protocol-independent codes */ + XF_EXIT_SUCCESS = 0, + XF_EXIT_DISCONNECT = 1, + XF_EXIT_LOGOFF = 2, + XF_EXIT_IDLE_TIMEOUT = 3, + XF_EXIT_LOGON_TIMEOUT = 4, + XF_EXIT_CONN_REPLACED = 5, + XF_EXIT_OUT_OF_MEMORY = 6, + XF_EXIT_CONN_DENIED = 7, + XF_EXIT_CONN_DENIED_FIPS = 8, + XF_EXIT_USER_PRIVILEGES = 9, + XF_EXIT_FRESH_CREDENTIALS_REQUIRED = 10, + XF_EXIT_DISCONNECT_BY_USER = 11, + + /* section 16-31: license error set */ + XF_EXIT_LICENSE_INTERNAL = 16, + XF_EXIT_LICENSE_NO_LICENSE_SERVER = 17, + XF_EXIT_LICENSE_NO_LICENSE = 18, + XF_EXIT_LICENSE_BAD_CLIENT_MSG = 19, + XF_EXIT_LICENSE_HWID_DOESNT_MATCH = 20, + XF_EXIT_LICENSE_BAD_CLIENT = 21, + XF_EXIT_LICENSE_CANT_FINISH_PROTOCOL = 22, + XF_EXIT_LICENSE_CLIENT_ENDED_PROTOCOL = 23, + XF_EXIT_LICENSE_BAD_CLIENT_ENCRYPTION = 24, + XF_EXIT_LICENSE_CANT_UPGRADE = 25, + XF_EXIT_LICENSE_NO_REMOTE_CONNECTIONS = 26, + + /* section 32-127: RDP protocol error set */ + XF_EXIT_RDP = 32, + + /* section 128-254: xfreerdp specific exit codes */ + XF_EXIT_PARSE_ARGUMENTS = 128, + XF_EXIT_MEMORY = 129, + XF_EXIT_PROTOCOL = 130, + XF_EXIT_CONN_FAILED = 131, + XF_EXIT_AUTH_FAILURE = 132, + XF_EXIT_NEGO_FAILURE = 133, + XF_EXIT_LOGON_FAILURE = 134, + XF_EXIT_ACCOUNT_LOCKED_OUT = 135, + XF_EXIT_PRE_CONNECT_FAILED = 136, + XF_EXIT_CONNECT_UNDEFINED = 137, + XF_EXIT_POST_CONNECT_FAILED = 138, + XF_EXIT_DNS_ERROR = 139, + XF_EXIT_DNS_NAME_NOT_FOUND = 140, + XF_EXIT_CONNECT_FAILED = 141, + XF_EXIT_MCS_CONNECT_INITIAL_ERROR = 142, + XF_EXIT_TLS_CONNECT_FAILED = 143, + XF_EXIT_INSUFFICIENT_PRIVILEGES = 144, + XF_EXIT_CONNECT_CANCELLED = 145, + XF_EXIT_SECURITY_NEGO_CONNECT_FAILED = 146, + XF_EXIT_CONNECT_TRANSPORT_FAILED = 147, + XF_EXIT_CONNECT_PASSWORD_EXPIRED = 148, + XF_EXIT_CONNECT_PASSWORD_MUST_CHANGE = 149, + XF_EXIT_CONNECT_KDC_UNREACHABLE = 150, + XF_EXIT_CONNECT_ACCOUNT_DISABLED = 151, + XF_EXIT_CONNECT_PASSWORD_CERTAINLY_EXPIRED = 152, + XF_EXIT_CONNECT_CLIENT_REVOKED = 153, + XF_EXIT_CONNECT_WRONG_PASSWORD = 154, + XF_EXIT_CONNECT_ACCESS_DENIED = 155, + XF_EXIT_CONNECT_ACCOUNT_RESTRICTION = 156, + XF_EXIT_CONNECT_ACCOUNT_EXPIRED = 157, + XF_EXIT_CONNECT_LOGON_TYPE_NOT_GRANTED = 158, + XF_EXIT_CONNECT_NO_OR_MISSING_CREDENTIALS = 159, + XF_EXIT_UNKNOWN = 255, +}; + +#define xf_lock_x11(xfc) xf_lock_x11_(xfc, __FUNCTION__); +#define xf_unlock_x11(xfc) xf_unlock_x11_(xfc, __FUNCTION__); + +void xf_lock_x11_(xfContext* xfc, const char* fkt); +void xf_unlock_x11_(xfContext* xfc, const char* fkt); + +BOOL xf_picture_transform_required(xfContext* xfc); + +#define xf_draw_screen(_xfc, _x, _y, _w, _h) \ + xf_draw_screen_((_xfc), (_x), (_y), (_w), (_h), __FUNCTION__, __FILE__, __LINE__) +void xf_draw_screen_(xfContext* xfc, int x, int y, int w, int h, const char* fkt, const char* file, + int line); + +FREERDP_API DWORD xf_exit_code_from_disconnect_reason(DWORD reason); + +#endif /* FREERDP_CLIENT_X11_FREERDP_H */ diff --git a/client/common/CMakeLists.txt b/client/common/CMakeLists.txt new file mode 100644 index 0000000..b465a63 --- /dev/null +++ b/client/common/CMakeLists.txt @@ -0,0 +1,94 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP Client Common +# +# Copyright 2012 Marc-Andre Moreau +# +# Licensed 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. + +set(MODULE_NAME "freerdp-client") +set(MODULE_PREFIX "FREERDP_CLIENT") + +# Policy CMP0022: INTERFACE_LINK_LIBRARIES defines the link +# interface. Run "cmake --help-policy CMP0022" for policy details. Use the +# cmake_policy command to set the policy and suppress this warning. +if(POLICY CMP0022) + cmake_policy(SET CMP0022 NEW) +endif() + +set(${MODULE_PREFIX}_SRCS + client.c + cmdline.c + file.c + geometry.c) + +if(NOT DEFINE_NO_DEPRECATED) + list(APPEND ${MODULE_PREFIX}_SRCS + compatibility.c + compatibility.h) +endif() + +foreach(FREERDP_CHANNELS_CLIENT_SRC ${FREERDP_CHANNELS_CLIENT_SRCS}) + get_filename_component(NINC ${FREERDP_CHANNELS_CLIENT_SRC} PATH) + include_directories(${NINC}) + set(${MODULE_PREFIX}_SRCS ${${MODULE_PREFIX}_SRCS} "${FREERDP_CHANNELS_CLIENT_SRC}") +endforeach() + + +# On windows create dll version information. +# Vendor, product and year are already set in top level CMakeLists.txt +if (WIN32 AND BUILD_SHARED_LIBS) + set (RC_VERSION_MAJOR ${FREERDP_VERSION_MAJOR}) + set (RC_VERSION_MINOR ${FREERDP_VERSION_MINOR}) + set (RC_VERSION_BUILD ${FREERDP_VERSION_REVISION}) + set (RC_VERSION_FILE "${CMAKE_SHARED_LIBRARY_PREFIX}${MODULE_NAME}${CMAKE_SHARED_LIBRARY_SUFFIX}" ) + + configure_file( + ${CMAKE_SOURCE_DIR}/cmake/WindowsDLLVersion.rc.in + ${CMAKE_CURRENT_BINARY_DIR}/version.rc + @ONLY) + + set ( ${MODULE_PREFIX}_SRCS ${${MODULE_PREFIX}_SRCS} ${CMAKE_CURRENT_BINARY_DIR}/version.rc) +endif() + +add_library(${MODULE_NAME} ${${MODULE_PREFIX}_SRCS}) + +set_target_properties(${MODULE_NAME} PROPERTIES OUTPUT_NAME ${MODULE_NAME}${FREERDP_API_VERSION}) +include_directories(${OPENSSL_INCLUDE_DIR}) +if (WITH_LIBRARY_VERSIONING) + set_target_properties(${MODULE_NAME} PROPERTIES VERSION ${FREERDP_VERSION} SOVERSION ${FREERDP_API_VERSION}) +endif() + +set(${MODULE_PREFIX}_LIBS ${${MODULE_PREFIX}_LIBS} winpr) + +target_link_libraries(${MODULE_NAME} ${PRIVATE_KEYWORD} ${FREERDP_CHANNELS_CLIENT_LIBS}) +if(OPENBSD) + target_link_libraries(${MODULE_NAME} ${PUBLIC_KEYWORD} ${${MODULE_PREFIX}_LIBS} ossaudio) +else() + target_link_libraries(${MODULE_NAME} ${PUBLIC_KEYWORD} ${${MODULE_PREFIX}_LIBS}) +endif() + + +install(TARGETS ${MODULE_NAME} DESTINATION ${CMAKE_INSTALL_LIBDIR} COMPONENT libraries EXPORT FreeRDP-ClientTargets) + +if (WITH_DEBUG_SYMBOLS AND MSVC AND BUILD_SHARED_LIBS) + get_target_property(OUTPUT_FILENAME ${MODULE_NAME} OUTPUT_NAME) + install(FILES ${CMAKE_PDB_BINARY_DIR}/${OUTPUT_FILENAME}.pdb DESTINATION ${CMAKE_INSTALL_LIBDIR} COMPONENT symbols) +endif() + +set_property(TARGET ${MODULE_NAME} PROPERTY FOLDER "Client/Common") + +if(BUILD_TESTING) + add_subdirectory(test) +endif() + +export_complex_library(LIBNAME ${MODULE_NAME}) diff --git a/client/common/client.c b/client/common/client.c new file mode 100644 index 0000000..6862deb --- /dev/null +++ b/client/common/client.c @@ -0,0 +1,805 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * FreeRDP Client Common + * + * Copyright 2012 Marc-Andre Moreau + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include + +#include + +#include +#include +#include +#include +#include +#include + +#include +#define TAG CLIENT_TAG("common") + +static BOOL freerdp_client_common_new(freerdp* instance, rdpContext* context) +{ + RDP_CLIENT_ENTRY_POINTS* pEntryPoints = instance->pClientEntryPoints; + return IFCALLRESULT(TRUE, pEntryPoints->ClientNew, instance, context); +} + +static void freerdp_client_common_free(freerdp* instance, rdpContext* context) +{ + RDP_CLIENT_ENTRY_POINTS* pEntryPoints = instance->pClientEntryPoints; + IFCALL(pEntryPoints->ClientFree, instance, context); +} + +/* Common API */ + +rdpContext* freerdp_client_context_new(RDP_CLIENT_ENTRY_POINTS* pEntryPoints) +{ + freerdp* instance; + rdpContext* context; + + if (!pEntryPoints) + return NULL; + + IFCALL(pEntryPoints->GlobalInit); + instance = freerdp_new(); + + if (!instance) + return NULL; + + instance->settings = pEntryPoints->settings; + instance->ContextSize = pEntryPoints->ContextSize; + instance->ContextNew = freerdp_client_common_new; + instance->ContextFree = freerdp_client_common_free; + instance->pClientEntryPoints = (RDP_CLIENT_ENTRY_POINTS*)malloc(pEntryPoints->Size); + + if (!instance->pClientEntryPoints) + goto out_fail; + + CopyMemory(instance->pClientEntryPoints, pEntryPoints, pEntryPoints->Size); + + if (!freerdp_context_new(instance)) + goto out_fail2; + + context = instance->context; + context->instance = instance; + context->settings = instance->settings; + + if (freerdp_register_addin_provider(freerdp_channels_load_static_addin_entry, 0) != + CHANNEL_RC_OK) + goto out_fail2; + + return context; +out_fail2: + free(instance->pClientEntryPoints); +out_fail: + freerdp_free(instance); + return NULL; +} + +void freerdp_client_context_free(rdpContext* context) +{ + freerdp* instance; + + if (!context) + return; + + instance = context->instance; + + if (instance) + { + RDP_CLIENT_ENTRY_POINTS* pEntryPoints = instance->pClientEntryPoints; + freerdp_context_free(instance); + + if (pEntryPoints) + IFCALL(pEntryPoints->GlobalUninit); + + free(instance->pClientEntryPoints); + freerdp_free(instance); + } +} + +int freerdp_client_start(rdpContext* context) +{ + RDP_CLIENT_ENTRY_POINTS* pEntryPoints; + + if (!context || !context->instance || !context->instance->pClientEntryPoints) + return ERROR_BAD_ARGUMENTS; + + pEntryPoints = context->instance->pClientEntryPoints; + return IFCALLRESULT(CHANNEL_RC_OK, pEntryPoints->ClientStart, context); +} + +int freerdp_client_stop(rdpContext* context) +{ + RDP_CLIENT_ENTRY_POINTS* pEntryPoints; + + if (!context || !context->instance || !context->instance->pClientEntryPoints) + return ERROR_BAD_ARGUMENTS; + + pEntryPoints = context->instance->pClientEntryPoints; + return IFCALLRESULT(CHANNEL_RC_OK, pEntryPoints->ClientStop, context); +} + +freerdp* freerdp_client_get_instance(rdpContext* context) +{ + if (!context || !context->instance) + return NULL; + + return context->instance; +} + +HANDLE freerdp_client_get_thread(rdpContext* context) +{ + if (!context) + return NULL; + + return ((rdpClientContext*)context)->thread; +} + +static BOOL freerdp_client_settings_post_process(rdpSettings* settings) +{ + /* Moved GatewayUseSameCredentials logic outside of cmdline.c, so + * that the rdp file also triggers this functionality */ + if (settings->GatewayEnabled) + { + if (settings->GatewayUseSameCredentials) + { + if (settings->Username) + { + free(settings->GatewayUsername); + settings->GatewayUsername = _strdup(settings->Username); + + if (!settings->GatewayUsername) + goto out_error; + } + + if (settings->Domain) + { + free(settings->GatewayDomain); + settings->GatewayDomain = _strdup(settings->Domain); + + if (!settings->GatewayDomain) + goto out_error; + } + + if (settings->Password) + { + free(settings->GatewayPassword); + settings->GatewayPassword = _strdup(settings->Password); + + if (!settings->GatewayPassword) + goto out_error; + } + } + } + + /* Moved logic for Multimon and Span monitors to force fullscreen, so + * that the rdp file also triggers this functionality */ + if (settings->SpanMonitors) + { + settings->UseMultimon = TRUE; + settings->Fullscreen = TRUE; + } + else if (settings->UseMultimon) + { + settings->Fullscreen = TRUE; + } + + return TRUE; +out_error: + free(settings->GatewayUsername); + free(settings->GatewayDomain); + free(settings->GatewayPassword); + return FALSE; +} + +int freerdp_client_settings_parse_command_line(rdpSettings* settings, int argc, char** argv, + BOOL allowUnknown) +{ + int status; + + if (argc < 1) + return 0; + + if (!argv) + return -1; + + status = + freerdp_client_settings_parse_command_line_arguments(settings, argc, argv, allowUnknown); + + if (status < 0) + return status; + + /* This function will call logic that is applicable to the settings + * from command line parsing AND the rdp file parsing */ + if (!freerdp_client_settings_post_process(settings)) + status = -1; + + WLog_DBG(TAG, "This is %s", freerdp_get_build_config()); + return status; +} + +int freerdp_client_settings_parse_connection_file(rdpSettings* settings, const char* filename) +{ + rdpFile* file; + int ret = -1; + file = freerdp_client_rdp_file_new(); + + if (!file) + return -1; + + if (!freerdp_client_parse_rdp_file(file, filename)) + goto out; + + if (!freerdp_client_populate_settings_from_rdp_file(file, settings)) + goto out; + + ret = 0; +out: + freerdp_client_rdp_file_free(file); + return ret; +} + +int freerdp_client_settings_parse_connection_file_buffer(rdpSettings* settings, const BYTE* buffer, + size_t size) +{ + rdpFile* file; + int status = -1; + file = freerdp_client_rdp_file_new(); + + if (!file) + return -1; + + if (freerdp_client_parse_rdp_file_buffer(file, buffer, size) && + freerdp_client_populate_settings_from_rdp_file(file, settings)) + { + status = 0; + } + + freerdp_client_rdp_file_free(file); + return status; +} + +int freerdp_client_settings_write_connection_file(const rdpSettings* settings, const char* filename, + BOOL unicode) +{ + rdpFile* file; + int ret = -1; + file = freerdp_client_rdp_file_new(); + + if (!file) + return -1; + + if (!freerdp_client_populate_rdp_file_from_settings(file, settings)) + goto out; + + if (!freerdp_client_write_rdp_file(file, filename, unicode)) + goto out; + + ret = 0; +out: + freerdp_client_rdp_file_free(file); + return ret; +} + +int freerdp_client_settings_parse_assistance_file(rdpSettings* settings, int argc, char* argv[]) +{ + int status, x; + int ret = -1; + char* filename; + char* password = NULL; + rdpAssistanceFile* file; + + if (!settings || !argv || (argc < 2)) + return -1; + + filename = argv[1]; + + for (x = 2; x < argc; x++) + { + const char* key = strstr(argv[x], "assistance:"); + + if (key) + password = strchr(key, ':') + 1; + } + + file = freerdp_assistance_file_new(); + + if (!file) + return -1; + + status = freerdp_assistance_parse_file(file, filename, password); + + if (status < 0) + goto out; + + if (!freerdp_assistance_populate_settings_from_assistance_file(file, settings)) + goto out; + + ret = 0; +out: + freerdp_assistance_file_free(file); + return ret; +} + +/** Callback set in the rdp_freerdp structure, and used to get the user's password, + * if required to establish the connection. + * This function is actually called in credssp_ntlmssp_client_init() + * @see rdp_server_accept_nego() and rdp_check_fds() + * @param instance - pointer to the rdp_freerdp structure that contains the connection settings + * @param username - unused + * @param password - on return: pointer to a character string that will be filled by the password + * entered by the user. Note that this character string will be allocated inside the function, and + * needs to be deallocated by the caller using free(), even in case this function fails. + * @param domain - unused + * @return TRUE if a password was successfully entered. See freerdp_passphrase_read() for more + * details. + */ +static BOOL client_cli_authenticate_raw(freerdp* instance, BOOL gateway, char** username, + char** password, char** domain) +{ + static const size_t password_size = 512; + const char* auth[] = { "Username: ", "Domain: ", "Password: " }; + const char* gw[] = { "GatewayUsername: ", "GatewayDomain: ", "GatewayPassword: " }; + const char** prompt = (gateway) ? gw : auth; + + if (!username || !password || !domain) + return FALSE; + + if (!*username) + { + size_t username_size = 0; + printf("%s", prompt[0]); + + if (GetLine(username, &username_size, stdin) < 0) + { + WLog_ERR(TAG, "GetLine returned %s [%d]", strerror(errno), errno); + goto fail; + } + + if (*username) + { + *username = StrSep(username, "\r"); + *username = StrSep(username, "\n"); + } + } + + if (!*domain) + { + size_t domain_size = 0; + printf("%s", prompt[1]); + + if (GetLine(domain, &domain_size, stdin) < 0) + { + WLog_ERR(TAG, "GetLine returned %s [%d]", strerror(errno), errno); + goto fail; + } + + if (*domain) + { + *domain = StrSep(domain, "\r"); + *domain = StrSep(domain, "\n"); + } + } + + if (!*password) + { + *password = calloc(password_size, sizeof(char)); + + if (!*password) + goto fail; + + if (freerdp_passphrase_read(prompt[2], *password, password_size, + instance->settings->CredentialsFromStdin) == NULL) + goto fail; + } + + return TRUE; +fail: + free(*username); + free(*domain); + free(*password); + *username = NULL; + *domain = NULL; + *password = NULL; + return FALSE; +} + +BOOL client_cli_authenticate(freerdp* instance, char** username, char** password, char** domain) +{ + if (instance->settings->SmartcardLogon) + { + WLog_INFO(TAG, "Authentication via smartcard"); + return TRUE; + } + + return client_cli_authenticate_raw(instance, FALSE, username, password, domain); +} + +BOOL client_cli_gw_authenticate(freerdp* instance, char** username, char** password, char** domain) +{ + return client_cli_authenticate_raw(instance, TRUE, username, password, domain); +} + +static DWORD client_cli_accept_certificate(rdpSettings* settings) +{ + char answer; + + if (settings->CredentialsFromStdin) + return 0; + + while (1) + { + printf("Do you trust the above certificate? (Y/T/N) "); + fflush(stdout); + answer = fgetc(stdin); + + if (feof(stdin)) + { + printf("\nError: Could not read answer from stdin."); + + if (settings->CredentialsFromStdin) + printf(" - Run without parameter \"--from-stdin\" to set trust."); + + printf("\n"); + return 0; + } + + switch (answer) + { + case 'y': + case 'Y': + fgetc(stdin); + return 1; + + case 't': + case 'T': + fgetc(stdin); + return 2; + + case 'n': + case 'N': + fgetc(stdin); + return 0; + + default: + break; + } + + printf("\n"); + } + + return 0; +} + +/** Callback set in the rdp_freerdp structure, and used to make a certificate validation + * when the connection requires it. + * This function will actually be called by tls_verify_certificate(). + * @see rdp_client_connect() and tls_connect() + * @deprecated Use client_cli_verify_certificate_ex + * @param instance - pointer to the rdp_freerdp structure that contains the connection settings + * @param common_name + * @param subject + * @param issuer + * @param fingerprint + * @param host_mismatch Indicates the certificate host does not match. + * @return 1 if the certificate is trusted, 2 if temporary trusted, 0 otherwise. + */ +DWORD client_cli_verify_certificate(freerdp* instance, const char* common_name, const char* subject, + const char* issuer, const char* fingerprint, BOOL host_mismatch) +{ + WINPR_UNUSED(common_name); + WINPR_UNUSED(host_mismatch); + + printf("WARNING: This callback is deprecated, migrate to client_cli_verify_certificate_ex\n"); + printf("Certificate details:\n"); + printf("\tSubject: %s\n", subject); + printf("\tIssuer: %s\n", issuer); + printf("\tThumbprint: %s\n", fingerprint); + printf("The above X.509 certificate could not be verified, possibly because you do not have\n" + "the CA certificate in your certificate store, or the certificate has expired.\n" + "Please look at the OpenSSL documentation on how to add a private CA to the store.\n"); + return client_cli_accept_certificate(instance->settings); +} + +/** Callback set in the rdp_freerdp structure, and used to make a certificate validation + * when the connection requires it. + * This function will actually be called by tls_verify_certificate(). + * @see rdp_client_connect() and tls_connect() + * @param instance pointer to the rdp_freerdp structure that contains the connection settings + * @param host The host currently connecting to + * @param port The port currently connecting to + * @param common_name The common name of the certificate, should match host or an alias of it + * @param subject The subject of the certificate + * @param issuer The certificate issuer name + * @param fingerprint The fingerprint of the certificate + * @param flags See VERIFY_CERT_FLAG_* for possible values. + * + * @return 1 if the certificate is trusted, 2 if temporary trusted, 0 otherwise. + */ +DWORD client_cli_verify_certificate_ex(freerdp* instance, const char* host, UINT16 port, + const char* common_name, const char* subject, + const char* issuer, const char* fingerprint, DWORD flags) +{ + const char* type = "RDP-Server"; + + if (flags & VERIFY_CERT_FLAG_GATEWAY) + type = "RDP-Gateway"; + + if (flags & VERIFY_CERT_FLAG_REDIRECT) + type = "RDP-Redirect"; + + printf("Certificate details for %s:%" PRIu16 " (%s):\n", host, port, type); + printf("\tCommon Name: %s\n", common_name); + printf("\tSubject: %s\n", subject); + printf("\tIssuer: %s\n", issuer); + printf("\tThumbprint: %s\n", fingerprint); + + printf("The above X.509 certificate could not be verified, possibly because you do not have\n" + "the CA certificate in your certificate store, or the certificate has expired.\n" + "Please look at the OpenSSL documentation on how to add a private CA to the store.\n"); + return client_cli_accept_certificate(instance->settings); +} + +/** Callback set in the rdp_freerdp structure, and used to make a certificate validation + * when a stored certificate does not match the remote counterpart. + * This function will actually be called by tls_verify_certificate(). + * @see rdp_client_connect() and tls_connect() + * @deprecated Use client_cli_verify_changed_certificate_ex + * @param instance - pointer to the rdp_freerdp structure that contains the connection settings + * @param common_name + * @param subject + * @param issuer + * @param fingerprint + * @param old_subject + * @param old_issuer + * @param old_fingerprint + * @return 1 if the certificate is trusted, 2 if temporary trusted, 0 otherwise. + */ +DWORD client_cli_verify_changed_certificate(freerdp* instance, const char* common_name, + const char* subject, const char* issuer, + const char* fingerprint, const char* old_subject, + const char* old_issuer, const char* old_fingerprint) +{ + WINPR_UNUSED(common_name); + + printf("WARNING: This callback is deprecated, migrate to " + "client_cli_verify_changed_certificate_ex\n"); + printf("!!! Certificate has changed !!!\n"); + printf("\n"); + printf("New Certificate details:\n"); + printf("\tSubject: %s\n", subject); + printf("\tIssuer: %s\n", issuer); + printf("\tThumbprint: %s\n", fingerprint); + printf("\n"); + printf("Old Certificate details:\n"); + printf("\tSubject: %s\n", old_subject); + printf("\tIssuer: %s\n", old_issuer); + printf("\tThumbprint: %s\n", old_fingerprint); + printf("\n"); + printf("The above X.509 certificate does not match the certificate used for previous " + "connections.\n" + "This may indicate that the certificate has been tampered with.\n" + "Please contact the administrator of the RDP server and clarify.\n"); + return client_cli_accept_certificate(instance->settings); +} + +/** Callback set in the rdp_freerdp structure, and used to make a certificate validation + * when a stored certificate does not match the remote counterpart. + * This function will actually be called by tls_verify_certificate(). + * @see rdp_client_connect() and tls_connect() + * @param instance pointer to the rdp_freerdp structure that contains the connection + * settings + * @param host The host currently connecting to + * @param port The port currently connecting to + * @param common_name The common name of the certificate, should match host or an alias of it + * @param subject The subject of the certificate + * @param issuer The certificate issuer name + * @param fingerprint The fingerprint of the certificate + * @param old_subject The subject of the previous certificate + * @param old_issuer The previous certificate issuer name + * @param old_fingerprint The fingerprint of the previous certificate + * @param flags See VERIFY_CERT_FLAG_* for possible values. + * + * @return 1 if the certificate is trusted, 2 if temporary trusted, 0 otherwise. + */ +DWORD client_cli_verify_changed_certificate_ex(freerdp* instance, const char* host, UINT16 port, + const char* common_name, const char* subject, + const char* issuer, const char* fingerprint, + const char* old_subject, const char* old_issuer, + const char* old_fingerprint, DWORD flags) +{ + const char* type = "RDP-Server"; + + if (flags & VERIFY_CERT_FLAG_GATEWAY) + type = "RDP-Gateway"; + + if (flags & VERIFY_CERT_FLAG_REDIRECT) + type = "RDP-Redirect"; + + printf("!!!Certificate for %s:%" PRIu16 " (%s) has changed!!!\n", host, port, type); + printf("\n"); + printf("New Certificate details:\n"); + printf("\tCommon Name: %s\n", common_name); + printf("\tSubject: %s\n", subject); + printf("\tIssuer: %s\n", issuer); + printf("\tThumbprint: %s\n", fingerprint); + printf("\n"); + printf("Old Certificate details:\n"); + printf("\tSubject: %s\n", old_subject); + printf("\tIssuer: %s\n", old_issuer); + printf("\tThumbprint: %s\n", old_fingerprint); + printf("\n"); + if (flags & VERIFY_CERT_FLAG_MATCH_LEGACY_SHA1) + { + printf("\tA matching entry with legacy SHA1 was found in local known_hosts2 store.\n"); + printf("\tIf you just upgraded from a FreeRDP version before 2.0 this is expected.\n"); + printf("\tThe hashing algorithm has been upgraded from SHA1 to SHA256.\n"); + printf("\tAll manually accepted certificates must be reconfirmed!\n"); + printf("\n"); + } + printf("The above X.509 certificate does not match the certificate used for previous " + "connections.\n" + "This may indicate that the certificate has been tampered with.\n" + "Please contact the administrator of the RDP server and clarify.\n"); + return client_cli_accept_certificate(instance->settings); +} + +BOOL client_cli_present_gateway_message(freerdp* instance, UINT32 type, BOOL isDisplayMandatory, + BOOL isConsentMandatory, size_t length, + const WCHAR* message) +{ + char answer; + const char* msgType = (type == GATEWAY_MESSAGE_CONSENT) ? "Consent message" : "Service message"; + + if (!isDisplayMandatory && !isConsentMandatory) + return TRUE; + + printf("%s:\n", msgType); +#if defined(WIN32) + printf("%.*S\n", (int)length, message); +#else + { + LPSTR msg; + if (ConvertFromUnicode(CP_UTF8, 0, message, (int)(length / 2), &msg, 0, NULL, NULL) < 1) + { + printf("Failed to convert message!\n"); + return FALSE; + } + printf("%s\n", msg); + free(msg); + } +#endif + + while (isConsentMandatory) + { + printf("I understand and agree to the terms of this policy (Y/N) \n"); + fflush(stdout); + answer = fgetc(stdin); + + if (feof(stdin)) + { + printf("\nError: Could not read answer from stdin.\n"); + return FALSE; + } + + switch (answer) + { + case 'y': + case 'Y': + fgetc(stdin); + return TRUE; + + case 'n': + case 'N': + fgetc(stdin); + return FALSE; + + default: + break; + } + + printf("\n"); + } + + return TRUE; +} + +BOOL client_auto_reconnect(freerdp* instance) +{ + return client_auto_reconnect_ex(instance, NULL); +} + +BOOL client_auto_reconnect_ex(freerdp* instance, BOOL (*window_events)(freerdp* instance)) +{ + BOOL retry = TRUE; + UINT32 error; + UINT32 maxRetries; + UINT32 numRetries = 0; + rdpSettings* settings; + + if (!instance || !instance->settings) + return FALSE; + + settings = instance->settings; + maxRetries = settings->AutoReconnectMaxRetries; + + /* Only auto reconnect on network disconnects. */ + error = freerdp_error_info(instance); + switch (error) + { + case ERRINFO_GRAPHICS_SUBSYSTEM_FAILED: + /* A network disconnect was detected */ + WLog_WARN(TAG, "Disconnected by server hitting a bug or resource limit [%s]", + freerdp_get_error_info_string(error)); + break; + case ERRINFO_SUCCESS: + /* A network disconnect was detected */ + WLog_INFO(TAG, "Network disconnect!"); + break; + default: + return FALSE; + } + + if (!settings->AutoReconnectionEnabled) + { + /* No auto-reconnect - just quit */ + return FALSE; + } + + /* Perform an auto-reconnect. */ + while (retry) + { + UINT32 x; + + /* Quit retrying if max retries has been exceeded */ + if ((maxRetries > 0) && (numRetries++ >= maxRetries)) + { + return FALSE; + } + + /* Attempt the next reconnect */ + WLog_INFO(TAG, "Attempting reconnect (%" PRIu32 " of %" PRIu32 ")", numRetries, maxRetries); + + if (freerdp_reconnect(instance)) + return TRUE; + + switch (freerdp_get_last_error(instance->context)) + { + case FREERDP_ERROR_CONNECT_CANCELLED: + WLog_WARN(TAG, "Autoreconnect aborted by user"); + retry = FALSE; + break; + default: + break; + } + for (x = 0; x < 50; x++) + { + if (!IFCALLRESULT(TRUE, window_events, instance)) + return FALSE; + + Sleep(10); + } + } + + WLog_ERR(TAG, "Maximum reconnect retries exceeded"); + return FALSE; +} diff --git a/client/common/cmdline.c b/client/common/cmdline.c new file mode 100644 index 0000000..d2d949b --- /dev/null +++ b/client/common/cmdline.c @@ -0,0 +1,3907 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * FreeRDP Client Command-Line Interface + * + * Copyright 2012 Marc-Andre Moreau + * Copyright 2014 Norbert Federa + * Copyright 2016 Armin Novak + * + * Licensed 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include + +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "compatibility.h" +#include "cmdline.h" + +#include + +#define TAG CLIENT_TAG("common.cmdline") + +static BOOL freerdp_client_print_codepages(const char* arg) +{ + size_t count = 0, x; + DWORD column = 2; + const char* filter = NULL; + char buffer[80]; + RDP_CODEPAGE* pages; + + if (arg) + filter = strchr(arg, ',') + 1; + pages = freerdp_keyboard_get_matching_codepages(column, filter, &count); + if (!pages) + return TRUE; + + printf("%-10s %-8s %-60s %-36s %-48s\n", "", "", "", "", + ""); + for (x = 0; x < count; x++) + { + const RDP_CODEPAGE* page = &pages[x]; + if (strnlen(page->subLanguageSymbol, ARRAYSIZE(page->subLanguageSymbol)) > 0) + _snprintf(buffer, sizeof(buffer), "[%s|%s]", page->primaryLanguageSymbol, + page->subLanguageSymbol); + else + _snprintf(buffer, sizeof(buffer), "[%s]", page->primaryLanguageSymbol); + printf("id=0x%04" PRIx16 ": [%-6s] %-60s %-36s %-48s\n", page->id, page->locale, buffer, + page->primaryLanguage, page->subLanguage); + } + freerdp_codepages_free(pages); + return TRUE; +} + +static BOOL freerdp_path_valid(const char* path, BOOL* special) +{ + const char DynamicDrives[] = "DynamicDrives"; + BOOL isPath = FALSE; + BOOL isSpecial; + if (!path) + return FALSE; + + isSpecial = (strncmp(path, "*", 2) == 0) || + (strncmp(path, DynamicDrives, sizeof(DynamicDrives)) == 0) || + (strncmp(path, "%", 2) == 0) + ? TRUE + : FALSE; + if (!isSpecial) + isPath = winpr_PathFileExists(path); + + if (special) + *special = isSpecial; + + return isSpecial || isPath; +} + +static BOOL freerdp_sanitize_drive_name(char* name, const char* invalid, const char* replacement) +{ + if (!name || !invalid || !replacement) + return FALSE; + if (strlen(invalid) != strlen(replacement)) + return FALSE; + + while (*invalid != '\0') + { + const char what = *invalid++; + const char with = *replacement++; + + char* cur = name; + while ((cur = strchr(cur, what)) != NULL) + *cur = with; + } + return TRUE; +} + +static char* name_from_path(const char* path) +{ + const char* name = "NULL"; + if (path) + { + if (_strnicmp(path, "%", 2) == 0) + name = "home"; + else if (_strnicmp(path, "*", 2) == 0) + name = "hotplug-all"; + else if (_strnicmp(path, "DynamicDrives", 2) == 0) + name = "hotplug"; + else + name = path; + } + return _strdup(name); +} + +static BOOL freerdp_client_add_drive(rdpSettings* settings, const char* path, const char* name) +{ + RDPDR_DRIVE* drive; + + drive = (RDPDR_DRIVE*)calloc(1, sizeof(RDPDR_DRIVE)); + + if (!drive) + return FALSE; + + drive->Type = RDPDR_DTYP_FILESYSTEM; + + if (name) + { + /* Path was entered as secondary argument, swap */ + if (winpr_PathFileExists(name)) + { + if (!winpr_PathFileExists(path) || (!PathIsRelativeA(name) && PathIsRelativeA(path))) + { + const char* tmp = path; + path = name; + name = tmp; + } + } + } + + if (name) + { + if (!(drive->Name = _strdup(name))) + goto fail; + } + else /* We need a name to send to the server. */ + { + if (!(drive->Name = name_from_path(path))) + goto fail; + } + + if (!path || !freerdp_sanitize_drive_name(drive->Name, "\\/", "__")) + goto fail; + else + { + BOOL isSpecial = FALSE; + BOOL isPath = freerdp_path_valid(path, &isSpecial); + + if ((!isPath && !isSpecial) || !(drive->Path = _strdup(path))) + goto fail; + } + + if (!freerdp_device_collection_add(settings, (RDPDR_DEVICE*)drive)) + goto fail; + + return TRUE; + +fail: + free(drive->Path); + free(drive->Name); + free(drive); + return FALSE; +} + +static BOOL copy_value(const char* value, char** dst) +{ + if (!dst || !value) + return FALSE; + + free(*dst); + (*dst) = _strdup(value); + return (*dst) != NULL; +} + +static BOOL append_value(const char* value, char** dst) +{ + size_t x = 0, y; + size_t size; + char* tmp; + if (!dst || !value) + return FALSE; + + if (*dst) + x = strlen(*dst); + y = strlen(value); + + size = x + y + 2; + tmp = realloc(*dst, size); + if (!tmp) + return FALSE; + if (x == 0) + tmp[0] = '\0'; + else + winpr_str_append(",", tmp, size, NULL); + winpr_str_append(value, tmp, size, NULL); + *dst = tmp; + return TRUE; +} + +static BOOL value_to_int(const char* value, LONGLONG* result, LONGLONG min, LONGLONG max) +{ + long long rc; + + if (!value || !result) + return FALSE; + + errno = 0; + rc = _strtoi64(value, NULL, 0); + + if (errno != 0) + return FALSE; + + if ((rc < min) || (rc > max)) + return FALSE; + + *result = rc; + return TRUE; +} + +static BOOL value_to_uint(const char* value, ULONGLONG* result, ULONGLONG min, ULONGLONG max) +{ + unsigned long long rc; + + if (!value || !result) + return FALSE; + + errno = 0; + rc = _strtoui64(value, NULL, 0); + + if (errno != 0) + return FALSE; + + if ((rc < min) || (rc > max)) + return FALSE; + + *result = rc; + return TRUE; +} + +BOOL freerdp_client_print_version(void) +{ + printf("This is FreeRDP version %s (%s)\n", FREERDP_VERSION_FULL, GIT_REVISION); + return TRUE; +} + +BOOL freerdp_client_print_buildconfig(void) +{ + printf("%s", freerdp_get_build_config()); + return TRUE; +} + +static char* print_token(char* text, size_t start_offset, size_t* current, size_t limit, + const char delimiter) +{ + int rc; + size_t len = strlen(text); + + if (*current < start_offset) + { + rc = printf("%*c", (int)(start_offset - *current), ' '); + if (rc < 0) + return NULL; + *current += (size_t)rc; + } + + if (*current + len > limit) + { + size_t x; + + for (x = MIN(len, limit - start_offset); x > 1; x--) + { + if (text[x] == delimiter) + { + printf("%.*s\n", (int)x, text); + *current = 0; + return &text[x]; + } + } + + return NULL; + } + + rc = printf("%s", text); + if (rc < 0) + return NULL; + *current += (size_t)rc; + return NULL; +} + +static size_t print_optionals(const char* text, size_t start_offset, size_t current) +{ + const size_t limit = 80; + char* str = _strdup(text); + char* cur = print_token(str, start_offset, ¤t, limit, '['); + + while (cur) + cur = print_token(cur, start_offset, ¤t, limit, '['); + + free(str); + return current; +} + +static size_t print_description(const char* text, size_t start_offset, size_t current) +{ + const size_t limit = 80; + char* str = _strdup(text); + char* cur = print_token(str, start_offset, ¤t, limit, ' '); + + while (cur) + { + cur++; + cur = print_token(cur, start_offset, ¤t, limit, ' '); + } + + free(str); + current += (size_t)printf("\n"); + return current; +} + +static void freerdp_client_print_command_line_args(COMMAND_LINE_ARGUMENT_A* arg) +{ + if (!arg) + return; + + do + { + int rc; + size_t pos = 0; + const size_t description_offset = 30 + 8; + + if (arg->Flags & COMMAND_LINE_VALUE_BOOL) + rc = printf(" %s%s", arg->Default ? "-" : "+", arg->Name); + else + rc = printf(" /%s", arg->Name); + + if (rc < 0) + return; + pos += (size_t)rc; + + if ((arg->Flags & COMMAND_LINE_VALUE_REQUIRED) || + (arg->Flags & COMMAND_LINE_VALUE_OPTIONAL)) + { + if (arg->Format) + { + if (arg->Flags & COMMAND_LINE_VALUE_OPTIONAL) + { + rc = printf("[:"); + if (rc < 0) + return; + pos += (size_t)rc; + pos = print_optionals(arg->Format, pos, pos); + rc = printf("]"); + if (rc < 0) + return; + pos += (size_t)rc; + } + else + { + rc = printf(":"); + if (rc < 0) + return; + pos += (size_t)rc; + pos = print_optionals(arg->Format, pos, pos); + } + + if (pos > description_offset) + { + printf("\n"); + pos = 0; + } + } + } + + rc = printf("%*c", (int)(description_offset - pos), ' '); + if (rc < 0) + return; + pos += (size_t)rc; + + if (arg->Flags & COMMAND_LINE_VALUE_BOOL) + { + rc = printf("%s ", arg->Default ? "Disable" : "Enable"); + if (rc < 0) + return; + pos += (size_t)rc; + } + + print_description(arg->Text, description_offset, pos); + } while ((arg = CommandLineFindNextArgumentA(arg)) != NULL); +} + +BOOL freerdp_client_print_command_line_help(int argc, char** argv) +{ + return freerdp_client_print_command_line_help_ex(argc, argv, NULL); +} + +BOOL freerdp_client_print_command_line_help_ex(int argc, char** argv, + COMMAND_LINE_ARGUMENT_A* custom) +{ + const char* name = "FreeRDP"; + COMMAND_LINE_ARGUMENT_A largs[ARRAYSIZE(args)]; + memcpy(largs, args, sizeof(args)); + + if (argc > 0) + name = argv[0]; + + printf("\n"); + printf("FreeRDP - A Free Remote Desktop Protocol Implementation\n"); + printf("See www.freerdp.com for more information\n"); + printf("\n"); + printf("Usage: %s [file] [options] [/v:[:port]]\n", argv[0]); + printf("\n"); + printf("Syntax:\n"); + printf(" /flag (enables flag)\n"); + printf(" /option: (specifies option with value)\n"); + printf(" +toggle -toggle (enables or disables toggle, where '/' is a synonym of '+')\n"); + printf("\n"); + freerdp_client_print_command_line_args(custom); + freerdp_client_print_command_line_args(largs); + printf("\n"); + printf("Examples:\n"); + printf(" %s connection.rdp /p:Pwd123! /f\n", name); + printf(" %s /u:CONTOSO\\JohnDoe /p:Pwd123! /v:rdp.contoso.com\n", name); + printf(" %s /u:JohnDoe /p:Pwd123! /w:1366 /h:768 /v:192.168.1.100:4489\n", name); + printf(" %s /u:JohnDoe /p:Pwd123! /vmconnect:C824F53E-95D2-46C6-9A18-23A5BB403532 " + "/v:192.168.1.100\n", + name); + printf("\n"); + printf("Clipboard Redirection: +clipboard\n"); + printf("\n"); + printf("Drive Redirection: /drive:home,/home/user\n"); + printf("Smartcard Redirection: /smartcard:\n"); + printf("Serial Port Redirection: /serial:,,[SerCx2|SerCx|Serial],[permissive]\n"); + printf("Serial Port Redirection: /serial:COM1,/dev/ttyS0\n"); + printf("Parallel Port Redirection: /parallel:,\n"); + printf("Printer Redirection: /printer:,\n"); + printf("TCP redirection: /rdp2tcp:/usr/bin/rdp2tcp\n"); + printf("\n"); + printf("Audio Output Redirection: /sound:sys:oss,dev:1,format:1\n"); + printf("Audio Output Redirection: /sound:sys:alsa\n"); + printf("Audio Input Redirection: /microphone:sys:oss,dev:1,format:1\n"); + printf("Audio Input Redirection: /microphone:sys:alsa\n"); + printf("\n"); + printf("Multimedia Redirection: /video\n"); +#ifdef CHANNEL_URBDRC_CLIENT + printf("USB Device Redirection: /usb:id:054c:0268#4669:6e6b,addr:04:0c\n"); +#endif + printf("\n"); + printf("For Gateways, the https_proxy environment variable is respected:\n"); +#ifdef _WIN32 + printf(" set HTTPS_PROXY=http://proxy.contoso.com:3128/\n"); +#else + printf(" export https_proxy=http://proxy.contoso.com:3128/\n"); +#endif + printf(" %s /g:rdp.contoso.com ...\n", name); + printf("\n"); + printf("More documentation is coming, in the meantime consult source files\n"); + printf("\n"); + return TRUE; +} + +static int freerdp_client_command_line_pre_filter(void* context, int index, int argc, LPSTR* argv) +{ + if (index == 1) + { + size_t length; + rdpSettings* settings; + + if (argc <= index) + return -1; + + length = strlen(argv[index]); + + if (length > 4) + { + if (_stricmp(&(argv[index])[length - 4], ".rdp") == 0) + { + settings = (rdpSettings*)context; + + if (!copy_value(argv[index], &settings->ConnectionFile)) + return COMMAND_LINE_ERROR_MEMORY; + + return 1; + } + } + + if (length > 13) + { + if (_stricmp(&(argv[index])[length - 13], ".msrcIncident") == 0) + { + settings = (rdpSettings*)context; + + if (!copy_value(argv[index], &settings->AssistanceFile)) + return COMMAND_LINE_ERROR_MEMORY; + + return 1; + } + } + } + + return 0; +} + +BOOL freerdp_client_add_device_channel(rdpSettings* settings, size_t count, char** params) +{ + if (strcmp(params[0], "drive") == 0) + { + BOOL rc; + if (count < 2) + return FALSE; + + settings->DeviceRedirection = TRUE; + if (count < 3) + rc = freerdp_client_add_drive(settings, params[1], NULL); + else + rc = freerdp_client_add_drive(settings, params[2], params[1]); + + return rc; + } + else if (strcmp(params[0], "printer") == 0) + { + RDPDR_PRINTER* printer; + + if (count < 1) + return FALSE; + + settings->RedirectPrinters = TRUE; + settings->DeviceRedirection = TRUE; + + if (count > 1) + { + printer = (RDPDR_PRINTER*)calloc(1, sizeof(RDPDR_PRINTER)); + + if (!printer) + return FALSE; + + printer->Type = RDPDR_DTYP_PRINT; + + if (!(printer->Name = _strdup(params[1]))) + { + free(printer); + return FALSE; + } + + if (count > 2) + { + if (!(printer->DriverName = _strdup(params[2]))) + { + free(printer->Name); + free(printer); + return FALSE; + } + } + + if (!freerdp_device_collection_add(settings, (RDPDR_DEVICE*)printer)) + { + free(printer->DriverName); + free(printer->Name); + free(printer); + return FALSE; + } + } + + return TRUE; + } + else if (strcmp(params[0], "smartcard") == 0) + { + RDPDR_SMARTCARD* smartcard; + + if (count < 1) + return FALSE; + + settings->RedirectSmartCards = TRUE; + settings->DeviceRedirection = TRUE; + smartcard = (RDPDR_SMARTCARD*)calloc(1, sizeof(RDPDR_SMARTCARD)); + + if (!smartcard) + return FALSE; + + smartcard->Type = RDPDR_DTYP_SMARTCARD; + + if (count > 1 && strlen(params[1])) + { + if (!(smartcard->Name = _strdup(params[1]))) + { + free(smartcard); + return FALSE; + } + } + + if (!freerdp_device_collection_add(settings, (RDPDR_DEVICE*)smartcard)) + { + free(smartcard->Name); + free(smartcard); + return FALSE; + } + + return TRUE; + } + else if (strcmp(params[0], "serial") == 0) + { + RDPDR_SERIAL* serial; + + if (count < 1) + return FALSE; + + settings->RedirectSerialPorts = TRUE; + settings->DeviceRedirection = TRUE; + serial = (RDPDR_SERIAL*)calloc(1, sizeof(RDPDR_SERIAL)); + + if (!serial) + return FALSE; + + serial->Type = RDPDR_DTYP_SERIAL; + + if (count > 1) + { + if (!(serial->Name = _strdup(params[1]))) + { + free(serial); + return FALSE; + } + } + + if (count > 2) + { + if (!(serial->Path = _strdup(params[2]))) + { + free(serial->Name); + free(serial); + return FALSE; + } + } + + if (count > 3) + { + if (!(serial->Driver = _strdup(params[3]))) + { + free(serial->Path); + free(serial->Name); + free(serial); + return FALSE; + } + } + + if (count > 4) + { + if (!(serial->Permissive = _strdup(params[4]))) + { + free(serial->Driver); + free(serial->Path); + free(serial->Name); + free(serial); + return FALSE; + } + } + + if (!freerdp_device_collection_add(settings, (RDPDR_DEVICE*)serial)) + { + free(serial->Permissive); + free(serial->Driver); + free(serial->Path); + free(serial->Name); + free(serial); + return FALSE; + } + + return TRUE; + } + else if (strcmp(params[0], "parallel") == 0) + { + RDPDR_PARALLEL* parallel; + + if (count < 1) + return FALSE; + + settings->RedirectParallelPorts = TRUE; + settings->DeviceRedirection = TRUE; + parallel = (RDPDR_PARALLEL*)calloc(1, sizeof(RDPDR_PARALLEL)); + + if (!parallel) + return FALSE; + + parallel->Type = RDPDR_DTYP_PARALLEL; + + if (count > 1) + { + if (!(parallel->Name = _strdup(params[1]))) + { + free(parallel); + return FALSE; + } + } + + if (count > 2) + { + if (!(parallel->Path = _strdup(params[2]))) + { + free(parallel->Name); + free(parallel); + return FALSE; + } + } + + if (!freerdp_device_collection_add(settings, (RDPDR_DEVICE*)parallel)) + { + free(parallel->Path); + free(parallel->Name); + free(parallel); + return FALSE; + } + + return TRUE; + } + + return FALSE; +} + +BOOL freerdp_client_add_static_channel(rdpSettings* settings, size_t count, char** params) +{ + int index; + ADDIN_ARGV* args; + + if (!settings || !params || !params[0] || (count > INT_MAX)) + return FALSE; + + if (freerdp_static_channel_collection_find(settings, params[0])) + return TRUE; + + args = (ADDIN_ARGV*)calloc(1, sizeof(ADDIN_ARGV)); + + if (!args) + return FALSE; + + args->argc = (int)count; + args->argv = (char**)calloc((size_t)args->argc, sizeof(char*)); + + if (!args->argv) + goto error_argv; + + for (index = 0; index < args->argc; index++) + { + args->argv[index] = _strdup(params[index]); + + if (!args->argv[index]) + { + for (--index; index >= 0; --index) + free(args->argv[index]); + + goto error_argv_strdup; + } + } + + if (!freerdp_static_channel_collection_add(settings, args)) + goto error_argv_index; + + return TRUE; +error_argv_index: + + for (index = 0; index < args->argc; index++) + free(args->argv[index]); + +error_argv_strdup: + free(args->argv); +error_argv: + free(args); + return FALSE; +} + +BOOL freerdp_client_add_dynamic_channel(rdpSettings* settings, size_t count, char** params) +{ + int index; + ADDIN_ARGV* args; + + if (!settings || !params || !params[0] || (count > INT_MAX)) + return FALSE; + + if (freerdp_dynamic_channel_collection_find(settings, params[0])) + return TRUE; + + args = (ADDIN_ARGV*)malloc(sizeof(ADDIN_ARGV)); + + if (!args) + return FALSE; + + args->argc = (int)count; + args->argv = (char**)calloc((size_t)args->argc, sizeof(char*)); + + if (!args->argv) + goto error_argv; + + for (index = 0; index < args->argc; index++) + { + args->argv[index] = _strdup(params[index]); + + if (!args->argv[index]) + { + for (--index; index >= 0; --index) + free(args->argv[index]); + + goto error_argv_strdup; + } + } + + if (!freerdp_dynamic_channel_collection_add(settings, args)) + goto error_argv_index; + + return TRUE; +error_argv_index: + + for (index = 0; index < args->argc; index++) + free(args->argv[index]); + +error_argv_strdup: + free(args->argv); +error_argv: + free(args); + return FALSE; +} + +static int freerdp_client_command_line_post_filter(void* context, COMMAND_LINE_ARGUMENT_A* arg) +{ + rdpSettings* settings = (rdpSettings*)context; + BOOL status = TRUE; + BOOL enable = arg->Value ? TRUE : FALSE; + CommandLineSwitchStart(arg) CommandLineSwitchCase(arg, "a") + { + char** p; + size_t count; + p = CommandLineParseCommaSeparatedValues(arg->Value, &count); + + if ((status = freerdp_client_add_device_channel(settings, count, p))) + { + settings->DeviceRedirection = TRUE; + } + + free(p); + } + CommandLineSwitchCase(arg, "vc") + { + char** p; + size_t count; + p = CommandLineParseCommaSeparatedValues(arg->Value, &count); + status = freerdp_client_add_static_channel(settings, count, p); + free(p); + } + CommandLineSwitchCase(arg, "dvc") + { + char** p; + size_t count; + p = CommandLineParseCommaSeparatedValues(arg->Value, &count); + status = freerdp_client_add_dynamic_channel(settings, count, p); + free(p); + } + CommandLineSwitchCase(arg, "drive") + { + char** p; + size_t count; + p = CommandLineParseCommaSeparatedValuesEx(arg->Name, arg->Value, &count); + status = freerdp_client_add_device_channel(settings, count, p); + free(p); + } + CommandLineSwitchCase(arg, "serial") + { + char** p; + size_t count; + p = CommandLineParseCommaSeparatedValuesEx(arg->Name, arg->Value, &count); + status = freerdp_client_add_device_channel(settings, count, p); + free(p); + } + CommandLineSwitchCase(arg, "parallel") + { + char** p; + size_t count; + p = CommandLineParseCommaSeparatedValuesEx(arg->Name, arg->Value, &count); + status = freerdp_client_add_device_channel(settings, count, p); + free(p); + } + CommandLineSwitchCase(arg, "smartcard") + { + char** p; + size_t count; + p = CommandLineParseCommaSeparatedValuesEx(arg->Name, arg->Value, &count); + status = freerdp_client_add_device_channel(settings, count, p); + free(p); + } + CommandLineSwitchCase(arg, "printer") + { + char** p; + size_t count; + p = CommandLineParseCommaSeparatedValuesEx(arg->Name, arg->Value, &count); + status = freerdp_client_add_device_channel(settings, count, p); + free(p); + } + CommandLineSwitchCase(arg, "usb") + { + char** p; + size_t count; + p = CommandLineParseCommaSeparatedValuesEx(URBDRC_CHANNEL_NAME, arg->Value, &count); + status = freerdp_client_add_dynamic_channel(settings, count, p); + free(p); + } + CommandLineSwitchCase(arg, "multitouch") + { + settings->MultiTouchInput = enable; + } + CommandLineSwitchCase(arg, "gestures") + { + settings->MultiTouchGestures = enable; + } + CommandLineSwitchCase(arg, "echo") + { + settings->SupportEchoChannel = enable; + } + CommandLineSwitchCase(arg, "ssh-agent") + { + settings->SupportSSHAgentChannel = enable; + } + CommandLineSwitchCase(arg, "disp") + { + settings->SupportDisplayControl = enable; + } + CommandLineSwitchCase(arg, "geometry") + { + settings->SupportGeometryTracking = enable; + } + CommandLineSwitchCase(arg, "video") + { + settings->SupportGeometryTracking = enable; /* this requires geometry tracking */ + settings->SupportVideoOptimized = enable; + } + CommandLineSwitchCase(arg, "sound") + { + char** p; + size_t count; + p = CommandLineParseCommaSeparatedValuesEx("rdpsnd", arg->Value, &count); + status = freerdp_client_add_static_channel(settings, count, p); + if (status) + { + status = freerdp_client_add_dynamic_channel(settings, count, p); + } + free(p); + } + CommandLineSwitchCase(arg, "microphone") + { + char** p; + size_t count; + p = CommandLineParseCommaSeparatedValuesEx("audin", arg->Value, &count); + status = freerdp_client_add_dynamic_channel(settings, count, p); + free(p); + } +#if defined(CHANNEL_TSMF_CLIENT) + CommandLineSwitchCase(arg, "multimedia") + { + char** p; + size_t count; + p = CommandLineParseCommaSeparatedValuesEx("tsmf", arg->Value, &count); + status = freerdp_client_add_dynamic_channel(settings, count, p); + free(p); + } +#endif + CommandLineSwitchCase(arg, "heartbeat") + { + settings->SupportHeartbeatPdu = enable; + } + CommandLineSwitchCase(arg, "multitransport") + { + settings->SupportMultitransport = enable; + + if (settings->SupportMultitransport) + settings->MultitransportFlags = + (TRANSPORT_TYPE_UDP_FECR | TRANSPORT_TYPE_UDP_FECL | TRANSPORT_TYPE_UDP_PREFERRED); + else + settings->MultitransportFlags = 0; + } + CommandLineSwitchCase(arg, "password-is-pin") + { + settings->PasswordIsSmartcardPin = enable; + } + CommandLineSwitchEnd(arg) return status ? 1 : -1; +} + +BOOL freerdp_parse_username(const char* username, char** user, char** domain) +{ + char* p; + size_t length = 0; + p = strchr(username, '\\'); + *user = NULL; + *domain = NULL; + + if (p) + { + length = (size_t)(p - username); + *user = _strdup(&p[1]); + + if (!*user) + return FALSE; + + *domain = (char*)calloc(length + 1UL, sizeof(char)); + + if (!*domain) + { + free(*user); + *user = NULL; + return FALSE; + } + + strncpy(*domain, username, length); + (*domain)[length] = '\0'; + } + else if (username) + { + /* Do not break up the name for '@'; both credSSP and the + * ClientInfo PDU expect 'user@corp.net' to be transmitted + * as username 'user@corp.net', domain empty (not NULL!). + */ + *user = _strdup(username); + + if (!*user) + return FALSE; + + *domain = _strdup("\0"); + + if (!*domain) + { + free(*user); + *user = NULL; + return FALSE; + } + } + else + return FALSE; + + return TRUE; +} + +BOOL freerdp_parse_hostname(const char* hostname, char** host, int* port) +{ + char* p; + p = strrchr(hostname, ':'); + + if (p) + { + size_t length = (size_t)(p - hostname); + LONGLONG val; + + if (!value_to_int(p + 1, &val, 1, UINT16_MAX)) + return FALSE; + + *host = (char*)calloc(length + 1UL, sizeof(char)); + + if (!(*host)) + return FALSE; + + CopyMemory(*host, hostname, length); + (*host)[length] = '\0'; + *port = (UINT16)val; + } + else + { + *host = _strdup(hostname); + + if (!(*host)) + return FALSE; + + *port = -1; + } + + return TRUE; +} + +BOOL freerdp_set_connection_type(rdpSettings* settings, UINT32 type) +{ + settings->ConnectionType = type; + + if (type == CONNECTION_TYPE_MODEM) + { + settings->DisableWallpaper = TRUE; + settings->AllowFontSmoothing = FALSE; + settings->AllowDesktopComposition = FALSE; + settings->DisableFullWindowDrag = TRUE; + settings->DisableMenuAnims = TRUE; + settings->DisableThemes = TRUE; + } + else if (type == CONNECTION_TYPE_BROADBAND_LOW) + { + settings->DisableWallpaper = TRUE; + settings->AllowFontSmoothing = FALSE; + settings->AllowDesktopComposition = FALSE; + settings->DisableFullWindowDrag = TRUE; + settings->DisableMenuAnims = TRUE; + settings->DisableThemes = FALSE; + } + else if (type == CONNECTION_TYPE_SATELLITE) + { + settings->DisableWallpaper = TRUE; + settings->AllowFontSmoothing = FALSE; + settings->AllowDesktopComposition = TRUE; + settings->DisableFullWindowDrag = TRUE; + settings->DisableMenuAnims = TRUE; + settings->DisableThemes = FALSE; + } + else if (type == CONNECTION_TYPE_BROADBAND_HIGH) + { + settings->DisableWallpaper = TRUE; + settings->AllowFontSmoothing = FALSE; + settings->AllowDesktopComposition = TRUE; + settings->DisableFullWindowDrag = TRUE; + settings->DisableMenuAnims = TRUE; + settings->DisableThemes = FALSE; + } + else if (type == CONNECTION_TYPE_WAN) + { + settings->DisableWallpaper = FALSE; + settings->AllowFontSmoothing = TRUE; + settings->AllowDesktopComposition = TRUE; + settings->DisableFullWindowDrag = FALSE; + settings->DisableMenuAnims = FALSE; + settings->DisableThemes = FALSE; + } + else if (type == CONNECTION_TYPE_LAN) + { + settings->DisableWallpaper = FALSE; + settings->AllowFontSmoothing = TRUE; + settings->AllowDesktopComposition = TRUE; + settings->DisableFullWindowDrag = FALSE; + settings->DisableMenuAnims = FALSE; + settings->DisableThemes = FALSE; + } + else if (type == CONNECTION_TYPE_AUTODETECT) + { + settings->DisableWallpaper = FALSE; + settings->AllowFontSmoothing = TRUE; + settings->AllowDesktopComposition = TRUE; + settings->DisableFullWindowDrag = FALSE; + settings->DisableMenuAnims = FALSE; + settings->DisableThemes = FALSE; + settings->NetworkAutoDetect = TRUE; + + /* Automatically activate GFX and RFX codec support */ +#ifdef WITH_GFX_H264 + settings->GfxAVC444 = TRUE; + settings->GfxH264 = TRUE; +#endif + settings->RemoteFxCodec = TRUE; + settings->SupportGraphicsPipeline = TRUE; + } + else + { + return FALSE; + } + + return TRUE; +} + +static int freerdp_map_keyboard_layout_name_to_id(char* name) +{ + int i; + int id = 0; + RDP_KEYBOARD_LAYOUT* layouts; + layouts = freerdp_keyboard_get_layouts(RDP_KEYBOARD_LAYOUT_TYPE_STANDARD); + + if (!layouts) + return -1; + + for (i = 0; layouts[i].code; i++) + { + if (_stricmp(layouts[i].name, name) == 0) + id = (int)layouts[i].code; + } + + freerdp_keyboard_layouts_free(layouts); + + if (id) + return id; + + layouts = freerdp_keyboard_get_layouts(RDP_KEYBOARD_LAYOUT_TYPE_VARIANT); + + if (!layouts) + return -1; + + for (i = 0; layouts[i].code; i++) + { + if (_stricmp(layouts[i].name, name) == 0) + id = (int)layouts[i].code; + } + + freerdp_keyboard_layouts_free(layouts); + + if (id) + return id; + + layouts = freerdp_keyboard_get_layouts(RDP_KEYBOARD_LAYOUT_TYPE_IME); + + if (!layouts) + return -1; + + for (i = 0; layouts[i].code; i++) + { + if (_stricmp(layouts[i].name, name) == 0) + id = (int)layouts[i].code; + } + + freerdp_keyboard_layouts_free(layouts); + + if (id) + return id; + + return 0; +} + +static int freerdp_detect_command_line_pre_filter(void* context, int index, int argc, LPSTR* argv) +{ + size_t length; + WINPR_UNUSED(context); + + if (index == 1) + { + if (argc < index) + return -1; + + length = strlen(argv[index]); + + if (length > 4) + { + if (_stricmp(&(argv[index])[length - 4], ".rdp") == 0) + { + return 1; + } + } + + if (length > 13) + { + if (_stricmp(&(argv[index])[length - 13], ".msrcIncident") == 0) + { + return 1; + } + } + } + + return 0; +} + +static int freerdp_detect_windows_style_command_line_syntax(int argc, char** argv, size_t* count, + BOOL ignoreUnknown) +{ + int status; + DWORD flags; + int detect_status; + COMMAND_LINE_ARGUMENT_A* arg; + COMMAND_LINE_ARGUMENT_A largs[ARRAYSIZE(args)]; + memcpy(largs, args, sizeof(args)); + + flags = COMMAND_LINE_SEPARATOR_COLON | COMMAND_LINE_SILENCE_PARSER; + flags |= COMMAND_LINE_SIGIL_SLASH | COMMAND_LINE_SIGIL_PLUS_MINUS; + + if (ignoreUnknown) + { + flags |= COMMAND_LINE_IGN_UNKNOWN_KEYWORD; + } + + *count = 0; + detect_status = 0; + CommandLineClearArgumentsA(largs); + status = CommandLineParseArgumentsA(argc, argv, largs, flags, NULL, + freerdp_detect_command_line_pre_filter, NULL); + + if (status < 0) + return status; + + arg = largs; + + do + { + if (!(arg->Flags & COMMAND_LINE_ARGUMENT_PRESENT)) + continue; + + (*count)++; + } while ((arg = CommandLineFindNextArgumentA(arg)) != NULL); + + if ((status <= COMMAND_LINE_ERROR) && (status >= COMMAND_LINE_ERROR_LAST)) + detect_status = -1; + + return detect_status; +} + +static int freerdp_detect_posix_style_command_line_syntax(int argc, char** argv, size_t* count, + BOOL ignoreUnknown) +{ + int status; + DWORD flags; + int detect_status; + COMMAND_LINE_ARGUMENT_A* arg; + COMMAND_LINE_ARGUMENT_A largs[ARRAYSIZE(args)]; + memcpy(largs, args, sizeof(args)); + + flags = COMMAND_LINE_SEPARATOR_SPACE | COMMAND_LINE_SILENCE_PARSER; + flags |= COMMAND_LINE_SIGIL_DASH | COMMAND_LINE_SIGIL_DOUBLE_DASH; + flags |= COMMAND_LINE_SIGIL_ENABLE_DISABLE; + + if (ignoreUnknown) + { + flags |= COMMAND_LINE_IGN_UNKNOWN_KEYWORD; + } + + *count = 0; + detect_status = 0; + CommandLineClearArgumentsA(largs); + status = CommandLineParseArgumentsA(argc, argv, largs, flags, NULL, + freerdp_detect_command_line_pre_filter, NULL); + + if (status < 0) + return status; + + arg = largs; + + do + { + if (!(arg->Flags & COMMAND_LINE_ARGUMENT_PRESENT)) + continue; + + (*count)++; + } while ((arg = CommandLineFindNextArgumentA(arg)) != NULL); + + if ((status <= COMMAND_LINE_ERROR) && (status >= COMMAND_LINE_ERROR_LAST)) + detect_status = -1; + + return detect_status; +} + +static BOOL freerdp_client_detect_command_line(int argc, char** argv, DWORD* flags) +{ +#if !defined(DEFINE_NO_DEPRECATED) + int old_cli_status; + size_t old_cli_count; +#endif + int posix_cli_status; + size_t posix_cli_count; + int windows_cli_status; + size_t windows_cli_count; + BOOL compatibility = FALSE; + const BOOL ignoreUnknown = TRUE; + windows_cli_status = freerdp_detect_windows_style_command_line_syntax( + argc, argv, &windows_cli_count, ignoreUnknown); + posix_cli_status = + freerdp_detect_posix_style_command_line_syntax(argc, argv, &posix_cli_count, ignoreUnknown); +#if !defined(DEFINE_NO_DEPRECATED) + old_cli_status = freerdp_detect_old_command_line_syntax(argc, argv, &old_cli_count); +#endif + + /* Default is POSIX syntax */ + *flags = COMMAND_LINE_SEPARATOR_SPACE; + *flags |= COMMAND_LINE_SIGIL_DASH | COMMAND_LINE_SIGIL_DOUBLE_DASH; + *flags |= COMMAND_LINE_SIGIL_ENABLE_DISABLE; + + if (posix_cli_status <= COMMAND_LINE_STATUS_PRINT) + return compatibility; + + /* Check, if this may be windows style syntax... */ + if ((windows_cli_count && (windows_cli_count >= posix_cli_count)) || + (windows_cli_status <= COMMAND_LINE_STATUS_PRINT)) + { + windows_cli_count = 1; + *flags = COMMAND_LINE_SEPARATOR_COLON; + *flags |= COMMAND_LINE_SIGIL_SLASH | COMMAND_LINE_SIGIL_PLUS_MINUS; + } +#if !defined(DEFINE_NO_DEPRECATED) + else if (old_cli_status >= 0) + { + /* Ignore legacy parsing in case there is an error in the command line. */ + if ((old_cli_status == 1) || ((old_cli_count > posix_cli_count) && (old_cli_status != -1))) + { + *flags = COMMAND_LINE_SEPARATOR_SPACE; + *flags |= COMMAND_LINE_SIGIL_DASH | COMMAND_LINE_SIGIL_DOUBLE_DASH; + compatibility = TRUE; + } + } + WLog_DBG(TAG, "windows: %d/%d posix: %d/%d compat: %d/%d", windows_cli_status, + windows_cli_count, posix_cli_status, posix_cli_count, old_cli_status, old_cli_count); +#else + WLog_DBG(TAG, "windows: %d/%d posix: %d/%d", windows_cli_status, windows_cli_count, + posix_cli_status, posix_cli_count); +#endif + + return compatibility; +} + +int freerdp_client_settings_command_line_status_print(rdpSettings* settings, int status, int argc, + char** argv) +{ + return freerdp_client_settings_command_line_status_print_ex(settings, status, argc, argv, NULL); +} + +int freerdp_client_settings_command_line_status_print_ex(rdpSettings* settings, int status, + int argc, char** argv, + COMMAND_LINE_ARGUMENT_A* custom) +{ + COMMAND_LINE_ARGUMENT_A* arg; + COMMAND_LINE_ARGUMENT_A largs[ARRAYSIZE(args)]; + memcpy(largs, args, sizeof(args)); + + if (status == COMMAND_LINE_STATUS_PRINT_VERSION) + { + freerdp_client_print_version(); + goto out; + } + + if (status == COMMAND_LINE_STATUS_PRINT_BUILDCONFIG) + { + freerdp_client_print_version(); + freerdp_client_print_buildconfig(); + goto out; + } + else if (status == COMMAND_LINE_STATUS_PRINT) + { + COMMAND_LINE_ARGUMENT_A largs[ARRAYSIZE(args)]; + memcpy(largs, args, sizeof(largs)); + CommandLineParseArgumentsA(argc, argv, largs, 0x112, NULL, NULL, NULL); + + arg = CommandLineFindArgumentA(largs, "kbd-lang-list"); + + if (arg->Flags & COMMAND_LINE_ARGUMENT_PRESENT) + { + freerdp_client_print_codepages(arg->Value); + } + + arg = CommandLineFindArgumentA(largs, "kbd-list"); + + if (arg->Flags & COMMAND_LINE_VALUE_PRESENT) + { + DWORD i; + RDP_KEYBOARD_LAYOUT* layouts; + layouts = freerdp_keyboard_get_layouts(RDP_KEYBOARD_LAYOUT_TYPE_STANDARD); + // if (!layouts) /* FIXME*/ + printf("\nKeyboard Layouts\n"); + + for (i = 0; layouts[i].code; i++) + printf("0x%08" PRIX32 "\t%s\n", layouts[i].code, layouts[i].name); + + freerdp_keyboard_layouts_free(layouts); + layouts = freerdp_keyboard_get_layouts(RDP_KEYBOARD_LAYOUT_TYPE_VARIANT); + // if (!layouts) /* FIXME*/ + printf("\nKeyboard Layout Variants\n"); + + for (i = 0; layouts[i].code; i++) + printf("0x%08" PRIX32 "\t%s\n", layouts[i].code, layouts[i].name); + + freerdp_keyboard_layouts_free(layouts); + layouts = freerdp_keyboard_get_layouts(RDP_KEYBOARD_LAYOUT_TYPE_IME); + // if (!layouts) /* FIXME*/ + printf("\nKeyboard Input Method Editors (IMEs)\n"); + + for (i = 0; layouts[i].code; i++) + printf("0x%08" PRIX32 "\t%s\n", layouts[i].code, layouts[i].name); + + freerdp_keyboard_layouts_free(layouts); + printf("\n"); + } + + arg = CommandLineFindArgumentA(largs, "monitor-list"); + + if (arg->Flags & COMMAND_LINE_VALUE_PRESENT) + { + settings->ListMonitors = TRUE; + } + + goto out; + } + else if (status < 0) + { + freerdp_client_print_command_line_help_ex(argc, argv, custom); + goto out; + } + +out: + if (status <= COMMAND_LINE_STATUS_PRINT && status >= COMMAND_LINE_STATUS_PRINT_LAST) + return 0; + return status; +} + +static BOOL ends_with(const char* str, const char* ext) +{ + const size_t strLen = strlen(str); + const size_t extLen = strlen(ext); + + if (strLen < extLen) + return FALSE; + + return _strnicmp(&str[strLen - extLen], ext, extLen) == 0; +} + +static void activate_smartcard_logon_rdp(rdpSettings* settings) +{ + settings->SmartcardLogon = TRUE; + /* TODO: why not? settings->UseRdpSecurityLayer = TRUE; */ + freerdp_settings_set_bool(settings, FreeRDP_PasswordIsSmartcardPin, TRUE); +} + +/** + * parses a string value with the format x + * @param input: input string + * @param v1: pointer to output v1 + * @param v2: pointer to output v2 + * @return if the parsing was successful + */ +static BOOL parseSizeValue(const char* input, unsigned long* v1, unsigned long* v2) +{ + const char* xcharpos; + char* endPtr; + unsigned long v; + errno = 0; + v = strtoul(input, &endPtr, 10); + + if ((v == 0 || v == ULONG_MAX) && (errno != 0)) + return FALSE; + + if (v1) + *v1 = v; + + xcharpos = strchr(input, 'x'); + + if (!xcharpos || xcharpos != endPtr) + return FALSE; + + errno = 0; + v = strtoul(xcharpos + 1, &endPtr, 10); + + if ((v == 0 || v == ULONG_MAX) && (errno != 0)) + return FALSE; + + if (*endPtr != '\0') + return FALSE; + + if (v2) + *v2 = v; + + return TRUE; +} + +static BOOL prepare_default_settings(rdpSettings* settings, const COMMAND_LINE_ARGUMENT_A* args, + BOOL rdp_file) +{ + size_t x; + const char* arguments[] = { "network", "gfx", "rfx", "bpp" }; + WINPR_ASSERT(settings); + WINPR_ASSERT(args); + + if (rdp_file) + return FALSE; + + for (x = 0; x < ARRAYSIZE(arguments); x++) + { + const char* arg = arguments[x]; + COMMAND_LINE_ARGUMENT_A* p = CommandLineFindArgumentA(args, arg); + if (p && (p->Flags & COMMAND_LINE_ARGUMENT_PRESENT)) + return FALSE; + } + + return freerdp_set_connection_type(settings, CONNECTION_TYPE_AUTODETECT); +} + +int freerdp_client_settings_parse_command_line_arguments(rdpSettings* settings, int argc, + char** argv, BOOL allowUnknown) +{ + char* p; + char* user = NULL; + char* gwUser = NULL; + char* str; + size_t length; + int status; + BOOL ext = FALSE; + BOOL assist = FALSE; + DWORD flags = 0; + BOOL promptForPassword = FALSE; + BOOL compatibility = FALSE; + COMMAND_LINE_ARGUMENT_A* arg; + COMMAND_LINE_ARGUMENT_A largs[ARRAYSIZE(args)]; + memcpy(largs, args, sizeof(args)); + + /* Command line detection fails if only a .rdp or .msrcIncident file + * is supplied. Check this case first, only then try to detect + * legacy command line syntax. */ + if (argc > 1) + { + ext = ends_with(argv[1], ".rdp"); + assist = ends_with(argv[1], ".msrcIncident"); + } + + if (!ext && !assist) + compatibility = freerdp_client_detect_command_line(argc, argv, &flags); + else + compatibility = freerdp_client_detect_command_line(argc - 1, &argv[1], &flags); + + freerdp_settings_set_string(settings, FreeRDP_ProxyHostname, NULL); + freerdp_settings_set_string(settings, FreeRDP_ProxyUsername, NULL); + freerdp_settings_set_string(settings, FreeRDP_ProxyPassword, NULL); + +#if !defined(DEFINE_NO_DEPRECATED) + if (compatibility) + { + WLog_WARN(TAG, "----------------------------------------"); + WLog_WARN(TAG, "Using deprecated command-line interface!"); + WLog_WARN(TAG, "This will be removed with FreeRDP 3!"); + WLog_WARN(TAG, "----------------------------------------"); + return freerdp_client_parse_old_command_line_arguments(argc, argv, settings); + } + else +#endif + { + if (allowUnknown) + flags |= COMMAND_LINE_IGN_UNKNOWN_KEYWORD; + + if (ext) + { + if (freerdp_client_settings_parse_connection_file(settings, argv[1])) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + } + + if (assist) + { + if (freerdp_client_settings_parse_assistance_file(settings, argc, argv) < 0) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + } + + CommandLineClearArgumentsA(largs); + status = CommandLineParseArgumentsA(argc, argv, largs, flags, settings, + freerdp_client_command_line_pre_filter, + freerdp_client_command_line_post_filter); + + if (status < 0) + return status; + + prepare_default_settings(settings, largs, ext); + } + + CommandLineFindArgumentA(largs, "v"); + arg = largs; + errno = 0; + + do + { + BOOL enable = arg->Value ? TRUE : FALSE; + + if (!(arg->Flags & COMMAND_LINE_ARGUMENT_PRESENT)) + continue; + + CommandLineSwitchStart(arg) CommandLineSwitchCase(arg, "v") + { + if (!arg->Value) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + free(settings->ServerHostname); + settings->ServerHostname = NULL; + p = strchr(arg->Value, '['); + + /* ipv4 */ + if (!p) + { + p = strchr(arg->Value, ':'); + + if (p) + { + LONGLONG val; + + if (!value_to_int(&p[1], &val, 1, UINT16_MAX)) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + + length = (size_t)(p - arg->Value); + settings->ServerPort = (UINT16)val; + + if (!(settings->ServerHostname = (char*)calloc(length + 1UL, sizeof(char)))) + return COMMAND_LINE_ERROR_MEMORY; + + strncpy(settings->ServerHostname, arg->Value, length); + settings->ServerHostname[length] = '\0'; + } + else + { + if (!(settings->ServerHostname = _strdup(arg->Value))) + return COMMAND_LINE_ERROR_MEMORY; + } + } + else /* ipv6 */ + { + char* p2 = strchr(arg->Value, ']'); + + /* not a valid [] ipv6 addr found */ + if (!p2) + continue; + + length = (size_t)(p2 - p); + + if (!(settings->ServerHostname = (char*)calloc(length, sizeof(char)))) + return COMMAND_LINE_ERROR_MEMORY; + + strncpy(settings->ServerHostname, p + 1, length - 1); + + if (*(p2 + 1) == ':') + { + LONGLONG val; + + if (!value_to_int(&p[2], &val, 0, UINT16_MAX)) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + + settings->ServerPort = (UINT16)val; + } + + printf("hostname %s port %" PRIu32 "\n", settings->ServerHostname, + settings->ServerPort); + } + } + CommandLineSwitchCase(arg, "spn-class") + { + if (!copy_value(arg->Value, &settings->AuthenticationServiceClass)) + return COMMAND_LINE_ERROR_MEMORY; + } + CommandLineSwitchCase(arg, "redirect-prefer") + { + size_t count = 0; + char* cur = arg->Value; + if (!arg->Value) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + settings->RedirectionPreferType = 0; + + do + { + UINT32 mask; + char* next = strchr(cur, ','); + + if (next) + { + *next = '\0'; + next++; + } + + if (_strnicmp(cur, "fqdn", 5) == 0) + mask = 0x06U; + else if (_strnicmp(cur, "ip", 3) == 0) + mask = 0x05U; + else if (_strnicmp(cur, "netbios", 8) == 0) + mask = 0x03U; + else + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + + cur = next; + mask = (mask & 0x07); + settings->RedirectionPreferType |= mask << (count * 3); + count++; + } while (cur != NULL); + + if (count > 3) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + } + CommandLineSwitchCase(arg, "credentials-delegation") + { + settings->DisableCredentialsDelegation = !enable; + } + CommandLineSwitchCase(arg, "vmconnect") + { + settings->VmConnectMode = TRUE; + settings->ServerPort = 2179; + settings->NegotiateSecurityLayer = FALSE; + + if (arg->Flags & COMMAND_LINE_VALUE_PRESENT) + { + settings->SendPreconnectionPdu = TRUE; + + if (!copy_value(arg->Value, &settings->PreconnectionBlob)) + return COMMAND_LINE_ERROR_MEMORY; + } + } + CommandLineSwitchCase(arg, "w") + { + LONGLONG val; + + if (!value_to_int(arg->Value, &val, -1, UINT32_MAX)) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + + settings->DesktopWidth = (UINT32)val; + } + CommandLineSwitchCase(arg, "h") + { + LONGLONG val; + + if (!value_to_int(arg->Value, &val, -1, UINT32_MAX)) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + + settings->DesktopHeight = (UINT32)val; + } + CommandLineSwitchCase(arg, "size") + { + if (!arg->Value) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + p = strchr(arg->Value, 'x'); + + if (p) + { + unsigned long w, h; + + if (!parseSizeValue(arg->Value, &w, &h) || (w > UINT16_MAX) || (h > UINT16_MAX)) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + + settings->DesktopWidth = (UINT32)w; + settings->DesktopHeight = (UINT32)h; + } + else + { + if (!(str = _strdup(arg->Value))) + return COMMAND_LINE_ERROR_MEMORY; + + p = strchr(str, '%'); + + if (p) + { + BOOL partial = FALSE; + + if (strchr(p, 'w')) + { + settings->PercentScreenUseWidth = 1; + partial = TRUE; + } + + if (strchr(p, 'h')) + { + settings->PercentScreenUseHeight = 1; + partial = TRUE; + } + + if (!partial) + { + settings->PercentScreenUseWidth = 1; + settings->PercentScreenUseHeight = 1; + } + + *p = '\0'; + { + LONGLONG val; + + if (!value_to_int(str, &val, 0, 100)) + { + free(str); + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + } + + settings->PercentScreen = (UINT32)val; + } + } + + free(str); + } + } + CommandLineSwitchCase(arg, "f") + { + settings->Fullscreen = enable; + } + CommandLineSwitchCase(arg, "suppress-output") + { + settings->SuppressOutput = enable; + } + CommandLineSwitchCase(arg, "multimon") + { + settings->UseMultimon = TRUE; + + if (arg->Flags & COMMAND_LINE_VALUE_PRESENT) + { + if (_stricmp(arg->Value, "force") == 0) + { + settings->ForceMultimon = TRUE; + } + } + } + CommandLineSwitchCase(arg, "span") + { + settings->SpanMonitors = enable; + } + CommandLineSwitchCase(arg, "workarea") + { + settings->Workarea = enable; + } + CommandLineSwitchCase(arg, "monitors") + { + if (arg->Flags & COMMAND_LINE_VALUE_PRESENT) + { + UINT32 i; + char** p; + size_t count = 0; + p = CommandLineParseCommaSeparatedValues(arg->Value, &count); + + if (!p) + return COMMAND_LINE_ERROR_MEMORY; + + if (count > 16) + count = 16; + + settings->NumMonitorIds = (UINT32)count; + + for (i = 0; i < settings->NumMonitorIds; i++) + { + LONGLONG val; + + if (!value_to_int(p[i], &val, 0, UINT16_MAX)) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + + settings->MonitorIds[i] = (UINT32)val; + } + + free(p); + } + } + CommandLineSwitchCase(arg, "monitor-list") + { + settings->ListMonitors = enable; + } + CommandLineSwitchCase(arg, "t") + { + if (!copy_value(arg->Value, &settings->WindowTitle)) + return COMMAND_LINE_ERROR_MEMORY; + } + CommandLineSwitchCase(arg, "decorations") + { + settings->Decorations = enable; + } + CommandLineSwitchCase(arg, "dynamic-resolution") + { + if (settings->SmartSizing) + { + WLog_ERR(TAG, "Smart sizing and dynamic resolution are mutually exclusive options"); + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + } + + settings->SupportDisplayControl = TRUE; + settings->DynamicResolutionUpdate = TRUE; + } + CommandLineSwitchCase(arg, "smart-sizing") + { + if (settings->DynamicResolutionUpdate) + { + WLog_ERR(TAG, "Smart sizing and dynamic resolution are mutually exclusive options"); + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + } + + settings->SmartSizing = TRUE; + + if (arg->Value) + { + unsigned long w, h; + + if (!parseSizeValue(arg->Value, &w, &h) || (w > UINT16_MAX) || (h > UINT16_MAX)) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + + settings->SmartSizingWidth = (UINT32)w; + settings->SmartSizingHeight = (UINT32)h; + } + } + CommandLineSwitchCase(arg, "bpp") + { + LONGLONG val; + + if (!value_to_int(arg->Value, &val, 0, UINT32_MAX)) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + + switch (settings->ColorDepth) + { + case 32: + case 24: + case 16: + case 15: + case 8: + settings->ColorDepth = (UINT32)val; + break; + + default: + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + } + } + CommandLineSwitchCase(arg, "admin") + { + settings->ConsoleSession = enable; + } + CommandLineSwitchCase(arg, "relax-order-checks") + { + settings->AllowUnanouncedOrdersFromServer = enable; + } + CommandLineSwitchCase(arg, "restricted-admin") + { + settings->ConsoleSession = enable; + settings->RestrictedAdminModeRequired = enable; + } + CommandLineSwitchCase(arg, "pth") + { + settings->ConsoleSession = TRUE; + settings->RestrictedAdminModeRequired = TRUE; + + if (!copy_value(arg->Value, &settings->PasswordHash)) + return COMMAND_LINE_ERROR_MEMORY; + } + CommandLineSwitchCase(arg, "client-hostname") + { + if (!copy_value(arg->Value, &settings->ClientHostname)) + return COMMAND_LINE_ERROR_MEMORY; + } + CommandLineSwitchCase(arg, "kbd") + { + LONGLONG val; + + if (!value_to_int(arg->Value, &val, 1, UINT32_MAX)) + { + const int rc = freerdp_map_keyboard_layout_name_to_id(arg->Value); + + if (rc <= 0) + { + WLog_ERR(TAG, "Could not identify keyboard layout: %s", arg->Value); + WLog_ERR(TAG, "Use /kbd-list to list available layouts"); + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + } + + /* Found a valid mapping, reset errno */ + val = rc; + errno = 0; + } + + settings->KeyboardLayout = (UINT32)val; + } + CommandLineSwitchCase(arg, "kbd-remap") + { + if (!copy_value(arg->Value, &settings->KeyboardRemappingList)) + return COMMAND_LINE_ERROR_MEMORY; + } + CommandLineSwitchCase(arg, "kbd-lang") + { + LONGLONG val; + + if (!value_to_int(arg->Value, &val, 1, UINT32_MAX)) + { + WLog_ERR(TAG, "Could not identify keyboard active language %s", arg->Value); + WLog_ERR(TAG, "Use /kbd-lang-list to list available layouts"); + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + } + + settings->KeyboardCodePage = (UINT32)val; + } + CommandLineSwitchCase(arg, "kbd-type") + { + LONGLONG val; + + if (!value_to_int(arg->Value, &val, 0, UINT32_MAX)) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + + settings->KeyboardType = (UINT32)val; + } + CommandLineSwitchCase(arg, "kbd-subtype") + { + LONGLONG val; + + if (!value_to_int(arg->Value, &val, 0, UINT32_MAX)) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + + settings->KeyboardSubType = (UINT32)val; + } + CommandLineSwitchCase(arg, "kbd-fn-key") + { + LONGLONG val; + + if (!value_to_int(arg->Value, &val, 0, UINT32_MAX)) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + + settings->KeyboardFunctionKey = (UINT32)val; + } + CommandLineSwitchCase(arg, "u") + { + user = _strdup(arg->Value); + } + CommandLineSwitchCase(arg, "d") + { + if (!copy_value(arg->Value, &settings->Domain)) + return COMMAND_LINE_ERROR_MEMORY; + } + CommandLineSwitchCase(arg, "p") + { + if (!copy_value(arg->Value, &settings->Password)) + return COMMAND_LINE_ERROR_MEMORY; + } + CommandLineSwitchCase(arg, "g") + { + free(settings->GatewayHostname); + + if (arg->Flags & COMMAND_LINE_VALUE_PRESENT) + { + if (!arg->Value) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + p = strchr(arg->Value, ':'); + + if (p) + { + size_t s; + LONGLONG val; + + if (!value_to_int(&p[1], &val, 0, UINT32_MAX)) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + + s = (size_t)(p - arg->Value); + settings->GatewayPort = (UINT32)val; + + if (!(settings->GatewayHostname = (char*)calloc(s + 1UL, sizeof(char)))) + return COMMAND_LINE_ERROR_MEMORY; + + strncpy(settings->GatewayHostname, arg->Value, s); + settings->GatewayHostname[s] = '\0'; + } + else + { + if (!(settings->GatewayHostname = _strdup(arg->Value))) + return COMMAND_LINE_ERROR_MEMORY; + } + } + else + { + if (!(settings->GatewayHostname = _strdup(settings->ServerHostname))) + return COMMAND_LINE_ERROR_MEMORY; + } + + settings->GatewayEnabled = TRUE; + settings->GatewayUseSameCredentials = TRUE; + freerdp_set_gateway_usage_method(settings, TSC_PROXY_MODE_DIRECT); + } + CommandLineSwitchCase(arg, "proxy") + { + /* initial value */ + if (!freerdp_settings_set_uint32(settings, FreeRDP_ProxyType, PROXY_TYPE_HTTP)) + return COMMAND_LINE_ERROR_MEMORY; + + if (arg->Flags & COMMAND_LINE_VALUE_PRESENT) + { + char* atPtr; + if (!arg->Value) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + /* value is [scheme://][user:password@]hostname:port */ + p = strstr(arg->Value, "://"); + + if (p) + { + *p = '\0'; + + if (_stricmp("no_proxy", arg->Value) == 0) + { + if (!freerdp_settings_set_uint32(settings, FreeRDP_ProxyType, + PROXY_TYPE_IGNORE)) + return COMMAND_LINE_ERROR_MEMORY; + } + if (_stricmp("http", arg->Value) == 0) + { + if (!freerdp_settings_set_uint32(settings, FreeRDP_ProxyType, + PROXY_TYPE_HTTP)) + return COMMAND_LINE_ERROR_MEMORY; + } + else if (_stricmp("socks5", arg->Value) == 0) + { + if (!freerdp_settings_set_uint32(settings, FreeRDP_ProxyType, + PROXY_TYPE_SOCKS)) + return COMMAND_LINE_ERROR_MEMORY; + } + else + { + WLog_ERR(TAG, "Only HTTP and SOCKS5 proxies supported by now"); + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + } + + arg->Value = p + 3; + } + + /* arg->Value is now [user:password@]hostname:port */ + atPtr = strrchr(arg->Value, '@'); + + if (atPtr) + { + /* got a login / password, + * atPtr + * v + * [user:password@]hostname:port + * ^ + * colonPtr + */ + char* colonPtr = strchr(arg->Value, ':'); + + if (!colonPtr || (colonPtr > atPtr)) + { + WLog_ERR( + TAG, + "invalid syntax for proxy, expected syntax is user:password@host:port"); + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + } + + *colonPtr = '\0'; + if (!freerdp_settings_set_string(settings, FreeRDP_ProxyUsername, arg->Value)) + { + WLog_ERR(TAG, "unable to allocate proxy username"); + return COMMAND_LINE_ERROR_MEMORY; + } + + *atPtr = '\0'; + + if (!freerdp_settings_set_string(settings, FreeRDP_ProxyPassword, colonPtr + 1)) + { + WLog_ERR(TAG, "unable to allocate proxy password"); + return COMMAND_LINE_ERROR_MEMORY; + } + + arg->Value = atPtr + 1; + } + + p = strchr(arg->Value, ':'); + + if (p) + { + LONGLONG val; + + if (!value_to_int(&p[1], &val, 0, UINT16_MAX)) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + + length = (size_t)(p - arg->Value); + if (!freerdp_settings_set_uint16(settings, FreeRDP_ProxyPort, val)) + return FALSE; + *p = '\0'; + } + + p = strchr(arg->Value, '/'); + if (p) + *p = '\0'; + if (!freerdp_settings_set_string(settings, FreeRDP_ProxyHostname, arg->Value)) + return FALSE; + } + else + { + WLog_ERR(TAG, "Option http-proxy needs argument."); + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + } + } + CommandLineSwitchCase(arg, "gu") + { + if (!(gwUser = _strdup(arg->Value))) + return COMMAND_LINE_ERROR_MEMORY; + + settings->GatewayUseSameCredentials = FALSE; + } + CommandLineSwitchCase(arg, "gd") + { + if (!copy_value(arg->Value, &settings->GatewayDomain)) + return COMMAND_LINE_ERROR_MEMORY; + + settings->GatewayUseSameCredentials = FALSE; + } + CommandLineSwitchCase(arg, "gp") + { + if (!copy_value(arg->Value, &settings->GatewayPassword)) + return COMMAND_LINE_ERROR_MEMORY; + + settings->GatewayUseSameCredentials = FALSE; + } + CommandLineSwitchCase(arg, "gt") + { + if (_stricmp(arg->Value, "rpc") == 0) + { + if (!freerdp_settings_set_bool(settings, FreeRDP_GatewayRpcTransport, TRUE) || + !freerdp_settings_set_bool(settings, FreeRDP_GatewayHttpTransport, FALSE) || + !freerdp_settings_set_bool(settings, FreeRDP_GatewayHttpUseWebsockets, FALSE)) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + } + else + { + char* c = strchr(arg->Value, ','); + if (c) + { + *c++ = '\0'; + if (_stricmp(c, "no-websockets") != 0) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + + if (!freerdp_settings_set_bool(settings, FreeRDP_GatewayHttpUseWebsockets, + FALSE)) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + } + + if (_stricmp(arg->Value, "http") == 0) + { + if (!freerdp_settings_set_bool(settings, FreeRDP_GatewayRpcTransport, FALSE) || + !freerdp_settings_set_bool(settings, FreeRDP_GatewayHttpTransport, TRUE)) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + } + else if (_stricmp(arg->Value, "auto") == 0) + { + if (!freerdp_settings_set_bool(settings, FreeRDP_GatewayRpcTransport, TRUE) || + !freerdp_settings_set_bool(settings, FreeRDP_GatewayHttpTransport, TRUE)) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + } + } + } + CommandLineSwitchCase(arg, "gat") + { + if (!copy_value(arg->Value, &settings->GatewayAccessToken)) + return COMMAND_LINE_ERROR_MEMORY; + } + CommandLineSwitchCase(arg, "gateway-usage-method") + { + UINT32 type = 0; + + if (_stricmp(arg->Value, "none") == 0) + type = TSC_PROXY_MODE_NONE_DIRECT; + else if (_stricmp(arg->Value, "direct") == 0) + type = TSC_PROXY_MODE_DIRECT; + else if (_stricmp(arg->Value, "detect") == 0) + type = TSC_PROXY_MODE_DETECT; + else if (_stricmp(arg->Value, "default") == 0) + type = TSC_PROXY_MODE_DEFAULT; + else + { + LONGLONG val; + + if (!value_to_int(arg->Value, &val, TSC_PROXY_MODE_NONE_DIRECT, + TSC_PROXY_MODE_NONE_DETECT)) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + } + + freerdp_set_gateway_usage_method(settings, type); + } + CommandLineSwitchCase(arg, "app") + { + if (!copy_value(arg->Value, &settings->RemoteApplicationProgram)) + return COMMAND_LINE_ERROR_MEMORY; + + settings->RemoteApplicationMode = TRUE; + settings->RemoteAppLanguageBarSupported = TRUE; + settings->Workarea = TRUE; + settings->DisableWallpaper = TRUE; + settings->DisableFullWindowDrag = TRUE; + } + CommandLineSwitchCase(arg, "app-workdir") + { + if (!copy_value(arg->Value, &settings->RemoteApplicationWorkingDir)) + return COMMAND_LINE_ERROR_MEMORY; + } + CommandLineSwitchCase(arg, "load-balance-info") + { + if (!copy_value(arg->Value, (char**)&settings->LoadBalanceInfo)) + return COMMAND_LINE_ERROR_MEMORY; + + settings->LoadBalanceInfoLength = (UINT32)strlen((char*)settings->LoadBalanceInfo); + } + CommandLineSwitchCase(arg, "app-name") + { + if (!copy_value(arg->Value, &settings->RemoteApplicationName)) + return COMMAND_LINE_ERROR_MEMORY; + } + CommandLineSwitchCase(arg, "app-icon") + { + if (!copy_value(arg->Value, &settings->RemoteApplicationIcon)) + return COMMAND_LINE_ERROR_MEMORY; + } + CommandLineSwitchCase(arg, "app-cmd") + { + if (!copy_value(arg->Value, &settings->RemoteApplicationCmdLine)) + return COMMAND_LINE_ERROR_MEMORY; + } + CommandLineSwitchCase(arg, "app-file") + { + if (!copy_value(arg->Value, &settings->RemoteApplicationFile)) + return COMMAND_LINE_ERROR_MEMORY; + } + CommandLineSwitchCase(arg, "app-guid") + { + if (!copy_value(arg->Value, &settings->RemoteApplicationGuid)) + return COMMAND_LINE_ERROR_MEMORY; + } + CommandLineSwitchCase(arg, "compression") + { + settings->CompressionEnabled = enable; + } + CommandLineSwitchCase(arg, "compression-level") + { + LONGLONG val; + + if (!value_to_int(arg->Value, &val, 0, UINT32_MAX)) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + + settings->CompressionLevel = (UINT32)val; + } + CommandLineSwitchCase(arg, "drives") + { + settings->RedirectDrives = enable; + } + CommandLineSwitchCase(arg, "home-drive") + { + settings->RedirectHomeDrive = enable; + } + CommandLineSwitchCase(arg, "ipv6") + { + settings->PreferIPv6OverIPv4 = enable; + } + CommandLineSwitchCase(arg, "clipboard") + { + if (arg->Value == BoolValueTrue || arg->Value == BoolValueFalse) + { + settings->RedirectClipboard = (arg->Value == BoolValueTrue); + } + else + { + int rc = 0; + char** p; + size_t count, x; + p = CommandLineParseCommaSeparatedValues(arg->Value, &count); + for (x = 0; (x < count) && (rc == 0); x++) + { + const char usesel[14] = "use-selection:"; + + const char* cur = p[x]; + if (_strnicmp(usesel, cur, sizeof(usesel)) == 0) + { + const char* val = &cur[sizeof(usesel)]; + if (!copy_value(val, &settings->XSelectionAtom)) + rc = COMMAND_LINE_ERROR_MEMORY; + settings->RedirectClipboard = TRUE; + } + else + rc = COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + } + free(p); + + if (rc) + return rc; + } + } + CommandLineSwitchCase(arg, "shell") + { + if (!copy_value(arg->Value, &settings->AlternateShell)) + return COMMAND_LINE_ERROR_MEMORY; + } + CommandLineSwitchCase(arg, "shell-dir") + { + if (!copy_value(arg->Value, &settings->ShellWorkingDirectory)) + return COMMAND_LINE_ERROR_MEMORY; + } + CommandLineSwitchCase(arg, "audio-mode") + { + LONGLONG val; + + if (!value_to_int(arg->Value, &val, 0, UINT32_MAX)) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + + switch (val) + { + case AUDIO_MODE_REDIRECT: + settings->AudioPlayback = TRUE; + break; + + case AUDIO_MODE_PLAY_ON_SERVER: + settings->RemoteConsoleAudio = TRUE; + break; + + case AUDIO_MODE_NONE: + settings->AudioPlayback = FALSE; + settings->RemoteConsoleAudio = FALSE; + break; + + default: + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + } + } + CommandLineSwitchCase(arg, "network") + { + UINT32 type = 0; + + if (_stricmp(arg->Value, "modem") == 0) + type = CONNECTION_TYPE_MODEM; + else if (_stricmp(arg->Value, "broadband") == 0) + type = CONNECTION_TYPE_BROADBAND_HIGH; + else if (_stricmp(arg->Value, "broadband-low") == 0) + type = CONNECTION_TYPE_BROADBAND_LOW; + else if (_stricmp(arg->Value, "broadband-high") == 0) + type = CONNECTION_TYPE_BROADBAND_HIGH; + else if (_stricmp(arg->Value, "wan") == 0) + type = CONNECTION_TYPE_WAN; + else if (_stricmp(arg->Value, "lan") == 0) + type = CONNECTION_TYPE_LAN; + else if ((_stricmp(arg->Value, "autodetect") == 0) || + (_stricmp(arg->Value, "auto") == 0) || (_stricmp(arg->Value, "detect") == 0)) + { + type = CONNECTION_TYPE_AUTODETECT; + } + else + { + LONGLONG val; + + if (!value_to_int(arg->Value, &val, 1, 7)) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + + type = (UINT32)val; + } + + if (!freerdp_set_connection_type(settings, type)) + return COMMAND_LINE_ERROR; + } + CommandLineSwitchCase(arg, "fonts") + { + settings->AllowFontSmoothing = enable; + } + CommandLineSwitchCase(arg, "wallpaper") + { + settings->DisableWallpaper = !enable; + } + CommandLineSwitchCase(arg, "window-drag") + { + settings->DisableFullWindowDrag = !enable; + } + CommandLineSwitchCase(arg, "window-position") + { + unsigned long x, y; + + if (!arg->Value) + return COMMAND_LINE_ERROR_MISSING_ARGUMENT; + + if (!parseSizeValue(arg->Value, &x, &y) || x > UINT16_MAX || y > UINT16_MAX) + { + WLog_ERR(TAG, "invalid window-position argument"); + return COMMAND_LINE_ERROR_MISSING_ARGUMENT; + } + + settings->DesktopPosX = (UINT32)x; + settings->DesktopPosY = (UINT32)y; + } + CommandLineSwitchCase(arg, "menu-anims") + { + settings->DisableMenuAnims = !enable; + } + CommandLineSwitchCase(arg, "themes") + { + settings->DisableThemes = !enable; + } + CommandLineSwitchCase(arg, "timeout") + { + ULONGLONG val; + if (!value_to_uint(arg->Value, &val, 1, 600000)) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + settings->TcpAckTimeout = (UINT32)val; + } + CommandLineSwitchCase(arg, "aero") + { + settings->AllowDesktopComposition = enable; + } + CommandLineSwitchCase(arg, "gdi") + { + if (_stricmp(arg->Value, "sw") == 0) + settings->SoftwareGdi = TRUE; + else if (_stricmp(arg->Value, "hw") == 0) + settings->SoftwareGdi = FALSE; + } + CommandLineSwitchCase(arg, "gfx") + { + settings->SupportGraphicsPipeline = TRUE; + + if (arg->Value) + { + int rc = CHANNEL_RC_OK; + char** p; + size_t count, x; + + p = CommandLineParseCommaSeparatedValues(arg->Value, &count); + if (!p || (count == 0)) + rc = COMMAND_LINE_ERROR; + else + { + for (x = 0; x < count; x++) + { + const char* val = p[x]; +#ifdef WITH_GFX_H264 + if (_strnicmp("AVC444", val, 7) == 0) + { + settings->GfxH264 = TRUE; + settings->GfxAVC444 = TRUE; + } + else if (_strnicmp("AVC420", val, 7) == 0) + { + settings->GfxH264 = TRUE; + settings->GfxAVC444 = FALSE; + } + else +#endif + if (_strnicmp("RFX", val, 4) == 0) + { + settings->GfxAVC444 = FALSE; + settings->GfxH264 = FALSE; + settings->RemoteFxCodec = TRUE; + } + else if (_strnicmp("mask:", val, 5) == 0) + { + ULONGLONG v; + const char* uv = &val[5]; + if (!value_to_uint(uv, &v, 0, UINT32_MAX)) + rc = COMMAND_LINE_ERROR; + else + settings->GfxCapsFilter = (UINT32)v; + } + else + rc = COMMAND_LINE_ERROR; + } + } + free(p); + if (rc != CHANNEL_RC_OK) + return rc; + } + } + CommandLineSwitchCase(arg, "gfx-thin-client") + { + settings->GfxThinClient = enable; + + if (settings->GfxThinClient) + settings->GfxSmallCache = TRUE; + + settings->SupportGraphicsPipeline = TRUE; + } + CommandLineSwitchCase(arg, "gfx-small-cache") + { + settings->GfxSmallCache = enable; + + if (enable) + settings->SupportGraphicsPipeline = TRUE; + } + CommandLineSwitchCase(arg, "gfx-progressive") + { + settings->GfxProgressive = enable; + settings->GfxThinClient = !enable; + + if (enable) + settings->SupportGraphicsPipeline = TRUE; + } +#if !defined(DEFINE_NO_DEPRECATED) +#ifdef WITH_GFX_H264 + CommandLineSwitchCase(arg, "gfx-h264") + { + settings->SupportGraphicsPipeline = TRUE; + settings->GfxH264 = TRUE; + + if (arg->Value) + { + int rc = CHANNEL_RC_OK; + char** p; + size_t count, x; + + p = CommandLineParseCommaSeparatedValues(arg->Value, &count); + if (!p || (count == 0)) + rc = COMMAND_LINE_ERROR; + else + { + for (x = 0; x < count; x++) + { + const char* val = p[x]; + + if (_strnicmp("AVC444", val, 7) == 0) + { + settings->GfxH264 = TRUE; + settings->GfxAVC444 = TRUE; + } + else if (_strnicmp("AVC420", val, 7) == 0) + { + settings->GfxH264 = TRUE; + settings->GfxAVC444 = FALSE; + } + else if (_strnicmp("mask:", val, 5) == 0) + { + ULONGLONG v; + const char* uv = &val[5]; + if (!value_to_uint(uv, &v, 0, UINT32_MAX)) + rc = COMMAND_LINE_ERROR; + else + settings->GfxCapsFilter = (UINT32)v; + } + else + rc = COMMAND_LINE_ERROR; + } + } + free(p); + if (rc != CHANNEL_RC_OK) + return rc; + } + } +#endif +#endif + CommandLineSwitchCase(arg, "rfx") + { + settings->RemoteFxCodec = enable; + } + CommandLineSwitchCase(arg, "rfx-mode") + { + if (!arg->Value) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + + if (strcmp(arg->Value, "video") == 0) + settings->RemoteFxCodecMode = 0x00; + else if (strcmp(arg->Value, "image") == 0) + settings->RemoteFxCodecMode = 0x02; + } + CommandLineSwitchCase(arg, "frame-ack") + { + LONGLONG val; + + if (!value_to_int(arg->Value, &val, 0, UINT32_MAX)) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + + settings->FrameAcknowledge = (UINT32)val; + } + CommandLineSwitchCase(arg, "nsc") + { + settings->NSCodec = enable; + } +#if defined(WITH_JPEG) + CommandLineSwitchCase(arg, "jpeg") + { + settings->JpegCodec = enable; + settings->JpegQuality = 75; + } + CommandLineSwitchCase(arg, "jpeg-quality") + { + LONGLONG val; + + if (!value_to_int(arg->Value, &val, 0, 100)) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + + settings->JpegQuality = (UINT32)val; + } +#endif + CommandLineSwitchCase(arg, "nego") + { + settings->NegotiateSecurityLayer = enable; + } + CommandLineSwitchCase(arg, "pcb") + { + settings->SendPreconnectionPdu = TRUE; + + if (!copy_value(arg->Value, &settings->PreconnectionBlob)) + return COMMAND_LINE_ERROR_MEMORY; + } + CommandLineSwitchCase(arg, "pcid") + { + LONGLONG val; + + if (!value_to_int(arg->Value, &val, 0, UINT32_MAX)) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + + settings->SendPreconnectionPdu = TRUE; + settings->PreconnectionId = (UINT32)val; + } + CommandLineSwitchCase(arg, "sec") + { + if (!arg->Value) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + + if (strcmp("rdp", arg->Value) == 0) /* Standard RDP */ + { + settings->RdpSecurity = TRUE; + settings->TlsSecurity = FALSE; + settings->NlaSecurity = FALSE; + settings->ExtSecurity = FALSE; + settings->UseRdpSecurityLayer = TRUE; + } + else if (strcmp("tls", arg->Value) == 0) /* TLS */ + { + settings->RdpSecurity = FALSE; + settings->TlsSecurity = TRUE; + settings->NlaSecurity = FALSE; + settings->ExtSecurity = FALSE; + } + else if (strcmp("nla", arg->Value) == 0) /* NLA */ + { + settings->RdpSecurity = FALSE; + settings->TlsSecurity = FALSE; + settings->NlaSecurity = TRUE; + settings->ExtSecurity = FALSE; + } + else if (strcmp("ext", arg->Value) == 0) /* NLA Extended */ + { + settings->RdpSecurity = FALSE; + settings->TlsSecurity = FALSE; + settings->NlaSecurity = FALSE; + settings->ExtSecurity = TRUE; + } + else + { + WLog_ERR(TAG, "unknown protocol security: %s", arg->Value); + } + } + CommandLineSwitchCase(arg, "encryption-methods") + { + if (arg->Flags & COMMAND_LINE_VALUE_PRESENT) + { + UINT32 i; + char** p; + size_t count = 0; + p = CommandLineParseCommaSeparatedValues(arg->Value, &count); + + for (i = 0; i < count; i++) + { + if (!strcmp(p[i], "40")) + settings->EncryptionMethods |= ENCRYPTION_METHOD_40BIT; + else if (!strcmp(p[i], "56")) + settings->EncryptionMethods |= ENCRYPTION_METHOD_56BIT; + else if (!strcmp(p[i], "128")) + settings->EncryptionMethods |= ENCRYPTION_METHOD_128BIT; + else if (!strcmp(p[i], "FIPS")) + settings->EncryptionMethods |= ENCRYPTION_METHOD_FIPS; + else + WLog_ERR(TAG, "unknown encryption method '%s'", p[i]); + } + + free(p); + } + } + CommandLineSwitchCase(arg, "from-stdin") + { + settings->CredentialsFromStdin = TRUE; + + if (arg->Flags & COMMAND_LINE_VALUE_PRESENT) + { + if (!arg->Value) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + promptForPassword = (_strnicmp(arg->Value, "force", 6) == 0); + + if (!promptForPassword) + return COMMAND_LINE_ERROR; + } + } + CommandLineSwitchCase(arg, "log-level") + { + wLog* root = WLog_GetRoot(); + + if (!WLog_SetStringLogLevel(root, arg->Value)) + return COMMAND_LINE_ERROR; + } + CommandLineSwitchCase(arg, "log-filters") + { + if (!WLog_AddStringLogFilters(arg->Value)) + return COMMAND_LINE_ERROR; + } + CommandLineSwitchCase(arg, "sec-rdp") + { + settings->RdpSecurity = enable; + } + CommandLineSwitchCase(arg, "sec-tls") + { + settings->TlsSecurity = enable; + } + CommandLineSwitchCase(arg, "sec-nla") + { + settings->NlaSecurity = enable; + } + CommandLineSwitchCase(arg, "sec-ext") + { + settings->ExtSecurity = enable; + } + CommandLineSwitchCase(arg, "tls-ciphers") + { + if (!arg->Value) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + free(settings->AllowedTlsCiphers); + + if (strcmp(arg->Value, "netmon") == 0) + { + if (!(settings->AllowedTlsCiphers = _strdup("ALL:!ECDH"))) + return COMMAND_LINE_ERROR_MEMORY; + } + else if (strcmp(arg->Value, "ma") == 0) + { + if (!(settings->AllowedTlsCiphers = _strdup("AES128-SHA"))) + return COMMAND_LINE_ERROR_MEMORY; + } + else + { + if (!(settings->AllowedTlsCiphers = _strdup(arg->Value))) + return COMMAND_LINE_ERROR_MEMORY; + } + } + CommandLineSwitchCase(arg, "tls-seclevel") + { + LONGLONG val; + + if (!value_to_int(arg->Value, &val, 0, 5)) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + + settings->TlsSecLevel = (UINT32)val; + } + CommandLineSwitchCase(arg, "enforce-tlsv1_2") + { + if (!(freerdp_settings_set_uint16(settings, FreeRDP_TLSMinVersion, TLS1_2_VERSION) && + freerdp_settings_set_uint16(settings, FreeRDP_TLSMaxVersion, TLS1_2_VERSION))) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + } + CommandLineSwitchCase(arg, "cert") + { + int rc = 0; + char** p; + size_t count, x; + p = CommandLineParseCommaSeparatedValues(arg->Value, &count); + for (x = 0; (x < count) && (rc == 0); x++) + { + const char deny[] = "deny"; + const char ignore[] = "ignore"; + const char tofu[] = "tofu"; + const char name[5] = "name:"; + const char fingerprints[12] = "fingerprint:"; + + const char* cur = p[x]; + if (_strnicmp(deny, cur, sizeof(deny)) == 0) + settings->AutoDenyCertificate = TRUE; + else if (_strnicmp(ignore, cur, sizeof(ignore)) == 0) + settings->IgnoreCertificate = TRUE; + else if (_strnicmp(tofu, cur, 4) == 0) + settings->AutoAcceptCertificate = TRUE; + else if (_strnicmp(name, cur, sizeof(name)) == 0) + { + const char* val = &cur[sizeof(name)]; + if (!copy_value(val, &settings->CertificateName)) + rc = COMMAND_LINE_ERROR_MEMORY; + } + else if (_strnicmp(fingerprints, cur, sizeof(fingerprints)) == 0) + { + const char* val = &cur[sizeof(fingerprints)]; + if (!append_value(val, &settings->CertificateAcceptedFingerprints)) + rc = COMMAND_LINE_ERROR_MEMORY; + } + else + rc = COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + } + free(p); + + if (rc) + return rc; + } +#if !defined(DEFINE_NO_DEPRECATED) + CommandLineSwitchCase(arg, "cert-name") + { + if (!copy_value(arg->Value, &settings->CertificateName)) + return COMMAND_LINE_ERROR_MEMORY; + } + CommandLineSwitchCase(arg, "cert-ignore") + { + settings->IgnoreCertificate = enable; + } + CommandLineSwitchCase(arg, "cert-tofu") + { + settings->AutoAcceptCertificate = enable; + } + CommandLineSwitchCase(arg, "cert-deny") + { + settings->AutoDenyCertificate = enable; + } +#endif + CommandLineSwitchCase(arg, "authentication") + { + settings->Authentication = enable; + } + CommandLineSwitchCase(arg, "encryption") + { + settings->UseRdpSecurityLayer = !enable; + } + CommandLineSwitchCase(arg, "grab-keyboard") + { + settings->GrabKeyboard = enable; + } + CommandLineSwitchCase(arg, "unmap-buttons") + { + settings->UnmapButtons = enable; + } + CommandLineSwitchCase(arg, "toggle-fullscreen") + { + settings->ToggleFullscreen = enable; + } + CommandLineSwitchCase(arg, "floatbar") + { + /* Defaults are enabled, visible, sticky, fullscreen */ + settings->Floatbar = 0x0017; + + if (arg->Value) + { + char* start = arg->Value; + + do + { + char* cur = start; + start = strchr(start, ','); + + if (start) + { + *start = '\0'; + start = start + 1; + } + + /* sticky:[on|off] */ + if (_strnicmp(cur, "sticky:", 7) == 0) + { + const char* val = cur + 7; + settings->Floatbar &= ~0x02u; + + if (_strnicmp(val, "on", 3) == 0) + settings->Floatbar |= 0x02u; + else if (_strnicmp(val, "off", 4) == 0) + settings->Floatbar &= ~0x02u; + else + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + } + /* default:[visible|hidden] */ + else if (_strnicmp(cur, "default:", 8) == 0) + { + const char* val = cur + 8; + settings->Floatbar &= ~0x04u; + + if (_strnicmp(val, "visible", 8) == 0) + settings->Floatbar |= 0x04u; + else if (_strnicmp(val, "hidden", 7) == 0) + settings->Floatbar &= ~0x04u; + else + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + } + /* show:[always|fullscreen|window] */ + else if (_strnicmp(cur, "show:", 5) == 0) + { + const char* val = cur + 5; + settings->Floatbar &= ~0x30u; + + if (_strnicmp(val, "always", 7) == 0) + settings->Floatbar |= 0x30u; + else if (_strnicmp(val, "fullscreen", 11) == 0) + settings->Floatbar |= 0x10u; + else if (_strnicmp(val, "window", 7) == 0) + settings->Floatbar |= 0x20u; + else + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + } + else + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + } while (start); + } + } + CommandLineSwitchCase(arg, "mouse-motion") + { + settings->MouseMotion = enable; + } + CommandLineSwitchCase(arg, "parent-window") + { + ULONGLONG val; + + if (!value_to_uint(arg->Value, &val, 0, UINT64_MAX)) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + + settings->ParentWindowId = (UINT64)val; + } + CommandLineSwitchCase(arg, "client-build-number") + { + ULONGLONG val; + + if (!value_to_uint(arg->Value, &val, 0, UINT32_MAX)) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + if (!freerdp_settings_set_uint32(settings, FreeRDP_ClientBuild, (UINT32)val)) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + } + CommandLineSwitchCase(arg, "bitmap-cache") + { + settings->BitmapCacheEnabled = enable; + } + CommandLineSwitchCase(arg, "offscreen-cache") + { + settings->OffscreenSupportLevel = (UINT32)enable; + } + CommandLineSwitchCase(arg, "glyph-cache") + { + settings->GlyphSupportLevel = arg->Value ? GLYPH_SUPPORT_FULL : GLYPH_SUPPORT_NONE; + } + CommandLineSwitchCase(arg, "codec-cache") + { + if (!arg->Value) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + settings->BitmapCacheV3Enabled = TRUE; + + if (strcmp(arg->Value, "rfx") == 0) + { + settings->RemoteFxCodec = TRUE; + } + else if (strcmp(arg->Value, "nsc") == 0) + { + settings->NSCodec = TRUE; + } + +#if defined(WITH_JPEG) + else if (strcmp(arg->Value, "jpeg") == 0) + { + settings->JpegCodec = TRUE; + + if (settings->JpegQuality == 0) + settings->JpegQuality = 75; + } + +#endif + } + CommandLineSwitchCase(arg, "fast-path") + { + settings->FastPathInput = enable; + settings->FastPathOutput = enable; + } + CommandLineSwitchCase(arg, "max-fast-path-size") + { + LONGLONG val; + + if (!value_to_int(arg->Value, &val, 0, UINT32_MAX)) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + + settings->MultifragMaxRequestSize = (UINT32)val; + } + CommandLineSwitchCase(arg, "max-loop-time") + { + LONGLONG val; + + if (!value_to_int(arg->Value, &val, -1, UINT32_MAX)) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + + if (val < 0) + settings->MaxTimeInCheckLoop = + 10 * 60 * 60 * 1000; /* 10 hours can be considered as infinite */ + else + settings->MaxTimeInCheckLoop = (UINT32)val; + } + CommandLineSwitchCase(arg, "auto-request-control") + { + if (!freerdp_settings_set_bool(settings, FreeRDP_RemoteAssistanceRequestControl, + enable)) + return COMMAND_LINE_ERROR; + } + CommandLineSwitchCase(arg, "async-input") + { + settings->AsyncInput = enable; + } + CommandLineSwitchCase(arg, "async-update") + { + settings->AsyncUpdate = enable; + } + CommandLineSwitchCase(arg, "async-channels") + { + settings->AsyncChannels = enable; + } + CommandLineSwitchCase(arg, "wm-class") + { + if (!copy_value(arg->Value, &settings->WmClass)) + return COMMAND_LINE_ERROR_MEMORY; + } + CommandLineSwitchCase(arg, "play-rfx") + { + if (!copy_value(arg->Value, &settings->PlayRemoteFxFile)) + return COMMAND_LINE_ERROR_MEMORY; + + settings->PlayRemoteFx = TRUE; + } + CommandLineSwitchCase(arg, "auth-only") + { + settings->AuthenticationOnly = enable; + } + CommandLineSwitchCase(arg, "auto-reconnect") + { + settings->AutoReconnectionEnabled = enable; + } + CommandLineSwitchCase(arg, "auto-reconnect-max-retries") + { + LONGLONG val; + + if (!value_to_int(arg->Value, &val, 0, 1000)) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + + settings->AutoReconnectMaxRetries = (UINT32)val; + } + CommandLineSwitchCase(arg, "reconnect-cookie") + { + BYTE* base64 = NULL; + int length; + if (!arg->Value) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + + crypto_base64_decode((const char*)(arg->Value), (int)strlen(arg->Value), &base64, + &length); + + if ((base64 != NULL) && (length == sizeof(ARC_SC_PRIVATE_PACKET))) + { + memcpy(settings->ServerAutoReconnectCookie, base64, (size_t)length); + } + else + { + WLog_ERR(TAG, "reconnect-cookie: invalid base64 '%s'", arg->Value); + } + + free(base64); + } + CommandLineSwitchCase(arg, "print-reconnect-cookie") + { + settings->PrintReconnectCookie = enable; + } + CommandLineSwitchCase(arg, "pwidth") + { + LONGLONG val; + + if (!value_to_int(arg->Value, &val, 0, UINT32_MAX)) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + + settings->DesktopPhysicalWidth = (UINT32)val; + } + CommandLineSwitchCase(arg, "pheight") + { + LONGLONG val; + + if (!value_to_int(arg->Value, &val, 0, UINT32_MAX)) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + + settings->DesktopPhysicalHeight = (UINT32)val; + } + CommandLineSwitchCase(arg, "orientation") + { + LONGLONG val; + + if (!value_to_int(arg->Value, &val, 0, UINT16_MAX)) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + + settings->DesktopOrientation = (UINT16)val; + } + CommandLineSwitchCase(arg, "old-license") + { + settings->OldLicenseBehaviour = TRUE; + } + CommandLineSwitchCase(arg, "scale") + { + LONGLONG val; + + if (!value_to_int(arg->Value, &val, 100, 180)) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + + switch (val) + { + case 100: + case 140: + case 180: + settings->DesktopScaleFactor = (UINT32)val; + settings->DeviceScaleFactor = (UINT32)val; + break; + + default: + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + } + } + CommandLineSwitchCase(arg, "scale-desktop") + { + LONGLONG val; + + if (!value_to_int(arg->Value, &val, 100, 500)) + return FALSE; + + settings->DesktopScaleFactor = (UINT32)val; + } + CommandLineSwitchCase(arg, "scale-device") + { + LONGLONG val; + + if (!value_to_int(arg->Value, &val, 100, 180)) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + + switch (val) + { + case 100: + case 140: + case 180: + settings->DeviceScaleFactor = (UINT32)val; + break; + + default: + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + } + } + CommandLineSwitchCase(arg, "action-script") + { + if (!copy_value(arg->Value, &settings->ActionScript)) + return COMMAND_LINE_ERROR_MEMORY; + } + CommandLineSwitchCase(arg, "rdp2tcp") + { + free(settings->RDP2TCPArgs); + + if (!(settings->RDP2TCPArgs = _strdup(arg->Value))) + return COMMAND_LINE_ERROR_MEMORY; + } + CommandLineSwitchCase(arg, "fipsmode") + { + settings->FIPSMode = enable; + } + CommandLineSwitchCase(arg, "smartcard-logon") + { + if (!settings->SmartcardLogon) + activate_smartcard_logon_rdp(settings); + } + + CommandLineSwitchCase(arg, "tune") + { + size_t x, count; + char** p = CommandLineParseCommaSeparatedValuesEx("tune", arg->Value, &count); + if (!p) + return COMMAND_LINE_ERROR; + for (x = 1; x < count; x++) + { + char* cur = p[x]; + char* sep = strchr(cur, ':'); + if (!sep) + { + free(p); + return COMMAND_LINE_ERROR; + } + *sep++ = '\0'; + if (!freerdp_settings_set_value_for_name(settings, cur, sep)) + { + free(p); + return COMMAND_LINE_ERROR; + } + } + + free(p); + } + CommandLineSwitchCase(arg, "tune-list") + { + size_t x; + SSIZE_T type = 0; + + printf("%s\t%50s\t%s\t%s", "", "", "", "\n"); + for (x = 0; x < FreeRDP_Settings_StableAPI_MAX; x++) + { + const char* name = freerdp_settings_get_name_for_key(x); + type = freerdp_settings_get_type_for_key(x); + + switch (type) + { + case RDP_SETTINGS_TYPE_BOOL: + printf("%" PRIuz "\t%50s\tBOOL\t%s\n", x, name, + freerdp_settings_get_bool(settings, x) ? "TRUE" : "FALSE"); + break; + case RDP_SETTINGS_TYPE_UINT16: + printf("%" PRIuz "\t%50s\tUINT16\t%" PRIu16 "\n", x, name, + freerdp_settings_get_uint16(settings, x)); + break; + case RDP_SETTINGS_TYPE_INT16: + printf("%" PRIuz "\t%50s\tINT16\t%" PRId16 "\n", x, name, + freerdp_settings_get_int16(settings, x)); + break; + case RDP_SETTINGS_TYPE_UINT32: + printf("%" PRIuz "\t%50s\tUINT32\t%" PRIu32 "\n", x, name, + freerdp_settings_get_uint32(settings, x)); + break; + case RDP_SETTINGS_TYPE_INT32: + printf("%" PRIuz "\t%50s\tINT32\t%" PRId32 "\n", x, name, + freerdp_settings_get_int32(settings, x)); + break; + case RDP_SETTINGS_TYPE_UINT64: + printf("%" PRIuz "\t%50s\tUINT64\t%" PRIu64 "\n", x, name, + freerdp_settings_get_uint64(settings, x)); + break; + case RDP_SETTINGS_TYPE_INT64: + printf("%" PRIuz "\t%50s\tINT64\t%" PRId64 "\n", x, name, + freerdp_settings_get_int64(settings, x)); + break; + case RDP_SETTINGS_TYPE_STRING: + printf("%" PRIuz "\t%50s\tSTRING\t%s" + "\n", + x, name, freerdp_settings_get_string(settings, x)); + break; + case RDP_SETTINGS_TYPE_POINTER: + printf("%" PRIuz "\t%50s\tPOINTER\t%p" + "\n", + x, name, freerdp_settings_get_pointer(settings, x)); + break; + default: + break; + } + } + return COMMAND_LINE_STATUS_PRINT; + } + CommandLineSwitchDefault(arg) + { + } + CommandLineSwitchEnd(arg) + } while ((arg = CommandLineFindNextArgumentA(arg)) != NULL); + + if (user) + { + free(settings->Username); + + if (!settings->Domain && user) + { + BOOL ret; + free(settings->Domain); + ret = freerdp_parse_username(user, &settings->Username, &settings->Domain); + free(user); + + if (!ret) + return COMMAND_LINE_ERROR; + } + else + settings->Username = user; + } + + if (gwUser) + { + free(settings->GatewayUsername); + + if (!settings->GatewayDomain && gwUser) + { + BOOL ret; + free(settings->GatewayDomain); + ret = freerdp_parse_username(gwUser, &settings->GatewayUsername, + &settings->GatewayDomain); + free(gwUser); + + if (!ret) + return COMMAND_LINE_ERROR; + } + else + settings->GatewayUsername = gwUser; + } + + if (promptForPassword) + { + const size_t size = 512; + + if (!settings->Password) + { + settings->Password = calloc(size, sizeof(char)); + + if (!settings->Password) + return COMMAND_LINE_ERROR; + + if (!freerdp_passphrase_read("Password: ", settings->Password, size, 1)) + return COMMAND_LINE_ERROR; + } + + if (settings->GatewayEnabled && !settings->GatewayUseSameCredentials) + { + if (!settings->GatewayPassword) + { + settings->GatewayPassword = calloc(size, sizeof(char)); + + if (!settings->GatewayPassword) + return COMMAND_LINE_ERROR; + + if (!freerdp_passphrase_read("Gateway Password: ", settings->GatewayPassword, size, + 1)) + return COMMAND_LINE_ERROR; + } + } + } + + freerdp_performance_flags_make(settings); + + if (settings->RemoteFxCodec || settings->NSCodec || settings->SupportGraphicsPipeline) + { + settings->FastPathOutput = TRUE; + settings->FrameMarkerCommandEnabled = TRUE; + settings->ColorDepth = 32; + } + + arg = CommandLineFindArgumentA(largs, "port"); + + if (arg->Flags & COMMAND_LINE_ARGUMENT_PRESENT) + { + LONGLONG val; + + if (!value_to_int(arg->Value, &val, 1, UINT16_MAX)) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + + settings->ServerPort = (UINT32)val; + } + + arg = CommandLineFindArgumentA(largs, "p"); + + if (arg->Flags & COMMAND_LINE_ARGUMENT_PRESENT) + { + FillMemory(arg->Value, strlen(arg->Value), '*'); + } + + arg = CommandLineFindArgumentA(largs, "gp"); + + if (arg->Flags & COMMAND_LINE_ARGUMENT_PRESENT) + { + FillMemory(arg->Value, strlen(arg->Value), '*'); + } + + return status; +} + +static BOOL freerdp_client_load_static_channel_addin(rdpChannels* channels, rdpSettings* settings, + char* name, void* data) +{ + PVIRTUALCHANNELENTRY entry = NULL; + PVIRTUALCHANNELENTRYEX entryEx = NULL; + entryEx = (PVIRTUALCHANNELENTRYEX)(void*)freerdp_load_channel_addin_entry( + name, NULL, NULL, FREERDP_ADDIN_CHANNEL_STATIC | FREERDP_ADDIN_CHANNEL_ENTRYEX); + + if (!entryEx) + entry = freerdp_load_channel_addin_entry(name, NULL, NULL, FREERDP_ADDIN_CHANNEL_STATIC); + + if (entryEx) + { + if (freerdp_channels_client_load_ex(channels, settings, entryEx, data) == 0) + { + WLog_DBG(TAG, "loading channelEx %s", name); + return TRUE; + } + } + else if (entry) + { + if (freerdp_channels_client_load(channels, settings, entry, data) == 0) + { + WLog_DBG(TAG, "loading channel %s", name); + return TRUE; + } + } + + return FALSE; +} + +BOOL freerdp_client_load_addins(rdpChannels* channels, rdpSettings* settings) +{ + UINT32 index; + ADDIN_ARGV* args; + + if (settings->AudioPlayback) + { + char* p[] = { "rdpsnd" }; + + if (!freerdp_client_add_static_channel(settings, ARRAYSIZE(p), p)) + return FALSE; + } + + /* for audio playback also load the dynamic sound channel */ + if (settings->AudioPlayback) + { + char* p[] = { "rdpsnd" }; + + if (!freerdp_client_add_dynamic_channel(settings, ARRAYSIZE(p), p)) + return FALSE; + } + + if (settings->AudioCapture) + { + char* p[] = { "audin" }; + + if (!freerdp_client_add_dynamic_channel(settings, ARRAYSIZE(p), p)) + return FALSE; + } + + if ((freerdp_static_channel_collection_find(settings, "rdpsnd")) || + (freerdp_dynamic_channel_collection_find(settings, "rdpsnd")) +#if defined(CHANNEL_TSMF_CLIENT) + || (freerdp_dynamic_channel_collection_find(settings, "tsmf")) +#endif + ) + { + settings->DeviceRedirection = TRUE; /* rdpsnd requires rdpdr to be registered */ + settings->AudioPlayback = TRUE; /* Both rdpsnd and tsmf require this flag to be set */ + } + + if (freerdp_dynamic_channel_collection_find(settings, "audin")) + { + settings->AudioCapture = TRUE; + } + + if (settings->NetworkAutoDetect || settings->SupportHeartbeatPdu || + settings->SupportMultitransport) + { + settings->DeviceRedirection = TRUE; /* these RDP8 features require rdpdr to be registered */ + } + + if (settings->DrivesToRedirect && (strlen(settings->DrivesToRedirect) != 0)) + { + /* + * Drives to redirect: + * + * Very similar to DevicesToRedirect, but can contain a + * comma-separated list of drive letters to redirect. + */ + char* value; + char* tok; + char* context = NULL; + + value = _strdup(settings->DrivesToRedirect); + if (!value) + return FALSE; + + tok = strtok_s(value, ";", &context); + if (!tok) + { + free(value); + return FALSE; + } + + while (tok) + { + /* Syntax: Comma seperated list of the following entries: + * '*' ... Redirect all drives, including hotplug + * 'DynamicDrives' ... hotplug + * '%' ... user home directory + *